一.考虑使用静态工厂方法代替构造器
优点:①静态方法有合适的名字。
②不必在每次调用他们的时候都创建一个新对象
③静态方法可以返回类型的任何子类型对象。
④创建参数化类型实例的时候,它们使代码变得更加简洁
服务提供者框架中有三个重要的组件:服务接口(提供者实现的),提供者注册API(系统用来注册实现的), 服务访问API(客户端用来获取服务的)
缺点:①如果不含公有的或者受保护的构造器,就不能被子类化。
②它们与其他的静态方法实际上没有任何区别。
静态方法常用的名称:valueOf, of, getInstance, newInstance, getType, newType
二.遇到多个构造器参数时要考虑用构建器
重构构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,并且仍然较难以阅读。
第一种方式是重载多个构造函数
第二种方式是JavaBeans模式(缺点是在构造过程中JavaBean可能处于不一致的状态,javaBean模式阻止了把类做成不可变的可能)
第三种方式是Builder模式,使用构造器或者静态工厂生成对象,然后设置每个相关的可选参数。
平时使用Builder模式的时候,我们可以事先创建一个Builder泛型接口,然后每次都继承Builder接口
传统的抽象工厂是使用Class对象的newInstance方法充当builder的一部分, 然而newInstance总是试图调用无参的构造函数,可能产生异常。
builder的缺点:为了创建对象,必须先创建它的构造器,有一定的性能损耗。
三.用私有构造器或者枚举类型强化Singleton属性
Singleton就是我们常说的单例,代表那些本质上是唯一的组件。
①可以将构造器设为私有的,避免其违反Singleton的规则。如果使用反射机制,反射会忽略权限,
可能也会使用私有的构造函数,此时我们可以加上一些我们的判断。
②公有的成员是个静态工厂方法,公有的静态域是final的,所以该域总是包含相同的对象引用,
使用这种方法不会带来性能上的损失,因为虚拟机已经实现了静态
工厂方法的内联调用
为了保证Singleton,必须声明所有实例域都是瞬时的(transient),并且提供readResolve方法。
③枚举方式
四.通过私有构造器强化不可实例化的能力
有时用java实现算法题的时候编写只包含静态方法和静态域的类,这是一种很不好的习惯。
但是有时工具类也需要这样,这些类是希望不被实例化的。
企图通过将类做成抽象类来强制该类不能被实例化,这是行不通的,这些类可以被子类化。
解决方法:让类中所有的构造器都编程私有的(谨记,系统提供的缺省构造器是显式的)
辅作用:该类不能被子类化。
五.避免创建不必要的对象
如果是常量的话,可以采用字面常量,这样使用常量池,不会每次都创建新的对象。
如果是对象的话,通常可以使用静态工厂方法而不是构造器。
适配器:它把功能委托给一个后备对象,从而为后备对象提供一个可以替代的接口
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱会导致性能的降低。
对象池:一般用来装载重量级的对象,真正正确使用对象池的典型对象示例就是数据库连接池。
这里不能和保护性拷贝搞混,因为在那里重用的话可能会导致潜在的错误和安全漏洞。
六.消除过期的对象引用
如果一个对象引用被无意识地保留起来,那么垃圾回收机制不仅不会处理这个对象,而且不会垂柳被这个对象引用的所有其他对象
解决方式:一旦对象引用已经过期,只需清空这些引用即可。
eg: public Object pop() {
if(size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // this is necessary.
return result;
}
消除过期引用的最好的方法是:在最紧凑的作用于范围内定义每一个变量,这种情况会自然而然的发生。
如果类是自己管理内存的,程序员就应该警惕内存泄露问题。
缓存也是造成内存泄露的一个原因,常用的解决方式是定期的清理缓存。
内存泄露的第三个常见来源是监听器和其他回调。你实现了一个API,客户端在这个API中注册回调,却没有显示地取消注册,除非你采取某些措施
确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用。
七.避免使用终结方法
终结方法是不可预测的,也是很危险的,一般情况下不必要的。
在java一般使用try-catch的方式来完成c++析构函数的功能
终结方法被调用不是即使,并且终结方法的队列优先级比较低,并且终结方法依赖于JVM平台。
图形对象的终结速度达不到它们进入队列的速度可能造成超内存的情况
java规范不仅不保证终结方法会被及时地执行,而且根本就不保证他们会被执行。
tip:不要依赖终结方法来更新重要的持久状态。
System.gc System.runFinalization这两个方法增加了终结方法被执行的机会,但是也并不保证终结方法一定会被执行。
如果未被捕获的异常在终结过程中被抛出来,那么这种异常可能被忽略,并且该对象的终结过程也会终止。
终结方法有严重的性能损失。
①如果想将封装的资源终止,只需要提供一个显示的终止方法。
终结方法通常和try-finally结构结合起来使用,确保及时终止。
如果使用终结方法记住要调用父类的终结方法。
终结方法链是不会被自动执行的,如果类有终结方法,并且子类覆盖了终结方法,子类的终结方法就必须手工调用超类的终结方法
八.覆盖equals方法遵守通用约定
覆盖equals的时机:
如果类具有自己特有的“逻辑相等”概念,并且超类没有覆盖equals以期望的行为。
对于那些每个值至多只存在一个对象的情况,无需覆盖equals方法,因为逻辑相等就等于对象相同。
equals的等价关系:
①自反性 ②对称性 ③传递性 ④一致性 ⑤x非空,x.equals(null)必须返回false
关于Point, ColorPoint的问题可以显示出一个问题:我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定。
平时实现equals时都会首先用instanceof进行判断。
实现equals方法时需要注意里氏替换原则,一个类型的任何重要属性也将适用于它的子类型。
无论类是否可变,都不要让equals方法依赖于不可靠的资源
equals方法的诀窍:
①使用==操作符检查参数是否为这个对象的引用。
②使用instanceof操作符检查“参数是否为正确的类型”
③把参数转换成正确的类型。
④对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。
注意:基本类型中除了float和double使都用==操作符,对象使用equals,float和double使用Compare方法。
对于部分允许为空的域可以采用下面的代码来进行优化:
field == null ? o.field == null : field.equals(o.field))
为了获得高性能,应该最先比较最有可能不一致的域,或者是低开销的域。
覆盖eqeuals时总要覆盖hashCode方法
九.覆盖equals时总要覆盖hashCode方法
如果不这样做的话,很可能会违反Object,hashCode的通用约定,使该类无法结合所有基于散列点的集合一起正常工作。
如果两个对象equals方法比较是相等的,那么调用者两个对象中任意一个对象的hashCode方法都必须产生同样的结果。
如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果
Hash中,可以将与每个项关联点的散列码缓存起来,如果散列码不匹配,也不必检验对象的等同性。
hashCode方法的实现的准则:
①把某个非零的常数值,保存到一个名为result的int类型变量中
②对于对象中每个关键域f,完成以下步骤
(1)如果该域是boolean类型,则计算(f ? 1 : 0)
(2)如果该域是byte,char,short或者是int类型,则计算(int)f
(3) 如果该域是long类型,则计算(int)(f ^ (f >> 32))
(4) 如果该域是float,则计算Float.floatToIntBits(f), double一样
(5) 如果是对象则递归调用equals方法
(6) 如果该域是一个数组,则要把每一个元素当做单独的域来处理
按照下面的公式,计算得到的散列码c合并到result中:
result = 31 + result + c
返回result
在散列码的计算过程中,可以把冗余域排除在外,必须排除equals比较计算中没有用到的任何域。
平时我们可以使用31来代替以为操作(31为低位全1,可以防止信息会丢失)
如果一个类是不可变的, 并且计算散列码的开销比较大,就应该考虑把散列码缓存在对象内部
十.始终要覆盖toString方法
Object提供了toString的实现 包含类的名称 + @ + hashCode(16进制的表示)
无论是否指定格式,都为toString返回值中包含的所有信息,提供一种编程式的访问途径
十一.谨慎地覆盖clone
Cloneable接口的目的是表明这样的对象允许克隆,其主要的缺陷在于它缺少一个clone方法,
Cloneable方法中没有任何方法,它是为了改变超类中受保护的方法的行为。
拷贝的精确含义: x.clone() != x x.clone().getClass() == x.getClass() x.clone().equals(x)
super.clone只是浅层次的拷贝
注意:如果elements域是final的,上述方案就不能正常工作,因此在super.clone之后elements的值是不可改变的,
所以,如果想让类是可克隆的,可能有必要从某些域中去掉final修饰符。
克隆复杂对象的最后一种方法是:先调用super.clone, 然后把对象中的所有域都设置成它们的空白状态,然后调用搞成的克隆函数
总结:所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone方法,此公有方法首先调用super.clone,然后修正任何需要修正的域
如果你扩展了实现了Cloneable接口的类,那么你除了实现一个行为良好的clone方法外,没有别的选择
拷贝的另外一种实现是:一个实现对象拷贝的好方法是提供一个拷贝构造器或拷贝工厂。
eg: public Yum(Yum yum); public static Yum newInstance(Yum yum);
上面的两种方法都要比Cloneable/clone方法具有更多的优势。
十二.考虑实现Comparable接口
一旦类实现了Comparable接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作
由compareTo方法施加的等同性测试,也一定遵守自反性,对称性和传递性
TreeSet和TreeMap中使用的比较就是compareTo方法
如果一个域并没有实现Comparable接口,或者你需要使用一个非标准的排序关系,就可以使用一个显示的Comparator来代替
compareTo方法约定并没有指定返回值的大小,可以利用这一点来进行简化代码(这里需要注意别超过int的最大范围)
eg: public int compareTo(PhoneNumber pn) {
int areaCodeDiff = areaCode - pn.areaCode;
if(areaCodeDiff != 0)
return areaCodeDiff;
//...
return 0;
}
十三.使类和成员的可访问性最小化
又名信息隐藏或封装
信息隐藏之所以重要的原因:他可以有效的解除组成系统的各模块之间的耦合关系
规则1:尽可能使每个类或者成员不被外界访问,只有两种可能的访问级别:包级私有的和公有的
可访问性的递增顺序:
①私有的(private): 私有的——只有声明该成员的顶层类才可以访问这个成员
②缺省: 该成员的包内部的任何类都可以访问这个成员
③受保护的(protected): 声明该成员的类的子类可以访问这个成员
④公有的(public): 在任何地方都可以访问该成员
当你仔细设计了类的公有API之后,可能觉得应该把所有其他的成员都变成私有的
访问性的规定:如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别,
这样可以确保任何可使用超类的实例的地方也都可以使用子类的实例
为了便于测试,一般将级别声明为包级私有的,可以让测试作为被测试的包的一部分来运行,从而能够访问它的包级私有元素
实例域不能是公有的,一旦将其公有话,就放弃了对存储在这个域中的值进行限制的能力
长度非零的数组总是可变的,所以,类具有公有的静态final数组域,这几乎是错的。
十四.在公有类中使用访问方法而非公有域
这也就是平常使用的setter getter模式
如果类可以在它所在的包的外部进行访问,就提供访问方法
十五.使可变性最小化
为了使类称为不可变类,要遵循下面五条规则:
1.不要提供任何修改对象状态的方法
2.保证类不会被扩展
3.使所有的域都是final的
4.使所有的域都称为私有的
5.确保对于任何可变组件的互斥访问
函数式的做法可以保证对象的状态不会被改变
过程的或者命令式的做法可能会导致它的状态发生改变
如果一个对象具有不可变性,可以将其做成一个不可变对象,这会带来大量的好处
不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。
十六.复合优先于继承
子类脆弱的原因(overriding中出现的问题):
①与方法调用不同,继承打破了封装性,子类依赖于其超类中特定功能的实现细节,因为在子类中可能调用非public的方法
②超类在后面的发型版本中增加新的方法,而子类无法改变具体实现了,只能使用旧版本的方法实现。
上面的这些问题可以通过改用复合的方式得以解决,新类中的每个实例方法都可以调用所包含的组件中的对应方法,这被称之为转发。
用继承的时机:当两者之间真正满足“is - a”关系的时候才会使用继承
十七.要么为继承而设计,并提供文档说明,要么就禁止继承
好的API文档应该秒速一个给定的方法做了什么工作而不是描述其是如何做到的
为了允许继承,类还必须遵守其他一些约定。构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用
为了继承而设计类的时候,Cloneable和Serializable接口出现了特殊的困难,无论实现着其中的哪个接口通常都不是好主意
因为clone和readObject方法在行为上非常类似于构造器,所以类似的限制规则也是使用的, 无论是clone还是readObject,都不能调用可覆盖的方法
对于普通的具体类,它们既不是final的,也不是为了子类化而设计和编写文档的,这种状态很危险
这个问题的最佳解决方案是,对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化
十八.接口优于抽象类
接口和抽象类这两种机制都用用来允许多个实现的类型
接口的优点:
①抽象类会间接伤害到类层次
②接口是定义mixin的理想选择(mixin是允许任选的功能可被混合都类型的主要功能=中)
③接口允许我们构造非层次结构的类型框架
骨架实现上有个小小的不同,就是简单实现,AbstractMap,SimpleEntry就是个例子
eg:SimpleEntry, AbstarctMap 简单实现就像个骨架实现,因为它实现了接口,并且是为了继承而设计的,他是最简单的可能的有效实现
你可以原封不动的使用,也可以看情况将它子类化
抽象类的优势:抽象类的演变比接口的演变要容易得多
十九.接口只用于定义类型
一旦类实现了某个接口,就表明客户端可以对这个类的实例实施某些动作
如果要导出常量的话,常量接口模式是对接口的不良使用,它的所有子类的命名空间会被接口中的常量所“污染”
导出常量的合理方案:
①如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中
②如果这些常量最好被看作枚举类型的成员,就应该用枚举类型来导出这些常量
③使用不可实例化的工具类来导出这些常量
接口应该制备用来定义类型,它们不应该被用来导出常量
二十.类层次优于标签类
面向对象就提供了其他更好的方法来定义能表示多种风格对象的单个数据类型:子类化
为了将标签转变成类层次,首先要为标签类中的每个方法都定义一个包含抽象方法的抽象类,这每个方法的行为都依赖标签值
类层次的另一个好处在于,它们可以用来反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查
二十一.用函数对象表示策略
c语言标准库中的qsort函数要求用一个指向comparator函数的指针作为参数,比较函数有两个参数,返回值根据两者的大小情况而定
java中的Comparator也拥有同样的功能,这也是策略模式的一个例子
java没有提供函数指针,但是可以用对象引用实现同样的功能
我们在设计具体的策略类时,还需要定义一个策略接口
二十二.优先考虑静态成员类
嵌套类存在的目的应该只是为了它的外围类提供服务。嵌套类有四种类型:
①静态成员类 ②非静态成员类 ③匿名类 ④局部类
静态成员类可以访问外部类的所有成员,静态成员类是外围类的一个静态成员,也遵循同样的可访问性规则
非静态成员类虽然和静态成员类最明显的区别是static的有无,但是每个非静态实例都隐含着与外围类的一个外围相关联,
并且非静态成员类的类中可以调用外围对象的方法或者域
如果生命成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中。
私有静态成员类的一种常见用法是用来代表外围类类所代表的对象的组件
匿名类不同于其他的任何语法单元,匿名类没有名字,在使用的同时被声明和实例化
匿名类不能执行instanceof测试,或者做任何需要命名类的其他事情,可以用来创建函数对象或者多线程类。
局部类是四种嵌套类中用得很少的类,在任何可以声明局部变量的地方都可以声明局部类,并且局部类也遵守同样的作用域规则
二十三.请不要在新代码中使用原生态类型
有了泛型就可以限制容器中装载的对象的类型了
如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势,现在的原生态类型就是为了兼容没有泛型之前的代码。
List和List<Object>, 前者使用原生态类型,会失掉类型安全性,但是如果使用像List<Object>这样的参数化类型,则不会
java提供了一种安全的替代方法,称作无限制的通配符类型,如果要使用泛型,但不确定或者不关心实际的类型参数,就可以使用?代替
Set<?>和原生态类型Set之间的区别:通配符类型是安全的,原生态类型则不是安全,
由于可以将任何元素放进使用原生态类型的集合中,因此很容易破坏该集合的类型约束条件,所以Set<?>只允许放null类型
使用原生态类型的情况:
①在类文字中必须使用原生态类型,因为List<?>.class是不合法的
②因为泛型信息可以在运行时被檫除,因此在参数化类型而非无限制通配符类型上使用instanceof是非法的,故得用原生态类型
Set<Object>是个参数化类型,表示可以包含任何对象类型的一个集合
Set<?>是一个通配符类型,表示只能包含某种未知对象类型的一个集合.
Set则是个原声态类型
二十四.消除非受检警告
当遇到警告时,要尽可能地消除每一个非受检警告,如果消除了所有警告,就可以确保代码是类型安全的
如果无法消除警告,同时可以保证引起警告的代码是类=类型安全的,可以用一个@SuppressWarnings("unchecked")注解来禁止这条警告
SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以。
将SuppressWarnings注解放在return语句中是非法的,因为它不是一个声明。
每当使用SuppressWarnings都要添加一条注释,说明为什么这么做是安全的。
二十五.列表优先于数组
列表和数组的两个重要的不同点:
①数组是协变的, Sub是Super的子类型,Sub[]是Super[]的子类型,但是泛型是不可变的
②数组是具体化的,数组会在运行的时候才知道并检查它们的元素类型约束,但是泛型是运行时檫除的,檫除使得泛型可以在没有使用泛型的代码中随意互用
上面的区别造成两者不能很好的混合使用
不可具体化的类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。
唯一可以具体化的就是无限制的通配符类型, List<?>, Map<?, ?>
当你想使用泛型数组时,可以使用泛型集合来解决
优点:①静态方法有合适的名字。
②不必在每次调用他们的时候都创建一个新对象
③静态方法可以返回类型的任何子类型对象。
④创建参数化类型实例的时候,它们使代码变得更加简洁
服务提供者框架中有三个重要的组件:服务接口(提供者实现的),提供者注册API(系统用来注册实现的), 服务访问API(客户端用来获取服务的)
缺点:①如果不含公有的或者受保护的构造器,就不能被子类化。
②它们与其他的静态方法实际上没有任何区别。
静态方法常用的名称:valueOf, of, getInstance, newInstance, getType, newType
二.遇到多个构造器参数时要考虑用构建器
重构构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,并且仍然较难以阅读。
第一种方式是重载多个构造函数
第二种方式是JavaBeans模式(缺点是在构造过程中JavaBean可能处于不一致的状态,javaBean模式阻止了把类做成不可变的可能)
第三种方式是Builder模式,使用构造器或者静态工厂生成对象,然后设置每个相关的可选参数。
平时使用Builder模式的时候,我们可以事先创建一个Builder泛型接口,然后每次都继承Builder接口
传统的抽象工厂是使用Class对象的newInstance方法充当builder的一部分, 然而newInstance总是试图调用无参的构造函数,可能产生异常。
builder的缺点:为了创建对象,必须先创建它的构造器,有一定的性能损耗。
三.用私有构造器或者枚举类型强化Singleton属性
Singleton就是我们常说的单例,代表那些本质上是唯一的组件。
①可以将构造器设为私有的,避免其违反Singleton的规则。如果使用反射机制,反射会忽略权限,
可能也会使用私有的构造函数,此时我们可以加上一些我们的判断。
②公有的成员是个静态工厂方法,公有的静态域是final的,所以该域总是包含相同的对象引用,
使用这种方法不会带来性能上的损失,因为虚拟机已经实现了静态
工厂方法的内联调用
为了保证Singleton,必须声明所有实例域都是瞬时的(transient),并且提供readResolve方法。
③枚举方式
四.通过私有构造器强化不可实例化的能力
有时用java实现算法题的时候编写只包含静态方法和静态域的类,这是一种很不好的习惯。
但是有时工具类也需要这样,这些类是希望不被实例化的。
企图通过将类做成抽象类来强制该类不能被实例化,这是行不通的,这些类可以被子类化。
解决方法:让类中所有的构造器都编程私有的(谨记,系统提供的缺省构造器是显式的)
辅作用:该类不能被子类化。
五.避免创建不必要的对象
如果是常量的话,可以采用字面常量,这样使用常量池,不会每次都创建新的对象。
如果是对象的话,通常可以使用静态工厂方法而不是构造器。
适配器:它把功能委托给一个后备对象,从而为后备对象提供一个可以替代的接口
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱会导致性能的降低。
对象池:一般用来装载重量级的对象,真正正确使用对象池的典型对象示例就是数据库连接池。
这里不能和保护性拷贝搞混,因为在那里重用的话可能会导致潜在的错误和安全漏洞。
六.消除过期的对象引用
如果一个对象引用被无意识地保留起来,那么垃圾回收机制不仅不会处理这个对象,而且不会垂柳被这个对象引用的所有其他对象
解决方式:一旦对象引用已经过期,只需清空这些引用即可。
eg: public Object pop() {
if(size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // this is necessary.
return result;
}
消除过期引用的最好的方法是:在最紧凑的作用于范围内定义每一个变量,这种情况会自然而然的发生。
如果类是自己管理内存的,程序员就应该警惕内存泄露问题。
缓存也是造成内存泄露的一个原因,常用的解决方式是定期的清理缓存。
内存泄露的第三个常见来源是监听器和其他回调。你实现了一个API,客户端在这个API中注册回调,却没有显示地取消注册,除非你采取某些措施
确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用。
七.避免使用终结方法
终结方法是不可预测的,也是很危险的,一般情况下不必要的。
在java一般使用try-catch的方式来完成c++析构函数的功能
终结方法被调用不是即使,并且终结方法的队列优先级比较低,并且终结方法依赖于JVM平台。
图形对象的终结速度达不到它们进入队列的速度可能造成超内存的情况
java规范不仅不保证终结方法会被及时地执行,而且根本就不保证他们会被执行。
tip:不要依赖终结方法来更新重要的持久状态。
System.gc System.runFinalization这两个方法增加了终结方法被执行的机会,但是也并不保证终结方法一定会被执行。
如果未被捕获的异常在终结过程中被抛出来,那么这种异常可能被忽略,并且该对象的终结过程也会终止。
终结方法有严重的性能损失。
①如果想将封装的资源终止,只需要提供一个显示的终止方法。
终结方法通常和try-finally结构结合起来使用,确保及时终止。
如果使用终结方法记住要调用父类的终结方法。
终结方法链是不会被自动执行的,如果类有终结方法,并且子类覆盖了终结方法,子类的终结方法就必须手工调用超类的终结方法
八.覆盖equals方法遵守通用约定
覆盖equals的时机:
如果类具有自己特有的“逻辑相等”概念,并且超类没有覆盖equals以期望的行为。
对于那些每个值至多只存在一个对象的情况,无需覆盖equals方法,因为逻辑相等就等于对象相同。
equals的等价关系:
①自反性 ②对称性 ③传递性 ④一致性 ⑤x非空,x.equals(null)必须返回false
关于Point, ColorPoint的问题可以显示出一个问题:我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定。
平时实现equals时都会首先用instanceof进行判断。
实现equals方法时需要注意里氏替换原则,一个类型的任何重要属性也将适用于它的子类型。
无论类是否可变,都不要让equals方法依赖于不可靠的资源
equals方法的诀窍:
①使用==操作符检查参数是否为这个对象的引用。
②使用instanceof操作符检查“参数是否为正确的类型”
③把参数转换成正确的类型。
④对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。
注意:基本类型中除了float和double使都用==操作符,对象使用equals,float和double使用Compare方法。
对于部分允许为空的域可以采用下面的代码来进行优化:
field == null ? o.field == null : field.equals(o.field))
为了获得高性能,应该最先比较最有可能不一致的域,或者是低开销的域。
覆盖eqeuals时总要覆盖hashCode方法
九.覆盖equals时总要覆盖hashCode方法
如果不这样做的话,很可能会违反Object,hashCode的通用约定,使该类无法结合所有基于散列点的集合一起正常工作。
如果两个对象equals方法比较是相等的,那么调用者两个对象中任意一个对象的hashCode方法都必须产生同样的结果。
如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果
Hash中,可以将与每个项关联点的散列码缓存起来,如果散列码不匹配,也不必检验对象的等同性。
hashCode方法的实现的准则:
①把某个非零的常数值,保存到一个名为result的int类型变量中
②对于对象中每个关键域f,完成以下步骤
(1)如果该域是boolean类型,则计算(f ? 1 : 0)
(2)如果该域是byte,char,short或者是int类型,则计算(int)f
(3) 如果该域是long类型,则计算(int)(f ^ (f >> 32))
(4) 如果该域是float,则计算Float.floatToIntBits(f), double一样
(5) 如果是对象则递归调用equals方法
(6) 如果该域是一个数组,则要把每一个元素当做单独的域来处理
按照下面的公式,计算得到的散列码c合并到result中:
result = 31 + result + c
返回result
在散列码的计算过程中,可以把冗余域排除在外,必须排除equals比较计算中没有用到的任何域。
平时我们可以使用31来代替以为操作(31为低位全1,可以防止信息会丢失)
如果一个类是不可变的, 并且计算散列码的开销比较大,就应该考虑把散列码缓存在对象内部
十.始终要覆盖toString方法
Object提供了toString的实现 包含类的名称 + @ + hashCode(16进制的表示)
无论是否指定格式,都为toString返回值中包含的所有信息,提供一种编程式的访问途径
十一.谨慎地覆盖clone
Cloneable接口的目的是表明这样的对象允许克隆,其主要的缺陷在于它缺少一个clone方法,
Cloneable方法中没有任何方法,它是为了改变超类中受保护的方法的行为。
拷贝的精确含义: x.clone() != x x.clone().getClass() == x.getClass() x.clone().equals(x)
super.clone只是浅层次的拷贝
注意:如果elements域是final的,上述方案就不能正常工作,因此在super.clone之后elements的值是不可改变的,
所以,如果想让类是可克隆的,可能有必要从某些域中去掉final修饰符。
克隆复杂对象的最后一种方法是:先调用super.clone, 然后把对象中的所有域都设置成它们的空白状态,然后调用搞成的克隆函数
总结:所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone方法,此公有方法首先调用super.clone,然后修正任何需要修正的域
如果你扩展了实现了Cloneable接口的类,那么你除了实现一个行为良好的clone方法外,没有别的选择
拷贝的另外一种实现是:一个实现对象拷贝的好方法是提供一个拷贝构造器或拷贝工厂。
eg: public Yum(Yum yum); public static Yum newInstance(Yum yum);
上面的两种方法都要比Cloneable/clone方法具有更多的优势。
十二.考虑实现Comparable接口
一旦类实现了Comparable接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作
由compareTo方法施加的等同性测试,也一定遵守自反性,对称性和传递性
TreeSet和TreeMap中使用的比较就是compareTo方法
如果一个域并没有实现Comparable接口,或者你需要使用一个非标准的排序关系,就可以使用一个显示的Comparator来代替
compareTo方法约定并没有指定返回值的大小,可以利用这一点来进行简化代码(这里需要注意别超过int的最大范围)
eg: public int compareTo(PhoneNumber pn) {
int areaCodeDiff = areaCode - pn.areaCode;
if(areaCodeDiff != 0)
return areaCodeDiff;
//...
return 0;
}
十三.使类和成员的可访问性最小化
又名信息隐藏或封装
信息隐藏之所以重要的原因:他可以有效的解除组成系统的各模块之间的耦合关系
规则1:尽可能使每个类或者成员不被外界访问,只有两种可能的访问级别:包级私有的和公有的
可访问性的递增顺序:
①私有的(private): 私有的——只有声明该成员的顶层类才可以访问这个成员
②缺省: 该成员的包内部的任何类都可以访问这个成员
③受保护的(protected): 声明该成员的类的子类可以访问这个成员
④公有的(public): 在任何地方都可以访问该成员
当你仔细设计了类的公有API之后,可能觉得应该把所有其他的成员都变成私有的
访问性的规定:如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别,
这样可以确保任何可使用超类的实例的地方也都可以使用子类的实例
为了便于测试,一般将级别声明为包级私有的,可以让测试作为被测试的包的一部分来运行,从而能够访问它的包级私有元素
实例域不能是公有的,一旦将其公有话,就放弃了对存储在这个域中的值进行限制的能力
长度非零的数组总是可变的,所以,类具有公有的静态final数组域,这几乎是错的。
十四.在公有类中使用访问方法而非公有域
这也就是平常使用的setter getter模式
如果类可以在它所在的包的外部进行访问,就提供访问方法
十五.使可变性最小化
为了使类称为不可变类,要遵循下面五条规则:
1.不要提供任何修改对象状态的方法
2.保证类不会被扩展
3.使所有的域都是final的
4.使所有的域都称为私有的
5.确保对于任何可变组件的互斥访问
函数式的做法可以保证对象的状态不会被改变
过程的或者命令式的做法可能会导致它的状态发生改变
如果一个对象具有不可变性,可以将其做成一个不可变对象,这会带来大量的好处
不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。
十六.复合优先于继承
子类脆弱的原因(overriding中出现的问题):
①与方法调用不同,继承打破了封装性,子类依赖于其超类中特定功能的实现细节,因为在子类中可能调用非public的方法
②超类在后面的发型版本中增加新的方法,而子类无法改变具体实现了,只能使用旧版本的方法实现。
上面的这些问题可以通过改用复合的方式得以解决,新类中的每个实例方法都可以调用所包含的组件中的对应方法,这被称之为转发。
用继承的时机:当两者之间真正满足“is - a”关系的时候才会使用继承
十七.要么为继承而设计,并提供文档说明,要么就禁止继承
好的API文档应该秒速一个给定的方法做了什么工作而不是描述其是如何做到的
为了允许继承,类还必须遵守其他一些约定。构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用
为了继承而设计类的时候,Cloneable和Serializable接口出现了特殊的困难,无论实现着其中的哪个接口通常都不是好主意
因为clone和readObject方法在行为上非常类似于构造器,所以类似的限制规则也是使用的, 无论是clone还是readObject,都不能调用可覆盖的方法
对于普通的具体类,它们既不是final的,也不是为了子类化而设计和编写文档的,这种状态很危险
这个问题的最佳解决方案是,对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化
十八.接口优于抽象类
接口和抽象类这两种机制都用用来允许多个实现的类型
接口的优点:
①抽象类会间接伤害到类层次
②接口是定义mixin的理想选择(mixin是允许任选的功能可被混合都类型的主要功能=中)
③接口允许我们构造非层次结构的类型框架
骨架实现上有个小小的不同,就是简单实现,AbstractMap,SimpleEntry就是个例子
eg:SimpleEntry, AbstarctMap 简单实现就像个骨架实现,因为它实现了接口,并且是为了继承而设计的,他是最简单的可能的有效实现
你可以原封不动的使用,也可以看情况将它子类化
抽象类的优势:抽象类的演变比接口的演变要容易得多
十九.接口只用于定义类型
一旦类实现了某个接口,就表明客户端可以对这个类的实例实施某些动作
如果要导出常量的话,常量接口模式是对接口的不良使用,它的所有子类的命名空间会被接口中的常量所“污染”
导出常量的合理方案:
①如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中
②如果这些常量最好被看作枚举类型的成员,就应该用枚举类型来导出这些常量
③使用不可实例化的工具类来导出这些常量
接口应该制备用来定义类型,它们不应该被用来导出常量
二十.类层次优于标签类
面向对象就提供了其他更好的方法来定义能表示多种风格对象的单个数据类型:子类化
为了将标签转变成类层次,首先要为标签类中的每个方法都定义一个包含抽象方法的抽象类,这每个方法的行为都依赖标签值
类层次的另一个好处在于,它们可以用来反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查
二十一.用函数对象表示策略
c语言标准库中的qsort函数要求用一个指向comparator函数的指针作为参数,比较函数有两个参数,返回值根据两者的大小情况而定
java中的Comparator也拥有同样的功能,这也是策略模式的一个例子
java没有提供函数指针,但是可以用对象引用实现同样的功能
我们在设计具体的策略类时,还需要定义一个策略接口
二十二.优先考虑静态成员类
嵌套类存在的目的应该只是为了它的外围类提供服务。嵌套类有四种类型:
①静态成员类 ②非静态成员类 ③匿名类 ④局部类
静态成员类可以访问外部类的所有成员,静态成员类是外围类的一个静态成员,也遵循同样的可访问性规则
非静态成员类虽然和静态成员类最明显的区别是static的有无,但是每个非静态实例都隐含着与外围类的一个外围相关联,
并且非静态成员类的类中可以调用外围对象的方法或者域
如果生命成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中。
私有静态成员类的一种常见用法是用来代表外围类类所代表的对象的组件
匿名类不同于其他的任何语法单元,匿名类没有名字,在使用的同时被声明和实例化
匿名类不能执行instanceof测试,或者做任何需要命名类的其他事情,可以用来创建函数对象或者多线程类。
局部类是四种嵌套类中用得很少的类,在任何可以声明局部变量的地方都可以声明局部类,并且局部类也遵守同样的作用域规则
二十三.请不要在新代码中使用原生态类型
有了泛型就可以限制容器中装载的对象的类型了
如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势,现在的原生态类型就是为了兼容没有泛型之前的代码。
List和List<Object>, 前者使用原生态类型,会失掉类型安全性,但是如果使用像List<Object>这样的参数化类型,则不会
java提供了一种安全的替代方法,称作无限制的通配符类型,如果要使用泛型,但不确定或者不关心实际的类型参数,就可以使用?代替
Set<?>和原生态类型Set之间的区别:通配符类型是安全的,原生态类型则不是安全,
由于可以将任何元素放进使用原生态类型的集合中,因此很容易破坏该集合的类型约束条件,所以Set<?>只允许放null类型
使用原生态类型的情况:
①在类文字中必须使用原生态类型,因为List<?>.class是不合法的
②因为泛型信息可以在运行时被檫除,因此在参数化类型而非无限制通配符类型上使用instanceof是非法的,故得用原生态类型
Set<Object>是个参数化类型,表示可以包含任何对象类型的一个集合
Set<?>是一个通配符类型,表示只能包含某种未知对象类型的一个集合.
Set则是个原声态类型
二十四.消除非受检警告
当遇到警告时,要尽可能地消除每一个非受检警告,如果消除了所有警告,就可以确保代码是类型安全的
如果无法消除警告,同时可以保证引起警告的代码是类=类型安全的,可以用一个@SuppressWarnings("unchecked")注解来禁止这条警告
SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以。
将SuppressWarnings注解放在return语句中是非法的,因为它不是一个声明。
每当使用SuppressWarnings都要添加一条注释,说明为什么这么做是安全的。
二十五.列表优先于数组
列表和数组的两个重要的不同点:
①数组是协变的, Sub是Super的子类型,Sub[]是Super[]的子类型,但是泛型是不可变的
②数组是具体化的,数组会在运行的时候才知道并检查它们的元素类型约束,但是泛型是运行时檫除的,檫除使得泛型可以在没有使用泛型的代码中随意互用
上面的区别造成两者不能很好的混合使用
不可具体化的类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。
唯一可以具体化的就是无限制的通配符类型, List<?>, Map<?, ?>
当你想使用泛型数组时,可以使用泛型集合来解决