类、对象及方法——读《编写高质量代码:改善Java程序的151个建议》(三)

读书,收获,分享
建议后面的五角星仅代表笔者个人需要注意的程度。
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。

对于静态方法来说:

  1. 静态方法及属性不依赖实例对象,它是通过类名访问的
  2. 如果通过实例调用静态方法,JVM则会通过对象的表面类型查找到静态方法的入口来执行

在子类中构建与父类相同的方法名、输入参数、输出参数、访问权限(权限可以扩大),并且父类、子类都是静态方法,此种行为叫做隐藏(Hide),它与覆写有两点不同:

  1. 表现形式不同。隐藏用于静态方法,覆写用于非静态方法。在代码上的表现是:@Override注解可以用于覆写,不能用于隐藏。
  2. 职责不同。隐藏的目的是为了抛弃父类静态方法,重现子类方法,也就是期望父类的静态方法不要破坏子类的业务行为;而覆写则是将父类的行为增强或减弱,延续父类的职责。

建议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是否已经存在,不存在则创建之。

构造代码块的两个特性:

  1. 在每个构造函数中都运行

  2. 在构造函数中它会首先运行

注意:很好地利用构造代码块的这两个特性不仅可以减少代码量,还可以让程序更容易阅读

建议37:构造代码块会想你所想★☆☆☆☆

public class Other {
    
    {
        System.out.println("构造代码块");
    }
    
    public Other(){
        
    }

    public Other(String str){
        //如果遇到this关键字(也就是构造函数调用自身其他的构造函数时)则不插入构造代码块
       //Java知道把代码块插入到没有this方法的构造函数中即可,"智能"
        this();
    }
    
}

建议38:使用静态内部类提高封装性★★☆☆☆

静态内部类的两个优点:加强了类的封装性和提高了代码的可读性

静态内部类一般定义的类有什么区别呢?又有什么吸引人的地方呢?

  1. 提高封装性。从代码位置上来讲,静态内部类放置在外部类内,其代码层意义就是:静态内部类是外部类的子行为或子属性,两者直接保持着一定的关系

  2. 提高代码的可读性。相关联的代码放在一起,可读性当然提高了。

  3. 形似内部,神似外部。静态内部类虽然存在于外部类内,而且编译后的类文件名也包含外部类(格式是:外部类+$+内部类),但是它可以脱离外部类存在.

静态内部类非静态内部类有什么区别呢?

(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 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可不是一个好招数!

你可能感兴趣的:(类、对象及方法——读《编写高质量代码:改善Java程序的151个建议》(三))