JAVA中的泛型

泛型

1. 什么是泛型

  • 泛型是JDK5中引入的一种参数化类型的特性

2. 泛型的作用

  1. 可以使得代码更加的健壮,将类型检查提前到编译期,便于更早的发现问题
  2. 使得代码更加的简洁,使得代码可以更方便的复用

3. 泛型的三种使用场景

  1. 泛型类

    class Generic_class<T> { }

  2. 泛型接口

    interface Generic_interface<T> { }

  3. 泛型方法

    <T> void Generic(T t) { }

    • 注意void function(T t) { } 这个函数并非泛型方法,这只是使用了泛型类型作为传参的普通方法;
/* 一、泛型类 */
class Generic_class<K, V> {
    public K key;
    public V value;
}

/* 二、泛型接口 */
interface Generic_interface<K, V> {
    public void setKey(K key);
    public V getValue();
}

/* 三、泛型方法 */
<K, V> void Generic_function(K key, V value) {}
static <K, V> void Generic_static_function(K key, V Value) {}

/* 四、使用了泛型类型的普通方法 */
class Generic_class<T> {
    //这个方法就不是泛型方法,而只是使用了泛型类型的普通方法
	void function(T t);
	//原因在于,该方法中的T,并非是可以由function决定的,这个T的类型是由泛型类Generic_class在实例化的时候决定的
}

4. 常见的类型变量名

The most commonly used type parameter names are:

  • E - Element (used extensively by the Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types
  • 如上类型变量名,只是一个约定俗成,实际并没有具体的含义

5. 类型参数和类型变量


Type Parameter and Type Argument Terminology: Many developers use the terms “type parameter” and “type argument” interchangeably, but these terms are not the same. When coding, one provides type arguments in order to create a parameterized type. Therefore, the T in Foo<T> is a type parameter and the String in Foo<String> f is a type argument. This lesson observes this definition when using these terms.

  • 如上定义,通常我们将 Foo<T> 中的 T 称作类型参数,而将 Foo<String> 中的 String 称作类型变量 ,亦即 实际类型参数
  • 其中 Foo<T> 称作泛型类型,而 Foo<String> 称作参数化类型

6. 原始类型

  • 定义:缺少实际类型变量泛型,如下代码中,就定义了一个原始类型

    class Box<T> {
        private T data;
    
        public void SetData(T t) {
            data = t;
        }
    
        public T GetData() {
            return data;
        }
    }
    
    public static void main(String[] args) {
    	Box<Integer> box = new Box<>();  //这个就是存在实际类型参数的
    	Box raw_box = new Box();		//这个就是原始类型
    	raw_box = box;            
    	
    	raw_box.SetData("123");
    }
    

    如上代码中的 Box raw_box = box; 就是原始类型,显然,泛型类型可以直接赋给原始类型,但是这样写,实际上是绕过了泛型的类型检查,如上代码,我们限定了 boxInteger ,但是通过原始类型 raw_boxString 类型赋值进去了,显然这并不是我们要的效果,所以我们应该尽量避免使用原始类型,而如果使用 javac -Xlint:unchecked XXX 进行编译,我们会得到如下警告信息:

    Generic_demo_01.java:74: 警告: [unchecked] 对作为原始类型Box的成员的SetData(T)的调用未经过检查
    raw_box.SetData(“123”);
    ^
    其中, T是类型变量:
    T扩展已在类 Box中声明的Object
    1 个警告

7. 受限类型

  • 类型限制,只能用 T extends XXX ,和通配符上下界不同,限制条件只能用 extends ,而没有 super 的用法!

  • 受限类型的作用:

    1. 限定泛型类型,如下例子:

      //定义一个泛型方法,限定其传参必须是Number及其子类
      static <V extends Number> void setValue(V value) { }
      //此处传参String类型,就会报错
      setValue("123");
      
    2. 保证方法调用,如下例子:

      static <V extends Number> void setValue(V value) {
              //业务逻辑
          	/* 因为限定V是Number及其子类,所以传参value一定拥有Number类的方法,所以可以保证能够调用到其中的intValue等方法 */
              value.intValue();
              //业务逻辑
          }
      
      //限制类型继承接口
      interface promise_func {
          public void function();
      }
      
      static <V extends promise_func> void test(V value) {
          //业务逻辑
          //原因如上
          value.function();
          //业务逻辑
      }
      
  • 具有多个限定类型的,顺序是有要求的,且限制类型是类的话只能有一种(受限于JAVA中的单继承)

    • 当存在多个限定类型的时候,如果限定类型中有类,则类必须放在最前面

    • 如果是多重限定,代表着对应类型参数必须是包含所有限定条件的实际类型,演示如下:

      //接口
      interface Generic_interface {
          public void test();
      }
      
      //继承接口和Number类的test类
      class test extends Number implements Generic_interface { .... }
      
      //泛型方法中,多重限定,类型必须是实现Number及接口的类
      static <V extends Number & Generic_interface> void setValue(V value) { ... }
      
      //实际传入test类满足要求
      setValue(new test());
      
      • tips:

        • 根据类型擦除机制,泛型最终都会被擦除,那么如果是多重限定的情况,擦除后泛型会被替换成第一个限定条件,即:

          Test<T extends A & B> {
              public T t;
          }
          
          //那么编译后,T会被替换成第一个限定条件A,即
          Test<A> {
              public A t;
          }
          
          //那么第二个参数有什么用呢?当使用到第二个限定条件的时候,编译器会做一个强制的类型转换,下面的例子是说明泛型真正实现的时候会存在强制类型转换的
          public class Theory {
              public static void main(String[] args) {
                  Map<String, String> map = new HashMap<>();
                  map.put("1111", "18");
                  System.out.println(map.get("1111"));
              }
          }
          
          /* 编译后生成的class文件,使用jd-gui 1.4.1工具解析 */
          public class Theory
          {
            public static void main(String[] args)
            {
              Map<String, String> map = new HashMap();
              map.put("1111", "18");
              System.out.println((String)map.get("1111"));   //这里就是强制转型得到的String
            }
          }
          

          注意:需要用旧一点的工具,新的的反编译工具太好了,已经可以把泛型也反编译出来了!

8. 推断算法

  • 推断算法尝试找到与所有参数一起使用的最基本的类型

9. 通配符

  • 通配符种类:

    1. 上限通配符:? extends XXX

    2. 下限通配符:? super XXX

    3. 无限通配符:?

      • 无限通配符的适用场景:

        1. 如果你正在编写一个可以使用 Object 类中提供的功能实现的方法;源码Class类中就存在大量这样的应用,比如下面的代码:

          public boolean isAssignableFrom(Class<?> cls) {
              if (this == cls) {
                  return true;  // Can always assign to things of the same type.
              } else if (this == Object.class) {
                  return !cls.isPrimitive();  // Can assign any reference to java.lang.Object.
              } else if (isArray()) {
                  return cls.isArray() && componentType.isAssignableFrom(cls.componentType);
              } else if (isInterface()) {
                  // Search iftable which has a flattened and uniqued list of interfaces.
                  Object[] iftable = cls.ifTable;
                  if (iftable != null) {
                      for (int i = 0; i < iftable.length; i += 2) {
                          if (iftable[i] == this) {
                              return true;
                          }
                      }
                  }
                  return false;
              } else {
                  if (!cls.isInterface()) {
                      for (cls = cls.superClass; cls != null; cls = cls.superClass) {
                          if (cls == this) {
                              return true;
                          }
                      }
                  }
                  return false;
              }
          }
          
        2. 当代码使用通用类中不依赖于类型参数的方法时。例如, List.sizeList.clear

  • 使用范围:

    1. 参数类型
    2. 字段类型
    3. 局部变量类型
    4. 返回类型(极少用,也尽量避免使用)
  • 通配符无法用作泛型方法调用,泛型类实例创建或超类的类型参数,即无法实现如下代码:

    /* 用作泛型方法 */
    <? extends Number> void test() {}
    
    /* 泛型类 */
    class <? extends Number> CLASS {}
    
  • PESC

    • 上限的限制(副作用) ------> 生产者:只能获取数据,不能消费数据 PE

      • 当使用 List<? extends Number> 后,该List就会变为只读的,即无法调用:List.add(Integer.valueOf(1))
        • 不能存放数据的原因:当我们定义了上界之后,首先这个 List 中存放的数据必定是对应上界的子类(或其本身),而向其中存放数据的时候,由于限定的是子类,所以我们无法确定传入的数据和限定的数据其之间的继承关系,而由于 JAVA 只支持 向上转型 所以这种情况下极有可能出现非法的向下转型的情况,显然这是不正确的,所以我们无法向使用了上界通配符的类中传入数据。
        • 可以读取数据的原因:与不能存放数据的原因正好相反的,因为定义了上界之后,我们可以确定的是这个List里面存放是必然是限制类型的子类(或其本身),那么由于 向上转型 的存在,我们只要用限制类型去接收List里面的数据,必定是能够成功的,或者限制类型的父类也是可以的。
      • 但是可以通过使用反射进行绕过
    • 下限的限制(副作用) -----> 消费者:只能消费数据,不能获取数据(确切来说是无法明确知晓获取的数据是什么类型) SC

      • 可以往内写数据,但是无法去读取数据

        • 可以写入数据的原因:当我们定义了下界之后,首先这个 List 中存放的数据必定是对应下界的父类(或其本身),这个时候向其中传入的数据,我们基本可以确认,会是该限制类型的子类,这是符合 向上转型 的原则,所以是可以向其中传入数据的,同样的,若是我们特意向其中放置限制类型的父类,同样是会报错的。

          /* 举例下界设值的限制 */
          class A {}
          class B extends A {}
          class C extends B {}
          
          class Test {
          
              public static void main(String[] args) {
                  List<? super B> list = new ArrayList<>();
                  list.add(new C());
                  list.add(new B());
                  
                  //报错,因为这样会存在可能为:B b = a的向下转型的不安全情况
                  list.add(new A());
              }
          }
          
          
          
        • 不可以读取数据的原因:与可以写入数据的原因正好相反,因为定义了下界,我们无法确定其中存放的数据类型和限制类型之间的继承关系,而这时候如果擅自采用某一种类型去接收List中的数据,那么极有可能出现 向下转型 的情况,显然这是不符合要求的,所以我们无法去获取数据,但是因为所有的类都是Object的子类,所以我们如果用Object类进行数据接收,是没有问题的。

    • 无限通配符的限制(副作用)

      • 类型退化,如有 List<?> ,不能(再往其中添加)使用任何带有 ?(T) 的类型的参数,只能往其中添加null

10. 类型擦除

  • JAVA是一种伪泛型,因为虚拟机并不支持泛型;作用是为了兼容低版本;JDK5才有的泛型;
  • 在代码编译时,编译器会将所有泛型类型擦除,这样就不存在新的类型到字节码,所有的泛型最终实际都变成了原始类型(或者限制类型),而这样就使得JAVA在运行时不存在泛型信息做到了兼容
  • JAVA编译器擦除泛型的具体做法:
    1. 检查泛型类型,获取目标类型
    2. 擦除泛型信息,替换为限定类型
      • 如果没有限定信息,就替换为 Object
    3. 在必要的时候插入类型转换以保持类型安全
    4. 生成桥方法以在拓展时保持多态性

11. 桥接

  • 因为JAVA的泛型属于伪泛型,实际是进行了类型擦除,即有类型限制时,会擦除类型成限制类型,而如果没有限制,则都会擦除成 object ,而为了维持 多态,如下代码就会有桥接函数出现

    class Mint<T> {
        private T m_data;
         
        public void SetData(T t) {
            m_data = t;
        }
          
    }
        
    class Beef extends Mint<Integer> {
        private Integer m_data;
    
        public void SetData(Integer t) {
            m_data = t;
        }
    }
    
    • 由于类型擦除的存在,父类 Mint 中的类型会被擦除为 Object ,即父类的方法变为 SetData(Object t) 但是子类限定了类型为 Integer ,而为了维持多态性,此时就会生成一个桥接方法,保证类型不变,如下为解析出来的class文件:
    Compiled from "Beef.java"
    class demo_0.Beef extends demo_0.Mint<java.lang.Integer> {
      demo_0.Beef();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method demo_0/Mint."<init>":()V
           4: return
     
      public void SetData(java.lang.Integer);
        Code:
           0: aload_0
           1: aload_1
           2: putfield      #2                  // Field m_data:Ljava/lang/Integer;
           5: return
      
      public void SetData(java.lang.Object);     												-----> 这个就是桥接方法
        Code:
           0: aload_0
           1: aload_1
           2: checkcast     #3                  // class java/lang/Integer  					 ---->这里有一个类型强转
           5: invokevirtual #4                  // Method SetData:(Ljava/lang/Integer;)V
           8: return
    }
    

12. 使用泛型的七个限制

  1. 泛型变量不能是基本数据类型
    • 限制于类型擦除,当不存在限制条件时,所有的泛型都会被擦除为object,但是基本数据类型并不是object的子类,所以无法将泛型变量设置为基本数据类型
  2. 无法创建类型参数的实例
    • 因为类型参数是不确定的,所以无法进行实例化
  3. 无法声明类型参数为静态变量
    • 因为类型参数都是在进行实例化时进行指定具体类型的,而静态变量无需实例化就可以使用,这就导致存在无法获知其具体类型的情况
  4. 无法对泛型使用Castsinstanceof
    • 因为类型擦除的存在,实际运行时,泛型类型都是被擦除的,而这样也就无法判断对象和类之间的归属了
  5. 无法创建泛型数组
    • 协变:所谓的协变即,B继承于A,那么B[]也是继承于A[]
    • 而在泛型数据中, B继承于A,*Plant并不是继承于Plant*的,即泛型是不支持协变的,这也就导致无法创建泛型数组
  6. 无法创建、捕获、抛出参数化类型的对象
    • 实例化对象的时候,都必须是指定类型参数的,而未知的参数化类型在实例化的时候是不被允许
  7. 无法重载具有泛型的函数
    • 类型擦除

13. 通配符的捕获和帮助方法

  • 所谓通配符的捕获:在某些场景下,编译器能够自己推断出通配符的类型,类似类型推断,例如:

    class Wildcard_catch<T> {
        private T data1;
        private T data2;
    
        public Wildcard_catch(T t1, T t2) {
            data1 = t1;
            data2 = t2;
        }
    
        public void SetData(T t1) {
            data1 = t1;
        }
    
        public T GetNumber() {
            return data2;
        }
        
        public static void Catch(Wildcard_catch<? extends Number> tmp) {
        	System.out.print("class = " + tmp.GetNumber().getClass().getName() + "\n");
        }
        
        public static void main(String[] args) {
        	Wildcard_catch<Integer> tmp = new Wildcard_catch<>(10, 20);
        	Generic_demo_01.Catch(tmp);
        }
    }
    
    • 如上代码,Catch()函数使用了上界通配符,我们传入参数时,并未进行类型参数的指定,但是编译器根据我们传入的参数类型,进行推断,得出了当前 Wildcard_catch 类中的类型参数。
  • 编译器能够自行捕获通配符当然是我们最希望看到的情况,但是编译器并没有我们想象中的那么灵活,很多场景下,它无法正确捕获到通配符的类型,而在这种场景下,就需要我们编写相关的帮助函数去帮助编译器识别具体类型,如下例子:

    class Wildcard_catch<T> {
        private T data1;
        private T data2;
    
        public Wildcard_catch(T t1, T t2) {
            data1 = t1;
            data2 = t2;
        }
    
        public void SetData(T t1) {
            data1 = t1;
        }
    
        public T GetNumber() {
            return data2;
        }
        
        public static void Catch(Wildcard_catch<? extends Number> tmp) {
        	System.out.print("class = " + tmp.GetNumber().getClass().getName() + "\n");
        	tmp.SetData(tmp.GetNumber());	//这里就会出错
        }
        
        public static void main(String[] args) {
        	Wildcard_catch<Integer> tmp = new Wildcard_catch<>(10, 20);
        	Generic_demo_01.Catch(tmp);
        }
    }
    
    • 如上例子中,使用javac进行编译会报错,报错信息如下:

      Generic_demo_01.java:52: 错误: 无法将类 Wildcard_catch中的方法 SetData应用到给定类型;
      tmp.SetData(tmp.GetNumber());
      ^
      需要: CAP#1
      找到: CAP#2
      原因: 参数不匹配; Number无法转换为CAP#1
      其中, T是类型变量:
      T扩展已在类 Wildcard_catch中声明的Object
      其中, CAP#1,CAP#2是新类型变量:
      CAP#1从? extends Number的捕获扩展Number
      CAP#2从? extends Number的捕获扩展Number
      1 个错误

      如上错误信息,原因是类型变量匹配不上,这是因为泛型的类型擦除,实际上,擦除类型后,都是使用一个标志代表对应的类型,可以看到 SetData(T t) 中的 T 擦除后标志是 CAP#1 ,而 tmp.GetNumber() 对应的 T 会被标志为 CAP#2 ,而显然,在编译器看来,这两个标志是没有关联的,因此就需要借助辅助方法进行类型的确认

    class Wildcard_catch<T> {
        private T data1;
        private T data2;
    
        public Wildcard_catch(T t1, T t2) {
            data1 = t1;
            data2 = t2;
        }
    
        public void SetData(T t1) {
            data1 = t1;
        }
    
        public T GetNumber() {
            return data2;
        }
        
        public static <T> void Catchhelp(Wildcard_catch<T> tmp) {
            tmp.SetData(tmp.GetNumber());
        }
        
        public static void Catch(Wildcard_catch<? extends Number> tmp) {
        	System.out.print("class = " + tmp.GetNumber().getClass().getName() + "\n");
        	Generic_demo_01.Catchhelp(tmp);
        }
        
        public static void main(String[] args) {
        	Wildcard_catch<Integer> tmp = new Wildcard_catch<>(10, 20);
        	Generic_demo_01.Catch(tmp);
        }
    } 
    
    • 借由辅助捕获方法,在调用 tmp 时指定了 T ,此时 tmp 类中的所有 T 都被明确指定,那么对应的 SetData(T t)tmp.GetNumber()T 就建立了联系,即 CAP#1CAP#2 建立了关联,编译器就可以推断出相应的类型

14. 泛型类的继承关系

  • 用一幅图来说明:

在这里插入图片描述

/* 下界的继承关系 */
List<? super Number> list1 = new ArrayList<>();
List<? super Integer> list2 = new ArrayList<>();
list2 = list1;
  • 关于这个继承关系,我们把他们类比为集合的概念就会比较好理解:

    • 如果存在D继承C,C继承B,B继承A,这样关系的四个类
    • 那么:
      1. List<? extends B> 即可以是:List<B>List<C>List<D>
      2. List<? extends C> 即可以是:List<C>List<D>
      3. List<? super B> 既可以是:List<A>List<B>
      4. List<? super C> 既可以是:List<A>List<B>List<C>
    • 由以上列举,可以发现:List<? extends B>是包含List<? extends C>List<? super C>是包含List<? super B>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值