Wednesday, April 2, 2014

Java 8 ile Gelen Yenilikler

Java’nın çıkışından beri geçirdiği en önemli değişim, hiç tereddütsüz Standard Edition 5 ile gerçekleşmiştir. Kanımca Java SE 8’in çıkışı bu durumu değiştirecektir. Üzerinde uygulama geliştirdiğimiz platformlar değişim geçirirken, uygulama geliştirme ortamları ve programlama dilleri de varlıklarını devam ettirebilmek ve piyasadaki konumlarını güçlendirmek için bu değişime ayak uydurmak zorundadır. Ne yazık ki Java uzun bir süre değişime ayak uydurmakta zorlandı. Java 6 ile Java 7’nin çıkışları arasında yaklaşık 5 yıl bir süre olması bunun bir göstergesidir. Bunun çeşitli nedenleri bulunuyor. Ancak platformun artık Oracle firmasının önderliğinde, daha sağlıklı geliştiğini gözlemliyoruz. Ortalama iki yılda bir platformun yeni bir sürümünün çıkarılması hedefleniyor.
Uzun bir süredir, geliştirdiğimiz uygulamalar çok çekirdekli, yüksek bellek kapasiteli sistemler üzerinde çalışıyor. Çok çekirdekli mimariler hem merkezi işlem biriminde hem de grafik işlemci birimlerinde karşımıza çıkıyor. Örneğin İntel, bu yılın dördüncü çeyreğinde 8 çekirdekli işlemcileri piyasaya sürecek. Bu işletim sistemi tarafında 16 adet sanal işlemci anlamına geliyor. Grafik işlemci biriminde ise çok daha fazla sayıda çekirdek bulunuyor. Bugün ortalama üstü bir ekran kartında bin civarında çekirdek bulunur. Elbette bu çekirdekler özel amaçlı vektörel işlemler yapmak üzere tasarlanmış olsalar da CUDA ya da OpenCL gibi kütüphaneler sayesinde programlanabilirler. Tüm bu gelişmelere rağmen, uygulamalarımızı hala sanki tek işlemcili bir sistem üzerinde çalışacakmış gibi yazıyoruz. Bu durumda çok çekirdekli yapının bilgi işleme kapasitesinden yararlanamıyoruz ve uygulamamız sadece tek bir çekirdek üzerinde çalıştığı için yanıt süresi ya da hesaplama süresinde artan çekirdek sayısı ile birlikte bir iyileşme gözlenmiyor. Bir süredir yazılım geliştirme ortamları, çok çekirdekli mimariler üzerinde uygulama geliştirmeyi kolaylaştıracak çözümler sunmaya başladılar. Java SE 6 platformunda Concurrency API'yi kullanarak çok iş parçacıklı uygulamalar geliştirmek mümkün olsa da Java SE'deki diğer standart API'ler (örneğin Collections, Networking, Security API) çok çekirdekli mimari göz önüne alınarak tasarlanmadıkları için çekirdek sayısı arttırıldığında, yazılımın çalışma zamanındaki başarımında bir iyileşme gözlenmiyor. Üstelik elimizdeki problemi paralel olarak çözmek üzere parçalara ayırıp, ardından her bir parçayı, bir iş parçası (=Thread) ile çözmeye çalıştığımızda, ölçeklenebilir bir çözüm elde etmekte zorlanıyoruz. Problemi daha akıllı bir biçimde alt problemlere parçalayacak ve bu alt problemlerin çözümüne götüren iş parçaları arasında düşük eş zamanlama (synchronization) maliyeti olan yapılara ihtiyacımız var. Nihayet Java 7 Böl/Katıl çatısı (Fork/Join Framework) olarak adlandırılan bir çözüm sunarak çok çekirdekli sistemlerde uygulama geliştirmeyi nispeten kolaylaştırdı. Bu çözüm, Concurrency API ile gelen yeni bir iş parçası havuzu olan ForkJoinPool ve bu havuza atayacağımız görevleri tanımladığımız RecursiveAction ve RecursiveTask sınıflarını kullanmaktadır. Buna rağmen, Böl/Katıl çatısı, alt düzeyde bir çözüm sunduğu için bu çatıyı kullanarak uygulama geliştirmek hala zordur. Java 8 ile gelen Stream API’si, Lambda ifadeleri ve ihtiyaç duyulduğunda değerlendirme (Lazy Evalution) gibi yeni özellikler sayesinde soyutlama seviyesinin üst düzeye çıktığını görüyoruz. Artık görünürde iş parçası ve eş zamanlama yapıları olmadan paralel programlama yapabileceğiz. Bu hem geliştirme hem de hata ayıklama sürelerinin kısalması anlamına geliyor. Örneğin, geliştirici, Collection API ile hazır olarak gelen paralel kapları ya da Dönüştür-İndirge (=MapReduce) çerçevesini kullanarak çekirdek sayısına göre ölçeklenebilir çözümlere hızlıca ulaşabilir. Başta Lambda ifadeleri ve Dönüştür-İndirge çatısı ile gelen yenilikler, Java platformu üzerinde kodlama biçimlerimizi, bu zamana kadar hiç olmadığı kadar değiştirecektir. Üstelik Java 8 ile gelen yeniliklerin bir kısmından kodda basit değişiklikler yaparak yararlanmak mümkün olacaktır. Örneğin, Java 8'de Arrays yardımcı sınıfına, diziler üzerinde paralel sıralama yapmamızı sağlayan parallelSort() metodu eklenmiştir. Bu yeteneği kullanmak için tek yapmanız gereken, Arrays.sort() çağrısını Arrays.parallelSort() çağrısı ile değiştirmektir. Paralel sıralamada Java 7 ile gelen Böl/Katıl çatısı kullanılmıştır. Java 8 ile birlikte sadece diziler üzerinde değil Collection API içinde yer alan kaplar üzerinde de paralel işler çalıştırmak mümkün olabilmektedir. Java 8 platformunun, çok çekirdekli sistemler üzerinde önemli bir hızlanma sağlaması beklenmelidir. Ancak bu hızlanmanın, ancak uygulamamıza adanmış bir sistemde sağlanabileceğini unutmayın. Çünkü tüm çekirdekleri kullandığımızda diğer uygulamaların işlemci zamanından çalıyoruz. Dolayısı ile diğer uygulamalar için bu durum başarımın düşmesi anlamına gelir.
Java 7 ile birlikte çok uzun bir süre sonra Java Sanal Makinası (JSM) yeni bir bytecode ile tanıştı: invokedynamic. Her ne kadar yeni bir komutla tanışmış olsak da henüz Java 7'de derlendiğinde invokedynamic kodu ürettirecek herhangi bir Java ifadesi bulunmuyor. Java 8'de ise fonksiyonel programlama ve arayüz metot eklentileri (=Method Extensions) içeren kodlar derlendiklerinde invokedynamic kodu içerebilecek. Java 7’ye kadar, Java platformlarının tek dilli olduğunu söyleyebiliriz. Çok sayıda Java platformu var ve bu platformlarda uygulama geliştirecekseniz Java programlama dilini kullanmak zorundasınız. Elbette bunda bir sorun yok. Java programlama dili C++ ile karşılaştırıldığında dil olarak daha yalın, yazılan kodun okunması ve bakımı daha kolay, üstelik sizi iyi kod yazmaya teşvik ediyor. Ancak her türlü problem için Java en uygun programlama dili olmayabilir. Başka programlama dilleri de var. Java 6 ile birlikte gelen Scripting API ile Javascript, Awk, Lisp, Python gibi birçok farklı dilde yazdığımız kod, JSM üzerinde çalışabiliyor. Scripting API, yoğun olarak Reflection API’yi kullanır. Bu da bu kod parçalarının yavaş çalıştığı anlamına geliyor. Aynı Java’nın ilk çıktığı dönemde C/C++ ile yazdığımız koda göre on kat daha yavaş çalışmasına benzer bir yavaşlıktan bahsediyoruz. İşte invokedynamic ve JSM’nin yapısındaki yenilikler Java programlama dili dışındaki dillerin, JSM üzerinde yüksek başarımla çalışmasına olanak sağlamaktadır. Java platformu çok dilli bir platforma doğru evriliyor. Bunun ilk uygulaması Java 8 ile birlikte gelen yeni Javascript motoru Nashorn olacaktır. Platformdaki bu yenilikler sayesinde Javascript kodlarının çok hızlı çalıştığına tanık olacaksınız. Java 7 bunun altyapısını oluşturdu. Java 8 ise ilk uygulamasını bize sunuyor.
Java denince genellikle akla ilk olarak Java programlama dili gelmektedir. Bu çok da yanlış sayılmaz. Ancak Java öncelikli olarak bir platformun adıdır. Bu platform bize Java programlama dilini kullanarak uygulama geliştirmek, geliştirdiğimiz bu uygulamayı dağıtmak ve en sonunda bu uygulamayı çalıştırmak için bir ortam sağlar. Üstelik bu platform tek de değildir. Java Standard Edition (SE), Java Enterprise Edition (EE), Java Micro Edition (ME), Gömülü Java, Java Card, Java TV gibi biri biriyle ilintili ama farklı platformlar/API’ler bulunmaktadır. Java platformunun en güçlü bileşeni ise Java uygulamalarını çalıştıran Java Sanal Makinasıdır (JSM). Java uygulamaları JSM üzerinde çalışır. JSM satın alabileceğiniz, üretebileceğiniz bir işlemci tanımlar. Bu işlemcinin bir komut kümesi, yığın temelli adresleme kipleri, saklayıcı kümesi, yığın göstergesi, program sayacı ve bir bellek modeli vardır. JSM içinde uygulamamızın yürütme zamanındaki başarımını belirleyen iki önemli parça yer alır: JIT (Just-in-Time) Derleyici ve Çöp Toplayıcı (Garbage Collector). JIT derleyici bytecode’ları JSM’nin üzerinde çalıştığı platformun anlayacağı, örneğin Intel x86 kodlarına dönüştürür. Üstelik bunu yaparken devingen en iyileme de yapar. Bunun için uygulamanın basit bir kesitini (=profiling) alır. Bu kesit bilgisine göre onlarca en iyileme tekniğinden hangilerini uygulayacağına karar verir. Java geliştiricisi açık ve yalın kod yazmaktan sorumludur, bu kodun en yüksek başarımla çalıştırılmasından ise JIT derleyici sorumludur. Yürütme zamanında ortaya çıkan bellek ihtiyacını ise Heap olarak adlandırılan bir bellek alanından new işlecini kullanarak karşılıyoruz. new ile aldığımız alanı işimiz bitince geri vermemiz gerekmez. Bu alanın yönetiminden Çöp toplayıcı sorumludur. Çöp toplayıcı, temel olarak Heap’de canlı nesneleri saptar ve ölü nesneleri yeniden kullanılabilir bellek alanlarına dönüştürür. Çöp toplayıcı geliştiricinin üzerinden önemli bir yükü alır. Ancak bunun için yürütme zamanında bir bedel ödenir. HotSpot içinde çok sayıda çöp toplayıcı yer alır. Bunlardan hangilerinin seçileceği ve en iyilenmesi önemli bir konudur. JDK 7u4 ile birlikte G1 (Garbage First) olarak adlandırılan yeni bir çöp toplayıcısı geldi. Uzun dönemde, G1’in Java platformunun tek çöp toplayıcısı olması hedeflenmektedir. JSM’nin en zayıf halkası çöp toplayıcıdır. Çöp toplama sırasında tüm uygulamanın iş parçalarının durdurulduğu "tüm dünyanın durduğu" (stop-the-world) bir evre vardır. Bu evre boyunca uygulama hiçbir iş yapmaz. Ne yazık ki çöp toplayıcı algoritmaları Heap boyutuna göre ölçeklenebilir değildir. Heap boyutu arttıkça bu süre de hızla artmaktadır. Örneğin 64GB heap boyutu olan bir JSM’de bu süre bir dakikayı aşabilir. Java 8’de çöp toplama ile ilgili dikkate değer tek gelişme Kalıcı Alan (=Perm(anent)Gen(eration)) olarak adlandırılan alanın genel Heap alanına eklemlenmesidir. Özellikle web uygulamalarında sık yapılan güncellemelerden kaynaklanan bu alanının dolmasına nedeni ile aldığımız taşma hatası ile şimdi daha seyrek karşılaşmayı umuyoruz.
Java’nın çok kipli bir programlama dili olduğu söylenebilir. Örneğin, desteklediği en temel kip nesneye dayalı programlamadır. Bunun dışında üretken programlamaya (Generic Programming) destek verdiği düşünülebilir. Java’daki çözüm, C++’daki template çözümü ile karşılaştırılamaz ve asıl amaç ise derleme zamanında tip güvenliğini sağlamaktır. İlgiye dönük programlamayı (Apsect Oriented Programming) ise reflect paketinde yer alan Proxy sınıfı aracılığı ile gerçekleştirebiliyoruz. Proxy çözümü bir arayüz gerektirdiği ve sınıf metotlarının kendisini ya da sınıfın diğer bir metodunu çağırması durumunda ilgiler çalışmadığı için kullanımı kısıtlıdır. Bu tür durumlarda sınıfın içinde derleme ya da yürütme zamanında değişiklikler yaparak ilgileri sınıfımıza işleyen dokuma tabanlı AspectJ gibi çözümleri kullanıyoruz. Java 8 ile birlikte gelen Lambda ifadeleri ise fonksiyonel programlama yapmamıza olanak sağlıyor. Lambda ifadeleri sayesinde fonksiyonlar artık dilin birinci sınıf vatandaşı olarak işlem görecek. Fonksiyon tipinden bir değişken tanımlayabilecek ve fonksiyona parametre olarak başka bir fonksiyonu geçirebileceğiz. Açıkçası Java 8 öncesinde, LambdaJ, Functional Java, Guava gibi çeşitli kütüphaneler aracılığı ile fonksiyonel programlama yapabiliyorduk. Ama şimdi lambda ifadeleri hem dilin bir parçası haline geldi hem de yüksek başarımla çalışıyorlar. Lambda ifadeleri esas olarak torbalar (collections) üzerinde Dönüştür/İndirge çatısını kullanırken, süçgeç, dönüşüm ve indirgeme ifadelerinin tanımında kullanılacaktır. Torbalar ile paralel programlama yapabilmeyi Stream API’sine borçluyuz. Java 8’de Collection arayüzünde artık seri ve paralel Stream oluşturmamamızı sağlayan metotlar bulunuyor.
Sınıf, nesne, kalıtım ve çok şekillilik nesneye dayalı programlamanın yapı taşlarıdır. Modellemeyi ve gerçeklemeyi bu yapıları kullanarak sağlıyoruz. Java programlama dili de nesneye dayalı bir programlama dilidir. Söz dizimsel özellikleri C++ programlama dilininkine benzer. İşleyişte ise bazı temel farklar bulunur. Bu farklardan biri C++ ve Java'nın çoklu kalıtımı destekleme biçimleriyle ilgilidir. C++, bir sınıfın, birden fazla sınıftan, doğrudan türetilmesine izin verir. Bu çoklu kalıtım olarak adlandırılır. Ancak bu özelliğin sorunlu bazı yönleri bulunmaktadır. Çoklu kalıtımda yer alan sınıfların ortak bir sınıftan türetilmiş olmaları durumunda, bu ortak sınıftaki öznitelikler, tekrarlı olarak, türetilmiş sınıfa geçer ve bu üyeye erişilmesi durumunda, hangi sınıf üzerinden aktarılan üyeye erişildiği ile ilgili olarak bir belirsizlik oluşur. C++’da bu belirsizliği aşmanın sanal kalıtım (virtual inheritence) olarak adlandırılan bir çözümü olsa da bu çözüm hem sezgiseldir hem de başka yan etkileri vardır. Bu nedenle Java’da bir sınıfın sadece tek bir sınıftan türetilmesine izin verilir. Çoklu kalıtıma ihtiyaç duyulan noktalarda ise Java arayüzleri kullanılır. Java arayüzleri arasında çoklu kalıtım vardır. Bir arayüz birden fazla arayüzden türetilebilinir. Java’da arayüzler arasındaki çoklu kalıtımın, C++’daki gibi bir problem çıkarmamasının nedeni ise Java arayüzlerinde veri tanımlayamıyor olmamızdır. Java arayüzlerindeki tüm metotlar soyuttur ve public tanımlı olmak zorundadır. Bu nedenle çoğu zaman hem abstract hem de public tanımlamalarını kullanmıyoruz.
Java 8 ile birlikte arayüzün sözdizimsel yapısında, Lambda ifadelerinin tanımlanabilmesi ve Dönüştür-İndirge çatısının torbalar ile birlikte kullanılabilmesi için önem değişiklikler geldi. Bunlardan ilki varsayılan metottur (default method). Arayüzleri, hizmet alan ve hizmet veren sınıf arasında birer sözleşme olarak düşünebilirsiniz. Hizmet alan sınıf bu arayüzden bir referans tanımlarken, hizmet veren sınıf bu arayüzü gerçekler. Arayüze, yani sözleşmemize, yeni bir metot eklediğimizde kod kırılacaktır. Java ilerlerken bir taraftan da geriye doğru uyumluluğu gözetir. İşte, varsayılan metotlar, arayüze yeni bir metot eklediğimizde kodun kırılmamasını sağlıyor. Bu yeni metodu eklerken gövdesini veriyoruz. Böylelikle bu arayüzü gerçekleyen sınıf, yeni eklenen metoda işlev yüklememiş ise varsayılan metot tanımı kullanılıyor. İkinci yenilik, arayüze statik metot ekleyebiliyoruz. Statik tanımlı metotları, Dönüştür-İndirge çatısında, filter, map ve reduce metotlarına parametre olarak gönderebiliyoruz. Bunları birer Lambda ifadesi olarak düşünebilirsiniz. Adreslerken ise yeni gelen :: metot referans işlecini kullanıyoruz.
2010 yılında Java’nın yönetimi Oracle’a geçtiğinde, Java platformunda yapılması planlanan ve çok uzun bir süredir bekleyen çok sayıda iyileştirme ve eklenti bulunuyordu. Oracle, bu değişiklikleri kısa bir sürede yetiştirmenin zorluklarını görünce, bu değişiklikleri daha iyi yönetebilmek için iki sürüme yayma kararı aldı. Java 7 daha basit ve daha çok geliştiricinin verimliliğini arttıracak ve Java 8’deki yeniliklerin alt yapısını oluşturacak değişiklikleri içeriyor. Bu yüzden Java 5/6’dan Java 7’ye geçiş kolay olacaktır. Java 8 ise çok daha önemli yenilikler içeriyor. Java 8’e geçiş için öncelikli olarak en az Update 10’u beklemek gerekir. Öncelikli olarak dil ve API seviyesinde gelen yenilikleri kavramak gerekir.
Java 9 ve sonrasında Java’da ne tür gelişmeler beklemeliyiz? Java platformunun ve programlama dilinin çok çekirdekli programlamayı Grafik İşlemci Birimlerine (GİB) taşımasını umuyoruz. Bunun önce platform sonrasında dil seviyesinde olmasını bekliyorum. Aslında bunun için OpenJDK içinde bir proje de bulunuyor: Project Sumatra (http://openjdk.java.net/projects/sumatra/). CUDA ya da OpenCL gibi API’leri kullanarak GİB programlama yapmak mümkün. CUDA nVidia firmasının çözümü. Dolayısı ile platform bağımlı, nVidia ekran kartı ya da hızlandırıcısı kullanmanız gerekir. OpenCL ise üreticiden bağımsız bir çözüm sunuyor. Üstelik hem MİB’i hem de GİB’i beraberce programlamak mümkün. Her ikisinde de temel programlama dili C’dir. Java’da bazı metotları Java Native Interface (JNI) kullanarak native tanımlı metot olarak C’de kodlamak mümkündür. Ancak bunun bazı zorlukları bulunuyor. Ayrıca JNI, Java çıktığından beridir hiçbir güncelleme geçirmedi ve çözüm için yetersiz kalıyor. Java 9 veya sonrasında düşünülen yeniliklerden biri JNI 2.0 olacaktır.
Java platformu aynı .Net platformu gibi modüler değildir. Bir projede kullandığımız iki ayrı kütüphanenin, ortak bir kütüphanenin farklı sürümlerini kullanması durumunda, yürütme zamanında sınıf yükleyici ilk önce hangi sınıfı yüklemiş ise o sınıf kullanılacaktır. Sınıf yükleyici aynı sınıfın farklı sürümlerini yüklemesi ve yönetmesi mümkün değildir. Bu problemi, OSGi, Enterprise OSGi ya da JBoss’un MCS (Modular Container Service) ile çözmek mümkün olsa da ağır sıklet çözümler oldukları için geliştirmesi zordur. Uzun süredir beklediğimiz modülerlik çözümü, Jigsaw projesi ile platforma kazandırılması amaçlansa da Java 8’e yetiştirilemeyeceği anlaşıldığı için çıkartılmıştı. Java 9’da bu problemin çözülmesini umuyoruz.
View Binnur Kurt's profile on LinkedIn