Java基础 自学讲义 5.泛型程序设计

目录
一. 泛型基础

  1. 泛型类
  2. 泛型方法

二. 泛型代码和虚拟机

  1. 类型擦除
  2. 翻译泛型表达式
  3. 翻译泛型方法
  4. 调用遗留代码

三. 约束与局限性

  1. 不能用基本类型实例化类型参数
  2. 运行时类型查询只适用于原始类型
  3. 不能创建参数化类型的数组
  4. Varargs警告
  5. 不能实例化类型变量
  6. 不能构造泛型数组
  7. 泛型类的静态上下文中类型变量无效
  8. 不能抛出或捕获泛型类的实例
  9. 可以消除对受查异常的检查
  10. 注意擦除后的冲突

四. 泛型类型的继承规则
五. 通配符类型

  1. 通配符概念
  2. 通配符的超类型限定
  3. 无限定通配符
  4. 通配符捕获

六. 反射和泛型

一. 泛型基础

泛型程序设计(Generic programming)为什么会出现呢? 我个人理解是有2个原因的:
1.数组的长度一旦设定了就不能改变了, 在C++中必须设定固定长度的数组, 在java中虽然数组长度可以是变量, 但是长度一旦设定也就不能改变了, 这一点其实也可以用vector这样的数据结构来实现;
2.数组只能存储同类的数据, 比如定义了一个Class People的数组, 那么这个数组中就只能存People和它的子类了, 不能同时在一个数组中存储多种对象;
比如我们可以用ArrayList来创建一个数组:

ArrayList<String> file = new Arraylist<>();//后面的尖括号中不用写, 会自动推测

1. 泛型类

然后也可以自己定义泛型, 实例的pair的泛型代码如下:

public class pair<T>
{
	private T first;
	private T second;
	
	public pair(){
		first = null;
		second = null;
	}
	public pair(T first, T second){
		this.first = first;
		this.second = second;
	}
	
	public T getFirst(){
		return this.first;
	}
	public void setFirst(T first){
		this.first = first;
	}
	public static void main(String[] args) {
		pair<Integer> x = new pair<>(2,3);
		System.out.println(x.getFirst());
	}
}  

T可以理解为C++中的template模板;

2.泛型方法

可以这样写泛型方法:

public <C> C getFirst(C x){
	return x;
}

泛型方法不一定要写在泛型类中, 也可以写在普通类中, 可以像下面这样调用:

System.out.println(x.<Integer>getFirst(3));

我们使用泛型方法的时候, 常常会有一个担忧, 就是我不能确定传入的泛型参数是否有某个方法, 是否继承了某个父类或者接口, 所以我们可以使用extends关键词来表示继承自某个父类或者支持某个接口:

public <C extends Person & red> C getFirst(C x){//Person是一个父类,red是一个接口
		return x;
}

要注意的是, 如果要限定绑定类型, 最多只能限定一个类(Java不支持多重继承), 如果要绑定某一个类, 必须把这个类放在第一个;
这里的extends表示的是子类型, 不等同于继承, 只是Java开发者不愿意多加一个sub关键词而已…
用&来表示连接多个接口;

二. 泛型代码和虚拟机

1. 类型擦除

在Java中定义泛型之后, java会自动用一个原始类型(raw type)来代替泛型, 原始类型的名字就是泛型类型的名字, 原始类型的类型取决于有没有extends一个类或者接口这样的限定类型, 如果有的话, 就是限定类型, 如果没有限定类型就是Object类型, 这个过程也叫类型擦除(type erased);
有一个问题是,如果限定类型是多个接口, 那么会怎么样呢?
实际上会使用extends的第一个接口作为原始类型, 在必要的时候编译器会加入强制类型转换(这里没太具体理解), 所以CoreJava建议把标签接口(tagging interface, 就是没有方法的接口)放在边界列表的末尾;

2.翻译泛型表达式

然后在泛型返回的时候其实是会返回一个Object类, 在最后会自动做强制类型转换成想要的类型, 这个过程就是翻译泛型表达式;

3. 翻译泛型方法

编译器在处理泛型方法的时候, 也会先擦除泛型类型, 然后对于返回值在最后进行强制类型转换, 但是这样的话, 在泛型类被继承的时候, 会出现一个问题, 看下面的代码:

import java.util.*;
import java.lang.reflect.*;
import java.time.*;

public class Pa<T> {
	private T first;
	private T second;

	public Pa(){}
	public Pa(T first, T second){
		this.first = first;
		this.second = second;
	}
	public T getFirst(){
		return this.first;
	}
	public void setFirst(T first){
		this.first = first;
	}
}

class PaSon extends Pa<Date>{
	public void setFirst(Date first){
		super.setFirst(first);
		
	}
}

class test{
	public static void main(String[] args) {
		PaSon me = new PaSon();
		Class<?> cl = me.getClass();
		Method[] methods = cl.getDeclaredMethods();
		for(Method i : methods) System.out.println(i);
	}
}

//public void PaSon.setFirst(java.util.Date)
//public void PaSon.setFirst(java.lang.Object)

事情是这样发生滴:
我定义了一个泛型类, 叫Pa, 然后我在里面定义了一个泛型方法叫setFirst;
然后我写了一个Pa的子类叫PaSon, 然后在继承的时候我就指定了泛型的类型是Date的, 然后我希望在子类PaSon中重写setFirst方法, 因为已经指定了类型是Date所以我重写的时候应该就不用使用泛型了, 我就直接用参数类型Date;
然后我重写之后功能都能正常实现, 但是我用反射去看这个子类的方法的时候,我发现还有一个从父类继承过来的方法public void PaSon.setFirst(java.lang.Object), 参数类型是Object, 为什么这个方法还存在呢?
不应该已经被重写覆盖掉了吗? 而且我尝试直接去调用这个方法是不能直接调用的, 他无论如何都会优先使用我重写的这个方法, 要求输入的参数是一个Date类型;

要分析这个问题, 就要回溯到类型擦除的概念了, 在泛型方法被编译器处理的时候, 因为这里没有extends一个限定类型, 所以被编译器看做是Object对象, 所以在继承的时候, 这个方法的参数为Object被继承过来了, 但是因为Java希望能实现多态性, 也就是说这里不管是传一个Date进来还是传一个Object进来, 都可以自动找到合适的方法去调用, 所以Jav引入了一个桥方法(bridge method)的概念:
public void PaSon.setFirst(java.lang.Object)这个方法内部实现大概是这样的:

public void setFirst(Object aDate){
	this.setFirst((Date)aDate);
}

会自动调用另外一个重写过的方法;
说桥方法是为了实现Java多态, 这个解释我是在CoreJava和一些博客中都看到了, 但是我不太理解, 因为我并不能真实去使用这个桥方法, 即我不能这样去使用它:

PaSon me = new PaSon();
Object aDate = new Date();
me.setFirst(aDate);

还有一个解释说, 是为了实现Java中的类型协变, 我还是不太理解… 以后在说吧, mmp;
最后关于泛型转换的结论:
泛型转换

4.调用遗留代码

在我们把用到了泛型的代码和一些遗留的没有使用泛型的代码一起使用的时候, 可能会出现一些问题, 因为不能确定这些遗留代码会怎样操作泛型类, 编译器会给出warnings, 如果希望屏蔽这些警告, 可以在方法前面使用@SuppressWarnings("unchecked"), 像下面这样:

@SuppressWarnings("unchecked")
public <C>C getFirst(){
	return (C)this.first;
}

三.约束与局限性

介绍一下在泛型变成中一些约束和局限性:

1.不能用基本类型实例化类型参数

因为泛型会自动擦除类型, 转成Object, 所以泛型不能用基本数据类型来实例化, 比如不能用int, double这些, 但是可以用Integer和Double这样的包装器来做;

2.运行时类型查询只适用于原始类型

不能使用instaneof去检查一个泛型类的实例是否是某个类的对象, 但是可以使用getClass方法, 但是要注意的是, 不同的泛型类型的getClass方法返回的是一样的, 都是返回原始类型, 不包括泛型变量的:

Pa<Integer> me = new Pa<>(3,5);
Pa<Date> you = new Pa<>(new Date(), new Date());
System.out.println(me.getClass() == you.getClass());//true 返回的都是class Pa

3.不能创建参数化类型的数组

不能定义泛型数组, 因为在类型擦除之后, 定义的数组的类型就固定了, 后续如果插入了其他类型的对象, 数组就会报错, 所以Java不允许new一个泛型数组, 但是声明一个泛型数组变量是合法的;
只是不允许创建这些数组,而声明类型为Pa[]的变量仍是合法的,不过不能用new Pa[10]初始化这个变量

4.Varargs警告

在Java中虽然是不允许创建泛型数组, 但是可以在参数列表中使用参数数量可变参数, 但是实质上传进来的就是一个真实的泛型数组, Java对此放松了限制, 但是会抛出一个Varargs警告, 要想避免这个警告, 可以有两种方法:

1.为包含addAll调用的方法增加标注@SuppressWarning(“Unchecked”)
2.用@SafeVarargs直接标注addAll方法

遍历这个变参数组的时候可以用for each去遍历;

5. 不能实例化类型变量

这个比较好的方法是用Java8的新特性, 使用函数指针去调用一个自己写的构造器;
或者也可以使用反射去创建一个新的类;

6.不能构造泛型数组

同前面的3. 不能创建参数化类型的数组
我已经看的脑阔疼了,烦的一比啊, 嘤嘤嘤;

7. 泛型类的静态上下文中类型变量无效

泛型类里面不能用static, 这很显然, 如果泛型里面还能用static, 那么根本就没有实例化, 不知道泛型是什么类型, 静态直接调用的时候编译器也不知道泛型类到底是啥, 还泛型你麻痹呢;

8.不能抛出或捕获泛型类的实例

花里胡哨的, 用到再说吧;

9. 可以消除对受查异常的检查

看起来烦的一比, 跟上面那个好像差不多, 以后再说吧;

10. 注意擦除后的冲突

在类型擦除后, 泛型类变成了Object类, 所以在equals方法里就有了两个, 比如对于一个Pa<String>来说, 他会有两个方法:

四. 泛型类型的继承规则


在泛型类继承的时候, 泛型变量的类T不同, 构成的类就不同, 比如, 如果Employee是Manager的父类, 如果我构建了一个Pair<Manager>这个声明, 我不能把一个Pair<Employee>的对象传给它, 因为他们是完全不同的对象, 考虑如果可以传过去, 那么如果我使用setFirst方法, 就可以把一个Manager的对象传给first, 那么就会出现Employee和Manager对象出现在相同的Pair<manager>中的,这显然是错的啊;
上两张图片看起来比较帅, hoho~

五. 通配符类型

1.通配符概念

前面已经提到了这样的一个场景:

	public static void main(String[] args) {
		Pa<manager> me = new Pa<>();
		printPa(me);
	}
	public static void printPa(Pa<employee> u){
		employee first = u.getFirst();
		employee second = u.getSecond();
		System.out.println(first+" "+second);
	}

如果我想去传一个Pa<manager>给一个参数是Pa<employee> u的方法, 这是不合法的, 那么问题来了, 其实manager是employee的一个子类, 讲道理的话, 我要想把一个Pa的first和second都设置成manager应该是可以的, 那么怎么样才能实现呢, 我们可以修改一下方法的参数类型, 改成:

public static void printPa(Pa<? extends employee> u)

这样就可以传进来一个Pa<manager>了, 但是这样使用的话也有一个问题, 如果使用这个参数的话, 在方法中不能使用setFirst方法, 也就是说在方法中像下面这样使用是会报错的:

u.setFirst(new employee());

究其原因, 这是为什么呢?
因为在setFirst方法实现的时候, 传入的参数类型是泛型T, 虽然我这里实际传进来的是一个employee类型的对象, 但是实际上我的参数是<? extends employee>这个东西本质上只是一个通配符?, 这个东西看起来应该不像是一个对象, 因为他没办法对一个通配符做一些操作和引用, 所以无法这样调用setFirst方法;

2. 通配符的超类型限定

可以使用super关键词做通配符的限定, 用<? super manager>表示只能传入一个manager的父类的类型, 当然可以传入一个Object类;
但是要注意这样的超类通配符限定只能做参数类型不能做返回值类型,

3. 无限定通配符

就这样用:Pair<?>
有两种用法:

getFirst()
void setFirst(?)

getFirst 的返回值只能赋给一个Object, setFirst 方法不能被调用, 甚至不能用Object调用
Pa<?> 和Pa本质的不同在于: 可以用任意Object对象调用原始Pa类的setObject方法。

4.通配符捕获

编写一个交换一个pair元素的方法:public static void swap(Pair<?> p),通配符不是类型变量,因此,不能再编写代码时使用" ? "作为一种类型

可以编写一个辅助方法swapHelper来解决这个问题,如下:

public static <T> void swapHelper(Pair<T> p)
{
    T t = p.getFirst();
    p.setFirst(p.getSecond);
    p.setSecond(t);
}

swapHelper是一个泛型方法, 在这种情况下,swapHelper方法的参数T捕获通配符

这里不太理解, 以后遇到之后再详细解决吧;

六. 反射和泛型

Class类是泛型的, 例如, String.class实际上是一个Class<String>类对象(事实上是唯一的对象) 类型参数十分有用, 这是因为它允许Class<T>方法返回类型更加具有针对性;

虚拟机中的泛型类型信息很有点东西的, 它虽然不能知道自己是从哪个类型的泛型类, 不知道自己是Pa<Integer>还是Pa<String>, 但是它知道自己是来自于Pa<T>;

总的来说可以用反射来分析泛型的这些特点:

1.这个泛型方法有一个叫做T的类型参数
2.这个泛型参数有一个子类型限定,其自身又是一个泛型类型
3.这个限定类型有一个通配符参数
4.这个通配符参数有一个超类型限定
5.这个泛型方法有一个泛型数组参数

为了表达泛型类型声明,Java SE 5.0 在java.lang.reflect包中提供了一个新的接口Type,这个接口包含下列子类型:

1.Class类:描述具体类型
2.TypeVariable接口:描述类型变量(如T extends Comparable<? super T>)
3.WildcardType接口:描述通配符(如?super T)
4.ParamenterizedType接口:描述泛型类型或接口类型(如 Comparable<? super T>)
5.GenericArrayType接口:描述泛型数组(如T[])

注: 本文部分内容来源于狮锅艺的JAVA实践博客;

上一张继承层次的图:

猜你喜欢

转载自blog.csdn.net/qq_33982232/article/details/82955642