【Java】ListとSetの違いを理解する


このドキュメントは10年位前に新人エンジニア向けに作成した資料の転載になります。
一部APIや構文など古臭い箇所が目に付く場合もありますが、考え方については一度覚えたら長く使える基礎の部分を整理したつもりなので、今の若いエンジニアの方にも参考になればと思います。

理解のポイント

  • JavaにおけるListとSetの違いを理解する。
  • Listは重複要素を保持する。
  • Setは重複要素を保持しない。
  • そもそもJavaにおける重複要素とは。

前提条件

equalsとhashCodeで実装したUserクラスを使用します。
なお、オブジェクトの状態を出力するためにObject#toString()メソッドをオーバーライドしています。

User.java
    class User {

        /** ID */
        private int id;

        /** 名前 */
        private String name;

        /**
         * コンストラクタです。
         * 
         * @param id ID
         * @param name 名前
         */
        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }

        // (略) getter等

        /**
         * このクラスのハッシュ値を返します。
         * 
         * @return このクラスのハッシュ値
         */
        public int hashCode() {
            return this.id * 13;
        }

        /**
         * このクラスのインスタンスと引数で渡されたオブジェクトが
         * 同値であるばあいtrueを返します。
         * 引数で渡されたオブジェクトがUserクラスのインスタンスであり、
         * idの値が等しい場合、同値であるとみなされます。
         *
         * @return 引数で渡されたオブジェクトがUserクラスのインスタンスであり、idが等しい場合true。
         */
        public boolean equals(Object other) {

            if (this == other) { // 引数で渡されたオブジェクトがこのオブジェクト自身であった場合true
                return true;
            }

            if (!(other instanceof User)) { // 引数で渡されたオブジェクトが、Userクラスのオブジェクト
                return false;               // では無い場合はfalse。
            }

            User otherUser = (User) other;
            if (this.id == otherUser.getId()) { // IDの値を比較し、等しければtrue、等しくなければfalse。
                return true;
            }
            return false;
        }

        /**
         * このクラスのインスタンスの文字列表現です。
         * 
         * @return このクラスのインスタンスの文字列表現。
         */
        public String toString() {
            return "User : id = " + id + " name = " + name;
        }
    }

ListとSetの違いについて

Listインターフェースを実装しているクラスに要素を追加した場合

  • 要素を追加した順番を保証します。 つまり、add()した順番にindexを指定して要素を取得することができるます。
  • 重複要素を保持します。そもそも重複要素とは?はあとで説明します。

Setインターフェースを実装しているクラスに要素を追加した場合

  • 要素を追加した順番を保証しません。 つまり、indexを指定して要素を取得することができません。
  • 重複要素を保持しません。

Listに重複要素を追加した場合の例

idの値によって同値性を判定する(equalsメソッド内でidの値が等しい場合にtrueを返す) Userクラスのインスタンスを下記のようにArrayListに追加し、追加された要素を文字列として標準出力 するならば・・・

    User user1 = new User(1, "前川");
    User user2 = new User(2, "鈴木");
    User user3 = new User(3, "前川");
    User user4 = new User(1, "佐藤"); // IDがuser1と重複

    List userList = new ArrayList();
    userList.add(user1);
    userList.add(user2);
    userList.add(user3);
    userList.add(user4);

    for (Iterator userIter = userList.iterator(); userIter.hasNext(); ) {
        User user = (User) userIter.next();
        System.out.println("userListのUser : " + user); // toStringメソッド呼び出しの内容を出力
    }

出力結果は以下のようになります。
要素の追加順に出力され、重複要素を保持します。

    *****************************************
    userListのUser : User : id = 1 name = 前川
    userListのUser : User : id = 2 name = 鈴木
    userListのUser : User : id = 3 name = 前川
    userListのUser : User : id = 1 name = 佐藤
    *****************************************

Setに重複要素を追加した場合の例

idの値によって同値性を判断する(equalsメソッド内でidの値が等しい場合にtrueを返す)Userクラスのインスタンスを下記のようにHashSetに追加し、追加された要素を文字列として標準出力するならば・・・

    User user1 = new User(1, "前川");
    User user2 = new User(2, "鈴木");
    User user3 = new User(3, "前川");
    User user4 = new User(1, "佐藤"); // IDがuser1と重複

    Set userSet = new HashSet();
    userSet.add(user1);
    userSet.add(user2);
    userSet.add(user3);
    userSet.add(user4);

    for (Iterator userIter = userSet.iterator(); userIter.hasNext(); ) {
        User user = (User) userIter.next();
        System.out.println("userSetのUser : " + user); // toStringメソッド呼び出しの内容を出力
    }

出力結果は以下のようになります。
要素の追加順には出力されず(保証されず)、重複要素を保持しません。

    *****************************************
    userSetのUser : User : id = 3 name = 前川
    userSetのUser : User : id = 2 name = 鈴木
    userSetのUser : User : id = 1 name = 前川
    *****************************************

発展的な話題

そもそも重複とは

そもそもJavaにおける"Set"とは数学におけるSet(集合)の概念の抽象表現です。

Set(集合)に含まれる要素はすべてユニークです。

すなわち、同一のSet(集合)に含まれる要素で、複数の要素が同一(同値)とみなされることはありません。

これをJavaで表現すると

e1.equals(e2) である e1 と e2 の要素ペアは持たない。
と言い表すことができます。
JavaにおけるSetやListといったコレクションにおいて"重複する要素"とは二つの要素(オブジェクト)をequalsメソッド で比較した場合に戻り値としてtrueが返る複数の要素にほかなりません。

(Objectクラスで定義されるデフォルトのequalsメソッドを使用しない限り)equalsメソッドはプログラマが実装しなければ ならないため、Javaにおけるオブジェクトの"重複"に関するルールは、アプリケーションのビジネスルールやプログラマの実装によってさまざまな 方法で定義が可能ということになります。

equalsメソッドの実装方法により、Setの保持する要素は異なる

以前の例では、Userクラスのインスタンスの同値性を"ID"の値によって判定していました。

Userクラスのインスタンスの同値性を"名前"の値によって判定するように実装するとUserクラスのインスタンスをSetに 追加した場合どのような挙動をみせるでしょうか?

User.java

        // nameの値で同値性を判定するUserクラスの実装例
        // (ただし都合上nameの値にnullは格納されないこととする)

        /**
         * このクラスのハッシュ値を返します。
         * 
         * @return このクラスのハッシュ値
         */
        public int hashCode() {
            return this.name.hashCode();
        }

        /**
         * このクラスのインスタンスと引数で渡されたオブジェクトが
         * 同値であるばあいtrueを返します。
         * 引数で渡されたオブジェクトがUserクラスのインスタンスであり、
         * nameの値が等しい場合、同値であるとみなされます。
         *
         * @return 引数で渡されたオブジェクトがUserクラスのインスタンスであり、nameが等しい場合true。
         */
        public boolean equals(Object other) {

            if (this == other) { // 引数で渡されたオブジェクトがこのオブジェクト自身であった場合true
                return true;
            }

            if (!(other instanceof User)) { // 引数で渡されたオブジェクトが、Userクラスのオブジェクト
                return false;               // では無い場合はfalse。
            }

            User otherUser = (User) other;
            if (this.name.equals(otherUser.getName())) { // nameの値を比較し、等しければtrue、等しくなければfalse。                 return true;
            }
            return false;
        }

nameの値によって同値性を判断する(equalsメソッド内でidの値が等しい場合にtrueを返す)Userクラスのインスタンスを下記のようにHashSetに追加し、追加された要素を文字列として標準出力するならば・・・

    *****************************************
    userSetのUser : User : id = 1 name = 佐藤
    userSetのUser : User : id = 2 name = 鈴木
    userSetのUser : User : id = 1 name = 前川
    *****************************************

さらに

  • equalsメソッドとhashCodeメソッドを実装しない場合はどのような動きをするか。
  • equalsメソッドを実装しhashCodeメソッドを実装しない場合はどのような動きをするか。

を確かめてみてください