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) {
sum += ((Account) account).getAmount();
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 ExamplesHere 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;
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;
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 topicA 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);
AnswerIf 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);
AnswerCan'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.
- If we declared a wildcard with extends , then it is producer . It only "produces", provides an element from the container, but does not accept anything.
- If we declared a wildcard with super , then this is a consumer . He only accepts, but can not provide anything.
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);
<?> 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) {
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;
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);
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
XRead 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:
- The rubbing of the parameterized type G < T1 , ..., Tn > is | G |
- Overwriting nested type TC is | T |. C
- The rubbing of the array type T [] is | T | []
- Rubbing a type variable is rubbing its left border.
- Mashing any other type is this type itself.
During the execution of Type Erasure (type mashing), the compiler performs the following actions:
- adds casting to ensure type safety, if necessary
- generates bridge methods to save polymorphism
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?
AnswerIn 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:
- Primitive types ( int , long , boolean )
- Non-parameterized (non-generic) types ( String , Integer )
- Parameterized types whose parameters are represented as an unbounded wildcard (unlimited wildcard characters) ( List <?> , Collection <?> )
- Raw (unformed) types ( List , ArrayList )
- Arrays whose components are Reifiable types ( int [] , Number [] , List <?> [] , List [ )
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:
- Variable type ( t )
- Parameterized type with specified parameter type ( List <Number> ArrayList <String> , List <List <String >> )
- Parameterized type with specified upper or lower bound ( List <? Extends Number>, Comparable <? Super String> ). But here it is worth mentioning: List <? extends Object> is not reifiable, but List <?> is 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; }
AnswerEach 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 {
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;
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;
Consider another example:
ArrayList<String> strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings;
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.
- All reifiable are available through the Reflection mechanism.
- Information about the type of class fields, the parameters of the methods and the values they return is available via 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<>();
ArrayList –
List<Integer> .
type inference .
Java 8 JEP 101.
Type Inference. :
- (reduction)
- (incorporation)
- (resolution)
: , , — .
, . 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());
JDK 7 compile-time error. JDK 8 . JEP-101, — . JDK 8 — :
List.cons(42, List.<Integer>nil());
JEP-101 , , :
String s = List.nil().head();
, . , 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
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
,
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 . , :
- Bloch, Joshua. Effective Java. Third Edition. Addison-Wesley. ISBN-13: 978-0-13-468599-1
, Java Generics.