Java IO总结

Java中IO的内容非常丰富,相信第一次学习的时候,所有人都会被一大堆API绕晕,今天我们就来系统的总结一下.

Java的IO体系主要包括流式部分(各种IO流的操作)及非流式部分(文件的实体)。

IO流

Java中的IO流实际上用起来都非常方便简单,为我们封装了大量实用性的接口,同时采用了装饰器设计模式,所以看起来很庞大,但还是比较清晰的,总览见下图(图片来源于网络)



可见分为字符流和字节流两大部分,每部分又有输入输出之分,首先先了解一下字符流和字节流的区别:

字符流处理的单元为2个字节的Unicode字符,主要是操作字符、字符数组或字符串,而字节流处理单元为1个字节,操作字节和字节数组。所以字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的。一般而言,音频文件、图片、歌曲之类的使用字节流,如果是涉及到中文(文本)的,用字符流好点

1.字节流

1.1 FileInputStreamFileOutputStream

这两个类是专门操作文件的。这两个类是直接继承于装饰器模式的顶层类:InputStream和OutputStream。是字节流体系中文件操作的核心。由于是装饰器模式,所以他们的功能也最简单。

单从方法上看,他们都只有寥寥几个方法,主要是读和写(详细APi可以参考官方文档,标题的超链接就是每个类的文档,这里不复述了,每各类只记录关键用法,下面也一样)下面我们举一个拷贝文件的例子:

        try (FileInputStream inputStream = new FileInputStream(new File("file/test.png"));
             FileOutputStream outputStream = new FileOutputStream(new File("file/copy.png"))){
                byte[] b = new byte[1024];
                int len = 0;
                while ((len = inputStream.read(b))!=-1){
                    outputStream.write(b,0,len);
                }
            } catch (Exception e) {
            e.printStackTrace();
        }

这算是IO操作的标准写法了。上面代码中应用了try-with-resources语法,这时Java1.7的新特性。主要是为了帮我们在IO操作或与之类似的操作中,摆脱无尽的try-catch语句,想一想,之前若没有这样写法,上面代码应该是这样的:

        FileInputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {
            inputStream = new FileInputStream(new File("file/test.png"));
                outputStream = new FileOutputStream(new File("file/copy.png"));
                byte[] b = new byte[1024];
                int len = 0;
                while ((len = inputStream.read(b))!=-1){
                    outputStream.write(b,0,len);
                }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream!=null)
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            if (outputStream!=null)
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }

try-with-resources语法只是自动帮我们调用了close方法,不仅仅是用在流上,所有实现了AutoCloseable接口的类都可以。而AutoCloseable只有一个close方法。下面的实例我都会采用这种语法。

最后还有比较重要的一点,在FileOutputStream中,默认对文件操作是覆盖形式的,也就是打开一个文件后不管写不写东西都会讲文件内容清空,若想追加的形式操作,可利用他的两参数构造,第二个布尔型参数传入true即可。后面类中对文件操作需要传入FileOutputStream参数时,也是一样。另外字符流也有类似操作,可参看API,下面就不多说了。

1.2 ObjectInputStreamObjectOutputStream

这两个类用的也很多,用于存储对象。曾经有过一次开发经验,就是从一个xml文件中解析内容,如果解析完之后序列化存储到文件中,下次再反序列化时用的时间远远少于从xml文件中直接解析。

接下来我们就来认识一下他们,以ObjectInputStream为例(ObjectOutputStream也一样,对应读方法都有相应的写方法),他除了有基本的read方法,还有许多如readInt,readBoolean等直接读出对应类型的方法,另外还有一个很重要的readObject()方法,这个是通用的,可以读一切对象,下面简单举一个例子:

public static void write(){
        ArrayList<String> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        list.add("ddd");
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(new File("file/obj")))){
            out.writeObject(list);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void read(){
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(new File("file/obj")))){
            ArrayList<String> list = (ArrayList<String>) in.readObject();
            System.out.println(list.get(1));
        }catch (Exception e){
            e.printStackTrace();
        }
    }

这里有两个问题需要额外关注一下:

  • 1.写的时候,所有对象都必须可序列化,如集合内的所有对象。序列化知识参考这里

  • 2.关于如何判断读完的问题。首先如果不加限制的读,一个文件中所有对象都读完后,再读是不会读到null之类的,而是直接抛出java.io.EOFException异常,所以我们要加以控制。第一个办法可以在写的时候最后添加一个空对象,每读一个对象判断一次,读到null的时候不再读了。第二个办法是将所有要存东西添加到集合中,读的时候永远只读一次。最后一个办法是利用异常判断,捕获到异常后在finally代码块中处理,不过这种方法影响性能,不提倡。

1.3 DataInputStreamDataOutputStream

这一对更像是上面的简化版,他只能读写基本数据类型,示例:

    public static void write(){
        ArrayList<String> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        list.add("ddd");
        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(new File("file/obj")))){
            for (String str : list)
                out.writeUTF(str);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void read(){
        try (DataInputStream in = new DataInputStream(new FileInputStream(new File("file/obj")))){
            System.out.println(in.readUTF());
            System.out.println(in.readUTF());
            System.out.println(in.readUTF());
            System.out.println(in.readUTF());
        }catch (Exception e){
            e.printStackTrace();
        }
    }

由于是读写基本数据类型,所以不用担心序列化问题,但是还是要处理EOFException异常,和上面类似,只不过不能写集合了,可以在第一个位置写入对象数,后面根据数量取对象。或者写入特殊标志位也可以。

1.4 ByteArrayInputStreamByteArrayOutputStream

这两个类的共同点都是有一个字节数组作为缓冲区。

先看ByteArrayInputStream,该类主要是从一个字节数组中读内容,他的两个构造都需要传入字节数组,示例

        try(ByteArrayInputStream bi = new ByteArrayInputStream("adsd".getBytes())){
            System.out.println((char)bi.read());
            System.out.println(bi.available());
        }catch (Exception e){
            e.printStackTrace();
        }

再看ByteArrayOutputStream,它有两个构造方法:

public ByteArrayOutputStream()
public ByteArrayOutputStream(int size)

第一个默认创建长度为32的数组,第二个指定数组大小。该类所有写方法,都是把内容写到字节缓冲区中,最后可以调用toString或者toByteArray()取出,还可以查询写入的长度。示例:

        try(ByteArrayOutputStream bo = new ByteArrayOutputStream()){
            bo.write("adsd".getBytes());
            System.out.println(bo.toString());
            System.out.println(bo.size());
        }catch (Exception e){
            e.printStackTrace();
        }

这两个类可以和FileInputStream与FileOutputStream 类比,FileInputStream与FileOutputStream 是读写文件,ByteArrayInputStream和ByteArrayOutputStream则是读写内存,也就是内存中的字节数组,可以把字节数组看做虚拟文件。

1.5 BufferedInputStreamBufferedOutputStream

我们在学习FileInputStream与FileOutputStream时就考虑到了,如果一字节一字节的从硬盘中读写,由于每次都需要启动硬盘寻找数据,效率很低,所以我们设置了一个数组作为缓冲。而这两个类自带缓冲,默认大小为8192,可以自己设定。每次读的时候他会一次读满缓冲,然后再从缓冲中取数据,写的时候也类似。

基本用法和FileInputStream与FileOutputStream类似,我们这里对二者做一个比较,看缓冲有没有用:

long start = System.currentTimeMillis();
        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("file/test.png"));
             BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("file/copy.png"))){
            int a;
            do {
                a = in.read();
                if (a!=-1)
                    out.write(a);
                else
                    break;
            }while(true);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(System.currentTimeMillis()-start);
        Runtime.getRuntime().gc();
        start = System.currentTimeMillis();
        try (FileInputStream in = new FileInputStream("file/test1.png");
             FileOutputStream out = new FileOutputStream("file/copy1.png")){
            int a;
            do {
                a = in.read();
                if (a!=-1)
                    out.write(a);
                else
                    break;
            }while(true);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(System.currentTimeMillis()-start);
        Runtime.getRuntime().gc();
        start = System.currentTimeMillis();
        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("file/test2.png"));
             BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("file/copy2.png"))){
            byte[] b = new byte[8192];
            int len = 0;
            while ((len = in.read(b))!=-1){
                out.write(b,0,len);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(System.currentTimeMillis()-start);

第一次用的是带缓冲的,但是每次只读一个字节和写一个字节,第二次是用不带缓冲的,也是每次只读一个字节和写一个字节,第三次使用的带缓冲的,但是每次读写为一整个数组,运行时间如下:

61
3840
5

可见差距还是很大的,首先说明了缓冲的确有作用,另外即使有缓冲,也不建议一字节一字节的操作,还是要借助数据成块读写。

1.6 SequenceInputStream

这个类的作用是合并多个输入流,它有两个构造函数,若是合并两个流,直接用双参数构造,若是多余两个。需要以Enumeration实例的形式传入。不多说,看代码:

try (FileInputStream in = new FileInputStream("file/test.txt");
             FileInputStream in1 = new FileInputStream("file/test1.txt");
             FileInputStream in2 = new FileInputStream("file/test2.txt");
             FileOutputStream out = new FileOutputStream("file/out.txt")){
                Vector<InputStream> vector = new Vector<>();
                vector.add(in);
                vector.add(in1);
                vector.add(in2);
            SequenceInputStream sis = new SequenceInputStream(vector.elements());
            byte[] b = new byte[1024];
            int len;
            while ((len = sis.read(b))!=-1){
                out.write(b,0,len);
            }
        }catch (Exception e){
            e.printStackTrace();
        }

有一点需要注意,虽然是合并,但输出是还是有顺序的,就是添加的顺序。

1.7 PushbackInputStream

这个类有个缓存,可以将从流中读取到的数据,回退到流中。为什么特意说有何缓存,其实回退操作是借助与缓存实现的,并不是流中真的有回退数据。它有两个构造,除了都要穿输入流外,一个可以指定缓存的大小,也就是最大回退的大小,另外一个默认为1,示例

        try (PushbackInputStream pis = new PushbackInputStream(new ByteArrayInputStream("asdfertghuji".getBytes()),7)){
            byte[] buffer = new byte[7];
            pis.read(buffer);
            System.out.println(new String(buffer));
            pis.unread(buffer,0,4);
            pis.read(buffer);
            System.out.println(new String(buffer));
        }catch (Exception e){
            e.printStackTrace();
        }

输出

asdfert
asdfghu

简单解释一下,第一次读了7个字节,asdfert,然后回退4个字节,也就是上次读的前四个,回退的逻辑上放在流的前方,下一次在读7字节,首先读到的是回退的asdf,然后从流中在读3字节ghu,组成asdfghu。

除了回退已读到的,还可以回退任意数据

        try (PushbackInputStream pis = new PushbackInputStream(new ByteArrayInputStream("asdfertghuji".getBytes()),7)){
            byte[] buffer = new byte[7];
            pis.read(buffer);
            System.out.println(new String(buffer));
            pis.unread("1234".getBytes(),0,4);
            pis.read(buffer);
            System.out.println(new String(buffer));
        }catch (Exception e){
            e.printStackTrace();
        }

但是回退的长度不能超过我们在构造中指定的长度,因为回退是基于缓存数组的,放不下就会抛异常java.io.IOException: Push back buffer is full

1.8 PipedInputStreamPipedOutputStream

这两个类比较特殊,他们是沟通两个线程用的,根据名字很好理解,在两个线程中架设一个管道。这两个类必须成对使用,因为要在两个线程间传输的前提是PipedInputStream与PipedOutputStream建立连接,可以利用构造或者connect方法。其实在线程中传输也是利用缓存实现的,默认大小是1024,可以指定。简单示例

public class Receiver extends Thread{
    private PipedInputStream in = new PipedInputStream();

    public PipedInputStream getIn(){
        return in;
    }
    @Override
    public void run() {
        super.run();
        try {
            System.out.println("receiver start");
            System.out.println((char)in.read());
            System.out.println("receiver end");
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class Sender extends Thread{
    private PipedOutputStream out = new PipedOutputStream();

    public PipedOutputStream getOut(){
        return out;
    }
    @Override
    public void run() {
        super.run();
        try {
            System.out.println("sender start");
            out.write('d');
            System.out.println("sender end");
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
    public static void main(String[] args) throws IOException {
        Receiver receiver = new Receiver();
        Sender sender = new Sender();
        receiver.getIn().connect(sender.getOut());
        receiver.start();
        sender.start();
    }

不必担心说先启动Receiver 会收不到消息,首先两个线程是并行的,其次read方法也是有阻塞的,除非PipedOutputStream 关闭了,否则会一直等待另一端写入内容。

关于connect方法,不管是input取绑定output,还是output绑定input都是一样的效果。另外可以利用构造方法绑定,详见API介绍

2.字符流

2.1 FileReaderFileWriter

这是字符流中一对文件操作类,它是按字符从文件中读写,基本用法和FileInputStream/FileOutputStream类似,示例

    public static void main(String[] args) throws IOException {
        try (FileReader reader = new FileReader("file/test.txt");
             FileWriter writer = new FileWriter("file/copy.txt")){
            char[] c = new char[1024];
            int len;
            while ((len = reader.read(c)) != -1){
                writer.write(c,0,len);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

那么这两个类和FileInputStream/FileOutputStream有什么区别呢?主要一点是字符上,这两个类是以字符为单位读的,简单演示一下区别:

    public static void main(String[] args) throws IOException {
        try (FileReader reader = new FileReader("file/test.txt");
             FileInputStream in = new FileInputStream("file/test.txt")){
            System.out.println((char)reader.read());
            System.out.println((char)in.read());
        }catch (Exception e){
            e.printStackTrace();
        }
    }

读一个中文字符,结果如下:

陈
é

具体字符和字节的区别,可以自行学习。

2.2 BufferedReaderBufferedWriter

同字节流一样,这里也给我们提供了自带缓冲的类,默认大小也是8192,原理不多讲了,但是这里有一个特殊方法,就是能一次读一行readLine(),以及插入换行符newLine()。

2.3 CharArrayReaderCharArrayWriter

和 ByteArrayInputStream与ByteArrayOutputStream很类似,只不过这里操作的是字符数组而已,连方法都很类似。

2.4 PushbackReader

字符版的回退流,操作和字节流对应的一样

        try (PushbackReader pis = new PushbackReader(new CharArrayReader("asdfertghuji".toCharArray()),7)){
            char[] buffer = new char[7];
            pis.read(buffer);
            System.out.println(new String(buffer));
            pis.unread("1234".toCharArray(),0,4);
            pis.read(buffer);
            System.out.println(new String(buffer));
        }catch (Exception e){
            e.printStackTrace();
        }

2.5 PipedReaderPipedWriter

字符版的线程间传输,用法和字节版的一样,甚至都不用改代码,只需替换对应的类即可,不再举例。

2.6 StringReaderStringWriter

和CharArrayReader与CharArrayWriter类似,主不过被操作的是字符串而不是字符数组。

文件

主要有FileRandomAccessFile

File是文件的实体类,包含了大量对文件的操作,RandomAccessFile则可以对文件内容进行操作。File类一般都很熟悉,它相当于一个工具类,这里不多讲,主要看RandomAccessFile。

RandomAccessFile有两个构造:

RandomAccessFile(File file, String mode)
RandomAccessFile(String name, String mode)

都是需要指定文件和操作模式。一般有以下4种模式

r 代表以只读方式打开指定文件 。
rw 以读写方式打开指定文件 。
rws 读写方式打开,并对内容或元数据都同步写入底层存储设备 。
rwd 读写方式打开,对文件内容的更新同步更新至底层存储设备 。

与流式操作每种类只能读或写文件不同,这个以既可以读也可以写,他的一大特点是随机读写,这也是一些断点续传,多线程下载等技术的基本。

基本的读写操作就不介绍了,主要介绍一些特色方法,如下例在文件末尾追加内容:

        try (RandomAccessFile file = new RandomAccessFile("file/test.txt","rw")){
            file.seek(file.length()); //获取文件长度,设置指针位置
            file.write('d'); //写一个字符
            System.out.println(file.getFilePointer()); //获取当前指针位置
        }catch (Exception e){
            e.printStackTrace();
        }

另外可以设置文件大小,只占位置,没有内容,适合多线程下载时配置文件

file.setLength(1024*1024*8); //设置大小8M
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容