创建类的实例的最常见的方式是用new语句调用类的构造方法。在这种情况下,程序可以创建类的任意多个实例,每执行一条new语句,都会导致Java虚拟机的堆区中产生一个新的对象。假如类需要进一步封装创建自身实例的细节,并且控制自身实例的数目,那么可以提供静态工厂方法。
例如Class实例是Java虚拟机在加载一个类时自动创建的,程序无法用new语句创建java.lang.Class类的实例,因为Class类没有提供public类型的构造方法。为了使程序能获得代表某个类的Class实例,在Class类中提供了静态工厂方法forName(String name),它的使用方式如下:
Class c=Class.forName("Sample"); //返回代表Sample类的实例
构造方法的名字必须与类名相同。这一特性的优点是符合Java语言的规范,缺点是类的所有重载的构造方法的名字都相同,不能从名字上区分每个重载方法,容易引起混淆。静态工厂方法的方法名可以是任意的,这一特性的优点是可以提高程序代码的可读性,在方法名中能体现与实例有关的信息。例如Gender类有两个静态工厂方法:getFemale()和getMale()。
这一特性的缺点是:静态工厂方法与其他的静态方法没有明显的区别,使用户难以识别类中到底哪些静态方法专门负责返回类的实例。为了减少这一缺点带来的负面影响,可以在为静态工厂方法命名时尽量遵守约定俗成的规范,当然这不是必需的。目前比较流行的规范是把静态工厂方法命名为 valueOf() 或者 getInstance() 。
每次执行new语句时,都会创建一个新的对象。而静态工厂方法每次被调用的时候,是否会创建一个新的对象完全取决于方法的实现。
调用静态工厂方法返回的可能是缓存的一个对象,而不是一个新的对象。这可以减少创建新的对象,从来提高性能,
new语句只能创建当前类的实例,而静态工厂方法可以返回当前类的子类的实例,这一特性可在创建松耦合的系统接口时发挥作用。
静态工厂方法最主要的特点是:每次被调用的时候,不一定要创建一个新的对象。利用这一特点,静态工厂方法可用来创建以下类的实例:
单例类:只有惟一的实例的类。
枚举类:实例的数量有限的类。
具有实例缓存的类:能把已经创建的实例暂且存放在缓存中的类。
具有实例缓存的不可变类:不可变类的实例一旦创建,其属性值就不会被改变。
静态工厂方法所创建的对象可以在编译时不存在,动态创建对象,采用 反射 ,类似 Spring 的IOC容器反转。
静态工厂方法还可以简化参数化类型的对象创建 ,这个优点有点语法糖的味道,不过语法糖人人都喜欢啦。
Map< String,List < String > > m =
newHashMap< String,List < String > >();
publicstatic<K, V>HashMap<K, V> newInstance(){
returnnewHashMap<K, V>();
}
Map< String,List < String > > m =HashMap.newInstance();
第一行冗长的代码我们就可以简化成第三行的代码。
如果我们在一个类中将构造函数设为private,只提供静态工厂方法来创建对象,那么我们就不能通过继承的方式来扩展该类。不过还好的是,在需要进行扩展的时候,我们现在一般提倡用组合而不是继承。
类如果不含共有的或者受保护的构造器,就不能被子类化
第二个缺点是,静态构造方法不能和其他的静态方法很方便的区分开来。好吧,原文的意思是静态构造方法做的是一等公民(构造函数)的事,却得不到一等公民的待遇。遵守标准的命名习惯,可以弥补这一劣势,下面是静态工厂方法的一些惯用名称:
valueOf — 返回和参数一样的对象,通常都用作类型转换,比如 Intger.valueOf(int i)
of — 和 valueOf 类似。
getInstance — 根据参数返回对应的对象,该对象可能是缓存在对象池中的对象。对于单例 singleton,我们使用无参数的 getInstance,并且总是返回同一个对象
newInstance — 和 getInstance 一样,不过这个方法的调用每次返回的都是新的对象。
getType — 和 getInstance 类似,不过区别是这个方法返回的对象是另外一个不同的类。
newType — 和 getType 类似,不过每次返回的都是一个新的对象。
静态工厂和构造方法都有一个限制:它们不能很好地扩展到很多可选参数的情景。请考虑一个代表包装食品上的营养成分标签的例子。这些标签有几个必需的属性——每次建议的摄入量,每罐的份量和每份卡路里 ,以及超过20个可选的属性——总脂肪、饱和脂肪、反式脂肪、胆固醇、钠等等。大多数产品都有非零值,只有少数几个可选属性。
应该为这样的类编写什么样的构造方法或静态工厂?传统上,程序员使用了可伸缩(telescoping constructor)构造方法模式,在这种模式中,只提供了一个只所需参数的构造函数,另一个只有一个可选参数,第三个有两个可选参数,等等,最终在构造函数中包含所有可选参数。这就是它在实践中的样子。为了简便起见,只显示了四个可选属性:
// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
private final int servingSize; // (mL) required
private final int servings; // (per container) required
private final int calories; // (per serving) optional
private final int fat; // (g/serving) optional
private final int sodium; // (mg/serving) optional
private final int carbohydrate; // (g/serving) optional
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
当想要创建一个实例时,可以使用包含所有要设置的参数的最短参数列表的构造方法:
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
通常情况下,这个构造方法的调用需要许多你不想设置的参数,但是你不得不为它们传递一个值。 在这种情况下,我们为 fat 属性传递了 0 值。 『只有』六个参数可能看起来并不那么糟糕,但随着参数数量的增加,它会很快失控。
简而言之,可伸缩构造方法模式是有效的,但是当有很多参数时,很难编写客户端代码,而且很难读懂它。读者不知道这些值是什么意思,并且必须仔细地计算参数才能找到答案。一长串相同类型的参数可能会导致一些细微的bug。如果客户端意外地颠倒了两个这样的参数,编译器并不会抱怨,但是程序在运行时会出现错误行为
当在构造方法中遇到许多可选参数时,另一种选择是JavaBeans模式,在这种模式中,调用一个无参数的构造函数来创建对象,然后调用setter方法来设置每个必需的参数和可选参数:
// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
// Parameters initialized to default values (if any)
private int servingSize = -1; // Required; no default value
private int servings = -1; // Required; no default value
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public NutritionFacts() {
}
// Setters
public void setServingSize(int val) {
servingSize = val; }
public void setServings(int val) {
servings = val; }
public void setCalories(int val) {
calories = val; }
public void setFat(int val) {
fat = val; }
public void setSodium(int val) {
sodium = val; }
public void setCarbohydrate(int val) {
carbohydrate = val; }
}
这种模式没有伸缩构造方法模式的缺点。有点冗长,但创建实例很容易,并且易于阅读所生成的代码:
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
不幸的是,JavaBeans模式本身有严重的缺陷。由于构造方法在多次调用中被分割,所以在构造过程中JavaBean可能处于不一致的状态。该类没有通过检查构造参数参数的有效性来执行一致性的选项。在不一致的状态下尝试使用对象可能会导致与包含bug的代码大相径庭的错误,因此很难调试。一个相关的缺点是,JavaBeans模式排除了让类不可变的可能性,并且需要在程序员的部分增加工作以确保线程安全。
当对象的构造完成时,手动“冻结”对象,并且不允许它在解冻之前使用,可以减少这些缺点,但是这种变体在实践中很难使用并且很少使用。 而且,在运行时会导致错误,因为编译器无法确保程序员在使用对象之前调用 freeze 方法。
幸运的是,还有第三种选择,它结合了 可伸缩构造方法模式 的安全性和 javabean 模式的可读性。 它是 Builder 模式的一种形式。客户端不直接调用所需的对象,而是调用构造方法(或静态工厂),并使用所有必需的参数,并获得一个 builder 对象。然后,客户端调用 builder 对象的 setter 相似方法来设置每个可选参数。最后,客户端调用一个无参的 build 方法来生成对象,该对象通常是 不可变 的。Builder通常是它所构建的类的一个静态成员类。以下是它在实践中的示例:
// Builder Pattern
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
注意 NutritionFacts 类是不可变的,所有的参数默认值都在一个地方。builder的setter方法返回builder本身,这样调用就可以被链接起来,从而生成一个流畅的API。下面是客户端代码的示例:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
这个客户端代码很容易编写,更重要的是易于阅读。 Builder模式模拟Python和Scala中的命名可选参数。
为了简洁起见,省略了有效性检查。 要尽快检测无效参数,检查builder的构造方法和方法中的参数有效性。 在build方法调用的构造方法中检查包含多个参数的不变性。为了确保这些不变性不受攻击,在从builder复制参数后对对象属性进行检查。 如果检查失败,则抛出IllegalArgumentException异常,其详细消息指示哪些参数无效。
一些类是不希望其他类对它实例化的,比如java.util.Arrays、java.lnag.Math、java.util.Collections。我们发现这些类都会声明一个私有的构造方法,这样外界就无法通过默认无参的构造方法来创建这些类的对象,同时这些类也不能被继承。
我们知道抽象类也是不可以被实例化的,那么为什么不使用抽象类呢?
抽象类是可以有子类的,而且抽象类的出现可能会让人误解它的用途,被人理解为是故意用来被继承的。
总结:通过private修饰无参的构造器,可以防止其他地方实例化该类的对象,但是同样这个类不会再有子类了,因为子类对象的创建,首先就要先访问父类的构造方法。
在每次需要时重用一个对象而不是创建一个新的相同功能对象通常是恰当的。重用可以更快更流行。如果对象是不可变的(条目 17),它总是可以被重用。
作为一个不应该这样做的极端例子,请考虑以下语句:
String s = new String("bikini"); // DON'T DO THIS!
语句每次执行时都会创建一个新的String实例,而这些对象的创建都不是必需的。String构造方法(“bikini”)的参数本身就是一个bikini实例,它与构造方法创建的所有对象的功能相同。如果这种用法发生在循环中,或者在频繁调用的方法中,就可以毫无必要地创建数百万个String实例。
改进后的版本如下:
String s = "bikini";
该版本使用单个String实例,而不是每次执行时创建一个新实例。此外,它可以保证对象运行在同一虚拟机上的任何其他代码重用,而这些代码恰好包含相同的字符串字面量[JLS,3.10.5]。
通过使用静态工厂方法(static factory methods(项目1),可以避免创建不需要的对象。例如,工厂方法Boolean.valueOf(String) 比构造方法Boolean(String)更可取,后者在Java 9中被弃用。构造方法每次调用时都必须创建一个新对象,而工厂方法永远不需要这样做,在实践中也不需要。除了重用不可变对象,如果知道它们不会被修改,还可以重用可变对象。
一些对象的创建比其他对象的创建要昂贵得多。 如果要重复使用这样一个“昂贵的对象”,建议将其缓存起来以便重复使用。 不幸的是,当创建这样一个对象时并不总是很直观明显的。 假设你想写一个方法来确定一个字符串是否是一个有效的罗马数字。 以下是使用正则表达式完成此操作时最简单方法:
// Performance can be greatly improved!
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
这个实现的问题在于它依赖于String.matches方法。 虽然String.matches是检查字符串是否与正则表达式匹配的最简单方法,但它不适合在性能临界的情况下重复使用。 问题是它在内部为正则表达式创建一个Pattern实例,并且只使用它一次,之后它就有资格进行垃圾收集。 创建Pattern实例是昂贵的,因为它需要将正则表达式编译成有限状态机(finite state machine)。
为了提高性能,作为类初始化的一部分,将正则表达式显式编译为一个Pattern实例(不可变),缓存它,并在isRomanNumeral方法的每个调用中重复使用相同的实例:
// Reusing expensive object for improved performance
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
如果经常调用,isRomanNumeral的改进版本的性能会显著提升。 在我的机器上,原始版本在输入8个字符的字符串上需要1.1微秒,而改进的版本则需要0.17微秒,速度提高了6.5倍。 性能上不仅有所改善,而且更明确清晰了。 为不可见的Pattern实例创建静态final修饰的属性,并允许给它一个名字,这个名字比正则表达式本身更具可读性。
当你从手工管理内存的语言(比如C或者C++)转换到具有垃圾回收功能的语言的时候,程序猿的工作就会变得更加容易,因为当你用完了对象之后,他们就会被自动回收。当你第一次经历对象回收功能的时候,会觉得这简直有点不可思议。这很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情了,其实不然。
考虑下面这个简单的栈实现的例子:
// Can you spot the "memory leak"?
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
这个程序没有明显的错误(它的通用版本请见29项)。无论如何测试,它都会成功地通过每一项测试,但是这个程序中隐藏着一个问题。简而言之,改程序存在“内存泄漏”,由于垃圾收集器的活动增加或者内存占用增加,程序性能的降低会逐渐表现出来。在极端的情况下,这种内存泄漏会导致磁盘分页(Disk Paging),甚至导致程序失败并出现OutOfMemoryError,但这种失败情形相对比较少见。
那么,程序中哪里发生了内存泄漏呢?如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象将不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会回收,因为,栈内部维护着对这些对象的过期引用(obsolete references),所谓的过期引用,是指永远也不会再被解除的引用。在本例中,凡是在element数组的“活动部分”(active portion)之外的任何引用都是过期的。活动部分是指element中下标小于size的那些元素。
具有垃圾收集功能的编程语言中的内存泄漏(更恰当地称为无意识的对象保留)是隐蔽的。如果无意中保留了对象引用,则不仅将该对象从垃圾回收中排除,而且该对象引用的任何对象也是如此,依此类推。即使无意中保留了少量对象引用,也会阻止许多对象被垃圾回收器收集,对性能可能产生很大影响。
这类问题的修复方法很简单:一旦对象引用已经过期,只需要清空这些引用即可。对于上述例子中的Stack类而言,只要一个单元被弹出栈,指向它的引用就过期了,pop方法的修订版本如下所示:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
清空过期引用的另一个好处是,如果它们以后又被错误地解除引用,程序就会立即抛出NullPointerException异常,而不是悄悄地错误运行下去。尽快检测出程序中的错误总是有益的。
当程序员第一次被类似这样的问题困扰的时候,它们往往会过分小心:对于每一个对象的引用,一旦程序不再用到它,就把它清空。其实这样做即没必要,也不是我们所期望的,因为这样做会把程序代码弄得很乱。清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。如果你是在最紧凑的作用域范围内定义每一个变量(第57项),这种情形就会自然而然地发生。
那么,何时应该清空引用呢?Stack类的哪方面特性使它易于遭受内存泄漏的影响呢?简而言之,问题在于,Stack类自己管理内存(manage its own memory)、存储池(storage pool)包含了elements数组(对象引用单元,而不是对象本身)的元素。数组活动区域(同前面的定义)中的元素是已分配的(allocated),而数组其余部分的元素则是自由的(free)。但是垃圾回收器无法知道这一点;对于垃圾回收器而言,elements数组中的所有对象引用都同等有效。只有程序猿知道数组的非活动部分是不重要的。程序猿可以把这个情况告知垃圾回收器,做法很简单:一旦数组元素变成了非活动部分的一部分,程序猿就手动清空这些数组元素。
通常来说,只要类是自己管理内存,程序猿就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。
内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它在很长一段时间没有使用,但是却仍然留在缓存中。对于这个问题,这里有好几种解决方案。如果你正好要实现这样的缓存,只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存,当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
更为常见的情形则是,“缓存项的生命周期是否有意义”并不是很容易确定,随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该时不时地清除掉没用的项。这项清除工作可以由一个后台线程(可能是Timer或者ScheduledThreadPoolExecutor)来完成,或者也可以在给缓存添加新项的时候顺便进行清理。LinkedHashMap类利用它的removeEldestEntry方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用java.lang.ref。
内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显示地取消注册,那么除非你采取某些动作,否则他们就会积累下来。确保回调立即被当做垃圾回收的最佳方法是只保存他们的弱引用(weak reference),例如,只将它们保存成WeakHashMap中的键。
由于内存泄漏通常不会表现出明显的失败迹象,所以他们可以在一个系统中存在很多年。往往只有通过仔细检查代码,或者借助于Heap剖析工具(Heap Profiler)才能发现内存泄漏问题。因此,如果能在内存泄漏发生之前就知道如何预测此类问题,并阻止它们发生,那是最好不过的了。
为什么要避免使用终结方法
终结方法(finalizer)通常是不可预测的,也是很危险的,一般是不必要的。
不能保证会被及时的执行
不保证他们会被执行
使用终结方法会有严重的性能损失
不能保证会被及时的执行
一个对象变为不可达状态后,在JVM下一次执行垃圾回收时才会调用终结方法,中间这段时间间隔是不固定的,所以只要终结方法中做的处理注重时间,则它一定不会达到我们期望的理想状态。所以,不应该依赖终结方法来做一些对时间有要求的操作。
不保证他们会被执行
由于终结方法的执行时间是不确定的,所以,就有可能存在直到一个应用已经终止,终结方法仍未被执行。
使用终结方法会有严重的性能损失
重载了finalize()方法的对象,创建之后,当它没有任何一个对象引用的时候,GC会创建一个名为Finalizer对象,并放入一个 Finalize Queue(F-Queue) 的特殊队列里,下次执行垃圾回收的时候队列里标识的这些资源会被回收。但如果是在循环中大量创建了这种对象,随后其引用变为零,在这种场景下,就会大量创建Finalizer对象,而处理 Finalizer对象的线程比主线程的优先级要低,创建的速度大于回收的速度,就有可能导致内存溢出。
总结:就当finalize()不存在。
一、不需要覆盖equals的情景
1、类的每个实例本质上都是唯一的
对于代表活动实体而不是值(value)的类来说确实如此,比如Thread
2、不关心类是否提供了“逻辑相等”的测试功能
书本上的例子是java.util.Random覆盖equals,以检查两个Random实例是否产生相同的随机数序列,但是设计者并不认为客户需要或者期望这样的功能。在这种情况下,从Object继承得到的equals实现已经足够了
3、父类已经覆盖了equals,从父类继承过来的行为对于子类也是合适的
例如大多数的Set实现都是从AbstractSet继承equals实现
4、类是私有的或者是包级私有的,可以确定它的equals方法永远不会被调用。在这种情况下,无疑是应该覆盖equals方法的,以防它被以外调用
这种情况下,只是对equals的一种废弃,并没有新加什么功能
@override
public boolean equals(Object obj){
throw new AssertionError();
}
二、、需要覆盖equals的情景
如果类具有自己特有的“逻辑相等”概念(不同于对象等同的概念),而且父类还没有覆盖equals以实现期望行为,这时就需要覆盖equals方法。覆盖equals方法的时候,必须遵守以下通用约定
自反性(reflexive):对于任何非null的引用值x,x.equals(x)必须返回true
对称性(symmetric):对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true
传递性(transitive):对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)必须返回true
一致性(consistent):对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false
非空性(Non—nullity):对于任何非null的引用值x,x.equals(null)必须返回falsepublic class
四、实现高质量equals的诀窍
1、使用==操作符检查“参数是否为这个对象的引用”。如果是则返回true。这是一种性能优化
if(this==obj)
return true;
2、使用instanceof操作符检查“参数是否为正确的类型”,如果不是则返回false。所谓的正确的类型是指equals方法所在的那个类,或者是该类的父类或接口
3、把参数转化成正确的类型:因为上一步已经做过instanceof测试,所以确保转化会成功
4、对于该类的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配(其实就是比较两个对象的值是否相等了)
5、当你编写完equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?当然equals也必须满足其他两个约定(自反性、非空性)但是这两种约定通常会自动满足
根据以上的诀窍,就可以构建出一个比较不错的equals方法实现了
可以看到下面重写的equals就是根据以上几点诀窍来写的
package linjie.com.xxx;
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(short areaCode, short prefix, short lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = (short)areaCode;
this.prefix = (short)prefix;
this.lineNumber = (short)lineNumber;
}
private static void rangeCheck(int arg,int max,String name) {
if(arg < 0 || arg > max)
throw new IllegalArgumentException(name +": "+ arg);
}
@Override
public boolean equals(Object obj) {
//1、参数是否为这个对象的引用
if(obj == this)
return true;
//2、使用instanceof检查
if(!(obj instanceof PhoneNumber))
return false;
//3、把参数转化成正确的类型
PhoneNumber pn = (PhoneNumber)obj;
//4、比较两个对象的值是否相等
return pn.lineNumber == lineNumber
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
}
五、注意点
覆盖equals时总要覆盖hashCode(见第9条,这里暂不阐述了)
不要企图让equals方法过于智能:不要想过度地去寻求各种等价关系,否则容易陷入各种麻烦
不要将equals声明中的Object对象替换为其他类型,不然就不是重写equals了,而是重载了。加上@override可以避免这种错误发生
1.为什么覆盖equals()时总要覆盖hashCode()?
如果不这样做的话,就会违反了Object.hashCode()的通用约定。
通用约定如下:
只要对象的equals()方法的比较操作所用到的信息没有被修改,那么多洗调用hashCode()方法都必须返回同一个整数。
如果两个对象equals()判断相等,那么其hashCode()返回值也相等。
如果两个对象hashCode()返回值相等,那么equals()判断不一定相等(尽可能的满足不同对象hashCode不一致,有可能提高散列表的性能)。
2.覆盖equals()而未覆盖hashCode的结果
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode,int prefix,int lineNumber) {
this.areaCode=(short)areaCode;
this.prefix=(short)prefix;
this.lineNumber=(short)lineNumber;
}
@Override//实现对象的逻辑相等
public boolean equals(Object obj) {
if(obj==this) return true;
if(!(obj instanceof PhoneNumber))
return false;
PhoneNumber pn=(PhoneNumber)obj;
return pn.areaCode ==areaCode
&&pn.prefix == prefix
&&pn.lineNumber == lineNumber;
}
/*这里将自己实现的hashCode注释,测试的时候是没有这个方法的
@Override //正确的hashCode()方法重写
public int hashCode() {
int result=17;
result =31*result + areaCode;
result =31*result + prefix;
result =31*result + lineNumber;
return result;
}
*/
}
测试代码如下:
import hashCode.PhoneNumber;
//如果你不明白哈希表的工作原理,请自行百度
public class HashCodeMain {
public static void main(String[] args) {
Map<PhoneNumber, String> map=new HashMap<PhoneNumber, String>();
PhoneNumber pn=new PhoneNumber(707, 867, 5309);
map.put(pn, "Jenny");
map.get(new PhoneNumber(707, 867, 5309));//null
map.get(pn);//Jenny
}
上面的结果违反了第二条约定:相等的对象必须具有相等的散列值;此处的相等是根据equals()方法的逻辑相等,而默认的散列值却被实现成了与对象地址相关。可见equals()方法的逻辑相等与hashCode()方法的地址相等发生了冲突。
如果我们取消hashCode()方法的注释,那么这个测试代码的结果就会变成两个Jenny,而这才是我们想要的结果。
3.如何正确的覆盖hashCode()?
把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中
对于对象中每个关键域f(指equals()方法中涉及的每个域),完成以下步骤:
a.为该域计算int类型的散列码c:
1.如果该域是boolean,则计算(f ? 1:0)。
2.如果该域是byte,char,short,或int类型,则计算(int)f。
3.如果该域是long类型,则计算(int)(f^(f>>>32))。
4.如果该域是float类型,则计算Float.floatToBits(f)。
5.如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤a.3,为得到的long类型计算散列值。
6.如果该域是一个对象引用,并且该类的equals()方法通过递归的调用equals()的方式来比较这个域,则同样为这个域递归地调用hashCode();如果这个域的值为null,则返回0。
7.如果该域是一个数组,则要把每一个元素当做单独的域来处理。可以递归地引用上述规则。如果数组中的每个元素都很重要,可以使用JDK1.5增加的方法Arrays.hashCode()方法。
public static int hashCode(long a[]) {
if (a == null)
return 0;
int result = 1;
for (long element : a) {
int elementHash = (int)(element ^ (element >>> 32));
result = 31 * result + elementHash;
}
return result;
}
b.按照下面的公式,把步骤a中计算得到的散列码c合并到result中:
result =31 *result + c;
返回result(如果一个类是不可变的,并且计算散列码的开销也比较大,就应该考虑把散列码作为属性存在对象内部,而不是每次请求的时候都重新计算散列码)。
toString方法应该返回对象中包含的所有值得关注的信息
在实现toString时需要做一个决定:是否在文档中指定返回值的格式.
指定格式的好处是,它可以被用做一种标准的,明确的,适合人阅读的对象表示法,如果指定格式,最好再提供一个相匹配的静态工厂或构造器,以便程序员可以方便在对象和它的字符串表示法之间转换.
指定格式的坏处是:如果这个类被广泛使用了,那么一旦指定格式,就要一如既往
无论是否指定格式,都应该在文档中明确的表明意图
无论是否指定格式,都为toString返回值中包含的所有信息,提供了一种编程式的访问途径.
Cloneable 接口的目的是作为对象的一个 mixin 接口, 表明这样的对象允许克隆.
遗憾的是,因为Object 对象的clone 方法是受保护的, 一个对象仅仅实现了Cloneable, 但能直接调用clone, 所以导致Cloneable 没有成功达到目的.
如果一个类实现了Cloneable, Object 的 clone 方法就返回该对象的逐域拷贝, 否则就会抛出 CloneNotSupportException 异常.
Clone 方法的通用约定是非常弱的:
创建和返回该对象的一个拷贝,这个拷贝的精确含义取决于该对象的类. 通常情况下满足,但是不强制要求一定要满足:
x.clone() != x;
x.clone().getClass() == x.getClass();
x.clone.equals(x);
对于实现了 Cloneable 的类, 我们总是期望它能提供一个功能适当的公有的clone的方法.
实际上, clone 方法就是另一个构造器; 必须确保它不会伤害到原始的对象, 并确保正确地创建被克隆对象中的约束条件.
clone 架构与引用可变对象的final域的正常使用是不兼容的.
简而言之, 所有实现了 Cloneable 接口的类都应该用一个公有的方法覆盖 clone.
此公有的clone 方法首先调用 super.clone, 然后修正任何需要修正的域.一般情况下,这意味着要拷贝任何包含内部"深层结构"的可变对象, 并用指向新对象的引用来代替原来指向这些对象的引用. 如果该类只包含基本类型的域,或者指向不可变对象的引用,那么多半情况下是没有域需要修正的.
另一个实现对象拷贝的好办法就是提供一个拷贝构造器,或拷贝工厂:
拷贝构造器: public Yum( Yum yum)
拷贝工厂: public static Yum nerInstance(Yum yum)
拷贝构造器和拷贝工厂,都比Cloneable/clone方法具有更多的优势:
不依赖于某一种有风险的,语言之外的对象创建机制
不要求遵守尚未制定好的文档规范
不会与final 域的正常使用发生冲突
不会抛出不必要的首检异常
不需要进行类型转换
Cloneable 具有很多的限制,所以其他的接口都不应该扩展(extend)这个接口, 为了继承而设计的类也不应该实现(implement)这个接口.
compareTo的通用约定
当将这个对象与指定对象进行比较时,当该对象小于、等于或大于指定对象的时候,分别返回一个负整数、零或正整数;
如果由于指定的对象的类型而无法与对象进行比较,则抛出ClassCastException异常;
agn(x.compareTo(y)) == -sgn(y.compareTo(y))(自反性),并且前者抛出异常时,后者才抛出异常;
(x.compareTo(y) > 0 && y.compareTo(z) > 0)则(x.compareTo(z) > 0)(传递性)
如果x.compareTo(y) == 0,则所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z));
若compareTo方法与equals方法的比较结果不同则需在文档中注明。
compareTo与equals不同
在跨越不同的类的时候,compareTo可以不做比较:如果两个被比较的对象引用不同的类对象,compareTo可以抛出ClassCastException异常。
为实现Compareable接口的类增加值组件
代码演示
class PhoneNumber implements Compareable{
private final short areaCode;
private final short prefix;
private final short lineNumber;
···
public int compareTo(PhoneNumber pn) {
// compare areaCode
if (areaCode < pn.areaCode) {
return -1;
}
if (areaCode > pn.areaCode) {
return 1;
}
// areaCode are equal, compare prefixes
if (prefix < pn,prefix) {
return -1;
}
if (prefix > pn,prefix) {
return 1;
}
// areaCode and prefixes are equal, compare lineNumbers
if (lineNumber < pn.lineNumber) {
return -1;
}
if (lineNumber > pn.lineNumber) {
return 1;
}
return 0;
}
}
若程序中为了简便写成如下形式,注意溢出的问题
public int compareTo(PhoneNumber pn) {
int areaCodeDiff = areaCode - pn.areaCode;
if (areaCodeDiff != 0) {
return areaCodeDIff;
}
int prefixDiff = prefix - pn.prefix;
if (prefixDiff != 0) {
return prefixDIff;
}
return lineNumber - pn.lineNumber;
}
1、不可变类是实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。
例如String、BigInteger和BigDecimal类。不可变类更易于设计、实现和使用。
2、设计不可变类的原则:
1:不要提供任何修改对象状态的方法。(setter方法)
2:保证类不会被扩展,用final修饰类或者是私有化构造器提供公有静态工厂方法。
3:使所有域都是final的
4:使所有的域都是私有的
5:确保对于任何可变组件的互斥访问
3、不可变对象本质上是线程安全的,它们不要求同步。所以,不可变对象可以被自由的共享。
4、使类成为不可子类化的另一种方法是将类的构造函数私有化,然后提供静态工厂来产生对象。
public class Complex{
private final double re;
private final double im;
private Complex(double im, double re) {
this.im = im;
this.re = re;
}
public static Complex valueOf(double re,double im){
return new Complex(re,im);
}
}
5、总之,坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由也要让类成为可变的类,否则就应该是不可变的。
对于有些类而言,其不可变性是不切实际的。如果类不能被做成是不可变的,仍然应该是尽可能地限制它的可变性。
降低对象可以存在的状态数,可以更容易地分析该对象的行为,同时降低出错的可能性。
因此,除非有令人信服的理由要使域变成是非final的,否则要使每个域都是final的。
继承(inheritance) 是实现代码重用的有力手段,但它并非永远是完成这项工作的最佳工具.
在包的内部使用继承是非常安全的.然而,对于普通的具体类进行跨越包边界的继承,则是非常危险的.
与方法调用不同的是,继承打破了封装性.
子类依赖于其超类中特定功能的实现细节.
超类在后续的版本中可以获得新的方法.
避免上述问题: 不用扩展现有的类,而是在新的类中新增一个私有域,它引用现有类的一个实例. 这种设计被称为"复合"(composition)
这样得到的类会非常稳固,它不依赖于现有类的实现细节,即使现有的类添加了新的方法,也不会影响新的类.
只有当子类真正是超类的子类型时, 才适合继承.
也就是对于两个类A和B, 只有当两者之间确实存在"is-a"关系的时候,类B才应该扩展A.
简而言之, 继承的功能非常强大,但是也存在诸多问题,因为它违背了封装原则.只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的. 即便如此, 如果子类和超类处在不同的包中, 并且超类并不是为了继承而设计的, 那么继承将会导致脆弱性. 为了避免这种脆弱性, 可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候. 包装类不仅比子类更加健壮, 而且功能也更加强大.
1、 对于专门为了继承而设计并且具有良好文档说明的类而言,该类的文档必须精确地描述覆盖每个方法所带来的影响。
该类必须有文档说明它可覆盖的方法的自用性。
对于每个公有的或受保护的方法或者构造器,它的文档必须指明该方法或者构造器调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程的。
更一般的,类必须在文档中说明,在哪些情况下它会调用可覆盖的方法。
2、按惯例,如果方法调用到了可覆盖的方法,在它的文档注释的末尾应该包含关于这些调用的描述信息。
这段描述信息要以这样的句子开头:“This implementation…”。这样的句子不应该被认为是在表明该行为可能会随着版本的变迁而改变。它意味着这段描述关注该方法的内部工作情况。
3、在第16条的情形中,程序员在子类化HashSet的时候,并无法说明覆盖add方法是否会影响addAll方法的行为。
关于程序文档的格言:好的API文档应该打桩一个给定的方法做了什么工作,而不是描述它是如何做到的。
所以,为了设计一个类的文档,以便它能够被安全的子类化,必须描述清楚那些有可能未定义的实现细节。
4、为了继承而进行设计不仅仅涉及自用模式的文档设计。为了使程序员能够编写出更加有效的子类,而无需随不必要的痛苦,类必须通过某种形式提供适当的钩子,以便能够进入到它的内部工作流程中,这种形式可以是精心选择的受保护的方法,也可以是受保护的域,后者比较少见。
5、一个为了继承而设计的类(超类)应该是:
a.为了允许被继承,无论是直接还是间接,构造方法都不能够调用非final方法。否则程序有可能失败。
由于超类的构造方法比子类的构造方法先执行,所以子类中改写版本的方法将会在子类的构造方法运行之前先被调用。如果该改写版本的方法依赖于子类构造方法所执行的初始化工作,那么该方法将不会如预期执行。
构造器决不能调用可被覆盖的方法。
class Super {
public Super() {
method();
}
public void method() {
System.out.println("Super method");
}
}
public class Sub extends Super {
private final Date date;
public Sub() {
date = new Date();
}
public void method() {
System.out.println(date);
}
public static void main(String[] args) {
Sub s = new Sub();
s.method();
}
}
输出:
null
Mon Mar 02 14:51:39 CST 2020
构造器中调用了可覆盖方法,而导致NullPointerException。
b.设计的超类实现Serializable或者Cloneable接口都是很麻烦的事情。因为clone和readObject都类似一个构造方法。
所以超类要实现这两个接口就必须遵循这个规则:clone和readObject都不能够调用可改写的方法,无论是直接还是间接方式。对于readObject方法,子类改写版本的方法将在子类的状态被反序列化之前先执行;
对于clone方法,改变版本的方法将在子类的clone方法有机会修正被克隆对象的状态之前先被执行。无论哪种情况,都可能导致程序失败,在clone方法中,这种失败会导致损害原始对象以及克隆的对象本身。
c.如果超类实现了Serializable接口,并且该类有一个readResolve或者writeReplace方法,那么就必须是readResolve或者writeReplace方法成为protectd,而不是private。如果是私有的话,那么子类会忽略掉这两个方法。
d.对于一些不需要被继承的类,最好是把它定义为不能够被继承的类。可以使用final或者提供静态工厂方式来提供对象。
Java程序设计语言提供两种机制,可以用来定义允许多个实现的类型:接口和抽象方法,这两者直接最为明显的区别在于,抽象类允许某些方法的实现,但接口不允许,一个更为重要的区别在于,为了实现由抽象类定义的类型,类必须成为抽象类的一个子类。任何一个类,只要定义了所有必要的方法,并且遵守通用约定,它就被允许实现一个接口,而不管这个类是处于类层次的哪个位置。因为Java只允许单继承,所有抽象类作为类型定义受到类极大的限制。
现有的类很容易被更新,以实现新的接口。
如果你希望让两个类扩展同一个抽象类,就必须把抽象类放到类型层次结构的高处,以便这两个类的一个祖先成为它的子类。遗憾的是这样做会间接到伤害到类层次,迫使这个公共祖先到所有后代类都扩展这个新的抽象类,无论它对于这些后代类是否合适。
接口是定义混合类型的理想选择。
接口允许我们构造非层次结构的类型框架。
假如我们有两个接口,一个表示歌唱家,另一个表示作曲家,在现实生活中,有很多人即是歌唱家又是作曲家,如果是接口,我只需要同时实现这两个接口就可以,如果是抽象类,因为Java是单继承的,我就没有办法描述这一类的人。
虽然接口不允许包含方法的实现,但是,使用接口来定义类型并不妨碍你为程序猿提供实现上的帮助。通过对你导出的每个重要接口都提供一个抽象骨架的实现类,把接口和抽象类的优点都结合起来。接口的作用仍然是定义类型,但是骨架的实现类接管类所有与接口实现相关的工作。
骨架为接口提供实现上的帮助,但又不强加“抽象类被用作类型定义时”所特有的严格限制。对于接口大多数的实现来讲,扩展骨架实现类是个很显然的选择,但不是必须的。如果预制的类无法扩展骨架实现类,这个类始终可以收工实现这个接口。此外,骨架实现类仍然有助于接口的实现。实现类这个接口的类可以把对于这个接口方法的调用,转发到一个内部私有类的实例上,这个内部私有类扩展骨架实现类。这种方法被称作模拟多重继承。这项技术具有多重继承的绝大多数有点,同时又避免了相应的缺陷。
编写骨架实现类相对比较简单,只是有点单调乏味。首先,必须认真研究接口,并确定哪些方法时最为基本的,其他方法则可以根据它们来实现。这些方法将成为骨架实现类中的抽象方法。然后,必须为接口中的所有其他的方法提供具体实现。例如,下面是Map.Entry接口的骨架实现类:
/**
* 接口优于抽象类
* @author weishiyao
*
* @param
* @param
*/
// Skeletal Implementation
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V>{
// Primitive operations
public abstract K getKey();
public abstract V getValue();
// Entries in modifiable maps must override this method
public V setValue(V value) {
throw new UnsupportedOperationException();
}
// Implements the general contract of Map.Entry.equals
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Map.Entry)) {
return false;
}
Map.Entry<?, ?> arg = (Map.Entry) obj;
return equals(getKey(), arg.getKey()) && equals(getValue(), arg.getValue());
}
private static boolean equals(Object obj1, Object obj2) {
return obj1 == null ? obj2 == null : obj1.equals(obj2);
}
// Implements the general contract of Map.Entry.hashCode
@Override
public int hashCode() {
return hashCode(getKey()) ^ hashCode(getValue());
}
private static int hashCode(Object obj) {
return obj == null ? 0 : obj.hashCode();
}
}
骨架实现类时为了继承的目的而设计的,这种使用方式在平时开发中经常可以见到,很多框架中都有使用方式,之前为也是见过好多次框架中这么使用,大致能明白这么写是干什么,这次经过这么详细讲解,算是彻底明白为什么要这么使用了,能想到这种写法的人真的也是大神了。
当类实现接口时,接口就充当可以引用这个类的实例的类型(type)。因此类实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了其他目的而定义接口是不恰当的。
有一种接口被称为常量接口(constant interface),他不满足上面的条件。这种接口没有包含任何方法,他只包含静态的final域,每个域都到处一个常量。使用这些常量的类实现这个接口,以避免用类名来修饰常量名。
常量接口模式是对接口的不良使用
类在内部使用某些常量,这纯粹是实现细节。实现常量接口,会导致把这样的实现细节泄露到该类的导出API中。类实现常量接口,这对于用户来讲并没有什么价值。实际上,这样做反而会使他们更加糊涂。更糟糕的是,他代表了一种承诺:如果将来的发行版本中,这个类被修改了,他不再需要使用这些常量了,他依然必须实现这个接口,以确保二进制兼容性。如果非final类实现了常亮接口,他的所有子类的命名空间也会被接口中的常量所“污染”。
如果要导出常量,可以有几种合理的选择方案
如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中
如果这些常量最好被看做枚举类型的成员,就应该用枚举类型(enum type)来导出这些常量
使用不可实例化的工具类(utility class)来导出这些常量
工具类通常要求客户端要用类名来修饰这些常量名,例如PhysicalConstants.AVOGADROS_NUMBER。如果大量利用工具类导出的常量,可以通过利用静态导入(static import)机制,避免用类名来修饰常量名。
总结
简而言之,接口应该只被用来定义类型,他不应该被用来导出常量。
有时候,可能会遇到带有两种甚至更多钟风格的类的实例的类,并包含表示实例风格的(tag)域。例如下面这个类,它能够表示圆形或者矩形:
// Tagged class - vastly inferior to a class hierarchy
{
public class Figure1{
enum Shape {
RECTANGLE,
CIRCLE
}
// Tag field - the shape of this figure
final Shape shape;
// These field are use only if shape if RECTANGLE
double length;
double width;
// This field is use only if shape is CIRCLE
double radius;
// Constructor for circle
public Figure1(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// Constructor for rectangle
public Figure1(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch (shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError();
}
}
}
}
这种标签类有着许多缺点:
1.它们中充斥着样板代码,包括枚举声明,标签域以及条件语句。由于许多个实现乱七八糟的挤在了单个类中,破坏了可读性。
2.内存占用也增加了,因为实例承担了属于其他风格的不相关的域。
3.域也不能做成final类型的,除非构造器初始化了不相关的域,产生了更多的样板代码。构造器必须不借助编译器,来设置标签域,并且初始化正确的数据域;如果初始化了错误的域,程序就会在运行的时候出错。
4.无法给标签类添加风格,除非可以修改源文件,如果一定要添加风格,就必须给每个条件语句都添加一个条件,否则就会在运行的时候失败。
5.最后,实例的数据类型没有提供任何关于其风格的线索
总结:标签类过于冗长、容易出错,并且效率低下
幸运的是,面向对象的语言java,提供了其他更好的方法来定义能表示多种风格对象的单个数据类型:子类型化。标签类正是类层次的一种简单效仿。
为了将标签类转化成类层次,首先要为标签类中的每一个方法都定义一个包含抽象方法的抽象类,这每个方法的行为都依赖于标签值。在Figure类中,只有一个这样的方法:area。这个抽象类是类层次的根。如果还有其他的方法行为不依赖于某个标签的值,就把这样的方法放到这个类中。同样的,如果所有的方法都用到了某些数据域,就应该把他们放在这个类中。在Figure类中,不存在这种类型独立的方法或者数据域。
// Class hierarchy replacement for a tagged class
abstract class Figure2 {
abstract double area();
}
class Circle extends Figure2 {
final double radius;
Circle(double radius) {
this.radius = radius;
}
double area() {
return Math.PI * (radius * radius);
}
}
class Rectangle extends Figure2 {
final double length;
final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
double area() {
return length * width;
}
}
这个类纠正了前面提到过的标签类的所有缺点。这段代码简单且清楚,没有包含在原来版本中所见到的所有样板代码。每个类型都有自己的类,这些类都没有受到不相关的数据域的拖累。所有的域都是final的。编译器确保每个类的构造器都初始化它的数据域,对于根类中声明的每个抽象方法,都确保有一个实现。这样就杜绝了由于遗漏switch case而导致运行失败的可能性。多个程序员都可以独立的扩展层次结构,并且不用访问根类的资源代码就能互相操作。每种类型都有一种相关的独立的数据类型,允许程序员指明变量类型,限制变量,并将参数输入到特殊的类型。
类层次的另一个好处在于,它们可以用来反应类型之间本质上的层次关系,有助于增强灵活性,并更好的进行编译时类型检查。
总而言之,标签类很少有适用的时候。当你想要编写一个包含显示的标签域的类时,应该考虑一下,这个标签是否可以被取消,这个类是否可以用类层次来代替,当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去。
有些语言支持函数指针、代理、lambda表达式,或者支持类似的机制,允许程序把“调用特殊函数的能力”储存起来并传递这种能力。这种机制通常用于允许函数的调用者通过传入第二个函数,来指定自己的行为。比较器函数有两个参数,都是指向元素的指针。如果第一个参数所指的元素小于第二个参数所指的元素,则返回一个负整数;如果两个元素相等则返回零;如果第一个参数所指的元素大雨第二个,则返回一个正整数。通过传递不同的比较器函数,就可以获得各种不同的排列顺序。这正是策略模式的一个例子。比较器函数代表一种为元素排列的策略。
Java没有提供函数指针,但是可以用对象引用实现同样的功能。调用对象上的方法通常是执行该对象上的某个操作。然而,我们也可能定义这样一种对象,它的方法执行其他对象上的操作。如果一个类仅仅导出这样的一个方法,它的实例上就等同于一个指向该方法的指针。这样的实例被称为函数对象。考虑这样一个类:
class StringLengthComparator {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
这个类导出一个带两个字符串参数的方法。指向StringLengthComparator对象的引用可以被当做是一个指向该比较器的“函数指针”,可以在任意一对字符串上被调用。换句话说,StringLengthComparator实例是用于字符串比较操作的具体策略。
作为典型的具体策略类,StringLengthComparator类是无状态的:它没有域,所以,这个类的所有实例在功能上是相互等价的。因此,它作为一个Singleton是非常合适的,可以节省不必要的对象创建开销:
/**
* 用函数对象表示策略
* @author weishiyao
*
*/
public class StringLengthComparator {
private StringLengthComparator() {
}
public static final StringLengthComparator INSTANCE = new StringLengthComparator();
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
为了把StringLengthComparator实例传递给方法,需要适当的参数类型。使用StringLengthComparator并不好,因为客户端无法传递任何其他的比较策略。相反,我们需要定义一个Comparator接口,并修改StringLengthComparator来实现这个接口。换句话说,我们在设计具体的策略类时,还需要定义一个策略接口
// Strategy interface
public interface Comparator<T> {
public int compare(T t1, T t2);
}
Comparator接口的这个定义碰巧也出现在java.util包中,但是这并不神奇,我们自己也可以定义它。Comparator接口时范型的,因此它适合作为除字符串之外其他对象的比较器。它的compare两个参数类型为T,而不是String。只要声明前面所示的StringLengthComparator类要这么做,就可以用它实现Comparator接口。
具体的策略类往往使用匿名类声明,下面的语句根据长度对一个字符串数组进行排序:
Arrays.sort(stringArray, new Comparator<T>() {
@Override
public int compare(String s1, String s2) {
// TODO Auto-generated method stub
return s1.length() - s2.length();
}
});
但是注意,以这种方式使用匿名类时,将会在每次执行调用的时候创建一个新的实例。如果它被重复执行,考虑将函数对象存储到一个私有的静态final域里,并重用它。这样做的另一种好处是,可以为这个函数对象取一个有意义的域名。
因为策略接口被用作所有具体策略实例的类型,所以并不需要为了倒出具体策略,而把策略类做成公有的。相反“宿主类”还可以导出公有的静态域,其类型为策略接口,具体的策略类可以是宿主类的私有前套类。下面的例子使用静态成员类,而不是匿名类,以便允许具体的策略类实现第二个接口Serializable
/**
* 用函数对象表示策略
* @author weishiyao
*
*/
public class Host {
private static class StrLenCmp implements Comparator<String>, Serializable {
/**
*
*/
private static final long serialVersionUID = -5797980299250787300L;
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
// Return comparator is serializable
public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
public static void main(String[] args) {
System.out.println(STRING_LENGTH_COMPARATOR.compare("aaaaaa", "aaaaa"));
}
}
String类利用这种模式,通过它的STRING_LENGTH_COMPARATOR域,导出一个不区分大小写的字符串比较器。
总而言之,函数指针的主要用途就是实现策略模式。为了在java中实现这种模式,要声明一个接口来表示该策略,并且为每一个策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略类时设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。
嵌套类(nested class)是指被定义在另一个类的内部的类。嵌套类存在的目的应该是为它的外围类(enclosing class)提供服务。如果嵌套类将来可能会用于其他的某个环境中,它就应该是顶层类(top-level class)。嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和局部类(local class)。除了第一种之外,其他三种都被称为内部类(inner class)。
静态成员类是最简单的一种嵌套类。最好把它看作是普通的类,只是碰巧被声明在另一个类的内部而已,它可以访问外围类的所有成员,包括那些声明为私有的成员。静态成员类是外围类的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性规则。如果它被声明为私有的,它就只能在外围类的内部才可以被访问,等等。
静态成员类的一种常见用是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。
从语法上讲,静态成员类和非静态成员类之间唯一的区别是,静态成员类的声明中包含了修饰符static。尽管他们的语法非常的相似,但是这两种嵌套类有很大的不同。非静态成员类的每个示例都隐含着与外围类的一个外围实例(enclosing instance)相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用。如果嵌套类的实例可以在外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。
当非静态成员类的实例被创建的时候,它和外围实例之间的关联关系也随之建立起来;而且,这种关联关系以后也不能被修改。通常情况下,当在外围类的某个实例方法的内部调用了非静态成员类的构造器时,这种管理就自动建立起来。使用表达式enclosingInstance.new MemberClass(args)来手工建立这种关系也是有可能的,但是很少使用。正如你预料的那样,这种关联需要消耗非静态成员类的实例空间,并且增加了构造的时间开销。
非静态成员类常见用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关的类的实例。例如,Map接口的实现往往使用非静态成员类来实现它们的集合视图(collection view),这些集合视图是由Map的keySet、entrySet和Values方法返回的。同样地,诸如Set和List这种集合接口的实现往往也是用非静态成员类来实现他们的迭代器(iterator):
//Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E>
{
public Iterator<E> iterator()
{
return new MyIterator();
}
private class MyIterator implements Iterator<E>
{
}
}
如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的生命中,使它成为静态成员类,而不是非静态成员类。如果省略了static修饰符,则每个实例都将包含一个额外的指向外围对象的引用。保存这份引用要消耗时间和空间,并且会导致外围实例在符合垃圾回收时仍然得以保留。如果没有外围实例的情况下,也需要分配实例,就不能使用非静态成员类,因为非静态成员类的实例必须要有一个外围实例。
私有静态成员类的一种常见用法用来代表外围类所代表的对象的组件。
匿名类不同于Java程序设计语言中的其他任何语法单元。正如你所想象的,匿名类没有名字。它不是外围类的一个成员。它并不与其他的成员一起被声明,而是在使用的同时被声明和实例化。匿名类可以出现在代码中任何允许存在表达式的地方。当且仅当匿名类出现在非静态的环境中时,它才有外围实例。但是即使它们出现在静态的环境中,也不可能拥有任何静态成员。
匿名类的适用性受到诸多的限制。除了在它们被声明的时候之外,是无法将它们实例化的,你不能执行instanceof测试,或者做任何需要命名类的其他事情。你无法声明一个匿名类来实现多个接口,或者扩展一个类,并同时扩展类和实现接口。匿名类的客户端无法调用任何成员,除了从它的超类型中继承得到之外。由于匿名类出现在表达式当中,它们必须保持简短——大约10行或者更少些——否则会影响程序的可读性。
匿名类的一种常见用法就是动态的创建函数对象(function object,见21条)。例如,第21条中Arrays.sort方法调用,利用匿名的Comparator实例,根据一组字符串的长度对它们进行排序。匿名类的另一种常见的用法是创建过程对象(process object),比如Runnable、Thread或者TimerTask实例。第三种常见的用法是在静态工厂内部(参见第18条中国i部分的intArrayAsList方法)。
局部类是四种嵌套类中用的最少的类。在任何“可以声明局部变量”的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。局部类与其他三种嵌套类中的每一种都有一些共同的属性。与成员类一样,局部类有名字,可以被重复使用。与匿名类一样,只有当局部类实在非静态环境中定义的时候,才有外围实例,它们也不能包含静态成员。与匿名类一样,它们必须简短以便不会影响到可读性。
简而言之,共有四种不同的嵌套类,每一种都有自己的用途。如果一个嵌套类需要在单个方法之外仍然可见的,或者它太长了,不适合于放在方法内部,就应该使用成员类。如果成员类的每个示例都需要一个指向外围实例的引用,就要把成员类做成非静态的;否则,就做成静态的。假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类;否则,就做成局部类。
java1.5发行版本中增加了泛型。在没有泛型之前,从集合中读取到的每一个对象都必须进行装换。如果有人不小心插入了类型错误的对象,在运行时的装换处理器就会出错。有了泛型之后,可以告诉编译器每个集合中接受那些对象类型。编译器自动为你的插入进行转化,并在编译器告知是否插入了类型错误对象。这样可以使程序更加安全,也更加清楚,但是要享有这些优势有一点的难度。
定义
具有一个或者多个类型参数的类或者接口。
泛型类或者接口统称为泛型。
在java1.5版发行之前,没有泛型时,集合声明:
// Now a raw collection type - don’t do this!
/**
My stamp collection, Contains only Stamp instance.
*/
private final Collection stamps = ...;
如果不小心将一个coin放进了stamp集合中,这一错误的插入照样得以编译和运行并且不会出现任何错误提示:
如果不小心将一个coin放进了stamp集合中,这一错误的插入照样得以编译和运行并且不会出现任何错误提示:
// Erroneous insertion of coin into stamp collection
stamps.add(new Coin( ... ));
直到从stamp集合中获取coin时才会收到错误提示。
//Now a raw interator type - don't do this !
for (Iterator i = stamps.iterator(); i.hasNext();) {
Stamp s = (Stamp) i.next(); // Throws ClassCastException
... // Do something with the stamp
}
出错之后应该尽快发现,最好是编译时就发现。上述例子,直到运行时才发现错误,已经错误了很久。
有了泛型,就可以改进后的类型声明来代替集合中的这种注释,告诉编译器之前的注释中所隐含的信息:
// Parameterized collection type - typesafe
private final Collection<Stamp> stamps = ...;
通过这条声明,编译器直到stamps应该只包括Stamp实例,并给予保证,假设整个代码是利用java 1.5机器之后的编译器进行编译的,所有代码在编译国企成都没有发出任何警告。当stamps利用一个参数化的类型进行声明时,错误的插入会产生一条编译时的错误消息,准确地告诉你那里出错了。
还有一个好处是:从集合中取元素时不需要再进行手动的转换了,编译器会替你插入隐式的转换,并确保他们不会失败,无论你是否使用for-each循环,上述功能都适用。
// for-each loop over a parameterized collection - typesafe
for(Stamp s:stamps){
//No cast
// Do something with the stamp
或者无论是否使用传统的for循环也一样
// for loop with parameterized iterator declaration - typesafe
for(Iterator<Stamp> i=stamps.iterator();i.hasNext;{
Stamp s=i.next();//No cast necessary
//Do something with the stamp
原生态的List和参数化的类型List《Object》之间有什么区别呢?不严格的讲,前者逃避了泛型检查,后者明确告诉编译器,它能够持有任意类型的对象。虽然可以将List《String》传递给类型为List的参数,但是不能将它传给类型为List《Object》的参数。
如果使用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像List《Object》 这样的参数化类型,则不会。
// Use raw type (List) - fails at runtime!
public static void main(String[] args) {
List<String> strings = new ArrayList<String>();
unsafeAdd(strings, new Integer(42));
String s = strings.get(0); // Compiler - generated cast
}
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
这个程序可以运行,但是因为使用了原生态List,会收到警告。
如果在调用unsafeAdd声明中用参数化类型List代替原生态List,并试着重新编译这段程序,会发现它无法再进行编译。
//Use of raw type for unknow element type - don't do this
static int numElementsInCommon(Set s1,Set s2){
int result=0;
for(Object o1:s1){
if(s2.contains(o1)
return result++;
}
return result;
}
这个倒是可以,但它使用了原生态类型,这是很危险的。从java 1.5发行版本开始,java就提供了一种安全的替代方法,称作:无限制的通配符类型。
这个倒是可以,但它使用了原生态类型,这是很危险的。从java 1.5发行版本开始,java就提供了一种安全的替代方法,称作:无限制的通配符类型。
如果要使用泛型,但不确定或者不关心实际的类型参数,就可以使用一个问号代替。例:
static int numElementsInCommon(Set<?> s1,Set<?> s2){
int result=0;
for(Object o1:s1){
if(s2.contains(o1)
return result++;
}
return result;
}
Set与Set>区别:
后者是安全的,前者则不安全。
Set与Set《?》区别:
后者是安全的,前者则不安全。
总之,使用原生态类型在运行时会导致异常,因此不要再新代码中使用。
1、用泛型编程时,会遇到许多编译器警告:非受检强制转化警告(unchecked cast warning)、非受检方法调用警告、非受检普通数组创建警告、非受检转换警告。
2、许多非受检警告很容易消除,如:
Set<String> s = new HashSet<String>();
编译器提醒你
HashSet is a raw type. References to generic type HashSet should be parameterized
同时提供方法告诉你如何纠正。
Set<String> s = new HashSet<String>();
Set<String> s = new HashSet();
编译器提醒你 HashSet is a raw type. References to generic type HashSet< E> should be parameterized
同时提供方法告诉你如何纠正。
3、有些警告难以消除,如果消除了所有的警告,可以确保代码是类型安全的,
如果无法消除,但可以证明引起警告的代码是类型安全的,可以用一个@SuppressWarnings(“unchecked”)注解来禁止这条警告。
4、SuppressWarnings注解可以用在任何粒度的级别,应该始终在尽可能小的范围中使用该注解,否则可能会掩盖重要的警告。
5、在java.util.Arrays类中的一个方法,因为可以保证类型安全,所以用@SuppressWarnings(“unchecked”)注解禁止了警告,同时注解的作用范围是下面的一条语句。
public class testArray {
public static <T, U> T[] copyOf(U[] original, int newLength,
Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object) newType == (Object) Object[].class) ? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
}
数组与泛型集合相比,有两个重要的不同点。
首先,数组是协变的,相反,泛型则是不可变的。所谓的协变是指:如果Sub类是Super类的子类型,那么数组类型Sub[] 就是Super[]的子类型,也就是
说可以将Sub[]数组实例赋给Super[]数组类型变量。相反泛型是不可变的,那么List< Sub>与List< Super>则不存在子类型与超类型的关系。也就是说不能将ArrayList< Sub>实例赋给ArrayList类型的变量。
其次,数组是具体化的。因此数组会在运行时才知道并检查它们的元素类型约束。如果企图将String类型的对象保存在Long数组中,编译时并不会报错。
但是在运行会抛出ArrayStoreException异常。相比之下,泛型则是通过擦除来实现的。因此泛型只是在编译的时候强化它们的类型信息,并在运行时
丢弃它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。
由于上面的根本区别,因此数组和泛型集合不能很好的混合使用。如:new List< E>[] ,new List< String>[],new E[]都是不合法的。
考虑第6条的简单堆栈实现:
public class Stack {
pprivate Object[] elements;
private int size = 0;
private static final int DEFAULT_INITAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if(size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if(elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
这个类是泛型化的主要备选对象,换句话说,可以适当的强化这个类来利用泛型。根据实际情况来看,必须转换从堆栈中弹出的对象,以及可能在运行时失败的那些转换。将类泛型化的第一个步骤,就是给他的声明添加一个或者多个类型参数。在这个例子中有一个类型参数,它表示堆栈的元素类型,这个参数的名称通常为E。
下一步是用相应的类型参数替换所有的Object类型,然后试着编译最终的程序:
public class Stack {
pprivate E[] elements;
private int size = 0;
private static final int DEFAULT_INITAL_CAPACITY = 16;
public Stack() {
elements = new E[DEFAULT_INITAL_CAPACITY];//此处报错
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if(size == 0) {
throw new EmptyStackException();
}
E result = elements[--size];
elements[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if(elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
上面代码通常会出现一个错误或者警告,cannot create a generic array of E
解决方法:
1.直接绕开创建泛型数组的禁令,不创建泛型数组,创建一个Object数组,并将它转换成泛型数组类型,但是编译器仍然出现了一条警告。这种用法是合法的,但(整体上而言)不是类型安全的:
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
2.将elements域的类型从E[]改为Object[]:
private Object[] elements;
E result = (E) elements[--size];
产生一条警告,因为可以保证类型安全,所以所以可以用SupressWarning注释忽略掉该警告。
产生一条警告,因为可以保证类型安全,所以所以可以用SupressWarning注释忽略掉该警告。
具体选择哪种方法来处理泛型数组创建错误,则主要看个人的偏好了,所有其他的东西都一样,但是禁止数组类型的未受检转换比禁止标量类型的更加危险,所以建议采用第二种,但是在比Stack更实际的泛型类中,或许代码中会有多个地方需要从数组中读取元素,因此选择第二种方法需要多次转换成E,而不是只转换E[],,这也是第一种方法之所以更常用的原因。
总而言之,使用泛型比使用需要在客户端代码中进行类型转换的类型来的更加安全,也更加容易。在设计新类型的时候,更确保他们不需要这种类型转换就可以使用。这通常意味着要把类做成泛型的。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端。
就如类可以从泛型中受益一般,方法也一样。静态工具方法尤其适合于泛型化。
编写泛型方法与编写类型类型相类似。
例:他返回两个集合的联合:
// Users raw types - unaccepable!
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
这个方法可以编译,但是有两条警告:
Unioc.java:5:warning:[unchecked] unchecked call to
HastSet(Collection< ? extends E> as a member fo raw type HastSet
Set result = new HashSet(s1);
Unioc.java:5:warning:[unchecked] unchecked call to
addAll(Collection< ? extends E> as a member fo raw type HastSet
result.addAll(s2);
为了修正这些警告,使方法变成类型安全的,要将方法声名修改为声明一个类型参数,表示这三个元素类型(两个参数及一个返回值),并在方法中使用类型参数。声名类型参数的类型参数列表,处在方法的修饰符及其返回类型之间。在这个实例中,类型参数列表为< E>,返回类型为Set< E>。类型参数的命名惯例与泛型方法以及泛型的相同。
// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
至少对于简单的泛型方法而言,就是这么回事了。现在改方法编译时不会产生任何警告,并提供了类型安全性,也更容易使用。以下是一个执行该方法的简单程序。程序不包含装换,编译时不会有错误或者警告:
至少对于简单的泛型方法而言,就是这么回事了。现在改方法编译时不会产生任何警告,并提供了类型安全性,也更容易使用。以下是一个执行该方法的简单程序。程序不包含装换,编译时不会有错误或者警告:
//Simple program to exercise generic method
public static void main(String[] args){
Set<String> guys =new HashSet<String>{
Array.asList("Tom","Dick","Harry"));
Set<String> stooges =new HashSet<String>{
Array.asList("Larry","Moe","Curly"));
Set<String> aflCio=unioc(guys,stooges);
System.out.printle(aflCio);
}
}
运行这段程序是,会打印 [Moe,Harry,Tom,Curly,Larry,Dick]。 元素的顺序是依赖于实现的。
union方法局限在于,三个集合的类型(两个输入参数及一个返回值)必须全部相同。利用有限制的通配符类型可以使这个方法变得更回灵活。
泛型方法的一个显著特征是,无需明确指定类型参数的值,不像调用泛型构造器的时候是必须指定的。对于上述程序而言,编译器发现uniond的两个参数都是Set< String>类型,因此知道类型参数E必须为String,这个过程称作为类型推导。
如第一条所述,可以利用泛型方法调用所提供的类型推导,是创建参数化类型实例的过程变得更加轻松。提醒一下:在调用泛型构造器的时候,要明确传递类型参数的值可能有点麻烦。类型参数出现在了变量的声明的左右两边,显得冗余:
// Parameterized type instance creation with constructor
Map<String, List<String>> anagrams =
new HashMap<String, List<String>>();
为了消除冗余,可以编写一个泛型静态工厂方法,与想要使用的每个构造器相对应。例如,下面是一个无参的HashMap构造器相对应的泛型静态工厂方法:
// Generic static factory method
public static <K, V> HashMap<K, V> newHashMap() {
return new HashMap<K, V>();
}
通过这个泛型静态工厂方法,可以用下面这段简洁的大码来取代上面那个重复的声明:
//Parameterized type instance creation with static factory
Map<String,List<String>> anagrans=newHashMap();
相关的模式泛型单例工厂。有时,会需要创建不可变但又适合于许多不同类型的对象。由于泛型是通过擦除来实现的,可以给所有的必要的类型参数使用同一个单个对象,但是需要编写一个静态的工厂方法,重复地给每个必要的类型参数分发对象。这种模式叫做“泛型单例工厂”,这种模式最常用于函数对象。如Collections.reverseOrder,但也适用于像Collections.emptySet这样的集合。
相关的模式泛型单例工厂。有时,会需要创建不可变但又适合于许多不同类型的对象。由于泛型是通过擦除来实现的,可以给所有的必要的类型参数使用同一个单个对象,但是需要编写一个静态的工厂方法,重复地给每个必要的类型参数分发对象。这种模式叫做“泛型单例工厂”,这种模式最常用于函数对象。如Collections.reverseOrder,但也适用于像Collections.emptySet这样的集合。
假设有一个接口,描述了一个方法,该方法接受和返回某个类型T的值:
public interface UnaryFunction<T> {
T apply(T arg);
}
现在假设要提供一个恒等函数。如果 在每次需要的时候都重新创建一个,这样会很浪费,因为它是无状态的。如果泛型被具体化,每个类型都需要一个桓等函数,但是它们被擦除以后,就只需要一个泛型单例。请看以下示例:
// Generic singleton factory pattern
private static UnaryFunction<Object> INDENTITY_FUNCTION =
new UnaryFunction<Object> {
public Object apply(Object arg) {
return arg; }
};
// IDENTITY_FUNCTION is stateless and its type parameter is
// unbounded so it's safe to share one instance across all types.
@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> identityFunction() {
return (UnaryFunction<T>)INDENTITY_FUNCTION;
}
IDENTITY_FUNCTION装换成(UnaryFunction< T>),产生一条为受检的装换警告。因为UnaryFunction< Object>对于每个T来说并非额每个都是UnaryFunction< T>。但是恒等函数很特殊;他返回未被修改的参数,因此我们知道无论T的值是什么,用它作为UnaryFunction< T>都是类型安全的。因此:我们可以放心地禁止由这个装换所产生的未受检转换警告。一旦禁止,代码在编译时就不会出现任何错误或者警告。
以下是一个范例程序,利用泛型单例作为UnaryFunction< String>和UnaryFunction< Number>。像往常一样,它不包含,编译时没有出现错误或者警告:
//Simple program to exercise generic singleton
public static void main(String[] args){
String[] strings ={
"jute","hemp","nylon"};
UnaryFunction<String> sameString=identityFunction();
for(String s:strings)
System.out.printle(sameString.addly(s);
Number[] numbers={
1,2.0,3L};
UnaryFunction<Number> sameNumber=identityFunction();
for(Number n:numbers)
System.out.printle(sameNumber.addly(n);
}
虽然相对少见,但是通过某个包含该类型参数本身的表达式来限制类型参数是允许的。这就是递归类型限制。递归类型限制最普遍的用途与Comparable接口有关,它定义类型的自然顺序:
public interface Comparable<T>{
int compareTo(T o);
}
类型参数T定义的类型,可以与实现Comparable< T> 的类型的元素进行比较。实际上,几乎所有类型都只能与他们自身的类型的元素相比较。因此,例如String实现Comparable< String>,Integer实现Comparable< Integer>,等等。
许多方法都带有一个实现Comparable接口的元素列表,并在其中进行搜索,计算出它的最小值或者最大值,等等。要完成这其中的任何一项工作,要求列表中的每个元素都能够与列表中的其他元素相比较,换句话说,列表的元素可以相互比较。下面是如何表达这种约束条件的一个示例:
// Using a recursive type bound to express mutual comparability
public static <T extends Comparable<T>> T max(List<T> list) {
...
}
类型限制< T extends Comparable>,可以读作“针对可以与自身进行比较的每个类型T”,这与互比性的概念或多或少有一些一致。
类型限制< T extends Comparable>,可以读作“针对可以与自身进行比较的每个类型T”,这与互比性的概念或多或少有一些一致。
下面的方法就带有上述声明。它根据元素的自然顺序计算列表的最大值,编译时没有出现错误或者警告:
//Returns the maximun value in a list - uses recursive type bound
public static <T extends Comparable<T>> T max(List<T> list){
Iterator<T> i=list.iterator();
T result=i.next();
while(i.hasNext){
T t=i.next();
if(t.compareTo(result)>0)
result=t;
}
return result;
}
递归类型限制可能比这个要复杂得多,但幸运的是,这种情况并不经常发生。如果你理解这种习惯用法以及其通配符变量,就能够处理在实践中遇到的许多递归类型限制了。
递归类型限制可能比这个要复杂得多,但幸运的是,这种情况并不经常发生。如果你理解这种习惯用法以及其通配符变量,就能够处理在实践中遇到的许多递归类型限制了。
总之,泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来的更加安全,也更加容易。就像类型一样,你应该确保新的方法可以不用转换就能使用,这通常意味着要将它们泛型化。并且就像类型一样,还应该将现有的方法泛型化,使新用户使用起来更加轻松,且不会破坏现有的客户端。
枚举类型(enum type)是指由一组固定的常量组成合法值的类型,例如一年中的季节、太阳系中的行星或者一副牌中的花色。
在编程语言还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组具名的 int 常量,每个类型成员一个常量:
// The int enum pattern - severely deficient!
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
这种方法称作int枚举模式(int enum pattern),存在着诸多不足。它在类型安全性和使用方便性方面没有任何帮助。如果你将 apple 传到想要 orange 的方法中,编译器也不会出现警告,还会使用 == 操作符将 apple 与 orange 进行对比,甚至更糟糕:
int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;
注意每个 apple 常量的名称都以 APPLE_ 作为前缀,每个 orange 常量则都以 ORANGE_ 作为前缀。这是因为 Java 没有为 int 枚举组提供命名空间。当两个 int 枚举组具有相同的明明常量时,前缀可以防止名称发生冲突。
采用 int 枚举模式的程序是身份脆弱的。因为 int 枚举是编译时常量,被编译到使用它们的客户端中。如果域枚举常量关联的 int 发生了变化,客户端就必须重新编译。如果没有重新编译,程序还是可以运行,但是它们的行为就是不确定的。
将 int 枚举常量翻译成可打印的字符串,并没有很便利的方法。如果将这种常量打印出来,或者从调试器中将它显示出来,你所见到的就是一个数字,这没有太大的用处。要遍历一个组中的所有 int 枚举常量,甚至获得 int 枚举组的大小,这些都没有很可靠的方法。
你还可能碰到这种模式的变体,在这种模式中使用的是 String 常量,而不是 int 常量。这样的变体被称作 String 枚举模式,同样也是我们最不期望的。虽然它为这些常量提供了可打印的字符串,但是它会导致性能问题,因为它依赖于字符串的比较操作。更糟糕的是,它会导致初级用户把字符串创两硬编码到客户端代码中,而不是使用适当的域(field)名。如果这样的硬编码字符串常量中包含有书写错误,那么,这样的错误在编译时不会被检测到,但是在运行的时候却会报错。
一种可以替代的解决方案,可以避免 int 和 String 枚举模式的缺点,并提供许多额外的好处。这就是。下面以最简单的形式演示了这种模式:
public enum Apple {
FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange {
NAVEL, TEMPLE, BLOOD }
Java 枚举类型背后的基本想法非常简单:它们就是通过公有的静态 final 域为每个枚举常量导出实例的类。因为没有可以访问的构造器,枚举类型是真正的 final 。因为客户端既不能创建枚举类型的实例,也不能对它进行扩展,因此很可能没有实例,而只有声明过的枚举常量。换句话说,枚举类型是实例受控的。它们是单例(Singleton)的泛型化(见第3条),本质上是单元素的枚举。
枚举提供了编译时的类型安全。如果声明一个参数的类型为 Apple ,就可以保证,被传到该参数上的任何非 null 的对象引用一定属于三个有效的 Apple 值之一。试图传递类型错误的值时,会导致编译时错误,就像试图将某种枚举类型的表达式赋给另一种枚举类型的变量,或者试图利用 == 操作符比较不同枚举类型的值一样。
包含同名常量的多个枚举类型可以在一个系统中和平共处,因为每个类型都有自己的命名空间。你可以增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码,因为导出常量的域在枚举类型和它的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中,而是在 int 枚举模式中。最终,可以通过调用 toString 方法,将枚举转换成可打印的字符串。
枚举类型可以先作为枚举常量的一个简单集合,随着时间的推移再演变成为全功能的抽象。
下面这个枚举它们就是行星的质量和半径:
public enum Planet {
MERCURY(3.302e+23,2.439e6),
VENUS(3.302e+23,2.439e6),
EARTH(3.302e+23,2.439e6),
MARS(3.302e+23,2.439e6),
JUPITER(3.302e+23,2.439e6),
SATURN(3.302e+23,2.439e6),
URANUS(3.302e+23,2.439e6),
NEPTUNE(3.302e+23,2.439e6);
private final double mass;//值私有化 提供公有的提取方法。
private final double radius;
private final double surfaceGravity;
private final double G = 6.67300E-11;
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
this.surfaceGravity = G * mass/(radius * radius);
}
public double mass() {
return mass;
}
public double radius() {
return radius;
}
public double surfaceGravity() {
return surfaceGravity;
}
public double surfaceWeight(double mass){
return mass * surfaceGravity;
}
}
枚举 是指由一组固定的常量组成合法值得类型,int枚举和String枚举 不建议
枚举的目的:
通过公有的静态的final域 为每个枚举常量导出实例的类, 因为它没有访问的构造器,而且是final的,单例的泛型化。
为了将数据和枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器 如下:
劣势:与int常量相比,枚举在装载和初始化枚举时 会多一些 时间和空间成本。
优势:枚举 代码优雅易读,安全性高,功能强大。(每个常量与属性的关联)(提供行为受属性影响的方法)
如果多个常量共享相同的行为 可考虑策略枚举(枚举嵌套)如薪酬发放,各种加班行为的薪酬策略。
应用方式:每当需要 一组 固定常量的时候,如 菜单的选项,操作代码,命令行标记,系统编码等等。
特定于常量的方法实现有一个美中不足的地方,它们使得在枚举常量中共享代码变得更加困难了。
一般来说,枚举会优先使用 comparable 而非 int 常量。与 int 常量相比,枚举有个小小的性能缺点,即装在和初始化枚举时会有空间和时间的成本。
总而言之,与 int 常量相比,枚举类型的优势是不言而喻的。枚举要易读得多,也更加安全,功能更加强大。许多枚举都不需要显式的构造器或者成员,但许多其他枚举则受益于“每个常量与属性的关联”以及“提供行为受这个属性影响的方法”。只有极少数的枚举受益于将多种行为与单个方法关联。在这种相对少见的情况下,特定于常量的方法要优先于启用自有值的枚举。
许多枚举天生就与一个单独的 int 值相关联。所有的枚举都有一个 ordinal 方法,它返回每个枚举常量在类型中的数字位置。
你可以试着从序数中得到关联的 int 值:
// Abuse of ordinal to derive an associated value - DON'T DO THIS
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians(){
return ordinal() + 1; }
}
虽然这个枚举不错,但是维护起来就想一场恶梦。如果常量进行重新排序, numberOfMusicians 方法就会遭到破坏。如果要再添加一个与已经用过的 int 值关联的枚举常量,就没那么走运了。例如,给双四重奏(double quartet)添加一个常量,它就像个八重奏一样,是由8位演奏家组成,但是没有办法做到。
要是没有给所有这些 int 值添加常量,也无法给某个 int 值添加常量。例如,假设想要添加一个常量表示三四重奏(triple quartet),它由12位演奏家组成。对于由11位演奏家组成的合奏曲并没有标准的术语,因此只好给没有用过的 int 值(11)添加一个虚拟(dummy)常量。这么做顶多就是不太好看。如果有许多 int 值都是从未用过的,可就不切实际了。
幸运的是,有一种很简单的方法可以解决这些问题。永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中:
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) {
this.numberOfMusicians = size; }
public int numberOfMusicians(){
return numberOfMusicians; }
}
Enum 规范中谈到 ordinal 时这么写到:
“大多数程序员都不需要这个方法。它是设计成用于像 EnumSet 和 EnumMap 这种基于枚举的通用数据结构的。”
除非你在编写的是这种数据结构,否则最好完全避免使用 ordinal 方法。
如果枚举类型的元素主要用于集合中,一般来说使用int枚举模式(条目 34),下面将2的不同倍数赋值给每个常量:
// Bit field enumeration constants - OBSOLETE!
public class Text {
public static final int STYLE_BOLD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public static final int STYLE_UNDERLINE = 1 << 2; // 4
public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
// Parameter is bitwise OR of zero or more STYLE_ constants
public void applyStyles(int styles) {
... }
}
这种表示方式允许你使用按位或(or)运算将几个常量合并到一个称为位属性(bit field)的集合中:
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
位属性表示还允许你使用按位算术有效地执行集合运算,如并集和交集。 但是位属性具有int枚举常量等的所有缺点。 当打印为数字时,解释位属性比简单的int枚举常量更难理解。 没有简单的方法遍历所有由位属性表示的元素。 最后,必须预测在编写API时需要的最大位数,并相应地为位属性(通常为int或long)选择一种类型。 一旦你选择了一个类型,你就不能超过它的宽度(32或64位)而不改变API。
一些程序员使用枚举优于int常量,当他们需要传递常量集合时仍然使用位属性。 没有理由这样做,因为存在更好的选择。 java.util包提供了EnumSet类来有效地表示从单个枚举类型中提取的值集合。 这个类实现了Set接口,提供了所有其他Set实现的丰富性,类型安全性和互操作性。 但是在内部,每个EnumSet都表示为一个位矢量(bit vector)。 如果底层的枚举类型有64个或更少的元素,并且大多数情况下,整个EnumSet用单个long表示,所以它的性能与位属性的性能相当。 批量操作(如removeAll和retainAll)是使用按位算术实现的,就像你为位属性手动操作一样。 但是完全避免了手动位混乱的丑陋和错误倾向:EnumSet为你做了很大的努力。
下面是前一个使用枚举和枚举集合替代位属性的示例。 它更短,更清晰,更安全:
// EnumSet - a modern replacement for bit fields
public class Text {
public enum Style {
BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set<Style> styles) {
... }
}
这里是将EnumSet实例传递给applyStyles方法的客户端代码。 EnumSet类提供了一组丰富的静态工厂,可以轻松创建集合,其中一个代码如下所示:
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
请注意,applyStyles方法采用Set< Style >而不是EnumSet< Style >参数。 尽管所有客户端都可能会将EnumSet传递给该方法,但接受接口类型而不是实现类型通常是很好的做法(条目 64)。 这允许一个不寻常的客户端通过其他Set实现的可能性。
总之,仅仅因为枚举类型将被用于集合中,所以没有理由用位属性来表示它。 EnumSet类将位属性的简洁性和性能与条目 34中所述的枚举类型的所有优点相结合。EnumSet的一个真正缺点是,它不像Java 9那样创建一个不可变的EnumSet,但是在即将发布的版本中可能会得到补救。 同时,你可以用Collections.unmodifiableSet封装一个EnumSet,但是简洁性和性能会受到影响。
大多数方法和构造方法对可以将哪些值传递到其对应参数中有一些限制。 例如,索引值必须是非负数,对象引用必须为非null。 你应该清楚地在文档中记载所有这些限制,并在方法主体的开头用检查来强制执行。 应该尝试在错误发生后尽快检测到错误,这是一般原则的特殊情况。 如果不这样做,则不太可能检测到错误,并且一旦检测到错误就更难确定错误的来源。
如果将无效参数值传递给方法,并且该方法在执行之前检查其参数,则它抛出适当的异常然后快速且清楚地以失败结束。 如果该方法无法检查其参数,可能会发生一些事情。 在处理过程中,该方法可能会出现令人困惑的异常。 更糟糕的是,该方法可以正常返回,但默默地计算错误的结果。 最糟糕的是,该方法可以正常返回但是将某个对象置于受损状态,在将来某个未确定的时间在代码中的某些不相关点处导致错误。 换句话说,验证参数失败可能导致违反故障原子性(failure atomicity )(条目 76)。
对于公共方法和受保护方法,请使用Java文档@throws注解来记在在违反参数值限制时将引发的异常(条目 74)。 通常,生成的异常是IllegalArgumentException,IndexOutOfBoundsException或NullPointerException(条目 72)。 一旦记录了对方法参数的限制,并且记录了违反这些限制时将引发的异常,那么强制执行这些限制就很简单了。 这是一个典型的例子:
/**
* Returns a BigInteger whose value is (this mod m). This method
* differs from the remainder method in that it always returns a
* non-negative BigInteger.
*
* @param m the modulus, which must be positive
* @return this mod m
* @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0)
throw new ArithmeticException("Modulus <= 0: " + m);
... // Do the computation
}
请注意,文档注释没有说“如果m为null,mod抛出NullPointerException”,尽管该方法正是这样做的,这是调用m.sgn()的副产品。这个异常记载在类级别文档注释中,用于包含的BigInteger类。类级别的注释应用于类的所有公共方法中的所有参数。这是避免在每个方法上分别记录每个NullPointerException的好方法。它可以与@Nullable或类似的注释结合使用,以表明某个特定参数可能为空,但这种做法不是标准的,为此使用了多个注解。
在Java 7中添加的Objects.requireNonNull方法灵活方便,因此没有理由再手动执行空值检查。 如果愿意,可以指定自定义异常详细消息。 该方法返回其输入的值,因此可以在使用值的同时执行空检查:
// Inline use of Java's null-checking facility
this.strategy = Objects.requireNonNull(strategy, "strategy");
你也可以忽略返回值,并使用Objects.requireNonNull作为满足需求的独立空值检查。
在Java 9中,java.util.Objects类中添加了范围检查工具。 此工具包含三个方法:checkFromIndexSize,checkFromToIndex和checkIndex。 此工具不如空检查方法灵活。 它不允许指定自己的异常详细消息,它仅用于列表和数组索引。 它不处理闭合范围(包含两个端点)。 但如果它能满足你的需要,那就很方便了。
对于未导出的方法,作为包的作者,控制调用方法的环境,这样就可以并且应该确保只传入有效的参数值。因此,非公共方法可以使用断言检查其参数,如下所示:
// Private helper function for a recursive sort
private static void sort(long a[], int offset, int length) {
assert a != null;
assert offset >= 0 && offset <= a.length;
assert length >= 0 && length <= a.length - offset;
... // Do the computation
}
本质上,这些断言声称断言条件将成立,无论其客户端如何使用封闭包。与普通的有效性检查不同,断言如果失败会抛出AssertionError。与普通的有效性检查不同的是,除非使用-ea(或者-enableassertions)标记传递给java命令来启用它们,否则它们不会产生任何效果,本质上也不会产生任何成本。有关断言的更多信息,请参阅教程assert。
检查方法中未使用但存储以供以后使用的参数的有效性尤为重要。例如,考虑第101页上的静态工厂方法,它接受一个int数组并返回数组的List视图。如果客户端传入null,该方法将抛出NullPointerException,因为该方法具有显式检查(调用Objects.requireNonNull方法)。如果省略了该检查,则该方法将返回对新创建的List实例的引用,该实例将在客户端尝试使用它时立即抛出NullPointerException。 到那时,List实例的来源可能很难确定,这可能会使调试任务大大复杂化。
构造方法是这个原则的一个特例,你应该检查要存储起来供以后使用的参数的有效性。检查构造方法参数的有效性对于防止构造对象违反类不变性(class invariants)非常重要。
你应该在执行计算之前显式检查方法的参数,但这一规则也有例外。 一个重要的例外是有效性检查昂贵或不切实际的情况,并且在进行计算的过程中隐式执行检查。 例如,考虑一种对对象列表进行排序的方法,例如Collections.sort(List)。 列表中的所有对象必须是可相互比较的。 在对列表进行排序的过程中,列表中的每个对象都将与其他对象进行比较。 如果对象不可相互比较,则某些比较操作抛出ClassCastException异常,这正是sort方法应该执行的操作。 因此,提前检查列表中的元素是否具有可比性是没有意义的。 但请注意,不加选择地依赖隐式有效性检查会导致失败原子性( failure atomicity)的丢失(条目 76)。
有时,计算会隐式执行必需的有效性检查,但如果检查失败则会抛出错误的异常。 换句话说,计算由于无效参数值而自然抛出的异常与文档记录方法抛出的异常不匹配。 在这些情况下,你应该使用条目 73中描述的异常翻译( exception translation)习惯用法将自然异常转换为正确的异常。
不要从本条目中推断出对参数的任意限制都是一件好事。 相反,你应该设计一些方法,使其尽可能通用。 假设方法可以对它接受的所有参数值做一些合理的操作,那么对参数的限制越少越好。 但是,通常情况下,某些限制是正在实现的抽象所固有的。
总而言之,每次编写方法或构造方法时,都应该考虑对其参数存在哪些限制。 应该记在这些限制,并在方法体的开头使用显式检查来强制执行这些限制。 养成这样做的习惯很重要。 在第一次有效性检查失败时,它所需要的少量工作将会得到对应的回报。
愉快使用Java的原因,它是一种安全的语言(safe language)。 这意味着在缺少本地方法(native methods)的情况下,它不受缓冲区溢出,数组溢出,野指针以及其他困扰C和C ++等不安全语言的内存损坏错误的影响。 在一种安全的语言中,无论系统的任何其他部分发生什么,都可以编写类并确切地知道它们的不变量会保持不变。 在将所有内存视为一个巨大数组的语言中,这是不可能的。
即使在一种安全的语言中,如果不付出一些努力,也不会与其他类隔离。必须防御性地编写程序,假定类的客户端尽力摧毁类其不变量。随着人们更加努力地试图破坏系统的安全性,这种情况变得越来越真实,但更常见的是,你的类将不得不处理由于善意得程序员诚实错误而导致的意外行为。不管怎样,花时间编写在客户端行为不佳的情况下仍然保持健壮的类是值得的。
如果没有对象的帮助,另一个类是不可能修改对象的内部状态的,但是在无意的情况下提供这样的帮助却非常地容易。例如,考虑以下类,表示一个不可变的时间期间:
// Broken "immutable" time period class
public final class Period {
private final Date start;
private final Date end;
/**
* @param start the beginning of the period
* @param end the end of the period; must not precede start
* @throws IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(
start + " after " + end);
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
... // Remainder omitted
}
乍一看,这个类似乎是不可变的,并强制执行不变式,即period实例的开始时间并不在结束时间之后。然而,利用Date类是可变的这一事实很容易违反这个不变式:
// Attack the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!
从Java 8开始,解决此问题的显而易见的方法是使用Instant(或LocalDateTime或ZonedDateTime)代替Date,因为Instant和其他java.time包下的类是不可变的(条目17)。Date已过时,不应再在新代码中使用。 也就是说,问题仍然存在:有时必须在API和内部表示中使用可变值类型,本条目中讨论的技术也适用于这些时间。
为了保护Period实例的内部不受这种攻击,必须将每个可变参数的防御性拷贝应用到构造方法中,并将拷贝用作Period实例的组件,以替代原始实例:
// Repaired constructor - makes defensive copies of parameters
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(
this.start + " after " + this.end);
}
有了新的构造方法后,前面的攻击将不会对Period实例产生影响。注意,防御性拷贝是在检查参数(条目49)的有效性之前进行的,有效性检查是在拷贝上而不是在原始实例上进行的。虽然这看起来不自然,但却是必要的。它在检查参数和拷贝参数之间的漏洞窗口期间保护类不受其他线程对参数的更改的影响。在计算机安全社区中,这称为 time-of-check/time-of-use或TOCTOU攻击[ Viega01 ]。
还请注意,我们没有使用Date的clone方法来创建防御性拷贝。因为Date是非final的,所以clone方法不能保证返回类为java.util.Date的对象,它可以返回一个不受信任的子类的实例,这个子类是专门为恶意破坏而设计的。例如,这样的子类可以在创建时在私有静态列表中记录对每个实例的引用,并允许攻击者访问该列表。这将使攻击者可以自由控制所有实例。为了防止这类攻击,不要使用clone方法对其类型可由不可信任子类化的参数进行防御性拷贝。
虽然替换构造方法成功地抵御了先前的攻击,但是仍然可以对Period实例进行修改,因为它的访问器提供了对其可变内部结构的访问:
// Second attack on the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!
为了抵御第二次攻击,只需修改访问器以返回可变内部字属性的防御性拷贝:
// Repaired accessors - make defensive copies of internal fields
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
使用新的构造方法和新的访问器,Period是真正不可变的。 无论程序员多么恶意或不称职,根本没有办法违反一个period实例的开头不跟随其结束的不变量(不使用诸如本地方法和反射之类的语言外方法)。 这是正确的,因为除了period本身之外的任何类都无法访问period实例中的任何可变属性。 这些属性真正封装在对象中。
在访问器中,与构造方法不同,允许使用clone方法来制作防御性拷贝。 这是因为我们知道Period的内部Date对象的类是java.util.Date,而不是一些不受信任的子类。 也就是说,由于条目13中列出的原因,通常最好使用构造方法或静态工厂来拷贝实例。
参数的防御性拷贝不仅仅适用于不可变类。 每次编写在内部数据结构中存储对客户端提供的对象的引用的方法或构造函数时,请考虑客户端提供的对象是否可能是可变的。 如果是,请考虑在将对象输入数据结构后,你的类是否可以容忍对象的更改。 如果答案是否定的,则必须防御性地拷贝对象,并将拷贝输入到数据结构中,以替代原始数据结构。 例如,如果你正在考虑使用客户端提供的对象引用作为内部set实例中的元素或作为内部map实例中的键,您应该意识到如果对象被修改后插入,对象的set或map的不变量将被破坏。
在将内部组件返回给客户端之前进行防御性拷贝也是如此。无论你的类是否是不可变的,在返回对可拜年的内部组件的引用之前,都应该三思。可能的情况是,应该返回一个防御性拷贝。记住,非零长度数组总是可变的。因此,在将内部数组返回给客户端之前,应该始终对其进行防御性拷贝。或者,可以返回数组的不可变视图。这两项技术都记载于条目15。
可以说,所有这些的真正教训是,在可能的情况下,应该使用不可变对象作为对象的组件,这样就不必担心防御性拷贝(条目17)。在我们的Period示例中,使用Instant(或LocalDateTime或ZonedDateTime),除非使用的是Java 8之前的版本。如果使用的是较早的版本,则一个选项是存储Date.getTime()返回的基本类型long来代替Date引用。
可能存在与防御性拷贝相关的性能损失,并且它并不总是合理的。如果一个类信任它的调用者不修改内部组件,也许是因为这个类和它的客户端都是同一个包的一部分,那么它可能不需要防御性的拷贝。在这些情况下,类文档应该明确指出调用者不能修改受影响的参数或返回值。
即使跨越包边界,在将可变参数集成到对象之前对其进行防御性拷贝也并不总是合适的。有些方法和构造方法的调用指示参数引用的对象的显式切换。当调用这样的方法时,客户端承诺不再直接修改对象。希望获得客户端提供的可变对象的所有权的方法或构造方法必须在其文档中明确说明这一点。
包含方法或构造方法的类,这些方法或构造方法的调用指示控制权的转移,这些类无法防御恶意客户端。 只有当一个类和它的客户之间存在相互信任,或者当对类的不变量造成损害时,除了客户之外,任何人都不会受到损害。 后一种情况的一个例子是包装类模式(第18项)。 根据包装类的性质,客户端可以通过在包装后直接访问对象来破坏类的不变性,但这通常只会损害客户端。
总之,如果一个类有从它的客户端获取或返回的可变组件,那么这个类必须防御性地拷贝这些组件。如果拷贝的成本太高,并且类信任它的客户端不会不适当地修改组件,则可以用文档替换防御性拷贝,该文档概述了客户端不得修改受影响组件的责任。
先看一个使用重载错误的例子:
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> lst) {
return "List";
}
public static String classify(Collection<?> c) {
return "Unknown Collection";
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};
for (Collection<?> c : collections)
System.out.println(classify(c));
}
}
我们希望打出的是,set,list,Unknown Collection。实际上,它的输出是:
Unknown Collection
Unknown Collection
Unknown Collection
首先,classify方法在这里被重载了是肯定的,事实上要调用哪个方法进行重载,在编译时就已经做出决定了。循环中的三个类,编译器都认为是Collection>类,所以输出的是三个Unknown Collection。
在这里,把函数的重载和多态混淆了。方法的覆盖用来实现多态,这才是动态的,在运行时选择被覆盖的方法。
下面是一个多态的典型正确例子,在调用被覆盖方法的时候,编译的类型不会有影响,根据new的类型的方法将会被执行:
class Wine {
String name() {
return "wine"; }
}
class SparklingWine extends Wine {
@Override String name() {
return "sparkling wine"; }
}
class Champagne extends SparklingWine {
@Override String name() {
return "champagne"; }
}
public class Overriding {
public static void main(String[] args) {
Wine[] wines = {
new Wine(), new SparklingWine(), new Champagne()
};
for (Wine wine : wines)
System.out.println(wine.name());
}
}
如果想修正第一个例子,可以在classify的内部使用instanceof做一个显示的测试,来得到结果:
public static String classify(Collection<?> c) {
return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknown Collection";
}
如果对于API来说,普通用户根本不知道对于一组给定的参数,到底会重载哪个函数的时候,那么这样的API就很容易出错。而且这类错误只有等到程序出现非常怪异的行为的时候才能被发现,而且不容易诊断错误。因此,尽量避免胡乱使用重载。
使用重载的原则
永远不要导出两个具有相同参数数目的重载方法。尽量使重载方法的参数数量不同;对于使用的可变参数,最好不要重载。
如果一定要重载,那么对于一对重载方法,至少要有一个对应的参数在两个重载方法中的类型“完全不同”。这样一来,就不可以把一种实例转换为另一种实例,相对来说是保守安全的。
针对上面第2条,给出一个典型的错误例子:
public class SetList {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<Integer>();
List<Integer> list = new ArrayList<Integer>();
for (int i = -3; i < 3; i++) {
set.add(i);
list.add(i);
}
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(i);
}
System.out.println(set + " " + list);
}
}
可以看到,在第一个循环中,依次加入了-3,-2,-1,0,1,2,在第二个循环中,试着删除0,1,2,希望留下-3,-2,-1。可是输出结果是:
[-3, -2, -1] [-2, 0, 2]
Set的输出结果如同我们想的一样,但是List的结果不一样。实际发生的情况是:set.remove(E),选择重载方法的参数实际上是Integer,这里进行了自动装箱,把int装箱成了Integer;对于List,有两个重载函数,这里直接重载了list.remove(i),并没有重载到list.remove(E),是从list的指定位置进行remove,所以先移除了第0个,也就是-3,list中所有元素前移;再移除第1个,也就是list中当前第2个,也就是-1;以此类推,最后得到-2,0,2。我们可以在源码中看到:
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
/**
* Removes the first occurrence of the specified element from this list,
* if it is present. If the list does not contain the element, it is
* unchanged. More formally, removes the element with the lowest index
* i such that
* (o==null ? get(i)==null : o.equals(get(i)))
* (if such an element exists). Returns true if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @param o element to be removed from this list, if present
* @return true if this list contained the specified element
*/
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
因此,如果想修正上面的代码,我们可以写成:
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(Integer.valueOf(i));
}
总结
实际情况往往比上面分析的复杂的多,确定选择哪个重载方法的规则也极其困难。有时候,新增的API可能会违背上面的规则,但是,只要重载的方法执行相同的功能,返回相同的结果,这样也是可以接受的。确保这种行为的标准做法是,让更具体化的重载方法把调用转发给更一般的重载方法去做。
总之,能够重载并不意味者应该重载,一般来说,对于多个具有相同参数数目的重载方法,还是尽量避免使用重载。另外一些情况下,重载方法尽量把调用转发给一般的重载方法去做,不同的重载方法尽量保证行为一致。
Java 1.5增加可变参数方法,可变参数方法接受0个或者多个指定类型的参数。
可变参数的机制是通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法
static int sum(int... args) {
int sum=0;
for(int arg : args)
sum += arg;
return sum;
}
但是,不传参也是可以的,这样容易导致错误的出现,所以常见的策略是首先指定正常参数,把可选参数放在后面。
//可变参数必须放在参数列表的最后
static int min(int firstArg, int... remainingArgs) {
int min = firstArg;
for(int arg : remainingArgs)
if(arg < min)
min = arg;
return min;
}
滥用参数列表的例子:
jdk1.5以前:Array.asList是这样的
public static List< Object > asList(Object[]obj) {
return new ArrayList< Object >(obj);
}
我们想要打印对象引用数组一般这样用Arrays.asList(),因为直接调用数组的toString()会产生没有意义的结果,如下:
String[]strings = {
"12","23","34","56"};
System.out.println(Arrays.asList(strings));//[12, 23, 34, 56]
System.out.println(strings.toString());//[Ljava.lang.String;@3d4eac69
所以当
int[]ints = {
12,23,34,56};
System.out.println(Arrays.asList(ints));
会发生编译错误Object[] can’t apply to int[]
jdk1.5以后,Arrays.asList变成了可变参数
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
这样也就相当于允许了Array.asList(基本参数类型数组)
产生下面的结果
int[]ints = {
12,23,34,56};
System.out.println(Arrays.asList(ints));//[[I@42a57993]
System.out.println(ints.toString());//[I@42a57993123]
产生令人遗憾的结果。这就解释为什么1.5在Arrys类中增加toString(long[ ]),toString(int[ ]),toString(short[ ]),toString(char[ ]),toString(byte[ ]),toString(boolean[ ]),toString(float[ ]),toString(double[ ]),toString(Object[]) 来提供打印数组功能。
在重视性能的情况下,使用可变参数机制要小心,因为可变参数方法的每次调用都会导致进行一次数组分配和初始化,有一种折中的解决方案,假设确定某个方法大部分调用会有3个或者更少的参数,就声明该方法的5个重载,每个重载带有0至3个普通参数,当参数数目超过3个时,使用可变参数方法。
public void foo() {
}
public void foo() {
int a1}
public void foo() {
int a1, int a2}
public void foo() {
int a1, int a2, int a3}
public void foo() {
int a1, int a2, int a3, int... rest}
@throws
@param
@return
/**
* Returns the element at the specified position in this list.
*
* This method is not guaranteed to run in constant
* time. In some implementations it may run in time proportional
* to the element position.
*
* @param index index of element to return; must be
* non-negative and less than the size of this list
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= this.size()})
*/
E get(int index);
使用 StringBuilder
核心反射机制java.lang.reflect提供了“通过程序来访问关于已装载的类的信息”的能力,给定一个Class实例,可以获得Constructor、Method、Field实例,这些对象提供“通过程序来访问类的成员名称、域类型、方法签名等信息”的能力。
反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在,存在的代价:
1.失去编译时类型检查的好处,包括异常检查。
2.执行反射访问所需的代码很长。
3.性能上的损失。
反射机制的使用场景
反射功能只是在设计时被用到,通常,普通应用程序在运行时不应该以反射的方式访问对象。
有些复杂的应用程序需要使用反射机制,包括类浏览器、对象检测器、代码分析工具、解释型的内嵌式系统。在RPC中使用反射机制也是合适的,这样就不再需要存根编译器。
对于有些程序,必须用到在编译时无法获取的类,但是在编译时存在适当的接口或者超类,通过它们可以引用这个类,就可以以反射的方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例。
Java Native Interface(JNI)允许Java程序调用本地方法(native methods),这些方法是用本地编程语言(如C或C ++)编写的方法。 从历史上看,本地方法有三个主要用途。 它们提供对特定于平台设备(如注册表)的访问。 它们提供对现有本地代码库的访问,包括提供对遗留数据库的数据访问。 最后,本地方法用于以本地语言编写应用程序的性能关键部分,以提高性能。
现在已经不再建议使用本地方法来提高性能。 在早期版本(Java 3之前)中,它通常是必需的,但从那时起JVM就变得更快了。 对于大多数任务,现在可以在Java中获得可比较的性能。 例如,当在Java版本1.1中添加java.math时,BigInteger依赖于用C语言编写的一个快速多精度算术库。在Java 3中,BigInteger在Java中重新实现,并仔细调整到比原始本地实现运行得更快的程度。
这个故事的一个可悲的结尾是,除了在Java 8中对大数进行更快的乘法运算之外,BigInteger此后几乎没有发生什么变化。在此期间,对本地类库的工作继续快速进行,尤其是GNU多精度算术类库(GMP)。如果需要真正高性能多精度算法,Java程序员现在可以通过本地方法使用GMP [ Blum14 ]。
使用本地方法具有严重的缺点。 由于本地语言不安全(条目50),使用本地方法的应用程序不再免受内存损坏错误的影响。 由于本地语言比Java更依赖于平台,因此使用本地方法的程序不太可移植。 它们也更难调试。 如果使用不当,本地方法可能会降低性能,因为垃圾收集器无法自动甚至跟踪本地内存使用情况(条目 8),并且存在进入和退出本地代码相关的成本。 最后,本地方法需要“粘合代码”,难以阅读,编写还繁琐。
总之,在使用本地方法之前要三思而后行。 很少需要使用它们来提高性能。 如果必须使用本地方法来访问地城资源或本地类库,请尽可能少地使用本地代码,并对其进行彻底测试。 本地代码中的单个错误可能会破坏整个应用程序。
关于优化有三个格言,每个人都应该知道:
更多的计算上的过失是以效率的名义(不一定实现它)而不是任何其他单一原因——包括盲目做愚蠢的事情。
——William A. Wulf [ Wulf72]
我们应该不去计较小小的效率,大约97%时间里:过早的优化是所有问题的根源。
———Donald E. Knuth [ Knuth74]
在优化方面,我们遵循两条规则:
规则1。不要优化。
规则2(只适用于专家)。先不要优化——也就是说,直到你有了一个完全清晰的还未优化的解决方案之前,不要优化。
所有这些格言都比Java编程语言的出现早二十年。 他们讲述了优化的深层真理:特别是如你过早优化的话,弊大于利。 在此过程中,可能会生成既不快,又不正确,且无法轻松修复的软件。
不要为了性能而牺牲合理的架构原则。努力编写好的程序,而不是快的程序。如果一个好的程序不够快,它的架构允许对其进行优化。好的程序体现了信息隐藏的原则:在可能的情况下,他们设计决策本地化为单个组件,因此可以在不影响系统其余部分的情况下更改单个决策(条目15)。
这并不意味着可以在程序完成之前忽略性能问题。 实现问题可以通过以后的优化来解决,但是如果不重写系统,就无法修复限制性能的普遍存在的架构缺陷。 事后改变设计的基本方面可能导致结构不良的系统难以维护和发展。 因此,必须在设计过程中考虑性能。
尽量避免限制性能的设计决策。设计中最难以更改的组件是那些指定组件之间以及与外部系统的交互的组件。这些设计组件中最主要的是API、线路层(wire-level)协议和持久化数据格式。这些设计组件不仅难以或不可能在事后更改,而且所有这些组件都可能对系统能够达到的性能造成重大限制。
考虑API设计决策的性能影响。 使公共类型可变可能需要大量不必要的防御性拷贝(条目 50)。 类似地,在一个公共类中应该使用复用更为合适,但依旧使用继承会把该类永远绑定到它的父类,这会人为地限制子类的性能(第18项)。最后一个例子是,在API中使用实现类型而不是接口会把你绑定到特定的实现,即使将来可能会编写更快的实现(条目 64)。
API设计对性能的影响是非常真实存在的。 考虑java.awt.Component类中的getSize方法。 这个性能关键方法决定,是返回Dimension实例,而且Dimension实例是可变的,强制此方法的任何实现都在每次调用时分配一个新的Dimension实例。 尽管在现代VM上分配小对象的成本很低,但是不必要地分配数百万个对象会对性能造成实际损害。
存在几种API设计替代方案。 理想情况下,Dimension应该是不可变的(条目 17); 或者,getSize可能已被两个返回Dimension对象的各个基本组件的方法所代替。 实际上,出于性能原因,在Java 2中将两个这样的方法添加到Component类中。 但是,预先存在的客户端代码仍然使用getSize方法,并且仍然会受到原始API设计决策的性能影响。
幸运的是,通常情况下,好的API设计与好的性能是一致的。为了获得良好的性能而包装API是一个非常糟糕的想法。导致包装API的性能问题可能在平台或其他底层软件的未来发型版本中消失,但是包装API和随之而来的支持问题将永远伴随着你。
一旦仔细设计了程序并生成了清晰,简洁且结构良好的实现,那么可能是时候考虑优化,假设你对程序的性能还不是不满意。
回想一下Jackson的两条优化规则是“不要优化”和“(只针对专家)还是先别优化”。他本可以再加上一条:在每次尝试优化之前和优化之后,要测量性能。你可能会对自己发现感到惊讶。通常,尝试的优化对性能没有可测量的影响;有时候,他们让事情变得更糟。主要原因是很难猜测程序将时间花在哪里。程序中你认为很慢的部分可能并没有错,在这种情况下,浪费时间来优化它。一般认为,程序将90%的时间花在10%的代码上。
性能分析工具可以帮助你决定将优化工作的重点放在哪里。这些工具提供了运行时信息,比如每个方法大约花费多少时间以及调用了多少次。除了关注调优工作之外,还可以提醒你需要进行算法更改。如果程序中潜藏着平方级(或更糟)算法,那么再多的调优也无法解决这个问题。必须用一个更有效的算法来代替这个算法。系统中的代码越多,使用分析工具就越重要。这就像大海捞针:大海捞针越大,金属探测器就越有用。另一个值得特别提及的工具是jmh,它不是一个分析工具,而是一个微基准测试框架,提供了非并行的可见对Java代码的详细性能 [JMH]。
与C和C++等更传统的语言相比,Java甚至更需要度量尝试优化的效果,因为Java的性能模型很弱:各种基本操作的相对成本没有得到很好的定义。程序员编写的内容和CPU执行的内容之间的“抽象鸿沟(abstraction gap)”更大,这使得可靠地预测优化的性能结果变得更加困难。有很关于性能的说法流传开来,但最终被证明是半真半假或彻头彻尾的谎言。
Java的性能模型不仅定义不清,而且在不同的实现之间、不同的发布之间、不同的处理器之间都有所不同。如果要在多个实现或多个硬件平台上运行程序,那么度量优化对每个平台的效果是很重要的。有时候,可能会被迫在不同实现或硬件平台上的性能之间进行权衡。
自本条目首次编写以来的近20年里,Java软件堆栈的每个组件都变得越来越复杂,从处理器到不同的虚拟机再到类库,Java运行的各种硬件都有了极大的增长。所有这些加在一起,使得Java程序的性能比2001年更难以预测,而对它进行度量的需求也相应增加。
总而言之,不要努力写出快速的程序——努力写出好的程序; 这样速度将随之而来。 但是在设计系统时要考虑性能,尤其是在设计API,线级协议和持久化数据格式时。 完成系统构建后,请测量其性能。 如果它足够快,你就完成了。 如果没有,请借助分析工具找到问题的根源,然后开始优化系统的相关部分。 第一步是检查算法选择:再多低级优化也不可以弥补差的算法选择。 根据需要重复此过程,在每次更改后测量性能,直到满意为止。
不严格的讲,这些命名惯例分为两大类:字面的和语法的。
字面的命名惯例比较少,但也涉及包,类,方法,域和类型变量。
包的名称应该是层次状的,用句号分隔每个部分。 任何将在你的组织之外使用的包,其名称都应该以你的组织的Internet域名开头,并且将顶级域名放在前面,例如edu.cmu , com.sun ,gov.nsa。标准类库和一些可选的包,其名称以java和javax开头,这属于这一规则的例外。
类和接口的名称,包括枚举和注解类型的名称,都应该包括一个或者多个单词,每个单词的首字母大写,例如Timer和TimerTask。 应该尽量避免用缩写,除非是一些首字母缩写和一些通用的缩写,比如max和min。
方法和域的名称与类和接口的名称一样,都遵守相同的字面惯例,只不过方法或者域的名称的第一个字母应该小写,例如remove,ensureCapacity。
常量域则全是大写,它的名称应该包含一个或者多个大写的单词,中间用下划线相连,例如VALUES,NEGATIVE_INFINITY。 常量域是静态的final域,它的值是不可变的。
类型参数名称通常由单个字母组成。 这个字母通常是以下五种类型之一:
T代表任意的类型 (泛型), E表示集合的元素类型 (集合应用) , K和V表示映射的键和值的类型 (map应用), X表示异常的类型。
特别注意: boolean 类型 尽量避免is 开头,否则部分框架解析会引起序列化错误.定义为基本数据类型boolean isFlag;的属性,它的方法也是isFlag(),RPC进而抛出异常。Mybatis框架在反向解析的时候,“以为”对应的属性名称是flag,导致属性获取不到,进而抛出异常。
有一天,如果你运气不好,你可能会偶然发现这样一段代码:
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while(true)
range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {
}
这段代码是做什么的?检查结果看来一点也不明显,这就是不使用它的充分理由(条目 67)。事实证明,这是一种用于循环遍历数组元素的非常错误的习惯用法。当试图访问数组边界之外的第一个数组元素时,无限循环通过抛出、捕获和忽略ArrayIndexOutOfBoundsException异常来终止。它应该等同于循环数组的标准习惯用法,任何Java程序员都可以一眼就能识别出来:
for (Mountain m : range)
m.climb();
那么为什么有人会使用基于异常的循环而不是尝试和正确的用法? 根据错误推理提高性能是一种错误的尝试,因为虚拟机检查所有数组访问的边界,由编译器隐藏但仍然存在于for-each循环中的正常循环终止测试是多余的,应该避免。 这个推理有三个问题:
因为异常是为特殊情况设计的,所以JVM实现者几乎没有试图让它们像显式测试一样快。
将代码放在try-catch块中会抑制虚拟机实现可能执行的某些优化。
遍历数组的标准习惯用法不一定会导致冗余检查。许多虚拟机实现对它们进行了优化。
事实上,基于异常的习惯用法比标准用法慢得多。在我的机器上,100个元素的数组,基于异常的习惯用法的速度大约是标准习惯用法的两倍。
基于异常的循环不仅混淆了代码的目的,降低了代码的性能,而且不能保证它能正常工作。如果循环中存在bug,使用异常进行流控制可以掩盖该bug,从而大大增加调试过程的复杂性。假设循环体中的计算调用一个方法,该方法对一些不相关的数组执行越界访问。如果使用合理的循环习惯用法,该bug将生成一个未捕获的异常,导致线程立即终止,并带有完整的堆栈跟踪。如果使用错误的基于异常的循环,则会捕获与bug相关的异常,并将其误解为正常的循环终止。
这个示例说明的道理很简单:顾名思义,异常仅用于特殊情况; 它们永远不应该用于正常的控制流程。 通常来说,使用标准的、易于识别的习惯用法,而不是声称可以提供更好性能的过度聪明的技术。即使性能优势是真实存在的,但在稳步改进平台实现的情况下,这种优势也可能不复存在。然而,来自过度聪明的技术的细微缺陷和维护难题肯定会继续存在。
这个原则对API设计也有影响。一个设计良好的API不能强迫它的客户端为正常的控制流使用异常。只有在某些不可预知的条件下才能调用具有“状态依赖(state-dependent)”方法的类,通常应该有一个单独的“状态测试(state-testing)”方法,指示是否适合调用状态依赖方法。例如,Iterator接口具有依赖于状态的next方法和对应的状态测试方法hasNext。这支持使用传统for循环(以及for-each循环,其中内部使用了hasNext方法)在集合上迭代的标准习惯用法:
for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
Foo foo = i.next();
...
}
如果Iterator缺少hasNext方法,则客户端将被迫执行此操作:
// Do not use this hideous code for iteration over a collection!
try {
Iterator<Foo> i = collection.iterator();
while(true) {
Foo foo = i.next();
...
}
} catch (NoSuchElementException e) {
}
这数组迭代的例子非常类似于本条目一开始的那个例子。除了冗长和误导之外,基于异常的循环很可能执行得很差,并且可以掩盖系统中不相关部分中的bug。
提供单独的状态测试方法的另一种方式是,让依赖于状态的方法返回一个空的Optional值(条目 55),或者在它不能执行所需的计算时返回一个区分值,比如null。
下面是一些指导原则,帮助你在状态测试方法,Optional的或区分的返回值之间进行选择。如果要在没有外部同步的情况下并发地访问对象,或者受制于外部引发的状态转换,则必须使用Optional的或可区分的返回值,因为在调用状态测试方法与其依赖于状态的方法之间的间隔内,对象的状态可能会发生变化。如果一个单独的状态测试方法将重复依赖于状态的方法的工作,那么性能问题可能要求使用一个Optional的或可区分的返回值。在所有其他条件相同的情况下,状态测试方法略优于区分的返回值。它提供了更好的可读性,而且不正确的使用可能更容易检测:如果忘记调用状态测试方法,依赖于状态的方法将抛出异常,使错误变得明显;如果忘记检查一个可区分的返回值,那么这个bug可能很微妙。这不是Optional返回值的问题。
总之,异常是针对特殊情况而设计的。不要将它们用于正常的控制流程,也不要编写强制其他人这样做的API。
Java提供了三种可抛出异常对象:已检查异常( checked exceptions)、运行时异常(runtime exceptions)和虚拟机错误(errors)。程序员们对什么时候使用每种抛出的异常比较困惑。虽然决策并不总是明确的,但是有一些通用规则可以提供有力的指导。
决定是否使用已检查异常或未检查异常的基本规则是:对于可以合理地预期调用者将从中恢复的条件,使用已检查异常。通过抛出一个已检查的异常,可以强制调用者在catch子句中处理异常,或者将其传播出去。因此,声明要抛出方法的每个已检查异常都有力地向API用户表明,关联的条件是调用该方法的一个可能的结果。
通过向用户提供已检查异常,API设计器提供了从异常条件中恢复的要求。用户可以通过捕获异常并忽略异常来无视这个要求,但这通常不是一个好主意(条目 77)。
有两种未检查的可抛出的异常:运行时异常和虚拟机错误。它们在行为上是一样的:都是可抛出的,通常不应该被捕获。如果程序抛出未检查的异常或错误,通常情况下是无法恢复的,继续执行的话弊大于利。如果程序没有捕捉到这样的可抛出的异常,会导致当前线程挂起或停止,并发出适当的错误消息。
使用运行时异常来指出编程错误。 绝大多数运行时异常表示违反了先决条件(precondition violation)。 违反先决条件的原因仅仅是客户端API无法遵守API规范建立的约定。 例如,数组访问的约定指定数组索引必须介于0和数组长度减去1之间)。 ArrayIndexOutOfBoundsException异常指出违反了此先决条件。
这个建议的一个问题是,你并不总是清楚是在处理可恢复的异常还是编程错误。例如,考虑资源耗尽的情况,这可能是由编程错误(如分配一个不合理的大数组)或真正的资源短缺造成的。如果资源枯竭是由于临时短缺或需求临时增加造成的,这种情况很可能是可以恢复的。对于API设计人员来说,判断给定的资源耗尽实例是否允许恢复是一个问题。如果你认为某个条件可能允许恢复,请使用已检查的异常;如果不能,则使用运行时异常。如果不清楚是否可以恢复,最好使用未检查的异常,原因将在条目 71中讨论。
虽然Java语言规范没有要求,但有一个强烈的约定,即保留错误(errors)以供JVM使用,以指示资源缺陷,不变性失败(invariant failures),以及其他无法继续执行的条件。 鉴于几乎普遍接受这种约定,最好不要实现任何新的Error子类。 因此,实现所有未经检查的可抛出异常应该是RuntimeException的子类(直接或间接子类)。 不仅不应该定义Error子类,而且除了AssertionError之外,也不应该抛出它们。
可以定义一个可抛出的异常,不是Exception、RuntimeException或Error的子类。JLS不直接处理这些可抛出类,而是隐式地指定它们作为普通的检查异常(它们是Exception的子类,但不是RuntimeException)。那么,什么时候应该使用这样的可抛出异常?总之,永远不会。与普通的检查异常相比,它们没有任何好处,只会让API的使用者感到困惑。
API设计者经常忘记异常是也是完全成熟的对象,可以在其上定义任意方法。此类方法的主要用途是提供捕获异常的代码,其中包含有关导致抛出异常的条件的其他信息。 在没有这样的方法的情况下,已知程序员解析异常的字符串表示以发现附加信息。 这是非常糟糕的做法(条目 12)。 可抛出的类很少指定其字符串表示的细节,因此字符串表示可能因实现而异,也可能因发布而异。 因此,解析异常的字符串表示的代码可能是不可移植且脆弱的。
因为检查异常通常表示可恢复的异常条件,所以对它们来说,提供信息的方法来帮助调用者从异常条件中恢复尤为重要。例如,假设当使用礼品卡购物的尝试由于资金不足而失败时,抛出一个已检查的异常。异常应该提供一个访问器方法来查询差额的数量。这会使调用者能够将金额传递给购物者。有关此主题的更多信息,请参见条目 75。
总而言之,为可恢复的异常条件抛出已检查异常,为编程错误抛出未检查异常。当有疑虑不确定时,抛出未检查的异常。不要定义任何既不是已检查异常也不是运行时异常的可抛出异常。提供已检查异常的方法,用来帮助恢复。
受检异常是java程序语言设计的一项很好的特性。与返回代码不同,他们强迫程序员处理异常的条件,大大增强了可靠性。也就是说,过份的使用受检异常会使API使用起来非常不方便。如果方法抛出一个或者多个受检异常,调用该方法的代码就必须再一个或者多个catch块中处理这些异常,或者他必须声明他抛出这些异常,并让他们传播出去。无论哪种方法,都给程序员增添了不可忽略的负担。
如果正确的使用API并不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员可以立即采取有用的动作,这种负担就被认为是正当的。除非这两个条件都成立,否则更适合于使用未受检的异常。作为一个“石蕊”测试,你可以试着问自己:程序员将如何处理该异常。下面做法是最好的吗?
}catch(TheCheckedException e){
throw new AssertionError();
}
下面这种做法如何?
}catch(TheCheckedException e){
e.printStackTrace();
System.exit(1);
}
如果使用API的程序员无法做的比这更好,那么未受检的异常可能更为合适。这种例子就是CloneNotSupportedException。他就是Object,clone抛出来的,而Object.clone应该只是在实现了 Cloneable的对象上才可以被调用。在实践中catch块几乎总是具有要付出努力,还使程序更为复杂。
被一个方法单独抛出的受检异常,会给程序员带来非常高的额外负担。如果这个方法还有其他的受检异常,他被调用的时候一定已经出现再一个try块中,所以这个异常只需要另外一个catch块。如果方法抛出单个受检异常仅仅一个异常就会导致该方法不得不外与try块中,在这些情况下,应该问问自己,是否有别的路径来避免使用受检异常。
把受检异常变成未受检异常”的一种方法是,把这个抛出异常的方法分成两个方法,其中一个方法返回一个boolean,表明是否应该抛出异常。这种API重构,把下面的调用序列:
try{
obj.action(args);
}catch(TheCheckException e){
}
重构为:
if(obj.actionPermitted(args)){
obj.action(args);
}catch(TheCheckException e ){
}
这种重构并不总是恰当的,但是,凡是在恰当的地方,他都会使API用起来更加舒服,虽然后者的调用序列没有前者漂亮,但是这样得到的API更加灵活。如果程序员知道调用将会成功,或者不介意由于调用失败而导致的线程终止,这种重构还允许以下更为简单的调用形式:
obj.action(args);
如果你怀疑这个简单的调用序列是否符合要求,这个API重构可能就是恰当的,。这种重构之后的API在本质上等同于异常的“状态测试方法“,并且,同样的告诫依然适用:如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,这种重构就是不恰当的,因为在actionPermitted和action这两个调用的时间间隔之中,对象的状态有可能会发生变化,如果单独的actionPermitted方法必须重复action方法工作,处于性能的考虑,这种API重构就不值得去做。
Java平台类库提供了一组基本的未受检的异常,他们满足了绝大部分API的异常抛出异常。
为什么优先使用标准异常
1.它使你的API可读性更强,因为它与程序员习惯的用法一致。
2.异常类越少,程序在类装载阶段的负担就越少,时间开销也越少。
怎么使用标准异常
常用的标准异常:
IllegalArgumentException 参数不符合条件
IllegalStateException 接受对象的状态异常
NullpointerException 空指针异常
IndexOutOfBoundsException 数组越界
ConcurrentModificationException 并发修改异常
UnsopportedOperationException 不支持操作异常
如果方法抛出的异常与他所执行的任务没有明显的联系,这种情形将会使人不知所措。当方法传递由底层抽象抛出的异常时,往往会发生这种情况。除了使人感到困惑之外,这也让实现细节污染了更高层的API。如果高层的实现在后续的发行版本中发生了变化,他所抛出的异常也可能会跟着发生变化,从而潜在的破坏现有的客户端程序。
为了避免这个问题,更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法被称为异常转译。
一种特殊的异常转译形式称为异常链,如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很适合。低层的异常(原因)被传到高层的异常,高层的异常提供访问方法(Throwable.getCause)来获得低层的异常。
高层异常的构造器将原因传到支持连的超级构造器,因此他最终将被传给Throwable的其中一个运行异常链的构造器。
大多数标准的异常都有支持链的构造器。对于没有支持链的异常,可以利用Throwable的initCause方法设置原因。异常链不仅让你可以通过程序(用getCause)访问原因,它还可以将原因的堆栈轨迹继承到更高层的异常中。
尽管异常转移与不加选择的从低层传递异常的做法相比有所改进,但是他也不能被滥用。如有可能,处理来自低层异常的最好做法是,在调用低层方法之前确保他们会成功执行,从而避免他们抛出异常。有时候,可以在给低层传递参数之前,检查更高层方法的参数的有效性,从而避免低层方法抛出异常。
如果无法避免低层异常,次选方案是,让更高层来悄悄地绕开这些异常,从而将高层方法的调用者与低层的问题隔离开来。在这种情况下,可以用某种适当的记录机制(如java.util.logging)将异常记录下来。这样有助于管理员调查问题,同时又将客户端代码和最终用户与问题隔离开来。
总而言之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证他抛出的所有异常对高层也适合才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:他允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析。
在对象抛出异常之后,通常希望对象仍然处于定义良好的可用状态,即使失败发生在执行操作中。对于检查异常尤其如此,调用者希望从检查异常中恢复。一般来说,失败的方法调用应该使对象处于调用之前的状态。具有此属性的方法称为失败原子性( failure-atomic)。
有几种方法可以达到这种效果。最简单的方法是设计不可变对象(条目 17)。如果对象是不可变的,则失败原子性是必然的。如果一个操作失败,它可能会阻止创建一个新对象,但是它不会让一个现有对象处于不一致的状态,因为每个对象的状态在创建时是一致的,并且在创建后不能修改。
对于对可变对象进行操作的方法,实现失败原子性的最常用方法是:在执行操作之前检查参数的有效性(条目 49)。 这导致在对象修改开始之前就会抛出大多数异常。 例如,考虑条目 7中的Stack.pop方法:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
如果取消了初始大小检查,当该方法试图从空栈中弹出元素时,仍然会抛出异常。但是,这会使size属性处于不一致的(负数)状态,导致以后对对象的任何方法调用失败。此外,pop方法抛出的ArrayIndexOutOfBoundsException针对抽象来讲是不合适的。(条目 73)。
实现失败原子性的一种密切相关的方法是对计算进行排序,以便任何可能失败的部分在修改对象的部分之前发生。 在执行部分计算时进行参数检查,此方法是前一个方法的自然扩展。 例如,考虑TreeMap的情况,其元素按照某种顺序排序。 为了向TreeMap添加元素,元素必须是可以使用TreeMap的顺序进行比较的类型。 在以任何方式修改tree之前,尝试添加错误键的元素自然会因为在tree中搜索元素失败而导致ClassCastException异常。
实现失败原子性的第三种方法是,在对象的临时拷贝上执行操作,并在操作完成后用临时拷贝替换对象的内容。当数据存储在临时数据结构中后,计算可以更快地执行时,这种方法自然会出现。例如,一些排序方法在排序之前将其输入列表拷贝到数组中,以降低访问排序内循环中的元素的成本。这样做是为了提高性能,但是作为一个额外的好处,它确保如果排序失败,输入列表保持不变。
实现失败原子性的最后的方法是,编写恢复代码(recovery code),但这种做法并不长用,该代码拦截在操作中发生的失败,并使对象将其状态回滚到操作开始之前的点。 此方法主要用于持久性的(基于磁盘)的数据结构。
虽然失败原子性通常是可取的,但它并不总是可以实现的。例如,如果两个线程试图在没有适当同步的情况下并发地修改同一个对象,那么该对象可能会处于不一致的状态。因此,如果假定在捕捉到ConcurrentModificationException之后对象仍然可用,那就错了。错误是不可恢复的,所以方法在抛出AssertionError时,甚至不需要尝试保存失败原子性。
即使在可能存在实现失败原子性的情况下,也并非总是可取的。 对于某些操作,它会显着增加成本或复杂性。 也就是说,一旦你意识到这个问题,通常都可以自由而轻松地做到失败原子性。
总之,作为规则,任何生成的异常都是方法规范的一部分,应该使对象处于方法调用之前的状态。 违反此规则的地方,API文档应清楚地指出该对象将保留在哪种状态。遗憾的是,许多现有的API文档无法实现这一理想。
虽然这一建议似乎显而易见,但它经常被违反,因此值得重复提及。当API的设计人员声明一个抛出异常的方法时,他们试图告诉你一些事情。不要忽忽略它!在方法调用的周围加上一条try语句,其catch块为空,这样就很容易忽略了异常:
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
空的catch块违背了异常的初衷,而异常的目的是强迫处理异常情况。忽略异常类似于忽略火灾警报——关掉它,这样其他人就没有机会看到是否真的发生了火灾。你可能侥幸逃脱,或者结果可能是灾难性的。每当你看到一个空的catch块,你的脑海中就应该响起警报。
但在某些情况下,忽略异常是合适的。例如,在关闭FileInputStream时,它可能是合适的。你没有更改文件的状态,因此不需要执行任何恢复操作,并且已经从文件中读取了所需的信息,因此没有理由中止正在进行的操作。记录异常可能是明智的,这样如果这些异常经常发生,你就可以调查这个问题。如果选择忽略异常,catch块应该包含一条解释为什么这样做是合适的注释,并且变量应该被命名为ignore:
Future<Integer> f = exec.submit(planarMap::chromaticNumber);
int numColors = 4; // Default; guaranteed sufficient for any map
try {
numColors = f.get(1L, TimeUnit.SECONDS);
} catch (TimeoutException | ExecutionException ignored) {
// Use default: minimal coloring is desirable, not required
}
本条目中的建议同样适用于检查异常和未检查异常。不管异常是表示可预测的异常情况还是编程错误,用空catch块忽略它将导致程序在错误面前默默地执行下去。然后,程序可能会在未来的任意时间失败,在代码中与问题根源没有明显关系的某个点上。正确处理异常可以完全避免失败。仅仅让异常向外传播至少会导致程序迅速失败,保留信息以帮助调试失败的原因。
序列化(object serialization)API,它提供了一个框架,用来将对象编码成字节流,以及从字节流编码中重新构建对象。“将一个对象编码成一个字节流”,这就称作序列化(serializing)该对象;相反的处理过程被称作反序列化(deserializing)。
要想使一个类的实例可被序列化,非常简单,只要在它的声明中加入"implements Serializable"字样即可。虽然使一个类可被序列化的直接开销低到甚至可以忽略不计,但是为了序列化而付出的长期开销往往是实实在在的。
为实现Serializable而付出的最大代价是,一旦一个类被发布,就大大降低了"改变这个类的实现"的灵活性。 如果一个类实现了Serializable,它的字节流编码(或者说序列化形式,serialized form)就变成了它的导出的API的一部分。一旦这个类被广泛使用,往往必须永远支持这种序列化形式,就好像你必须要支持导出的API的所有其他部分一样。如果你不努力设计一个自定义的序列化形式(custom serialized form),而仅仅接受了默认的序列化形式,这种序列化形式将被永远地束缚在该类最初的内部表示法上。换句话说,如果你接受了默认的序列化形式,这个类中私有的和包级私有的实例域将都变成导出的API的一部分,这不符合"最低限度地访问域"的实践准则(见第13条),从而它就失去了作为信息隐藏工具的有效性。
如果你接受了默认的序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容。客户端程序企图用这个类的旧版本来序列化一个类,然后用新版本进行反序列化,结果将导致程序失败。在改变内部表示法的同时仍然维持原来的序列化形式(使用ObjectOutputStream.putFields和ObjectInputStream.readFields),这也是可能的,但是做起来比较困难,并且会在源代码中留下一些可以明显的隐患。因此,你应该仔细地设计一种高质量的序列化形式,并且在很长时间内都愿意使用这种形式(见第75,78条)。这样做将会增加开发的初始成本,但这是值得的。设计良好的序列化形式也许会给类的演变带来限制;但是设计不好的序列化形式则可能会使类根本无法演变。
序列化会使类的演变受到限制,这种限制的一个例子与流的唯一标识符(stream unique identifier)有关,通常它也被称为序列版本UID(serial version UID)。每个可序列化的类都有一个唯一标识号与它相关联。如果你没有在一个名为serialVersionUID的私有静态final的long域中显式地指定该标识号,系统就会自动地将一个复杂的过程作用在这个类上,从而在运行时产生该标识号。这个自动产生的值会受到类名称、它所实现的接口的名称、以及所有公有的和受保护的成员的名称所影响。如果你通过任何方式改变了这些信息,比如,增加了一个不是很重要的工具方法,自动产生的序列版本UID也会发生变化。因此,如果你没有声明一个显式的序列版本UID,兼容性将会遭到破坏,在运行时导致InvalidClassException异常。
实现Serializable的第二个代价是,它增加了出现Bug和安全漏洞的可能性。 通常情况下,对象是利用构造器来创建的;序列化机制是一种语言之外的对象创建机制(extralinguistic mechanism)。无论你是接受了默认的行为,还是覆盖了默认的行为,反序列化机制(deserialization)都是一个"隐藏的构造器",具备与其他构造器相同的特点。因为反序列化机制中没有显式的构造器,所以你很容易忘记要确保:反序列化过程必须也要保证所有"由构造器建立起来的约束关系",并且不允许攻击者访问正在构造过程中的对象的内部信息。依靠默认的反序列化机制,可以很容易地使对象的约束关系遭到破坏,以及遭受到非法访问(见第76条)。
实现Serializable的第三个代价是,随着类发行新的版本,相关的测试负担也增加了。 当一个可序列化的类被修订的时候,很重要的一点是,要检查是否可以"在新版本中序列化一个实例,然后在旧版本中反序列化",反之亦然。因此,测试所需要的工作量与"可序列化的类的数量和发行版本号"的乘积成正比,这个乘积可能会非常大。这些测试不可能自动构建,因为除了二进制兼容性(binary compatibility)以外,你还必须测试语义兼容性(semantic compatibility)。换句话说,你必须既要确保"序列化-反序列化"过程成功,也要确保结果产生的对象真正是原始对象的复制品。可序列化类的变化越大,它就越需要测试。如果在最初编写一个类的时候,就精心设计了自定义的序列化形式,测试的要求就可以有所降低,但是也不能完全没有测试。
实现Serializable接口并不是一个很轻松就可以做出的决定。它提供了一些实在的益处:如果一个类将要加入到某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化,对于这个类来说,实现Serializable接口就非常有必要。更进一步来看,如果这个类要成为另一个类的一个组件,并且后者必须实现Serializable接口,若前者也实现了Serializable接口,它就会更易于被后者使用。 然而,有许多实际的开销都与实现Serializable接口有关。每当你实现一个类的时候,都需要权衡一下所付出的代价和带来的好处。根据经验,比如Date和BigInteger这样的值类应该实现Serializable,大多数的集合类也应该如此。代表活动实体的类,比如线程池(thread pool),一般不应该实现Serializable。
为了继承而设计的类(见第17条)应该很少实现Serializable,接口也应该很少会扩展它。如果违反了这条规则,扩展这个类或者实现这个接口的程序员就会背上沉重的负担。然而在有些情况下违反这条规则却是合适的。例如,如果一个类或者接口存在的目的主要是为了参与到某个框架中,该框架要求所有的参与者都必须实现Serializable,
那么,对于这个类或者接口来说,实现或者扩展Serializable就是非常有意义的。
为了继承而设计的类中真正实现了Serializable的有Throwable、Component和HttpServlet。因为Throwable实现了Serializable,所以RMI的异常可以从服务器端传到客户端。Component实现了Serializable,因此GUI可以被发送、保存和恢复。HttpServlet实现了Serializable,因此会话状态可以被缓存。
如果你实现了一个带有实例域的类,它是可序列化和可扩展的,你就应该担心这样一条告诫。
如果一个专门为了继承而设计的类不是可序列化的,就不可能编写出可序列化的子类。特别是,如果超类没有提供可访问的无参构造器,子类也不可能做到可序列化。因此,对于为继承而设计的不可序列化的类,你应该考虑提供一个无参构造器。
内部类(inner class)(见第22条)不应该实现Serializable。它们使用编译器产生的合成域(synthetic field)来保存指向外围实例(enclosing instance)的引用,以及保存来自外围作用域的局部变量的值。"这些域如何对应到类定义中"并没有明确的规定,就好像没有指定匿名类和局部类的名称一样。因此,内部类的默认序列化形式是定义不清楚的。然而,静态成员类(static member class)却可以实现Serializable。
简而言之,千万不要认为实现Serializable会很容易。除非一个类在用了一段时间之后就会被抛弃,否则,实现Serializable就是个很严肃的承诺,必须认真对待。如果一个类是为了继承而设计的,则更加需要加倍小心。对于这样的类而言,在"允许子类实现Serializable"或"禁止子类实现Serializable"两者之间的一个折衷设计方案是,提供一个可访问的无参构造器。这种设计方案允许(但不要求)子类实现Serializable。
如果这个类实现了Serializable接口,并且使用了默认的序列化形式,你就永远无法彻底摆脱那个应该丢弃的实现了。它将永远牵制住这个类的序列化形式。
如果没有先认真考虑默认的序列化形式是否适合,则不要贸然接受。一般来将,只有当你自行设计的自定义序列化形式与默认的序列化形式基本相同时,才能接受默认序列化形式。
默认的序列化形式描述了改对象内部所包含的数据,以及每一个可以从这个对象到达其他对象的内部数据。它也描述了所有这些对象被链接起来后的拓扑结构。对于一个对象来说,理想的序列化形式应该只包含改对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的。
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合使用默认的序列化形式。
即使你确定了默认的序列化形式是适合的,通常还必须提供一个readObject方法以保证约束关系和安全性。
当一个对象的物理表示法与它的逻辑内容有实质性区别时,使用默认序列化形式会有以下四个缺点:
1 它是这个类的导出API永远地束缚在该类的内部类表示法上。
2 它会消耗过多的空间。例如实现细节不需要记录在序列化中。
3 它会消耗过多的时间。序列化逻辑并不了解对象图的拓扑关系,所以它必须要经过一个昂贵的图遍历(traversal)过程。
4 它会引起栈溢出。默认的序列化过程要对对象执行一次递归遍历,即使对中等规模的对象图,这样的操作也可能引起栈溢出。
transient 修饰符表明这个实例域将从一个类的默认序列化形式中省略掉。
如果所有的实例域都是瞬时的(transient),从技术角度而言,不调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这么做。即使所有的实例域都是transient的,调用defaultWriteObject也会影响该类的序列化形式,从而极大地增强灵活性。这样得到的序列化形式允许再以后的发行版本中增加非transient的实例域,并且还能保持向前或者向后兼容性。
无论你是否使用默认的序列化形式,当defaultWriteObject方法被调用时,每一个未被标记为transient的实例域都会被序列化。在决定一个域做成非transient的之前,请一定要确信它的值将是该对象逻辑状态的一部分。
如果你正在使用默认的序列化形式,并且把一个或多个域标记为transient,则要记住,当他被反序列化的时候,这些域将被初始化为它们的默认值。引用类型 , null,数值型 ,0 /0.0 ,boolean型 , false。如果这些值对于任何瞬时状态的属性都不可接受,则必须提供一个readObject方法,该方法调用defaultReadObject方法,然后将瞬时状态的属性恢复为可接受的值(76条)。或者,这些属性可以在第一次使用时进行延迟初始化(71条)。
如果在读取整个对象状态的其他任何方法上强制任何同步,则也必须在对象序列化上强制这种同步。
不管你选择了哪种序列化形式,都要为自己编写的每个可序列化类声明一个显示的序列版本UID。 不要更改序列版本UID,除非想破坏与类的所有现有序列化实例的兼容性。
一个类将要加入到某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化,对于这个类来说,实现Serializable接口就非常有必要。更进一步来看,如果这个类要成为另一个类的一个组件,并且后者必须实现Serializable接口,若前者也实现了Serializable接口,它就会更易于被后者使用。 然而,有许多实际的开销都与实现Serializable接口有关。每当你实现一个类的时候,都需要权衡一下所付出的代价和带来的好处。根据经验,比如Date和BigInteger这样的值类应该实现Serializable,大多数的集合类也应该如此。代表活动实体的类,比如线程池(thread pool),一般不应该实现Serializable。
为了继承而设计的类(见第17条)应该很少实现Serializable,接口也应该很少会扩展它。如果违反了这条规则,扩展这个类或者实现这个接口的程序员就会背上沉重的负担。然而在有些情况下违反这条规则却是合适的。例如,如果一个类或者接口存在的目的主要是为了参与到某个框架中,该框架要求所有的参与者都必须实现Serializable,
那么,对于这个类或者接口来说,实现或者扩展Serializable就是非常有意义的。
为了继承而设计的类中真正实现了Serializable的有Throwable、Component和HttpServlet。因为Throwable实现了Serializable,所以RMI的异常可以从服务器端传到客户端。Component实现了Serializable,因此GUI可以被发送、保存和恢复。HttpServlet实现了Serializable,因此会话状态可以被缓存。
如果你实现了一个带有实例域的类,它是可序列化和可扩展的,你就应该担心这样一条告诫。
如果一个专门为了继承而设计的类不是可序列化的,就不可能编写出可序列化的子类。特别是,如果超类没有提供可访问的无参构造器,子类也不可能做到可序列化。因此,对于为继承而设计的不可序列化的类,你应该考虑提供一个无参构造器。
内部类(inner class)(见第22条)不应该实现Serializable。它们使用编译器产生的合成域(synthetic field)来保存指向外围实例(enclosing instance)的引用,以及保存来自外围作用域的局部变量的值。"这些域如何对应到类定义中"并没有明确的规定,就好像没有指定匿名类和局部类的名称一样。因此,内部类的默认序列化形式是定义不清楚的。然而,静态成员类(static member class)却可以实现Serializable。
简而言之,千万不要认为实现Serializable会很容易。除非一个类在用了一段时间之后就会被抛弃,否则,实现Serializable就是个很严肃的承诺,必须认真对待。如果一个类是为了继承而设计的,则更加需要加倍小心。对于这样的类而言,在"允许子类实现Serializable"或"禁止子类实现Serializable"两者之间的一个折衷设计方案是,提供一个可访问的无参构造器。这种设计方案允许(但不要求)子类实现Serializable。
如果这个类实现了Serializable接口,并且使用了默认的序列化形式,你就永远无法彻底摆脱那个应该丢弃的实现了。它将永远牵制住这个类的序列化形式。
如果没有先认真考虑默认的序列化形式是否适合,则不要贸然接受。一般来将,只有当你自行设计的自定义序列化形式与默认的序列化形式基本相同时,才能接受默认序列化形式。
默认的序列化形式描述了改对象内部所包含的数据,以及每一个可以从这个对象到达其他对象的内部数据。它也描述了所有这些对象被链接起来后的拓扑结构。对于一个对象来说,理想的序列化形式应该只包含改对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的。
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合使用默认的序列化形式。
即使你确定了默认的序列化形式是适合的,通常还必须提供一个readObject方法以保证约束关系和安全性。
当一个对象的物理表示法与它的逻辑内容有实质性区别时,使用默认序列化形式会有以下四个缺点:
1 它是这个类的导出API永远地束缚在该类的内部类表示法上。
2 它会消耗过多的空间。例如实现细节不需要记录在序列化中。
3 它会消耗过多的时间。序列化逻辑并不了解对象图的拓扑关系,所以它必须要经过一个昂贵的图遍历(traversal)过程。
4 它会引起栈溢出。默认的序列化过程要对对象执行一次递归遍历,即使对中等规模的对象图,这样的操作也可能引起栈溢出。
transient 修饰符表明这个实例域将从一个类的默认序列化形式中省略掉。
如果所有的实例域都是瞬时的(transient),从技术角度而言,不调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这么做。即使所有的实例域都是transient的,调用defaultWriteObject也会影响该类的序列化形式,从而极大地增强灵活性。这样得到的序列化形式允许再以后的发行版本中增加非transient的实例域,并且还能保持向前或者向后兼容性。
无论你是否使用默认的序列化形式,当defaultWriteObject方法被调用时,每一个未被标记为transient的实例域都会被序列化。在决定一个域做成非transient的之前,请一定要确信它的值将是该对象逻辑状态的一部分。
如果你正在使用默认的序列化形式,并且把一个或多个域标记为transient,则要记住,当他被反序列化的时候,这些域将被初始化为它们的默认值。引用类型 , null,数值型 ,0 /0.0 ,boolean型 , false。如果这些值对于任何瞬时状态的属性都不可接受,则必须提供一个readObject方法,该方法调用defaultReadObject方法,然后将瞬时状态的属性恢复为可接受的值(76条)。或者,这些属性可以在第一次使用时进行延迟初始化(71条)。
如果在读取整个对象状态的其他任何方法上强制任何同步,则也必须在对象序列化上强制这种同步。
不管你选择了哪种序列化形式,都要为自己编写的每个可序列化类声明一个显示的序列版本UID。 不要更改序列版本UID,除非想破坏与类的所有现有序列化实例的兼容性。