透過lombok帶你讀透Builder構建器

airland發表於2021-09-09

透過lombok帶你讀透Builder構建器

很久之前,我在《effective java》上看過Builder構建器相關的內容,但實際開發中不經常用。後來,在專案中使用了lombok,發現它有一個註解“@Builder”,就是為java bean生成一個構建器。於是,回頭重新複習了下相關知識,整理如下。

lombok使用樣例

// 建立名為Officer的java bean@Builderpublic class Officer {    private final String id;    private final String name;    private final int age;    private final String department;
}// 呼叫構建器生成Officer例項class BuilderTest {    public static void main(String[] args) {
        Officer officer = Officer.builder().id("00001").name("simon qi")
                .age(26).department("departmentA").build();
    }
}

反編譯lombok生成的Officer.class

注意:下面請區分兩組名詞:"builder方法"和“build方法”,“構造器”和“構建器”。

public class Officer {    private final String id;    private final String name;    private final int age;    private final String department;

    Officer(String id, String name, int age, String department) {        this.id = id;        this.name = name;        this.age = age;        this.department = department;
    }    public static Officer.OfficerBuilder builder() {        return new Officer.OfficerBuilder();
    }    public static class OfficerBuilder {        private String id;        private String name;        private int age;        private String department;

        OfficerBuilder() {
        }        public Officer.OfficerBuilder id(String id) {            this.id = id;            return this;
        }        public Officer.OfficerBuilder name(String name) {            this.name = name;            return this;
        }        public Officer.OfficerBuilder age(int age) {            this.age = age;            return this;
        }        public Officer.OfficerBuilder department(String department) {            this.department = department;            return this;
        }        public Officer build() {            return new Officer(this.id, this.name, this.age, this.department);
        }        public String toString() {            return "Officer.OfficerBuilder(id=" + this.id + ", name=" + this.name + ", age=" + this.age + ", department=" + this.department + ")";
        }
    }
}

我們透過反編譯Officer.class,獲得上方的原始碼(最好用idea自帶的反編譯器,jd-gui反編譯的原始碼不全)。

我們發現原始碼中有一個OfficerBuilder的靜態內部類,我們在呼叫builder方法時,實際返回了這個靜態內部類的例項。這個OfficerBuilder類,具有和Officer相同的成員變數,且擁有名為id,name,age和department的方法。這些以Officer的成員變數命名的方法,都是給OfficerBuilder的成員變數賦值,並返回this。

這些方法返回this,其實就是返回撥用這些方法的OfficerBuilder物件,也可稱為“返回物件本身”。透過返回物件本身,形成了方法的鏈式呼叫。

再看build方法,它是OfficerBuilder類的方法。它建立了一個新的Officer物件,並將自身的成員變數值,傳給了Officer的成員變數。所以“Officer officer = Officer.builder().id("00001").name("simon qi").age(26).department("departmentA").build();”的寫法,等價於下面的寫法:

Officer.OfficerBuilder officerBuilder = new Officer.OfficerBuilder();
officerBuilder.id("00001").name("simon qi").age(26).department("departmentA");
Officer officer = officerBuilder.build();
System.out.println(officer);

所以為什麼這種模式叫“構建器”,因為要建立Officer類的例項,首先要建立OfficerBuilder類的例項。而這個OfficerBuilder也就是構建器,是建立Officer物件的一個過渡者。所以利用這種模式,會有中間例項的建立,會加大虛擬機器記憶體的消耗。

只用@Builder註解的bug

我們只用@Builder註解,我發現lombok為Officer類生成的構造器是“default”的(不新增許可權修飾符,預設為“default”的)。我們之所以用構建器模式,是希望使用者用構建器提供的方法去建立例項。但“default”的構造器,可以被同package的類呼叫(default限制不同package類的呼叫)。所以,我們需要將此構造器設為private的。這時就需要用到“@AllArgsConstructor(access = AccessLevel.PRIVATE)”。我們這時再看反編譯後的構造器:

private Officer(String id, String name, int age, String department) {        this.id = id;        this.name = name;        this.age = age;        this.department = department;
}

所以,使用lombok的構建器,應將“@Builder”和“@AllArgsConstructor(access = AccessLevel.PRIVATE)”相結合,最終寫法:

@Builder@AllArgsConstructor(access = AccessLevel.PRIVATE)public class Officer {    private final String id;    private final String name;    private final int age;    private final String department;
}

為什麼使用構建器模式

若一個類具有大量的成員變數,我們就需要提供一個全參的構造器或大量的set方法。這讓例項的建立和賦值,變得很麻煩,且不直觀。我們透過構建器,可以讓變數的賦值變成鏈式呼叫,而且呼叫的方法名對應著成員變數的名稱。讓物件的建立和賦值都變得很簡潔、直觀。

鏈式方法賦值,一定要用構建器模式嗎?

不一定要用到構建器模式,之所以使用構建器模式,是因為我們要創造的物件是一個成員變數不可變的物件。

你返回去看Officer類和OfficerBuilder類,你會發現Officer類的成員變數都是final的,而OfficerBuilder的成員變數卻沒用final修飾。因為final修飾的成員變數,需要在例項建立時就把值確定下來。但在類具有大量成員變數的時候,我們是不希望使用者直接呼叫全參構造器的。所以我們使用了OfficerBuilder的中間類。這個類為了實現鏈式賦值,才將變數設為非final的。無論你OfficerBuilder例項怎麼賦值,怎麼改變,但你呼叫build方法時,就會返回一個成員變數不可變的Officer例項。

那如果有大量屬性,但不需要它是成員變數不可變的物件,我們還需要構建器模式嗎?答案是,不需要,我們可以參考構建器,把程式碼賦值改成鏈式的即可:

public class Officer {    private String id;    private String name;    private int age;    private String department;    public static Officer build() {        return new Officer();
    }    private Officer() {
    }    public Officer id(String id) {        this.id = id;        return this;
    }    public Officer name(String name) {        this.name = name;        return this;
    }    public Officer age(int age) {        this.age = age;        return this;
    }    public Officer department(String department) {        this.department = department;        return this;
    }
}

呼叫樣式:
Officer officer = Officer.build().id("00001").name("simon qi").age(26).department("departmentA");
其實這時候構造器設為非private也行,寫成private,只是為了呼叫build()顯得更好看。
將構造器設為非private,可以寫為如下形式:
Officer officer = new Officer().id("00001").name("simon qi").age(26).department("departmentA");

所以,我覺得你在使用lombok的"@Builder"註解的時候,還是要思考一下。當你不需要成員變數不可變的時候,你完全沒必要使用構建器模式,因為這會消耗java虛擬機器的記憶體。

總結

所以,我一直推薦學習知識,要在專案中去學習。透過專案,去探索專案以外的知識點,才是提升自己的快捷方法。而且知識不能學死了,不能網上有哪些知識點,我們就只考慮這些知識點。我們要去思考一些別人不常想到的問題。比如,我們為什麼要用中間類去做過渡,這麼寫的目的是什麼。

將上述知識吃透,面試應對構建器的時候,也就得心應手了。而且透過實戰去回答問題,也更能彰顯你是個愛思考的員工。




作者:simonQi
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/964/viewspace-2815848/,如需轉載,請註明出處,否則將追究法律責任。

相關文章