JavaRush /Java блог /Random UA /Кава-брейк #155. 10 основних функцій у мові Java

Кава-брейк #155. 10 основних функцій у мові Java

Стаття з групи Random UA

10 основних функцій у мові Java

Джерело: DZone У цій статті перераховуються десять функцій програмування мовою Java, які часто використовуються розробниками в їх повсякденній роботі. Кава-брейк #155.  10 головних функцій у мові Java - 1

1. Фабричний метод у колекціях (Collection Factory Method)

Колекції — одна з функцій, що найчастіше використовуються в програмуванні. Вони використовуються як контейнер, в якому ми зберігаємо об'єкти та передаємо їх далі. Колекції також використовуються для сортування, пошуку та повторення об'єктів, що помітно спрощує життя програміста. Вони мають кілька основних інтерфейсів, таких як List, Set, Map, а також кілька реалізацій. Традиційний спосіб створення Collections та Maps може здатися багатьом розробникам багатослівним. Тому Java 9 з'явилося кілька лаконічних фабричних методів. List :
List countries = List.of("Bangladesh", "Canada", "United States", "Tuvalu");
Set :
Set countries = Set.of("Bangladesh", "Canada", "United States", "Tuvalu");
Map :
Map countriesByPopulation = Map.of("Bangladesh", 164_689_383,
                                                            "Canada", 37_742_154,
                                                            "United States", 331_002_651,
                                                            "Tuvalu", 11_792);
Фабричний метод дуже зручний, коли ми хочемо створити незмінні контейнери. Але якщо ви збираєтеся створювати змінні колекції, рекомендується використовувати традиційний підхід.

2. Висновок локального типу (Local Type Inference)

Java 10 додали висновок типів для локальних змінних. До цього розробникам доводиться двічі вказувати типи при оголошенні та ініціалізації об'єкта. Це було дуже втомливо. Подивіться наступний приклад:
Map> properties = new HashMap<>();
Тут вказано тип інформації з обох сторін. Якщо ж ми визначимо його в одному місці, то читач коду та компілятор Java легко зрозуміють, що це має бути тип Map. Висновок локального типу робить саме це. Ось приклад:
var properties = new HashMap>();
Тепер все пишеться лише один раз і код виглядає не набагато гіршим. А коли ми викликаємо метод і зберігаємо результат у змінній, то код стає ще коротшим. Приклад:
var properties = getProperties();
І далі:
var countries = Set.of("Bangladesh", "Canada", "United States", "Tuvalu");
Хоча висновок локального типу здається зручною функцією, дехто її критикує. Деякі розробники стверджують, що таке знижується читабельність. А це важливіше, ніж стислість.

3. Розширені вирази Switch

Традиційний оператор switch існував у Java від початку і тоді він нагадував C і C++. Це було нормально, але в міру розвитку мови цей оператор не пропонував нам жодних покращень, аж до Java 14. Звичайно, у нього були деякі недоліки. Найсумніше відомим було провалювання (fall-through): Щоб вирішити цю проблему, розробники використовували оператори break, які значною мірою є шаблонним кодом. Однак у Java 14 з'явилася вдосконалена версія оператора switch з набагато більшим переліком функцій. Тепер нам більше не потрібно додавати оператори break і це вирішує проблему провалу. Крім того, оператор switch може повертати значення, що означає, що ми можемо використовувати його як вираз і надавати його змінній.
int day = 5;
String result = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";
    case 6, 7 -> "Weekend";
    default -> "Unexpected value: " + day;
};

4. Записи

Хоча Записи (Records) - це відносно нова функція, що з'явилася в Java 16, багато розробників знаходять її дуже корисною, головним чином завдяки створенню незмінних об'єктів. Часто нам потрібні об'єкти даних у нашій програмі для зберігання або передачі значень з одного методу до іншого. Наприклад, клас для перенесення координат x, y та z, який ми запишемо наступним чином:
package ca.bazlur.playground;

import java.util.Objects;

public final class Point {
    private final int x;
    private final int y;
    private final int z;

    public Point(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int x() {
        return x;
    }

    public int y() {
        return y;
    }

    public int z() {
        return z;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (obj == null || obj.getClass() != this.getClass()) return false;
        var that = (Point) obj;
        return this.x == that.x &&
                this.y == that.y &&
                this.z == that.z;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y, z);
    }

    @Override
    public String toString() {
        return "Point[" +
                "x=" + x + ", " +
                "y=" + y + ", " +
                "z=" + z + ']';
    }

}
Клас здається надто багатослівним. За допомогою записів весь цей код можна замінити на більш короткий варіант:
package ca.bazlur.playground;

public record Point(int x, int y, int z) {
}

5.Optional

Метод – це контракт, у якому ми позначаємо умови. Ми вказуємо параметри з їх типом, а також тип, що повертається. Потім ми очікуємо, що при виклику методу він поводитиметься відповідно до контракту. Однак, часто ми отримуємо null з методу замість значення зазначеного типу. Це помилка. Щоб її усунути, ініціатор зазвичай перевіряє значення з умовою if, незалежно від того, чи це значення є нульовим чи ні. Приклад:
public class Playground {

    public static void main(String[] args) {
        String name = findName();
        if (name != null) {
            System.out.println("Length of the name : " + name.length());
        }
    }

    public static String findName() {
        return null;
    }
}
Подивіться на наведений вище код. Передбачається, що метод findName поверне значення String але він повертає null. Ініціатор тепер повинен спочатку перевірити nulls, щоб упоратися з проблемою. Якщо ініціатор забуває зробити це, то зрештою ми отримаємо NullPointerException . З іншого боку, якби сигнатура методу вказувала на можливість неповернення значення, це вирішило б всю плутанину. І ось тут нам може допомогти Optional .
import java.util.Optional;

public class Playground {

    public static void main(String[] args) {
        Optional optionalName = findName();
        optionalName.ifPresent(name -> {
            System.out.println("Length of the name : " + name.length());
        });
    }

    public static Optional findName() {
        return Optional.empty();
    }
}
Тут ми переписали метод findName з Optional , у якому вказано можливість повертати ніякого значення. Це заздалегідь попереджає програмістів та усуває проблему.

6. Java Date Time API

Кожен розробник тією чи іншою мірою плутається з обчисленням дати та часу. Це не перебільшення. В основному це було пов'язано з відсутністю хорошого Java API для роботи з датами та часом. Зараз ця проблема вже не актуальна, тому що в Java 8 з'явився відмінний набір API у пакеті java.time, який вирішує всі питання, пов'язані з датою та часом. Пакет java.time має безліч інтерфейсів та класів, які усувають більшість проблем, включаючи часові пояси. Найчастіше у цьому пакеті використовуються такі класи:
  • LocalDate
  • LocalTime
  • LocalDateTime
  • Duration
  • Period
  • ZonedDateTime
Приклад використання класів із пакету java.time:
import java.time.LocalDate;
import java.time.Month;

public class Playground3 {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2022, Month.APRIL, 4);
        System.out.println("year = " + date.getYear());
        System.out.println("month = " + date.getMonth());
        System.out.println("DayOfMonth = " + date.getDayOfMonth());
        System.out.println("DayOfWeek = " + date.getDayOfWeek());
        System.out.println("isLeapYear = " + date.isLeapYear());
    }
}
Приклад використання класу LocalTime для розрахунку часу:
LocalTime time = LocalTime.of(20, 30);
int hour = time.getHour();
int minute = time.getMinute();
time = time.withSecond(6);
time = time.plusMinutes(3);
Додавання часового поясу:
ZoneId zone = ZoneId.of("Canada/Eastern");
LocalDate localDate = LocalDate.of(2022, Month.APRIL, 4);
ZonedDateTime zonedDateTime = date.atStartOfDay(zone);

7.NullPointerException

Кожен розробник ненавидить виняток NullPointerException. Особливо складно буває, коли StackTrace не надає корисної інформації, в чому полягає проблема. Щоб це продемонструвати, погляньмо на приклад коду:
package com.bazlur;

public class Main {

    public static void main(String[] args) {
        User user = null;
        getLengthOfUsersName(user);
    }

    public static void getLengthOfUsersName(User user) {
        System.out.println("Length of first name: " + user.getName().getFirstName());
    }
}

class User {
    private Name name;
    private String email;

    public User(Name name, String email) {
        this.name = name;
        this.email = email;
    }

   //getter
   //setter
}

class Name {
    private String firstName;
    private String lastName;

    public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

   //getter
   //setter
}
Подивіться на основний метод у цьому уривку. Ми бачимо, що з'явиться далі NullPointerException . Якщо ми запустимо та скомпілюємо код у версії до Java 14, то отримаємо наступний StackTrace:
Exception in thread "main" java.lang.NullPointerException
at com.bazlur.Main.getLengthOfUsersName(Main.java:11)
at com.bazlur.Main.main(Main.java:7)
Тут дуже мало інформації, де і чому виник виняток NullPointerException . А ось у Java 14 і пізніших версіях ми отримуємо набагато більше відомостей у StackTrace, що дуже зручно. У Java 14 ми побачимо:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "ca.bazlur.playground.User.getName()" because "user" is null
at ca.bazlur.playground.Main.getLengthOfUsersName(Main.java:12)
at ca.bazlur.playground.Main.main(Main.java:8)

8. CompletableFuture

Ми пишемо програми рядково, і зазвичай вони виконуються рядково. Але трапляються випадки, коли нам потрібне паралельне виконання, щоб зробити програму швидше. Для цього ми зазвичай використовуємо Java Thread. Програмування потоків Java не завжди пов'язане з паралельним програмуванням. Натомість воно дає нам можливість скласти кілька незалежних модулів програми, які виконуватимуться незалежно і часто навіть асинхронно. Однак програмування потоків є досить складним, особливо для новачків. Ось чому Java 8 пропонує простіший API, який дозволяє виконувати частину програми асинхронно. Давайте подивимося приклад. Припустимо, потрібно викликати три REST API, а потім об'єднати результати. Ми можемо викликати їх поодинці. Якщо кожен із них займає близько 200 мілісекунд, то загальний час їх отримання займе 600 мілісекунд. А що, якби ми могли запускати їх паралельно? Оскільки сучасні процесори зазвичай багатоядерні, вони можуть легко обробляти три виклики rest на трьох різних процесорах. Використовуючи CompletableFuture, ми можемо це зробити.
package ca.bazlur.playground;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class SocialMediaService {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        var service = new SocialMediaService();

        var start = Instant.now();
        var posts = service.fetchAllPost().get();
        var duration = Duration.between(start, Instant.now());

        System.out.println("Total time taken: " + duration.toMillis());
    }

    public CompletableFuture> fetchAllPost() {
        var facebook = CompletableFuture.supplyAsync(this::fetchPostFromFacebook);
        var linkedIn = CompletableFuture.supplyAsync(this::fetchPostFromLinkedIn);
        var twitter = CompletableFuture.supplyAsync(this::fetchPostFromTwitter);

        var futures = List.of(facebook, linkedIn, twitter);

        return CompletableFuture.allOf(futures.toArray(futures.toArray(new CompletableFuture[0])))
                .thenApply(future -> futures.stream()
                        .map(CompletableFuture::join)
                        .toList());
    }
    private String fetchPostFromTwitter() {
        sleep(200);
        return "Twitter";
    }

    private String fetchPostFromLinkedIn() {
        sleep(200);
        return "LinkedIn";
    }

    private String fetchPostFromFacebook() {
        sleep(200);
        return "Facebook";
    }

    private void sleep(int millis) {
        try {
            TimeUnit.MILLISECONDS.sleep(millis);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

9. Лямбда-вирази

Лямбда-вирази, мабуть, – найпотужніша функція Java. Вони змінабо, як ми пишемо код. Лямбда-вираз схоже на анонімну функцію, яка може приймати аргументи та повертати значення. Ми можемо надати функцію змінної і передати її методу як аргументи, а метод може її повернути. Має тіло. Єдина відмінність від методу у тому, що тут немає імені. Вирази короткі та лаконічні. Зазвичай вони містять великої кількості шаблонного коду. Давайте подивимося приклад, у якому нам потрібно перерахувати всі файли в каталозі з розширенням java.
var directory = new File("./src/main/java/ca/bazlur/playground");
String[] list = directory.list(new FilenameFilter() {
    @Override
    public boolean accept(File dir, String name) {
        return name.endsWith(".java");
    }
});
Якщо уважно подивитися на цей уривок коду, то ми передали метод анонімний внутрішній клас list() . А у внутрішній клас ми розмістабо логіку для фільтрації файлів. По суті нас цікавить саме ця частина логіки, а не шаблон навколо логіки. Лямбда-вираз дозволяє нам видалити весь шаблон і ми можемо написати код, який нас цікавить. Ось приклад:
var directory = new File("./src/main/java/ca/bazlur/playground");
String[] list = directory.list((dir, name) -> name.endsWith(".java"));
Звичайно, це лише один приклад, у лямбда-вираження є багато інших переваг.

10. Stream API

У нашій повсякденній роботі одним із найпоширеніших завдань є обробка набору даних. У ній існує кілька загальних операцій, таких як фільтрація, перетворення та збирання результатів. До Java 8 такі операції були імперативними за своєю суттю. Ми повинні були написати код для нашого наміру (тобто чого ми хотіли досягти) і як би ми цього хотіли б зробити. З винаходом лямбда-виразу та Stream API тепер ми можемо записувати функції обробки даних декларативно. Ми лише вказуємо свій намір, і нам не потрібно записувати, як ми отримуємо результат. Ось приклад: У нас є список книг, і ми хочемо знайти всі імена книг по Java, розділені комами та відсортовані.
public static String getJavaBooks(List books) {
    return books.stream()
            .filter(book -> Objects.equals(book.language(), "Java"))
            .sorted(Comparator.comparing(Book::price))
            .map(Book::name)
            .collect(Collectors.joining(", "));
}
Наведений вище код простий, легкочитаємо і лаконічний. А ось нижче можна побачити альтернативний імперативний код:
public static String getJavaBooksImperatively(List books) {
    var filteredBook = new ArrayList();
    for (Book book : books) {
        if (Objects.equals(book.language(), "Java")){
            filteredBook.add(book);
        }
    }
    filteredBook.sort(new Comparator() {
        @Override
        public int compare(Book o1, Book o2) {
            return Integer.compare(o1.price(), o2.price());
        }
    });

    var joiner = new StringJoiner(",");
    for (Book book : filteredBook) {
        joiner.add(book.name());
    }

    return joiner.toString();
}
Хоча обидва методи повертають те саме значення, ми чітко бачимо різницю.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ