Hello! We continue the series of lectures on generics. Earlier , we figured out in general terms what it is and why it is needed. Today we will talk about some of the features of generics and consider some of the pitfalls when working with them. Go! In the last lecture, we talked about the difference between Generic Types and Raw Types . In case you forgot, Raw Type is a generic class that has had its type removed.
List list = new ArrayList();
Here is an example. Here we do not specify what type of objects will be placed in our List
. If we try to create one List
and add some objects to it, we will see a warning in IDEa:
“Unchecked call to add(E) as a member of raw type of java.util.List”.
But we also talked about the fact that generics appeared only in the Java 5 language version. By the time it was released, programmers had managed to write a bunch of code using Raw Types, and so that it would not stop working, the ability to create and work with Raw Types in Java was preserved. However, this problem turned out to be much broader. Java code, as you know, is converted into special bytecode, which is then executed by the Java virtual machine. And if, during the translation process, we put information about parameter types into the bytecode, this would break all previously written code, because before Java 5, no type parameters existed! There is one very important feature to keep in mind when working with generics. It's called type erasure. Its essence lies in the fact that no information about its type-parameter is stored inside the class. This information is available only at compile time and is erased (becomes inaccessible) at runtime. If you try to put an object of the wrong type in yourList<String>
, the compiler will throw an error. This is exactly what the creators of the language were trying to achieve by creating generics - checks at the compilation stage. But when all the Java code you write turns into bytecode, there will be no information about type parameters in it. Inside the bytecode, your list of List<Cat>
cats will be no different from List<String>
strings. Nothing in the bytecode will say that cats
it is a list of objects Cat
. Information about this will be erased during compilation, and only information that you have a certain list in the program will get into the byte code List<Object> cats
. Let's see how it works:
public class TestClass<T> {
private T value1;
private T value2;
public void printValues() {
System.out.println(value1);
System.out.println(value2);
}
public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) {
TestClass<T> result = new TestClass<>();
result.value1 = (T) o1;
result.value2 = (T) o2;
return result;
}
public static void main(String[] args) {
Double d = 22.111;
String s = "Test String";
TestClass<Integer> test = createAndAdd2Values(d, s);
test.printValues();
}
}
We have created our own generic class TestClass
. It is quite simple: in fact, it is a small “collection” of 2 objects that are placed there immediately when the object is created. It has 2 objects as fields T
. When executing the method, the two passed objects and createAndAdd2Values()
must be cast to our type , after which they will be added to the object . In the method we create , that is, as we will have . But at the same time, we pass a number and an object to the method . Do you think our program will work? After all, we specified , as a type parameter , but it certainly cannot be cast to ! Let's run the methodObject a
Object b
T
TestClass
main()
TestClass<Integer>
T
Integer
createAndAdd2Values()
Double
String
Integer
String
Integer
main()
and check. Console output: 22.111 Test String Unexpected result! Why did this happen? It's because of type erasure. During the compilation of the code, information about the type parameter Integer
of our object TestClass<Integer> test
was erased. It turned into TestClass<Object> test
. Our parameters were converted to (and not to , as we expected!) without any problems Double
and quietly added to . Here is another simple but very revealing example of type erasure: String
Object
Integer
TestClass
import java.util.ArrayList;
import java.util.List;
public class Main {
private class Cat {
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass());
System.out.println(numbers.getClass() == cats.getClass());
}
}
Console output: true true It would seem that we have created collections with three different types-parameters - String
, Integer
, and the class we created Cat
. But during the conversion to bytecode, all three lists turned into List<Object>
, so when executed, the program tells us that in all three cases we use the same class.
Type erasure when working with arrays and generics
There is one very important point that must be clearly understood when working with arrays and generics (for example,List
). It should also be considered when choosing a data structure for your program. Generics are subject to type erasure. Information about the type parameter is not available during program execution. In contrast, arrays know and can use information about their data type during program execution. Attempting to put a value of the wrong type into an array will result in an exception:
public class Main2 {
public static void main(String[] args) {
Object x[] = new String[3];
x[0] = new Integer(222);
}
}
Console output:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
Because there is such a big difference between arrays and generics, they can have compatibility issues. First of all, you can't create an array of generic objects, or even just a typed array. Sounds a little confusing? Let's look at it clearly. For example, you can't do any of the following in Java:
new List<T>[]
new List<String>[]
new T[]
If we try to create an array of lists List<String>
, we get a generic array creation compilation error:
import java.util.List;
public class Main2 {
public static void main(String[] args) {
//ошибка компиляции! Generic array creation
List<String>[] stringLists = new List<String>[1];
}
}
But what is it for? Why is it forbidden to create such arrays? This is all for type safety. If the compiler allowed us to create such arrays from generic objects, we could get in a lot of trouble. Here is a simple example from Joshua Bloch's Effective Java:
public static void main(String[] args) {
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = Arrays.asList(42, 65, 44); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
}
Let's imagine that the creation of an array List<String>[] stringLists
would be allowed, and the compiler would not swear. Here are some things we could do in this case: In line 1, we create an array of sheets List<String>[] stringLists
. Our array contains one List<String>
. On line 2 we create a list of numbers List<Integer>
. On line 3, we assign our array List<String>[]
to a variable Object[] objects
. The Java language allows you to do this: X
you can put both objects X
and objects of all child classes in an array of objects Х
. Accordingly, Objects
anything can be placed in an array. In line 4, we replace the only element of the array objects (List<String>)
with a list List<Integer>
. As a result, we placed List<Integer>
in our array, which was intended only for storageList<String>
! We will encounter an error only when the code reaches line 5. During the execution of the program, an exception will be thrown ClassCastException
. Therefore, the prohibition on creating such arrays was introduced into the Java language - this allows us to avoid such situations.
How can type erasure be bypassed?
Well, we have learned about type erasure. Let's try to cheat the system! :) Task: We have a generic classTestClass<T>
. We need to create a method in it createNewT()
that will create and return a new object of type Т
. But that's impossible to do, right? All type information Т
will be erased at compile time, and in the process of running the program, we will not be able to find out exactly what type of object we need to create. In fact, there is one tricky way. You probably remember that Java has a class Class
. Using it, we can get the class of any of our objects:
public class Main2 {
public static void main(String[] args) {
Class classInt = Integer.class;
Class classString = String.class;
System.out.println(classInt);
System.out.println(classString);
}
}
Console output:
class java.lang.Integer
class java.lang.String
But here's one feature we didn't talk about. You will see in the Oracle documentation that the Class class is a generic! The documentation says: “T is the type of the class modeled by this Class object.” If we translate this from the documentation language into human, this means that the class for an object Integer.class
is not just Class
, but Class<Integer>
. The object type string.class
is not just Class
, Class<String>
, etc. If you still don't understand, try adding a type parameter to the previous example:
public class Main2 {
public static void main(String[] args) {
Class<Integer> classInt = Integer.class;
//ошибка компиляции!
Class<String> classInt2 = Integer.class;
Class<String> classString = String.class;
//ошибка компиляции!
Class<Double> classString2 = String.class;
}
}
And now, using this knowledge, we can bypass type erasure and solve our problem! Let's try to get information about the parameter type. The class will play its role MySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("Объект секретного класса успешно создан!");
}
}
And here is how we use our solution in practice:
public class TestClass<T> {
Class<T> typeParameterClass;
public TestClass(Class<T> typeParameterClass) {
this.typeParameterClass = typeParameterClass;
}
public T createNewT() throws IllegalAccessException, InstantiationException {
T t = typeParameterClass.newInstance();
return t;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
MySecretClass secret = testString.createNewT();
}
}
Console output:
Объект секретного класса успешно создан!
We simply passed the required parameter class to the constructor of our generic class:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Thanks to this, we saved information about the type parameter and saved it from erasure. As a result, we were able to create an object T
! :) This is the end of today's lecture. Type erasure should always be kept in mind when dealing with generics. This case does not look very convenient, but you need to understand that generics were not part of the Java language when it was created. This is a feature later screwed in that helps us create typed collections and catch errors at compile time. In some other languages, where generics have appeared since the first version, there is no type erasure (for example, in C#). However, we have not finished studying generics! In the next lecture, you will get acquainted with a few more features of working with them. In the meantime, it would be nice to solve a couple of problems! :)
GO TO FULL VERSION