What is a generic
When it comes to generics, we are certainly familiar with them. There are many statements like this in our code:
List<String> list=new ArrayList<>();
Copy the code
ArrayList is a generic class, and we can store different types of data into the collection by setting them to different types (and only the specified data types, which is one of the advantages of generics). “Generic” simply means a generic type (parameterized type). Imagine this scenario: if we were to write a container class that supports queries that add or delete data, we would write a container class that supports String, and then we would write a container class that supports Integer. And then what? Doubel, Float, various custom types? That’s a lot of repetitive code, and the algorithms for these containers are the same. We can replace all the types that we need by referring to one type, T, in general, and passing the types that we need as arguments into the container, so that we can write one set of algorithms that will fit all the types. The classic example is ArrayList, a collection that works no matter what data type we pass. After reading the above description, the smart student got an idea and wrote the following code:
class MyList{
private Object[] elements=new Object[10];
private int size;
public void add(Object item) {
elements[size++]=item;
}
public Object get(int index) {
returnelements[index]; }}Copy the code
This code is very flexible, all types can be cast up to the Object class, so we can store various types of data in it. Indeed, Java did the same thing before generics. But there is a problem with this: if there is a lot of data in the collection, a data transformation error will not be detected at compile time. But will happen at runtime Java. Lang. ClassCastException. Such as:
MyList myList=new MyList();
myList.add("A");
myList.add(1);
System.out.println(myList.get(0));
System.out.println((String)myList.get(1));
Copy the code
We store multiple types in this collection (in some cases the container may store multiple types of data), and if there is a large amount of data, it is inevitable that there will be exceptions during the transformation, which are not known at compile time. Generics, on the other hand, allow us to add only one type of data to the collection, while allowing us to detect errors at compile time, avoiding runtime exceptions and improving code robustness.
Introduction to Java generics
Let’s take a look at Java generics. The following aspects will be introduced:
- Java generic class
- Java generic methods
- Java generic interfaces
- Java generic erasure and related content
- Java generic wildcards
Java generic class
Class structure is one of the most basic elements of object orientation. If our class needs to be extensible, we can make it generic. Suppose we need a wrapper class for data that, by passing in different types of data, can store the corresponding types of data. Let’s look at the design of this simple generic class:
class DataHolder<T>{
T item;
public void setData(T t) {
this.item=t;
}
public T getData() {
returnthis.item; }}Copy the code
A generic class is defined by simply adding the type parameter after the class name. Of course, you can add multiple parameters, such as
,
, etc. This allows us to use defined type parameters within the class. The most common use of generic classes is the use of tuples. We know that a method return value can only return a single object. If we define a generic class with two or even three type parameters, then when we return an object, we build such a “tuple” of data, passing in multiple objects through the generic, so that we can method multiple data at once.
Java generic methods
Generics apply to the entire class. Now let’s look at generic methods. Generic methods can exist in either generic or generic classes. If you can solve a problem with a generic approach, you should use a generic approach whenever possible. Here’s a look at the use of generic methods:
class DataHolder<T>{
T item;
public void setData(T t) {
this.item=t;
}
public T getData() {
returnthis.item; } @param e */ public < e > void PrinterInfo(e e) {system.out.println (e); }}Copy the code
Let’s look at the results:
1
AAAAA
8.88
Copy the code
From the above example, we see that we are defining a generic method printInfo inside a generic class. By passing in different data types, we can print them all out. In this method, we define the type parameter E. There is no relationship between this E and the T in the generic class. Even if we set the generic method like this:
// Note that this T is a completely new type and may not be the same type as T declared in generic classes. public <T> void PrinterInfo(T e) { System.out.println(e); DataHolder<String> DataHolder =new DataHolder<>(); dataHolder.PrinterInfo(1); dataHolder.PrinterInfo("AAAAA"); DataHolder. PrinterInfo (8.88 f);Copy the code
The generic method can still pass Double, Float, and so on. The type parameter T in the generic method is of a different type than the type parameter T in the generic class. PrintInfo is not affected by the type parameter String in the DataHolder. Let’s summarize some basic characteristics of the generic approach:
- The distinction between public and return value is very important, which can be interpreted as declaring this method generic.
- Only declared methods are generic. Member methods that use generics in a generic class are not generic.
- Indicates that the method will use the generic type T before you can use the generic type T in the method.
- As with the definition of generic classes, T can be written as an arbitrary identifier, and common parameters such as T, E, K, and V are often used to represent generics.
Java generic interfaces
The definition of a Java generic interface is basically the same as that of a Java generic class. Here is an example:
Public interface Generator<T> {public T next(); }Copy the code
There are two things to note here:
- If a generic interface does not pass in a generic argument, it is the same as the definition of a generic class. When declaring a class, the declaration of the generic type must also be added to the class. Examples are as follows:
DataHolder implements Generator<T>{DataHolder implements Generator<T>"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
returnnull; }}Copy the code
- If a generic interface passes a type parameter to the implementation class that implements the generic interface, all uses of the generic are replaced by the type of the argument passed in. Examples are as follows:
class DataHolder implements Generator<String>{
@Override
public String next() {
returnnull; }}Copy the code
As we can see from this example, everything that implements T in a class needs to be implemented as String.
Java generic erasure and related content
Let’s look at an example:
Class<? > class1=new ArrayList<String>().getClass(); Class<? > class2=new ArrayList<Integer>().getClass(); System.out.println(class1); //class java.util.ArrayList System.out.println(class2); //class java.util.ArrayList System.out.println(class1.equals(class2)); //true
Copy the code
Class1 and class2 are the same type ArrayList when we look at the output, and the String and Integer type variables we passed in at run time are discarded. Java language generics were designed to be compatible with older code. Java’s generics mechanism uses an “erase” mechanism. Let’s take a more thorough example:
class Table {} class Room {} class House<Q> {} class Particle<POSITION, List<Table> tableList = new ArrayList<Table>(); Map<Room, Table> maps = new HashMap<Room, Table>(); House<Room> house = new House<Room>(); Particle<Long, Double> particle = new Particle<Long, Double>(); System.out.println(Arrays.toString(tableList.getClass().getTypeParameters())); System.out.println(Arrays.toString(maps.getClass().getTypeParameters())); System.out.println(Arrays.toString(house.getClass().getTypeParameters())); System.out.println(Arrays.toString(particle.getClass().getTypeParameters())); /** [E] [K, V] [Q] [POSITION, MOMENTUM] */Copy the code
In the above code, we want to get the type parameters of the class at runtime, but we see that all the parameters are returned. We don’t get any information about declared types at runtime. Note: The compiler will remove the parameter type information during compilation, but it will ensure that the parameter type is consistent within the class or method. A generic parameter will be erased to its first boundary (there can be more than one boundary, using the extends keyword to add a boundary to the parameter type). The compiler actually replaces the type parameter with the type of its first boundary. If no boundary is specified, the type parameter is erased to Object. In the following example, the generic parameter T can be used as an HasF type.
public interface HasF {
void f();
}
public class Manipulator<T extends HasF> {
T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) { this.obj = obj; }}Copy the code
The type information after the extend keyword determines what information generic parameters can retain. Java type erasure only erases to HasF types.
Principles of Java generic erasure
Let’s look at an example, starting with a non-generic version:
// SimpleHolder.java
public class SimpleHolder {
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.setObj("Item");
String s = (String) holder.getObj();
}
}
// SimpleHolder.class
public class SimpleHolder {
public SimpleHolder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
":()V
4: return
public java.lang.Object getObj();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void setObj(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class SimpleHolder
3: dup
4: invokespecial #4 // Method "
":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;) V
14: aload_1
15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
}
Copy the code
Here’s a generic version, from a bytecode point of view:
//GenericHolder.java
public class GenericHolder<T> {
T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
public static void main(String[] args) {
GenericHolder<String> holder = new GenericHolder<>();
holder.setObj("Item");
String s = holder.getObj();
}
}
//GenericHolder.class
public class GenericHolder<T> {
T obj;
public GenericHolder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
":()V
4: return
public T getObj();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void setObj(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class GenericHolder
3: dup
4: invokespecial #4 // Method "
":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;) V
14: aload_1
15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
}
Copy the code
Information about type variables is available during compilation. Therefore, the set method can be checked by the compiler, and invalid types cannot be compiled. For the GET method, however, the actual reference type at run time is of type Object because of the erasing mechanism. To “restore” the type of the result returned, the compiler adds type conversions after GET. So, there is some type conversion logic in line 18 of the main method body of the Genericholder.class file. It was added automatically by the compiler for us. So we do something for us where the generic class objects are read and written, adding constraints to the code.
Defects in Java generics erasure and remedies
Generic types cannot be explicitly used in run-time type operations, such as transitions, instanceof, and new. Because at run time, all parameter type information is lost. Code like the following will not compile:
public class Erased<T> { private final int SIZE = 100; Public static void f(Object arg) {// Failed to compileif(arg instanceof T) {} var = new T(); T[] array = new T[SIZE]; T[] array = (T) new Object[SIZE]; }}Copy the code
So what can we do to fix it? Here are some ways to solve the above problems one by one.
Type judgment problem
We can solve the problem that the type information of generics cannot be determined due to erasure by the following code:
/** * GenericType<T>{class <T>? > classType; public GenericType(Class<? >type) {
classType=type;
}
public boolean isInstance(Object object) {
returnclassType.isInstance(object); }}Copy the code
In the main method we can call this:
GenericType<A> genericType=new GenericType<>(A.class);
System.out.println("-- -- -- -- -- -- -- -- -- -- -- --");
System.out.println(genericType.isInstance(new A()));
System.out.println(genericType.isInstance(new B()));
Copy the code
We do the type determination by recording the Class object of the type parameter.
Creating type Instances
There are two reasons for not being able to use new T() in generic code. Instead, there is no way to determine whether T contains a no-parameter constructor. To avoid both problems, we use the explicit factory pattern:
/** @param <T> */ interface Factory<T>{T create(); } class Creater<T>{ T instance; public <F extends Factory<T>> T newInstance(F f) { instance=f.create();return instance;
}
}
class IntegerFactory implements Factory<Integer>{
@Override
public Integer create() {
Integer integer=new Integer(9);
return integer; }}Copy the code
We create instance objects using factory mode + generic method. In the above code, we create an IntegerFactory to create an Integer instance. If the code changes later, we can add a new factory type. The call code is as follows:
Creater<Integer> creater=new Creater<>();
System.out.println(creater.newInstance(new IntegerFactory()));
Copy the code
Create a generic array
Creating generic arrays is generally not recommended. Use arrayLists instead of generic arrays whenever possible. But here’s a way to create a generic array.
public class GenericArrayWithTypeToken<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[]) Array.newInstance(type, sz);
}
public void put(int index, T item) {
array[index] = item;
}
public T[] rep() {
return array;
}
public static void main(String[] args) {
}
}
Copy the code
Here we still use the pass parameter type, using the newInstance method of the type to create an instance.
Wildcard character for Java generics
The upper bound wildcard <? extends T>
Let’s start with an example:
class Fruit {}
class Apple extends Fruit {}
Copy the code
Now let’s define a plate class:
class Plate<T>{
T item;
public Plate(T t){
item=t;
}
public void set(T t) {
item=t;
}
public T get() {
returnitem; }}Copy the code
Now, let’s define a fruit plate, and in a fruit plate, of course, there could be apples
Plate<Fruit> p=new Plate<Apple>(new Apple());
Copy the code
You will find that this code does not compile. Apple plate cannot be converted to fruit plate:
cannot convert from Plate<Apple> to Plate<Fruit>
Copy the code
As we know from the above code, even if there is an inheritance relationship between the types in the container, there is no inheritance relationship between Plate and Plate containers. In this case, Java is designed as Plate<? Extend Fruit> to allow inheritance between the two containers. Our code above is ready to do the assignment
Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
Copy the code
Plate
is the base class of Plate< Fruit> and Plate< Apple >. Let’s look at the bounds of upper bounds with a more detailed example:
class Food{}
class Fruit extends Food {}
class Meat extends Food {}
class Apple extends Fruit {}
class Banana extends Fruit {}
class Pork extends Meat{}
class Beef extends Meat{}
class RedApple extends Apple {}
class GreenApple extends Apple {}
Copy the code
In the class hierarchy above, Plate<? Extend Fruit>, cover the blue portion below:
If we add data to the plate, for example:
p.set(new Fruit());
p.set(new Apple());
Copy the code
You’ll notice that you can’t set data in there, so logically we’ll set the generic type to? The extend of Fruit. It should be possible for us to add a Fruit subclass to it. But the Java compiler does not allow this.
invalidates the set() method of putting things on a plate. The Java compiler only knows that the container is holding Fruit and its derived classes, but it could be Fruit. Maybe Apple? Maybe Banana, RedApple, GreenApple? After the compiler sees the Plate< Apple > assignment, the Plate is not marked as “Apple”. It simply marks a placeholder “CAP#1” to capture a Fruit or Fruit derived class. None of the calling code that inserts Apple or Meat or Fruit into the container is known by the compiler to match this “CAP#1”, so none of this is allowed. A Plate
may refer to a plate-type Plate on which Banana is of course not allowed. Plate
extends Fruit> represents a plate that can only fit a certain type of Fruit, not a plate that can fit any Fruit, but an upper bound wildcard allows reading operations. For example:
Fruit fruit=p.get();
Object object=p.get();
Copy the code
This is easy to understand, because the upper wildcard allows only Fruit and its derived classes to be stored in the container, so we can implicitly convert everything we get to its base (or Object base) class. So the upper bound descriptor Extends is suitable for scenarios with frequent reads.
The lower wildcard <? super T>
A lower-bound wildcard means that only T and its base type can be stored in a container. Let’s look at the class level above, <? Super Fruit> Cover the red part below:
The principle of PECS
Finally, a brief introduction to PECS principles introduced in Effective Java.
- The upper bound
extends T> extends T> extends T> extends T> extends T> extends T> extends T> extends T> extends T> extends T> extends T> - Lower bound
does not affect storage, but can only be stored in Object objects, suitable for frequently inserted data scenarios.
<? > infinite wildcard
An unbounded wildcard implies that any object can be used, so using it is similar to using a primitive type. But it does work. A primitive type can hold any type, whereas an unbounded wildcard container holds a specific type. For example, in the List<\? A reference of type > cannot add Object to it, whereas a reference of type List can add variables of type Object. As a final reminder, List<\Object> and List
is not the same as List<\Object> is List
subclass. And can’t go to List
add any object to list except null.