Public Data Assigned to Private Array

弱點掃描遇到這個問題
因此來研究一下
話不多說先看程式碼

範例

import java.util.Arrays;

public class Demo {

    private int[] data;

    public int[] getData() {
        return data;
    }

    public void setData(int[] data) {
        this.data = data;
    }

    public void doubleData() {
        for (int i = 0; i < data.length; i++) {
            data[i] *= 2;
        }
    }

    public void printData() {
        System.out.println(Arrays.toString(this.data));
    }

    public static void main(String[] args) {
        int[] publicData = {1, 2, 3};

        Demo demo = new Demo();
        demo.setData(publicData);
        demo.printData(); // [1, 2, 3]
        demo.doubleData();
        demo.printData(); // [2, 4, 6]

        publicData[0] = 100;
        demo.printData(); // [100, 4, 6]
    }
}

問題描述

這個範例是類別中有一個整數陣列
可以看到修改外部陣列會影響類別內部的陣列
原因是它們指向同樣的記憶體地址
換言之它們是同一份資料

這違反了物件導向的「封裝」原則
因為 private 屬性已經對外暴露出來
程式可以繞過 Demo 物件直接存取資料

而且這帶來了不可預測性
例如這個範例是讓數值變為兩倍
但後續結果卻不同
因為程式邏輯被破壞了

如果專案中許多地方都是這種寫法
可能一份資料同時被多個地方修改
造成問題難以追蹤

修改方式

既然問題的核心在於內外共用一份資料
解決方式就是複製一份
讓物件擁有屬於自己的資料,就不會交互干擾了
同樣的邏輯適用於 Array、List、Set、Map…

public void setArr(int[] arr) {
    if (arr == null) {
        this.arr = null;
    } else {
        this.arr = new int[arr.length];
        System.arraycopy(arr, 0, this.arr, 0, arr.length);
    }
}

public void setList(List<Integer> list) {
    if (list == null)
        this.list = null;
    else
        this.list = new ArrayList<>(list);
}

public void setSet(Set<Integer> set) {
    if (set == null)
        this.set = null;
    else
        this.set = new HashSet<>(set);
}

public void setMap(Map<String, Integer> map) {
    if (map == null)
        this.map = null;
    else
        this.map = new HashMap<>(map);
}

淺複製與深複製

在 Java 基礎觀念中
複製物件參考本質算不上複製
因為記憶體位置是相同的

Person p1 = new Person("John");
Person p2 = p1;
System.out.println(p1 == p2); // true

要複製的話應當使用 Object.clone()

import java.util.*;

public class Data implements Cloneable {

    private int[] arr;

    public int[] getArr() {
        return this.arr;
    }

    public void setArr(int[] arr) {
        this.arr = arr;
    }

    public static void main(String[] args) throws Exception {
        Data d1 = new Data();
        d1.setArr(new int[] {1, 2, 3});
        Data d2 = (Data) d1.clone();
        System.out.println(d1 == d2); // false

        int[] arr = d1.getArr();
        arr[0] = 100;

        System.out.println(Arrays.toString(d2.getArr())); // [100, 2, 3]
        System.out.println(d1.getArr() == d2.getArr()); // true
    }
}

但是這個例子顯示明明改的是 Data 1 的屬性
Data 2 的屬性也受牽連
這就叫做「淺複製」Shallow Copy
只有軀殼不同,靈魂卻相同
沒有把內部屬性一一複製出來

因此真的要複製就要「深複製」Deep Copy
這只能重寫 clone 方法
而且若物件有多層嵌套就很費工
例如 Map<String, Integer[]>

if (map == null) {
    this.map = null;
    reutrn;
}

this.map = new HashMap<>();
for (Map.Entry<String, Integer[]> entry : map.entrySet()) {
    Integer[] original = entry.getValue();
    Integer[] copy = original == null ? null : Arrays.copyOf(original, original.length);
    this.map.put(entry.getKey(), copy);
}

影響

這在軟體工程中有術語叫「防禦性編程」
也就是預先防範可能的錯誤
就算發生了,程式也不受影響

但凡事都有代價
首先是記憶體開銷
再來是會影響效能

因此到底要做到什麼地步?
零信任嗎?那每個地方都得這麼做
增加許多開發成本
甚至讓 Lombok 的功能大打折扣
安全性與便利性不可兼得

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *