Hi,
I think that, to avoid confusion, there should be no special treatment
of arrays. They should be treated as any other reference. Why?
Reason #1:
Suppose we have:
value U {
int[] array;
U(int[] array) {
this.array = array;
}
}
value V {
Object obj;
V(Object obj) {
this.obj = obj;
}
}
int[] a1 = new int[] { ... };
int[] a2 = new int[] { ... };
U u1 = U(a1);
U u2 = U(a2);
V v1 = V(a1);
V v2 = V(a2);
I think that it would be very confusing for the following to no hold for
any a1 and a2:
assert (u1 == u2) == (v1 == v2);
assert u1.equals(u2) == v1.equals(v2);
If you want to support above assertions and still treat arrays
differently from non-array references, then a runtime dispatch dependent
on a runtime reference type will be needed. If you do that then you have
specified an operation performed on components of a value that is not
expressible in language with two locals holding the values of the those
component(s) short of a library function call.
Reason #2:
It's not difficult to create a standard reusable value type wrapping a
single array with suitable equals() override:
value Array<any T> {
private T[] array;
Array(int length) {
array = new T[length];
}
@Override public boolean equals() {
...
}
}
... and use it instead of plain array(s) as components of other value
or reference types, with no runtime overhead compared to plain arrays.
For this to work then default .equals() for value types should:
- compare primitive components with ==(p, p) (modulo float/double
variations for which I don't have an opinion yet)
- compare reference components with .equals()
- compare value components with .equals() - yes, this gives the most
intuitive result and encapsulates equality into the classes of the
components.
For == operator on value types then I don't know. Perhaps it should be
the "bit-wise" comparison of components or, a John would say,
copy-equivalence.
That's simple to grasp and explain and is also usable.
Regards, Peter
On 05/17/2016 04:56 PM, Brian Goetz wrote:
So, its worth pointing out here that the only disagreement is on a
pretty cornery (and fundamentally messy) case -- arrays as components
of a value.
I think there's no dispute over:
- primitives components compared with ==(p,p)
- value components compared recursively with ==(v,v)
- non-array reference components compared recursively with .equals()
So the only remaining question is what to do with arrays (and, for
equals vs deepEquals, these differ only when arrays are nested within
arrays within values -- very much a corner case). Options include:
- reference equality / Object.equals()
- Arrays.equals on primitive / value arrays, but reference equality
on object arrays
- Arrays.equals
- Arrays.deepEquals
The first says "are they the same array". The second lifts equality
onto components known to be immutable, but punts on mutable array
components. The third lifts .equals() equality onto
potentially-mutable array components; the third goes farther and
recursively lifts this definition of equality onto elements that are
actually arrays (which must be done reflectively -- see the nasty code
below which is called on each element.)
In what cases will you have an array nested in an array as a value
component? I'm having a hard time thinking of any -- can you dig up
examples?
The cases where I'd have an ordinary array in a value fall into two
(opposite) categories:
- Where the array is effectively immutable and holds the state of the
value, such as a string-like value class (holding a char[]) or a
BigDecimal-like value class (holding a long[]);
- Where the value is behavior like a cursor into an array.
In the first case, two BigDecimal are equals() if they have the same
array contents, so at first we might think Arrays.equals() is what we
want (and same for the String case.) But if I were coding a
BigDecimal value, I wouldn't want this default anyway -- since I want
to treat 1.0 and 1.00 as equal, which the default won't do. So while
the default is OK, I'm still going to override it (and similarly for
String), because Arrays.equals still produces false negatives.
In the cursor case, we definitely want to compare the arrays by
reference -- two cursors represent the same thing if they point to the
same element in the SAME array, not equivalent arrays. And, same with
an Optional<Foo[]>.
So, grading the possibilities:
- reference equality -- produces right answer for cursor, produces
wrong answer for BigDecimal/String cases.
- Arrays.equals -- produces wrong answer for cursor, produces
tempting-seeming but still wrong answer for BigDecimal/String.
So both cases produce the wrong answer for the value-like uses of
arrays -- and the user has to override the default anyway. The only
one that produces the right answer in any case is reference equality
(which is .equals()). Maybe there's more cases, but these are the
ones that come immediately to mind.
For arrays, it seems that trying to read the user's mind here is a
losing (and at the same time, expensive) game. Arrays are what they
are; trying to turn them into acts-like-lists when you don't know the
semantics of the array in their enclosing value seems pretty
questionable. Which leads me to prefer the following simpler default
for equals() on values:
- compare primitive and components with ==
- compare reference components with .equals()
Do you have data on what percentage of your AutoValue objects contains
arrays?
Secondary question: to what degree is the "if you don't like the
default, write your own" mitigated by helpers like Guava's
Objects.equals? (It does kind of suck that if you disagree with the
default treatment of one field, you have to rewrite the whole
equals/hashCode methods.)
FYI, nasty and expensive method called on every element by deepEquals:
static boolean deepEquals0(Object e1, Object e2) {
assert e1 !=null;
boolean eq;
if (e1instanceof Object[] && e2instanceof Object[])
eq =deepEquals ((Object[]) e1, (Object[]) e2);
else if (e1instanceof byte[] && e2instanceof byte[])
eq =equals((byte[]) e1, (byte[]) e2);
else if (e1instanceof short[] && e2instanceof short[])
eq =equals((short[]) e1, (short[]) e2);
else if (e1instanceof int[] && e2instanceof int[])
eq =equals((int[]) e1, (int[]) e2);
else if (e1instanceof long[] && e2instanceof long[])
eq =equals((long[]) e1, (long[]) e2);
else if (e1instanceof char[] && e2instanceof char[])
eq =equals((char[]) e1, (char[]) e2);
else if (e1instanceof float[] && e2instanceof float[])
eq =equals((float[]) e1, (float[]) e2);
else if (e1instanceof double[] && e2instanceof double[])
eq =equals((double[]) e1, (double[]) e2);
else if (e1instanceof boolean[] && e2instanceof boolean[])
eq =equals((boolean[]) e1, (boolean[]) e2);
else eq = e1.equals(e2);
return eq;
}
On 5/17/2016 1:06 AM, John Rose wrote:
On May 16, 2016, at 8:29 PM, Kevin Bourrillion <[email protected]> wrote:
And by "deep equals" I mean equivalent behavior to
https://docs.oracle.com/javase/8/docs/api/java/util/Arrays.html#deepEquals-java.lang.Object:A-java.lang.Object:A-
at least that is what AutoValue has been doing.
To the extent I understand this, it seems wrong: deepEquals treats
arrays as if they are lists, which is an abstraction shift. Surely
you aren't suggesting cracking a component reference in a subfield
and treating its object as a List of its components, and so on
recursively? Because that's what deepEquals does.
Or do you mean that, just as deepEquals avoids using op==(ref,ref) on
array components, so "deep equals" should avoid using op==(ref,ref)
on value components?
You can avoid op==(ref,ref) by replacing it by a call to
ref.equals(ref), or you can avoid op==(ref,ref) by cracking the refs
and recursing. That latter breaks an abstraction, so I think we
agree it's bad, but *that* is a more "equivalent behavior" to
Arrays.deepEquals.
— John