1. Stream(流) Category(分类)
数据单位不同:字节流(8 bit,处理字节,如图片视频)/字符流(16 bit,处理文本)
流的流向不同:输入流/输出流(在程序的角度看待流的流向)
流的角色不同:节点流(也叫“文件流”,直接作用于文件上)/处理流(在节点流之上又装饰一层流)
抽象基类
字节流
字符流
输入流
InputStream
Reader
输出流
OutputStream
Writer
2. 节点流
直接作用在文件上的流就是节点流,也叫做文件流,java.io包下有四个类是节点流。
FileInputStream
FileOutputStream
FileReader
FileWriter
3. 处理流
处理流作用在基本流之上,可以额外的添加一些功能,比如一开始使用FileReader读入文件,如果在FileReader之上再包装一层缓冲流BufferReader可以增加我呢见的读取速度。这种嵌套包装的方式称作装饰者模式。
4. 标准输入、输出流
在不同编程语言中,标准输入/输出都是指键盘(标准输入)、显示器或控制台(标准输出)。再Java中标准输入输出对应了两个流:
标准输入:System.in -> InputStream
标准输出:System.out -> OutputStream
那么标准输入输出有什么用呢?Java编程中我们通过要获取键盘输出,有两种方式:
Scanner
使用标准输入流
方式二代码:
public class test1 {
public static void main(String[] args) {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr);
try {
// br.readLine()方法会阻塞住,直到有键盘输入
String s = br.readLine();
System.out.println(s);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3. flush()方法作用
flush()方法是在类java.io.OutputStream中的,所有的输出流都有这个方法,作用是:刷新缓冲区。
image
上图中,web服务器通过一个输出流向客户端发送300字节的信息,但是缓冲区的大小有1024字节,默认情况下输出流会等待缓冲区填满后再一次性把缓冲区全部数据发送给客户端,所有此时300字节的信息太小,服务器就不会发送,客户端也就接受不带消息。而调用输出流的flush()函数会强制刷新缓冲区,将缓冲区的数据发送出去。
4. 对象流实现Java对象序列化和反序列化
/**
* @description: 序列化目标类
* @author: sanjin
* @date: 2019/7/6 18:59
*/
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
/**
* @description: 进行序列化
* @author: sanjin
* @date: 2019/7/6 18:59
*/
public class 对象序列化 {
public static void main(String[] args) {
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("P:\\Projects\\Java\\Demos\\spring-framework\\my-spring-demo\\src\\main\\java\\com\\sanjin\\blog02\\java对象序列化\\person.txt"));
Person perosn = new Person("小白", 11);
oos.writeObject(perosn);
oos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos!=null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* @description: 从文本反序列化出对象
* @author: sanjin
* @date: 2019/7/6 19:08
*/
public class 反序列化对象 {
public static void main(String[] args) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("P:\\Projects\\Java\\Demos\\spring-framework\\my-spring-demo\\src\\main\\java\\com\\sanjin\\blog02\\java对象序列化\\person.txt"));
Person object = (Person) ois.readObject();
System.out.println(object);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (ois !=null){
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
序列化的意义在于通过序列化将对象能够持久化到硬盘中,在需要的时候在通过反序列化将对象从硬盘中加载到内存,比如我们在使用redis,MQ时候就需要考虑序列化问题。
序列化中的serialVersionUID
如果要序列化某个对象,该对象就必须实现java.io.Serializable接口才能被序列化,
使用该接口后,我们通常还会添加一个字段:
private static final long serialVersionUID = 1L// 值可以改变
那么这个字段有什么作用呢?
serialVersionUID用来表明类的不同版本之间的兼容性。
简单来说,就是为了解决这么一个问题?
之前我们的Person类是这样的:
/**
* @description: 序列化目标类
* @author: sanjin
* @date: 2019/7/6 18:59
*/
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
我们显示的声明了serialVersionUID,然后使用ObjectOutputStream对其进行序列化,然后下一步我们修改这个类,修改后如下:
/**
* @description: 序列化目标类
* @author: sanjin
* @date: 2019/7/6 18:59
*/
public class Person implements Serializable {
private static final long serialVersionUID = 545641L;
private String name;
private Integer age;
private int id;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public Person(String name, Integer age, int id) {
this.name = name;
this.age = age;
this.id = id;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", id=" + id +
'}';
}
}
可以看到我在Person类中添加了一个Id字段,并且为这个字段添加了一个三个参数的构造器。那么此时,我们还能反序列化出这个Persion对象么?如果能正确序列化,那么Person对象有没有这个id值呢?
结果如下:
1.png
可以看到,我们在对Person添加了一个id字段,还是可以完成反序列化,并且id字段的值是int类型的默认值。这是我们手动给定了一个serialVersionUID的情况,那么我们不手动指定会出现什么情况呢?
再次修改Person类:
/**
* @description: 序列化目标类
* @author: sanjin
* @date: 2019/7/6 18:59
*/
public class Person implements Serializable {
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
可以看到我仅仅只是删除了serailVersionUID字段,实现序列化接口后,如果不手动指定serialVersionUID,那么Java在运行时环境会根据类的内部细节自动生成,这就意味着我们修改Person类会生成不同的serialVersionUID。
修改Person后进行序列化。
再次修改Person类,添加id字段。
/**
* @description: 序列化目标类
* @author: sanjin
* @date: 2019/7/6 18:59
*/
public class Person implements Serializable {
private String name;
private Integer age;
private int id;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public Person(String name, Integer age, int id) {
this.name = name;
this.age = age;
this.id = id;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", id=" + id +
'}';
}
}
再次进行反序列化:
1.png
可以看到,在反序列化时出现了异常,总结一下serialVersionUID作用:
Java序列化机制通过运行时判断类的serialVersionUID来验证版本的一致性。在进行反序列化时,JVM从字节流中读取到的serialVersionUID是Person(没有id字段),JVM会将这个serialVersionUID与本地Person(添加了id字段)的SerialVersionUID进行对比,如果一致,则进行序列化,若不一致则抛出java.io.InvalidClassException异常。
一句话总结:手动指定serialVersionUID则可以兼容进行序列化,若由JVM自动生成如果修改了类字段或者其他内部信息,其生成的serialVersionUID就会与第一次不同,序列化就会抛出异常。所以在实现了序列化接口后最好指定一下serialVersionUID。