使用泛型和Varargs的ClassCastException [英] ClassCastException using Generics and Varargs

查看:75
本文介绍了使用泛型和Varargs的ClassCastException的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我最近刚接触Java Generics,想复制可与Array一起使用的JavaScript映射函数.现在,我无法弄清楚代码中出了什么问题:

 公共类测试{公共接口功能< T>{公开电话(T ... vals);}公共< E>E [] map(E [] array,Function< E> func){for(int i = 0; i< array.length; i ++){array [i] = func.call(array [i]);<-例外}返回数组;}公共Test(){整数foo [] = {3,3,4,9};foo = map(foo,new Function< Integer>(){@Overridepublic Integer call(Integer ... vals){返回vals [0] + = 2;}});对于(Integer l:foo){System.out.println(l);}}公共静态void main(String [] args){新的Test();}} 

我在指定的行遇到ClassCastException:

 线程主"中的异常java.lang.ClassCastException:[Ljava.lang.Object;无法转换为[Ljava.lang.Integer; 

我不会在任何地方将Integer Array转换回Object Array,并且我无法真正弄清楚出了什么问题,因为在那一行中,该数组仍然是Integer Array,并且永远不会进入匿名方法./p>

很抱歉,如果这是我应该轻松找到解决方案的问题,我尝试过,但没有找到解决方案.另外,如果您不赞成投票,请告诉我原因.

解决方案

实际答案

泛型和数组,以及泛型和varargs在Java中混合不好.无论如何,数组在Java中使用不多.在Java中,使用 List< Integer> 比使用 Integer [] 更为常见,这样可以避免您将要听到的所有麻烦.因此,对于实际程序,只需不要将泛型和数组或泛型和varargs一起使用,就可以了.

没有比这更深入的阅读了!只需使用 List< T> 而不是 T [] ,并且不要对泛型使用varags!

您已经收到警告.

技术答案

让我们稍微打开问题功能的包装.我们可以更明确地将其重写为:

  public< E>E [] map(E [] array,Function< E> func){E e = array [0];E res = func.call(e);array [0] = res;返回数组;} 

如果您在调试器中逐步执行此功能,则会看到 E res = func.call(e); 行引发了异常,但我们甚至从未到达过函数调用的主体.

要了解原因,您必须了解数组,泛型和varargs如何一起工作(或不工作). call 被声明为 public T call(T ... vals).在Java中,语法糖意味着两件事:

  1. call 实际上具有类型 T call(T [] vals)
  2. 任何呼叫站点 call(T t1,T t2,/* etc */)应该转换为 call(new T [] {t1,t2,/* etc */}),在调用该方法之前隐式地在调用者的代码中构建一个数组.

如果不是在 call 的声明中使用诸如 T 之类的类型变量,我们将使用诸如 Integer 之类的普通类型那就是故事的结局.但是,由于它是类型变量,因此我们必须对泛型和数组之间的相互作用有更多的了解.

泛型和数组

Java中的数组包含一些有关它们是什么类型的运行时信息:例如,当您说 new Integer [] {7} 时,数组对象本身会记住它是作为 Integer 数组.这有两个原因,都与转换有关:

  • 如果您使用的是 Integer [] ,则如果尝试执行诸如((Object [])myIntegerArray)["not a number!"]之类的偷偷摸摸的事情,Java会抛出运行时错误.会使您陷入一种奇怪的情况,即您的整数数组包含一个字符串;为此,需要在运行时检查您放入数组中的每个值是否与 Integer
  • 兼容
  • 您可以将 Integer [] 强制转换为 Object [] ,然后向下转换为 Integer [] ,但不能向下转换为, String [] -如果尝试,则会在运行时出现向下转换的类广播异常.Java需要知道该值是作为 Integer [] 创建的,以支持该值.

另一方面,泛型没有运行时组件.您可能已经听说过 erasure ,这就是它的含义:所有泛型内容在编译时都会检查,但是会从程序中删除,并且完全不会影响运行时.

那么,如果您尝试创建某种通用类型的数组,例如 new T [] {} ,会发生什么?它不会编译,因为数组需要在运行时知道什么 T 才能正常工作,但是Java不知道什么 T >在运行时.因此,编译器根本不允许您构建其中之一.

但是有漏洞.如果使用泛型类型调用varargs函数,则Java允许程序进行编译.回想一下varargs方法的callsite创建了一个新数组-那么它赋予该数组什么类型呢?在这种情况下,由于 Object T 的擦除,因此将其仅创建为 Object [] .

在某些情况下,这会很好地工作,但是您的程序不是其中之一.在您的程序中, func 的值是

 公共整数调用(整数... vals){返回vals [0] + = 2;} 

身体并不重要;您可以将其替换为 return null; ,程序的行为将相同.重要的是它需要 Integer ... ,而不是 Object ... T ... 或类似的东西.请记住, Integer ... 是语法糖,编译后确实需要 Integer [] .因此,当您在数组上调用此方法时,首先要做的是将其强制转换为 Integer [] .

这就是问题所在:在呼叫站点,我们所知道的只是该函数使用了 T ... ,因此我们使用新创建的 Object [] (在运行时碰巧填充了一个整数,但是编译器并不静态知道).但是被调用方需要一个 Integer [] ,并且不能将构造为 new Object [] {} 的数组下转换为 Integer [] ,原因如上所述.当您尝试时,会得到 java.lang.ClassCastException:[Ljava.lang.Object;不能强制转换为[Ljava.lang.Integer; ,这正是您的程序所产生的.

故事的寓意

没有理由尝试了解发生问题的所有详细信息.相反,这是我希望您带走的东西:

  • 首选Java集合(例如 List )而不是数组.集合是Java的方式.
  • 可变参数和泛型不能混用.将它们混合在一起时,您可以轻松进入这样的陷阱.请勿打开此类门.

希望有帮助.

I got into Java Generics just recently and wanted to replicate the JavaScript map function you can use with Arrays. Now I can't figure out what's going wrong in my code:

public class Test {

public interface Function<T> {
    public T call(T... vals);
}

public <E> E[] map(E[] array, Function<E> func) {
    for (int i = 0; i < array.length; i++) {
        array[i] = func.call(array[i]);    <--- Exception
    }
    return array;
}

public Test() {
    Integer foo[] = {3, 3, 4, 9};

    foo = map(foo, new Function<Integer>() {
        @Override
        public Integer call(Integer... vals) {
            return vals[0] += 2;
        }
    });
    for (Integer l : foo) {
        System.out.println(l);
    }
}

public static void main(String[] args) {
    new Test();
}
}

I am encountering a ClassCastException at the specified line:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;

I'm not casting the Integer Array back to a Object Array anywhere and I can't really figure out what is going wrong since in that line the array is still an Integer Array and it never gets into the anonymous method.

Sorry if this is a question I should easily find solutions for, I tried but I found none. Also if you downvote it please tell me why.

解决方案

The practical answer

Generics and arrays, and generics and varargs, don't mix well in Java. Arrays are not used much in Java anyway; in Java it's much more common to use a List<Integer> than an Integer[] and doing so will avoid all the pain you're about to hear about. So for practical programs, just don't use generics and arrays, or generics and varargs, together and you'll be fine.

Don't read any further than this! Just use List<T> instead of T[] and don't use varags with generics!

You've been warned.

The technical answer

Let's unpack the problem function a bit. We could rewrite it more explicitly as:

public <E> E[] map(E[] array, Function<E> func) {
  E e = array[0];
  E res = func.call(e);
  array[0] = res;
  return array;
}

If you step through this function in a debugger, you'll see that the line E res = func.call(e); is throwing the exception, but that we never even get to the body of the function call.

To understand why, you have to understand how arrays, generics, and varargs work (or don't work) together. call is declared as public T call(T... vals). In Java that's syntactic sugar that means two things:

  1. call actually has the type T call(T[] vals)
  2. Any callsite call(T t1, T t2, /*etc*/) should be transformed into call(new T[]{t1, t2, /*etc*/}), implicitly building an array in the caller's code before calling the method.

If instead of using a type variable like T in the declaration of call we'd used a plain type like Integer or something like that, this would be the end of the story. But since it's a type variable, we have to understand a bit more about the interplay between generics and arrays.

Generics and arrays

Arrays in Java carry around some runtime information about what type they are: for instance, when you say new Integer[]{7}, the array object itself remembers that it was created as an Integer array. This is for a couple of reasons, both related to casting:

  • If you have an Integer[], Java throws a runtime error if you try to do something sneaky like ((Object[]) myIntegerArray)["not a number!"] which would otherwise put you in a weird situation where your integer array contained a string; to do that it needs to check at runtime that each value you put in an array is compatible with Integer
  • You can cast Integer[] up to Object[] and back down to Integer[], but not back down to, say, String[] -- if you try you'll get a class-cast exception on the downcast at runtime. Java needs to know that the value was created as an Integer[] to support that.

Generics, on the other hand, don't have a runtime component. You may have heard of erasure, and that's what that refers to: all the generics stuff is checked at compile time but gets removed from the program and doesn't affect runtime at all.

So what happens if you try to make an array of some generic type, like new T[]{}? It won't compile, because the array needs to know what T is at runtime in order to work, but Java doesn't know what T is at runtime. So the compiler just doesn't let you build one of those at all.

But there is a loophole. If you call a varargs function with a generic type, Java allows the program to compile. Recall that the callsite of a varargs method creates a new array, though -- so what type does it give that array? In this case, it'll just create it as an Object[], since Object is the erasure of T.

In some cases this will work just fine, but your program is not one of them. In your program, the value that func takes on is

public Integer call(Integer... vals) {
  return vals[0] += 2;
}

The body isn't important; you could replace it with return null; and the program would behave identically. What's important is that it takes Integer..., not Object... or T... or something like that. Remember that Integer... is syntactic sugar, and after compilation it really takes Integer[]. So when you call this method on an array, the first thing it's going to do is cast it to Integer[].

And that's the problem: At the callsite, all we knew was that this function took T..., so we compiled it with a newly-created Object[] (which happened to be filled with an integer at runtime, but the compiler didn't know that statically). But the callee needed an Integer[], and you can't downcast an array that was constructed as new Object[]{} into an Integer[] for reasons discussed above. When you try, you get java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer; which is exactly what your program produces.

The moral of the story

There's no reason to try to learn all the details of what went wrong above. Instead, here's what I hope you take away:

  • Prefer Java collections such as List to arrays. Collections are the Java way.
  • Varargs and generics don't mix. You can easily walk into nasty traps like this one when you mix them together. Don't open the door to these kinds of things.

Hope that helps.

这篇关于使用泛型和Varargs的ClassCastException的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆