7 Ocak 2020 Salı

Stream.collect metodu - Supplier + Accumulator + Combiner - Kendi Container Nesnemiz

Giriş
İmzası şöyle.
<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);
Supplier
Sonuç olarak kullanılacak container'ı döner.

Accumulator
Açıklaması şöyle. Yani container'a nesne ekler
accumulator - an associative, non-interfering, stateless function that must fold an element into a result container.
Combiner Ne İşe Yarar
Açıklaması şöyle. Eğer stream paralel kullanılıyorsa, iki tane sonuç container'ını birleştirir.
combiner - an associative, non-interfering, stateless function that accepts two partial result containers and merges them, which must be compatible with the accumulator function. The combiner function must fold the elements from the second result container into the first result container.
Combiner yerine kendimiz yazmak istersek, lambda kullanabiliriz ya da sınıfımızın bir metodunu çağırabiliriz. Bu metod paralelleştirme varsa işe yarar. Açıklaması şöyle. Yani iki thread'in çıktısını birleştirmek için kullanılır.
The first argument is a supplier that supplies an empty array list for adding collected stuff into. The second argument is a biconsumer that consumes each element of the array. The third argument is there only to provide parallelism support. This enables it to collect the elements into multiple array lists at the same time, and it asks you for a way to connect all these array lists together at the end.
Combiner'ın devreye girdiği ve girmediğini görmek için şöyle yaparız.
String resultParallel = list.parallelStream().collect(
            StringBuilder::new,
            (builder, elem) -> builder.append(" ").append(elem),
            (left, right) -> left.append(" ").append(right)).toString();

String result = list.stream().collect(
            StringBuilder::new,
            (builder, elem) -> builder.append(" ").append(elem),
            (left, right) -> left.append(" ").append(right)).toString();


System.out.println("ResultParallel: ->" + resultParallel + "<-"); // -> 1  2  3  4<-
System.out.println("Result: ->" + result + "<-"); // -> 1 2 3 4<-
Combiner ve Accumulator aynı şekilde yani aynı sonucu dönmeli
Açıklaması şöyle.
What this is saying is that it should not matter whether the elements are collected by "accumulating" or "combining" or some combination of the two. But in your code, the accumulator and the combiner concatenate using a different separator. They are not "compatible" in the sense required by the javadoc.
Elimizde şöyle bir kod olsun. Combiner ve Accumulator aynı sonucu dönmedikleri için serial ve paralel koşmalarda farklı sonuç elde ederiz. Paralel koşmadan fazladan space karakterleri vardır. Çünkü accumulator nesnesine iki tane StringBuilder gelir ve her birinin başında space karakteri vardır.
String resultParallel = list.parallelStream().collect(
            StringBuilder::new,
            (builder, elem) -> builder.append(" ").append(elem),
            (left, right) -> left.append(" ").append(right)).toString();

String result = list.stream().collect(
            StringBuilder::new,
            (builder, elem) -> builder.append(" ").append(elem),
            (left, right) -> left.append(" ").append(right)).toString();


System.out.println("ResultParallel: ->" + resultParallel + "<-"); // -> 1  2  3  4<-
System.out.println("Result: ->" + result + "<-"); // -> 1 2 3 4<-
Kullanım Örnekleri
Örnek - ArrayList
Şöyle yaparız.
ArrayList<Integer> collected = Stream.of(1,2,3)
    .collect(
        ArrayList::new, 
        ArrayList::add, 
        ArrayList::addAll);
System.out.println(collected);
Örnek - List
Şöyle yaparız.
Path path = ...;
List<String> lines = Files.lines(path)
  .collect(
    ArrayList::new, 
    new BiConsumer<ArrayList<String>, String>() {
      @Override
      public void accept(ArrayList<String> lines, String line) {
      ...                  
      }
    }, 
    ArrayList::addAll
);
Örnek - HashMap
Elimizde şöyle bir kod olsun
static class Foo {
    private final int id;

    private final String s;

    public Foo(int id, String s) {
        super();
        this.id = id;
        this.s = s;
    }

    public int getId() {
        return id;
    }

    public String getS() {
        return s;
    }

    @Override
    public String toString() {
        return "" + id;
    }
}
Şöyle yaparız
HashMap<String, Foo> result = Arrays.asList(new Foo(1, "a"), new Foo(2, "b"))
            .stream()
            .collect(HashMap::new, 
                    (map, foo) -> map.put(foo.getS(), foo), 
                    HashMap::putAll);

    System.out.println(result); // {a=1, b=2}
Ancak eğer Foo'nun s alanı unique değilse şöyle yaparız
HashMap<String, List<Foo>> result = Arrays.asList(
            new Foo(1, "a"),
            new Foo(2, "b"),
            new Foo(3, "a"))
            .stream()
            .parallel()
            .collect(HashMap::new,
                    (map, foo) -> {
                        map.computeIfAbsent(foo.getS(), s -> new ArrayList<>()).add(foo);
                    },
                    (left, right) -> {
                        for (HashMap.Entry<String, List<Foo>> e : right.entrySet()) {
                            left.merge(
                                    e.getKey(),
                                    e.getValue(),
                                    (l1, l2) -> {
                                        l1.addAll(l2);
                                        return l1;
                                    });
                        }
                    });

    System.out.println(result); // {a=[1, 3], b=[2]}
Örnek - LinkedHashMap
Şöyle yaparız. Burada Array içindeki string'lerin unique olduğu varsayılıyor.
List<String> myList = Arrays.asList("a", "bb", "ccc"); 
// or since java 9 List.of("a", "bb", "ccc");

LinkedHashMap<String, Integer> mapInOrder = myList
    .stream()
    .collect(
        LinkedHashMap::new,                                   // Supplier
        (map, item) -> map.put(item, item.length()),          // Accumulator
        Map::putAll);                                         // Combiner

System.out.println(mapInOrder);  // {a=1, bb=2, ccc=3}
Örnek - LinkedHashSet
Şöyle yaparız
LinkedHashSet<Student> studentsLinkedHashSet = students().collect(
      LinkedHashSet::new,
      LinkedHashSet::add,
      LinkedHashSet::addAll
);

Örnek - Averager
 Şöyle bir sınıfımız olsun.
// Averager.java from the official Java tutorial
public class Averager
{
  private int total = 0;
  private int count = 0;


  public void addcount(int i) { total += i; count++;} //accumulator
  public void combine(Averager other) { //combiner
    total += other.total;
    count += other.count;
  }
}
Kendi metodlarımızı çağırmak istersek şöyle yaparız. Bir Averager nesnesi yaratılır. Her eleman için addCount metodu çağrılır. addAccount bir accumulator metodudur. Stream elemanını bir result container nesnesine ekler. combine bir combiner metodudur. İki tane result container nesnesini birleştirir.
List<Integer> list = new LinkedList<Integer>();
...
Averager averageCollect = list.stream()
      .collect(Averager::new, Averager::addcount, Averager::combine);
Eğer lambda kullanmak istersek şöyle yaparız.
Averager averageCollect = list.stream()
  .collect(Averager::new, (a,b) -> a.addcount(b), (a,b) -> a.combine(b));

Hiç yorum yok:

Yorum Gönder