IS-A Relationship
In object-oriented programming, IS-A relationship is as common as breathing for programmers. See the class hierarchy diagram below.diagram 1: IS-A relationship |
Nothing much to say and the code line in diagram 1 above is 100% valid because Phone IS A Device. When the classes above become type argument of parameterized type (e.g, List<Phone>, List<Device>, List<Camera> in diagram 2 below), the IS-A relationship is still applied to determine what type of object can be set/put into the object with parameterized type.
diagram 2: Type argument determines what object can be accepted |
Common Misunderstanding
However, when looking into the relationship of the List<Phone>, List<Device>, List<Camera> we always subconsciously think that List<Device> is the parent of List<Phone> and List<Camera>. This is not true. In fact, three of them are referring to the same class, List. In the List class hierarchy the type Device, Phone and Camera don't even exist.This is the very common misunderstanding in Java Generics yet the important concept to learn. You have to aware that the code such as the code line in diagram 3 below is invalid due to incompatible type compilation error.
diagram 3: Common misunderstanding |
Wildcard
Is that means they do not have a relationship at all? Not really, they do have a relationship indirectly. Thanks to the Wildcard(appears as ?) which make this relationship happen. In short,- The wildcard is a special kind of type argument with unknown type,
- The wildcard type (e.g. List<?>) is type-safe guaranteed by Java compiler like other parameterized type,
- The wildcard type is an abstract parameterized type. The actual parameterized type of a wildcard typed variable could be any other normal parameterized type. See the code line in diagram 4 below. The actual type of list variable is List<Phone>.
diagram 4: Common parent of parameterized types |
In general, three of them are having the same parent which is List<?>.
This is not the end of the story. Besides unbounded wildcard type as you have seen in diagram 4, the wildcard type can also appear in 2 other forms, upper-bounded wildcard type (<? extends ...>), and lower-bounded wildcard type (<? super ...>). This makes the wildcard types hierarchy (diagram 5) even more interesting.
diagram 5: Wildcard types hierarchy |
Upper-bounded Wildcard Type
The upper-bounded wildcard type hierarchy could be illustrated in the following diagram 6.
Take a look at the List<? extends Phone>. This wildcard type basically claims that "whichever List with type argument IS A Phone is under my territory". In other words, any List with type argument IS A Phone become a subtype of List<? extends Phone>.
If we look at the List<? extends Device>, not only the actual parameterized type List<Phone>, List<Camera> and List<Device>, the abstract parameterized type List<? extends Phone> and List<? extends Camera> also is its subtypes. Because all of their type argument IS A Device.
The assignment statements in the following code snippet are all valid.
Now, let's recap what does the actual parameterized type could contain. This is important because together the type-safe principle, they make up the upper-bounded wildcard type characteristics. See diagram 7 below.
If we look at the List<? extends Device>, not only the actual parameterized type List<Phone>, List<Camera> and List<Device>, the abstract parameterized type List<? extends Phone> and List<? extends Camera> also is its subtypes. Because all of their type argument IS A Device.
The assignment statements in the following code snippet are all valid.
List<Phone> phones = new ArrayList<>(); List<Camera> cameras = new ArrayList<>(); List<Device> devices = new ArrayList<>(); List<? extends Phone> phonesWildcard; List<? extends Camera> cameraWildcard; List<? extends Device> deviceWildcard; // Assignments phonesWildcard = phones; cameraWildcard = cameras; deviceWildcard = devices; deviceWildcard = phones; deviceWildcard = cameras; deviceWildcard = phonesWildcard; deviceWildcard = cameraWildcard;
Now, let's recap what does the actual parameterized type could contain. This is important because together the type-safe principle, they make up the upper-bounded wildcard type characteristics. See diagram 7 below.
diagram 7: What makes up upper-bounded wildcard type characteristics |
Question 1: What type of object is eligible to be put into List<? extends Device>?
The answer is none. The actual parameterized type of List<? extends Device> is not clear-cut. We never know if its actual parameterized type is List<Phone>, List<Camera> or List<Device>. See the following code snippet.
List<Phone> phones = new ArrayList<>(); phones.add(new Phone()); List<? extends Device> deviceWildcard; deviceWildcard = phones; // The actual type is List<Phone> that contains Phone object deviceWildcard.add(new Camera()); // break type-safe. Compilation error!
The actual parameterized type of deviceWildcard above is List<Phone>. Although Camera is a Device, putting it into deviceWildcard breaks the type-safe principle. Java compiler will make sure we get a compilation error for that.
Question 2: What type of object can be retrieved from List<? extends Device>?
The answer is Device. Look at the content object of all the actual parameterized type covered under List<? extends Device> in diagram 7, they have one common fact - they are having the same parent, Device. This means it is safe to assume that whatever object get from the List<? extends Device> is definitely IS A Device.List<Phone> phones = new ArrayList<>(); phones.add(new Phone()); List<? extends Device> deviceWildcard; deviceWildcard = phones; // The actual type is List<Phone> that contains Phone object Device device = deviceWildcard.get(0); // OK. Phone is a Device
Example of using Upper-bounded Wildcard Type
If you wonder how could the upper-bounded wildcard type help, here is an example. Given the Device classes below (well, the methods implementation are not important.)
public class Device { public void on() {...} public void off() {...} } public class Phone extends Device { public void call() {...} } public class Camera extends Device { public void shootPhoto() {...} }
Every device can be on() and off(). Now, we need a method to test every device simply by turning it on then off.
public static void testDevices(List<Device> devices) { for (Device device : devices) { device.on(); device.off(); } } public static void main(String[] args) { List<Device> devices = new ArrayList<>(); devices.add(new Phone()); devices.add(new Camera()); testDevices(devices); // OK }
The codes above are valid and works fine. We can test a devices list which its content objects could be Phone or Camera typed. Now, how about if we pass phones list or cameras list to the testDevices() method as below?
List<Phone> phones = new ArrayList<>(); phones.add(new Phone()); testDevices(phones); // Compilation error! Incompatible types. List<Camera> cameras = new ArrayList<>(); cameras.add(new Camera()); testDevices(cameras); // Compilation error! Incompatible types.
Logically, testDevices() should able to accept phones and cameras list. After all, Phone and Camera are Device which can be turned on() and off(). However, from the code, Java Compiler is expecting the actual parameterized type with type argument Device. This restriction causing phones and cameras which logically should be accepted by testDevices() become compilation errors.
In this case, we can use the upper-bounded wildcard type to loose the type restriction. See the following updated testDevices() below.
The method parameter becomes an abstract parameterized type and able to accept actual parameterized type as long as its type argument IS A Device.
In this case, we can use the upper-bounded wildcard type to loose the type restriction. See the following updated testDevices() below.
public static void testDevice(List<? extends Device> devices) { for (Device device : devices) { device.on(); device.off(); } }
The method parameter becomes an abstract parameterized type and able to accept actual parameterized type as long as its type argument IS A Device.
Lower-bounded Wildcard Type
The lower-bounded wildcard type hierarchy could be illustrated in the following diagram 8.diagram 8: Lower-bounded wildcard type hierarchy |
If we look at the List<? super Phone>, this time, the actual parameterized type List<Phone> and List<Device> as well as the abstract parameterized type List<? super Device> meet the acceptance criteria and hence, they are subtypes of List<? super Phone>.
The assignment statements in the following code snippet are all valid.
List<Phone> phones = new ArrayList<>(); List<Camera> cameras = new ArrayList<>(); List<Device> devices = new ArrayList<>(); List<? super Phone> phonesWildcard; List<? super Camera> cameraWildcard; List<? super Device> deviceWildcard; // Assignments deviceWildcard = devices; phonesWildcard = phones; phonesWildcard = devices; phonesWildcard = deviceWildcard; cameraWildcard = cameras; phonesWildcard = devices; cameraWildcard = deviceWildcard;
Again, let's recap what does the actual type could contain. This is important because together the type-safe principle, they make up the lower-bounded wildcard type characteristics. See diagram 9 below.
diagram 9: What makes up lower-bounded wildcard type characteristics |
Question 1: What type of object is eligible to be put into List<? super Phone>?
The answer is any object that IS A Phone. Refer to diagram 9, the actual parameterized type for List<? super Phone> is List<Phone> and List<Device>. Both can definitely keep any object that IS A Phone.
The answer is the highest type - Object. This is because the actual parameterized type of List<? super Phone> is not clear-cut. It could be List<Phone> or List<Device>. Whereby the List<Device> could contain an object with type other than Phone. See the following code snippet.Question 2: What type of object can be retrieved from List<? super Phone>?
List<Device> devices = new ArrayList<>(); devices.add(new Camera()); // devices list that contains Camera object List<? super Phone> phonesWildcard; phonesWildcard = devices; Phone phone = phonesWildcard.get(0); // Compilation error! Incompatible types.
phonesWildcard never able to confirm whether its content object is a Phone. Doing that will get a compilation error. Therefore, the safest way is to return the highest type - Object.
Example of using Lower-bounded Wildcard Type
If you also wonder how could the lower-bounded wildcard help, here is an example. Let's say we need respective methods to create and test a specific device 's functionality then put it into the given List parameter.
public static void packPhone(List<Phone> phones) { Phone phone = new Phone(); phone.call(); // test Phone functionality phones.add(phone); // put Phone into given list } public static void packCamera(List<Camera> cameras) { Camera camera = new Camera(); camera.shootPhoto(); // test Camera functionality cameras.add(camera); // Put Camera into given list } public static void main(String[] args) { List<Phone> phonesList = new ArrayList<>(); packPhone(phonesList); List<Camera> camerasList = new ArrayList<>(); packCamera(camerasList); }
The codes above are valid and works fine. Now, how about if we have a devices list that would like to pack Phone and Camera?
List<Device> devicesList = new ArrayList<>(); packPhone(devicesList); // Compilation error! incompatible types. packCamera(devicesList); // Compilation error! incompatible types.
By right, we can put Phone and Camera object into devicesList. However, from the code, Java Compiler is expecting the actual parameterized type with type argument Phone for packPhone() and Camera for packCamera(). This restriction makes both methods can't accept deviceList.
In this case, we can use the lower-bounded wildcard type to loose the type restriction. See the following updated packPhone() and packCamera() below.
public static void packPhone(List<? super Phone> phonesWildcard) { Phone phone = new Phone(); phone.call(); // test Phone functionality phonesWildcard.add(phone); // put Phone into given list } public static void packCamera(List<? super Camera> camerasWildcard) { Camera camera = new Camera(); camera.shootPhoto(); // test Camera functionality camerasWildcard.add(camera); // Put Camera into given list }
The method parameter becomes an abstract parameterized type and able to accept the parameterized type as long as its type argument IS A super-type of Phone and Camera respectively.
Unbounded Wildcard Type
The unbounded wildcard type is an abstract parameterized type and the common parent of all parameterized type, either actual or abstract. It could contain a object of any type (like List<Object>); the actual parameterized type that it referencing to is never clear-cut (like List raw type). However, it is loose restriction compare to the actual parameterized type List<Object> and type-safe compare to raw type List.
Example of using Unbounded Wildcard Type
If you also wonder how could the unbounded wildcard help, here is an example. Let's say we want to compare two devices lists to check if they are holding the same quantity.
public static boolean isContainSameQuantity(List<Device> list1, List<Device> list2) { return list1.size() == list2.size(); } public static void main(String[] args) { List<Device> devices1 = new ArrayList<>(); List<Device> devices2 = new ArrayList<>(); isContainSameQuantity(devices1, devices2); }
The codes above are valid and works fine. Now, how about if we want the comparison happen between list with different types?
List<Phone> phones = new ArrayList<>(); List<Camera> cameras = new ArrayList<>(); List<Device> devices = new ArrayList<>(); isContainSameQuantity(phones, cameras); // Compilation error! Incompatible type. isContainSameQuantity(phones, devices); // Compilation error! Incompatible type. isContainSameQuantity(devices, cameras); // Compilation error! Incompatible type.
Java Compiler will not let us to do so as it so concern about the type argument, Device that has been assigned to the method parameters of isContainSameQuantity(). In fact, the isContainSameQuantity() implementation does not deal with the object with its type is genericized in List generic type at all. Therefore, we can use unbounded wildcard type to loose the restriction to the max. See the following update version of isContainSameQuantity().
public static boolean isContainSameQuantity(List<?> list1, List<?> list2) { return list1.size() == list2.size(); } public static void main(String[] args) { List<Phone> phones = new ArrayList<>(); List<Camera> cameras = new ArrayList<>(); List<Device> devices = new ArrayList<>(); isContainSameQuantity(phones, cameras); // OK
isContainSameQuantity(phones, devices); // OK isContainSameQuantity(devices, cameras); // OK }
Summary
Wildcard type helps us to make our code type-safe yet flexible. It could be a hard time to pick up this tool but once you familiar with it, it will become a great tool in your hand. Below is the summary of when I should use which wildcard form. Hope it helps.
Upper-bound Wildcard Type <? extend ...>
Upper-bound Wildcard Type <? extend ...>
- All I need is to get a specific type of objects from a generic typed object and I never need to put an object into it.
Lower-bounded Wildcard Type <? super ...>
- All I need is to put a specific type of objects into a generic typed object and I am aware that it will only return the object with the Object type. I take the risk to cast the return object to a specific class.
Unbounded Wildcard Type <?>
- I will not deal with the object with its type is genericized in the generic typed object. And I am aware that it will only return the object with the Object type. I take the risk to cast the return object to a specific class.
References:
https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html
Book: Effective Java 2nd Edition by Joshua Blosh - Item 28