Objects.hashCode
вызывает Arrays.hashCode
, но его использование в качестве метода varargs дублирует код создания массива на стороне вызывающего, поэтому результат может быть даже хуже, чем метод вашего базового класса.На стороне JVM могут быть оптимизации, но это чистая спекуляция, и было бы бессмысленно создавать больше кода, не зная фактического влияния на производительность.
Я думаю, стоит пойти по пути реализации equals
и hashCode
здесь вручную, если вы делаете это только один раз для базового класса без необходимости создавать специализации в подклассах, поскольку все, что вам нужно для реализации без копирования, уже есть, а именно arity()
иget(int)
.Реализация в основном делает то же самое, что и AbstractList
, который вы не хотите расширять / наследовать ради чистого API (без неподдерживаемых методов).
Метод toString()
, основанный на Stream API, долженбудет достаточно для большинства целей, хотя даже здесь нет необходимости сначала создавать массив, не говоря уже о сборе в List
, просто вызвать String.join
, когда вы можете собрать в строку нужного формата впервое место:
public abstract class Tuple {
public abstract int arity();
public abstract Object get(int index);
public Stream<Object> elements() {
return IntStream.range(0, arity()).mapToObj(this::get);
}
public Object[] toArray() {
return elements().toArray();
}
@Override
public String toString() {
return elements().map(Objects::toString).collect(Collectors.joining(", ", "(", ")"));
}
@Override
public boolean equals(Object o) {
if(o == this) return true;
if(!(o instanceof Tuple)) return false;
Tuple t = (Tuple)o;
int n = t.arity();
if(n != arity()) return false;
for(int i = 0; i < n; i++) if(!Objects.equals(get(i), t.get(i))) return false;
return true;
}
@Override
public int hashCode() {
int result = 1;
for(int i = 0, n = arity(); i < n; i++)
result = 31 * result + Objects.hashCode(get(i));
return result;
}
}
Усилия, затрачиваемые на методы базового класса, окупаются на подклассах, которым не требуется предоставлять оптимизированные версии
public final class Tuple2<T0, T1> extends Tuple {
public final T0 t0;
public final T1 t1;
public Tuple2(T0 t0, T1 t1) {
this.t0 = t0;
this.t1 = t1;
}
@Override
public int arity() {
return 2;
}
@Override
public Object get(int index) {
// starting with Java 9, you may consider Objects.checkIndex(index, arity());
if((index|1) != 1) throw new IndexOutOfBoundsException(index);
return index == 0? t0: t1;
}
}
public final class Tuple3<T0, T1, T2> extends Tuple {
public final T0 t0;
public final T1 t1;
public final T2 t2;
public Tuple3(T0 t0, T1 t1, T2 t2) {
this.t0 = t0;
this.t1 = t1;
this.t2 = t2;
}
@Override
public int arity() {
return 3;
}
@Override
public Object get(int index) {
switch(index) {
case 0: return t0;
case 1: return t1;
case 2: return t2;
default: throw new IndexOutOfBoundsException(index);
}
}
}
etc
Beyondэто, только предоставьте переопределяющие методы, если инструмент профилирования показал, что в определенном месте действительно есть узкое место, а также доказал, что специализированная реализация действительно имеет преимущество в производительности по сравнению с общим кодом.