Подтвердить что ты не робот

Должен ли Сопоставимый когда-либо сравнивать с другим типом?

Мне интересно, существует ли когда-либо допустимый прецедент для следующего:

class Base {}

class A implements Comparable<Base> {
    //...
}

Кажется, это общий шаблон (см. Collections для ряда примеров), чтобы принять коллекцию типа T, где T extends Comparable<? super T>.

Но, по сравнению с базовым классом, технически невозможно выполнить контракт compareTo(), потому что нет способа гарантировать, что другой класс не расширяет базу с помощью противоречивого сравнения. Рассмотрим следующий пример:

class Base {
    final int foo;
    Base(int foo) {
        this.foo = foo;
    }
}

class A extends Base implements Comparable<Base> {
    A(int foo) {
        super(foo);
    }
    public int compareTo(Base that) {
        return Integer.compare(this.foo, that.foo); // sort by foo ascending
    }
}

class B extends Base implements Comparable<Base> {
    B(int foo) {
        super(foo);
    }
    public int compareTo(Base that) {
        return -Integer.compare(this.foo, that.foo); // sort by foo descending
    }
}

У нас есть два класса, расширяющие Base, используя сравнения, которые не следуют общему правилу (если бы существовало общее правило, оно почти наверняка было бы реализовано в Base). Однако следующий разбитый сорт будет компилироваться:

Collections.sort(Arrays.asList(new A(0), new B(1)));

Не было бы безопаснее принимать только T extends Comparable<T>? Или есть какой-то прецедент, который бы подтвердил подстановочный знак?

4b9b3361

Ответ 1

Это очень хороший вопрос. Во-первых, давайте начнем с причины, по которой Collections используют методы типа

binarySearch(List<? extends Comparable<? super T>> list, T key)

В самом деле, почему не просто

binarySearch(List<? extends Comparable<T>> list, T key)

Причиной этого является принцип PECS: продюсер продюсеров, потребительский супер. Что делает binarySearch? Он считывает элементы из списка и затем сравнивает их, передавая их значения функции compareTo. Поскольку он читает элементы, список действует как производитель, и, следовательно, первая часть-продюсер расширяет. Это очевидно, так как насчет части "Потребительская супер"?

Потребитель Super в основном означает, что, если вы собираетесь передавать значения какой-либо функции, вам все равно, принимает ли он точный тип вашего объекта или какой-либо его суперкласс. Итак, что говорит декларация binarySearch: я могу искать что угодно, если что-либо может быть передано методу compareTo элементов списка.

В случае сортировки это не так очевидно, потому что элементы сравниваются только друг с другом. Но даже тогда, что, если Base действительно реализует Comparable<Base> (и делает все сравнение) и A и B просто расширяется Base, не трогая сравнение каким-либо образом? Тогда вы не сможете сортировать списки A и B, потому что они не реализуют Comparable<A> и Comparable<B> соответственно. Вам придется переопределять весь интерфейс каждый раз, когда вы подклассы!

Другой пример: что, если кто-то захотел выполнить двоичный поиск в списке, содержащем экземпляры какого-либо класса, который даже не расширяет ваш Base?

class BaseComparable implements Comparable<Base> {
    private final Base key;
    // other fields

    BaseComparable(Base key, ...) {
        this.key = key;
        // other fields initialization
    }

    @Override
    public int compareTo(Base otherKey) {
        return this.key.compareTo(otherKey);
    }
};

И теперь они хотят использовать экземпляр A в качестве ключа для этого двоичного поиска. Они могут делать это только из-за части ? super T. Обратите внимание, что этот класс не знает, является ли ключ A или B, поэтому он не может реализовать Comparable<A/B>.

Что касается вашего примера, я думаю, что это всего лишь пример плохого дизайна. К сожалению, я не вижу возможности предотвратить такие вещи, не нарушая принцип PECS и/или ограничивая уже ограниченную функциональность Java-дженериков.

Ответ 2

Ограничение

    T extends Comparable<? super T>

следует понимать как

    T extends S, S extends Comparable<S>

A Comparable тип всегда самосогласован.

Если X <: Comparable<Y>, это должно быть в случае X <: Y <: Comparable<Y>.

Мы могли бы даже "доказать", что из договора compareTo(). Поскольку реверс y.compareTo(x) должен быть определен, он должен быть правдой, что Y <: Comparable<A>, X<:A. Следуя тому же рассуждению, имеем A <: Comparable<Y>. Тогда транзитивность приведет к A=Y.