Содержание статьи
Многочисленные поклонники Java задавались вопросом, сможет ли Oracle продолжать успешное развитие языка, сохранит ли Java свою лидирующую позицию в рейтинге самых популярных языков программирования?
Изменений произошло много. Наконец были переработаны классы работы с датами и временем (больше не придется подключать библиотеку Joda-Time). Немного видоизменился синтаксис языка — появились лямбда‑выражения, ссылки на методы, методы по умолчанию. Оптимизирована работа с коллекциями и потоками данных: итеративная обработка коллекций, которая раньше занимала несколько строк, теперь сводится к одной‑двум, при этом улучшилась читаемость кода. Претерпела изменения даже сама концепция языка. Так, стало возможным добавление статических методов и методов по умолчанию в интерфейсы. Как обычно, не осталась без внимания организация эффективной работы с памятью. Получили дальнейшее развитие многопоточность и параллельное выполнение кода.
Одни изменения выглядят логичными и востребованными, другие вызовут еще немало споров в форумах и блогах, а мы в рамках этой статьи пойдем более практически ориентированным путем — рассмотрим нововведения на конкретном примере.
Пишем программу на Java 8
Посмотрим, насколько красивее и эффективнее справляется Java 8 с повседневными задачами, на примере простой программы для анализа логов. Представим, что живет где‑то на просторах нашей родины администратор Вася Пупкин и приказало ему начальство следить, чем таким занимаются пользователи на рабочих местах, по каким сайтам ходят вместо работы, и раздавать грозные предупреждения, если они дольше разрешенного зависают в социальных сетях. Самому рыскать в логах Васе не хочется, поэтому решил он написать небольшую программу на Java, которая бы логи парсила, анализировала и по заданным правилам напоминала пользователям, что Большой Брат за ними присматривает.
Лог‑файлы собирает прокси‑сервер в формате:
<временная метка> <имя пользователя> <url>
Раньше, чтобы прочитать такой файл, нам пришлось бы создать BufferedReader и загружать его по строчкам, пока Reader не вернет null. В Java 8 появился способ лучше — интерфейс Stream. Stream представляет собой последовательность объектов, что‑то вроде итератора. Но в отличие от итератора он позволяет не только проходить по коллекции, но и сортировать ее, накладывать фильтры, преобразовывать в словарь или выделять набор уникальных значений, находить максимум и минимум и многое другое. Получается нечто похожее на простенькую SQL-базу. Кроме того, Stream поддерживает ленивую загрузку и параллельную обработку данных. Бывают даже Stream с бесконечным потоком данных, данные в этом случае создаются методом generate. Так можно создать бесконечный пул объектов или бесконечную последовательность случайных чисел.
Для загрузки строки из файла в Stream можно создать экземпляр BufferedReader или воспользоваться классом утилит Files. Стоит упомянуть, что данные в Stream грузятся не все сразу, а порциями (для оптимизации расхода памяти), поэтому входной поток не стоит сразу закрывать.
try (Stream stream = Files.lines(Paths.get("access.log"))) { ...
}
Допустим, нам нужно найти всех пользователей, которые заходили на vkontakte более десяти раз в день. Чтобы работать с данными было проще, преобразуем исходные строки в массив строк, использовав в качестве разделителя пробел. Интерфейс Stream содержит метод map, который позволяет преобразовывать одни данные в другие. В качестве входного параметра метод принимает класс, реализующий функциональный интерфейс Function. Функциональный интерфейс — это интерфейс с одним абстрактным методом. Под это описание подходят даже интерфейсы, известные еще с 7-й версии Java, например ActionListener или Runnable. Такие интерфейсы часто используются при создании анонимных классов, но в результате получается некоторое нагромождение кода. Чтобы исправить эту ситуацию, в Java 8 появились лямбда‑выражения. Говоря простыми словами, лямбда‑выражение — это упрощенное представление анонимного класса с одним методом в виде «параметр → тело». Например,
// Было в Java 7
ActionListener listener = new ActionListener() { @Override
public void actionPerformed(ActionEvent e) { System.out.println(e.getActionCommand()); }};// Стало в Java 8
ActionListener al8 = e -> System.out.println(e.getActionCommand());
В Java 8 включены несколько стандартных функциональных интерфейсов, которые подходят практически под все нужды программиста:
- Function
— на входе объект типа T, на выходе объект типа R; - Supplier
— возвращает объект типа T; - Predicate
— принимает объект типа T, возвращает булево значение; - Consumer
— совершает какое‑то действие над объектом типа T; - BiFunction — такой же, как Function, но с двумя параметрами;
- BiConsumer — работает, как Consumer, но с двумя параметрами.
Стандартные функциональные интерфейсы сделаны по одному принципу. В них есть абстрактный метод apply, в котором необходимо реализовать нужное действие, метод по умолчанию andThen, который позволяет выполнять другое действие вслед за текущим, метод по умолчанию compose, который позволяет выполнить другое действие непосредственно перед текущим, и метод по умолчанию identity, который возвращает входное значение.
Методы по умолчанию были добавлены в Java 8 для лучшей поддержки обратной совместимости. Они позволяют расширять существующие интерфейсы, не ломая при этом классы, их реализующие. Можно сказать, что это первый шаг в сторону множественного наследования. Стоить помнить, что в случае, когда класс реализует несколько интерфейсов с дефолтными методами, у которых сигнатура совпадает, необходимо переопределить этот метод и явно указать, реализацию какого интерфейса использовать, иначе компилятор будет недоволен.
Пришло время воспользоваться полученными знаниями. Превращаем строку в массив:
stream.map(new Function<String, Object>() { @Override
public Object apply(String s) { return s.split(" "); }})
Преобразуем в лямбда‑выражение:
stream.map(s -> s.split(" "))
Оставляем только тех пользователей, кто лазит по vkontakte:
.filter(strings -> strings[2].contains("vkontakte"))
Осталось сгруппировать результаты, чтобы видеть, сколько раз каждый пользователь посетил сайт. Группировкой занимается метод collect, который ожидает класс, реализующий Collector на входе. К счастью, в Java 8 имеется утилитный класс Collectors, который предоставляет практически все группировщики, которые могут понадобиться программисту.
Преобразуем список в словарь, где ключ — имя пользователя, а значение — повторяемость его в списке, и выводим на экран полученные результаты:
.collect(Collectors.groupingBy(strings -> strings[1], Collectors.counting())).forEach((userName, count) -> System.err.println(userName + " " + count));
Теперь заведем отдельный метод для обработки результатов:
public class Inspector { public static void processBadUsers(String userName, Long visitCount) {...}}
Передадим в него результаты:
.forEach(Inspector::processBadUsers)
Странный синтаксис — это обращение к методу по ссылке, еще одно новшество Java 8. Использовать можно не только статические методы, но и методы класса, который передается в параметрах. Например, чтобы преобразовать список строк в список целых чисел, нужно выполнить:
List<String> digits = Arrays.asList("1", "2", "3", "4", "5");List<Integer> result = digits.stream().map(new Function<String, Integer>() { @Override
public Integer apply(String s) { return new Integer(s); }}).collect(Collectors.toList());
Применив лямбда‑выражение, получим:
List<Integer> result = digits.stream().map(s -> new Integer(s)).collect(Collectors.toList());
Используем ссылку на метод:
List<Integer> result = digits.stream().map(Integer::new).collect(Collectors.toList());
Нужно отдать должное Oracle, результат впечатляет: всего пара строк кода делает то, на что в ранних версиях Java ушло бы гораздо больше времени и места.
Добавляем JavaScript
Провинившиеся найдены, пора приступать к порицанию. Наш админ Василий не знает заранее, какую репрессивную меру начальство придумает на этой неделе. Поэтому, чтобы не переписывать программу каждый раз, он решил написать скрипт на JavaScript, который Java-программа могла бы подгрузить и исполнить. К счастью, в Java 8 был реализован JavaScript-движок с гордым названием Nashorn. Запустить JavaScript теперь можно прямо из Java-программы всего несколькими строками кода:
ScriptEngineManager engineManager = new ScriptEngineManager();ScriptEngine engine = engineManager.getEngineByName("nashorn");engine.eval(new FileReader("script.js));
Однако стоит помнить, что JavaScript-программа не будет иметь доступа к переменным, обычно доступным в браузере, таким как document или window. Зато она может импортировать и использовать Java-классы. К примеру, у нас есть класс, который умеет отправлять письма пользователям:
public class SendMailUtil { public void sendMail(String user) { System.err.println("Sending letter to " + user); }}
Тогда в файле скрипта нужно объявить тип SendMailUtil, создать класс этого типа и вызвать нужный метод:
function punish(userName) { var SendMailUtil = Java.type("ru.xakep.inspector.SendMailUtil"); var utils = new SendMailUtil(); utils.sendMail(userName);}
Вызываем функцию скрипта:
Invocable invocable = (Invocable) engine;invocable.invokeFunction("punish", "Kolya");
Запускаем и получаем:
> Sending letter to Kolya
Nashorn позволяет использовать стандартные Java-классы в скриптах, например, можно отправить письма целому списку пользователей:
// Java-код
List<String> userNames = new ArrayList<>();userNames.add("Katya");userNames.add("Kolya");Invocable invocable = (Invocable) engine;invocable.invokeFunction("punish", userNames);// JavaScript
function punish(userNames) { ...
for each (var userName in userNames) { utils.sendMail(userName); }}
В JavaScript-функциях можно использовать даже stream и лямбда‑выражения. Так что при желании можно написать код, ничем не уступающий джавовскому по возможностям и функционалу. Запустить скрипт можно даже из командной строки, воспользовавшись командой jjs:
$ jjs script.js
Другие фичи
На этом список изменений Java 8 не заканчивается. Отдельно хочется упомянуть добавление класса Optional, который помогает избавиться от NullPointerException. Появились аннотации на тип данных для более строгой типизации, повторяющиеся аннотации. Получила дальнейшее развитие многопоточность — появился новый класс CompletableFuture. Его статический метод supplyAsync на вход принимает функциональный интерфейс Supplier и выполняется в другом потоке. После завершения в родительском потоке вызывается метод thenAccept с входным параметром типа Consumer. Это особенно удобно использовать в программах с графическим интерфейсом. Претерпела изменения даже сама виртуальная машина — в Java 8 метаданные классов вынесены в память операционной среды. Больше никаких java.lang.OutOfMemoryError: Permgen space!
Перечисление всех изменений займет не одну страницу. Версия получилась довольно революционная — одни только stream и лямбда‑выражения кардинально меняют многие подходы и паттерны. Но не стоит забывать и о подводных камнях, стоящих за этими нововведениями. Разобраться в том, как и что работает изнутри, сколько потребляет ресурсов и к каким потенциальным проблемам может привести, будет тоже не так просто.
Однако, как бы там ни было, приятно осознавать, что язык развивается. И как мне хочется верить, что развивается в лучшую сторону, оставаясь красивым, лаконичным и простым в использовании.