AI智能
改变未来

Java 基础 一文搞懂泛型

本文将从以下四个方面来系统的讲解一下泛型,基本上涵盖了泛型的主体内容。

  1. 什么是泛型?
  2. 为什么要使用泛型?
  3. 如何使用泛型?
  4. 泛型的特性

1. 什么是泛型?

泛型的英文是Generics,是指在定义方法、接口或类的时候,不预先指定具体的类型,而使用的时候再指定一个类型的一个特性。

写过Java代码的同学应该知道,我们在定义方法、接口或类的时候,都要指定一个具体的类型。比如:

public class test {private String name;public void setName(String name) {this.name = name;}public String getName() {return name;}}

上面代码就定义了字段

name

的类型为

String

,方法

getName

的返回类型为

String

,这种写法就是预先指定了具体的类型。而泛型就是不预先指定具体的类型。

Java

中有一个类型叫

ArrayList

,相当于一个可变长度的数组。在

ArrayList

类型中就没有预先指定具体的类型。因为数组可以存放任何类型的数据,如果要预先指定一个数组类型的话,那要满足大家对各种类型的需求,就要写很多类型的

ArrayList

,要为每个class写一个单独的

ArrayList

,比如:

  • IntegerArrayList

  • StringArrayList

  • FloatArrayList

  • LongArrayList

这显然不太现实,因为class有上千种,还有自己定义的class。那么在

ArrayList

中预先指定具体的类型就无法满足需求。这个时候就需要使用泛型,即不指定存储数据的具体的类型,这个类型由使用者决定。

为了解决类型的问题,我们必须把

ArrayList

变成一种模板:

ArrayList<T>

,代码如下:

public class ArrayList<T> {private T[] array;private int size;public void add(T e) {...}public void remove(int index) {...}public T get(int index) {...}}

T

可以是任何class,这样一来,我们就实现了:编写一次模版,可以创建任意类型的

ArrayList

// 创建可以存储String的ArrayList:ArrayList<String> strList = new ArrayList<String>();// 创建可以存储Float的ArrayList:ArrayList<Float> floatList = new ArrayList<Float>();// 创建可以存储Person的ArrayList:ArrayList<Person> personLi56cst = new ArrayList<Person>();

因此,泛型也可以说是定义一种模板,例如

ArrayList<T>

,然后在代码中为用到的类创建对应的

ArrayList<类型>

。(泛型是指在定义方法、接口或类的时候,不预先指定具体的类型,而使用的时候再指定一个类型的一个特性。)后面这种定义可能会更好理解其本质。

更为官方的定义是:泛型指“参数化类型”。泛型的本质是为了参数化类型(将类型参数化传递)(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型,可以在类、接口和方法中,分别被称为泛型类,泛型接口,泛型方法

2. 为什么要使用泛型?

参考自:Oracle 泛型文档

与非泛型的代码相比,使用泛型的代码具有很多优点:

  1. 在编译时会有更强的类型检查

    Java编译器对泛型代码进行强ad8类型检查,如果代码违反类型安全,则会发出错误。修复编译时的错误比修复运行时的错误会更加简单,运行时的错误会更难找到。

    说人话就是,使用泛型时,编译器会对输入的类型的进行检查,类型与声明的类型不一致时就会报错。而不使用泛型,编译器可能就检测不到这个类型错误,就会在运行的时候报错。

  2. 消除类型转换

    下面的代码是没有使用泛型的情况,这时候需要对类型进行转换

    List list = new ArrayList();list.add(\"hello\");String s = (String) list.get(0);

    使用泛型,就不需要对类型进行转换

    List<String> list = new ArrayList<String>();list.add(\"hello\");String s = list.get(0);   // no cast
  3. 可以实现更通用的算法

    通过使用泛型,程序员可以对不同类型的集合进行自定义操作以实现通用算法,并且代码类型会更加安全、代码更易读

3. 如何使用泛型?

还是以

ArrayList

为例,如果不定义泛型类型时,泛型类型此时就是

Object

// 编译器警告:List list = new ArrayList();list.add(\"Hello\");list.add(\"World\");String first = (String) list.get(0);String second = (String) list.get(1);

此时,只能把

<T>

当作

Object

使用,没有发挥泛型的优势。

当我们定义泛型类型

<String>

后,

List<T>

的泛型接口变为强类型

List<String>

// 无编译器警告:List<String> list = new ArrayList<String>();list.add(\"Hello\");list.add(\"World\");// 无强制转型:String first = list.get(0);String second = list.get(1);

编译器看到泛型类型

List<String>

就可以自动推断出后面的

ArrayList<T>

的泛型类型必须是

ArrayList<String>

,因此,可以把代码简写为:

// 可以省略后面的Number,编译器可以自动推断泛型类型:List<String> list = new ArrayList<>();

3.1 泛型类

泛型类的语法形式:

class name<T1, T2, ..., Tn> { /* ... */ }

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。由尖括号(

56c<>

)分隔的类型参数部分跟在类名后面。它指定类型参数(也称为类型变量)T1,T2,…和 Tn。

一般将泛型中的类名称为原型,而将

<>

指定的参数称为类型参数

在泛型出现之前,一个类要想处理所有类型的数据,只能使用

Object

做数据转换。实例如下:

public class Info {private Object value;public Object getValue() {return value;}public void setValue(Object value) {this.value = value;}}

使用泛型之后,其实就是将

Object

换成

T

,并声明

<T>

public class Info<T> {private T value;public T getValue() {return value;}public void setValue(T value) {this.value = value;}}

在上面的例子中,在初始化一个泛型类时,使用

<>

指定了内部具体类型,在编译时就会根据这个类型做强类型检查。

实际上,不使用

<>

指定内部具体类型,语法上也是支持的(不推荐这么做),这样的调用就失去泛型类型的优势。如下所示:

public static void main(String[] args) {Info info = new Info();info.setValue(10);System.out.println(info.getValue());info.setValue(\"abc\");System.out.println(info.getValue());}

上面是单个类型参数的泛型类

下面我们看一下多个类型参数的泛型类该如何编写。

例如,我们定义

Pair

不总是存储两个类型一样的对象,就可以使用类型

<T, K>

public class Pair<T, K> {private T first;private K last;public Pair(T first, K last) {this.first = first;this.last = last;}public T getFirst() {return first;}public K getLast() {return last;}}

使用的时候,需要指出两种类型:

Pair<String, Integer> p = new Pair<>(\"test\", 123);

Java标准库的

Map<K, V>

就是使用两种泛型类型的例子。它对Key使用一种类型,对Value使用另一种类型。

小结

编写泛型时,需要定义泛型类型

<T>

泛型可以同时定义多种类型,例如

Map<K, V>

3.2 泛型接口

接口也可以声明泛型。

泛型接口语法形式:

public interface Content<T> {T text();}

泛型接口有两种实现方式:

  • 实现接口的子类明确声明泛型类型

    预先声明继承的具体类型的接口类,下面就是继承的

    Integer

    类型的接口类。

    public class IntContent implements Content<Integer> {private int text;public IntContent(int text) {this.text = text;}@Overridepublic Integer text() {return text;}}

    因为子类并没有泛型类型,所以正常使用就行。

    InContent ic = new IntContent(10);
  • 实现接口的子类不明确声明泛型类型

    public class GenericsContent<T> implements Content<T> {private T text;public GenericsContent(T text) {this.text = text;}@Overridepublic T text() {return text;}}

    此时子类也使用了泛型类型,就需要指定具体类型

    Content<String> gc = new GenericsContent<>(\"ABad8C\");

3.3 泛型方法

泛型方法是引入其自己的类型参数的方法。泛型方法可以是普通方法、静态方法以及构造方法。

泛型方法语法形式如下:

public <T> T func(T obj) {}

注意:是否拥有泛型方法,与其所在的类是否是泛型没有关系。

泛型方法的语法包括一个类型参数列表,在尖括号内,它出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际类型参数的占位符。

使用泛型方法的时候,通常不必指明类型参数,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference)。类型推断只对赋值操作有效,其他时候并不起作用。如果将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行推断。

编译器会认为:调用泛型方法后,其返回值被赋给一个 Object 类型的变量。

public class GenericsMethodDemo01 {public static <T> void printClass(T obj) {System.out.println(obj.getClass().toString());}public static void main(String[] args) {printClass(\"abc\");printClass(10);}}// Output:// class java.lang.String// class java.lang.Integer

泛型方法中也可以使用可变参数列表

public class GenericVarargsMethodDemo {public static <T> List<T> makeList(T... args) {List<T> result = new ArrayList<T>();Collections.addAll(result, args);return result;}public static void main(String[] args) {List<String> ls = makeList(\"A\");System.out.println(ls);ls = makeList(\"A\", \"B\", \"C\");System.out.println(ls);}}// Output:// [A]// [A, B, C]

4. 泛型的特性

4.1 类型擦除(Type Erasure)

Java 语言引入泛型是为了在编译时提供更严格的类型检查,并支持泛型编程。不同于 C++ 的模板机制,Java 泛型是使用类型擦除来实现的,使用泛型时,任何具体的类型信息都被擦除了

那么,类型擦除做了什么呢?它做了以下工作:

  • 把泛型中的所有类型参数替换为 Object,如果指定1044类型边界,则使用类型边界来替换。因此,生成的字节码仅包含普通的类,接口和方法。
  • 擦除出现的类型声明,即去掉
    <>

    的内容。比如

    T get()

    方法声明就变成了

    Object get()

    List<String>

    就变成了

    List

    。如有必要,插入类型转换以保持类型安全。

  • 生成桥接方法以保留扩展泛型类型中的多态性。类型擦除确保不为参数化类型创建新类;因此,泛型不会产生运行时开销。

Java 泛型的实现方式不太优雅,但这是因为泛型是在 JDK5 时引入的,为了兼容老代码,必须在设计上做一定的折中。

简单来说类型擦除是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

例如,我们编写了一个泛型类

Pair<T>

,这是编译器看到的代码:

public class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}}

而虚拟机根本不知道泛型。这是虚拟机执行的代码:

public class Pair {private Object first;private Object last;public Pair(Object first, Object last) {this.first = first;this.last = last;}public Object getFirst() {return first;}public Object getLast() {return last;}}

因此,Java使用类型擦拭实现泛型,导致了:

  • 编译器把类型
    <T>

    视为

    Object

  • 编译器根据
    <T>

    实现安全的强制转型。

因此,Java使用擦拭法实现泛型,导致了:

  • 编译器把类型
    <T>

    视为

    Object

  • 编译器根据
    <T>

    实现安全的强制转型。

使用泛型的时候,我们编写的代码也是编译器看到的代码:

Pair<String> p = new Pair<>(\"Hello\", \"world\");String first = p.getFirst();String last = p.getLast();

而虚拟机执行的代码并没有泛型:

Pair p = new Pair(\"Hello\", \"world\");String first = (String) p.getFirst();String last = (String) p.getLast();

所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型

T

视为

Object

处理,但是,在需要转型的时候,编译器会根据

T

的类型自动为我们实行安全地强制转型。

泛型的局限

了解了Java泛型的实现方式——类型擦除,我们就知道了Java泛型的局限:

局限一

<T>

不能是基本类型,例如

int

,因为实际类型是

Object

Object

类型无法持有基本类型:

Pair<int> p = new Pair<>(1, 2); // compile error!

局限二:无法取得带泛型的

Class

。观察以下代码:

public class test {public static void main(String[] args) {List<Object> list1 = new ArrayList<Object>();List<String> list2 = new ArrayList<String>();System.out.println(list1.getClass());System.out.println(list2.getClass());}}// Output:// class java.util.ArrayList// class java.util.ArrayList

因为

T

Object

,我们对

ArrayList<Object>

ArrayList<String>

类型获取

Class

时,获取到的是同一个

Class

,也就是

ArrayList类的Class

换句话说,所有泛型实例,无论

T

的类型是什么,

getClass()

返回同一个

Class

实例,因为编译后它们全部都是

ArrayList<Object>

局限三:无法判断带泛型的类型:

List<Integer> p = new ArrayList<>();// Compile error:if (p instanceof List<String>) {}

原因和前面一样,并不存在

List<String>.class

,而是只有唯一的

List.class

泛型和继承

正是由于泛型时基于类型擦除实现的,所以,泛型类型无法向上转型

向上转型是指用子类实例去初始化父类,这是面向对象中多态的重要表现。

Integer

继承了

Object

ArrayList

继承了

List

;但是

List<Interger>

却并非继承了

List<Object>

这是因为,泛型类并没有自己独有的

Class

类对象。比如:并不存在

List<Object>.class

或是

List<Interger>.class

,Java 编译器会将二者都视为

List.class

4.2 上边界

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类

extend通配符

为泛型添加上边界,即传入的类型实参必须是指定类型的子类型

// 可以限制传入方法的参数的类型<? extends xxx>// 也可以限制T的类型<T extends XXX>// 类型边界可以设置多个,语法形式如下:<T extends B1 & B2 & B3>

注意:extends 关键字后面的第一个类型参数可以是类或接口,其他类型参数只能是接口。

<? extends xxx>

举个例子:

public class test {public static void main(String[] args) {Pair<Integer> p = new Pair<>(123, 456);int n = add(p);System.out.println(n);}static int add(Pair<? extends Number> p) {Number first = p.getFirst();Number last = p.getLast();return first.intValue() + last.intValue();}}class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}}

通过使用

<? extends Number>

,我们可以传入

Number

类型的子类类型的数组。就可以执行数值类型的加法。

这种使用

<? extends Number>

的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型

T

的上界限定在

Number

了。除了可以传入

Pair<Integer>

类型,我们还可以传入

Pair<Double>

类型,

Pair<BigDecimal>

类型等等,因为

Double

BigDecimal

都是

Number

的子类。

如果我们考察对

Pair<? extends Number>

类型调用

getFirst()

方法,实际的方法签名变成了:

<? extends Number> getFirst();

接下来,我们再来考察一下

Pair<T>

set

方法:

public class test {public static void main(String[] args) {Pair<Integer> p = new Pair<>(123, 456);int n = add(p);System.out.println(n);}static int add(Pair<? extends Number> p) {Number first = p.getFirst();Number last = p.getLast();p.setFirst(new Integer(first.intValue() + 100));p.setLast(new Integer(last.intValue() + 100));return p.getFirst().intValue() + p.getFirst().intValue();}}class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}public void setFirst(T first) {this.first = first;}public void setLast(T last) {this.last = last;}}// 会得到一个编译错误// The method setFirst(capture#3-of ? extends Number) in the type Pair<capture#3-of ? extends Number> is not applicable for the arguments (int)Java(67108979)

编译错误的原因在于,如果一开始我们传入的

p

Pair<Double>

,显然它满足参数定义

Pair<? extends Number>

,然而,

Pair<Double>

setFirst()

显然无法接受

Integer

类型。

这就是

<? extends Number>

通配符的一个重要限制:方法参数签名

setFirst(? extends Number)

无法传递任何

Number

的子类型给

setFirst(? extends Number)

这里唯一的例外是可以给方法参数传入

null

p.setFirst(null); // ok, 但是后面会抛出NullPointerExceptionp.getFirst().intValue(); // NullPointerException

使用extends限定T类型

在定义泛型类型

Pair<T>

的时候,也可以使用

extends

通配符来限定

T

的类型:

public class Pair<T extends Number> { ... }

现在,我们只能定义:

Pair<Number> p1 = null;Pair<Integer> p2 = new Pair<>(1, 2);Pair<Double> p3 = null;

因为

Number

Integer

Double

都符合

<T extends Number>

Number

类型将无法通过编译:

Pair<String> p1 = null; // compile error!Pair<Object> p2 = null; // compile error!

因为

String

Object

都不符合

<T extends Number>

,因为它们不是

Number

类型或

Number

的子类。小结

使用类似

<? extends Number>

通配符作为方法参数时表示:

  • 方法内部可以调用获取
    Number

    引用的方法,例如:

    Number n = obj.getFirst();

  • 方法内部无法调用传入
    Number

    引用的方法(

    null

    除外),例如:

    obj.setFirst(Number n);

即一句话总结:使用

extends

通配符表示可以读,不能写。

使用类似

<T extends Number>

定义泛型类时表示:

  • 泛型类型限定为
    Number

    以及

    Number

    的子类。

4.3 下边界

super

下界通配符

将未知类型限制为该类型的特定类型或超类类型。

extends

通配符相反,这次,我们希望接受

Pair<Integer>

类型,以及

Pair<Number>

Pair<Object>

,因为

Number

Object

Integer

的父类,

setFirst(Number)

setFirst(Object)

实际上允许接受

Integer

类型。

我们使用

super

通配符来改写这个方法:

void set(Pair<? super Integer>ad8p, Integer first, Integer last) {p.setFirst(first);p.setLast(last);}

注意到

Pair<? super Integer>

表示,方法参数接受所有泛型类型为

Integer

Integer

父类的

Pair

类型。

这里注意到我们无法使用

Integer

类型来接收

getFirst()

的返回值,即下面的语句将无法通过编译:

Integer x = p.getFirst();

因为如果传入的实际类型是

Pair<Number>

,编译器无法将

Number

类型转型为

Integer

因此,使用

<? super Integer>

通配符表示:

  • 允许调用
    set(? super Integer)

    方法传入

    Integer

    的引用;

  • 不允许调用
    get()

    方法获得

    Integer

    的引用。

唯一例外是可以获取

Object

的引用:

Object o = p.getFirst()

换句话说,使用

<? super Integer>

通配符作为方法参数,表示方法内部代码对于参数只能写,不能读。

对比extends和super通配符

我们再回顾一下

extends

通配符。作为方法参数,

<? extends T>

类型和

<? super T>

类型的区别在于:

  • <? extends T>

    允许调用读方法

    T get()

    获取

    T

    的引用,但不允许调用写方法

    set(T)

    传入

    T

    的引用(传入

    null

    除外);

  • <? super T>

    允许调用写方法

    set(T)

    传入

    T

    的引用,但不允许调用读方法

    T get()

    获取

    T

    的引用(获取

    Object

    除外)。

一个是允许读不允许写,另一个是允许写不允许读。

4.4 无限定通配符

我们已经讨论了

<? extends T>

<? super T>

作为方法参数的作用。实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个

?

void sample(Pair<?> p) {}

因为

<?>

通配符既没有

extends

,也没有

super

,因此:

  • 不允许调用
    set(T)

    方法并传入引用(

    null

    除外);

  • 不允许调用
    T get()

    方法并获取

    T

    引用(只能获取

    Object

    引用)。

无界通配符有两种应用场景:

    ad0

  • 可以使用 Object 类中提供的功能来实现的方法。
  • 使用不依赖于类型参数的泛型类中的方法。

语法形式:

<?>

public class GenericsUnboundedWildcardDemo {public static void printList(List<?> list) {for (Object elem : list) {System.out.print(elem + \" \");}System.out.println();}public static void main(String[] args) {List<Integer> li = Arrays.asList(1, 2, 3);List<String> ls = Arrays.asList(\"one\", \"two\", \"three\");printList(li);printList(ls);}}// Output:// 1 2 3// one two three

小结

使用类似

<? super Integer>

通配符作为方法参数时表示:

  • 方法内部可以调用传入
    Integer

    引用的方法,例如:

    obj.setFirst(Integer n);

  • 方法内部无法调用获取
    Integer

    引用的方法(

    Object

    除外),例如:

    Integer n = obj.getFirst();

即使用

super

通配符表示只能写不能读。

无限定通配符

<?>

很少使用,可以用

<T>

替换,同时它是所有

<T>

类型的超类。

4.5 泛型命名

泛型一些约定俗成的命名(实际并无意义,但是建议对应着来命名泛型):

  • E - Element
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

5. end

理解泛型之后可以方便我们更好的阅读Java框架的源码,实际编程来说不一定会用到,但是可以用到泛型编程的地方,建议使用,可以简化代码。

6. 参考资料

  1. 廖雪峰Java
  2. 深入理解Java泛型
  3. Oracle Java文档
赞(0) 打赏
未经允许不得转载:爱站程序员基地 » Java 基础 一文搞懂泛型