Logo

郎哥编程

输入与输出流

2021-07-08 211

学习目标:掌握文件的读写操作。

输入输出流

前面介绍了Java.io包的File类,File类用于目录和文件的创建、删除、遍历等操作,但不能用于文件的读写。

Java 对文件的写入和读取涉及到流的概念,写入为输出流,读取为输入流。如何理解流的概念呢?可以把流看成流动的自来水,打开水龙头,自来水就会通过自来水管从水源流到用户家中。同样的道理,水库中的水也会通过管道流入到水源。从水源流出到用户住家为自来水的输出流,从水库流入到水源为自来水的输入流,只有这样,自来水才能源源不断地输送到用户家中。

如果把水源看成文件,用户住家为读取文件的对象,水库为写入文件的对象,就很容易理解Java的输入与输出流了。当Java程序的写入对象(水库)需要将数据写入到文件(水源)时,需要建立一条从写入对象(水库)到文件(水源)的通道,这个通道就是输入流;当Java程序的读取对象(用户住家)需要读取文件(水源)时,也需要建立一条从文件(水源)到读取对象(用户住家)的通道,这个通道就是输出流。

在Java程序中,要想从文件中读取数据,需要在程序和文件之间建立一条数据输入的通道,这样程序就可以从文件中读取数据了。反之,如果要在Java程序中把数据写入到文件中,也需要在程序和文件之间建立一条数据输出的通道。当程序创建输入流对象时,Java会自动建立这个数据输入通道;创建输出流时,Java也会自动建立这个数据输出通道。如下图所示:

11.png

输入流是从文件读取数据,是一个拉取数据的过程;输出流是将数据写入到文件,是一个推送数据的过程。

为了便于理解输入输出流,前面都是以文件为数据源来讨论的。其实,Java的输入与输出流支持任何数据源的读取与写入,包括键盘、文件、网络、数据库等数据源。

输入流和输出流按读取和写入的数据单位可分为字节流和字符流,字节流是以字节为单位传输数据的流,字符流是以字符为单位传输数据的流。

Java所提供的输入流和输出流类封装在Java.io包中,Java输入输出流的体系结构如下图所示:

12.png

从图中可以看出,Reader和Write为字符输入输出流,InputStream和OutputStream为字节输入输出流。这四个类属于抽象流类,不能在程序中直接实例化使用,可以使用其派生的类。

InputStream类

InputStream抽象类是表示字节输入流的所有类的超类,它以字节为单位从数据源中读取数据,其派生的常用子类说明如下:

●   FileInputStream类

 该类以字节为单位从文件中读取数据。

●   ByteArrayInputStream类

该类在内存中创建一个字节数组缓冲区,从输入流读取的数据保存在该字节数组缓冲区中。

●   ObjectInputStream类

该类从输入流读入对象,读取对象信息。

InputStream类定义了Java的输入流模型,下面是其常用方法的一个说明:

●   public abstract int read() throws IOExecption

该方法用于从输入流中读取数据的下一个字节,返回读到的字节值,若遇到流的末尾,返回-1。

●   public int read(byte[] b) throws IOExecption

该方法用于从输入流中读取b.length个字节的数据,并将数据存储到缓冲区数组b中,返回的是实际读到的字节数。

●   public int read(byte[] b,int off,int len) throws IOExecption

该方法用于从输入流中读取len个字节的数据,并从数组b的off位置开始写入到这个数组中。

●   public void close() throws IOExecption

关闭此输入流,并释放与此输入流相关联的所有系统资源。

OutputStream类

OutputStream抽象类是表示字节输出流的所有类的超类,它以字节为单位将数据写入数据源,其派生的常用子类说明如下:

●   FileOutputStream类

 该类以字节为单位将数据写入到文件。

●   ByteArrayOutputStream类

该类在内存中创建一个字节数组缓冲区,所有发送到输出流的数据保存在该字节数组缓冲区中。

●   ObjectOutputStream类

该类将对象信息写入到输出流。

下面是OutputStream类的常用方法介绍。

●   public abstract void write(int b) throws IOExecption

该方法用于将指定的字节写入到输出流。

●   public int write(byte[] b) throws IOExecption

该方法用于将b.length个字节从指定的byte数组写入到输出流。

●   public int write(byte[] b,int off,int len) throws IOExecption

该方法用于将len个字节的数据,并从数组b的off位置开始写入到输出流。

●   public void close() throws IOExecption

关闭此输出流,并释放与此输出流相关联的所有系统资源。

Reader类

Read抽象类是表示字符输入流的所有类的超类,它以字符为单位从数据源中读取数据。其派生的常用子类说明如下:

●   InputStreamReader类

 该类从数据源读取字节并将其解码为使用指定的字符集的字符。

●   FileReader类

 该类继承于InputStreamReader,用于读取字符类文件,如文本文件。

●   BufferedReader类

该类用于将缓冲区中的数据以字符为单位读取。

下面是Reader类的常用方法介绍。

●   public int read(int b) throws IOExecption

该方法用于读取单个字符,返回作为整数读取的字符,如果已经到达流的末尾,返回-1。

●   public int read(char[] cbuf) throws IOExecption

该方法用于将字符读入到cbuf,返回读取的字符数。

●   public abstract int read(char[] cbuf,int off,int len) throws IOExecption

该方法用于读取len个字符的数据,并从数组cbuf的off位置读入到这个数组中。

●   public abstract  void close() throws IOExecption

关闭此输入流,并释放与此输出流相关联的所有系统资源。

Writer类

Writer抽象类是表示字符输出流的所有类的超类,它以字符为单位向数据源写出数据。其派生的常用子类说明如下:

●   OutputStreamWriter类

该类将输出的字符流变为字节流,即将一个字符流的输出对象变为字节流的输出对象。

●   FileWriter类

 该类从 OutputStreamWriter 类继承而来。该类按字符向字符类文件写入数据。

●   BufferedWriter类

该类用于将文本写入字符输出流,缓冲各个字符,从而提供单个字符,数组和字符串的高效写入。

下面是Writer类的常用方法介绍。

●   public void write(int b) throws IOExecption

该方法用于向数据源写入单个字符。

●   public void write (char[] cbuf) throws IOExecption

该方法用于向数据源写入字符数组。

●   public abstract void write (char[] cbuf,int off,int len) throws IOExecption

该方法用于向数据源写入len个字符数据,并从数组cbuf的off位置开始。

●   public void write (String  str) throws IOExecption

该方法用于向数据源写入字符串。

●   public abstract void flush() throws IOExecption

刷新该输出流的缓冲,将缓冲的数据全部写入到数据源。

●   public abstract  void close() throws IOExecption

关闭此输出流,关闭之前需先调用flush()。

流是一个抽象的概念,它代表一串数据的集合,当Java程序需要从数据源读取数据时,就需要开启一个到数据源的流。同样,当程序需要输出数据到目的地时,也需要开启一个流。流的创建是为了更方便地处理数据的输入和输出。

FileInputStream类

本小节探讨如何通过FileInputStream读取本地文件。本地文件是指存储在本地磁盘中的文件,我们存储到电脑中的图片、视频、音乐、文档资料都属于本地文件。

这些本地文件都可以用相对应的程序打开。图片可以用Windows自带的照片查看器或画图程序打开查看;音乐和视频可以用Windows自带的Media Player或暴风影音等播放器打开;文档资料可以用办公软件Word等程序打开编辑。这些程序在开发过程中,都需要使用编程语言提供的文件读写技术。

在介绍文件读写技术之前,有必要先了解一下二进制文件和文本文件。二进制文件和文本文件在物理存储上并没有什么区别,存储在硬盘上的文件都是以二进制方式存储的。二者的区别在于解释数据的逻辑不同。程序读取文本文件时,可以以字符方式读取,也可以以字节方式读取,将读取的数据解释为ASCII或unicode编码,也就是解释为字符,读取的字符可以直接输出到屏幕上显示出来;当程序读取二进制文件时,是以字节方式读取的,对读取数据的解释由读取程序决定。如读取图片文件时,读取图片的程序需要了解该文件的结构,并解释读取的数据,如果不了解该图片文件的结构,读取图片文件就会失败,也就无法把图片显示出来了。

从前面的讨论可知,Java提供的FileInputStream类适合于读取二进制文件,而不太适合读取文本文件。当然也可以读取文本文件,不过需要做相应地读取处理,否则遇到中文就会出现乱码。

用FileInputStream读取文件流程如下:

18.png

用FileInputStream读取文件时,可以先用File类打开本地文件,然后实例化FileInputStream对象时,传入已打开文件的File对象,就可以调用FileInputStream的read方法从文件读取数据了。

FileInputStream也提供了另外一种构造方法,该构造方法直接传入文件的存储路径,而无需实例化File对象。该构造方法把实例化File对象的语句放到了构造方法里面。

案例6:建立FileInputStreamTest1测试类,使用FileInputStream读取文本文件。

在PUnit11项目新建inputstream包,在inputstream包下新建FileInputStreamTest1类。代码如下:

package inputstream;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
 
public class FileInputStreamTest1 {
    public static void main(String[] args) {
        // 实例化File对象,准备读取sample.txt文件
        File file = new File("d:\\sample.txt");
        try {
            // 实例化FileInputStream
            // FileInputStream构造方法要求传入File对象
            FileInputStream fis = new FileInputStream(file);
            int b;
            try {
                while ((b = fis.read()) != -1) {
                    System.out.print((char) b);
                }
                fis.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
    }
}

案例代码用FileInputStream读取文本文件,被读取的文本文件存储在D盘根目录下,文件名称为sample.txt。使用FileInputStream读取文件数据之前,需要先实例化File对象,File类的构造方法要求传入被读取的文件路径。然后,实例化FileInputStream对象,并通过FileInputStream类的构造方法传入已实例化的File对象。因为FileInputStream在实例化过程中可能会抛出异常,因此需要用try-catch语句捕获异常。

FileInputStream类提供了多种读取文件的方法,例子程序使用了read()方法,该方法用于从输入流中读取数据的下一个字节,并返回读到的字节值,若遇到流的末尾,返回-1。程序通过while循环读取文件的所有数据,每读取一个字节就输出到控制台,输出之前需要做类型转换,将整型转换为字符输出。

FileInputStream类的read()方法只能一个字节一个字节地读取。FileInputStream类还提供了read(byte[] b) 方法读取文件数据,该方法用于从输入流中读取b.length个字节的数据,并将数据存储到缓冲区数组b中,返回的是实际读到的字节数。

案例7:建立FileInputStreamTest2测试类,使用FileInputStream的读取read(byte[] b) 方法读取文件数据。

在inputstream包下新建FileInputStreamTest2类。代码如下:

package inputstream;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
 
public class FileInputStreamTest2 {
    public static void main(String[] args) {
        // 实例化File对象,准备读取read.txt文件
        File file = new File("d:\\sample.txt");
        try {
            // 实例化FileInputStream
            // FileInputStream构造方法要求传入File对象
            FileInputStream fis = new FileInputStream(file);
            try {
                // 创建了一个和文件大小一样的缓冲区,刚刚好
                byte[] buf = new byte[fis.available()];
                fis.read(buf);
                fis.close();
                System.out.println(new String(buf));
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
 
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
    }
}

FileInputStream类提供了一个available()方法,该方法在读取本地文件时,返回文件的总字节数。程序声明了一个byte数组,并通过new关键字分配与被读取文件大小相同的空间,然后调用read(byte[] b)方法一次性读取文件全部数据。

显示中文乱码的问题

如果文本文件含有中文字符,建议使用read(byte[] b)方法读取整个文本文件,并将读取文件后的byte数组转换为字符串类型,这样做的好处是不会出现乱码。

建立sample1.txt文件,文件内容为中文,保存为utf-8编码。修改案例1的代码,读取sample1.txt文件。

sample1.txt文件内容如下:

一个完整的数学建模过程主要有三部分组成:

1、用适当的方法对实际问题进行描述;

2、采用各种数学和计算机手段求解模型;

3、验证模型运行的正确性

程序执行结果如下图所示:

19.png

从输出结果可以看出,控制台输出的中文内容为乱码。原因是FileInputStream类read()方法每次读取文件都是按照1个字节读取的,而中文字符都是用两个字节表示的,输出时自然就乱码了。

另外,在Java语言中,中文和英文字符默认都被处理为unicode编码,unicode编码都是用两个字节表示一个字符,既然中文和英文都是用2个字节表示一个字符,为什么英文字符输出没有问题呢?原因是在unicode编码中,英文字符依然是ASCII编码,多出的一个字节值为0没有用到。

修改列2程序代码,读取sample1.txt文件,程序输出结果如下图所示:

21.png

从图中可以看出,使用read(byte[] b)方法可以正确读取含中文内容的文本文件。

FileInputStream一般用来读取二进制文件,如果要读取文本文件,建议使用FileInputStream的read(byte[] b)方法读取整个文本文件,并将读取文件后的byte数组转换为字符串类型。用循环语句读取文件时,必须设定中止循环条件,一般以读取到文件尾部为中止条件。

FileOutputStream类

前面介绍了如何应用输入流FileInputStream从本地文件读取数据。本课介绍应用输出流FileOutputStream把数据写入本地文件。

用FileOutputStream写入文件流程如下:

26.png

使用FileOutputStream写入文件的过程,同使用FileInputStream过程相同,都是先用File类打开本地文件,实例化输入输出流,然后调用流的读写方法写入数据,最后关闭流。

FileOutputStream的构造方法

FileOutputStream提供了4个常用构造方法,用于实例化FileOutputStream对象,不同的场景使用不同的构造方法。

场景1:使用File对象打开本地文件,从文件读取数据。

public static void main(String[] args) {
    File  file = new File("d://samplew.txt");
    try {
        //实例化FileOutputStream
        FileOutputStream  fos = new FileOutputStream(file);
    } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

场景2:不使用File对象,直接传入文件路径。

public static void main(String[] args) {
    try {
        //实例化FileOutputStream
        FileOutputStream  fos = new FileOutputStream("d://samplew.txt");
    } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

FileOutputStream的构造方法允许直接传入文件路径,而无须使用File对象。查看该构造方法的源代码,其内部使用了File对象打开文件。

场景3:打开文件,在文件的尾部追加写入数据。

public static void main(String[] args) {
    File  file = new File("d://samplew.txt");
    try {
        //实例化FileOutputStream,在文件的尾部写入数据
        FileOutputStream  fos = new FileOutputStream(file,true);
    } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

场景要求在文件的尾部写入数据,由于前面两个构造函数都是从文件开始写入数据(覆盖原文件),因此不能使用前面2个场景的构造函数。FileOutputStream提供了另外两个构构造方法,分别是:

FileOutputStream(File file, boolean append);                                  

FileOutputStream(String name, boolean append);

同前面的构造方法相比,这两个构造方法各多了一个boolean参数append。append参数为true时,数据从文件尾部写入;append参数为false时,数据覆盖原文件。

FileOutputStream的写入方法

FileOutputStream类提供了多种文件写入方法,可以单独写一个字节到文件,也可以写一个byte数组到文件,也可以取byte数组的部分数据写入到文件。

案例9:建立FileOutputStreamTest测试类,使用FileOutputStream类的write()方法写入数据到文件。

在PUnit11项目新建outputstream包,在outputstream包下新建FileOutputStreamTest类。代码如下:

package outputstream;
 
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class FileOutputStreamTest {
    public static void main(String[] args) {
        File file = new File("d://new.txt");
        try {
            //创建文件
            file.createNewFile();
        } catch (IOException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
        try {
            //实例化FileOutputStream,在文件的尾部写入数据
            FileOutputStream fos = new FileOutputStream(file);
            String  str = "this is new file";
            for( int i = 0; i < str.length();i++ )
            {
                int b = (int)str.charAt(i);
                fos.write(b);
 
            }
            fos.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
    }
}

案例代码首先调用File类的createNewFile()创建new.txt文件,然后将str内容写入到新创建的new.txt文件中。

案例10:建立FileOutputStreamTest1测试类,使用FileOutputStream类的write(byte[] b)方法写入数据到文件。

在outputstream包下新建FileOutputStreamTest1类。代码如下:

package outputstream;
 
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class FileOutputStreamTest1 {
    public static void main(String[] args) {
        File file = new File("d://new.txt");
        try {
            //创建文件
            file.createNewFile();
        } catch (IOException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
        try {
            //实例化FileOutputStream,在文件的尾部写入数据
            FileOutputStream fos = new FileOutputStream(file);
            String  str = "this is new file";
            fos.write(str.getBytes());
            fos.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
    }
}

String类的getBytes()方法可以将字符串转换为byte数组,使用FileOutputStream 类的write(byte[] b)方法,将转换的byte数组写入文件。

案例11:建立FileOutputStreamTest3测试类,使用FileOutputStream类的w write(byte[] b,int off,int len)方法写入数据到文件。

在outputstream包下新建FileOutputStreamTest3类。代码如下:

package outputstream;
 
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class FileOutputStreamTest3 {
    public static void main(String[] args) {
        File file = new File("d://new.txt");
        try {
            //创建文件
            file.createNewFile();
        } catch (IOException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
        try {
            //实例化FileOutputStream,在文件的尾部写入数据
            FileOutputStream fos = new FileOutputStream(file);
            String  str = "this is new file";
            fos.write(str.getBytes(),5,11);
            fos.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
   }
}

程序把指定的str内容写入到文件,fos.write(str.getBytes(),5,10)语句的第一个参数为byte数组,第二个参数5是从byte数组的下标5开始,第三个参数是写入的字节数。程序执行后,写入的内容为“is new file”。

使用该方法一定要注意数组越界的问题。例如,byte数组长度为20,从下标12开始,写入15个字节到文件,就会造成数组越界,程序报错。

案例12:建立FileOutputStreamTest4测试类,使用FileOutputStream复制文件。

在outputstream包下新建FileOutputStreamTest4类。代码如下:

package outputstream;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class FileOutputStreamTest4 {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        String source = "d://sample.txt";
        String dest = "d://samplenew.txt";
        File fSource = new File(source);
        File  fDest =  new File(dest);
        //创建一个新文件
        try {
            fDest.createNewFile();
            //复制文件
            copyFile(fSource,fDest);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
    }
 
    public static void copyFile(File src, File dest){
        if(!src.exists()){
            System.out.println("源文件不存在");
            return ;
        }
        try {
            //创建文件输入流,读取源文件
            FileInputStream fis = new FileInputStream(src);
            //创建文件输出流,写入目标文件
            FileOutputStream fos = new FileOutputStream(dest);
 
            byte[] buf = new byte[fis.available()];
            //读取源文件
            fis.read(buf);
            //将缓冲区内的数据写入到目标文件
            fos.write(buf);
            //刷新缓冲区
            fos.flush();
 
            //关闭文件
            fos.close();
            fis.close();
            System.out.println("复制成功");
        } catch (IOException e) {
            e.printStackTrace();
        }
 
    }
 
}

案例代码的copyFile()方法完成文件的复制,在复制之前,先判断一下源文件是否存在,然后申请一个byte数组,用于存储读取的源文件数据,该数组大小与源文件总字节数相同,读取成功后,再将数组内容写入到目标文件。

使用FileOutputStream流可以写入字节数据到目标文件,FileOutputStream提供了单字节写入和byte数组写入两种方式。建议使用byte数组写入,将待写入的数据存储到一个byte数组中,然后再写入文件。当写入的文件已经存在时,需要指明写入方式是覆盖还是追加。

代码在线纠错(通义千问 qwen-max)

支持粘贴多个代码文件,提交后由阿里云通义千问自动分析代码漏洞、语法错误、逻辑问题并给出修改建议。
您已解锁 AI 代码纠错功能,可正常使用!

评论区

登录 后发表评论
暂无评论