02. JAVA泛型机制

1.概述

泛型(Generics),是JDK5中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型,不用到虚拟机运行期(检测),避免报ClassCastException(类型转换异常)泛型的本质是为参数化类型,即把类型当参数一样传递,该参数类型可用在类,接口和方法中,即泛型类,泛型方法,泛型接口;

特性: 泛型只在 编译阶段 有效,示例如下

List classStringList = new ArrayList<>();

List classIntegerList = new ArrayList<>();

boolean isSame = classStringList.getClass() == classIntegerList.getClass();

Log.d(TAG,isSame ?"类型相同为"+classIntegerList.getClass():"类型不同");

//打印结果 类型相同为class java.util.ArrayList

通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施,也就是说Java中的泛型只在编译阶段有效;

在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并在对象进入和离开方法的边界处添加类型检查

和类型转换的方法,即泛型信息不会进入到运行时阶段。这也是Java的泛型被称为“伪泛型”的原因。

小结: 泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

2.为什么使用泛型

1)增强代码的健壮性,泛型将类型检查由运行时提前到了编译时,不用进行强转,增强错误检测,避免因类型问题引发的运行时异常;

2)适用于多种类型执行相同的代码,从而增加代码复用性;

3.泛型的使用方式

泛型有三种使用方式:分别是泛型类、泛型接口、泛型方法;

1>泛型类

泛型类型用于类的定义中,被称为泛型类。

通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

注意事项:

1.泛型的类型参数只能是类类型(包括自定义类),不能是基本数据类型;

2.传入的实参类型需要与泛型的类型参数类型相同,Generic generic = new Generic<>(111);

3.不能对确切的泛型类型使用 instanceof 操作,编译时会报非法操作;

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
   //在实例化泛型类时,必须指定T的具体类型
   public class Generic<T>{
       //key这个成员变量的类型为T,T的类型由外部指定
       private T key;
       public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
           this.key = key;
       }
       public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
           return key;
       }
   }
//泛型类
public class Generic<T>{...}
//继承类
public class Generic2<T> extends Generic<T>{}

2>泛型接口

泛型接口与泛型类的定义及使用基本相同,泛型接口常被用于在各种类的生产器中

   public interface Generator<T>{
      T next();
   }
   /**
    * 当时实现泛型接口的类,未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
    * 即:class FruitGenerator implements Generator{}
    * 如果不声明泛型,如:class FruitGenerator implements Generator编译器会报错:"Unknown class"
    * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
    */
   class FruitGenerator<T> implements Generator<T>{
       @Override
       public T next() {
           return null;
       }
   }
    //泛型接口
    interface Generic<T> {fun next():T}
    //接口实现类1
    class ImplGeneric1<T> : Generic<T>{ 
        override fun next(): T {
            TODO("Not yet implemented")
        }
    }
    //接口实现类1
    class ImplGeneric2 : Generic<String>{
        override fun next(): String { //返回类型跟随接口泛型类型
            TODO("Not yet implemented")
        }
    }


4>泛型方法

/**
 * 这才是一个真正的泛型方法。
 * 首先在public与返回值之间的必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
 * 这个T可以出现在这个泛型方法的任意位置.
 * 泛型的数量也可以为任意多个
 *  如:public  K showKeyName(Generic container){
 *        ...
 *     }
 */
public <T> T showKeyName(Generic<T> container){
    System.out.println("container key :" + container.getKey());
    //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
    T test = container.getKey();
    return test;
}


//泛型方法
public <K,V> V showKeyName(Generic<K> container){
    ...
}
public <T> test(Generic<T> container){
    ...
}
//泛型方法与可变参数
public void test() {
   printMsg("111",222,"abc",555);
}
public <T> void printMsg(T...args) {
    for (T t : args) Log.d(TAG, "t is " + t);
}


5>静态方法与泛型

在类中的静态方法使用泛型:

静态方法无法访问类上定义的泛型,因为静态方法是类属性,而泛型初始化在创建对象时;故静态方法要将泛型定义在方法上;

即:如果静态方法要使用泛型的话,必须将静态方法页定义成泛型方法:

public class StaticGenerator<T>{
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"
     */
     public static <T> void show(T t){


     }
}


4.最常用的泛型类型变量名

E:元素(在Java集合框架中有广泛的应用)

K:键,

V:值,

N:数字,

T:类型,

S,U,V等: 第二,第三,第四个类型

//砖石运算符
JDK7以下版本
BoxintegerBox=new Box();
JDK7及以上版本
Box integerBox2=new Box<>();//少写类型

5.类型参数 与 类型变量 的不同

Foo中的T为类型参数;

Foo中的String为类型变量;

泛型方法的辨析

public class Generic{
    //key这个成员变量的类型为T,T的类型由外部指定
    private T key;
    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
      this.key = key;
    }
    //虽然在方法中使用了泛型,但这并不是一个泛型方法
    //这只是类中一个普通成员方法,只不过它的返回值是在声明泛型类已经声明过的泛型;
    //所以在这个方法中才可以继续使用T 这个泛型
    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
      return key;
    }
}
public class Generic{
    static class Fruit{}
    static class Apple extends Fruit{}


    public void show_1(T t) {
        System.out.println(t.toString());
    }


    //在泛型类中声明了一个泛型方法,使用泛型类T,这种泛型T可以为任意类型:
    //注意这个T是一种全新的类型,可以类型与T相同,也可以不同
    //由于泛型方法在声明时会声明泛型,因此即使在泛型类中未声明泛型,
    //编译器也能够正确的第识别泛型方法中的泛型
    public  void show_2(T t) {
        System.out.println(t.toString());
    }
    public static void main(String[] args) {
        Person person = new Person();
        Apple apple = new Apple();
        Generic generic = new Generic<>();
        generic.show_1(apple);
        //generic.show_1(person);//show_1不是泛型方法只是方法参数为泛型,所以参数必须跟类中声明的泛型一致;
        generic.show_2(apple); //show_2为泛型方法,在声明时会声明泛型,因此参数中的泛型类型随方法走
        generic.show_2(person);
    }
}

6.约束

缺少实际类型变量的泛型就是一个原始类型

例如:class Box{} Box b=new Box();//这个Box就是Box的原始类型;

1>类约束

/**
 * 类说明:约束
 */
public class Restrict {
    private T data;
    //不能实例化类型变量
    public Restrict(T data) {
        this.data = data;
//        this.data= new T();//不能new T
    }


    public Restrict() {
    }


    //静态域或方法不能引用类型变量
//    private static T instance;


    //静态方法,本身是泛型方法就行
    private static  T getT(T t){
        return t;
    }


    public static void main(String[] args) {
//        Restrict data1;//泛型不能为基本数据类型
        Restrict data2=new Restrict() ;//泛型只能为引用数据类型
        Restrict data3 = new Restrict();
        System.out.println(data2.getClass().getSimpleName());// Restrict
        System.out.println(data3.getClass().getSimpleName());// Restrict
        System.out.println(data2.getClass()==data3.getClass()); //true 泛型类的类型永远为原生类
    }
}

2>异常约束

    /**泛型类不能 extends Exception/Throwable */
    //private class Problem extends Exception


     // 不能捕获泛型类对象,只能抛出去
     public  void doWork(T x) throws T {
//        try {} catch (T x) {//do sth}
        try {//do sth
        } catch (Throwable e) { throw x;}
    }

7.受限的类型参数

功能:对泛型变量的范围作出限制

格式:

单一限制:

多种限制:

extends表达的意义:这里指的是广义上的"扩展",兼有"类继承"和"接口实现"之意;

具有多个限定的类型变量是范围中列出的所有类型的子类型,若上限类型是一个类,必须第一位标出,否则编译错误;

关键:利用受限类型参数;

知道泛型的受限类型,可以调用受限类型的方法 B和C都是接口,单继承 可以实现多个接口,但只能继承一个类;

/**
 * 类型变量的限定--方法上
 * @author NorthStar
 */
public class ArrayAlg {
    //确保泛型T 有 compareTo方法, 需要用 
    //T extends 类(唯一在前) &(连接符) 接口(多个)
    public static  T min(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }


    static class Test extends ArrayList implements Comparable,Serializable{
        @Override
        public int compareTo(@NonNull Object o) {
            return 0;
        }
    }
    public static void main(String[] args) {
        ArrayAlg.min(new Test(), new Test());
    }
}

给定两种具体的类型A和B(例如Number和Integer)

1) 无论A和B是否相关,MyClass与MyClass没有关系

2) MyClass和MyClass的公共父对象是Object; //和擦除机制相关;

8. 泛型通配符 '?'

我们知道Integer是Number的一个子类,同时在特性章节中我们也验证过Generic与Generic

实际上是相同的一种基本类型。那么问题来了,在使用Generic作为形参的方法中,能否使用Generic

的实例传入呢?在逻辑上类似于Generic和Generic是否可以看成具有父子关系的泛型类型呢?

Generic gNumber = new Generic<>(123);

Generic gInteger = new Generic<>(456);

//showKeyValue(gInteger);不是泛型通配符不能用使用多态

showKeyValue1(gInteger);

public void showKeyValue(Generic obj){Log.d("泛型测试","key value is " + obj.getKey());}

public void showKeyValue1(Generic obj){Log.d("泛型测试","key value is " + obj.getKey());}

public void showKeyValue(Generic obj){}

类型通配符一般是使用?代替具体的类型实参,此处'?'是类型实参,而不是类型形参,再直白点的意思是

此处'?' 和 String,Integer都一样是一种实际的类型,可以把?看成所有类型的父类,是一种真实类型;

当操作类型时,不需要使用类型的具体功能,只使用Object类中的功能,那么可以用 ? 通配符来表未知类;

1.泛型中的问号符"?"名为"通配符"

通配符使用的原因: 泛型所传出的具体类如果不用通配符, 传入的泛型类不能使用继承关系以及多态关系,

通配符的适用范围:参数类型,字段类型,局部变量类型,返回值类型(但返回一个具体类型值更好)

受上下限控制的通配符和不受限的通配符;

open class Food{}
open class Fruit:Food() {}
class Apple : Fruit() {}
class Wildcard {
    fun printFruit(p: Generic<Fruit>) {
        print(p.toString())
    }
    fun printFruit2(p: Generic<out Fruit?>) {
        println(p.toString())
    }


    fun printFruit3(p: Generic<in Fruit?>) {
        println(p.toString())
    }
    fun use(){
        val apple:Apple= Apple()
//        printFruit(apple)// 报错 不能用泛型的派生类, 所以出现了通配符
    }
}

2.泛型上下界( )

通配符(Wildcards)和 边界(Bounds)的概念。传入泛型类型实参进行上下界的限制,如:类型实参只准传入指定类型的父类或子类;

是Java泛型通配符, 是 kotlin泛型中的 通配符

  1. / :是指 “上界通配符(Upper Bounds Wildcards)”

称为生产者,上限只读(保证安全访问数据),可获取数据 get(),作为返回值,返回值类型为指定类型T或其子类;

频繁往外读取内容的,适合用上界extends /out。

  1. / :是指 “下界通配符(Lower Bounds Wildcards)”

称为消费者,下限只能写不能读,可获取数据 set(item:T)可以作为参数传递,传入的实参是指定类型T或其父类;

经常往里插入的,适合用下界super / in

open class Food{}
open class Fruit:Food() {}
open class Apple : Fruit() {}
data class Basket<out T>(val item: T)
class Wildcard {
    fun printFruit(p: Generic<Fruit>) {
        print(p.toString())
    }
    //上界通配符,传入的参数类型是Apple或其子类 
    private fun printFruit2(p:Generic<out Fruit>): Generic<out Fruit>{
        return p
    }


    //下界通配符,传入的参数类型是Apple或其父类 
    private fun printFruit3(p: Generic<in Fruit>) {
        println(p.toString())
    }
    fun use(){
        val food:Generic<Food> = Generic()
        val fruit:Generic<Fruit> = Generic()
        val apple:Generic<Apple> = Generic()
        printFruit2(apple)
        printFruit3(food)
        //无法通过 因为food不是Fruit或其子类
//        printFruit2(food) //报错 extends 或 in 上界通配符, 指传入的参数不能高于形参范围(本身或子类)
        //无法通过 因为apple不是Fruit或其父类
//        printFruit3(apple)
    }
}

适用通配符有上限就是只能有只读属性, 下限只有写属性 PESC原则

  //PE原则 Product extends 生产者原则  只读 可获取数据
   public static double sum(List list) {
        list.add(1);//不能写 报错
        Integer i= list.get(1);//上限只能读,不能写
   }


   //SC Consumer super 消费者原则  只写, 可以添加数据
   public static double sum(List list) {
        list.add(0);//可写
        Integer i=list.get(0);//下限只能写,不能读 报错
   }


   //不受限,会退化不能使用List中任何依赖类型参数[T]的方法
   public static double sum(List list) {
       list.add(1);//报错
       list.get(1);
   }


    public void get() {
       //如果 class Apple extends Fruit;
       Plate fruitPlate= new Plate();//可以
       Plate p = new Plate();//使用通配符可以
       Plate p= new Plate();//不行
       使用 ? extends 要符合PE原则  导致 只能取,不能放
       使用 ? super   要符合CS原则  导致 只能放,不能取
       //但通过反射还能放:
       Method set=fruitPlate.getClass().getMethod("set",Object.class);
       set.invoke(fruitPlate,new Banana);
    }

9.类型擦除

1)定于:

保证了泛型在不运行时出现,java中的泛型为 伪泛型,编译器会把泛型类型中所有的类型参数替换为它们的上(下)限,

如果没有对类型参数做出限制,那么就替换为Object类型,因此编译出的字节码仅仅包含原始类型而不需要产生新的类型

到字节码,在必要时插入类型转换以保持类型安全;生成桥方法以扩展泛型时保持多态性;

2)擦除流程

*1.检测泛型类型,获取目标类型 ╭如果泛型类型的类型变量没有限定(),则用Object作为原始类型;

*2.擦除类型变量,并替换为限定类型< 如果有限定(T extends XClass),则使用XClass1作为原始类;

╰如果有多个限定(T extends X1 & XInterface),则使用第一个边界作为原始类;

*3.在必要时插入类型转换以保持类型安全;

*4.生成桥方法以在扩展时保持多态性;

Bridge Method 桥方法

当编译一个扩展参数化类的类,或一个实现了参数化接口的接口时,编辑器可能因此要创建一个合成方法,名为桥方法,

它是类型擦除过程中的一部分; 擦除还是会写到常量池中;

3)没有泛型数组: 因为数组是协变,擦除后就没法满足数组协变的原则; 协变: A extends B 则 A[] extends B[];

10.使用泛型的副作用

  1)无法利用原始类型,只能使用包装类型
    Pair p=new Pair<>(Integer.valueOf(8),new Character('a'));
    Char-->Character, int->Integer


  2)无法创建类型参数的实例
    public static void append(Class  a) throws Exception{
        //E elem0 = new E();//不能new 创建
        E elem = a.newInstance();//可以通过反射创建
    }


  3)无法创建参数化类型的静态变量,因为static是类方法
    public class Mobile {
        public static T t;//不能声明静态变量
        public static void get(T,t){...}//不能用于静态方法
    }


  4)无法使用instanceOf关键字
    public static void test(List list) {
       //用通配符?可以 用T不行
       boolean isaArray=list instanceof ArrayList;
    }


  5)无法创建参数化类型的数组
    List[] array=new List[2];//报错,因为擦除机制;


  6)一个类中是无法存在类型擦除后具有相同签名的两个方法重载
    public void test(List list) {}
    public void test(L)


11.使用泛型解析Json数据

   api返回的json数据
   {
       "code":200,
       "msg":"成功",
       "data":{
           "name":"Jay",
           "email":"10086"
       }
   }


   baseResponse.java
   //接口数据接收基类
   public class BaseResponse {


       private int code;
       private String msg;


       public int getCode() {
           return code;
       }


       public void setCode(int code) {
           this.code = code;
       }


       public String getMsg() {
           return msg;
       }


       public void setMsg(String msg) {
           this.msg = msg;
       }
   }


  /**
   * Description: 用户信息接口实体类
   */
  public class UserResponse<T> extends BaseResponse {
      private T data;


      public T getData() {
          return data;
      }


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


12.泛型相关面试题

1)Array中不支持泛型

2)ArrayList array1=new ArrayList(); //报错,没有继承关系

3)Set与Set到底区别在哪? Set是受类型检查的

4>List 和 List区别;

List 不能赋给 List 但可以赋给List

//举例
List arrayList = new ArrayList();
  arrayList.add("aaaa");
  arrayList.add(100);


  for(int i = 0; i< arrayList.size();i++){
      String item = (String)arrayList.get(i);
      Log.d("泛型测试","item = " + item);
  }
  //毫无疑问,程序的运行结果会以崩溃结束:
  java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String



ArrayList可以放任意类型,例子中添加了一个String类型,添加了一个Integer类型,但都以String方式使用因此程序崩溃了,

为了解决此类问题(使其在编译阶段就可以解决),泛型应运而生;

我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。

List arrayList=new ArrayList();

//arrayList.add(100); 在编译阶段就会报错,

这样泛型通过类型参数使得我们的程序具有更好的可读性和安全性;

1>Java中的泛型是什么 ? 使用泛型的好处是什么?

泛型是一种参数化类型机制,他可以使java中的类,方法以及接口适用于各种类型,从而编写更加通用的代码,例如集合框架

泛型一种编译时类型确认机制,它提供了编译期的类型安全,确保在泛型类型上只能使用正确类型的对形象,避免了在运行时

出现ClassCastException(类型转换异常);

2>java的泛型是如何工作的,什么是类型擦除

泛型的正常工作是依赖编译源码的时候,先进行类型检查,然后进行类型擦除并在类型参数出现的地方插入

强制转换的相关指令实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。

这是为了避免类型膨胀;

3>什么是泛型中的限定通配符和非限定通配符?

限定通配符对类型进行了限制,有两种限定通配符:一种是它通过确保类型必须是T的子类来设定类型的上界

另一种是它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,

否则会导致编译错误。另一方面表示了非限定通配符,因为可以用任意类型来替代。

4>Array中可以用泛型吗?

这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,

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

5>List和原始类型List之间的区别

带参数类型是类型安全的,而且其类型安全是由编译器保证的,但原始类型List却不是类型安全的。

你不能把String之外的任何其它类型的Object存入String类型的List中,但可以把任意类型传入List中;

6>Java中List和List之间的区别是什么?

List 是一个未知类型的List,而List其实是任意类型的List。

你可以把List, List赋值给List,却不能把List赋值给List

List isOfAnyType = new ArrayList<>();

List isOfObjectType = new ArrayList<>();

List stringLs = new ArrayList<>();

isOfAnyType = stringLs;//list表示是未知类型的List,可以赋予String类型;

//isOfObjectType = stringLs; List代表是任意类型的List,不能单一指定String类型

你可能感兴趣的:(JAVA,java)