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