Что такое wildcard java
Перейти к содержимому

Что такое wildcard java

  • автор:

Wildcards

Consider the problem of writing a routine that prints out all the elements in a collection. Here’s how you might write it in an older version of the language (that is, a pre-5.0 release):

void printCollection(Collection c) < Iterator i = c.iterator(); for (k = 0; k < c.size(); k++) < System.out.println(i.next()); >>

And here is a naive attempt at writing it using generics (and the new for loop syntax):

void printCollection(Collection c) < for (Object e : c) < System.out.println(e); >>

The problem is that this new version is much less useful than the old one. Whereas the old code could be called with any kind of collection as a parameter, the new code only takes Collection , which, as we’ve just demonstrated, is not a supertype of all kinds of collections!

So what is the supertype of all kinds of collections? It’s written Collection (pronounced «collection of unknown»), that is, a collection whose element type matches anything. It’s called a wildcard type for obvious reasons. We can write:

void printCollection(Collection c) < for (Object e : c) < System.out.println(e); >>

and now, we can call it with any type of collection. Notice that inside printCollection() , we can still read elements from c and give them type Object . This is always safe, since whatever the actual type of the collection, it does contain objects. It isn’t safe to add arbitrary objects to it however:

Collection c = new ArrayList(); c.add(new Object()); // Compile time error

Since we don’t know what the element type of c stands for, we cannot add objects to it. The add() method takes arguments of type E , the element type of the collection. When the actual type parameter is ? , it stands for some unknown type. Any parameter we pass to add would have to be a subtype of this unknown type. Since we don’t know what type that is, we cannot pass anything in. The sole exception is null , which is a member of every type.

On the other hand, given a List , we can call get() and make use of the result. The result type is an unknown type, but we always know that it is an object. It is therefore safe to assign the result of get() to a variable of type Object or pass it as a parameter where the type Object is expected.

Bounded Wildcards

Consider a simple drawing application that can draw shapes such as rectangles and circles. To represent these shapes within the program, you could define a class hierarchy such as this:

public abstract class Shape < public abstract void draw(Canvas c); > public class Circle extends Shape < private int x, y, radius; public void draw(Canvas c) < . >> public class Rectangle extends Shape < private int x, y, width, height; public void draw(Canvas c) < . >>

These classes can be drawn on a canvas:

public class Canvas < public void draw(Shape s) < s.draw(this); > >

Any drawing will typically contain a number of shapes. Assuming that they are represented as a list, it would be convenient to have a method in Canvas that draws them all:

public void drawAll(List shapes) < for (Shape s: shapes) < s.draw(this); > >

Now, the type rules say that drawAll() can only be called on lists of exactly Shape : it cannot, for instance, be called on a List . That is unfortunate, since all the method does is read shapes from the list, so it could just as well be called on a List . What we really want is for the method to accept a list of any kind of shape:

public void drawAll(Listextends Shape> shapes)

There is a small but very important difference here: we have replaced the type List with Listextends Shape> . Now drawAll() will accept lists of any subclass of Shape , so we can now call it on a List if we want.

Listextends Shape> is an example of a bounded wildcard. The ? stands for an unknown type, just like the wildcards we saw earlier. However, in this case, we know that this unknown type is in fact a subtype of Shape . (Note: It could be Shape itself, or some subclass; it need not literally extend Shape .) We say that Shape is the upper bound of the wildcard.

There is, as usual, a price to be paid for the flexibility of using wildcards. That price is that it is now illegal to write into shapes in the body of the method. For instance, this is not allowed:

public void addRectangle(Listextends Shape> shapes) < // Compile-time error! shapes.add(0, new Rectangle()); >

You should be able to figure out why the code above is disallowed. The type of the second parameter to shapes.add() is ? extends Shape — an unknown subtype of Shape . Since we don’t know what type it is, we don’t know if it is a supertype of Rectangle ; it might or might not be such a supertype, so it isn’t safe to pass a Rectangle there.

Bounded wildcards are just what one needs to handle the example of the DMV passing its data to the census bureau. Our example assumes that the data is represented by mapping from names (represented as strings) to people (represented by reference types such as Person or its subtypes, such as Driver ). Map is an example of a generic type that takes two type arguments, representing the keys and values of the map.

Again, note the naming convention for formal type parameters— K for keys and V for values.

public class Census < public static void addRegistry(Mapextends Person> registry) < >. Map allDrivers = . ; Census.addRegistry(allDrivers);

Java generics. В чем разница между wildcard() и parameterized types()?

T — тип, который произволен и при стирании трансформируется в Object — суперкласс всех классов в Java.
? — по идее то же самое или нет?
Я не могу понять, зачем нужен метасимвол ? . В Шилдте приведен пример с суммированием: если использовать T , то нельзя сравнивать между собой Double и Integer , а если ? , то можно. До конца не могу понять разницу. А также есть ли разница, например, и ? Ведь если они были бы одинаковы, то смысл вводить их в язык.

Отслеживать
111 3 3 бронзовых знака
задан 20 мая 2020 в 22:48
Александр Скворцов Александр Скворцов
123 1 1 серебряный знак 6 6 бронзовых знаков

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

Документация

Переменная типа указывается в объявлении обобщенного класса, интерфейса, метода или конструктора. После этого ее можно использовать в качестве типа в теле этого класса, интерфейса, метода или конструктора. Переменная типа может называться как угодно ( T , X , MyAwesomeType ).

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

Можете почитать об этом в спецификации языка Java:

  • §4.4. Переменные типа.
  • §4.5.1. Аргументы типа и символы подстановки.

Пример

В Шилдте приведен пример с суммированием: если использовать T, то нельзя сравнивать между собой Double и Integer, а если ?, то можно

Давайте попробуем разобрать пример. Насколько я понимаю используется код вроде этого:

class Stats  < T[] nums; Stats(T[] o) < nums = o; >double average() < return Arrays.stream(nums).mapToDouble(n ->n.doubleValue()).sum() / nums.length; > boolean sameAvg(Stats ob) < return average() == ob.average(); >public static void main(String args[]) < Statsiob = new Stats<>(new Integer[]); Stats dob = new Stats<>(new Double[]); if (iob.sameAvg(dob)) < System.out.println("are the same."); >else < System.out.println("differ."); >> > 

Во-первых, ? нельзя указать в качестве переменной типа. Если объявить класс так:

class Stats  

, то возникнет ошибка компиляциии так как мы объявляем переменную не указывая ее названия.

Во-вторых, мы не можем использовать T для аргумента типа sameAvg . Если сделать так:

boolean sameAvg(Stats ob)

, то не скомпилируется следующая строка

if (iob.sameAvg(dob))  

Дело в том, что T — это один конкретный тип для всего класса. Для переменной iob это Integer , соответственно ее метод sameAvg принимает только Stats и не может принять Stats .

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

boolean sameAvg(Stats ob) < //любой Stats, не только Stats

Второй тип можно объявить и явно в качестве переменной типа для метода sameAvg :

 boolean sameAvg(Stats ob) < //в примере выше T - Integer, U - Double return average() == ob.average(); >

Повторюсь, здесь в этом нет необходимости, т.к. U не используется внутри метода. В других случаях может понадобится именно такое объявление, а не символ подстановки. Почитайте Java: bounded wildcards or bounded type parameter?

Wildcard для спецификации подтипов — Java: Дженерики

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

public class Application  public static Double average(ListInteger> numbers)  // Double var sum = 0.0; for (var number : numbers)  sum += number; > return sum / numbers.size(); > public static void main(String[] args)  var numbers = List.of(1, 2, 3, 4, 5); System.out.println(average(numbers)); // => 3.0 > > 

Если сделать из метода average() дженерик, то он не сработает, так как тип T будет Object , для которого операция сложения не определена.

public static T> Double average(ListT> numbers)  var sum = 0.0; for (var number : numbers)  // The operator += is undefined for the argument type(s) double, T sum += number; > return sum / numbers.size(); > 

Для подобных задач в Java есть механизм Wildcard, с его помощью можно уточнить типы, с которыми работает дженерик. В нашем случае и Integer и Double являются подтипами Number , а значит мы можем написать так: List . В таком случае в метод попадут только числа, какими бы они не были, а типом параметра numbers станет List .

public static T> Double average(List extends Number> numbers)  var sum = 0.0; for (var number : numbers)  sum += number; > return sum / numbers.size(); > 

Обновленный код почти работает. Он все еще выдает ошибку The operator += is undefined for the argument type(s) double, Number, так как во время сложения получается что тип переменной sum это Double , а тип переменной number - Number . Эта задача решается за счет метода doubleValue() определенного у Number , который любое число преобразует в Double . Рабочий код:

public static T> Double average(List extends Number> numbers)  var sum = 0.0; for (var number : numbers)  sum += number.doubleValue(); > return sum / numbers.size(); > 

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

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов

Наши выпускники работают в компаниях:

Использование generic wildcards для повышения удобства Java API

Этот пост для тех, кто работает над очередным API на языке Java, либо пытается усовершенствовать уже существующий. Здесь будет дан простой совет, как с помощью конструкций ? extends T и ? super T можно значительно повысить удобство вашего интерфейса.

Перейти сразу к сути

Исходный API

Предположим, у вас есть интерфейс некого хранилища объектов, параметризованный, допустим, двумя типами: тип ключа ( K ) и тип значения ( V ). Интерфейс определяет набор методов для работы с данными в хранилище:

public interface MyObjectStore  < /** * Кладёт значение в хранилище по заданному ключу. * * @param key Ключ. * @param value Значение. */ void put(K key, V value); /** * Читает значение из хранилища по заданному ключу. * * @param key Ключ. * @return Значение либо null. */ @Nullable V get(K key); /** * Кладёт все пары ключ-значение в хранилище. * * @param entries Набор пар ключ-значение. */ void putAll(Mapentries); /** * Читает все значения из хранилища по заданным * ключам. * * @param keys Набор ключей. * @return Пары ключ-значение. */ Map getAll(Collection keys); /** * Читает из хранилища все значения, удовлетворяющие * заданному условию (предикату). * * @param p Предикат для проверки значений. * @return Значения, удовлетворяющие предикату. */ Collection getAll(Predicate p); . // и так далее > 

Определение Predicate

interface Predicate  < /** * Возвращает true, если значение удовлетворяет * условию, false в противном случае. * * @param exp Выражение для проверки. * @return true, если удовлетворяет; false, если нет. */ boolean apply(E exp); >

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

MyObjectStore carsStore = . ; carsStore.put(20334L, new Car("BMW", "X5", 2013)); Car c = carsStore.get(222L); . 

Однако, в чуть менее тривиальных случаях клиент вашего API столкнётся с неприятными ограничениями.

Использование ? super T

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

Collection cars = carsStore.getAll(new Predicate() < @Override public boolean apply(Car exp) < . // Здесь наша логика по выбору автомобиля. >>); 

Но дело в том, что у нашего клиента уже есть предикат для выбора автомобилей. Только он параметризован не классом Car , а классом Vehicle , от которого Car унаследован. Он может попытаться запихать Predicate вместо Predicate , но в ответ получит ошибку компиляции:

no suitable method found for getAll(Predicate)

Компилятор говорит нам, что вызов метода невалиден, поскольку Vehicle — это не Car . Но ведь он является родительским типом Car , а значит, всё, что можно сделать с Vehicle , можно сделать и с Car ! Так что мы вполне могли бы использовать предикат по Vehicle для выбора значений типа Car . Просто мы не сказали компилятору об этом, и, тем самым, заставляем пользователя городить конструкции вроде:

final Predicate vp = mgr.getVehiclePredicate(); Collection cars = carsStore.getAll(new Predicate() < @Override public boolean apply(Car exp) < return vp.apply(exp); >>); 

А ведь всё решается так просто! Нам нужно лишь слегка изменить сигнатуру метода:

Collection getAll(Predicate p); 

Запись Predicate означает «предикат от V или любого супертипа V (вплоть до Object)». Данное изменение никак не ломает компиляцию существующего кода, зато устраняет абсолютно бессмысленные ограничения на параметр предиката. Клиент теперь может использовать свой предикат для Vehicle совершенно свободно:

MyObjectStore carsStore = . ; Predicate vp = mgr.getVehiclePredicate(); Collection cars = carsStore.getAll(vp); 

Мы обобщим данный приём чуть ниже, и запомнить его будет совсем просто.

Использование ? extends T

С передаваемыми коллекциями та же история, только в обратную сторону. Здесь, в большинстве случаев, имеет смысл использовать ? extends T для типа элементов коллекции. Посудите сами: имея ссылку на MyObjectStore , пользователь вполне вправе положить в хранилище набор объектов Map (ведь Car — это подтип Vehicle ), но текущая сигнатура метода не позволяет ему это сделать:

MyObjectStore carsStore = . ; Map cars = new HashMap(2); cars.put(1L, new Car("Audi", "A6", 2011)); cars.put(2L, new Car("Honda", "Civic", 2012)); carsStore.putAll(cars); // Ошибка компиляции. 

Чтобы снять это бессмысленное ограничение, мы, как и в предыдущем примере, расширяем сигнатуру нашего интерфейсного метода, используя wildcard ? extends T для типа элемента коллекции:

void putAll(Map entries); 

Запись Map буквально означает «мапка с ключами типа K или любого из подтипов K и со значениями типа V или любого из подтипов V».

Принцип PECS — Producer Extends Consumer Super

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

Этот принцип Joshua Bloch называет PECS (Producer Extends Consumer Super), а авторы книги Java Generics and Collections (Maurice Naftalin, Philip Wadler) — Get and Put Principle. Но давайте остановимся на PECS, запомнить проще. Этот принцип гласит:

Если метод имеет аргументы с параметризованным типом (например, Collection или Predicate ), то в случае, если аргумент — производитель (producer), нужно использовать ? extends T , а если аргумент — потребитель (consumer), нужно использовать ? super T .

Производитель и потребитель, кто это такие? Очень просто: если метод читает данные из аргумента, то этот аргумент — производитель, а если метод передаёт данные в аргумент, то аргумент является потребителем. Важно заметить, что определяя производителя или потребителя, мы рассматриваем только данные типа T.

В нашем примере Predicate — это потребитель (метод getAll(Predicate) передаёт в этот аргумент данные типа T), а Map — производитель (метод putAll(Map) читает данные типа T — в данном случае под T подразумевается K и V — из этого аргумента).

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

С возвращаемыми значениями тоже ничего делать не нужно — никакого удобства использование wildcard-ов в этом случае пользователю не принесёт, а лишь вынудит его использовать wildcard-ы в собственном коде.

Вооружившись PECS-принципом, мы можем теперь пройтись по всем методам нашего MyObjectStore интерфейса и сделать улучшения там, где это требуется. Методы put(K, V) и get(K) улучшений не требуют (т.к. они не имеют аргументов с параметризованным типом); методы putAll(Map) и getAll(Predicate) мы уже и так улучшили, дальше некуда; а вот метод getAll(Collection) имеет аргумент-производитель с параметризованным типом, который мы можем расширить. Вместо

Map getAll(Collection keys); 
Map getAll(Collection keys); 

и радуемся новому, более удобному API! (Заметьте, возвращаемое значение мы не трогаем!)

Другие примеры потребителя и производителя

Производителями могут быть не только коллекции. Самый очевидный пример производителя — это фабрика:

interface Factory  < /** * Создаёт новый экземпляр объекта заданного типа. * * @param args Аргументы. * @return Новый объект. */ T create(Object. args); >

Хорошим примером аргумента, являющегося и производителем, и потребителем, будет аргумент вот такого типа:

interface Cloner  < /** * Клонирует объект. * * @param obj Исходный объект. * @return Копия. */ T clone(T obj); >

Коллекция может быть потребителем в случае, если это ouput-коллекция, в которую метод складывает результат своей работы (хотя такой стиль в Java редко используется и считается плохим тоном).

Заключение

В этой статье мы познакомились с принципом PECS (Producer Extends Consumer Super) и научились его применять при разработке API на Java. Как показывает практика, даже в самых продвинутых программистских конторах об этом принципе некоторые разработчики не знают, и в результате проектируют не совсем удобное API. Но, к счастью, исправляются подобные ошибки очень легко, а запомнив мнемонику PECS однажды, вы уже просто не сможете не пользоваться ей в дальнейшем.

Литература
  1. Joshua Bloch — Effective Java (2nd Edition)
  2. Maurice Naftalin, Philip Wadler — Java Generics and Collections

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *