io思维导向图

IO里边的东西确实特别多,所以再另开一篇,把IO里面剩下的东西记录一下,这篇里面主要的内容有打印流、对象的序列化和反序列化,随机读取流等等,还是相当实用的。

一、打印流

打印流在IO第一篇文章里面是有接触过的,当时用的转换流打印从键盘接受到的数据的时候用到了System.out这么个流对象,其实它返回的就是打印流对象PrintStream,当时实用接口直接调用了,也就是多态,并没有关注其到底是哪个子类。说道这里,也就清楚了,最常用的System.out.println()中println()方法就是打印流里面的方法。

打印流分两种:

  1. 字节输出流:PrintStream
  2. 字符输出流:PrintWriter,注意PrintWriter实现了PrintStream中所有的打印方法

两者的区别:

  • 共性:都是负责打印数据,不会抛出异常,不操作数据源,有很多种打印方法。
  • 区别:还是之前的说话,一个是字节流,一个是字符流,所以两个流的构造方法不一样,字符流(PrintWriter)流构造方法接收File对象、字节输出流、字符串形式的文件名、字符输出流四种类型的参数;而字节流(PrintStream)接收File文件对象,字符形式的文件名和字节输出流这三种类型的参数。

作用:

打印流到底有啥作用?其实说白了就是将数据打印到指定的目标上,目标可以是文件,控制台,别的电脑等等,所以打印流和之前的比如说FileWriter这些输出流的功能是有重叠的,更甚至于打印流还可以干转换流的活,代码如下:

PrintWriter pw = new PrintWriter(System.out,true); //true为开启自动刷新

所以以后要是需要输出数据,优先选择打印流其实是比较方便和实用的。实际开发中,web开发,服务器其实都用的是打印流,将数据打印到客户浏览器端。

打印流还可以开启自动刷新,具体做法就是在构造方法上加上true开启即可。

注意:这里是有限制的。要开启自动刷新,必输是流对象,且在调用printlnprintfformat这三个方法时才会启用。

demo:

package io;
/*
 * 打印流demo
 */
import java.io.*;

public class PrintWriterDemo {
	public static void main(String[] args) throws Exception{
		method();
	}
	
	/*
	 * 使用打印流,将数据打印到各个目的地
	 */
	public static void method() throws Exception{
		
		//常规打印方式,和以前的字符输出流区别不大
		PrintWriter pw = new PrintWriter("e:\\PrintDemo.txt");
		pw.print("this is a test!");
		pw.flush();
		pw.close();
		
		
		//将字符打印到字符输出流中
		PrintWriter pw1 = new PrintWriter(new FileWriter("e:\\PrintDemo1.txt"),true); //开启自动刷新
		pw1.println("this is a test!");//换行打印效果,相当于缓冲流的write+newLine
		pw1.print("this is a test!");
		pw1.close();
		
		//实现转换流的效果,只有字节流才有转换
        //开启自动刷新,重新编码
		PrintStream pw2 = new PrintStream(new FileOutputStream("e:\\PrintDemo2.txt"),true,"UTF-8");
		pw2.print("测试");//换行打印效果,相当于缓冲流的write+newLine
		pw2.flush();
		pw2.close();
		
		//从控制台到文件
		BufferedReader bfr = new BufferedReader(new InputStreamReader(System.in));//控制台接收的缓冲流
		PrintWriter pw3 = new PrintWriter(new FileOutputStream("e:\\PrintDemo3.txt"),true);
		String s = null ;
		while((s = bfr.readLine()) != null){
			if(s.endsWith("over"))
				break;
			pw3.println(s);
		}
		pw3.close();
	}
}

二、对象序列化

将对象的数据写到文件中就是对象序列化,相反把文件中的数据读取并转换为对象就是反序列化。涉及到的两个流是:

  • ObjectOutputStream,这个是写对象的流
  • ObjectInputStream,这个是读对象的流

1. 序列化

写对象数据的流 是ObjectOutputStream类,构造方法和写的方法:

ObjectOutputStream(OutputStream out);//一般传递子类对象 FileOutputStream
//写入方法支持基本数据类型和对象
writeObject(Object o); //写入对象
writeInt(int i);//写入基本类型,都是write+基本类型名,这个基本没用我觉得

通过将对象和数据写入文件中 ,可是实现对数据的永久性保存。通过读取和重构就可以恢复数据,但是读取数据时类型和顺序应该和写入的顺序相同。

注意:序列化的类必须继承Serializable接口,此接口中没有方法,作用就是做一个序列化的标记。

2. 反序列化

其实io学到这里,基本的规律基本上已经出来了,有输出就对应着有输入,有写就有读,方法都是对应起来的。

ObjectInputStream(new FileInputStream("e:\\ObjectStream.txt"));//构造方法的一个例子
Object readObject();// 读的方法

读这里需要注意一点,返回的是Object对象,也就是说类型被提升,多态调用时没有什么问题,但是需要调用子类特有方法时,需要做类型转化,加之如果文件里写了很多种不同的类对象,在顺序没有保证的情况下,最后在做类型转换之前进行判断。

序列化和反序列化的demo:

package io;
/*
 * 对象序列化demo
 */
import java.io.*;
import SetDemo.Person;//person类的代码在set那篇文章里有写

public class ObjectStreamDemo {
	public static void main(String[] args) throws Exception{
		method();
		method_1();
	}
	
	/*
	 * 序列化
	 */
	public static void method() throws Exception{
		//其实输出的是二进制文件
      //Person类已实现Serializable接口,否则会报异常
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("e:\\ObjectStream.txt"));
		
		Person p1 = new Person("杨比轩", 20);//新建两个对象用于序列化
		Person p2 = new Person("比轩", 21);
		
		oos.writeObject(p1);
		oos.writeObject(p2);
		
		oos.flush();
		oos.close();
	}
	
	/*
	 * 反序列化
	 */
	public static void method_1() throws Exception{
		//读比较简单,保证对象class文件可以加载,并且按顺序走就可以了
		//如果没有person类, 会报出 找不到此类  的异常
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:\\ObjectStream.txt"));
		
		System.out.println(ois.readObject());
		System.out.println(ois.readObject());
		
		ois.close();
	}
}

3. 阻止变量的序列化

有两种方法:

  • 一种是将对象内的成员变量变为静态的,此时默认不会将其序列化,反之读取的时候就获取不到此数据。
  • 另一种是加关键字transient即可,这个关键字也就这一个作用:阻止变量序列化。

4.Serializable接口的意义

上面说了,没有实现Serializable接口的对象时没有办法序列化的,但是Serializable接口中有没有方法,那这个到底是干嘛用的呢?

存在意义,称为标记型接口,就是在你的类上标记一下,JVM看到这个标记,可以进行序列化。就像现实生活中给文件盖个章一样,有戳JVM就认,没戳就不认,这么个道理。

5. 序列化ID

首先先解释一下,什么是序列号。其实java文件在编译生成class文件的时候,会计算出一个序列号保存在二进制文件当中。所以我们当我们反序列化时,如果此时被序列化的对象的class文件在被序列化后改动过,其序列号就会发生变化,一做反序列化,就会报出序列号不一致的异常(InvalidClassException)。

那如果实现就算了改了对象的源码,也重新编译了class文件,此时反序列化也不会报错呢,这就需要显式的申明一个序列号的long型变量:

static final long serialVersionUID = XXXX L;//变量的修饰符和变量名都是java固定的,只能修改具体的数值。

三、关于接口和Object

接口是不继承任何类的,但是下面这个代码却可以编译通过:

interface A{	
}

class B implements A{
}

public class demo{
  public static void main(String[] args){
    A temp = new B();
    A.hashCode();
  }
}

我们都知道,编译看左边,所以编译的时候其实和子类B是没有什么关系的,此时接口A既没有继承Object类,也没有任何方法,但是却可以调用Object类下的任一方法,这个是不是有点想不通。

原因呢Sun公司其实有说过:就是在借楼中,隐含定义了Object中的所有方法,这些方法都是抽象的。

四、Properties和IO的结合使用

Properties和IO的结合使用可以用来加载配置文件等等,使用起来还是比较方便的。而且,之前也说话,Properties是一个线程安全的类,存储键值对的集合,不能存储null和重复对象,特有方法为setProperty和getProperty。

load方法的形式为public synchronized void(Reader/InputStream),传递字节流对象或者字符流对象,没有返回值。只需要将io对象传递进去即可,全自动,比较方便。

同时,如果需要将内存中的Properties对象中的数据写回文件,可以使用public void store(Writer writer, String comments)方法, comments为注释。

demo:

package map;
/*
 * IO和Properties的配合使用
 */
import java.io.*;
import java.util.*;
public class PropertiesDemo1 {
	public static void main(String[] args) throws Exception {
      method();
	}
	
	/*
	 * 从文本中读取键值对存储到Properties中
	 * 修改使用setProperty
	 * 
	 * 保存使用store
	 * 注意先load后再新建文件输出流,否则会清空掉文件内的数据
	 * 注释不能写中文,因为写进去的是utf的十六进制编码,不是文本
	 * 
	 * list方法,传递打印流,将键值对打印到配置文件中
	 */
	public static void method() throws Exception{
		Properties p = new Properties();
		
		FileReader fr = new FileReader("f:\\config.ini");
		
		p.load(fr);
		
		//p.setProperty("id", "789");
		
		FileWriter fw = new FileWriter("f:\\config.ini");
		p.store(fw,null);
		fr.close();
		fw.close();
		System.out.println(p);
		
		p.setProperty("address", "home");
		p.setProperty("address1", "school");
		p.setProperty("address2", "company");
		p.setProperty("id", "123");
		
		p.list(new PrintStream(new File("f:\\config.ini")));
		
	}
	
	//测试结论:使用完load方法后,Reader指向了文件末尾,但是没有被关闭
	public static void test() throws Exception{
		
		Properties p = new Properties();
		
		FileReader fr = new FileReader("f:\\config.ini");
		
		p.load(fr);
		
		char[] ch = new char[100];
		
		fr.read(ch);
		System.out.println(ch);
	}
}

附上一个练习,自己实现load方法:

package map;
/*
 * 自己实现load方法
 * 方法申明和原方法基本一致
 * 有两个重载形式字节流和字符流
 * 因为是自定义方法,所以没有this对象,直接把Properties当做参数传递机那里
 */

import java.io.*;
import java.util.*;

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

		Properties p = new Properties();
		FileReader fr = new FileReader("f:\\config.ini");
		
		load(fr, p);
		System.out.println(p);
	}

	/*
	 * 字符流load
	 * 目标,读取配置文件里的数据,转换为Properties的元素
	 * 按行读取,获取行数据,然后按等号切割,返回String[],前K后V
	 * 
	 * 疑问:
	 * 按照io的规范,其实BufferedReader在这里是要close掉的
	 * 但是此处要是关闭了BufferedReader,传递进来的Reader也会被关掉
	 * 也就是说,用户传递进来的参数一进被关闭掉了,这回使的用户后续使用出现问题
	 * 但是,思考了下,也不知道怎么解决这个问题,希望有大神可以回答
	 * java的load是没有关闭传递进去的对象的,但是看不大懂内部的具体实现
	 */
	public static void load(Reader reader, Properties p) throws IOException {

		String str = null;
		BufferedReader br = new BufferedReader(reader); //使用Buffered可以使用readLine
		while ((str = br.readLine()) != null) {
			//先去掉空格
			str = str.trim();
			// 忽略掉注释的内容...
			if (!(str.startsWith("#") || str.startsWith("-"))) {
				
				String[] kv = str.split("=");

				// 去掉不符合规定的内容
				if (kv.length == 2) {
					p.setProperty(kv[0], kv[1]);
				}
			}
		}
	}
	
	//重载字节流的形式
	public static void load(InputStream is, Properties p) throws IOException{
		
		//使用转换流,将字节流数据处理为字符流,之后和字符流处理方式相同
		InputStreamReader br = new InputStreamReader(new FileInputStream("F:\\config.ini"));
		load(br,p);//其余都是一样的,调参数为reader类型的方法
	}
}

这个练习也引出了一个疑问,在代码中有提到:

按照io的规范,其实BufferedReader在这里是要close掉的。但是此处要是关闭了BufferedReader,传递进来的Reader也会被关掉。也就是说,用户传递进来的参数已经被关闭掉了,这会使得用户后续使用出现问题。思考了下,也不知道怎么解决这个问题,希望有大神可以回答。试了一下,java的load是没有关闭传递进去的对象的,但是看不大懂内部的具体实现

五、IO读写基本类型数据

可以使用IO流DataOutputStream,DataInputStream来读写基本数据。

  • DataOutputStream继承OutputStream,字节输出流。构造方法和输出方法为:
DataOutputStream(OutputStream o);//实际操作都传递FileOutputStream
writeInt(int) //方法名为:write + 基本数据类型名称
  • DataInputStream继承InputStream,字节输入流。构造方法和读取方法为:
DataInputStream(InputStream i);//实际操作都传递FileInputStream
int readInt();//方法名为:read + 基本数据类型名

DataInputStream读到末尾时会抛出EOFException(end of file exception),所以需要循环读取时一般都利用异常赖退出循环。

直接提供demo:

package io;
/*
 * 基本数据类型的读写demo
 * DataOutputStream 基本数据类型的输出
 * 只有一种构造方法
 * DataOutputStream(OutputStream out) 
 * 
 * DataInputStream 基本数据类型的读取,其余一样    
 * 
 */
import java.io.*;
public class DataStreamDemo {
	public static void main(String[] args) throws Exception{
		
//		method();
//		method2();
		wirteAndReadUTF();
	}
	
	/*
	 * 基本数据类型的输出
	 * 写入的是实际的数据内容,而且是连续性的写入
	 * 并不会记录写的数据类型及其数量信息
	 * 所以,读取的时候,不知道写的具体类型是什么,
	 * 读到文件末尾了,就报出EOF异常
	 */
	public static void method() throws Exception{
		DataOutputStream dos =  new DataOutputStream(new FileOutputStream("F:\\data.txt"));
		
		dos.writeInt(654);
		dos.writeByte(45);
		dos.writeByte(45);
		dos.writeByte(45);
		dos.writeByte(45);
		dos.writeDouble(68516951.684162);
		dos.close();
	}
	
	/*
	 * 基本数据类型的读取
	 * while永真循环读取,抓住异常时break
	 * 
	 */
	public static void method2() throws Exception{
		DataInputStream dis =  new DataInputStream(new FileInputStream("F:\\data.txt"));
		double a = 0;
		while(true){
			try{
				a = dis.readDouble();
				System.out.println(a);
			}catch(EOFException e){
				break;
			}
		}
		dis.close();
	}
	
	/*
	 * 使用writeUTF的形式进行写入和读取
	 */
	public static void wirteAndReadUTF() throws Exception{
		
		DataOutputStream dos = new DataOutputStream(new FileOutputStream("F:\\utf.txt"));
		dos.writeUTF("写什么都行0\r\n");
		
		dos.writeUTF("写什么都行1");
		dos.writeUTF("写什么都行2");
		dos.writeUTF("写什么都行3");
		dos.close();
		
		//先尝试用普通的方式去读取
		//结论,内容可以正确读取,但是前面有不能解码的数据
		BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("f:\\utf.txt"),"utf-8"));
		System.out.println("UTF编码的转换流进去读取:" + br.readLine());
		br.close();
		
		//尝试使用DataInputStram 进行读取,可以正确的读取,没有任何的异常出现
		DataInputStream dis = new DataInputStream(new FileInputStream("F:\\utf.txt"));
		System.out.println("writeUTF读取:" + dis.readUTF());
		dis.close();
	}
}

六、操作内存中的字节数组

ByteArrayInputStream,此流对象用来读取字节数组,而数组是存在内存中的,不占用底层系统的资源,所以此流关闭无效,强行调用close方法之后也继续可以使用,同时不会产生IO异常,构造方法中传递一个字节数组,用来存储读取的数据。

ByteArrayOutputStream,用来写字节数组,实际的数据是写进的内存中,同时也是关闭无效,空参构造。

输出流常用的有以下方法:

Byte[] toByteArray(); //返回流中的数组
String toString(); //将流中的数据,转成字符串
String toString(String字符集名字); //将流中的数据,按照传入的编码表转成字符串

demo

import java.io.*;
/*
 * ByteArrayStreamDemo
 */

public class ByteArrayStreamDemo {
	public static void main(String[] args) throws Exception{
		method1();
	}
	
	/*
	 * 读取文件到内存,然后使用ByteArrayOutputStream输出到新文件
	 */
	public static void method1() throws Exception{
		
		//指定文件,替换为自己的测试文件即可
		FileInputStream fis = new FileInputStream
          (new File("F:\\迅雷下载\\372.90-notebook-win10-64bit-international-whql.exe"));
		ByteArrayOutputStream dos = new ByteArrayOutputStream();
		
		byte[] data = new byte[2014];
		int len = 0;
		
		//这样做会把整个文件在装到内存中,文件过大会出内存溢出的异常
		//我装的文件大概在330MB,此程序结束后,内存会被释放掉
		//可以调用垃圾回收提前释放内存
		while((len = fis.read(data)) != -1){
			dos.write(data,0,len);
		}
		fis.close();
		
		//睡十秒,便于观察内存的占用的情况
		Thread.sleep(10000);
		
		//直接全部输出到新的文件中去
		FileOutputStream fos = new FileOutputStream(new File("F:\\nivdia.exe"));
		fos.write(dos.toByteArray());
		fos.flush();
		fos.close();
	}
	
	/*
	 * 普通读写操作
	 */
	public static void method() throws UnsupportedEncodingException{
		
		ByteArrayInputStream bats = new ByteArrayInputStream("嘻嘻哈哈".getBytes());
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		
		int len = 0;
		while((len = bats.read()) != -1){
			System.out.println(len);
			baos.write(len);
		}
		
		String s = baos.toString("gbk");
		System.out.println(s);
		
	}
}

七、随机读写流

RandomAccessFile这个随机读写流有点类似于C++中的文件操作对象,能够指定读写的位置,同时同一流对象既能够读取数据也能够写入数据,在已经有数据存在的位置继续写数据会覆盖掉之前的数据。

构造方法:

RandomAccessFile(File file, String mode); //mode为文件打开的方式
RandomAccessFile(String name, String mode); 

mode为文件打开的方式,具体有以下四种方式:

mode 含义
r 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException
rw 打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
rwd 打开以便读取和写入,对于 “rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
rws 打开以便读取和写入,对于 “rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。

rws和rwd的具体区别:

“rwd” 模式可用于减少执行的 I/O 操作数量。使用 “rwd” 仅要求更新要写入存储的文件的内容;使用 “rws” 要求更新要写入的文件内容及其元数据,这通常要求至少一个以上的低级别 I/O 操作。

常用的方法:

void close(); //关闭流对象
long getFilePointer(); //获取当前文件指针的位置
long length(); //获取文件的长度,按字节计算
int read(); //读取一个字节数据
int read(byte[] b); //读取一个字节数组长度的数据
int read(byte[] b, int off, int len); //读取指定长度的数据
int readInt(); //读取int长度的数据
String readLine(); //读行
void seek(long pos); //设置文件指针的位置
void setLength(long newLength); //设置文件大小,可以用于创建空白文件
void write(byte[] b); //写数据

demo:

package io;

import java.io.*;

/*
 * 随机读写流RandomAccessFile
 * 此流特点,能读能写,能指定文件指针位置,四种打开模式
 * 写:可以写基本数据类型和字节数据
 */
public class RandomAccessFileDemo {
	public static void main(String[] args) throws Exception {
		method();
	}

	/*
	 * 实现文件的指定位置读写 
	 * 1. 获取一个文件,包括文件名和大小信息
	 * 2. 在新目录下创建一个相同大小的新文件,模拟下载文件 
	 * 3.然后先复制文件的后半段,再复制文件的前半段信息 
	 * 4. 完成复制,尝试打开复制的exe文件,看是否成功
	 */
	public static void method() throws Exception {

		// 初始化源文件
		//一个330MB的exe程序
		File file = new File("F:\\nivdia.exe");
		long fileLength = file.length();

		// 源文件名字的处理
		String fileName = file.getName();
		System.out.println(fileName);
		String[] nameAndType = fileName.split("\\.");
		System.out.println(nameAndType.length);
		fileName = nameAndType[0] + "2." + nameAndType[1];

		// 创建随机读取对象,指定模式为rw模式
		RandomAccessFile raf = new RandomAccessFile
          (new File(file.getParentFile(), fileName), "rw");
		// 设置文件大小,如果文件存在,文件大小会被改变
		raf.setLength(fileLength);
		
		//先写后半部分的数据
		// 因为要同时操作两个文件,都还要指定位置读写,所有还需要创建一个随机读写流
		// 因为原文件只需要读取数据,所以为只读模式
		RandomAccessFile rf = new RandomAccessFile(file, "r");
		int len = 0;
		byte[] data = new byte[1024];
		long backHalf = fileLength / 2;
		rf.seek(backHalf);
		raf.seek(backHalf);
		
		//因为文件不规律,所以需要处理好尾部
		//有规律的话,用抓文件末尾异常也可以结束掉循环
		byte[] endsData = new byte[(int) (backHalf % 1024)];
		
		while (true) {
			if((rf.getFilePointer() - backHalf + 1024) > backHalf){
				rf.read(endsData);
				raf.write(endsData);
				break;
			}else{
				rf.read(data);
				raf.write(data);
			}
		}
		
		//接着写前半部分的数据
		rf.seek(0);
		raf.seek(0);
		//此处由于没有到文件末尾,所以不能使用EOF异常来进行判断
		//前后两次肯定会有交叉的地方,由于会覆盖读写,所以不用考虑,
		//只需要确定超过一半就可以
		while(true){
			len = rf.read(data);
			raf.write(data, 0, len);
			if(rf.getFilePointer() > backHalf)
				break;
		}
		rf.close();
		raf.close();
	}
}

模拟下载后文件信息对比(下篇关于线程的文章里会提供多线程通信发包的方式来模拟下载):

信息对比

以上就是IO流的所以内容。

附: java–IO流(一)