stream流去除对象的值_I/O流(过滤流、对象序列化、字符流)

过滤流基础

Data Stream

首先我们来介绍一对过滤流:DataInputStream 和 DataOutputStream。这两个类有什么作用呢?首先来思考下面的需求:假设,要把一个

double 类型的数据写入文件中(例如 3.14),应当怎么做呢?由于FileOutputStream 没有一个接受 double

类型作为参数的 write

方法,因此必须要想别的方法。一个 double 变量占据 8

个字节,因此写一个 double

类型的时候,比较直观的方法应当是把这个 double 类型的数拆分成 8

个字节,然后把这 8

个字节写入到流中去。当然,把 double类型进行拆分是一件比较麻烦的事情。

幸运的是,拆分 double

这件事情不需要程序员自己去做,而可以使用 Java 中现成的类。Sun

公司提供了 DataInputStream 以及 DataOutputStream 这两个类,这两个类为流增强的功能就是:增强了读写八种基本类型和字符串的功能。我们可以看一下 DataOutputStream 的方法:除了有 OutputStream 中有的几个 write

方法之外,还有 writeBoolean,

writeByte, writeShort ... 等一系列方法,这些方法接受某一种基本类型,把基本类型写入到流中。需要注意的是,有一个 writeInt(int n)方法,这个方法接受一个

int 类型的参数。这个方法和 write(int

v)方法不同。writeInt 方法是 DataOutputStream 特有的方法,这个方法一次写入参数 n 的四个字节。而 write

方法则一次写入参数 v

的最后一个字节。与之对应的,DataInputStream 的方法中,除了有几个 read

方法之外,还有 readBoolean,readByte,readInt 等一系列方法,这些方法能够读入若干个字节,然后拼成所需要的数据。例如 readDouble 方法,就会一次读入 8

个字节,然后把这 8

个字节拼接成一个 double

类型。

最后要提示的是,DataXXXStream 中有 readUTF

和 writeUTF

这两个方法用来读写字符

串,但是一般来说,我们读写字符串的时候几乎不使用 Data 流。Data

流主要是用在 8

种基本类型的读写上。

下面,我们来写一个程序,在一个文件中存入3.14 这个 double

值,然后再从文件中把这个值读取出来。在写代码之前,我们先来研究一下过滤流的使用。

过滤流使用的基本步骤

首先,过滤流的构造方法中,一般都会有一个构造方法,接受其他类型的流。例如,在

DataInputStream 的构造方法中,唯一的构造方法如下:

DataInputStream(InputStream is)

而 DataOutputStream 类唯一的构造方法如下:

DataOutputStream(OutputStream os)

这个参数表示什么呢?可以这么来理解:过滤流使用来为其他流增强功能的,而构造方

法中的这个参数,表明的是过滤流为哪一个流增强功能。

过滤流的使用分为下面四个步骤:

1、创建节点流。这个步骤是使用过滤流的先决条件,由于过滤流无法直接实现数据传

输功能,因此必须先有一个节点流,才能够进行数据传输。

2、封装过滤流。所谓的“封装”,指的是创建过滤流的时候,必须以其他的流作为构造

方法的参数。需要注意的是,可以为一个节点流封装多个过滤流。

3、读/写数据。

4、关闭外层流。这指的是,关闭流的时候,只需要关闭最外层的过滤流即可,内层流

会随着外层流的关闭而一起被关闭。

我们结合上面对过滤流的使用,给出下面的代码:

import

java.io.*;

public

class TestDataStream{

public static void main(String args[]) throws

Exception{

//创建节点流

FileOutputStream fout = new

FileOutputStream("pi.dat");

//封装过滤流

DataOutputStream dout = new

DataOutputStream(fout);

//写数据

dout.writeDouble(3.14);

//关闭外层流

dout.close();

//创建节点流

FileInputStream fin= new

FileInputStream ("pi.dat");

//封装过滤流

DataInputStream din = new

DataInputStream(fin);

//读数据

double pi =

din.readDouble();

//关闭外层流

din.close();

System.out.println(pi);

}

}

需要注意的是,由于 Data

流保存八种基本类型的方式采用的是拆分字节的方式,而不

是采用文本的方式,因此保存的 pi.dat 这个文件无法用文本编辑器直接进行编辑。

Buffered

Stream

下面要介绍的是一对流:BufferedInputStream 和 BufferedOutputStream。这两个流

增强了缓冲区的功能。

什么叫缓冲区呢?在之前的代码中,我们每调用一次 read 或者 write

方法,都会触发一次 I/O

操作。而由于 I/O

操作要跨越 JVM

的边界,因此进行 I/O

操作的时候,事实上效率会非常低,这非常不利于程序的高效。为了让程序的效率得到提升,我们引入了缓冲机制。我们会在内存中开辟一块空间,当调用

read 或者 write

方法时,并不真正进行 I/O

操作,而是对内存中的这块空间进行操作。我们以 write 操作为例,使用了缓冲机制之后,我们调用 write 方法时,并不真正把数据写入到文件中,而是先把数据放到缓冲区里。等到缓冲区满了之后,再一次性把数据写入文件中。为什么这样就能提高效率了呢?考虑下面这个生活中的例子。在大学里面,我们都用手洗衣服。手洗衣物有一个很大的问题,往往衣服洗完以后很难拧干,晾在阳台上之后会滴水。如果任由衣物在阳台上滴水的话,有可能会让阳台变得很脏,如果阳台上放了一些其他的杂物的话,更有可能因为滴水而损坏那些物品。因此,我们需要使用一个方式,把衣服上滴的水运输到洗手间,倒掉。

那怎么运输呢?一般来说,我们不会在阳台傻等,等一滴水下来以后马上就用手接着然

后跑到洗手间倒掉。我们往往会用一个盆先接水,当这个盆满了以后,我们才会真正把这个

盆中的水倒掉,也就是真正完成 I/O 操作。这个盆,就好比是我们的缓冲区。通过这个盆,

我们减少了 I/O

的次数,从而提高了 I/O

的效率。

Buffered 流几乎没有为流增加新的方法,我们给出一个输出流的例子代码:

import

java.io.*;

public

class TestBufferedStream{

public static void main(String args[]) throws

Exception{

String data = "Hello

World";

byte[] bs =

data.getBytes();

//创建节点流

FileOutputStream

fout

= new

FileOutputStream("test.txt");

//封装过滤流

BufferedOutputStream

bout

= new

BufferedOutputStream(fout);

//写数据

bout.write(bs);

//关闭外层流

bout.close();

}

}

需要注意的是,如果把bout.close()方法去掉,此时在看

test.txt 文件,会发现文件的内容为空。这是因为,我们在调用 write 方法的时候,其实并没有真正把数据写入到文件中,而只是把数据写入到缓冲区中。那什么时候缓冲区中的数据会真正写入到文件中呢?有三种情况:第一种情况是缓冲区已满,第二种情况是调用

close 方法。除了这两种情况之外,假设程序员希望在缓冲区没有满并且不关闭流的情况下,把缓冲区内的东西真正写入流中,应当调用一个方法:flush()。这个方法用来清空缓冲区,往往用在输出流上面。当一个带缓冲的输出流调用

flush()之后,就能保证之前在缓冲区中的内容真正进行了

I/O 操作,而不是仅仅停留在缓冲区。

PrintStream

PrintStream 是一个比较特殊的过滤流,简单介绍一下。PrintStream 作为过滤流,增强的功能有以下几个:

1、缓冲区的功能

2、写八种基本类型和字符串

3、写对象

需要注意的是,这个流写基本类型和写对象的时候,是按照字符串的方式写的。也就是

说,这个流写八种基本类型的时候,会把基本类型转换成字符串以后再写,而写对象的时候,

会写入对象的 toString()方法返回值。

这个类具体如何使用不多介绍了。需要介绍的是这个类的一个对象:我们所熟知的

向屏幕输出数据的对象:System.out 对象,这就是一个 PrintStream 类型的对象。

对象序列化

序列化的概念

下面介绍另外一对过滤流:ObjectInputStream 和 ObjectOutputStream。这两

个也是过滤流,增强的功能如下:

1、

增强了缓冲区功能

2、

增强了读写八种基本类型和字符串的功能。读写基本类型和字符串的方式,与 Data

流完全一样。

3、

增强了读写对象的功能。这是这两个流最主要的作用。在 ObjectInputStream 类有一个 readObject

方法,这个方法能够从流中读取一个对象;而 ObjectOutputStream 类中有一个writeObject

方法,这个方法能够向流中写入一个对象。

如上所述,ObjectInputStream 和 ObjectOutputStream 能够完成对对象的读写。这种把对象放到流上进行传输的过程,称之为“对象序列化”。一个对象如果能够放到流上进行传输,则我们称这个对象是“可序列化”的。

Serializable

接口和 transient

关键字

需要注意的是,并不是所有对象都是“可序列化”的。举个例子说,搬家就是一个传输

对象的过程。然而,搬家的时候并不是所有对象都能够搬走的。例如,家具、电器,这些对

象往往在搬家的时候是能够搬走的,但是,门、窗户、地板,这些对象无法搬走。那我们可

以说家具、电器是可序列化的对象,而窗户、地板是不可序列化的对象。

那怎么让对象能够在流上进行传输呢?如果要让一个类成为可序列化的,只要让这个类

实现一个接口:java.io.Serializable 接口即可。

要实现这个接口,就要实现这个接口中的所有方法。好了,现在请去查一下 Serializable

接口,看看这个接口中定义了哪些方法?

但是,这个接口中没有任何的方法。也就是说,如果要实现这个 Serializable 接口,只需要写上 implements

Serializable 就可以了。

我们可以尝试一下。定义一个 Student 类,创建两个对象并保存到文件中,然后再利用另一个流读取文件。代码如下:

import

java.io.*;

class

Student implements Serializable{

String name;

int age;

public Student(String name, int age) {

this.name = name;

this.age = age;

}

}

public

class TestSerializable {

public static void main(String[] args) throws

Exception {

Student stu1 = new

Student("tom", 18);

Student stu2 = new

Student("jerry", 18);

FileOutputStream fout = new

FileOutputStream("stu.dat");

ObjectOutputStream oout = new

ObjectOutputStream(fout);

oout.writeObject(stu1);

oout.writeObject(stu2);

oout.close();

FileInputStream fin = new

FileInputStream("stu.dat");

ObjectInputStream oin = new

ObjectInputStream(fin);

Student s1 = (Student)

oin.readObject();

Student s2 = (Student)

oin.readObject();

oin.close();

System.out.println(s1.name + "

" + s1.age);

System.out.println(s2.name + "

" + s2.age);

}

}

需要注意的是,由于 readObject 方法返回值为 Object

类型,因此需要对返回值进行强转。

运行结果如下:

同时,也产生了一个 stu.dat

文件。这个文件中保存的都是一些二进制数据,这些二进

制数据就是保存对象时保存的数据。

下面,介绍一下一个新的关键字:transient。这个关键字是一个修饰符,这个修饰

符可以用来修饰属性,用 transient 修饰的属性表示:这个属性不参与序列化。

我们可以修改原有的代码,把 Student 类的 age

属性修改为 transient

的。这样之后产生

的改变是:1、stu.dat 文件的大小变小了。这很容易理解,因为由于参与序列化的属性变少

了,因此序列化之后保存的数据也变少了,从而导致文件也变小了。2、输出的

age 属性都

为 0。这是因为由于在

writeObject 的时候没有保存 age

属性,而读取时从文件中也读取不到age 属性,从而导致这个属性的值只能是默认值:0。

修改后的代码以及运行结果如下:

(其他代码与上一个例子相同)

class

Student implements Serializable{

String name;

transient int age;

public Student(String name, int age) {

this.name = name;

this.age = age;

}

}

运行结果:

另外,在使用对象序列化的时候,注意这样两个问题:

1、不要使用追加的方式写对象。也就是说,如果我们创建一个文件输出流采用FileOutputStream(file, true)的方式创建节点流,然后再在外面封装ObjectOutputStream,这样将无法完成我们设想的结果。如果对一个文件多次写入的话,读取对象的时候只能读取第一次写入的对象,而后面用追加的方式写入的对象将无法被读取。这是对象序列化底层机制所决定的。

2、如果一个对象的属性又是一个对象,则要求这个属性对象也实现了Serializable接口,如果一个对象的属性是一个集合,则要求集合中所有对象都实现

Serializable

接口。除非这

个对象的属性被标记为 transient,不参与序列化。

字符流

研究完字节流之后,开始介绍字符流。要理解字符流,首先要理解字符编码的含义。

字符编码

计算机中显示文字的时候,本质上是在屏幕上绘制一些图像用来显示文字。从这个意义

上说,文字就是一种特殊的图片。

然而,在计算机中保存文字的时候,并不是按照图片的方式保存。当保存文件的时候,计算机底层会把文字转换成数字,然后再进行保存。计算机把字符转换为数字的过程,称之

为“编码”。而读取文件的时候,则过程相反,计算机会把数字转化为文字,并绘制到屏幕

上。计算机把数字转换为字符的过程,称之为“解码”。

很显然,不同的字符必须对应不同的数字,不然,在解码时会遇到问题。

那么,什么字符对应于什么数字呢?有些标准化组织,会规定字符和数字之间的对应关

系,这种对应关系就是所谓的编码规范。常见的编码规范如下:

ASCII : 最早的编码方式,规定了英文字母和英文标点对应的编码

ISO-8859-1 : 这种编码方式包括了所有的西欧字符以及西欧标点。

GB2312/ GBK:大陆广泛使用的简体中文编码。其中,GB2312 是 GBK

的一个子集,也就是说,在 GB2312 中有的汉字,在 GBK

中也有,且同一个汉字在 GB2312 和 GBK

中的

编码相同。GBK

主要是在 GB2312

的基础上扩展了很多新的字符。

GBK 是简体中文 Windows

的默认编码方式。

Big5 : 台湾地区广泛使用的繁体中文编码。

UTF-8 :一种国际通用编码,包括简体和繁体中文。与 GB2312/GBK 不兼容,也就是

说,同一个汉字,在 GBK

和 UTF-8

的编码是不同的。大部分简体中文 Linux 使用的是 UTF-8编码。

由于有了多种编码规范,因此就会有乱码的问题。乱码问题是怎么产生的呢。例如,我

们在保存文件的时候,使用了 GBK

编码,这个时候,假设一个字符“程”,被编码成了数

字 31243,于是这个文件在底层保存的数据就是

31243。之后,这个文件被传送到了台湾,

台湾地区的工程师打开文件的时候,使用的软件对这个文件采用 Big5 解码。此时,就会把

31243 这个数字给解码成字符“最”。这样,就会产生理解上的误会,从而产生乱码。

换而言之,产生乱码的根源在于:编解码方式不一致。

我们可以用程序来演示编解码。String 类有一个方法 getBytes(),这个方法能够把字符串转换成一个

byte 类型的数组,实际上就是把字符转化为数字的过程,本质上,就是在进行编码。在调用 getBytes 的时候,也可以指定编码方式。那得到 byte 数组之后,如何解码呢?String

类有一个构造方法,能够接受 byte 数组作为参数,这就是能够把数字转化为字符串,本质上是解码的过程。在构造的时候,也可以指定解码的方式。

示例代码如下:

public

class TestEncoder {

public static void main(String[] args) throws

Exception {

String str =

"欢迎学习

Java";

//编码,指定编码方式为

GBK

byte[] bs =

str.getBytes("GBK");

//解码,指定解码方式为

GBK

String str2 = new String(bs,

"GBK");

System.out.println(str2);

}

}

上面的程序中,编码和解码的方式都为 GBK,输出结果为:

可以看到,没有乱码。

如果修改一下,编码用 GBK,解码用

Big5,则输出结果如下:

可以看到,产生了乱码。同样的,如果编码用 GBK,而解码用

UTF-8,则输出结果如下:

同样产生了乱码。

需要注意的是,英文字母(例如上面字符串中的“Java”),无论采用什么方式编码和解码,都不会产生乱码。世界上任何一种编码方式,都与

ASCII 编码兼容,也就是说,任何

一种编码方式下面,A

都对应 65,a 都对应 97,没有例外。

获得字符流与桥转换

由于编码方式的不一致,导致了传输文本的时候会有一些比较棘手的问题。为了让传输文本文件更加方便,我们使用字符流。首先是字符流的父类。所有输入字符流的父类是

Reader,所有输出字符流的父类是Writer。与

InputStream 和 OutputStream

类似,这两个类也是抽象类。此外,与 FileInputStream 以及 FileOutputStream 类似,有两个类 FileReader 和 FileWriter,这两个类分别表示文件输入字符流和文件输出字符流。这两个流的使用与

FileInputStream

以及 FileOutputStream 也非常雷同,在此不多介绍,需要注意的是,使用这两个流的时候,无法指定编解码方式。

通过 FileReader

和 FileWriter

可以直接获得文件字符流。

下面,介绍两个流:InputStreamReader 和 OutputStreamWriter。InputStreamReader 这个类本身是 Reader

类的子类,因此这个类的对象是一个字符流。而这个流的构造方法如下:

可以看到,这个流所有构造方法,都可以接受一个 InputStream 类型的参数。也就是说,通过这个流,可以接受一个字节流作为参数,创建一个字符流。这个对象就起到了字节流向字符流转换的功能,我们往往称之为:桥转换。

类似的,OutputStreamWriter 类能够把一个输出字节流转换为一个输出字符流。在桥转换的过程中,我们还可以指定编解码方式。如果不指定的话,则编码方式采用系统默认的编码方式。因此,通过桥转换获得字符流,也是一个获得字符流的方式。这种方式有两个用法:

1、如果需要指定编码方式,则应当使用桥转换。

2、在无法直接获得字符流的情况下,可以先获得字节流,再通过桥转换获得字符流。

利用桥转换进行编程,需要以下五个步骤:

1、创建节点流

2、桥转换为字符流

3、在字符流的基础上封装过滤流

4、读/写数据

5、关闭外层流

字符过滤流

介绍完如何获得字符流之后,下面单刀直入,介绍一些字符流的过滤流。对于字符流来

说,常用的过滤流只有两个:读入使用 BufferedReader,写出使用

PrintWriter。下面我们分

别进行介绍。

BufferedReader

顾名思义,BufferedReader 提供了缓冲区功能。但是更重要的是,BufferedReader 中有

一个 readLine()方法,签名如下:

public String readLine()

这个方法也很容易理解:每次读入一行文本,并把读入的这一行文本当做返回值返回。

当读到流末尾时,返回一个 null

值。

下面给出一个示例代码,介绍BufferedReader 的使用。首先在当前目录下准备一个文本文件,文本文件中保存一段文字。对于 Windows 来说,默认的编码为 GBK。如下:

然后有如下代码:

import

java.io.*;

public

class TestPoem {

public static void main(String[] args) throws

Exception {

//创建节点流

FileInputStream fin = new

FileInputStream("poem.txt");

//桥转换

Reader r = new

InputStreamReader(fin, "GBK");

//封装过滤流

BufferedReader br = new

BufferedReader(r);

//读/写数据

String line = null;

while( (line=br.readLine())

!=null){

System.out.println(line);

}

//关闭外层流

br.close();

}

}

上面的代码演示了如何使用 BufferedReader 读取文件。

PrintWriter

PrintWriter 是一个很特殊的类。

首先,PrintWriter

可以作为一个过滤流。这个流可以接受一个 Writer 作为参数。增强了如下一些功能:

1、缓冲区的功能。因此使用PrintWriter 应当及时关闭或刷新

2、写八种基本类型和字符串的功能。

3、写对象的功能。

在 PrintWriter

类中,有一系列 print

方法,这些方法能够接受八种基本类型、字符串和对象。同样的,还有一系列 println 方法,这些方法在写入数据之后,会在数据后面写入一个换行符。要注意的是,PrintWriter 写基本类型的方式,是把基本类型转换为字符串再写入流中,与 Data 流不同。举例来说,对于 3.14

这个 double

类型的数,Data

流会把这个数拆分成 8个字节写入文件,而

PrintWriter 会把这个数字转化为字符串“3.14”,写入文件中。

此外,PrintWriter

写对象的时候,写入的是对象的 toString()方法返回值,与对象序列化有本质区别。

PrintWriter 除了可以作为过滤流之外,还可以作为节点流。PrintWriter 类的构造方法中,可以直接接受一个文件名或 File 对象作为参数,直接获得一个输出到文件的 PrintWriter。当然,编码方式采用的是系统默认的编码方式。最后,PrintWriter 的构造方法可以接受一个 InputStream,也就是说,可以使用

PrintWriter进行桥转换。只不过使用

PrintWriter 进行桥转换的时候,无法指定编码方式,采用的是系统默认的编码方式。

下面,我们把 PrintWriter 当做过滤流,给出一段代码的例子:

import

java.io.*;

public

class TestPrintWriter {

public static void main(String[] args) throws

Exception {

FileOutputStream

fout

= new

FileOutputStream("poem2.txt");

Writer w = new

OutputStreamWriter(fout, "GBK");

PrintWriter pw = new

PrintWriter(w);

pw.println("一个人在清华园");

pw.println("我写的

Java 程序");

pw.println("是全天下");

pw.println("最面向对象的");

pw.close();

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值