一、对象流概述
1.1 为什么需要对象流?
我们前边学到的数据流只能实现对基本数据类型和字符串类型的读写,并不能读取对象(字符串除外),如果要对某个对象进行读写操作,我们需要学习一对新的处理流:ObjectInputStream和ObjectOutputStream。
ObjectOutputStream代表对象输出流,可以对Java中的基本数据和对象进行序列化操作。
ObjectInputStream代表对象输入流,可以对 ObjectOutputStream 写入的Java中基本数据和对象进行反序列化。
【示例】对象流的简单应用
public class ObjectStreanTest { /** * 通过ObjectOutputStream写出数据 */ public static void write() { ObjectOutputStream oos = null; try { // 字节输出流 FileOutputStream fos = new FileOutputStream("a.txt"); // 缓冲流 BufferedOutputStream bos = new BufferedOutputStream(fos); // 对象输出流 oos = new ObjectOutputStream(bos); // 数据输出,执行序列化操作 oos.writeInt(123); // 写入int类型数据 oos.writeDouble(123.45); // 写入Double类型数据 oos.writeBoolean(false); // 写入Boolean类型数据 oos.writeUTF("whpowernode"); // 写入String类型数据 oos.writeObject(new Date()); // 写入Date对象 } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if(oos != null) { try { oos.close(); // 关闭流 } catch (IOException e) { e.printStackTrace(); } } } } /** * 通过ObjectInputStream写入数据 */ public static void read() { ObjectInputStream ois = null; try { //字节输入流, “a.txt”就是前面ObjectOutputStream写入的文件 FileInputStream fos = new FileInputStream("a.txt"); //缓冲流 BufferedInputStream bos = new BufferedInputStream(fos); //对象输入流 ois = new ObjectInputStream(bos); //读取数据,执行反序列化操作,需要保证存取顺序一致 int i = ois.readInt(); double d = ois.readDouble(); boolean b = ois.readBoolean(); String str = ois.readUTF(); Date date = (Date)ois.readObject(); // 读取对象 System.out.println(i + " " + d + " " + b + " " + str + date); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { if(ois != null) { try { ois.close(); // 关闭流 } catch (IOException e) { e.printStackTrace(); } } } } } |
补充,对象流不仅可以读写对象,还可以读写基本数据类型。使用对象流读写对象时,该对象必须实现序列化与反序列化,系统提供的类(如Date和String等)已经实现了序列化接口,但是自定义类必须手动实现序列化接口。
二、序列化和反序列化
2.1 序列化和反序列化概述
1)为什么需要序列化和反序列化?
当两个进程远程通信时,彼此可以发送任意数据类型的数据。 无论是何种类型的数据,都会以二进制序列的形式在网络上传送。比如,我们可以通过HTTP协议发送字符串信息,我们也可以在网络上直接发送Java对象。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
2)序列化和反序列化概述:
把Java对象转换为字节序列的过程称为对象的序列化。把字节序列恢复为Java对象的过程称为对象的反序列化。
3)对象序列化的作用:
1)把对象的字节序列永久保存到硬盘上,通常存放在一个文件中,也就是执行持久化操作。
2)在网络上传送对象的字节序列。比如:服务器之间的数据通信、对象传递。
2.2 对象的序列化的实现
对象输出流:
ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
自定义类需手动实现java.io.Serializable接口:
当一个对象要能被序列化,这个对象所属的类必须实现 java.io.Serializable 接口。系统提供的类(如Date和String等)已经默认帮我们实现了java.io.Serializable接口,但是自定义类必须手动实现java.io.Serializable接口,否则会发生NotSerializableException 异常。
另外, java.io.Serializable接口是空接口,不需要重写任何方法,只起到可序列化的标志的作用。
【示例】将Student类实现Serializable接口
class Student implements Serializable { // 实现Serializable接口 private String name; private int age; public Student() {} public Student(String name, int age) { this.name = name; this.age = 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; } } |
注意:如果对象的属性是对象,则属性对应类也必须实现java.io.Serializable接口。
【示例】将Student对象进行序列化
public class ObjectOutputStreamTest { public static void main(String[] args) { ObjectOutputStream oos = null; try { // 明确存储对象的文件 FileOutputStream fos = new FileOutputStream("stu.object"); // 给操作文件对象加入写入对象功能 oos = new ObjectOutputStream(fos); // 调用了写入对象的方法 oos.writeObject(new Student("小明", 18)); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if(oos != null) { try { oos.close(); // 关闭资源 } catch (IOException e) { e.printStackTrace(); } } } } } |
如果需要持久化Student对象的个数不确定,,建议把需要序列化的Student对象添加进入集合中,然后对集合对象进行序列化,这样做的好处就是方便存取。
【示例】对多个Student对象进行序列化
public class ObjectOutputStreamTest { public static void main(String[] args) { ObjectOutputStream oos = null; try { // 明确存储对象的文件 FileOutputStream fos = new FileOutputStream("list.object"); // 给操作文件对象加入写入对象功能 oos = new ObjectOutputStream(fos); // 把需要持久化的Student对象添加进入List中 ArrayList<Student> list = new ArrayList<Student>(); for (int i = 0; i < 5; i++) { list.add(new Student("小明" + i, 18 + i)); } // 调用了写入对象的方法,进行持久化操作 oos.writeObject(list); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if(oos != null) { try { oos.close(); // 关闭资源 } catch (IOException e) { e.printStackTrace(); } } } } } |
2.3 对象的反序列化的实现
ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
【示例】将单个Student对象进行反序列化
public class ObjectInputStreamTest { public static void main(String[] args) { ObjectInputStream ois = null; try { // 定义流对象关联存储了对象文件。 FileInputStream fis = new FileInputStream("stu.object"); // 建立用于读取对象的功能对象。 ois = new ObjectInputStream(fis); // 读取Student对象,也就是执行反序列化操作 Student p = (Student) ois.readObject(); System.out.println(p.getName() + " " + p.getAge()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { if(ois != null) { try { ois.close(); // 关闭流 } catch (IOException e) { e.printStackTrace(); } } } } } |
注意:只有支持 java.io.Serializable 接口的对象才能从流读取。
【示例】将多个Student对象进行反序列化
public class ObjectInputStreamTest { public static void main(String[] args) { ObjectInputStream ois = null; try { // 定义流对象关联存储了对象文件。 FileInputStream fis = new FileInputStream("list.object"); // 建立用于读取对象的功能对象。 ois = new ObjectInputStream(fis); // 读取list集合 ArrayList<Student> list = (ArrayList<Student>)ois.readObject(); for (int i = 0; i < list.size(); i++) { Student p = list.get(i); System.out.println(p.getName() + " " + p.getAge()); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { if(ois != null) { try { ois.close(); // 关闭流 } catch (IOException e) { e.printStackTrace(); } } } } } |
三、序列化接口
为什么需要序列化?
1)当一个对象要能被序列化,这个对象所属的类必须实现java.io.Serializable 接口,否则会抛出java.io.NotSerializableException 异常。
2)当反序列化对象时,如果对象所属的 class 文件在序列化之后进行的修改,那么进行反序列化操作时会抛出 java.io.InvalidClassException异常。
发生这个异常的原因是:该类的序列版本号与从流中读取的类描述符的版本号不匹配。
为了避免对象所属的class文件在序列化之后发生修改,从而引发出序列化版本号不一致的问题,我们需要在实现了java.io.Serializable接口之后,通过Eclipse手动的添加一个serialVersionUID。
序列化ID在Eclipse下提供了两种生成策略:
1)一个是固定的ID,serialVersionUID值固定为:1L
2)一个是随机生成一个不重复的 long 类型数据(实际上是使用JDK工具,根据类名、接口名、成员方法及属性等来生成)
如果没有特殊需求的话,使用用默认的1L就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化ID有什么作用呢,有些时候,通过改变序列化ID可以用来限制某些用户的使用。
【示例】将需要序列化的类实现Serializable接口
// 实现Serializable接口 class Student implements Serializable { /** * 给类显示声明一个动态的序列版本号。 */ private static final long serialVersionUID = 4913547067458582336L; private String name; private int age; public Student() {} public Student(String name, int age) { this.name = name; this.age = 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; } }
|
四、 瞬态关键字
当一个类的对象需要被序列化时,某些属性不需要被序列化,这时不需要序列化的属性可以使用关键字 transient 修饰。只要被 transient 修饰了,序列化时这个属性就不会被序列化了。
同时静态修饰也不会被序列化(因为序列化是把对象数据进行持久化存储,而静态的属于类加载时的数据,不会被序列化)。
【示例】代码修改如下,并进行读取对象测试
// 实现Serializable接口 class Student implements Serializable { /** * 给类显示声明一个动态的序列版本号。 */ private static final long serialVersionUID = 4913547067458582336L; private transient /*瞬态*/ String name; private static /*静态*/int age; public Student() {} public Student(String name, int age) { this.name = name; this.age = 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; } } |
五、 对象克隆
在原始对象的基础上繁殖新的对象,新对象有自己独立的内存空间,也就是说新对象和原始对象是两个不同的内存地址,当你修改克隆对象的数据丝毫不会影响原始对象。
克隆实现步骤:
1)创建原始对象
2)使用ByteArrayOutputStream和ObjectOutputStream将对象写入到内存流中
3)使用ByteArrayInputStream和ObjectInputStream将内存流中的对象读取到程序
【示例】实现对象的克隆操作
import java.io.Serializable; public class User implements Serializable{ private static final long serialVersionUID = 2773260196226678038L; // 成员变量 private int age; private String userName; /** * transient 修饰属性:表示password不会参与序列化和反序列化 */ private transient String password;
// 构造方法 public User() {} public User(int age, String userName, String password) { this.age = age; this.userName = userName; this.password = password; } // getter和setter方法 public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "User [age=" + age + ", userName=" + userName + ", password=" + password + "]"; } } import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; /** * 克隆对象工具类 */ class CloneObject { /** * 执行克隆的方法 * @param type 需要克隆的对象 * @return 返回克隆后的对象 * @throws Exception */ @SuppressWarnings("unchecked") public static <T> T clone(T type) throws Exception { // 1.使用ByteArrayOutputStream和ObjectOutputStream将对象写入到内存流中 try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos)) { // 对象写入到内存 oos.writeObject(type); // 2.使用ByteArrayInputStream和ObjectInputStream将内存流中的对象读取到程序 try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis)) { return (T) ois.readObject(); } } } } /** * 测试类 */ public class Test { public static void main(String[] args) { // 实例化需要克隆的对象 User user = new User(18, "zhangsan", "123"); try { // 执行克隆对象的操作 User robert = CloneObject.clone(user); System.out.println(robert); } catch (Exception e) { e.printStackTrace(); } } }
|