1. Interfejsy

Aby zrozumieć, czym są funkcje lambda, musisz najpierw zrozumieć, czym są interfejsy. Dlatego przypominamy główne punkty.

Interfejs jest rodzajem klasy. Mocno okrojone, jeśli mogę tak powiedzieć. Interfejs w przeciwieństwie do klasy nie może mieć własnych zmiennych (poza statycznymi). Nie można również tworzyć obiektów typu Interfejs:

  • Nie można zadeklarować zmiennych klasy
  • Nie można tworzyć obiektów

Przykład:

interface Runnable
{
   void run();
}
Przykład standardowego interfejsu

Korzystanie z interfejsu

Dlaczego więc potrzebny jest interfejs? Interfejsy są używane tylko w połączeniu z dziedziczeniem. Ten sam interfejs może być dziedziczony przez różne klasy, albo mówią, że klasy implementują interfejs .

Jeśli klasa implementuje interfejs, musi wewnętrznie implementować metody, które zostały zadeklarowane, ale nie zostały zaimplementowane w interfejsie. Przykład:

interface Runnable
{
   void run();
}

class Timer implements Runnable
{
   void run()
   {
      System.out.println(LocalTime.now());
   }
}

class Calendar implements Runnable
{
   void run()
   {
      var date = LocalDate.now();
      System.out.println("Сегодня " + date.getDayOfWeek());
   }
}

Klasa Timerimplementuje (implementuje) interfejs Runnable, dlatego jest zobowiązana zadeklarować w sobie wszystkie metody, które znajdują się w interfejsie Runnable i zaimplementować je: napisać kod w ciele metody. To samo dotyczy klasy Calendar.

Ale teraz zmienne typu Runnablemogą przechowywać odwołania do obiektów klas, które implementują Runnable.

Przykład:

Kod Notatka
Timer timer = new Timer();
timer.run();

Runnable r1 = new Timer();
r1.run();

Runnable r2 = new Calendar();
r2.run();

run()Metoda klasowa zostanie wywołana Timer


Metoda klasowa zostanie wywołana run()Metoda klasowa Timer


zostanie wywołanarun()Calendar

Zawsze możesz przypisać odwołanie do obiektu do zmiennej dowolnego typu, o ile ten typ jest jedną z klas nadrzędnych obiektu. Dla klas Timeri Calendartego typu typów są dwa: Objecti Runnable.

Jeśli przypiszesz odwołanie do obiektu do zmiennej typu Object, możesz na niej wywoływać tylko metody zadeklarowane w pliku Object. A jeśli przypiszesz odwołanie do obiektu do zmiennej typu Runnable, możesz wywoływać na nim metody, które są w typie Runnable.

Przykład 2:

ArrayList<Runnable> list = new ArrayList<Runnable>();
list.add (new Timer());
list.add (new Calendar());

for (Runnable element: list)
    element.run();

Ten kod zadziała, ponieważ obiekty Timermają Calendar doskonałe metody działania. Dlatego nie ma problemu, aby do nich zadzwonić. Gdybyśmy po prostu dodali metodę run() do obu klas, nie bylibyśmy w stanie wywoływać ich w tak prosty sposób.

Interfejs Runnablejest właściwie używany tylko po to, aby mieć miejsce na umieszczenie metody run.



2. Sortowanie

Przejdźmy do czegoś bardziej praktycznego. Rozważmy na przykład sortowanie ciągów.

Aby posortować kolekcję łańcuchów alfabetycznie, w Javie istnieje świetna metoda −Collections.sort(коллекция);

Ta statyczna metoda sortuje przekazaną kolekcję, a w trakcie sortowania porównuje jej elementy parami: aby zrozumieć, czy należy zamienić elementy, czy nie.

Porównanie elementów podczas sortowania odbywa się za pomocą compareTometody (), którą posiadają wszystkie standardowe klasy: Integer, String, ...

Metoda CompareTo() klasy Integer porównuje wartości dwóch liczb, natomiast metoda CompareTo() klasy String sprawdza alfabetyczną kolejność napisów.

Tak więc zbiór liczb zostanie posortowany rosnąco, podczas gdy zbiór ciągów znaków zostanie posortowany alfabetycznie.

Sortowanie alternatywne

Ale co, jeśli chcemy posortować ciągi znaków nie alfabetycznie, ale według ich długości? I chcemy posortować liczby w kolejności malejącej. Jak być w takiej sytuacji?

Aby to zrobić, klasa Collectionsma inną metodę sort(), ale z dwoma parametrami:

Collections.sort(коллекция, компаратор);

Gdzie komparator  jest specjalnym obiektem, który wie, jak porównać obiekty w kolekcji podczas procesu sortowania . Komparator pochodzi od angielskiego słowa Comparator(comparator), a Comparator- od słowa Compare - porównywać.

Czym więc jest ten szczególny przedmiot?

InterfejsComparator

W rzeczywistości wszystko jest bardzo proste. Typ drugiego parametru metody sort()Comparator<T>

Gdzie T jest parametrem typu, takim samym jak typ elementów kolekcji i Comparator jest interfejsem, który ma jedną metodęint compare(T obj1, T obj2);

Innymi słowy, obiekt komparatora to dowolny obiekt klasy, który implementuje interfejs komparatora. Interfejs komparatora wygląda bardzo prosto:

public interface Comparator<typ T>
{
   public int compare(typ T obj1, typ T obj2);
}
Kod interfejsu komparatora

Metoda compare()porównuje dwa parametry, które są do niej przekazywane.

Jeśli metoda zwraca liczbę ujemną, to obj1 < obj2. Jeśli metoda zwraca liczbę dodatnią, to obj1 > obj2. Jeśli metoda zwraca 0, to obj1 == obj2.

Oto jak wyglądałby obiekt komparatora, który porównuje łańcuchy według ich długości:

public class StringLengthComparator implements Comparator<String>
{
   public int compare (String obj1, String obj2)
   {
      return obj1.length() - obj2.length();
   }
}
Kod klasowyStringLengthComparator

Aby porównać długości łańcuchów, po prostu odejmij jedną długość od drugiej.

Kompletny kod programu sortującego łańcuchy według długości wyglądałby tak:

public class Solution
{
   public static void main(String[] args)
   {
      ArrayList<String> list = new ArrayList<String>();
      Collections.addAll(list, "Привет", "Jak", "дела?");
      Collections.sort(list, new StringLengthComparator());
   }
}

class StringLengthComparator implements Comparator<String>
{
   public int compare (String obj1, String obj2)
   {
      return obj1.length() - obj2.length();
   }
}
Sortuj łańcuchy według długości


3. Cukier syntaktyczny

Jak myślisz, czy ten kod można napisać krócej? W rzeczywistości jest tylko jedna linia, która zawiera przydatne informacje - obj1.length() - obj2.length();.

Ale przecież kod nie może istnieć poza metodą, więc musieliśmy dodać metodę compare(), a dla metody musieliśmy dodać nową klasę - StringLengthComparator. I jeszcze trzeba określić typy zmiennych... Generalnie wszystko wydaje się być poprawne.

Istnieją jednak sposoby na skrócenie tego kodu. Mamy dla Ciebie trochę cukru składniowego. Są dwa wiadra!

Anonimowa klasa wewnętrzna

Możesz napisać kod komparatora bezpośrednio w metodzie main()i pozwolić kompilatorowi zrobić resztę. Przykład:

public class Solution
{
    public static void main(String[] args)
    {
        ArrayList<String> list = new ArrayList<String>();
        Collections.addAll(list, "Привет", "Jak", "дела?");

        Comparator<String> comparator = new Comparator<String>()
        {
            public int compare (String obj1, String obj2)
            {
                return obj1.length() - obj2.length();
            }
        };

        Collections.sort(list, comparator);
    }
}
Sortuj łańcuchy według długości

Możesz stworzyć obiekt, który dziedziczy po interfejsie Comparatorbez tworzenia samej klasy! Kompilator utworzy go automatycznie i nada mu tymczasową nazwę. Porównywać:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() - obj2.length();
    }
};
Anonimowa klasa wewnętrzna
Comparator<String> comparator = new StringLengthComparator();

class StringLengthComparator implements Comparator<String>
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() - obj2.length();
    }
}
KlasaStringLengthComparator

Te same bloki kodu są pokolorowane na ten sam kolor w dwóch różnych przypadkach. Różnice są naprawdę niewielkie.

Gdy kompilator napotka pierwszy blok kodu w kodzie, po prostu wygeneruje dla niego drugi blok kodu i nada klasie losową nazwę.


4. Wyrażenia lambda w Javie

Załóżmy, że zdecydujesz się użyć anonimowej klasy wewnętrznej w swoim kodzie. W takim przypadku będziesz mieć taki blok kodu:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() - obj2.length();
    }
};
Anonimowa klasa wewnętrzna

Tutaj deklaracja zmiennej i utworzenie anonimowej klasy - wszystko razem. Istnieje jednak sposób na krótsze napisanie tego kodu. Na przykład tak:

Comparator<String> comparator = (String obj1, String obj2) ->
{
    return obj1.length() - obj2.length();
};

Średnik jest potrzebny, ponieważ mamy tutaj nie tylko ukrytą deklarację klasy, ale także tworzenie zmiennej.

Taki wpis nazywany jest wyrażeniem lambda.

Jeśli kompilator napotka taki wpis w twoim kodzie, po prostu wygeneruje z niego pełną wersję kodu (z anonimową klasą wewnętrzną).

Zauważ, że pisząc wyrażenie lambda pominęliśmy nie tylko nazwę klasy , ale również nazwę metody .Comparator<String>int compare()

Kompilator nie będzie miał problemu ze zdefiniowaniem metody , ponieważ Wyrażenia lambda można pisać tylko dla interfejsów, które mają jedną metodę. Istnieje jednak sposób na obejście tej zasady, ale o tym dowiesz się, gdy zaczniesz aktywniej uczyć się OOP (mówimy o metodach domyślnych).

Przyjrzyjmy się jeszcze raz pełnej wersji kodu, po prostu zaznacz na szaro tę część, którą można pominąć podczas pisania wyrażenia lambda:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
   {
      return obj1.length() - obj2.length();
   }
};
Anonimowa klasa wewnętrzna

Wygląda na to, że nic ważnego nie zostało pominięte. Rzeczywiście, jeśli interfejs Comparatorma tylko jedną metodę compare(), kompilator może z powodzeniem odzyskać szary kod z pozostałego kodu.

Sortowanie

Nawiasem mówiąc, kod wywołania sortowania można teraz zapisać w następujący sposób:

Comparator<String> comparator = (String obj1, String obj2) ->
{
   return obj1.length() - obj2.length();
};
Collections.sort(list, comparator);

Lub nawet tak:

Collections.sort(list, (String obj1, String obj2) ->
   {
      return obj1.length() - obj2.length();
   }
);

Po prostu zastąpiliśmy zmienną comparatornatychmiast wartością, która została do niej przypisana comparator.

Wpisz wnioskowanie

Ale to nie wszystko. Kod w tych przykładach można zapisać jeszcze krócej. Po pierwsze, kompilator może sam określić, jakie mają zmienne obj1i obj2jaki jest ich typ String. Po drugie, nawiasy klamrowe i instrukcję return można również pominąć, jeśli w kodzie metody jest tylko jedno polecenie.

Skrócona wersja byłaby taka:

Comparator<String> comparator = (obj1, obj2) ->
   obj1.length() - obj2.length();

Collections.sort(list, comparator);

A jeśli comparator od razu podstawimy jej wartość zamiast zmiennej, otrzymamy następującą opcję:

Collections.sort(list, (obj1, obj2) ->  obj1.length() - obj2.length() );

A co z tobą: tylko jedna linia kodu, bez dodatkowych informacji - tylko zmienne i kod. Krótko mówiąc, nigdzie! Albo jest gdzieś?



5. Jak to działa

W rzeczywistości kod można napisać jeszcze krócej. Ale o tym później.

Wyrażenie lambda można napisać tam, gdzie używany jest typ interfejsu z pojedynczą metodą.

Na przykład w tym kodzie możesz napisać wyrażenie lambda, ponieważ podpis metody wygląda następująco:Collections.sort(list, (obj1, obj2) -> obj1.length() - obj2.length());sort()

sort(Collection<T> colls, Comparator<T> comp)

Kiedy przekazaliśmy kolekcję jako pierwszy parametr do metody sortowania ArrayList<String>, kompilator był w stanie wywnioskować typ drugiego parametru jako . I z tego wywnioskowałem, że ten interfejs ma jedną metodę . Reszta to kwestia technologii.Comparator<String>int compare(String obj1, String obj2)