【Java IO流】字节流详解

在这里插入图片描述

1. IO 流概述

什么是 IO 流?

IO 流是存取数据的解决方案,在计算机中数据存放在硬盘的文件中,如果程序需要使用这些数据时,就会从文件中把数据读取到内存中,内存中数据的特点是不能永久化存储,程序停止,数据丢失。那么如何持久的保存程序中的数据呢?

程序中的数据会通过写入的方式存储到硬盘的文件中,特点是可以长期的存储,不会随着程序的终止而丢失,那么 Java 语言是怎样读取和写入数据的呢?

这里就引出了流的概念,流是一个抽象的概念,我们把数据在两设备的传输抽象为流的方式,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。

2. IO 流分类

Java中的流可以从不同的角度进行分类,按照流动方向可以分为输入流和输出流,输入流用于数据的读取,输出流用于数据的写出。按照操作对象的不同可以分为字节流和字符流,字节流可以操作所有类型的文件,例如:文本,图像,音频等,字符流用于操作纯文本文件。

image-20230114213143003

Java中有四种顶层的流 InputStreamOutputStreamReaderWriter ,这四种流是抽象类,不能用来实例化对象,其又分别有更具体的子类,分为文件流,缓冲流,数据流,转换流,Print流,Object流等,都分别有特定的功能或用来操作特定的数据。

我们一般不会使用字节流来操作文本文件,因为会出现乱码的情况,相信学完今天的内容,你就会明白其中的原理。纯文本文件是指使用记事本的形式创建的文件,例如 txt 文件,md 文件,而 Word 文件就不是纯文本文件。

在学习时,为了逻辑清晰,一般通过字节流和字符流两类来学习,每一类又包括输出流和输入流。

扫描二维码关注公众号,回复: 14548149 查看本文章

总结

  • IO流是存储和读取数据的解决方案
  • I 表示 input,O表示 output,流则是抽象的一种概念,表示数据的传输
  • IO 流用于读取数据,既可以读取本地文件,也可以是网络文件
  • 按照流的方向,IO 流分为输出流(程序到文件)和输入流(文件到程序)
  • 按照文件类型,IO 流分为字节流(操作所有类型)和字符流(操作纯文本文件)

3. 字节输出流

上面说到的四种基本的流类都是抽象类,不能用来实例化对象,我们要使用其子类创建对象用来传输数据。

例如,往本地文件中写出数据时,可以使用 FileOutputStream ,该类被称为字节输出流。使用该类往本地文件中写出数据可以分为三步:

  1. 创建流对象
  2. 写出数据
  3. 释放资源

image-20230117125637749

示例:

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test {
    
    
    public static void main(String[] args) throws IOException {
    
    
        /*
        使用 FileOutputStream 往本地文件中写入数据
        FileOutputStream 构造方法的参数既可以使用String类型也可以使用File类
        程序需要进行异常处理,直接抛出异常即可
         */

        //1. 创建流对象
        FileOutputStream fos=new FileOutputStream("test.txt");
        //2. 写出数据
        fos.write(97);
        //3. 释放资源
        fos.close();
    }
}

在程序创建流对象时,程序和文件之间就会建立一个通道,此时我们就可以通过调用 write 方法往文件中写出数据,写出数据完成之后,进行资源释放,相当于打断了这个通道。

image-20230115183723684

细节:

在创建 FileOutputStream 流对象时,构造方法中既可以传入 String 类型也可以传入 File 类对象,如果传入的是 String 类型,其底层会自动创建 File 类对象。如果目标文件不存在,则会创建一个新的文件,并且把数据写出到新创建的文件中,但是要保证父级目录存在。如果文件存在,则会默认清空文件,并且写出数据。

write() 方法传入的参数是一个整数,实际写出到文件中的是参数在字符集表中对应的字符。几乎所有的流操作都要进行释放资源的操作。释放资源实际就是打断了程序和文件之间的流通道,如果不释放资源,文件则一直被程序占用。

上面的方式每次在只能传输一个字节的数据,那么如何一次传输多个字节数据呢?

FileOutputStream 中一共有三种方法进行数据的写出:

方法 说明
void write(int b) 一次写一个字节数据
void write(byte[] b) 一次写一个字节数组的数据
void write(byte[] b,int off,int len) 一次写一个字节数组的部分数据

如果要一次性的写多个数据,那么你可以先把数据放到 byte 类型的数组中,然后写入文件中。

示例,在写出数据时:

byte[] bytes={
    
    97,98,99,100};
fos.write(bytes);

或者:

//2. 写出数据
String s="abcd";
byte[] bytes = s.getBytes();
fos.write(bytes);

两种方法效果相同,运行结果:

abcd

前面说到,创建流对象时,如果文件存在,则会默认清空文件。那么,我们如何把数据追加或者写入到文件中呢?

其实,在 FileOutputStream 类中的构造方法中,有一个 boolean 类型的参数,这个参数控制写出数据时是否追加在文件末尾,默认传入的是 false ,我们只需要在创建对象时传入 true 即可把数据追加写出到文件末尾。

示例,假设文件中已有数据 Hello:

//2. 写出数据
String s1="abcd";
byte[] bytes1 = s1.getBytes();
fos.write(bytes1);
String s2="\r\n";
byte[] bytes2 = s2.getBytes();
fos.write(bytes2);
String s3="Hello";
byte[] bytes3 = s3.getBytes();
fos.write(bytes3);

运行结果:

Helloabcd
Hello

接下来我们查看一下 JDK 源码中的 FileOutputStream 类,这个问题就不难理解了。

  public FileOutputStream(String name, boolean append)
        throws FileNotFoundException
    {
    
    
        this(name != null ? new File(name) : null, append);
    }
    public FileOutputStream(File file, boolean append)
        throws FileNotFoundException
    {
    
    
    ...
    }

在不同的操作系统中,换行符的定义是不同的,Windows系统中,换行符是\r\n,表示回车换行,回车是指把光标移动到一行的开始,换行指光标移动到下一行,Java语言对其进行了优化,只需要使用\r或者\n来实现换行,实际上Java在底层会进行补全操作。MacOS 中换行使用 \r,而 Linux 中使用 \n 表示换行。

4. 字节输入流

我们可以使用 FileInputStream 类把本地文件中的数据读取到程序中,该类称为字节输入流类,和字节输出流类似,使用字节输入流读取本地文件可以分为三个步骤:

  1. 创建流对象
  2. 读取数据
  3. 释放资源

示例,假设文件中以后数据abcd:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class Test {
    
    
    public static void main(String[] args) throws IOException {
    
    
        /*
        使用 FileInputStream 把本地文件中的数据读取到程序中
        FileInputStream 构造方法的参数既可以使用String类型也可以使用File类对象
        程序需要进行异常处理,直接抛出异常即可
         */

        //1. 创建流对象
        FileInputStream fis=new FileInputStream("test.txt");

        //2. 读取数据
        int b1 = fis.read();
        System.out.println((char)b1);
        int b2 = fis.read();
        System.out.println((char)b2);
        int b3 = fis.read();
        System.out.println((char)b3);
        int b4 = fis.read();
        System.out.println((char)b4);
        int b5 = fis.read();
        System.out.println(b5);
        //3. 释放资源
        fis.close();
    }
}

同样的,在程序创建流对象时,程序和文件之间就会建立一个通道,此时我们就可以通过调用 read() 方法读取本地文件中的数据,读取完数据以后,需要释放资源,相当于打断了这个通道,否则文件将一直被程序占用。

细节:

在创建流对象时,传入的参数既可以是 String 类型,也可以是 File 类的对象。不同的是,如果目标文件不存在则会报错,如果文件存在,则会读取数据。read() 方法的返回值是文件中字符在字符集中对应的十进制值,如果读取到文件末尾,则会返回 -1 。

image-20230115202933750

如果文件中存放的数据恰好是 -1 ,其实它是分负号和 1 两次读取的。如果读取的数据很多时,这样的方法显然是不可取的,此时就要使用循环来读取文件中的数据。

示例:

//2. 读取数据
int b;
while((b=fis.read())!=-1){
    
    
     System.out.println((char)b);
}

这里定义一个临时变量是十分重要的,而不是多此一举。否则将无法实现循环打印读取到的数据的效果。

使用 FileInputStream 读取数据时,一次只能读取一个字节的数据,显然这样的方式效率是非常低的,那么怎样解决这个问题呢?此时我们可以使用 read() 方法的重载方法一次读取多个数据,往 read() 方法中传入一个字节类型的数组,read() 方法一次读取多少个数据是由数组的大小决定。

示例,假设文件中存放数据 abc:

import java.io.FileInputStream;
import java.io.IOException;

public class Test {
    
    
    public static void main(String[] args) throws IOException {
    
    
        //使用字节数组来一次读取多个字节数据
        FileInputStream fis=new FileInputStream("test.txt");

        byte[] bytes=new byte[2];
        int len1 = fis.read(bytes);
        System.out.println(new String(bytes,0,len1));

        int len2= fis.read(bytes);
        System.out.println(new String(bytes,0,len2));
    }
}

上面的例子中,read() 方法每次读取两个字节的数据,并且返回读取到的数据的个数,读取到文件末尾返回 -1 。为了防止 read() 方法读取到最后时获取残留数据,如下图。可以往 String 类构造方法中加入两个参数,表示从某个索引开始,读取 len 个字符。

image-20230115212819664

5. 文件拷贝

前面已经学习了数据的读取和写入,那么我们就可以实现文件拷贝了。之前说过,字节流可以操作所有类型的文件,那么,我们今天使用图片文件作为示例来演示文件拷贝。

示例:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test {
    
    
    public static void main(String[] args) throws IOException {
    
    

        //1. 创建流对象
        FileInputStream fis=new FileInputStream("C:\\Users\\24091\\Desktop\\java.png");
        FileOutputStream fos=new FileOutputStream("copy.png");
        //2. 拷贝文件
        int b;
        while((b=fis.read())!=-1){
    
    
            fos.write(b);
        }
        //3. 释放资源
        fos.close();
        fis.close();
    }
}

在创建多个流对象的程序中释放资源时,先创建的后释放。此时,桌面的 java.png 文件已经被拷贝到了项目中的 copy.png 文件中。

前面说到,FileInputStream 每次读取一个字节的效率是非常低的,那么我们可以改写上面的程序,每次读取多个字节来实现文件的拷贝。

修改示例:

 		//1. 创建流对象
        FileInputStream fis=new FileInputStream("C:\\Users\\24091\\Desktop\\java.png");
        FileOutputStream fos=new FileOutputStream("copy.png");
        //2. 拷贝文件
        int len;
        byte[] bytes = new byte[5 * 1024];
        while((len=fis.read())!=-1){
    
    
            fos.write(bytes,0,len);
        }
        //3. 释放资源
        fos.close();
        fis.close();

6. IO 流中的异常处理

在 JDK 1.7 中,Java 提供了一个 autoCloseable 接口,用于在特定情况下进行异常处理。在 Java 7 中,可以把定义流对象的代码写在 try 后面的括号中,表示当 try…catch 语句执行完成后,会自动释放资源,前提是写在括号中的类必须是实现了 autoCloseable 接口的类。

但是这样在括号中定义流对象的代码是难以阅读的,所以在Java 9 中,我们可以把定义流对象的代码放在 try 语句前面,括号中只需要写流的引用变量名,执行逻辑与前面相同。

例如,拷贝文件时使用 try…catch 捕获异常:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test {
    
    
    public static void main(String[] args) throws FileNotFoundException {
    
    
        //1. 创建流对象
        FileInputStream fis = new FileInputStream("C:\\Users\\24091\\Desktop\\java.png");
        FileOutputStream fos = new FileOutputStream("copy.png");
        try (fis; fos) {
    
    
            //2. 拷贝文件
            int len;
            byte[] bytes = new byte[5 * 1024];
            while ((len = fis.read()) != -1) {
    
    
                fos.write(bytes, 0, len);
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
}

在学习 Java 编程基础时,对于 IO 流中出现的异常我们抛出即可,后面在学习 Spring框架时,再做探讨。

7. 总结

在 File 类中,我们可以使用类的对象来操作文件和目录,包括增删查等。不同的是,IO 流用于文件的读写操作,这些操作是 File 无法实现的。在创建流对象时,相当于在文件和程序之间建立了一个流的通道,方便对数据进行操作。

流是个抽象的概念,是对输入输出设备的抽象,无论采取什么样的形式输出输入数据,只是针对流做处理,无关与输入输出的设备,可以说这个思想是很优秀的。


Java编程基础教程系列

【Java集合】Collection 体系集合

【Java基础】泛型详解

猜你喜欢

转载自blog.csdn.net/zhangxia_/article/details/128724609