美文网首页
Java系列 - 序列化

Java系列 - 序列化

作者: lzyickobe | 来源:发表于2021-12-28 15:14 被阅读0次

    一、为什么需要序列化

    人类语言内容太丰富了(各种数据格式:图像、声音、文本、json/xml、java对象等),计算机要想去存储,肯定是要转化成它所能理解的某种方式。


    image.png

    视频、图像、声音这些内容我们一般使用编码的形式成为二进制流进行传输,比如通过H264/H265、JPEG、AAC等编码算法把这些让人类理解的视频图像声音内容转换成计算机可以理解的二进制数据,也方便计算机的存储和网络传输。
    所以做翻译主要有两个目标:
    1、本地保存:我们经常将一些信息保存在计算机内部;
    2、网络传输:我们还会把一些信息传给网线另一侧的计算机。


    image.png
    为了完成让代码中的对象信息实现 本地保存 和 网络传输 两个目标,所以就需要序列化。也可以这么理解,凡是离开内存的对象信息都需要进行序列化。

    二、什么是序列化和反序列化

    广义上来说:

    1、序列化:将内存中易于处理和阅读的数据格式转换称为二进制数据流或者文本流的过程。(序列化后的数据方便在网络上传输和在硬盘上存储)
    2、反序列化:与序列化相反,是将二进制数据流或者文本流转换称为易于处理和阅读的数据格式的过程。

    狭义点来说:

    1、Java序列化:把Java对象(java bean)转换为字节序列(byte[])。
    2、Java反序列化:把字节序列(byte[])恢复为原先的Java对象(java bean)。

    三、序列化的基础使用

    在Java中,如果一个对象要想实现序列化,必须要实现下面两个接口之一:
    Serializable 接口
    Externalizable 接口

    1、 Serializable 接口

    一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。
    这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient(临时的)修饰,static修饰的字段也不会被序列化。

    在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。

    2、Externalizable 接口

    它是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。

    对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式。因此Externalizable对象必须有默认构造函数,而且必需是public的。

    3、Serializable 和 Externalizable 对比

    • readExternal(),writeExternal()两个方法,这两个方法除了方法签名和readObject(),writeObject()两个方法的方法签名不同之外,其方法体完全一样。
    • 当使用Externalizable机制反序列化该对象时,程序会使用public的无参构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参构造。
    • 虽然实现Externalizable接口能带来一定的性能提升,但由于实现ExternaLizable接口导致了编程复杂度的增加,所以大部分时候都是采用实现Serializable接口方式来实现序列化。
    4、Serializable接口有何用

    我们点进Serializable接口内部查看,发现它竟然是一个空接口,并没有包含任何方法!
    Serializable接口也仅仅只是做一个标记用!它告诉代码只要是实现了Serializable接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。
    真正的序列化是靠这几个API完成的:ObjectOutputStream.writeObject();ObjectInputStream.readObject()


    image.jpeg
    5、serialVersionUID号有何用
    private static final long serialVersionUID = -4392658638228508589L;
    

    1、serialVersionUID是序列化前后的唯一标识符
    serialVersionUID序列化ID,可以看成是序列化和反序列化过程中的版本号(“暗号”),在反序列化时,JVM会把字节流中的序列号ID和被序列化类中的序列号ID做比对,只有两者一致,才能重新反序列化,否则就会报异常来终止反序列化的过程。

    2、默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!
    如果在定义一个可序列化的类时,没有人为显式地给它定义一个serialVersionUID的话,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID,一旦像上面一样更改了类的结构或者信息,则类的serialVersionUID也会跟着变化!

    四、演示Java序列化

    public class SerialDemo {
    
        public static void main(String[] args) throws IOException, ClassNotFoundException {
        //序列化
            FileOutputStream fos = new FileOutputStream("object.out");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            User user1 = new User("xuliugen", "123456", "male");
            oos.writeObject(user1);
            oos.flush();
            oos.close();
        //反序列化
            FileInputStream fis = new FileInputStream("object.out");
            ObjectInputStream ois = new ObjectInputStream(fis);
            User user2 = (User) ois.readObject();
            System.out.println(user2.getUserName()+ " " + 
                user2.getPassword() + " " + user2.getSex());
            //反序列化的输出结果为:xuliugen 123456 male
        }
    }
    
    public class User implements Serializable {
        private String userName;
        private String password;
        private String sex;
        //全参构造方法、get和set方法省略
    }
    
    

    五、Android中的序列化Parcelable

    重点1

    鉴于Serializable在内存序列化上开销比较大,而内存资源属于android系统中的稀有资源(android系统分配给每个应用的内存开销都是有限的),为此android中提供了Parcelable接口来实现序列化操作。

    重点2

    Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如通过Intent在activity间传输数据,而Parcelable的缺点就使用起来比较麻烦。

    Parcelable接口的实现案例

    public class Book implements Parcelable{
     private String bookName;
     private int publishDate;
     public Book(){
       
     }
     public String getBookName(){
      return bookName;
     }
     public void setBookName(String bookName){
      this.bookName = bookName;
     }
     public int getPublishDate(){
      return publishDate;
     }
     public void setPublishDate(int publishDate){
      this.publishDate = publishDate;
     }
      
     @Override
     public int describeContents(){
      return 0;
     }
      
     @Override
     public void writeToParcel(Parcel out, int flags){
      out.writeString(bookName);
      out.writeInt(publishDate);
     }
      
     public static final Parcelable.Creator<Book> CREATOR = new Creator<Book>(){
       
      @Override
      public Book[] newArray(int size){
       return new Book[size];
      }
       
      @Override
      public Book createFromParcel(Parcel in){
       return new Book(in);
      }
     };
      
     public Book(Parcel in){
      bookName = in.readString();
      publishDate = in.readInt();
     }
    }
    
    重点3:简单用一句话概括来说就是通过writeToParcel将我们的对象映射成Parcel对象,再通过createFromParcel将Parcel对象映射成我们的对象。

    也可以将Parcel看成是一个类似Serliazable的读写流,通过writeToParcel把对象写到流里面,在通过createFromParcel从流里读取对象,这个过程需要我们自己来实现并且写的顺序和读的顺序必须一致。

    重点4:哪里会使用到Parcelable对象

    1、通过Intent传递复杂类型


    image.png

    2、Bundle、Bitmap,它们本身实现了Parcelable序列化,因此我们可以方便地使用它们在组件间进行数据传递,当然Bundle本身也是一个类似键值对的容器,也可存储Parcelable实现类

    public class ParceBean implements Parcelable{
        private  Bitmap dw;
        private String name;
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Bitmap getDw() {
            return dw;
        }
    
        public void setDw(Bitmap dw) {
            this.dw = dw;
        }
    
         public static final Parcelable.Creator<ParceBean> CREATOR = new Creator<ParceBean>() { 
                public ParceBean createFromParcel(Parcel source) { 
                    ParceBean pb = new ParceBean(); 
                    pb.name = source.readString(); 
                    pb.dw = Bitmap.CREATOR.createFromParcel(source);
                    return pb; 
                } 
                public ParceBean[] newArray(int size) { 
                    return new ParceBean[size]; 
                } 
            }; 
        @Override
        public int describeContents() {
            return 0;
        }
    
        @Override
        public void writeToParcel(Parcel parcel, int flags) {
            parcel.writeString(name);
            dw.writeToParcel(parcel, 0);
        }
    }
    

    Bitmap本身实现了Parcelable接口,利用writeToParccel之后可以用createFromParcel来rebuild这个Bitmap。

    重点5:Parcelable 与 Serializable 区别
    (1)两者的实现差异(Serializable比Parcelabel简单)

    Serializable的实现,只需要实现Serializable接口即可。这只是给对象打了一个标记(UID),系统会自动将其序列化。而Parcelabel的实现,不仅需要实现Parcelabel接口,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator 接口,并实现读写的抽象方法。

    (2)两者的设计初衷

    Serializable的设计初衷是为了序列化对象到本地文件、数据库、网络流、RMI以便数据传输,当然这种传输可以是程序内的也可以是两个程序间的。而Android的Parcelable的设计初衷是由于Serializable效率过低,消耗大,而android中数据传递主要是在内存环境中(内存属于android中的稀有资源),因此Parcelable的出现为了满足数据在内存中低开销而且高效地传递问题。

    (3)两者效率选择(Serializable < Parcelable)

    Serializable使用IO读写存储在硬盘上。序列化过程使用了反射技术,并且期间产生临时对象,优点代码少,在将对象序列化到存储设置中或将对象序列化后通过网络传输时建议选择Serializable。
    Parcelable是直接在内存中读写,我们知道内存的读写速度肯定优于硬盘读写速度,所以Parcelable序列化方式性能上要优于Serializable方式很多。

    重点6:最后一点,如何选择序列化方式

    1、Android应用程序在内存间数据传输时推荐使用Parcelable,如activity间传输数据和AIDL数据传递。大多数情况下使用Serializable也是没什么问题的。
    2、Parcelable也可以在网络中传输,只不过实现和操作过程过于麻烦并且为了防止android版本不同而导致Parcelable可能不同的情况,因此在序列化到存储设备或者网络传输方面还是尽量选择Serializable接口。

    相关文章

      网友评论

          本文标题:Java系列 - 序列化

          本文链接:https://www.haomeiwen.com/subject/akmvqrtx.html