Javaでリストを反転させるのが意外と難しかったのでメモ。
[1, 2, 3].reverse
rubyでは配列は↑のように簡単に反転します。
JavaもStreamAPIとかに標準で反転が搭載されているだろうと思っていましたが、
list.stream().reverse();
こんなことはできませんでした。
ストリーム関連いろいろ探してみましたが、見つからず、困ってしまいました。
とりあえず、for文でリストを逆から回して反転するリストを生成するという愚行に。。。
ということでリストを反転させて利用する3つの方法をまとめてみました。
1. 最も一般的なCollections.reverseを使う方法
標準ライブラリで用意されている最も標準的な方法です。
List<String> list = Arrays.asList("a", "b", "c", "d", "e", "f");
Collections.reverse(list);
// list = [f, e, d, c, b, a]
この処理はlist
を破壊的に変更するので注意してください。
ちなみにCollections
はList
やSet
などのコレクションを操作する処理が集まっています。例えば、shuffle
やsort
などがあります。
2. Dequeを使う方法
頻繁に右に左に構造を渡り歩くなら双方向リストに変えてしまうのがよいです。
Deque<String> deque = new LinkedList<>(list);
// 順
for(String s: deque) {
// sの処理
}
// 逆
Iterator<String> iter = deque.descendingIterator();
while(iter.hasNext()) {
String s = iter.next();
// sの処理
}
ただ、少しレガシーな書き方になってしまうのが難点です。。。
3. StreamAPIのCollectを使う方法
StreamAPIを使う方法もあります。Stream処理自体は基本的には要素の順番に依存しない、それぞれの要素で独立した処理が得意です。一方で順序関係が含まれる処理は不得意となります。ただし、順序は保証できるので、これを利用して反転させることができます。
// 少し長い。。。が、何度も逆順リストを得る処理が出現しないなら
List<String> reversedList = list.stream().collect(ArrayList::new, (l, e) -> l.add(0, e), (l, subl) -> l.addAll(0, subl));
collectは集計処理を記述するメソッドになります。
-
ArrayList::new
は集計のためのクラスを決める。 -
(l, e) -> l.add(0, e)
は集計処理の本体。ここでリストlの先頭に要素を追加する。 -
(l, subl) -> l.addAll(0, subl)
は並列実行時のまとめの処理。第1引数にまとめること。(並列処理時はリストを適当に区切って処理され、それぞれ集計結果が出力される。この複数の結果を統合する必要がある。)
これはやりたいことが単純な割に記述が面倒です。本当は以下のように書きたいです。
List<String> reversedList = strs.stream().collect(MyCollectors.reverse());
この場合は以下のようなCollectionの実装クラスを用意します。
public class MyCollectors {
public static <T> Collector<T, List<T>, List<T>> reverse() {
return new Collector<T, List<T>, List<T>>() {
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return (l, e) -> l.add(0, e);
}
@Override
public BinaryOperator<List<T>> combiner() {
return (l, subl) -> {
l.addAll(0, subl);
return l;
};
}
@Override
public Function<List<T>, List<T>> finisher() {
// 最終的な集計処理は必要ないので、そのままリストを返す
return l -> l;
}
@Override
public Set<Characteristics> characteristics() {
// 並列処理可
return EnumSet.of(Characteristics.CONCURRENT);
}
};
}
}
書くのは面倒ですが、1度書けば、stream処理で簡単に使うことができるようになります。
おまけ) 場合によっては反転したリストを生成しなくてもいい
反転したリスト自体が中間生成物の場合はリストを反転させる必要はありません。
例えば、文字列を逆順で結合したいなら、反転したリストを作る必要もなく、以下の処理で済みます。
List<String> strs = Arrays.asList("a", "b", "c", "d", "e", "f");
String reverse = strs.stream().reduce((e1, e2) -> e2.concat(e1)).get();
// reverse = "fedcba"
まとめ
Javaの場合はチェーンメソッドで一言だけで簡単にというわけにはいきませんが、主に以下の3つ方法がありました。
0. Java標準のCollections.reverseを使う
- Dequeを使う
- StreamAPIを使う
個別の処理にあった方法を使う必要がありそうです。