浅析Java中的深克隆和浅克隆

说实话,目前为止还没在项目中遇到过关于Java深克隆和浅克隆的场景。今天手抖戳开了花呗账单,双十二败家的战绩真是惨不忍睹,若能在我的客户端“篡改”下账单金额,那该(简)有(止)多(做)好(梦)啊!于是乎,有了以下的设想。采用工厂模式,根据所传入的帐户名accountName 得到账单bill返回客户端client,代码实现如下:

账单类Bill代码:

/**
 * @Author: mollychin
 * @Date: 2019/1/1 20:39
 */
@Data
public class Bill{
    /**
     * 账单流水号
     */
    private Long id;
    /**
     * 账单总金额
     */
    private BigDecimal totalAmount;

    /**
     * 对应的账户
     */
    private String accountName;
}

账单工厂类 BillFactory代码:

/**
 * @Author: mollychin
 * @Date: 2019/1/1 20:44
 */
public class BillFactory {
    private Bill bill = null;

    public Bill getBill(String name) {
        if (bill == null) {
            synchronized (this) {
                if (bill == null) {
                    bill = new Bill();
                    bill.setAccountName(name);
                    bill.setTotalAmount(BigDecimal.valueOf(5000.0));
                }
            }
        }
        return bill;
    }
}

客户端类 BillClient代码:

/**
 * @Author: mollychin
 * @Date: 2019/1/1 20:51
 */
public class BillClient {
    public static void main(String[] args) {
        BillFactory billFactory = new BillFactory();
        Bill bill = billFactory.getBill("mollychin");
        System.out.println("original bill amount:"+bill.getTotalAmount());
    }
}
// 输出:
original bill amount:5000.0

显而易见,此时我的账单金额是工厂类返回的5000.00,没毛病。但万一遇上吃土少女想动点歪脑筋呢?嘿嘿。请看下面:

试图篡改账单金额的客户端代码:

/**
 * @Author: mollychin
 * @Date: 2019/1/1 20:51
 */
public class BillClient {
    public static void main(String[] args) {
        BillFactory billFactory = new BillFactory();
        Bill bill = billFactory.getBill("mollychin");
        System.out.println("bill before:"+bill.getTotalAmount());
        Bill fakeBill = bill;
        fakeBill.setTotalAmount(BigDecimal.valueOf(2000.00));
        System.out.println("bill after:"+bill.getTotalAmount());
        System.out.println("fakeBill:"+fakeBill.getTotalAmount());

        Bill newMollychin = billFactory.getBill("mollychin");
        System.out.println("get bill again:"+newMollychin.getTotalAmount());
    }
}
// 输出:
bill before:5000.0
bill after:2000.0
fakeBill:2000.0
get bill again:2000.0

bill.setTotalAmount(BigDecimal.valueOf(2000.00)); 代码里自有黄金屋啊,一行代码三千块欸。可花呗账单怎可能被我们如此轻易修改呢?值得思考的是为什么会出现上述情况呢?因为billfakeBill都是对同一对象的引用,任何一方的改动都会影响另一方的变化。那么如何避免这种及其不安全的情况呢?即希望fakeBill是一个新对象,它和bill的初始状态一样,但随后会有互不影响的改动,这时可实现Cloneable并重写clone()方法。

实现了Cloneable接口的账单类CloneableBill代码:

@Data
public class CloneableBill implements Cloneable {

	/**
	 * 账单流水号
	 */
	private Long id;
	/**
	 * 账单总金额
	 */
	private BigDecimal totalAmount;

	/**
	 * 对应的账户
	 */
	private String accountName;

	/**
	 * 重写了父类的克隆方法.
	 * @return
	 */
	@Override
	public CloneableBill clone() {
		CloneableBill cloneableBill = null;
		try {
			cloneableBill = (CloneableBill) super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return cloneableBill;
	}
}
> 工厂类`CloneableBillFactory`的返回也发生了改变:
/**
 * @author: mollychin
 * @date: 2019/1/2
 */
public class CloneableBillFactory {

	private CloneableBill cloneableBill = null;

	public CloneableBill getCloneableBill(String name) {
		if (cloneableBill == null) {
			synchronized (this) {
				if (cloneableBill == null) {
					cloneableBill = new CloneableBill();
					cloneableBill.setTotalAmount(BigDecimal.valueOf(5000.00));
					cloneableBill.setAccountName(name);
				}
			}
		}
		// 新的改动
		return cloneableBill.clone();
	}
}

执行客户端CloneableBillClient代码,调用 clone()方法查看结果,此时的账单金额:

/**
 * @author: mollychin
 * @date: 2019/1/2
 */
public class CloneableBillClient {

	public static void main(String[] args) {
		CloneableBillFactory cloneableBillFactory = new CloneableBillFactory();
		CloneableBill originalBill = cloneableBillFactory.getCloneableBill("mollychin");
		System.out.println("Before clone:" + originalBill.getTotalAmount());
		CloneableBill fakeBill = null;
		try {
			fakeBill = originalBill.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		fakeBill.setTotalAmount(BigDecimal.valueOf(2000.00));
		System.out.println("After clone:" + originalBill.getTotalAmount());
	}
}
输出:
Before clone:5000.0
After clone:5000.0

失不失望!现在已经无法通过客户端随意篡改我们的账单金额了=.= 那么究竟Cloneable接口以及clone()方法对我们的代码做了什么操作呢?戳进源码一探究竟!

浅析Java中的深克隆和浅克隆_第1张图片

你会发现,Cloneable接口里没有一个方法,俗称“标记接口”tagging interface,实现了该接口的类可调用Object类的clone()。若未实现该接口,当调用clone()会抛出CloneNotSupportedException异常。

通过上述操作,妄想篡改账单金额貌似是不可能的了。但一般情况下,账单类里面会含有账单明细类的一个对象,用来描述该账单的明细账单。这样可通过账单对象get到账单明细对象,优化代码如下:

持有账单明细对象的账单类DoubleCloneableBill

/**
 * @description:
 * @author: mollychin
 * @date: 2019/1/2
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DoubleCloneableBill implements Cloneable {

	/**
	 * 账单流水号
	 */
	private Long id;
	/**
	 * 账单总金额
	 */
	private BigDecimal totalAmount;

	/**
	 * 对应的账户
	 */
	private String accountName;

	/**
	 * 账单明细对象
	 */
	private BillDetail billDetail;


	@Override
	public DoubleCloneableBill clone() {
		DoubleCloneableBill doubleCloneableBill = null;
		try {
			doubleCloneableBill = (DoubleCloneableBill) super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return doubleCloneableBill;
	}
}

工厂类DoubleCloneableBillFactory:

/**
 * @description:
 * @author: mollychin
 * @date: 2019/1/2
 */
public class DoubleCloneableBillFactory {

	private DoubleCloneableBill doubleCloneableBill;

	public DoubleCloneableBill getDoubleCloneableBill(String name) {
		if (doubleCloneableBill == null) {
			synchronized (this) {
				if (doubleCloneableBill == null) {
					doubleCloneableBill = new DoubleCloneableBill();
					doubleCloneableBill.setAccountName(name);
					doubleCloneableBill.setTotalAmount(BigDecimal.valueOf(5000.00));
					doubleCloneableBill
						.setBillDetail(new BillDetail("camera", BigDecimal.valueOf(5000.00)));
				}
			}
		}
		return doubleCloneableBill;
	}
}

客户端DoubleCloneableBillClient

/**
 * @description:
 * @author: mollychin
 * @date: 2019/1/2
 */
public class DoubleCloneableBillClient {

	public static void main(String[] args) {
		DoubleCloneableBillFactory doubleCloneableBillFactory = new DoubleCloneableBillFactory();
		DoubleCloneableBill originalBill = doubleCloneableBillFactory
			.getDoubleCloneableBill("mollychin");
		System.out.println("Bill Detail:Before clone:"+originalBill.getBillDetail().getPrice());
		System.out.println("Bill Amount Before clone:"+originalBill.getTotalAmount());
		DoubleCloneableBill clonedBill = originalBill.clone();
		clonedBill.getBillDetail().setPrice(BigDecimal.valueOf(2000.00));
		System.out.println("Bill Detail:After clone:"+originalBill.getBillDetail().getPrice());
		System.out.println("Bill Amount After clone:"+originalBill.getTotalAmount());
	}
}
// 输出:
Bill Detail:Before clone:5000.0
Bill Amount Before clone:5000.0
Bill Detail:After clone:2000.0
Bill Amount After clone:5000.0

由输出可见,虽然我们篡改不了账单金额,但账单明细的金额居然可以轻松被改??之所以会发生这种“怪异”事件,是因为这里采用的是Java中的浅克隆shadowClone,接下来我们尝试下深克隆deepClone可否避免这种情况。

账单DoubleCloneableBill以及账单明细类BillDetail

/**
 * @description:
 * @author: mollychin
 * @date: 2019/1/2
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DoubleCloneableBill implements Cloneable {

	/**
	 * 账单流水号
	 */
	private Long id;
	/**
	 * 账单总金额
	 */
	private BigDecimal totalAmount;

	/**
	 * 对应的账户
	 */
	private String accountName;

	/**
	 * 账单明细对象
	 */
	private BillDetail billDetail;


	@Override
	public DoubleCloneableBill clone() {
		DoubleCloneableBill doubleCloneableBill = null;
		try {
			doubleCloneableBill = (DoubleCloneableBill) super.clone();
			BillDetail clonedBillDetail = doubleCloneableBill.getBillDetail().clone();
			doubleCloneableBill.setBillDetail(clonedBillDetail);
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return doubleCloneableBill;
	}
}

/**
 * @description:
 * @author: liuyiMao
 * @date: 2019/1/2
 */
@Data
@AllArgsConstructor
public class BillDetail implements Cloneable {

	/**
	 * 商品名称
	 */
	private String goodName;
	/**
	 * 商品价格
	 */
	private BigDecimal price;

	@Override
	public BillDetail clone() {
		BillDetail billDetail = null;
		try {
			billDetail = (BillDetail) super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return billDetail;
	}
}

工厂类DoubleCloneableBillFactory:

/**
 * @description:
 * @author: mollychin
 * @date: 2019/1/2
 */
public class DoubleCloneableBillFactory {

	private DoubleCloneableBill doubleCloneableBill;

	public DoubleCloneableBill getDoubleCloneableBill(String name) {
		if (doubleCloneableBill == null) {
			synchronized (this) {
				if (doubleCloneableBill == null) {
					doubleCloneableBill = new DoubleCloneableBill();
					doubleCloneableBill.setAccountName(name);
					doubleCloneableBill.setTotalAmount(BigDecimal.valueOf(5000.00));
					doubleCloneableBill
						.setBillDetail(new BillDetail("camera", BigDecimal.valueOf(5000.00)));
				}
			}
		}
		return doubleCloneableBill;
	}
}

客户端(妄图篡改金额)DoubleCloneableBillClient

/**
 * @description:
 * @author: mollychin
 * @date: 2019/1/2
 */
public class DoubleCloneableBillClient {

	public static void main(String[] args) {
		DoubleCloneableBillFactory doubleCloneableBillFactory = new DoubleCloneableBillFactory();
		DoubleCloneableBill originalBill = doubleCloneableBillFactory
			.getDoubleCloneableBill("mollychin");
		System.out.println("Bill Detail:Before clone:"+originalBill.getBillDetail().getPrice());
		System.out.println("Bill Amount Before clone:"+originalBill.getTotalAmount());
		DoubleCloneableBill clonedBill = originalBill.clone();
		clonedBill.getBillDetail().setPrice(BigDecimal.valueOf(2000.00));
		System.out.println("Bill Detail:After clone:"+originalBill.getBillDetail().getPrice());
		System.out.println("Bill Amount After clone:"+originalBill.getTotalAmount());
	}
}
// 输出:
Bill Detail:Before clone:5000.0
Bill Amount Before clone:5000.0
Bill Detail:After clone:5000.0
Bill Amount After clone:5000.0

由此可见,采用了深克隆之后,不管是值类型还是引用类型,都无法在客户端被随意篡改,是不是相对来说健壮了不少呢?

  • 最后一点叨叨:
    借着花呗账单的案例,逐渐了解了Java中深克隆和浅克隆,并感受到它们在系统健壮性方面发挥的巨大作用。使用深克隆可以解决引用对象克隆的问题,但假若类之间嵌套的层次很多,其复杂程度是显而易见的。譬入一个保险系统,类的嵌套可能是这样的:账单类->账单明细类->条款类->条款费用类。前者都持有后者的一个或者多个引用对象,这时该如何实现深克隆所达到的效果呢?此时可以使用org.apache.commons.lang3.SerializationUtils.clone()方法使用Java序列化来简易地达到同样的效果。这将会在后面的博文中继续介绍。

你可能感兴趣的:(Java,学习笔记,优质代码风格)