ラムダ式・streamAPIとの出会い
最近までは,大量のデータを成形するときは迷わずfor文,while文(もしくはIterator)で処理していました.
実際のコードではないですが,代わりに犬の名前のリストから10歳以上の犬の種類のリストを取得する処理を書いておきます.
List<String> dogTypes = new ArrayList<>();
//dogNames:犬の名前が入った List<String>
for(String dogName : dogNames){
Dog dog = //DBからdogエンティティの取得;
if(dog.getAge() >= 10){
dogTypes.add(dog.getDogType());
}
}
SQLで対処できそうなのは置いといて,
こんなコードを書いて満足していましたが,友人からもっと格好良い処理があると言われ彼のコードを見ると...
List<Dog> dogs = //DBから全てのdogエンティティの取得;
//dogNames:犬の名前が入った List<String>
List<String> dogTypes = dogs.stream()
.filter(dog -> dogNames.contains(dog.getName()) && dog.getAge() >= 10)
.toList();
forやifのネストが無く非常にスタイリッシュなコードになっていました.
・
・
この出会いからラムダ式とstreamAPIの勉強を並行して進めてきました.
先程のコードで言うとラムダ式とstreamAPIは以下の内容を表しています.
- ラムダ式 : .filter()の引数の記述方法
- streamAPI : .stream()から.toList()で行われている,リストを成形するための処理を提供
streamの使い方はわかりましたが内部で使用されているラムダ式そのものに関しての理解が浅かったため,streamを再現して感覚を掴もうと思い記事にまとめました.
streamAPIの基本機能を作る
大まかな流れとしては ①関数型インターフェースの実装 ②streamの処理を持つクラスの実装 ③使ってみる となります.
①関数型インターフェースの実装
Javaで用意されたインターフェースを参考に,新たに4つのインターフェースを作成しました.
//値を渡して処理し,処理結果はいらない時に使用
public interface ConsumerTest<T> {
//引数〇,戻り値×
public void accept(T t);
}
//TrueFalseの判定が欲しい時に使用
public interface PredicateTest<T> {
//引数〇,戻り値boolean
public boolean test(T t);
}
//値を渡して処理し,処理結果も欲しい時に使用
public interface FunctionTest<T, R> {
//引数〇,戻り値〇
public R apply(T t);
}
//2つの値を比較したい時に使用
public interface ComparatorTest<T> {
//引数〇,戻り値boolean
public boolean compare(T t1, T t2);
}
②streamの処理を持つクラスの実装
今回はstreamの中でfilter, sorted, map, toList,ついでにforEachも作成しました.
streamメソッドはほぼ見た目用になってます
実装方法として,自作のstream用クラスを元々のListクラスに継承させるのもワンチャンあると思いましたが,一旦今回はListとstreamの処理の両方を持つクラスを作成しました.
個人的にmapの部分で初めて引数だけでなく戻り値にジェネリクスを使ったので新鮮な気持ちでした.ジェネリクスありがたいですね.
import java.util.ArrayList;
public class Array<T> {
public ArrayList<T> list = new ArrayList<>();
//forEach
public void forEach(ConsumerTest<T> c) {
for(T listVal : list) {
c.accept(listVal);
}
}
//stream
public Array<T> stream(){
Array<T> newArray = new Array<>();
for(T listVal : list) {
newArray.list.add(listVal);
}
return newArray;
}
//toList
public ArrayList<T> toList(){
return this.list;
}
//filter
public Array<T> filter(PredicateTest<T> p){
Array<T> newArray = new Array<>();
for(T listVal : list) {
if(p.evaluate(listVal)) {
newArray.list.add(listVal);
}
}
return newArray;
}
//sorted
public Array<T> sorted(ComparatorTest<T> c){
for(int i=0; i < list.size() - 1; i++ ) {
for(int j=0; j < list.size() - 1 - i; j++ ) {
if(c.compare(list.get(j), list.get(j+1))) {
T tmp = list.get(j);
list.set(j, list.get(j+1));
list.set(j + 1, tmp);
};
}
}
return this;
}
//map
public <R> Array<R> map(FunctionTest<T, R> e){
Array<R> newArray = new Array<>();
for(T listVal : list) {
newArray.list.add(e.apply(listVal));
}
return newArray;
}
}
③使ってみる
forEachとstreamnの処理の2つを試してみました.
public static void main(String[] args) {
Array<String> strList = new Array<>();
strList.list.add("asffvdss");
strList.list.add("bbsfaavavb");
strList.list.add("cfc");
strList.list.add("ddasd");
strList.list.add("vavvdaadadavdb");
strList.list.add("b");
strList.list.add("dcsfasfasdd");
//forEach
System.out.println("-----------forEach-----------");
strList.forEach((str)->{
System.out.println(str);
});
//stream
System.out.println("-----------stream-----------");
System.out.println(
strList.stream()
.map(str -> str.length())
.filter(num -> num > 3)
.sorted((first, second) -> first > second)
.map(num -> {
String res = "";
int i = 0;
while(i<num) {
res += i;
i++;
}
return res;
})
.toList()
);
}
以下の結果が出力されました.上手く動いてそうですね.
-----------forEach-----------
asffvdss
bbsfaavavb
cfc
ddasd
vavvdaadadavdb
b
dcsfasfasdd
-----------stream-----------
[01234, 01234567, 0123456789, 012345678910, 012345678910111213]
実際に書いたことで,(関数型)インターフェースの実装クラスをラムダ式に置き換えているだけで,本質的にはただ作成したオブジェクトを引数に渡しているという感覚が掴めてきました.