[Java] 印出物件內容的好幫手 – ReflectionToStringBuilder (1) 基本用法介紹與範例

在開發程式時,時常需要印出物件屬性內容來進行 log 或 debug。最簡單的方法就是呼叫物件的原生 toString(),但當物件不是基本型態(Primitive Data Types),而是陣列或自訂類別,原生的 toString() 可能就不敷使用。去逐個類別覆寫(Override) toString() 函式又過於繁瑣且不切實際。

這篇文章將介紹一個方便的 API —— ReflectionToStringBuilder,該 API 利用映射技巧,協助我們便利地印出物件資訊。

 

原生 toString() 的限制

底下是一個使用物件原生 toString() 的簡單範例:

EX1: 原生 toString()

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

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

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

        Mobile myPhone = new Mobile();

        // toString
        System.out.println( fruitAry ); // "[Ljava.lang.String;@2e5d6d97"
        System.out.println( osList ); // "[iOS, Android]"
        System.out.println( myPhone ); // "[email protected]"
    }
}

 

可以觀察到:

  1. 列印 List 類的 String 集合時,由於原生 Java 已經覆寫過集合類別的 toString,能印出 List 的元素內容 (可參考 AbstractCollection.toString() )。
  2. 列印 Array 或自訂類別 Mobile 時,就只印出物件 class 名稱 hash code
註:hash code 的介紹可參考這篇文章,概念上先簡單理解為「物件儲存的記憶體位置」即可。
熟悉 Java 的開發者馬上能反應:可以使用 java.util.Arrays 的 toString():
System.out.println( Arrays.toString( fruitAry ) ); // "[Apple, Banana, Orange]"

 

是的,針對 Array 類型我們可以使用 Arrays.toString(),但這代表每次要印出物件內容時,必須先判斷物件的類型是 Array 才能針對其使用。

而且其他自訂類別呢?當你的專案有上千個自訂類別,逐個物件覆寫 toString() 顯然不是一個好主意。

我們的目標只是印出物件屬性內容,非常單純,無涉任何商業邏輯,難道沒有更一致性的方法嗎?當然有,就是本篇要介紹的主角 —— ReflectionToStringBuilder

ReflectionToStringBuilder 基本用法

ReflectionToStringBuilder (org.apache.commons.lang3.builder.ReflectionToStringBuilder) 是 Apache Common Lang 的 API 之一,利用映射技巧取得物件屬性內容並加以印出。

詳細規格可參考 API 文件 。

ReflectionToStringBuilder 可以建立實體 ReflectionToStringBuilder 物件操作,也提供靜態函式的呼叫。以下示範靜態函式的使用方式,呼叫上非常簡單:

EX2: ReflectionToStringBuilder 入門用法

System.out.println( ReflectionToStringBuilder.toString( fruitAry ) ); 
//"[Ljava.lang.String;@2e5d6d97[{Apple,Banana,Orange}]"

System.out.println( ReflectionToStringBuilder.toString( osList ) ); 
//"[email protected][size=2]"

System.out.println( ReflectionToStringBuilder.toString( myPhone ) ); 
//"[email protected][brand=Apple,os=iOS 10,type=iPhone 7,owner=<null>]"

可以觀察到:

  1. 不管是什麼物件,都會印出 class 名稱hash code物件內容三種資訊。比起只印出物件內容,保留 class 名稱的資訊在許多時候對 debug 很有幫助。
  2. Array 類型的物件內容會印出陣列的元素內容。
  3. 自訂類別 Mobile 的物件內容會逐一印出屬性內容。更精確來說,應該是「會取得該類別所宣告的成員變數,並呼叫該成員變數的 toString()」。因此即使該屬性的作用域是私有(private),不具備公開(public)的 getter,ReflectionToStringBuilder 依舊會取得所有成員變數,並協助逐一 toString()。
  4. List 類的 String 集合只印出 size,甚至不是呼叫原生 toString() 去印出集合內容。這是我個人較為疑惑和覺得不足的一點。

在自訂類別部分最能感受到 ReflectionToStringBuilder 方便性。

再看一個常見的自訂類別用法:

EX3: ReflectionToStringBuilder 對自訂類別的應用

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

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

public class Demo
{
    @Test
    public void ex03_ReflectionToStringBuilder_basic()
    {
        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( ReflectionToStringBuilder.toString( me ) );
    }
}

輸出結果:

[email protected][name=OneJar99,favoriteFruit={Apple,Banana,Orange},favoriteOs=[iOS, Android],[email protected]]

由於 ReflectionToStringBuilder 取得成員變數後會呼叫原生 toString(), Person 類別裡的 List 變數內容順利被印出來。

但同時也可以注意到,由於呼叫的是成員變數原生的 toString(),因此 Person 類別裡的 Mobile 類別成員也僅印出 class 名稱和 hash code,不免讓人覺得美中不足。

如果自訂類別裡又有其他的自訂類別,可以讓它自動逐層取得物件內容嗎?

當然可以!只要增加個簡單的設計變化。

 

ReflectionToStringBuilder 衍伸用法

為了讓每個自訂類別在 toString() 時都能夠擁有 ReflectionToStringBuilder 的效果,一個典型的作法就是宣告一個 Base 類別,讓所有自訂類別繼承,並在 Base 類別裡覆寫 toString()

EX4: 宣告 BaseObj 並以 ReflectionToStringBuilder 覆寫 toString()

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

以 EX3 為例,就是將 Person 和 Mobile 類別都繼承 BaseObj 類別,然後直接呼叫 Person 物件的 toString() 即可。

輸出結果:
[email protected][name=OneJar99,favoriteFruit={Apple,Banana,Orange},favoriteOs=[iOS, Android],[email protected][brand=Apple,os=iOS 10,type=iPhone 7,owner=<null>]]

 

ReflectionToStringBuilder 對物件父類別屬性的支援性

物件可能有繼承父類別,ReflectionToStringBuilder 另一個便利的特性,就是在取得物件的宣告成員變數時,支援取到該物件的父類別宣告屬性。

以 EX5 為例,Mobile 類別繼承了 Device 類別,間接繼承 Product 類別,在印出 Mobile 類別時,繼承自 Device 和 Product 類別的屬性也能夠被印出來。

EX5: ReflectionToStringBuilder 對多層繼承的支援

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 );
    }
}

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

輸出結果:

[email protected][brand=Apple,os=iOS 10,type=iPhone 7,owner=<null>,serialNumber=MIT00001,price=30000]

小結

從前述的介紹,可以認識到 ReflectionToStringBuilder 的幾個特性:

1. 在印出自訂類別的物件內容資訊上非常實用。
2. 對取得物件的成員變數時無視作用域,即使是私有變數(private) 也能被取得。
3. 支援取得繼承自父類別的屬性。

前述示範的 ReflectionToStringBuilder 僅是非常基本的用法,事實上 ReflectionToStringBuilder 可以做到更多靈活的效果,例如排除特定屬性、客製化 ToStringStyle 等等。

發表留言