深入理解Java泛型

未完待续

一、引言

泛型(Generics)和面向对象、函数式编程一样,也是一种程序设计的范式,泛型允许程序员在定义类、接口和方法时使用引用类型的类型形参代表一些以后才能确定下来的类型,在声明变量、创建对象、调用方法时像调用函数传参一样将具体类型作为实参传入来动态指明类型。

Java的泛型,是在jdk1.5中引入的一个特性,最主要应用是在jdk1.5的新集合框架中。作为Java语法层面的东西,本博客原本不打算介绍,但考虑到泛型理解和使用起来有一定的难度,应用的还很普遍,再加上自己工作多年好像也没有能够完全理解和灵活的运用泛型,因此还是决定看一些相关的书籍中与泛型有关的内容,并用一些篇幅总结下学习成果,介绍下我理解的泛型。

二、泛型类(接口)

2.1 创建泛型类

先来看两个类

public class StringPrinter {
 private String thingsToPrint;
 public StringPrinter() {
 }
 public StringPrinter(String thing) {
 thingsToPrint = thing;
 }
 public void setThingsToPrint(String thing) {
 thingsToPrint = thing;
 }
 public void print() {
 System.out.println(thingsToPrint);
 }
}
public class IntegerPrinter {
 private String thingsToPrint;
 public IntegerPrinter() {
 }
 public IntegerPrinter(String thing) {
 thingsToPrint = thing;
 }
 public void setThingsToPrint(String thing) {
 thingsToPrint = thing;
 }
 public void print() {
 System.out.println(thingsToPrint);
 }
}

两个类的作用相当,都是将传进来的参数进行打印,类的功能几乎完全相同,唯一的不同是参数的类型不一样,假如要为很多类型实现这个打印功能,就会编写很多的Printer类,如果要实现一个类统一实现这个功能,就可以采用泛型。

先来讲讲泛型语法,泛型用一个“菱形”<>声明,<>中是类型形参列表,如有多个类型形参,使用英文逗号,隔开。

下面程序定义了一个带有泛型声明的Printer类,有一个类型形参T(Type),声明了类的泛型参数后,就可以在类内部使用此泛型参数,构造函数名仍然是类名本身不需要加泛型

public class Printer<T> {
 private T thingsToPrint;
 public Printer() {
 
 }
 public void setThingsToPrint(T thing) {
 thingsToPrint = thing;
 }
 public Printer(T thing) {
 thingsToPrint = thing;
 }
 public void print() {
 System.out.println(thingsToPrint);
 }
}

Java还能在定义类型参数时设置限制条件,如下例定义了一个NumberPrinter类,通过extends指定T的类型上限只能是Number。

⚠️注意:类型参数和第四章提到的类型通配符是不一样的,类型参数上的限制不能用super关键字,因为会造成不确定,使用extends指定T的类型上限,编译器至少知道T是个Number,如果是super关键字,编译器根本不知道T有哪些属性和方法。

public class NumberPrinter<T extends Number> {
 private T thingsToPrint;
 public NumberPrinter() {
 }
 public void setThingsToPrint(T thing) {
 thingsToPrint = thing;
 }
 public NumberPrinter(T thing) {
 thingsToPrint = thing;
 }
 public void print() {
 System.out.println(thingsToPrint);
 }
 public T get() {
 return thingsToPrint;
 }
}

还可以设置多个限制条件,extends后面只能有一个类但是可以有多个接口:

public class NumberPrinter<T extends Number & Comparable<T>> {
 private T thingsToPrint;
}

创建泛型接口同理,例如jdk中的List实际上就是一个接口。

public interface List<E> extends Collection<E> {
 
}

并非任何类都能声明为泛型类,Java规定:异常类(java.lang.Throwable)不得带有泛型

public class MyException<T> extends Exception { //编译出错❌,Generic class may not extend 'java.lang.Throwable'
 T msg;
}
public class MyException<T> extends RuntimeException { //编译出错❌,Generic class may not extend 'java.lang.Throwable'
 T msg;
}
public class MyException<T> extends Throwable { //编译出错❌,Generic class may not extend 'java.lang.Throwable'
 T msg;
}

2.2 实例化泛型类

使用泛型类创建对象时就可以为类型形参T传入具体类型,就可以生成类似Printer<String>Printer<Double>的类型

public static void main(String[] args) {
 // 构造器T形参是String,只能用String初始化
 Printer<String> printer1 = new Printer<String>("apple");
 printer1.print(); //apple
 // 构造器T形参是Double,只能用Double初始化
 Printer<Double> printer2 = new Printer<Double>(3.8);
 printer2.print(); //3.8
}

jdk1.7以后,支持泛型类型推断,可以简写为:

Printer<String> printer1 = new Printer<>("apple");
Printer<Double> printer2 = new Printer<>(3.8);

如不指定类型实参默认为Object类型,因为所有引用类型都能被Object代表,int、double、char等基本数据类型不能被Object代表,这就是类型实参必须是引用类型的原因,不过注意如果定义类型形参时通过entends指定了上限例如NumberPrinter<T extends Number>,则不传递类型实参时默认为上限类型Number

public static void main(String[] args) {
 Printer printer1 = new Printer("apple");
 printer1 = new Printer(12);
 printer1 = new Printer(new Date());
 NumberPrinter numberPrinter1 = new NumberPrinter(5);
 NumberPrinter numberPrinter2 = new NumberPrinter(5.8);
 NumberPrinter numberPrinter3 = new NumberPrinter(""); //编译出错❌
}

2.3 派生泛型类

派生该类时,需要指定类型实参

public class HPPrinter extends Printer<Integer> {
}

通过entends指定了上限的类型需不超过上限类型,以下同理

public class SuperNumberPrinter extends NumberPrinter<Double> {
}
public class SuperNumberPrinter extends NumberPrinter<Date> { //编译出错❌
}

如不使用泛型,不指定类型实参,则泛型转换为Object类型或上限类型

public class HPPrinter extends Printer {
 public static void main(String[] args) {
 HPPrinter hpPrinter = new HPPrinter();
 hpPrinter.setThingsToPrint(new Object());
 hpPrinter.setThingsToPrint("hello");
 hpPrinter.setThingsToPrint(12);
 }
}

还可以子类和父类声明同一个类型形参,子类中也不确定具体的类型,需要子类被实例化时将类型间接传递给父类,同时子类还可以一同定义自己的泛型

public class HPPrinter<T> extends Printer<T> {
}
public class HPPrinter<T, E> extends Printer<T> {
}

子类确定父类泛型类型的同时,又可以有自己的泛型

public class HPPrinter<E> extends Printer<Integer> {
}

使用泛型又不指定类型的写法是错误的

public class HPPrinter extends Printer<T> { //编译出错❌
}

三、泛型方法和泛型构造器

有时候,在类和接口上不需定义类型形参,只是具体方法中的某个类型不确定,需要在方法上面定义类型形参,这个也是支持的,jdk1.5提供了对于泛型方法的支持。

3.1 泛型方法

声明方法时,在返回值前指明泛型的类型形参列表<>,类型形参仅作用于方法内,这个方法就声明为了泛型方法。类型形参可以出现在参数和返回值中,调用方法时指定具体类型。泛型方法可以根据需要声明为静态。任何类中都可以存在泛型方法,而不是只有泛型类中才能声明泛型方法。

?在返回值前面指明泛型的类型形参列表<>是泛型方法的特征,没有这个特征的都不是泛型方法,泛型类中使用类<>里面声明的类型作为方法参数或返回值类型的方法,不属于泛型方法,例如2.1中Printer类中的任何方法都不是泛型方法。

public class Demo {
 public <T> E fun1(T e) {
 return null;
 }
 public <T> void fun2(T e) {
 
 }
 public static <T> List<T> copyArray(T[] arr) {
 List<T> list = new ArrayList<>();
 
 for (int i = 0; i < arr.length; i++) {
 list.add(arr[i]);
 }
 
 return list;
 }
 public <T> T getMiddle(T... a) {
 return a[a.length / 2];
 }
 public static <T> T getMiddleStatic(T... a) {
 return a[a.length / 2];
 }
}

与类、接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数,当程序调用copyArray()方法时,无须在调用该方法前传入String、Obiect等类型,但系统依然可以知道类型形参的数据类型,因为编译器根据实参推断类型形参的值,它通常推断出最直接的类型参数。例如,下面调用代码:

public static void main(String[] args) {
 List<Dog> dogs = copyArray(new Dog[]{});
}

如果要显示指明类型实参,则需要和实际类型一致,而且必须在对象名.this.类名.之后指定,否则语法报错。

public static void main(String[] args) {
 Demo demo = new Demo();
 Integer middle1 = getMiddleStatic(1, 2, 3);
 
 String middle2 = Demo.<String>getMiddleStatic("a", "b", "c");
 // 指定的和传入的类型不匹配,编译出错❌
 Integer middle3 = Demo.<String> getMiddleStatic(1, 2, 3);
 Double d = demo.<Double>getMiddleStatic(1.0, 2.0, 3.0);
}

当使用自动推断时,如果涉及多个类型进行自动推断则取多个类型的共同父类(接口)

public static void main(String[] args) {
 Number n1 = getMiddleStatic(1, 2, 3.9);
 Serializable n2 = getMiddleStatic(1, 2, "", new Date());
 Serializable n3 = getMiddleStatic( 2, "", null);
}

3.2 泛型构造器

构造器也可能成为泛型方法,Java也允许在构造器签名中声明类型形参,这样就产生了所谓的泛型构造器。例如:

public class Demo {
 public <T> Demo(T obj) {
 System.out.println(obj.getClass());
 }
 public static void main(String[] args) {
 new Demo("hello"); //class java.lang.String
 new Demo(12); //java.lang.Integer
 new <String> Demo("hello"); //class java.lang.String
 new <Integer> Demo(12); //java.lang.Integer
 new <String> Demo(12); // 编译出错❌
 }
}

如果泛型构造器上指明了类型形参,则不可以在new后使用“菱形”语法又手动指定,否则会导致类型无法推断。不过这种奇怪的写法应该不常碰见


public class Demo<T> {
 public <T> Demo(T obj) {
 System.out.println(obj.getClass());
 }
 public static void main(String[] args) {
 
 // 类指定String 方法显示指定Integer
 Demo<String> demo1 = new <Integer> Demo<String>(4);
 //类指定Integer 方法隐式指定String
 Demo<Integer> demo2 = new Demo<Integer>("4");
 
 //类型推断为Integer
 Demo<Integer> demo3 = new Demo<>(3);
 
 // 编译出错❌,不能既要求自动类型推断,又要手动指定
 Demo<Integer> demo4 = new <String> Demo<>("4");
 }
}

四、不存在泛型类

包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上并不存在。

即使加了不同泛型,运行时仍然是同一种类,并不会因为类型参数的不同,产生新的类

public static void main(String[] args) {
 Fruit<String> fruit = new Fruit<>("apple");
 Fruit<Double> fruit2 = new Fruit<>(3.8);
 System.out.println(fruit2.getClass() == fruit.getClass()); //true
}

因此在泛型类中的静态的代码块、静态变量和静态方法上,不能使用类型形参

public class Demo<T> {
 
 public static T st; //编译出错❌
 
 static {
 T a = st; //编译出错❌
 }
 
 public static void fun1(T obj) { //编译出错❌
 st = obj;
 }
}

由于并不存在真正的泛型类,因此instanceof关键字后不能接泛型类

if (new ArrayList<>() instanceof List<String>) { //编译出错❌
 
}
if (new ArrayList<>() instanceof List) { //正确写法✅
 
}

事实上,泛型在编译后会被擦除,运行时Java虚拟机中没有泛型,只有普通类和普通方法,<T>会变为Object类型,<T extends Serializable>会变成Serializable类型

例如下面例子通过反射忽略了泛型,从而在运行时将一个List<String>中添加进去了一个Integer和Date类型的对象。

public class TestReflection {
 public static void main(String[] args) {
 List<String> strs = new ArrayList<>();
 strs.add("hello");
 strs.add("world");
 Class clazz = strs.getClass();
 try {
 Method method = clazz.getMethod("add", Object.class);
 method.invoke(strs, 1);
 method.invoke(strs, new Date());
 }
 catch (Exception e) {
 e.printStackTrace();
 }
 finally {
 for (Object obj : strs) {
 System.out.println(obj);
 }
 }
 }
}

运行结果

hello
world
1
Fri Apr 25 22:30:10 CST 2025

之前提到Java规定异常类不得带有泛型,原因就是异常在运行时是存在的,而泛型在运行时不存在,进行捕获处理时根本不能区分出来。

因为泛型运行时被擦除,因此泛型会影响方法的重载。例如下例由于List的泛型被擦除,导致两个方法不能重载。

public class Demo {
 //编译出错❌ 'test(List<String>)' clashes with 'test(List<Integer>)'; both methods have same erasure
 void test(List<String> list) {
 
 }
 void test(List<Integer> list) {
 }
}

五、类型通配符

有时,如要实现一个遍历打印list的方法,list中是哪一种元素都有可能,于是我们将泛型实参指定为Object类型,看似解决了问题,但是调用时却会编译报错:无法将List<Object>用于List<String>

public class Demo {
 public static void main(String[] args) {
 List<String> strings = new ArrayList<>();
 test(strings); //编译出错❌
 }
 public static void test(List<Object> list) {
 for (int i = 0; i < list.size(); i++) {
 System.out.println(list.get(i));
 }
 }
}

在Java中,两个类通过继承和实现接口可以具有父子关系,但不能认为使用了父子类型的两个泛型类具有父子关系,例如上面程序出现了编译错误,说明List<String>不能被当成List<Object>的子类来用。

?泛型与数组不同,如果是两个有父子关系的类各声明一个数组,例如:Object[]String[]String[]Object[]的子类型,是可以将String[]类型的变量赋值给Object[]的,这是一种Java语言早期不安全的设计,操作不当会引发ArrayStoreException,因此jdk1.5设计泛型时避免了这种设计。

为了表示任意类型,可以使用类型通配符,类型通配符是一个问号?,例如将一个问号作为类型实参传给List集合,写作List<?>,意思是元素类型未知的List。这个问号?被称为通配符,它的元素类型可以匹配任何类型。

现在使用任何类型的List来调用它,程序依然可以访问集合中的元素,其类型是Obiect,这永远是安全的,因为不管List的真实类型是什么,它包含的都是Obiect。

public static void test(List<?> list) {
 Object obj = list.get(i);
}

但是,如果调用add()方法向其中添加非null元素,又会发现编译出错

public static void test(List<?> list) { 
 list.add(new Date()); //编译出错❌
 list.add(null); //能通过编译✅
}

List.java

E get(int index);
boolean add(E e);

通过分析List的get()add()两个方法的源码,可知get()方法是对泛型的读取,返回为EE类型虽然不确定,但肯定是一个Object类型,而add()方法需要为E类型赋值一个参数,是对泛型的写入,而传进来的?不能确定是什么类型,假如传进来的List是个List<String>,在方法中又add(new Date())写入Date类型,就会导致类型混乱,因此无法处理,但是null除外,它是任何引用类型的实例。

说白了,Java的泛型系统是类型安全优先的,不确定类型的泛型可读不可写。

类型通配符还能进行类型范围的限制,例如如果不希望List<?>可以传入任意一种类型,只希望传入某一类具体的类型,在设置类型通配符时,可以添加extendssuper限制条件,叫做受限制的通配符,extends代表某种类型及子类,super代表某种类型及父类。

例如有这样的一些类:

/**
 * 动物
 */
public class Animal {
}
/**
 * 猫
 */
public class Cat extends Animal {
}
/**
 * 狗
 */
public class Dog extends Animal {
}
/**
 * 英短猫
 */
public class YingDuan extends Cat {
 
}
/**
 * 布偶猫
 */
public class BuOu extends Cat {
 
}

首先看extends,extends设置的是类型的上限,保证传入的类型不能超过某个类型,在使用时,如果只希望泛型参数类型是某个类型及其子类,List的类型参数就可以写成? extends,例如:List<? extends Animal>就是只允许传入的泛型类型是Animal及其子类,这样修饰的泛型可读,读出为父类Animal类型,但不可写

为什么不能写入?因为允许写入会导致类型混乱,只要泛型中限制某个类及其子类,那随着类的不断继承就一定会出现更小的子类,当更小的子类作为类型参数时,比这个子类大一些的父类祖父类对象就不能写到泛型修饰的变量中,因为没有子类引用指向父类的道理。再者假如两个兄弟类AB继承自同一父类,AB甚至没有父子关系,当A类作为类型形参,B类的对象更不能写到A类的泛型中。

例如传进来的list是个List<Dog>,方法中又去add(new Cat())会导致类型混乱,因为虽然Cat和Dog都继承自Animal但是Cat不是Dog的子类,会破坏List<Dog>的类型一致性。再例如传进来的是个List<Dog>,方法中又去add(new Animal())也会导致类型混乱,所以extends修饰的泛型禁止写入任何一个非null实例

public class Demo {
 public static void test(List<? extends Animal> list) {
 Animal animal = list.get(1); //获取返回值时,由父类Animal接收
 list.add(new Cat()); //编译出错❌
 
 list.add(new YingDuan()); //编译出错❌
 
 list.add(new Dog()); //编译出错❌
 list.add(new Animal()); //同样编译出错❌
 }
}

再来看super,super和extends的情况会略有不同,super代表限制类型为某种类型及父类,设置的是类型的下限,保证传入的泛型类型不能低于某个类型,super修饰的泛型可读,但只能读出为Object,可写,但只能写入对应类型及其子类,例如? super Cat修饰的变量只允许赋值Cat类及其子类的对象,因为Cat类及其子类的对象肯定可以被Cat类及其父类的引用指向

public class Demo{
 public static void test(List<? super Cat> list) {
 Object object = list.get(1);
 list.add(new Animal()); //编译出错❌
 list.add(new Cat()); //编译通过✅
 list.add(new YingDuan()); //编译通过✅
 list.add(new BuOu()); //编译通过✅
 list.add(null); //编译通过✅
 list.add(new Dog()); //编译出错❌
 }
}

举例来讲的话,可以传给List<? super Cat> list的不是“List<猫>”就是“List<动物>”,所以首先不能add“动物”进去,因为有可能传进来的是“List<猫>”,只有“动物”包含“猫”,没有“猫”包含“动物”。同理,不能add“狗”进去,因为“狗”属于“动物”但不属于“猫”(废话)。所以只有“猫”和“英短”以及“布偶”能add进去,因为无论传进来的是“List<猫>”还是“List<动物>”,“英短”和“布偶”既直接继承自“猫”,也间接继承自“动物”,而“猫”本身就能添加进去

所以只有类及其子类(猫、英短、布偶)可写入super修饰的泛型是因为这样符合程序里面类的继承关系,不会导致泛型中类型混乱。说白了就是:存放“动物”的List,存一只“猫”进去也行,存放“猫”的List,存进去“英短”以及“布偶”逻辑上都是正确的,都可以实现父类引用指向子类对象而不是颠倒过来。

上面例子中,List是一个带有泛型,但是泛型参数没有类型限制的类,如果定义一个泛型类,并限制类型参数的范围,该怎样和类型通配符搭配使用呢

此处定义一个限制类型参数的泛型类Pet,指定类型上限是Animal

public class Pet<T extends Animal> {
 private T thing;
 public T get() {
 return thing;
 }
 public void set(T t) {
 thing = t;
 }
}

使用类型通配符?时,类型参数需严格按照定义泛型时指定的上限,除了读取时返回的都是上限Animal类型,其他的和不加类型限制的泛型类没有区别,都是可读不可写,extends和super修饰的类型通配符也类似,可直接看结论:

public static void main(String[] args) {
 Pet<?> pet = new Pet<Object>(); //编译出错❌
 Pet<?> pet2 = new Pet<>();
 pet2.set(new Animal()); //编译出错❌
 Animal animal = pet2.get();
}
public static void main(String[] args) {
 Pet<? extends Cat> pet = new Pet<YingDuan>();
 pet.set(new Cat()); //编译出错❌
 Cat cat = pet.get();
}
public static void main(String[] args) {
 Pet<? super Cat> pet1 = new Pet<Animal>();
 
 Pet<? super Dog> pet2 = new Pet<>();
 Pet<? super Cat> pet3 = new Pet<Object>(); //编译出错❌
 Animal animal1 = pet1.get();
 Animal animal2 = pet2.get();
}

?可直接记住结论:
? extends T → “只能读(读出是T或类型上限)不能写”
? super T → “能写(T和T的子类),读出来只能是Object或者类型上限”

类型通配符和类型参数的区别:

用法位置意义是否允许
? extends Cat通配符接受某类或子类(只读)✅ 允许
? super Cat通配符接受某类或父类(只写)✅ 允许
T extends Cat类型参数限制上界,T至少是某类✅ 允许
T super Cat类型参数企图限制下界(但 Java 不支持)❌ 不允许
作者:LiuZijian原文地址:https://www.cnblogs.com/changelzj/p/18848121

%s 个评论

要回复文章请先登录注册