Java Language
Wyrażenia lambda
Szukaj…
Wprowadzenie
Wyrażenia lambda zapewniają jasny i zwięzły sposób implementacji interfejsu jednoprocesowego za pomocą wyrażenia. Pozwalają zmniejszyć ilość kodu, który musisz stworzyć i utrzymać. Chociaż są podobne do klas anonimowych, same nie mają informacji o typie. Wnioskowanie typu musi się zdarzyć.
Odwołania do metod implementują interfejsy funkcjonalne przy użyciu istniejących metod zamiast wyrażeń. Należą również do rodziny lambda.
Składnia
- () -> {wyrażenie zwrotne; } // Zero-arity z treścią funkcji, aby zwrócić wartość.
- () -> wyrażenie // Stenografia dla powyższej deklaracji; dla wyrażeń nie ma średnika.
- () -> {function-body} // Efekt uboczny w wyrażeniu lambda do wykonywania operacji.
- parametrName -> expression // Jednorazowe wyrażenie lambda. W wyrażeniach lambda z jednym argumentem można usunąć nawias.
- (Wpisz nazwa parametru, wpisz drugą nazwę parametru, ...) -> wyrażenie // lambda oceniające wyrażenie z parametrami wymienionymi po lewej stronie
- (parametrName, secondParameterName, ...) -> wyrażenie // Stenografia, która usuwa typy parametrów dla nazw parametrów. Może być używany tylko w kontekstach, które można wywnioskować przez kompilator, w którym podany rozmiar listy parametrów odpowiada jednemu (i tylko jednemu) rozmiarowi oczekiwanego interfejsu funkcjonalnego.
Używanie wyrażeń lambda do sortowania kolekcji
Sortowanie list
Przed Javą 8 konieczne było zaimplementowanie interfejsu java.util.Comparator
z anonimową (lub nazwaną) klasą podczas sortowania listy 1 :
List<Person> people = ...
Collections.sort(
people,
new Comparator<Person>() {
public int compare(Person p1, Person p2){
return p1.getFirstName().compareTo(p2.getFirstName());
}
}
);
Począwszy od Java 8, anonimową klasę można zastąpić wyrażeniem lambda. Zauważ, że typy parametrów p1
i p2
można p2
, ponieważ kompilator automatycznie je wywnioskuje:
Collections.sort(
people,
(p1, p2) -> p1.getFirstName().compareTo(p2.getFirstName())
);
Przykład można uprościć za pomocą Comparator.comparing
i referencje metod wyrażone za pomocą symbolu ::
(podwójny dwukropek).
Collections.sort(
people,
Comparator.comparing(Person::getFirstName)
);
Import statyczny pozwala nam wyrazić to bardziej zwięźle, ale można dyskutować, czy poprawi to ogólną czytelność:
import static java.util.Collections.sort;
import static java.util.Comparator.comparing;
//...
sort(people, comparing(Person::getFirstName));
Tak zbudowane komparatory można również łączyć ze sobą. Na przykład po porównaniu osób według imienia, jeśli są osoby o tym samym imieniu, metoda thenComparing
z porównaniem według nazwiska:
sort(people, comparing(Person::getFirstName).thenComparing(Person::getLastName));
1 - Należy pamiętać, że kolekcja.sort (...) działa tylko w kolekcjach, które są podtypami List
. Interfejsy API Set
i Collection
nie sugerują żadnego uporządkowania elementów.
Sortowanie map
Możesz sortować wpisy HashMap
według wartości w podobny sposób. (Zauważ, że LinkedHashMap
musi być użyty jako cel. Klucze w zwykłej HashMap
są nieuporządkowane.)
Map<String, Integer> map = new HashMap(); // ... or any other Map class
// populate the map
map = map.entrySet()
.stream()
.sorted(Map.Entry.<String, Integer>comparingByValue())
.collect(Collectors.toMap(k -> k.getKey(), v -> v.getValue(),
(k, v) -> k, LinkedHashMap::new));
Wprowadzenie do lambd Java
Interfejsy funkcjonalne
Lambdas może działać tylko na interfejsie funkcjonalnym, który jest interfejsem z tylko jedną metodą abstrakcyjną. Interfejsy funkcjonalne mogą mieć dowolną liczbę default
lub static
metod. (Z tego powodu są one czasami nazywane interfejsami pojedynczej metody abstrakcyjnej lub interfejsami SAM).
interface Foo1 {
void bar();
}
interface Foo2 {
int bar(boolean baz);
}
interface Foo3 {
String bar(Object baz, int mink);
}
interface Foo4 {
default String bar() { // default so not counted
return "baz";
}
void quux();
}
Deklarując funkcjonalny interfejs, można dodać adnotację @FunctionalInterface
. Nie ma to żadnego efektu specjalnego, ale błąd kompilatora zostanie wygenerowany, jeśli adnotacja zostanie zastosowana do interfejsu, który nie działa, działając w ten sposób jako przypomnienie, że interfejs nie powinien zostać zmieniony.
@FunctionalInterface
interface Foo5 {
void bar();
}
@FunctionalInterface
interface BlankFoo1 extends Foo3 { // inherits abstract method from Foo3
}
@FunctionalInterface
interface Foo6 {
void bar();
boolean equals(Object obj); // overrides one of Object's method so not counted
}
I odwrotnie, nie jest to interfejs funkcjonalny, ponieważ ma więcej niż jedną metodę abstrakcyjną :
interface BadFoo {
void bar();
void quux(); // <-- Second method prevents lambda: which one should
// be considered as lambda?
}
To również nie jest funkcjonalny interfejs, ponieważ nie ma żadnych metod:
interface BlankFoo2 { }
Zwróć uwagę na następujące kwestie. Załóżmy, że masz
interface Parent { public int parentMethod(); }
i
interface Child extends Parent { public int ChildMethod(); }
Child
nie może być funkcjonalnym interfejsem, ponieważ ma dwie określone metody.
Java 8 zapewnia również szereg ogólnych interfejsów funkcjonalnych w szablonie w pakiecie java.util.function
. Na przykład wbudowany interfejs Predicate<T>
otacza pojedynczą metodę, która wprowadza wartość typu T
i zwraca wartość boolean
.
Wyrażenia lambda
Podstawowa struktura wyrażenia Lambda to:
fi
będzie wówczas przechowywać instancję singletona klasy, podobną do klasy anonimowej, która implementuje interfejs FunctionalInterface
i gdzie definicją jednej metody jest { System.out.println("Hello"); }
. Innymi słowy, powyższe jest w większości równoważne z:
FunctionalInterface fi = new FunctionalInterface() {
@Override
public void theOneMethod() {
System.out.println("Hello");
}
};
Lambda jest tylko „głównie odpowiednik” anonimowy klasy, ponieważ w lambda, znaczenie wyrażeń takich jak this
, super
lub toString()
odwoływać się do klasy, w którym odbywa się przydział, a nie nowo utworzony obiekt.
Nie możesz podać nazwy metody, gdy używasz lambda - ale nie musisz, ponieważ interfejs funkcjonalny musi mieć tylko jedną metodę abstrakcyjną, więc Java zastępuje tę.
W przypadkach, w których typ lambda nie jest pewien (np. Metody przeciążone), możesz dodać rzut do lambda, aby poinformować kompilator, jaki powinien być typ, na przykład:
Object fooHolder = (Foo1) () -> System.out.println("Hello");
System.out.println(fooHolder instanceof Foo1); // returns true
Jeśli pojedyncza metoda interfejsu funkcjonalnego przyjmuje parametry, lokalne formalne nazwy powinny pojawić się między nawiasami lambda. Nie ma potrzeby deklarowania typu parametru ani zwracania, ponieważ są one pobierane z interfejsu (chociaż deklarowanie typów parametrów nie jest błędem, jeśli chcesz). Zatem te dwa przykłady są równoważne:
Foo2 longFoo = new Foo2() {
@Override
public int bar(boolean baz) {
return baz ? 1 : 0;
}
};
Foo2 shortFoo = (x) -> { return x ? 1 : 0; };
Nawiasy wokół argumentu można pominąć, jeśli funkcja ma tylko jeden argument:
Foo2 np = x -> { return x ? 1 : 0; }; // okay
Foo3 np2 = x, y -> x.toString() + y // not okay
Implikowane zwroty
Jeśli kod umieszczony w lambda jest wyrażeniem Java, a nie instrukcją , jest traktowany jako metoda zwracająca wartość wyrażenia. Zatem dwa następujące są równoważne:
IntUnaryOperator addOneShort = (x) -> (x + 1);
IntUnaryOperator addOneLong = (x) -> { return (x + 1); };
Dostęp do zmiennych lokalnych (zamknięcia wartości)
Ponieważ lambdy są skrótami składniowymi dla anonimowych klas, przestrzegają tych samych zasad dostępu do zmiennych lokalnych w otaczającym zakresie; zmienne muszą być traktowane jako final
i nie mogą być modyfikowane w lambda.
IntUnaryOperator makeAdder(int amount) {
return (x) -> (x + amount); // Legal even though amount will go out of scope
// because amount is not modified
}
IntUnaryOperator makeAccumulator(int value) {
return (x) -> { value += x; return value; }; // Will not compile
}
Jeśli konieczne jest zawijanie zmieniającej się zmiennej w ten sposób, należy użyć zwykłego obiektu, który przechowuje kopię zmiennej. Przeczytaj więcej w Java Closures z wyrażeniami lambda.
Akceptuje Lambdas
Ponieważ lambda jest implementacją interfejsu, nie trzeba nic specjalnego robić, aby metoda zaakceptowała lambda: każda funkcja, która przyjmuje interfejs funkcjonalny, może również zaakceptować lambda.
public void passMeALambda(Foo1 f) {
f.bar();
}
passMeALambda(() -> System.out.println("Lambda called"));
Typ wyrażenia lambda
Samo wyrażenie lambda nie ma określonego typu. Chociaż prawdą jest, że typy i liczba parametrów, wraz z rodzajem wartości zwracanej, mogą przenosić pewne informacje o typie, informacje takie będą ograniczały tylko to, do których typów można przypisać. Lambda odbiera typ, gdy jest przypisany do funkcjonalnego typu interfejsu na jeden z następujących sposobów:
- Bezpośrednie przypisanie do typu funkcjonalnego, np.
myPredicate = s -> s.isEmpty()
- Przekazanie go jako parametru o typie funkcjonalnym, np.
stream.filter(s -> s.isEmpty())
- Zwracanie go z funkcji zwracającej typ funkcjonalny, np.
return s -> s.isEmpty()
- Rzutowanie na typ funkcjonalny, np.
(Predicate<String>) s -> s.isEmpty()
Dopóki takie przypisanie do typu funkcjonalnego nie zostanie wykonane, lambda nie ma określonego typu. Aby to zilustrować, rozważ wyrażenie lambda o -> o.isEmpty()
. To samo wyrażenie lambda można przypisać do wielu różnych typów funkcjonalnych:
Predicate<String> javaStringPred = o -> o.isEmpty();
Function<String, Boolean> javaFunc = o -> o.isEmpty();
Predicate<List> javaListPred = o -> o.isEmpty();
Consumer<String> javaStringConsumer = o -> o.isEmpty(); // return value is ignored!
com.google.common.base.Predicate<String> guavaPredicate = o -> o.isEmpty();
Teraz, gdy są już przypisane, pokazane przykłady są zupełnie różnych typów, mimo że wyrażenia lambda wyglądały tak samo i nie można ich przypisać do siebie.
Referencje metod
Odwołania do metod umożliwiają przekazywanie predefiniowanych metod statycznych lub instancji zgodnych z kompatybilnym interfejsem funkcjonalnym jako argumentów zamiast anonimowego wyrażenia lambda.
Załóżmy, że mamy model:
class Person {
private final String name;
private final String surname;
public Person(String name, String surname){
this.name = name;
this.surname = surname;
}
public String getName(){ return name; }
public String getSurname(){ return surname; }
}
List<Person> people = getSomePeople();
Odwołanie do metody instancji (do dowolnej instancji)
people.stream().map(Person::getName)
Odpowiednik lambda:
people.stream().map(person -> person.getName())
W tym przykładzie przekazywane jest odwołanie do metody instancji getName()
typu Person
. Ponieważ wiadomo, że jest to typ kolekcji, zostanie wywołana metoda instancji (znana później).
Odwołanie do metody instancji (do konkretnej instancji)
people.forEach(System.out::println);
Ponieważ System.out
jest instancją PrintStream
, odwołanie do metody do tej konkretnej instancji jest przekazywane jako argument.
Odpowiednik lambda:
people.forEach(person -> System.out.println(person));
Odniesienie do metody statycznej
Również do przekształcania strumieni możemy zastosować odniesienia do metod statycznych:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream().map(String::valueOf)
W tym przykładzie przekazano odwołanie do metody static valueOf()
typu String
. Dlatego obiekt instancji w kolekcji jest przekazywany jako argument wartości valueOf()
.
Odpowiednik lambda:
numbers.stream().map(num -> String.valueOf(num))
Odniesienie do konstruktora
List<String> strings = Arrays.asList("1", "2", "3");
strings.stream().map(Integer::new)
Przeczytaj Zbieranie elementów strumienia do kolekcji, aby zobaczyć, jak zbierać elementy do kolekcji.
Używa się tutaj konstruktora pojedynczego ciągu typu Integer
, aby skonstruować liczbę całkowitą, biorąc pod uwagę ciąg podany jako argument. W takim przypadku, dopóki ciąg reprezentuje liczbę, strumień będzie mapowany na liczby całkowite. Odpowiednik lambda:
strings.stream().map(s -> new Integer(s));
Ściągawka
Format referencyjny metody | Kod | Odpowiednik Lambda |
---|---|---|
Metoda statyczna | TypeName::method | (args) -> TypeName.method(args) |
Metoda niestatyczna (na przykład * ) | instance::method | (args) -> instance.method(args) |
Metoda niestatyczna (bez wystąpienia) | TypeName::method | (instance, args) -> instance.method(args) |
Konstruktor ** | TypeName::new | (args) -> new TypeName(args) |
Konstruktor tablic | TypeName[]::new | (int size) -> new TypeName[size] |
* instance
może być dowolnym wyrażeniem, które getInstance()::method
odwołanie do instancji, np. getInstance()::method
, this::method
** Jeśli TypeName
jest niestatyczną klasą wewnętrzną, odwołanie do konstruktora jest prawidłowe tylko w zakresie wystąpienia klasy zewnętrznej
Implementowanie wielu interfejsów
Czasami możesz chcieć mieć wyrażenie lambda implementujące więcej niż jeden interfejs. Jest to szczególnie przydatne w przypadku interfejsów znaczników (takich jak java.io.Serializable ), ponieważ nie dodają one metod abstrakcyjnych.
Na przykład chcesz utworzyć zestaw TreeSet
za pomocą niestandardowego Comparator
a następnie zserializować go i wysłać przez sieć. Trywialne podejście:
TreeSet<Long> ts = new TreeSet<>((x, y) -> Long.compare(y, x));
nie działa, ponieważ lambda dla komparatora nie implementuje Serializable
. Możesz to naprawić, używając typów skrzyżowań i wyraźnie określając, że lambda musi być możliwa do serializacji:
TreeSet<Long> ts = new TreeSet<>(
(Comparator<Long> & Serializable) (x, y) -> Long.compare(y, x));
Jeśli często używasz typów skrzyżowań (na przykład jeśli używasz frameworka, takiego jak Apache Spark, w którym prawie wszystko musi być możliwe do serializacji), możesz utworzyć puste interfejsy i użyć ich w kodzie:
public interface SerializableComparator extends Comparator<Long>, Serializable {}
public class CustomTreeSet {
public CustomTreeSet(SerializableComparator comparator) {}
}
W ten sposób masz gwarancję, że przekazany komparator będzie można serializować.
Lambda i wzór wykonania
Istnieje kilka dobrych przykładów użycia lambdas jako interfejsu funkcjonalnego w prostych scenariuszach. Dość powszechnym przypadkiem użycia, który można ulepszyć za pomocą lambdas, jest tak zwany wzorzec wykonywania. W tym wzorcu masz zestaw standardowego kodu konfiguracji / porzucenia, który jest potrzebny w wielu scenariuszach otaczających kod specyficzny dla przypadku użycia. Kilka typowych tego przykładów to bloki pliku io, bazy danych, try / catch.
interface DataProcessor {
void process( Connection connection ) throws SQLException;;
}
public void doProcessing( DataProcessor processor ) throws SQLException{
try (Connection connection = DBUtil.getDatabaseConnection();) {
processor.process(connection);
connection.commit();
}
}
Następnie wywołanie tej metody za pomocą lambda może wyglądać następująco:
public static void updateMyDAO(MyVO vo) throws DatabaseException {
doProcessing((Connection conn) -> MyDAO.update(conn, ObjectMapper.map(vo)));
}
Nie ogranicza się to do operacji we / wy. Może mieć zastosowanie do każdego scenariusza, w którym podobne zadania konfiguracji / rozbiórki mają zastosowanie z niewielkimi zmianami. Główną zaletą tego Wzorca jest ponowne użycie kodu i wymuszanie OSUSZANIA (Don't Repeat Yourself).
Używanie wyrażenia lambda z własnym interfejsem funkcjonalnym
Lambdy mają zapewniać wbudowany kod implementacyjny dla interfejsów pojedynczej metody i możliwość przekazywania ich tak, jak robiliśmy to przy normalnych zmiennych. Nazywamy je interfejsem funkcjonalnym.
Na przykład napisanie Runnable w anonimowej klasie i rozpoczęcie wątku wygląda następująco:
//Old way
new Thread(
new Runnable(){
public void run(){
System.out.println("run logic...");
}
}
).start();
//lambdas, from Java 8
new Thread(
()-> System.out.println("run logic...")
).start();
Teraz, zgodnie z powyższym, załóżmy, że masz jakiś niestandardowy interfejs:
interface TwoArgInterface {
int operate(int a, int b);
}
Jak używasz lambda do implementacji tego interfejsu w kodzie? Taki sam jak pokazany powyżej przykład Runnable. Zobacz program sterownika poniżej:
public class CustomLambda {
public static void main(String[] args) {
TwoArgInterface plusOperation = (a, b) -> a + b;
TwoArgInterface divideOperation = (a,b)->{
if (b==0) throw new IllegalArgumentException("Divisor can not be 0");
return a/b;
};
System.out.println("Plus operation of 3 and 5 is: " + plusOperation.operate(3, 5));
System.out.println("Divide operation 50 by 25 is: " + divideOperation.operate(50, 25));
}
}
„return” zwraca tylko lambda, a nie zewnętrzną metodę
Metoda return
zwraca tylko z lambda, a nie z metody zewnętrznej.
Uważaj, że różni się to od Scali i Kotlin!
void threeTimes(IntConsumer r) {
for (int i = 0; i < 3; i++) {
r.accept(i);
}
}
void demo() {
threeTimes(i -> {
System.out.println(i);
return; // Return from lambda to threeTimes only!
});
}
Może to prowadzić do nieoczekiwanego zachowania podczas próby napisać własne konstrukcje języka, jak na polecenie wbudowane konstrukcje takie jak for
pętle return
zachowuje się inaczej:
void demo2() {
for (int i = 0; i < 3; i++) {
System.out.println(i);
return; // Return from 'demo2' entirely
}
}
W Scali i Kotlin zarówno demo
i demo2
wydrukowałyby tylko 0
. Ale to nie jest bardziej spójne. Podejście Java jest spójne z refaktoryzacją i użyciem klas - return
w kodzie u góry, a poniższy kod zachowuje się tak samo:
void demo3() {
threeTimes(new MyIntConsumer());
}
class MyIntConsumer implements IntConsumer {
public void accept(int i) {
System.out.println(i);
return;
}
}
Dlatego Java return
jest bardziej spójna z metod klasy i refaktoringu, ale mniej z for
i while
builtins, pozostają one wyjątkowe.
Z tego powodu następujące dwa są równoważne w Javie:
IntStream.range(1, 4)
.map(x -> x * x)
.forEach(System.out::println);
IntStream.range(1, 4)
.map(x -> { return x * x; })
.forEach(System.out::println);
Ponadto użycie try-with-resources jest bezpieczne w Javie:
class Resource implements AutoCloseable {
public void close() { System.out.println("close()"); }
}
void executeAround(Consumer<Resource> f) {
try (Resource r = new Resource()) {
System.out.print("before ");
f.accept(r);
System.out.print("after ");
}
}
void demo4() {
executeAround(r -> {
System.out.print("accept() ");
return; // Does not return from demo4, but frees the resource.
});
}
wydrukuje before accept() after close()
. W semantyce Scali i Kotlina try-with-resources nie zostałby zamknięty, ale zostałby wydrukowany before accept()
.
Zamknięcia Java z wyrażeniami lambda.
Zamknięcie lambda jest tworzone, gdy wyrażenie lambda odwołuje się do zmiennych obejmującego zakres (globalny lub lokalny). Zasady wykonywania tego są takie same jak w przypadku metod wbudowanych i klas anonimowych.
Zmienne lokalne z zakresu obejmującego, które są używane w lambda, muszą być final
. W Javie 8 (najwcześniejszej wersji, która obsługuje lambdas), nie muszą one być deklarowane jako final
w kontekście zewnętrznym, ale muszą być traktowane w ten sposób. Na przykład:
int n = 0; // With Java 8 there is no need to explicit final
Runnable r = () -> { // Using lambda
int i = n;
// do something
};
Jest to legalne, dopóki wartość zmiennej n
nie zostanie zmieniona. Jeśli spróbujesz zmienić zmienną, wewnątrz lub na zewnątrz lambda, pojawi się następujący błąd kompilacji:
„zmienne lokalne, do których odwołuje się wyrażenie lambda, muszą być ostateczne lub faktycznie ostateczne ”.
Na przykład:
int n = 0;
Runnable r = () -> { // Using lambda
int i = n;
// do something
};
n++; // Will generate an error.
Jeśli konieczne jest użycie zmieniającej się zmiennej w lambdzie, normalnym podejściem jest zadeklarowanie final
kopii zmiennej i użycie tej kopii. Na przykład
int n = 0;
final int k = n; // With Java 8 there is no need to explicit final
Runnable r = () -> { // Using lambda
int i = k;
// do something
};
n++; // Now will not generate an error
r.run(); // Will run with i = 0 because k was 0 when the lambda was created
Oczywiście ciało lambda nie widzi zmian oryginalnej zmiennej.
Pamiętaj, że Java nie obsługuje prawdziwych zamknięć. Lambda Java nie może zostać utworzona w sposób, który pozwala jej zobaczyć zmiany w środowisku, w którym została utworzona. Jeśli chcesz wdrożyć zamknięcie, które obserwuje lub wprowadza zmiany w swoim otoczeniu, powinieneś symulować je przy użyciu zwykłej klasy. Na przykład:
// Does not compile ...
public IntUnaryOperator createAccumulator() {
int value = 0;
IntUnaryOperator accumulate = (x) -> { value += x; return value; };
return accumulate;
}
Powyższy przykład nie zostanie skompilowany z powodów omówionych wcześniej. Możemy obejść błąd kompilacji w następujący sposób:
// Compiles, but is incorrect ...
public class AccumulatorGenerator {
private int value = 0;
public IntUnaryOperator createAccumulator() {
IntUnaryOperator accumulate = (x) -> { value += x; return value; };
return accumulate;
}
}
Problem polega na tym, że powoduje to IntUnaryOperator
kontraktu projektowego dla interfejsu IntUnaryOperator
, który stwierdza, że instancje powinny być funkcjonalne i bezstanowe. Jeśli takie zamknięcie zostanie przekazane wbudowanym funkcjom, które akceptują obiekty funkcjonalne, może to spowodować awarie lub błędne zachowanie. Zamknięcia, które zawierają stan zmienny, powinny być implementowane jako zwykłe klasy. Na przykład.
// Correct ...
public class Accumulator {
private int value = 0;
public int accumulate(int x) {
value += x;
return value;
}
}
Lambda - przykład słuchacza
Anonimowy detektor klas
W wersjach wcześniejszych niż Java 8 bardzo często anonimowa klasa jest używana do obsługi zdarzenia kliknięcia JButton, jak pokazano w poniższym kodzie. Ten przykład pokazuje, jak zaimplementować anonimowy detektor w zakresie btn.addActionListener
.
JButton btn = new JButton("My Button");
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button was pressed");
}
});
Słuchacz Lambda
Ponieważ interfejs ActionListener
definiuje tylko jedną metodę actionPerformed()
, jest to interfejs funkcjonalny, co oznacza, że istnieje miejsce do użycia wyrażeń Lambda w celu zastąpienia kodu actionPerformed()
. Powyższy przykład można przepisać przy użyciu wyrażeń Lambda w następujący sposób:
JButton btn = new JButton("My Button");
btn.addActionListener(e -> {
System.out.println("Button was pressed");
});
Styl tradycyjny do stylu Lambda
Tradycyjny sposób
interface MathOperation{
boolean unaryOperation(int num);
}
public class LambdaTry {
public static void main(String[] args) {
MathOperation isEven = new MathOperation() {
@Override
public boolean unaryOperation(int num) {
return num%2 == 0;
}
};
System.out.println(isEven.unaryOperation(25));
System.out.println(isEven.unaryOperation(20));
}
}
Styl Lambda
- Usuń nazwę klasy i funkcjonalny interfejs.
public class LambdaTry {
public static void main(String[] args) {
MathOperation isEven = (int num) -> {
return num%2 == 0;
};
System.out.println(isEven.unaryOperation(25));
System.out.println(isEven.unaryOperation(20));
}
}
- Opcjonalna deklaracja typu
MathOperation isEven = (num) -> {
return num%2 == 0;
};
- Opcjonalny nawias wokół parametru, jeśli jest to pojedynczy parametr
MathOperation isEven = num -> {
return num%2 == 0;
};
- Opcjonalne nawiasy klamrowe, jeśli w treści funkcji jest tylko jedna linia
- Opcjonalne słowo kluczowe powrotu, jeśli w treści funkcji jest tylko jeden wiersz
MathOperation isEven = num -> num%2 == 0;
Lambda i wykorzystanie pamięci
Ponieważ lambda Java są zamknięciami, mogą „uchwycić” wartości zmiennych w otaczającym zakresie leksykalnym. Chociaż nie wszystkie lambdy przechwytują cokolwiek - proste lambdy takie jak s -> s.length()
nie przechwytują niczego i są nazywane bezstanowymi - przechwytywanie lambd wymaga tymczasowego obiektu do przechowywania przechwyconych zmiennych. W tym fragmencie kodu lambda () -> j
jest lambda przechwytującą i może powodować przydzielenie obiektu podczas jego oceny:
public static void main(String[] args) throws Exception {
for (int i = 0; i < 1000000000; i++) {
int j = i;
doSomethingWithLambda(() -> j);
}
}
Chociaż może nie być to od razu oczywiste, ponieważ new
słowo kluczowe nie pojawia się nigdzie we fragmencie, kod ten może utworzyć 1 000 000 000 oddzielnych obiektów reprezentujących instancje wyrażenia () -> j
lambda. Należy jednak zauważyć, że przyszłe wersje Java 1 mogą być w stanie zoptymalizować to, aby w czasie wykonywania instancje lambda były ponownie wykorzystywane lub były reprezentowane w inny sposób.
1 - Na przykład Java 9 wprowadza opcjonalną fazę „linku” do sekwencji kompilacji Java, która zapewni możliwość globalnej optymalizacji takiej jak ta.
Używanie wyrażeń i predykatów lambda w celu uzyskania określonych wartości z listy
Począwszy od Java 8, możesz używać wyrażeń i predykatów lambda.
Przykład: użyj wyrażeń lambda i predykatu, aby uzyskać określoną wartość z listy. W tym przykładzie każda osoba zostanie wydrukowana z faktem, czy ma 18 lat i więcej, czy nie.
Klasa osoby:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() { return age; }
public String getName() { return name; }
}
Wbudowany interfejs Predicate z pakietów java.util.function.Predicate to funkcjonalny interfejs z metodą boolean test(T t)
.
Przykładowe użycie:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class LambdaExample {
public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Jeroen", 20));
personList.add(new Person("Jack", 5));
personList.add(new Person("Lisa", 19));
print(personList, p -> p.getAge() >= 18);
}
private static void print(List<Person> personList, Predicate<Person> checker) {
for (Person person : personList) {
if (checker.test(person)) {
System.out.print(person + " matches your expression.");
} else {
System.out.println(person + " doesn't match your expression.");
}
}
}
}
print(personList, p -> p.getAge() >= 18);
Metoda przyjmuje wyrażenie lambda (ponieważ predykat jest używany jako parametr), w którym można zdefiniować potrzebne wyrażenie. Metoda testowa sprawdzania sprawdza, czy to wyrażenie jest poprawne, czy nie: checker.test(person)
.
Możesz łatwo zmienić to na coś innego, na przykład print(personList, p -> p.getName().startsWith("J"));
. Spowoduje to sprawdzenie, czy imię osoby zaczyna się od litery „J”.