31.在接口中不要存在实现代码
接口是一个契约,不仅仅约束着实现者,同时也是一个保证,保证提供的服务(常量、方法)是稳定、可靠的,如果把实现代码写在接口中,那接口就绑定了可能变化的因素,这就会导致 实现不再稳定和可靠,是随时都可能被抛弃、被更改、被重构的。所以,接口中虽然可以有实现,但应该避免使用。
32.静态变量一定要先声明后再赋值
下面的代码,输出结果为1
public
class
OtherClient
{
static
{
a=100;
}
public
static
int
a =1;
public
static
void
main
(
String
[]
args
) {
System
.out.
println
(a);
}
}
下面的代码,输出结果为100
public
class
OtherClient
{
public
static
int
a =1;
static
{
a=100;
}
public
static
void
main
(
String
[]
args
) {
System
.out.
println
(a);
}
}
这是为什么呢?
这要从静态变量的诞生说起了,静态变量是类加载时被分配到数据区(Data Area)的,它在内存中只有一个拷贝,不会被分配多次 ,其后的所有赋值操作都是值改变,地址则保持不变。jvm初始化变量是先声明空间,然后再赋值的,也就是说: int a=100; 在jvm中是分开执行的,等价于int a; a=100; 静态变量是在类初始化时首先被加载的,JVM会去查找类中所有的静态声明,然后分配空间,注意这时只是完成了地址空间的分配,还没有赋值,之后jvm会根据类中静态赋值(包括静态赋值和静态块赋值)的先后顺序来执行。
33.不要覆写静态方法
在java中可以通过覆写(Override)来增加或减弱父类的方法和行为,但覆写是针对非静态方法(也叫实例方法,只有生成实例才能调用的方法)的,不能针对静态方法(static修饰的方法,也叫类方法),why?
我们知道一个实例对象有两个类型:表面类型(Apparent Type)和实际类型(Actual Type),表面类型是声明时的类型,实际类型是对象产生时的类型。
对于非静态方法,它是根据对象的实际类型来执行的。而对于静态方法来说比较特殊,首先静态方法不依赖实例对象,它是通过类名访问的;其次,可以通过对象访问静态方法,如果是通过对象调用静态方法,JVM则会通过对象的表面类型查找到静态方法的入口,继而执行之。
静态方法不能覆写,但是可以隐藏。隐藏的目的是为了抛弃父类静态方法,重现子类方法,也就是期望父类的静态方法不要破坏子类的业务行为;而覆写则是将父类的行为增加或减弱,延续父类的职责。
需要说明的是:实例对象访问静态方法或静态属性不是好习惯。
34.构造函数尽量简化
子类实例化时 ,会首先初始化父类(注意这里是初始化,可不是生成父类对象),也就是初始化父类的变量,调用父类的构造函数,然后才会初始化子类的变量,调用子类自己的构造函数,最后生成一个实例对象。
执行父类无参构造函数,也就是子类的有参构造函数中默认包含了super()方法
35.避免在构造函数中初始化其他类
36.使用构造代码块精炼程序
在java中一共有四种类型的代码块:
(1)普通代码块:就是在方法后面使用{}括起来的代码片段,它不能单独执行,必须通过方法名调用执行
(2)静态代码块:static修饰,并使用{}括起,用于静态变量的初始化或对象创建前的环境初始化。
(3)同步代码块:synchronized修饰,并使用{}括起,它表同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。
(4)构造代码块:在类中没有任何的前缀或后缀,并使用{}括起来的代码片段。关于构造代码块,我们知道代码块不具有独立执行的能力,那么编译器是如何处理构造代码块的呢?很简单,编译器会把构造代码块插入到每个构造函数的最前端。构造代码块会在每个构造函数内首先执行(需要注意的是:构造代码块不是在构造函数之前运行,它依托于构造函数的执行)
可将构造代码块应用到如下场景中:1)初始化实例变量 2)初始化实例环境
37.构造代码块会想你所想
在上一个建议是编译器会把构造代码块插入到每一个构造函数中,但有一个例外的 情况没有说明:如果遇到this关键字(也就是构造函数调用自身其他的构造函数时)则不插入构造代码块。还有一点需要说明,super不会这样处理,编译器把构造代码块插入到super方法之后执行。
38.使用静态内部类提高封装性
java中的嵌套类(nested class)分为两种:静态内部类(也叫静态嵌套类,static nested class)和内部类(inner class),只有在是静态内部类的情况下才能把static修复符放在类前,其他任何时候static都是不能修饰类的。
静态内部类:内部类,并且是静态(static修饰)的即为静态内部类
静态内部类主要有两个优点:加强了类的封装性和提高了代码的可读性
静态内部类与普通内部类有什么区别?
(1)静态内部类不持有外部类的引用(静态内部类只可以访问外部类的静态方法和静态属性,普通内部类可以自由访问)
(2)静态内部类不依赖外部类(普通内部类与外部类之间是相互依赖关系,内部类实例不能脱离外部类实例,也就是说它们是同生同死,而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的)
(3)普通内部类不能声明 static的方法和变量
39.使用匿名类的构造函数
40.匿名类的构造函数很特殊
匿名类的构造函数特殊处理机制:一般类(也就是具有显示名字的类)的所有构造函数默认都是调用父类的无参构造函数的,而匿名类因为没有名字,只能由构造函数代码块代替,也就无所谓的有参和无参构造函数,它在初始化时直接调用父类的同参构造函数,然后再调用了自己的构造代码块。
41.让多重继承成为现实
其实说的是通过内部类曲折解决问题
42.让工具类不可实例化
一般是通过设置构造函数为private访问权限,但是如果用户仍然要生成一个实例来访问(通过反射修改构造函数权限),隐藏问题可能爆发。那有没有更好的限制办法呢?有,即不仅仅设置成private访问权限,还抛异常,代码如下:
public class UtilsClass{
private UtilsClass(){
throw new Error("不要实例化我!");
}
}
本建议主要说的是,如果一个类不允许实例化,就要保证“平常”渠道都不能实例化它。
43.避免对象的浅拷贝
我们知道一个类实现了Cloneable接口就表示它具备了被拷贝的能力,如果再覆写clone()方法就完全具体拷贝能力。拷贝是在内存中进行的,所以在性能方面比直接通过new生成对象要快很多,特别是大对象的生成上,这会使性能提升非常显著。但要注意一个问题:浅拷贝(Shwdow Clone)存在对象属性拷贝不彻底的问题。
例:
public
class
Person
implements
Cloneable
{
private
String
name;
private
Person
father;
public
Person
(
String
_name
){
name=
_name
;
}
public
Person
(
String
_name
,
Person
_parent
){
name=
_name
;
father=
_parent
;
}
public
String
getName
() {
return
name;
}
public
void
setName
(
String
name
) {
this
.name =
name
;
}
public
Person
getFather
() {
return
father;
}
public
void
setFather
(
Person
father
) {
this
.father =
father
;
}
@Override
public
Person
clone
(){
Person
person=
null
;
try
{
person=(
Person
)
super
.clone();
}
catch
(
CloneNotSupportedException
e){
e.
printStackTrace
();
}
return
person;
}
}
public
static
void
main
(
String
[]
args
) {
//test1
Person
f=
new
Person
(
"Father A"
);
Person
s1=
new
Person
(
"大儿子"
,f);
Person
s2=s1.
clone
();
s2.
setName
(
"小儿子"
);
System
.out.
println
(s1.
getName
()+
"的父亲"
+s1.
getFather
().
getName
());
System
.out.
println
(s2.
getName
()+
"的父亲"
+s2.
getFather
().
getName
());
//test2
Person
fathPerson=
new
Person
(
"Father B"
);
Person
son1=
new
Person
(
"big son"
,fathPerson);
Person
son2=son1.
clone
();
son2.
setName
(
"little son"
);
//son1认干爹
son1.
getFather
().
setName
(
"gan die C"
);
System
.out.
println
(son1.
getName
()+
"的父亲"
+son1.
getFather
().
getName
());
//结果显示son2的父亲也是 gan die c了,Father B两个儿子都没了
System
.out.
println
(son2.
getName
()+
"的父亲"
+son2.
getFather
().
getName
());
}
分析:我们知道所有类都继承自Object,Object提供了一个对象拷贝的默认方法 ,即上面代码中的super.clone方法 ,但是该方法是有缺陷的,它提供的是一种浅拷贝方式,也就是说它并不会把对象的所有属性全部拷贝一份,而是有选择性的拷贝,它的拷贝规则如下:
(1)基本类型:如果变量是基本类型,则拷贝其值,比如int,float等。
(2)对象:如果变量是一个实例对象,则拷贝地址引用,也就是说此时新拷贝出的对象与原对象共享该实例变量,不受访问权限的权限。(有点变态)
(3)String字符串:这个比较特殊,拷贝的也是一个地址,是个引用,但在修改时,它会从字符串池(string pool)中重新生成新的字符串,原有的字符串对象保持不变,在此处我们可以认为String是一个基本类型。
上面示例代码的原因就是第(2)点所致。
修正:
@Override
public
Person
clone
(){
Person
p=
null
;
try
{
p=(
Person
)
super
.clone();
//基于拷贝出的对象p,获取其父亲的姓名,生成新的对象new Person,做为p的父亲
p.
setFather
(
new
Person
(p.
getFather
().
getName
()));
}
catch
(
CloneNotSupportedException
e){
e.
printStackTrace
();
}
return
p;
}
44.推荐使用序列化实现对象的拷贝
被拷贝的类只要实现Serializable接口即可,不需要任何实现,当然serialVersionUID常量还是要加上去的。
例如:
@SuppressWarnings
(
"unchecked"
)
public
static
<
T
extends
Serializable
>
T
clone
(
T
obj
){
T
cloneObj=
null
;
try
{
//读取对象字节数据
ByteArrayOutputStream
baos=
new
ByteArrayOutputStream
();
ObjectOutputStream
oos=
new
ObjectOutputStream
(baos);
oos.
writeObject
(
obj
);
oos.
close
();
//分配内存空间,写入原始对象,生成新对象
ByteArrayInputStream
bais=
new
ByteArrayInputStream
(baos.
toByteArray
());
ObjectInputStream
ois=
new
ObjectInputStream
(bais);
cloneObj=(
T
)ois.
readObject
();
ois.
close
();
}
catch
(
Exception
e){
e.
printStackTrace
();
}
return
cloneObj;
}
用此方法进行对象拷贝时需要注意两点:
(1)对象的内部属性都是可序列化的
如果有内部属性不可序列化,则会抛出序列化异常,这会让调试者很纳闷:生成一个对象怎么会出现序列化异常呢?从这一点考虑,还需要将上面的这个工具方法的异常进行细化处理
(2)注意方法和属性的特殊修饰符。在建议11,12点中有提到
final、static、transient(瞬时变量,不进行序列化的变量)
当然还有一个更简单的办法,使用Apache下的commons工具包中的SerializationUtils类
45.覆写equals方法时不要识别不出自己
在这里此点主要是用来说明覆写equals方法时,不要违背equals方法的自反性原则:对于任何非空引用x,x.equals(x) 应该返回true
46.equals应该考虑null值情景
47.在equals中使用getClass进行类型判断
总之这里就一句话,覆写equals时,建议使用getClass进行类型判断,而不要使用instance of
48.覆写equals方法必须覆写hashCode方法