@TOC

Java IO概述

IO就是输入/输出。Java IO类库基于抽象基础类InputStream和OutputStream构建了一套I/O体系,主要解决从数据源读入数据和将数据写入到目的地问题。我们把数据源和目的地可以理解为IO流的两端。当然,通常情况下,这两端可能是文件或者网络连接。

我们用下面的图描述下,加深理解:

其实除了面向字节流的InputStream/OutputStream体系外,Java IO类库还提供了面向字符流的Reader/Writer体系。Reader/Writer继承结构主要是为了国际化,因为它能更好地处理16位的Unicode字符。

在学习是这两套IO流处理体系可以对比参照着学习,因为有好多相似之处。

要理解总体设计

刚开始写IO代码,总被各种IO流类搞得晕头转向。这么多IO相关的类,各种方法,啥时候能记住。

其实只要我们掌握了IO类库的总体设计思路,理解了它的层次脉络之后,就很清晰。知道啥时候用哪些流对象去组合想要的功能就好了,API的话,可以查手册的嘛。

首先从流的流向上可以分为输入流InputStream或Reader,输出流OutputStream或Writer。任何从InputStream或Reader派生而来的类都有read()基本方法,读取单个字节或字节数组;任何从OutputSteam或Writer派生的类都含有write()的基本方法,用于写单个字节或字节数组。

从操作字节还是操作字符的角度,有面向字节流的类,基本都以XxxStream结尾,面向字符流的类都以XxxReader或XxxWriter结尾。当然这两种类型的流是可以转化的,有两个转化流的类,这个后面会说到。

一般在使用IO流的时候会有下面类似代码:

1 FileInputStream inputStream = new FileInputStream(new File("a.txt"));
2 BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);

这里其实是一种装饰器模式的使用,IO流体系中使用了装饰器模式包装了各种功能流类。不了解装饰器模式的看下这篇【详解设计模式】-装饰者模式

在Java IO流体系中FilterInputStream/FilterOutStreamFilterReader/FilterWriter就是装饰器模式的接口类,从该类向下包装了一些功能流类。有DataInputStream、BufferedInputStream、LineNumberInputStream、PushbackInputStream等,当然还有输出的功能流类;面向字符的功能流类等。

下面几张图描述了整个IO流的继承体系结构

最后再附加一张表加深印象:

File其实是个工具类

File类其实不止是代表一个文件,它也能代表一个目录下的一组文件(代表一个文件路径)。下面我们盘点一下File类中最常用到的一些方法

 1File.delete() 删除文件或文件夹目录。
2File.createNewFile() 创建一个新的空文件。
3File.mkdir() 创建一个新的空文件夹。
4File.list() 获取指定目录下的文件和文件夹名称。
5File.listFiles() 获取指定目录下的文件和文件夹对象。
6File.exists() 文件或者文件夹是否存在
7
8String   getAbsolutePath()   // 获取绝对路径
9long   getFreeSpace()       // 返回分区中未分配的字节数。
10String   getName()         // 返回文件或文件夹的名称。
11String   getParent()         // 返回父目录的路径名字符串;如果没有指定父目录,则返回 null。
12File   getParentFile()      // 返回父目录File对象
13String   getPath()         // 返回路径名字符串。
14long   getTotalSpace()      // 返回此文件分区大小。
15long   getUsableSpace()    //返回占用字节数。
16int   hashCode()             //文件哈希码。
17long   lastModified()       // 返回文件最后一次被修改的时间。
18long   length()          // 获取长度,字节数。
19boolean canRead()  //判断是否可读
20boolean canWrite()  //判断是否可写
21boolean isHidden()  //判断是否隐藏
22
23
24// 成员函数
25static File[]    listRoots()    // 列出可用的文件系统根。
26boolean    renameTo(File dest)    // 重命名
27boolean    setExecutable(boolean executable)    // 设置执行权限。
28boolean    setExecutable(boolean executable, boolean ownerOnly)    // 设置其他所有用户的执行权限。
29boolean    setLastModified(long time)       // 设置最后一次修改时间。
30boolean    setReadable(boolean readable)    // 设置读权限。
31boolean    setReadable(boolean readable, boolean ownerOnly)    // 设置其他所有用户的读权限。
32boolean    setWritable(boolean writable)    // 设置写权限。
33boolean    setWritable(boolean writable, boolean ownerOnly)    // 设置所有用户的写权限。
34

需要注意的是,不同系统对文件路径的分割符表是不一样的,比如Windows中是“\”,Linux是“/”。而File类给我们提供了抽象的表示File.separator,屏蔽了系统层的差异。因此平时在代码中不要使用诸如“\”这种代表路径,可能造成Linux平台下代码执行错误。

下面是一些示例:

根据传入的规则,遍历得到目录中所有的文件构成的File对象数组

 1public class Directory {
2    public static File[] getLocalFiles(File dir, final String regex){
3        return dir.listFiles(new FilenameFilter() {
4            private Pattern pattern = Pattern.compile(regex);
5            public boolean accept(File dir, String name) {
6                return pattern.matcher(new File(name).getName()).matches();
7            }
8        });
9    }
10
11    // 重载方法
12    public static File[] getLocalFiles(String path, final String regex){
13        return getLocalFiles(new File(path),regex);
14    }
15
16    public static void main(String[] args) {
17        String dir = "d:";
18        File[] files = Directory.getLocalFiles(dir,".*\\.txt");
19        for(File file : files){
20            System.out.println(file.getAbsolutePath());
21        }
22    }
23}

输出结果:

1d:\\1.txt
2d:\\新建文本文档.txt

上面的代码中dir.listFiles(FilenameFilter ) 是策略模式的一种实现,而且使用了匿名内部类的方式。

【详解设计模式】-策略模式

Java内部类超详细总结(含代码示例)

InputStream和OutputStream

InputStream是输入流,前面已经说到,它是从数据源对象将数据读入程序内容时,使用的流对象。通过看InputStream的源码知道,它是一个抽象类,

1public abstract class InputStream  extends Object  implements Closeable
2

提供了一些基础的输入流方法:

 1//从数据中读入一个字节,并返回该字节,遇到流的结尾时返回-1
2abstract int read() 
3
4//读入一个字节数组,并返回实际读入的字节数,最多读入b.length个字节,遇到流结尾时返回-1
5int read(byte[] b)
6
7// 读入一个字节数组,返回实际读入的字节数或者在碰到结尾时返回-1.
8//b:代表数据读入的数组, off:代表第一个读入的字节应该被放置的位置在b中的偏移量,len:读入字节的最大数量
9int read(byte[],int off,int len)
10
11// 返回当前可以读入的字节数量,如果是从网络连接中读入,这个方法要慎用,
12int available() 
13
14//在输入流中跳过n个字节,返回实际跳过的字节数
15long skip(long n)
16
17//标记输入流中当前的位置
18void mark(int readlimit) 
19
20//判断流是否支持打标记,支持返回true
21boolean markSupported() 
22
23// 返回最后一个标记,随后对read的调用将重新读入这些字节。
24void reset() 
25
26//关闭输入流,这个很重要,流使用完一定要关闭
27void close()
28

直接从InputStream继承的流,可以发现,基本上对应了每种数据源类型。

OutputStream是输出流的抽象,它是将程序内存中的数据写入到目的地(也就是接收数据的一端)。看下类的签名:

1public abstract class OutputStream implements CloseableFlushable {}

提供了基础方法相比输入流来说简单多了,主要就是write写方法(几种重载的方法)、flush冲刷和close关闭。

 1// 写出一个字节的数据
2abstract void write(int n)
3
4// 写出字节到数据b
5void write(byte[] b)
6
7// 写出字节到数组b,off:代表第一个写出字节在b中的偏移量,len:写出字节的最大数量
8void write(byte[] b, int off, int len)
9
10//冲刷输出流,也就是将所有缓冲的数据发送到目的地
11void flush()
12
13// 关闭输出流
14void close()
15

同样地,OutputStream也提供了一些基础流的实现,这些实现也可以和特定的目的地(接收端)对应起来,比如输出到字节数组或者是输出到文件/管道等。

使用装饰器包装有用的流

Java IO 流体系使用了装饰器模式来给哪些基础的输入/输出流添加额外的功能。这写额外的功能可能是:可以将流缓冲起来提高性能、是流能够读写基本数据类型等。

这些通过装饰器模式添加功能的流类型都是从FilterInputStream和FilterOutputStream抽象类扩展而来的。可以再返回文章最开始说到IO流体系的层次时,那几种图加深下印象。

大多数情况下,其实我们在使用流的时候都是输入流和输出流搭配使用的。目的就是为了转移和存储数据,单独的read()对我们而言有啥用呢,读出来一个字节能干啥?对吧。因此要理解流的使用就是搭配起来或者使用功能流组合起来去转移或者存储数据。

Reader和Writer

Reader是Java IO中所有Reader的基类。ReaderInputStream类似,不同点在于,Reader基于字符而非基于字节。

Writer是Java IO中所有Writer的基类。与ReaderInputStream的关系类似,Writer基于字符而非基于字节,Writer用于写入文本,OutputStream用于写入字节。

ReaderWriter的基础功能类,可以对比InputStreamOutputStream来学习。

有两个“适配器” 流类型,它们可以将字节流转化成字节流。这就是InputStreamReader 可以将InputStream转成为Reader,OutputStreamWriter可以将OutputStream转成为Writer。

当然也有类似字节流的装饰器实现方式,给字符流添加额外的功能或这说是行为。这些功能字符流类主要有:

  • BufferedReader
  • BufferedWriter
  • PrintWriter
  • LineNumberReader
  • PushbackReader

System类中的I/O流

想想你的第一个Java程序是啥?我没猜错的话,应该是 hello world。

1System.out.println("hello world")

简单到令人发指,今天就说说标准的输入/输出流。

在标准IO模型中,Java提供了System.in、System.out和System.error。

先说System.in,看下源码

1public final static InputStream in

是一个静态域,未被包装过的InputStream。通常我们会使用BufferedReader进行包装然后一行一行地读取输入,这里就要用到前面说的适配器流InputStreamReader

1public class SystemInReader {
2    public static void main(String[] args) throws IOException {
3        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
4        String s;
5        while ((s = reader.readLine()) != null && s.length() != 0){
6            System.out.println(s);
7        }
8    }
9}

该程序等待会一直等待我们输入,输入啥,后面会接着输出。输入空字符串可以结束。

11
21
3123
4123

System.out是一个PrintStream流。System.out一般会把你写到其中的数据输出到控制台上。System.out通常仅用在类似命令行工具的控制台程序上。System.out也经常用于打印程序的调试信息(尽管它可能并不是获取程序调试信息的最佳方式)。

System.err是一个PrintStream流。System.err与System.out的运行方式类似,但它更多的是用于打印错误文本。

可以将这些系统流重定向

尽管System.in, System.out, System.err这3个流是java.lang.System类中的静态成员,并且已经预先在JVM启动的时候初始化完成,你依然可以更改它们。

可以使用setIn(InputStream)、setOut(PrintStream)、setErr(PrintStream)进行重定向。比如可以将控制台的输出重定向到文件中。

1OutputStream output = new FileOutputStream("d:/system.out.txt");
2PrintStream printOut = new PrintStream(output);
3System.setOut(printOut);

压缩(ZIP文档)

Java IO类库是支持读写压缩格式的数据流的。我们可以把一个或一批文件压缩成一个zip文档。这些压缩相关的流类是按字节处理的。先看下设计压缩解压缩的相关流类。

表格中CheckedInputStream 和 CheckedOutputStream 一般会和Zip压缩解压过程配合使用,主要是为了保证我们压缩和解压过程数据包的正确性,得到的是中间没有被篡改过的数据。

我们以CheckedInputStream 为例,它的构造器需要传入一个Checksum类型:

1    public CheckedInputStream(InputStream in, Checksum cksum) {
2        super(in);
3        this.cksum = cksum;
4    }

而Checksum 是一个接口,可以看到这里又用到了策略模式,具体的校验算法是可以选择的。Java类库给我提供了两种校验和算法:Adler32 和 CRC32,性能方面可能Adler32 会更好一些,不过CRC32可能更准确。各有优劣吧。

好了,接下来看下压缩/解压缩流的具体使用。

将多个文件压缩成zip包

 1public class ZipFileUtils {
2    public static void compressFiles(File[] files, String zipPath) throws IOException {
3
4        // 定义文件输出流,表明是要压缩成zip文件的
5        FileOutputStream f = new FileOutputStream(zipPath);
6
7        // 给输出流增加校验功能
8        CheckedOutputStream checkedOs = new CheckedOutputStream(f,new Adler32());
9
10        // 定义zip格式的输出流,这里要明白一直在使用装饰器模式在给流添加功能
11        // ZipOutputStream 也是从FilterOutputStream 继承下来的
12        ZipOutputStream zipOut = new ZipOutputStream(checkedOs);
13
14        // 增加缓冲功能,提高性能
15        BufferedOutputStream buffOut = new BufferedOutputStream(zipOut);
16
17        //对于压缩输出流我们可以设置个注释
18        zipOut.setComment("zip test");
19
20        // 下面就是从Files[] 数组中读入一批文件,然后写入zip包的过程
21        for (File file : files){
22
23            // 建立读取文件的缓冲流,同样是装饰器模式使用BufferedReader
24            // 包装了FileReader
25            BufferedReader bfReadr = new BufferedReader(new FileReader(file));
26
27            // 一个文件对象在zip流中用一个ZipEntry表示,使用putNextEntry添加到zip流中
28            zipOut.putNextEntry(new ZipEntry(file.getName()));
29
30            int c;
31            while ((c = bfReadr.read()) != -1){
32                buffOut.write(c);
33            }
34
35            // 注意这里要关闭
36            bfReadr.close();
37            buffOut.flush();
38        }
39        buffOut.close();
40    }
41
42    public static void main(String[] args) throws IOException {
43        String dir = "d:";
44        String zipPath = "d:/test.zip";
45        File[] files = Directory.getLocalFiles(dir,".*\\.txt");
46        ZipFileUtils.compressFiles(files, zipPath);
47    }
48}

在main函数中我们使用了本文中 File其实是个工具类 章节里的Directory工具类。

解压缩zip包到目标文件夹

 1    public static void unConpressZip(String zipPath, String destPath) throws IOException {
2        if(!destPath.endsWith(File.separator)){
3            destPath = destPath + File.separator;
4            File file = new File(destPath);
5            if(!file.exists()){
6                file.mkdirs();
7            }
8        }
9        // 新建文件输入流类,
10        FileInputStream fis = new FileInputStream(zipPath);
11
12        // 给输入流增加检验功能
13        CheckedInputStream checkedIns = new CheckedInputStream(fis,new Adler32());
14
15        // 新建zip输出流,因为读取的zip格式的文件嘛
16        ZipInputStream zipIn = new ZipInputStream(checkedIns);
17
18        // 增加缓冲流功能,提高性能
19        BufferedInputStream buffIn = new BufferedInputStream(zipIn);
20
21        // 从zip输入流中读入每个ZipEntry对象
22        ZipEntry zipEntry;
23        while ((zipEntry = zipIn.getNextEntry()) != null){
24            System.out.println("解压中" + zipEntry);
25
26            // 将解压的文件写入到目标文件夹下
27            int size;
28            byte[] buffer = new byte[1024];
29            FileOutputStream fos = new FileOutputStream(destPath + zipEntry.getName());
30            BufferedOutputStream bos = new BufferedOutputStream(fos, buffer.length);
31            while ((size = buffIn.read(buffer, 0, buffer.length)) != -1) {
32                bos.write(buffer, 0, size);
33            }
34            bos.flush();
35            bos.close();
36        }
37        buffIn.close();
38
39        // 输出校验和
40        System.out.println("校验和:" + checkedIns.getChecksum().getValue());
41    }
42
43    // 在main函数中直接调用
44    public static void main(String[] args) throws IOException {
45        String dir = "d:";
46        String zipPath = "d:/test.zip";
47//        File[] files = Directory.getLocalFiles(dir,".*\\.txt");
48//        ZipFileUtils.compressFiles(files, zipPath);
49
50        ZipFileUtils.unConpressZip(zipPath,"F:/ziptest");
51    }

这里解压zip包还有一种更加简便的方法,使用ZipFile对象。该对象的entries()方法直接返回ZipEntry类型的枚举。看下代码片段:

1        ZipFile zipFile = new ZipFile("test.zip");
2        Enumeration e = zipFile.entries();
3        while (e.hasMoreElements()){
4            ZipEntry zipEntry = (ZipEntry) e.nextElement();
5            System.out.println("file:" + zipEntry);
6        }

对象序列化

什么是序列化和反序列化呢?

序列化就是将对象转成字节序列的过程,反序列化就是将字节序列重组成对象的过程。

为什么要有对象序列化机制

程序中的对象,其实是存在有内存中,当我们JVM关闭时,无论如何它都不会继续存在了。那有没有一种机制能让对象具有“持久性”呢?序列化机制提供了一种方法,你可以将对象序列化的字节流输入到文件保存在磁盘上。

序列化机制的另外一种意义便是我们可以通过网络传输对象了,Java中的 远程方法调用(RMI),底层就需要序列化机制的保证。

在Java中怎么实现序列化和反序列化

首先要序列化的对象必须实现一个Serializable接口(这是一个标识接口,不包括任何方法)

1public interface Serializable {
2}

其次需要是用两个对象流类:ObjectInputStream 和ObjectOutputStream主要使用ObjectInputStream对象的readObject方法读入对象、ObjectOutputStream的writeObject方法写入对象到流中

下面我们通过序列化机制将一个简单的pojo对象写入到文件,并再次读入到程序内存。

 1public class User implements Serializable {
2    private String name;
3    private int age;
4
5    public User(String name, int age) {
6        this.name = name;
7        this.age = age;
8    }
9
10    @Override
11    public String toString() {
12        return "User{" +
13                "name='" + name + '\'' +
14                ", age='
" + age + '\'' +
15                '}';
16    }
17
18    public static void main(String[] args) throws IOException, ClassNotFoundException {
19        User user = new User("
二营长",18);
20        ObjectOutputStream objectOps = new ObjectOutputStream(new FileOutputStream("
f:/user.out"));
21        objectOps.writeObject(user);
22        objectOps.close();
23
24        // 再从文件中取出对象
25        ObjectInputStream objectIns = new ObjectInputStream(new FileInputStream("
f:/user.out"));
26
27        // 这里要做一次强转
28        User user1 = (User) objectIns.readObject();
29        System.out.println(user1);
30        objectIns.close();
31    }
32}
33

程序运行结果:

1User{name='二营长', age='18'}

不想序列化的数据使用transient(瞬时)关键字屏蔽

如果我们上面的user对象有一个password字段,属于敏感信息,这种是不能走序列化的方式的,但是实现了Serializable 接口的对象会自动序列化所有的数据域,怎么办呢?在password字段上加上关键字transient就好了。

1 private transient String password;

序列化机制就简单介绍到这里吧。这是Java原生的序列化,现在市面上有好多序列化协议可以选择,比如Json、FastJson、Thrift、Hessian 、protobuf等

I/O流的典型使用方式

IO流种类繁多,可以通过不同的方式组合I/O流类,但平时我们常用的也就几种组合。下盘通过示例的方式盘点几种I/O流的典型用法。

缓冲输入文件

 1public class BufferedInutFile {
2    public static String readFile(String fileName) throws IOException {
3        BufferedReader bf = new BufferedReader(new FileReader(fileName));
4        String s;
5
6        // 这里读取的内容存在了StringBuilder,当然也可以做其他处理
7        StringBuilder sb = new StringBuilder();
8        while ((s = bf.readLine()) != null){
9            sb.append(s + "\n");
10        }
11        bf.close();
12        return sb.toString();
13    }
14
15    public static void main(String[] args) throws IOException {
16        System.out.println(BufferedInutFile.readFile("d:/1.txt"));
17    }
18}

格式化内存输入

要读取格式化的数据,可以使用DataInputStream。

 1public class FormattedMemoryInput {
2    public static void main(String[] args) throws IOException {
3        try {
4            DataInputStream dataIns = new DataInputStream(
5                    new ByteArrayInputStream(BufferedInutFile.readFile("f:/FormattedMemoryInput.java").getBytes()));
6            while (true){
7                System.out.print((char) dataIns.readByte());
8            }
9        } catch (EOFException e) {
10            System.err.println("End of stream");
11        }
12    }
13}

上面程序会在控制台输出当前类本身的所有代码,并且会抛出一个EOFException异常。抛出异常的原因是已经到留的结尾了还在读数据。这里可以使用available()做判断还有多少可以的字符。

 1package com.herp.pattern.strategy;
2
3import java.io.ByteArrayInputStream;
4import java.io.DataInputStream;
5import java.io.IOException;
6
7public class FormattedMemoryInput {
8    public static void main(String[] args) throws IOException {
9        DataInputStream dataIns = new DataInputStream(
10                new ByteArrayInputStream(BufferedInutFile.readFile("FormattedMemoryInput.java").getBytes()));
11        while (true){
12            System.out.println((char) dataIns.readByte());
13        }
14    }
15}

基本的文件输出

FileWriter对象可以向文件写入数据。首先创建一个FileWriter和指定的文件关联,然后使用BufferedWriter将其包装提供缓冲功能,为了提供格式化机制,它又被装饰成为PrintWriter

 1public class BasicFileOutput {
2    static String file = "BasicFileOutput.out";
3
4    public static void main(String[] args) throws IOException {
5        BufferedReader in = new BufferedReader(new StringReader(BufferedInutFile.readFile("f:/BasicFileOutput.java")));
6        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
7
8        int lineCount = 1;
9        String s;
10        while ((s = in.readLine()) != null){
11            out.println(lineCount ++ + ": " + s);
12        }
13        out.close();
14        in.close();
15    }
16}

下面是我们写出的BasicFileOutput.out文件,可以看到我们通过代码字节加上了行号

 11package com.herp.pattern.strategy;
22
33import java.io.*;
44
55public class BasicFileOutput {
66:     static String file = "BasicFileOutput.out";
77
88:     public static void main(String[] args) throws IOException {
99:         BufferedReader in = new BufferedReader(new StringReader(BufferedInutFile.readFile("f:/BasicFileOutput")));
1010:         PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
1111
1212:         int lineCount = 1;
1313:         String s;
1414:         while ((s = in.readLine()) != null){
1515:             out.println(lineCount ++ + ": " + s);
1616:         }
1717:         out.close();
1818:         in.close();
1919:     }
2020: }

数据的存储和恢复

为了输出可供另一个“流”恢复的数据,我们需要使用DataOutputStream写入数据,然后使用DataInputStream恢复数据。当然这些流可以是任何形式(这里的形式其实就是我们前面说过的流的两端的类型),比如文件。

 1public class StoringAndRecoveringData {
2    public static void main(String[] args) throws IOException {
3        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("data.txt")));
4        out.writeDouble(3.1415926);
5        out.writeUTF("我是二营长");
6        out.writeInt(125);
7        out.writeUTF("点赞加关注");
8        out.close();
9
10        DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("data.txt")));
11        System.out.println(in.readDouble());
12        System.out.println(in.readUTF());
13        System.out.println(in.readInt());
14        System.out.println(in.readUTF());
15        in.close();
16    }
17}

输出结果:

13.1415926
2我是二营长
3125
4点赞加关注

需要注意的是我们使用writeUTF()和readUTF()来写入和读取字符串。

好了。关于Java I/O流体系就总结这么多吧。


04-09 10:59