ソースコード
toCollection
ArrayList<String> list = Stream.of("a", "b", "c").collect(Collectors.toCollection(ArrayList::new));
LinkedList<String> linkedList = Stream.of("a", "b", "c").collect(Collectors.toCollection(LinkedList::new));
Stream型を任意のListに変換する。
Java17からは、Stream.toList
を使ったほうがいい。
List<String> list = Stream.of("a", "b", "c").toList();
toSet
Set<String> set = Stream.of("a", "b", "c").collect(Collectors.toSet());
Stream -> Setに変換する。
toUnmodifiableSet
Set<String> set = Stream.of("a", "b", "c").collect(Collectors.toSet());
set.add("d"); //OK
Set<String> unmodifiableSet = Stream.of("a", "b", "c").collect(Collectors.toUnmodifiableSet());
unmodifiableSet.add("d"); //UnsupportedOperationException
名前の通り、変更不可のSetを生成する。
add
自体はできるが、UnsupportedOperationException
が発生する。
joining
var s1 = Stream.of("a", "b", "c");
var s2 = Stream.of("a", "b", "c");
var s3 = Stream.of("a", "b", "c");
var j1 = s1.collect(Collectors.joining());
var j2 = s2.collect(Collectors.joining(","));
var j3 = s3.collect(Collectors.joining(",", "[", "]"));
System.out.println(j1); //abc
System.out.println(j2); //a,b,c
System.out.println(j3); //[a,b,c]
Stream -> Stringにjoinする。
delimiter, prefix, suffixもオプションで設定可能。
mapping
record Person(String name, int age, String sex) {}
var persons = List.of(
new Person("Tom", 20, "Male"),
new Person("John", 22, "Male"),
new Person("Jessy", 20, "Female"),
new Person("Hanako", 30, "Female")
);
Map<String, List<String>> mappedToList = persons.stream()
.collect(
Collectors.groupingBy(
Person::sex,
Collectors.mapping(Person::name, Collectors.toList())
)
);
mappedToList.forEach((key, value) -> System.out.printf("%s=%s%n", key, value));
//Female=[Jessy, Hanako]
//Male=[Tom, John]
Map<String, String> mappedToString = persons.stream()
.collect(
Collectors.groupingBy(
Person::sex,
Collectors.mapping(Person::name, Collectors.joining("|"))
)
);
mappedToString.forEach((key, value) -> System.out.printf("%s=%s%n", key, value));
//Female=Jessy|Hanako
//Male=Tom|John
Collectors.groupingBy
した結果のvalueの方を任意の形にマッピングする。
flatMapping
record Product (String name, int value) {}
record Selling (String clientName, List<Product> products){}
var sellings = List.of(
new Selling("clientA", List.of(
new Product("p1-1", 11),
new Product("p1-2", 12),
new Product("p1-3", 13)
)
),
new Selling("clientB", List.of(
new Product("p2-1", 21),
new Product("p2-2", 22),
new Product("p2-3", 23),
new Product("p2-4", 24)
)
),
new Selling("clientC", List.of(
new Product("p3-1", 31),
new Product("p3-2", 32)
)
)
);
Map<String, List<Product>> flatMapped = sellings.stream()
.collect(Collectors.groupingBy(Selling::clientName,
Collectors.flatMapping(selling -> selling.products.stream(), Collectors.toList())));
flatMapped.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
Map<String, List<List<Product>>> mapped = sellings.stream()
.collect(Collectors.groupingBy(Selling::clientName,
Collectors.mapping(Selling::products, Collectors.toList())));
mapped.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
//clientC=[Product[name=p3-1, value=31], Product[name=p3-2, value=32]]
//clientB=[Product[name=p2-1, value=21], Product[name=p2-2, value=22], Product[name=p2-3, value=23], Product[name=p2-4, value=24]]
//clientA=[Product[name=p1-1, value=11], Product[name=p1-2, value=12], Product[name=p1-3, value=13]]
Collectors.groupingBy
した結果がListのネストになりうる場合に、flatなListにできる。
参考:https://retheviper.github.io/posts/java-new-methods-from-9-to-11/#flatmapping-9
filtering
record Employee (String name, String department, int salary) {}
var employees = List.of(
new Employee("Tom", "d1", 10000),
new Employee("John", "d2", 20000),
new Employee("Michael", "d2", 20000),
new Employee("Bob", "d3", 30000)
);
Map<String, List<Employee>> filtered = employees.stream().collect(Collectors.groupingBy(
Employee::department, Collectors.filtering(e -> e.salary >= 20000, Collectors.toList())));
filtered.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
// d1=[]
// d2=[Employee[name=John, department=d2, salary=20000], Employee[name=Michael, department=d2, salary=20000]]
// d3=[Employee[name=Bob, department=d3, salary=30000]]
groupingByした結果をfilterできる。
collectingAndThen
List<String> list = Stream.of("a", "b", "c").collect(
Collectors.collectingAndThen(Collectors.toList(),
e -> e.stream().map(String::toUpperCase).toList())
);
list.forEach(System.out::println);
// A
// B
// C
collectした結果に対して更に操作を加えることができる。
counting
var count = Stream.of("a", "b", "c").collect(Collectors.counting());
System.out.println(count);
// 3
Streamの個数を返す。
var count = (Long) Stream.of("a", "b", "c").count();
でいい。
minBy
record Entity(String key, String seq) {}
var entities = List.of(
new Entity("k1", "0001"),
new Entity("k1", "0003"),
new Entity("k1", "0002"),
new Entity("k2", "0002"),
new Entity("k2", "0001")
);
Map<String, Optional<Entity>> map = entities.stream()
.collect(Collectors.groupingBy(Entity::key, Collectors.minBy(Comparator.comparing(Entity::seq))));
map.forEach((key, value) -> System.out.printf("%s=%s%n", key, value));
collect結果から最小値だけを取り出す
maxBy
record Entity(String key, String seq) {}
var entities = List.of(
new Entity("k1", "0001"),
new Entity("k1", "0003"),
new Entity("k1", "0002"),
new Entity("k2", "0002"),
new Entity("k2", "0001")
);
Map<String, Optional<Entity>> map = entities.stream()
.collect(Collectors.groupingBy(Entity::key, Collectors.maxBy(Comparator.comparing(Entity::seq))));
map.forEach((key, value) -> System.out.printf("%s=%s%n", key, value));
collect結果から最小値だけを取り出す
summingInt
var sum = Stream.of(1, 2, 3).collect(Collectors.summingInt(Integer::intValue));
System.out.println(sum);
// 6
合計値をIntで返す。
var sum = (Integer) Stream.of(1, 2, 3).mapToInt(Integer::intValue).sum();
でええやろ、とIDEから警告が出るので、あまり使うことはない?
averagingInt
var ave = Stream.of(1, 2, 3).collect(Collectors.averagingInt(Integer::intValue));
System.out.println(ave);
// 2.0
平均値を出す。返り値はDouble
reducing
reducing(T identity, BinaryOperator<T> op)
TBD
reducing(BinaryOperator<T> op)
record Person(String name, String city, int height){}
var people = List.of(
new Person("Tom", "NY", 170),
new Person("John", "NY", 180),
new Person("Bob", "LA", 175),
new Person("Michael", "LA", 172)
);
var byHeight = Comparator.comparing(Person::height);
Map<String, Optional<Person>> tallestByCity = people.stream().collect(
Collectors.groupingBy(Person::city,
Collectors.reducing(BinaryOperator.maxBy(byHeight))));
tallestByCity.forEach((k, v) -> System.out.printf("%s=%s", k, v));
// LA=Optional[Person[name=Bob, city=LA, height=175]]NY=Optional[Person[name=John, city=NY, height=180]]
引数のBinaryOperator<T> op
に畳み込み関数を渡すことで、groupingByした結果を畳み込む。
Javadocに以下の記載がある。
API Note:
The reducing() collectors are most useful when used in a multi-level reduction, downstream of groupingBy or partitioningBy. To perform a simple reduction on a stream, use Stream.reduce(BinaryOperator) instead.
シンプルなreduceならStream APIのreduceを使ったほうがいいよ、とのこと。
reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)
var byLength = Comparator.comparing(String::length);
Map<String, String> longestNameByCity = people.stream().collect(
Collectors.groupingBy(Person::city,
Collectors.reducing("",
Person::name,
BinaryOperator.maxBy(byLength))));
longestNameByCity.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
// LA=Michael
// NY=John
最後のMapのvalueの型をStringにできる。
groupingBy
record Person(String name, String city, int height){}
var people = List.of(
new Person("Tom", "NY", 170),
new Person("John", "NY", 180),
new Person("Bob", "LA", 175),
new Person("Michael", "LA", 172)
);
Map<String, List<Person>> tallestByCity = people.stream().collect(Collectors.groupingBy(Person::city));
tallestByCity.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
// LA=[Person[name=Bob, city=LA, height=175], Person[name=Michael, city=LA, height=172]]
// NY=[Person[name=Tom, city=NY, height=170], Person[name=John, city=NY, height=180]]
オブジェクトのリストに対して、オブジェクトの任意のプロパティでGROUP BYする。
返り値がMap<String, List<Person>>
とやや扱いづらいので、必要に応じて前述のmapping
, flatMapping
などで任意の型に整形する。
groupingBy
した結果に更に処理を加えたい場合はmaxBy
などで処理する。
groupingByConcurrent
record Person(String name, String city, int height){}
var people = List.of(
new Person("Tom", "NY", 170),
new Person("John", "NY", 180),
new Person("Bob", "LA", 175),
new Person("Michael", "LA", 172)
);
ConcurrentMap<String, List<Person>> groupingBy1 = people.stream().collect(Collectors.groupingByConcurrent(Person::city));
groupingBy1.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
// LA=[Person[name=Bob, city=LA, height=175], Person[name=Michael, city=LA, height=172]]
// NY=[Person[name=Tom, city=NY, height=170], Person[name=John, city=NY, height=180]]
ConcurrentMapを返すgroupingBy
partitioningBy
record Person(String name, String city, int height){}
var people = List.of(
new Person("Tom", "NY", 170),
new Person("John", "NY", 180),
new Person("Bob", "LA", 175),
new Person("Michael", "LA", 172)
);
Map<Boolean, List<Person>> partitioningBy1 = people.stream()
.collect(Collectors.partitioningBy(person -> person.height >= 175));
partitioningBy1.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
// false=[Person[name=Tom, city=NY, height=170], Person[name=Michael, city=LA, height=172]]
// true=[Person[name=John, city=NY, height=180], Person[name=Bob, city=LA, height=175]]
Map<Boolean, Long> partitioningBy2 = people.stream()
.collect(Collectors.partitioningBy(person -> person.height >= 175, Collectors.counting()));
partitioningBy2.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
// false=2
// true=2
streamに対してPredicateによるfilteringを実行しMap<Boolean, List<Person>>
で返す。
第2引数を指定することで、返り値を少し使いやすくもできる。
toMap
record Person(String name, String city, int height){}
var people = List.of(
new Person("Tom", "NY", 170),
new Person("John", "NY", 180),
new Person("Bob", "LA", 175),
new Person("Michael", "LA", 172)
);
Map<String, String> map = people.stream().collect(Collectors.toMap(
Person::city,
Person::name,
(oldValue, newValue) -> newValue // key重複時は後勝ちでPUT
));
map.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
// LA=Michael
// NY=John
streamをmapにする。
第3引数でkey重複時の挙動をきちんと定義しておく必要がある。
toUnmodifiableMap
record Person(String name, String city, int height){}
var people = List.of(
new Person("Tom", "NY", 170),
new Person("John", "NY", 180),
new Person("Bob", "LA", 175),
new Person("Michael", "LA", 172)
);
Map<String, String> map = people.stream().collect(Collectors.toUnmodifiableMap(
Person::city,
Person::name,
(oldValue, newValue) -> newValue // key重複時は後勝ちでPUT
));
map.put("hoge", "fuga"); //UnsupportedOperationException
immutableなmapを生成する。
putしようとすると、UnsupportedOperationExceptionが発生する。
toConcurrentMap
record Person(String name, String city, int height){}
var people = List.of(
new Person("Tom", "NY", 170),
new Person("John", "NY", 180),
new Person("Bob", "LA", 175),
new Person("Michael", "LA", 172)
);
ConcurrentMap<String, String> map = people.stream().collect(Collectors.toConcurrentMap(
Person::city,
Person::name,
(oldValue, newValue) -> newValue // key重複時は後勝ちでPUT
));
map.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
// LA=Michael
// NY=John
streamからConcurrentMap(スレッドセーフなMap)を生成する。
【番外編】concurrentMap
@Test
@DisplayName("Mapの場合")
void mapTest() throws Exception {
var map = new HashMap<String, Integer>();
var sumList = parallelSum100(map);
assertNotEquals(1, sumList
.stream()
.distinct()
.count());
var wrongResultCount = sumList
.stream()
.filter(num -> num != 100)
.count();
assertTrue(wrongResultCount > 0);
}
@Test
@DisplayName("ConcurrentHashMapの場合")
void concurrentMapTest() throws Exception {
var map = new ConcurrentHashMap<String, Integer>();
var sumList = parallelSum100(map);
assertEquals(1, sumList
.stream()
.distinct()
.count());
var wrongResultCount = sumList
.stream()
.filter(num -> num != 100)
.count();
assertEquals(0, wrongResultCount);
}
private List<Integer> parallelSum100(Map<String, Integer> map) throws InterruptedException {
List<Integer> sumList = new ArrayList<>(1000);
for (int i = 0; i < 100; i++) {
map.put("test", 0);
var executorService = Executors.newFixedThreadPool(4);
for (int j = 0; j < 10; j++) {
executorService.execute(() -> {
for (int k = 0; k < 10; k++)
map.computeIfPresent(
"test",
(key, value) -> value + 1
);
});
}
executorService.shutdown();
executorService.awaitTermination(5, TimeUnit.SECONDS);
sumList.add(map.get("test"));
}
return sumList;
}
参考:
summarizingInt
IntSummaryStatistics intSummaryStatistics = Stream.of(1, 2, 3).collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(intSummaryStatistics);
// IntSummaryStatistics{count=3, sum=6, min=1, average=2.000000, max=3}
streamをIntSummaryStatisticsに入れ込む。