架空の拡張-filter,mapをStreamを使わずに組み込むとしたら
Java8では、Stream APIにより、コレクションの関数型ライクな操作ができるようになりました。
mapのような処理が1行で書けるようになってうれしい反面、もうちょっとすっきりしたのが良かったな、という不満もあります。
ただ不満を言っているだけの人になるのも嫌なので、「もし、こういう風に実装したならば、あるいは」というのを考えてみました。
おさらい - Java8でStreamでfilterとmapを使う
既存のコードとの兼ね合いを考えると、入力も出力もList<String>
の場合が多いと思うので、その前提で。
List<String> input = Arrays.asList("Map", "List", "Collection", "Stream"); List<String> filtered = input.stream().filter(x -> x.length() >= 5).collect(Collectors.toList()); List<String> mapped = input.stream().map(String::toUpperCase).collect(Collectors.toList());
1行で書けるのはうれしいんですけど、1つのfilter/map
を使うために余分に3つものメソッド呼び出しを書かなければなりません。
List
にfilter/map
をつけてくれたら良かったのに、と思いました。
関数型ライブラリーを使えば良いんじゃないの?
FunctionalJavaのような、可能な限りの関数型の表現を実現しようとするライブラリーがありますが、これは新たなクラス体系を作っています。
できれば、コアライブラリーのJCFをそのまま使って実現したいし、あと、関数型のレイヤーが厚すぎて拡張されたJavaになってしまうのは避けたい。
関数型じゃなくて、あくまで、コア機能に直接組み込んでもそれほど違和感が無い程度のfilter/map
だけで良いんです。
とにかく、List
のfilter/map
だけで良いから、欲しい。
もし、標準ライブラリーに組み込むとしたら
java.util.List
とStream
の上位に、Sequence
のようなインターフェイスがあると綺麗になりそうですが。
Sequence
を導入したら、Sequence
には実装をほとんど書かないで、NonLazySequence
にdefault
メソッドを書く、といった感じでしょうか。
Sequence
(実在しない)Stream
NonLazySequence
(実在しない)List
上の方法だと実験が大掛かりになってしまうので、java.util.List
だけに実装するようにして、極力、java.util.List
以外を触らなくて良いように、default
メソッドかstatic
メソッドを追加するだけで拡張できる方法を考えてみました。
以下のインターフェイスは、List
に継承させる前提で書いています。実際はList
に直接追加しても良いかと思いますが、ここではコードを分割するためにも別のインターフェイスとしています。
なお、架空のコードなので、実在のクラス名にはYa
という接頭辞を付けています。
ListFunctionalStyleOperationSupport
インターフェイス
import java.util.function.*; interface ListFunctionalStyleOperationSupport<E> extends Iterable<E> { boolean add(E e); static <R> Supplier<? extends YaList<R>> getDefaultListConstructor() { return YaArrayList::new; } default YaList<E> filteredList(Predicate<E> pred) { return filteredList(getDefaultListConstructor(), pred); } default YaList<E> filteredList(Supplier<? extends YaList<E>> ctor, Predicate<E> pred) { YaList<E> a = ctor.get(); for (E o : this) if (pred.test(o)) a.add(o); return a; } default <R> YaList<R> mappedList(Function<E, R> mapper) { return mappedList(getDefaultListConstructor(), mapper); } default <R> YaList<R> mappedList(Supplier<? extends YaList<R>> ctor, Function<E, R> mapper) { YaList<R> a = ctor.get(); for (E o : this) a.add(mapper.apply(o)); return a; } }
まず、filter/map
を実現するには、元のコレクションを複製する手段が必要になります。少なくとも、
の操作ができるようになっていなければなりません。
最初のイテレートは、Iterable
を実装することで対応できます。List
の上位インターフェイスであるCollection
でもIterable
を実装しているので、直接List
にコードを追加するなら不要です。
インスタンスの生成は、デフォルトではArrayList
固定で。コンストラクター参照を使います。コンストラクターを指定したい場合は、引数が3のメソッドで、2番目にコンストラクター参照(たとえばLinkedList::new
)を渡します。オーバーライドは必須ではありませんが、可能であればサブクラス自体のインスタンスを返すのが相応しいと思われます。
値を追加するのは、add
メソッドで。これも、Collection
のものなので、直接List
にコードを追加するなら不要です。
あとは、filter/map
を書くだけ。ご覧のとおりです。
メソッド名がfilteredList/mappedList
なのは、Javaでは継承階層の途中で自分自身の型を戻り値の型にすることができない*1ので、Sequence
があった場合に名前がかぶってしまうのを避けるためです。
ちなみにScalaだと、オーバーライドしたメソッドの戻り値を変えることができるのでそれが可能になっています。(scala.collection.TraversableLike
、scala.collection.SeqLike
あたりを参照。)
実際にできる方法で
できないことばかりを言うのはこのくらいにして、現実的な解法を考えましょう。
ユーティリティーメソッドクラスを作ってしまうのが手っ取り早いでしょうか。
- ユーティリティーメソッドクラスの例(
FilterAndMapUtil
)
package lib; import java.util.*; import java.util.function.*; public final class FilterAndMapUtil { // E: element type, R: mapped element type, T: mapped List type private FilterAndMapUtil() { // } public static <E> List<E> filter(List<E> src, Predicate<E> pred) { return filter(src, ArrayList::new, pred); } public static <E, T extends List<E>> T filter(T src, Supplier<T> ctor, Predicate<E> pred) { T a = ctor.get(); for (E o : src) if (pred.test(o)) a.add(o); return a; } public static <E> E[] filter(E[] src, Predicate<E> pred) { final int n = src.length; E[] a = Arrays.copyOf(src, n); int index = 0; for (int i = 0; i < n; i++) if (pred.test(src[i])) a[index++] = src[i]; return Arrays.copyOf(src, index); } public static <E> List<E> map(List<E> src, Function<E, E> mapper) { return map(src, ArrayList::new, mapper); } public static <E, R, T extends List<R>> T map(List<E> src, Supplier<T> ctor, Function<E, R> mapper) { T a = ctor.get(); for (E o : src) a.add(mapper.apply(o)); return a; } public static <E> E[] map(E[] src, Function<E, E> mapper) { return map(src, Arrays.copyOf(src, src.length), mapper); } public static <E, R> R[] map(E[] src, IntFunction<R[]> ctor, Function<E, R> mapper) { return map(src, ctor.apply(src.length), mapper); } private static <E, R> R[] map(E[] src, R[] dst, Function<E, R> mapper) { assert (dst.length >= src.length); for (int i = 0; i < src.length; i++) dst[i] = mapper.apply(src[i]); return dst; } public static <E> List<E> filterMap(List<E> src, Function<E, Optional<E>> mapper) { return filterMap(src, ArrayList::new, mapper); } public static <E, R, T extends List<R>> T filterMap(List<E> src, Supplier<T> ctor, Function<E, Optional<R>> mapper) { T a = ctor.get(); for (E o : src) { Optional<R> r = mapper.apply(o); if (r.isPresent()) a.add(r.get()); } return a; } public static <E> E[] filterMap(E[] src, Function<E, Optional<E>> mapper) { E[] a = Arrays.copyOf(src, src.length); final int newSize = filterMap(src, a, mapper); return Arrays.copyOf(a, newSize); } public static <E, R> R[] filterMap(E[] src, IntFunction<R[]> ctor, Function<E, Optional<R>> mapper) { R[] a = ctor.apply(src.length); final int newSize = filterMap(src, a, mapper); return Arrays.copyOf(a, newSize); } private static <E, R> int filterMap(E[] src, R[] dst, Function<E, Optional<R>> mapper) { assert (dst.length >= src.length); int index = 0; for (int i = 0; i < src.length; i++) { Optional<R> v = mapper.apply(src[i]); if (v.isPresent()) dst[index++] = v.get(); } return index; } }
ここでは、filterMap
と、配列向けのオーバーロードメソッドも作ってみました。
- 使い方
// import static lib.FilterAndMapUtil.*; List<String> stringList = Arrays.asList("Map", "List", "Collection", "Stream"); String[] strings = { "Map", "List", "Collection", "Stream" }; List<String> filtered = filter(stringList, x -> x.length() >= 5); List<String> mapped = map(stringList, String::toUpperCase); Integer[] lengths = map(strings, Integer[]::new, x -> x.length());
あまりスマートとは言えないやり方ですが、少なくとも私にとっては、配列かList
でfilter/map
ができれば事足りるので、これで十分です。
*1:分かりやすく説明するのが難しい。継承関係が親子1世代しかない場合ならできます。