1. Generic base
Generics, a new feature introduced in JDK 5.0, are similar to the C++ template classes, meaning that you can write code that can be reused by different types of objects.
Before using generics, if you wanted to implement a generic method that could handle different types of data, we would use the Object’s public parent:
public class Storage {
Object value;
public Object getValue(a) {
return value;
}
public void setValue(Object value) {
this.value = value; }}Copy the code
In this way, the Storage class can access any type of data. As long as no strong type exceptions occur.
Storage storage = new Storage();
storage.setValue(134);
int value = (int) storage.getValue();
storage.setValue("hello");
String value1 = (String) storage.getValue();
Copy the code
The type of the value attribute is parameterized, which is passed in for external calls:
public class Storage<T> {
T value;
public T getValue(a) {
return value;
}
public void setValue(T value) {
this.value = value; }}Copy the code
When using generic Storage, you need to pass in the type as a parameter, and then you do not need to strong-type the return value of getValue().
Storage<Integer> storage1 = new Storage<Integer>();
storage1.setValue(134);
int value = storage1.getValue();
Storage<String> storage2 = new Storage<String>();
storage2.setValue("hello");
String value1 = (String) storage2.getValue();
Copy the code
By comparing the above two methods, it can be concluded that:
- When Object is used, all parameters and return values are of Object type, and each use requires type strong-casting. And the compiler has no way of knowing if the conversion is correct, only
At runtime,
I realized the program was extremely insecure. - With generics, once the type is determined, the compiler can specify everything
T
Location type, no longer need to perform type coercion. And if there is a type mismatch, the compilation fails, for example, it cannot be executedstorage1.setValue("abc");
, it only accepts arguments of type Integer.
So, we can summarize several characteristics of generics:
- The nature of generics is parameterized types, which parameterize the data types of a class so that they can be passed in from outside, thus extending the scope of the data that a class can handle.
- When the type is determined, the generic type will check the type. If it does not match the type, the compilation fails.
- Improve code readability without having to wait until runtime to cast. The type of data that can be manipulated when defined or instantiated.
2. Use of generics
Generics can be applied to classes, interfaces, and methods.
2.1 a generic class
can be understood as a type parameter declaration, and T stands for type parameter and is used to refer to any type.
public class testGenerics<T> {
private T value;
public testGenerics(T v) { value = v; }}// Generic classes can also accept multiple type parameters
public class testGenerics<T.E> {
private T value;
private E element;
public testGenerics(T v, E e) { value = v; element = e; }}Copy the code
T is a parameter that can actually be replaced by another character, but Java recommends that we use a single uppercase letter for type arguments due to coding specifications. Common ones are:
- T: Type, which generally stands for any class;
- E: Element, which generally stands for Element type, or Exception, which stands for Exception;
- K: Key, Key value type.
- V: Value, Value Value type, usually used with K.
- S: Subtype: indicates the Subtype.
When you create an instance using a generic class, you simply declare the corresponding type in Angle brackets, and type parameters such as T and E are replaced with the corresponding type.
2.2 Generic interfaces
Generic interfaces are used in much the same way as generic classes:
public interface Comparator<T> {
public int compare(T lhs, T rhs);
public boolean equals(Object object);
}
Copy the code
When implementing an interface, specify a specific type:
public class StringCompare implements Comparator<String> {
@Override
public int compare(String lhs, String rhs) {... }}Copy the code
2.3 Generic methods
If the class of the generic method is a generic class, and the method handles the same type of data as the declared type of the generic class, you can simply use the parameters of the class declaration.
public class testGenerics<T> {
private T value;
public testGenerics(T v) {
value = v;
}
public T compare(T a, T b) {... }}Copy the code
You need to declare your own type if you are in a non-generic class or if the method needs to handle data types that are different from those declared by the generic class. When you define a generic method, you must declare that it is a generic method by preceded by a
(T can also be replaced by another character).
public class testGenerics<T> {
public <E> E compare(E a, E b) {... }public <E> T compare(T a, T b) { // This is the same as if there were no
declaration. The compiler still replaces T with the specified type
. }}Copy the code
Generic methods of type parameter, associated with a generic class is no generic method type is determined at runtime, and because E is not like a generic class by the specified, so when the call a generic method, although the parameter may be declared as type E, but actually the type of argument can be different, are not related parameter types.
public class testGenerics<T> {
public <E> E compare(E a, E b) {... } } testGenerics<Integer> g =new testGenerics<Integer>();
g.compare("abc"."abc");
g.compare("abc".1);// Although both parameters are declared as type E, they are not actually related, and two parameters of different types can be passed
Copy the code
3. Generic wildcards
3.1 Unrestricted Wildcard <? >
Indicates that any type can be held and can be used when you are unsure or do not care about the actual type to operate on. ? Unlike Object, List<? > represents a List of unknown type, while List<Object> represents a List of type Object.
public void setList(List
list) {
if(list.get(0) != null && list.get(1) != null) {
list.add(list.get(0));// Failed to compile
list.set(0, list.get(1));// Failed to compile
}
if(list.size() > 0) {
list.remove(0);// Write operations independent of type can be performed}}Copy the code
3.2 Upper bound Wildcard <? extends E>
The parameter in the generic must be a subclass of E or E. If the type passed is not a subclass of E or E, the compilation fails and E’s methods can be used in the generic. Limits are defined, and only the ability to read, not write.
tells the compiler to deal with a subclass of type E or E, so the class is not yet determinable. To ensure type safety, the compiler does not allow any type of data to be added to it, lest a cast exception occur.
Type E data cannot be written either: Superclasses cannot be strong-subclassed. For example, if you want to change “Fruit” to “Apple” and put it in your “Apple” List, you can’t do that.
public void test(List<? extends Fruit> list) {
list.add(new Fruit()); // Failed to compile
}
Copy the code
3.3 Lower Wildcard <? super E>
Super means that the argument to the generic type must be E or a parent of E. Defines the lower limit, has the ability to read and part of the ability to write, subclass can be strong into the parent class.
describes a superclass object/superclass container of E, but since the superclass is uncertain, only subclasses of E/E can be added /set. Since all subclasses of E can be converted to any superclass of E, set/add is absolutely safe. We can only get an Object because we don’t know which parent class it is.
public void test(List<? super Fruit> list) {
list.get(0).read();// Compile failed because the parent of Fruit might not have defined the read method
Fruit f = list.get(0); Fruit
List
List
List
List
List
List
List
List
List
List
List
List
List
List
list.add(new Apple("apple"));
list.add(new Fruit("fruit"));
}
Copy the code
3.4 the principle of PECS
The PECS principle is the basic principle for using wildcards: Producer extends, Consumer super.
If the parameterized type represents a producer, use <? Extends E >; If a consumer is represented, use <? Super E >; If you are both producer and consumer, you should not use wildcards.
Producers and consumers are distinguished from parameter containers: if the parameter container is suitable for reading but not writing, it is a producer. And if the parameter container is suitable for writing but not for reading, only in but not out, it is the consumer.
From the above analysis, it can be seen that
Cannot write data to the undefined type to avoid type exceptions; However, the data it reads can be guaranteed to be E or E subclasses, so it is read-only, not written, only out, not in, belonging to the producer class; The
allows the subclass data of E or E to be written. However, due to the uncertainty of the parent class, only Object instance can be read. Therefore, it is suitable for writing but not reading.
Generic type erasure
4.1 Erasing mechanism
Java generics were designed to be compatible with older code by introducing an erasing mechanism for generics. Java generics actually belong to the concept of compilation layer. The generated bytecode does not contain the type information in generics. The compiler will automatically complete the translation from Generic Java to ordinary Java, and the Java virtual machine will not be aware of generics when running.
Generic erase: When the compiler compiles code with generics, it performs type checking and type inference and generates plain bytecode without generics.
Keep only the primitive type after erasing: THE primitive type is the true type of the type variable that ends up in the bytecode after erasing the generic information.
When a generic type is defined, the corresponding primitive type is automatically provided. Type variables are erased at compile time and replaced with their qualified types (unqualified types are replaced with Object).
For example, a simple generic class is defined:
public class Storage<T> {
T value;
public T getValue(a) {
return value;
}
public void setValue(T v) { value = v; }}Copy the code
As can be seen from the compiled bytecode, the type of the attribute value value, the return value of getValue(), and the argument to setValue() are all replaced with Object:
When the compiler compiles, the generic parameter T is erased and replaced with the original type, Object in this case. The Storage class is equivalent to the following form:
public class Storage {
Object value;
public Object getValue(a) {
return value;
}
public void setValue(Object v) { value = v; }}Copy the code
So, generics are actually a syntactic sugar in Java, erased by generics in the same way that we defined a generic class before generics, and replaced it with a primitive replaceable class.
4.2 Signature Attribute Table
One might look at this and wonder: Since whatever type argument is passed in is eventually erased and replaced with the original type, what happens to the type argument passed in?
In fact, erasing does not mean that the type parameters passed in are lost within the JVM. The actual type of any object is visible to the JVM at run time, based on which the reflection system can be implemented and cast exceptions can be fired.
When generics were introduced in JDK 5, the JVM made changes to support generics, most importantly the Signature property sheet. After Java is compiled into bytecode, its declared generic information is stored in Signature, and any generic information that can be retrieved through reflection comes from this property.
For example, we create an instance object of the generic class Storage and a method with generic parameters:
public class testGenerics extends Storage<String>{
Storage<Integer> storage;
public testGenerics(a) {
storage = new Storage<>();
}
public void test(List<? extends Number> list) {
for(int i = 0; i < list.size(); i++) {
if(list.get(i) instanceof Integer) {
storage.setValue((Integer)list.get(i));
break; }}int value = storage.getValue();
}
public List<Integer> getList(a) {
return null; }}Copy the code
To view its compiled bytecode, run the javap command:As you can see,Parent generics, member variables, method parameters, and return values
The generic parameter is retained in the Signature table and retrieved when the generic parameter needs to be used during the run.
5. Problems and solutions about generic erasure
5.1 How do I guarantee qualified types of generic variables?
After a generic erasure, both String and Integer are converted to Object. How do I determine whether a variable should use String or Integer?
To ensure that the type used is correct, Java checks the referenced type before type erasure and performs type erasure only after checking that there is no problem. So when we try to add a string to a list declared Integer, it will not compile.
Type checking is for references, which variable is a reference, and calling a generic method with that reference does type checking for the method called by that reference, regardless of the object it actually refers to.
5.2 Automatic type conversion Problems
Because of generic erasure, all generic types are replaced with primitive types, but in practice, the data returned by the get method does not need to be cast. It is the type data that corresponds to the generic parameter type we pass in.
Let’s test the Storage class:
public class testGenerics{
private static Storage<Integer> storage;
public static void main(String[] args) {
storage = new Storage<>();
storage.setValue(1); System.out.print(storage.getValue().getClass()); }}Copy the code
The output result is:class java.lang.Integer
As you can see, it does automatically convert from Object to the incoming Integer class. Let’s look at its bytecode:
The compiler also added the checkcast directive, which checks the type conversion, to force an Integer. So the conversion of generics is done by the compiler adding the checkcast directive.
5.3 Type erasure conflicts with polymorphism
The previous generic class Storage, we define a subclass to inherit from it. In a subclass, the two methods of the superclass are overridden. Super calls the method of the superclass, and the generic type of the superclass is limited to String.
As you can see from the @Override tag, there is no problem overwriting parent methods this way.
public class Save extends Storage<String> {
@Override
public void setValue(String v) {
// TODO Auto-generated method stub
super.setValue(v);
}
@Override
public String getValue(a) {
// TODO Auto-generated method stub
return super.getValue(); }}Copy the code
However, after the type is erased, all the generic types of the parent class become the original type Object, and the parent class is compiled as:
public class Storage {
Object value;
public Object getValue(a) {
return value;
}
public void setValue(Object v) { value = v; }}Copy the code
As you can see, the setValue method has a parent class of type Object and a subclass of type String. The argument types are different, so it is not overridden but overridden.
So after type erasure, the intent of the override to implement polymorphism is not fulfilled, subclass overwriting becomes overloading, and type erasure conflicts with polymorphism.
So how was the conflict resolved? We looked at the bytecode of the Save class through Javap: it went from two methods to four, with two more set and GET methods for parameters and return values of Object.
When we call the set and GET methods of the Save class, we actually call the methods generated by the two compilers, which then call our own overridden methods.
The two methods generated by the compiler are also called bridge methods, and the bridge method is the one that actually overrides the parent class. Our own overrides are just an illusion. Conflicts between type erasers and polymorphisms are resolved by an automatically generated intermediate bridge method.
5.4 Runtime Type Check exception –instanceof
ArrayList
list = new ArrayList<>(); If (List instanceof ArrayList
) is not allowed at run time because the type erasers leave ArrayList
with the original type.
Java only limits this type of query: if(list instanceof ArrayList
).
5.5 Generics and exception catching
5.5.1 Generic class objects cannot be thrown or caught
Generic classes are not allowed to extend the Throwable interface, and the following class definitions will not compile:
public class GenericException<T> extends Exception {... }Copy the code
Because exceptions are thrown and caught at run time, if you allow the GenericException generic class definition above, you get the following exception catch:
try{... }catch(GenericException<Integer> e) {
...
} catch(GenericException<String> e) {
...
}
Copy the code
GenericException
5.5.2 Generic variables cannot be used in catch clauses
Java forbids the use of generic variables in catch statements and will not compile as follows.
public static <T extends Throwable> void test(Class<T> t) {
try{... }catch(T e) {// Failed to compile. }}Copy the code
If catch generic variables are allowed, the following definitions are allowed:
public static <T extends Throwable> void test(Class<T> t) {
try{... }catch(T e) {// Failed to compile. }catch(IndexOutOfBounds e) { ... }}Copy the code
The principle of exception catching is that the child class comes first and the parent class comes later. Assuming that the T type used here is ArrayIndexOutofBounds, the capture order here appears to be in order (IndexOutOfBounds is a parent of ArrayIndexOutofBounds); But ArrayIndexOutofBounds is replaced by Throwable after compilation, violating the order of exception catches. So to avoid this, Java disallows the use of generic variables in catch statements.
However, it is legal to use generic variables in exception declarations:
public static <T extends Throwable> void test(T t) {
try{... }catch(Throwable e) {
t.initCause(e);
throwt; }}Copy the code
5.6 Generic type arrays cannot be created
Java does not allow you to create arrays of parameterized types.
Storage<Integer>[] storages = new Storage<Integer>[10];// Failed to compile
Copy the code
Assuming it compacts, the type of the storages becomes Storage[] after erasing. Since arrays are covariant, it can be converted to an Object[].
Object[] objs = storages;
Copy the code
But sometimes the wrong use of the covariant nature of arrays can cause security problems, as in the following two examples:
objs[0] = "abc";// The compiler passes, but since arrays can remember the types of elements they store, ArrayStoreException is thrown at runtime
objs[0] = new Storage<String>();// The storage type of the array is checked
int n = storages[0].getValue();// A type conversion error occurs at this point
Copy the code
To avoid security risks associated with parameterized arrays, Java does not allow you to create parameterized arrays. If you need to, you can use ArrayList
> instead.
5.6 other
-
A generic type variable cannot be a primitive data type.
-
ArrayList
list = new ArrayList
-
Generic types cannot be instantiated, a = new T(); Is not allowed because new cannot allocate memory for indeterminate types; You can’t create a generic array, but you can use reflection to construct generic objects and arrays, using reflection to call newInstance.
public static <T> void add(Box<T> box, Class<T> clazz) { // Because T is reflected at runtime, what type is it try { T item = clazz.newInstance(); // Use bytecode through reflection } catch(Exception e) { ... } box.add(item); } Copy the code
-
You cannot use generics to create a method with the same name as a parent method.
For example, if you define an equals method, it will become equals(Object value) after type erasure, which conflicts with the equals method of Object.
public boolean equals(T value) { return null; } Copy the code
-
To support erasing conversions, it is necessary to enforce that a class or type variable cannot be a subclass of two interfaces that are different parameterizations of the same interface.
For example, a parent class and a subclass both implement the Comparable interface, and a subclass inherits the Comparable interface, resulting in the PairChild class implementing both the Comparable and Comparable interfaces, which are different parameterized implementations of the same interface.
public class Pair implements Comparable<Pair> {... }public class PairChild extends Pair implements Comparable<PairChild> {... }// Compile error Copy the code
-
Static methods and variables in a generic class may not use the generic type parameters declared by the generic class, because generic parameters are specified at instantiation time and static methods and variables are not objects.
Reference: Generic erasure Why can the runtime still get specific generic types through reflection, problems with generic erasure, generic basics