Использование общих подстановочных знаков в возвращаемых параметрах

Использование общих типов подстановочных знаков в возвращаемых параметрах в Java обычно не рекомендуется. Например, «Эффективная Java», пункт 28 гласит:

Do not use wildcard types as return types. Rather than providing additional flexibility for your users, it would force them to use wildcard types in client code.

Properly used, wildcard types are nearly invisible to users of a class. They cause methods to accept the parameters they should accept and reject those they should reject. If the user of a class has to think about wildcard types, there is probably something wrong with the class’s API.

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

import java.util.*;
import java.lang.*;
import java.io.*;

class Container
{
    private final Map<String, Type<?>> itemMap = new HashMap<>();

    public <T> void addItem(String key, T t) {
        itemMap.put(key, new Type<>(t));
    }

    public Collection<Type<?>> getAllItems() {
        return itemMap.values();
    }

    public static class Type<T> {
        private final T value;
        public Type(T value) {
            this.value = value;
        }
        @Override
        public String toString() {
            return "Type(" + value + ")";
        }
    }

    public static void main (String[] args)
    {
        Container c = new Container();
        c.addItem("double", 1.0);
        c.addItem("string", "abc");
        System.out.println(c.getAllItems());
    }
}

Пример, конечно, упрощен, давайте просто предположим, что существует реальная причина, по которой Type<T> является общим.

Метод, который возвращает коллекцию всех элементов (getAllItems()), требуется по дизайну. Элементы, хранящиеся в itemMap, могут иметь разный тип, поэтому параметр типа T нельзя переместить в объявление класса Container. По той же причине мы не можем вернуть Collection<Type<T>>, так как это означало бы, что все элементы имеют один и тот же тип T, а на самом деле это не так. Согласно «Эффективной Java», с этим методом или с этим API-интерфейсом что-то не так, но, на мой взгляд, ни то, ни другое не соответствует действительности.

Я знаю, что подобные вопросы задавались здесь раньше (например, Общие типы подстановочных знаков не должны использоваться в возвращаемых параметрах), но они либо сосредоточены на конкретном примере, который немного отличается от моего, либо имеют ответы, вращающиеся вокруг того факта, что нет смысла возвращать коллекции формы Collection<? extends ...> когда вместо этого можно использовать параметр ограниченного типа. Это не относится к приведенному выше варианту использования (и меня не интересуют другие, потому что я вижу, как их можно решить).

Вопрос в следующем: если Effective Java верна (как и другие источники, такие как правила SonarQube и различные статьи в Интернете), какова альтернатива возврату Collection<Type<?>> в приведенном выше коде или что не так с дизайном класса Container ?

ОБНОВИТЬ:

Слегка измененный/расширенный пример для решения некоторых вопросов, поднятых в комментариях (это ближе к реальному коду, с которым я работаю):

import java.util.*;
import java.lang.*;
import java.io.*;

class Type<T> {
    private T value;
    private Class<T> clazz;

    public Type(T value, Class<T> clazz) {
        this.value = value;
        this.clazz = clazz;
    }
    T getValue() {
        return value;
    }
    void setValue(T value) {
        this.value = value;
    }
    Class<T> getItemClass() {
        return clazz;
    }
}

abstract class BaseContainer
{
    protected abstract Collection<Type<?>> getItems();
    // ... other methods
}

class Client {

    public void processItems(BaseContainer container) {
        Collection<Type<?>> items = container.getItems();
        for (Type<?> item: items) {
            processItem(item);
        }
    }

    public <T> void processItem(Type<T> item) {
        T currentValue = item.getValue();
        Class<T> clazz = item.getItemClass();
        T newValue = null;
        // ... calculate new value
        item.setValue(newValue);
    }
}

Если возвращаемый тип BaseContainer.getItems() объявлен как Collection<Type<Object>>, мы предоставляем лазейку для «мошеннических» клиентов, чтобы поместить значение недопустимого типа в экземпляр Type. По той же причине List<Object> не то же самое, что List<Integer> - легко увидеть, как возврат первого вместо последнего позволит любому поместить как String в список целых чисел (List<Integer> является однако List<? extends Object> ).

Следуя подходу, предложенному @isi, мы могли бы заставить Type реализовать некоторый интерфейс (не универсальный) и вместо этого использовать его в возвращаемом типе getItems. Я вижу, как это может работать. Но в реальном коде это означало бы совершенно новый интерфейс, имитирующий большинство методов универсального типа без параметра типа, что кажется несколько неуклюжим. Также похоже, что интерфейс Type<T> с T = Object по существу записывается, что граничит с дублированием кода. В некотором смысле я вижу Type<?> как удобное сокращение для того, чтобы избежать этой работы и дополнительного интерфейса.


person elk    schedule 06.01.2016    source источник
comment
Не могли бы вы уточнить, как пользователи Container будут использовать возвращаемое значение getAllItems? Текущий пример обеспечивает переопределение toString для Type. Кроме того, в реальных условиях должны ли вызывающие объекты возвращаться к конкретному типу каждого элемента? Если да, ожидаете ли вы, что это будет сделано с помощью проверки типов во время выполнения и понижения? В текущем примере мне трудно понять, почему вы не вернете Collection<Type<Object>>.   -  person Chris Nauroth    schedule 06.01.2016
comment
ну, книга неверна, как и все, кто повторяет ее точку зрения :) Не беспокойтесь об этом; если метод вызывает возвращаемый тип с подстановочным знаком, просто сделайте это. stackoverflow.com/a/30548589/2158288   -  person ZhongYu    schedule 14.01.2016
comment
моя статья о подстановочных знаках - bayou.io/draft/Capturing_Wildcards.html   -  person ZhongYu    schedule 14.01.2016


Ответы (3)


Если вы действительно собираетесь хранить объекты абсолютно любого типа, используйте Object. Нет необходимости добавлять сахар проверки типов, если вы специально не хотите проверять типы.

person OldCurmudgeon    schedule 06.01.2016

Я думаю, что класс Type молча предоставляет два интерфейса*. Один для пользователей, которым небезразличен универсальный тип T, а другой — для пользователей, которым он не нужен. Я попытаюсь сделать это немного яснее с помощью некоторого кода:

interface TypeWithoutT { Object getValueObj(); } // exposes untyped api

class Type<T> implements TypeWithoutT { // exposes several apis
    private final T value;

    // exposes typed part of the api (can be on a separate interface)
    public T getValue() { return value; }
    public Type(T value) { this.value = value; }

    // overrides commonly available apis
    @Override
    public String toString() { return "Type(" + value + ")"; }

    // exposes untyped part of the api and Object's api
    public Object getValueObj() { return value; }
}

Типизированный интерфейс класса Type<T> может предоставлять больше специфичных для типов операций, чем нетипизированный интерфейс оппонента TypeWithoutT. Метод toString доступен для каждого экземпляра, поэтому он является частью общедоступного интерфейса экземпляров Object. Вероятно, будут и другие полезные операции, кроме тех, которые предоставляет Object, которые должны быть доступны пользователю, что является нетипизированной частью API.

Перефразировать

Теперь возникает вопрос: что может сделать пользователь с кучей смешанных экземпляров Type? Чего они ожидают от каждого экземпляра в смешанной коллекции из Type экземпляров?

Достаточно ли Object?

Вероятно, они не ожидают ничего другого, кроме одного и того же набора операций, который является общим для всех экземпляров Type<T> для любого из возможных вариантов выбора T. Как метод toString из Object. Если это все, что нужно знать пользователям API об использовании коллекций экземпляров Type, определенно должно быть достаточно использовать Collection<Type<Object>> в качестве возвращаемого типа в методе getAllItems.

Или не?

Если пользователи API ожидают гораздо большего, чем интерфейс, предоставляемый Object, описанный выше подход можно использовать для возврата более специализированной коллекции. В этом случае возвращаемый тип метода getAllItems может быть изменен на Collection<TypeWithoutT>.

Таким образом, можно явно предоставить полезный API обоим пользователям — тому, кому важен тип, и тому, кому нет.

Однако

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


интерфейс*: я имею в виду интерфейс в общем смысле, а не явные интерфейсы Java. Таким образом, что-то с контрактом и набором операций.

person isaias-b    schedule 06.01.2016
comment
Спасибо, это, вероятно, ответ, который я искал. Это помогает думать об этой проблеме с точки зрения интерфейсов и, вероятно, предоставляет решения для большинства подобных проблем. - person elk; 14.01.2016
comment
Слишком рано нажал Enter... Я все же не совсем согласен с тем, что в этом примере можно вернуть Collection<Type<Object>>. Type<Object> указывает компилятору, что Type содержит Object (а не подтип Object). Например, если бы у нас был Type<Integer> и был определен метод setValue(T), можно было бы написать Object o = new Object(); type.setValue(o); в обход требования, что Type должен содержать Integer - person elk; 14.01.2016

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

Но это НЕ "подстановочный знак". «Подстановочный тип» может быть чем-то вроде Foo<?> или Foo<? extends Something> или Foo<? super Something>, где аргумент типа представляет собой (возможно, ограниченный) ?. Вы можете назначить Foo аргументов разного типа переменной с подстановочным знаком Foo<?>. Тип подстановочного знака обеспечивает своего рода полиморфизм.

Вы говорите о типе Collection<Type<?>>. В этом случае аргумент типа — Type<?>, который является определенным типом. Это совсем другое. Где-то там есть ?, но на более глубоком уровне. С чем-то вроде Collection<Type<?>> вы не можете назначить Collection аргументов разных типов переменной типа Collection<Type<?>>. Аргумент типа фиксируется как Type<?>; ничего другого не совместимо.

person newacct    schedule 14.01.2016