JavaRush /Java Blog /Random EN /The theory of generics in Java or how to put parentheses ...
Viacheslav
Level 3

The theory of generics in Java or how to put parentheses in practice

Published in the Random EN group

Introduction

Starting with JSE 5.0, generics were added to the Java language arsenal.
The theory of generics in Java or how to put parentheses in practice - 1

What are generics in Java?

Generics (generalizations) are special means of the Java language for implementing generalized programming: a special approach to describing data and algorithms that allows you to work with different types of data without changing their description. On the Oracle website, a separate tutorial is dedicated to generics: “ Lesson: Generics ”.

First, to understand generics, you need to understand why they are needed at all and what they provide. In the tutorial in the section " Why Use Generics ?" It is said that one of the purposes is stronger compile-time type checking and eliminating the need for explicit casting.
The theory of generics in Java or how to put parentheses in practice - 2
Let's prepare our favorite tutorialspoint online java compiler for experiments . Let's imagine this code:
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 fine. But what if they came to us and said that the phrase “Hello, world!” beaten and you can only return Hello? Let's remove the concatenation with the string from the code ", world!". It would seem that what could be more harmless? But in fact, we will receive an error DURING COMPILATION : 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 descendant of Object (since all classes are implicitly inherited from Object in Java), it requires an explicit cast, which we did not do. And when concatenating, the static method String.valueOf(obj) will be called on the object, which will ultimately call the toString method on the 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 accepts 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 receive an error DURING EXECUTION (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 what types we are going to use, Java SE 5 introduced generics . Let's correct 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 the cast to String. In addition, we now have angle brackets that frame 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 those same castes when compiled. Let's look at the bytecode of the compiled classes: with manual casting and using generics:
The theory of generics in Java or how to put parentheses in practice - 3
After compilation, any information about generics is erased. This is called "Type Erasure" or " Type Erasure ". Type erasure and generics are designed to provide backward compatibility with older versions of the JDK, while still allowing the compiler to assist with type inference in newer versions of Java.
The theory of generics in Java or how to put parentheses in practice - 4

Raw Types or raw types

When talking about generics, we always have two categories: typed types (Generic Types) and “raw” types (Raw Types). Raw types are types without specifying the “qualification” in angle brackets:
The theory of generics in Java or how to put parentheses in practice - 5
Typed types are the opposite, with the indication of "clarification":
The theory of generics in Java or how to put parentheses in practice - 6
As we can see, we used an unusual design, marked with an arrow in the screenshot. This is a special syntax that was added in Java SE 7, and it is called " the diamond ", which means diamond. Why? You can draw an analogy between the shape of a diamond and the shape of curly braces: <> Diamond syntax is also 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 declaration of the type of the variable to which the value is assigned is located. And from 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 not 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 would be a mixture of the new style with generics and the old style without them. And this is extremely undesirable. When compiling the code above we will receive the message: Note: HelloWorld.java uses unchecked or unsafe operations. In fact, it seems unclear why we need to add diamond 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 deceit lies. Without diamond syntax, the compiler does not understand that it is being deceived, but with diamond it does. Therefore, rule #1 : always use diamond syntax if we use 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 on the method or class being used: @SuppressWarnings("unchecked") Suppress is translated as suppress, that is, literally, to suppress warnings. But think about why you decided to indicate it? Remember rule number one and maybe you need to add typing.
The theory of generics in Java or how to put parentheses in practice - 7

Generic Methods

Generics allow you to type methods. There is a separate section dedicated to this feature in the Oracle tutorial: “ Generic Methods ”. From this tutorial, it is important to remember the syntax:
  • includes a list of typed parameters inside angle brackets;
  • the list of typed parameters goes before the returned method.
Let's look at an example:
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 you look at the Util class, we see two typed methods in it. With type inference, we can provide the type definition directly to the compiler, or we can specify it ourselves. Both options are presented 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 will not be able to figure out which type to use. Therefore, we first announce that we will use generic T, and then we say that we are going to return this generic. Naturally, Util.<Integer>getValue(element, String.class)it will fail 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 console output with the following line: System.out.println(Util.getValue(element) + 1); And we get the error: bad operand types for binary operator '+', first type: Object , second type: int That is, types have been erased. The compiler sees that no one has specified the type, the type is specified as Object and the code execution fails with an error.
The theory of generics in Java or how to put parentheses in practice - 8

Generic Types

You can type not only methods, but also classes themselves. Oracle has a “ Generic Types ” section dedicated to this in their guide. Let's look at 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 use 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);
}
It 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 will get the 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, a method with List<Integer> is more appropriate. And we fall with a mistake. Therefore, rule #2: If a class is typed, always specify the type in the generic .

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 in the Bounded Type Parameters section . 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 be 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: The so-called Get Put principle applies to Wildcards . They can be expressed in the following form:
The theory of generics in Java or how to put parentheses in practice - 9
This principle is also called the PECS (Producer Extends Consumer Super) principle. You can read more on Habré in the article “ Using generic wildcards to improve the usability of the Java API ”, as well as in the excellent discussion on stackoverflow: “ Using wildcards in Generics Java ”. Here is a small example from the Java source - the Collections.copy method:
The theory of generics in Java or how to put parentheses in practice - 10
Well, a small example of how it will NOT work:
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 with a value before outputting it, it is a consumer for us, that is, a consumer. Therefore, we use super.

Inheritance

There is another unusual feature of generics - their inheritance. Inheritance of generics is described in the Oracle tutorial in the section " Generics, Inheritance, and Subtypes ". The main thing is to remember and realize the following. We can't do this:
List<CharSequence> list1 = new ArrayList<String>();
Because inheritance works differently with generics:
The theory of generics in Java or how to put parentheses in practice - 11
And here is another good example that will fail with an error:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Everything is simple here too. List<String> is not a descendant of List<Object>, although String is a descendant of Object.

Final

So we refreshed our memory of generics. If they are rarely used in all their power, some details fall out of memory. I hope this short review helps refresh your memory. And for greater results, I strongly recommend that you read the following materials: #Viacheslav
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION