泛型程序设计
泛型,即“参数化类型”,将原来的具体类型参数化。在不创建新类型的情况下,通过泛型指定不同的类型形参,来控制实际传入实参的具体类型。换句话说,就是在使用和调用时传入具体的类型。
为什么使用泛型?
- 能够对类型进行限定(比如集合)
- 将运行期错误提前到编译期错误
- 获取明确的限定类型时无需进行强制类型转化
- 具有良好的可读性和安全性
泛型类
泛型类的定义
一个简单的泛型类,和普通类的区别是,类名后添加了<T>
一个泛型标识,“T"类型参数(类型形参),传入的是类型实参,当然也可以用其他字母标识,但是"<>"左右尖括号必须存在。
public class Generic<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String[] args) {
/* 1.没有传入具体的类型参数,可以存放任何类型的数据【下图】
* 本质:虚拟机会对泛型代码进行类型擦除,类型擦除后Generic<T>会变为
* Generic(原始类型),后面会讲到,无限定类型的域用Object代替,即擦除
* 后T data->Object data,这也是为什么没有传入具体类型,却能存放多种
* 类型的原因。
*/
Generic generic = new Generic();
generic.setData(1);
generic.setData("String");
generic.setData(new Object());
Generic genericStr = new Generic<String>();//CORRECT [1]
genericStr.setData("hello");
genericStr.setData(1);
/*
* 2.参数化类型(初始化时传入具体的类型参数)【下图】
* 本质:由编译器进行类型转化的处理,无需人为干预。
* 当调用泛型方法时,编译器自动在调用前后插入相应的
* 强转和调用语句。
*/
Generic<String> genericString=new Generic<>();
genericString.setData("hello");
//Generic<int> genericInt=new Generic<>(); ERROR [2]
Generic<Integer> genericInt=new Generic<>();
}
}
【1】原始类型可以接受任何参数化类型,即Generic generic == new Generic<String>()
如[1]处所示。
【2】泛型的类型参数只能是类类型,不能是基本类型。如[2]处,但可以使用期包装类型。
【3】泛型参数命名规范如下:
泛型命名规范:国际惯例,类型参数的命名采用单个大写字母。
常见的泛型命名有:
T
- Type:第一类通用类型参数。S
- Type:第二类通用类型参数。U
- Type:第三类通用类型参数。V
- Type:第四类通用类型参数。E
- Element:主要用于Java集合(Collections)框架使用。K
- KeyV
- ValueN
- NumberR
- Result
泛型接口
泛型接口的定义
和泛型类定义相似,如下:
public interface GenericInterface<T> {
T getData();
T setData(T data);
}
类接口的实现
类接口的实现存在三种形式,第一种无泛型,域类型用Object定义;第二种有泛型,域变量用泛型参数定义;第三种传递具体的类型参数,域变量的类型为具体的类型。
public class GenericInterfaceImpl implements GenericInterface {
@Override
public Object getData() {
return null;
}
@Override
public Object setData(Object data) {
return null;
}
}
/*
* 实现类的类型参数也需要声明,否则编译器会报错
*/
class GenericInterfaceImpT<T> implements GenericInterface<T> {
@Override
public T getData() {
return null;
}
@Override
public T setData(T data) {
return null;
}
}
/*
* 传入具体的类型实参
*/
class GenericInterfaceImplStr implements GenericInterface<String> {
@Override
public String getData() {
return null;
}
@Override
public String setData(String data) {
return null;
}
}
泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
1、泛型方法的定义:类型变量放在修饰符前,返回类型的后面
# 修饰符 <T> 返回值 方法名(...);
* 示例
class ArrayAlg {
/*
*修饰符与返回值(T)中间的<T>标识此方法为泛型方法
*<T>表明该方法可以使用泛型类型T,可以在形参或者方法体中声明变量
/
public static <T> T getMiddle(T...a) {
return a[a.length/2];
}
}
2、调用泛型方法,在方法名前的尖括号放入具体的类型:
String middle = ArrayAlg.<String>getMiddle("John","Q","Public");
// 类型推断:类型参数可以省略 等同于
String middle = ArrayAlg.getMiddle("John","Q","Public");
使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 类型参数推断。
3、泛型方法辨别真假
/*
* 泛型类
* 注意:下面为了介绍,不把泛型方法归类到成员方法里,泛型方法是特指!
*/
public class GenericMethod<T> {
private T data;
/*
* [成员方法:非泛型方法]
* T getData()和setData(T data) 都不是泛型方法
* 他们只是类中的成员方法,只不过是方法的返回值类型
* 和方法的形参类型是用的泛型类上的T所声明的。
*/
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
/*
* [泛型方法]
* 泛型参数可以有多个,这里的T和泛型类上的T无任何关联,但是但是
* 它和泛型类上的参数类型变量相同,这时候idea会给予一个rename提示
*/
public <T,S> T genericMethod(S...a) {
return null;
}
/*
* [泛型方法]
* 使用泛型类上的泛型变量
* 这时候的T就和泛型类的类型相关了
*/
public <V> T genericMethod$1(T a, V b) {
return null;
}
/*
* [静态方法]
* 静态方法不能使用泛型类上的类型参数
* 如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法
*/
//public static T getDataStatic(T e) { } //ERROR [1]
/*
* [泛型方法]
* 静态泛型方法
*/
public static <E> E genericMethodS(E e) {
return e;
}
}
/*
*普通类中的泛型方法
*/
class OrdinaryClass {
public <T> void sayHello() {
T a;
//...
}
}
下面以苹果为例:
public class GenericClass<T> {
//成员方法,形参类型与泛型类的类型参数相关
public void print$1(T a) {
System.out.println(a.toString());
}
//下面三个都为泛型方法
//--begin
public <T> void print$2(T a) {
System.out.println(a.toString());
}
public <S> void print$3(S a) {
System.out.println(a.toString());
}
public static <T> void print$4(T a) {
System.out.println(a.toString());
}
//--end
public static void main(String[] args) {
Apple apple = new Apple();
MacBook macBook = new MacBook();
HongFuShi hongFuShi = new HongFuShi();
// 泛型类在初始化时限定了参数类型,成员方法中若使用泛型参数将会受限
GenericClass<Apple> genericCls = new GenericClass<>();
genericCls.print$1(apple);
//MacBook是apple的一个子类
genericCls.print$1(macBook);// OK
//由于初始化指定了泛型类型,print$1形参中的参数类型和泛型类的类型参数相关联
//所以,只能打印Apple及其子类
//genericCls.print$1(hongFuShi); ERROR
//泛型方法中的泛型变量类型与泛型类中的泛型参数没有任何关联
//所以说下面都能正常执行
genericCls.print$2(apple);
genericCls.<MacBook>print$2(macBook);//类型参数可以省略 [2]
genericCls.print$2(hongFuShi);
GenericClass.print$4(hongFuShi);
}
}
class Apple {
@Override
public String toString() {
return "Apple,Steve Jobs";
}
}
class MacBook extends Apple {
@Override
public String toString() {
return "MacBook";
}
}
class HongFuShi{
@Override
public String toString() {
return "HongFushi";
}
}
小结:
【1】泛型方法的标识:方法修饰符后返回值之前有"<...>"的声明。(判断是否为泛型方法)
【2】泛型方法可以定义在普通类中,也可以定义在泛型类中。
【3】静态方法不能使用泛型类上的类型参数,如[1]处
【4】成员方法中使用的参数类型和泛型类中的声明的类型参数有关联。
【5】泛型类中的参数类型与泛型方法中的参数类型的关联:泛型方法可以使用泛型类上的参数类型,这时候就与泛型类上的参数相关联;如果泛型方法中声明了与泛型类上相同的参数类型,那么优先使用泛型方法上的参数类型,这时候idea会给予一个rename提示。
【6】泛型方法的使用过程中,无需对类型进行声明,它可以根据传入的参数,自动判断。[2]
【7】方法中泛型参数不是凭空而来的,要么来自于泛型类上所定义的参数类型,要么来自于泛型方法中定义的参数类型。
泛型方法能独立于类而发生变化,所以说在使用原则上,在能达到目的的情况下,尽量使用泛型方法。即,如果使用泛型方法可以取代将整个类泛化,那么应该有限采用泛型方法。
类型变量的限定
对于类型变量没有限定的泛型类或方法, 它是默认继承自Object
,当没有传入具体类型时,它有的能力只有Object
类中的几个默认方法实现,原因就是类型擦除。
如果某个类实现Comparable接口中的compareTo方法,我们就可以通过compareTo比较两个值的大小。比如我们要计算数组中的最小元素:
public static void main(String[] args) {
// 传入 4 , 2 , 自动装箱成Integer类
int r = max(4, 2);
}
static <T> T min(T[] a) {
if (a == null || a.length == 0)
return null;
T smallest = a[0];
for (int i = 1; i < a.length; i++)
if (smallest.compareTo(a[i]) > 0)// ERROR,因为编译器不知道T声明的变量是什么类型
smallest = a[i];
return smallest;
}
如果没有对类型进行限定,它默认只有Object能力,变量smallest类型为T,编译器不知道他是否是实现了Comparable接口(是否是Comparable类型),所以可以通过将T限定为实现了Comparable接口的类,就可以解决这一问题。
对 类型参数进行限定,让它能够默认拥有一些类的"能力"。
static <T extends Comparable> T min(T[] a){...}
类型变量限定格式:<T extends BoundingType>
【1】T应该是绑定类型的子类型(subtype)。T和绑定类型可以是类,也可以是接口。
【2】一个类型变量或通配符可以有多个限定,限定类型用”&“分隔,类型变量用逗号分隔。例如:
<T extends Comparable & Serializable>;
<T,E extends Comparable & Serializable>;
【3】在 Java 的继承中, 可以拥有多个接口超类型, 但限定中至多有一个类。 如果用 一个类作为限定, 它必须是限定列表中的第一个。
<T extends ArrayList & LinkedList>;//ERROR,限定中至多有一个类
<T extends Comparable & LinkedList>;//ERROR,必须是限定列表中的第一个
<T extends ArrayList & Comparable>;//CORRECT
【4】类型限定不仅可以在泛型方法上,也可以在泛型类上,类型限定必须与泛型的声明在一起。
public <T extends Number> T compare(Generic<T extends Comparable> a) {..}//ERROR
public <T extends Number> T compare(Generic<T> a) {..}//ERROR
类型擦除
虚拟机没有泛型类型对象—所有对象都属于普通类。
类型擦除:无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型,如果没有给定限定就用Object替换。
例如,Holder的原始类型如下:
public class Holder {
private Object holder;
public Holder(Object holder) {
this.holder = holder;
}
public Object getHolder() {
return holder;
}
}
// 类型擦除也会出现在泛型方法中。
public static <T extends Comparable> T min(T[] a);
// 擦除类型之后,只剩下
public static Comparable min(Comparable[] a);
Java泛型 转换的事实:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都用它们的限定类型替换。
- 桥方法被合成来保持多态。
- 为保持类型安全性,必要时插入强制类型转换。
泛型的约束与局限
(1)不能用基本类型实例化类型参数
其原因是当类型擦除后,Object类型的域不能存储基本类型的值。
(2)所有的类型查询只产生原始类型
List<Number> numbers = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(numbers.getClass() == integers.getClass());//true
if (integers instanceof List){//true
System.out.println(true);
}
/*if (integers instanceof List<Integer>){//compile error
System.out.println(true);
}*/
(3)不能创建一个确切的泛型类型的数组
//List<Integer>[] lists = new ArrayList<Integer>[10];//ERROR
//可以声明原始类型创建数组,但是会得到一个警告
//可以通过@SuppressWarnings("unchecked")去除
List<Integer>[] list = new ArrayList[10];
//使用通配符创建泛型数组也是可以的,但是需要强制转换
List<Integer>[] listWildcard = (List<Integer>[])new ArrayList<?>[1];
下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK
(4)不能实例化类型变量
static <T> Object init(Class<T> cls) throws Exception {
//T a = new T(); // ERROR
// 注意不存在T.class.newInstance();
T t = cls.newInstance();//Class本身也是一个泛型类
return t;
}
(5)不能构造泛型数组
(6)泛型类的静态上下文中类型变量无效
(7)不能抛出或捕获泛型类的实例
不变 协变 逆变
首先看一段代码
Number[] n = new Integer[10];
ArrayList<Number> list = new ArrayList<Integer>(); // ERROR type mismatch
为什么Number
类型的数组可以由Integer
实例化,而ArrayList<Number>
却不能被ArrayList<Integer>
实例化呢?这就涉及到将要介绍的主题。
不变协变逆变的定义:
逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果AA、BB表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类);
- f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
- f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
- f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
容易证明数组是协变的即Number[] n = new Integer[10];
泛型是不变的,即使它的类型参数存在继承关系,但是整个泛型之间没有继承关系 : ArrayList<Number> list = new ArrayList<Integer>();
泛型类型的继承规则
1. 泛型参数是继承关系的泛型类之间是没有任何继承关系的。
在java中,Number是所有数值类型的父类,任何基本类型的包装类型都继承于它。
ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Number> numbers = new ArrayList<>();
numbers = integers; // ERROR,就上上面刚刚提到的泛型是不变的
2. 泛型类可以扩展或实现其他的泛型类。这一点和普通类没有声明区别。比如ArrayList<T>
类实现List<T>
接口。这意味着,一个ArrayList<Integer>
可以转换为一个List<Integer>
(父类指向了子类的引用),但是,一个ArrayList<Integer>
不是一个ArrayList<Number>
或List<Number>
(泛型参数继承与类无关)。
通配符
Java中引入通配符?
来实现逆变和协变,通过通配符之前的操作也能赋值成功,如下所示:
List<? extends Number> number = new ArrayList<Integer>();// CORRECT
List<? super Number> list = new ArrayList<Object>();// CORRECT 逆变的代表
使用通配符的子类型关系
ArrayList<Integer>
是ArrayList<? extends Number>
的一个子类型。
通配符的分类
-
? extends T
(上边界通配符 upper bounded wildcard)"?"是继承自T的任意子类型,表示一种约束关系。即泛型类型的范围不能超过T。
可以取元素,不能添加元素。
-
?
(无限定通配符) -
? super T
(下边界通配符 lower bounded wildcard)可以取元素,但是取出的元素是Object,可以添加元素,添加的元素,必须是T类或者其子类
记忆:上不存,下不取
示例:类型上边界通配符为什么只能添加?
ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Object> objects = new ArrayList<>();
ArrayList<Number> nums = new ArrayList<>();
/*
* 1. 类型上边界通配符
* 对变量numbers赋值,引用的集合类型参数只能是Number或者其子类。
*/
ArrayList<? extends Number> numbers;
numbers = nums;
numbers = integers;
//引用的类对象类型超过了泛型类型的上边界
//numbers = objects; ERROR
integers.add(1);// 正常添加元素
//numbers.add(1); ERROR numbers只能读取,不能添加[1]
//但是可以添加null
numbers.add(null);
Number number = numbers.get(0);//可以读取元素
为什么只能读取,不能添加?[1]
? extends T 表示类型的上界,类型参数是T的子类,那么可以肯定的说,get方法返回的一定是个T(不管是T或者T的子类)编译器是可以确定知道的。但是add方法只知道传入的是个T,至于具体是T的那个子类,不知道。
转化到本例来说就是:
理解方式一:
? extends Number指定类型参数必须是Number的子类,get方法返回的一定是Number
编译器确定,但是对于ArrayList的add方法为来说add(E e)->add(? extends Number e);
调用add函数不能够确定传入add的是Number的哪个子类型。编译器不确定。
理解方式二:
List是线性表吧【线性结构的存储】,线性表是n个具有相同类型的数据元素的有限序列。假若number能够add,因为? extends Number泛型通配符,可以添加Number的任何子类型,那么numbers在get时,极有可能引发ClassCastException,比如numbers引用了<Integer>
,但是在索引0处却add了float类型的数据,取出的时候如果numbers.get(0).intValue();就会抛出异常。并且这也违背了线性表中特性,只能存放单一类型的元素。
/*
* 2. 类型下边界通配符
* numbersSuper所能引用的变量必须是Number或者其父类
*/
ArrayList<? super Number> numbersSuper;
numbersSuper = objects;// 逆变
numbersSuper = nums;
//限定了通配符的下界,类型最低是Number,Integer达不到
//下界,类型不匹配
//numbersSuper = integers; ERROR
numbersSuper.add(1); [2]
numbersSuper.add(2.0f); [3]
//numbersSuper.add(new Object()) ERROR
Object object = numbersSuper.get(0);
System.out.println(object);
? super T 表示类型的下界,类型参数是T的超类(包括T本身), 那么可以肯定的说,get方法返回的一定是个T的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以get方法返回Object。 编译器是可以确定知道的。对于add方法来说,编译器不知道它需要的确切类型,但是T和T的子类可以安全的转型为T。
为什么? super Number
就可以add了呢?[2、3]
首先要明确的一点是,add的时候只能是Number[T]及其它的子类,不要和numberSuper只能引用Number的父类所混淆了。正因为numberSuper引用了<Object>
,那么numberSuper在add的时候类型确定,都可以看作是Object类型,即Number的子类Integer和Float也是其Object的子类。但是相对于? extends T
就add就不能调用,numbers如果限定了<Integer>
,还是那句话,假若能放的话,number存放float类型的数据,取值时极易引发类型转化异常。
泛型方法和类型通配符
类型通配符所能解决的泛型方法一定也能解决
# 类型通配符
`public void func(List<? extends E> list);`
# 泛型方法
`public <T extends E> void func(List<T> list);`
* 上面两种方法可以达到同样的效果,两者的主要区别还是
i. 泛型对象是只读的,不可修改,因为?类型是不确定的,可以代表范围内任意类型;
ii. 而泛型方法中的泛型参数对象是可修改的,因为类型参数T是确定的(在调用方法时确定),因为T可以用范围内任意类型指定;
泛型方法和类型通配符(上界和下界)我们应该如何选择?
(1)泛型方法和通配符之间
修改最好使用泛型方法,在多个参数、返回值之间存在类型依赖关系就应该使用泛型方法,否则就应该使用通配符。
(2)什么时候用extends什么时候用super
PECS: producer-extends, consumer-super.
—《Effective Java》
- 要从泛型类取数据时,用extends
- 要往泛型类写数据时,用super