Java records with nullable components

后端 未结 3 1451
执念已碎
执念已碎 2021-01-06 10:57

I really like the addition of records in Java 14, at least as a preview feature, as it helps to reduce my need to use lombok for simple, immutable "data holders".

相关标签:
3条回答
  • 2021-01-06 11:07

    A record comprises attributes that primarily define its state. The derivation of the accessors, constructors, etc is completely based on this state of the records.

    Now in your example, the state of the attribute value is null, hence the access using the default implementation ends up providing the true state. To provide customized access to this attribute you are instead looking for an overridden API that wraps the actual state and further provides an Optional return type.

    Ofcourse as you mentioned one of the ways to deal with it would be to have a custom implementation included in the record definition itself

    record MyClass(String id, String value) {
        
        Optional<String> getValue() {
            return Optional.ofNullable(value());
        }
    }
    

    Alternatively, you could decouple the read and write APIs from the data carrier in a separate class and pass on the record instance to them for custom accesses.

    The most relevant quote from JEP 384: Records that I found would be(formatting mine):

    A record declares its state -- the group of variables -- and commits to an API that matches that state. This means that records give up a freedom that classes usually enjoy -- the ability to decouple a class's API from its internal representation -- but in return, records become significantly more concise.

    0 讨论(0)
  • 2021-01-06 11:16

    Credits go to Holger! I really like his proposed way of questioning the actual need of null. Thus with a short example, I wanted to give his approach a bit more space, even if a bit convoluted for this use-case.

    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);
        }
    }
    

    Usage would be something like

    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);
    

    which leads to the following output...

    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'.]
    

    The only minor issue is that the toString prints now the actual... variants. And of course we do not NEED to use records for this.

    0 讨论(0)
  • 2021-01-06 11:17

    Don't have the rep to comment, but I just wanted to point out that you've essentially reinvented the Either datatype. https://hackage.haskell.org/package/base-4.14.0.0/docs/Data-Either.html or https://www.scala-lang.org/api/2.9.3/scala/Either.html. I find Try, Either, and Validation to be incredibly useful for parsing and there are a few java libraries with this functionality that I use: https://github.com/aol/cyclops/tree/master/cyclops and https://www.vavr.io/vavr-docs/#_either.

    Unfortunately, I think your main question is still open (and I'd be interested in finding an answer).

    doing something like

    RecordA(String a)
    RecordAandB(String a, Integer b)
    

    to deal with an immutable data carrier with a null b seems bad, but wrapping recordA(String a, Integer b) to have an Optional getB somewhere else seems contra-productive. There's almost no point to the record class then and I think the lombok @Value is still the best answer. I'm just concerned that it won't play well with deconstruction for pattern matching.

    0 讨论(0)
提交回复
热议问题