JavaIO流

Java IO流概述

在日常的开发中,经常会遇到对本地文件进行操作的功能,例如将数据库中的数据导出成Excel并保存在本地,或者将本地的文本文件读入程序中,再或者在本地新建目录删除目录等等。这些都需要对本地文件进行读写。
Java为开发者提供了File类用于操作本地的文件,File类是java.io包下代表与平台无关的文件和目录,也就是说,如果希望在程序中操作文件和目录,都可以通过File类来完成。值得指出的是,不管是文件还是目录都是使用File来操作的,File能新建、删除、重命名文件和目录,File不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。

访问文件和目录

File类可以使用文件路径字符串来创建File实例,该文件路径字符串既可以是绝对路径,也可以是相对路径。一旦创建了File对象后,就可以调用File对象的方法来访问,File类提供了很多方法来操作文件和目录,下面列出一些比较常用的方法。

访问文件名

方法 描述
String getName() 返回File对象的文件名或者路径名
String getPath() 返回File对象对应的路径名
File getAbsoluteFile() 返回File对象的绝对路径
String getAbsolutePath() 返回File对象的绝对路径名
String getParent() 返回File对象的父目录
boolean renameTo(File newName) 重命名文件

下面通过示例来学习以上方法的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cn;

import java.io.File;

public class bytecollege {
public static void main(String[] args) {
String path = "E:/ByteCollege/study/Node.exe";
File file = new File(path);
File newFile = new File("E:/ByteCollege/study/Code.exe");
//返回File对象的文件名或者路径名
System.out.println(file.getName());
//返回File对象对应的路径名
System.out.println(file.getPath());
//返回File对象的绝对路径
System.out.println(file.getAbsoluteFile());
//返回File对象的绝对路径名
System.out.println(file.getAbsolutePath());
//返回File对象的父亲目录
System.out.println(file.getParent());
//重命名文件
file.renameTo(newFile);
}
}

运行后的结果为:

文件检测相关方法

方法 描述
boolean exists() 判断File对象所对应的文件或目录是否存在
boolean canWrite() 判断文件和目录是否可写
boolean canRead() 判断文件和目录是否可读
boolean isFile() 判断是否是文件
boolean isDirectory() 判断是否是目录
boolean isAbsolute() 判断是否是绝对路径
boolean isHidden() 判断文件是否隐藏

下面通过示例来学习以上方法的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cn.bytecollege.IODemo;

import java.io.File;

public class FileDemo {
public static void main(String[] args) {
File file = new File("D:/BYTE/study/Node.txt");
//判断File对象所对应的文件或目录是否存在
System.out.println(file.exists());
//判断文件和目录是否可写
System.out.println(file.canWrite());
//判断文件和目录是否可读
System.out.println(file.canRead());
//判断是否是文件
System.out.println(file.isFile());
//判断是否是目录
System.out.println(file.isDirectory());
//判断是否是绝对路径
System.out.println(file.isAbsolute());
//判断文件是否隐藏
System.out.println(file.isHidden());
}
}

获取文件信息

方法 描述
long lastModified() 最后一次修改时间,返回值为修改时间的毫秒数
long lenght() 获取文件长度,单位为字节

下面通过示例来学习以上方法的使用:

1
2
3
4
5
6
7
8
9
10
11
package cn.bytecollege.IODemo;

import java.io.File;

public class FileDemo2 {
public static void main(String[] args) {
File file = new File("D:/BYTE/study/Node.txt");
System.out.println(file.lastModified());
System.out.println(file.length());
}
}

运行后的结果为:

目录操作

方法 描述
boolean mkdir() 创建新目录
boolean mkdirs() 创建多层目录
String[] list() 列出File对象子文件名和路径名,返回String数组
File[] listFiles() 列出File对象的索引子文件和路径
static File[] listRoots() 获取系统根路径
boolean delete() 删除文件或者路径,如果有多层路径,调用1次只删除最后一层目录,并且目录必须为空

下面通过示例来学习以上方法的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cn.bytecollege.IODemo;

import java.io.File;

public class FileDemo3 {
public static void main(String[] args) {
File file = new File("D:/BYTE/study/Node.txt");
File file2 = new File("D:/BYTE/study/code");
//创建新目录
file2.mkdir();
//创建多层目录
file2.mkdirs();
//列出File对象子文件名和路径名,返回String数组
file2.list();
//列出File对象的索引子文件和路径
file.listFiles();
//获取系统根目录
file.listRoots();
}
}

获取分区大小

方法 描述
long getTotalSpace() 获取分区大小
long getFreeSpace() 获取分区空闲大小
long getUsableSpace() 获取分区可用大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.bytecollege.IODemo;

import java.io.File;

public class FileDemo4 {
public static void main(String[] args) {
String path = "D:/";
File file = new File(path);
//获取分区大小
System.out.println(file.getTotalSpace());
//获取分区空闲大小
System.out.println(file.getFreeSpace());
//获取分区可用空间大小
System.out.println(file.getUsableSpace());
}
}

运行后的结果为:

Java中的I/O流

Java中I/O流是实现输入/输出的基础,可以方便的实现输入和输出操作,Java中把不同的输入/输出源抽象表述为“流”,通过流的方式运行Java程序使用相同的方式来访问不同的输入/输出源,这里的流可以理解为字节序列,通俗一点说就是一串0和1。

流的分类

流的分类可以从以下两个角度划分:

  1. 流向:按照流的流向来分可以分为输入流和输出流,其中输入流只能从中读取数据,而不能向其写入数据;输出流只能向其写入数据,而不能从中读取数据。
  2. 读取单元:按照读取单元划分,流可以分为字节流和字符流,顾名思义所谓字节流就是输入和输出的基本单位都是字节,而字符流输入输出的基本单元都是字符。

上面提到的输入和输出都涉及到方向的问题,这里的输入和输出都是从程序运行所在的内存角度划分的,也就是说从硬盘或者网络读取到程序运行内存中的流叫做输入流,反之从内存中写入硬盘的流则叫做输出流,换句话说,输入流和输出流都是以当前程序运行的内存作为参照物。
Java中输入流主要使用InputStream和Reader作为基类,而输出流主要使用OutputStream和Writer作为基类,它们都是一些抽象类,无法直接创建实例。

流的概念模型

Java把所有设备里的有序数据抽象成流模型,简化了输入/输出处理,理解了流的概念模型也就了解了Java IO。
Java中有关IO流的类都是从如下4个抽象类中派生的。

  • InputStream/Reader:所有输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。

对于InputStream和Reader而言,它们把输入设备抽象成一个“水管”,这个水管里的每个“水滴”依次排列,如下图所示:

字节流和字符流的处理方式其实非常相似,只是它们处理的输入/输出单位不同而已。输入流使用隐式的记录指针来表示当前正准备从哪个“水滴”开始读取,每当程序从InputStream或Reader里取出一个或多个“水滴”后,记录指针自动向后移动;除此之外,InputStream和Reader里都提供一些方法来控制记录指针的移动。
对于OutputStream和Writer而言,它们同样把输出设备抽象成一个“水管”,只是这个水管里没有任何水滴。
当执行输出时,程序相当于依次把“水滴”放入到输出流的水管中,输出流同样采用隐式的记录指针来标识当前水滴即将放入的位置,每当程序向OutputStream或Writer里输出一个或多个水滴后,记录指针自动向后移动。

字节流

InputStream

InputStream是输入流的基类,读取的最小单元是字节。由于InputStream是抽象类,所以其本身并不能创建实例来执行输入。但它们将成为所有输入流的模板,所以它们的方法是所有输入流都可使用的方法。
InputStream里包含如下方法:

  • int read():从输入流中读取单个字节(相当于从如图所示的水管中取出一滴水),返回所读取的字节数据(字节数据可直接转换为int类型)。
  • int read(byte[]b):从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中,返回实际读取的字节数。
  • int read(byte[]b,int off,int len):从输入流中最多读取len个字节的数据,并将其存储在数组b中,放入数组b中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字节数。

InputStream有个子类用于读取文件的输入流:FileInputStream,下面的程序将示范FileInputStream的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cn.bytecollege.IODemo;

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

public class FileDemo5 {
public static void main(String[] args) {
String path = "D:/BYTE/study/Node.txt";
FileInputStream fileInputStream = null;
File file = new File(path);
try{
fileInputStream = new FileInputStream(file);
byte[] ch = new byte[1024];
int length;
while ((length = fileInputStream.read(ch)) != -1){
System.out.println(new String(ch));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

运行后的结果为:

OutputStream

OutputStream是输出流的基类,输入的最小单元是字节。它提供了如下方法:

  • void write(int c):将指定的字节输出到输出流中,其中c既可以代表字节,也可以代表字符。
  • void write(byte[] buf):将字节数组中的数据输出到指定输出流中。
  • void write(byte[] buf,int off,int len):将字节数组中从off位置开始,长度为len的字节/字符输出到输出流中。

同样OutputStream也有子类用于输入文件流:FileOutputStream,下面的程序将示范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
package cn.bytecollege.IODemo;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileDemo6 {
public static void main(String[] args) {
File file = new File("D:/BYTE/study/byte.txt");
try{
//创建文件
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
try{
//实例化FileOutputStream
FileOutputStream fileOutputStream = new FileOutputStream(file);;
String str = "Bulid Your Technical Excellence";
for (int i = 0;i<str.length();i++) {
int b = (int)str.charAt(i);
fileOutputStream.write(b);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

接下来,同时使用输入流和输出流来复制文件。

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
package cn.bytcollege.io.test;

import java.io.*;

public class CopyUtil {
public static void main(String[] args) {

String input = "D:/BYTE/study/Node.txt";
String output = "F:/Node.txt";
System.out.println("----------------copy start-----------------------");
long start = System.currentTimeMillis();
copy(input,output);
long end = System.currentTimeMillis();
System.out.println("-----------------copy over!-----------------------");
System.out.println("------------------"+(end-start)+"-----------------");
}
public static void copy(String input,String output) {
try{
FileInputStream fis = new FileInputStream(input);
FileOutputStream fos = new FileOutputStream(output);
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
byte[] bytes = new byte[1024*10];
int i = 0;
while ((i=bis.read(bytes))>-1){
bos.write(bytes);
}
bis.colse();
bos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

使用Java的IO流执行输出时,不要忘记关闭输出流,关闭输出流除了可以保证流的物理资源被回收之外,可能还可以将输出流缓冲区中的数据flush到物理节点里(因为在执行close()方法之前,自动执行输出流的flush()方法)

字符流

Reader

在Reader里包含如下3个方法:

  • int read():从输入流中读取单个字符(相当于如图所示的水管中取出一滴水),返回所读取的字符数据(字符数据可直接转换为int类型)。
  • int read(char[]cbuf):从输入流中最多读取cbuf.length个字符的数据,并将其存储在字符数组cbuf中,返回实际读取的字符数。
  • int read(char[]cbuf,int off,int len):从输入流中最多读取len个字符的数据,并将其存储在字符数组cbuf中,放入数组cbuf中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字符数。

对比InputStream和Reader所提供的方法,就不难发现这两个基类的功能基本是一样的,只是InputStream读取的最小单位是字节,而Reader读取的最小单位是字符。
Reader也有一个子类FileReader用于实现字符的读取,下面通过示例来学习FileReader的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package cn.bytecollege.IODemo;

import java.io.*;

public class ReadDemo {
public static void main(String[] args) throws IOException {
Reader reader = new FileReader("d:/BYTE/study/Node.txt");

int i = 0;
char[] chars = new char[8];
while((i=reader.read(chars))>-1){
System.out.println(new String(chars,0,i));
}
}
}

Writer

Writer抽象类中的方法和OutputStream方法类似,只需要将方法参数中的byte[]更换成char[]即可,字符流直接以字符作为操作单位,所以Writer可以用字符串来代替字符数组,即以String对象作为参数。Writer里还包含如下两个方法。

  • void write(String str):将str字符串里包含的字符输出到指定输出流中。
  • void write(String str,int off,int len):将str字符串里从off位置开始,长度为len的字符输出到指定输出流中。

同样Writer也有一个子类FileWriter用于实现字符的输出,下面通过示例来学习Writer的子类FileWirter的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.bytecollege.IODemo;

import java.io.*;

public class WriterDemo {
public static void main(String[] args) throws IOException {
Reader reader = new FileReader("d:/BYTE/study/Node.txt");
Writer writer = new FileWriter("F:/2.txt");
int i = 0;
char[] chars = new char[8];
while((i=reader.read(chars))>-1){
writer.write(chars);
}
writer.close();
}
}

对象序列化

对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种二进制流(无论是从磁盘中获取的,还是通过网络获取的),都可以将这种二进制流恢复成原来的Java对象。
序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。对象的序列化(Serialize)指将一个Java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize)则指从IO流中恢复该Java对象。
如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的(继承serializable接口,该接口是一个标记接口,实现该接口无须实现任何方法,它只是表明该类的实例是可序列化的)
使用Serializable来实现序列化非常简单,主要让目标类实现Serializable标记接口即可,无须实现任何方法。
一旦某个类实现了Serializable接口,该类的对象就是可序列化的,程序可以通过如下两个步骤来序列化该对象。

  1. 创建一个ObjectOutputStream
  2. 调用ObjectOutputStream对象的writeObject()方法输出可序列化对象

下面通过示例来学了对象的序列化和反序列化:

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
package cn.bytecollege.IODemo;

import java.io.Serializable;

public class Student implements Serializable {

}
package cn.bytecollege.IODemo;

import java.io.*;

public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
File file = new File("student.txt");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
Student student = new Student();
oos.writeObject(student);

oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Student newStudent = (Student)ois.readObject();
ois.close();
System.out.println(newStudent);
}
}

注意:
1.如果修改类是仅修改了方法,则反序列化不受任何影响。不需要修改serialVersionUID的值
2.如果修改类时仅仅修改了静态变量或者transient实例变量,则反序列化不受任何影响。不需要修改serialVersionUID的值。
3.如果修改类时修改了非瞬时的实例变量,则可能导致序列化版本不兼容。如果对象流中的对象和新类中包含同名的实例变量,而实例变量类型不同,则反序列化失败,类定义应该更新serialVersionUID类变量的值。如果对象流中的对象比新类中包含更多的实例变量,则多出的实例变量值被忽略,序列化版本可以兼容,类定义可以不更新serialVersionUID的值。如果新类比对象流中的对象包含更多的实例变量,则序列化版本也可以兼容,类定义可以不更新serialVersionUID的值。但反序列化的对的新对象中多出的实例变量值都是null。

课后练习

  1. 遍历任意盘,打印出该盘中所有的只读文件的名称。
  2. 遍历任意盘,打印出该盘中所有的隐藏文件的名称。
  3. 遍历任意盘,打印出该盘中所有的路径及文件名称。