《Java核心技术》学习笔记——第8章 泛型程序设计
版权声明:本文为博主ExcelMann的原创文章,未经博主允许不得转载。
第8章 泛型程序设计
作者:ExcelMann,转载需注明。
第8章内容目录:
- 为什么要使用泛型程序设计
- 定义简单泛型类
- 泛型方法
- 类型变量的限定
- 泛型代码和虚拟机
- 限制与局限性
- 泛型类型的继承规则
- 通配符类型
- 反射和泛型
本章内容将介绍实现自己的泛型代码所需了解的全部知识,这些知识大多数情况下是用来帮助排除自己代码的问题。
一、为什么要使用泛型程序设计
泛型程序设计意味着编写的代码可以对多种不同类型的对象重用。
在Java中增加泛型类之前,泛型程序设计是通过继承实现的。例如,ArrayList只维护一个Object引用的数组。
后来,泛型提供了一个更好的解决方案:类型参数。例如,ArrayList类有一个类型参数用来指示元素的类型。
1、类型参数的好处
好处一:使得代码具有更好的可读性;
好处二:编译器可以充分利用这个类型信息。调用ArrayList对象的get方法时,编译器知道返回的类型是指定的类型,而不是Object,所以不需要强制类型转换;
好处三:编译器可以检查,防止插入错误类型的对象;
二、定义简单泛型类
- 注意:泛型类可以有多个类型变量。例如,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;}
}
三、泛型方法
- 可以在非泛型类中,定义一个带有类型参数的泛型方法;
泛型方法的例子:
其中<T>指类型变量,T指方法的返回值类型。
class ArrayAlg
{public static <T> T getMiddle(T... a){return a[a.length/2];}
}
- 当调用一个泛型方法时,可以按下面第一种方式写,但是编译器其实可以推导出泛型的类型,所以可以简化为第二种方式:
String middle = ArrayAlg.<String>getMiddle("abs","dba");String middle = ArrayAlg.getMiddle("abs","dba");
- 编译器的推导类型原理:例如,对于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)
注意:
- 一个类型变量或通配符可以有多个限定,例如: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泛型转换的总结:
- 虚拟机中没有泛型,只有普通的类和方法;
- 所有的类型参数都会替换为它们的限定类型;
- 会合成桥方法来保持多态;
- 为保持类型安全性,必要时会插入强制类型转换;
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注解方法;
注意:
- 对于任何只需要读取参数数组元素的方法都可以使用这个注解;
- 该注解只能用于声明为static、final或(Java 9中)private的构造器或方法。所有其他方法可能会被覆盖,所以使得该注解没有意义;
- 可以使用该注解来消除创建泛型数组的有关限制,方法如下:
@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、不能抛出或捕获泛型类的实例
- 既不能抛出也不能捕获泛型类的对象。实际上,泛型类拓展Throwable甚至都是不合法的;
- 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、注意擦除后的冲突
- 方法冲突:倘若出现擦除后方法冲突的问题,补救的方法是重新命名引发冲突的方法。
- 桥方法冲突:倘若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口的子类。因为会出现桥方法的冲突。例子如下:
class Employee implements Comparable<Employee>{...}
class Manager extends Employee implements Comparable<Maneger>{...} //ERROR//桥方法,若是上述情况,会出现两个同签名的方法
public int compareTo(Object object){return compareTo((X) object);
}
七、泛型类型的继承规则
- 无论类型变量T和S是什么关系,通常,Pair<S>和Pair<T>之间都没有任何关系;
- 泛型类型与Java数组之间的一个重要的区别:数组中,可以将一个Manager[]数组赋值给一个类型为Employee[]的变量。
Manager[] managerBuddies = {ceo,cfo};
Employee[] employeeBuddies = managerBuddies; //OK
不过数组有特别的保护。如果试图将一个低级别的员工存储到employeeBuddies,虚拟机将会抛出ArrayStoreException异常。
- 总是可以将参数化类型转换为一个原始类型(在与遗留代码合并的时候会出现)。不过可能会出现一些异常;
- 泛型类可以拓展或实现其它的泛型类(同普通类一样);
八、通配符类型
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、通配符的超类型限定(安全的更改器和不安全的访问器)
介绍:
通配符的限定可以指定一个超类型限定,如下所示:
? super Manager
这个通配符限制为Manager的所有超类型。作用:
超类型限定实现了安全的更改器和不安全的访问器。例子:对于Pair<? super Manager>有以下方法
? super Manager getFirst()
void setFirst(? super Manager)
对于get方法,返回值的类型不能保证,所以只能赋值给一个Object对象。
对于set方法,编译器无法知道参数的具体类型,因此不能接受参数类型为Employee或Object。但是可以接受Manager或其子类型。另一个例子,有助于对超类型限定的理解:
假如有一个经理数组,并且想把奖金最高和最低的经理放在一个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或其子类型!
- 超类型限定的另一种应用:
在处理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的超类型对象作为参数。
- 超类型限定的另一种应用(作为函数式接口的参数类型):
//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章 泛型程序设计相关推荐
- 【学习笔记】java核心技术学习笔记整理
<java核心技术> 花了半天到一天又认真读了一下java核心技术中的类部分,感觉最近编程时候好多迷迷糊糊,"这样对不对呢,试一试.怎么不对呢"这类的迷糊问题原来都早 ...
- 【深入理解Java虚拟机学习笔记】第二章 Java 内存区域与内存溢出异常
最近想好好复习一下java虚拟机,我想通过深读 [理解Java虚拟机 jvm 高级特性与最佳实践] (作者 周志明) 并且通过写一些博客总结来将该书读薄读透,这里文章内容仅仅是个人阅读后简短总结,加强 ...
- java JDK8 学习笔记——第13章 时间与日期
第十三章 时间与日期 13.1 认识时间与日期 13.1.1 时间的度量 1.格林威治标准时间GMT 格林威治标准时间的正午是太阳抵达天空最高点之时.现在已经不作为标准时间使用. 2.世界时UT 世 ...
- java核心技术读书笔记(第二天:基本程序设计结构)
java基本程序设计结构
- Java核心技术学习--第八天
Java核心技术学习--第八天 第九章 集合 算法 排序与混排 二分查找 简单算法 批操作 集合与数组的转换 编写自己的算法 遗留的集合 Hashtable类 枚举 属性映射 栈 位集 第十章 图形程 ...
- java核心技术卷I 第1-3章 笔记
java核心技术卷I 第1-3章 本书将详细介绍下列内容: ● 面向对象程序设计 ● 反射与代理 ● 接口与内部类 ● 异常处理 ● 泛型程序设计 ● 集合框架 ● 事件监听器模型 ● 使用Swing ...
- 【Java基础学习笔记】- Day11 - 第四章 引用类型用法总结
Java基础学习笔记 - Day11 - 第四章 引用类型用法总结 Java基础学习笔记 - Day11 - 第四章 引用类型用法总结 4.1 class作为成员变量 4.2 interface作为成 ...
- Java编程思想学习笔记-第11章
<?xml version="1.0" encoding="utf-8"?> Java编程思想学习笔记-第11章 Java编程思想学习笔记-第11章 ...
- 《疯狂Java讲义》学习笔记 第六章 面向对象(下)
<疯狂Java讲义>学习笔记 第六章 面向对象(下) 6.1包装类 基本数据类型 包装类 byte Byte short Short int Integer long Long char ...
最新文章
- 自然语言处理(NLP)之gensim中的TF-IDF的计算方法
- malloc 和alloc及calloc的区别
- Windows Server 2008 R2之十一Windows Server Backup
- java中获取错误,在简单程序中获取分段错误
- C语言学习之输入任意年份,判断是否为闰年
- sql查询索引语句_sql优化总结--基于sql语句优化和索引优化
- static关键字 void和void指针 函数指针
- 推荐一款eclipse快速打开项目文件夹所在路径插件
- 购物车单选全选,计算总价,出现个小问题,没找到.....
- vue 打开html流_三种方案解决Vue项目打包后dist中的index.html用浏览器无法直接打开的问题...
- 我是如何把SpringBoot项目的并发提升十倍量级的
- 计算机组成原理统一试卷,计算机组成原理试卷(含答案).doc
- JESD204B调试笔记(实用版)
- 对SPEA算法的一些总结
- 小米路由器4刷padavan固件
- Python+OpenCV利用KNN背景分割器进行静态场景行人检测与轨迹跟踪
- 优必选在 CES 上发布了一个真·家庭服务机器人
- 第二季:5公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁【Java面试题】
- 真阳率(true positive rate)、假阳率(false positive rate),AUC,ROC
- 图片文字怎么转换成文本?可以试试这三种途径
热门文章
- 从特斯拉人形机器人亮相看AI人工智能模型落地面临的两个难题
- Python 中文(大写)数字转阿拉伯数字
- 前端开发必备神级资源
- MT7628 OpenWRT21 SIM8202驱动ppp拨号rndis拨号
- 【函数参数传递】编写一个函数,统计字符串中小写字母的个数,并把字符串中的小写字母转化成大写字母。
- Apache Calcite论文概要
- 计算机科学丛书20周年——20本跨世经典 夯筑科技基石
- 那些年,从博客到出书的博主
- /usr/bin/ld: 找不到 -lstdc++
- meta标签(以京东首页为例)