从Java序列化到Hadoop序列化

Java序列化

         序列化指的是将Java对象转换成字节序列,便于网络传输和持久化;反序列化指的是将字节序列转换成Java对象。Java对基本数据类型和String类型实现了序列化,不需要显式的序列化/反序列化,直接使用即可。对于一般的类对象,则需要用户显式序列化/反序列化,并且类的成员变量也得是可序列化/反序列化才行。

        在Java中,一个对象想要实现序列化,有两种方式:一、实现Serializable接口。二、实现Externalizable接口。后者是Serializable接口的子类,提供了writeExternal()和readExternal()方法可以更加灵活实现序列化。下面重点讲解的是Serializable接口的方法

        序列化/反序列化的详细讲解:

  • 需要序列化的类应该实现一个接口Serializable。

Serializable虽然是一个空接口,但是想要一个类的对象能够序列化就必须实现这个接口。因为根据报错信息NotSerializableException定位到ObjectOutputStream.writeObject()方法:里面有一句obj instanceof Seriablizable。仔细分析这个方法可知道,如果一个对象不是字符串不是数组不是枚举也没有实现Serializable接口,就会抛出异常。实现serializable接口只是声明这个类可以被序列化,而真正的序列化动作不需要它实现。

  • serialVersionUID

serialVersionUID是一个特殊的静态变量,保证序列化和反序列化的兼容性。每个可序列化的类都有一个serialVersionUID,可以显式指定,如果不显示定义,系统会根据类的结构自动生成。如果不显式声明,对一个类进行修改,那么系统自动生成的serialVersionUID就会发生变化。反序列化时serialVersionUID和序列化时serialVersionUID不一致,就会抛出异常,导致序列化失败。因此,显式声明serialVersionUID可以确保类在发生变化的时候,依然能够正确的序列化反序列化。

  • 需要序列化的类的成员变量也必须是可序列化的。
  • Java序列化同一个对象,并不会序列化多次。

所有保存到磁盘的对象都有一个序列化编码号。当一个程序试图序列化一个对象,会先检查此对象是否被序列化过,只有对象从没有被序列化过才会转换为字节数组。如果已经被序列化过,直接输出编号。但是由于Java序列化算法不重复序列化同一个对象,只记录编号。如果序列化一个对象之后,这个对象更改内容,再次序列化,也不会将序列化。

  • 子类实现序列化,但是父类没有实现序列化

如果父类存在无参构造函数,可以序列化反序列化成功,但是父类的属性其实并没有参与到序列化反序列化的过程中来。

如果父类不存在无参构造函数,父类的属性没有序列化,只序列化子类的属性。不能反序列化

  • 在以下两种情况下不进行序列化

    1>static修饰的属性不会被序列化:序列化保存的是对象的状态不是类的状态,所以不序列化。static静态字段也是理所当然。

    2>transient修饰的属性也不会被序列化:如果在序列化类的对象的时候,不希望某个字段被序列化,就用transient修饰。

虽然Java原生序列化很简单方便,但是它也有无法无法跨语言、序列化后的字节流太大、序列化时间长等缺点。因此,除了Java原生序列化之外,还有很多高效的序列化框架,kyro等。

 序列化的应用案例:
 

1.从外部实现序列化的方式

import java.io.Serializable;
public class Example_Outer implements Serializable {
    private String name;
    private  String address;
    private int age;

    //省略getter、setter方法
    @Override
    public String toString() {
        return name+" "+address+" "+age;
    }
}


import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String path="D:\\Java_Project\\untitled\\a.txt";
        serialize(path);
        deserialize(path);
    }

    public static void serialize(String path) throws IOException {
        ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream(path));
        Example_Outer example_outer=new Example_Outer();
        example_outer.setAddress("NY");
        example_outer.setName("alice");
        example_outer.setAge(10);
        out.writeObject(example_outer);
        out.close();
        System.out.println(example_outer.toString());
    }
    public static void deserialize(String path) throws IOException, ClassNotFoundException {
        ObjectInputStream in=new ObjectInputStream(new FileInputStream(path));
        Example_Outer example_outer=(Example_Outer) in.readObject();
        in.close();
        System.out.println(example_outer.toString());
    }
}

2.自定义实现序列化的方式

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Example_Inner implements Serializable {
    private static final Long serialVersionUID=10000L;
    private String name;
    private  String address;
    private  int age;
    //省略getter setter

    @Override
    public String toString() {
        return name+" "+address+" "+age;
    }

    private void readObject(ObjectInputStream in ) throws IOException, ClassNotFoundException {
        this.name = ((StringBuffer)in.readObject()).reverse().toString();

        this.age = in.readInt();
        this.address =(String) in.readObject();


    }
    private void writeObject(ObjectOutputStream out) throws IOException {
        //可以灵活的翻转name
        out.writeObject(new StringBuffer(this.name).reverse());
        out.writeInt(this.age);
        out.writeObject(this.address);

    }


}



import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String path="D:\\Java_Project\\untitled\\a.txt";
        serialize(path);
        deserialize(path);

    }

    public static void serialize(String path) throws IOException {
        ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream(path));
        Example_Inner example_inner=new Example_Inner();
        example_inner.setAddress("NY");
        example_inner.setName("alice");
        example_inner.setAge(10);
        out.writeObject(example_inner);
        out.close();
        System.out.println(example_inner.toString());

    }
    public static void deserialize(String path) throws IOException, ClassNotFoundException {
        ObjectInputStream in=new ObjectInputStream(new FileInputStream(path));
        Example_Inner example_inner=(Example_Inner) in.readObject();
        in.close();
        System.out.println(example_inner.toString());

    }
}

方式二相对于方式一,重写writeObject和readObject。它们都是private方法,但是它却被外部类(ObjectOutputStream和ObjectInputStream)调用,并且它们既不存在与Java.lang.Object,也没有在Serializeable中声明,那它们是如何被ObjectOutputStream和ObjectInputStream调用的呢?因为利用反射机制。ObjectOutputStream和ObjectInputStream使用了反射来寻找是否声明了这两个方法。因为它们使用getPrivateMethod,所以这些方法不得不被声明为priate以至于供ObjectOutputStream来使用。另外,Write的顺序和read的顺序需要对应。譬如writeObject有多个字段都用writeInt写入流中,那么readint需要按照顺序将其赋值。

 方式二相对于方式一的好处:

        1.性能优化:可以跳过不需要序列化的字段,减少序列化后的字节流大小,提高性能。

        2.安全性:对于敏感数据进行加密或者特别处理,提供安全性。

        3.版本兼容:可以灵活控制序列化格式,更好的兼容不同版本的类。

Java排序

        在Java编程中,经常需要对对象进行排序。为实现排序,Java提供了两种方式:Comparable和Compator。

1、Comparable

        java.lang.Comparable接口,允许用户定义对象之间的排序方法。它的内容和使用案例如下:

public interface Comparable<T>{
    int compareTo(T o);
    //返回0,表示两个对象等于;返回正整数,表示大于;返回负数,表示小于
}


public class Example implements Comparable<Example> {

    private String name;
    private String address;
    private  int age;

   //省略getter setter方法
    @Override
    public String toString() {
        return name+" "+address+" "+age;
    }

    @Override
    public int compareTo(Example o)
    {
     
        return this.age-o.age;
        
    }
}

2.Comparator

        java.util.Comparator也是用于定义对象排序的接口,它允许为一个类定义不同的排序方式或者为没有实现Comparable的类定义排序顺序。它的定义和使用如下:

public interface Comparator<T> 
{
   int compare(T o1, T o2);
    //....还有一些方法
}

import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Comparator;

public class Example implements Comparable<Example>  {

    private String name;
    private  String address;
    private  int age;
    //省略getter  setter方法
    @Override
    public String toString() {
        return name+" "+address+" "+age;
    }

    @Override
    public int compareTo(Example o) {
        return this.age-o.age;
    }
}

import java.util.Comparator;

public class MyComparator implements Comparator<Example> {

    @Override
    public int compare(Example o1, Example o2) {
        return o2.getAge()-o1.getAge();
    }
}



public static void main(String[] args) throws IOException, ClassNotFoundException 
{

        Example[] array=new Example[3];
        Example ex1=new Example();
        ex1.setName("a");
        ex1.setAge(10);
        Example ex2=new Example();
        ex2.setName("b");
        ex2.setAge(9);
        Example ex3=new Example();
        ex3.setName("c");
        ex3.setAge(8);
        array[0]=ex1;
        array[1]=ex2;
        array[2]=ex3;

        //没有指定比较器,如果array的数据类型实现了comparable则用其规定的顺序。
        //如果没有实现,抛出异常。
        Arrays.sort(array);
        

        //实现Comparator接口,指定了比较器。
        MyComparator myComparator=new MyComparator();
        Arrays.sort(array,myComparator);

        //采用匿名内部类的方式
        Arrays.sort(array, new Comparator<Example>() {
            @Override
            public int compare(Example o1, Example o2) {
                return o1.getAge()-o2.getAge();
            }
        });

    }

3.Comparable和Comparator的比较

  • 可以理解Comparable是内部比较器,Comparator是外部比较器。如果类使用 Comparable ,那么就要修改类实现 Comparable 接口并重写 compareTo 方法,所以 Comparable 更像是“对内”进行排序的接口。而 Comparator 的使用则不相同,Comparator 无需修改原有类。也就是在最极端情况下,即使类是第三方提供的,我们依然可以通过创建新的自定义比较器 Comparator,来实现对第三方类的排序功能。也就是说通过 Comparator 接口可以实现和原有类的解耦,在不修改原有类的情况下实现排序功能,所以 Comparator 可以看作是“对外”提供排序的接口。
  • 使用场景:当类只需要一种排序方式,并且类的所有实例都按照这种方式排序,那么Comparable。当类需要多种排序方式,或者类没有实现Comparable接口也无法修改,用Comparator。
  • 如果一个类实现Comparable接口,并且定义了Comparator的时候,Comparable被认为是自然排序顺序,Comparator是可选的。
  • Collections.sort()或者Arrays.sort()排序的时候如果不指定Comparator,那就以实现Comparable接口设定的方式排序。

Hadoop序列化

        Hadoop中涉及到数据在网络中传输或者存储在磁盘,所以序列化/反序列化在Hadoop中至关重要。而Java序列化附带了很多额外的信息,不适合在网络中高效传输,因此,Hadoop开发了自己的序列化机制,称为Writable。Hadoop的序列化接口是 org.apache.hadoop.io.Writable。和Java一样,Hadoop的一些数据类型已经实现序列化直接使用即可,但是对于一些类还是需要手动进行。

 public interface Writable{
        void write(DataOutput out) throws IOException;
        void readFields(DataInput in) throws IOException;
}



import org.apache.hadoop.io.Writable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

public class MyWritable implements Writable {
    // 一些数据
    private int counter;
    private long timestamp;

    // 默认的构造器,用来支持序列化/反序列化
    MyWritable() {
    }

    public void write(DataOutput out) throws IOException {
        out.writeInt(counter);
        out.writeLong(timestamp);
    }

    public void readFields(DataInput in) throws IOException {
        counter = in.readInt();
        timestamp = in.readLong();
    }

    public static MyWritable read(DataInput in) throws IOException {
        MyWritable w = new MyWritable();
        w.readFields(in);
        return w;
    }
}

Hadoop序列化的步骤:

1.实现Writable接口

2.反序列化时,需要反射调用无参构造函数,因此必须有。(Serializable不通过无参构造函数,所以不“必须有”)

 3.重写write()方法和readFields()方法。注意顺序一致。

4.在自定义数据类型中,建议使用java原生数据类型,最好不要使用hadoop对原生类型封装好的数据类型。(Why?)

Hadoop排序

        排序在MapReduce框架中是非常重要的操作,而排序是根据Key进行的。如果Key的数据类型是Hadoop实现的数据类型,是按照字典顺序排序。而如果Key的数据类型是自定义的类,那么类就需要实现比较功能。

        MapReduce有一下比较方式:

1. WritableComparable:

        可以使用java排序中的comparable方法实现,重写compareTo()方法即可。同时,在MapReduce中自定义的类往往也需要实现序列化反序列化,于是将Writable和Comparable结合起来,形成一个新的接口WritableComparable。使用方法和单独使用的时候一样。

​
public interface WritableComparable<T> extends org.apache.hadoop.io.Writable,java.lang.Comparable<T>{ 
       public void write(DataOutput out) throws IOException;
       public void readFields(DataInput in) throws IOException;  
       public int compareTo(MyWritableComparable o);

}

 public class MyWritableComparable implements
      WritableComparable <MyWritableComparable> {
      
       private int counter;
       private long timestamp;
       
       public void write(DataOutput out) throws IOException {
         out.writeInt(counter);
         out.writeLong(timestamp);
       }
       
       public void readFields(DataInput in) throws IOException {
         counter = in.readInt();
         timestamp = in.readLong();
       }
       
       public int compareTo(MyWritableComparable o) {
         int thisValue = this.value;
         int thatValue = o.value;
         return (thisValue < thatValue ? -1 : (thisValue==thatValue ? 0 : 1));
       }


     }

2. RawComparator:

        如果使用WritableComparable的实现比较,Hadoop的默认方式是先将序列化后的对象字节流反序列化为对象,然后再进行比较(compareTo方法),比较过程需要一个反序列化的步骤,而这一整个过程会产生额外的开销。

        Hadoop中提供了原生的比较接口RawComparator,该接口继承于 Java Comparator 接口。RawComparator接口允许其实现直接比较数据流中的记录,无需先把数据流反序列化为对象,这样避免了新建对象的额外开销,从而加速程序的运行。

        RawComparator是一个接口。

public interface RawComparator<T> extends java.util.Comparator<T>{
    int compare(byte[] bytes,int i,int i1,byte[] bytes2,int i2,int i3);
    //除了基于字节的比较方法,还有对象的比较方法(继承了Comparator)
}

public class MyRawComparator implements RawComparator<Example> {
    private Example o1=new Example();
    private Example o2=new Example();
    private DataInputBuffer buffer=new DataInputBuffer();
    @Override
    public int compare(byte[] bytes,int i,int i1,byte[] bytes2,int i2,int i3)
    {
        try{
            buffer.reset(bytes,i,i1);
            o1.readFields(buffer);
            buffer.reset(bytes2,i2,i3);
            o2.readFields(buffer);
            buffer.reset(null,0,0);

        }catch( IOException e){
            throw new RuntimeException(e);

        }
        return compare(o1,o2);

    }
    @Override
    public int compare(Example o1,Example o2)
    {
        //此处定义两个变量比较的方式
        return 0;
    }

}

        在第一个方法是在程序中先调用的,在此方法中将字节数组封装成要比较的类型,然后调用第二个方法。

        在完成自定义比较器之后,在Driver中设置:

                job.setSortComparatorClass(MyRawComparator.class);

3. WritableComparator:

        在Hadoop中编写Writable的RawComparator一般不直接继承RawComparator类,而是继承RawComparator的子类WritableComparator。需要注意的是,继承WritableComparator的类必须是实现WritableComparable接口的类。

        Hadoop自身提供的IntWritable、LongWritabe等类通过将WritableComparator的子类作为内部类的方法实现了优化,使得用Key进行比较时,直接使用序列化的字节数组进行比较大小,而不用进行反序列化。而对于自定义的类,需要用户手动设置。

        作为实现Writable接口的类的内部类(以IntWritable为例子)

public class IntWritable implements WritableComparable<IntWritable> {
  private int value;

  public IntWritable() {}

  public IntWritable(int value) { set(value); }

  /** Set the value of this IntWritable. */
  public void set(int value) { this.value = value; }

  /** Return the value of this IntWritable. */
  public int get() { return value; }

  @Override
  public void readFields(DataInput in) throws IOException {
    value = in.readInt();
  }

  @Override
  public void write(DataOutput out) throws IOException {
    out.writeInt(value);
  }

  /** Returns true iff <code>o</code> is a IntWritable with the same value. */
  @Override
  public boolean equals(Object o) {
    if (!(o instanceof IntWritable))
      return false;
    IntWritable other = (IntWritable)o;
    return this.value == other.value;
  }

  @Override
  public int hashCode() {
    return value;
  }

  /** Compares two IntWritables. */
  @Override
  public int compareTo(IntWritable o) {
    int thisValue = this.value;
    int thatValue = o.value;
    return (thisValue<thatValue ? -1 : (thisValue==thatValue ? 0 : 1));
  }

  @Override
  public String toString() {
    return Integer.toString(value);
  }

  /** A Comparator optimized for IntWritable. */
  public static class Comparator extends WritableComparator {
    public Comparator() {
      super(IntWritable.class);
    }
    
    @Override
    public int compare(byte[] b1, int s1, int l1,
                       byte[] b2, int s2, int l2) {
      int thisValue = readInt(b1, s1);
      int thatValue = readInt(b2, s2);
      return (thisValue<thatValue ? -1 : (thisValue==thatValue ? 0 : 1));
    }
  }

  static { // register this comparator
    WritableComparator.define(IntWritable.class, new Comparator());
  }
}

        和RawComparator一样作为自定义比较器

public class MyWritableComparator extends WritableComparator {

    @Override
    public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {
        //return super.compare(b1, s1, l1, b2, s2, l2);
        //通过WritableComparator自带的相关readxx方法解析出变量然后进行判断
    }

}

         WritableComparator的详细解析

4. 总结

        WritableComparable就像是Java排序中Comparable,是内部比较器。如果使用compreTo定义的方式排序,就直接用即可不需要额外的设置。如果不使用这个compateTo的方法,就需要自定义一个外部比较器,外部比较器需要设置。

        对于外部比较器,常使用的是继承WritableComparator方式自定义,因为 compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2)方法已经实现,而RawComparator接口没有实现。但是如果自定义的类没有实现WritableComparable接口,就必须采用RawComparator的方式。

        此外,如果不需要使用自定义比较器而又想要避免反序列带来的开销,可以将WritableComparator子类作为自定义类的静态内部类。

参考资料

https://blog.csdn.net/qq_35462323/article/details/106569701
https://mp.weixin.qq.com/s/KNoQha6JR7kn8smpHlMoLg
https://www.jianshu.com/p/352fa61e0512
https://www.cnblogs.com/9dragon/p/10901448.html

https://blog.csdn.net/CSDN_WYL2016/article/details/109114549
https://blog.csdn.net/qq_43193797/article/details/86093138
https://www.cnblogs.com/sunbr/p/13398499.html
https://blog.csdn.net/CPP_MAYIBO/article/details/88074987
https://blog.csdn.net/maixia24/article/details/16964655
https://blog.csdn.net/fengge18306/article/details/105038728

https://blog.csdn.net/Doecy/article/details/812969

https://blog.csdn.net/cnkxy68446/article/details/100300236

https://blog.csdn.net/epitomizelu/article/details/117195218 https://blog.csdn.net/chengyuan2789/article/details/100839245 https://blog.csdn.net/weixin_46845300/article/details/110158613 https://blog.csdn.net/m0_37606374/article/details/115276726 https://www.cnblogs.com/smartloli/p/4443714.html https://blog.51cto.com/lawsonabs/3002924 https://www.cnblogs.com/yxym2016/p/14409083.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值