Почему определение ковариантного метода сравнения считается плохой практикой?

#java #findbugs

#java #findbugs

Вопрос:

Вот пример из моего кода:

Базовый класс:

 abstract class AbstractBase implements Comparable<AbstractBase> {
    private int a;
    private int b;

    public int compareTo(AbstractBase other) {
        // compare using a and b
    }
}
  

Реализация:

 class Impl extends AbstractBase {
private int c;

public int compareTo(Impl other) {
    // compare using a, b and c with c having higher impact than b in AbstractBase
}
  

FindBugs сообщает об этом как о проблеме. Но почему это так? Что может произойти?

И как мне правильно реализовать решение?

Комментарии:

1. Это не ковариация. В Impl вы перегружаете (попробуйте поместить @Override туда).

2. Перегрузка @pingw33n здесь не сработает, поскольку типы параметров разные. И я не могу сравнить c с AbstractBase, поскольку у него его нет.

3. @UweAllner Но то, что вы делаете в Impl with compareTo(Impl other) , это распространенная ошибка, и она будет вести себя не так, как ожидалось. Это не переопределяет метод Comparable .

4. Вопрос в том, что вы хотите здесь реализовать? Рассмотрите экземпляры a и b подклассов A и B of AbstractBase : хотите ли вы, чтобы a и b были сопоставимы, даже если они происходят из разных подклассов (т. Е. Хотите ли вы, чтобы какой-либо экземпляр AbstractBase был сопоставим с любым другим экземпляром?) Или вы хотите указать, что каждый подкласс S AbstractBase должен иметь возможность сравнивать свои собственные экземпляры друг с другом, но не обязательно свои собственные экземпляры с экземплярами других подклассов?

5. @UweAllner Вы всегда можете создать отдельный Comparator<Impl> и использовать его во время сортировки.

Ответ №1:

Impl#compareTo(Impl) не является переопределяющим AbstractBase#compareTo(AbstractBase) , поскольку они не имеют одинаковой подписи. Другими словами, он не будет вызываться при использовании Collections#sort , например.

Комментарии:

1. Но он вызывается, поскольку сортировка не выполняет понижение; возможно, он не будет вызван, когда коллекция была объявлена, например, как List<AbstractBase> , чего я не делаю.

2. @UweAllner Ну, вы уверены, что это называется? Вставьте syso Impl#compareTo . Если он будет напечатан, то предоставленный вами код не будет представлять код, который вы выполняете, потому что он не может быть вызван, если явно (например new Impl().compareTo(...) ). sort использует AbstractBase#compareTo метод.

3. Извините, вы были правы, моя ошибка. Вызывается compareTo абстрактной базы. Теперь я хотел бы понять, почему, как и в sort, элементы преобразуются в сопоставимые (нетипизированные), а сравниваемый объект является Impl …

4. @UweAllner Самостоятельно создайте простую общую коллекцию и добавьте метод сортировки. Тогда вы увидите, что это единственный способ сделать это, если не использовать отражение.

Ответ №2:

РЕДАКТИРОВАТЬ: добавлено решение без приведения

Если вы не хотите выполнять приведение, вы можете попробовать следующее.

Измените свой базовый класс на:

 abstract class AbstractBase<T extends AbstractBase<?>> implements Comparable<T> {
//...
    public int compareTo(T other) {
      //... 
    }
}
  

И вы применяете класс к:

 class Impl extends AbstractBase<Impl> {
//...
    @Override
    public int compareTo(Impl other) {
    //...
    }
}
  

Решение с приведением:

Возможным решением было бы переопределить метод compareTo(AbstractBase) в классе Impl и явно проверить, передается ли экземпляр Impl в:

    class Impl extends AbstractBase {
   //...
        @Override
        public int compareTo(AbstractBase other) {

            if (other instanceof Impl) {

                int compC = Integer.compare(c, ((Impl) other).c);

                if (compC == 0) {
                    return super.compareTo(other);
                }
                return compC;
            }

            return super.compareTo(other);
        }
    }
  

Комментарии:

1. Я тоже думал об этом, но я считаю это приведение довольно уродливым. Предполагаемое решение (от FindBugs) использовать Object в качестве параметра для compareTo не упоминать…

2. каким бы уродливым это ни было, но я попробую использовать решение для кастинга; в противном случае мне также пришлось бы модифицировать все производные объекты. Спасибо за ваши усилия.

Ответ №3:

Я попробовал следующее. Не совсем уверен, что это причина, по которой findbugs выдает ошибку.

Смотрите следующий код с гипотетической реализацией compareTo метода.

Сравнение одних и тех же объектов приводит к разным результатам.

 public class Main
{
  public static void main(String[] args)
  {
    Impl implAssignedToImpl = new Impl(1, 2, 3);
    Impl otherImpl = new Impl(3, 2, 1);
    System.out.println(implAssignedToImpl.compareTo(otherImpl)); // prints -2

    AbstractBase implAssignedToAbstract = implAssignedToImpl;
    System.out.println(implAssignedToAbstract.compareTo(otherImpl)); //prints 0
  }
}

class AbstractBase implements Comparable<AbstractBase>
{
  private int a;

  private int b;

  public AbstractBase(int a, int b)
  {
    super();
    this.a = a;
    this.b = b;
  }

  public int compareTo(AbstractBase other)
  {
    return (a   b) - (other.a   other.b);
  }
}

class Impl extends AbstractBase
{
  private int c;

  public Impl(int a, int b, int c)
  {
    super(a, b);
    this.c = c;
  }

  public int compareTo(Impl other)
  {
    return super.compareTo(other)   (c - other.c);
  }
}
  

Основываясь на моей гипотезе compareTo , следующее кажется хорошим решением. Вы можете попробовать использовать метод, аналогичный getSum которому, который присваивает экземпляру объекта значение.

 public class Main
{
  public static void main(String[] args)
  {
    Impl implAssignedToImpl = new Impl(1, 2, 3);
    Impl otherImpl = new Impl(3, 2, 1);
    System.out.println(implAssignedToImpl.compareTo(otherImpl)); // prints 0

    AbstractBase implAssignedToAbstract = implAssignedToImpl;
    System.out.println(implAssignedToAbstract.compareTo(otherImpl)); //prints 0
  }
}

class AbstractBase implements Comparable<AbstractBase>
{
  private int a;

  private int b;

  public AbstractBase(int a, int b)
  {
    super();
    this.a = a;
    this.b = b;
  }

  public int compareTo(AbstractBase other)
  {
    return getSum() - other.getSum();
  }

  public int getSum()
  {
    return a   b;
  }
}

class Impl extends AbstractBase
{
  private int c;

  public Impl(int a, int b, int c)
  {
    super(a, b);
    this.c = c;
  }

  @Override
  public int getSum()
  {
    return super.getSum()   c;
  }
}
  

Комментарии:

1. Такой ситуации не произойдет (чего FindBugs, конечно, не могут знать). Но это, вероятно, будет причиной сообщения.

2. @UweAllner Я отредактировал ответ, чтобы добавить решение. Может быть, вы можете попробовать что-то подобное?

3. Мой пример был несколько упрощен; на самом деле мне пришлось сравнивать и другие значения, кроме целых чисел; поэтому я бы просто перенес проблему подписи в метод calculate . Но в любом случае спасибо за ваши усилия.

Ответ №4:

Как сказал sp00m, ваша Impl#compareTo(Impl) подпись отличается AbstractBase#compareTo(AbstractBase) от, поэтому она не перегружает ее.

Ключевой момент заключается в понимании того, почему он не работает, даже когда вы пытаетесь sort() сравнить с другим Impl , где более конкретная подпись совпадает.

Как вы определили Comparable<AbstractBase> , вам нужно определить, как ваши экземпляры compareTo AbstractBase экземпляры. И поэтому вам нужно реализовать compareTo(AbstractBase) .

Вы можете подумать, что, будучи Impl подтипом AbstractBase , более конкретный метод будет использоваться при сравнении между двумя Impl s. Проблема в том, что Java имеет статическую привязку, и поэтому компилятор определяет во время компиляции , какой метод будет использоваться для решения каждого вызова метода. Если то, что вы делали, было сортировкой AbstractBase s, тогда компилятор будет использовать compareTo(AbstractBase) , то есть AbstractBase интерфейс, определенный при реализации Comparable(AbstractBase) интерфейса.

Вы можете Impl реализовать Comparable<Impl> интерфейс для использования compareTo(Impl) метода, но это будет работать только в том случае, если вы явно сортируете вещи, которые, как известно, являются Impl s во время компиляции (т. Е. Impl Объект или Collection<Impl> ).

Если вы действительно хотите применить другое сравнение всякий раз, когда ваши два объекта равны Impl s, вам следует прибегнуть к какой-то двойной отправке в вашем Impl#compareTo(AbstractBase) like:

 Impl >>>
int compareTo(AbstractBase other) {
    return other.compareToImpl(this);
}

int compareToImpl(Impl other) {
    // perform custom comparison between Impl's
}

AbstractBase >>>
int compareTo(AbstractBase other) {
    // generic comparison
}

int compareToImpl(Impl other) {
    // comparison between an AbstractBase and an Impl.
    //Probably could just "return this.compareTo(other);", but check for loops :)
}
  

Для этого требуется, чтобы вы добавили некоторую Impl информацию в свой AbstractBase , что, впрочем, не очень красиво, но решает проблему более элегантным способом — использование отражения для этого совсем не элегантно.

Ответ №5:

Принцип подстановки Лискова (http://en.wikipedia.org/wiki/Liskov_substitution_principle ) состояния: если S является подтипом T, то объекты типа T могут быть заменены объектами типа S (т. Е. Объекты типа S могут заменять объекты типа T) без изменения каких-либо желательных свойств этой программы (корректность, выполняемая задача,и т.д.)

В вашем случае вы переопределяете метод compareTo из базового класса таким образом, что нарушает поведение исходного метода. Вероятно, именно поэтому у FindBugs есть проблема с этим.

Если вы хотите, чтобы это было правильно:

 abstract class AbstractBase {
}

class Impl1 extends AbstractBase implements Comparable<Impl1> ...  
class Impl2 extends AbstractBase implements Comparable<Impl2> ...
  

или

еще лучше, вообще не используйте сопоставимый интерфейс — вместо этого используйте компаратор во время сортировки.

Однако в реальной жизни бывают ситуации, когда вам нужно обойти это (возможно, у вас нет доступа к источнику AbstractBase, или, может быть, ваш новый класс — это просто POC). В этих особых случаях я бы выбрал «уродливое» приведенное решение, предложенное Джоном.