Java泛型(一)

1. Java泛型概述

1.1 Object实现参数类型任意化

  • 考虑自定义list,支持不同类型数据的add、get、getSize操作

  • JDK 1.5之前,由于不支持泛型,所以需要使用Object创建数组作为容器。

  • 获取item时,需要显式地进行强制类型转换。

    class MyList {
          
          
        public static final int LENGTH = 10;
        private Object[] list = new Object[LENGTH];
        private int size = 0;
    
        public void add(Object object) {
          
          
            list[size++] = object;
        }
    
        public Object get(int index) {
          
          
            return list[index];
        }
    
        public int size() {
          
          
            return size;
        }
    }
    // 使用方式
    String str = (String) list.get(1);
    
  • 基于自定义的list创建一个Integer列表,向list中添加元素时,错误添加了String类型的参数

  • get时,仍然按照Integer进行强制类型转换

    public static void main(String[] args) {
          
          
        MyList integerList = new MyList();
        // 基于Object的list,可以添加任意类型的参数
        integerList.add(12);
        integerList.add("24");
        integerList.add(36);
    
        // 访问元素
        for (int i = 0; i < integerList.getSize(); i++) {
          
          
            // 显式进行强制类型转换
            Integer integer = (Integer) integerList.get(i);
            System.out.println(integer + " + 1 = " + (++integer));
        }
    }
    
  • 上述代码,java编译时不会检查非法的强制类型转换,在运行时现ClassCastException使得程序崩溃
    在这里插入图片描述

Object实现参数类型任意化的优缺点

  1. Object可以实现参数任意化,在一定程度上提高了程序的灵活性
  2. 随之而来的要求,想要访问Object类型的属性,必须显式地进行强制类型转换
  3. 错误的强制类型转换,在编译时无法发现,只有运行时才会抛出ClassCastException,降低了代码的健壮性

1.2 泛型的引入

  • 在JDK源码中,我们经常看到类似代码,他们使用大写字母去限定自己的参数类型。这就是泛型!

    public interface List<E> extends Collection<E>
    public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
    
  • 泛型的意思就是泛指的类型(参数化类型),即所操作的数据类型可以以参数的形式进行指定。

    class Printer<T>{
          
          
        private T item;
        public Printer(T item){
          
          
            this.item=item;
        }
        public void print(){
          
          
            System.out.println(item.getClass().getName());
        }
    }
    

泛型(generics)是 JDK 5 中引入的一个新特性,:

  1. 泛型提供了编译时类型安全检测机制,该机制将运行时非法类型转换的check提前到编译时,提升了代码的健壮性。
  2. 泛型会进行隐式地、自动地强制类型转换,提高了代码的可读性、可复用性。

参考文档:

2. 泛型的类型

2.1 泛型类

泛型类的定义

  1. 类名后面使用<>声明类中使用的泛型,可以包含一个或多个泛型通配符

  2. 在类中使用泛型,若不使用IDE会有提示,但不影响编译和运行
    ① 使用泛型通配符定义成员变量
    ② 使用泛型通配符定义方法的入参或返回值

    class ClassName<T>{
          
           // 泛型通配符可以是A-Z的任意大写字母以及'?'
    	private T item; // 使用泛型通配符定义成员变量
    	public T getItem() {
          
           // 返回泛型值的方法,并非是泛型方法
    		return item;
    	}
    	...
    }
    
  • 泛型类在Java的容器(CollectionMap)中最常见,通过泛型实现了数据类型的任意化,使得代码灵活、安全、易维护。

泛型类的简单示例:

  1. 实例化泛型类不指定泛型类型,就与使用Object实现参数类型任意化的效果一样:无编译时的类型安全检测,需要对数据进行显式类型转换,

  2. 实例化泛型类指定泛型类型,才能开启编译时的类型安全检测,增加代码的健壮性

    class MyGeneric<T> {
          
          
        private T item;
    
        public void setItem(T item) {
          
          
            this.item = item;
        }
    
        public T getItem() {
          
          
            return item;
        }
    }
    
    // 使用泛型类
    public static void main(String[] args) {
          
          
        // 创建泛型类对象时不指定类型,使用时与Object定义的类一样,需要进行显式转换
        MyGeneric generic1 = new MyGeneric();
        generic1.setItem(new Double(12.5));
        Double number = (Double) generic1.getItem();
        System.out.println("number: " + number);
        // 创建泛型类对象是指定类型,可以充分利用泛型的优势
        MyGeneric<Integer> generic2=new MyGeneric<>();
        // generic2.setItem(12.3);  // 编译无法通过,提示double类型无法转换成Integer类型
        generic2.setItem(12);
        if (generic2.getItem() instanceof Integer){
          
          
            System.out.println("Item is Integer");
        }
    }
    

2.2 泛型方法

泛型方法的定义

  1. 在权限修饰符和返回之间,通过<>指定方法将使用到的一个或多个泛型通配符

  2. 使用泛型通配符(非强制):使用声明的泛型通配符定义入参或返回值

    // 有参数的泛型方法
    public <E> void printInfo(E input){
          
          
       System.out.println(input);
    }
    // 调用泛型方法
    obj.printInfo("sunrise");
    // 无参数的泛型方法
    public <E> List<E> createList(){
          
          
        return new ArrayList<>();
    }
    // 调用泛型方法
    List<String> list = obj.createList(); // 创建的是String类型的list
    

泛型方法的注意事项

  1. 只有在权限修饰符和返回值之间增加泛型通配符的方法,才是泛型方法。public T getItem()这样的方法,不是泛型方法

  2. 使用泛型通配符时,必须是声明过的泛型通配符,否则编译器会报错:Cannot resolve symbol 'E'

    public <T> void fanMethod(E data) {
          
          
        System.out.println("泛型方法,入参: " + data);
    }
    
  3. 泛型类与泛型方法是独立的:
    (1)泛型类和泛型方法都使用通配符T,但二者的泛型类型是独立的
    (2)实际使用时,泛型类传入的泛型类型为String,而泛型方法的参数类型任意,不受泛型类的限制
    (3)泛型方法可以在泛型类中定义,也可以在普通类中定义,泛型类不一定包含泛型方法

    public class FanTest1<T> {
          
          
        private T data;
    
        /** 
        * 省略构造方法、getter、setter方法
        * 泛型方法,使用的泛型在返回值和权限修饰符之间定义
        * 泛型类型不受类的泛型类型限制,即使都是使用相同的泛型符号
        */
        public <T> void print(T data) {
          
          
            System.out.println("静态方法,定义为泛型方法: " + data);
        }
    
        public static void main(String[] args) {
          
          
            FanTest1<Integer> test = new FanTest1<>(1);
            System.out.println(test.getData());
    
            // 访问泛型方法,传入的泛型类型可以不是Integer
            test.print("hello");
        }
    }
    
  4. 泛型类中,使用泛型的静态方法,必须定义为泛型方法
    (1)泛型类只有实例化后,才会传入泛型类型
    (2)静态方法属于类,并非对象,因此无法获取实例化后传入的泛型类型
    (3)此时,必须将静态方法定义为泛型方法

泛型方法与可变参数

  • Java方法中,通常使用...来定义可变长度的参数( >= 0个参数)。

  • 编译后的可变参数将转为数组,使用方法与数组的使用方法一致

    public void printNames(String... names) {
          
          
        for (String name : names) {
          
          
            System.out.print(name + " ");
        }
    }
    // 传入任意数量的参数
    generic.printNames("lucy", "grace", "john", "张三");
    
  • 使用可变参数有一些限制
    (1)若不使用Object, 则参数类型是固定的
    (2) 可变参数必须位于方法入参的最后一个,且只能使用一个可变参数

  • 将泛型方法与可变参数结合,可以充分利用泛型优势:
    (1)编译时类型安全检查、可读性、代码复用性
    (2)不传入泛型类型时,参数类型的任意化

    public <T> void printNumbers(T... args) {
          
          
        for (T arg : args) {
          
          
            System.out.print(arg + " ");
        }
    }
    // 方法的调用
    generic.printNumbers("12.3", 24, 24.5, 2.4f);
    

2.3 泛型接口

泛型接口的定义

  1. 与泛型类的定义基本一致:接口名后使用<>声明接口中使用到的泛型,可以是一个或多个泛型

  2. 在接口中使用泛型:
    ① 使用泛型通配符定义public static final类型的成员变量
    ② 使用泛型通配符定义方法的入参或返回值:方法默认为public abstract,JDK 1.8开始支持定义default方法,使得接口不再是完全抽象类

    //定义一个泛型接口
    public interface Generator<T> {
          
          
        public T next();
    }
    

实现泛型接口

  • 实现泛型接口时,传入泛型类型:
    ① 实现类无需声明泛型,类中所有使用到对应泛型的地方都会被自动替换成传入的泛型类型
    ② 这时,实现类就是一个普通类
    // 已传入泛型参数,实现类中泛型的类型则被固定,会自动将泛型替换成传入的泛型类型
    public class StringImplement implements GenericInterface<String> {
          
          
        @Override
        public String next() {
          
          
            return RandomStringUtils.random(8, false, true);
        }
    }
    
  • 实现泛型接口,未传入泛型类型:实现类需要声明泛型,这时实现类仍是一个泛型类
    class GenericImplement<T> implements GenericInterface<T>{
          
          
        @Override
        public T next() {
          
          
            return null;
        }
    }
    

参考文档

3. 泛型擦除

3.1 概述

3.1.1 泛型擦除

  • Java泛型JDK 1.5开始引入的,为了兼容之前的代码,泛型成为了伪泛型
  • 所谓的伪泛型,个人理解:
    • 从Java代码上看,GenericImplement<T>中的T在字节码中也应该是一个泛型
    • 只有实例化传入具体的泛型类型后,才会转为具体的类,如 GenericImplement<String>编译后TString替代
    • 实际上,GenericImplement<T>TObject替代,泛型的相关信息都不存在了
    • 这根本不是我们理解的泛型
  • 编译后泛型被原始类型替代(泛型类变普通类),使得泛型信息被擦除,这就是泛型擦除
    • 如果使用T声明泛型,则被Object类型替代;如果使用<? extends xxx>,则被xxx类型替代 —— 替代类型,是同一父类的最小级

3.1.2 原始类型

  • 上面的Object类型和xxx类型就是编译后的原始类型,是擦除泛型后,类型变量在字节码中的原始类型
  • 泛型类TypeRemoveTest如下
    public class TypeRemoveTest<T> {
          
          
        private T value;
    
        public T getValue() {
          
          
            return value;
        }
    
        public void setValue(T value) {
          
          
            this.value = value;
        }
    }
    
  • 其原始类型如下
    public class TypeRemoveTest {
          
          
        private Object value;
    
        public Object getValue() {
          
          
            return value;
        }
    
        public void setValue(Object value) {
          
          
            this.value = value;
        }
    }
    

泛型方法中的泛型变量

  • 泛型方法中,泛型的具体类型需要分情况讨论
    • 不为泛型方法指定泛型时,泛型类型为所有参数同一父类的最小级,即所有参数的最近祖先类
    • 为泛型方法指定泛型时,泛型类型为指定的类型或子类型
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
          
          
        // 不指定泛型
        int value1 = selectOne(12, 24); // 同为Integer,拆包为int
        Number value2 = selectOne(12, 2.4); // Integer和Double的最小父类为Number
        Object value3 = selectOne("hello", 12); // String和Integer的最小父类为Object
    
        // 指定泛型
        int value4 = TypeRemoveTest.<Integer>selectOne(12, 24); // 均为Integer
        Number value5 = TypeRemoveTest.<Number>selectOne(12, "hello"); // 编译报错,指定泛型类型后,要求必须为Number类型或其子类型
        Number value5 = TypeRemoveTest.<Number>selectOne(12, 2.2); // Integer和Double,是Number的子类
    }
    
    // 泛型方法
    public static <T> T selectOne(T x, T y) {
          
          
        return y;
    }
    

3.1.3 疑问一:get时为何不用进行显式类型转换?

  • 疑问: 既然泛型信息被擦除,为何通过get获取的值不是原始类型,而是我们传入的泛型类型?或者说:为何get时不用进行显式类型转换

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
          
          
        TypeRemoveTest<String> stringType = new TypeRemoveTest<>();
        stringType.setValue("Hello");
        // 如果泛型擦除,获取的应该是Object类型,需要显式类型转换成String类型
        // 这样使用并未报错,为何?
        String value = stringType.getValue();
    }
    
  • 通过查看字节码发现:获取到的值最开始确实是Object类型,后来被隐式地转为了String类型
    在这里插入图片描述

3.1.3 疑问二:泛型类不指定泛型类型,泛型类型将会是什么?

  • 需要具体分析:
    • 对于无限定的泛型<T>,泛型类不指定泛型类型,泛型类型默认为Object,与使用Object实现参数任意化的效果一样
    • 对于有限定的泛型<T extends xxx>,泛型类不指定泛型类型,泛型类型默认为xxx,与使用xxx实现参数任意化的效果一样
  • 以无限定的泛型为例:
    public static void main(String[] args)
    			throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
          
          
        // 不指定泛型类型,与Object的参数任意化效果一样
        TypeRemoveTest type = new TypeRemoveTest();
        type.setValue("Hello");
        String value = type.getValue(); // 编译报错: 不兼容的类型: java.lang.Object无法转换为java.lang.String
        String str = (String) type.getValue();
    
        type.setValue(12);
        // 不会进行隐式的强制类型转换
        type.getValue();
        type.setValue(2.4);
    }
    
  • 通过查看字节码发现,第二次的get没有进行隐式的强制类型转换
    在这里插入图片描述

3.2 证明泛型擦除

3.2.1 原始类型相等

  • 传入不同泛型类型的实例对象,通过getClass()获得的信息一致 —— 类型擦除变为原始类型

    public static void main(String[] args) {
          
          
        TypeRemoveTest<String> stringType = new TypeRemoveTest<>();
        TypeRemoveTest<Integer> integerType = new TypeRemoveTest<>();
        System.out.println(stringType.getClass() + " == " + integerType.getClass() + " ? " + (stringType.getClass() == integerType.getClass()));
    }
    

3.2.2 反射调用获取原始类型(反射绕过编译时类型安全检测)

  • 反射调用获取的是原始类型,可以添加其他类型的元素

    public static void main(String[] args) 
    		throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
          
          
        TypeRemoveTest<String> stringType = new TypeRemoveTest<>();
        TypeRemoveTest<Integer> integerType = new TypeRemoveTest<>();
    
        // 编译时类型安全检测,不允许添加其他类型元素
        stringType.setValue("hello");
        // integerType.setValue("2");
        
        // 可以通过反射获取原始类型,从而添加其他类型的元素
        Method method = integerType.getClass().getMethod("setValue", Object.class);
        method.invoke(integerType, "hello");
        System.out.println(integerType.getValue());
    }
    

3.3 泛型擦除引起的问题

3.3.1 类型检查 —> 泛型擦除 —> 编译成字节码

编译前的类型检查

  • 在3.1.3中,我们回答了泛型使用原始类型替代,在get时却无需进行显式类型转换的原因
  • 现在,也有一个类似的问题:set时,为何会进行类型安全检查,泛型不是已经被真实类型替代了吗?
  • 按理说,无论我传入String还是Integer,只要是真实类型Object的子类,都是ok的
  • 原因: 类型检查是在编译前
  • 注意: 这里的get和set是指获取值、传入值,并非局限于成员变量的getter/setter操作

类型检查的依据是什么?

  • 引入泛型前,容器类的实例化如下:

    • 可以向list中加入各种类型的值,编译器不会报错
    • 获取值时,如果我们单纯地认为值为某种类型,很可能出现ClassCastException
    • 这也是为什么需要引入泛型,通过其编译时类型安全检测,提高代码的健壮性
    ArrayList list = new ArrayList();
    
  • 引入泛型后,参数类型的任意化通过泛型类型去指定:实例化时指定泛型类型,引用中也指定对应的泛型类型

    ArrayList<String> arrayList = new ArrayList<String>();
    
  • 细心的你会发现,IDE会提示你:new ArrayList<String>中的String可以移除
    在这里插入图片描述

  • 原因: 类型检查是基于引用的,new ArrayList<>()只是开辟一块空间

    • get或set的值类型,需要与引用传入的泛型类型一致

    • 如果引用未传入泛型类型,则泛型类与Object实现参数类型任意化一样

      public static void main(String[] args)
      		throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
              
              
          ArrayList<String> list1 = new ArrayList<>();
          list1.add("hello");
          // 引用指定的泛型类型为String,传入Integer无法通过检查
          // list1.add(12);
          // 引用指定的泛型类型为String,获取的值为String类型,
          // Integer number = list1.get(0);
          String str = list1.get(0);
      
          // 未指定泛型类型,将使用真实类型Object
          ArrayList list2 = new ArrayList<String>();
          // 可以添加任意类型的值
          list2.add("hello");
          list2.add(2.4);
          // 获取的值为Object类型
          Object value = list2.get(1);
      }
      
    • 特殊的: 未显式指定引用,新建对象后立即使用。如果指定了泛型类型,会基于此类型进行安全检测

      public static void main(String[] args) {
              
              
          new ArrayList<String>().add("Hello");
          new ArrayList<String>().add(10); // 编译错误:对于add(int), 找不到合适的方法
          new ArrayList<>().add("Hello");
      }
      

为什么不支持泛型类型的继承?

  • 下面的代码编译错误:提示:不兼容的类型
    ArrayList<String> list1 = new ArrayList<Object>();
    ArrayList<Object> list2 = new ArrayList<String>();
    
  • 第一种写法错误,本人倒是能理解:
    • 相当于将ArrayList<Object>引用传递给ArrayList<String>引用,已经存储的Object值需要转为String类型
    • 这样很容易出现ClassCastException,因此Java不予许出现这样的引用传递
  • 第二种写法:
    • 将String向上转型为Object明明是可以的
    • 泛型出现的意义,就是为了实现类型安全检测
    • 到头来,还需要自己进行强制类型检查,违背了泛型的设计初衷
  • 所以,Java中将以上写法都视为错误写法

3.3.2 泛型类型不能是基本数据类型

  • ArrayList<int>是不允许的,要想存储整数必须使用ArrayList<Integer>
  • 原因: 泛型擦除后,Object不能存储int类型的值,只能引用Integer类型的值

3.3.3 不能进行类型判断

getClass()获取泛型擦除后的class对象

  • 泛型擦除后,无法通过getClass()获取类似ArrayList<Number>这样的带泛型信息的class对象,

  • 转为原始类型的类,就是一个普通类,获取到的就是一个普通类的class对象

    ArrayList<Number> list1 = new ArrayList<>();
    System.out.println(list1.getClass());
    
  • 上述代码的执行结果:
    在这里插入图片描述

list instanceof ArrayList<String>编译无法通过,更无法知道list对应的泛型类型是什么

  • 下面的写法是错误的,更无法知道list的泛型类型是什么
    在这里插入图片描述

3.3.4 桥方法

setter方法是重写还是重载?

  • 基于上面的TypeRemoveTest这个泛型类,我们想生成一个指定泛型类型的子类

    class StringRemoveTest extends TypeRemoveTest<String> {
          
          
        @Override
        public String getVal() {
          
          
            System.out.println("执行子类的get方法");
            return "子类";
        }
    
        @Override
        public void setVal(String val) {
          
          
            System.out.println("执行子类的set方法");
        }
    }
    
  • 疑问来了:针对setter方法,泛型擦除后,父类中的setter方法为setVal(Object val)

  • 这明显不满足重写的定义:子类重新定义父类的方法,方法名和参数列表都相同

  • 这更像是子类继承了父类的setVal(Object val),然后又重载为setVal(String val)

  • 如果通过反射调用到了继承得到的setVal(Object val),最终的执行结果就不是我们所期待的

  • 编译器自动在子类中生成了重写的setVal(Object val),其内实际调用子类的setVal(String val)方法,避免了泛型擦除与多态之间的矛盾
    在这里插入图片描述

  • 个人理解:

    • 子类中定义没有setVal(Object val)方法,通过父类(指向子类对象)调用setVal时,实际应该调用父类的setVal(Object val)方法
    • 编译器自动为子类生成了桥方法setVal(Object val),实现了对父类方法的重写,解决了泛型擦除对父类与子类间多态造成的影响
    • 桥方法十分巧妙:内部实际调用的是子类重载的setVal(String val)方法
    • 看起来就像是setVal(String val)重写了父类的setVal(Object val),也允许我们为其添加@Override注解
    • 总之,利用编译器自动生成的桥方法,子类实现了对setter方法的重写

get方法

  • 子类中的String getVal()满足重写的定义,因此可以实现子类和父类间的多态性
  • 通过查看字节码发现,编译器也为getter方法生成了桥方法
    在这里插入图片描述
  • 质疑:在子类中出现方法名和参数列表相同的getter方法,这根本就是同一个方法,是不允许这样编写代码的!!
  • 实际上,这俩个方法的返回值是不同的,在jvm看来就是两个不同的方法
    • jvm判断方法时,是方法明、参数列表、返回值综合判断的
    • 但是,编写代码时却不允许程序员定义这样的相同方法
  • 疑惑: 子类都实现getter方法重写了,干嘛编译器还需要定义一个桥方法
  • 欢迎讨论

3.4 絮絮叨叨

3.4.1 一些理解

  • 关于泛型擦除,自己也是似懂非懂
  • 只知道为了与之前的JDK代码兼容,Java中的泛型是伪泛型
    • 编译后的字节码中,泛型不是使用泛型通配符表示,而是被原始类型替换
    • 看起来就像是泛型信息被擦除了,称作泛型擦除
  • 原始类型:
    • 无限定时,使用Object替换;有限定<T extends xxx>时,使用xxx替换
    • 不管是泛型类,还是泛型方法,泛型类型都满足最近祖先类原则(同一父类的最小级)
    • 通过get获取值时,字节码中会存在隐式地强制类型转换CHECKCAST,而不是返回被擦除后的原始类型
    • 泛型类实例化时,不指定泛型类型,泛型类型也会满足最近祖先类原则

一些问题(及解决方法)

  • 泛型类型安全检查的时机和依据
    • 类型安全检查 —> 泛型擦除 —> 编译成字节码,set方法传入的值类型不正确,IDE会有提示
    • 类型安全检查的依据是引用中的泛型类型: new ArrayList<>()无需显式指定泛型类型
  • 泛型类型不支持继承,下面的两种写法,均是错误的:
    第一种写法,可能会导致ClassCastException
    第二种写法,没有充分利用泛型的优势
    	ArrayList<String> list1 = new ArrayList<Object>();
    	ArrayList<Object> list2 = new ArrayList<String>();
    	```	 		
    
  • 泛型类型必须为引用类型,不能是基本数据类型:Object不能存储int值,只能引用Integer对象
  • 无法进行类型判断
    • 泛型擦除后,getClass()获取的不是传入的泛型类型,而是原始类型

参考文档

猜你喜欢

转载自blog.csdn.net/u014454538/article/details/119722789