在写Lab4的过程中,被map的深拷贝和浅拷贝困扰了一天,感觉被java的内存分配机制狠狠坑了一把。下面分享一下我的心酸心路历程。

首先看看代码,当时的想法很简单,用另一个map来记录未分配资源前的计划项集合,如果分配资源后存在资源冲突,则将存储改变前的计划项集合赋值给flightentries:

map 深度学习是怎么弄得 map深度复制_map 深度学习是怎么弄得


(在这段代码执行之前是判断待分配计划项存在与否、是否已分派资源以及欲分配的飞机存在与否,本文主要针对后半部分进行修改,下文就不再赘述)

但是执行结果并不如我所愿:

map 深度学习是怎么弄得 map深度复制_浅拷贝_02


虽然程序执行提示资源冲突,操作失败,但选择15查看所有计划项后发现计划项的状态是Allocated,选择11,检测结果是存在资源冲突,程序显然执行了把clone赋值给flightentires的语句,但flightentries却没有回到原来的模样,这是为什么呢?

显然,clone被改了,不太相信的我又在程序执行中加入了print语句:

map 深度学习是怎么弄得 map深度复制_map 深度学习是怎么弄得_03


运行程序,该行语句的执行结果果然打印出了Allocated:

map 深度学习是怎么弄得 map深度复制_map 深度学习是怎么弄得_04


显然,虽然没有对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 + "分配资源成功");

运行结果:

map 深度学习是怎么弄得 map深度复制_java_05

最后的最后的最后

显然,还是没找出深拷贝的方法,只留下了一天中踩的所有坑(还试过一个什么流水线方法,但抛出了异常,原理也没看懂,这里就不记录了)。据说为需要深拷贝的类实现Cloneable接口可以实现深拷贝,同时,对对象其中的所有可变对象也要实现Cloneable接口,因为这个实验用到的深拷贝不多,类与类中成员变量各自都比较复杂,就暂时不做尝试了。
如果有尝试,再来补充。
后来发现Map中有一个很好用的方法:replace。当分配资源冲突时,将更改过的计划项运用replace方法替换为一个新的、未分配资源的计划项,可谓方便快捷、简单易懂。