Louis Tran
Louis Tran

Reputation: 1166

ClassCastException when overriding a super's method (Comparable<T>)

I did search but couldn't find a similar problem. I'm sorry if this is duplicated. I write a regular queue method and try to extends it to have a priority queue. I don't understand why I can only insert if I use the super class' method, and not the code in the sub class while storage[n] is a Comparable and data is a Comparable too. If I try to do that in the sub class, a ClassCastException will be thrown. Did I do anything wrong?

RegularQueue.java

import java.util.Arrays;

public class RegularQueue<T> {

    protected int capacity;

    protected T[] storage;

    @SuppressWarnings("unchecked")
    RegularQueue(int capacity) {
        this.capacity = capacity;
        storage = (T[]) new Object[this.capacity];
    }

    @Override
    public String toString() {
        return "Queue{" +
                "capacity=" + capacity +
                ", storage=" + Arrays.toString(storage) +
                '}';
    }

    void insert(T data) {
        storage[0] = data;
    }
}

PriorityQueue.java

public class PriorityQueue<T> extends RegularQueue<Comparable<T>> {

    PriorityQueue(int capacity) {
        super(capacity);
    }

    // This doesn't work
    @Override
    void insert(Comparable<T> data) {
        storage[1] = data;
    }

    // ---> This works fine.
    //    @Override
    //    void insert(Comparable<T> data) {
    //        super.insert(data);
    //    }


    public static void main(String[] args) {
        PriorityQueue<Integer> q = new PriorityQueue<>(5);
        q.insert(1);
        System.out.println(q.toString());
    }

}

Upvotes: 3

Views: 162

Answers (2)

samabcde
samabcde

Reputation: 8114

This answer will try to supplement on Marcono1234 answer.

Using Eclipse Class File Editor, we can see in the class file
RegularQueue.class

// Signature: <T:Ljava/lang/Object;>Ljava/lang/Object;
public class RegularQueue {
  ...
  // Field descriptor #8 [Ljava/lang/Object;
  // Signature: [TT;
  protected java.lang.Object[] storage;
  ...
  // Method descriptor #56 (Ljava/lang/Object;)V
  // Signature: (TT;)V
  // Stack: 3, Locals: 2
  void insert(java.lang.Object data);
    0  aload_0 [this]
    1  getfield RegularQueue.storage : java.lang.Object[] [19]
    4  iconst_0
    5  aload_1 [data]
    6  aastore
    7  return
  ...

PriorityQueue.class

// Signature: <T:Ljava/lang/Object;>LRegularQueue<Ljava/lang/Comparable<TT;>;>;
public class PriorityQueue extends RegularQueue {
...
  // Method descriptor #19 (Ljava/lang/Comparable;)V
  // Signature: (Ljava/lang/Comparable<TT;>;)V
  // Stack: 3, Locals: 2
  void insert(java.lang.Comparable data);
     0  aload_0 [this]
     1  getfield PriorityQueue.storage : java.lang.Object[] [22]
     4  checkcast java.lang.Comparable[] [26]  //<-- reason for ClassCastException
     7  iconst_1
     8  aload_1 [data]
     9  aastore
  ...
  1. checkcast only exists in PriorityQueue, and not RegularQueue
  2. It is checked for java.lang.Comparable[] against storage as erasure of Comparable<T> is Comparable, so storage is of type Comparable[] in the view of PriorityQueue.

In addition ClassCastException will also throw, for

  • PriorityQueue<T> extends RegularQueue<Number>
  • PriorityQueue<T> extends RegularQueue<String>

ClassCastException will not throw (checkcast will disappear), when the type/erasure of the type argument is Object.

  • PriorityQueue<T> extends RegularQueue<T>
  • PriorityQueue<T> extends RegularQueue<Object>

Solution

As suggested by Marcono1234,

The solution is to not declare storage as T[] but instead use Object[] since that is the actual type.

For better type safety and readability, I suggest to make storage as private field also, and provide setStorage and getStorage method:

protected void setStorage(int index, T data) {
    storage[index] = data;
}

@SuppressWarnings("unchecked")
protected T getStorage(int index) {
    return (T) storage[index];
}

As we can see in following example,

public class PriorityQueue<T> extends RegularQueue<Comparable<T>> {
...
    @Override
    void insert(Comparable<T> data) {
        setStorage(1, new Object()); // Compile error
        // following is allowed if storage is protected, error only occur when casting the value to Comparable<T>
        // storage[1] = new Object();
    }

    public Comparable<T> getByIndex(int index) {
        return getStorage(index);
        // Need to repeatedly cast when using storage value
        // return (Comparable<T>) storage[index];
    }
...

Reference:
Reference Type Casting
The Java Virtual Machine Instruction Set - checkcast

Upvotes: 0

Marcono1234
Marcono1234

Reputation: 6914

You are seeing this ClassCastExpression because in RegularQueue you are using the non type-safe assignment storage = (T[]) new Object[this.capacity]. In PriorityQueue you are using Comparable<...> as type argument for T of RegularQueue. It is therefore known at compile time that this T must at runtime be Comparable or a subtype of it. Thus the compiler emits Comparable[] casts inside PriorityQueue every time you access T[] storage to enforce this.
The issue is now that storage is not actually of type T[] but only of type Object[] which is causing the ClassCastException you are seeing. This occurs when accessing the field in any way, even storage.length triggers it.

The reason why you are not seeing this exception in the insert method calling super.insert is that it does not directly access storage. Only the super implementation does this which however does not perform any casts since inside of RegularQueue the type of T is unknown at compile time.

The solution is to not declare storage as T[] but instead use Object[] since that is the actual type.

Someone else reported this as bug to the JDK team but the report has (as expected) been resolved as "Not an Issue". However Stuart Marks, one of the JDK developers, explains in his comment on the report in depth (and probably better than this answer) the underlying issue. I highly recommend reading it.

Upvotes: 3

Related Questions