读书,收获,分享
建议后面的五角星仅代表笔者个人需要注意的程度。
Talk is cheap.Show me the code
建议31:在接口中不要存在实现代码★☆☆☆☆
本书作者的认为:如果把实现代码写到接口中,那接口就绑定了可能变化的因素,这就会导致实现不再稳定和可靠,是随时都可能被抛弃、被更改、被重构的,应避免使用。
但是在Java8中引入了default关键字,是为了解决接口的修改与现有的实现不兼容的问题(可以在不破坏java现有实现架构的情况下能往接口里增加新方法)
"因地、因时制宜"
建议32:静态变量一定要先声明后赋值★☆☆☆☆
示例代码:
先声明,后赋值
public class Client {
public static int i = 1;
static {
i = 100;
}
public static void main(String[] args) {
//运行结果:100
System.out.println(i);
}
}
先赋值,后声明
public class Client {
static {
i = 100;
}
public static int i = 1;
public static void main(String[] args) {
//运行结果:1
System.out.println(i);
}
}
静态变量的加载及赋值过程如下:
int i = 100;//在JVM中是分开执行,等价于
int i; //先分配地址空间
i = 100; //然后再赋值
/**
* 静态变量是在类初始化时首先被加载的,
* JVM会去查找类中所有的静态声明,然后分配空间,
* 注意这时候只是完成了地址空间的分配,还没有赋值,
* 之后JVM会根据类中静态赋值(包括静态类赋值和静态块赋值)的先后顺序来执行。
* 对于程序来说,就是先声明了int类型的地址空间,并把地址传递给了i,然后按照类中的先后顺序执行赋值动作,
* 首先执行静态块中i=100,接着执行i=1,那最后的结果就是i=1了。
*/
建议33:不要覆写静态方法★☆☆☆☆
示例代码:
public class Client {
public static void main(String[] args) {
Person person = new Son();
person.doSomething();
//运行结果:我是父类方法
/**
* 其实IDE挺智能的,除非特意这么写,否则基本不会出现这样的低级错误
* 再者,静态方法及属性,也不会用实例去访问
*/
}
}
public class Person {
public static void doSomething(){
System.out.println("我是父类方法");
}
}
public class Son extends Person{
public static void doSomething(){
System.out.println("我是子类方法");
}
}
一个实例对象有两个类型:表面类型(Apparent Type)和实际类型(Actual Type),表面类型是声明时的类型,实际类型是对象产生时的类型,比如我们例子,变量person的表面类型是Person,实际类型是Son。
对于静态方法来说:
- 静态方法及属性不依赖实例对象,它是通过类名访问的
- 如果通过实例调用静态方法,JVM则会通过对象的表面类型查找到静态方法的入口来执行
在子类中构建与父类相同的方法名、输入参数、输出参数、访问权限(权限可以扩大),并且父类、子类都是静态方法,此种行为叫做隐藏(Hide),它与覆写有两点不同:
- 表现形式不同。隐藏用于静态方法,覆写用于非静态方法。在代码上的表现是:@Override注解可以用于覆写,不能用于隐藏。
- 职责不同。隐藏的目的是为了抛弃父类静态方法,重现子类方法,也就是期望父类的静态方法不要破坏子类的业务行为;而覆写则是将父类的行为增强或减弱,延续父类的职责。
建议34:构造函数尽量简化★☆☆☆☆
构造函数的简繁情况会直接影响实例对象的创建是否繁。
不建议的写方法,示例代码:
public abstract class Client {
public Client() {
//获得子类提供的端口号
int potr = getPort();
//例如这种有集成关系的类调用时,无法"一眼洞穿"
//...
}
protected abstract int getPort();
}
注意:构造函数简化,再简化,应该达到“一眼洞穿”的境界。
建议35:避免在构造函数中初始化其他类★☆☆☆☆
错误写法示例代码:
public class Client {
public static void main(String[] args) {
Son son = new Son();
son.doSomething();
}
}
//造成了死循环
//在现实项目中构造函数可不是一两个,类之间的关系更加复杂,到时候是否还能瞥一眼就能看出缺陷在哪儿吗?
//解决此类问题的最好办法就是:不要在构造函数中声明初始化其他类,养成良好的习惯。
public class Person {
Person(){
new Other();
}
}
public class Son extends Person{
public void doSomething(){
System.out.println("我是子类方法");
}
}
public class Other {
Other(){
new Son();
}
}
建议36:使用构造代码块精炼程序★★☆☆☆
在Java中一共有四种类型的代码块:
(1)普通代码块
就是在方法后面使用“{}”括起来的代码片段,它不能单独执行,必须通过方法名调用执行。
(2)静态代码块
在类中使用static修饰,并使用“{}”括起来的代码片段,用于静态变量的初始化或对象创建前的环境初始化。
(3)同步代码块
使用synchronized关键字修饰,并使用“{}”括起来的代码片段,它表示同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。
(4)构造代码块
在类中没有任何的前缀或后缀,并使用“{}”括起来的代码片段。
那么构造函数和构造代码块是什么关系?构造代码块是在什么时候执行的?
构造代码块会在每个构造函数内首先执行(需要注意的是:构造代码块不是在构造函数之前运行的,它依托于构造函数的执行),如下示例:
public class Other {
{
//构造代码块
System.out.println("执行构造代码块");
}
public Other(){
//构造函数在实行时,会把构造代码块中的代码加载到此处执行,注意是在构造函数内的第一行
System.out.println("执行构造代码块");
System.out.println("执行构造方法内容...");
}
}
(1)初始化实例变量(Instance Variable)
public class Other {
public Other(){
//虽然通过如下这种定义方法的方式也能实现,但是构造代码块不用定义和调用,会直接由编译器写入到每个构造函数中
init();
}
public void init(){
System.out.println("我也能实现构造代码块的功能啊!";
}
}
(2)初始化实例环境
例如在JEE开发中,要产生HTTP Request必须首先建立HTTP Session,在创建HTTP Request时就可以通过构造代码块来检查HTTP Session是否已经存在,不存在则创建之。
构造代码块的两个特性:
在每个构造函数中都运行
在构造函数中它会首先运行
注意:很好地利用构造代码块的这两个特性不仅可以减少代码量,还可以让程序更容易阅读
建议37:构造代码块会想你所想★☆☆☆☆
public class Other {
{
System.out.println("构造代码块");
}
public Other(){
}
public Other(String str){
//如果遇到this关键字(也就是构造函数调用自身其他的构造函数时)则不插入构造代码块
//Java知道把代码块插入到没有this方法的构造函数中即可,"智能"
this();
}
}
建议38:使用静态内部类提高封装性★★☆☆☆
静态内部类的两个优点:加强了类的封装性和提高了代码的可读性
静态内部类和一般定义的类有什么区别呢?又有什么吸引人的地方呢?
提高封装性。从代码位置上来讲,静态内部类放置在外部类内,其代码层意义就是:静态内部类是外部类的子行为或子属性,两者直接保持着一定的关系
提高代码的可读性。相关联的代码放在一起,可读性当然提高了。
形似内部,神似外部。静态内部类虽然存在于外部类内,而且编译后的类文件名也包含外部类(格式是:外部类+$+内部类),但是它可以脱离外部类存在.
那静态内部类与非静态内部类有什么区别呢?
(1)静态内部类不持有外部类的引用
在普通内部类中,我们可以直接访问外部类的属性、方法,即使是private类型也可以访问,这是因为内部类持有一个外部类的引用,可以自由访问。而静态内部类,则只可以访问外部类的静态方法和静态属性(如果是private权限也能访问,这是由其代码位置所决定的),其他则不能访问。
(2)静态内部类不依赖外部类
普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例,也就是说它们会同生同死,一起声明,一起被垃圾回收器回收。而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的。
(3)普通内部类不能声明static的方法和变量
普通内部类不能声明static的方法和变量,注意这里说的是变量,常量(也就是final static修饰的属性)还是可以的,而静态内部类形似外部类,没有任何限制。
建议39:使用匿名类的构造函数★☆☆☆☆
public class Other {
public static void main(String[] args) {
//list1代表创建了一个ArrayList实例
List list1 = new ArrayList();
//list2代表的是一个匿名类的声明和赋值,定义了一个继承ArrayList的内部类
List list2 = new ArrayList(){};
//list3同list2,不过多了一个初始化块,起到了构造函数的功能,初始化块就是它的构造函数
List list3 = new ArrayList(){{}};
//以上三个虽然父类相同,但是类还是不同的。
}
}
建议40:匿名类的构造函数很特殊★★☆☆☆
带有参数的匿名类声明时到底是调用的哪一个构造函数呢?
匿名类的构造函数特殊处理机制,一般类(也就是具有显式名字的类)的所有构造函数默认都是调用父类的无参构造的,而匿名类因为没有名字,只能由构造代码块代替,也就无所谓的有参和无参构造函数了,它在初始化时直接调用了父类的同参数构造,然后再调用了自己的构造代码块。
示例代码:
public class Person {
private int i, j, result;
public Person() {
}
public Person(int i, int j) {
this.i = i;
this.j = j;
}
public void count() {
result = i + j;
}
}
public static void main(String[] args) {
Person person = new Person(1, 2) {
{
count();
}
};
}
//模拟上面程序的调用
public class Add extends Other {
{
count();
}
//覆写父类的构造方法
public Add(int _i, int _j) {
}
}
建议41:让多重继承成为现实★★☆☆☆
一个曲折的实现,示例代码:
//父亲
interface Father {
public int strong();
}
//母亲
interface Mother {
public int kind();
}
public class FatherImpl implements Father {
//父亲的强壮指数是8
@Override
public int strong() {
return 8;
}
}
public class MotherImpl implements Mother{
//母亲的温柔指数是9
@Override
public int kind() {
return 9;
}
}
//儿子
public class Son extends FatherImpl implements Mother{
@Override
public int strong() {
//儿子比父亲强壮
return super.strong()+1;
}
@Override
public int kind() {
return new MotherSpecial().kind();
}
private class MotherSpecial extends MotherImpl{
public int kind(){
//儿子的温柔指数降低了
return super.kind()-1;
}
}
}
儿子继承自父亲,变得比父亲更强壮了(覆写父类strong方法),同时儿子也具有母亲的优点,只是温柔指数降低了。注意看,这里构造了MotherSpecial类继承母亲类,也就是获得了母亲类的行为方法,这也是内部类的一个重要特性:内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,多重继承才会成为可能。MotherSpecial的这种内部类叫做成员内部类(也叫做实例内部类,Instance Inner Class)。
注意:Java的接口是可以多继承的,也可实现上述功能,需仔细体会两者之间的区别
建议42:让工具类不可实例化★★★☆☆
工具类的方法和属性都是静态的,不需要生成实例即可访问,而且JDK也做了很好的处理,由于不希望被初始化,于是就设置构造函数为private访问权限,表示除了类本身外,谁都不能产生一个实例。
java.lang. Math代码:
public final class Math {
/**
* Don't let anyone instantiate this class.
*/
private Math() {}
}
在项目开发中有没有更好的限制办法呢?示例如下:
public class Util {
//private访问权限
private Util(){
//还抛异常
throw new Error("不允许实例化");
}
}
注意:如果一个类不允许实例化,就要保证“平常”渠道都不能实例化它。
建议43:避免对象的浅拷贝★★★☆☆
浅拷贝,它的拷贝规则如下:
(1)基本类型
如果变量是基本类型,则拷贝其值,比如int、float等。
(2)对象
如果变量是一个实例对象,则拷贝地址引用,也就是说此时新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制。这在Java中是很疯狂的,因为它突破了访问权限的定义:一个private修饰的变量,竟然可以被两个不同的实例对象访问。
(3)String字符串
这个比较特殊,拷贝的也是一个地址,是个引用,但是在修改时,它会从字符串池(String Pool)中重新生成新的字符串,原有的字符串对象保持不变。
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝
注意:浅拷贝只是Java提供的一种简单拷贝机制,不便于直接使用。
建议44:推荐使用序列化实现对象的拷贝★★★☆☆
如果一个项目中有大量的对象是通过拷贝生成的,那我们该如何处理?每个类都写一个clone方法,并且还要深拷贝?想想看这是何等巨大的工作量呀,是否有更好的方法呢?
可以通过序列化方式来处理,在内存中通过字节流的拷贝来实现,也就是把母对象写到一个字节流中,再从字节流中将其读出来,这样就可以重建一个新对象了,该新对象与母对象之间不存在引用共享的问题,也就相当于深拷贝了一个新对象。
示例代码:
public class CloneUtils {
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();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
被拷贝的类只要实现Serializable这个标志性接口即可,不需要任何实现,当然serialVersionUID常量还是要加上去的,然后我们就可以通过CloneUtils工具进行对象的深拷贝了。用此方法进行对象拷贝时需要注意两点:
(1)对象的内部属性都是可序列化的
如果有内部属性不可序列化,则会抛出序列化异常,这会让调试者很纳闷:生成一个对象怎么会出现序列化异常呢?从这一点来考虑,也需要把CloneUtils工具的异常进行细化处理。
(2)注意方法和属性的特殊修饰符
比如final、static变量的序列化问题会被引入到对象拷贝中来,这点需要特别注意,同时transient变量(瞬态变量,不进行序列化的变量)也会影响到拷贝的效果。当然,采用序列化方式拷贝时还有一个更简单的办法,即使用Apache下的commons工具包中的SerializationUtils类,直接使用更加简洁方便。
建议45:覆写equals方法时不要识别不出自己★☆☆☆☆
注意:equals方法的自反性原则:对于任何非空引用x,x.equals(x)应该返回true。
建议46:equals应该考虑null值情景★☆☆☆☆
注意:equals方法的称性原则:对于任何引用x和y的情形,如果x.equals(y)返回true,那么y.equals(x)也应该返回true。
建议47:在equals中使用getClass进行类型判断★☆☆☆☆
例:在覆写equals时,对于存在继承关系的两个类,用instanceof关键字检查,当然返回true。
所以在覆写equals时建议使用getClass进行类型判断,而不要使用instanceof。
注意:equals方法的传递性原则:对于实例对象x、y、z来说,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。
建议48:覆写equals方法必须覆写hashCode方法★★☆☆☆
覆写equals方法必须覆写hashCode方法,这条规则是每个Javaer都应该知道的
为什么要这样做呢?
hashcode是用于散列数据的快速存取,如利用HashSet/HashMap/Hashtable类来存储数据时,都是根据存储对象的hashcode值来进行判断是否相同的。
如果我们对一个对象重写了euqals,意思是只要对象的成员变量值都相等那么euqals就等于true,但不重写hashcode,那么我们再new一个新的对象,当原对象.equals(新对象)等于true时,两者的hashcode却是不一样的,由此将产生了理解的不一致,如在存储散列集合时(如Set类),将会存储了两个值一样的对象,导致混淆,因此,就也需要重写hashcode()
建议49:推荐覆写toString方法★☆☆☆☆
为什么要覆写toString方法?
因为Java提供的默认toString方法不友好,打印出来看不懂,不覆写不行。
(默认打印出来的内容是:类名+@+hashCode)
为什么通过println方法打印一个对象会调用对象的toString方法?
System.out.println(new Person("谨以书为马"));
println的实现机制:如果是一个原始类型就直接打印,如果是一个类类型,则打印出其toString方法的返回值
建议50:使用package-info类为包服务★☆☆☆☆
Java中有一个特殊的类:package-info类,它是专门为本包服务的,为什么说它特殊呢?主要体现在3个方面:
(1)它不能随便被创建
会报“Type name isnotvalid”错误,类名无效。
创建方式:用记事本创建一个,然后拷贝进去再改一下就成了,更直接的办法就是从别的项目中拷贝过来。
(2)它服务的对象很特殊
它是描述和记录本包信息的。
(3)package-info类不能有实现代码
在package-info.java文件里不能声明package-info类。
package-info类还有几个特殊的地方,比如不可以继承,没有接口,没有类间关系(关联、组合、聚合等)等,
存在即有用,主要表现在以下三个方面:
(1)声明友好类和包内访问常量
这个比较简单,而且很实用,比如一个包中有很多内部访问的类或常量,就可以统一放到package-info类中,这样很方便,而且便于集中管理,可以减少友好类到处游走的情况。
(2)为在包上标注注解提供便利
比如我们要写一个注解(Annotation),查看一个包下的所有对象,只要把注解标注到package-info文件中即可,而且在很多开源项目也采用了此方法,比如Hibernate的@FilterDef等。
(3)提供包的整体注释说明
如果是分包开发,也就是说一个包实现了一个业务逻辑或功能点或模块或组件,则该包需要有一个很好的说明文档,说明这个包是做什么用的,版本变迁历史,与其他包的逻辑关系等,package-info文件的作用在此就发挥出来了,这些都可以直接定义到此文件中,通过javadoc生成文档时,会把这些说明作为包文档的首页,让读者更容易对该包有一个整体的认识。当然在这点上它与package.htm的作用是相同的,不过package-info可以在代码中维护文档的完整性,并且可以实现代码与文档的同步更新。
总结成一句话:在需要用到包的地方,就可以考虑一下package-info这个特殊类
建议51:不要主动进行垃圾回收★★★☆☆
主动进行垃圾回收(System.gc)是一个非常危险的动作
因为System.gc要停止所有的响应(Stop the world),才能检查内存中是否有可回收的对象,这对一个应用系统来说风险极大,如果是一个Web应用,所有的请求都会暂停,等待垃圾回收器执行完毕,若此时堆内存(Heap)中的对象少的话则还可以接受,一旦对象较多(现在的Web项目是越做越大,框架、工具也越来越多,加载到内存中的对象当然也就更多了),那这个过程就非常耗时了,可能0.01秒,也可能是1秒,甚至是20秒,这就会严重影响到业务的正常运行。
注意: 不要调用System.gc,即使经常出现内存溢出也不要调用,内存溢出是可分析的,是可以查找出原因的,GC可不是一个好招数!