Came, saw, generalized: we plunge into Java Generics

Java Generics is one of the most significant changes in the history of the Java language. Generics, available with Java 5, have made using the Java Collection Framework simpler, more convenient, and safer. Errors related to incorrect use of types are now detected at compile time. And the Java language itself has become even safer. Despite the seeming simplicity of generic types, many developers have difficulty using them. In this post I will talk about the features of working with Java Generics, so that you have fewer difficulties. It is useful if you are not a guru in generics, and will help to avoid many difficulties when immersing in the topic.



Work with collections


Suppose a bank needs to calculate the amount of savings in customer accounts. Before the advent of “generics”, the sum calculation method looked like this:

public long getSum(List accounts) {   long sum = 0;   for (int i = 0, n = accounts.size(); i < n; i++) {       Object account = accounts.get(i);       if (account instanceof Account) {           sum += ((Account) account).getAmount();       }   }   return sum; } 

We iterated, ran through the list of accounts, and checked whether the item from this list was really an instance of the Account class — that is, the user's account. The type of our object of class Account and the getAmount method that returned the amount on this account were getAmount . Then it was all summed up and returned the total amount. Two steps were required:
 if (account instanceof Account) { // (1) 

 sum += ((Account) account).getAmount(); // (2) 

If you do not make an ( instanceof ) test for belonging to the class Account , then at the second stage ClassCastException is possible - that is, the program crashes. Therefore, such a check was mandatory.

With the advent of Generics, the need to check and type cast disappeared:
 public long getSum2(List<Account> accounts) {  long sum = 0;  for (Account account : accounts) {      sum += account.getAmount();  }  return sum; } 

Now the method
 getSum2(List<Account> accounts) 
takes as arguments only the list of objects of the class Account . This limitation is indicated in the method itself, in its signature, the programmer simply cannot transfer any other list — only the list of client accounts.

We do not need to check the type of elements from this list: it is implied by the type description of the method parameter
 List<Account> accounts 
(can be read as a Account ). And the compiler will give an error if something goes wrong - that is, if someone tries to transfer to this method a list of objects other than the Account class.

In the second line of the test, the necessity also disappeared. If required, casting will be done at compile time.

Substitution principle


Barbara Liskov's substitution principle is a specific definition of a subtype in object-oriented programming. Liskov's idea of ​​a “subtype” defines the concept of substitution: if S is a subtype of T , then objects of type T in the program can be replaced by objects of type S without any changes to the desired properties of this program.

Type of
Subtype
Number
Integer
List <E>
ArrayList <E>
Collection <E>
List <E>
Iterable <E>
Collection <E>

Type / Subtype Relationship Examples

Here is an example of using the principle of substitution in Java:
 Number n = Integer.valueOf(42); List<Number> aList = new ArrayList<>(); Collection<Number> aCollection = aList; Iterable<Number> iterable = aCollection; 

Integer is a Number subtype, therefore, you can assign a value to a variable of type Number , which is returned by the Integer.valueOf(42) method.

Covariance, contravariance and invariance


First, a little theory. Covariance is the preservation of the inheritance hierarchy of source types in derived types in the same order. For example, if the Cat is a subtype of Animals , then the Set <Cats> is a subtype of Set <Animals> . Therefore, taking into account the principle of substitution, you can perform this assignment:

Set <Animals> = Set <Cats>

Contravariance is the inversion of the hierarchy of source types to the opposite in derived types. For example, if Cat is a subtype of , then Set <Animals> is a subtype of Set <Cats> . Therefore, taking into account the principle of substitution, you can perform this assignment:

Set <Cats> = Set <Animals>

Invariance is the lack of inheritance between derived types. If the Cat is a subtype of Animals , then the Set <Cats> is not a subtype of the Set <Animals> and the Set <Animals> is not a subtype of the Set <Cats> .

Java arrays are covariant . Type S[] is a subtype of T[] if S is a subtype of T Assignment Example:
 String[] strings = new String[] {"a", "b", "c"}; Object[] arr = strings; 

We assigned a reference to an array of strings to the variable arr , the type of which is « » . If the arrays were not covariant, we would not be able to do this. Java allows you to do this, the program will compile and run without errors.

 arr[0] = 42; // ArrayStoreException.       

But if we try to change the contents of the array through the variable arr and write the number 42 there, we get an ArrayStoreException at the program execution stage, since 42 is not a string, but a number. This is a disadvantage of the covariance of Java arrays: we cannot perform checks at the compilation stage, and something can break already in runtime.

"Generics" are invariant. Let's give an example:
 List<Integer> ints = Arrays.asList(1,2,3); List<Number> nums = ints; // compile-time error.      nums.set(2, 3.14); assert ints.toString().equals("[1, 2, 3.14]"); 

If you take a list of integers, it will not be a subtype of the Number type, nor any other subtype. He is only a subtype of himself. That is, List <Integer> is List<Integer> and nothing else. The compiler will take care that the ints variable, declared as a list of objects of the Integer class, contains only objects of the Integer class and nothing else. At the compilation stage, a check is performed, and nothing will fall in our runtime.

Wildcards


Are Generics Invariants Always? Not. I will give examples:
 List<Integer> ints = new ArrayList<Integer>(); List<? extends Number> nums = ints; 

This is covariance. List<Integer> - subtype List<? extends Number> List<? extends Number>

 List<Number> nums = new ArrayList<Number>(); List<? super Integer> ints = nums; 

This contravariant. List<Number> is a subtype of List<? super Integer> List<? super Integer> .

A record of the form "? extends ..." or "? super ..." is called a wildcard or wildcard, with an upper bound ( extends ) or a lower bound ( super ). List<? extends Number> List<? extends Number> can contain objects whose class is Number or inherited from Number . List<? super Number> List<? super Number> may contain objects whose class is Number or whose Number is a descendant (supertype from Number ).


extends B - wildcard with upper bound
super B - wildcard with lower bound
where B is the border

An entry of the form T 2 <= T 1 means that the set of types described by T 2 is a subset of the set of types described by T 1

those.
Number <=? extends Object
? extends Number <=? extends Object
and
? super Object <=? super number


More mathematical interpretation of the topic

A couple of tasks for testing knowledge:

1. Why in the example below compile-time error? What value can be added to the nums list?
 List<Integer> ints = new ArrayList<Integer>(); ints.add(1); ints.add(2); List<? extends Number> nums = ints; nums.add(3.14); // compile-time error 

Answer
If the container is declared with wildcard ? extends ? extends , you can only read the values. Nothing can be added to the list except null . In order to add an object to the list we need another type of wildcard - ? super ? super


2. Why can't I get an item from the list below?
 public static <T> T getFirst(List<? super T> list) {  return list.get(0); // compile-time error } 

Answer
Can't read item from container with wildcard ? super ? super , except for an object of class Object

 public static <T> Object getFirst(List<? super T> list) {  return list.get(0); } 



The Get and Put Principle or PECS (Producer Extends Consumer Super)


The wildcard feature with the upper and lower bound provides additional features associated with the safe use of types. From one type of variables, you can only read, to another - just enter it (the exception is the ability to write null for extends and read Object for super ). To make it easier to remember which wildcard to use, there is the principle of PECS - Producer Extends Consumer Super.


Consider the use of Wildcard and the PECS principle on the example of the copy method in the java.util.Collections class.

 public static <T> void copy(List<? super T> dest, List<? extends T> src) { … } 

The method copies the elements from the source src list to the dest list. src - declared with wildcard ? extends ? extends and is the producer, and dest - declared with a wildcard ? super ? super and is a consumer. Given the wildcard covariance and contravariance, you can copy elements from the ints list to the nums list:
 List<Number> nums = Arrays.<Number>asList(4.1F, 0.2F); List<Integer> ints = Arrays.asList(1,2); Collections.copy(nums, ints); 


If we mistakenly mistake the parameters of the copy method and try to copy from the nums list to the ints list, the compiler will not allow us to do this:
 Collections.copy(ints, nums); // Compile-time error 


<?> and raw types


Below is a wildcard with an unlimited wildcard. We simply put <?> , Without the keywords super or extends :
 static void printCollection(Collection<?> c) {  // a wildcard collection  for (Object o : c) {      System.out.println(o);  } } 


In fact, such an “unlimited” wildcard is still limited, from above. Collection<?> Is also a wildcard character, just like " ? extends Object ". A record of type Collection<?> equivalent to Collection<? extends Object> Collection<? extends Object> , which means - the collection can contain objects of any class, since all classes in Java are inherited from Object - so the substitution is called unbounded.

If we omit the type indication, for example, like this:
 ArrayList arrayList = new ArrayList(); 

then, they say that ArrayList is the Raw type of parameterized ArrayList <T> . Using Raw types, we return to the pre-generic era and consciously discard all the features inherent in parametrized types.

If we try to call a parameterized method on the Raw type, the compiler will give us a warning "Unchecked call". If we try to assign the reference to the parameterized Raw type to the type, the compiler will generate the warning “Unchecked assignment”. Ignoring these warnings, as we will see later, can lead to errors during the execution of our application.
 ArrayList<String> strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings; // Ok strings = arrayList; // Unchecked assignment arrayList.add(1); //unchecked call 


Wildcard capture


Let us now try to implement a method that permutes the elements of the list in the reverse order.

 public static void reverse(List<?> list); // ! public static void reverse(List<?> list) { List<Object> tmp = new ArrayList<Object>(list); for (int i = 0; i < list.size(); i++) {   list.set(i, tmp.get(list.size()-i-1)); // compile-time error } } 

A compilation error occurred because in the reverse method a list with an unbounded wildcard <?> taken as an argument.
<?> means the same as <? extends Object> <? extends Object> . Therefore, according to the PECS principle, the list is the producer . A producer only produces elements. And in the for loop we call the set() method, i.e. trying to write to the list . And therefore we rest against the protection of Java, which does not allow setting any value by index.

What to do? We will help Wildcard Capture pattern. Here we create a generic rev method. It is declared with a variable of type T This method takes a list of types T , and we can make a set.
 public static void reverse(List<?> list) { rev(list); } private static <T> void rev(List<T> list) { List<T> tmp = new ArrayList<T>(list); for (int i = 0; i < list.size(); i++) {   list.set(i, tmp.get(list.size()-i-1)); } } 

Now we have everything compiled. This is where the wildcard capture was captured. When calling the reverse(List<?> list) method reverse(List<?> list) , a list of some objects (for example, strings or integers) is passed as an argument. If we can capture the type of these objects and assign it to a variable of type X , then we can conclude that T is X

Read more about Wildcard Capture here and here .

Conclusion


If you need to read from the container, then use a wildcard with the upper limit " ? extends ". If you need to write to the container, then use a wildcard with a lower " ? super ". Do not use a wildcard if you need to write and read.

Do not use Raw types! If the type argument is not defined, use a wildcard <?> .

Type variables


When we write an identifier in angle brackets when declaring a class or method, for example, <T> or <E> , we create a type variable . A type variable is an unqualified identifier that can be used as a type in the body of a class or method. A type variable can be bounded above.
 public static <T extends Comparable<T>> T max(Collection<T> coll) { T candidate = coll.iterator().next(); for (T elt : coll) {   if (candidate.compareTo(elt) < 0) candidate = elt; } return candidate; } 

In this example, the T extends Comparable<T> expression defines T (type variable), bounded above by the type Comparable<T> . Unlike a wildcard, type variables can only be bounded from above (only extends ). Cannot write super . In addition, in this example, T depends on itself; this is called a recursive bound - a recursive boundary.

Here is another example from the Enum class:
 public abstract class Enum<E extends Enum<E>>implements Comparable<E>, Serializable 

Here, the Enum class is parameterized by the type E, which is a subtype of Enum<E> .

Multiple bounds


Multiple Bounds - multiple restrictions. It is written through the character " & ", that is, we say that the type represented by a variable of type T must be bounded above by the Object class and the Comparable interface.

 <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) 

Record Object & Comparable<? super T> Object & Comparable<? super T> forms the type of intersection Multiple Bounds . The first limitation - in this case Object - is used for erasure , the type mashing process. It is performed by the compiler at compile time.

Conclusion


A type variable can only be bounded above by one or more types. In the case of multiple constraints, the left margin (the first constraint) is used during the mashing process (Type Erasure).

Type erasure


Type Erasure is a mapping of types (possibly including parameterized types and type variables) to types that are never parametrized types or variable types. We write T type mashing as |T| .

The rubbing mapping is defined as follows:


During the execution of Type Erasure (type mashing), the compiler performs the following actions:


T (Type)
| T | (Rubbing type)
List <Integer>, List <String>, List <List <String >>
List
List <Integer> []
List []
List
List
int
int
Integer
Integer
<T extends Comparable <T >>
Comparable
<T extends Object & Comparable <? super T >>
Object
LinkedCollection <E> .Node
LinkedCollection.Node

This table shows what the different types are turning into in the process of mashing, Type Erasure.

In the screenshot below, two program examples:


The difference between them is that on the left a compile-time error occurs, and on the right, everything compiles without errors. Why?

Answer
In Java, two different methods cannot have the same signature. In the Type Erasure process, the compiler will add the public int compareTo(Object o) bridge method. But the class already contains a method with such a signature, which will cause an error at compile time.

Compile the Name class, removing the compareTo(Object o) method, and look at the resulting bytecode using javap:
 # javap Name.class Compiled from "Name.java" public class ru.sberbank.training.generics.Name implements java.lang.Comparable<ru.sberbank.training.generics.Name> { public ru.sberbank.training.generics.Name(java.lang.String); public java.lang.String toString(); public int compareTo(ru.sberbank.training.generics.Name); public int compareTo(java.lang.Object); } 

We see that the class contains the int compareTo(java.lang.Object) method, although we have removed it from the source code. This is the bridge method that the compiler added.


Reifiable types


In Java, we say that a type is reifiable if information about it is fully accessible at runtime. The reifiable types include:


Why is information on some types available and not on others? The fact is that due to the type overwriting process by the compiler, information about some types may be lost. If it is lost, then this type will no longer be reifiable. That is, it is not available at runtime. If available - respectively, reifiable.

The decision not to make all generic types available at runtime is one of the most important and controversial design decisions in the Java type system. So did, first of all, for compatibility with existing code. I had to pay for migratory compatibility - full availability of the system of generalized types at runtime is impossible.

Which types are not reifiable:


And one more problem. Why in the example below it is impossible to create a parameterized Exception?

 class MyException<T> extends Exception {  T t; } 

Answer
Each catch expression in try-catch checks the type of the exception received at runtime (which is equivalent to instanceof), respectively, the type must be Reifiable. Therefore, Throwable and its subtypes cannot be parameterized.

 class MyException<T> extends Exception {// Generic class may not extend 'java.lang.Throwable'  T t; } 



Unchecked warnings


Compiling our application may give the so-called Unchecked Warning - a warning that the compiler was unable to correctly determine the level of security using our types. This is not a mistake, but a warning, so you can skip it. But it is advisable to fix everything in order to avoid problems in the future.

Heap Pollution


As we mentioned earlier, assigning a reference to the Raw type of a variable of a parameterized type results in the warning “Unchecked assignment”. If we ignore it, a situation called " Heap Pollution " (heap pollution) is possible. Here is an example:
 static List<String> t() {  List l = new ArrayList<Number>();  l.add(1);  List<String> ls = l; // (1)  ls.add("");  return ls; } 

In line (1), the compiler warns about “Unchecked assignment”.

We need to cite another example of “pollution of the heap” - when we use parameterized objects. The code snippet below vividly demonstrates that it is unacceptable to use parameterized types as method arguments using Varargs . In this case, the parameter of the method m is List<String>… , i.e. in fact, an array of elements of type List<String> . Given the type mapping rule for mashing, the type stringLists becomes a raw list array ( List[] ), i.e. you can perform the assignment Object[] array = stringLists; and then write to the array an object other than the list of lines (1), which will cause a ClassCastException in line (2).

 static void m(List<String>... stringLists) {  Object[] array = stringLists;  List<Integer> tmpList = Arrays.asList(42);  array[0] = tmpList; // (1)  String s = stringLists[0].get(0); // (2) } 


Consider another example:
 ArrayList<String> strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings; // (1) Ok arrayList.add(1); // (2) unchecked call 

Java permits assignment in string (1). This is necessary for backward compatibility. But if we try to execute the add method in line (2), we get a warning Unchecked call - the compiler warns us of a possible error. In fact, we are trying to add an integer to the list of strings.

Reflection


Although during compilation, parameterized types undergo a type erasure procedure, we can get some information using Reflection.


Reflection Reifiable , . , , , - , :
 java.lang.reflect.Method.getGenericReturnType() 

Generics java.lang.Class . :
 List<Integer> ints = new ArrayList<Integer>(); Class<? extends List> k = ints.getClass(); assert k == ArrayList.class; 


ints List<Integer> ArrayList< Integer> . ints.getClass() Class<ArrayLis> , List<Integer> List . Class<ArrayList> k Class<? extends List> , ? extends . ArrayList.class Class<ArrayList> .

Conclusion


, Reifiable. Reifiable : , , , Raw , reifiable.

Unchecked Warnings « » .

Reflection , Reifiable. Reflection , .

Type Inference


« ». () . :
 List<Integer> list = new ArrayList<Integer>(); 

- Java 7 ArrayList :
 List<Integer> list = new ArrayList<>(); 

ArrayListList<Integer> . type inference .

Java 8 JEP 101.
Type Inference. :

: , , — .
, . JEP 101 .

, :
 class List<E> {  static <Z> List<Z> nil() { ... };  static <Z> List<Z> cons(Z head, List<Z> tail) { ... };  E head() { ... } } 

List.nil() :
 List<String> ls = List.nil(); 

, List.nil() String — JDK 7, .

, , , :
 List.cons(42, List.nil()); //error: expected List<Integer>, found List<Object> 

JDK 7 compile-time error. JDK 8 . JEP-101, — . JDK 8 — :
 List.cons(42, List.<Integer>nil()); 


JEP-101 , , :
 String s = List.nil().head(); //error: expected String, found Object 

, . , JDK , :
 String s = List.<String>nil().head(); 


JEP 101 StackOverflow . , , 7- , 8- – ? :
 class Test {  static void m(Object o) {      System.out.println("one");  }  static void m(String[] o) {      System.out.println("two");  }  static <T> T g() {      return null;  }  public static void main(String[] args) {      m(g());  } } 


- JDK1.8:
   public static void main(java.lang.String[]);   descriptor: ([Ljava/lang/String;)V   flags: ACC_PUBLIC, ACC_STATIC   Code:     stack=1, locals=1, args_size=1        0: invokestatic  #6   // Method g:()Ljava/lang/Object;        3: checkcast     #7   // class "[Ljava/lang/String;"        6: invokestatic  #8   // Method m:([Ljava/lang/String;)V        9: return     LineNumberTable:       line 15: 0       line 16: 9 


0 g:()Ljava/lang/Object; java.lang.Object . , 3 («») , java.lang.String , 6 m:([Ljava/lang/String;) , «two».

- JDK1.7 – Java 7:
   public static void main(java.lang.String[]);   flags: ACC_PUBLIC, ACC_STATIC   Code:     stack=1, locals=1, args_size=1        0: invokestatic  #6   // Method g:()Ljava/lang/Object;        3: invokestatic  #7   // Method m:(Ljava/lang/Object;)V        6: return            LineNumberTable:       line 15: 0       line 16: 6 


, checkcast , Java 8, m:(Ljava/lang/Object;) , «one». Checkcast – , Java 8.

, Oracle JDK1.7 JDK 1.8 , Java, , .

, Java 8 , Java 7, :

 public static void main(String[] args) { m((Object)g()); } 


Conclusion


Java Generics . , :



, Java Generics.

Source: https://habr.com/ru/post/416413/


All Articles