Как стать автором
Обновить

Комментарии 16

А где примеры? Следующие части тоже «сухим текстом» будут?

По теме: «Объект всегда выдается, но не всегда живой» похоже на паттерн NullObject. В принципе самый оптимальный вариант на все времена. ИМХО, единственное преимущество Optional — в методах map()/filter(), что врядли делали, используя упомянутый паттерн.
Примеры начнутся начиная со следующей части, которую я планирую опубликовать в ближайшие дни.
По второму пункту Вашего комментария позволю себе с Вами не согласиться. Надеюсь и Вы измените своё мнение после прочтения следующей статьи серии.
Жду с нетерпением!
imho, Я бы не разбивал эту статью на две и более части, в этой статье вообще ничего не раскрывается интересного. hasX перед getX вообще оверкил :/
Проблема представления и использования обьектов с динамической структурой решается в Java и без использования Optional. Многие проекты по разным причинам до сих пор не могут использовать Java 8/Java 9. В этой статье я попытался систематизировать подходы к решению проблемы именно для этих случаев. Информацию о подходах я почерпнул из литературы и guidlines проектов, в которых участвовал сам. Хотите верьте, хотите нет — но вариант с hasX...getX из реального проекта.
Почему это идея со списком или итератором или массивом так уж плоха(а уж в сравнении с Optional — это ещё два раза посмотреть!)? ПОЧЕМУ именно логически она плоха? На примере кофе-машины — разве она не может выдать НЕСКОЛЬКО порций кофе? И разве она так не делает? Но разберем тезисы подробнее:
Фактически мы принуждаем тем самым пользователей нашего метода на месте, сразу же после вызова функции разобраться со списком. Вряд ли он будет этот список передавать дальше как параметр других методов.

Нет, не принуждаем! Откуда это взято? И список и итератор или массив можно и потом передать в методы и сохранить и по сети передать — всё зависит от задачи.
Недостатком метода является его очевидная вычурность

В ЧЕМ КОНКРЕТНО эта «очевидная вычурность» проявляется? И как тогда проявляется «неочевидная невычурность»? К чему использовать такие «интересные» речевые обороты?
Уж очень сильно начинают различаться ситуации, когда get… метод возвращает элементарное значение, например int, заведомо существующий объект и условно существующий объект.

А тут вообще натягиваем сову на глобус! В коде, видите ли, сильно ситуации различаются с методами? В этом проблема? В том что есть методы которые возвращают РАЗНЫЕ значения? Так по моему они только этим и занимаются — на вход принимают значения, обрабатывают их и выдают результат на выходе. Разные на входе и разные на выходе.
А теперь практика — как это должно выглядеть например с итератором:
interface IMarks {
	Iterator<String> findById(Long ... ids);
	void print(PrintStream ps, Iterator<String> values);
	void printOne(PrintStream ps, String value);
}
public final class Marks implements IMarks{
	Map<Long, String> test = new HashMap<>();
	{
		test.put(1L, "1");
		test.put(2L, "2");
		test.put(3L, "3");
		test.put(4L, "4");
		test.put(5L, "5");
	}	
	@Override
	public Iterator<String> findById(Long... ids) {
		List<Long>idsList = Arrays.asList(ids);
		List<String> list = new ArrayList<String>();
		for(Entry<Long, String>entry : test.entrySet()) {
			if(idsList.contains(entry.getKey())) list.add(entry.getValue());
		}
		return list.iterator();
	}
	public static void main(String[] args) {
		IMarks marks = new Marks();
		Iterator<String>it = marks.findById(2L, 3L, 5L);
		if(it.hasNext()) marks.printOne(System.out, it.next());
		if(it.hasNext()) marks.print(System.out, it);	
	}
	@Override
	public void print(PrintStream ps, Iterator<String> values) {
		while (values.hasNext()) {
			ps.println(values.next());
		}
	}
	@Override
	public void printOne(PrintStream ps, String value) {
		ps.println(value);
	}
}

Смотрите метод main. Сравните сигнатуры методов print и printOne.
Ну а если использовать аналогию с кофе, то будет примерно так:

Iterator<CoffeePortion> coffeePortions() // или
List<CoffeePortion> coffeePortions()

А вишенкой на торте последний вопрос — так зачем нужен Optional, если и так можно делать?
Прежде всего, спасибо Вам за потраченные усилия на обоснования Ваших тезисов.
Попытаюсь представить мои возражения в порядке соответствующем Вшему тексту.
Идея со списком или массивом активно рекомендовалась экспертами как одно из лучших решений проблемы избежания NullPointerException до появления Optional. Но так было за неимением лучшего решения.
Передавать список или массив дальше я не считаю хорошей идеей, поскольку вызывающие клиенты могут не знать, что на самом деле ожидается строго 1 или 0 обьектов в списке.
Про «вычурность». Никак не умоляя Вашего класса в программировании, должен сказать, что именно представленный Вами пример подпадает под моё определение «вычурности». Другими словами, если вспомнить о том, какую задачу (представление обьекта либо его отсутствия) мы решаем, то подобное решение неоправдано сложно для понимания, использования и реализации. Но это моё личное мнение, я Вам его не навязываю.
Зачем нужен Optional, если и так можно сделать. Да, можно. Можно сделать и напрямую на ассемблере или даже на машине Тьюринга. Вопрос в затратах на изготовление, использование и сопровождение.
Надеюсь, следующие статьи серии Вас в этом убедят.
Поверьте, я действительно признателен Вам за Ваш комментарий. Он мне показывает ещё раз, что я не зря затеял написание этого tutorial.
Спасибо Вам за ответ на комментарий.
Да, можно. Можно сделать и напрямую на ассемблере или даже на машине Тьюринга.
Я оценил Ваш тонкий логический ход. Отвечаю в том же ключе: А можно еще ввести новый объект с интерфейсами и абстракциями, самостоятельной проверкой на null или исполнением(да это же паттерн NullObject!). А можно просто все проверки на null написать в отдельном классе и добавить туда методы работы с объектом.( да это же наш Optional!) Это лучшее решение?
Передавать список или массив дальше я не считаю хорошей идеей, поскольку вызывающие клиенты могут не знать, что на самом деле ожидается строго 1 или 0 обьектов в списке.

Здесь проблемы нет — работайте всегда со списком или массивом. Также как работаете в других местах. Такое впечатление что эти клиенты не знают как работать со списком значений полученных из метода! Я же специально привел примеры кода работы со списком.
Но как говорил Торвальдс: “Talk is cheap. Show me the code.”
Посмотрим на Вашу вторую часть с примерами кода. Огромная просьба будет к Вам — приведите пожалуйста примеры когда методы возвращают один обьект и несколько обьектов. Например — тот же поиск обьектов по Id и нескольким Id. Во избежание манипуляций, а также для более объемного и подробного материала для дальнейшего обсуждения.
Договорились. Только рассказа о списке Вам придётся ждать до третьей статьи серии, поскольку соответствующие методы добавлены в Optional только в Java 9.

Не использовать вообще нулевые значения.
У большинства стандартных типов существует некое дефолтное значение, которым можно заменить null. Например для числовых типов это ноль, для String это пустая строка "", для списка — пустой список, etc.
Поля класса сразу инициализируйте дефолтными значениями.


Для собственных типов можно использовать можно искусственно определить нулевое значение и при необходимости сравнивать с ним.


public class MyClass {
  public static final MyClass EMPTY = new MyClass();

  public final String name;
  // Optional field, "" by default
  public final String address;

  protected MyClass() {this("","");}
  public MyClass(@Nonnull String name) {
    this(name,"");
  }
  public MyClass(@Nonnull String name, @Nonnull String address) {
    this.name= name; this.address = address;
  }
}

Возвращается лист, возможно пустой
Проблема в том, что лист mutable. Лучше возвращать Iterator или Stream.
То что Вы предлагаете, фактически является разновидностью варианта «Объект всегда выдается, но не всегда живой». Его можно назвать примерно так: «Объект всегда выдается, но иногда как зародыш».
Признаться, я не понял зачем нужен EMPTY если не имплементируется equals(). И как раз на Вашем примере мы видим, что надо весьма специальным методом отличать нормальный обьект от «зародыша». В другом обьекте при таком подходе надо знать его собственный метод распознования «зародыша». Поэтому и вводят в больших проектах при использовании подобного подхода общий интерфейс с методом типа isActive() или isPresent().
Ну а список можно сделать immutable с помощью Collections.unmodifiableList()

Идея как раз в том, чтобы исключить искусственные методы типа isActive() и isPresent() и соответствующие проверки (они ни чем не лучше проверки на null). Возвращаемый "пустой" объект имеет полностью осмысленное поведение при вызове своих методов, и не требует специальных кейсов в логике программы. Например с пустой сторокой можно делать все те же операции, или например пустой список можно также итерировать в цикле без специальной проверки.


я не понял зачем нужен EMPTY если не имплементируется equals()

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


public class AnotherClass {
    public MyClass myClass = MyClass.EMPTY;
}

Это в случае если класс MyClass immutable. Тогда проверку на empty можно делать простым сравнением "==", но при желании можно добавить и equals()/hashCode(). Хотя как я уже сказал выше, поведение кода не должно отличаться в случае пустого или непустого объекта и не должно содержать искусственных проверок.


Ну а список можно сделать immutable с помощью Collections.unmodifiableList()

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


userService.getUsers().add(new User());

в зависимости от имплементации UserService в одних случаях будет прокатывать, а в других нет.

Вы с помощью частного примера пытаетесь обосновать общий подход. Но Ваш пример весьма искусственный. Это Value-Object и к тому же по определению immutable.
И давайте уточним ещё раз наши позиции. Я не утверждаю, что подход с использованием массива, списка, итератора для для представления объектов с динамической структурой всегда плох. Я утверждаю, что в большинстве реальных случаев он работает хуже, чем использование Optional.
Но доказательство этого тезиса я попытаюсь дать в следующей статье серии.

Плохая идея. Дефолтовое значение зачастую входит в пространство допустимых значений переменной и тогда начинают появляться другие магические дефолтные значения и прочее уродство. Пррстой пример — функция поиска подстроки в строке. 0 — резрешённое значения, будем возвращать при неуспехе -1 в надежде, что у нас нет таких длинных строк — "гениально"! А теперь, чтение байта из файла… Опс, что-то пошло не так… Мы уже не можем вернуть ни 0 ни -1 — это допустимые значения. Что ж, давайте возвращать не байт, а int… "гениально"!

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

Вы правы. Обе статьи я упоминаю во второй части этой серии, которую я как раз дорабатываю. Но с другой стороны, то что написано в этой первой части, в тех статьях (и известных мне других tutorials и обзорах) нет. Так что в этой первой части новизна и отличие от ранее опубликованых статей наличиствует.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории