網頁

2020/6/17

Effective Java 3e - Item 50: Make defensive copies when needed 筆記

Effective Java 3e - Item 50: Make defensive copies when needed 防禦性複製。

若傳入方法或建構式的參數,傳出的成員為可變的(mutable),應先進行做防禦複製。

重點:

  • 假設外部會破壞類別的不變性。
  • 對傳入方法或建構式的可變參數做防禦複製。
  • 若類別成員為可變的,傳出時做防禦複製。
  • 先做防禦複製,再檢驗參數的有效性。
  • 避免使用clone()做防禦性複製。

例如下面的Course類別以兩個Date參數傳入建構式後直接設為成員變數的值。

Course

package com.abc.demo.dto;

import java.util.Date;

public class Course {

    private final Date startDate;
    private final Date endDate;

    public Course(Date startDate, Date endDate) {
        this.startDate = startDate; // 值接設為成員變數的值。
        this.endDate = endDate;
    }

    // getters and setters
}

由於Date是mutable(可變的),所以即使加上final仍可以從外部修改,破壞了Course的不可變性,可能造成系統的漏洞及除錯時的麻煩。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date startDate = sdf.parse("2020-06-17");
Date endDate = sdf.parse("2020-06-20");

Course course = new Course(startDate, endDate);

startDate.setTime(1594915200000L); // 默默修改了Course.startDate
System.out.println(sdf.format(course.getStartDate())); // 2020-07-17

而defensive copy可避免以上狀況,例如把Course的建構式修改如下。

Course

package com.abc.demo.dto;

import java.util.Date;

public class Course {

    private final Date startDate;
    private final Date endDate;

    public Course(Date startDate, Date endDate) {
        this.startDate = new Date(startDate.getTime()); // defensive copy
        this.endDate = new Date(endDate.getTime());
    }

    // getters and setters
}

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date startDate = sdf.parse("2020-06-17");
Date endDate = sdf.parse("2020-06-20");

Course course = new Course(startDate, endDate);

startDate.setTime(1594915200000L); // Course.startDate 是另一個新的Date物件,非外部傳入的startDate了
System.out.println(sdf.format(course.getStartDate())); // 2020-06-17 // Course.startDate仍為原本建構時的狀態。

除了傳入參數,用getter取得的內部成員若為mutable也可能被外部修改。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date startDate = sdf.parse("2020-06-17");
Date endDate = sdf.parse("2020-06-20");

Course course = new Course(startDate, endDate);
Date date = course.getStartDate();
date.setTime(1594915200000L); // Course.startDate被默默修改了

System.out.println(sdf.format(course.getStartDate())); // 2020-07-17

因此傳出去的成員若為mutable,也應該用defensive copy複製後再傳出。

Course

package com.abc.demo.dto;

import java.util.Date;

public class Course {

    private final Date startDate;
    private final Date endDate;

    public Course(Date startDate, Date endDate) {
        this.startDate = new Date(startDate.getTime()); // defensive copy
        this.endDate = new Date(endDate.getTime());
        
    }

    public Date getStartDate() {
        return new Date(startDate.getTime()); // defesive copy
    }

    public Date getEndDate() {
        return new Date(endDate.getTime()); // defesive copy
    }
}

先進行防禦複製後再檢查傳入參數的有效性(validity)

Course

package com.abc.demo.dto;

import java.util.Date;

public class Course {

    private final Date startDate;
    private final Date endDate;

    public Course(Date startDate, Date endDate) {
        this.startDate = new Date(startDate.getTime());
        this.endDate = new Date(endDate.getTime());
        if(this.startDate.compareTo(this.endDate) > 0) { // 先做defensive copy,然後才檢查有效性
            throw new IllegalArgumentException(this.startDate + " after " + this.endDate);
        }
            
    }
    // getters and setters
}

不要使用clone()複製未知第三方繼承的類別參數。因為clone()的可能產生非預期的結果。


不過如果每個方法都要做防禦複製會影響校能(當然還有開發速度),因此若客戶端可信任且不隨意修改傳入參數或傳出成員的情況下可以不做防禦性複製。


沒有留言:

張貼留言