*/

.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. Js默认参数(多参数情况)

    js function example(settings) { var defaultSetting = { name: '小红', age: '30', sex: '女', phone: '1008 ...

  2. Python3基础1

    Python介绍及特点 发展史 Python 2 or 3? 安装Python3 Hello World程序 变量 用户输入 模块初识 .pyc是个什么? 数据类型初识 数据运算 表达式if ...e ...

  3. PIL遇到问题解决

    PIL 全称:Pillow 在使用PIL4.2.1版本读取jpeg文件时,报cannot identify image file,去github源查找原因:https://github.com/pyt ...

  4. web前端性能优化总结

    网站的划分一般为二:前端和后台.我们可以理解成后台是用来实现网站的功能的,比如:实现用户注册,用户能够为文章发表评论等等.而前端呢?其实应该是属于功能的表现.并且影响用户访问体验的绝大部分来自前端页面 ...

  5. IntelliJ IDEA 17和Maven构建javaWeb项目

    前言 电脑又断电了,眼看着写好的东西就没有了,这是第二次犯这个错误了.很难受呀!还是回到正题吧,我们来使用IDEA和Maven构建一个JavaWeb项目 软件环境: IDEA:2017.2.1 JDK ...

  6. 从好用到更好用 —— 2017 年又拍云 CDN 功能更新回顾

    又拍云一直致力于为客户带来更好的服务,在 2017 年又拍云 CDN 服务进行了数次重大更新,在功能上更加全面.完善,进一步提升了 CDN 的稳定性与安全性. 在过去一年里又拍云 CDN 服务共进行了 ...

  7. hdu_1010_Tempter of the Bone_dfs

    题意:给出一个地图,起点和终点,四通路(上下左右),问在一定的时间内可以走出这个地图吗 题解:首先这个题意一定要好好读,很容易读错题,理解成最短路径小于给定时间就可以出去,其实是不可以的,必须要在给定 ...

  8. 最长上升子序列(LIS) dp学习~3

    题目连接:http://acm.hdu.edu.cn/showproblem.php?pid=1087 Super Jumping! Jumping! Jumping! Time Limit: 200 ...

  9. dijk

    .....................用矩阵存..................... 1 int mp[N][N]; bool p[N]; int dist[N]; void dijk(int ...

  10. SQL强化(三) 自定义函数

    ---恢复内容开始--- Oracle中我们可以通过自定义函数去做一些逻辑判断,这样可以减少查询语句,提高开发效率 create  -- 创建自定义函数 or replace -- 有同名函数就替换, ...