The introduction
Generics is a very important knowledge point in Java. Generics are widely used in Java set class framework. In this article we’ll take a look at Java generics design from scratch, involving wildcard handling and annoying type erasure.
Generic basis
A generic class
Let’s start by defining a simple Box class:
This is the most common way to do this, but the downside is that Box can only load elements of String. If we need to load elements of other types such as Integer, we will have to rewrite another Box, and the code will not be reused.
So that our Box class can be reused, we can replace T with whatever type we want:
Generic method
Having looked at generic classes, let’s look at generic methods. Declaring a generic method is as simple as prefixing the return type with a similar form:
We can call a generic method like this:
Or use type inference in Java1.7/1.8 to make Java automatically derive the corresponding type parameters:
Boundary operator
Now we want to implement a function to find the number of elements in a generic array that are greater than a particular element. We can implement it like this:
But this is obviously wrong, for in addition to the short, int, double, long, float, byte, char primitive types, such as other classes is not necessarily can use operators >, so the compiler error, and how to solve this problem? The answer is to use a boundary character.
Making a declaration like the following tells the compiler that the type parameter T represents classes that implement the Comparable interface, which tells the compiler that they all implement at least the compareTo method.
The wildcard
Before we look at wildcards, we need to clarify a concept. Again, using the Box class we defined above, suppose we add a method like this:
So what types of arguments are Box N now allowed to accept? Can we pass in Box or Box? The answer is no. Although Integer and Double are subclasses of Number, there is no relationship between Box or Box in the generic. This point is very important, so let’s go through a complete example to further understand it.
Let’s start by defining a few simple classes that we’ll use:
FruitReader’s readCovariant method accepts arguments that are subclasses of Fruit (including Fruit itself), and the relationship between subclasses and their parents is related.
The principle of PECS
We can get elements from a list. Can we add elements from a list? Let’s try it:
The answer is no, the Java compiler doesn’t allow us to do this. Why? We can consider this problem from the perspective of the compiler. Because Listflist itself can have multiple meanings:
When we try to add an Apple, flist might point to a New ArrayList
When we try to add an Orange, flist might point to a New ArrayList
So when we try to add a Fruit, it could be any type of Fruit, and flist might only want a certain type of Fruit, and the compiler won’t recognize it and will get an error. Therefore, the implemented set class can only be treated as a Producer providing external elements (GET), and cannot be used as a Consumer to obtain external elements (Add).
So what do we do if we want to add elements? You can use:
This allows us to add elements to the container, but the downside of using super is that we can’t get elements from the container in the future, and the reason is simple. Let’s continue to think about this from the compiler’s point of view. For List List, it can have the following meanings:
When we try to get an Apple through the list, we might get a “Fruit”, which could be Orange or other types of Fruit.
Based on the above example, we can conclude that Producer Extends, Consumer Super:
“Producer Extends” — If you need a read-only List and use it to produce T, then use? Extends T.
“Consumer Super” – If you need a write-only List to consume T, then use? Super T.
If we need to read and write at the same time, we can’t use wildcards.
If you read the source code for some Java collection classes, you can see that we usually use both together, such as the following:
Type erasure
Perhaps the most vexing aspect of Java generics, especially for programmers with C++ experience, is type erasations. Type erasures mean that Java generics can only be used for static type checking at compile time, and then the compiler generates code that erases the corresponding type information so that at run time the JVM actually knows exactly what type the generics represent. This is done because Java generics were not introduced until after 1.5, and in order to maintain downward compatibility, only type erasure can be done to accommodate previous non-generic code. For this, if you read the source code for the Java Collections framework, you’ll see that some classes don’t actually support generics. Having said that, what exactly does generic erase mean? Let’s start with a simple example:
After the compiler does the appropriate type checking, at runtime this code will actually be converted to:
This means that whether we declare Node or Node, at runtime, the JVM is treated as a Node. Is there any way to solve this problem? This requires us to reset the bounds ourselves, changing the above code to look like this:
Comparable instead of the default Object, the compiler will replace T where it appears with Comparable:
The above concepts may be easier to understand, but there are more problems with generic erasure. Let’s take a systematic look at some of the problems associated with type erasure, some of which may not be encountered in C++ generics, but need to be taken care of in Java.
Problem a
Creating generic arrays is not allowed in Java, and the compiler will raise an error if something like the following is done:
Why don’t compilers support this? Continuing with the reverse thinking, let’s look at the problem from the compiler’s point of view.
Let’s take a look at this example:
The above code is easy to understand. String arrays cannot hold integer elements, and this error is usually not detected until the code is run, so the compiler cannot recognize it. Let’s take a look at what happens if Java supports generic array creation:
If you still doubt this, try running the following code:
Question 2
Continuing to reuse our Node class above, the Java compiler actually helps us implement a Bridge method secretly for generic code.
After reading the above analysis, you might think that after type erasure, the compiler would change Node and MyNode to something like this:
This is not the case. Let’s look at the following code, which will throw a ClassCastException indicating that String cannot be converted to Integer:
MyNode does not have a setData(String data) method, so only the parent Node’s setData(Object data) method can be called. If so, line 3 above should not be an error, because of course String can be converted to Object. How does ClassCastException get thrown?
In fact, the Java compiler automatically does one more thing for the above code:
So that’s why I got an error, setData((Integer) data); String cannot be converted to Integer. Thus, when the compiler prompts you for an unchecked warning in line 2 above, you cannot ignore it until runtime to detect the exception. It would have been nice if we had added Node n = mn at the beginning, so the compiler would have found the error ahead of time. Question 3
As we mentioned above, Java generics can largely only provide static type checking, after which the type information is erased, so creating instances of type parameters like the following would not pass the compiler:
But what if there are scenarios where we want to create instances with type parameters? Reflection can be used to solve this problem:
In fact, there are two design patterns that can be used to solve the above problem: Factory and Template. If you are interested, check out chapter 15 of Thinking in Java about Creating Instance of Types (Page 664). We won’t go into that here.
Problem four
We can’t use the instanceof keyword directly for generic code because the Java compiler erases all relevant generic type information when generating code, just as the JVM we verified above couldn’t tell the difference between an ArrayList and an ArrayList at runtime:
As above, we can fix this problem by resetting the bounds with the wildcard:
The factory pattern
Let’s use generics to simply implement the Factory pattern. First we declare an interface Factory:
Next we will create several entity classes FuelFilter and AirFilter as well as FanBelt and GeneratorBelt.
The implementation of the Part class is as follows, noting that the above entity classes are indirect subclasses of the Part class. In the Part class we register the entity class we declared above. So if we want to create the related entity class, we only need to call the related method of the Part class. One advantage of this is that if CabinAirFilter or PowerSteeringBelt appears in your business, you don’t need to change much code, just register them in the Part class.
Finally, let’s test it out: