- What are wildcards arguments in Generics In Java?
- Example
- Output
- Wildcards
- Upper-bounded wildcards
- Example
- Output
- Example
- Compile time error
- Lower-Bounded wildcards
- Example
- Output
- Example
- Compile time error
- Unbounded wildcards
- Example
- Output
- Compile time error
- Использование generic wildcards для повышения удобства Java API
- Исходный API
- Использование ? super T
- Использование ? extends T
- Принцип PECS — Producer Extends Consumer Super
- Заключение
What are wildcards arguments in Generics In Java?
Generics is a concept in Java where you can enable a class, interface and, method, accept all (reference) types as parameters. In other words it is the concept which enables the users to choose the reference type that a method, constructor of a class accepts, dynamically. By defining a class as generic you are making it type-safe i.e. it can act up on any datatype.
To define a generic class you need to specify the type parameter you are using in the angle brackets “<>” after the class name and you can treat this as datatype of the instance variable an proceed with the code.
Example
class Student < T age; Student(T age)< this.age = age; >public void display() < System.out.println("Value: "+this.age); >> public class GenericsExample < public static void main(String args[]) < Studentstd1 = new Student(25.5f); std1.display(); Student std2 = new Student("25"); std2.display(); Student std3 = new Student(25); std3.display(); > >
Output
Value: 25.5 Value: 25 Value: 25
Wildcards
Instead of the typed parameter in generics (T) you can also use “?”, representing an unknown type. You can use a wild card as a −
The only restriction on wilds cards is that you cannot it as a type argument of a generic method while invoking it.
Java provides 3 types of wild cards namely upper-bounded, lower-bounded, un-bounded.
Upper-bounded wildcards
Upper bounds in wild cards is similar to the bounded type in generics. Using this you can enable the usage of all the subtypes of a particular class as a typed parameter.
For example, if want to accept a Collection object as a parameter of a method with the typed parameter as a sub class of the number class, you just need to declare a wild card with the Number class as upper bound.
To create/declare an upper-bounded wildcard, you just need to specify the extends keyword after the “?” followed by the class name.
Example
Following Java example demonstrates the creation of the upper-bounded wildcard.
import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.HashSet; public class UpperBoundExample < public static void sampleMethod(Collectioncol) < for (Number num: col) < System.out.print(num+" "); >System.out.println(""); > public static void main(String args[]) < ArrayListcol1 = new ArrayList(); col1.add(24); col1.add(56); col1.add(89); col1.add(75); col1.add(36); sampleMethod(col1); List col2 = Arrays.asList(22.1f, 3.32f, 51.4f, 82.7f, 95.4f, 625.f); sampleMethod(col2); HashSet col3 = new HashSet(); col3.add(25.225d); col3.add(554.32d); col3.add(2254.22d); col3.add(445.21d); sampleMethod(col3); > >
Output
24 56 89 75 36 22.1 3.32 51.4 82.7 95.4 625.0 25.225 554.32 2254.22 445.21
If you pass a collection object other than type that is subclass of Number as a parameter to the sampleMethod() of the above program a compile time error will be generated.
Example
import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.HashSet; public class UpperBoundExample < public static void sampleMethod(Collectioncol) < for (Number num: col) < System.out.print(num+" "); >System.out.println(""); > public static void main(String args[]) < ArrayListcol1 = new ArrayList(); col1.add(24); col1.add(56); col1.add(89); col1.add(75); col1.add(36); sampleMethod(col1); List col2 = Arrays.asList(22.1f, 3.32f, 51.4f, 82.7f, 95.4f, 625.f); sampleMethod(col2); HashSet col3 = new HashSet(); col3.add("Raju"); col3.add("Ramu"); col3.add("Raghu"); col3.add("Radha"); sampleMethod(col3); > >
Compile time error
UpperBoundExample.java:31: error: incompatible types: HashSet cannot be converted to Collection sampleMethod(col3); ^ Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output 1 error
Lower-Bounded wildcards
upper-bounded wildcard enables the usage of all the subtypes of a particular class as a typed parameter.
Similarly, if we use the lower-bounded wildcards you can restrict the type of the “?” to a particular type or a super type of it.
For example, if want to accept a Collection object as a parameter of a method with the typed parameter as a super class of the Integer class, you just need to declare a wildcard with the Integer class as lower bound.
To create/declare a lower-bounded wildcard, you just need to specify the super keyword after the “?” followed by the class name.
Following Java example demonstrates the creation of the Lower-bounded wildcard.
Example
import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Iterator; public class LowerBoundExample < public static void sampleMethod(Collectioncol) < Iterator it = col.iterator(); while (it.hasNext()) < System.out.print(it.next()+" "); >System.out.println(""); > public static void main(String args[]) < ArrayListcol1 = new ArrayList(); col1.add(24); col1.add(56); col1.add(89); col1.add(75); col1.add(36); sampleMethod(col1); List col2 = Arrays.asList(22.1f, 3.32f, 51.4f, 82.7f, 95.4f, 625.f); sampleMethod(col2); > >
Output
24 56 89 75 36 22.1 3.32 51.4 82.7 95.4 625.0
If you pass a collection object other of type other than Integer and its super type as a parameter to the sampleMethod() of the above program a compile time error will be generated.
Example
import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Iterator; import java.util.HashSet; public class LowerBoundExample < public static void sampleMethod(Collectioncol) < Iterator it = col.iterator(); while (it.hasNext()) < System.out.print(it.next()+" "); >System.out.println(""); > public static void main(String args[]) < ArrayListcol1 = new ArrayList(); col1.add(24); col1.add(56); col1.add(89); col1.add(75); col1.add(36); sampleMethod(col1); List col2 = Arrays.asList(22.1f, 3.32f, 51.4f, 82.7f, 95.4f, 625.f); sampleMethod(col2); HashSet col3 = new HashSet(); col3.add(25.225d); col3.add(554.32d); col3.add(2254.22d); col3.add(445.21d); sampleMethod(col3); > >
Compile time error
LowerBoundExample.java:34: error: incompatible types: HashSet cannot be converted to Collection sampleMethod(col3); ^ Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output 1 error
Unbounded wildcards
An unbounded wildcard is the one which enables the usage of all the subtypes of an unknown type i.e. any type (Object) is accepted as typed-parameter.
For example, if want to accept an ArrayList of object type as a parameter, you just need to declare an unbounded wildcard.
To create/declare a Unbounded wildcard, you just need to specify the wild card character “?” as a typed parameter within angle brackets.
Example
Following Java example demonstrates the creation of the Unbounded wildcard.
import java.util.List; import java.util.Arrays; public class UnboundedExample < public static void sampleMethod(Listcol) < for (Object ele : col) < System.out.print(ele+" "); >System.out.println(""); > public static void main(String args[]) < ArrayListcol1 = new ArrayList(); col1.add(24); col1.add(56); col1.add(89); col1.add(75); col1.add(36); sampleMethod(col1); ArrayList col2 = new ArrayList(); col2.add(24.12d); col2.add(56.25d); col2.add(89.36d); col2.add(75.98d); col2.add(36.47d); sampleMethod(col2); > >
Output
24 56 89 75 36 24.12 56.25 89.36 75.98 36.47
If you pass an List object created from arrays (contains elements of primitive type) a compile time error will be generated.
import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class UnboundedExample < public static void sampleMethod(Listcol) < for (Object ele : col) < System.out.print(ele+" "); >System.out.println(""); > public static void main(String args[]) < ArrayListcol1 = new ArrayList(); col1.add(24); col1.add(56); col1.add(89); col1.add(75); col1.add(36); sampleMethod(col1); ArrayList col2 = new ArrayList(); col2.add(24.12d); col2.add(56.25d); col2.add(89.36d); col2.add(75.98d); col2.add(36.47d); sampleMethod(col2); List col2 = Arrays.asList(22.1f, 3.32f, 51.4f, 82.7f, 95.4f, 625.f); sampleMethod(col2); > >
Compile time error
UnboundedExample.java:27: error: variable col2 is already defined in method main(String[]) List col2 = Arrays.asList(22.1f, 3.32f, 51.4f, 82.7f, 95.4f, 625.f); ^ 1 error
Использование 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); . // и так далее >
Интерфейс выглядит вполне адекватно и логично, пользователь без проблем может написать простой код для работы с хранилищем:
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 для типа элемента коллекции:
Запись 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! (Заметьте, возвращаемое значение мы не трогаем!)
Другие примеры потребителя и производителя
Производителями могут быть не только коллекции. Самый очевидный пример производителя — это фабрика:
Хорошим примером аргумента, являющегося и производителем, и потребителем, будет аргумент вот такого типа:
Коллекция может быть потребителем в случае, если это ouput-коллекция, в которую метод складывает результат своей работы (хотя такой стиль в Java редко используется и считается плохим тоном).
Заключение
В этой статье мы познакомились с принципом PECS (Producer Extends Consumer Super) и научились его применять при разработке API на Java. Как показывает практика, даже в самых продвинутых программистских конторах об этом принципе некоторые разработчики не знают, и в результате проектируют не совсем удобное API. Но, к счастью, исправляются подобные ошибки очень легко, а запомнив мнемонику PECS однажды, вы уже просто не сможете не пользоваться ей в дальнейшем.