详解 Java 泛型(Generic)机制

泛型是什么?

使用泛型可以指定类型变量,从而让代码可以对不同类型的对象进行重用。以及,还可以让编译器更好的了解类型,从而避免强制类型转换,提升代码的安全性。

类型变量就是尖括号 <>中的变量,类型变量的命名规范是使用大写字母,例如 E 表示元素类型,K、V 分别表示键和值类型,T 和相邻的 U、S 表示任意类型。当然你也可以起其他的名字,编译器对此并没有强制限制,但是还是按照规范来。

泛型类

泛型类(generic class)就是有一个或多个类型变量的类。例如下面的 Pair 类,就有两个类型变量,分别是 T 和 U:

public class Pair<T, U> {
   
  public T first;
  public U second;
  
  public Pair(T first, U second) {
   
    this.first = first;
    this.second = second;
  }
}

在使用时,就可以传入任意的类型。例如:

Pair<Integer, String> pair = new Pair<>(1, "A");

思考一下,如果没有泛型的存在,那么当我们要操作不同类型的对象时,要么为每种类型都创建一个类,但是这样就会存在大量的重复代码,不容易维护。要么就直接使用对象的顶级父类 Object 类,但是这样的话在使用时就需要进行强制转换,存在安全问题,即使每次强转前进行判断,也会存在重复代码和遗漏的风险。

说回泛型类,泛型类的类型变量是定义在类名后面,在整个类中都可以使用定义的类型变量。所以在选择使用泛型类时,泛型变量应该是和类所关联的。如果仅与方法关联,那么可以使用泛型方法。

泛型方法

泛型方法的类型参数是定义在方法返回类型的前面的,只在当前方法中可以使用。例如:

public static <T> T getMiddle(T... a) {
   
  return a[a.length / 2];
}

泛型方法可以在普通类中定义,也可以在泛型类中定义。如果泛型变量仅在方法内会用到,就可以考虑使用泛型方法。

类型变量的限制

默认情况下,类型变量可以是任何类型。但是有时候,我们需要限制类型变量的类型。此时可以通过 extends关键字来限制:

  • <T extends UpperBoundType> 限制泛型类型为特定类型或者特定类型的子类

例如,假设我们有一个 Printer 类,我们只需要这个类打印动物的信息,那么就可以使用 <T extends Animal> 来限制只能接收 Animal 或其实现类:

public interface Animal {
   
  String getName();
}

public static class Printer<T extends Animal> {
   
  public void print(T t) {
   
    System.out.println(t.getName());
  }
}

public static class Dog implements Animal {
   
  @Override
  public String getName() {
   
    return "Woof";
  }
}

public static class Cat implements Animal {
   
  @Override
  public String getName() {
   
    return "Meow";
  }
}

public static void main(String[] args) {
   
  Printer<Animal> animalPrinter = new Printer<>();
  animalPrinter.print(new Dog());
  animalPrinter.print(new Cat());
  animalPrinter.print(new People("Abc")); // 如果试图传入一个其他类型,则会编译失败
}

一个类型变量,可以指定多个限制类型。例如:

public class Printer<T extends Dog & Animal> {
   
  ...
}

上面的限制的意思是,类型变量必须是 Dog 或其子类,并且实现了 Animal 接口。需要注意的是,在指定多个限制的类型时,除了第一个限制类型可以是类以外,其他的限制类型必须是接口类型。这是因为 Java 是不支持多重继承的。

类型擦除

由于泛型是在 Java 1.5 才推出的,所以 Java 为了保证在之前版本上的兼容性,泛型实际上在 JVM 中是没有的!

也就是说,编译器在编译时会擦除掉泛型的参数类型,并提供一个相应的原始类型(raw type),这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除(erased),并替换为其限定类型,没有限定类型则替换为 Object 类型。

举个例子,如果我们定义了一个泛型类:

public class Printer<T> {
   
  ...
}

那么当类型擦除后,实际上就变为了 Printer<Object> 类型。

如果指定了限定类型,例如:

public class Printer<T extends Animal> {
   
  ...
}

那么擦除后,就变为了 Printer<Animal> 类型。

如果指定了多个限定的类型,那么擦除后就会是第一个限定的类型。例如:

public class Printer<T extends Dog & Animal> {
   
  ...
}

擦除后就变为了 Printer<Dog>类型。

Java 泛型的很多限制都是由于泛型擦除导致的,下面会详细介绍下泛型的局限性。

通配符

通配符(Wildcard)就是 Java 泛型中的问号 ? 。要想知道通配符是要解决什么样的问题,我觉得首先需要知道什么是 Variance(不知道这个正确的译名是什么,如有知道请留言)?

提到 Variance 必须先提到子类型(subtyping),子类型是面向对象中类型多态的其中一种表现形式,主要用于描述 is-a 这样的关系,例如 S 是 T 的子类型,那么 S is a subtype of T。

Variance 指的是如何根据组成类型之间的子类型关系,来确定更复杂的类型之间的子类型关系。

上面这段话比较绕,举个实际的例子来说,Variance 指的就是当子类型在更复杂的场景下,例如 If S is a subtype of T,那 Generic<S> is subtype of Generic<T> 这种关系是否还能成立。

Variance 分为下面几种形式:

  1. 不变(Invariance):如果 B 是 A 的
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值