1. 程式人生 > >四、原型模式與建造者模式詳解

四、原型模式與建造者模式詳解

5.原型模式

5.1.課程目標

1、掌握原型模式和建造者模式的應用場景

2、掌握原型模式的淺克隆和深克隆的寫法。

3、掌握建造者模式的基本寫法。

4、瞭解克隆是如何破壞單例的。

5、瞭解原型模式的優、缺點

6、掌握建造者模式和工廠模式的區別。

5.2.內容定位

1、已瞭解並掌握工廠模式的人群。

2、已瞭解並掌握單例模式。

3、聽說過原型模式,但不知道應用場景的人群。

5.3.定義

原型模式(PrototypePattern)是指原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件,屬於建立型模式。

官方原文:Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.

原型模式的核心在於拷貝原型物件。以系統中已存在的一個物件為原型,直接基於記憶體二進位制流進行拷貝,無需再經歷耗時的物件初始化過程(不呼叫建構函式),效能提升許多。當物件的構建過程比較耗時時,可以利用當前系統中已存在的物件作為原型,對其進行克隆(一般是基於二進位制流的複製),躲避初始化過程,使得新物件的建立時間大大減少。下面,我們來看看原型模式類結構圖:

從 UML 圖中,我們可以看到,原型模式 主要包含三個角色:

客戶(Client):客戶類提出建立物件的請求。

抽象原型(Prototype):規定拷貝介面。

具體原型(Concrete Prototype):被拷貝的物件。

注:對不通過 new 關鍵字,而是通過物件拷貝來實現建立物件的模式就稱作原型模式。

5.4.原型模式的應用場景

你一定遇到過大篇幅getter、setter賦值的場景。

程式碼非常工整,命名非常規範,註釋也寫的很全面,其實這就是原型模式的需求場景。但是,大家覺
得這樣的程式碼優雅嗎?我認為,這樣的程式碼屬於純體力勞動。那原型模式,能幫助我們解決這樣的問題。

原型模式主要適用於以下場景:

1、類初始化消耗資源較多。

2、new產生的一個物件需要非常繁瑣的過程(資料準備、訪問許可權等)

3、建構函式比較複雜。

4、迴圈體中生產大量物件時。

在 Spring 中,原型模式應用得非常廣泛。例如 scope=“prototype”,在我們經常用的
JSON.parseObject()也是一種原型模式。

5.5.原型模式的通用寫法(淺拷貝)

一個標準的原型模式程式碼,應該是這樣設計的。先建立原型IPrototype介面:

public interface IPrototype<T> {
    T clone();
}

建立具體需要克隆的物件ConcretePrototype

public class ConcretePrototype implements IPrototype {

    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public ConcretePrototype clone() {
        ConcretePrototype concretePrototype = new ConcretePrototype();
        concretePrototype.setAge(this.age);
        concretePrototype.setName(this.name);
        return concretePrototype;
    }

    @Override
    public String toString() {
        return "ConcretePrototype{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

測試程式碼:

public class Client {
    public static void main(String[] args) {
        //建立原型物件
        ConcretePrototype prototype = new ConcretePrototype();
        prototype.setAge(18);
        prototype.setName("Tom");
        System.out.println(prototype);

        //拷貝原型物件
        ConcretePrototype cloneType = prototype.clone();
        System.out.println(cloneType);
    }
}

執行結果:

ConcretePrototype{age=18, name='Tom'}
ConcretePrototype{age=18, name='Tom'}

這時候,有小夥伴就問了,原型模式就這麼簡單嗎?對,就是這麼簡單。在這個簡單的場景之下,看上
去操作好像變複雜了。但如果有幾百個屬性需要複製,那我們就可以一勞永逸。但是,上面的複製過程
是我們自己完成的,在實際編碼中,我們一般不會浪費這樣的體力勞動,JDK已經幫我們實現了一個現
成的API,我們只需要實現Cloneable介面即可。來改造一下程式碼,修改ConcretePrototype類:

@Data
public class ConcretePrototype implements Cloneable {

    private int age;
    private String name;

    @Override
    public ConcretePrototype clone() {
        try {
            return (ConcretePrototype)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}

重新執行,也會得到同樣的結果。

有了JDK的支援再多的屬性複製我們也能輕而易舉地搞定了。下面我
們再來做一個測試,給ConcretePrototype增加一個個人愛好的屬性hobbies:

@Data
public class ConcretePrototype implements Cloneable {

    private int age;
    private String name;
    private List<String> hobbies;

    @Override
    public ConcretePrototype clone() {
        try {
            return (ConcretePrototype)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}

修改客戶端測試程式碼:

public class Client {

    public static void main(String[] args) {
        //建立原型物件
        ConcretePrototype prototype = new ConcretePrototype();
        prototype.setAge(18);
        prototype.setName("Tom");
        List<String> hobbies = new ArrayList<String>();
        hobbies.add("書法");
        hobbies.add("美術");
        prototype.setHobbies(hobbies);

        //拷貝原型物件
        ConcretePrototype cloneType = prototype.clone();
        cloneType.getHobbies().add("技術控");
        System.out.println("原型物件:" + prototype);
        System.out.println("克隆物件:" + cloneType);
        System.out.println(prototype == cloneType);
        
        System.out.println("原型物件的愛好:" + prototype.getHobbies());
        System.out.println("克隆物件的愛好:" + cloneType.getHobbies());
        System.out.println(prototype.getHobbies() == cloneType.getHobbies());
    }
}

執行結果:

原型物件:ConcretePrototype(age=18, name=Tom, hobbies=[書法, 美術, 技術控])
克隆物件:ConcretePrototype(age=18, name=Tom, hobbies=[書法, 美術, 技術控])
false
原型物件的愛好:[書法, 美術, 技術控]
克隆物件的愛好:[書法, 美術, 技術控]
true

我們給,複製後的克隆物件新增一項愛好,發現原型物件也發生了變化,這顯然不符合我們的預期。
因為我們希望克隆出來的物件應該和原型物件是兩個獨立的物件,不應該再有聯絡了。從測試結果分析
來看,應該是hobbies共用了一個記憶體地址,意味著複製的不是值,而是引用的地址。這樣的話,如果我們修改任意一個物件中的屬性值,prototype 和cloneType的hobbies值都會改變。這就是我們常
說的淺克隆。只是完整複製了值型別資料,沒有賦值引用物件。換言之,所有的引用物件仍然指向原來
的物件,顯然不是我們想要的結果。那如何解決這個問題呢?下面我們來看深度克隆繼續改造。

擴充套件知識:String物件在記憶體中是不可變的(final型別),雖然克隆後,兩個物件String的引用指向的是同一個記憶體地址,但是如果給克隆後的物件的String屬性改變值,那麼相當於是在記憶體中重新開闢了一塊記憶體來儲存這個改變的值,而此時的String屬性物件就指向了該記憶體值,所以這個時候克隆前和克隆後物件的String屬性是不一樣的)。

String 每次賦值,相當於new String()。

5.6.使用序列化實現深度克隆

在上面的基礎上我們繼續改造,來看程式碼,增加一個deepClone()方法:

@Data
public class ConcretePrototype implements Cloneable,Serializable {

    private int age;
    private String name;
    private List<String> hobbies;

    @Override
    public ConcretePrototype clone() {
        try {
            return (ConcretePrototype)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    public ConcretePrototype deepClone(){
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (ConcretePrototype)ois.readObject();
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }
}

來看客戶端呼叫程式碼:

public class Client {
    public static void main(String[] args) {
        //建立原型物件
        ConcretePrototype prototype = new ConcretePrototype();
        prototype.setAge(18);
        prototype.setName("Tom");
        List<String> hobbies = new ArrayList<String>();
        hobbies.add("書法");
        hobbies.add("美術");
        prototype.setHobbies(hobbies);

        //拷貝原型物件
        ConcretePrototype cloneType = prototype.deepClone();
        cloneType.getHobbies().add("技術控");

        System.out.println("原型物件:" + prototype);
        System.out.println("克隆物件:" + cloneType);
        System.out.println(prototype == cloneType);

        System.out.println("原型物件的愛好:" + prototype.getHobbies());
        System.out.println("克隆物件的愛好:" + cloneType.getHobbies());
        System.out.println(prototype.getHobbies() == cloneType.getHobbies());
    }
}

執行程式,我們發現得到了我們期望的結果:

原型物件:ConcretePrototype(age=18, name=Tom, hobbies=[書法, 美術])
克隆物件:ConcretePrototype(age=18, name=Tom, hobbies=[書法, 美術, 技術控])
false
原型物件的愛好:[書法, 美術]
克隆物件的愛好:[書法, 美術, 技術控]
false

5.7.克隆破壞單例模式

如果我們克隆的目標的物件是單例物件,那意味著,深克隆就會破壞單例。實際上防止克隆破壞單
例解決思路非常簡單,禁止深克隆便可。要麼你我們的單例類不實現 Cloneable 介面;要麼我們重寫
clone()方法,在clone方法中返回單例物件即可,具體程式碼如下:

@Override
protected Object clone() throws CloneNotSupportedException {
    return INSTANCE;
}

5.8.原型模式在原始碼中的應用

先來JDK中Cloneable介面:

public interface Cloneable {
}

介面定義還是很簡單的,我們找原始碼其實只需要找到看哪些介面實現了 Cloneable 即可。來看
ArrayList類的實現。

Object方法

protected native Object clone() throws CloneNotSupportedException;

ArrayList是實現的clone方法

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }
}

我們發現方法中只是將List中的元素迴圈遍歷了一遍。這個時候我們再思考一下,是不是這種形式
就是深克隆呢?其實用程式碼驗證一下就知道了,繼續修改 ConcretePrototype 類,增加一個
deepCloneHobbies()方法:

@Data
public class ConcretePrototype implements Cloneable,Serializable {
    ...
    public ConcretePrototype deepCloneHobbies(){
        try {
            ConcretePrototype result = (ConcretePrototype)super.clone();
            result.hobbies = (List)((ArrayList)result.hobbies).clone();
            return result;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
    ...
}

修改客戶端程式碼:

public class Client {
    public static void main(String[] args) {
        ...
        //拷貝原型物件
        ConcretePrototype cloneType = prototype.deepCloneHobbies();
        ...
    }
}

執行也能得到期望的結果。但是這樣的程式碼,其實是硬編碼,如果在物件中聲明瞭各種集合型別,
那每種情況都需要單獨處理。因此,深克隆的寫法,一般會直接用序列化來操作。

5.9.原型模式的優缺點

優點:

1、效能優良,Java自帶的 原型模式 是基於記憶體二進位制流的拷貝,比直接new一個物件效能上提
升了許多。

2、可以使用深克隆方式儲存物件的狀態,使用原型模式將物件複製一份並將其狀態儲存起來,簡化
了建立物件的過程,以便在需要的時候使用(例如恢復到歷史某一狀態),可輔助實現撤銷操作。

缺點:

1、需要為每一個類配置一個克隆方法。

2、克隆方法位於類的內部,當對已有類進行改造的時候,需要修改程式碼,違反了開閉原則。

3、在實現深克隆時需要編寫較為複雜的程式碼,而且當物件之間存在多重巢狀引用時,為了實現深克
隆,每一層物件對應的類都必須支援深克隆,實現起來會比較麻煩。因此,深拷貝、淺拷貝需要運用得
當。

6.0總結

克隆方式:1.序列化 反序列化 2.jsonobject 3淺克隆加賦值

淺克隆:繼承Cloneable介面的都是淺克隆。

深克隆兩種方式:序列化,轉JSON。

6.建造者模式

6.1.定義

建造者模式(Builder Pattern)是將一個複雜物件的構建過程與它的表示分離,使得同樣的構建過
程可以建立不同的表示,屬於建立型模式。使用建造者模式對於使用者而言只需指定需要建造的型別就可
以獲得物件,建造過程及細節不需要了解。

官方原文:Separate the construction of a complex object from its representation so that the same construction process can create different representations.

建造者模式適用於建立物件需要很多步驟,但是步驟的順序不一定固定。如果一個物件有非常複雜
的內部結構(很多屬性),可以將複雜物件的建立和使用進行分離。先來看一下建造者模式的類圖:

建造者模式的設計中主要有四個角色:

1、產品(Product):要建立的產品類物件

2、建造者抽象(Builder):建造者的抽象類,規範產品物件的各個組成部分的建造,一般由子類
實現具體的建造過程。

3、建造者(ConcreteBuilder):具體的Builder類,根據不同的業務邏輯,具體化物件的各個組成
部分的建立。

4、呼叫者(Director):呼叫具體的建造者,來建立物件的各個部分,在指導者中不涉及具體產品
的資訊,只負責保證物件各部分完整建立或按某種順序建立。

6.2.建造者模式的應用場景

建造者模式適用於一個具有較多的零件的複雜產品的建立過程,由於需求的變化,組成這個複雜產
品的各個零件經常猛烈變化,但是它們的組合方式卻相對穩定。

  1. 相同的方法,不同的執行順序,產生不同的結果時
  2. 多個部件或零件,都可以裝配到一個物件中,但是產生的結果又不相同。
  3. 產品類非常複雜,或者產品類中的呼叫順序不同產生不同的作用。
  4. 當初始化一個物件特別複雜,引數多,而且很多引數都具有預設值時。

建造者模式,只關注使用者需要什麼,將最少的關鍵字傳過來,生成你想要的結果。

實際順序是在build方法裡面。那是順序和條件都確定了。每個順序和條件都分別儲存下來了。判斷有沒有,有就新增到product後面。當然就是先判斷條件再判斷order順序了

6.3.建造者模式的基本寫法

我們還是以課程為例,一個完整的課程需要由PPT課件、回放視訊、課堂筆記、課後作業組成,但
是這些內容的設定順序可以隨意調整,我們用建造者模式來代入理解一下。首先我們建立一個需要構造
的產品類Course:

@Data
public class Course {
    private String name;
    private String ppt;
    private String video;
    private String note;
    private String homework;
}

然後建立建造者類CourseBuilder,將複雜的構造過程封裝起來,構造步驟由使用者決定:

public class CourseBuilder{

    private Course course = new Course();

    public void addName(String name) {
        course.setName(name);
    }
    
    public void addPPT(String ppt) {
        course.setPpt(ppt);
    }
    
    public void addVideo(String video) {
        course.setVideo(video);
    }
    
    public void addNote(String note) {
        course.setNote(note);
    }
    
    public void addHomework(String homework) {
        course.setHomework(homework);
    }
    
    public Course build() {
        return course;
    }
}

編寫測試類:

public class Test {
    public static void main(String[] args) {
        CourseBuilder builder = new CourseBuilder();
        builder.addName("設計模式");
        builder.addPPT("【PPT課件】");
        builder.addVideo("【回放視訊】");
        builder.addNote("【課堂筆記】");
        builder.addHomework("【課後作業】");
        System.out.println(builder.build());
    }
}

執行結果:

Course(name=設計模式, ppt=【PPT課件】, video=【回放視訊】, note=【課堂筆記】, homework=【課後作業】)

來看一下類結構圖:

6.4.建造者模式的鏈式寫法

在平時的應用中,建造者模式通常是採用鏈式程式設計的方式構造物件,下面我們來一下演示程式碼,修
改CourseBuilder類,將Course變為CourseBuilder的內部類。然後,將構造步驟新增進去,每完成一個步驟,都返回this:

public class CourseBuilder {
    private Course course = new Course();

    public CourseBuilder addName(String name) {
        course.setName(name);
        return this;
    }

    public CourseBuilder addPPT(String ppt) {
        course.setPpt(ppt);
        return this;
    }

    public CourseBuilder addVideo(String video) {
        course.setVideo(video);
        return this;
    }

    public CourseBuilder addNote(String note) {
        course.setNote(note);
        return this;
    }

    public CourseBuilder addHomework(String homework) {
        course.setHomework(homework);
        return this;
    }

    public Course build() {
        return this.course;
    }

    @Data
    public class Course {
        private String name;
        private String ppt;
        private String video;
        private String note;
        private String homework;
    }
}

客戶端使用:

public class Test {
    public static void main(String[] args) {
        CourseBuilder builder = new CourseBuilder()
                    .addName("設計模式")
                    .addPPT("【PPT課件】")
                    .addVideo("【回放視訊】")
                    .addNote("【課堂筆記】")
                    .addHomework("【課後作業】");
        System.out.println(builder.build());
    }
}

這樣寫法是不是很眼熟,好像在哪見過呢?後面我們分析建造者模式在原始碼中的應用大家就會明白。
接下來,我們再來看一下類圖的變化:

6.5.建造者模式應用案例

下面我們再來看一個實戰案例,這個案例參考了開源框架JPA的SQL構造模式。是否記得我們在構
造SQL查詢條件的時候,需要根據不同的條件來拼接SQL字串。如果查詢條件複雜的時候,我們SQL
拼接的過程也會變得非常複雜,從而給我們的程式碼維護帶來非常大的困難。因此,我們用建造者類
QueryRuleSqlBuilder 將複雜的構造 SQL 過程進行封裝,用 QueryRule 物件專門儲存 SQL 查詢時的
條件,最後根據查詢條件,自動生成SQL語句。來看程式碼,先建立QueryRule類:

/**
 * QueryRule,主要功能用於構造查詢條件
 */
public final class QueryRule implements Serializable
{
    ... 
    /**
     * 添加升序規則
     * @param propertyName
     * @return
     */
    public QueryRule addAscOrder(String propertyName) {
        this.ruleList.add(new Rule(ASC_ORDER, propertyName));
        return this;
    }
    
    public QueryRule andEqual(String propertyName, Object value) {
        this.ruleList.add(new Rule(EQ, propertyName, new Object[] { value }).setAndOr(AND));
        return this;
    }
    
    public QueryRule andLike(String propertyName, Object value) {
        this.ruleList.add(new Rule(LIKE, propertyName, new Object[] { value }).setAndOr(AND));
        return this;
    }
    ...
}

然後,建立QueryRuleSqlBuilder類:

/**
 * 根據QueryRule自動構建sql語句
 */
public class QueryRuleSqlBuilder {
    ...   
   /**
    * 處理like
    * @param rule
    */
   private  void processLike(QueryRule.Rule rule) {
      if (ArrayUtils.isEmpty(rule.getValues())) {
         return;
      }
      Object obj = rule.getValues()[0];

      if (obj != null) {
         String value = obj.toString();
         if (!StringUtils.isEmpty(value)) {
            value = value.replace('*', '%');
            obj = value;
         }
      }
      add(rule.getAndOr(),rule.getPropertyName(),"like","%"+rule.getValues()[0]+"%");
   }
    
    /**
     * 處理 =
     * @param rule
     */
    private  void processEqual(QueryRule.Rule rule) {
        if (ArrayUtils.isEmpty(rule.getValues())) {
            return;
        }
        add(rule.getAndOr(),rule.getPropertyName(),"=",rule.getValues()[0]);
    }
    
    /**
     * 處理 order by
     * @param rule 查詢規則
     */
    private void processOrder(Rule rule) {
        switch (rule.getType()) {
        case QueryRule.ASC_ORDER:
            // propertyName非空
            if (!StringUtils.isEmpty(rule.getPropertyName())) {
                orders.add(Order.asc(rule.getPropertyName()));
            }
            break;
        case QueryRule.DESC_ORDER:
            // propertyName非空
            if (!StringUtils.isEmpty(rule.getPropertyName())) {
                orders.add(Order.desc(rule.getPropertyName()));
            }
            break;
        default:
            break;
        }
    }
    ...
}

建立Order類:

/**
 * sql排序元件
 */
public class Order {
   private boolean ascending; //升序還是降序
   private String propertyName; //哪個欄位升序,哪個欄位降序
   
   public String toString() {
      return propertyName + ' ' + (ascending ? "asc" : "desc");
   }

   /**
    * Constructor for Order.
    */
   protected Order(String propertyName, boolean ascending) {
      this.propertyName = propertyName;
      this.ascending = ascending;
   }

   /**
    * Ascending order
    *
    * @param propertyName
    * @return Order
    */
   public static Order asc(String propertyName) {
      return new Order(propertyName, true);
   }

   /**
    * Descending order
    *
    * @param propertyName
    * @return Order
    */
   public static Order desc(String propertyName) {
      return new Order(propertyName, false);
   }
}

編寫測試程式碼:

public class Test {
    public static void main(String[] args) {
        QueryRule queryRule = QueryRule.getInstance();
        queryRule.addAscOrder("age");
        queryRule.andEqual("addr","Changsha");
        queryRule.andLike("name","Tom");
        QueryRuleSqlBuilder builder = new QueryRuleSqlBuilder(queryRule);
        System.out.println(builder.builder("t_member"));
        System.out.println("Params: " + Arrays.toString(builder.getValues()));
    }
}

這樣一來,我們的客戶端程式碼就非常清朗,來看執行結果:

select * from t_member where  addr = ?  and name like ?  order by age asc
Params: [Changsha, %Tom%]

6.6.建造者模式在原始碼中的體現

下面來看建造者模式在哪些原始碼中有應用呢?首先來看JDK的StringBuilder,它提供append()方
法,給我們開放構造步驟,最後呼叫toString()方法就可以獲得一個構造好的完整字串,原始碼如下:

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
    public StringBuilder append(StringBuffer sb) {
        super.append(sb);
        return this;
    }
}

在MyBatis中也有體現,比如CacheBuilder類。

同樣在 MyBatis 中,比如 SqlSessionFactoryBuilder 通過呼叫 build()方法獲得的是一個
SqlSessionFactory 類。

當然,在 Spring中自然也少不了,比如 BeanDefinitionBuilder 通過呼叫getBeanDefinition()方法獲得一個BeanDefinition物件。

6.7.建造者模式的優缺點

建造者模式的優點:

1、封裝性好,建立和使用分離;

2、擴充套件性好,建造類之間獨立、一定程度上解耦。

建造者模式的缺點:

1、產生多餘的Builder物件;

2、產品內部發生變化,建造者都要修改,成本較大。

6.8.建造者模式和工廠模式的區別

建造者模式和工廠模式的區別

1、建造者模式更加註重方法的呼叫順序,工廠模式注重於建立物件。

2、建立物件的力度不同,建造者模式建立複雜的物件,由各種複雜的部件組成,工廠模式創建出來
的都一樣。

3、關注重點不一樣,工廠模式模式只需要把物件創建出來就可以了,而建造者模式中不僅要創建出
這個物件,還要知道這個物件由哪些部件組成。

4、建造者模式根據建造過程中的順序不一樣,最終的物件部件組成也不一樣。

可以理解為工廠建立過程是靜態的,構建者模式建立過程經過外放而變成動態的。

6.9.總結

7.0.作業

1.用JSON方式實現一個原型模式的深克隆,並畫出UML圖。

一行程式碼,比IO流簡單。

public ConcretePrototype deepCloneByJSON(){
    try {
        return JSON.parseObject(JSON.toJSONString(this), ConcretePrototype.class);
    }catch (Exception e){
        e.printStackTrace();
        return null;
    }
}

2.請列舉1-3個需要用到建造者模式的業務場景。

建造者模式:適用於物件建立需要動態拼接複雜屬性值的業務場景。

例如:SQL拼接,鏈式程式設計,NIO