Java流编程实例及代码

Java流编程实例及代码

1.前言

在众多流行的编程语言中,Java对IO的处理应该是最特殊的,Java打着“尽量减少IO类的设计理念”,搞出了目前应该是最复杂的一套IO相关类,并称之为Java流。
对于新手来说,Java流包含的类众多,含义混杂,上手困难且其中暗藏的陷阱众多;但是对于熟悉了Java流的程序员来说,它的确称得上功能强大。
本文总结了一些Java流的使用指南,给出了一些实例代码,主要内容包括:
- Java流中的字节与字符
- 文件流
- 字节数组流
- 管道流
- 缓冲流
- 数据流
- 压缩流
- 摘要流
- 加密流
- 多重流使用范例
本文的读者应该是Java程序员,最好具有基本的Java基础知识。本文给出的代码都已经在Jdk7上运行通过,如有错误,请及时反馈。

2.Java中的字节与字符

2.1Java流概论

Java流是Java语言用来处理IO的一套类,针对字节进行处理的类被命名为stream结尾,例如FileInputStream、FileOutputStream;针对字符进行处理的类被命名为Reader或Writer结尾,例如FileReader或者FileWriter。要弄清Java流,首先要明白Java中的字节与字符的区别。
包含InputStream或Reader的类被称为输入流,此处的“输入”是指从外部(文件、网络、其他程序)将信息输入到Java程序内部;或者指从Java程序内部的某个变量输入到当前操作的代码块。
包含OutputStream或Writer的类被称为输出流,此处的“输出”是指从Java程序内部将信息输出到外部(文件、网络、其他程序);或者指从当前操作的代码块将信息输出到其他变量。

2.2 字节(byte)的取值

一个字节包含一个8位的二进制数字,在Java中使用byte来表示。最大值为127,最小值为-128。下面的代码列出了所有byte的值:

public static void listAllByteValue(){
    for (int i = -128; i < 128; i++) {
        byte b = (byte) i;
        System.out.println("byte:" + b + ",Binary:" + byteToBinaryString(b));
    }
}

public static String byteToBinaryString(byte b) {
    String s = Integer.toBinaryString(b);
    if (b < 0) {
        s = s.substring(24);
    } else {
        if (s.length() < 8) {
            int len = s.length();
            for (int i = 0; i < 8 - len; i++) {
                s = "0" + s;
            }
        }
    }
    return s;
}

大于等于0的byte值,使用其原码表示,即直接存储该byte的二进制值;小于0的byte值,使用其补码表示,即将该值的绝对值的原码按位取反再加1。例如42就存储为00101010;而-42先求42的值00101010,然后按位取反得到11010101,再加1得到11010110,此即为-42的二进制值。

2.3 字节(byte)的赋值

byte可以用多种方法来赋值,见下列代码:

public static void byteGetValue(){
    //二进制以0b开头
    byte b1 = 0b00101010;
    System.out.println("b1:"+b1+",Binary:"+byteToBinaryString(b1));
    //八进制以0开头
    byte b2 = 052;
    System.out.println("b2:"+b2+",Binary:"+byteToBinaryString(b2));
    //十进制
    byte b3 = 42;
    System.out.println("b3:"+b3+",Binary:"+byteToBinaryString(b3));
    //十六进制
    byte b4 = 0x2a;
    System.out.println("b4:"+b4+",Binary:"+byteToBinaryString(b4));

    //-42的赋值
    //二进制,由于11010110以原码来理解已经超过了127,因此必须使用byte进行强制类型转换
    byte b5 = (byte) 0b11010110;
    System.out.println("b5:"+b5+",Binary:"+byteToBinaryString(b5));
    //八进制以0开头
    byte b6 = -052;
    System.out.println("b6:"+b6+",Binary:"+byteToBinaryString(b6));
    //十进制
    byte b7 = -42;
    System.out.println("b7:"+b7+",Binary:"+byteToBinaryString(b7));
    //十六进制,由于0xd6以原码来理解已经超过了127,因此必须使用byte进行强制类型转换
    byte b8 = (byte) 0xd6;
    System.out.println("b8:"+b8+",Binary:"+byteToBinaryString(b8));

    //将两个int转为byte的示例,示例告诉我们int转byte,就是简单的截取最后8位
    int i1 = 0b001011010110;
    int i2 = 0b110011010110;
    System.out.println("i1 = "+i1+", i2 = "+i2);
    byte b9 = (byte) i1;
    byte b10 = (byte) i2;
    System.out.println("b9 = "+b9+", b10 = "+b10);
}

值得注意的是,当int转换为byte时,直接截取了int的后8位。
运行结果:

b1:42,Binary:00101010
b2:42,Binary:00101010
b3:42,Binary:00101010
b4:42,Binary:00101010
b5:-42,Binary:11010110
b6:-42,Binary:11010110
b7:-42,Binary:11010110
b8:-42,Binary:11010110
i1 = 726, i2 = 3286
b9 = -42, b10 = -42

2.4 字符(char)的取值

由上面的例子可知byte就是一个单纯的8位二进制数字,它可以有多种赋值方法,但是在内存中始终不变。
字符在Java中使用char基本类型来存储,它是一个16位的unicode码,也可以理解为一个16位的二进制数字,其取值范围为0到65535(2的16次方-1)。下面的例子给出了一段中文的char值:

public static void listCharValue() {
    System.out.println("char max value is :"+(int)Character.MAX_VALUE+", min value is :"+(int)Character.MIN_VALUE);

    for (char c = 19968; c < 20271; c++) {
        System.out.print(c);
    }
}

运行结果:

char max value is :65535, min value is :0
一丁丂七丄丅丆万丈三上下丌不与丏丐丑丒专且丕世丗丘丙业丛东丝丞丟丠両丢丣两严並丧丨丩个丫丬中丮丯丰丱串丳临丵丶丷丸丹为主丼丽举丿乀乁乂乃乄久乆乇么义乊之乌乍乎乏乐乑乒乓乔乕乖乗乘乙乚乛乜九乞也习乡乢乣乤乥书乧乨乩乪乫乬乭乮乯买乱乲乳乴乵乶乷乸乹乺乻乼乽乾乿亀亁亂亃亄亅了亇予争亊事二亍于亏亐云互亓五井亖亗亘亙亚些亜亝亞亟亠亡亢亣交亥亦产亨亩亪享京亭亮亯亰亱亲亳亴亵亶亷亸亹人亻亼亽亾亿什仁仂仃仄仅仆仇仈仉今介仌仍从仏仐仑仒仓仔仕他仗付仙仚仛仜仝仞仟仠仡仢代令以仦仧仨仩仪仫们仭仮仯仰仱仲仳仴仵件价仸仹仺任仼份仾仿伀企伂伃伄伅伆伇伈伉伊伋伌伍伎伏伐休伒伓伔伕伖众优伙会伛伜伝伞伟传伡伢伣伤伥伦伧伨伩伪伫伬伭伮

2.5 字符(char)的赋值

后面会提到,char可以表现为世界各国的各种字符,但是在内存中,它就是一个16位的二进制数字,因此其赋值方法也与byte一样多种多样。

public static void charGetValue() {
    //把'一'赋值给char
    char c1 = '一';
    System.out.println(c1);

    char c2 = 0b100111000000000;
    System.out.println(c2);

    char c3 = 047000;
    System.out.println(c3);

    char c4 = 19968;
    System.out.println(c4);

    char c5 = 0x4e00;
    System.out.println(c5);

    char c6 = '\u4e00';
    System.out.println(c6);

    int i1 = (int) '一';
    System.out.println(Integer.toHexString(i1));
}

运行结果:

一
一
一
一
一
一
4e00

2.6 char转换为byte

char存储的是字符,在很多情况下它需要被转换为字节,例如存储到文件中时,或者在网络上进行传递时。当字符转换为byte时,需要用到编码格式,例如GBK或者unicode或者UTF-8等等,不同的编码格式转换得到的byte数组也不一样,如下图所示:


关于编码格式的详细介绍,可以去搜索其他文章,简要说一下:
Java内部使用Unicode作为char的编码格式,它又分为UCS-2(每两个字节代表一个char)和UCS-4(每四个字节代表一个char),目前主流的都是UCS-2。当把字符或者字符串转换为unicode时,会在头部加入两个字节的标志位(FE FF)表示big Endian(字节序,内存低位地址存放最高有效字节)。
UTF-8编码:一个英文字符占一个字节,一个汉字占三个字节;
GBK编码:一个英文字符占一个字节,一个汉字占两个字节。
代码如下:

public static void charToBytes() {
    try {
        byte[] buf1 = "一".getBytes("unicode");
        System.out.println("---------unicode---------");
        for (int i = 0; i < buf1.length; i++) {
            System.out.println(Integer.toHexString(buf1[i]));
        }

        System.out.println("---------UTF-8---------");
        byte[] buf2 = "一".getBytes("UTF-8");
        for (int i = 0; i < buf2.length; i++) {
            System.out.println(Integer.toHexString(buf2[i]));
        }

        System.out.println("---------UTF-16---------");
        byte[] buf3 = "一".getBytes("UTF-16");
        for (int i = 0; i < buf3.length; i++) {
            System.out.println(Integer.toHexString(buf3[i]));
        }

        System.out.println("---------gbk---------");
        byte[] buf4 = "一".getBytes("gbk");
        for (int i = 0; i < buf4.length; i++) {
            System.out.println(Integer.toHexString(buf4[i]));
        }
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
}
 public static String byteToHexString(byte b) {
    String s = Integer.toHexString(b);
    int len = s.length();
    if (len >= 2) {
        s = s.substring(len - 2);
    }else{
        s = "0"+s;
    }
    return s;
}

运行结果:

---------unicode---------
fffffffe
ffffffff
4e
0
---------UTF-8---------
ffffffe4
ffffffb8
ffffff80
---------UTF-16---------
fffffffe
ffffffff
4e
0
---------gbk---------
ffffffd2
ffffffbb

2.7 byte转换为char

当Java程序从外部(文件、网络、其他应用程序)读入字节流时,若该字节流代表的是字符,则需要将字节转换为字符,此动作被称为转码。转码时需要注意其编码格式,例子如下:

public static void byteToChar() {
    byte[] unicode_b = new byte[4];
    unicode_b[0] = (byte) 0XFE;
    unicode_b[1] = (byte) 0XFF;
    unicode_b[2] = (byte) 0X4E;
    unicode_b[3] = (byte) 0X2D;
    String unicode_str;

    byte[] gbk_b = new byte[2];
    gbk_b[0] = (byte) 0XD6;
    gbk_b[1] = (byte) 0XD0;
    String gbk_str;

    byte[] utf_b = new byte[3];
    utf_b[0] = (byte) 0XE4;
    utf_b[1] = (byte) 0XB8;
    utf_b[2] = (byte) 0XAD;
    String utf_str;

    try {
        unicode_str = new String(unicode_b, "unicode");
        System.out.println("unicode string is:" + unicode_str);
        gbk_str = new String(gbk_b, "gbk");
        System.out.println("gbk string is:" + gbk_str);
        utf_str = new String(utf_b, "utf-8");
        System.out.println("utf-8 string is:" + utf_str);
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
}

运行结果如下:

unicode string is:中
gbk string is:中
utf-8 string is:中

2.8 转码和乱码

将一种编码格式转换为另一中编码格式称之为转码。Java内部使用unicode来进行字符编码,当将字符转换为byte时需要转码,转为GBK或者其他编码;当从外部读入一段byte数组时,也需将其他编码转换为unicode编码的字符。
转码发生的地方只有两个:从char到byte,或者从byte到char。转码发生时必须指定编码格式,如果不指定编码格式,则会使用默认的编码格式(一是IDE的环境中指定的格式,二是操作系统默认的编码格式),你可以用如下代码获取默认的编码格式:

private static void showNativeEncoding() {
    String enc = System.getProperty("file.encoding");
    System.out.println(enc);
}

如果在转码时使用了错误的编码格式,则会出现乱码。

3. 文件流

3.1 如何选择文件流的类

文件流应该是Java流中使用最普遍、最广泛的流了。文件流分为两组,一组是操作字节的FileInputStream和FileOutputStream,另一组是操作字符的FileReader和FileWriter,事实上,我们还经常用到FileReader和FileWriter的父类InputStreamReader和OutputStreamWriter,原因后面会阐述。
让我们从最简单的开始,使用FileWriter将一个字符串写入某文件:

private static void writeString2File(String info,String filename){
    try {
        BufferedWriter bufferedWriter=new BufferedWriter(new FileWriter(filename));
        bufferedWriter.write(info);
        bufferedWriter.flush();
        bufferedWriter.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
private static void exeWriteString2File() {
    String info = "Today is a good day.\r\n今天天气很好。";
    try {
        String outputfile = new File(".").getCanonicalPath() + File.separator + "output1.txt";
        writeString2File(info,outputfile);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

调用exeWriteString2File方法将一个包含两行字符的字符串写入了文件output1.txt。
接下来,使用FileReader从output1.txt中读取信息:

private static String readFile(String filename) {
    String str = null;
    try {
        BufferedReader bufferedReader = new BufferedReader(new FileReader(filename));

        StringBuilder sb = new StringBuilder();
        while ((str = bufferedReader.readLine()) != null) {
            sb.append(str + "\n");
        }
        str = sb.toString();
        bufferedReader.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return str;
}
private static void exeReadFile() {
    try {
        String outputfile = new File(".").getCanonicalPath() + File.separator + "output1.txt";
        String str = readFile(outputfile);
        System.out.println(str);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

调用exeReadFile方法从文件output1.txt中读取了信息,一切都很简单。
以上代码的简单性来自一个假设,那就是我们假设文件编码都是统一的,但是实际编程中你会经常碰到文件编码不一致的情景。例如中文编码最常用的有GBK(多个版本的windows经常将GBK设置为默认编码格式)、UTF-8(网络传输、XML文档中经常使用这个编码格式)和UTF-16。下面的代码将同一个字符串分别使用三种编码格式写入了三个文件:

private static void writeString2FileWithEncoding(String info,String filename,String charset){
    try {
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filename));
        byte[] bytes = info.getBytes(charset);
        bos.write(bytes);
        bos.flush();
        bos.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

private static void exeWriteString2FileWithEncoding() {
    try {
        String info = "Today is a good day.\r\n今天天气很好。";
        String output_gbk = new File(".").getCanonicalPath() + File.separator + "output_gbk.txt";
        writeString2FileWithEncoding(info,output_gbk,"gbk");

        String output_utf8 = new File(".").getCanonicalPath() + File.separator + "output_utf-8.txt";
        writeString2FileWithEncoding(info,output_utf8,"utf-8");

        String output_utf16 = new File(".").getCanonicalPath() + File.separator + "output_utf-16.txt";
        writeString2FileWithEncoding(info,output_utf16,"utf-16");

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

调用exeWriteString2FileWithEncoding将会创建三个不同编码格式的文件。在操作系统(windows、linux、Mac)中使用恰当的工具都能正确的打开这些文件。
但如果我们使用前面的FileReader类来直接打开这三个文件,就会出现乱码:

Today is a good day.
���������ܺá�

Today is a good day.
今天天气很好。

�� T o d a y   i s   a   g o o d   d a y . 

N�Y)Y)l _�Y}0 

因此我们知道,在对文件进行读取时,需要考虑编码的问题。因此FileInputStream/FileOutputStream,FileReader/FileWriter和InputStreamReader和OutputStreamWriter这三组类都有了用武之地,下面是我总结的文件流使用指南:
当对文件进行拷贝、加密、压缩、摘要等与编码不相关的操作时,尽量使用字节流FileInputStream/FileOutputStream,文件的加密、压缩、摘要等功能留待后续章节(加密流、压缩流和摘要流)介绍;
当需要对文件读取内容或者写入指定编码格式的内容时,使用InputStreamReader和OutputStreamWriter,因为它们可以指定编码;也可以使用FileInputStream/FileOutputStream进行字节的读写,然后利用String和byte数组的转换来得到指定编码的内容;
当程序员可以确认默认的编码一定能满足要求时,直接使用FileReader/FileWriter来进行文件的读写。

3.2 FileInputStream和FileOutputStream

FileInputStream和FileOutputStream都是处理字节的类,因此使用它们时需要把信息转换为字节数组,然后进行输入输出操作。
将一个字符串以GBK编码格式转换为字节后写入当前目录下的output.txt文件中:

private static void fileOutputStreamExam() {
    try {
        //得到当前目录下名为output.txt文件的路径
        String outputfile = new File(".").getCanonicalPath() + File.separator + "output.txt";
        System.out.println(outputfile);
        FileOutputStream fos = new FileOutputStream(outputfile);
        String str = "Today is a good day.\r\n今天天气很好。";
        byte[] buf = str.getBytes("GBK");
        fos.write(buf, 0, buf.length);
        fos.flush();
        fos.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

将当前目录下的output.txt文件以字节流方式读入,并将读入的字节数组从GBK编码格式转换为字符(即转换为java内部使用的unicode)串:

private static void fileInputStreamExam() {
    try {
        //得到当前目录下名为output.txt文件的路径
        String outputfile = new File(".").getCanonicalPath() + File.separator + "output.txt";
        FileInputStream fis = new FileInputStream(outputfile);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buf = new byte[1024];
        int len = 0;
        while ((len = fis.read(buf)) != -1) {
            baos.write(buf, 0, len);
        }
        String s = baos.toString("GBK");
        System.out.println(s);
        fis.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

注意上面这段代码中使用了后面要讲到的字节数组流ByteArrayOutputStream,这个流可以存储动态长度的字节数组,因此非常适合在这里作为信息暂存的对象。

3.3 InputStreamReader和OutputStreamWriter

FileReader和FileWriter只能使用默认的编码格式来输入输出字符,当需要使用其他编码格式时,可以使用更加通用的类InputStreamReader和OutputStreamWriter。
将字符串以GBK编码格式输出到文件output3.txt中:

private static void outputStreamWriterExam() {
    try {
        String outputfile = new File(".").getCanonicalPath() + File.separator + "output3.txt";
        //此处可以指定编码
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(outputfile), "GBK");

        String str = "Today is a good day.\r\n今天天气很好。";
        outputStreamWriter.write(str);
        outputStreamWriter.flush();
        outputStreamWriter.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

将output3.txt文件中的字符串以GBK编码格式读入程序中:

private static void inputStreamReaderExam() {
    try {
        String outputfile = new File(".").getCanonicalPath() + File.separator + "output3.txt";
        InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(outputfile), "GBK");
        CharArrayWriter charArrayWriter = new CharArrayWriter();
        char[] buf = new char[1024];
        int len = 0;
        while((len=inputStreamReader.read(buf))!=-1){
            charArrayWriter.write(buf,0,len);
        }
        String s = charArrayWriter.toString();
        System.out.println(s);
        inputStreamReader.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

注意上面这段代码中使用了字符数组流CharArrayWriter,这个流可以存储动态长度的字符数组,因此非常适合在这里作为信息暂存的对象。

3.4 FileReader和FileWriter

这两个类的使用前面已经介绍过了,就不赘述了。

3.5 文件编码和乱码

对于Java编程来说,如果使用IDE开发,则在IDE中会指定具体项目的编码,例如UTF-8或者GBK,那么在运行代码时IDE会自动加上-Dfile.encoding=UTF-8等参数,使得当前的默认编码被设置为UTF-8。如果在java运行时没有指定编码,则会使用操作系统的默认编码格式,中文windows一般默认是GBK。
一般来说,输出文件时不太可能产生乱码,因为无论你以何种格式编码将字符流转换为字节流并存储到文件中时,该编码一定能够被识别出来,只要你找到合适的文件浏览工具。
但是当输入文件时,如果使用了错误的编码格式进行字节–字符转换,例如将GBK编码的文件以UTF-8格式读入,则会造成乱码。

4. 字节数组流和字符数组流

很多网上的教程在介绍FileInputStream的时候,经常写出这样的代码:

        FileInputStream fis = new FileInputStream(outputfile);
        byte[] buf = new byte[1024];
        int len = fis.read(buf);

代码中使用1024字节的byte数组来存储从文件中读入的字节,但实际工作中文件不一定会小于1024字节,因此这里需要的是一个可变长的字节数组。但是java中并不支持ArrayList,如果自己编写动态可扩展的byte数组又比较浪费时间,因此这里最合适的选择便是ByteArrayOutputStream。
ByteArrayInputStream和ByteArrayOutputStream是用来表示内存中的字节数组流。其中ByteArrayOutputStream可以用来写入变长的字节数组,这对于不知道输入内容的具体长度时非常有用,例如要将一个文件的内容或者网络上的内容读入一个字节数组时。
例子代码:读入一个未知大小的文件到内存中(假设此文件使用默认编码)

public static void byteArrayOutputStreamExam() {
    try {
        FileInputStream fis = new FileInputStream("d:\\d.txt");
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buf = new byte[1024];
        int len = 0;
        while((len = fis.read(buf))!=-1){
            baos.write(buf,0,len);
        }
        baos.flush();
        byte[] result = baos.toByteArray();
        String s = new String(result);
        System.out.println(s);

        baos.close();
        fis.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

例子代码:将一个byte数组做为输入流ByteArrayInputStream的来源,输入到一个ByteArrayOutputStream中。有趣的是,这段byte数组实际上是一段unicode编码,代表一些中文字符,程序中最后将byte数组转换为unicode编码的字符,并打印了这些字符。

public static void byteArrayInputStreamExam() {
    byte[] buf = new byte[256];
    //赋值一段汉字的unicode编码,从4e00至4eff
    for (int i = 0x00; i < 0xff; i = i + 2) {
        buf[i] = 0x4e;
        buf[i + 1] = (byte) i;
    }
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(buf);
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    byte[] getbuf = new byte[128];
    int len = 0;
    try {
        while ((len = byteArrayInputStream.read(getbuf)) != -1) {
            byteArrayOutputStream.write(getbuf, 0, len);
        }
        byte[] result = byteArrayOutputStream.toByteArray();
        //将得到的字节转换为unicode编码的字符串
        String s = new String(result, "unicode");
        System.out.println(s);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

运行输出为:

一丂丄丆丈上丌与丐丒且世丘业东丞丠丢两並丨个丬丮丰串临丶丸为丼举乀乂乄乆么乊乌乎乐乒乔乖乘乚乜乞习乢乤书乨乪乬乮买乲乴乶乸乺乼乾亀亂亄了予亊二于亐互五亖亘亚亜亞亠亢交亦亨亪京亮亰亲亴亶亸人亼亾什仂仄仆仈今仌从仐仒仔他付仚仜仞仠仢令仦仨仪们仮仰仲仴件仸仺仼仾

ByteArrayInputStream和ByteArrayOutputStream是字节数组流,那么与之对应的字符数组流则是StringReader和StringWriter(早期java使用StringBufferInputStream和StringBufferOutputStream,这两个类在jdk1.1后被废弃),也给出一个例子:

public static void stringReaderExam() {
    String str = "This is a good day.今天是个好天气。";
    StringReader stringReader = new StringReader(str);
    StringWriter stringWriter = new StringWriter();
    char[] buf = new char[128];
    int len = 0;
    try {
        while ((len = stringReader.read(buf)) != -1) {
            stringWriter.write(buf, 0, len);
        }
        stringWriter.flush();
        String result = stringWriter.toString();
        System.out.println(result);
        stringWriter.close();
        stringReader.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

与字节数组流相比,字符数组流反而用得更少,因为StringBuilder和StringBuffer也能方便的用来存储动态长度的字符,而且大家更熟悉这些类。

5. 管道流

管道流是用来在多个线程之间进行信息传递的Java流,被号称是最难使用的流,被使用的频率也非常低。但事实上,管道流是非常有用的,它提供了多线程间信息传输的一种有效手段。
管道流包括两个类PipedOutputStream和PipedInputStream。其中PipedOutputStream是写入者/生产者/发送者;PipedInputStream是读取者/消费者/接收者。在使用管道流之前,需要注意以下要点:
第一,管道流仅用于多个线程之间传递信息,若用在同一个线程中可能会造成死锁;
第二,管道流的输入输出是成对的,一个输出流只能对应一个输入流,使用构造函数或者connect函数进行连接;
第三,一对管道流包含一个缓冲区,其默认值为1024个字节,若要改变缓冲区大小,可以使用带有参数的构造函数;
第四,管道的读写操作是互相阻塞的,当缓冲区为空时,读操作阻塞;当缓冲区满时,写操作阻塞;
第五,管道依附于线程,因此若线程结束,则虽然管道流对象还在,仍然会报错“read dead end”;
第六,管道流的读取方法与普通流不同,只有输出流正确close时,输出流才能读到-1值。

5.1 在管道中写入读取一个字符串

源代码一:在线程Sender中向管道流中写入一个字符串,写入后关闭该管道流;在线程Reciever中读取该字符串。

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class PipedStreamExam1 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();

        try {
            //创建管道流
            PipedOutputStream pos = new PipedOutputStream();
            PipedInputStream pis = new PipedInputStream(pos);

            //创建线程对象
            Sender sender = new Sender(pos);
            Reciever reciever = new Reciever(pis);

            //运行子线程
            executorService.execute(sender);
            executorService.execute(reciever);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //等待子线程结束
        executorService.shutdown();
        try {
            executorService.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class Sender extends Thread {
        private PipedOutputStream pos;

        public Sender(PipedOutputStream pos) {
            super();
            this.pos = pos;
        }

        @Override
        public void run() {
            try {
                String s = "This is a good day. 今天是个好天气。";
                System.out.println("Sender:" + s);
                byte[] buf = s.getBytes();
                pos.write(buf, 0, buf.length);
                pos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    static class Reciever extends Thread {
        private PipedInputStream pis;

        public Reciever(PipedInputStream pis) {
            super();
            this.pis = pis;
        }

        @Override
        public void run() {
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                byte[] buf = new byte[1024];
                int len = 0;
                while ((len = pis.read(buf)) != -1) {
                    baos.write(buf, 0, len);
                }
                byte[] result = baos.toByteArray();
                String s = new String(result, 0, result.length);
                System.out.println("Reciever:" + s);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

注意,若管道流没有关闭,则使用这种方法读取管道中的信息会报错:

while ((len = pis.read(buf)) != -1) {
    baos.write(buf, 0, len);
}

错误代码为java.io.IOException: Write end dead;发生这个错误的原因在于,由于管道未关闭,所以read语句不会读到-1,因此PipedInputStream会持续从管道中读取数据,但是因为Sender线程已经结束,所以会抛出“Write end dead”异常。

5.2 在管道中多次写入读取字符串

管道流之所以难用,是因为很多情况下写入管道的数据难以区分“长度”,它的设计理念是“通过管道,将源数据源源不绝的发送到目的地”。因此,如果应用场景为“通过管道,将一段一段的数据一次次的发送到目的地”,就会发现很难使用。为此,使用Java多线程中的信号量来进行同步可以很好的满足此需求。
源代码二:在线程Sender中反复写入多个字符串,在Reciever中多次接收字符串;使用两个信号量Semaphore来控制写入和读取。

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class PipedStreamExam2 {
    //控制读取的信号量,初始状态为0
    public static Semaphore readSignal = new Semaphore(0,true);
    //控制写入的信号量,初始状态为1,表示允许一次写入
    public static Semaphore writeSignal = new Semaphore(1,true);

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();

        try {
            PipedOutputStream pos = new PipedOutputStream();
            PipedInputStream pis = new PipedInputStream(pos);

            Sender sender = new Sender(pos);
            Reciever reciever = new Reciever(pis);

            executorService.execute(sender);
            executorService.execute(reciever);

        } catch (IOException e) {
            e.printStackTrace();
        }
        executorService.shutdown();
    }


    static class Sender extends Thread {
        private PipedOutputStream pos;

        public Sender(PipedOutputStream pos) {
            super();
            this.pos = pos;
        }

        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    Thread.sleep(new Random().nextInt(1000));
                    //获取写入信号量
                    writeSignal.acquire();
                    String content = "today is a good day. 今天是个好天气:"+i;
                    System.out.println("Sender:" + content);
                    pos.write(content.getBytes("utf-8"));
                    //释放读取信号量
                    readSignal.release();
                }
                pos.close();
                readSignal.release();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class Reciever extends Thread {
        private PipedInputStream pis;
        public Reciever(PipedInputStream pis) {
            super();
            this.pis = pis;
        }

        @Override
        public void run() {
            try {
                byte[] buf = new byte[1024];
                int len = 0;
                String s;
                while(true) {
                    //获取读取信号量
                    readSignal.acquire();
                    len = pis.read(buf);
                    if(len == -1)
                        break;
                    s = new String(buf, 0, len, "utf-8");
                    System.out.println("Reciever:" + s);
                    //释放写入信号量
                    writeSignal.release();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

5.3 通过管道将信息流发送至另一个线程

经过思考,我认为管道流应用的经典场景应该是将某个输入流从一个线程通过管道发送到另一个线程进行处理,从而提升程序效率。例如:线程A负责从网络上持续读取信息,线程B负责处理信息,那么线程A就会将读取的信息通过管道流发送至线程B,从而确保线程A的读取性能。
下面的例子中,Sender线程从文件中读取未知长度的字节流,然后交给Reciever线程,Reciever线程将此字节流存入另一个文件:

import java.io.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class PipedStreamExam3 {
    public static void main(String[] args) {
        try {
            PipedInputStream pis = new PipedInputStream();
            PipedOutputStream pos = new PipedOutputStream(pis);

            Sender sender = new Sender(pos);
            Reciever reciever = new Reciever(pis);

            ExecutorService executorService = Executors.newCachedThreadPool();
            executorService.execute(sender);
            executorService.execute(reciever);

            executorService.shutdown();
            executorService.awaitTermination(1, TimeUnit.DAYS);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class Sender extends Thread {
        private PipedOutputStream pos = null;

        public Sender(PipedOutputStream pos) {
            this.pos = pos;
        }

        @Override
        public void run() {
            try {
                FileInputStream fis = new FileInputStream("d:\\input.txt");
                byte[] buf = new byte[1024];
                int len = 0;
                while ((len = fis.read(buf)) != -1) {
                    pos.write(buf, 0, len);
                }
                pos.flush();
                pos.close();
                fis.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    static class Reciever extends Thread {
        private PipedInputStream pis = null;

        public Reciever(PipedInputStream pis) {
            this.pis = pis;
        }

        @Override
        public void run() {
            try {
                FileOutputStream fos = new FileOutputStream("d:\\output.txt");
                byte[] buf = new byte[1024];
                int len = 0;
                while ((len = pis.read(buf)) != -1) {
                    fos.write(buf, 0, len);
                }
                fos.flush();
                fos.close();
                pis.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

5.4 管道流小结

比起文件流、字节数组流和对象流这样使用很普遍的流,管道流有很多不同之处。首先它必须依附线程对象,当线程对象已经失效而流未关闭时会出错;其次它往往读不到-1,因此在很多场景中需要程序员自己来保证同步;第三管道流能够保证良好的互斥,这往往是很有用的一点。多加练习,管道流大有用武之地。

6. Java过滤器概论

前面章节讲到的各种Java流,包括文件流、字节数组流、管道流等等被称之为原始流,它们提供了对某类数据的输入输出功能。为了在流处理的过程中简化和标准化某一类功能,例如缓冲、压缩、加密、摘要等,Java提供了一系列过滤器类,每组类提供了一种典型的信息处理功能。根据操作对象是字节还是字符,过滤器又分为过滤器流和阅读器/书写器两类,举例如下:

  • BufferedInputStream/BufferedOutputStream提供了字节流的缓冲功能;
  • BufferedReader/BufferedWriter提供了字符流的缓冲功能;
  • DataInputStream/DataOutputStream用来将java的简单数据类型和字符串保存为二进制格式,并从二进制格式读取;
  • ZipInputStream/ZipOutputStream提供了字节流的zip格式压缩功能;
  • GZIPInputStream/GZIPOutputStream提供了字节流的gzip格式压缩功能;
  • DigestInputStream/DigestOutputStream提供了字节流的信息摘要功能;
  • CipherInputStream/CipherOutputStream提供了字节流的加密解密功能;

6.1 过滤器使用规则

过滤器可以和其他过滤器、原始流和阅读器/书写器链接使用,但要遵守以下规则:

  • 过滤器流可以置于原始流或其他过滤器流之上;
  • 阅读器/书写器可置于原始流、过滤器流或其他阅读器/书写器之上;
  • 过滤器流不能放在阅读器/书写器之后。

6.2 最简单的过滤器-缓冲流

在文件拷贝的过程中加入缓冲功能,提升性能:

import java.io.*;

public class BufferedStreamExam {
    public static void main(String[] args) {
        try {
            BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("d:\\input.txt"));
            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("d:\\output.txt"));
            byte[] buf = new byte[1024];
            int len = 0;
            while ((len = bufferedInputStream.read(buf)) != -1) {
                bufferedOutputStream.write(buf, 0, len);
            }
            bufferedOutputStream.flush();
            bufferedOutputStream.close();
            bufferedInputStream.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

观察代码可知,当多个流链接到一起时,关闭最上层的流即可。为了防止多次关闭一个链条中的流,在创建多个流时仅保留最上层流的对象引用,类似:

BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("d:\\input.txt"));

后面会讲到,为了创建一个带有缓冲、加密、压缩的文件输入流,我们会这样写代码:

BufferedInputStream bis = new BufferedInputStream(
                    new CipherInputStream(
                            new GZIPInputStream(
                                    new FileInputStream(desFile)),pbe));

7. 数据流

数据流DataOutputStream和DataInputStream用来将Java的简单数据类型和字符串保存为二进制格式,并从二进制格式读取。使用它们时需要注意以下几点:
第一,DataOutputStream输出的二进制流,必须使用DataInputStream读入,且各个变量的输出输出顺序必须相同;
第二,boolean,byte,short,char,int,long,float,double和String可以使用相应的write和read方法进行输出和输入,例如writeInt和readInt;
第三,输入输出字符串时使用readUTF和writeUTF,避免使用writeChars和wirteBytes等方法。因为writeUTF方法中将字符串长度一并保存,所以在readUTF中可以正确读取。
例子如下:

public static void dataOutputStreamExam() {
    try {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        boolean b = true;
        dos.writeBoolean(b);
        showDataByte(baos);

        byte by = 0x03;
        dos.writeByte(by);
        showDataByte(baos);

        short sh = 42;
        dos.writeShort(sh);
        showDataByte(baos);

        char c = '一';
        dos.writeChar(c);
        showDataByte(baos);

        int i = 1234567;
        dos.writeInt(i);
        showDataByte(baos);

        long l = 12345678L;
        dos.writeLong(l);
        showDataByte(baos);

        float f = 3.1415926f;
        dos.writeFloat(f);
        showDataByte(baos);

        double dou = 3.1415926535;
        dos.writeDouble(dou);
        showDataByte(baos);

        String s = "This is a good day. 今天是个好天气.";
        dos.writeUTF(s);
        showDataByte(baos);

        int i2 =233;
        dos.writeInt(i2);
        showDataByte(baos);

        dos.flush();
        baos.flush();

        //依次读取数据
        DataInputStream dis = new DataInputStream(new ByteArrayInputStream(baos.toByteArray()));
        b = dis.readBoolean();
        System.out.println(b);
        by = dis.readByte();
        System.out.println(by);
        sh =dis.readShort();
        System.out.println(sh);
        c=dis.readChar();
        System.out.println(c);
        i=dis.readInt();
        System.out.println(i);
        l=dis.readLong();
        System.out.println(l);
        f=dis.readFloat();
        System.out.println(f);
        dou=dis.readDouble();
        System.out.println(dou);
        s=dis.readUTF();
        System.out.println(s);
        i2=dis.readInt();
        System.out.println(i2);
        dis.close();

        dos.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

}

public static void showDataByte(ByteArrayOutputStream baos) {
    byte[] buf = baos.toByteArray();
    for (int j = 0; j < buf.length; j++) {
        System.out.print( byteToHexString(buf[j])+",");
    }
    System.out.println("------------------------------");
}

public static String byteToHexString(byte b) {
    String s = Integer.toHexString(b);
    int len = s.length();
    if (len >= 2) {
        s = s.substring(len - 2);
    }else{
        s = "0"+s;
    }
    return s;
}

例子中使用了ByteArrayOutputStream和ByteArrayInputStream来作为输入输出流,这样可以显得程序易读。事实上更多情况下是使用文件流来进行输出输入。

8. 压缩流

压缩流可以将输入的数据变为压缩格式后进行输出,或者读取压缩格式的数据后,解压为正常数据。

8.1 将一个文件压缩为一个压缩文件

注意ZipEntry的使用,一个ZipEntry代表压缩文件中的一个文件入口。

import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Created by test2 on 2016/8/19.
 */
public class ZipStreamExam1 {
    public static void main(String[] args) {
        try {
            BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("D:\\input.txt"));
            ZipOutputStream zipOutputStream = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream("d:\\output.zip")));
            byte[] buf = new byte[1024];
            int len = 0;
            ZipEntry ze = new ZipEntry("input.txt");
            zipOutputStream.putNextEntry(ze);
            while ((len = bufferedInputStream.read(buf)) != -1) {
                zipOutputStream.write(buf, 0, len);
            }
            zipOutputStream.flush();
            zipOutputStream.close();
            bufferedInputStream.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

8.2 将一个多重目录压缩为一个文件

代码中定义使用了递归函数zipDir:

import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class ZipStreamExam2 {
    public static void main(String[] args) {
        try {
            File file = new File("d:\\zipmultidir");
            ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream("d:\\zipmultidir.zip")));
            zipDir(file, zos, file);
            zos.flush();
            zos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //压缩一个目录至zip文件
    private static void zipDir(File dir, ZipOutputStream zos, File rootDir) throws IOException {
        if (!dir.isDirectory())
            return;

        File[] files = dir.listFiles();
        for (int i = 0; i < files.length; i++) {
            if (files[i].isFile()) {
                System.out.println(files[i].getAbsolutePath());
                String now = files[i].getAbsolutePath();
                String root = rootDir.getAbsolutePath();
                String name = now.substring(root.length() + 1);
                System.out.println(name);

                FileInputStream fis = new FileInputStream(files[i]);

                byte buf[] = new byte[1024];
                int len = 0;
                ZipEntry ze = new ZipEntry(name);
                zos.putNextEntry(ze);
                while ((len = fis.read(buf)) != -1) {
                    zos.write(buf, 0, len);
                }
                fis.close();
            } else if (files[i].isDirectory()) {
                zipDir(files[i], zos, rootDir);
            }
        }
    }
}

8.3 将一个压缩文件解压到当前目录

该压缩文件中可能有多重目录结构。

import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * Created by test2 on 2016/8/19.
 */
public class ZipStreamExam3 {
    public static void main(String[] args) {
        try {
            File srcFile = new File("d:\\zipmultidir.zip");
            System.out.println(srcFile.getCanonicalPath());
            String curDir = srcFile.getParent()+File.separator+"destDir"+File.separator;

            ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(new FileInputStream(srcFile)));
            ZipEntry ze = null;
            byte[] buf = new byte[1024];
            int len = 0;
            while ((ze = zipInputStream.getNextEntry()) != null) {
                String filePath = curDir + ze.getName();
                File destFile = new File(filePath);
                File destDir = new File(destFile.getParent());
                if(!destDir.exists()){
                    destDir.mkdirs();
                }
                BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(destFile));
                while ((len = zipInputStream.read(buf)) != -1) {
                    bufferedOutputStream.write(buf, 0, len);
                }
                bufferedOutputStream.flush();
                bufferedOutputStream.close();
            }
            zipInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

9. 摘要流

摘要流在对一组信息进行输入输出操作的同时,将摘要信息记录下来,最后通过getMessageDigest().digest()方法得到摘要信息的byte数组。
摘要流示例代码:对字符串进行摘要和对文件进行摘要。

import java.io.*;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class DigestStreamExam {
    public static void main(String[] args) {
        String str = "Today is a good day. 今天是个好天气。";
        digestString(str);
        File file = new File("d:\\a.txt");
        digestFile(file);
    }

    private static void digestFile(File file) {
        try {
            MessageDigest sha = MessageDigest.getInstance("SHA");
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            DigestOutputStream digestOutputStream = new DigestOutputStream(byteArrayOutputStream, sha);

            FileInputStream fis = new FileInputStream(file);
            byte[] buf = new byte[1024];
            int len = 0;
            while ((len = fis.read(buf)) != -1) {
                digestOutputStream.write(buf, 0, len);
            }
            digestOutputStream.flush();
            digestOutputStream.close();
            byte[] result = digestOutputStream.getMessageDigest().digest();
            for (int i = 0; i < result.length; i++) {
                System.out.print(byteToHexString(result[i]));
            }
            System.out.println();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void digestString(String str) {
        try {
            MessageDigest sha = MessageDigest.getInstance("SHA");
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            DigestOutputStream digestOutputStream = new DigestOutputStream(byteArrayOutputStream, sha);

            byte[] bytes = str.getBytes();
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

            byte[] buf = new byte[1024];
            int len = 0;
            while ((len = byteArrayInputStream.read(buf)) != -1) {
                digestOutputStream.write(buf, 0, len);
            }
            digestOutputStream.flush();
            digestOutputStream.close();
            byte[] result = digestOutputStream.getMessageDigest().digest();
            for (int i = 0; i < result.length; i++) {
                System.out.print(byteToHexString(result[i]));
            }
            System.out.println();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static String byteToHexString(byte b) {
        String s = Integer.toHexString(b);
        int len = s.length();
        if (len >= 2) {
            s = s.substring(len - 2);
        } else {
            s = "0" + s;
        }
        return s;
    }
}

运行输出为:

cb3492a453633b7ade978a7214c064b82a332a94
788dc68d752ffb1a12c52c5436cbd4351ba8bf3c

注意,为了输出美观,我提供了一个byteToHexString方法,该方法可以将byte转换为一个两个字符的String,该String中包含了该byte的十六进制格式。
摘要流被广泛用于各种信息的验证中,是一种很常用的手段。
本例中的加密流使用了PBEWithMD5AndDES加密算法,该算法是对称加密算法,加密和解密使用的密钥是一样的。不同的是除了密码之外,还需要“盐”。代码如下:

import javax.crypto.*;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;

public class CypherStreamExam {
    public static void main(String[] args) {
        String infile = "D:\\a.txt";
        String outfile = "D:\\a.des";
        String password = "This is my key";
        byte[] salt = initSalt();
        encryptFile(infile, outfile, password, salt);

        infile = "D:\\a.des";
        outfile = "D:\\a.out";
        decryptFile(infile, outfile, password, salt);
    }

    //产生加密所用的盐
    public static byte[] initSalt() {
        //实例化安全随机数
        SecureRandom random = new SecureRandom();
        //产出盐
        return random.generateSeed(8);
    }

    private static void decryptFile(String infile, String outfile, String password, byte[] salt) {

        byte[] desKeyData = password.getBytes();
        try {
            FileInputStream fin = new FileInputStream(infile);
            FileOutputStream fout = new FileOutputStream(outfile);

            Provider sunJce = new com.sun.crypto.provider.SunJCE();
            Security.addProvider(sunJce);

            //创建密钥
            char[] pbeKeyData = password.toCharArray();
            PBEKeySpec pbeKeySpec = new PBEKeySpec(pbeKeyData);
            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
            SecretKey pbeKey = keyFactory.generateSecret(pbeKeySpec);
            PBEParameterSpec paramSpec = new PBEParameterSpec(salt, 100);

            //解密
            Cipher pbe = Cipher.getInstance("PBEWithMD5AndDES");
            pbe.init(Cipher.DECRYPT_MODE, pbeKey, paramSpec);
            CipherOutputStream cout = new CipherOutputStream(fout, pbe);

            byte[] input = new byte[64];
            int len = 0;
            while ((len = fin.read(input)) != -1) {
                cout.write(input, 0, len);
            }
            cout.flush();
            cout.close();
            fin.close();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        }
    }

    private static void encryptFile(String filein, String fileout, String password, byte[] salt) {
        byte[] desKeyData = password.getBytes();
        try {
            FileInputStream fin = new FileInputStream(filein);
            FileOutputStream fout = new FileOutputStream(fileout);

            Provider sunJce = new com.sun.crypto.provider.SunJCE();
            Security.addProvider(sunJce);

            //创建密钥
            char[] pbeKeyData = password.toCharArray();
            PBEKeySpec pbeKeySpec = new PBEKeySpec(pbeKeyData);
            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
            SecretKey pbeKey = keyFactory.generateSecret(pbeKeySpec);
            PBEParameterSpec paramSpec = new PBEParameterSpec(salt, 100);

            //加密
            Cipher pbe = Cipher.getInstance("PBEWithMD5AndDES");
            pbe.init(Cipher.ENCRYPT_MODE, pbeKey, paramSpec);
            CipherOutputStream cout = new CipherOutputStream(fout, pbe);

            byte[] input = new byte[64];
            int len = 0;
            while ((len = fin.read(input)) != -1) {
                cout.write(input, 0, len);
            }
            cout.flush();
            cout.close();
            fin.close();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        }
    }
}

在真实的应用中,加密方和解密方都知道密码,这个密码一般来说是双方约定的,例如你银行账户密码。如果这个密码被泄露,也不用担心,因为解密还需要“盐”。盐一般来说是由一种随机生成算法生成的,而且生存期很短。例如网上银行使用的U Key就是一种盐生成器,特别是“中国银行”的U Key,在进行转账时直接要求用户输入6位数字的盐。由于网上银行的服务端和客户手中的U Key使用同一种盐生成算法(这个算法当然是保密的,也无从泄露),因此保证了盐不会泄露,从而保证用户进行操作时不会泄密。
将一组信息缓冲、加密、压缩后保存到一个文件中,然后从此文件中读取该信息,经解密、解压缩后输出到屏幕。代码如下:

import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;

import javax.crypto.*;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.io.*;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * Created by test2 on 2016/9/2.
 * 真实世界的流使用范例
 */
public class RealWorldStream {
    public static void main(String[] args) {
        String srcFile = "D:\\src.txt";
        String desFile = "D:\\src.des.gz";
        makeSourceFile(srcFile);

        //密码和盐
        String password = "This is my key";
        byte[] salt = initSalt();
        //加密压缩保存
        gzipCipherFile(srcFile, desFile, password, salt);

        //解密解压,输出到屏幕
        ungzipCipherFile(desFile, password, salt);
    }

    private static void ungzipCipherFile(String desFile, String password, byte[] salt) {
        try {
            //创建密钥
            byte[] desKeyData = password.getBytes();
            Provider sunJce = new com.sun.crypto.provider.SunJCE();
            Security.addProvider(sunJce);
            char[] pbeKeyData = password.toCharArray();
            PBEKeySpec pbeKeySpec = new PBEKeySpec(pbeKeyData);
            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
            SecretKey pbeKey = keyFactory.generateSecret(pbeKeySpec);
            PBEParameterSpec paramSpec = new PBEParameterSpec(salt, 100);
            Cipher pbe = Cipher.getInstance("PBEWithMD5AndDES");
            pbe.init(Cipher.DECRYPT_MODE, pbeKey, paramSpec);

            BufferedInputStream bis = new BufferedInputStream(
                    new CipherInputStream(
                            new GZIPInputStream(
                                    new FileInputStream(desFile)),pbe));
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buff =new byte[1024];
            int len=0;
            while((len=bis.read(buff))!=-1){
                baos.write(buff,0,len);
            }
            baos.flush();
            String str = baos.toString("UTF-16");
            baos.close();
            bis.close();
            System.out.println(str);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        }
    }

    //产生加密所用的盐
    public static byte[] initSalt() {
        //实例化安全随机数
        SecureRandom random = new SecureRandom();
        //产出盐
        return random.generateSeed(8);
    }

    //将源文件进行加密压缩后保存到目标文件
    private static void gzipCipherFile(String srcFile, String desFile, String password, byte[] salt) {
        try {
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));

            //创建密钥
            byte[] desKeyData = password.getBytes();
            Provider sunJce = new com.sun.crypto.provider.SunJCE();
            Security.addProvider(sunJce);
            char[] pbeKeyData = password.toCharArray();
            PBEKeySpec pbeKeySpec = new PBEKeySpec(pbeKeyData);
            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
            SecretKey pbeKey = keyFactory.generateSecret(pbeKeySpec);
            PBEParameterSpec paramSpec = new PBEParameterSpec(salt, 100);
            Cipher pbe = Cipher.getInstance("PBEWithMD5AndDES");
            pbe.init(Cipher.ENCRYPT_MODE, pbeKey, paramSpec);

            BufferedOutputStream bos = new BufferedOutputStream(
                    new CipherOutputStream(
                            new GZIPOutputStream(
                                    new FileOutputStream(desFile)), pbe));
            byte[] buff = new byte[1024];
            int len = 0;
            while ((len = bis.read(buff)) != -1) {
                bos.write(buff, 0, len);
            }
            bos.flush();
            bos.close();
            bis.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        }
    }

    //创建一个程序中用来执行的源文件
    private static void makeSourceFile(String srcFile) {
        File file = new File(srcFile);
        String str = "Today is a good day. 今天天气很好。";
        try {
            //使用JAVA内部的编码标准UTF-16来进行char到byte的编码转换
            byte[] bytes = str.getBytes("UTF-16");

            //写入文件
            ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
            FileOutputStream fos = new FileOutputStream(file);
            byte[] buff = new byte[1024];
            int len = 0;
            while ((len = bais.read(buff)) != -1) {
                fos.write(buff, 0, len);
            }
            fos.flush();
            fos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

如果你已经阅读过本博客的其他文章,理解这一段代码也没有任何问题了。Java的流就是这样使用的,入手稍微有一点困难,但是熟悉之后很容易写出功能强大的代码。

编辑于 2016-12-30 23:35