转载--编写高质量代码:改善java程序的151个建议(第3章:类对象及方法___建议36~40)

     2022-03-15     599

关键词:

阅读目录

  • 建议36:使用构造代码块精简程序
  • 建议37:构造代码块会想你所想
  • 建议38:使用静态内部类提高封装性
  • 建议39:使用匿名类的构造函数
  • 建议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的处理机制也正是如此处理的。

 

作者:阿赫瓦里
出处:http://www.cnblogs.com/selene/
本文以学习、研究和分享为主,版权归作者和博客园共有,欢迎转载,如果文中有不妥或者错误的地方还望大神您不吝指出。如果觉得本文对您有所帮助不如【推荐】一下吧!如果你有更好的建议,不如留言一起讨论,共同进步! 再次感谢您耐心的读完本篇文章。

转载---编写高质量代码:改善java程序的151个建议(第2章:基本类型___建议26~30)

阅读目录建议26:提防包装类型的null值建议27:谨慎包装类型的大小比较建议28:优先使用整型池建议29:优先选择基本类型建议30:不要随便设置随机种子回到顶部建议26:提防包装类型的null值  我们知道Java引入包装类型(Wrapp... 查看详情

转载--编写高质量代码:改善java程序的151个建议(第5章:数组和集合___建议60~64)

阅读目录建议60:性能考虑,数组是首选建议61:若有必要,使用变长数组建议62:警惕数组的浅拷贝建议63:在明确的场景下,为集合指定初始容量建议64:多种最值算法,适时选择      噢,它明白了,河水既没有牛伯... 查看详情

转载--编写高质量代码:改善java程序的151个建议(第3章:类对象及方法___建议31~35)

阅读目录建议31:在接口中不要存在实现代码建议32:静态变量一定要先声明后赋值建议33:不要覆写静态方法建议34:构造函数尽量简化建议35:避免在构造函数中初始化其它类                  书读的多... 查看详情

转载--编写高质量代码:改善java程序的151个建议(第4章:字符串___建议52~55)

阅读目录建议52:推荐使用String直接量赋值建议53:注意方法中传递的参数要求建议54:正确使用String、StringBuffer、StringBuilder建议55:注意字符串的位置回到顶部建议52:推荐使用String直接量赋值  一般对象都是通过new关键字生... 查看详情

转载---编写高质量代码:改善java程序的151个建议(第3章:类对象及方法___建议47~51)

阅读目录建议47:在equals中使用getClass进行类型判断建议48:覆写equals方法必须覆写hashCode方法建议49:推荐覆写toString方法建议50:使用package-info类为包服务建议51:不要主动进行垃圾回收回到顶部建议47:在equals中使用getClass进行... 查看详情

转载--编写高质量代码:改善java程序的151个建议(第5章:数组和集合___建议65~69)

阅读目录建议65:避开基本类型数组转换列表陷阱建议66:asList方法产生的List的对象不可更改建议67:不同的列表选择不同的遍历算法建议68:频繁插入和删除时使用LinkList建议69:列表相等只关心元素数据回到顶部建议65:避开基... 查看详情

转载---编写高质量代码:改善java程序的151个建议(第3章:类对象及方法___建议41~46)

阅读目录建议41:让多重继承成为现实建议42:让工具类不可实例化建议43:避免对象的浅拷贝建议44:推荐使用序列化对象的拷贝建议45:覆写equals方法时不要识别不出自己建议46:equals应该考虑null值情景回到顶部建议41:让多重... 查看详情

转载--编写高质量代码:改善java程序的151个建议(第1章:java开发中通用的方法和准则___建议16~20)

阅读目录建议16:易变业务使用脚本语言编写建议17:慎用动态编译建议18:避免instanceof非预期结果建议19:断言绝对不是鸡肋建议20:不要只替换一个类回到顶部建议16:易变业务使用脚本语言编写  Java世界一直在遭受着异种... 查看详情

转载----编写高质量代码:改善java程序的151个建议(第1章:java开发中通用的方法和准则___建议1~5)

阅读目录建议1:不要在常量和变量中出现易混淆的字母建议2:莫让常量蜕变成变量  建议3:三元操作符的类型务必一致 建议4:避免带有变长参数的方法重载建议5:别让null值和空值威胁到变长方法      ... 查看详情

编写高质量代码:改善java程序的151个建议-笔记

1、字母“l”作为长整形标志时务必大写。eg: long num=11L;2、3、4、 查看详情

编写高质量代码:改善java程序的151个建议-笔记

1、字母“l”作为长整形标志时务必大写。eg: long num=11L;2、3、4、 查看详情

转载--编写高质量代码:改善java程序的151个建议(第1章:java开发中通用的方法和准则___建议11~15)

阅读目录建议11:养成良好习惯,显示声明UID建议12:避免用序列化类在构造函数中为不变量赋值建议13:避免为final变量复杂赋值建议14:使用序列化类的私有方法巧妙解决部分属性持久化问题建议15:break万万不可忘回到顶部建... 查看详情

编写高质量代码:改善java程序的151个建议--[78~92](代码片段)

编写高质量代码:改善Java程序的151个建议--[78~92]HashMap中的hashCode应避免冲突多线程使用Vector或HashTableVector是ArrayList的多线程版本,HashTable是HashMap的多线程版本。非稳定排序推荐使用List对于变动的集合排序set=newTreeSet使用TreeSet是... 查看详情

编写高质量代码:改善java程序的151个建议--[52~64](代码片段)

编写高质量代码:改善Java程序的151个建议--[52~64]推荐使用String直接量赋值Java为了避免在一个系统中大量产生String对象(为什么会大量产生,因为String字符串是程序中最经常使用的类型),于是就设计了一个字符串池(也叫作字符串常... 查看详情

编写高质量代码:改善java的151个建议四(类对象方法)31-51

31.接口中不要存在实现代码  接口中不能存在实现代码(虽然可以实现,但是如果把实现代码写在接口中,那么接口就绑定了可能变化的因素,这就导致实现不在文档和可靠,是随时可能被抛弃,被修改,被重构的)packagejsont... 查看详情

编写高质量代码:改善java程序的151个建议(第1章:java开发中通用的方法和准则___建议11~15)

建议11:养成良好习惯,显示声明UID我们编写一个实现了Serializable接口(序列化标志接口)的类,Eclipse马上就会给一个黄色警告:需要添加一个SerialVersionID。为什么要增加?他是怎么计算出来的?有什么用?下面就来解释该问题... 查看详情

编写高质量代码:改善java程序的151个建议(第1章:java开发中通用的方法和准则___建议16~20)

建议16:易变业务使用脚本语言编写  Java世界一直在遭受着异种语言的入侵,比如PHP,Ruby,Groovy、Javascript等,这些入侵者都有一个共同特征:全是同一类语言-----脚本语言,它们都是在运行期解释执行的。为什么Java这种强编... 查看详情

编写高质量代码:改善java程序的151个建议(第1章:java开发中通用的方法和准则___建议6~10)

建议6:覆写变长方法也循规蹈矩   在JAVA中,子类覆写父类的中的方法很常见,这样做既可以修正bug,也可以提供扩展的业务功能支持,同时还符合开闭原则(Open-ClosedPrinciple)。符合开闭原则(Open-ClosedPrinciple)的主要特征: ... 查看详情