黑马程序员技术交流社区

标题: 【上海校区】 Object克隆方法解析 [打印本页]

作者: 小影姐姐    时间: 2018-5-11 11:57
标题: 【上海校区】 Object克隆方法解析
学习目标
1. 能够理解clone方法的由来
2. 能够使用clone方法创建对象
3. 能够理解克隆对象和原对象的关系
4. 能够理解clone方法创建对象与反射和new关键字创建对象的不同
5. 能够理解浅表复制和深层复制的含义
6. 能够探寻对象的复制必须实现Cloneable接口的底层源码

1. 克隆方法的由来
问题一:什么是克隆(clone)方法        
答: 创建并返回此对象的一个副本——按照原对象,创建一个新的对象(复制原对象的内容)。

问题二:已经存在new关键字和反射技术都可以创建对象,为什么还需要一个Object的clone方法呢?
答:必然是new关键字和反射技术,存在一些弊端。

1.1 new关键字和反射创建对象的弊端
        我们来看一个需求:使用new关键字和反射创建内容一模一样的对象,并且打印他们哈希值。
演示素材——Person对象:
    package cn.itcast.domain;
   
    public class Person {
        private String name;
        private int age;
   
        public String getName() {
            return name;
        }
   
        public void setName(String name) {
            this.name = name;
        }
   
        public int getAge() {
            return age;
        }
   
        public void setAge(int age) {
            this.age = age;
        }
   
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
   
演示new关键字和反射创建对象:
     @Test
        public void test1() throws Exception {
            Person p1 = new Person();
            p1.setName("张三");
            p1.setAge(18);
   
            Person p2 = new Person();
            p2.setName("张三");
            p2.setAge(18);
   
            System.out.println(p1+":"+p1.hashCode());
            System.out.println(p2+":"+p2.hashCode());
   
            Class clazz = Class.forName("cn.itcast.domain.Person");
            Person p3 = (Person) clazz.getConstructor().newInstance();
            p3.setName("李四");
            p3.setAge(28);
   
            Person p4 = (Person) clazz.getConstructor().newInstance();
            p4.setName("李四");
            p4.setAge(28);
   
            System.out.println(p3+":"+p3.hashCode());
            System.out.println(p4+":"+p4.hashCode());
        }

效果:
    Person{name='张三', age=18}:103536485
    Person{name='张三', age=18}:1279309678
    Person{name='李四', age=17}:48914743
    Person{name='李四', age=17}:1106131243

总结:通过new和反射可以创建内容一模一样的对象。但是,创建对象之后,通过setter方法,完成设置一不一样的内容,如果需要创建更多的内容一致的对象,那么setter方法调用就不断在重复。

接下来,使用Object的clone方法演示,更加简便方式,复制对象的操作!

1.2  使用clone方法创建对象

1.2.1 使用步骤

1. 在需要调用clone方法的对象上添加实现Cloneable接口
2. 复写clone方法,在自己的clone方法中调用父类的clone方法,将返回值类型强转成本类类型,将当前clone方法修饰符改成public
3. 在测试中调用对象的clone方法

1.2.2 代码演示

1. 在需要调用clone方法的对象上添加实现Cloneable接口
2. 复写clone方法,在自己的clone方法中调用父类的clone方法,将返回值类型强转成本类类型
       package cn.itcast.domain;
      
       public class Person implements Cloneable{
           private String name;
           private int age;
      
           public String getName() {
               return name;
           }
      
           public void setName(String name) {
               this.name = name;
           }
      
           public int getAge() {
               return age;
           }
      
           public void setAge(int age) {
               this.age = age;
           }
      
           @Override
           public String toString() {
               return "Person{" +
                       "name='" + name + '\'' +
                       ", age=" + age +
                       '}';
           }
      
           @Override
           public Person clone() throws CloneNotSupportedException {
               return (Person) super.clone();
           }
       }
      
3. 在测试中调用对象的clone方法
        @Test
           public void test2() throws Exception {
      
               Person p1 = new Person();
               p1.setName("张三");
               p1.setAge(18);
      
               Person p2 = p1.clone();
      
               System.out.println(p1+":"+p1.hashCode());
               System.out.println(p2+":"+p2.hashCode());
      
           }
   效果:
       Person{name='张三', age=18}:2104028992
       Person{name='张三', age=18}:1790421142
   通过使用clone方法,我们发现大大的减少了创建重复对象代码。这也就是clone方法存在的意义。
   

2. 克隆出来的对象和原来的对象有什么关系
        通过上面的测试,我们已经知道了,克隆出来的对象内容一致,但是对象哈希值不同,所以是不同对象。
        那么两个对象的内容之间有什么关联呢——两个对象的内容是彼此独立,还是,两个对象底层使用的同一个内容呢?
素材(新Person):
    package cn.itcast.domain;
    public class Person implements Cloneable{
        private String name;
        private Integer age;
        private Children child;
   
        public String getName() {
            return name;
        }
   
        public void setName(String name) {
            this.name = name;
        }
   
        public Integer getAge() {
            return age;
        }
   
        public void setAge(Integer age) {
            this.age = age;
        }
   
        public Children getChild() {
            return child;
        }
   
        public void setChild(Children child) {
            this.child = child;
        }
   
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", child=" + child +
                    '}';
        }
   
        @Override
        public Person clone() throws CloneNotSupportedException {
            return (Person) super.clone();
        }
    }

素材(Person内部的Children):
    package cn.itcast.domain;
    public class Children {
        private String name;
        private Integer age;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Integer getAge() {
            return age;
        }
        public void setAge(Integer age) {
            this.age = age;
        }
        @Override
        public String toString() {
            return "Children{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

测试代码:
    @Test
        public void test3() throws Exception {
            Person p1 = new Person();
            p1.setName("张三");
            p1.setAge(28);
            Children children1 = new Children();
            children1.setName("张伟");
            children1.setAge(5);
            p1.setChild(children1);
   
            Person p2 = p1.clone();
   
            System.out.println(p1+":对象的哈希值:"+p1.hashCode()+":child成员变量的哈希值:"+p1.getChild().hashCode());
            System.out.println(p2+":对象的哈希值:"+p2.hashCode()+":child成员变量的哈希值:"+p2.getChild().hashCode());
   
        }

效果:
    Person{name='张三', age=28, child=Children{name='张伟', age=5}}:对象的哈希值:1527430292:child成员变量的哈希值:1975546571
    Person{name='张三', age=28, child=Children{name='张伟', age=5}}:对象的哈希值:1978869058:child成员变量的哈希值:1975546571

结论:通过测试发现克隆出来的对象虽然不一致,但是底层的成员变量的哈希值是一致的。
这种复制我们称之为:浅表复制。

浅表复制的内存结构:



3.能不能让克隆对象其中成员变量也变成新的对象

3.1 浅表复制的弊端

        由于浅表复制导致克隆的对象中成员变量的底层哈希值一致,如果我们操作其中一个对象的成员变量内容,就会导致,所有的克隆对象的成员内容发送改变。

测试代码:

     /**
         * 需求:测试浅表复制的弊端
         * */
        @Test
        public void test4() throws Exception {
   
            Person p1 = new Person();
            p1.setName("张三");
            p1.setAge(28);
   
            Children children1 = new Children();
            children1.setName("张伟");
            children1.setAge(5);
            p1.setChild(children1);
   
            Person p2 = p1.clone();
   
            System.out.println(p1.getChild());
            System.out.println(p2.getChild());
   
            children1.setName("张三丰");
            System.out.println(p1.getChild());
            System.out.println(p2.getChild());
   
            Children children2 = p2.getChild();
            children2.setName("张无忌");
            System.out.println(p1.getChild());
            System.out.println(p2.getChild());
   
   
        }

效果:

    Children{name='张伟', age=5}
    Children{name='张伟', age=5}
    Children{name='张三丰', age=5}
    Children{name='张三丰', age=5}
    Children{name='张无忌', age=5}
    Children{name='张无忌', age=5}

结论:clone方法默认的复制操作是浅表复制,浅表复制存在弊端——仅仅创建新的对象,对象的成员内容底层哈希值是一致的,因此,不管是原对象还是克隆对象,只有其中一个修改了成员的数据,就会影响所有的原对象和克隆对象。



要解决浅表复制的问题:进行深层的复制。

3.2 深层复制

目的:不仅在执行克隆的时候,克隆对象是一个新对象,而且,克隆对象中的成员变量,也要求是一个新的对象。

3.2.1 开发步骤

1. 修改children类实现Cloneable接口
2. 修改children类重写clone方法
3. 修改Person类重写clone方法,在clone方法中调用children的clone方法

3.2.2 代码实现

1. 修改child类实现Cloneable接口
2. 修改child类重写clone方法
       package cn.itcast.domain;
      
       public class Children implements Cloneable {
           private String name;
           private Integer age;
      
           public String getName() {
               return name;
           }
      
           public void setName(String name) {
               this.name = name;
           }
      
           public Integer getAge() {
               return age;
           }
      
           public void setAge(Integer age) {
               this.age = age;
           }
      
           @Override
           public String toString() {
               return "Children{" +
                       "name='" + name + '\'' +
                       ", age=" + age +
                       '}';
           }
      
           @Override
           public Children clone() throws CloneNotSupportedException {
               return (Children) super.clone();
           }
       }
3. 修改Person类重写clone方法,在clone方法中调用child的clone方法
       package cn.itcast.domain;
      
       public class Person implements Cloneable{
           private String name;
           private Integer age;
           private Children child;
      
           public String getName() {
               return name;
           }
      
           public void setName(String name) {
               this.name = name;
           }
      
           public Integer getAge() {
               return age;
           }
      
           public void setAge(Integer age) {
               this.age = age;
           }
      
           public Children getChild() {
               return child;
           }
      
           public void setChild(Children child) {
               this.child = child;
           }
      
           @Override
           public String toString() {
               return "Person{" +
                       "name='" + name + '\'' +
                       ", age=" + age +
                       ", child=" + child +
                       '}';
           }
      
           @Override
           public Person clone() throws CloneNotSupportedException {
               Person clone = (Person) super.clone();
               clone.setChild(child.clone());
               return clone;
           }
       }
4. 测试代码
        /**
            * 需求:测试深层复制
            * */
           @Test
           public void test5() throws Exception {
      
               Person p1 = new Person();
               p1.setName("张三");
               p1.setAge(28);
      
               Children children1 = new Children();
               children1.setName("张伟");
               children1.setAge(5);
               p1.setChild(children1);
      
               Person p2 = p1.clone();
      
               System.out.println(p1.getChild());
               System.out.println(p2.getChild());
      
               children1.setName("张三丰");
               System.out.println(p1.getChild());
               System.out.println(p2.getChild());
      
               Children children2 = p2.getChild();
               children2.setName("张无忌");
               System.out.println(p1.getChild());
               System.out.println(p2.getChild());
      
               System.out.println(p1.getChild().hashCode());
               System.out.println(p2.getChild().hashCode());
           }
   
5. 效果:
       Children{name='张伟', age=5}
       Children{name='张伟', age=5}
       Children{name='张三丰', age=5}
       Children{name='张伟', age=5}
       Children{name='张三丰', age=5}
       Children{name='张无忌', age=5}
       1131040331
       254749889

深层复制内存结构:


4. 成员变量不实现clone接口的情况下进行深度复制

4.1 使用clone接口实现深层复制的弊端

        以上的方法虽然完成了深度复制,但是修改类中成员变量对应的源码,如果成员变量特别多,那么就需要修改多个类的源码。

例如一下代码,我们就需要修改两个成员变量对应类的源码(Children,Grandson):

    package cn.itcast.domain;
   
    public class Person implements Cloneable{
        private String name;
        private Integer age;
        private Children child;
        private Grandson gdson;
   
        public String getName() {
            return name;
        }
   
        public void setName(String name) {
            this.name = name;
        }
   
        public Integer getAge() {
            return age;
        }
   
        public void setAge(Integer age) {
            this.age = age;
        }
   
        public Children getChild() {
            return child;
        }
   
        public void setChild(Children child) {
            this.child = child;
        }
   
        public Grandson getGdson() {
            return gdson;
        }
   
        public void setGdson(Grandson gdson) {
            this.gdson = gdson;
        }
   
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", child=" + child +
                    ", gdson=" + gdson +
                    '}';
        }
   
        @Override
        public Person clone() throws CloneNotSupportedException {
            return (Person) super.clone();
        }
    }

素材:Grandson类

    package cn.itcast.domain;
   
    public class Grandson {
   
        private String name;
        private Integer age;
   
        public String getName() {
            return name;
        }
   
        public void setName(String name) {
            this.name = name;
        }
   
        public Integer getAge() {
            return age;
        }
   
        public void setAge(Integer age) {
            this.age = age;
        }
   
        @Override
        public String toString() {
            return "Grandson{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

结论:使用克隆接口完成深度复制的弊端:

    1 重复实现cloneable接口
    2 重复实现clone方法
    3 重复改写Person类的clone方法

可以使用IO流的方式进行复制操作(深层复制),可以解决重复修改源代码的问题。

4.2 使用IO进行克隆复制(深层复制)
4.2.1 使用IO复制相关的API介绍

1、ByteArrayOutputStream


构造方法:

2、ByteArrayInputStream



构造方法:



3、ObjectOutputStream

构造方法:

将对象写入流的方法:


4、O

构造函数:

要调用的方法:

简单演示:一个对象的复制。

开发步骤:
1. 创建ByteArrayOutputStream,将数据可以转换成字节
2. 创建ObjectOutputStream,关联ByteArrayOutputStream
3. 使用ObjectOutputStream的writeObject,读取要复制的对象
4. 使用ByteArrayInputStream读取ByteArrayOutputStream的转换的对象字节数据
5. 创建ObjectInputStream读取对象字节数据,创建新的对象

素材:
    package cn.itcast.domain;
   
    import java.io.Serializable;
   
    public class User implements Serializable {
   
        private String name;
        private int age;
   
        public String getName() {
            return name;
        }
   
        public void setName(String name) {
            this.name = name;
        }
   
        public int getAge() {
            return age;
        }
   
        public void setAge(int age) {
            this.age = age;
        }
   
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

测试代码:

     @Test
        public void test7() throws Exception {
            User user = new User();
            user.setName("李四");
            user.setAge(18);
   
            //1. 创建ByteArrayOutputStream,将数据可以转换成字节
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            //2. 创建ObjectOutputStream,关联ByteArrayOutputStream
            ObjectOutputStream out = new ObjectOutputStream(bout);
            //3. 使用ObjectOutputStream的writeObject,读取要复制的对象
            out.writeObject(user);
            //4. 使用ByteArrayInputStream读取ByteArrayOutputStream的转换的对象字节数据
            ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
            //5. 创建ObjectInputStream读取对象字节数据,创建新的对象
            ObjectInputStream in =  new ObjectInputStream(bin);
            User obj = (User) in.readObject();
   
            System.out.println(user+":"+user.hashCode());
            System.out.println(obj+":"+obj.hashCode());
        }

效果:

    User{name='李四', age=18}:556529265
    User{name='李四', age=18}:667447085

4.3 使用IO改写Person的clone方法
4.3.1 开发步骤

1. 克隆涉及的所有的类实现Serializable接口
2. 修改Person类的clone方法,使用IO复制对象
3. 测试演示
   
4.3.2 代码实现

1. 克隆涉及的所有的类实现Serializable
2. 修改Person类的clone方法,使用IO复制对象
       @Override
           public Person clone() {
      
               try {
                   //1. 创建ByteArrayOutputStream,将数据可以转换成字节
                   ByteArrayOutputStream bout = new ByteArrayOutputStream();
                   //2. 创建ObjectOutputStream,关联ByteArrayOutputStream
                   ObjectOutputStream out = new ObjectOutputStream(bout);
                   //3. 使用ObjectOutputStream的writeObject,读取要复制的对象
                out.writeObject(this);
                   //4. 使用ByteArrayInputStream读取ByteArrayOutputStream的转换的对象字节数据
                   ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
                   //5. 创建ObjectInputStream读取对象字节数据,创建新的对象
                   ObjectInputStream in = new ObjectInputStream(bin);
                   Person clone = (Person) in.readObject();
                   return clone;
               } catch (Exception e) {
                   e.printStackTrace();
                   return null;
               }
           }
3. 测试演示
          @Test
           public void test8() throws Exception {
      
               Person p1 = new Person();
               p1.setName("张三");
               p1.setAge(58);
      
               Children children1 = new Children();
               children1.setName("张伟");
               children1.setAge(25);
               p1.setChild(children1);
      
               Grandson grandson = new Grandson();
               grandson.setAge(2);
               grandson.setName("张无忌");
               p1.setGdson(grandson);
      
               Person p2 = p1.clone();
      
               System.out.println(p1.getChild()+":"+p1.getChild().hashCode());
               System.out.println(p2.getChild()+":"+p2.getChild().hashCode());
      
               System.out.println(p1.getGdson()+":"+p1.getGdson().hashCode());
               System.out.println(p2.getGdson()+":"+p2.getGdson().hashCode());
      
           }

效果:

    Children{name='张伟', age=25}:370869802
    Children{name='张伟', age=25}:1139700454
    Grandson{name='张无忌', age=2}:1213349904
    Grandson{name='张无忌', age=2}:592617454

5. 为什么使用clone方法需要实现Cloneable接口
答:源代码就是这么设定的,实现接口仅仅是一个可以使用clone方法的标记。
那么源代码是在哪里设定的呢?
查看jdk源码我们发现:


因此,我们需要查看native修饰的背后的源码,这个一直要追溯到jdk底层c,c++源码。

5.1 下载完整jdk源码
下载地址:http://jdk.java.net/java-se-ri/7

下载效果:

解压后:

由于Object必须在虚拟机启动的时候加载,想要查看Object底层的源码一定涉及虚拟机文件中。
这个clone native方法 位于openjdk\hotspot\src\share\vm\prims\jvm.cpp文件中。

关键代码在:JVM_Clone方法中

源码展示:
    JVM_ENTRY(jobject, JVM_Clone(JNIEnv* env, jobject handle))
      JVMWrapper("JVM_Clone");
      Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
      const KlassHandle klass (THREAD, obj->klass());
      JvmtiVMObjectAllocEventCollector oam;
   
    #ifdef ASSERT
      // Just checking that the cloneable flag is set correct
      if (obj->is_javaArray()) {
        guarantee(klass->is_cloneable(), "all arrays are cloneable");
      } else {
        guarantee(obj->is_instance(), "should be instanceOop");
        bool cloneable = klass->is_subtype_of(SystemDictionary::Cloneable_klass());
        guarantee(cloneable == klass->is_cloneable(), "incorrect cloneable flag");
      }
    #endif
   
      // Check if class of obj supports the Cloneable interface.
      // All arrays are considered to be cloneable (See JLS 20.1.5)
      if (!klass->is_cloneable()) {
        ResourceMark rm(THREAD);
        THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
      }
   
      // Make shallow object copy
      const int size = obj->size();
      oop new_obj = NULL;
      if (obj->is_javaArray()) {
        const int length = ((arrayOop)obj())->length();
        new_obj = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
      } else {
        new_obj = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);
      }
      // 4839641 (4840070): We must do an oop-atomic copy, because if another thread
      // is modifying a reference field in the clonee, a non-oop-atomic copy might
      // be suspended in the middle of copying the pointer and end up with parts
      // of two different pointers in the field.  Subsequent dereferences will crash.
      // 4846409: an oop-copy of objects with long or double fields or arrays of same
      // won't copy the longs/doubles atomically in 32-bit vm's, so we copy jlongs instead
      // of oops.  We know objects are aligned on a minimum of an jlong boundary.
      // The same is true of StubRoutines::object_copy and the various oop_copy
      // variants, and of the code generated by the inline_native_clone intrinsic.
      assert(MinObjAlignmentInBytes >= BytesPerLong, "objects misaligned");
      Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj,
                                   (size_t)align_object_size(size) / HeapWordsPerLong);
      // Clear the header
      new_obj->init_mark();
   
      // Store check (mark entire object and let gc sort it out)
      BarrierSet* bs = Universe::heap()->barrier_set();
      assert(bs->has_write_region_opt(), "Barrier set does not have write_region");
      bs->write_region(MemRegion((HeapWord*)new_obj, size));
   
      // Caution: this involves a java upcall, so the clone should be
      // "gc-robust" by this stage.
      if (klass->has_finalizer()) {
        assert(obj->is_instance(), "should be instanceOop");
        new_obj = instanceKlass::register_finalizer(instanceOop(new_obj), CHECK_NULL);
      }
   
      return JNIHandles::make_local(env, oop(new_obj));
    JVM_END

校验当前类是否实现克隆接口的代码:
    // Check if class of obj supports the Cloneable interface.
      // All arrays are considered to be cloneable (See JLS 20.1.5)
      if (!klass->is_cloneable()) {
        ResourceMark rm(THREAD);
        THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
      }

注释翻译:
    数组类型默认可以直接克隆,而其他对象实现clone需要先实现Cloneable接口,否则抛出:
            CloneNotSupportedException异常


代码演示:
    package cn.itcast.domain;
    public class ExampleA {
   
            @Override
            public Object clone() throws CloneNotSupportedException {
                    return super.clone();
            }
    }
   

测试:

    @Test
            public void test4() throws Exception{
                    ExampleA a = new ExampleA();
                    Object clone = a.clone();
                    System.out.println(clone);
            }

效果:
    java.lang.CloneNotSupportedException: cn.itcast.domain.ExampleA
            at java.lang.Object.clone(Native Method)
            at cn.itcast.domain.ExampleA.clone(ExampleA.java:7)
            at cn.itcast.listener.TestUtils.test4(TestUtils.java:62)
            at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
            at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
            at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
            at java.lang.reflect.Method.invoke(Method.java:606)
            at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
            at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
            at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
            at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
            at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
            at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
            at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
            at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
            at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
            at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
            at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
            at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
            at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
            at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
            at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
            at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
            at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
            at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
            at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
   






欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2