Java: що нового
Привіт, я Володимир, Java-розробник в Perfectial, Java Lead в LITS і ментор на Cursor Education. Готуючись до доповіді на JavaDay Lviv 2020, я розбирав основні фічі, що з’явились в останніх версіях Java і які, на мою думку, важливо знати розробнику. Тепер вирішив поділитись інформацією у статті.
Як відомо, у вересні 2017 року архітектор Java-платформи Марк Рейнхольд запропонував змінити реліз-трейн: замість релізу кожних два (а то і більше) років, випускати новий реліз кожні півроку. На відміну від попередньої стратегії, коли версія не релізилась, поки не було готових запланованих JEP-ів, тепер в реліз йдуть лише готові. Усе недопрацьоване — чекає наступного релізу.
Local variable type inference
Перше, на що хочу звернути увагу, це Local variable type inference.
Поняття Type Inference не є новим. У Java 10 додали можливість використовувати це для локальних змінних. Тепер же, замість оголошення типу, можна написати слово «var» і компілятор сам визначить тип змінної.
Отже, код Object obj = new Object();
можна записати таким чином: var obj = new Object();
Сама назва jep-у говорить про те, що var — для локальних змінних. Для змінних класу і аргументів його використати не можна. Код:
var i = null; var i; var func = () -> System.out.println("Hello world");
не буде компілюватись, а видасть помилку компіляції. У перших двох рядках компілятор просто не знатиме, який тип потірібно взати, а у третьому — результатом буде тип функціонального інтерфейсу, а не інтерфейс.
Ми не зможемо зберегти результат лямбда-виразу у змінну var, оскільки отримаємо сам тип функціонального інтерфейсу, а не інтерфейс. Маючи Local variable type inference, ми втарчаємо можливість використовувати поліморфізм, і наступний код видасть помилку, оскільки вказуємо тип ArrayList, а не List.
var list = new ArrayList<String> (); list = new LinkedList<String> ();
При роботі з примітивами потрібно вказувати літерал, оскільки за замовчуванням відбувається неявне приведення типів до int:
var intNum = 42; // cast to int var longNum = 42; // cast to int var doubleNum = 42; // cast to int
Тому, щоб зберегти коректний тип, потрібно використовувати літерали:
var intNum = 42; // cast to int var longNum = 42L; // cast to long var doubleNum = 42D; // cast to double, value is 42.0 При роботі з примітивами потрібно вказувати літерал, оскілька за замовчуванням відбувається приведення типів до "int":
HttpClient
Однією з найцікавіших функцій, що з’явилась у Java 11 (якщо бути точним, то її додали ще у Java 9, але як інкубаційний модуль), є HttpClient.
До виходу класу HttpClient для роботи з http, в Java використовувався URLConnection, що створювало складнощі. Підтримки HTTP/2 не було, тому багато хто для роботи з http використовував зовнішні бібліотеки. HttpClient підтримує протокол HTTP/1.1 і HTTP/2 , синхронні і асинхронні моделі програмування, дає змогу отримувати body як reactive-stream.
HttpClient реалізований на основі патерну Builder.
var client = HttpClient.newBuilder() .version(Version.HTTP_2) .build();
Наступний код створить GET запит:
var request = HttpRequest.newBuilder() .uri(URI.create(«URL») .GET() .build()
Щоб виконати запит var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Тип response буде HttpResponse<String>
Http-клієнт не має функціоналу для пітримки form-data, тому це потрібно створювати вручну:
public static HttpRequest.BodyPublisher ofFormData(Map<Object, Object> data) { var builder = new StringBuilder(); for (Map.Entry<Object, Object> entry : data.entrySet()) { if (builder.length() > 0) { builder.append("&"); } builder.append(URLEncoder.encode(entry.getKey().toString(), StandardCharsets.UTF_8)); builder.append("="); builder.append(URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8)); } return HttpRequest.BodyPublishers.ofString(builder.toString()); }
Асинхронний запит виглядає наступним чином: var asyncResponse = httpClient. sendAsync(request, HttpResponse.BodyHandlers.ofString());
Тип змінної asyncResponse
у цьому випадку буде CompletableFuture<HttpResponse<String>>
.
API Updates
У Java 11 до класу String були додані нові методи:
String strip()
повертає String, видаливши всі пробіли на початку і в кінці.
String stripLeading()
повертає String, видаливши всі пробіли з лівої частини.
String stripTrailing()
повертає String, видаливши всі пробіли з правої частини.
String isBlank()
перевіряє, чи є String пустою без символів, табуляцій (окрім пробілів).
String isEmpty()
повертає результат чи є String пустою без символів, табуляцій (окрім пробілів).
String repeat()
повертає String задану кількість разів.
String lines()
перетворює String y Stream з поділом: \n«, «\r», «\r\n».
Окрім класу String, нові методи додано і до інших класів.
Path of(String path)
повертає Path за вказаною адресою.
Path of(URI uri)
повертає Path за вказаним URI.
У класі Files з’явились статичні методи writeString
і readString
, що дозволяють просто записати чи прочитати String з заданого файлу.
Щоб записати String у файл text.txt:
var path = Path.of("text.txt"); Files.writeString(path, "Some text");
і для зчитування з файлу:
var path = Path.of("text.txt"); var text = Files.readString(path);
Predicate not(Predicate predicate):
повертає предикат, що є запереченням заданого predicate.
Optional isEmpty():
повертає true, якщо optional є порожнім.
Цей метод зручний, коли при роботі з Optional є потреба перевіряти, чи Optional порожній чи ні. Для цього є метод optional.isPresent()
, що повертає true, якщо optional не є порожнім.
У випадку, коли треба перевірити, чи optional є порожнім, можна без проблем написати !optional.isPresent()
.
return !userRepository .getAllByDepartmentId(id) .map(user -> modelMapper.map(user, UserDto.class)) .filter(UserService::isUserHavePermissions) .isPresent();
У такому випадку втрачається читабельність коду і знак «!» можна не побачити і пропустити, тому використання isEmpty()
у таких випадках дає нами кращу читабельність коду:
return userRepository .getAllByDepartmentId(id) .map(user -> modelMapper.map(user, UserDto.class)) .filter(UserService::isUserHavePermissions) .isEmpty;
Collections toArray(Function function):
приймає лямбда-вираз як аргумент, i за допомогою переданої function, перетворює колекцію у масив елементів.
var list = Arrays.asList(1, 2, 3, 4, 5); Integer[] integers = list.toArray(Integer[]::new);
Окрім нових методів, у Java 11 видалили методи класу Thread:destroy() i stop(Throwable)
.
Більшість з нас «любить» switch-expression. У Java 12 він зазнав значних змін. Запустивши програму з прапорцем --enalved-preview
, отримаємо новий switch. Тепер в switch є multiple case lable і можна писати код наступним чином:
switch (number) { case 1, 3, 5, 7, 9: result = “not even”; break; case 2, 4, 6, 8: result = “even”; break; default: result = “zero”; }
break
тепер може повертати значення:
var result = switch (number) { case 1, 3, 5, 7, 9: break "not even"; case 2, 4, 6, 8: break "even"; default: break "zero"; }
Можна написати код, використовуючи «arrow syntax»:
var result = switch (number) { case 1, 3, 5, 7, 9 -> “not even”; case 2, 4, 6, 8 -> “even”; default -> “zero”; }
У Java 12 до класу String додано декілька нових методів.
String indent(int count):
додає вказану в аргументах кількість пробілів перед стрінгою (якщо є \n) і додає і вкінці \n.
І в консолі отримаємо:
Hi, Hello Hi, Hello Hi, Hello
String transform(Function<? super String, ? extends R> f):
приймає String як аргумент і R як результат.
char[] transform = template.transform(String::toCharArray);
Teeing collector
Функція Teeing Collector
не була анонсована в офіційному JEP, а додана як мінорний change request.
Teeing collector повертає колектор, що складається з двох колекторів. Кожний елемент, переданий у результуючий колектор, опрацьовується двома колекторами, після чого вони змерджуються в один.
var result = Stream.of("Rob", "Max", "John", "Bob") .collect(Collectors.teeing( Collectors.filtering(n -> n.contains("o"), Collectors.toList()), Collectors.filtering(n -> n.endsWith("ob"), Collectors.toList()), (List<String> list1, List<String> list2) -> List.of(list1, list2)));
System.out.println(result);
І результатом буде: [[Rob, John, Bob], [Rob, Bob]]
Text blocks
Усім знайомий наступний код:
String loremIpsum = "Lorem ipsum dolor sit amet," + "consectetur adipiscing elit," + " sed do eiusmod tempor incididunt ut" + "labore et dolore magna aliqua.";
Код є не надто читабельним і зручним, тоді як у Scala i Kotlin є текстові блоки, що дозволяють записувати такий код зручніше. Text blocks у Java 13 є частиною майбутнього «Raw String Literals», що дозволяє писати і читати багаторядковий код набагато зручніше. Ця фіча давно підтримується у Scala, Kotlin, а тепер і в Java. Щоб зберегти багаторядковий String, раніше доводилось використовувати конкатенацію і літерал \n, а тепер все набагато простіше. Такий синтаксис має читабельний вигляд і записувати його набагато зручніше.
var s = """ <html> <title> <p > Java is a top</p> </title> <body> <p> Text Block</p> </body> """
var day = switch (day) { case 1 -> numericString = "SUN"; case 2 -> numericString = "MON"; case 3 -> numericString = "THU"; default -> { numericString = "N/A"; System.out.println("Incorrect input"); yield "n / a"; }
Новий switch в Java 13 є в статусі preview language feature, тобто за замовчуванням цей синтаксис не включений.
Dynamic CDS
Також варто згадати Dynamic CDS (Class Data Sharing) Archiver, що дозволяє запакувати найбільш використовувані класи в спеціальний архів, який можна завантажувати декількома JVM. Щоб завантажити класи, JVM виконує ряд операцій: зчитування класів та зберігання їх у внутрішніх структурах, пошук залежностей, перевірки над класом і т. д. У Java 5 додано CDS, який працює з bootstrap class loader.
У Java 10 додали CDS з префіксом Application, ідея якого розширити можливості вже існуючого CDS, включаючи в архів application класи.
Dynamic CDS покращує CDS таким чином, що він зможе створювати архіви при завершенні роботи програми, тобто класи, завантажені при роботі програми, будуть додані в архів.
P. S.
За декілька місяців має вийти реліз Java 14, що містить доволі цікаві JEP-и: HelpfulNullPointerExceptions, Records, Pattern Matching for Instanceof, second preview of Text Blocks. Зі зміною реліз-трейну нові фічі почали виходити набагато швидше, що говорить про те, що Java never die :)