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.
Пример:
| Код | Примечание |
|---|---|
|
Будет вызван метод 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);
}
Метод 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). Остальное уже дело техники.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ