【Java】StreamAPIの機能を使いこなす

2022年6月1日

今回はラムダ式を使用する時に、よく利用するjava.util.streamjava.util.functionパッケージのAPIの使い方を見ていきます。これらのAPIは開発工数の削減に役立つので是非とも動きを覚えておきたいですね。

StreamAPIの基本的な挙動に関してはこちらで紹介しています。

内容

事前にテストで使用するGameCharacterクラスを作成し、クラス内にはAPIで利用するためのListを作成するメソッドを定義しておきます。これを利用して動きをみていくことにしましょう。

GameCharacter.java
import java.util.List;

//名前、HP、MP、敵か仲間のフラグを持つキャラクタークラス
public class GameCharacter {

	public String name;
	public int hp;
	public int mp;
	public boolean isEnemy;
	public GameCharacter(String name, int hp, int mp, boolean isEnemy){
		this.name = name;
		this.hp = hp;
		this.mp = mp;
		this.isEnemy = isEnemy;
	}
	
	//内容を表示するメソッド
	public String toString() {
		return name+":{hp="+hp+",mp="+mp+","+(isEnemy ? "敵" : "仲間")+"}";
	}
	
	//キャラクターリスト生成メソッド
	public static List<GameCharacter> createList(){
		return List.of(
				new GameCharacter("勇者",100,50,false)
				,new GameCharacter("戦士",150,30,false)
				,new GameCharacter("魔王",300,200,true)
				,new GameCharacter("格闘家",180,0,false)
				,new GameCharacter("スライム",10,0,true)
				,new GameCharacter("オーク",60,0,true)
				,new GameCharacter("リビングデッド",50,150,true)
				,new GameCharacter("ペット",10,10,false)
		);
	}
}

紹介する内容では、createListを呼び出してList取得、Stream化してからの操作で実行していく流れになります。基本的にはList.streamで実行していきますが、結果を変えるために並列ストリームを生成するparallelStreamを使用するパターンも含まれます。

※parallelやunorderedを使用すると結果が大きく変化する(説明とは異なる動きをする)メソッドもありますのでご注意ください。



各要素に対し、特定処理を実行するforEach

forEachメソッドを利用すると各要素に対して何らかの処理を実行できます。

次の処理はリスト内のキャラの情報を表示する処理です。

import java.util.List;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//各要素を表示する
		charaList.stream().forEach(System.out::println);
	}
}

実行結果
勇者:{hp=100,mp=50,仲間}
戦士:{hp=150,mp=30,仲間}
魔王:{hp=300,mp=200,敵}
格闘家:{hp=180,mp=0,仲間}
スライム:{hp=10,mp=0,敵}
オーク:{hp=60,mp=0,敵}
リビングデッド:{hp=50,mp=150,敵}
ペット:{hp=10,mp=10,仲間}

特定要素に絞り込むfilter

filterメソッドを利用すると要素の検索を実行できます。

次の処理はHP100以上のキャラ一覧を表示し、そのあとに敵キャラ一覧を表示します。

import java.util.List;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//HP100以上のキャラクターを表示する
		System.out.println("--------HP100以上--------");
		charaList.stream()
			.filter(e -> e.hp > 100)
			.forEach(System.out::println);
 
		System.out.println("--------HP100以上--------");
		System.out.println("--------敵キャラ--------");
		
		//敵キャラクターを表示する
		charaList.stream()
			.filter(e ->e.isEnemy)
			.forEach(System.out::println);
		System.out.println("--------敵キャラ--------");
	}
}

実行結果
--------HP100以上--------
戦士:{hp=150,mp=30,仲間}
魔王:{hp=300,mp=200,敵}
格闘家:{hp=180,mp=0,仲間}
--------HP100以上--------
--------敵キャラ--------
魔王:{hp=300,mp=200,敵}
スライム:{hp=10,mp=0,敵}
オーク:{hp=60,mp=0,敵}
リビングデッド:{hp=50,mp=150,敵}
--------敵キャラ--------

要素を別の要素に変換するmap

mapメソッドを利用すると要素の変換を実行できます。

次の処理はキャラ一覧をキャラの名前一覧に変換してから内容を表示します。

import java.util.List;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//キャラクターの名前のみ表示する
		charaList.stream()
			.map(e -> e.name)
			.forEach(System.out::println);
	}
}

実行結果
勇者
戦士
魔王
格闘家
スライム
オーク
リビングデッド
ペット

要素をさらに細かく分割し変換するflatMap

flatMapは特定の要素をさらに細かい要素に分離したい場合に使えます。

import java.util.List;
import java.util.stream.Stream;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//キャラクターオブジェクトをname,hp,mpの要素に分離してstream化したものを表示
		charaList.stream()
			.flatMap(e-> Stream.of(e.name,e.hp,e.mp))
			.forEach(System.out::println);
	}
}
実行結果
勇者
100
50
戦士
150
30
魔王
300
200
格闘家
180
0
スライム
10
0
オーク
60
0
リビングデッド
50
150
ペット
10
10

最初の要素を取得するfindFirst

findFirstは最初の要素を取得できます。

import java.util.List;
import java.util.Optional;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//最初の要素を取得
		Optional<GameCharacter> data = charaList.parallelStream()
			.findFirst();

		System.out.println(data);
	}
}
実行結果
Optional[勇者:{hp=100,mp=50,仲間}]

要素を取得するfindAny

findAnyはいずれかの要素を取得できます。

import java.util.List;
import java.util.Optional;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//要素を取得
		Optional<GameCharacter> data = charaList.parallelStream()
			.findAny();
		
		System.out.println(data);
	}
}
実行結果
Optional[オーク:{hp=60,mp=0,敵}]

要素の集合同士を連結するconcat

concatメソッドはstream同士を結合できます。

import java.util.List;
import java.util.stream.Stream;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//Streamの結合
		Stream.concat(charaList.stream(), Stream.of(new GameCharacter("新人",1,1,false)))
			.forEach(System.out::println);
	}
}
実行結果
勇者:{hp=100,mp=50,仲間}
戦士:{hp=150,mp=30,仲間}
魔王:{hp=300,mp=200,敵}
格闘家:{hp=180,mp=0,仲間}
スライム:{hp=10,mp=0,敵}
オーク:{hp=60,mp=0,敵}
リビングデッド:{hp=50,mp=150,敵}
ペット:{hp=10,mp=10,仲間}
新人:{hp=1,mp=1,仲間}

要素を並び替えるsorted

要素の並び替えを行う場合はsortedを使用します。

サンプルプログラムはHPが大きい順に並べ替えたときの結果になります。

import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//HPの降順に並び替えて表示
		charaList.stream()
			.sorted((c1,c2) -> - (c1.hp - c2.hp))
			.forEach(System.out::println);
	}
}
実行結果
魔王:{hp=300,mp=200,敵}
格闘家:{hp=180,mp=0,仲間}
戦士:{hp=150,mp=30,仲間}
勇者:{hp=100,mp=50,仲間}
オーク:{hp=60,mp=0,敵}
リビングデッド:{hp=50,mp=150,敵}
スライム:{hp=10,mp=0,敵}
ペット:{hp=10,mp=10,仲間}



条件に一致する要素が存在するか判定するanyMatch

anyMatchは条件に一致する要素が存在する場合にtrueを返します。

また、要素がない場合はfalseを返します。

import java.util.List;
import java.util.stream.Stream;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//mp100以上はいるか
		boolean isMp100 = charaList.stream()
				.anyMatch(e -> e.mp >= 100);
		System.out.println("mp100以上はいるか:"+isMp100);
		
		//mp0未満はいるか
		boolean isMpMinus = charaList.stream()
				.anyMatch(e -> e.mp < 0);
		System.out.println("mp0未満はいるか:"+isMpMinus);

		//空の場合
		System.out.println("要素が空の場合:"+Stream.<GameCharacter>empty().anyMatch(e -> e.isEnemy));
		
	}
}
実行結果
mp100以上はいるか:true
mp0未満はいるか:false
要素が空の場合:false

すべての要素が条件に一致するか判定するallMatch

allMatchはすべての要素が条件に一致する場合にtrueを返します。

また、要素がない場合はtrueを返します。

import java.util.List;
import java.util.stream.Stream;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//全員mp100以上か
		boolean isMp100 = charaList.stream()
				.allMatch(e -> e.mp >= 100);
		System.out.println("全員mp100以上か:"+isMp100);
		
		//全員hp10以上か
		boolean isHp10 = charaList.stream()
				.allMatch(e -> e.hp >= 10);
		System.out.println("全員hp10以上か:"+isHp10);
 
		//空の場合
		System.out.println("要素が空の場合:"+Stream.<GameCharacter>empty().allMatch(e -> e.isEnemy));
		
	}
}
実行結果
全員mp100以上か:false
全員hp10以上か:true
要素が空の場合:true

条件に一致する要素が存在しないか判定するnoneMatch

noneMatchは条件に一致する要素が存在しない場合にtrueを返します。

また、要素がない場合はtrueを返します。

import java.util.List;
import java.util.stream.Stream;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//mp300以上が存在しないか
		boolean isMp300 = charaList.stream()
				.noneMatch(e -> e.mp >= 300);
		System.out.println("mp300以上が存在しないか:"+isMp300);
		
		//hp10以上が存在しないか
		boolean isHp10 = charaList.stream()
				.noneMatch(e -> e.hp >= 10);
		System.out.println("hp10以上が存在しないか:"+isHp10);

		//空の場合
		System.out.println("要素が空の場合:"+Stream.<GameCharacter>empty().noneMatch(e -> e.isEnemy));
		
	}
}
実行結果
mp300以上が存在しないか:true
hp10以上が存在しないか:false
要素が空の場合:true

各要素の処理結果でtrueが返された要素のStreamを返すtakeWhile​​

通常の順次ストリームであれば最初の要素から順に処理し、Predicateの結果がtrueのデータを含むStreamを返します。途中でfalseとなった場合以降の要素はすべて削除されます。

import java.util.List;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//最初のfalseが見つかるまでの要素を表示
		charaList.stream()
			.takeWhile(e -> e.mp < 100)
			.forEach(e -> System.out.println(e.name));
		
	}
}
実行結果
勇者
戦士

要素の順はこんな感じでしたので魔王(MP200)の処理がfalseを返し魔王以降のデータが削除された結果になったようです。

				new GameCharacter("勇者",100,50,false)
				,new GameCharacter("戦士",150,30,false)
				,new GameCharacter("魔王",300,200,true)
				,new GameCharacter("格闘家",180,0,false)
				,new GameCharacter("スライム",10,0,true)
				,new GameCharacter("オーク",60,0,true)
				,new GameCharacter("リビングデッド",50,150,true)
				,new GameCharacter("ペット",10,10,false)

各要素の処理結果でtrueが返された要素を削除したStreamを返すdropWhile​​

通常の順次ストリームであれば最初の要素から順に処理し、Predicateの結果がtrueのデータを削除したStreamを返します。途中でfalseとなった場合以降の要素の削除は行いません。

import java.util.List;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//最初のfalseが見つかった以降の要素を表示
		charaList.stream()
			.dropWhile(e -> e.mp < 100)
			.forEach(e -> System.out.println(e.name));
		
	}
}
実行結果
魔王
格闘家
スライム
オーク
リビングデッド
ペット

要素の順はこんな感じでしたので魔王(MP200)の処理がfalseを返し、勇者と戦士だけが削除された結果になりました。

				new GameCharacter("勇者",100,50,false)
				,new GameCharacter("戦士",150,30,false)
				,new GameCharacter("魔王",300,200,true)
				,new GameCharacter("格闘家",180,0,false)
				,new GameCharacter("スライム",10,0,true)
				,new GameCharacter("オーク",60,0,true)
				,new GameCharacter("リビングデッド",50,150,true)
				,new GameCharacter("ペット",10,10,false)

途中で特定処理を実行するpeek

peekは操作の途中で処理を実行することができます。

import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//途中で処理をねじ込む
		long cnt = charaList.stream()
			.peek(System.out::println)
			.count();
		System.out.println(cnt);
	}
}
実行結果
8

あれ?内容が表示されない・・・

どうやらStreamの最適化にはまったようです。

countは処理を実行しなくても数の確定が可能である場合は中間処理を行わないようですね。

最適化がされないように少しコードを改変しましょうか。

import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//途中で処理をねじ込む
		long cnt = charaList.stream()
			.filter(e -> !e.isEnemy)
			.peek(System.out::println)
			.count();
		System.out.println(cnt);
	}
}
実行結果
勇者:{hp=100,mp=50,仲間}
戦士:{hp=150,mp=30,仲間}
格闘家:{hp=180,mp=0,仲間}
ペット:{hp=10,mp=10,仲間}
4

なぜこのような処理になったのかはStreamAPIの挙動についての記事をご覧ください。

要素の結合的累積操作を行えるreduce

reduceは各要素の連結処理や比較処理を行った結果を取得できます。

import java.util.List;
import java.util.Optional;
import java.util.function.BinaryOperator;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//文字列結合処理を行い表示
		Optional<String> names = charaList.stream()
			.map(e -> e.name)
			.reduce(String::concat);

		System.out.println("--------名前連結--------");
		System.out.println(names);
		System.out.println("--------名前連結--------");

		//比較処理を行い表示
		Optional<GameCharacter> result = charaList.stream()
			.reduce(BinaryOperator.maxBy((e1,e2)->e1.hp - e2.hp));

		System.out.println("--------最大HPのキャラ--------");
		System.out.println(result);
		System.out.println("--------最大HPのキャラ--------");
	}
}
実行結果
--------名前連結--------
Optional[勇者戦士魔王格闘家スライムオークリビングデッドペット]
--------名前連結--------
--------最大HPのキャラ--------
Optional[魔王:{hp=300,mp=200,敵}]
--------最大HPのキャラ--------

要素を切り詰めるlimit

limitは指定した数だけの最初の要素数のみの状態にします。

import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//最初の3要素までにする
		charaList.stream()
			.limit(3)
			.forEach(System.out::println);
	}
}
実行結果
勇者:{hp=100,mp=50,仲間}
戦士:{hp=150,mp=30,仲間}
魔王:{hp=300,mp=200,敵}

最初の要素を破棄するskip

skipは指定された数だけ最初の要素から削除します。

import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//最初の3要素スキップ
		charaList.stream()
			.skip(3)
			.forEach(System.out::println);
	}
}
実行結果
格闘家:{hp=180,mp=0,仲間}
スライム:{hp=10,mp=0,敵}
オーク:{hp=60,mp=0,敵}
リビングデッド:{hp=50,mp=150,敵}
ペット:{hp=10,mp=10,仲間}



重複要素を削除するdistinct​

distinctは重複データを排除します。

同じ要素かの判定はequalshashCodeで行われるので、必要であれば各種メソッドのオーバーライドで判定を定義しておきましょう。

GameCharacterクラスは、これらのメソッドをオーバーライドしていないので、全く同じ参照を要素に追加して試したいと思います。

import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		GameCharacter chara1 = new GameCharacter("勇者", 10,10, false);
		GameCharacter chara2 = new GameCharacter("魔王", 10,10, true);
		List<GameCharacter> disCharas = List.of(chara1,chara2,chara2,chara2);
		
		//重複データを表示
		System.out.println("--------------重複データ--------------");
		disCharas.stream().forEach(System.out::println);
		System.out.println("--------------重複データ--------------");
		
		//distinct実行
		System.out.println("--------------distinct実行--------------");
		disCharas.stream()
			.distinct()
			.forEach(System.out::println);
		System.out.println("--------------distinct実行--------------");
	}
}
実行結果
--------------重複データ--------------
勇者:{hp=10,mp=10,仲間}
魔王:{hp=10,mp=10,敵}
魔王:{hp=10,mp=10,敵}
魔王:{hp=10,mp=10,敵}
--------------重複データ--------------
--------------distinct実行--------------
勇者:{hp=10,mp=10,仲間}
魔王:{hp=10,mp=10,敵}
--------------重複データ--------------

equals、hashCodeが定義されているようなクラス(StringやInteger型など)の場合はしっかり値での判定になっていますのでご安心ください。

試しにString型で参照ではなく値で重複排除されるか見てみましょう。

import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		List<String> disCharas = List.of(new String("a"),new String("b"),new String("a"));
		
		//重複データを表示
		System.out.println("--------------重複データ--------------");
		disCharas.stream().forEach(System.out::println);
		System.out.println("--------------重複データ--------------");
		
		//distinct実行
		System.out.println("--------------distinct実行--------------");
		disCharas.stream()
			.distinct()
			.forEach(System.out::println);
		System.out.println("--------------distinct実行--------------");
	}
}
実行結果
--------------重複データ--------------
a
b
a
--------------重複データ--------------
--------------distinct実行--------------
a
b
--------------distinct実行--------------

繰り返しStreamを構築するiterate

iterateを使用すると無限ループを行うStreamや条件指定でループを終了する時のような挙動をするStreamを作成できます。

まずは無限ループのStreamのサンプルを見てみましょう。

import java.util.stream.Stream;
 
public class Test {
 
	public static void main(String[] args) {
		
		//無限ストリーム
		Stream.iterate(0,i->i+1)
			.forEach(i -> {
				try {Thread.sleep(100);} catch(Exception ex) {}
				System.out.println(i);
			});
	}
}
実行結果
0
1
2
3
4
...

※無限ループなので途中で強制停止してください。

無限ストリームはtakeWhileやlimitで止めることができます。

import java.util.List;
import java.util.stream.Stream;
 
public class Test {
 
	public static void main(String[] args) {
		
		System.out.println("--------limitで止める--------");
		
		//limitによって止める
		Stream.iterate(0, i -> i + 1)
			.limit(3)
			.forEach(System.out::println);
		
		System.out.println("--------limitで止める--------");
		

		System.out.println("--------takeWhileで止める--------");

		List<GameCharacter> charaList = GameCharacter.createList();
		
		//takeWhileによって止める
		Stream.iterate(0, i -> i + 1)
			.takeWhile(i -> i < 2)
			.peek(e-> System.out.print(e+"::"))
			.map(i -> charaList.get(i).name)
			.forEach(System.out::println);

		System.out.println("--------takeWhileで止める--------");
	}
}
実行結果
--------limitで止める--------
0
1
2
--------limitで止める--------
--------takeWhileで止める--------
0::勇者
1::戦士
--------takeWhileで止める--------

条件によって終了させるようにiterateメソッドで指定することもできます。

import java.util.List;
import java.util.stream.Stream;
 
public class Test {
 
	public static void main(String[] args) {
 
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//hasNextによって処理を続行するか判定する有限ストリーム
		Stream.iterate(0, i -> i < charaList.size(), i -> i + 1)
			.map(i -> charaList.get(i).name)
			.forEach(System.out::println);
	}
}
実行結果
勇者
戦士
魔王
格闘家
スライム
オーク
リビングデッド
ペット

なんかfor文みたいに使えちゃいます。

最大要素を取得するmax

maxを利用すると何かしらの最大要素を取得できます。何を最大として判定するかはComparatorで実装します。

import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//一番文字数の長いキャラを表示
		charaList.stream()
				.max((c1,c2)->c1.name.length() - c2.name.length())
				.ifPresent(System.out::println);
	}
}
実行結果
リビングデッド:{hp=50,mp=150,敵}

最小要素を取得するmin

minを利用すると何かしらの最小要素を取得できます。何を最小として判定するかはComparatorで実装します。

import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//一番文字数の短いキャラを表示
		charaList.stream()
				.min((c1,c2)->c1.name.length() - c2.name.length())
				.ifPresent(System.out::println);
	}
}
実行結果
勇者:{hp=100,mp=50,仲間}

要素の個数を数えるcount

countは要素数を取得します。

import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//要素の個数表示
		long cnt = charaList.stream()
				.count();
		System.out.println(cnt);
		
	}
}
実行結果
8

要素の合計を取得するsum

mapTo系メソッドで数値ストリームに変換するとsumメソッドが使用できます。

sumメソッドは値の合計を返します。

import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//全キャラのHPの合計を表示
		long sum = charaList.stream()
				.mapToInt(e -> e.hp)
				.sum();
		System.out.println(sum);
		
	}
}
実行結果
860

要素の平均を取得するaverage

mapTo系メソッドで数値ストリームに変換するとaverageメソッドが使用できます。

averageメソッドは値の平均を返します。

import java.util.List;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//全キャラのHPの平均を表示
		charaList.stream()
				.mapToInt(e -> e.hp)
				.average()
				.ifPresentOrElse(System.out::println, () -> System.out.println("平均演算を実行できない"));
		
	}
}
実行結果
107.5



配列化するtoArray

toArrayを利用してStreamを配列化します。

import java.util.Arrays;
import java.util.List;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//キャラクターの名前のみを配列化する
		String[] names = charaList.stream()
			.map(e -> e.name)
			.toArray(String[]::new);
		
		//配列内の名前を表示する
		System.out.println(Arrays.toString(names));
	}
}
実行結果
[勇者, 戦士, 魔王, 格闘家, スライム, オーク, リビングデッド, ペット]

List化するCollectors.toList

collectメソッドを使用してCollectors.toListを使用するとStreamをList化できます。

import java.util.List;
import java.util.stream.Collectors;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//Listに変換
		List<GameCharacter> data = charaList.stream()
			.collect(Collectors.toList());
		
		//List内のキャラを表示する
		System.out.println(data);
	}
}
実行結果
[勇者:{hp=100,mp=50,仲間}, 戦士:{hp=150,mp=30,仲間}, 魔王:{hp=300,mp=200,敵}, 格闘家:{hp=180,mp=0,仲間}, スライム:{hp=10,mp=0,敵}, オーク:{hp=60,mp=0,敵}, リビングデッド:{hp=50,mp=150,敵}, ペット:{hp=10,mp=10,仲間}]

Set化するCollectors.toSet

collectメソッドを使用してCollectors.toSetを使用するとStreamをSet化できます。

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//Setをに変換
		Set<GameCharacter> data = charaList.stream()
				.collect(Collectors.toSet());
		
		//Set内のキャラを表示する
		System.out.println(data);
		
	}
}
実行結果
[魔王:{hp=300,mp=200,敵}, 戦士:{hp=150,mp=30,仲間}, ペット:{hp=10,mp=10,仲間}, 勇者:{hp=100,mp=50,仲間}, リビングデッド:{hp=50,mp=150,敵}, 格闘家:{hp=180,mp=0,仲間}, スライム:{hp=10,mp=0,敵}, オーク:{hp=60,mp=0,敵}]

Map化するCollectors.toMap

collectメソッドを使用してCollectors.toMapを使用するとStreamをMap化できます。

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//Mapに変換
		Map<String, Integer> data = charaList.stream()
				.collect(Collectors.toMap(e -> e.name, e -> e.mp));
			
		//名前=MPのマップ情報を表示する
		System.out.println("-----------各キャラのMP-----------");
		data.forEach((k,v) -> System.out.println(k+"="+v));
		System.out.println("-----------各キャラのMP-----------");
	}
}
実行結果
-----------各キャラのMP-----------
オーク=0
ペット=10
格闘家=0
勇者=50
魔王=200
リビングデッド=150
戦士=30
スライム=0
-----------各キャラのMP-----------

文字列を連結させるCollectors.joining

collectメソッドを使用してCollectors.joiningを使用すると文字列Streamを結合できます。

import java.util.List;
import java.util.stream.Collectors;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//文字列をスラッシュ区切りで結合
		String names = charaList.stream()
				.map(e -> e.name)
				.collect(Collectors.joining("/"));
		
		//スラッシュ区切りの名前を表示する
		System.out.println(names);
	}
}
実行結果
勇者/戦士/魔王/格闘家/スライム/オーク/リビングデッド/ペット

true、falseでMap化する​Collectors.partisioningBy

collectメソッドとCollectors.partisioningByを利用することでtrueかfalseかのオブジェクトに分割したMapを生成できます。

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//敵か仲間かで分割したMapを作成
		Map<Boolean, List<GameCharacter>> partitioningMap = charaList.stream()
			.collect(Collectors.partitioningBy(e -> e.isEnemy));
		
		//敵か仲間でそれぞれ名前を表示する
		System.out.println("-----------仲間-----------");
		partitioningMap.get(false).stream().forEach(e -> System.out.println(e.name));
		System.out.println("-----------仲間-----------");
		System.out.println("-----------敵-----------");
		partitioningMap.get(true).stream().forEach(e -> System.out.println(e.name));
		System.out.println("-----------敵-----------");
	}
}
実行結果
-----------仲間-----------
勇者
戦士
格闘家
ペット
-----------仲間-----------
-----------敵-----------
魔王
スライム
オーク
リビングデッド
-----------敵-----------

特定データでグループ化Mapを作成するCollectors.groupingBy​

collectメソッドとCollectors.groupingByを利用することで特定データをキーにグループ化したMapを作成できます。

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//HP200以上、100以上、100未満で分割したMapを作成
		Map<String, List<GameCharacter>> groupingMap = charaList.stream()
			.collect(Collectors.groupingBy(e -> {
				if ( e.hp >= 200 ) {
					return "HP200以上";
				} else if ( e.hp >= 100 ){
					return "HP100以上";
				} else {
					return "HP100未満";
				}
			}));
		
		//グループごとの情報を表示する
		groupingMap.forEach((k,v)->System.out.println(k+"="+v));
	}
}
実行結果
HP100未満=[スライム:{hp=10,mp=0,敵}, オーク:{hp=60,mp=0,敵}, リビングデッド:{hp=50,mp=150,敵}, ペット:{hp=10,mp=10,仲間}]
HP100以上=[勇者:{hp=100,mp=50,仲間}, 戦士:{hp=150,mp=30,仲間}, 格闘家:{hp=180,mp=0,仲間}]
HP200以上=[魔王:{hp=300,mp=200,敵}]

さらに高度なグループ化を行った入れ子Mapの生成も有効利用できそうなため紹介しておきます。

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
 
public class Test {
 
	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//敵、仲間でグループ化したものをさらにHP100以上、100未満でグループ化したMapを作成
		Map<String, Map<String, List<GameCharacter>>> groupingMap = charaList.stream()
			.collect(
				Collectors.groupingBy(e -> e.isEnemy ? "敵" : "仲間"
					, Collectors.groupingBy(e -> e.hp >= 100 ? "HP100以上" : "HP100未満")
				)
			);
		
		//グループごとの情報を表示する
		groupingMap.forEach((k,v)->v.forEach((k2,v2)->System.out.println(k+"で"+k2+"="+v2)));
		
	}
}
実行結果
敵でHP100未満=[スライム:{hp=10,mp=0,敵}, オーク:{hp=60,mp=0,敵}, リビングデッド:{hp=50,mp=150,敵}]
敵でHP100以上=[魔王:{hp=300,mp=200,敵}]
仲間でHP100未満=[ペット:{hp=10,mp=10,仲間}]
仲間でHP100以上=[勇者:{hp=100,mp=50,仲間}, 戦士:{hp=150,mp=30,仲間}, 格闘家:{hp=180,mp=0,仲間}]

groupingByの第二引数はCollector型を指定することができ、Collectorsのfiltering、countingなど多様な処理をするCollectorオブジェクトを引数にして処理ができます。

Collector型を指定できる機能は他にもいくつか用意されており、複雑な処理を行う場合に覚えておくと役に立つかもしれません。

close時に特定の処理を実行するonClose

streamにはcloseメソッドが存在します。closeを呼び出すと、onCloseで指定した処理が呼び出される仕組みになっています。

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Test {

	public static void main(String[] args) {
		List<GameCharacter> charaList = GameCharacter.createList();
		
		//クローズ実行時にENDと表示
		try(Stream<GameCharacter> stream = charaList.parallelStream()){
			System.out.println(
				stream
				.onClose(()->System.out.println("END"))
				.map(e -> e.name)
				.collect(Collectors.joining(","))
			);
		}
		
	}
}
実行結果
勇者,戦士,魔王,格闘家,スライム,オーク,リビングデッド,ペット
END

closeがあるということはStreamを使用したら解放しなきゃいけないのかと思われてしまうかもしれませんが、この記事で紹介したような処理では実行する必要はありません。

ただしIO系の処理の時はリソース開放のために実行が必要です。(Files.linesなど)

使用頻度の高い、覚えておくと良いメソッドは?

まずは、forEachfiltermapanyMatchallMatchtoArrayだけ覚えとけば良いかと思います。

個人的によく使うのは上記に加えて、Collectors.toListCollectors.groupingByなので、開発するアプリケーションにもよるでしょうが、この辺も覚えておくと役に立つかもしれません。


関連記事

Java

Posted by nompor