15.IO(四)【IO包的其他类】


 

一、操作对象(对象序列化)

1.概述

     将内存中的对象保存到文件并存储到硬盘中,使其持久化。以后如果用到这些对象时直接从硬盘中读取,不用再new。保存的文件后缀名是.object。

     这是额外功能,其底层还是File。

2.使用到的两个类

     1)ObjectInputStream:反序列化,读取ObjectOutputStream写的文件。

           其特有的方法是Object readObject(),该方法实现读取对象。

                                    注意:读取时必须有存储的文件和所存储对象对应的类文件。否则抛出ClassNotFoundException。

           例如:Person p = (Person)ois.readObject();

     2)ObjectOutputStream:对象的序列化。将对象写入文件。

           其特有的方法是void writeObject(Objcet obj),将指定对象写入文件。

         注意:被操作的对象必须实现Serializable标记接口。

3.Serializable接口

    1)Serializable接口用于标记被序列化的类,判断类和对象是否是同一版本。类实现了Serializable后。类此时的版 

          本就有了serialVersionUID号。创建对象时,对象也跟着有了serialVersionUID号。serialVersionUID号的定

         义是根据类中成员的修饰符来算的。若修改了类,会产生新的serialVersionUID号。再读取时会报错。

          

          注意:为了保证修改类内容或者不同版本的编译器给类标记的ID不一样而引起报错,可以在类中定义一个默认

                    serialVersionUID号。

        

         显式定义serialVersionUID的好处:

                  ①如果你在对类对象进行了序列化之后,又修改了这个类,那么再次读取修改前序列化的对象时,编译器

           可以识别;

         ②如果没有显式定义,你修改后的类经过编译器编译后会生成一个新的serialVersionUID,这个

           serialVersionUID跟修改前类的serialVersionUID不同,当你再次读取时,编译器会报出

           InvalidClassException异常。

    2) transient和static

         transient:短暂的,暂时的,修饰成员变量时,写入对象时,该变量数据将不被写入。

                       非静态数据不想被序列化可以使用这个关键字修饰。

         static:非静态的,修饰成员变量时,写入对象时,该变量数据将不被写入。

        因为writeObject()方法只能操作不是非瞬态和非静态的数据。

        

<span style="font-size:18px">//操作对象,对象序列化。

import io.p2.bean.Person;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ObjectStreamDemo {

	public static void main(String[] args) throws IOException, ClassNotFoundException {

		//对象序列化。写入文件
//		writeObj();
		
		//对象反序列化。读取文件
		readObj();
	}

	public static void readObj() throws IOException, ClassNotFoundException {
		
		//反序列化。读取ObjectOutputStream写的文件。
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.object"));
		
		//读取时必须有存储的文件和所存储对象对应的类文件。否则抛出ClassNotFoundException
		Person p = (Person)ois.readObject();
		
		System.out.println(p.getName()+":"+p.getAge());
		
		ois.close();
	}

	public static void writeObj() throws IOException {
		//存储对象到硬盘中,使其持久化。以后使用到时直接读取,不用再new。后缀名是.object。容易辨认出是存储对象。
		//额外功能,基础还是File
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.object"));
		
		oos.writeObject(new Person("小强",30));
		
		oos.close();
	}

}
</span>


二、RandomAccessFile类

1.概述

       该类不是IO包中四大类的子类,而是直接继承Object类。但是它也是IO包的成员,因为它具有读写功能。完成读

写的原理是内部封装了字节输入流和输出流。

       RandomAccess内部封装了一个byte数组,而且通过指针对数组的元素进行操作,可以通过getFilePointer获取指

针位置,同时可以通过seek改变指针的位置。

    通过构造函数可以看出,该类只能操作文件。

2.构造函数

  RandomAccessFile(File file, String mode)

     创建从中读取和向其中写入(可选)的随机访问文件流,该文件由 File 参数指定。

 RandomAccessFile(String name, String mode)

     创建从中读取和向其中写入(可选)的随机访问文件流,该文件具有指定名称。

 

  mode 参数值及其含意:
    |--->"r"   :以只读方式打开。调用结果对象的任何 write 方法或者如果该文件不存在
将导致抛出 

                 IOException。
    |--->
"rw"  :打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。 如果文件存在不会覆盖。
    |--->
"rws" :打开以便读取和写入,对于 "rw",还要求对文件的内容或元数据的每个更新都同步写入到底层存

                 储设备。
    |--->"rwd" :打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到底层存储设备。

3.常见方法

  void writeInt(int v):按四个字节将 int 写入该文件,先写高字节。

  voidseek(long pos):设置到此文件开头测量到的文件指针偏移量,在该位置发生下一个读取或写入操作。

  int skipBytes(int n):尝试跳过输入的 n 个字节以丢弃跳过的字节。

4.随机写入细节

  若文件里有内容,如果没有设置指针位置,在写入时,将从0角标开始写入,覆盖之前的数据。可以seek方法设置指针,避免文件内容丢失。

 

代码示例

<span style="font-size:18px">import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

public class RandomAccessFileDemo {

	public static void main(String[] args) throws IOException {

//		writeFile();
		
//		readFile();
		randomWrite();
	}
	
	
	public static void randomWrite() throws IOException{
		RandomAccessFile raf = new RandomAccessFile("rannacc.txt","rw");
		
		raf.seek(3*8);
		raf.write("哈哈".getBytes());
		raf.writeInt(108);
		
		raf.close();
	}
	
	public static void readFile() throws IOException {
		RandomAccessFile raf = new RandomAccessFile("rannacc.txt", "r");
		
		//通过seek设置指针位置,体现随机访问。只要指定指针的位置即可。
		raf.seek(1*8);
		byte[] buf = new byte[4];
		raf.read(buf);
		
		String name = new String(buf);
		
			
		System.out.println("name:"+name);
		int age = raf.readInt();//获取当前指针位置往后的4个字节整数
		
		System.out.println("age="+age);
		//getFilePointer获取指针位置。
		System.out.println("pos:"+raf.getFilePointer());
	
		raf.close();
	}

	//使用RandomAccessFile对象写入一些人员信息。
	public static void writeFile() throws IOException{
		/*
		 * 若文件不存在,则创建,文件存在,则不创建。
		 */
		RandomAccessFile raf = new RandomAccessFile("rannacc.txt","rw");
		
		raf.write("张三".getBytes());
//		raf.write(609);//只写最低字节,即低8位、导致数据丢失。
		raf.writeInt(97);//写入四个字节,即整数。
		raf.write("小强".getBytes());
		raf.writeInt(99);
		raf.close();
	}
	</span>


三、管道流(piped)

1.概述

       PipedInputStream输入流提供数据到Pipe的OutputStream输出流。

      但是因为输入流中的read()方法,没有数据读取时,会一直阻塞,造成死锁,所以要将输入流和输出流分开,即多线程。所以管道流是IO技术和多线程技术的结合。

      通常,数据由某个线程从 PipedInputStream 对象读取,并由其他线程将其写入到相应的 PipedOutputStream。不建议对这两个对象尝试使用单个线程,因为这样可能死锁线程。管道输入流包含一个缓冲区,可在缓冲区限定的范围内将读操作和写操作分离开。如果向连接管道输出流提供数据字节的线程不再存在,则认为该管道已损坏。

2.一般操作步骤

  1)分别创建读和写的类,并都实现Runable接口。分别重写run方法,需要在内部处理异常。

  2)创建两个管道流,并用connect方法将其连接起来。

  3)创建两个线程对象,将读写对象分别传入不同线程,调用线程的start方法启动线程。

代码示例

 

<span style="font-size:18px">import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class PipedStream {

	public static void main(String[] args) throws IOException {

		PipedInputStream input = new PipedInputStream();
		PipedOutputStream output = new PipedOutputStream();
		
		//输入流连接输出流
		/*
		 * 两种方法:
		 * 1,输入流一初始化就明确输出流,将输出流作为参数传入构造函数。
		 * 2、connect。
		 */
		input.connect(output);
		new Thread(new Input(input)).start();
		new Thread(new Output(output)).start();
	}

}

//读线程
class Input implements Runnable{
	private PipedInputStream in;
	Input(PipedInputStream in){
		this.in = in;
	}
	public void run() {
		
			try {
				byte[] buf = new byte[1024];
				
				int len = in.read(buf);
				
				String s = new String(buf,0,len);
				System.out.println(s);
			} catch (IOException e) {
				e.printStackTrace();
			}
	}
	
}

//写线程
class Output implements Runnable{
	private PipedOutputStream out;
	Output(PipedOutputStream out){
		this.out = out;
	}
	
	public void run() {
		try {
			out.write("嘿!管道来啦!".getBytes());
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
}
</span>


 

四、操作基本数据类型的流对象

1.概述

    操作基本数据类型的流对象是DataInputStream和DataOutputStream。这两个读写对象,可用于操作基本数据类型的流对象,包含读写各种数据类型的方法。

2.方法

读  

        byte型            byte readbyte()
 
        int型               intreadInt()
 
        boolean型       boolean readBoolean()
 
        double型        doublereadDouble()
 
String readUTF();//对应writeUTF,读取以UTF-8修改版编码写入的字符串
 

 
        byte型           writeByte(int b);//将b的低八位写入
 
        int型              writeInt(int n) 
 
        boolean型      writeBoolean(boolean b)   
 
        double型        writeDouble(double d) 
 
writeUTF(String str);//以与机器无关方式使用UTF-8修改版编码将一个字符串写入基础输出流。

 代码示例

<span style="font-size:18px">import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

//操作基本数据类型的流对象

public class DataStreamDemo {

	public static void main(String[] args) throws IOException {

//		writeData();
		readData();
	}

	public static void readData() throws IOException {
		
		DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"));
	
		String s = dis.readUTF();
		
		dis.close();
		System.out.println(s);
	}

	public static void writeData() throws IOException {
		DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"));
		
		dos.writeUTF("你好啊");//utf是utf-8编码表的修改版。写完之后,用转换流指定不了修改版的编码表,只
							//相应的读取流DataInputStream读取。
		dos.close();
	}
</span>
<span style="font-size:18px">}</span>

五、操作字节数组

1.ByteArrayInputStream

   包含一个内部缓冲区,该缓冲区包含从流中读取的字节。一创建流对象,必须要明确数据源。该源是字节数组。

2. ByteArrayOutputStream

   此类实现一个输出流。其中的数据被写入一个byte数组,而且该数组会自动增长。可使用toByteArray() 和 toString()获取数据。

3.关闭ByteArrayInputStream和ByteArrayOutputStream流无效

   原因:因为操作都是在内存中,即源和目的都是在内存中,没有调用底层资源,所以就没有资源释放。关闭后仍可以调用,不会产生IO异常。所以一般不用关闭该流。

4.应用

   这两个对象是在用流的思想来操作数组,当我们需要把一个文件中的数据加入内存中的数组时,就可以考虑用这个两个对象。此外,它还有writeTo(OutputStream os)可以把ByteArrayOutputStream对象内部定义的缓冲区内容,一次性写入os中。操作字符数组、字符串的流对象类型与之相似,可以参与它们的使用方法。

代码示例

<span style="font-size:18px">import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

public class ByteArrayStreamDemo {

	public static void main(String[] args) {

		ByteArrayInputStream bis = new ByteArrayInputStream("fafera".getBytes());
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		
		int ch = 0;
		
		while((ch=bis.read())!=-1){
			bos.write(ch);
		}
		
		//打印看看
		System.out.println(bos.toString());
	}

}</span>


六、字符编码

1.概述

        字符流的出现为了方便操作字符。更重要的是加入了编码的转换,即转换流。通过子类转换流来完成。在两个对象进行构造的时候,可以加入字符集(即编码表)。

2.可指定编码表的有:

  1)转换流:InuputStreamReaderOutputStreamWriter

   2)打印流:PrintStreamPrintWriter,只有输出流。

3.编码表的由来

       计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。这就是编码表。

4.常见的编码表

       1ASCII:美国标准信息交换码表。用一个字节的7位表示

        2IOS8859-1:拉丁码表;欧洲码表。用一个字节的8位表示

        3GB2312:中国的中文编码表()早期

        4GBK:中国的中文编码表升级,融合了更多的中文文字字符。打头的是两个高位为1的两个字节编码。为负数

        5Unicode:国际标准码,融合了多种文字。所有文字都用两个字节来表示,Java语言使用的就是unicode

        6UTF-8:最多用三个字节表示一个字符的编码表,根据字符所占内存空间不同,分别用一个、两个、三个字节来编码。

 

UTF-8编码格式:

    一个字节:0开头

       两个字节:字节一  ---> 110   位数:10 ~ 6

                           字节二  ---> 10    位数:5 ~ 0

       三个字节:字节一  ---> 110    位数:15 ~ 12

                          字节二  ---> 10    位数:11 ~ 6

                         字节三 ---> 10    位数:5 ~ 0

5.转换流的编码应用

    可以将字符以指定编码格式存储,也可以指定编码格式读取数据。指定编码表的动作有构造函数来完成。

6.编码和解码

  1)编码:字符串变成字节数组

        ①默认字符集:

              String  ---> byte[] str.getBytes()

        ②指定字符集:

              String  --->  byte[] str.getBytes(charsetName)

  2)解码:字节数组变成字符串

        ①默认字符集:

              byte[]  --->  Stringnew String(byte[])

        ②指定字符集:

              byte[]   --->  StringnewString(byte[],charsetName)

7.编码解码的注意事项

  1)如果编码成功,解码出来的是乱码,,则需对乱码通过再次编码(用解错码的编码表),然后再通过正确的编码表解码。针对于

     IOS8859-1是通用的。

 2)如果用的是GBK编码,UTF-8解码,此时通过再次编码后解码的方式,就不能成功了,因为UTF-8也支持中文,在UTF-8解的时候,会将对应的字节数改变,所以不会成功。

 3)对于中文的”联通“,这两个字比较特别,它的二进制位正好是和在UTF-8中两个字节打头的相同,所以在文本文件中,如果单独写“联通”或者和满足UTF-8编码格式的字符一起保存时,记事本就会用UTF-8来进行解码动作,这样显示的就会是乱码。

代码示例

<span style="font-size:18px">import java.io.IOException;
import java.io.UnsupportedEncodingException;

public class EncodeDemo {

	public static void main(String[] args) throws IOException {

		/*//编码解码
		 * 
		 *文字-->二进制:编码
		 *二进制 -->文字解码
		 *
		 *在内存中体现:
		 *	字符串-->字节数组:编码       把看的懂的转成看不懂的。
		 *	字节数组-->字符串:解码        把看不懂得专程看得懂的。
		 *
		 *你好:GBK: -60 -29 -70 -61
		 *		一个中文对应两个字节,每个字节高位都是1,所以是负数。
		 *
		 *你好:UTF-8:-28 -67 -96 -27 -91 -67 
		 *
		 *如果编码错误,解码不出来。
		 *如果编码正确,解错了,有可能有救(如发现解错,可以再解一次)。
		 *	iso8859-1能再一次解码,因为它不是中文,而且是单字节编码。
		 *	UTF-8编错后不可救。码很多,可以识别单字节,2字节,3字节等等。有些字节码没有对应的数据,用
		 *未知数来表示,未知数又对应自己的一个编码表,这样源字节码就被改变了。再编码一次结果也不对。
		 *以下为演示。
		 *
		 */
		
		jieMaAgain();
		
//		printBytes(buf);
//		jieMa(buf);
		
	}

	/**
	 * 如果编码正确,解错了,有可能有救(如发现解错,可以再解一次)。
	 * @throws UnsupportedEncodingException
	 */
	public static void jieMaAgain() throws UnsupportedEncodingException {
		//编码
		String str = "你好";
		byte[] buf = str.getBytes("GBK");//可以指定编码表。
		
		//解码
		String s1 = new String(buf,"iso8859-1");
		System.out.println("s1="+s1);//解码错误。编码表不对。
		
		//解错之后,再编一次,因为得出来的字符串在原来的编码表中有对应的字节,可以获取源字节。
		byte[] buf2 = s1.getBytes("iso8859-1");
		String s2 = new String(buf,"gbk");//换编码表
		
		System.out.println("s2="+s2);
	}

	/**
	 * @param buf
	 * @throws UnsupportedEncodingException
	 */
	public static void jieMa(byte[] buf) throws UnsupportedEncodingException {
		//将字节数组变成字符串,解码
		String s = new String(buf,"utf-8");
		System.out.print("s="+s);
	}

	/**
	 * @param buf
	 */
	//将字符串变成字节数组,编码
	public static void printBytes(byte[] buf) {
		for(byte b : buf){
			System.out.print(b+" ");
		}
	}

}
</span>


练习:

<span style="font-size:18px">import java.io.UnsupportedEncodingException;

/*
 * 在Java中,字符串"abcd"与"ab你好"的长度是一样的,都是四个字符,
 * 但是对应的字节数不同,一个汉字占两个字节。
 * 
 * 定义一个方法,按照最大的字节数来取子串。
 * 如:对于"ab你好",如果取三个字节,那么子串就是ab与"你"字的半个,那么半个就要舍弃。
 * 如果取四个字节,就是。取五个字节还是"ab你".
 * 
 */
public class Test {

	public static void main(String[] args) throws UnsupportedEncodingException {

		String str = "ab你好cd谢谢";
		
		//GBK:汉字中字节一个正,一个是负数的特例:"琲"。utf-8中,三个字节都是负数。
//		String str = "ab琲琲cd琲琲";;
		
		/*
		 * GBK编码
		int len = str.getBytes("gbk").length;
		for (int i = 0; i < len; i++) {
			System.out.println("截取"+(i+1)+"个字节数的结果是:"+CutByBytes(str,(i+1)));
		}
		*/
		
		//utf编码
		int len = str.getBytes("utf-8").length;
		for (int i = 0; i < len; i++) {
			System.out.println("截取"+(i+1)+"个字节数的结果是:"+CutByU8Bytes(str,(i+1)));
		}
	}

	public static String CutByU8Bytes(String str, int len) throws UnsupportedEncodingException {
		
			//将字符串转换为字节数
				byte[] buf = str.getBytes("utf-8");//一个汉字对应三个负数
				
				int count = 0;
				for (int i = len-1; i>=0; i--) {
					//从最大字节数末端开始取数。
					if(buf[i]<0){
						count++;//记录小于0的字节数,方便读取汉字。
					}
					else
						break;
			
				}
				//判断负数个数
				if(count%3==0){//如果为偶数
					return new String(buf,0,len,"utf-8");
				}
				//如果是奇数,舍弃最后一位负数,因为这个数是下一个汉字半个。不完整。
				else if(count%3==1)//多一个
					return new String(buf,0,len-1,"utf-8");
				else//多两个,舍弃
					return new String(buf,0,len-2,"utf-8");
	}

	public static String CutByBytes(String str, int len) throws UnsupportedEncodingException {
		//将字符串转换为字节数
		byte[] buf = str.getBytes("gbk");
		
		int count = 0;
		for (int i = len-1; i>=0; i--) {
			//从最大字节数末端开始取数。
			if(buf[i]<0){
				count++;//记录小于0的字节数,方便读取汉字。
			}
			else
				break;
	
		}
	
		//判断负数个数
		if(count%2==0){//如果为偶数
			return new String(buf,0,len,"gbk");
		}
		else//如果是奇数,舍弃最后一位负数,因为这个数是下一个汉字半个。不完整。
			return new String(buf,0,len-1,"gbk");
	
	}
</span>}

         

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值