Cześć! W zadaniu Java Syntax Pro badaliśmy wyrażenia lambda i stwierdziliśmy, że są one niczym więcej niż implementacją metody funkcjonalnej z funkcjonalnego interfejsu. Innymi słowy jest to implementacja jakiejś anonimowej (nieznanej) klasy, jej niezrealizowanej metody. A jeśli na wykładach kursu zagłębiliśmy się w manipulacje wyrażeniami lambda, teraz rozważymy, że tak powiem, drugą stronę: mianowicie te właśnie interfejsy. Ósma wersja Java wprowadziła koncepcję interfejsów funkcjonalnych . Co to jest? Za funkcjonalny uważa się interfejs z jedną niezaimplementowaną (abstrakcyjną) metodą. Definicja ta obejmuje wiele gotowych interfejsów, jak na przykład omówiony wcześniej interfejs Orzec
Konsument
Dostawca
Funkcjonować
Operator jednoargumentowy
Comparator
. A także interfejsy, które sami tworzymy, takie jak:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
}
Mamy interfejs, którego zadaniem jest konwersja obiektów jednego typu na obiekty innego typu (rodzaj adaptera). Adnotacja @FunctionalInterface
nie jest czymś bardzo skomplikowanym ani ważnym, ponieważ jej celem jest poinformowanie kompilatora, że ten interfejs jest funkcjonalny i powinien zawierać nie więcej niż jedną metodę. Jeśli interfejs z tą adnotacją ma więcej niż jedną niezaimplementowaną (abstrakcyjną) metodę, kompilator nie pominie tego interfejsu, ponieważ odbierze go jako błędny kod. Interfejsy bez tej adnotacji można uznać za funkcjonalne i będą działać, ale @FunctionalInterface
to nic innego jak dodatkowe ubezpieczenie. Wróćmy do zajęć Comparator
. Jeśli spojrzysz na jego kod (lub dokumentację ), zobaczysz, że ma on o wiele więcej niż jedną metodę. Następnie pytasz: jak w takim razie można to uznać za funkcjonalny interfejs? Interfejsy abstrakcyjne mogą zawierać metody, które nie mieszczą się w zakresie pojedynczej metody:
- statyczny
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
}
Po otrzymaniu tej metody kompilator nie narzekał, co oznacza, że nasz interfejs nadal działa.
- metody domyślne
default
:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий obiekt - " + t.toString());
}
}
Znów widzimy, że kompilator nie zaczął narzekać i nie wyszliśmy poza ograniczenia funkcjonalnego interfejsu.
- Metody klas obiektów
Object
. Nie dotyczy to interfejsów. Ale jeśli mamy w interfejsie metodę abstrakcyjną, która pasuje do sygnatury jakiejś metody klasy Object
, taka metoda (lub metody) nie przełamie naszych ograniczeń funkcjonalnych interfejsu:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий obiekt - " + t.toString());
}
boolean equals(Object obj);
}
I znowu nasz kompilator nie narzeka, więc interfejs Converter
nadal jest uważany za funkcjonalny. Teraz pytanie brzmi: dlaczego musimy ograniczać się do jednej niezaimplementowanej metody w funkcjonalnym interfejsie? A potem, abyśmy mogli to zaimplementować za pomocą lambd. Spójrzmy na to na przykładzie Converter
. W tym celu utwórzmy klasę Dog
:
public class Dog {
String name;
int age;
int weight;
public Dog(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
I podobny Raccoon
(szop):
public class Raccoon {
String name;
int age;
int weight;
public Raccoon(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
Załóżmy, że mamy obiekt Dog
i musimy go utworzyć na podstawie jego pól Raccoon
. Oznacza to, że Converter
konwertuje obiekt jednego typu na inny. Jak to będzie wyglądać:
public static void main(String[] args) {
Dog dog = new Dog("Bobbie", 5, 3);
Converter<Dog, Raccoon> converter = x -> new Raccoon(x.name, x.age, x.weight);
Raccoon raccoon = converter.convert(dog);
System.out.println("Raccoon has parameters: name - " + raccoon.name + ", age - " + raccoon.age + ", weight - " + raccoon.weight);
}
Kiedy go uruchomimy, otrzymamy następujące dane wyjściowe na konsolę:
Raccoon has parameters: name - Bobbbie, age - 5, weight - 3
A to oznacza, że nasza metoda zadziałała poprawnie.
Podstawowe interfejsy funkcjonalne Java 8
Cóż, teraz spójrzmy na kilka funkcjonalnych interfejsów, które przyniosła nam Java 8 i które są aktywnie używane w połączeniu z API Stream.Orzec
Predicate
— funkcjonalny interfejs umożliwiający sprawdzenie, czy spełniony jest określony warunek. Jeśli warunek jest spełniony, zwraca true
, w przeciwnym razie - false
:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
Jako przykład rozważ utworzenie elementu, Predicate
który sprawdzi parzystość szeregu typów Integer
:
public static void main(String[] args) {
Predicate<Integer> isEvenNumber = x -> x % 2==0;
System.out.println(isEvenNumber.test(4));
System.out.println(isEvenNumber.test(3));
}
Wyjście konsoli:
true
false
Konsument
Consumer
(z angielskiego - „konsument”) - funkcjonalny interfejs, który jako argument wejściowy przyjmuje obiekt typu T, wykonuje pewne akcje, ale nic nie zwraca:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
Jako przykład rozważmy , którego zadaniem jest wyświetlenie na konsoli powitania z przekazanym argumentem w postaci ciągu znaków: Consumer
public static void main(String[] args) {
Consumer<String> greetings = x -> System.out.println("Hello " + x + " !!!");
greetings.accept("Elena");
}
Wyjście konsoli:
Hello Elena !!!
Dostawca
Supplier
(z języka angielskiego - dostawca) - funkcjonalny interfejs, który nie pobiera żadnych argumentów, ale zwraca obiekt typu T:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Jako przykład rozważ Supplier
, który wygeneruje losowe nazwy z listy:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList .add("Elena");
nameList .add("John");
nameList .add("Alex");
nameList .add("Jim");
nameList .add("Sara");
Supplier<String> randomName = () -> {
int value = (int)(Math.random() * nameList.size());
return nameList.get(value);
};
System.out.println(randomName.get());
}
A jeśli to uruchomimy, zobaczymy losowe wyniki z listy nazw w konsoli.
Funkcjonować
Function
— ten interfejs funkcjonalny przyjmuje argument T i rzutuje go na obiekt typu R, który jest zwracany w wyniku:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
Jako przykład weźmy program , który konwertuje liczby z formatu łańcuchowego ( ) na format liczbowy ( ): Function
String
Integer
public static void main(String[] args) {
Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
System.out.println(valueConverter.apply("678"));
}
Kiedy go uruchomimy, otrzymamy następujące dane wyjściowe na konsolę:
678
PS: jeśli do ciągu znaków przekażemy nie tylko liczby, ale także inne znaki, zostanie zgłoszony wyjątek - NumberFormatException
.
Operator jednoargumentowy
UnaryOperator
— interfejs funkcjonalny, który jako parametr przyjmuje obiekt typu T, wykonuje na nim pewne operacje i zwraca wynik operacji w postaci obiektu tego samego typu T:
@FunctionalInterface
public interface UnaryOperator<T> {
T apply(T t);
}
UnaryOperator
, który wykorzystuje swoją metodę apply
do kwadratu liczby:
public static void main(String[] args) {
UnaryOperator<Integer> squareValue = x -> x * x;
System.out.println(squareValue.apply(9));
}
Wyjście konsoli:
81
Przyjrzeliśmy się pięciu funkcjonalnym interfejsom. To nie wszystko, co mamy do dyspozycji począwszy od Javy 8 – to główne interfejsy. Reszta dostępnych to ich skomplikowane odpowiedniki. Pełną listę można znaleźć w oficjalnej dokumentacji Oracle .
Funkcjonalne interfejsy w Stream
Jak omówiono powyżej, te interfejsy funkcjonalne są ściśle powiązane z API Stream. Jak, pytasz? I tak, że wiele metodStream
działa specjalnie z tymi interfejsami funkcjonalnymi. Przyjrzyjmy się, jak można wykorzystać interfejsy funkcjonalne w Stream
.
Metoda z predykatem
Weźmy na przykład metodę klasowąStream
, filter
która przyjmuje jako argument Predicate
i zwraca Stream
tylko te elementy, które spełniają warunek Predicate
. W kontekście Stream
-a oznacza to, że przechodzi tylko przez te elementy, które są zwracane, true
gdy jest użyte w metodzie test
interfejsu Predicate
. Tak wyglądałby nasz przykład Predicate
, gdyby nie filtr elementów w Stream
:
public static void main(String[] args) {
List<Integer> evenNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
.filter(x -> x % 2==0)
.collect(Collectors.toList());
}
W rezultacie lista evenNumbers
będzie składać się z elementów {2, 4, 6, 8}. I jak pamiętamy, collect
zbierze wszystkie elementy w pewną kolekcję: w naszym przypadku w List
.
Metoda z Konsumentem
Jedną z metod w programieStream
wykorzystującą interfejs funkcjonalny Consumer
jest metoda peek
. Tak będzie wyglądał nasz przykład Consumer
in Stream
:
public static void main(String[] args) {
List<String> peopleGreetings = Stream.of("Elena", "John", "Alex", "Jim", "Sara")
.peek(x -> System.out.println("Hello " + x + " !!!"))
.collect(Collectors.toList());
}
Wyjście konsoli:
Hello Elena !!!
Hello John !!!
Hello Alex !!!
Hello Jim !!!
Hello Sara !!!
Ponieważ jednak metoda peek
działa z Consumer
, modyfikacja ciągów in Stream
nie nastąpi, ale peek
powróci Stream
z oryginalnymi elementami: takimi samymi, jakie do nich przyszły. Dlatego lista peopleGreetings
będzie składać się z elementów „Elena”, „John”, „Alex”, „Jim”, „Sara”. Istnieje również powszechnie stosowana metoda foreach
, która jest podobna do metody peek
, z tą różnicą, że jest ostateczna - terminalna.
Metoda z dostawcą
Przykładem metodyStream
korzystającej z interfejsu funkcji Supplier
jest generate
, która generuje nieskończoną sekwencję na podstawie przekazanego jej interfejsu funkcji. Skorzystajmy z naszego przykładu, Supplier
aby wydrukować pięć losowych nazw na konsoli:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList.add("Elena");
nameList.add("John");
nameList.add("Alex");
nameList.add("Jim");
nameList.add("Sara");
Stream.generate(() -> {
int value = (int) (Math.random() * nameList.size());
return nameList.get(value);
}).limit(5).forEach(System.out::println);
}
A oto wynik, który otrzymujemy w konsoli:
John
Elena
Elena
Elena
Jim
Tutaj użyliśmy metody limit(5)
, aby ustawić limit metody generate
, w przeciwnym razie program będzie drukował losowe nazwy na konsoli w nieskończoność.
Metoda z funkcją
Typowym przykładem metody zStream
argumentem Function
jest metoda map
, która pobiera elementy jednego typu, robi coś z nimi i przekazuje je dalej, ale mogą to być już elementy innego typu. Jak mógłby wyglądać przykład z Function
in Stream
:
public static void main(String[] args) {
List<Integer> values = Stream.of("32", "43", "74", "54", "3")
.map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
W rezultacie otrzymujemy listę liczb, ale w formacie Integer
.
Metoda z UnaryOperatorem
Jako metodę wykorzystującąUnaryOperator
jako argument przyjmijmy metodę klasową Stream
- iterate
. Ta metoda jest podobna do metody generate
: generuje również nieskończoną sekwencję, ale ma dwa argumenty:
- pierwszy to element, od którego rozpoczyna się generowanie sekwencji;
- druga to
UnaryOperator
, która wskazuje zasadę generowania nowych elementów z pierwszego elementu.
UnaryOperator
, ale w metodzie iterate
:
public static void main(String[] args) {
Stream.iterate(9, x -> x * x)
.limit(4)
.forEach(System.out::println);
}
Kiedy go uruchomimy, otrzymamy następujące dane wyjściowe na konsolę:
9
81
6561
43046721
Oznacza to, że każdy z naszych elementów jest mnożony przez siebie i tak dalej dla pierwszych czterech liczb. To wszystko! Byłoby wspaniale, gdybyś po przeczytaniu tego artykułu był o krok bliżej zrozumienia i opanowania Stream API w Javie!
GO TO FULL VERSION