Comment by layer8

14 hours ago

> But careful: == looks at internal state, which isn’t always what the object represents, so for “is this the same data” comparisons keep using equals.

So == for value classes will basically be like memcmp(). That is a bit unfortunate, as it breaks encapsulation, exposing implementation details. Client code can use this to do case distinctions based on how a given value is internally represented. In a way, it’s worse than identity comparison, because identity comparison at least doesn’t expose internal state.

Value types are a concept very far away from the "magic black box organism" school of OOP thinking. It's not a novel way of doing classic OOP (does anyone still do that?), it's a way for a language born in OOP ideology get one step further into the post-OOP world.

  • That’s just not true, you can have a completely value-based language without OOP that still doesn’t leak implementation details of the values, while also supporting UDTs.

    • OOP isn't just about values vs objects. Yes, the idea that everything needs identity is a big part of the problem. But another big problem is the idea that the implementation and representation of types should be hidden by default. The mindset that there isn't a known and useful data representation for a given type. That everything is done by methods parameterized by a type. It's a misguided idea. There is a place for objects and implementation hiding. But the idea that this should be done on a type granularity is a complete and utter failure.

      To see why, consider that to do any useful work, data from different objects (also from different types) has to be combined. To be able to do that in the OOP framework, the encapsulation has to be unwrapped. That's why Java code is littered with getters and setters that don't do any useful work at all, they just make it too painful to get any real work done.

      Again, there is a place for objects and implementation hiding, but it's at the highest levels of an architecture where different components get integrated.

      4 replies →

  • Not if you do DDD where a calue type has exactly those semantics and for record types this is actually a free lunch.

If your bags of data have internal state, there's something wrong with your bags of data. I assume that the Java guys thought far enough to either exclude padding from comparisons or force padding bytes to be zero.

It should work even for strings: They will surely continue to be heap-allocated, and memcmp-ing pointers (inside the new "structs") is exactly an identity comparison.

  • There’s nothing wrong with having non-normalized representations, that’s why there is equals().

    For example, you might have a value class for representing (limited-precision) fractions using two longs internally, for the numerator and denominator. For efficiency trade-off reasons, you don’t want to always shorten the fraction. But now client code can distinguish 2/3 from 4/6 using ==.

    Scenarios of that sort are conceivable where this actually leaks sensitive information. In any case, it creates dependencies on implementation details where you don’t want to have them.

    When designing a value class, you are now in the dilemma of either always having to normalize the representation, costing performance, or having your class be a funnel for leaking implementation details.

    • Well. I'd be upset if custom operator==() for plain-old-data structs was removed from C++, but Java never had it to begin with, so for Java, it just means that you have to fall back to using traditional classes (or compare using something other than ==) if you need such "fancy" features.

    • Java can also distinguish a 2/3 object from a 4/6 object using == when they are not value types. It can even distinguish a 2/3 object from a different 2/3 object.

      1 reply →

    • > There’s nothing wrong with having non-normalized representations

      There is a lot wrong with that: complexity, bloat, and slowness.

      > But now client code can distinguish 2/3 from 4/6 using ==

      That's a great way to obfuscate code. Not a good idea. The right way to do the comparison is, just make a function called CompareRational().

the whole point of value class is that they should not encapsulate state, i.e. its a totally transparent data holder

I wanted to comment on this as well. The article mentions it but if you've never used Java in anger (is there any other way?) then readers may not understand the true implications of this because it's a breaking change, something Java rarely does. I'll explain for the non-Java people.

Java separates checking identity and equality for objects. == basically checks if two pointers are the same. Equality is a subjective concept based on an interface (ie equals/hashCode). So this means:

    new Integer(1000) == new Integer(1000) // true, used to be false
    new Integer(1000).equals(new Integer(1000)) // true
    new Integer(10) == new Long(10) // compiler error, used to false
    new Integer(10) == new Integer(10) // true

There's a lot going on here. The complication is that in previous versions of Java (and I'm not sure when this changed), integers below a certain value would be replaced with canonical types below a certain value. I think it was 128 but its's been awhile. This led to the difference between 10 and 1000. That's now changed, I suspect because the above comparisons are being implicitly unboxed. That didn't used to happen either. I saw this because the Integer/Long comparison used to return false and it's now a compiler error so there must be unboxing going on.

You may still be able to get the old behavior through variables too.

Anyway, if value classes lose identity then == changes from pointer equality to bitwise equality. That will hopefully resolve a bunch of corner cases like this but it is a breaking change, technically.