Tinking in Java ---Java的NIO和对象序列化
前面一篇博客的IO被称为经典IO,因为他们大多数都是从Java1.0开始就有了的;然后今天这篇博客是关于NIO的,所以的NIO其实就是JDK从1.4开始,Java提供的一系列改进的输入/输出处理的新功能,这些新功能被统称为新IO(New IO ,简称NIO)。另一个概念对象序列化指的是将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列再转换成原来的对象。这样的话我们就可以将对象写入磁盘中,或者是将对象在网络上进行传递。下面就对这两个内容进行总结。
一.Java的NIO
java的NIO之所以拥有更高的效率,是因为其所使用的结构更接近与操作系统的IO方式:使用通道和缓冲器。通道里面放有数据,但是我们不能直接与它打交道,无论是从通道中取数据还是放数据,我们都必须通过缓冲器进行,更严格的是缓冲器中存放的是最原始的字节数据而不是其它类型。其中Channel类对应我们上面讲的通道,而Buffer类则对应缓冲器,所以我们有必要了解一下这两个类。
(1).Buffer类
从底层的数据结构来看,Buffer像是一个数组,我们可以在其中保存多个相同类型的数据。Buffer类是一个抽象数组,它最常用的子类是ByteBuffer,这个类存取的最小单位是字节,正好用于和Channel打交道。当然除了ByteBuffer外,其它基本类型(除boolean外)都有自己对应的Buffer:CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。使用这些类型我们可以很方便的将基本类型的数据放入ByteBuffer中去.Buffer类还有一个子类MappedByteBuffer,这个子类用于表示Channel将磁盘文件的部分或全部内容得到的结果。
Buffer中有三个概念比较重要:容量(capacity),界限(limit)和位置(positiion)。
容量指的缓冲区的大小,即该Buffer能装入的最大数据量。
界限指的是当前装入的最后一个数据位置加1的那个值,表示的是第一个不应该被读或写的位置。
位置用于表明下一个可以被读出或写入的缓冲区位置索引。
Buffer的主要功能就是装数据,然后输出数据。所以我们有必要了解一下这个具体的过程:首先Buffer的positiion为0,limit等于capacity,程序可以通过put()方法向Buffer中放入一些数据(或者是从Channel中取出一些数据),在这个过程中position会往后移动。当Buffer装入数据结束以后,调用Buffer的flip()方法为输出数据做好准备,这个方法会把limit设为position,将position设为limit。当输出数据结束后,Buffer调用clear()方法,clear()方法不会情况所有的数据,只会把position设为0,将limit设为capacity,这样又为向Buffer中输入数据做好了准备。
另外指的注意的是Buffer的子类是没有构造函数的,所以不能显式的声明一个Buffer。下面的这份代码展示了CharBuffer的基本用法:
package lkl;
import java.nio.*;
public class BufferTest {
public static void main(String[] args){
//通过静态方法创建一个CharBuffer
System.out.println("创建buffer之后: ");
CharBuffer buffer = CharBuffer.allocate(10);
System.out.println("position: "+buffer.position());
System.out.println("limit: "+buffer.limit());
System.out.println("capacity: "+buffer.capacity());
///向buffer里放三个字符
buffer.put("a");
buffer.put("b");
buffer.put("c");
///为使用buffer做准备
System.out.println();
System.out.println("在向buffer中装入数据并调用flip()方法后: ");
buffer.flip();
System.out.println("position: "+buffer.position());
System.out.println("limit: "+buffer.limit());
System.out.println("capacity: "+buffer.capacity());
///读取buffer中的元素,以绝对方式和相对方式两种
//绝对方式不会改变position指针的
//而相对方式每次都会让position指针后移一位
System.out.println(buffer.get());
System.out.println(buffer.get(2));
System.out.println();
System.out.println("调用clear()后: ");
//调用clear()方法,为再次向buffer中输入数据做准备
//但是这个方法只是移动各个指针的位置,而不会清空缓冲区中的数据
buffer.clear();
System.out.println("position: "+buffer.position());
System.out.println("limit: "+buffer.limit());
System.out.println("capacity: "+buffer.capacity());
///clear()方法没有清空缓冲区
//所以还可以通过绝对方式来访问缓冲区里面的内容
System.out.println(buffer.get(2));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
一般都是用的ByteBuffer,所以需要先将数据转成字节数组后在放入,但是对应基本数据类型可以使用ByteBuffer类的asXXXBuffer简化这个过程。如下面的代码所示:
package lkl;
import java.nio.ByteBuffer;
//向Channel中写入基本类型的数据
//向ByteBuffer中插入基本类型数据的最简单的方法就是:利用ascharBuffer()
//asShortBuffer()等获得该缓冲器上的视图,然后调用该视图的put()方法
//short类型需要转一下型,其它基本类型不需要
public class GetData {
public static void main(String[] args){
ByteBuffer buff = ByteBuffer.allocate(1024);
///读取char型数据
buff.asCharBuffer().put("java");
//buff.flip(); //这时候不需要flip()
char c;
while((c=buff.getChar())!=0){
System.out.print(c+" ");
}
System.out.println();
buff.rewind();
//读取short型数据
buff.asShortBuffer().put((short)423174);
System.out.println(buff.getShort());
buff.rewind();
//读取long型数据
buff.asLongBuffer().put(689342343);
System.out.println(buff.getLong());
buff.rewind();
//读取float型数据
buff.asFloatBuffer().put(2793);
System.out.println(buff.getFloat());
buff.rewind();
//读取double型数据
buff.asDoubleBuffer().put(4.223254);
System.out.println(buff.getDouble());
buff.rewind();
}/*Output
j a v a
29958
689342343
2793.0
4.223254
*/
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
当然Buffer类还有其它的很多方法,可以通过它的API文档来进行了解。反正现在我们知道了要想跟Channel打交道,必须要使用Buffer。
(2).Channel类
Channel类对应我们开头说的通道了,注意到Channel类是面向字节流的,所以并不是我们前面学习的所有IO类都可以转换成Channel的。实际上Java为Channel提供了FileChannel,DataGramChannel,selectableChannel,ServerSocketChannel,SocketChannel等实现类。在这里我们只了解FileChannel,它可以通过FileInputStream,FileOutputStream,RandomAccessFile这几个类的getChannel()方法得到;当然这几个类得到的对应的FileChannel对象在功能上也是不同的,FileOutputStream对应的FileChannel只能向文件中写入数据,FileInputStream对应的FileChannel只能向文件中读数据,而RandomAccessFile对应的FileChannel对文件即能读又能写。这也说明这个类也是没有构造器可以调用的。下面的代码演示了如何使用Channel向文件中写数据和读取文件中的数据:
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
///Channel是java提供的新的一种流对象
//Channel可以将文件部分或则全部映射为Channel
///但是我们不能直接与Channel打交道,无论是读写都需要通过Buffer才行
///Channel不通过构造器来获得,而是通过传统结点的InputStream,OutputStream的getChannel()方法获得
public class FileChannelTest {
public static void main(String[] args){
File f= new File("/Test/a.java");
try(
///创建FIleInputStream,以该文件输入流创建FileChannel
FileChannel inChannel = new FileInputStream(f).getChannel();
///以文件输出流创建FileChannel,用以控制输出
FileChannel outChannel = new FileOutputStream("/Test/test.txt").getChannel())
{
///将FileChannel里的全部数据映射成ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
///使用GBK字符集来创建解码器
Charset charset = Charset.forName("GBK");
///将buffer中的内容全部输出
outChannel.write(buffer);
buffer.clear();
///创建解码器对象
CharsetDecoder decoder = charset.newDecoder();
///使用解码器将ByteBuffer转换成CharBuffer
CharBuffer charbuffer = decoder.decode(buffer);
///CharBuffer中的toString方法可以获得对应的字符串
System.out.println(charbuffer.toString());
}
catch(IOException e){
e.printStackTrace();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
注意到上面的代码中使用了解码,这是因为ByteBuffer中装的是字节,所以如果我们直接输出则会产生乱码,如果想从ByteBuffer中读取到正确得内容,那么就需要进行编码。有两种形式,第一种是在将数据写入ByteBuffer中时就进行编码;第二种是从ByteBuffer中读出后进行解码。至于编码解码的话可以使用Charset类进行。如下面的代码所示:
package lkl;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.io.*;
///FileChannel转换数据类型
//FileChannel的写入类型只能是ByteBuffer,所以就引生出来编码解码的问题
public class BufferToText {
private static final int SIZE =1024;
public static void main(String[] args) throws IOException{
FileChannel fc = new FileInputStream("/Test/b.txt").getChannel();
ByteBuffer buff = ByteBuffer.allocate(SIZE);
fc.read(buff);
buff.flip();
///将ByteBuffer转成CharBuffer,但是实际上没有实现类型的转换,输出乱码
System.out.println(buff.asCharBuffer());
buff.rewind(); //指针返回开始位置,为解码做准备
//输出时解码,使得字节正确的转换成字符
String encoding = System.getProperty("file.encoding");
System.out.println("Decoded using "+encoding+": \n"+Charset.forName(encoding).decode(buff));
buff.clear();
//输入时进行编码,使得字节正确的转换成字符
fc= new FileOutputStream("/Test/a1.txt").getChannel();
buff.put("some txt".getBytes("UTF-8")); ///将字符转成字节时进行编码
buff.flip();
fc.write(buff);
fc.close();
fc= new FileInputStream("/Test/a1.txt").getChannel();
buff.clear();
fc.read(buff);
buff.flip();
System.out.println(buff.asCharBuffer()); //进行编码以后再转换就不会有问题了
///如果直接使用CharBuffer进行写入的话,也不会有编码的问题
fc = new FileOutputStream("/Test/a1.txt").getChannel();
buff.clear();
buff.asCharBuffer().put("this is test txt");
fc.write(buff);
fc = new FileInputStream("/Test/a1.txt").getChannel();
buff.clear();
fc.read(buff);
buff.flip();
System.out.println(buff.asCharBuffer());
fc.close();
}
/*
瑨楳猠瑥獴楬攊
Decoded using UTF-8:
this is test file
獯浥⁴硴
this is test txt
*/
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
(3).关于大端序和小端序的问题
大端序(高位优先)和小端序(低位优先)的问题
大端序是指将重要的字节放在地址最低的存储单元
小端序是指将重要的字节放在地址最高的存储单元
ByteBuffer是以大端序的形式存储数据的。
举个例子:00000000 01100001
上面这组二进制数据表示short型整数(一个数8位)
如果采用大端序表示97,如果是小端序则表示(0110000100000000)24832
下面的代码演示了大端序和小端序的比较:
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
//我们可以采用带有参数的ByteOrder.BIG_ENDIAN 或ByteOrder.LITTLE_ENDIAN的
//oder()方法改变ByteBuffer的字节排序方式
public class Endians {
public static void main(String[] args){
ByteBuffer bb =ByteBuffer.allocate(12);
bb.asCharBuffer().put("abcdef");
System.out.println(Arrays.toString(bb.array()));
bb.rewind();
bb.order(ByteOrder.BIG_ENDIAN);
bb.asCharBuffer().put("abcdef");
System.out.println(Arrays.toString(bb.array()));
bb.rewind();
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.asCharBuffer().put("abcdef");
System.out.println(Arrays.toString(bb.array()));
}
}/*Output
[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102]
[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102]
[97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102, 0]
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
数据在网上传输时用的也是大端序(高位优先)。
二.对象序列化问题
对象序列化指的是将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原先的对象。利用对象的序列化可以实现轻量级的持久性。“持久性”意味着一个对象的生存周期并不取决与程序是否正在执行;他可以生存在程序的调用之间。通过将一个序列化的对象写入磁盘然后在重新调用程序时恢复该对象就可以实现持久性的过程。之所以称为”轻量级”,是因为没有一个关键字可以方便的实现这个过程,整个过程还需要我们手动维护。
总的来说一般的序列化是没有什么很困难的,我们只要然相应的类继承一下Serializable接口就行了,而这个接口是一个标记接口,并不需要实现什么具体的内容,然后调用ObjectOutputStream将对象写入文件(序列化),如果想要恢复就用ObjectInputStream从文件中读取出来(反序列化);注意这两个类都是包装流,需要传入其它的结点流。如下面的代码所示,从输出来看,反序列化后对象的确实和原来的对象是一样的:
package lkl;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.*;
class Base implements Serializable{
private int i;
private int j;
public Base(int i,int j){
this.i=i;
this.j=j;
}
public String toString(){
return"[ "+ i+" "+j+" ]";
}
}
public class Test {
public static void main(String[] args) throws IOException,ClassNotFoundException{
Base base =new Base(1,3);
System.out.println(base);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("/Test/Base.out"));
out.writeObject(base); //将对像写入磁盘
ObjectInputStream in = new ObjectInputStream(new FileInputStream("/Test/Base.out"));
Base base1 =(Base)in.readObject(); ///将对象从磁盘读出
System.out.println(base1);
}/*
[ 1 3 ]
[ 1 3 ]
*/
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
除了实现Serializable接口外我们也可以通过实现Externalizable接口来实现序列话,这个接口运行我们自己对序列化的过程进行控制,我们手动的选择对那些变量进行序列化和反序列化。这些是依据这个接口中的两个函数:writeExternal()和readExternal()函数实现的。下面的代码演示了Externalizable接口的简单实现,要注意Blip1和Blip2类是有轻微不同的:
Constructin objects:
Blip1 Constructor
Blip2.Constructor
Saving objects:
Blip1.writeExternal
Blip2.writeExternal
Recovering p1:
Blip1 Constructor
Blip1.readExternal
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
我们可以看到在反序列化的过程中,是会调用默认构造器的,如果没有默认构造器可以调用(权限不为public)则在反序列的过程中,会出错。
另外如果我们实现的是Serializable接口但是我们希望某些变量不进行序列化,那么我们就可以用transient关键字对它们进行修饰。然后还要注意的是对于实现了Serializable接口的量,static变量是不会自动序列化的,我们必须手动进行序列化和反序列化才行。下面的代码演示了这两点:
package lkl;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.*;
class Base implements Serializable{
private int i;
private transient int j;
private static int k=9;
public Base(int i,int j){
this.i=i;
this.j=j;
}
public String toString(){
return"[ "+ i+" "+j+" "+k+" ]";
}
}
public class Test {
public static void main(String[] args) throws IOException,ClassNotFoundException{
Base base =new Base(1,3);
System.out.println(base);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("/Test/Base.out"));
out.writeObject(base); //将对像写入磁盘
ObjectInputStream in = new ObjectInputStream(new FileInputStream("/Test/Base.out"));
Base base1 =(Base)in.readObject(); ///将对象从磁盘读出
System.out.println(base1);
}/*
[ 1 3 9 ]
[ 1 0 9 ]
*/
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
下面的代码演示了在Serializable接口中我们也可以通过自己编写方法来控制序列化和反序列的过程(感觉很乱):
package lkl;
import java.io.*;
//通过在Serializable接口的实现中添加以下两个方法:
//private void writeObject(ObjectOutputStream stream) throws IOException
//private void readObject(ObjectInputStream stream) throws IOException,ClassNotFoundException
//就可以在这两个方法中自己自定需要序列化和反序列化的元素
///在writeObject()中调用defaultWriteObject()就可以选择执行默认的writeObject()
//在readObject()中调用defaultReadObject()就可以执行默认的readObject()
public class SerialCtl implements Serializable{
private String a;
private transient String b;
public SerialCtl(String aa,String bb){
a="Not Transient: "+aa;
b="transient: "+bb;
}
public String toString(){
return a+"\n"+b;
}
private void writeObject(ObjectOutputStream stream) throws IOException{
stream.defaultWriteObject();
stream.writeObject(b);
}
private void readObject(ObjectInputStream stream) throws IOException,ClassNotFoundException{
stream.defaultReadObject();
b=(String)stream.readObject();
}
public static void main(String[] args)throws IOException,ClassNotFoundException{
SerialCtl sc = new SerialCtl("Test1","Test2");
System.out.println("Before: ");
System.out.println(sc);
//这次序列化信息不存到文件,而是存到缓冲区去
ByteArrayOutputStream buf = new ByteArrayOutputStream();
ObjectOutputStream o = new ObjectOutputStream(buf);
o.writeObject(sc);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));
SerialCtl sc2 =(SerialCtl)in.readObject();
System.out.println("After: ");
System.out.println(sc2);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
最后需要强调的是:如果我们有很多个可以序列化的对象存在相互引用关系,序列化时只需要将他们统一打包进行序列化就可以,系统会自动维护一个序列化关系的网络。然后我们进行反序列化时,其实系统还是通过.class文件获得这个对象相应的信息的。