Понимание использования дженериков в java
Я пытаюсь понять, как работает класс, дженериков и это не имеет смысла для меня.
Так, например, если у меня есть следующие классы:
class A<E> {
void go (E e) { }
}
class B extends A { }
и тогда я пытаюсь
A<? extends A> a1 = new A<A>();
A<? extends A> a2 = new A<B>();
a1.go(new A()); // i get a compiler error
a2.go(new B()); // i get a compiler error
не должен ли метод go принимать A или любой подкласс A??
спасибо :)
3 ответов
причины этого основаны на том, как Java реализует дженерики.
Пример Массивы
с массивами вы можете сделать это (массивы ковариантны, как объяснили другие)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
но что произойдет, если вы попытаетесь это сделать?
Number[0] = 3.14; //attempt of heap pollution
эта последняя строка будет компилироваться просто отлично, но если вы запустите этот код, вы можете получить ArrayStoreException
. Потому что вы пытаетесь поместить double в целочисленный массив (независимо от доступ через номер ссылки).
это означает, что вы можете обмануть компилятор, но вы не можете обмануть систему типов среды выполнения. И это так, потому что массивы-это то, что мы называем типы reifiable. Это означает, что во время выполнения Java знает, что этот массив фактически был создан как массив целых чисел, к которому просто оказывается доступ через ссылку типа Number[]
.
Итак, как вы можете видеть, одна вещь-это фактический тип объекта, еще одна вещь-это тип ссылки, которую вы используете для доступа к ней, верно?
проблема с Java Generics
теперь проблема с общими типами Java заключается в том, что информация о типе отбрасывается компилятором и недоступна во время выполнения. Этот процесс называется стирания типа. Есть веская причина для реализации дженериков, подобных этому в Java, но это долгая история, и она связана с бинарной совместимостью с ранее существовавший код.
но важным моментом здесь является то, что, поскольку во время выполнения нет информации о типе, нет никакого способа гарантировать, что мы не совершаем загрязнение кучи.
например,
List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution
если компилятор Java не останавливает вас от этого, система типов среды выполнения также не может остановить вас, потому что во время выполнения невозможно определить, что этот список должен был быть только списком целых чисел. Среда выполнения Java позволит вам поместите все, что вы хотите, в этот список, когда он должен содержать только целые числа, потому что при его создании он был объявлен как список целых чисел.
таким образом, разработчики Java убедились, что вы не можете обмануть компилятор. Если вы не можете обмануть компилятор (как мы можем сделать с массивами), вы также не можете обмануть систему типов среды выполнения.
таким образом, мы говорим, что общие типы не-reifiable.
очевидно, это будет препятствовать полиморфизму. Рассмотрим следующий пример:
static long sum(Number[] numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
теперь вы можете использовать его так:
Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};
System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));
но если вы попытаетесь реализовать тот же код с универсальными коллекциями, вам это не удастся:
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
вы получите компилятор erros, если попытаетесь...
List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);
System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error
решение состоит в том, чтобы научиться использовать две мощные функции Java generics, известные как ковариация и контравариантность.
ковариации
С ковариацией вы можете читать элементы из структуры, но вы не можете ничего записать в нее. Все эти заявления.
List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>()
List<? extends Number> myNums = new ArrayList<Double>()
и вы можете узнать от myNums
:
Number n = myNums.get(0);
потому что вы можете быть уверены, что независимо от фактического списка содержится, он может быть upcasted ряда (ведь все, что расширяет число-это число, верно?)
однако, вы не разрешено помещать что угодно в ковариантную структуру.
myNumst.add(45L); //compiler error
это не будет разрешено, потому что Java не может гарантировать, что является фактическим типом объекта в общей структуре. Это может быть все, что расширяет число, но компилятор не может быть уверен. Так что вы можете читать, но не писать.
контравариантность
С contravariance вы можете сделать противоположное. Вы можете поместить вещи в общую структуру, но вы не можете прочитать из он.
List<Object> myObjs = new List<Object();
myObjs.add("Luke");
myObjs.add("Obi-wan");
List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);
в этом случае фактическая природа объекта-это список объектов, и через контравариантность вы можете поместить в него числа, в основном потому, что все числа имеют объект как их общий предок. Таким образом, все числа являются объектами, и поэтому это допустимо.
однако вы не можете безопасно читать что-либо из этой контравариантной структуры, предполагая, что вы получите номер.
Number myNum = myNums.get(0); //compiler-error
как вы можете видеть, если компилятор позволил вам пишу эту строку, вы получите ClassCastException во время выполнения.
Принцип Get/Put
таким образом, используйте ковариацию, когда вы намереваетесь только взять общие значения из структуры, используйте контравариацию, когда вы намереваетесь только поместить общие значения в структуру и использовать точный общий тип, когда вы намереваетесь сделать оба.
лучший пример у меня есть следующее, что копирует любые числа из одного списка в другой список. Это только получает предметы из источника, и это только ставит предметы в судьбе.
public static void copy(List<? extends Number> source, List<? super Number> destiny) {
for(Number number : source) {
destiny.add(number);
}
}
благодаря силе ковариации и контравариации это работает для такого случая:
List<Integer> myInts = asList(1,2,3,4);
List<Integer> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();
copy(myInts, myObjs);
copy(myDoubles, myObjs);
при объявлении
A<? extends A> a1 = new A<A>();
компилятор знает только, что 'тип S составляет A
или какой-то его подкласс-он не знает, какой.
если вы измените его на A<A> a1
, тогда он будет знать, что экземпляр A
является юридическим аргументом. То же самое относится и к a2
.
на соответствующей ноте строка
class B extends A
использует сырье тип. Вместо этого вы должны сказать Class B extends A<SomeType>
или Class B<E> extends A<E>
.
сообщение об ошибке: метод go(capture#1-of ? расширяет A) в типе A не применяется для Аргументов (A)
причина этого: вы не конкретизируете ссылку a1. Это определяется по захвату "? расширяет", который никогда не может быть дублирован. Опять же, когда вы создаете новый() передает другой захват "? расширяет "а", само "А", которое не может быть преобразовано в первое.