1. Интерфейсы

Чтобы понимать, что такое лямбда-функции, сначала нужно понимать, что такое интерфейсы. Поэтому напомним основные моменты.

Интерфейс — это разновидность класса. Сильно урезанная, если можно так сказать. У интерфейса, в отличие от класса, не может быть своих переменных (кроме статических). Также нельзя создавать объекты типа Интерфейс:

  • Нельзя объявлять переменные класса
  • Нельзя создавать объекты

Пример:

interface Runnable
{
   void run();
}
Пример стандартного интерфейса

Использование интерфейса

Так зачем же интерфейс нужен? Интерфейсы используются только совместно с наследованием. Один и тот же интерфейс могут наследовать разные классы, или еще говорят, что классы реализуют интерфейс.

Если класс реализует интерфейс, он обязан реализовать у себя внутри те методы, которые были объявлены, но не реализованы внутри интерфейса. Пример:

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());
   }
}

Класс Timer реализует (implements) интерфейс Runnable, поэтому обязан объявить внутри себя все методы, которые есть в интерфейсе Runnable и реализовать их: написать код в теле метода. То же касается и класса Calendar.

Зато теперь в переменные типа Runnable можно сохранять ссылки на объекты классов, которые реализуют интерфейс Runnable.

Пример:

Код Примечание
Timer timer = new Timer();
timer.run();

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

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

Будет вызван метод run() класса Timer


Будет вызван метод run() класса Timer


Будет вызван метод run() класса Calendar

Вы всегда можете присвоить ссылку на объект переменной любого типа, если этот тип — один из классов-родителей объекта. Для классов Timer и Calendar таких типов два: Object и Runnable.

Если вы присвоите ссылку на объект переменной типа Object, сможете вызывать у нее только методы, объявленные в классе Object. А если присвоите ссылку на объект переменной типа Runnable, сможете вызвать у нее методы, которые есть в типе Runnable.

Пример 2:

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

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

Такой код будет работать, ведь у объектов Timer и Calendar есть отличные рабочие методы run. Поэтому нет никаких проблем с тем, чтобы их вызвать. Если бы мы просто добавили метод run() в оба класса, не смогли бы вызвать их таким простым способом.

Интерфейс Runnable фактически используется только для того, чтобы было куда поместить метод run.



2. Сортировка

Давайте перейдем к чему-то более практичному. Например, рассмотрим сортировку строк.

Чтобы сортировать коллекцию строк в алфавитном порядке, в Java есть отличный метод — Collections.sort(коллекция);

Этот статический метод выполняет сортировку переданной коллекции, и в процессе сортировки попарно сравнивает ее элементы: чтобы понять, менять элементы местами или нет.

Сравнение элементов в процессе сортировки выполняется с помощью метода compareTo(), который есть у всех стандартных классов: Integer, String, ...

Метод compareTo() класса Integer сравнивает значения двух чисел, а метод compareTo() класса String смотрит на алфавитный порядок строк.

Таким образом, коллекция чисел будет отсортирована в порядке их возрастания, а коллекция строк — в алфавитном порядке.

Альтернативная сортировка

А если мы хотим сортировать строки не по алфавиту, а по их длине? И числа хотим сортировать в порядке убывания. Как быть в этой ситуации?

Для этого у класса Collections есть еще один метод sort(), но уже с двумя параметрами:

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

Где компаратор — это специальный объект, который знает, как сравнивать объекты в коллекции в процессе сортировки. Компаратор происходит от английского слова Comparator (сравнитель), а Comparator — от слова Compare — сравнивать.

Так что же это за специальный объект-то такой?

Интерфейс Comparator

На самом деле все очень просто. Тип второго параметра метода sort()Comparator<T>

Где T — это тип-параметр, такой же, как и тип элементов коллекции, а Comparator — это интерфейс, который имеет единственный метод int compare(T obj1, T obj2);

Другими словами, объект-компаратор — это любой объект класса, который реализует интерфейс Comparator. Выглядит интерфейс Comparator очень просто:

public interface Comparator<Tип>
{
   public int compare(Tип obj1, Tип obj2);
}
Код интерфейса Comparator

Метод compare() сравнивает два параметра, которые в него передают.

Если метод возвращает отрицательное число, то obj1 < obj2. Если метод возвращает положительное число, то obj1 > obj2. Если метод возвращает 0, то считается, что obj1 == obj2.

Вот как будет выглядеть объект компаратор, который сравнивает строки по их длине:

public class StringLengthComparator implements Comparator<String>
{
   public int compare (String obj1, String obj2)
   {
      return obj1.length() - obj2.length();
   }
}
Код класса StringLengthComparator

Для того, чтобы сравнить длины строк, достаточно просто вычесть одну длину из другой.

Полный код программы, которая сортирует строки по длине, будет выглядеть вот так:

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

class StringLengthComparator implements Comparator<String>
{
   public int compare (String obj1, String obj2)
   {
      return obj1.length() - obj2.length();
   }
}
Сортировка строк по длине


3. Синтаксический сахар

А как вы думаете, можно ли записать данный код короче? По сути, тут только одна строка, которая несет полезную информацию — obj1.length() - obj2.length();.

Но ведь код не может существовать вне метода, поэтому пришлось добавить метод compare(), а для метода пришлось добавить новый класс – StringLengthComparator. И еще типы переменных нужно указывать... В общем, вроде бы все правильно.

Однако, есть способы записать этот код короче. У нас для вас припасено немного синтаксического сахара. Ведра эдак два!

Анонимный внутренний класс

Вы можете записать код компаратора прямо внутри метода main(), а компилятор сам сделает все остальное. Пример:

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

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

        Collections.sort(list, comparator);
    }
}
Сортировка строк по длине

Вы можете создать объект наследник интерфейса Comparator, не создавая сам класс! Компилятор создаст его автоматически и даст ему какое-нибудь временное имя. Сравните:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() - obj2.length();
    }
};
Анонимный внутренний класс
Comparator<String> comparator = new StringLengthComparator();

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

Одинаковым цветом раскрашены одинаковые блоки кода в двух разных случаях. Отличия совсем небольшие на самом деле.

Когда компилятор встретит в коде первый блок кода, он просто сгенерирует для него второй блок кода и даст классу какое-нибудь случайное имя.


4. Лямбда-выражения в Java

Допустим, вы решили использовать в вашем коде анонимный внутренний класс. В этом случае у вас будет блок кода типа такого:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() - obj2.length();
    }
};
Анонимный внутренний класс

Тут и объявление переменной, и создание анонимного класса — все вместе. Однако есть способ записать этот код короче. Например, так:

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

Точка с запятой нужна, т.к. у вас тут не только скрытое объявление класса, но и создание переменной.

Такая запись называется лямбда-выражением.

Если компилятор встретит такую запись в вашем коде, он просто сгенерирует по ней полную версию кода (с анонимным внутренним классом).

Обратите внимание: при записи лямбда-выражения мы опустили не только имя класса Comparator<String>, но и имя метода int compare().

У компилятора не возникнет проблем с определением метода, т.к. лямбда-выражение можно писать только для интерфейсов, у которых метод один. Впрочем есть способ обойти это правило, но об этом вы узнаете, когда начнете изучать ООП активнее (мы говорим о default-методах).

Давайте еще раз посмотрим на полную версию кода, только раскрасим серым цветом ту ее часть, которую можно опустить при записи лямбда выражения:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
      return obj1.length() - obj2.length();
    }
};
Анонимный внутренний класс

Вроде бы ничего важного не упустили. Действительно, если у интерфейса Comparator есть только один метод compare(), по оставшемуся коду компилятор вполне может восстановить серый код.

Сортировка

Кстати, код вызова сортировки теперь можно записать так:

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

Или даже так:

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

Мы просто подставили вместо переменной comparator сразу то значение, которое присваивали переменной comparator.

Выведение типов

Но и это еще не все. Код в этих примерах можно записать еще короче. Во-первых, компилятор может сам определить, что у переменных obj1 и obj2 тип String. А во-вторых, фигурные скобки и оператор return тоже можно не писать, если у вас в коде метода всего одна команда.

Сокращенный вариант будет таким:

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

Collections.sort(list, comparator);

А если вместо переменной comparator сразу подставить ее значение, то получим такой вариант:

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

Ну и как вам: всего одна строка кода, никакой лишней информации — только переменные и код. Короче уже некуда! Или есть куда?



5. Как это работает

На самом деле, код можно записать еще короче. Но об этом чуть позже.

Лямбда-выражение можно записать там, где используется тип-интерфейс с одним-единственным методом.

Например, в этом коде Collections.sort(list, (obj1, obj2) -> obj1.length() - obj2.length()); можно записать лямбда-выражение, т.к. сигнатура метода sort() имеет вид:

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

Когда мы передали в метод sort в качестве первого параметра коллекцию ArrayList<String>, компилятор смог определить тип второго параметра как Comparator<String>. А из этого сделал вывод, что этот интерфейс имеет единственный метод int compare(String obj1, String obj2). Остальное уже дело техники.