java进阶知识--缓冲流、转换流、序列化流、打印流

上篇介绍了IO基础概念,IO流的基类、文件流等。此篇将介绍一些更强大的流,比如能够高效读写的缓冲流、能够转换编码的转换流、能够持久化存储对象的序列化流等等。

一、缓冲流

 1.1 概述

    缓冲流,也叫高效流。它的基本原理是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

    按数据类型分类:

    • 字节缓冲流BufferedInputStreamBufferedOutputStream

    • 字符缓冲流BufferedReaderBufferedWriter

 1.2 字节缓冲流

   构造方法

    • public BufferedInputStream(InputStream in) :创建一个新的缓冲输入流。

    • public BufferedOutputStream(OutputStream out): 创建一个新的缓冲输出流。

  1.3 字符缓冲流

   构造方法

    • public BufferedReader(Reader in) :创建一个新的缓冲输入流。

    • public BufferedWriter(Writer out): 创建一个新的缓冲输出流。

   特有方法

    字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。

    • BufferedReader:public String readLine(): 读取一个文本行。 

public class BufferedReaderDemo {
    public static void main(String[] args) throws IOException {
      	 // 创建流对象
        BufferedReader br = new BufferedReader(new FileReader("in.txt"));
		// 定义字符串,保存读取的一行文字
        String line  = null;
      	// 循环读取,读取到最后返回null
        while ((line = br.readLine()) != null) {
            System.out.print(line);
            System.out.println("------");
        }
		// 释放资源
        br.close();
    }
}
    • BufferedWriter:public void newLine(): 写入一个行分隔符。行分隔符字符串由系统属性 line.separator 定义,并且不一定是单个新行 (' ') 符。

public class BufferedWriterDemo throws IOException {
    public static void main(String[] args) throws IOException  {
      	// 创建流对象
		BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));
      	// 写出数据
        bw.write("你好啊");
      	// 写出换行
        bw.newLine();
        bw.write("老铁");
        bw.newLine();
		// 释放资源
        bw.close();
    }
}

二、转换流

 2.1 字符编码和字符集

   2.1.1 字符编码 Character Encoding

    概述:就是一套自然语言的字符与二进制数之间的对应规则

    •  编码:按照某种规则,将字符转换成二进制数存储到计算机中。字符到字节的过程。
    •  解码:将存储在计算机中的二进制数按照某种规则显示出来。字节到字符的过程。

   2.1.2 字符集 Charset

    概述:字符集也叫编码表,是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。

 编码表:生活中文字和计算机中二进制的对应规则。

    计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。

    可见,当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。

  • ASCII字符集

    • ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。

    • 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。

  • ISO-8859-1字符集

    • 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。

    • ISO-8859-1使用单字节编码,兼容ASCII编码。

  • GBxxx字符集

    • GB就是国标的意思,是为了显示中文而设计的一套字符集。

    • GB2312:简体中文码表。一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。

    • GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。

    • GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。

  • Unicode字符集

    • Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码。

    • 最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。

    • UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则:

      1. 128个US-ASCII字符,只需一个字节编码。

      2. 拉丁文等字符,需要二个字节编码。

      3. 大部分常用字(含中文),使用三个字节编码。

      4. 其他极少使用的Unicode辅助字符,使用四字节编码。

 2.2 InputStreamReader类

   2.2.1 概述

    转换流java.io.InputStreamReader,是Reader的子类,是FileReader的父类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

   2.2.2 构造方法

    • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。

    • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。

 2.3 OutputStreamWriter类

   2.3.1 概述

    转换流java.io.OutputStreamWriter ,是Writer的子类,是FileWriter的父类,是从字符流到字节流的桥梁。使用指定的字符集将其编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

   2.3.2 构造方法

    • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。

    • OutputStreamWriter(OutputStream in, String charsetName): 创建一个指定字符集的字符流。

三、序列化流

 3.1 概述

    Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据对象的类型对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化对象的数据对象的类型对象中存储的数据信息,都可以用来在内存中创建对象。

    序列化把对象转换为字节序列,写入到文件中的过程称为对象的序列化,也叫写对象。也就是将对象写入到IO流中,是一种用来处理对象流的机制。(所谓对象流也就是将对象的内容进行流化。)

    序列化的作用:是为了解决在对对象流进行读写操作时所引发的问题。序列化机制允许将实现序列化的Java对象转换为字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。

    实现序列化的步骤

      1. 相关对象实现Serializable接口;

      2. 创建一个ObjectOutputStream输出流;

      3. 调用ObjectOutputStream对象的writeObject(Object obj)方法输出可序列化对象。

实现对象序列化需要满足的条件以及注意点:

  • 要对一个对象序列化,这个对象就需要实现Serializable接口,如果这个对象中有一个变量是另一个对象的引用,则引用的对象也要实现Serializable接口,这个过程是递归的。Serializable接口中没有定义任何方法,只是作为一个标记来指示实现该接口的类可以进行序列化。
  • 该类必须实现java.io.Serializable 接口,Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException

  • 序列化只能保存对象的非静态成员变量,而不能保存任何成员方法和静态成员变量,并且保存的只是变量的值,变量的修饰符对序列化没有影响。
  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化,或者因为其数据的特性决定了它会经常变化而不具有可持久性,则该属性必须注明是瞬态的,使用transient 关键字修饰,否则编译器将报错。任何用 transient 关键字标明的成员变量,都不会被保存。

  • 序列化可能涉及将对象存放到磁盘上或在网络上发送数据,这时会产生安全问题。对于一些需要保密的数据(如用户密码等),不应保存在永久介质中,为了保证安全,应在这些变量前也加上 transient 关键字。

    反序列化将序列化过程中所生成的字节序列,读取出来转换成对象的过程成为对象的反序列化,也叫读对象。

    实现反序列化的步骤

      1. 创建一个ObjectInputStream输入流;

      2. 调用ObjectInputStream对象的readObject()方法得到序列化对象。

 注意:反序列化的对象是由 JVM 自己生成的,不通过构造方法生成。

 3.2 ObjectOutputStream类

    java.io.ObjectOutputStream 序列化流,将Java对象的原始数据类型写出到文件,实现对象的持久存储。

   构造方法

    • public ObjectOutputStream(OutputStream out): 创建一个指定输出流的序列化流。

   写出对象方法

    • public final void writeObject(Object obj) : 将指定的对象写出。

 3.3 ObjectInputStream类

    ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。

   构造方法

    • public ObjectInputStream(InputStream in): 创建一个指定InputStream的ObjectInputStream。

   读取对象方法

    • public final Object readObject() : 读取一个对象。

 注意:1. 对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException 异常。

    2. 当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常(序列化冲突异常)。

      发生InvalidClassException异常的原因如下:

      • 该类的序列版本号与从流中读取的类描述符的版本号不匹配

      • 该类包含未知数据类型

      • 该类没有可访问的无参数构造方法

      如何解决InvalidClassException异常,来保证对象前后的兼容性?

      Serializable 接口给需要序列化的类,提供了一个序列版本号。serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。

 3.4 序列化版本号serialVersionUID

    java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。

 注意:序列化版本号可自由指定。

    如果不指定

      ① JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;

      ② 不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。

    什么情况下需要修改serialVersionUID呢?

    • 如果只是修改了方法,反序列化不受影响,则无需修改版本号;
    • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
    • 如果修改了非瞬态变量,或者如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

    序列化应用场景与注意点

    • 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
    • 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
    • 如果想让某个变量不被序列化,使用transient修饰。
    • 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
    • 反序列化时必须有序列化对象的class文件。
    • 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
    • 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
    • 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
    • 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。

小贴士:序列化流和反序列化流不同于字节流、字符流等,后者是把字符串写入/读取文件,前者可以将对象写入/读取文本文件中。

四、打印流(了解)

 4.1 概述

    平时我们在控制台打印输出,是调用print方法和println方法完成的,这两个方法都来自于java.io.PrintStream类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。

 4.2 PrintStream类

   构造方法

    • public PrintStream(String fileName): 使用指定的文件名创建一个新的打印流。

   改变打印流向

     System.out就是PrintStream类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以改变它的流向。

     方法:setOut(PrintStream out);  //改变输出的目的地

     比如,将设置系统的打印流流向,内容打印到文本文件中:

public class PrintDemo {
    public static void main(String[] args) throws IOException {
		// 调用系统的打印流,控制台直接输出97
        System.out.println(97);
      
		// 创建打印流,指定文件的名称
        PrintStream ps = new PrintStream("ps.txt");
      	
      	// 设置系统的打印流流向,输出到ps.txt
        System.setOut(ps);
      	// 调用系统的打印流,ps.txt中输出97
        System.out.println(97);
    }
}

  

原文地址:https://www.cnblogs.com/sun9/p/13527553.html