Trong số các tính năng mới xuất hiện của Java 8, Stream được xem như một yếu tố tác động mạnh mẽ tới việc viết code của lập trình viên Java.

Quá trình sử dụng Stream mang tính tuyến tính: stream được tạo ra từ một collection, nó được xử lý bởi một hoặc nhiều stream method, sau đó nó được thu hồi về phía collection hoặc object.. Tại bước này, ta có thể xuất thành các kiểu collection sau:

  • Collectors.toList()
  • Collectos.toSet()
  • Collectors.toMap()

......

Và vấn đề phát sinh ở đây là làm thế nào xuất từ Stream ra một kiểu collection hoàn toàn khác, không có trong API của Collectors? Bài viết này sẽ giúp bạn làm điều đó.

#1 Interface Collector

Mỗi static method liệt kê ở trên đều trả về một object kiểu Collector. Nhưng Collector là gì? Bạn hãy xem biểu đồ sau:

Ta thấy có 4 interface liên quan:

Supplier: biểu diễn supplier của một kết quả. Mỗi khi supplier được invoke, không có ràng buộc về tính chất của result (result mới hoặc trùng nhau)

BiConsumer: biểu diễn một operation có 2 biến đầu vào nhưng đầu ra không có gì.

Function: Biểu diễn một hàm có 1 biến đầu vào và đầu ra là 1 kết quả nào đó

BinaryOperator: biểu diễn một operation có đầu vào là 2 toán tử cùng loại (same type), kết quả cũng cùng type như toán tử. Đây là một trường hợp của BiFunction khi mà toán tử và kết quả có cùng kiểu (type).

Trích dẫn từ phần documentation của Collector, ta có:

Một Collector được định nghĩa từ  4 function làm việc cùng nhau để dồn các entries thành một result container (container này có đặc tính mutable), bên cạnh đó nó có thể thực hiện bước final transform cho result (nếu được yêu cầu). 4 functions đó gồm:

supplier() - có vai trò khởi tạo result container

accumulator() - đẩy các phần tử dữ liệu mới vào result container

combiner() - kết hợp 2 result container thành 1

finisher() - thực thi final transform cho container (nếu được yêu cầu)

Trải nghiệm chương trình đào tạo Lập trình viên FullStack tại Techmaster

#2 Stream.collect()

Phần doc của Stream.collect() hé lộ khá nhiều điều:

Method này thực hiện một phép toán tên là mutable reduction (mutable reduction operation). Đây là phép toán làm giảm giá trị của một mutable result container (Ví dụ: ArrayList), và các thành phần trong result container đó cũng bị tác động bới việc bị giảm đi một giá trị tương ứng. Phép toán này tương đương:

R result = supplier.get();
    for (T element : this stream)
        accumulator.accept(result, element);
    return result;

combiner() sẽ không được sử dụng cho đến khi ta làm việc với parallel stream

#3 Các ví dụ

Single-value example:

Để khởi động, hãy tính toán kích thước của một collection sử dụng Collector. Mặc dù cách này không thực sự hiệu quả và được sử dụng rộng rãi nhưng nó là một ví dụ dễ làm quen.

Sau đây là các yêu cầu của 4 interface:

1. Nếu kết quả cuối cùng (end result) là integer thì supplier cũng phải trả về integer. Tuy nhiên int hay Integer đều có tính Immutable, do đó ta cần đến MutableInt từ thư viện Apache Common Lang.

2. Bộ gộp (accumulator) chỉ nên thay đổi giá trị của các phần tử mang kiểu MutableInt thuộc result container (cụ thể trong ví dụ này là gọi hàm increment() )

3. Giá trị trả về là int được wrap trong MutableInt

Hãy xem qua 4 class của ví dụ này:

MutableIntSupplier.java

package ch.frankel.blog.stream.value;

import org.apache.commons.lang3.mutable.MutableInt;

import java.util.function.Supplier;

class MutableIntSupplier implements Supplier<MutableInt> {

    public MutableInt get() {
        return new MutableInt();
    }
}

MutablieIntObjectAccumulator.java

package ch.frankel.blog.stream.value;

import org.apache.commons.lang3.mutable.MutableInt;

import java.util.function.BiConsumer;

class MutableIntObjectAccumulator<T> implements BiConsumer<MutableInt, T> {

    @Override
    public void accept(MutableInt mutableInt, T t) {
        mutableInt.increment();
    }
}

MutableIntCombiner.java

package ch.frankel.blog.stream.value;

import org.apache.commons.lang3.mutable.MutableInt;

import java.util.function.BinaryOperator;

class MutableIntCombiner implements BinaryOperator<MutableInt> {

    @Override
    public MutableInt apply(MutableInt mutableInt1, MutableInt mutableInt2) {
        return new MutableInt(mutableInt1.intValue() + mutableInt2.intValue());
    }
}

MutableIntFinisher.java

package ch.frankel.blog.stream.value;

import org.apache.commons.lang3.mutable.MutableInt;

import java.util.function.Function;

class MutableIntIntFinisher implements Function<MutableInt, Integer> {

    @Override
    public Integer apply(MutableInt mutableInt) {
        return mutableInt.getValue();
    }
}

SizeCollector.java

package ch.frankel.blog.stream.value;

import org.apache.commons.lang3.mutable.MutableInt;

import java.util.HashSet;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

public class SizeCollector<T> implements Collector<T, MutableInt, Integer> {

    @Override
    public Supplier<MutableInt> supplier() {
        return new MutableIntSupplier();
    }

    @Override
    public BiConsumer<MutableInt, T> accumulator() {
        return new MutableIntObjectAccumulator<>();
    }

    @Override
    public BinaryOperator<MutableInt> combiner() {
        return new MutableIntCombiner();
    }

    @Override
    public Function<MutableInt, Integer> finisher() {
        return new MutableIntIntFinisher();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return new HashSet<>();
    }
}

Grouping example:

Ví dụ thứ hai liên quan tới collection chứa string. Ta sẽ tạo một multi-valued Map với 2 tiêu chí:

  • Phần Key có kiểu dữ liệu là char
  • Phần Values tương ứng là String có kí tự đầu tiên là Key.

Các yêu cầu dành cho 4 inteface:

1. Supplier trả về một bản thể kiểu MultivaluedMap

2. Accumulator sẽ gọi put() từ multi-valued map, sử dụng các mô tả đi kèm với bản thể MultivaluedMap được trả về bởi supplier

3. finisher sẽ trả về map

Đây là source code minh họa:

MultiValuedMapSupplier.java

package ch.frankel.blog.stream.aggregate;

import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;

import java.util.function.Supplier;

class MultiValuedMapSupplier implements Supplier<MultiValuedMap<Character, String>> {

    @Override
    public MultiValuedMap<Character, String> get() {
        return new ArrayListValuedHashMap<>();
    }
}

MultiValuedMapAccumulator.java

package ch.frankel.blog.stream.aggregate;

import org.apache.commons.collections4.MultiValuedMap;

import java.util.function.BiConsumer;

class MultiValuedMapAccumulator implements BiConsumer<MultiValuedMap<Character, String>, String> {

    @Override
    public void accept(MultiValuedMap<Character, String> map, String s) {
        map.put(s.charAt(0), s);
    }
}

MultiValuedMapCombiner.java

package ch.frankel.blog.stream.aggregate;

import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;

import java.util.function.BinaryOperator;

class MultiValuedMapCombiner implements BinaryOperator<MultiValuedMap<Character, String>> {

    @Override
    public MultiValuedMap<Character, String> apply(MultiValuedMap<Character, String> map1, MultiValuedMap<Character, String> map2) {
        ArrayListValuedHashMap<Character, String> map = new ArrayListValuedHashMap<>();
        map.putAll(map1);
        map.putAll(map2);
        return map;
    }
}

GroupbyFirstCharacterCollector.java

package ch.frankel.blog.stream.aggregate;

import org.apache.commons.collections4.MultiValuedMap;

import java.util.Collections;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

import static java.util.stream.Collector.Characteristics.IDENTITY_FINISH;

public class GroupByFirstCharacterCollector implements Collector<String, MultiValuedMap<Character, String>, MultiValuedMap<Character, String>> {

    @Override
    public Supplier<MultiValuedMap<Character, String>> supplier() {
        return new MultiValuedMapSupplier();
    }

    @Override
    public BiConsumer<MultiValuedMap<Character, String>, String> accumulator() {
        return new MultiValuedMapAccumulator();
    }

    @Override
    public BinaryOperator<MultiValuedMap<Character, String>> combiner() {
        return new MultiValuedMapCombiner();
    }

    @Override
    public Function<MultiValuedMap<Character, String>, MultiValuedMap<Character, String>> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.singleton(IDENTITY_FINISH);
    }
}

Partritioning example
Ví dụ thứ 3 diễn tả lại một use-case mà tác giả gặp phải: cho một collection và các thành phần sẽ chuẩn bị được thêm vào collection, tách riêng các thành phần này thành 2 nhóm: 1 nhóm có thể được add vào Collection này, và nhóm còn lại thì không thể add vào.

Yêu cầu cho 4 interface:

1. Supplier sẽ trả về một bản thể mang kiểu của một data structure phù hợp (trong trường hợp này, hãy xem xét kiểu DoubleList)

2. Accumulator cần được init với các thành phần chuẩn bị được thêm vào collection,

3. Finisher cần phải trả về 1 bản thể của DoubleList.

DoubleListSupplier.java

package ch.frankel.blog.stream.partition;

import java.util.function.Supplier;

class DoubleListSupplier<T> implements Supplier<DoubleList<T>> {

    @Override
    public DoubleList<T> get() {
        return new DoubleList<T>();
    }
}

DoubleListAccumulator.java

package ch.frankel.blog.stream.partition;

import java.util.function.BiConsumer;
import java.util.function.Predicate;

class DoubleListAccumulator<T> implements BiConsumer<DoubleList<T>, T> {

    private final Predicate<T> predicate;

    DoubleListAccumulator(Predicate<T> predicate) {
        this.predicate = predicate;
    }

    @Override
    public void accept(DoubleList<T> doubleList, T t) {
        if (predicate.test(t)) {
            doubleList.addToFirst(t);
        } else {
            doubleList.addToSecond(t);
        }
    }
}

DoubleListCombiner.java

package ch.frankel.blog.stream.partition;

import java.util.function.BinaryOperator;

class DoubleListCombiner<T> implements BinaryOperator<DoubleList<T>> {

    @Override
    public DoubleList<T> apply(DoubleList<T> list1, DoubleList<T> list2) {
        return list1.merge(list2);
    }
}

DoubeList.java

package ch.frankel.blog.stream.partition;

import java.util.Collections;
import java.util.Set;
import java.util.function.*;
import java.util.stream.Collector;

import static java.util.stream.Collector.Characteristics.IDENTITY_FINISH;

public class PartitionByPredicateCollector<T> implements Collector<T, DoubleList<T>, DoubleList<T>> {

    private final Predicate<T> predicate;

    public PartitionByPredicateCollector(Predicate<T> predicate) {
        this.predicate = predicate;
    }

    @Override
    public Supplier<DoubleList<T>> supplier() {
        return new DoubleListSupplier<T>();
    }

    @Override
    public BiConsumer<DoubleList<T>, T> accumulator() {
        return new DoubleListAccumulator<>(predicate);
    }

    @Override
    public BinaryOperator<DoubleList<T>> combiner() {
        return new DoubleListCombiner<>();
    }

    @Override
    public Function<DoubleList<T>, DoubleList<T>> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.singleton(IDENTITY_FINISH);
    }
}

Techmaster via N.Frankel's blog