管道流
管道流主要用于连接两个线程的通信。 管道流也分为字节流(PipedInputStream、PipedOutputStream)和字符流(PipedReader、 PipedWriter)。比如一个PipedInputStream必须和一个PipedOutputStream对象进行连接而产生一个通信管 道,PipedOutputStream向管道中写入数据,PipedInputStream从管道中读取数据。管道流的工作如下图所示:
下面看一下管道流的用法。既然管道流的作用是用于线程间的通信,那么势必有发送线程和接收线程,两个线程通过管道流交互数据。首先写一个发送数据的线程:
public class Sender implements Runnable
{private PipedOutputStream out = new PipedOutputStream();
public PipedOutputStream getOutputStream()
{return out;
}
public void run()
{String str = "Receiver, 你好!";
try
{out.write(str.getBytes()); // 向管道流中写入数据(发送)
out.close();
}
catch (IOException e){e.printStackTrace();
}
}
}
用流写数据的时候注意关注一下,该流是否支持直接写String,不可以的话要用String的getBytes()方法获取字符串的字节。既然有一个发送数据的线程了,接下来来一个接收数据的线程:
public class Receiver implements Runnable
{private PipedInputStream in = new PipedInputStream();
public PipedInputStream getInputStream()
{return in;
}
public void run()
{String s = null;
byte b0[] = new byte[1024];
try
{int length = in.read(b0);
if (-1 != length)
{s = new String(b0, 0 , length);
System.out.println("收到了以下信息:" + s);
}
in.close();
} catch (IOException e)
{e.printStackTrace();
}
}
}
两个线程都有了,写一个main线程,利用管道输出流的connect方法连接管道输出流和管道输入流:
public static void main(String[] args)
{try
{Sender sender = new Sender();
Receiver receiver = new Receiver();
Thread senderThread = new Thread(sender);
Thread receiverThread = new Thread(receiver);
PipedOutputStream out = sender.getOutputStream(); // 写入
PipedInputStream in = receiver.getInputStream(); // 读出
out.connect(in);// 将输出发送到输入
senderThread.start();
receiverThread.start();
}
catch (IOException e){e.printStackTrace();
}
}
输出结果应该很明显了,大家都知道,接收线程接收到了来自发送线程通过管道流输出流发送的数据:
收到了以下信息:Receiver, 你好!
注意一下,PipedInputStream运用的是一个1024字节固定大小的循环缓冲区,写入PipedOutputStream的数据实际上保存到了对应的PipedInputStream的内部缓冲区。PipedInputStream执行读操作时,读取的数据实际上来自这个内部缓冲区。如果对应的PipedInputStream输入缓冲区已满,任何企图写入PipedOutputStream的线程都将被阻塞。而且这个写操作线程将一直阻塞,直至出现读取PipedInputStream的操作从缓冲区删除数据。
这意味着,向PipedOutputStream写入数据的线程不应该是负责从对应PipedInputStream读取数据的唯一线程(所以这里开了两个线程分别用于读写)。假定t线程试图一次对PipedOutputStream的write()方法的调用中向对应的PipedOutputStream写入2000字节的数据,在t线程阻塞之前,它最多能够写入1024字节的数据(PipedInputStream内部缓冲区的大小)。然而,一旦t被阻塞,读取PipedInputStream的操作就再也不能出现了,因为t是唯一读取PipedInputStream的线程,这样,t线程已经完全被阻塞。
对象流
ObjectInputStream、ObjectOutputStream这两个类是用于存储和读取对象的输入输出流类。这些对象必须是可以序列化的对象,Java中最简单的做法是实现Serializable接口,序列化是为了保存对象的状态信息。
序列化:将一个对象转换成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。
反序列化:将字节数组重新构造成对象。
1、简单序列化
序列化只需要实现java.io.Serializable接口就可以了。序列化的时候有一个serialVersionUID参数, Java序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的 。在进行反序列化,Java虚拟机会把传过来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的实体类,可以进行反序列化,否则Java虚拟机会拒绝对这个实体类进行反序列化并抛出异常。serialVersionUID有两种生成方式:
1、默认的1L
2、根据类名、接口名、成员方法以及属性等来生成一个64位的Hash字段
如果实现java.io.Serializable接口的实体类没有显式定义一个名为serialVersionUID、类型为long的变量时,Java序列化机制会根据编译的.class文件自动生成一个serialVersionUID,如果.class文件没有变化,那么就算编译再多次,serialVersionUID也不会变化。换言之,Java为用户定义了默认的序列化、反序列化方法,其实就是ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法。
看一个例子:
public class SerializableObject implements Serializable {
private static final long serialVersionUID = 1L;
private String str0;
private transient String str1;
private static String str2 = "abc";
public SerializableObject(String str0, String str1) {
this.str0 = str0;
this.str1 = str1;
}
public String getStr0() {
return str0;
}
public String getStr1() {
return str1;
}
public static void main(String[] args) throws Exception {
File file = new File("D:" + File.separator + "s.txt");
OutputStream os = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(new SerializableObject("str0", "str1"));
oos.close();
InputStream is = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(is);
SerializableObject so = (SerializableObject) ois.readObject();
System.out.println("str0 = " + so.getStr0());
System.out.println("str1 = " + so.getStr1());
ois.close();
}
}
运行一下,打开s.txt文件,文件内容就是序列化的对象信息:
◇AC ED:STREAM_MAGIC序列化协议
◇00 05:STREAM_VERSION序列化协议版本
◇73:TC_OBJECT声明这是一个新的对象
第2部分是要序列化的类的描述,在这里是SerializableObject类
◇72:TC_CLASSDESC声明这里开始一个新的class
◇00 1F:十进制的31,表示class名字的长度是31个字节
◇63 6F 6D ... 65 63 74:表示的是“com.xrq.test.SerializableObject”这一串字符,可以数一下确实是31个字节
◇00 00 00 00 00 00 00 01:SerialVersion,序列化ID,1
◇02:标记号,声明该对象支持序列化
◇00 01:该类所包含的域的个数为1个
第3部分是对象中各个属性项的描述
◇4C:字符"L",表示该属性是一个对象类型而不是一个基本类型
◇00 04:十进制的4,表示属性名的长度
◇73 74 72 30:字符串“str0”,属性名
◇74:TC_STRING,代表一个new String,用String来引用对象
第4部分是该对象父类的信息,如果没有父类就没有这部分。有父类和第2部分差不多
◇00 12:十进制的18,表示父类的长度
◇4C 6A 61 ... 6E 67 3B:“L/java/lang/String;”表示的是父类属性
◇78:TC_ENDBLOCKDATA,对象块结束的标志
◇70:TC_NULL,说明没有其他超类的标志
第5部分输出对象的属性项的实际值,如果属性项是一个对象,这里还将序列化这个对象,规则和第2部分一样
◇00 04:十进制的4,属性的长度
◇73 74 72 30:字符串“str0”,str0的属性值
从以上对于序列化后的二进制文件的解析,我们可以得出以下几个关键的结论:
1、序列化之后保存的是类的信息
2、被声明为transient的属性不会被序列化,这就是transient关键字的作用
3、被声明为static的属性不会被序列化,这个问题可以这么理解,序列化保存的是对象的状态,但是static修饰的变量是属于类的而不是属于变量的,因此序列化的时候不会序列化它
输出结果:
str0 = str0
str1 = null
因为str1是一个transient类型的变量,没有被序列化,因此反序列化出来也是没有任何内容的,显示的null,符合我们的结论。
当对象实现Serializable接口进行自动序列化时,类中某些字段不想被序列化,需要使用transient关键字,虽然Externalizable通过writeExternal()方法也可以实现此功能,但是序列化不是自动进行的,使用Serializable和transient关键字更加方便。
注意:由于Externalizable默认序列化不存储任何字段,所以transient关键字只在Serializable中使用。
2.序列化控制
默认的Serializable序列化是将对象整体序列化,但是对于一些特殊的需求例如:序列化部分对象或者反序列化部分对象的情况,可以使用Externalizable接口来代替Serializable接口,重写Externalizable的writeExternal()和readExternal()方法可以实现对序列化的控制,这两个方法在对象序列化和反序列化时自动调用,例子如下:
class Blip1 implements Externalizable {
private int i;
private String s;
//不加public无参构造函数,在反序列化的时候会报错:
//Exception in thread "main" java.io.InvalidClassException: Blip1; no valid constructor
public Blip1(){
System.out.println("Blip1 Empty Constructor");
}
public Blip1(String x, int a){
System.out.println("Blip1 Constructor");
i = a;
s = x;
}
public String toString(){
return s + i;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Blip1.writeExternal");
out.writeObject(s);
out.writeInt(i);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("Blip1.readExternal");
s = (String)in.readObject();
i = in.readInt();
}
}
class Blip2 implements Externalizable {
Blip2(){
System.out.println("Blip2 Constructor");
}
@Override
public void writeExternal(ObjectOutput out)throws IOException{
System.out.println("Blip2.writeExternal");
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("Blip2.readExternal");
}
}
public class Blips {
public static void main(String[] args)throws IOException, ClassNotFoundException{
System.out.println("Constructing objects:");
Blip1 b1 = new Blip1("Blip1",100);
Blip2 b2 = new Blip2();
//序列化
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Blips.out"));
System.out.println("Saving objects:");
o.writeObject(b1);
o.writeObject(b2);
//反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blips.out"));
System.out.println("Recovering b1:");
b1 = (Blip1)in.readObject();
System.out.println(b1);
//由于Blip2的默认无参数构造方法不是public的,所以会抛异常
//System.out.println("Recovering b2:");
//b2 = (Blip2)in.readObject();
}
}
输出结果:
Constructing objects:
Blip1 Constructor
Blip2 Constructor
Saving objects:
Blip1.writeExternal
Blip2.writeExternal
Recovering b1:
Blip1 Empty Constructor
Blip1.readExternal
Blip1100
注意:
(1)使用Externalizable反序列化时,只会调用默认的public无参构造方法,对象必须要有public类型的无参数构造方法,所以Blip2非public类型无参数构造方法无法反序列化。
总结:
1、当父类继承Serializable接口时,所有子类都可以被序列化
2、子类实现了Serializable接口,父类没有,父类中的属性不能序列化(不报错,数据丢失),但是在子类中属性仍能正确序列化
3、如果序列化的属性是对象,则这个对象也必须实现Serializable接口,否则会报错
4、反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失,但不会报错
5、反序列化时,如果serialVersionUID被修改,则反序列化时会失败