都是个人学习过程的笔记,不是总结,没有参考价值,但是这本书很棒
简单泛型
一个元组类库
一个堆栈类
RandomList
代码有点错误
不应该是Array.stream("The quick brown fox jumped over the lazy brown dog".split(" ")).forEach(rs::add);
而应该是Arrays.stream("The quick brown fox jumped over the lazy brown dog".split(" ")).forEach(rs::add);
当然这也就是个使用流和函数式编程的例子
泛型接口
当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。生成器无需额外的信息就知道如何创建新对象。
举个例子,Supplier
对象不需要什么配置信息,而工厂模式构建新对象需要一些配置信息。
泛型方法
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f("");
gm.f(1);
gm.f(1.0);
gm.f(1.0F);
gm.f('c');
gm.f(gm);
}
}
可以看到上面的泛型方法并不依赖于泛型类,这也印证了书中所说
类本身可能是泛型的,也可能不是,不过这与它的方法是否是泛型的并没有什么关系。
当然你也的确可以写成下面这样,但这样的话方法前面的泛型参数就没有意义了,和类型上的泛型一致
class GenericMethods2 <T> {
<T> void f(T x) {
System.out.println(x.getClass().getName());
}
}
当然下面代码还是有意义的
class GenericMethods2 <T> {
<R> void f(R x) {
System.out.println(x.getClass().getName());
}
}
当然书里还有一句话是
如果方法是 static 的,则无法访问该类的泛型类型参数,因此,如果使用了泛型类型参数,则它必须是泛型方法。
看下面代码,这段代码是无法编译的,具体原因和泛型擦除应该有关
class GenericMethods2<T> {
// 无法编译
static void f(T x) {
System.out.println(x.getClass().getName());
}
}
而下面这段代码就没问题了
class GenericMethods2 {
static <T> void f(T x) {
System.out.println(x.getClass().getName());
}
}
变长参数和泛型方法
一个泛型的 Supplier
简化元组的使用
一个Set工具类
EnumSet.range
的功能是从枚举对象里的一定范围内元素生成一个集合
构建复杂模型
这一节里的那个商店代码非常好非常好,不难理解,Suppliers工具类编写得也特别好,那里用到了一个复杂的三个参数的collect
方法,在流那一章介绍过
直观上理解,这个类的功能就是对容器类型进行操作,create
方法是构造一个有元素的容器,元素的生成是通过Supplier
和流的generate
方法创建的,剩下两个方法类似
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class Suppliers {
// Create a collection and fill it:
public static <T, C extends Collection<T>> C
create(Supplier<C> factory, Supplier<T> gen, int n) {
return Stream.generate(gen)
.limit(n)
.collect(factory, C::add, C::addAll);
}
// Fill an existing collection:
public static <T, C extends Collection<T>>
C fill(C coll, Supplier<T> gen, int n) {
Stream.generate(gen)
.limit(n)
.forEach(coll::add);
return coll;
}
// Use an unbound method reference to
// produce a more general method:
public static <H, A> H fill(H holder,
BiConsumer<H, A> adder, Supplier<A> gen, int n) {
Stream.generate(gen)
.limit(n)
.forEach(a -> adder.accept(holder, a));
return holder;
}
}
泛型擦除
高潮来了,核心就是一句话
在泛型代码内部,无法获取任何有关泛型参数类型的信息。因此,你可以知道如类型参数标识符和泛型边界这些信息,但无法得知实际的类型参数从而用来创建特定的实例。
这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。
意思就是,你只知道<T>
是一个类,但是永远无法知道它具体是什么,也无法创建一个T
对象,更别说获取这个对象的方法
public class HasF {
public void f() {
System.out.println("HasF.f()");
}
}
public class Manipulator2<T extends HasF> {
private T obj;
Manipulator2(T x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
边界
<T extends HasF>
声明 T 必须是HasF
类型或其子类。如果情况确实如此,就可以安全地在obj
上调用f()
方法。
我们说泛型类型参数会擦除到它的第一个边界(可能有多个边界,稍后你将看到)。我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例,T
擦除到了HasF
,就像在类的声明中用HasF
替换了T
一样。如下所示
class Manipulator3 {
private HasF obj;
Manipulator3(HasF x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
例如,如果某个类有一个返回 T 的方法,那么泛型就有所帮助,因为它们之后将返回确切的类型
也就是说我现在不止是想像Manipulator3
那样返回一个HasF
,,我想直接返回其确定的类型
// generics/ReturnGenericType.java
public class ReturnGenericType<T extends HasF> {
private T obj;
ReturnGenericType(T x) {
obj = x;
}
public T get() {
return obj;
}
}
迁移兼容性
一句话就是,为了保障之前的没泛型的代码还能运行,java就得实现擦除,当某个类库变为泛型时,不会破坏依赖于它的代码和应用
擦除的问题
记住下面的话
泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。
class Foo<T> {
T var;
}
Foo<Cat> f = new Foo<>();
class Foo 中的代码应该知道现在工作于 Cat 之上。泛型语法也在强烈暗示整个类中所有 T 出现的地方都被替换,就像在 C++ 中一样。但是事实并非如此,当你在编写这个类的代码时,必须提醒自己:“不,这只是一个 Object“。
// generics/ErasureAndInheritance.java
class GenericBase<T> {
private T element;
public void set(T arg) {
element = arg;
}
public T get() {
return element;
}
}
class Derived1<T> extends GenericBase<T> {}
class Derived2 extends GenericBase {} // No warning
// class Derived3 extends GenericBase<?> {}
// Strange error:
// unexpected type
// required: class or interface without bounds
public class ErasureAndInteritance {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Derived2 d2 = new Derived2();
Object obj = d2.get();
d2.set(obj); // Warning here!
}
}
Derived1
里面的泛型指的就是父类中的泛型,当然你也可以写成class Derived1<R> extends GenericBase<R> {}
总之你知道两个泛型一致就行了。
至于Derived2
,记住:“不,这只是一个 Object”
至于Derived3
,强行编译的话,编译器说得很明白,需要无边界的类或者接口
但如果你写的是class Derived3 extends GenericBase<T> {}
,仍然会报错,报错理由也好理解,这样写的话Derived3
就没有泛型了,那么就会造成运行过程中永远不知道T
到底是个什么类型,想象我们如果写了一个Derived3 d3=new Derived3();d3.returnT();
,那里面的returnT
返回的到底个什么永远没法知道了。所以要么写成,class Derived3 extends GenericBase<String> {}
直接指定T
就是String
,要么就像Derived1
那样子类也保留泛型类。
边界处的动作
我突然好奇,泛型可不可以用来进行强制类型转换,答案是可以
@SuppressWarnings("unchecked")
T[] create(int size) {
return (T[]) Array.newInstance(kind, size);
}
T change(R r){
return (T)r;
}
FilledList.java这个代码里面括号有点错误
即使编译器无法得知 add() 中的 T 的任何信息,但它仍可以在编译期确保你放入 FilledList 中的对象是 T 类型。因此,即使擦除移除了方法或类中的实际类型的信息,编译器仍可以确保方法或类中使用的类型的内部一致性。
这句话进一步证明了,使用泛型的之后,只知道他是一个类
那个SimpleHolder.java和GenericHolder2.java的程序码对比简直绝了,太棒了,高潮有两个
- 首先,两段代码的
get
和set
方法的字节码一毛一样,尤其set
方法里,接受的参数类型都是Object
,这就说明泛型被擦除了,在里面实际操作的时候操作的就是Object
,当然我也知道也无法对泛型对象也操作不了什么东西。 - 主函数里,
SimpleHolder.java
在调用holder.set("Item");
方法时候,对应字节码11: invokevirtual #6; // Method set:(Object;)V
看注释可以知道,在调用之前,对“Item”
进行了类型转换成Object
,而get的时候使用的是String s = (String) holder.get();
,对应的字节码是15: invokevirtual #7; // Method get:()Object;
和18: checkcast #8; // class java/lang/String
,也就是获取的是Object
然后转型成String
;然后我们惊奇的发现GenericHolder2.java反编译之后的程序和SimpleHolder.java一模一样,所以实际上做的事儿也是经历了两次类型转换
产生了相同的字节码,这就告诉我们泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型。这有助于澄清对擦除的困惑,记住:“边界就是动作发生的地方”。
补偿擦除
// generics/ClassTypeCapture.java
class Building {
}
class House extends Building {
}
public class ClassTypeCapture<T> {
Class<T> kind;
public ClassTypeCapture(Class<T> kind) {
this.kind = kind;
}
public boolean f(Object arg) {
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture<Building> ctt1 =
new ClassTypeCapture<>(Building.class);
System.out.println(ctt1.f(new Building()));
System.out.println(ctt1.f(new House()));
ClassTypeCapture<House> ctt2 =
new ClassTypeCapture<>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
}
}
这里就可以获得泛型类的信息了,但是注意,其实上面代码里使用的直接是传入的class
信息才获得到的泛型类具体的类型信息,但我们仍然没法直接对泛型T
进行操作,这也是补偿恢复的重要手段,就是把类字节码传递进去
创建类型的实例
泛型数组
// generics/ArrayOfGenericReference.java
class Generic<T> {
}
public class ArrayOfGenericReference {
static Generic<Integer>[] gia;
}
// generics/ArrayOfGeneric.java
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
try {
gia = (Generic<Integer>[]) new Object[SIZE];
} catch (ClassCastException e) {
System.out.println(e.getMessage());
}
// Runtime type is the raw (erased) type:
gia = (Generic<Integer>[]) new Generic[SIZE];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<>();
//- gia[1] = new Object(); // Compile-time error
// Discovers type mismatch at compile time:
//- gia[2] = new Generic<Double>();
}
}
/* Output:
[Ljava.lang.Object; cannot be cast to [LGeneric;
Generic[]
*/
上面代码是泛型的数组
书上说
成功创建泛型类型的数组的唯一方法是创建一个已擦除类型的新数组,并将其强制转换。
原因书上说:“在运行时,它仍然是一个 Object 数组”。但我不这么认为,这明明是个没泛型的Generic数组。
但是我们永远无法创建具有该确切类型(包括类型参数)的数组
// generics/GenericArray.java
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int sz) {
array = (T[]) new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
// Method that exposes the underlying representation:
public T[] rep() {
return array;
}
public static void main(String[] args) {
GenericArray<Integer> gai = new GenericArray<>(10);
try {
Integer[] ia = gai.rep();
} catch (ClassCastException e) {
System.out.println(e.getMessage());
}
// This is OK:
Object[] oa = gai.rep();
}
}
/* Output:
[Ljava.lang.Object; cannot be cast to
[Ljava.lang.Integer;
*/
上面代码是泛型数组
Integer[] ia = gai.rep();
会报错,这个的原因实际上就是:“这再次是因为实际的运行时类型为 Object[]”,Object是Integer的父类啊,所以不能Integer[] kids=new Object[10];
ArrayOfGeneric.java和GenericArray.java里的代码还是有差别的,前者是带泛型的类的数组,后者是泛型自身的数组
边界
这一章的内容很有意思
这一章里讲的extends
关键字都是用在限定泛型类型时,也就是T extends R
,而不是下一章所讲的? extends R
这一章的EpicBattle.java代码很有趣,也概括了边界的用法,简单应用不谈了,讲两个点
class SuperSleuth<POWER extends XRayVision> extends SuperHero<POWER>
而不能写成class SuperSleuth<POWER> extends SuperHero<POWER extends XRayVision >
,理由和上面的Derived3
类似,required: class or interface without bounds
即需要的都是无边界的类型、接口(包括泛型)- 泛型方法也是同理,例如
<POWER extends SuperHearing> void useSuperHearing(SuperHero<POWER> hero)
,也不能写成<POWER> void useSuperHearing(SuperHero<POWER extends SuperHearing> hero)
通配符
// generics/CovariantArrays.java
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Runtime type is Apple[], not Fruit[] or Orange[]:
try {
// Compiler allows you to add Fruit:
fruit[0] = new Fruit(); // ArrayStoreException
} catch (Exception e) {
System.out.println(e);
}
try {
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // ArrayStoreException
} catch (Exception e) {
System.out.println(e);
}
}
}
/* Output:
java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange
*/
往fruit[]
里放Orange
或者Fruit
类型都会在运行阶段报错,而编译阶段并不会报错。这是因为编译阶段,编译器只知道fruit
是一个Fruit
类型的列表,那么编译器自然会认为往这个列表里放Fruit
类型的对象是可行的;但是在运行阶段,fruit
实际引用的对象是一个Apple
类型的列表,往里放其他类型的当然是不可行的了。
// generics/NonCovariantGenerics.java
// {WillNotCompile}
import java.util.*;
public class NonCovariantGenerics {
// Compile Error: incompatible types:
List<Fruit> flist = new ArrayList<Apple>();
}
就记住,这两者没有关系,编译阶段编译器就已经知道了这俩玩意不一致
尽管你在首次阅读这段代码时会认为“不能将一个 Apple 集合赋值给一个 Fruit 集合”。记住,泛型不仅仅是关于集合,它真正要表达的是“不能把一个涉及 Apple 的泛型赋值给一个涉及 Fruit 的泛型”。
// generics/GenericsAndCovariance.java
import java.util.*;
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<>();
// Compile Error: can't add any type of object:
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null); // Legal but uninteresting
// We know it returns at least Fruit:
Fruit f = flist.get(0);
}
}
我原来一直也不理解通配符到底有什么卵用,它意味着某种 flist
引用没有指定的具体类型只是知道是Fruit
的子类。这就告诉了编译器,我们不在乎也不知道这个具体类型是什么,只知道是某个继承自Fruit
的类型。也因此,没办法向这个列表里添加任何类型的变量了。而就像最后一行代码一样,取出是没什么问题的,以为flist
里的元素,最差也是个Fruit
类型的变量。
List<? extends Fruit> flist = new ArrayList<>();
List<Orange> olist = new ArrayList<>();
flist=olist;
当然,向上面代码那样赋值也是可行的,但同样也没法向flist
添加东西,这就类似于完成了泛型的向上转型。再次理解为什么不能使用add
了,上面代码里,flist
里对象的具体类型丢失了,向其中调用add
方法有可能是不安全的,因此被编译器禁止掉了。
当然你可能会说
List<? extends Fruit> flist = new ArrayList<>();
flist.add(new Fruit());
总安全吧,这有什么不安全的呢?当然不安全了啊,万一flist
是一个new ArrayList<Orange>();
,怎么可能往里添加Fruit
呢?
编译器有多聪明
// generics/CompilerIntelligence.java
import java.util.*;
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist = Arrays.asList(new Apple());
Apple a = (Apple) flist.get(0); // No warning
flist.contains(new Apple()); // Argument is 'Object'
flist.indexOf(new Apple()); // Argument is 'Object'
}
}
那如果我们使用了通配符,是不是就无法调用泛型类里的任何方法了呢?并不是。通过查看源码可以知道,contains
和indexOf
方法接受的参数都是Object
类型的,上一段代码里的get
方法接受的是整数类型,这也就是说
因此当你指定一个
ArrayList<? extends Fruit>
时,add() 的参数(类型)就变成了"? extends Fruit"
。编译器仅仅会拒绝调用像add()
这样参数列表中涉及通配符的方法。
当然返回值是通配符泛型的方法还是可以调用的,例如get
方法
逆变
可以声明:<?super T>
和<?super MyClass>
不可以声明<T super MyClass>
含义就是,MyClass的某个父类
// generics/SuperTypeWildcards.java
import java.util.*;
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
Object object = apples.get(0);
// Apple apple = apples.get(0); // Error
// List<Apple> realApples=apples; // Error
}
}
书上代码并没有最后三行,我自己添加了点。super
和extends
异曲同工,有三点不同,
apples
只知道持有的对象是Apple
的某个父类,但并不知道具体是什么,但与extends
不同的是,向apples
添加Apple
类型的任何子类一定是安全的,所以被编译器放过了。- 但是当使用
get
的时候,他只能返回一个Object
对象,除此之外返回任何其他类型的(例如Fruit
)都有可能是不安全的,因为编译器咋知道它父类都有谁。 - 同样也无法向最后一行那样赋值,这一点与
extends
也相反,在extends
里这样赋值是安全的,而在supper
里是不安全的。因为List<Object>
和List<Fruit>
没有半毛钱关系
自己概括一下这俩的使用时候的区别,extends
类似于把泛型进行了向上转型为边界,但付出的代价是处理泛型对象的能力;super
的功能类似于指定泛型的下界,付出的代价是失去了泛型的具体类型
无界通配符
原生类型就是new ArrayList();
这种不带泛型的。
UnboundedWildcards1.java这段代码虽然注释了很多地方,但其实都是些warning,并不是error,因为只抛了一些warning出来,所以这段代码告诉我们:
编译器很少关心使用的是原生类型还是 <?>
UnboundedWildcards2.java讲的是?
的使用功能之一
我是想用 Java 的泛型来编写这段代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。
例如下面代码,写成<T> void show(List<T> list)
当然也行,但是没意义
void show(List<?> list){
System.out.println("yeah");
}
Wildcards.java代码是一个本节通配符内容的总结归纳,里面有warning和error,总之就是各种各样的规则
rawArgs
内部实际上是可以set
一个Object
类型的,只不过会有个warning而已unboundedArg
内部不可以set
一个Object
类型的,因为即使使用的是无边界通配符,那这个泛型也是确定的某个泛型,不能随随便便把一个Object
类型作为参数传递进去,这个机制与extends
不能add
类似wildSubtype
和wildSupertype
不谈rawArgs
方法的参数是原生类型,其主函数里的四次成功调用说明:原生类型对于参数的类型接受相当宽泛,带不带泛型带啥泛型都行unboundedArg
方法的参数是无边界通配符泛型类,其主函数里的四次调用说明:无边界通配符类型对于参数的类型接受也相当宽泛,带不带泛型带啥泛型都行exact1
方法参数是确定的泛型类参数,从r1
到r4
可以看出,即使泛型类是确定的,原生、确定泛型、无边界泛型和有边界泛型都能传入(当然是在保证不产生冲突的前提下)r5
讲的和第4点相同,r6
没啥说的r7
和r8
是两个error,对比r3
和r4
却成功了,说明extract2
的第二个参数导致了这个泛型T
被唯一确定了,例如exact2(raw, lng);
就导致了泛型类就是个Long
而不是<?>
或者<? extends Long>
,这就引入了冲突。r9
仍然说明原生类型万金油r10
和r12
理所应当,没什么好说的,但r11
也成功了,这一块我不太清楚为什么会成功,我个人理解是:由于使用的是? extends
通配符,因此无法调用以? extends
通配符为参数的方法,所以即使传入了无边界通配符类型的参数,运行过程中也不会产生什么问题,反正你没法调用,不会产生类型方面的错误,所以编译器给了通过;但反过来我们是可以获取以? extends
通配符为返回类型的方法,而由于传入的是无边界,所以返回的只能是Object
才能确保安全。wildSupertype(unbounded, lng);
调用却失败了,个人理解是:由于使用的是? super
通配符,因此是可以调用以? super
通配符为参数的方法,而由于传入了无边界通配符类型的参数,一边是? super
通配符为参数的方法,一边是无边界通配符,这就导致了类型冲突,所以直接被编译器禁止了。wildSupertype(bounded, lng);
调用失败理所应当- 这段代码解释里的一句话特别好:“这取决于是否想要从泛型参数中返回类型确定的返回值(就像在
wildSubtype()
中看到的那样),或者是否想要向泛型参数传递类型确定的参数(就像在wildSupertype()
中看到的那样)”
捕获转换
有一种特殊情况需要使用
<?>
而不是原生类型。如果向一个使用<?>
的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。
问题
任何基本类型不可以作为类型参数
实现参数化接口
// generics/MultipleInterfaceVariants.java
// {WillNotCompile}
interface Payable<T> {}
class Employee1 implements Payable<Employee1> {}
//
class Hourly extends Employee1 implements Payable<Hourly> {}
书上的代码是Employee
但是这个类在别的类里被定义过了,所以我改了个名,上面的Hourly
无法编译,但是下面的代码就可以了,上面代码有冲突,下面代码没冲突
interface Payable<T> {}
class Employee1 implements Payable<Employee1> {}
//
class Hourly extends Employee1 implements Payable<Employee1> {}
转型和警告
GenericCast.java代码很有趣啊
import java.util.*;
import java.util.stream.*;
class FixedSizeStack<T> {
private final int size;
private Object[] storage;
private int index = 0;
FixedSizeStack(int size) {
this.size = size;
storage = new Object[size];
}
public void push(T item) {
if(index < size)
storage[index++] = item;
}
@SuppressWarnings("unchecked")
public T pop() {
return index == 0 ? null : (T)storage[--index];
}
@SuppressWarnings("unchecked")
Stream<T> stream() {
return (Stream<T>)Arrays.stream(storage);
}
}
public class GenericCast {
static String[] letters = "ABCDEFGHIJKLMNOPQRS".split("");
public static void main(String[] args) {
FixedSizeStack<String> strings =
new FixedSizeStack<>(letters.length);
Arrays.stream("ABCDEFGHIJKLMNOPQRS".split(""))
.forEach(strings::push);
System.out.println(strings.pop().getClass().getSimpleName());
strings.stream()
// .map(s -> s + " ")
.forEach(System.out::print);
}
}
书上说 pop
方法里实际上没进行任何转型,因为擦除的存在,运行过程中,里面处理的一直是个Object
,正如上文所说,类型变换发生在边界处,并且是靠编译器完成的。
后边那个类型转换是什么狗屁
重载
永远考虑擦除
基类劫持接口
就是不要有泛型类的冲突
// generics/ComparablePet.java
public class ComparablePet implements Comparable<ComparablePet> {
@Override
public int compareTo(ComparablePet o) {
return 0;
}
}
// generics/RestrictedComparablePets.java
public class Hamster extends ComparablePet implements Comparable<ComparablePet> {
@Override
public int compareTo(ComparablePet arg) {
return 0;
}
}
// Or just:
class Gecko extends ComparablePet {
public int compareTo(ComparablePet arg) {
return 0;
}
}
// generics/HijackedInterface.java
// {WillNotCompile}
class Cat extends ComparablePet implements Comparable<Cat> {
// error: Comparable cannot be inherited with
// different arguments: <Cat> and <ComparablePet>
// class Cat
// ^
// 1 error
public int compareTo(Cat arg) {
return 0;
}
}
上面的代码就造成了泛型类之间的冲突
自限定的类型
古怪的循环泛型
首先记得
Java 中的泛型关乎参数和返回类型。因此它能够产生使用导出类作为其(泛型)参数和返回类型的基类。
这里面的代码还算好理解,就是把导出类型作为基类的泛型
基类用导出类替代其(泛型)参数。
自限定
这是个有趣的用法,
// generics/SelfBounding.java
class SelfBounded<T extends SelfBounded<T>> {
T element;
SelfBounded<T> set(T arg) {
element = arg;
return this;
}
T get() { return element; }
}
class A extends SelfBounded<A> {}
class B extends SelfBounded<A> {} // Also OK
class C extends SelfBounded<C> {
C setAndGet(C arg) {
set(arg);
return get();
}
}
class D {}
// Can't do this:
// class E extends SelfBounded<D> {}
// Compile error:
// Type parameter D is not within its bound
// Alas, you can do this, so you cannot force the idiom:
class F extends SelfBounded {}
public class SelfBounding {
public static void main(String[] args) {
A a = new A();
a.set(new A());
a = a.set(new A()).get();
a = a.get();
C c = new C();
c = c.setAndGet(new C());
}
}
// generics/NotSelfBounded.java
public class NotSelfBounded<T> {
T element;
NotSelfBounded<T> set(T arg) {
element = arg;
return this;
}
T get() { return element; }
}
class A2 extends NotSelfBounded<A2> {}
class B2 extends NotSelfBounded<A2> {}
class C2 extends NotSelfBounded<C2> {
C2 setAndGet(C2 arg) {
set(arg);
return get();
}
}
class D2 {}
// Now this is OK:
class E2 extends NotSelfBounded<D2> {}
上面两段代码,前者SelfBounding.java使用了自限定,后者NotSelfBounded.java没有使用自限定。通过对比才能看懂自限定的功能:
自限定的参数有何意义呢?它可以保证类型参数必须与正在被定义的类相同。
解释一下SelfBounding.java代码:
class SelfBounded<T extends SelfBounded<T>>
要求泛型是个自身类型SelfBounded<T>
的子类class A extends SelfBounded<A>
代码当然没什么问题,因为A
满足边界要求,它是SelfBounded<A>
的子类,这个是自限定的主要用法class B extends SelfBounded<A>
也没问题,因为A
满足边界要求,它是SelfBounded<A>
的子类,但这个不是自限定的主要用法,这个和普通的集成更像一点class E extends SelfBounded<D>
失败了,因为D
满足边界要求,它是SelfBounded<D>
的子类
再解释一下NotSelfBounded.java代码:
class E2 extends NotSelfBounded<D2>
成功了,因为没有自限定的要求
两者结合就能看出来自限定的功能了:它可以保证类型参数必须与正在被定义的类相同(或者继承自某个已经被定义的自限定类)。你看class A
就保证了里面的泛型都必须是A
参数协变
// generics/CovariantReturnTypes.java
class Base {}
class Derived extends Base {}
interface OrdinaryGetter {
Base get();
}
interface DerivedGetter extends OrdinaryGetter {
// Overridden method return type can vary:
@Override
Derived get();
}
public class CovariantReturnTypes {
void test(DerivedGetter d) {
Derived d2 = d.get();
}
}
这种重写方法返回子类的模式叫做协变
// generics/OrdinaryArguments.java
class OrdinarySetter {
void set(Base base) {
System.out.println("OrdinarySetter.set(Base)");
}
}
class DerivedSetter extends OrdinarySetter {
void set(Derived derived) {
System.out.println("DerivedSetter.set(Derived)");
}
}
public class OrdinaryArguments {
public static void main(String[] args) {
Base base = new Base();
Derived derived = new Derived();
DerivedSetter ds = new DerivedSetter();
ds.set(derived);
// Compiles--overloaded, not overridden!:
ds.set(base);
}
}
/* Output:
DerivedSetter.set(Derived)
OrdinarySetter.set(Base)
*/
上述代码的ds
对象有两个set
方法,他们的参数类型不同,因此是重载
// generics/SelfBoundingAndCovariantArguments.java
interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
void set(T arg);
}
interface Setter extends SelfBoundSetter<Setter> {}
public class SelfBoundingAndCovariantArguments {
void
testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
s1.set(s2);
//- s1.set(sbs);
// error: method set in interface SelfBoundSetter<T>
// cannot be applied to given types;
// s1.set(sbs);
// ^
// required: Setter
// found: SelfBoundSetter
// reason: argument mismatch;
// SelfBoundSetter cannot be converted to Setter
// where T is a type-variable:
// T extends SelfBoundSetter<T> declared in
// interface SelfBoundSetter
// 1 error
}
}
书上说:编译器不能识别将基类型当作参数传递给 set() 的尝试,因为没有任何方法具有这样的签名。这里所谓签名就是这个方法的一些特征,比如接受的参数类型,因为s1.set
方法只能接受Setter
类对象,因此s1.set(sbs)
传递一个父类进去当然不行了。
// generics/PlainGenericInheritance.java
class GenericSetter<T> { // Not self-bounded
void set(T arg) {
System.out.println("GenericSetter.set(Base)");
}
}
class DerivedGS extends GenericSetter<Base> {
void set(Derived derived) {
System.out.println("DerivedGS.set(Derived)");
}
}
public class PlainGenericInheritance {
public static void main(String[] args) {
Base base = new Base();
Derived derived = new Derived();
DerivedGS dgs = new DerivedGS();
dgs.set(derived);
dgs.set(base); // Overloaded, not overridden!
}
}
/* Output:
DerivedGS.set(Derived)
GenericSetter.set(Base)
*/
上述代码发生的也是重载而不是重写
动态类型安全
这个是用来处理类型安全的讲解
// generics/CheckedList.java
// Using Collection.checkedList()
import java.util.*;
class Pet{}
class Cat extends Pet{}
class Dog extends Pet{}
public class CheckedList {
@SuppressWarnings("unchecked")
static void oldStyleMethod(List probablyDogs) {
probablyDogs.add(new Cat());
}
public static void main(String[] args) {
List<Dog> dogs1 = new ArrayList<>();
oldStyleMethod(dogs1); // Quietly accepts a Cat
List<Dog> dogs2 = Collections.checkedList(
new ArrayList<>(), Dog.class);
try {
oldStyleMethod(dogs2); // Throws an exception
} catch(Exception e) {
System.out.println("Expected: " + e);
}
// Derived types work fine:
List<Pet> pets = Collections.checkedList(
new ArrayList<>(), Pet.class);
pets.add(new Dog());
pets.add(new Cat());
}
}
/* Output:
Expected: java.lang.ClassCastException: Attempt to
insert class typeinfo.pets.Cat element into collection
with element type class typeinfo.pets.Dog
*/
我惊了,dogs1
真的能插入一只猫,并且如果我尝试获取这只猫Dog d=dogs1.get(0);
,会报错java.lang.ClassCastException: chapter20.Cat cannot be cast to chapter20.Dog
,那么checkList
的功能正如书上所说
受检查的集合(经过check的)在你试图插入类型不正确的对象时抛出 ClassCastException ,这与泛型之前的(原生)集合形成了对比,对于后者来说,当你将对象从集合中取出时,才会通知你出现了问题。
为什么能成功插入一只猫呢?因为oldStyleMethod
接受的是原生集合,丢掉了泛型信息,编译器无法判定插入的是正确的还是错误的,只能认为这个泛型就是个Object
,而又因为实际运行过程中操作的都是Object
,然后插入这只猫的时候,也是当Object
插入的,所以成功了。
为什么取出的时候失败了呢?因为泛型的类型转换发生在边界,运行期间列表里存的实际上都是Object
,取出的时候要进行类型转换把他转换成Dog
的时候,失败了。
泛型异常
就是继承自Exception
类的泛型,没啥特殊的地方
混型
C++中的混型
与接口混合
使用装饰器模式
简单介绍:装饰器模式
实际上就是给一个对象增强功能
在Mixins.java文件里,为了将BasicImp
、TimeStamped
和SerialNumbered
的功能统一到一个类里,我们创建了一个继承了一大堆东西的Mixin
类
装饰器的作用
装饰器经常用于满足各种可能的组合
Decoration.java文件的目标是这样的,我们现在有一个Basic
类的对象,现在希望这个对象能够有点新的功能,希望他能有个像Mixins.java里的TimeStamped
那样的功能,但又不希望弄出个新子类,怎么办?
// generics/decorator/Decoration.java
// {java generics.decorator.Decoration}
package generics.decorator;
import java.util.*;
class Basic {
private String value;
public void set(String val) { value = val; }
public String get() { return value; }
}
class Decorator extends Basic {
protected Basic basic;
Decorator(Basic basic) { this.basic = basic; }
@Override
public void set(String val) { basic.set(val); }
@Override
public String get() { return basic.get(); }
}
class TimeStamped extends Decorator {
private final long timeStamp;
TimeStamped(Basic basic) {
super(basic);
timeStamp = new Date().getTime();
}
public long getStamp() { return timeStamp; }
}
class SerialNumbered extends Decorator {
private static long counter = 1;
private final long serialNumber = counter++;
SerialNumbered(Basic basic) { super(basic); }
public long getSerialNumber() { return serialNumber; }
}
public class Decoration {
public static void main(String[] args) {
TimeStamped t = new TimeStamped(new Basic());
TimeStamped t2 = new TimeStamped(
new SerialNumbered(new Basic()));
//- t2.getSerialNumber(); // Not available
SerialNumbered s = new SerialNumbered(new Basic());
SerialNumbered s2 = new SerialNumbered(
new TimeStamped(new Basic()));
//- s2.getStamp(); // Not available
}
}
思路就是,让TimeStamped
持有一个Basic
对象就可以了啊,而代码里使用了Decorator
这个类来帮助他持有这个Basic
对象。
所以,所谓装饰器模式,就是拒绝创建新子类,仅仅靠持有原有对象,在新类里加上点功能。
当然书里也说了装饰器的缺陷
只有最后一层的方法是可视的。明显的缺陷是它只能有效地工作于装饰中的一层
意思就是,以t2
为例,主函数里的t2
实际上和Mixin
一样,拥有着三种功能,System.out.println(((SerialNumbered)t2.basic).getSerialNumber());
就可以获得序列码了,但从方法签名上看,我们不知道他也能打印序列码的,我们只知道他除了能干Basic
的活,就只能打印时间戳了
与动态代理混合
// generics/DynamicProxyMixin.java
import java.lang.reflect.*;
import java.util.*;
import onjava.*;
import static onjava.Tuple.*;
class MixinProxy implements InvocationHandler {
Map<String, Object> delegatesByMethod;
@SuppressWarnings("unchecked")
MixinProxy(Tuple2<Object, Class<?>>... pairs) {
delegatesByMethod = new HashMap<>();
for(Tuple2<Object, Class<?>> pair : pairs) {
for(Method method : pair.a2.getMethods()) {
String methodName = method.getName();
// The first interface in the map
// implements the method.
if(!delegatesByMethod.containsKey(methodName))
delegatesByMethod.put(methodName, pair.a1);
}
}
}
@Override
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
String methodName = method.getName();
Object delegate = delegatesByMethod.get(methodName);
return method.invoke(delegate, args);
}
@SuppressWarnings("unchecked")
public static Object newInstance(Tuple2... pairs) {
Class[] interfaces = new Class[pairs.length];
for(int i = 0; i < pairs.length; i++) {
interfaces[i] = (Class)pairs[i].a2;
}
ClassLoader cl = pairs[0].a1.getClass().getClassLoader();
return Proxy.newProxyInstance(cl, interfaces, new MixinProxy(pairs));
}
}
public class DynamicProxyMixin {
public static void main(String[] args) {
Object mixin = MixinProxy.newInstance(
tuple(new BasicImp(), Basic.class),
tuple(new TimeStampedImp(), TimeStamped.class),
tuple(new SerialNumberedImp(), SerialNumbered.class));
Basic b = (Basic)mixin;
TimeStamped t = (TimeStamped)mixin;
SerialNumbered s = (SerialNumbered)mixin;
b.set("Hello");
System.out.println(b.get());
System.out.println(t.getStamp());
System.out.println(s.getSerialNumber());
}
}
/* Output:
Hello
1494331653339
1
*/
就是创建了一个动态代理对象,这个对象持有了三个用于实现三种功能的对象,delegatesByMethod
记录了方法名-持有方法的对象,但这样可能产生冲突,如果两个对象A,B
之间的方法存在重载关系,那这两个方法的名称一样豆角func
,如果map里记录的是func-A
,而我想调用的是B
对象的func
,上述代码就会错误
详细来说,如果上面连个接口的方法如下所示
interface SerialNumbered {
long getSerialNumber();
public void overload(String i);}
interface TimeStamped { long getStamp(); public void overload(int i);}
在上述动态代理代码里调用如下代码,就会报错
public class DynamicProxyMixin {
public static void main(String[] args) {
Object mixin = MixinProxy.newInstance(
tuple(new BasicImp(), Basic.class),
tuple(new TimeStampedImp(), TimeStamped.class),
tuple(new SerialNumberedImp(), SerialNumbered.class));
Basic b = (Basic) mixin;
TimeStamped t = (TimeStamped) mixin;
SerialNumbered s = (SerialNumbered) mixin;
t.overload(2);
s.overload("1");
}
}
如下所示,说对象不是所声明类的一个实例,什么意思呢?当我们执行s.overload("1");
的时候,实际上执行的是method.invoke(delegate, args);
,而在上述代码里,delegate
就是一个那个new TimeStampedImp()
,而要调用这个对象的overload
方法,这个方法需要的是int
,传入的却是个字符串"1"
,那么java就会告诉我们对象"1"
不是声明类int
的一个实例,调用失败
最后最后newInstance
方法可以稍微改改,SafeVarargs
代表我们不会对变长参数进行修改,@SafeVarargs注解的使用
@SafeVarargs
public static Object newInstance(Tuple2<Object,Class<?>>... pairs) {
Class<?>[] interfaces = new Class[pairs.length];
for (int i = 0; i < pairs.length; i++) {
interfaces[i] = pairs[i].a2;
}
ClassLoader cl = pairs[0].a1.getClass().getClassLoader();
return Proxy.newProxyInstance(cl, interfaces, new MixinProxy(pairs));
}
Class<?>[] interfaces = new Class[pairs.length];
不能写成Class<?>[] interfaces = new Class<?>[pairs.length];
,范型数组那节讲过,你永远无法创建具有具体类型的数组
另外我还发现了无边界通配符的一点补充,我复用了Holder
类,下面代码会编译错误,don’t know why
static void ho(Holder<Holder<?>> h){
}
public static void main(String[] args) {
Holder<Holder> t=new Holder<>();
ho(t);//编译错误,需要的是Holder<Holder<?>>,而并非Holder<Holder>
}
对缺乏潜在类型机制的补偿
反射
将一个方法应用于序列
反射提供了一些有用的可能性,但是它将所有的类型检查都转移到了运行时,因此在许多情况下并不是我们所希望的。
Java8 中的辅助潜在类型
// generics/DogsAndRobotMethodReferences.java
// "Assisted Latent Typing"
import typeinfo.pets.*;
import java.util.function.*;
class PerformingDogA extends Dog {
public void speak() { System.out.println("Woof!"); }
public void sit() { System.out.println("Sitting"); }
public void reproduce() {}
}
class RobotA {
public void speak() { System.out.println("Click!"); }
public void sit() { System.out.println("Clank!"); }
public void oilChange() {}
}
class CommunicateA {
public static <P> void perform(P performer,
Consumer<P> action1, Consumer<P> action2) {
action1.accept(performer);
action2.accept(performer);
}
}
public class DogsAndRobotMethodReferences {
public static void main(String[] args) {
CommunicateA.perform(new PerformingDogA(),
PerformingDogA::speak, PerformingDogA::sit);
CommunicateA.perform(new RobotA(),
RobotA::speak, RobotA::sit);
CommunicateA.perform(new Mime(),
Mime::walkAgainstTheWind,
Mime::pushInvisibleWalls);
}
}
/* Output:
Woof!
Sitting
Click!
Clank!
*/
尽管他们不需要实现通用接口Performs
了,但还得实现Consumer
接口。
上面的方法是非绑定方法调用,RobotA::speak
代表着需要实现的方法接受一个RobotA
对象,调用其speak
方法