Effective Java
(一)创建和销毁对象
1.考虑使用静态工厂方法替代构造方法
静态工厂方法说的不是设计模式的工厂模式
静态工厂方法优点
1. 不像构造方法,它们是有名字的,更易于阅读.
2. 与构造方法不同,它们不需要每次调用时候都创建一个新的对象
这允许不可变的类使用预先构建的实例,或者在构造时缓存实例,并反复分配它们以避免创建不必要的重复对象(类似享元模式)
3. 它们可以返回其返回类型的任何子类型对象.
4. 静态工厂的第四个优点是返回对象的类可以根据输入参数的不同而不同
5. 在编写包含该方法的类时,返回的对象的类不需要存在
缺点
只提供静态工厂方法的主要限制是,没有公共或受保护构造方法的类不能被子类化
静态工厂方法的第二个缺点是,程序员很难找到它们。
静态工厂方法常用名称
from——A类型转换方法,它接受单个参数并返回此类型的相应实例,例如:Date d = Date.from(instant);
of——一个聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:Set
valueOf——from和to更为详细的替代方式,例如:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance或getInstance——返回一个由其参数(如果有的话)描述的实例,但不能说它具有和参数相同的值,例如:StackWalker luke = StackWalker.getInstance(options);
create 或 newInstance——与instance 或 getInstance类似,除了该方法保证每个调用返回一个新的实例,例如:Object newArray = Array.newInstance(classObject, arrayLen);
getType——与getInstance类似,但是如果在工厂方法中不同的类中使用。Type是工厂方法返回的对象类型,例如:FileStore fs = Files.getFileStore(path);
newType——与newInstance类似,但是如果在工厂方法中不同的类中使用。Type是工厂方法返回的对象类型,例如:BufferedReader br = Files.newBufferedReader(path);
type—— getType 和 newType简洁的替代方式,例如:List
2.当构造方法参数过多的时候使用builder模式
参数过多的时候容易写颠倒参数,可能会出现错误的行为,
也可以使用实体类提供set方法进行赋值,就是JavaBeans模式.JavaBeans模式自身缺点,就是构造过程被分解到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。
传统构造方法缺点是构造函数灵活性不高,参数过多输入的时候让人头疼,
使用builder模式的好处之一就是灵活
在设计模式中对Builder模式的定义是用于构建复杂对象的一种模式,所构建的对象往往需要多步初始化或赋值才能完成。那么,在实际的开发过程中,我们哪些地方适合用到Builder模式呢?其中使用Builder模式来替代多参数构造函数是一个比较好的实践法则。
我们常常会面临编写一个这样的实现类(假设类名叫DoDoContact),这个类拥有多个构造函数,
DoDoContact(String name);
DoDoContact(String name, int age);
DoDoContact(String name, int age, String address);
DoDoContact(String name, int age, String address, int cardID);
这样一系列的构造函数主要目的就是为了提供更多的客户调用选择,以处理不同的构造请求。这种方法很常见,也很有效力,但是它的缺点也很多。类的作者不得不书写多种参数组合的构造函数,而且其中还需要设置默认参数值,这是一个需要细心而又枯燥的工作。其次,这样的构造函数灵活性也不高,而且在调用时你不得不提供一些没有意义的参数值,例如,DoDoContact(“Ace”, -1, “SH”),显然年龄为负数没有意义,但是你又不的不这样做,得以符合Java的规范。如果这样的代码发布后,后面的维护者就会很头痛,因为他根本不知道这个-1是什么含义。对于这样的情况,就非常适合使用Builder模式。Builder模式的要点就是通过一个代理来完成对象的构建过程。这个代理职责就是完成构建的各个步骤,同时它也是易扩展的。
传统使用javaBeans如果你做的类是API类,那就是麻烦所在,使用的人,在new完之后,完全不知道该给此对象设置哪些必须的属性,才能达到对象应该有的状态。
3.使用私有构造方法或枚举实现Singleton属性
枚举方式类似于公共属性方法,但更简洁,提供了免费的序列化机制,并提供了针对多个实例化的坚固保证,,即使是在复杂的序列化或反射攻击的情况下。这种方法可能感觉有点不自然,但是单一元素枚举类通常是实现单例的最佳方式。
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {
System.out.println(“Whoa baby, I’m outta here!”);
}
/
demo演示单例
@param args
*/
public static void main(String[] args) {
Elvis instance = Elvis.INSTANCE**;
instance.leaveTheBuilding();
}
4.使用私有构造方法执行非实例化
这种习惯有一个副作用,阻止了类的子类化。所有的构造方法都必须显式或隐式地调用父类构造方法,而子类则没有可访问的父类构造方法来调用。
// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();
}
… // Remainder omitted
}
5.使用依赖注入取代硬连接资源
在创建新的实例时将资源传递到构造方法中,这是依赖注入.该模式还有一个有用的扩展就是设计模式中的工厂模式.Spring就是依赖注入框架.
这个是写死在代码上
// Inappropriate use of singleton - inflexible & untestable!
public class SpellChecker {
private final Lexicon dictionary = …; //硬连接不好,代码写死了
private SpellChecker(…) {}
public static INSTANCE = new SpellChecker(…);
public boolean isValid(String word) { … }
public List
}
这个是依赖构造注入的
// Dependency injection provides flexibility and testability
public class SpellChecker {
private final Lexicon dictionary;
//依赖注入
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { …}
public List
}
6.避免创建不必要的对象
在每次需要时重用一个对象而不是创建一个新的相同功能对象通常是恰当的。重用可以更快更流行。如果对象是不可变的,它总是可以被重用。
比如String,不可取
String s = new String(“bikini”); // DON’T DO THIS!
改进之后:
String s = “bikini”;
另一种创建不必要的对象的方法是自动装箱(autoboxing),它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。 有微妙的语义区别和不那么细微的性能差异.
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
这个程序的结果是正确的,但由于写错了一个字符,运行的结果要比实际慢很多。变量sum被声明成了Long而不是long,这意味着程序构造了大约231不必要的Long实例(大约每次往Long类型的 sum变量中增加一个long类型构造的实例),把sum变量的类型由Long改为long,在我的机器上运行时间从6.3秒降低到0.59秒。这个教训很明显:优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。
7.消除过期的对象引用
如果你从使用手动内存管理的语言(如C或c++)切换到像Java这样的带有垃圾收集机制的语言,那么作为程序员的工作就会变得容易多了,因为你的对象在使用完毕以后就自动回收了。当你第一次体验它的时候,它就像魔法一样。这很容易让人觉得你不需要考虑内存管理,但这并不完全正确。
考虑以下简单的堆栈实现:
// 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)。 过期引用简单来说就是永远不会解除的引用。 在这种情况下,元素数组“活动部分(active portion)”之外的任何引用都是过期的。 活动部分是由索引下标小于size的元素组成。
垃圾收集语言中的内存泄漏(更适当地称为无意的对象保留 unintentional object retentions)是隐蔽的。 如果无意中保留了对象引用,那么不仅这个对象排除在垃圾回收之外,而且该对象引用的任何对象也是如此。 即使只有少数对象引用被无意地保留下来,也可以阻止垃圾回收机制对许多对象的回收,这对性能产生很大的影响。
这类问题的解决方法很简单:一旦对象引用过期,将它们设置为 null。 在我们的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类的哪个方面使它容易受到内存泄漏的影响?简单地说,它管理自己的内存。存储池(storage pool)由elements数组的元素组成(对象引用单元,而不是对象本身)。数组中活动部分的元素(如前面定义的)被分配,其余的元素都是空闲的。垃圾收集器没有办法知道这些;对于垃圾收集器来说,elements数组中的所有对象引用都同样有效。只有程序员知道数组的非活动部分不重要。程序员可以向垃圾收集器传达这样一个事实,一旦数组中的元素变成非活动的一部分,就可以手动清空这些元素的引用。
一般来说,当一个类自己管理内存时,程序员应该警惕内存泄漏问题。 每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。
另一个常见的内存泄漏来源是缓存。一旦将对象引用放入缓存中,很容易忘记它的存在,并且在它变得无关紧要之后,仍然保留在缓存中。对于这个问题有几种解决方案。如果你正好想实现了一个缓存:只要在缓存之外存在对某个项(entry)的键(key)引用,那么这项就是明确有关联的,就可以用WeakHashMap**来表示缓存;这些项在过期之后自动删除。记住,只有当缓存中某个项的生命周期是由外部引用到键(key)而不是值(value)决定时,WeakHashMap才有用。
更常见的情况是,缓存项有用的生命周期不太明确,随着时间的推移一些项变得越来越没有价值。在这种情况下,缓存应该偶尔清理掉已经废弃的项。这可以通过一个后台线程(也许是ScheduledThreadPoolExecutor)或将新的项添加到缓存时顺便清理。LinkedHashMap类使用它的removeEldestEntry方法实现了后一种方案。对于更复杂的缓存,可能直接需要使用java.lang.ref。
第三个常见的内存泄漏来源是监听器和其他回调。如果你实现了一个API,其客户端注册回调,但是没有显式地撤销注册回调,除非采取一些操作,否则它们将会累积。确保回调是垃圾收集的一种方法是只存储弱引用(weak references),例如,仅将它们保存在WeakHashMap的键(key)中。
因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。 通常仅在仔细的代码检查或借助堆分析器( heap profiler)的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的。
8.使用try-with-resources语句替代try-finally语句
Java类库中包含许多必须通过调用close方法手动关闭的资源。 比如InputStream,OutputStream和java.sql.Connection。 客户经常忽视关闭资源,其性能结果可想而知。 尽管这些资源中有很多使用finalizer机制作为安全网,但finalizer机制却不能很好地工作(条目 8)。
从以往来看,try-finally语句是保证资源正确关闭的最佳方式,即使是在程序抛出异常或返回的情况下:
// try-finally - No longer the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
这可能看起来并不坏,但是当添加第二个资源时,情况会变得更糟:
// try-finally is ugly when used with more than one resource!
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}
这可能很难相信,但即使是优秀的程序员,大多数时候也会犯错误。首先,我在Java Puzzlers[Bloch05]的第88页上弄错了,多年来没有人注意到。事实上,2007年Java类库中使用close方法的三分之二都是错误的。
即使是用try-finally语句关闭资源的正确代码,如前面两个代码示例所示,也有一个微妙的缺陷。 try-with-resources块和finally块中的代码都可以抛出异常。 例如,在firstLineOfFile方法中,由于底层物理设备发生故障,对readLine方法的调用可能会引发异常,并且由于相同的原因,调用close方法可能会失败。 在这种情况下,第二个异常完全冲掉了第一个异常。 在异常堆栈跟踪中没有第一个异常的记录,这可能使实际系统中的调试非常复杂——通常这是你想要诊断问题的第一个异常。 虽然可以编写代码来抑制第二个异常,但是实际上没有人这样做,因为它太冗长了。
当Java 7引入了try-with-resources语句时,所有这些问题一下子都得到了解决[JLS,14.20.3]。要使用这个构造,资源必须实现 AutoCloseable接口,该接口由一个返回为void的close组成。Java类库和第三方类库中的许多类和接口现在都实现或继承了AutoCloseable接口。如果你编写的类表示必须关闭的资源,那么这个类也应该实现AutoCloseable接口。
以下是我们的第一个使用try-with-resources的示例:
// try-with-resources - the the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
}
}
以下是我们的第二个使用try-with-resources的示例:
// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
不仅 try-with-resources版本比原始版本更精简,更好的可读性,而且它们提供了更好的诊断。 考虑firstLineOfFile方法。 如果调用readLine和(不可见)close方法都抛出异常,则后一个异常将被抑制(suppressed),而不是前者。 事实上,为了保留你真正想看到的异常,可能会抑制多个异常。 这些抑制的异常没有呗被抛弃, 而是打印在堆栈跟踪中,并标注为被抑制了。 你也可以使用getSuppressed方法以编程方式访问它们,该方法在Java 7中已添加到的Throwable中。
可以在 try-with-resources语句中添加catch子句,就像在常规的try-finally语句中一样。这允许你处理异常,而不会在另一层嵌套中污染代码。作为一个稍微有些做作的例子,这里有一个版本的firstLineOfFile方法,它不会抛出异常,但是如果它不能打开或读取文件,则返回默认值:
// try-with-resources with a catch clause
static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}
结论明确:在处理必须关闭的资源时,使用try-with-resources语句替代try-finally语句。 生成的代码更简洁,更清晰,并且生成的异常更有用。 try-with-resources语句在编写必须关闭资源的代码时会更容易,也不会出错,而使用try-finally语句实际上是不可能的。
9.重写equals方法适合遵守通用约定
虽然Object是一个具体的类,但是它主要是为了继承而设计的,它的所有非final方法都能通用约定,因为这些方法都能被子类重写,任何类都有义务重写这些方法.
什么时候需要重写equals
如果一个类包含一个逻辑相等( logical equality)的概念,此概念有别于对象标识(object identity),而且父类还没有重写过 equals 方法。这通常用在值类( value classes)的情况。值类只是一个表示值的类,例如 Integer 或 String 类。程序员使用 equals 方法比较值对象的引用,期望发现它们在逻辑上是否相等,而不是引用相同的对象。重写 equals 方法不仅可以满足程序员的期望,它还支持重写过 equals 的实例作为 Map 的键(key),或者 Set 里的元素,以满足预期和期望的行为。
当你重写equals方法时,必须遵守它的通用约定:
equals方法实现了一个等价关系(equivalence relation)。它有以下这些属性:
•自反性:对于任何非空引用x,x.equals(x)必须返回true。•对称性:对于任何非空引用x和y,如果且仅当y.equals(x)返回true时x.equals(y)必须返回true。•传递性:对于任何非空引用x、y、z,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)必须返回true。•一致性:对于任何非空引用x和y,如果在equals比较中使用的信息没有修改,则x.equals(y)的多次调用必须始终返回true或始终返回false。•对于任何非空引用x,x.equals(null)必须返回false。
10.重写equals方法时同时也要重写hashcode方法
在每个类中,在重写 equals 方法的时侯,一定要重写 hashcode 方法。如果不这样做,你的类违反了hashCode的通用约定,这会阻止它在HashMap和HashSet这样的集合中正常工作。根据 Object 规范,以下时具体约定:
1.当在一个应用程序执行过程中,如果在equals方法比较中没有修改任何信息,在一个对象上重复调用hashCode方法时,它必须始终返回相同的值。从一个应用程序到另一个应用程序的每一次执行返回的值可以是不一致的。
2.如果两个对象根据equals(Object)方法比较是相等的,那么在两个对象上调用hashCode就必须产生的结果是相同的整数。
3.如果两个对象根据equals(Object)方法比较并不相等,则不要求在每个对象上调用hashCode都必须产生不同的结果。 但是,程序员应该意识到,为不相等的对象生成不同的结果可能会提高散列表(hash tables)的性能。
当无法重写hashCode时,所违反第二个关键条款是:相等的对象必须具有相等的哈希码( hash codes)。根据类的equals方法,两个不同的实例可能在逻辑上是相同的,但是对于Object 类的hashCode方法,它们只是两个没有什么共同之处的对象。因此, Object 类的hashCode方法返回两个看似随机的数字,而不是按约定要求的两个相等的数字。
举例说明,假设你使用条目 10中的PhoneNumber类的实例做为HashMap的键(key):
Map
m.put(new PhoneNumber(707, 867, 5309), “Jenny”);
你可能期望m.get(new PhoneNumber(707, 867, 5309))方法返回Jenny字符串,但实际上,返回了 null。注意,这里涉及到两个PhoneNumber实例:一个实例插入到 HashMap 中,另一个作为判断相等的实例用来检索。PhoneNumber类没有重写 hashCode 方法导致两个相等的实例返回了不同的哈希码,违反了 hashCode 约定。put 方法把PhoneNumber实例保存在了一个哈希桶( hash bucket)中,但get方法却是从不同的哈希桶中去查找,即使恰好两个实例放在同一个哈希桶中,get 方法几乎肯定也会返回 null。因为HashMap 做了优化,缓存了与每一项(entry)相关的哈希码,如果哈希码不匹配,则不会检查对象是否相等了。
解决这个问题很简单,只需要为PhoneNumber类重写一个合适的 hashCode 方法。hashCode方法是什么样的?写一个不规范的方法的是很简单的。以下示例,虽然永远是合法的,但绝对不能这样使用:
// The worst possible legal hashCode implementation - never use!
@Override public int hashCode() { return 42; }
这是合法的,因为它确保了相等的对象具有相同的哈希码。这很糟糕,因为它确保了每个对象都有相同的哈希码。因此,每个对象哈希到同一个桶中,哈希表退化为链表。应该在线性时间内运行的程序,运行时间变成了平方级别。对于数据很大的哈希表而言,会影响到能够正常工作。
一个好的 hash 方法趋向于为不相等的实例生成不相等的哈希码。这也正是 hashCode 约定中第三条的表达。理想情况下,hash 方法为集合中不相等的实例均匀地分配int 范围内的哈希码。实现这种理想情况可能是困难的。 幸运的是,要获得一个合理的近似的方式并不难。 以下是一个简单的配方:
1. 声明一个 int 类型的变量result,并将其初始化为对象中第一个重要属性c的哈希码,如下面步骤2.a中所计算的那样。(回顾条目10,重要的属性是影响比较相等的领域。)
2.对于对象中剩余的重要属性f,请执行以下操作:
a. 比较属性f与属性c的 int 类型的哈希码:— i. 如果这个属性是基本类型的,使用 Type.hashCode(f)方法计算,其中Type类是对应属性 f 基本类型的包装类。— ii 如果该属性是一个对象引用,并且该类的equals方法通过递归调用equals来比较该属性,并递归地调用hashCode方法。 如果需要更复杂的比较,则计算此字段的“范式(“canonical representation)”,并在范式上调用hashCode。 如果该字段的值为空,则使用0(也可以使用其他常数,但通常来使用0表示)。
— iii 如果属性f是一个数组,把它看作每个重要的元素都是一个独立的属性。 也就是说,通过递归地应用这些规则计算每个重要元素的哈希码,并且将每个步骤2.b的值合并。 如果数组没有重要的元素,则使用一个常量,最好不要为0。如果所有元素都很重要,则使用Arrays.hashCode方法。
b. 将步骤2.a中属性c计算出的哈希码合并为如下结果:result = 31 result + c;
3.返回 result 值。
当你写完hashCode方法后,问自己是否相等的实例有相同的哈希码。 编写单元测试来验证你的直觉(除非你使用AutoValue框架来生成你的equals和hashCode方法,在这种情况下,你可以放心地忽略这些测试)。 如果相同的实例有不相等的哈希码,找出原因并解决问题。
可以从哈希码计算中排除派生属性(derived fields)。换句话说,如果一个属性的值可以根据参与计算的其他属性值计算出来,那么可以忽略这样的属性。您必须排除在equals比较中没有使用的任何属性,否则可能会违反hashCode约定的第二条。
步骤2.b中的乘法计算结果取决于属性的顺序,如果类中具有多个相似属性,则产生更好的散列函数。 例如,如果乘法计算从一个String散列函数中被省略,则所有的字符将具有相同的散列码。 之所以选择31,因为它是一个奇数的素数。 如果它是偶数,并且乘法溢出,信息将会丢失,因为乘以2相当于移位。 使用素数的好处不太明显,但习惯上都是这么做的。 31的一个很好的特性,是在一些体系结构中乘法可以被替换为移位和减法以获得更好的性能:31 i ==(i << 5) - i。 现代JVM可以自动进行这种优化。
让我们把上述办法应用到PhoneNumber类中:
// Typical hashCode method
@Override public int hashCode() {
**int **result = Short._hashCode_(areaCode);
result = 31 * result + Short._hashCode_(prefix);
result = 31 * result + Short._hashCode_(lineNum);
**return **result;
}
因为这个方法返回一个简单的确定性计算的结果,它的唯一的输入是PhoneNumber实例中的三个重要的属性,所以显然相等的PhoneNumber实例具有相同的哈希码。 实际上,这个方法是PhoneNumber的一个非常好的hashCode实现,与Java平台类库中的实现一样。 它很简单,速度相当快,并且合理地将不相同的电话号码分散到不同的哈希桶中。
虽然在这个项目的方法产生相当好的哈希函数,但并不是最先进的。 它们的质量与Java平台类库的值类型中找到的哈希函数相当,对于大多数用途来说都是足够的。 如果真的需要哈希函数而不太可能产生碰撞,请参阅Guava框架的的com.google.common.hash.Hashing[Guava]方法。
Objects类有一个静态方法,它接受任意数量的对象并为它们返回一个哈希码。 这个名为hash的方法可以让你编写一行hashCode方法,其质量与根据这个项目中的上面编写的方法相当。 不幸的是,它们的运行速度更慢,因为它们需要创建数组以传递可变数量的参数,以及如果任何参数是基本类型,则进行装箱和取消装箱。 这种哈希函数的风格建议仅在性能不重要的情况下使用。 以下是使用这种技术编写的PhoneNumber的哈希函数:
// One-line hashCode method - mediocre performance
@Override public int hashCode() {
**return **Objects.hash(lineNum, prefix, areaCode);
}
如果一个类是不可变的,并且计算哈希码的代价很大,那么可以考虑在对象中缓存哈希码,而不是在每次请求时重新计算哈希码。 如果你认为这种类型的大多数对象将被用作哈希键,那么应该在创建实例时计算哈希码。 否则,可以选择在首次调用hashCode时延迟初始化(lazily initialize)哈希码。 需要注意确保类在存在延迟初始化属性的情况下保持线程安全(项目83)。 PhoneNumber类不适合这种情况,但只是为了展示它是如何完成的。 请注意,属性hashCode的初始值(在本例中为0)不应该是通常创建的实例的哈希码:
// hashCode method with lazily initialized cached hash code
private int hashCode; // Automatically initialized to 0
@Override public int hashCode() {
**int **result = **hashCode**;
**if **(result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
hashCode = result;
}
**return **result;
}
不要试图从哈希码计算中排除重要的属性来提高性能。 由此产生的哈希函数可能运行得更快,但其质量较差可能会降低哈希表的性能,使其无法使用。 具体来说,哈希函数可能会遇到大量不同的实例,这些实例主要在你忽略的区域中有所不同。 如果发生这种情况,哈希函数将把所有这些实例映射到少许哈希码上,而应该以线性时间运行的程序将会运行平方级的时间。
这不仅仅是一个理论问题。 在Java 2之前,String 类哈希函数在整个字符串中最多使用16个字符,从第一个字符开始,在整个字符串中均匀地选取。 对于大量的带有层次名称的集合(如URL),此功能正好显示了前面描述的病态行为。
不要为hashCode返回的值提供详细的规范,因此客户端不能合理地依赖它; 你可以改变它的灵活性。 Java类库中的许多类(例如String和Integer)都将hashCode方法返回的确切值指定为实例值的函数。 这不是一个好主意,而是一个我们不得不忍受的错误:它妨碍了在未来版本中改进哈希函数的能力。 如果未指定细节并在散列函数中发现缺陷,或者发现了更好的哈希函数,则可以在后续版本中对其进行更改。
总之,每次重写equals方法时都必须重写hashCode方法,否则程序将无法正常运行。你的hashCode方法必须遵从Object类指定的常规约定,并且必须执行合理的工作,将不相等的哈希码分配给不相等的实例。如果使用第51页的配方,这很容易实现。如条目 10所述,AutoValue框架为手动编写equals和hashCode方法提供了一个很好的选择,IDE也提供了一些这样的功能。
11.始终重写toString()方法
虽然Object类提供了toString方法的实现,但它返回的字符串通常不是你的类的用户想要看到的。 它由类名后跟一个“at”符号(@)和哈希码的无符号十六进制表示组成,例如PhoneNumber@163b91。
toString的通用约定要求,返回的字符串应该是“一个简洁但内容丰富的表示,对人们来说是很容易阅读的”。虽然可以认为PhoneNumber@163b91简洁易读,但相比于707-867-5309,但并不是很丰富 。 toString通用约定“建议所有的子类重写这个方法”。好的建议,的确如此!
虽然它并不像遵守equals和hashCode约定那样重要(条目 10和11),但是提供一个良好的toString实现使你的类更易于使用,并对使用此类的系统更易于调试。当对象被传递到println、printf、字符串连接操作符或断言,或者由调试器打印时,toString方法会自动被调用。即使你从不调用对象上的toString,其他人也可以。例如,对对象有引用的组件可能包含在日志错误消息中对象的字符串表示。如果未能重写toString,则消息可能是无用的。
如果为PhoneNumber提供了一个很好的toString方法,那么生成一个有用的诊断消息就像下面这样简单:
System.out.println(“Failed to connect to “ + phoneNumber);
程序员将以这种方式生成诊断消息,不管你是否重写toString,但是除非你这样做,否则这些消息将不会有用。 提供一个很好的toString方法的好处不仅包括类的实例,同样有益于包含实例引用的对象,特别是集合。 打印map 对象时你会看到哪一个,{Jenny=PhoneNumber@163b91}还是{Jenny=707-867-5309}?
12.谨慎的重写Clone方法
1.Clone使用方法详解
http://www.blogjava.net/jerry-zhaoj/archive/2009/10/14/298141.html
简单总结一下,在java中我们想得到一个和之前用过的对象一模一样的新对象,简单的使用=符号复制是不行的,我们需要用到clone方法,这就是clone方法的用处。因为clone方法是protected类型的,所以不能在外部直接使用,下面一段代码简单说明一下clone方法的使用:
public class CloneObject implements Cloneable {
public String field01;
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new AssertionError();
}
}
}
首相我们需要是想Cloneable接口,然后再重载一个public类型的clone方法,然后在里面返回super.clone()就行了。
2. 使用拷贝构造器或者拷贝工厂方法来代替覆盖Clone方法
拷贝构造器列子:
public class MyObject {
public String field01;
public MyObject() {
}
public MyObject(MyObject object) {
this.field01 = object.field01;
}
}
拷贝静态工厂:
public class MyObject {
public String field01;
public MyObject() {
}
public static MyObject newInstance(MyObject object) {
MyObject myObject = new MyObject();
myObject.field01 = object.field01;
return myObject;
}
}
13.考虑实现Comparable接口
一个类,如果实现了Comparable接口,我们可以很容易的将它在数组或是集合中进行排序。so。。。Comparable这个接口是用来实现对象排序的。
假设有这么一个类:
public class CompObj implements Comparable
private int age;
private String name;
public CompObj(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
//重点在这,这是接口中唯一的一个方法
@Override
public int compareTo(@NonNull CompObj another) {
return this.age - another.age;
}
}
然后我们可以这样对他进行排序:
List
Collections.sort(list); //集合排序,就是这么简单
CompObj array[] = …;//初始化一个数组
Arrays.sort(list); //数组排序,就是这么简单
现在对comparaTo()方法进行简单说明。comparaTo()方法传入该类的另外一个实例,返回一个int值,这个方法每执行一次都是对传入的对象和和本生对象进行比较。返回的int值如果是一个正值(不包括零)则在数组或是集合中交换两个实例的位置,否则位置保持不变。
2. 为什么要考虑实现Comparable接口
实现了Comparable接口的类,可以很好的和集合类或是一些泛型算法很好的协作,你可以付出很小的代价实现强大的功能。
3. 什么时候应该考虑是想Comparable接口
(1)你写的类是一个值类(前面的文章介绍过)。
(2)类中有很明显的内在排序关系,如字母排序、按数值顺序或是时间等。
(3)前面两者是并且关系。
4. 如何很好的实现Comparable接口
(1)满足对称性。
即对象A.comparaTo(B) 大于0的话,则B.comparaTo(A)必须小于0;
(2)满足传递性。
即对象A.comparaTo(B) 大于0,对象B.comparaTo(Z)大于0,则对象A.comparaTo(Z)一定要大于0;
(3)建议comparaTo方法和equals()方法保持一致。
即对象A.comparaTo(B)等于0,则建议A.equals(B)等于true。
(4)对于实现了Comparable接口的类,尽量不要继承它,而是采取复合的方式。
(二)类和接口
1.使类和成员的可访问性最小化
为什么要使类和成员的可访问性最小化?
可以有效的解除系统中各个模块的耦合度、实现每个模块的独立开发、使得系统更加的可维护,更加的健壮。
2. 如何最小化类和接口的可访问性?
(1)能将类和接口做成包级私有就一定要做成包级私有的。
(2)如果一个类或者接口,只被另外的一个类应用,那么最好将这个类或者接口做成其内部的私有类或者接口。
3. 如何最小化一个了类中的成员的可访问性?
(1)首先设计出该类需要暴露出来的api,然后将剩下的成员的设计成private类型。然后再其他类需要访问某些private类型的成员时,在删掉private,使其变成包级私有。如果你发现你需要经常这样做,那么就请你重新设计一下这个类的api。
(2)对于protected类型的成员,作用域是整个系统,所以,能用包访问类型的成员的话就尽量不要使用保护行的成员。
(3)不能为了测试而将包中的类或者成员变为public类型的,最多只能设置成包级私有类型。
(4)实例域绝对不能是public类型的.2.在公有类中使用访问方法而非公有域
1.简单介绍
这一条的意思是让你这样做
public class MyObject{
private int filed01;
public int getFiled01() {
return filed01;
}
public void setFiled01(int filed01) {
this.fild01 = filed01;
}
}
而不要这样做:
public class MyObject{
public int filed01;
}
如果直接将类中的域暴露为共有域,那么你将失去这个域的控制权。
举个例子,对于上面例子中不好的用法,如果你以后想要在类中记录field01被赋值了多少次和被获取了多少次,你将无法做到,因为你根本不知道它什么时候在外部被获取,什么时候再外部被赋值。3.最小化可变性
1.什么是不可变类?
(1)需要的所有参数必须在实例化的时候都传进去。
(2)对象中所有信息在对象的整个生命周期中都保持不变。
2.使类不可变的原则
(1)不要提供任何修改对象状态的方法。
(2)保证类不会被继承。
(3)使所有的域都是final类型的。
(4)使所有的域都是私有类型的。
(5)确保对于任何可变组件的互斥性。意思就是,确保在该类的外部不会获取到该类中可变对象的引用。比如下面这个例子:
public class MyObject{
private final Listlist = new ArrayList<>;//可变对象
public ListgetList() {
return new ArrayList(list);
}
public void setList(Listlist) {
this.list = new ArrayList(list);
}
}
3.不可变类的优点
(1)不可变类简单。
不可变类只有一种状态,就是它被创建出来时候的状态,如果你要根据这个类进行一系列复杂操作,那么这个操作无论在什么时候结果都是相同的,所以你可以直接将结果缓存起来,在下一次执行同样操作的时候取出来,而不必再进行下一次操作。
(2)不可变类本质上是线程安全的,它不需要同步锁。
(3)对于不可变类,你永远都不需要实现拷贝方法。拷贝方法对它来说是没有意义的。
(4)不可变类可以被自由的共享。
4.不可变对象的缺点
(1)对于每一个不同的值都需要创建一个新的对象。
5.缺点的弥补办法
(1)先猜测一下经常用到哪些多步骤的操作,然后将它们作为基本数据类型提供。比如Integer,它将值为-128到127的对象缓存起来,但调用valueOf(int i)的时候,直接从缓存中拿,不用再重复创建提高效率。
(2)我们可以创建一个可变配套类。例如String是一个不可变类,它的可变配套类为StirngBuilder和StringBuffer。下面是一个例子,我们现在实现一个复数类,对外提供一个相加的方法如下:
public class Complex {
private final double re;//实部
private final double im;//虚部private Complex(double re, double im) {
this.re = re;
this.im = im;
}public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}public double realPart() {
return re;
}
public double imaginaryPart() {
return im;
}//复数相加
public Complex add(Complex c) {
return new Complex(re + c.re, im + c.im);
}
}
我们可以为他创建一个配套类:
public class ComplexBuilder {
private double re;
private double im;private ComplexBuider(double re, double im) {
this.re = re;
this.im = im;
}
public static ComplexBuider newInstance(Complex c) {
return new ComplexBuilder(c.realPart(), c.imaginaryPart());
}public void add(Complex c) {
this.re = this.re + c.realPart();
this.im = this.im + c.imaginaryPart();
}public Complex toComplex() {
return Complex.valueOf(this.re, this.im);
}
}
在客户端中我们如果需要用一个复数和另一个复数相加100次,我们如果不用ComplexBuilder的话就像下面这样,算上最开始穿件的两个实例,我们将会创建102个实例:
public class Test {
@Test
public void addNoBuiderTest() throws Exception{
Complex c1 = Complex.valueOf(1, 2);
Complex c2 = Complex.valueOf(2, 3);
for (int i = 0 ; i < 100 ; i++) {
c1 = c1.add(c2);
}
}
}
现在改用ComplexBuilder,现在我们只会创建4个实例:
public class Test {
@Test
public void addNoBuiderTest() throws Exception{
Complex c1 = Complex.valueOf(1, 2);
Complex c2 = Complex.valueOf(2, 3);
ComplexBuilder cb = ComplexBuider.newInstance(c1);
for (int i = 0 ; i < 100 ; i++) {
cb.add(c2);
}
c1 = cb.toComplex();
}
}
6.总结
(1)坚决不要为每个getter都生成setter。
(2)能将类做成不可变的就做成不可变的。
(3)一般比较小的值类都是需要做成不可变的。
(4)对于一些比较大的值类尽量考虑实现成不可变类。
(5)性能方面很有必要的时候才需提供配套类。
(6)如果真的不能作为不可变类,那就尽量限制其可变性
(7)对于一个类的初始化只能在构造器或是静态工厂中完成。也就是说类的初始化操作(赋值之类的)只能执行一次,就是在构造器或是静态工厂中。这一点参考java类库中的TimerTask类。4.组合优于继承
本条内容的继承不包括接口继承。
1.什么事复合
复合就是在你的类中添加一个私有域,引用一个类的实例,使被引用类成为引用类的一个组件。
2. 继承的缺点
(1)继承不容易控制,使用不当容易导致软件非常脆弱,特别是继承不再同一个包下的类。
(2)继承打破了父类的封装性,有的时候父类的内部实现改变,可能会导致子类遭到破坏。
举个比书上简单一点的例子,比如我们有个类,他包含一个集合,我们要并对外提供了两个api,分别是add(String str)和addAll(Liststrs),具体的类如下:
public class MyObject {
private Listlist = new ArrayList<>(); public void add(String ele) {
list.add(ele);
}public void addAll(List
elements) {
for(String ele : elements) {
add(ele);
}
}
}
然后我们需要记录这个类的从创建到销毁,一共添加过多少元素,如果我们想要用继承的方式,并且在不知道具体内部实现的前提之下,我们可能会这样写:
public class MyChildObject extends MyObject {
private int addedEleNum = 0;@Overried
public void add(String ele) {
addedEleNum++;
super.add(ele);
}@Overried
public void addAll(Listelements) {
addedEleNum += elements.size();
super.addAll(elements);
}
}
很明显,这样做是得不到我们想要的结果的,想要得到我们想要的结果,我们一般需要查看MyObject的具体实现,这就打破了封装性,好吧,看了具体实现之后我们知道怎么做了,那就是不覆盖addAll()方法。那问题又来了,如果在下一个版本中,MyObject的addAll()方法改了呢,改成想下面这样的:public void addAll(List
elements) {
for(String ele : elements) {
list.add(ele);
}
}
这样的话MyChildObject又不能正常工作了,OMG。导致子类不能正常工作的原因还有很多,甚至父类中新添加一个类似add()的方法都会导致子类不能正常工作。所以这样的子类是异常脆弱的。so,可以被继承的类要么在同一个包内(在同一个程序员的控制之下),要么是专门为继承而实际,并提供了很好的文档说明。
(3)看了第二点,你可能会觉得,我继承的时候只要不覆盖父类的方法不就可以了么?确实,相对于覆盖确实安全一些,不过这不是绝对安全,当父类新增了一个方法,并且方法名和和参数都和父类相同,但返回值不同,那么子类将无法通过编译。如果返回值也相同的话,又回到了第二个问题。同样导致了子类不健壮。
3.复合的优点
上面说到继承的缺点就是复合的优点。
4.复合的正确使用姿势
在这里需要先解释一下“转发“的概念,转发就是,你先复合一个类,然后在复合的类中实现所有被复合类的公有方法(api),实现的方式就是在相应的方法中调用被复合类的方法,并且不能被添加其他方法。比如为上面的MyObject写一个转发类:
public class ForwardingMyObject {
private MyObject mObject;//这里使用依赖注入的方式来得到被复合类的引用
//目的是提高可测试性和灵活性
public ForwardingMyObject(Myobject object) {
this.mObject = object;
}public void add(String ele) {
mObject.add(ele);
}public void addAll(List
elements) {
mObject.addAll(elements);
}
}
转发类就是上面提到的,专门为继承而设计的类。
现在来阐述复合的正确使用姿势:
(1)为想要被继承的类设计一个转发类。
(2)继承这个转发类。
(3)覆盖想要覆盖的方法,或者添加想要添加的方法。
将例子写完,我们来快乐的继承ForwardingMyObject吧:
public class MyChildObject extends ForwardingMyObject {
private int count = 0;public MyChildObject(MyObject object) {
super(object);
}@Override
public void add(String ele) {
count ++;
super.add(ele);
}@Override
public void addAll(Listelements) {
cout += elements.size();
super.addAll(elements);
}
}
为什么不直接在在转发类中直接实现计数功能?这样好麻烦!
好吧,我承认,上面的例子太简单,不利于解释这个问题,主要是为了便于理解,那我们继续。
首先问个问题,我们在设计一个类的api的时候是直接在类中写一堆public的方法么?
什么?是的,好吧,你这种没追求的程序员快滚去睡觉吧,我不想和你聊天T_T。
我们在设计一个类的api的时候一般都是先将类的接口写出来,然后在用这个类来实现这个接口。
行,明白这里我们就来看书里的栗子吧,这里我把set改成list,联系下上文中的栗子:
转发类:public class ForwardingList
implements List {
private ListmList;
public ForwardingList(Listlist) {
this.mList = list;
}
@Override
public void add(int location, E object) {
mList.add(location, object);
}
@Override
public boolean add(E object) {
return mList.add(object);
}
//其他的一些api
…
}
包装类(相当于上面例子中的MyChildObject):
public class InstrumentedList
private int addCount = 0;
public InstrumentedList(List
super(list);
}
@Override
public boolean add(E object) {
addCount++;
return super.add(object);
}
@Override
public void add(int location, E object) {
addCount++;
super.add(location, object);
}
@Override
public boolean addAll(@NonNull Collection<? extends E> collection) {
addCount += collection.size();
return super.addAll(collection);
}
@Override
public boolean addAll(int location, @NonNull Collection<? extends E> collection) {
addCount += collection.size();
return super.addAll(location, collection);
}
public int getAddCount() {
return addCount;
}
}
看到好处了么?现在我们写的InstrumentedList是一个真正List,不仅仅只是名字里有List而已!这意味着任何需要List作为参数的地方都可以把他传递过去!
不仅如此,它实现了一个传入List的构造方法,也就是说只要是实现了List的类都可以传递进去,什么ArrayList呀LinkedList都可以传进去并统计add了多少个元素。十分灵活!
5.复合的缺点
不适合用于回调框架。
6. 总结
简而言之,继承的功能十分强大,但也存在诸多问题,因为他违背了封装原则。只有当子类和超类确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类存在不同的包中,并且超类并不是为继承而设计的,那么继承将导致脆弱性,可以用复合和转转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。
5.要么为继承而设计,并提供文档说明,要么禁止继承
1.如何编写为继承而设计的类?
(1)对于public或是protected的方法(非final)或是构造器,在文档中要说明它们调用了哪些自己的public或是protected类型的方法(非final),并说明调用顺序,并说明每次调用的作用。
(2)文档编写完成,并发布新版本之后,后续的版本不能违背文档中的细节。
(3)构造器绝对不能调用可被覆盖的方法。
(4)对于实现了Cloneable接口的类,在clone方法中不能调用可被覆盖的方法。
(5)对于实现了Serializable接口的类,readResolve和writeReplace方法必须是protected的。
(6)对于实现了Serializable接口的类,在readObject方法中不能调用可被覆盖的方法。
2. 如何将类设计成不可继承的?
方法一:用final类型来申明这个类。
方法二:将类的构造器申明为private类型的,然后用静态工厂获取到这个类的实例。
3.总结
要设计一个专门为继承而设计的类是十分困难的,所以能用复合就用复合,如果需要被继承的类没有实现类型接口,不能实现复合包装,并且该类还是需要被继承的,那么请不要在可覆盖的方法中调用其它可覆盖方法,或者将想要覆盖的父类方法拷贝一份出来到子类中,做成私有方法,然后用这个私有方法替代调用super.method()(method指的是方法名)。
6.接口优于抽象类
![读书[笔记] - 图2](/uploads/projects/zjj1994@javabasic/a5fa3178927bb36eb915a16debbff0d9.png)
本条内容的主要重点——骨架类,集成接口和抽象类的优点于一身。
接口的优点
现有的类可以很容易的被更新,以实现新的接口。
如果你前期编写了一个类A,后期有在系统中加入了一个新的接口B,当你想让前期编写的类来实现这个接口,你只用加上一句implement B,然后在类A中实现里面的方法即可,不会影响到以前的类对类A的使用。
接口是定义mixin(混合类型)的理想选择。
这一条主要说的是,一个类可以实现多个接口。一个类除了可以实现一个它的主要类型接口之外,还可以加入一些辅助接口来实现一些新的功能。
接口允许我们构造非层次结构的类型框架。
之一条主要说的是,接口可以多继承。
层次结构的类型:
如上图,对于这种遵守参差结构的类型来说,我们要用类的继承来实现是容易的。但是我们遇到的类型关系并不总是遵循这种层次类型。
比如:web开发,我们有一种程序猿类型为“前端攻城狮”,一种类型为“后端攻城狮”还有一种是“web全栈攻城狮”,我们要用类来表示他们三者之间的三角关系,啊不对,是类型关系的话将会是一件十分困难的事情。但是如果用接口,将会十分轻松。直接定义三个类型的接口,让“web全栈攻城狮”同时继承前端和后端工程师即可。
接口可以使得类的增强变得安全。
这一点主要说的是第16条(复合优于继承)中的“包装类”。
骨架类
众所周知,java 8之前接口是不可以有方法体的,这就是抽象类相对于接口的优势,为了将抽象类和接口的优势整合起来,“骨架类”就诞生了,骨架类的做法是用一个抽象类来实现一个接口,在抽象类中为接口的某些方法提供实现。
骨架类的实现的一般步骤是,找出接口中的基本方法,在抽象类中声明为抽象方法,然后用这些基本方法来实现其他方法,所谓基本方法,就是通过将这些方法组合或是变换,可以实现其他的方法。
有关骨架类的例子
为了便于理解,这里举一个炒鸡简单的例子,想看更深层次的例子,可以去看java类库中的AbstractCollection
例子:
假设有一个接口,它可以实现一组对象的求和,代码如下:
public interface Summation
//实现两个对象的相加
T towEleAdd(T obj01, T obj02);
//实现List求和
T listEleSum(List
//实现数组求和
T arrayEleSum(T[] array);
}
根据观察,它的基本方法只有一个T towEleAdd(T obj01, T obj02);,现在我们可以来实现他的“骨架”了:
public abstract class AbstractSummation
@Override
public abstract T towEleAdd(T obj01, T obj02);
@Override
public T listEleSum(List
T firstEle = null;
for (T t : list) {
**if **(firstEle == **null**) {<br /> firstEle = t;<br /> **continue**;<br /> }
firstEle = towEleAdd(firstEle, t);<br /> }<br /> **return **firstEle;<br /> }
@Override
public T arrayEleSum(T[] array) {
T firstEle = null;
for (T t : array) {
**if **(firstEle == **null**) {<br /> firstEle = t;<br /> **continue**;<br /> }
firstEle = towEleAdd(firstEle, t);<br /> }<br /> **return **firstEle;<br /> }<br />}<br /> <br />继承这个骨架类就只用实现towEleAdd方法,就可以完成一组对象的求和工作了。<br /> <br /> <br />
7.接口只用于定义类型
总之,接口应该只被用来定义类开,它们不应该用来导出常量。
术语:
常量接口(constant interface):这种接口不包含任何方法,它只包含静态的final域,每个域都导出一个常量。
当类实现接口时,接口就充当可以引用这个类的实例类型。因此,类实现了接口,就表明客户端对这个类的实例可以实施某些动作。为了任何其他目的而定义的接口是不恰当的。
常量接口是对接口的一种不良使用。类在内部使用某些常量,纯粹是实现细节,实现常量接口,会导致把这样的实现细节泄露到该类的导出API中,因为接口中所有的域都是及方法public的。类实现常量接口,这对于这个类的用户来讲并没有实际的价值。实际上,这样做返回会让他们感到更糊涂,这还代表了一种承诺:如果在将来的发行版本中,这个类被修改了,它不再需要使用这些常量了,依然必须实现这个接口,以确保二进制兼容性。如果非final类实现了常量接口,它的所有子类的命名空间都受到了污染。Java平台类库中存在几个常量接口,如java.io.ObjectStreamConstants,这些接口都是反面典型,不值得效仿。
那既然不适合存在全部都是导出常量的常量接口,那么如果需要导出常量,它们应该放在哪里呢?如果这些常量与某些现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中,注意,这里说添加到接口中并不是指的常量接口。在Java平台类库中所有的数值包装类都导出MINVALUE和MAX_VALUE常量。如果这些常量最好被看作是枚举类型成员,那就应该用枚举类型来导出。否则,应该使用不可实例化的工具类来导出这些常量。
public class PhysicalConstants {
private PhysicalConstants() {}
**public static final double _AVOGADROS_NUMBER = 6.23156412e23;
public static final double BOLTZMANN_CONSTANT = 1.12588456e-23;
…
}
工具类通常要求客户端要用类名来修饰这些常量名。例如PhysicalConstants.AVOGADROS_NUMBER。如果大量利用工具类导出的常量,那么可以通过静态导入(static import)机制来避免用类名来修饰常量名。
// Use of static import to avoid qualifying constants
import static *effectivejava.PhysicalConstants.;
public class Test {
double atoms(double mols) {
return AVOGADROS_NUMBER = mols;
}
…
// Many more uses of PhysicalConstants justify static import
}
8.类层次优于标签类
术语:
标签类(tagged class):带有两种甚至更多风格的实例的类,并包含表示实例风格的标签域。
书中对于标签类,给了如下示例:
// Tagged class - vastly inferior to a class hierarchy!
class Figure {
enum Shape { RECTANGLE, CIRCLE };
// Tag field - the shape of this figure
final Shape shape;
// These fields are used only if shape is RECTANGLE
double length;
double width;
// This field is used only is shape is CIRCLE
double radius;
// Constructor for circle
public Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// COnstructor for rectangle
public Figure(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();
}
}
}
标签类存在着许多缺点:它们中充斥着样板代码,包括枚举声名、标签域及条件语句,破坏了可读性。内存占用也增加了,因为实例承担着属于其他风格的不相关的域。域不能做成是final的,除非构造器初始化了不相关的域,否则声名为final的域得不到机会初始化。构造器必须不借助编译器来设置标签域,并初始化正确的数据域,如果初始化了错误的域,程序就会在运行时失败。无法给标签类添加风格,除非可以修改它的源文件。如果一定要添加风格的话,就必须刻给每个可能的switch条件都添加一个case,否则类就会在运行时失败。最后,实例的数据类型没有提供任何关于风格的线索。一句话,标签类过于冗长、容易出错,并且效率低下。
标签类是对类层次的一种简单的仿效。既然如此,那么为了表示之种风格对象的单个数据类型,用子类化更为合适。为了将标签转变为类层次,首先要为标签中的每个方法都定义一个包含抽象方法的抽象类,每个方法的行为都信赖于标签值。在Figure中,area就是这样的方法。这个抽象类是类层次的根。同样的,如果所有的方法都用到了某些数据域,就应该把它们放到这个类中。接下来,为每种原始标签类都定义根类的具体子类,在前面的例子了,应该定义Circle和Rectangle两个子类,每个子类都包含特定于该类型的数据。同时每个子类中还包括针对根类每个抽象方法的相应实现。根据以下的修改原则,Figure的类层次表示如下:
// Class hierarchy replacement for a tagged class
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
public Circle(double radius) {
this.radius = radius;
}
double area() {
return Math.PI (radius radius);
}
}
class Rectangle extends Figure {
final double length;
final double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
double area() {
return length * width;
}
}
这个类层次纠正了前面提到过的标签类的所有缺点。这段代码简洁而清楚,没有包含在原来的版本中所见到的所有样本代码。每个类型的实现都配有自己的类,这些类都没有受到不相关域的拖累。所有的域都是final的。编译器确保每个类的构造器都初始化它的数据域,对于根类中声名的每个抽象方法,都确保有一个实现。这样就杜绝了因为遗漏switch case而导致运行时失败的可能。多个程序员可以独立的扩展层次结构。并且不用访问根类的源代码就能相互操作。每种类型都有一种相关的独立的数据类型,就是相应的子类类型,允许程序员指明变量的类型,限制变量,并将参数输入到特殊的类型。类层次的另一个好处在于,它们可以用来反映类型之间本质上的层次关系。有助于增强灵活性,并进行更好的编译时类型检查。
总之,标签类很少有适用的时候。当想要编写民一个包含显示标签域的类时,应该考虑一下,这个标签是否可以被取消,是否可以用类层次来代替。当遇到一个包含标签域的现有类时,就要考虑一下将它重构到一外类层次结构中去。
9.用函数对象表示策略
术语:
策略模式:策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。详见http://www.cnblogs.com/justinw/archive/2007/02/06/641414.html
函数对象:
Java没有提供函数指针,但是可以用对象引用实现同样的功能。调用对象上的方法通常是执行该对象上的某项操作。然而,也可能定义这样一种对象,它的方法执行其他对象上的操作,如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象。例如下而这个类:
class StringLengthComparator {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
在这里,指向StringLengthComparator对象的引用可以被当作是一个指向该对象内部比较器compare的“函数指针”,可以在任意一对字符串上被调用,StringLengthComparator实例是用于比较字符串比较操作的具体策略。对于这种具体策略类,它的所有实例在功能上是相互等价的,所以根据前面的原则,将它作成是Singleton是非常合适的。
class StringLengthComparator {
private StringLengthComparator() {}
public static final StringLengthComparator
INSTANCE = new StringLengthComparator();
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
但是,用这述这种方法有个问题,就是规定了参数的类型,这样就无法传递任何其他的比较策略。相反,对于这种情况,应该定义一个Comparator接口,并修改StringLengthComparator来实现这个接口。换句话说,在设计具体的策略类时,还需要定义一个策略接口:
// Strategy interface
public interface Comparator
public int compare(T t1, T t2);
}
当下,前面的具体策略类声名如下:
class StringLengthComparator implements Comparator
…
}
这样,在传递具体策略类的对象的时候,只需要将参数类型定为接口类型(使用接口做类型定义),现在可以传递其他的比较策略了。
具体策略类往往使用匿名类声名,如下:
Arrays.sort(stringArray, new Comparator
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
});
这里存在一个问题,就是在每次执行调用的时候都会创建一个新的实例。如果它被重复执行,那就应该考虑将函数对象存储到一个私有的静态final域里并重用它。这样做的另一个好处就是为这个函数对象取一个有意义的名子。
因为策略接口被用做所有具体策略实例的类型,所以我们并不需要为了导出具体策略而把具体策略类做成公有的。可以导出公有的静态域或者静态工厂方法,其类型是策略接口,具体的策略类可以是宿主类的私有嵌套类:
import java.io.Serializable;
import java.util.Comparator;
// Exporting a concrete strategy
class Host {
private static class StrlenCmp implements Comparator
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
// Returned comparator is serializable
public static final Comparator
}
总之,函数指针的主要作用就是实现策略模式,为了在Java中实现这种模式,要声名一个接口来表示策略,并且为每个具体策略声名一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声名和实例化这个具体策略。当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态final域或静态工厂方法导出,其类型为策略接口。
10.优先考虑静态成员类
术语:
嵌套类(nested class):指被定义在另一个类的内部的类。
嵌套类存在的目的应该只是为了它的外围类提供服务。如果嵌套类将来可能会用于其他的某个环境中,它应该是顶层类。嵌套类一共有四种:静态成员类、非静态成员类、匿名类、局部类。除了静态成员类外,其他的三种被称为内部类。
静态成员类应该被看作是普通的碰巧被饭锅在另一个类的内部的类,它可以访问外部类的所有成员。静态成员类是外围类的一个静态成员,遵守着可访问性规则。如果它被声名为私有的,它就只能在外围类的内部才可以被访问。这种类的一种常见的用法就是作为公有的辅助类,仅当它的外部类一起使用时才有意义。例如,一个描述了计算器支持的各种操作的枚举。Operation枚举应该是Calculator类的公有静态成员类,然后Calculator类的客户端就可以用诸如Calculator.Operation.PLUS这样的名称来引用这些操作。
非静态成员类的每个实例都隐含着对外围类的一个外围实例,在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用。如果嵌套类的实例可以在它外围类的实例之外单独存在,那么这个嵌套类就必须是静态成员类,否则它就必须与某个外围类的实例相关联。
当非静态成员类的实例被创建的时候,它和外围实例之间的关联关系也随之被建立起来,而且这种关系在以后也不能再被修改了。通常情况下,当在外围类的某个实例方法的内部调用非静态成员类的构造器时,这种关联关系就被自动建立起来。使用表达式enclosingInstance.new MemberClass(args)来手动建立这种关系也是有可能的。这种关联关系需要消耗非静态成员类实例的空间,并且增加了构造的时间开销。非静态成员类的一个常见的用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关的类的实例。比如说Map的keySet、entrySet等方法所返回的。
如果声名成员类不要求访问外围类的实例,那么就要始终把它声名为static的。如果忘记了static修饰符,那么每个实例都将包含一个额外的指向外围对象的引用。保存这份引用要消耗时间和空间,并且会导致外围实例在符合垃圾回收时却仍然得以保留(由于内部类的引用)。私有静态成员类的一种常见的用法是用来代表外围类所代表的对象的组件。如果相关的类是导出类的公有的或受保护的成员,那么在静态和非静态成员类之间做出正确的选择是非常重要的。在这种情况下,该成员类就是导出的API元素,因此也就面临了升级和兼容等一系列的问题。
匿名类不同于Java中其他任何语法单元,它没有名子。它不是外围类的一个成员,它并不与其他的成员一起被声名而是在使用的同时被声名和实例化。匿名类可以出现在代码中任何允许存在表达式的地方。当且仅当匿名类出现在非静态环境中时,它才有外围实例。但是即使它们出现在静态环境中,也不可能拥有任何静态成员。匿名类除了在它们被声名的时候是无法将它们实例化的。不能执行instanceof测试或者做任何需要命名类的其他事情。无法声名匿名类来实现多个接口,或者扩展一个类。匿名类的客户端也无法调用任何成员,除了从它的超类型中继承得到的之外。由于它们出现在表达式中,它们必须保持简短---大约10行或者更少些,否则会影响可读性。
局部类用的最少的嵌套类,在任何可以声名局部变量的地方都可以声名局部类。并且局部类也遵守着同样的作用域约定。总之,如果一个嵌套类需要在单个方法之外仍是可见的,或者它太长了,不适合放在方法内部,就应该使用成员类,如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的。否则就做成静态的。假设这个嵌套类属于一个方法的内部,如果只需要在一个地方创建实例并且已经有了一个预置的类型可以说明这个类的特征,那就做成匿名类,否则就做成局部类。
(三)泛型
1.请不要在新代码使用原生态类型
2.消除非受检警告
3.列表优先于数组
4.优先考虑泛型
5.优先考虑泛型方法
6.利用有限制通配符来提升API的灵活性
7.优先考虑类型安全的易购容器
(四)枚举和注解
(五)方法
1.检查参数的有效性
2.必要时进行保护性拷贝
3.谨慎设计方法签名
4.慎用重载
5.慎用可变参数
6.返回零长度的数组或者集合,而不是null
7.为所有导出的API元素编写文档注释
(六)通用程序设计
1.将局部变量的作用域最小化
2.for-each循环优先于传统的for循环
3.了解和使用类库
4.如何需要精确的答案,请避免使用float和double
5.基本类型优于装箱基本类型
6.如何其它类型更合适,则尽量避免使用字符串
7.当心字符串连接的性能
8.通用接口引用对象
9.接口优先于反射机制
10.谨慎地使用本地方法
11.谨慎地进行优化
12.遵守普通接受的命名惯例
(七)异常
1.只针对异常的情况才使用异常
2.对可恢复的情况使用受检异常,对编程错误使用运行时异常
3.避免不必要地使用受检的异常
4.优先使用标准的异常
5.抛出与抽象相对应的异常
6.每个方法抛出的异常都要有文档
7.在细节消息中包含能捕获失败的信息
8.努力使失败保持原子性
9.不要忽略异常
(八)并发
(九)序列化
1.谨慎地实现Serializable接口
2.考虑使用自定义的序列化形式
3.保护性地编写readObject方法
4.对于实例控制,枚举类型优先于readResolve
5.考虑用序列化代理代替序列化实例