Java'nın Veri Dünyasına Lambda İle Yolculuğu - III

Lambda İfadelerinin Tam Zamanı

Geldiğimiz noktada yaşadığımız sorunların tümünü lambda ifadelerini kullanarak çözebiliriz. Fakat öncesinde bir miktar sadeleştirme yapmamız gerekecek.

java.util.function

Bir önceki örnekte fonksiyonel arayüz olan MyTest arayüzünü metotlara parametre geçirirken kullanmıştık. Aslında böyle bir arayüzü yazmamıza gerek yok, zira Java SE 8 java.util.function paketi ile bize bu amaçlar için kullanabileceğimiz çok sayıda hazır arayüz sunulmaktadır. Bizim durumumuz için Predicate arayüzü tam olarak işimizi görmektedir.


3 public interface Predicate<T> {
4   public boolean test(T t);
5 }

Buradaki test metodu değişken olarak generic bir sınıf kabul etmekte ve boolean bir değer dönmektedir ki bu da bizim tam olarak ihtiyacımız olan şeydir. Bu arayüzü de kodumuza eklediğimizde son hali şu şekilde olacaktır.

RoboContactsLambda.java
  1 package com.example.lambda;
  2
  3 import java.util.List;
  4 import java.util.function.Predicate;
  5
  6 /**
  7 *
  8 * @author MikeW
  9 */
 10 public class RoboContactLambda {
 11  public void phoneContacts(List<Person> pl, Predicate<Person> pred){
 12    for(Person p:pl){
 13      if (pred.test(p)){
 14        roboCall(p);
 15      }
 16    }
 17  }
 18
 19  public void emailContacts(List<Person> pl, Predicate<Person> pred){
 20    for(Person p:pl){
 21      if (pred.test(p)){
 22        roboEmail(p);
 23      }
 24    }
 25  }
 26
 27  public void mailContacts(List<Person> pl, Predicate<Person> pred){
 28    for(Person p:pl){
 29      if (pred.test(p)){
 30        roboMail(p);
 31      }
 32    }
 33  }  
 34  
 35  public void roboCall(Person p){
 36    System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
 37  }
 38  
 39  public void roboEmail(Person p){
 40    System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
 41  }
 42  
 43  public void roboMail(Person p){
 44    System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
 45  }
 46
 47 }

Son değişiklik ile kullandığımız fonksiyonel arayüzü, tanımlı bir arayüz olan Predicate ile değiştirmiş olduk. Bu yaklaşımla sadece üç metot kullanarak seçim yapma imkânımız olacak. Metotlara geçireceğimiz lambda ifadeleri sayesinde şartları sağlayan Person nesnelerinin yığınlarını elde etmiş olacağız.


Dikey Problem Çözüldü

Lambda ifadeleri sayesinde dikey problemimiz çözüldü ve böylece ifadelerin tekrar kullanımı artık mümkün hale geldi. Şimdi kodumuzun lambda ifadeleri eklenerek düzenlenmiş son haline bir bakalım.

RoboCallTest04.java
  1 package com.example.lambda;
  2
  3 import java.util.List;
  4 import java.util.function.Predicate;
  5
  6 /**
  7 *
  8 * @author MikeW
  9 */
 10 public class RoboCallTest04 {
 11  
 12  public static void main(String[] args){
 13
 14    List<Person> pl = Person.createShortList();
 15    RoboContactLambda robo = new RoboContactLambda();
 16    
 17    // Predicates
 18    Predicate<Person> allDrivers = p -> p.getAge() >= 16;
 19    Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 &&       
p.getGender() == Gender.MALE;
 20    Predicate<Person> allPilots = p -> p.getAge() >= 23 && p.getAge() <= 65;
 21    
 22    System.out.println("\n==== Test 04 ====");
 23    System.out.println("\n=== Calling all Drivers ===");
 24    robo.phoneContacts(pl, allDrivers);
 25    
 26    System.out.println("\n=== Emailing all Draftees ===");
 27    robo.emailContacts(pl, allDraftees);
 28    
 29    System.out.println("\n=== Mail all Pilots ===");
 30    robo.mailContacts(pl, allPilots);
 31    
 32    // Mix and match becomes easy
 33    System.out.println("\n=== Mail all Draftees ===");
 34    robo.mailContacts(pl, allDraftees);  
 35    
 36    System.out.println("\n=== Call all Pilots ===");
 37    robo.phoneContacts(pl, allPilots);    
 38    
 39  }
 40 }

Gördüğünüz gibi her bir grup seçimi için birer Predicate  arayüzü tanımlandı. (allDrivers, allDraftees ve allPilots) Bu arayüz tanımlarından herhangi birini grupların ilgili işlemlerini yapan metotlara parametre olarak geçirebiliriz. Böylece kodumuz daha derli toplu, kolay okunabilir ve tekrarları olmayan bir hale dönüşmüş oldu.

Bu pakette neler var :java.util.function

Yukarıda incelediğimiz Predicate fonksiyonel arayüzü gibi daha bir çok yeni arayüz olduğundan bahsetmiştik. Şimdi bunlardan başlıca bir kaç tanesini inceleyelim.

  • Predicate:  değişken olarak, konu olan nesnenin kendisi parametre olarak geçiliyor ve boolean bir değer dönüyor.
  • Consumer: değişken olarak verilen nesne üzerinde bir işlem yapılıyor ve geri dönüş değeri yok.
  • Function: değişken olarak verilen bir nesne üzerinde işlem yapıp yeni bir nesne geri dönüyor.
  • Supplier: herhangi bir değişken almaksızın bir nesne geri dönüyor. Bir nevi nesne üreteci.
  • UnaryOperator: verilen değeri dönen tekil bir işlemci.
  • BinaryOperator: değişken olarak verilen iki nesneden karşılaştırıcısına göre küçük olanı döner.

Ayrıca bu fonksiyonel arayüzlerin bir çoğunun nesne olmayan ilkel sürümleri de mevcuttur.

Doğu tipi isimlendirme ve metot referansları
Batıdaki ülkelerde genellikle isimler yazılırken ilk olarak ad ardından soyad yazılır. Fakat doğu ülkelerinde ise genellikle soyad önce ad sonra yazılır. Şimdi, önceki örneği tekrar ele alıp Person nesnesini yazmak için daha esnek bir yapı kuralım. Bu yeni yapıda yazım işlemini hem doğu hem de batı tarzında yapabilmemiz gerekiyor.

Lambda Öncesi Örneği

Aşağıda lambda kullanmadan nasıl çözüm üretildiğinin örneğini bulabilirsiniz.

Person.java
128  public void printWesternName(){
129  
130      System.out.println("\nName: " + this.getGivenName() + " " + this.getSurName() +  "\n" +
131             "Age: " + this.getAge() + " " + "Gender: " + this.getGender() + "\n" +
132             "EMail: " + this.getEmail() + "\n" +
133             "Phone: " + this.getPhone() + "\n" +
134             "Address: " + this.getAddress());
135  }
136    
137    
138    
139  public void printEasternName(){
140      
141    System.out.println("\nName: " + this.getSurName() + " " + this.getGivenName() + "\n" +
142             "Age: " + this.getAge() + " " + "Gender: " + this.getGender() + "\n" +
143             "EMail: " + this.getEmail() + "\n" +
144             "Phone: " + this.getPhone() + "\n" +
145             "Address: " + this.getAddress());
146  }

Her bir yazım tarzı için bir metot yazılarak sorun çözülüyor.

Fonksiyon Arayüzü

Function arayüzü bu sorun için iyi bir çözüm. Tek bir metodu var, apply ve görünümü ise şöyle :

public R apply(T t){ }

Genel bir sınıfı T değişken olarak alıp R yi geri dönüyor. Bizim örneğimizi için Person nesnesini alıp String nesnesini dönüyor. Şimdi bu bilgileri kullanarak hedeflediğimiz daha esnek metodu yazalım.

Person.java
123  public String printCustom(Function <Person, String> f){
124      return f.apply(this);
125  }
126  


Daha basit bir görünüm var ama nasıl çalışıyor ? Aşağıdaki kod, uygulamanın nasıl yapıldığını gösteriyor.

NameTestNew.java

9 public class NameTestNew {
10
11  public static void main(String[] args) {
12    
13    System.out.println("\n==== NameTestNew02 ===");
14    
15    List<Person> list1 = Person.createShortList();
16    
17    // Print Custom First Name and e-mail
18    System.out.println("===Custom List===");
19    for (Person person:list1){
20        System.out.println(
21            person.printCustom(p -> "Name: " + p.getGivenName() + " EMail: " + p.getEmail())
22        );
23    }
24
25    
26    // Define Western and Eastern Lambdas
27    
28    Function<Person, String> westernStyle = p -> {
29      return "\nName: " + p.getGivenName() + " " + p.getSurName() + "\n" +
30             "Age: " + p.getAge() + " " + "Gender: " + p.getGender() + "\n" +
31             "EMail: " + p.getEmail() + "\n" +
32             "Phone: " + p.getPhone() + "\n" +
33             "Address: " + p.getAddress();
34    };
35    
36    Function<Person, String> easternStyle =  p -> "\nName: " + p.getSurName() + " "
37            + p.getGivenName() + "\n" + "Age: " + p.getAge() + " " +
38            "Gender: " + p.getGender() + "\n" +
39            "EMail: " + p.getEmail() + "\n" +
40            "Phone: " + p.getPhone() + "\n" +
41            "Address: " + p.getAddress();   
42    
43    // Print Western List
44    System.out.println("\n===Western List===");
45    for (Person person:list1){
46        System.out.println(
47            person.printCustom(westernStyle)
48        );
49    }
50
51    // Print Eastern List
52    System.out.println("\n===Eastern List===");
53    for (Person person:list1){
54        System.out.println(
55            person.printCustom(easternStyle)
56        );
57    }
58    
59    
60  }
61 }

Kodu üç ayrı kısımda inceleyelim. İlk bölüm döngü olan bölüm. Buradaki işlem çok basit. Döngü içinde tüm Person nesneleri alınarak isim ve eposta alanları istenilen biçimde yazılıyor. İkinci bölümde iki fonksiyon tanımlanıyor. Bu tanım lambda ifadeleri ile yapılıyor. Burada satır değil ama gövde tipi bir lambda ifadesi tanımı yapılıyor. Verilen nesne için yapılacak işlemler bir blok halinde veriliyor. Sonuçta iki fonksiyon elde ediyoruz, batı ve doğu tarzı. Son bölümde ise iki adet döngü var. İlk döngüye batı tarzı,  ikinciye doğu tarzı yazım biçim, değişken olarak geçiliyor..

Sonuç olarak sadece değişken olarak tanımlı fonksiyon verilerek tek bir metot ile sorun çözülmüş oldu.  Böylece daha esnek bir yapıya sahip olduk. Yeni bir yazım türü eklemek istersek sadece fonksiyon tanımı yapıp bunu parametre olarak geçmemiz yeterli olacak. Eğer bu fonksiyonları bir Map içine koyarsak tekrar kullanım imkanını da bir hayli arttırmış oluruz.

Örnek Çıktılar
==== NameTestNew02 ===
===Custom List===
Name: Bob EMail: bob.baker@example.com
Name: Jane EMail: jane.doe@example.com
Name: John EMail: john.doe@example.com
Name: James EMail: james.johnson@example.com
Name: Joe EMail: joebob.bailey@example.com
Name: Phil EMail: phil.smith@examp;e.com
Name: Betty EMail: betty.jones@example.com

===Western List===

Name: Bob Baker
Age: 21  Gender: MALE
EMail: bob.baker@example.com
Phone: 201-121-4678
Address: 44 4th St, Smallville, KS 12333

Name: Jane Doe
Age: 25  Gender: FEMALE
EMail: jane.doe@example.com
Phone: 202-123-4678
Address: 33 3rd St, Smallville, KS 12333

===Eastern List===

Name: Baker Bob
Age: 21  Gender: MALE
EMail: bob.baker@example.com
Phone: 201-121-4678
Address: 44 4th St, Smallville, KS 12333

Name: Doe Jane
Age: 25  Gender: FEMALE
EMail: jane.doe@example.com
Phone: 202-123-4678
Address: 33 3rd St, Smallville, KS 12333

Lambda İfadeleri ve Yığınlarda Kullanımı


Önceki örnekte Function arayüzü inceleyip basit lambda ifadeleri ile nasıl çözüm üretildiğini incelemiştik. Bu bölümde ise yığın sınıflarını (Collection) lambda ifadeleri ile kullanarak nasıl daha da zengin bir kullanıma kavuştuğumuzu göreceğiz.

Şimdiye kadarki örneklerimizde  yığın kullanımı çok fazla olmadı ancak lambdalar sayesinde artık yığın kullanımı farklı bir boyut kazandı. Şimdi bunlara bir göz atalım.

Sınıfları Toplama

Daha önce yaptığımız ayrıştırmayı hatırlarsak, şimdi bu grupları SearchCriteria sınıfı içinde bir yığında toplayarak genelden soyutlayalım.
SearchCriteria.java

1 package com.example.lambda;
2
3 import java.util.HashMap;
4 import java.util.Map;
5 import java.util.function.Predicate;
6
7 /**
8 *
9 * @author MikeW
10 */
11 public class SearchCriteria {
12
13  private final Map<String, Predicate<Person>> searchMap = new HashMap<>();
14
15  private SearchCriteria() {
16    super();
17    initSearchMap();
18  }
19
20  private void initSearchMap() {
21    Predicate<Person> allDrivers = p -> p.getAge() >= 16;
22    Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
23    Predicate<Person> allPilots = p -> p.getAge() >= 23 && p.getAge() <= 65;
24
25    searchMap.put("allDrivers", allDrivers);
26    searchMap.put("allDraftees", allDraftees);
27    searchMap.put("allPilots", allPilots);
28
29  }
30
31  public Predicate<Person> getCriteria(String PredicateName) {
32    Predicate<Person> target;
33
34    target = searchMap.get(PredicateName);
35
36    if (target == null) {
37
38      System.out.println("Search Criteria not found... ");
39      System.exit(1);
40    
41    }
42      
43    return target;
44
45  }
46
47  public static SearchCriteria getInstance() {
48    return new SearchCriteria();
49  }
50 }


Döngüler

Şimdi yeni bir özellik olarak yığın sınıflarına eklenen forEach metodunu inceleyelim. Aşağıdaki kod parçasında  Person nesne listesi üzerindeki döngü ile yazım yapan bir kaç örneği bulabilirsiniz.
Test01ForEach.java
11 public class Test01ForEach {
12  
13  public static void main(String[] args) {
14    
15    List<Person> pl = Person.createShortList();
16    
17    System.out.println("\n=== Western Phone List ===");
18    pl.forEach( p -> p.printWesternName() );
19    
20    System.out.println("\n=== Eastern Phone List ===");
21    pl.forEach(Person::printEasternName);
22    
23    System.out.println("\n=== Custom Phone List ===");
24    pl.forEach(p -> { System.out.println(p.printCustom(r -> "Name: " + r.getGivenName() + " EMail: " + r.getEmail())); });
25    
26  }
27
28 }

İlk örnekte lambda ifadesi verilerek printWesternName metodu çağrılıyor ve her bir nesne için bu yazım işlemi yapılıyor. İkinci örnek ise metot referansı kullanarak işlem yapmaktadır. Bu da yeni bir kullanım türüdür. Eğer nesnede tanımlı bir metot var ise normal lambda ifadesi yerine bu tür bir kullanım da mümkündür. Son olarak ise printCustom metodu nasıl kullanılabilir onu görüyoruz. Burada biraz kafa karışıklığı olabilir zira lambda içinde lambda kulanımı var ve değişken isimlerinin iç içe kullanım olduğunda nasıl değiştiğine dikkat edilmeli.

Bu şekilde herhangi bir yığın içinde tekrarlı işlem yapmak mümkündür. Aslında burada yapılan işlem for döngüsüne çok benzemekle birlikte işlemin sınıfın içinde dışarıya kapalı olarak yapılması bir çok faydayı beraberinde getirmektedir.

Zincirleme Filtre Kullanımı
Dizinlerin içeriği üzerinde döngü ile işlem yapma imkanına ek olarak yığın metotlarını zincirleme olarak da kullanmak artık mümkün. İlk olarak Predicate arayüzünü değişken olarak alan filter metoduna bir bakalım.

Aşağıdaki örnekte, filtreleme adımı sonrası List nesnesi üzerinde döngü ile işlem yapılıyor.
Test02Filter.java
9 public class Test02Filter {
10  
11  public static void main(String[] args) {
12
13    List<Person> pl = Person.createShortList();
14    
15    SearchCriteria search = SearchCriteria.getInstance();
16    
17    System.out.println("\n=== Western Pilot Phone List ===");
18
19    pl.stream().filter(search.getCriteria("allPilots"))
20      .forEach(Person::printWesternName);
21    
22   
23    System.out.println("\n=== Eastern Draftee Phone List ===");
24
25    pl.stream().filter(search.getCriteria("allDraftees"))
26      .forEach(Person::printEasternName);
27    
28  }
29 }


İlk ve son döngüde arama kısıtına bağlı nasıl filtreleme yapıldığı görülüyor. Çıktı ise şu şekilde :


=== Eastern Draftee Phone List ===

Name: Baker Bob
Age: 21  Gender: MALE
EMail: bob.baker@example.com
Phone: 201-121-4678
Address: 44 4th St, Smallville, KS 12333

Name: Doe John
Age: 25  Gender: MALE
EMail: john.doe@example.com
Phone: 202-123-4678
Address: 33 3rd St, Smallville, KS 12333

Biraz Tembelleşelim
Gördüğümüz gibi bu özellikler gerçekten etkileyici. İyi ama eğer bizim nesneler içinde döngü yapma imkanımız varsa neden bir yığına doldurup orada çalışalım ki ? Eğer nesne içinde tarama imkânını kütüphanelere eklersek Java geliştiricilerinin daha verimli kod yazmasına imkân sunmuş oluruz. Bu ne demek diyorsanız, ilk olarak şu iki kavramı bir tanımlayalım.

  • Tembellik (Laziness) : Programlama dünyasında tembellik demek, sadece ihtiyacın olan nesne ile ve sadece ihtiyacın olduğu zamanda işlem yapmak demek. Örneğin bir önceki örnekteki son döngü bu kapsama giriyor çünkü ilk olarak filtreleme yapıldığından döngü sadece kalan iki Person nesnesi için çalışıyor.
  • Açgözlülük (Eagerness) : Bir yığın içindeki tüm kayıtlar için işlem yapılıyorsa bu kapsama giriyor. Eğer sadece iki nesne ile işlem yapacakken bütün liste üzerinde döngü çalışıyorsa buna açgözlü yaklaşım denir.

Eğer döngü özelliği kütüphaneye eklenirse, fırsat çıktığında tembellik özelliği kullanarak daha verimli kod yazmak mümkün hale gelir. Tabii ki ortalama alma veya toplama işlemi gibi bir işlem yapıyorsanız tüm liste üzerinde çalışmak bir zorunluluktur ve açgözlü işlem seçimi bu bağlamda olması gerekendir. Ama zorunlu haller dışında kesinlikle tembellik yöntemi tercih edilmelidir.

Stream Metodu
Bir önceki örneğe bakarsak, filtreleme işlemi ve döngü ile filtrelenen değerler üzerinde işlem yapmadan önce stream metodu çağrılıyor. Bu metot Collection nesnesini değişken olarak alıp geriye java.util.stream.Stream nesnesi dönüyor. Burada Stream nesnesi, üzerinde birden çok faklı zincirleme metot çağırma imkânı veren bir öğeler dizisi anlamına gelir. Aksi belirtilmediği taktirde eğer bir öğe diziden alınırsa harcanmış olur ve  tekrar kullanılamaz. Bu nedenle eğer zincirleme metot kullanımı söz konusu ise, her metot, her defasında bir önceki kullanımdan  kalan ve farklı öğe grubu üzerinde çalışacaktır. Ayrıca istenirse Stream kullanılan metoda bağlı olarak paralel ya da seri olarak da işlenebilir. En sonda paralel işlemeye ait bir örnek olacak.

Dönüşüm ve Sonuçları

Az öncede değindiğimiz gibi Stream harcanıp biten bir yapıya sahiptir. Bu nedenle de eğer Stream kullanıyorsak, bu yığındaki öğeleri değiştirme veya dönüştürme işlemine tabi tutamayız. Peki eğer böyle bir ihtiyacımız var ise, yani harcanmasın elimizde kalsın diyorsak ne yapacağız ? Buyrun bakalım, aşağıdaki örnek bu işi nasıl yapıyor.

Test03toList.java


10 public class Test03toList {
11  
12  public static void main(String[] args) {
13    
14    List<Person> pl = Person.createShortList();
15    
16    SearchCriteria search = SearchCriteria.getInstance();
17    
18    // Make a new list after filtering.
19    List<Person> pilotList = pl
20            .stream()
21            .filter(search.getCriteria("allPilots"))
22            .collect(Collectors.toList());
23    
24    System.out.println("\n=== Western Pilot Phone List ===");
25    pilotList.forEach(Person::printWesternName);
26
27  }
28
29 }

Kodu incelediğimizde collect metodunun Collectors tipinde bir nesne geçirilerek çağrıldığını görüyoruz. Burada kullanılan nesne Stream’da bulunan veriye göre List ya da Set dönebiliyor. Bizim örneğimiz için dönüş değeri döngü ile üzerinde  işlem yapılabilen bir List oluyor.

Map Kullanarak Hesaplama
Map metodu genellikle filter metodu ile birlikte kullanılır. Bu metot sınıfın öğelerinden birini alıp üzerinde bir işlem yapar. Aşağıda kişi yaşı üzerinde işlem yapan bir örnek kod var.

Test04Map.java

10 public class Test04Map {
11
12  public static void main(String[] args) {
13    List<Person> pl = Person.createShortList();
14    
15    SearchCriteria search = SearchCriteria.getInstance();
16    
17    // Calc average age of pilots old style
18    System.out.println("== Calc Old Style ==");
19    int sum = 0;
20    int count = 0;
21    
22    for (Person p:pl){
23      if (p.getAge() >= 23 && p.getAge() <= 65 ){
24        sum = sum + p.getAge();
25        count++;
26      }
27    }
28    
29    long average = sum / count;
30    System.out.println("Total Ages: " + sum);
31    System.out.println("Average Age: " + average);
32    
33    
34    // Get sum of ages
35    System.out.println("\n== Calc New Style ==");
36    long totalAge = pl
37            .stream()
38            .filter(search.getCriteria("allPilots"))
39            .mapToInt(p -> p.getAge())
40            .sum();
41
42    // Get average of ages
43    OptionalDouble averageAge = pl
44            .parallelStream()
45            .filter(search.getCriteria("allPilots"))
46            .mapToDouble(p -> p.getAge())
47            .average();
48
49    System.out.println("Total Ages: " + totalAge);
50    System.out.println("Average Age: " + averageAge.getAsDouble());    
51    
52  }
53  
54 }

Bu da çıktısı :

== Calc Old Style ==
Total Ages: 150
Average Age: 37

== Calc New Style ==
Total Ages: 150
Average Age: 37.5

Bu kod liste içinde buluna pilotların yaşlarının ortalamasını hesaplıyor. Birinci döngü eski usûl hesaplamayı for döngüsü yardımı ile örnekliyor. İkinci döngü ise seri stream üzerinden mapToInt metodu kullanarak her bir bireyin yaşını alıyor ve dönüş değeri IntStream tipinde bir nesne dönüyor ki bu nesnenin metotları arasında bulunan  geri dönüş değeri long olan sum metodu sayesinde de yaşları toplayabiliyoruz. Ortalamayı bulmak için yaşları toplamaya gerek yok ama sum metodunun kullanımına örnek olması açısından değinildi.

Son döngü ise pilotların yaşlarının ortalamasını hesaplıyor. Burada dikkat edilmesi gereken hesaplamanın eş zamanlı olarak paralel yapılabilmesi amacı ile parallelStream metodunun kullanılmasıdır.

Comments

Popular posts from this blog

Automation of daily build process with TlosLite

Java Sürümleri ve Özellikleri Kılavuzu

Java 14'de neler var - 1