Java

【Java】StreamAPIについて理解をまとめる【groupingBy】

こんにちは、ともです。

Java8から導入されたStreamAPIを上手く使って、昔ながらのfor文などを完結に書けるようになりたいと考えています。

そんな場合のStreamAPIの知識をメモしておきます。何番煎じだよという感じかも知れませんが、メモしておきます。特にgroupingByが感動しました。

インスタンスを作成

Stream<>のインスタンスを生成するには2通りあります。

1つはofメソッドを利用する方法、もう一つはstream()を利用する方法です。

Stream<String> fluitsStream1 = Stream.of("apple", "orange", "banana");
List<String> fluitsList = Arrays.asList("apple", "orange", "banana");
Stream<String> fluitsStream2 = fluitsList.stream();

filterメソッドでフィルタリング

java.util.stream.Streamのfilterメソッドで絞り込みを行うことができます。

package stream.api;

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

public class MyStream {
	public static void main(String[] args) {
		Stream<String> fluitsStream = Stream.of("apple", "orange", "banana");

		// オレンジとリンゴだけ残す①
		List<String> filtered =fluitsStream.filter(fluit->"orange".equals(fluit) || "apple".equals(fluit)).collect(Collectors.toList());

		// 絞り込み結果の表示
		filtered.forEach(System.out::println);
	}
}

filterメソッドの引数はPredicate<? super T>のインスタンスです。

Predicateインタフェースは

引数の型 T(上記例ではList<String>のためTはString)
戻り値の型 Boolean

PredicateインタフェースがTrueを返した値のみ、collectメソッドに流れて行きます。よって”orange”と”apple”がcollectメソッドの引数としてやってきます。

forEachメソッドでループ処理

ループ処理をする場合にjava.util.stream.StreamのforEachメソッドが利用できます。

package stream.api;

import java.util.stream.Stream;

public class MyStream {
	public static void main(String[] args) {
		Stream<String> fluitsStream = Stream.of("apple", "orange", "banana");
		// 絞り込み結果の表示
		fluitsStream.forEach(System.out::println);
	}
}

forEachメソッドの引数は引数にConsumer<? super T>をとります。

引数の型 T(上記例ではList<String>のためTはString)
戻り値の型  void

Consumerは引数に何かを受け取って何も返さない、ただの消費者(Consumer)です。

mapで値を加工

値を加工して次のStreamに流したい場合はjava.util.stream.Streamのmapメソッドが利用できます。

次の例ではStream<Customer>からStream<Integer>に変更しています。

package stream.api;

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

class Customer {
	public int age;
	public String name;
	public Customer(int age, String name) {
		this.age = age;
		this.name = name;
	}
}
public class MyStream {
	public static void main(String[] args) {
		Customer customer1 = new Customer(20, "Bob");
		Customer customer2 = new Customer(25, "Tom");
		Stream<Customer> customerStream = Stream.of(customer1, customer2);
		// 年齢のみを以降のStreamに流す
		List<Integer> ageList = customerStream.map(customer->customer.age).collect(Collectors.toList());

		ageList.stream().forEach(System.out::println);// 20, 25
	}
}

mapの引数はFunction<? super T, ? extends R>となります。何らかをreturnするインタフェースです。

引数の型 T(上記例ではList<Customer>のためTはCustomer)
戻り値の型 R 何らかを返すこと 。mapメソッドの戻り値の型になります。

ソートしたい

流れてくるStreamをソートして次のStreamに流したい。

そんな時はjava.util.stream.Streamのsortedメソッドが利用できます。

次の例では年齢をソートして表示しています。

class Customer {
	public int age;
	public String name;

	public Customer(int age, String name) {
		this.age = age;
		this.name = name;
	}
}

public class MyStream {
	public static void main(String[] args) {
		Customer customer1 = new Customer(20, "Bob");
		Customer customer2 = new Customer(25, "Tom");
		Customer customer3 = new Customer(15, "Tim");
		Customer customer4 = new Customer(35, "Max");
		Customer customer5 = new Customer(5, "Jonny");
		Stream<Customer> customersStream = Stream.of(customer1, customer2, customer3, customer4, customer5);
		// 偶数のみを
		List<Integer> ageList = customersStream.map(customer->customer.age).sorted().collect(Collectors.toList());

		ageList.stream().forEach(System.out::println);//5, 15, 20, 25, 35
	}
}

sortedメソッドはオーバーロードされています。

sortedの引数の型 やってくれること
void 自然順序(Comparable)で整列
Comparator<? super T> Comparatorの比較結果でソート

上記例では引数がないため、自然順序整列で昇順になっています。

この辺りはこのTeraTailの質問への回答が参考になりました。

Comparatorは様々な比較方法を用意しています。自作のComparatorインタフェースのインスタンスを生成することはあまりないかも知れませんね。

Comparator(Java SE 11)

自作するなら下記のようになります。

// 昇順
List<Integer> ageList = fluitsStream.map(customer->customer.age).sorted( (age1, age2) -> {
	return age1 - age2;
}).collect(Collectors.toList());

Comparatorインタフェースの比較関数を使うなら下記のようになります。

// 昇順
List<Integer> ageList = fluitsStream.map(customer->customer.age).sorted(Comparator.naturalOrder()).collect(Collectors.toList());
// 降順
List<Integer> ageList = fluitsStream.map(customer->customer.age).sorted(Comparator.reverseOrder()).collect(Collectors.toList());

最大値・最小値を求めたい

そんな時はjava.util.stream.Streamのmaxメソッド・minメソッドを利用しましょう。

次の例では年齢が最年少の人・最年長の人を取得しています。

package stream.api;

import java.util.Optional;
import java.util.stream.Stream;

class Customer {
	public int age;
	public String name;

	public Customer(int age, String name) {
		this.age = age;
		this.name = name;
	}
}

public class MyStream {
	public static void main(String[] args) {
		Customer customer1 = new Customer(20, "Bob");
		Customer customer2 = new Customer(25, "Tom");
		Customer customer3 = new Customer(15, "Tim");
		Customer customer4 = new Customer(35, "Max");
		Customer customer5 = new Customer(5, "Jonny");
		Stream<Customer> customerStream1 = Stream.of(customer1, customer2, customer3, customer4, customer5);
		Stream<Customer> customerStream2 = Stream.of(customer1, customer2, customer3, customer4, customer5);
		// 最年長
		Optional<Customer> oldestCustomer = customerStream1.max((cus1, cus2) -> cus1.age - cus2.age);
		Optional<Customer> youngestCustomer = customerStream2.min((cus1, cus2) -> cus1.age - cus2.age);

		System.out.println("最年長" + oldestCustomer.get().name);// Max
		System.out.println("最年少" + youngestCustomer.get().name);// Jonny
	}
}

maxメソッドやminメソッドの引数はCompratorインタフェースです。

やっていることはComparatorで整列した後にmaxの場合は最後の値、minの場合は最初の値を取得しています。

存在チェックしたい

そんな時はjava.util.stream.StreamのanyMatchやnoneMatchメソッドが利用できます。

下記の例では、Tomという名前のCustomerが存在するか確認しています。

package stream.api;

import java.util.stream.Stream;

class Customer {
	public int age;
	public String name;

	public Customer(int age, String name) {
		this.age = age;
		this.name = name;
	}
}

public class MyStream {
	public static void main(String[] args) {
		Customer customer1 = new Customer(20, "Bob");
		Customer customer2 = new Customer(25, "Tom");
		Customer customer3 = new Customer(15, "Tim");
		Customer customer4 = new Customer(35, "Max");
		Customer customer5 = new Customer(5, "Jonny");
		Stream<Customer> customerStream = Stream.of(customer1, customer2, customer3, customer4, customer5);
		// 最年長
		if(customerStream.anyMatch(cus -> "Tom".equals(cus.name))) {//TRUE
			System.out.println("存在します。");
		} else {
			System.out.println("存在しません。");
		}
	}
}

Tomは存在するのでTrueが返ってきました。

anyMatchの引数はPredicate<? super T>です。filterの引数で出てきましたね。

反対に存在しない場合はnoneMatchが利用できます。

終端処理で好きな形にまとめる

streamで一通りの処理をして、最後に好きな形でまとめたい(collect)なと思いますよね。そんな時はjava.util.stream.Streamのcollectメソッドを利用しましょう。

様々なまとめ方がCollectorsに用意されています。先ほどから書いているCollectors.toList()はListにまとめてくれていました。下記がその例。

List<Integer> ageList = customersStream.map(customer->customer.age).collect(Collectors.toList());

他にはどのようなまとめ方があるのでしょうか。

Collectors(Java SE 11)を確認しましょう。

toList List<T>に蓄積して欲しい
toSet Set<T>に蓄積して欲しい
groupingBy Mapに蓄積して欲しい
joinning 文字列を結合させて欲しい
maxBy 最大値の物が欲しい
minBy 最小値の物が欲しい

その他色々あります。toListとtoSetは自明なので省略して、groupingBy, joinning, maxBy, minByについて例を出します。

groupingByでマップに蓄積

同じ年齢のCustomerだけを集めたMapを作成してみます。keyが年齢でvalueがList<Customer>です。

package stream.api;

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

class Customer {
	public int age;
	public String name;

	public Customer(int age, String name) {
		this.age = age;
		this.name = name;
	}
}

public class MyStream {
	public static void main(String[] args) {
		Customer customer1 = new Customer(10, "Bob");
		Customer customer2 = new Customer(25, "Tom");
		Customer customer3 = new Customer(25, "Tim");
		Customer customer4 = new Customer(15, "Max");
		Customer customer5 = new Customer(15, "Jonny");
		Stream<Customer> customerStream = Stream.of(customer1, customer2, customer3, customer4, customer5);
		Map<Object, List<Customer>> collected = customerStream.collect(Collectors.groupingBy(c->c.age, Collectors.toList()));
		for(Object age: collected.keySet()) {
			System.out.println(age+"才の人一覧");
			collected.get(age).stream().forEach(a->System.out.println(a.name));
		}		
	}
}

この結果は下記のようになります。

25才の人一覧
Tom
Tim
10才の人一覧
Bob
15才の人一覧
Max
Jonny

joinningで文字列として

文字列を結合させて欲しい場合もあるかと思います。その場合はjoinningを利用してみましょう。Cutomerの名前を色々な形式で取得してみました。

package stream.api;

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

class Customer {
	public int age;
	public String name;

	public Customer(int age, String name) {
		this.age = age;
		this.name = name;
	}
}

public class MyStream {
	public static void main(String[] args) {
		Customer customer1 = new Customer(20, "Bob");
		Customer customer2 = new Customer(25, "Tom");
		Customer customer3 = new Customer(15, "Tim");
		Customer customer4 = new Customer(35, "Max");
		Customer customer5 = new Customer(5, "Jonny");
		Stream<Customer> customerStream1 = Stream.of(customer1, customer2, customer3, customer4, customer5);
		Stream<Customer> customerStream2 = Stream.of(customer1, customer2, customer3, customer4, customer5);
		Stream<Customer> customerStream3 = Stream.of(customer1, customer2, customer3, customer4, customer5);

		String nameList = customerStream1.map(cus -> cus.name).collect(Collectors.joining());
		System.out.println(nameList);//BobTomTimMaxJonny

		nameList = customerStream2.map(cus -> cus.name).collect(Collectors.joining(","));
		System.out.println(nameList);//Bob,Tom,Tim,Max,Jonny

		nameList = customerStream3.map(cus -> cus.name).collect(Collectors.joining(",", "【", "】"));
		System.out.println(nameList);//【Bob,Tom,Tim,Max,Jonny】
	}
}
Collectors.joining ただ結合するだけ
Collectors.joining(“,”) コンマ区切り
Collectors.joining(“,”, “【”, “】”) コンマ区切り、先頭に【、末尾に】

CSVにしたい場合とかに使えそうですね。

maxBy, minByで最大値・最小値

package stream.api;

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

class Customer {
	public int age;
	public String name;

	public Customer(int age, String name) {
		this.age = age;
		this.name = name;
	}
}

public class MyStream {
	public static void main(String[] args) {
		Customer customer1 = new Customer(20, "Bob");
		Customer customer2 = new Customer(25, "Tom");
		Customer customer3 = new Customer(15, "Tim");
		Customer customer4 = new Customer(35, "Max");
		Customer customer5 = new Customer(5, "Jonny");
		Stream<Customer> customerStream1 = Stream.of(customer1, customer2, customer3, customer4, customer5);
		Stream<Customer> customerStream2 = Stream.of(customer1, customer2, customer3, customer4, customer5);

		Optional<Customer> oldestCustomer = customerStream1.collect(Collectors.maxBy((cus1, cus2) ->cus1.age -cus2.age));
		System.out.println(oldestCustomer.get().name + ", " + oldestCustomer.get().age);// Max, 35

		Optional<Customer> youngestCustomer = customerStream2.collect(Collectors.minBy((cus1, cus2) ->cus1.age -cus2.age));
		System.out.println(youngestCustomer.get().name + ", " + youngestCustomer.get().age);// Jonny, 5
	}
}

streamのmax, minと違いはありませんね。

まとめ

StreamAPIの色々なメソッドについてまとめました。ちょっとしてfor文を回す時、それstreamで出来るんじゃないかな?と考えると意外と見つかるかも知れません。

また何か使えるメソッドがあれば随時追記していきたいと思います。