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

Java generics: wildcard <?> Vs type parameter <E>?

Я обновляю свои знания о дженериках Java. Поэтому я обратился к превосходному учебнику от Oracle... и начал собирать презентацию для моих коллег. Я наткнулся на раздел на подстановочные знаки в tutorial, в котором говорится:

Рассмотрим следующий метод: printList:

public static void printList(List<Object> list) {
...

Цель printList - распечатать список любого типа, но он не достигает этой цели - он печатает только список экземпляров Object; он не может печатать List<Integer>, List<String>, List<Double> и т.д., потому что они не являются подтипами List<Object>. Чтобы написать общий метод printList, используйте List<?>:

public static void printList(List<?> list) {

Я понимаю, что List<Object> не будет работать; но я изменил код на

static <E> void printObjects(List<E> list) {
    for (E e : list) {
        System.out.println(e.toString());
    }
}
...
    List<Object> objects = Arrays.<Object>asList("1", "two");
    printObjects(objects);
    List<Integer> integers = Arrays.asList(3, 4);
    printObjects(integers);

И угадайте, что; используя List<E> Я могу печатать различные типы списков без каких-либо проблем.

Короче говоря: по крайней мере, в учебном пособии указано, что одному нужен шаблон для решения этой проблемы; но, как показано, это также можно решить таким образом. Итак, что мне не хватает?!

(сторона примечания: протестировано с Java7, так что, возможно, это была проблема с Java5, Java6, но, с другой стороны, Oracle, похоже, хорошо справляется с обновлениями своих учебников)

4b9b3361

Ответ 1

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

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

Почему общий метод более мощный, чем метод подстановочных знаков: вы указываете параметр имя, которое вы можете ссылаться. Например, рассмотрим метод, который удаляет первый элемент списка и добавляет его в конец списка. С помощью общих параметров мы можем сделать следующее:

static <T> boolean rotateOneElement(List<T> l){
    return l.add(l.remove(0));
}

с подстановочным знаком, это невозможно, так как l.remove(0) вернет capture-1-of-?, но l.add потребует capture-2-of-?. I.e., компилятор не может вывести, что результат remove - тот же тип, который ожидает add. Это противоречит первому примеру, когда компилятор может вывести, что оба типа одного типа T. Этот код не будет компилироваться:

static boolean rotateOneElement(List<?> l){
    return l.add(l.remove(0)); //ERROR!
}

Итак, что вы можете сделать, если хотите использовать метод rotateOneElement с подстановочным знаком, поскольку он проще в использовании, чем общее решение? Ответ прост: пусть подстановочный метод вызывает общий, затем он работает:

// Private implementation
private static <T> boolean rotateOneElementImpl(List<T> l){
    return l.add(l.remove(0));
}

//Public interface
static void rotateOneElement(List<?> l){
     rotateOneElementImpl(l);
}

Стандартная библиотека использует этот трюк в нескольких местах. Один из них - IIRC, Collections.java

Ответ 2

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

static <E> void printObjects(List<E> list, PrintFormat<E> format) {

Ответ 3

Технически существует без разницы между

<E> void printObjects(List<E> list) {

и

void printList(List<?> list) {

  • Когда вы объявляете параметр типа и используете его только один раз, он по существу становится параметром подстановки.
  • С другой стороны, если вы используете его более одного раза, разница становится значительной. например.

    <E> void printObjectsExceptOne(List<E> list, E object) {
    

    полностью отличается от

    void printObjects(List<?> list, Object object) {
    

    Вы можете видеть, что первый случай обеспечивает для обоих типов тот же. Хотя во втором случае нет ограничений.


В результате, если вы собираетесь использовать параметр типа только один раз, он даже не имеет смысла называть его. Вот почему архитекторы java придумали так называемые подстановочные аргументы (скорее всего).

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

Надеюсь, что это поможет.