009资源网

009知识分享网,分享知识,创造价值,分享是一种态度!

理解Java中的三个标记接口,原来如此!!

2022-11-07 23:19 49次浏览 评论已关闭 编程

标识接口是没有任何方法和属性的接口。标识接口不对实现它的类有任何语义上的要求,它仅仅表明实现它的类属于一个特定的类型。

Java中的标记接口有很多,这里介绍其中三个比较经典的标记接口:

1.Serializable

2.Cloneable3.RandomAccess

Serializable理解Java中的三个标记接口,原来如此!!-1

这个接口相信大家都不陌生,该接口用于实现序列化,实现了该接口的类就是可序列化的(序列化指的是将对象的数据写入文件),来看看Serializable接口的源代码:

public interface Serializable {
}

Serializable内部其实没有任何内容,所以它实质上是一个标记型的接口,用于表示某些类可以被序列化。下面就来简单地使用一下Serializable接口,首先创建一个普通的Java类:

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
 private String name;
 private Integer age;
}

然后编写一段序列化代码:

public static void writeObject() throws Exception {
 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\Desktop\\test.txt"));
 User user = new User("张三", 20);
 oos.writeObject(user);
 oos.close();
}

执行该方法,程序报错:

Exception in thread "main" java.io.NotSerializableException: com.wwj.collection_.User

这是因为User类没有实现序列化接口,看看底层源码是如何对其进行检验的。首先查看writeObject方法:

public final void writeObject(Object obj) throws IOException {
 if (enableOverride) {
     writeObjectOverride(obj);
     return;
 }
 try {
     writeObject0(obj, false);
 } catch (IOException ex) {
     if (depth == 0) {
         writeFatalException(ex);
     }
     throw ex;
 }
}

enableOverride是类的一个成员变量,初始值为false,所以接着执行writeObject0方法:

private void writeObject0(Object obj, boolean unshared)
 throws IOException{
 ......
     // remaining cases
     if (obj instanceof String) {
         writeString((String) obj, unshared);
     } else if (cl.isArray()) {
         writeArray(obj, desc, unshared);
     } else if (obj instanceof Enum) {
         writeEnum((Enum<?>) obj, desc, unshared);
     } else if (obj instanceof Serializable) {
         writeOrdinaryObject(obj, desc, unshared);
     } else {
         if (extendedDebugInfo) {
             throw new NotSerializableException(
                 cl.getName() + "\n" + debugInfoStack.toString());
         } else {
             throw new NotSerializableException(cl.getName());
         }
     }
}

方法很长,我们挑重要的看,首先判断当前的object是否为String类型,若为String,则执行writeString;其次判断是否为Array类型,接着判断是否为Enum,最后看看object是否为Serializable类型,如果该object实现了Serializable接口,那么就可以正确执行之后的方法,若是没有实现,则抛出NotSerializableException异常。明白其原理后,我们就需要让User类实现Serializable接口:

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User implements Serializable {
 private String name;
 private Integer age;
}

此时再次执行序列化方法即可将对象内容存入文件:

$ cat test.txt
▒▒srcom.wwj.collection_.User▒I]a▒▒4LagetLjava/lang/Integer;LnametLjava/lang/String;xpsrjava.lang.Integer⠤▒▒▒8Ivaluexrjava.lang.Number▒▒▒
                                                     ▒▒▒xpt张三

也可以将文件中的内容反序列化成一个对象,代码如下:

public static void readObject() throws Exception {
 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\Desktop\\test.txt"));
 User user = (User) ois.readObject();
 System.out.println(user);
 ois.close();
}

运行结果:

User(name=张三, age=20)

Cloneable

理解Java中的三个标记接口,原来如此!!-1

该接口与Serializable接口一样,都是标记型接口:

public interface Cloneable {
}

当一个类实现了Cloneable接口时,就可以使用该类的clone方法对其进行克隆,否则,就会抛出CloneNotSupportedException异常。克隆指的是根据已有的数据,创建一份完全一样的副本,一个类要想能够被克隆,则必须实现Cloneable接口并重写clone方法,比如:

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User implements Cloneable {
 private String name;
 private Integer age;

 @Override
 protected Object clone() throws CloneNotSupportedException {
     return super.clone();
 }
}

此时即可对User对象进行克隆:

public static void main(String[] args) throws Exception {
 User user = new User("张三", 20);
 User user2 = (User) user.clone();
 System.out.println(user);
 System.out.println(user2);
}

运行结果:

User(name=张三, age=20)
User(name=张三, age=20)

然而这样的克隆方式是有局限性的,比如我们修改一下User类:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Cloneable {
 private String name;
 private Integer age;
 private Address address;

 @Override
 protected Object clone() throws CloneNotSupportedException {
     return super.clone();
 }
}

@Data @NoArgsConstructor @AllArgsConstructor public class Address { private String province; private String city; }

此时我们再去克隆一下试试:
```java
public static void main(String[] args) throws Exception {
    Address address = new Address("浙江", "杭州");
    User user = new User("张三", 20,address);
    User user2 = (User) user.clone();
    System.out.println(user);
    System.out.println(user2);
}

运行结果:

User(name=张三, age=20, address=Address(province=浙江, city=杭州))
User(name=张三, age=20, address=Address(province=浙江, city=杭州))

看着运行结果好像并没有产生什么问题,接下来我们尝试修改其中一个对象的属性值:

public static void main(String[] args) throws Exception {
    Address address = new Address("浙江", "杭州");
    User user = new User("张三", 20,address);
    User user2 = (User) user.clone();
    user.setName("李四");
    address.setCity("金华");
    System.out.println(user);
    System.out.println(user2);
}

运行结果:

User(name=李四, age=20, address=Address(province=浙江, city=金华))
User(name=张三, age=20, address=Address(province=浙江, city=金华))

通过运行结果能够发现两点,首先name属性值的修改并没有影响到user2,但是Address中city属性值的修改却影响到了它,这种现象就叫做 浅拷贝 。

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

通过一张图来理解一下:理解Java中的三个标记接口,原来如此!!-2对于基本类型,user2会将原值拷贝一份,放入自己的对象中,而对于引用类型,它只会拷贝内存地址,也就是说,user2中的Address属性实际上只是引用了user中的内容,现在修改user中的值:理解Java中的三个标记接口,原来如此!!-3由于name值是一份真的拷贝,所以user中对name的修改并不会影响到user2,但修改了user中的city,此时读取user2的city时,它读取的仍然是user中的city,所以就出现了刚才的现象。

要想解决这一问题,我们可以使用深拷贝,若想实现深拷贝,我们就需要改造刚才的程序,首先要让Address类也实现Cloneable接口:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address implements Cloneable{
    private String province;
    private String city;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

接着需要改造一下User类的clone方法:

Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Cloneable {
    private String name;
    private Integer age;
    private Address address;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        Address address = (Address) user.address.clone();
        user.setAddress(address);
        return user;
    }
}

首先我们仍然通过父类的clone方法克隆出一个新的User对象,但我们知道,这个User对象仅仅是拷贝了基本数据类型,即:name和age的值,对于引用类型Address,只是拷贝了它的内存地址,若想让引用类型也能够完全拷贝值,就需要手动调用User对象中Address的clone方法,并将克隆得到的Address对象赋值给属性address;需要注意的是,如果Address类中仍然有引用类型变量,那么就需要改造Address的clone方法,并手动克隆出引用类型变量,将其赋值。现在重新运行之前的代码:

public static void main(String[] args) throws Exception {
    Address address = new Address("浙江", "杭州");
    User user = new User("张三", 20,address);
    User user2 = (User) user.clone();
    user.setName("李四");
    address.setCity("金华");
    System.out.println(user);
    System.out.println(user2);
}

结果如下:

User(name=李四, age=20, address=Address(province=浙江, city=金华))
User(name=张三, age=20, address=Address(province=浙江, city=杭州))

其原理图如下:理解Java中的三个标记接口,原来如此!!-4经过深拷贝克隆出来的对象是完全相互独立的,无论user对象如何变化,user2都不会受到影响。

RandomAccess

理解Java中的三个标记接口,原来如此!!-1

该接口也是一个标记接口:

public interface RandomAccess {
}

若某个类实现了RandomAccess,则表明该类支持快速的随机访问,其目的是允许通用算法更改其行为,以便在应用于随机访问列表(访问索引)或顺序访问列表(迭代器)时提供良好的性能。比如Java集合中的ArrayList就实现了该接口,而且对于实现了该接口的List,其随机访问列表的效率要高于顺序访问列表,我们可以尝试着验证一下。先来看看随机访问列表的耗时:

public static void main(String[] args) throws Exception {
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 100000; i++) {
        list.add(i);
    }
    long start = System.currentTimeMillis();
    for (int i = 0; i < list.size(); i++) {
        Integer num = list.get(i);
    }
    long end = System.currentTimeMillis();
    System.out.println(end - start);
}

运行结果:

8

再看看顺序访问列表的耗时:

public static void main(String[] args) throws Exception {
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 100000; i++) {
        list.add(i);
    }
    Iterator<Integer> iterator = list.iterator();
    long start = System.currentTimeMillis();
    while(iterator.hasNext()){
        Integer num = iterator.next();
    }        
    long end = System.currentTimeMillis();
    System.out.println(end - start);
}

运行结果:

17

而对于没有实现RandomAccess接口的LinkedList,它的效率又会是如何呢?首先测试随机访问列表:

public static void main(String[] args) throws Exception {
        List<Integer> list = new LinkedList<>();
        for (int i = 0; i < 100000; i++) {
            list.add(i);
        }
        long start = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            Integer num = list.get(i);
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

运行结果:

11076

然而测试顺序访问列表:

public static void main(String[] args) throws Exception {
    List<Integer> list = new LinkedList<>();
    for (int i = 0; i < 100000; i++) {
        list.add(i);
    }
    Iterator<Integer> iterator = list.iterator();
    long start = System.currentTimeMillis();
    while(iterator.hasNext()){
        Integer num = iterator.next();
    }
    long end = System.currentTimeMillis();
    System.out.println(end - start);
}

运行结果:

36

由此可以看出,LinkedeList的随机和顺序访问效率都很低,其中随机访问效率低的离谱。

发表评论
该文章暂时关闭了评论哦~