【Java基础】Java IO流的总结

时间:2023-03-09 07:53:42
【Java基础】Java IO流的总结

  Java IO流分为输入流和输出流,而输入流和输出流中又分字符流和字节流。顾名思义,输入流则是输入到程序中计算,输出流是把程序的结果输出到文件或者设备。而字符流输入输出以字符为单位,字节流则是以字节为单位。以一张图来表示这种流的结构关系如下:

【Java基础】Java IO流的总结

下面从最基本且也最万能的字节流开始分析:

字节流

  InputStream

    FileInputStream

    BufferedInputStream

  OutputStream

    FileOutputStream

    BufferedOutputStream

  InputStream是一个抽象类,该抽象类是表示字节输入流的所有类的超类。先看看超类中定义的几个通用方法:

abstract  int read()       从输入流中读取数据的下一个字节。
int       read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
int       read(byte[] b, int off, int len) 将输入流中最多 len 个数据字节读入 byte 数组。
void       close() 关闭此输入流并释放与该流关联的所有系统资源。

  可以看出,可以支持按字节读,或者一次读一个字节数组,并且返回此次读取了多少个字节,因为如果我们定义了一个1024长度的byte[1024]来读取一个文件,最后不一定在读取的末尾也返回1024个字节,这时候如果操作整个字节数组会出现非我们想要的数据,所以返回值很重要,它能让我们知道一次读了多少个字节。第三个read(byte[] b, int off, int len)用来限定读取len个字节,第一个字节存放的位置off,read(byte[] n)方法中off默认为0,len默认为数组的长度,如果off+len>数组长度,则会报IndexOutOfBoundException。另外,之所以给出数组形式的方法读,说明read()方法效率不够高。

  OutputStream也是抽象类,此抽象类是表示输出字节流的所有类的超类。输出流接受输出字节并将这些字节发送到某个接收器,从InputStream中的方法就可以很好的猜出OutputStream中的通用方法,它们分别是:

 void    close()  关闭此输出流并释放与此流有关的所有系统资源。
void flush() 刷新此输出流并强制写出所有缓冲的输出字节。
void write(byte[] b) 将 b.length 个字节从指定的 byte 数组写入此输出流。
void write(byte[] b, int off, int len) 将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流。
abstract void write(int b) 将指定的字节写入此输出流。

  其他方法一一对应,这里主要是flush方法,看看这个方法的解释:

flush()
刷新此输出流并强制写出所有缓冲的输出字节。flush 的常规协定是:如果此输出流的实现已经缓冲了以前写入的任何字节,则调用此方法指示应将这些字节立即写入它们预期的目标。
如果此流的预期目标是由基础操作系统提供的一个抽象(如一个文件),则刷新此流只能保证将以前写入到流的字节传递给操作系统进行写入,但不保证能将这些字节实际写入到物理设备(如磁盘驱动器)。

  InputStream和OutputStream的一些子类如下,其中最常用的是FileInputStream和FileOutputStream。

AudioInputStream, ByteArrayInputStream, FileInputStream, FilterInputStream, InputStream, ObjectInputStream, PipedInputStream, SequenceInputStream, StringBufferInputStream

ByteArrayOutputStream, FileOutputStream, FilterOutputStream, ObjectOutputStream, OutputStream, PipedOutputStream

  从InputStream和OutputStream的读写方法可以看到,都有“缓冲”byte数组来提高效率的方法。其实为了效率考虑,Java本身也提供了一个带缓冲的输入输出流:BufferedInputStreamBufferedOutputStream,它们继承自Filter**Stream,它们的构造方法如下:

BufferedInputStream(InputStream in)            创建一个 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。
BufferedInputStream(InputStream in, int size) 创建具有指定缓冲区大小的 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。 BufferedOutputStream(OutputStream out) 创建一个新的缓冲输出流,以将数据写入指定的底层输出流。
BufferedOutputStream(OutputStream out, int size) 创建一个新的缓冲输出流,以将具有指定缓冲区大小的数据写入指定的底层输出流。

关于字节为单位读写,带缓冲数组去读写和用Buffer的流去读写的效率比较,代码如下:

import java.io.*;

public class TestNew {
public static void main(String args[]) {
String src = "/Users/lili/IdeaPro/Test/src/src.avi";
String des = "/Users/lili/IdeaPro/Test/src/des.avi";
long startTime = System.currentTimeMillis();
copy1(src, des);//19631ms
copy2(src, des);//88ms
copy3(src, des);//442ms
copy4(src,des); //55ms
long endTime = System.currentTimeMillis();
System.out.println("It takes " + (endTime-startTime) + "ms"); } public static void copy1(String src, String des) {
InputStream is = null;
OutputStream os = null; try {
is = new FileInputStream(src);
os = new FileOutputStream(des);
int b = 0;
while ((b = is.read()) != -1) {
os.write(b);
} } catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
} if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
} }
} public static void copy2(String src, String des) {
InputStream is = null;
OutputStream os = null; try {
is = new FileInputStream(src);
os = new FileOutputStream(des);
byte[] bytes = new byte[1024];
int len = 0;
while ((len = is.read(bytes)) != -1) {
os.write(bytes, 0, len);
} } catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
} if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
} }
} public static void copy3(String src, String des) {
InputStream is = null;
OutputStream os = null; try {
is = new BufferedInputStream(new FileInputStream(src));
os = new BufferedOutputStream(new FileOutputStream(des));
int b = 0;
while ((b = is.read()) != -1) {
os.write(b);
} } catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
} if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
} }
} public static void copy4(String src, String des) {
InputStream is = null;
OutputStream os = null; try {
is = new BufferedInputStream(new FileInputStream(src));
os = new BufferedOutputStream(new FileOutputStream(des));
byte[] bytes = new byte[1024];
int len = 0;
while ((len = is.read(bytes)) != -1) {
os.write(bytes, 0, len);
} } catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
} if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
} }
}
}

字符流 = 字节流+编码

  使用场景:除非用记事本打开能读懂可以用字符流,其他的都用字节流。

  Reader

    InputStreamReader  

        FileReader

    BufferedReader

  Writer

    OutputStreamWriter

        FileWriter

    BufferedWriter

  Reader是用于读取字符流的抽象类。子类必须实现的方法只有 read(char[], int, int) 和 close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。一般多用的子类为FileReader和BufferedReader,先来看看Reader的几种read方法:

 int    read()  读取单个字符。
int read(char[] cbuf) 将字符读入数组。
abstract int read(char[] cbuf, int off, int len) 将字符读入数组的某一部分。

  这个和字节流没有区别,可以读取单个字符,为了效率考虑也可以定义一个字符数组,一次读取放入到字符数组并返回读取字符个数。 

  InputStreamReader 是字节流通向字符流的桥梁:它使用指定的charset读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。其构造方法如下:

InputStreamReader(InputStream in) 创建一个使用默认字符集的 InputStreamReader。
InputStreamReader(InputStream in, Charset cs) 创建使用给定字符集的 InputStreamReader。
InputStreamReader(InputStream in, CharsetDecoder dec) 创建使用给定字符集解码器的 InputStreamReader。
InputStreamReader(InputStream in, String charsetName) 创建使用指定字符集的 InputStreamReader。

可以看出InputStreamReader接受的输入是字节流,并可以指定编码集的名字来编码。

  FileReader是InputStreamReader的子类,用来读取字符文件的便捷类,它接受文件名或者文件对象的传参构造,免去了其父类InputStreamReader接受InputStream传参的麻烦,直接按照字符来读。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是适当的。要自己指定这些值,可以先在 FileInputStream 上构造一个 InputStreamReader。FileReader 用于读取字符流。要读取原始字节流,请考虑使用 FileInputStream。

  BufferedReader是为了达到最高效率进行设计的,一般包装在 InputStreamReader和FileReader之上,而且还可以指定缓冲大小,构造方法如下:

BufferedReader(Reader in) 创建一个使用默认大小输入缓冲区的缓冲字符输入流。
BufferedReader(Reader in, int sz) 创建一个使用指定大小输入缓冲区的缓冲字符输入流。

除了缓冲提供效率外,还有一个特别的福利,就是一次读取一行的方法,几个常用方法如下:

 int    read() 读取单个字符。
int read(char[] cbuf, int off, int len) 将字符读入数组的某一部分。
String readLine() 读取一个文本行。

    

  Writer是写入字符流的抽象类。子类必须实现的方法仅有 write(char[], int, int)、flush() 和 close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。常用方法如下:

abstract  void    close()  关闭此流,但要先刷新它。
abstract void flush() 刷新该流的缓冲。
void write(char[] cbuf) 写入字符数组。
abstract void write(char[] cbuf, int off, int len) 写入字符数组的某一部分。
void write(int c) 写入单个字符。
void write(String str) 写入字符串。
void write(String str, int off, int len) 写入字符串的某一部分。

  可以看出常用方法中有一些特别的,可以写字符串或者字符串的一部分了。

  OutputStreamWriter 是字符流通向字节流的桥梁,其构造方法和InputStreamReader一一对应:可使用指定的 charset 将要写入流中的字符编码成字节。它使用的字符集可以由名称指定或显式给定,否则将接受平台默认的字符集。

  每次调用 write() 方法都会导致在给定字符(或字符集)上调用编码转换器。在写入底层输出流之前,得到的这些字节将在缓冲区中累积。可以指定此缓冲区的大小,不过,默认的缓冲区对多数用途来说已足够大。注意,传递给 write() 方法的字符没有缓冲。

  FileWriter用来写入字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是可接受的。要自己指定这些值,可以先在 FileOutputStream 上构造一个 OutputStreamWriter。文件是否可用或是否可以被创建取决于底层平台。特别是某些平台一次只允许一个 FileWriter(或其他文件写入对象)打开文件进行写入。在这种情况下,如果所涉及的文件已经打开,则此类中的构造方法将失败。FileWriter 用于写入字符流。要写入原始字节流,请考虑使用 FileOutputStream。FileWriter的构造方法相对于FileReader,这里可以指定是否append,具体如下:

FileWriter(File file) 根据给定的 File 对象构造一个 FileWriter 对象。
FileWriter(File file, boolean append) 根据给定的 File 对象构造一个 FileWriter 对象。
FileWriter(FileDescriptor fd) 构造与某个文件描述符相关联的 FileWriter 对象。
FileWriter(String fileName) 根据给定的文件名构造一个 FileWriter 对象。
FileWriter(String fileName, boolean append) 根据给定的文件名以及指示是否附加写入数据的 boolean 值来构造 FileWriter 对象。

  BufferedWriter将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。可以指定缓冲区的大小,或者接受默认的大小。在大多数情况下,默认值就足够大了。该类提供了 newLine() 方法,它使用平台自己的行分隔符概念,此概念由系统属性 line.separator 定义。并非所有平台都使用新行符 ('\n') 来终止各行(windows:'\r\n',linux:'\n',mac:'\r')。因此调用此方法来终止每个输出行要优于直接写入新行符。通常 Writer 将其输出立即发送到底层字符或字节流。除非要求提示输出,否则建议用 BufferedWriter 包装所有其 write() 操作可能开销很高的 Writer(如 FileWriters 和 OutputStreamWriters)。其常用方法如下:

  

 void    close()
关闭此流,但要先刷新它。
void flush()
刷新该流的缓冲。
void newLine()
写入一个行分隔符。
void write(char[] cbuf, int off, int len)
写入字符数组的某一部分。
void write(int c)
写入单个字符。
void write(String s, int off, int len)
写入字符串的某一部分。

数据输入输出流

  数据输入流DataInputStream允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型。应用程序可以使用数据输出流写入稍后由数据输入流读取的数据。数据输出流允许应用程序以适当方式将基本 Java 数据类型写入输出流中。然后,应用程序可以使用数据输入流将数据读入。它们的构造方法如下

DataInputStream(InputStream in)  使用指定的底层 InputStream 创建一个 DataInputStream。

 DataOutputStream(OutputStream out) 创建一个新的数据输出流,将数据写入指定基础输出流

  输出流和输入流的顺序必须严格一直,不然会导致非正确的读取,一个简单示例如下:

public static void main(String args[]) throws IOException {
DataOutputStream dos = new DataOutputStream(new FileOutputStream("t.txt"));
dos.write(22);
dos.writeBoolean(true);
dos.writeChar('a');
dos.writeDouble(12.34);
dos.writeInt(100);
dos.close();
DataInputStream dis = new DataInputStream(new FileInputStream("t.txt"));
System.out.println(dis.read());
System.out.println(dis.readBoolean());
System.out.println(dis.readChar());
System.out.println(dis.readDouble());
System.out.println(dis.readInt());
}

内存操作流

  内存操作流是在内存中开辟了一个区域,流的输入输出都是内存,而不像之前说的流都是文件。内存操作流分为以下几种:  

  1. 操作字节数组
    ByteArrayInputStream
    ByteArrayOutputStream
  2. 操作字符数组
    CharArrayReader
    CharArrayWrite
  3. 操作字符串
    StringReader
    StringWriter

  下面以字节数组操作流举个例子来说明用法。

    public static void main(String[] args) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 写数据
for (int x = 0; x < 10; x++) {
baos.write(("test" + x).getBytes());
} // 释放资源
// 通过查看源码可以看到close方法是空实现,什么都没做,所以根本不需要close()
// baos.close();
byte[] bys = baos.toByteArray();
// 读数据,需要定义一个数组来接收
ByteArrayInputStream bais = new ByteArrayInputStream(bys);
int by = 0;
while ((by = bais.read()) != -1) {
System.out.print((char) by);
}
// bais.close();//可以不需要close()
}

  其他几种类似,只是操作的类型不一样,上面的示例是操作字节数组的。

打印流

  打印流是一个很特别的流,只有输出,没有输入。分别是字节打印流PrintStream和字符打印流PrintWriter,打印流的特点如下:

  1. 只有写数据的,没有读取数据。只能操作目的地,不能操作数据源。
  2. 可以操作任意类型的数据。
  3. 如果启动了自动刷新,能够自动刷新。即此时的println()等价于write()+newLine()+flush()
  4. 该流是可以直接操作文本文件的(即构造方法中可以传入File和String路径) 
  5. PrintStream 永远不会抛出 IOException

  以PrintStream为例,它不仅有其他输出流常见的write字节和字节数组,还有一系列的print和println方法,可以输出任意类型数据。

标准输入输出流

  标准输入流等价于InputStream,标准输出流等价于PrintStream,即有InputStream is = System.in;PrintStream ps = System.out;

串联输入流

  为了满足将多个流串联在一起输入,Java提供了SequenceInputStream,它提供了两个构造方法,可以将2个以上的流进行串联,其中如果要串联多个流需要用Vector(Vector<InputStream> vector = new Vector<InputStream>();)来添加多个流,最后利用vector.elements()方法得到Enumeration<InputStream>对象。

SequenceInputStream(Enumeration<? extends InputStream> e)
通过记住参数来初始化新创建的 SequenceInputStream,该参数必须是生成运行时类型为 InputStream 对象的 Enumeration 型参数。
SequenceInputStream(InputStream s1, InputStream s2)
通过记住这两个参数来初始化新创建的 SequenceInputStream(将按顺序读取这两个参数,先读取 s1,然后读取 s2),以提供从此 SequenceInputStream 读取的字节。

序列化和反序列化流

  序列化流:把对象按照流一样的方式存入文本文件或者在网络中传输。       对象    -- 流数据(ObjectOutputStream)
  反序列化流:把文本文件中的流对象数据或者网络中的流对象数据还原成对象。 流数据 -- 对象(ObjectInputStream)
  下面是JDK文档的叙述:

ObjectInputStream 对以前使用 ObjectOutputStream 写入的基本数据和对象进行反序列化。

ObjectOutputStream 和 ObjectInputStream 分别与 FileOutputStream 和 FileInputStream 一起使用时,可以为应用程序提供对对象图形的持久存储。ObjectInputStream 用于恢复那些以前序列化的对象。其他用途包括使用套接字流在主机之间传递对象,或者用于编组和解组远程通信系统中的实参和形参。

ObjectInputStream 确保从流创建的图形中所有对象的类型与 Java 虚拟机中显示的类相匹配。使用标准机制按需加载类。

只有支持 java.io.Serializable 或 java.io.Externalizable 接口的对象才能从流读取。

readObject 方法用于从流读取对象。应该使用 Java 的安全强制转换来获取所需的类型。在 Java 中,字符串和数组都是对象,所以在序列化期间将其视为对象。读取时,需要将其强制转换为期望的类型。

可以使用 DataInput 上的适当方法从流读取基本数据类型。

默认情况下,对象的反序列化机制会将每个字段的内容恢复为写入时它所具有的值和类型。反序列化进程将忽略声明为瞬态或静态的字段。对其他对象的引用使得根据需要从流中读取这些对象。使用引用共享机制能够正确地恢复对象的图形。反序列化时始终分配新对象,这样可以避免现有对象被重写。

读取对象类似于运行新对象的构造方法。为对象分配内存并将其初始化为零 (NULL)。为不可序列化类调用无参数构造方法,然后从以最接近 java.lang.object 的可序列化类开始和以对象的最特定类结束的流恢复可序列化类的字段。

例如,要从由 ObjectOutputStream 中的示例写入的流读取:
FileInputStream fis = new FileInputStream("t.tmp");
ObjectInputStream ois = new ObjectInputStream(fis); int i = ois.readInt();
String today = (String) ois.readObject();
Date date = (Date) ois.readObject(); ois.close(); 类控制实现 java.io.Serializable 或 java.io.Externalizable 接口时的序列化方式。 实现 Serializable 接口允许对象序列化,以保存和恢复对象的全部状态,并且允许类在写入流时的状态和从流读取时的状态之间变化。它自动遍历对象之间的引用,保存和恢复全部图形。 在序列化和反序列化进程中需要特殊处理的 Serializable 类应该实现以下方法: private void writeObject(java.io.ObjectOutputStream stream)
throws IOException;
private void readObject(java.io.ObjectInputStream stream)
throws IOException, ClassNotFoundException;
private void readObjectNoData()
throws ObjectStreamException; readObject 方法负责使用通过对应的 writeObject 方法写入流的数据,为特定类读取和恢复对象的状态。该方法本身的状态,不管是属于其超类还是属于其子类,都没有关系。恢复状态的方法是,从个别字段的 ObjectInputStream 读取数据并将其分配给对象的适当字段。DataInput 支持读取基本数据类型。 尝试读取由对应的 writeObject 方法写入的超出自定义数据边界的对象数据将导致抛出 OptionalDataException(eof 字段值为 true)。超出已分配数据末尾的非对象读取以指示流末尾的方式反映数据结束:按位读取与字节读取或字节数读取一样,将返回 -1,基元读取将抛出 EOFException。如果不存在对应的 writeObject 方法,则默认的序列化数据的末尾标记已分配数据的末尾。 从 readExternal 方法发出的基元和对象读取调用的行为方式一样:如果流已经定位在由相应 writeExternal 方法写入的数据末尾,则对象读取将抛出 OptionalDataException(其 eof 设置为 true),按位读取将返回 -1,基元读取将抛出 EOFException。注意,此行为不适用于使用旧 ObjectStreamConstants.PROTOCOL_VERSION_1 协议写入的流,在这些流中,没有划分出由 writeExternal 方法写入的数据末尾,因此无法检测。 如果序列化流没有将给定类列为要反序列化的对象的超类,则 readObjectNoData 方法负责初始化其特定类的对象状态。在接收方使用的反序列化实例类的版本不同于发送方,并且接收者版本扩展的类不是发送者版本扩展的类时,此事可能发生。如果序列化流已经被篡改,也会发生这种情况;因此,不管源流是“敌意的”还是不完整的,readObjectNoData 方法都可以用来正确地初始化反序列化的对象。 对于没有实现 java.io.Serializable 接口的任何对象,序列化不会对其字段进行读取或赋值。非 serializable 的 Object 的子类可以为 serializable。在此情况下,非 serializable 类必须具有无参数的构造方法以允许其字段能被初始化。在此情况下,子类负责保存和恢复非 serializable 类的状态。经常出现的情况是,该类的字段是可访问的(public、package 或 protected),或者存在可用于恢复状态的 get 和 set 方法。 反序列化对象进程中发生的所有异常将由 ObjectInputStream 捕获并将中止读取进程。 实现 Externalizable 接口允许对象假定可以完全控制对象的序列化形式的内容和格式。调用 Externalizable 接口的方法(writeExternal 和 readExternal)来保存和恢复对象状态。当这两种方法被某个类实现时,它们可以使用 ObjectOutput 和 ObjectInput 的所有方法读写其本身的状态。对象负责处理出现的任何版本控制。 Enum 常量的反序列化不同于普通的 serializable 或 externalizable 对象。Enum 常量的序列化形式只包含其名称;不传送常量的字段值。要反序列化 enum 常量,ObjectInputStream 需要从流中读取常量的名称;然后将 enum 常量的基本类型和接收到的常量名称作为参数,调用静态方法 Enum.valueOf(Class, String) 获取反序列化的常量。与其他 serializable 或 externalizable 对象一样,enum 常量可以作为序列化流中随后出现的反向引用的目标。不可以自定义 enum 常量的反序列化进程:在反序列化期间,enum 类型所定义的任何与类有关的 readObject、readObjectNoData 和 readResolve 方法都将被忽略。类似地,任何 serialPersistentFields 或 serialVersionUID 字段声明也将被忽略(所有 enum 类型都有一个固定的 0L 的 serialVersionUID)。

一个序列化的例子如下:

import java.io.Serializable;

/**
* Exception in thread "main" java.io.InvalidClassException:
* Person; local class incompatible:
* stream classdesc serialVersionUID = 2354112110978210568,
* local class serialVersionUID = -4033550198229762186
*/
public class Person implements Serializable{ private String name;
private int age;
// int age;//先写入,然后切换属性定义,会抛异常,从异常可以看出,序列化会检查序列化时的UID,这个UID应该和所有成员变量相关
transient int pid;//transient关键字可以定义不被序列化的属性
public Person() {
} public Person(String name, int age) {
this.name = name;
this.age = age;
} @Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
} public int getPid() {
return pid;
} public void setPid(int pid) {
this.pid = pid;
} 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;
}
}
import java.io.*;

public class TestNew {
public static void main(String[] args) throws IOException,ClassNotFoundException {
// write();
read();
} private static void read() throws IOException, ClassNotFoundException {
// 创建反序列化对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
"test.txt"));
Object obj = ois.readObject(); ois.close();
System.out.println(obj);
} private static void write() throws IOException {
// 创建序列化流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
"test.txt"));
Person p = new Person("zhangsan", 17);
oos.writeObject(p);
oos.close();
} }

关于如何解决改变类中的成员变量后读取序列号对象出错的问题,有一个方法就是自定义这个UID:

private final static long serialVersionUID = -4033550198229762186L;