Java IO流学习记录

1 篇文章 0 订阅
1 篇文章 0 订阅

本文为个人学习所记录的笔记,每个字都是自己纯手打,文本内容更多的也是自己的理解。如在文中有发现错误、遗漏或讲得不清楚的地方都请各位大佬不吝指出!

IO流

IO是英文Input和Output的缩写,意思是输入与输出。在一个软件系统中,运行中的程序需要与硬盘进行交互,将程序内的数据写入到硬盘中,才能实现数据的持久化存储,这就是输出Output。
同样地,若想读取在硬盘中被持久化存储的数据,也需要某种方式,将其读入程序、内存中,这种从硬盘到内存的过程我们称之为输入Input,它是与输出相反的。

File类

File类用于表示一个文件对象,可以是文件或文件夹。其构造方法的参数为文件/文件夹的路径,可以是相对也可以是绝对。使用File类,可以很方便地对文件或文件夹进行各种操作,如创建、删除、获取文件名称等。

在之后要学到的输入输出流中,我们也可以使用File类对象来作为输入或输出流的文件路径

File类的一些方法(对某文件的操作)

获取绝对路径:
getAbsolutePath()

获取文件名:
getName()

获取File构造参数的内容:
getPath()

获取文件长度
length()

创建文件或文件夹,返回布尔值
createNewFile()

删除文件/文件夹
delete()

判断文件是否存在
exists()

判断是否为文件:
isFile()

判断是否为目录(文件夹):
isDirectory

获取文件夹下的所有文件和目录的**名字**:
list()
注意获取的是名字。其返回字符串数组,所以应用String[] 来接收

获取文件夹下的所有文件和目录的File对象
listFile()
应该使用File[] 来接收返回的结果

字节流

在Java中,实现流的输入与输出有两种方式:一种是字节流、一种是字符流。在处理用记事本打开,里面是一堆看不懂的机器码文件的时候,应该使用字节流;而在处理人类读的懂的文字时,用字符流效率会更高

字节流写数据

字节流写数据的实现类是FileOutputStream,它继承于OutputStream抽象基类。

字节流写数据的实现

如何使用字节流进行数据的写呢?看:

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

public class IOTest {
    public static void main(String[] args) throws IOException {
        //在实例化对象时,它会检测该文件是否存在,如果不存在则会自动创建一个
        FileOutputStream fos = new FileOutputStream("./test.txt");
        //字节流写数据,write内写入的是ASCII值,一次write只能写一个字符
        fos.write(57);
        fos.write(54);
        fos.write(65);
        //在输入输出操作完成后,必须使用close来释放资源
        fos.close();
    }
}

需要注意的几个点:

  1. 在实例化时,构造参数可以填写File类对象,也可以填写路径名字符串
  2. 若FileOutputStream所指向的文件对象不存在,则会自动创建该文件。仅限输出流,输入流无法创建
  3. 字节流的write一次只能写一个字符,而且参数必须为字符对应的ASCII值
  4. 输入输出流使用完毕后务必记得close释放资源
改进的字节流写入数据

一次只能写一个字符,效率之低下可想而知。write方法除了可以写入单个字符,还可以写入字符数组,不止如此,它还可以指定输出的字符数组输出的偏移量(起始索引)和输出长度。

void write(byte[] b)方法:

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

public class IOTest {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("./test.txt");
        // 使用byte字节类型数组来存储编码后的字符串,写入到文件中
        byte[] bys = "Tank:96A".getBytes();
        //此时的write接收的是byte[]型变量
        fos.write(bys);
        fos.close();
    }
}

关于 getBytes():该方法的作用是将字符串进行编码。编码后,会得到一个字节数组来存储编码后的字符数据。如果未指定字符集,则使用平台默认的字符集

编码后的数据需要使用String来进行解码。具体操作为:String s = new String(bys);
传入的参数为使用getBytes()编码后的字节数组对象,若未指定字符集,则使用平台默认字符集

当然,为了简洁,对于write我们还可以直接这么写:

fos.write("Tank:96A".getBytes());

如果只希望输出该字符串的一部分地方,可以加上偏移量和长度参数:

fos.write("Tank:96A".getBytes(),5,3);

这里的含义是:从字符索引5(包括)开始,往后总共读取3个字符
也就是说,这里的读取结果为:96A

字节流写数据的一些其他小问题
1. 如何换行?

使用换行符即可,但需要注意,在不同操作系统中,换行符是不一样的

for(int i = 0; i < 10; i++){
    fos.write("Tank:96A".getBytes());
    fos.write("/r/n".getBytes());
}
操作系统对应换行符
Windows/r/n
Linux/n
Mac/r

如果换行符没有写对,在IDEA中打开记事本可以看到正确的结果,但在使用操作系统中自带的记事本查看文本时,就会看不到换行的效果。

2. 如何追加内容

上面的写入数据都是覆盖写入,那么如何追加写入呢?
很简单,只需要在FileOutputStream的第二个构造参数写上true即可:

FileOutputStream fos = new FileOutputStream("./test.txt",true);

第二个参数append的作用是,设定是否追加内容,如果为true则追加,如果不写就默认为false,也就是覆盖原内容

3. 关于异常处理

先看示例代码:

public static void main(String[] args) {
        //1. fos分别在try和finally两个代码块中被使用,在不同代码块中,该fos相对另一个代码块都是局部变量。因此需要提前先在外部对fos进行声明,先指向一个null
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("./test.txt");
            fos.write(65);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //2. 这里在close前,需要先判断fos是否为空,也就是判断fos是否已经指向了某个地址。否则会发生空指针异常
            if (fos != null) {
                try {
                    //3. 这里的close也需要处理异常,所以这里要嵌套一个try、catch
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
根据演示代码可知,在处理字节输出流的异常时,需要注意这些问题:
  1. fos分别在try和finally两个代码块中被使用,在不同代码块中,该fos相对另一个代码块都是局部变量。因此需要提前先在外部对fos进行声明,先指向一个null
  2. 在close前,需要先判断fos是否为空,也就是判断fos是否已经指向了某个地址。否则会发生空指针异常
  3. close也需要处理异常,所以这里要嵌套一个try、catch

字节流读数据

上面是字节流写入数据。与读相对应的是写数据,也就是将外部的文件数据写入程序内部
字节流读数据的实现类是FileInputStream,它继承于InputStream抽象基类。

字节流读取数据的基本实现:
import java.io.FileInputStream;
import java.io.IOException;

public class IOTest011 {
    public static void main(String[] args) throws IOException {
        //创建字符输入流对象,与字符输出流一样的创建形式
        FileInputStream fis = new FileInputStream("./test.txt");
        //读入一个字符
        int byt = fis.read();
        //一次只能读入一个字符,且读入的结果是字符对应的ASCII码
        System.out.println(byt);
        //若想输出正确结果,需要强制类型转化为char
        System.out.println((char)byt);
    }
}

和字节输出流一样,一次只能读入一个字节。若想读入多个字符就要执行多次read()方法。
这里以读入外部文本中的字符“96A”为例:

//创建字符输入流对象
FileInputStream fis = new FileInputStream("./test.txt");
//读多个字符,一次read读取一个。
int byt = fis.read();
//需要注意的是,这里必须使用print,不能使用println,否则会换行,使文字不能完整
System.out.print((char)byt);
byt = fis.read();
System.out.print((char)byt);
byt = fis.read();
System.out.print((char)byt);
//这里故意多读取一次,看会发生什么
byt = fis.read();
System.out.print((char)byt);

关于read()方法:
read方法每执行一次,其指针就会向后移动一位,指向下一个字符。初始时read指针是指向头部的(第一个字符之前的位置), 在执行read后,指针向后移动一位,指向第一个字符,并读入。

查看结果可以发现,我们成功读取了96A。
但同时可以看到在最后多了一个-1,这就是多读取一次的结果。
也就是说,当字符串已经被读取完,没有内容读取的时候,read方法就会返回-1值。根据这个思路,我们或许可以设计一个循环语句,让读取数据的代码更简洁更方便

优化字节流读取数据

上面说到,我们可以根据字节输入流的read方法读取空字符返回值为-1的特性,去设计一个循环,使代码更简洁(自行准备一个文本文件):

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

public class IOTest011 {
    public static void main(String[] args) throws IOException {
        //创建字符输入流对象
        FileInputStream fis = new FileInputStream("./test.txt");
        //读入第一个字符,并存入一个整型变量中
        int ch = fis.read();
        //判断:如果ch不等于-1,也就是说字符读进来还有内容,就执行循环,读取字符
        while(ch != -1){
            //先输出一个字符,再进行读取,直到出现-1时停止循环
            System.out.print((char)ch);
            ch = fis.read();
        }
    }
}

这样一来,代码就简洁高效了很多。

但我们依旧可以再进行优化:
import java.io.FileInputStream;
import java.io.IOException;

public class IOTest011 {
    public static void main(String[] args) throws IOException {
        //创建字符输入流对象
        FileInputStream fis = new FileInputStream("./test.txt");
        //初始化一个变量,用于临时存储字符数据
        int ch = 0;
        /*
        * 关键点,这里的代码可以看成三个步骤执行:
        * 1. 先执行fis.read(),读取一个字符,指针从头部移动至第一个字符处,并读入
        * 2. 将读入的结果——字符的ASCII值赋值给临时存储字符数据的ch变量
        * 3. 判断ch是否不等于-1,也就是判断是否已经到末尾,如果没有到末尾就把字符输出出来,并强制转化为char类型
        * */
        while((ch=fis.read()) != -1){
            System.out.print((char)ch);
        }
    }
}

需要特别注意的地方是while里面的写法,先执行哪个后执行哪个必须搞清楚。这种写法是最终的写法,在之后的其他流读入数据的方式中都会用到该写法。

使用读取字节数组的方式读取数据

使用读取单个字节的方式读取数据,效率是比较低下的。我们可以使用一次读取一个字节数组的方式增加读取效率
(自行准备一个文本文件)

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

public class IOTest012 {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("./test.txt");

        //定义一个字节数组byt,长度设定为1024的整数倍
        byte[] byt = new byte[1024];
        //len代表的含义:表示读入了长度为多少的字符串
        int len = fis.read(byt);
        //要正常输出读入的字符串,需要使用String,传入byte数组,然后会将其内容(ASCII码)解码为字符
        String str = new String(byt);
        System.out.println(str);
        fis.close();
    }
}

需要注意的是,如果内容的长度少于字节数组长度,超出的部分会读取到-1进来。
因此,在输出字符数组中的内容时,为了避免读取多余内容,我们不应只给String传入单一的字符数组参数,而应该再加上偏移量和长度两个参数,像这样:

String str = new String(byt,0,len);

这样一来,就可以确保正确无误地读取数据了。

同样地,读取方法也可以进行改进优化:
import java.io.FileInputStream;
import java.io.IOException;

public class IOTest012 {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("./test.txt");

        //定义一个字节数组byt,长度设定为1024的整数倍
        byte[] byt = new byte[1024];
        //len代表的含义:表示读入了长度为多少的字符串
        int len = 0;

        while((len=fis.read(byt)) != -1){
            System.out.print(new String(byt,0,len));
        }
        fis.close();
    }
}

这是字符数组读取数据的最终版本,也就是直接在while判断条件中进行读取和赋值。这种写法在数据读入中很常用,务必需要掌握。

字节缓冲流

在上面说到的字节流中,每一次的数据读取都需要对系统操作进行一次调用,效率会比较低下
而使用字节缓冲流能够很好地解决这个问题。

官方文档对于字节缓冲输出流的解释:
通过设置这样的输出流,应用程序可以向底层输出流写入字符,而不必对写入的每个字节导致底层系统的调用

字节缓冲流的原理是:先将数据读入到缓冲里面,然后再将缓冲中的数据一次性写到外部,这样就避免了对于底层系统的频繁调用,调高了效率

接下来演示一个使用字节缓冲流来实现文件复制的案例(自行准备一个大一点的文件):

import java.io.*;

public class IOTest013 {
    public static void main(String[] args) throws IOException {
        //使用字节缓冲输入输出流,字节缓冲流的构造参数接收的是普通字节流对象
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\Game\\Undertale v1.001.rar"));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("C:\\Users\\Triomhpe\\Desktop\\Undertale.rar"));

        //剩下的操作与普通字节输入输出流一样,使用字节数组的方式读写
        byte[] bys = new byte[1024];
        int len = 0;
        while((len = bis.read(bys)) != -1){
            bos.write(bys,0,len);
        }

        //一定记得释放资源,只需要释放缓冲流的资源即可
        bis.close();
        bos.close();
    }
}

基本上与普通的字节输入输出流操作一致,只需要注意,在字节缓冲流的构造参数需要接收的是字节流对象。

使用字节缓冲流来进行IO操作,可以获得更高的读写效率。

字符流

在使用字节流读取数据时,对于英文、数字的读取是没有问题的,但对于中文等字符的读取,若想要一个个字符地读取,然后直接输出到控制台中,会出现编码的问题(就是乱码)。

而如果想使用读取字节数组的方式来对中文字符进行读写,则需要编码与解码(getBytes与String),比较麻烦。因此,为了更高效率地读取字符,字符流就出现了。

字符流输出流

与字节流一样,字符流也有字符输入流和字符输出流,而且也类似字节流,它们分别继承Reader和Writer两个抽象基类。

字符输出流的实现

实现字符输出流的类为OutputStreamWriter,其需要传入一个FileOutputStream的对象作为构造参数。
这种用法的大致思路是:首先使用字节流读入字节,然后根据每个字节和编码方式,字符流自动将其转化为字符。

OutputStreamWriter有两个构造参数,其中一个为上述所述的字节流对象,还有一个为字符集,也就是编码表。第二个参数是可选的,如果不写,默认为开发平台所使用的编码(如IntelliJ的编码默认为UTF-8,在右下角可以看到)

现在来实现不同字符流写字符的方法(自行准备一个文本文件):

import java.io.*;

public class IOTest021 {
    public static void main(String[] args) throws IOException {
        //使用字符流来读写数据,省去了字节流的编码解码过程,快捷方便了不少
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("./test.txt"),"UTF-8");

        //三种写字符char的方法

        //写单个字符,要输入ASCII值
        osw.write(97);
        //写字符数组
        char[] chs = {'a','b','c','d','e'};
        //直接写入
        osw.write(chs);
        //选择性地写入,和String中一样。不同的是第三个参数没有括号
        osw.write(chs,0,chs.length);

        //两种写字符串String的方法
        osw.write("I want 油 我想抽");
        osw.write("I want 油 我想抽",0,"I want 油 我想抽".length());
        //这个是刷新缓冲区操作,字符流必须要通过刷新缓冲区,才能将数据写入
        osw.flush();
        //关闭输出流之前,会先执行刷新缓冲区的操作,以保证数据正常写入
        osw.close();
    }
}

这里就使用字符流实现了不同的写操作。其实总得来说,无论是用String来写入,还是用char来写入,语法上基本都是一致的。设计者肯定也是考虑到为了让开发者便捷使用,才使语法一致化。

需要特别注意:在使用字符流写入数据后,必须要执行flush刷新缓冲区,数据才可被写入。当然,也可以直接执行close,因为执行close后,系统会先刷新缓冲区再释放资源。

案例(自行准备一个文本文件):

字符输入流
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class IOTest022 {
    public static void main(String[] args) throws IOException {
        InputStreamReader isr = new InputStreamReader(new FileInputStream("test.txt"));

        int len = 0;
        //和字节流的思路一样,这里使用一个字符数组来做存储
        char[] c = new char[1024];
        while((ch=isr.read(c)) != -1){
            //同样地,String也有处理char数组的构造方法
            System.out.println(new String(c,0,len));
        }
    }
}

没什么好说的,与字节流的操作一致,只不过是字节数组换成了字符数组,不然怎么叫字符流呢?

字符流改进与优化

在上面的案例中,每次定义字符输入和输出流的时候,都要写很长一串,还要在里面嵌套一个FileInputStream实现,作为其构造参数传入,非常麻烦。

为了简化这一大串代码,FileReader和FileWriter就有了。
如果在不考虑字符编码的情况下(如使用默认编码)使用字符流,建议直接使用FileReader和FileWriter会更简洁和方便。

案例:用改进的字符流复制文件(自行准备一个文本文件)

import java.io.*;

public class IOTest023 {
    public static void main(String[] args) throws IOException {
        FileReader fr = new FileReader("./Test01.txt");
        FileWriter fw = new FileWriter("./Text02.txt");

        char[] chs = new char[1024];
        int len = 0;
        while((len=fr.read(chs)) != -1){
            fw.write(new String(chs,0,len));
        }
        //释放资源,与此同时会进行flush,否则数据写不进去
        fw.close();
        fr.close();
    }
}

是不是简洁很多了?但这样写的话就只能使用默认编码,而不能更改编码了,因此还是要根据实际情况来考虑使用哪一种方法。

字符缓冲流

与字节缓冲流的原理和使用基本一致,这里就不过多赘述,详细往上翻,看字节缓冲流。

同样,这里使用字节缓冲流来实现一个文件的复制粘贴,文件自行准备:

import java.io.*;

public class IOTest024 {
    public static void main(String[] args) throws IOException {
        //创建字符缓冲流,构造参数为FileReader和FileWriter类实例
        BufferedReader br = new BufferedReader(new FileReader("./Test01.txt"));
        BufferedWriter bw = new BufferedWriter(new FileWriter("./Text02.txt"));

        char[] chs = new char[1024];
        int len = 0;
        while((len=br.read(chs)) != -1){
            bw.write(new String(chs,0,len));
        }
        //释放资源,与此同时会进行flush,否则数据写不进去
        bw.close();
        br.close();
    }
}

这样操作文件就快多了,而且也比较简洁,可谓一举多得

字符缓冲流中特有的方法

在字符缓冲流中,提供了一些特有的方法,可以方便我们的文件读写。

  1. void newLine():换行,作用和换行符一样,不同点是,它可以自动识别当前的操作系统,以自动匹配到正确的换行符
  2. void readLine():读一行数据,但不读换行符

现在使用这两个方法来实现缓冲流复制文件:

import java.io.*;

public class IOTest025 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader("./Test01.txt"));
        BufferedWriter bw = new BufferedWriter(new FileWriter("./Test02.txt"));

        //readLine是直接读一行字符串,所以直接使用String来存储
        String line;

        //如果readLine读到了末尾没有数据了,就会返回null
        while((line=br.readLine()) != null){
            //这里的write可以直接一次写入一个字符串
            bw.write(line);
            //因为readLine不能读进换行符,所以得使用newLine手动换行
            bw.newLine();
            //刷新流,才能写入
            bw.flush();
        }
        //记得释放资源!!!
        bw.close();
        br.close();
    }
}

注意几个问题:

  1. readLine方法读取到了末尾,返回的是null,而不是-1
  2. readLine是直接读取一行字符串,所以要使用String存储读取的数据
  3. readLine不会读取换行符,因此需要手动实现换行,使用newLine就可以
  4. 因为使用的是字符缓冲流,必须要flush才能看到结果
  5. 记得释放资源,养成好习惯

其他流

打印流

打印流只能把数据输出,而不能读入数据。相比字节和字符输出流,打印流简化了语句写法,使代码看得更简洁。

字节打印流

使用字节打印流输出字符:

import java.io.FileNotFoundException;
import java.io.PrintStream;

public class IOTest031 {
    public static void main(String[] args) throws FileNotFoundException {
        //创建打印流
        PrintStream ps = new PrintStream("./Test01.txt");
        //调用打印流的print方法,print方法中,写入什么就会输出什么
        ps.print("abcdefghijklmn");
        //写入97就会输出97,而不会输出a。加了ln就会自动换行
        ps.println(97);
    }
}

打印流里面也有write方法,如果使用了write方法,那么97输出的结果就是字符a。所以要注意区分write和print两种方法

序列化与反序列化

对序列化的个人简单理解就是,把某个类的对象存储到外部文本中,使其持久化;而反序列化就是把这个存储到外部的对象读入进来。这个对象存储到外部后,外部文本中的内容我们人是看不懂的。

使用序列化与反序列化存储和读取对象

首先要定义一个简单的Student类

class Student implements Serializable {
    private String name;
    private int age;
    private String gender;

    public Student() {
    }

    public Student(String name, int age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getGender() {
        return gender;
    }
}

这个类必须要实现Serializable接口,才能够被序列化

然后是主类:

import java.io.*;

public class IOTest041 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Student s1 = new Student("Kyle",22,"male");
        //创建序列化对象,参数要传入OutputStream的子类
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./ObjectInfo.txt"));
        oos.writeObject(s1);

        //创建反序列化对象,参数传入InputStream子类
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./ObjectInfo.txt"));
        //反序列化读进来的对象是Object类型的,必须强制转化为Student类,然后给一个Student类对象接收
        Student s2 = (Student)ois.readObject();

        //然后像正常的对象一样,使用其方法
        System.out.println("姓名: " + s2.getName() + " 年龄: " + s2.getAge() + " 性别: " + s2.getGender());
    }
}
如果对类进行了修改该怎么办?

如果在序列化之后对类的内容进行了修改,在反序列化时就会报错(InvalidClassException)。要想解决这个问题,只需要在类中加上serialVersionUID变量并赋值,也就是添加一个版本号就可以了

serialVersionUID变量为私有静态长整型常量,在定义时就赋予初值,相当于给定一个版本号,这个值可以由你随意给定:

class Student implements Serializable {
    private static final long serialVersionUID = 10L;
    ...
    .
    .
}

这样一来,在序列化之后修改了类,在反序列化时也不会报错了

如果不想序列化某个属性

如果想在序列化时不希望某些属性被序列化,可以在属性的数据类型前加上transient

例如,我不希望年龄age被序列化,我就可以这样写:

class Student implements Serializable {
    private String name;
    private transient int age;
    private String gender;
    ...
    .
    .
}

在反序列化时,该值就会显示为0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值