万物皆对象
把万物看作对象,对象之间在传递信息。
对象的创建,对象的存储(数量、类型),对象之间的关系处理(对象间传递信息),对象的销毁,异常处理
抽象
封装
继承
Java 为单继承语言,有别于多继承的C++
多态
多样式与向上转型
多态和构造器
多态的好处在于解耦
复用
组合和继承
构造器加载顺序(对比销毁顺序)
接口与抽象类
接口为抽象类
接口被用来建立类之间的协议
接口中的守卫方法或虚拟扩展方法(default)
类可以实现多个接口,但是抽象类只能继承单一抽象类
面向接口编程,将接口与实现解耦可以应用于多种不同的实现
选择问题:
尽可能地抽象,更倾向使用接口而非抽象类
接口和策略模式
接口和工厂模式
优先使用类而不是接口,若有必要使用接口,再对代码重构也不迟。如果只是单纯为了设计接口而设计接口,只会徒增复杂性。
内部类
内部自动类拥有对外部类所有成员的访问权,外部类与内部类产生的引用
匿名内部类,与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备,而如果是实现接口,也只能实现一个接口。
嵌套类
内部类有效地实现了多重继承,每个内部类都能独立地继承自一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实习,对于内部类都没有影响
内部类的其他特性:
内部类可以有多个实例
再单个外部类中,可以让多个内部类以不同的方 式实现
创建内部类对象的时刻并不依赖于外部类对象的 创建
内部类并没有令人迷惑的“is-a”关系,它就是一个独立的实体
内部类提供的闭包功能,相比指针更灵活更安全
回调的价值在于它的灵活性-可以再运行时动态地决定需要调用什么方法
在控制框架使用内部类的价值
局部内部类和匿名内部类
设计模式总是将变化的事物与保持不变的事物分离开,在这个模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物
集合
持有对象的思想
泛型与类型安全的集合
散列码和hashCode()
List可以在创建后添加或删除元素,并自行调整大小
collection
迭代器,能够将遍历序列的操作与该序列的底层结构分离
for-in 和迭代器
集合和迭代器
上图中用粗黑的框包裹的类为常用的类
常见问题
1.hashmap如何解决哈希碰撞,都有哪些方法?
为什么hashtable 桶数通常会取一个素数?如何有效避免hash结果值的碰撞
2.hashmap的内存利用率?如果想让内存利用率达到100%,并且时间复杂度降到O(1)怎么办?
3.源码问题,集合类的底层实现
4.安全性问题,线程安全与否
5.Map源码
函数式编程
lambda表达式
方法引用
函数式接口:
在使用函数式接口时,名称无关紧要,只要参数类型和返回类型相同
高阶函数
闭包,利用闭包可以轻松生成函数。支持闭包也叫变量捕获。只要有内部类就会有闭包
等同final效果
函数组合,多个还能输组合成新函数
柯里化和部分求值
柯里化,将一个多参数函数转换为一系列单参数函数
纯函数式编程,Scala,Clojure
流式编程
集合优化了对象的存储,而流和对象有关。流是一系列与特定存储机制无关的元素。利用流,可以不迭代集合中的元素,就可以提取和操作数据。流的好处是,它使得程序更加短小和容易理解。
Lambda表达式和方法引用结合流式编程会更加简便、简洁。
流式编程是一种声明式编程,声明要做什么,而非怎么做的编程风格。
流式编程采用内部迭代。
流是懒加载的。
流操作,创建流,修该流元素,消费流元素
创建流:
stream.of(); stream(); 集合通过stream()方法来产生一个流。
中级流操作
optional类
终端操作
异常
异常处理程序,不仅能节省代码,而且把“描述在正常执行剁成中做什么事”和“出了问题怎么办”的代码相分离。异常机制使代码的阅读、编写和调试工作更加井井有条。
基本异常
异常捕获
自定义异常
异常声明
重新抛出异常
异常链
Java标准异常
finally 用于把除内存之外的资源恢复到他们初始状态时。如果把finally子句和带标签的break及continue配合使用,在Java里就没必要使用goto语句了
异常丢失
异常限制
构造器和异常处理
Try-With-Resources和构造器异常处理
异常匹配
异常处理的一个重要原则是“只有在你知道如何处理的情况下才捕获异常”。异常处理的一个中哟啊目标就是把错误处理的代码同错误发生的地点相分离。
吞了异常
被检查的异常与并发症
所有模型都是错误的,但有些是 能用的。
反射和泛型就是用来补偿静态类型检查所带来的过多限制。
好的程序设计语言能帮助程序员写出好程序,但无论哪种语言都避免不了程序员用它写出坏程序。
把被检查的异常转换为不检查的异常
异常链
用RuntimeException来包装,被检查的异常
异常指南
1.尽可能使用 try-with-resource。
2.在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)
3.解决问题并且重新调用产生异常的方法。
4.进行少许修补,然后绕过异常发生的地方继续执行。
5.用别的数据进行计算,以代替方法预计会返回的值。
6.把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
7.把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
8.终止程序。
9.进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人。)
10.让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。)
报告功能是异常的精髓所在
代码校验
让代码健壮的方法
测试
单元测试
前置条件(契约式设计DBC)
断言
Java断言语法
Guava断言
检查指令
测试驱动开发(TDD)
日志
调试
JDB、图形化调试器
基准测试
剖析和优化
剖析和优化
优化准则
避免为了性能牺牲代码的可读性。
不要独立地看待性能。衡量与带来的收益相 比所需投入的工作量。
程序的大小很重要。性能优化通常只对运行 了长时间的大型项目有价值。性能通常不是小项 目的关注点。
运行起来程序比一心钻研它的性能具有更高 的优先级。一旦你已经有了可工作的程序,如有 必要的话,你可以使用剖析器提高它的效率。只 有当性能是关键因素时,才需要在设计/开发阶段 考虑性能。
不要猜测瓶颈发生在哪。运行剖析器,让剖 析器告诉你。
无论何时有可能的话,显式地设置实例为 null 表明你不再用它。这对垃圾收集器来说是个 有用的暗示。
static final 修饰的变量会被 JVM 优化从而 提高程序的运行速度。因而程序中的常量应该声 明 static final。
风格检测
静态错误分析
代码重审
结对编程
重构:
重构基石:
测试
自动构建
版本控制
持续集成
持续集成服务器
持续集成需要分布式版本管理,自动构建和自动 测测试系统作为基础
文件
流与文件结合使得文件操作编程变得更加优雅
文件和目录路径
选取部分路径片段
路径分析
FIile工具类
文件系统
路径监听
文件删除和线程
文件查找
文件读写
java.nio.file
java.nio.file.Files
字符串
字符串的不可变
参数是为该方法提供信息的,而不是想让该方法 改变自己的
+的重载与StringBuilder
不可变性与效率,string与stringbuilder类
在有循环且有性能问题时,使用stringbuilder类
stringbuffer与stringbuilder,stringbuffer是线 程安全的,因此开销会大一些。
意外递归
打印对象内存地址,使用super.tostring(),而不 去使用this, 使用this会发生自动类型转换、递归调用
字符串操作
当需要改变字符串的内容时,String
类的方法 都会返回一个新的 String
对象。同时,如果内容不 改变,String
方法只是返回原始对象的一个引用而 已。这可以节约存储空间以及避免额外的开销
在 Java 中,字符串操作还主要集中于String
、 StringBuffer
和 StringTokenizer
类
格式化输出
printf()
System.out.format()
Formatter类,
在 Java 中,所有的格式化功能都是由 java.util.Formatter
类处理的。
格式化修饰符
Formatter
转换
还有许多不常用的类型转换与格式修饰符选 项,你可以在 JDK 文档中的 Formatter
类部分 找到它们。
String.format()
在 String.format()
内部,它也是创建了 一个 Formatter
对象,然后将你传入的参数转 给 Formatter
。不过,与其自己做这些事情,不 如使用便捷的 String.format()
方法,何况这 样的代码更清晰易读。
一个十六进制转储(dump)工具
为了打开及读入二进制文件,我们用到了另一个 工具 Files.readAllBytes()
,这已经在 Files章节 介绍过了。这里的 readAllBytes()
方法将整个文件 以 byte
数组的形式返回
正则表达式
处理string的匹配、选择、编辑以及验证
String.split()
还有一个重载的版本,它允许 你限制字符串分割的次数
创建正则表达式
正则表达式的完整构造子列表,请参考JDK文档 java.util.regex
包中的 Pattern
类
当你学会了使用字符类(character classes)之 后,正则表达式的威力才能真正显现出来
量词
量词描述了一个模式捕获输入文本的方式
CharSequence
接口 CharSequence
从 CharBuffer
、 String
、StringBuffer
、StringBuilder
类中抽象 出了字符序列的一般化定义
Pattern和
Matcher
java.util.regext.Matcher
find()
Matcher.find()
方法可用来在 CharSequence
中查找多个匹配
start()和
end()
Pattern
标记
split()
替换操作
reset()
正则表达式与 Java I/O
扫描输入
Scanner
分隔符
StringTokenizer类
在 Java 引入正则表达式(J2SE1.4)和 Scanner
类(Java SE5)之前,分割字符 串的唯一方法是使用 StringTokenizer
来 分词。不过,现在有了正则表达式和 Scanner
,我们可以使用更加简单、更加简洁 的方式来完成同样的工作了
基本上,我们可以放心地说, StringTokenizer
已经可以废弃不用了。
运算符
Java &、&&、|、||、^、<<、>>、~、>>>等运算符
类型信息
java如何在运行时识别对象和类信息:
“传统的”RTTI(运行时类型信息)
使用 RTTI,我们可以查询某个 Shape
引用所指向对象的确切类型, 然后择或者剔除特例。
“反射”机制:允许我们在运行时发现和 使用类信息。
Class
对象
类加载器
原生类加载器与额外的类加载器
Class
对象仅在需要的时候才会被 加载,static
初始化是在类加载时进 行的
无论何时,只要你想在运行时使用 类型信息,就必须先得到那个 Class
对 象的引 用。Class.forName()
就是 实现这个功能的一个便捷途径,因为使 用该方法你不需要先持有这个类型 的对 象。但是,如果你已经拥有了目标类的 对象,那就可以通过调用 getClass()
方法来获取 Class
引用了,这个方法来 自根类 Object
,它将返回表示该对象 实际类型的 Class
对象的引用
Class
对象的 newInstance()
方 法是实现“虚拟构造器”的一种途径,虚 拟构造器可以让你在不知道一个类的确 切类型的时候,创建这个类的对象
类字面常量
类字面常量用于生成类对象的引用
为了使用类而做的准备工作实际包含三 个步骤
加载:这是由类加载器执行的。该步骤 将查找字节码(通常在 classpath 所指定的 路径中查找,但这并非是必须的),并从这 些字节码中创建一个 Class
对象
链接:在链接阶段将验证类中的字节 码,为 static
字段分配存储空间,并且如 果需要的话,将解析这个类创建的对其他类 的所有引用。
初始化:如果该类具有超类,则先初始 化超类,执行 static
初始化器和 static
初始化块。
仅使用 .class
语法来获得对类对象的 引用不会引发初始化。但与此相反,使用 Class.forName()
来产生 Class
引用会 立即就进行初始化
泛化的 Class
引用
向 Class
引用添加泛型语法的原因只是 为了提供编译期类型检查
cast()
方法
Java 中用于 Class
引用的转型语 法
类型转换检测
已知的 RTTI 类型
传统的类型转换,如 “(Shape)
”, 由 RTTI 确保转换的正确性,如果执行了 一个 错误的类型转换,就会抛出一 个 ClassCastException
异常。
代表对象类型的 Class
对象. 通过 查询 Class
对象可以获取运行时所需的 信息.
RTTI 在 Java 中还有第三种形式,那就 是关键字 instanceof
使用类字面量
使用类字面量重新实现 PetCreator
类的话,其结果在 很多方面都会更清晰。
一个动态 instanceof
函数
Class.isInstance()
方法提供 了一种动态测试对象类型的方法。
isInstance()
方法消除了对 instanceof
表达式的需要
递归计数
可以使用 Class.isAssignableFrom()
而不是预加载 Map
,并创建一个不限于计数 Pet
的通用工具
注册工厂
类的等价比较
查询类型信息时,需要注意: instanceof 的形式(即 instanceof
或 isInstance()
,这两者产生的结果相同) 和与 Class 对象直接比较这两者间存在重要区别
instanceof
说的是“你是这个类,还是从这个类派生的类?”。而如果使用 ==
比较实际的Class
对象,则与继承无关 —— 它要么是确切的类型,要么不是。
反射:运行时类信息
如果你不知道对象的确切类型,RTTI 会告 诉你。但是,有一个限制:必须在编 译时 知道类型,才能使用 RTTI 检测它,并对信息做 一些有用的事情。换句话说,编译器必须知道你 使用的所有类
反射提供了检测可用方法并生成方法名称 的机制
在运行时发现类信息的另一个令人信服的 动机是提供跨网络在远程平台上创建和执行对象 的能力。这称为远程方法调用(RMI),它使 Java 程序的对象分布在许多机器上
重要的是要意识到反射没有什么魔力。当 你使用反射与未知类型的对象交互时,JVM 将查 看该对象,并看到它属于特定的类(就像普通的 RTTI)。在对其执行任何操作之前,必须加载 Class
对象。因此,该特定类型的 .class
文件必须在本地计算机上或通过网络对 JVM 仍 然可用。因此,RTTI 和反射的真正区别在于, 使用 RTTI 时,编译器在编译时会打开并检查 .class文件。换句话说,你可以用“正常”的方式 调用一个对象的所有方法。通过反射,.class文 件在编译时不可用;它由运行时环境 打开 并检查。
类方法提取器
反射是用来支持其他java特性的,如对象序 列化、动态提取有关类的信息
编程时,当你不记得某个类是否有特定的 方法,并且不想在 JDK 文档中搜索索引或类层 次结构时,或者如果你不知道该类是否可以对 Color
对象执行任何操作时,该工具能节省不 少时间
动态代理
当你希望将额外的操作与“真实对象”做分离 时,代理可能会有所帮助,尤 其是当你想要轻松 地启用额外的操作时,反之亦然(设计模式就是 封装 变更—所以你必须改变一些东西以证明模 式的合理性)。例如,如果你想跟踪 RealObject中方法的调用,或衡量此类调用的开销,该怎么 办?你不想这部分 代码耦合到你的程序中,而代理能使你可以很轻松地添加或删除它
Java 的动态代理更进一步,不仅动态创建代理对 象而且动态处理对代理方法的调用。在动态代理上进行 的所有调用都被重定向到单个调用处理程序,该处理程 序负责发现调用的内容并决定如何处理
可以通过调用静态方法Proxy.newProxyInstance()来创建动态代理,该方法需要一个类加载器(通常可以从已加载的对象中获取)
通常执行代理操作,然后使用 Method.invoke()
将请求转发给被代理对象,并携带必要的参数。这在一开始看起来是有限制的,好像你只能执行一般的操作。但是,可以过滤某些方法调用,同时传递其他方法调用:
Optional类
如果你使用内置的 null
来表示没有对象,每次 使用引用的时候就必须测试一下引用是否为 null
,这 显得有点枯燥,而且势必会产生相当乏味的代码。问题 在于 null
没什么自己的行为,只会在你想用它执行 任何操作的时候产生 NullPointException
。 java.util.Optional
(首次出现是在函数式编程这 章)为 null
值提供了一个轻量级代理,Optional
对象可以防止你的代码直接抛出NullPointException。
标记接口
有时候使用一个标记接口来表示空值会更方便。标记接口里边什么都没有,你只要把它的名字当做标签来用就可以。
假设存在许多不同类型的 Robot
,我们想让每种 Robot
都创建一个 Null
对象来执行一些特殊的操作——在本例中,即提供 Null
对象所代表 Robot
的确切类型信息。这些信息是通过动态代理捕获的:
无论何时,如果你需要一个空 Robot
对象,只需要调用 newNullRobot()
,并传递需要代理的 Robot
的类型。这个代理满足了 Robot
和 Null
接口的需要,并提供了它所代理的类型的确切名字。
Mock 对象和桩
Mock 对象和 桩(Stub)在逻辑上都是 Optional
的变体。他们都是最终程序中所使用的“实际”对象的代理。不过,Mock 对象和桩都是假扮成那些可以传递实际信息的实际对象,而不是像 Optional
那样把包含潜在 null
值的对象隐藏
Mock 对象和桩之间的的差别在于程度不同。Mock 对象往往是轻量级的,且用于自测试。通常,为了处理各种不同的测试场景,我们会创建出很多 Mock 对象。而桩只是返回桩数据,它通常是重量级的,并且经常在多个测试中被复用。桩可以根据它们被调用的方式,通过配置进行修改。因此,桩是一种复杂对象,它可以做很多事情。至于 Mock 对象,如果你要做很多事,通常会创建大量又小又简单的 Mock 对象。
接口和类型
interface
关键字的一个重要目标就是允许程序员隔离组件,进而降低耦合度。使用接口可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并不是对解耦的一种无懈可击的保障
通过使用反射,仍然可以调用所有方法,甚至是 private
方法!如果知道方法名,你就可以在其 Method
对象上调用 setAccessible(true)
,就像在 callHiddenMethod()
中看到的那样。
任何方式都没法阻止反射调用那些非公共访问权限的方法。对于字段来说也是这样,即便是 private
字段:
程序员往往对编程语言提供的访问控制过于自信,甚至认为 Java 在安全性上比其它提供了(明显)更宽松的访问控制的语言要优越。然而,正如你所看到的,事实并不是这样。
泛型
普通的类和方法只能使用特定的类型:基本数据类型或类类型。如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大。
多态是一种面向对象思想的泛化机制。你可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类
泛型实现了参数化类型,这样你编写的组件(通常是集合)可以适用于多种类型。“泛型”这个术语的含义是“适用于很多类型”。编程语言中泛型出现的初衷是通过解耦类或方法与所使用的类型之间的约束,使得类或方法具备最宽泛的表达力。随后你会发现 Java 中泛型的实现并没有那么“泛”,你可能会质疑“泛型”这个词是否合适用来描述这一功能。
与 C++ 的比较
Java 中的泛型需要与 C++ 进行对比
只有知道了某个技术不能做什么,你才能更好地做到所能做的
简单泛型
一个集合中存储多种不同类型的对象的情况很少见,通常而言,我们只会用集合存储同一种类型的对象。泛型的主要目的之一就是用来约定集合要存储什么类型的对象,并且通过编译器确保规约得以满足
与其使用 Object
,我们更希望先指定一个类型占位符,稍后再决定具体使用什么类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类时,再用实际的类型替换此类型参数
Java 泛型的核心概念:你只需告诉编译器要使用什么类型,剩下的细节交给它来处理。
一个元组类库
有时一个方法需要能返回多个对象。而 return 语句只能返回单个对象,解决方法就是创建一个对象,用它打包想要返回的多个对象
这个概念称为元组*,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 *数据传输对象 或 信使 )。
一个堆栈类
我们可以看出,泛型只不过是一种类型罢了(稍后我们会看到一些例外的情况)。
内部类 Node
也是一个泛型,它拥有自己的类型参数
RandomList
作为容器的另一个例子,假设我们需要一个持有特定类型对象的列表,每次调用它的 select()
方法时都随机返回一个元素。如果希望这种列表可以适用于各种类型,就需要使用泛型
泛型接口
泛型也可以应用于接口。例如 生成器*,这是一种专门负责创建对象的类。实际上,这是 *工厂方法 设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。生成器无需额外的信息就知道如何创建新对象
泛型方法
到目前为止,我们已经研究了参数化整个类。其实还可以参数化类中的方法。类本身可能是泛型的,也可能不是,不过这与它的方法是否是泛型的并没有什么关系
泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂
要定义泛型方法,请将泛型参数列表放置在返回值之前
变长参数和泛型方法
泛型方法和变长参数列表可以很好地共存
一个泛型的 Supplier
这是一个为任意具有无参构造方法的类生成 Supplier 的类。为了减少键入,它还包括一个用于生成 BasicSupplier 的泛型方法
简化元组的使用
使用类型参数推断和静态导入,我们将把早期的元组重写为更通用的库
一个 Set 工具
对于泛型方法的另一个示例,请考虑由 Set 表示的数学关系。这些被方便地定义为可用于所有不同类型的泛型方法:
构建复杂模型
泛型的一个重要好处是能够简单安全地创建复杂模型
泛型擦除
Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,List<String>
和 List<Integer>
在运行时实际上是相同的类型。它们都被擦除成原生类型 List
理解擦除并知道如何处理它,是你在学习 Java 泛型时面临的最大障碍之一
为了调用 f()
,我们必须协助泛型类,给定泛型类一个边界,以此告诉编译器只能接受遵循这个边界的类型。这里重用了 extends 关键字。由于有了边界,下面的代码就能通过编译:
泛型只有在类型参数比某个具体类型(以及其子类)更加“泛化”——代码能跨多个类工作时才有用。因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换更加复杂。但是,不能因此认为使用 <T extends HasF>
形式就是有缺陷的。例如,如果某个类有一个返回 T 的方法,那么泛型就有所帮助,因为它们之后将返回确切的类型
你必须查看所有的代码,从而确定代码是否复杂到必须使用泛型的程度。
迁移兼容性
为了减少潜在的关于擦除的困惑,你必须清楚地认识到这不是一个语言特性。它是 Java 实现泛型的一种妥协,因为泛型不是 Java 语言出现时就有的,所以就有了这种妥协。它会使你痛苦,因此你需要尽早习惯它并了解为什么它会这样
擦除减少了泛型的泛化性。泛型在 Java 中仍然是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。
擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为“迁移兼容性”。
因此 Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法,继续保持之前的含义——而且还必须支持迁移兼容性,使得类库能按照它们自己的步调变为泛型,当某个类库变为泛型时,不会破坏依赖于它的代码和应用。
擦除使得这种向泛型的迁移成为可能,允许非泛型的代码和泛型代码共存。
类库毫无争议是编程语言的一部分,对生产效率有着极大的影响
擦除的问题
因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下将泛型融入到语言中。擦除允许你继续使用现有的非泛型客户端代码,直至客户端准备好用泛型重写这些代码。这是一个崇高的动机,因为它不会骤然破坏所有现有的代码。
擦除和迁移兼容性意味着,使用泛型并不是强制的,
边界处的动作
因为擦除,我发现了泛型最令人困惑的方面是可以表示没有任何意义的事物
对于在泛型中创建数组,使用 Array.newInstance()
是推荐的方式
即使编译器无法得知 add()
中的 T 的任何信息,但它仍可以在编译期确保你放入 FilledList 中的对象是 T 类型。因此,即使擦除移除了方法或类中的实际类型的信息,编译器仍可以确保方法或类中使用的类型的内部一致性。
泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型
补偿擦除
因为擦除,我们将失去执行泛型代码中某些操作的能力。无法在运行时知道确切类型
有时,我们可以对这些问题进行编程,但是有时必须通过引入类型标签来补偿擦除。这意味着为所需的类型显式传递一个 Class 对象,以在类型表达式中使用它
创建类型的实例
试图在 Erased.java 中 new T()
是行不通的,部分原因是由于擦除,部分原因是编译器无法验证 T 是否具有默认(无参)构造函数。但是在 C++ 中,此操作自然,直接且安全(在编译时检查)
泛型数组
正如在 Erased.java 中所看到的,我们无法创建泛型数组。通用解决方案是在试图创建泛型数组的时候使用 ArrayList
成功创建泛型类型的数组的唯一方法是创建一个已擦除类型的新数组,并将其强制转换
由于擦除,数组的运行时类型只能是 Object[]
。 如果我们立即将其转换为 T[]
,则在编译时会丢失数组的实际类型,并且编译器可能会错过一些潜在的错误检查。因此,最好在集合中使用 Object[]
,并在使用数组元素时向 T 添加强制类型转换
边界
边界(bounds)在本章的前面进行了简要介绍。边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中调用方法
通配符
真正的问题是我们在讨论的集合类型,而不是集合持有对象的类型。与数组不同,泛型没有内建的协变类型。这是因为数组是完全在语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及应该采用什么规则。
但是,有时你想在两个类型间建立某种向上转型关系。通配符可以产生这种关系。
编译器有多聪明
如果创建了一个 Holder<Apple>
,就不能将其向上转型为 Holder<Fruit>
,但是可以向上转型为 Holder<? extends Fruit>
逆变?
还可以走另外一条路,即使用超类型通配符。这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定 <?super MyClass>
,或者甚至使用类型参数: <?super T>
(尽管你不能对泛型参数给出一个超类型边界;即不能声明 <T super MyClass>
)。这使得你可以安全地传递一个类型对象到泛型类型中。因此,有了超类型通配符,就可以向 Collection 写入了
无界通配符
无界通配符 <?>
看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。事实上,编译器初看起来是支持这种判断的
List 实际上表示“持有任何 Object 类型的原生 List ”,而 List<?>
表示“具有某种特定类型的非原生 List ,只是我们不知道类型是什么。
因此,使用确切类型来替代通配符类型的好处是,可以用泛型参数来做更多的事,但是使用通配符使得你必须接受范围更宽的参数化类型作为参数。因此,必须逐个情况地权衡利弊,找到更适合你的需求的方法。
捕获转换
有一种特殊情况需要使用 <?>
而不是原生类型。如果向一个使用 <?>
的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。
捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。注意,不能从 f2()
中返回 T,因为 T 对于 f2()
来说是未知的。捕获转换十分有趣,但是非常受限。
问题
任何基本类型都不能作为类型参数。
解决方法是使用基本类型的包装器类以及自动装箱机制。如果创建一个 ArrayList<Integer>
,并将基本类型 int 应用于这个集合,那么你将发现自动装箱机制将自动地实现 int 到 Integer 的双向转换——因此,这几乎就像是有一个 ArrayList<int>
一样
自动装箱机制解决了一些问题,但并没有解决所有问题。
自动装箱不适用于数组,因此我们必须创建 FillArray.fill()
的重载版本,或创建产生 Wrapped 输出的生成器。 FillArray 仅比 java.util.Arrays.setAll()
有用一点,因为它返回填充的数组
实现参数化接口
个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口
转型和警告
使用带有泛型类型参数的转型或 instanceof 不会有任何效果
通过泛型类来转型?
重载
当擦除后的参数不能产生唯一的参数列表时,你必须提供不同的方法名
基类劫持接口
一旦 Comparable 的类型参数设置为 ComparablePet,其他的实现类只能比较 ComparablePet:
自限定的类型
这就像两面镜子彼此照向对方所引起的目眩效果一样,是一种无限反射。SelfBounded 类接受泛型参数 T,而 T 由一个边界类限定,这个边界就是拥有 T 作为其参数的 SelfBounded
古怪的循环泛型
这可以按照 Jim Coplien 在 C++ 中的古怪的循环模版模式的命名方式,称为古怪的循环泛型(CRG)。“古怪的循环”是指类相当古怪地出现在它自己的基类中这一事实。 为了理解其含义,努力大声说:“我在创建一个新类,它继承自一个泛型类型,这个泛型类型接受我的类的名字作为其参数。”当给出导出类的名字时,这个泛型基类能够实现什么呢?好吧,Java 中的泛型关乎参数和返回类型,因此它能够产生使用导出类作为其参数和返回类型的基类。它还能将导出类型用作其域类型,尽管这些将被擦除为 Object 的类型
注意,这里有些东西很重要:新类 Subtype 接受的参数和返回的值具有 Subtype 类型而不仅仅是基类 BasicHolder 类型。这就是 CRG 的本质:基类用导出类替代其参数。这意味着泛型基类变成了一种其所有导出类的公共功能的模版,但是这些功能对于其所有参数和返回值,将使用导出类型。也就是说,在所产生的类中将使用确切类型而不是基类型。因此,在Subtype 中,传递给 set()
的参数和从 get()
返回的类型都是确切的 Subtype。
自限定 ?
自限定的参数有何意义呢?它可以保证类型参数必须与正在被定义的类相同。正如你在 B 类的定义中所看到的,还可以从使用了另一个 SelfBounded 参数的 SelfBounded 中导出,尽管在 A 类看到的用法看起来是主要的用法。对定义 E 的尝试说明不能使用不是 SelfBounded 的类型参数。 遗憾的是, F 可以编译,不会有任何警告,因此自限定惯用法不是可强制执行的。如果它确实很重要,可以要求一个外部工具来确保不会使用原生类型来替代参数化类型。 注意,可以移除自限定这个限制,这样所有的类仍旧是可以编译的,但是 E 也会因此而变得可编译
因此很明显,自限定限制只能强制作用于继承关系
参数协变 ?
自限定类型的价值在于它们可以产生协变参数类型——方法参数类型会随子类而变化。
自限定泛型事实上将产生确切的导出类型作为其返回值,就像在 get()
中所看到的一样
set(derived)
和 set(base)
都是合法的,因此 DerivedSetter.set()
没有覆盖 OrdinarySetter.set()
,而是重载了这个方法。从输出中可以看到,在 DerivedSetter 中有两个方法,因此基类版本仍旧是可用的,因此可以证明它被重载过。 但是,在使用自限定类型时,在导出类中只有一个方法,并且这个方法接受导出类型而不是基类型为参数:
动态类型安全
因为可以向 Java 5 之前的代码传递泛型集合,所以旧式代码仍旧有可能会破坏你的集合。Java 5 的 java.util.Collections 中有一组便利工具,可以解决在这种情况下的类型检查问题,它们是:静态方法 checkedCollection()
、checkedList()
、 checkedMap()
、 checkedSet()
、checkedSortedMap()
和 checkedSortedSet()
。这些方法每一个都会将你希望动态检查的集合当作第一个参数接受,并将你希望强制要求的类型作为第二个参数接受。
泛型异常
由于擦除的原因,catch 语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。泛型类也不能直接或间接继承自 Throwable(这将进一步阻止你去定义不能捕获的泛型异常)。但是,类型参数可能会在一个方法的 throws 子句中用到。这使得你可以编写随检查型异常类型变化的泛型代码
混型?
术语混型随时间的推移好像拥有了无数的含义,但是其最基本的概念是混合多个类的能力,以产生一个可以表示混型中所有类型的类。这往往是你最后的手段,它将使组装多个类变得简单易行。 混型的价值之一是它们可以将特性和行为一致地应用于多个类之上。如果想在混型类中修改某些东西,作为一种意外的好处,这些修改将会应用于混型所应用的所有类型之上。正由于此,混型有一点面向切面编程 (AOP) 的味道,而切面经常被建议用来解决混型问题
C++ 中的混型?
泛型类不能直接继承自一个泛型参数
与接口混合?
一种更常见的推荐解决方案是使用接口来产生混型效果
使用装饰器模式?
装饰器是通过使用组合和形式化结构(可装饰物/装饰器层次结构)来实现的,而混型是基于继承的。因此可以将基于参数化类型的混型当作是一种泛型装饰器机制,这种机制不需要装饰器设计模式的继承结构。
也就是说,尽管可以添加多个层,但是最后一层才是实际的类型,因此只有最后一层的方法是可视的,而混型的类型是所有被混合到一起的类型。因此对于装饰器来说,其明显的缺陷是它只能有效地工作于装饰中的一层(最后一层),而混型方法显然会更自然一些。因此,装饰器只是对由混型提出的问题的一种局限的解决方案
与动态代理混合
可以使用动态代理来创建一种比装饰器更贴近混型模型的机制(查看 类型信息 一章中关于 Java 的动态代理如何工作的解释)。通过使用动态代理,所产生的类的动态类型将会是已经混入的组合类型。 由于动态代理的限制,每个被混入的类都必须是某个接口的实现
因为只有动态类型而不是静态类型才包含所有的混入类型,因此这仍旧不如 C++ 的方式好,因为可以在具有这些类型的对象上调用方法之前,你被强制要求必须先将这些对象向下转型到恰当的类型。
潜在类型机制
在本章的开头介绍过这样的思想,即要编写能够尽可能广泛地应用的代码。为了实现这一点,我们需要各种途径来放松对我们的代码将要作用的类型所作的限制,同时不丢失静态类型检查的好处。然后,我们就可以编写出无需修改就可以应用于更多情况的代码,即更加“泛化”的代码。
泛型代码典型地只能在泛型类型上调用少量方法,而具有潜在类型机制的语言只要求实现某个方法子集,而不是某个特定类或接口,从而放松了这种限制(并且可以产生更加泛化的代码)。正由于此,潜在类型机制使得你可以横跨类继承结构,调用不属于某个公共接口的方法。因此,实际上一段代码可以声明:“我不关心你是什么类型,只要你可以 speak()
和 sit()
即可。”由于不要求具体类型,因此代码就可以更加泛化。
潜在类型机制是一种代码组织和复用机制。有了它,编写出的代码相对于没有它编写出的代码,能够更容易地复用。代码组织和复用是所有计算机编程的基本手段:编写一次,多次使用,并在一个位置保存代码。因为我并未被要求去命名我的代码要操作于其上的确切接口,所以,有了潜在类型机制,我就可以编写更少的代码,并更容易地将其应用于多个地方
支持潜在类型机制的语言包括 Python(可以从 www.Python.org 免费下载)、C++、Ruby、SmallTalk 和 Go。Python 是动态类型语言(几乎所有的类型检查都发生在运行时),而 C++ 和 Go 是静态类型语言(类型检查发生在编译期),因此潜在类型机制不要求静态或动态类型检查
pyhton 中的潜在类型
perform()
不关心其参数的类型,因此我可以向它传递任何对象,只要该对象支持 speak()
和 sit()
方法。如果传递给 perform()
的对象不支持这些操作,那么将会得到运行时异常。
C++ 中的潜在类型
在 Python 和 C++ 中,Dog 和 Robot 没有任何共同的东西,只是碰巧有两个方法具有相同的签名。从类型的观点看,它们是完全不同的类型。但是,perform()
不关心其参数的具体类型,并且潜在类型机制允许它接受这两种类型的对象。 C++ 确保了它实际上可以发送的那些消息,如果试图传递错误类型,编译器就会给你一个错误消息(这些错误消息从历史上看是相当可怕和冗长的,是 C++ 的模版名声欠佳的主要原因)。尽管它们是在不同时期实现这一点的,C++ 在编译期,而 Python 在运行时,但是这两种语言都可以确保类型不会被误用,因此被认为是强类型的。潜在类型机制没有损害强类型机制。
Go 中的潜在类型
main()
证明 perform()
确实对其参数的确切类型不在乎,只要可以在该参数上调用 talk()
和 sit()
即可。 但是,就像 C ++ 模板函数一样,在编译时检查类型。
java中的直接潜在类型
因为泛型是在这场竞赛的后期才添加到 Java 中,因此没有任何机会可以去实现任何类型的潜在类型机制,因此 Java 没有对这种特性的支持。所以,初看起来,Java 的泛型机制比支持潜在类型机制的语言更“缺乏泛化性”。(使用擦除来实现 Java 泛型的实现有时称为第二类泛型类型)例如,在 Java 8 之前如果我们试图用 Java 实现上面 dogs-and-robots 的示例,那么就会被强制要求使用一个类或接口,并在边界表达式中指定它
对缺乏潜在类型机制的补偿
尽管 Java 不直接支持潜在类型机制,但是这并不意味着泛型代码不能在不同的类型层次结构之间应用。也就是说,我们仍旧可以创建真正的泛型代码,但是这需要付出一些额外的努力。
反射
这些类完全是彼此分离的,没有任何公共基类(除了 Object )或接口。通过反射, CommunicateReflectively.perform()
能够动态地确定所需要的方法是否可用并调用它们。它甚至能够处理 Mime 只具有一个必需的方法这一事实,并能够部分实现其目标。
将一个方法应用于序列
反射提供了一些有用的可能性,但是它将所有的类型检查都转移到了运行时,因此在许多情况下并不是我们所希望的。如果能够实现编译期类型检查,这通常会更符合要求。但是有可能实现编译期类型检查和潜在类型机制吗?
Java8 中的辅助潜在类型
尽管传递未绑定的方法引用似乎要花很多力气,但潜在类型的最终目标还是可以实现的。 我们创建了一个代码片段 CommunicateA.perform()
,该代码可用于任何具有符合签名的方法引用的类型。 请注意,这与我们看到的其他语言中的潜在类型有所不同,因为这些语言不仅需要签名以符合规范,还需要方法名称。 因此,该技术可以说产生了更多的通用代码。
使用Suppliers类的通用方法
通过辅助潜在类型,我们可以定义本章其他部分中使用的 Suppliers 类。 此类包含使用生成器填充 Collection 的工具方法。 泛化这些操作很有意义:
总结:类型转换真的如此之糟吗?
使用泛型类型机制的最吸引人的地方,就是在使用集合类的地方,这些类包括诸如各种 List 、各种 Set 、各种 Map 等你在 集合 和 附录:集合主题 这两章所见。在 Java 5 之前,当你将一个对象放置到集合中时,这个对象就会被向上转型为 Object ,因此你会丢失类型信息。当你想要将这个对象从集合中取回,用它去执行某些操作时,必须将其向下转型回正确的类型。我用的示例是持有 Cat 的 List (这个示例的一种使用苹果和桔子的变体在 集合 章节的开头展示过)。如果没有 Java 5 泛型版本的集合,你放到容集里和从集合中取回的都是 Object 。因此,我们很可能会将一个 Dog 放置到 Cat 的 List 中
但是,泛型出现之前的 Java 并不会让你误用放入到集合中的对象。如果将一个 Dog 扔到 Cat 的集合中,并且试图将这个集合中的所有东西都当作 Cat 处理,那么当你从这个 Cat 集合中取回那个 Dog 引用,并试图将其转型为 Cat 时,就会得到一个 RuntimeException 。你仍旧可以发现问题,但是是在运行时而非编译期发现它的
泛型正如其名称所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型具有更少的限制,因此单个的代码段可以应用到更多的类型上。
正如你在本章中看到的,编写真正泛化的“持有器”类( Java 的容器就是这种类)相当简单,但是编写出能够操作其泛型类型的泛化代码就需要额外的努力了,这些努力需要类创建者和类消费者共同付出,他们必须理解这些代码的概念和实现。这些额外的努力会增加使用这种特性的难度,并可能会因此而使其在某些场合缺乏可应用性,而在这些场合中,它可能会带来附加的价值。
数组
随着 Java Collection 和 Stream 类中高级功能的不断增加,日常编程中使用数组的需求也在变少,所以你暂且可以放心地略读甚至跳过这一章。但是,即使你自己避免使用数组,也总会有需要阅读别人数组代码的那一天。
数组特性
将数组和其他类型的集合区分开来的原因有三:效率,类型,保存基本数据类型的能力。在 Java 中,使用数组存储和随机访问对象引用序列是非常高效的。数组是简单的线性序列,这使得对元素的访问变得非常快。然而这种高速也是有代价的,代价就是数组对象的大小是固定的,且在该数组的生存期内不能更改。
不管在编译时还是运行时,Java都会阻止你犯向对象发送不正确消息的错误。然而不管怎样,使用数组都不会有更大的风险。比较好的地方在于,如果编译器报错,最终的用户更容易理解抛出异常的含义。
一个数组可以保存基本数据类型,而一个预泛型的集合不可以。然而对于泛型而言,集合可以指定和检查他们保存对象的类型,而通过 自动装箱 (autoboxing)机制,集合表现地就像它们可以保存基本数据类型一样,因为这种转换是自动的。
数组和 ArrayList 之间的相似是设计者有意为之,所以在概念上,两者很容易切换。但是就像你在集合中看到的,集合的功能明显多于数组。随着 Java 自动装箱技术的出现,通过集合使用基本数据类型几乎和通过数组一样简单。数组唯一剩下的优势就是效率。然而,当你解决一个更加普遍的问题时,数组可能限制太多,这种情形下,您可以使用集合类。
用于显示数组的实用程序
一等对象
不管你使用的什么类型的数组,数组中的数据集实际上都是对堆中真正对象的引用。数组是保存指向其他对象的引用的对象,数组可以隐式地创建,作为数组初始化语法的一部分,也可以显式地创建,比如使用一个 new 表达式。数组对象的一部分(事实上,你唯一可以使用的方法)就是只读的 length 成员函数,它能告诉你数组对象中可以存储多少元素。[ ] 语法是你访问数组对象的唯一方式。
返回数组
假设你写了一个方法,这个方法不是返回一个元素,而是返回多个元素。对 C++/C 这样的语言来说这是很困难的,因为你无法返回一个数组,只能是返回一个指向数组的指针。这会带来一些问题,因为对数组生存期的控制变得很混乱,这会导致内存泄露。
多维数组
非基元的对象数组也可以定义为不规则数组
数组初始化时使用自动装箱技术
泛型数组
一般来说,数组和泛型并不能很好的结合。你不能实例化参数化类型的数组,类型擦除需要删除参数类型信息,而且数组必须知道它们所保存的确切类型,以强制保证类型安全。但是,可以参数化数组本身的类型。
如果你知道你不会进行向上类型转换,你的需求相对简单,那么可以创建一个泛型数组,它将提供基本的编译时类型检查。然而,一个泛型 Collection 实际上是一个比泛型数组更好的选择。
一般来说,您会发现泛型在类或方法的边界上是有效的。在内部,擦除常常会使泛型不可使用。
Arrays的fill方法
Arrays的setAll方法
增量生成
随机生成
泛型和基本数组
在本章的前面,我们被提醒,泛型不能和基元一起工作。在这种情况下,我们必须从基元数组转换为包装类型的数组,并且还必须从另一个方向转换。下面是一个转换器可以同时对所有类型的数据执行操作
数组元素修改
数组并行
用简单的方法编写代码。不要开始处理并行性,除非它成为一个问题。您仍然会遇到并行性。在本章中,我们将介绍一些为并行执行而编写的Java库方法。因此,您必须对它有足够的了解,以便进行基本的讨论,并避免出现错误。
parallelSetAll()
流式编程产生优雅的代码
Arrays工具类
您已经看到了 java.util.Arrays 中的 fill() 和 setAll()/parallelSetAll() 。该类包含许多其他有用的 静态 程序方法,我们将对此进行研究
数组拷贝
与使用for循环手工执行复制相比,copyOf() 和 copyOfRange() 复制数组要快得多。这些方法被重载以处理所有类型
数组比较
数组相等的含义:数组必须有相同数量的元素,并且每个元素必须与另一个数组中的对应元素相等,对每个元素使用 equals()(对于原生类型,使用原生类型的包装类的 equals() 方法;例如,int的Integer.equals()。
流和数组
stream() 方法很容易从某些类型的数组中生成元素流。
通常,将数组转换为流来生成所需的结果要比直接操作数组容易得多。请注意,即使流已经“用完”(您不能重复使用它),您仍然拥有该数组,因此您可以以其他方式使用它—-包括生成另一个流。
数组排序
编程设计的一个主要目标是“将易变的元素与稳定的元素分开”,在这里,保持不变的代码是一般的排序算法,但是变化的是对象的比较方式。因此,使用策略设计模式而不是将比较代码放入许多不同的排序源码中。使用策略模式时,变化的代码部分被封装在一个单独的类(策略对象)中。
Java有两种方式提供比较功能。第一种方法是通过实现 java.lang.Comparable 接口的原生方法。这是一个简单的接口,只含有一个方法 compareTo()。该方法接受另一个与参数类型相同的对象作为参数,如果当前对象小于参数,则产生一个负值;如果参数相等,则产生零值;如果当前对象大于参数,则产生一个正值。
Arrays.sort()的使用
Java标准库中使用的排序算法被设计为最适合您正在排序的类型—-原生类型的快速排序和对象的归并排序。
并行排序
如果排序性能是一个问题,那么可以使用 Java 8 parallelSort(),它为所有不可预见的情况(包括数组的排序区域或使用了比较器)提供了重载版本
binarySearch二分查找
如果找到了搜索项,Arrays.binarySearch() 将生成一个大于或等于零的值。否则,它将产生一个负值,表示如果手动维护已排序的数组,则应该插入元素的位置。产生的值是 -(插入点) - 1 。插入点是大于键的第一个元素的索引,如果数组中的所有元素都小于指定的键,则是 a.size() 。
parallelPrefix并行前缀
如前所述,使用流进行初始化非常优雅,但是对于大型数组,这种方法可能会耗尽堆空间。使用 setAll() 执行初始化更节省内存:
因为正确使用 parallelPrefix() 可能相当复杂,所以通常应该只在存在内存或速度问题(或两者都有)时使用。否则,Stream.reduce() 应该是您的首选。
在经历了这么多年的Java发展之后,我们可以很有趣地推测,如果重新开始,设计人员是否会将原生类型和低级数组放在该语言中(同样在JVM上运行的Scala语言不包括这些)。如果不考虑这些,就有可能开发出一种真正纯粹的面向对象语言(尽管有这样的说法,Java并不是一种纯粹的面向对象语言,这正是因为它的底层缺陷)。关于效率的最初争论总是令人信服的,但是随着时间的推移,我们已经看到了从这个想法向更高层次的组件(如集合)的演进。此外,如果集合可以像在某些语言中一样构建到核心语言中,那么编译器就有更好的机会进行优化。
枚举
注解
并发编程
设计模式
总结
类层次结构
除了内存清理之外,所有的清理都不会自动发生
Java 各个版本
JDK源码
JVM
常见问题
参考
[1] 廖雪峰java教程
[2] On java 8