泛型

2023/11/10 Java基础

# 1、泛型概念

# 1.1、泛型初识

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

先看个简单的泛型案例

public class TestClass<T> {
    private T value;
 
    public TestClass(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这是一个简单的类,只是类名称后加了<T>,并且value的类型也是T,这个T就是指代类型的意思,因为我们不确定这个value是什么类型,所以用T先代替,这就是泛型的简单案例。

泛型的本质是为了参数化类型。

# 1.2、泛型的好处

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

可以总结为以下四点:保证了类型的安全性、消除强制转换、提高程序的性能、提高了代码的重用性

  • 保证了类型的安全性

在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。

案例

没有泛型的情况下使用集合:

public static void noGeneric() {
    ArrayList names = new ArrayList();
    names.add("星云系测试");
    names.add(123); //编译正常
}
1
2
3
4
5

编译正常。但是运行使用的时候会报异常:ClassCastException

有泛型的情况下使用集合:

public static void useGeneric() {
    ArrayList<String> names = new ArrayList<>();
    names.add("星云系测试");
    names.add(123); //编译不通过
}
1
2
3
4
5

有了泛型后,定义好的集合names在编译的时候add(123)就会编译不通过。

相当于告诉编译器每个集合接收的对象类型是什么,编译器在编译期就会做类型检查,告知是否插入了错误类型的对象,使得程序更加安全,增强了程序的健壮性。

  • 消除强制转换

泛型的一个附带好处是,消除源代码中的许多强制类型转换,这使得代码更加可读,并且减少了出错机会。

还是举例说明,以下没有泛型的代码段需要强制转换:

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

当重写为使用泛型时,代码不需要强制转换:

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // no cast
1
2
3
  • 避免了不必要的装箱、拆箱操作,提高程序的性能

在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。

泛型变量固定了类型,使用的时候就已经知道是值类型还是引用类型,避免了不必要的装箱、拆箱操作。

object a=1;//由于是object类型,会自动进行装箱操作。
 
int b=(int)a;//强制转换,拆箱操作。这样一去一来,当次数多了以后会影响程序的运行效率。
1
2
3

使用泛型之后

public static T GetValue<T>(T a){
  return a;
}
 
public static void Main(){
  int b=GetValue<int>(1);//使用这个方法的时候已经指定了类型是int,所以不会有装箱和拆箱的操作。
}
1
2
3
4
5
6
7
  • 提高了代码的重用性

通用方法的使用,避免重复写代码。

定义了三个方法,都是相加后返回结果,但是类型不一致:

private static int add(int a, int b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

private static float add(float a, float b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

private static double add(double a, double b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法:

private static <T extends Number> double add(T a, T b) {
    System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
    return a.doubleValue() + b.doubleValue();
}
1
2
3
4

# 1.3、泛型分类

泛型分类是指在使用上的分类,可以分为以下三种:

  • 泛型类:通过在类定义时使用泛型类型参数,可以使类具有通用性。
  • 泛型接口:通过在接口定义时使用泛型类型参数,可以使接口具有通用性。
  • 泛型方法:通过在方法定义时使用泛型类型参数,可以使方法具有通用性。

# 1.4、泛型参数

先看一下JDK中两个泛型类源码:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    ...
}

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    ...
}
1
2
3
4
5
6
7
8
9

上面这些泛型类定义中的泛型参数E、K和V都是什么意思呢?其实这些参数名称是可以任意指定,就想方法的参数名一样可以任意指定,但是我们通常会起一个有意义的名称,让别人一看就知道是什么意思。泛型参数也一样,E一般是指元素,用来集合类中。

泛型参数

  • T:Type(Java 类),表示任意类型。
  • E:Element (在集合中使用,因为集合中存放的是元素)。
  • K:表示键(Key)。
  • V:表示值(Value)。
  • N:表示数值类型(Number)。
  • ? :表示不确定的java类型。

# 2、泛型的基本使用

# 2.1、泛型类

通过在类定义时使用泛型类型参数,可以使类具有通用性。

  • 语法
public class 类名 <泛型类型1,...> {
    
}
1
2
3

  • 泛型类型必须是引用类型(非基本数据类型)
  • 定义泛型类,在类名后添加一对尖括号,并在尖括号中填写类型参数,参数可以有多个,多个参数使用逗号分隔
  • 泛型类定义
public class GenericClass<T> {
    private T value;
 
    public GenericClass(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 测试类
//TODO 1:泛型类
GenericClass<String> name = new GenericClass<>("星云系测试泛型");
System.out.println(name.getValue());
 
GenericClass<Integer> number = new GenericClass<>(123);
System.out.println(number.getValue());

/**
 * 输出结果:
 * 星云系测试泛型
 * 123
 */
1
2
3
4
5
6
7
8
9
10
11
12

# 2.2、泛型接口

通过在接口定义时使用泛型类型参数,可以使接口具有通用性。

  • 语法
public interface 接口名称 <泛型类型1,...> {
    
}
1
2
3
  • 泛型接口定义
public interface GenericInterface<T> {
    void show(T value);
}

public class StringShowImpl implements GenericInterface<String> {
    @Override
    public void show(String value) {
        System.out.println(value);
    }
}
 
public class NumberShowImpl implements GenericInterface<Integer> {
    @Override
    public void show(Integer value) {
        System.out.println(value);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 测试类

使用泛型的时候,前后定义的泛型类型必须保持一致,否则会出现编译异常:

GenericInterface<String> genericInterface = new NumberShowImpl();//编译异常
1

或者干脆不指定类型,那么 new 什么类型都是可以的:

GenericInterface g1 = new NumberShowImpl();
GenericInterface g2 = new StringShowImpl();
1
2

# 2.3、泛型方法

方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。

  • 语法
修饰符 <泛型类型> 返回类型 方法名(泛型类型 变量名) {
    
}
1
2
3
  • 泛型方法定义
/**
 *
 * @param t 传入泛型的参数
 * @param <T> 泛型的类型
 * @return T 返回值为T类型
 * 说明:
 *   1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *   2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *   3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *   4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E等形式的参数常用于表示泛型。
 */
public <T> T genercMethod(T t){
    System.out.println(t.getClass());
    System.out.println(t);
    return t;
}
 
public static void main(String[] args) {
    // 这里的泛型跟下面调用的泛型方法可以不一样。
    GenericsClassDemo<String> genericString  = new GenericsClassDemo("helloGeneric"); 
    // 传入的是String类型,返回的也是String类型
    String str = genericString.genercMethod("hello");
    // 传入的是Integer类型,返回的也是Integer类型
    Integer i = genericString.genercMethod(123);
}
 
/**
 * 输出结果:
 * class java.lang.String
 * hello
 * 
 * class java.lang.Integer
 * 123
 */
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

这里可以看出,泛型方法随着我们的传入参数类型不同,他得到的类型也不同。泛型方法能使方法独立于类而产生变化。

# 2.4、泛型通配符

Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法, 主要有以下三类:

  • 无边界的通配符:表示类型参数可以是任何类型
  • 固定上边界的通配:表示类型参数必须是上限或上限之下
  • 固定下边界的通配符:表示类型参数必须是下限或下限之上

看个例子

//表示类型参数可以是任何类型
public class Apple<?>{}
 
//表示类型参数必须是A或者是A的子类
public class Apple<T extends A>{}
 
//表示类型参数必须是A或者是A的超类型
public class Apple<T supers A>{}
1
2
3
4
5
6
7
8
  • 无边界的通配符(Unbounded Wildcards)

  • 采用<?>格式, 比如List<?>
  • 无边界的通配符的主要作用就是让泛型能够接受未知类型的数据.
  • 固定上边界的通配符(Upper Bounded Wildcards)

  • 采用<? extends E>的形式
  • 使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。
  • 要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界。
  • 固定下边界的通配符(Lower Bounded Wildcards)

  • 采用<? super E>的形式
  • 使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据.。
  • 要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界。
  • 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界

# 2.5、泛型数组

我们以List为例来看看数组如何使用泛型:

List<String>[] list11 = new ArrayList<String>[10];                // 编译错误,非法创建 
List<String>[] list12 = new ArrayList<?>[10];                     // 编译错误,需要强转类型 
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10];    // OK,但是会有警告 
List<?>[] list14 = new ArrayList<String>[10];                     // 编译错误,非法创建 
List<?>[] list15 = new ArrayList<?>[10];                          // OK 
List<String>[] list6 = new ArrayList[10];                         // OK,但是会有警告
1
2
3
4
5
6

要如何定义使用泛型数组呢?

  • 讨巧的使用场景
public class GenericsDemo30{  
    public static void main(String args[]){  
        Integer i[] = fun1(1,2,3,4,5,6) ;   // 返回泛型数组  
        fun2(i) ;  
    }  

    public static <T> T[] fun1(T...arg){  // 接收可变参数  
        return arg ;            // 返回泛型数组  
    }  

    public static <T> void fun2(T param[]){   // 输出  
        System.out.print("接收泛型数组:") ;  
        for(T t:param){  
            System.out.print(t + "、") ;  
        }  
    }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 合理使用

通过反射创建数组

public ArrayWithTypeToken(Class<T> type, int size) {
    array = (T[]) Array.newInstance(type, size);
}
1
2
3

在 Java 中是不能创建一个确切的泛型类型的数组的,除非是采用通配符的方式且要做显式类型转换才可以。

# 3、深入理解泛型

# 3.1、类型擦除

Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了伪泛型的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的类型擦除(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。

  • 泛型的类型擦除原则

  • 消除类型参数声明,即删除<>及其包围的部分。
  • 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
  • 为了保证类型安全,必要时插入强制类型转换代码。
  • 自动产生桥接方法以保证擦除类型后的代码仍然具有泛型的多态性
  • 类型擦除案例
public class Caculate<T> {
    private T num;
}
1
2
3

我们定义了一个泛型类,定义了一个属性成员,该成员的类型是一个泛型类型,这个 T 具体是什么类型,我们也不知道,它只是用于限定类型的。

反编译一下这个 Caculate 类:

public class Caculate{
    public Caculate(){}
    private Object num;
}
1
2
3
4

发现编译器擦除 Caculate 类后面的两个尖括号,并且将 num 的类型定义为 Object 类型。

那么是不是所有的泛型类型都以 Object 进行擦除呢?大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了extends和super语法的有界类型,如:

public class Caculate<T extends String> {
    private T num;
}
1
2
3

  • 这种情况的泛型类型,num 会被替换为 String 而不再是 Object。
  • 这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Caculate 实例的时候只能限定 T 为 String 或者 String 的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用 String 进行类型擦除。
  • 实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。
  • 实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换,这一个过程就叫做『泛型翻译』。
  • 类型擦除总结

  • Java泛型的实现是伪泛型,是一种语法糖
  • 类型擦除在编译阶段进行
  • 类型擦除会将泛型类型转为原生态类型

# 3.2、基本类型不能作为泛型类型解析

因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储int值,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作。

ArrayList<int>     // error
ArrayList<Integer> // 编译通过
1
2

# 3.3、泛型类型不能实例化解析

T test = new T(); // ERROR
1

因为在 Java 编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了,此外由于T 被擦除为 Object,如果可以 new T() 则就变成了 new Object(),失去了本意。 如果我们确实需要实例化一个泛型,应该如何做呢?可以通过反射实现:

static <T> T newTclass (Class < T > clazz) throws InstantiationException, IllegalAccessException {
    T obj = clazz.newInstance();
    return obj;
}
1
2
3
4

# 3.4、泛型类中不能有静态方法和静态变量解析

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

案例

public class Test2<T> {    
    public static T one;   //编译错误    
    public static  T show(T one){ //编译错误    
        return null;    
    }    
}
1
2
3
4
5
6

因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

但是要注意区分下面的一种情况:

public class Test2<T> {    
    public static <T> T show(T one){ //这是正确的    
        return null;    
    }    
}
1
2
3
4
5

因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T。

# 3.5、通过反射获取泛型的参数类型

通过反射(java.lang.reflect.Type)获取泛型

java.lang.reflect.Type是Java中所有类型的公共高级接口, 代表了Java中的所有类型. Type体系中类型的包括:数组类型(GenericArrayType)、参数化类型(ParameterizedType)、类型变量(TypeVariable)、通配符类型(WildcardType)、原始类型(Class)、基本类型(Class), 以上这些类型都实现Type接口。

public class GenericType<T> {
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public static void main(String[] args) {
        GenericType<String> genericType = new GenericType<String>() {};
        Type superclass = genericType.getClass().getGenericSuperclass();
        //getActualTypeArguments 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
        Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; 
        System.out.println(type);//class java.lang.String
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

ParameterizedType 源码:

public interface ParameterizedType extends Type {
    // 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
    Type[] getActualTypeArguments();
    
    //返回当前class或interface声明的类型, 如List<?>返回List
    Type getRawType();
    
    //返回所属类型. 如,当前类型为O<T>.I<S>, 则返回O<T>. 顶级类型将返回null 
    Type getOwnerType();
}
1
2
3
4
5
6
7
8
9
10

参考:Java泛型详解,史上最全图文详解「建议收藏」 (opens new window)