Home > Back-end >  having trouble making a method that creates a 2d array out of a T type unrolled linked list
having trouble making a method that creates a 2d array out of a T type unrolled linked list

Time:11-04

public T[][] getArrayOfBlocks() {
    Node node = this.first;
    @SuppressWarnings("unchecked")
    T[][] result = (T[][]) new Object[this.nNodes][this.arraySize];
    for(int i = 0; i < this.nNodes; i  )
    {
        for(int j = 0; j < this.arraySize; j  )
            if(node.a[j] != null)
                result[i][j] = node.a[j];
        node = node.next;
    }
    return result;
}

(Im a newbie in java so my wording will be a bit weird) Im trying to make a method that creates a 2d array out of a T type unrolled linked list. When i test out the method above using the Integer class instead of the T type i get an error that says

Exception in thread "main" java.lang.ClassCastException: class [[Ljava.lang.Object; cannot be cast to class [[Ljava.lang.Integer; ([[Ljava.lang.Object; and [[Ljava.lang.Integer; are in module java.basa of loader 'bootstrap')

So yeah i would like to know if theres any way to solve this error without changing the return type. Thanks in advance :)

CodePudding user response:

y out of a T type unrolled linked list.

This is impossible.

Generics are a figment of the compiler's imagination. Generics completely disappear once your java code has been turned into a class file, or if they don't (generics in signatures), the JVM treats it as a comment. It has absolutely no effect. Instead, the compiler uses it to generate errors or warnings and inject invisible casts. This:

List<String> x = new ArrayList<String>();
x.add("Hello");
String y = x.get(0);

ends up in class code as indistinguishable from compiling:

List x = new ArrayList();
x.add("Hello");
String y = (String) x.get(0);

Try it if you're having a hard time getting your head around this idea. Write both, compile it, run javap -c -v to see the bytecode. Identical.

The reason x.add(5) would not work as replacement for x.add("Hello") is simply because javac won't let it happen. If you hack javac to allow it, you get a class file just fine and it verifies just fine. The x.add(5) will even execute just fine. You'd get a ClassCastException on the next line, simply because you're casting an instance of Integer to String.

As a consequence, there is no way to tell the difference between a new ArrayList<String>(); and a new ArrayList<Integer>() at runtime. Obviously; generics disappears; those are both just new ArrayList(), that's it.

In contrast, arrays are 'reified': They aren't a figment of javac's imagination. You can actually get this stuff at runtime. There is a difference between new String[0] and new Integer[0]:

Object[] arr1 = new String[0];
System.out.println(arr1.getClass().getComponentType()); // prints 'String'

It is impossible to write the identical code for generics:

List<?> list1 = new ArrayList<String>();
System.out.println(--nothing you can write here will print String--);

Hence, in your 'unrolled code with T', T is not something you can translate to an actual runtime type, and that means it is impossible to make an array of T.

Still having a hard time believing this? Peruse the API of java.util.List, specifically the various toArray methods it contains.

Look at the no-args one: toArray(). There are two explanations here:

  • The designer of this class was an utter idiot, because that returns Object[], which is stupid, because clearly that should return T[].
  • Or, perhaps something else is going on and they 'know' that it is in fact impossible to return a T[] there.

It's, as the rest of this post hopefully already suggested, the second reason.

Fortunately, there are 2 other toArray methods and those two do both return T[] as you desire. They are both based around the notion that the caller puts in the effort of providing that T type for you.

The first version is toArray(T[] in). The toArray code will use the provided array if it is large enough, but if not, it just makes a new one that is the right size and returns it. In practice, you always call listOfStrings.toArray(new String[0]) (you may think new String[list.size()] would be faster - no, that is slower1. A nice example of why writing more complex code because it seems faster is a bad idea. JVMs are far too complex to predict performance like this).

The trick here is that the code in list's toArray will take that array, grab its class (tossing the created array aside), get the component type from that, and then use that to make a new array.

There is another one, too: toArray(IntFunction<T[]> arrayCreator) (you need to look at the javadoc of Collection to see it; it is inherited).

Here we ask the caller to provide code that makes a new array. You use it like this: listOfStrings.toArray(String[]::new).

Pick your poison, or add both. Either trick will work here:

public T[][] getArrayOfBlocks(T[] dummy) {
  Class<?> componentType = dummy.getClass().getComponentType();

  @SuppressWarnings("unchecked")
  T[][] arr = (T[][]) java.lang.reflect.Array.newInstance(componentType, this.nNodes, this.arraySize);

 .. code continues here ..
}

or:

public T[][] getArrayOfBlocks(BiFunction<Integer, Integer, T[][]> arrayMaker) {
  T[][] arr = arrayMaker.apply(this.nNodes, this.arraySize);

 .. code continues here ..
}

Yes, they are both annoying. There are other options but they have significant downsides - the above 2 options are your best bet. That or forget about arrays. Why do you even want a `T[][]` in the first place? Arrays can't grow or shrink, assuming it's not a primitive array (and this isn't, by definition; generics cannot be primitive) they are not more performant, and their toString/equals/hashCode implementations are surprising (that's programmer-ese for 'basically broken'). Their API is non-existent. Why would you want to offer it?


1) In case you desire explanations for this one: It's because the toArray code is hotspot intrinsiced and knows it doesn't need to wipe out the memory space, whereas with `new String[100]`, those 100 references all need to be nulled out first because java guarantees you can't 'see' uninitialized memory.
  • Related