java IO(六):额外功能处理流
*/
.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)的基础上提供额外的功能。常见的额外功能可归纳为以下几种。
- 是否使用缓冲功能:BufferedInputStream/BufferedOutputStream/BufferedReader/BufferedWriter(字符流的缓冲对象还提供了操作行的方法)
- 是否串联多个字节输入流:SequenceInputStream
- 是否使用对象序列化功能:ObjectInputStream/ObjectOutputStream(涉及序列化接口Serializable)
- 是否让流保证数据类型不变:DataInputStream/DataOutputStream
- 是否让输出流输出时保证输出字面符号:PrintStream/PrintWriter(打印流)
- 是否要操作内存中的字符串和数组: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()); //多态
看上去序列化和反序列化是一件很简单的事情。确实如此,但其有不少知识点和需要注意的关键点。
- 什么是序列化?
从前面的例子中可以看出,序列化的方式非常简单,只需实现Serializable接口即可。它不提供任何方法。序列化的意义仅仅只是为类进行一种特殊的标识,即所谓的"盖戳"。就像夫妻如何证明他们是夫妻,颁发一个结婚证即可,再例如猪肉凭什么是合格的?给它贴一张合格标签即可。 - 序列化的目的是什么?
为了将某些对象持久化保存起来,供以后反序列化的时候读取。 - 序列化后的对象在存储时会存储哪些数据?
存储的内容包括:类名和类签名(类的序列化版本号SerialVersionUID)、对象的字段值和数组值,以及从初始对象中引用的其他所有对象的闭包。大致可以看作是存储了一个类的版本号、类名、某些字段的值以及引用的对象。当然,并非所有字段值都会存储,见下文的第6点。 - 对某个对象序列化后,修改对象的属性(例如将成员变量的修饰符从public改为private),反序列化时将会如何?
因为保存起来的序列化数据带有一个类签名SerialVersionUID,而修改类的定义后,编译时这个类会生成Class文件,而这个文件中的版本号将不再和之前序列化时保存的版本号相同。于是抛出异常。java.io.InvalidClassException: Student; local class incompatible: stream classdesc serialVersionUID = -9151998530267376490, local class serialVersionUID = -3521625297801190192
由此可以明确一点:class文件中仅只存储类的定义语句,在new对象时将在堆内存中开辟一段空间并存储对象数据(如成员变量)。反序列化实际上是将保存起来的类对象数据加载到这个开辟出来的对象空间中。
- 如何保证反序列化的成功?
强烈建议显式在实现了Serializable接口的类中,声明一个固定的序列化。如:public/private/... static final long serialVersionUID = 123456L;
这样一来,无论是序列化保存时,还是后来修改了类定义生成的class文件中,其版本号都是固定且相同的,也就是说不会因为序列化版本号不同而反序列化失败。
- 类中所有属性都应该序列化吗?
显然不是。有两类数据不会被保存:静态变量(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(六):额外功能处理流的更多相关文章
- Java IO(Properties/对象序列化/打印流/commons-io)
Java IO(Properties/对象序列化/打印流/commons-io) Properties Properties 类表示了一个持久的属性集.Properties 可保存在流中或从流中加载. ...
- 系统学习 Java IO (六)----管道流 PipedInputStream/PipedOutputStream
目录:系统学习 Java IO---- 目录,概览 PipedInputStream 类使得可以作为字节流读取管道的内容. 管道是同一 JVM 内的线程之间的通信通道. 使用两个已连接的管道流时,要为 ...
- Java IO详解(二)------流的分类
一.根据流向分为输入流和输出流: 注意输入流和输出流是相对于程序而言的. 输出:把程序(内存)中的内容输出到磁盘.光盘等存储设备中 输入:读取外部数据(磁盘.光盘等存储设备的数据)到程序(内 ...
- Java IO(四)——字符流
一.字符流 字节流提供了处理任何类型输入/输出操作的功能(因为对于计算机而言,一切都是0和1,只需把数据以字节形式表示就够了),但它们不可以直接操作Unicode字符,因为一个Unicode字符占用2 ...
- Java基础(二十七)Java IO(4)字符流(Character Stream)
字符流用于处理字符数据的读取和写入,它以字符为单位. 一.Reader类与Writer类 1.Reader类是所有字符输入流的父类,它定义了操作字符输入流的各种方法. 2.Writer类是所有字符输出 ...
- java.IO输入输出流:过滤流:buffer流和data流
java.io使用了适配器模式装饰模式等设计模式来解决字符流的套接和输入输出问题. 字节流只能一次处理一个字节,为了更方便的操作数据,便加入了套接流. 问题引入:缓冲流为什么比普通的文件字节流效率高? ...
- java IO之字节流和字符流-Reader和Writer以及实现文件复制拷贝
接上一篇的字节流,以下主要介绍字符流.字符流和字节流的差别以及文件复制拷贝.在程序中一个字符等于两个字节.而一个汉字占俩个字节(一般有限面试会问:一个char是否能存下一个汉字,答案当然是能了,一个c ...
- Java IO(五)——字符流进阶及BufferedWriter、BufferedReader
一.字符流和字节流的区别 拿一下上一篇文章的例子: package com.demo.io; import java.io.File; import java.io.FileReader; impor ...
- JAVA IO操作:数据操作流:DataOutputStream和DataInputStream
掌握DataOutputStream和DataInputStream的作用. 可以使用DataOutputStream和DataInputStream写入和读取数据. 在IO包中提供了两个与平台无关的 ...
随机推荐
- CSS选择器:伪类(图文详解)
本文最初发表于博客园,并在GitHub上持续更新前端的系列文章.欢迎在GitHub上关注我,一起入门和进阶前端. 以下是正文. 伪类(伪类选择器) 伪类:同一个标签,根据其不同的种状态,有不同的样式. ...
- css的学习笔记
CSS3有哪些新特性? 1. CSS3实现圆角(border-radius),阴影(box-shadow), 2. 对文字加特效(text-shadow.),线性渐变(gradient),旋转(tra ...
- [笔记]Linux命令行大全
date 显示当前时间和日期 cal 显示当前月份的日历 df 查看磁盘剩余空间的数量 free 显示空闲内存的数量 pwd 打印当前工作目录 cd 切换目录 ls 列出文件夹内容 绝对路径:开始于根 ...
- webp图像批量转换软件推荐——XnConvert
XnConvert是一款简单易用的批量图像格式转换软件,其所支持图片格式有JPG.PNG.TIFF.GIF.RAW.JPEG2000.WebP.OpenEXR等等.你可以轻松的实现图像格式的转换.缩放 ...
- 使用JSON JavaScriptSerializer 进行序列化或反序列化时出错。字符串的长度超过了为 maxJsonLength属性
"/"应用程序中的服务器错误.使用 JSON JavaScriptSerializer 进行序列化或反序列化时出错.字符串的长度超过了为 maxJsonLength 属性设置的值. ...
- C语言实现整数和16进制互相转换
编译环境:Dev-C++ 5.2.0.3 使用sprintf()函数实现转换,代码如下: #include <stdio.h> #include <stdlib.h> int ...
- sublime汉化教程
转自: http://www.cnblogs.com/marsggbo/p/6622960.html 如何给sublime text3安装汉化包?so easy 哦 这是我本人亲身测试过的,肯定有效, ...
- java web开发 高并发处理
转自:http://blog.csdn.net/zhangzeyuaaa/article/details/44542161 java处理高并发高负载类网站中数据库的设计方法(java教程,java处理 ...
- 在Ubuntu虚拟机搭建数据库系统
连接数据库: mysql -uroot -p 输入数据库密码即可登陆. 查看mysql版本信息: mysql> select version(); +---------------------- ...
- 快速搭建appium自动测试环境
首先申明本文是基本于Python与Android来快速搭建Appium自动化测试环境: 主要分为以下几个步骤: 前提条件: 1)安装与配置python环境,打开 Python官网,找到"Do ...