1、对象的序列化
ObjectInputStream与ObjectOutoutStream是可以直接操作对象的流。
对象本身存在于堆内存当中,当程序结束,堆内存被释放回收,对象便不存在。我们可以通过流的方式将对象存放到硬盘上,那么对象中的数据也随着对象存储到硬盘之上。后面的程序向使用相应对象的方法,只需要读取相应的硬盘内存即可。
将对象存储到硬盘之上,称之为对象的实体化存储,或者叫对象的序列化。
对象序列化的示例如下(较为复杂,参考21-01)
---ObjectStreamDemo类
package pack;
import java.io.*;
import java.util.*;
//多个源对应一个目的
public class ObjectStreamDemo
{
public static void main(String[] args) throws IOException,ClassNotFoundException
{
// writeObject();
readObject();
//结果1:lkj:23,刚刚好是我们重写的toString()方法的格式
}
//首先,创建一个写入对象的方法
public static void writeObject() throws IOException
{
//创建一个写对象的流
//ObjectOutputStream(OutputStream out) 创建写入指定 OutputStream 的 ObjectOutputStream。
//对象里面是字节码文件,因此必须用字节流。
ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("G:\\obj.txt"));//其实一般不存入txt文件,这里存入Person.Object
//使用oos的方法写入Person对象
//我们需要在同一个包下面创建一个Person类,才可以将这个类写入相应的文件,否则会发现该对象不存在
oos.writeObject(new Person("lkj",2333,"kr"));
oos.close();
/*结果1:
* 相应文件夹下面出现包含一个Person对象的obj.txt文件
* 控制台报异常:java.io.NotSerializableException: pack.Person:Serializable:可串行化的异常
Serializable接口:类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。
既想要序列化的对象的类必须实现Serializable接口,该接口没有方法,这类接口称之为标记接口(21-01,14.00)
*/
}
//对象存储到文件之后,我们读取它
public static void readObject() throws IOException, ClassNotFoundException//注意,readObject()方法会抛出这两个异常
{
//创建对象读取的流对象
//ObjectInputStream(InputStream in) 创建从指定 InputStream 读取的 ObjectInputStream。
ObjectInputStream ois =
new ObjectInputStream(new FileInputStream("G:\\obj.txt"));
//存入文件的对象全部向上转型为Object对象,读取的时候需要向下转型为Person
//Object readObject() 从 ObjectInputStream 读取对象。
Person p = (Person) ois.readObject();
System.out.println(p);
}
}
-------------------------
---Person类
package pack;
import java.io.*;
class Person implements Serializable
{
private String name;
transient int age;
/*
我们读写完成之后,往Person中加入一些新的内容,给name加一个private修饰,
注意,此时“obj.txt”文件里面存储的是修改之前的Person对象,我们将相应的Person.class删除,再次编译Person,出现一个新的Person.class
然后再次读文件内容,会报出如下异常
java.io.InvalidClassException: pack.Person; local class incompatible: stream classdesc serialVersionUID = 6284087597627212845,
local class serialVersionUID = -6689543595023836829(21-01,25.30)
我们生成新的Person.class文件后会生成新的序列号,这个Person对象的序列号与存储到“obj.txt”的Person对象的序列号不对应,那么就没办法写入“obj.txt”
序列号是根据成员内容来获取的,用于标识区分不同的对象
*/
//为了修改Person我们也可以写入新的Person对象,我们可以自己创建一个固定的UID标识Person
//这样不管Person如何变化,我们均可以将Person传入存储Person对象文“obj.txt”
//UID用于给类定义固定的标识
public static final long serialVersionUID = 42L;
/*
接下来我们定义一个新的变量——国籍,同样,先写入,再读取,发现结果为
lkj:2333:cn
既我们写入的“kr”没有成功写入,既静态变量是无法被序列化的!静态变量在方法区,对象在堆里面,只能序列化堆中的内容。
*/
//如果我们不想让age序列化,既非静态成员也不想将其序列化,可以加入transient关键字(21-01,34.10)
//同样先读取再写入,结果:lkj:0:cn,既age没有存入文件,既其无法被序列化
static String country = "cn";
Person(String name,int age,String country)
{
this.name = name;
this.age = age;
this.country = country;//这里,非静态变量访问静态变量
}
public String toString()
{
return name+":"+age+":"+country;
}
}
2、管道流
管道流:PipedlnputStream和PipedOutputStream,输入输出可以直接进行连接,通过结合线程使用。
管道输入流应该连接到管道输出流;管道输入流提供要写入管道输出流的所有数据字节。通常,数据由某个线程从 PipedInputStream 对象读取,并由其他线程将其写入到相应的 PipedOutputStream。不建议对这两个对象尝试使用单个线程,因为这样可能死锁线程。既管道流涉及了多线程!
集合中涉及IO流的类是Properties。IO流中涉及多线程的是PipedInputStream与PipedOutputStream。
/*
* 连接管道输入与输出——构造方法PipedInputStream(PipedOutputStream src) 或者是connect()方法
* 管道流2个线程之间的沟通(重要!),见视频21-02,11.05
*/
package pack;
import java.io.*;
import java.util.*;
//多个源对应一个目的
public class PipedStreamDemo
{
public static void main(String[] args) throws IOException
{
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream();
//创建管道输出以及输入流,两个流必须使用不同的线程描述,因此创建2个类用于描述读取线程与写出线程,并将2个流连接起来
WritePiped wp = new WritePiped(pos);
ReadPiped rp = new ReadPiped(pis);
pis.connect(pos);
//创建2个线程
Thread t1 = new Thread(wp);
Thread t2 = new Thread(rp);
t1.start();
t2.start();
}
}
//首先创建一个写管道流的类,实现Runnable
class WritePiped implements Runnable
{
//创建一个PipedInputStream引用,在构造方法中为其赋予对象
private PipedOutputStream pos;
WritePiped(PipedOutputStream pos)
{
this.pos = pos;
}
//重写run方法,在里面写读入的方法
public void run()
{
//由于run()方法无法抛出异常,我们只能try
try
{
System.out.println("开始写入数据,等待6秒后。");
Thread.sleep(6000);//这里抛出中断异常
pos.write("PipeStream has run now".getBytes());//将字符串转换为数组直接写入
}
catch (Exception e)
{
throw new RuntimeException("管道读取失败");
}
finally
{
try
{
if(pos!=null)
pos.close();
}
catch(IOException e)
{
throw new RuntimeException("管道读取流关闭失败");
}
}
}
}
//再创建一个读取管道流的类,实现Runnable
class ReadPiped implements Runnable
{
private PipedInputStream pis;
ReadPiped(PipedInputStream pis)
{
this.pis = pis;
}
public void run()
{
try
{
byte[] buf = new byte[1024];
System.out.println("读取前。。没有数据阻塞");
int len = pis.read(buf);//从输入端管道读入一个数组的数据,存储到buf数组中,并返回读到的数组长度
System.out.println("读到数据。。阻塞结束");
String str = new String(buf,0,len);//将字符数组构造为字符串
System.out.println(str);
}
catch (IOException e)
{
throw new RuntimeException("管道写入失败");
}
finally
{
try
{
if(pis!=null)
pis.close();
}
catch(IOException e)
{
throw new RuntimeException("管道写入流关闭失败");
}
}
}
}
/*
结果:先显示
开始写入数据,等待6秒后。
读取前。。没有数据阻塞
这说明写入线程在挂起等待,而读取线程没有内容读取,必须等待写入线程写入,也在等待
6s后显示
读到数据。。阻塞结束
PipeStream has run now
写入线程获取执行权写入完成,读取线程获得执行权后读取数据,打印
哪个线程先获得CPU执行资格执行不重要,因为读取有一个阻塞式方法read,只有等读取线程的write()方法写入后,read才会读取
*/
3、RandomAccessFile类
RandomAccessFile类:随机访问文件的特点如下
RandomAccessFile
该类不是算是IO体系中子类,而是直接继承自Object。但是它是IO包中成员,因为它具备读和写功能。
内部封装了一个数组,而且通过指针对数组的元素进行操作。
可以通过getFilePointer获取指针位置,同时可以通过seek改变指针的位置。
其实完成读写的原理就是内部封装了字节输入流和输出流。
通过构造函数可以看出,该类只能操作文件。RandomAccessFile(File file, String mode) 、RandomAccessFile(String name, String mode)
而且操作文件还有模式:只读r,读写rw等。
如果模式为只读 r,不会创建文件,会去读取一个已存在文件,如果该文件不存在,则会出现异常。
如果模式rw,操作的文件不存在,会自动创建。如果存在则不会覆盖。
RandomAccessFile类的示例如下
package pack;
import java.io.*;
import java.util.*;
public class PipedStreamDemo
{
public static void main(String[] args) throws IOException
{
writeFile();
// readFile();
writeFile_2();
}
//首先,调用RandomAccessFile的写入方法
public static void writeFile() throws IOException
{
RandomAccessFile raf = new RandomAccessFile("G:\\haha.txt","rw");
// void write(byte[] b)
raf.write("李四".getBytes());//将字符串转换为字符数组存入
//void write(int b) 向此文件写入指定的字节(write()方法只能写入最低的8位)。
// raf.write(97)//写入李四a:txt文件读入97之后会查GBK表,显示相应的字符
//当我们想写入超过8位(2^8=256)的数的时候,就会溢出,只取最低8位,数据丢失从而结果错乱(12-03,12.00)
//writeInt(int v) 按四个字节将 int 写入该文件,先写高字节。
raf.writeInt(97);//因此,为了防止溢出,我们写整数的时候使用4个字节32位的writeInt()方法
raf.write("王五".getBytes());
raf.writeInt(99);
raf.close();
/*
* 注意GBK下一个汉字占2个字节,一个字母占1个字节;UTF_8下一个汉字占3个字节
*/
}
//再创建一个写入方法
public static void writeFile_2() throws IOException
{
RandomAccessFile raf = new RandomAccessFile("G:\\haha.txt","rw");//与上一个方法写入同一个文件
//我们想在第四个位置一对占8字节(64位),第4个位置从24个字节开始
//通过seek()方法我们可以随机在任何位置读写!而且seek()方法还能对数据进行修改!
//既RandomAccessFile在new对象的时候不会创建新的文件覆盖原来的文件,而是在旧文件上直接写数据,这与之前的输出流不同。输出流一new对象就覆盖文件。
//如果按照输出流覆盖文件,我们这个方法创建的haha.txt就会覆盖前面的haha.txt,这样前面方法写入的内容就会不存在!
raf.seek(8*3);
raf.write("周七".getBytes());
raf.writeInt(107);
raf.close();
//李四 a王五 c 周七 k
}
//RandomAccessFile的读取方法
public static void readFile()throws IOException
{//创建RandomAccessFile对象,唯一不同就是设置为只读模式
RandomAccessFile raf = new RandomAccessFile("G:\\haha.txt","r");
//调整指针的2类方式(20-03,21.00)
//1、public void seek(long pos):前后都可以指
// raf.seek(8);//将指针移到第9个字节处(数组下标为8,RandomAccessFile里面存储的数据结构是数组)
// //打印:name=王五 age=99
//2、int skipBytes(int n) 尝试跳过输入的 n 个字节以丢弃跳过的字节。 只能往下跳,不能往回跳。
raf.skipBytes(8);//我们试着跳过8个字节再打印
//打印:name=王五 age=99
byte[] buf = new byte[4];//设置一个4个字节长度的缓冲区
//int read(byte[] b) 将最多 b.length 个数据字节从此文件读入 byte 数组。
raf.read(buf);//将4个字节的数据读入buf数组
String name = new String(buf);//将字节数组转换为字符串
//int readInt():从此文件读取一个有符号的 32 位整数(4个字节)。
int age = raf.readInt();//这里为我们也可以读出4个字节的byte数组,再转换为整数,不过这样太麻烦了!
System.out.println("name="+name);//name=李四:Ian读取“李四”,4个字节
System.out.println("age="+age);//age=97:前面以4个字节存入“李四”,这次的4个字节存储97
raf.close();
}
}
4、操作基本数据类型的流对象——DataStream
DataOutputStream与DataInputStream示例如下——凡是操作基本数据类型就使用这2个类。
/*
DataInputStream与DataOutputStream:可以用于操作基本数据类型的数据的流对象。
这两个类的方法类似于ObjectOutputStream与ObjectInputStream,但是前者用于操作基本数据类型,后者用于操作数据
*/
package pack;
import java.io.*;
import java.util.*;
public class DataStreamDemo
{
public static void main(String[] args) throws IOException
{
// writeData();
// readData();
// writeUTFDemo();
readUTFDemo();
//以前我们想使用UTF-8将文件存入,需要使用转换流
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("G:\\utf.txt"),"UTF-8");
osw.write("哈哈哈");
osw.close();
//这里用GBK写入“哈哈”有4个字节,UTF-8直接写入有6个字节(一个汉字3字节),用writeUTF()有8个字节(一个汉字分配4字节)
}
//首先,创建写入数据流方法
public static void writeData() throws IOException
{
//DataInputStream(InputStream in) 使用指定的底层 InputStream 创建一个 DataInputStream。
DataOutputStream dos =
new DataOutputStream(new FileOutputStream("G:\\data.txt"));
dos.writeBoolean(true);
dos.writeInt(234);
dos.writeChar('a');
dos.close();
//G盘里面生成fata.txt,且内容乱七八糟(21-04,4.50),一共写入7个字节,
//因为记事本显示字符,记事本拿这些字节的值查GBK表,把表中对应的字符显示出来(看不看得懂不重要,会存取即可)
// ObjectOutputStream oos = null;//创建一个写出对象的流
// oos.writeObject(new O());
}
//首先,创建读取数据流方法
public static void readData() throws IOException
{
DataInputStream dis =
new DataInputStream(new FileInputStream("G:\\data.txt"));
//用读取各类基本数据的方法读取——这里必须按照存储顺序读取,否则结果不对应
boolean flag = dis.readBoolean();
int num = dis.readInt();
char ch = dis.readChar();
System.out.println("flag:"+flag);
System.out.println("num:"+num);
System.out.println("ch:"+ch);
dis.close();
/*
* 这里很容易就读取出来了,如果按以前字节流的存取,必须一个字节或者字节数组存取,而且还要各种类型之间的转换。
*/
}
//特殊方法:void writeUTF(String str):以与机器无关方式使用 UTF-8 修改版编码将一个字符串写入基础输出流。(21-04,8.00)
public static void writeUTFDemo() throws IOException
{
DataOutputStream dos =
new DataOutputStream(new FileOutputStream("G:\\utf-data.txt"));
//这里写中文,写英文不涉及UTF编码
dos.writeUTF("哈哈哈");//用这种方式写入,只能按照对应的readUTF()读取,用流无法取出。
dos.close();
}
//String readUTF() 读入一个已使用 UTF-8 修改版格式编码的字符串。
public static void readUTFDemo() throws IOException
{
DataInputStream dis =
new DataInputStream(new FileInputStream("G:\\utf-data.txt"));
String str = dis.readUTF();
System.out.println(str);
dis.close();
}
}
5、ByteArrayStream——字节数组操作流
ByteArrayInputStream 与ByteArrayOutputStream,因为这两个流对象都操作的数组,并没有使用系统资源。比如我们创建文件并写文件,需要Windows操作系统创建文件并往文件内部写入数据,这时才算调用了底层(系统)资源。
所以,不用进行close关闭。关闭 ByteArrayOutputStream或者ByteArrayOutputStream无效。此类中的方法在关闭此流后仍可被调用,而不会产生任何 IOException,因为这个流没有调用过底层资源,关闭是没有效果的。
ByteArrayOutputStream类实现了一个输出流,其中的数据被写入一个 byte 数组。缓冲区会随着数据的不断写入而自动增长。可使用 toByteArray() 和 toString() 获取数据。关闭 ByteArrayOutputStream 无效。此类中的方法在关闭此流后仍可被调用,而不会产生任何 IOException。
ByteArrayStream流的具体特点如下:
用于操作字节数组的流对象。
ByteArrayInputStream :在构造的时候,需要接收数据源,而且数据源是一个字节数组。
ByteArrayOutputStream: 在构造的时候,不用定义数据目的,因为该对象中已经内部封装了可变长度的字节数组,这就是数据目的地。
因为这两个流对象都操作的数组,并没有使用系统资源。所以,不用进行close关闭。
在流操作规律讲解时:(21-05,12.00)
源设备,
键盘 System.in,硬盘 FileStream,内存 ArrayStream。
目的设备:
控制台 System.out,硬盘FileStream,内存 ArrayStream。
示例如下
package pack;
import java.io.*;
import java.util.*;
public class ByteArrayStream
{
public static void main(String[] args) throws IOException
{
//数据源。数据源可以根据需要选择,可以将文件等写进来,最后是字符数组,满足成为ByteArrayInputStream的参数即可
ByteArrayInputStream bais = new ByteArrayInputStream("abcdefgh".getBytes());
//数据目的
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//创建字符数组写出对象ByteArrayOutputStream,它不需要目的地,
//因为因为该对象中已经内部封装了可变长度的字节数组,这就是数据目的地。
//这里使用一次写一个字符的方式,因为输入输出都是使用数组,这里就直接使用字符
int by = 0;
while((by = bais.read()) != -1)
{
baos.write(by);//将字符写入可编程的的字节数组
}
//size() 返回缓冲区的当前大小。
System.out.println(baos.size());
System.out.println(baos.toString());
//2个流对象没有使用系统资源,无须关闭!
//对于数组的元素操作,只有设置和获取,在IO中就是写和读。上面这一部分,用流的读写思想来操作数组。
//void writeTo(OutputStream out) 将ByteArrayOutputStream中 byte 数组输出流的全部内容写入到指定的输出流参数中
baos.writeTo(new FileOutputStream("G:\\haha.txt"));
}
}
6、IO中的其他类总结
7、就业班补充
补充1:
序列化与反序列化
序列化的注意事项
readObject方法声明抛出了ClassNotFoundException(class文件找不到异常)
当不存在对象的class文件时抛出此异常
反序列化的前提:
1.类必须实现Serializable
2.必须存在类对应的class文件
transient关键字
static关键字:静态关键字
静态优先于非静态加载到内存中(静态优先于对象进入到内存中)
被static修饰的成员变量不能被序列化的,序列化的都是对象
private static int age;
oos.writeObject(new Person("小美女",18));
Object o = ois.readObject();
Person{name='小美女', age=0}
transient关键字:瞬态关键字
被transient修饰成员变量,不能被序列化
private transient int age;
oos.writeObject(new Person("小美女",18));
Object o = ois.readObject();
Person{name='小美女', age=0}
另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException
异常。**发生这个异常的原因如下:
- 该类的序列版本号与从流中读取的类描述符的版本号不匹配
- 该类包含未知数据类型
- 该类没有可访问的无参数构造方法
Serializable
接口给需要序列化的类,提供了一个序列版本号。serialVersionUID
该版本号的目的在于验证序列化的对象和对应类是否版本匹配。
public class Employee implements java.io.Serializable {
// 加入序列版本号
private static final long serialVersionUID = 1L;
public String name;
public String address;
// 添加新的属性 ,重新编译, 可以反序列化,该属性赋为默认值.
public int eid;
public void addressCheck() {
System.out.println("Address check : " + name + " -- " + address);
}
}
序列号冲突原理以及解决方案(参考就业班视频)
补充2:
序列化练习
package com.itheima.demo04.ObjectStream;
import java.io.*;
import java.util.ArrayList;
/*
练习:序列化集合
当我们想在文件中保存多个对象的时候
可以把多个对象存储到一个集合中
对集合进序列化和反序列化
分析:
1.定义一个存储Person对象的ArrayList集合
2.往ArrayList集合中存储Person对象
3.创建一个序列化流ObjectOutputStream对象
4.使用ObjectOutputStream对象中的方法writeObject,对集合进行序列化
5.创建一个反序列化ObjectInputStream对象
6.使用ObjectInputStream对象中的方法readObject读取文件中保存的集合
7.把Object类型的集合转换为ArrayList类型
8.遍历ArrayList集合
9.释放资源
*/
public class Demo03Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//1.定义一个存储Person对象的ArrayList集合
ArrayList<Person> list = new ArrayList<>();
//2.往ArrayList集合中存储Person对象
list.add(new Person("张三",18));
list.add(new Person("李四",19));
list.add(new Person("王五",20));
//3.创建一个序列化流ObjectOutputStream对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("10_IO\\list.txt"));
//4.使用ObjectOutputStream对象中的方法writeObject,对集合进行序列化
oos.writeObject(list);
//5.创建一个反序列化ObjectInputStream对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("10_IO\\list.txt"));
//6.使用ObjectInputStream对象中的方法readObject读取文件中保存的集合
Object o = ois.readObject();
//7.把Object类型的集合转换为ArrayList类型
//注意此处序列化的是保存Person对象的集合,向下强转为集合类型
ArrayList<Person> list2 = (ArrayList<Person>)o;
//8.遍历ArrayList集合
for (Person p : list2) {
System.out.println(p);
}
//9.释放资源
ois.close();
oos.close();
}
}