书读的多而不思考,你会以为本身知道的不少。java
书读的多而思考,你会以为本身不懂的愈来愈多。 -----江疏影程序员
在面向对象编程(Object-Oriented Programming, OOP)的世界里,类和对象是真实世界的描述工具,方法是行为和动做的展现形式,封装、继承、多态则是其多姿多彩的主要实现方式,本章主要讲述关于Java对象,方法的种种规则,限制和建议。算法
建议31:在接口中不要存在实现代码编程
建议32:静态变量必定要先声明后赋值session
建议33:不要重写静态方法多线程
实例对象访问静态方法或静态属性不是好习惯,直接类名调用就好了。模块化
建议34:构造函数尽可能简化函数
建议35:避免在构造函数中初始化其余类工具
建议36:使用构造函代码块精简程序性能
一、代码块基本概念:
什么叫作代码块(Code Block)?用大括号把多行代码封装在一块儿,造成一个独立的数据体,实现特定算法的代码集合即为代码块,通常来讲代码快不能单独运行的,必需要有运行主体。在Java中一共有四种类型的代码块:
普通代码块:就是在方法后面使用"{}"括起来的代码片断,它不能单独运行,必须经过方法名调用执行;
静态代码块:在类中使用static修饰,并用"{}"括起来的代码片断,用于静态变量初始化或对象建立前的环境初始化。
同步代码块:使用synchronized关键字修饰,并使用"{}"括起来的代码片断,它表示同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。
构造代码块:在类中没有任何前缀和后缀,并使用"{}"括起来的代码片断;
二、代码实例展现:
package OSChina.Client; public class Base { static { System.out.println("我是父类静态代码块1"); } { System.out.println("我是父类构造代码块2"); } Base() { System.out.println("我是父类无参构造3"); } public static void doSomething(){ System.out.println("我是父类静态函数"); } }
package OSChina.Client; public class Sub extends Base{ static { System.out.println("我是子类静态代码块1"); } { System.out.println("我是子类构造代码块2"); } Sub(){ System.out.println("我是子类无参构造3"); } }
当实例化时:
Base base = new Sub();
当实例化时,执行顺序:静态代码块>>构造代码块>>构造函数>>普通方法。
当类名调用静态方法,不实例化时:
只执行静态代码块和对应的静态函数,构造函数不执行!
三、构造代码块应用场景:
① 初始化实例变量
若是每一个构造函数都要初始化变量,能够经过构造代码块来实现。
注:不是每一个构造函数都要加载的,而构造代码块必定加载。
② 初始化实例变量
一个对象必须在适当的场景下才能存在,若是没有适当的场景,就须要在建立该对象的时候建立场景。例如HTTP request必须首先创建HTTP session,此时就能够经过构造代码块来检查HTTP session是否已经存在,不存在则建立。
注:构造函数要实现复杂逻辑的时候,能够经过编写多个构造代码块来实现,每一个代码块完成不一样的业务逻辑(构造函数尽可能简单的原则),按照业务顺序依次存放,这样在建立实例对象时JVM会按照顺序依次执行,实现复杂对象的模块化建立。
建议37:构造代码块会想你所想
建议38:使用静态内部类提升封装性
一、Java中的嵌套类分为两种:静态内部类和内部类。
public class Person { // 姓名 private String name; // 家庭 private Home home; public Person(String _name) { name = _name; } /* home、name的setter和getter方法略 */ public static class Home { // 家庭地址 private String address; // 家庭电话 private String tel; public Home(String _address, String _tel) { address = _address; tel = _tel; } /* address、tel的setter和getter方法略 */ } }
其中,Person类中定义了一个静态内部类Home,它表示的意思是“人的家庭信息”,因为Home类封装了家庭信息,不用在Person类中定义homeAddr,homeTel等属性,这就提升了封装性,可读性也提升了。
public static void main(String[] args) { // 定义张三这我的 Person p = new Person("张三"); // 设置张三的家庭信息 p.setHome(new Home("北京", "010")); }
二、静态内部类的优点:
提升封装性
提升代码的可读性
形似内部,神似外部
静态内部类虽然存在于外部类中,并且编译后的类文件也包含外部类(格式是:外部类+$+内部类),可是它能够摆脱外部类存在,也就是说能够经过new Home()声明一个home对象,只是须要导入Person.Home而已。
三、静态内部类和普通内部类的区别:
① 静态内部类不持有外部类的引用:
普通内部类能够访问外部类的全部属性和方法。
静态内部类只能访问外部类的静态属性和静态方法。
② 静态内部类不依赖外部类:
普通内部类与外部类同生共死,一块儿被垃圾回收。
静态内部类能够独立存在。
③ 普通内部类不能声明static的方法和变量,final static修饰的常量除外。
建议39:使用匿名类的构造函数?
建议40:匿名类的构造函数很特殊?
建议39和建议40暂时没看出有啥用,再说吧!若有须要请阅读原著《编写高质量代码:改善Java程序的151个建议》!
建议41:让多继承成为现实
在Java中一个类能够多重实现,但不能多重继承,也就是说一个类可以同时实现多个接口,但不能同时继承多个类。
Java中提供的内部类能够曲折的解决此问题。
建议42:让工具类不可实例化
建议43:避免对象的浅拷贝
咱们知道一个类实现了cloneable接口就表示它具有了被拷贝的能力,若是重写clone()方法就会彻底具有拷贝能力。拷贝是在内存中进行的,因此性能方面比直接new生成的对象要快不少,特别在大对象的生成上,性能提高很是显著。可是浅拷贝存在对象属性拷贝不完全的问题。
浅拷贝:
一、概念
建立一个新对象,而后将当前对象的非静态字段复制到新对象,若是是值类型,对该字段进行复制;若是是引用类型,复制引用但不复制引用的对象。所以,原始对象及其副本引用同一个对象。
二、实现方法
调用对象的 clone 方法,必需要让类实现 Cloneable 接口,而且覆写 clone 方法。
建议44:推荐使用序列化对象的拷贝
深拷贝:
一、概念
建立一个新对象,而后将当前对象的非静态字段复制到该新对象,不管该字段是值类型的仍是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另外一个对象的内容。
二、实现方法
① 让每一个引用类型属性内部都重写clone()方法
② 利用序列化
序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里写到流中的对象是原始对象的一个拷贝,由于原始对象还存在在JVM中,因此咱们能够利用对象的序列化产生克隆对象,而后经过反序列化获取这个对象。
注意每一个须要序列化的类都要实现serializable接口,若是有某个属性不须要序列化,能够将其声明为transient,即将其排除在克隆属性以外。
三、利用序列化完成深拷贝的代码实例
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public final class CloneUtils { private CloneUtils() { throw new Error(CloneUtils.class + " cannot instance "); } // 拷贝一个对象 public static <T extends Serializable> T clone(T obj) { // 拷贝产生的对象 T cloneObj = null; try { // 读取对象字节数据 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(cloneObj); oos.close(); // 分配内存空间,写入原始对象,生成新对象 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); // 返回新对象, 并作类型转换 cloneObj = (T) ois.readObject(); ois.close(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return cloneObj; } }
此工具类要求被拷贝的对象实现serializable接口。
注:Apache下的Commons工具包中SerializationUtils类,直接使用更加简洁。
建议45:重写equals方法时不要识别不出本身
重写的equals方法作了多个校验,考虑到Web上传递过来的对象有可能输入了先后空格,因此用trim方法剪切了一下。
建议46:equals应该考虑null值情景
建议47:在equals中使用getClass进行类型判断
建议48:重写equals方法必须重写hashcode方法
为啥?官方解释没看懂。
建议49:推荐重写toString方法
Java提供的默认toString方法不友好,打印出来看不懂,不重写不行!
建议50:使用package-info类为包服务
package-info简称包文档,为包级文档和包级别注释提供一个地方,并且该文件惟一必须包含的是包的声明。
一、package-info的建立
二、package-info类不能继承,没有接口,没有类间关系等。
但package-info有啥用呢?只是对这个包的注释说明?
① 声明友好类和包内访问常量:
这个比较简单,并且很实用,好比一个包中有不少内部访问的类或常量,就能够统一放到package-info类中,这样很方便,便于集中管理,能够减小友好类处处游走的状况,代码以下:
class PkgClazz { public void test() { } } class PkgConstant { static final String PACKAGE_CONST = "ABC"; }
注意以上代码是放在package-info.java中的,虽然它没有编写package-info的实现,可是package-info.class类文件仍是会生成。经过这样的定义,咱们把一个包须要的常量和类都放置在本包下,在语义上和习惯上都能让程序员更适应。
② 为在包上提供注解提供便利:
好比咱们要写一个注解(Annotation),查一下包下的对象,只要把注解标注到package-info文件中便可,并且不少开源项目中也采用了此方法,好比Struts2的@namespace、hibernate的@filterDef等。
③ 提供包的总体注释说明:
若是是分包开发,也就是说一个包实现了一个业务逻辑或功能点或模块或组件,则该包须要一个很好的说明文档,说明这个包是作什么用的,版本变迁历史,与其余包的逻辑关系等,package-info文件的做用在此就发挥出来了,这些均可以直接定义到此文件中,经过javadoc生成文档时,会吧这些说明做为包文档的首页,让读者更容易对该包有一个总体的认识。
建议51:不要主动进行垃圾回收
System.gc要中止全部的响应,才能检查内存中是否存在能够回收的对象,这对一个应用系统来讲风险极大。