iT邦幫忙

2024 iThome 鐵人賽

DAY 5
0
Software Development

深入淺出Java 30天系列 第 5

Day 5: 使用private constructor或enum type實作singleton

  • 分享至 

  • xImage
  •  

singleton是一個保證一個類別只會產生一個物件的設計模式,可應用在file system等系統。使用singleton有一個比較明顯的缺點,因為需要透過static method取得物件,所以無法透過junit或Mockito去mock物件,會比較難寫單元測試(不過在Mockito 3.4.0之後,就可以mock static method,也許可以解決這個問題)。

在Java 1.5之前,有兩種方式可以實作singleton,這兩種方式都是先把constructor設為private,Java 1.5之後,可以用enum實作singleton,下面會一一介紹每一種方法。

方法一:宣告public static final變數,讓使用者可以直接透過變數取得instance。

public class FileUploader {
    public static final FileUploader INSTANCE = new FileUploader();
    private FileUploader() {}

    public void uploadFile(String filePath) {
        // Upload file to server
        System.out.println("File uploaded: " + filePath);
    }
}

class main {
    public static void main(String[] args) {
       FileUploader uploader = FileUploader.INSTANCE;
       uploader.uploadFile("myfile.txt");
    }
}

方法二:撰寫public static final method,透過method取得instance。

這個方法會比方法一更彈性一點,如果這個class之後有其他new instance的需求,可以透過一些參數判斷,更彈性的讓使用者決定是要拿原本的instance或者new instance。

public class FileUploader {
    private static final FileUploader INSTANCE = new FileUploader();
    private FileUploader() {}
    public static FileUploader getInstance() { return INSTANCE; }

    public void uploadFile(String filePath) {
        // Upload file to server
        System.out.println("File uploaded: " + filePath);
    }
}

class main {
    public static void main(String[] args) {
       FileUploader uploader = FileUploader.getInstance();
       uploader.uploadFile("myfile.txt");
    }
}

上述的兩種方法都有漏洞,可以用AccessibleObject.setAccessible的方式繞過去,產生第二個新的instance。以FileUploader 為例,只要把Constructor的Accessible設為true,在拿Constructor去呼叫newInstance ,就可以取得一個全新的instance,用== 檢查即可確認第二個instance是不是新的。

import java.lang.reflect.Constructor;

class Main {
    public static void main(String[] args) {
      FileUploader uploader = FileUploader.getInstance();
      try {
        Class<? extends FileUploader> uploaderClass = uploader.getClass();
        Constructor<? extends FileUploader> fileUploaderConstructor = uploaderClass.getDeclaredConstructor();
        fileUploaderConstructor.setAccessible(true);
        FileUploader newUploader = fileUploaderConstructor.newInstance();
        boolean isSame = (uploader == newUploader);
        System.out.println("uploader and newUploader is same? " + isSame);
      } catch (Exception e) {
        e.printStackTrace();
      }
      uploader.uploadFile("myfile.txt");
    }
}

compile完和執行之後,發現uploader和newUploader是兩個不同的instance,就可以確認AccessibleObject.setAccessible可以利用singleton的漏洞,破解了singleton保證一個類別只會產生一個物件的原則。

那麼,要如何避免上述兩個singleton的寫法,被破壞產生第二個instance呢?可以在FileUploader的constructor檢查是不是已經有instance,如果有就拋出exception,不讓使用者透過constructor去new instance。

public class FileUploader {
    private static final FileUploader INSTANCE = new FileUploader();
    private FileUploader() {
        if (INSTANCE != null) {
            throw new IllegalStateException("Instance already exists");
        }
    }
    public static FileUploader getInstance() { return INSTANCE; }

    public void uploadFile(String filePath) {
        // Upload file to server
        System.out.println("File uploaded: " + filePath);
    }
}

方法三:使用Enum實作singleton(Java 1.5之後支援)

在java 1.5版之後,有一個簡單的方法可以實作singleton,就是使用Enum並定義type,即可完成實作singleton。這個方法比較簡便也比較沒有上面兩個方法的問題。

public enum FileUploader {
    INSTANCE;

    public void uploadFile(String filePath) {
        // Upload file to server
        System.out.println("File uploaded: " + filePath);
    }
}

class Main {
    public static void main(String[] args) {
      FileUploader uploader = FileUploader.INSTANCE;
      uploader.uploadFile("myfile.txt");
    }
}

上一篇
Day 4: 當constructor需要很多參數時,考慮使用builder(下)
下一篇
Day 6: 避免生成不必要的物件
系列文
深入淺出Java 30天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言