0
点赞
收藏
分享

微信扫一扫

Java程序员必备基础:泛型解析

前言

整理一下Java泛型的相关知识,算是比较基础的,希望大家一起学习进步。Java程序员必备基础:泛型解析_java

一、什么是Java泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性,其本质是参数化类型,解决不确定具体对象类型的问题。其所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

泛型类

泛型类(generic class) 就是具有一个或多个类型变量的类。一个泛型类的简单例子如下:

    1. ​​//常见的如T、E、K、V等形式的参数常用于表示泛型,编译时无法知道它们类型,实例化时需要指定。​​
    2. ​​public class Pair <K,V>{​​
    3. ​​ private K first;​​
    4. ​​ private V second;​​
    5.
    6. ​​ public Pair(K first, V second) {​​
    7. ​​ this.first = first;​​
    8. ​​ this.second = second;​​
    9. ​​ }​​
    10.
    11. ​​ public K getFirst() {​​
    12. ​​ return first;​​
    13. ​​ }​​
    14.
    15. ​​ public void setFirst(K first) {​​
    16. ​​ this.first = first;​​
    17. ​​ }​​
    18.
    19. ​​ public V getSecond() {​​
    20. ​​ return second;​​
    21. ​​ }​​
    22.
    23. ​​ public void setSecond(V second) {​​
    24. ​​ this.second = second;​​
    25. ​​ }​​
    26.
    27. ​​ public static void main(String[] args) {​​
    28. ​​ // 此处K传入了Integer,V传入String类型​​
    29. ​​ Pair<Integer,String> pairInteger = new Pair<>(1, "第二");​​
    30. ​​ System.out.println("泛型测试,first is " + pairInteger.getFirst()​​
    31. ​​ + " ,second is " + pairInteger.getSecond());​​
    32. ​​ }​​
    33. ​​}​​

    运行结果如下:


    1. ​​泛型测试,first is 1 ,second is 第二​​

    泛型接口

    泛型也可以应用于接口。


    1. ​​public interface Generator<T> {​​
    2. ​​ T next();​​
    3. ​​}​​

    实现类去实现这个接口的时候,可以指定泛型T的具体类型。

    指定具体类型为Integer的实现类:


    1. ​​public class NumberGenerator implements Generator<Integer> {​​
    2.
    3. ​​ @Override​​
    4. ​​ public Integer next() {​​
    5. ​​ return new Random().nextInt();​​
    6. ​​ }​​
    7. ​​}​​

    指定具体类型为String的实现类:


    1. ​​public class StringGenerator implements Generator<String> {​​
    2.
    3. ​​ @Override​​
    4. ​​ public String next() {​​
    5. ​​ return "测试泛型接口";​​
    6. ​​ }​​
    7. ​​}​​

    泛型方法

    具有一个或多个类型变量的方法,称之为泛型方法。


    1. ​​public class GenericMethods {​​
    2.
    3. ​​ public <T> void f(T x){​​
    4. ​​ System.out.println(x.getClass().getName());​​
    5. ​​ }​​
    6.
    7. ​​ public static void main(String[] args) {​​
    8. ​​ GenericMethods gm = new GenericMethods();​​
    9. ​​ gm.f("字符串");​​
    10. ​​ gm.f(666);​​
    11. ​​ }​​
    12. ​​}​​

    运行结果:


    1. ​​java.lang.String​​
    2. ​​java.lang.Integer​​

    二、泛型的好处

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

    我们先来看看一个只能持有单个对象的类。


    1. ​​public class Holder1 {​​
    2. ​​ private Automobile a;​​
    3.
    4. ​​ public Holder1(Automobile a) {​​
    5. ​​ this.a = a;​​
    6. ​​ }​​
    7.
    8. ​​ public Automobile getA() {​​
    9. ​​ return a;​​
    10. ​​ }​​
    11. ​​}​​

    我们可以发现,这个类的重用性不怎样。要使它持有其他类型的任何对象,在jdk1.5泛型之前,可以把类型设置为Object,如下:


    1. ​​public class Holder2 {​​
    2. ​​ private Object a;​​
    3.
    4. ​​ public Holder2(Object a) {​​
    5. ​​ this.a = a;​​
    6. ​​ }​​
    7.
    8. ​​ public Object getA() {​​
    9. ​​ return a;​​
    10. ​​ }​​
    11.
    12. ​​ public void setA(Object a) {​​
    13. ​​ this.a = a;​​
    14. ​​ }​​
    15.
    16. ​​ public static void main(String[] args) {​​
    17. ​​ Holder2 holder2 = new Holder2(new Automobile());​​
    18. ​​ //强制转换​​
    19. ​​ Automobile automobile = (Automobile) holder2.getA();​​
    20. ​​ holder2.setA("测试泛型");​​
    21. ​​ String s = (String) holder2.getA();​​
    22. ​​ }​​
    23. ​​}​​

    我们引入泛型,实现功能那个跟Holder2类一致的Holder3,如下:


    1. ​​public class Holder3<T> {​​
    2.
    3. ​​ private T a;​​
    4.
    5. ​​ public T getA() {​​
    6. ​​ return a;​​
    7. ​​ }​​
    8.
    9. ​​ public void setA(T a) {​​
    10. ​​ this.a = a;​​
    11. ​​ }​​
    12.
    13. ​​ public Holder3(T a) {​​
    14. ​​ this.a = a;​​
    15. ​​ }​​
    16.
    17. ​​ public static void main(String[] args) {​​
    18. ​​ Holder3<Automobile> holder3 = new Holder3<>(new Automobile());​​
    19. ​​ holder3.setA("测试泛型");​​
    20. ​​ Automobile automobile = holder3.getA();​​
    21. ​​ }​​
    22. ​​}​​

    因此,泛型的好处很明显了:

    • 不用强制转换,因此代码比较简洁;(简洁性)
    • 代替Object来表示其他类型对象,与ClassCastException异常划清界限。(安全性)
    • 泛型使代码可读性增强。(可读性)

    三、泛型通配符

    我们定义泛型时,经常碰见T,E,K,V,?等通配符。本质上这些都是通配符,是编码时一种约定俗成的东西。当然,你换个A-Z中另一个字母表示没有关系,但是为了可读性,一般有以下定义:

    • ? 表示不确定的 java 类型
    • T (type) 表示具体的一个java类型
    • K V (key value) 分别代表java键值中的Key Value
    • E (element) 代表Element

    为什么需要引入通配符呢,我们先来看一个例子:

      1. ​​class Fruit{​​
      2. ​​ public int getWeigth(){​​
      3. ​​ return 0;​​
      4. ​​ }​​
      5. ​​}​​
      6. ​​//Apple是水果Fruit类的子类​​
      7. ​​class Apple extends Fruit {​​
      8. ​​ public int getWeigth(){​​
      9. ​​ return 5;​​
      10. ​​ }​​
      11. ​​}​​
      12.
      13. ​​public class GenericTest {​​
      14. ​​ //数组的传参​​
      15. ​​ static int sumWeigth(Fruit[] fruits) {​​
      16. ​​ int weight = 0;​​
      17. ​​ for (Fruit fruit : fruits) {​​
      18. ​​ weight += fruit.getWeigth();​​
      19. ​​ }​​
      20. ​​ return weight;​​
      21. ​​ }​​
      22.
      23. ​​ static int sumWeight1(List<? extends Fruit> fruits) {​​
      24. ​​ int weight = 0;​​
      25. ​​ for (Fruit fruit : fruits) {​​
      26. ​​ weight += fruit.getWeigth();​​
      27. ​​ }​​
      28. ​​ return weight;​​
      29. ​​ }​​
      30. ​​ static int sumWeigth2(List<Fruit> fruits){​​
      31. ​​ int weight = 0;​​
      32. ​​ for (Fruit fruit : fruits) {​​
      33. ​​ weight += fruit.getWeigth();​​
      34. ​​ }​​
      35. ​​ return weight;​​
      36. ​​ }​​
      37.
      38. ​​ public static void main(String[] args) {​​
      39. ​​ Fruit[] fruits = new Apple[10];​​
      40. ​​ sumWeigth(fruits);​​
      41. ​​ List<Apple> apples = new ArrayList<>();​​
      42. ​​ sumWeight1(apples);​​
      43. ​​ //报错​​
      44. ​​ sumWeigth2(apples);​​
      45. ​​ }​​
      46. ​​}​​

      我们可以发现,Fruit[]与Apple[]是兼容的。 ​List<Fruit>​​与 ​List<Apple>​​不兼容的,集合List是不能协变的,会报错,而 ​List<Fruit>​​与 ​List<?extendsFruits>​ 是OK的,这就是通配符的魅力所在。通配符通常分三类:

      • 无边界通配符,如 ​<?>
      • 上边界限定通配符,如 ​<?extendsE>​;
      • 下边界通配符,如 ​<?superE>​;

      ?无边界通配符

      无边界通配符,它的使用形式是一个单独的问号: ​List<?>​,也就是没有任何限定。

      看个例子:


      1. ​​public class GenericTest {​​
      2.
      3. ​​ public static void printList(List<?> list) {​​
      4. ​​ for (Object object : list) {​​
      5. ​​ System.out.println(object);​​
      6. ​​ }​​
      7. ​​ }​​
      8.
      9. ​​ public static void main(String[] args) {​​
      10. ​​ List<String> list1 = new ArrayList<>();​​
      11. ​​ list1.add("A");​​
      12. ​​ list1.add("B");​​
      13. ​​ List<Integer> list2 = new ArrayList<>();​​
      14. ​​ list2.add(100);​​
      15. ​​ list2.add(666);​​
      16. ​​ //报错,List<?>不能添加任何类型​​
      17. ​​ List<?> list3 = new ArrayList<>();​​
      18. ​​ list3.add(666);​​
      19. ​​ }​​
      20. ​​}​​

      无界通配符 ​(<?>)​可以适配任何引用类型,看起来与原生类型等价,但与原生类型还是有区别,使用无界通配符则表明在使用泛型 。同时, ​List<?>list​不可以添加任何类型,因为并不知道实际是哪种类型。但是List list因为持有的是Object类型对象,所以可以add任何类型的对象。

      上边界限定通配符 < ? extends E>

      使用 ​<?extendsFruit>​ 形式的通配符,就是上边界限定通配符。 extends关键字表示这个泛型中的参数必须是 E 或者 E 的子类,请看demo:


      1. ​​class apple extends Fruit{}​​
      2. ​​static int sumWeight1(List<? extends Fruit> fruits) {​​
      3. ​​ int weight = 0;​​
      4. ​​ for (Fruit fruit : fruits) {​​
      5. ​​ weight += fruit.getWeigth();​​
      6. ​​ }​​
      7. ​​ return weight;​​
      8. ​​}​​
      9. ​​public static void main(String[] args) {​​
      10. ​​ List<Apple> apples = new ArrayList<>();​​
      11. ​​ sumWeight1(apples);​​
      12. ​​}​​

      但是,以下这段代码是不可行的:


      1. ​​static int sumWeight1(List<? extends Fruit> fruits){​​
      2. ​​ //报错​​
      3. ​​ fruits.add(new Fruit());​​
      4. ​​ //报错​​
      5. ​​ fruits.add(new Apple());​​
      6. ​​}​​

      • 在 ​List<Fruit>​​里只能添加Fruit类对象及其子类对象(如Apple对象,Oragne对象),在 ​​List<Apple>​​里只能添加Apple类和其子类对象。
      • 我们知道 ​List<Fruit>、List<Apple>​​等都是List<? extends Fruit>的子类型。假设一开始传参是 ​​List<Fruit>list​​,两个添加没问题,那如果传来 ​​List<Apple>list​​,添加就失败了,编译器为了保护自己,直接禁用添加功能了。
      • 实际上,不能往 ​List<?extendsE>​添加任意对象,除了null。

      下边界限定通配符 < ? super E>

      使用 ​<?superE>​形式的通配符,就是下边界限定通配符。 super关键字表示这个泛型中的参数必须是所指定的类型E,或者是此类型的父类型,直至 Object。


      1. ​​public class GenericTest {​​
      2.
      3. ​​ private static <T> void test(List<? super T> dst, List<T> src){​​
      4. ​​ for (T t : src) {​​
      5. ​​ dst.add(t);​​
      6. ​​ }​​
      7. ​​ }​​
      8.
      9. ​​ public static void main(String[] args) {​​
      10. ​​ List<Apple> apples = new ArrayList<>();​​
      11. ​​ List<Fruit> fruits = new ArrayList<>();​​
      12. ​​ test(fruits, apples);​​
      13. ​​ }​​
      14. ​​}​​

      可以发现, ​List<?superE>​​添加是没有问题的,因为子类是可以指向父类的,它添加并不像 ​List<?extendsE>​会出现安全性问题,所以可行。

      四、泛型擦除

      什么是类型擦除

      什么是Java泛型擦除呢? 先来看demo:


      1. ​​Class c1 = new ArrayList<Integer>().getClass();​​
      2. ​​Class c2 = new ArrayList<String>().getClass();​​
      3. ​​System.out.println(c1 == c2);​​
      4. ​​/* Output​​
      5. ​​true​​
      6. ​​*/​​

      日常开发中, ​ArrayList<Integer>​​ 和 ​ArrayList<String>​​ 很容易被认为是不同的类型。但是这里输出结果是true,这是因为Java泛型是使用擦除实现的,不管是 ​ArrayList<Integer>()​​ 还是 ​newArrayList<String>()​,在编译生成的字节码中都不包含泛型中的类型参数,即都擦除成了ArrayList,也就是被擦除成“原生类型”,这就是泛型擦除。

      类型擦除底层

      Java泛型在编译期完成,它是依赖编译器实现的。其实,编译器主要做了这些工作:

      • set()方法的类型检验
      • get()处的类型转换,编译器插入了一个checkcast语句,

      再看个例子:

      1. ​​public class GenericTest<T> {​​
      2.
      3. ​​ private T t;​​
      4.
      5. ​​ public T get() {​​
      6. ​​ return t;​​
      7. ​​ }​​
      8.
      9. ​​ public void set(T t) {​​
      10. ​​ this.t = t;​​
      11. ​​ }​​
      12.
      13. ​​ public static void main(String[] args) {​​
      14. ​​ GenericTest<String> test = new GenericTest<String>();​​
      15. ​​ test.set("jay@huaxiao");​​
      16. ​​ String s = test.get();​​
      17. ​​ System.out.println(s);​​
      18. ​​ }​​
      19. ​​}​​
      20. ​​/* Output​​
      21. ​​jay@huaxiao​​
      22. ​​*/​​
      javap -c GenericTest.class反编译GenericTest类可得
      1. ​​public class generic.GenericTest<T> {​​
      2. ​​ public generic.GenericTest();​​
      3. ​​ Code:​​
      4. ​​ 0: aload_0​​
      5. ​​ 1: invokespecial #1 // Method java/lang/Object."<init>":()V​​
      6. ​​ 4: return​​
      7.
      8. ​​ public T get();​​
      9. ​​ Code:​​
      10. ​​ 0: aload_0​​
      11. ​​ 1: getfield #2 // Field t:Ljava/lang/Object;​​
      12. ​​ 4: areturn​​
      13.
      14. ​​ public void set(T);​​
      15. ​​ Code:​​
      16. ​​ 0: aload_0​​
      17. ​​ 1: aload_1​​
      18. ​​ 2: putfield #2 // Field t:Ljava/lang/Object;​​
      19. ​​ 5: return​​
      20.
      21. ​​ public static void main(java.lang.String[]);​​
      22. ​​ Code:​​
      23. ​​ 0: new #3 // class generic/GenericTest​​
      24. ​​ 3: dup​​
      25. ​​ 4: invokespecial #4 // Method "<init>":()V​​
      26. ​​ 7: astore_1​​
      27. ​​ 8: aload_1​​
      28. ​​ 9: ldc #5 // String jay@huaxiao​​
      29. ​​ 11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V​​
      30. ​​ 14: aload_1​​
      31. ​​ 15: invokevirtual #7 // Method get:()Ljava/lang/Object;​​
      32. ​​ 18: checkcast #8 // class java/lang/String​​
      33. ​​ 21: astore_2​​
      34. ​​ 22: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;​​
      35. ​​ 25: aload_2​​
      36. ​​ 26: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V​​
      37. ​​ 29: return​​
      38. ​​}​​

      • 看第11,set进去的是原始类型Object(#6);
      • 看第15,get方法获得也是Object类型(#7),说明类型被擦出了。
      • 再看第18,它做了一个checkcast操作,是一个String类型,强转。

      五、泛型的限制与局限

      使用Java泛型需要考虑以下一些约束与限制,其实几乎都跟泛型擦除有关。

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

      不能用类型参数代替基本类型。因此, 没有 ​Pair<double>​​, 只 有 ​Pair<Double>​。 当然, 其原因是类型擦除。擦除之后, Pair 类含有 Object 类型的域, 而 Object 不能存储 double值。

      运行时类型查询只适用于原始类型

      如,getClass()方法等只返回原始类型,因为JVM根本就不知道泛型这回事,它只知道原始类型。


      1. ​​if(a instanceof Pair<String>) //ERROR,仅测试了a是否是任意类型的一个Pair,会看到编译器ERROR警告​​
      2.
      3. ​​if(a instanceof Pair<T>) //ERROR​​
      4.
      5. ​​Pair<String> p = (Pair<String>) a;//WARNING,仅测试a是否是一个Pair​​
      6.
      7. ​​Pair<String> stringPair = ...;​​
      8. ​​Pair<Employee> employeePair = ...;​​
      9. ​​if(stringPair.getClass() == employeePair.getClass()) //会得到true,因为两次调用getClass都将返回Pair.class​​

      不能创建参数化类型的数组

      不能实例化参数化类型的数组, 例如:


      1. ​​Pair<String>[] table = new Pair<String>[10]; // Error​​

      不能实例化类型变量

      不能使用像 new T(...),newT[...] 或 T.class 这样的表达式中的类型变量。例如, 下面的 ​Pair<T>​ 构造器就是非法的:


      1. ​​public Pair() { first = new T(); second = new T(); } // Error ​​

      使用泛型接口时,需要避免重复实现同一个接口


      1. ​​interface Swim<T> {}​​
      2.
      3. ​​class Duck implements Swim<Duck> {}​​
      4.
      5. ​​class UglyDuck extends Duck implements Swim<UglyDuck> {}​​

      可以消除对受查异常的检查


      1. ​​@SuppressWamings("unchecked") ​​
      2. ​​public static <T extends Throwable〉void throwAs(Throwable e) throws T { throw (T) e; }​​

      定义API返回报文时,尽量使用泛型;


      1. ​​public class Response<T> extends BaseResponse {​​
      2. ​​ private static final long serialVersionUID = -xxx;​​
      3.
      4. ​​ private T data;​​
      5.
      6. ​​ private String code;​​
      7.
      8. ​​ public Response() {​​
      9. ​​ }​​
      10.
      11. ​​ public T getData() {​​
      12. ​​ return this.data;​​
      13. ​​ }​​
      14.
      15. ​​ public void setData(T data,String code ) {​​
      16. ​​ this.data = data;​​
      17. ​​ this.code = code;​​
      18. ​​ }​​
      19. ​​}​​

      六、Java泛型常见面试题

      Java泛型常见几道面试题

      • Java中的泛型是什么 ? 使用泛型的好处是什么?(第一,第二小节可答)
      • Java的泛型是如何工作的 ? 什么是类型擦除 ? (第四小节可答)
      • 什么是泛型中的限定通配符和非限定通配符 ? (第三小节可答)
      • List和List 之间有什么区别 ?(第三小节可答)
      • 你了解泛型通配符与上下界吗?(第三小节可答)

      个人公众号

      Java程序员必备基础:泛型解析_泛型_02

      • 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。
      • 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,大家一起学习进步哈。


      举报

      相关推荐

      0 条评论