泛型(generics
)是JDK1.5引入的一个新特性,泛型的本质是参数化类型,也就是说,可以把数据类型指定为一个参数,这个参数类型可以用在类、接口和方法的创建中。
泛型的引入
任何技术的存在都有其必要性,泛型的机制同样如此。语言开发者提供的各种机制,我们需要思考其存在的必要性。这就回到我们java语言的定位问题上了。我们java语言是静态语言。这意味着可以在编译器就可以知道开发者是不是哪里写错代码了,不需要再运行时进行检验,提高效率。JDK1.5之前呢,jdk也已经做得不错了,都是尽可能在编译的时候告诉开发者哪里有问题。但是往往还不够完善。
例如,很早很早以前,我们在使用容器帮我们干活的时候是这样的:
List list = new ArrayList();
list.add("a");// 正常
list.add(1); //正常
for(Object o : list){
// o你默认是个String的话,就要强转,然后做处理
// o你默认是任何类型的话,你每次都要instanceOf一下看看具体的数据类型是什么,然后根据具体的数据类型进行特殊化处理
}
这样的话,写出来的代码也太丑了,到处都是判断,到处都是强转。有了泛型之后我们就可以把代码写成这样:
List<String> list = new ArrayList<>();
list.add("a");// 正常add
// list.add(1); // 编译的时候就报错
for(String s : list){
System.out.println(s)
}
这样就顺眼很多了。循环内完全不需要去管类型信息,拿到的保证都是String。
那么,List<String>
和List
是不是同一个类呢?使用下面的代码进行测试:
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
List list3 = new ArrayList();
if(list1.getClass() == list2.getClass()){
System.out.println("list1==list2")
}
if(list2.getClass() == list3.getClass()){
System.out.println("list2==list3")
}
if(list1.getClass() == list3.getClass()){
System.out.println("list1==list3")
}
通过打印的结果可以发现,泛型类和原生类是引用的同一个Class对象,意味着他们背地里是同一个代码。因此我们可以认定,泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。这就是所谓的泛型擦除的概念,泛型信息只在编译器生效,但是在运行期这个信息就被丢弃了。
同样也是因为类型擦除的原因,例如只有List<Double>
没有List<double>
被擦除后含有 Object 类型的域, 而 Object 不能存储 double值。因此泛型尖括号中基础类型是不予支持的。
泛型的使用
泛型的目的本质上是针对方法进行限制出入参的参数格式。
回顾方法的种类,方法有静态方法,实例方法。这种限制我们称为泛型方法
那如果控制多个方法怎么办?看来这种限制只能挂在类上。这种控制方式,我们称为泛型类,那么连带的,类里面的成员是不是也应该有类型控制?没错,的确是这样。
那如果一口气控制多个类怎么办?什么东西可以在类与类之间共享?父类,或者接口,父类要么是抽象类,要么是普通类,前辈们思考一番,将父类划分到泛型类。但是还是单独把接口拎出来,给予一个名字:泛型接口。
综上,泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法(静态方法&实例方法)。
泛型类
泛型类型用于类的定义中,被称为泛型类。通过类上的泛型信息来控制这个类下的字段和方法的泛型。很常见的就是各种容器。我们尝试着制造一个最普通的泛型类:
public class Generic<T> {
/**
* key这个成员变量的类型为T,T的类型由外部指定
*/
private T key;
/**
* 泛型构造方法形参key的类型也为T,T的类型由外部指定
* @param key
*/
public Generic(T key) {
this.key = key;
}
/**
* 泛型方法getKey()的返回值类型为T,T的类型由外部指定
* @return
*/
public T getKey(){
return key;
}
}
说明:
- 此处
T
可以随便写为任意标识,常见的如T
、E
、K
、V
等形式的参数常用于表示泛型;- 在实例化泛型类时,必须指定
T
的具体类型。- 泛型的类型参数只能是类类型,不能是简单类型;
- 不能使用像 new T(…),newT[…] 或 T.class 这样的表达式。可以使用
Class<T>
的方式获得T的类类型,使用反射制造实例。
当然了,针对Generic类我们只给它定义了一个T,一个泛型类也可以具备多个泛型参数,只要在类上标明即可:
public class Generic2<T,U> {
/**
* key这个成员变量的类型为T,T的类型由外部指定
*/
private T key;
/**
* key2这个成员变量的类型为U,U的类型由外部指定
*/
private U key2;
/**
* 泛型构造方法形参key的类型也为T,T的类型由外部指定
* @param key
*/
public Generic(T key, U key2) {
this.key = key;
this.key2 = key2;
}
/**
* 泛型方法getKey()的返回值类型为T,T的类型由外部指定
* @return
*/
public T getKey(){
return key;
}
public static void main(String[] args){
Generic2<String,Integer> g = new Generic2("吃饭吃饭",10);
System.out.println(g.getKey())
}
}
同样的,虽然Generic类是一个泛型类,但是对于开发者而言,也可以不去使用泛型话参数的特性,如此一来我们就有两种代码的写法,一种是执行泛型类型,一种是不赋予泛型化参数。
- 指定泛型类型
@Test
public void genericDemoWithType() {
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型,比如这里Integer改为int编译将不通过
Generic<Integer> integerGeneric = new Generic<Integer>(123456);
log.info("integerGeneric key is:{}", integerGeneric.getKey());
//传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic<String> stringGeneric = new Generic<String>("吃饭吃饭吃饭");
log.info("stringGeneric key is:{}", stringGeneric.getKey());
}
- 不指定泛型类型
如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
@Test
public void genericDemoWithOutType() {
Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);
log.info("generic key is:{}",generic.getKey());
log.info("generic1 key is:{}",generic1.getKey());
log.info("generic2 key is:{}",generic2.getKey());
log.info("generic3 key is:{}",generic3.getKey());
}
如果我们的一个泛型类定义出来是为了制造子类怎么办呢?
// 定义出来的A,在定义的时候还是继续使用泛型,这样是不被允许的
public class A extends Container<K, V> {}
// 派生子类的时候,必须使用明确化的泛型信息
public class A extends Container<Integer, String> {}
// 你也可以不指定泛型信息,那么意味着,获得到的相关的数据类型会成为Object
public class A extends Container {}
如果泛型类派生出来的类也是一个泛型类怎么办?——当然是定义出来的子类也需要使用泛型类的语法标记啦
public class A<K, V> extends Container<K, V> {}
泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,例如:
public interface Generator<T> {
public T next();
}
- 当实现泛型接口的类,未传入泛型实参时
未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。
public class FruitGenerator<T> implements Generator<T>{
public T next() {
return null;
}
}
- 当实现泛型接口的类,传入泛型实参
在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型。
public class VegetablesGenerator implements Generator<String>{
private String[] vegetables = new String[]{"Potato", "Tomato"};
public String next() {
Random rand = new Random();
return vegetables[rand.nextInt(2)];
}
}
使用泛型接口时,需要避免重复实现同一个接口,不然可能会出现泛型定义冲突的问题。
泛型方法
泛型类的定义非常简单,在类上定义泛型,这个类下的所有的和泛型相关的方法都会被整体影响。但是,万一某个类的某个方法的确需要传入不同的参数的时候,就不能被当前类的泛型信息限制。意味着方法自己也需要具备泛型话的能力,并且可以覆盖外部泛型化。什么时候能体现方法的独立自主化的泛型机制呢?也就只能在调用方法的时候指明泛型的具体类型了。
例如,我们在类内制造一个方法:
public class GenericMethod{
public <T> T showKeyName(T aa){
return aa;
}
public static void main(Stringp[] args){
GenericMethod g = new GenericMethod();
Integer r1 = g.showKeyName(1);
String r2 = g.showKeyName("2");
}
}
你会发现这个方法上多了一个标记<T>
,这就表明了这个方法是一个泛型方法,在方法接收到参数之后,自然而然就知道T是什么类型。如果移除<T>
这个方法也就变成了泛型类下面的泛型方法,T所代表的类型在定义出泛型类的时候就已经确定下来了。
泛型方法可以出现杂任何地方和任何场景中使用,我们故意让泛型方法和泛型类呆一块儿:
public class GenericDemo<T>{
private T t;
public GenericDemo(T t){
this.t = t;
}
public T getT(){
return t;
}
// 这里的T 和当前的泛型类的T所代表的的含义是不一样的,不能混淆地看待他们
public <T> T showKeyName(T aa){
return aa;
}
public static void main(String[] args){
GenericDemo<Long> g = new GenericDemo<>(12L);
Integer r1 = g.showKeyName(1); // 值为1
String r2 = g.showKeyName("2");// 值为“2”
Long r3 = g.getT();// 值为12L
}
}
虽然泛型方法和泛型类所使用的代号都是T
,但是两者并不冲突。
泛型和可变参数碰撞呢?
public <T> void printMsg(T...args){
for(T t : args){
System.out.println(t);
}
}
可以运行,很强大。
如果我们的泛型方法是一个构造函数呢?
public class Person {
public <T> Person(T t) {
System.out.println(t);
}
}
这样会让我们的新建实例的语法看上去和一般的语法不一样:
public static void main(String[] args) {
new Person(22);// 隐式
new <String> Person("hello");//显示
}
如果构造器是泛型方法,并且当前类是个泛型类呢?
public class Person<E> {
public <T> Person(T t) {
System.out.println(t);
}
}
对应的,我们事实上在初始化实例的时候就要同时指定两个泛型信息了:
public static void main(String[] args) {
Person<String> person = new Person("sss");
}
静态方法的泛型化
类和实例是一对多的关系,因此,归属于类定义的static方法是不能去访问实例的信息的。泛型类的泛型信息是在制造实例的时候赋予的,因此泛型类的泛型信息本质上是实例信息。所以静态方法是没有办法知道当前的泛型类的泛型信息,因此需要完全脱离类的掌控,在方法上定义泛型信息,就和泛型方法一样。
例如,如果我们将一个静态方法的入参设置为泛型入参,编译器直接报错。
public static void show(T t){}
我们针对静态方法的泛型话,正确写法应该长这样:
public static <T> void show(T t){}
泛型数组
前面我们可以把信息挂在类上(连带着实例方法,实例字段都可以泛型化),方法上(包含静态),接口上。泛型只能是引用类型,但是数组也是个引用,那么数组和泛型可以碰撞出什么火花?
不假思索的写出这样的一段代码:
public class GenericArray<T>{
private T[] t = new T[3];
}
当我真正去运行的时候,出了问题:
不能理解,很生气。改!
public class GenericArray<T>{
private T[] t = new Object[3];
}
还是失败,改!
public class GenericArray<T>{
private T[] t;
}
这下不报错了,看来问题出在如何制造一个泛型数组上,泛型数组定义是没有问题的。接下来就是怼着这个问题不断找资料,终于找到了一种方式,可以帮助我们去制造泛型数组;
public class GenericArray<T>{
private T[] t;
public void init(Class<T> clazz, int length){
t = (T[]) Array.newInstance(clazz, length);
}
}
Array是JDK自带的类,类路径是import java.lang.reflect.Array
,这个方法是个神奇的方法,最底层使用的是native,那意味着我们用普通的方式已经制造不出来泛型数组了,只能借助JDK下的native方法才能制造出我们想要的东西。
代码有点黄,不喜欢,我们需要加上@SuppressWarnings({ "unchecked", "hiding" }),
止住泛黄。
开发中我们遇到的困难可能是集合类型的继承问题。设Employee IS-A Person。那么,这是不是就代表数组Employee [] IS-A Person [] 呢?简单说就是,一个方法接收Person [] 作为参数,那么我们能不能把Employee [] 作为参数传递进去呢?
乍一看,可能我们第一感觉那肯定能传递进去呀,然而,这个问题却不是想的那样。下面我用一个例子来表述。
//假设Student IS-A Persion ,Employee IS-A Persion*
//1.Person数组接收 5 个 空的Employee数组*
Persion[] arr = new Employee[5];
arr[0] = new Student();
两句话都编译通过,而arr[0] 实际上是引用一个Employee,可是Student IS-Not-A Employee。 这样就产生了错误,运行的时候,会抛出一个ClassCastException的异常,因为类型转换不过去,这里在开发过程中需要特别注意。
泛型通配符
我们上面聊了泛型类,泛型方法,泛型接口,按道理已经帮我们解决了不少问题了,但是真实世界永远没有那么简单。
无界通配符?
回到刚开始的例子,我们为了解决在List里面放了很多不同类型的数据,然后导致各种编码复杂化的问题,引入了泛型。当我们使用泛型进行实例化的时候,可以帮助我们只去存储特定类型的数据。
问题来了,如果我的确不在意往List里面的数据是否类型不一致。
简单,我不用泛型化不就好了?
仔细思考,“我的这个List什么都能放” 和 "我的这个List只能放Object"的含义上是有区别的,只不过最后表现出来的样子看上去一样。那么我们就要制造出一种语法,表达,这个类,或者说这个容器,什么都能放,不管你是什么。通配符?
就此诞生。
// 表达我什么都能放
List<?> list = new ArrayList<>();
list.add("cc")
list.add(1);
// 表达我只能放Object
List list = new ArrayList();
list.add("cc")
list.add(1);
这个符号不是用作定义泛型信息阶段的,而是使用阶段的。因此不可能出现类似这种代码:
? car = operate();
但是?是个很蛋疼的东西:
List<?>这个写法非常坑,因为这时候通配符会捕获具体的String类型,但编译器不叫它String,而是起个临时的代号,比如”CAP#1“。所以以后再也不能往list里存任何元素,包括String。唯一能存的就是null。
List<?> list = new ArrayList<String>();
list.add("hello");
list.add(111);
// argument mismatch; int cannot converted to CAP#1
另外如果拿List<?>做参数,也会有奇妙的事情发生。还是刚才Box的例子,有get()和set()两个方法,一个存,一个取。
仔细思考,2.5里面数组的类型直接是T,那如果数组的类型不是直接的T会怎么样?
List<String>[] ls = new ArrayList<String>[10];
这样不行,稍加修改:
List<?>[] ls = new ArrayList<?>[10]; // 这样可以过去
List<?>[] ls = new ArrayList[10]; // 这样也可以
看来通配符可以让我们跳过编译检查,这里的本质都是因为泛型信息擦除的问题。第一种如果可以通过,那么意味着这样的代码是检查不出来的:
List<String>[] lsa = new List<String>[10];
Object o = lsa;
Object[] oa=(Object[]) o;
List<Integer>[] lsa = new ArrayList<Integer>[10];
li.add(new Integer(3));
oa[1]=li;
String s = lsa[1].get(0);// 这样只有在运行时才能报错
而使用通配符的方式,会导致我们在取数据的时候,必须认为做一步强转。
3.3 上界通配符<? extends E>
假设这个时候我们制造了这样的代码:
List<Number> numberList = new ArrayList<Integer>();
逻辑上有问题吗?Integer继承自Number,我一个装Integer的容器赋给装数字的容器,有什么问题?
事实上,编译器是报错的,它认为虽然容器存放的东西有继承关系,但是容器没有继承关系。
那么,怎么才可以让这两个容器之间也产生关系呢?这个招就是上界通配符
我们把代码改成这样:
List<? extends Number> numberList = new ArrayList<Integer>();
第一个容器表示,自己可以存放任何Number的子类,那么就顺畅了,右边的容器就是方Integer的,是number的子类,自然而然可以赋值给左边,如此一来,两个容器就产生了一定意义上的关联。边界让Java不同泛型之间的转换更容易了,但是也导致了我们的容器的功能受损,例如,上面的numberList这个容器就没有办法往里面塞东西。
但是呢,可以从容器里面拿东西,但是只能用Number去接收,不然会报错。
问题是,为什么呢?我们看到了这个现象,就要考虑这个现象的本质原因是什么。
左边的容器只知道是Number或者是其派生类,可以是Integer,可以是Long,但具体是什么类型不知道。编译器在看到后面用List<Integer>
赋值之后,左边的容器没有标记上Integer,而是标记上一个占位符#CAP1
来表示捕获Number类或者它的子类。然后呢,如果我真的想往里面塞值,还是不能知道是不是和Integer匹配的,那么就干脆都不给存。
3.4 下界通配符 < ? super E>
下界通配符和上界通配符刚好相反,假定我的代码是这样的:
List<? super Integer> numberList = new ArrayList<Integer>();
put数据不会有问题,get数据的能力又被腰斩了,只能用Object去接。