This article is the continuation of the topic “Subtyping and Java” that we started in Part 1 of the series Solid Code Design. In Part 2, we discussed preconditions, postconditions, invariants, Java subtyping and true subtypes. In this article, we will focus on subtyping and Java generics.
Suppose we have designed a class named IntegerPrinter, as follows:
public class IntegerPrinter {
private final Integer element;
IntegerPrinter(Integer e) {
this.element = e;
}
public void printElement() {
System.out.println(this.element);
}
}
Analyzing one requirement, we realize that we need another two classes like IntegerPrinter (one for Double, another for String). Immediately we design the below classes.
public class DoublePrinter {
private final Double element;
DoublePrinter(Double e) {
this.element = e;
}
public void printElement() {
System.out.println(this.element);
}
}
public class StringPrinter {
private final String element;
StringPrinter(String e) {
this.element = e;
}
public void printElement() {
System.out.println(this.element);
}
}
Here, the logical structures of the classes IntegerPrinter, DoublePrinter, and StringPrinter, are the same. The only difference is that they deal with different types of data.
We can abstract out the most relevant part of the classes IntegerPrinter, DoublePrinter, and StringPrinter as follows:
Now we need to generalize the above abstracted code so that it can work for different types. How to do that? We can use Java generic class. We design a generic class named GenericPrinter for the above abstraction with type parameter (also known as type variable) T as follows.
public class GenericPrinter<T> {
private final T element;
GenericPrinter(T t) {
this.element = t;
}
public void printElement() {
System.out.println(this.element);
}
}
Now we will write a PrinterUtil class as shown below.
public class PrinterUtil {
public static void printUsingGenericPrinter(
GenericPrinter<Number> number) {
number.printElement();
}
public static void printAnyNumber(Number number) {
System.out.println(number);
}
}
Client code starts using the PrinterUtil.
Integer i = new Integer(45);
Double d = new Double(5.67);
Float f = new Float(7.89f);
PrinterUtil.printAnyNumber(i);
PrinterUtil.printAnyNumber(d);
PrinterUtil.printAnyNumber(f);
The above client code works perfectly fine because Integer, Double, and Float are subclasses (subtypes too) of Number.
The method PrinterUtlil.printAnyNumber happily accept objects of Integer, Float, and Double. Because each of them extends the superclass Number. Hence we can use a subclass (e.g., Integer) where a superclass Number is expected. We call it subtype polymorphism.
Now consider the below client code-snippet.
GenericPrinter<Integer> intPrinter = new GenericPrinter<Integer>(45);
PrinterUtil.printUsingGenericPrinter(intPrinter);
In this case, compiler gives error at line 2 saying "incompatible types: GenericPrinter<Integer> cannot be converted to GenericPrinter<Number>." Why is it so? Because GenericPrinter<Integer> is not a subtype of GenericPrinter<Number> though Integer is a subtype of Number.
Now we will consider the below design.
The code for the above design is given below.
public interface RootPrinter<T> {
public void printElement();
}
public class GenericPrinter<T> implements RootPrinter<T> {
protected final T element;
GenericPrinter(T t) {
this.element = t;
}
@Override
public void printElement() {
System.out.println(this.element);
}
}
public class MyGenericPrinter<T> extends GenericPrinter<T> {
private static final Logger logger =
Logger.getLogger("MyGenericPrinter");
public MyGenericPrinter(T t){
super(t);
}
@Override
public void printElement() {
System.out.println(this.element);
logger.info("Printing of element is completed");
}
}
We can subtype a generic interface by implementing it. For example, GenericPrinter<T> implements RootPrinter<T>. Similarly, we can subtype a generic class by extending it. For example, MyGenericPrinter<T> extends GenericPrinter<T>. In this case, we can say MyGenericPrinter<Number> is a subtype of GenericPrinter<Number> and GenericPrinter<Number> is a subtype of RootPrinter<Number>.
We have seen that GenericPrinter<Integer> is not a subtype of GenericPrinter<Number> even though Integer is a subtype of Number.
Before going further, we should be aware of a few generics terminolgies.
Now we will focus on improving the design of the printUsingGenericPrinter method in PrinterUtil class.
public class PrinterUtil {
public static void printUsingGenericPrinter(
GenericPrinter<Number> number) {
number.printElement();
}
// some other code
}
We have seen that when we call the printUsingGenericPrinter with GenericPrinter<Integer> or GenericPrinter<Float> or GenericPrinter<Double>, compiler error occurs because they are not a subtype of GenericPrinter<Number>. Parent class of GenericPrinter<Integer> or GenericPrinter<Float> or GenericPrinter<Double> is Object, not GenericPrinter<Number>. But when type argument is same, subtyping generalizes to generics. For example, MyGenericPrinter<Number> is a subtype of GenericPrinter<Number>. That said, we will redesign the printUsingGenericPrinter method in PrinterUtil class so that it can accept GenericPrinter<Integer> or GenericPrinter<Float> or GenericPrinter<Double> etc. We will use the below design.
Don’t worry about the question mark (?) in the above design. We will discuss it next.
Wildcard
For any type T, we can say GenericPrinter<T> is a subtype of GenericPrinter<?>. For example, GenericPrinter<Integer>, GenericPrinter<Double>, GenericPrinter<String> are subtypes of GenericPrinter<?>. This rule is applicable in other classes and interfaces also. In this context, the question mark (?) is called the wildcard. It represents an unknown type.
There are three kinds of wildcards: upper bounded wildcards, unbounded wildcards, and lower bounded wildcards.
In our printUsingGenericPrinter method design, we want to make sure that the method call should work on GenericPrinter<Integer>, GenericPrinter<Float>, GenericPrinter<Double>, and GenericPrinter<Number>. Here we can use upper bounded wildcard. Therefore, we have changed the parameter to GenericPrinter<? extends Number>.
When we write GenericPrinter<?>, we mean “A GenericPrinter of Unknown Type.” When we write GenericPrinter<? extends Number>, we mean “A GenericPrinter of type Number or any subtype of Number.”
Now below client code will work perfectly without any compile-time errors related to type incompatibility.
GenericPrinter<Integer> intPrinter =
new GenericPrinter<Integer>(45);
PrinterUtil.printUsingGenericPrinter(intPrinter);
GenericPrinter<Double> doublePrinter =
new GenericPrinter<Double>(45.67);
PrinterUtil.printUsingGenericPrinter(doublePrinter);
GenericPrinter<Number> numberPrinter
= new GenericPrinter<Number>(Short.MAX_VALUE);
PrinterUtil.printUsingGenericPrinter(numberPrinter);
Now we are going to consider a new method in our PrinterUtil as follows:
The structure “? super Car” is an example of lower bound wildcard.
By GenericPrinter<? super Car>, we mean “A GenericPrinter of type Car or any supertype of Car.” The below client code shows usage of the new method..
GenericPrinter<? super Car> carPrinter1 =
new GenericPrinter<Car>(new Car());
PrinterUtil.printCarUsingGenericPrinter(carPrinter1);
GenericPrinter<? super Car> carPrinter2 =
new GenericPrinter<Vehicle>(new Vehicle());
PrinterUtil.printCarUsingGenericPrinter(carPrinter2);
GenericPrinter<? super Car> carPrinter3 =
new GenericPrinter<Object>(new Object());
PrinterUtil.printCarUsingGenericPrinter(carPrinter3);
GenericPrinter<Vehicle> carPrinter4 =
new GenericPrinter<Vehicle>(new Car());
PrinterUtil.printCarUsingGenericPrinter(carPrinter4);
GenericPrinter<Vehicle> carPrinter5 =
new GenericPrinter<Vehicle>(new Vehicle());
PrinterUtil.printCarUsingGenericPrinter(carPrinter5);
GenericPrinter<Object> carPrinter6 =
new GenericPrinter<Object>(new Object());
PrinterUtil.printCarUsingGenericPrinter(carPrinter6);
GenericPrinter<Car> carPrinter7 =
new GenericPrinter<Car>(new Car());
PrinterUtil.printCarUsingGenericPrinter(carPrinter7);
We will design one more method in PrinterUtil, as shown below.
By GenericPrinter<?>, we mean “A GenericPrinter of unknown type.” Basically it can represent a GenericPrinter of any type. There is no bound on type. The below client code shows usage of the new method named printAythingUsingGenericPrinter.
GenericPrinter<?> numberPrinter =
new GenericPrinter<Number>(Integer.MAX_VALUE);
PrinterUtil.printAnythingUsingGenericPrinter(numberPrinter);
GenericPrinter<?> stringPrinter =
new GenericPrinter<String>("Hello World");
PrinterUtil.printAnythingUsingGenericPrinter(stringPrinter);
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
GenericPrinter<List> listPrinter =
new GenericPrinter<List>(list);
PrinterUtil.printAnythingUsingGenericPrinter(listPrinter);
GenericPrinter<Integer> intPrinter =
new GenericPrinter<Integer>(23);
PrinterUtil.printAnythingUsingGenericPrinter(intPrinter);
We will conclude this article with the below points.
- Integer is a subtype of Number. But GenericPrinter<Integer> is not subtype of GenericPrinter<Number>. However both GenericPrinter<Integer> and GenericPrinter<Number> are subtype of GenericPrinter<?>.
- Both GenericPrinter<? extends Number> and Generic<? super Car> are subtypes of GenericPrinter<?>.
- GenericPrinter<B> is a subtype of GenericPrinter<? extends B>, where B is the upper bound. GenericPrinter<? extends B> is a subtype of GenericPrinter<? extends C>, where B is a subtype of C. And GenericPrinter<? extends C> is a subtype of GenericPrinter<?>.
- GenericPrinter<B> is a subtype of GenericPrinter<? super B>, where B is the lower bound. GenericPrinter<? super B> is a subtype of GenericPrinter<? super C>, where B is a supertype of C. And GenericPrinter<? super C> is a subtype of GenericPrinter<?>.
That’s all for this article. In Part 4 of the series Solid Code Design, we will discuss more on Java Generics.
I have shared knowledge based on my understanding and experiences. If you find any significant errors or want to give me some feedback, feel free to contact me at maliksanjoykumar[@]gmail.com.
References
https://levelup.gitconnected.com/generics-and-wildcards-in-java-1e678f7792
https://www.cs.cmu.edu/~pattis/15-1XX/15-200/lectures/collectionsv/lecture.html
https://docs.oracle.com/javase/tutorial/java/generics/index.html
http://thegreyblog.blogspot.com/2011/03/java-generics-tutorial-part-i-basics.html
http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeParameters.html
Sanjoy Kumar Malik is an experienced software architect and technologist. He is passionate about Cloud Computing, Software Architecture, and System Design. Apart from technology and software, he is an avid LinkedIn networker. You can join his 5.5+ lacs supporters on LinkedIn.