第三章 万物皆对象
基本类型的存储
布尔(boolean)类型的大小没有明确的规定
高精度数值
BigInteger 和 BigDecimal。尽管它们大致可以划归为“包装类型”,但是它们并没有对应的基本类型。
包名都是小写
在 Java 2 的开发过程中,他们发现这会导致问题,所以现在整个包名都是小写的
第四章 运算符
下划线
Java 7 中有一个深思熟虑的补充:我们可以在数字字面量中包含下划线 _,以使结果更清晰。这对于大数值的分组特别有用。代码示例:
double d = 341_435_936.445_667;
System.out.println(d);
下面是合理使用的规则:
- 仅限单 _,不能多条相连。
- 数值开头和结尾不允许出现 _。
- F、D 和 L的前后禁止出现 _。
- 二进制前导 b 和 十六进制 x 前后禁止出现 _。
移位运算符
截断和舍入
从 float 和 double 转换为整数值时,小数位将被截断。若你想对结果进行四舍五入,可以使用 java.lang.Math 的 round()方法:
第五章 控制流
在 Java 中,涉及的关键字包括 if-else,while,do-while,for,return,break 和选择语句 switch。
while 和 do-while 之间的唯一区别是:即使条件表达式返回结果为 false, do-while 语句也至少会执行一次。
通过在循环前边加标号来实现break跳出
public static void main(String[] args){
tag2:
for(int i=0;i<5;i++){
tag1:
for(int k=0;k<5;k++){
System.out.println(k);
if(k==1){
break tag2;
}
}
}
System.out.println(“finished!”);
}
1、标号紧贴循环语句,并且处于循环语句的前边;
2、通过标号能跳出任意层数的循环;
3、break和continue都可以使用标号来灵活控制循环语句;
4、我们反编译.class文件时,能经常见到"break label;"这样的标号使用。
第六章 初始化和清理
可变参数列表
static void printArray(Object… args) {
枚举类型
四要素: 枚举常量, 私有属性, 构造方法, get方法
私有属性和get方法是为了可以访问到枚举中的值
直接调用枚举返回的是枚举值如GET,
调用getKey方法返回的是枚举内部值如1
多个枚举值之间用逗号隔开,最后一个用分号
public enum RequestEnum {
GET(1,“get”),POST(2,“post”),PUT(3,“put”);
private int key;
private String val;
RequestEnum(int key, String val) {
this.key = key;
this.val = val;
}
public int getKey() {
return key;
}
public String getVal() {
return val;
}
}
第七章 封装
第八章 复用
代码复用是面向对象编程(OOP)最具魅力的原因之一。
组合语法
在前面的学习中,“组合”(Composition)已经被多次使用。你仅需要把对象的引用(object references)放置在一个新的类里,
继承语法
委托
第九章 多态
第十章 接口
默认方法
Java 8 为关键字 default 增加了一个新的用途(之前只用于 switch 语句和注解中)。当在接口中使用它时,任何实现接口却没有定义方法的时候可以使用 default 创建的方法体。默认方法比抽象类中的方法受到更多的限制,但是非常有用,我们将在“流式编程”一章中看到。现在让我们看下如何使用:
interface InterfaceWithDefault {
default void newMethod() {
System.out.println(“newMethod”);
}
}
接口实现类可以在不实现此方法时候调用此方法, 且也不会报错
接口中的静态方法
Java 8 允许在接口中添加静态方法。这么做能恰当地把工具功能置于接口中,从而操作接口,或者成为通用的工具:
public interface Operations {
void execute();
static void runOps(Operations… ops) {
for (Operations op: ops) {
op.execute();
}
}
第十一章 内部类
第十二章 集合
第十三章 函数式编程
• [3] Java 8 的 Lambda 表达式。由箭头 -> 分隔开参数和函数体,箭头左边是参数,箭头右侧是从 Lambda 返回的表达式,即函数体。这实现了与定义类、匿名内部类相同的效果,但代码少得多。
• [4] Java 8 的方法引用,由 :: 区分。在 :: 的左边是类或对象的名称,在 :: 的右边是方法的名称,但没有参数列表。
任何 Lambda 表达式的基本语法是:
- 参数。
- 接着 ->,可视为“产出”。
- -> 之后的内容都是方法体。
o [1] 当只用一个参数,可以不需要括号 ()。 然而,这是一个特例。
o [2] 正常情况使用括号 () 包裹参数。 为了保持一致性,也可以使用括号 () 包裹单个参数,虽然这种情况并不常见。
o [3] 如果没有参数,则必须使用括号 () 表示空参数列表。
o [4] 对于多个参数,将参数列表放在括号 () 中。
到目前为止,所有 Lambda 表达式方法体都是单行。 该表达式的结果自动成为 Lambda 表达式的返回值,在此处使用 return 关键字是非法的。 这是 Lambda 表达式缩写用于描述功能的语法的另一种方式。
[5] 如果在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 return。
方法引用
Java 8 方法引用没有历史包袱。方法引用组成:类名或对象名,后面跟 :: [^4],然后跟方法名称。
高阶函数
这个名字可能听起来令人生畏,但是:高阶函数(Higher-order Function)只是一个消费或产生函数的函数
第十四章 流式编程(stream)
Java8之前显式地编写迭代机制称为外部迭代。而在stream 中,流式编程采用内部迭代,这是流式编程的核心特性之一。这种机制使得编写的代码可读性更强,也更能利用多核处理器的优势。通过放弃对迭代过程的控制,我们把控制权交给并行化机制。我们将在并发编程一章中学习这部分内容。
另一个重要方面,流是懒加载的。这代表着它只在绝对必要时才计算。你可以将流看作“延迟列表”。由于计算延迟,流使我们能够表示非常大(甚至无限)的序列,而不需要考虑内存问题。
流支持
Java 设计者面临着这样一个难题:现存的大量类库不仅为 Java 所用,同时也被应用在整个 Java 生态圈数百万行的代码中。如何将一个全新的流的概念融入到现有类库中呢?
比如在 Random 中添加更多的方法。只要不改变原有的方法,现有代码就不会受到干扰。
问题是,接口部分怎么改造呢?特别是涉及集合类接口的部分。如果你想把一个集合转换为流,直接向接口添加新方法会破坏所有老的接口实现类。
Java 8 采用的解决方案是:在接口中添加被 default(默认)修饰的方法。通过这种方案,设计者们可以将流式(stream)方法平滑地嵌入到现有类中。流方法预置的操作几乎已满足了我们平常所有的需求。流操作的类型有三种:创建流,修改流元素(中间操作, Intermediate Operations),消费流元素(终端操作, Terminal Operations)。最后一种类型通常意味着收集流元素(通常是到集合中)。
下面我们来看下每种类型的流操作。
流创建
你可以通过 Stream.of() 很容易地将一组元素转化成为流
除此之外,每个集合都可以通过调用 stream() 方法来产生一个流
随机数流
Random 类被一组生成流的方法增强了
int 类型的范围
IntStream 类提供了 range() 方法用于生成整型序列的流
generate()
参照 RandomWords.java 中 Stream.generate() 搭配 Supplier 使用的例子
iterate()
Stream.iterate() 以种子(第一个参数)开头,并将其传给方法(第二个参数)。方法的结果将添加到流,并存储作为第一个参数用于下次调用 iterate(),依次类推。我们可以利用 iterate() 生成一个斐波那契数列。
流的建造者模式
在建造者设计模式(也称构造器模式)中,首先创建一个 builder 对象,传递给它多个构造器信息,最后执行“构造”。Stream 库提供了这样的 Builder。在这里,我们重新审视文件读取并将其转换成为单词流的过程。
Arrays
Arrays 类中含有一个名为 stream() 的静态方法用于把数组转换成为流。我们可以重写 interfaces/Machine.java 中的主方法用于创建一个流,并将 execute() 应用于每一个元素
正则表达式
Java 的正则表达式将在字符串这一章节详细介绍。Java 8 在 java.util.regex.Pattern 中增加了一个新的方法 splitAsStream()。这个方法可以根据传入的公式将字符序列转化为流。但是有一个限制,输入只能是 CharSequence,因此不能将流作为 splitAsStream() 的参数。
中间操作
中间操作用于从一个流中获取对象,并将对象作为另一个流从后端输出,以连接到其他操作。
跟踪和调试
peek() 操作的目的是帮助调试。它允许你无修改地查看流中的元素
流元素排序
在 Randoms.java 中,我们熟识了 sorted() 的默认比较器实现。其实它还有另一种形式的实现:传入一个 Comparator 参数。代码示例:
// streams/SortedComparator.java
import java.util.*;
public class SortedComparator {
public static void main(String[] args) throws Exception {
FileToWords.stream(“Cheese.dat”)
.skip(10)
.limit(10)
.sorted(Comparator.reverseOrder())
.map(w -> w + " ")
.forEach(System.out::print);
}
}
sorted() 预设了一些默认的比较器。这里我们使用的是反转“自然排序”。当然你也可以把 Lambda 函数作为参数传递给 sorted()。
移除元素
• distinct():在 Randoms.java 类中的 distinct() 可用于消除流中的重复元素。相比创建一个 Set 集合,该方法的工作量要少得多。
• filter(Predicate):过滤操作会保留与传递进去的过滤器函数计算结果为 true 元素。
应用函数到元素
• map(Function):将函数操作应用在输入流的元素中,并将返回值传递到输出流中。
• mapToInt(ToIntFunction):操作同上,但结果是 IntStream。
• mapToLong(ToLongFunction):操作同上,但结果是 LongStream。
• mapToDouble(ToDoubleFunction):操作同上,但结果是 DoubleStream。
.mapToDouble(Double::parseDouble)
在 map() 中组合流
假设我们现在有了一个传入的元素流,并且打算对流元素使用 map() 函数。现在你已经找到了一些可爱并独一无二的函数功能,但是问题来了:这个函数功能是产生一个流。我们想要产生一个元素流,而实际却产生了一个元素流的流。
flatMap() 做了两件事:将产生流的函数应用在每个元素上(与 map() 所做的相同),然后将每个流都扁平化为元素,因而最终产生的仅仅是元素。
flatMap(Function):当 Function 产生流时使用。
flatMapToInt(Function):当 Function 产生 IntStream 时使用。
flatMapToLong(Function):当 Function 产生 LongStream 时使用。
flatMapToDouble(Function):当 Function 产生 DoubleStream 时使用。
map()输出流,flatMap()输出值
Optional类
在我们学习终端操作之前,我们必须考虑如果你在一个空流中获取元素会发生什么。我们喜欢为了“happy path”而将流连接起来,并假设流不会被中断。在流中放置 null 是很好的中断方法。那么是否有某种对象,可作为流元素的持有者,即使查看的元素不存在也能友好地提示我们(也就是说,不会发生异常)?
Optional 可以实现这样的功能。一些标准流操作返回 Optional 对象,因为它们并不能保证预期结果一定存在。包括:
• findFirst() 返回一个包含第一个元素的 Optional 对象,如果流为空则返回 Optional.empty
• findAny() 返回包含任意元素的 Optional 对象,如果流为空则返回 Optional.empty
• max() 和 min() 返回一个包含最大值或者最小值的 Optional 对象,如果流为空则返回 Optional.empty
reduce() 不再以 identity 形式开头,而是将其返回值包装在 Optional 中。(identity 对象成为其他形式的 reduce() 的默认结果,因此不存在空结果的风险)
对于数字流 IntStream、LongStream 和 DoubleStream,average() 会将结果包装在 Optional 以防止流为空。
便利函数
有许多便利函数可以解包 Optional ,这简化了上述“对所包含的对象的检查和执行操作”的过程:
• ifPresent(Consumer):当值存在时调用 Consumer,否则什么也不做。
• orElse(otherObject):如果值存在则直接返回,否则生成 otherObject。
• orElseGet(Supplier):如果值存在则直接返回,否则使用 Supplier 函数生成一个可替代对象。
• orElseThrow(Supplier):如果值存在直接返回,否则使用 Supplier 函数生成一个异常。
创建 Optional
当我们在自己的代码中加入 Optional 时,可以使用下面 3 个静态方法:
• empty():生成一个空 Optional。
• of(value):将一个非空值包装到 Optional 里。
• ofNullable(value):针对一个可能为空的值,为空时自动生成 Optional.empty,否则将值包装在 Optional 中。
Optional 对象操作
当我们的流管道生成了 Optional 对象,下面 3 个方法可使得 Optional 的后续能做更多的操作:
• filter(Predicate):将 Predicate 应用于 Optional 中的内容并返回结果。当 Optional 不满足 Predicate 时返回空。如果 Optional 为空,则直接返回。
• map(Function):如果 Optional 不为空,应用 Function 于 Optional 中的内容,并返回结果。否则直接返回 Optional.empty。
• flatMap(Function):同 map(),但是提供的映射函数将结果包装在 Optional 对象中,因此 flatMap() 不会在最后进行任何包装。
以上方法都不适用于数值型 Optional。一般来说,流的 filter() 会在 Predicate 返回 false 时移除流元素。而 Optional.filter() 在失败时不会删除 Optional,而是将其保留下来,并转化为空。
Optional 流
假设你的生成器可能产生 null 值,那么当用它来创建流时,你会自然地想到用 Optional 来包装元素。
终端操作
以下操作将会获取流的最终结果。至此我们无法再继续往后传递流。可以说,终端操作总是我们在流管道中所做的最后一件事。
数组
• toArray():将流转换成适当类型的数组。
• toArray(generator):在特殊情况下,生成自定义类型的数组。
循环
• forEach(Consumer)常见如 System.out::println 作为 Consumer 函数。
• forEachOrdered(Consumer): 保证 forEach 按照原始流顺序操作。
第一种形式:无序操作,仅在引入并行流时才有意义。在 并发编程 章节之前我们不会深入研究这个问题。这里简单介绍下 parallel():可实现多处理器并行操作。实现原理为将流分割为多个(通常数目为 CPU 核心数)并在不同处理器上分别执行操作。因为我们采用的是内部迭代,而不是外部迭代,所以这是可能实现的。
parallel() 看似简单,实则棘手。更多内容将在稍后的 并发编程 章节中学习。
下例引入 parallel() 来帮助理解 forEachOrdered(Consumer) 的作用和使用场景。
集合
• collect(Collector):使用 Collector 收集流元素到结果集合中。
• collect(Supplier, BiConsumer, BiConsumer):同上,第一个参数 Supplier 创建了一个新结果集合,第二个参数 BiConsumer 将下一个元素包含到结果中,第三个参数 BiConsumer 用于将两个值组合起来。
在这里我们只是简单介绍了几个 Collectors 的运用示例。实际上,它还有一些非常复杂的操作实现,可通过查看 java.util.stream.Collectors的 API 文档了解。例如,我们可以将元素收集到任意一种特定的集合中。
假设我们现在为了保证元素有序,将元素存储在 TreeSet 中。Collectors 里面没有特定的 toTreeSet(),但是我们可以通过将集合的构造函数引用传递给 Collectors.toCollection(),从而构建任何类型的集合。
组合
• reduce(BinaryOperator):使用 BinaryOperator 来组合所有流中的元素。因为流可能为空,其返回值为 Optional。
• reduce(identity, BinaryOperator):功能同上,但是使用 identity 作为其组合的初始值。因此如果流为空,identity 就是结果。
• reduce(identity, BiFunction, BinaryOperator):更复杂的使用形式(暂不介绍),这里把它包含在内,因为它可以提高效率。通常,我们可以显式地组合 map() 和 reduce() 来更简单的表达它。
匹配
• allMatch(Predicate) :如果流的每个元素根据提供的 Predicate 都返回 true 时,结果返回为 true。在第一个 false 时,则停止执行计算。
• anyMatch(Predicate):如果流中的任意一个元素根据提供的 Predicate 返回 true 时,结果返回为 true。在第一个 false 是停止执行计算。
• noneMatch(Predicate):如果流的每个元素根据提供的 Predicate 都返回 false 时,结果返回为 true。在第一个 true 时停止执行计算。
查找
• findFirst():返回第一个流元素的 Optional,如果流为空返回 Optional.empty。
• findAny(:返回含有任意流元素的 Optional,如果流为空返回 Optional.empty。
信息
• count():流中的元素个数。
• max(Comparator):根据所传入的 Comparator 所决定的“最大”元素。
• min(Comparator):根据所传入的 Comparator 所决定的“最小”元素。
min() 和 max() 的返回类型为 Optional,这需要我们使用 orElse()来解包
数字流信息
• average() :求取流元素平均值。
• max() 和 min():数值流操作无需 Comparator。
• sum():对所有流元素进行求和。
• summaryStatistics():生成可能有用的数据。目前并不太清楚这个方法存在的必要性,因为我们其实可以用更直接的方法获得需要的数据。
第十五章 异常
第十六章 代码校验
JUnit
第十七章 文件
第十八章 字符串
第十九章 类型信息
RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现和使用类型信息
本章将讨论 Java 是如何在运行时识别对象和类信息的。主要有两种方式:
- “传统的” RTTI:假定我们在编译时已经知道了所有的类型;
- “反射”机制:允许我们在运行时发现和使用类的信息。
向上转型的时候也丢失了这些对象的具体类型
所有类型转换的正确性检查都是在运行时进行的
Class 对象
其实构造器也是类的静态方法,虽然构造器前面并没有 static 关键字。所以,使用 new 操作符创建类的新对象,这个操作也算作对类的静态成员引用
Class.forName(“Gum”);
forName() 是 Class 类的一个静态方法,我们可以使用 forName() 根据目标类的类名(String)得到该类的 Class 对象
如果 Class.forName() 找不到要加载的类,它就会抛出异常 ClassNotFoundException
无论何时,只要你想在运行时使用类型信息,就必须先得到那个 Class 对象的引用
Class.forName() 就是实现这个功能的一个便捷途径,因为使用该方法你不需要先持有这个类型 的对象。但是,如果你已经拥有了目标类的对象,那就可以通过调用 getClass() 方法来获取 Class 引用了,这个方法来自根类 Object,它将返回表示该对象实际类型的 Class 对象的引用。Class 包含很多有用的方法
传递给 forName() 的字符串必须使用类的全限定名(包含包名)。
getName() 来产生完整类名
使用 getSimpleName() 产生不带包名的类名
getCanonicalName() 也是产生完整类名(除内部类和数组外,对大部分类产生的结果与 getName() 相同)。
isInterface() 用于判断某个 Class 对象代表的是否为一个接口
Class.getInterfaces() 方法返回的是存放 Class 对象的数组,它能够获得当前对象所实现的接口。
getSuperclass() 方法来得到父类的 Class 对象,再用父类的 Class 对象调用该方法,重复多次,你就可以得到一个对象完整的类继承结构。
Class 对象的 newInstance() 方法是实现“虚拟构造器”的一种途径,
使用 newInstance() 来创建的类,必须带有无参数的构造器。
类字面常量
Java 还提供了另一种方法来生成类对象的引用:类字面常量。对上述程序来说,就像这样:FancyToy.class;。
类字面常量不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。另外,对于基本数据类型的包装类,还有一个标准字段 TYPE。TYPE 字段是一个引用,指向对应的基本数据类型的 Class 对象,
short.class 等价于 Short.TYPE
我的建议是使用 .class 的形式,以保持与普通类的一致性。
注意,有一点很有趣:当使用 .class 来创建对 Class 对象的引用时,不会自动地初始化该 Class 对象。为了使用类而做的准备工作实际包含三个步骤:
- 加载,这是由类加载器执行的。该步骤将查找字节码(通常在 classpath 所指定的路径中查找,但这并非是必须的),并从这些字节码中创建一个 Class 对象。
- 链接。在链接阶段将验证类中的字节码,为 static 字段分配存储空间,并且如果需要的话,将解析这个类创建的对其他类的所有引用。
- 初始化。如果该类具有超类,则先初始化超类,执行 static 初始化器和 static 初始化块。
直到第一次引用一个 static 方法(构造器隐式地是 static)或者非常量的 static 字段,才会进行类初始化。
泛化的 Class 引用
Class genericIntClass = int.class;
通过使用泛型语法,我们可以让编译器强制执行额外的类型检查。
为了在使用 Class 引用时放松限制,我们使用了通配符,它是 Java 泛型中的一部分。通配符就是 ?,表示“任何事物”。
Class<?> intClass = int.class;
为了创建一个限定指向某种类型或其子类的 Class 引用,我们需要将通配符与 extends 关键字配合使用,创建一个范围限定。这与仅仅声明 Class 不同,现在做如下声明:
Class<? extends Number> bounded = int.class;
当你将泛型语法用于 Class 对象时,newInstance() 将返回该对象的确切类型
cast() 方法
Java 中还有用于 Class 引用的转型语法,即 cast() 方法:
public class ClassCasts {
public static void main(String[] args) {
Building b = new House();
Class houseType = House.class;
House h = houseType.cast(b);
h = (House)b; // … 或者这样做.
}
}
Java 类库中另一个没有任何用处的特性就是 Class.asSubclass(),该方法允许你将一个 Class 对象转型为更加具体的类型。
类型转换检测
直到现在,我们已知的 RTTI 类型包括:
- 传统的类型转换,如 “(Shape)”,由 RTTI 确保转换的正确性,如果执行了一个错误的类型转换,就会抛出一个 ClassCastException 异常。
- 代表对象类型的 Class 对象. 通过查询 Class 对象可以获取运行时所需的信息.
关键字 instanceof。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例
if(x instanceof Dog)
((Dog)x).bark();
一个动态 instanceof 函数
Class.isInstance() 方法提供了一种动态测试对象类型的方法。
isAssignableFrom()方法与instanceof关键字的区别总结为以下两个点:
isAssignableFrom()方法是从类继承的角度去判断,instanceof关键字是从实例继承的角度去判断。
isAssignableFrom()方法是判断是否为某个类的父类,instanceof关键字是判断是否某个类的子类。
使用方法:
父类.class.isAssignableFrom(子类.class)
子类实例 instanceof 父类类型
类的等价比较
当你查询类型信息时,需要注意:instanceof 的形式(即 instanceof 或 isInstance() ,这两者产生的结果相同) 和 与 Class 对象直接比较 这两者间存在重要区别。
instanceof 说的是“你是这个类,还是从这个类派生的类?”。而如果使用 == 比较实际的Class 对象,则与继承无关 —— 它要么是确切的类型,要么不是。
反射:运行时类信息
类 Class 支持反射的概念, java.lang.reflect 库中包含类 Field、Method 和 Constructor(每一个都实现了 Member 接口)。这些类型的对象由 JVM 在运行时创建,以表示未知类中的对应成员。然后,可以使用 Constructor 创建新对象,get() 和 set() 方法读取和修改与 Field 对象关联的字段,invoke() 方法调用与 Method 对象关联的方法。此外,还可以调用便利方法 getFields()、getMethods()、getConstructors() 等,以返回表示字段、方法和构造函数的对象数组。(你可以通过在 JDK 文档中查找类 Class 来了解更多信息。)因此,匿名对象的类信息可以在运行时完全确定,编译时不需要知道任何信息。
Class 方法 getmethods() 和 getconstructors() 分别返回 Method 数组和 Constructor 数组。这些类中的每一个都有进一步的方法来解析它们所表示的方法的名称、参数和返回值。但你也可以像这里所做的那样,使用 toString(),生成带有整个方法签名的 String。
动态代理
Java 的动态代理更进一步,不仅动态创建代理对象而且动态处理对代理方法的调用。在动态代理上进行的所有调用都被重定向到单个调用处理程序,该处理程序负责发现调用的内容并决定如何处理
Interface proxy = (Interface) Proxy.newProxyInstance(
Interface.class.getClassLoader(),
new Class[]{Interface.class},
new DynamicProxyHandler(real));
可以通过调用静态方法 Proxy.newProxyInstance() 来创建动态代理,该方法需要一个类加载器(通常可以从已加载的对象中获取),希望代理实现的接口列表(不是类或抽象类),以及接口 InvocationHandler 的一个实现。动态代理会将所有调用重定向到调用处理程序,因此通常为调用处理程序的构造函数提供对“真实”对象的引用,以便一旦执行中介任务便可以转发请求
其 Method 对象上调用 setAccessible(true),
第二十章 泛型
第二十一章 数组
第二十二章 枚举
第二十三章 注解
注解的语法十分简单,主要是在现有语法中添加 @ 符号。Java 5 引入了前三种定义在 java.lang 包中的注解:
• @Override:表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,编译器就会发出错误提示。
• @Deprecated:如果使用该注解的元素被调用,编译器就会发出警告信息。
• @SuppressWarnings:关闭不当的编译器警告信息。
• @SafeVarargs:在 Java 7 中加入用于禁止对具有泛型varargs参数的方法或构造函数的调用方发出警告。
• @FunctionalInterface:Java 8 中加入用于表示类型声明为函数式接口
还有 5 种额外的注解类型用于创造新的注解。你将会在这一章学习它们。
基本语法
定义注解
如下是一个注解的定义。注解的定义看起来很像接口的定义
package onjava.atunit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {}
除了 @ 符号之外, @Test 的定义看起来更像一个空接口。注解的定义也需要一些元注解(meta-annoation),比如 @Target 和 @Retention。@Target 定义你的注解可以应用在哪里(例如是方法还是字段)。@Retention 定义了注解在哪里可用,在源代码中(SOURCE),class文件(CLASS)中或者是在运行时(RUNTIME)。
注解通常会包含一些表示特定值的元素。当分析处理注解的时候,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,但是可以为其指定默认值。
不包含任何元素的注解称为标记注解(marker annotation),例如上例中的 @Test 就是标记注解。
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
int id();
String description() default “no description”;
}
description 元素拥有一个 default 值,如果在注解某个方法时没有给出 description 的值。则该注解的处理器会使用此元素的默认值
使用
@UseCase(id = 47, description = "Passwords ")
元注解
Java 语言中目前有 5 种标准注解(前面介绍过),以及 5 种元注解。元注解用于注解其他的注解
注解 解释
@Target 表示注解可以用于哪些地方。可能的 ElementType 参数包括:
CONSTRUCTOR:构造器的声明
FIELD:字段声明(包括 enum 实例)
LOCAL_VARIABLE:局部变量声明
METHOD:方法声明
PACKAGE:包声明
PARAMETER:参数声明
TYPE:类、接口(包括注解类型)或者 enum 声明
@Retention 表示注解信息保存的时长。可选的 RetentionPolicy 参数包括:
SOURCE:注解将被编译器丢弃
CLASS:注解在 class 文件中可用,但是会被 VM 丢弃。
RUNTIME:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。
@Documented 将此注解保存在 Javadoc 中
@Inherited 允许子类继承父类的注解
@Repeatable 允许一个注解可以被使用一次或者多次(Java 8)。
编写注解处理器
如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解中一个很重要的部分就是,创建与使用注解处理器。Java 拓展了反射机制的 API 用于帮助你创造这类工具。
Method m : cl.getDeclaredMethods()) {
UseCase uc = m.getAnnotation(UseCase.class);
getAnnotation() 方法返回指定类型的注解对象
注解元素
在 UseCase.java 中定义的 @UseCase 的标签包含 int 元素 id 和 String 元素 description。注解元素可用的类型如下所示:
• 所有基本类型(int、float、boolean等)
• String
• Class
• enum
• Annotation
• 以上类型的数组
如果你使用了其他类型,编译器就会报错。注意,也不允许使用任何包装类型,但是由于自动装箱的存在,这不算是什么限制。注解也可以作为元素的类型。稍后你会看到,注解嵌套是一个非常有用的技巧。
默认值限制
编译器对于元素的默认值有些过于挑剔。首先,元素不能有不确定的值。也就是说,元素要么有默认值,要么就在使用注解时提供元素的值。
这里有另外一个限制:任何非基本类型的元素, 无论是在源代码声明时还是在注解接口中定义默认值时,都不能使用 null 作为其值。这个限制使得处理器很难表现一个元素的存在或者缺失的状态,因为在每个注解的声明中,所有的元素都存在,并且具有相应的值。为了绕开这个约束,可以自定义一些特殊的值,比如空字符串或者负数用于表达某个元素不存在。
// annotations/SimulatingNull.java
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimulatingNull {
int id() default -1;
String description() default “”;
}复制ErrorOK!
这是一个在定义注解的习惯用法。
注解不支持继承
你不能使用 extends 关键字来继承 @interfaces。
使用javac处理注解
使用
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IsTestAop {
String tesId() default "";
}
处理器
类上注解
@Aspect
@Component
/**
* 想要获取有返回某个id的时候要修改某些操作
* 现在要知道这个id在返回值中叫什么,
* 所以采用注解中的元素获取值,
* 使用注解标注这个方法要引起联动反应
* @param joinPoint
* @param result
*/
@AfterReturning(value = “@annotation(com.kgg.test.aop.IsTestAop)”,returning = “result”)
public void afterTest(JoinPoint joinPoint,Object result){
//获取方法签名
MethodSignature signature = (MethodSignature) joinPoint. getSignature() ;
//获取切入方法的对象
Method mehtod = signature. getMethod() ;
//获取方法上的AOP注解
IsTestAop annotation = mehtod.getAnnotation(IsTestAop.class);
String tesId = annotation.tesId();
// 使用JsonPath读取返回值中的
// JsonPath. parse(JSONUtils. toJSON(result)). read (tesId). toString()
System.out.println(tesId);
}
第二十四章 并发编程
第二十五章 设计模式
附录:编程指南
本附录包含了有助于指导你进行低级程序设计和编写代码的建议。
当然,这些只是指导方针,而不是规则。我们的想法是将它们用作灵感,并记住偶尔会违反这些指导方针的特殊情况。
设计
- 优雅总是会有回报。从短期来看,似乎需要更长的时间才能找到一个真正优雅的问题解决方案,但是当该解决方案第一次应用并能轻松适应新情况,而不需要数小时,数天或数月的挣扎时,你会看到奖励(即使没有人可以测量它们)。它不仅为你提供了一个更容易构建和调试的程序,而且它也更容易理解和维护,这也正是经济价值所在。这一点可以通过一些经验来理解,因为当你想要使一段代码变得优雅时,你可能看起来效率不是很高。抵制急于求成的冲动,它只会减慢你的速度。
- 先让它工作,然后再让它变快。即使你确定一段代码非常重要并且它是你系统中的主要瓶颈**,也是如此。不要这样做。使用尽可能简单的设计使系统首先运行。然后如果速度不够快,请对其进行分析。你几乎总会发现“你的”瓶颈不是问题。节省时间,才是真正重要的东西。
- 记住“分而治之”的原则。如果所面临的问题太过混乱**,就去想象一下程序的基本操作,因为存在一个处理困难部分的神奇“片段”(piece)。该“片段”是一个对象,编写使用该对象的代码,然后查看该对象并将其困难部分封装到其他对象中,等等。
- 将类创建者与类用户(客户端程序员)分开。类用户是“客户”,不需要也不想知道类幕后发生了什么。类创建者必须是设计类的专家,他们编写类,以便新手程序员都可以使用它,并仍然可以在应用程序中稳健地工作。将该类视为其他类的服务提供者(service provider)。只有对其它类透明,才能很容易地使用这个类。
- 创建类时,给类起个清晰的名字,就算不需要注释也能理解这个类。你的目标应该是使客户端程序员的接口在概念上变得简单。为此,在适当时使用方法重载来创建直观,易用的接口。
- 你的分析和设计必须至少能够产生系统中的类、它们的公共接口以及它们与其他类的关系,尤其是基类。 如果你的设计方法产生的不止于此,就该问问自己,该方法生成的所有部分是否在程序的生命周期内都具有价值。如果不是,那么维护它们会很耗费精力。对于那些不会影响他们生产力的东西,开发团队的成员往往不会去维护,这是许多设计方法都没有考虑的生活现实。
- 让一切自动化。首先在编写类之前,编写测试代码,并将其与类保持一致。通过构建工具自动运行测试。你可能会使用事实上的标准Java构建工具Gradle。这样,通过运行测试代码可以自动验证任何更改,将能够立即发现错误。因为你知道自己拥有测试框架的安全网,所以当发现需要时,可以更大胆地进行彻底的更改。请记住,语言的巨大改进来自内置的测试,包括类型检查,异常处理等,但这些内置功能很有限,你必须完成剩下的工作,针对具体的类或程序,去完善这些测试内容,从而创建一个强大的系统。
- 在编写类之前,先编写测试代码,以验证类的设计是完善的。如果不编写测试代码,那么就不知道类是什么样的。此外,通过编写测试代码,往往能够激发出类中所需的其他功能或约束。而这些功能或约束并不总是出现在分析和设计过程中。测试还会提供示例代码,显示了如何使用这个类。
- 所有的软件设计问题,都可以通过引入一个额外的间接概念层次(extra level of conceptual indirection)来解决。这个软件工程的基本规则[^1]是抽象的基础,是面向对象编程的主要特征。在面向对象编程中,我们也可以这样说:“如果你的代码太复杂,就要生成更多的对象。”
- 间接(indirection)应具有意义(与准则9一致)。这个含义可以简单到“将常用代码放在单个方法中。”如果添加没有意义的间接(抽象,封装等)级别,那么它就像没有足够的间接性那样糟糕。
- 使类尽可能原子化。 为每个类提供一个明确的目的,它为其他类提供一致的服务。如果你的类或系统设计变得过于复杂,请将复杂类分解为更简单的类。最直观的指标是尺寸大小,如果一个类很大,那么它可能是做的事太多了,应该被拆分。建议重新设计类的线索是:
o 一个复杂的switch语句:考虑使用多态。
o 大量方法涵盖了很多不同类型的操作:考虑使用多个类。
o 大量成员变量涉及很多不同的特征:考虑使用多个类。
o 其他建议可以参见Martin Fowler的Refactoring: Improving the Design of Existing Code(重构:改善既有代码的设计)(Addison-Wesley 1999)。 - 注意长参数列表。那样方法调用会变得难以编写,读取和维护。相反,尝试将方法移动到更合适的类,并且(或者)将对象作为参数传递。
- 不要重复自己。如果一段代码出现在派生类的许多方法中,则将该代码放入基类中的单个方法中,并从派生类方法中调用它。这样不仅可以节省代码空间,而且可以轻松地传播更改。有时,发现这个通用代码会为接口添加有价值的功能。此指南的更简单版本也可以在没有继承的情况下发生:如果类具有重复代码的方法,则将该重复代码放入一个公共方,法并在其他方法中调用它。
- 注意switch语句或链式if-else子句。一个类型检查编码(type-check coding)的指示器意味着需要根据某种类型信息选择要执行的代码(确切的类型最初可能不明显)。很多时候可以用继承和多态替换这种代码,多态方法调用将会执行类型检查,并提供了更可靠和更容易的可扩展性。
- 从设计的角度,寻找和分离那些因不变的事物而改变的事物。也就是说,在不强制重新设计的情况下搜索可能想要更改的系统中的元素,然后将这些元素封装在类中。
- 不要通过子类扩展基本功能。如果一个接口元素对于类来说是必不可少的,则它应该在基类中,而不是在派生期间添加。如果要在继承期间添加方法,请考虑重新设计。
- 少即是多。从一个类的最小接口开始,尽可能小而简单,以解决手头的问题,但不要试图预测类的所有使用方式。在使用该类时,就将会了解如何扩展接口。但是,一旦这个类已经在使用了,就无法在不破坏客户端代码的情况下缩小接口。如果必须添加更多方法,那很好,它不会破坏代码。但即使新方法取代旧方法的功能,也只能是保留现有接口(如果需要,可以结合底层实现中的功能)。如果必须通过添加更多参数来扩展现有方法的接口,请使用新参数创建重载方法,这样,就不会影响到对现有方法的任何调用。
- 大声读出你的类以确保它们合乎逻辑。将基类和派生类之间的关系称为“is-a”,将成员对象称为“has-a”。
- 在需要在继承和组合之间作决定时,问一下自己是否必须向上转换为基类型。如果不是,则使用组合(成员对象)更好。这可以消除对多种基类型的感知需求(perceived need)。如果使用继承,则用户会认为他们应该向上转型。
- 注意重载。方法不应该基于参数的值而有条件地执行代码。在这里,应该创建两个或多个重载方法。
- 使用异常层次结构,最好是从标准Java异常层次结构中的特定适当类派生。然后,捕获异常的人可以为特定类型的异常编写处理程序,然后为基类型编写处理程序。如果添加新的派生异常,现有客户端代码仍将通过基类型捕获异常。
- 有时简单的聚合可以完成工作。航空公司的“乘客舒适系统”由独立的元素组成:座位,空调,影视等,但必须在飞机上创建许多这样的元素。你创建私有成员并建立一个全新的接口了吗?如果不是,在这种情况下,组件也应该是公共接口的一部分,因此应该创建公共成员对象。这些对象有自己的私有实现,这些实现仍然是安全的。请注意,简单聚合不是经常使用的解决方案,但确实会有时候会用到。
- 考虑客户程序员和维护代码的人的观点。设计类以便尽可能直观地被使用。预测要进行的更改,并精心设计类,以便轻松地进行更改。
- 注意“巨型对象综合症”(giant object syndrome)。这通常是程序员的痛苦,他们是面向对象编程的新手,总是编写面向过程程序并将其粘贴在一个或两个巨型对象中。除应用程序框架外,对象代表应用程序中的概念,而不是应用程序本身。
- 如果你必须做一些丑陋的事情,至少要把类内的丑陋本地化。
- 如果必须做一些不可移植的事情,那就对这个事情做一个抽象,并在一个类中进行本地化。这种额外的间接级别可防止在整个程序中扩散这种不可移植性。 (这个原则也体现在桥接模式中,等等)。
- 对象不应该仅仅只是持有一些数据。它们也应该有明确的行为。有时候,“数据传输对象”(data transfer objects)是合适的,但只有在泛型集合不合适时,才被明确用于打包和传输一组元素。
- 在从现有类创建新类时首先选择组合。仅在设计需要时才使用继承。如果在可以使用组合的地方使用继承,那么设计将会变得很复杂,这是没必要的。
- 使用继承和覆盖方法来表达行为的差异,而不是使用字段来表示状态的变化。如果发现一个类使用了状态变量,并且有一些方法是基于这些变量切换行为的,那么请重新设计它,以表示子类和覆盖方法中的行为差异。一个极端的反例是继承不同的类来表示颜色,而不是使用“颜色”字段。
- 注意协变(variance)。两个语义不同的对象可能具有相同的操作或职责。为了从继承中受益,会试图让其中一个成为另一个的子类,这是一种很自然的诱惑。这称为协变,但没有真正的理由去强制声明一个并不存在的父子类关系。更好的解决方案是创建一个通用基类,并为两者生成一个接口,使其成为这个通用基类的派生类。这仍然可以从继承中受益,并且这可能是关于设计的一个重要发现。
- 在继承期间注意限定(limitation)。最明确的设计为继承的类增加了新的功能。含糊的设计在继承期间删除旧功能而不添加新功能。但是规则是用来打破的,如果是通过调用一个旧的类库来工作,那么将一个现有类限制在其子类型中,可能比重构层次结构更有效,因此新类适合在旧类的上层。
- 使用设计模式来消除“裸功能”(naked functionality)。也就是说,如果类只需要创建一个对象,请不要推进应用程序并写下注释“只生成一个。”应该将其包装成一个单例(singleton)。如果主程序中有很多乱七八糟的代码去创建对象,那么找一个像工厂方法一样的创建模式,可以在其中封装创建过程。消除“裸功能”不仅会使代码更易于理解和维护,而且还会使其能够更加防范应对后面的善意维护者(well-intentioned maintainers)。
- 注意“分析瘫痪”(analysis paralysis)。记住,不得不经常在不了解整个项目的情况下推进项目,并且通常了解那些未知因素的最好、最快的方式是进入下一步而不是尝试在脑海中弄清楚。在获得解决方案之前,往往无法知道解决方案。Java有内置的防火墙,让它们为你工作。你在一个类或一组类中的错误不会破坏整个系统的完整性。
- 如果认为自己有很好的分析,设计或实施,请做一个演练。从团队外部带来一些人,不一定是顾问,但可以是公司内其他团体的人。用一双新眼睛评审你的工作,可以在一个更容易修复它们的阶段发现问题,而不仅仅是把大量时间和金钱全扔到演练过程中。
实现 - 遵循编码惯例。有很多不同的约定,例如,谷歌使用的约定(本书中的代码尽可能地遵循这些约定)。如果坚持使用其他语言的编码风格,那么读者就会很难去阅读。无论决定采用何种编码约定,都要确保它们在整个项目中保持一致。集成开发环境通常包含内置的重新格式化(reformatter)和检查器(checker)。
- 无论使用何种编码风格,如果你的团队(甚至更好是公司)对其进行标准化,它就确实会产生重大影响。这意味着,如果不符合这个标准,那么每个人都认为修复别人的编码风格是公平的游戏。标准化的价值在于解析代码可以花费较少的脑力,因此可以更专注于代码的含义。
- 遵循标准的大写规则。类名的第一个字母大写。字段,方法和对象(引用)的第一个字母应为小写。所有标识符应该将各个单词组合在一起,并将所有中间单词的首字母大写。例如:
o ThisIsAClassName
o thisIsAMethodOrFieldName
将 static final 类型的标识符的所有字母全部大写,并用下划线分隔各个单词,这些标识符在其定义中具有常量初始值。这表明它们是编译时常量。
o 包是一个特例,它们都是小写的字母,即使是中间词。域扩展(com,org,net,edu等)也应该是小写的。这是Java 1.1和Java 2之间的变化。 - 不要创建自己的“装饰”私有字段名称。这通常以前置下划线和字符的形式出现。匈牙利命名法(译者注:一种命名规范,基本原则是:变量名=属性+类型+对象描述。Win32程序风格采用这种命名法,如WORD wParam1;LONG lParam2;HANDLE hInstance)是最糟糕的例子,你可以在其中附加额外字符用于指示数据类型,用途,位置等,就好像你正在编写汇编语言一样,编译器根本没有提供额外的帮助。这些符号令人困惑,难以阅读,并且难以执行和维护。让类和包来指定名称范围。如果认为必须装饰名称以防止混淆,那么代码就可能过于混乱,这应该被简化。
- 在创建一般用途的类时,遵循“规范形式”。包括equals(),hashCode(),toString(),clone()的定义(实现Cloneable,或选择其他一些对象复制方法,如序列化),并实现Comparable和Serializable。
- 对读取和更改私有字段的方法使用“get”,“set”和“is”命名约定。这种做法不仅使类易于使用,而且也是命名这些方法的标准方法,因此读者更容易理解。
- 对于所创建的每个类,请包含该类的JUnit测试(请参阅junit.org以及第十六章:代码校验中的示例)。无需删除测试代码即可在项目中使用该类,如果进行更改,则可以轻松地重新运行测试。测试代码也能成为如何使用这个类的示例。
- 有时需要继承才能访问基类的protected成员。这可能导致对多种基类型的感知需求(perceived need)。如果不需要向上转型,则可以首先派生一个新类来执行受保护的访问。然后把该新类作为使用它的任何类中的成员对象,以此来代替直接继承。
- 为了提高效率,避免使用final方法。只有在分析后发现方法调用是瓶颈时,才将final用于此目的。
- 如果两个类以某种功能方式相互关联(例如集合和迭代器),则尝试使一个类成为另一个类的内部类。这不仅强调了类之间的关联,而且通过将类嵌套在另一个类中,可以允许在单个包中重用类名。Java集合库通过在每个集合类中定义内部Iterator类来实现此目的,从而为集合提供通用接口。使用内部类的另一个原因是作为私有实现的一部分。这里,内部类将有利于实现隐藏,而不是上面提到的类关联和防止命名空间污染。
- 只要你注意到类似乎彼此之间具有高耦合,请考虑如果使用内部类可能获得的编码和维护改进。内部类的使用不会解耦类,而是明确耦合关系,并且更方便。
- 不要成为过早优化的牺牲品。过早优化是很疯狂的行为。特别是,不要担心编写(或避免)本机方法(native methods),将某些方法设置为final,或者在首次构建系统时调整代码以使其高效。你的主要目标应该是验证设计。即使设计需要一定的效率,也先让它工作,然后再让它变快。
- 保持作用域尽可能小,以便能见度和对象的寿命尽可能小。这减少了在错误的上下文中使用对象并隐藏了难以发现的bug的机会。例如,假设有一个集合和一段迭代它的代码。如果复制该代码以用于一个新集合,那么可能会意外地将旧集合的大小用作新集合的迭代上限。但是,如果旧集合比较大,则会在编译时捕获错误。
- 使用标准Java库中的集合。熟练使用它们,将会大大提高工作效率。首选ArrayList用于序列,HashSet用于集合,HashMap用于关联数组,LinkedList用于堆栈(而不是Stack,尽管也可以创建一个适配器来提供堆栈接口)和队列(也可以使用适配器,如本书所示)。当使用前三个时,将其分别向上转型为List,Set和Map,那么就可以根据需要轻松更改为其他实现。
- 为使整个程序健壮,每个组件必须健壮。在所创建的每个类中,使用Java所提供的所有工具,如访问控制,异常,类型检查,同步等。这样,就可以在构建系统时安全地进入下一级抽象。
- 编译时错误优于运行时错误。尝试尽可能在错误发生点处理错误。在最近的处理程序中尽其所能地捕获它能处理的所有异常。在当前层面处理所能处理的所有异常,如果解决不了,就重新抛出异常。
- 注意长方法定义。方法应该是简短的功能单元,用于描述和实现类接口的离散部分。维护一个冗长而复杂的方法是很困难的,而且代价很大,并且这个方法可能是试图做了太多事情。如果看到这样的方法,这表明,至少应该将它分解为多种方法。也可能建议去创建一个新类。小的方法也可以促进类重用。(有时方法必须很大,但它们应该只做一件事。)
- 保持“尽可能私有”。一旦公开了你的类库中的一个方面(一个方法,一个类,一个字段),你就永远无法把它拿回来。如果这样做,就将破坏某些人的现有代码,迫使他们重写和重新设计。如果你只公开了必须公开的内容,就可以轻易地改变其他一切,而不会对其他人造成影响,而且由于设计趋于发展,这是一个重要的自由。通过这种方式,更改具体实现将对派生类造成的影响最小。在处理多线程时,私有尤其重要,只有私有字段可以防止不同步使用。具有包访问权限的类应该仍然具有私有字段,但通常有必要提供包访问权限的方法而不是将它们公开。
- 大量使用注释,并使用Javadoc commentdocumentation语法生成程序文档。但是,注释应该为代码增加真正的意义,如果注释只是重申了代码已经清楚表达的内容,这是令人讨厌的。请注意,Java类和方法名称的典型详细信息减少了对某些注释的需求。
- 避免使用“魔法数字”。这些是指硬编码到代码中的数字。如果后续必须要更改它们,那将是一场噩梦,因为你永远不知道“100”是指“数组大小”还是“完全不同的东西”。相反,创建一个带有描述性名称的常量并在整个程序中使用常量标识符。这使程序更易于理解,更易于维护。
- 在创建构造方法时,请考虑异常。最好的情况是,构造方法不会做任何抛出异常的事情。次一级的最佳方案是,该类仅由健壮的类组成或继承自健壮的类,因此如果抛出异常则不需要处理。否则,必须清除finally子句中的组合类。如果构造方法必然失败,则适当的操作是抛出异常,因此调用者不会认为该对象是正确创建的而盲目地继续下去。
- 在构造方法内部,只需要将对象设置为正确的状态。主动避免调用其他方法(final方法除外),因为这些方法可以被其他人覆盖,从而在构造期间产生意外结果。(有关详细信息,请参阅第六章:初始化和清理章节。)较小,较简单的构造方法不太可能抛出异常或导致问题。
- 如果类在客户端程序员用完对象时需要进行任何清理,请将清理代码放在一个明确定义的方法中,并使用像 dispose() 这样的名称来清楚地表明其目的。另外,在类中放置一个 boolean 标志来指示是否调用了 dispose() ,因此 finalize() 可以检查“终止条件”(参见第六章:初始化和清理章节)。
- finalize() 的职责只能是验证对象的“终止条件”以进行调试。(参见第六章:初始化和清理一章)在特殊情况下,可能需要释放垃圾收集器无法释放的内存。因为可能无法为对象调用垃圾收集器,所以无法使用 finalize() 执行必要的清理。为此,必须创建自己的 dispose() 方法。在类的 finalize() 方法中,检查以确保对象已被清理,如果没有被清理,则抛出一个派生自RuntimeException的异常,以指示编程错误。在依赖这样的计划之前,请确保 finalize() 适用于你的系统。(可能需要调用 System.gc() 来确保此行为。)
- 如果必须在特定范围内清理对象(除了通过垃圾收集器),请使用以下准则: 初始化对象,如果成功,立即进入一个带有 finally 子句的 try 块,并在 finally中执行清理操作。
- 在继承期间覆盖 finalize() 时,记得调用 super.finalize()。(如果是直接继承自 Object 则不需要这样做。)调用 super.finalize() 作为重写的 finalize()的最终行为而不是在第一行调用它,这样可以确保基类组件在需要时仍然有效。
- 创建固定大小的对象集合时,将它们转换为数组, 尤其是在从方法中返回此集合时。这样就可以获得数组编译时类型检查的好处,并且数组的接收者可能不需要在数组中强制转换对象来使用它们。请注意,集合库的基类 java.util.Collection 有两个 toArray() 方法来完成此任务。
- 优先选择 接口 而不是 抽象类。如果知道某些东西应该是基类,那么第一选择应该是使其成为一个接口,并且只有在需要方法定义或成员变量时才将其更改为抽象类。一个接口关心客户端想要做什么,而一个类倾向于关注(或允许)实现细节。
- 为了避免非常令人沮丧的经历,请确保类路径中的每个名称只对应一个不在包中的类。否则,编译器可以首先找到具有相同名称的其他类,并报告没有意义的错误消息。如果你怀疑自己有类路径问题,请尝试在类路径的每个起始点查找具有相同名称的 .class 文件。理想情况下,应该将所有类放在包中。
- 注意意外重载。如果尝试覆盖基类方法但是拼写错误,则最终会添加新方法而不是覆盖现有方法。但是,这是完全合法的,因此在编译时或运行时不会获得任何错误消息,但代码将无法正常工作。始终使用 @Override 注释来防止这种情况。
- 注意过早优化。先让它工作,然后再让它变快。除非发现代码的特定部分存在性能瓶颈。除非是使用分析器发现瓶颈,否则过早优化会浪费时间。性能调整所隐藏的额外成本是代码将变得难以理解和维护。
- 请注意,相比于编写代码,代码被阅读的机会更多。清晰的设计可能产生易于理解的程序,但注释,详细解释,测试和示例是非常宝贵的,它们可以帮助你和你的所有后继者。如果不出意外,试图从JDK文档中找出有用信息的挫败感应该可以说服你。
附录:文档注释
提取注释的工具称为Javadoc,它是 JDK 安装的一部分
Javadoc输出为一个html文件,您可以使用web浏览器查看它。
句法规则
所有Javadoc指令都发生在以 /** 开头(但仍然以 */ 结尾)的注释中。
有三种类型的注释文档,它们对应于注释前面的元素:类、字段或方法。
Javadoc处理注释文档仅适用于 公共 和 受保护 的成员。
默认情况下,将忽略对 私有成员 和包访问成员的注释(请参阅"隐藏实现"一章),并且您将看不到任何输出。
这是有道理的,因为仅客户端程序员的观点是,在文件外部可以使用 公共成员 和 受保护成员 。
内嵌 HTML
Javadoc传递未修改的HTML代码,用以生成的HTML文档。这使你可以充分利用HTML。但是,这样做的主要目的是让你格式化代码,
/**
- System.out.println(new Date());
示例标签
以下是一些可用于代码文档的Javadoc标记。
@see
这个标签可以将其他的类连接到文档中,Javadoc 将使用 @see 标记超链接到其他文档中,形式为:
@see classname
{@link package.class#member label}
和 @see 非常相似,不同之处在于它可以内联使用,并使用标签作为超链接文本,而不是“另请参阅”。
{@docRoot}
生成文档根目录的相对路径。对于显式超链接到文档树中的页面很有用。
{@inheritDoc}
将文档从此类的最近基类继承到当前文档注释中。
@version
其形式为:
@version version-information复制ErrorOK!
其中 version-information 是你认为适合包含的任何重要信息。当在Javadoc命令行上放置 -version 标志时,特别在生成的HTML文档中用于生成version信息。
@author
其形式为:
@author author-information复制ErrorOK!
author-information 大概率是你的名字,但是一样可以包含你的 email 地址或者其他合适的信息。当在 Javadoc 命令行上放置 -author 标志的时候,在生成的HTML文档中特别注明了作者信息。
你可以对作者列表使用多个作者标签,但是必须连续放置它们。所有作者信息都集中在生成的HTML中的单个段落中。
@since
此标记指示此代码的版本开始使用特定功能。例如,它出现在HTML Java文档中,以指示功能首次出现的JDK版本。
@param
这将生成有关方法参数的文档:
@param parameter-name description复制ErrorOK!
其中parameter-name是方法参数列表中的标识符,description 是可以在后续行中继续的文本。当遇到新的文档标签时,说明被视为完成。@param 标签的可以任意使用,大概每个参数一个。
@return
这记录了返回值:
@return description复制ErrorOK!
其中description给出了返回值的含义。它可延续到后面的行内。
@throws
一个方法可以产生许多不同类型的异常,所有这些异常都需要描述。异常标记的形式为:
@throws fully-qualified-class-name description复制ErrorOK!
fully-qualified-class-name 给出明确的异常分类名称,并且 description (可延续到后面的行内)告诉你为什么这特定类型的异常会在方法调用后出现。
@deprecated
这表示已被改进的功能取代的功能。deprecated 标记表明你不再使用此特定功能,因为将来有可能将其删除。标记为@不赞成使用的方法会导致编译器在使用时发出警告。在Java 5中,@deprecated Javadoc 标记已被 @Deprecated 注解取代
你可以在Java标准库的源代码中找到许多Javadoc注释文档的示例。