目录
File
File 对象
*
* //可以将一个已存在的,或者不存在的文件或者目录封装成file对象。
* 是对文件和目录本身和其包含文件和目录基本信息的描述。
创建对象只是在内存中,需要中creatnewfile 方法在外存中创建。
*
*
* File f1 = new File("c:\\a.txt");
* File f2 = new File("c:\\","a.txt");
* File f = new File("c:\\");
* File f3 = new File(f,"a.txt");
* File f4 = new File("c:"+File.separator+"abc"+File.separator+"a.txt");
*
* File 方法请见 File 类
*
* 过滤器 (类似与比较器。容器使用过滤器中方法进行筛选,其中容器可以通过listFiles方法获得)
* 用来获取指定类型的内容(文件或目录)。
* FileFilter 文件对象过滤器 根据传递对象根据对象属性筛选
* FilenameFilter 文件名称过滤器 根据文件名筛选
*
*
* 递归:
* 删除完整文件
* 删除文件用队列结构,删除空文件夹用堆栈结构
*
* 文件队列,与堆栈的操作。
package text;
import java.io.File;
import java.util.LinkedList;
public class Demo {
public static void main(String[] args) {
File dir = new File("E:\\腾讯游戏");
Queue11<File> queue = new Queue11<File>();
Duizhan<File> duizhan = new Duizhan<File>();
queue.myAdd(dir);
*
* File[] files = dir.listFiles(); for (File file : files) { if
* (file.isDirectory()) { queue.myAdd(file); } else { file.delete(); } }
*
**
* 删除文件中所有文件(不含文件夹)
*
while (!queue.isNull()) {
File deFile = queue.myGet();
File[] deFiles = deFile.listFiles();
for (File file : deFiles) {
if (file.isDirectory()) { // 判断File是文件还是文件夹
queue.myAdd(file);
} else { // 若为文件直接删除。
file.delete();
}
}
}
**
* 删除所有空文件夹。
*
duizhan.myAdd(dir);
while (!duizhan.isNull()) {
File deFile = duizhan.myGet(); //取出第一个
File[] deFiles = deFile.listFiles();
if (deFiles.length == 0) { //每次取出都要先判断是否为空,为空则删除文件夹,并从堆栈中删除第一个。
deFile.delete();
duizhan.myD();
} else { // 否则遍历其子文件夹
for (File file : deFiles) {
File[] fils = file.listFiles();
if (fils.length == 0) { //如果其子文件夹为空,直接删除文件夹,不寸入堆栈。
file.delete();
} else {
duizhan.myAdd(file); //否则 存入堆栈。
}
}
}
}
}
}
**
* 泛型队列,用于删除所有文件。
* @author YFG
*
* @param <E>
*
class Queue11<E> {
private LinkedList<E> link;
public Queue11() {
link = new LinkedList<E>();
}
public void myAdd(E obj) {
link.addLast(obj);
}
public E myGet() {
return link.removeFirst();
}
public boolean isNull() {
return link.isEmpty();
}
}
**
* 泛型堆栈,用于删除所有空文件夹。
* @author YFG
*
* @param <E>
*
class Duizhan<E> {
private LinkedList<E> linktoo;
public Duizhan() {
linktoo = new LinkedList<E>();
}
public void myAdd(E obj) {
linktoo.addFirst(obj);
}
public E myGet() {
return linktoo.getFirst();
}
public void myD() {
linktoo.removeFirst();
}
public boolean isNull() {
return linktoo.isEmpty();
}
}
package textvido;
*
*
* 递归删除目录
*
import java.io.File;
import java.io.IOException;
public class Zijieliu {
public static void main(String[] args) throws IOException {
/ TODO Auto-generated method stub
File dir = new File("E:\\修改一 - 副本");
removeDir(dir);
}
public static void removeDir (File dir){
File[] files = dir.listFiles();
if(files != null){
for (File file : files) {
if(file.isDirectory() && file != null){
removeDir(file);
}else{
file.delete();
}
}
}
dir.delete(); /以上只將子目錄刪除 ,此处将本级目录删除。
}
}
io字符流
public static void main(String[] args){InputStreamReader isr = new InputStreamReader(new Filereader("地址") "编码表")}
z
转换流 : 实现字节流到字符流的转换,在网络传输中,可使用该流实现对字符串的直接操作,不需要手动转为字节数组,该流内部使用的就是字节流。
字符流:Reader ,Writer 字符流超类 使用字符数组 , 操作的基本单位是字符,而字节流操作的基本单位是字节
OutputstreamWriter : 是 FileWriter 的父类 前者使用自定义编码表编码,后者使用默认的编码表编码
inputStreamReader : 是 Filereader 的父类 前者使用自定义的编码表解码 , 后者使用默认的编码表解码。
InputStreamReader 与 OutputstreamWriter:使用缓冲区进行编码和解码。要指定码表;
创建对象要传递一个文件字节流对象和指定码表,因为底层使用的是字节流进行读取;
字节流和字符流缓冲区的区别: 字节流缓冲区是使用字节数组;字符流缓冲区使用字符数组
编码和解码的过程就发生在字符数组和字节数组转换的过程。因此:此流也称转换流
是字符流和字节流的桥梁。
缓冲区对象:
1. 自己构建缓冲区数组
2.
以给予的缓冲区:原理
使用底层流方法从硬盘上获取数据,并存储在缓冲区中,此缓冲区就是利用 Reader 的read();方法将数据存在字符数组缓冲区中;
此缓冲区被封装不可见;
再通过缓冲区的方法从缓冲区获得具体的字符数据,提高效率
如果通过缓冲区read();方法获取字符存储再另一个容器中,直到督导换行符时,另一个
容器的临时存储数据转成字符串返回,就形成了redline()l;方法。
BufferedReader BufferedWriter 提供了缓冲区的方法。利用已有的流写入和读入数据。
提供各种格式的写入和读入方法;换行方法;newLine() 写入一个行分隔符。
readLine() 读取一个文本行。
BufferedReader bufu = new BufferedReader(new Filereader("地址"));
3.
自定义缓冲区对象:提供更多的缓冲区方法
自己从从新构建 BufferedReader BufferedWriter 类
缓冲区数组也要自己构建并封装,对外不可见。
只有out 和Writer 有刷新
flush();将流中的缓冲区的数据刷新到目的低,刷新后还可以使用
close(); 关闭资源之前刷新一次。
如果写入数据多,一边写一边刷新。最后可以用close();
刷新的目的是及时将数据写出。防止缓冲区未满,由于其他原因流意外关闭造成的数据丢失。
此外缓冲区在满的情况下会自动刷新。
GBK码表: 如果第一个字节为正,就查一个字节:如果第一个是负数,就连续读两个字节;
UTF-8: 三个字节单元;
装饰设计模式:BufferedReader BufferedWriter 都是装饰类
继承 Reader 对其他被装饰子类进行操作,扩展功能。避免继承各子类扩展功能造成的臃肿。
装饰类通常不单独存在,与被装饰类一同存在,提供对外的构造函数接受被装饰类。
io字节流
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
*
* 字节流
* 数据要写入文件中。要先创建文件对象。
* 创建一个用于操作文件的字节输出流对象,一创建就要明确数据存储的目的
*
* 转义字符: \r 在换行符前要加转义字符
* \r\n 为换行。
* 使用此类常量,要用system.获取系统常量值代替。
*
*
*
*
* 输入流与输出流类似与File关联;输出流有write()方法,输入流有read();方法;
*
* 磁针读取数据是连续读取,不会跳跃。
* 字节流写出和读入的数据都是字节数据,需要转换才能获取有效信息。
* *
public class 字节流 {
public static void main(String[] args) throws IOException {
/ 数据要写入文件中。要先创建文件对象。
File file = new File("E:\\ads.txt");
/ 输出流目的是文件,会自动在外存中创建文件。如果存在,则覆盖。此处也打开了文件
FileOutputStream fos = new FileOutputStream(file);//
/write();方法输出字节。
}
}
public class IOExceptionDemo {
private static final String LINE_SEPARATOR = System.getProperty("line.separator"); // 获得系统换行符。
public static void main(String[] args) {
FileOutputStream fw = null;
try {
fw = new FileOutputStream("k:\\demo.txt");
fw.write("abcde".getBytes());
} catch (IOException e) {
System.out.println(e.toString());
} finally {
if (fw != null) //如果字符流差个创建失败。
try {
fw.close(); //可能关闭失败
} catch (IOException e) {
/ code....
throw new RuntimeException("关闭失败");// 此异常无法解决 将异常转换为运行时异常抛出。
}
}
}
}
*
* 输入流与输出流类似与File关联;输出流有write()方法,输入流有read();方法;
*
* */
各种应用
ObjectOutputStream:
对象序列化: 想要将保存了数据的对象进行持久化,读取更方便。
当写入的很多对象会按照顺序排列,也称为序列化。
ObjectOutputStream(OutputStream out) 是对 OutputStream 的功能扩展
writeObject(Object obj)
静态数据不会被序列化。
对于一个非静态数据也不要被序列化,如密码
可用:transient 瞬态 标记
ObjectInputStream:
反序列化: 对持久化对象中数据的读取。依靠 InputStream,对其进行扩展。
读取对象必须有相应对象的 class
ObjectInputStream(InputStream in)
readObject();
用于防止程序崩溃丢失数据。
Serializable:序列化接口,要序列化的对象必须实现该接口
序列化接口的作用:没有方法,不需要覆盖。是一个标记接口
唯一作用给每一个需要序列话的类分配一个序列版本号
这个版本号和该类相关联。
版本号作用: 验证序列化对象和类是否相同,如有不同抛出异常。
序列化时,版本号也一同存在序列化文件中。
反序列化时会读取版本号进行验证。
可序列化类可以通过声明名为 "serialVersionUID" 的字段
(该字段必须是静态 (static)、最终 (final) 的 long 型字段)
显式声明其自己的 serialVersionUID :
强烈建议 所有可序列化类都显式声明 serialVersionUID 值,
默认版本号容易变动,所以显示给出。就不会因为修饰符而改变。
需求: 将一个整数表现形式不变,写出。
字节打印流: PrintStream
字节流writer方法只将一个整数四个字节中的最后一个写入。
1.将整数转为字符串变成字节数组。
2. PrintStream 方便打印各种数据值的表现形式。
扩展 FileOutputStream 利用 print()进行原样打印。
构造: PrintStream(File file)
PrintStream(OutputStream out)
PrintStream(String fileName)
提供众多打印方法,保证数据的表现形式不变。
字符打印流 PrintWriter : 详情查阅API 含有自动刷新和非自动刷新的构造方法。
需求: 保证数据源字节原样形式的不变。字节不变。
需要可以操作基本数据类型的对象。将数据类型数据写入。
DataOutputStream : 输出基本数据类型的完整字节
DataInputStream : 按照基本数据类型的长短读取并解码。
字节数组流:ByteArrayOutputStream: 写到内部字节数组;
ByteArrayInputStream : 构造时可直接传递字节数组;
利用流的方法对数组进行操作。
没有iO异常,不调用底层资源,不需要关闭流,不会抛出io异常。
需求: 对文件进行读或写的操作。 想从哪读从哪读,想从哪写从哪写。
io 工具类 : RandomAccessFile: 随机访问文件的读取和写入。
1. 随机访问 2. 操作文件 3. 既可以读又可以写。
4. 内部维护了用于存储数据的byte数组。
5.提供了对数组操作的文件指针。
6. 文件 指针可以通过 getFilePointer()方法获取,并通过seek();方法设置
7.要明确指针位置,避免数据混乱。
8. 可以用于多线程
特别是下载。
mode :文件访问方式:
"r" 只读
"rw" 读和写 不存在则创建
"rws"
"rwd"
键盘录入
获取标准输入流: InputStream in = System.in; 键盘录入时字节流。
输入流已打开,不需要关闭,一但关闭,不可打开。
字节流 --> 桥梁 :InputStreamReader -->字符流 实现对字符单位的操作。
InputStreamReader isr = new InputStreamReader(in);
通过缓冲区提高效率 BufferReader buff = new BufferReader(isr);
此时就可以使用缓冲区方法经进行操作。
BufferReader buff = new BufferReader(new InputStreamReader(System.in));三句合一句
键盘录入注意定义结束标记,或强制停止。
line = buff.readLine();键盘输入一行。
文件切割与合并
文件切割:读取文件复制到多个文件中
切割文件方式,按碎片个数,按碎片大小
一个输入流对应多个输出流。
每一个输出流需要编号,顺序不可乱。
File srcFile = new File("src");
File partsDir = new File("partFiles");
splitFile();
pubic static void splitFile(File srcFile, File partsDir){
if(!(srcFile.exists() && srcFile.isFile())){
throw new RuntimeException();
}
if(!partsDir.exists()){
partsDir.mkdirs();
}
1. 字节读取流和源文件关联
FileInputStream fis = new FileInputStream(srcFile);
2. 明确目的。输出流又多个,只创建引用。
FileOutputStream fos = null;
3.定义缓冲区。
byte[] buf = new byte[BUFFER_SIZE];//BUFFER_SIZE = 1048576(1mb)
4.频繁读写操作
int len = 0;
碎片编号
conut = 1;
while((len =fis.read(buf))!=-1){
创建输出流对象,满足缓冲区大小,写入碎片
fos = new FileOutputStream(new File(partsDir,(conut++)+".part"));
fos.writer(buf, 0 , len);
fos.close();
}//
fis.close();
}
文件切割,配置文件的建立和读取
将源文件俺以及切割的信息也保存起来随碎片一发送。
信息: 源文件名称,文件类型。碎片的个数 。封装在一个文件中
还要一个输出流完成此动作。
问题:配置文件过多需要存储,使用map集合键值对,存储信息都是字符串,信息不在内存中,再硬盘上
使用hashmap有泛型,取出需要强转,因此使用 Properties; system 属性就是使用的
Properties ;
很多常用的配置文件都是使用 Properties 存储。
使用:setProperty("string","string");存
getProperty("string"); 取
演示从流中加载和保存的方法(持久化)
File configFle = new File("");
FileReader fr = new FileReader(configFle);
Properties Pror = new Properties();
Pror.load(fr);
在流中加载数据。
FileWriter fw = new FileWriter("规范info.Properties");
Pror.store(fw, "info");
将 Properties 中信息存储在fw流中。
文件合并:多个源多个读取流;多个源的数据都要和同一个输出流关联
多个源---> 一个目的地
代码的问题: 碎片过多,意味着流过多,需要容器先存储起来,操作也更方便
1.需要容器 2. 将流与碎片关联起来存储起来 3. 遍历容器。
使用 list 集合循环存储读取流对象。
SequenceInputStream 将流序列化。
此问题以封装为对象:SequenceInputStream 将多个源合并为一个源。
将读取流遍历存入list
获取枚举对象
Enumeration<FileInputStream> en = Collections.enumeration(list);
枚举传入序列流
SequenceInputStream sis = new SequenceInputStream(en);
sis 为一个序列流可作为一个整体读取数据,和关闭。
在获取配置文件中的信息时, 可以用过滤器获得配置文件,在getProperties.获取配置信息。
RandomAccessFile实例
访问中间部分数据
public class RandomAccessFileTest
{
public static void main(String[] args)
{
try(
RandomAccessFile raf = new RandomAccessFile(
"RandomAccessFileTest.java" , "r"))
{
// 获取RandomAccessFile对象文件指针的位置,初始位置是0
System.out.println("RandomAccessFile的文件指针的初始位置:"
+ raf.getFilePointer());
// 移动raf的文件记录指针的位置
raf.seek(300);
byte[] bbuf = new byte[1024];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环来重复“取水”过程
while ((hasRead = raf.read(bbuf)) > 0 )
{
// 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!
System.out.print(new String(bbuf , 0 , hasRead ));
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
追加内容
public class AppendContent
{
public static void main(String[] args)
{
try(
//以读、写方式打开一个RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile("out.txt" , "rw"))
{
//将记录指针移动到out.txt文件的最后
raf.seek(raf.length());
raf.write("追加的内容!\r\n".getBytes());
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
插入到指定位置
public class InsertContent
{
public static void insert(String fileName , long pos
, String insertContent) throws IOException
{
File tmp = File.createTempFile("tmp" , null);
tmp.deleteOnExit();
try(
RandomAccessFile raf = new RandomAccessFile(fileName , "rw");
// 使用临时文件来保存插入点后的数据
FileOutputStream tmpOut = new FileOutputStream(tmp);
FileInputStream tmpIn = new FileInputStream(tmp))
{
raf.seek(pos);
// ------下面代码将插入点后的内容读入临时文件中保存------
byte[] bbuf = new byte[64];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环方式读取插入点后的数据
while ((hasRead = raf.read(bbuf)) > 0 )
{
// 将读取的数据写入临时文件
tmpOut.write(bbuf , 0 , hasRead);
}
// ----------下面代码插入内容----------
// 把文件记录指针重新定位到pos位置
raf.seek(pos);
// 追加需要插入的内容
raf.write(insertContent.getBytes());
// 追加临时文件中的内容
while ((hasRead = tmpIn.read(bbuf)) > 0 )
{
raf.write(bbuf , 0 , hasRead);
}
}
}
public static void main(String[] args)
throws IOException
{
insert("InsertContent.java" , 45 , "插入的内容\r\n");
}
}
java9改进的对象序列化
- 必须实现两个接口中的一个 : Seralizable 或 Externallizable 标记其可序列化。z
- 建议每个JavaBean类都实现Seralizable
- 反序列化机制无须通过构造器初始化对象
- 反序列化对象,必须存在该类的class文件
- 将多个对象写入同一个文件时,反序列化要根据这个顺序读取
- 一个可序列化类的父类必须是可序列化的,或则有无参数的构造器。否则抛出,IncalidClassException
- 如果父类不可序列化,只有无参数的构造器,则父类中定义的成员变量不会序列化到二进制流中。
-
序列化 public class WriteObject { public static void main(String[] args) { try( ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("object.txt"))) { Person per = new Person("?????", 500); oos.writeObject(per); } catch (IOException ex) { ex.printStackTrace(); } } } 反序列化 public class ReadObject { public static void main(String[] args) { try( ObjectInputStream ois = new ObjectInputStream( new FileInputStream("object.txt"))) { Person p = (Person)ois.readObject(); System.out.println("???????" + p.getName() + "\n???????" + p.getAge()); } catch (Exception ex) { ex.printStackTrace(); } } }
对象的引用序列化
- 引用的对象必须是可序列化的,否则该对象不可序列化。
- 多次序列化同一个对象,只有第一次会输出字节数据,之后只输出序列号。
- 多个对象引用同一个对象,该对象仅第一次被序列化。
- 序列化可变对象,仅在第一次会序列化,即使发生改变,也不会再序列化。
-
public class WriteTeacher { public static void main(String[] args) { try( // 创建一个ObjectOutputStream输出流 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("teacher.txt"))) { Person per = new Person("孙悟空", 500); Teacher t1 = new Teacher("唐僧" , per); Teacher t2 = new Teacher("菩提祖师" , per); // 依次将四个对象写入输出流 oos.writeObject(t1); oos.writeObject(t2); oos.writeObject(per); oos.writeObject(t2); } catch (IOException ex) { ex.printStackTrace(); } } } public class ReadTeacher { public static void main(String[] args) { try( // 创建一个ObjectInputStream输出流 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("teacher.txt"))) { // 依次读取ObjectInputStream输入流中的四个对象 Teacher t1 = (Teacher)ois.readObject(); Teacher t2 = (Teacher)ois.readObject(); Person p = (Person)ois.readObject(); Teacher t3 = (Teacher)ois.readObject(); // 输出true System.out.println("t1的student引用和p是否相同:" + (t1.getStudent() == p)); // 输出true System.out.println("t2的student引用和p是否相同:" + (t2.getStudent() == p)); // 输出true System.out.println("t2和t3是否是同一个对象:" + (t2 == t3)); } catch (Exception ex) { ex.printStackTrace(); } } }
java9新增 过滤功能
- setObjectInputFilter(ObjectInputFilter filter) ,getObjectInputFilter()
- ObjectInputFilter : 函数式接口,可以用lambda代替
- 反序列化时会自动调用ObjectInputFilter .checkInput(ObjectInputFilter.info info)方法
- 具体请查阅java9:java9api
自定义序列化
-
transient:修饰属性,序列化时将无视这个变量
-
public class Person implements java.io.Serializable { private String name; private transient int age; } public class TransientTest { public static void main(String[] args) { try( / 创建一个ObjectOutputStream输出流 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("transient.txt")); / 创建一个ObjectInputStream输入流 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("transient.txt"))) { Person per = new Person("孙悟空", 500); / 系统会per对象转换字节序列并输出 oos.writeObject(per); Person p = (Person)ois.readObject(); System.out.println(p.getAge()); /age = 0; } catch (Exception ex) { ex.printStackTrace(); } } }
- 替换序列化的属性。正常对person序列化就可以。
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
{
// 将name实例变量的值反转后写入二进制流
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException
{
// 将读取的字符串反转后赋给name实例变量
this.name = ((StringBuffer)in.readObject()).reverse()
.toString();
this.age = in.readInt();
}
}
- 替换要序列化的对象
-
public class Person implements java.io.Serializable { private String name; private int age; // 注意此处没有提供无参数的构造器! public Person(String name , int age) { System.out.println("有参数的构造器"); this.name = name; this.age = age; } // 省略name与age的setter和getter方法 // name的setter和getter方法 public void setName(String name) { this.name = name; } public String getName() { return this.name; } // age的setter和getter方法 public void setAge(int age) { this.age = age; } public int getAge() { return this.age; } // 重写writeReplace方法,程序在序列化该对象之前,先调用该方法 private Object writeReplace()throws ObjectStreamException { ArrayList<Object> list = new ArrayList<>(); list.add(name); list.add(age); return list; } } public class ReplaceTest { public static void main(String[] args) { try( // 创建一个ObjectOutputStream输出流 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("replace.txt")); // 创建一个ObjectInputStream输入流 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("replace.txt"))) { Person per = new Person("孙悟空", 500); // 系统将per对象转换字节序列并输出 oos.writeObject(per); // 反序列化读取得到的是ArrayList ArrayList list = (ArrayList)ois.readObject(); System.out.println(list); } catch (Exception ex) { ex.printStackTrace(); } } }
- 替换读取到的序列化对象,并丢弃读取到的
-
public class Orientation implements java.io.Serializable { public static final Orientation HORIZONTAL = new Orientation(1); public static final Orientation VERTICAL = new Orientation(2); private int value; private Orientation(int value) { this.value = value; } // 为枚举类增加readResolve()方法 private Object readResolve()throws ObjectStreamException { if (value == 1) { return HORIZONTAL; } if (value == 2) { return VERTICAL; } return null; } } public class ResolveTest { public static void main(String[] args) { try( ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("transient.txt")); ObjectInputStream ois = new ObjectInputStream( new FileInputStream("transient.txt"))) { oos.writeObject(Orientation.HORIZONTAL); Orientation ori = (Orientation)ois.readObject(); System.out.println(ori == Orientation.HORIZONTAL); } catch (Exception ex) { ex.printStackTrace(); } } }
版本号
private static final long serialVersionUID = ****L
最好显示给出,这样即使类发生了改变,也能正常反序列化。因为类一旦改变,jvm自动分配的id将会改变,会认为不是同一个类。
不能正常反序列化的情况: 实例变量更改类型。