【Java】Listの結合で考える破壊的操作と非破壊的操作


概要

JavaでListを結合する方法を現役エンジニアが解説【初心者向け】の記事にある通り、JavaのListで結合を行う方法は何通りかあるのですが、どれも同じに見えて実は違ったりします。大きな違いとしては、それが破壊的操作か非破壊的操作かという点。この違いを意識しないと、バグを生みやすいコードを書いてしまいかねないと私は考えてますので、今回記事に書いてみたいと思います。

破壊的操作と非破壊的操作

Rubyの記事になってしまうのですが、破壊的と非破壊的の違いは破壊的メソッドと非破壊的メソッドの話に分かりやすくまとめらています。すごーくざっくり言うと、メソッドを呼び出した時に操作対象自身に変更がかかる操作が破壊的、操作対象自身には変更はかからず新しくオブジェクトが生成される操作が非破壊的なものになります。(厳密に言うと違うかもしれませんが、あくまで分かりやすいイメージとして)

JavaのList結合で試す

JavaのList結合の話に戻ると、破壊的なメソッドの代表例がaddAllで、非破壊的なメソッドの代表例がStreamのconcatメソッドです。以下のサンプルソースで試してみました。

ListTest.java
public class ListTest {

    public static void main(String[] args){
        // 破壊的な結合
        List<Integer> aList = new ArrayList<>();
        aList.add(1);
        aList.add(2);
        List<Integer> bList = new ArrayList<>();
        bList.add(3);
        bList.add(4);
        aList.addAll(bList);
        System.out.println(aList); // [1, 2, 3, 4]
        System.out.println(bList); // [3, 4]

        // 非破壊的な結合
        List<Integer> cList = Arrays.asList(1, 2);
        List<Integer> dList = Arrays.asList(3, 4);
        // Arrays.asListの初期化で破壊的な結合を行うとexception
        // cList.addAll(dList);
        // 新しいオブジェクトを生成
        List<Integer> eList = Stream.concat(cList.stream(), dList.stream())
                .collect(Collectors.toList());
        System.out.println(cList); // [1, 2]
        System.out.println(dList); // [3, 4]
        System.out.println(eList); // [1, 2, 3, 4]
    }
}

addAllメソッドだと追加先になるaListの内容が変わるのが分かると思います。逆にconcatメソッドだと元のcListには変化を与えず、eListで結合した結果が得られます。ちなみに、Arrays.asListで生成されたオブジェクトはJava - Arrays.asList の注意点の記事にあるとおり、要素の数を変えられないので、破壊的な変更に関して一定のガードをかけられるという利点もあります。

どっちを使うべきか

個人的には、基本的に非破壊的なメソッドを使う方がおすすめです。破壊的なメソッドをむやみに濫用すると、値の変化が追えなくなり、バグを生みやすくなるからです。もちろん、破壊的なメソッドを使った方がシンプルに処理をかけるケースもあると思うので、特定のメソッド内やブロックで使うことは全然ありだと思います。その時は変更をかけるソースコードの範囲が分かるように、きちんと切り出すことが大事と感じます。