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 türünden nesneler çeşitli yollarla elde edilebilmektedir.
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.
List<String> names = Arrays.asList("Ali","Veli","Selami"); (1)
Stream<String> stream = names.stream(); (2)
Stream<String> parallelStream = names.parallelStream(); (2)
-
Collection türünden bir nesne
-
Ardışık stream
-
Paralel stream
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)
-
/var/log
dizinine denk gelen bir Path nesnesi -
Files#list metodu üzerinden bir Stream<Path> nesnesi
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.
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,2,3) içeren IntStream nesnesi
-
(1,…,10) arasını içeren IntStream nesnesi
-
(1.0, 3.5, 6.6) içeren DoubleStream nesnesi
-
(3, 5, Long.MAX_VALUE,9) içeren LongStream nesnesi
-
(1,…,100) arasını içeren LongStream nesnesi
Bu kısımda çeşitli Stream API metodları ile küçük uygulamalar yer almaktadır.
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);
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)
-
Stream nesnesi elde ediliyor.
-
Predicate sorgusu hazırlanıyor
-
Süzme işlemi yapılıyor, yeni bir Stream nesnesi sunuluyor.
-
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. |
names
.stream()
.filter(name -> name.length() == 4)
.forEach(System.out::println);
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)
-
IntStream nesnesi
-
[1,2,3,5,8,13]
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)
-
IntStream nesnesi
-
[1,1,2,3,5,8,8,13,13]
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,…,10000) arasını içeren bir Stream
-
İlk 10 veri : [1,…,10]
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)
-
9
-
10
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)
-
Stream nesnesinden List nesnesi üretir.
List["Ali", "Veli", "Selami", "Veli", "Selami", "Can", "Hüseyin"] -
Stream nesnesinden Set nesnesi üretir.
Set["Ali", "Veli", "Selami","Can", "Hüseyin"] -
Stream nesnesinin eleman sayısını üretir.
7 -
Stream nesnesini birleştirir.
Ali - Veli - Selami - Veli - Selami - Can - Hüseyin -
Stream nesnesini isim uzunluğuna göre gruplar.
Key | Value |
---|---|
3 |
Ali |
Can |
|
4 |
Veli |
Veli |
|
6 |
Selami |
Selami |
|
7 |
Hüseyin |
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.
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)
-
Stream<String> nesnesi elde ediliyor
-
Her bir ismin harfleri büyütülüyor
-
List["ALİ","VELİ","SELAMİ","CEM"]
1,5 arası sayıların karelerini hesaplayalım.
IntStream
.rangeClosed(1, 5)
.map(n -> n*n)
.forEach(System.out::println); (1)
-
[1, 4, 9, 16, 25]
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.
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 |
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 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.
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)
-
Person listesi
-
Stream nesnesi elde ediliyor
-
Nesnenin yaş alanına göre mapping yapılıyor.
-
Toplamları hesaplanıyor
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));
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.
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.
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
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.
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)
-
Lazy
-
Lazy
-
Lazy
-
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..