Java 8 мож­но по пра­ву наз­вать самой ожи­даемой вер­сией. Тысячи прог­раммис­тов по все­му миру, зата­ив дыхание, пытались понять, по какому пути пой­дет раз­витие Java пос­ле пог­лощения ком­пании Sun Oracle и ухо­да мно­гих талан­тли­вых инже­неров, вклю­чая самого Джей­мса Галин­га, которо­го называ­ют авто­ром Java.

Мно­гочис­ленные пок­лонни­ки 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 и лям­бда‑выраже­ния кар­диналь­но меня­ют мно­гие под­ходы и пат­терны. Но не сто­ит забывать и о под­водных кам­нях, сто­ящих за эти­ми новов­ведени­ями. Разоб­рать­ся в том, как и что работа­ет изнутри, сколь­ко пот­ребля­ет ресур­сов и к каким потен­циаль­ным проб­лемам может при­вес­ти, будет тоже не так прос­то.

Од­нако, как бы там ни было, при­ятно осоз­навать, что язык раз­вива­ется. И как мне хочет­ся верить, что раз­вива­ется в луч­шую сто­рону, оста­ваясь кра­сивым, лаконич­ным и прос­тым в исполь­зовании.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии