Записи Java с компонентами, допускающими значение NULL
Мне очень нравится добавление записей в Java 14, по крайней мере, в качестве функции предварительного просмотра, так как это помогает уменьшить мою потребность в использовании ломбока для простых, неизменных "держателей данных". Но у меня проблема с реализацией компонентов, допускающих значение NULL. Я пытаюсь не возвращатьсяnull
в моей кодовой базе, чтобы указать, что значение может отсутствовать. Поэтому в настоящее время я часто использую что-то вроде следующего шаблона с ломбоком.
@Value
public class MyClass {
String id;
@Nullable String value;
Optional<String> getValue() { // overwrite the generated getter
return Optional.ofNullable(this.value);
}
}
Когда я сейчас пробую тот же образец с записями, это не разрешается указывать incorrect component accessor return type
.
record MyRecord (String id, @Nullable String value){
Optional<String> value(){
return Optional.ofNullable(this.value);
}
}
Поскольку я думал об использовании Optional
s, поскольку теперь предпочтение отдается возвращаемым типам, мне действительно интересно, почему это ограничение действует. Я неправильно понимаю использование? Как я могу добиться того же, не добавляя еще один метод доступа с другой подписью, которая не скрывает подпись по умолчанию? ДолженOptional
вообще не использовать в этом случае?
4 ответа
А record
содержит атрибуты, которые в первую очередь определяют его состояние. Получение средств доступа, конструкторов и т.д. полностью основано на этом состоянии записей.
Теперь в вашем примере состояние атрибута value
является null
, следовательно, доступ с использованием реализации по умолчанию в конечном итоге обеспечивает истинное состояние. Чтобы предоставить индивидуальный доступ к этому атрибуту, вы вместо этого ищете переопределенный API, который обертывает фактическое состояние и дополнительно предоставляетOptional
тип возврата.
Конечно, как вы упомянули, одним из способов справиться с этим было бы включение настраиваемой реализации в само определение записи.
record MyClass(String id, String value) {
Optional<String> getValue() {
return Optional.ofNullable(value());
}
}
В качестве альтернативы вы можете отделить API чтения и записи от носителя данных в отдельном классе и передать им экземпляр записи для настраиваемого доступа.
Самая уместная цитата из JEP 384: Записи, которые я нашел, будут (форматирование моего):
Запись объявляет свое состояние - группу переменных - и фиксируется API, который соответствует этому состоянию. Это означает, что записи отказываются от свободы, которой обычно пользуются классы - способности отделять API класса от его внутреннего представления - но, в свою очередь, записи становятся значительно более краткими.
Из-за ограничений, наложенных на записи, а именно, что канонический тип конструктора должен соответствовать типу доступа, прагматичный способ использования
Optional
с записями можно было бы определить как тип свойства:
record MyRecord (String id, Optional<String> value){
}
Было отмечено, что это проблематично из-за того, что null может быть передан конструктору в качестве значения. Это можно решить, запретив такие
MyRecord
инварианты через канонический конструктор:
record MyRecord(String id, Optional<String> value) {
MyRecord(String id, Optional<String> value) {
this.id = id;
this.value = Objects.requireNonNull(value);
}
}
На практике наиболее распространенные библиотеки или фреймворки (например, Jackson, Spring) поддерживают распознавание необязательного типа и перевод null в
Optional.empty()
автоматически, поэтому необходимость решения этой проблемы в вашем конкретном случае зависит от контекста. Я рекомендую изучить поддержку Optional в вашей кодовой базе, прежде чем загромождать ваш код, возможно, ненужным.
Кредиты идут Holger! Мне действительно нравится его предложенный способ поставить под сомнение действительную потребность вnull
. Таким образом, с помощью короткого примера я хотел дать его подходу немного больше места, даже если он немного запутан для этого варианта использования.
interface ConversionResult<T> {
String raw();
default Optional<T> value(){
return Optional.empty();
}
default Optional<String> error(){
return Optional.empty();
}
default void ifOk(Consumer<T> okAction) {
value().ifPresent(okAction);
}
default void okOrError(Consumer<T> okAction, Consumer<String> errorAction){
value().ifPresent(okAction);
error().ifPresent(errorAction);
}
static ConversionResult<LocalDate> ofDate(String raw, String pattern){
try {
var value = LocalDate.parse(raw, DateTimeFormatter.ofPattern(pattern));
return new Ok<>(raw, value);
} catch (Exception e){
var error = String.format("Invalid date value '%s'. Expected pattern '%s'.", raw, pattern);
return new Error<>(raw, error);
}
}
// more conversion operations
}
record Ok<T>(String raw, T actualValue) implements ConversionResult<T> {
public Optional<T> value(){
return Optional.of(actualValue);
}
}
record Error<T>(String raw, String actualError) implements ConversionResult<T> {
public Optional<String> error(){
return Optional.of(actualError);
}
}
Использование будет примерно таким
var okConv = ConversionResult.ofDate("12.03.2020", "dd.MM.yyyy");
okConv.okOrError(
v -> System.out.println("SUCCESS: "+v),
e -> System.err.println("FAILURE: "+e)
);
System.out.println(okConv);
System.out.println();
var failedConv = ConversionResult.ofDate("12.03.2020", "yyyy-MM-dd");
failedConv.okOrError(
v -> System.out.println("SUCCESS: "+v),
e -> System.err.println("FAILURE: "+e)
);
System.out.println(failedConv);
что приводит к следующему выводу...
SUCCESS: 2020-03-12
Ok[raw=12.03.2020, actualValue=2020-03-12]
FAILURE: Invalid date value '12.03.2020'. Expected pattern 'yyyy-MM-dd'.
Error[raw=12.03.2020, actualError=Invalid date value '12.03.2020'. Expected pattern 'yyyy-MM-dd'.]
Единственная незначительная проблема заключается в том, что toString
печатает сейчас actual...
варианты. И, конечно, нам НЕ НУЖНО использовать для этого записи.
У меня нет представителя для комментариев, но я просто хотел указать, что вы по сути заново изобрели тип данных Either. https://hackage.haskell.org/package/base-4.14.0.0/docs/Data-Either.html или https://www.scala-lang.org/api/2.9.3/scala/Either.html. Я считаю, что Try, Either и Validation невероятно полезны для синтаксического анализа, и я использую несколько java-библиотек с этой функциональностью: https://github.com/aol/cyclops/tree/master/cyclops и https://www.vavr.io/vavr-docs/.
К сожалению, я думаю, что ваш главный вопрос все еще открыт (и мне было бы интересно найти ответ).
делать что-то вроде
RecordA(String a)
RecordAandB(String a, Integer b)
иметь дело с неизменяемым носителем данных с нулевым b кажется плохим, но обертывание recordA(String a, Integer b), чтобы иметь дополнительный getB где-то еще, кажется контрпродуктивным. Тогда в классе записи почти нет смысла, и я думаю, что ломбок @Value по-прежнему является лучшим ответом. Я просто обеспокоен тем, что это не будет хорошо работать с деконструкцией для сопоставления с образцом.