Java性能优化系列之5-JavaIO

Java性能优化系列之五--JavaIO

1、关于Java序列化与反序列化:

(1)作用:

1、实现对象状态的保存到本地,以便下一次启动虚拟机的时候直接读取保存的序列化字节生成对象,而不是初始化对象;2、实现对象的网络传输(RMI分布对象);3、实现对象的深拷贝。

    一:对象序列化可以实现分布式对象。主要应用例如:RMI要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。

    二:java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。

(2)基本方式:

  ObjectOutputStream只能对Serializable接口的类的对象进行序列化。默认情况下,ObjectOutputStream按照默认方式序列化,这种序列化方式仅仅对对象的非transient的实例变量进行序列化,而不会序列化对象的transient的实例变量,也不会序列化静态变量。

  当ObjectOutputStream按照默认方式反序列化时,具有如下特点:

  1) 如果在内存中对象所属的类还没有被加载,那么会先加载并初始化这个类。如果在classpath中不存在相应的类文件,那么会抛出ClassNotFoundException

  2) 在反序列化时不会调用类的任何构造方法。

  如果用户希望控制类的序列化方式,可以在可序列化类中提供以下形式的writeObject()readObject()方法。

  private void writeObject(java.io.ObjectOutputStream out) throws IOException

  private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;

  当ObjectOutputStream对一个Customer对象进行序列化时,如果该对象具有writeObject()方法,那么就会执行这一方法,否则就按默认方式序列化。在该对象的writeObjectt()方法中,可以先调用ObjectOutputStreamdefaultWriteObject()方法,使得对象输出流先执行默认的序列化操作。同理可得出反序列化的情况,不过这次是defaultReadObject()方法。

  有些对象中包含一些敏感信息,这些信息不宜对外公开。如果按照默认方式对它们序列化,那么它们的序列化数据在网络上传输时,可能会被不法份子窃取。对于这类信息,可以对它们进行加密后再序列化,在反序列化时则需要解密,再恢复为原来的信息。

  默认的序列化方式会序列化整个对象图,这需要递归遍历对象图。如果对象图很复杂,递归遍历操作需要消耗很多的空间和时间,它的内部数据结构为双向列表。

  在应用时,如果对某些成员变量都改为transient类型,将节省空间和时间,提高序列化的性能。

|-1、实体对象实现seriable接口以及自定义seriousid

|-2、 ObjectOutputStream out= new ObjectOutputStream(baos);  

        out.writeObject(new PersonDemo("rollen", 20));  

        out.close();  

|-3ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray());

     ObjectInputStream input=new ObjectInputStream(bais);

        Object obj =input.readObject();  

        input.close();  

(3)Java自定义序列化反序列化:复写实现了seriliable的实体类的readObject()writeObject()的方法的原因:

有些对象中包含一些敏感信息,这些信息不宜对外公开。如果按照默认方式对它们序列化,那么它们的序列化数据在网络上传输时,可能会被不法份子窃取。对于这类信息,可以对它们进行加密后再序列化,在反序列化时则需要解密,再恢复为原来的信息。此时便不能使用默认的readObjectwriteObject()方法。 

private void writeObject(java.io.ObjectOutputStream out) throws IOException{

     out.defaultWriteObject();

     out.writeUTF(name);

    }

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{

     in.defaultReadObject();

     name=in.readUTF();

    }

一般情况直接实现Serializable接口就可以实现序列化的要求,但是有些情况需要对序列化做一些特殊的要求。

(4)Transits关键字的作用:屏蔽一些不想进行序列化的成员变量,解屏蔽的方法可以用(3

(5)Externalize的作用:

    Externalizable接口继承自Serializable接口,如果一个类实现了Externalizable接口,那么将完全由这个类控制自身的序列化行为。Externalizable接口声明了两个方法:

public void writeExternal(ObjectOutput out) throws IOException

public void readExternal(ObjectInput in) throws IOException , ClassNotFoundException

前者负责序列化操作,后者负责反序列化操作。

    在对实现了Externalizable接口的类的对象进行反序列化时,会先调用类的不带参数的构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常。

(6)与Java构造函数的关系:

    实现了Externalizable接口的类的对象进行反序列化时,会先调用类的不带参数的构造方法;而实现了Serializable接口的类的对象进行反序列化时,不会调用任何构造方法。仅仅是根据所保存的对象的状态信息,在内存中重新构建对象!

(7)注意事项:

   1)、序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:

      在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID

      在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID

2)、java有很多基础类已经实现了serializable接口,比如string,vector等。但是比如hashtable就没有实现serializable接口。

3)、并不是所有的对象都可以被序列化。由于安全方面的原因一个对象拥有private,publicfield,对于一个要传输的对象,比如写到文件,或者进行rmi传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的;资源分配方面的原因,比如socket,thread,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现.

4)、反序列化对象时,并不会调用该对象的任何构造方法,仅仅是根据所保存的对象的状态信息,在内存中重新构建对象!

5)、当一个对象被序列化时,只保存对象的非静态成员变量,不能保存任何的成员方法和静态的成员变量

6)、如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因。

(8)序列化与单例模式的冲突解决办法:

    另外还有两个自定义序列化方法writeReplacereadResolve,分别用来在序列化之前替换序列化对象 和 在反序列化之后的对返回对象的处理。一般可以用来避免singleTon对象跨jvm序列化和反序列化时产生多个对象实例,事实上singleTon的对象一旦可序列化,它就不能保证singleTon了。JVMEnum实现里就是重写了readResolve方法,由JVM保证Enum的值都是singleTon的,所以建议多使用Enum代替使用writeReplacereadResolve方法。

Java代码

private Object readResolve()     

    {     

        return INSTANCE;     

    }     

        

    private Object writeReplace(){     

        return INSTANCE;     

    }    

注:writeReplace调用在writeObject;readResolve调用在readObject之后。

(9)序列化解决深拷贝的代码:

public Object deepClone() throws IOException, OptionalDataException,   

        ClassNotFoundException {   

    // 将对象写到流里   

    ByteArrayOutputStream bo = new ByteArrayOutputStream();   

    ObjectOutputStream oo = new ObjectOutputStream(bo);   

    oo.writeObject(this); // 从流里读出来   

    ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());   

    ObjectInputStream oi = new ObjectInputStream(bi);   

    return (oi.readObject());   

对象所属的类要实现Serializable接口。同时将该方法写入到对象所属的类中。

深拷贝的时候,调用该方法即可。

2、JavaIO中的装饰模式:

 Java性能优化系列之5-JavaIO

Java中使用的最广泛的装饰器模式就是JavaIO类的设计。比如,OutPutStream是输出流的基类,其子类有FileOutputStream FilterOutputStream,FilterOutputStream的子类有BufferedOutputStreamDataOutputStream两个子类。其中,FileOutputStream为系统的核心类,它实现了向文件写数据的功能,使用DataOutputStream可以在FileOutputStream的基础上增加多种数据类型的写操作支持(DataOutputStream类中有writeUTFwriteInt等函数),而BufferdOutputStream装饰器可以对FileOutputStream增加缓冲功能,优化I/O性能。

3、JavaIO流的使用场景:

(1)IO流:用于处理设备上的数据,这里的设备指的是:硬盘上的文件、内存、键盘输入、屏幕显示。

(2)字节流和字符流:字节流好理解,因为所有格式的文件都是以字节形式硬盘上存储的,包括图片、MP3avi等,因此字节流可以处理所有类型的数据。字符流读取的时候读到一个或多个字节时(中文对应的 字节数是两个,在UTF-8码表中是三个字节)时,先去查指定的编码表,将查到的字符返回。字符流之所以出现,就是因为有了文件编码的不同,而有了对字符进行高效操作的字符流对象。因此,只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都使用字节流。

(3)流操作的基本规律:

  1)、明确数据源和数据汇,目的是明确使用输入流还是输出流。

  2)、明确操作的数据是否是纯文本数据。

  3)、是否需要进行字节流和字符流的转换。

  4)、是否需要使用缓存。

(4)实例说明流操作的基本流程:把键盘上读入的数据以指定的编码存入到文件中。

   1)、明白数据源:键盘输入,System.in,可用InputStreamReader

   2)、发现System.in对应的流是字节读入流,所以要将其进行转换,将字节转换为字符。

   3)、所以要使用InputStreamReader转换流

   4)、如果想提高效率,要加入缓存机制,那么就要加入字符流的缓冲区。BufferedReader,因此前四步构造出的输入流为:

BufferedReader bur = new BufferedReader(new InputStreamReader(System.in));

   5)、明白数据汇:既然是数据汇,则一定是输出流,可以用OutputStreamWriter

   6)、往文件中存储的都是文本文件,因此选用Writer

   7)、因为要指定编码表,所以使用Writer中的转换流,OutputStreamWriter

注意:虽然最终是文件,但是不可以选择FileWriter,因为该对象是使用默认编码表。

   8)是否要提高效率,选择BufferedWriter

   9)转换输出流需要接收一个字节输出流进来,所以要是用OutputStream体系,而最终输出到一个文件中。那么就要使用OutputStream体系中可以操作的文件的字符流对象,FileOutputStream

  10)、通过前面的分析,得到的输出流对象如下:

//String charSet = System.getProperty("file.encoding");

String charSet = "utf-8";

BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(new

FileOutputStream("a.txt"),charSet);
4、可以和流相关联的集合对象Properties

Map

  |--HashTable

    |--Properties

Properties:该集合不需要泛型,因为该集合中的键值都是String类型。

5、其他流对象:

  (1)打印流:

PrintStream:是一个字节打印流System.out对应的就是PrintStream。它的构造函数可以接收三种数据类型的值:字符串路径、File对象、OutputStream(当为System.out的时候即把输入显示到屏幕上)

PrintWriter:是一个字符打印流。构造函数可以接收四种类型的值。字符串路径、File对象(对于这两中类型的数据,还可以指定编码表。也即是是字符集)、OutPutSreamWriter(对于三、四类型的数据,可以指定自动刷新,注意:当自动刷新的值为true时,只有三个方法可以用:printlfprintfformat

(2)管道流:PipedOutputStreamPipedInputStream。一般在多线程中通信的时候用。

(3)RandomAccessFile:该对象不是流体系中的一员,但是该队选中封装了字节流,同时还封装了一个缓冲区(字节数组),通过内部的指针来操作数组中的数据。该对象特点:只能操作文件和对文件读写都可以。多用于多线程下载。、

(4)合并流:可以将多个读取流合并成一个流。其实就是将每一个读取流对象存储到一个集合中,最后一个流对象结尾作为这个流的结尾。

(5)对象的序列化。ObjectInputStreamObjectInputStream

(6)操作基本数据类型的流对象:DataInputStreamDataOutputStream

(7)操纵内存数组的流对象,这些对象的数据源是内存,数据汇也是内存:ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter。这些流并未调用系统资源,使用的是内存中的数组,所以在使用的时候不用close

(8)编码转换:

IO中涉及到编码转换的流是转换流和打印流,但是打印流只有输出。转换流是可以指定编码表的,默认情况下,都是本机默认的编码表,GBK。可以通过:Syetem.getProperty(“file.encoding”)得到。字符串到字节数组成为编码的过程,通过getBytes(charset)完成,从字节数组到字符串的过程是解码的过程,通过String类的构造函数完成Stringbyte[],charset.

(9)编码实例与解析:

(10)JavaNIOCharset类专门用来编码和解码。