Introduction
Starting with JSE 5.0, generics have been added to the Java language arsenal.What are generics in Java?
Generics (generalizations) are special tools of the Java language for implementing generic programming: a special approach to describing data and algorithms that allows you to work with different types of data without changing their description. There is a separate tutorial dedicated to generics on the Oracle website: " Lesson: Generics ".
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello");
String text = list.get(0) + ", world!";
System.out.print(text);
}
}
This code will run well. But what if they came to us and said that the phrase "Hello, world!" beaten and you can return only Hello? Let's remove the concatenation with the string from the code ", world!"
. It would seem, what could be more harmless? But in fact, we will get an error WHEN COMPILING : error: incompatible types: Object cannot be converted to String
The thing is that in our case List stores a list of objects of type Object. Since String is a successor to Object (because all classes are implicitly inherited from Object in Java), it requires an explicit cast, which we did not. And when concatenating, the static method String.valueOf(obj) will be called for the object, which will eventually call the toString method for Object. That is, our List contains Object. It turns out that where we need a specific type, and not Object, we will have to do the type casting ourselves:
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println((String)str);
}
}
}
However, in this case, because List takes a list of objects, it stores not only String but also Integer. But the worst thing is that in this case the compiler will not see anything wrong. And here we will get an error already DURING RUNNING (they also say that the error was received "at Runtime"). The error will be: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Agree, not the most pleasant. And all this is because the compiler is not artificial intelligence and it cannot guess everything that the programmer means. To tell the compiler more about our intentions, what types we are going to use, Java SE 5 introduced generics . Let's fix our version by telling the compiler what we want:
import java.util.*;
public class HelloWorld {
public static void main(String []args){
List<String> list = new ArrayList<>();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println(str);
}
}
}
As we can see, we no longer need to cast to String. In addition, we now have angle brackets around generics. Now the compiler will not allow the class to be compiled until we remove the addition of 123 to the list, because this is Integer. He will tell us so. Many people call generics "syntactic sugar". And they are right, since generics will indeed become the same casts when compiled. Let's look at the bytecode of the compiled classes: with manual customization and using generics:
Raw Types or Raw Types
Speaking of generics, we always have two categories: typed types (Generic Types) and "raw" types (Raw Types). Raw types are types without a "specifier" in curly braces (angle brackets):<>
Also, Diamond syntax is associated with the concept of " Type Inference ", or type inference. After all, the compiler, seeing <> on the right, looks at the left side, where the type declaration of the variable is located, into which the value is assigned. And for this part, he understands what type the value on the right is typed. In fact, if a generic is specified on the left side and no generic is specified on the right side, the compiler will be able to infer the type:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = new ArrayList();
list.add("Hello World");
String data = list.get(0);
System.out.println(data);
}
}
However, this will be a mix of the new style with generics and the old style without them. And this is highly undesirable. When compiling the code above, we will get the message: Note: HelloWorld.java uses unchecked or unsafe operations
. In fact, it seems incomprehensible why diamond is needed here at all. But here's an example:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = Arrays.asList("Hello", "World");
List<Integer> data = new ArrayList(list);
Integer intNumber = data.get(0);
System.out.println(data);
}
}
As we remember, ArrayList also has a second constructor that takes a collection as input. And this is where the trickiness lies. Without the diamond syntax, the compiler does not understand that it is being deceived, but with the diamond syntax, it does. Therefore, rule #1 is to always use the diamond syntax if we are using typed types. Otherwise, we risk missing where we use raw type. To avoid warnings in the log that "uses unchecked or unsafe operations" you can specify a special annotation over the used method or class: @SuppressWarnings("unchecked")
Suppress is translated as suppress, that is, literally - suppress warnings. But think about why you decided to specify it? Remember rule number one and maybe you need to add some typing.
Typed methods (Generic Methods)
Generics allow you to type methods. This feature is covered in a separate section in Oracle's tutorial: " Generic Methods ". From this tutorial, it is important to remember about the syntax:- includes a list of typed parameters inside angle brackets;
- the list of typed parameters comes before the returned method.
import java.util.*;
public class HelloWorld{
public static class Util {
public static <T> T getValue(Object obj, Class<T> clazz) {
return (T) obj;
}
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList("Author", "Book");
for (Object element : list) {
String data = Util.getValue(element, String.class);
System.out.println(data);
System.out.println(Util.<String>getValue(element));
}
}
}
If we look at the Util class, we see two typed methods in it. With type inference, we can provide a type definition directly to the compiler, or we can specify it ourselves. Both options are shown in the example. By the way, the syntax is quite logical, if you think about it. When typing a method, we specify the generic BEFORE the method, because if we use the generic after the method, Java won't be able to figure out which type to use. Therefore, we first declare that we will use the generic T, and then we say that we are going to return this generic. Naturally, Util.<Integer>getValue(element, String.class)
it will fall with an error incompatible types: Class<String> cannot be converted to Class<Integer>
. When using typed methods, you should always remember about type erasure. Let's look at an example:
import java.util.*;
public class HelloWorld {
public static class Util {
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList(2, 3);
for (Object element : list) {
System.out.println(Util.<Integer>getValue(element) + 1);
}
}
}
It will work great. But only as long as the compiler understands that the called method has an Integer type. Let's replace the output to the console with the following line: System.out.println(Util.getValue(element) + 1);
And we get an error: bad operand types for binary operator '+', first type: Object , second type: int That is, type erasure has occurred. The compiler sees that no one has specified the type, the type is specified as Object, and the code execution fails.
Typed classes (Generic Types)
You can type not only methods, but also classes themselves. Oracle has a section on this in their guide called Generic Types . Consider an example:public static class SomeType<T> {
public <E> void test(Collection<E> collection) {
for (E element : collection) {
System.out.println(element);
}
}
public void test(List<Integer> collection) {
for (Integer element : collection) {
System.out.println(element);
}
}
}
Everything is simple here. If we are using a class, the generic is listed after the class name. Let's now create an instance of this class in the main method:
public static void main(String []args) {
SomeType<String> st = new SomeType<>();
List<String> list = Arrays.asList("test");
st.test(list);
}
He will work well. The compiler sees that there is a List of numbers and a Collection of type String. But what if we erase the generics and do this:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
We'll get an error: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Type erasure again. Since the class no longer has a generic, the compiler decides that since we passed a List, the List<Integer> method is more appropriate. And we fall with an error. Therefore, rule #2: If the class is typed, always specify the generic type .
Restrictions
We can apply a restriction to types specified in generics. For example, we want the container to accept only Number as input. This feature is described in the Oracle Tutorial under Bounded Type Parameters . Let's look at an example:import java.util.*;
public class HelloWorld{
public static class NumberContainer<T extends Number> {
private T number;
public NumberContainer(T number) { this.number = number; }
public void print() {
System.out.println(number);
}
}
public static void main(String []args) {
NumberContainer number1 = new NumberContainer(2L);
NumberContainer number2 = new NumberContainer(1);
NumberContainer number3 = new NumberContainer("f");
}
}
As you can see, we have limited the generic type to the Number class/interface and its descendants. Interestingly, you can specify not only a class, but also interfaces. For example: public static class NumberContainer<T extends Number & Comparable> {
Generics also have the concept of Wildcard https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html They, in turn, are divided into three types:
- Upper Bounded Wildcards - < ? extends Number >
- Unbounded Wildcards - < ? >
- Lower Bounded Wildcards - < ? super Integer >
public static class TestClass {
public static void print(List<? extends String> list) {
list.add("Hello World!");
System.out.println(list.get(0));
}
}
public static void main(String []args) {
List<String> list = new ArrayList<>();
TestClass.print(list);
}
But if you replace extends with super, everything will be fine. Since we fill the list list with a value before outputting it, it is a consumer for us, that is, a consumer. Hence, we use super.
Inheritance
There is another unusual feature of generics - this is their inheritance. Generic inheritance is described in Oracle's tutorial under " Generics, Inheritance, and Subtypes ". The main thing is to remember and realize the following. We cannot do this:List<CharSequence> list1 = new ArrayList<String>();
Because inheritance works differently with generics:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Here, too, everything is simple. List<String> is not derived from List<Object>, although String is derived from Object.
Final
So we refreshed the memory of generics. If they are rarely used in all their power, some details fall out of memory. I hope this short review will help refresh your memory. And for better results, I strongly recommend that you read the following materials:- Yuri Tkach: Raw Types - Generics #1 - Advanced Java
- Inheritance and Generic Extenders - Generics #2 - Advanced Java
- Recursive Type Extension - Generics #3 - Advanced Java
- Alexander Matorin - Unobvious Generics
- Introduction to Java. Generics. Wildcards | Technostream
- O'Reilly: Java Generics and Collections
GO TO FULL VERSION