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). Решта — вже справа техніки.