文章目录
2 创建和销毁对象
第1条: 考虑用静态工厂方法代替构造器
public static Boolean valueOf(boolean b) {
return b? Boolean.TRUE : Boolean.FALSE;
}
静态工厂方法不是设计模式中的工厂。
服务提供者框架(Service Provider Framework):多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把它们从多个实现中解耦出来。
服务提供者框架有三个重要的组件:服务接口,这是服务提供者需要实现的;提供者注册API,将提供者注册到系统中的方法;服务访问API,客户端用来获取服务实例的方法。服务提供者接口,这些提供者负责创建其服务实现的实例。
// Service interface
public interface Service{
}
// Service provider interface
public interface Provider{
}
// Noninstantialble class for service registration and access
public class Services{
public static final String DEFAULT_PROVIDER_NAME = "<def>";
private Services() {}
// maps to service names to services
private statci final Map<String, Provider> providers = new ConcurrentHashMap<String, Provider>();
// provider registration api
public static void registerDefaultProvider(Provider p) {
registerProvider(DEFAULT_PROVIDER_NAME, p);
}
public static void registerProvider(String name, Provider p) {
providers.put(name, p);
}
// Services access api
public static Service newInstance() {
return newInstance(DEFAULT_PROVIDER_NAME);
}
public static Service newInstance(String name) {
Provider p = providers.get(name);
if (p == null) {
throw new IllegalArgumentException("No provider registered with name : " + name);
}
return p.newService();
}
}
valueOf——不太严格地将,此方法返回的实例与参数有相同的值。此方法本质上是类型转换方法
of——valueOf的简洁写法
getInstance——返回的实例是要根据参数进行决定的
newInstance——确保每个返回的实例都是与其他实例不同的。
getType——像getInstance一样,但是在工厂方法处于不同的类的时候使用。Type表示工厂方法所返回的对象类型。
newType——与newInstance一样。
第2条:遇到多个构造器参数时要考虑使用构建者Builder
重叠构造器:重叠构造器可行,但是当有很多参数的时候,客户端代码比较难编写,并且仍然难以阅读。一连串相同类型的参数会导致一些微妙的错误,如果客户端不小心颠倒了两个参数的位置,编译器也不会报错,但是在程宇运行的时候会出现错误的行为。
JavaBeans模式:只有无参构造器,然后提供setter方法来设置参数。JavaBeans模式本身存在严重的缺陷。(1)构造过程被分到了多个调用中,类构造过程中JavaBean很可能处于不一致的状态。类无法仅仅通过检验构造器参数的有效性来保证一致性。(2)JavaBean模式阻止了把类做成不可变的可能。这就需要程序员付出额外的努力来确保线程安全。
Builder模式。
第3条:用私有构造器或者枚举类型强化Singleton属性
从Java 1.5发行版起,可以通过编写一个包含单个元素的枚举类型来实现Singleton。
// Enum singleton
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {...}
}
这种方法与公有域方法相近,但是它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化(面对复杂的序列化或者反射攻击的时候也能保证单例)。虽然没有被广泛采用,但是单元素的枚举类型已经成为实现singleton的最佳方法
第4条:通过私有构造器强化类不可实例化的能力
public class UtiltyClass {
// suppress default constructor for noninstantiability
private UtilityClass() {}
}
第5条:避免创建不必要的对象
String s = new String("a string") // don not do this
这样创建了一个新的实例。String s = "s string"
就只有一个String实例。
除了重用不可变的对象之外,也可以重用那些已知不会被修改的对象。
/**
* 判断一个人是否是出生于婴儿潮
**/
class Person {
private final Date birthDate;
/**
* The starting and ending dates of baby boom
**/
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
Calender gmtCal = Calender.getInstance(TimeZone.getTimeZome("GMT"));
gmtCal.set(1946, Calender.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCal.getTime();
gmtCal.set(1946, Calender.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0
&& birthDate.compareTo(BOOM_END) < 0;
}
}
Person类的Calender、TimeZone和Date实例只在初始化的时候创建一次,而不是在每次调用isBabyBoomer方法的时候创建一次。
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum)
}
虽然答案正确,但是速度会慢很多。sum被声明为Long,意味着程序多余构造了2^31个Long实例。
“对象池”中的对象都是非常重量级的。使用对象池的典型案例就是数据库连接池,建立数据库连接的代价是非常昂贵的,并且数据库会限制连接的个数。但是,一般而言,维护自己的对象池必定会把代码弄得很乱,同时增加内存占用,并且还会损害性能。JVM的gc机制的性能优化有可能很容易就超过对象池的性能。
第6条:消除过期的对象引用
内存泄漏的一个例子
package java.effective;
import java.util.Arrays;
import java.util.EmptyStackException;
// 有内存泄漏的一个类
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];
elements[size] = null; //清除过期引用
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
内存泄露出现的三种场景:
(1)类自己管理内存
此种情况,一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空
(2)缓存
对象丢到缓存里面,它就很容易被遗忘掉。
解决方案:<1> WeakHashMap。只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以使用WeakHashMap代表缓存。当所有要缓存的项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。<2> 起后台线程时不时地清除掉没用的项(Timer或者ScheduledThreadPoolExecutor)或者给缓存添加条目的时候顺便进行清理(LinkedHashMap利用它的removeEldestEntry方法可以轻松实现)。 对于更加复杂的缓存,必须直接使用java.lang.ref。
(3)监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显示地取消注册,除非你采取某些动作,否则它们就会积聚。确保回调被立即当做垃圾回收的最佳方法是只保存它们的弱引用(weak reference),例如只将它们保存成WeakHashMap中的键。
第7条:避免使用终结方法(finalize方法)
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。finalize方法会导致行为不稳定、降低性能,以及可移植性的问题。
finalize方法的缺点是不能保证会被及时地执行。从一个对象变得不可到达开始,到它的终结方法被执行,所花费的这段时间是任意长的。注重时间(time-critial)的任务不应该由finalize方法来完成。
什么时候执行finalize方法,不同的jvm的实现中大相径庭。如果程序依赖finalize方法被执行的时间点,这个程序在不同的jvm上的表现会不同。
finalize方法线程的优先级比应用程序的其他线程的优先级要低得多。
Java语言规范不仅不保证finalize方法是否会被及时地执行,而且不保证它们是否会被执行。存在这种情况:当一个程序终止的时候,某些已经无法访问的对象上的finalize方法却根本没有被执行。不应该依赖finalize方法来更新重要的持久状态。比如依赖finalize方法来释放共享资源上的永久锁,很容易让整个分布式系统垮掉。
System.gc和System.runFinalization这两个方法增加了finalize方法被执行的机会,但是并不能保证finalize方法一定会被执行。
如果在终结方法中出现了异常,不会被抛出,甚至连告警都不会打印出来。出现了异常的时候,对象的终结过程也会终止,对象处于被破坏的状态(a corrupt state),如果另一个现成企图使用这种被破坏的对象,则会发生任何不确定的行为。
终结方法有非常严重的(Severe)性能损失。
【sk总结】
finalize方法的坏处:
(1)执行时间不确定
(2)是否会被执行也不确定
(3)函数内部不抛出异常
(4)性能极差
建议做法:提供显示的终止方法。典型例子:InputStream, java.sql.Connection等的close方法。显示的终止方法通常和try-finally结构结合起来使用,以确保及时终止。
终结方法的好处:
(1)如果忘记调用显示终止方法时,finalize方法可以作为兜底机制。迟一点释放资源总比不释放资源的好。如果finalize方法发现资源还未被终止,应该在日志中增加一条告警。 这是bug,应该被修复。FileInputStream等类都具有finalize方法。
(2)本地对等体(native peer)是一个本地对象(native object),普通对象通过本地方法委托给一个本地对象。Java gc的时候不会回收本地对等体。 如果本地对等体不拥有关键的资源,可以通过finalize方法进行回收。
“终结方法链”不会被自动执行。如果类有终结方法,并且子类覆盖了终结方法,子类的终结方法必须手动调用超类的终结方法。推荐做法:try块中终结子类,finally中调用超类的终结方法。
// Manual finalizer chaining
@Override
protected void finalize() throws Throwale {
try {
// Finalize subclass state
} finally {
super.finalize()
}
}
防止忘记调用超类的finalize方法:把终结方法放在一个匿名类中,匿名类的唯一用途就是释放它的外围实例(enclosing instance)。该匿名类的单个实例被称为终结方法守卫者(finalizer guardian)。
// Finalizer Guardian idiom
public class Foo {
//Sole purpose of the object is to finalize outer Foo Object.
private final Object finalizerGuardian = new Object() {
@Override
protected void finalize() throws Throwale {
// Finalize the foo object
}
}
}
3 对于所有对象都通用的方法
第8条:覆盖equals时要遵守通用约定
不覆盖equals方法的情况(类的每个实例都只与它自身相等):
(1)类的每个实例本质上都是唯一的。对于代表活动的而不是值(value)实体确实是每个实例都是唯一的。例如Thread
(2)不关心类是否提供了“逻辑相等(logical equality)”的测试功能。例如,java.util.Random覆盖了equals,以检查两个Random实例是否产生相同的随机数序列,但是设计者并不认为客户需要或者期望这样的功能。此类情况,就使用Object的equals方法就足够了。
Random类中没有覆写equals方法
(3)超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。例如,大多数的Set都从AbstractSet继承equals实现,List继承AbstractList的,Map继承AbstractMap的
// AbstractList的equals方法就是比较元素是否相等。
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof List))
return false;
ListIterator<E> e1 = listIterator();
ListIterator<?> e2 = ((List<?>) o).listIterator();
while (e1.hasNext() && e2.hasNext()) {
E o1 = e1.next();
Object o2 = e2.next();
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
return !(e1.hasNext() || e2.hasNext());
}
(4)类是私有的或者包级私有的,可以确定其equals方法永远不会被调用。这种情况可以通过覆写,防止equals方法被意外调用。
@Override
public boolean equals(Object o) {
throw new AssertionError(); // method is never called
}
一般“值类”需要覆写equals方法。 但是有特例:实例受控的类(每个值至多只存在一个对象),枚举类型属于此列。此类情况,逻辑等同和对象等同是一回事。
【覆盖equals方法的约定】
equals方法实现了等价关系(Equivalence Relation,离散数学里面有这个概念)。
(1)自反性:x.equals(x)为true
(2)对称性:y.equals(x)为true时,x.equals(x)为true
违反对称性的一个例子
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
if (s == null) {
throw new NullPointerException();
}
this.s = s;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
}
/**
* 违反对称性:虽然CaseInsensitiveString类中的equals方法知道String对象,但是String对象并不知道CIS对象。
*/
if (obj instanceof String) {
return s.equalsIgnoreCase((String) obj);
}
return super.equals(obj);
}
public static void main(String[] args) {
CaseInsensitiveString caseInsensitiveString = new CaseInsensitiveString("Polish");
String s = "polish";
System.out.println(caseInsensitiveString.equals(s)); // true
System.out.println(s.equals(caseInsensitiveString)); // false
}
}
(3)传递性:如果x.equals(y)为true,且y.equals(z)为true,则x.equals(z)为true
违反的情况通常发生在子类扩展超类的时候,添加值组件(Value Component)。
package book.effective;
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// @Override
// public boolean equals(Object obj) {
// if (!(obj instanceof Point)) {
// System.out.println("!(obj instanceof Point)");
// return false;
// }
// Point p = (Point) obj;
// System.out.println("p.x = " + p.x);
// System.out.println("x = " + x);
// System.out.println("p.y = " + p.y);
// System.out.println("y = " + y);
// return p.x == x && p.y == y;
// }
// 只有对象有相同的实现的时候,对象才相同。结果是无法接受的。
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != getClass()) {
return false;
}
Point p = (Point) obj;
return p.x == x && p.y == y;
}
}
package book.effective;
public class ColorPoint extends Point {
private final int color;
public ColorPoint(int x, int y, int color) {
super(x, y);
this.color = color;
}
// 1 如果不提供equals方法,则忽略掉了颜色信息
// @Override
// public boolean equals(Object obj) {
// if (!(obj instanceof ColorPoint)) {
// return false;
// }
// return super.equals(obj) && ((ColorPoint)obj).color == color;
// }
// public static void main(String[] args) {
// Point p = new Point(1,2);
// ColorPoint colorPoint = new ColorPoint(1, 2, 1);
//
// // 忽略了颜色信息
// System.out.println(p.equals(colorPoint));
// // 恒为false,因为类型不匹配。
// System.out.println(colorPoint.equals(p));
// }
// /**
// * 尝试在混合比较的时候忽略颜色信息
// *
// * @param obj
// * @return
// */
// @Override
// public boolean equals(Object obj) {
// if (!(obj instanceof Point)) {
// return false;
// }
//
// if (!(obj instanceof ColorPoint)) {
// return obj.equals(this);
// }
//
// return super.equals(obj) && ((ColorPoint) obj).color == color;
// }
//
// /**
// * 这种违反了传递性
// *
// * @param args
// */
// public static void main(String[] args) {
// ColorPoint p1 = new ColorPoint(1,2,1);
// Point p2 = new Point(1,2);
// ColorPoint p3 = new ColorPoint(1,2,2);
// System.out.println(p1.equals(p2));
// System.out.println(p2.equals(p3));
// System.out.println(p1.equals(p3));
// }
}
package book.effective;
import java.util.concurrent.atomic.AtomicInteger;
public class CounterPoint extends Point {
private static final AtomicInteger counter = new AtomicInteger();
public CounterPoint(int x, int y) {
super(x, y);
counter.incrementAndGet();
}
public int numberCreated() {
return counter.get();
}
}
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class UnitCircleTester {
private static final List<Point> unitCircle;
static {
unitCircle = new ArrayList<>();
unitCircle.add(new Point(1, 0));
unitCircle.add(new Point(0, 1));
unitCircle.add(new Point(-1, 0));
unitCircle.add(new Point(0, -1));
}
public static boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
}
public static void main(String[] args) {
/**
* 使用了getClass的equals方法,返回的都是false
*/
System.out.println(onUnitCircle(new CounterPoint(1, 0)));
}
}
复合:既可以扩展不可实例化的类,有可以增加值组件。是一种不错的权宜之计。
package book.effective;
public class ColorPoint {
private final Point point;
private final int color;
public ColorPoint(int x, int y, int color) {
point = new Point(x, y);
this.color = color;
}
/**
* 共有视图方法
*
* @return
*/
public Point asPoint() {
return point;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ColorPoint)) {
return false;
}
ColorPoint colorPoint = (ColorPoint) obj;
return colorPoint.asPoint().equals(point) && colorPoint.color == color;
}
}
(4)一致性:多次equals判断的结果应该一样。
(5)对于任何非null的引用x,x.equals(null)必须返回false。
【实现高质量equals方法的诀窍】
(1)使用操作符检查“参数是否为这个对象的引用”。是一种性能优化
(2)使用instanceof判断“参数是否为正确的类型”。
(3)把参数转换成为正确的类型。因为instanceof判断过了,所以必定会成功。
(4)对于该类中的每个“关键域”,检查参数中的域是否与该对象中对应的域相匹配。
非浮点数的基本类型域,可以使用操作符进行比较;
对于对象引用域可以递归地调用equals方法;
float和double,可以使用Float和Double的compareTo方法;
对于数组域,要把这些原则应用到每个元素上。
(5)一定要检验equals的自反性、对称性、传递性、一致性和非空性。
- 覆盖equals时总要覆盖hashCode
- 不要让equals方法过于智能。
不要过度地寻求各种等价关系。例如File类不应该把指向同一个文件的符号链接(symbolic link)当作相等的对象来看待。 - 不要将equals声明中的Object对象替换为其他的类型。
如果改了类型,就没有覆盖Object.equals类,而是提供了一个重载方法。
第9条 覆盖equals时总要覆盖hashCode方法
【Object规范中对于hashCode方法的描述】
(1)在应用程序执行期间,只要对象的equals方法的比较操作用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
(2)如果两个对象根据equals方法比较是相等的,那么调用这两个对象中任一个对象的hashCode方法都必须产生同样的整数结果。
(3)如果两个对象根据equals方法比较是不相等的,那么调用这两个对象中任一个对象的hashCode方法不一定要产生不同的整数结果。
但是coder应该知道,产生不同的结果会提升散列表的性能。
如果没有覆盖hashCode方法会违反约定的第二条。
package book.effective.rule9;
import java.util.HashMap;
import java.util.Map;
public class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix, int 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) {
if (obj == this) {
return true;
}
if (!(obj instanceof PhoneNumber)) {
return false;
}
PhoneNumber phoneNumber = (PhoneNumber) obj;
return phoneNumber.lineNumber == lineNumber
&& phoneNumber.prefix == prefix
&& phoneNumber.areaCode == areaCode;
}
public static void main(String[] args) {
Map<PhoneNumber, String> map = new HashMap<>();
map.put(new PhoneNumber(707, 867, 5309), "Jenny");
// 如果没有覆写hashCode方法,map的get就会返回null
System.out.println(map.get(new PhoneNumber(707, 867, 5309)));
}
}
好的散列函数倾向于“为不相等的对象产生不相等的散列码”。hash函数把不相等的实例均匀地分不到所有可能的散列值上,这种理想情况实现比较困难。现提供简单实现办法:
- 把非零常数值(例如17,这个是任选的数值)保存在一个名为result的int型变量中。
- 对于对象中的每个关键域f,完成以下步骤:
a.为该域计算int类型的散列码c
i. 如果是boolean类型,计算(f ?1:0)
ii. 如果f是byte char short或者int类型,则计算(int)f
iii. 如果f是long类型,则计算(int)(f^(f>>>32));
iv. 如果是float类型,计算Float.floatToIntBits(f)
v. 如果是double类型,Double.doubleToLongBits之后用iii的方法处理
vi. 如果是对象引用,如果equals方法是递归比较的,hashcode也是要递归计算的。如果更复杂,需要提供一个范式(canonical representation),然后针对范式调用hashCode方法。如果为null,返回0
vii. 如果是数组,对每一个元素利用上述规则,然后累加每个元素的hash值。
b. result = 31 * result + c; 选择31是因为31是一个奇素数,并且31有一个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能。例如:31 * i == (i << 5) - i; - 返回result
- 测试。“相等的实例应该有相等的散列码”
【在散列码的计算过程中,可以把冗余域(redundante field)排除在外。必须排除equals比较计算中没有用到的任何域】
PhoneNumber的hashCode方法
@Override
public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
如果一个类不可变,并且计算散列码的开销也比较大,可以考虑把散列码缓存在对象内部。
// Lazy initialized, cached hashCode
private volatile int hashCode;
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
hashCode = result;
}
return result;
}
不要试图从散列码计算中排除掉一个对象的关键部分来提高性能。
第10条 始终要覆盖toString
第11条 谨慎地覆盖clone
@Override
public PhoneNumber clone() {
try {
// 体现了一条通则:永远不要让客户去做任何类库能够替客户完成的事情。
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
java1.5之后引入了协变返回类型(covariant return type)作为泛型。换言之,目前覆盖方法的返回类型可以是被覆盖方法的返回值的子类了。
实际上,clone方法就是另一个构造器;你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件(invariant)。
简言之,所有实现了Cloneable接口的类都应该用一个public的方法覆盖clone。此公有方法首先调用super.clone,然后修正任何需要修正的域(一般而言,这意味着拷贝任何包含内部“深层结构”的拷贝对象,并用指向新对象的引用代替原来指向这些对象的引用)。
另外的实现对象拷贝的好办法是提供拷贝构造器(copy constructor)或者拷贝工厂(copy factory)。
第12条 考虑实现Comparable接口
【compareTo方法应该满足的几个条件】
(1)sgn(x.compareTo(y)) == -sgn(y.compareTo(x)). 当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)也应该抛出异常。
(2)可传递性:x.compareTo(y) > 0 && y.compareTo(z) > 0 ==> x.compareTo(z) > 0
(3)x.compareTo(y) == 0 ==> 对于任意的z,sgn(x.compareTo(z)) == sng(y.compareTo(z))
(4)强烈建议:(x.compareTo(y) == 0) == (x.equals(y))
如果遵守了这一条,那么由compareTo方法所施加的顺序关系就被认为“与equals一致(consistent with equals)”,如果违反了这个规则,顺序关系就被认为“与equals不一致(inconsistent with equals)”