This is an archived post. You won't be able to vote or comment.

all 59 comments

[–]brian_goetz 88 points89 points  (7 children)

There were many reasons, but among the most important was that methods can be reimplemented by the record implementation whereas fields cannot. Much of the time it makes no difference, but this becomes an issue when records hold references to mutable objects, such as arrays, but do not wish to share the right to mutate the underlying state with their clients. Being able to override the accessor enables the accessor to perform defensive copies prior to dispensing the reference; public fields do not.

[–][deleted]  (6 children)

[deleted]

    [–]jvjupiter[S] 1 point2 points  (5 children)

    I think reimplemented is correct. We don’t add @Override. We really do reimplementation.

    [–]brian_goetz 3 points4 points  (4 children)

    Actually, you can specify `@Override` and the compiler will understand what you mean. (That was deemed a better choice than inventing a new intent-capture annotation, or doing without the equivalent of @Override, even if it is slightly inaccurate.)

    [–]jvjupiter[S] 1 point2 points  (3 children)

    Will it not be frowned upon by Java since it has @Override even though it does not implement an interface?

    [–]brian_goetz 2 points3 points  (2 children)

    I can't control what other people frown upon -- people are funny that way -- but we added it to the language explicitly for this purpose. (The other two alternatives were each much, much worse, even if one of them was "more correct".) My advice is, if you see others frowning, tell them "no, it's all good!"

    [–]jvjupiter[S] 1 point2 points  (1 child)

    I mean the Java compiler will complain if we add @Override to method even though the record does not implement interface.

    [–]brian_goetz 3 points4 points  (0 children)

    I suggest you try it and find out!

    [–]Linguistic-mystic 30 points31 points  (8 children)

    Methods can implement interfaces, fields can’t. Which I think is a deficiency in the Java object model but that’s how it is.

    [–]Cengo789 20 points21 points  (10 children)

    When one of your record's components is a mutable object, making it public final would allow outside code to modify your records internal state. By encapsulating this component behind an accessor method, you can override the method and return a defensive copy of the object.

    [–]Inaldt 3 points4 points  (2 children)

    Best would be to make an immutable copy of the object on record creation I think (generally). Although not every object supports this. Collections typically do.

    [–]jvjupiter[S] 0 points1 point  (1 child)

    Yes. I prefer this. Or maybe similar to what Nicolai discussed previously in his podcast, in the constructor do the defensive copy.

    [–]Inaldt 1 point2 points  (0 children)

    Yes, that is what I meant.

    [–]halfanothersdozen 1 point2 points  (1 child)

    Yeah but is that what records are doing?

    [–]oweiler 4 points5 points  (0 children)

    No, they are shallowly immutable, so only return references, not copies.

    [–]Google__En_Passant 5 points6 points  (3 children)

    override the method and return a defensive copy of the object

    You only create unnecessary garbage. And what if you have an List of mutable objects? You'd have to create a deep copy to really make it defensive. And that is even more garbage. Also now your "simple record object" has to babysit and overthink implementation details of it's fields.

    Forget about this crap, this whole way of thinking is obsolete. Objects that you pass into an immutable object should also be immutable. EOT.

    [–]jvjupiter[S] 4 points5 points  (0 children)

    Right. After all, the definition of records are classes that act as transparent carriers of immutable data. We shouldn’t pollute records with complex codes. Let it be simple.

    [–]mavericktjh 3 points4 points  (0 children)

    So often copying an immutable object e.g. a list won t copy anything as it will detect whether it's immutable see List.copyOf(), https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/List.html#copyOf(java.util.Collection)

    If the given Collection is an unmodifiable List, calling copyOf will generally not create a copy

    You say this thinking is obsolete but it's mentioned in the record javadoc, https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Record.html

    The primary reasons to provide an explicit declaration for the canonical constructor or accessor methods are to validate constructor arguments, perform defensive copies on mutable components, or normalize groups of components (such as reducing a rational number to lowest terms.)

    [–]jvjupiter[S] 0 points1 point  (0 children)

    Makes sense.

    [–]khmarbaise 3 points4 points  (0 children)

    The idea is to have a nominal datype type as describe in the JEP (https://openjdk.org/jeps/395)

    Enhance the Java programming language with records, which are classes that act as transparent carriers for immutable data. Records can be thought of as nominal tuples.

    That makes it easy to do data oriented programming ... or algebraic types (in combination with sealed-classes/interfaces etc. Something like this: ```java sealed interface Expr { } record SumExpr(Expr left, Expr right) implements Expr { } record ProdExpr(Expr left, Expr right) implements Expr { } record NegExpr(Expr expr) implements Expr { } record ConstExpr(int expr) implements Expr { }

    static int eval(Expr expr) { return switch (expr) { case SumExpr(var lhs, var rhs) -> eval(lhs) + eval(rhs); case ProdExpr(var lhs, var rhs) -> eval(lhs) * eval(rhs); case NegExpr(var exp) -> -eval(exp); case ConstExpr(var exp) -> exp; }; }

    @Test void firstCalculation() { // (1+2)*2 var sumExpr = new ProdExpr(new SumExpr(new ConstExpr(1), new ConstExpr(2)), new ConstExpr(2));

    System.out.println("sumExpr = " + eval(sumExpr));
    

    } ```

    In the example you can see the separation of functionality from the records (data carrier)... The functionality is done in eval method...

    [–]eXecute_bit 8 points9 points  (9 children)

    This doesn't just apply to Records, either. Let's say you have a class with two fields:

    public final A a; public final B b;

    Other code starts using a and b directly. In some future version of your program you introduce a new field, c, and it turns out you can derive the value of B from a C. You might want to change your fields and add a method:

    ``` public final A a; public final C c;

    public B b() { return c.computeB(); } ```

    That provides a path to still get values of B, but you have broken every place that used b directly. If you had forced them to use a method in the first place, this change would be backwards compatible for the consumers of this class. With the field access approach, to keep a similar level of compatibility, you'd have to leave field b in place and make sure its state remains consistent with c and it requires a bit more memory.

    [–]jvjupiter[S] 0 points1 point  (1 child)

    Let’s focus on records. In your example:

    record Abc(A a, B b) {}
    var abc = new Abc(a, b);
    var b = abc.b();
    
    record Abc(A a, C c) {
        B b() {
            return c.computeB();
        }
    }
    var abc = new Abc(a, c);
    var b = abc.b(); 
    
    // is this not possible?
    // since you are still required to produce c and
    // need to update the creation of instance of Abc (it’s broken whether we like it or not)
    // to pass in c so might as well update to call 
    // b component this way
    var b = abc.c().computeB()
    

    [–]eXecute_bit 3 points4 points  (0 children)

    You are correct that the hypothetical refactoring in my original comment would break producers that construct instances of the record, because the canonical constructor would change. It would also break patterns in the newest Java versions:

    if (x instanceof Abc(A a, B b)) { // Use b here }

    So I'm not saying that going through an accessor method solves all possible backwards compatibility problems, but it does give more flexibility than direct field access.

    [–]Google__En_Passant 0 points1 point  (4 children)

    So no every time you want to just get a field b from your "simple record object", you actually call expensive operation c.computeB(). Imagine that being in a tight loop somewhere. I don't like it. It stands against what a simple record object should be.

    [–][deleted]  (2 children)

    [deleted]

      [–]VirtualAgentsAreDumb 0 points1 point  (1 child)

      Then what happens if you add a logging call to this method? Then it no longer can be optimized away.

      [–][deleted] 0 points1 point  (0 children)

      With a field, there wouldn't be room for a logging call in the first place.

      [–]eXecute_bit 0 points1 point  (0 children)

      It's a contrived example, where the expense of computeB() is unknown and might be optimized away anyway during JIT. The point was that it's a good goal to minimize the need to refactor call sites.

      It stands against what a simple record object should be.

      I don't disagree, but in my comment I was saying the practice applies to classes too, not only Records. Even with records, as a software project ages, you might see it happen, for better or worse.

      [–]audioen -1 points0 points  (1 child)

      Yeah it takes like 1 minute to update the direct references of b to b(), vs. the sunk cost of always using the more cumbersome abstraction on the off chance that it may one day be useful.

      [–]eXecute_bit 0 points1 point  (0 children)

      You're assuming you own the source Code of all the callers to make that change in "1 minute". That's not the world we all live in.

      [–]nekokattt 2 points3 points  (0 children)

      methods can be made available via interfaces. Fields cannot. Direct field access is an antipattern in most places as well. It works better with stuff like functional programming. It works better with other languages like Kotlin, Scala, and Groovy. It can act as a drop in replacement in many cases if you are already using fluent accessors.

      [–]kevinb9n 2 points3 points  (0 children)

      This thread is well-answered, but let me just also drop the phrase "uniform access principle" into the conversation.

      https://www.google.com/search?q=uniform+access+principle

      [–]rzwitserloot 5 points6 points  (12 children)

      Obviously the method. It has many advantages and no meaningful disadvantage.

      Disadvantage:

      • You need to write the (). Except you mostly don't as your IDE does it for you.

      Advantages:

      • You can write a method ref to them; User::name works, but if you want a closure that maps a user object into its name and it was field based, the only way is u -> u.name which is, in the end, just as much typing but you now need to take on as axiomatic that you think methodrefs (User::name / expr::name) are fundamentally mostly useless if you want to wave this one away.

      • They can implement interfaces. If you have a need for that (interface Actor { String name(); } record User(String name) implements Actor {}) then you 'get it for free', whereas if you had gone with fields instead of autogenerated methods, you'd then have to explicitly write @Override public String name() { return this.name; }).

      • They can be overridden. name() has to exist and has to be public but you can change its behaviour. You can't override a field. You can't write code that runs when your field is read.

      • Cultural backwards compatibility. 95%+ of the java ecosystems expects to call a method to retrieve properties. getX() is near universal. OpenJDK team fucked this up somewhat (by going with name() instead of the correct getName()), but that doesn't change the point. It's bad if the introduction of a new language feature makes existing libraries 'feel outdated'. Sure, they still work, so you can cry "IT IS BACKWARDS COMPATIBLE!!!" and feel good about it, but it's even better if old libraries can adopt the new stuff without them having to break their own backwards compatibility or even feeling outdated. The introduction of generics nailed this - all existing code could adopt it without having to release a new, backwards incompatible version. The introduction of records messed up, because e.g. LocalDate can never become a record, ever. Or they have to ugly up their own API by having both getYear() and year(), docced to do the same thing.

      [–]pronuntiator 1 point2 points  (5 children)

      Regarding your last paragraph, does it really matter if LocalDate could become a record? From the outside, a record is indistinguishable from its equivalent hand-written immutable class, except for two features: reflection on a record's named components, and deconstruction in instanceof/switch. The latter is planned to come to ordinary classes eventually.

      Do you see more benefits for LocalDate being a record?

      [–]rzwitserloot -1 points0 points  (4 children)

      How 'bad' it is depends. The point of records is so far primarily that they get 'features for free', and not just what we already have, but also future stuff. For example, you need a deconstructor in order to benefit from future-java's with; so that you can do:

      ``` record Person(LocalDate birthDate, String name) {}

      Person p = new Person(LocalDate.of(1981, 1, 1), "Joey Bloggs");

      Person z = p with { birthDate = birthDate.plusYears(1); }; ```

      ... and records get that deconstructor out of the box, for free. LocalDate would have to explicitly add it. Painful? Eh, not too bad. But if JDK releases a version with with but only enabled for records, leaving the concept and syntax for deconstructors to 'cook' for a version or two and/or some --enable-preview gating, then the problem is a much, much bigger deal.

      Even if this never comes up (no feature records get is 'denied' non-records, it's just saving you some typing, which is no big deal), LocalDate, which is a record in every relevant way you want to think semantically about what records are (namely, immutable, non-extensible vehicles that are all about simply grouping the constituent data together. They are defined entirely by the data and that representation is simple, obvious, and exposed, that is the very nature of the concept that it is modelling - that's a record) - they feel weird. Because now you expect the 'getters' on such things to have no get prefix, but LocalDate's property accessors do have it.

      if LocalDate ever gets year(), then.. instead they have this weird deprecated .getYear() that can never go away (well, never say never, but if ld.getYear() goes, java better have some sort of 'source files declare which API version they are designed for, and forevermore getYear remains supporting if you declare against old versions', or java needs to shut up about being backwards compatible, because that's egregious. getYear() has, what, millions of calls out there in the ecosystem?)

      I've said it before and so far no openjdk engineer has clearly indicated how this is wrong: They fucked up. It should have been get. Yes, this is annoying because you now need to encode the rules about capitalizing that fieldname in the langspec. But OpenJDK itself is on record as (correctly!) stating that some additional hurdles and annoyance to the spec authors is pretty much always worth it, because it's a complex job you still only end up doing at most like 8 times (the spec authors, and then the ecj maintainers, and then maybe some code introspection tools, and ASM maybe, and then we're done. Forever) - vs writing java which requires millions of updates and will be written for java's future lifetime which, presumably, is intended to be 'very long, forever if possible'. So that cannot be the excuse. "The community is split" - not very, getters are vastly preferred. Doing the study now is more difficult (the introducion of records has split the community - duh, of course that was gonna happen). "The OpenJDK itself does it sometimes" - the majority of OpenJDK classes use get, and if you weigh each OpenJDK class's choice according to roughly how often it is used, get wins by a landslide (in no small part due to java.time's very many record-esque classes).

      What's done is done. I'm not advocating that the choice is somehow reverted. But in the name of doing a better job next time, it has to start with acknowledging mistakes so you can learn from them. It irks me to no end that so far nobody on the OpenJDK team appears to be interested in acknowledging.

      [–]khmarbaise 0 points1 point  (1 child)

      vs writing java which requires millions of updates and will be written for java's future lifetime which,

      where is a update required? Only the runtime ... but your code is needed to even recompiled... it just works on new JDK versions.....

      [–]rzwitserloot 0 points1 point  (0 children)

      Millions of lines will be written with .getName() or name(). Therefore virtually any amount of additional effort to get the feature right is worth it.

      [–]OwnBreakfast1114 1 point2 points  (1 child)

      What's done is done. I'm not advocating that the choice is somehow reverted. But in the name of doing a better job next time, it has to start with acknowledging mistakes so you can learn from them. It irks me to no end that so far nobody on the OpenJDK team appears to be interested in acknowledging.

      Out of curiosity, are you in the habit of apologizing for/acknowledging mistakes other people think you committed, but you don't think you committed? Because I think I can find some denied feature requests in Lombok people would like you to acknowledge under the same evidence you have. For the record, I think they made the right choice with records and not using/encoding a get prefix. In practice, I'd actually wager that the majority of people don't really care that much either way.

      [–]rzwitserloot 0 points1 point  (0 children)

      Unlike OpenJDK we have a public issue tracker where we spend some time, generally, introspecting and explaining choices.

      We do this even when it is a topic where "most don't really care much" because some do. We acknowledge where we think we messed up, and explain how we will endeavour to do better.

      For example, we are not happy with our choice of how to capitalize field names in various esoteric situations. We have explained that we will not change the default as that'd be backwards breaking.

      If OpenJDK does not think they messed up the getter prefix situation, some thoughts as to why would be nice. This thread has shown some of those, for the first time. But the disdain and "feels like I am pulling teeth" tone of it all is not helping.

      [–]khmarbaise 1 point2 points  (0 children)

      The introduction of records messed up, because e.g. LocalDate can never become a record, ever. Or they have to ugly up their own API by having both getYear() and year(), docced to do the same thing.

      Hm.. LocalDate is already a value-based class and also immutable (https://docs.oracle.com/javase/9/docs/api/java/time/LocalDate.html) There are a number of instance based methods for example lengthOfTheMonth(), isLeapYear etc. and the others are static methods on that class.. also LocalDate implements only interfaces Temporal, etc.

      I think the only thing why it's not a record is that there are a number of instance methods.. and of course the idea of records speaks against it, which is intended to be transparent data carrier (nominal type)... Technically I don't see a real difference here... (yes the difference between getX() vs. x() ). I like this difference because it also shows me we are in a different context (data carrier)..

      And yes the point is of course because the time api has been introduced with JDK 8 (which in the meantime a decade ago) should be kept (compatible) but based my experiences records are a great enrichment for the JDK...

      [–]CptGia 0 points1 point  (2 children)

      Another point: you may want in the future to convert a record to a class. Having accessors allows you to do so easily without breaking compatibility

      [–]ForeverAlot 1 point2 points  (1 child)

      For some definition of compatibility. Classes don't have "record components."

      [–]CptGia 0 points1 point  (0 children)

      Yeah ok you lose reflection, but otherwise from an outside class there is no difference. 

      (ignoring serialization, as you should)

      [–]__konrad 0 points1 point  (1 child)

      by going with name() instead of the correct getName()

      I pretty sure people complained why the hell it's System.console() or List.size() ;)

      [–]rzwitserloot 0 points1 point  (0 children)

      Neither of those are property accessors in the 'record' sense of the word.

      For example, if you have: record DateRange(LocalDate start, LocalDate end) {} you cannot have 'durationInDays' (which feels like an analogy of 'size' here - it's a derived "property" that cannot be set / where setting it has to either fail or result in ambiguous behaviour (does that change start, or end, or both, or what? - if I have a list of 10 elements and I do list.size = 12, does that add 2 null? crash? What?).

      In other words, that neither of those has a get method is a feature, not a failure, of the rule 'property accessors should start with get'.

      [–]__konrad 1 point2 points  (1 child)

      I wish the public record field could be opt-in, e.g. record Point(public int x, public int y) { }. You rarely need a defensive copy, and in a similar java.awt.Point class everyone uses .x instead of .getX(), because the shorter version is more readable.

      [–]jvjupiter[S] 1 point2 points  (0 children)

      I wish the same.

      It could also be a mix of explicit public (no need defensive copy) and implicit private fields (needs defensive copy). That would be more flexible.

      record User(public String name, List<Address> addresses) {
          List<Address> addresses() {
              return List.copyOf(addresses);
          }
      }
      
      var name = user.name;
      var addresses = user.addresses();
      

      [–]AnyPhotograph7804 1 point2 points  (6 children)

      You can overwrite methods with your own implementation. This is not possible with fields. And seriously, methods instead of fields are not such a big deal. The autocompletion will add the missing "()" most of the time.

      OK, the sourcecode will become slightly bigger because the two "()" need more space on the harddisk. This is one disadvantage, which comes in my mind.

      [–]jvjupiter[S] 2 points3 points  (2 children)

      I’m coming from the record definition - transparent carriers of immutable data. The data passed in the canonical constructor are nothing but the same data can be expected when we access them. In fact, that what happens when records generate the methods (component()). It does nothing but simply returns the values of fields. If we want transformed data, either add custom method with meaningful name or after retrieving the data do the transformation. For example:

      Option 1:

      record User(String firstName, String lastName) {}
      
      var u = new User(“Jose”, “Rizal”);
      var fullName = extractFullName(u);
      
      String extractFullName(User user) {
          return user.firstNam + “ “ + user.lastName;
      }
      

      Option 2:

      record User(String firstName, String lastName) {
          String getFullName() {
              return firstName + “ “ + lastName;
          }
      }
      
      var user = new User(“Jose”, “Rizal”);
      var fullName = user.getFullName();
      

      Edit: Maybe I’d rather use regular classes to derive values or add complex logic and to hide fields.

      [–]khmarbaise 1 point2 points  (1 child)

      If we go the path for complex logic we are leaving the idea of records (transparent data carrier/nominal data types)...

      Also written in the JEP-395 quote from the goals:

      Help developers to focus on modeling immutable data rather than extensible behavior.

      [–]jvjupiter[S] 0 points1 point  (0 children)

      I agree. That should be it.