泛型在Java中有着很重要的地位,在面向对象编程及其各种设计模式中有着非常广泛的应用。
什么是泛型?为什么要使用泛型?
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时的形参,然后调用此方法传入实参。
那么参数化类型怎么理解呢?
“参数化类型”顾名思义,就是将原来的具体类型,进行参数化。类似于方法中的变量参数,此时类型也定义成参数类型(可以称之为类型形参)。
泛型的本质就是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型,来控制形参具体限制的类型),也就是说在泛型的使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口、方法中使用,分别被称为泛型类、泛型接口、泛型方法。
List arrayList = new ArrayList();
arrayList.add("aaa");
arrayList.add(123);
for(int i=0, i<arrayList.size(), i++){
String item = (String)arrayList.get(i);
System.out.println("泛型测试:item = " + item);
}
毫无疑问,程序的运行结果会报错结束:
// 类型转换错误
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
ArrayList可以存放任意类型的元素,栗子中添加一个String类型,添加了一个Integer类型,在使用集合的过程中,都已Sring类型的方式进行了使用,因此程序崩溃了。为了解决这样的问题(在编译阶段进行处理)泛型就应运而生了。我们将第一行声明初始化list的代码修改一下,编译器就会在编译阶段就能帮我们发现类似这样的问题。
List<String> arrayList = new ArrayList();
arrayList.add("aaa");
arrayList.add(123); // 在编译阶段程序就会报错,不允许在String类型的集合中添加Integer类型的元素
泛型只是在编译阶段有效。具体看一下代码:
List<String> stringList = new ArrayList();
List<Integer> integerList = new ArrayList();
Class classStringList = stringList.getClass();
Class classIntegerList = integerList.getClass();
if(classStringList.equals(classIntegerList)){
System.out.println("泛型测试:类型相同");
}
输出结果:泛型测试: 类型相同
通过上边的例子可以证明,程序在编译之后会自动采取去泛型化的处理。也就是说Java的泛型,只是在编译阶段有效。在编译过程中,正确检验泛型结果后,会自动的将泛型相关信息给擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。泛型信息是不会进入到运行阶段的。
对此总结一句话就是,泛型类型在逻辑上可以看作为不通的类型,但是实际上都是相同的基本类型。
泛型有三种使用方式:泛型类、泛型接口、泛型方法
泛型类型用于类的定义中,被称之为泛型类。通过泛型可以完成对一组类的操作,对外开放相同的接口。最典型的就是各种容器类List、Map、Set
泛型类最基本的写法如下:
class 类名<泛型标识符:可以随便写任意标识符号,标识指定的泛型的类型>{
private 泛型标识 成员变量名var;
public 泛型标识 方法名(){
}
.....等
}
一个普通的泛型类:
/**
* 自定义泛型类
*/
// 此处的T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于标识泛型
// 在实例化泛型类时,必须制定T的具体类型
public class Genericity<T> {
// key这个成员变量的类型T,是由外部进行指定的
private T key;
// 构造方法的形参key的类型也是T,T也是由外边进行指定的
public Genericity(T key){
this.key = key;
}
// 泛型方法getkey()的返回值类型T,T也是由外部进行指定的
public T getKey(){
return key;
}
}
@SpringBootTest
@Slf4j
public class GenericityTest {
@Test
public void test(){
// 泛型的类型参数只能是 类类型(包括自定义类),不能是简单类型
// 构造器传入的实参类型需要与泛型的类型参数类型相同,即为String
Genericity<String> stringGenericity = new Genericity<String>("测试");
// 构造器传入的实参类型需要与泛型的类型参数类型相同,即为Integer
Genericity<Integer> integerGenericity = new Genericity<Integer>(123);
log.info("泛型测试key is " + stringGenericity.getKey());
log.info("泛型测试key is " + integerGenericity.getKey());
}
}
定义泛型类,就一定要传入泛型类型的实参吗?
并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参来做出相应的限制,此时泛型才会起到本应该起到的限制作用。
如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量等的类型为任意类型。
@Test
public void test2(){
Genericity a1 = new Genericity("测试");
Genericity a2 = new Genericity(123);
Genericity a3 = new Genericity(8.324);
Genericity a4 = new Genericity(true);
log.info("泛型测试key is " + a1.getKey());
log.info("泛型测试key is " + a2.getKey());
log.info("泛型测试key is " + a3.getKey());
log.info("泛型测试key is " + a4.getKey());
}
注意:
泛型接口与泛型类的定义与使用基本相同。泛型接口常被使用在各种类的生产器中,可以先看一个例子:
/**
* 自定义泛型接口
*/
public interface Generator<T> {
public T test();
}
当实现泛型接口的类,未传入实参时:
/**
* 实现自定义泛型接口的类1
*
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需要将泛型的声明也加到类中
* 既:public class FruitGenerator1 implements Generator
* 如果不声明泛型,如:public class FruitGenerator1 implements Generator 则编译器会报错
* 如果不传入任何类型则是任意类型,如: public class FruitGenerator1 implements Generator
*/
public class FruitGenerator1<T> implements Generator<T>{
@Override
public T test() {
return null;
}
}
当实现泛型接口的类,传入实参时:
/**
* 实现自定义泛型接口的类2
* 传入泛型参数时:
* 定义一个类实现这个接口,虽然我们只创建了一个泛型接口Generator
* 但是我们可以为T传入多种类型的实参,从而形成多种类型的Generator接口
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都有替换成传入的实参类型
* 既:Generator、public String test(){}中的T都要替换为传入的实参类型
*
*/
public class FruitGenerator2 implements Generator<String>{
@Override
public String test() {
return null;
}
}
我们知道 Integer 是 Number 的一个子类,同时在上边泛型的特性中我们也验证过List和List 实际上是一种基本类型,同理 Generator 和 Generic实际上也应该是同一种基本类型。那么问题来了,在使用Generic作为形参参数时,能否使用Generator的实例作为实参传入呢?在逻辑上类似于Generic和Generator是否可以看成具有父子关系的泛型类型呢?
为了弄清楚这个问题,我们使用Generator这个泛型类举个例子
public void showKeyValue(Genericity<Number> obj){
log.info("泛型测试key is " + obj.getKey());
}
/**
* 测试泛型类是否具有父子继承关系
*/
@Test
public void test3(){
Genericity<Integer> integerGenericity = new Genericity<>(123);
Genericity<Number> numberGenericity = new Genericity<>(456);
// showKeyValue(numberGenericity); 程序正常执行
showKeyValue(integerGenericity); // 编译器报错 Generic cannot be applied to Generic
}
通过提示的信息我们可以看到 Genericity不能看为 Genericity的子类。由此可以看出:同一种泛型可以对应多个版本(因为实参参数类型不确定),不同版本的泛型类实例是不兼容的。
回到上边例子,如何解决类似上边的问题?总不能再定义一个新的方法来进行处理Genericity类型的参数吧,这显然是不正确的。因此我们需要一个在逻辑上可以同时表示Genericity和Genericity等引用类型。因此类型通配符应运而生。
我们将上边的代码改一下:
public void showKeyValue(Genericity<?> obj){
log.info("泛型测试key is " + obj.getKey());
}
/**
* 测试泛型类是否具有父子继承关系
*/
@Test
public void test3(){
Genericity<Integer> integerGenericity = new Genericity<>(123);
Genericity<Number> numberGenericity = new Genericity<>(456);
Genericity<String> dfas = new Genericity<>("dfas");
showKeyValue(numberGenericity); // 正常编译
showKeyValue(integerGenericity); // 正常编译
showKeyValue(dfas); // 正常编译
}
类型通配符一般是使用?代替具体的类型实参,注意:此处’?‘是类型实参,而不是类型形参。直白点的意思就是,此处的’?’ 和Number、Integer、String一样都是一种实际的类型,可以吧?看成所有类型的父类,是一种真是的类型。
可以解决当具体类型不确定的时候,这个通配符就是?;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用?通配符来标识未知类型。
在Java中泛型类的定义非常简单,但是泛型方法就比较复杂了。
尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样我们就非常容易将泛型方法理解错。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。
/**
* 泛型方法的基本介绍
* @param tClass 传入的泛型实参
* @return T 返回值类型为T类型
*
* 说明:
* 1、public与返回值之间的非常重要,可以理解为声明此方法为泛型方法。
* 2、只有声明了的方法才是泛型方法,泛型类中使用了泛型的类成员方法并不是泛型方法。
* 3、表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4、与泛型类的定义一样,此处T可以随便写为任意标识,常见的T、E、K、V等形式的参数常用于标识泛型。
*/
public <T> T genericMethod(Class<T> tClass) throws InstantiationException, IllegalAccessException {
T instance = tClass.newInstance();
return instance;
}
package com.proxys.business.genericity;
import lombok.extern.slf4j.Slf4j;
/**
* 泛型方法的基本使用方法
*/
@Slf4j
public class GenericTest {
// 这个类时个泛型类,在上边已经介绍过了
public class Generic<T>{
// 使用类的泛型的成员变量
private T key;
// 类的构造方法,参数为类的泛型类型
public Generic(T key){
this.key = key;
}
/**
* 想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法
* 这只是泛型类中的一个普通成员方法,只不过他的返回值,是在声明泛型类上已经声明过的泛型
* 所以在这个方法中才可以继续使用 T 这个泛型
*/
public T getKey() {
return key;
}
/**
* 这个方法显然是有问题的,在编译器会给我们提示出错误信息“cannot reslove symbol E”
* 因为在类的声明中并未声明泛型E,所以在使用E作为形参和返回值类型时,编译器无法识别通过
*
*/
// public E setKey(E key){
// this.key = key;
// }
}
/**
* 这才是一个真正的泛型方法
* 首先在public访问修饰符和返回值之间的是不可少的,这表明这是一个泛型方法,并声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置
* 泛型的数量也可以存在任意多个
* 如:public K showKeyName(Generic container){}
*/
public <T> T showKeyName(Generic<T> container){
T key = container.getKey();
return key;
}
/**
* 这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic这个泛型类作为形参而已
*/
public void showKeyValue1(Generic<Number> obj){
log.info("泛型测试key is " + obj.getKey());
}
/**
* 这也不是一个泛型方法,这只是一个普通的方法,只不过是使用了泛型通配符?
* 同时这个也验证了泛型通配符章节所描述的问题,? 是一种实参,可以看做所有类的父类
*/
public void showKeyValue2(Generic<?> obj){
log.info("泛型测试key is " + obj.getKey());
}
/**
* 这个方法是有问题的,编译器会给出错误提示信息“ UnKnown class 'E' ”未知类型E
* 虽然声明了,也表明这是一个可以处理泛型类型的泛型方法
* 但是,只是声明了泛型类型T,并未声明泛型E,因此编译器并不知道到该如何处理E这个类型
*/
public <T> void showKeyValue3(Generic<E> obj){
log.info("泛型测试key is " + obj.getKey());
}
/**
* 这个方法也是有问题的,编译器会提示错误信息“UnKnown class 'T'” 未知类型T
* 对于编译器来说,类型T并未在方法中声明过,因此编译器也不知道如何编译这个类
* 所以这也不是一个正确的泛型方法
*/
public void showKeyValue4(T obj){
}
}
当然这并不是泛型方法的全部,泛型方法可以出现在任何地方和场景中。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们通过一个例子看一下
package com.proxys.business.genericity;
/**
* 测试 泛型类中的泛型方法
*/
public class GenericFruit {
static class Fruit{
@Override
public String toString(){
return "Fruit";
}
}
static class Apple extends Fruit {
@Override
public String toString(){
return "Apple";
}
}
static class Person {
@Override
public String toString(){
return "Person";
}
}
static class GenerateTest<T> {
public void show_1(T t){
System.out.println(t.toString());
}
/**
* 在泛型类中声明了一个泛型方法,使用泛型E,这个泛型E可以是任意类型。可以与类型T相同,也可以不通
* 由于泛型方法声明的时候声明了泛型,因此即时在泛型类中未声明泛型,编译器也能够正确识别泛型方法中的泛型
*/
public <E> void show_2(E e){
System.out.println(e.toString());
}
/**
* 在泛型类中声明了一个泛型方法,使用泛型T,注意这个泛型T是一个全新的类型,与类中的泛型T并不相同。
* 这个泛型T可以与泛型类中声明的T不是一种类型
*/
public <T> void show_3(T t){
System.out.println(t.toString());
}
}
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
GenerateTest<Fruit> generateTest = new GenerateTest<>();
// 因为Apple是Fruit的子类,所以这个方法可以传入Apple类的实例作为实参
generateTest.show_1(apple);
// 编译器报错,因为泛型类的泛型类型指定的实参是Fruit类型,所以show_1方法的形参也为Fruit类型,
// 而传入方法实参为Person类型,所以会报错
//generateTest.show_1(person);
//这两个方法都会执行成功,因为show_2方法为泛型方法,
// 接收参数类型是方法调用的时候指定的,传入不同类型的实参所指定的类型就不相同
generateTest.show_2(apple);
generateTest.show_2(person);
// 这两个方法都会执行成功,因为show_3方法为泛型方法,
generateTest.show_3(apple);
generateTest.show_3(person);
}
}
/**
* 泛型方法与可变参数
*/
public <T> void printMsg(T... args){
for (T t : args) {
System.out.println(t);
}
}
/**
* 测试 泛型方法与可变参数
*/
@Test
public void test4(){
printMsg("测试","ceshi","123",456);
}
静态方法有一种情况是需要注意的,那就是在类中的静态方法中使用泛型:静态方法是无法访问类上定义的泛型的;如果静态方法操作的引用数据类型不确定的时候,则必须将泛型定义在方法上(既静态泛型方法)
/**
* 静态方法与泛型
*/
public class StaticGenerator<T> {
/**
* 如果在类中的静态方法中使用泛型,需要将这个方法定义为泛型方法
* 即时静态方法中使用的泛型,是类中已经声明的泛型,也是不可以的,因为访问不到
*/
public static <T> void show(T t){
}
/**
* 例如:
* 此时编译器会报错,无法从 static 上下文引用 'com.proxys.business.genericity.StaticGenerator.this'
* 因为静态方法访问不到类上的非静态参数
*/
public static void show(T t){
}
}
泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:
无论何时,如果你能做到,就尽量的使用泛型方法。
也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。
对于一个static的方法,无法访问泛型类的参数,所以static方法要具有泛型能力就必须声明为泛型方法
在使用泛型的时候,我们还可以为传入的泛型类型的实参进行上下边界的限制,如:类型实参只允许传入某种类型的父类或某种类型的子类。为泛型添加上下边界,既传入的类型必须是制定的子类型。
/**
* 泛型的上下边界
*/
public void showKeyValue1(Genericity<? extends Number> obj){
System.out.println(obj.getKey());
}
// 测试 泛型的上下边界
@Test
public void test5(){
Genericity<String> stringGenericity = new Genericity<>("1111");
Genericity<Integer> integerGenericity = new Genericity<>(123);
Genericity<Double> doubleGenericity = new Genericity<>(123D);
Genericity<Float> floatGenericity = new Genericity<>(123.3f);
// 这一行代码会编译错误,因为String类型并不是Number类型的子类
// showKeyValue1(stringGenericity);
showKeyValue1(integerGenericity);
showKeyValue1(doubleGenericity);
showKeyValue1(floatGenericity);
}
如果把泛型类的定义改一下
public class Generic<T extends Number>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
//这一行代码也会报错,因为String不是Number的子类
Generic<String> generic1 = new Generic<String>("11111");
再来一个泛型方法的例子:
// 在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的上添加上下边界,
// 既在泛型方法声明的时候添加限制
// 编译器会报错,"Unexpected bound"以外的绑定,泛型方法只能在定义的时候制定上下文边界
public <T> T showKeyName(Generic<T extends Number> container){}
public <T extends Number> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
T test = container.getKey();
return test;
}
在Java中是“不能创建一个确切的泛型类型的数组”
// 也就是说下边这个例子是不可以的,编译会抛出异常
List<String> ls = new ArrayList<String>[10];
// 而使用通配符创建泛型数组是可以的(因为泛型通配符标识不确定的类型)
List<?> ls = new ArrayList<?>[10];
// 这样也是可以的,因为new的时候没有指定确切的泛型
List<String> ls = new ArrayList[10];