1 泛型的定义
泛型的定义:参数化类型。将具体的数据类型参数化,在使用/调用时再传入具体的类型。
如何理解呢?参考下面的例子:
ArrayList list = new ArrayList();
list.add("xx");
list.add(123);
for (int i = 0;i < list.size;i++) {
String s = (String) list.get(i);
}
我们定义了一个ArrayList,其中存储的是一个Object数组,故而可以存储任意类型,可以存字符串“xx”,也可以存整形123。但是当我们调用时,会发现报ClassCastException。
如何解决?我们可以定义一个专门用来存储特定一种类型对象的ArrayList,但是不可能每当用到一个类型就去定义一个对应该类型的ArrayList。泛型就可以很好的解决这个问题。我们将ArrayList内部的数组类型参数化为T,通过在外部调用ArrayList时指定具体的类型。如此,我们希望ArrayList存储String,我们就指定泛型参数T类型为String,即ArrayList
public ArrayList {
private T[] value;
private int size;
......
}
ArrayList list = new ArrayList<>();
ArrayList list2 = new ArrayList<>();
这里举的例子是泛型应用在类上,ArrayList
2 使用泛型
2.1 泛型类
使用泛型类时,把泛型参数
可以省略编译器能自动推断出的类型,例如:List
如果不指定泛型参数类型时,编译器会给出警告,并且将
2.2 泛型接口
泛型接口的定义与泛型类是类似的,如下:
public interface List {
public T next();
}
泛型接口的实现类分两种情况:
//1.实现未指定泛型参数具体类型的接口
//此种情况,实现类必须声明与接口一样的泛型参数
public ArrayList implements List {
@Override
public T next() {
......
}
}
//2.实现指定泛型参数具体类型的接口
public ArrayList implements List {
@Override
public String next() {
......
}
}
2.3 泛型方法
/**
* 泛型方法的基本介绍
* @param tClass 传入的泛型实参
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间的非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)表明该方法将使用泛型参数T,此时才可以在方法中使用泛型参数T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public T genericMethod(Class tClass) throws InstantiationException, IllegalAccessException {
T instance = tClass.newInstance();
return instance;
}
需要注意:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。因为静态方法无法访问类声明的泛型参数。
在使用泛型方法时,既可以指定泛型类型,也可以不指定。如下:
public Test {
public static void main(String[] args) {
//不指定泛型类型时
//当不指定泛型类型时,泛型类型为传入参数类型的最小共同父类
Integer a = Test.next(1, 2);//传入参数都是Integer,故泛型参数类型为Integer
Number a = Test.next(1, 2.1);//传入了一个Integer,一个Double,它们的最小共同父类是Number,故泛型参数是Number
//不指定泛型类型时
//指定了泛型类型,那么传入参数只能为指定类型的本身类型及其子类,否则会报错
Integer c = Test.next(1, 2);
Number d = Test.next(1, 1.2);
}
//定义了一个泛型方法
static T next(T t1, T t2) {
return t1;
}
}
在声明泛型类、泛型接口或泛型方法时,可以同时声明多个泛型参数。
3 类型擦除
Java的泛型是伪泛型。Java的泛型基本上都是在编译器这个层次上实现的,在编译后生成的字节码中是不包含泛型的类型信息的,使用泛型的时候指定具体类型,在编译器编译的时候会去掉,这个过程称为类型擦除。对于JVM来说,它不知道泛型的具体信息。
泛型参数经过类型擦除后会得到一个原始类型,通常情况下为其限定类型,如果泛型参数没有限定类型,则为Object类型。例如:ArrayList
3.1 类型检查
Java编译器是先对代码进行类型检查,在进行类型擦除,然后再编译。
这也就是为什么ArrayList
ArrayList list = new ArrayList<>();
list.add("xx");
ArrayList list2 = new ArrayList();
list2.add(12);
new ArrayList().add("xx");
ArrayList list3 = new ArrayList();
ArrayList list4 = new ArrayList();
可以看到类型检查是针对引用的。对一个引用指定了泛型类型,那么只能add该类型的对象。但是我们看到 直接初始化一个对象,也会对其进行类型检查。
注意:编译器不允许最后两种用法
3.2 对类型擦除的个人理解
实际上,类型擦除可以理解为,在“写”的时候,用Obejct类型(或其限定类型)的引用指向泛型类型的对象。而在“读”的时候,将Object类型(或其限定类型)引用向下转型为原来的泛型类型。
public class cp3 {
public static void main(String[] args) {
String[] a = asArray("xa", "xx");
System.out.println(Arrays.toString(a));
String[] b = cp3.pickTwo("xxx", "kkk", "123");
System.out.println(Arrays.toString(b));
ArrayList list = new ArrayList<>();
list.add("12");
System.out.println(list.get(0));
}
static K[] pickTwo(K k1, K k2, K k3) {
return asArray(k1, k2);
}
static T[] asArray(T... objs) {
return objs;
}
}
//输出结果为:
[xa, xx]
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
at cp3.main(cp3.java:11)
可以看到报错了,第11行,也就是代码中的第二个输出那里。那么为什么呢?报的错显示,强转错误,Object数组不能强转为String数组。我们先使用javac指令编译源码,再使用jad反编译得到字节码文件,最终得到反编译后的代码:
public class cp3
{
public cp3()
{
}
public static void main(String args[])
{
String a[] = (String[])asArray(new String[] {
"xa", "xx"
});
System.out.println(Arrays.toString(a));
String b[] = (String[])pickTwo("xxx", "kkk", "123");
System.out.println(Arrays.toString(b));
ArrayList list = new ArrayList();
list.add("12");
System.out.println((String)list.get(0));
System.out.println(((String[])Cnm.createArray(java/lang/String)).getClass());
}
static Object[] pickTwo(Object k1, Object k2, Object k3)
{
return asArray(new Object[] {
k1, k2
});
}
static transient Object[] asArray(Object objs[])
{
return objs;
}
}
可以看到,对于pickTwo方法,new Object[] {}
,返回值即这个Object数组,pickTwo方法也返回这个Object数组,这个Object数组是无法强转为String数组的,所以报错。
但是第一行输出为什么不报错呢?对于asArray方法,传入的是一个Object[]类型的引用,指向一个String数组,将这个数组直接返回,是可以强转为String数组的。
原理很简单:一个父类引用指向子类对象,这是向上转型,我们可以将这个引用进行向下转型为该子类类型。
String[] s = {"xx", "kk"};
Object[] o = s;//向下转型
String[] s1 = (String[]) o;//向上转型,强转成功
Object[] o1 = new Object[] {"xx", "kk"};
String[] s2 = (String[]) o1;//强转失败
3.3 类型擦除与多态的冲突及解决
假设我们在泛型接口Pair
里面写了一个方法,它的实现类DataPair(在声明类时指定了接口的泛型类型为Date,不是泛型类)实现了这个方法,编译后,接口里面的泛型类型被擦除为原始类型Obejct,但是实现类里面的类型仍为指定的类型,这使得该方法不再是被重写,而是发生了重载,实现类还应继承了一个参数类型为Object类型的方法。
但是实际情况并非如此,Java编译器我们实现了一个桥方法。在反编译后的代码中,我们看到:桥方法重写了接口中的该方法,并且在桥方法中调用了我们自己重写的方法。桥方法对外是不可见的,所以呈现的效果是我们自己写的方法重写了原接口的方法。
interface Pair {
T getValue();
void setValue(T value);
}
class DataPair implements Pair {
Date value;
@Override
public Date getValue() {
return value;
}
@Override
public void setValue(Date value) {
this.value = value;
}
}
//反编译后的代码
class DataPair
implements Pair
{
DataPair()
{
}
public Date getValue()
{
return value;
}
public void setValue(Date value)
{
this.value = value;
}
public volatile void setValue(Object obj)
{
setValue((Date)obj);
}
public volatile Object getValue()
{
return getValue();
}
Date value;
}
参考
ttps://www.cnblogs.com/wuqinglong/p/9456193.html
https://www.cnblogs.com/coprince/p/8603492.html