本文为个人学习所记录的笔记,每个字都是自己纯手打,文本内容更多的也是自己的理解。如在文中有发现错误、遗漏或讲得不清楚的地方都请各位大佬不吝指出!
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();
}
}
需要注意的几个点:
- 在实例化时,构造参数可以填写File类对象,也可以填写路径名字符串
- 若FileOutputStream所指向的文件对象不存在,则会自动创建该文件。仅限输出流,输入流无法创建
- 字节流的write一次只能写一个字符,而且参数必须为字符对应的ASCII值
- 输入输出流使用完毕后务必记得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();
}
}
}
}
根据演示代码可知,在处理字节输出流的异常时,需要注意这些问题:
- fos分别在try和finally两个代码块中被使用,在不同代码块中,该fos相对另一个代码块都是局部变量。因此需要提前先在外部对fos进行声明,先指向一个null
- 在close前,需要先判断fos是否为空,也就是判断fos是否已经指向了某个地址。否则会发生空指针异常
- 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();
}
}
这样操作文件就快多了,而且也比较简洁,可谓一举多得
字符缓冲流中特有的方法
在字符缓冲流中,提供了一些特有的方法,可以方便我们的文件读写。
- void newLine():换行,作用和换行符一样,不同点是,它可以自动识别当前的操作系统,以自动匹配到正确的换行符
- 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();
}
}
注意几个问题:
- readLine方法读取到了末尾,返回的是null,而不是-1
- readLine是直接读取一行字符串,所以要使用String存储读取的数据
- readLine不会读取换行符,因此需要手动实现换行,使用newLine就可以
- 因为使用的是字符缓冲流,必须要flush才能看到结果
- 记得释放资源,养成好习惯
其他流
打印流
打印流只能把数据输出,而不能读入数据。相比字节和字符输出流,打印流简化了语句写法,使代码看得更简洁。
字节打印流
使用字节打印流输出字符:
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