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);
Generics. Wildcards (Маски)
Термин Wildcard Parameters ( wildcards ) в разных источниках переводится по-разному: метасимвольные аргументы, подстановочные символы, групповые символы, специальные символы, универсальные символы, шаблоны, маски и т.д. Мы будем использовать маску , (если удобно, можете их запомнить без перевода — просто wildcard ).
Вот такая строка кода не скомпилируется
List intList = new ArrayList();
и будет следующая ошибка
Error:() java: incompatible types: java.util.ArrayList cannot be converted to java.util.List
Но есть возможность похожей реализации:
List intList = new ArrayList();
Под маской мы будем понимать вот эту штуку – .
А сейчас пример кода, использующего маску и пригодного к компиляции:
import java.util.ArrayList; import java.util.List; class WildCardsDemo < private static void printList(List >list) < System.out.println("Начало нового списка: "); for (Object l : list) < System.out.println(" + l + ">"); > > public static void main(String[] args) < ListintList = new ArrayList<>(); intList.add(7); intList.add(777); printList(intList); List strList = new ArrayList<>(); strList.add("7"); strList.add("любая строка"); strList.add("777"); printList(strList); > >
В результате в консоль выведется следующее:
Начало нового списка: Начало нового списка:
Метод printList принимает список, для которого в сигнатуре использована маска:
static void printList(List list)
И этот метод работает для списков с любыми типами данных (в примере Integer и String ).
Однако, такой код не скомпилируется:
List intList = new ArrayList(); intList.add(666.66d);
и будет следующая ошибка
Error:(19, 16) java: no suitable method found for add(double) method java.util.Collection.add(java.lang.Integer) is not applicable (argument mismatch; double cannot be converted to java.lang.Integer) method java.util.List.add(java.lang.Integer) is not applicable (argument mismatch; double cannot be converted to java.lang.Integer)
Почему не компилируется? При использовании маски мы сообщаем компилятору, чтобы он игнорировал информацию о типе, т.е. — неизвестный тип. При каждой попытке передачи аргументов дженерик-типа компилятор Java пытается определить тип переданного аргумента. Однако теперь мы используем метод add() для вставки элемента в список.
При использовании маски мы не знаем, какого типа аргумент может быть передан. Тут вновь видна возможность ошибки, т.к. если бы добавление было возможно, то мы могли бы попытаться вставить в наш список, предназначенный для чисел, строковое значение.
Во избежание этой проблемы, компилятор не позволяет вызывать методы, которые могут добавить невалидный тип — например, добавить значение типа Double, с которым мы потом попробуем работать как с Integer.
results matching » «
No results matching » «
Практика применения Wildcards в Java: от простых Generic типов до подстановочных символов
Подстановочные символы Wildcards сегодня используются в большей степени для разработки библиотек и иногда в создании бизнес-приложений. Действительно мощный инструмент зачастую вызывает затруднение даже у senior программистов. Эксперт в области тестирования ПО, тренер Luxoft Training Денис Цыганов рассказал, в чем суть использования Wildcards и Generic в Java.
В чем суть
После появления Collection API в близком к современному виде разработчики Sun Microsystems (в дальнейшем и Oracle) искали решение для упрощения проверки безопасности по типам (typesafe) и ситуаций «загрязнения кучи» (heap pollution). Так появились Generic (обобщения), сделавшие возможным обнаружение несоответствия типов на этапе компиляции. До реализации этого механизма разработчики «ловили» указанные ошибки при непосредственном запуске приложения. В итоге, проблемы иногда могли впервые проявляться спустя годы использования продукта.
Разработчики Java внедряли Generics в существующую экосистему. Это заставило Oracle обеспечить обратную совместимость. Был выбран такой вариант реализации «дженериков», при котором все проверки выполнялись на этапе компиляции, а после происходило стирание типов (type erasure). То есть исполняемый код (скомпилированный с type erasure) ничем не отличается от скомпилированного кода без Generics. Следовательно, существующий не требовал изменения или перекомпиляции.
Как следствие такого решения, generic типы – инвариантны. Даже если generic типы находятся в отношении наследования, производные от этих типов в отношении наследования не находятся.
К примеру, следующая конструкция вызовет ошибку компиляции:
ArrayList list = new ArrayList();
Т.к. ArrayList не является суперклассом для ArrayList.
Это не всегда удобно, так как разработчикам часто хочется иметь метод с параметром, содержащим generic тип, который является более общим по отношения к используемым generic типам. Ожидая, что метод “охватит” эти типы. Например:
void doSomethingWithList(List list)
На первый взгляд, логично, что такой метод мог бы сортировать любые списки содержащие любых потомков Numbers, типа
Для начала попробуем обойтись без Wildcards. В решении данного вопроса разработчику не поможет стандартный для подобных случаев механизм перегрузки методов. Следуя этому подходу можно написать несколько методов:
void doSomethingWithList(List list)
void doSomethingWithList(List list)
void doSomethingWithList(List list)
К сожалению, такой код не скомпилируется благодаря тому же стиранию типов. Т.к. после все три метода имеют одинаковую сигнатуру:
void doSomethingWithList(List list)
Мы можем поиграть с названиями методов, но их тело, скорее всего, будет выглядеть абсолютно одинаково (или очень похоже), а это нарушает один из главных принципов разработки – DRY (Don’t Repeat Yourself).
Эту проблему можно решить с помощью Wildcards. Символ «?» может быть использован вместо Generic типа, обозначая любой тип. Также можно задать границы для семейства типов, определенных обобщенным классом, делая api понятным и изящным. Wildcards способен ограничивать тип вниз (extends) или вверх (super).
Важно понимать, что Wildcard тип мо жет иметь только ссылка, но не сам объект. То есть он может быть задан для локальной переменной, поля класса, параметра метода или типа возвращаемого значения, но не может быть использован в операторе new :
new ArrayList(); // не скомпилируется
Примеры объявления переменных c Wildcards:
Первая и вторая ссылки — ковариантны. Третья — контравариантна.
Что это означает?
Ковариантность — перено с отн ошения наследования с исходного типа на производный (обобщенный) тип. Т.е. List является суперклассом для List , List , List и даже для List и ему подобных.
Такая ковариантность на практике делает невозможным добавление элементов в список адресуемый через Wildcard тип. И действительно, какого же типа может быть добавляемый элемент? Допустим, у нас есть переменная типа List . При попытке добавления объекта любого типа, нас ждет неудача, т.к. за этой ссылкой может находиться объект типа List , List или даже List . В первый невозможно положить BigDecimal , а во второй Integer . И оба не примут ссылку типа Number . А т.к. настоящий generic тип объекта по такой ссылке компилятор определить не может, это означает, что нет таких объектов, которые можно записать в любой из них. Единственное исключение – null (может быть добавлен, т.к. может выступать как любой тип).
Контравариантность — обращение отношения наследования для производных типов. Т.е. List не является суперклассом для List , зато является для List , List , List.
В такой список, очевидным образом, можно добавить только объекты типа ограничителя. Т.е. List примет Number, но не примет Object по вышеуказанной причине. Если задуматься, то список также примет любые типы, расширяющие Number , т.к. любой из этих типов совместим с Number (помните, мы говорим о типе объекта, а не о Generic типе).
Примечание 1: обозначенные ограничения относятся только к Generic типу. Так, например, следующий код скомпилируется без проблем:
List list = new ArrayList();
Для краткости можно опускать generic тип при создании объекта:
List list = new ArrayList<>();
Эта возможность появилась в версии 1.7 и называется Diamonds (угловые скобки напоминают бриллиант).
Примечание 2: Массивы в отличии от generics ковариантны. Иными словами, если Объекты находятся в отношении наследования, то и их производные (массивы), находятся в том же отношении. Т.е. следующий код корректен:
Object[] array = new Integer[5];
Практика применения
Чтобы понять суть Generic разберем пример программирования до и после их появления. Мир без «генериков» выглядит так:
// Список объектов без ограничения по типу
//теперь он называется raw type List
List listOfStrings = new ArrayList();
//В него можно добавить элементы любого объектного типа
Для извлечения элемента нужно знать точный тип (для применения оператора Cast).
String string = (String) listOfStrings.get(0);
//есть шанс получить ClassCastException, если мы не угадали с типом
String secondString = (String) listOfStrings.get(1);
Эта ошибка проявится в среде выполнения (Runtime) только во время выполнения конкретной строки кода (если соответствующая ветка исполняется не часто, велик шанс эту ошибку не обнаружить на этапе разработки).
С появлением «генериков» код выглядит иначе. Ошибки проявляются намного раньше – во время разработки, а стоимость их исправления значительно ниже. Кроме того, при извлечении элемента из списка нет необходимости приводить тип, а также знать (читай – искать по коду) точный тип — он будет предложен IDE или упомянут компилятором.
Список элементов определенного типа – String в примере ниже.
List trueListOfStrings = new ArrayList<>();
//it accepts String parameter only
В случае использования несовместимого типа – получим ошибку компиляции.
String alwaysCorrectType = trueListOfStrings.get(0);
//Возможность получить ClassCastException пропадает вместе с Cast операцией.
Теперь рассмотрим задачу отбора животных из существующего списка по определенному условию (conditional copy). В этом примере мы уже будем применять Wildcards.
Заранее обозначим требования к нашему API:
- Метод должен копировать некоторые объекты из исходного списка в результирующий;
- Оба списка уже существуют и принимаются в параметрах;
- Для принятия решения о копировании объекта необходимо вызывать методы самого объекта для получения необходимой информации (доступность методов объекта).
- Метод должен давать возможность копирования в списки обобщенные любым супертипом (гибкость).
Для начала определим типы животных:
boolean result = false;
//имитация некоторой логики
class Cat extends Animal
//переопределяем логику isNeeded() для класса Cat
Подготовим несколько списков для тестирования:
List objects = new ArrayList<>();
List animals = new ArrayList<>();
List cats = new ArrayList<>();
Теперь попробуем решить задачу «в лоб»:
void conditionalCopy(List dst, List src)
/* при такой сигнатуре метода нет возможности вызвать методы класса Animal, т.к. тип T в рамках метода не ограничен и может представлять любой тип */
Итак, у нас проблема с реализацией требования 3.
void conditionalCopy(List dst, List src)
List list = src.stream()
Проблема решена, пробуем копировать:
conditionalCopy(animals, cats); //1
conditionalCopy(animals, animals); //2
conditionalCopy(objects, cats); //3
Строки 1 и 3 вызывают проблему компиляции. Мы помним, что «дженерики» и поэтому требование 4 не реализовано. Давайте решать эту проблему:
void conditionalCopy(List dst, List src)
List list = src.stream()
conditionalCopy(animals, cats); //1
conditionalCopy(animals, animals); //2
conditionalCopy(objects, cats); //3
На этот раз проблема компиляции только в одной строке (3). Т ограничен типом Animal , в результате копировать в List невозможно.
Самое время применить контравариантность:
List list = src.stream()
Последняя проверка подтверждает правильность решения. Все строки компилируются и отлично работают.
Теперь реализованы все требования.
Кстати, наш API не позволяет некорректные варианты копирования вроде таких:
conditionalCopy(cats, animals); //4
conditionalCopy(cats, objects); //5
Обе строки (4) и (5) вызывают ошибку компиляции.
Несмотря на кажущуюся простоту Wildcards стали камнем преткновения для многих программистов. Я думаю, что использование символа «?» в каком-то смысле запутывает разработчика, создавая ощущение, что объект готов работать с любым типом в любой момент. Однако это не так. «Любой» — это не «каждый» и, тем более, не «все сразу». В данном контексте правильно было бы сказать «какой-то конкретный, из допустимого множества типов». Для их правильного определения необходима практика. Однако для полного понимания разработчикам необходимо узнать основные механизмы стирания типов, правильно использовать ограничения extends и super, принцип PECS, raw type, проблему heap pollution, и механизм Generic в целом.
- Об авторе
- Недавние публикации
Digital-Report.ru — информационно-аналитический портал, который отслеживает изменения цифровой экономики. Мы описываем все технологические тренды, делаем обзоры устройств и технологических событий, которые влияют на жизнь людей.
Digital Report недавно публиковал (посмотреть все)
- Флагманский смартфон OnePlus 12 получит деревянный корпус — 22/11/2023 18:50
- Турецкий Steam переходит на доллары: что это значит для игроков — 22/11/2023 17:26
- Сотрудники OpenAI совету директоров: «Вы не обладаете компетенцией для управления компанией» — 20/11/2023 20:06
Использование wildcard imports в Java. Хорошо или плохо?
В этой заметке мы обсудим преимущества и недостатки использования импорта wildcard Import в Java.
23 июля 2022 · 4 минуты на чтение
Разработчики Java используют подстановочный знак ( * ) в операторах import для добавления всех классов из определенного пакета. Но в ходе ревью большинство из вас, возможно, просили убрать этот подстановочный знак импорта и добавить полное имя класса. Разберемся почему не стоит использовать знак подстановки?
Но прежде, давайте вспомним, что полное имя класса, помимо названия, содержит также пакет, например: java.util.List . Такая запись позволяет нам иметь классы с одинаковыми именами, которые находятся в разных пакетах.
package com.example; public class WithoutImport < public static void main(String[] args) < java.util.List myList = new java.util.ArrayList(); >>
Однако, полная запись весьма непрактична, поэтому в Java существует оператор импорта – import . Операторы импорта объявляют компилятору источник имен классов, статических переменных и статических имен методов, используемых в коде. Один раз написали полное имя класса, а дальше используем только название класса.

Также читаемость ухудшается, потому что разработчик не получает четкого представления о полном пути классов, используемых в коде.
Однако, это тоже не является проблемой. Достаточно удерживая Ctrl (cmd) навести курсор на класс и вы увидите пакет, в котором этот класс размещается.

Таким образом проблемы «чистого кода» более не актуальны.
Настройки в Idea
Intellij Idea автоматически сворачивает импорты, когда полные импорты из какого-то пакета достигают заданного количества. Эта опция настраивается в раделе Preferences > Editor > Code Style > Java во вкладке imports.

Чтобы отключить автоматическое сворачивание импортов достаточно указать число 999. Вот и все, теперь Idea не будет автоматически сворачивать импорты.
Резюмирую
Использование подстановочных импортов никак не повлияет на производительность программы во время выполнения, но может немного повлиять на производительность во время компиляции.
Также при обновлении библиотек вы можете столкнуться с проблемой конфликта имен. Шанс этой проблемы не велик, но и не нулевой.
Что касается «чистого кода», то с использованием современных IDE эта проблема не является актуальной.
