本文已同步发表于我的微信公众号,搜索 代码说 即可关注,欢迎与我沟通交流。

文章目录

  • 一、什么是序列化?为什么要序列化?怎么进行序列化?
  • 二、Serializable
    • 2.1 序列化举例
    • 2.2 重写readObject、writeObject、readResolve、writeReplace
    • 2.3 serialVersionUID
    • 2.4 实现原理
    • 2.5 Externalizable
  • 三、Parcelable
    • 3.1 序列化举例
    • 3.2 实现原理
  • 四、Parcelable、Serializable比较
    • 4.1 效率对比
    • 4.2 容错率对比
  • 五、总结
  • 六、参考

一、什么是序列化?为什么要序列化?怎么进行序列化?

序列化定义:将一个类对象转换成可存储、可传输状态的过程。序列化有两个过程:

1、序列化:将对象编码成字节流(serializing)
2、反序列化:从字节流编码中重新构建对象(deserializing)。对象序列化后,可以在进程内/进程间、网络间进行传输,也可以做本地持久化存储。

为什么要序列化: 系统底层并不认识对象,数据传输是以字节序列形式传递,以进程间通信为例,需要将对象转化为字节序列(字节序列中包括该对象的类型,成员信息等),然后在目标进程里通过反序列化字节序列,将字节序列转换成对象。

序列化方式:

  • Serializable(Java提供 后面简称为S)
  • Parcelable(Android特有 下面简称为P)

二、Serializable

S是Java API,是一个通用的序列化机制,可以将对象序列化并保存在本地或内存中。S是一个空接口:

public interface Serializable {}

S只起到了一个标识的作用,用于告知程序实现了Serializable的对象是可以被序列化的,但真正进行序列化和反序列化的操作是通过ObjectOutputStreamObjectInputStream实现的。

2.1 序列化举例

S_Shop.java:

public class S_Shop implements Serializable {private static final long serialVersionUID = -1399695071515887643L;public String mShopName;public int mShopId;public String mShopPhone;public static int STATIC_VALUE = 100;//静态值public transient int TRANSIENT_VALUE;//被transient修饰 不能序列化@NonNull@Overridepublic String toString() {return "Serializable: mShopName is " + mShopName+ ",mShopId is " + mShopId+ ",mShopPhone is " + mShopPhone+ ",STATIC_VALUE is " + STATIC_VALUE+ ",TRANSIENT_VALUE is " + TRANSIENT_VALUE;}
}

执行序列化和反序列化过程:

    public static void main(String[] args) throws IOException {//------------------Serializable------------------S_Shop shop = new S_Shop();shop.mShopName = "便利蜂";shop.mShopId = 2020;shop.mShopPhone = "18888888888";shop.TRANSIENT_VALUE = 1000;saveObject(shop); //序列化readObject();//反序列化}//序列化private static void saveObject(S_Shop shop) {ObjectOutputStream outputStream = null;try {outputStream = new ObjectOutputStream(new FileOutputStream("shop.obj"));outputStream.writeObject(shop);System.out.println("write-hashCode: " + shop.hashCode());outputStream.close();} catch (IOException e) {e.printStackTrace();} finally {if (outputStream != null) {try {outputStream.close();} catch (IOException e) {e.printStackTrace();}}}}private static void readObject() {//反序列化ObjectInputStream inputStream = null;try {inputStream = new ObjectInputStream(new FileInputStream("shop.obj"));S_Shop shop = (S_Shop) inputStream.readObject();System.out.println(shop.toString());System.out.println("read-hashCode: " + shop.hashCode());} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();} finally {if (inputStream != null) {try {inputStream.close();} catch (IOException e) {e.printStackTrace();}}}}

执行结果:

Serializable: mShopName is 便利蜂,mShopId is 2020,mShopPhone is 18888888888,STATIC_VALUE is 100,TRANSIENT_VALUE is 0

结果看到反序列化成功,从序列化结构中又重新生成了对象,这里注意一点,类中的变量TRANSIENT_VALUE是由transient修饰的,不能被序列化,所以反序列化时得到的是默认值。另外STATIC_VALUEstatic修饰,也不参与序列化过程。

2.2 重写readObject、writeObject、readResolve、writeReplace

一般来讲,只有当你自行设计的自定义序列化形式与默认的序列化形式基本相同时,才能接受默认的序列化形式。否则就要设计一个自定义的序列化形式,通过它合理地描述对象的状态。——《Effective Java》

Serializable实现自定义序列化必须重写readObject、writeObject方法,readResolve、writeReplace方法是可选的,看下面的例子:

public class S_Shop implements Serializable{private static final long serialVersionUID = -1399695071515887643L;public transient String mShopName;//注意mShopName是瞬态的public int mShopId;public String mShopPhone;/*** 序列化时执行 执行顺序早于writeObject 可以在此方法中做一些替换*/private Object writeReplace() {System.out.println("-----writeReplace() start-----");S_Shop shop = new S_Shop();shop.mShopName = "物美超市";//将mShopName替换shop.mShopId = mShopId;shop.mShopPhone = mShopPhone;return shop;}/*** 序列化时执行 通过defaultWriteObject将非transient字段序列化 也可以自定义序列化字段*/private void writeObject(ObjectOutputStream outputStream) throws IOException {System.out.println("-----writeObject() start-----");outputStream.defaultWriteObject();outputStream.writeObject(mShopName);}/*** 反序列化时执行 通过defaultReadObject将非transient字段反序列化 也可以将自定义字段反序列化*/private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {System.out.println("-----readObject() start-----");inputStream.defaultReadObject();mShopName = (String) inputStream.readObject();}/*** 反序列化时执行,执行顺序在readObject之后 可以在此方法中重新生成一个新对象*/private Object readResolve() {System.out.println("-----readResolve() start-----");S_Shop shop = new S_Shop();shop.mShopName = mShopName;shop.mShopId = mShopId;shop.mShopPhone = "12345678";//将mShopPhone替换return shop;}@NonNull@Overridepublic String toString() {return "Serializable: mShopName is " + mShopName+ ",mShopId is " + mShopId+ ",mShopPhone is " + mShopPhone;}
}

执行结果:

修改前:Serializable: mShopName is 便利蜂,mShopId is 2020,mShopPhone is 18888888888
-----writeReplace() start-----
-----writeObject() start-----
-----readObject() start-----
-----readResolve() start-----
修改后:Serializable: mShopName is 物美超市,mShopId is 2020,mShopPhone is 12345678

序列化过程的执行顺序:writeReplace->writeObject;反序列化过程的执行顺序:readObject->readResolve 通过上面四个方法,可以实现Serializable的自定义序列化。

:虽然上述的四个方法都是private级别的,但在反序列化过程中是通过反射执行的。

2.3 serialVersionUID

序列化会导致类的演变收到限制。这种限制与序列化唯一标识符serialVersionUID(后面简称sUID)有关,每个可序列化的类都有一个唯一标识号与它相关,sUID用来辅助序列化和反序列化的,序列化过程中会把类中的sUID写入序列化文件中。在反序列化时,检测序列化文件中sUID和当前类中的sUID是否一致,如果一致,才可以继续进行反序列化操作,否则说明序列化后类发生了一些改变,比如成员变量的类型发生改变等,此时是不能反序列化的。

是否需要指定serialVersionUID? 答案是肯定的,如果不指定sUID,在序列化时系统也会经过一个复杂运算过程,自动帮我们生成一个并写入序列化文件中。sUID的值受当前类名称、当前类实现的接口名称、以及所有公有、受保护的成员名称等所影响,此时即使当前类发生了微小的变化(如添加/删除一个不重要的方法)也会导致sUID改变,进而反序列化失败;如果指定了sUID,上述操作依然可以进行反序列化,但一些类结构发生改变,如类名改变、成员变量的类型发生了改变,此时即使sUID验证通过了,反序列化依然会失败。

2.4 实现原理

使用hexdump命令来查看上述生成的shop.obj二进制文件:

0000000 ac ed 00 05 73 72 00 1a 63 6f 6d 2e 65 78 61 6d
0000010 70 6c 65 2e 64 65 6d 6f 61 70 70 2e 53 5f 53 68
0000020 6f 70 ec 93 48 bf 94 6e 37 e5 02 00 03 49 00 07
0000030 6d 53 68 6f 70 49 64 4c 00 09 6d 53 68 6f 70 4e
0000040 61 6d 65 74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67
0000050 2f 53 74 72 69 6e 67 3b 4c 00 0a 6d 53 68 6f 70
0000060 50 68 6f 6e 65 71 00 7e 00 01 78 70 00 00 07 e4
0000070 74 00 09 e4 be bf e5 88 a9 e8 9c 82 74 00 0b 31
0000080 38 38 38 38 38 38 38 38 38 38
000008a
  • AC ED: STREAM_MAGIC. 声明使用了序列化协议.
  • 00 05: STREAM_VERSION. 序列化协议版本.
  • 0x73: TC_OBJECT. 声明这是一个新的对象.
  • 0x72: TC_CLASSDESC. 声明这里开始一个新Class
  • 00 1a: Class名字的长度.

序列化步骤:

  • 将对象实例相关的类元数据输出
  • 递归地输出类的超类描述直到不再有超类
  • 类元数据完了之后,开始从最顶层的超类开始输出对象实例的实际数据值
  • 从上至下递归输出实例的数据

Serializable 的序列化与反序列化分别通过 ObjectOutputStreamObjectInputStream 进行,都是在Java层实现的。两个相关概念:
ObjectStreamClass: 序列化类的描述符。它包含类的名称和serialVersionUID。它由Java VM加载,可以使用lookup方法找到或创建。
ObjectStreamField: 类(可序列化)的可序列化字段的描述。ObjectStreamFields数组用于声明类的可序列化字段。

1、序列化过程(writeObject方法)

  • 通过ObjectStreamClass记录目标对象的类型、类名等信息,内部有个ObjectStreamFields数组,用来记录目标对象的内部变量(内部变量可以是基本类型,也可以是自定义类型,但是必须都支持序列化—必须是S不能是P)。
  • 首先通过ObjectStreamClass.lookup()找到或创建ObjectStreamClass,然后调用defaultWriteFields方法,在方法中通过getPrimFieldValues()获取基本数据类型并赋值到primVals(byte[]类型)中,再通过getObjFieldValues()获取到自定义对象(通过Unsafe类实现而不是反射)并赋值到objVals(Object[]类型)中,接着遍历objVals数组,然后递归调用writeObject方法重复上述操作。
  • 调用过程:writeObject() -> writeObject0()-> writeOrdinaryObject() -> writeSerialData() -> invokeWriteObject() -> defaultWriteFields()

2、反序列化过程(readObject方法)

  • 通过readClassDescriptor()读取InputStream里的数据并初始化ObjectStreamClass类,再根据这个实例通过反射创建目标对象实例。
  • 调用过程:readObject() -> readObject0() -> readOrdinaryObject() -> readSerialData() -> defaultReadFields()

Serializable常见异常

异常名称 异常起因
java.io.InvalidClassException 1、序列化时自动生成serialVersionUID,此时改变类名、类实现的接口名、内部成员变化或添加/删除方法,都会导致serialVersionUID改变,反序列化时就会抛出此异常
java.io.NotSerializableException 当前类或类中成员变量未实现序列化

2.5 Externalizable

Externalizable 继承自Serializable,并在其基础上添加了两个方法:writeExternal()readExternal()。这两个方法在序列化和反序列化时会被执行,从而可以实现一些特殊的需求(如指定哪些元素不参与序列化,作用等同于transient)。如果说默认的Serializable序列化方式是自动序列化,那么Externalizable就是手动序列化了,通过writeExternal()指定参与序列化的内部变量个数,然后通过readExternal()反序列化重新生成对象。

public class S_Shop_External implements Externalizable {private static final long serialVersionUID = -61368254488136487L;public String mShopName;public int mShopId;public String mShopPhone;public S_Shop_External() {System.out.println("S_Shop_External()构造方法");}@Overridepublic void writeExternal(ObjectOutput out) throws IOException {out.writeObject(mShopName);out.writeInt(mShopId);out.writeObject(mShopPhone);}@Overridepublic void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {mShopName = (String) in.readObject();mShopId = in.readInt();mShopPhone = (String) in.readObject();}@NonNull@Overridepublic String toString() {return "Serializable: mShopName is " + mShopName+ ",mShopId is " + mShopId+ ",mShopPhone is " + mShopPhone;}
}

执行代码跟Serializable一样,只是将对象变成了S_Shop_External,执行结果:

Externalizable: mShopName is 便利蜂,mShopId is 2020,mShopPhone is 18888888888

readExternal()中得到的数据都是在writeExternal()中写入的数据。

Externalizable常见异常

异常名称 异常起因
java.io.InvalidClassException: no valid constructor 反序列化时,必须要有修饰符为public的默认构造参数
java.io.OptionalDataException:readExternal 反序列化时readExternal添加元素、删除末尾之外的元素、修改元素类型

三、Parcelable

P是Android SDK API,其序列化操作完全由底层实现,可以在进程内、进程间(AIDL)高效传输数据。不同版本的API实现方式可能不同,不宜做本地持久化存储。

3.1 序列化举例

P_Shop.java:

public class P_Shop implements Parcelable {public P_Shop(){}public String mShopName;public int mShopId;public String mShopPhone;public static int STATIC_VALUE = 100;//静态值public transient int TRANSIENT_VALUE;//被transient修饰 不能序列化/*** 从序列化结构中创建原始对象*/protected P_Shop(Parcel in) {mShopName = in.readString();mShopId = in.readInt();mShopPhone = in.readString();}/*** 反序列化*/public static final Creator<P_Shop> CREATOR = new Creator<P_Shop>() {/*** 从序列化对象中创建原始对象*/@Overridepublic P_Shop createFromParcel(Parcel in) {return new P_Shop(in);}/*** 创建指定长度的原始对象数组*/@Overridepublic P_Shop[] newArray(int size) {return new P_Shop[size];}};/*** 序列化:将当前对象写入序列化结构中*/@Overridepublic void writeToParcel(Parcel dest, int flags) {dest.writeString(mShopName);dest.writeInt(mShopId);dest.writeString(mShopPhone);}/*** 当前对象的内容描述,存在文件描述符时返回1  其余全返回0*/@Overridepublic int describeContents() {return 0;}@NonNull@Overridepublic String toString() {return "Parcelable: mShopName is " + mShopName+ ",mShopId is " + mShopId+ ",mShopPhone is " + mShopPhone+ ",STATIC_VALUE is " + STATIC_VALUE+ ",TRANSIENT_VALUE is " + TRANSIENT_VALUE;}
}

注意createFromParcel()writeToParcel()方法中对应变量读写的顺序必须是一致的,否则序列化会失败。

Parcel处理工具:

public class PUtil {private static final String SP_NAME = "sp_parcel";private static final String PARCEL_KEY = "parcel_key";//marshall Parcel将自身保存的数据以byte数组形式返回public static byte[] marshall(Parcelable parcelable) {Parcel parcel = Parcel.obtain();parcel.setDataPosition(0);parcel.writeValue(parcelable);byte[] bytes = parcel.marshall();parcel.recycle();return bytes;}//将bytes经过base64转换成字符串并存储到sp中public static void save(Context context, byte[] bytes) {SharedPreferences preferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);SharedPreferences.Editor editor = preferences.edit();String saveStr = Base64.encodeToString(bytes, 0);editor.putString(PARCEL_KEY, saveStr);editor.apply();}//从sp中取出字符串并转换成bytes 然后bytes->Parcel->Objectpublic static Object getParcel(Context context) {SharedPreferences preferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);byte[] bytes = Base64.decode(preferences.getString(PARCEL_KEY, "").getBytes(), Base64.DEFAULT);//从bytes中获取ParcelParcel parcel = unmarshall(bytes);return parcel.readValue(context.getClassLoader());}//从byte数组中获取数据,存入自身的Parcel中private static Parcel unmarshall(byte[] bytes) {Parcel parcel = Parcel.obtain();parcel.unmarshall(bytes, 0, bytes.length);parcel.setDataPosition(0);return parcel;}
}

执行序列化/反序列化:

        //------------------Parcelable------------------Context context = this;P_Shop shopP = new P_Shop();shopP.mShopName = "便利蜂";shopP.mShopId = 2020;shopP.mShopPhone = "18888888888";shopP.TRANSIENT_VALUE = 1000;//序列化过程byte[] bytes = PUtil.marshall(shopP);//Parcel->bytes[]PUtil.save(context, bytes);//保存bytes[]//反序列化过程Object object = PUtil.getParcel(context);//bytes[]->Parcel->Objectif (object == null) return;if (object instanceof P_Shop) {P_Shop shop = (P_Shop) object;Log.e("TTT", shop.toString());}

执行结果:

Parcelable: mShopName is 便利蜂,mShopId is 2020,mShopPhone is 18888888888,STATIC_VALUE is 100,TRANSIENT_VALUE is 0

3.2 实现原理

P序列化过程中会用到ParcelParcel可以被认为是一个包含数据或者对象引用的容器,能够支持序列化及在跨进程之后的反序列化。P的序列化操作在Native层实现,通过write内存写入及read读内存数据重新生成对象。P将对象进行分解,且分解后每一部分都是支持可传递的数据类型。

序列化过程(Parcelable的写过程)

调用过程Parcel.writeValue()->writeParcelable(),下面主要来看下此方法:

 public final void writeParcelable(@Nullable Parcelable p, int parcelableFlags) {if (p == null) {writeString(null);return;}//1、先写入序列化类名writeParcelableCreator(p);//2、调用类中复写的writeToParcel方法按顺序写入p.writeToParcel(this, parcelableFlags);}//写入序列化类名public final void writeParcelableCreator(@NonNull Parcelable p) {String name = p.getClass().getName();writeString(name);}

序列化过程中,首先写入序列化类名,然后调用类中复写的writeToParcel()方法依次写入

反序列化过程(Parcelable的读过程)

调用过程:Pacel.readValue()->readParcelable()

    public final <T extends Parcelable> T readParcelable(@Nullable ClassLoader loader) {//1、通过反射或缓存获取序列化类中的CREATORParcelable.Creator<?> creator = readParcelableCreator(loader);if (creator == null) {return null;}if (creator instanceof Parcelable.ClassLoaderCreator<?>) {Parcelable.ClassLoaderCreator<?> classLoaderCreator =(Parcelable.ClassLoaderCreator<?>) creator;return (T) classLoaderCreator.createFromParcel(this, loader);}//2、调用CREATOR中的createFromParcel进行反序列化return (T) creator.createFromParcel(this);}private static final HashMap<ClassLoader,HashMap<String,Parcelable.Creator<?> mCreators = new HashMap<>();public final Parcelable.Creator<?> readParcelableCreator(@Nullable ClassLoader loader) {//1、首先读取之前写入的类名String name = readString();if (name == null) {return null;}Parcelable.Creator<?> creator;synchronized (mCreators) {//如果之前某个classLoader缓存过Parcelable的Creator,然后通过mCreators缓存过,//那么直接从缓存取;否则通过反射去加载并加入缓存中HashMap<String,Parcelable.Creator<?>> map = mCreators.get(loader);if (map == null) {map = new HashMap<>();mCreators.put(loader, map);}creator = map.get(name);if (creator == null) {try {ClassLoader parcelableClassLoader =(loader == null ? getClass().getClassLoader() : loader);//通过反射去获取Parcelable中的CREATORClass<?> parcelableClass = Class.forName(name, false /* initialize */,parcelableClassLoader);Field f = parcelableClass.getField("CREATOR");Class<?> creatorType = f.getType();creator = (Parcelable.Creator<?>) f.get(null);}map.put(name, creator);}}return creator;}

四、Parcelable、Serializable比较

4.1 效率对比

S序列化和反序列化会经过大量的I/O操作,产生大量的临时变量引起GC;P是基于内存实现的封装和解封(marshalled& unmarshalled),效率比S快很多。

下面的测试来自非官方测试,通过ParcelableSerializable分别执行序列化/反序列化过程,循环1000次取平均值,实验结果如下:

数据来自 parcelable-vs-serializable,实验结果对比Parcelable的效率比Serializable快10倍以上。

4.2 容错率对比

序列化到本地时,新版本字段改变对旧版本反序列化的影响

改变字段 默认的Serializable序列化方式 Externalizable Parcelable
增加字段 ✔️ 追加到末尾:✔️ 其他:❌
删除字段 ✔️ 删除末尾:✔️ 其他:❌
修改字段类型

总结:

  • Externalizable中,writeExternal参与序列化,readExternal参与的是反序列化。readExternal()中读入的元素一定是writeExternal()中写入过的,且读写的顺序、字段类型要一致。另外,readExternal中的元素可以少于writeExternal中的,但是注意少的元素一定是在末尾的元素(即不能删除前面的元素),否则反序列化就会失败。
  • 对于Parcelable来说,如果新版本中修改字段类型,那么该字段反序列化时会失败;如果是添加字段,那么反序列化时在添加字段位置到末尾位置都会失败;同样删除字段,反序列化时在删除字段的位置到末尾位置都会失败。

五、总结

对比 Serializable Parcelable
所属API Java API Android SDK API
特点 序列化和反序列化会经过大量的I/O操作,产生大量的临时变量引起GC,且反序列化时需要反射 基于内存拷贝实现的封装和解封(marshalled& unmarshalled),序列化基于Native层实现,不同版本的API实现可能不同
开销 相对高 相对低
效率 相对低 相对高
适用场景 序列化到本地、网络传输 主要内存序列化

另外序列化过程中的几个注意点:

  • 下面两种成员变量不会参与到默认序列化过程中:
    1、static静态变量属于类而不属于对象
    2、transient标记的成员变量
  • 参与序列化的成员变量本身也是需要可序列化的
  • 反序列化时,非可序列化的(如被transient修饰)变量将会调用自身的无参构造函数重新创建,因此也要求此成员变量的构造函数必须是可访问的,否则会报错。

六、参考

【1】Android 面试(七):Serializable 这么牛逼,Parcelable 要你何用?
【2】每日一问 Parcelable 为什么效率高于 Serializable ?
【3】Android中两种序列化方式的比较Serializable和Parcelable
【4】Android之序列化详解
【5】Android 开发之漫漫长途 X——Android序列化

Android | 序列化Serializable/Parcelable 使用总结相关推荐

  1. android 序列化传参数,Android序列化之Parcelable和Serializable的使用详解

    序列化与反序列 首先来了解一下序列化与反序列化. 序列化 由于存在于内存中的对象都是暂时的,无法长期驻存,为了把对象的状态保持下来,这时需要把对象写入到磁盘或者其他介质中,这个过程就叫做序列化. 反序 ...

  2. Android 序列化(Serializable和Parcelable),kotlin开发windows程序

    Parcelabel的实现,不仅需要实现Parcelabel接口,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator 接口,并实现读写的抽象方法.如 ...

  3. Android 序列化(Serializable和Parcelable),android开发板底层开发

    实现Parcelable接口

  4. Android 序列化(Serializable和Parcelable)

  5. android 保存 parcelable对象,Android 使用序列化Serializable和Parcelable

    Android 序列化Serializable和Parcelable使用和区别 一:Serializable 1.什么是序列化 将一个类对象转换成可存储,可传输状态的过程. 2.什么是Serializ ...

  6. Android序列化:Serializable Parcelable

    原文出处:http://blog.csdn.net/jdsjlzx/article/details/51122109?locationNum=14&fps=1 对于Parcel的理解: 在An ...

  7. Android的序列化(Serializable和Parcelable)

    [齐天的博客]转载请注明出处(万分感谢!): https://blog.csdn.net/qijinglai/article/details/80813423 前言 Android中要实现对象持久化或 ...

  8. android 序列化 xml serializable,关于Android中的序列化Serializable和Parcelable的学习

    简单地说,"序列化"就是将运行时的对象状态转换成二进制,然后保存到流,内存或者通过网络传输给其他端. 两者最大的区别在于 存储媒介的不同,Serializable使用 I/O 读写 ...

  9. Android 进阶——持久化存储序列化方案Serializable和IPC及内存序列化方案Parcelable详解与应用

    文章大纲 引言 一.文件的本质 二.序列化和反序列化概述 1.序列化和反序列化的定义 2.序列化和反序列化的意义 三.Serializable 1.Serializable 概述 2.JDK中序列化和 ...

最新文章

  1. WindowsPhone设置启动欢迎页面
  2. [YTU]_2008( 简单编码)
  3. 包邮送书 50 本,你还有什么理由不上进?
  4. R语言数据转换(split-apply-combin…
  5. 关于java包_关于Java包
  6. (原创)SpringBoot入门
  7. Flink CDC 实时同步mysql
  8. (转)淘淘商城系列——zookeeper单机版安装
  9. 什么样的运营才是好运营?
  10. react调用api等待返回结果_React新Context API在前端状态管理的实践
  11. .net remoting学习(3) ---配置与服务端广播
  12. linux 黑苹果 win7双系统,学习笔记:安装黑苹果和win双系统(基础篇)
  13. PDF阅读器开发商福昕曝出数据泄露事件,涉及用户帐户密码
  14. java 聊天室源代码_java聊天室源码(含客户端、服务端)
  15. 射频中的回波损耗,反射系数,电压驻波比以及S参数的含义
  16. 手写文本 matlab 识别,手写汉字识别matlab
  17. SAPnbsp;BORnbsp;--nbsp;…
  18. dell r720xd 裸机配置系列 3 配置网络
  19. MX550性能怎么样 mx550 属于什么档次的显卡
  20. Excel表格中输入一个姓,就可以选择输入需要的姓名了

热门文章

  1. MATLAB中字符串数组的文件输出
  2. Altium Designer导出Gerber文件的一般步骤
  3. 如何使用计算机蓝色,电脑中如何用Word的红绿蓝三原色配出天蓝色?
  4. python学习课件
  5. maven基础:mvn命令常用参数整理;如:-am构建指定模块,同时构建指定模块依赖的其他模块
  6. 用js进行日期的加减
  7. 放大镜 讲课_放大镜说课稿
  8. Java如何获取当前系统时间
  9. RDkit二:利用RDkit筛选进行化学小分子2D药效团筛选
  10. linux版docker安装镜像