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));
    }
}

一般都是用的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
          */
}

当然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();
                }
    }
}

注意到上面的代码中使用了解码,这是因为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
     */
}

(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]
*/

数据在网上传输时用的也是大端序(高位优先)。

二.对象序列化问题

对象序列化指的是将那些实现了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 ]
    */
}

除了实现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

我们可以看到在反序列化的过程中,是会调用默认构造器的,如果没有默认构造器可以调用(权限不为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 ]
    */
}

下面的代码演示了在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);
    }
}

最后需要强调的是:如果我们有很多个可以序列化的对象存在相互引用关系,序列化时只需要将他们统一打包进行序列化就可以,系统会自动维护一个序列化关系的网络。然后我们进行反序列化时,其实系统还是通过.class文件获得这个对象相应的信息的。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-10 07:46:24

Tinking in Java ---Java的NIO和对象序列化的相关文章

java中为什么要进行对象序列化?

序列化其实很好理解,假如你现在做一个项目,项目是分工合作的,并且你喝其他小组成员不在同一个城市,那么你要如何把你写的那些类给其他小组成员呢?这个时候就要用到序列化了,简单的说:序列化就是将内存中的类或者对象(你写的类都是存储在内存中的)变成可以存储到存储媒介中的流,你将类序列化成流之后可以通过互联网传输给别人,你也可以反序列化将别人的序列化流转换成内存中的对象,就这么简单 ——--大神级般的解释

Thinking in java 琐碎知识点之 I/O流 、对象序列化

Java I/O流 .对象序列化 1.File类 此类的实例可能表示(也可能不表示)实际文件系统对象,如文件或目录. File类可以新建.删除和重命名文件和目录,但是File不能访问文件本身的内容,这要使用IO流. File对象的createNewFile()方法在磁盘上创建真实的文件 例程:FileTest.java import java.io.*; public class FileTest { public static void main(String[] args) throws I

Java核心知识点-NIO

文件读取中的NIO 在Java1.4之前的I/O系统中,提供的都是面向流的I/O系统,系统一次一个字节地处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据,面向流的I/O速度非常慢,而在Java 1.4中推出了NIO,这是一个面向块的I/O系统,系统以块的方式处理处理,每一个操作在一步中产生或者消费一个数据库,按块处理要比按字节处理数据快的多. 在NIO中有几个核心对象需要掌握:缓冲区(Buffer).通道(Channel).选择器(Selector). 缓冲区Buffer 缓

Java网络编程-Nio 实例代码

IO NIO 区别请看 : http://blog.csdn.net/jiangtao_st/article/details/38041479 一.基于Nio的 Server ,过程略复杂,但是无疑这样的效率高:代码中得注释比较详细,请看注释说明 package com.zhuoxuan.net.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; imp

Java IO模型&NIO

Java IO模型&NIO Java IO模型NIO 楔子 概述 网络服务 经典的服务设计 经典的SocketServer循环阻塞 可伸缩目标 分而治之 事件驱动设计 背景知识AWT 事件 Reactor 模式 Reactor基础模式 Java NIO 支持 Channels Buffers Selectors SelectionKeys Reactor 模式实践 第一步初始化 第二步循环分发 第三步接收者 第四步 Handler设置 第五步请求处理 还有一种状态Handler 多线程版本Rea

Java中的NIO基础知识

上一篇介绍了五种NIO模型,本篇将介绍Java中的NIO类库,为学习netty做好铺垫 Java NIO 由3个核心组成,分别是Channels,Buffers,Selectors.本文主要介绍着三个部分. Channel 所有的I/O都从一个Channel开始.通道与流不同,通道是双向的,流是单向的. 即可以从通道中读取数据,也可以写数据到通道里 . 读的话,是从通道读取数据到缓冲区,写的话是从缓冲区写入数据到通道. 四种通道: FileChannel.从文件中读写数据 DatagramCha

java IO、NIO、AIO详解

概述 在我们学习Java的IO流之前,我们都要了解几个关键词 同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步:而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件.回调等机制来实现任务间次序关系 阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket新连接建立完毕,或者数据读取.写入操作完成:而非阻塞则是不

[转帖]JAVA BIO与NIO、AIO的区别(这个容易理解)

JAVA BIO与NIO.AIO的区别(这个容易理解) https://blog.csdn.net/ty497122758/article/details/78979302 2018-01-05 11:26:13 涂有 阅读数 41728 文章标签: javaaiobionio 更多 分类专栏: java IO的方式通常分为几种,同步阻塞的BIO.同步非阻塞的NIO.异步非阻塞的AIO. 一.BIO 在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个Server

Java BIO、NIO与AIO的介绍(学习过程)

Java BIO.NIO与AIO的介绍 因为netty是一个NIO的框架,所以在学习netty的过程中,开始之前.针对于BIO,NIO,AIO进行一个完整的学习. 学习资源分享: Netty学习:https://www.bilibili.com/video/BV1DJ411m7NR?from=search&seid=8747534277052777648 Netty源码:https://www.bilibili.com/video/BV1cb411F7En?from=search&seid