【JavaSE】IO流

一、IO流技术


输入(Input)指的是:可以让程序从外部系统获得数据(核心含义是“读”,读取外部数据)。

输出(Output)指的是:程序输出数据给外部系统从而可以操作外部系统(核心含义是“写”,将数据写出到外部系统)。

java.io包为我们提供了相关的API,实现了对所有外部系统的输入输出操作,这就是我们这章所要学习的技术。

数据源

数据源data source,提供数据的原始媒介。常见的数据源有:数据库、文件、其他程序、内存、网络连接、IO设备:

数据源分为:

  1. 源设备:为程序提供数据,一般对应输入流;
  2. 目标设备:程序数据的目的地,一般对应输出流;

流是一个抽象、动态的概念,是一连串连续动态的数据集合

对于输入流而言,数据源就像水箱,流(stream)就像水管中流动着的水流,程序就是我们最终的用户。我们通过流(A Stream)将数据源(Source)中的数据(information)输送到程序(Program)中。

对于输出流而言,目标数据源就是目的地(dest),我们通过流(A Stream)将程序(Program)中的数据(information)输送到目的数据源(dest)中。

流与源数据源和目标数据源之间的关系:

1.1 两种程序写法


try-catch-finally

/**
 * IO程序的经典写法
 */
public class Test01 {
    public static void main(String[] args) {
        FileInputStream fis = null; // 作用域为全局
        StringBuilder sb = new StringBuilder(); // 用于字符串拼接
        try{
            fis = new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\a.txt");
            int temp = 0;
            // 当temp等于-1时,表示已经到了文件结尾,停止读取
            while( (temp = fis.read()) != -1){
                sb.append( (char)temp );
            }
            System.out.println(sb);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
                try {
                    // 保证了即使遇到异常情况,也会关闭流对象
                    if( fis != null) {
                        fis.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
}

// 输出结果
kk 123456 sgsg

try-with-resource

在JDK7以及以后的版本中可以使用try-with-resource语法更优雅的关闭资源。

java.lang.AutoCloseable接口:

java.lang.AutoCloseable接口中包含了一个close方法,该方法用于关闭资源。

只要是实现了java.lang.AutoCloseable接口的对象,都可以使用try-with-resource关闭资源,使用最新的try-with-resource简化:

/**
 * try-with-resource写法
 */
public class Test02 {
    public static void main(String[] args) {

        try (FileInputStream fis = new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\a.txt")) {
            StringBuilder sb = new StringBuilder();
            int temp = 0;
            while ((temp = fis.read()) != -1) {
                sb.append((char) temp);
            }
            System.out.println(sb);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

1.2 流概念的细分与体系


1.2.1 流的细分

按流的方向分类:

  • 输入流:数据流向是数据源到程序(以InputStream、Reader结尾的流);
  • 输出流:数据流向是程序到目的地(以OutPutStream、Writer结尾的流);

按处理的数据单元分类:

  • 字节流:以字节为单位获取数据,命名上以Stream结尾的流一般是字节流,如FileInputStream、FileOutputStream;

  • 字符流:以字符为单位获取数据,命名上以Reader/Writer结尾的流一般是字符流,如FileReader、FileWriter;

按处理对象不同分类:

  • 节点流可以直接从数据源或目的地读写数据,如FileInputStream、FileReader、DataInputStream等;
  • 处理流不直接连接到数据源或目的地,是”处理流的流”。通过对其他流的处理提高程序的性能,如BufferedInputStream、BufferedReader等。处理流也叫包装流;

节点流处于IO操作的第一线,所有操作必须通过它们进行;处理流可以对节点流进行包装,提高性能或提高程序的灵活性。

1.2.2 流的体系

Java为我们提供了多种多样的IO流,我们可以根据不同的功能及性能要求挑选合适的IO流,如图所示,为Java中IO流类的体系。

抽象类

  1. InputStream / OutputStream:字节流的抽象类;
  2. Reader / Writer:字符流的抽象类;

节点流

  1. FileInputStream / FileOutputStream:节点流,以字节为单位直接操作“文件”;
  2. ByteArrayInputStream / ByteArrayOutputStream:节点流,以字节为单位直接操作“字节数组对象”;
  3. FileReader/FileWriter:节点流,以字符为单位直接操作“文本文件”(注意:只能读写文本文件);

处理流

  1. ObjectInputStream / ObjectOutputStream:处理流,以字节为单位直接操作“对象”;
  2. DataInputStream / DataOutputStream:处理流,以字节为单位直接操作“基本数据类型与字符串类型”;
  3. BufferedReader / BufferedWriter:处理流,将Reader/Writer对象进行包装,增加缓存功能,提高读写效率;
  4. BufferedInputStream / BufferedOutputStream:处理流,将InputStream/OutputStream对象进行包装,增加缓存功能,提高读写效率;
  5. InputStreamReader / OutputStreamWriter:处理流,转换流,将字节流对象转化成字符流对象;
  6. PrintStream:处理流,将OutputStream进行包装,可以方便地输出字符,更加灵活;

1.3 IO中四大抽象类


InputStream / OutputStreamReader / writer类是所有IO流类的抽象父类。

InputStream

此抽象类是表示字节输入流的所有类的父类。InputSteam是一个抽象类,它不可以实例化。 数据的读取需要由它的子类来实现。根据节点的不同,它派生了不同的节点流子类 。

继承自InputSteam的流都是用于向程序中输入数据,且数据的单位为字节(8 bit)。

常用方法:

方法名

使用说明

int read()

读取一个字节的数据,并将字节的值作为int类型返回(0-255之间的一个值)。如果未读出字节则返回-1(返回值为-1表示读取结束)

void close()

关闭输入流对象,释放相关系统资源

OutputStream

此抽象类是表示字节输出流的所有类的父类。输出流接收输出字节并将这些字节发送到某个目的地。

方法名

使用说明

void write(int n)

向目的地中写入一个字节

void close()

关闭输出流对象,释放相关系统资源

Reader

Reader用于读取的字符输入流抽象类,数据单位为字符

方法名

使用说明

int read()

读取一个字符的数据,并将字符的值作为int类型返回(0-65535之间的一个值,即Unicode值)。如果未读出字符则返回-1(返回值为-1表示读取结束)

void close()

关闭流对象,释放相关系统资源

Writer

Writer用于输出的字符输出流抽象类,数据单位为字符。

方法名

使用说明

void write(int n)

向输出流中写入一个字符

void close()

关闭输出流对象,释放相关系统资源

1.4 文件字节流-FileStream


FileInputStream通过字节的方式读取文件,适合读取所有类型的文件(图像、视频、文本文件等);

FileOutputStream通过字节的方式写数据到文件中,适合所有类型的文件(图像、视频、文本文件等);

FileInputStream

/**
 * 文件字节输入流
 */
public class FileInputStreamTest {

    public static void main(String[] args) {

        try(FileInputStream fis = new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\a.txt")){
            StringBuilder sb = new StringBuilder();
            int temp = 0;
            while( (temp = fis.read()) != -1 ){
                sb.append((char)temp);
            }
            System.out.println(sb);
        }catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

FileOutputStream

/**
 * 字节输出流
 */
public class FileOutputStreamTest {
    public static void main(String[] args) {
        String str = "kkkkkkk 18";
        // true表示内容会追加到文件末尾(append),false表示覆盖写
        try(FileOutputStream fos = new FileOutputStream("D:\\JAVA\\workSpace\\ioDemo\\a.txt",true)){
            // 将整个字节数组写入到文件中
            fos.write(str.getBytes());
            // 将数据从内存写入到磁盘中
            fos.flush();
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

注意fos.flush()操作才是将数据从内存写入磁盘

1.4.1 通过字节缓冲区提高读写效率

通过创建一个指定长度的字节数组作为缓冲区,以此来提高IO流的读写效率。该方式适用于读取较大文件时的缓冲区定义。

注意:缓冲区的长度一定是2的整数幂。一般情况下1024B的长度较为合适。

Q:为什么加缓冲区会提高读写IO的效率?

答:因为在操作系统中,CPU、内存处理的速度是非常快的,而与硬件交互的IO操作往往是低了很多个数量级的;如果还是采用上述一个字节一个字节进行一次写入的话,一个字节产生一次IO操作,但是实际上文件在内存中和磁盘中的存储单元是远远大于1个B的(以2的整数次幂为标准),也就是说一次IO写入或读出的数据只要不超过这个范围就可以一次性写入或读出;

所以建立缓冲区的作用就是把数据囤一囤再一块进行IO,这样最耗时的IO就用最少的次数却能达到一样的效果,速度和效率就提升了;

来看看chat老师的答案

IO操作引入缓冲区可以提高效率的原因主要有以下几点:

  1. 减少系统调用次数: 操作系统在执行IO操作时需要切换上下文,这是一项相对较为昂贵的操作。使用缓冲区可以减少实际的IO操作次数,因为数据可以在内存中累积一段时间,然后一次性写入磁盘或从磁盘读取,从而减少了系统调用的频率。
  2. 减少磁盘访问次数: 磁盘IO是相对慢速的操作,通过使用缓冲区,可以将多个小的IO操作合并为一个较大的IO操作,从而减少了磁盘访问的次数。这有助于减少磁盘头寻道的时间,提高了IO操作的效率。
  3. 数据局部性: 缓冲区可以使数据在内存中形成局部性,这意味着相关数据项在内存中是相邻的。当数据按照顺序存储在内存中时,CPU可以更有效地访问这些数据,而不需要频繁地在内存中进行跳跃。
  4. 异步IO: 缓冲区可以用于实现异步IO操作。在异步IO中,应用程序可以继续执行其他任务,而不必等待IO操作完成。当IO操作完成后,数据会被复制到应用程序的缓冲区中,这提高了程序的并发性和响应性。
  5. 数据重用: 缓冲区允许多次访问相同的数据,而无需重新从磁盘读取或重新写入。这在需要多次处理相同数据的情况下可以大大减少IO操作的成本。

总之,引入缓冲区可以将多个小的IO操作合并成更大的操作,减少了系统调用和磁盘访问的次数,提高了IO操作的效率,同时也提供了更好的数据管理和数据访问方式,有助于提高应用程序的性能和响应速度。

测试

/**
 * 采用字节缓冲区进行文件复制
 */
public class FileStreamBuffer {
    public static void main(String[] args) {
        long t1 = System.currentTimeMillis();
        copyFile1("D:\\JAVA\\workSpace\\ioDemo\\1.png","D:\\JAVA\\workSpace\\ioDemo\\2.png");
        long t2 = System.currentTimeMillis();
        System.out.println("未引入缓冲区耗时: " + (t2 - t1));

        long t3 = System.currentTimeMillis();
        copyFile2("D:\\JAVA\\workSpace\\ioDemo\\1.png","D:\\JAVA\\workSpace\\ioDemo\\3.png");
        long t4 = System.currentTimeMillis();
        System.out.println("引入缓冲区后耗时: " + (t4 - t3));


    }

    /**
     * 实现文件的拷贝(未引入缓冲区)
     * @param sour 源文件
     * @param dest 目的文件
     */
    public static void copyFile1(String sour, String dest){

        try(FileInputStream fis = new FileInputStream(sour);
            FileOutputStream fos = new FileOutputStream(dest);){
            int temp = 0;
            while( ( temp = fis.read()) != -1){
                fos.write(temp);
            }
            // 将文件从内存写入磁盘
            fos.flush();
        }catch(IOException e){
            e.printStackTrace();
        }
    }

    /**
     * 实现文件的拷贝(引入缓冲区)
     * @param sour 源文件
     * @param dest 目的文件
     */
    public static void copyFile2(String sour, String dest){

        try(FileInputStream fis = new FileInputStream(sour);
            FileOutputStream fos = new FileOutputStream(dest);){

            byte[] buffer  = new byte[1024]; // 缓冲数组
            int temp = 0;
            while( ( temp = fis.read(buffer)) != -1){
                // 每次都从buffer中取本次相对位置0开始temp字节长度的内容写入内存
                fos.write(buffer,0,temp);
            }
            // 将文件从内存写入磁盘
            fos.flush();
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

输出结果

未引入缓冲区耗时: 10108
引入缓冲区后耗时: 16

结果还是很明显的,一千倍左右的差距;

注意

  • 为了减少对硬盘的读写次数,提高效率,通常设置缓存数组。相应地,

>读取时使用的方法为:read(byte[] b)

>写入时的方法为:write(byte[ ] b, int off, int length)

  • 程序中如果遇到多个流,每个流都要单独关闭,防止其中一个流出现异常后导致其他流无法关闭的情况;关闭的顺序为:后开先关

1.4.2 缓冲字节流-BufferedStream

Java缓冲流本身并不具有IO流的读取与写入功能,只是在别的流(节点流或其他处理流)上加上缓冲功能提高效率,就像是把别的流包装起来一样,因此缓冲流是一种处理流(包装流);

BufferedInputStreamBufferedOutputStream这两个流是缓冲字节流,通过内部缓存数组来提高操作流的效率;缓存区的大小默认是8192字节,也可以使用其它的构造方法自己指定大小。

/**
 * 字节缓冲流的基本使用
 */
public class BufferedStream {
    public static void main(String[] args) {
        long t1 = System.currentTimeMillis();
        copyFile3("D:\\JAVA\\workSpace\\ioDemo\\1.png","D:\\JAVA\\workSpace\\ioDemo\\4.png");
        long t2 = System.currentTimeMillis();
        System.out.println("使用缓冲字节流耗时: " + (t2-t1));
    }

    /**
     * 使用字节缓冲流进行文件复制
     * @param sour 源文件
     * @param dest 目的文件
     */
    public static void copyFile3(String sour, String dest){

        try(FileInputStream fis = new FileInputStream(sour);
            FileOutputStream fos = new FileOutputStream(dest);
            // 创建缓冲字节流(处理流)
            BufferedInputStream bis = new BufferedInputStream(fis);
            BufferedOutputStream bos = new BufferedOutputStream(fos);){

            int temp = 0;
            while( ( temp = bis.read()) != -1){
                bos.write(temp);
            }
            bos.flush();
        }catch (IOException e  ){
            e.printStackTrace();
        }
    }
}


// 控制台输出
使用缓冲字节流耗时: 57

1.5 文件字符流-FileReader/FileWriter


当执行IO操作处理的文件类型是文本文件时,可以使用文件字符流进行读写,会以字符为单位进行处理

FileReader

/**
 * 文件字符输入流
 */
public class FileReaderTest {
    public static void main(String[] args) {

        try(FileReader fr = new FileReader("D:\\JAVA\\workSpace\\ioDemo\\aa.txt")){
            int temp = 0;
            StringBuilder sb = new StringBuilder();
            while( ( temp = fr.read()) != -1){
                sb.append((char)temp);
            }
            System.out.println(sb);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

// 输出结果
文件字符输入流 FileReader测试 

FileWriter:

/**
 * 文件字符输出流
 */
public class FileWriterTest {
    public static void main(String[] args) {

        try(FileWriter fw = new FileWriter("D:\\JAVA\\workSpace\\ioDemo\\aa.txt",true)){
            String str = "当执行IO操作处理的文件类型是文本文件时,可以使用文件字符流进行读写,会以字符为单位进行处理;";
            fw.write(str);
            fw.flush();
        }catch (IOException e ){
            e.printStackTrace();
        }
    }
}

1.5.1 缓冲字符流-BufferedReader/BufferedWriter

乍一看文件字符流的作用,好像和文件字节流没有什么区别,这难道是鸡肋的类吗??

实际情况下,文本文件中包含有换行段落等间隔符,如果以文件字节流读取还需要进行转换,提高了操作成本;这个时候结合缓冲字符流(处理流)的文件字符流(节点流)就可以轻易地对文本文件进行行段的处理;

BufferedReader

  • 基本方法

  • 测试

对于下面的文本文件,我们采用BufferedReader进行读取:

/**
 * 字符输入缓冲流
 */
public class BufferedReaderTest {
    public static void main(String[] args) {
        // 简化创建
        try (BufferedReader br = new BufferedReader(new FileReader("D:\\JAVA\\workSpace\\ioDemo\\aa.txt"))){
            String temp = "";
            // readLine() 一次读取一行
            while( (temp = br.readLine()) != null){
                System.out.println(temp);
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

//输出结果
当执行IO操作处理的文件类型是文本文件时,可以使用文件字符流进行读写;
会以字符为单位进行处理;当执行IO操作处理的文件类型是文本文件时;
可以使用文件字符流进行读写,会以字符为单位进行处理;
当执行IO操作处理的文件类型是文本文件时\br,
可以使用文件字符流进行读写,会以字符为单位进行处理;
当执行IO操作处理的文件类型是文本文件时,
可以使用文件字符流进行读写,会以字符为单位进行处理;

BufferedWriter

  • 基本方法

  • 测试
/**
 * 字符输出缓冲流
 */
public class BufferedWriterTest {
    public static void main(String[] args) {

        try(BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\JAVA\\workSpace\\ioDemo\\aa.txt"))){
            bw.write("白日依山尽");
            bw.newLine(); // 换行
            bw.write("黄河入海流");
            bw.flush();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

案例1:为文件中的内容添加行号

/**
 * 为文件的内容添加行号
 */
public class LineNumberTest {
    public static void main(String[] args) {

        try(BufferedReader br = new BufferedReader(new FileReader("D:\\JAVA\\workSpace\\ioDemo\\aa.txt"));
            BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\JAVA\\workSpace\\ioDemo\\aaa.txt"))){

            int index = 1; // 定义序号
            String temp = "";
            while( (temp = br.readLine()) != null){
                bw.write(index +". " +  temp);
                index++;
                bw.newLine(); // 换行
            }
            bw.flush();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}


//aa.txt
白日依山尽
黄河入海流
// aaa.txt
1. 白日依山尽
2. 黄河入海流

1.6 转换流-InputStreamReader/OutputStreamWriter


InputStreamReader / OutputStreamWriter用来实现将字节流转化成字符流

即:

  • 输入端,将字节数据转换为字符数据进行处理;
  • 输出端,将字节数据转换为字符数据进行处理;

常用的转换流功能是:解决乱码问题

乱码的产生是由于编码的类型和阶码的类型不匹配导致的;

案例1:解决乱码问题

采用ANSI(实际上是GBK)对文本文件进行编码,使用转换流进行乱码处理:

/**
 * 转换流的使用1(出现乱码)
 */
public class InputStreamReaderTest {
    public static void main(String[] args) {

        try(InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\aaa1.txt"))){
            StringBuilder sb = new StringBuilder();
            int temp = 0;
            while( ( temp = isr.read()) != -1){
                sb.append((char)temp);
            }
            System.out.println(sb);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

//输出结果
1. ������ɽ��
2. �ƺ��뺣��

由于Java是采用UTF-8作为编码格式的,因此实际上就存在着文件编码和解码类型不匹配的问题;

解决

try(InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\aaa1.txt"),"gbk")){

只需要在转换流的创建中加入和编码对应的解码类型即可(此处指定为gbk);

// 输出结果
1. 白日依山尽
2. 黄河入海流

案例2:通过字节流读取文本文件并添加行号

分析题意

基于上图所示的代码如下:

/**
 * 转换流的使用2
 */
public class InputStreamReaderTest {
    public static void main(String[] args) {
        // 依次创建转换流(字节输入流)、字符输出缓冲流
        try(InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\aa.txt"),"utf-8");
            BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\JAVA\\workSpace\\ioDemo\\aaaa.txt"));){
            //
            StringBuilder sb = new StringBuilder();
            int temp = 0;
            int index = 1;
            while( ( temp = isr.read()) != -1){
                bw.write(index + ". "+(char)temp);
                bw.newLine();
                index++;
            }
            bw.flush();

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

注意第7-8行,程序只是将字节输入流转换成了字符输入流,此时读取的方式还是逐个字符逐个字符读取,这导致第13-14行中每一个while循环中isr.read()返回的都是一个字符的ASCII码,而非一行的!!!

写入文件的结果如上图所示,这个并非我们想要的结果;

改进

上述问题的原因在于输入流最后的处理形式是“逐字符”而非“逐行”!!所以在转换流将字节流转为字符流后,还需要将字符流转换为字符缓冲流,使用其中的readLine()方法才能实现“逐行”;

/**
 * 转换流的使用2
 */
public class InputStreamReaderTest {
    public static void main(String[] args) {
        // 依次创建转换流(字节输入流)、字符缓冲流、字符输出缓冲流
        try(InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\aa.txt"),"utf-8");
            BufferedReader br = new BufferedReader(isr); // 字符输入缓冲流
            BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\JAVA\\workSpace\\ioDemo\\aaaa.txt"));){
            //
            StringBuilder sb = new StringBuilder();
            String temp = null;
            int index = 1;
            while( ( temp = br.readLine()) != null){ // 逐行读取
                bw.write(index + ". "+temp);
                bw.newLine();
                index++;
            }
            bw.flush();

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

结果

案例3:通过转换流实现键盘输入屏幕输出

首先分析“键盘输入”是如何实现的,可能很快就会想到是System.in,那么同理屏幕输出是System.out,那么它们都是什么类型的输入输出流对象呢?

public final static PrintStream out = null; // out对象定义
public final static InputStream in = null; // in对象定义

可以看到它们都是字节流对象,那么就要考虑实际情况了,键盘的输入是包含多字符、可换行的,那么在处理输入流时就要将其转换成缓冲字符输入流进行逐行处理;同理屏幕的输出是包含多字符、可换行的,因此输出流应该是缓冲字符输出流

/**
 * 案例3:通过转换流实现键盘输入屏幕输出
 */
public class ConvertStreamTest3 {
    public static void main(String[] args) {
        // 实现 字节流->字符流->缓冲字符流 的转换
        try(BufferedReader br = new BufferedReader( new InputStreamReader( System.in));
            BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( System.out));){
            String inputStr = "";
            while( !( inputStr = br.readLine()).equals("quit")){
                bw.write(inputStr);
                bw.newLine();
                bw.flush();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

输出结果

sjakjl
sjakjl
456
456
撒寄快递
撒寄快递
quit

Process finished with exit code 0

1.7 字符输出流-PrintWriter


基于前文可以知道,要在文件中写入带有换行的字符是需要用到两个流对象的:缓冲字符输出流BufferedWriter(处理流)中嵌套文件字符输出流FileWriter(节点流);

PrintWriter集两者为一体,具有自动行刷新缓冲字符输出流,特点是可以按行写出字符串,并且可通过

println()方法实现自动换行;

案例1:通过PrintWriter为文件添加行号

/**
 * 添加行号
 */
public class PrintWriterTest {
    public static void main(String[] args) {
        try(BufferedReader br = new BufferedReader(new FileReader("D:\\JAVA\\workSpace\\ioDemo\\aa.txt"));
            PrintWriter pw = new PrintWriter("D:\\JAVA\\workSpace\\ioDemo\\aaaaa.txt");){
            int index =1;
            String temp = "";
            while( ( temp = br.readLine())  != null ){
                pw.println((index++) +". " + temp);
            }
            pw.flush();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

//aa.txt
白日依山尽
黄河入海流

//aaaaa.txt
1. 白日依山尽
2. 黄河入海流

1.8 数据流-DataStream


数据流将“基本数据类型与字符串类型”作为数据源,从而允许程序以与机器无关的方式从底层输入输出流中操作Java基本数据类型与字符串类型。

DataInputStreamDataOutputStream提供了可以存取与机器无关的所有Java基础类型数据(如:int、double、String等)的方法;

public class TestDataStream {
  public static void main(String[] args) {
    //创建数据输出流对象与文件字节输出流对象
    try(DataOutputStream dos = new DataOutputStream(new FileOutputStream("d:/data"));
      //创建数据输入流对象与文件字节输入流对象
      DataInputStream dis = new DataInputStream(new FileInputStream("d:/data"))){
      //将如下数据写入到文件中
      dos.writeChar('a');
      dos.writeInt(10);
      dos.writeDouble(Math.random());
      dos.writeBoolean(true);
      dos.writeUTF("你好中国");
      //手动刷新缓冲区:将流中数据写入到文件中
      dos.flush();
      //直接读取数据:读取的顺序要与写入的顺序一致,否则不能正确读取数据。
      System.out.println("char: " + dis.readChar());
      System.out.println("int: " + dis.readInt());
      System.out.println("double: " + dis.readDouble());
      System.out.println("boolean: " + dis.readBoolean());
      System.out.println("String: " + dis.readUTF());


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

1.9 对象流-ObjectStream


其中嵌套的是字节流;

1.9.1 基本使用:

/**
 * 基本使用
 */
public class ObjectStreamTest1 {
    public static void main(String[] args) {
        try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\JAVA\\workSpace\\ioDemo\\aaaaa.txt",false));
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\aaaaa.txt"));){

            // 写入文件
            oos.writeInt(100);
            oos.writeChar('a');
            oos.writeBoolean(false);
            oos.writeUTF("卡卡的");
            oos.flush(); // 刷新

            // 读出文件内容
            System.out.println("int: "+ois.readInt());
            System.out.println("char: "+ois.readChar());
            System.out.println("boolean: "+ois.readBoolean());
            System.out.println("String: "+ois.readUTF());

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

1.9.2 序列化和反序列化

当两个进程远程通信时,彼此可以发送各种类型的数据。 无论是何种类型的数据,都会以二进制序列的形式在网络上传送。比如,我们可以通过http协议发送字符串信息;我们也可以在网络上直接发送Java对象。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象才能正常读取。

序列化:把数据结构或Java对象转换为二进制字节序列的过程;

反序列化:字节序列恢复为数据结构或Java对象的过程;

涉及的类和接口

ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中;

ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回;

只有实现了Serializable接口的类的对象才能被序列化。 Serializable接口是一个空接口(标识接口),只起到标记作用;

序列化和反序列化常见应用场景

  • 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  • 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
  • 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
  • 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。

Q:如果有些字段不想进行序列化怎么办?

对于不想进行序列化的变量,可以使用transient关键字修饰;

  • transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被
  • transient 修饰的变量值不会被持久化和恢复;

几点注意

  • transient 只能修饰变量,不能修饰类和方法。
  • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0
  • static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。

参考博客:

看懂java序列化,这篇就够了 - 掘金

序列化
/**
 * 实现序列化
 */
public class ObjectStreamTest2 {
    public static void main(String[] args) {
        try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\JAVA\\workSpace\\ioDemo\\aaaaa.txt"))){

            User user = new User(1,"kk",18);
            oos.writeObject(user);
            oos.flush();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

反序列化
/**
 * 实现反序列化
 */
public class ObjectStreamTest3 {
    public static void main(String[] args) {
        try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\aaaaa.txt"))){

            User user = (User) ois.readObject();
            System.out.println(user);
        }catch (IOException | ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}

// 输出结果
Users{userid=1, username='kk', userage='18'}

1.10 IO中的设计模式思考


装饰器模式:是GOF23种设计模式中较为常用的一种模式。它可以实现对原有类的包装和装饰,使新的类具有更强的功能

IO流体系中大量使用了装饰器模式,让流具有更强的功能、更强的灵活性。比如:

FileInputStream fis = new FileInputStream(src);
BufferedInputStream bis = new BufferedInputStream(fis);

显然BufferedInputStream装饰了原有的FileInputStream,让普通的FileInputStream也具备了缓存功能,提高了效率。

致谢&部分资源出处:百战程序员 

  • 19
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值