* Ламбда изрази
Публикувано на 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 генерират обекти и въпреки аналогията - няма анонимни методи или предаване на метод като параметър на друг метод! Има наподобяване на тези операции - както виждате доста успешно.
В следваща статия ще разгледаме по-задълбочено някои особености в ламбда изразите и ще покажем още някои неща, които се случват "зад кулисите".
Здравейте! Срещали сме се в ТУ по ПИК3 и БД. До ден днешен използвам сайта, когато искам да си припомня нещо, за което исках да Ви благодаря.