🍇

13.1 Objectクラスとオブジェクトの様々な特性(toString()、同一性と等価性、イミュータブル)~Java Basic編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
  • 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
  • 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
  • 今後、フリーランスエンジニアとしてのキャリアを検討している方
  • Chat GPT」のエンジニアリングへの活用に興味のある方
  • Oracle認定Javaプログラマ」の資格取得を目指している方
  • IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

13.1 Objectクラスとオブジェクトの様々な特性

チャプターの概要

このチャプターでは、クラスの階層構造において、最上位の位置するObjectクラスの様々な特性について学びます。

13.1.1 Objectクラスの基本

Objectクラスとは

java.lang.Objectクラスは、クラスの階層構造において最上位に位置するクラスです。Javaのあらゆるクラスは、暗黙的にこのクラスを継承します。
Objectクラスの代表的なAPIを、以下に示します。

API(メソッド) 説明
String toString() 自身の属性を文字列として返す
Class<?> getClass() 自身のClassオブジェクトを返す(『Java Advanced編』参照)
boolean equals(Object) 指定されたオブジェクトとの等価性を判定する
int hashCode() 自身のハッシュ値を返す
Object clone() 自身のコピーを返す

Objectクラスにはこれらのメソッドが実装されていますが、Objectクラスは階層構造の最上位に位置するため、これらのメソッドはすべてのクラスに継承されています。
開発者がクラスを作成したとき、これらのメソッドのうち、toString()メソッド、equals()メソッド、hashCode()メソッド、clone()メソッドの4つについては、必要に応じてオーバーライドします。これら4つのメソッドがどういう意味を持ち、どういう場合にオーバーライドが必要なのかという点について、次項から順に説明します。

toString()メソッド

toString()メソッドは、インスタンスが保持する属性の情報を文字列として返すためのメソッドです。Objectクラスの実装では、当該クラスのFQCNとハッシュ値が返される仕様になっているため、開発者がクラスを作成する場合、基本的にはこのメソッドはオーバーライドした方が良いでしょう。このメソッドを適切に実装すると、インスタンスの内容をログやコンソールに出力することができるため、プログラムの動作を確認したり、デバッグしたりすることが容易になります。
それではここで、「人物」を表すPersonクラスを題材にtoString()メソッドの実装を見ていきましょう。
まずPersonクラスのコードを、次に示します。

pro.kensait.java.basic.lsn_13_1_1.Person
public class Person {
    // フィールド
    private String name; // 名前
    private int age; // 年齢
    private String address;// 住所
    // コンストラクタ
    ........
    // アクセサメソッド
    ........
}

このクラスには、name(String型)、age(int型)、address(String型)という3つのフィールドがあり、コンストラクタとアクセサメソッドも定義されているものとします。
このクラスに対して、以下のようなtoString()メソッドを追加します。

snippet (pro.kensait.java.basic.lsn_13_1_1.Person)
@Override
public String toString() {
    return "Person [name=" + name + ", age=" + age + ", address=" + address + "]";
}

では、このPersonクラスのインスタンスを生成し、toString()メソッドを呼び出してみましょう。
以下のコードを見てください。

snippet (pro.kensait.java.basic.lsn_13_1_1.Main)
Person person = new Person("Alice", 25, "中央区1-1-1");
System.out.println(person);

このようにSystem.out.println()メソッドにPersonインスタンスを指定すると、toString()メソッドが自動的に呼び出されます。
よってこのコードを実行すると、コンソールに以下のように表示されます。

Person [name=Alice, age=25, address=中央区1-1-1]

13.1.2 同一性と等価性

同一性と等価性の概念

一般的に2つの「モノ」に対して「同じである」という表現をした場合、2つの意味が考えられます。
1つ目は「モノ」の実体が同一である、という意味で、これを「同一性」と呼びます。例えば「AliceとBobは同じ会社に勤めている」と言った場合、Aliceの勤める会社とBobが勤める会社は、両者の実体が同一です。
もう1つは「モノ」の特徴や性質が同じである、という意味で、これを「等価性」と呼びます。例えば「AliceとBobは同じスマホを持っている」と言った場合、AliceとBobが同一のスマホを共有しているわけではなく、「特徴が同じ種類のスマホを持っている」と解釈するのが自然でしょう。

【図13-1-1】同一性と等価性
image.png

Javaプログラムの中でも、クラス型変数同士を比較する場合、同一性と等価性を意識的に使い分けなくてはなりません。
例えば「AliceとBobは同じスマホを持っている」わけですが、仮にスマホをクラスとして設計するのであれば、何をもってスマホ同士を「同じ」と見なすのかを決める必要があります。すなわち、機種が一緒であれば「同じ」なのか、機種と色の両方が同じのときにはじめて「同じ」と見なすのかによって、「同じ」の持つ意味も変わってきます。

同一性の判定

同一性および等価性の判定方法は、プリミティブ型変数と参照型変数で考え方が異なります。まずプリミティブ型変数の場合は、既出のとおり==によって同一性を判定します。
プリミティブ型では、同一性と等価性は同義と見なされます。一方で参照型変数の場合は、同一性と等価性の判定方法が異なります。
まず同一性を==によって判定する点は、プリミティブ型と同様です。ただし参照型における同一性は、値が同じという意味ではなく、「同一のインスタンスであること」を意味します。
以下のコードを見てください。

snippet (pro.kensait.java.basic.lsn_13_1_2.Main)
Foo foo1 = new Foo();
Foo foo2 = new Foo();
boolean b1 = foo1 == foo2; //【1】falseになる
Foo foo3 = foo1;  //【2】
boolean b2 = foo1 == foo3; //【3】trueになる

変数foo1とfoo2は、それぞれ別のFooインスタンスです。従ってfoo1とfoo2の同一性を==によって判定すると、その結果はfalseになります【1】。また変数foo3にはfoo1をそのまま代入しています【2】が、このようにすると、変数foo3はfoo1と同一のインスタンスを参照することになります。
従ってfoo1とfoo3の同一性を==によって判定すると、その結果はtrueになります【3】。

等価性の判定

参照型では、等値性を判定するためにequals()メソッドを用います。equals()メソッドは、Objectクラスに定義されたメソッドのため、すべてのクラスが暗黙的に保持しています。ただしObjectクラスにおけるequals()メソッドは、同一性を判定する実装になっています。従って開発者が何らかのクラスを作成した場合は、基本的にはequals()メソッドをオーバーライドし、そのクラス固有の等価性判定ができるようにする必要があります。
どのように等価性を判定するかという点は、開発者の戦略に委ねられます。
ここで「人物」を例に、等価性がどのようにあるべきかについて、考えてみましょう。
例えば「名前はAlice、年齢は25歳、住所は中央区1-1-1」という人物は、「名前はAlice、年齢は25歳、住所は杉並区1-1-1」という人物と、等価でしょうか。これは考え方次第ですが、住所が異なることから同姓同名の別人物と見なすのであれば、両者は等価ではないことになります。
また「IDが1、名前はAlice、年齢は25歳」という人物と、「IDが1、名前はAlice、年齢は26歳」という人物を比較する場合はどうでしょうか。IDが何らかの一意性が保証された仕組みによって振り出されたものだとしたら、たとえ年齢が異なっていても、IDの一致を持って、両者は等価と見なすのが妥当でしょう。
次に「名前はAlice、年齢は25歳、最終更新日が9月1日」というデータと、「名前はAlice、年齢は25歳、最終更新日は10月1日」というデータを比較する場合はどうでしょうか。最終更新日という属性が、何らかのシステムの違いによって生じる差異であることが確認できれば、この両者は等価と見なして問題ないでしょう。
つまり等価性の戦略は「どの属性を選択するか」によって決まるのです。

equals()メソッドの実装

equals()メソッドは、既出のとおり「自分.equals(判定相手)」という形式で呼び出されます。このequals()メソッドの中身を実装するためには、前項で説明した等価性の戦略に則り、まずは判定に必要な属性を選択します。
その上で、基本的に以下のような手順で実装します。

  1. 自分と判定相手が、同一のインスタンスの場合は、trueを返す。
  2. 判定相手がnull値の場合は、falseを返す。
  3. 自分と判定相手が、異なるクラス型の場合は、falseを返す。
  4. 自分と判定相手のすべての「選択された属性」が一致していた場合に、trueを返す。このとき属性の中に、片方がnull値であり、もう片方がnull値以外となる属性が1つでもあったら、不一致と見なす。

それではここで、既出のPersonクラスを題材にequals()メソッドの実装を見ていきましょう。このクラスには、name(String型)、age(int型)、address(String型)という3つのフィールドがあります。そして3つの属性がいずれも一致している場合に、trueを返す戦略を取るものとします。
以下のコードを見てください。

snippet_1 (pro.kensait.java.basic.lsn_13_1_2.Person)
@Override
public boolean equals(Object obj) {
    if (this == obj) return true; //【1】
    if (obj == null) return false; //【2】
    if (getClass() != obj.getClass()) return false; //【3】
    Person other = (Person) obj;
    if (name == null) { //【4】
        if (other.name != null) {
            return false;
        }
    } else if (! name.equals(other.name)) {
        return false;
    }
    if (age != other.age) { //【5】
        return false;
    }
    if (address == null) { //【6】
        // 【4】と同じように判定する
        ........
    }
    return true;
}

まず自分と判定相手が、同一のインスタンスの場合は、trueを返します【1】。次に判定相手がnull値の場合は、falseを返します【2】。次に自分と判定相手が、異なるクラス型の場合は、falseを返します【3】。そして属性の比較です。このクラスは3つの属性がいずれも一致している場合にtrueを返す戦略なので、それに従って一致の判定を行います【4、5、6】。
なお本レッスンではequals()メソッドの実装方法について説明しましたが、Eclipseなどの統合開発環境を使って開発する場合は、ツールによって自動生成させるケースが一般的です。Eclipseによるequals()メソッドの自動生成については、レッスン14.1.1で取り上げます。

hashCode()メソッドの実装

hashCode()メソッドとは、インスタンス自身のハッシュ値を返すためのメソッドです。
元来、ハッシュ値とは、ハッシュ関数によって生成される固定長の符号で、データの完全性(改ざんされていないこと)を検証するために使用します。同じデータからは必ず同じハッシュ値が生成されます。また異なるデータからは基本的に異なるハッシュ値が生成されますが、仮にハッシュ値が衝突することがあっても問題はありません。つまりハッシュ値は「ハッシュ値が異なる場合は、確実に元データは等価ではない」ことを保証します。
Javaでは、インスタンスの等価性はequals()メソッドで判定するため、hashCode()メソッドが返すハッシュ値(int型の値)は、equals()メソッドと平仄を合わせる必要があります。つまりequals()メソッドがtrueになる2つのインスタンスは、hashCode()メソッドを呼び出すと、同じ値が返されることを保証しなければなりません。
それでは、前項のPersonクラスにおけるhashCode()メソッドを見ていきましょう。以下のコードを見てください。

snippet_2 (pro.kensait.java.basic.lsn_13_1_2.Person)
@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    result = prime * result + age;
    result = prime * result + ((address == null) ? 0 : address.hashCode());
    return result;
}

このコードの詳細はここでは割愛しますが、name、age、address、すべての値をパラメータにして、ハッシュ値を計算しています。
なお本レッスンではhashCode()メソッドの実装方法について説明しましたが、Eclipseなどの統合開発環境を使って開発する場合は、ツールによって自動生成させるケースが一般的です。EclipseによるhashCode()メソッドの自動生成については、レッスン14.1.1で取り上げます。

13.1.3 イミュータブルオブジェクトと参照の値渡し

参照型変数への代入

チャプター5.2で取り上げたとおり、参照型変数(配列型、クラス型、インタフェース型の変数)とは、データそのものではなく、実データのメモリ上の配置場所(これをアドレスと呼ぶ)への「参照」を表す変数です。
ここではクラス型を取り上げて、参照型変数の挙動を復習します。変数がデータを表しているのか実データへの「参照」を表しているのかという違いは、通常はあまり意識する必要はありませんが、変数を別の変数に代入すると両者の挙動の違いが表れます。
以下のコードを見てください。

snippet_2 (Main_5_2)
Person p1 = new Person("Alice", 25, "中央区1-1-1");
Person p2 = p1; // 参照型変数から参照型変数に代入する
p2.setAddress("中央区2-2-2"); // 代入先の値を書き換える
System.out.println(p1.getAddress()); // 代入元はどうなる?

このコードのように参照型変数を別の変数に代入すると、変数に格納されている値(要素)が代入されるわけではなく「参照」が代入されるため、同じ実データを指す2つの変数が登場することになります。

【図13-1-2】参照型変数から参照型変数への代入
image.png

このような状態で代入先である変数p2の値を更新すると、代入元である変数p1も同じ実データを参照しているため、値が書き換わります。従ってこのコードを実行すると、"中央区2-2-2"が表示されます。
なお代入元である変数p1の方を更新しても、同様に代入先である変数p2の値が書き換わります。

メソッドへの変数の渡し方

メソッドへの変数の渡し方は、プログラミング言語の種類によってまちまちです。
Javaの場合は、まずメソッドにプリミティブ型変数を渡すと、値そのものが渡されます。これを「値渡し」と呼びます。値そのものが渡されるため、呼び出し先で変数の値を書き換えても、呼び出し元には影響しません。
一方、参照型変数は既出のとおり、実データのメモリ上の配置場所を「参照」する変数です。従ってメソッドに参照型変数を指定すると、渡されるのは実データではなく「参照」です。このとき前項で説明した「参照型変数への代入」と同じように、呼び出し先で変数の値を書き換えると、呼び出し元にも影響が発生します。Javaにおけるこのような変数の渡し方を、「参照の値渡し」と呼びます。他のプログラミング言語にはこれに近しい「参照渡し」がありますが、この両者は厳密には挙動が異なるため、区別した方がよいでしょう。
それでは、「参照の値渡し」の挙動をコードで確認してみましょう。

snippet (pro.kensait.java.basic.lsn_13_1_3.mutable.Main_2)
void process() {
    Person person = new Person("Alice", 25, "中央区1-1-1");
    updateAddress(person); //【1】
    System.out.println(person); //【2】住所はどうなる?
}
void updateAddress(Person person) {
    person.setAddress("中央区2-2-2"); //【3】
}

process()メソッドでは、Personインスタンス(参照型変数)を生成し、それをupdateAddress()メソッドに渡しています【1】。そしてupdateAddress()メソッドでは、受け取った変数の住所を書き換えます【3】が、その結果を受けて、呼び出し元の変数personまで書き換わります。従ってこのprocess()メソッドを実行すると、最終的にコンソールには"中央区2-2-2"が表示されます【2】。
このように参照型変数を他のメソッドに渡し、その後呼び出し元で改めて値を取得する場合は、十分な注意が必要です。もしこのような処理が必要なのであれば、メソッドの戻り値として当該のインスタンスを受け取るようにした方が、不具合は発生しにくいでしょう。

イミュータブルオブジェクト

Javaでは参照型変数の値が書き換わることにより、思わぬ不具合が発生するケースがあります。その1つが前項で取り上げたような「参照の値渡し」による副作用です。
また本コースでは取り上げませんが、マルチスレッド環境においてスレッド間で1つの参照型変数を共有する場合、複数スレッドが同時に値を書き換えようとすると、状態が不正になる可能性があります(『Java Advanced編』参照)。

このような事態を回避するために、クラスをイミュータブルにする、という戦略があります。イミュータブルとは「不変」という意味で、一度インスタンスを生成したらその後は値の変更ができないことを表します。このように値の変更ができないインスタンスを、イミュータブルオブジェクトと呼びます。逆にセッターなどによって後から値を書き換えられるインスタンスを、ミュータブルオブジェクトと呼びます。
それでは、既出のPersonクラスをイミュータブルにしてみましょう。以下のコードを見てください。

pro.kensait.java.basic.lsn_13_1_3.immutable.Person
public class Person {
    // フィールドはすべてfinal
    final private String name;
    final private int age;
    final private String address;
    // コンストラクタで一度に初期化
    public Person(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
    // アクセサメソッドは必然的にゲッターのみ
    public String getName() {
         return name;
    }
    public int getAge() {
         return age;
    }
    public String getAddress() {
        return address;
    }
}

フィールドはすべてfinalにし、一度のコンストラクタ呼び出しで、すべてのフィールドを初期化できるようにします。またフィールドがfinalなので、必然的にアクセサメソッドはゲッターのみになります。このようにすることで、クラスをイミュータブルにすることができます。
なおJava SEが提供するクラスの中にも、イミュータブルなものは数多く存在します。その代表がStringクラス、および、Integerクラスなどのラッパークラス(チャプター17.1参照)や、BigDecimalクラス(チャプター17.2参照)などです。

13.1.4 インスタンスのコピー

インスタンスのコピー

Javaのアプリケーションでは、インスタンスのコピーを作成し、それをスナップショットとして利用するケースがあります。ここで言うコピーとは「同じクラス型であり、同じフィールド値を保持する(等価性がある)が、インスタンスとしては別物(同一性はない)」のことを意味します。
コピーを作成する方法には、手動でコピーを行う方法か、後に取り上げるclone()メソッドを利用する方法がありますが、ここではまず前者の方法について説明します。手動でコピーを行う方法とは、コピー元インスタンスから属性を取得し、それをコピー先インスタンスに設定するだけです。
例えば既出のPersonインスタンスのコピーを作成する場合は、以下のようなコードになります。

snippet
Person person = new Person("Alice", 25, "中央区1-1-1");
Person copy = new Person(person.getName(), person.getAge(), person.getAddress());

このようにインスタンスのコピーを行うためには、コピー元からゲッターでフィールド値を1つ1つ取得します。そして取得した値をコピー先のコンストラクタ(またはセッター)に渡すことで、コピーを作成します。このような方法でインスタンスをコピーすれば、コピー元またはコピー先で値を書き換えても、インスタンスとして別なので(後述するケースを除いて)相互に影響は発生しません。

【図13-1-3】インスタンスのコピー
image.png

シャローコピー

前項のような方法でインスタンスをコピーすれば、コピー元またはコピー先で値を書き換えても、インスタンスとして別なので相互に影響は発生しませんが、常にそうなるとは限りません。
そのようなケースを見ていきましょう。ここでは住所という属性(String型)を、以下のようなAddressクラスとして独立させます。

pro.kensait.java.basic.lsn_13_1_4.copy.Address
public class Address {
    private String zipCode; // 郵便番号
    private String city; // 都市
    private String addressLine; // 番地
    // コンストラクタ
    ........
    // アクセサメソッド
    ........
}

これに合わせてPersonクラスも、Addressクラスと関連を持つように修正します。

pro.kensait.java.basic.lsn_13_1_4.copy.Person
public class Person {
    private String name;
    private int age;
    private Address address; // String型からAddress型へ修正
    // コンストラクタ
    ........
    // アクセサメソッド
    ........
}

このようにクラス間に関連がある状態で、前項と同じ方法でインスタンスをコピーします。

snippet (pro.kensait.java.basic.lsn_13_1_4.copy.Main_ShallowCopy)
Address address = new Address("103-0004", "中央区", "1-1-1");
Person person = new Person("Alice", 25, address);
Person copy = new Person(person.getName(), person.getAge(), person.getAddress()); //【1】

このコードでは、コピー先インスタンスを生成するときに、コピー元の属性をゲッターで取得し、それをコンストラクタに渡しています【1】。このコードを実行すると、Personインスタンスがコピーされます。ただし【1】のperson.getAddress()によって取得されるのは、あくまでも関連先であるAddressインスタンスの参照です。つまり以下の図のように、変数personと変数copyは、同じAddressインスタンスへの参照を持つことになります。

【図13-1-4】シャローコピー
image.png

従ってコピー元またはコピー先で住所(関連先であるAddressインスタンス)を書き換えると、その内容が相互に反映されます。
このようにあるインスタンスが直接保持するフィールドをコピーはするものの、保持するフィールドが参照するインスタンスまではコピーされていないことを、シャローコピーと呼びます。

ディープコピー

シャローコピーで問題がある場合には、関連先クラスが保持するフィールドも1つ1つ手動でコピーをしなければなりません。
具体的には以下のようなコードになります。

snippet (pro.kensait.java.basic.lsn_13_1_4.copy.Main_DeepCopy)
Address a1 = new Address("103-0004", "中央区", "1-1-1");
Person person = new Person("Alice", 25, a1);
Address a2 = new Address(a1.getZipCode(), a1.getCity(), a1.getAddressLine()); //【1】
Person copy = new Person(person.getName(), person.getAge(), a2); //【2】

このコードでは、関連先のAddressクラスのインスタンスをコピーによって生成します【1】。そしてPersonクラスのコピーを作るときのコンストラクタに、コピーされたAddressインスタンス(変数a2)を渡しています【2】。このようにすれば以下の図のように、コピー元とコピー先、2つのPersonインスタンスの間で、関連先のAddressインスタンスも含めて完全なコピーができたことになります。この状態の場合、コピー元またはコピー先で住所(関連先であるAddressインスタンス)を書き換えても、相互に影響は発生しません。
このように関連先クラスのフィールドまで含めて値をコピーすることを、ディープコピーと呼びます。

【図13-1-5】ディープコピー
image.png

clone()メソッド

clone()メソッドは、インスタンス自身のコピーを返すためのメソッドです。前項までは手動でインスタンスをコピーしていましたが、clone()メソッドを利用すると、効率的にコピーを作成することができます。
ここでも前項で取り上げた、関連を持つ2つのクラス、PersonクラスとAddressクラスを題材に説明します。まずclone()メソッドによってインスタンスのコピーを作成するためには、当該のクラスにclonableインタフェースをimplementsさせる必要があります。
従ってPersonクラスとAddressクラスのclass宣言は、以下のようになります。

snippet
public class Person implements Cloneable { .... }
public class Address implements Cloneable { .... }

clonableインタフェースは「当該クラスがclone()によってコピーを作成可能であること」を表すインタフェースですが、メソッドは1つも宣言されていません。このようにメソッドを1つも持たず、「ある機能を有していることを表す」ために存在するインタフェースを「マーカーインタフェース」と呼びます。
さてそれでは既出のPersonクラスに、clone()メソッドを追加してみましょう。

snippet (pro.kensait.java.basic.lsn_13_1_4.clone.Person)
@Override
public Person clone() throws CloneNotSupportedException { //【1】
    Person copy = (Person) super.clone(); //【2】
    copy.address = this.address.clone(); //【3】
    return copy;
}

clone()メソッドは、Objectクラスで定義されたものをオーバーライドして実装します。Objectクラスにおけるclone()メソッドの可視性はprotectedですが、ここでは可視性をpublicに広げています【1】。またこのメソッドは、自身のコピーを返すためのものなので、この場合の戻り値はPerson型になります。メソッド宣言の後ろにはthrows句が記述されていますが、指定されたCloneNotSupportedExceptionは、対象クラスがClonableインタフェースをimplementsしていない場合に送出される例外クラス(チャプター19.1参照)です。
メソッドの中を見ていくと、まず親クラス、すなわちObjectクラスのclone()メソッドを呼び出し、コピーを作成しています【2】。このようにclone()メソッドを呼び出すだけでインスタンス自身のコピーを作ることができますが、実はこのコピーはシャローコピーに過ぎません。すなわち、この処理によってPersonインスタンスはコピーされますが、関連先であるAddressインスタンスは参照がコピーされるに留まります。
そこで次に、Addressクラスのclone()メソッドを呼び出し、それを自身のaddressフィールドに格納します【3】。このようにすると、ディープコピーを行うことが可能になります。なお当然ですが、Addressクラスにも、Personクラスと同様に、clone()メソッドを実装しておく必要があります。
これでPersonクラス、Addressクラスの作成は完了です。

それでは次に、Personクラスのインスタンスを生成し、clone()メソッドを呼び出してみましょう。以下のコードを見てください。

snippet (pro.kensait.java.basic.lsn_13_1_4.clone.Main)
Address address = new Address("103-0004", "中央区", "1-1-1");
Person person = new Person("Alice", 25, address);
Person copy = person.clone();

このコードを実行すると、Personインスタンスのディープコピーされたインスタンスがコピーとして生成され、変数copyに格納されます。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. Objectクラスは階層構造の最上位に位置するクラスであること。
  2. toString()メソッドをオーバーライドする目的や方法について。
  3. 同一性と等価性の概念や両者の違いについて。
  4. 同一性は==によって判定すること。
  5. 等価性はequals()メソッドをオーバーライドすることによって判定すること。
  6. hashCode()メソッドをオーバーライドする目的や方法について。
  7. 参照型変数にまつわる諸問題や「参照の値渡し」について。
  8. イミュータブルオブジェクトの意味や目的について。
  9. インスタンスのコピーにはシャローコピーとディープコピーがあり、両者には違いがあること。
  10. clone()メソッドをオーバーライドする目的や方法について。

Discussion