Java IO流最全详解
前言
一个程序总是离不开数据的输入(Input)输出(IO),而Java IO流就是用于程序从外部读取数据,以及向外部写出数据的java处理包,本文详细讲解Java IO包中常用的IO流,通过本文的知识,可以使得你在开发中随心所欲的高效使用IO流。
一、IO流简介
以流的方式进行输入输出的操作就是IO(Input Output)流。流是一个抽象的概念,它是一串连续动态的数据集合,可以将流理解为水管,而数据就是水,水通过水管从一端流到另一端。
输入流是指读的操作,指从外部设备向程序内存中读取数据的过程。
输出流是指写的操作,指从程序内存中向外部设备写入数据的过程。
二、JAVA IO流的分类
1. 按流的方向分类
输入流:侧重于读,数据从数据源读入程序。
输出流:侧重于写,数据从程序写出到目的地。
2. 按处理的数据单元分类
字节流:以 字节(1Byte=8bit) 为单位进行读取或者写出数据。
字符流:以 字符为单位进行读取或者写出数据。
比如图片本身就是以二进制存储的,因此采用字节流。
比如文本本身是以字符存储的,因此采用字符流。
3. 按处理对象分类
节点流:直接处理数据源或者目的地数据的读写。
处理流:不直接连接到数据源或目的地,而是为了提高节点流的效率,对节点流进行处理的流,主要指缓冲流。
三、Java中四大IO抽象类
Java IO包中的类结构图如图所示。
上面说道IO主要用于数据的读写,因此从上图也可以看出,IO包主要包括四大抽象类,然后基于这四大抽象类继承从众多的子类,这四大抽象类分别包括Writer、OutputStream、InputStream、Reader,其中Writer和OutputStream都属于输出流,只是Writer是字符输出流,OutputStream是字节输出流;同理,InputStream和Reader都属于输入流,只是Reader是字符输入流,InputStream是字节输入流。Java IO包对于字节流和字符流的命名都很规范,类名中包含Stream的都是字节流,不包含Stream的都是字符流。
四、JAVA IO包中常用的流对象及使用方法
1. 字节流
1.1 文件字节读(输入)写(输出)流
- Java IO包中以字节流进行读取文件的类是FileInputStream,它是InputStream(字节输入流)抽象类的子类。
- 如使用文件输入流读取文本文件的示例代码如代码1所示:
//代码1
public static void main(String[] args) throws IOException {
//1. 连接数据:FileInputStream 直接与数据源连接,因此属于节点流
FileInputStream fis = new FileInputStream("D:\\1.txt");
//2. 循环逐个读取字节
int temp=0;
StringBuilder sb = new StringBuilder();
while ((temp=fis.read())!=-1)//如果文件中的字节读取完毕,则返回-1,否则返回该字节数值(0-255)
sb.append((char)temp);
System.out.println(sb.toString());
//3. 关闭流
fis.close();
}
- Java IO包中以字节流进行写出数据的类是FileOutputStream,它是OutputStream(字节输出流)抽象类的子类。
- 如使首先使用文件输入流读取图片数据,然后使用文件输出流将读取的数据写出到图片文件中,代码示例如代码2所示:
//代码2
public static void main(String[] args) throws IOException {
//1. 连接数据源:直接与数据源连接,因此属于节点流
FileInputStream fis_img = new FileInputStream("D:/1.jpg");
//2. 连接目的地:直接与目的地连接,因此属于节点流
FileOutputStream fos_img = new FileOutputStream("D:/2.jpg");
//3. 逐个读取字节,并将字节逐个写出
int temp=0;
while ((temp=fis_img.read())!=-1)
{
fos_img.write(temp);
}
//4. 关闭流
fos_img.flush();
fis_img.close();
fos_img.close();
}
1.2 基于缓冲区的文件字节读写流(高效)
上面所介绍的FileInputStream及FileOutputStream都是一个字节一个字节的读取或者写出数据,那么如果我们每次读取多个字节,同时将多个字节写出,自然比一个一个字节读写的效率高。
这就好比从果园搬100斤的苹果回家,如果你每次拿一个回家,那肯定要很久,而如果你准备一个可以装20斤苹果的麻袋,每次都装满一麻袋带回家,拿5趟就拿完了。这里的麻袋就是缓冲区,每次将多个字节装入缓冲区,然后一次性从缓冲区中写出多个字节。
- 如对代码2,通过缓冲区进行改造,示例代码如代码3所示:
//代码3
public static void main(String[] args) throws IOException {
FileInputStream fis_img = new FileInputStream("D:/1.jpg");
FileOutputStream fos_img = new FileOutputStream("D:/2.jpg");
int temp=0;
byte[] buffer = new byte[1024];//设置缓冲区,缓冲区大小去2^n合适
while ((temp=fis_img.read(buffer))!=-1)//每次读取多个字节存入缓冲区
{
fos_img.write(buffer,0,temp);//每次将缓冲区中的多个字节写出
}
fos_img.flush();
fis_img.close();
fos_img.close();
}
上面是手动设置的缓冲区大小,对于较小的文件而言,我们明明可以一次性搞定,就没必要分多次,因此将缓冲区的大小设置为文件内容的实际大小即可,为满足这一需求,FileInputStream对象提供了一个估算文件字节个数的方法available,通过该方法获取文件字节个数,然后将其设置为缓冲区大小,即可实现一次性读写文件内容,最大的提高文件读写效率。很明显,缓冲区的空间来自内存,缓冲区越大,那么占用的空间越大,因此,一次性读写数据只适用于较小的数据,如果一次读写较大的数据会占用过多的内存,导致计算机崩溃,或者性能降低。
- 对代码3进行改造,通过available方法设置缓冲区大小,进而提高读写效率的示例代码,如代码4所示。
//代码4
public static void main(String[] args) throws IOException {
FileInputStream fis_img = new FileInputStream("D:/1.jpg");
FileOutputStream fos_img = new FileOutputStream("D:/2.jpg");
int temp=0;
//fis_img.available()直接获取数据的长度,类似一次搬完,不用分批,
//但这比较占内存,因此适用于小文件,比如一个人只能扛100斤苹果,你让他一次扛200斤,那就累趴了。
byte[] buffer = new byte[fis_img.available()];
fis_img.read(buffer);
fos_img.write(buffer);
fos_img.flush();
fis_img.close();
fos_img.close();
}
根据通过缓冲区提高IO效率的思想,Java提供了BufferedInputStream和BufferedOutputStream两个处理流,来实现通过缓冲区读写文件。
- 基于java提供的缓冲处理流对代码3进行改造,示例代码如代码5所示:
//代码5
public static void main(String[] args) {
FileInputStream fis=null;//节点流
FileOutputStream fos=null;//节点流
BufferedInputStream bis=null;//处理流:缓冲流,实现原理就是通过缓冲区提高读效率
BufferedOutputStream bos=null;//处理流:缓冲流,实现原理就是通过缓冲区提高写效率
try {
fis = new FileInputStream("D:/1.jpg");
bis=new BufferedInputStream(fis);
fos=new FileOutputStream("D:/2.jpg");
bos=new BufferedOutputStream(fos);
//默认缓冲区大小:DEFAULT_BUFFER_SIZE = 8192;
int temp=0;
while ((temp=bis.read())!=-1)
{
bos.write(temp);
}
bos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
//流的关闭顺序:后开的先关
try {
if (bis!=null)
bis.close();
if (fis!=null)
fis.close();
if (bos!=null)
bos.close();
if (fos!=null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.3 字节数组流
ByteArrayInputStream将内存中的字节数组对象作为数据源,示例代码如代码6所示。
//代码6
public static void main(String[] args) throws IOException {
byte[] source="mekeater".getBytes();
//内存中的字节数组对象作为数据源
ByteArrayInputStream bis = new ByteArrayInputStream(source);
int temp=0;
StringBuilder sb = new StringBuilder();
while ((temp=bis.read())!=-1)
{
sb.append((char) temp);
}
System.out.println(sb.toString());
bis.close();
}
ByteArrayOutputStream将读取的数据写入到字节数组输出流中,示例代码如代码7所示。
//代码7
public static void main(String[] args) throws IOException {
//将流中的数据写入到字节数组中
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write('m');
bos.write('e');
bos.write('k');
bos.write('e');
bos.write('a');
bos.write('t');
bos.write('e');
bos.write('r');
byte[] bytes = bos.toByteArray();
for (byte b : bytes) {
System.out.print((char) b);
}
bos.close();
}
1.4 随机访问流
上面几种流读取内容,都是从第一个字节开始逐个往后读取,直至结束,而RandomAccessFile可以指定开始查找的字节位置,如对一个文件采用随机访问流进行读取示例代码如代码8所示。
//代码8
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("D:/2.txt", "rw");
for (int i = 0; i < 10; i++) {
raf.writeInt(i);
}
raf.seek(4);//seek指定开始查找的位置从第4个字节开始,一个int整数占4个字节,因此从第二个位置查找
System.out.println(raf.readInt());
//隔一个位置查找一个
for (int i = 0; i < 10; i+=2) {
raf.seek(i*4);
System.out.printf(raf.readInt()+"\t");
}
System.out.println();
//seek指定位置,重新写入该位置的数据,替换原有数据
raf.seek(4);//指定第4个字节位置,即第二个整数
raf.writeInt(66);
raf.seek(0);//重新定位查找的起始位置
for (int i = 0; i < 10; i++) {
System.out.print(raf.readInt()+"\t");
}
}
1.5 数据流(实现基本类型数据序列化到文件)
- DataOutputStream将基本数据类型的数据以字节流形式输出到本地文件(类似序列化,但是它只能对基本数据类型进行操作,不能对java自定义对象进行操作),基本操作示例代码如代码9所示。
//代码9
public static void main(String[] args) throws IOException {
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("D:/data")));
//写出基本数据类型数据文件中
dos.writeChar('m');
dos.writeBoolean(false);
dos.writeDouble(Math.random());
dos.writeInt(66);
dos.writeUTF("Mekeater");
dos.flush();
dos.close();
}
- DataInputStream从文件中读取存储的基本数据类型的数据,示例代码如代码10所示。
//代码10
public static void main(String[] args) throws IOException {
DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream("D:/data")));
//注意读取数据顺序要去写入数据一致
System.out.println(dis.readChar());
System.out.println(dis.readBoolean());
System.out.println(dis.readDouble());
System.out.println(dis.readInt());
System.out.println(dis.readUTF());
dis.close();
}
1.6 对象流(实现基本类型数据及自定义对象数据序列化到文件)
对象本身也是数据,如果能将内存中对象数据存储到文件,然后通过网络传输该对象,那么对于程序中数据的保存及共享具有重要意义,而序列化和反序列化就是实现这一思想的。
序列化:将程序内存中的任何数据以二进制文件的形式进行保存,并通过网络传输。Java IO中提供了ObjectOutputStream类实现对象的序列化。’
反序列化:将二进制文件恢复为对象数据的过程。Java IO中提供了ObjecInputStream类实现对象的序列化。
通过对象序列化可以实现对象的持久化及网络通信功能。
- 对象序列化示例代码如代码11所示
//代码11
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("D:/data1")));
//系列化基本数据类型
oos.writeChar('m');
oos.writeBoolean(false);
oos.writeDouble(Math.random());
oos.writeInt(66);
oos.writeUTF("Mekeater");
序列化自定义对象(对象能够序列化,必须实现Serializable标记接口)
User user = new User(18, "mekeater", true);
oos.writeObject(user);
oos.flush();
oos.close();
}
//实现序列化接口的对象
public class User implements Serializable {
private int age;
private String name;
private boolean isMan;
public User() {
}
public User(int age, String name, boolean isMan) {
this.age = age;
this.name = name;
this.isMan = isMan;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isMan() {
return isMan;
}
public void setMan(boolean man) {
isMan = man;
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", name='" + name + '\'' +
", isMan=" + isMan +
'}';
}
}
- 对代码11中所序列化的二进制文件进行反序列化,恢复对象数据的示例代码如代码12所示
//代码12
public static void main(String[] args) throws IOException {
ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream("D:/data1")));
//反序列化数据,读取必须和写入顺序一致
System.out.println(ois.readChar());
System.out.println(ois.readBoolean());
System.out.println(ois.readDouble());
System.out.println(ois.readInt());
System.out.println(ois.readUTF());
User user = (User)ois.readObject();
System.out.println(user.getAge());
System.out.println(user.getName());
System.out.println(user.isMan());
ois.close();
}
2. 字符流
2.1 文件字符读(输入)写(输出)流
- FileWriter类是Writer抽象类的子类,它实现==将数据以字符为单位写入文件。==示例代码如代码13所示。
//代码13
public static void main(String[] args) throws IOException {
FileWriter fw = new FileWriter("D:/2.txt");//如果文件存在,则默认覆盖
//如需追加需要设置参数
//FileWriter fw = new FileWriter("D:/2.txt",true);
fw.write("hello IO Stream\r\n");//换行
fw.write("Mekeater come on!");
fw.flush();
fw.close();
}
- FileReader类是Reader抽象类的子类,它实现将以字符为单位读取文件内容示例代码如代码14所示。
//代码14
public static void main(String[] args) throws IOException {
FileReader frd = new FileReader("D:/2.txt");
int temp=0;
while ((temp=frd.read())!=-1)
{
System.out.println((char) temp);
}
frd.close();
}
2.2 基于缓冲区的文件字符读写流(高效)
与1.2 基于缓冲区的文件字节读写流思想一致,文件字符流通常是逐个字符读写内容,效率较低,我们可以通过开辟一块空间(数组)每次将读写的数据装满该空间,达到一次读取或者写出多个字符的效果,进而提高读写效率。
- 逐个字符读写内容,实现文件拷贝功能示例代码如代码15所示:
//代码15
//通过字符流实现(但这是一个字符一个字符的读写,效率低)
public static void CopyFile(String source, String target) throws IOException {
FileReader fr = new FileReader(source);
FileWriter fw = new FileWriter(target);
int temp=0;
while ((temp=fr.read())!=-1)
{
fw.write(temp);
}
fw.flush();
fr.close();
fw.close();
}
- 通过缓冲机制对代码15进行改造,示例代码如代码16所示:
//代码16
//通过字符流实现(通过缓冲区提高读写效率)
public static void CopyFileBuffer(String source, String target) throws IOException {
FileReader fr = new FileReader(source);
FileWriter fw = new FileWriter(target);
int temp=0;
char[] buffer = new char[1024];//这个长度只能自己指定,不能像字节流可以获取大概的总长度
while ((temp=fr.read(buffer))!=-1)
{
fw.write(buffer,0,temp);
}
fw.flush();
fr.close();
fw.close();
}
- Java IO包提供了BufferedReader类及BufferedWriter类两个处理流,实现缓冲区的思想,具体使用的示例代码如代码17所示:
//代码17
public static void main(String[] args) throws IOException {
FileWriter fw = new FileWriter("D:/2.txt");//节点流
BufferedWriter bw= new BufferedWriter(fw);//缓冲处理流
bw.write("BufferedWrite context");
bw.newLine();
bw.write("I am Mekeater");
bw.flush();
bw.close();
fw.close();
}
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("D:/1.txt");//节点流
BufferedReader br = new BufferedReader(fr);//处理流
String temp="";
while ((temp=br.readLine())!=null)
System.out.println(temp);
br.close();
fr.close();
}
3. 转换流
有时候我们能够方便的获取字节流对象,但是我们需要将字节流转为字符流,这就需要Java IO提供的转换流了,它能将字节流转换为字符流。
如我们可以通过System.in获取用户输入的键盘数据,System.out向控制台输出用户输入的数据,但是System.in和System.out返回的都是字节流对象,而我们需要将字节流转为字符流进行操作更为方便,Java IO提供了InputStreamReader类实现输入字节流转为读取字符流,OutputStreamWriter类实现输出字节流转为写出字符流,具体转换代码如代码18所示。
//代码18
public static void main(String[] args) throws IOException {
//InputStreamReader将字节输入流转为字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
//OutputStreamWriter将字节输出流转为字符输出流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
String line="";
while (true)
{
bw.write("请输入:");
bw.flush();
line=br.readLine();
if (line.equals("exit"))
break;
bw.write(line);
bw.newLine();
bw.flush();
}
}
4. File类
4.1 操作文件常用方法
public static void main(String[] args) throws IOException {
File file = new File("D:/2.txt");//连接文件
System.out.println(file.createNewFile());//创建文件(若存在,则默认覆盖,不存在,则创建)
System.out.println(file.exists());//判断文件是否存在
System.out.println(file.getName());//获取文件名
System.out.println(file.getAbsolutePath());//获取文件的绝对路径
System.out.println(file.isHidden());//判断文件是否为隐藏文件
System.out.println(file.delete());//删除文件
}
4.2 操作目录常用方法
public static void main(String[] args) throws IOException {
File file = new File("D:/a");//连接目录
System.out.println(file.mkdir());//创建单级目录
File file1 = new File("D:/a/b/c");
System.out.println(file1.mkdirs());//创建多级目录
System.out.println(file.isFile());//判断是否为文件
System.out.println(file.isDirectory());//判断是否为目录
System.out.println(file1.getParent());//获取目录的上一级目录路径
File file2 = new File("D:/");
String[] list = file2.list();//获取目录下的所有文件名
for (String s : list) {
System.out.println(s);
}
System.out.println("=====================");
File[] files = file2.listFiles();//获取目录下的所有文件对应的File类
for (File file3 : files) {
System.out.println(file3.getAbsolutePath());//获取目录下的所有文件的绝对路径
}
}
五、Apache IO包扩展
JAVA IO包都是非常基础的IO操作,使用复杂,Apache提供了更高层次的,使用更加方便的IO包Commons IO,通过该包可以更方便对IO进行操作。
Commons IO下载过程:
- 访问官网:https://www.apache.org/
- 拉到官网最下方,找到Commons
- 点击Commons,在组件中找到IO
- 点击IO,选择指定版本下载
- 解压下载的压缩包,查看docs文件夹下的index.html,查看该包的相关方法