【第19期】你了解Java泛型吗?

文摘   2024-09-30 07:44   广东  

1 什么是JAVA 泛型?

         泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本 质是参数化类型,也就是说所操作的数据类型被指定为一个参数。比如我们要写一个排序方法, 能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,我们就可以使用 Java 泛型。

2 引入泛型的动机

假设下,我们不用泛型,需要实现一个通用的类,用来处理不同类型的方法,我们则会用到Object来作为属性和方法参数,但是这样做有两个缺点:

  • 每次使用时都需要强制转换成想要的类型

  • 在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全

《Java 编程思想》中描述了,泛型出现是在于:创建容器类,JDK1.5泛型出来之后,很多集合用泛型保存不同类型的元素,概况的来讲主要有以下几点:

(1)类型安全

  • 泛型的主要目标是提高 Java 程序的类型安全
  • 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常
  • 符合越早出错代价越小原则

(2)消除强制类型转换

  • 泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换
  • 所得即所需,这使得代码更加可读,并且减少了出错机会

(3)为性能考虑

  • 由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改
  • 所有工作都在编译器中完成
  • 编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已

3 泛型使用

3.1 泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一 样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开

一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数, 这些类被称为参数化的类或参数化的类型。

public class A<T> { private T t; public void add(T t) { this.t = t; } public T get() { return t; }

3.2 泛型接口

和泛型类一样,泛型接口在接口名后添加类型参数,比如以下 PhoneInterface,接口声明类型后,接口方法就可以直接使用这个类型

public interface PhoneInterface<T>{
void doSomething(T t);
}

3.3 泛型方法(

使用泛型的方法,比如我下面写的一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数 类型,编译器适当地处理每一个方法调用。

 // 泛型方法 打印 Array public static < E > void printArray( E[] inputArray ) {  for ( E element : inputArray ){  System.out.printf( "%s ", element ); } }

(1)<? extends T>表示该通配符所代表的类型是 T 类型的子类。

(2)<? super T>表示该通配符所代表的类型是 T 类型的父类。

4 泛型通配符

泛型中有三种通配符形式:

(1)<?>无限制通配符

(2)<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类。

(3)<? super E> super 关键字声明了类型的下界,表示参数化类型可能是指定类型,或者是此类型的父类。

4.1 无限制通配符 < ?>

  • 要使用泛型,但是不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。

  • ? 和 Object 不一样,List<?> 表示未知类型的列表,而 List表示任意类型的列表。

  • 如传入1个 List,这时 List 的元素类型就是 String,想要往 List 里添加一个 Object,这当然是不可以的。

4.2 上界通配符 < ? extends E>

在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:

    • 如果传入的类型不是 E 或者 E 的子类,编辑不成功

    • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用

4.3 下界通配符 < ? super E>

在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类

4.4 通配符该怎么选择?

(1)无限制通配符 < ?> 和 Object 有些相似,用于表示无限制或者不确定范围的场景。

(2)< ? super E> 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象。

(3)< ? extends E> 用于灵活读取,使得方法可以读取 E 或 E 的任意子类型的容器对象

一言以蔽之:

    • 如果参数化类型结果要返回一个T,使用 < ? extends T>;(T 的子类)

    • 操作一个类型 T ,比如容器得是T的父类,就使用 < ? super T>;(T 的父类)

举个例子:

    //求容器最大元素private <E extends Comparable<? super E>> E getMaxListElement(List<? extends E> e){    if(e == null){        return null;    }     //迭代器返回的元素属于 E 的某个子类型       Iterator<? extends E> iterator = e.iterator();    E result = iterator.next();    while (iterator.hasNext()){     E next = iterator.next();      if(next.compareTo(result)>0){       result = next;      }    }    return result;}

    5 类型擦除

    Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛 型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个 过程就称为类型擦除。

    如在代码中定义的 List和 List等类型,在编译之后 都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。

    类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般 是 Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换 成具体的类。

    6 泛型规范

    (1)泛型的参数类型只能是类(包括自定义类),不能是简单类型。

    (2)同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

    (3)泛型的类型参数可以有多个

    (4)泛型的参数类型可以使用 extends 语句,习惯上称为“有界类型”

    (5)泛型的参数类型还可以是通配符类型,例如 Class

    7 特别注意

    (1)数组中不能使用泛型

    Array 不支持泛型,应使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能

    (2)Java 中 List和原始类型 List 区别

    • 在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查

    • 通过使用 Object 作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String 或 Integer

    • 你可以把任何带参数的类型传递给原始类型 List,但却不能把 List< String> 传递给接受 List< Object> 的方法,因为泛型的不可变性,会产生编译错误

    8 小结

    (1)使用 Object 来达到复用,会失去泛型在安全性和直观表达性上的优势

    (2) 泛型可以用在方法,类,接口,通过擦除实现

    (3)泛型只在编译时强化它的类型信息,而在运行时丢弃(或者擦除)它的元素类型信息。擦除使得使用泛型的代码可以和没有使用泛型的代码互用

    (4)Array 不支持泛型,建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而array无

    (5)可以把任何带参数的类型传递给原始类型 List,但却不能把 List< String> 传递给接受 List< Object> 的方法,因为泛型的不可变性,会产生编译错误


    一起探索架构技术,拥抱AI,欢迎与我交流!



    顶尖架构师栈
    大厂架构师,专注科技资讯,AI前沿信息,日常分享技术干货,程序员副业,职场三两事。
     最新文章