什么是I/OI/O 是Input/Output的缩写翻译为 “输入 / 输出”是 Java 程序与外部设备如文件、网络、控制台、内存等进行数据交互的机制。输入Input数据从外部设备进入程序比如读取文件内容到程序输出Output数据从程序发送到外部设备比如把程序中的数据写入文件其实简单来说能够支持这些操作的设备就叫I/O设备同时Java给我们提供了许多IO操作的框架可以直接使用。Java I/O 的核心分类按数据类型分字节流以 “字节byte” 为单位处理数据1 字节 8 位适用于所有类型的文件如图片、视频、二进制文件、文本文件核心抽象类是InputStream所有字节输入流的父类读数据OutputStream所有字节输出流的父类写数据。字符流以 “字符char” 为单位处理数据基于字节流自动处理编码如 UTF-8、GBK仅适用于文本文件核心抽象类是Writer所有字符输出流的父类写文本。Reader所有字符输入流的父类读文本按流向分输入流读数据程序 ← 外部设备输出流写数据程序 → 外部设备。、文件字节流了解文件字节流首先我们先了解可以用FileInputStream来获取文件的输入流也就是读取文件FileInputStream stream new FileInputStream(D://develop/aaa.txt);这里引号里书写你想要读取的文件路径这里的路径也分为绝对路径和相对路径像上面书写的以盘符开头的路径即为绝对路径但是这里要注意如果直接复制文件路径的话有可能是以“\”隔开的但是这时候我们需要手动改成“\\”或者“/”因为在Java中“\”是转义符!相对路径既是以当前文件出发来寻找想要读取的文件比如FileInputStream stream new FileInputStream(_22_IO/aaa);相对路径不需要表明盘符比如“.”表示本级文件“../”表示上一级“../../”表示上上一级以此类推...但是在书写路径时尽量避免出现中文容易出现编译错误。而且Windows系统和Mac OS系统在路径上也有差异。但是有因为IO的相关操作比较复杂比如路径不存在网络延迟等问题我们在使用IO的时候需要提前对异常进行处理public static void main(String[] args) { try { FileInputStream stream new FileInputStream(_22_IO/aaa); } catch (FileNotFoundException e) { e.printStackTrace(); } }但是当我们使用一个IO流之后需要将该流关闭否则被读取的文件资源会一直被占用其他软件就无法打开建议在finally中关闭流因为释放资源是必须要做的public static void main(String[] args) { FileInputStream stream null; try { stream new FileInputStream(D:/develop/aaa.txt); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { try { if (stream ! null) stream.close(); System.out.println(aaa); } catch (IOException e) { e.printStackTrace(); } } }但是我们会发现这样写代码会使代码很繁琐所有在JDK1.7之后新增了try-with-resource语法用来简化写法本质与上面的操作一致public static void main(String[] args) { try (FileInputStream stream new FileInputStream(D:/develop/aaa.txt)){ //直接在try()中定义要在完成之后释放的资源 } catch (IOException e) { //这里要用IOException是因为调用close()可能会出去IOException是一个父类 e.printStackTrace(); } //这里就不需要写finally语句因为在最后自动帮我们调用了close() }这里要注意定义的资源要放进括号里catch中要用IOException以后都会使用这个其实不只是IO可以用只要语法支持实现AutoCloseable的接口类都可以这样使用。读取文件内容输入流当我们想要读取文件里的内容的时候就要使用read方法public static void main(String[] args) { try (FileInputStream stream new FileInputStream(D:/develop/aaa.txt)){ //使用read方法来读取文件中的字节 System.out.println((char)stream.read()); //读取一个字节的数据这里正常需要是英文中文会有错 } catch (IOException e) { } }这里是如果不强制数据类型的话会输出该字符所对应的ask值我们想要看到正确的字符就需要进行char强转一下。这里可以利用循环来获取所有的字符但是当字符读取完之后在进行循环则会获得空不强转也就会显示”-1“public static void main(String[] args) { try (FileInputStream stream new FileInputStream(D:/develop/aaa.txt)){ //使用read方法来读取文件中的字节 //文件内容为abcdef for (int i 0; i 7; i) { System.out.print(stream.read() ); } //读取一个字节的数据这里正常需要是英文中文会有错 } catch (IOException e) { } }运行结果如下这里要注意只要使用一次stream.read()读取的字符就会往后一位如果一次循环内多次使用就会出现public static void main(String[] args) { try (FileInputStream stream new FileInputStream(D:/develop/aaa.txt)){ //使用read方法来读取文件中的字节 //文件内容为abcdef for (int i 0; i 7; i) { System.out.print((char) stream.read() : stream.read() ); } //读取一个字节的数据这里正常需要是英文中文会有错 } catch (IOException e) { } }我们会发现运行出的字符和ask值是对应不上的所有在运用的时候要格外注意一下又因为我们已经知道了当文件读取完的时候会返回”-1“所有我们就可以使用while循环来完成读取所有内容public static void main(String[] args) { try (FileInputStream stream new FileInputStream(D:/develop/aaa.txt)){ //使用read方法来读取文件中的字节 //文件内容为abcdef int temp; while ((temp stream.read()) ! -1) { System.out.print((char) temp ); } //读取一个字节的数据这里正常需要是英文中文会有错 } catch (IOException e) { } }运行结果为使用available方法可以查看当前可读的剩余字节数量这里并不一定真实的数据就是这么多尤其是在网络IO操作时当然在磁盘IO下一般就是真实的数据量public static void main(String[] args) { try (FileInputStream stream new FileInputStream(D:/develop/aaa.txt)) { System.out.println(stream.available()); }catch (IOException e) { e.printStackTrace(); } }当然一个一个循环读取的话效率太低所以我们可以先将预制一个与文件字符数一致的数组来存放public static void main(String[] args) { try (FileInputStream stream new FileInputStream(D:/develop/aaa.txt)) { byte[] bytes new byte[stream.available()]; System.out.println(stream.read(bytes));//这里返回读了多少字符文件有多少字节就读多少不会多读 System.out.println(new String(bytes));//这里打印出字符 }catch (IOException e) { e.printStackTrace(); } }打印结果为也可以控制读取的数量System.out.println(stream.read(bytes, 1, 2));//这里是指从1索引读的长度为2通过skip()方法可以跳过指定数量的字节返回值为实际跳过的字节public static void main(String[] args) { try (FileInputStream stream new FileInputStream(D:/develop/aaa.txt)) { byte[] bytes new byte[stream.available()]; stream.skip(1); System.out.println(stream.read(bytes)); System.out.println(new String(bytes));//这里打印出字符 }catch (IOException e) { e.printStackTrace(); } }写入文件内容输出流我们已经有了读取文件的能力那么也同样要能去改写文件的内容这时候我们就需要用到FileOutputStreampublic static void main(String[] args) { //输出流也需要在最后调用close()方法并且支持try-with-resource try (FileOutputStream stream new FileOutputStream(D:/develop/aaa.txt)){ //注意当该路径不存在时会直接新建一个该路径文件来存储 }catch (IOException e) { e.printStackTrace(); } }我们知道输入流使用的read()来读取字节但是在输出流中我们使用write()来像文件里写内容public static void main(String[] args) { //输出流也需要在最后调用close()方法并且支持try-with-resource try (FileOutputStream stream new FileOutputStream(D:/develop/aaa.txt)){ //注意当该路径不存在时会直接新建一个该路径文件来存储 stream.write(h);//同read一样可以直接写入内容 stream.write(abcd.getBytes()); //也可以直接写入byte[] stream.write(abcd.getBytes(), 0, 2); //写入从0索引开始长度为2 stream.flush(); //建议最后执行一次刷新操作强行写入确保数据正确写入 }catch (IOException e) { e.printStackTrace(); } }这里要注意使用write的时候是直接覆盖原文件重写的并且建议要养成好习惯在最后执行一次flush(),来提升程序性能。但是如果我们只是想在文件结尾追加一些内容呢write肯定就是不能用的了这时候我们可以使用另一种方式来追加public static void main(String[] args) { try (FileOutputStream stream new FileOutputStream(D:/develop/aaa.txt, true)){ stream.write(h); }catch (IOException e) { e.printStackTrace(); } }其实也就是在路径后面添加一个”true“这里就不会替换原文件之后追加内容。好了现在我们基本的读写已经会了那么我们就可以进行文件的拷贝了public static void main(String[] args) { try (FileInputStream in new FileInputStream(D:/develop/aaa.txt); FileOutputStream out new FileOutputStream(D:/develop/bbb.txt)){ //可以写入多个 byte[] arr new byte[1024 * 1024]; //创建一个数组来处理输入流 int temp; //记录读取资源的有效长度 while ((temp in.read(arr)) ! -1) { //确保每个字节都进行了读取 out.write(arr, 0 , temp); //将每个有效字节写入 } }catch (IOException e) { e.printStackTrace(); } }这里如果所需拷贝的文件过大的话可以让预处理数组更大一些这里1024*1024也就是一兆但是并不是越大越好能满足当前需求即可按照实际情况而定。文件字符流字符流不同于字节流字符流是以一个具体的字符进行读取的因此他只适合纯文本文件如果是其他类型的文件就不适用。读的时候用FileReaderpublic static void main(String[] args) { try (FileReader reader new FileReader(D:/develop/aaa.txt)){ reader.skip(1); //跳过的是一个字符 System.out.println((char)reader.read()); } catch (Exception e) { e.printStackTrace(); } }同理字符流也可以用char[]来存储。public static void main(String[] args) { try (FileReader reader new FileReader(D:/develop/aaa.txt)){ char[] str new char[10]; reader.read(str); System.out.println(str);//直接读取到char[]中 } catch (Exception e) { e.printStackTrace(); } }同理有了read也同样有write这时候就要使用FileWriterpublic static void main(String[] args) { try (FileWriter writer new FileWriter(D:/develop/aaa.txt)){ writer.write(asdasdasd); } catch (Exception e) { e.printStackTrace(); } }这里比字节流更简便在输入的时候不需要再强调输入的类型没有这么多限制。这里也可以不用write()方法append()方法也和他效果也一样。但是要注意这里和字节流一样默认是直接覆盖原文而不是追加如果想要追加文本的话同样需要再路径后面将添加”true“也就是将append改成”true“public static void main(String[] args) { try (FileWriter writer new FileWriter(D:/develop/aaa.txt, true)){ writer.append(asdasdasd); writer.flush(); //刷新 } catch (Exception e) { e.printStackTrace(); } }注意这里无论是编译文件还是读取文件都要是UTF-8才可以编译中文否则中文乱码这里的append也同样可以链式应用public static void main(String[] args) { try (FileWriter writer new FileWriter(D:/develop/aaa.txt, true)){ writer.append(asdasdasd).append(1234); System.out.println(writer.getEncoding()); writer.flush(); //刷新 } catch (Exception e) { e.printStackTrace(); } }到这里我们也同样就可以进行纯文本文件的拷贝public static void main(String[] args) { try (FileReader reader new FileReader(D:/develop/aaa.txt); FileWriter writer new FileWriter(D:/develop/bbb.txt)){ char[] str new char[10]; int temp; while ((temp reader.read(str)) ! -1) { writer.write(str); writer.flush(); } } catch (IOException e) { e.printStackTrace(); } }这里我们还有File类它专门用于表示一个文件或者文件夹但并不是这个文件本身通过File可以更好的管理和操作硬盘上的文件public static void main(String[] args) throws IOException { File file new File(D:/develop); System.out.println(file.exists()); //判断文件是否存在 System.out.println(file.canRead()); //查看文件是否可读 System.out.println(file.canWrite()); //查看文件是否可写 System.out.println(file.isHidden()); //查看文件是否隐藏 System.out.println(file.isFile()); //查看文件是否是纯文本文件 System.out.println(file.isDirectory());//查看文件是否为文件夹 System.out.println(file.canExecute()); //是否可执行 System.out.println(Arrays.toString(file.list())); //查看当前目录下是文件 file.createNewFile(); //创建新文件 file.delete(); //删除文件 file.mkdir(); //创建目录, 比如test file.mkdirs(); //创建一连串文件比如创建“test/xxx/xxx” }如果我们希望读取某个文件的内容可以直接将File作为参数传入字节流或者是字符流public static void main(String[] args) throws IOException { File file new File(D:/develop/aaa.txt); try (FileReader reader new FileReader(file)){ System.out.println(reader.read()); } catch (IOException e) { e.printStackTrace(); } }这里我们就可以尝试将一个文件夹拷贝到另一个文件夹File工具类Files类是java.nio.file包中的一个实用类在Java7中推出提供了许多静态方法用于文件和目录整理的工作。public static void main(String[] args) throws IOException { Files.createDirectory(Path.of(aaa)); //新建目录 Files.createDirectories(Path.of(test/xxx/xxx)); //新建一系列目录 }这里Files给出了方便的方法来新建目录但是该工具类要与Path一起使用用Path来指定文件路径可以直接通过of方法来创建。同时Path也有一些方法public static void main(String[] args) { Path path Path.of(aaa); System.out.println(path.toAbsolutePath()); //相对路径转成绝对路径 System.out.println(path.toAbsolutePath().getParent()); //获取父路径 }利用该类型表示路径可以给我们带来更多的便利对于删除文件也是很方便的public static void main(String[] args) throws IOException { Path path Path.of(aaa); Files.delete(path); //删除没有文件就报错 Files.deleteIfExists(path); //删除没有文件返回false否则返回true }Files中还有许多有用的工具类public static void main(String[] args) throws IOException { Path path Path.of(aaa.txt); Files.readString(path); //Java 11新增读取所有内容并以字符串返回 ListString lines Files.readAllLines(path); //一键读取所有文件内容并按行分割返回 StreamString linesStream Files.lines(path); //同上以Java 8之后的Stream形式返回 Files.write(path, HelloWorld.getBytes()); //一键写入内容 Files.write(path, HelloWorld.getBytes(), StandardOpenOption.WRITE); //覆盖相同位置 Files.write(path, HelloWorld.getBytes(), StandardOpenOption.APPEND); //续写 Files.writeString(path, HelloWorld); //Java 11新增更快捷的用字符串一键写入 Files.copy(path, Path.of(another)); //拷贝文件到另一个路径 Files.move(path, Path.of(another)); //移动文件到另一个路径 Files.exists(path); //判断文件是否存在 Files.notExists(path); //判断文件是否不存在 Files.isExecutable(path); //文件是否可执行 Files.isDirectory(path); //文件是否是文件夹 Files.isWritable(path); //文件是否可写 Files.isReadable(path); //文件是否可读 Files.isHidden(path); //文件是否为隐藏文件 Files.newInputStream(path); //创建新的文件输入流 Files.newOutputStream(path); //创建新的文件输出流 }这个工具类中还有许多好用的API比如当你想要查找一个文件时就可以//其中第一个参数为起始查找点第二参数为最大查找深度最后就是一个断言函数式就是判断每一个文件是否符合我们要查找的要求 Files.find(Path.of(.), 4, (path, attributes) - { if (path.getFileName().toString().equals(Main.java)) { return true; } else { return false; } }).forEach(path - { //find的结果会以Stream的形式返回 System.out.println(path); });遍历指定类型的文件可以使用walk方法Files.walk(Path.of(.)) //返回StreamPath .filter(Files::isRegularFile) //filter过滤判断是否为普通文件 .forEach(System.out::println); //打印缓冲流我们正常的IO操作的底层原理是每次都需要从硬盘中取出文件来读取当数据量少的时候我们并不会感觉到什么但是当数据量过大的时候我们会发现程序可能或发生卡顿因为硬盘的速度跟不上内存的读取速度这时候如果我们想让程序更流畅就可以用到缓冲流其实简单来说就是先暂时将硬盘里的数据存入到内存中当我们使用这个数据的时候直接从内存中取并且缓冲流的底层原理和IO还是一样的使用的流程也差不多public static void main(String[] args) { try (BufferedInputStream stream new BufferedInputStream(new FileInputStream(aaa.txt))) { System.out.println(stream.read()); } catch (IOException e) { e.printStackTrace(); } }这里的主要区别就是先将IO文件的信息存入BufferedInputStream也就是将硬盘文件存入内存。效果是一样的只是底层多了存进内存这一步。我们知道IO读取文件都是一次性的所以我们没办法对已读取的信息回退但是由于我们的缓存流是提前存入内存的所以我们也就可以回退信息这时候我们就需要使用mark()和reset()方法当调用reset后会使当前读取的位置回到mark调用时的位置public static void main(String[] args) { try (BufferedInputStream stream new BufferedInputStream(new FileInputStream(src/_22_IO/aaa))) { stream.mark(1); System.out.print((char) stream.read()); System.out.print((char) stream.read()); System.out.print((char) stream.read()); stream.reset(); System.out.print((char) stream.read()); } catch (IOException e) { e.printStackTrace(); } }这里我们用mark记录的初始位置当运行到reset的时候读取回到了mark我们该文件的内容是“hello”但是看我们的运行结果但是这里我们要注意mark中传入的数据要大于缓冲流存储的数据量我们的缓冲流默认量是8192当我们的值小于存储数据量时他就会自动取最大值现在是默认也就是8192所以说当我们自己设置数据量为2时如果还输入1我们的最大值也就是2这时候当我们在读取三个read时mark也就自动失效了我们的reset()也就不可以用了程序就会报错public static void main(String[] args) { try (BufferedInputStream stream new BufferedInputStream(new FileInputStream(src/_22_IO/aaa), 2)) { stream.mark(1); System.out.print((char) stream.read()); System.out.print((char) stream.read()); System.out.print((char) stream.read()); stream.reset(); System.out.print((char) stream.read()); } catch (IOException e) { e.printStackTrace(); } }我们可以看到前三个读取是正常的但是运行到reset()时就报错了。了解完BufferedInputStream后我们还要学习BufferedOutputStream其实和BufferedInputStream的原理一样只是反向操作public static void main(String[] args) { try (BufferedOutputStream stream new BufferedOutputStream(new FileOutputStream(aaa.txt, true))) { stream.write(bbb.getBytes(StandardCharsets.UTF_8)); stream.flush(); } catch (IOException e) { e.printStackTrace(); } }上面我们讲的是缓冲字节流当然缓冲字符流也是一样的也有专门的缓冲区BufferedReader构造时需要传入Reader对象public static void main(String[] args) { try (BufferedReader reader new BufferedReader(new FileReader(aaa.txt))){ reader. lines() .filter(str - str.charAt(0) z str.charAt(0) a) .forEach(System.out::println); } catch (IOException e) { e.printStackTrace(); } }这里我们可以看到当我们使用lines()方法时他返回的时一个stream流可以更方便我们的运算用法上和BufferedInputStream一致。当然BufferedWriter也是一样的public static void main(String[] args) { try (BufferedWriter writer new BufferedWriter(new FileWriter(aaa.txt))){ writer.write(hello); writer.newLine(); writer.write(hello); writer.flush(); } catch (IOException e) { e.printStackTrace(); } }这里的newLine方法是进行了换行。转换流有时候会遇到一些很麻烦的问题我们在这里读取的是一个字符串或是一个个字符但是我只能往一个OutputStream里输出但是OutputStream又只支持byte类型如果我们想往里面写入内容进行类型转换就会很麻烦这时我们就会使用到转换流这里写的话就是用转换流OutputStreamWriterpublic static void main(String[] args) { try(OutputStreamWriter stream new OutputStreamWriter(new FileOutputStream(D:/develop/aaa.txt, true))){ stream.write(aaaaaabbba); } catch (IOException e) { e.printStackTrace(); } }同样我们拿到了一个InputStream但是我们希望能够按照字符的方式读取我们就可以使用InputStreamReader来帮我们实现public static void main(String[] args) { try (InputStreamReader stream new InputStreamReader(new FileInputStream(D:/develop/aaa.txt))){ System.out.println((char) stream.read()); }catch (IOException e){ e.printStackTrace(); } }这里我们想要读取一行的话也可以再套一个BufferedReaderpublic static void main(String[] args) { try (BufferedReader stream new BufferedReader(new InputStreamReader(new FileInputStream(D:/develop/aaa.txt))) ){ System.out.println((char) stream.read()); System.out.println(stream.readLine()); }catch (IOException e){ e.printStackTrace(); } }InputStreamReader和OutputStreamWriter本质也是Reader和Writer因此可以放入BufferedReader来实现更方便的操作。打印流打印流其实已经是我们的老熟人了我们平时正常的输出System.out就是一个输出流也就是PrintStreamPrintStream也继承FilerOutputStream类但是它存在自动刷新机制例如当向PrintStream流中写入一个字节数组后自动调用flush()方法。public static void main(String[] args) { try (PrintStream stream new PrintStream(new FileOutputStream(D:/develop/aaa.txt))){ stream.printf(hello world); }catch (IOException e){ e.printStackTrace(); } }像这里我们就是将hello world打印到文件里而不是打印在控制台。我们平时用的println方法就是PrintStream中的方法它会直接打印基本数据类型或是调用对象的toString()方法得到一个字符串并将字符串转化为字符放入缓冲区在经过转换流输出到给定的输出流上。这里的输出其实不止有printlnprint是不换行打印printf其实和c语言的输出差不多这里不多解释了。我们之前使用的Scanner使用的是系统提供的输入流我们也可以使用它来扫描其他的输入流public static void main(String[] args) { try (Scanner scanner new Scanner(new FileInputStream(D:/develop/aaa.txt))){ System.out.println(scanner.nextLine()); } catch (IOException e){ e.printStackTrace(); } }这里要注意打印的时候要调用Scanner的读取方法否则不会读取文件内容数据流数据流DataInputStream也是FilterInputStream的子类同样是采用装饰者模式最大的不同就是它支持基本数据类型的直接读取public static void main(String[] args) { try (DataInputStream stream new DataInputStream(new FileInputStream(D:/develop/aaa.txt))){ System.out.println(stream.readBoolean()); //直接将数据读取为任意基本数据类型 } catch (IOException e) { e.printStackTrace(); } }用于写入基本数据类型public static void main(String[] args) { try (DataOutputStream outputStream new DataOutputStream(new FileOutputStream(D:/develop/aaa.txt))){ outputStream.writeBoolean(true); } catch (IOException e) { e.printStackTrace(); } }注意这里写入的是二进制数据并不是写入字符串使用DataInputStream可以读取但是基本数据类型能够转换否侧就会public static void main(String[] args) { try (DataOutputStream outputStream new DataOutputStream(new FileOutputStream(D:/develop/aaa.txt)); DataInputStream inputStream new DataInputStream(new FileInputStream(D:/develop/aaa.txt))){ outputStream.writeBoolean(true); //写入布尔类型 System.out.println(inputStream.readInt()); //读取整形 } catch (IOException e) { e.printStackTrace(); } }对象流这里就是将我们原有的或者是自己创建的一些对象类通过输入输出流来运行这里就会使用ObjectOutputStream和ObjectInputStream它不仅支持基本数据类型而且通过对对象的的序列化以某种格式保存对象来支持对象类型的IO注意它不是继承自FilerInputStream的。public static void main(String[] args) { try (ObjectOutputStream outputStream new ObjectOutputStream(new FileOutputStream(D:/develop/aaa.txt)); ObjectInputStream inputStream new ObjectInputStream(new FileInputStream(D:/develop/aaa.txt))){ student stu new student(zhang); outputStream.writeObject(stu); System.out.println(inputStream.readObject()); }catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } static class student implements Serializable{ //这里必须要继承Serializable来让对象实现序列化 private String name; public student() { } public student(String name) { this.name name; } public String getName() { return name; } public void setName(String name) { this.name name; } public String toString() { return student{name name }; } }这里我们要注意要用到对象类要继承Serializable类否则会报错在后续操作中有可能会使这个类发生一些结构化的变化而原来保存的数据只适用于之前版本的这个类因此我们需要一种方法来区分类的不同版本static class student implements Serializable{ //这里必须要继承Serializable来让对象实现序列化 private static final long serialVersionUID 123456; //在序列化时会被添加这个属性代表当前版本 private String name; public student() { } public student(String name) { this.name name; } public String getName() { return name; } public void setName(String name) { this.name name; } public String toString() { return student{name name }; } }这里其实也就相当于给类定义了一下版本每次版本新更新一些东西旧版的软件就没法用。当我们运行时发现版本不匹配就会报错无法反序列化为对象如果我们不希望一些属性参与到序列化中进行保存我们可以添加transiect关键字static class student implements Serializable{ private static final long serialVersionUID 123456; //在序列化时会被添加这个属性代表当前版本 private String name; transient private int age; Override public String toString() { return student{ name name \ , age age }; } }这里我的理解是比如游戏的更新有一些是下个版本更新的东西这个版本用不到就暂时给他添加这个关键字。如果你强行读它的话就会返回这个类型的默认值。这里其实在一些JDK内部的源码中也存在大量的transient关键字使得某些属性不参与序列化取消这些不必要保存到属性可以节省数据空间占用以及减少序列化时间。