🍇

18.2 コレクションの様々な機能(ソート、コンパレータ、同期化など)~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

18.2 コレクションの様々な機能

チャプターの概要

このチャプターでは、ソートや同期化など、コレクションフレームワークが提供する様々な機能について学びます。

18.2.1 Collectionsクラスの特徴とAPI

CollectionsクラスのAPI

java.util.Collectionsクラスは、コレクション(セットやマップ)を操作するためのユーティリティクラスです。
このクラスには多数のAPIが定義されていますが、その中から特に代表的なものを以下に示します。

API(メソッド) 説明
static void sort(List<T>) 指定されたリストを、その要素の「自然順序付け」に従って昇順にソートする。
static void sort(List<T>, Comparator<? super T>) 第一引数のリストを、第二引数のコンパレータが示す順序に従って昇順にソートする。
static Comparator<T> reverseOrder() 「逆自然順序付け」による特殊なコンパレータを生成して返す。
static boolean addAll(Collection<? super T>, T...) 第一引数のコレクションに、第二引数(可変引数)に指定されたすべての要素を追加する。
static void reverse(List<?>) 指定されたリストの要素を反転する。
static void rotate(List<?>, int) 第一引数のリストの要素を、第二引数の数により回転する。
static void shuffle(List<?>) 指定されたリストの要素を無作為に入れ替える。
static List<T> unmodifiableList(List<? extends T>) 指定されたリストから、イミュータブルなリストを生成して返す。
static Set<T> unmodifiableSet(Set<? extends T>) 指定されたセットから、イミュータブルなセットを生成して返す。
static Map<K,V> unmodifiableMap(Map<? extends K, ? extends V>) 指定されたマップから、イミュータブルなマップを生成して返す。
static List<T> synchronizedList(List<T>) 指定されたリストから、同期化されたリストを生成して返す。
static Set<T> synchronizedSet(Set<T>) 指定されたセットから、同期化されたセットを生成して返す。
static Map<K,V> synchronizedMap(Map<K,V>) 指定されたマップから、同期化されたマップを生成して返す。

Collectionsクラスによるコレクションのソート

Collectionsクラスのsort()メソッドにより、コレクション内の要素をソートすることができます。以下のコードを見てください。

snippet_1 (pro.kensait.java.basic.lsn_18_2_1.Main_Sort_Integer)
List<Integer> list = new ArrayList<>();
list.add(5);
list.add(10);
list.add(3);
Collections.sort(list);

まずInteger型のリストを生成し、3つの整数、5、10、3を追加します。ArrayListは配列の拡張実装のため、追加された順に要素が並びます。
次にCollectionsクラスのsort()メソッドにより、ソートを行います。このようにsort()メソッドにリストを渡すと、格納された要素の「自然順序付け」に従って昇順にソートが行われます。「自然順序付け」とは当該クラスのデフォルトのソートロジックを意味し、後述するComparableインタフェースによって実装されます。例えばInteger型の場合は整数の大きさ順(昇順)、Stirng型の場合には辞書順になります。ここではリストに5、10、3という順に数値が格納されているため、ソートの結果3、5、10という順に並べ替えられます。

Comparableインタフェースによる「自然順序付け」の実装

インスタンス同士を「自然順序付け」によって並べ替えるためには、java.lang.Comparableというインタフェースを実装する必要があります。ここでは「人物」を表すPersonクラスを題材に、「自然順序付け」を実装する方法を説明します。このクラスには以下のように、name(名前)、age(年齢)、gender(性別)という3つのフィールドがあり、コンストラクタとアクセサメソッドも定義されています。

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

Personクラスにおける「自然順序付け」をどのようにするかは開発者の設計次第ですが、ここでは「年齢による昇順」としましょう。まずPersonクラスは、Comparableインタフェースをimplementsする必要があります。

snippet_1 (pro.kensait.java.basic.lsn_18_2_1.Person)
public class Person implements Comparable<Person> { .... }

そしてこのインタフェースで宣言されているcompareTo()メソッドをPersonクラスに追加し、以下のようにオーバーライドします。

snippet_2 (pro.kensait.java.basic.lsn_18_2_1.Person)
@Override
public int compareTo(Person other) {
    if (age < other.getAge()) return -1; //【1】
    if (other.getAge() < age) return 1; //【2】
    return 0; //【3】
}

このメソッドには、インスタンス同士の大小比較のロジックを実装します。大小比較の結果、自身のインスタンスの方が比較対象よりも小さい場合は-1を、大きい場合は1を、等しい場合は0を、それぞれ返すように実装します。ここでは「年齢による昇順」での比較を行うので、「自身の方が年齢が小さい」場合に-1を返し【1】、「自身の方が年齢が大きい」場合に1を返し【2】、それ以外の場合に0を返す【3】ように実装します。同じ年齢による比較でも降順の比較であれば、【1】と【2】の判定が逆転する形になります。
なおこのメソッドは、以下のようにIntegerクラスのcompareTo()メソッドを呼び出しても、同じ結果が得られます。

snippet
@Override
public int compareTo(Person other) {
    return Integer.compare(age, other.getAge());
}

「自然順序付け」によるソート

それでは前項で取り上げたPersonクラスを、Collectionsクラスのsort()メソッドでソートしてみましょう。
以下のコードを見てください。

snippet_1 (pro.kensait.java.basic.lsn_18_2_1.Main_Sort_Person)
Person alice = new Person("Alice", 25, "female");
Person bob = new Person("Bob", 35, "male");
Person carol = new Person("Carol", 30, "female");
List<Person> list = new ArrayList<>();
list.add(alice);
list.add(bob);
list.add(carol);
Collections.sort(list);

Alice、Bob、Carol、3人のPersonインスタンスからなるリストを作成し、それをsort()メソッドに渡しています。このコードを実行すると、sort()メソッドの内部で前項で取り上げたcompareTo()メソッドが呼び出され、その比較結果に応じてソートが行われます。PersonクラスのcompareTo()メソッドには、「年齢による昇順」での比較が実装されているため、年齢の低い方からAlice、Carol、Bobの順にリスト内の要素がソートされます。ソート後のリストから、例えばlist.get(1)で要素を取り出すと、2番目の要素であるCarolのインスタンスが返されます。

コンパレータによるソート

前項では「自然順序付け」、すなわちデフォルトのソートロジックによってソートする方法を取り上げましたが、コレクションのソートでは、様々なロジックを切り替えて使いたい、というケースもあります。このようなケースでは、コンパレータという機能によってソートします。コンパレータは、java.util.Comparatorインタフェースをimplementsしてクラスとして作成します。
それではコンパレータクラスを作成してみましょう。今回はソートのロジックは若干複雑になり、2つのキーでソートを行うものとします。まず性別で男性、女性の順にソートし、次に名前の辞書順にソートします。要は、一昔前の学校における一般的な出席番号の並びと同じロジックです。
このようなソートロジックを実現するために、以下のようなコンパレータクラスを作成します。

pro.kensait.java.basic.lsn_18_2_1.PersonNameComparator
public class PersonNameComparator implements Comparator<Person> { //【1】
    @Override
    public int compare(Person p1, Person p2) {
        if (p1.getGender().equals("male")) { // 男性の場合
            if (p2.getGender().equals("female")) return -1; //【2】 
            return p1.getName().compareTo(p2.getName()); //【3】
        } else { // 女性の場合
            if (p2.getGender().equals("male")) return 1; //【4】
            return p1.getName().compareTo(p2.getName()); //【5】
        }
    }
}

コンパレータクラスは、Comparatorインタフェースをimplementsし、宣言されているcompare()メソッドをオーバーライドします【1】。このメソッドには、Comparableインタフェースと同様に、自身のインスタンスの方が小さい場合は負の整数を、大きい場合は正の整数を、等しい場合は0を返すように実装します。
メソッドの中を見ると、今回のソート要件に従い、まず性別で分岐します。そしてそれぞれの分岐の中で、性別が同じかどうかの判定を行い、異なる場合は「男性は女性よりも小さい」と見なします【2、4】。性別が同じ場合は名前の辞書順に比較を行うため、StringクラスのcompareTo()メソッドを呼び出してその結果を返します【3、5】。

それでは作成したコンパレータクラスを使って、既出のリストをソートしてみましょう。ソートの実行には、今回もCollectionsクラスのsort()メソッドを使用します。
以下のコードを見てください。

snippet_2 (pro.kensait.java.basic.lsn_18_2_1.Main_Sort_Person)
Collections.sort(list, new PersonNameComparator());

このようにsort()メソッドの第2引数に、作成したコンパレータクラスのインスタンスを指定します。ソートロジックは、指定されたコンパレータクラスの種類によって切り替えることができます。
このコードを実行すると、sort()メソッドの内部でコンパレータクラスのcompare()メソッドが呼び出され、その大小比較の結果に応じてソートが行われます。その結果Bob、Alice、Carolの順に、リスト内の要素がソートされます。

コンパレータによる逆順ソート

CollectionsクラスのreverseOrder()メソッドを呼び出すと、「逆自然順序付け」による特殊なコンパレータを生成可能です。従ってリストを「自然順序付け」の逆順にソートしたい場合は、以下のようにしてsort()メソッドを呼び出します。

snippet_2 (pro.kensait.java.basic.lsn_18_2_1.Main_Sort_Integer)
Collections.sort(list, Collections.reverseOrder());

Collectionsクラスによるコレクションの操作(ソート以外)

ここではCollectionsクラスのAPIによるコレクションの操作のうち、ソート以外のものを取り上げます。まずaddAll()メソッドは、コレクション(リストやセット)に任意の個数の要素を追加します。
以下は、生成したリストに1から10までの数値を追加するためのコードです。

snippet_1 (pro.kensait.java.basic.lsn_18_2_1.Main_Collections)
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

このユーティリティは便利ですので、使う機会は比較的多いのではないでしょうか。
次にreverse()メソッドは、リストの要素を反転させます。以下のようにすると既出のリスト(変数list)が反転し、[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]となります。

snippet_2 (pro.kensait.java.basic.lsn_18_2_1.Main_Collections)
Collections.reverse(list);

次にrotate()メソッドは、リストを回転させます。以下のようにすると既出のリスト(変数list)が左に3つ回転され、[4, 5, 6, 7, 8, 9, 10, 1, 2, 3]となります。

snippet_3 (pro.kensait.java.basic.lsn_18_2_1.Main_Collections)
Collections.rotate(list, -3);

最後にshuffle()メソッドは、リストの要素をシャッフルします。

snippet_4 (pro.kensait.java.basic.lsn_18_2_1.Main_Collections)
Collections.shuffle(list);

なおそれ以外のunmodifiable〇〇()メソッドや、synchronized〇〇()メソッドについては、レッスン18.2.3で取り上げます。

18.2.2 ソートされたセットとマップ

ソートされたセット

セット系インタフェースの1つであるjava.util.SortedSetは、文字通りソートされたセットを表します。このインタフェースの実装クラスの代表が、java.util.TreeSetです。TreeSetに格納された要素は、「自然順序付け」に従ってセット内で自動的にソートされます。TreeSetを使うとセット内の要素が「自然順序付け」によって並ぶため、「ある要素未満の要素」であったり「ある要素以上の要素」であったりと、並び順に応じた部分集合を作ることができます。
以下のコードを見てください。

snippet (pro.kensait.java.basic.lsn_18_2_2.Main_TreeSet)
// 1から10までの数値が格納されたセットを作成する
SortedSet<Integer> sortedSet = new TreeSet<>(); //【1】
for (int i = 0; i < 10; i++) {
    sortedSet.add(i + 1);
}
// 部分集合を作る
SortedSet<Integer> hs = sortedSet.headSet(5); //【2】
SortedSet<Integer> ts = sortedSet.tailSet(5); //【3】
SortedSet<Integer> ss = sortedSet.subSet(3, 7); //【4】

実装クラスとしてTreeSetを選択して、SortedSetインタフェースの変数を宣言します【1】。ここでは1から10までの数値が格納されたセットを作成します。
headSet()メソッドを呼び出すと、指定された要素「未満」の部分集合を取り出すことができます【2】。ここでは変数hsには、1から5までの5つの要素が格納されます。
tailSet()メソッドを呼び出すと、指定された要素「以上」の部分集合を取り出すことができます【3】。ここでは変数tsには、6から10までの5つの要素が格納されます。
またsubSet()メソッドを呼び出すと、指定されたインデックスの範囲から、新しい部分集合を作ることができます【4】。2つの引数は、部分集合の始点となるインデックス(これを含む)と、終点となるインデックス(これを含まない)です。従って変数ssには、値4(インデックス3)から値7(インデックス6)までの要素が格納されます。
なおSortedSetに、Comparableインタフェースを実装していないクラスを格納しようとすると、コンパイルエラーになります。

ソートされたマップ

マップ系インタフェースの1つであるjava.util.SortedMapは、キーでソートされたマップを表します。このインタフェースの実装クラスの代表が、java.util.TreeMapです。TreeMapに格納された要素のキーは、「自然順序付け」によって自動的にソートされます。従ってTreeMapを使うと、TreeSetと同じ考え方で、並び順に応じたサブマップを作ることができます。TreeMapのAPIはTreeSetと同じ考え方のため、本コースでは割愛します。

18.2.3 コレクションのその他の機能

イミュータブルなコレクション

コレクションは基本的に、値の書き換えが可能なミュータブルオブジェクトです。レッスン13.1.3で取り上げたような、ミュータブルであることの諸問題を解決するために、イミュータブルなコレクションを作ることができます。
イミュータブルなコレクションは、以下のようにCollectionsクラスのユーティリティによって生成します。

  • List … Collections.unmodifiableList(List)
  • Set … Collections.unmodifiableSet(Set)
  • Map … Collections.unmodifiableMap(Map)

例えばリストであれば、以下のようにします。

snippet_1 (pro.kensait.java.basic.lsn_18_2_3.Main)
List<String> list = new ArrayList<>();
Collections.addAll(list, "foo", "bar", "baz");
List<String> il = Collections.unmodifiableList(list);

このように一度作成したリストから、CollectionsクラスのunmodifiableList()メソッドによって、新しいイミュータブルなリストを生成します。

同期化されたコレクション

コレクションは同期化されていないため、マルチスレッド環境において複数スレッドが同時に値を書き換えようとすると、状態が不正になる可能性があります。このような課題を解決するために、同期化されたコレクションを利用します[1]
同期化されたコレクションは、以下のようにCollectionsクラスのユーティリティによって生成します。

  • List … Collections.synchronizedList(List)
  • Set … Collections.synchronizedSet(Set)
  • Map … Collections.synchronizedMap(Map)

例えばリストであれば、以下のようにします。

snippet_2 (pro.kensait.java.basic.lsn_18_2_3.Main)
List<String> list = new ArrayList<>();
Collections.addAll(list, "foo", "bar", "baz");
List<String> sl = Collections.synchronizedList(list);

このように一度作成したリストから、CollectionsクラスのsynchronizedList()メソッドによって、新しい同期化されたリストを生成します。

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

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

  1. Collectionsクラスの特徴やAPIについて。
  2. 「自然順序付け」やコンパレータによるソートについて。
  3. ソートされたセットおよびマップの特徴やAPIについて。
  4. イミュータブルや同期化されたコレクションの生成方法について。
脚注
  1. 同期化されたコレクションでは、書き込み操作、読み込み操作を行うときにコレクション全体を同期化するため、その分、並列度が犠牲になる。読み込み操作の比率が高い場合は、並行処理ユーティリティとして導入されたコピーオンライト方式によるリストやセットを利用することで並列度を高めることができる。これらの機能については、『Java Advanced編』のチャプター3.3「並行処理ユーティリティのその他の機能」を参照のこと。 ↩︎

Discussion