0

I've just started learning about generics in Java. I need some help in understanding about casting an array to the generic type.

The following code is from the book Introduction to Java Programming by Y. Daniel Liang Chapter 19. The code is shortened to show where I'm missing understanding.

GenericMatrix.java:

public abstract class GenericMatrix<E extends Number> {
  protected abstract E add(E o1, E o2);

  public E[][] addMatrix(E[][] matrix1, E[][] matrix2) {
    // Check bounds of the two matrices
    if ((matrix1.length != matrix2.length) ||
        (matrix1[0].length != matrix2[0].length)) {
      throw new RuntimeException(
        "The matrices do not have the same size");
    }

    E[][] result = (E[][])new Number[matrix1.length][matrix1[0].length];

    for (int i = 0; i < result.length; i++)
      for (int j = 0; j < result[i].length; j++) {
        result[i][j] = add(matrix1[i][j], matrix2[i][j]);
      }

    return result;
  }

IntegerMatrix.java:

public class IntegerMatrix extends GenericMatrix<Integer> {
  @Override /** Add two integers */
  protected Integer add(Integer o1, Integer o2) {
    return o1 + o2;
  }
}

TestIntegerMatrix.java:

public class TestIntegerMatrix {
  public static void main(String[] args) {
    Integer[][] m1 = new Integer[][]{{1, 2, 3}, {4, 5, 6}, {1, 1, 1}};
    Integer[][] m2 = new Integer[][]{{1, 1, 1}, {2, 2, 2}, {0, 0, 0}};

    IntegerMatrix integerMatrix = new IntegerMatrix();
    
    Integer[][] m3 = (Integer[][])integerMatrix.addMatrix(m1, m2);
  }
}

My understanding is that since I'm declaring my instance of GenericMatrix as an Integer, then the addMatrix method should return an Integer[][] (2 dimensional array of type Integer). This is why I declared m3 variable as such. When I compile this code it compiles without any error. But when I run it I get the error that Number can't ve concerted to Integer. if I change m3 to Number[][] everything works.

From what I understood the main gain of generics is to avoid this issue. That the compiler would catch this. But what I wonder even more is why wouldn't it return an an Integer[][].

As per the book you can't create an array of a generic type. to circumvent this problem you cast it to the generic type. But fron I'm seeing here the cast doesn't work

Exert from book:

"Restriction 2: Cannot Use new E[] You cannot create an array using a generic type parameter. For example, the following statement is wrong: E[] elements = new E[capacity]; You can circumvent this limitation by creating an array of the Object type then casting it to E[], as follows: E[] elements = (E[])new Object[capacity];"

Why wouldn't my code instantiate the returned result array as an Integer since E is instantiated as an Integer?

Please help me understand this. Any suggestions on reading material would be greatly appreciated.

Thank You, Isr

Sruly
  • 169
  • 1
  • 13
  • 2
    The concept you're looking for is _type erasure_. – chrylis -cautiouslyoptimistic- Oct 12 '21 at 23:26
  • Java array covariance [is a lie](https://stackoverflow.com/a/28570899/2288659). Don't downcast arrays; it only leads to sadness. – Silvio Mayolo Oct 13 '21 at 00:42
  • @SilvioMayolo I have seen what you show here. But what I don't get is since I'm using generics and my IntegerMatrix class is instantiating the GenericMatrix ad an Integer shouldn't this be mean that this instance should all be as an Integer? – Sruly Oct 13 '21 at 14:35
  • @chrylis-cautiouslyoptimistic- After erasure shouldn't the array be an integer array (to be more exact an array of integer arrays) – Sruly Oct 13 '21 at 14:38
  • 1
    No, after erasure the best the compiler would be able to do would be `Number[]`. This is the difference between C++ templates and Java generics. – chrylis -cautiouslyoptimistic- Oct 13 '21 at 14:43

1 Answers1

1

Arrays in Java know their component type at runtime. For example, an array you created with new String[5] knows that it is a String[]. Even if you store a reference to the array in a variable of type Object[] (which is allowed since array types are covariant), the array object will still know that it's a String[], and putting an Integer into it will produce an ArrayStoreException at runtime. You can assign a reference to this array object back to a variable of type String[] with no problem, since its actual runtime class is String[]. On the other hand, if you create an array with new Object[5], its actual runtime class will be Object[], and assigning a reference to it to a variable of type String[] will produce a ClassCastException at runtime, since Object[] is not a subclass of String[].

Here, you create an array with new Number[something][something]. The actual runtime class of the array object will be Number[][]. When you try to store it in a variable of type Integer[][], somewhere along the line it must produce a ClassCastException, since Number[][] is not a subclass of Integer[][]. This is true with or without generics.

Generics is basically a compile-time syntactic sugar that allows you to avoid some casts that are provably safe. But since it's a syntactic sugar, it basically compiles down into an equivalent non-generic version of the code (this is "type erasure"). This means that anything that can be done with generics can be done without generics (with appropriate erasure of types and insertion of appropriate casts), and that if it can't be done without generics, it can't be done with generics either. So ask yourself, how can your method be written without generics (with extra casts)? If it can't, then it can't be written with generics either. (Let's ignore the fact that runtime class metadata (class, method, and field declarations) can contain generics, since declarations are hard-coded in the source code and are not really relevant to a discussion of polymorphism.)

I am not sure what the context of your book suggesting you do (E[])new Object[...] is. In principle, this cast is incorrect if E is anything other than exactly Object, as then Object[] would not be a subtype of E[]. However, you do not get a ClassCastException immediately, since within the scope of E, E is erased to its upper bound (which I assume in this case is Object). However, you get an unchecked cast warning, warning you that the cast is not completely checked, and bad things may happen elsewhere.

Let's say you are implementing your own MyArray<E> class, and you decide store the contents in an internal variable of type E[], with something like this:

public class MyArray<E> {
    private E[] contents;
    public MyArray(int size) {
        contents = (E[]) new Object[size];
    }
    public E get(int index) {
        return contents[index];
    }
    public void set(int index, E val) {
        contents[index] = val;
    }
}

The cast to E[] is wrong if E is anything other than Object, but it won't cause a ClassCastException, because E is erased to its upper bound (here, Object) within the scope of E (i.e. instance methods of MyArray class). The fact that we are putting an array in the wrong type won't crash as long as this fact does not "get out" of the class. But if you ever expose the false claim that contents is an E[] to the outside of the scope of E[], that will cause problems. For example, let's say you have this:

public E[] getContents() {
    return contents;
}

This method compiles fine without any warnings, but if someone with a MyArray<String> calls getContents() and assigns the result to a String[], as they should be entitled to do, they will get a ClassCastException. This is what the unchecked warning was warning you about. Another case where you might expose the false claim that contents is an E[] is if contents has any access modifier other than private, where someone might access the variable directly and think it is an E[]. (Even if it is private, static methods can still access them and static methods are outside the scope of E, so would have the same danger.) It is bad design to use something that requires you to have to be very careful about using it correctly and there is no warning if you use it incorrectly; that's why doing (E[]) new Object[size] is not recommended; you should just have contents be an Object[] or List<E> instead.

In your case, you are doing E[][] result = (E[][])new Number[...][...]. this will not crash as long as you keep result inside the scope of E, i.e. inside instance methods of GenericMatrix. But you return result to the outside of the class; as we talked about above, that is unsafe.

You could try to create an array whose actual runtime class is an instance of E[][]. One way to do that would be to have the caller pass in the Class object corresponding to type E, like this:

import java.lang.reflect.Array;

public E[][] addMatrix(Class<E> clazz, E[][] matrix1, E[][] matrix2) {
    //...
    E[][] result = (E[][])Array.newInstance(clazz, matrix1.length, matrix1[0].length);

Or, you can take advantage of the fact that you are passed in arguments of type E[][], and you can simply extract the component class from them at runtime. This is basically what Arrays.copyOf() does. (But you have to be careful of the fact that the passed argument might actually be of a subclass of E[][], and so creating the result based on the component type of matrix1 might not work for matrix2.):

import java.lang.reflect.Array;

public E[][] addMatrix(E[][] matrix1, E[][] matrix2) {
    //...
    E[][] result = (E[][])Array.newInstance(matrix1.getClass().getComponentType().getComponentType(), matrix1.length, matrix1[0].length);
newacct
  • 119,665
  • 29
  • 163
  • 224