Generics: T extends MyClass vs. T extends MyClass <T>

Есть ли семантическая разница между этими двумя объявлениями или это только синтаксический сахар?

class C<T extends C> vs class C<T extends C<T>>

Предыстория: Недавно я ответил на question на дженериках с использованием подхода C<T extends C>, а одноранговый узел предоставил аналогичный ответ на основе C<T extends C<T>>. В итоге обе альтернативы дали тот же результат (в контексте заданного вопроса). Мне оставалось любопытно разницу между этими двумя конструкциями.

Есть ли семантическая разница? Если да, то каковы последствия и последствия каждого подхода?

Ответы

Ответ 1

Конечно - часто эти "типы я" используются, чтобы ограничить подтипы возвратом именно их собственного типа. Рассмотрим следующее:

public interface Operation {
    // This bit isn't very relevant
    int operate(int a, int b);
}

public abstract class AbstractOperation<T extends AbstractOperation<T>> {
    // Lets assume we might need to copy operations for some reason
    public T copy() {
        // Some clever logic that you don't want to copy and paste everywhere
    }
}

Cool - у нас есть родительский класс с полезным оператором, который может быть специфичным для подклассов. Например, если мы создадим AddOperation, каковы могут быть его общие параметры? Из-за "рекурсивного" общего определения это может быть только добавление AddOperation:

public class AddOperation extends AbstractOperation<AddOperation> {
    // Methods etc.
}

И, следовательно, гарантируется, что метод copy() возвращает AddOperation. Теперь представьте, что мы глупые, злые или творческие, или что-то еще, и попытаемся определить этот класс:

public class SubtractOperation extends AbstractOperation<AddOperation> {
    // Methods etc.

    // Because of the generic parameters, copy() will return an AddOperation
}

Это будет отклонено компилятором, потому что общий тип не находится в пределах его границ. Это очень важно - это означает, что в родительском классе, хотя мы не знаем, каков конкретный тип (и он может быть даже классом, который не существовал во время компиляции), метод copy() вернется экземпляр того же подкласса.

Если вы просто пошли с C<T extends C>, то это странное определение SubtractOperation будет законным, и вы потеряете гарантии о том, что T в этом случае - следовательно, операция вычитания может скопировать себя в операцию добавления.

Это не столько о защите иерархии классов от вредоносных подклассов, сколько от того, что она дает компилятору более надежные гарантии относительно задействованных типов. Если вы вызываете copy из другого класса altogther на произвольном Operation, одна из ваших образований гарантирует, что результат будет одного класса, в то время как другой будет требовать кастинга (и может быть не корректным, с помощью SubtractOperation выше).

Что-то вроде этого, например:

// This prelude is just to show that you don't even need to know the specific
// subclass for the type-safety argument to be relevant
Set<? extends AbstractOperation> operations = ...;
for (AbstractOperation<?> op : operations) {
    duplicate(op);
}

private <T extends AbstractOperation<T>> Collection<T> duplicate(T operation) {
    T opCopy = operation.copy();
    Collection<T> coll = new HashSet<T>();
    coll.add(operation);
    coll.add(opCopy);

    // Yeah OK, it ignored after this, but the point was about type-safety! :)
    return coll; 
}

Назначение в первой строке от duplicate до T не будет безопасным по типу с более слабым из двух предложенных вами границ, поэтому код не будет компилироваться. Даже если вы определяете все подклассы разумно.

Ответ 2

Скажем, у вас есть класс под названием Animal

class Animal<T extends Animal>

Если T - собака, это означает, что собака должна быть подклассом Animal < Собакa > , Животное <Cat> , Animal < Осел > что угодно.

class Animal<T extends Animal<T>>

В этом случае, если T является Dog, он должен быть подклассом Animal <Dog> и ничего другого, т.е. У вас должен быть класс

class Dog extends Animal<Dog>

у вас не может быть

class Dog extends Animal<Cat>

И даже если у вас есть это, вы не можете использовать Dog в качестве параметра типа для Animal, и вы должны иметь

class Cat extends Animal<Cat>

Во многих случаях существует большая разница между ними, особенно если у вас есть некоторые ограничения.

Ответ 3

Да, существует семантическая разница. Здесь минимальная иллюстрация:

class C<T extends C> {
}

class D<T extends D<T>> {
}

class Test {
    public static void main(String[] args) {
        new C<C>();  // compiles
        new D<D>();  // doesn't compile.
    }
}

Ошибка довольно очевидна:

Связанное несоответствие: тип D не является допустимым заменой ограниченного параметра <T extends D<T>> типа D<T>