天天看点

第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和intellij,其为该项目路径,E:\IdeaProjects\untitled,对于JAVA WEB项目,为eclipse的安装目录
    2. 系统属性"java.class.path"
      1. 对于intellij:为E:\IdeaProjects\untitled\out\production\untitled
      2. 对于eclipse:bin目录
      3. 所谓的在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. 处理流:也称高级流、包装流,对已存在的节点流进行封装后产生的流
      1. 处理流实际上就是装饰者模式的一个应用,例如使用PrintStream或PrinWriter对OutputStream对象进行封装,从而获得新的功能
      2. 构造器中需要传入一个节点流的流,都是处理流
      3. 关闭最上层的处理流后,系统自动关闭被该处理流包装的节点流
      4. 优点
        1. 提高输入输出的效率
        2. 编程更加方便
功能 字节输入流 字节输出流 字符输入流 字符输出流 节点流/处理流
文件转为流 FileInputStream FileOutputStream Reader Writer 节点流
数组转为流 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter 节点流
管道转为流 PipedInputStream PipedOutputStream PipedReader PipedWriter 节点流
String转为流 StringReader StringWriter 节点流
为流提供缓冲功能 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter 处理流
为字节流提供转为字符流的功能 InputStreamReader OutpuStreamWriter 处理流
为输出流提供便利的打印功能 PrintStream PrintWriter 处理流
为输入流提供推回重读的功能 PushbackInputStream PushbackReader 处理流
为流提供写入和读取基本类型变量的功能 DataInputStream DataOutputStream 处理流
为流提供序列化与反序列化的功能 ObjectInputStream ObjectOutputStream 处理流
15.2.2 流的概念模型
  1. 二进制码:水
  2. 流:水管,指针位于水管开头,每次取水、装水,都向后移动指针
  3. 输入流:取水,取空为止
  4. 输出流:装水,装满为止

15.3 InputStream、Reader、OutputStream、Writer

15.3 InputStream、Reader
  1. InputStream和Reader都是抽象类,不允许直接创建对象
  2. InputStream方法
//read相当于读取到数组中,读到数组中,实际上也就读到内存里了
//读取单个字节(byte)(相当于取出一滴水),返回读取的字节数据,返回实际读取到的字节(byte可以转int,所以该方法返回值为int)
int read()
//取水放入b,从输入流中一次读取b.length个字节,放入字节数组b中,返回实际读取的字节数
int read(byte[] b)
//一次读取len个字节,存放在数组b中,放入数组b时,不是从数组b的起点开始,而是从off位置开始放置,返回实际读取的字节数
int read(byte[] b,int off,int len)
           
  1. Reader方法
int read()
int read(char[] cbuf)
int read(char[] b,int off,int len)
           
  1. InputStream、Reader移动记录指针的方法
//在记录指针当前位置记录一个标记(mark),FileInputStream不支持mark
void mark(int readAheadLimit)
//判断此输入流是否支持mark操作,即是否支持记录标记
boolean markSupported()
//将此流的记录指针重新定位到上一次mark的位置
void reset()
//记录指针向后移动n个字符或字节
long skip(long n)
           
15.3.2 OutputStream、Writer
  1. OutputStream和Writer中的方法
//write相当于将数组写出去,写出去实际上也就写到了目标位置
//将指定的字节/字符c输出到输出流中
void write(int c)
//将字节数组/字符数组中的的数据输出到指定输出流中
void write(byte[]/char[] buf)
//将字节数组/字符数组从off
void write(byte[]/char[] buf,int off,int len)
           
  1. 由于Writer以字符作为操作单位,所以Writer可以用字符串来代替字符数组,即以String对象作为参数,所以Writer包含额外两个方法
void write(String str)
//将str从off位置开始,长度为leng的字符输出到指定输出流
void write(String str,int off,int len)
           

15.4 流的常用实现类

15.4.1 FileInputStream、FileOutputStream、FileReader、FileWriter
  1. 将文件转为流
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileInputOutputStreamTest {
    public static void main(String[] args) throws IOException {
        String userDir = System.getProperty("user.dir");
        FileInputStream fis = new FileInputStream(userDir+"//src//FileOutputStreamTest.java");
        FileOutputStream fos = new FileOutputStream("C:\\Users\\含低调\\Desktop\\FileOutputStream.java");
        int readSize =  0;
        //1. GBK编码中文字符占2字节,如果read读到半个中文字符会出现乱码。即每次应至少取出2的倍数个字节,才不会出现乱码
        byte[] b = new byte[20];
        //2. read方法,如果全读完了,会返回-1
        while((readSize=fis.read(b))>0){
            //4. 如果直接fos.write(),最后一次可能只读取了3个字节,那么bbuf只有前三个字节是新的,后32-3个字节还是倒数第二次读出的,但是还是会将bbuf所有字节都输出,于是导致倒数第二次的字节有些会被再次输出
            fos.write(b,0,readSize);
        }
        //3. 物理资源需要显式回收
        fis.close();
        fos.close();
    }
}
           
15.4.2 ByteArrayInputStream、ByteArrayOutputStream、CharArrayReader、CharArrayWriter
  1. 将字节、字符数组转为流
  2. 提供了以字节、字符数组为参数的构造器
15.4.3 PipedInputStream、PipedOutputStream、PipedReader、PipedWriter
  1. 将管道转为流
  2. 通常用于线程间通信,管道的两边连接这两个不同的线程,参考高并发中的练习一章
15.4.4 StringReader、StringWriter
  1. 将字符串转为流
//需为构造器传入一个字符串
StringReader sr = new StringReader(src);
StringWriter sw = new StringWriter();
//toString方法可以直接返回流内字符串的内容
sw.toString();
           
15.4.5 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
  1. 为流提供缓冲、mark、reset等功能
  2. 不带缓冲区的流的无参的read()方法的问题
    1. 每从硬盘读取一个字节,就向内存放入一个字节,内存处理速度快,硬盘处理速度慢,浪费了内存等硬盘的时间,也浪费了交互的时间
  3. 缓冲功能
    1. 所谓的缓冲功能,实际上只是在其内部维护了一个byte[],称为缓存
      1. 调用read()方法时,实际上是先判断该数组里是否还有值,如果有,就直接从该数组中返回值,如果没有,就调用其包装的流的read(byte b[], int off, int len)方法,重新为这个byte[]赋值,然后将byte[]中的一个元素返回
      2. 调用write(int b)方法时,先将b放到这个数组中,然后判断该数组是否满了,如果满了,调用其包装的输出流的write(byte b[], int off, int len),一次性将数组中所有内容写出,如果没满,什么操作也不做
    2. 只能提升其包装的流的一次读一个字节的read()、write(int b)方法的效率,不会提升read(byte b[], int off, int len)、write(byte b[], int off, int len)方法的效率
  4. BufferedReader新增了readLine方用于读取一行并返回,该方法如果读不到换行,才会一直阻塞,因此读文件时不会阻塞,可能因为文件为空时,默认会返回换行
  5. BufferedOutputStream、BufferedWriter新增flush方法,可以直接将缓存的内容全部写入磁盘的功能
15.4.6 InputStreamReader、OutputStreamWriter
  1. 为字节流提供字符流的功能
  2. 可以叫做转换流
  3. 将字节流转为字符流,因为字符流更方便,不存在将字符流转为字节流的转换流,因为没必要往麻烦了转
15.4.7 PrintStream、PrintWriter
  1. 为输出流提供输出一行的功能,即额外提供了println方法,为处理流
  2. 内部使用了BufferedReader和BufferedWriter,只不过println方法会自动帮助flush
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.8 PushbackInputStream、PushbackReader
  1. 为输入流提供推回重读的功能,新增方法unread
  2. 可以叫做推回输入流
  3. 推回输入流内部维护了一个byte[]或者char[],称为推回缓冲区,可以通过unread方法向这个byte[]或char[]中放入值,而每次调用read方法时总是先从这个推回缓冲区读取,只有完全读完了推回缓冲区的内容,但还没装满read方法参数列表中的那个数组,才从原输入流中继续读取,如果程序中放入推回缓冲区的内容超出其大小,引发Pushback buffer overflow的IOException异常
  4. PushbackInputStream、PushbackReader方法
//注意调用unread时,原输入流的隐式指针位置是不变的,即下次调用read后,先从推回缓冲区中读内容,之后,再接着原输入流的原指针位置继续读
//将一个字节/字符数组放入推回缓冲区
void unread(byte[]/char[] buf)
//将一个字节/字符数组从off开始,长度为len,放入到推回缓冲区
void unread(byte[]/char[] buf,int off,int len)
void unread(int b)
           
  1. 示例
import java.io.FileReader;
import java.io.IOException;
import java.io.PushbackReader;

//打印PushbackReaderTest.java类中"new PushbackReader"之前所有内容
public class PushbackReaderTest {
    public static void main(String[] args) throws IOException {
        String filePath = System.getProperty("user.dir")+"//src//PushbackReaderTest.java";
        //1. 创建一个PushbackReader对象,指定推回缓冲区的长度为两个buf长度,20*2=40,如果不指定长度,默认1位,推回时会报错
        PushbackReader pr = new PushbackReader(new FileReader(filePath),40);
        char[] buf = new char[20];
        int readSize = 0;
        String lastStr = "";
        String thisStr = "";
        int index = 0;
        while((readSize=pr.read(buf))>0){
            thisStr = new String(buf,0,readSize);
            if((index=(lastStr+thisStr).indexOf("new PushbackReader"))>0){
                pr.unread((lastStr+thisStr).toCharArray());
                char[] doubleByte = new char[index];
                pr.read(doubleByte);
                System.out.println(new String(doubleByte));
                break;
            }else{
                System.out.print(lastStr);
                lastStr = thisStr;
            }
        }
    }
}
           
15.4.9 DataInputStream、DataOutputStream
  1. 为流提供了写入和读取基本类型变量的功能,与ObjectInputStream、ObjectOutputStream使用方法很类似,一个是写入读取基本类型变量,一个是写入读取Object对象
import java.io.*;

public class DataInputOutputStreamTest {
    public static void main(String[] args) throws IOException {
        String file = "C:\\Users\\含低调\\Desktop\\test.txt";
        OutputStream os = new FileOutputStream(file);
        DataOutputStream dos = new DataOutputStream(os);
        dos.writeBoolean(true);
        dos.writeBytes("123334");
        dos.writeFloat(3.13f);
        InputStream is = new FileInputStream(file);
        DataInputStream dis = new DataInputStream(is);
        System.out.println(dis.readBoolean());
        byte[] a = new byte["123334".length()];
        dis.read(a);
        System.out.println(new String(a));
        System.out.println(dis.readFloat());
    }
}

           

15.5 重定向标准输入/输出

  1. java中默认的标准输入设备为键盘,标准输出设备为显示器,标准显示错误的设备也是显示器
  2. 标准输入、输出流,指的就是将标准设备与内存连接的这个输入、输出流
  3. System.in/out/err方法,分别可以获取标准的输入流、输出流、错误流
15.5.1 System类提供的三个重定向方法
  1. 所谓重定向就是将一个自定义的流对象,替代默认的标准的输入、输出流对象
//重定向标准错误的输出流
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);
//System.in表示标准输入的输入流
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类中提供的字符转字节方法
  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);
	}
}