泛型理解
泛型,即泛化类型,通用的类型。
在没有泛型之前,有没有“通用的类型”呢?
有:父类类型就是子孙类对象的通用类型,接口类型就是其实现类对象的通用类型。
但是这些父类类型,接口类型好像还是不够通用。有没有一种类型可以是所有对象的通用类型呢?
有,那就是Object类型。
JDK5之前的集合类就是利用Object来实现存入所有引用类型的元素的能力的。
但是Object的泛化显得很僵硬,我们需要一种更加优雅的方式实现通用。
我们利用方法处理某逻辑时,不会针对某个特定的值去处理,而是定义方法形参,来接受一个可变的值,那么方法内部的代码就具备的通用性。
这种可变的值 实现通用性显得优雅而灵活。
那么类型可不可以实现可变呢?
那就是JDK5提出的泛型,可以理解为像方法形参一样的东西,不够方法是 值形参。而泛型是 类型形参。或者说叫“参数化类型”。
泛型作用
集合类中泛型的作用
泛型是JDK5新推出的特性。而最能体现泛型作用的就是集合类。JDK5中将集合类全部按照泛型特性处理重写了。
在JDK5前,没有泛型的集合有如下几个缺点:
1.存入集合的元素可以是任意引用类型。【无类型限制的存入规则,导致集合元素实际类型混乱,后期处理可能造成运行期间ClassCastException】
2.存入集合的元素的类型都会被集合忘记,元素都会以Object类型存储在集合中。【取出元素的类型都是Object类型,使用前一般都要强转为其真实类型。】
在JDK5后,指定泛型的集合解决以上两个缺点:
1.存入集合的元素只能是指定泛型的类型实参,不能是其他类型。否则编译报错。
2.集合元素取出时,元素类型还是存入时的类型。
综上所属:泛型的重要作用就是将 运行期间的ClassCastException异常,提前在编译期间暴露出来。
泛型的本质
泛型的本质应该是:泛化处理类型。
JDK5之前的集合类的泛化处理方式是将集合元素都以Object类型存储。这样不管什么引用类型的元素就都可以存入集合了。
这样的处理方式有明显的两个缺点。且代码底层还是固化的,Object类型写死在了源码中。
而泛型的出现,解决了使用Object类型泛化处理的缺点,即在底层代码中可以将需要泛化的类型 参数化,而不是写死。在使用时,才指定对应类型的实参。
Java给出了可以在类,接口,方法上定义泛型。
泛型类、接口
泛型类的定义格式
[修饰符] class 类名 <泛型形参> { 类体 }
举例如下
public class GenericTest<E>{
private E info;
{
E age;
}
public GenericTest(E info) {
this.info = info;
}
public E getInfo() {
E name;
return info;
}
public void setInfo(E info) {
this.info = info;
}
}
一般泛型形参可以是T,E,K,V这四个大写字母(也可以是其他的大写字母,单个大写字母比较规范)
比如上面代码中GenericTest就是一个泛型类,它的泛型形参是E。
另外泛型形参只能是引用类型,不能是基本数据类型。
泛型类的特点
泛型类的实例成员可以将E当成普通数据类型来使用。
比如作为实例变量的变量类型,作为构造器的形参类型,作为实例初始化块的局部变量类型,作为方法的形参类型,返回值类型,以及局部变量类型。这些都是合法的。
注意这里泛型类的泛型只能用于泛型类的实例成员使用,不能用于静态成员!
我们知道泛型类的泛型形参只能在使用泛型类时指定具体的实参。而泛型类的使用一般有:
1.使用泛型类定义变量
2.使用泛型类创建对象
而这些都是在泛型类的类加载后才有的动作。而泛型类在类加载时会初始化类成员,即静态成员。
所以如果静态成员使用泛型类的泛型形参作为类型的话,在类加载时是没有类型的,那么类加载就会失败。
所以Java这里禁止将泛型类的泛型作为其静态成员的类型。
泛型类的基本使用
定义泛型类变量,创建泛型类对象
public class Main {
public static void main(String[] args) {
// 泛型类定义变量,创建对象
GenericTest<String> test = new GenericTest<String>("123");
GenericTest<String> test0 = new GenericTest<>("123");//等价于上面,这是JDK7推出的菱形语法
GenericTest<Integer> test1 = new GenericTest<>(100);
Integer info = test1.getInfo();//此时泛型形参E是Integer,而泛型类的info实例变量也使用了E作为变量类型,所以info实例变量的类型是Integer。
GenericTest<Boolean> test2 = new GenericTest<>(true);
//test2.setInfo(123);//由于此时setInfo方法的形参info类型是Boolean,所以不能传入其他类型,否则编译报错
}
}
泛型类的JDK5前后版本兼容性
我们需要考虑一个问题:
JDK5中的集合类做了大量的改动,变成了典型的泛型类,那么那些使用了JDK5前集合类的老代码也需要改成使用泛型的集合类吗?
答案是否定的。因为很多地方都会用到没有泛型的集合类,改动它们是个巨大而又危险的工作。
所以Sun公司决定让泛型类适配那些老代码。即泛型类可以不使用泛型。那么不使用泛型的泛型类有什么特点呢?
不使用泛型的泛型类的特点:其内部使用泛型作为类型的地方 全部变为 使用Object类型作为类型
即使用泛型类时,不指定泛型实参的泛型类,默认使用Object类型作为其泛型实参。
public class Main {
public static void main(String[] args) {
//不使用泛型的泛型类 可以正常定义变量,创建对象。
GenericTest genericTest = new GenericTest("123");
//但是不使用泛型的泛型类,其内部使用泛型作为类型的成员 都变为 使用Object类型作为类型的成员
Object info1 = genericTest.getInfo();
//String info2 = genericTest.getInfo();//编译报错,getInfo返回值类型是Object的,不是String。
}
}
泛型类派生子类
类的使用不仅仅可以声明变量,创建对象,还有更重要的一点是派生子类。
那么泛型类派生子类有何特点呢?
public class Super<E> {}
class Base extends Super{}
//class Base1 extends Super<>{}//编译报错,未指定泛型父类的泛型
class Base2 extends Super<String>{}
class Base4<E,K,V> extends Super<E>{}
通过上面代码可以总结出:
1.泛型类可以不带泛型直接派生子类
2.泛型类带泛型时,如果子类不是泛型类,则必须指定泛型实参后,才能派生子类,否则编译报错
3.泛型类带泛型时,如果子类时泛型类,可以指派派生子类,但是子类的泛型中必须有父类泛型(注意泛型类可以带多个泛型)
泛型类型和原始类型
另外还有一个思考题:
ArrayList,ArrayList<String>这两个类的关系?
其中ArrayList是不带泛型的泛型类,ArrayList是带泛型String的泛型类。二者逻辑上是父子类,本质上是同一个类。
为什么说本质上是同一个类呢?
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList list = new ArrayList();
ArrayList<String> list1 = new ArrayList<>();
System.out.println(list.getClass().getName());//java.util.ArrayList
System.out.println(list1.getClass().getName());//java.util.ArrayList
System.out.println(list.getClass() == list1.getClass());//true
}
}
getClass()方法可以获得调用它的对象的类型。通过上述代码可以验证ArrayList<String>本质就是ArrayList。
其实这里我们将ArrayList称为ArrayList<String>的原始类型。ArrayList<String>泛型类型。
那么有又新的问题了,既然ArrayList<String>本质就是ArrayList,那么ArrayList<String>如何做到存入元素只能是String类型,取出元素类型不丢失的呢?
因为在编译时,Java的编译器会根据泛型信息校验存入ArrayList的元素是否符合要求,如果不符合,则直接编译报错。
说明:泛型是帮助编译器发现错误的。
而保证从集合中取出元素类型不丢失,涉及到泛型的擦除,补偿机制。在后面讨论。
泛型接口定义格式
[修饰符] interface 接口名 <泛型形参> { 接口体 }
由于接口成员中,成员变量是静态的,静态方法是静态的,内部类,内部接口,枚举类都是静态的。
只有抽象方法和默认方法不是静态的。所以接口成员只有抽象方法和默认方法可以使用接口泛型作为类型。(JDK9还有私有非静态方法也可以)
public interface GenericInterface<K,V> {
public abstract V test(K k);
public default void test2(K k){}
}
泛型接口的使用
1.定义变量
2.创建匿名内部类对象
public class Main {
public static void main(String[] args) {
// 泛型接口,定义变量,创建对象
// 需要注意的是JDK7时推出的菱形语法不适用于创建匿名内部类对象,即不能使用new GenericInterface<>代替new GenericInterface<Integer, String>
GenericInterface<Integer,String> kv = new GenericInterface<Integer, String>() {
@Override
public String test(Integer integer) {
return null;
}
};
// 当然也可以使用不带泛型的泛型接口:定义变量,创建对象
// 只是此时泛型接口的泛型都是被指定为Object类型
GenericInterface genericInterface = new GenericInterface() {
@Override
public Object test(Object o) {
return null;
}
};
}
}
3.被实现类实现,被子接口继承
class GImpl1 implements GenericInterface{//直接实现原始接口
@Override
public Object test(Object o) {
return null;
}
}
class GImpl2 implements GenericInterface<String,String>{//实现类不是泛型类,则实现的泛型接口必须指定泛型参数
@Override
public String test(String s) {
return null;
}
}
class GImpl3<K,V> implements GenericInterface<K,V>{//实现类是泛型类,则实现的泛型接口可以不指定泛型参数
@Override
public V test(K k) {
return null;
}
}
interface GenericInterface1 extends GenericInterface{//子接口直接继承原始接口
}
interface GenericInterface2 extends GenericInterface<String,String>{//子接口不是泛型接口,则泛型父接口必须指定泛型参数
}
interface GenericInterface3<K,V> extends GenericInterface<K,V>{//子接口是泛型接口,则泛型符接口可以不指定泛型参数
}
其他的基本和泛型类注意事项一致。不再赘述。
泛型方法
泛型方法的引入
我们知道泛型类,泛型接口中的非静态方法可以使用它们的泛型作为自己的方法形参类型,返回值类型,以及方法内局部变量类型。
这使得方法也能够实现泛化。但是这种泛化,有严重的耦合性。
可能一个类中只有一个方法需要泛化,其他地方不需要泛化,那么仅仅为了这一个方法的泛化,就将类变成泛型类,显得小题大作。
所以泛型方法应运而生。泛型方法的特点之一就是泛型方法的泛型形参不依赖于泛型类或接口。泛型方法可以自己定义自己的泛型。
泛型方法定义格式
[修饰符] <泛型形参> 返回值类型 方法名(形参列表){ 方法体 }
public class GenericTest{
public <T> T doSomething(T t){
T anotherT;
// do something for t
return t;
}
}
通过上面代码,我们可以知道
泛型方法的泛型可以用于定义自己的方法形参类型,方法返回值类型,以及方法内局部变量类型。
泛型方法的特点
泛型方法还有一个重要特点就是:泛型方法即可以是实例方法,也可以是静态方法。
public class GenericTest{
public static <T> T doSomething(T t){
T anotherT;
// do something for t
return t;
}
}
为什么泛型类,泛型接口的静态方法不能使用它们所在类和接口上定义的泛型,而泛型方法可以是静态方法?
之前在泛型类中说过,类第一次加载时,会先初始化静态成员,但是此时泛型类还没有被使用(如定义变量,创建对象),所以此时泛型类的泛型还没有被指定为具体的类型。因此静态方法的泛型类型不确定,所以无法完成初始化。
而泛型方法的泛型即可以在调用该方法时指定,也可以不指定。如果不指定泛型方法的泛型,则泛型方法底层会进行类型推断,若无法进行类型推断,则泛型方法的泛型为Object类型。
即泛型方法的泛型可以不依赖于任何地方,它可以自己指定。所以泛型方法可以是静态的。
而泛型类如果确定使用泛型的话,则必须明确指定泛型实参。否则编译报错。
泛型方法的类型推断能力
public class GenericTest{
public static <T> T doSomething(T t){
return t;
}
public static <T> T[] doOtherThing(String... x){
return (T[]) x;
}
}
class Main {
public static void main(String[] args) {
// 严格来说:调用泛型方法时,需要指定其泛型实参,如下格式指定
Integer integer = GenericTest.<Integer>doSomething(1);
// 但是泛型方法有类型推断能力,例如doSomething的形参T t,现在传入“123”,则泛型方法可以直接推断出T为String类型
String s = GenericTest.doSomething("123");
// 如果泛型方法的形参类型不是它的泛型,则默认T为Object类型
Object[] objects = GenericTest.doOtherThing("123", "456");
}
}
泛型方法与可变形参
public class GenericTest<E>{
public static <T> T[] doSomething(T... t){
return t;
}
}
class Main {
public static void main(String[] args) {
String[] arr = GenericTest.doSomething("123", "456", "qfc");
for (String s : arr) {
System.out.print(s);//123456qfc
}
}
}
以上代码正常运行。说明泛型方法支持泛型类型可变形参。
泛型方法与方法重载
方法重载的特点时两同,一不同。即同一个类中,方法名相同,形参类型组合不同(形参类型名字,个数,顺序不同)
public class GenericTest<E>{
public <T> T doSomething(T t){
return t;
}
// public E doSomething(E e){
// return e;
// }
public Integer doSomething(Integer i){
return i;
}
}
由于静态方法的泛化只能是泛型方法,不能是非泛型方法。所以静态泛型方法没有可以比较的点。
而泛型方法的重载主要关注其实例泛型方法和实例非泛型泛化方法的重载,以及和非泛化方法的重载。
其实如果已经存在泛化方法,那就没有必要存在重载的非泛化方法。因为泛化方法已经包含了非泛化方法。但是本质上来说,还是可以重载的。
而泛型方法是不能重载为非泛型泛化方法的。因为会造成调用混乱。调用者的形参两者都可以匹配。
类型通配符
类型通配符的定义
类型通配符就是?,表示通配任何引用类型。
为什么要引入类型通配符
在回答这个问题前,我们要先看看另一个问题
泛型类型是否存在型变?
型变的定义是
Object num = new String("123");
即向上转型。
或者强转。
那么ArrayList<Object> al = new ArrayList<String>();可以吗?
答案是编译报错。
ArrayList<Object> al = new ArrayList<String>();这里我们是期望将ArrayList<String> 向上转型为 ArrayList<Object>,我们期望它们存在父子关系,能发生型变。
但事实是:它们无法发生型变。因为在泛型类中我们认识到:不管是ArrayList<String>还是ArrayList<Object>,它们的本质都是ArrayList,是同一个类。
那么泛型类A,B(A,B是同一个类,但是它们的泛型实参存在父子类关系)之间赋值的意义何在?
如果一个方法的形参是一个ArrayList对象,但是不知道这个集合存什么类型的元素,那么我们应该来如何设计这个方法呢?
public class GenericTest{
public static void test(ArrayList al){
}
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
test(arrayList);
ArrayList<Integer> integers = new ArrayList<>();
test(integers);
}
}
上面test方法就是这样一个方法,它的形参是一个没有带泛型的集合类。它可以支持上面的需求。但是现在已经是JDK5之后了,我们已经不提倡这种不使用泛型的集合类了。
所以一般使用集合类时,都要带泛型。那么test方法参数设计成 ArrayList<Object>类型吗?显然之前验证了泛型类自己和自己不存在型变。
为了解决这一困境,Java提出了类型通配符?
public class GenericTest{
public static void test(ArrayList<?> al){
}
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
test(arrayList);
ArrayList<Integer> integers = new ArrayList<>();
test(integers);
}
}
即将test方法形参类型设计为 ArrayList<?>,表示test方法可以接受 ArrayList<泛型可以是任何引用类型> 的对象作为参数。
类型通配符算泛型形参,还是泛型实参?
类型通配符?指代的是类型实参,而不是类型形参。
类型通配符上限
类型通配符上限概念
类型通配符?默认可以匹配所有引用类型,即类型通配符的上限就是Object。
那么如果有一个场景,test方法形参ArrayList对象的集合元素只确定是Number类型子类类型。那么单单一个?就无法解决问题了。
此时可以给类型通配符加一个上限,即限制类型通配符的父类为Numebr即可。
类型通配符上限语法格式
泛型类名<? extends 上限类名>
举例
public class GenericTest{
public static void test(ArrayList<? extends Number> al){
}
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
//test(arrayList);//编译报错,因为String不是Number的子类或其本身
ArrayList<Integer> integers = new ArrayList<>();
test(integers);
ArrayList<Double> doubles = new ArrayList<>();
test(doubles);
ArrayList<Number> numbers = new ArrayList<>();
test(numbers);
ArrayList<Object> objects = new ArrayList<>();
//test(objects);//编译报错,因为Object不是Number的子类或其本身
}
}
类型通配符上限特点
public class GenericTest{
public static void test(ArrayList<? extends Number> al){
//al.add(1);//编译报错
//al.add(1.23);//编译报错
al.get(0);
}
}
被类型通配符上限控制的泛型类对象 只出不进。
即只能获取对象的内容,不能修改对象的内容。
比如上面代码,al只能get操作,不能add操作。这是为什么呢?
假设al可以执行add操作,由于al泛型类型只能确定是Number类型子类对象,那么al.add的元素既可以是1,也可以是1.23,因为1是Integer类型,1.23是Double类型,都是Number类型的子类。
这就违背了泛型的初衷,即约束元素类型。所以此时不能存入元素。
那么为什么可以get操作呢?
同理,因为al泛型类型只能确定是Number类型的子类对象,那么al中元素类型一定也是Number类型子类对象,所以从集合中取出的元素都可以当成Number类型。
协变
之前我们讨论过 泛型类自己和自己是不能发生型变的。那么现在ArrayList<Integer> 可以赋值给 ArrayList<? extends Number>貌似就是向上转型的能力,但是又不是向上转型,
我们把这种转型叫做“协变”。
即发生协变的泛型类间,一定是同一个类,且存在类型通配符上限。
通过源码理解类型通配符上限的意义
上面代码是ArrayList类的addAll(Collection<? extends E> c)的源码。
我们知道这里addAll操作是将一个Collection集合中的元素全部取出来 存入 一个ArrayList集合中。这个是没有学习泛型之前的理解。
学习完泛型之后:
调用addAll方法的ArrayList对象的 元素类型逻辑上 E
而addAll的形参c的类型是 Collection(? extends E),
说明两点:
1.添加的Collection集合元素的类型是:E本身或其子类类型
2.既可以添加Collection<E>,也可以添加Collection<E的子类>
如果将addAll改为 addAll(Collection<E> c)
这样的话:
1.添加的Collection集合元素的类型是:E本身或其子类类型
2.只能添加Collection<E>
而对于ArrayList<E>对象来说,对象中的元素,既可以是E类型,也可以是E子类类型。而E及其子类类型元素来源,既可以是Collection<E>集合,也可以是Collection<? extends E>集合。
所以说:Collection(? extends E) 比 Collection<E> 范围更大。
类型通配符下限
类型通配符下限概念
类型统配符的下限就是指:限制?的类型只能某个类型的父类,或其本身
类型通配符上限语法格式
泛型类名<? super 下限类名>
举例
public class GenericTest{
public static void test(ArrayList<? super Number> al){
}
public static void main(String[] args) {
ArrayList<Integer> integers = new ArrayList<>();
//test(integers);//编译报错
ArrayList<Number> numbers = new ArrayList<>();
test(numbers);
ArrayList<Object> objects = new ArrayList<>();
test(objects);
}
}
类型通配符下限特点
public class GenericTest{
public static void test(ArrayList<? super Number> al){
//可以正常add操作
al.add(1);
al.add(1.23);
Object object = al.get(0);//发现取出来的元素类型都是Object类型
}
public static void main(String[] args) {
ArrayList<Number> numbers = new ArrayList<>();
test(numbers);
}
}
我们通过上面代码可以发现,类型通配符下限控制的泛型类对象,可以进行插入元素,也可以取出元素。但是取出元素的类型变为Object类型。
这是因为方法形参的泛型类对象的元素类型:只知道是某个类的父类或其本身,如Number类的父类或其本身
则最大的父类就是Object,所以取出的元素就都可以当成Object类型处理。
逆变
我们将ArrayList<Object>对象赋值给ArrayList<? super Number>对象这种情况称为“逆变”
通过源码理解类型通配符下限的意义
这是TreeSet的构造器源码
该构造器的作用是创建一个指定比较器的TreeSet集合对象。
这里比较器的元素类型 是 E本身或其父类,接口。
我们知道TreeSet集合对象的元素类型是E,或者E的子类类型。
那么在E或E的子类类型元素被传入比较器后,如果都是E还好,可以基于E类中定义的属性来操作。
如果一个是E,一个是E子类对象,那么万一比较的比较逻辑是基于E子类对象特有属性来比较的(即E没有该属性)
那么就比较麻烦了。所以我们应该尽量选择E和E子类对象都有的属性来比较。
那么这种条件的属性来源:E本身,或E的父类中。
类型形参上限
不仅类型通配符可以设置上限,类型形参也可以设置上限
public class GenericTest<E extends Father>{//表示泛型形参E 的上限是Father,即E可以是Father本身或其子类
public static void main(String[] args) {
//GenericTest<Grand> grandGenericTest = new GenericTest<>();//编译报错
GenericTest<Father> fatherGenericTest = new GenericTest<>();
GenericTest<Child> childGenericTest = new GenericTest<>();
}
}
class Grand{}
class Father extends Grand{}
class Child extends Father{}
既然已经有了类型通配符上限,为啥还要有类型形参上限?即:类型通配符上限和类型形参上限的区别?
本质原因是:?代表的是类型实参,而不是类型形参。
类型形参可以有多个上限,通配符只能有一个上限。
类型形参多个上限间以&隔开,且必须只能包含一个父类上限,父类上限必须排在第一个位置,可以包含有多个接口上限。
java没有提供类型形参下限,为什么?
准确来说,可以提供类型形参下限。但是没有意义。
因为一般来说类型形参都应该是一个具体类。
举个例子:
如果集合泛型是抽象类或接口的话,那么集合元素的类型虽然可以表面上统一,但是实际已经胡乱了。
比较ArrayList<Number>集合的元素,它的内部元素既可以是Integer,也可是Double,还可以是Boolean。
那么如果我们允许存在类型形参下限的话
就会出现 ArrayList<T super Integer>,这样的话,ArrayList的元素类型T 就是Integer或者Integer的父类类型,那么就会出现上面的问题。
泛型擦除,自动转型
public class Main {
public static void main(String[] args) {
ArrayList al = new ArrayList<String>();
al.add(123);
}
}
上面代码中:ArrayList类型变量al指向了 被泛型String约束的ArrayList对象。
那么向al中加入一个Integer类型的对象123,会编译报错吗?
答案是不会。
可能很多人会这样理解:
al有两个类型,一个是编译时类型ArrayList,一个时运行时类型ArrayList<String>
而al调用add方法,其实其运行时类型的方法。所以al.add(123)肯定会编译报错。
这样的理解是基于多态。但是上面的代码没有多态发生。
因为ArrayList,ArrayList<String>是同一个类型,内存上不存在ArrayList<String>,只存在ArrayList。
而我们理解的al.add(123)编译报错,也不是运行期发生的,而是在编译期发生。
即al.add操作会在编译期根据al的编译时类型ArrayList检查插入的元素是引用类型即可。
如果想让上面的程序,al泛型检查只能插入String,则必须将al的变量类型改为 ArrayList<String>
通过上面代码的运行结果分析,
我们更加确定了ArrayList<String>在内存中是不存在的,即运行期间不存在ArrayList<String>,而泛型检查元素类型的能力是编译器在编译期提供的。
那么为什么运行期间不存在ArrayList<String>类型呢?那就要引入泛型的擦除
泛型的擦除
由于泛型不是Java的初始版本能力,而是JDK5才引入的新能力。为了兼容以前的代码,Java的泛型设计的不够彻底。或者说设计的瞻前顾后。
即Java的泛型既要支持去泛型能力,也要支持加泛型能力。
去泛型能力是为了引入泛型前写的老代码,老代码中使用的集合类没有使用泛型,如果强制泛型类的使用必须带泛型,那么老代码必将受到牵连。
所以Java的泛型设计出现了 类型擦除。
即:源码编译完成后,将完全剔除掉字节码中的泛型信息,只保留泛型类型对应的原始类型。即:ArrayList<String> 编译后 只剩 ArrayList
那么ArrayList类中使用了类定义的泛型形参作为自身类型的实例成员的类型该怎么办?
泛型擦除后,使用泛型形参作为类型的地方会被替代成什么?
1.如果泛型形参有上限,则替换为上限
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws NoSuchFieldException {
Field info = Generic.class.getDeclaredField("info");
System.out.println(info.getName()+"成员变量类型是:"+info.getType().getName());
Method[] declaredMethods = Generic.class.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod.getName()
+"方法的返回值类型是:"+declaredMethod.getReturnType().getName()
+",形参类型列表是:"+ Arrays.toString(declaredMethod.getParameterTypes()));
}
}
}
class Generic<E extends Number>{
E info;
public E getInfo() {
return info;
}
public void setInfo(E info) {
this.info = info;
}
public <T extends System> T test(T t){
return t;
}
}
运行结果如下
info成员变量类型是:java.lang.Number
test方法的返回值类型是:java.lang.System,形参类型列表是:[class java.lang.System]
getInfo方法的返回值类型是:java.lang.Number,形参类型列表是:[]
setInfo方法的返回值类型是:void,形参类型列表是:[class java.lang.Number]
2.如果泛型形参无上限,则替换为Object类型
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws NoSuchFieldException {
Field info = Generic.class.getDeclaredField("info");
System.out.println(info.getName()+"成员变量类型是:"+info.getType().getName());
Method[] declaredMethods = Generic.class.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod.getName()
+"方法的返回值类型是:"+declaredMethod.getReturnType().getName()
+",形参类型列表是:"+ Arrays.toString(declaredMethod.getParameterTypes()));
}
}
}
class Generic<E>{
E info;
public E getInfo() {
return info;
}
public void setInfo(E info) {
this.info = info;
}
public <T> T test(T t){
return t;
}
}
运行结果如下
info成员变量类型是:java.lang.Object
test方法的返回值类型是:java.lang.Object,形参类型列表是:[class java.lang.Object]
getInfo方法的返回值类型是:java.lang.Object,形参类型列表是:[]
setInfo方法的返回值类型是:void,形参类型列表是:[class java.lang.Object]
自动转型
我们现在已经知道了泛型在运行期间根本就不存在,它会被擦除后替换成泛型形参的上限类型。如果没有上限,就被替换为Object类型。
public class Main {
public static void main(String[] args) throws NoSuchFieldException {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("123");
arrayList.add("456");
String s = arrayList.get(1);
}
}
上面代码内部操作解析:
编译器的类型检查和转型动作
即:编译器在编译期间 会对存入元素做类型检查,若符合泛型实参要求,则直接存入(不做转型操作。)
另外如果运行时还有元素取出操作,则检查元素取出后有没有赋值,若有,编译器还会在取出元素地方 做类型检查,若无误,还会自动转型。
自动转型是取出操作引发的吗?
有很多人会以为自动转型是在get操作发生时做的,其实不是,下面给个例子验证。
通过上图可以看出,单纯的get操作不会引发checkcast转型操作。
继续看下面加了赋值给String s操作后,才会有checkcast转型检查,即强转操作。
泛型擦查引入问题及补偿
前面介绍了为什么要引入泛型擦查,因为要兼容JDK老版本代码。那么泛型擦除造成了什么问题?
泛型擦除即将泛型形参擦除并替换成其上限类型或Object类型。这说明泛型本质是一个参数化类型,而不是一个具体类型。
而一个非具体化的类型不能做的事有:
不能创建泛型形参类型对象,创建泛型形参类型数组对象,无法使用instanceof操作符,等等
class Generic<E>{
E info;
public E getInfo() {
return new E();//编译报错,type parameter 'E' cannot be instantiated directly
}
public E[] get(){
return new E[10];//编译报错,Type parameter 'E' cannot be instantiated directly
}
public boolean check(E e){
return e instanceof E;//编译报错,Class or array expected
}
}
关于上面泛型擦除引入的问题的补偿操作(涉及到反射知识,后面再说)
class Generic<E>{
E info;
public E getInfo(Class<E> kind) throws IllegalAccessException, InstantiationException {
return kind.newInstance();
}
public E[] get(int size){
return (E[]) new Object[size];
}
public boolean check(E e,Class<E> kind){
return kind.isInstance(e);
}
}
重写父类,实现接口泛化方法引入的问题
public class Generic implements GenericIn<Integer> {
@Override
public Integer test(Integer t) {
return null;
}
}
interface GenericIn<T>{
T test(T t);
}
我们知道泛型类,泛型接口在运行期间,泛型会被擦除,所以接口GenericIn在运行时,它的泛型T会被擦除,且替代为Object类型
即GenericIn接口的抽象方法为
Object test(Object t)
而Generic类实现了GenericIn接口,则必须重写其抽象方法。
根据重写规则:两同两小一大。其中两同指:方法名,形参类型列表相同
那么Generic类重写的方法的形参类型应该是Object类型。以上是实际情况分析。
但是根据GenericIn<Integer>泛型要求,GenericIn接口的抽象方法逻辑上应该是 Integer test(Integer t),
所以实现该接口的实现类 重写的方法的形参类型也应该是Integer。
现在实际和理想情况不一致。Java提出了桥方法解决方案。
即在编译器会在实现类Generic内部生成一个隐式的public Object test(Object t)方法,并且在这个方法中调用test(Integer t)方法
而在源码中只体现test(Integer t)方法,且能通过@Override注解检验
泛型类数组的使用问题
注意:无法直接创建泛型类数组,但是可以声明泛型类数组变量
public class Main {
public static void main(String[] args) {
ArrayList<String>[] arr = new ArrayList<String>[1];//编译报错 Generic array creation
}
}
所以采用间接方式:泛型类数组变量 指向 其原始类型数组对象
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList[] arr = new ArrayList[1];
ArrayList<String>[] genericArr = arr;
/**
* 以上代码中:arr,genericArr指向了同一个对象,即ArrayList原始类型数组对象
*/
ArrayList<Integer> list1 = new ArrayList<>();
list1.add(4);
/**
* arr[0]是一个ArrayList原始类型的元素,所以没有泛型约束它去检查list1指向的集合对象的内部元素
*/
arr[0]=list1;
/**
* genericArr[0]是一个ArrayList<String>泛型类型的元素,
* 而编译器会根据泛型实参在插入和取出元素的边界点去做类型检查,并在取出时添加自动转型的代码
* 即get(0)时取出的是Object类型元素,但是赋值给String变量s会导致取出时类型检查和自动转型,导致运行时报错
*/
String s = genericArr[0].get(0);//注意,此处代码改为 genericArr[0].get(0); 就不会报错,因为没有触发编译器的类型检查和自动转型操作
//或者改为 Object s = genericArr[0].get(0); 也不会触发类型检查,自动转型
}
}
上述代码存在隐患,所以应该修改为
泛型类数组变量 直接指向 其原始类型数组对象。
public class Main {
public static void main(String[] args) {
ArrayList<String>[] arr = new ArrayList[1];//直接将arr定义为ArrayList<String>
ArrayList<Integer> list1 = new ArrayList<>();
list1.add(4);
arr[0]=list1;//此时此行在编译期间就会报错,因为泛型会提示编译器在插入元素时触发类型检查
String s = arr[0].get(0);
}
}
总结