LoginSignup
1
0

More than 1 year has passed since last update.

【Java発展学習1日目】等価と等値の違い、インスタンスの複製

Last updated at Posted at 2021-06-29

大半のインスタンスの共通メソッド

以下のメソッドは利用機会の多いクラスの上位クラスであり、特にObjectクラスは全てのクラス上位クラスであるため、
ユーザ定義クラスを実装する場合は基本的にオーバーライドが推奨される。

メソッド 内容 関連クラス
toString() String型への変換 Object
equals() 等価判定 Object
hashCode() ハッシュ値の取得 Object
clone() オブジェクトの複製 Object, Cloneable
compareTo() 大小関係の判定 Comparable

equals()メソッドとhashCode()メソッド

Object.equals()メソッドはハッシュ値が等しいオブジェクト間の等価判定を行い、
Object.hashCode()メソッドはオブジェクトハッシュ値の生成を行う。

定義

// オブジェクト間の等価判定
boolean Object.equals(Object obj)
// パラメータ
// obj: 等価判定を行う参照型オブジェクト

// オブジェクトのハッシュ値の生成
int Object.hashCode()

等価(equivalent)と等値(equality)

等価(equals)は、ポインタに格納される値が同じであることを指し、
等値(==)は、ポインタが同じであることを指す。

サンプルコード1(String型の等値判定)

EquivalentAndEquality.java
class EquivalentAndEquality {
    public static void main(String[] args) {
        // 同じ綴りをもつ文字列リテラルは、同一のString型インスタンスを参照
        String s1 = "ABC";
        String s2 = "ABC";

        // 実行時に生成される文字列は、異なるインスタンスとして扱われる
        String s3 = new String("ABC");
        String s4 = new String("ABC");

        // 文字列リテラルで定義したString型インスタンス間の「等価」判定
        if (s1.equals(s2)) {
            System.out.println("Both strings are equivalent.");
        }
        else {
            System.out.println("Both strings are not equivalent.");
        }
        // 文字列リテラルで定義したString型インスタンス間の「等値」判定
        if (s1 == s2) {
            System.out.println("Both strings have equality.");
        }
        else {
            System.out.println("Both strings does not have equality.");
        }

        // 実行時に生成した文字列(Stringオブジェクト)間の「等価」判定
        if (s3.equals(s4)) {
            System.out.println("Both strings are equivalent.");
        }
        else {
            System.out.println("Both strings are not equivalent.");
        }
        // 実行時に生成した文字列(Stringオブジェクト)間の「等値」判定
        if (s3 == s4) {
            System.out.println("Both strings have equality.");
        }
        else {
            System.out.println("Both strings do not have equality.");
        }
    }
}
実行結果
Both strings are equivalent.
Both strings have equality.  // 文字列リテラルを代入した場合は「参照先」も同じ
Both strings are equivalent.
Both strings do not have equality.  // 実行時にオブジェクトを生成した場合は「参照先」が異なる

サンプルコード2(配列の等価判定)

ArraysEquivalent.java
import java.util.Arrays;

public class ArraysEquivalent {
    public static void main(String[] args) {
        int[] a = {1,2,3};
        int[] b = {1,2,3};

        // 配列間の比較では、Objectクラスのequals()メソッドは「等値」判定を行う
        System.out.println("Equality: " + a.equals(b));
        // 配列間で「等価」判定を行う場合は、Arraysクラスのequals()メソッドを用いる
        System.out.println("Equivalence: " + Arrays.equals(a, b));
    }
}

コレクション(Collection)と配列(Arrays)

コレクションは新しい要素が追加されるたびにメモリ領域を確保し、
配列は生成時にメモリ領域を確保する。

また、コレクションリスト(List)セット(Set)に分類され、
リストは内部的に順序(Order)をもつため、値の重複を許しているが、
セットは内部的に順序をもたないため、値の重複を許さない

サンプルコード(HashSetの等価判定)

HashSetEquivalent.java
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class HashSetEquivalent {
    public static void main(String[] args) {
        // HashSetオブジェクトの生成
        Set<A> list = new HashSet<>();

        // ユーザ定義クラスのオブジェクトを生成・HashSetオブジェクトへの追加
        A a1 = new A("Object A", 1);
        list.add(a1);

        System.out.println("components: " + list.size());

        // ユーザ定義クラスの(等値でない)オブジェクトの再生成・HashSetオブジェクトから削除
        a1 = new A("Object A", 1);
        // HashSetクラスのremove()メソッドは、以下の手順で等価である構成要素を走査
        // 1. 構成要素に対してhashCode()メソッドを呼び出しハッシュ値を生成
        // 2. ハッシュ値が同じである構成要素を走査(->ハッシュ値はint型であり比較が容易であるため)
        // 3. ハッシュ値が同じである構成要素に対してequals()メソッドで等価判定を実行
        // 4. 等価である構成要素をHashSetから削除
        list.remove(a1);

        System.out.println("components: " + list.size());
    }
}

// ユーザ定義クラス
class A {
    private String str;
    private int n;

    // コンストラクタ
    public A(String str, int n) {
        this.str = str;
        this.n = n;
    }

    // 等価判定
    // -> 「等価」の基準を定義するためにオーバーライドする必要がある
    @Override
    public boolean equals(Object o) {
        // ①「等値」であれば「等価」である
        if (o == this) return true;

        // ②「引数」がnullであればそもそも「等価」でない(=「等価」判定が実施できない)
        if (o == null) return false;

        // ③「引数」が「クラスA」または「クラスAの下位クラス」でなければそもそも「等価」でない
        if (!(o instanceof A)) return false;
        // また、「引数」が「クラスAの下位クラス」である場合に備えてアップキャストを行う
        A h = (A) o;

        // ④String.trim()メソッドを用いて先頭・末尾の「空白文字」を削除し、「等価」でなければfalseを返却
        if (!this.str.trim().equals(h.str.trim())) {
            return false;
        }

        // 上記の条件にすべて当てはまらない場合はtrueを返却
        return true;
    }

    // ハッシュ値の生成
    // -> 「ハッシュ値」の算出方法を定義するためにオーバーライドする必要がある
    @Override
    public int hashCode() {
        // ユーザ定義の引数をもとにハッシュ値を生成
        return Objects.hash(this.str, this.n);
    }
}
実行結果
components: 1
components: 0  // 「等価」判定が正常に行われ、構成要素が削除される

compareTo()メソッド

Comparable<T>インタフェースのcompareTo()メソッドを実装することで、自然順序付け(natural ordering)を定義でき、
Comparator<T>インタフェースのcompare()メソッドを実装することで、コンパレータ(comparator)を作成することができる。

自然順序付けコンパレータによって、大小関係を比較するインスタンス変数を定義することができる。
ただし、大小関係を比較するクラスはComparable<T>インタフェースの実装クラスでなければならない。

また、定義したソート基準に基づいて、Collectionsクラスのsort()メソッドでソートすることができる。

なお、compareTo()およびcompare()メソッドのint型返り値には、以下の規則がある。

内容
の数 自身 < 比較対象
0 自身 = 比較対象
の数 自身 > 比較対象

定義

// 「自然順序付け」をもとに大小関係を比較
int Comparable<T>.compareTo(T o)
// パラメータ
// o: 比較対象となるオブジェクト

// 「コンパレータ」をもとに大小関係を比較
int Comparator<T>.compare(T o1, T o2)
// パラメータ
// o1: 比較対象となる1つ目のオブジェクト
// o2: 比較対象となる2つ目のオブジェクト

// 自然順序によるソート
// <? super T>: クラスTまたはその上位クラス
// -> "?"は「型ワイルドカード」を表し、共変性(covariance)を利用
<T extends Comparable<? super T>> void Collections.sort(List<T> list)
// パラメータ
// list: ソート対象となるListオブジェクト

// コンパレータによるソート
<T> void sort(List<T> list, Comparator<? super T> c)
// パラメータ
// list: 「ソート対象」となるListオブジェクト
// c: 「ソート基準」となるコンパレータオブジェクト

サンプルコード(自然順序付けとコンパレータ)

NaturalOrderingAndComparator.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class Main {
    public static void main(String[] args) {
        // ArrayListオブジェクト
        List<CompareTo> list = new ArrayList<>();

        list.add(new CompareTo(1, 10, 9));
        list.add(new CompareTo(3, 7, 11));
        list.add(new CompareTo(8, 8, 5));

        System.out.println("-- Before --");
        for (int i = 0; i < list.size(); i++) {
            System.out.print("index " + i + ": ");
            System.out.println(list.toArray()[i]);
        }
        System.out.println();

        // ユーザ定義の「自然順序付け」によるソート
        Collections.sort(list);

        System.out.println("-- By Natural Ordering(num2) --");
        for (int i = 0; i < list.size(); i++) {
            System.out.print("index " + i + ": ");
            System.out.println(list.toArray()[i]);
        }
        System.out.println();

        // "num1"プロパティの「コンパレータ」によるソート
        Collections.sort(list, new Num1Comparator());

        System.out.println("-- By num1 --");
        for (int i = 0; i < list.size(); i++) {
            System.out.print("index " + i + ": ");
            System.out.println(list.toArray()[i]);
        }
        System.out.println();

        // "num3"プロパティの「コンパレータ」によるソート
        Collections.sort(list, new Num3Comparator());

        System.out.println("-- By num3 --");
        for (int i = 0; i < list.size(); i++) {
            System.out.print("index " + i + ": ");
            System.out.println(list.toArray()[i]);
        }
    }
}

// "num1"プロパティの値をもとに順序付けを行うコンパレータ
// -> Comparator<T>インタフェースの実装クラス
class Num1Comparator implements Comparator<CompareTo> {
    // Comparator<T>インタフェースのcompare()メソッドをオーバーライド
    public int compare(CompareTo x, CompareTo y) {
        return (x.getNum1() - y.getNum1());
    }
}

// "num2"プロパティの値をもとに順序付けを行うコンパレータ
class Num3Comparator implements Comparator<CompareTo> {
    // Comparator<T>インタフェースのcompare()メソッドをオーバーライド
    public int compare(CompareTo x, CompareTo y) {
        return (x.getNum3() - y.getNum3());
    }
}

// 大小関係を比較するクラス
// -> オブジェクトの大小関係を比較する場合、Comparable<T>インタフェースを実装し、
//    compareTo()メソッドをオーバーライドすることで「自然順序付け」を定義する必要がある。
// <- Comparable<T>の型は"実装クラス名"を指定
class CompareTo implements Comparable<CompareTo> {
    private int num1;
    private int num2;
    private int num3;

    // コンストラクタ
    public CompareTo(int num1, int num2, int num3) {
        this.num1 = num1;
        this.num2 = num2;
        this.num3 = num3;
    }

    // ゲッタ(=アクセサ)
    public int getNum1() {
        return num1;
    }

    // ゲッタ(=アクセサ)
    public int getNum3() {
        return num3;
    }

    // toString()メソッドのオーバーライドによる出力形式の指定
    @Override
    public String toString() {
        return ( "CompareTo(" + num1 + ", " + num2 + ", " + num3 + ")" );
    }

    // 自然順序付け
    // -> Comparable<T>インタフェースのcompareTo()メソッドをオーバーライド
    // => "num2"プロパティの値をもとに自然順序付けが行われる
    @Override
    public int compareTo(CompareTo obj) {
        if (this.num2 < obj.num2) {
            return -1;
        }
        else if (this.num2 > obj.num2) {
            return 1;
        }
        // 比較対象の方が 大きくない かつ 小さくない(=「等価」である)場合
        return 0;
    }
}
実行結果
-- Before --
index 0: CompareTo(1, 10, 9)
index 1: CompareTo(3, 7, 11)
index 2: CompareTo(8, 8, 5)

-- By Natural Ordering(num2) --  // "num2"プロパティの値をもとに昇順ソート
index 0: CompareTo(3, 7, 11)
index 1: CompareTo(8, 8, 5)
index 2: CompareTo(1, 10, 9)

-- By num1 --  // "num1"プロパティの値をもとに昇順ソート
index 0: CompareTo(1, 10, 9)
index 1: CompareTo(3, 7, 11)
index 2: CompareTo(8, 8, 5)

-- By num3 --  // "num3"プロパティの値をもとに昇順ソート
index 0: CompareTo(8, 8, 5)
index 1: CompareTo(1, 10, 9)
index 2: CompareTo(3, 7, 11)

equals()メソッドとcompareTo()メソッドの一貫性

等価である2つのオブジェクトについて、等価判定を行うequals()メソッドの実行結果と大小関係比較を行うcompareTo()メソッドの実行結果は常に一致していることが望ましい。(=equalsとcompareToの一貫性)
そのため、一貫性のないBigIntegerBigDecimalクラスは、利用にあたって注意が必要となる。


clone()メソッド

マーカーインタフェース(marker interface)であるCloneableインタフェースの実装クラス
clone()メソッドをオーバーライドすることで、インスタンス複製が可能になる。

2種類の複製

複製には、以下の2種類が存在する。

種類 内容
浅い複製(shallow copy) インスタンスのみを複製
深い複製(deep copy) 参照先インスタンスを含めた複製

サンプルコード(インスタンスの複製)

CopyInstance.java
public class CopyInstance {
    public static void main(String[] args) {
        // 複製元オブジェクト
        Y b1 = new Y("b1", 1, new X("a1"));

        // 複製先オブジェクト
        Y b2 = b1.clone();

        System.out.println("-- Before --");
        System.out.println("b1: " + b1);
        System.out.println("b2: " + b2);

        System.out.println();

        // 複製先オブジェクトの値の書き換え
        b2.setY("b2", 2, new X("a2"));

        System.out.println("-- After --");
        System.out.println("b1: " + b1);
        System.out.println("b2: " + b2);
    }
}

// インスタンスの複製を行うクラス
// -> Cloneableインタフェースの実装クラス
class X implements Cloneable {
    private String str;

    // デフォルトコンストラクタ
    public X() {}

    // コンストラクタ
    public X(String str) {
        this.str = str;
    }

    public String getStr() {
        return str;
    }

    // 深い複製(deep copy)を行うclone()メソッドの定義
    // -> clone()メソッドのオーバーライド(※Cloneableインタフェースでは未宣言(=マーカーインタフェース))
    @Override
    public X clone() {
        X a = new X();
        a.str = this.str;

        return a;
    }
}

// インスタンスの複製を行うクラス
// -> Cloneableインタフェースの実装クラス
class Y implements Cloneable {
    // 参照型(特殊)プロパティ
    private String str;
    // 値型プロパティ
    private int n;
    // 参照型(一般)プロパティ
    // -> 深い複製を行う場合は、参照先プロパティの値を複製する必要がある
    private X aObj;

    // デフォルトコンストラクタ
    public Y() {}

    // コンストラクタ
    public Y(String str, int n, X aObj) {
        this.str = str;
        this.n = n;
        this.aObj = aObj;
    }

    // セッタ(=アクセサ)
    public void setY(String str, int n, X aObj) {
        this.str = str;
        this.n = n;
        this.aObj = aObj;
    }

    @Override
    public String toString() {
        return ( "Y(" + str + ", " + n + ", " + aObj.getStr() + ")" );
    }

    // 深い複製(deep copy)を行うclone()メソッドの定義
    @Override
    public Y clone() {
        Y b = new Y();
        b.str = this.str;
        b.n = this.n;
        // 深い複製(deep copy)
        // -> 参照先インスタンスを複製
        b.aObj = this.aObj.clone();

        return b;
    }
}
実行結果
-- Before --
b1: Y(b1, 1, a1)
b2: Y(b1, 1, a1)

-- After --
b1: Y(b1, 1, a1)  // 複製先オブジェクト(b2)を書き換えても値はそのまま
b2: Y(b2, 2, a2)

用語集

用語 内容
自然順序付け(natural ordering) 任意のクラスにおいて、一般的に想定されるソート順
コンパレータ(comparator) ソート基準を定義する、Comparatorインタフェースの実装クラス
マーカーインタフェース(marker interface) メソッドフィールドが定義されていないインタフェース
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0