《Java核心卷 I》第10版阅读笔记(书第7、8章)


day4

6 异常、断言和曰志

当出现异常时至少要做到:
•向用户通告错误;
•保存所有的工作结果;
•允许用户以妥善的形式退出程序

6.1 处理错误

为了在程序中处理异常情况,须研究程序中可能会出现的错误和问题, 以及哪类问题需要关注。

● 用户输入错误
●设备错误
●物理限制
●代码错误

异常分类:异常对象都是派生于 Throwable 类的一个实例,如果 Java 中内置的异常类不能够满足需求,用户可以创建自己的异常类。
 Java 中的异常层次结构●Error类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。需要关注 Exception 层次的错误。
●Java语言规范将派生于 Error 类 或 RuntimeException类的所有异常称为非受查 ( unchecked) 异常,所有其他的异常称为受查(checked) 异常。
●声明受查异常:方法应该在其首部声明所有可能抛出的异常,编译器就会发出一个错误消息.例如:
public FilelnputStream(String name) throws FileNotFoundException .
●如果在子类中覆盖了超类的一个方法, 子类方法中声明的受查异常不能比超类方法中声明的异常更通用。 即子类方法中可以抛出更特定的异常, 或者根本不抛出任何异常。并且如果超类方法没有抛出任何受查异常, 子类也不能抛出任何受查异常。
抛出异常:创建异常类的对象并throw
自定义异常类:定义一个派生于 Exception 的类,或者派生于 Exception 子类的类

6.2 捕获异常

●如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行。要想捕获一个异常, 必须设置 try/catch语句块。
●捕获(try/catch)那些知道如何处理的异常, 而将不知道怎样处理的异常继续进行传递(throw),这里的继续传递有一个特例,如果子类编写一个覆盖超类的方法, 而超类中这个方法没有抛出异常,那么子类中这个方法必须捕获方法代码中出现的每一个受查异常,不允许将其继续传递(throw)给超类,即不允许在子类的 throws超过超类方法所列出的异常类范围
●获取异常的信息:e.getMessage();e.getClass().getName();
●同一个 catch 子句中可以捕获多个异常类型。例如,假设多个异常对应的处理动作是一样,且异常类型彼此之间不存在子类关系,就可以合并 catch 子句。
●再次抛出异常与异常链: catch 子句中可以抛出一个异常,可以改变异常的类型(例如不同的子系统对异常可能有不同的解释);可以抛出新异常,并且将原始异常设置为新异常的“ 原因”,这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节
●finally子句:不管是否有异常被捕获,finally 子句中的代码都被执行,常用于关闭资源
● 带资源的 try语句:try (Resource res = . . .) ,这样try块退出时(无论是否发生异常),会自动调用 res.close()方法,就好像使用了 finally块一样,如果你用常规方式手动编程,就需要两个嵌套的 try/finally语句,但是如果finally中的代码抛出了异常,try中原始的异常将会丢失,不利于分析。使用【带资源的 try语句】的原来的异常会重新抛出,而 close 方法抛出的异常会“ 被抑制“。 这些异常将自动捕获,并由 addSuppressed 方法增加到原来的异常。

6.3 使用异常机制的技巧

●异常处理不能代替简单的测试
●不要过分地细化异常
●利用异常层次结构
●不要压制异常
● 在检测错误时, “ 苛刻” 要比放任更好
●不要羞于传递异常(早抛出,晚捕获)

6.4 使用断言

day5
●断言的概念:断言机制允许在测试期间向代码中插入一些检査语句。当代码发布时,这些检测语句会被自动地移走。

●assert关键字,有两种形式
assert 条件;
assert 条件:表达式;
这两种形式都会对条件进行检测, 如果结果为 false, 则抛出一个 AssertionError 异常。 在第二种形式中,表达式将被传人 AssertionError 的构造器,并转换成一个消息字符串。

●启用和禁用断言:在默认情况下,断言被禁用
java -enableassertions MyApp(用 -enableassertions 或 -ea选项启用)
选项-disableassertions 或 -da 禁用某个特定类和包的断言

在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器 ( class loader) 的功能。当断言被禁用时, 类加载器将跳过断言代码, 因此,不会降低程序运行的速度。

有些类不是由类加载器加载, 直接由虚拟机加载,对于这些系统类来说, 使用-enablesystemassertions/-esa 启用断言

在程序中也可以控制类加载器的断言状态

●使用断言完成参数检查(前置条件Precondition)
在 Java 语言中, 给出了 3 种处理系统错误的机制:
1.抛出一个异常;
2.日志;
3.使用断言。
断言的使用场景:
1 . 断言失败是致命的、 不可恢复的错误。
2 . 断言检查只用于开发和测阶段

●为文档假设使用断言
示例:程序员如何使用断言来进行自我检查。
在这里插入图片描述

6.5 记录日志

可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易

●基本日志:要生成简单的日志记录,可以使用全局日志记录器(global logger) 并调用其 info方法

●高级曰志:自定义日志记录器,调用 getLogger方法创建或获取记录器
private static final Logger myLogger = Logger.getLogger(“com.mycompany.myapp”):

与包名类似, 日志记录器名也具有层次结构。如果对 com.mycompany 日志记录器设置了日志级别, 它的子记录器也会继承这个级别,通常, 有以下 7 个日志记录器级别。
SEVERE • WARNING • INFO • CONFIG • FINE • FINER • FINEST

在默认情况下,只记录前三个级别。 也可以设置其他的级別。例如, logger.setLevel(Level.FINE);
还可以使用 Level.ALL 开启所有级别的记录, 或者使用 Level.OFF 关闭所有级别的记录

对于所有的级别有下面几种记录方法: logger.warning(message): logger.fine(message);
同时,还可以使用 log方法指定级别, 例如: logger.log(Level.FINE, message);

●记录日志的常见用途是记录那些不可预料的异常。可以使用下面两个方法,将异常描述内容包含在日志记录中:
第一种
在这里插入图片描述第二种
在这里插入图片描述●修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于: jre/lib/logging.properties
要想使用另一个配置文件, 就要将 java.util.logging.config.file 属性设置为配置文件的存储位置,并用下列命令启动应用程序:java -Djava.util.logging.config.file-configFile MainClass

要想修改默认的日志记录级别, 就需要编辑配置文件,并修改以下命令行 .level=INFO

可以通过添加以下内容来指定自己的日志记录级别 com.mycompany_myapp.level=FINE

日志记录并不将消息发送到控制台上,这是处理器的任务。另外,处理器也有级别。要想在控制台上看到 FINE级别的消息, 就需要进行下列设置 java.util.logging.ConsoleHandler.level=FINE

●本地化
将日志消息本地化, 以便让全球的用户都可以阅读它
本地化的应用程序包含资源包( ResourceBundle )中的本地特定信息

ResourceBundle这个类的作用就是读取资源属性文件(properties),然后根据.properties文件的名称信息(本地化信息),匹配当前系统的国别语言信息(也可以程序指定),然后获取相应的properties文件的内容。
参考:https://blog.csdn.net/mupengfei6688/article/details/79060014

●处理器 Handler
在默认情况下,日志记录器将记录发送到 ConsoleHandler 中, 并由它输出到 System.err 流中。特别是,日志记录器还会将记录发送到父记录器,最终的根记录器(命名为“ ”)有 一个 ConsoleHandler。参考:https://www.jianshu.com/p/1282e7426d49
与日志记录器一样,处理器也有日志记录级别。对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值 ,要想记录 FINE 级别的日志,就必须修改配置文件中的默认日志记录级别和处理器级别。 另外,还可以绕过配置文件,安装自己的处理器

要将日志记录发送到其他地方, 就要添加其他的处理器。日志 API为此提供了两个很有用的处理器, FileHandler和SocketHandler。SocketHandler 将记录发送到特定的主机和端口;FileHandler可以收集文件中的记录。处理器的分类:https://www.jianshu.com/p/8fe95aee562d

●过滤器
在默认情况下, 过滤器根据日志记录的级别进行过滤。每个日志记录器和处理器都可以有一个可选的过滤器来完成附加的过滤。另外,可以通过实现 filter 接口并定义下列方法来自定义过滤器
boolean isLoggable(LogRecord record)

调用 setFilter方法,将一个过滤器安装到一个日志记录器或处理器中,同一时刻最多只能有一个过滤器

●格式化器
ConsoleHandler类和 FileHandler类可以生成文本和 XML 格式的日志记录。
自定义格式:扩展 Formatter 类并覆盖下面这个方法: String format(LogRecord record) ,调用 setFormatter方法将格式化器安装到处理器中

●日志记录说明(最常用的操作)
1)为一个简单的应用程序,选择一个日志记录器,并把日志记录器命名为与主应用程序包一样的名字,例如,com.mycompany.myprog, 这是一种好的编程习惯。
得到日志记录器:Logger logger = Logger.getLogger(“com.mycompany.myprog”);
为了方便起见,可能希望利用一些日志操作将下面的静态域添加到类中: private static final Logger logger = Logger.getLogger(“com.mycompany.nyprog”):
2)默认的日志配置将级别等于或高于 INFO级别的所有消息记录到控制台。用户可以覆盖默认的配置文件,但是很麻烦。因此,最好在应用程序中安装一个更加适宜的默认配置。(配置见p.314)
3)现在,可以记录自己想要的内容了。但级别等于或高于 INFO级别的消息默认显示到控制台上。因此, 最好只将对程序用户有意义的消息设置为这几个级别。将程序员想要的日志记录,设定为 FINE 是一个很好的选择。

6.6 调试技巧

1.打印参数值,使用this打印隐式参数值。
2.每一个类中放置一个单独的 main方法,做单元测试
等等(详见p321)

7 泛型程序设计

day6
使用泛型机制编写的程序代码要比使用 Object 变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类尤其有用。

7.1为什么使用泛型程序设计

意味着编写的代码可以被很多不同类型的对象重用
●类型参数的好处
在泛型类之前, 泛型程序设计是用继承实现的。ArrayList 类只维护一个 Object 引用的数组。
泛型提供了一个更好的解决方案: 类型参数(type parameters)。ArrayList 类有一个类型参数用来指示元素的类型。
●谁想成为泛型程序员
常见问题之一:程序员可能想要将 ArrayList < Manager> 中的所有元素添加到 ArrayList< Employee>中去,然而, 反过来就不行.要怎么设计呢?
Java语言的设计者发明了一个具有独创性的新概念,通配符类型(wildcard type), 它解决了这个问题

7.2定义简单泛型

● 一个泛型类(generic class) 就是具有一个或多个类型变量的类,泛型类的定义示例如下:
在这里插入图片描述Pair 类引人了一个类型变量 T,用尖括号 ( < >) 括起来,并放在类名的后面。泛型类可以有多个类型变量。例如, 可以定义 Pair 类,其中第一个域和第二个域使用不同的类型: public class Pair<T, U> { . . . }

●在 Java 库中, 使用变量 E 表示集合的元素类型, K 和 V 分别表示表的关键字与值的类型。T ( 还可以用临近的字母 U 和 S) 表示“ 任意类型”。

●用具体的类型替换类型变量就可以实例化泛型类型, 例如: Pair < String>.换句话说,泛型类可看作普通类的工厂

7.3泛型方法

●定义一个带有类型参数的简单方法
class ArrayAlg
{
public static < T> T getMiddle(T… a)
{
return a[a.length / 2];
}
}

●泛型方法可以定义在普通类中,也可以定义在泛型类中。 当调用一个泛型方法时 ,在方法名前的尖括号中放人具体的类型: String middle = ArrayAlg.< String>getMiddle("JohnM, "Q.n, “Public”);

大多数情况下,方法调用中可以省略 < String> 类型参数。编译器能够推断泛型的实际类型,它用 names 的类型(即 String[ ]) 与泛型类型 T[ ] 进行匹配并推断出 T 一定是 String)

偶尔,编译器也会提示错误, 比如: double middle = ArrayAlg.getMiddle(3.14, 1729, 0); 错误消息会以晦涩的方式指出(不同的编译器给出的错误消息可能有所不同): 解释这句代码有两种方法,而且这两种方法都是合法的。 编译器将会自动打包参数为 1 个 Double 和 2 个 Integer 对象,而后寻找这些类的共同超类型。找到 2 个这样的超类型:Number 和 Comparable 接口。补救措施:将所有的参数写为 double 值。

想知道编译器对一个泛型方法调用最终推断出哪种类型, 可以故意引入一个错误, 查看错误消息。

Java 泛型< T> T 与 T的用法参考:
https://www.cnblogs.com/east7/p/13210761.html
https://blog.csdn.net/qianzhitu/article/details/107962657

7.4 类型变量的限定

类或方法需要对类型变量加以约束
●我们要计算数组中的最小元素
在这里插入图片描述
变量 smallest 类型为 T, 这意味着它可以是任何一个类的对象。为了确保 T 所属的类有 compareTo方法,需要将 T 限制为实现了 Comparable 接口(只含一个方法 compareTo 的标准接口)的类。可以通过对类型变量 T 设置限定(bound) : public static < T extends Comparable> T min(T[ ] a ) . . .

● < T extends BoundingType> 表示 T 应该是绑定类型的子类型(subtype)。T 和绑定类型可以是类, 也可以是接口。选择关键字 extends 而不是implements的原因是,extends更接近子类的概念

●一个类型变量或通配符可以有多个限定, 例如:
T extends Comparable & Serializable, 限定类型用“ &” 分隔,而逗号用来分隔类型变量。如果用 一个类作为限定,它必须是限定列表中的第一个,且只能有一个类

7.5 泛型代码和虚拟机

虚拟机没有泛型类型对象,所有对象都属于普通类
●类型擦除
泛型类型自动提供了一个相应的原始类型 ( raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased) 类型变量, 并替换为限定类型(无限定的变量用 Object)。
在这里插入图片描述原始类型用第一个限定的类型变量来替换, 如果没有给定限定就用 Object 替换,上图中因为 T 是一个无限定的变量, 所以直接用 Object 替换,下图使用限定类型替换。
在这里插入图片描述●翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型, 编译器插入强制类型转换。例如,下面这个语句序列 Pair< Employee> buddies = . . .;
Employee buddy = buddies.getFirst( );
擦除 getFirst 的返回类型后将返回 Object 类型。编译器自动插人 Employee 的强制类型转换。
●翻译泛型方法
类型擦除也会出现在泛型方法中
泛型方法 public static < T extends Comparable> T min(T[] a) 是一个完整的方法族,在擦除类型之后,只剩下一个方法: public static Comparable min(Comparable[] a)
在多态场景中,编译时,类型擦除可能会破坏多态,需要使用桥方法解决。
参考:https://www.cnblogs.com/xiaozhang9/p/6033955.html(父类的方法的参数类型是Object,而子类的类型是Date,参数类型不一样,这如果是在普通的继承关系中,根本就不会是重写,而是重载,就会破坏继承的多态)
●桥方法
桥方法不仅用于泛型类型。 在一个方法覆盖另一个方法时可以指定一个更严格的返回类型。例如:
在这里插入图片描述上述方法,具有相同参数类型的两个同名方法是不合法的,clone方法都没有参数,但在虚拟机中,是用参数类型和返回类型确定一个方法,因此, 编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。

day7
总之,需要记住有关 Java 泛型转换的事实:
•虚拟机中没有泛型,只有普通的类和方法。
•所有的类型参数都用它们的限定类型替换。
•桥方法被合成来保持多态。
•为保持类型安全性,必要时插入强制类型转换。

● 调用遗留代码
设计 Java 泛型类型时,主要目标是允许泛型代码和遗留代码之间能够互操作。 因为实现某些类时,Java中还不存在泛型。

7.6 约束与局限性

使用 Java 泛型时需要考虑的一些限制。大多数限制都是由类型擦除引起的。
●不能用基本类型实例化类型参数
没有 Pair< double>, 只 有 Pair< Double>。 当然, 其原因是类型擦除之后, Pair 类含有 Object 类型的域, 而 Object 不能存储 double值
●运行时类型查询只适用于原始类型,不支持泛型
例如:
if (a instanceof Pair< String>) // Error
实际上仅仅测试 a 是否是任意类型的一个 Pair。下面的测试同样如此:
if (a instanceof Pair< T>) // Error
或强制类型转换: Pair< String> p = (Pair< String>) a; // Warning-can only test that a is a Pair

试图查询一个对象是否属于某个泛型类型时,倘若使用 instanceof 会报编译器错误, 如果使用强制类型转换会有警告。
同样的道理, getClass 方法总是返回原始类型,a.getClass()将返回 Pair.class原始类型

●不允许直接创建参数类型的数组(因为编译器检查不出错误,所以直接禁止了)
例如: Pair< String>[] table = new Pair< String>[10]; // Error ,编译直接报错
主要是为了避免数组里出现类型不一致的元素。
泛型的作用是使得程序在编译期可以检查出与类型相关的错误,但是如果使用了泛型数组,这种能力就会受到破坏。
参数类型的数组创建参考:https://blog.csdn.net/qq_41286138/article/details/105250938
(此参考末尾处原始类型参数化类型是兼容转换的原因:https://www.it1352.com/788967.html,因为泛型擦除,最后都会变成Object)
https://blog.csdn.net/x_iya/article/details/79550667

●Varargs 警告
向参数个数可变的方法传递一个泛型类型的实例
考虑下面这个简单的方法, 它的参数个数是可变的:
public static < T> void addAll(Collections coll, T… ts)
{
for (t : ts) coll.add(t);
}
应该记得,实际上参数 ts 是一个数组, 包含提供的所有实参。
现在考虑以下调用:
Collection< Pair< String>> table = . . .;
Pair< String> pairl = …;
Pair< String> pair2 = . . addAll(table, pairl, pair2);
为了调用这个方法,Java 虚拟机必须建立一个 Pair< String> 数组来存储可变参数, 这就违反了前面的规则(不支持创建泛型类型的数组)。不过,对于这种情况, 规则有所放松,会得到一个警告,而不是错误。

可以采用两种方法来抑制这个警告。
①一种方法是为调用addAll 的方法增加注解@ SuppressWarnings(“unchecked”)。
②在 Java SE7中,直接给addAll 方法增加注解@SafeVarargs,会消除创建泛型数组的有关限制,数组中加入不同类型的对象也不会报错,因为数组存储只会检查擦除的类型,即无法保证数组元素类型一致,后续可能会有隐患。

● 不能实例化类型变量
不能使用像 new T(…),newT[…] 或 T.class 这样的表达式中的类型变量
例如, 下面的 Pair 构造器就是非法的:
public Pair()
{ first = new T(); second = new T(); } // Error
类型擦除将 T 改变成 Object, 而且, 本意肯定不希望调用 new Object()。

在 Java SE 8 之后, 最好的解决办法是让调用者提供一个构造器表达式。
例如: Pair< String> p = Pair.makePair(String::new);
makePair方法接收一个 Supplier< T>,这是一个函数式接口,表示一个无参数且返回类型为 T 的函数:
public static < T> Pair< T> makePair(Supplier< T> constr)
{
return new Pair<>(constr.get(), constr.get());
}

比较传统的解决方法是通过反射调用 Class.newlnstance 方法来构造泛型对象。 前面提到表达式 T.class 是不合法的, 因为它会擦除为 Object.class。必须像下面这样设计 API 以便得 到一个 Class 对象.
public static < T> Pair< T> makePair(Class< T> c1)
{
try {
return new Pair<>(c1.newInstance(), c1.newInstance());
} catch (Exception ex) {
return null;
}
}
调用: Pair< String> p = Pair.makePair(String.class);
注意,Class类本身是泛型。 例如,String.class 是一个 Class< String> 的实例(事实上, 它是唯一的实例)。因此,makePair 方法能够推断出 pair 的类型。

●不能构造泛型数组
就像不能实例化一个泛型实例一样, 也不能实例化数组。不过原因有所不同,毕竟数组会填充 null值,构造时看上去是安全的。
考虑下面的例子:
public static < T extends Comparable> T[] minmax(T[] a)
{T[] mm = new T[2];…} // Error
类型擦除会让这个方法永远构造 Comparable[2] 数组
解决方法:
①在这种情况下, 最好让用户提供一个数组构造器表达式:
String[] ss = ArrayAlg.minmax(String[]::new,“Tom”, “Dick”, “Harry”);
构造器表达式 String::new 指示一个函数, 给定所需的长度, 会构造一个指定长度的 String 数组。 public static <T extends Comparable〉T[] minmax(IntFunction< T[ ]> constr, T… a)
{
T[] mm = constr.apply(a.length);
for(int i=0;i<a.length;i++)
mm[i] = a[i];
return mm;
}
②利用反射, 调用 Array.newlnstance
public static < T extends Comparable> T[] minmax(T… a)
{
T[] mm = (T[])Array.newInstance(a.getClass().getComponentType(), a.length);
for(int i=0;i<a.length;i++)
mm[i] = a[i];
return mm;
}

●泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量
public class Singleton< T>
{
private static T singlelnstance; // Error
public static T getSinglelnstance() // Error
{
if (singleinstance == null)
construct new instanceof T
return singlelnstance;
}
}
●不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。实际上, 甚至泛型类扩展 Throwable 都是不合法的。
例如, 以下定义就不能正常编译:
public class Problem< T> extends Exception { /* …*/ } // Error can’t extend Throwable
catch 子句中不能使用类型变量,不过,在异常规范中使用类型变量是允许的。
public static < T extends Throwable> void doWork(T t) throws T // OK
{
try{
do work
} catch (Throwable realCause)
{ t.initCause(realCause); throw t; }
}
● 可以消除对受查异常的检查
Java 异常处理的一个基本原则是, 必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。
@SuppressWarnings(“unchecked”)
public static < T extends Throwable> void throwAs(Throwable e) throws T
{ throw (T)e; }
假设这个方法包含在类 Block 中, 如果调用 Block.< RuntimeException>throwAs(t); 编译器就会认为 t 是一个非受查异常。以下代码会把所有异常都转换为编译器所认为的非受查异常
try {
do work
} catch (Throwable t)
{ Block.< RuntimeException>throwAs(t); }
通过使用泛型类、擦除和@SuppressWarnings 注解, 就能消除 Java 类型系统的部分基本限制。

●注意擦除后的冲突
当泛型类型被擦除时, 无法创建引发冲突的条件。
冲突一:
public class Pair< T> {
public boolean equals(T value){ return true;}
}
代码编译会报错,原因是就是Pair< T>的equals(T)方法在类型擦除后和Object的equals(Object)相同但是又没有重写它,会冲突。补救的办法是重新命名引发错误的方法。

冲突二:
要想支持擦除后的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一个接口的不同参数化。
class Employee implements Comparable< Employee> { . . . }
class Manager extends Employee implements Comparable< Manager> {… } // Error
错误原因:Manager 会实现Comparable< Employee> 和 Comparable< Manager>, 这是同一接口的不同参数化。 这一限制与类型擦除的关系并不十分明确。毕竟,下列非泛型版本是合法的。
class Employee implements Comparable { . . . }
class Manager extends Employee implements Comparable { . . . }
其原因非常微妙, 有可能与合成的桥方法产生冲突。实现了 Comparable< X> 的类可以获得一 个桥方法: public int compareTo(Object other) { return compareTo((X) other); } 对于不同类型的 X 不能有两个这样的方法。

7.7 泛型类型的继承规则

Manager是Employee 的子类,但是Pair< Manager> 不是 Pair< Employee> 的子类。
无论S 与 T 有什么联系,通常,Pair< S> 与 Pair< T>没有什么联系。此限制对类型安全非常必要(p346)。

必须注意泛型与 Java 数组之间的重要区别。可将Manager[ ]数组赋给类型为 Employee[ ] 的变量,因为数组带有特别的保护。如果试图将一个低级别的雇员存储到经理的位置, 虚拟机将会抛出 ArrayStoreException 异常。

永远可以将参数化类型转换为一个原始类型。例如,Pair< Employee> 是原始类型 Pair 的 一个子类型。在与遗留代码衔接时,这个转换非常必要(向后兼容)。 需要注意:转换成原始类型之后可能会产生类型错误,例如:
Pair< Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair rawBuddies = managerBuddies; // OK
rawBuddies.setFirst(new File(". . .")); // only a compile-time warning
当使用 getFirst 获得外来对象并赋给 Manager 变量时,与通常一样,会抛出 ClassCastException 异常。这里失去的只是泛型程序设计提供的附加安全性

最后, 泛型类可以扩展或实现其他的泛型类,就这一点而言,与普通的类没有什么 区别。例如, ArrayList< T> 类实现 List< T> 接口。这意味着, 一个 ArrayList< Manager> 可 以被转换为一个 List< Manager>。但是, 如前面所见, 一个 ArrayList< Manager> 和 ArrayList < Employee> 或 List< Employee>没有关系。
在这里插入图片描述

7.8 通配符类型

固定的泛型类型系统使用起来并没有那么令人愉快, Java 的设计者发明了一种巧妙的(仍然是安全的) “ 解决方案”:通配符类型。
●通配符概念
通配符类型中, 允许类型参数变化。 例如, 通配符类型 Pair<? extends Employee>,表示任何泛型 Pair 类型, 它的类型参数是 Employee 的子类, 如 Pair< Manager>。
在这里插入图片描述前面提到,不能将 Pair< Manager> 传递给上图的方法。
解决的方法:使用通配符类型: public static void printBuddies(Pair<? extends Employee> p) 类型,Pair< Manager> 是 Pair<? extends Employee> 的子类型,可以将 Pair< Manager> 传递给上图的方法。
在这里插入图片描述使用通配符会通过 Pair<? extends Employee> 的引用破坏 Pair< Manager> 吗?(不会)
Pair< Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK wildcardBuddies.setFirst(lowlyEmployee); // compile-time error
原因:
类型 Pair<? extends Employee> 的get,set方法似乎是这样的:
? extends Employee getFirst()
void setFirst(? extends Employee)
这样将不可能调用 setFirst 方法。编译器只知道需要某个 Employee 的子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型。毕竟?不能用来匹配。
使用 getFirst 就不存在这个问题: 将 getFirst 的返回值赋给一个 Employee 的引用完全合法。

这就是引入有限定的通配符的关键之处。现在已经有办法区分安全的访问器方法和不安全的更改器方法了。

●通配符的超类型限定
即可以指定一个超类型限定(supertypebound),如:? super Manager,这个通配符限制为 Manager 的所有超类型。

带有超类型限定的通配符的行为与 7.8 节介绍的相反。可以为方法提供参数(set), 但不能使用返回值(get)。
Pair<? super Manager> 有方法:
void setFirst(? super Manager)
? super Manager getFirst()
这不是真正的 Java 语法,但是可以看出编译器知道什么。编译器无法知道 setFirst 方法 的具体类型, 因此调用这个方法时不能接受类型为 Employee 或 Object 的参数。 只能传递 Manager 类型的对象,或者某个子类型(如 Executive) 对象。另外, 如果调用 getFirst, 不能保证返回对象的类型,只能把它赋给一个 Object。
在这里插入图片描述直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
通配符参考:https://segmentfault.com/a/1190000005337789

day8
超类型限定的另一种应用: <T extends Comparable<? super T>>
参考:https://www.cnblogs.com/xiaomiganfan/p/5390252.html
https://www.jianshu.com/p/2005318f171f

●无限定通配符
例如,Pair<?>。初看起来,这好像与原始的 Pair 类型一样。 实际上,有很大的不同。类型 Pair<?> 有以下方法:
? getFirst()
void setFirst(?)
getFirst 的返回值只能赋给一个 Object。setFirst 方法不能被调用, 甚至不能用 Object 调 用。Pair<?> 和 Pair 本质的不同在于: 可以用任意 Object 对象调用原始 Pair 类的 setObject 方法。

这种脆弱的类型对于许多简单的操作非常有用。例如,测试一个 pair 是否包含一个 null引用,它不需要实际的类型。
public static boolean hasNulls(Pair<?> p)
{ return p.getFirst() = null || p.getSecond() = null; }
当然通过将 hasNulls转换成泛型方法,可以避免使用通配符类型:
public static < T> boolean hasNulls(Pair< T> p)
但是,带有通配符的版本可读性更强。

●通配符捕获
编写一个交换成对元素的方法:
public static void swap(Pair<?> p) ,通配符不是类型变量, 因此, 不能在编写代码中使用“ ?” 作为一种类型。即下述代码是非法的:
? t = p.getFirst(); // Error
p.setFirst(p_getSecond());
p.setSecond(t);

这是一个问题, 因为在交换的时候必须临时保存第一个元素。我们可以写一个辅助方法 swapHelper, 如下所示:
public static < T> void swapHelper(Pair< T> p)
{
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
注意, swapHelper 是一个泛型方法, 而 swap不是, 它具有固定的 Pair<?> 类型的参数。 现在可以由 swap 调用 swapHelper:
public static void swap(Pair<?> p) { swapHelper( p ); }
在这种情况下,swapHelper 方法的参数 T 捕获通配符。它不知道是哪种类型的通配符, 但是, 这是一个明确的类型.

在这种情况下, 并不是一定要使用通配符。 可以直接实现没有通配符的泛型方法 < T> void swap(Pair< T> p)。但对于类似Pair<? super Manager> result类型的数据交换,无法实现没有通配符的泛型方法,通配符捕获机制是不可避免的。

通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、 确定的类型。 例如, ArrayList<Pair< T>> 中的 T 永远不能捕获 ArrayList<Pair<?>> 中的通配符。数组列表可以保存两个 Pair<?>, 分别针对?的不同类型。

7.9 反射和泛型

反射允许在运行时分析任意的对象。如果对象是泛型类的实例,关于泛型类型参数则得不到太多信息,因为它们会被擦除。
●泛型Class类
现在, Class 类是泛型的。例如, String.class 实际上是一个 Class< String>类的对象(唯一的对象)。

类型参数十分有用, 这是因为它允许 Class< T> 方法的返回类型更加具有针对性。下面 Class< T> 中的方法就使用了类型参数:
在这里插入图片描述newlnstance 方法返回一个实例,这个实例所属的类由默认的构造器获得。它的返回类型目前被声明为 T, 其类型与 Class< T> 描述的类相同,这样就免除了类型转换。

如果给定的类型确实是 T 的一个子类型,cast 方法就会返回一个现在声明为类型 T 的对象, 否则,抛出一个 BadCastException 异常。

●使用 Class< T> 参数进行类型匹配
有时, 匹配泛型方法中的 Class< T> 参数的类型变量很有实用价值。
public static < T> Pair< T> makePair(Class< T> c) throws InstantiationException, IllegalAccessException
{
return new Pair<>(c.newInstance(), c.newInstance());
}
调用 makePair(Employee.class) ,Employee.class 是类型 Class< Employee> 的一个对象。makePair方法的类型参数 T 同 Employee 匹配, 编译器推断出此方法将返回 Pair< Employee>

●虚拟机中的泛型类型信息
Java 泛型的卓越特性之一是在虚拟机中泛型类型的擦除。但是擦除的类仍然保留一些泛型祖先的微弱记忆。例如, 原始的 Pair 类知道源于泛型类 Pair< T>, 即使一 个 Pair 类型的对象无法区分是由 Pair< String> 构造的还是由 Pair< Employee> 构造的。

为了表达泛型类型声明,使用java.lang.reflect 包中提供的接口 Type。这个接口包含下列子类型:
•Class 类,描述具体类型。
•TypeVariable 接口,描述类型变量(如 T extends Comparable<?super T>)
•WildcardType 接口, 描述通配符(如?super T)
•ParameterizedType 接口, 描述泛型类或接口类型(如 Comparable<? super T>)
•GenericArrayType 接口,描述泛型数组(如 T[ ])

下图给出了继承层次。注意, 最后 4 个子类型是接口, 虚拟机将实例化实现这些接口的适当的类。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值