Neden Zaten Java 8'e Yükseltmeniz Gerekiyor?

Yayınlanan: 2022-03-11

Java 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.

JAVA 8 LAMBDA İFADE

Ş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.)

JAVA 8 AKIŞ API'si

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:

  1. Eşzamansız görevlerin yürütülmesini yöneten ve ilerlemelerini izlemek için Future nesneleri oluşturabilen bir ExecutorService oluşturun.
  2. Eşzamansız olarak Runnable bir görev oluşturun.
  3. Görevi, duruma veya sonuçlara erişim sağlayan bir Future sağlayacak olan ExecutorService 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 ile JAVA 8 ASENKRON PROGRAMLAMA

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 ve java.util.TimeZone sınıfları ve bunların java.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 ve Period - 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ı olan Period 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.

JAVA 8 ZAMAN API'si

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)); }
Hala Java 7 kullanıyor musunuz? Programla alın! #Java8
Cıvıldamak

Ö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.