《Java 核心卷1》ch 8 泛型程序设计

@author:posper

@version:v 1.0

@date:2021/7/9-2021/7/10

本文档为第二遍看 《Java 卷1》ch 8 整理,第一遍看的时候画的思维导图。

薄弱点:

  • 8.6 泛型的限制与局限性

ch 8 泛型程序设计

8.1 为什么使用泛型?

  • 背景:在泛型类推出之前都是使用 Objec[] 数组
    • 即,维护一个 Objec[] 数组,可以向数组中添加任意类型的元素。

使用 Object[] 的缺点(2个)

  • (1)获取一个值时必须进行强制类型转换
  • (2)调用一个方法前必须使用 instanceof 判断对象类型

泛型概述

  • 泛型在 JDK 1.5 引入

  • 使用泛型类型时,编译器会进行检查,防止插入错误类型的对象

    • 泛型只发生在编译阶段,与虚拟机无关
  • 泛型的本质是参数化数据类型

泛型的好处

  • (1)类型安全

    • 调用方法时更安全
  • (2)减少了强制类型转换的次数

    • 获取数据值更方便

8.2 泛型类

泛型类概述

  • 泛型类(generic class)就是有一个或多个类型变量(也叫,泛型标识)的类

  • 类型变量(泛型标识):用于指定泛型类中方法的返回类型以及字段和局部变量的类型

  • 常用的类型变量

    • E:标识集合的元素类型
    • K 和 V 分别表示表的键和值
    • T 表示 “任意类型”

泛型类定义语法

  • 泛型类的定义语法:class 类名<类型变量1,类型变量2, …>

    public class Student<K, V> {
        K key;
        V value;
        ...
    }
    

泛型类的注意事项:(3 条)

  • 1)泛型类 如果没有指定具体的数据类型,此时,操作类型是 0bject;
  • 2)泛型的类型参数只能是类 类型,不能是基本数据类型;
  • 3)泛型类型在逻辑上可以看成是多个不同的类型,但实际上都是相同类型。

从泛型类派生子类

  • 从泛型类派生子类 (2 种情况)

    • 1、如果子类也是泛型类,子类和父类的泛型类型要保持一致
    class ChildGeneric<T> extends Generic<T>
    
    • 2、如果子类不是泛型类,父类要明确泛型的数据类型
    Generic<E> { // 父类
        ...
    }
    
    class ChildGeneric extends Generic<String> { ... } // 明确父类泛型类型
    

泛型接口

  • 泛型接口的使用 (同上 从泛型类派生子类)
    • 1、如果泛型接口的实现类也是泛型类,则实现类和接口的泛型类型要一致;
    • 2、如果泛型接口的实现类不是泛型类,则泛型接口要明确数据类型。

8.3 泛型方法

  • 泛型方法是方法首部带有类型变量<T, E…>的方法

    • 注意:泛型类中使用了泛型的成员方法不是泛型方法
    public static T setKey(T t) { // 不是泛型方法
    	...
    }
    
  • 泛型方法定义语法

    • 语法:修饰符 <类型变量1, 类型变量2, …> 方法返回类型 方法名(参数列表 …)
    public static <T> T setValue(T t) { // 泛型方法
    	...
    }
    
  • 泛型方法可以定义在泛型类中可以定义在普通类中

    • 泛型类,是在实例化类的时候指定泛型的具体类型;
    • 泛型方法,是在调用方法的时候指明泛型的具体类型
  • 泛型方法的调用 (2 种方法)

    • (1)方法1:可以把具体类型包围在尖括号 <> 中,放在方法名的前面

      • 通用,可以避免方法二义性冲突;但较为复杂。
      <String>setValue("java"); // <具体类型>泛型方法
      
    • (2)方法2:大多情况下,可以省略 <具体类型>,由编译器推断出具体的方法

      • 常用,大多情况都可以推断出方法类型;使用方便
      setValue("java"); // 通常用法,省略 <具体类型>
      

8.4 类型变量的限定

  • 引入类型限定的原因:有时,类和方法需要对类型变量(泛型标识)加以约束

    • eg:只有实现了 Comparable 接口的类才具有 compareTo 方法,所以需要对类型变量 T 设置一个限定
    public static <T extends Comparable> T min(T[] a) {
        T smallest = a[0];
        for (int i = 1; i < a.length(); i++) {
    		if (smallest.compareTo(a[i] > 0)) { // 只有实现了 Comparable 接口的类才具有 compareTo 方法
                smallest = a[i];
            }
        }
        return smallest;
    }
    
  • 语法

    <T extends BoundingType>
    
  • 一个变量可以有多个限定类型,限定类型用 ‘&’ 分隔

    T extends Comparable & Serializable // T 类型可以是 Comparable 和 Serializable 实现类
    
  • 为什么使用 extends 而不是 implements ?

    • 表示 T 是限定类型(BoundingType)的子类型
    • T 和限定类型可以是类,也可以是接口
    • 选择 extends 是因为它更接近子类型的概念

8.5 泛型类型代码和虚拟机

类型擦除

  • 泛型只在编译阶段有效,虚拟机运行的时候没有泛型类型对象(因为进行了类型擦除)

  • 定义泛型类型后,会自动提供一个相应的 原始类型

    • 即,原始类型就是将类型参数擦除后,替换为对应的具体类型(如下 2 种情况)
  • 类型擦除的 2 种情况:

    • (1)如果类型变量有限定类型,则原始类型用第一个限定替换类型变量
    // (1)类型变量有限定类型
    public class Interval <T extends Comparable & Serializableimplements Serializable {
    	private T lower;
    	private T upper;
    	
        public Interval (T first , T second) {
    		if (first .compareTo(second) <= 0) { 
                lower = first ; upper = second; 
            } else { 
                lower = second; upper = first; 
            }
    	}
    }
    
    // 使用第一限定类型 Comparable 来替换类型变量 T(泛型标识)
    public class Interval implements Serializable { // 类型擦除后,得到原始类型
    	private Comparable lower;
    	private Coiparable upper;
    	
        public Interval (Coiparable first, Coiparable second) { 
            ...
        }
    }
    
    // Note:如果泛型类改为如下形式
    public class Interval <T extends Serializable & Comparableimplements Serializable {
        ...
    }
    
    // 此时,进行类型擦除时,使用第一个限定类型 Serializable 来替换类型变量 T
    public class Interval implements Serializable { // 原始类型
        private Serializable lower;
    	private Serializable upper;
        ...
    }
    
    • (2)如果类型变量没有限定类型,则将类型变量替换为 Object
    // (2)没有限定类型的类型变量
    public class Pair<T> {
    	private T first;
    	private T second;
    
    	public T getFirst() { return first; }
    	public T getSecond() { return second; }
    	public void setFirst(T newValue) { first = newValue; }
    	public void setSecond(T newValue) { second = newValue; }
    }
    
    // 类型擦除后,将所有类型变量 T 都替换为 Object
    public class Pair<Object> { // 原始类型
    	private Object first;
    	private Object second;
    
    	public Object getFirst() { return first; }
    	public Object getSecond() { return second; }
    	public void setFirst(Object newValue) { first = newValue; }
    	public void setSecond(Object newValue) { second = newValue; }
    }
    

类型擦除泛型表达式

  • (1)调用泛型方法时,如果擦除返回类型(如果没有限定类型,擦除后返回类型为 Object),编译器自动插入强制类型转换

    Pair<Employee> buddies = ... 		  // Pair 类同上
    Employee buddy = buddies.getFirst()// 调用泛型方法。同上,类型擦除后 getFirst 返回类型为 Object
        
    // 其实,编译器将以上调用泛型方法转换为两条虚拟机指令:
    // 1)调用原始方法 getFirst();
    // 2)将返回的 Object 类型强转为 Employee 类型。
    Employee buddy = (Employee) buddies.getFirst(); // 调用后编译器自动插入一个强转
    
  • (2)当访问一个泛型字段时,编译器也会插入强制类型转换(尽管将字段设置为 public 不是合理的,但是在 Java 中的合法的)

    // 访问泛型字段
    Employee buddy = buddies.first; // 通常字段都是 private 的(封装性),但是 public 也是合法的(尽量不用)
    
    // 编译器会在字节码中加入强制类型转换,如下:
    Employee buddy = (Employee) buddies.first; // 类型擦除后,first 字段为 Object 类型,访问时会强转
    

类型擦除泛型方法

  • Java 泛型会自动合成一个桥方法,来解决类型擦除与多态发生冲突的问题

    • 桥方法是编译器在泛型父类的子类中自动生成的
      • eg:Parent类中有个 set(T val) 方法,然后 Child extends Parent,且 Child 类中有个 set(String val)方法
      • Parent 中 T 没有类型限定,所以Parent 类型擦除后会将其中 T 全替换为 Object,即 Parent.set(Object val)
      • 但是,Child.set(String val),此时方法参数类型不同,理论上是不能进行方法覆盖的(没覆盖,则无法多态)。但是Java泛型会在 Child 类中自动合成一个桥方法 set(Object val) { setVal(String val) }; 这样就又可以多态了…
    public class Parent<T> { // 泛型父类
    	...
    	
    	public void test(T  t) { // 类型擦除后,这里参数类型 T 变成 Object
    		System.out.println("调用父类 test 方法");
    	}
    }
    
    // 具体子类
    public class Child extends Parent<String> { // 泛型类继承时,子类不是泛型类,需要具体明确父类泛型类的泛型类型
        ...
            
        public void test(String s) { // 这里不是重写,而是相当于在子类中建立了一个特有方法。
            						 // 因为父类 test 方法进行类型擦除后,参数类型为 Object;
            						 // 而子类 test方法参数类型为 String,无法重写。
    		System.out.println("调用子类 test 方法");
    	}
        
        // 桥方法(类中没有显示给出,编译器自动合成的)
        @Override
    	public void test(Object obj) { // 桥方法(但其实只在泛型方法中有,这里只是为了看清多态)
    		test((String) obj); // 如果删除子类中的 test(String s) 方法,这里会死递归;但此时,不会死递归
    		System.out.println("不会死递归吧....");
    	}
    }
    
    public static void main(String[] args) {
        Parent parent = new Child(); // 多态,上转型对象
        
        /**
        * 若不存在桥方法时(即父子类都不是泛型类),调用如下:
        */
    	parent.test(obj);   // OK.调用父类 test
    	parent.test("abc"); // OK.仍然调用父类 test,因为此时子类的  test(String s) 是子类特有方法
        			
        /**
        * 泛型类,存在桥方法:
        */
        parent.test(obj);  // error。抛出异常ClassCastException,这里只能接受String类型,因为桥方法里面有个String强转
    	parent.test("abc"); // OK。这里调用子类方法,利用桥方法实现了多态!!!
    }
    

注意:只要子类重写方法失败,则上转型对象调用父类方法(准确地说,调用子类从父类继承来的方法)。

Java 泛型转换的注意事项(4点)

  • 虚拟机中没有泛型, 只有普通的类和方法

    • 因为发生了类型擦除
    • 泛型只发生在编译阶段
  • 所有的类型参数都会替换为它们的限定类型

    • 有具体类型则替换为具体类型;
    • 否则,替换为第一个限定类型;
    • 否则,替换为 Object。
  • 桥方法被编译器合成来保持多态

  • 为保持类型安全性,必要时插入强制类型转换

    • 转换泛型表达式
      • 泛型方法的返回类型,强转;
      • 泛型字段,强转

8.6 泛型的限制与局限性

大多数限制都是由类型擦除引起的

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

  • 没有Pair, 只有Pair

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

  • 原因:
  • if (a instanceof Pair) // Error
  • Pair 和 Pair 调用 getClass 方法返回的都是 Pair 类型

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

  • 原因:类型擦除后,table 数组类型可能变成 Object[],此时看似可存放其他类型,但是存入其他类型就报错

    Pair<String>[] table = new Pair<String>[10] ; // Error
    
  • 只是不允许创建这些数组, 而声明类型为Pair[] 的变量仍是合法的。

    • 不过不能用new Pair[10] 初始化这个变量
    Pair<String>[] table; // ok
    

4、Varargs 警告

  • 变参方法中,如果变参类型是泛型类型时,

5、不能实例化类型变置

public Pair<T> {
    ...
        
	// 这种 Pair 构造器是非法的
	public Pair() {
		first = new T(); // ERROR
    	second = new T(); // ERROR
    }
}
  • 原因:类型擦除后 T 变成 Object,但是肯定不希望调用 new Object()
  • 解决办法:
  • 不能在类似 new T(…),new T[…] 或 T.class 这样的表达式中使用类型变量

6、不能构造泛型数组

  • 原因:

7、泛型类的静态上下文中类型变量无效

  • 原因:
  • 不能在静态字段或静态方法中引用类型变量

8、不能抛出或捕获泛型类的实例

  • 甚至泛型类扩展Throwable 都是不合法的

    • public class Problem extends Exception { /* . . . */ } // Error can’t extend Throwable
  • catch 子句中不能使用类型变量

    • try { … } catch (T e) // Error can’t catch type variable

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

10、注意擦除后的冲突

8.7 泛型类型的继承规则

  • 无论 S 与 T 有什么联系, Pair 与 Pair 之间没有任何联系(没有继承关系)
  • 使用这种规定的原因:保证类型安全
    • 具体没细看,参考 《Java 核心卷1》第 11 版本 p346

泛型类型的继承规则

8.8 通配符类型

什么是通配符?

  • 通配符一般是使用 “?” 代替具体的类型实参
  • 注意:通配符是类型实参,而不是类型形参

通配符上限(带有子类型限定的通配符)

  • 语法

    /接口<? extends 类型实参>
    
  • 要求泛型类型,只能是实参类型,或者实参类型的子类型

  • 允许读取一个泛型对象

    • 能调用 getter,不能用 setter
      • 因为 setter(? extends 类型实参),无法确定具体的参数类型,无法执行;
      • ? extends 类型实参 getter(),完全 ok,因为返回类型就算是 类型实参 也是合法的。

通配符下限(通配符的超类型限定)

  • 语法

    /接口<? super 类型实参>
    
  • 要求泛型类型,只能是实参类型,或者实参类型的父类型

  • 允许写入一个泛型对象

    • 能调用 setter, 不能用 getter

无限定通配符

  • 语法

    /接口<?> {
        ...
    }
    
  • Pair<?> 和 Pair 本质的不同在于: 可以用任意 Object 对象调用原始 Pair 类的 setObject 方法。

通配符捕获

  • 通配符不是类型变量

    • 因此,不能在编写代码中使用 “ ?” 作为一种类型
    ? t = p.getFirst();  // Error
    
  • 带有通配符的方法不能使用 “?" 作为变量类型

    • 解决方案为:构造一个辅助的泛型方法,然后让带有通配符 “?” 的方法调用辅助泛型方法
    // 带有通配符的方法,不能使用 ? 作为类型变量
    public static void swap(Pair<?> p) {
        // ? t = p.getFirst();  // Error 
        swapHelper(p); // 调用辅助方法
    }
    
    // 辅助方法
    public static <T> void swapHelper (Pair<T> p) {
    	T t = p.getFirst();
    	p.setFirst(p.getSecond());
    	p.setSecond(t);
    }
    

反射和泛型

  • 第一遍暂时没看…
  • 2021/7/10 第二遍仍然没看…
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值