LoginSignup
11
13

More than 5 years have passed since last update.

意外と深いJavaのリストの反転 ~ Stream処理まで

Posted at

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を破壊的に変更するので注意してください。
ちなみにCollectionsListSetなどのコレクションを操作する処理が集まっています。例えば、shufflesortなどがあります。

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は集計処理を記述するメソッドになります。

  1. ArrayList::newは集計のためのクラスを決める。
  2. (l, e) -> l.add(0, e)は集計処理の本体。ここでリストlの先頭に要素を追加する。
  3. (l, subl) -> l.addAll(0, subl)は並列実行時のまとめの処理。第1引数にまとめること。(並列処理時はリストを適当に区切って処理され、それぞれ集計結果が出力される。この複数の結果を統合する必要がある。)

これはやりたいことが単純な割に記述が面倒です。本当は以下のように書きたいです。

List<String> reversedList = strs.stream().collect(MyCollectors.reverse());

この場合は以下のようなCollectionの実装クラスを用意します。

MyCollectors.java
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を使う
1. Dequeを使う
2. StreamAPIを使う
個別の処理にあった方法を使う必要がありそうです。

11
13
2

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
11
13