Java IO流初相识

304 阅读15分钟

流是一种抽象概念,它代表了数据的无结构化传递。按照流的方式进行输入输出,数据被当成无结构的字节序或字符序列。从流中取得数据的操作称为提取操作,而向流中添加数据的操作称为插入操作。用来进行输入输出操作的流就称为IO流。换句话说,IO流就是以流的方式进行输入输出 。---百度百科

Java中IO流的分类

按照流的流向:

分为输入流和输出流

通常我们说的输入输出是从内存的角度来划分的,例如下图数据从服务端到客户端,客户端的内容负责从网络里读取数据,所以客户端的程序应该使用输入流。

按照操作单元:

分为字节流和字符流

字符流和字节流的用法几乎一致,区别在于字节流操作的数据单元是8位的字节,而字符流次奥组的数据单元是16位的字符。

Java中IO流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在着非常紧密的练习,Java中IO流的40多个类都是从下面四个抽象类基类中派生出来的。

InputStream/Reader:所有的输入流的基类,前者是字节输入流,后者是字符输入流。

OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。

既然有了字节流,为什么还要有字符流?

字符流是有Java虚拟机将字节转换得到的。问题是这个过程比较耗时,而且如果事先不知道编码类型就很容易出现乱码问题,所以IO流就提供了一个直接操作字符的接口,方便对字符进行流的操作。一般情况下,如果是二进制文件(音频,图片等)使用字节流,如果是文本文件使用字符流操作。(二进制文件和文本文件详情可看扩展)

按照流的角色:

分为节点流和处理流

可以从/向一个特定的IO设备(磁盘,网络等)读/写数据的流,称为节点流,节点流也称低级流,从下图中可以看出,当使用节点流进行输入/输出时,程序直接链接到实际的数据源。

处理流则用于时对一个已存在的流进行连接或者封装,通过封装后的流实现数据读/写的功能,处理流也称为高级流如下图,使用处理流进行输入/输出时,程序不会直接连接到实际的数据源,使用处理流的优点是,只要使用相同的处理流,程序就可以采用完全想的输入/输出代码来访问不同的数据源,随着处理流所包装结点流的变化,程序所访问的数据员也相应的发生变化。

Java字节流的基本使用

字节输出流向文件中写一个字节数组

public static void main(String[] args) throws IOException {
    //创建FileOutputStream输出流
    OutputStream os = new FileOutputStream("D:\\demo.txt");
    //调用方法,写入字节数组
    //byte[] bArr = "helloWorld".getBytes();
    //os.write(bArr);
    //写汉字
    //os.write("小舍学Java".getBytes());
    //写字节数组的一部分
    //表示将helloWorld对应的字节数组的一部分写到文件,从索引为1的位置开始写, 写3个
    os.write("helloWorld".getBytes(), 0, 5);//hello
    //释放资源
    os.close();
}

文件的续写

/**
 *     当创建字节输出流对象,并传递字符串文件的时候, 会创建该文件,并且覆盖原来的文件。
 *     如果想要在原来文件后面写内容,要使用其他的构造方法创建流
 *     构造方法:
 *         FileOutputStream(File file, boolean append):第一个参数表示向哪个文件写数据,第二个参数表示是否续写,如果参数是true表示续写
 *         FileOutputStream(String name, boolean append) :第一个参数表示向哪个文件写数据,第二个参数表示是否续写,如果参数是true表示续写
 * @param args
 * @throws IOException
 */
public static void main(String[] args) throws IOException {
    //创建FileOutputStream输出流
    OutputStream os = new FileOutputStream("D:\\demo.txt",true);
    os.write("临渊羡鱼不如退而结网".getBytes());
    //释放资源
    os.close();
}

换行写入

/**
 *     如果要向文件中写入换行,需要使用换行符。
 *     每种操作系统换行符都不同
 *         windows: \r\n
 *         linux: \n
 *         macOS: \r
 * @param args
 * @throws IOException
 */
public static void main(String[] args) throws IOException {
    //创建FileOutputStream输出流
    OutputStream os = new FileOutputStream("D:\\demo.txt");
    os.write("临渊羡鱼\r\n不如退而结网".getBytes());
    //释放资源
    os.close();
}

文件复制

/**
 *  文件复制本质就是读和写, 将源文件中的字节读取出来,然后写到新的文件中即可。
 *
 *     复制步骤:
 *         1. 创建一个字节输入流,用来读取
 *         2. 创建一个字节输出流,用来写。
 *         3. 开始循环读写。 每读取一次数据,就将读取到的数据写到目的地文件。
 * @param args
 * @throws IOException
 */
public static void main(String[] args) throws IOException {
    //创建一个字节输入流,用来读取
    InputStream is = new FileInputStream("D:\\demo.txt");
    //创建一个字节输出流,用来写。
    OutputStream os = new FileOutputStream("D:\\copy.txt");
    //开始循环读写。 每读取一次数据,就将读取到的数据写到目的地文件。
    //一次读取一个字节数组,然后将读取到的字节数组复制到目的地文件。
    byte[] bArr = new byte[1024];
    int len;
    while((len = is.read(bArr)) != -1) {
        //如果条件成立,那么就将读取到的数据写到目的地文件。
        //读取到的数据保存在了bArr这个数组中, 返回值len是读取到的个数。
        os.write(bArr, 0, len); //将bArr中的一部分写入到文件,从索引为0的位置开始写, 写len个
    }
    //释放资源
    os.close();
    is.close();
}

Java字符流的基本使用

使用字符流一次读取一个字符

public static void main(String[] args) throws IOException {
    //创建字符输入流对象, 绑定一个数据源文件。
    Reader r = new FileReader("D:\\demo.txt");
    //调用read方法,读取数据
    //使用一次读取一个字符的方式读取数据
    int i; //定义变量i,用来接收每次读取到的字符。
    while((i = r.read()) != -1) {
        System.out.print((char)i);
    }
    //释放资源
    r.close();
}

使用字符输入流读取一个字符数组

public static void main(String[] args) throws IOException {
    //创建字符输入流对象
    Reader r = new FileReader("D:\\demo.txt");
    //调用read方法进行读取(一次读取一个字符数组)
    //定义一个字符数组,用来接收每次读取到的数据
    char[] cArr = new char[1024];
    //定义变量len,用来接收每次读取到的字符的个数。
    int len;
    //开始循环读取
    while((len = r.read(cArr)) != -1) {
        //读取到几个内容,那么就将几个内容转成字符串输出
        System.out.println(new String(cArr, 0, len));
    }
    //释放资源
    r.close();
}

字符输出流的基本使用

/**
 * FileWriter的使用步骤:
 *         1. 创建一个FileWriter字符输出流
 *         2. 调用write方法写数据。
 *         3. 调用flush方法刷新。
 *         4. 调用close方法关闭。
 *
 *     在所有的流中,字符输出流需要刷新。
 * @param args
 * @throws IOException
 */
public static void main(String[] args) throws IOException {
    //创建FileWriter对象
    Writer w = new FileWriter("D:\\demo.txt");
    //调用write方法写数据。
    //字符输出流写数据的时候,先会把数据放到内存缓冲区中, 并没有直接写到文件中。
    //想要把内存缓冲区中的数据放到文件中,那么需要进行刷新操作。
    w.write("临渊羡鱼不如退而结网");
    //调用flush方法刷新
    w.flush();
    //释放资源
    w.close();
}

字符输出流刷新方法和close方法的区别

flush:做的仅仅是刷新的操作。 流在刷新之后还可以使用。

close:先刷新,然后关闭流。 流在关闭之后就不能使用了。

为什么创建Java IO流后必须关闭

程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源。如果不关闭该资源,那么磁盘的文件将一直被程序引用着,不能删除也不能更改。同时还会浪费系统资源的浪费,与数据库连接类似。所以应该手动调用 close() 方法关闭流资源。

在java7之后,可以使用try-with-resources语句来释放java流对象,其实他就是一个特殊的try...catch语句,从而避免了try-catch-finally语句的繁琐,尤其是在finally子句中,close()方法也会抛出异常,其实就是一个特殊的try...catch语句。

其他流类型

转换流

Java IO流中提供了两种用于将字节流转换为字符流的转换流。其中InputStreamReader用于将字节输入流转换为字符输入流,其中OutputStreamWriter用于将字节输出流转换为字符输出流。使用转换流可以在一定程度上避免乱码,还可以指定输入输出所使用的字符集

InputStreamReader

/**
 *  InputStreamReader构造方法:
 *     InputStreamReader(InputStream in):参数要传递一个字节输入流。  使用这个构造方法创建的转换流对象会采用开发环境的编码去读取数据。
 *     InputStreamReader(InputStream in, String charsetName):第一个参数是一个输入流, 第二个参数是编码的名字,会采用指定的编码进行读取。
 *
 *  InputStreamReader读取数据的方法:
 *     InputStreamReader里面的读取数据的方法和之前字符流读取数据的方法一模一样。
 *
 *   使用步骤:
 *     1. 创建一个InputStreamReader用来指定编码进行读取。
 *     2. 调用read方法进行读取。
 *     3. 释放资源
 *
 * @param args
 * @throws IOException
 */
public static void main(String[] args) throws IOException {
    //创建一个InputStreamReader,指定以UTF-8的方式读取文件
    InputStreamReader isr = new InputStreamReader(new FileInputStream("d:\\demo.txt"), "UTF-8");//如果不指定编码,那么就会以UTF-8的方式读取
    //开始读取
    //开始读取,一次读取一个字符。
    int i;
    while((i = isr.read()) != -1) {
        System.out.print((char) i);
    }
    //释放资源
    isr.close();
}

OutputStreamWriter

/**
 *     OutputStreamWriter也是转换流, 用来写, 可以指定编码写数据。
 *
 *     OutputStreamWriter构造方法:
 *         OutputStreamWriter(OutputStream out): 参数要传递一个字节输出流, 使用这个构造方法创建的转换流对象会采用开发环境的编码写数据。
 *         OutputStreamWriter(OutputStream out, String charsetName): 第一个参数是字节输出流, 第二个参数为指定的编码方式,会以指定的编码去写数据。
 *
 *     OutputStreamWriter里面写数据的方法和字符流写数据的方法一模一样, 以为OutputStreamWriter是字符流的一种。
 *
 *     使用步骤:
 *         1. 创建流
 *         2. 写数据。
 *         3. 刷新。
 *         4. 释放资源
 * @param args
 * @throws IOException
 */
public static void main(String[] args) throws IOException {
    //创建转换流
    OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("d:\\demo.txt"), "utf-8");
    //调用write方法,写数据
    osw.write("临渊羡鱼不如退而结网");
    //刷新
    osw.flush();
    //释放资源
    osw.close();
}

将GBK编码的文本文件,转换为UTF-8编码的文本文件

/**
 *     在读取的时候指定以GBK的方式读取文件, 在写的时候指定以UTF-8的方法写即可。
 *
 *     步骤:
 *         1. 创建InputStreamReader,指定以GBK的方式进行读取。
 *         2. 创建OutputStreamWriter, 指定以UTF-8的方式进行写。
 *         3. 一边读,一边写, 每读取到数据就写到目的地文件中。
 *         4. 刷新
 *         5. 释放资源。
 * @param args
 * @throws IOException
 */
public static void main(String[] args) throws IOException {
    //创建InputStreamReader,指定以GBK的方式进行读取。
    InputStreamReader isr = new InputStreamReader(new FileInputStream("d:\\demo.txt"), "GBK");
    //创建OutputStreamWriter, 指定以UTF-8的方式进行写。
    OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("d:\\copy.txt"), "UTF-8");
    //一边读,一边写, 每读取到数据就写到目的地文件中。
    int i;
    while((i = isr.read()) != -1) {
        //将读取出来的数据写到新的文件中
        osw.write(i);
        //刷新
        osw.flush();
    }
    //释放资源
    osw.close();
    isr.close();
}

缓冲流

缓冲流是处理流的一种, 它依赖于原始的输入/输出流, 缓冲流它的特点是可以提高读写的效率, 原因是因为内部有一个缓冲区, 这个缓冲区可以起到加速的作用。缓冲流其实本身并不具备读或者写的功能, 它的作用是给其他流提供加速。

/**
 * 字节缓冲流的构造方法
 *         BufferedOutputStream(OutputStream out): 参数要传递一个字节输出流。
 *         BufferedInputStream(InputStream in): 参数要传递一个字节输入流。
 *
 *     字节缓冲流的使用步骤:
 *         1. 创建字节缓冲流。
 *         2. 读或者写
 *         3. 释放资源
 * @param args
 * @throws IOException
 */
public static void main(String[] args) throws IOException {
    //创建字节输入缓冲流,用于读取
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\demo.txt"));
    //创建字节输出缓冲流,用来写入
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\copy.txt"));
    //开始读写,一次读写一个字节。
    int i;
    while((i = bis.read()) != -1) {
        bos.write(i);
    }
    //释放资源
    bos.close();
    bis.close();
}

字符缓冲流中的特有的功能

在BufferedWriter中有一个方法,可以实现一个跨平台的换行:void newLine() 实现一个跨平台的换行

打印流

PrintStream是打印流, 打印流是输出流的一种, 打印流有下面特点:

  1. 打印流只有输出,没有输入。
  2. 输出数据十分方便。
/**
 *
 *     PrintStream的构造方法:
 *         PrintStream(String fileName): 参数要传递一个字符串类型的文件路径。
 *         PrintStream(File file): 参数要传递一个File类型的文件。
 *         PrintStream(OutputStream out): 传递要传递一个字节输出流。
 *
 *     PrintStream写数据的方法(特有的方法):
 *         void print(任意类型): 输出数据,但是不会换行。
 *         void println(任意类型): 输出数据,但是会换行。
 *
 *     使用步骤:
 *         1. 创建打印流
 *         2. 写数据。
 *         3. 关流
 * @param args
 * @throws IOException
 */
public static void main(String[] args) throws IOException {
    //创建打印流
    PrintStream ps = new PrintStream("D:\\demo.txt");
    ps.println("临渊羡鱼");
    ps.println("不如退而结网");
    //释放资源
    ps.close();
}

拓展

编码表

编码表是由现实世界的字符和其对应的数值组成的一张表,用来解析和转换各种字符,是一种代码说明表格。作用是用来帮助用户明确无解释数据和字符代码的含义。

大家都知道在计算机中保存的数据都是字节,而我们在计算机中看到的是字符(汉字,英文),这是因为在打开文件时编辑工具默默的进行了解码。

字节(Byte):指一小组相邻的二进制数码,二进制数据的单位。 一个字节通常8位长,是一个很具体的存储空间。0x01, 0x45, 0xFA……

字符:是指计算机中使用的字母、数字、字和符号,通俗的说就是人们使用的记号,抽象意义上的一个符号。包括:1、2、3、A、B、C、~!·#¥%……—*()+等等。

编码:字符 -> 字节,字符转化为字节称为编码

解码:字节 -> 字符,字节转化为字符称为解码

乱码:因为文件存的格式和读取格式不一致就会乱码了。简单的说乱码的出现是因为: 编码 (encode) 和 解码(decode) 时用了不同或者不兼容的字符集。

不同编码标准里,字符和字节的对应关系不同。

ASCII码中:一共有128个字符(常见的英文字母标点符号等)

  1. 一个英文字母(不分大小写)占一个字节的空间;
  2. 一个中文汉字占两个字节的空间。

Unicode编码中:

  1. 一个英文字符等于两个字节;
  2. 英文标点占一个字节;
  3. 一个中文(含繁体)等于两个字节;
  4. 中文标点占两个字节.

UTF-8编码中:

  1. 一个英文字符等于一个字节;
  2. 英文标点占一个字节
  3. 一个中文(含繁体)等于三个字节
  4. 中文标点占三个字节,

UTF-16编码中:

  1. 一个英文字母字符需要2个字节
  2. 一个汉字字符存储需要2个字节(Unicode扩展区的一些汉字存储需要4个字节)。

UTF-32编码中:

  1. 世界上任何字符的存储都需要4个字节。

二进制文件和文本文件

大家都知道计算机的存储在物理上是二进制的,所以文本文件与二进制文件的区别并不是物理上的,而是逻辑上的。这两者只是在编码层次上有差异。

简单来说,文本文件是基于字符编码的文件,常见的编码有ASCII编码,UNICODE编码等等。二进制文件是基于值编码的文件,你可以根据具体应用,指定某个值是什么意思(这样一个过程,可以看作是自定义编码)。

广义的二进制文件即指文件,由文件在外部设备的存放形式为二进制而得名。狭义的二进制文件即除文本文件以外的文件。

第一是二进制文件比较节约空间,这两者储存字符型数据时并没有差别。但是在储存数字,特别是实型数字时,二进制更节省空间;第二个原因是,内存中参加计算的数据都是用二进制无格式储存起来的,因此,使用二进制储存到文件就更快捷。如果储存为文本文件,则需要一个转换的过程。在数据量很大的时候,两者就会有明显的速度差别了。第三,就是一些比较精确的数据,使用二进制储存不会造成有效位的丢失问题。