今回は、Javaの革新に焦点を当てます。 つまり、ImmutableCollectionsについてです。 おそらくどこかでList.of()を既に使用しているでしょう。 これらの方法には実用的な価値がないため、テストで最も可能性が高くなります。 しかし、テストでも、ありふれた落とし穴に出くわすことがあります。 一度コードを読むと、すべてが一度に所定の位置に収まるので、それらはありふれていますが、これがなぜそうであり、異なっていないのか、非常に多くの質問があります。
静的機能を持つListインターフェイスから始めましょう。
11もの!!!
List<E> List<E>.<E>of(E e1); List<E> List<E>.<E>of(E e1, E e2); List<E> List<E>.<E>of(E e1, E e2, E e3); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4, E e5); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4, E e5, E e6); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4, E e5, E e6, E e7); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10); List<E> List<E>.<E>of(E... elements)
Javaにはなぜ多くのメソッドがあるのでしょうか? 長い間、私はこの質問をしましたが、私の手は、ソースコードを調べてグーグルで検索する方法には決して達しませんでした。 今日、私は完全に不満な答えを見つけました。
vararg呼び出しの場合の配列割り当て、初期化、およびガベージコレクションのオーバーヘッドを節約するために、固定引数オーバーロードメソッドが提供されます。
それにもかかわらず、非常に論理的で、オブジェクト、メモリ、ガベージコレクターの作業が少ないように思えます。 ただし、テストで全般的に使用すると、どのような違いが生じますか。 しかし、実際のところ、この答えは間違っていると思います。
これらのメソッドのコードを見ると、次のことがわかります。
static <E> List<E> of() { return ImmutableCollections.List0.instance(); } static <E> List<E> of(E e1) { return new ImmutableCollections.List1<>(e1); } static <E> List<E> of(E e1, E e2) { return new ImmutableCollections.List2<>(e1, e2); } static <E> List<E> of(E e1, E e2, E e3) { return new ImmutableCollections.ListN<>(e1, e2, e3); } static <E> List<E> of(E e1, E e2, E e3, E e4) { return new ImmutableCollections.ListN<>(e1, e2, e3, e4); } /* ... ... */ static <E> List<E> of(E... elements) { switch (elements.length) { // implicit null check of elements case 0: return ImmutableCollections.List0.instance(); case 1: return new ImmutableCollections.List1<>(elements[0]); case 2: return new ImmutableCollections.List2<>(elements[0], elements[1]); default: return new ImmutableCollections.ListN<>(elements); } }
最初の3つのメソッドは、List0、List1、List2クラスの奇妙なオブジェクトを返します。 他のすべてはListNを返します。 そして、可変引数を使用する最後のメソッドは、リストされたリストのいずれかを返すことができます。 3要素から10要素までを実際にメソッドに引数を送信し、このデータはどの配列にもラップされません。
すべては順調に思えますが、さらに掘り下げてみましょう。 ListNコンストラクターの実装を見てみましょう<>
ListN(E... input) { @SuppressWarnings("unchecked") E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input for (int i = 0; i < input.length; i++) { tmp[i] = Objects.requireNonNull(input[i]); } this.elements = tmp; }
ご覧のとおり、varargs構文はすでに存在しています。 そしてこれは、配列にラップする最初の呼び出し中に、コンストラクターを呼び出す場合に、これが発生することを回避することが可能であったとしても意味します。 そして、なぜそのような実装が必要でしたか? この質問はまだ私に開かれています。 コメントの誰かが言ったら嬉しいです。
次に、これらのコレクションの落とし穴について説明します。 これらのコレクションの実装を内側から見てみましょう。
すべての不変リストの先頭には:
abstract static class AbstractImmutableList<E> extends AbstractList<E> implements RandomAccess, Serializable
この抽象コレクションのすべてのメソッドは、UnsupportedOperationExceptionをスローします。 このクラスでは、AbstractListのすべての抽象メソッドが有効というわけではありません。 したがって、私はコードを囲んでいます:
@Override public boolean add(E e) { throw uoe(); } @Override public boolean addAll(Collection<? extends E> c) { throw uoe(); } @Override public boolean addAll(int index, Collection<? extends E> c) { throw uoe(); } @Override public void clear() { throw uoe(); } @Override public boolean remove(Object o) { throw uoe(); } @Override public boolean removeAll(Collection<?> c) { throw uoe(); } @Override public boolean removeIf(Predicate<? super E> filter) { throw uoe(); } @Override public void replaceAll(UnaryOperator<E> operator) { throw uoe(); } @Override public boolean retainAll(Collection<?> c) { throw uoe(); } @Override public void sort(Comparator<? super E> c) { throw uoe(); }
つまり、たとえば、containsAllメソッドは、正常に使用した他のすべてのコレクションと同じ実装を持つことになりますが、常にそうとは限りません。 しかし、今はそれについてではありません。
List0、List1、List2、およびListNクラスは、AbstractImmutableListクラスを継承します。 各クラスはメソッドの一部を実装します。
たとえば、クラスList0を取り上げます。 containsメソッドはNullPointerExceptionをスローできます。
@Override public boolean contains(Object o) { Objects.requireNonNull(o); return false; }
これは非常に予想外です。 なぜ常に常にfalseを返すことができなかったのですか? ここにnullチェックがあるのはなぜですか。 私には理解できないままです。
containsAllメソッドの動作は、通常のリストと同じです。
public boolean containsAll(Collection<?> o) { return o.isEmpty(); // implicit nullcheck of o }
確かに、通常のリストのようにfor eachループではなく、isEmpty()メソッドの呼び出しによりNPEがポップアップします。
ソースコードで、私は気分を高め、マシン(コンパイラーに関するもの)が人間よりもどれだけ馬鹿げているかを思い出させるコメントに気付きました。
@Override public E get(int index) { Objects.checkIndex(index, 0); // always throws IndexOutOfBoundsException return null; // but the compiler doesn't know this }
List1に進みましょう。 他にも質問があります。 コンストラクターから始めましょう。
List1(E e0) { this.e0 = Objects.requireNonNull(e0); }
リストにnullを格納できないのはなぜですか? ここのロジックは何ですか? さらに進みましょう。 containsメソッドは引き続きNPEをスローします。
@Override public boolean contains(Object o) { return o.equals(e0); // implicit nullcheck of o }
ここではより論理的ですが コンストラクターがnullを含むリストの作成を許可しない場合、これは予想されます。 しかし、書くことを妨げたのは:
return o != null && o.equals(e0);
またははるかにきれい:
return e0.equals(o);
繰り返しますが、e0をnullにすることはできません。 containsAllメソッドの実装は、AbstractCollectionクラスにあります。
public boolean containsAll(Collection<?> c) { for (Object e : c) if (!contains(e)) return false; return true; }
コレクションがnullの場合、通常のコレクションと同じNPEを取得します。 しかし、パラメータのコレクションにnullが含まれる場合もNPEを取得します。これは、このNPEが提供するcontainsメソッドへの依存関係があるためです。
NPEは非常に危険でなければなりません。
List<String> list = new ArrayList<>(); list.add("FOO"); list.add(null); List<String> immutableList = List.of("foo", "bar"); immutableList.containsAll(list);
この場合、リストに文字列「FOO」が含まれていないため、NPEをキャッチしません。 すぐに誤った応答を受け取ります。 ArrayListで最初の要素が「foo」の場合、すぐにNPEをキャッチします。 したがって、このような状況では非常に注意してください。
List2とListNは同じです。
書かれたことを要約すると、まだいくつか質問があります。 これらのコレクションが通常のArrayList、LinkedListと同じように動作しないのはなぜですか? コレクションにnullを含めることができない理由。 なぜそんなに多くのメソッドを作成するのですか? このコードは逆さまに投げられており、誰もそれに対処したくないですか? しかし、これらの質問に答えることはできないので、これらの新しい便利な機能に存在する落とし穴について知っていることをそのまま使用します。
追伸 SetとMapにも落とし穴があると思います。 しかし、私は彼らに後で連絡し、さらに数分の空き時間が現れます。