版权声明:本文为博主ExcelMann的原创文章,未经博主允许不得转载。

第8章 泛型程序设计

作者:ExcelMann,转载需注明。

第8章内容目录:

  • 为什么要使用泛型程序设计
  • 定义简单泛型类
  • 泛型方法
  • 类型变量的限定
  • 泛型代码和虚拟机
  • 限制与局限性
  • 泛型类型的继承规则
  • 通配符类型
  • 反射和泛型

本章内容将介绍实现自己的泛型代码所需了解的全部知识,这些知识大多数情况下是用来帮助排除自己代码的问题。

一、为什么要使用泛型程序设计

泛型程序设计意味着编写的代码可以对多种不同类型的对象重用。
在Java中增加泛型类之前,泛型程序设计是通过继承实现的。例如,ArrayList只维护一个Object引用的数组。
后来,泛型提供了一个更好的解决方案:类型参数。例如,ArrayList类有一个类型参数用来指示元素的类型。

1、类型参数的好处

好处一:使得代码具有更好的可读性;

好处二:编译器可以充分利用这个类型信息。调用ArrayList对象的get方法时,编译器知道返回的类型是指定的类型,而不是Object,所以不需要强制类型转换;

好处三:编译器可以检查,防止插入错误类型的对象;

二、定义简单泛型类

  1. 注意:泛型类可以有多个类型变量。例如,public class Pair<T,U>{…};

例子:

public class Pair<T> {private T first;private T second;public Pair(){}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;}
}

三、泛型方法

  1. 可以在非泛型类中,定义一个带有类型参数的泛型方法;
    泛型方法的例子:
    其中<T>指类型变量,T指方法的返回值类型。
class ArrayAlg
{public static <T> T getMiddle(T... a){return a[a.length/2];}
}
  1. 当调用一个泛型方法时,可以按下面第一种方式写,但是编译器其实可以推导出泛型的类型,所以可以简化为第二种方式:
String middle = ArrayAlg.<String>getMiddle("abs","dba");String middle = ArrayAlg.getMiddle("abs","dba");
  1. 编译器的推导类型原理:例如,对于double middle = ArrayAlg.getMiddle(3.14,1729);
    编译器会将参数自动装箱为1个Double和1个Integer对象,然后寻找这些类的共同超类型。在这里,它找到两个,分别是Number和Comparable接口。

四、类型变量的限定

有的时候,类或方法需要对类型变量进行限定(加以约束)。

例子:我们要计算一个数组的最小和最大元素

public static <T> Pair<T> minmax(T[] a){if(a == null || a.length==0) return null;T min = a[0];T max = a[0];for(T temp:a){if(min.compareTo(temp)>0) min = temp;if(max.compareTo(temp)<0) max = temp;}return new Pair<>(min,max);}

问题存在:对于这里的类型T,我们怎么知道该类型的对象一定有compareTo方法呢?

解决方法:限制T只能是实现了Comparable接口的类。可以通过对类型变量T设置一个限定来实现这一点,如下所示
public static <T extends Comparable> Pair<T> minmax(T[] a)

注意:

  1. 一个类型变量或通配符可以有多个限定,例如:T extends Comparable & Serializable;

五、泛型代码和虚拟机

请注意,虚拟机没有泛型类型对象——所有对象都属于普通类。下面的内容中介绍编译器如何“擦除”类型参数,以及该过程对Java程序员有什么影响。

1、类型擦除(如何擦除)

无论何时定义一个泛型类型的类或者方法,都会自动地提供一个原始类型的类和方法。
其中原始类型指的是类型变量被擦除后的类型,一般为Object(无限定的情况下)或者限定类型(第一个限定类型)。

注意:应该将标签接口(即没有方法的接口)放在限定列表的末尾!)

比如,对于Pair类,其原始类型的类如下:

public class Pair<Object> {private Object first;private Object second;public Pair(){}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;}
}

2、转换泛型表达式(★)

当编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。

例子:

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

在这里getFirst返回值的类型在擦除后是Object。
所以编译器会自动插入强制类型转换,把这个方法调用转换为两条虚拟机指令:
1)对原始方法Pair.getFirst的调用;
2)将返回的Object类型强制转换为Employee类型;

注意:当访问一个泛型字段时,也会插入强制类型转换;

3、转换泛型方法(★)

问题描述

class DateInteval extends Pair<Employee>
{public void setSecond(LocalDate second){if(second.compareTo(getFirst())>=0)super.setSecond(second);}
}

当该类擦除后,会变成:

class DateInteval extends Pair //after erasure
{public void setSecond(LocalDate second)    {...}public void setSecond(Object second){...} //从Pair继承的setSecond方法
}

其中存在一个从Pair继承的setSecond方法,显然两个方法不是同一个方法(所以不存在动态绑定)。
故考虑下面的语句序列:

var interval = new DateInterval();
Pair<LocalDate> pair = interval;  //ok。赋值给父类
pair.setSecond(aDate);

当调用pair.setSecond(aDate)方法时,因为pair对象声明的是Pair<LocalDate>类型,所以将会调用setSecond(Object)方法(而我们想要的是调用setSecond(LocalDate)方法)。

解决方法:
为了解决这个问题,编译器引入了桥方法。通过在DateInteval类中生成一个桥方法:

//从父类覆盖的方法
public void setSecond(Object second){ setSecond((LocalDate) second);
}

使得调用DateInterval对象的setSecond(Object)方法时,顺利调用setSecond(LocalDate)方法。

桥方法的另一用处:
当一个方法覆盖另一个方法时,可以指定不同的返回类型,这叫做有协变的返回类型
第5章中的clone方法中,对于下面的例子,实际上Employee类有两个克隆方法:

Employee clone()
Object clone() //合成的桥方法,重写Object.clone

合成的桥方法会调用新定义的clone方法。

public class Employee implements Cloneable
{public Employee clone() throws CloneNotSupportedException {...}
}

关于Java泛型转换的总结:

  1. 虚拟机中没有泛型,只有普通的类和方法;
  2. 所有的类型参数都会替换为它们的限定类型;
  3. 会合成桥方法来保持多态;
  4. 为保持类型安全性,必要时会插入强制类型转换;

4、调用遗留代码

对于遗留代码,一般会存在两个问题:
1)将泛型类对象赋值给遗留类变量的情况;
2)将遗留类的方法得到的原始类型对象赋值给泛型类变量的情况;

对于这两种情况,可以判断警告的严重性,如果是可控的,那么可以使用注解使警告消失。(因为这种警告不会比没有泛型之前的情况更加糟糕。最差的情况也就是抛出一个异常。)

六、限制与局限性

在这一章节中,将讨论使用Java泛型时需要考虑的一些限制,大多数都是由于类型擦除导致的限制。

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

因此,没有Pair<double>,只有Pair<Double>。原因在于类型擦除。擦除之后,Pair类含有Object类型的字段,而Object不能存储double值。

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

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

例子1:
if(a instanceof Pair<String>) //ERROR
实际上仅仅测试a是否是任意类型的一个Pair,会得到一个编译器错误。

例子2:
Pair<String> p = (Pair<String>) a;
会得到一个警告。

例子3:
Pair<String> stringPair = …;
stringPair.getClass(); //return Pair.class
对于泛型类对象,其调用getClass方法,都会返回擦除类型变量的类名.class。

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

不能实例化参数化类型的数组,例如:
var table = new Pair<String>[10]; //ERROR

如果需要收集参数化类型对象,可以简单地使用ArrayList:ArrayList<Pair<String>>更加安全和有效。

4、Varargs警告

上一节中提出不能创建参数化类型的数组。不过,对于参数个数可变的一些方法却需要传递一个参数化类型的数组。

对于这种情况,规则有所放松,你只会得到一个警告,而不是一个错误。

可以采用两种方法抑制该警告:第一种是添加注解@SuppressWarnings(“unchecked”);第二种是在Java7中,可以用@SafeVarargs注解方法;

注意:

  1. 对于任何只需要读取参数数组元素的方法都可以使用这个注解;
  2. 该注解只能用于声明为static、final或(Java 9中)private的构造器或方法。所有其他方法可能会被覆盖,所以使得该注解没有意义;
  3. 可以使用该注解来消除创建泛型数组的有关限制,方法如下:
@SafeVarargs static <E> E[] array(E... array){return array;
}//故现在可以调用:
Pair<String> table = array(pair1,pair2);
//不过该代码隐藏着危险,由于类型擦除

5、不能实例化类型变量

问题描述
不能在类型new T(…)的表达式中使用类型变量。例如,下面的Pair<T>构造器就是非法的:
public Pair(){ first = new T(); second = new T(); } //ERROR
因为类型擦除后,变成了new Object();

解决方法一
在Java8之后,最好的解决方法就是让调用者提供一个构造器表达式。例如:

Pair<String> p = Pair.makePair(String::new); //方法引用:指定了Supplier.get方法执行的代码

makePair方法接收一个Supplier<T>,这是一个函数式接口,表示一个无参数而且返回类型为T的函数:

public static <T> Pair<T> makePair(Supplier<T> constr){return new Pair<>(constr.get(), constr.get());
}

解决方法二
比较传统的解决方法是通过反射调用Constructor.newInstance方法来构造泛型对象。

public static <T> Pair<T> makePair(Class<T> cl){try{return new Pair<>(cl.getConstructor().newInstance(),cl.getConstructor().newInstance());}catch(Exception e){return null;}
}//如下调用:
Pair<String> p = Pair.makePair(String.class); //因为String.class是Class<T>的对象

6、不能构造泛型T的数组(与第3点不同)

问题描述
考虑下面的例子:

public static <T extends Comparable> T[] minmax(T... a)
{T[] mm = new T[2]; //ERROR
}

类型擦除会让这个方法总是构造Comparable[2]数组。

解决方法一
在这种情况下,最好让用户提供一个数组构造器表达式:

String[] names = ArrayAlg.minmax(String[]::new, "Tom", "Dick");

mixmax方法使用这个参数生成一个有正确类型的数组:

public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a)
{T[] result = constr.apply(2);...
}

构造器表达式String::new指示一个函数式接口,该接口的apply方法,通过给定所需的长度,会构造一个指定长度的String数组。

解决方法二
比较老式的方法是利用反射,并调用Array.newInstance:

public static <T extends Comparable> T[] minmax(T... a){var result = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);...
}

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

注意:不能在静态字段或方法中引用类型变量。例如:
private static T temp;

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

  1. 既不能抛出也不能捕获泛型类的对象。实际上,泛型类拓展Throwable甚至都是不合法的;
  2. catch子句中不能使用类型变量;

9、可以取消对检查型异常的检查

技术介绍

@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T
{throw (T) t;
}//★假如该方法存在于接口Task中,如果有一个检查型异常e,并调用
Task.<RuntimeException>throwAs(e);
//那么编译器就会认为e是一个非检查型异常

Java异常处理的规则要求对每一个检查型异常提供一个处理器。不过可以利用泛型取消这个机制,技术代码如上所示。

应用
解决一个棘手的问题。要在一个线程中运行代码,需要把代码放在一个实现了Runnable接口的类的run方法中。不过该方法不允许抛出检查型异常。
所以我们提供一个从Task到Runnable的适配器,它的run方法可以抛出任何异常。

interface Task
{//run()是唯一的未实现接口,故该接口是一个函数式接口void run() throws Exception;@SuppressWarnings("unchecked")static <T extends Throwable> void throwAs(Throwable t) throws T{throw (T) t;}static Runnable asRunnable(Task task){//Runnable也是一个函数式接口,lambda表达式代表了接口的run()return ()->{try{task.run();    }catch(Exception e){Task.<RuntimeException>.throwAs(e);}};}
}

例如,以下程序运行了一个线程,它会抛出一个异常:
(对于该段代码的解释:创建一个Thread对象,参数是Runnable,该Runnable通过接口Task的静态方法返回得到,而该静态方法的参数是一个Task对象,所以采用lambda表达式代替Task函数式接口)

public class Test
{public void static main(String[] args){var thread = new Thread(Task.AsRunnable(()->{Thread.sleep(1000);System.out.println("hello");throw new Exception("check this exception");}));}
}

10、注意擦除后的冲突

  1. 方法冲突:倘若出现擦除后方法冲突的问题,补救的方法是重新命名引发冲突的方法。
  2. 桥方法冲突:倘若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口的子类。因为会出现桥方法的冲突。例子如下:
class Employee implements Comparable<Employee>{...}
class Manager extends Employee implements Comparable<Maneger>{...} //ERROR//桥方法,若是上述情况,会出现两个同签名的方法
public int compareTo(Object object){return compareTo((X) object);
}

七、泛型类型的继承规则

  1. 无论类型变量T和S是什么关系,通常,Pair<S>和Pair<T>之间都没有任何关系;
  2. 泛型类型与Java数组之间的一个重要的区别:数组中,可以将一个Manager[]数组赋值给一个类型为Employee[]的变量。
Manager[] managerBuddies = {ceo,cfo};
Employee[] employeeBuddies = managerBuddies; //OK

不过数组有特别的保护。如果试图将一个低级别的员工存储到employeeBuddies,虚拟机将会抛出ArrayStoreException异常。

  1. 总是可以将参数化类型转换为一个原始类型(在与遗留代码合并的时候会出现)。不过可能会出现一些异常;
  2. 泛型类可以拓展或实现其它的泛型类(同普通类一样);

八、通配符类型

1、通配符概念

介绍
在通配符类型中,允许类型参数发生变化,例如:

Pair<? extends Employee>

表示任何泛型Pair类型,它的类型参数是Employee的子类。

用法
假设要编写一个打印员工对的方法,如下所示:

public static void printBuddies(Pair<Employee> p)
{Employee first = p.getFirst();Employee second = p.getSecond();
}

对于该代码,不能将Pair<Manager>传递给这个方法。不过解决的方法很简单:可以使用一个通配符类型:

public static void printBuddies(Pair<? extends Employee> p)
//extends的情形下,p存的是Employee类型

对于setXxx的问题(不安全的更改器和安全的访问器)
例如:

var managerBuddies = new Pair<Manager>(ceo,cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies;
wildcardBuddies.setFirst(lowlyEmployee);

此时调用setFirst会出现类型错误。
因为对于Prir<? extends Employee>,它的方法如下:
? extends Employee getFirst()
void setFirst(? extends Employee)

而这样子将不可能调用setFirst方法。因为编译器只知道需要是Employee的某个子类型,单不知道是具体的哪个类型。它拒绝传递任何特定的类型,毕竟?不能匹配。

而对于getFirst方法则不会出现问题。因为将?类型赋值给一个Employee类型变量没问题。

2、通配符的超类型限定(安全的更改器和不安全的访问器)

  1. 介绍
    通配符的限定可以指定一个超类型限定,如下所示:
    ? super Manager
    这个通配符限制为Manager的所有超类型。

  2. 作用
    超类型限定实现了安全的更改器和不安全的访问器。

  3. 例子:对于Pair<? super Manager>有以下方法
    ? super Manager getFirst()
    void setFirst(? super Manager)
    对于get方法,返回值的类型不能保证,所以只能赋值给一个Object对象。
    对于set方法,编译器无法知道参数的具体类型,因此不能接受参数类型为Employee或Object。但是可以接受Manager或其子类型。

  4. 另一个例子,有助于对超类型限定的理解
    假如有一个经理数组,并且想把奖金最高和最低的经理放在一个Pair对象中。对于这个Pair对象,其类型可以是Manager的超类型!

public static void minmaxBonus(Manager[] a, Pair<? super Manager> result){//super下的情形,result存的是Manager类型if(a.length == 0) return;Manager min = a[0];Manager max = a[0];for(int i=1;i<a.length;i++){if(min.getBonus() > a[i].getBonus()) min = a[i];if(max.getBonus() < a[i].getBonus()) max = a[i];}result.setFirst(min);result.setSecond(max);
}

注意区分:对于result的泛型类型,可以是Manager的超类型。而对于result的set方法,其方法参数的类型只能是Manager或其子类型!

  1. 超类型限定的另一种应用
    在处理LocalDate对象的数组时,我们会遇到一个问题。LocalDate实现了ChronoLocalDate,而ChronoLocalDate扩展了Comparable<ChronoLocalDate>。因此,LocalDate实现的是Comparable<ChronoLocalDate>而不是Comparable<LocalDate>。
    这种情况下,可以用超类型限定解决:
public static <T extends Comparable<? super T>> Pair<T> minmax(T[] a)//现在的compareTo方法写成
int compareTo(? super T)

它可以使用任何T的超类型对象作为参数。

  1. 超类型限定的另一种应用(作为函数式接口的参数类型)
//Collection接口有一个方法
default boolean removeIf(Predicate<? super E> filter)...
//该方法会删除所有满足给定谓词条件的元素。//假如你不喜欢有奇数散列码的员工,就可以将他们删除
ArrayList<Employee> staff =...;
Predicate<Object> oddHashCode = obj->obj.hashCode()%2==0;
staff.removeIf(oddHashCode);//这样你就能够传入一个Predicate<Object>,而不是Predicate<Employee>

3、无限定通配符

介绍
类型Pair<?>采用了无限定的通配符。它有以下方法:
? getFirst()
void setFirst(?)
其getXxx返回值只能赋值给一个Object对象。而setXxx不能调用(Object对象也不行)。

用法之一:下面这个方法可优惠用来测试一个对组是否只包含一个null引用,它不需要实际的类型(或者任何类型参数都行)。

public static boolean hasNulls(Pair<?> pair)
{return pair.getFirst()==null || pair.getSecond()==null;
}

4、通配符捕获

例子说明

public static <T> void swapHelper(Pair<T> p)
{T t = p.getFirst();p.setFirst(p.getSecond());p.setSecond(t);
}public static void maxminBonus(Manager[] a,Pair<? super Manager> result)
{minmaxBonus(a,result);PairAlg.swapHelper(result); //这里的swapHelper的类型变量T捕获了?通配符
}

在该例子中,首先有一个泛型方法,在maxminBonux方法中,通过调用该泛型方法,它的类型变量T会捕获通配符?。

通配符捕获只在非常限定的情况下才是合法的。因为编译器需要保证通配符表示单个确定的类型。

九、反射和泛型

这部分内容暂时没浏览书籍,不过在另一篇博客的第十五章节记录过一些笔记。
【原创】深入理解Java——注解和反射

《Java核心技术》学习笔记——第8章 泛型程序设计相关推荐

  1. 【学习笔记】java核心技术学习笔记整理

    <java核心技术>  花了半天到一天又认真读了一下java核心技术中的类部分,感觉最近编程时候好多迷迷糊糊,"这样对不对呢,试一试.怎么不对呢"这类的迷糊问题原来都早 ...

  2. 【深入理解Java虚拟机学习笔记】第二章 Java 内存区域与内存溢出异常

    最近想好好复习一下java虚拟机,我想通过深读 [理解Java虚拟机 jvm 高级特性与最佳实践] (作者 周志明) 并且通过写一些博客总结来将该书读薄读透,这里文章内容仅仅是个人阅读后简短总结,加强 ...

  3. java JDK8 学习笔记——第13章 时间与日期

    第十三章 时间与日期 13.1 认识时间与日期 13.1.1 时间的度量 1.格林威治标准时间GMT  格林威治标准时间的正午是太阳抵达天空最高点之时.现在已经不作为标准时间使用. 2.世界时UT 世 ...

  4. java核心技术读书笔记(第二天:基本程序设计结构)

    java基本程序设计结构

  5. Java核心技术学习--第八天

    Java核心技术学习--第八天 第九章 集合 算法 排序与混排 二分查找 简单算法 批操作 集合与数组的转换 编写自己的算法 遗留的集合 Hashtable类 枚举 属性映射 栈 位集 第十章 图形程 ...

  6. java核心技术卷I 第1-3章 笔记

    java核心技术卷I 第1-3章 本书将详细介绍下列内容: ● 面向对象程序设计 ● 反射与代理 ● 接口与内部类 ● 异常处理 ● 泛型程序设计 ● 集合框架 ● 事件监听器模型 ● 使用Swing ...

  7. 【Java基础学习笔记】- Day11 - 第四章 引用类型用法总结

    Java基础学习笔记 - Day11 - 第四章 引用类型用法总结 Java基础学习笔记 - Day11 - 第四章 引用类型用法总结 4.1 class作为成员变量 4.2 interface作为成 ...

  8. Java编程思想学习笔记-第11章

    <?xml version="1.0" encoding="utf-8"?> Java编程思想学习笔记-第11章 Java编程思想学习笔记-第11章 ...

  9. 《疯狂Java讲义》学习笔记 第六章 面向对象(下)

    <疯狂Java讲义>学习笔记 第六章 面向对象(下) 6.1包装类 基本数据类型 包装类 byte Byte short Short int Integer long Long char ...

最新文章

  1. 自然语言处理(NLP)之gensim中的TF-IDF的计算方法
  2. malloc 和alloc及calloc的区别
  3. Windows Server 2008 R2之十一Windows Server Backup
  4. java中获取错误,在简单程序中获取分段错误
  5. C语言学习之输入任意年份,判断是否为闰年
  6. sql查询索引语句_sql优化总结--基于sql语句优化和索引优化
  7. static关键字 void和void指针 函数指针
  8. 推荐一款eclipse快速打开项目文件夹所在路径插件
  9. 购物车单选全选,计算总价,出现个小问题,没找到.....
  10. vue 打开html流_三种方案解决Vue项目打包后dist中的index.html用浏览器无法直接打开的问题...
  11. 我是如何把SpringBoot项目的并发提升十倍量级的
  12. 计算机组成原理统一试卷,计算机组成原理试卷(含答案).doc
  13. JESD204B调试笔记(实用版)
  14. 对SPEA算法的一些总结
  15. 小米路由器4刷padavan固件
  16. Python+OpenCV利用KNN背景分割器进行静态场景行人检测与轨迹跟踪
  17. 优必选在 CES 上发布了一个真·家庭服务机器人
  18. 第二季:5公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁【Java面试题】
  19. 真阳率(true positive rate)、假阳率(false positive rate),AUC,ROC
  20. 图片文字怎么转换成文本?可以试试这三种途径

热门文章

  1. 从特斯拉人形机器人亮相看AI人工智能模型落地面临的两个难题
  2. Python 中文(大写)数字转阿拉伯数字
  3. 前端开发必备神级资源
  4. MT7628 OpenWRT21 SIM8202驱动ppp拨号rndis拨号
  5. 【函数参数传递】编写一个函数,统计字符串中小写字母的个数,并把字符串中的小写字母转化成大写字母。
  6. Apache Calcite论文概要
  7. 计算机科学丛书20周年——20本跨世经典 夯筑科技基石
  8. 那些年,从博客到出书的博主
  9. /usr/bin/ld: 找不到 -lstdc++
  10. meta标签(以京东首页为例)