1. IO流

IO流.png

Java中不同的输入和输入源都抽象表述为流(stream),通过流的方法允许程序可以使用相同的方法访问不同的输入和输出源。Java中流可以分为:

输入流 输出流
字节流 InputStream OutputStream
字符流 Reader Writer

2. 字节流

在具体学习字节流中的输入流和输出流之前,我们需要明白一个本质概念:一切皆为字节!根据计算机组成原理中的知识可知,任何类型的数据在计算机中只能以二进制的形式存储,表示为一个一个字节的形式。

因此,字节流就可以实现任何形式文件的传输,只需要关注所作的操作类型,而不必在意文件的类型。

2.1 OutputStream

java.io.OutputStream表示字节输出流,它是表示输出字节流的所有相关类的超类。
image-20200503195152970.png

共性的成员方法:

  • public void close():关闭此输出流并释放与此相关的任何系统资源

    关闭一个输出流的同时还会冲刷用于该输出流的缓冲区,所有被临时置于缓冲区中,以便用更大的包的形式传递的字节在关闭输出流时都会被送出。

  • public void flush():刷新此输出流并强制任何缓冲的输出字节被写入
  • public void write(byte[] b): 将b.length的字节从指定的字符数组写入此输出流
    • 如果写的第一个字节是正数(0-127),那么显式的时候会查询ASCII表
    • 如果写的第一个字节是负数,那么第一个字节和第二个字节共同组成一个中文显式,查询默认码表(GBK)
  • public void write(byte[] b, int off, int len): 从指定的字节数组写入len长度的字节,从偏移量off开始输出到此输出流中
  • public abstract void write(int b):将指定的字节输出流

下面我们看一个它的子类FileOutputStream,它表示文件字节输出流,作用是把内存中的数据写入到硬盘的文件中。FileOutputStream中的构造方法有:

  • FileOutputStream(String name):创建一个向指定名称的文件中写入数据的输出文件流
  • FileOutputStream(File file):创建一个向指定的File对象表示的文件中写入数据的文件输出流
  • FileOutputStream(string name, boolean append):append为追加写开关
  • FileOutputStream(File file, boolean append):append为追加写开关

通过构造方法会创建一个FileOutputStream对象,并根据传递的参数创建一个空文件,最后将FileOutputStream对象指向创建好的文件。

  1. import java.io.FileOutputStream;
  2. import java.io.IOException;
  3. public class OutputStreamTest {
  4. public static void main(String[] args) throws IOException {
  5. FileOutputStream fos = new FileOutputStream("test.txt");
  6. // 一次写一个字节
  7. // fos.write(68);
  8. // 一次写多个字节
  9. byte[] b = {65, 66, 67, 68, 69}; // ABCDE
  10. // byte[] b = {-65, 66, 67}; // 緽C
  11. // fos.write(b, 0, 2);
  12. fos.write(b);
  13. fos.close();
  14. }
  15. }

2.2 InputStream

java.io.InputStreams是字节输入流,它是所有表示字节输入流类的超类。共性的成员方法有:

  • int read():从输入流中读取数据的下一个字节
  • int read(byte[] b):从输入流中读取一定数量的字节,并将其存储在缓冲区数组b中
  • void close():关闭此输入流并释放与该留相关的所有系统资源

同样看其中的一个子类FileInputStream,它的作用是将把硬盘文件中的数据读取到内存中使用。构造方法有:

  • FileInputStream(String name)
  • FileInputStream(File file)

通过构造方法创建FileInputStream对象,并把FileInputStream对象指定构造方法中读取的文件。

字节输入流的使用步骤:

  • 创建FileInputStream对象,构造方法中指定要读取的数据源
  • 使用FileInputStream对象的read方法,读取文件
  • 释放资源

根据InputStreams中读取数据的方法,我们可以使用三种方法读取:

  • 一次读取一个字节 ```java import java.io.FileInputStream; import java.io.IOException;

public class InputStreamTest { public static void main(String[] args) throws IOException { FileInputStream fis = new FileInputStream(“test.txt”); int len = 0; // -1表示文件的结束符 while ((len = fis.read()) != -1){ System.out.println(len); // 65 66 67 68 69 } fis.close(); } }


- 一次读取多个字节
```java
import java.io.FileInputStream;
import java.io.IOException;

public class InputStreamTest {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("test.txt");
        byte[] b = new byte[2];
        int len = 0;
        while((len = fis.read(b)) != -1){
            System.out.println(len); // 2 2 1
            System.out.println(new String(b)); // AB  CD ED
        }
        fis.close();
    }
}

String构造方法:

  • String(byte[] bytes):把字节数组转换为字符串
  • String(byte[] bytes, int offset, int length):把字节数组的一部分转换为字符串


那么FileInputStream是如何读取多个字节的数据呢?下面通过图来直观的感受一下: 字节输入流读取数据流程图.png

我们使用Java程序读取硬盘中的数据,中间需要JVM和操作系统的参与,这里为了简便只关注两端的情况。当使用read(byte[] b)方法读取多个字节时,首先要新建一个byte类型数组的缓冲区,每一次读取数据都更新数组中的值,并移动读取硬盘数据的指针,直到遇到文件的结束符-1。
特别是读取完CD之后,后续有效数据只有E一个,因此第四次读取中只更新数组的第一个元素,数组的输出为E D。

  • 一次读取全部字节或一部分

    • byte[] readAllBytes() :读取流中所有的字节
    • int readNBytes(byte[] b, int off, int len):读取流中指定数量的字节

      private static void read3() throws IOException {
      FileInputStream fis = new FileInputStream("test.txt");
      
      byte[] bytes = fis.readAllBytes();
      System.out.println(Arrays.toString(bytes));  // [98, 57, 55, 13, 10]
      }
      

除了读取流中字节数据外,InputStream中的long transferTo(OutputStream out)可以实现将当前输入流中的所有字节传送到指定的输出流。

    private static void read4() throws IOException{
        FileInputStream fis = new FileInputStream("test.txt");
        FileOutputStream fos = new FileOutputStream("test1.txt");
        long l = fis.transferTo(fos);
        System.out.println(l);

        fis.close();
        fos.close();

    }

其他的一些方法:

  • int available() :返回在不阻塞的情况下可获取的字节数
  • long skip(long n) :在输入流中跳过n个字节,返回实际跳过的字节数
  • void mark(int readlimit):在输入流的当前位置打标记
  • boolean markSupported(): 判断当前输入流是否支持打标记

3. 字符流

前面使用字节流读取和保存英文数据时不会有什么问题,但是中文数据每个字通过都是用多个字节保存,因此如果使用字节流会出现读取和保存不完整字符的问题。因此,更好的方法就是使用字符流。

3.1 Reader

Java.io.Reader时一个抽象类,它是用于读取字符流的所有类的超类,作用是读取信息到内存 中。实现类共性的成员方法有:

  • public void close():关闭此流并释放于此六相关联的任何系统资源
  • public int read():从输入流中读取一个字符
  • public int read(char[] cbuf):从输入流中读取一些字符,并将它们存储到字符数组中

这里同样先看和文件相关的实现类FileReader ,它负责把硬盘文件中的数据以字符的性质读取到内存中。构造方法:

  • FileReader(String filename)
  • FileReader(File file)
import java.io.FileReader;
import java.io.IOException;

public class ReaderTest {
    public static void main(String[] args) throws IOException {
        FileReader fr = new FileReader("test.txt");
//        int len = 0;
//        while((len = fr.read()) != -1){
//            System.out.println((char)len);
//        }

        char[] c = new char[10];
        int len = 0;
        while((len = fr.read(c)) != -1){
            System.out.println(new String(c, 0, len));
        }
        fr.close();
    }
}

它使用上和字节流并没有太大的区别,所以对比字节流很容易明白Reader中方法的使用。

3.2 Writer

java.io.Writer表示字符输出流,它是所有字符输出流的最顶层的父类,同样它也是一个抽象类。实现类共性的成员方法:

  • void writer(int c): 写入单个字符
  • void write(char[] cbuf): 写入字符数组
  • abstract void write(char[] cbuf, int off, int len):写入字符数组的某一部分,off为开始索引,len为写入长度
  • void write(String str): 写入字符串
  • void write(String str, int off, int len): 写入字符串的某一部分
  • void flush():刷新该流中的缓冲
  • void close():关闭流对象

flush()close()的区别:

  • flush() : 刷新缓冲区,流对象可以继续使用
  • close() : 先刷新缓冲区,然后系统释放资源,流对象不可以再被使用了

FileWriter表示文件字符输出流,作用是把内存中字符数据写入到文件中。构造方法:

  • FileWriter(File file)
  • FileWriter(File, boolean append)
  • FileWriter(String filename)
  • FileWriter(String filename, boolean append)
    字符输出流的使用步骤:
  • 创建FileWriter对象,构造方法中绑定要写入数据的目的地
  • 使用FileWriter中的write方法 ,把数据写入到内存缓冲区中(字符转换为字节的过程)
  • 使用FileWriter中flush方法,把内存中缓冲区中的数据,刷新到文件中
  • 释放资源(会先把内存缓冲区中的数据刷新到文件中)
import java.io.FileWriter;
import java.io.IOException;

public class WriterTest{
    public static void main(String[] args) throws IOException {
        FileWriter fw = new FileWriter("test.txt", true);
        fw.write(97);
        char[] c = {'1', 'b', 'c'};
        fw.write(c);
        fw.write(c, 0, 2);
        fw.write("AAAAA");
        fw.write("AAAAA", 0, 2);
        fw.close();
    }
}

4. IO流中的异常处理

不管是使用字节流还是字符流,程序都会抛出IOException。我们上面只是简单的使用throws关键字将其抛出,将异常交给JVM处理。同样我们也可以使用try-catch语句块进行自行处理。

import java.io.FileWriter;
import java.io.IOException;

public class WriterTest {
    public static void main(String[] args){
        FileWriter fw = null;
        try{
            fw = new FileWriter("test.txt", true);
            for (int i = 0; i < 10; i++) {
                fw.write(97 + "\r\n");
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally {
            try {
                fw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

但是上面的写法看起来有点复杂,我们可以使用JDK7之后出现的新特性简化书写。我们在try的后便可以增加一个(),在括号中可以定义流对象,因此,这个流对象只在try中有效。当try中的代码执行完毕,会自动的把该流对象释放,不用写finally手动释放。使用格式为:

  try(定义流对象...){
        可以会抛出异常的代码
    } catch (){
        异常处理逻辑
    }
import java.io.FileWriter;
import java.io.IOException;

public class WriterTest {
    public static void main(String[] args){
        // JDK7新特性使用
        try(FileWriter fw =  new FileWriter("test.txt", true) ){
            for (int i = 0; i < 10; i++) {
                fw.write(97 + "\r\n");
            }
        } catch (IOException e){
            e.printStackTrace();
        }
    }
}

5. Properties集合

java.util.Properties集合继承了HashTable并实现了Map接口。Properties类表示一个持久的属性集,可保存在流中或从流中加载,它是一个唯一和IO流相结合的集合。Properties集合是一个双列集合,key和value默认都是字符串。

  • 可以使用Properties集合中的方法store,把集合中的临时数据持久化写入到硬盘中存储
  • 可以使用Properties集合中的方法load,把硬盘中保存的文件读取到集合中使用

常用的成员方法:

  • Object setProperty(String key, String value):相当于HashTable的put()
  • String getProperty(String key):通过key找到value值,相当于Map集合中的get(key)方法
  • Set<String> stringPropertyNames():返回此属性列中中的键集,其中该键及其对应值是字符串,相当于Map集合中的keySet()
import java.io.*;
import java.util.Properties;
import java.util.Set;

public class PropertiesTest {
    public static void main(String[] args) throws IOException {
        Properties pro = new Properties();
        pro.setProperty("Forlogen", "10");
        pro.setProperty("kobe", "24");
        Set<String> set = pro.stringPropertyNames();
        for (String key:set) {
            System.out.println(key + " = " + pro.getProperty(key));
        }

        /*
        #store data
        #Sun May 03 19:28:02 CST 2020
        kobe=24
        Forlogen=10
        */
    }
}
  • void store(OutputStream out, String comments)
  • void store(Writer writer, String comments)
    参数:
    • OutputStream:字节输出流,不能写入中文
    • writer:字符输出流,可以写入中文
    • String comments:注释,不能使用中文,默认为Unicode编码,一般使用空字符串

使用步骤:

  1. 创建Properties集合对象,添加数据
  2. 创建字节输出流。字符输出流对象,构造方法中绑定输出目的地
  3. 使用Properties集合中的store方法把集合中的数据持久化写入到硬盘中存储
  4. 释放资源 ```java import java.io.*; import java.util.Properties; import java.util.Set;

public class PropertiesTest { public static void main(String[] args) throws IOException { Properties pro = new Properties(); pro.setProperty(“Forlogen”, “10”); pro.setProperty(“kobe”, “24”); FileWriter fw = new FileWriter(“test.txt”); pro.store(fw, “store data”); fw.close(); } }


- `void load(InputStream inStream)`
- `void load(Reader reader)`
<br />参数:
   - InputStream:字节输入流,不能读取含有中文的键值对
   - reader:字符输入流,能读取含有中文的键值对


使用步骤:

   - 创建Properties集合对象
   - 用Properties集合对象中的load方法读取保存键值对的文件
   - 遍历Properties集合
> NOTE:
> - 存储键值对的文件中,键与默认的连接符号可以使用空格
> - 存储键值对的文件中,可以使用#进行注释,被注释的键值对不会再被读取
> - 存储键值对的文件中,键与值默认都是字符串,不用再加引号


```java
import java.io.*;
import java.util.Properties;
import java.util.Set;

public class PropertiesTest {
    public static void main(String[] args) throws IOException {
        Properties pro  = new Properties();
        pro.load(new FileReader("test.txt"));
        Set<String> set = pro.stringPropertyNames();
        for(String key:set){
            System.out.println(key + " = " + pro.getProperty(key));
        }
    }
}