泛型
2.简单泛型
-********
Java泛型的核心概念:告诉编译器想使用什么类型, 然后编译器帮你处理一切细节
2.1 一个元组类库
元组:将一组对象直接打包存储于其中的一个单一对象.这个容器对象允许读取其中元素, 但是不允许向其中存放新的对象.
2.2 一个堆栈类
末端哨兵 end sentinel来判断堆栈何时为空
2.3 RandomList
构建一个可以应用于各种类型的对象的List工具
3.泛型接口
泛型也可以用于接口, 例如生成器, 这是一种专门负责创建对象的类. 是工厂方法设计模式的一种应用.
这里写了一个Generator类, 用泛型来生成不同类型的对象.
4. 泛型方法
4.1杠杆利用类型参数判断
类型参数推断避免了重复的泛型参数列表.
Map<Person,List<? extends Pet>> petpeople = New.map()
这样就在后面避免了重复写参数类型, 但是这样只对赋值操作有效
4.2 可变参数与泛型方法
泛型方法和可变参数列表可以很好地共存.
4.3 用于Generator的泛型方法
这里利用了之前的Generator生成器来填充一个List.通过泛型化操作可以填充不同的类型.
4.4 一个通用的Generator
这里写了一个可以为任何一个具有默认构造器的类构造Generator的方法.
生成的BasicGenerator中的next()方法可以产生一个该类的对象.
4.5 简化元组的使用
4.6 一个Set实用工具
介绍了并集,交集等的set方法.
5. 匿名内部类
泛型还可以用于内部类以及匿名内部类.
6. 构建复杂模型
泛型的好处就是能够简单而安全的创建复杂的模型.
7. 擦除的神秘之处
ArrayList<Integer>().getClass()==ArrayList<String>().getClass()
这里两者是相同的类型, 因为在代码内部,我们无法获得任何有关泛型参数类型的信息.
Java泛型是用擦除来实现的, 这意味着你在使用泛型的时候, 任何具体的类型信息都被擦除了.你唯一知道的就是你在使用一个对象.
7.1 C++的方式
C++中, 当模板被实例化时, 模板代码就知道其参数类型.
由于有擦除,Java无法知晓参数的类型.为了调用某些方法, 我们必须给定泛型类的边界,以告知编译器只能接受遵循这个边界的类型, 通过extends关键字. List<T extends Haf>
泛型类型参数将擦除到他的第一个边界.
7.2 迁移兼容性
如果泛型在Java1.0中就是一部分了,那么这个特性将不会使用擦除来实现, 它将使用具体化, 使类型参数保持为第一类实体, 因此就能够在类型参数上执行基于类型的语言操作和反射操作.
在基于擦除的实现中, 泛型类型被当做第二类类型处理.即不能在某些重要的上下文环境中使用的类型.
泛型类型只有在静态类型检查期间才出现.在此之后, 程序中的所有泛型类型都将被擦除替换为他们的非泛型上界. 比如List<Integer> 被擦除为List.
擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用.这被称为迁移兼容性.
所以Java类型不仅必须支持向后兼容性, 而且还要支持迁移兼容性, 使得类库按照他们自己的步调变为泛型的. 所以擦除是唯一可行的解决方案, 允许非泛型代码与泛型代码共存.
7.3 擦除的问题
擦除的代价是显著的. 泛型不能显式的引用运行时类型的操作之中,比如转型, instanceOf操作和new表达式. 因为所有有关参数的类型信息都丢失了.
7.4 边界处的动作
在泛型中创建数组:Array.newInstance()
泛型最令人困惑的方面是因为他可以表示任何没有意义的事物.
即使编译器因为擦除而无法知道任何类型的信息, 但是他仍然可以在编译期内确保你放置到result中的对象具有T类型, 使其适合ArrayList.
也就是即使擦除在方法或类内部移除了有关实际类型的信息, 编译期仍旧可以确保在方法或者类中使用的类型的内部一致性.
因为擦除在方法体中移除了类型信息, 所以在运行时的问题就是边界"即对象进入和离开的地点,这些正是编译器在编译期执行类型检查并插入转型代码的地点.
8. 擦除的补偿
擦除使得任何在运行时需要知道确切类型信息的操作都无法进行.
但是有时候必须通过引入类型标签来对擦除进行补偿, 这意味着你需要显示的传递你的类型的Class对象, 以便可以在表达式中使用它.
8.1 创建类型实例
创建一个new T()的尝试将无法实现, 部分原因是因为擦除, 而部分原因是因为编译器不能验证T具有默认的构造器.
Java的解决方法是传递一个工厂对象, 并使用它来创建新的实例, 通过newInstance()来创建这个类的新对象.
T x = kind.newInstance();
另一个方法是模板方法设计模式
8.2 泛型数组
不能创建泛型数组, 一般的解决方案是在任何想要创建泛型数组的地方都使用ArrayList
成功创建泛型数组唯一方式就是创建一个被擦除类型的新数组,然后对其转型.
9. 边界
边界使得你可以在用于泛型的参数类型上设置限制条件,可以按照自己的边界类型来调用方法.
10. 通配符
数组的特殊行为: 可以向导出类型的数组赋予基类型的数组引用.
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple();
fruit[1] = new Jonathan();
fruit[0] = new Fruit();//在运行期会报错
这里将Apple数组赋值给一个Fruit数组引用, 但是数组的实际类型是Apple, 只能在其中放置Apple或者他的子类型. 如果放置其他的类型, 编译期不会报错. 但是在运行的时候数组机制知道他处理的是Apple的数组,因此会抛出异常.
这种赋值的异常,在运行期可以发现. 而泛型的主要目标之一是将这种错误检测移到编译期.
List<Fruit> list = new ArrayList<Apple>();
这是无法编译的.这里不是向上转型.
10.1 编译器有多聪明
add()方法接受一个具有泛型参数类型的参数, 而contains()和indexOf()将接受Object类型的参数.
这意味着将由泛型类的设计者来决定哪些调用是安全的.
为了在类型中使用了通配符的情况下禁止这类调用, 我们需要在参数列表中使用类型参数.
也就是在add,set这样的方法参数中加上类型参数, 就会变编译期报错提示.
如果创建了一个Holder ,不能将其向上转型为Holder, 但是可以向上转型为Holder<? extends Fruit>. 然后调用get, 会返回一个Fruit对象, 这是因为设置的边界. 同时可以转型为apple :d = (Apple)fruit.get();转型为其他类型可能会得到转换异常信息.
但是Set方法不能工作于Apple或者Fruit, 因为Set()的参数是? extends Fruit,这意味着他可以是任何事物, 而编译器无法验证任何事物的安全性.
也就是说, 可以get不能set
10.2 逆变
另一种方法是用"超类型通配符", 这里可以声明通配符是由某个特定类的任何基类来界定的.
方法是指定<? super MyClass>,或者使用类型参数<? super T>
static <T> void writeExact(List<T> list, T item){
list.add(item);
}
static <T> void writeWithWildcard(List<? super T> list, T item){
list.add(item);
}
static List<Fruit> fruit = new ArrayList<Fruit>();
writeExact(fruit ,new Apple());//报错 类型不匹配
writeWithWildcard(fruit, new Apple());//可以编译
在writeWithWildcard中, 其参数是List<? super T>,因此这个List将持有从T导出的某种具体类型.这样就可以将一个T类型的或者从T导出的类型的对象作为参数传递进去.
10.3 无界通配符
无界通配符<?> 看起来好像意味着"任何事物", 因此使用无界通配符好像等价于使用原生类型.
他的意思是: 我是想用Java的泛型来编写这段代码, 我在这里并不是要使用原生类型, 但是在当前这种情况下, 泛型参数可以持有任何类型.
Map<?,?> map = Map map;
List<?> 表示"具有某种特定类型的非原生List,只是我们不知道那种类型是什么"
10.4 捕获转换
f1()中的参数类型是确切的, 没有通配符或边界, f2()中参数是一个无界通配符, 同时调用f1().
参数类型在f2()的调用过程中被捕获, 因此可以在对f1()的调用中被使用.
11. 问题
11.1 任何基本类型都不能作为类型参数
不能创建Array<int>, 解决方法是用过Java基本类型的自动包装机制, 创建Array<Integer>
11.2 实现参数化接口
一个类不能实现同一个泛型接口的两种变体, 由于擦除的原因, 这两个变体会成为相同的接口.
11.3 转型和警告
使用带有泛型类型参数的转型或instanceOf不会有任何效果,.
11.4 重载
其实和接口相同
11.5 基类劫持了接口
12. 自限定的类型
12.1 古怪的循环泛型CRG
class GenericType<T>{}
public class CuriouslyRecurringGeneric extends
GenericType<CuriouslyRecurringGeneric> {}
它能够产生使用导出类作为其参数和返回类型的基类, 它还能将导出类型用作其域类型. CRG的本质: 基类用导出类型替代其参数.
12.2 自限定
自限定所做的, 就是要求在继承关系中, 像下面这样使用这个类
class A extends SelfBounded<A>{}
这会强制要求将正在定义的类当做参数传递给基类
意义: 他可以保证类型参数必须与正在被定义的类相同.
自限定只能强制作用于继承关系. 如果使用自限定, 就应该了解这个类所用的类型参数将于使用这个参数的类具有相同的基类型.
12.3 参数协变
自限定类型的价值在于他们可以产生协变参数类型--反法参数类型会随子类而变化
子类重写父类的方法并且修改返回值的类型为子类.
如果不使用自限定类型, 普通的继承机制就会介入, 而你将能够重载.也就是既可以返回子类的对象, 也可以调用父类的方法, 接受其他类型返回其他对象.
13. 动态类型安全
因为可以向JavaSE5之前的代码传递泛型容器, 所以旧式代码有可能会破坏你的容器.
java.util.Collections中的一组工具会解决这种情况下的类型检查问题.
chekedList(param1, param2)param1是被检查的list, param2是希望强制要求的类型.
受检查的容器在你试图插入类型不正确的对象时抛出ClassCastException.
而之前的旧List在放入的时候是不会报错的, 只有在取出的时候才会报错.
14. 异常
因为擦除的原因, 在编译期和运行期都不知道异常的确切类型, 所以catch不能捕获.
15. 混型
基本概念: 混合多个类的能力, 以产生一个可以表示混型中所有类型的类, 它使组装多个类变得简单易行.
混型的价值之一是他们可以将特性和行为一致的应用于多个类之上.
15.1 C++中的混型
15.2 与接口混合
常见的方案是用接口来产生混型效果.
15.3 使用装饰器模式
15.4 与动态代理混合
可以使用动态代理来创建一种比装饰器更贴近混型模型的机制.
通过使用动态代理, 所产生的类的动态类型将会是已经混入的组合类型
16. 潜在类型机制
潜在类型机制使得你可以横跨类继承结构, 调用不属于某个公共接口的方法.