JAVA-IO编程


1. 文件操作类

文件是磁盘的重要组成元素,java.io包通过File类描述文件。其类本身是一个与文件操作有关系的工具类。

用于实现文件文件或者目录的处理操作,如创建文件删除文件目录列表

File类在实现的时候实现了SerializeableComparable两个接口,其中对文件的的数据排序是依据文件的路径名称来实现的

使用File路径分割符

java开发与操作系统的原生开发是不同的,它并不是直接与操作系统绑定在一起的,而是通过java虚拟机翻译后形成操作单元,如下图是说,这样在进行文件处理操作的时候一定会存在延迟的问题

1.1 文件的目录操作

文件的创建与目录

目录是操作系统进行文件管理的基本单元,所有的文件都应该要按照需要保存在相应的目录之中,而在进行文件创建前首先一定要保证父路径存在

用法示例

public class YootkDemo { 	
	public static void main(String[] args) throws Exception { 	
		File file = new File("h:" + File.separator + "muyan" + File.separator + "vip" + 
			File.separator + "yootk.txt"); 	// 定义要操作的文件路径
		if (!file.getParentFile().exists()) { 	// 父路径不存在
			file.getParentFile().mkdirs();	// 创建父目录
		}
		if (file.exists()) { 	// 文件已存在
			System.out.println("【文件存在】执行删除操作:" + file.delete()); // 删除文件
		} else { 	// 文件不存在
			System.out.println("【文件不存在】执行创建操作:" + file.createNewFile()); // 创建文件
		}
	}
}

1.2 获取文件的信息

获取文件的元数据

磁盘中的文件除了可以实现具体的数据内容之外,实际上还会同时伴随有一些附加的元素数据信息,当文件创建或者修改的时候,操作系统会自动进行这些元数据的维护,而当使用者获取到一个文件后,也可以直接通过元素数据信息获取自己所需要的数据。

1.3 获取目录信息

获取目录数据

在一个目录之中往往会有若干个子目录或者子文件,如果要想获取指定目录下的结构列表操作,则可以通过File类提供的方法来完成,如果现在仅仅是要进行目录下结构的名称,获取直接通过list()方法即可,而如果想要对指定目录下的子目录或者文件进行处理操作,则建议通过listFiles()方法来实现

文件的过滤处理

由于文件列表中可能包含有大量的子路径(文件或者目录)信息,所以为了便于这些子路径的过滤,在java.io包中还提供了一个FileFilter接口,该接口是一个函数式接口,只提供有一个accept()方法,在此方法中设置文件的过滤条件即可,注:accpet()是供给型接口

列出指定目录下的全部txt文件

public static void main(String[] args) throws Exception { 	
	File file = new File("H:" + File.separator); 	// 目录路径
	info(file); 		// 目录查询
}
public static void info(File file) { 	// 目录列出
	if (file.isDirectory()) { 		// 路径是目录
		File list[] = file.listFiles((f) -> f.isDirectory() ? true : 
				f.getName().endsWith(".txt")); 	// 目录数据过滤
		if (list != null) { 		// 文件列表不为空
			for (File temp : list) {	// 列表递归操作
				info(temp); 		// 递归操作,继续列出
			}
		}
	} else {
		System.out.println(file); 	// 直接输出文件信息
	}
}

其中这一句代码比较难懂:File list[] = file.listFiles((f) -> f.isDirectory() ? true : f.getName().endsWith(".txt")); // 目录数据过滤

首先我们看到这是一个供给型接口,然后用了一个三目表达式,如果发现有目录,那么就加入结果列表,如果发现有txt结尾的那么就加入结果列表,然后把这个结果列表重新递归搜索。

1.4 文件的更名

文件的重命名

文件或者目录在进行定义后可以根据需要进行名称的修改,在File类中提供了renameTo()方法,在使用该方法的时候需要明确的定义一个新的文件路径,而该方法除了可以实现方法的命名之外,实际上还提供了文件移动的功能,有点像linux中的move指令

public static void main(String[] args) throws Exception { 	
	File oldFile = new File("H:" + File.separator + "yootk.png"); // 原始文件
	File newFile = new File("D:" + File.separator + "muyan.png"); // 原始文件
	// renameTo()方法定义:public boolean renameTo(File dest)
	System.out.println("【文件更名处经理】" + oldFile.renameTo(newFile)); // 文件更名
}

oldFile调用其内部的renameTo()方法会根据给定的dest实现文件的移动和更名

2. 输入流与输出流

**流(stream)**主要指的是数据的处理方式,应用程序在需要进行文件处理的时候,会通过输入流将磁盘中的文件内容读取到内存中,而在处理完成后可以通过输出流来实现数据的保存

java.io包针对I/O操作的数据形式提供两类流:字节流(InputStream、OutputStrem)字符流(Reader、Writer)。这两种操作流的基本形式相同,唯一的区别就是字节流是以字节(byte)数据操作为主,而字符流则是以字符(char)数据操作为主,流的使用步骤

  • 通过FIle类定义一个要操作的文件路径(这是在进行文件I/O的时候所需要采用的方式)
  • 通过字节流或者字符流的子类将父类对象实例化
  • 实现数据的读(read)写(write)操作
  • 流是非常宝贵的资源,在使用完毕之后需要关闭

2.1 OuputStream字节输出流

OutputStream类是java提供的字节输出流的操作父类,由于实际的开发中可能会存在任意的输出终端,所以OutputStream仅提供了接口定义规范,而在具体的输出操作则是交给了实现子类。关系如下

OutputStream主要通过字节实现数据输出操作,所以在OutputStream类中提供了三个write()方法来实现数据输出,这三个方法可以接受的参数类型为byte或者byte[]数组

向文件中写入数据

public static void main(String[] args) throws Exception {
	File file = new File("H:" + File.separator + "muyan" + File.separator + "vip" + 
			File.separator + "yootk.txt"); 	// 输出文件路径
	if (!file.getParentFile().exists()) { 		// 父路径不存在
		file.getParentFile().mkdirs();	// 创建父目录
	}
	OutputStream output = new FileOutputStream(file); 	// 实例化输出流对象
	String message = "www.yootk.com"; 		// 待输出数据
	// OutputStream类的输出是以字节数据类型为主的,所以需要将字符串转为字节数据类型
	byte data[] = message.getBytes();		// 将字符串转为字节数组
	output.write(data); 			// 输出全部字节数组的内容
	output.close();		// 关闭输出流
}

重复执行以上的过程,每次都会进行文件数据的覆盖,除了write()之外,OutputStrem还规定了append方法进行已有数据的追加

2.2 InputStream字节输入流

InputStream的设计与OutputStream是类似的。继承类图如图所示

它提供了read()方法,并对该方法进行了重载,可以将数据读取到字节数组中,也可以每次读取单个数据,在JDK9之后还提供了流的转换处理支持

InputStream读取操作实例

public static void main(String[] args) throws Exception { 		
	File file = new File("H:" + File.separator + "muyan" + File.separator + "vip" + 
		File.separator + "yootk.txt"); 		// 输出文件路径
	if (file.exists()) { 				// 文件存在
		// 此处采用AutoCloseable自动关闭处理,程序执行完成后自动调用close()方法
		try (InputStream input = new FileInputStream(file)) { 	// 文件输入流
			// 此时开辟的数组长度远远超过yootk.txt文件所保存的数据长度
			byte data[] = new byte[1024]; 	// 开辟1K的空间进行读取
			int len = input.read(data); 	// 读取数据并返回读取个数
			System.out.println("读取到的数据内容【" + 
				new String(data, 0, len) + "】"); 	// 字节转字符串后输出
		} catch (Exception e) {}
	}
}

在使用InputStream实现数据读取的时候,为了提高读取性能,往往会开辟一个字节数组,而后向该数组中进行数据的填充,在实际的开发中往往无法确认所开辟数组的长度,如果数组长度较小,则会影响读取的性能,如果数组长度较大又会造成空间的浪费,所以此时最佳的做法是根据自身应用的性能来开辟一个预计长度的数组,而后基于循环的方式来进行读取

public static void main(String[] args) throws Exception {
	StringBuffer buffer = new StringBuffer();		// 保存读取到的内容
	File file = new File("H:" + File.separator + "muyan" + File.separator + 
		"vip" + File.separator + "yootk.txt"); 	// 输出文件路径
	if (file.exists()) { 							// 文件存在
		try (InputStream input = new FileInputStream(file)) { // 文件输入流
			byte data[] = new byte[8]; 			// 开辟字节数组
			int len = 0; 	// 保存数据读取个数
			// 表达式1:input.read(data),将输入流的数据读取到字节数组之中
			// 表达式2:len = input.read(data),将读取到的数据长度赋值给len
			// 表达式3:(len = input.read(data)) != -1,判断len内容是否不为-1
			while ((len = input.read(data)) != -1) {
				buffer.append(new String(data, 0, len)); // 数据保存
			}
			System.out.println("读取到的数据内容【" + buffer + "】");
		} catch (Exception e) {}
	}
}

2.3 Writer字符输出流

Outputstream在进行数据输出到前需要将字符串转为字节数据或者字节数组,为了简化这一过程,设计了Writer类。

Writer最大的操作特点之一是可以直接实现字符串数据的输出,从而避免字符串与字节数据之间的重复的转换处理操作。这在中文数据的处理中非常重要

使用Writer输出数据代码

public static void main(String[] args) throws Exception { 		// 沐言科技:www.yootk.com
	File file = new File("H:" + File.separator + "muyan" + File.separator + 
		"vip" + File.separator + "yootk.txt"); 	// 输出文件路径
	if (!file.getParentFile().exists()) {
		file.getParentFile().mkdirs();
	}
	try (Writer out = new FileWriter(file)) { 		// 对象实例化
		out.write("沐言科技:www.yootk.com\n"); 		// 输出信息
		out.append("李兴华高薪就业编程训练营:edu.yootk.com"); 	// 追加输出信息
	} catch (Exception e) {}
}

2.4 Reader字符输入流

为了便于中文数据的读取操作,提供了Reader字符输入流支持类,可以直接基于字符数组的形式支持数据的读取操作,也可以将输入流的数据转到NIO的缓冲区进行存储

Reader类没有提供可以直接读取全部数据的处理方法,而是需要像OutputStream一样将数据读取到字符数组之中。

Reader类的常用方法

public static void main(String[] args) throws Exception {
	File file = new File("H:" + File.separator + "muyan" + File.separator + "vip" + 
		File.separator + "yootk.txt");	// 文件路径
	if (file.exists()) {			// 文件存在
		try (Reader in = new FileReader(file)) { 		// 对象实例化
			char data[] = new char[1024]; 		// 开辟字符数组
			// 读取数据到字符数组,随后返回读取到的数据长度
			int len = in.read(data);
			System.out.println(new String(data, 0, len)); // 字符数组转为字符串
		} catch (Exception e) {}
	}
}

2.5 字节输出流与字符输出流的区别

在实际的使用过程中,字节流一般直接在存储终端实现数据保存功能,而字符流则需要经过缓存区的处理,才可以实现数据的保存。

  • 字符流的缓存处理
public static void main(String[] args) throws Exception { 
	File file = new File("H:" + File.separator + "muyan" + File.separator + "vip" + 
		File.separator + "yootk.txt"); 	// 终端文件路径
	if (!file.getParentFile().exists()) { 		// 父目录不存在
		file.getParentFile().mkdirs();	// 创建父目录
	}
	Writer out = new FileWriter(file); 		// 对象实例化
	out.write("沐言科技:www.yootk.com\n"); 		// 数据输出
	out.append("李兴华高薪就业编程训练营:edu.yootk.com"); // 数据输出
	out.flush();		// 强制刷新缓冲区
}

每当用户通过FileOutputStream类所提供的write()方法进行数据输出的时候,即便不关闭文件输出流,内容也可以保存到终端文件中,而在使用FileWriter进行数据输出的时候,如果没有关闭文件输出流,则会造成某些数据不被保存到终端文件中。

这主要是因为字符流在操作的时候会将部分数据保存到缓冲中,当调用close()方法清空缓冲区之外,也可以使用flush()方法在不关闭文件输出流的情况下实现缓存的清空操作

3. 转换流

io包提供了两种不同的数据操作流,为了便于这两种操作流之间的转换处理,又提供了两个转换流的处理类。利用InputStreamReader可以将InputStream转为Reader对象,也可以利用OutputStreamWriter可以将OutputStream转化为Writer对象,利用字符输出流实现数据的写入操作

public static void main(String[] args) throws Exception { 	// 沐言科技:www.yootk.com
	File file = new File("H:" + File.separator + "muyan" + File.separator + "vip" + 
		File.separator + "yootk.txt"); 		// 终端文件路径
	if (!file.getParentFile().exists()) { 		// 此时文件有父目录
		file.getParentFile().mkdirs();	// 创建父目录
	}
	Writer out = new OutputStreamWriter(
			new FileOutputStream(file)); 		// 字节输出流转字符输出流
	out.write("沐言科技:www.yootk.com"); 		// 数据输出
	out.close();// 关闭输出流并强制刷新缓冲区
}
public static void main(String[] args) throws Exception {
	File file = new File("H:" + File.separator + "muyan" + File.separator + "vip" + 
			File.separator + "yootk.txt"); 	// 终端文件路径
	if (file.exists()) { 		// 终端文件存在
		Reader in = new InputStreamReader(new FileInputStream(file)); // 字节流转字符输入流
		char[] data = new char[1024]; 	// 开辟字符数组
		int len = in.read(data); 	// 数据读取并返回读取长度
		System.out.println(new String(data, 0, len)); // 字符数组转字符串输出
		in.close();		// 关闭字符输入流
	}
}

转换流的作用在于实现中文数据的读取

在JAVA中由于使用了Unicode,所以字符数据可以保存中文,这样在进行数据的读取或者写入的时候,使用转换流可以更加方便地进行中文数据的处理

4. 文件拷贝操作

传统的DOS支持文件复制处理命令,该命令的基本形式为copy src dst这样在命令执行完毕后就可以直接实现文件的复制操作。实际上这种文件的复制处理本身就是一种I/O流的应用,考虑到程序的适用性,应该采用字节流实现。

实现的源文件通过FileInputStream加载输入流,目标文件通过FileOutputStrem写入,由于不能确定源文件的大小,因此为了防止内存的溢出,在进行数据复制的时候无法对全部文件进行读取,只能采用边读边写的形式进行处理,即每次通过输入流读取指定长度的字节数组,而后将该数组的内容通过输出流写入目标文件

class CopyUtil {
	private File inFile; 		// 输入文件路径
	private File outFile; 			// 输出文件路径
	/**
	 * 通过数组实现拷贝参数的配置,这个数组的长度必须为2,两个数组元素的作用为:
	 * 第一个内容为拷贝文件的源路径,第二个内容为拷贝文件的输出目标路径
	 * @param args 文件拷贝处理中所需的源文件路径以及目标文件路径的保存数组
	 */
	public CopyUtil(String args[]) {
		if (args.length != 2) { 			// 参数的个数不足
			System.out.println("【ERROR】程序拷贝命令输入的参数不足,无法执行。"); // 提示信息
			System.out.println("使用参考:java YootkDemo 源文件路径 目标文件路径"); // 提示信息
			System.exit(1); 	// 程序退出
		}
		this.inFile = new File(args[0]); 		// 源文件
		this.outFile = new File(args[1]); 		// 目标文件
	}
	public CopyUtil(String inPath, String outPath) { 				// 构造方法
		this.inFile = new File(inPath); 	// 源文件
		this.outFile = new File(outPath); 	// 目标文件
	}
	/**
	 * 实现文件的拷贝处理操作
	 * @return 拷贝文件所花费时间
	 */
	public long copy() throws IOException { 		// IOException是最大的IO异常
		long start = System.currentTimeMillis();		// 获取开始时间
		InputStream input = null; 			// 输入流对象
		OutputStream output = null; 			// 输出流对象
		try {
			input = new FileInputStream(this.inFile); 				// 字节输入流
			output = new FileOutputStream(this.outFile); 			// 字节输出流
			byte data[] = new byte[2048]; 		// 每次拷贝2048个字节的内容
			int len = 0; 										// 保存每次拷贝长度
			while ((len = input.read(data)) != -1) { 				// 数据未读取完
				output.write(data, 0, len); 			// 内容输出
			}
		} catch (IOException e) { 	// IO异常
			throw e;
		} finally {
			if (input != null) { 		// 输入流不为空
				input.close();	// 关闭输入流
			}
			if (output != null) { 	// 输出流不为空
				output.close();	// 关闭输出流
			}
		}
		long end = System.currentTimeMillis();		// 获取结束时间
		return end - start; 		// 获取花费的时间
	}
}

InputStream的新支持

JDK9以后为了便于实现数据流的复制处理,在InputStream类中提供了一个数据流的传输处理方法transferTo(OutputStream out)只要在此方法中设置 了输出流,就可以直接将输入流的数据写入。

5. 字符编码

计算机中的全部操作都是由01二进制数所组成的处理单元,在进行网络数据的传输和磁盘文件到的存储的时候,最终传递的都是二进制的字节数据

所有的图片、音频、视频、文本等都需要进行有效的编码和解码处理,否则无法实现传输和保存,在现实的开发中文本的常见字符编码有如下几种

  • ISO 8859-1: 向下兼容ASCII,范围是0X00-0XFF,0x00-0x7F的编码与ASCII码一致
  • GBK/GB2312双字节编码,GBK可以表示简体中文和繁体中文,而GB2312只能表示简体中文,GBK是兼容GB2312的
  • Unicode:16进制编码,可以准确地表示出任何语言文字,此编码不兼容ISO 8859-1
  • UTF-8:可以用来表示Unicode标准中的任何字符,是针对Unicode的一种 可变长度的字符编码,第一个字节与ASCII兼容,使得原来处理ASCII的软件不需要修改或者只需要进行少量修改就可以继续使用

开发中建议使用UTF-8编码

在项目中之所以出现乱码一般有两个原因:

  • 编码方式与解码方式不同一
  • 采用了错误的编码方式。在实际的开发以及网络传输中常用的编码方式为UTF-8。在代码编写中不需要刻意地进行编码的转换,默认的编码与当前程序所在源代码的编码相同

6. 内存操作流

I/O处理的操作放在内存中,可以避免文件操作留下的磁盘痕迹。

在之前进行I/O操作的时候必须提供一个终端文件,而除了这种文件的I/O方式以外,也可以基于内存终端实现数据的输入与输出操作。

io包中提供了两种内存操作流,分别是字节内存操作流(ByteArrayInputStream、ByteArrayOutputStream)字符内存操作流(CharArrayInputStream、CharArrayOutputStream)。在使用ByteArrayInputStream(内存输入流)ByteArrayOutputStream(内存输出流)两个不同的处理类的时候,开发者可以通过ByteArrayInputStream将要读取的数据保存在内存中,而对于内存的数据取出,则可以先将其放置在ByteArrayOutputStream类的对象实例中,而后通过该类所提供的方法获取全部数据

public static void main(String[] args) throws Exception { 
	String message = "www.YOOTK.com"; 		// 字母有大小写
	InputStream input = new ByteArrayInputStream(message.getBytes()); // 内存数据读取
	ByteArrayOutputStream output = new ByteArrayOutputStream();	// 内存输出流
	int data = 0; 				// 单字节存储
	while ((data = input.read()) != -1) { 			// 数据读取
		output.write(Character.toLowerCase(data)); 	// 数据转小写输出
	}
	String loadData = new String(output.toByteArray());	// 字节数组转字符串
	System.out.println("处理后的数据:" + loadData); 	// 内容输出
	input.close();		// 关闭输入流
	output.close();		// 关闭输出流
}

本程序将要处理的字符串数据转为字节数组后保存在了内存输入流ByteArrayInputStream之中,这样在通过InputStream读取数据的时候,所读取的数据就是该字节数组。在进行内存数据取出的时候,可以按照传统I/O方式基于循环进行读取。

7. 管道流

可以通过若干个线程提高程序的执行性能,在进行I/O设计时也提供了不同线程间的管道流

在操作系统中每一个进程都是独立的运行单元,所以不同的进程之间如果想要通信则必须使用管道流。

Java为了提高程序的处理性能,采用了多线程的实现形式,所以不同的线程之间也可以基于管道流的概念来实现I/O通信

io包围线程管道里提供了两种管道流,一种是字节管道流PipedOutputStream、PipedInputStream另一种是字符管道流PipedWriter、PipedReader。以字节管道流的使用为例,在创建输入与输出两个管道之间的连接的时候,必须依靠PipedOutputStream类所提供的connect()方法,这样才可以将管道输出流所在的子线程数据发送到管道输入流所在的子线程中

class SendThread implements Runnable { 		// 发送线程
	private PipedOutputStream output = new PipedOutputStream();		// 管道输出流
	@Override
	public void run() {
		try {
			this.output.write("沐言科技:www.yootk.com".getBytes()); // 数据发送
		} catch (IOException e) {}
	}
	public PipedOutputStream getOutput() { 	// 获取管道输出流
		return this.output;
	}
}
class ReceiveThread implements Runnable { 		// 接收线程
	private PipedInputStream input = new PipedInputStream();		// 管道输入流
	@Override
	public void run() {
		try {
			byte data[] = new byte[1024]; 		// 开辟字节数组
			int len = this.input.read(data); 			// 接收管道的数据
			System.out.println("【接收到消息】" + new String(data, 0, len)); // 数据输出
		} catch (IOException e) {}
	}
	public PipedInputStream getInput() { 		// 获取管道输入流
		return this.input;
	}
}
public class YootkDemo { 			// 李兴华高薪就业编程训练营
	public static void main(String[] args) throws Exception { 	// 沐言科技:www.yootk.com
		SendThread send = new SendThread();		// 发送线程
		ReceiveThread receive = new ReceiveThread();		// 接收线程
		send.getOutput().connect(receive.getInput());		// 管道连接
		new Thread(send).start();			// 线程启动
		new Thread(receive).start();			// 线程启动
	}
}

首先定义了数据发送以及数据接收两个处理线程,随后分别在各自的类中绑定了所需要的管道流对象,当通过PipedOutputStream中提供的connect()方法将输出管道流有管道输入流建立连接后,发送线程发送的数据将自动被接收线程接收到

8. RandomAccessFile

InputStreamReader可以实现文件的批量读取,但是对较大文件的处理逻辑非常复杂。

RandomAccessFile是JDK1.0开始提供的一个文件随机读写处理类,该类的最大特点之一是可以根据用户的需要从指定的位置开始实现文件数据的读取处理,这就要求文件在保存时必须进行有效的格式定义(数据保存的长度需要进行明确的设计)

RandomAccessFile类为了简化数据的输入与输出的处理操作,对常见的数据类型提供了完整的支持,例如,写入或者读取整形数据、浮点型数据等的处理方法,这些数据都是有固定的存储长度的。而在进行字符串数据读写的时候就需要进行数据长度的设定

定长数据写入

public static final int MAX_LENGTH = 8; 				// 字符串最大长度为8位
public static void main(String[] args) throws Exception { 	// 沐言科技:www.yootk.com
	File file = new File("H:" + File.separator + "muyan" + File.separator + "vip" + 
		File.separator + "yootk.data"); 	// 输出文件路径
	if (!file.getParentFile().exists()) { 		// 父路径不存在
		file.getParentFile().mkdirs();		// 创建父目录
	}
	RandomAccessFile raf = new RandomAccessFile(file, "rw"); 	// 读写模式进行输出
	String names[] = new String[] { "zhangsan", "lisi", "wangwu", 
		"zhaoliu", "sunqi" }; 		// 姓名数据
	int ages[] = new int[] { 17, 18, 16, 19, 20 }; // 年龄数据
	for (int x = 0; x < names.length; x++) { 	// 循环写入
		String name = addEscape(names[x]); 	// 长度处理
		raf.write(name.getBytes());		// 输出8位的字节
		raf.writeInt(ages[x]); 			// 输出4位的整数
	}
	raf.close();				// 关闭随机读写流
}
public static String addEscape(String val) { 	// 增加空格
	StringBuffer buffer = new StringBuffer(val); // 字符串缓冲
	while (buffer.length() < MAX_LENGTH) { 	// 循环添加
		buffer.append(" "); 	// 在最后添加空格
	}
	return buffer.toString();		// 返回处理后的数据
}

public static final int MAX_LENGTH = 8; 			// 字符串最大长度为8位
public static void main(String[] args) throws Exception { 
	File file = new File("H:" + File.separator + "muyan" + File.separator + "vip" + 
			File.separator + "yootk.data"); 					// 输入件路径
	if (file.exists()) { 				// 文件存在
		RandomAccessFile raf = new RandomAccessFile(file, "r"); // 采用读模式进行处理
		{ 	// 读取“lisi”的数据,属于第2条内容
			raf.skipBytes(12); 		// 跨过12个字节
			byte data[] = new byte[MAX_LENGTH]; 		// 姓名数据长度统一为8位
			raf.read(data); 			// 读取姓名数据
			int age = raf.readInt();		// 读取整型
			System.out.printf("【第2条数据】姓名:%s、年龄:%d\n", 
				new String(data).trim(), age); 		// 数据输出
		}
		{ 	// 读取“lisi”的数据,属于第1条内容
			raf.seek(0); 			// 回到指定的索引位置
			byte data[] = new byte[MAX_LENGTH]; 	// 姓名数据长度统一为8位
			raf.read(data); 			// 读取姓名数据
			int age = raf.readInt();			// 读取整型
			System.out.printf("【第1条数据】姓名:%s、年龄:%d\n", 
				new String(data).trim(), age); 		// 数据输出
		}
		{ 	// 读取“sunqi”第5条数据
			raf.skipBytes(36); 		// 跨过12个字节
			byte data[] = new byte[MAX_LENGTH]; 	// 姓名数据长度统一为8位
			raf.read(data); 			// 读取姓名数据
			int age = raf.readInt();		// 读取整型
			System.out.printf("【第5条数据】姓名:%s、年龄:%d\n", 
					new String(data).trim(), age); 	// 数据输出
		}
	}
}

此时由于每条数据的长度是固定的,因此可以依据RandomAccessFile类实现二进制文件的定位读取。开发中采用随机读取的数据操作可以避免全部数据的加载,实现更高线的数据读取处理。

9. 打印流

java.io包所提供的OutputStreamWriter两个类虽然定义了核心的数据输出功能,但是存在数据支持上的缺陷,例如OutputStream只允许字节数据的输出,Writer只允许字符串或者字符数据的输出。但是java的基本数据是非常多元的,所以为了简化这类数据的I/O操作,其提供了打印流的概念,开发者利用打印流就可以方便地实现各类数据的输出

打印流采用了装饰设计模式

Java提供了一种装饰设计模式,而打印流就是基于此设计模式设计的工具类,该模式的主要特点是对已有的类功能进行重新包装,并提供新的更加全面的处理方法,以实现操作简化调用的目的

提供了字节打印流(PrintStream)字符打印流(PrintWriter)

在打印流的构造方法中需要明确地接收一个输出的终端输出流对象(文件输出、内存输出、管道输出)这样在进行输出的时候,开发者调用打印流所提供的方法即可实现OutputStream或者Writer类中的write()方法的调用,以实现最终的I/O输出处理。

public static void main(String[] args) throws Exception { 	// 沐言科技:www.yootk.com
	File file = new File("H:" + File.separator + "muyan" + File.separator + 
		"vip" + File.separator + "yootk.txt"); 			// 输出文件
	PrintWriter pu = new PrintWriter(new FileOutputStream(file)); // 文件输出流
	String name = "李兴华"; 		// 待输出数据
	int age = 18; 			// 待输出数据
	double score = 98.531982; 		// 待输出数据
	pu.printf("姓名:%s、年龄:%d、成绩:%5.2f\n", name, age, score); // 格式化输出
	pu.println("沐言科技:www.yootk.com"); 	// 输出数据并换行
	pu.close();				// 关闭输出流
}

10. System类对I/O的支持

System类提供了用于屏幕显示的输出操作,实际上这也是基于I/O操作实现的。

可以发现其提供的3个常量的类型是InputStream或者PrintStream,所以先前进行数据输出时所使用的System.out.println()操作本质上调用的是字节打印流的输出方式,但是其输出的终端为控制台,而System.in对应的是标准键盘输入,而且该对象的实例化是JVM进程在启动时自动提供的

对象输出操作

开发则只需要在指定的类中覆写toString()方法,在进行对象输出的时候就会自动调用该方法将输出对象转为字符串输出,实际上这一功能是依靠PrintStream类提供的方法完成的

/**
 * Prints an Object and then terminate the line.  This method calls
 * at first String.valueOf(x) to get the printed object's string value,
 * then behaves as
 * though it invokes <code>{@link #print(String)}</code> and then
 * <code>{@link #println()}</code>.
 *
 * @param x  The <code>Object</code> to be printed.
 */
public void println(Object x) {
    String s = String.valueOf(x);//将对象转为字符串
    synchronized (this) {
        print(s);
        newLine();
    }
}

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();//如果是null
}

public void print(String s) {//将字符串写到终端
    if (s == null) {
        s = "null";
    }
    write(s);
}

11. BufferedReader

System.in在接收数据的时候需要进行字节数组的开辟,一旦该数组开辟得较大,则会造成内存空间的浪费,数组开辟得较小,则可能出现数据丢失以及乱码的问题,所以应该在输入流中引入一种缓存机制,即用户输入的数据可以暂时保存在缓冲中,待输入完成后通过缓存一次性取出全部的数据。

在进行键盘输入数据接收的时候,由于用户输入的数据可能包含中文,因此比较好的做法是使用字符流缓冲区处理类。同时考虑到数据类型的转换支持,最佳的做法是以字符串的形式返回。io包中提供了BufferedReader字符流缓冲区处理类,该类为Reader的子类

public static void main(String[] args) throws Exception {
	// 实例化BufferedReader字符流缓冲区读取类,并且接收键盘输入流
	BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));
	System.out.print("请输入你要发送的信息:"); 		// 提示信息
	String str = keyboard.readLine();			// 数据读取
	System.out.println("【数据回显】" + str); 		// 数据输出
}
public static void main(String[] args) throws Exception { 
	File file = new File("h:" + File.separator + "message.txt"); // 文件路径
	if (file.exists()) { 					// 文件存在
		BufferedReader input = new BufferedReader(new FileReader(file)); // 文件输入流
		String data = null; 			// 保存每行读到的数据
		while ((data = input.readLine()) != null) { 	// 循环数据读取
			System.out.println(data); 		// 内容输出
		}
		input.close();	// 关闭输入流
	}
}

该程序中保证了所有的输入流数据都会先保证存在缓冲区中,而后就可以通过readLine()方法依据分隔符\n一次性将数据取出,由于readLine()返回的是字符串,因此根据最终的项目需要将数据任意转为各种类型

BufferedReader在读取的时候是以换行符\n为分隔符实现数据缓冲区加载的,那么就可以利用这一机制通过BufferedReader实现文件流数据的读取,假设有一个文本文件,该文件包含若干行文本数据内容,如果此时直接使用InputStream字节流进行读取,则数据的保存较为繁琐,但是使用BufferedReader可以直接返回字符串,这样不仅适用于程序处理,也便于实现中文数据的完整加载

12. Scanner

为了实现更标准而且更简化的数据读取操作,提供了Scanner类,该类可以由用户自定义读取匹配分隔符(使用正则规则),也可以直接通过文件或者InputStream实现数据的读取

考虑到实际项目开发中进行数据读取的时候一般会有多种数据类型的返回要求,所以Scanner类设计为可以通过nextXxx()方法根据需要返回指定类型,例如,当返回int数据时使用nextInt()方法的,当返回nextDouble()返回double类型,一般这些方法在返回之前先用hasNextXxx()方法进行判断,该方法可以根据所需的类型进行校验,如果发现读取到的数据不是最终需要的类型则返回false

正则表达式格式匹配

public static void main(String[] args) throws Exception { 
	Scanner scanner = new Scanner(System.in); 	// 实例化Scanner类对象
	System.out.print("请输入注册邮箱:"); 		// 提示信息
	if (scanner.hasNext("\\w+@\\w+\\.\\w+")) { 		// 正则校验
		String value = scanner.next("\\w+@\\w+\\.\\w+"); 		// 获取数据内容
		System.out.println("回显输入数据:" + value); // 数据打印
	} else { 				// 校验失败
		System.err.println("【ERROR】输入数据的格式不正确。"); 	// 错误打印
	}
}

13. 对象序列化

序列化是一种对象传输手段。Java中的I/O提供了常规数据(如字符串、数字、字节数据)的传输支持,其实例化对象都是保存在堆内存中的,如果想要实现对象数据的I/O传输处理,则应该通过对象序列化的形式,将堆内存中的数据转化为二进制数据再进行处理

考虑到类的功能设计,并不是所有类的对象都允许序列化的处理操作,为了可以明确地进行序列化操作的标记,Java提供了java.io.Serializable接口,该接口没有任何方法,仅仅作为功能标记的使用,而在java类库中的StringIntegerDouble等系统类也是该接口的子类,所以这些类的对象都可以序列化为二进制数据

13.1 序列化与反序列化

Serizable仅仅提供了一个序列化的处理标记,同时在io包中提供了与之对应的实现类,对象序列化的处理类(ObjectInputStream),在ObjectOutputStream类中提供了writeObject()方法可以将对象转为指定结构的二进制数据流,而要想通过此数据流进行对象的解析,则必须依靠ObjectInputStream类所提供的readObject()方法来完成

private static final File BINARY_FILE = new File("h:" + 
		File.separator + "book.ser"); 		// 文件路径
public static void main(String[] args) throws Exception { 
	serial(new Book("Java就业编程实战", "李兴华", 69.8)); 		// 序列化处理
	System.out.println(dserial());			// 反序列化处理
}
public static void serial(Object object) throws IOException { 	// 对象序列化
	ObjectOutputStream oos = new ObjectOutputStream(
			new FileOutputStream(BINARY_FILE)); 		// 文件序列化
	oos.writeObject(object); 				// 序列化输出
	oos.close();						// 关闭输出流
}
public static Object dserial() throws Exception { 	// 对象反序列化
	ObjectInputStream ois = new ObjectInputStream(
		new FileInputStream(BINARY_FILE)); 	// 对象输入流
	Object data = ois.readObject();	// 反序列化对象
	ois.close();			// 关闭输入流
	return data;
}

13.1 transient关键字

在默认情况下,每一个对象在进行序列化处理的时候,都会将该对象中保存的所有属性全部转为二进制数据,但是在一些情况下,有些属性可能不需要进行存储,这时候就可以通过transient进行标记


文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录