欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

简单理解java泛型的本质(非类型擦除)

程序员文章站 2023-11-24 13:25:46
背景 之前在网上发现这个问题 public class generictest { //方法一 public static

背景

之前在网上发现这个问题

public class generictest {
 //方法一
 public static <t extends comparable<t>> list<t> sort(list<t> list) {
 return arrays.aslist(list.toarray((t[]) new comparable[list.size()]));
 }
 //方法二
 public static <t extends comparable<t>> t[] sort2(list<t> list) {
 // 这里没报错
 return list.toarray((t[]) new comparable[list.size()]);
 }
 public static void main(string[] args) {
 list<integer> list = new arraylist<>();
 list.add(1);
 list.add(2);
 // 方法一调用正常
 system.out.println(sort(list).getclass());
 // 方法二调用报错了,这里报错了
 system.out.println(sort2(list).getclass());
 }
}

这个问题有以下四个现象:

(1)方法一调用完全正常;

(2)方法二调用报错了;

(3)方法二报错的地方是在system.out.println(sort2(list).getclass());这行,而不是return list.toarray((t[]) new comparable[list.size()]);这行;

(4)报的错是[ljava.lang.comparable; cannot be cast to [ljava.lang.integer;;

怎么样?你心中有答案嘛?类型擦除?怎么擦?摩擦摩擦?

解决

刚拿到这道题,我也是一脸懵逼,这要报错也应该是在return list.toarray((t[]) new comparable[list.size()]);这行啊,而且要报错应该两个方法都报错啊。

抱着不放弃不抛弃的心态,彤哥做了大量的实验,终于得出了泛型的本质,且听我娓娓道来。

小插曲

首先,我们要明白,java中的数组是不支持向下转型的,但是如果本身就是那个类型的是可以转过去的,请看下面的例子:

public static void main(string[] args) {
 object[] objs = new object[]{1};
 // 类型转换错误
// integer[] ins = (integer[]) objs;
 object[] objs2 = new integer[]{1};
 // 不报错
 integer[] ins2 = (integer[]) objs2;

}

java里的泛型是假泛型,只在编译期有效,在运行时是没有泛型的概念的,举个简单的例子:

public static void main(string[] args) {
 list<string> strlist = arrays.aslist("1");
 list<integer> intlist = arrays.aslist(1);
 // 打印:true
 system.out.println(strlist.getclass() == intlist.getclass());
 }

可以看到两个list的类型是一样的,如果你觉得这个例子不够说服力,那我给你个过分点的例子:

public static void main(string[] args) throws nosuchmethodexception, invocationtargetexception, illegalaccessexception {
 list<string> strlist = new arraylist<>();
 method addmethod = strlist.getclass().getmethod("add", object.class);
 addmethod.invoke(strlist, 1);
 addmethod.invoke(strlist, true);
 addmethod.invoke(strlist, new long(1));
 addmethod.invoke(strlist, new byte[]{1});

 // 打印:[1, true, 1, 1]
 system.out.println(strlist);
}

瞧,我可以往一个string类型的list中扔任何我想扔的东西,服不服?!

所以说java里面的泛型是假的,运行时不存在滴。

回归正题

数组不能向下强转我懂了,类型擦除我也懂了,似乎还是过不好这一生,呃不是,是还是解决不了这道题啊?

呃,好像是~~

我们再来看一个简单的例子:

// generictest2.java(源码)
public class generictest2 {
 public static void main(string[] args) {
 system.out.println(raw("1"));
 }
 public static <t> t raw(t t) {
 return t;
 }
}
// generictest2.class(反编译)
public class generictest2 {
 public generictest2() {
 }
 public static void main(string[] args) {
 system.out.println((string)raw("1"));
 }
 public static <t> t raw(t t) {
 return t;
 }
}

嗯~似乎看出来点端倪,反编译后多了个构造方法。

呃,没错。还有呢?

仔细一看,system.out.println((string)raw("1"));这一句多加了个string强转。

这就是关键所在,结合类型擦除,运行时并没有所谓的泛型,所以raw()返回的其实是object,但是调用者自己知道我要的是string类型啊,所以我就知道强转一下喽。

我们再来看个极端的例子:

// generictest2.java(源码)
public class generictest2 {
 public static void main(string[] args) {
 system.out.println(raw("1"));
 }
 public static <t> t raw(t t) {
 return (t)new integer(1);
 }
}
// generictest2.class(反编译)
public class generictest2 {
 public generictest2() {
 }
 public static void main(string[] args) {
 system.out.println((string)raw("1"));
 }
 public static <t> t raw(t t) {
 return new integer(1);
 }
}

仔细观察,可以发现,raw()方法里的强转(t)new integer(1)变成了new integer(1),强转被擦除了,实际上在运行时这里的t变成了object,所有类型都是object的子类,也就不需要强转了。

而(string)raw("1")的强转还是加上的,这是调用者知道类型是string,所以raw()返回后自己强转成string一下。

当然,这个代码运行是会报错的,java.lang.integer cannot be cast to java.lang.string,因为raw()返回的是integer类型,强转成string类型失败了。

好了,基本思路就是这样。

泛型类呢?

我们上面举的例子都是泛型方法,那么泛型类呢?

同样地,我们来看个例子:

// generictest3.java(源码)
public class generictest3 {
 public static void main(string[] args) {
 system.out.println(new raw<string>().raw("1"));
 }
}
class raw<t> {
 public t raw(t t) {
 return (t)new integer(1);
 }
}
// generictest3.class(反编译)
public class generictest3 {
 public generictest3() {
 }
 public static void main(string[] args) {
 system.out.println((string)(new raw()).raw("1"));
 }
}
class raw<t> {
 raw() {
 }
 public t raw(t t) {
 return new integer(1);
 }
}

可以看到,跟泛型方法的表现一模一样。当然,这里运行时也会报java.lang.integer cannot be cast to java.lang.string这个错误。

总结

java中的泛型只在编译期有效,在运行时只有调用者知道需要什么类型,且调用者调用泛型方法后自己做强制转换,被调用者是完全无感的。

所以,出现问题不要问被调用者,而是要问调用者,你丫是怎么调用的?!

解答开篇

为了方便我们还是把开篇的问题拿过来。

// generictest.java(源码)
public class generictest {
 //方法一
 public static <t extends comparable<t>> list<t> sort(list<t> list) {
 return arrays.aslist(list.toarray((t[]) new comparable[list.size()]));
 }
 //方法二
 public static <t extends comparable<t>> t[] sort2(list<t> list) {
 // 这里没报错
 return list.toarray((t[]) new comparable[list.size()]);
 }
 public static void main(string[] args) {
 list<integer> list = new arraylist<>();
 list.add(1);
 list.add(2);
 // 方法一调用正常
 system.out.println(sort(list).getclass());
 // 方法二调用报错了,这里报错了
 system.out.println(sort2(list).getclass());
 }
}

这里似乎又不太一样,变成了<t extends comparable<t>>,其实是一样的啦,如果单独写<t>是相当于<t extends object>的。

那么,我们就延伸一下,被调用者是完全无感的,它只能尽力拿到它知道的类型,比如这里就只能尽力拿到comparable,如果是<t>拿到的就是object。

所以,方法二返回的就是实打实的comparable[]类型,作为被调用者,它一点问题都没有。

但是,调用方是知道我需要的是integer[]类型的,因为list里面是integer类型,所以返回的应该是integer[]类型,所以我就强转喽,然后就报错了。

到底是不是这样?我们来看看反编译后的代码:

// generictest.class(反编译)
public class generictest {
 public generictest() {
 }
 public static <t extends comparable<t>> list<t> sort(list<t> list) {
 return arrays.aslist(list.toarray((comparable[])(new comparable[list.size()])));
 }
 public static <t extends comparable<t>> t[] sort2(list<t> list) {
 // 这里使用的是comparable[]强转,所以返回的也是实打实的comparable[]类型
 return (comparable[])list.toarray((comparable[])(new comparable[list.size()]));
 }
 public static void main(string[] args) {
 list<integer> list = new arraylist();
 list.add(1);
 list.add(2);
 system.out.println(sort(list).getclass());
 // 数组向下转型失败
 system.out.println(((integer[])sort2(list)).getclass());
 }
}

可以看到,跟我们的分析完全一致。

java中的泛型只在编译期有效,在运行时只有调用者知道它自己需要什么类型,且调用者调用泛型方法后自己做强制转换,被调用者是完全无感的,被调用者只能尽力拿到它所知道的类型。

此时,我的脑海中不经响起那熟悉的旋律,“一句话,一辈子……”,今天的这句话你记住了吗?

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。