[Java] 印出物件內容的好幫手 – ReflectionToStringBuilder (2) 排除指定欄位 toStringExclude 範例

Last Updated on 2019-07-21 by OneJar99

上一篇介紹了 ReflectionToStringBuilder 的基本用法,這篇進一步示範 ReflectionToStringBuilder 提供的另一個功能:排除指定名稱的欄位。

自訂類別裡可能有非常多成員變數,在印出自訂類別的內容來進行 log 儲存時,我們需要的也許只是其中較關鍵的幾項,印出所有變數不僅不具意義,甚至是浪費儲存空間。這時候就可以使用 ReflectionToStringBuilder 的 toStringExclude 功能,來排除不需要的欄位名稱。

ReflectionToStringBuilder 的 excludeFieldNames 變數

API 規格文件可以看到 ReflectionToStringBuilder 擁有一個成員變數「excludeFieldNames 」,顯示 ReflectionToStringBuilder 在印出物件的成員變數內容時,會以成員變數的名稱去比對這個陣列裡的名單,判斷是否排除。

以下示範如何使用。

ReflectionToStringBuilder 的 toStringExclude 基本範例

以下面這個例子作示範:

EX1: 尚未排除任何欄位名稱

public class Demo1
{
    class Mobile extends Device
    {
        private String brand = "Apple";
        private String os = "iOS 10";
        private String type = "iPhone 7";
        private String owner;
    }

    class Device extends Product
    {
        private String serialNumber = "MIT00001";
    }

    class Product extends BaseObj
    {
        private int price = 30000;
    }

    class BaseObj
    {
        @Override
        public String toString()
        {
            return ReflectionToStringBuilder.toString( this );
        }
    }

    @Test
    public void ex01_ReflectionToStringBuilder_example()
    {
        Mobile myPhone = new Mobile();
        System.out.println( myPhone );
    }
}

輸出結果:

com.pcc.demo.reflectToString.ex2.Demo1$Mobile@7aec35a[brand=Apple,os=iOS 10,type=iPhone 7,owner=<null>,serialNumber=MIT00001,price=30000]

在沒有設定排除任何屬性名稱之前,會印出該 Mobile 物件 myPhone 擁有的 6 個屬性,包含繼承自父類別的 serialNumber 和 price 屬性。

這時候我們希望排除 price 和 os 兩個屬性,寫法如下:

EX2: 排除指定名稱的屬性

/* Example 1 */
System.out.println( ReflectionToStringBuilder.toStringExclude( myPhone, "price", "os" ) );

/* Example 2 */
List<String> excludeFieldNameList = new ArrayList<>();
excludeFieldNameList.add( "price" );
excludeFieldNameList.add( "os" );
System.out.println( ReflectionToStringBuilder.toStringExclude( myPhone, excludeFieldNameList ) );

/* Example 3 */
String[] excludeFieldNameAry = new String[] { "price", "os" };
ReflectionToStringBuilder builder = new ReflectionToStringBuilder( myPhone );
builder.setExcludeFieldNames( excludeFieldNameAry );
System.out.println( builder.toString() );

上面 Example 1~3 只是寫法不同,效果一樣:

輸出結果:
com.pcc.demo.reflectToString.ex2.Demo1$Mobile@7aec35a[brand=Apple,type=iPhone 7,owner=<null>,serialNumber=MIT00001]

ReflectionToStringBuilder 的 toStringExclude 進階範例

再看另一個例子,Person 類別包含成員變數是 Mobile 類別,我們希望排除 favoriteOs、os、type 三個屬性名稱。

如果照以下寫法,預期會得到什麼?

EX3: 自訂類別的成員變數包含其他自訂類別

public class Demo2
{
    class Mobile extends BaseObj
    {
        private String brand = "Apple";
        private String os = "iOS 10";
        private String type = "iPhone 7";
        private String owner;
    }

    class Person extends BaseObj
    {
        private String name = "OneJar99";
        public String[] favoriteFruit;
        public List<String> favoriteOs;
        public Mobile mobile;
    }

    class BaseObj
    {
        @Override
        public String toString()
        {
            return ReflectionToStringBuilder.toString( this );
        }
    }
    
    @Test
    public void ex03_ReflectionToStringBuilder_excludeExample()
    {
        String[] fruitAry = new String[] { "Apple", "Banana", "Orange" };

        List<String> osList = new ArrayList<>();
        osList.add( "iOS" );
        osList.add( "Android" );

        Mobile myPhone = new Mobile();

        Person me = new Person();
        me.favoriteFruit = fruitAry;
        me.favoriteOs = osList;
        me.mobile = myPhone;

        System.out.println( "============ result 1 ===============" );
        System.out.println( me );
        
        System.out.println( "============ result 2 ===============" );
        System.out.println( ReflectionToStringBuilder.toStringExclude( me, "favoriteOs", "os", "type" ) );
    }
}

輸出結果:

============ result 1 ===============
com.pcc.demo.reflectToString.ex2.Demo2$Person@7aec35a[name=OneJar99,favoriteFruit={Apple,Banana,Orange},favoriteOs=[iOS, Android],mobile=com.pcc.demo.reflectToString.ex2.Demo2$Mobile@579bb367[brand=Apple,os=iOS 10,type=iPhone 7,owner=<null>]]
============ result 2 ===============
com.pcc.demo.reflectToString.ex2.Demo2$Person@7aec35a[name=OneJar99,favoriteFruit={Apple,Banana,Orange},mobile=com.pcc.demo.reflectToString.ex2.Demo2$Mobile@579bb367[brand=Apple,os=iOS 10,type=iPhone 7,owner=<null>]]

沒錯,os 和 type 屬性並沒有被排除,依然印出來。

這是因為上述寫法是針對「me」這個 Person 物件去進行排除,Person 物件裡的 mobile 屬性雖然透過 reference 參考到另一個 Mobile 物件,但他們仍然是兩個不同的物件實體,ReflectionToStringBuilder 的排除效果與 Mobile 物件無關。而 Person 並沒有名為 os 和 type 的屬性,等同於無效。

那該怎麼達到期望的效果呢?

通常來說,每個自訂類別裡那些屬性是需要印出的關鍵屬性,在開發設計階段就會決定。

每個類別都可能有需要排除的屬性,因此在 BaseObj 類別增加一個變數,負責記住每個自訂類別需要被排除的欄位名稱,在覆寫的 toString() 裡調整呼叫的 ReflectionToStringBuilder API,加入排除效果。

接著,由每個自訂類別透過建構子,個別依需求實作需要排除的屬性

EX4: 設定每個自訂類別需要被排除的屬性名稱

public class Demo3
{
    class Mobile extends BaseObj
    {
        private String brand = "Apple";
        private String os = "iOS 10";
        private String type = "iPhone 7";
        private String owner;

        public Mobile()
        {
            excludeFieldList.add( "os" ); // 在建構子實作要排除的屬性名稱
            excludeFieldList.add( "type" );
        }
    }

    class Person extends BaseObj
    {
        private String name = "OneJar99";
        public String[] favoriteFruit;
        public List<String> favoriteOs;
        public Mobile mobile;

        public Person()
        {
            excludeFieldList.add( "favoriteOs" ); // 在建構子實作要排除的屬性名稱
        }
    }

    class BaseObj
    {
        protected List<String> excludeFieldList = new ArrayList<>(); // 紀錄需要排除的屬性名稱

        @Override
        public String toString()
        {
            return ReflectionToStringBuilder.toStringExclude( this, excludeFieldList ); // 改呼叫 toStringExclude()
        }
    }

    @Test
    public void ex04_ReflectionToStringBuilder_excludeExample()
    {
        String[] fruitAry = new String[] { "Apple", "Banana", "Orange" };

        List<String> osList = new ArrayList<>();
        osList.add( "iOS" );
        osList.add( "Android" );

        Mobile myPhone = new Mobile();

        Person me = new Person();
        me.favoriteFruit = fruitAry;
        me.favoriteOs = osList;
        me.mobile = myPhone;

        System.out.println( me );
    }
}

輸出結果:

com.pcc.demo.reflectToString.ex2.Demo3$Person@42110406[name=OneJar99,favoriteFruit={Apple,Banana,Orange},mobile=com.pcc.demo.reflectToString.ex2.Demo3$Mobile@255316f2[brand=Apple,owner=<null>,excludeFieldList=[os, type]],excludeFieldList=[favoriteOs]]

如此成功達到排除 Mobile 物件的 os 和 type 屬性。

但由於在 BaseObj 類別增加 excludeFieldList 變數,而 BaseObj 被 Person、Mobile 類別繼承,因此這兩個物件都增加印出 excludeFieldList。但 excludeFieldList 變數的存在只是程式開發上的需求,並不具備紀錄 log 的意義,理想上也應該對其進行排除:

EX5: 將 BaseObj 類別的 excludeFieldList 屬性也加入排除名單

class BaseObj
{
    protected List<String> excludeFieldList = new ArrayList<>();
        
    public BaseObj()
    {
        excludeFieldList.add( "excludeFieldList" );
    }
    @Override
    public String toString()
    {
        return ReflectionToStringBuilder.toStringExclude( this, excludeFieldList );
    }
}

輸出結果:

com.pcc.demo.reflectToString.ex2.Demo3$Person@277050dc[name=OneJar99,favoriteFruit={Apple,Banana,Orange},mobile=com.pcc.demo.reflectToString.ex2.Demo3$Mobile@531d72ca[brand=Apple,owner=<null>]]

發表留言