黑马毕向东Java课程笔记(day21-01——21-05)IO流:对象的序列化、Pipe的Stream、RandomAccessFile、DataStream、ByteArrayStream

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();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值