[疯狂Java讲义精粹] 第十章|输入/输出

0. Java的IO通过java.io包下的类和接口来支持, java.io包下主要包括输入和输出两种IO流, 每种输入、输出流又可分为字节流和字符流两大类(字节流以字节来处理输入输出操作, 字符流以字符来处理). 此外, Java的IO流使用了一种装饰器设计模式, 将IO流分成"底层节点流"和"上层处理流", 节点流用于和底层的物理存储节点直接关联--不同的物理节点获取节点流的方式可能存在一定的差异, 但程序可以把不同的物理节点流包装成统一的处理流, 从而允许程序使用统一的输入输出代码来读取不同的物理存储节点的资源. 

- Java7在java.nio及其子包下提供了一系列全新的API, 这些API是对原有新IO(NIO)的升级, 因此也被称为NIO2. 通过这些NIO2能更高效地进行输入输出操作. 


1. File类
File类是代表与平台无关的文件和目录, 在程序中操作文件和目录都可以通过File来完成. 但File不能访问文件内容本身, 如需访问文件内容本省要用输入/输出流. 
1.1 常用的方法
  1. 访问文件名相关方法:
    - String getName(): 返回文件名或路径名(如果是路径, 返回最后一级子路径名). 
    - String getPath()
    - File getAbsoluteFile()
    - String getAbsolutePath()
    - String getParent(): 返回此File对象所对应目录(最后一级子目录)的父目录名. (当使用相对路径的File对象来获取父路径时可能引起错误, 因为该方法返回将File对象所对
    应的目录名、文件名里最后一个子目录名、子文件名删除后的结果.)
    - boolean renameTo(File newName) 
  2. 文件检测相关方法:
    - boolean exists()
    - boolean canWrite()
    - boolean canRead():
    - boolean isFile()
    - boolean isDirectory()
    - boolean isAbsolute():判断File对象所对应的文件或目录是否是绝对路径. 在UNIX/Linux/BSD上, 如果路径名开头是一条斜线(/), 表名该File对象对应一个绝对路径; Windows上, 开头是盘符, 则绝对路径. 
  3. 获取常规文件信息: 
    - long lastModified(): 返回文件的最后修改时间. 
    - long length(): 返回文件内容的长度. 
  4. 文件操作相关方法: 
    - boolean createNewFile() 
    - boolean delete()
    - static File createTempFile(String prefix, String suffix): 在默认的临时文件目录中和创建一个临时空文件, 使用给定的前缀、系统生成的随机数和给定后缀作为文件名. prefix参数至少是三个字节长. suffix参数指定后缀(形如".txt"), 可以为null, 此时将使用默认的后缀".tmp". 
    - static File createTempFile(String prefix, String suffix, File directory)
    - void deleteOnExit(): 注册一个删除钩子, 指定当Java虚拟机退出时, 删除File对象所对应的文件和目录. 
  5. 目录操作相关方法: 
    -boolean mkdir(): 试图创建一个File对象所对应的目录. 调用该方法时File对象必须对应一个路径(不是文件). 
    - String[] list(): 列出File对象的所有子文件名和路径名. 
    public String[] list(FilenameFilter filter)

    - File[] listFiles(): 列出File对象的所有子文件和路径. 
    - static File[] listRoots(): 列出系统所有的根路径(就是所有盘符). 
import java.io.*;

public class FileTest
{
	public static void main(String[] args)
			throws IOException
	{
		File file = new File(".");
		System.out.println(file.getName());
		System.out.println(file.getParent());
		System.out.println(file.getAbsoluteFile().getParent());

		File tmpFile = File.createTempFile("aaa", ".txt", file);
		tmpFile.deleteOnExit();

		File newFile = new File(System.currentTimeMillis() + "");
		System.out.println(newFile.exists());
		newFile.createNewFile();
		newFile.mkdir();   // ? 以newFile对象创建一个目录, 因为newFile已经存在所以无法创建该目录
		String[] fileList = file.list();
		for (String fileName: fileList)
		{
			System.out.println(fileName);
		}
		File[] roots = File.listRoots();
		for (File root: roots)
		{
			System.out.println(root);
		}
	}
}
* Java支持将斜线(/)当成平台无关的路径分隔符. (所以Windows下表示路径用"\\"(转义成\)行, 用"/"也行.)
* Windows平台的换行符是\r\n; UNIX/Linux/BSD的是\n. 
1.2 文件过滤器
// 列出当前路径下所有文件夹, 和以".java"结尾的文件
import java.io.*;

public class FilenameFilterTest
{
	public static void main(String[] args)
	{
		File file = new File(".");
		String[] nameList = file.list(new MyFilenameFilter());
		for (String name : nameList)
		{
			System.out.println(name);
		}
	}
}

class MyFilenameFilter implements FilenameFilter 
{
	public boolean accept(File dir, String name)
	{
		return name.endsWith(".java")
			|| new File(name).isDirectory();
	}
}
2. Java的IO流
- Java把不同的输入/输出源(键盘、文件、网络连接等)抽象表述为"流(stream)", 通过流的方式允许程序使用相同的方式访问不同输入/输出源. stream是从起源(source)到接受(sink)的"有序"数据. 
- 所有传统的流类型(类或抽象类)都放在java.io包中. 
2.1 流的分类
  1. 输入流和输出流.
    - 输入流: 只能从中读取数据, 不能写入. 
    - 输出流 
    # 划分输入/输出是从程序运行所在的内存的角度来考虑的. 
    # Java的输入流主要由InputStream和Reader作为基类, 输出流主要以OutputStream和Writer作为基类. (他们都是抽象类. 可以用FileInputStream/FileReader/FileOutputStream/FileWriter创建实例.) 
  2. 字节(byte)流和字符(char)流.
    # 用法几乎完全一样, 但字节流操作的数据单元是8位的字节, 字符流操作的数据单元是16位的字符. 
    # 字节流主要由InputStream和OutoutStream作为基类, 字符流以Reader和Writer. 
  3. 节点流和处理流. 
    - 节点流是可以从一个特定的IO设备读/写数据的流, 使用节点流是程序直接连接到实际的数据源. 节点流也叫低级流(Low Level Stream). 
    - 处理流用于的对一个已存在的流进行连接或封装, 通过封装后的流来实现读/写功能. 处理流也叫高级流. 
    # 使用处理流时程序不会直接连接到实际的数据源, 没有和实际的输入/输出节点连接. 
    # Java用处理流来包装节点流是"装饰器设计模式", 可以消除不同节点流的实现差异, 也能提供更方便的方法来完成输入/输出功能. 所以处理流也叫包装流. 
3. 字节流和字符流
InputStream/Reader是所有输入流的抽象基类, 前者是字节输入流, 后者是字符输入流.
OutputStream/Writer是所有输出类的抽象基类. 前者是字节输入流, 后者是字符输入流.
3.1 InputStream和Reader
  1. InputStream的常用方法:
    - int read(): 从输入流读取单个字节, 返回所读取的字节数据(字节数据可直接转换成int类型). 
    - int read(byte[] b): 从输入流最多读取b.length个字节的数据, 并将其存储在字节数组b中, 返回实际读取的字节数.
    - int read(byte[] b, int off, int len): 从输入流的off位置开始最多读取len个字节的数据, 并将其存储在数组b中. 返回实际读取的字节数. 
  2. Reader常用方法:
    - int read(): 字符数据可直接转换成int类型. 
    - int read(char[] cbuf)
    - int read(char[] cbuf, int off, int len)
  3. 它们都支持的移动指针记录的方法:
    - void mark(int readAheadLimit): 在记录指针当前位置记录一个标记(mark).
    - boolean markSupported(): 是否支持标记. 
    - void reset(): 将此流的记录指针重新定位到标记(mark)位置. 
    - long skip(long n): 记录指针向前移动你个字节/字符. 
3.2 OutputStream和Writer
  1. 常用方法:
    - void write(int c): 将指定的字节/字符输出到输出流中. 
    - void write(byte[]/char[] buf)
    - void write(byte[]/char[] buf, int off, int len):将字节数组/字符数组中从off位置开始, 长度为len的字节/字符输出到输出流中. 
  2. 因为字符流以字符作操作单位, 所以Writer可以用字符串代替字符数组, 即以String对象作为参数. 
4. 输入/输出流体系
4.1 处理流用法
//用PrintStream处理流包装OutputStream
import java.io.*;

public class PrintStreamTest
{
	public static void main(String[] args)
	{
		try(
			FileOutputStream fos = new FileOutputStream("test.txt");
			PrintStream ps = new PrintStream(fos))
		{
			ps.println("normal string. ");
			ps.println(new PrintStreamTest());
		}
		catch (IOException ioe)
		{
			ioe.printStackTrace();
		}
	}
}

  1. 通常只需要在创建处理流时传入一个节点流作为构造器参数即可. (需要输出文本内容的时候一般都包装成PrintStream再输出.)
  2. 关闭输入/输出流时, 只要关闭最上层的处理流即可(这样系统会自动关闭被该处理流包装的节点流.). 
4.2 输入/输出流体系

# 表中"访问文件" 到"访问字符串"的四种, 都是节点流, 必须直接与指定的物理节点关联. 
# 表中只列出了java.io包下的输入/输出流, 还有如AudioInputStream、CipherInputStream、DeflaterInputStream、ZipInputStream等具有访问音频文件、加密/解密、压缩/解压等功能的字节流, 它们位于JDK的其他包下. 
# 4个访问管道的流用于实现进程间的通信. 
4.3 转换流
转换流(InputStreamReader和OutputStreamWriter)用于将字节流转换成字符流.
import java.io.*;

public class KeyinTest
{
	public static void main(String[] args)
	{
		try(
			InputStreamReader reader = new InputStreamReader(System.in);  //* Java使用System.in代表标准输入(即键盘输入), 它是InputStream类的实例. 
			BufferedReader br = new BufferedReader(reader))
		{
			String buffer = null;
			while ((buffer = br.readLine()) != null)
			{
				if (buffer.equals("exit"))
				{
					System.exit(1);
				}
				System.out.println("print: " + buffer);
			}
		}
		catch (IOException ioe)
		{
			ioe.printStackTrace();
		}
	}
}
# Java使用System.in代表标准输入(即键盘输入), 它是InputStream类的实例, 为了更方便可以用InputStreamReader把它转换成字符串输入流. 普通的Reader读取输入内容也不太方便, 可以把Reader再次包装成BufferedReader, 用BufferedReader的readLine()方法一次读取一行内容. 
4.4 推回输入流
输入/输出流体系中有两个特殊的流, PushbackInputStream和PushbackReader, 他们都有这仨方法: 
  1. void unread(byte[]/char[] buf): 讲一个字节/字符数组内容推回到推回缓冲区里, 从而允许重复读取刚刚读取的内容. 
  2. void unread(byte[]/char[] b, int off, int len): 将一个字节/字符数组里从off开始, 长度为len字节/字符内容推回到推回缓冲区里, 从而允许重复读取刚刚读取的内容. 
  3. void unread(int b): 将一个字节/字符推回到推回缓冲区里, 从而允许重复读取刚刚读取的内容. 
5. 重定向标准输入/输出
Java的标准输入/输出分别通过System.in和System.out代表(默认情况下分别代表键盘和显示器).  但System类提供了三个重定向标准输入/输出的方法:
  1. static void setErr(PrintStream err): 重定向"标准"错误输出流. 
  2. static void setIn(InputStream in): 重定向"标准"输入流. 
  3. static void setOut(PrintStream out)
6. Java虚拟机读写其他进程的数据
使用Runtime对象的exec()方法可以运行平台上的其他程序, 该方法产生一个Process对象(Process对象代表由该Java程序启动的子进程). Process类有这三个常用方法用于让程序和其子进程通信:
InputStream getErrorStream(): 获取子进程的错误流.
InputStream getInputStream(): 获取子进程的输入流. 
InputStream getOutputStream()
7. RandomAccessFile
RandomAccessFile是Java输入/输出体系中功能最丰富的文件内容访问类, 可以读取文件内容, 也能向文件输出数据. 而且RandomAccessFile支持"随机访问"的方式(程序可以直接跳转到文件的任意地方来读写数据). *插入内容会将原有的内容覆盖, 所以可以先把插入点后的内容读入缓冲区, 最后重新输出. 
  1. RandomAccessFile对象操作文件记录指针的方法常用的:
    - long getFilePointer(): 返回文件记录指针当前的位置. 
    - void seek(long pos): 将文件记录指针定位到pos位置. 
  2. RandomAccessFile能读又能写, "所以"包含用法类似InputStream的三个read()方法和类似于OutputStream的三个write()方法. 此外还有一系列readXxx()和writeXxx(). 
  3. 构造器: RandomAccessFile有俩类似的构造器,一个以String参数指定文件名, 一个直接用File参数指定文件本身; 还有一个mode参数指定RandomAccessFile的访问模式, 有四个值:
    - "r": 以只读方式打开指定文件.
    - "rw": 读写, 若该文件不存在则创建. 
    - "rws": 读写. 相对于"rw"模式, 要求对文件内容和元数据的每个更新都同步写入到底层存储设备. 
    - "rwd": 读写. 相对于"rw"模式, 要求对文件内容的每个更新都同步写入到底层存储设备. 
  4.  向指定文件后追加内容
    import java.io.*;
    
    public class AppendContent
    {
    	public static void main(String[] args)
    	{
    		try( 
    			RandomAccessFile raf = new RandomAccessFile("out.txt", "rw"))
    		{
    			raf.seek(raf.length());
    			raf.write("追加的内容!\r\n".getBytes());
    		}
    		catch (IOException ex)
    		{
    			ex.printStackTrace();
    		}
    	}
    }
  5. 如果将记录指针移动到中间某位置开始输入, 则新输入会覆盖掉原有内容. (若要插入内容, 要先把插入点后面的内容读入缓冲区, 等需要插入的内容写入文件后再将缓冲区的内容追加到文件后面.) 
    import java.io.*;
    
    public class InsertContent
    {
    	public static void insert(String fileName, long pos, String insertContent)
    					throws IOException
    	{
    		File tmp = File.createTempFile("tmp", null);
    		tmp.deleteOnExit();
    		try (
    			RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
    			FileOutputStream tmpOut = new FileOutputStream(tmp);
    			FileInputStream tmpIn = new FileInputStream(tmp))
    		{
    			raf.seek(pos);
    			byte[] bbuf = new byte[64];
    			int hasRead = 0;
    			while((hasRead = raf.read(bbuf)) > 0)
    			{
    				tmpOut.write(bbuf, 0, hasRead);
    			}
    			raf.seek(pos);
    			raf.write(insertContent.getBytes());
    			while((hasRead = tmpIn.read(bbuf)) > 0)
    			{
    				raf.write(bbuf, 0, hasRead);
    			}
    		}
    	}
    	public static void main(String[] args)
    					throws IOException
    	{
    		insert("InsertContent.java", 45, "插入的内容\r\n");
    	}
    }
8. 对象序列化
对象的序列化(Serialize)指将Java对象写入IO流中; 反序列化(Deserialize)指从IO流中回复该Java对象. 对象序列化机制可以把内存中的Java对象转换成平台无关的二进制流, 从而将对象保存在磁盘上. 其他程序可以将这种二进制流恢复成原来的Java对象. 序列化机制使得对象可以脱离程序的运行独立存在. 
8.1 序列化的含义和意义
  1. 可序列化对象的类必须是可序列化(serializable)的, 可序列化类必须实现俩接口之一: Serializable和 Externalizable(后者是前者的子接口). 
  2. * Serializable接口是一个标记接口, 实现这种接口的无须实现任何方法, 它只表明该类的实例是可序列化的. (标识接口是没有任何方法和属性的接口, 它仅表明它的类属于一个特定的类型.) 
  3. 因为序列化是RMI(Remote Method Invoke, 远程方法调用)过程的参数和返回值都必须实现的机制, 而RMI又是Java EE技术的基础--所有的分布式应用常常需要跨平台、跨网络, 所以要求所有传递的参数、返回值必须实现序列化. 因此序列化机制是Java EE平台的基础. 通产建议: 程序创建的每个JavaBean类都实现Serializable.
8.2 使用对象流实现序列化
  1. 使用Serializable实现序列化只要让目标类实现Serializable标记接口即可(无须实现任何方法). 
  2. 使普通类可序列化:
    public class Person implements java.io.Serializable
    {
    	private String name;
    	private int age;
    	public Person(String name, int age)
    	{
    		System.out.println("xxx");
    		this.name = name;
    		this.age = age;
    	}
    }
    then, 把对象写到磁盘文件(用ObjectOutputStream的writeObject()方法把对象吸入输出流):
    import java.io.*;
    
    public class WriteObject
    {
    	public static void main(String[] args)
    	{
    		try (
    			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")))
    		{
    			Person per = new Person("KUN", 500);
    			oos.writeObject(per);  
    		}
    		catch (IOException ex)
    		{
    			ex.printStackTrace();
    		}
    	}
    }
    then, 从磁盘文件获取对象(用ObjectInputStream的ReadObject()方法(返回Object对象)): 
    public class ReadObject
    {
    	public static void main(String[] args)
    	{
    		try(
    			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt")))
    		{
    			Person p = (Person)ois.readObject();
    			System.out.println(p.getName() + p.getAge() + "years old.");
    		}
    		catch (Exception ex)
    		{
    			ex.printStackTrace();
    		}
    	}
    }
    反序列化读取的仅仅是Java对象的数据, 而不是Java类, 因此采用反序列化恢复对象时, 必须提供对象所属的class文件, 否则引起ClassNotFoundException. 
    * 如果向文件中写入了多个对象, 则使用恢复对象时必须按实际写入的顺序读取. 
    *? 当一个可序列化类有多个父类时(包括直接父类和间接父类), 这些父类要么有无参数的构造器, 要么也是可序列化的(否则反序列化时将抛出InvalidClassException异常). 如果父类时不可序列化的, 只是带有无参数的构造器, 则该父类中定义的Field值不会序列化到二进制流. 
8.3 对象引用的序列化
  1. 如果某个类的Field类型不是进本类型或String类型, 而是另一个引用类型, 那么这个引用类型必须是可序列化的, 否则又有该类型的Field的类也是不可序列化的(及时实现了Serilizable或Externalizable接口). 例如: 当程序序列化一个Teacher对象时, 如果该Teacher对象持有一个Person对象的引用, 为了在反序列化时可以正常恢复该Teacher对象, 程序会顺带将该Person对象也进行序列化, 所以Person类必须也是可序列化的, 否则Teacher类的对象将不能序列化. 
  2. 保存到磁盘的对象都有一个"序列化编号", 当程序试图序列化一个对象时, 程序先检查该对象是否已被序列化过, 只有该对象从未(在本次虚拟机中)被序列化过, 系统才会将对象转换成字节序列并输出, 否则只输出一个序列化编号. 
  3. 也因为②的原因, 多次序列化同一个可变对象时, 只有第一次使用writeObject()方法输出时才会将该对象转换成字节序列并输出, 以后即使该对象的Field值已经改变也只会输出第一次的"序列化编号"(改变的Field值不会被输出). 
8.4 自定义序列化
  1. Java会"递归序列化(如果某个Field引用到另一个对象, 被引用的对象也会被序列化; 被引用的对象的Field也是如此)"
  2. 通过在Field前面使用transient关键字修饰, 可以指定序列化时无须理会该Field. (transient只能修饰Field.) 
  3. 反序列化时试图获取用transient修饰的Field时会得到0, null等值.  (? 按"整数类型(byte、short、char、int)初始化为0; 浮点型 -> 0.0; 字符型 -> '\u0000'; 布尔型 -> false; 引用类型(类、接口和数组) ->null."来?)
  4. 自定义序列化机制可以让程序控制如何序列化各Field, 在序列化和反序列化中需要特殊处理的类应该提供如下特殊签名方法, 这些特殊的方法用以实现自定义序列化:
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {}
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {}
    private void readObjectNoData() throws ObjectStreamException {}
    - 重写writeObject()方法来定义自己的序列化机制, 该方法默认会调用out.defaultWriteObject来保存Java对象的各Field. 
    - 重写readObject()方法来定义反序列化机制, 默认调用in.defaultReadObject来恢复Java对象的非静态和非瞬态Field.  ---> transient: 瞬态. 
    - 当序列化流不完整时, readObjectNoData()方法可以用来正确地初始化反序列化的对象. 例如:接收方使用的反序列化类的版本不同与发送方, 或者接收方版本扩展的类不是发送方本本扩展的类, 或者序列化流被篡改时, 系统都会调用readObjectNoData()方法来初始化反序列化的对象. 
  5. 如果需要在序列化某个对象时替换该对象, 应为序列化类提供如下特殊方法: 
    ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException {}
    - 此方法拥有私有(private)、受保护的(protected)和包私有(package-private)等访问权限, 所以其子类有可能获得该方法. 
    - 序列化机制保证在序列化某个对象之前, 先调用该对象的writeReplace()方法, 如果该方法返回另外一个Java对象b, 则系统转为序列化这个对象b(序列化对象b时, 如果b也有writeReplace()方法, 则继续调用对象b的writePeplace()方法...一直递归下去, 知道木writeReplace()). 
    // Person.java用了writeReplace()
    import java.util.*;
    import java.io.*;
    
    public class Person implements java.io.Serializable
    {
    	private String name;
    	private int age;
    	public Person(String name, int age)
    	{
    		this.name = name;
    		this.age = age;
    	}
    	private Object writeReplace() throws ObjectStreamException
    	{
    		ArrayList<Object> list = new ArrayList<Object>();
    		list.add(name);
    		list.add(age);
    		return list;
    	}
    }
    //ReplaceTest.java 输出[KUN, 500]
    import java.util.*;
    import java.io.*;
    
    public class ReplaceTest
    {
    	public static void main(String[] args)
    	{
    		try(
    			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("replace.txt"));
    			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("replace.txt")))
    		{
    			Person per = new Person("KUN", 500);
    			oos.writeObject(per);
    			ArrayList list = (ArrayList)ois.readObject();
    			System.out.println(list);
    		}
    		catch (Exception ex)
    		{
    			ex.printStackTrace();
    		}
    	}
    }
  6. 与writeReplace()对应的有readReplace()方法. 
    ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException {}
    这个方法紧跟着readObject()之后调用(序列化某个对象之前, 会先调用该对象的writeReplace()和writeObject()两个方法, read与这类似), 该方法的返回值将会代替原来反序列化的对象, 而原来readObject()反序列化的对象被立即丢弃. 
  7. readResolve()方法在序列化单例类、枚举类时尤其有用. 
8.5 另一种自定义序列化机制
实现了Externalizable接口的类, 可以使用另一种完全由程序员决定存储和恢复对象数据的序列化机制. "实际上, 采用Externalizable接口方式的序列化与前面介绍的自定义序列化非常相似, 只是Externalizable接口强制自定义序列化." Externalizable接口有两个方法:
  1. void writeExternal(ObjectOutput out): 需要序列化的类实现writeExternal()方法来保存对象的状态. 该方法调用DataOutput(它是ObjectOutput的父接口)的方法来保存基本类型的Field值, 调用ObjectOutput的writeObject()方法来保存引用类型的Field值. 
  2. void readExternal(ObjectInput in): 需要序列化的类实现readExternal()方法来实现反序列化. 该方法调用DataInput(它是ObjectInput的父接口)的发那个发来恢复基本类型的Field值, 调用ObjectInput的readObject()方法来恢复引用类型的Field值. 
这两个方法除了签名和readObject()、writeObject()不同外, 完全一样. 
8.6 版本
  1. 反序列化对象时需要该对象的class文件, 为了确定序列化时的class文件版本, 可以为序列化类提供一个private static final的serialVersonUID值(UID: unique identifier, 唯一标识符), 该Field值用于标识该Java类的序列化版本(只要class文件升级前后SerialVersonUID值不变, 序列化机制就认定它们是同一个类版本). 
  2. 如果不显式定义serialVersionUID值的话, 将由JVM根据类的相关信息自动计算后赋给它. 修改后的类的serialVersonUID计算结果往往改变. 
  3. * 可以用bin目录下的serialver.exe 工具获得指定类的serialVersionUID值.
    获取Person的serialVersionUID: 
    >serialver Person
    启动图形界面的"序列版本检查器": 
    >serialver -show
  4. 不显式指定serialVersionUID值的另一坏处是不利于程序在不同的JVM之间移植. 因为不同的编译器计算该Field值的策略可能不同, 从而造成虽然类完全没有改变, 但是因为JVM不同, 出现序列化版本不兼容而无法正确序反列化的现象. 
8.7 重点.
  1. 对象的类名、Field(包括基本类型、数组、引用对象)都会被序列化; 方法、static Field、transient Field(瞬态Field)都不会被序列化. 
  2. 实现Serializable接口的类如果需要让某个Field不被序列化, 则可在该Field前加transient修饰符, 而不是加static关键字. 虽然static关键字也可达到这个效果, 但static关键字不能这样用(诶诶, , 就是不能用(╯#-皿-)╯~~╧═╧). 
  3. 保证序列化对象的Field类型也是可序列化的, 否则需要使用transient关键字来修饰该Field, 要不然, 该类是不可序列化的. 
  4. 反序列化对象时必须有序列化对象的class文件. 
  5. 当通过文件、网络来读取序列化后的对象时, 必须按实际写入的顺序读取. 
9. NIO
JDK 1.4加入了一系列改进I/O处理的新功能, 称新IO(NIO), 新增的类位于java.nio包及其子包下, 并且对原java.io包中的很多类以NIO为基础进行了改写. 
9.1 NIO概述
  1. NIO使用了不同的方式处理I/O, NIO采用内存映射文件的方式处理I/O, NIO将文件或文件的前一段区域映射到内存中, 这样就可以想访问内存一样来访问文件了(这种方式模拟了操作系统上的虚拟内存的概念), 这种方式比传统I/O快滴多. 
  2. 与新IO相关的包:
    - java.nio包: 主要包含各种与Buffer相关的类.
    - java.nio.channels包:主要包含与Channel和Selector相关的类.
    - java.nio.charset包:主要包含与字符集相关的类. 
    - java.nio.channels.spi包:主要包含与Channnel相关的服务提供者编程接口.
    - java.io.charset.spi包:包含与字符集相关的服务提供者编程接口. 
  3. Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象. 
  4. Channel是对传统I/O的模拟, 在新IO系统中所有的数据都要通过通道传输; Channel与传统的InputStream、OutputStream最大区别在于它提供了一个map()方法, 可以将"一块数据"一块数据映射到内存中. 如果说传统I/O是面向"流"的处理, 则NIO是面向"块"的处理. 
  5. Buffer可以被理解成一个容器, 它的本质是一个数组, 发送到Channel中的所有对象都必须首先放到Buffer中, 而从Channel中读取的数据也必须先读到Buffer中. 此处的Buffer有点类似于前面我们介绍的“竹筒”, 但该Buffer既可以像前面那样一次一次去Channel中取水, 也允许使用Channel直接将文件的某块数据映射成Buffer. 
  6. 此外NIO还提供了用于将Unicode字符串映射成字节序列以及你映射操作的Charset类, 和用于支持非阻塞式I/O的Selector类. 
9.2 使用Buffer
  1. Buffer是个抽象类, 最常用ByteBuffer, 可以在底层字节数组上进行get/set操作. 其他基本类型(除boolean)都有对应的Buffer类: CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer. 这些Buffer类, 除ByteBuffer外都有类似方法管理数据. 
  2. 这些Buffer类都没提供构造器, 这样获得一个Buffer对象:
    static XxxBuffer allocate(int capacity)   //创建一个容量capacity的XxxBuffer对象. 
    
    
    
    
  3. 常用的是ByteBuffer和CharBuffer, 其中ByteBuffer类还有个子类MappedByteBuffer, 用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结果, 通常MappedByteBuffer对象有Channel的map()方法返回. 
  4. Buffer三个重要概念: 容量(capacity)、界限(limit)、位置(position).  (Buffer还支持一个可选的标记(mark, 类似传统IO流的mark).
    - limit: 第一个不应该被读出或者写入的缓冲区位置索引. (未装入数据时, limit为capacity.) 
    - 满足: 0 <= mark <= position <= limit <= capacity
    - 示意图:
     
  5. Buffer中有俩重要方法: flip()和clear(), flip()为从Buffer中取出数据做好准备, clear()为再次向Buffer中装入数据做好准备. 
    - 当Buffer装入数据结束后, 调用Buffer的flip()方法, 将limit设置为position所在的位置, 并将position设为0, 为取数据做好准备. 
    - 当Buffer输出数据结束后, 调用Buffer的clear()方法, clear()方法不是清空Buffer的数据, 而是将position置0, 将limit置为capacity, 为装数据做好准备. 
    * 开始时(为装入数据时)Buffer的position为0, limit为capacity. 
  6. Buffer其他的常用方法:
    - int capacity(): 返回Buffer的capacity大小. 
    - int limit() 
    - int position() 
    - int remaining(): 返回position和limit之间的元素个数. 
    - boolean hasRemaining(): 判断position和limit之间是否还有元素可供处理. 
    - Buffer limit(int newLt): 设置limit的值. 
    - Buffer position(int newPs)
    - Buffer mark(): 设置mark的位置, 只能在0和position之间. Sets this buffer's mark at its position. 
    - Buffer reset(): 将position转到mark处. 
    - Buffer rewind(): 将position置0, 取消设置的mark. 
  7. 除了这些, Buffer的所有子类还提供了两个重要的方法: put()和get(), 用于向Buffer中放入和取出数据. 访问时, 分相对和绝对两种方式:
    - 相对(Relative): 从Buffer的当前position处读写, 并改变position.
    - 绝对(Absolute): 根据索引来读写, 使用绝对方式访问时不会影响position值. 
    import java.nio.*;
    
    public class BufferTest
    {
    	public static void main(String[] args)
    	{
    		CharBuffer buff = CharBuffer.allocate(8);
    		System.out.println("capacity: " + buff.capacity());
    		System.out.println("limit: " + buff.limit());
    		System.out.println("position: " + buff.position());
    		System.out.println("-------put-----------");
    		buff.put('a');
    		buff.put('b');
    		buff.put('c');
    		System.out.println("position: " + buff.position());
    		System.out.println("-------flip----------");
    		buff.flip();
    		System.out.println("limit: " + buff.limit());
    		System.out.println("position: " + buff.position());
    		System.out.println("-------get-----------");
    		System.out.println(buff.get());  // 这是相对(Relative)
    		System.out.println("position: " + buff.position());
    		System.out.println("-------clear----------");
    		buff.clear();
    		System.out.println("limit: " + buff.limit());
    		System.out.println("position: " + buff.position());
    		System.out.println(".get(2): " + buff.get(2));  // 这就是绝对(Absolute)
    		System.out.println("position: " + buff.position());
    	}
    }
    
    
    
    
9.3 使用Channel
  1. Channel类似传统的流对象, 但担忧俩主要区别:
    - Channel可以直接将指定文件的部分或全部直接映射成Buffer.
    - 程序不能直接访问Channel中的数据, Channel只能与Buffer进行交互. (想从Channel中取得数据, 要先用Buffer从Channel中取出, 然后程序从Buffer中取出; 写入是也是. ) 
  2. Channel不能用构造器直接创建, 而要用传统的节点InputStream、OutputStream的getChannel()方法返回. 不同节点流获得的Channel不一样. (如FileInputStream的获得FileChannel.) 
  3. Channel最常用的方法,
    - MappedByteBuffer map(FileChanel.MapMode mode, long position, long size): 用于将Channel对应的部分或全部数据映射成ByteBuffer. 参数mode有只读、读写等模式; position和size控制将Channel中的哪些数据映射成Buffer. 
    - read()和write()都有一系列重载形式, 用于从Buffer中读取数据或向Buffer中写入数据. 
9.4 字符集和Charset
  1. Charset用来处理字节序列和字符序列之间的转换关系, 该类包含了创建解码器和编码器的方法, 还提供了获取Charset所支持字符集的方法, Charset类是不可变的. 
  2. static SortedMap<String, Charset> availableCharsets(): 获取当前JDK支持的所有字符集. 
  3. Java默认使用Unicode字符集, 天朝程序员常用字符集:
    - GBK: 简体中文字符集. 
    - BIG5: 繁体. 
    - ISO-8859-1: ISO拉丁字母表No.1, 也叫ISO-LATIN-1.
    - UTF-8: 8位UCS(Universal Character Set, 通用字符集)转换格式.  (UTF: Unicode Transformation Format, Unicode转换格式.)
    - UTF-16BE: 十六位UCS转换格式, Big-endian(最低地址存放高位字节)字节顺序. 
    - UTF-16LE: Little-endian(最高地址存放低位字节). 
    - UTF-16: 16位UCS转换格式, 字节顺序由可选的字节顺序标记来标识. 
  4. 编码(Encode): 二进制序列 --> 字符. 解码(Decode): 字符 --> 二进制序列. 
  5. static Charset forName(String charsetName): 用字符集别名穿件相应的Charset对象. 
  6. 获得Charset对象后, 可以用对象的newDecoder()和newEncoder()方法返回CharsettDecoder和CharsetEncoder对象, 代表解码器和编码器. 调用CharsetDecoder的decode()方法可以讲ByteBuffer(字节序列)转换成CharBuffer(字符序列)...
  7. 法克啊!!!
    实际上,
    Charset类有方法直接转: CharBuffer decode(ByteBuffer bb) / ByteBuffer encode(CharBuffer cb) / ByteBuffer encode(String str). 
  8. String类的getBytes(String charset)方法返回byte[], 该方法也是使用指定的字符集将字符串转换成字节序列. 
9.5 文件锁
文件锁控制文件的全部或部分字节的访问, 能阻止多个进程并发修改同一个文件. 
  1. java.nio.channels.filelock类的lock()/tryLock()方法可以获得文件锁(FileLock)对象. 
  2. lock()和tryLock()区别: lock()试图锁定某个文件时, 如果无法得到文件锁, 程序将阻塞; tryLock()尝试锁定文件, 它将直接返回而不是阻塞, 得到文件锁就将其返回否则返回null. 
  3. 如果要锁定文件的部分内容则这样:
    - lock(long position, long size, boolean shared) / tryLock(long position, long size, boolean shared): shared为true表示该锁是个共享锁, 允许多个进程读取该文件, 但阻止其他程序获得对该文件的排他锁; 为false表示是排它锁. 
  4. 直接使用lock()/tryLock()获得的文件锁是排它锁. 可以使用FileLock的isShared判断是否是共享锁. 
  5. 处理完文件后用FileLock的release()方法释放文件锁. 
  6. * 文件锁虽然可以控制并发访问, 但对高并发访问的情况, 还是推荐使用数据库来保存程序信息, 而不是使用文件. 
  7. 某些平台上, 文件锁仅是建议性的, 不是强制性的. 这意味着即使一个程序不能获得文件锁, 也可以对文件进行读写. 
  8. 在某些平台上, 不能同步地锁定一个文件并把它映射到内存中. 
  9. 文件锁是有JVM所持有的, 如果两个Java程序使用同一个JVM虚拟机运行, 则它们不能对同一个文件进行加锁. 
  10. 在某些平台上挂壁FileChannel时会释放JVM在该文件上的所有锁, 因此应该避免对同一个被锁定的文件打开多个FileChannel. 
10. Java7的NIO.2
Java7新增了java.nio.file包及其子包, 提供了全面的文件IO和文件系统访问支持; 在java.io.channels包下新增了多个以Asynchronous开头的Channel接口和类, 用于基于异步Channel的IO. 
10.1 Path、Paths和Files核心API
  1. 早期Java只有一个File类访问文件系统, 为了弥补它的不足, 现在引入了一个Path接口, Path接口代表一个平台无关的平台路径. 此外NIO.2还提供了Files和Paths两个工具类, 其中Files包含了大量静态的工具方法来操作文件, Paths包含两个返回Path的静态工厂方法. 
  2. Paths例子
    import java.io.*;
    import java.nio.file.*;
    
    public class PathTest
    {
    	public static void main(String[] args)
    		throws Exception
    	{
    		// 以当前路径来创建Path对象
    		Path path = Paths.get(".");
    		System.out.println("path里包含的路径数量:"
    			+ path.getNameCount());   // 如g:\publish\codes返回3
    		System.out.println("path的根路径:" + path.getRoot());
    		// 获取path对应的绝对路径。
    		Path absolutePath = path.toAbsolutePath();
    		System.out.println(absolutePath);
    		// 获取绝对路径的根路径
    		System.out.println("absolutePath的跟路径:"
    			+ absolutePath.getRoot());
    		// 获取绝对路径所包含的路径数量
    		System.out.println("absolutePath里包含的路径数量:"
    			+ absolutePath.getNameCount());
    		System.out.println(absolutePath.getName(3));
    		// 以多个String来构建Path对象
    		Path path2 = Paths.get("g:" , "publish" , "codes");  // 将字符串连缀起来表示路径
    		System.out.println(path2);
    	}
    }
    
    
    
    
  3. Files例子 (运行出错, 没看出错在哪)
    import java.nio.file.*;
    import java.nio.charset.*;
    import java.io.*;
    import java.util.*;
    
    public class FilesTest
    {
    	public static void main(String[] args) 
    		throws Exception
    	{
    		// 复制文件
    		Files.copy(Paths.get("FilesTest.java") 
    			, new FileOutputStream("a.txt"));
    		// 判断FilesTest.java文件是否为隐藏文件
    		System.out.println("FilesTest.java是否为隐藏文件:"
    			+ Files.isHidden(Paths.get("FilesTest.java")));
    		// 一次性读取FilesTest.java文件的所有行
    		List<String> lines = Files.readAllLines(Paths
    			.get("FilesTest.java"), Charset.forName("gbk"));
    		System.out.println(lines);
    		// 判断指定文件的大小
    		System.out.println("FilesTest.java的大小为:"
    			+ Files.size(Paths.get("FilesTest.java")));
    		List<String> poem = new ArrayList<>();
    		poem.add("水晶潭底银鱼跃");
    		poem.add("清徐风中碧竿横");
    		// 直接将多个字符串内容写入指定文件中
    		Files.write(Paths.get("pome.txt") , poem 
    			, Charset.forName("gbk"));
    		FileStore cStore = Files.getFileStore(Paths.get("C:"));
    		// 判断C盘的总空间,可用空间
    		System.out.println("C:共有空间:" + cStore.getTotalSpace());
    		System.out.println("C:可用空间:" + cStore.getUsableSpace());
    	}
    }
    
    
    
    
10.2 使用FileVisitor遍历文件和目录
  1. java.io.file.Files类提供两个方法遍历文件和子目录: 
    - walkFileTree(Path start, FileVisitor<? super Path> visitor): 遍历statr路径下的所有文件和子目录. 
    - walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor)
  2. 上面两个方法都用了FileVisitor参数, 代表一个文件访问器, walkFileTree()方法遍历文件和子目录时都会"触发"FileVisitor中对应的方法. 
  3. FileVisitor接口中定义了四个方法: (java.io.file.FileVisitor接口有个java.io.file.SimpleFileVisitor实现类)
    - FileVisitResult postVisitDirectory(T dir, IOException exc): 访问子目录之后出发该方法. 
    - FileVisitResult perVisitDirectory(T dir, BasicFileAttributes attrs): 访问子目录之前触发该方法. 
    - FileVisitResult visitFile(T file, BasicFileAttributes attrs): 访问子目录之后触发. 
    - FileVisitResult visitFileFailed(T file, IOException exc): 访问file文件失败是触发. 
    # 它们都返回FileVisitResult, 它是个枚举类, 代表访问之后的后续行为. 值有: 
    - CONTINUE
    - SKIP_SIBLINGS: 不访问该文件/目录的兄弟文件/目录. 
    - SKIP_SUBTREE: 不访问它的子目录. 
    - TERMINATE: 终止. 
10.3 使用WatchService监控文件变化
java.nio.path类提供了监控的方法: register(WatchService watcher, WatchEvent.Kind<?>...events): watch监听该path目录下的文件变化, events指定监听哪些时间. 
import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;

public class WatchServiceTest
{
	public static void main(String[] args) 
		throws Exception
	{
		// 获取文件系统的WatchService对象
		WatchService watchService = FileSystems.getDefault()
			.newWatchService();
		// 为C:盘根路径注册监听
		Paths.get("C:/").register(watchService 
			, StandardWatchEventKinds.ENTRY_CREATE
			, StandardWatchEventKinds.ENTRY_MODIFY
			, StandardWatchEventKinds.ENTRY_DELETE);
		while(true)
		{
			// 获取下一个文件改动事件
			WatchKey key = watchService.take();    //①
			for (WatchEvent<?> event : key.pollEvents()) 
			{
				System.out.println(event.context() +" 文件发生了 "
					+ event.kind()+ "事件!");
			}
			// 重设WatchKey
			boolean valid = key.reset();
			// 如果重设失败,退出监听
			if (!valid) 
			{
				break;
			}
		}
	}
}
10.4 访问文件属性
java.nio.file.attribute包下提供了大量工具类, 用以读取、修改文件属性. 这些工具类主要分为XxxAttributeView和XxxAttributes两类. 

----------------------------------------------
看完这章我活活吐了一地狗血啊!!!
法克, 太没公德了!! 写成这样, 
这书就是糊弄事儿啊!!!恁玛
----------------------------------------------
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值