1. Proposing the concept of generics (why are generics needed)?
First, let's look at the following short code:
public class GenericTest { public static void main(String[] args) { List list = new ArrayList(); list.add("qqyumidi"); list.add("corn"); list.add(100); for (int i = 0; i < list.size(); i++) { String name = (String) list.get(i); // 1 System.out.println("name:" + name); } }}Define a collection of List type, first add two string type values to it, and then add an Integer type value. This is completely allowed because the default type of list is Object at this time. In the subsequent loop, it is easy to have errors similar to //1 because I forgot to add Integer type values or other encoding reasons in the list before. Because the compilation stage is normal, the "java.lang.ClassCastException" exception will appear at runtime. Therefore, such errors are difficult to detect during the encoding process.
During the encoding process as above, we found that there are two main problems:
1. When we put an object into the collection, the collection will not remember the type of this object. When this object is taken out from the collection again, the compile type of the changed object becomes the Object type, but its runtime type is still its own type.
2. Therefore, when taking out the collection element at //1, artificially forced types need to be converted to the specific target type, and it is easy to see the "java.lang.ClassCastException" exception.
So is there any way to make the collection remember various types of elements in the collection, and to achieve that as long as there is no problem during compilation, there will be no "java.lang.ClassCastException" exception during runtime? The answer is to use generics.
2. What is a generic?
Generics, that is, "parameterized type". When it comes to parameters, the most familiar thing is to have concrete parameters when defining a method, and then pass the actual parameters when calling this method. So how do you understand the parameterization type? As the name suggests, it means parameterizing the type from the original specific type, similar to the variable parameters in the method. At this time, the type is also defined as a parameter form (can be called a type formal parameter), and then the specific type (type actual parameter) is passed in when used/invoked.
It seems a bit complicated. First, let’s take a look at the above example using generic writing.
public class GenericTest { public static void main(String[] args) { /* List list = new ArrayList(); list.add("qqyumidi"); list.add("corn"); list.add(100); */ List<String> list = new ArrayList<String>(); list.add("qqyumidi"); list.add("corn"); //list.add(100); // 1 prompts a compilation error for (int i = 0; i < list.size(); i++) { String name = list.get(i); // 2 System.out.println("name:" + name); } }}After using generic writing, a compilation error occurs when you want to add an Integer type object at //1. Through List<String>, it is directly limited that only elements of String type can be contained in the list collection, so there is no need to cast type at //2, because at this time, the collection can remember the type information of the element, and the compiler can confirm that it is String type.
Combining the above generic definition, we know that in List<String>, String is a type parameter, that is, the corresponding List interface must contain type formal parameters. Moreover, the return result of the get() method is directly this formal parameter type (that is, the corresponding incoming type parameter). Let’s take a look at the specific definition of the List interface:
public interface List<E> extends Collection<E> { int size(); boolean isEmpty(); boolean contains(Object o); Iterator<E> iterator(); Object[] toArray(); <T> T[] toArray(T[] a); boolean add(E e); boolean remove(Object o); boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); boolean addAll(int index, Collection<? extends E> c); boolean removeAll(Collection<?> c); boolean retainAll(Collection<?> c); void clear(); boolean equals(Object o); int hashCode(); E get(int index); E set(int index, E element); void add(int index, E element); E remove(int index); int indexOf(Object o); int lastIndexOf(Object o); ListIterator<E> listIterator(); ListIterator<E> listIterator(int index); List<E> subList(int fromIndex, int toIndex);}We can see that after the generic definition is adopted in the List interface, E in <E> represents a type formal parameter, which can receive specific type parameters. In this interface definition, where E appears, it means that the same type parameters accepted from the outside are accepted.
Naturally, ArrayList is an implementation class for the List interface, and its definition form is:
From this, we understand from the source code perspective why the Integer type object is compiled incorrectly at //1, and the type obtained at //2 is directly the String type.
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } public E get(int index) { rangeCheck(index); checkForComodification(); return ArrayList.this.elementData(offset + index); } //...Omit other specific definition processes}3. Customize generic interfaces, generic classes and generic methods
From the above content, everyone has understood the specific operation process of generics. It is also known that interfaces, classes and methods can also be defined using generics and used accordingly. Yes, when used specifically, it can be divided into generic interfaces, generic classes and generic methods.
Custom generic interfaces, generic classes and generic methods are similar to List and ArrayList in the above Java source code. As follows, we look at the simplest generic class and method definition:
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); System.out.println("name:" + name.getData()); }}class Box<T> { private T data; public Box() { } public Box(T data) { this.data = data; } public T getData() { return data; }}In the process of defining generic interfaces, generic classes and generic methods, our common parameters such as T, E, K, V, etc. are often used to represent generic formal parameters because they receive type parameters passed from external use. So for different types of incoming parameters, are the types of the corresponding object instances generated the same?
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); Box<Integer> age = new Box<Integer>(712); System.out.println("name class:" + name.getClass()); // com.qqyumidi.Box System.out.println("age class:" + age.getClass()); // com.qqyumidi.Box System.out.println(name.getClass() == age.getClass()); // true }}From this, we found that when using generic classes, although different generic arguments are passed in, different types are not generated in the true sense. There is only one generic class passing in different generic arguments in memory, that is, it is still the original most basic type (Box in this example). Of course, logically, we can understand it as multiple different generic types.
The reason is that the purpose of the concept of generics in Java is that it only works in the code compilation stage. During the compilation process, after correctly checking the generic results, the relevant information of the generics will be erased. That is to say, the successfully compiled class file does not contain any generic information. Generic information will not enter the runtime phase.
This is summarized in one sentence: generic types are logically regarded as multiple different types, and are actually the same basic types.
Four. Type wildcard
Following the above conclusion, we know that Box<Number> and Box<Integer> are actually both Box types. Now we need to continue to explore a question. So, logically, can Box<Number> and Box<Integer> be regarded as generic types with parent-child relationships?
To clarify this problem, let's continue to look at the following example:
public class GenericTest { public static void main(String[] args) { Box<Number> name = new Box<Number>(99); Box<Integer> age = new Box<Integer>(712); getData(name); //The method getData(Box<Number>) in the type GenericTest is //not applicable for the arguments (Box<Integer>) getData(age); // 1 } public static void getData(Box<Number> data){ System.out.println("data :" + data.getData()); }}We found that an error message appeared at code //1: The method getData(Box<Number>) in the t ype GenericTest is not applicable for the arguments (Box<Integer>). Obviously, by prompting information, we know that Box<Number> cannot be logically considered as the parent class of Box<Integer>. So, what is the reason?
public class GenericTest { public static void main(String[] args) { Box<Integer> a = new Box<Integer>(712); Box<Number> b = a; // 1 Box<Float> f = new Box<Float>(3.14f); b.setData(f); // 2 } public static void getData(Box<Number> data) { System.out.println("data :" + data.getData()); }}class Box<T> { private T data; public Box() { } public Box(T data) { setData(data); } public T getData() { return data; } public void setData(T data) { this.data = data; }}In this example, there will be an error message at //1 and //2. Here we can use the counter-proof method to explain it.
Assuming that Box<Number> can be logically regarded as the parent class of Box<Integer>, then there will be no error prompts at //1 and //2. Then the problem arises. What type is it when fetching data through the getData() method? Integer? Float? or Number? Moreover, due to the uncontrollable order in the programming process, type judgment must be made when necessary and type conversion is performed. Obviously, this contradicts the idea of generics, so logically, Box<Number> cannot be regarded as the parent class of Box<Integer>.
OK, then let's look back at the first example in "Type Wildcards", we know the deeper reason for its specific error prompts. So how to solve it? The headquarters can define a new function. This is obviously contrary to the polymorphism concept in Java, so we need a reference type that can be logically used to represent the parent class of both Box<Integer> and Box<Number>, and thus, the type wildcard came into being.
Type wildcards are generally used instead of specific type arguments. Note that this is a type parameter, not a type parameter! And Box<?> is logically the parent class of all Box<Integer>, Box<Number>..., etc. Therefore, we can still define generic methods to fulfill such requirements.
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); Box<Integer> age = new Box<Integer>(712); Box<Number> number = new Box<Number>(314); getData(name); getData(age); getData(number); } public static void getData(Box<?> data) { System.out.println("data :" + data.getData()); }}Sometimes, we may also hear about the upper and lower types of wildcards. What exactly is it like?
In the above example, if you need to define a method that functions similar to getData(), but there are further restrictions on type arguments: it can only be the Number class and its subclasses. At this time, the upper limit of type wildcards is required.
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); Box<Integer> age = new Box<Integer>(712); Box<Number> number = new Box<Number>(314); getData(name); getData(age); getData(number); //getUpperNumberData(name); // 1 getUpperNumberData(age); // 2 getUpperNumberData(number); // 3 } public static void getData(Box<?> data) { System.out.println("data :" + data.getData()); } public static void getUpperNumberData(Box<? extends Number> data){ System.out.println("data :" + data.getData()); }}At this point, obviously, the call at code //1 will appear an error message, while the call at //2 //3 will be normal.
The upper limit of type wildcards is defined by the form of Box<? extends Number>. Correspondingly, the lower limit of type wildcards is the form of Box<? super Number>, and its meaning is exactly the opposite of the upper limit of type wildcards. I will not explain it too much here.
5. Extra chapter
The examples in this article are mainly cited to illustrate some ideas in generics and do not necessarily have practical usability. In addition, when it comes to generics, I believe that what you use most is in the collection. In fact, in the actual programming process, you can use generics to simplify development and can ensure the quality of the code well. And one thing to note is that there is no so-called generic array in Java.
For generics, the most important thing is to understand the ideas and purposes behind them.
The above is a compilation of knowledge and information on Java generics. We will continue to add relevant information in the future. Thank you for your support for this site!