47.戻り値の型としてストリームよりCollectionを選択すべし
連続した要素をどの型で返すべきか?
連続した要素の戻り値型としてあり得るのは、collectionインターフェース、Iterable、配列、ストリームである。
ストリームで返す
ストリームで返すのが善であると聞くかもしれないが、Item45で述べたように、ストリームとイテレーションをうまくすみ分けることが重要である。
StreamはIterableを継承していないため、ストリームとして返ってきた値をfor-each文でまわすのには、Streamのiteratorメソッドを使うしかない。
ぱっとみ以下のコードようにiteratorメソッド使ってうまくいくように思える。
// Won't compile, due to limitations on Java's type inference
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
// Process the process
}
しかし、このコードはコンパイルできず、以下のようにキャストをしてやる必要がある。
// Hideous workaround to iterate over a stream
for (ProcessHandle ph : (Iterable<ProcessHandle>)
ProcessHandle.allProcesses()::iterator)
このコードは動くものの、乱雑で分かりにくい。
代替方法としては、アダプターメソッドを使う方法がある。JDKではそのようなメソッドは提供されていないが、以下のように簡単に書ける。
// Adapter from Stream<E> to Iterable<E>
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
このアダプターメソッドを使うことによって、ストリームに対して以下のようにfor-eachを回すことが可能となる。
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
// Process the process
}
Iterableで返す
逆に、クライアント側でストリームとして処理しようとしているのに、戻り値はIterableにのみ対応したものであるときも対応が必要である。
この対応もJDKでは用意されていないが、以下のようなに簡単に対応メソッドを書ける。
// Adapter from Iterable<E> to Stream<E>
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
Collectionで返す
CollectionインターフェースはIterableのサブタイプであり、ストリームのメソッドも持っているので、イテレーション処理でもストリーム処理でも対応できる。
そのため、連続した要素を返すメソッドの最適な戻り値型は、たいていの場合、Collectionか適切なCollectionのサブタイプであると言える。
もし返却する連続要素が十分小さければ、ArrayListやHashSetなどのCollectionを実装したものを返せばいいのだが、Collectionとして返却するために、大きな連続要素をメモリに蓄えることはすべきでない。
返却する連続要素が、大きいけれど簡潔に表現できるものであれば、特別にcollectionを実装してやることを考える。
例えば、与えられた集合のべき集合を返すような実装を考える。
べき集合とは、例えば、{a,b,c}のべき集合は、{{}, {a}, {b}, {c}, {a, b}, {a, c}, {b, c}, {a, b, c}}のようであり、n個の要素の集合があれば、2のn乗個のべき集合ができる。
とても大きな数の集合になるため、べき集合をスタンダードなcollectionの中に入れようと考えてはならない。
これを実現するカスタムcollectionは、AbstractListを使って簡単に実装することができる。
仕組みとしては、集合の各要素にインデックスを付して、それらが存在するかしないかをbitで判断する、というものである。コードは以下のようになる。
package tryAny.effectiveJava;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
//Returns the power set of an input set as custom collection
public class PowerSet {
public static final <E> Collection<Set<E>> of(Set<E> s) {
List<E> src = new ArrayList<>(s);
if (src.size() > 30)
throw new IllegalArgumentException("Set too big " + s);
return new AbstractList<Set<E>>() {
@Override
public int size() {
return 1 << src.size(); // 2 to the power srcSize
}
@Override
public boolean contains(Object o) {
return o instanceof Set && src.containsAll((Set) o);
}
@Override
public Set<E> get(int index) {
Set<E> result = new HashSet<>();
for (int i = 0; index != 0; i++, index >>= 1)
if ((index & 1) == 1)
result.add(src.get(i));
return result;
}
};
}
}
上記のコードでは、集合が30以上の要素を持っている場合には例外をスローさせるようにしている。
これはCollectionのsizeメソッドで返せる最大値が2の31乗-1であることによる。
どの型で返すかを、実装の難易度だけで決めることもある。
例えば、インプットのlistの全てのサブリストを返却するようなメソッドを書くような場合を考える。
サブリストを作り出して、それらを標準のcollectionに入れるコードは3行で書けるが、メモリが2次元構造のcollectionを保持しなくてはならない。
これは指数関数的に増えるべき集合に比べれば悪くないが、受け入れられない。
べき集合の時のようにカスタムcollectionを実装するのは面倒である。
しかし、リストの全サブリストをストリームで返すのは、少し工夫をすれば直接的に実装できる。
リストの最初の文字を保持しているサブリスト群をprefixesと呼ぶ。つまり、(a, b, c)のprefixesは(a), (a, b), (a, b, c)になる。
リストの最後の文字を保持しているサブリスト群をsuffixesと呼ぶ。つまり、(a, b, c)のsuffixesは(a, b, c), (b, c), (c)になる。
このとき、リストの全サブリストは、リストのprefixesのsuffixesと、空のリストである。実装は以下のようになる。
package tryAny.effectiveJava;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
//Returns a stream of all the sublists of its input list
public class SubLists {
public static <E> Stream<List<E>> of(List<E> list) {
return Stream.concat(Stream.of(Collections.emptyList()), prefixes(list).flatMap(SubLists::suffixes));
}
private static <E> Stream<List<E>> prefixes(List<E> list) {
return IntStream.rangeClosed(1, list.size()).mapToObj(end -> list.subList(0, end));
}
private static <E> Stream<List<E>> suffixes(List<E> list) {
return IntStream.range(0, list.size()).mapToObj(start -> list.subList(start, list.size()));
}
}
このコードは以下のような通常のforループのネストしたものと同様の考え方をしている。
for (int start = 0; start < src.size(); start++)
for (int end = start + 1; end <= src.size(); end++)
System.out.println(src.subList(start, end));
このforループをストリーム処理に直訳した形で表すと、簡潔にはなるが、可読性は落ちる。具体的には以下のようになる。
// Returns a stream of all the sublists of its input list
public static <E> Stream<List<E>> of(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start ->
IntStream.rangeClosed(start + 1, list.size())
.mapToObj(end -> list.subList(start, end)))
.flatMap(x -> x);
}
どちらの実装も悪くないが、使用するユーザーによってはストリームからイテレートできるように変換するコードが必用となる、または、イテレート処理が自然なところでストリーム処理を行わなければならないかもしれない。
ストリームからイテレートできるように変換するコードは、クライアントコードが乱雑になるだけでなく、Collectionでの実装に比べて、性能的にも問題がある。