Java 拷贝

  • java的赋值都是传值的,对于基础类型来说,会拷贝具体的内容,但是对于引用对象来说,存储的这个值只是指向实际对象的地址,拷贝也只会拷贝引用地址。

  • java中所有的对象都是继承自java.lang.Object。Object对象中提供了一个clone方法,来供我们对java对象进行拷贝。protected native Object clone() throws CloneNotSupportedException;

    • 方法是native的,所以不需要我们来实现

    • clone方法还是protected,这意味着clone方法只能在java.lang包或者其子类可见。

    • 如果我们想要在一个程序中调用某个对象的clone方法则是不可以的。因为clone方法是定义在Object中的,该对象并没有对外可见的clone方法。

    • JDK的建议是让我们去实现接口Cloneable,实现了这个接口就表示这个对象可以调用Object的clone方法。

      1
      2
      3
      // Cloneable是空的,明没有强制要你去实现clone方法。
      public interface Cloneable {
      }
    • clone只是对象的拷贝,它只是简单的拷贝对象,而不会去执行对象的构造函数。clone会导致浅拷贝的问题。

  • 根据对对象属性的拷贝程度(基本数据类和引用类型),有三种类型的拷贝:

    • 浅拷贝 (Shallow Copy)
    • 深拷贝 (Deep Copy)
    • 延迟拷贝(Lazy Copy)

浅拷贝

  • 只是增加了一个指针指向已存在的内存地址
    • 基本类型,拷贝的就是基本类型的值;
    • 如果属性是内存地址(引用类型),拷贝的就是内存地址,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
    • 即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

浅拷贝的实现

  • 实现对象拷贝的类,需要实现 Cloneable 接口,并覆写 clone() 方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    class Student implements Cloneable {
    //引用类型
    private Teacher teacher;
    //基础数据类型
    private String name;


    public Teacher getTeacher() {
    return teacher;
    }

    public void setTeacher(Teacher teacher) {
    this.teacher = teacher;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    @Override
    protected Object clone() {
    try {
    // 浅拷贝:直接调用父类的clone()方法
    return super.clone();
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    return null;
    }
    }

    @Override
    public String toString() {
    return "[Student: " + this.hashCode()
    + ",Teacher:" + this.getTeacher()+":"
    + this.getTeacher().getName()
    + ",name:" + this.getName() + "]";
    }
    }

    class Teacher {
    private String name;

    public String getName() {
    return name;
    }

    public Teacher(String name) {
    this.name = name;
    }
    }
  • 测试clone结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public static void main(String[] args) {
    Student studentA = new Student();
    studentA.setName("张三");
    studentA.setTeacher(new Teacher("老师1"));
    //克隆
    Student studentB = (Student) studentA.clone();
    studentB.setName("李四");
    System.out.println("studentA:" + studentA.toString());
    System.out.println("studentB:" + studentB.toString());
    // 修改引用数据类型
    studentA.getTeacher().setName("张老师");
    System.out.println("studentB:" + studentB.toString());

    }

    studentA:[Student: 1531448569,Teacher:pers.fulsun.Teacher@1e80bfe8:老师1,name:张三]
    studentB:[Student: 1198108795,Teacher:pers.fulsun.Teacher@1e80bfe8:老师1,name:李四]
    studentB:[Student: 1198108795,Teacher:pers.fulsun.Teacher@1e80bfe8:张老师,name:李四]
    • 输出的结果可见,通过 studentA.clone() 拷贝对象后得到的 studentB,和 studentA 是两个不同的对象。
    • studentAstudentB 的基础数据类型的修改互不影响,而引用类型 subject 修改后是会有影响的。
    • 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个。
    • 对于引用类型,比如数组或者类对象,因为引用类型是引用传递,所以浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响。

深拷贝

  • 可以看到,浅拷贝会带来数据安全方面的隐患,例如我们只是想修改了 studentAname,但是 studentBname 也被修改了,因为它们都是指向的同一个地址。所以,此种情况下,我们需要用到深拷贝。

  • 深拷贝,在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。

深拷贝特点

  • 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。
    • 基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
  • 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。
    • 改变其中一个,不会对另外一个也产生影响。
  • 对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝。
  • 深拷贝相比于浅拷贝速度较慢并且花销较大。

拷贝的实现

  • Studentclone() 方法中,需要拿到拷贝自己后产生的新的对象,然后对新的对象的引用类型再调用拷贝操作,实现对引用类型成员变量的深拷贝。

  • 对于 Student 的引用类型的成员变量 Teacher ,需要实现 Cloneable 并重写 clone() 方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Teacher implements Cloneable{
    private String name;

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public Teacher(String name) {
    this.name = name;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
    //如果也有引用类型的成员属性,也应该和 Student 类一样实现
    return super.clone();
    }
    }
  • 重写clone()方法,对克隆对象的应用对象进行克隆

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Student implements Cloneable {
    @Override
    protected Object clone() {
    try {
    // 浅拷贝:直接调用父类的clone()方法
    Student clone = (Student) super.clone();
    // 对新的对象的引用类型再调用拷贝操作
    clone.teacher = (Teacher) teacher.clone();
    return clone;
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    return null;
    }
    }
    }

序列化进行拷贝

  • 序列化是属于那种类型拷贝?
  • 答案是:通过序列化来实现深拷贝。可以思考一下,为何序列化对象要用深拷贝而不是用浅拷贝呢?

注意要点

  • 可以序列化是干什么的?
  • 它将整个对象图写入到一个持久化存储文件中并且当需要的时候把它读取回来, 这意味着当你需要把它读取回来时你需要整个对象图的一个拷贝。
  • 这就是当你深拷贝一个对象时真正需要的东西。请注意,当你通过序列化进行深拷贝时,必须确保对象图中所有类都是可序列化的。

序列化案例

  • 看一下下面案例,很简单,只需要实现Serializable这个接口。Android中还可以实现Parcelable接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class ColoredCircle implements Serializable {

    private int x;
    private int y;

    public ColoredCircle(int x, int y) {
    this.x = x;
    this.y = y;
    }

    public int getX() {
    return x;
    }

    public void setX(int x) {
    this.x = x;
    }

    public int getY() {
    return y;
    }

    public void setY(int y) {
    this.y = y;
    }

    @Override
    public String toString() {
    return "x=" + x + ", y=" + y;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    private void test3() {
    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;
    try {
    // 创建原始的可序列化对象
    DouBi c1 = new DouBi(100, 100);
    System.out.println("原始的对象 = " + c1);
    DouBi c2 = null;
    // 通过序列化实现深拷贝
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    oos = new ObjectOutputStream(bos);
    // 序列化以及传递这个对象
    oos.writeObject(c1);
    oos.flush();
    ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
    ois = new ObjectInputStream(bin);
    // 返回新的对象
    c2 = (DouBi) ois.readObject();
    // 校验内容是否相同
    System.out.println("复制后的对象 = " + c2);
    // 改变原始对象的内容
    c1.setX(200);
    c1.setY(200);
    // 查看每一个现在的内容
    System.out.println("查看原始的对象 = " + c1);
    System.out.println("查看复制的对象 = " + c2);
    } catch (IOException e) {
    System.out.println("Exception in main = " + e);
    } catch (ClassNotFoundException e) {
    e.printStackTrace();
    } finally {
    if (oos != null) {
    try {
    oos.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    if (ois != null) {
    try {
    ois.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    }
  • 输出结果如下:

    1
    2
    3
    4
    原始的对象 = x=100, y=100
    复制后的对象 = x=100, y=100
    查看原始的对象 = x=200, y=200
    查看复制的对象 = x=100, y=100
  • 注意:需要做以下几件事儿:

    • 确保对象图中的所有类都是可序列化的
    • 创建输入输出流
    • 使用这个输入输出流来创建对象输入和对象输出流
    • 将你想要拷贝的对象传递给对象输出流
    • 从对象输入流中读取新的对象并且转换回你所发送的对象的类
  • 得出的结论

    • 在这个例子中,创建了一个DouBi对象c1然后将它序列化 (将它写到ByteArrayOutputStream中). 然后我反序列化这个序列化后的对象并将它保存到c2中。随后我修改了原始对象c1。然后结果如你所见,c1不同于c2,对c1所做的任何修改都不会影响c2。
    • 注意,序列化这种方式有其自身的限制和问题:因为无法序列化transient变量, 使用这种方法将无法拷贝transient变量。再就是性能问题。创建一个socket, 序列化一个对象, 通过socket传输它, 然后反序列化它,这个过程与调用已有对象的方法相比是很慢的。所以在性能上会有天壤之别。如果性能对你的代码来说是至关重要的,建议不要使用这种方式。它比通过实现Clonable接口这种方式来进行深拷贝几乎多花100倍的时间。

延迟拷贝

  • 延迟拷贝是浅拷贝和深拷贝的一个组合,实际上很少会使用。这个以前几乎都没听说过,后来看书才知道有这么一种拷贝!
  • 当最开始拷贝一个对象时,会使用速度较快的浅拷贝,还会使用一个计数器来记录有多少对象共享这个数据。当程序想要修改原始的对象时,它会决定数据是否被共享(通过检查计数器)并根据需要进行深拷贝。
  • 延迟拷贝从外面看起来就是深拷贝,但是只要有可能它就会利用浅拷贝的速度。当原始对象中的引用不经常改变的时候可以使用延迟拷贝。由于存在计数器,效率下降很高,但只是常量级的开销。而且, 在某些情况下, 循环引用会导致一些问题。

如何选择拷贝方式

  • 如果对象的属性全是基本类型的,那么可以使用浅拷贝。
  • 如果对象有引用属性,那就要基于具体的需求来选择浅拷贝还是深拷贝。
  • 意思是如果对象引用任何时候都不会被改变,那么没必要使用深拷贝,只需要使用浅拷贝就行了。如果对象引用经常改变,那么就要使用深拷贝。没有一成不变的规则,一切都取决于具体需求。

数组的拷贝

  • 数组除了默认实现了clone()方法之外,还提供了Arrays.copyOf方法用于拷贝,这两者都是浅拷贝。

基本数据类型数组

  • 如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public void test4() {
    int[] lNumbers1 = new int[5];
    int[] rNumbers1 = Arrays.copyOf(lNumbers1, lNumbers1.length);
    rNumbers1[0] = 1;
    boolean first = lNumbers1[0] == rNumbers1[0];
    Log.d("小杨逗比", "lNumbers2[0]=" + lNumbers1[0] + ",rNumbers2[0]=" + rNumbers1[0]+"---"+first);

    int[] lNumbers3 = new int[5];
    int[] rNumbers3 = lNumbers3.clone();
    rNumbers3[0] = 1;
    boolean second = lNumbers3[0] == rNumbers3[0];
    Log.d("小杨逗比", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]+"---"+second);
    }
  • 打印结果如下所示

    1
    2
    小杨逗比: lNumbers2[0]=0,rNumbers2[0]=1---false
    小杨逗比: lNumbers3[0]=0,rNumbers3[0]=1---false

引用数据类型数组

  • 如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public static void test5() {
    People[] lNumbers1 = new People[5];
    lNumbers1[0] = new People();
    People[] rNumbers1 = lNumbers1;
    boolean first = lNumbers1[0].equals(rNumbers1[0]);
    Log.d("小杨逗比", "lNumbers1[0]=" + lNumbers1[0] + ",rNumbers1[0]=" + rNumbers1[0]+"--"+first);

    People[] lNumbers2 = new People[5];
    lNumbers2[0] = new People();
    People[] rNumbers2 = Arrays.copyOf(lNumbers2, lNumbers2.length);
    boolean second = lNumbers2[0].equals(rNumbers2[0]);
    Log.d("小杨逗比", "lNumbers2[0]=" + lNumbers2[0] + ",rNumbers2[0]=" + rNumbers2[0]+"--"+second);

    People[] lNumbers3 = new People[5];
    lNumbers3[0] = new People();
    People[] rNumbers3 = lNumbers3.clone();
    boolean third = lNumbers3[0].equals(rNumbers3[0]);
    Log.d("小杨逗比", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]+"--"+third);
    }

    public static class People implements Cloneable {

    int age;
    Holder holder;

    @Override
    protected Object clone() {
    try {
    return super.clone();
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    }
    return null;
    }

    public static class Holder {
    int holderValue;
    }
    }
  • 打印日志如下

    1
    2
    3
    小杨逗比: lNumbers1[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18,rNumbers1[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18--true
    小杨逗比: lNumbers2[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671,rNumbers2[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671--true
    小杨逗比: lNumbers3[0]=org.yczbj.ycrefreshview.MainActivity$People@91e9c56,rNumbers3[0]=org.yczbj.ycrefreshview.MainActivity$People@91e9c56--true

集合的拷贝

  • 集合的拷贝也是我们平时经常会遇到的,一般情况下,我们都是用浅拷贝来实现,即通过构造函数或者clone方法。

集合浅拷贝

  • 构造函数和 clone() 默认都是浅拷贝

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    public static void test6() {
    ArrayList<People> lPeoples = new ArrayList<>();
    People people1 = new People();
    lPeoples.add(people1);
    Log.d("小杨逗比", "lPeoples[0]=" + lPeoples.get(0));
    // 拷贝
    ArrayList<People> rPeoples = (ArrayList<People>) lPeoples.clone();
    Log.d("小杨逗比", "rPeoples[0]=" + rPeoples.get(0));
    boolean b = lPeoples.get(0).equals(rPeoples.get(0));
    Log.d("小杨逗比", "比较两个对象" + b);
    }

    public static class People implements Cloneable {

    int age;
    Holder holder;

    @Override
    protected Object clone() {
    try {
    People people = (People) super.clone();
    people.holder = (People.Holder) this.holder.clone();
    return people;
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    }
    return null;
    }

    public static class Holder implements Cloneable {

    int holderValue;

    @Override
    protected Object clone() {
    try {
    return super.clone();
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    }
    return null;
    }
    }
    }
  • 打印日志

    1
    2
    3
    小杨逗比: lPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18
    小杨逗比: rPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18
    小杨逗比: 比较两个对象true

集合深拷贝

  • 在某些特殊情况下,如果需要实现集合的深拷贝,那就要创建一个新的集合,然后通过深拷贝原先集合中的每个元素,将这些元素加入到新的集合当中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    public static void test7() {
    ArrayList<People> lPeoples = new ArrayList<>();
    People people1 = new People();
    people1.holder = new People.Holder();
    lPeoples.add(people1);
    Log.d("小杨逗比", "lPeoples[0]=" + lPeoples.get(0));
    ArrayList<People> rPeoples = new ArrayList<>();
    for (People people : lPeoples) {
    rPeoples.add((People) people.clone());
    }
    Log.d("小杨逗比", "rPeoples[0]=" + rPeoples.get(0));
    boolean b = lPeoples.get(0).equals(rPeoples.get(0));
    Log.d("小杨逗比", "比较两个对象" + b);
    }

    public static class People implements Cloneable {

    int age;
    Holder holder;

    @Override
    protected Object clone() {
    try {
    People people = (People) super.clone();
    people.holder = (People.Holder) this.holder.clone();
    return people;
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    }
    return null;
    }

    public static class Holder implements Cloneable {

    int holderValue;

    @Override
    protected Object clone() {
    try {
    return super.clone();
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    }
    return null;
    }
    }
    }
  • 打印日志

    1
    2
    3
    小杨逗比: lPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18
    小杨逗比: rPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671
    小杨逗比: 比较两个对象false

transient

作用

  • 我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
  • 然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
  • 总之,java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

transient案例

  • 示例code如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    /**
    * @description 使用transient关键字不序列化某个变量
    * 注意读取的时候,读取数据的顺序一定要和存放数据的顺序保持一致
    */
    public class TransientTest {

    public static void main(String[] args) {

    User user = new User();
    user.setUsername("Alexia");
    user.setPasswd("123456");

    System.out.println("read before Serializable: ");
    System.out.println("username: " + user.getUsername());
    System.err.println("password: " + user.getPasswd());

    try {
    ObjectOutputStream os = new ObjectOutputStream(
    new FileOutputStream("C:/user.txt"));
    os.writeObject(user); // 将User对象写进文件
    os.flush();
    os.close();
    } catch (FileNotFoundException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    }
    try {
    ObjectInputStream is = new ObjectInputStream(new FileInputStream(
    "C:/user.txt"));
    user = (User) is.readObject(); // 从流中读取User的数据
    is.close();

    System.out.println("\nread after Serializable: ");
    System.out.println("username: " + user.getUsername());
    System.err.println("password: " + user.getPasswd());

    } catch (FileNotFoundException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    } catch (ClassNotFoundException e) {
    e.printStackTrace();
    }
    }
    }

    class User implements Serializable {
    private static final long serialVersionUID = 8294180014912103005L;

    private String username;
    private transient String passwd;

    public String getUsername() {
    return username;
    }

    public void setUsername(String username) {
    this.username = username;
    }

    public String getPasswd() {
    return passwd;
    }

    public void setPasswd(String passwd) {
    this.passwd = passwd;
    }

    }
  • 输出为:

    1
    2
    3
    4
    5
    6
    7
    read before Serializable:
    username: Alexia
    password: 123456

    read after Serializable:
    username: Alexia
    password: null

transient使用小结

  • transient使用小结

    • 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
    • transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。
    • 变量如果是用户自定义类变量,则该类需要实现Serializable接口。
    • 被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。
  • 第三点可能有些人很迷惑,因为发现在User类中的username字段前加上static关键字后,程序运行结果依然不变,即static类型的username也读出来为“Alexia”了,这不与第三点说的矛盾吗?

    • 实际上是这样的:第三点确实没错(一个静态变量不管是否被transient修饰,均不能被序列化)
    • 反序列化后类中static型变量username的值为当前JVM中对应static变量的值,这个值是JVM中的不是反序列化得出的,下面证明:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    /**
    * @description 使用transient关键字不序列化某个变量
    * 注意读取的时候,读取数据的顺序一定要和存放数据的顺序保持一致
    */
    public class TransientTest {

    public static void main(String[] args) {

    User user = new User();
    user.setUsername("Alexia");
    user.setPasswd("123456");

    System.out.println("read before Serializable: ");
    System.out.println("username: " + user.getUsername());
    System.err.println("password: " + user.getPasswd());

    try {
    ObjectOutputStream os = new ObjectOutputStream(
    new FileOutputStream("C:/user.txt"));
    os.writeObject(user); // 将User对象写进文件
    os.flush();
    os.close();
    } catch (FileNotFoundException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    }
    try {
    // 在反序列化之前改变username的值
    User.username = "jmwang";

    ObjectInputStream is = new ObjectInputStream(new FileInputStream(
    "C:/user.txt"));
    user = (User) is.readObject(); // 从流中读取User的数据
    is.close();

    System.out.println("\nread after Serializable: ");
    System.out.println("username: " + user.getUsername());
    System.err.println("password: " + user.getPasswd());

    } catch (FileNotFoundException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    } catch (ClassNotFoundException e) {
    e.printStackTrace();
    }
    }
    }

    class User implements Serializable {
    private static final long serialVersionUID = 8294180014912103005L;

    public static String username;
    private transient String passwd;

    public String getUsername() {
    return username;
    }

    public void setUsername(String username) {
    this.username = username;
    }

    public String getPasswd() {
    return passwd;
    }

    public void setPasswd(String passwd) {
    this.passwd = passwd;
    }

    }
  • 输出为:

    1
    2
    3
    4
    5
    6
    7
    read before Serializable:
    username: Alexia
    password: 123456

    read after Serializable:
    username: jmwang
    password: null

transient思考

  • transient使用细节——被transient关键字修饰的变量真的不能被序列化吗?思考下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class ExternalizableTest implements Externalizable {

    private transient String content = "是的,我将会被序列化,不管我是否被transient关键字修饰";

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
    out.writeObject(content);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException,
    ClassNotFoundException {
    content = (String) in.readObject();
    }

    public static void main(String[] args) throws Exception {

    ExternalizableTest et = new ExternalizableTest();
    ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
    new File("test")));
    out.writeObject(et);

    ObjectInput in = new ObjectInputStream(new FileInputStream(new File(
    "test")));
    et = (ExternalizableTest) in.readObject();
    System.out.println(et.content);

    out.close();
    in.close();
    }
    }
  • content变量会被序列化吗?好吧,我把答案都输出来了,是的,运行结果就是:

    1
    是的,我将会被序列化,不管我是否被transient关键字修饰
  • 这是为什么呢,不是说类的变量被transient关键字修饰以后将不能序列化了吗?

    • 在Java中,对象的序列化可以通过实现两种接口来实现,若实现的是Serializable接口,则所有的序列化将会自动进行,若实现的是Externalizable接口,则没有任何东西可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量,这与是否被transient修饰无关。因此第二个例子输出的是变量content初始化的内容,而不是null。