目录
概述
范型的使用
类型参数
类型通配符
泛型方法
泛型类
限定类型参数上限
上界通配符(Upper Bounds Wildcards),用来限定泛型的上界。
下界通配符(Lower Bounds Wildcards),用来限定泛型的下界。
范型的特点
范型是类型擦除的
不能创建一个范型类型实例
不能初始化范型数组
基本类型不能做类型参数
static 的语境不能引用类型变量
所谓范型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。
范型可以减少强制类型的转换,可以规范集合的元素类型,还可以提高代码的安全性和可读性,正是因为有这些优点,自从 Java 引入范型后,项目的编码规则上便多了一条:优先使用范型。
类型参数就是我们在定义泛型类或者方法是动态指定的参数。
类型参数的命名规则
类型参数名称命名为单个大写字母,比如 Collection
。
但是这个命名规则我们一般会遵循一般的约定,以便可以在使用普通类或接口名称时能够容易地区分类型参数,增加代码的可读性。
以下是常用的类型参数名称列表:
类型通配符一般是使用 ?
代替具体的类型参数,表示未知类型。例如 List>
在逻辑上是 List
,List
等所有 List<具体类型实参>
的父类。
类型通配符的形式有 >
、 extends Class>
和 super Class>
基本使用
为了表示各种范型 List 的父类,我们需要使用类型通配符,类型的通配符就是一个问号 ?,将它作为类型实参传给 List集合:List>
,就标示未知类型元素的 List,它的元素类型可以匹配任何类型。
public void testGeneric() {
List name = new ArrayList();
List age = new ArrayList();
List shape = new ArrayList();
name.add("icon");
age.add(18);
shape.add(new Shape());
getData(name);
getData(age);
getData(shape);
}
public static void getData(List> data) {
Log.e("Test","data :" + data.get(0));
}
public static class Shape {
@Override
public String toString() {
return "Shape";
}
}
编译没问题,运行结果:
E/Test: data :icon
E/Test: data :18
E/Test: data :Shape
因为 getData()
方法的参数是 List
类型的,所以 name,age,shape 都可以作为这个方法的实参,这就是通配符的作用。
上面程序中使用的 List>
,其实这种写法可以适用于任何支持范型声明的接口和类,比如 Set>
、Map,?>
等。
设定类型通配符的上限
假设有下面的使用场景,我们不想使 List>
使任何范型 List 的父类,只想表示它是某一类范型List的父类,这时候我们就要限定通配符的上限了。 extends Class>
表示该通配符所代表的类型是 Class 类型本身或者它的子类。或者 extends T>
把上面的 getData
方法修改一下:
public void testGeneric() {
List name = new ArrayList();
List age = new ArrayList();
List shape = new ArrayList();
List circle = new ArrayList();
name.add("icon");
age.add(18);
shape.add(new Shape());
getData(name); // 编译报错
getData(age); // 编译报错
getData(shape); // 编译通过
getData(circle);// 编译通过
}
public static void getData(List extends Shape> data) {
Log.e("Test","data :" + data.get(0));
}
public static class Shape {
@Override
public String toString() {
return "Shape";
}
}
public static class Circle extends Shape {
@Override
public String toString() {
return "Circle";
}
}
前面两个用法就会报错:
Error:(74, 17) 错误: 不兼容的类型: List无法转换为List extends Shape>
Error:(75, 17) 错误: 不兼容的类型: List无法转换为List extends Shape>
设定类型通配符的下限
public static void getData(List super Shape> data) {
Log.e("Test","data :" + data.get(0));
}
getData(name); // 编译报错
getData(age); // 编译报错
getData(shape); // 编译通过
getData(circle);// 编译报错
范型方法就是在声明方法时定义一个或多个类型形参,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
范型方法的类型作用域是整个方法。
范型方法的用法格式如下:
修饰符 返回值类型 方法名 (形参列表) {
}
下面是定义泛型方法的规则:
)。先来看一个范型参数来声明方法参数的例子:
public String getData(T t) {
return String.valueOf(t);
}
再来看一个范型参数来声明返回值类型的例子:
private Map mDatas = new ArrayMap<>();
public T getData(String name) {
return (T) mDatas.get(name);
}
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。
一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
public class Paradigm {
private T t;
public Paradigm() {
}
public Paradigm(T t) {
this.t = t;
}
public T getT() {
return t;
}
public Paradigm setT(T t) {
this.t = t;
return this;
}
}
Java 范型不仅允许在使用通配符形参时设定上限,而且也可以在定义类型参数时设定上限,用于表示传给该类型形参的实际类型要么是该类型上限,要么使该类型的子类。
这种做法可以用在范型方法和范型类中。
用法:
public String getData(T t) {
return String.valueOf(t);
}
public static class Shape {
@Override
public String toString() {
return "Shape";
}
}
形如 表示 T 是继承了 Class1 的类以及实现了 Interface1,后面的接口可以有多个,因为 Java 是单继承,因此父类只能有1个。类要写在接口的前面。
Paradigm extends Number> numberParadigm = new Paradigm(Integer.valueOf(123));
Number t2 = numberParadigm.getT();
//不能存入任何元素
numberParadigm.setT(Integer.valueOf(123)); //Error 编译错误
numberParadigm.setT(Float.valueOf(0.5F)); //Error 编译错误
numberParadigm.setT(Double.valueOf(0.5F)); //Error 编译错误
extends Number> 这里的问号「?」即为泛型持有的类型,它的范围必须是Number类的子类型或者本身,但不可以是Number的超类或者其它不相关的类型。
extends Number>会使Paradigm的setT()方法失效。但getT()方法还有效。
原因是编译器只知道容器内是Nubber或者它的派生类,但具体是什么类型不知道。可能是Nubber?可能是Integer?也可能是Float,Double?编译器在看到后面用Paradigm
Paradigm super Number> numberParadigm = new Paradigm(Integer.valueOf(123));
//存入元素正常
numberParadigm.setT(Integer.valueOf(123));
numberParadigm.setT(Float.valueOf(0.5F));
numberParadigm.setT(Double.valueOf(0.5F));
//读取出来的东西只能存放在Object类里
Object o1 = numberParadigm.getT();
Integer o2 = numberParadigm.getT(); //Error
Float o3 = numberParadigm.getT(); //Error
因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是Nubber的基类,那往里存粒度比Nubber小的都可以。但往外读取元素就费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。
自定义泛型T和类型通配符?的区别
首先他们都表示不确定的类型。
自定义泛型 T 可以在方法体内进行各种操作,比如:
T t = it.next();
System.out.println(t);
也可以方法返回值:
public static T getData(List data){
Log.e("Test","data :" + data.get(0));
return data.get(0);
}
也就是说,当你仅仅想表达一种不确定类型时可以用类型通配符?,但你如果相对类型参数进行操作或者是想表达两个类型参数之间或者参数与返回值之间关系时,这时就要用自定义泛型 T。
Java 的范型在编译器有效,在运行期被删除,也就是说所有的反省参数类型在编译期后都会被清除掉。
下面看一段代码:
public void listMethod(List strings) {
}
public void listMethod(List strings) {
}
这段代码是否能编译呢?
事实上,这段代码时无法编译的,编译时报错信息如下:
`listMethod(List)` clashes with `listMethod(List)`; both methods have same erasure
此错误信息是说 listMethod(List
方法在编译时擦除类型后的方法是 listMethod(List
,它与另一个方法相冲突。这就是 Java 范型擦除引起的问题:在编译后所有的范型类型都会做相应的转化:
转化规则如下:
List
、List
、List
擦除后的类型为List
List[]
擦除后的类型为List[]
List extends E>、List super E>
擦除后的类型为 List
List
擦除后为 List
范型的擦除还表现在当把一个具有范型信息的对象赋给另一个没有范型信息的变量时,所有再尖括号之间的类型信息都将被扔掉。
比如:一个 List
类型被转换为 List
,则该 List
对集合元素的类型检查变成了类型变量的上限(即 Object)。
下面用一个例子来示范这种擦除:
public void testGenericErasure() {
// 传入 Integer 作为类型形参的值
Paradigm paradigm = new Paradigm<>(8);
// paradigm 的 get 方法返回 Integer 对象
Integer size = paradigm.get();
// 把 paradigm 对象赋值给b变量,此时会丢失<>里的类型信息
Paradigm b = paradigm;
//这个代码会引起编译错误
// Integer size1 = b.get();
// b只知道size的类型是Number,但具体是Number的哪个子类就不清楚了。
//下面的用法是正确的
Number size2 = b.get();
}
public class Paradigm {
private T size;
public Paradigm(T size) {
this.size = size;
}
public void add(T size) {
this.size = size;
}
public T get() {
return size;
}
}
类型转换:
public void testGenericConvert() {
List li = new ArrayList<>();
li.add(8);
// 类型擦除
List list = li;
// 类型转换
List ls = list;
// 下面的代码会引起运行时异常
// Caused by: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Log.e("Test",ls.get(0));
}
明白了这些,对下面的代码就容易理解了:
List stringList = new ArrayList<>();
List integerList = new ArrayList<>();
Log.e("Test",""+stringList.getClass().equals(integerList.getClass()));
返回结果为 true。 List
和 List
擦除后的类型都是 List,没有任何区别。
之所以设计成可擦除的,有下面两个原因:
如果 T 是一个类型变量,那么下面的语句是非法的:
T obj = new T();
T 由它的限界代替,这可能是 Object,或者是抽象类,因此对 new T()
的调用没有意义。
数组元素的类型不能包含类型变量或者类型形参,除非是无上限的类型通配符。但是可以声明元素类型包含类型变量或类型形参的数据。
也就是说,下面的代码是OK的:
List[] stringList ;
public class Paradigm {
private List list = new ArrayList();
}
下面的代码,等号后面的代码是非法的:
List[] stringList = new ArrayList[10];
因此,List
是非法的,我们必须使用包装类。
在一个范型类中,static 方法和 static 域均不可以引用类的类型变量,因为类型擦除后类型变量就不存在了。而且,static 域在该类的诸范型实例之间是共享的。因此,static 的语境不能引用类型变量。