第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. 编程更加方便
功能字节输入流字节输出流字符输入流字符输出流节点流/处理流
文件转为流FileInputStreamFileOutputStreamReaderWriter节点流
数组转为流ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter节点流
管道转为流PipedInputStreamPipedOutputStreamPipedReaderPipedWriter节点流
String转为流StringReaderStringWriter节点流
为流提供缓冲功能BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter处理流
为字节流提供转为字符流的功能InputStreamReaderOutpuStreamWriter处理流
为输出流提供便利的打印功能PrintStreamPrintWriter处理流
为输入流提供推回重读的功能PushbackInputStreamPushbackReader处理流
为流提供写入和读取基本类型变量的功能DataInputStreamDataOutputStream处理流
为流提供序列化与反序列化的功能ObjectInputStreamObjectOutputStream处理流
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类中提供的字符转字节方法
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);
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值