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

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

Published in the Random EN group

Introduction

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

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 ".

First, to understand generics, you need to understand why they are needed at all and what they give. In the tutorial under " Why Use Generics ?" it is said that one of the purposes is stronger type checking at compile time and elimination of the need for explicit casts.
The theory of generics in Java or how to put brackets 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 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:
The theory of generics in Java or how to put brackets 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 be backward compatible with older versions of the JDK, while still being able to help the compiler with type inference in new versions of Java.
The theory of generics in Java or how to put brackets in practice - 4

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):
The theory of generics in Java or how to put brackets in practice - 5
Typed types - on the contrary, with the indication of "clarification":
The theory of generics in Java or how to put brackets 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 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: <> 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.
The theory of generics in Java or how to put brackets in practice - 7

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.
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 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.
The theory of generics in Java or how to put brackets in practice - 8

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: 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 brackets 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 convenience of the Java API ", as well as in the excellent discussion on stackoverflow: " Using wildcards in Java Generics ". Here is a small example from the Java source - the Collections.copy method:
The theory of generics in Java or how to put brackets 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 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:
The theory of generics in Java or how to put brackets in practice - 11
And here is another good example that will fail with an error:
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: #Viacheslav
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION