In previous lectures, we saw Generics in the program. Today we'll talk about it.
They were designed to extend Java's type system to allow "a type or method to operate on objects of various types while providing compile time type safety". The two uses of Generics generics are clearly pointed out here. One is to allow a class or method to manipulate different types of objects, and the other is to provide compile time type safety. This is introduced in Java 5. This is the result of the Java design team advancing with the times and compromising with history. It has basically achieved its goal, but it is far from perfect, and the industry has a mixed reputation.
In order to understand this concept, let's start from scratch and see how we write programs without Generics.
First write a simple program to print data array and string array. The code is as follows (OverwriteTest.java):
public class OverwriteTest { public static void printArray(Double[] dArray) { for (Double d : dArray) { System.out.println(d); } } public static void printArray(String[] sArray) { for (String s : sArray) { System.out.println(s); } } public static void main(String args[]) { Double[] dArray = { 1.618, 2.71828, 3.14159 }; String[] sArray = { "I", "love", "beijing", "tiananmen" }; printArray(dArray); printArray(sArray); } }
This is a common example when learning Java. Printing arrays is very simple. At the beginning, a printArray(Double []) is provided to print the data array, and then the teacher will say: what about printing a string array? Do you want to write a printStringArray() method? No, students, we can use the method overload feature, or use the printArray() method, but just give different parameters, such as printArray(String []).
This is a good way. You don't have to write multiple different method names. You can overload them with different parameters. However, there are still many methods to write, but the names are the same. Is there a way to really write only one method? Yes, using Object instead is one way. Another way is to use genetics. Let's rewrite the above program. The code is as follows (GenericMethodTest.java):
public class GenericMethodTest { public static <E> void printArray(E[] eArray) { for (E e : eArray) { System.out.println(e); } } public static void main(String args[]) { Integer[] iArray = { 1, 2, 3, 4, 5 }; Double[] dArray = { 1.618, 2.71828, 3.14159 }; String[] sArray = { "I", "love", "beijing", "tiananmen" }; printArray(iArray); printArray(dArray); printArray(sArray); } }
We rewrite printArray() with the concept of Generics. Instead of adding a method with the same name to each data type, we use a general abstract type E (E, T, K,V, etc. can be named at will).
public static <E> void printArray(E[] eArray) { for (E e : eArray) { ...
Take a closer look at the definition of generic methods. A method is declared before the return value of the method, indicating that e used in this method is a general abstract type. The following parameter definitions and the use in the method body can be directly used with E. In short, it is to replace the specific types previously written, such as String and Double, with E. In main(), we tried three specific data types, all of which can be printed.
This is very nice. Then we will wonder how to do it? We can try to put the generated Decompile the class file,
The decompilation result of printArray() method is:
public static <E> void printArray(E[] eArray) { for (Object e : eArray) { System.out.println(new Object[] { e }); } }
It turns out that after compilation, it turns all the abstract data type E into Object, which is the mother of objects in Java. In fact, before Generics, we also used Object to realize generalization. You can change the parameter of the printArray() code above to Object [], and the result will be the same.
Similarly, if there are both generic methods and ordinary methods in the class, ordinary methods will be used when calling. For example, printArray(E []) and printArray(String []), when calling printArray({"I", "love", "beijing"}), the common method printArray(String []) is called. The reason is that after compilation, printArray(E []) actually becomes printArray(Object []).
OK, let's make a bad one. While defining printArray(E []), we have to define another printArray(Object []) to see what the result will be. The test results are as follows:
Compilation error: error of method printArray(E []) is the same as another method in type GenericMethodTest.
Now it's clear that the compiler played a trick, providing generics on the surface, but actually using Object internally. This technique is called erasure. The main consideration in doing so is not to change the virtual machine and maintain backward compatibility.
If the application is simply applied here, it is generally understood that way. However, things are not so simple. After further discussion, there are still a lot of knowledge (and pits) waiting for us.
In addition to generic methods, there are generic classes. Let's take a look at an example. The code is as follows (GenericTest.java):
import java.util.ArrayList; import java.util.List; public class GenericTest { public static void main(String[] args) { List<String> name = new ArrayList<String>(); List<Number> number = new ArrayList<Number>(); name.add("Alice"); number.add(234); getData(name); getData(number); } public static void getData(List<?> data) { System.out.println("data :" + data.get(0)); } }
We used the ready-made generic classes List and ArrayList. These two classes have generic versions after Java 5. When used, it is slightly different from the previous writing method. Listname = new ArrayList(); Type tags are added when declaring and creating. Indicates that the List contains Number (after Java 7, it can be abbreviated as: listname = new ArrayList < > ();).
Recall, how did we do it when there was no generics? Examples of procedures are as follows:
List arrayList = new ArrayList(); arrayList.add(100); arrayList.add("Test String"); for(int i = 0; i< arrayList.size();i++){ Number n = (Number)arrayList.get(i); }
Define an ArrayList, put number and String in it, and finally print it out with an error! Because everything can be placed in ArrayList, the compiler does not check, and only errors occur at runtime. At this time, we say that the type is unsafe. Generic classes solve this problem, and such errors can be detected at compile time. For the generic version of ArrayList, we write numberlist add("314");, Compilation error: the # method add(Number) in the # type # Listis # not applicable # for the # arguments (String). In this way, it is ensured that the number is put in. When it is taken out, there is no need to carry out type conversion. It is also number. With this mechanism, our program is much safer. For collection classes, generics are particularly useful when you need to put a lot of things in them.
Let's see how to define a generic class ourselves.
public class GenericClass<T> { private T t; public void put(T t) { this.t = t; } public T get() { return this.t; } public static void main(String[] args) { GenericClass<Integer> iClass = new GenericClass<Integer>(); GenericClass<String> sClass = new GenericClass<String>(); iClass.put(new Integer(10)); System.out.println(iClass.get()); sClass.put(new String("test.")); System.out.println(sClass.get()); } }
The syntax is very simple, that is, when defining a class, follow the class name (T is also optional, and there are several commonly used letters.) With generic classes, you can not only support different data types, but also be type safe, which is very good and powerful. That's exactly what I said at the beginning of this lecture - the role of genetics.
When defining generic methods just now, I said that the compiler is actually converted to Object. Is that true for generic classes? Let's have a look. After decompiling, you can see the following programming in main():
public static void main(String[] args) { GenericClass sClass = new GenericClass(); sClass.put(new String("test.")); System.out.println((String)sClass.get()); }
be careful:
In the first sentence, we write GenericClasssClass = new GenericClass();, After compilation, it becomes GenericClass # sClass = new # GenericClass(); In fact, it is an ordinary class.
The last sentence, when we wrote it, was sclass Get(), compiled into (string) sclass get(). The compiler automatically adds type conversion and checking.
So ah, in fact, generics is a compile time process, which uses Object internally, plus type conversion and checking. Under the code, there is no secret.
In the previous program, we defined two objects:
GenericClass<Integer> iClass = new GenericClass<Integer>(); GenericClass<String> sClass = new GenericClass<String>();
Is iClass and sClass one class or two? We can take a look at:
Add the following lines of code:
Class GenericClassIntegerCls = iClass.getClass(); Class GenericClassStringCls = sClass.getClass(); System.out.println(GenericClassIntegerCls); System.out.println(GenericClassStringCls); if (GenericClassIntegerCls.equals(GenericClassStringCls)) { System.out.println("the same"); }
The running result shows that "the same" is indeed the same class. This means that all generic types at run time are erased. Many pits caused by generics are caused by this, and beginners will be confused.
The interesting question is, since Java generics is just a compilation time, and the type is erased after compilation, can you cheat by other means and plug a value of a different type into a type? Do we remember reflection? It works dynamically at runtime and can deceive generic compilation. For example, ArrayList, we use list directly When add (100) tries to add a value, the compiler reports an error:
The method add(int, String) in the type List<String> is not applicable for the arguments (int).
Don't let me do it. We can do it with reflection:
Class<?> clz = list.getClass(); Method m; m = clz.getMethod("add", Object.class); m.invoke(name, 100);
You can try. We don't recommend this for actual code.
Although up to now, the examples are all one type parameter, generic classes can actually have multiple parameters. Let's take a look at an example. The code is as follows (Pair.java):
public class Pair<K,V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } public K getKey() { return key; } public V getValue() { return value; } }
The Pair generic class above requires two type parameters K and V.
The program used is as follows (Test.java):
public class Test { public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); } public static void main(String[] args){ Pair<Integer,String> p1 = new Pair<>(1,"aaa"); Pair<Integer,String> p2 = new Pair<>(1,"bbb"); System.out.println(Test.compare(p1, p2)); } }
In fact, these are the basic contents of generics. Indeed, it has achieved the purpose of "pan", which is more general and closer to human thinking. As I said in my previous lectures, the similarity between "problem space" and "solution space" determines the complexity of system implementation, and the technology closer to people's thinking structure will always be promising.
Here, I want to say a few more words. The IT change rapidly flower briefly as the broad-leaved epiphyllum, which can be said to be changing rapidly. Every day, there are different technologies, frameworks, different languages, different papers and different tools, and "foam is gradually becoming more attractive." at this time, how can we distinguish the main stream from the big technology trend or small technological improvement or even short-lived bubbles? It is very important and urgent, otherwise it will be lost in the rapid iteration of knowledge and skills. Many people say that programming is a young man's business. It's hard to work after the age of 35 or even 30. They eat youth food. In fact, this is a misunderstanding caused by not understanding the essence of technology. Technology is developing rapidly, but the basic theories, algorithms, mathematical models and methodologies are still relatively stable. We should grasp this essence, compare and choose, so as to be invincible. Someone said, "fashionable technology is like a wind and moon, and basic knowledge is like eternal sky".
Next, let's take a look at the places around generics. In the above example, we add a method:
public static void process(GenericClass<Number> obj){ System.out.println(obj.get()); }
The parameter of this process() is a GenericClass. We know that Number is the parent class of Integer. Can we pass GenericClass in the past? The method process (GenericClass) in the type GenericClass is not applicable for the arguments (GenericClass). The answer is No.
I want to emphasize here and focus on it, students. Integer is a subclass of Number, but GenericClass is not a subclass of GenericClass. There is a term called invariant.
Naturally, we can't write another overloaded method process(GenericClass), because for the reason of type erasure, it will be considered the same as process(GenericClass).
Do you want to change the method name? Sure, but it's too stupid. Java will not be so stupid. At this time, wildcards can be used. The code is as follows:
public static void process(GenericClass<?> obj){ System.out.println(obj.get()); }
? Wildcards can match any type. In this way, you can pass in GenericClass or GenericClass.
Once a problem is solved, it will lead to a new problem.? If all types are configured, the GenericClass can also be passed in. If the process() method is used for calculation, there will be a problem. Can you let it specify that the type of a class is passed in? The answer is yes. The code is as follows:
public static void process(GenericClass<? extends Number> obj){ System.out.println(obj.get()); }
Specifies that the parameter can be passed into Number and its subclasses. At this time, if a GenericClass is given when writing code, the compiler will report an error:
The method getVal(GenericClass<? extends Number>) in the type GenericClass<T> is not applicable for the arguments (GenericClass<String>).
We go back to the Covariant problem. We know that for this reason, the compiler stipulates that GenericClass cannot be passed to GenericClass. But sometimes we need this ability. Is it possible to find some way to realize it in disguise? Let's study it.
First look at the common code:
static List<String> str = Arrays.asList("Test String"); static List<Object> obj = Arrays.asList(new Object()); static class Reader<T> { T read(List<T> list) { return list.get(0); } } static void test() { Reader<Object> objReader = new Reader<Object>(); Object o = objReader.read(obj); //String s = objReader.read(str); }
The purpose of this code is very simple. It is to provide a general reader to read data from list. I hope it can support both list and list. But in String s = objreader read(str); In this line, the compiler will report an error: the # method read(List) in the # type Reader is not applicable for the arguments (List).
Can we use it? To achieve the goal, we modify it and give the program a covariant version. The code is as follows (CovariantReading.java):
import java.util.Arrays; import java.util.List; public class CovariantReading { static List<String> str = Arrays.asList("Test String"); static List<Object> obj = Arrays.asList(new Object()); static class CovariantReader<T> { T read(List<? extends T> list) { return list.get(0); } } static void cotest() { CovariantReader<Object> objReader = new CovariantReader<Object>(); Object o = objReader.read(obj); String s = (String) objReader.read(str); } public static void main(String[] args) { cotest(); } }
We declare T , read(List , list), which means that data can be read from the container of a certain type and its subclasses.
Similarly, we can write a covariant version of the writer with the following code (CovariantWriting.java):
import java.util.ArrayList; import java.util.List; public class CovariantWriting { static List<String> str = new ArrayList<String>(); static List<Object> obj = new ArrayList<Object>(); static class CovariantWriter<T> { void write(List<? super T> list, T item) { list.add(item); } } static void cotest() { CovariantWriter<String> strWriter = new CovariantWriter<String>(); strWriter.write(obj, "Test"); strWriter.write(str, "Test"); } public static void main(String[] args) { cotest(); } }
We declare to write an item of a certain type to the container of the type or parent type through write(List, T item).
In this way, it is somewhat disguised to realize the inheritance of parent-child relationship under generics. It seems like this.
In addition, under generics, types do not support primitive types, such as int and long, which must be Object. The reason is that the type erasure of generics is all made into objects. If you want to support int and long, you will have to consider that int and long are compatible with Object replication. It's troublesome. It's too late before the release of Java 5 version, so it's put on hold. This is a famous example of laziness by the Java design team.
Another classic problem is generic arrays. It's hard to understand. It's a big head.
In Java, it is not allowed to define generic arrays like new List[10]. This is the most incomprehensible place when I first came into contact with Java generics. I'm very angry. Write List[] name = new ArrayList[10]; Compilation error: Cannot create a generic array of ArrayList.
The reason is that there is a paragraph that is particularly good. The excerpt is as follows:
Covariant: It means you can assign subclass type array to its superclass array reference. For instance, Object objectArray[] = new Integer[10]; // it will work fine Invariant: It means you cannot assign subclass type generic to its super class generic reference because in generics any two distinct types are neither a subtype nor a supertype. For instance, List<Object> objectList = new ArrayList<Integer>(); // won't compile Because of this fundamental reason, arrays and generics do not fit well with each other.
I once wanted to explain this reason in other ways, but it seems that this explanation is the best. Let me explain again. I still say that.
Code can be clearer. Sun's document says "you can't create an array of exact generic types". For example, suppose what problems can be caused:
1) List<Integer> arrayOfIdList[] = new ArrayList<Integer>[10];// Suppose generic array creation is legal. 2) List<String> nameList = new ArrayList<String>(); 3) Object objArray[] = arrayOfIdList; // that is allowed because arrays are covariant 4) objArray[0] = nameList; 5) Integer id = objArray[0].get(0);
But with? sure.
1) List<?> arrayOfIdList[] = new ArrayList<?>[10]; 2) List<Integer> nameList = new ArrayList<Integer>(); 3) Object objArray[] = arrayOfIdList; 4) objArray[0] = nameList; 5) Integer id = (Integer)objArray[0].get(0);
This is very annoying. If we really want a generic Array, what should we do? There's a way. We can use newInstance of Array to generate generic arrays. Or bypass it with reflection.
GenericClass<Integer>[] lClass = (GenericClass<Integer>[])Array.newInstance(GenericClass.class, 10);
That's all for my introduction. I think this is the general content of advanced understanding.
Generics were developed some years after the emergence of Java. Historical compatibility should be considered. The goal of Java compatibility is backward compatibility. The previous code can still be used, so it limits the hands and feet. These bizarre problems with generics are obvious examples. Compared with C + +, Java generics have many limitations and difficult to understand, so that master Bruce Eckel once wrote an article saying: This is not generics. However, we should consider the difficulties of the Java design team and consider history. We must do something, but we can't do it. Generics are not generics, and there is no exact standard. The Java team also has a roadmap and continues to improve generics. According to my understanding, generic arrays and primitive types will be better supported in the near future.
Technology always advances on the basis of our predecessors, but we should look at this process from a historical perspective, not easily deny our predecessors, and have an on-the-spot understanding of their shortcomings. The ancients said: "in order to go to the saint and continue the unique learning", generations of technicians passed on from generation to generation, stood on the shoulders of their predecessors, deepened their understanding and changed the world. Young students are born at the right time. Readers should work hard.