1. 程式人生 > 實用技巧 >詳解 Java 內部類

詳解 Java 內部類

原型模式

案例

公司使用自行開發的一套系統進行日常工作辦理,但在使用過程中,我們需要每週上傳週報總結工作內容。基於此,我們簡單的通過程式碼模擬這一過程。

1.首先定義一個週報類:

public class WeeklyReport {
    // 填寫人
    private String name;
    // 週報內容
    private String content;
    // 上報時間
    private String date;

    @Override
    public String toString() {
        return "姓名='" + name + '\'' +
                ", 內容='" + content + '\'' +
                ", 周次='" + date + '\'';
    }
    // 省略getter/setter
}

2.客戶端使用:

public class Main {
    public static void main(String[] args) {
        WeeklyReport weeklyReport1 = new WeeklyReport();
        weeklyReport1.setName("張三");
        weeklyReport1.setContent("一週都在努力工作~~");
        weeklyReport1.setDate("第一週");
        System.out.println("第一週週報:" + weeklyReport1);

        WeeklyReport weeklyReport2 = new WeeklyReport();
        weeklyReport2.setName("張三");
        weeklyReport2.setContent("一週都在努力工作~~");
        weeklyReport2.setDate("第二週");
        System.out.print("第二週週報:" + weeklyReport2);
    }
}

其實我們在填寫週報的時候,其實會有很多必要但又不太重要的描述需要填寫。比如以上面的程式碼來看,只有date(週報時間)是變化的,而name(填寫人)和content(週報內容部分)是不變的,然而我們也不得不重新填寫不變的部分。如果我們能夠複製一份相同的內容,然後對其只修改不一樣的地方,對於我們的工作來說就相對簡單一點,這樣我們就可以引入原型模式。

原型模式

原型模式是一種建立型設計模式,Prototype模式允許一個物件再建立另外一個可定製的物件,根本無需知道任何如何建立的細節,工作原理是:通過將一個原型物件傳給那個要發動建立的物件,這個要發動建立的物件通過請求原型物件拷貝它們自己來實施建立。

角色構成:

  • Prototype(抽象原型類):它是宣告克隆方法的介面,是所有具體原型類的公共父類,可以是抽象類也可以是介面,甚至還可以是具體實現類。
  • ConcretePrototype(具體原型類):它實現在抽象原型類中宣告的克隆方法,在克隆方法中返回自己的一個克隆物件。

特點:

一個原型物件克隆自身從而建立一個新的物件,在客戶類中只需要直接例項化或通過工廠方法等方式建立一個原型物件,再通過呼叫該物件的克隆方法即可得到多個相同的物件。由於客戶類針對抽象原型類Prototype程式設計,因此使用者可以根據需要選擇具體原型類,系統具有較好的可擴充套件性,增加或更換具體原型類都很方便。

程式碼改造

1.抽象類:

/**
 * 抽象類
 */
public abstract class Prototype {
    // 實現這個介面,返回自身物件的克隆物件
    public abstract Prototype create();
    private String name;
    private String content;
    private String date;

    @Override
    public String toString() {
        return "姓名='" + name + '\'' +
                ", 內容='" + content + '\'' +
                ", 周次='" + date + '\'';
    }
    // 省略getter/setter
}

2.週報類:

/**
 * 具體實現類
 */
public class WeeklyReport extends Prototype {
    // 實現介面,返回自身物件的克隆物件
    public Prototype create() {
        WeeklyReport weeklyReport = new WeeklyReport();
        weeklyReport.setName(this.getName());
        weeklyReport.setContent(this.getContent());
        weeklyReport.setDate(this.getDate());
        return weeklyReport;
    }
}

3.客戶端使用:

public class Main {
    public static void main(String[] args) {
        Prototype weeklyReport1 = new WeeklyReport();
        weeklyReport1.setName("張三");
        weeklyReport1.setContent("一週都在努力工作~~");
        weeklyReport1.setDate("第一週");
        System.out.println("第一週週報:" + weeklyReport1);

        // 直接克隆使用,減少常見的屬性賦值操作
        Prototype weeklyReport2 = weeklyReport1.create();
        weeklyReport2.setDate("第二週");
        System.out.println("第一週週報:" + weeklyReport2);
    }
}

通過上面的改造,我們在寫後面的週報的時候就可以基於第一次寫的週報修改其中我們需要修改的屬性,就可以快速的簡單的創建出物件出來。這樣我們的物件在建立時需要設定大量相同屬性時,就會很有用。

學過Java語言的人都知道,所有的Java類都繼承自java.lang.Object。事實上,Object類提供一個clone()方法,可以將一個Java物件複製一份。因此在Java中可以直接使用Object提供的clone()方法來實現物件的克隆,Java語言中的原型模式實現很簡單。只需要重寫clone()方法就可以了,此時,Object類相當於抽象原型類,所有實現了Cloneable介面的類相當於具體原型類。

這裡有兩種克隆方式,稱之為淺克隆和深克隆。

  • 淺克隆:在淺克隆中,如果原型物件的成員變數是值型別,將複製一份給克隆物件;如果原型物件的成員變數是引用型別,則將引用物件的地址複製一份給克隆物件,也就是說原型物件和克隆物件的成員變數指向相同的記憶體地址。簡單來說,在淺克隆中,當物件被複制時只複製它本身和其中包含的值型別的成員變數,而引用型別的成員物件並沒有複製。比如下面的程式碼:
public class WeeklyReportJDK implements Cloneable {
    public static void main(String[] args) {
        WeeklyReportJDK weeklyReport1 = new WeeklyReportJDK();
        weeklyReport1.setName("張三");
        weeklyReport1.setContent("一週都在努力工作~~");
        weeklyReport1.setDate("第一週");
        System.out.println("第一週週報:" + weeklyReport1);

        // 直接使用jdk克隆,減少常見的屬性賦值操作
        WeeklyReportJDK weeklyReport2 = weeklyReport1.clone();
        weeklyReport2.setDate("第二週");
        System.out.println("第一週週報:" + weeklyReport2);
    }
    // 填寫人
    private String name;
    // 週報內容
    private String content;
    // 上報時間
    private String date;

    // 重寫 Object 中的方法
    public WeeklyReportJDK clone() {
        WeeklyReportJDK clone = null;
        try {
            clone = (WeeklyReportJDK) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }

    @Override
    public String toString() {
        return "姓名='" + name + '\'' +
                ", 內容='" + content + '\'' +
                ", 周次='" + date + '\'';
    }
    // 省略getter/setter
}
  • 深克隆:在深克隆中,無論原型物件的成員變數是值型別還是引用型別,都將複製一份給克隆物件,深克隆將原型物件的所有引用物件也複製一份給克隆物件。簡單來說,在深克隆中,除了物件本身被複制外,物件所包含的所有成員變數也將複製。比如說我們的週報中含有附件Attachment物件:
public class WeeklyReportJDKDeep implements Cloneable {
    public static void main(String[] args) {
        WeeklyReportJDKDeep weeklyReport1 = new WeeklyReportJDKDeep();
        weeklyReport1.setName("張三");
        weeklyReport1.setContent("一週都在努力工作~~");
        Attachment attachment = new Attachment();
        attachment.setName("附件一");
        weeklyReport1.setAttachment(attachment);
        weeklyReport1.setDate("第一週");
        System.out.println("第一週週報:" + weeklyReport1);

        // 直接使用jdk克隆,減少常見的屬性賦值操作
        WeeklyReportJDKDeep weeklyReport2 = weeklyReport1.clone();
        weeklyReport2.setDate("第二週");
        System.out.println("第一週週報:" + weeklyReport2);
        // 這裡使用深克隆之後,是兩個物件,所以 false
        System.out.println(weeklyReport1.getAttachment() == weeklyReport2.getAttachment());
    }
    // 填寫人
    private String name;
    // 週報內容
    private String content;
    // 上報時間
    private String date;
    // 週報附件
    private Attachment attachment;

    // 重寫 Object 中的方法
    public WeeklyReportJDKDeep clone() {
        WeeklyReportJDKDeep clone = null;
        try {
            clone = (WeeklyReportJDKDeep) super.clone();
            clone.setAttachment(attachment.clone());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }

    @Override
    public String toString() {
        return "姓名='" + name + '\'' +
                ", 內容='" + content + '\'' +
                ", 周次='" + date + '\'';
    }
    // 省略getter/setter
}
class Attachment implements Cloneable {
    private String name;

    @Override
    public Attachment clone() {
        Attachment clone = null;
        try {
            clone = (Attachment) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }
    // 省略getter/setter
}

注意:Java語言提供的Cloneable介面和Serializable介面的程式碼非常簡單,它們都是空介面,這種空介面也稱為標識介面,標識介面中沒有任何方法的定義,其作用是告訴JRE這些介面的實現類是否具有某個功能,如是否支援克隆、是否支援序列化等。

模式應用

我們使用 Spring 來配置 Bean 時,可以通過 scope 來區分建立的物件是單例的還是圓形的。如果是單例的那麼每次從 Spring 中獲取 Bean 就都是同一個;如果是原型的那麼每次獲取時就是不同的。

​ 1.pom 檔案:

<properties>
    <spring.version>5.1.15.RELEASE</spring.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
</dependencies>

​ 2.實體類:

public class Person {
    private String name;
    private Integer age;
    // 省略getter/setter
}

​ 3.spring 配置檔案:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="person" class="com.phoegel.prototype.analysis.Person" scope="prototype">
        <property name="name" value="張三"/>
        <property name="age" value="18"/>
    </bean>
</beans>

​ 4.獲取 Bean 例項:

public class Main {
    public static void main(String[] args) {
        // spring 配置檔案
        String config = "applicationContext.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
        Person person1 = (Person) applicationContext.getBean("person");
        Person person2 = (Person) applicationContext.getBean("person");
        System.out.println(person1 == person2);// false
    }
}

可以看到我們在配置 Bean 時scope="prototype"屬性使得,我們兩次獲取到的person都是不同的。通過深入原始碼後,我們找到了 Bean 建立時在AbstractBeanFactorydoGetBean()的核心判斷程式碼:

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
      @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
    // ...
    // Create bean instance.
    			// 這裡判斷是否設定為單例模式
				if (mbd.isSingleton()) {
					sharedInstance = getSingleton(beanName, () -> {
						try {
							return createBean(beanName, mbd, args);
						}
						catch (BeansException ex) {
							// Explicitly remove instance from singleton cache: It might have been put there
							// eagerly by the creation process, to allow for circular reference resolution.
							// Also remove any beans that received a temporary reference to the bean.
							destroySingleton(beanName);
							throw ex;
						}
					});
					bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
				}
				// 這裡判斷是否設定為原型模式
				else if (mbd.isPrototype()) {
					// It's a prototype -> create a new instance.
					Object prototypeInstance = null;
					try {
						beforePrototypeCreation(beanName);
						prototypeInstance = createBean(beanName, mbd, args);
					}
					finally {
						afterPrototypeCreation(beanName);
					}
					bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
				}
    // ...
}

總結

1.主要優點:

  • 當建立新的物件例項較為複雜時,使用原型模式可以簡化物件的建立過程,通過複製一個已有例項可以提高新例項的建立效率。
  • 擴充套件性較好,由於在原型模式中提供了抽象原型類,在客戶端可以針對抽象原型類進行程式設計,而將具體原型類寫在配置檔案中,增加或減少產品類對原有系統都沒有任何影響。
  • 原型模式提供了簡化的建立結構,工廠方法模式常常需要有一個與產品類等級結構相同的工廠等級結構,而原型模式就不需要這樣,原型模式中產品的複製是通過封裝在原型類中的克隆方法實現的,無須專門的工廠類來建立產品。
  • 可以使用深克隆的方式儲存物件的狀態,使用原型模式將物件複製一份並將其狀態儲存起來,以便在需要的時候使用(如恢復到某一歷史狀態),可輔助實現撤銷操作。

2.主要缺點:

  • 需要為每一個類配備一個克隆方法,而且該克隆方法位於一個類的內部,當對已有的類進行改造時,需要修改原始碼,違背了“開閉原則”。
  • 在實現深克隆時需要編寫較為複雜的程式碼,而且當物件之間存在多重的巢狀引用時,為了實現深克隆,每一層物件對應的類都必須支援深克隆,實現起來可能會比較麻煩。

3.適用場景:

  • 建立新物件成本較大(如初始化需要佔用較長的時間,佔用太多的CPU資源或網路資源),新的物件可以通過原型模式對已有物件進行復制來獲得,如果是相似物件,則可以對其成員變數稍作修改。
  • 如果系統要儲存物件的狀態,而物件的狀態變化很小,或者物件本身佔用記憶體較少時,可以使用原型模式配合備忘錄模式來實現。
  • 需要避免使用分層次的工廠類來建立分層次的物件,並且類的例項物件只有一個或很少的幾個組合狀態,通過複製原型物件得到新例項可能比使用建構函式建立一個新例項更加方便。

參考資料

本篇文章github程式碼地址:https://github.com/Phoegel/design-pattern/tree/main/prototype
轉載請說明出處,本篇部落格地址:https://www.cnblogs.com/phoegel/p/13909477.html