Должен ли я использовать Objects.equals () для каждого элемента, а не Arrays.equals (), когда массив состоит из полей в моем классе? - PullRequest
0 голосов
/ 08 июня 2018

Я пишу утилиту кортежей, и поэтому, чтобы у меня была безопасность типов обобщенных типов, у меня есть TupleN классы, где N - это количество элементов (и, следовательно, параметров типа), которые оно имеет, все из которых наследуются отабстрактный класс Tuple.Вот (сокращенная версия) мой класс Tuple, а также класс Tuple2 (все остальные классы TupleN следуют той же схеме).

Tuple.java

public abstract class Tuple {

  public abstract int arity();

  public abstract Object get(int index);

  public Object[] toArray() {
    ArrayList<Object> list = new ArrayList<>();
    int arity = arity();
    for (int i = 0; i < arity; i++) list.add(get(i));
    return list.toArray();
  }

  @Override
  public boolean equals(Object o) {
    return o instanceof Tuple && Arrays.equals(toArray(), ((Tuple) o).toArray());
  }

  @Override
  public int hashCode() {
    return Arrays.hashCode(toArray());
  }

  @Override
  public String toString() {
    return String.format("(%s)", String.join(", ", toArray().stream().map(t -> Objects.toString(t)).toList()));
  }

}

Tuple2.java

public 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) {
    switch (index) {
      case 0: return t0;
      case 1: return t1;
      default: throw new IndexOutOfBoundsException(index);
    }
  }

  @Override
  public Object[] toArray() {
    return new Object[] {t0, t1};
  }

  @Override
  public boolean equals(Object o) {
    if (o instanceof Tuple2) {
      @SuppressWarnings("unchecked")
      Tuple2<T0, T1> other = (Tuple2<T0, T1>) o;
      return Objects.equals(t0, other.t0) && Objects.equals(t1, other.t1);
    }
    return false;
  }

  @Override
  public String toString() {
    return String.format("(%s, %s)", t0, t1);
  }

}

Я собирался переопределить equals и hashCode в моих TupleN классах для эффективности, но я понял, что моя реализация hashCode будет return Objects.hashCode(t0, t1);, который (из документов ) совпадает с return Arrays.hashCode(new Object[] {t0, t1});, который (что с моим переопределенным методом toArray) является именно тем, что делает реализация по умолчанию.После этого я понял, что мое переопределение equals в значительной степени является просто реализацией по умолчанию equals, за исключением случаев, когда Arrays.equals был "развернут".Мой вопрос таков: «разворачивается» достаточно более эффективно, чтобы стоило выписать переопределение (что не слишком долго для Tuple2, но когда дело доходит до Tuple7, это звучит какмного шаблонного кода), или было бы хорошо просто использовать реализацию по умолчанию?

РЕДАКТИРОВАТЬ:

Пока я на это, я могу такжеспросите то же самое о toString, хотя я представляю реализацию по умолчанию, которая требует больше усилий (что с потоками и тому подобное).

Ответы [ 3 ]

0 голосов
/ 08 июня 2018

Да, Arrays.equals() достаточно для этого.Вам не нужно проверять каждый из них самостоятельно.

Как я понимаю, вы не используете много своих вещей в своем абстрактном классе.Вы должны иметь возможность сохранить свой собственный массив (Object[]) и позволить вашему абстрактному классу выполнять большую часть работы.

public abstract class Tuple {
    private final Object[] data;

    protected Tuple(Object... data) {
        this.data = data;
    }

    protected final Object[] getArray() { return data; }
    public final Object[] toArray() { return Arrays.copyOf(data, data.length); }

    public Object get(int index) {
        return data[index]; // You need to do your boundary checks
    }

    // Same hashcode and equals methods
}

public class Tuple2<T0, T1> extends Tuple {
    public final T0 t0;
    public final T1 t1;

    public Tuple2(T0 t0, T1 t1) {
         super(t0, t1);
         this.t0 = t0;
         this.t1 = t1;
    }
}
0 голосов
/ 11 июня 2018

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это, только предоставьте переопределяющие методы, если инструмент профилирования показал, что в определенном месте действительно есть узкое место, а также доказал, что специализированная реализация действительно имеет преимущество в производительности по сравнению с общим кодом.

0 голосов
/ 08 июня 2018

Arrays.equals() вызовет ваш метод equals() и выполнит цикл для вас.

То же самое toString().В этом случае, если вы удовлетворены форматом вывода по умолчанию Arrays.toString(), нет необходимости искать дальше.

...