java泛型深度解读

简介: 简介泛型是Java SE 1.5的新特性,泛型的本质是参数化类型 ( type parameters ),也就是说所操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法的创建中.

简介

泛型是Java SE 1.5的新特性,泛型的本质是参数化类型 ( type parameters ),也就是说所操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法的创建中.

在泛型类中定义参数化类型,在泛型表达式中,需要指定具体类型,即泛型在使用过程中将会被替换为具体的类型.

// 定义 参数类型
class ArrayList<E>
// 使用中 指定具体类型
ArrayList<String> list = new ArrayList<>();

原始类型(raw type) : 就是去掉参数类型后的类,如示例中的ArrayList.

为什么需要泛型

我们来看一个例子:

        List list = new ArrayList();
        // 下列的添加方法完全没问题
        list.add("one");
        list.add(1);

        // 取的时候, 如果你小心的,也没问题
        // 需要强转, 内部是以Object引用来存放
        String s = (String) list.get(0);
        int    i = (int) list.get(1);

        // 但是如果, 不小心在获取时 类型判断出错的话
        for (int index = 0; index < list.size(); index++) {
            String str = (String) list.get(index);
            // index = 1时, 抛出java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
        }

上诉方式,有两个问题.

  1. 由于java是静态语言,应该尽量避免在一个容器数组中,添加不相干的类型实例.否则可能引起类型转换错误.
  2. 这种方式,没有类型检查,只能够在运行时候,系统抛出异常后,你才会发现错误.

接下来使用泛型:

        List<Animal> list = new ArrayList<>();
        // 可以添加Animal及其子类
        list.add(new Animal());
        list.add(new Tiger());
        // 编译器进行类型检査,避免插人错误类型的对象
        // 编译时期报错,
        list.add("one");

可以看出,泛型只允许添加 声明的类及其子类,其他无关类无法加入到list中,并且尝试将其他类型加入列表,将在编译时直接报错.

由此可以看出泛型的特点:

  1. 能够对类型进行限定
  2. 在编译期对类型进行检查,编译时报错
  3. 对于获取明确的限定类型,无需进行强制类型转化
  4. 具有良好的可读性和安全性

泛型类

一个泛型类 ( generic class ) 就是具有一个或多个类型变量的类.定义的变量用尖括号 <> 括起来,放在类名的后面.

public class Holder<T> {
    private T obj;
    public Holder(T t) {
        obj = t;
    }
    public void put(T t) {
        obj = t;
    }
    public T get() {
        return obj;
    }
}

泛型定义的类型变量,可以在 成员变量, 方法参数, 局部变量, 方法返回值中使用.
要注意的是,静态变量和静态方法中,不能使用类中定义的泛型参数.

这里的 T 可以代表任意类型(Object或其子类),需要注意的是,基本数据类型不能够使用泛型,需要使用它们对应的包装类(wrapper Class)

用具体的类型替换类型变量就可以实例化泛型类型,如

Holder<String> holder = new Holder<>();

泛型接口

泛型接口与泛型类对比区别别是,泛型接口中不能使用 类型参数作为成员变量.

泛型类的继承

当父类为泛型类或者接口时,子类可以使用具体类型来继承父类,也可以使用类型参数继承父类

public interface Parent<T> {
    ...
// ======
// 使用具体类型来继承父类
public class Son implements Parent<Animal> {
    ...
// ======
// 使用类型参数继承父类
public class Son<E> implements Parent<E> {
    ...

但是要注意, 一个类不能实现同一个泛型接口的两种变体,由于类型擦除的原因,这两个变体会成为县宫廷的接口

// Error
public class Son implements Parent<Animal> {
    ...
// ========= Error
public class Child extends Son implements Parent<String> {
    ...

这种方式,Child是实现了Parent<Animal>Parent<String>,是不允许的.

泛型方法

除了泛型类,还可以声明一个泛型方法. 泛型方法可以在泛型类中声明,也可以在普通方法中声明.

注意的是,静态方法中,只能使用方法中定义的类型参数,而不能使用泛型类中的类型参数.

// 普通类中的泛型方法
public class Normal {
    // 成员泛型方法
    public <E> String getString(E e) {
        return e.toString();
    }
    // 静态泛型方法
    public static <V> void printString(V v) {
        System.out.println(v.toString());
    }
}
// 泛型类中的泛型方法
public class Generics<T> {
    // 成员泛型方法
    public <E> String getString(E e) {
        return e.toString();
    }
    // 静态泛型方法
    public static <V> void printString(V v) {
        System.out.println(v.toString());
    }
}

一个原则:在能达到目的的情况下,尽量使用泛型方法。即,如果使用泛型方法可以取代将整个类泛化,那么应该有限采用泛型方法。

泛型类中的参数类型和泛型方法中的参数类型,即使声明为相同的类型参数,如T,两者的类型不会相互影响,甚至可以说没有任何关联.方法中的类型,由传入的参数决定,与泛型类的类型无关.

// 泛型类中的类型参数 用 T 表示
public class Holder<T> {
    ...
    // 成员泛型方法, 声明的类型参数,也用 T 表示
    public <T> String getString(T t) {
        return t.toString();
    }
}
public static void main(String[] args) {
        // 泛型类为 Animal
        Holder<Animal> holder = new Holder<>(new Animal());
        // 泛型方法为 Vegetation
        String s = holder.getString(new Vegetation());
        System.out.println(s);
        // I'm Vegetation
    }

泛型方法的使用过程中,无需对类型进行声明,它可以根据传入的参数,自动判断.

public class Main {
    public static void main(String[] args) {
        // 指定类型
        Main.<String>printString("one");
        // 不指定,自动推倒
        Main.printString("two");
    }
    static <T> void printString(T t) {
        System.out.println(t.toString());
    }
}

类型变量的限定

对于类型变量没有限定的泛型类或方法, 它是默认继承自Object,当没有传入具体类型时,它有的能力只有Object类中的几个默认方法实现.

如果我们要实现一个方法, 传入两个参数,返回其中大的一个,即max()函数.

public static void main(String[] args) {
        // 传入 4 , 2 , 自动装箱成Integer类
        int r = max(4, 2);
    }
static <T> T max(T t1,T t2){
        // Cannot resolve method 'compareTo(T)'
        return t1.compareTo(t2) > 0 ? t1 : t2;
    }

如果没有对类型进行限定,它默认只有Object能力,它没有compareTo方法,因此没有比较能力,此时,即使在调用的时候传入可以比较的对象, max方法会在编译器报错.

此时, 我们就需要对 类型参数进行限定,让它能够默认拥有一些类的"能力".

    public static void main(String[] args) {
        // 传入 4 , 2 , 自动装箱成Integer类
        int r = max(4, 2);
        // r = 4
    }
    // 继承 Comparable 的类具有比较功能,能够比较大小 , 该函数返回传入的最大值
    static <T extends Comparable<T>> T max(T t1, T t2) {
        return t1.compareTo(t2) > 0 ? t1 : t2;
    }

从代码中可以看出, T 被限定为Comparable的子类(Comparable类本身是泛型类,也需要对他进行类型参数声明,否则会引发编译警告.),因此它拥有了 父类Comparable有的能力,即比较功能,这样我们才能得到正确的结果.

类型参数的限定 可以记为 <T extends BoundingType>,由于java有单继承类多实现接口的特点,因此还可以有多个限定. <T extends BoundingType1 & BoundingType2 & ...>

在 Java 的继承中, 可以拥有多个接口超类型, 但限定中至多有一个类。 如果用 一个类作为限定, 它必须是限定列表中的第一个.

泛型的实现原理

java中泛型的实现是采用 类型擦除 的方式实现.

所谓的类型擦除,就是程序在编译阶段,编译器会对泛型变量进行擦除(erased)操作,并替换为限定类型 (没有限定的变量用 Object);泛型类也将擦除为原始类型; 在泛型表达式中(泛型的使用),会将类型替换为具体的类型(此时将发生强制转换)

下面我们通过,反编译泛型类的方式来揭开类型擦除的面纱.
使用jad -sjava Holder.class来反编译Holder类.

// 泛型类
public class Holder<T> {
    private T obj;
    public void put(T t) {
        obj = t;
    }
    public T get() {
        return obj;
    }
}
// 泛型使用
public static void main(String[]args){
        Holder<Animal> holder=new Holder<>();
        holder.put(new Tiger());
        // 在使用过程中没有发生强转
        Animal animal=holder.get();
        }
// ---------------------------------
// 反编译出来的类, 它的类型被擦除为Object
public class Holder {
    private Object obj;
    public Holder() {
    }
    public void put(Object t) {
        obj = t;
    }
    public Object get() {
        return obj;
    }
}
public static void main(String args[]){
        Holder holder=new Holder();
        holder.put(new Tiger());
        // 泛型的使用过程中,使用强制转换为目标类型
        Animal animal=(Animal)holder.get();
        }
// -------------------------------------
// javap 反汇编对main方法,到处的指令码
public static void main(java.lang.String[]);
    Code:
        0:new           #2                  // class generics/Holder
        3:dup
        4:invokespecial #3                  // Method generics/Holder."<init>":()V
        7:astore_1
        8:aload_1
        9:new           #4                  // class bean/Tiger
        12:dup
        13:invokespecial #5                  // Method bean/Tiger."<init>":()V
        16:invokevirtual #6                  // Method generics/Holder.put:(Ljava/lang/Object;)V
        19:aload_1
        20:invokevirtual #7                  // Method generics/Holder.get:()Ljava/lang/Object;
        23:checkcast     #8                  // class bean/Animal
        26:astore_2
        27:return

由上述反编译的代码可以看出,泛型类被擦除为 原始类型;

泛型类中的类型参数变量也擦除为Object类型;

泛型的表达式中,发生了强制转换为目标类型.
从反汇编代码中可以看出,holder.get()方法,被分解为两条指令.
1. 原始类型调用方法,对应 invokevirtual指令
2. Object类型强制转换为Animal类型,对应 checkcast指令

如果限定为 T extends Animal,则类型参数变量将被擦除为限定类型Animal.

// 泛型类
public class Holder<T extends Animal> {
    private T obj;
    ...
// 反编译类
public class Holder {
    private Animal obj;
    ...

java泛型的局限

  1. 不能用基本类型实例化类型参数

泛型中的 类型参数在没有限定的情况下 是默认 擦除为 Object,而基本类型变量无法转化为 Object类型.

不过没关系, java中8中基本类型,都有其对应的包装类(Wrapper Class), 并且基本类型 使用参数传递是,将被 (自动装箱)AutoBoxing 为包装类型.

  1. 运行时,无法对类型参数进行检查

由于编译时,擦除了类型参数, 因此,所有的类型查询只产生原始类型.
因此,一下的语句是不可行的.

Holder<Tiger> holder = new Holder<>(new Tiger());
        // ERROR 无法对类型参数进行判断
        if (holder instanceof Holder<Tiger>) ;
        if (holder instanceof Holder<T>) ;

由于类型擦除,Holder<String>Holder<Animal>的实例,获取的类都是原始类,是一样的,所以他们的getClass()方法的返回是一样的.

  1. 不能直接创建参数化类型的数组

如这样的代码Holder<Animal>[] holders = new Holder<Animal>[2],是通过不了编译的.

但是,可以通过以下方式来创建数组,不会报错,只是受到警告

// 使用原始类型而后强制转换
Holder<Animal>[] holders = (Holder<Animal>[]) new Holder[2];
// 使用通配符而后强制转换
Holder<Animal>[] holders = (Holder<Animal>[]) new Holder<?>[2];
  1. 不能够实例化类型变量

这样的语句T t = new T()T t = new Object(),是通过不了编译的,一定要在泛型表达式中申明了具体类型,才能创建.

某些情况下,我们需要创建 参数类型的变量, 那么前提是一定要知道被创建的类型.可以通过以下两种方式来创建:

  • 反射创建
    // 使用(如) newObject(Animal.class);
    static <T> T newObject(Class<T> cls) throw Exception{
        return cls.newInstance();
    }
  • jdk8以后,可使用构造器表达式
    // 使用(如) newObject(Animal::new);
    static <T> T newObject(Supplier<T> constr) {
        return constr.get();
    }
  1. 不能构造泛型数组

不能直接实例化 类型数组,如T[] arr = new T[2].

但是可以这样 T[] arr = new Object[2], 原因是数组本身也有类型,用来监控存储在虚拟机
中的数组,这个类型会被擦除为Object.

虽然这种方式能够创建泛型数组,但是为了类型安全起见,最好提供构造器来实现泛型数据的创建.

    // 使用构造器
    // 使用(如) newArray(Animal[]::new, 2);
    static <T> T[] newArray(IntFunction<T[]> constr, int length) {
        return constr.apply(length);
    }
    // 使用反射
    // 使用(如) newArray(String.class, 2);
    static <T> T[] newArray(Class<T> cls, int length) throws Exception {
        return (T[]) Array.newInstance(cls, length);
    }
  1. 不能在静态变量和静态方法中,使用泛型类中的类型参数

如以下的方式都不允许

public class Test<T> {
    // Error
    private static T t;
    // Error
    public static T test() {
        T t;
    }
}
  1. 注意擦除后的冲突
  • 由于类型擦除,方法重写时,会下列冲突.
public class Holder<T> {
    public boolean equals(T t) {
        ...
    }

由于类型擦除,该方法会被擦除为boolean equals(Object t),这和Object类中的equals方法完全冲突了,返回值和方法名,参数都一致了.

这种冲突,解决方案只能是,将方法重命名!

  • 由于类型擦除,方法重载时,也可能发生冲突.

再观察下面的代码:

public interface Parent<T> {
    T get();
}
public class Son implements Parent<Animal> {
    @Override
    public Animal get() {
        return new Animal();
    }
}
public class Main {
    public static void main(String[] args) {
        Parent<Animal> parent = new Son();
        Animal animal = parent.get();
    }
}

父类中根据类型擦除, 拥有Object get()方法, 子类传入具体的类型参数,拥有Animal get()方法,且继承父类方法,所以子类中同时拥有这两个方法.

这两个方法,方法参数相同,就只有返回值类型不一样.
在java语法中,是不允许这样的两个方法同时在一个类中存在,会把他们认为是同一种方法(方法签名根据方法参数和方法名来确定).
但是jvm却能分辨,jvm的方法签名,是通过方法参数,方法返回值,方法名来确定的,所以jvm允许这样的方法存在, 因此,对于这种冲突,不需要我们自己来处理, jvm通过一种称为 Bridge Method的方式来实现这种方式下的多态调用冲突.

感兴趣的可以查看,笔者的另一篇文章java中多态的实现原理.

为什么使用 类型擦除来实现泛型

因为,泛型提出来时,已经是java1.5的版本,java已经经历过10年的发展,java遗留的代码量可想而知.

为了兼容这部分的(旧)代码,而不得不采用这种方式来实现.

通配符 <?> <? extends T> <? super T>

泛型通配符解释起来比较复杂,这里就不进行展开,感兴趣的可以查看笔者的另一篇文章java泛型通配符详解及实践

引用

  1. java核心技术 卷1(第10版)
目录
相关文章
|
1月前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
47 2
|
4天前
|
Java 编译器 容器
Java——包装类和泛型
包装类是Java中一种特殊类,用于将基本数据类型(如 `int`、`double`、`char` 等)封装成对象。这样做可以利用对象的特性和方法。Java 提供了八种基本数据类型的包装类:`Integer` (`int`)、`Double` (`double`)、`Byte` (`byte`)、`Short` (`short`)、`Long` (`long`)、`Float` (`float`)、`Character` (`char`) 和 `Boolean` (`boolean`)。包装类可以通过 `valueOf()` 方法或自动装箱/拆箱机制创建。
21 9
Java——包装类和泛型
|
7天前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
4天前
|
存储 安全 搜索推荐
Java中的泛型
【9月更文挑战第15天】在 Java 中,泛型是一种编译时类型检查机制,通过使用类型参数提升代码的安全性和重用性。其主要作用包括类型安全,避免运行时类型转换错误,以及代码重用,允许编写通用逻辑。泛型通过尖括号 `&lt;&gt;` 定义类型参数,并支持上界和下界限定,以及无界和有界通配符。使用泛型需注意类型擦除、无法创建泛型数组及基本数据类型的限制。泛型显著提高了代码的安全性和灵活性。
|
27天前
|
安全 Java Go
Java&Go泛型对比
总的来说,Java和Go在泛型的实现和使用上各有特点,Java的泛型更注重于类型安全和兼容性,而Go的泛型在保持类型安全的同时,提供了更灵活的类型参数和类型集的概念,同时避免了运行时的性能开销。开发者在使用时可以根据自己的需求和语言特性来选择使用哪种语言的泛型特性。
35 7
|
1月前
|
存储 算法 Java
14 Java集合(集合框架+泛型+ArrayList类+LinkedList类+Vector类+HashSet类等)
14 Java集合(集合框架+泛型+ArrayList类+LinkedList类+Vector类+HashSet类等)
38 2
14 Java集合(集合框架+泛型+ArrayList类+LinkedList类+Vector类+HashSet类等)
|
26天前
|
存储 安全 Java
如何理解java的泛型这个概念
理解java的泛型这个概念
|
30天前
|
存储 缓存 Java
|
1月前
|
安全 Java
【Java 第六篇章】泛型
Java泛型是自J2 SE 1.5起的新特性,允许类型参数化,提高代码复用性与安全性。通过定义泛型类、接口或方法,可在编译时检查类型安全,避免运行时类型转换异常。泛型使用尖括号`&lt;&gt;`定义,如`class MyClass&lt;T&gt;`。泛型方法的格式为`public &lt;T&gt; void methodName()`。通配符如`?`用于不确定的具体类型。示例代码展示了泛型类、接口及方法的基本用法。
10 0
|
1月前
|
Java
【Java基础面试四十五】、 介绍一下泛型擦除
这篇文章解释了Java泛型的概念,它解决了集合类型安全问题,允许在创建集合时指定元素类型,避免了类型转换的复杂性和潜在的异常。