книги хакеры / журнал хакер / 187_Optimized
.pdf
|
|
|
|
hang |
e |
|
|
|
|
|
|
|
C |
|
E |
|
|||
|
|
X |
|
|
|
|
|||
|
- |
|
|
|
|
|
d |
||
|
F |
|
|
|
|
|
|
t |
|
|
D |
|
|
|
|
|
|
|
i |
|
|
|
|
|
|
|
|
r |
|
P |
|
|
|
|
|
NOW! |
o |
||
|
|
|
|
|
|
||||
|
|
|
|
|
BUY |
|
|||
|
|
|
|
to |
110 m |
||||
w Click |
|
||||||||
|
|
||||||||
w |
|
|
|
|
|
|
|
|
|
|
w |
|
|
|
|
|
|
|
o |
|
. |
|
|
|
|
|
.c |
||
|
|
p |
|
|
|
|
g |
|
|
|
|
|
df |
|
|
n |
e |
||
|
|
|
|
-xcha |
|
|
|
Ţ
|
|
|
|
|
|
hang |
e |
|
|
|
|
|
|
|
|
|
|
C |
|
E |
|
|
|||
|
|
|
|
X |
|
|
|
|
|
|||
|
|
|
- |
|
|
|
|
|
d |
|
||
|
|
|
F |
|
|
|
|
|
|
t |
|
|
|
|
|
D |
|
|
|
|
|
|
|
i |
|
|
|
|
|
|
|
|
|
|
|
r |
||
|
|
P |
|
|
|
|
|
NOW! |
o |
|||
|
|
|
|
|
|
|
|
|
||||
|
|
|
|
|
|
|
BUY |
|
|
|||
Кодинг |
|
|
|
|
|
to |
|
|
|
|
|
|
ХАКЕР 08 |
w Click |
|
|
|
|
|
m |
|||||
|
|
/187/ 2014 |
|
|
|
|
|
|
||||
|
|
w |
|
|
|
|
|
|
|
|
|
|
|
|
|
w |
|
|
|
|
|
|
|
o |
|
|
|
|
. |
|
|
|
|
|
.c |
|
||
|
|
|
|
p |
|
|
|
|
g |
|
|
|
|
|
|
|
|
df |
|
|
n |
e |
|
||
|
|
|
|
|
|
-x cha |
|
|
|
|
ногочисленные поклонники 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());
}
};
//DZȦȔȟȢ Ȗ Java 8
ActionListener al8 = e ->
System.out.println(e.getActionCommand());
В Java 8 включены несколько стандартных функциональных интерфейсов, которые подходят практически под все нужды программиста:
•Function<T,R> — на входе объект типа T, на выходе объект типа R;
•Supplier<T> — возвращает объект типа T;
•Predicate<T> — принимает объект типа T, возвращает булево значение;
•Consumer<T> — совершает какое-то действие над объектом типа 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()))
|
|
|
|
hang |
e |
|
|
|
|
|
|
|
|
C |
|
E |
|
|
|||
|
|
X |
|
|
|
|
|
|||
|
- |
|
|
|
|
|
d |
|
||
|
F |
|
|
|
|
|
|
t |
|
|
|
D |
|
|
|
|
|
|
|
i |
|
|
|
|
|
|
|
|
|
r |
||
P |
|
|
|
|
|
NOW! |
o |
|
||
|
|
|
|
|
|
|
||||
|
|
|
|
|
BUY |
|
|
|||
|
|
|
|
to |
|
|
|
|
|
|
w Click |
|
ХАКЕР m |
08 /187/ 2014 |
|||||||
|
|
|||||||||
w |
|
|
|
|
|
|
|
|
|
|
|
w |
|
|
|
|
|
|
|
o |
|
|
. |
|
|
|
|
|
.c |
|
||
|
|
p |
|
|
|
|
g |
|
|
|
|
|
|
df |
|
|
n |
e |
|
||
|
|
|
|
-xcha |
|
|
|
|
Анализатор логов на Java 8
|
|
|
|
|
hang |
e |
|
|
|
|
|
|
|
|
|
C |
|
E |
|
|
|||
|
|
|
X |
|
|
|
|
|
|||
|
|
- |
|
|
|
|
|
d |
|
||
|
F |
|
|
|
|
|
|
t |
|
||
|
D |
|
|
|
|
|
|
|
|
i |
|
|
|
|
|
|
|
|
|
|
r |
||
P |
|
|
|
|
|
|
NOW! |
o |
|||
|
|
|
|
|
|
|
|
||||
|
|
|
|
|
|
BUY |
|
|
|||
|
|
|
|
|
to |
|
|
|
|
|
|
w |
|
|
|
|
|
|
|
|
m |
||
111Click |
|
|
|
|
|
||||||
w |
|
|
|
|
|
|
|
|
|
|
|
|
w |
|
|
|
|
|
|
|
o |
|
|
|
|
. |
|
|
|
|
|
.c |
|
||
|
|
|
p |
|
|
|
|
g |
|
|
|
|
|
|
|
df |
|
|
n |
e |
|
||
|
|
|
|
|
-x cha |
|
|
|
|
.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 и лямбдавыражения кардинально меняют многие подходы и паттерны. Но не стоит забывать и о подводных камнях, стоящих за этими нововведениями. Разобраться в том, как и что работает изнутри, сколько потребляет ресурсов и к каким потенциальным проблемам может привести, будет тоже не так просто.
Однако, как бы там ни было, приятно осознавать, что язык развивается. И как мне хочется верить, что развивается в лучшую сторону, оставаясь красивым, лаконичным и простым в использовании.