读书笔记--编写高质量代码:改善java程序的151个建议(二)匿名类与构造代码块

读书笔记--编写高质量代码:改善java程序的151个建议(二)匿名类与构造代码块

使用构造代码块精炼程序

什么叫代码块(Code Block)?用大括号把多行代码封装在一起,形成一个独立的数据 体,实现特定算法的代码集合即为代码块,一般来说代码块是不能单独运行的,必须要有运 行主体。在 Java 中一共有四种类型的代码块:

(1)普通代码块

就是在方法后面使用“{}”括起来的代码片段,它不能单独执行,必须通过方法名调用执行。

(2)静态代码块

在类中使用 static 修饰,并使用“{}”括起来的代码片段,用于静态变量的初始化或对象创建前的环境初始化。

(3)同步代码块

使用 synchronized 关键字修饰,并使用“{}”括起来的代码片段,它表示同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。

(4)构造代码块

在类中没有任何的前缀或后缀,并使用“{}”括起来的代码片段。

我们知道,一个类至少有一个构造函数(如果没有,编译器会无私地为其创建一个无参 构造函数) ,构造函数是在对象生成时调用的,那现在的问题来了 :构造函数和构造代码块 是什么关系?构造代码块是在什么时候执行的?在回答这个问题之前,我们先来看看编译器 是如何处理构造代码块的,看如下代码:

public class Client {
{
// 构造代码块
System.out.println("执行构造代码块");
}

public Client(){
System.out.println("执行无参构造 ");
}

public Client(String _str){
System.out.println("执行有参构造 ");
}
}

这是一段非常简单的代码,它包含了构造代码块、无参构造、有参构造,我们知道代码 块不具有独立执行的能力,那么编译器是如何处理构造代码块呢?很简单,编译器会把构造 代码块插入到每个构造函数的最前端,上面的代码与如下代码等价:

public class Client {
public Client(){
System.out.println("执行构造代码块");
System.out.println("执行无参构造");
}

public Client(String _str){
System.out.println("执行构造代码块");
System.out.println("执行有参构造");
}
}

每个构造函数的最前端都插入了构造代码块,很显然,在通过 new 关键字生成一个实例时会先执行构造代码块,然后再执行其他代码,也就是说 :构造代码块会在每个构造函数 内首先执行(需要注意的是 :构造代码块不是在构造函数之前运行的,它依托于构造函数的 执行),即使构造函数中包含本类的其他构造函数,也只会执行一次构造代码块,明白了这一点,我们就可以把构造代码块应用到如下场景中:

(1)初始化实例变量(Instance Variable)

如果每个构造函数都要初始化变量,可以通过构造代码块来实现。当然也可以通过定义 一个方法,然后在每个构造函数中调用该方法来实现,没错,可以解决,但是要在每个构造 函数中都调用该方法,而这就是其缺点,若采用构造代码块的方式则不用定义和调用,会直 接由编译器写入到每个构造函数中,这才是解决此类问题的绝佳方式。

(2)初始化实例环境

一个对象必须在适当的场景下才能存在,如果没有适当的场景,则就需要在创建对象时 创建此场景,例如在 JEE 开发中,要产生 HTTP Request 必须首先建立 HTTP Session,在创建 HTTP Request 时就可以通过构造代码块来检查 HTTP Session 是否已经存在,不存在则创建之。 以上两个场景利用了构造代码块的两个特性 :在每个构造函数中都运行和在构造函数中 它会首先运行。很好地利用构造代码块的这两个特性不仅可以减少代码量,还可以让程序更 容易阅读,特别是当所有的构造函数都要实现逻辑,而且这部分逻辑又很复杂时,这时就可 以通过编写多个构造代码块来实现。每个代码块完成不同的业务逻辑(当然了,构造函数尽 量简单,这是基本原则) ,按照业务顺序依次存放,这样在创建实例对象时 JVM 也就会按照顺序依次执行,实现复杂对象的模块化创建。

使用匿名类的构造函数

阅读如下代码,看看是否可以编译:

public static void main(String[] args) {  
 List l1 = new ArrayList();  
 List l2 = new ArrayList(){};  
 List l3 = new ArrayList(){{}};  
 System.out.println(l1.getClass() == l2.getClass());  
 System.out.println(l2.getClass() == l3.getClass());  
 System.out.println(l1.getClass() == l3.getClass());  
} 

注意ArrayList后面的不同点:l1变量后面什么都没有,l2后面有一对{},l3后面有2对嵌套的{},这段程序能不能编译呢?若能编译,那输出是多少呢?

答案是能编译,输出的是3个false。l1很容易解释,就是声明了ArrayList的实例对象,那l2和l3代表的是什么呢?

(1)l2=new ArrayList(){}

l2代表的是一个匿名类的声明和赋值,它定义了一个继承于ArrayList的匿名类,只是没有任何的覆写方法而已,其代码类似于:

//定义一个继承ArrayList的内部类  
class Sub extends ArrayList{  
}  
//声明和赋值  
List l2 = new Sub(); 

(2) l3=new ArrayList(){{}}

这个语句就有点怪了,还带了两对大括号,我们分开来解释就会明白了,这也是一个匿名类的定义,它的代码类似于:

//定义一个继承ArrayList的内部类  
class Sub extends ArrayList{  
 {  
//初始化块  
 }  
}  
//声明和赋值  
List l3 = new Sub(); 

看到了吧,就是多了一个初始化块而已,起到构造函数的功能。我们知道一个类肯定有一个构造函数,且构造函数的名称和类名相同,那问题来了:匿名类的构造函数是什么呢?它没有名字呀!很显然,初始化块就是它的构造函数。当然,一个类中的构造函数块可以是多个,也就是说可以出现如下代码:

List l3 = new ArrayList(){{}{}{}{}{}}; 

上面的代码是正确无误,没有任何问题的。现在清楚了:匿名函数虽然没有名字,但也是可以有构造函数的,它用构造函数块来代替,那上面的3个输出就很清楚了:虽然父类相同,但是类还是不同的。

匿名类的构造函数很特殊

在上一个建议中我们讲到匿名类虽然没有名字,但可以有一个初始化块来充当构造函数,那这个构造函数是否就和普通的构造函数完全一样呢?我们来看一个例子,设计一个计算器,进行加减乘除运算,代码如下:

//定义一个枚举,限定操作符  
enum Ops {ADD, SUB}  
class Calculator {  
 private int i, j, result;  
 //无参构造  
 public Calculator() {}  
 //有参构造  
 public Calculator(int _i, int _j) {  
  i = _i;  
  j = _j;  
 }  
 //设置符号,是加法运算还是减法运算  
 protected void setOperator(Ops _op) {  
  result = _op.equals(Ops.ADD)?i+j:i-j;  
 }  
 //取得运算结果  
 public int getResult(){  
  return result;  
 }  
} 

代码的意图是,通过构造函数输入两个int类型的数字,然后根据设置的操作符(加法还是减法)进行计算,编写一个客户端调用:

public static void main(String[] args) {  
Calculator c1 = new Calculator(1,2) {  
 {  
 setOperator(Ops.ADD);  
 }  
};  
System.out.println(c1.getResult());  
} 

这段匿名类的代码非常清晰:接收两个参数1和2,然后设置一个操作符号,计算其值,结果是3,这毫无疑问,但是这中间隐藏着一个问题:带有参数的匿名类声明时到底是调用的哪一个构造函数呢?我们把这段程序模拟一下:

//加法计算  
class Add extends Calculator {  
 {  
  setOperator(Ops.ADD);  
 }  
 //覆写父类的构造方法  
 public Add(int _i, int _j) {  
 }  
} 

匿名类和这个Add类是等价的吗?可能有人会说:上面只是把匿名类增加了一个名字,其他的都没有改动,那肯定是等价的啦!毫无疑问!那好,你再写个客户端调用Add类的方法看看。是不是输出结果为0(为什么是0?这很容易,有参构造没有赋值)。这说明两者不等价,不过,原因何在呢?

原来是因为匿名类的构造函数特殊处理机制,一般类(也就是具有显式名字的类)的所有构造函数默认都是调用父类的无参构造的,而匿名类因为没有名字,只能由构造代码块代替,也就无所谓的有参和无参构造函数了,它在初始化时直接调用了父类的同参数构造,然后再调用了自己的构造代码块,也就是说上面的匿名类与下面的代码是等价的:

//加法计算  
class Add extends Calculator {  
 {  
  setOperator(Ops.ADD);  
 }  
 //覆写父类的构造方法  
 public Add(int _i, int _j) {  
  super(_i,_j);  
 }  
} 

它首先会调用父类有两个参数的构造函数,而不是无参构造,这是匿名类的构造函数与普通类的差别,但是这一点也确实鲜有人细细琢磨,因为它的处理机制符合习惯呀,我传递两个参数,就是希望先调用父类有两个参数的构造,然后再执行我自己的构造函数,而Java的处理机制也正是如此处理的!

让多重继承成为现实

在Java中一个类可以多重实现,但不能多重继承,也就是说一个类能够同时实现多个接口,但不能同时继承多个类。但有时候我们确实需要继承多个类,比如希望拥有两个类的行为功能,就很难使用单继承来解决问题了(当然,使用多层继承是可以解决的)。幸运的是Java中提供的内部类可以曲折地解决此问题,我们来看一个案例,定义一个父亲、母亲接口,描述父亲强壮、母亲温柔的理想情形,代码如下:

//父亲  
interface Father{  
   public int strong();  
}  
//母亲  
interface Mother{  
   public int kind();  
} 

其中strong和kind的返回值表示强壮和温柔的指数,指数越高强壮度和温柔度也就越高,这与在游戏中设置人物的属性值是一样的。我们继续来看父亲、母亲这两个实现:

    class FatherImpl implements Father{  
        //父亲的强壮指数是8  
        public int strong(){  
        return 8;  
        }  
    }  

    class MotherImpl implements Mother{  
        //母亲的温柔指数是8  
        public int kind(){  
        return 8;  
        }  
    } 

父亲强壮指数是8,母亲温柔指数也是8,门当户对,那他们生的儿子、女儿一定更优秀了,我们先来看儿子类,代码如下:

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)。我们再来看看女儿类,代码如下:

class Daughter extends MotherImpl implements Father{  

 @Override  
 public int strong() {  
      return new FatherImpl(){  
        @Override  
        public int strong() {  
            //女儿的强壮指数降低了  
            return super.strong() - 2 ;  
        }  
    }.strong();  
 }  
} 

女儿继承了母亲的温柔指数,同时又覆写父类的强壮指数,不多解释。注意看覆写的strong方法,这里是创建了一个匿名内部类(Anonymous Inner Class)来覆写父类的方法,以完成继承父亲行为的功能。

多重继承指的是一个类可以同时从多于一个的父类那里继承行为与特征,按照这个定义来看,我们的儿子类、女儿类都实现了从父亲类、母亲类那里所继承的功能,应该属于多重继承。这要完全归功于内部类,诸位在需要用到多重继承时,可以思考一下内部类。

在现实生活中,也确实存在多重继承的问题,上面的例子是说后人即继承了父亲也继承了母亲的行为和特征,再比如我国的特产动物“四不像"(学名麋鹿),其外形“似鹿非鹿,似马非马,似牛非牛,似驴非驴”,这你要是想用单继承表示就麻烦了,如果用多继承则可以很好地解决问题:定义鹿、马、牛、驴四个类,然后建立麋鹿类的多个内部类,继承它们即可

猜你喜欢

转载自blog.csdn.net/qq413041153/article/details/31349683