Title: Core Java — Volume I Fundamentals
Edition: Eleventh Edition
Author: Cay S. Horstmann
读书笔记:对原书的归纳、总结、补充和疑问。
文章目录
- Chapter 8 Generic Programming
- Dictionary
Chapter 8 Generic Programming
Generics 的主要作用:让类和方法在面对不同的类型的时候采取不同的行为(此解读存疑) 。代码对于不同类型都能复用。
Generics 的主要目标:提供让程序员感到“自然”的类和方法。
本章的其他内容:由于 Java 设计的时候考虑到了向后兼容,所以造成了一些限制和缺点。Generics 的优点和缺点本章都会讲到
8.1 Why Generic Programming
8.1.1 The Advantage of Type Parameters
文章需要记住的点 Interesting Notes:
有同学会觉得,因为所有类都是 Object 的子类嘛,那为什么泛型不用 Object 呢?其实最开始就是这样实现的啦,但是这样有两个不好的地方:1) Downcasting,你每用一次对象就要 casting 一次 2)Error Checking 既然所有类都是Object,所以其实你可以放任何类。
8.1.2 Who Wants to Be a Generic Programmer?
文章需要记住的点 Interesting Notes:
这一段作者就是想表达,用 Generics 可能会很简单但是设计自己的 Generics 会很复杂。尤其是和 inheritance 结合在一起的时候。因为 Inheritance 直接涉及到了一个单项的从属关系。 A 是 B 但不代表 B 是 A。这样会给泛型增加很多的难度。不过本章的主要目的还是在于希望教给读者如何设计自己的 Generics。
8.2 Defining a Simple Generic Class
这个简单,加个 type parameter 然后把它当成一般的 type 使用就行了。
8.3 Generic Methods
文章需要记住的点 Interesting Notes:
- 就算是在普通的类里面,也可以定义 Generic methods。
- 有点反直觉的是,在 method 签名上,generic type 必须在返回数据类型之前声明。我觉得不太对劲因为compiler 完全可以从 参数数据类型或是返回数据类型上判断出此method 是否使用了泛型,那么之前的声明就感觉有点冗余。
public static <T> T getMiddle(T... a){
// bla bla bla
}
//这里的 <T> 就感觉有点冗余。也许 compiler 有特殊要求吧。
//看到后面的补充:是滴,没有多余,可以用来描述 bounds
- 接受泛型的方法如果被传进了多个类型的数据,compiler 就会寻找一个 common superclass or common interface。然后很有可能找到多个符合条件的结果,这样就会出错。
8.4 Bounds for Type Variables
文章需要记住的点 Interesting Notes:
- 需求痛点是啥:我们想要复用的代码也不是说所有 object 都可以啦,最好是可以圈定某些特定的 object。
- 解决方案是啥:你想要圈定那就在 type parameter 圈定就好了。
8.5 Generic Code and the Virtual Machine
8.5.1 Type Erasure
文章需要记住的点 Interesting Notes:
Generic Type 在虚拟机里面是如何清除 type parameter的?有 bound 呢就换成 bound,没有呢就换成 通用的 Object。有多个 Bound 的呢就选择第一个。
8.5.2 Translating Generic Expressions
这里也简单,就一个字 Cast。
8.5.3 Translating Generic Methods
我自己的碎碎念 Whatever I wanna say:
首先上结论:Type Erasue 对 polymorphism 会有干涉作用,为了解决这一问题 compiler 会悄悄加上一种叫 bridge method 的东西。
要理解这一个概念,必须对 overloading 和 overriding 有更深入的理解。(
作为参考可以见这里:https://stackoverflow.com/questions/14676395/java-overloading-method-selection)
At compile time:
- compiler 会检查变量的声明类型,寻找最合适的方法。
- 如果在这时候有 overloading 的话,那么就选最精准的那个。
- 大家可能会觉得反正都有 polymorphism,这个时候选不选有什么意义。但其实这个选择会作为下一步的最初选择。
At run time:
4. 从刚刚的选择着手,检查对象是到底是什么类型。
5. 如果被调用的方法被 override 了,那就调用实际对象的方法。
6. 如果没有被 override,那么就调用刚刚选择的方法,这个时候哪怕有 overloading 也不管了。
归根结底,可以这么说:overloading 只在编译的时候决定选择什么,overriding 只在运行时决定选择什么。
结合上面的结论,就不能理解为什么 generics 对 polymorphism 有影响了。因为当 type erasure 发生之后,所有没有bound 的类型参数都被替换成了 Object,那么如果有人继承了这个 generic type,他原先 override 的方法,现在就会变成 overloading了。
//Before Type Erasure: 这里我们写代码的时候会觉得是 overriding
class Pair<T>{
public void setSecond(T second){
//....
}
}
class DateInterval extends Pair<LocalDate>{
public void setSecond(LocalDate second){
//...
}
}
//After Type Erasure:类型擦除之后就变成了了 overloading
class Pair{
public void setSecond(Object second){
//....
}
}
class DateInterval extends Pair<LocalDate>{
public void setSecond(LocalDate second){
//...
}
//解决办法
@override
public void setSecond(Object second){
return setSecond((LocalDate)second);
}
}
//所以唯一解决办法,就是在 DateInerval 里面 override 父类的方法。
8.5.4 Calling Legacy Code
感觉没啥重要的。
8.6 Restrictions and Limitations
这几大 limitation 都是比较简短的点,我就合在一起记录了。
文章需要记住的点 Interesting Notes:
记住所有的 limitations 基本都是因为 type erasure 造成的。
- Type 参数不能是原始数据类型。
Pair<double>
这样是不允许的,因为原始数据类型不是对象,不能被替换成 Object.
(为啥 primitive types 不是对象呢?) - Type 查询只能查询没有参数的那个类(raw type)。
比如Pair<String> sp = ...;
如果你在这个对象使用sp.getClass()
返回的则是Pair
。就是因为 Type Erasure 嘛,在JVM里面不管你的 Type 是啥对于虚拟机来说都是一样的。 - 不能构造带有 Type 参数的数组。You cannot create arrays of parameterized types.
var table = new Pair<String>[10] // error
我想可能是因为 Type Erasure 之后,该数组在虚拟机中将会以 Pair[] 的形式存在。那么其实不管你的 Type Parameter 是什么,该数组都会通过编译器的检查。然而其实际类型可能就不一样了。 - 这一条是根据上一条延伸的:Varargs Warning
Varargs = Variable arguments = 不定数量的方法参数。
前面几章我们提过,不定数量的方法参数其实就是把参数转化成了一个数组,但是第三点我们又说,带有 Type Parameter 的 generic type 不可以形成数组,所以我们就会面临麻烦。这里JVM没有那么严格要求,允许形成数组,只是会警告你。 - 不可以初始化 Type 变量。You cannot instantiate type variables。
new T(...) //not allowed
(为啥不允许啊Type Erasure 之后变成了 Object 类,根本不是你想要 new 的那个对象。)
我们可以用 method reference/lambda expression 的方式来解决问题,也可以用 reflective 的方式来解决问题。 - 不可以构造 Type 数组。You cannot contruct a generic array.
注意了,这一点和第三点很像,但是说的不是一个事情。
第三点意思是:new Pair<String>[10] // 不可以
第六点意思是:new T[10] // 不可以
有一种 trick 在于,反正都要被 erase 掉,你干脆直接 erase 后的类型好了啦。
比如:与其new T[10]
不如new Object[]
后面有需要的时候 Cast 就好了啦。但这样的坏处在于 cast back 的时候会报错。 - Type 变量不可以用在静态上下文当中。Type variables are not valid in static context of generic classes.
- 不可以抛出或是接住泛型类的实例。 You cannot throw or catch isntances of a generic class.
- ?
第八点和第九点看不太懂因为涉及到了第七章的 Exception Handling,后续将会补上。 - Type Erasure 之后可能会带来 method 间的冲突。
一个 class 或是 type variable T 不能同时是两个特殊 interface 的子类。怎样才算特殊的 interface 呢?如果这两个 interface 其实是一样的只是 type parameter 不一样的时候。
因为在 type erasure 之后,compiler 会生成两个 bridge methods,这两个bridge methods 会是完全一样的。
8.7 Inheritance Rules for Generic Types
文章需要记住的点 Interesting Notes:
Pair<S>
andPair<T>
没有关系,哪怕 S 和 T 有密切的关系。
如果 S 是 T 的子类,而且我们也允许Pair<S>
是Pair<T>
的子类,父类的引用里面可以存储父类和子类的对象,可能就会出现问题。这和数组是不一样的。在数组里父类的引用可以存储子类的对象,但是 virtual machine 会记住这个存储的类型,一旦你存储了不一样的东西则会报错。
Cross Reference: Section 5.1.5 Caution
因为 8.7 实际是 Generic types 和 数组 Arrays 比较大的一个区别,我先试图弄清楚 Array 在相关概念中是如何表现的。
- 数组在被关键字 new 创建的时候,会记住它所储存的类型,如果后期我们试图储存其他非子类的类型,则会报错 ArrayStoreException。
- 这样做的原因在于:数组是有继承的概念的,如果我们把创建的数组赋值给了他的父类,他们我可以很轻松的在数组中添加一个父类对象而 compiler 不会报错。这样的话数组里面储存的对象就会不一致,有些事父类对象有些是子类对象。而且该数组还有两个 reference:子类的 reference 和 父类的 reference。那么如果此时我们在子类的 reference 上调用子类对象的方法,就会出问题,因为该数组里面有父类对象,父类对象是没有子类方法的。我们会调用一个不存在的方法。
我想这里也是同样的原因。 如果我们允许 Pair<S>
继承 Pair<T>
,那么父类和子类的两个引用可以用来引用同一个子类对象,你还可以在子类对象中添加父类对象。这时候子类引用就就会引用一个父类对象,到时候调用在父类对象上调用子类对象的方法就会出错了。
8.8 Wildcard Types
作者说这玩意儿弄出来为了使泛型更加灵活的,所以它会解决大部分 wildcard 的 limitations 吗?
8.8.1 The Wildcard Concepts
文章需要记住的点 Interesting Notes:
- 理解这一章的关键句子在于:
In a wildcard type, a type parameter is allowed to vary.
比如:Pair<? extends Employee>
可以读作 “某个带有未知 Type Parameter 的泛型,而这个未知的 Type Parameter 必须是 Employee 的子类。”
(那我就不懂了,Pair<?>
和 Pair<T>
有什么样的区别呢?)
- 这样的话带有通配符的泛型就可以当做好多泛型的父类了。
我自己的碎碎念 Whatever I wanna say:
我觉得这完全是针对 8.7 的泛型限制来讲的,专门就是解决了上一个问题。
比如:
Manager
继承 Employee
Pair<Manager>
不继承 Pair<Employee>
但是 Pair<Manager>
继承 Pair<? extends Employee>
这一节有个问题我花了好多时间都没有看懂:
public static void print(Pair<? extends Employee> p){
//blablablac
}
//Here you can pass Pair<Manager> as the argument.
public setFirst (? extends Employee){
}
//Here you cannot pass Manager as the argument.
怎么理解这个还是不太清楚
看了一下午吧,终于把这个地方搞清楚了。
- 首先我们要明白 Pair<? extends Employee> 代表什么。这是一个类,还可以用作是泛型的父类,我甚至觉得可以看做是泛型的泛型。所谓泛型就是说同一串代码可以反复在不同的类型上面使用。可是泛型本身却没有父类。比如你写了个代码专门处理
List<Shapes>
,这个代码就不能用来处理List<Circle>
即便是说 Shape/Circle 本身具有继承关系。 - 接着人们就想,能不能设计一个类,可以用来代表不同 Type Parameter 的各种泛型,所以就有了 wildcard。
- 这个地方的第二段代码不可以是因为,首先要认识到,这一段代码出现在定义 class Pair<? extends Employee> 当中,这个class 代表着拥有某个 Type Parameter 的泛型类。后续你极有可能用好多个符合条件的泛型来替代这个 wildcard,所以setFirst 方法不能随便接受参数因为 compiler 不确定你会用哪个泛型类来替换通配符。
8.8.2 Supertype Bounds for Wildcards
文章需要记住的点 Interesting Notes:
复习一下什么是 Type Variable Bounds,就是你在编写泛型类的时候,可以要求你的泛型必须继承什么什么类或是 interface。限制一下泛型的类型,告诉电脑也不是任何类型都可以的。
? super Manager
意思是任何一个 Manager 的父类,这和我们前面学的 bounds:任何一个 xxx class 的子类是相反的。- 这里也是和上一节同样的,声明带有 wildcard 的泛型类时,我们不确定 client 到底会用什么泛型类。所以使用 super 的话,除了client 可能会提供任意一个父类,然后方法的参数自然会根据这个提供的父类而变化。那么自然你不能随便接受参数类型了,谁知道会变成什么样啊。
- 还有一种使用情况有点复杂,需要一步步发展起来。
a) 我们现在想要设计一个泛型,但是对于 Type Parameter 有一定的限制。
b) 什么样的限制呢?我们的限制就是这个 Type Parameter 一定要继承一些接口,而这一堆接口正好可以用一个 wildcard 来表示。(为什么是一堆而不是一个呢?因为有些 class 没有继承某个我们需要的接口,但是继承了相关的接口)。
8.8.3 Unbounded Wildcards
文章需要记住的点 Interesting Notes:
Pair<?> vs Pair
前一个是所有泛型的父类,它有一个重要的特征在于,你完全不能 setFirst,不管是什么类型的对象都不能传送给它当参数。(因为它可以轻松地被向下转型到任意一个类型,所以传什么参数都有可能会错。)
8.8.3 Wildcard Capture
文章需要记住的点 Interesting Notes:
我觉得看书的确给我一大感触就是,作者和读者的角度是不一样的。Pair<?>
以及 Pair<T>
这种明显特别让人混淆的概念作者居然不放在一开始讲,只是随便提了一下。这两个的区别在于,前者代表了一众泛型类,但是一旦应用开发者选定某个泛型类后,该 ?就已经被确定了。而后者是一个泛型方法,意味着他可以用在所有类型上。
- 这一章的内容在于,作为一个 library programmer,我们不知道 application programmer (client) 会具体使用什么类,所以我们用 wildcard 来代替。但是有时候我们必须要声明一个变量在暂时性的储存一下这个 wildcard,可是 “?” 又不能用来声明变量类型,我们该怎么办呢,答案是写一个泛型helper来帮我们。
我自己的碎碎念 Whatever I wanna say:
- 这章有个细节吧我觉得,那就是作者把 application programmer 和 library programmer 分得很清,而且作者基本是在以 library programmer的角度来写的。
- 震惊了!wildcard 请问通配符为什么要取这个名字:因为 wildcard 就是斗地主癞子版里的癞子牌,可以用来随意代表任何一张牌。
8.9 Reflection and Generics
8.9.1 The Generic Class
Class.
这一小节就是讲了讲 Class
这个类其实也是泛型类,里面的细节都是泛型实现的。
8.9.2 Using Class<T>
Parameters for Type Matching.
文章需要记住的点 Interesting Notes:
什么是 type matching:保证传过来的泛型 Type 和返回的泛型 Type 是一样的。
这个时候就可以利用 Class,因为 T 是不可以 new。如果你没有看懂,你可以自己写代码试试 type matching,你就明白了。
8.9.3 Generic Type Information in the Virtual Machine
文章需要记住的点 Interesting Notes:
- reflection API 可以帮我们重新构建泛型类。
(这一部分没太看懂,或者说这一部分有什么用呢。)
8.9.4 Type Literals
文章需要记住的点 Interesting Notes:
- 我们遇到的问题:如何对不同的类采取不同的行为?因为在 Type Erasure 之后,
ArrayList<Integer>
和ArrayList<String>
应该是一样的,那么我怎么怎么设计一个程序,使得改程序会对这两不同的类有不同的行为呢。 - 我们的解决方案:我也还是不太懂。
Dictionary
Application Programmer 应用开发者
Library Programmer 库开发者
Generics 泛型
Wildcard 通配符
Type Erasure 类型擦除
Reference 引用