Java|重学泛型

之前提到泛型,更多的时候只是在容器类中用到这个,知道他是参数化类型,除此之外别无其它了,甚至自己写也不一定能写的明白

今天写业务需求刚好需要用到泛型

趁此机会参考一个博客系统性的学习一下泛型相关的知识点

这边以自定义一个简单的通用容器 GenericContainer 为例开始学习

泛型的特性

Java中的泛型,特性是只会在编译期生效

对于定义了参数化类型的泛型类,不一定需要传入参数,例如常见的容器类型,泛型参数只是一个约束限制,如果没有传也可以,但是在编译期间得不到对应类型的结果,具体在 IDEA 中就是会爆黄

1
2
3
4
5
6
//如果不传入泛型参数,默认得到的都是Object类型
List list = new ArrayList();
Object o = list.get(0);

List<String> paramiterizedList = new ArrayList<>();
String s = paramiterizedList.get(0);

从泛型类开始

知道了上面泛型的特性之后我们可以自定义做一个容器,来运用泛型类的特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class GenericContainer<T> {

//参数化类型属性作为类的成员,一种很常见的操作,容器类也都是这样定义的
private T item;

T getItem() {
return item;
}

void setItem(T item) {
this.item = item;
}

GenericContainer() {
}

GenericContainer(T item) {
this.item = item;
}
}

我们在声明这个泛型类的时候,编译阶段就锁死了容器中泛型的类型

1
2
3
4
5
6
public static void main(String[] args) {
//String 类型容器
GenericContainer<String> strContainer = new GenericContainer<>();
//Integer 类型容器
GenericContainer<Integer> intContainer = new GenericContainer<>();
}

泛型接口

1
2
3
4
5
6
7
8
9
10
11
public interface Generator <T>{

T duplicateItem(T item);

}
public class GeneratorImpl implements Generator{
@Override
public Object duplicateItem(Object item) {
return null;
}
}

如果没有传递类型作为参数约束,那么类型就没有特定化,只能返回 Object 类型的通用对象

当然更常见的是将参数设置权抛出给上游,在实现类中继续接收类型参数

1
2
3
4
5
6
public class GeneratorImpl<T> implements Generator<T>{
@Override
public T duplicateItem(T item) {
return null;
}
}

泛型通配符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
//String 类型容器
GenericContainer<String> strContainer = new GenericContainer<>();
//Integer 类型容器
GenericContainer<Integer> intContainer = new GenericContainer<>();
//Number 类型容器
GenericContainer<Number> numberContainer = new GenericContainer<>();

//显示Integer类型容器
showItem(intContainer);
//显示Number类型容器
showItem(numberContainer);
}
//需要用?通配符来兼容所有泛型类型,但是由于兼容性高,只能对具体item看作Object进行操作
public static void showItem(GenericContainer<?> numberContainer){
Object item = numberContainer.getItem();
System.out.println(item);
}

我们通过 ? 来标识,这个类型参数我们不知道,但是为了适配兼容具有多态通用性,使用通配符 ?作为类型实参

但是需要注意由于使用了通配符,兼容性很强,可以把?看作是所有参数类型的父类,相当于是参数类型中的Object,因此只能当作 Object 用,可用的方法在编译期间十分有限

可以解决当具体类型不确定的时候,这个通配符就是 ?

当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能,那么可以用 ? 通配符来表未知类型

泛型方法

泛型类和泛型方法差别很大,并且这两个都会更加常见,一开始学习的时候也是把泛型方法和泛型类混淆了,尤其是泛型方法看着很吃力

泛型类,是在实例化类的时候指明泛型的具体类型

泛型方法,是在调用方法的时候指明泛型的具体类型

具体看一下代码就知道了,泛型方法需要在 范围修饰符 和 返回值之间通过一个 <> 来限定作用在这个方法中的所有参数类型

1
2
3
4
5
6
7
//泛型方法
public static <T,E> E changeItem(GenericContainer<T> container){
GenericContainer<E> changedContainer = new GenericContainer<>();
//这里只是简单模拟转换,具体根据业务场景进行转换
changedContainer.setItem((E)container.getItem());
return changedContainer.getItem();
}

需要注意的是泛型方法 和带有通配符的泛型方法之间很容易在初学的时候被混淆

下面这两个都是和泛型确实有关的方法,但是只是确定参数类型的泛型类对象 作为方法参数的 一个普通方法

1
2
3
4
5
6
7
8
9
10
   //这个只是带有特殊确定实参的容器,作为参数,的方法
public static void showItem(GenericContainer<?> trueGenericContainer){
Object item = trueGenericContainer.getItem();
System.out.println(item);
}
//本质上和这个方法没有任何区别
public static void showIntItem(GenericContainer<Integer> intContainer){
Integer item = intContainer.getItem();
System.out.println(item);
}

但是如果你想要在调用方法的时候,传递不确定的参数,就需要声明泛型方法👆

泛型类中的泛型方法

我们在之前的 GenericContainer 泛型类中新增三个方法,当然第一个不是泛型方法,只是带有泛型类型参数的 在泛型类中存在的 普通方法,他的 T 一定等于 泛型类初始化的时候的 T

1
2
3
4
5
6
7
8
9
10
11
12
//注意这三个方法的名称不能重复,由于泛型只作用于编译阶段,因此同名的方法在编译阶段会被认为是同一个方法
public void showContainerItem(T item){
System.out.println("item is " + item);
}

public <E> void showParameterizedItem(E item){
System.out.println("item is " + item);
}

public <T> void showAnotherParameterizedItem(T item){
System.out.println("item is " + item);
}

之后我们测试调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//-----测试泛型类中泛型方法-----//
GenericContainer<Number> numberGenericContainer = new GenericContainer<Number>();
Number numberItem = 0;
//泛型类型作为参数调用传参,没有任何问题
numberGenericContainer.showContainerItem(numberItem);
Integer intItem = 1;
//也没有问题,对应的是函数调用多态,要求传入 T 类型,由于上面的实例化,在编译期间已经确定了是 Number 类型
// 实际传入子类,一点问题没有
numberGenericContainer.showContainerItem(intItem);
String strItem = "str";
//这个有问题,因为泛型类型是Number,showContainerItem本质不是泛型方法
//只能固定接收泛型类型以及对应类型的子类,所以这里编译器会报错
numberGenericContainer.showContainerItem(strItem);
//没有问题,showParameterizedItem本质上是一个泛型方法,此时E = String
numberGenericContainer.showParameterizedItem(strItem);
//没有问题,showClassSameParameterizedItem本质上也是一个泛型方法,此时方法的T = String,类的T = Number,两个不是一个T
numberGenericContainer.showClassSameParameterizedItem(strItem);

泛型方法和可变参数

可变参数可以看作是 Object[]

那么我们可以编写一个通用的工具方法打印不同类型数据

1
2
3
4
5
6
7
8
9
public class GenericUtils {
public static <E> void printData(E... data){
for (E e : data) {
//要求传入的类都实现toString方法
System.out.println(e);
}
}
}
GenericUtils.printData(1,"2",3L,4.0f,5.0);

泛型方法和静态

当静态方法想要使用泛型的时候,不可以存在之前那样将泛型类型作为形参,而本身不是泛型方法的情况

我们上面的工具方法就是一个静态的泛型方法, 不可删除

小总结

  • 泛型方法可以使得方法独立于类做到动态接收不同类型的变化,在设计一些通用的工具方法和设计模式中十分好用
  • 泛型方法定义在泛型类中时,泛型方法自身定义的泛型类型 和 类定义的泛型类型 可以重名会覆盖,但是在设计时应当尽量避免重名减少歧义(T E V 不够你用了吗XD)
  • 需要警惕区分 在泛型类中存在的,泛型类型作为函数接收形参(本质上和泛型类初始化强绑定)的普通方法 和正常的泛型方法,自己在定义泛型方法的时候也应当先看看所在类是否已经是泛型类

泛型上下边界

在实际使用的时候,我们往往希望限定传入的类型实参 是某个特定的父类或者子类,这样在对类型实参对象操作的时候能力会更多

  • 在通配符(特定不知道的类型实参限定,从Object下更加细化)
1
2
3
4
5
public static void showNumberItem(GenericContainer<? extends Number> trueGenericNumberContainer){
//限定了上下界后,通配符获得的就不再是 Object 类型,能力更强
Number item = trueGenericNumberContainer.getItem();
System.out.println(item.longValue());
}
  • 泛型类中限定(容器只能存储特定类型)
1
public class GenericContainer<T extends  Number> {}
  • 泛型方法中限定
1
2
3
public <E extends Number> void showParameterizedNumberItem(E item){
System.out.println("item is " + item.longValue());
}

Java|重学泛型
http://example.com/2024/08/06/Java-重学泛型/
作者
Noctis64
发布于
2024年8月6日
许可协议