Skip to content

Latest commit

 

History

History
568 lines (414 loc) · 18.4 KB

chapter-08.asc

File metadata and controls

568 lines (414 loc) · 18.4 KB

Stream API

Java 8 içerisinde yığınsal verileri kolay işlemek açısından Stream API yeniliği getirilmiştir.

Stream API yığınsal veriler üzerinde çalışmaktadır. Yığınsal veri deyince ilk akla gelen hiç şüphesiz diziler (byte[],String[] gibi ) ve Java Collection API bileşenleridir (List,Set gibi)

Stream API, bu gibi yığınsal veriler üzerinde çeşitli sık kullanılan operasyonları kolay, özlü ve verimli bir biçimde koşturmaya olanak tanımaktadır. Bu operasyonlardan en sık kullanılanları aşağıdaki gibidir.

Metod Açıklama

filter

Filtreleme

forEach

iterasyon

map

Dönüştürme

reduce

İndirgeme

distinct

Tekilleştirme

sorted

Sıralama

limit

Aralık alma

collect

Türe dönüşüm

count

Sayma

…​

Bu operasyonlar ve daha fazlası java.util.stream.Stream arayüzü içinde bulunmaktadır. Stream arayüzünün sadeleştirilmiş hali aşağıdaki gibidir.

public interface Stream<T> extends BaseStream<T, Stream<T>> {

    Stream<T> filter(Predicate<? super T> predicate);
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    IntStream mapToInt(ToIntFunction<? super T> mapper);
    LongStream mapToLong(ToLongFunction<? super T> mapper);
    DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
    IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);
    LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper);
    DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);
    Stream<T> distinct();
    Stream<T> sorted();
    Stream<T> sorted(Comparator<? super T> comparator);
    Stream<T> peek(Consumer<? super T> action);
    Stream<T> limit(long maxSize);
    Stream<T> skip(long n);
    void forEach(Consumer<? super T> action);
    void forEachOrdered(Consumer<? super T> action);
    Object[] toArray();
    <A> A[] toArray(IntFunction<A[]> generator);
    T reduce(T identity, BinaryOperator<T> accumulator);
    Optional<T> reduce(BinaryOperator<T> accumulator);
    <U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);
    <R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);
    <R, A> R collect(Collector<? super T, A, R> collector);
    Optional<T> min(Comparator<? super T> comparator);
    Optional<T> max(Comparator<? super T> comparator);
    long count();
    boolean anyMatch(Predicate<? super T> predicate);
    boolean allMatch(Predicate<? super T> predicate);
    boolean noneMatch(Predicate<? super T> predicate);
    Optional<T> findFirst();
    Optional<T> findAny();
}

Stream nesnesi nasıl elde edilir?

Stream türünden nesneler çeşitli yollarla elde edilebilmektedir.

Collection API ile

Collection arayüzü türünden türeyen tüm nesneler, stream() veya parallelStream() metodlarını çağırarak Stream<E> türünden bir nesne elde edebilmektedir.

public interface Collection<E> extends Iterable<E> {

...

    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }

...

}

stream() metodu ile elde edilen Stream nesnesi yapacağı işlemleri ardışıl olarak yaparken, parallelStream() metoduyla elde edilen Stream nesnesi, bazı operasyonları paralel olarak koşturabilmektedir.

Örneğin;
List<String> names = Arrays.asList("Ali","Veli","Selami"); (1)

Stream<String> stream = names.stream(); (2)
Stream<String> parallelStream = names.parallelStream(); (2)
  1. Collection türünden bir nesne

  2. Ardışık stream

  3. Paralel stream

New I/O ile

Java içerisindeki bazı I/O sınıfları üzerinden Stream nesneleri elde edilebilmektedir.

Path dir = Paths.get("/var/log"); (1)

Stream<Path> pathStream = Files.list(dir); (2)
  1. /var/log dizinine denk gelen bir Path nesnesi

  2. Files#list metodu üzerinden bir Stream<Path> nesnesi

IntStream, DoubleStream, LongStream ile

Stream arayüzü BaseStream arayüzünden türemektedir. Stream arayüzüne benzer biçimde IntStream, DoubleStream ve LongStream arayüzleri de BaseStream arayüzünden türemektedir.

Stream arayüzü türünden nesneler tüm veri tipleriyle çalışmak için oluşturulan bir arayüzken, buradaki üç eleman ise, sadece sınıf başındaki tip ile özel olarak çalışmak için oluşturulan arayüzlerdir.

Örneğin;
IntStream intOf = IntStream.of(1, 2, 3); (1)
IntStream intRange = IntStream.range(1, 10); (2)

DoubleStream doubleOf = DoubleStream.of(1.0, 3.5, 6.6); (3)

LongStream longOf = LongStream.of(3, 5, Long.MAX_VALUE,9); (4)
LongStream longRange = LongStream.range(1, 100); (5)
  1. (1,2,3) içeren IntStream nesnesi

  2. (1,…​,10) arasını içeren IntStream nesnesi

  3. (1.0, 3.5, 6.6) içeren DoubleStream nesnesi

  4. (3, 5, Long.MAX_VALUE,9) içeren LongStream nesnesi

  5. (1,…​,100) arasını içeren LongStream nesnesi

Stream API Örnekleri

Bu kısımda çeşitli Stream API metodları ile küçük uygulamalar yer almaktadır.

forEach

Stream içerisindeki yığınsal veriyi tek tek tüketmek için yapılandırılmıştır. Consumer arayüzü türünden bir parametre bekler.

List<String> names = Arrays.asList("Ali","Veli","Selami","Cem","Zeynel","Can","Hüseyin");

Stream<String> stream = names.stream();

stream.forEach(name -> {
    System.out.println(name);
});

// veya stream.forEach(System.out::println);

filter

Stream içerisindeki yığınsal veri üzerinde süzme işlemi yapar. Predicate arayüzü türünden bir parametre ile filtreleme işlemini yapar.

List<String> names = Arrays.asList("Ali", "Veli", "Selami", "Cem", "Zeynel", "Can", "Hüseyin");

Stream<String> stream = names.stream(); (1)

Predicate<String> predicate = name -> name.length() < 4; (2)

Stream<String> filtered = stream.filter(predicate); (3)

filtered.forEach(System.out::println); (4)
  1. Stream nesnesi elde ediliyor.

  2. Predicate sorgusu hazırlanıyor

  3. Süzme işlemi yapılıyor, yeni bir Stream nesnesi sunuluyor.

  4. Listeleniyor. [Ali, Cem, Can]

Note
Stream nesneleri tek kullanımlıktır. Stream nesnesinin çoğu metodu yeni bir Stream nesnesi sunmaktadır. Bu sebeple tüm operasyonlar zincirlemeli olarak yapılabilmektedir.
Örneğin;
names
    .stream()
    .filter(name -> name.length() == 4)
    .forEach(System.out::println);

distinct

Bir Stream içerisinden tekrarlı veriler çıkarılmak isteniyorsa distinct metodundan faydalanılabilir.

IntStream stream = IntStream.of(1, 1, 2, 3, 5, 8, 13, 13, 8); (1)

stream
    .distinct()
    .forEach(System.out::println); (2)
  1. IntStream nesnesi

  2. [1,2,3,5,8,13]

sorted

Stream içerisindeki yığınsal verinin sıralanmış Stream nesnesini döndürür.

IntStream stream = IntStream.of(13, 1, 3, 5, 8, 1, 13, 2, 8); (1)

stream
        .sorted()
        .forEach(System.out::println); (2)
  1. IntStream nesnesi

  2. [1,1,2,3,5,8,8,13,13]

limit

Bir Stream yığını içerisindeki ilk N veri barındıran yeni bir Stream nesnesi sunmaktadır.

LongStream range = LongStream.range(1, 10000); (1)

        range
                .limit(10)
                .forEach(System.out::println); (2)
  1. (1,…​,10000) arasını içeren bir Stream

  2. İlk 10 veri : [1,…​,10]

count

Stream içerisindeki eleman sayısını hesaplar.

IntStream range = IntStream.range(1, 10);
IntStream rangeClosed = IntStream.rangeClosed(1, 10);

System.out.println(range.count()); (1)
System.out.println(rangeClosed.count()); (2)
  1. 9

  2. 10

collect

Stream türünden nesneler, yığın verileri temsil eden özel nesnelerdir. Fakat Stream biçimi bir veri yapısı sunmamaktadır. collect metodu ağırlıklı olarak , Stream nesnelerini başka biçimdeki bir nesneye, veri yapısına dönüştürmek için kullanılmaktadır.

Stream#collect metodu Collector türünden bir parametre kabul etmektedir. Bu parametre ile istendik türe dönüşüm sağlanmaktadır. Collector türünden arayüzler, Collectors sınıfının çeşitli statik metodlarıyla elde edilebilmektedir.

List<String> names = Arrays.asList("Ali", "Veli", "Selami", "Veli", "Selami", "Can", "Hüseyin");

List<String> list = names.stream().collect(Collectors.toList()); (1)

Set<String> set = names.stream().collect(Collectors.toSet()); (2)

Long count = names.stream().collect(Collectors.counting()); (3)

String collect = names.stream().collect(Collectors.joining(" - ")); (4)

Map<Integer, List<String>> integerListMap = names.stream().collect(Collectors.groupingBy(name -> name.length())); (5)
  1. Stream nesnesinden List nesnesi üretir.
    List["Ali", "Veli", "Selami", "Veli", "Selami", "Can", "Hüseyin"]

  2. Stream nesnesinden Set nesnesi üretir.
    Set["Ali", "Veli", "Selami","Can", "Hüseyin"]

  3. Stream nesnesinin eleman sayısını üretir.
    7

  4. Stream nesnesini birleştirir.
    Ali - Veli - Selami - Veli - Selami - Can - Hüseyin

  5. Stream nesnesini isim uzunluğuna göre gruplar.

Table 1. Map<Integer, List<String>> nesnesinin temsili tablo görünümü
Key Value

3

Ali

Can

4

Veli

Veli

6

Selami

Selami

7

Hüseyin

map

Stream içindeki yığınsal olarak bulunan her bir veriyi dönüştürmeye olanak tanır. Dönüştürüm işlemi Stream içerisindeki her bir öğe için ayrı ayrı yapılmaktadır. Stream#map metodu Function türünden bir parametre beklemektedir.

Örnek 1;

Bir List<String> içindeki her bir öğenin harflerini büyütelim.

List<String> names = Arrays.asList("Ali", "Veli", "Selami", "Cem");

Stream<String> stream = names.stream(); (1)
Stream<String> upperStream= stream.map(name -> name.toUpperCase()); (2)
List<String> upperNames = upperStream.collect(Collectors.toList()); (3)
  1. Stream<String> nesnesi elde ediliyor

  2. Her bir ismin harfleri büyütülüyor

  3. List["ALİ","VELİ","SELAMİ","CEM"]

Örnek 2;

1,5 arası sayıların karelerini hesaplayalım.

IntStream
        .rangeClosed(1, 5)
        .map(n -> n*n)
        .forEach(System.out::println); (1)
  1. [1, 4, 9, 16, 25]

reduce

Bir Stream içerisindeki verilerin teker teker işlenmesidir. Teker teker işleme sürecinde, bir önceki adımda elde edilen sonuç bir sonraki adıma girdi olarak sunulmaktadır. Bu sayede yığılmlı bir hesaplama süreci elde edilmiş olmaktadır.

Stream#reduce metodu ilk parametrede identity değeri, ikinci parametrede ise BinaryOperator türünden bir nesne kabul etmektedir.

reduce işleminde bir önceki hesaplanmış değer ile sıradaki değer bir işleme tabi tutulmaktadır. İşleme başlarken bir önceki değer olmadığı için bu değer identity parametresinde tanımlanmaktadır.

Örnek 1;

1,2,3,4,5 sayılarının toplamını hesaplayalım.

int result = IntStream
                .of(1, 2, 3, 4, 5)
                .reduce(0, (once, sonra) -> {
                    System.out.format("%d - %d %n", once, sonra);
                    return once + sonra;
                });

Toplama işleminde 0 etkisiz eleman olduğu için, identity değeri 0 seçildi.

Uygulama çalıştırıldığında 15 sonucu elde edilir. reduce içindeki Lambda ifadesinde ise aşağıdaki çıktı elde edilir.

 0 - 1
 1 - 2
 3 - 3
 6 - 4
10 - 5

Önce hesaplanmış değeri, Sonra ise sıradaki değeri temsil etmektedir. Bir adımda çıkan hesaplamanın sonucu, bir sonraki adımda (satırda) Önce sütununa sunulmaktadır.

Önce Sonra Hesaplama

0

1

0+1 ↵

1

2

1+2 ↵

3

3

3+3 ↵

6

4

6+4 ↵

10

5

10+5 = 15

Örnek 2;

1,2,3,4,5 sayılarının çarpımını hesaplayalım.

// Lambda ile
int result = IntStream
                .of(1, 2, 3, 4, 5)
                .reduce(1, (once, sonra) -> once*sonra);

// veya Method reference ile
result = IntStream
            .of(1, 2, 3, 4, 5)
            .reduce(1, Math::multiplyExact);

map & reduce

map ve reduce işlemleri birlikte kullanımı çok fazla tercih edilen iki operasyondur. Bu operasyonları önemli kılan ise, bu iki operasyonun dağıtık sistemler için çok uygun olmasıdır. Piyasada Map & Reduce işlemlerini dağıtık mimarilerde kullanan birçok teknoloji bulunmaktadır. Tabiki Java 8 ile kullandığımız map & reduce ikilisi tek JVM üzerinde koştuğu için dağıtık değildir.

Örneğin;

  • Hazelcast

  • Hadoop

  • MongoDB gibi.

Örnek 1;

Elimizde Person sınıfı türünden 5 nesne bulunsun. Bu 5 nesne içinden tüm kişilerin yaşlarının toplamını hesaplamak isteyelim. Böyle bir senaryo için map ve reduce metodlarını birlikte tercih edebiliriz.

public class Person {
    private String name;
    private Integer age;

   // getter, setter ve constructor metodları
}
Person p1 = new Person("Ahmet", 12);
Person p2 = new Person("Ali", 20);
Person p3 = new Person("Ayşe", 30);
Person p4 = new Person("Murat", 51);
Person p5 = new Person("Zeynep", 60);

List<Person> personList = Arrays.asList(p1, p2, p3, p4, p5); (1)

personList
        .stream() (2)
        .map(p -> p.getAge()) (3)
        .reduce(0, (a, b) -> (a + b)); (4)
  1. Person listesi

  2. Stream nesnesi elde ediliyor

  3. Nesnenin yaş alanına göre mapping yapılıyor.

  4. Toplamları hesaplanıyor

Örnek 2;

Person listesinde bazı kişilerin yaş alanları null değer içersin. Bu durumda çalışma zamanında nullpointerexception istisnası elde edilecektir. Bu gibi bir durumda filtreleme yapısını işlemimize ekleyebiliriz.

Person p1 = new Person("Ahmet", 12);
Person p2 = new Person("Ali", null);
Person p3 = new Person("Ayşe", 30);
Person p4 = new Person("Murat", null);
Person p5 = new Person("Zeynep", 60);

List<Person> personList = Arrays.asList(p1, p2, p3, p4, p5);

personList
        .stream()
        .filter(Objects::nonNull) // Dikkat !!
        .map(p -> p.getAge())
        .reduce(0, (a, b) -> (a + b));

Parallel Stream

Stream arayüzü içindeki metodlardan ardışık işletilmesi gerekmeyenler, istenirse, CPU üzerinde paralel olarak koşturulabilmektedir. Bu sayede CPU çekirdeklerini tam verimli olarak kullanmak mümkün olmaktadır.

Stream API içerisinde paralel Stream elde etmek oldukça kolaydır.

Örneğin

List<Integer> ints = Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15);

Stream<Integer> stream = ints.stream();
Stream<Integer> parallelStream = ints.parallelStream();

Collection#stream() metoduyla ardışıl (sequential) , Collection#parallelStream() metoduyla da paralel Stream nesnesi elde edilmektedir. Elde edilen paralel Stream nesnesiyle koşturulan işlemler paralel olarak koşabilmektedir.

Aynı zamanda bir ardışıl Stream nesnesinden paralel Stream nesnesi elde edilebilmektedir. Bunun için Stream#parallel metodu kullanılmaktadır.

Örneğin;
List<Integer> ints = Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15);

Stream<Integer> stream = ints.stream(); // Ardışıl
Stream<Integer> parallelStream = stream.parallel(); // Paralel

Aynı zamanda bir paralel Stream nesnesinden ardışıl Stream nesnesi de elde edilebilmektedir. Bunun için Stream#sequential metodu kullanılmaktadır.

Örneğin;
List<Integer> ints = Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15);

Stream<Integer> parallelStream = ints.parallelStream(); // Paralel
Stream<Integer> stream = stream.sequential(); // Ardışıl
Örnek

Aşağıda bir dizi sayısal ifadeyi filtreleyen, sıralayan ve çıktılayan bir kod parçası görmekteyiz. Ayrıca bu işlemlerin paralel Stream nesnesiyle yapılmak istendiğini görüyoruz.

List<Integer> ints = Arrays.asList(1, 5, 3, 7, 11, 9, 15, 13);

 ints
    .parallelStream() // Paralel Stream
    .filter(Objects::nonNull) // null değilse
    .filter(n -> n > 0) // pozitif sayı ise
    .sorted() // sırala
    .forEach(System.out::println); // çıktıla

Bu örnekte filter ve sorted paralel olarak koşturulabilirdir. Fakat forEach metodu doğası gereği öğeleri ardışık çıktılamalıdır. İşte tam da bu adımda elimizdeki paralel Stream nesnesi ardışıl Stream nesnesine dönüştürülmektedir ve ardından forEach işlemini koşturmaktadır.

Yani elimizde paralel Stream nesnesi varsa, bu zincirlemeli işlemin her adımında paralel koşturma yapılacağı anlamını taşımamaktadır.

Lazy & Eager operasyonlar

Literatürde Lazy bir işlemin geç, ötelenmiş olarak yapılması iken, Eager ise yapılacak işlemin emir verilir verilmez yapılmasını temsilen kullanılır.

Stream API içerisindeki bazı operasyonlar Lazy bazıları ise Eager olarak koşturulmaktadır. Lazy davranışlı olan zincirli görevler, bir Eager operasyona gelene kadar koşturulmamaktadır.

List<Integer> names = Arrays.asList(1,2,3,6,7,8,9);

Stream<Integer> stream = names
    .stream()
    .filter(Objects::nonNull)
    .filter(n->n%2==1)
    .map(n->n*2);

Örneğin yukarıdaki liste üzerinde yapılmak istenen 2 filter ve 1 map işlemi Lazy işlemlerdir. Kod parçası bu haliyle çalıştırıldığında ne bir filtreleme ne de bir dönüştürme işlemi yapılacaktır. Burada yapılan sadece Stream nesnesini hazırlamaktır. Lazy işlemler gerekmedikçe işleme konulmamaktadır.

List<Integer> names = Arrays.asList(1,2,3,6,7,8,9);

Stream<Integer> stream = names
    .stream()
    .filter(Objects::nonNull) (1)
    .filter(n->n%2==1) (2)
    .map(n->n*2) (3)

stream.forEach(System.out::println); // Dikkat !! (4)
  1. Lazy

  2. Lazy

  3. Lazy

  4. Eager

Fakat bu hazırlanan Stream nesnesi, yukarıdaki gibi bir Eager operasyonla karşılaşırsa, önceki zincirlerde biriken Lazy işlemleri de harekete geçirecektir. Yani (4) numaradaki işlem, (1)(2)(3) numaralı işlemlerin tetikleyicisi konumundadır.

Tekrar görüşmek dileğiyle..