zoukankan      html  css  js  c++  java
  • 编写高质量代码:改善Java程序的151个建议(第3章:类、对象及方法___建议36~40)

    建议36:使用构造代码块精简程序

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

    1. 普通代码块:就是在方法后面使用"{}"括起来的代码片段,它不能单独运行,必须通过方法名调用执行;
    2. 静态代码块:在类中使用static修饰,并用"{}"括起来的代码片段,用于静态变量初始化或对象创建前的环境初始化。
    3. 同步代码块:使用synchronized关键字修饰,并使用"{}"括起来的代码片段,它表示同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。
    4. 构造代码块:在类中没有任何前缀和后缀,并使用"{}"括起来的代码片段;

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

     1 public class Client36 {
     2 
     3     {
     4         // 构造代码块
     5         System.out.println("执行构造代码块");
     6     }
     7 
     8     public Client36() {
     9         System.out.println("执行无参构造");
    10     }
    11 
    12     public Client36(String name) {
    13         System.out.println("执行有参构造");
    14     }15 }

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

     1 public class Client36 {
     2 
     3     public Client36() {
     4         System.out.println("执行构造代码块");
     5         System.out.println("执行无参构造");
     6     }
     7 
     8     public Client36(String name) {
     9         System.out.println("执行构造代码块");
    10         System.out.println("执行有参构造");
    11     }
    12 }

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

    1. 初始化实例变量(Instance Variable):如果每个构造函数都要初始化变量,可以通过构造代码块来实现。当然也可以通过定义一个方法,然后在每个构造函数中调用该方法来实现,没错,可以解决,但是要在每个构造函数中都调用该方法,而这就是其缺点,若采用构造代码块的方式则不用定义和调用,会直接由编译器写入到每个构造函数中,这才是解决此问题的绝佳方式。
    2. 初始化实例环境:一个对象必须在适当的场景下才能存在,如果没有适当的场景,则就需要在创建该对象的时候创建次场景,例如在JEE开发中,要产生HTTP Request必须首先建立HTTP Session,在创建HTTP Request时就可以通过构造代码块来检查HTTP Session是否已经存在,不存在则创建之。

      以上两个场景利用了构造代码块的两个特性:在每个构造函数中都运行和在构造函数中它会首先运行。很好的利用构造代码块的这连个特性不仅可以减少代码量,还可以让程序更容易阅读,特别是当所有的构造函数都要实现逻辑,而且这部分逻辑有很复杂时,这时就可以通过编写多个构造代码块来实现。每个代码块完成不同的业务逻辑(当然了构造函数尽量简单,这是基本原则),按照业务顺序一次存放,这样在创建实例对象时JVM就会按照顺序依次执行,实现复杂对象的模块化创建。

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

       上一建议中我们提议使用构造代码块来简化代码,并且也了解到编译器会自动把构造代码块插入到各个构造函数中,那我们接下来看看,编译器是不是足够聪明,能为我们解决真实的开发问题,有这样一个案例,统计一个类的实例变量数。你可要说了,这很简单,在每个构造函数中加入一个对象计数器补救解决了嘛?或者我们使用上一建议介绍的,使用构造代码块也可以,确实如此,我们来看如下代码是否可行:

     1 public class Client37 {
     2     public static void main(String[] args) {
     3         new Student();
     4         new Student("张三");
     5         new Student(10);
     6         System.out.println("实例对象数量:"+Student.getNumOfObjects());
     7     }
     8 }
     9 
    10 class Student {
    11     // 对象计数器
    12     private static int numOfObjects = 0;
    13 
    14     {
    15         // 构造代码块,计算产生的对象数量
    16         numOfObjects++;
    17     }
    18 
    19     public Student() {
    20 
    21     }
    22 
    23     // 有参构造调用无参构造
    24     public Student(String stuName) {
    25         this();
    26     }
    27 
    28     // 有参构造不调用无参构造
    29     public Student(int stuAge) {
    30 
    31     }
    32     //返回在一个JVM中,创建了多少实例对象
    33     public static int getNumOfObjects(){
    34         return numOfObjects;
    35     }
    36 }

      这段代码可行吗?能计算出实例对象的数量吗?如果编译器把构造代码块插入到各个构造函数中,那带有String形参的构造函数就可能有问题,它会调用无参构造,那通过它生成的Student对象就会执行两次构造代码块:一次是无参构造函数调用构造代码块,一次是执行自身的构造代码块,这样的话计算就不准确了,main函数实际在内存中产生了3个对象,但结果确是4。不过真的是这样吗?我们运行之后,结果是:

      实例对象数量:3;

      实例对象的数量还是3,程序没有问题,奇怪吗?不奇怪,上一建议是说编译器会把构造代码块插入到每一个构造函数中,但是有一个例外的情况没有说明:如果遇到this关键字(也就是构造函数调用自身的其它构造函数时),则不插入构造代码块,对于我们的例子来说,编译器在编译时发现String形参的构造函数调用了无参构造,于是放弃插入构造代码块,所以只执行了一次构造代码块。

      那Java编译器为何如此聪明?这还要从构造代码块的诞生说起,构造代码块是为了提取构造函数的共同量,减少各个构造函数的代码产生的,因此,Java就很聪明的认为把代码插入到this方法的构造函数中即可,而调用其它的构造函数则不插入,确保每个构造函数只执行一次构造代码块。

      还有一点需要说明,大家千万不要以为this是特殊情况,那super也会类似处理了,其实不会,在构造块的处理上,super方法没有任何特殊的地方,编译器只把构造代码块插入到super方法之后执行而已。仅此不同。

      注意:放心的使用构造代码块吧,Java已经想你所想了。

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

       Java中的嵌套类(Nested Class)分为两种:静态内部类(也叫静态嵌套类,Static Nested Class)和内部类(Inner Class)。本次主要看看静态内部类。什么是静态内部类呢?是内部类,并且是静态(static修饰)的即为静态内部类,只有在是静态内部类的情况下才能把static修饰符放在类前,其它任何时候static都是不能修饰类的。

      静态内部类的形式很好理解,但是为什么需要静态内部类呢?那是因为静态内部类有两个优点:加强了类的封装和提高了代码的可读性,我们通过下面代码来解释这两个优点。 

     1 public class Person {
     2     // 姓名
     3     private String name;
     4     // 家庭
     5     private Home home;
     6 
     7     public Person(String _name) {
     8         name = _name;
     9     }
    10 
    11     /* home、name的setter和getter方法略 */
    12 
    13     public static class Home {
    14         // 家庭地址
    15         private String address;
    16         // 家庭电话
    17         private String tel;
    18 
    19         public Home(String _address, String _tel) {
    20             address = _address;
    21             tel = _tel;
    22         }
    23         /* address、tel的setter和getter方法略 */
    24     }
    25 }

      其中,Person类中定义了一个静态内部类Home,它表示的意思是"人的家庭信息",由于Home类封装了家庭信息,不用再Person中再定义homeAddr,homeTel等属性,这就使封装性提高了。同时我们仅仅通过代码就可以分析出Person和Home之间的强关联关系,也就是说语义增强了,可读性提高了。所以在使用时就会非常清楚它表达的含义。  

    public static void main(String[] args) {
            // 定义张三这个人
            Person p = new Person("张三");
            // 设置张三的家庭信息
            p.setHome(new Home("北京", "010"));
    
        }

      定义张三这个人,然后通过Person.Home类设置张三的家庭信息,这是不是就和我们真是世界的情形相同了?先登记人的主要信息,然后登记人员的分类信息。可能你由要问了,这和我们一般定义的类有神么区别呢?又有什么吸引人的地方呢?如下所示:

    1. 提高封装性:从代码的位置上来讲,静态内部类放置在外部类内,其代码层意义就是,静态内部类是外部类的子行为或子属性,两者之间保持着一定的关系,比如在我们的例子中,看到Home类就知道它是Person的home信息。
    2. 提高代码的可读性:相关联的代码放在一起,可读性肯定提高了。
    3. 形似内部,神似外部:静态内部类虽然存在于外部类内,而且编译后的类文件也包含外部类(格式是:外部类+$+内部类),但是它可以脱离外部类存在,也就说我们仍然可以通过new Home()声明一个home对象,只是需要导入"Person.Home"而已。  

      解释了这么多,大家可能会觉得外部类和静态内部类之间是组合关系(Composition)了,这是错误的,外部类和静态内部类之间有强关联关系,这仅仅表现在"字面上",而深层次的抽象意义则依类的设计.

      那静态类内部类和普通内部类有什么区别呢?下面就来说明一下:

    1. 静态内部类不持有外部类的引用:在普通内部类中,我们可以直接访问外部类的属性、方法,即使是private类型也可以访问,这是因为内部类持有一个外部类的引用,可以自由访问。而静态内部类,则只可以访问外部类的静态方法和静态属性(如果是private权限也能访问,这是由其代码位置决定的),其它的则不能访问。
    2. 静态内部类不依赖外部类:普通内部类与外部类之间是相互依赖关系,内部类实例不能脱离外部类实例,也就是说它们会同生共死,一起声明,一起被垃圾回收,而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类也是可以存在的。
    3. 普通内部类不能声明static的方法和变量:普通内部类不能声明static的方法和变量,注意这里说的是变量,常量(也就是final static 修饰的属性)还是可以的,而静态内部类形似外部类,没有任何限制。

    建议39:使用匿名类的构造函数

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

        public static void main(String[] args) {
            List list1=new ArrayList();
            List list2=new ArrayList(){};
            List list3=new ArrayList(){{}};
            System.out.println(list1.getClass() == list2.getClass());
            System.out.println(list2.getClass() == list3.getClass());
            System.out.println(list1.getClass() == list3.getClass());
        }

      注意ArrayList后面的不通点:list1变量后面什么都没有,list2后面有一对{},list3后面有两个嵌套的{},这段程序能否编译呢?若能编译,那输结果是什么呢?

      答案是能编译,输出的是3个false。list1很容易理解,就是生命了ArrayList的实例对象,那list2和list3代表的是什么呢?

      (1)、list2 = new ArrayList(){}:list2代表的是一个匿名类的声明和赋值,它定义了一个继承于ArrayList的匿名类,只是没有任何覆写的方法而已,其代码类似于: 

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

      (2)、list3 = new ArrayList(){{}}:这个语句就有点奇怪了,带了两对{},我们分开解释就明白了,这也是一个匿名类的定义,它的代码类似于: 

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

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

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

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

    建议40:匿名类的构造函数很特殊

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

     1 public class Calculator {
     2     enum Ops {
     3         ADD, SUB
     4     };
     5 
     6     private int i, j, result;
     7 
     8     // 无参构造
     9     public Calculator() {
    10 
    11     }
    12 
    13     // 有参构造
    14     public Calculator(int _i, int _j) {
    15         i = _i;
    16         j = _j;
    17     }
    18 
    19     // 设置符号,是加法运算还是减法运算
    20     protected void setOperator(Ops _ops) {
    21         result = _ops.equals(Ops.ADD) ? i + j : i - j;
    22     }
    23 
    24     // 取得运算结果
    25     public int getResult() {
    26         return result;
    27     }
    28 
    29 }

     代码的意图是,通过构造函数传递两个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类的方法看看。代码就略了,因为很简单new Add,然后调用父类的getResult方法就可以了,经过测试,输出结果为0(为什么而是0?这很容易,有参构造没有赋值)。这说明两者不等价,不过,原因何在呢?

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

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

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

  • 相关阅读:
    不务正业系列-浅谈《过气堡垒》,一个RTS玩家的视角
    [LeetCode] 54. Spiral Matrix
    [LeetCode] 40. Combination Sum II
    138. Copy List with Random Pointer
    310. Minimum Height Trees
    4. Median of Two Sorted Arrays
    153. Find Minimum in Rotated Sorted Array
    33. Search in Rotated Sorted Array
    35. Search Insert Position
    278. First Bad Version
  • 原文地址:https://www.cnblogs.com/selene/p/5860067.html
Copyright © 2011-2022 走看看