【Java】关于序列化的事和Gson工具以及泛型


序列化

序列化:将对象可以写入IO流中,以便于传输和存储
反序列化:从IO流中恢复成序列化之前的对象,以便从流中得到数据进行解析
意义:可以将Java对象转换为字节序列,就方便保存在磁盘或者网络传输了,也可以将已存在的字节序列恢复为原来的对象。所有在网络上传输的对象和在磁盘中保存的对象都需要序列化。

什么是序列化

  序列化是一种将对象以一连串的字节描述的过程;
  Java 平台允许我们在内存中创建可复用的Java 对象,使用Java 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。反序列化是一种将这些字节重建成一个对象的过程。

序列化的必要性及应用

对象的序列化主要有两种用途:
(1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
(2) 在网络上传送对象的字节序列。

  当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java 对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java 对象。

  Java 序列化机制就是为了解决这个问题而产生。

Java 中序列化ID的作用

  简单来说,Java 的序列化机制是通过在运行时判断类的serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的serialVersionUID 与本地相应实体(类)的serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。

关键字transient

  transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient修饰的变量的值被设为初始值,如int 型的是0,对象型的是null。

给如下例子可以清晰感受到序列化的意义。

用户类

public class UserInfo {

	private String id;
	private String password;
	private int age;
	private String name;
	
	public UserInfo() {
	}

	/*相关的setter和getter省略*/
	public UserInfo(String id, String password, int age, String name) {
		this.id = id;
		this.password = password;
		this.age = age;
		this.name = name;
	}

	@Override
	public String toString() {
		return "UserInfo [id=" + id + ", password=" + password + ", age=" + age + ", name=" + name + "]";
	}
		
}

把类产生的对象写到文件里去。需要借助ObjectOutputStream这个类。

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerialDemo {
	public static void main(String[] args) {
		String path = "./tag";
		String fileName = "user.inf";
		File fileDir = new File(path);	
		if (!fileDir.exists()) {
			fileDir.mkdirs();
		}
		File file = new File(fileDir, fileName);
		if (!file.exists()) {
			try {
				file.createNewFile();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		try {
			UserInfo user = new UserInfo("123456", "qwer", 20, "张三");
			FileOutputStream fos = new FileOutputStream(file);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(user);
			oos.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

但是出现了异常。
在这里插入图片描述
说我们的UserInfo类没有序列化,同时可以说明ObjectOutputStreamwriteObject方法可以让我们将对象向文件或者网络写。但是写的这个对象的类必须有序列号这个属性。

那我们就给相关类加上序列号。

public class UserInfo implements Serializable {

	private static final long serialVersionUID = -3696056911082690713L;
	/*下面内容省略*/
}

再去运行我们的SerialDemo,没有发生异常,去查看相关文件,用UltraEdit打开。
在这里插入图片描述
文件用138字节仅仅才描述了一个对象的信息。可以仔细观察文件详细内容,确实有id,password,age,name等信息。但最重要的是,用红框勾出来的,我们将刚才产生的序列号转化为16进制,发现在写入的文件中也找到了这串序列号,那么这个序列号到底用来干什么呢?下面继续做实验。我们从写好的文件读取并形成对象。

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ReSerialDemo {

	public static void main(String[] args) {

		File file = new File("./tag/user.inf");
		try {
			FileInputStream fis = new FileInputStream(file);
			ObjectInputStream ois = new ObjectInputStream(fis);
			UserInfo user = (UserInfo) ois.readObject();
			System.out.println(user);
			ois.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

}
/*UserInfo [id=123456, password=qwer, age=20, name=张三]*/

确实准确的从文件中读取我们刚才序列化的对象。但是,如果将序列号一改呢?将对象写文件的时候是一个序列号,读取文件获得对象是另一个序列号会怎么样呢?继续实验。

public class UserInfo implements Serializable {
	private static final long serialVersionUID = -1L;
}

再去运行ReSerialDemo,出现了异常,说流中的序列号和本地类的序列号不匹配。

java.io.InvalidClassException: com.mec.serial.test.UserInfo; local class incompatible: stream classdesc serialVersionUID = -3696056911082690713, local class serialVersionUID = -1
	at java.base/java.io.ObjectStreamClass.initNonProxy(Unknown Source)
	at java.base/java.io.ObjectInputStream.readNonProxyDesc(Unknown Source)
	at java.base/java.io.ObjectInputStream.readClassDesc(Unknown Source)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
	at java.base/java.io.ObjectInputStream.readObject0(Unknown Source)
	at java.base/java.io.ObjectInputStream.readObject(Unknown Source)
	at com.mec.serial.test.ReSerialDemo.main(ReSerialDemo.java:17)

从这里我们分析可以得出序列号为了ObjectInputStreamreadObject()来用的。就是读了这文件的这么多信息,这些信息如何转为一个userinfo对象,读入的这些数据都是二进制数据,这些数据需要转换回成对象。其中这个号就是用来唯一标识这个类的,区别于其他的类。

把对象转换为纯二进制,把纯二进制转换为对象。就需要一个序列号,这个序列号的目的就是为了准确定位一个类。

通过上述例子想讲明一个问题,当我们把一个对象向外写到流中时,必须要先建立序列号,而且写的内容包含序列号,这个序列号成为识别这段二进制的重要的信息。

在网络流中写对象。

Socket socket = new Socket();
ObjectOutputStream netOos = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream netOis = new ObjectInputStream(socket.getInputStream());

但是我们在网络上传输消息,发送一个对象需要发送刚才UE看到的那么多二进制消息,内部效率相较于下面的方式十分低。

尽量把对象转换成JSON保存更为稳妥。

JSON

JSON概述

  • JSON指的是JavaScript对象的表示法(JavaScript Object Notation)。
  • 是轻量级的文本数据交换格式。在网络传输中,客户端和服务器两个之间传输数据。
  • 具有自我描述性,更容易理解
  • 虽然使用JavaScript语法来描述数据对象,但是JSON任然独立于语言和平台。JSON解析器和JSON库支持许多不同的编程语言。

综上,JSON是独立于语言,可以作为存储数据和交换数据的格式。从这功能我们就不得不想到XML。

JSON与XML比较

作为配置文件:XML因为具有规范的标签,所以更能得知层次结构,JSON却不行。(XML优于JSON

作为数据交换和存储:同样描述一个对象,JSON比XML更小,更快,更容易解析,网络传输更省带宽。(JSON优于XML

JSON语法规则

  • 数据在名称/值对中
  • 数据由逗号分隔
  • 花括号保存对象
  • 方括号保存数组

JSON的值可以是数字(整数或者浮点数),字符串(双引号中),逻辑值(true/false),数组(方括号中),对象(花括号中),null。

例子
在这里插入图片描述
例子的视图
在这里插入图片描述

Gson

Gson概念及基本用法

  Gson是Google公司提供的用来在Java对象和JSON数据之间进行映射的Java类库。主要用途为序列化Java对象为JSON字符串,或反序列化JSON字符串成Java对象。

基本用法:提供了两个方法,toJson生成JSON字符串,fromJson解析JSON字符串生成对象。

例子:需要gson的jar包

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.mec.serial.test.UserInfo;

public class GsonDemo {

	public static void main(String[] args) {

		UserInfo user = new UserInfo("123456", "qwer", 20, "张三");
		Gson gson = new GsonBuilder().create();
		String userGsonStr = gson.toJson(user);
		System.out.println("生成的JSON字符串:" + userGsonStr);
		UserInfo newUser = gson.fromJson(userGsonStr, UserInfo.class);
		System.out.println("由Gson字符串解析得到的对象:" + newUser);
	}

}

/*
生成的JSON字符串:{"id":"123456","password":"qwer","age":20,"name":"张三"}
由Gson字符串解析得到的对象:UserInfo [id=123456, password=qwer, age=20, name=张三]
*/

我有个对象,想发送给网络远端,发送字符串,readUTF,writeUTF,我希望对端接收到这个字符串能很方便的转换回成原对象。Gson就起到了这个作用,避免我们来回解析了。不仅如此,任意的,复杂的类型都可以处理,这个工具很强悍。

由Gson产生的工具ArgumentMaker

我们知道方法的调用需要参数,假如方法的参数从远端传来,我们应该怎样将它管理和传输呢?例如userLogin(String id,String password)

仔细观察方法,多仔细观察每种方法的共同点,处处都是映射/键值对。我们可以键是形参的名字,值是参数真正的值,构成键值对的一个map,再把map对象变成JSON字符串,就方便传输了!

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

public class ArgumentMaker {

	private Map<String, String> argumentMap;	//键是参数名字,值是参数值所形成的JSON对象!
	public static final Gson gson = new GsonBuilder().create();
	private static final Type mapType = new TypeToken<Map<String, String>> () {}.getType();
	//引入了泛型之后虽然要多写一句话用于获取泛型信息 TypeToken
	
	public ArgumentMaker() {
		argumentMap = new HashMap<>();
	}
	
	public ArgumentMaker addArg(String name, Object value) {
		argumentMap.put(name, gson.toJson(value));	//第一次toJson,将参数的值转为JSON字符串放到map去
		return this;
	}
	
	@Override
	public String toString() {
		return gson.toJson(argumentMap);//第二次toJson,将map转换为JSON字符串,方便传输
	}
	
	/*上述只完成一半,对象->JSON字符串,接下里完成解析*/
	
	/*字符串反向解析成Map,用到了Gson高级用法TypeToken*/
	public ArgumentMaker(String parameter) {
		this.argumentMap = gson.fromJson(parameter, mapType);
	}
	
	/*Type可以处理泛型,避免泛型擦除问题*/
	public Object getArgumentByName(String name, Type type) {
		String json = argumentMap.get(name);
		return gson.fromJson(json, type);
	}

	/*Class只能处理普通类型,不能处理泛型*/
	public Object getArgumentByName(String name, Class<?> klass) {
		String json = argumentMap.get(name);
		return gson.fromJson(json, klass);
	}
	
}

用法

public class ArgumentMakerDemo {

	public static void main(String[] args) {

		//对于发送userLogin(String id, String password)发送端就可以这样构造参数
		String argumentMakerStr = new ArgumentMaker()
				.addArg("id", "id的值")
				.addArg("password", "password的值").toString();
		System.out.println(argumentMakerStr);
		
		//对于接收端接收到argumentMakerStr,可以这样解析
		ArgumentMaker argumentMaker = new ArgumentMaker(argumentMakerStr);
		String id = (String) argumentMaker.getArgumentByName("id", String.class);
		System.out.println("收到的id的值:" + id);
		String password = (String) argumentMaker.getArgumentByName("password", String.class);
		System.out.println("收到的password的值:" + password);
	}

}
/*
{"password":"\"password的值\"","id":"\"id的值\""}
收到的id的值:id的值
收到的password的值:password的值
*/

关于ArgumentMaker的问题与回答。

Q:为什么参数的值需要转换成JSON(为什么要进行第一次JSON转换)?

A:不同的方法参数不尽相同,类型也是不尽相同的。值的类型是不确定的,模糊的,抽象的,以后用的时候,才能确定。要是直接写Object那就惨了,不安全,容易遭受破坏。Object是所有类的基类,它的能力越大,责任就越大,越容易遭人破坏,捣乱,随意放东西进去。(工具保护思想

Q:引入Gson高级用法TypeToken解决泛型擦除问题。

A:解释这个问题就要讲泛型和泛型擦除了(在下面)。然后还有Gson解决在运行时获得泛型信息的类TypeToken。

private static final Type mapType = new TypeToken<Map<String, String>> () {}.getType();

如上面所看到的,创建一个TypeToken的匿名继承类。由于匿名类的申明信息中保留了泛型信息,通过反射可得。

具体的如何操作需要转到这篇博文去看。具体如何获取泛型信息。他的例子很好,讲到了字节码的深度,对于初入Java的我现在是不能理解的,只能以后等见的多了再去了解。

所以Google给我们的Gson工具太强大了,我们要好好膜拜大佬。

泛型

  泛型是JDK1.5的一项新增特性,它的本质是参数化类型ParameterizedType),也就是说所操作的数据类型被指定为一个参数。这种参数类型可被应用于类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。在泛型出现之前,Java是通过Object是所有类型的父类和类型强制转换这两个特性来实现类型泛化的。 Java语言引入泛型的好处是安全简单(少了强制类型转换)。

  泛型通俗的来说就是为了把类型当成参数传递给一个类或者方法。

泛型擦除

首先给一个最经典的例子

import java.util.ArrayList;
import java.util.List;

public class Demo {

	public static void main(String[] args) {

		List<String> sList = new ArrayList<>();
		List<Integer> iList = new ArrayList<>();
		
		System.out.println(sList.getClass() == iList.getClass());
	}

}

  上述测试结果输出的为true?你也许会很疑惑,明明两个放不同类型的List,它们的class怎么会一样呢?

  造成这样的结果就是因为泛型擦除泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,JVM并不知道泛型的存在。所以不管是 ArrayList<Integer> 还是 ArrayList<String>,在编译完成后都会被编译器擦除成了 ArrayList

擦除具体规则:

  • 若泛型类型没有指定具体类型,用Object作为原始类型;
  • 若有限定类型< T exnteds Class1 >,使用Class1作为原始类型;
  • 若有多个限定< T exnteds Class1 & Class2 >,使用第一个边界类型Class1作为原始类型;

  Java 泛型擦除是 Java 泛型中的一个重要特性,其目的是避免过多的创建类而造成的运行时的过度消耗。所以, ArrayList<Integer >ArrayList<String> 这两个实例,其类实例是同一个。

泛型擦除的应用

对于一些编译型错误的问题(例如:List list = new ArrayList<>( ),如何把Activity对象放进list集合里去?),可以先略过,用反射机制进行。

举个例子吧。

public class Demo {

		public static void main(String[] args) {
	
			List<String> sList = new ArrayList<>();
			
			sList.add("abc");
			sList.add(456);//错误。The method add(int, String) in the type List<String> is not applicable for the arguments (int)
			
		}

}

也正是因为泛型擦除:泛型信息是在编译前起作用的,编译后,对于运行时,就不起作用了。只要让他跳过编译阶段,我们可以利用反射绕过编译器去调用相关方法。

上述问题这么改就解决了

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class Demo {

		public static void main(String[] args) {
	
			List<String> sList = new ArrayList<>();
			
			sList.add("abc");
			Class<?> klass = sList.getClass();
			try {
				Method method = klass.getMethod("add", Object.class);	//根据擦除规则,可知是Object
				method.invoke(sList, 456);
				for (Object o : sList) {				//遍历的时候也只可以for each了,因为get方法返回值是泛型,int 不能强转为String
					System.out.println(o);
				}
			} catch (NoSuchMethodException e) {
				e.printStackTrace();
			} catch (SecurityException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值