谈谈 Java IO

字节流、字符流、随机访问文件、Apache IO


版权声明:本文由 Hov 所有,发布于 http://chenhy.com ,转载请注明出处。



0 写在前面

IO 操作是任何编程语言都无法回避的问题,因为 IO 操作是机器获取和交换信息的主要途径。 Java 的 IO 是以流为基础进行输入输出的,所有的数据被串行化写入输出流或从输入流读入。 java.io 提供了全面的 IO 接口,包括文件读写、标准设备输出等。

注:流是一个很形象的概念。当程序需要读取数据时会开启一个通向数据源的流,这个数据源可以是文件、内存或网络连接。同样,当程序需要写入数据时会开启一个通向目的地的流,而数据就在数据源和目的地之间“流动”。

下图为 Java IO 体系的组成结构。由图可知, Java IO 主要分为字节流和字符流,其中字节流以 InputStream 和 OutputStream 向下继承,字符流以 Reader 和 Writer 向下继承。

本文将详细讲解字节流和字符流,并简要介绍随机存取文件 RandomAccessFile 和 Apache 的 IO 类库。



1 预备知识

在讲解 IO 相关的知识前,我们有必要了解一下字节、字符、编码等概念。毕竟 IO 操作实质上是基于字节、字符来实现的。

1.1 字节

字节是通过网络传输信息或硬盘、内存存储信息的单位,也是计算机用于计量存储容量和传输容量的一种计量单位。一个字节等于 8 位二进制,是一个很具体的存储空间,如 0x01,0x20 。

1.2 字符

字符是人们使用的记号,抽象意义上的一个符号。如“1”,“a”,“¥”,“好”等。实质上,字符是由字节组成的,一个字符可以由一个或多个字节组成。

1.3 字符集

不同国家和地区所制定的不同 ANSI 编码标准中,都只规定了各自语言所需的字符。汉字标准(GB2312)就是一个字符集,它规定的是汉字的编码标准。

字符集(编码)有两层含义:

  1. 使用哪些字符,也就是哪些字符、汉字或符号会被收入标准中,所包含“字符”的集合就叫做“字符集”;
  2. 规定每个字符分别用一个字节还是多个字节存储,用哪些字节存储,这个规定就叫做编码。 字符集和编码一般同时制定,字符集除了有“字符集合”含义外,还包含了“编码”的含义。

值得注意的是,为了使国际间信息交流更方便,国际组织制定了 UNICODE 字符集,它为各种语言中的每一个字符设定了统一且唯一的数字编号,以满足跨平台、跨语言进行文本交换、处理的要求,如我们熟悉的 UTF-8 。


2 字节流与字符流

Java IO 流可分为字节流和字符流两大类,两种流都可以实现输入输出操作。其中,字节流继承自 InputStream 和 OutputStream ,字符流继承自 Reader 和 Writer 。

2.1 字节流

字节流可以处理所有类型的数据,如图片、视频、文字等。字节流每读取一个字节就返回一个字节,字节流在 Java 中对应的类通常以“Stream”结尾。

2.1.1 字节输入流:InputStream

我们可以通过 InputStream 读取字节数据。 InputStream 类的定义如下:

public abstract class InputStream extends Object implements Closeable{
    ......
}

InputStream 是一个抽象类,这意味着其必须依靠其子类实现具体的功能。比如从文件中读取数据,我们可以用 FileInputStream 来实现, FileInputStream 就是 InputStream 的一个子类。当然, InputStream 下面还有其它的子类,如缓冲输入流 BufferedInputStream 。

(1)通过 FileInputStream 读取文件数据

/* ReadByte.java */

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

public class ReadByte {
    public static void main(String[] args) {
        try {
            //计时开始
            long times = System.currentTimeMillis();  
            //实例化FileInputStream对象
            File file = new File("Data.zip");
            FileInputStream fis = new FileInputStream(file);
            //读取文件内容
            byte[] input=new byte[500];
            int count = 0;        //记录读取次数
            while(fis.read(input)!=-1){
                count++;
            }
            //记得关闭流
            fis.close();   
            //计时结束
            System.out.println("用时: "+ (System.currentTimeMillis()-times) + "ms");  
            System.out.println("读取次数: "+count);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

程序运行结果:

用时: 117ms
读取次数: 188206

(2)通过 BufferedInputStream 读取文件数据

前面已经通过 FileInputStream 读取了文件数据,接下来通过缓冲的方式来读取同样的文件。两者实现的效果一致,对比它们之间的效率。值得注意的是,下面的代码就比上面多出两行(实例化缓冲流和关闭缓冲流)。

/* BufferedRead.java */

import java.io.*;

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

            long times = System.currentTimeMillis();

            File file = new File("Data.zip");
            FileInputStream fis = new FileInputStream(file);
            BufferedInputStream bis = new BufferedInputStream(fis); //实例化缓冲输入流

            byte[] input=new byte[500];
            int count = 0;
            while(bis.read(input)!=-1){
                count++;
            }

            bis.close();                //关闭缓冲流
            fis.close();

            System.out.println("用时: "+ (System.currentTimeMillis()-times) + "ms");
            System.out.println("读取次数: "+count);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

程序运行结果:

虽然读取同样的次数,但缓冲的方式用时明显减少了。

用时: 52ms
读取次数: 188206

注意:缓冲区大小、读取文件的次数会影响读取效率。比如,减少缓冲区大小,读取次数将增多,缓冲的方式可效率更快;如果增加缓冲区大小,读取次数将减少,有无缓冲可能效率差别不大。

2.1.2 字节输出流:OutputStream

我们可以通过 OutputStream 输出字节数据。 OutputStream 类的定义如下:

public abstract class OutputStream extends Object implements Closeable,Flushable{
    ......
}

同 InputStream 一样,OutputStream 也是一个抽象类。要想使用 OutputStream ,必须通过子类实例化对象。同样,我们可以通过 OutputStream 的子类 FileOutputStream 往文件写入数据。

注:Closeable 表示可以关闭的操作,因为程序运行到最后必须关闭; Flushable 表示刷新,清空内存的数据。

(1)通过 FileOutputStream 往文件写入数据

/* WriteByte.java */

import java.io.*;

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

            //实例化 FileOutputStream
            File file = new File("OutputData.txt");
            FileOutputStream fos = new FileOutputStream(file);
            //往文件写入字符串数据
            String str = "Hello China. 中文";
            byte[] b = str.getBytes("UTF-8");
            fos.write(b);
            //关闭流
            fos.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

程序运行结果:

文件 OutputData.txt 将被写入数据 “Hello China. 中文”。


2.2 字符流

不同于字节流可以处理多种类型的数据,字符流仅能处理纯文本数据,如 txt 文本。字符流读取一个或多个字节时,先查找编码表,再将查到的字符返回。字符流在 Java 中对应的类通常以“Reader”和”Writer”结尾。

2.2.1 字符输入流:Reader

通过 Reader 以字符的方式读取数据, Reader 类的定义如下:

public abstract class Reader extends Objects implements Readable,Closeable{
    ......
}

(1)通过 InputStreamReader 读取数据

/* ReadChar.java */

import java.io.*;

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

            //实例化 InputStreamReader 对象
            File file = new File("text.txt");
            FileInputStream fis = new FileInputStream(file);
            InputStreamReader isr = new InputStreamReader(fis);
            //读取数据
            char input[] = new char[100];
            int l = 0;      //一次读取了多少数据
            while((l=isr.read(input))!=-1){
                System.out.println(new String(input,0,l));
            }
            //关闭流
            isr.close();
            fis.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

程序运行结果:

控制台将输出 text.txt 文件的内容。

2.2.2 字符输出流:Writer

通过 Writer 以字符的方式输出数据, Writer 类的定义如下:

public abstract class Writer extends Object implements Appendable,Closeable,Flushable{
    ......
}

(1)通过 OutputStreamWriter 往文件写入数据

/* WriteChar.java */

import java.io.*;

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

            //实例化 OutputStreamWriter 对象
            File file = new File("OutputData.txt");
            FileOutputStream fos = new FileOutputStream(file);
            OutputStreamWriter osw = new OutputStreamWriter(fos);
            //往文件写入字符串数据
            String str = "Hello China.";
            osw.write(str);
            //关闭流
            osw.close();
            fos.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

程序运行结果:

字符串”Hello China.“将被写入 OutputData.txt 文件。

2.2.3 FileReader 和 FileWriter

同样,我们也可以通过 FileReader 和 FileWriter 来读写文件数据。

/* FileReaderWriter.java */

import java.io.*;

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

            //实例化 FileReader 对象
            FileReader fr = new FileReader("text.txt");
            BufferedReader br = new BufferedReader(fr);
            //实例化 FileWriter 对象
            FileWriter fw = new FileWriter("outputdata.txt");  
            BufferedWriter bw = new BufferedWriter(fw);
            //从 Reader 读取数据,往 Writer 写入数据
            String line = null;
            while((line=br.readLine())!=null){
                bw.write(line+"\n");
            }
            //刷新缓冲区
            bw.flush();
            //关闭流
            br.close();
            bw.close();
            fr.close();
            fw.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

程序运行结果:

文件 text.txt 中的内容将被写入 outputdata.txt 文件。

2.3 字节流与字符流的区别

前面我们已经介绍了字节流、字符流,并给出了具体的实现代码。当然,Java IO 并不仅限于上面提到的内容,但其它类的实现思路基本一致。字节流和字符流使用也非常相似,对比上面通过字节流和字符流分别从文件读取数据的代码你会发现,可在字节流的基础上包装成字符流来读取字符数据。当然,字节流和字符流还是有区别的:

  1. 字节流不使用缓冲区(内存),直接操作文件本身;字符流使用缓冲区。

  2. 字节流即使不关闭资源(close方法),也能输出内容;但字符流不使用 close 方法的话,则不会输出任何内容, 这正说明字符流使用的是缓冲区,这也是为什么我们要用 flush 方法强制刷新缓冲区,这样才能在不 close 的情况下输出内容。

那究竟用字节流好还是用字符流好呢?

字节流会比字符流更通用。字节流可以处理所有类型的数据,如视频、图片。字符流对视频或图片的读写就显得无能为力了,因为它只能处理纯文本数据。事实上,在硬盘上保存文件或进行传输时都是以字节的方式进行的,包括图片也是按字节完成,所以使用字节的操作是最多的。如果要通过 java 程序实现一个拷贝功能,显然应该选用字节流进行操作(因为可能拷贝的是图片),并且可采用边读边写的方式(节省内存)。


3 随机访问文件:RandomAccessFile

RandomAccessFile 唯一的父类是 Object ,这与其他流有所不同。它是一个完全独立的类,所有方法(绝大多数都只属于它自己)都是从零开始写的,它的行为与其它的 IO 类有些根本性的不同。

RandomAccessFile 一个很重要的特性就是它支持从文件的任意位置读写。

为了突出 RandomAccessFile 的特性,我们通过多个线程来操作同一个文件。

(1)通过线程往文件写入数据

/* WriteFile.java */

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

public class WriteFile extends Thread{
    File file;
    int block;
    int L = 100;

    public WriteFile(File f,int b){
        this.file = f;
        this.block = b;
    }

    @Override
    public void run() {
        try {

            //实例化 RandomAccessFile 对象
            RandomAccessFile raf = new RandomAccessFile(file,"rw");
            raf.seek((block-1)*L);                              //文件写入位置
            raf.writeBytes("\n"+"This is block "+block);        //写入数据

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

(2)模拟多个线程同时写文件

/* MultiWriteFile.java */

import java.io.File;

public class MultiWriteFile {
    
    static File file = new File("test.txt");    //文件

    public static void main(String[] args) {

        if(file.exists()){
            file.delete();
        }

        //开启5个线程往同一个文件写入数据,注意写入顺序是 1、5、4、3、2
        new WriteFile(file,1).start();
        new WriteFile(file,5).start();
        new WriteFile(file,4).start();
        new WriteFile(file,3).start();
        new WriteFile(file,2).start();

    }
}

程序运行结果:

虽然写入顺序是 1、5、4、3、2,但它们都分别写入各自的区块位置,所以在文件中的顺序变回了 1、2、3、4、5。这就说明了 RandomAccessFile 可以从文件的任意位置写入数据,而不需要从头往后写数据。

注:后面的方框说明该区域没有数据,因为每个区块是100,我们只写入了15个数据。

(3)同样,我们可以读取文件中任意位置的数据

/* MultiWriteFile.java */

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

public class MultiWriteFile {

    static File file = new File("test.txt");
    
    public static void main(String[] args) {
        try {

            //实例化 RandomAccessFile 对象
            RandomAccessFile raf = new RandomAccessFile(file,"r");
            //文件读写位置:200
            raf.seek(200);
            //读取数据
            byte[] str = new byte[16];
            raf.read(str);
            String in = new String(str);
            System.out.println(in);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

程序运行结果:

成功从位置 200 处读取数据,输出 “This is block 3”。


4 Apache IO

前面我们已经讲解了 Java 中常用的 IO 操作,其实我们还可以通过外部的包来使用 IO。比如, Apache 提供了一个功能强大的包来实现 IO 操作,通过它可以大量减少我们的实现代码,操作十分方便。

Apache IO下载地址: http://commons.apache.org/proper/commons-io/download_io.cgi

下载完毕后,将 jar 文件导入工程就可以使用了。

下面简单展示如何使用 Apache IO 实现文件拷贝。当然,还有其它强大的功能,大家有兴趣可以查看官方文档如何使用。

(1)通过 Apache IO 实现文件拷贝

/* TestApacheIO.java */

import org.apache.commons.io.FileUtils;

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

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

        File file = new File("text.txt");
        File newfile = new File("textnew.txt");

        try {
            //调用 FileUtils 的 copyFile 方法,实现文件拷贝
            FileUtils.copyFile(file,newfile);   
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

程序运行结果:

text.txt 文件将被拷贝到 textnew.txt 文件。


5 结语

  1. Java IO 可分为字节流和字符流两大类;
  2. 字节流能处理所有类型的数据,如视频、图片、文本;字符流只能处理纯文本数据;
  3. 字节流直接对文件操作,不需要缓冲区,直接输出内容;字符流需要缓冲区,刷新缓冲区才能输出内容。
  4. 随机访问文件 RandomAccessFile 支持从文件任意位置读写数据;
  5. Apache IO 提供了 IO 操作的简便接口。


参考资料:


 
comments powered by Disqus