JavaのStreamでソートをしたいんだけど、どうやるのかパッとコードが出てこないということはありませんか?
本記事ではStreamでソートする様々なケースを考慮してサンプルを掲載していますのでぜひ参考にして下さい!
※Java11を使っています。
基本的な sorted() の使い方
List<String>のソート
Stringの場合、アルファベットを元にソートすることができます。
降順ソートにしたい場合はsortedの引数にComparator.reverseOrder()を与えます。(昇順/降順を参照)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import java.util.List; import static java.util.stream.Collectors.toList; public class StringSortExample { public static void main(String[] args) { List<String> strings = List.of("Banana", "Apple", "Cherry", "Date", "Elderberry"); List<String> sortedStrings = strings.stream() .sorted() .collect(toList()); sortedStrings.forEach(System.out::println); } } // Apple // Banana // Cherry // Date // Elderberry |
List<Integer>のソート
intの場合(実際にはIntegerで保持することになりますが)、数値としてソートされます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import java.util.List; import static java.util.stream.Collectors.toList; public class IntSortExample { public static void main(String[] args) { List<Integer> numbers = List.of(5, 2, 9, 1, 8, 3); List<Integer> sortedNumbers = numbers.stream() .sorted() .collect(toList()); sortedNumbers.forEach(System.out::println); } } // 1 // 2 // 3 // 5 // 8 // 9 |
昇順/降順
昇順では特に指定する必要はありませんが、明示的にComparator.naturalOrder()を与えることもできます。
降順の場合、Comparator.reverseOrder()をsortedメソッドの引数に与えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import java.util.Comparator; import java.util.List; import static java.util.stream.Collectors.toList; public class ReverseSortExample { public static void main(String[] args) { List<Integer> numbers = List.of(5, 2, 9, 1, 8, 3); List<Integer> ascending = numbers.stream() .sorted() // .sorted(Comparator.naturalOrder()) 明示的に指定も可能 .collect(toList()); List<Integer> descending = numbers.stream() .sorted(Comparator.reverseOrder()) .collect(toList()); System.out.println("Ascending:"); ascending.forEach(System.out::println); System.out.println("Descending:"); descending.forEach(System.out::println); } } // Ascending: // 1 // 2 // 3 // 5 // 8 // 9 // Descending: // 9 // 8 // 5 // 3 // 2 // 1 |
複数条件
複数条件を指定したい場合、thenComparingを追加していきます。
最初に指定した条件から優先的に適用されるので、以下の例では [ 年齢 > 名前 ] の順でソートされています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public class MultiFieldSortExample { public static void main(String[] args) { List<Person> people = List.of( new Person("Alice", 25), new Person("Bob", 30), new Person("Charlie", 22), new Person("David", 30) ); List<Person> sortedPeople = people.stream() .sorted(Comparator.comparing(Person::getAge) .thenComparing(Person::getName)) .collect(toList()); sortedPeople.forEach(person -> System.out.println(person.getName() + " - " + person.getAge())); } static class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } } } // Charlie - 22 // Alice - 25 // Bob - 30 // David - 30 |
null値を含む場合の扱い
nullが含まれるキーをソートする場合、Comparator.nullsFirst または Comparator.nullsLast を使用する必要があります。
Comparator.nullsFirstはnullが最初になるようにソートし、Comparator.nullsLastは最後になるようにソートします。
使用しなかった場合は NullPinterException がスローされるので注意が必要です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import java.util.Arrays; import java.util.List; import java.util.Comparator; import static java.util.stream.Collectors.toList; public class NullSortExample { public static void main(String[] args) { List<String> strings = Arrays.asList("Banana", "Apple", "Cherry", null, "Date", "Elderberry"); List<String> sortedStrings = strings.stream() .sorted(Comparator.nullsFirst(Comparator.naturalOrder())) .collect(toList()); sortedStrings.forEach(System.out::println); } } // null // Apple // Banana // Cherry // Date // Elderberry |
独自クラスのソート方法
Comparableを実装する
ソート対象のクラスにComparableが実装してあれば、OverrideされたcompareToに従って比較が行われることでソート順が規定されます。
compareToの比較をソート順としたい場合、sortedメソッドに追加の条件は必要はありません。
以下の例では、scoreがcompareToメソッドで比較されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import java.util.List; import static java.util.stream.Collectors.toList; public class ComparableSortExample { public static void main(String[] args) { List<Student> students = List.of( new Student("Alice", 90), new Student("Bob", 75), new Student("Charlie", 82) ); List<Student> sortedStudents = students.stream() .sorted() .collect(toList()); sortedStudents.forEach( student -> System.out.println(student.getName() + " - " + student.getScore())); } /** * Studentクラス * Comparableを実装し、compareToをOverrideすることで比較可能にしている。 * これによりソート順が規定される。 */ static class Student implements Comparable<Student> { private String name; private int score; public Student(String name, int score) { this.name = name; this.score = score; } public String getName() { return name; } public int getScore() { return score; } @Override public int compareTo(Student other) { return Integer.compare(this.score, other.score); } } } // Bob - 75 // Charlie - 82 // Alice - 90 |
Comparatorを指定する
Comparatorを使用して対象クラスのフィールドでソートすることもできます。
以下の例ではscoreの降順でソートしています。
ちなみに、対象クラスにComparableが実装されている場合でも、Comparatorの指定が優先されるので注意してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import java.util.List; import java.util.Comparator; import static java.util.stream.Collectors.toList; public class ComparatorSortExample { public static void main(String[] args) { List<Student> students = List.of( new Student("Alice", 90), new Student("Bob", 75), new Student("Charlie", 82) ); List<Student> sortedStudents = students.stream() .sorted(Comparator.comparing(Student::getScore).reversed()) .collect(toList()); sortedStudents.forEach( student -> System.out.println(student.getName() + " - " + student.getScore())); } static class Student implements Comparable<Student> { private String name; private int score; public Student(String name, int score) { this.name = name; this.score = score; } public String getName() { return name; } public int getScore() { return score; } @Override public int compareTo(Student other) { return Integer.compare(this.score, other.score); } } } // Alice - 90 // Charlie - 82 // Bob - 75 |
Mapをソートする
Mapのソートではkeyまたはvalueのいずれでソートするかを指定します。
以下の例ではvalueで昇順ソートしていますが、Map.Entry.comparingByKey() を使用することでkeyによるソートが可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import java.util.*; import static java.util.stream.Collectors.toMap; public class MapSortExample { public static void main(String[] args) { Map<String, Integer> unsortedMap = new HashMap<>(); unsortedMap.put("Alice", 90); unsortedMap.put("Bob", 75); unsortedMap.put("Charlie", 82); Map<String, Integer> sortedMap = unsortedMap.entrySet().stream() .sorted(Map.Entry.comparingByValue()) .collect(toMap( Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); sortedMap.forEach( (key, value) -> System.out.println(key + " - " + value)); } } // Bob - 75 // Charlie - 82 // Alice - 90 |
Stream.sorted()以外のソート方法
Javaのリストをソートする方法はいくつかありますが、特に Stream.sorted()
と Collections.sort()
の違いや、 parallelStream().sorted()
を使った場合のパフォーマンスについて解説します。
Collections.sort() と Stream.sorted() の違い
Collections.sort()
- 直接リストを変更する 破壊的な ソート
List<T>
のメソッドではなく、Collections
クラスの静的メソッドとして提供。- 内部的に
TimSort
(計算量 O(n log n)) を使用
Stream.sorted()
- 非破壊的な ソートで、新しいストリームを返す
Stream<T>
のメソッドとして提供
実装例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; public class SortComparison { public static void main(String[] args) { // Collections.sort() List<Integer> collectionNumbers = Arrays.asList(5, 3, 8, 1, 2); Collections.sort(collectionNumbers); System.out.println("Collections.sort(): " + collectionNumbers); // Stream.sorted() List<Integer> streamNumbers = Arrays.asList(5, 3, 8, 1, 2); List<Integer> list2 = streamNumbers.stream().sorted().collect(Collectors.toList()); System.out.println("Stream.sorted(): " + list2); System.out.println("streamNumbers: " + streamNumbers); } } |
1 2 3 4 |
// 出力結果 Collections.sort(): [1, 2, 3, 5, 8] // 元のリストの並び順が変ってしまう Stream.sorted(): [1, 2, 3, 5, 8] streamNumbers: [5, 3, 8, 1, 2] // Streamだと変わらない |
parallelStream().sorted() とのパフォーマンス比較
parallelStream()
を使うと、複数のスレッドで並列処理される可能性がある。- しかし、中間結果を最終的に結合してソートするため並列処理としてのパフォーマンス向上を得られないこともある。
- データ量や構造にも依存するので、ベンチマークを取って採用するかを検討した方がいい。
実装例
100万個の整数をソートするケース
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import java.util.List; import java.util.Random; import java.util.stream.Collectors; import java.util.stream.IntStream; public class parallelStreamBenchMark { public static void main(String[] args) { // 100万個のランダムな整数 Random rand = new Random(); List<Integer> numbers = IntStream.range(0, 1_000_000) .map(i -> rand.nextInt()) .boxed() .collect(Collectors.toList()); // 計測用変数 long startTime = 0L; long endTime = 0L; // 通常のsortedのパフォーマンス startTime = System.nanoTime(); var sorted = numbers.stream().sorted().collect(Collectors.toList()); endTime = System.nanoTime(); System.out.println("sorted : " + (endTime - startTime)/ 1_000_000 + " ms"); // parallelStream.sortedのパフォーマンス startTime = System.nanoTime(); var parallelStreamSorted = numbers.parallelStream().sorted().collect(Collectors.toList()); endTime = System.nanoTime(); System.out.println("sorted : " + (endTime - startTime)/ 1_000_000 + " ms"); // ソート結果は同じ assert sorted.equals(parallelStreamSorted); } } |
1 2 3 |
// 出力結果 sorted : 543 ms parallel sorted : 296 ms |
- 既存のリストを変更したいなら
Collections.sort()
- 非破壊的に処理をしたいなら
Stream.sorted()
- 高パフォーマンスを得たいなら
parallelStream().sorted()
を試す
Stream.sorted() の時間計算量
- 時間計算量: O(n log n)
- 最終的に
Arrays.sort(T[])
を使用し、オブジェクト型 (T[]
) のソートでは TimSort が適用される。
まとめ
- 基本的なソートはStreamのソートではsortedという中間操作を行う。
- String、Integerにおいては単にsortedを指定するだけでよい。
- 昇順は特に指定不要だが、降順ではComparator.reverseOrder()を指定する。
- 複数条件の場合、thenComparingで条件を追加していく。
- nullを含む場合は、Comparator.nullsFirst または Comparator.nullsLastを使用する。
- 独自クラスをソートする場合は、Comparableを実装するか、Comparatorを指定する
- Comparableが実装されている場合でも、Comparatorの指定が優先される。
- Mapのソートではkeyかvalueのいずれでソートするかを指定する。
- ソートには Collections.sort() と parallelStream.sorted() もあるので、用途に合わせて使うと良い。
コメント