java中I/O操作主要是指使用‘java.io‘包下的内容,进行输入(Input)、输出(Output)操作。

输入:读取数据

输出:写入数据。

流:数据(字符、字节)

根据数据的流向可以分为2类

  1.   输入流:数据从其他设备读取到内存上的流
  2.   输出流:数据从内存写出到其他设备上的流

一切文件数据(图片,音频,文档)都以二进制形式存储在电脑内,字节即为基本的存储单元,所以都可以用一个一个的字节表示。即一切皆为字节。

有输入数据或者输出数据,便有字节输入流字节输出流

字节输入流和字节输出流皆有顶级父类。字节输入流的顶级父类是java.io包下的InputStream,字节输出流的顶级父类是java.io包下的OutputStream.

输入流 输出流
InputStream OutputStream

这两个类都是抽象类,其中包含了输入流或者输出流的共有的方法。

字节输出流

输入流原理:将10进制数据转换成二进制写入文件中。当文件打开时会查询系统的编码表,将二进制的数据变为字符输出。

OutputStream抽象类下的方法:

1 */    public void close():关闭此输出流并释放与此流相关联的任何系统资源。
2         public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
3         public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
4         public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
5         public abstract void write(int b):将指定的字节输出流。*/

OutputStream的子类重写抽象方法后,可以创建对象调用。最简单的子类FileOutputStream用于将内存中的数据写入文件中。

FileOutputStream的构造方法

  1. public FileOutputStream(String name):传入字符串类型的文件路径
  2. public FileOutputStream(File file):传入文件对象

构造方法作用:1.创建FileOutputStream对象  2.将FileOutputStream对象指向构造方法中要写入的文件

当创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据

数据写入文件的步骤

  1. 创建FileOutputStream对象
  2. 调用write()方法,写入数据
  3. 调用close()方法,关闭输出流,释放系统资源

最简单地调用write(int b)方法,每次写入一个字节的内容,int参数为ASCII码编号。int虽然是4个字节,但是最后只会保留1个字节的内容写出。

        FileOutputStream fileOutputStream = new FileOutputStream("b.txt");//传入相对路径
        fileOutputStream.write(97);//字符a
        fileOutputStream.write(98);//字符b
        fileOutputStream.write(99);//字符c
        fileOutputStream.close();

在b.txt中的结果是 abc。方法调用很简单,但是每次输入一个字节的内容过于低效,利用write(byte[] b)方法可以一次输入多个字节内容

write(byte[] b)方法,将字节数组中全部内容写入到文件中,共b.length(数组长度)个字节

        byte[] bytes={97, 98, 99, 100};
        FileOutputStream fileOutputStream = new FileOutputStream("b.txt");//传入相对路径
        fileOutputStream.write(bytes);//依次写入abcd
        fileOutputStream.close();

write(byte[] b, int off, int len)方法用于部分写入字节数组中内容


offset为偏移量,即相对于数组起始位置的偏移量,len为写入的字节数目

      byte[] bytes={97, 98, 99, 100};
        FileOutputStream fileOutputStream = new FileOutputStream("b.txt");//传入相对路径
        fileOutputStream.write(bytes,1,2);//依次写入bc
        fileOutputStream.close();

批量写入操作可以通过write完成,但是每一次运行都会创建一个FileOutputStream对象,构造方法遇到相同路径会清空原有数据来进行写入,无法进行追加书写。

输出流中的追加与换行

追加

在构造方法中加入第二个参数可以解决这个问题。第二个参数为布尔值,如果布尔值为true,则会追加书写,布尔值为false将会清空原有数据

public FileOutputStream(String name, boolean append)
public FileOutputStream(File file, boolean append)

举例如下:

        byte[] bytes={97, 98, 99, 100};
        FileOutputStream fileOutputStream = new FileOutputStream("b.txt",true);//传入相对路径
        fileOutputStream.write(bytes);//根据上一次运行结果,b.txt中的内容为bc,本次追加书写,结果是bcabcd
        fileOutputStream.close();

 换行:

换行符:\n 到下一行(newline)   回车符:\r 指针回到每一行开头(retrun)

根据操作系统指令不同,换行方式不同。

Windows:\r\n

Unix:\n

MacOS:\r

 字节输入流

InputStream抽象类下的方法:

1 /*
2         public void close() :关闭此输入流并释放与此流相关联的任何系统资源。
3         public abstract int read(): 从输入流读取数据的下一个字节。
4         public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。*/

InputStream的子类重写抽象方法后,可以创建对象调用。最简单的子类FileInputStream用于将文件中的数据读入内存当中。

FileInputStream的构造方法

  1. public FileInputStream(String name):传入字符串类型的文件路径
  2. public FileInputStream(File file):传入文件对象

构造方法作用:1. 创建FileInputStream对象  2.将FileInputStream对象指向构造方法中要读取的目标文件

与FileOutputStream文件输出流不同的是,FileInputStream文件输入流的构造方法必须传入一个实际存在的文件路径或者文件对象,不然会抛出FileNotFoundException。

读取文件数据的步骤

  1. 创建FileInputStream对象
  2. 调用read()方法,读取数据到内存。每一次调用此方法,指针指向下一个字节。
  3. 调用close()方法,关闭输入流,释放系统资源

调用read()方法,每次读取目标文件一个字节的内容,将数据提升为int类型,读到文末的时候返回-1

 1         byte[] bytes={97, 98, 99, 100};
 2         FileOutputStream fileOutputStream = new FileOutputStream("b.txt");//传入相对路径
 3         FileInputStream fileInputStream=new FileInputStream("b.txt");
 4         fileOutputStream.write(bytes);//写入abcd
 5         int read = fileInputStream.read();//第一次调用read方法,读取字节信息提升为int类型返回,指针自动指向下一位
 6         System.out.println((char) read);//读取a
 7         read = fileInputStream.read();//第二次调用read方法,读取字节信息提升为int类型返回,指针自动指向下一位
 8         System.out.println((char) read);//读取b
 9          read = fileInputStream.read();//以上类似于上面注释
10         System.out.println((char) read);
11         read = fileInputStream.read();
12         System.out.println((char) read);
13         fileOutputStream.close();
14         fileInputStream.close();

以上代码演示了read()方法无参调用,可以看出代码有多次重复,效率低下,可以使用循环来代替。

使用循环代替,就必须明确循环体与判断的条件。循环条件可以由read方法在读到文末时候返回-1得出。

        int read;
        while ((read = fileInputStream.read()) != -1) {
            System.out.println((char) read);
        }

此处代码可以用来代替上文中多次重复的代码,因为read方法中会自动将指针指向下一位的特殊性质,每次循环只能调用一次,所以需要定义一个变量来接收read方法的结果,便于循环体中的打印。循环条件中

(read = fileInputStream.read()) != -1同时完成了方法的一次调用和判断。
使用字节数组读取文件数据,调用方法read(byte[] b)
 1         byte[] bytes = {97, 98, 99, 100};
 2         FileOutputStream fileOutputStream = new FileOutputStream("b.txt");//传入相对路径
 3         FileInputStream fileInputStream = new FileInputStream("b.txt");
 4         fileOutputStream.write(bytes);//写入abcd
 5         byte[] b=new byte[3];//创建新字节数组作为存放读取出来的数据的容器
 6         int len;
 7         while ((len=fileInputStream.read(b))!=-1){
 8             System.out.println(new String(b));
 9         }
10         fileOutputStream.close();
11         fileInputStream.close();
12     }

此处有两个注意点:

  1. 新建数组b用于存放读取出来的字节信息,一次读取3(b.length)个字节的数据,并依次存入其中。每一次调用方法指针向下移动3(b.length)位
  2. 整数len用于表示每一次读取的有效位数。第一次读取有效位数3个,第二次有效位数仅1个。
但是代码会存在一个问题:代码显然循环次数2次,第一次循环可以得到正常结果abc,第二循环便会因为read遇到文末出现错误结果dbc

 这个问题产生的原因是赋值操作时,上一次读取的数据没有被完全替换。

为了解决这个问题可以改变String参数的构造方法解决,将上面while循环内部代码修改为如下代码

1         while ((len=fileInputStream.read(b))!=-1){
2             System.out.println(new String(b,0,len));
3         }

String构造方法变为3个参数,len为有效读取字节数,0是b数组偏移量,构造方法是将b数组从0索引开始的len个索引内容放入字符串。第一次l循环len为3放入abc,第二次循环len为1放入d,代码正确。

 

 

只使用字节输入英文和一些基本符号是没有问题的,但是想用字节流输入或读取中文时就会有一些问题,因为一个中文字符占用多个字节。所以java还有以字符为单位进行输入和输出的流,称为字符输入流字符输出流

 

字符输入流(Reader)

java.io.Reader是读取字符流的父类,里面存放了一些共用的读取方法。

 

 

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

 

 

 

FileReader 是Reader的子类,用于读取文件中的字符,构造时使用系统默认的字符编码和默认字节缓冲区。

小贴士:

1. 字符编码:字节与字符的对应规则。Windows系统的中文编码默认是GBK编码表。

idea中UTF-8

2. 字节缓冲区:一个字节数组,用来临时存储字节数据。

FileReader构造方法

  1. FileReader(File file): 创建一个新的 FileReader ,给定要读取的File对象。
  2. FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的名称

 

类似于FileInputStream,FileReader在构造时必须传入有效的文件名或者文件对象

read方法读取字符:基本方法和之前讲的FileInputStream一致

        FileReader fe=new FileReader("b.txt");
        int len;
        while((len=fe.read())!=-1){
            System.out.println((char) len);/将得到的字符结果打印出来
        }

read方法每次读取一个字符,自动将指针指向下一位,读到文末时返回-1。

利用字符数组读取字符调用方法read(char[] cubf)来完成 

        FileReader fe=new FileReader("b.txt");
        char[] chars=new char[3];
        int len;
        while((len=fe.read(chars))!=-1){
            System.out.println(new String(chars));
        }

len仍为有效读取字节个数

新建数组chars用来存放读取出来的字符数据,通过赋值操作获取。与刚才所讲的FileInputStream类似,以上代码会出现上一次循环结束后数组中的数据没有被完全替代的问题。

与之前FileInputStream处理的方法一样,利用String的构造方法来规避这个问题

1         while((len=fe.read(chars))!=-1){
2             System.out.println(new String(chars,0,len));
3         }

String构造方法变为3个参数,len为有效读取字节数,0是b数组偏移量,构造方法是将b数组从0索引开始的len个索引内容放入字符串。第一次l循环len为3放入甲乙丙,第二次循环len为1放入丁,代码正确。

字符输入流(Writer

java.io.Writer抽象类是所有用字符流写出数据的类的父类其中定义了带有共性的方法。

      */  void write(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) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
        void flush() 刷新该流的缓冲。
        void close() 关闭此流,但要先刷新它。*/

java.io.FileWriter 类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区

FileWriter构造方法:

  1. FileWriter(File file): 创建一个新的 FileWriter,给定要读取的File对象。
  2. FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件的名称。

write()方法无参调用,可以写入一个字符的信息,与FileOutputStream类似,但是与字节输出流不同的是,字符输出流必须在write()方法之后使用flush()方法将缓冲区内的数据输入到文件中。

        FileWriter fw=new FileWriter("b.txt");
        fw.write('你');
        fw.write('好');
        fw.flush();
     fw.close();

 当调用close方法时,flush方法会自动调用,将缓冲区内数据写入文件。当我们需要既写出数据到文件中,又要继续使用(不能关闭)流对象时要用到flush()方法。

利用数组可以批量写出字符到文件中,调用write(char[] cbuf)方法

        FileWriter fw=new FileWriter("b.txt");
        char[] chars = "丹尼格林".toCharArray();
        fw.write(chars);
        fw.close();//不需要继续使用流对象,直接关闭自动调用flush方法即可

write(char[] cbuf,int off,int len )可以从数组第off个索引开始写出之后的len个索引的内容,与FileOutputStream类似。

        FileWriter fw=new FileWriter("b.txt");
        char[] chars = "丹尼格林".toCharArray();
        fw.write(chars,2,1);//写入结果为 ‘格’
        fw.close();

 

字节、字符输入流和输出流会在创建变量和调用方法时抛出异常,需要处理之后才可以正常运行。

使用try catch finally处理异常(以字符输出流举例)

 1         FileWriter fw = null;//提升变量作用域并设置一个默认值初始化变量
 2         try {//创建对象,调用write方法会抛出异常(IOException),必须处理了异常才能正常运行
 3             fw = new FileWriter("b.txt");
 4             char[] chars = "丹尼格林".toCharArray();
 5             fw.write(chars, 2, 1);//写入结果为 ‘格’
 6         } catch (IOException e) {
 7             e.printStackTrace();
 8         } finally {
 9             if (fw != null) {//判断对象是否创建成功
10                 try {
11                     fw.close();//close方法也会抛出异常,也需要处理
12                 } catch (IOException e) {
13                     e.printStackTrace();
14                 }
15 
16             }
17 
18 
19         }

finally中必须执行的代码是释放资源代码,保证程序运行的效率。创建对象和调用写出方法都可能产生异常,放入try代码块中。

JDK7中新特性,可以在try后面加入一个括号(),在括号里面定义流对象(可以定义多个对象,相互之间用分号隔开),流对象的作用域仅在try代码块中,try代码块执行完自动关闭释放,不用写finally

1         try (FileWriter fw = new FileWriter("b.txt")) {
2             char[] chars = "丹尼格林".toCharArray();
3             fw.write(chars, 2, 1);//写入结果为 ‘格’
4         } catch (IOException e) {
5             e.printStackTrace();
6         }

JDK9中新特性,可以在try前面定义对象,然后直接在try后面的括号中使用对象的名称,但是在try之前定义对象仍然会抛出异常,需要抛出。

1         FileWriter fw = new FileWriter("b.txt");
2         try (fw) {
3             char[] chars = "丹尼格林".toCharArray();
4             fw.write(chars, 2, 1);//写入结果为 ‘格’
5         } catch (IOException e) {
6             e.printStackTrace();
7         }