Java 泛型详解
共 7830字,需浏览 16分钟
·
2020-11-25 17:22
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
作者 | 低吟不作语
来源 | urlify.cn/uiEfye
1、概述
在 Java5 以前,普通的类和方法只能使用特定的类型:基本数据类型或类类型,如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大
Java5 的一个重大变化就是引入泛型,泛型实现了参数化类型,使得你编写的组件(通常是集合)可以适用于多种类型。泛型的初衷是通过解耦类或方法与所使用的类型之间的约束,使得类或方法具备最宽泛的表达力。然而很快你就会发现,Java 中的泛型并没有你想的那么完美,甚至存在一些令人迷惑的实现
2、泛型类
促成泛型出现的最主要动机之一就是为了创建集合类,集合用于存放要使用到的对象。现有一个只能持有单个对象的类:
class Automobile {}
public class Holder1 {
private Automobile a;
public Holder1(Automobile a) { this.a = a; }
Automobile get() { return a; }
}
如果没有泛型,那么就必须明确指定其持有的对象的类型,会导致该复用性不高,它无法持有其他类型的对象,我们当然不希望为每个类型都编写一个新类
在 Java5 以前,为了解决这个问题,我们可以让这个类直接持有 Object 类型的对象,这样就可以持有多种不同类型的对象了。但通常而言,我们只会用集合存储同一类型的对象。泛型的主要目的之一就是用来约定集合要存储什么类型的对象,并且通过编译器确保规约得以满足
所以,与其使用 Object,我们更希望先指定一个类型占位符,稍后再决定具体使用什么类型。由此我们需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类时,再用实际的类型替换此类型参数
public class GenericHolder {
private T a;
public GenericHolder() {}
public void set(T a) { this.a = a; }
public T get() { return a; }
public static void main(String[] args) {
// 在 Java7 中右边的尖括号可以为空
GenericHolder h2 = new GenericHolder();
GenericHolder h3 = new GenericHolder<>();
h3.set(new Automobile()); // 此处有类型校验
Automobile a = h3.get(); // 无需类型转换
//- h3.set("Not an Automobile"); // 报错
}
}
3、元组类库
有时一个方法需要能返回多个对象,而 return语句只能返回单个对象,解决的方法就是创建一个对象,用它来打包想要返回的多个对象。元组的概念正是基于此,元组将一组对象直接打包存储于单一对象中,可以从该对象读取其中元素,却不允许向其中存储新对象(这个概念也称数据传输对象或信使)
元组可以具有任意长度,元组中的对象可以是不同类型的,我们希望能为每个对象指明类型,这时泛型就派上用场了。例如下面是一个可以存储两个对象的元组:
public class Tuple {
public final A a1;
public final B a2;
public Tuple(A a, B b) { a1 = a; a2 = b; }
public String rep() { return a1 + ", " + a2; }
@Override
public String toString() {
return "(" + rep() + ")";
}
}
使用 final 修饰成员变量可以保证其不被修改,如果用户想存储不同的元素,那么就必须创建新的 Tuple 对象。当然也可以允许用户重新对 a1、a2 赋值,但无疑前一种形式会更加安全
利用继承机制可以实现长度更长的元组:
public class Tuple3 extends Tuple2 {
public final C a3;
public Tuple3(A a, B b, C c) {
super(a, b);
a3 = c;
}
@Override
public String rep() {
return super.rep() + ", " + a3;
}
}
4、泛型方法
到目前为止,我们已经研究了参数化整个类,其实还可以参数化类中的方法。类本身是否是泛型,与它的方法是否是泛型并没有什么直接关系。我们应该尽可能使用泛型方法,通常将单个方法泛型化要比将整个类泛型化要更加清晰易懂
要定义泛型方法,请将泛型参数列表放置在返回值之前:
public class GenericMethods {
public void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f("");
gm.f(1);
gm.f(1.0);
gm.f(1.0F);
gm.f('c');
gm.f(gm);
}
}
使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型,这称为类型参数推断,因此,对 f() 的调用看起来像普通的方法调用,而且像是被重载了无数次一样
5、泛型擦除
当你开始深入研究泛型时,你会发现一个残酷的现实:在泛型代码内部,无法获取任何有关泛型参数类型的信息
class Frob {}
class Fnorkle {}
class Quark {}
class Particle {}
public class LostInformation {
public static void main(String[] args) {
List list = new ArrayList<>();
Map map = new HashMap<>();
Quark quark = new Quark<>();
Particle p = new Particle<>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
}
}
/* Output:
[E]
[K,V]
[Q]
[POSITION,MOMENTUM]
*/
正如上例中输出所示,你只能看到用作参数占位符的标识符,这并非有用的信息。Java 泛型是使用擦除实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此 List
再来看一个例子:
class Manipulator {
private T obj;
Manipulator(T x) {
obj = x;
}
// Error: cannot find symbol: method f():
public void manipulate() {
obj.f();
}
}
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator manipulator = new Manipulator<>(hf);
manipulator.manipulate();
}
}
因为擦除,Java 编译器无法将 manipulate() 方法能调用 obj 的 f() 方法这一需求映射到 HasF 具有 f() 方法这个事实上。为了调用 f(),我们必须协助泛型类,为泛型类给定一个边界,以此告诉编译器只能接受遵循这个边界的类型。这里重用了 extends 关键字。由于有了边界,下面的代码就能通过编译:
public class Manipulator2 {
private T obj;
Manipulator2(T x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
边界
这提出了很重要的一点:泛型只有在类型参数比某个具体类型(以及其子类)更加“泛化”,代码能跨多个类工作时才有用。因此,使用类型参数通常比简单的声明类更加复杂。但是,不能因此认为使用
有关泛型擦除的困惑,其实是 Java 为实现泛型的一种妥协,因为泛型并不是 Java 语言出现时就有的。擦除减少了泛型的泛化性,泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如, List
在 Java5 以前编写的类库是没有使用泛型的,而作者可能打算重新用泛型编写,或者根本不打算这样做。Java 设计者们既要保证旧代码和类文件依然合法,还得考虑当某个类库变为泛型时,不会破坏依赖于它的代码和应用。Java 设计者们最终认为泛型是唯一可行的解决方案,擦除使得向泛型的迁移成为可能,为了实现非泛型的代码和泛型代码共存,必须将某个类库使用了泛型这样的“证据”擦除
基于上述观点,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而言。因为擦除,我们无法在运行时知道确切的类型,为了补偿擦除带来的弊端,我们可以为所需的类型显示传递一个 Class 对象,以在类型表达式中使用它
class Building {
}
class House extends Building {
}
public class ClassTypeCapture {
Class kind;
public ClassTypeCapture(Class kind) {
this.kind = kind;
}
public boolean f(Object arg) {
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture ctt1 =
new ClassTypeCapture<>(Building.class);
System.out.println(ctt1.f(new Building()));
System.out.println(ctt1.f(new House()));
ClassTypeCapture ctt2 =
new ClassTypeCapture<>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
}
}
6、边界和通配符
由于擦除会删除类型信息,因此唯一可用于无限制泛型参数的方法是那些 Object 可用的方法。边界允许我们对泛型使用的参数类型施以类型,将参数限制为某类型的子集,那么就可以调用该子集中的方法。为了应用约束,Java 泛型使用了 extends 关键字
class Coord {
public int x, y, z;
}
interface Weight {
int weight();
}
class Solid {
T item;
Solid(T item) {
this.item = item;
}
T getItem() {
return item;
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
int weight() {
return item.weight();
}
}
class Bounded
extends Coord implements Weight {
@Override
public int weight() {
return 0;
}
}
public class BasicBounds {
public static void main(String[] args) {
Solid solid =
new Solid<>(new Bounded());
solid.getY();
solid.weight();
}
}
引入通配符可以在泛型实例化时更加灵活地控制,也可以在方法中控制方法的参数,具体语法如下:
? extends T:表示 T 或 T 的子类
? super T:表示 T 或 T 的父类
?:表示可以是任意类型
7、值得注意的问题
在这里主要阐述在使用 Java 泛型时会出现的各类问题
1. 任何基本数据类型不能作为类型参数
Java 泛型的限制之一是不能将基本类型用作类型参数。因此,不能创建 ArrayList
2. 实现参数化接口
一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。下面是产生这种冲突的情况:
interface Payable {}
class Employee implements Payable {}
class Hourly extends Employee implements Payable {}
Hourly 不能编译,因为擦除会将 Payable
3. 转型和警告
使用带有泛型类型参数的转型不会有任何效果,例如:
class Storage {
private Object obj;
Storage() {
obj = new Object();
}
@SuppressWarnings("unchecked")
public T pop() {
return (T)obj;
}
}
public class GenericCast {
public static void main(String[] args) {
Storage storage = new Storage<>();
System.out.println(storage.pop());
}
}
如果没有 @SuppressWarnings 注解,编译器将对 pop() 产生 “unchecked cast” 警告。由于擦除的原因,编译器无法知道这个转型是否是安全的,并且 pop() 方法实际上并没有执行任何转型。这是因为,T 被擦除到它的第一个边界,默认情况下是 Object,因此 pop() 实际上只是将 Object 转型为 Object
4. 重载
下面的程序是不能编译的,因为擦除,所以重载方法产生了相同的类型签名
public class UseList {
void f(List v) {}
void f(List v) {}
}
粉丝福利:实战springboot+CAS单点登录系统视频教程免费领取
???
?长按上方微信二维码 2 秒 即可获取资料
感谢点赞支持下哈