C, PHP, VB, .NET

Дневникът на Филип Петров


* Ламбда изрази

Публикувано на 06 март 2015 в раздел ПИК3 Java.

Ламбда изразите са функционалност, която е дълго чакана в Java. Появява се с версия Java 8 и е първата крачка към добавяне на... нека не казваме "елементи", а по-скоро "еквиваленти" на функционално програмиране в езика. При положение, че един от основните конкуренти на Java - .Net framework - ги поддържа още с версия 3.5 от 2007 г., може да се каже, че от Oracle (Sun) са доста закъснели. Разбира се това си има и причина - до известна степен ламбда изразите нарушават основната философия на абстрактния модел на интерфейсите в Java (за това няма да пишем сега, а в следваща статия по-подробно). Тук ще разгледаме общата философия на ламбда изразите и начина на работа с тях.

 Деф. Ще наричаме един интерфейс "функционален интерфейс" ако притежава само един единствен абстрактен метод.

Ние досега сме се сблъсквали с някои функционални интерфейси - java.lang.Runnable, java.awt.event.ActionListener и java.util.Comparator. Вие разбира се също може да декларирате свои интерфейси - единственото условие да са функционални е да имат само един метод:

@FunctionalInterface
public interface MyInterface{
  public void myInterfaceMethod();
}

Анотацията @FunctionalInterface помага на компилатора (всъщност го кара да провери дали наистина има само един единствен метод), но не е задължителна.

Нека сега разгледаме една тривиална задача. Даден е масив с адреси на уеб сайтове. Някои от сайтовете имат www. пред домейна, а други нямат. Трябва да сортирате масива по азбучен ред, но така, че ако пред домейна има www., то да се пропусне. Например ако имаме "www.abv.bg", "www.yahoo.com" и "google.com", сортирани трябва да излезат в следната последователност: "www.abv.bg", "google.com", "www.yahoo.com". Ще подходим по следния начин - ще дефинираме метод "trimWWW", който ще премахва "www." пред името на домейна (ако го има). Ще създадем анонимен клас работещ по интерфейс "Comparator", който ще реализира функцията "compare" и ще сортираме масива с него. За удобство ще си създадем и метод "print", който да отпечатва масива в конзолата:

import java.util.Arrays;
import java.util.Comparator;

public class MyProgram{
  public static void main(String[] args){
    String[] urls = {"www.abv.bg", "www.yahoo.com", "google.com"};
    print(urls);
    
    Comparator<String> cmp = new Comparator<String>(){ 
      public int compare(String s1, String s2) {
        return (trimWWW(s1)).compareTo(trimWWW(s2));
      }
    };
    
    Arrays.sort(urls, cmp);
    print(urls);
  }
  
  static String trimWWW(String s){
    if(s.startsWith("www.")) s = s.substring(4, s.length());
    return s;
  }
  
  static void print(String[] arr){
    for(String str: arr){
      System.out.print(str+" ");
    }
    System.out.println();
  }
}

Идеята е естествена и ясна за всички дотук - дефинираме функционалност в анонимния Comparator обект, след което извикваме този код многократно при сортирането на масива. И сега към ламбда изразите - те ни дават съкратен и компактен начин да извършим същото действие, но буквално на един ред. Следващият код е напълно еквивалентен на горния, но е променен начина на дефиниране на cmp - този път е написан с ламбда израз:

import java.util.Arrays;
import java.util.Comparator;

public class MyProgram{
  public static void main(String[] args){
    String[] urls = {"www.abv.bg", "www.yahoo.com", "google.com"};
    print(urls);
    
    Comparator<String> cmp = 
      (s1, s2) -> (trimWWW(s1)).compareTo(trimWWW(s2));
    
    Arrays.sort(urls, cmp);
    print(urls);
  }
  
  static String trimWWW(String s){
    if(s.startsWith("www.")) s = s.substring(4, s.length());
    return s;
  }
  
  static void print(String[] arr){
    for(String str: arr){
      System.out.print(str+" ");
    }
    System.out.println();
  }
}

Нека за удобство разгледаме само променения фрагмент от кода. Оказва се, че това:

Comparator<String> cmp = new Comparator<String>(){
   public int compare(String s1, String s2) {
      return (trimWWW(s1)).compareTo(trimWWW(s2));
   }
};

е еквивалентно като функционалност (а и като реален код) на това:

Comparator<String> cmp = 
  (s1, s2) -> (trimWWW(s1)).compareTo(trimWWW(s2));

Ламбда изразът се състои от два компонента - входни данни и връщан резултат. Входните данни са две променливи - s1 и s2 в случая. Виждате, че в ламбда израза те нямат указан тип - той се взима автоматично според дефинирания тип от функционалния интерфейс Comparator (в случая е String). При нужда може и да дефинирате тип на входните данни, но в по-честия случай това не е нужно. Връщаният резултат е резултата от израза, който е записан след оператора ->.

Не е задължително дясната част да е един единствен оператор. Може ламбда израза да е сложен - това се осъществява с отваряща и затваряща скоби и съответно добавяне ръчно на оператор return. Ако например в горния код премахнем метод trimWWW, ние отново може да създадем cmp с ламбда израз по следния начин:

Comparator<String> cmp = (s1, s2) -> {
      if(s1.startsWith("www.")) s1 = s1.substring(4, s1.length());
      if(s2.startsWith("www.")) s2 = s2.substring(4, s2.length());
      return s1.compareTo(s2);
};

Виждате, че изразът стана от няколко оператора. Единственото важно и задължително нещо е изходния резултат от него да съвпада с очакваното от метод "compare" на функционалния интерфейс "Comparator".

"Зад кулисите" реално няма особена промяна. Ламбда изразът генерира обект от анонимен клас, който имплементира Comparator<String> и дефинира неговия compare метод - в случая слагайки входни параметри лявата част и тяло дясната част на ламбда израза.

Нека разгледаме още един пример - да вземем фрагмент от кода при дефинирането на ActionListener от статията за въведение в Swing. Дефинирахме действие на бутона, което при натискането му сменя надписа (текста) му:

button1.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent event) {
      String text = (String)event.getActionCommand();
      if (text.equals("Test Button")) button1.setText("CLICK!");
      else button1.setText("Test Button");
   }
});

Това може да бъде преписано малко по-компактно чрез ламбда израз по следния начин:

button1.addActionListener(event -> {
   String text = (String)event.getActionCommand();
   if (text.equals("Test Button")) button1.setText("CLICK!");
   else button1.setText("Test Button");
});

Ето още един елементарен пример за ламбда израз приложен за функционалния интерфейс Runnable (както знаете той дефинира един абстрактен метод - run).

public class Example{
  static int[] arr = {1, 2, 3, 4, 5};
  public static void main(String[] args) {
    Runnable printer = new Runnable(){
      public void run(){
        for(int i:arr) System.out.print(i+" "); 
      }
    };
    Thread t1 = new Thread(printer);
    Thread t2 = new Thread(printer);
    t1.start();
    t2.start();
  }
}

може да се предефинира чрез ламбда израз като:

public class Example{
   static int[] arr = {1, 2, 3, 4, 5};
   public static void main(String[] args) {
      Runnable printer = () -> {
         for(int i:arr) System.out.print(i+" "); 
      };
      Thread t1 = new Thread(printer);
      Thread t2 = new Thread(printer);
      t1.start();
      t2.start();
   }
}

Виждате, че когато метода на функционалния интерефейс няма входни параметри (какъвто е случая с метод run()) ние просто оставяме празни скоби в лявата част на ламбда израза.

Нека накрая покажем и как можем сами да си създадем функционален интерфейс и да го използваме в ламбда изрази:

public class Example{
   public static void main(String [] args) {
     PrintableInterface<String> w = new PrintableInterface<String>() {
       public void print(String s) {
          System.out.println(s);
       }
     };
     w.print("Hello from abstract class");
 
     PrintableInterface<String> w2 = (s) -> System.out.println(s);
     w2.print("Hello from lambda expression");
   }
}
@FunctionalInterface
interface PrintableInterface<T> {
 public void print(T obj);
}

Интересното обаче не свършва дотук. Имаме възможност дори за т.нар. съкратени ламбда изрази. Когато имаме готов метод, който приема като входен параметър входния параметър на ламбда израза и дава изход търсения изход за ламбда израза, то може да се възползваме от един друг нов оператор в Java - ::. В последния пример ние имаме точно тази ситуация - ламбда изразът ни е (s) -> System.out.println(s), т.е. метод println приема входния параметър на ламбда израза (s) и връща като резултат точно това, което искаме ламбда израза да върне. Ето как можем да пренапишем ламбда израза в още по-съкратен вид:

public class Example{
   public static void main(String [] args) {
      PrintableInterface<String> w = System.out::println;
      w.print("Hello from lambda expression");
   }
}

@FunctionalInterface
interface PrintableInterface<T> {
   public void print(T obj);
}

Казано с други думи (s) -> System.out.println(s) е еквивалентно на System.out::println. Можем да използваме този съкратен запис навсякъде, където ламбда израза ни позволява. Когато има две променливи за входен параметър, ще бъде извършена операция по следната схема - Obj::instanceFunc ще бъде еквивалентно (Obj s1, Obj s2) -> s1.instanceFunc(s2). Друг вариант е извикването на статичен метод, т.е. Class::staticFunc. В този случай това ще е еквивалентно на (Obj s1, Obj s2) -> Class.staticFunc(s1, s2). Трети вариант е Class::instanceFunc - в този случай това ще е еквивалентно на Obj::instanceFunc. Ето един пример със сортиране на масив от текстови низове:

import java.util.Arrays;
import java.util.Comparator;

public class Example{
  public static void main(String [] args) {
    String[] strings = {"zzz", "Aaa", "abc", "Acb"};
    
    // Класически начин
    Arrays.sort(strings, new Comparator<String>(){
      public int compare(String s1, String s2){
        return s1.compareToIgnoreCase(s2); 
      }
    });

    // Дълъг ламбда израз
    Arrays.sort(strings, (s1, s2) -> s1.compareToIgnoreCase(s2));

    // Съкратен ламбда израз
    Arrays.sort(strings, String::compareToIgnoreCase);
  }
}

Виждаме, че съкратените ламбда изрази могат не само да съкратят писането на код, но и съществено да повишат неговата четимост! Така от гледна точка на програмиста може би би било по-удобно и по-лесно да си мислим за ламбда изразите не като за обекти, а като все едно са методи, които предаваме като параметър на други методи. Така има близка аналогия с предаването на указатели към функции, които знаете от C. Можете да си направите и аналогия с т.нар. "функционално програмиране", което е налично например в JavaScript. От потребителска гледна точка, можем да кажем, че ламбда изразите са нещо като "анонимни методи" - такива без декларация, т.е. без модификатори за достъп, връщани стойности и дори без име. Това е просто съкратен начин да дефинирате метод на мястото, където ще го използвате. Ето един класически пример (калкулатор), който демонстрира точно тази аналогия с предаване на анонимни методи:

public class CalcExample {
   @FunctionalInterface
   interface IntMath {
      int operation(int a, int b);
   }

   public static void main(String[] args) {
      IntMath add = (a, b) -> a + b;
      IntMath subtract = (a, b) -> a - b;
      IntMath multiply = (a, b) -> a * b;
      IntMath divide = (a, b) -> a / b;

      System.out.println("5 + 3 = " + operate(5, 3, add));
      System.out.println("5 - 3 = " + operate(5, 3, subtract));
      System.out.println("5 * 3 = " + operate(5, 3, multiply));
      System.out.println("5 / 3 = " + operate(5, 3, divide));
   }

   static int operate(int a, int b, IntMath op) {
      return op.operation(a, b);
   }
}

или дори още по-анонимно като "предаваме методите" (фактически обектите) без именована референция:

public class Example {
   @FunctionalInterface
   interface IntMath {
      int operation(int a, int b);
   }

   public static void main(String[] args) {
      System.out.println("5 + 3 = " + operate(5, 3, (a, b) -> a + b));
      System.out.println("5 - 3 = " + operate(5, 3, (a, b) -> a - b));
      System.out.println("5 * 3 = " + operate(5, 3, (a, b) -> a * b));
      System.out.println("5 / 3 = " + operate(5, 3, (a, b) -> a / b));
   }

   static int operate(int a, int b, IntMath op) {
      return op.operation(a, b);
   }
}

Все пак помнете, че това е само аналогия, но реално са много различни неща. Затова в началото казахме, че това са "еквиваленти", а не "елементи" на функционално програмиране. Все пак Java продължава да си работи само и единствено с обекти и в основата си остава стриктно език за обектно-ориентирано програмиране. Ламбда изразите в Java генерират обекти и въпреки аналогията - няма анонимни методи или предаване на метод като параметър на друг метод! Има наподобяване на тези операции - както виждате доста успешно.

В следваща статия ще разгледаме по-задълбочено някои особености в ламбда изразите и ще покажем още някои неща, които се случват "зад кулисите".

 



Един коментар


  1. Здравейте! Срещали сме се в ТУ по ПИК3 и БД. До ден днешен използвам сайта, когато искам да си припомня нещо, за което исках да Ви благодаря.

Добави коментар

Адресът на електронната поща няма да се публикува


*