Java Generics - эти два объявления метода эквивалентны?

Учитывая некоторый класс SomeBaseClass, эквивалентны ли эти два объявления метода?

public <T extends SomeBaseClass> void myMethod(Class<T> clz)

и

public void myMethod(Class<? extends SomeBaseClass> clz)

Ответы

Ответ 1

Для вызывающего: да, они эквивалентны.

Для кода внутри метода: no.

Разница в том, что в коде первого примера вы можете использовать тип T (например, для хранения объекта, созданного с помощью clz.newInstance()), а во втором вы не можете.

Ответ 2

Нет, это не так. С помощью первого определения вы можете использовать тип T внутри определения метода, например. создайте ArrayList<T> или верните T. Со вторым определением это невозможно.

Ответ 3

Ограниченные подстановочные знаки подлежат определенным ограничениям, чтобы избежать загрязнения кучи.

Когда вы используете подстановочный знак? расширяет X, вы знаете, что можете прочитать общую информацию, но вы не можете писать.

Например

List<String> jedis = new ArrayList<String>();
jedis.add("Obiwan");

List<? extends CharSequence> ls = jedis
CharSequence obiwan = ls.get(0); //Ok
ls.add(new StringBuffer("Anakin")); //Not Ok

Компилятор избегал загрязнения кучи, когда вы пытались добавить CharSequence (т.е. StringBuffer) в коллекцию. Поскольку компилятор не может быть уверен (из-за подстановочных знаков), что фактическая реализация коллекции имеет тип StringBuffer.

Когда вы используете? super X, вы знаете, что можете писать общую информацию, но вы не можете быть уверены в том, что вы читаете.

Например

List<Object> jedis = new ArrayList<Object>();
jedis.add("Obiwan"); 

List<? super String> ls = jedis;
ls.add("Anakin"); //Ok
String obiwan = ls.get(0); //Not Ok, we can´t be sure list is of Strings.

В этом случае, из-за подстановочных знаков, компилятор знает, что фактическая реализация коллекции может быть чем угодно в предках String. Таким образом, он не может гарантировать, что то, что вы получите, будет String. Правильно?

Этими же ограничениями являются те, которые вы бы тоже были подвержены любому объявлению с ограниченными подстановочными знаками. Они обычно известны как принцип get/put.

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

Например, рассмотрите код в методе Collections.sort. Если мы напишем метод следующим образом, мы получим ошибку компиляции:

public static void sort(List<? extends Number> numbers){
    Object[] a = numbers.toArray();
    Arrays.sort(a);
    ListIterator<? extends Number> i = numbers.listIterator();
    for (int j=0; j<a.length; j++) {
        i.next();
        i.set((Number)a[j]); //Not Ok, you cannot be sure the list is of Number
    }
}

Но если вы напишете это так, вы можете сделать работу

public static <T extends Number> void sort(List<T> numbers){
    Object[] a = numbers.toArray();
    Arrays.sort(a);
    ListIterator<T> i = numbers.listIterator();
    for (int j=0; j<a.length; j++) {
        i.next();
        i.set((T)a[j]);
    }
}

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

List<? extends Number> ints = new ArrayList<Integer>();
List<? extends Number> floats = new ArrayList<Float>();
sort(ints);
sort(floats);

Этого не может быть достигнуто иначе.

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

Ответ 4

Нет. На моей голове я могу думать о следующих различиях:

  • Две версии не переопределяют эквивалент. Например,

    class Foo {
        public <T extends SomeBaseClass> void myMethod(Class<T> clz) { }
    }
    
    class Bar extends Foo {
        public void myMethod(Class<? extends SomeBaseClass> clz) { }
    }
    

    не компилируется:

    Имя clash: метод myMethod (класс) типа Bar имеет то же стирание, что и myMethod (класс) типа Foo, но не переопределяет его

  • Если параметр типа появляется более одного раза в сигнатуре метода, он всегда представляет один и тот же тип, но если подстановочный знак появляется более одного раза, каждое событие может ссылаться на другой тип. Например,

    <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
    

    компилируется, но

    Comparable<?> max(Comparable<?> a, Comparable<?> b) {
        return a.compareTo(b) > 0 ? a : b;
    }
    

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

    max(Integer.MAX_VALUE, "hello");
    
  • Тело метода может ссылаться на фактический тип, используемый вызывающим пользователем с использованием параметра типа, но не с использованием типа подстановочных знаков. Например:

    <T extends Comparable<T>> T max(T... ts) {
        if (ts.length == 0) {
            return null;
        }
        T max = ts[0];
        for (int i = 1; i < ts.length; i++) {
            if (max.compareTo(ts[i]) > 0) {
                max = ts[i];
            }
        }
        return max;
    }
    

    компилирует.

Ответ 5

@Mark @Joachim @Michael

см. пример в JLS3 5.1.10 Преобразование захвата

public static void reverse(List<?> list) { rev(list);}
private static <T> void rev(List<T> list){ ... }

поэтому версия <?> может делать все, что может сделать версия <T>.

это легко принять, если среда выполнения подтверждена. a List<?> объект должен быть List<X> объектом какого-либо определенного несимметричного символа X в любом случае, и мы можем получить доступ к этому X во время выполнения. Таким образом, нет разницы, используя List<?> или List<T>

С стиранием типа у нас нет доступа к T или X, поэтому нет никакой разницы. Мы можем вставить T в List<T>, но где вы можете получить объект T, если T является приватным для вызова, и стерто? Возможны две возможности:

  • объект T уже сохранен в List<T>. поэтому мы сами манипулируем элементами. Как показывает пример reverse/rev, нет проблемы с этим, чтобы List<?> либо

  • он выходит из диапазона. Там другая компоновка, сделанная программистом, так что для объекта в любом месте гарантируется тип T для вызова. Для отмены компилятора необходимо выполнить немедленное кастинг. Опять же, не проблема делать то же самое с List<?>