说实话,目前为止还没在项目中遇到过关于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));
代码里自有黄金屋啊,一行代码三千块欸。可花呗账单怎可能被我们如此轻易修改呢?值得思考的是为什么会出现上述情况呢?因为bill
和fakeBill
都是对同一对象的引用,任何一方的改动都会影响另一方的变化。那么如何避免这种及其不安全的情况呢?即希望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()
方法对我们的代码做了什么操作呢?戳进源码一探究竟!
你会发现,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
由此可见,采用了深克隆之后,不管是值类型还是引用类型,都无法在客户端被随意篡改,是不是相对来说健壮了不少呢?
org.apache.commons.lang3.SerializationUtils.clone()
方法使用Java序列化来简易地达到同样的效果。这将会在后面的博文中继续介绍。