在写Lab4的过程中,被map的深拷贝和浅拷贝困扰了一天,感觉被java的内存分配机制狠狠坑了一把。下面分享一下我的心酸心路历程。
首先看看代码,当时的想法很简单,用另一个map来记录未分配资源前的计划项集合,如果分配资源后存在资源冲突,则将存储改变前的计划项集合赋值给flightentries:
(在这段代码执行之前是判断待分配计划项存在与否、是否已分派资源以及欲分配的飞机存在与否,本文主要针对后半部分进行修改,下文就不再赘述)
但是执行结果并不如我所愿:
虽然程序执行提示资源冲突,操作失败,但选择15查看所有计划项后发现计划项的状态是Allocated,选择11,检测结果是存在资源冲突,程序显然执行了把clone赋值给flightentires的语句,但flightentries却没有回到原来的模样,这是为什么呢?
显然,clone被改了,不太相信的我又在程序执行中加入了print语句:
运行程序,该行语句的执行结果果然打印出了Allocated:
显然,虽然没有对clone中的元素进行直接操作,但计划项的状态已经被改了。
这里是因为map发生了浅拷贝,clone只是复制了flightentries的引用,和flightentries仍使用同一个内存区域,所以,在修改flightentries的时候,clone的值同样会发生变化。
深拷贝与浅拷贝概念
浅拷贝:只拷贝对象的引用,两个引用仍然指向同一个对象,在内存中占用同一块内存。被拷贝对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅拷贝所考虑的对象,而不复制它所引用的对象。
深拷贝:被拷贝对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被拷贝过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要拷贝的对象所引用的对象都复制了一遍。
深拷贝尝试
既然知道了是深拷贝和浅拷贝引发的差异,接下来就想办法实现深拷贝,艰辛的探索之路开始了。
1.运用flightentries.clone()为clone赋值。
首先想到的是map中的clone方法,它是不是深拷贝呢?赋值,运行,呈现出的是与第一次运行完全相同结果。
2.网上看到最多的说法是:map不能实现深拷贝,但Hashmap可以。
于是,我把域中的flightentries和clone都设置成Hashmap,再运用flightentries.clone()为clone赋值。但是,运行结果依旧是错误的,显然并没有实现深拷贝。
3.据说HashMap的putAll()方法可以实现深拷贝
于是编写如下代码:
HashMap<String, HashMap<LocalDateTime, FlightEntry>> clone=new HashMap<String, HashMap<LocalDateTime, FlightEntry>>();
clone.putAll(flightentries);
结果依旧未改变。
4.使用List暂存flightentries中的所有计划项
新建list,遍历整个flightentries,将所有计划项暂存到列表中。如果存在资源冲突,清空flihghtentries,再遍历list,将所有计划项放入flightentries。
原以为这是必杀技,经过这么繁琐的操作,计划项的值一定不会被改变。然而,结果依旧失败了。
事实上,遍历中将计划项存入列表,存的也是计划项的引用,依旧是同一对象,同一内存地址。这一结果通过分配资源后打印list立即得到验证。(不禁感慨:java的引用也太强大了)
最后的最后
最后的最后,考虑到在经过对计划项的一系列检查后,此时能进行分配资源的计划项必然是一个全新的计划项,于是在遍历计划项暂存到list中时,对待分配资源的计划项利用其observer作为参数new一个新的计划项存入list,经过这一繁琐的操作以后,终于如我所愿地运行了。(更新:使用replace方法更方便快捷)
繁琐但易懂的代码如下:
List<FlightEntry> entries = new ArrayList<FlightEntry>();
for (Map.Entry<String, Map<LocalDateTime, FlightEntry>> e : flightentries.entrySet())
for (Map.Entry<LocalDateTime, FlightEntry> f : e.getValue().entrySet()) {
FlightEntry t = f.getValue();
if (e.getKey().equals(s) && f.getValue().gettime().get(0).getbegin()
.equals(LocalDateTime.of(begin, begin2))){//判断是否是待分配资源的计划项
t = FlightEntryfactory.create(f.getValue().getname(),
f.getValue().getlocation(), f.getValue().gettime());//新建计划项t
}
entries.add(t);
}
List<Airplane> airplane = new ArrayList<Airplane>();
airplane.add(sources.get(number));
flightentries.get(s).get(LocalDateTime.of(begin, begin2)).allocate(airplane);
if (this.sourceconflict()) {
log.info("分配资源:为航班" + s + "分配资源失败");
System.out.println("存在资源冲突,操作失败!");
flightentries.clear();//清空计划项
for (FlightEntry e : entries) {//从entries读入未改变前的所有计划项
if (flightentries.containsKey(e.getname()))
flightentries.get(e.getname()).put(e.gettime().get(0).getbegin(), e);
else {
Map<LocalDateTime, FlightEntry> temp = new HashMap<>();
temp.put(e.gettime().get(0).getbegin(), e);
flightentries.put(e.getname(), temp);
}
}
throw new SourceConflictException("存在资源冲突");
}
System.out.println("已分配!");
log.info("分配资源:为航班" + s + "分配资源成功");
运行结果:
最后的最后的最后
显然,还是没找出深拷贝的方法,只留下了一天中踩的所有坑(还试过一个什么流水线方法,但抛出了异常,原理也没看懂,这里就不记录了)。据说为需要深拷贝的类实现Cloneable接口可以实现深拷贝,同时,对对象其中的所有可变对象也要实现Cloneable接口,因为这个实验用到的深拷贝不多,类与类中成员变量各自都比较复杂,就暂时不做尝试了。
如果有尝试,再来补充。
后来发现Map中有一个很好用的方法:replace。当分配资源冲突时,将更改过的计划项运用replace方法替换为一个新的、未分配资源的计划项,可谓方便快捷、简单易懂。