JavaRush /Java блог /Random /Кофе-брейк #155. 10 главных функций в языке Java

Кофе-брейк #155. 10 главных функций в языке Java

Статья из группы Random

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();
}
Хотя оба метода возвращают одно и то же значение, мы четко видим разницу.
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Иван Голубев Уровень 108 Expert
17 сентября 2022
В п. 2 криво дженерики указаны Map> properties = new HashMap<>();