【学习笔记】Java基础知识点——第8章·泛型程序设计

第8章  泛型程序设计

8.1  泛型的概念

所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。

Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮。使用泛型的主要优点是能够在编译时而不是在运行时检测错误。

8.2  定义简单泛型类

本章使用一个简单Pair类作为例子,这个例子使我们只关注泛型,不用为数据存储的细节而分心。下面是泛型Pair类的代码:

public class Pair<T> {
    private T first;
    private T second;
    public Pair(){
        first = null;
        second = null;
    }
    public Pair(T first, T second){
        this.first = first;
        this.second = second;
    }
    public T getFirst() {return first;}
    public void setFirst(T first) {this.first = first;}
    public T getSecond() {return second;}
    public void setSecond(T second) {this.second = second;}
}

静态minmax方法遍历数组并同时计算出最小值和最大值。它用一个Pair对象返回两个结果。

public class GenericTest02 {
    public static void main(String[] args) {
        String[] words = {"a", "b", "c", "d", "e"};
        Pair<String> mm = ArrayAlg.minmax(words);
        System.out.println("min = " + mm.getFirst());//min = a
        System.out.println("max = " + mm.getSecond());//max = e
    }
}

class ArrayAlg{
    public static Pair<String> minmax(String[] a){
        if (a == null || a.length ==0) return null;
        String min = a[0];
        String max = a[0];
        for (int i = 0; i < a.length; i++){
            if (min.compareTo(a[i]) > 0) min = a[i];
            if (max.compareTo(a[i]) < 0) max = a[i];
        }
        return new Pair<>(min, max);
    }
}

8.2.1 泛型方法

上面介绍了如何定义一个泛型类,还可以定义一个带有类型参数的方法。

class Alag{
    public static<T> T getMiddle(T...a){ return a[a.length/2];}
}

这个方法是在普通类中定义的,而不是在泛型类中。不过,这是一个泛型方法,可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这里的修饰符就是public static)的后面,并在返回类型的前面。

泛型方法既可以在普通类中定义,也可以在泛型类中定义。当调用一个泛型方法时,可以把具体累心给包围在尖括号中。放在方法名前面:

String middle = Alag.<String>getMiddle("John", "Q", "Public");

在这种情况下(实际也是大多数情况下),方法调用中可以省略<String>类型参数。

8.2.2 类型变量的限定

有时,类或方法需要对类型变量加以约束。例如,我们要计算数组中的最小元素:

class ArrayAlg{    
    public static <T> T min(T[] a){
        if (a == null || a.length ==0) return null;
        T smallest = a[0];
        for (int i = 0; i < a.length; i++){
            if (smallest.compareTo(a[i]) > 0) smallest = a[i];
        }
        return smallest;
    }
}

这里有一个问题,变量smallest的类型为T,这意味着它可以是任何一个类的对象。如何知道T所属的类有一个compareTo方法呢?

解决这个问题的办法是限制T只能是实现了Comparable接口的类。通过对类型变量T设置一个限定(bound)来实现这一点(一个类型变量或通配符可以有多个限定,限定类型用“&”分隔,而逗号用来分隔类型变量):

public static <T extends Comparable> T min(T[] a)...

实际上Comparable接口本身就是一个泛型类型。目前,我们忽略其复杂性以及编译器产生的警告。

public class GenericTest02 {
    public static void main(String[] args) {
        LocalDate[] birthdays = {
                LocalDate.of(1906, 12, 9),
                LocalDate.of(1815, 12, 10),
                LocalDate.of(1903, 12, 3),
                LocalDate.of(1910, 6, 22)
        };
        Pair<LocalDate> mm = ArrayAlg.minmax(birthdays);
        System.out.println("min = " + mm.getFirst());//min = 1815-12-10
        System.out.println("max = " + mm.getSecond());//max = 1910-06-22
    }
}

class ArrayAlg{
    public static <T extends Comparable> Pair<T> minmax(T[] a){
        if (a == null || a.length ==0) return null;
        T min = a[0];
        T max = a[0];
        for (int i = 0; i < a.length; i++){
            if (min.compareTo(a[i]) > 0) min = a[i];
            if (max.compareTo(a[i]) < 0) max = a[i];
        }
        return new Pair<>(min, max);
    }
}

8.3  泛型代码和虚拟机

虚拟机没有泛型类型对象——所有对象都属于普通类。

8.3.1 类型擦除

无论何时定义一个泛型类型,都会自动提供一个相应的原始类型。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除,并替换为其限定类型(或者,对于无限定的变量则替换为Object)。

例如,Pair<T>的原始类型如下所示:

public class Pair {
    private Object first;
    private Object second;
    public Pair(){
        first = null;
        second = null;
    }
    public Pair(Object first, Object second){
        this.first = first;
        this.second = second;
    }
    public Object getFirst() {return first;}
    public void setFirst(Object first) {this.first = first;}
    public Object getSecond() {return second;}
    public void setSecond(Object second) {this.second = second;}
}

因为T是一个无限定的变量,所以直接用Object替换。结果是一个普通的类,不过擦除类型后,他们都会变成原始的类型。

原始类型用第一个限定来替换类型变量,或者,如果没有给定限定,就替换为Object。例如,类Pair<T>中的类型变量没有显式的限定,因此,原始类型用Object替换T。假定我们声明一个稍有不同的类型:

public class Interval<T extends Comparable & Serializable> implements Serializable{
    private T lower;
    private T upper;
    public Intervale(T first, T second){
        if(first.compareTo(second) <= 0){ 
            lower = first; 
            upper = second;
        } else { lower = second; 
            upper = first;
        }
    }
}

 原始类型Interval如下所示:

public class Interval implements Serializable{
    private Comparable lower;
    private Comparable upper;
    public Interval(Comparable first, Comparable second){...}
}

如果限定切换为class Interval<T extends Serializable & Comparable>,原始类型会用Serializable 替换T,而编译器在必要时向Comparable插入强制类型转换。为了提高效率,应该将标签接口(即没有方法的接口)放在限定列表的末尾。

8.3.2转换泛型表达式

编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。例如,对于下面这个语句序列:

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();

getFirst擦除类型后的返回类型是Object。编译器自动插入转换到Employee的强制类型转换。也就是说,编译器把这个方法调用转换为两条虚拟机指令:

  • 对原始方法Pair.getFirst的调用。
  • 将返回的Object类型强制转换为Employee类型。

当访问一个泛型字段时也要插入强制类型转换。假设Pair类的first字段和second字段都是公共的。表达式“Employee  buddy = buddies.first;”也会在结果字节码中插入强制类型转换。

8.4  限制与局限性

下面是使用Java泛型时需要考虑的一些限制。大多数限制都是类型擦除引起的。

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

不能用基本烈性代替类型参数。因此,没有Pair<double>,只有Pair<Double>。当然,其原因就在于类型擦除。擦除之后,Pair类含有Object类型的字段,而Object不能存储double值。

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

虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。例如:

if (a instanceof Pair<String>)	//ERROR

实际上仅仅测试a是否是任意类型的一个Pair。下面的测试同样如此:

if (a instanceof Pair<T>)	//ERROR

或强制类型转换:

Pair<String> p = (Pair<String>) a;	//警告:仅能测试出这是一个Pair类型

为提醒这一风险,如果视图查询一个对象是否属于某个泛型类型,会得到一个编译器错误(使用instanceof时),或得到一个警告(使用强制类型转换时)。同样的道理,getClass方法总是返回原始类型。例如:

Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
//true,两次getClass调用都返回Pair.class
if(stringPair.getClass() == employeePair.getClass())	

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

不能用new Pair<String>[10]初始化一个变量。当然声明为Pair<String>[]的变量仍是合法的。

8.4.4 不能实例化类型变量

不能在类似new T(...)的表达式中使用类型变量。例如,下面的Pair<T>构造器就是非法的:

public Pair(){ 
    first = new T(); 
    second = new T();
} 

类型擦除将T变成Object,而你肯定不希望调用new Object。

8.4.5 不能构造泛型数组

就像不能实例化泛型实例一样,也不能实例化数组。不过原因有所不同,毕竟数组可以填充null值,看上去可以安全地构造。不过,数组本身也带有类型,用来监控虚拟机中的数组存储。这个类型会被擦除。

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

不能在静态字段或方法中引用类型变量。

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

既不能抛出也不能捕获泛型类的对象。实际上,泛型类扩展Throwable甚至都是不合法的。

8.4.8 可以取消对检查型异常的检测

Java异常处理的一个基本原则是,必须为所有检查型异常提供一个处理器。不过可以利用泛型取消这个机制。

8.5  泛型类型的继承规则

虽然可以将Manager[]数组赋给一个类型为Employee[]的变量。但Pair<Manager>并不是Pair<Employee>的一个子类。

8.6  自定义泛型结构:泛型类、泛型接口

  1. 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>
  2. 泛型类的构造器如下:
    public GenericClass(){}
    而下面是错误的:
    public GenericClass<E>(){}
  3. 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
  4. 泛型不同的引用不能相互赋值。尽管在编译时ArrayList<String>和ArrayList<Integer>是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中。
  5. 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。经验:泛型要使用一路都用。要不用,一路都不要用。
  6. 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
  7. jdk1.7,泛型的简化操作:
    ArrayList<Fruit> flist = new ArrayList<>();
  8. 泛型的指定中不能使用基本数据类型,可以使用包装类替换。
  9. 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型。
  10. 异常类不能是泛型的。
  11. 不能使用new E[]。但是可以:
    E[] elements = (E[])new Object[capacity];
    参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。
  12. 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型,还可以增加自己的泛型。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值