Руководство по Java 8 Optional

от автора

1. Обзор

В этом учебном пособии мы рассмотрим класс Optional, который был представлен в Java 8.

Цель класса — предоставить решение на уровне типа для представления опциональных значений вместо null (нулевых) ссылок.

Для более глубокого понимания того, почему мы должны обратить внимание на класс Optional, ознакомьтесь с официальной статьей Oracle.

2. Создание объектов Optional

Существует несколько способов создания объектов Optional.

Чтобы создать пустой объект Optional, нужно просто использовать его статический метод empty():

@Test public void whenCreatesEmptyOptional_thenCorrect() {     Optional<String> empty = Optional.empty();     assertFalse(empty.isPresent()); }

Обратите внимание, что мы использовали метод isPresent() для проверки наличия значения внутри объекта Optional. Значение присутствует, только если мы создали Optional с non-null (ненулевым) значением. Мы рассмотрим метод isPresent() в следующем разделе.

Можно также создать объект Optional с помощью статического метода of():

@Test public void givenNonNull_whenCreatesNonNullable_thenCorrect() {     String name = "baeldung";     Optional<String> opt = Optional.of(name);     assertTrue(opt.isPresent()); }

Однако аргумент, переданный в метод of(), не может быть null. В противном случае мы получим NullPointerException:

@Test(expected = NullPointerException.class) public void givenNull_whenThrowsErrorOnCreate_thenCorrect() {     String name = null;     Optional.of(name); }

Но в случае, если мы предполагаем некоторые значения null, то можно использовать метод ofNullable():

@Test public void givenNonNull_whenCreatesNullable_thenCorrect() {     String name = "baeldung";     Optional<String> opt = Optional.ofNullable(name);     assertTrue(opt.isPresent()); }

Таким образом, если мы передаем null ссылку, это не вызовет исключения, а вернет пустой объект Optional:

@Test public void givenNull_whenCreatesNullable_thenCorrect() {     String name = null;     Optional<String> opt = Optional.ofNullable(name);     assertFalse(opt.isPresent()); }

3. Проверка наличия значения: isPresent() и isEmpty()

Когда у нас есть объект Optional, возвращенный из метода или созданный нами, мы можем проверить, есть ли в нем значение или нет, с помощью метода isPresent():

@Test public void givenOptional_whenIsPresentWorks_thenCorrect() {     Optional<String> opt = Optional.of("Baeldung");     assertTrue(opt.isPresent());      opt = Optional.ofNullable(null);     assertFalse(opt.isPresent()); }

Этот метод возвращает true, если обернутое значение не является null.

Также, начиная с Java 11, мы можем сделать обратное с помощью метода isEmpty:

@Test public void givenAnEmptyOptional_thenIsEmptyBehavesAsExpected() {     Optional<String> opt = Optional.of("Baeldung");     assertFalse(opt.isEmpty());      opt = Optional.ofNullable(null);     assertTrue(opt.isEmpty()); }

4. Условное действие с помощью ifPresent()

Метод ifPresent() позволяет нам запустить некоторый код для обернутого значения, если выяснится, что оно не является null. До метода Optional мы бы сделали следующее:

if(name != null) {     System.out.println(name.length()); }

Этот код проверяет, является ли переменная name null или нет, прежде чем приступить к выполнению какого-либо кода над ней. Такой подход занимает много времени, и это не единственная проблема — он также склонен к ошибкам.

В самом деле, где гарантия, что после печати этой переменной мы не воспользуемся ею снова, а потом забудем выполнить проверку на null?

Это может привести к NullPointerException во время выполнения программы, если в этот код попадет значение null. Когда программа терпит неудачу из-за проблем с вводом, это часто является результатом плохой практики программирования.

Optional заставляет нас иметь дело с допускающими null значениями в явном виде, как способ принуждения к хорошей практике программирования.

Теперь давайте посмотрим, каким образом приведенный выше код может быть рефакторизован в Java 8.

В типичном стиле функционального программирования мы можем выполнить действие над объектом, который действительно присутствует:

@Test public void givenOptional_whenIfPresentWorks_thenCorrect() {     Optional<String> opt = Optional.of("baeldung");     opt.ifPresent(name -> System.out.println(name.length())); }

5. Значение по умолчанию с помощью orElse()

Метод orElse() используется для получения значения, обернутого внутри экземпляра Optional. Он принимает один параметр, который выступает в качестве значения по умолчанию. Метод orElse() возвращает обернутое значение, если оно присутствует, либо его аргумент в противном случае:

@Test public void whenOrElseWorks_thenCorrect() {     String nullName = null;     String name = Optional.ofNullable(nullName).orElse("john");     assertEquals("john", name); }

6. Значение по умолчанию с помощью orElseGet()

Метод orElseGet() аналогичен методу orElse(). Однако вместо того, чтобы принимать значение для возврата, если Optional значение отсутствует, он принимает функциональный интерфейс поставщика, который вызван и возвращает значение вызова:

@Test public void whenOrElseGetWorks_thenCorrect() {     String nullName = null;     String name = Optional.ofNullable(nullName).orElseGet(() -> "john");     assertEquals("john", name); }

7. Разница между orElse и orElseGet()

Многим программистам, которые только начинают работать с Optional или Java 8, разница между orElse() и orElseGet() непонятна. На самом деле, создается впечатление, что эти два метода перекрывают друг друга по функциональности.

Однако между ними есть тонкое, но очень важное различие, которое может сильно повлиять на производительность нашего кода, если его не понять.

Давайте создадим в тестовом классе метод getMyDefault(), который не принимает никаких аргументов и возвращает значение по умолчанию:

public String getMyDefault() {     System.out.println("Getting Default Value");     return "Default Value"; }

Давайте рассмотрим два теста и понаблюдаем за их побочными эффектами, чтобы определить, где orElse() и orElseGet() пересекаются, а где отличаются:

@Test public void whenOrElseGetAndOrElseOverlap_thenCorrect() {     String text = null;      String defaultText = Optional.ofNullable(text).orElseGet(this::getMyDefault);     assertEquals("Default Value", defaultText);      defaultText = Optional.ofNullable(text).orElse(getMyDefault());     assertEquals("Default Value", defaultText); }

В приведенном выше примере мы обертываем null текст внутри объекта Optional и пытаемся получить обернутое значение, используя каждый из двух подходов.

Побочный эффект таков:

Getting default value... Getting default value...

В каждом случае вызывается метод getMyDefault(). Выходит, что если обернутое значение отсутствует, то и orElse(), и orElseGet() работают одинаково.

Теперь давайте проведем еще один тест, в котором значение присутствует, и в идеале значение по умолчанию даже не должно создаваться:

@Test public void whenOrElseGetAndOrElseDiffer_thenCorrect() {     String text = "Text present";      System.out.println("Using orElseGet:");     String defaultText        = Optional.ofNullable(text).orElseGet(this::getMyDefault);     assertEquals("Text present", defaultText);      System.out.println("Using orElse:");     defaultText = Optional.ofNullable(text).orElse(getMyDefault());     assertEquals("Text present", defaultText); }

В приведенном выше примере мы больше не оборачиваем значение null, а остальная часть кода остается прежней.

Теперь давайте посмотрим на побочный эффект от выполнения этого кода:

Using orElseGet: Using orElse: Getting default value...

Обратите внимание, что при использовании orElseGet() для извлечения обернутого значения метод getMyDefault() даже не вызывается, поскольку содержащееся значение присутствует.

Однако при использовании orElse(), независимо от того, есть ли обернутое значение или нет, создается объект по умолчанию. Таким образом, в данном случае мы просто создали один лишний объект, который никогда не используется.

В этом простом примере создание объекта по умолчанию не требует значительных затрат, поскольку JVM знает, как с ним обращаться. Однако когда такой метод, как getMyDefault(), должен выполнить вызов веб-сервиса или даже запрос к базе данных, затраты становятся весьма очевидными.

8. Исключения с помощью orElseThrow()

Метод orElseThrow() следует из orElse() и orElseGet() и добавляет новый подход к обработке отсутствующего значения.

Вместо того чтобы возвращать значение по умолчанию, когда обернутое значение отсутствует, он выбрасывает исключение:

@Test(expected = IllegalArgumentException.class) public void whenOrElseThrowWorks_thenCorrect() {     String nullName = null;     String name = Optional.ofNullable(nullName).orElseThrow(       IllegalArgumentException::new); }

Здесь пригодятся ссылки на методы в Java 8, чтобы передать в конструктор исключения.

Java 10 представила упрощенную версию метода orElseThrow() без аргументов. В случае пустого Optional он выбрасывает исключение NoSuchElementException:

@Test(expected = NoSuchElementException.class) public void whenNoArgOrElseThrowWorks_thenCorrect() {     String nullName = null;     String name = Optional.ofNullable(nullName).orElseThrow(); }

9. Возвращение значения с помощью get()

Последний способ получения обернутого значения — метод get():

@Test public void givenOptional_whenGetsValue_thenCorrect() {     Optional<String> opt = Optional.of("baeldung");     String name = opt.get();     assertEquals("baeldung", name); }

Однако, в отличие от предыдущих трех подходов, get() может вернуть значение, только если обернутый объект не является null; в противном случае он выбрасывает исключение нет такого элемента:

@Test(expected = NoSuchElementException.class) public void givenOptionalWithNull_whenGetThrowsException_thenCorrect() {     Optional<String> opt = Optional.ofNullable(null);     String name = opt.get(); }

Это основной недостаток метода get(). В идеале Optional должен помочь нам избежать таких непредвиденных исключений. Поэтому данный подход противоречит целям Optional и, вероятно, будет упразднен в одном из будущих выпусков.

Поэтому рекомендуется использовать другие варианты, которые позволяют нам быть готовыми к случаю null и явно его обрабатывать.

10. Условный возврат с помощью filter()

Мы можем запустить встроенный тест на нашем обернутом значении с помощью метода filter. Он принимает предикат в качестве аргумента и возвращает объект Optional. Если обернутое значение проходит проверку предикатом, то Optional возвращается как есть.

Однако если предикат вернет false, то будет возвращен пустой Optional:

@Test public void whenOptionalFilterWorks_thenCorrect() {     Integer year = 2016;     Optional<Integer> yearOptional = Optional.of(year);     boolean is2016 = yearOptional.filter(y -> y == 2016).isPresent();     assertTrue(is2016);     boolean is2017 = yearOptional.filter(y -> y == 2017).isPresent();     assertFalse(is2017); }

Метод filter обычно используется таким образом для отклонения обернутых значений на основе предопределенного правила. Мы могли бы использовать его для отбраковки неправильного формата электронной почты или недостаточно надежного пароля.

Давайте рассмотрим другой показательный пример. Допустим, мы хотим купить модем, и нас интересует только его цена.

С определенного сайта нам приходят push-уведомления о ценах на модемы, и они сохраняются в объектах:

public class Modem {     private Double price;      public Modem(Double price) {         this.price = price;     }     // standard getters and setters }

Затем мы передаем эти объекты некоторому коду, единственная цель которого — проверить, находится ли цена модема в пределах нашего бюджета.

Теперь давайте посмотрим на код без Optional:

public boolean priceIsInRange1(Modem modem) {     boolean isInRange = false;      if (modem != null && modem.getPrice() != null        && (modem.getPrice() >= 10          && modem.getPrice() <= 15)) {          isInRange = true;     }     return isInRange; }

Обратите внимание на то, как много кода нам приходится писать для достижения этой цели, особенно в условии if. Единственная часть условия if, которая критична для приложения, — это последняя проверка диапазона цен; остальные проверки носят вспомогательный характер:

@Test public void whenFiltersWithoutOptional_thenCorrect() {     assertTrue(priceIsInRange1(new Modem(10.0)));     assertFalse(priceIsInRange1(new Modem(9.9)));     assertFalse(priceIsInRange1(new Modem(null)));     assertFalse(priceIsInRange1(new Modem(15.5)));     assertFalse(priceIsInRange1(null)); }

Кроме того, о проверках на null можно забыть надолго, не получив ни одной ошибки во время компиляции.

Теперь давайте рассмотрим вариант с Optional#filter:

public boolean priceIsInRange2(Modem modem2) {      return Optional.ofNullable(modem2)        .map(Modem::getPrice)        .filter(p -> p >= 10)        .filter(p -> p <= 15)        .isPresent();  }

Вызов map используется для трансформации одного значения в другое. Следует помнить, что эта операция не изменяет исходное значение.

В нашем случае мы получаем объект цены из класса Model. Метод map() будет подробно рассмотрен в следующем разделе.

Во-первых, если в этот метод передается объект null, то никаких проблем не предвидится.

Во-вторых, единственная логика, которую мы прописываем внутри его тела, это именно то, что описывает название метода — проверка ценового диапазона. Об остальном позаботится Optional:

@Test public void whenFiltersWithOptional_thenCorrect() {     assertTrue(priceIsInRange2(new Modem(10.0)));     assertFalse(priceIsInRange2(new Modem(9.9)));     assertFalse(priceIsInRange2(new Modem(null)));     assertFalse(priceIsInRange2(new Modem(15.5)));     assertFalse(priceIsInRange2(null)); }

Предыдущий подход обещает проверить диапазон цен, но при этом должен сделать нечто большее, чтобы защититься от присущей ему хрупкости. Поэтому мы можем использовать метод filter, чтобы заменить ненужные операторы if и отбросить нежелательные значения.

11. Трансформация значения с помощью map()

В предыдущем разделе мы рассмотрели, как отклонить или принять значение на основе фильтра.

Мы можем использовать аналогичный синтаксис для преобразования значения Optional с помощью метода map():

@Test public void givenOptional_whenMapWorks_thenCorrect() {     List<String> companyNames = Arrays.asList(       "paypal", "oracle", "", "microsoft", "", "apple");     Optional<List<String>> listOptional = Optional.of(companyNames);      int size = listOptional       .map(List::size)       .orElse(0);     assertEquals(6, size); }

В этом примере мы оборачиваем список строк внутри объекта Optional и используем его метод map для выполнения действия над содержащимся в нем списком. Оно заключается в получении размера списка.

Метод map возвращает результат вычислений, завернутый внутрь Optional. Затем мы должны вызвать соответствующий метод для возвращенного Optional, чтобы получить это значение.

Обратите внимание, что метод filter просто выполняет проверку значения и возвращает Optional, описывающий это значение, только если оно соответствует заданному предикату. В противном случае возвращается пустой Optional. Метод map берет существующее значение, выполняет вычисления, используя его, и возвращает результат вычислений, обернутый в объект Optional:

@Test public void givenOptional_whenMapWorks_thenCorrect2() {     String name = "baeldung";     Optional<String> nameOptional = Optional.of(name);      int len = nameOptional      .map(String::length)      .orElse(0);     assertEquals(8, len); }

Мы можем соединить map и filter вместе, чтобы сделать что-то более мощное.

Допустим, нам нужно проверить правильность пароля, введенного пользователем. Можно очистить пароль с помощью трансформации map и проверить его правильность с помощью filter:

@Test public void givenOptional_whenMapWorksWithFilter_thenCorrect() {     String password = " password ";     Optional<String> passOpt = Optional.of(password);     boolean correctPassword = passOpt.filter(       pass -> pass.equals("password")).isPresent();     assertFalse(correctPassword);      correctPassword = passOpt       .map(String::trim)       .filter(pass -> pass.equals("password"))       .isPresent();     assertTrue(correctPassword); }

Как мы видим, без предварительной очистки ввода он будет отфильтрован. Однако пользователи могут считать само собой разумеющимся, что начальные и конечные пробелы — это всё входные данные. Поэтому мы преобразуем измененный пароль в очищенный с помощью map, прежде чем отфильтровать неправильные пароли.

12. Трансформация значения с помощью flatMap()

Подобно методу map(), у нас также есть метод flatMap() в качестве альтернативы для трансформации значений. Разница в том, что map преобразует значения только тогда, когда они извлечены (развернуты), в то время как flatMap берет обернутое значение и разворачивает его перед трансформацией.

Ранее мы создавали простые объекты String и Integer для обертывания в экземпляр Optional. Теперь, зачастую, мы будем получать эти объекты от асессора сложного объекта.

Чтобы лучше понять разницу, давайте рассмотрим объект Person, который принимает данные человека, такие как имя, возраст и пароль:

public class Person {     private String name;     private int age;     private String password;      public Optional<String> getName() {         return Optional.ofNullable(name);     }      public Optional<Integer> getAge() {         return Optional.ofNullable(age);     }      public Optional<String> getPassword() {         return Optional.ofNullable(password);     }      // normal constructors and setters }

Обычно мы создаем такой объект и оборачиваем его в Optional, как мы это делали со String.

В качестве альтернативы он может быть возвращен нам другим вызовом метода:

Person person = new Person("john", 26); Optional<Person> personOptional = Optional.of(person);

Обратите внимание, что когда мы обернем объект Person, он будет содержать вложенные экземпляры Optional:

@Test public void givenOptional_whenFlatMapWorks_thenCorrect2() {     Person person = new Person("john", 26);     Optional<Person> personOptional = Optional.of(person);      Optional<Optional<String>> nameOptionalWrapper         = personOptional.map(Person::getName);     Optional<String> nameOptional         = nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);     String name1 = nameOptional.orElse("");     assertEquals("john", name1);      String name = personOptional       .flatMap(Person::getName)       .orElse("");     assertEquals("john", name); }

Здесь мы пытаемся извлечь атрибут name объекта Person, чтобы выполнить утверждение.

Обратите внимание, как мы достигаем этого с помощью метода map() в третьем операторе, а затем заметьте, как мы делаем то же самое с помощью метода flatMap() после этого.

Ссылка на метод Person::getName похожа на вызов String::trim, который мы использовали в предыдущем разделе для очистки пароля.

Единственное отличие заключается в том, что getName() возвращает Optional, а не String, как операция trim(). Это, в сочетании с тем, что трансформация map оборачивает результат в объект Optional, и приводит к появлению вложенного Optional.

Поэтому во время применения метода map() необходимо добавить дополнительный вызов для получения значения перед использованием трансформированного значения. Таким образом, обертка Optional будет удалена. Эта операция выполняется неявно при использовании flatMap.

13. Цепочка Optional в Java 8

Иногда нам может понадобиться получить первый непустой объект Optional из нескольких Optional. В таких случаях было бы очень удобно использовать метод типа orElseOptional(). К сожалению, в Java 8 такая операция напрямую не поддерживается.

Давайте сначала представим несколько методов, которые мы будем использовать на протяжении всего этого раздела:

private Optional<String> getEmpty() {     return Optional.empty(); }  private Optional<String> getHello() {     return Optional.of("hello"); }  private Optional<String> getBye() {     return Optional.of("bye"); }  private Optional<String> createOptional(String input) {     if (input == null || "".equals(input) || "empty".equals(input)) {         return Optional.empty();     }     return Optional.of(input); }

Для того чтобы выстроить цепочку из нескольких объектов Optional и получить первый непустой объект в Java 8, мы можем использовать Stream API:

@Test public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturned() {     Optional<String> found = Stream.of(getEmpty(), getHello(), getBye())       .filter(Optional::isPresent)       .map(Optional::get)       .findFirst();          assertEquals(getHello(), found); }

Недостатком этого подхода является то, что все наши методы get выполняются всегда, независимо от того, где в Stream появляется непустой Optional.

Если мы хотим лениво оценить методы, переданные в Stream.of(), нам нужно использовать ссылку на метод и интерфейс Supplier:

@Test public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturnedAndRestNotEvaluated() {     Optional<String> found =       Stream.<Supplier<Optional<String>>>of(this::getEmpty, this::getHello, this::getBye)         .map(Supplier::get)         .filter(Optional::isPresent)         .map(Optional::get)         .findFirst();      assertEquals(getHello(), found); }

В случае если нам нужно использовать методы, принимающие аргументы, мы должны прибегнуть к лямбда-выражениям:

@Test public void givenTwoOptionalsReturnedByOneArgMethod_whenChaining_thenFirstNonEmptyIsReturned() {     Optional<String> found = Stream.<Supplier<Optional<String>>>of(       () -> createOptional("empty"),       () -> createOptional("hello")     )       .map(Supplier::get)       .filter(Optional::isPresent)       .map(Optional::get)       .findFirst();      assertEquals(createOptional("hello"), found); }

Часто мы хотим вернуть значение по умолчанию в случае, если все цепочки Optional пусты. Мы можем сделать это, просто добавив вызов orElse() или orElseGet():

@Test public void givenTwoEmptyOptionals_whenChaining_thenDefaultIsReturned() {     String found = Stream.<Supplier<Optional<String>>>of(       () -> createOptional("empty"),       () -> createOptional("empty")     )       .map(Supplier::get)       .filter(Optional::isPresent)       .map(Optional::get)       .findFirst()       .orElseGet(() -> "default");      assertEquals("default", found); }

14. Optional API в JDK 9

Релиз Java 9 добавил еще больше новых методов в Optional API:

  • метод or() для предоставления поставщика, который создает альтернативный Optional;

  • ifPresentOrElse() метод, позволяющий выполнить действие, если Optional присутствует, или другое действие, если нет;

  • метод stream() для преобразования Optional в Stream.

Вот полный текст статьи для дальнейшего чтения.

15. Неправильное использование Optional

Наконец, давайте рассмотрим привлекательный, но опасный способ использования Optional: передача параметра Optional методу.

Представьте, что у нас есть список Person, и мы хотим, чтобы метод искал в этом списке людей с заданным именем. Кроме того, желательно, чтобы этот метод находил записи, имеющие по крайней мере определенный возраст, если он указан.

Поскольку этот параметр является опциональным, мы пришли к такому методу:

public static List<Person> search(List<Person> people, String name, Optional<Integer> age) {     // Null checks for people and name     return people.stream()             .filter(p -> p.getName().equals(name))             .filter(p -> p.getAge().get() >= age.orElse(0))             .collect(Collectors.toList()); }

Затем мы публикуем наш метод, и другой разработчик пытается его использовать:

someObject.search(people, "Peter", null);

Теперь разработчик выполняет свой код и получает NullPointerException. Мы вынуждены осуществлять проверку на null нашего опционального параметра, что противоречит нашей первоначальной цели избежать подобной ситуации.

Вот некоторые возможности, которые можно было бы сделать, чтобы справиться с этим лучше:

public static List<Person> search(List<Person> people, String name, Integer age) {     // Null checks for people and name     final Integer ageFilter = age != null ? age : 0;      return people.stream()             .filter(p -> p.getName().equals(name))             .filter(p -> p.getAge().get() >= ageFilter)             .collect(Collectors.toList()); }

Здесь параметр по-прежнему является опциональным, но мы обрабатываем его только в одной проверке.

Другим возможным вариантом является создание двух перегруженных методов:

public static List<Person> search(List<Person> people, String name) {     return doSearch(people, name, 0); }  public static List<Person> search(List<Person> people, String name, int age) {     return doSearch(people, name, age); }  private static List<Person> doSearch(List<Person> people, String name, int age) {     // Null checks for people and name     return people.stream()             .filter(p -> p.getName().equals(name))             .filter(p -> p.getAge().get().intValue() >= age)             .collect(Collectors.toList()); }

Таким образом, мы предлагаем четкий API с двумя методами, делающими разные вещи (хотя они имеют общую имплементацию).

Итак, существуют решения, позволяющие избежать использования Optional в качестве параметров метода. Намерением Java было при выпуске Optional использовать его в качестве возвращаемого типа, таким образом указывая, что метод может вернуть пустое значение. На самом деле, практика использования Optional в качестве параметра метода даже не рекомендуется некоторыми инспекторами кода.

16. Optional и сериализация

Как уже говорилось выше, Optional предназначен для использования в качестве возвращаемого типа. Пытаться использовать его в качестве типа поля не рекомендуется.

Кроме того, использование Optional в сериализуемом классе приведет к возникновению исключения NotSerializableException. В нашей статье Java Optional как возвращаемый тип вопросы сериализации рассматриваются подробнее .

А в статье Использование Optional в Jackson мы объясняем, что происходит при сериализации полей Optional, и также приводим несколько обходных путей для достижения желаемых результатов.

17. Заключение

В этой статье мы охватили большинство важнейших фич класса Optional в Java 8.

Вкратце исследовали некоторые причины, побудившие нас использовать Optional вместо явной проверки на null и валидации ввода.

Также узнали, как получить значение Optional или значение по умолчанию, если оно пустое, с помощью методов get(), orElse() и orElseGet() (и заметили важную разницу между двумя последними).

Затем рассмотрели, как трансформировать или фильтровать наши Optional с помощью map(), flatMap() и filter(). Обсудили, что предлагает текучий API Optional, позволяющий легко соединять различные методы в цепочку.

Наконец, увидели, почему использование Optional в качестве параметров метода является плохой идеей и как этого избежать.

Исходный код всех примеров, приведенных в статье, доступен на GitHub.


Приглашаем все желающих на открытое занятие «Объектно-ориентированное и функциональное программирование», на котором разберем отличия между этими двумя подходами на максимально простом уровне. Примеры рассмотрим на языке Java. Во второй части занятия вас ждет подробное описание особенностей специализации Java-разработчик. Регистрация — по ссылке.


ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/668794/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *