編寫高質量代碼:改善Java程式的151個建議(第3章:類、對象及方法___建議36~40)

来源:http://www.cnblogs.com/selene/archive/2016/09/13/5860067.html
-Advertisement-
Play Games

建議36:使用構造代碼塊精簡程式 什麼叫做代碼塊(Code Block)?用大括弧把多行代碼封裝在一起,形成一個獨立的數據體,實現特定演算法的代碼集合即為代碼塊,一般來說代碼快不能單獨運行的,必須要有運行主體。在Java中一共有四種類型的代碼塊: 我麽知道一個類中至少有一個構造函數(如果沒有,編譯器會 ...


建議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的處理機制也正是如此處理的。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 不知道可能稱的上是ORM,其實就是一個DBHelper。看到網上不少人寫自己的ORM,但我覺得都不太好。 我這個ORM,學習成本非常低,常用的方法僅有幾個,在使用的過程中,你不需要有太多要註意的地方,也不會有“我怎樣實現連表查詢”的疑問。反射犧牲一些性能,但是降低了實現和使用的複雜度。 支持Orac ...
  • 一、前言 MD5驗證主要用於更新文件功能方面,伺服器告知客戶端要下載哪些更新文件並提供給客戶端其MD5值,客戶端從伺服器將更新文件下載到本地並計算下載文件的MD5值,將本地接收的MD5值與伺服器提供的MD5值進行比對,如果相同則說明下載的文件與伺服器提供的文件是一致的,如果不相同則說明下載後文件可能 ...
  • 剛開始學習Halcon,需要使用Halcon與C++聯合開發軟體,查了網上的資料都是Halcon10的,我用的是Halcon11和VS2010的開發環境,實踐了一下發現有一些問題,於是把自己的配置的過程寫出來共用一下。 首先新建一個Halcon工程,這裡用個讀入圖片的簡單例子。 新建一個Halcon... ...
  • 多終端數據同步機制設計(一) Intro 因為項目需要,需要設計一個多終端數據同步的機制, 需要滿足以下條件: 1. 多個終端數據操作及同步 2. 每次同步的時候只拉取需要同步的數據,且數據不能存在丟失 3. 儘可能少的調用伺服器端介面 同步流程 整體同步流程 我想仿照Git數據同步的方式來進行數據 ...
  • php生成隨機密碼(php自定義函數) 導讀:php隨機密碼的生成代碼,使用php自定義函數生成指定長度的隨機密碼,密碼規則為小寫字母與數字的隨機字元串,長度可自定義。 生成一個隨機密碼的函數,生成密碼為小寫字母與數字的隨機字元串,長度可自定義。 複製代碼代碼如下: <?php/* * php自動生 ...
  • 生活的味道 睜開眼看一看窗外的陽光,伸一個懶腰,拿起放在床一旁的水白開水,甜甜的味道,晃著尾巴東張西望的貓貓,在窗臺上舞蹈。你向生活微笑,生活也向你微笑。 請你不要詢問我的未來,這有些可笑。你問我你是不是要找個工作去上班?我告訴你不要,好好享受生活的味道,你所有現在所厭倦的誰說不是別人正羡慕的呢。 ...
  • http://flylib.com/books/en/1.142.1.125/1/ Using Stored Programs with MySQLdb The techniques for calling stored programs with MySQLdb differ only sligh ...
  • 棧數據結構 隊列數據結構 堆數據結構 固定長度的數組數據結構 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...