Насколько я понял замысел создателей Java - для хранения данных нужно использовать nullable поля. В параметрах публичных функций нужно принимать not-null значения, null нельзя (в приватных можно). Если хочешь принять null, то тебе надо сделать перегрузку для этой функции без соотв. параметра или спроектировать API по-другому. В возвращаемых значениях публичных функций надо использовать Optional (вместо возврата null). В локальных переменных - я точно не понял, но полагаю, что в общем случае nullable, но если из функции данные получил в виде Optional, то можно так и оставить.
В целом это всё вызвано желанием убрать ненужное создание объектов. Optional это полновесный объект, и на ровном месте его создавать не нужно. Также доступ к значению получается через промежуточную ссылку, это тоже не очень хорошо. С локальными переменными и полями можно и без Optional разобраться, оно как бы всё рядом. С параметрами методов - тоже можно разобраться путем создания доп перегрузок. И только с возвращаемыми значениями так разобраться не получится, поэтому тут Optional это необходимое зло.
В Idea есть предупреждения на Optional в параметрах метода и в поле, так что это я как бы не сам придумал. Ну и в статьях про это писали.
Всё это выливается в подобный код:
class Person {
private final String name;
private final Integer age;
private Person(String name, Integer age) {
this.name = requireNonNull(name);
this.age = age;
}
String name() { return name; }
Optional<Integer> age() { return Optional.ofNullable(age); }
class Builder {
private String name;
private Integer age;
void name(String name) { this.name = requireNonNull(name); }
void age(Integer age) { this.age = requireNonNull(age); }
Person build() { return new Person(name, age); }
}
}
Тут соблюдены все вышеперечисленные «каноны». Если хочешь создать персону без возраста, ты обязан не вызывать метод age в билдере.
С таким подходом получается, что создать копию персоны можно только так:
var personCopyBuilder = new Person.Builder();
personCopyBuilder.name(person.name());
person.age().ifPresent(age -> personCopyBuilder.age(age));
var personCopy = personCopyBuilder.build();
Если взять язык с nullable системой типов вроде Kotlin, то там как-то всё получается проще и естественней…
В частности мне не нравится вот эта сложная передача null, которая в том числе не даёт chain-ить методы билдера.
Как вы думаете - что тут можно улучшить? Или это хороший код и всё ок?
Скажем, один из вариантов это убрать проверку в Builder.age(Integer) на null, и тогда можно будет писать
var personCopy = new Person.Builder()
.name(person.name())
.age(person.age().orElse(null))
.build()
и по сути тут вроде ничего страшного и нет, кроме нарушения правила не принимать nonnull значения в параметрах.