Neden Zaten Java 8'e Yükseltmeniz Gerekiyor?
Yayınlanan: 2022-03-11Java platformunun en yeni sürümü Java 8, bir yıldan fazla bir süre önce piyasaya sürüldü. Birçok şirket ve geliştirici, bir platform sürümünden diğerine geçişle ilgili birçok sorun olduğundan, anlaşılabilir bir durum olan önceki sürümlerle çalışmaya devam ediyor. Buna rağmen, birçok geliştirici hala eski Java sürümleriyle yeni uygulamalar başlatıyor. Bunu yapmak için çok az iyi neden vardır, çünkü Java 8 dile bazı önemli iyileştirmeler getirmiştir.
Java 8'de pek çok yeni özellik var. Size en kullanışlı ve ilginç olanlardan birkaçını göstereceğim:
- Lambda ifadeleri
- Koleksiyonlarla çalışmak için Akış API'sı
-
CompletableFuture
ile asenkron görev zincirleme - Yepyeni Zaman API'si
Lambda İfadeleri
Bir lambda , referans alınabilen ve gelecekte bir veya daha fazla kez yürütülmek üzere başka bir kod parçasına iletilebilen bir kod bloğudur. Örneğin, diğer dillerdeki anonim işlevler lambdalardır. İşlevler gibi, lambdalar, yürütme sırasında sonuçlarını değiştirerek argümanlar iletilebilir. Java 8, lambdalar oluşturmak ve kullanmak için basit bir sözdizimi sunan lambda ifadelerini tanıttı.
Bunun kodumuzu nasıl iyileştirebileceğine dair bir örnek görelim. Burada iki Integer
değerini modulo 2 ile karşılaştıran basit bir karşılaştırıcımız var:
class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }
Gelecekte, bu karşılaştırıcının gerekli olduğu yerde kodda bu sınıfın bir örneği şöyle çağrılabilir:
... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...
Yeni lambda sözdizimi bunu daha basit yapmamızı sağlıyor. BinaryComparator
compare
yöntemiyle aynı şeyi yapan basit bir lambda ifadesi:
(Integer i1, Integer i2) -> i1 % 2 - i2 % 2;
Yapının bir fonksiyonla birçok benzerliği vardır. Parantez içinde bir argüman listesi oluşturduk. Sözdizimi ->
bunun bir lambda olduğunu gösterir. Ve bu ifadenin sağ tarafında lambdamızın davranışını kurduk.
Şimdi önceki örneğimizi iyileştirebiliriz:
... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...
Bu nesne ile bir değişken tanımlayabiliriz. Nasıl göründüğüne bir bakalım:
Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;
Şimdi bu işlevi şu şekilde yeniden kullanabiliriz:
... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...
Bu örneklerde lambda'nın, önceki örnekte BinaryComparator
örneğinin iletildiği şekilde sort()
yöntemine iletildiğine dikkat edin. JVM, lambda'yı doğru yorumlamayı nasıl biliyor?
Fonksiyonların lambdaları argüman olarak almasına izin vermek için Java 8 yeni bir konsept sunar: fonksiyonel arayüz . İşlevsel bir arabirim, yalnızca bir soyut yöntemi olan bir arabirimdir. Aslında Java 8, lambda ifadelerini işlevsel bir arabirimin özel bir uygulaması olarak ele alır. Bu, yöntem argümanı olarak bir lambda almak için, bu argümanın bildirilen tipinin sadece işlevsel bir arayüz olması gerektiği anlamına gelir.
İşlevsel bir arabirim bildirdiğimizde, geliştiricilere bunun ne olduğunu göstermek için @FunctionalInterface
gösterimini ekleyebiliriz:
@FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }
Şimdi, aşağıdaki gibi farklı davranışlar elde etmek için farklı lambdalardan geçerek sendDTO
yöntemini çağırabiliriz:
sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));
Yöntem Referansları
Lambda bağımsız değişkenleri, bir işlevin veya yöntemin davranışını değiştirmemize izin verir. Son örnekte gördüğümüz gibi, bazen lambda yalnızca başka bir yöntemi ( sendToAndroid
veya sendToIos
) çağırmaya yarar. Bu özel durum için Java 8, uygun bir kestirme yol sunar: yöntem referansları . Bu kısaltılmış sözdizimi, bir yöntemi çağıran ve objectName::methodName
biçimine sahip bir lambda'yı temsil eder. Bu, önceki örneği daha da özlü ve okunabilir hale getirmemizi sağlar:
sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);
Bu durumda, sendToAndroid
ve sendToIos
yöntemleri this
sınıfta uygulanır. Başka bir nesnenin veya sınıfın yöntemlerine de başvurabiliriz.
Akış API'sı
Java 8, Collections
ile çalışmak için yepyeni bir Akış API'si biçiminde yeni yetenekler getiriyor. Bu yeni işlevsellik, java.util.stream
paketi tarafından sağlanır ve koleksiyonlarla programlamaya daha işlevsel bir yaklaşım getirmeyi amaçlar. Göreceğimiz gibi, bu büyük ölçüde az önce tartıştığımız yeni lambda sözdizimi sayesinde mümkün.
Stream API, koleksiyonların kolay filtrelenmesi, sayılması ve eşleştirilmesinin yanı sıra, bunlardan bilgi dilimleri ve alt kümeleri almanın farklı yollarını sunar. İşlevsel stil sözdizimi sayesinde Stream API, koleksiyonlarla çalışmak için daha kısa ve daha zarif kodlara izin verir.
Kısa bir örnekle başlayalım. Bu veri modelini tüm örneklerde kullanacağız:
class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }
2005'ten sonra kitap yazan bir books
koleksiyonundaki tüm yazarları basmamız gerektiğini düşünelim. Java 7'de bunu nasıl yapardık?
for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }
Ve bunu Java 8'de nasıl yapardık?
books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element
Bu sadece bir ifadedir! Herhangi bir Collection
stream()
yöntemini çağırmak, o koleksiyonun tüm öğelerini içine alan bir Stream
nesnesi döndürür. Bu, Stream API'den filter()
ve map()
gibi farklı değiştiricilerle değiştirilebilir. Her değiştirici, daha fazla manipüle edilebilecek değişikliğin sonuçlarıyla birlikte yeni bir Stream
nesnesi döndürür. .forEach()
yöntemi, ortaya çıkan akışın her bir örneği için bazı eylemler gerçekleştirmemize izin verir.
Bu örnek aynı zamanda fonksiyonel programlama ile lambda ifadeleri arasındaki yakın ilişkiyi de gösterir. Akıştaki her bir yönteme iletilen bağımsız değişkenin özel bir lambda veya bir yöntem başvurusu olduğuna dikkat edin. Teknik olarak, her değiştirici önceki bölümde açıklandığı gibi herhangi bir işlevsel arabirimi alabilir.
Stream API, geliştiricilerin Java koleksiyonlarına yeni bir açıdan bakmasına yardımcı olur. Şimdi her ülkede mevcut dillerin bir Map
almamız gerektiğini hayal edin. Bu, Java 7'de nasıl uygulanır?
Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }
Java 8'de işler biraz daha düzenli:
import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));
collect()
yöntemi, bir akışın sonuçlarını farklı şekillerde toplamamızı sağlar. Burada önce ülkeye göre gruplandırdığını, ardından her grubu dile göre haritaladığını görebiliriz. ( groupingBy()
ve toSet()
, Collectors
sınıfındaki statik yöntemlerdir.)

Stream API'nin birçok başka yeteneği vardır. Tam belgeler burada bulunabilir. Bu paketin sunduğu tüm güçlü araçları daha iyi anlamak için daha fazla okumanızı tavsiye ederim.
CompletableFuture
ile Asenkron Görev Zincirleme
Java 7'nin java.util.concurrent
paketinde, gelecekteki bazı asenkron görevlerin durumunu veya sonucunu almamızı sağlayan Future<T>
arabirimi vardır. Bu işlevi kullanmak için şunları yapmalıyız:
- Eşzamansız görevlerin yürütülmesini yöneten ve ilerlemelerini izlemek için
Future
nesneleri oluşturabilen birExecutorService
oluşturun. - Eşzamansız olarak
Runnable
bir görev oluşturun. - Görevi, duruma veya sonuçlara erişim sağlayan bir
Future
sağlayacak olanExecutorService
içinde çalıştırın.
Asenkron bir görevin sonuçlarından yararlanmak için, Future
arayüzünün yöntemlerini kullanarak ilerlemesini dışarıdan izlemek ve hazır olduğunda, sonuçları açıkça almak ve bunlarla daha fazla eylem gerçekleştirmek gerekir. Bu, özellikle çok sayıda eşzamanlı görev içeren uygulamalarda hatasız olarak uygulanması oldukça karmaşık olabilir.
Ancak Java 8'de Future
kavramı, zaman uyumsuz görev zincirlerinin oluşturulmasına ve yürütülmesine izin veren CompletableFuture<T>
arabirimiyle daha da ileri götürülür. Java 8'de asenkron uygulamalar oluşturmak için güçlü bir mekanizmadır, çünkü tamamlandıktan sonra her görevin sonuçlarını otomatik olarak işlememize izin verir.
Bir örnek görelim:
import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);
CompletableFuture.supplyAsync
yöntemi, varsayılan Executor
(genellikle ForkJoinPool
) yeni bir zaman uyumsuz görev oluşturur. Görev tamamlandığında, sonuçları otomatik olarak this::getLinks
işlevine bağımsız değişkenler olarak sağlanır ve bu aynı zamanda yeni bir eşzamansız görevde de çalıştırılır. Son olarak, bu ikinci aşamanın sonuçları otomatik olarak System.out
yazdırılır. thenApply()
ve thenAccept()
, Executors
öğesini manuel olarak kullanmadan eşzamanlı görevler oluşturmanıza yardımcı olacak birkaç kullanışlı yöntemden yalnızca ikisidir.
CompletableFuture
, karmaşık eşzamansız işlemlerin sıralanmasını yönetmeyi kolaylaştırır. Üç görevle çok adımlı bir matematiksel işlem oluşturmamız gerektiğini varsayalım. Görev 1 ve Görev 2 , ilk adım için bir sonuç bulmak için farklı algoritmalar kullanır ve bunlardan yalnızca birinin çalışacağını, diğerinin başarısız olacağını biliyoruz. Ancak hangisinin işe yarayacağı, önceden bilmediğimiz giriş verilerine bağlıdır. Bu görevlerin sonucu görev 3'ün sonucuyla toplanmalıdır. Bu nedenle, görev 1 veya görev 2'nin sonucunu ve görev 3'ün sonucunu bulmamız gerekiyor. Bunu başarmak için şöyle bir şey yazabiliriz:
import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution
Java 8'in bunu nasıl ele aldığını incelersek, üç görevin de aynı anda, asenkron olarak çalıştırılacağını göreceğiz. Görev 2'nin bir istisna dışında başarısız olmasına rağmen, nihai sonuç başarılı bir şekilde hesaplanacak ve yazdırılacaktır.
CompletableFuture
, birden fazla aşama içeren eşzamansız görevler oluşturmayı çok daha kolay hale getirir ve bize her aşamanın sonunda tam olarak hangi eylemlerin gerçekleştirilmesi gerektiğini tanımlamamız için kolay bir arayüz sağlar.
Java Tarih ve Saat API'si
Java'nın kendi kabulünde belirtildiği gibi:
Java SE 8 sürümünden önce, Java tarih ve saat mekanizması
java.util.Date
,java.util.Calendar
vejava.util.TimeZone
sınıfları ve bunlarınjava.util.GregorianCalendar
. Bu sınıflar, aşağıdakiler de dahil olmak üzere çeşitli dezavantajlara sahipti:
- Calendar sınıfı güvenli tipte değildi.
- Sınıflar değişken olduğu için çok iş parçacıklı uygulamalarda kullanılamazlar.
- Alışılmadık ay sayıları ve tip güvenliğinin olmaması nedeniyle uygulama kodundaki hatalar yaygındı.”
Java 8, tarih ve saatle çalışmak için sınıflar içeren yeni java.time
paketiyle nihayet bu uzun süredir devam eden sorunları çözüyor. Hepsi değişmezdir ve hemen hemen tüm Java geliştiricilerinin uygulamalarında yerel Date
, Calendar
ve TimeZone
yerine kullandığı popüler Joda-Time çerçevesine benzer API'lere sahiptir.
İşte bu paketteki faydalı sınıflardan bazıları:
-
Clock
- Saat dilimiyle birlikte geçerli an, tarih ve saat dahil geçerli saati söyleyen bir saat. -
Duration
vePeriod
- Bir süre.Duration
, "76.8 saniye" gibi zamana dayalı değerleri ve "4 yıl, 6 ay ve 12 gün" gibi tarihe dayalı olanPeriod
değerlerini kullanır. -
Instant
- Çeşitli formatlarda anlık bir zaman noktası. -
LocalDate
,LocalDateTime
,LocalTime
,Year
,YearMonth
- ISO-8601 takvim sisteminde saat dilimi olmayan bir tarih, saat, yıl, ay veya bunların bir kombinasyonu. -
OffsetDateTime
,OffsetTime
- "2015-08-29T14:15:30+01:00" gibi, ISO-8601 takvim sisteminde UTC/Greenwich'ten ötelenmiş bir tarih-saat. -
ZonedDateTime
- ISO-8601 takvim sisteminde “1986-08-29T10:15:30+01:00 Europe/Paris” gibi ilişkili bir saat dilimine sahip bir tarih-saat.
Bazen “ayın ilk Salı günü” gibi göreceli bir tarih bulmamız gerekir. Bu durumlar için java.time
özel bir TemporalAdjuster
sınıfı sağlar. TemporalAdjuster
sınıfı, statik yöntemler olarak kullanılabilen standart bir ayarlayıcı kümesi içerir. Bunlar şunları yapmamızı sağlar:
- Ayın ilk veya son gününü bulun.
- Sonraki veya önceki ayın ilk veya son gününü bulun.
- Yılın ilk veya son gününü bulun.
- Sonraki veya önceki yılın ilk veya son gününü bulun.
- "Haziran ayının ilk Çarşambası" gibi bir ay içindeki haftanın ilk veya son gününü bulun.
- "Gelecek Perşembe" gibi haftanın sonraki veya önceki gününü bulun.
İşte ayın ilk Salı gününün nasıl alınacağına dair kısa bir örnek:
LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
Özette Java 8
Gördüğümüz gibi Java 8, Java platformunun çığır açan bir sürümüdür. Özellikle Java'ya daha işlevsel programlama yetenekleri getirme hareketini temsil eden lambdaların tanıtımıyla birlikte birçok dil değişikliği var. Stream API, lambdaların zaten alıştığımız standart Java araçlarıyla çalışma şeklimizi nasıl değiştirebileceğine iyi bir örnektir.
Ayrıca Java 8, eşzamansız programlama ile çalışmak için bazı yeni özellikler ve tarih ve saat araçlarının çok ihtiyaç duyulan revizyonunu da beraberinde getiriyor.
Birlikte, bu değişiklikler Java dili için ileriye doğru büyük bir adımı temsil eder ve Java geliştirmeyi daha ilginç ve daha verimli hale getirir.