Java 泛型

泛型是 JDK 5 中引入的一个新特性,它的本质是将数据类型参数化。

一、泛型

1. 什么是泛型?

首先来看这样一个案例:

在 Java 中,没有泛型的集合将所有对象都当作 object 处理。

这样的做法带来的问题是:缺失了类型检查,导致存取元素时需要进行额外的判断。

我们希望让集合只能存储某一类型的元素,可以这么做:

  • 简单封装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class StringList {

    List list = new ArrayList();

    add(String str) {
    list.add(str);
    }

    }

    这个做法的缺点是通用性不高,每增加一个类型的变量,就需要增加一个集合类

  • 通用封装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class MyList {

    String type;

    List list = new ArrayList();

    MyList(String type) {
    this.type = type;
    }

    add(Object object) {
    if (object.getClass() === type) {
    list.add(str);
    }
    }

    }

相比于上面的方法,泛型能帮助我们更好地解决这一问题。

泛型的本质是将数据类型参数化。

数据类型不再是既定的,而是可以被动态指定。

2. 泛型的好处

  • 代码复用,同一套代码可以适用于不同类型
  • 类型安全,编译器会根据泛型做类型检查,就像泛型是既定类型一样

二、泛型语法

1. 说明

  • 在方法、类、接口中声明类型参数,将类型参数当作真正的数据类型编写程序
  • 在使用泛型方法、泛型类、泛型接口时,传入类型实参,类型参数将被传入的数据类型所替代

2. 语法

1
2
3
4
5
6
7
8
9
10
11
// 声明类型参数
// 一种类型
<泛型标记符>
// 多种类型
<泛型标记符, 泛型标记符, ···, 泛型标记符>

// 传入类型实参
// 一种类型
<数据类型>
// 多种类型
<数据类型, 数据类型, ···, 数据类型>

3. 泛型标记符

可以在方法、类、接口中声明类型参数,使用 泛型标记符 来标识泛型。

类型标记符只是一个标记,它用来代指被传入的任何数据类型。

泛型标记符 可以随意设置,常用标记符如下:

  • E:表示集合中的元素
  • T:Type
  • K:表示键
  • V:表示值
  • N:表示数值类型
  • ?:表示不确定,可以用于指代一切类型

4. 泛型上下限

1
<T extends 指定类型>

T 应该是指定类型的子类型,可以把指定类型看作上限。

1
<T super 子类名>

T 应该是指定类型的父类型,可以把指定类型看作下限。

三、泛型方法

1. 语法

1
2
3
<类型参数> 返回值类型 方法名(){
···
}

2. 示例

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) {
doubleCall("123");
doubleCall(123);
doubleCall(66.6);
}

public static <T> void doubleCall(T parameter) {
System.out.println(parameter.getClass());
System.out.println(parameter + "+" + parameter);
}
}

四、泛型类

1. 语法

1
2
3
class 类名<类型参数> {
···
}

2. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Container<T> {
private T parameter;

public T getParameter() {
return parameter;
}

public void setParameter(T parameter) {
this.parameter = parameter;
}
}

public class Test {
public static void main(String[] args) {
// 不传入实参
Container a = new Container();
a.setParameter(123);
System.out.println(a.getParameter());
a.setParameter("123");
// 需要进行强制类型转换
String temp1 = (String) a.getParameter();
System.out.println(temp1);

System.out.println("------");

// 传入实参
Container<String> b = new Container<>();
// b.setParameter(123);
System.out.println(b.getParameter());
b.setParameter("123");
// 无需强制类型转换
String temp2 = b.getParameter();
System.out.println(temp2);
}
}

五、泛型接口

1. 语法

1
2
3
interface 接口名<类型参数> {
···
}

2. 示例

泛型接口

1
2
3
public interface Echo<T> {
void echo(T parameter);
}

类实现泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
public class Test implements Echo<String>{

@Override
public void echo(String parameter) {
System.out.println(parameter + "," + parameter);
}

public static void main(String[] args) {
Test test = new Test();
test.echo("Hello");
}
}

泛型类实现泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
public class Test2<T> implements Echo<T> {

@Override
public void echo(T parameter) {
System.out.println(parameter + "," + parameter);
}

public static void main(String[] args) {
Test2<String> test2 = new Test2<>();
test2.echo("world");
}
}

六、泛型方法的重载

泛型允许设定通配符的上限和下限,因此可以在一个类中定义多个相同的方法(只需要它们的类型通配符覆盖范围不重合),从而实现了泛型方法的重载。

1
2
3
4
5
6
7
8
9
[修饰符] <类型形参1> 返回值类型 方法名(类型形参1 参数名){
方法体
return 返回值;
}

[修饰符] <类型形参2> 返回值类型 方法名(类型形参2 参数名){
方法体
return 返回值;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
static <T extends Double> void fun(T parameter) {
System.out.println("Double");
}

static <T extends Integer> void fun(T parameter) {
System.out.println("Integer");
}

static <T extends String> void fun(T parameter) {
System.out.println("String");
}

public static void main(String[] args) {
Test.fun(66.6);
Test.fun(123);
Test.fun("123");
}
}

七、擦除与转移

1. 擦除

当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有的泛型信息将被擦除。

2. 转换

当将没有泛型信息的变量赋给具有泛型信息的变量时,并不会发生错误,编译器会提示“未经检查的转换”。

八、泛型支持 - 伪泛型

JDK 从 1.5 开始才支持泛型,为了兼容之前的版本,Java 采用了 “伪泛型” 的策略,具体来说:

  • 编译期检查:记录泛型的具体值,做类型检查

    1
    2
    ArrayList<Integer> arrayList = new ArrayList<>();
    // arraylist.add("hello"); 报错
  • 泛型擦除:在编译成字节码文件时,Java 会进行 “泛型擦除”,

    • 清除泛型声明,即清除 <···>

    • 根据类型参数的上下限,将不定的类型参数直接替换为具体的类型

      1
      2
      <T> -> Object
      <T extends Car> -> Car
  • 隐式装箱拆箱:为了使泛型类能够接收原始类型的参数,增加隐式装箱拆箱机制

参考