第15章:输入输出

15.1 File类:表示文件或目录

  1. File可以新建,删除,重命名文件和目录,不能访问文件内容本身
  2. 目录/文件:File对象
  3. 目录名/文件名/路径名:String对象
  4. 路径与目录、文件区别:路径是用来表示唯一的目录、文件的东西
例:路径名:C:\\wusihan/liuxueting 目录名:liuxueting
15.1.1 访问文件和目录
  1. 创建File实例:new File(String pathName)
    1. pathName既可以为绝对路径,也可以为相对路径,此相对路径为相对"user.dir"的路径。对于CMD,"user.dir"为用户当前所在的路径,对于eclipse,其为eclipse所在的workspace下该类所对应的项目的目录,对于JAVA WEB项目,为eclipse的安装目录
    2. 系统属性"java.class.path",对于eclipse,为类所对应的项目的bin目录,所谓的在eclipse中添加jar包,其实就是添加classpath所包含的路径,对于CMD,为当前目录
  2. 访问文件名相关方法
String getName():返回File对象表示的文件名、目录名
String getPath():返回File对象对应的路径名
File getAbsoluteFile():返回绝对路径对应的File对象
String getAbsolutePath():返回绝对路径名
String getParent():只是一个字符串截取,该File的上层
boolean renameTo(File newName):重命名此File对象所对应的文件或目录,成功返回true,否则返回false
  1. 文件检测的方法
boolean exists():判断File对象对应的目录或文件是否存在
boolean canWrite():是否可写
boolean canRead():
boolean isFile():判断是否为文件而不是目录
boolean isDirectory():判断是否为目录而不是文件
boolean isAbsolute():判断是否为绝对路径
  1. 获取常规文件信息
long lastModified():最后修改时间
long length():返回文件内容的长度
  1. 文件操作相关方法
boolean createNewFile():文件不存在时创建该文件,创建成功返回true,注意不会创建目录
boolean delete():删除File对象所对应的文件或目录
static File createTempFile(String prefix,String suffix):在默认的临时文件目录中创建一个临时的空文件,使用指定前缀prefix、系统随机生成的随机数和指定的后缀suffix作为文件名。prefix至少3个字节长,suffix默认为.tmp
static File createTempFile(String prefix,String suffix,File directory):在directory目录中创建临时文件
void deleteOnExit():java虚拟机退出时,删除File对象所对应的文件或目录
  1. 目录操作的相关方法
boolean mkdir():创建一个目录,成功返回true,File对象必须代表一个目录
String[] list():列出File对象的所有子文件名和目录名,不递归
File[] listFiles():列出File对象的所有子文件和目录,不递归
static File[] listRoots():列出所有根路径,静态方法
15.1.2 文件过滤器
File a = new File(".");
String[] nameList = a.list(new FilenameFilter() {
    ////dir表示对象a,name表示file中的目录或文件对象,依次指定a的所有子目录或文件进行迭代,如果该方法返回true,list方法返回的String[]中就会包括该目录名、文件名
    @Override
	public boolean accept(File dir, String name) {
		if(name.endsWith(".java")){
			return true;
		}
		return false;
	}
});

15.2 理解java的IO流

15.2.1 流的分类
  1. 输入流和输出流:按流的流向分,所谓的输入输出都是从程序所在内存角度来划分的,将内容放入内存使用输入流,将内容从内存放到外界使用输出流
    1. 输入流:只能从中读取数据,不能向其写入数据。InputStream、Reader
    2. 输出流:只能向其写入数据,不能从中读取数据。OutputStream、Writer
  2. 字节流和字符流:字节流操作的数据单元为字节(byte)=8bit,字符流操作的数据单元为字符(char)=16bit
    1. 字节流:InputStream、OutputStream
    2. 字符流:Reader、Writer
  3. 节点流和处理流:按角色分
    1. 节点流:也称低级流,可以从、向特定IO设备读写数据的流
    2. 处理流:也称高级流、包装流,对已存在的节点流进行封装后产生的流
15.2.2 流的概念模型
  1. 输入流:想象为装满水的水管,文件内容为水,流为水管,水管出口为内存,我们的目的是把水管中的水全部取出,read操作相当于取一定量的水,输入流使用隐式的记录指针表示正准备从哪个水滴开始读取,记录指针开始置于水管的开头,每次read操作后,记录指针向后移动
  2. 输出流:想象为空水管,内存中内容为水,流为水管,水管出口为要输出的位置,我们的目的是把水管填满,write操作相当于放入一定量的水,指针开始位于开头,每次write后,向后移动
  3. 处理流:
    1. 可以一次输入、输出大批量内容,而不是输入、输出一个水滴
    2. 提高了输入输出效率

15.3 字节流和字符流

15.3.1 InputStream(字节)和Reader(字符)
  1. InputStream和Reader都是抽象类,不允许直接创建对象
  2. InputStream方法
//read相当于读取到数组中,读到数组中,实际上也就读到内存里了
int read():读取单个字节(byte)(相当于取出一滴水),返回读取的字节数据,返回实际读取到的字节(byte可以转int,所以该方法返回值为int)
int read(byte[] b):从输入流中一次读取b.length个字节,放入字节数组b中,返回实际读取的字节数
int read(byte[] b,int off,int len):一次读取len个字节,存放在数组b中,放入数组b时,不是从数组b的起点开始,而是从off位置开始放置,返回实际读取的字节数
  1. Reader方法
int read():
int read(char[] cbuf):
int read(char[] b,int off,int len):
  1. 示例
package com.wsh.object;

import java.io.FileInputStream;
import java.io.IOException;

public class FileInputStreamTest {
	public static void main(String[] args) throws IOException {
	    //用InputStream子类FileInputStream来创建实例
		FileInputStream fis = new FileInputStream("./src/com/wsh/object/FileInputStreamTest.java");
		//本文件没有1024个字节这么大,即一次read可以全部返回,但如果创建较小长度,有可能返回乱码,因为GBK编码中文字符占2字节,如果read读到半个中文字符会出现乱码。即每次应至少取出2的倍数哥字节,才不会出现乱码
		byte[] bbuf = new byte[1024];
		int hasRead = 0;
		//read方法如果全读完了返回-1
		while((hasRead = fis.read(bbuf))>0){
			System.out.println(new String(bbuf , 0 , hasRead));
		}
		//物理资源需要显式回收
		fis.close();
	}
}

  1. InputStream、Reader移动记录指针的方法
void mark(int readAheadLimit):在记录指针当前位置记录一个标记(mark),FileInputStream不支持mark
boolean markSupported():判断此输入流是否支持mark操作,即是否支持记录标记
void reset():将此流的记录指针重新定位到上一次mark的位置
long skip(long n):记录指针向后移动n个字符或字节
15.3.2 OutputStream和Writer
  1. OutputStream和Writer中的方法
//write相当于将数组写出去,写出去实际上也就写到了目标位置
void write(int c):将指定的字节/字符c输出到输出流中
void write(byte[]/char[] buf):将字节数组/字符数组中的的数据输出到指定输出流中
void write(byte[]/char[] buf,int off,int len):将字节数组/字符数组从off
  1. 由于Writer以字符作为操作单位,所以Writer可以用字符串来代替字符数组,即以String对象作为参数,所以Writer包含额外两个方法
void write(String str):
void write(String str,int off,int len):将str从off位置开始,长度为leng的字符输出到指定输出流
  1. 示例
package com.wsh.object;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamTest {
	public static void main(String[] args) throws IOException {
		FileInputStream fis = new FileInputStream("./src/com/wsh/object/FileInputStreamTest.java");
		FileOutputStream fos = new FileOutputStream("D:/wusihan/newFile1.txt");
		byte[] bbuf = new byte[32];
		int hasRead = 0;
		while((hasRead = fis.read(bbuf))>0){
			//如果直接fos.write(),最后一次可能只读取了3个字节,那么bbuf只有前三个字节是新的,后32-3个字节还是倒数第二次读出的,但是还是会将bbuf所有字节都输出,于是导致倒数第二次的字节有些会被再次输出
			fos.write(bbuf,0,hasRead);
		}
		fis.close();
		fos.close();
	}
}

15.4 输入/输出流体系

15.4.1 处理流的用法

PrintStream、PrinWriter:实际上就是装饰者模式的一个应用,用PrintStream或PrinWriter对OutputStream对象进行封装,从而获得新的功能

package com.wsh.object;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;

public class PrintStreamTest {
	public static void main(String[] args) throws IOException {
		FileOutputStream fos = new FileOutputStream("D:/wusihan/newFile1.txt");
		//通常如果需要输出文本内容,都应该将输出流包装为PrintStream进行输出,注意PrintStream不能包装字符流
		PrintStream ps = new PrintStream(fos);
		ps.println("普通字符串");
		ps.println(new PrintStreamTest());
		//关闭最上层的处理流后,系统自动关闭被该处理流包装的节点流
		ps.close();
		//下面代码会引发java.io.IOException,因为fos被自动关闭
		//fos.write(new byte[5]);
	}
}

15.4.2 输入/输出流体系

StringReader、StringWriter:访问字符串的流,都属于字符流

//需为构造器传入一个字符串
StringReader sr = new StringReader(src);
StringWriter sw = new StringWriter();
//toString方法可以直接返回流内字符串的内容
sw.toString();
15.4.3 转换流

InputStreamReader、OutputStreamWriter:只有将字节流转为字符流,因为字符流更方便,没必要往麻烦了转

package com.wsh.object;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class KeyinTest {
	public static void main(String[] args) throws IOException {
	    //System.in为标准输入,是一个字节流,使用起来不太方便,包装成字符流
		InputStreamReader reader = new InputStreamReader(System.in);
		//InputStreamReader无法读取一行,所以再次包装成缓冲流BufferedReader,一般需要读入一行内容都用该流,输出缓冲流需要使用flush()方法才可以将缓冲区的内容写入实际物理节点
		BufferedReader br = new BufferedReader(reader);
		String line = null;
		//readLine方法如果没有读到回车,程序阻塞,所以控制台输入时,只有按下回车,程序才打印刚刚输入的内容
		while((line=br.readLine())!=null){
			if(line.equals("exit")){
				System.exit(1);
			}
			System.out.println("输入内容为:"+line);
		}
	}
}

15.4.4 推回输入流

PushbackInputStream、PushbackReader:推回输入流每次调用read方法时总是先从推回缓冲区读取,只有完全读完了推回缓冲区的内容,但还没装满read后的数组,才从原输入流中读取,如果程序中放入推回缓冲区的内容超出其大小,引发Pushback buffer overflow的IOException异常

  1. PushbackInputStream和PushbackReader特有方法
//注意调用unread时,原输入流的隐式指针位置是不变的,即下次调用read后,先从推回缓冲区中读内容,之后,再接着原输入流的原指针位置继续读
void unread(byte[]/char[] buf):将一个字节/字符数组放入推回缓冲区
void unread(byte[]/char[] buf,int off,int len):将一个字节/字符数组从off开始,长度为len,放入到推回缓冲区
void unread(int b):
  1. 示例
package test.wsh;

import java.io.FileReader;
import java.io.IOException;
import java.io.PushbackReader;
//本类功能为:打印PushbackTest.java文件中,new PushbackReader之前所有内容
public class PushbackTest {
	public static void main(String[] args) {
		// 创建一个PushbackReader对象,指定推回缓冲区的长度为64
		try (PushbackReader pr = new PushbackReader(new FileReader("E:\\Program Files\\Flexcube_dev\\Workspace\\AllInOneTest\\src\\test\\wsh\\PushbackTest.java"), 64)) {
			char[] buf = new char[32];
			// 用以保存上次读取的字符串内容
			String lastContent = "";
			int hasRead = 0;
			// 循环读取文件内容
			while ((hasRead = pr.read(buf)) > 0) {
				// 将读取的内容转换成字符串
				String content = new String(buf, 0, hasRead);
				int targetIndex = 0;
				// 将上次读取的字符串和本次读取的字符串拼起来,
				// 查看是否包含目标字符串, 如果包含目标字符串
				if ((targetIndex = (lastContent + content).indexOf("new PushbackReader")) > 0) {
					// 将本次内容和上次内容一起推回缓冲区
					pr.unread((lastContent + content).toCharArray());
					// "new PushbackReader"都在本次读取的content中,那么将lastContent + content中new PushbackReader之前的部分起一个字符串
					if (targetIndex > 32) {
						buf = new char[targetIndex];
					}
					// 这次会从字符缓冲区读取指定长度的内容(就是目标字符串之前的内容)
					pr.read(buf, 0, targetIndex);
					// 打印读取的内容
					System.out.print(new String(buf, 0, targetIndex));
					System.exit(0);
				} else {
					// 打印上次读取的内容
					System.out.print(lastContent);
					// 将本次内容设为上次读取的内容
					lastContent = content;
				}
			}
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}
}

15.5 重定向标准输入/输出

系统默认标准输入为键盘,标准输出与标准错误为显示器

15.5.1 System类提供的三个重定向方法
static void setErr(PrintStream err):重定向标准错误输出流
static void setIn(InputStream in):重定向标准输入
static void setOut(PrintStream err):重定向标准输出
15.5.2 示例
PrintStream ps = new PrintStream(new FileOutputStream("out.txt"));
System.setOut(ps);
//此时不再在显示器输出"吴思含"三个字,而是在out.txt中输出
System.out.println("吴思含");

FileInputStream fis = new FileInputStream("RedirectIn.java");
System.setIn(fis);
Scanner sc = new Scanner(System.in);
sc.userDelimiter("\n");
while(sc.hasNext()){
    //sc.next()取的不再是键盘输入的内容,而是RedirectIn.java中文件的内容
    System.out.println("键盘输入为:"+sc.next());
}
System.setErr(new PrintStream("C:\\Users\\含低调\\Desktop\\err.log"));
System.setOut(new PrintStream("C:\\Users\\含低调\\Desktop\\out.log"));
System.out.println("wusihan测试");
int i = 1/0;

//运行程序后,再err.log打印如下
//Exception in thread "main" java.lang.ArithmeticException: / by zero
//	at test.wsh.Chongdingxiang.main(Chongdingxiang.java:11)

//out.log打印如下
//wusihan测试

//控制台并没有任何输出结果
15.6 java 虚拟机读写其他进程的数据
package test.wsh;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ReadFromProcess {
	public static void main(String[] args) throws IOException {
	    //Runtime的exec方法可以运行平台上的其他程序,Process对象代表由java程序启动的子进程
		Process p = Runtime.getRuntime().exec("javac");
		//Process提供三个方法
		//InputStream getErrorStream():将javac命令的错误结果,转为一个输入流,以便传入内存
		//InputStream getInputStream():将javac命令的正确结果,转为一个输入流,以便传入内存
		//OutputStream getOutputStream():获取javac进程的输出流,以便将内存中(在这个程序中定义点什么)内容传输给该进程
		//例如子进程启动后,需要从控制台输出读取内容再进行处理,此时就可以用该流代替控制台的输入
		BufferedReader br = new BufferedReader(new InputStreamReader(p.getErrorStream()));
		String buff = null;
		while ((buff = br.readLine()) != null) {
			System.out.println(buff);
		}
	}
}

15.7 RandomAccessFile

支持跳转到文件的任意文件进行读写数据,且可以向文件中追加内容,而不是覆盖(FileInputStream)

15.7.1 构造方法
//mode为RandomAccessFile的访问模式
//r:只读方式打开文件,如果试图对RandomAccessFile执行写入方法抛出IOException
//rw:读写,文件不存在就创建。
//rws: 相对于rw还要求对文件内容或元数据的每个更新同步都写入到底层存储设备
//rwd:相对于rw还要求对文件内容的每个更新同步都写入到底层存储设备
RandomAccessFile(String name, String mode):
15.7.2 移动记录指针的方法
//文件指针初始位置为0
long getFilePointer():返回文件记录指针的当前位置
void seek(long pos):将文件记录指针移动到pos位置,pos值对应字节
15.7.3 其他
  1. RandomAccessFile无法在指定位置插入内容,因为新输出内容会覆盖文件中原有内容,如果需要插入内容,程序需要先把插入点后内容读入缓冲区,等把需要插入的数据写入文件后,再将缓冲区中内容追加到文件后面
  2. 通常用RandomAccessFile实现多线程断点下载

15.8 对象序列化

15.8.1 序列化的含义与意义
  1. 序列化:序列化是将对象的状态信息转换为二进制流(对象输出流)的过程,用以存储或传输。实际上将对象p,通过"对象输出流"ObjectOutputStream的writeObject§,就完成了序列化。而通过对象输入流ObjectInputStream的readObject()方法,来获取对象p,就完成了反序列化
  2. 反序列化:从IO流中恢复java对象
  3. java对象如果想支持序列化机制,需要实现Serializable或Externalizable
  4. 通常建议JavaBean(满足特定规则的java对象,以便一些工具识别)都应实现Serializable
15.8.2 使用对象流进行序列化
  1. 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
oos.writeObject(p);
  1. 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
//1. 反序列化恢复java对象时,如果没有该java对象对应的类,会引发ClassNotFoundException
//2. Person类只有有参构造器,其内打印一条语句,但反序列化时,未看到该语句,即反序列化时,无需通过构造器来初始化java对象
//3. 序列化时如果写入多个java对象,反序列化时必须按实际写入顺序读取
//4. 反序列化的java对象的父类必须具有"无参构造器","可序列化"二者之一,否则反序列化时会引发InvalidClassException
//5. 如果父类只有无参构造器,但不可序列化,那么序列化时会成功,但父类中定义的成员变量的值不会序列化到二进制流中
Person p1 = (Person)ois.readObject();
15.8.3 对象引用的序列化
  1. 如果某个类成员变量的类型不是基本类型或String,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的
  2. 对类A的对象a进行序列化时,其内的static成员变量,是不会被序列化的,static成员变量永远是最新的值
  3. 序列化算法
    1. 所有保存到磁盘中的对象都有一个序列化编号
    2. 当序列化一个对象时,程序先检查该对象是否已经被序列化过,只有该对象从未被序列化过(本次虚拟机中),系统才将该对象转换成字节序列并输出
    3. 如果已经序列化过,程序只是直接输出一个序列化编号,而不是再次重新序列化该对象
  4. 示例
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"));
Person per = new Person("孙悟空",500);
Teacher t1 = new Teacher("唐僧",per);
Teacher t2 = new Teacher("菩提祖师",per);
oos.writeObject(t1);
oos.writeObject(t2);
//将对象转换为字节序列,并写入
oos.writeObject(per);
per.setName("含");
oos.writeObject(t2);
//系统发现per已经被序列化过,所以只是输出序列化编号,所以改变后的name值不会被序列化
oos.writeObject(per);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"));
Teacher t1 = (Teacher)ois.readObject();
Teacher t2 = (Teacher)ois.readObject();
Person p = (Person)ois.readObject();
Teacher t3 = (Teacher)ois.readObject();
Teacher t4 = (Person)ois.readObject();
Person per = (Person)ois.readObject();
//都返回true
System.out.println(t1.getStudent()==p);
System.out.println(t2.getStudent()==p);
System.out.println(t2==t3);
//还是输出孙悟空
System.out.println(per.getName());
15.8.4 自定义序列化
  1. transient关键字
    1. 对某个对象序列化时,系统会把该对象的所有实例变量依次序列化,称为递归序列化
    2. 使用场景:某些实例变量为敏感信息(不想被序列化),或不可序列化(被序列化时引发NotSerializableException),可使用transient保证该实例变量不被序列化
    3. transient修饰的成员变量也称为瞬态(不能序列化,也就没法保存在硬盘,所以叫瞬态)成员变量
  2. 自定义序列化机制:可以让程序控制如何序列化各实例变量,甚至完全不序列化某些实例变量(类似transient功能)
    1. 序列化和反序列化中需要特殊处理的类,可以提供以下方法完成特殊处理
    //实际上ObjectOutputStream调用writeObject和readObject时,会利用反射,发现其要处理的对象含有private的writeObject和readObject方法,就会调用该方法,修改序列化得到的对象的属性值,或者修改反序列化得到的对象的属性值,优点类似构造器的功能,不创建对象,但初始化成员变量
    //1. 将某成员变量反转再序列化
    private void writeObject(ObjectOutputStream out) throws IOException{
        //就算黑客截获了Person对象流,看到的name也是加密后的,提高安全性
    	out.writeObject(new StringBuffer(name).reverse());
    	out.writeInt(age);
    }
    private void readObject(ObjectInputStream in)throws IOException, ClassNotFoundException{
        //恢复对象的顺序需要与writeObject写入对象的顺序一致
        //其实很好理解,这跟之前介绍的输入流和输出流的特性有关
    	this.name = ((StringBuffer)in.readObject()).reverse().toString();
    	this.age = in.readInt();
    }
    //当序列化流不完整(接收方使用的反序列化类与发送方的版本不一致,或序列化流被篡改)时,系统调用该方法初始化反序列化的对象
    private void readObjectNoData() throws ObjectStreamException{
        
    }
    
    //2. 用新对象替换要被序列化的对象
    //在写入Person对象时将该对象替换成ArrayList,即之后readObject时返回一个ArrayList对象。因为writeObject方法,在序列化某对象前,先调用该对象的writeReplace方法,如果该方法返回另一个对象,系统再调用另一个对象的writeReplace直到该方法不再返回另一个对象为止,最后再调用该对象的writeObject,对该对象状态进行序列化
    任意修饰符 Object writeReplace() throws ObjectStreamException{
    	ArrayList<Object> list = new ArrayList<Object>();
    	list.add(name);
    	list.add(age);
    	return list;
    }
    
    //3. 用新对象替换被反序列化返回的对象
    //一般用于单例类或枚举类,由于writeObject与readObject返回的对象不是同一个对象,但单例类和枚举类又不希望这种情况发生。readResolve的返回值会代替原反序列化对象,原反序列化对象会被丢弃
    任意修饰符 Object readResolve() throws ObjectStreamException{
        if(value == 1){
            return HORIZONTAL;
        }
        if(value == 2){
            return VERTICAL;
        }
        return null;
    }
    
15.8.5 另一种自定义序列化的机制
  1. 需要序列化的类实现Externalizable接口,并实现其void writeExternal(ObjectOutput out)和void readExternal(ObjectInput in)方法
package test.wsh;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Person implements Externalizable{
	
	//省略成员变量,构造器,get/set方法等
	...

	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeObject(new StringBuffer(name).reverse());
		out.writeInt(age);
	}

	@Override
	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		this.name = ((StringBuffer)in.readObject()).reverse().toString();
		this.age = in.readInt();
		
	}

}

  1. Externalizable性能比Serializable好,但编程比较复杂
15.8.6 版本
  1. 定义版本
public class Test{
    //1. 为序列化类提供serialVersionUID值,用于标识java类的序列化版本
    //2. serialVersionUID值不同时,反序列化会失败
    //3. 如果不人为设定,该值由JVM根据类的相关信息自动计算,因此往往修改后的类反序列化会失败,因为serialVersionUID值与修改前的类不同
    //4. 因此如果类的修改确实会导致反序列化失败,则应重新为该类的serialVersionUID成员变量重新分配值
    private static final long serialVersionUID=512L;
}
  1. 假设目前系统中类为A2,要反序列化的对象类型为A1,A2为在A1基础上做了修改
    1. UID不一致,反序列化报错
    2. A1比A2多成员变量,反序列得到的A2类型的对象的成员变量被忽略
    3. A1比A2少字段,反序列化得到的A2类型的对象的成员变量为null或0
    4. A1与A2成员变量名一致,但成员类型改变,反序列化失败
    5. A1与A2方法,静态成员变量,瞬态实例变量不同,反序列化无影响

15.9 NIO

15.9.1 java新IO概述
  1. 传统的输入/输出流每次只能处理处理一个字节,效率低。新IO采用内存映射文件的方式处理输入/输出流,新IO将文件或文件的一段区域映射到内存中(Buffer对象),这样就可以像访问内存一样访问文件了,效率高
  2. Channel:可看作装水的流,与类似于传统的InputStream、OutputStream,区别为其提供map方法,将一块数据映射到内存中,传统IO是面向流的处理,新IO是面向块的
  3. Buffer:可看作取水的竹筒,本质是一个数组,发送到Channel或从Channel中读取的所有对象必须先放入Buffer中
  4. Charset类:用于字节字符转换,可以将Unicode字符串映射成字节序列及逆映射
  5. Selector类:支持非阻塞式输入输出
15.9.2 使用Buffer
  1. Buffer为抽象类,对应于除了boolean外的基本类型都有对应的Buffer子类,例如:CharBuffer、ShortBuffer等
  2. 获取Buffer对象
//XxxBuffer类的静态方法
//创建一个容量为capacity的XxxBuffer对象
static XxxBuffer allocate(int capacity)
//创建一个创建成本高,读取效率也高的Buffer,叫直接Buffer。只有ByteBuffer提供该方法, 如果想使用其他类型,可以用该Buffer进行转换
static XxxBuffer allocateDirect(int capacity)
  1. Buffer中的重要概念:
    1. capacity:容量,表示该Buffer最大数据容量,即最多可以存储多少数据,不能为负,创建后不可改变
    2. limit:界限,第一个不应该被读出或写入的缓冲区位置索引。即limit后的数据既不能被读,也不能被写
    3. position:位置,指明下一个可以被读出或写入的缓冲区的位置索引,类似于IO中的记录指针
  2. Buffer对象的初始状态:position为0,limit与capacity相等
  3. Buffer中的方法
//put(),get()为相对,即position跟着改变
//put(int index,Xxx c),get(int index)为绝对,position位置不变
put():向Buffer中放入数据,放入多少数据,position跟着向后移多少
put(int index,Xxx c):直接向索引处放入数据,不会改变position位置
get():从Buffer中取出数据,取出多少,position跟着向后移多少
get(int index):从Buffer中指定索引处取出数据,不会改变position位置
flip():为get做准备,向Buffer中put数据后调用,将limit设为当前position的位置,将position设为0,即此时Buffer对象不再可以写入,同时为get数据做好准备
clear():为put做准备,Buffer中get数据后调用,将position设为0,limit设置为capacity,此时Buffer对象又可以从头写入,即为put做好准备,此时其实仍可以get到值
int capacity():返回Buffer的capacity大小
boolean hasRemaining():判断当前position和limit之间是否还有元素可供处理,即写入时,判断是否还能写入内容,读取时判断是否还有内容未被读取
int limit():返回limit位置
Buffer limit(int newLt):重新设置limit值,并返回一个新的Buffer对象
Buffer mark():设置Buffer的mark位置,只能在0和position之间做mark
int position():返回position值
Buffer position(int newPs):重新设置position,并返回一个新的Buffer对象
int remaining():返回当前position和limit之间元素个数
Buffer reset():将position转到mark所在位置
Buffer rewind():将position设为0,并取消mark
15.9.3 使用Channel
  1. Channel可以将指定文件的部分或全部直接映射成Buffer
  2. 程序不能直接读写Channel中数据,Channel只能与Buffer交互
  3. Channel对象的创建:通过传统的节点流的getChannel创建,不同的节点流返回的Channel不同。
    1. FileInputStream、FileOutputStream、RandomAccessFile会返回FileChannel
    2. InputStream获取的Channel只能读
    3. OutputStream获取的只能写
    4. RandomAccessFile获取的Channel是只读还是读写,取决于RandomAccessFile打开文件的模式,如果是读写,那么既可以读又可以写
  4. Channel中方法
//FileChannel.MapMode为执行映射的模式,有只读、读写等,position与size控制将Channel中哪些数据映射成ByteBuffer
MappedByteBufer map(FileChannel.MapMode mode,long position,long size)
//将src中内容写到某位置
write(ByteBuffer src)
//读取内容放入src中
read(ByteBuffer src)
  1. RandomAccessFile中方法
position(int position):将Channel的记录指针移动到最后
15.9.4 字符集和Charset
  1. 计算机里的文件、数据、图片都只是一种表面现象,所有文件在底层都是二进制文件,即全部是字节码,对于文本,之所以可以看到一个个字符,是因为系统将底层的二进制序列转换成字符的缘故
  2. 编码(Encode):将字符转为二进制序列,解码(Decode):将二进制序列转换成普通人能看懂的明文字符串
  3. java默认使用Unicode字符集,但很多操作系统不使用Unicode字符集,那么当从操作系统中读取数据到Java程序中时,就可能出现乱码等问题
package test.wsh;

import java.nio.charset.Charset;
import java.util.SortedMap;

public class CharsetTest {
	public static void main(String[] args) {
		//1. 返回当前Java支持的全部字符集Charset.availableCharsets获取当前JDK所支持所有字符集
		SortedMap<String,Charset> map = Charset.availableCharsets();
		for(String alias:map.keySet()) {
			System.out.println(alias+"-------->"+map.get(alias));
		}
		//2. 打印当前操作系统所使用字符集
		System.out.println(
				System.getProperty("file.encoding"));
	}
}

  1. String类中提供的字符转字节方法
byte[] getBytes(String charset)
  1. 示例:
package test.wsh;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;

public class CharsetTransform {
	public static void main(String[] args) throws CharacterCodingException {
		//1. 通过字符集创建Charset对象,这个GBK就是之前通过CharsetTest获取到的当前Java支持的字符集的一种
		Charset cn = Charset.forName("GBK");
		//2. 获取cn对象的编码器和解码器
		CharsetEncoder cnEncoder = cn.newEncoder();
		CharsetDecoder cnDecoder = cn.newDecoder();
		CharBuffer cbuff = CharBuffer.allocate(8);
		cbuff.put('孙');
		cbuff.put('悟');
		cbuff.put('空');
		cbuff.flip();
		//3. 将字符序列CharBuffer按字符集GBK转换为字节序列ByteBuffer
		ByteBuffer bbuff = cnEncoder.encode(cbuff);
		//4. 循环访问ByteBuffer中每个字节
		//打印:-53 -17 -50 -14 -65 -43 
		for(int i = 0;i<bbuff.capacity();i++) {
			System.out.print(bbuff.get(i)+" ");
		}
		//5. 重新解码成字符序列,由于CharBuffer重写了toString,所以直接可以打印其内内容
		//打印:孙悟空
		System.out.println("\n"+cnDecoder.decode(bbuff));
		//6. 可以简单调用Charset对象的encode、decode方法,使用默认字符集进行编码与解码
		//打印:java.nio.HeapByteBuffer[pos=0 lim=0 cap=0]
		System.out.println(cn.encode(cbuff));
		//由于之前bbuff已全部被读取,所以如果想重新读一遍,需要调用其flip方法重置指针位置
		bbuff.flip();
		System.out.println(cn.decode(bbuff));
	}
}

15.9.5 文件锁
  1. 文件锁目的是组织多个进程并发修改同一个文件
  2. 共享锁(S锁):又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改
  3. 排它锁(X锁):又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A
  4. 示例
package test.wsh;

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class FileLockTest {
	public static void main(String[] args) throws IOException, InterruptedException {
		FileChannel channel = new FileOutputStream("a.txt").getChannel();
		//tryLock():非阻塞,尝试失败时返回null,排他锁
		//lock():阻塞,排他锁
		//tryLock(long position,long size,boolean shared):shared为true表示共享锁,支持锁定文件中一部分内容 
		//lock(long position,long size,boolean shared):shared为true表示共享锁,支持锁定文件中一部分内容 
		//1. 锁定的10s内,其他进程无法读写a.txt
		FileLock lock = channel.tryLock();
		Thread.sleep(10000);
		lock.release();
	}
}

  1. 注意事项
    1. 某些平台,文件锁是建议性的,即使一个程序不能获得文件锁,它也可以对该文件进行读写
    2. 某些平台,不能同时锁定一个文件并把它映射进内存
    3. 文件所由Java虚拟机持有,即如果两个Java类在不同Java虚拟机上,锁无效
    4. 某些平台,关闭FileChannel时,会释放该文件上所有锁,因此应避免在一个文件上打开多个FileChannel。

15.10 Java7的NIO.2

  1. Files:用于简化IO
  2. Path、Paths:弥补File性能问题,以及其大多数方法出错时只返回失败,不抛异常
15.10.1 Path、Paths、Files核心API
  1. Path、Paths
package test.wsh;

import java.nio.file.Path;
import java.nio.file.Paths;

public class PathTest {
	public static void main(String[] args) {
		//1. 构造Path对象
		Path path = Paths.get(".");
		//2. 以多个String构造Path对象
		Path path2 = Paths.get("E:", "Program Files","Flexcube_dev","Workspace","AllInOneTest");
		//3. 获取path中包含的路径数量,例如g:\publish\codes调用该方法返回3
		//打印:1
		System.out.println(path.getNameCount());
		//4. 获取根路径
		//打印:null
		System.out.println(path.getRoot());
		//5. 获取绝对路径对应的Path
		Path absolutePath = path.toAbsolutePath();
		//打印:E:\Program Files\Flexcube_dev\Workspace\AllInOneTest\.
		System.out.println(absolutePath);
		//打印:E:\
		System.out.println(absolutePath.getRoot());
		//打印:5
		System.out.println(absolutePath.getNameCount());
		
	}
}

  1. Files
package test.wsh;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class FileTest {
	public static void main(String[] args) throws FileNotFoundException, IOException {
		Path pp = Paths.get(".\\src\\test\\wsh\\FileTest.java");
		//1. 复制文件
		Files.copy(pp, new FileOutputStream("a.txt"));
		//2 判断是否为隐藏文件
		//打印:false
		System.out.println(Files.isHidden(pp));
		//3 一次性将FileTest.java文件所有行放入集合中,字符集不正确会报错。相当于输入流功能
		List<String> lines = Files.readAllLines(pp,Charset.forName("UTF-8"));
		//打印:用一行字打印整个类中内容
		System.out.println(lines);
		//4 判断指定文件大小,单位为字节
		//打印:1426
		System.out.println(Files.size(pp));
		//5 将集合写入指定文件中,相当于输出流功能
		List<String> poem = new ArrayList<>();
		poem.add("低调处理");
		poem.add("不愿意写了");
		Files.write(Paths.get("poem.txt"), poem, Charset.forName("UTF-8"));
		//6 获取C盘总空间与可用空间
		FileStore cStore = Files.getFileStore(Paths.get("C:"));
		//打印:106571706368
		System.out.println(cStore.getTotalSpace());
		//打印:64986263552
		System.out.println(cStore.getUsableSpace());
	}
}

15.10.2 FileVisitor遍历目录
//遍历E:\\Program Files\\Flexcube_dev\\Workspace\\AllInOneTest下所有内容,并找到FileVisitorTest.java所在位置
package test.wsh;

import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;

public class FileVisitorTest {
	public static void main(String[] args) throws IOException {
		Path pp = Paths.get("E:\\Program Files\\Flexcube_dev\\Workspace\\AllInOneTest");
		//1. walkFileTree方法可以遍历pp路径下所有文件和子目录
		//2. 与下面方法类似,最多遍历maxDepth深度的文件:public static Path walkFileTree(Path start,Set<FileVisitOption> options,int maxDepth,FileVisitor<? super Path> visitor)
		Files.walkFileTree(pp, new SimpleFileVisitor<Path>() {
			//FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs):访问子目录前触发
			//FileVisitResult postVisitDirectory(T dir, IOException exc):访问子目录后触发
			//FileVisitResult visitFile(T file, BasicFileAttributes attrs):访问文件后触发
			//FileVisitResult visitFileFailed(T file, IOException exc):访问文件失败后触发
			@Override
		    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
		        throws IOException
		    {	
				System.out.println("正访问"+file+"文件");
				if(file.endsWith("FileVisitorTest.java")) {
		        	System.out.println("已找到该文件,文件路径为:"+file);
		        	//CONTINUE:继续访问
		        	//TERMINATE:停止访问
		        	//SKIP_SUBTREE:继续访问,但不访问该文件或目录的子目录树
		        	//SKIP_SIBLINGS:继续访问,但不访问该文件或目录的兄弟文件或目录
		        	return FileVisitResult.TERMINATE;
		        }else {
		        	
		        }
		        return FileVisitResult.CONTINUE;
		    }
			@Override
		    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
		        throws IOException
		    {
		        System.out.println("正访问"+dir+"路径");
		        return FileVisitResult.CONTINUE;
		    }
			
		});
	}
}	

15.10.3 WatchService监控文件变化

以前Java版本中,如果需要监控文件变化,需要启动一个后台线程,每隔一段时间扫描要监控的文件夹,看文件是否变化,十分繁琐,性能较差

  1. WatchService:监听服务
  2. WatchKey:获取到的事件的key
  3. WatchEvent:真正的事件
//持续监控E:下的文件变化,并打印
package test.wsh;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;

public class WatchServiceTest {
	public static void main(String[] args) throws IOException, InterruptedException {
		WatchService watchService = FileSystems.getDefault().newWatchService();
		//1. 将Path注册到WatchService中:用watcher监听Path对应的路径下的文件变化,events表示监听哪些类型的事件
		//WatchKey register(WatchService watcher,WatchEvent.Kind<?>... events)
		Paths.get("E:/").register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
				StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
		while(true) {
			//2. 注册后就可以使用WatchService的方法来获取被监听目录的文件变化事件
			//WatchKey take():获取下一个WatchKey,如果没发生就继续等待
			//WatchKey poll():尝试等待timeout时间来获取下一个WatchKey
			//WatchKey poll(long timeout,TimeUnit unit):获取下一个WatchKey,如果没发生就继续等待
			WatchKey key = watchService.take();
			for(WatchEvent event:key.pollEvents()) {
				System.out.println(event.context()+" 文件发生了 "+event.kind()+"事件!");
			}
			boolean valid = key.reset();
			if(!valid) {
				break;
			}
		}
	}
}

15.10.4 访问文件属性
//访问与设置文件的某些属性
package test.wsh;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.DosFileAttributeView;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.nio.file.attribute.UserDefinedFileAttributeView;
import java.nio.file.attribute.UserPrincipal;
import java.util.Date;
import java.util.List;

public class AttributeViewTest {
	public static void main(String[] args) throws IOException {
		Path testPath = Paths.get(".\\src\\test\\wsh\\FileTest.java");
		BasicFileAttributeView basicView = Files.getFileAttributeView(testPath, BasicFileAttributeView.class);
		BasicFileAttributes basicAttribs = basicView.readAttributes();
		System.out.println("创建时间:"+new Date(basicAttribs.creationTime().toMillis()));
		System.out.println("最后访问时间:"+new Date(basicAttribs.lastAccessTime().toMillis()));
		System.out.println("最后修改时间:"+new Date(basicAttribs.lastModifiedTime().toMillis()));
		System.out.println("文件大小:"+basicAttribs.size());
		
		FileOwnerAttributeView ownerView = Files.getFileAttributeView(testPath, FileOwnerAttributeView.class);
		System.out.println("该文件所属用户:"+ownerView.getOwner());
		UserPrincipal user = FileSystems.getDefault().getUserPrincipalLookupService().lookupPrincipalByName("guest");
		//修改用户,用户不存在会报错
//		ownerView.setOwner(user);
		UserDefinedFileAttributeView userView = Files.getFileAttributeView(testPath, UserDefinedFileAttributeView.class);
		//为文件写入属性,不写的话,这个java文件自定义属性是空的
		userView.write("发行者", Charset.defaultCharset().encode("Java疯狂讲义"));
		List<String> attrNames = userView.list();
		for(String name :attrNames) {
			ByteBuffer buf = ByteBuffer.allocate(userView.size(name));
			//将属性名为name的属性值,读入到buf中
			userView.read(name, buf);
			buf.flip();
			String value = Charset.defaultCharset().decode(buf).toString();
			System.out.println(name+"---->"+value);
		}
		DosFileAttributeView dosView = Files.getFileAttributeView(testPath, DosFileAttributeView.class);
		//设置隐藏与只读
		dosView.setHidden(true);
		dosView.setReadOnly(true);
	}
}

发布了32 篇原创文章 · 获赞 0 · 访问量 945

猜你喜欢

转载自blog.csdn.net/hanzong110/article/details/102518549