java IO
1. 字节流
1.1 字节流的概念
java中将数据的读取、数据的写入这种数据的传输抽象的描述为“流”,至于为什么叫流,可能是在这种模式下,数据的传输都是单向的,就像水的流动。java中的“流”都位于java.io包中,所以称为IO流,也可以叫做输入输出流。
按照操作的数据和传输方向来划分java中的流:
IO流
- 字节流
- 字节输入流
- 字节输出流
- 字符流
- 字符输入流
- 字符输出流
其实可以加上转换流:
所以说,字节流包括字节输入流和字节输出流。
注意:Input(输入)对应的是“读”操作,而Output(输出)对应的是“写”操作。
java中提供了两个抽象类InputStream和OutputStream,它们是字节流的顶级父类,所有的字节输入流都继承自InputStream,所有的字节输出流都继承自OutputStream。
InputStream的常用方法:
方法 | 描述 |
---|---|
int read() | 从输入流中读取一个8位的字节,把它转化为0-255之间的整数,并返回这一整数 |
int read(byte[] b) | 从输入流中读取若干个字节,把它们保存在字节数组b中,返回读取的字节的数目 |
int read(byte[] b,int off, int len) | 从输入流读取若干个字节,把它们保存在字节数组b中,off指定字节数组开始保存数据的起始下标,len表示读取的字节数目 |
void close() | 关闭此输入流并释放与该流关联的所有系统资源 |
OutputStream的常用方法:
方法 | 描述 |
---|---|
void writer(int b) | 向输入流中写入一个字节 |
void writer(byte[] b) | 把字节数组b中的所有字节写入到输出流 |
void writer(byte[] b,int off,int len) | 将字节数组b中从偏移量off开始的len个字节写入输出流 |
void flush() | 刷新此输出流并强制写出所有缓冲的输出字节 |
void close() | 关闭此输出流并释放与此流相关的所有系统资源 |
InputStream和OutputStream虽然提供了一系列和读写数据有关的方法,但是这两个类都是抽象类,不能被实例化。因此,针对不同的功能,InputStream和OutputStream提供了不同的子类:
1.2 使用字节流来读写文件
1.2.1 使用字节流来读文件
使用字节流来读取文件,要使用FileInputStream这个类,它是InputStream的子类,下面是一个例子:
public static void main(String[] args) throws IOException{
//字节输入流FileInputStream的使用
//创建一个字节输入流对象
FileInputStream in = new FileInputStream("FileInputStreamTest.txt");
int b = 0; //定义一个int类型的变量b,记住每次读取的一个字节
while (true) {
b = in.read(); //变量b记住读取的每个字节
if (b == -1) { //如果读取的字节为-1,则跳出循环
break;
}
System.out.print( (char) b); //否则将b写出
}
in.close(); //记得关闭流
}
注意:
由于文件不存在或路径不正确等原因会导致上面的代码发生文件读取或写入发生异常,这些异常都由java虚拟机封装在IOExecption中。所以,所有IO操作都要注意处理IOException。
上面的代码中,如果产生异常将会导致流没有关闭,占用系统资源。所以,最好是用try finally 来确保在发生错误的情况下流也会关闭。
上面的代码和下面的实例代码都使用的是相对路径,当然,也可以使用绝对路径。在上面代码的情况下,FileInputStreamTest.txt这个文件应该在java原程序的根目录下。
在路径中不要使用一个右斜杆,要使用左斜杆或使用两个斜杆,因为右斜杆是转义符。
在路径中要注意文件的后缀
我们使用try finally 将上面的代码完善一下,使得在发生异常时也能关闭流:
public static void main(String[] args) throws IOException{
//字节输入流FileInputStream的使用
//创建一个字节输入流对象
FileInputStream in = null;
try {
in = new FileInputStream("FileInputStreamTest.txt");
int b = 0; //定义一个int类型的变量b,记住每次读取的一个字节
while (true) {
b = in.read(); //变量b记住读取的每个字节
if (b == -1) { //如果读取的字节为-1,则跳出循环
break;
}
System.out.print( (char) b); //否则将b写出
}
}
finally {
if (in != null) {
in.close();
}
}
}
}
使用try finally 后,我们的代码变得复杂了。其实还有更好的处理方法,使用java7引入的try(resource)语法,我们只需要写try语句块,java会自动为我们关闭资源:
public static void main(String[] args) throws IOException{
//使用java7引入的try(resource)语法,让java知道关闭资源
try (FileInputStream in = new FileInputStream("FileInputStreamTest.txt")) {
int b = 0; //定义一个int类型的变量b,记住每次读取的一个字节
while (true) {
b = in.read(); //变量b记住读取的每个字节
if (b == -1) { //如果读取的字节为-1,则跳出循环
break;
}
System.out.print( (char) b); //否则将b写出
}
}
}
}
代码是不是变得简洁许多!
1.2.1.1 InputStream的read()方法是阻塞的
阻塞(blocking)的意思是:
以上面的 b = in.read()
这段代码为例,必须等到read()
方法返回,才会执行接下来的代码。有可能read()
方法很长时间没有返回,程序就会一直卡在read()
这里,就像是堵塞了。所以,read()
方法有可能会花很长的时间。
1.2.1.2 FileInputStream的缓存区
FileInputStream.read()
方法一次只会读取一个字节,通常来说,设备一次读取一个字节和一次读取多个字节所花费的时间是差不多相等的,所以很多流都支持先读取多个字节到缓存区,这样会提高效率。FileInputStream提供了两个重载的方法来做到一次读取多个字节:
int read(byte[] b)
:读取若干字节并填充到byte[]
数组,返回读取的字节数int read (byte[] b, int off , int len)
: 指定byte[]
数组的偏移量和最大填充数,返回读取到的字节数
注意:
- 上面的两个方法返回的都是读取到的字节数,而不是读取到的字节的int值
- 如果放回的是-1,表示读取完毕
下面是利用缓存区一次读取多个字节的例子:
try (InputStream in = new FileInputStream("FileInputStreamTest.txt")) {
byte[] b = new byte[1024]; //缓存区的大小为1024
int count;
while ( (count = in.read(b)) != -1) { //读取到缓冲区(缓存区)
System.out.println("读取到" + count + "个bytes");
}
}
}
1.2.2 使用字节流把数据写入文件
与FileInputStream相对应的是FileOutputStream,它可以将数据写入文件:
try (FileOutputStream out = new FileOutputStream("test02.txt")) {
out.write(101); //e
out.write(111); //o
}
上面的代码将e和o两个单词写入了test02.txt文件中。
1.2.2.1 FileOutputStream的缓存区
每次写入一个字节非常麻烦,更常见的方法是一次性写入若干个字节。这时,可以用OutputStream
提供的重载方法void write(byte[])
来实现:
public static void main(String[] args) throws IOException{
//创建一个FileOutputStream对象
FileOutputStream out = new FileOutputStream("test02.txt");
String str = "这是写入的数据!";
byte[] b = str.getBytes();
for (int i = 0; i < b.length; i++) {
out.write(b[i]);
}
out.close(); //记得关掉流
}
注意:构建FileOutputStream对象时,如果指定的文件不存在,会自动在当前java程序的根目录生成文件。
如果是向一个已经有数据的文件中写数据,上面的代码会将原有的数据清空,然后再写入数据。要将数据附加到文件中,不覆盖原有的数据,可以使用构造函数FileOutputStream(String fileName, boolean append)
来创建文件输入流对象,并把append参数的值设置为true,请看下面的例子:
public static void main(String[] args) throws IOException {
//字节输入流FileOutputStream的不覆盖原来数据的使用方法
FileOutputStream out = new FileOutputStream("FileOutputStreamTest.txt", true);
String str = "不覆盖原有的信息";
byte[] b =str.getBytes();
for (int i = 0; i < b.length; i++) {
out.write(b[i]);
}
out.close();
}
1.2.2.2 OutputStream的write()方法是阻塞的
和上面所讲的InputStream的read()
方法一样,OutputStream的write()
方法也是阻塞的。
1.2.2.3 OutputStream的flush()方法
InputStream是FileInputStream的父类,OutputStream是FileOutputStream的父类。inputStream和OutputStream都提供了close()
方法来关闭流,但要特别注意OutputStream的flush()
方法,这个方法的作用是将缓冲区的内容真正的输送到目的地,强制将缓冲区的数据输出。
一般来说,我们不需要调用这个flush()
方法,因为缓冲区如果满了,OutputStream会自动调用这个方法,在使用close()
方法关掉OutputStream时,也会自动调用这个方法。但是在一些特殊的情况下,这个方法也有用到。
1.2.3 使用字节流实现文件的拷贝
通常字节输入流和字节输出流都是同时使用的,下面给出一个利用字节流实现文件的拷贝的例子:
public static void main(String[] args) throws IOException{
//用字节流来将一个音频文件拷贝到宁一个文件中
//定义一个文件字节输入流读取文件
InputStream in = new FileInputStream("source\\音乐.mp3");
//定义一个文件字节输出流写入文件
OutputStream out = new FileOutputStream("target\\音乐.mp3");
int len; //定义一个int类型的变量len用于记录读取读入缓冲区的字节数
byte[] arr = new byte[1024]; //定义一个字节数组当做缓冲区
long begintime = System.currentTimeMillis(); //获取写入操作开始前系统时间
while ((len = in.read(arr)) != -1) {
out.write(arr,0,len);
}
long endtime = System.currentTimeMillis(); //获取写入操作后系统的时间
System.out.println("拷贝文件所消耗的时间为:" + (endtime - begintime) + "毫秒。");
in.close();
out.close();
}
1.3 字节流的缓冲区
在上面,我们讲了字节输入流(fileInputStream)和字节输出流(fileOutputStream)的使用,也介绍了它们的缓冲区,只是缓冲区是我们自己用字节数组实现的。下面介绍java自带的字节流的缓冲区。
在IO包中提供了带缓冲的字节流,它们是BufferedInputStream和BufferedOutputStream,它们的构造方法中分别接收InputStream和OutputStream类型的参数作为对象,在读写的时候提供缓冲的功能,也可称这两个类为包装类。
下面给出了例子:
public static void main(String[] args) throws Exception{
//字节输入流缓存区和字节输出流的缓冲区的使用,即BufferedInputStream和BufferedOutputStream
BufferedInputStream in = new BufferedInputStream(new FileInputStream("BufferedInputTest.txt"));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("BufferedOutputTest.txt"));
int len;
long begintime = System.currentTimeMillis();
while ((len = in.read()) != -1) {
out.write(len);
}
long endtime = System.currentTimeMillis();
System.out.println("复制文件共花了" + (endtime - begintime) + "毫秒。");
in.close();
out.close();
}
在上面的代码中,先分别传入FileInputStream对象(InputStream的子类)和FileOutputStream对象(OutputStream的子类)来创建BufferedInputStream(字节输入流缓冲区)和BufferedOutputStream(字节输出流缓冲区),这两个缓冲流对象都在内部定义了一个大小为8192的字节数组,然后将字节数组中的数据一次性的读写到文件中,提高数据的读写效率。
2.字符流
2.1字符流的介绍
字节流的操作对象是字节,如果想操作字符,字节流就非常不方便了,这个时候就要使用字符流了,和字节流一样,字符流也有两个顶级的父类,Reader和Writer,和它们的名字的意思一样,Reader是字符输入流,读取数据,而Writer是字符输出流,写数据。
字符流只能用来读取、处理文本文件。而字节流可以处理所有类型的文件,包括图片、视频、音频等,每种流都有对应的缓冲区,用缓冲区可以大大提高读写的速度。当然,你也可以不用缓冲区,自己写缓冲区也一样。
2.2字符流读取文件
要使用字符流读取文本文件,可以使用字符输入流FileReader:
public static void main(String[] args) throws Exception{
//字符输入流FileReader的使用
FileReader reader = new FileReader("FileReaderTest.txt");
int len;
while ((len = reader.read()) != -1) {
System.out.print( (char) len);
}
reader.close();
}
注意:字符输入流的read()方法返回的是int类型的值,如果想要获得字符,就需要进行强制类型转换,上面的代码中就使用了char将数值强制转换为字符类型再将其打印。
2.3字符流写入字符
要向文件中写入字符可以使用字符流FileWriter类:
public static void main(String[] args) throws Exception{
//字符输出流FileWriter的使用
FileWriter writer = new FileWriter("FilewriterTest.txt");
String string = "这是一个用来测试FileWriter的测试类!";
writer.write(string);
writer.write("\r\n"); //将输入语句换行,避免再写入数据是对之前的数据进行了覆盖
writer.close();
}
2.4 字符流的包装类(缓冲区)
在上面我们介绍了字节流的包装类:BufferedInputStream和BufferedOutputStream,也可以称它们两个为字节流的缓冲类。同样,字符流也提供了带缓冲区的包装类,分别是BufferedRead和BufferedWriter。
下面是一个字符输入流的包装类(BufferedRead)和字符输出流的包装类(BufferedWriter)的例子:
public static void main(String[] args) throws Exception {
//字符输入和输出流的缓冲区的使用,即BufferedReader和BufferedWriter
BufferedReader bReader = new BufferedReader(new FileReader("BufferedReaderTest.txt"));
BufferedWriter bWriter = new BufferedWriter(new FileWriter("BufferedWriterTest.txt"));
String string;
while ((string = bReader.readLine()) != null) {
bWriter.write(string);
bReader.lines();
}
bReader.close();
bWriter.close();
}
注意:BufferedReader中的readLine()方法会一次读取一行文本
3.转换流
转换流可以实现字节流和字符流之间的转换:
- InputStreamReader:Reader的子类,将字节输入流转换为字符输入流
- OutputStreamWriter: Writer的子类,将字节输出流装换为字符输出流
使用转换流将字节流转换为字符流时,转换的前提是字节流读取的是文本文件,否则,转换后将造成数据的丢失。
下面是使用转换流的例子:
public static void main(String[] args) throws Exception{
//转换流InputStreamReader和OutputStreamWriter的使用
//创建一个字节输入流
FileInputStream FIS = new FileInputStream("InputStreamReaderTest.txt");
//将字节输入流转换为字符输入流
InputStreamReader ISR = new InputStreamReader(FIS);
//为字符输入流创建字符缓冲区
BufferedReader BR = new BufferedReader(ISR);
//创建字节输出流
FileOutputStream fOS = new FileOutputStream("OutputStreamWriterTest.txt");
//将字节输出流转换为字符输出流
OutputStreamWriter OSW = new OutputStreamWriter(fOS);
//为字符输出流创建字符缓冲区
BufferedWriter BW = new BufferedWriter(OSW);
String line;
long begintime = System.currentTimeMillis();
while ((line = BR.readLine()) != null) {
BW.write(line);
}
long endtime = System.currentTimeMillis();
System.out.println("拷贝文件一共花了" + (begintime - endtime ) + "毫秒!");
BR.close();
BW.close();
}
还可以使用另一种嵌套的写法:
import java.io.*;
public class WriteAndRead {
public static void main(String[] args) {
file1();
file2();
}
public static void file1() {
//创建一个文件并向其中写入信息
//输出字符流缓冲区 <-- 输出转换流 <-- 输出字节流换冲区
try {BufferedWriter bw= new BufferedWriter(new OutputStreamWriter(new FileOutputStream("Example.txt")));
bw.write("name:tom"); //写入信息
bw.newLine(); //换行
bw.write("age:12");
bw.newLine();
bw.write("telephone:123456789");
bw.newLine();
bw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void file2() {
//读取文件中的信息并将其打印出来
//输入字符流缓冲区 <-- 输入转换流 <-- 输入字节流
try {BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("Example.txt")));
String line;
while ((line=br.readLine()) !=null) { //逐行读取文件中的信息,直到结尾
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace(); }
}
}
4. File类
前面所讲的IO流可以实现对文件的内容进行读写,实际上,我们通常需要创建一个文件、重命名文件、判断某个文件是否存在等。这些对文件的操作,java提供了一个File类来实现这些操作。
4.1 File类的创建
File类常用的构造方法:
方法 | 功能 |
---|---|
File(String pathname) | 通过指定的一个字符串类型的文件路径来创建一个新的File对象 |
File(String parent, String child) | 根据指定的一个字符串类型的父路径和一个字符串类型的子路径创建一个File对象 |
File(File parent, String child) | 根据指定的File类的父路径和字符串类型的子路径创建一个File对象 |
注意:
- 构造File对象时,即可以传入绝对路径,也可以传入相对路径。
- 路径中要使用两个右斜杆或一个左斜杆,不要使用一个右斜杆,因为右斜杆是转义符。
- 构造一个File对象时,即使传入的文件或目录的路径不存在,代码也不会出错,因为构造一个File对象并不会进行任何的磁盘操作。只有使用File对象的某些方法时,才真正进行磁盘操作。
4.2 File类的常用方法
File类提供了许多方法来操作其内部封装的路径指向的文件或目录,下面是File类中的常用方法:
方法 | 功能 |
---|---|
boolean exists() | 判断File对象对应的文件或目录是否存在,存在返回true,不存在返回false |
boolean delete() | 删除File对象对应的文件或目录,删除成功返回true,否则返回false |
boolean createNewFile() | 当File对象对应的文件不存在时,该方法将新建一个此File对象所指定的新文件,若创建成功则返回true,否则返回false |
String getName() | 返回File对象表示的文件或文件夹的名称 |
String getPath() | 返回File对象对应的路径 |
String getAbsolutePath() | 返回File对象对应的绝对路径 |
String getParent() | 放回File对象对应目录的父目录(即返回的目录不包含最后一级子目录) |
boolean canRead() | 判断File对象对应的文件或目录是否可读,可读返回true,否则返回false |
boolean canWrite() | 判断File对象对应的文件或目录是否可写,可写返回true,否则返回false |
boolean isFile() | 判断File对象对应的是否是文件,是文件返回true,否则返回false |
boolean isDirectory() | 判断File对象对应的是否是目录,是目录返回true,否则返回false |
boolean isAbsolute() | 判断File对象对应的文件或目录是否是绝对路径 |
long lastModified() | 返回1970年1月1日0时0分0秒到文件最后修改时间的毫秒值 |
long length() | 返回文件内容的长度 |
String[] list() | 列出指定目录的全部内容,只是列出名称 |
File[] listFiles() | 返回一个包含了File对象所有子文件和子目录的File数组 |
boolean mkdir() | 创建当前File对象表示的目录 |
boolean mkdirs() | 创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来 |
下面是这些方法的例子:
public static void main(String[] args) {
//File类的常用方法
File file = new File("FileExample");
System.out.println(file.exists() ? "文件或目录存在" : "文件或目录不存在");
System.out.println("文件名称:" + file.getName());
System.out.println("文件的相对路径:" + file.getPath());
System.out.println("文件的绝对路径:" + file.getAbsolutePath());
System.out.println("文件的父路径:" + file.getParent());
System.out.println(file.canRead() ? "文件可读" : "文件不可读");
System.out.println(file.canWrite() ? "文件可写" : "文件不可写");
System.out.println(file.isFile() ? "是一个文件" : "不是一个文件");
System.out.println(file.isDirectory() ? "是一个目录" : "不是一个目录");
System.out.println(file.isAbsolute() ? "是绝对路径" : "不是绝对路径");
System.out.println("文件最后一次修改时间为:" + file.lastModified());
System.out.println("文件大小为:" + file.length() + "bytes");
System.out.println("是否成功删除文件:" + file.delete());
}
注意:
- list()方法和listFiles()方法的区别
- 返回的类型不同:list()方法返回的是String数组,listFiles()返回的是File对象数组。
- 数组中的元素类型不同:list()方法返回的String数组中的元素是String类型的文件名,而listFiles()是File对象。
遍历文件夹中的所有文件,包括子文件夹中的文件时,必须使用listFiles()方法
delete()方法删除目录时,只有目录为空时才能删除成功。
4.3 遍历目录下的文件
4.3.1 直接遍历目录下的所有文件
如果要遍历一个目录下的所有文件,可以使用list()
方法:
public static void main(String[] args) {
// 使用File.list()来得到一个目录中所有的文件的文件名
File file = new File("E:\\EclipseWorkspace\\JavaBook_Examples");
if (file.isDirectory()) { // 判断file对象对应的目录是否存在
String[] names = file.list(); // 获得目录下所有文件的文件名
for (String name : names) {
System.out.println(name); // 输出文件名
}
}
}
4.3.2 获得目录下满足要求的文件
如果我们想要得到一个目录下指定类型的文件,如获取所有以.txt结尾的文件。这个时候,我们就可以使用File类中提供的一个重载的list(FilenameFilter filter)
方法,该方法接收一个FilenameFilter类型的参数。FilternameFilter是一个接口,被叫做文件过滤器,当中定义了一个抽象方法accept(File dir,String name)
。在调用list()方法时,需要实现文件过滤器FilenameFilter,并在accept()
方法中做出判断,从而获得指定类型的文件。
public static void main(String[] args) {
// 使用File.list(Filter),即使用过滤器来得到一个目录中满足过滤器要求的文件的文件名
File file = new File("E:\\EclipseWorkspace\\JavaBook_Examples");
// 创建过滤器对象
FilenameFilter filter = new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
File file1 = new File(dir, name);
// 判断文件名是否以.txt结尾,是就返回true,否则返回false
if (file1.isFile() && name.endsWith(".txt")) {
return true;
} else {
return false;
}
}
};
if (file.isDirectory()) {
// 创建文件名列表时加入过滤器参数,选出满足过滤器的文件名
String[] filenames = file.list(filter);
for (String filename : filenames) {
System.out.println(filename);
}
}
}
4.4 递归遍历目录下的文件和子目录
上面的例子只是遍历了目录下文件的文件名,在目录下可能还有目录,要得到所有子目录下的File类型对象,要使用listFiles()
方法。listFiles()
方法返回一个File对象数组,当对数组中的元素进行遍历时,如果元素中还有子目录需要遍历,则需要使用递归。下面是一个例子:
package Section7;
import java.io.File;
public class Example14 {
public static void main(String[] args) throws Exception {
// 由于一个目录中可能还有目录,所以使用File.list()方法只能获得所有文件的文件名
// 而不能获得子目录中的文件名,所以接下来用File.listFiles()方法来获得目录中包括子目录中的文件路径
File file = new File("E:\\EclipseWorkspace\\JavaBook_Examples");
long begintime = System.currentTimeMillis();
fileDir(file);
long endtime = System.currentTimeMillis();
System.out.println("遍历共花了:" + (endtime - begintime) + "毫秒。");
}
public static void fileDir(File dir) {
File[] files = dir.listFiles(); // 用listFile方法获得一个File对象数组
for (File file : files) { // 遍历File对象数组files
if (file.isDirectory()) { // 如果是目录,则递归
fileDir(file);
}
System.out.println(file.getAbsolutePath()); // 将每一个文件对象的绝对路径打印出来
}
}
}
4.5文件和目录的删除
如果仅仅是删除一个文件,可以使用File类的delete()
方法:
public static void main(String[] args) {
File delFile = new File("deleteTest.txt");
if (delFile.exists()) {
delFile.delete();
}
}
如果要删除目录,且目录下包含子目录或文件,则File类的delete()
方法不允许对这个目录直接删除。在这种情况下,需要通过递归的方式将整个目录及其中的文件全部删除:
package Section7;
import java.io.File;
public class Example16 {
public static void main(String[] args) {
File myFile = new File("G:\\JavaExample");
delDir(myFile);
}
public static void delDir(File dirFile) {
File[] files = dirFile.listFiles();
if (dirFile.exists()) {
for (File f : files) {
if (f.isDirectory()) {
delDir(f);
} else {
f.delete();
}
}
}
dirFile.delete();
}
}
注意:在java中删除目录或文件是从虚拟机直接删除而不走回收站,文件或目录一旦删除就无法恢复。
4.6 Path
java还提供了一个Path对象,它位于java.nio.file包。Path和File对象类似,如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。大家感兴趣的话可以自己查一下。