1. Нововведення в Java 8: функціональне програмування

У версії Java 8 з'явилася потужна підтримка функціонального програмування. Правду кажучи, довгоочікувана підтримка функціонального програмування. Тепер код можна писати швидше, натомість читати — складніше 🙂

Перед вивченням функціонального програмування в Java рекомендуємо як слід розібратися в трьох речах:

  1. ООП, успадкування та інтерфейси.
  2. Дефолтна реалізація методів в інтерфейсі.
  3. Внутрішні й анонімні класи.

Добра новина — навіть не знаючи всього цього, можна користуватися багатьма можливостями функціонального програмування в Java. Погана новина — зрозуміти, як саме все влаштовано і як все працює, без тих самих внутрішніх анонімних класів досить складно.

У наступних лекціях ми зосередимося на тому, як легко й просто користуватися можливостями функціонального програмування в Java без глибокого розуміння його принципів.

Щоб опанувати всі особливості функціонального програмування в Java, знадобиться не один місяць. А от читати такий код можна навчитися за кілька годин. Тому пропонуємо почати з малого. Скажімо, з тих-таки потоків вводу-виведення.


2. Потоки вводу-виведення: ланцюжки потоків

Пам'ятаєте, колись ви вивчали потоки вводу-виведення: InputStream, OutputStream, Reader, Writer тощо?

Були класи-потоки (як-от FileInputSteam), які читали дані з джерел даних, а були й проміжні потоки даних (як-от InputStreamReader й BufferedReader), які читали дані з інших потоків.

Ці потоки можна було поєднувати в ланцюжки обробки даних. Наприклад, отак:

FileInputStream input = new FileInputStream("c:\\readme.txt");
InputStreamReader reader = new InputStreamReader(input);
BufferedReader buff = new BufferedReader(reader);

String text = buff.readLine();

Важливо зазначити, що в перших кількох рядках коду ми просто складаємо ланцюжок зі Stream-об'єктів, але реальні дані цим ланцюжком потоків ще не передаються.

І тільки коли ми викличемо метод buff.readLine(), відбудеться таке:

  1. Об'єкт BufferedReader викличе метод read() для об'єкта InputStreamReader
  2. Об'єкт InputStreamReader викличе метод read() для об'єкта FileInputStream
  3. Об'єкт FileInputStream почне читати дані з файлу

Тобто ніякого руху даних у ланцюжку потоків не буде, доки ми не почнемо викликати методи типу read() або readLine(). Саме лише конструювання ланцюжка потоків дані цим ланцюжком не передає. Потоки не зберігають дані, а тільки читають їх з інших потоків.

Колекції й потоки

Починаючи з Java 8 з'явилася можливість отримати потік для читання даних із колекцій (і не тільки з них). Але й це ще не найцікавіше. Насправді з'явилася можливість легко й просто конструювати складні ланцюжки потоків даних; водночас код, який раніше потребував 5–10 рядків, тепер можна було записати 1-2 рядками.

Приклад — пошук рядка максимальної довжини у списку рядків:

Пошук рядка максимальної довжини
ArrayList<String> list = new ArrayList<String>();
Collections.addAll(list, "Привіт", "як", "справи?");
String max = list.stream().max((s1, s2)-> s1.length()-s2.length()).get();
ArrayList<String> list = new ArrayList<String>();
Collections.addAll(list, "Привіт", "як", "справи?");
Stream stream1 = list.stream();
Stream stream2 = stream1.max((s1, s2)-> s1.length()-s2.length());
String max = stream2.get();

3. Інтерфейс Stream

Розширену підтримку потоків у Java 8 реалізовано за допомогою інтерфейсу Stream<T>. де T — це тип-параметр. Він позначає тип даних, які передаються в потоці. Інакше кажучи, потік є повністю незалежним від типу даних, які він передає.

Для отримання об'єкта-потоку з колекції досить викликати для неї метод stream(). Цей код має приблизно такий вигляд:

Stream<Тип> ім'я = колекція.stream();
Отримання потоку з колекції

За цих обставин колекція вважатиметься джерелом даних потоку, а об'єкт типу Stream<Тип> — інструментом для отримання даних із колекції саме у вигляді потоку даних.

ArrayList<String> list = new ArrayList<String>();
Collections.addAll(list, "Привіт", "як", "справи?");
Stream<String> stream = list.stream();

До речі, потік можна отримати не тільки з колекції, а й із масиву. Для цього слід скористатися методом Arrays.stream(); Приклад:

Stream<Тип> ім'я = Arrays.stream(масив);
Отримання потоку з масиву

За цих обставин масив вважатиметься джерелом даних для потоку ім'я.

Integer[] array = {1, 2, 3};
Stream<Integer> stream = Arrays.stream(array);

Після створення об'єкта Stream<Тип> ніякого руху даних не відбувається. Ми просто отримали об'єкт-потік для того, щоб почати будувати ланцюжок із потоків-даних.