*/

.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #333;
background: #f8f8f8;
}

.hljs-comment,
.hljs-template_comment,
.diff .hljs-header,
.hljs-javadoc {
color: #998;
font-style: italic;
}

.hljs-keyword,
.css .rule .hljs-keyword,
.hljs-winutils,
.javascript .hljs-title,
.nginx .hljs-title,
.hljs-subst,
.hljs-request,
.hljs-status {
color: #333;
font-weight: bold;
}

.hljs-number,
.hljs-hexcolor,
.ruby .hljs-constant {
color: #099;
}

.hljs-string,
.hljs-tag .hljs-value,
.hljs-phpdoc,
.tex .hljs-formula {
color: #d14;
}

.hljs-title,
.hljs-id,
.coffeescript .hljs-params,
.scss .hljs-preprocessor {
color: #900;
font-weight: bold;
}

.javascript .hljs-title,
.lisp .hljs-title,
.clojure .hljs-title,
.hljs-subst {
font-weight: normal;
}

.hljs-class .hljs-title,
.haskell .hljs-type,
.vhdl .hljs-literal,
.tex .hljs-command {
color: #458;
font-weight: bold;
}

.hljs-tag,
.hljs-tag .hljs-title,
.hljs-rules .hljs-property,
.django .hljs-tag .hljs-keyword {
color: #000080;
font-weight: normal;
}

.hljs-attribute,
.hljs-variable,
.lisp .hljs-body {
color: #008080;
}

.hljs-regexp {
color: #009926;
}

.hljs-symbol,
.ruby .hljs-symbol .hljs-string,
.lisp .hljs-keyword,
.tex .hljs-special,
.hljs-prompt {
color: #990073;
}

.hljs-built_in,
.lisp .hljs-title,
.clojure .hljs-built_in {
color: #0086b3;
}

.hljs-preprocessor,
.hljs-pragma,
.hljs-pi,
.hljs-doctype,
.hljs-shebang,
.hljs-cdata {
color: #999;
font-weight: bold;
}

.hljs-deletion {
background: #fdd;
}

.hljs-addition {
background: #dfd;
}

.diff .hljs-change {
background: #0086b3;
}

.hljs-chunk {
color: #aaa;
}

#container {
padding: 15px;
}
pre {
border: 1px solid #ccc;
border-radius: 4px;
display: block;
background-color: #f8f8f8;
}
pre code {
white-space: pre-wrap;
}
.hljs,
code {
font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
}
:not(pre) > code {
padding: 2px 4px;
font-size: 90%;
color: #c7254e;
background-color: #f9f2f4;
white-space: nowrap;
border-radius: 4px;
}
-->

额外功能处理流的意思是在基础流(InputStream/OutputStream/Reader/Writer)的基础上提供额外的功能。常见的额外功能可归纳为以下几种。

  1. 是否使用缓冲功能:BufferedInputStream/BufferedOutputStream/BufferedReader/BufferedWriter(字符流的缓冲对象还提供了操作行的方法)
  2. 是否串联多个字节输入流:SequenceInputStream
  3. 是否使用对象序列化功能:ObjectInputStream/ObjectOutputStream(涉及序列化接口Serializable)
  4. 是否让流保证数据类型不变:DataInputStream/DataOutputStream
  5. 是否让输出流输出时保证输出字面符号:PrintStream/PrintWriter(打印流)
  6. 是否要操作内存中的字符串和数组:ByteArrayInputStream/ByteArrayOutputStream/CharArrayReader/CharArrayWriter

Bufferedxxx类和Array相关的功能此处不做介绍。本文将介绍除此之外的其余功能以及对象序列化时涉及到的序列化接口Serializable。

1.输入流的串联:SequenceInputStream

SequenceInputStream按照IO体系命名的特点来理解,大致是"将字节输入流存放到Sequence序列中",实际上,它可用来串联多个输入流。意思是:有输入流1、输入流2、输入流3,原本的行为是按照顺序先后读取输入流1、2、3,现在将这3个输入流按顺序连起来当作一个大输入流,直到输入流3读完后才到流的末尾。

这个序列输入流类在IO体系里有点特立独行,它只有输入流,没有对应的输出流。它的作用是以操作一个输入流的方式来将多个输入流按序追加读取。例如,将多个文件的数据以追加的方式写入到一个目标文件中。

当调用SequenceInputStream的close()方法时,它将会自动关闭所有它所串联的输入流。

如下图:

要使用SequenceInputStream,首先看构造方法SequenceInputStream(Enumeration<? extends InputStream> e),可见它只能接收枚举出来的字节输入流。但如何获取到这些枚举元素?可以将各个输入流存放到一个集合中,然后使用Collections工具类中的enumeration(Collection c)方法将这个集合转换为Enumeration对象。在此还需说明的是,通常SequenceInputStream要串联的多个流都是有先后顺序的,例如1.txt,2.txt,3.txt依序串联下去,所以枚举时也要保证能够依序枚举出来,这也要求在Collection转换为Enumeration时,集合中的流对象在集合中也是有序的,这意味着使用List集合来存储这些流对象是最佳的。

例如,下面的示例中将{1..6}.txt共6个txt文件按文件名排序先后串联成一个SequenceInputStream。

//存储多个字节输入流对象到List集合中
List<FileInputStream> list = new ArrayList<FileInputStream>();
for(int i=1;i<=6;i++){
list.add(new FileInputStream(i+".txt"));
}
//将List集合转换为枚举对象Enumeration
Enumeration<FileInputStream> en = Collections.enumeration(list);
//将枚举出来的各个字节输入流串联起来
SequenceInputStream sis = new SequenceInputStream(en);

2. ObjectInputStream/ObjectOutputStream和序列化接口Serializable

输入流和输出流可以按字节、存储读取媒体类、文本类文件,但能否将java中的对象也作为数据持久化到文件中呢?io包中提供了ObjectInputStream和ObjectOutputStream来读、写对象。

例如给定如下Student类,将以此类作为ObjectInputStream/ObjectOutputStream流读、写的对象。

class Student {
String name;
int age;
Student(String name,int age){
this.name = name;
this.age = age;
} public String getName() {
return name;
} public int getAge() {
return age;
} public String toString(){
return "{name="+name+",age="+age+"}";
}
}

下面使用ObjectOutputStream将Student对象写入到文件中,这类文件的规范后缀名为".object"。该类的构造方法为ObjectOutputStream(OutputStream out)

import java.io.*;
import java.util.*; public class ObjectStream {
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/temp/a.txt"));
Student stu = new Student("malongshuai",22);
oos.writeObject(stu);
}
}

编译并执行上述代码,将抛出NotSerializableException异常,意思是未序列化。那么谁没有序列化?Student对象。要想让某对象序列化的方式很简单,只需让Student类实现Serializable接口即可。如下:

class Student implements Serializable {
String name;
int age;
Student(String name,int age){
this.name = name;
this.age = age;
} public String getName() {
return name;
} public int getAge() {
return age;
} public String toString(){
return "{name="+name+",age="+age+"}";
}
}

可以使用ObjectInputStream从文件中读取曾被序列化的数据。这称为"反序列化"。

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/temp/a.txt"));
Object obj = ois.readObject();
System.out.println("read Object from file:"+stu1.toString()); //多态

看上去序列化和反序列化是一件很简单的事情。确实如此,但其有不少知识点和需要注意的关键点。

  1. 什么是序列化?
    从前面的例子中可以看出,序列化的方式非常简单,只需实现Serializable接口即可。它不提供任何方法。序列化的意义仅仅只是为类进行一种特殊的标识,即所谓的"盖戳"。就像夫妻如何证明他们是夫妻,颁发一个结婚证即可,再例如猪肉凭什么是合格的?给它贴一张合格标签即可。
  2. 序列化的目的是什么?
    为了将某些对象持久化保存起来,供以后反序列化的时候读取。
  3. 序列化后的对象在存储时会存储哪些数据?
    存储的内容包括:类名和类签名(类的序列化版本号SerialVersionUID)、对象的字段值和数组值,以及从初始对象中引用的其他所有对象的闭包。大致可以看作是存储了一个类的版本号、类名、某些字段的值以及引用的对象。当然,并非所有字段值都会存储,见下文的第6点。
  4. 对某个对象序列化后,修改对象的属性(例如将成员变量的修饰符从public改为private),反序列化时将会如何?
    因为保存起来的序列化数据带有一个类签名SerialVersionUID,而修改类的定义后,编译时这个类会生成Class文件,而这个文件中的版本号将不再和之前序列化时保存的版本号相同。于是抛出异常。
    java.io.InvalidClassException: Student; local class incompatible: stream classdesc serialVersionUID = -9151998530267376490, local class serialVersionUID = -3521625297801190192

    由此可以明确一点:class文件中仅只存储类的定义语句,在new对象时将在堆内存中开辟一段空间并存储对象数据(如成员变量)。反序列化实际上是将保存起来的类对象数据加载到这个开辟出来的对象空间中。

  5. 如何保证反序列化的成功?
    强烈建议显式在实现了Serializable接口的类中,声明一个固定的序列化。如:
    public/private/... static final long serialVersionUID = 123456L;

    这样一来,无论是序列化保存时,还是后来修改了类定义生成的class文件中,其版本号都是固定且相同的,也就是说不会因为序列化版本号不同而反序列化失败。

  6. 类中所有属性都应该序列化吗?
    显然不是。有两类数据不会被保存:静态变量(static)、瞬态变量(transient)。例如密码字段、时间点等随时改变、有安全隐患的数据不应该被序列化保存。在进行序列化的时候,只是将堆内存中的数据保存起来,所以加了static关键字的静态属性不会被序列化。而加了transient关键字的(如为Student类的age加上瞬态属性public transient int age;)也不会被序列化。

看上去说了一大堆,其实操作起来非常简单,只需为待序列化的对象实现Serializable接口并声明serialVersionUID就可以了。

以下是ObjectInputStream和ObjectOutputStream序列化、反序列化多个对象的示例。序列化的时候使用了集合的方式,将多个Student对象存储到集合中,然后遍历集合来序列化各个Student对象。反序列化的时候,由于ObjectInputStream的readObject()一次读取一个对象示例的数据,且没有提供合适的判断流结尾的返回值,只是在读取到结尾时会抛出EOFException异常。因此此处采用while无限循环的方式,并通过抛出的EOFException异常来结束循环。

import java.io.*;
import java.util.*; public class ObjectStream {
public static void main(String[] args) {
//将各学生对象存放到集合中
List<Student> list = new ArrayList<Student>();
list.add(new Student("Malongshuai",22));
list.add(new Student("Gaoxiaofang",22)); //序列化
//writeObj(list,"d:/temp/a.object"); //反序列化
readObj("d:/temp/a.object");
} //序列化
public static void writeObj(List list,String filename) {
//遍历集合中的对象并将它们序列化
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(filename));
for(Iterator it = list.iterator();it.hasNext();) {
oos.writeObject(it.next());
}
} catch (FileNotFoundException f) {
f.printStackTrace();
} catch (IOException i) {
i.printStackTrace();
} finally {
if(oos!=null) {
try {
oos.close();
} catch(IOException i){
i.printStackTrace();
}
}
}
} //反序列化:读取序列化数据
public static void readObj(String filename) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(filename));
while(true) {
Student student = (Student)ois.readObject();
System.out.println(student.toString());
}
} catch (EOFException e){ } catch (FileNotFoundException f) {
f.printStackTrace();
} catch (IOException i) {
i.printStackTrace();
} catch (ClassNotFoundException c){
c.printStackTrace();
} finally {
if(ois!=null) {
try {
ois.close();
} catch (IOException i) {
i.printStackTrace();
}
}
}
}
} class Student implements Serializable {
static final long serialVersionUID = 123456l;
String name = "hello";
public int age;
Student(String name,int age){
this.name = name;
this.age = age;
} public String getName() {
return name;
} public int getAge() {
return age;
} public String toString(){
return "{name="+name+",age="+age+"}";
}
}

3.PrintStream/PrintWriter

首先看一个容易出现疑惑的现象。

import java.io.*;

public class DataStream {

    public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("d:/temp/b.txt");
fos.write(97);
fos.write(353);
fos.close(); FileInputStream fis = new FileInputStream("d:/temp/b.txt");
byte[] buf = new byte[10];
int len = 0;
while((len=fis.read(buf))!=-1) {
String str = new String(buf);
System.out.println(str);
}
fis.close();
}
}

上面的代码中,向文件中写入的是数值97和353,但无论是用记事本解析还是从这里读取到的结果都是"aa"共两个字节的字母。为什么会如此?

在write(Int i)方法写入数据时,它会将最低位字节写入,而忽略前三个字节。例如97的二进制码为"00000000 00000000 00000000 01100001",忽略前三个字节,写入到文件中的二进制数据就只剩下"01100001",而这被读取或被解析时正好解析为字母a。同理353,它的二进制数据为"00000000 00000000 00000001 01100001",虽然第三个字节最后一位为1,但它还是被忽略,导致写入到文件中的二进制数据仍然为"01100001",解析后就是字母a。

要避免这种问题,可以使用字节打印流PrintStream或字符打印流PrintWriter,它会将数据按照字面展现形式输出。例如下面的例子中,将会向文件中分别写入"a97"。

FileOutputStream fos = new FileOutputStream("d:/temp/a.txt");
PrintStream ps = new PrintStream(fos);
//PrintStream ps = new PrintStream("d:/temp/a.txt")
ps.write(97); //它调用的其实还是fos.write(),所以仍然存储字母a
ps.print(97); //存储字面符号97
ps.close();

使用println()可以换行,使用printf()可以以C语言的打印格式输出。

另外,在PrintWriter中(不包括PrintStream),有一个自动更新autoFlush的概念,它表示每输出一次换行符就自动flush一次。但注意,PrintWriter的自动刷新只对println()和printf()方法有效,对print()无效。之所以不包括PrintStream,是因为PrintWriter因为字符集处理的原因在输出的时候涉及了一个额外的缓冲区,自动刷新就是将此缓冲区的数据flush,而PrintStream则没有这个额外的缓冲区,因此它是实时输出的。

4.DataInputStream/DataOutputStream

还是前面的问题,如何将97作为int数据类型保存到文件中(即将4个字节的97存到文件中)。也就是保证数据的数据类型不变。使用DataInputStream/DataOutputStream即可。

DataOutputStream dos = new DataOutputStream(new FileOutputStream("d:/temp/a.txt"));
dos.writeInt(97); DataInputStream dis = new DataInputStream(new FileInputStream("d:/temp/a.txt"));
System.out.println(dis.readInt());

这样将会把97的4个字节存储到文件中,如果使用记事本去解析,得到的结果会是" a",共4个字节,虽然得到的结果是a,但这只是用记事本解析的而已。使用上面的readInt()读取的结果则是正确的。

注:若您觉得这篇文章还不错请点击右下角推荐,您的支持能激发作者更大的写作热情,非常感谢!

java IO(六):额外功能处理流的更多相关文章

  1. Java IO(Properties/对象序列化/打印流/commons-io)

    Java IO(Properties/对象序列化/打印流/commons-io) Properties Properties 类表示了一个持久的属性集.Properties 可保存在流中或从流中加载. ...

  2. 系统学习 Java IO (六)----管道流 PipedInputStream/PipedOutputStream

    目录:系统学习 Java IO---- 目录,概览 PipedInputStream 类使得可以作为字节流读取管道的内容. 管道是同一 JVM 内的线程之间的通信通道. 使用两个已连接的管道流时,要为 ...

  3. Java IO详解(二)------流的分类

    一.根据流向分为输入流和输出流: 注意输入流和输出流是相对于程序而言的. 输出:把程序(内存)中的内容输出到磁盘.光盘等存储设备中      输入:读取外部数据(磁盘.光盘等存储设备的数据)到程序(内 ...

  4. Java IO(四)——字符流

    一.字符流 字节流提供了处理任何类型输入/输出操作的功能(因为对于计算机而言,一切都是0和1,只需把数据以字节形式表示就够了),但它们不可以直接操作Unicode字符,因为一个Unicode字符占用2 ...

  5. Java基础(二十七)Java IO(4)字符流(Character Stream)

    字符流用于处理字符数据的读取和写入,它以字符为单位. 一.Reader类与Writer类 1.Reader类是所有字符输入流的父类,它定义了操作字符输入流的各种方法. 2.Writer类是所有字符输出 ...

  6. java.IO输入输出流:过滤流:buffer流和data流

    java.io使用了适配器模式装饰模式等设计模式来解决字符流的套接和输入输出问题. 字节流只能一次处理一个字节,为了更方便的操作数据,便加入了套接流. 问题引入:缓冲流为什么比普通的文件字节流效率高? ...

  7. java IO之字节流和字符流-Reader和Writer以及实现文件复制拷贝

    接上一篇的字节流,以下主要介绍字符流.字符流和字节流的差别以及文件复制拷贝.在程序中一个字符等于两个字节.而一个汉字占俩个字节(一般有限面试会问:一个char是否能存下一个汉字,答案当然是能了,一个c ...

  8. Java IO(五)——字符流进阶及BufferedWriter、BufferedReader

    一.字符流和字节流的区别 拿一下上一篇文章的例子: package com.demo.io; import java.io.File; import java.io.FileReader; impor ...

  9. JAVA IO操作:数据操作流:DataOutputStream和DataInputStream

    掌握DataOutputStream和DataInputStream的作用. 可以使用DataOutputStream和DataInputStream写入和读取数据. 在IO包中提供了两个与平台无关的 ...

随机推荐

  1. 【知了堂学习笔记】/JavaScript对象--/暖妮

    JavaScript对象 1.什么是JavaScript对象? JavaScript 中的所有事物都是对象:字符串.数字.数组.日期,等等. 在 JavaScript 中,对象是拥有属性和方法的数据. ...

  2. SQLServer 创建服务器和数据库级别审计

    概述 在上一篇文章中已经介绍了审计的概念:本篇文章主要介绍如何创建审计,以及该收集哪些审核规范. 一.常用的审核对象 1.1.服务器审核对象 1.FAILED_LOGIN_GROUP( Audit L ...

  3. random seed()函数

    用seed()生成随机数字,生成的法则与seed内部的数字相关,如果数字相同,则生成的随机数是相同的. 刷题宝上面的题目: >>> import random >>> ...

  4. 【三十】php之PDO抽象层

    1.PDO介绍(php data object) PHP 数据对象 (PDO) 扩展为PHP访问数据库定义了一个轻量级的一致接口. PDO 提供了一个数据访问抽象层,这意味着,不管使用哪种数据库,都可 ...

  5. C#并发编程实例讲解-概述(01)

    在工作中经常遇到需要并发编程的实例,一直没有时间来整理,现在空了下来,个人整理对并发一下理解. 关于并发编程的几个误解 误解一:并发编程就是多线程 实际上多线只是并发编程的一中形式,在C#中还有很多更 ...

  6. mongodb 聚合查询

    操作符介绍: $project:包含.排除.重命名和显示字段 $match:查询,需要同find()一样的参数 $limit:限制结果数量 $skip:忽略结果的数量 $sort:按照给定的字段排序结 ...

  7. 线性表的顺序存储结构的实现及其应用(C/C++实现)

    存档--- #include <stdio.h> #include <stdlib.h> typedef int ElemType; #define MAXSIZE 10 #i ...

  8. codechef [snackdown2017 Onsite Final] AND Graph

    传送门 题解给出了一个很强势的dp: i<K $$dp[i][len]*Fib[len+2-(t[i]==1)] -> dp[i+1][len]$$ $$dp[i][len]*Fib[le ...

  9. Parade(单调队列优化dp)

    题目连接:http://acm.hdu.edu.cn/showproblem.php?pid=2490 Parade Time Limit: 4000/2000 MS (Java/Others)    ...

  10. c#中winform窗口的隐藏与显示

    最近在做一个C# 的winform客户端程序,要实现在打开新的窗口时将原来打开的窗口关闭,但是想在关闭新打开的窗口是将原来的那个窗口再次打开,在网上查找各种资料,找了很多代码,都是通过窗口.Hide( ...