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)
. Решта — вже справа техніки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ