ClassCastException 在重写父类方法时出现(Comparable)

huangapple go评论73阅读模式
英文:

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

问题

我确实进行了搜索,但找不到类似的问题。如果这是重复的问题,我很抱歉。我编写了一个常规的队列方法,并尝试将其扩展为具有优先级的队列。我不明白为什么我只能在使用超类的方法时才能插入,而在子类中的代码却不行,尽管 storage[n] 是 Comparable,data 也是 Comparable。如果我尝试在子类中这样做,将会抛出 ClassCastException。我做错了什么吗?

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);
    }

    // 这种写法不起作用
    @Override
    void insert(Comparable<T> data) {
        storage[1] = data;
    }

    // ---> 这种写法可以正常工作。
    //    @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());
    }
}

以上为你提供的代码的翻译。

英文:

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<T> 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&lt;T&gt; {

    protected int capacity;

    protected T[] storage;

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

    @Override
    public String toString() {
        return &quot;Queue{&quot; +
                &quot;capacity=&quot; + capacity +
                &quot;, storage=&quot; + Arrays.toString(storage) +
                &#39;}&#39;;
    }

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

PriorityQueue.java

public class PriorityQueue&lt;T&gt; extends RegularQueue&lt;Comparable&lt;T&gt;&gt; {

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

    // This doesn&#39;t work
    @Override
    void insert(Comparable&lt;T&gt; data) {
        storage[1] = data;
    }

    // ---&gt; This works fine.
    //    @Override
    //    void insert(Comparable&lt;T&gt; data) {
    //        super.insert(data);
    //    }


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

}

答案1

得分: 3

你看到这个ClassCastExpression是因为在RegularQueue中,你使用了非类型安全赋值storage = (T[]) new Object[this.capacity]。在PriorityQueue中,你将Comparable&lt;...&gt;用作RegularQueue的T的类型参数。因此在编译时已知,这个T在运行时必须是Comparable或其子类型。因此编译器会在每次访问T[] storage时发出Comparable[]的强制转换,以执行此操作。

问题在于,storage实际上并不是T[]类型,而只是Object[]类型,这导致了你所看到的ClassCastException。无论以任何方式访问该字段,甚至是storage.length,都会触发这个异常。

你没有在调用super.insertinsert方法中看到这个异常的原因是它并没有直接访问storage。只有超类的实现才会这样做,然而在RegularQueue内部,在编译时不知道T的类型,因此不执行任何强制转换。

解决方案是不将storage声明为T[],而是使用实际的类型Object[]

另有其他人将此问题报告给了JDK团队,但报告已被(预料中的)解决为“不是问题”。然而,JDK开发人员之一Stuart Marks在该报告的评论中深入解释了底层问题(可能比这个回答更好)。我强烈建议阅读他的评论。链接

英文:

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&lt;...&gt; 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.

答案2

得分: 0

本答案将尝试补充Marcono1234的回答。##

使用Eclipse Class File Editor,我们可以在类文件RegularQueue.class中看到以下内容:

// Signature: &lt;T:Ljava/lang/Object;&gt;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: &lt;T:Ljava/lang/Object;&gt;LRegularQueue&lt;Ljava/lang/Comparable&lt;TT;&gt;;&gt;;
public class PriorityQueue extends RegularQueue {
...
  // Method descriptor #19 (Ljava/lang/Comparable;)V
  // Signature: (Ljava/lang/Comparable&lt;TT;&gt;;)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]  //&lt;-- 出现ClassCastException的原因
     7  iconst_1
     8  aload_1 [data]
     9  aastore
  ...
  1. checkcast只存在于PriorityQueue中,而不在RegularQueue中。
  2. checkcast检查的是storage是否为java.lang.Comparable[],因为Comparable&lt;T&gt;的类型擦除后是Comparable,所以在PriorityQueue的视图中,storage的类型是Comparable[]

此外,当类型参数的类型擦除为Object时,也会抛出ClassCastException

  • PriorityQueue&lt;T&gt; extends RegularQueue&lt;Number&gt;
  • PriorityQueue&lt;T&gt; extends RegularQueue&lt;String&gt;

当类型参数的类型擦除为Object时,不会抛出ClassCastExceptioncheckcast将消失):

  • PriorityQueue&lt;T&gt; extends RegularQueue&lt;T&gt;
  • PriorityQueue&lt;T&gt; extends RegularQueue&lt;Object&gt;

解决方案

如Marcono1234建议的那样,

解决方案是不要将storage声明为T[],而是使用Object[],因为那才是实际类型。

为了获得更好的类型安全性和可读性,建议将storage作为私有字段,并提供setStoragegetStorage方法:

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

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

正如我们在以下示例中看到的那样,

public class PriorityQueue&lt;T&gt; extends RegularQueue&lt;Comparable&lt;T&gt;&gt; {
...
	@Override
	void insert(Comparable&lt;T&gt; data) {
		setStorage(1, new Object()); // 编译错误
        // 如果storage是受保护的,以下内容是允许的,只有在将值强制转换为Comparable&lt;T&gt;时才会出错
		// storage[1] = new Object();
	}

	public Comparable&lt;T&gt; getByIndex(int index) {
		return getStorage(index);
		// 使用storage值时需要反复强制转换
		// return (Comparable&lt;T&gt;) storage[index];
	}
...

参考链接:
Reference Type Casting
The Java Virtual Machine Instruction Set - checkcast

英文:

This answer will try to supplement on Marcono1234 answer.

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

// Signature: &lt;T:Ljava/lang/Object;&gt;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: &lt;T:Ljava/lang/Object;&gt;LRegularQueue&lt;Ljava/lang/Comparable&lt;TT;&gt;;&gt;;
public class PriorityQueue extends RegularQueue {
...
  // Method descriptor #19 (Ljava/lang/Comparable;)V
  // Signature: (Ljava/lang/Comparable&lt;TT;&gt;;)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]  //&lt;-- 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&lt;T&gt; is Comparable, so storage is of type Comparable[] in the view of PriorityQueue.

In addition
ClassCastException will also throw, for

  • PriorityQueue&lt;T&gt; extends RegularQueue&lt;Number&gt;
  • PriorityQueue&lt;T&gt; extends RegularQueue&lt;String&gt;

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

  • PriorityQueue&lt;T&gt; extends RegularQueue&lt;T&gt;
  • PriorityQueue&lt;T&gt; extends RegularQueue&lt;Object&gt;

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(&quot;unchecked&quot;)
protected T getStorage(int index) {
	return (T) storage[index];
}

As we can see in following example,

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

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

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

huangapple
  • 本文由 发表于 2020年9月5日 20:57:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/63754170.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定