Java 8 – 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 filter(Predicate<? super T> predicate);
Stream 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);
Stream 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 distinct();
Stream sorted();
Stream sorted(Comparator<? super T> comparator);
Stream peek(Consumer<? super T> action);
Stream limit(long maxSize);
Stream skip(long n);
void forEach(Consumer<? super T> action);
void forEachOrdered(Consumer<? super T> action);
Object[] toArray();
A[] toArray(IntFunction<A[]> generator);
T reduce(T identity, BinaryOperator accumulator);
Optional reduce(BinaryOperator accumulator);
U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator combiner);
R collect(Supplier supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);
Optional min(Comparator<? super T> comparator);
Optional 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 findFirst();
Optional 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 türünden bir nesne elde edebilmektedir.
public interface Collection<E> extends Iterable<E> {
...
default Stream stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream 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 names = Arrays.asList("Ali","Veli","Selami"); (1)
Stream stream = names.stream(); (2)
Stream parallelStream = names.parallelStream(); (2)
- Collection türünden bir nesne
- Ardışık stream
- 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 pathStream = Files.list(dir); (2)
/var/log
dizinine denk gelen bir Path nesnesi- Files#list metodu üzerinden bir Stream 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.
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
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 names = Arrays.asList("Ali","Veli","Selami","Cem","Zeynel","Can","Hüseyin");
Stream 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 names = Arrays.asList("Ali", "Veli", "Selami", "Cem", "Zeynel", "Can", "Hüseyin");
Stream stream = names.stream(); (1)
Predicate predicate = name -> name.length() < 4; (2)
Stream 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);
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)
- IntStream nesnesi
- [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)
- IntStream nesnesi
- [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,…,10000) arasını içeren bir Stream
- İ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)
- 9
- 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 names = Arrays.asList("Ali", "Veli", "Selami", "Veli", "Selami", "Can", "Hüseyin");
List list = names.stream().collect(Collectors.toList()); (1)
Set 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> 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 |
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.
Bir List
içindeki her bir öğenin harflerini büyütelim.
List names = Arrays.asList("Ali", "Veli", "Selami", "Cem");
Stream stream = names.stream(); (1)
Stream upperStream= stream.map(name -> name.toUpperCase()); (2)
List upperNames = upperStream.collect(Collectors.toList()); (3)
- Stream 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]
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.
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 & 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.
Elimizde Person sınıfı türünden 5 nesne bulunsun. Bu 5 nesne içinden tüm kişilerin yaşlarının ortalamasını hesaplamak isteyelim. Böyle bir senaryo için map & 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 personList = Arrays.asList(p1, p2, p3, p4, p5); (1)
personList
.stream() (2)
.map(p -> p.getAge()) (3)
.map(Double::valueOf) (4)
.reduce(0, (a, b) -> (a + b)/2); (5)
- Person listesi
- Stream nesnesi elde ediliyor
- Nesnenin yaş alanına göre mapping yapılıyor.
- Integer → Double dönüşümü
- Ortalamalar 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 personList = Arrays.asList(p1, p2, p3, p4, p5);
personList
.stream()
.filter(Objects::nonNull) // Dikkat !!
.map(p -> p.getAge())
.map(Double::valueOf)
.reduce(0, (a, b) -> (a + b)/2);
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 ints = Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15);
Stream stream = ints.stream();
Stream 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 ints = Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15);
Stream stream = ints.stream(); // Ardışıl
Stream 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 ints = Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15);
Stream parallelStream = ints.parallelStream(); // Paralel
Stream 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 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 names = Arrays.asList(1,2,3,6,7,8,9);
Stream 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 names = Arrays.asList(1,2,3,6,7,8,9);
Stream 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.
Tag:backend
5 Comments
Hocam konuyu çok güzel açıklamışsınız, çok faydalı oldu, ellerinize sağlık, çok teşekkür ederim.
Yazdıklarınız yıllar sonra bile işe yarar ve kullanışlı oluyor. Emeğiniz için çok teşekkür ederim.
Teşekkür ederim.
Teşekkür ederim Çok detaylı ve güzel anlatmışıınız.