Tam söz verilen günde olmasa da, JDK8 nihayet burada. Uzun zamandır Java dilinde yapılmış olan en büyük değişikliğe denk geliyor. Bundan önceki en büyük değişiklik, “Generics” eklenmesi olarak düşünülebilir, o da JDK5 ile gelmişti ki, o şu anda yapılan değişikliklerin yanında ufak kalır…
Hemen bir tur bineyim dedim. Java işlerini, virtual makina içinde Ubuntu var, onun üzerinde yapıyorum. (Neden direkt makinaya kurmuyorsun derseniz, driver falan uğraşmak işime gelmiyor. Bir de, virtual makinada backup falan kolay. İrice bir USB stick varsa, onun içinde taşınabiliyor da. Artık VMWare player da bedava.)
Sonuçta sistem Ubuntu. Java için de yıllardır Eclipse kullanırım. (Alışkanlık. Başka IDE’den hoşlanıyorsanız alınmayın, tepkilenmeyin.) Şimdi bize neler lazım. Bir JDK8 lazım. Maalesef openjdk henüz apt-get ile alınamadığından, Oracle’ın JDK8’ini indirecek bir şekil buldum. Eclipse developer arkadaşlar da boş durmamış, onun için de Eclipse’in kendi derleyicisini güncellemişler. İki cümlede anlattığım meseleyi, üç saat kadar bir sürede, indirdimdi, güncelledimdi, eski Java versiyonun sistemden yok ettimdi derken tamamladım.
Ayarlar yapıldı. Direksiyona geçtim, nereye gideceğimi bilmiyorum. Ne yazsam ki şimdi? İlginci, notasyon da bilmiyorum. Saat de geç olmuştu. Sırt üstü yatıp dokümantasyon okudum biraz. Daha önce de okumuştum ama, şimdi maksatlı okuduğumdan, kod örneklerine bakarak okudum.
Denemeye değer duran şeylerden biri, Java’nın stream() meselesi ve bunun paralel versiyonu (ki parallelStream() ile oluyor). Yani, eğer Microsoft taraflarını biliyorsanız, oradaki Linq işlerine denk geliyor. Bana sorarsanız isimlendirmesi daha iyi olmuş en azından…
Şöyle bir deneyelim dedim ve ilk olarak aşağıdaki kodu yumurtladım:
package com.safkanyazilim.java8; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Random; public class Main { public static void main(String[] args) { List<Integer> list = new ArrayList<Integer>(); Clock clock = Clock.systemDefaultZone(); Random random = new Random(); for (int i=0; i < 10_000_000; i++) { list.add(random.nextInt(1_000_000)); } Instant start = clock.instant(); double sum = list.stream().mapToInt(x -> x).sum(); Instant end = clock.instant(); System.out.println("Sum: " + sum); Duration duration = Duration.between(start, end); System.out.println("Duration : " + duration); } }
Yeniliğin sürüsüne bereket! İkinci satırda Clock geçiyor. Clock, yeni bir şey, java.time paketinin içinden çıkıyor. Eğer bugüne kadar java.util.Date, java.util.Calendar manyaklığından daraldıysanız, size iyi gelecek. Artık zamanı ölçmek için Clock kullanıyoruz (mantıklı!). Zaman aralığı tutmak için Duration var (evet bu da mantıklı!). Test işleri için “durmuş” saat de yapabiliyoruz. Deliliğe son! System.currentTimeMillis() halen kullanılabilir, ama yukarıdaki koda bakarsanız, sanırım bir daha kullanmak istemeyeceksiniz. Benim kullanasım yok.
Random, bildiğimiz Random. Burada yeni bir şey yok. On milyon elemanlı bir ArrayList yaratıyorum. İçinde de sıfırdan bir milyona kadar tam sayılardan var. Kodun içindeki on milyon ve bir milyon sayılarını biraz fazla kolay okumuş olabilir misiniz? JDK8’de bu da yeni. Sayıların istediğiniz yerine “underscore” veya “alt tire” dediğimiz karakteri koyabiliyorsunuz. Mantıklı bir şekilde üçer üçer ayırırsanız, hayat tam olarak bayram olmasa da, gözleriniz şaşı olmuyor, ekranınızı parmaklamıyorsunuz veya içinizden sayarak sıfır tuşuna basmıyorsunuz artık. (Nerdeydiniz lan siz?)
Clock ile ilgili işleri açıklamıyorum. Arife tarif gerekmez. Zaman işleri olması gerektiği gibi basit olmuş.
Mesele şu satırda olup bitiyor:
double sum = list.stream().mapToInt(x -> x).sum();
JDK8 ile, tüm Collection sülalesi, stream() diye yeni bir metod sahibi olmuş. Yaptığı iş, “Map Reduce” tarzı işler. Yani, her bir eleman üzerinden iterate edilecek yapılabilecek işleri, böyle de yapabiliyorsunuz. (Başka dillerde yıllardır var diyecekler hele bir geri dursun, biliyoruz onu. Geç oldu, güç de oldu, ama gel gelelim oldu, anlatıyoruz.) Bu satırda şöyle diyoruz: Listeyi al. Akıt onu dere gibi. Sonra her elemanı “int”e çevir (Map et diyesim var. Türkçesine ne diyeceğiz bunun? Evir?) Bunu da nasıl yapacağını söylemek lazım tabi. Burada yepisyeni “Lambda Expression” karşınızda. “x -> x” özetle şu demek: Argüman olarak x alan ve sonuç olarak x dönen bir fonksiyon. Son adım, “reduce” adımı, yani sonuçları bir araya getirme, toplama. Burada da, sum() diye hazır bir reduce fonksiyonu mevcut. Yani, bu satır ile, tüm sayıları toplamış oluyorum… Lambda ifadesi olarak, x -> x*x deseydim de, karelerini toplamış olacaktım.
Çalıştırdık. Çıktısı şöyle oldu:
Sum: -8.20944701E8 Duration : PT0.045S
Sizde her iki sayı da farklı çıkabilir tabii. Duration çıktısı böyle biraz garip, ama özetle iş 45 milisaniye sürdü diyor. Eh, on milyon sayı topladın, sen de haklısın. Biz bunları neden yaptık? Paralellik görecektik. Abiler diyor ki, işlemi paralel yapmak için, stream() yerine parallelStream() yazmanız yeterli. Eğer tabi tanımladığınız işlem için olabilecek gibiyse… Bizimki basit toplama, bunun için olmayacaksa ne için olacak? Haydi değiştirdik satırı, şöyle yaptık:
double sum = list.parallelStream().mapToInt(x -> x).sum();
Haydi bir daha çalıştır. Süper olacak şimdi her şey. Çalıştırdık. Sonuç:
Sum: 8.9358852E7 Duration : PT0.063S
Lan?!? Hani daha hızlı olacaktı ya bu? Yavaşladı resmen!
Ne oldu? Aslında düşününce, olanı bulmak çok zor değil. Elemanları toplamak o kadar, o kadar basit bir işlem ki, işlemi paralelleştirme maliyeti, işlemin kendi maliyetinin ötesine geçiyor. Bu meselenin etkisini görmek için, daha zor bir iş yaptırmak gerek. Yani, her bir eleman üzerinde yapılacak olan işlem biraz daha uzun sürmeli ki, paralelleştirdiğimize değsin.
Ne yapayım? Zaman alacak bir şey yaptırayım deyince de insanın aklına bir şey gelmiyor. Yıllar önce hesap makinasında bulduğum bir oyun geldi aklıma. Hesap makinasındaki “cos” tuşuna tekrar tekrar basarak, belli bir sayıya yakınsayabiliyorsunuz. İlk sayı ne olursa olsun. Bu sayı sıfır da olmuyor. (x = cos(x) denklemin çözümü oluyor.) Onun için kodu şu hale soktum:
package com.safkanyazilim.java8; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Random; public class Main { public static double transform(double x) { for (int i = 0; i < 100; i++) { x = Math.cos(x); } return x; } public static void main(String[] args) { List<Double> list = new ArrayList<Double>(); Clock clock = Clock.systemDefaultZone(); Random random = new Random(); for (int i=0; i < 10_000_000; i++) { list.add(random.nextDouble()); } Instant start = clock.instant(); double sum = list.stream().mapToDouble(Main::transform).sum(); Instant end = clock.instant(); System.out.println("Sum: " + sum); Duration duration = Duration.between(start, end); System.out.println("Duration : " + duration); } }
Tabii bir kaç değişiklik var. Kosinüs kullanmaya niyet edince, sayılar double’a döndü. Bir de, lambda ifadesi olan yere, Main::transform yazdık. Bu da JDK8’de yeni. Metod referansı. Argüman tipleri falan doğru olunca, metodu böyle verip, “al bunu kullan” diyebiliyoruz artık. Bunun yerine x -> Main.transform(x) de diyebilirdik, aynı anlama gelirdi.
Bastık, çalıştırdık… Sonuç:
Sum: 7390851.332151607 Duration : PT40.925S
Hah. cos(cos(cos(cos(cos…. (x….))))) hesaplatınca, makinayı yeterince yorduk. Çalışırken, “top” ekranına bakarsak, durum şöyle görünüyor:
top - 21:15:00 up 11:40, 2 users, load average: 0.47, 0.30, 0.20 Tasks: 215 total, 1 running, 214 sleeping, 0 stopped, 0 zombie Cpu0 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu1 : 0.0%us, 0.3%sy, 0.0%ni, 99.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu2 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu3 : 0.3%us, 0.0%sy, 0.0%ni, 99.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Mem: 4042108k total, 3379976k used, 662132k free, 110600k buffers Swap: 4192252k total, 2824k used, 4189428k free, 1039376k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 12665 safkan 20 0 3320m 416m 11m S 100 10.5 0:10.49 java 1331 root 20 0 240m 92m 9968 S 0 2.3 3:54.91 Xorg 2597 safkan 20 0 384m 18m 14m S 0 0.5 0:53.43 vmtoolsd 12663 safkan 20 0 17460 1404 980 R 0 0.0 0:00.05 top 1 root 20 0 24580 2356 1352 S 0 0.1 0:01.79 init 2 root 20 0 0 0 0 S 0 0.0 0:00.02 kthreadd 3 root 20 0 0 0 0 S 0 0.0 0:02.86 ksoftirqd/0 5 root 0 -20 0 0 0 S 0 0.0 0:00.00 kworker/0:0H 7 root 0 -20 0 0 0 S 0 0.0 0:00.00 kworker/u:0H 8 root RT 0 0 0 0 S 0 0.0 0:02.99 migration/0 9 root 20 0 0 0 0 S 0 0.0 0:00.00 rcu_bh
Java prosesi, tam olarak bir işlemcinin canına okuyor. Yani, beklediğimiz üzere, paralel olan biten herhangi bir durum yok. Olay kırk saniye sürdüğü için de, “top” ekranına bakmak mümkün oluyor.
Şimdi bakalım dananın kuyruğu kopacak mı? Satırı yeniden değiştirip, JDK8’den paralel işlem yapmasını istiyoruz:
double sum = list.parallelStream().mapToDouble(Main::transform).sum();
Haydi çalıştırdık, gözümüz saatte…. Sonuç:
Sum: 7390851.332151607 Duration : PT14.921S
Hızlandı gerçekten! Çalışırken, CPU fan, sağlam turbo vaziyetine de geçti. Bu esnada (elimizi çabuk tutup) top ekranına bakarsak (işlemin sonuna doğru) şöyle görünüyor:
top - 21:22:31 up 11:47, 2 users, load average: 1.09, 0.52, 0.32 Tasks: 215 total, 1 running, 214 sleeping, 0 stopped, 0 zombie Cpu0 : 99.3%us, 0.7%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu1 : 99.3%us, 0.7%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu2 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu3 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Mem: 4042108k total, 3385228k used, 656880k free, 110812k buffers Swap: 4192252k total, 2824k used, 4189428k free, 1040508k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 12782 safkan 20 0 3515m 416m 11m S 399 10.6 0:41.27 java 1331 root 20 0 242m 94m 9968 S 0 2.4 4:01.91 Xorg 2597 safkan 20 0 384m 18m 14m S 0 0.5 0:54.23 vmtoolsd 5859 safkan 20 0 5801m 1.0g 30m S 0 25.7 12:51.79 java 12780 safkan 20 0 17460 1404 980 R 0 0.0 0:00.05 top 1 root 20 0 24580 2356 1352 S 0 0.1 0:01.79 init 2 root 20 0 0 0 0 S 0 0.0 0:00.02 kthreadd 3 root 20 0 0 0 0 S 0 0.0 0:02.99 ksoftirqd/0 5 root 0 -20 0 0 0 S 0 0.0 0:00.00 kworker/0:0H 7 root 0 -20 0 0 0 S 0 0.0 0:00.00 kworker/u:0H 8 root RT 0 0 0 0 S 0 0.0 0:03.00 migration/0 9 root 20 0 0 0 0 S 0 0.0 0:00.00 rcu_bh
Java gerçekten CPU’ları sağlam pataklıyor. Peki, neden 10-11 saniye olmadı da 14.2 saniye oldu? Her ne kadar virtual makinada dört CPU tanımlı ise de, altında yatan makina iki core sahibi, hyperthreading bir makina. Yani, iki CPU gerçek, diğer ikisi biraz fasulyeden…
Tabii bir anda bayram etmelik bir durum yok. “Yaşasın her şey hızlanacak” gibi bir durum da yok. Toplam gibi, paralelleşmeye çok süper uygun bir işlem kullandık. Ama, bir çok Collection için tüm elemanlara bağımsız olarak uygulanacak işlemleri, artık fazla efor kullanmadan paralelleştirmek mümkün…
Java’daki bu değişiklikler oldukça kökten. Java, “Kingdom of Nouns”, yani “İsimlerin Krallığı” olarak nitelenirdi. Yani, “birinci sınıf” olan şeyler, hep objelerdi. Objeden başka argüman, değişken olmadı hiç. Şimdi, bu değişime uğruyor. Artık metodlara referans verebiliyoruz. Lambda ifadelerimiz var, onunla biraz daha esnek sulara doğru yelken açıyoruz.
Java, “minimalist” bir dildi de. Yani, “bir şeyi yapmanın bir yolu vardır” geçerliydi. Her şeyi uzun yoldan yazardık, kolaylık sağlayan şeyler, dilin içinde yoktu. Bu stream() meselesi, bunun kırıldığı noktalardan biri. Keza, java.time için de aynı şey düşünülebilir; nihayet kendimiz “DateUtil.java” yazmadan Java kodu yazabileceğiz gibi görünüyor.
Daha bir kaç yazı daha yazarım diye umudum var… Bakalım ne olacak…
Emre Sevinç der ki
Eline saglik! Keyifle okudum, birkac sey ogrenip sevindim.
2 eften puften not:
1- top elbette baki lakin htop bir apt-get uzaginda. http://hisham.hm/htop/
2- Su yazilari bir kenara not ettiydim, en yakin zamanda okumak uzere, senin de dusuncelerini merak ediyorum bu baglamda: A Java™ Parallel Calamity ve A Java™ Fork-Join Calamity
Erkin PEHLİVAN der ki
Teşekkürler, güzel yazı olmuş.
Yalnız sayılarda “underscore” veya “alt tire” kullanımı JDK 7 ile geldi.
İlgiyle okudum.
Yaşar Safkan der ki
Evet öyleymiş. O ara ben Java yazmamışım :-).
malik der ki
kodcu.com sitesinde 100 sayfalik ek java 8 ozellikleri mevcut. rahman usta yazmis
http://kodcu.com/java-8-ebook/
iyi calismalar