Записи 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); 
 }
}

Поскольку я думал об использовании Optionals, поскольку теперь предпочтение отдается возвращаемым типам, мне действительно интересно, почему это ограничение действует. Я неправильно понимаю использование? Как я могу добиться того же, не добавляя еще один метод доступа с другой подписью, которая не скрывает подпись по умолчанию? Должен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 по-прежнему является лучшим ответом. Я просто обеспокоен тем, что это не будет хорошо работать с деконструкцией для сопоставления с образцом.

Другие вопросы по тегам