IO流详解

一、File类

  • File类的一个对象代表一个文件或文件夹。
  • File能新建、删除、重命名文件和目录,但File不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。
  • 想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java程序的一个File对象可能没有一个真实存在的文件或目录。
  • File对象可以作为参数传递给流的构造器。

1、构造器

  • 构造器1:public File(String pathname)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * 以pathname为路径创建File对象,可以是绝对路径或者相对路径
    * ->绝对路径:是一个固定路径,从根目录开始
    * ->相对路径:是相对于某个位置开始
    * 关于路径分隔符:
    * Windows:\\ (也支持‘/’)
    * Linux:/
    * 也可以使用:File.separator,这个在不同的操作系统会自动适配不同的分隔符
    */
    //相对路径:相对于当前项目/模块
    File file1 = new File("hello.txt");
    //绝对路径
    File file2 = new File("D:\\File\\file.txt");
    //等同于上面的写法
    File file3 = new File("D:" + File.separator + "File" + File.separator + "file.txt");
    System.out.println(file1); //hello.txt
    System.out.println(file2); //D:\File\file.txt
    System.out.println(file3); //D:\File\file.txt
  • 构造器2:public File(String parent,String child)
    1
    2
    3
    4
    5
    /**
    * 以parent为父路径,child为子路径创建File对象
    */
    File file4 = new File("D:\\upload","picture");
    System.out.println(file4); //D:\upload\picture
  • 构造器3:public File(File parent,String child)
    1
    2
    3
    4
    5
    /**
    * 根据一个父File对象和子文件路径创建File对象
    */
    File file5 = new File(file4, "hello.png");
    System.out.println(file5); //D:\upload\picture\hello.png

2、常用方法

  • String getAbsolutePath():获取文件绝对路径
  • String getPath():获取路径
  • String getName():获取文件名
  • String getParent():获取上层文件目录路径。若无,返回null
  • long length():获取文件长度(即字节数)
  • long lastModified():获取最后一次修改时间,毫秒值
  • String[] list():获取指定目录下的所有文件或者文件目录的名称数组
  • File[] listFiles():获取指定目录下的所有文件或者文件目录的File数组
  • boolean renameTo(File dest):把文件重命名为指定的文件路径,例如:file1.renameTo(file2)要想返回true,file1必须在硬盘存在,且file2不能在硬盘中存在。
  • boolean isDirectory():判断是否是文件夹
  • boolean isFile():判断是否是文件
  • boolean exists():判断是否存在
  • boolean canRead():判断是否可读
  • boolean canWrite():判断是否可写
  • boolean isHidden():判断是否隐藏
  • boolean createNewFile():创建文件。若此文件存在,则不创建,返回false
  • boolean mkdir():创建文件夹。若此文件夹存在,不创建。若此文件夹的上层目录不存在,也不创建。
  • boolean mkdirs():创建文件夹。若上层文件夹不存在,则一并创建。
  • boolean delete():删除文件或文件夹。注意:删除不会放到回收站,要删除一个文件夹,请注意该文件夹下不可包含文件或文件夹。

二、IO流

  • I/O是Input/Output的缩写,用于处理设备间的数据传输。如读写文件,网络通信等。
  • Java程序中,对于数据的输入/输出操作以“流(Stream)“的方式进行。
  • java.io包下提供了各种流的类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。

1、何为输入/输出

  • 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
  • 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中。
  • 因此:输入/输出是站在程序(内存)的角度进行理解的。数据读进内存叫做输入,从内存中写出到外存叫做输出。

2、流的分类

  • 按操作数据的单位不同分为:字节流(8bit)、字符流(16bit)

  • 按数据流的流向不同分为:输入流、输出流

  • 按流的角色不同分为:节点流(直接对文件进行处理)、处理流(在节点流基础上进行加工,作用于节点流之上)

    (抽象基类) 字节流 字符流
    输入流 InputStream Reader
    输出流 OutputStream Writer

3、IO流的体系

分类 字节输入流 字节输出流 字符输入流 字符输出流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
访问管道 PipedInputStream PipedOutputStream PipedReader PipedWriter
访问字符串 StringReader StringWriter
缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
转换流 InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream
FilterInputStream FilterOutputStream FilterReader FilterWriter
打印流 PrintOutputStream PrintWriter
推回输入流 PushbackInputStream PushbackReader
特殊流 DataInputStream DataOutputStream
  • 其中FileInputStream 、FileOutputStream 、FileReader、FileWriter这四个流是节点流,即直接操作对文件本身进行操作的流。
  • BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter四个缓冲流是比较经典的处理流。
  • 处理流在节点流基础上进行加工,作用于节点流之上,可以提高流的读写速度。

4、节点流

4.1、FileReader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 基本操作:
* 将hello.txt内容打印到控制台
*/
@Test
public void test1() throws Exception {
//1.实例化File并指明要操作的文件
File file = new File("hello.txt");
//2.提供具体的流
FileReader fileReader = new FileReader(file);
//3.数据的读入(read方法返回读入的一个字符,如果读完,返回-1)
int data;
while ((data = fileReader.read()) != -1) {
System.out.print((char) data); //输出:helloWorld123
}
//4.流的关闭
fileReader.close();
}

处理异常(必须处理,否则流可能无法关闭):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void test1(){
FileReader fileReader = null;
try {
//1.实例化File并指明要操作的文件
File file = new File("hello.txt");
//2.提供具体的流
fileReader = new FileReader(file);
//3.数据的读入(read方法返回读入的一个字符,如果读完,返回-1)
int data;
while ((data = fileReader.read()) != -1) {
System.out.print((char) data); //输出:helloWorld123
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.流的关闭
try {
if (fileReader != null)
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

对read()的操作升级,即使用read的重载方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Test
public void test2() {
FileReader fileReader = null;
try {
File file = new File("hello.txt");
fileReader = new FileReader(file);
//read(char[] cbuf)返回每次读入数组的个数,到最后一个字符,返回-1
char[] cbuf = new char[5];
int len;
while ((len = fileReader.read(cbuf)) != -1) {
//错误写法:
//for (int i = 0; i < cbuf.length; i++) {
// System.out.print(cbuf[i]); //输出:helloWorld123ld
//}
for (int i = 0; i < len; i++) {
System.out.print(cbuf[i]); //输出:helloWorld123
}
//方式2:
//错误写法:
//String s = new String(cbuf);
//System.out.print(s); //输出:helloWorld123ld
//正确写法:
//String s = new String(cbuf, 0, len);
//System.out.print(s); //输出:helloWorld123
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileReader != null)
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

4.2、FileWriter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test1() {
FileWriter fileWriter = null;
try {
//1.实例化File并指明要写出到的文件
File file = new File("test1.txt");
//2.提供具体的流
fileWriter = new FileWriter(file);
//3.数据的写出
fileWriter.write("这是写出的数据");
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.流的关闭
try {
if (fileWriter != null)
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

注意:输出操作,对应的输出文件可以不存在,会自动创建。如果存在,默认情况下会对原有文件进行覆盖操作,不是追加操作。

如果想要进行追加操作,则添加构造器第二个参数,true则表示在末尾追加:

1
2
//写入时不会对已存在的文件内容覆盖,而是在末尾追加
fileWriter = new FileWriter(file, true);

FileReader+FileWriter实现对文本文件复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Test
public void test1() {
FileReader fileReader = null;
FileWriter fileWriter = null;
try {
//1.创建File类的对象,指明输入和输出文件
File srcFile = new File("hello.txt");
File desFile = new File("test.txt");
//2.创建输入和输出流的对象
fileReader = new FileReader(srcFile);
fileWriter = new FileWriter(desFile);
//3.数据的读入和写出
char[] cbuf = new char[5];
int len;
while ((len = fileReader.read(cbuf)) != -1) {
//每次写出len个字符
fileWriter.write(cbuf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关闭流资源
try {
if (fileReader != null)
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fileWriter != null)
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

注意:不可用像上面的字符流来处理图片视频等格式,可以运行,但是复制后的文件无法正常打开。

4.3、FileInputStream

首先,先用字节流来尝试打印出文本文件内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void test1() {
FileInputStream fis = null;
try {
File file = new File("hello.txt"); //原文:你好世界helloWorld
//声明字节输入流
fis = new FileInputStream(file);
//缓冲区长度5字节
byte[] buffer = new byte[5];
int len;
while ((len=fis.read(buffer))!=-1){
String str = new String(buffer,0,len);
System.out.print(str); //输出:你��世���helloWorld
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis!=null)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

从上面的运行结果可以看出:使用字节流输出文本内容,当文本中有中文时,可能会出现乱码情况。

出现这种情况的原因:在UTF8中,一个汉字3字节,而上面定义的缓冲区长度是5字节,byte[5],此时就有可能将一个汉字分隔开搬运,因此输出结果就会显示乱码。

因此:

1.对于文本文件(.txt,.java,.c,.cpp),应使用字符流处理。

2.对于非文本文件(.doc,.png,.jpg,.mp4,.ppt,….),使用字节流处理

4.4、FileOutputStream

使用FileInputStream+FileOutputStream实现对非文本文件复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//对图片进行复制
@Test
public void test1() {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
File srcFile = new File("122.jpg");
File desFile = new File("1.jpg");
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(desFile);
byte[] buffer = new byte[5];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fis != null)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

5、缓冲流

BufferedInputStream、BufferedOutputStream实现对非文本文件的复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//使用缓冲流对非文本文件的复制
@Test
public void test1() {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
//1.造文件
File srcFile = new File("122.jpg");
File desFile = new File("1.jpg");
//2.造流
//2.1造节点流
FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(desFile);
//2.2造缓冲流
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
//3.复制操作:写入写出
byte[] buffer = new byte[10];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关闭资源
//要求:先关外层流(缓冲流),后关内层(节点流)
try {
if (bos != null)
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bis != null)
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
//说明:在关闭外层流时,内层流会自动关闭,因此可以省略
//fos.close();
//fis.close();
}
}

BufferedReader和BufferedWriter和上面的字节流使用方法一致,都是在FileReader和FileWriter外再套一层即可,用于提高对文本文件的处理速率。

6、转换流

  • 转换流也属于处理流的一种,它提供了字符流和字节流之间的转换
  • Java中提供了两个转换流:
    • InputStreamReader:将InputStream转换为Reader(byte转换成char,解码)
    • OutputStreamWriter:将Writer转换为OutputStream(char转换成byte,编码)
  • 字节流中的数据都是字符时,转换成字符流操作更高效。
  • 很多时候我们使用转换流来处理文件的乱码问题。实现编码和解码的功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void test1() {
InputStreamReader isr = null;
try {
FileInputStream fis = new FileInputStream("hello.txt");
//第二个参数指明字符集,具体使用哪个,取决于文件保存时使用的字符集
isr = new InputStreamReader(fis, "UTF-8");
char[] cbuf = new char[1024];
int len;
while ((len = isr.read(cbuf)) != -1) {
String str = new String(cbuf, 0, len);
System.out.println(str); //输出:你好世界helloWorld
//若将上面的编码换成“GBK”,则会输出乱码:浣犲ソ涓栫晫helloWorld
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (isr!=null) {
try {
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

综合使用InputStreamReader和OutputStreamWriter(先用UTF-8读入,再用GBK写出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Test
public void test2() {
InputStreamReader isr = null;
OutputStreamWriter osw = null;
try {
File file1 = new File("hello.txt");
File file2 = new File("hello2.txt");
FileInputStream fis = new FileInputStream(file1);
FileOutputStream fos = new FileOutputStream(file2);
isr = new InputStreamReader(fis, "UTF-8"); //"UTF-8"编码读入内存
osw = new OutputStreamWriter(fos, "GBK"); //用"GBK"编码写出文件
char[] buffer = new char[1024];
int len;
while ((len = isr.read(buffer)) != -1) {
osw.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (isr!=null) {
try {
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (osw!=null) {
try {
osw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

7、标准输入、输出流

  • System.in:标准的输入流,默认从键盘输入
  • System.out:标准的输出流,默认从控制台输出
  • 可以通过System类的setIn(InputStream is)/setOut(PrintStream ps)方法重新指定输入和输出的流。

练习:

从键盘输入字符串,要求将读取到的整行字符串转换成大写输出,然后继续进行输入操作,直到输入‘e’或者‘exit’时,退出程序。

方法一:使用Scanner,调用next()返回一个字符串(省略)

方法二:使用System.in,System.in——>转换流——>BufferedReader的readline()(如下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static void main(String[] args) {
BufferedReader br = null;
try {
InputStreamReader isr = new InputStreamReader(System.in);
br = new BufferedReader(isr);
while (true) {
System.out.print("请输入字符串:");
String data = br.readLine();
if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {
System.out.println("程序结束");
break;
}
String upperCase = data.toUpperCase();
System.out.println("转换结果:" + upperCase);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/*
请输入字符串:avadd
转换结果:AVADD
请输入字符串:21312sdad
转换结果:21312SDAD
请输入字符串:e
程序结束
*/

8、对象流

  • ObjectInputStream和ObjectOutputStream用于存储和读取基本数据类型数据或者对象的处理流。它的强大之处就是可以将Java的对象写入数据源中,也能把对象从数据源中还原回来。
  • 序列化:用ObjectOutputStream类保存基本类型数据或对象的机制。
  • 反序列化:用ObjectInputStream类读取基本数据类型数据或对象的机制。
  • ObjectOutputStream和ObjectInputStream不能序列化statictransient修饰的成员变量。
  • 对象的序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久的保存在磁盘上,或通过网络这种二进制流来传输到另一个网络节点。当其他程序获取这种二进制流,就可以恢复成原来的Java对象
  • 序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原。
  • 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一,否则,会抛出NotSerializableException异常。
  • 凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:
    • private static final long serialVersionUID;
    • serialVersionUID用来表明类的不同版本的兼容性。简言之,其目的是以序列化对象进行版本控制,有关个版本反序列化时是否兼容
    • 如果类中没有显式定义该常量,它的值是Java运行环境根据类的内部细节自动生成。若类的实例变量做了修改,serialVersionUID就可能发生变化。故建议显式声明。
  • 简单来说,Java的序列化机制是通过在运行时判断serialVersionUID来验证版本一致性的。进行反序列化时,JVM会把过来的字节流中的serialVersionUID与本地相应实体类serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。

练习:

将String对象序列化和反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//将Java对象序列化成磁盘文件
@Test
public void test1() {
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("obj.dat"));
oos.writeObject(new String("这是一个String对象")); //将对象序列化写入文件obj.dat
oos.flush(); //刷新操作
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

//将磁盘文件反序列化成内存中的Java对象
@Test
public void test2() {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("obj.dat"));
Object obj = ois.readObject();
String str = (String) obj;
System.out.println(str); //输出结果:这是一个String对象
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

注意:被序列化的对象必须实现Serializable接口!!而且其内部的所有属性也必须是可序列化的!否则都会报错。