JavaRush /Blog Java /Random-PL /Interfejsy funkcjonalne w Javie

Interfejsy funkcjonalne w Javie

Opublikowano w grupie Random-PL
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. Interfejsy funkcjonalne w Javie - 1Ó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 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 @FunctionalInterfacenie 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 @FunctionalInterfaceto 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
Koncepcja interfejsów zakłada, że ​​w danej jednostce kodu nie można zaimplementować żadnych metod. Jednak począwszy od wersji Java 8 możliwe stało się używanie metod statycznych i domyślnych w interfejsach. Metody statyczne są powiązane bezpośrednio z klasą i nie wymagają konkretnego obiektu tej klasy, aby wywołać taką metodę. Oznacza to, że metody te harmonijnie wpisują się w koncepcję interfejsów. Jako przykład dodajmy statyczną metodę sprawdzania obiektu pod kątem wartości null do poprzedniej klasy:
@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
Przed wersją Java 8, jeśli musieliśmy utworzyć metodę w interfejsie dziedziczonym przez inne klasy, mogliśmy stworzyć jedynie metodę abstrakcyjną, która była zaimplementowana w każdej konkretnej klasie. Ale co, jeśli ta metoda jest taka sama dla wszystkich klas? W tym przypadku najczęściej używano klas abstrakcyjnych . Ale począwszy od Java 8 istnieje możliwość użycia interfejsów z zaimplementowanymi metodami - metodami domyślnymi. Dziedzicząc interfejs, możesz zastąpić te metody lub pozostawić wszystko bez zmian (pozostaw logikę domyślną). Tworząc metodę domyślną musimy dodać słowo kluczowe - 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
W wykładzie Porównywanie obiektów mówiliśmy o tym, że wszystkie klasy dziedziczą po klasie 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 Converternadal 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 Dogi musimy go utworzyć na podstawie jego pól Raccoon. Oznacza to, że Converterkonwertuje 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.Interfejsy funkcjonalne w Javie - 2

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, Predicatektó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 ( ): FunctionStringInteger
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ę applydo 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? Interfejsy funkcjonalne w Javie - 3I tak, że wiele metod Streamdział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, filterktóra przyjmuje jako argument Predicatei zwraca Streamtylko 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, truegdy jest użyte w metodzie testinterfejsu 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 evenNumbersbędzie składać się z elementów {2, 4, 6, 8}. I jak pamiętamy, collectzbierze wszystkie elementy w pewną kolekcję: w naszym przypadku w List.

Metoda z Konsumentem

Jedną z metod w programie Streamwykorzystującą interfejs funkcjonalny Consumerjest metoda peek. Tak będzie wyglądał nasz przykład Consumerin 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 peekdziała z Consumer, modyfikacja ciągów in Streamnie nastąpi, ale peekpowróci Streamz oryginalnymi elementami: takimi samymi, jakie do nich przyszły. Dlatego lista peopleGreetingsbę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 metody Streamkorzystającej z interfejsu funkcji Supplierjest generate, która generuje nieskończoną sekwencję na podstawie przekazanego jej interfejsu funkcji. Skorzystajmy z naszego przykładu, Supplieraby 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 z Streamargumentem Functionjest 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 Functionin 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ą UnaryOperatorjako 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.
Tak będzie wyglądał nasz przykład 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. Interfejsy funkcjonalne w Javie - 4To wszystko! Byłoby wspaniale, gdybyś po przeczytaniu tego artykułu był o krok bliżej zrozumienia i opanowania Stream API w Javie!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION