JavaRush /Java Blog /Random EN /Erasing types

Erasing types

Published in the Random EN group
Hello! We continue our series of lectures on generics. Previously , we figured out in general terms what it is and why it is needed. Today we’ll talk about some of the features of generics and look at some pitfalls when working with them. Go! Erase types - 1In the last lecture, we talked about the difference between Generic Types and Raw Types . In case you forgot, Raw Type is a generic class from which its type has been removed.
List list = new ArrayList();
Here's an example. Here we do not specify what type of objects will be placed in our List. If we try to create one Listand 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 version of the language. By the time it was released, programmers had written a lot 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 placed information about parameter types into the bytecode, it would break all previously written code, because before Java 5 no parameter types existed! When working with generics, there is one very important feature that you need to remember. It's called type erasure. Its essence lies in the fact that no information about its parameter type is stored inside the class. This information is available only at the compilation stage and is erased (becomes inaccessible) at runtime. If you try to put an object of the wrong type into your List<String>, the compiler will throw an error. This is precisely what the creators of the language achieved 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 parameter types. Inside the bytecode, your list of List<Cat>cats will not differ from List<String>strings. Nothing in the bytecode will say that catsthis is a list of objects Cat. Information about this will be erased during compilation, and only the information that you have a certain list in your 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 created our own generic class TestClass. It is quite simple: essentially it is a small “collection” of 2 objects, which are placed there immediately when the object is created. It has 2 objects as fields T. When the method is executed, createAndAdd2Values()the two passed objects should be cast Object ato Object bour type T, after which they will be added to the object TestClass. In the method main()we create TestClass<Integer>, that is, in quality Twe will have Integer. But at the same time, createAndAdd2Values()we pass a number Doubleand an object to the method String. Do you think our program will work? After all, we specified as a parameter type Integer, but Stringit certainly cannot be cast to Integer! Let's run the method main()and check. Console output: 22.111 Test String Unexpected result! Why did this happen? Precisely because of type erasure. During code compilation, information about the parameter type Integerof our object TestClass<Integer> testwas erased. He turned into TestClass<Object> test. Our parameters were transformed into without any problems Double( and not into , as we expected!) and were quietly added to . Here's another simple but very illustrative example of type erasure: StringObjectIntegerTestClass
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 parameter types - String, Integer, and the class we created Cat. But during conversion to bytecode, all three lists turned into List<Object>, so when executed, the program tells us that in all three cases we are using 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 is also worth considering when choosing a data structure for your program. Generics are subject to type erasure. Information about the parameter type is not available during program execution. In contrast, arrays know and can use information about their data type during program execution. Trying to put a value of the wrong type into an array will throw 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 take a closer look. For example, you can't do any of this 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 why was this done? Why is the creation of such arrays prohibited? This is all to ensure type safety. If the compiler allowed us to create such arrays from generic objects, we could get into a lot of trouble. Here's a simple example from Joshua Bloch's book “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 array creation List<String>[] stringListswould be allowed, and the compiler would not complain. Here's what 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: Xyou can put both objects Xand objects of all child classes into an array of objects Х. Accordingly, Objectsyou can put anything at all into the array. On line 4 we replace the single 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 storing List<String>! We will encounter an error only when the code reaches line 5. An exception will be thrown during program execution ClassCastException. Therefore, the ban on creating such arrays was introduced into the Java language - this allows us to avoid such situations.

How can I bypass type erasure?

Well, we've learned about type erasure. Let's try to cheat the system! :) Task: We have a generic class TestClass<T>. We need to create a method in it createNewT()that will create and return a new object of type Т. But this is impossible to do, right? All information about the type Тwill be erased during compilation, and while the program is running, we will not be able to find out what type of object we need to create. In fact, there is one tricky way. You probably remember that there is a class in Java 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. In the Oracle documentation you will see that Class is a generic class! Erase types - 3The documentation says: “T is the type of the class modeled by this Class object.” If we translate this from documentation language into human language, this means that the class for an object Integer.classis not just Class, but Class<Integer>. The object type string.classis not just Class, Class<String>, etc. If it's still not clear, 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. Its role will be played by the class MySecretClass:
public class MySecretClass {

   public MySecretClass() {

       System.out.println("Объект секретного класса успешно создан!");
   }
}
Here's 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 class parameter to the constructor of our generic class:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Thanks to this, we saved information about the parameter type and protected it from being erased. As a result, we were able to create an object T! :) This concludes today's lecture. Type erasure is always something to keep in mind when working with generics. This doesn’t look very convenient, but you need to understand that generics were not part of the Java language when it was created. This is a later added feature that helps us create typed collections and catch errors at the compilation stage. Some other languages ​​where generics have been around since version 1 do not have type erasure (for example, C#). However, we are not done studying generics! In the next lecture you will get acquainted with several more features of working with them. In the meantime, it would be nice to solve a couple of problems! :)
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION