Що відбувалося з Java в останні роки. Огляд найважливіших нововведень

by

Привіт, я Володимир, 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 і можна писати код наступним чином:

    var result = switch (number) {
            case 1, 3, 5, 7, 9:
                break "not even";
            case 2, 4, 6, 8:
                break "even";
            default:
                break "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>
            </html>
        """;
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 :)

Темы: Java, tech