核心技术Ⅱ:I/O
输入/输出流
- 在 Java API 中:
- 输入流:可以从其中读入一个字节序列的对象;
- 输出流:可以向其中写入一个字节序列的对象;
- 使用方法的不同:
- 字节流:抽象类“InputStream”和“OutputStream”构成了I/O类层次结构的基础;
- (不便于处于Unicode形式存储的信息)
- 字符流:抽象类“Reader”和“Writer”构成的专门用于处理“Unicode”字符的单独的类层次结构;
- (读写操作基于两字节的Char值,即Unicode码元,而非基于byte值)
- 字节流:抽象类“InputStream”和“OutputStream”构成了I/O类层次结构的基础;
- 输入/输出,都是相对于内存理解。
- 何时使用字节流、何时使用字符流?
- 字节流操作的基本单元为字节;字符流操作的基本单元为Unicode码元。
- 字节流默认不使用缓冲区;字符流使用缓冲区。
- 字节流通常用于处理二进制数据(实际上它可以处理任意类型的数据,但不支持直接写入或读取Unicode码元);字符流通常处理文本数据,支持写入及读取字符(Unicode码元)。
读写字节
- “abstract int read()”方法:(“InputStream”类的抽象方法)读入一个字节,并返回读入的字节,或者在遇到输入源结尾时返回“-1”;
- (继承于InputStream的具体输入流类,必须覆盖这个方法以提供适用的功能)
- (InputStream 类还有若干个非抽象的方法,可以读入一个字节数组,或者跳过大量的字节。这些方法都要调用抽象的“read”方法,因此,各个子类都只需覆盖这一个方法)
- “int read(byte[] b)”:用于读入一个字节数组;
- “abstract void write()”方法:(“OutputStream”类的抽象方法)向某个输出位置写出一个字节;
- (“write”与“read”类似,被其他方法调用,具体输出类需要实现该方法)
- “void write(byte[] b)”:用于写出一个字节数组;
- “write”与“read”方法,在执行时都将阻塞,直到字节处理完毕;
- “available”方法:检查当前可读入的字节数量:
- 则,以下代码片段不能被阻塞:
int bytesAvailable = in.available(); if (bytesAvai1able > 0) { byte[] data = new byte[bytesAvai1able]; in.read(data); }
- “flush”方法:刷新输出流的缓冲区;
- “close”方法:关闭输入/输出流,释放系统资源;
- (关闭一个输出流的同时还会冲刷用于该输出流的缓冲区)
- 如果不关闭文件,那么写出字节的最后一个包可能将永远也得不到传递。
- 【一般使用众多的从基本的“InputStream”和“OutputStream”类导出的某个输入/输出类,而不只是直接使用字节。】
- (数据可能包含数字、字符串和对象,而不是原生字节)
相关方法:
java.io.InputStream 1.0 | |
---|---|
abstract int read() | 从数据中读人一个字节,并返回该字节。这个 read 方法在碰到输入流的结尾时返回 -1 |
int read(byte[] b) | 读入一个字节数组,并返同实际读入的字节数,或者在碰到输人流的结尾时返同 -1,这个 read 方法最多读入 b.1ength 个字节。 |
int read(byte[] b, int off, int len) | 读入个字节数组。这个 read 方法返回实际读入的字节数, 或者在碰到输入流的结尾时返回-1
参数 :
|
long skip(long n) | 在输入流中跳过n个字节,返回实际跳过的字节数(如果碰到输入流的结尾,则可能小于n)。 |
int available() | 返回在不阻塞的情况卜可获取的字节数(回忆一下,阻塞意味着当前线程将失去它对资源的占用)。 |
void close() | 关闭这个输入流。 |
void mark(int readlimit) | 在输入流的当前位置打一个标记(并非所有的流都支待这个特性。如果从输入流中已经读入的字节多于readlimit个,则这个流允许忽略这个标记。 |
void reset() | 返回到最后一个标记,随后对 read 的调用将重新读入这些字节。如果当前没有任何标记,则这个流不被重置。 |
boolean markSupported() | 如果这个流支持打标记,则返回true。 |
java.io.OutputStream 1.0 | |
abstract void write(int n) | 写出一个字节的数据。 |
void write(byte[] b) | 写出所有字节或者某个范围的字节到数组b中 。
参数:
|
void write(byte[J b, int off, int len) | |
void close() | 冲刷并关闭输出流。 |
void flush() | 冲刷输出流,也就是将所有缓冲的数据发送到目的地。 |
流家族
- 输入流与输出流的层次结构:
- “DataInputStream”和“DataOutputStream”可以以二进制格式读写所有的基本Java类型;
- “ZipInputStream”和“ZipOutputStream”可以以常见的ZIP压缩格式读写文件;
- Reader和Writer的层次结构:
- 附加接口:
- “Closeable”:
void close() throws IOException
- (“InputStream”、“OutputStream”、“Reader”和“Writer”都实现了“Closeable”接口)
- (“java.io.Closeable”接口扩展了“java.lang.AutoCloseable”接口,因此,对任何“Closeable”进行操作时,都可以使用“try-with-resource”语句)
- (“java.io.Closeable”抛出“IOException”;“java.lang.AutoCloseable”可以抛出任意异常)
- “Flushable”:
void flush()
- (“OutputStream”和“Writer”还实现了“Flushable”接口)
- “Readable”:
int read(CharBuffer cb)
- “Appendable”:
// 添加单个字符 Appendable append(char c) // 添加字符序列 Appendable append(CharSequence s)
- 只有“Writer”实现了“Appendable”;
- “Closeable”:
【CharBuffer、CharSequence、String、StringBuilder、StringBuffer】
组合输入/输出流过滤器
Java 使用了一种灵巧的机制来分离这两种职责:
- 某些输入流(如“FileInputStream”和由 URL 类的“openStream”方法返回的输入流)可以从文件和其他更外部的位置上获取字节,
- 而其他的输入流(如“DatainputStream”) 可以将字节组装到更有用的数据类型中。
Java 程序员必须对二者进行组合,通过嵌套过滤器来添加多重功能。
例如:
- 为了从文件中读入数字,首先需要创建一个FileinputStream,然后将其传递给“DataInputStream”的构造器:
FileinputStream fin = new FileinputStream("employee.dat"); DataInputStream din = new DataInputStream(fin); doub1ex = din.readDouble();
- 使用缓冲机制,以及用于文件的数据输入方法:
DataInputStream din = new DataInputStream( new BufferedinputStream( new Fi1eInputStream("emp1oyee.dat")));
- 跟踪各个中介输入流(当读人输入时,预览下一个字节,以了解它是否是你想要的值):
- (读入和推回是可应用于可回推(pushback)输入流的仅有的方法)
PushbackInputStream pbin = new PushbackInputStream( new BufferedlnputStream( new FilelnputStream("employee.dat"))); // 预读下一个字节: int b = pbin.read(); // 并非期望值时将其推回流中 if(b != '<') pbin.unread(b);
- 预先浏览并且还可以读入数字(需要一个既是可回推输入流,又是一个数据输入流的引用):
DataInputStream din = new DataInputStream( pbin = new PushbackInputStream( new BufferedInputStream( new Fi1eInputStream("emp1oyee.dat"))));
- 从一个ZIP 压缩文件中通过使用下面的输入流序列来读入数字:
ZipInputStream zin = new ZipInputStream(new Fi1eInputStream("emp1oyee.zip")); DataInputStream din = new DataInputStream(zin);
文本输入/输出(文本即字符)
在存储文本字符串时,需要考虑字符编码(character encoding)方式。
- 在 Java 内部使用的“UTF-16”编码方式中,字符串“1234”编码为“00 31 00 32 00 33 00 34”(十六进制)。但是,以“UTF-8”编码方式中为“4A 6F 73 C3 A9”;
文本:输入(“PrintWriter”)/输出(“Scanner”)
写出文本
“PrintWriter”:文本格式打印字符串和数字;
- 它有一个将“PrintWriter”链接到“FileWriter”的便捷方法:
PrintWriter out = new PrintWriter("employee.txt", "UTF-8"); // 等同于 PrintWriter out = new PrintWriter(new Fi1eOutputStream("employee.txt"), "UTF-8");
- 需要使用与“System.out”类似的“print、println”和“printf”方法;
- (可以用这些方法来打印数字:int、short、1ong、float、double,字符,boolean值,字符串,及对象)
- 如果写出器为自动冲刷模式,则每次调用“println”时,缓冲区中的字符都会被发送到目的地(打印写出器总是带缓冲区的);
- (自动冲刷机制,默认禁用)
PrintWriter out = new PrintWriter( new OutputStreamWriter( new Fi1eOutputStream("employee.txt"), "UTF-8"), true); // auto flash
- “print”方法不抛出异常,可以调用“checkError”方法来杳看输出流是否出现了某些错误;
- 关于行结束符:
- Windows 系统是“\r\n”;UIX 系统是“\n”;
- 可以通过“System.getProperty("line.separator")”获得目标系统行结束符;
- “println”方法在行中添加了对目标系统来说恰当的行结束符;
读入文本
读入文本方式:
- 最简单的任意文本处理:“Scanner”类;【见:核心技术:基本程序设计结构#输入输出】
// 通过输入流构造“Scanner”对象 Scanner in = new Scanner(System.in); // 读取输入行 String name = in.nextLine(); // 读取字符串(以空格符作为分隔) String firstName = in.next(); // 读取整数 int age = in.nextInt(); //读取浮点数 double salary = in.nextDouble();
- 短文本:(直接读取到一个字符串)
String content = new String(Files.readAllBytes(path), charset);
- 文本按行读入:(读取到一个字符串集合)
List<String> 1ines = Files.readAllLines(path,charset);
- 长文本:(利用流“Stream<String>”)
try (Stream<String> lines = Files.lines(path, charset)) { . . . }
- (Java早期版本)通过“BufferReader”类:
InputStream inputStream = . . . ; try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { String line; while ((line = in.readline()) != null) { // do something with line } }
- “BufferedReader”类有一个“lines”方法,可以产生一个“Stream<String>”对象。
- “BufferedReader”与“Scanner”不同,没有任何用于读入数字的方法。
【转换流:“InputStreamReader”、“OutputStreamWriter”】
转换流:(在“字节流”与“字符流中转换”)
- “OutputStreamWriter”:将使用选定的字符编码方式,把 Unicode 码元的输出流转换为字节流。
- 继承于“Writer”,是字符流通向字节流的桥梁;【写出:字符 -> 字节】
- “InputStreamReader”:将包含字节(用某种字符编码方式表示的字符)的输入流转换为可以产生 Unicode 码元的读入器。
- 继承于“Reader”,是字节流通向字符流的桥梁;【读入:字节 -> 字符】
Reader in = new InputStreamReader(System.in); Reader in = new InputStreamReader(new Fi1eInputStream("data.txt"), StandardCharsets.UTF_8);
- 【“字节流”用于输出输出;“字符流”:用于代码中操作】
- 应用如“基于Socket的聊天系统”:“字节流”用于传输;“字符流”在客户端显示;
- “OutputStreamWriter”与“InputStreamReader”属于字符流“Reader/Writer”结构层次;
【缓冲字符高级流:“BufferedWriter”和“BufferedReader”】
- 以“行”作为单位,进行读取/写出;
- “BufferedReader”是“缓冲字符输入流”;
- “BufferedWriter”是“缓冲字符输出流”;
【“OutputStreamWriter”、“PrintWriter”与“BufferedWriter”】
OutputStreamWriter | PrintWriter | BufferedWriter |
---|---|---|
|
|
|
- 需要字节流转换为字符流时使用“OutputStreamWriter”;需要文本输出使用“PrintWriter”;
字符编码方式
Java 针对字符使用的是 Unicode标准,有多种不同的字符编码方式:
- “UTF-8”:(最常用)
- 将每个Unicode编码点编码为1到4个字节的序列。(好处是ASCII字符集中的每个字符都只会占用一个字节)
- “UTF-16”:(Java字符串中使用的编码方式)
- 将每个Unicode编码点编码为1个或2个16位值。(分为“高位优先”和“低位优先”两种形式,通过文件的“字节顺序标记”来确定)
- UTF-8编码的文件不需要字节顺序标记;
“StandardCharsets”类具有类型为“Charset”的静态变量,用于表示每种Java虚拟机都必须支持的字符编码方式:
- StandardCharsets.UTF_8
- StandardCharsets.UTF_16
- StandardCharsets.UTF_16BE
- StandardCharsets.UTF_16LE
- StandardCharsets.IS0_8859_1
- StandardCharsets.US_ASCII
- 为了获得另一种编码方式的“Charset”, 可以使用静态的“forName”方法:
Charset shiftJIS = Charset.forName("Shift-JIS");
- 在读入或写出文本时,应该使用“Charset”对象:
// 将一个字节数组转换为字符串 String str = new String(bytes, StandardCharsets.UTF_8);
- “Charset.defaultCharset”:静态方法,返回平台使用的编码方式;
- “Charset.availableCharsets”:静态方法,返回所有可用的“Charset”实例;
- 在不指定任何编码方式时,有些方法会使用默认的平台编码方式(如“String(byte[])”构造器),而其他方法会使用UTF-8(例如“Files.readAlllines”) 。
- Oracle的Java实现有一个用于覆盖平台默认值的系统属性“file.encoding”,但并非官方支持的属性,不应使用。
读写二进制数据
DataInput 和 DataOutput 接口
“DataOutput”接口定义了下面用于以二进制格式写数组、字符、boo l e an 值和字符串的方法:(“DataInput”接口类似)
- writeChars
- writeByte
- writeInt
- writeShort
- writeLong
- writeFloat
- writeDouble
- writeChar
- writeBoo1ean
- writeUTF:(使用修订版的8位Unicode转换格式写出字符串,与直接使用标准的UTF-8编码方式不同)
- 应该:只在写出用于Java虚拟机使用的字符串时,才使用“writeUTF”方法
如,“writeInt”总是将一个整数写出为4 字节的二进制数量值,而不管它有多少位。
- 对于给定类型的每个值,所需的空间都是相同的,将其读回也比解析文本要更快。
- (类型占用空间大小、存储方式等,是由处理器类型决定的,所以带来了跨平台的问题)
- (在Java中,所有的值都按照高位在前的模式写出,而不管使用何种处理器,这使得Java 数据文件可以独立于平台)
为了以二进制操作文件内容,可以将“DataInputStream/DataOutputStream”与某个字节源相组合:
DataInputStream in = new DataInputStream(new FileInputStream("emp1oyee.dat")); DataOutputStream out = new DataOutputStream(new FileOutputStream("employee.dat"));
随机访问文件(“RandomAccessFile”)
“RandomAccessFile”类,可以在文件中的任何位置查找或写入数据。
- (磁盘文件都是随机访问的,但是与网络套接字通信的输入/输出流却不是)
- 通过使用字符串“r”(用于读入访问)或“rw”(用于读入/写出访问)
RandomAccessFile in= new RandomAccessFile("employee.dat", "r");
RandomAccessFile inOut = new RandomAccessFile("employee.dat", "rw");
- 将已有文件作为“RandomAccessFile”打开时,这个文件并不会被删除。
- “seek”方法:用于将文件指针设置到文件中的任意字节位置(参数类型位“long”)。
- “getFilePointer”方法:将返回文件指针的当前位置。
- “RandomAccessFile”类同时实现了“DataInput”和“DataOutput”接口。为了读写随机访问文件,可以使用前小节中的“readInt”/“writeInt”和“readChar”/“writeChar”之类的方法。
ZIP 文档
ZIP文档以压缩格式存储了一个或多个文件,每个ZIP 文档都有一个头,包含诸如每个文件名字和所使用的压缩方法等信息。
“ZipInputStream”用于读入ZIP文档:
- “getNextEntry”:返回一个描述这些项的 ZipEntry 类型的对象;
- “getInputStream”:传递该项可以获取用于读取该项的输入流;
- “closeEntry”:读入下一项;
ZipInputStream zin = new ZipInputStream(new FileInputStream(zipname)); ZipEntry entry; while ((entry = zin.getNextEntry()) != null) { InputStream in = zin.getInputStream(entry); // read the contents of "in" . . . zin.closeEntry(); } zin.close();
“ZipOutputStream”用于写出到ZIP文件:
- 创建一个“ZipEntry”对象;
- (ZipEntry的构造器将设置其他诸如文件日期和解压缩方法等参数,可覆盖这些设置)
- “putNextEntry”:写出新文件;
- “closeEntry”:完成写操作;
Fi1eOutputStream fout = new FileOutputStream("test.zip"); ZipOutputStream zout = new ZipOutputStream(fout); // for all files { ZipEntry ze = new ZipEntry(filename); zout.putNextEntry(ze); // send data to zout . . . zout.closeEntry(); } zin.close();
- JAR 文件只是带有一个特殊项(清单)的 ZIP 文件(可以使用“JarInputStream”和“JarOutputStream”类来读写清单项)。
相关方法:
java.util.zip.ZipInputStream 1.1 | |
---|---|
ZipInputStream(InputStream in) | 创建一个 ZipInputStream,使得我们可以从给定的 InputStream 向其中填充数据。 |
ZipEntry getNextEntry() | 为下一项返回 ZipEntry 对象,或者在没有更多的项时返回null。 |
void closeEntry() | 关闭这个 ZIP 文件中当前打开的项。之后可以通过使用 getNextEntry() 读入下一项。 |
java.util.zip.ZipIOnputStream 1.1 | |
ZipIOnputStream(OutputStream out) | 创建一个将压缩数据写出到指定的 OutputStream 的 ZipOutputStream。 |
void putNextEntry(ZipEntry ze) | 将给定的 ZipEntry 中的信息写出到输出流中,并定位用于写出数据的流,然后这些数据可以通过write()写出到这个输出流中。 |
void closeEntry() | 关闭这个ZIP文件中当前打开的项。 使用 putNextEntry 方法可以开始下一项。 |
void setLevel(int level) | 设置后续的各个DEFLATED项的默认压缩级别。 这里默认值是 Deflater.DEFAULT_ COMPRESSION。 如果级别无效, 则抛出IllegalArgumentException。
参数:
|
void setMethod (int method) | 设置用于这个ZipOutputStream的默认压缩方法,这个压缩方法会作用于所有没有指定压缩方法的项上。
参数:
|
java.util.zlp.ZipEntry 1.1 | |
ZipEntry(String name) | 用给定的名字构建一个Zip项。
参数:
|
1ong getCrc() | 返回用千这个ZipEntry的CRC32校验和的值。 |
String getName() | 返回这一项的名字。 |
long getSize() | 返回这一项未压缩的尺寸, 或者在未压缩的尺寸不可知的情况下返回-1。 |
boolean isDirectory() | 当这一项是目录时返回true。 |
void setMethod(int method) | 参数:method 用于这一项的压缩方法, 必须是 DEFLATED 或 STORED |
void setSize(long size) | 设置这一项的尺寸,只有在压缩方法是 STORED 时才是必需的。
参数:
|
void setCrc(1ong crc) | 给这一项设置CRC32校验和,这个校验和是使用CRC32类计算的。只有在压缩方法是STORED时才是必需的。
参数:
|
java.util.zip.ZipFile 1.1 | |
ZipFile(String name) | 创建一个 ZipFile, 用于从给定的字符串或 File 对象中读入数据。 |
ZipFile(File file) | |
Enumeration entries() | 返回一个 Enumeration 对象, 它枚举了描述这个 ZipFile 中各个项的 ZipEntry 对象。 |
ZipEntry getEntry(String name) | 返回给定名字所对应的项,或者在没有对应项的时候返回 null。
参数:
|
InputStream getinputStream(ZipEntry ze) | 返回用于给定项的 InputStream。
参数:
|
String getName() | 返回这个ZIP文件的路径。 |
对象输入/输出流与序列化
Java 语言支持一种称为对象序列化 (object serialization) 的非常通用的机制,它可以将任何对象写出到输出流中,并在之后将其读回。
(对象序列化是以特殊的文件格式存储对象数据的)
保存和加载序列化对象
“ObjectOutputStream”:保存对象数据。(“writeObject”方法)
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.dat"));
Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
out.writeObject(harry);
out.writeObject (boss);
“ObjectinputStream”:读取对象数据。(“readObject”方法)
ObjectlnputStream in = new ObjectlnputStream(new FilelnputStream("employee.dat"));
Employee el = (Employee) in.readObject();
Employee e2 = (Employee) in.readObject();
- 需要保存对象的类必须实现“Serializable”接口:(“Serializable”接口没有任何方法)
保存“对象网络”:当一个对象被多个对象共享,作为它们各自状态的一部分时,每个对象都是用一个序列号(serial number)保存的。
保存对象时:
- 对遇到的每一个对象引用都关联一个序列号;
- 对于每个对象:
- 当第一次遇到时,保存其对象数据到输出流中;
- 如果已被保存过,那么只写出“与之前保存过的序列号为x的对象相同”;
读取对象时:
- 对于对象输入流中的对象:第一次遇到其序列号时,构建它,并使用流中数据来初始化它,然后记录这个顺序号和新对象之间的关联;
- 当遇到 “与之前保存过的序列号为x的对象相同”标记时,获取与这个序列号相关联的对象引用;
实例:
代码:
package objectStream; import java.io.*; class ObjectStreamTest { puolic static void main(String [] args) throws IOException, ClassNotFoundException { Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1); Manager carl = new Manager("Carl Cracker", 80000, 1987, 12, 15); carl .setSecretary(harry); Manager tony= new Manager("Tony Tester", 40000, 1990, 3, 15); tony. setSecretary(harry); Employee[] staff = new Employee[3]; staff[O] = carl; staff[l] = harry; staff[2] = tony; // save a11 emp1oyee records to the fi1e emp1oyee.dat try (ObjectOutputStream out = new ObjectOutputStream(new Fi1eOutputStream("employee.dat"))) { out.writeObject(staff); } try (ObjectInputStream in= new ObjectInputStream(new FileInputStream("employee.dat"))) { // retrieve all records into a new array Employee[] newStaff = (Employee[)) in.readObject(); // raise secretary's salary newStaff[l].raiseSalary(lO); // print the newly read employee records for (Employee e : newStaff) System.out.println(e); } } }
对象序列化的文件格式【了解】
修改默认的序列化机制
防止域被序列化
Java 拥有一种很简单的机制来防止域被实例化:将域标记为“transient”。
- 某些数据域是不可以序列化的,在重新加载对象或将其传送到其他机器上时都是没有用处的;
- 某些域属于不可序列化的类,也需要将它们标记成“transient”;
修改序列化默认读写
可序列化的类可以定义具有下列签名的方法:
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException;
private void writeObject(ObjectOutputStream out)
throws IOException;
之后,数据域就再也不会被自动序列化,取而代之的是调用这些方法。
示例:
public class LabeledPoint implements Serializable
{
private String label;
private transient Point2D.Doub1e point; // 不可序列化
...
private void writeObject(ObjectOutputStream out) throws IOException
{
// 调用该方法,写出对象描述符,和 String 域的1abel
out.defaultWriteObject();
// 写出不可序列化的point
out.writeDouble(point.getX());
out.writeDouble(point.getY()) ;
}
private void readObject(ObjectInputStream in) throws IOException
{
// 调用该方法,读入对象描述符,和 1abel 域
in.defaultReadObject();
// 读入不可序列化的point
doub1e x = in.readDoub1e();
double y = in.readDoub1e();
point = new Point2D.Double(x, y);
}
}
自定义序列化机制
自定义序列化,类必须实现“Externalizable”接口,并定义两个方法:
public void readExterna1(ObjectInputStream in)
throws IOException, ClassNotFoundException;
pub1ic void writeExternal(ObjectOutputStream out)
throws IOException;
这些方法对包括超类数据在内的整个对象的存储和恢复负全责。
- “readObject”和“writeObject”方法是私有的,并且只能被序列化机制调用。
- “readExternal”和“writeExternal”方法是公共的。(“readExternal”还潜在地允许修改现有对象的状态)
示例:
public class Employee implements Externalizable
{
private String name;
private Double salary;
private LocalDate hireOay;
...
private void writeExterna1(ObjectOutput s) throws IOException
{
name = s.readUTF();
salary = s.readDouble();
hireDay = LocalDate.ofEpochDay(s.readLong());
}
private void readExternal(ObjectInput s) throws IOException
{
s.writeUTF(name);
s.writeDouble(salary);
s.writeLong(hireDay.toEpochDay());
}
}
序列化单例和类型安全的枚举
在序列化和反序列化时,如果目标对象是唯一的(“单例”和“类型安全的枚举”),默认的序列化机制是不适用的:
- (使用Java语言的“enum”结构,不必担心序列化)
- 记住向遗留代码中所有类型安全的枚举以及向所有支持单例设计模式的类中添加“readResolve”方法;
如有:
public class Orientation { public static final Orientation HORIZONTAL = new Orientation(l); public static final Orientation VERTICAL = new Orientation(2); private int value; private Orientation(int v) { value = v; } }
写出一个 Orientation 类型的值, 并再次将其读回:
Orientation original = Orientation.HORIZONTAL; ObjectOutputStream out = . . . ; out.write(original); out.close(); ObjectInputStream in = . . . ; Orientation saved = (Orientation) in.read();
测试:
if (saved == Orientation.HORIZONTAL) . . .
读回对象并非枚举对象,即创建了一个新对象(即使构造器私有)。
解决:类中定义了“readResolve”方法
- (对象被序列化之后就会调用它)
- “readResolve”方法将检查“value”域并返回恰当的枚举常量:
protected Object readResolve() throws ObjectStreamException { if (value == 1) return Orientation.HORIZONTAL; if (value == 2) return Orientation.VERTICAL; throw new ObjectStreamExcepti on(); // this shou1dn't happen }
版本管理(serialVersionUID)
如果一个类具有名为“serialVersionUID”的静态数据成员,它就不再需要人工地计算其指纹,而只需直接使用这个值:
- (对象输入流将拒绝读入具有不同指纹的对象)
class Employee implements Serializable // version 1.1 { . . . public static final long serialVersionUID = -1814239825517340645L; }
- 类的方法产生了变化:那么在读人新对象数据时是不会有任何问题的。
- 类的数据域产生了变化:(只会考虑非瞬时和非静态的数据域)
- 如果两部分数据域之间“名字匹配而类型不匹配”,因为这两个对象不兼容(对象输入流不会尝试将一种类型转换成另一种类型);
- 如果“被序列化的对象具有在当前版本中所没有的数据域”,那么对象输入流会忽略这些额外的数据;
- 如果“当前版本具有在被序列化的对象中所没有的数据域”,那么这些新添加的域将被设置成它们的默认值(如果是对象则是“null”,如果是数字则为“0”,如果是boolean值则是“false”)。
获取类的指纹(JDK 中的单机程序“serialver”):
- 命令:
serialver Employee
- 结果:
Employee: static final long serialVersionUID = -1814239825517340645L;
- 结果:
- 图形化:
克隆与序列化
序列化机制提供了一种克隆对象的简便途径:
- (只要对应的类是可序列化的即可)
直接将对象序列化到输出流中,然后将其读回。这样产生的新对象是对现有对象的一个深拷贝(deep copy)。
- 在此过程中,不必将对象写出到文件中,因为可以用“ByteArrayOutputStream”将数据保存到字节数组中。
操作文件
“Path”接口和“Files”类封装了在用户机器上处理文件系统所需的所有功能:
- (“Path”、Files,是在 JavaSE 7 中新添加的,比自 JDK 1.0 以来的“File”类要方便得多)
- “Path”接口有一个“toFile”方法,“Files”类有一个“toPath”方法。
- 均位于NIO包中:“java.nio.file.Path”、“java.nio.file.Files”;
- 所有在“java.io”中的类都将相对路径名解释为以用户工作目录开始,可以通过调用“
System.getProperty("user.dir")
” 来获得这个信息。 - 关于文件分隔符:
- 【反斜杠字符“\”在 Java 字符串中是转义字符】
- 在 Windows 风格的路径名中,
- 使用“\\”(如,“C:\\Windows\\win.ini”);
- 使用单斜杠字符“/”(如,“C:/Windows/win.ini”):大部分Windows文件处理的系统调用都会将斜杠解释成文件分隔符。
- 【对于可移植的程序来说,应该使用程序所运行平台的文件分隔符:通过常量字符串“
java.io.File.separator
”荻得它】
- 默认分隔符:类Unix 文件系统是“/”, Windows 是“\”。
Path
Path表示的是一个目录名序列,其后还可以跟着一个文件名:
- 路径中的第一个部件可以是根部件(而允许访问的根部件取决于文件系统)
- 以根部件开始的路径是绝对路径;否则,就是相对路径。
Path absolute = Paths.get("/home", "harry"); Path relative = Paths.get("myprog", "conf", "user.properties");
- 静态的“Paths.get”方法接受一个或多个字符串, 并将它们用默认文件系统的路径分隔符(类Unix 文件系统是“/”, Windows 是“\”)连接起来。
- (如果其表示的不是给定文件系统中的合法路径,那么就抛出“InvalidPathException”异常)
String baseDir = props.getProperty("base.dir"); // May be a string such as /opt/myprog or c:\Program Files\myprog Path basePath = Paths.get(baseDir); // OK that baseDir has separators
常用方法:
- “resolve”:解析路径;
- 对于“p.resolve(q)”:
- 如果 q 是绝对路径 , 则结果就是 q;
- 否则,根据文件系统的规则,将“p后面跟若q”作为结果;
Path workRelative = Paths.get("work"); Path work Path = basePath. resol ve(workRelative); // 等价于 Path workPath = basePath. resol ve("work");
- “resolveSibling”:通过解析指定路径的父路径产生其兄弟路径;
// workPath 是 “/opt/myapp/work” Path tempPath = workPath.reso1veSib1ing("temp"); // 创建 “/opt/myapp/temp”
- “relativize”:(与“resolve”相对立)
- 例如,以“/home/cay”为目标对“/home/fred/myprog”进行相对化操作,会产生“../fred/myprog”;
- “normalize”:移除所有冗余的“.”和“..” 部件(或者文件系统认为冗余的所有部件);
- 例如,规范化“/home/cay/.. /fred/./myprog”将产生“/home/fred/myprog”;
- “toAbsolutePath”:产生给定路径的绝对路径(该绝对路径从根部件开始);
- 例如,“/home/fred/input.txt”或“c:\Users\fred\input.txt”;
- Path 类有许多有用的方法用来将路径断开:
Path p = Paths.get("/home", "fred", "myprog.properties"); Path parent = p.getParent(); // the path /home/fred Path file = p.getFileName(); // the path myprog.properties Path root = p. getRoot (); // the path /
Files
读写文件
Files类使得普通文件操作变得快捷:
- (相对于处理“FileInputStream”、“FileOutputStream”、“BufferedReader”和“BufferedWriter”的繁复操作,更为便捷)
- 如果处理的文件长度比较大,或者是二进制文件,应该使用所熟知的输入/输出流或者读入器/写出器:
InputStream in = Files.newInputStream(path); OutputStream out = Files.newOutputStream(path); Reader in = Files.newBufferedReader(path, charset); Writer out = Files.newBufferedWriter(path, charset);
- 读取文件的所有内容,并将文件当作字符串读入:
byte[] bytes = Files.readAllBytes(path); // 将文件当作字符串读入 String content = new String(bytes, charset);
- 将文件当作行序列读入:
List<String> lines = Files.readAlllines(path, charset);
- 将文件当作字符串读入:
Files.write(path, content.getBytes(charset));
- 向指定文件追加内容:
Files.write(path, content.getBytes(charset), StandardOpenOption.APPEND);
- 将一个行的集合写出到文件中:
Files.write(path, lines);
创建文件和目录
- 创建新目录:
// 路径中除最后一个部件外, 其他部分都必须是已存在的 Files.createDirectory(path); // 创建路径中的中间目录 Files.createDirectories(path);
- 创建一个空文件:
Fi1es.createFile(path);
- 如果文件已经存在了,那么这个洞用就会抛出异常。
- 检查文件是否存在和创建文件是原子性的,如果文件不存在,该文件就会被创建,并且其他程序在此过程中是无法执行文件创建操作的。
- 在给定位置或者系统指定位置创建临时文件或临时目录:
Path newPath = Files.createTempFile(dir, prefix, suffix); Path newPath = Files.createTempFile(prefix, suffix); Path newPath = Files.createTempDirectory(dir, prefix); Path newPath = Files.createTempDirectory(prefix); // dir是一个Path对象,prefix和suffix是可以为null的字符串。
- 例如,调用“Files.createTempFile(null, ".txt")”可能会返回一个像“/tmp/1234405522364837194.txt”这样的路径。
- 在创建文件或目录时, 可以指定屈性 , 例如文件的拥有者和权限。
复制、移动和删除文件
- 复制文件:
Files.copy(fromPath, toPath);
- 移动文件(复制并删除原文件:
Files.move(fromPath, toPath);
- 如果目标路径已经存在,那么复制或移动将失败:
- 覆盖已有的目标路径:“REPLACE_EXISTING”;
- 复制所有的文件属性:“COPY_ ATTRIBUTES”
Files.copy(fromPath, toPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
- 将移动操作定义为原子性的:(要么移动操作成功完成,要么源文件继续保持在原来位置)
Files.move(fromPath, toPath, StandardCopyOption.ATOMIC_MOVE);
- 将一个输人流复制到Path中,将一个Path复制到输出流中:
Files.copy(inputStream, toPath); Files.copy(fromPath, outputStrearn);
- 删除文件:
// 如果要删除的文件不存在,这个方法就会抛出异常; Files.delete(path); // 不会抛出异常,还可以用来移除空目录; boo1ean de1eted = Fi1es.de1etelfExists(path);
获取文件信息
- 检查路径的某个属性的结果:
- exists
- isHidden
- isReadable, isWritable, isExecutable
- isRegularFile, isDirectory, isSymboliclink
- “size”:返回文件的字节数
long fileSize = Files.size(path);
- “getOwner”:将文件的拥有者作为“java .nio.file.attribute.UserPrincipal”的一个实例返回;
- 所有的文件系统都会报告一个基本属性集,它们被封装在“BasicFileAttributes”接口中;基本文件属性包括:
- 创建文件、最后一次访问以及最后一次修改文件的时间,这些时间都表示成“java.nio.file.attribute .FileTime”;
- 文件是常规文件、目录还是符号链接,抑或这三者都不是;
- 文件尺寸;
- 文件主键,这是某种类的对象,具体所屈类与文件系统相关,有可能是文件的唯一标识符,也可能不是;
- 获取这些属性:
BasicFi1eAttributes attributes = Fi1es.readAttributes(path, BasicFi1eAttributes.c1ass);
- 如果文件系统兼容POSIX, 则可以获取一个“PosixFileAttributes”实例:
PosixFileAttributes attributes = Fi1es.readAttributes(path, PosixFileAttributes.c1ass);
- “BasicFileAttributes”接口方法:
- FileTime creationTime()
- FileTime lastAccessTime()
- FileTime lastModifiedTime()
- boolean isRegularFile()
- boolean isDirectory()
访问目录中的项
- “Files.list”:(静态方法)
- 返回一个可以读取目录中各个项的“Stream<Path>”对象;
- 目录是被惰性读取的;
- 该方法不会进入子目录;
try(Stream<Path> entries = Files.list(pathToDirectory)) { . . . }
- “File.walk”:
- 遍历目录时,在进入子目录之前,会继续访问它的兄弟项;
try(Stream<Path> entries = Files.walk(pathToRoot)) { // Contains all descendants, visited in depth-first order }
- 限制想要访问的树的深度:
File.walk(pathToRoot, depth);
- 如果要过滤 walk 返回的路径,并且过滤标准涉及与目录存储相关的文件属性,应该使用“find”方法代替“walk”;
- 示例,使用“Files.walk”方法来将一个目录复制到另一个目录:
Files.walk(source).forEach(p -> { try { Path q = target.reso1ve(source.relativize(p)); if (Files.isDirectory(p)) Fi1es.createDirectory(q); else Files.copy(p, q); } catch (IOException ex) { throw new UncheckedIOException(ex); } });
- 但,无法使用“Files.walk”方法来删除目录树,因为需要在删除父目录之前必须先删除子目录。
使用目录流(“File.newDirectoryStream”、“walkFileTree()”)
“File.newDirectoryStream”:对遍历过程进行更加细粒度的控制时,应该使用“File.newDirectoryStream”对象,它会产生一个“DirectoryStream”:
- (“Files.walk”不满足更加细粒度控制)
“DirectoryStream”:是“Iterable”的子接口,因此可以在增强的for循环中使用目录流:
- (并非“java.util.stream.Stream”的子接口)
try (DirectoryStream<Path> entries = Files.newDirectoryStream(dir))
{
for (Path entry : entries)
// Process entries
}
可以用glob栈式来过滤文件:
try (DirectoryStream<Path> entries = Files.newDirectoryStream(dir, "*.java"))
- 如果使用Windows的glob语法,则必须对反斜杠转义两次:一次为glob语法转义,一次为Java字符串转义;如:“Files.newDirectoryStream(dir, "C:\\\\")”;
如果想要访问某个目录的所有子孙成员,可以转而调用“walkFileTree”方法,并向其传递一个“FileVisitor”类型的对象,这个对象会得到下列通知:
- 在遇到一个文件或目录时:
FileVisitResult visitFile(T path, BasicFileAttributes attrs)
- 在一个目录被处理前:
FileVisitResult preVisitDirectory(T dir, IOException ex)
- 在一个目录被处理后:
FileVisitResult postVisitDirectory(T dir, IOException ex)
- 在试图访问文件或目录时发生错误,例如没有权限打开目录:
FileVisitResult visitFileFailed(path, IOException)
对于上述每种悄况 , 都可以指定是否执行下面的操作:
- 继续访问下一个文件:
FileVisitResult.CONTINUE
- 继续访问,但是不再访问这个目录下的任何项了:
FileVisitResult.SKIP_SUBTREE
- 继续访问,但是不再访问这个文件的兄弟文(和该文件在同一个目录下的文件)了:
FileVisitResult.SKIP _SIBLINGS
- 终止访问:
FileVis itResult.TERMINATE
当有任何方法抛出异常时,就会终止访问,而这个异常会从“walkFileTree”方法中抛出,
- “walkFileTree”方法可以接受“FileVisitor<? Super Path>”类型的参数,但是Path并没有多少超类型;
- “FileVisitor”接口是泛化类型,但是不太可能会使用除“FileVisitor<Path>”之外的东西。
示例1:打印给定目录下的所有子目录
Files.walkFileTree(Paths.get("/"), new Simplefi1eVisitor<Path>()
{
public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) throws IOException
{
System.out.println(path);
return FileVisitResult.CONTINUE;
}
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
{
return FileVisitResult.CONTINUE;
}
public FileVisitResult visitFileFailed(Path path, IOException exc) throws IOException
{
return FileVisitResult.SKIP _SUBTREE;
}
});
示例2:删除目录树
// De1ete the directory tree starting at root
Files.walkFileTree(root, new Simp1eFileVisitor<Path>()
{
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
{
Files.delete(file);
return FileVisitResult.CONTINUE;
}
public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException
{
if (e != null) throw e;
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
ZIP 文件系统(“FileSystem”)
- 从 ZIP 文件建立文件系统:
FileSystem fs = FileSystems.newFileSystem(Paths.get(zipname), nu11);
- 从 ZIP 文档中复制文件:
Fi1es.copy(fs.getPath(sourceName), targetPath);
- 列出 ZIP 文档中的所有文件:
Fi1es.wa1kFileTree(fs.getPath("/"), new Simp1eFi1eVisitor<Path>() { public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println(fi1e); return Fi1eVisitResult.CONTINUE; } });
- ZIP文件系统(“java.nio.file.FileSystems”包中)比前一节“#ZIP 文档”的API(“java.util.zip”包中)更方便;
相关方法:
java.nio.file.FileSystems 7
- static FileSystem newFileSystem(Path path, Classloader loader)
- 对所安装的文件系统提供者进行迭代,并且如果loader不为nul1, 那么就还迭代给定的类加载器能够加载的文件系统,返回由第一个可以接受给定路径的文件系统提供者创建的文件系统。默认情况下,对于ZIP文件系统是有介个提供者的,它接受名字以“.zip”或“.jar”结尾的文件。
java.nio.file.FileSystem 7
- static Path getPath(String first, String ... more)
- 将给定的字符串连接起来创建一个路径。
内存映射文件(FileChannel)
大多数操作系统都可以利用虚拟内存实现来将一个文件或者文件的一部分“映射”到内存中。然后,这个文件就可以当作是内存数组一样地访问,这比传统的文件操作要快得多。
- 对于中等尺寸文件的顺序读入则没有必要使用内存映射。
- “java.nio”包使内存映射变得十分简单。
内存映射文件的性能
从文件中获得一个通道(channel), 通道是用于磁盘文件的一种抽象,使我们可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。
FileChannel channel = FileChannel.open(path, options);
通过询用“FileChannel”类的“map”方法从这个通道中获得一个“ByteBuffer”。可以指定想要映射的文件区域与映射模式:
- “Fi1eChanne1.MapMode.REAO_ONLY”:所产生的缓冲区是只读的,任何对该缓冲区写入的尝试都会导致“ReadOnlyBufferException”异常;
- “FileChannel.MapMode.READ_WRITE”:所产生的缓冲区是可写的,任何修改都会在某个时刻写同到文件中。
- (其他映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的确切行为是依赖于操作系统的)
- “FileChannel.MapMode.PRIVATE”:所产生的缓冲区是可写的,但是任何修改对这个缓冲区来说都是私有的,不会传播到文件中。
一旦有了缓冲区,就可以使用“ByteBuffer”类和“Buffer”超类的方法读写数据了:
- 缓冲区支持顺序和随机数据访问,它有一个可以通过“get”和“put”操作来移动的位置。
- 顺序访问:
while (buffer.hasRemaining ()) { byte b = buffer.get(); . . . }
- 随机访问:
for (int i = O; i < buffer.limit(); i++) { byte b = buffer.get(i); . . . }
- 读写字节数组:
get(byte[] bytes) get(byte[], int offset, int length)
- 读入在文件中存储为二进制值的基本类型值:
- getInt
- getLong
- getShort
- getChar
- getFloat
- getDouble
- 若需要以低位在前的排序方式处理包含二进制数字的文件(Java对二进制数据使用高位在前的排序机制):
buffer.order(ByteOrder.LITTLE_ENDIAN);
- 查询缓冲区内当前的字节顺序:
ByteOrder b = buffer.order();
- “java.util.zip”包中包含一个 CRC32 类,可以使用下面的循环来计算一个字节序列的校验和:
CRC32 crc = new CRC32(); while (more bytes) crc.update(next byte) long checksum = crc.getValue();
示例代码:
package memoryMap;
import java.io.•;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.uti1.zip.*;
pub1ic class MemoryMapTest
{
// 普通输入流
public static long checksuminputStream(Path fi1ename) throws IOException
{
try (InputStream in = Fi1es.newInputStream(fi1ename))
{
CRC32 crc = new CRC32();
int c;
while ((c = in.read()) != -1)
crc.update(c);
return crc.getValue();
}
}
// 带缓冲的输入流
public static long checksumBufferedlnputStream(Path fi1ename) throws IOException
{
try (InputStream in = new BufferedInputStream(Fi1es.newInputStream(fi1ename)))
{
CRC32 crc = new CRC32();
int c;
while ((c = in.read()) != -1)
crc.update(c);
return crc.getValue();
}
}
// 随机访问文件
public static long checksumRandomAccessFile(Path fi1ename) throws IOException
{
try (RandomAccessFile file = new RandomAccessFile(Fi1es.toFile(), "r"))
{
long length = file.length();
CRC32 crc = new CRC32();
for (1ong p = 0; p < length; p++)
{
file.seek(p);
int c = file.readByte();
crc.update(c);
}
return crc.getValue();
}
}
// 内存映射文件
public static long checksumMappedFile(Path fi1ename) throws IOException
{
try (FileChannel channel = FileChannel.open(fi1ename))
{
CRC32 crc = new CRC32();
int length = (int) channel.size();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, length);
for (1ong p = 0; p < length; p++)
{
int c = buffer.get(p);
crc.update(c);
}
return crc.getValue();
}
}
public static void main(String[] args) throws IOException
{
System.out.println("Input Stream:");
long start= System.currentTimeMillis{);
Path filename = Paths.get(args[O]);
long crcValue = checksumlnputStream{filename); // 普通输入流
long end = System.currentTimeMillis();
System.out.println(Long.toHexString(crcValue));
System.out.println({end - start)+ " milliseconds");
System.out.println("Buffered Input Stream:");
start= System.currentTimeMillis{);
crcValue = checksumBufferedlnputStream(filename); // 带缓冲的输入流
end = System.currentTimeMillis();
System.out.println{Long.toHexString(crcValue));
System.out.println((end - start) + " milliseconds");
System.out.println("Random Access File:");
start = System.currentTimeMillis();
crcValue = checksumRandomAccessFile(filename); // 随机访问文件
end= System.currentTimeMillis();
System.out.println(Long.toHexString(crcVa1ue));
System.out.println((end - start) + " milliseconds");
System.out.println("Mapped File:");
start = System.currentTimeMillis();
crcValue = checksumMappedFile(filename); // 内存映射文件
end = System.currentTimeMillis();
System.out.println(Long.toHexString(crcValue));
System.out.println((end - start) + " milliseconds");
}
}
相关方法:
java.io.FileInputStream 1.0 | |
---|---|
FileChannel getChannel() 1.4 | 返回用于访问这个输入流的通道。 |
java.io.FileOutputStream 1.0 | |
FileChannel getChanne1() 1.4 | 返回用于访问这个输出流的通道。 |
java.io.RandomAccessFile 1.0 | |
FileChannel getChannel() 1.4 | 返同用于访问这个文件的通道。 |
java.nio.channels.FileChannel 1.4 | |
static FileChannel open(Path path, OpenOption ... options) 7 | 打开指定路径的文件通道,默认情况下,通道打开时用于读入。
参数:
|
MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) | 将文件的一个区域映射到内存中 。
参数:
|
java.nio.Buffer 1.4 | |
boolean hasRemaining() | 如果当前的缓冲区位赏没有到达这个缓冲区的界限位置, 则返回true。 |
int limit() | 返回这个缓冲区的界限位置, 即没有任何值可用的第一个位置。 |
java. nio.ByteBuffer 1.4 | |
byte get() | 从当前位笠获得一个字节, 并将当前位置移动到下一个字节 。 |
byte get(int index) | 从指定索引处获得一个字节。 |
ByteBuffer put(byte b) | 向当前位置推入一个字节 , 并将当前位置移动到下一个字节。 返回对这个缓冲区的 引用。 |
ByteBuffer put(int index, byte b) | 向指定索引处推入一个字节。 返回对这个缓冲区的引用。 |
ByteBuffer get(byte[] destination) | 用缓冲区中的字节来填充字节数组,或者字节数组的某个区域,并将当前位置向前移动读入的字节数个位置。如果缓冲区不够大,那么就不会读入任何字节,并抛出 BufferUnderflowException。返回对这个缓冲区的引用。
参数:
|
ByteBuffer get(byte[] destination, int offset, int length) | |
ByteBuffer put(byte[] source) | 将字节数组中的所有字节或者给定区域的字节都推入缓冲区中,并将当前位牲向前移动写出的字节数个位置。如果缓冲区不够大,那么就不会读入任何字节,并抛出 BufferUnderflowException。返回对这个缓冲区的引用。
参数:
|
ByteBuffer put(byte[] source, int offset, int length) | |
Xxx getXxx() | 获得或放置一个二进制数。 Xxx 是 Int、Long、Short、Char、Float 或 Double 中的一个。 |
Xxx getXxx(int index) | |
ByteBuffer putXxx(Xxx va 1 ue) | |
ByteBuffer putXxx(int index, Xxx va 1 ue) | |
ByteBuffer order(ByteOrder order) | 设置或获得字节顺序,order的值是 ByteOrder 类的常量 BIG_ENDIAN 或 LITTLE_ ENDIAN 中的一个。 |
ByteOrder order() | |
static ByteBuffer allocate(int capacity) | 构建具有给定容盘的缓冲区。 |
static ByteBuffer wrap(byte[] va1ues) | 构建具有指定容证的缓冲区,该缓冲区是对给定数组的包装。 |
CharBuffer asCharBuffer() | 构建字符缓冲区,它是对这个缓冲区的包装。对该字符缓冲区的变更将在这个缓冲区中反映出来,但是该字符缓冲区有自己的位置、界限和标记。 |
java.nio.CharBuffer 1.4 | |
char get() | 从这个缓冲区的当前位置开始, 获取一个char 值 , 或者一个范围内的所有char值, 然后将位置向前移动越过所有读入的字符。 最后两个方法将返回this |
CharBuffer get(char[] destination) | |
CharBuffer get(char[] destination, int offset, int length) | |
CharBuffer put(char c) | 从这个缓冲区的当前位堂开始,放置一个 char 值,或者一个范围内的所有char值,然后将位詈向前移动越过所有被写出的字符。当放置的值是从 CharBuffer 读人时,将读入所有剩余字符。所有方法将返回this。 |
CharBuffer put(char[] source) | |
CharBuffer put(char[J source, int offset, int length) | |
CharBuffer put(String source) | |
CharBuffer put(CharBuffer source) |
缓冲区数据结构
缓冲区是巾具有相同类刑的数值构成的数组。
“Buffer”类是—个抽象类,它有众多的具体子类,包括“ByteBuffer”、“CharBuffer”、“DoubleBuffer”、“IntBuffer”、“LongBuffer”和“ShortBuffer”。
- “StringBuffer”类与这些缓冲区没有关系;
每个缓冲区都具有:
- 一个容量,它永远不能改变;
- 一个读写位置,下一个值将在此进行读写;
- 一个界限,超过它进行读写是没有意义的;
- 一个可选的标记, 用于重复一个读入或写出操作。
- 这些值满足下面的条件:“0 <= 标记 <= 位置 <= 界限 <= 容量”
- 获取缓冲区,可以调用诸如“ByteBuffer.allocate”或“ByteBuffer.wrap”这样的静态方法;
相关方法:
java.nio.Buffer 1.4 | |
---|---|
Buffer clear() | 通过将位置复位到 0, 并将界限设置到容量,使这个缓冲区为写出做好准备。返回 this 。 |
Buffer flip() | 通过将界限设置到位置,并将位置复位到 0, 使这个缓冲区为读入做好准备。返回 this。 |
Buffer rewind() | 通过将读写位置复位到 0, 并保持界限不变,使这个缓冲区为重新读入相同的值做好准备。返回this。 |
Buffer mark () | 将这个缓冲区的标记设置到读写位置,返回this。 |
Buffer reset() | 将这个缓冲区的位置设置到标记,从而允许被标记的部分可以再次被读入或写出,返回this。 |
int remaining() | 返回剩余可读入或可写出的值的数量,即界限与位置之间的差异。 |
int position() | 返回这个缓冲区的位置。 |
void position(int newValue) | |
int capacity() | 返回这个缓冲区的容量。 |
文件加锁机制
- 锁定文件:
Fi1eChannel channel = Fi1eChannel.open(path); // 阻塞直至可获得锁 Filelock lock = channel.lock(); // 立即返回,要么返回锁,要么返回null FileLock lock = channel.trylock();
- 锁定文件的一部分:
Fi1elock 1ock(1ong start, long size, boo1ean shared) Fi1elock trylock(1ong start, 1ong size, boo1ean shared)
- 释放锁,最好在“try”语句中执行:
try (Filelock lock= channel .lock()) { // access the locked file or segment }
- 文件加锁机制是依赖于操作系统的:
- 在某些系统中,文件加锁仅仅是建议性的,如果一个应用未能得到锁,它仍旧可以向被另一个应用并发锁定的文件执行写操作。
- 在某些系统中,不能在锁定一个文件的同时将其映射到内存中。
- 文件锁是由整个Java虚拟机持有的。
- 同一个虚拟机启动的两个程序(例如Applet和应用程序启动器),不可能都获得一个在同一文件上的锁:当调用“1ock”和“trylock”方法时,如果虚拟机已经在同一个文件上持有了另一个重叠的锁,那么这两个方法将抛出“OverlappingFileLockException”。
- 在一些系统中,关闭一个通道会释放由Java虚拟机持有的底层文件上的所有锁。因此,在同一个锁定文件上应避免使用多个通道。
- 在网络文件系统上锁定义件是高度依赖于系统的,因此应该尽记避免。
【关于NIO】
正则表达式
语法
- 字符类 (character class) 是一个括在括号中的可选择的字符集。
- 如:“[Jj]”、“[0-9]”、“[A-Za-z]”或“[^0-9]”
- “-”表示是一个范围;(必须是第一项或最后一项)
- “^”表示补集(除了指定字符之外的所有字符);(可以是除开始位贤之外的任何位置)
- “[”必须是第一项;
- 有许多预定的字符类,例如“\d”(数字)和“\p{Sc}”(Unicode货币符号)。
- “.”符号可以匹配任何字符(有可能不包括行终止符,这取决于标志的设置)。
- “\”用作为转义字符。
- 如,“\.”匹配句号;而“\\”匹配反斜线
- 如果“X”和“Y”是正则表达式:那么“XY”表示“任何X的匹配后面跟随Y的匹配”;“X|Y”表示“任何X或Y的匹配”。
- 可以将量词运用到表达式X中:“X+”(1个或多个)、“X*”(0个或多个)与“X?”(0个或1个)。
- 默认情况下,量词要匹配能够使整个匹配成功的最大可能的重复次数。
- 可以修改这种行为:使用后缀“?”(使用勉强或吝啬匹配,也就是“匹配最小的重复次数”)或使用后缀“+”(使用占有或贪婪匹配,也就是“即使让整个匹配失败,也要匹配最大的重复次数”)。
- 如,字符串“cab”匹配“[a-z]*ab”,但是不匹配“[a-z]*+ab”:
- 在第一种情况中,表达式“[a-Z]*”只匹配字符“c”,使得字符“ab”匹配该模式的剩余部分;
- 但是贪婪版本中,“[a-z]*+”将匹配字符“cab”,模式的剩余部分将无法匹配。
- 使用群组来定义子表达式, 其中群组用括号“()”括起来。
- 例如,“([+-]?)([0-9]+)”。然后可以询间模式匹配器,让其返回每个组的匹配,或者用“\n”来引用某个群组(其中“n”是群组号,从1开始)。
如,描述了十进制、十六进制整数:
[+-]?[0-9]+|0[Xx][0-9A-Fa-f)+
Java使用
- 正则表达式的最简单用法就是测试某个特定的字符串是否与它匹配:
- 用表示正则表达式的字符串构建一个“Pattern”对象;(模式)
- 从这个模式中获得一个“Matcher”;(匹配器)
- 并凋用它的“matches”方法;
Pattern pattern = Pattern.compile(patternString); Matcher matcher = pattern.matcher(input); if (matcher. matches()) . . .
- 这个匹配器的输入可以是任何实现了“CharSequence”接口的类的对象,例如“String”、“StringBuilder”和“CharBuffer”。
- 在编译模式(“Pattern”)时,可以设置一个或多个标志:
Pattern pattern = Pattern .compil e(expression, Pattern.CASE_INSENSITIVE + Pattern.UNICODE_CASE);
- 或者可以在模式中指定它们:
String regex = "(?iU:expression)";
- 各个标志如下:(最后两个标志不能在正则表达式内部指定)
- “Pattern.CASE_INSENSITIVE”或“r”:匹配字符时忽略字母的大小写,默认情况下,这个标志只考虑US ASCII字符。
- “Pattern.UNICODE_CASE”或“u”:当与 CASE_INSENSITIVE 组合使用时, 用 Unicode 字母的大小写来匹配。
- “Pattern.UNICODE_CHARACTER_ CLASS”或“U”:选择 Unicode 字符类代替 POSIX, 其中 蕴含了 UNICODE_CASE。
- “Pattern.MULTILINE”或“m”: “^”和“$”匹配行的开头和结尾,而不是整个输入的开头和结尾。
- “Pattern.UNIX_LINES”或“d”:在多行膜式中匹配“^”和“$”时,只有'\n'被识别成行终止符。
- “Pattern.DOTALL”或“s”:当使用这个标志时,.符号匹配所有字符,包括行终止符。
- “Pattern.COMMENTS”或“x”:空白字符和注释(从#到行末尾)将被忽略。
- “Pattern.LITERAL”:该膜式将被逐字地采纳,必须精确匹配,因字母大小写而造成的差异除外。
- “Pattern.CANON_EQ”:考虑Unicode字符规范的等价性,例如,u后面跟随(分音符号) 匹配U。
- 在集合或流中匹配元素,那么可以将模式转换为谓词:
Stream<String> strings = . . . ; Stream<String> result = strings.filter(pattern.asPredicate());
- 其结果中包含了匹配正则表达式的所有字符串。
- 如果正则表达式包含群组,那么“Matcher”对象可以揭示群组的边界。下面的方法:
int start(int groupindex) int end(int groupindex)
- 将产生指定群组的开始索引和结束之后的索引。
- 可以直接通过调用下面的方法抽取匹配的字符串:
String group(int grouplndex)
- (群组0是整个输入,而用于第一个实际群组的群组索引是1)
- 调用“groupCount”方法可以获得全部群组的数址。对于具名的组,使用下面的方法:
int start(String groupName) int end(String groupName) String group(String groupName)
- 找出输入中一个或多个匹配的子字符串(而非匹配全部输入):可以使用“Matcher”类的“find”方法来查找匹配内容,如果返回“true”,再使用“start”和“end”方法来查找匹配的内容,或使用不带引元的 “group”方法来获取匹配的字符串。
while(matcher.find()) { int start = matcher.start(); int end = matcher.end(); String match = input.group(); . . . }
- “Matcher”类的“replaceAll”方法:将正则表达式出现的所有地方都用替换字符串来替换。
// 将所有的数字序列都替换成#字符 Pattern pattern = Pattern.compile("[0-9)+"); Matcher matcher = pattern.matcher(input); String output = matcher.rep1aceA11("#");
- 替换字符串可以包含对模式中群组的引用:“$n”表示替换成第“n”个群组,“${name}”被替换为具有给定名字的组,因此我们需要用“\$”来表示在替换文本中包含一个“$”字符。
- 如果字符串中包含“$”和“\”,但是又不希望它们被解释成群组的替换符,可以调用:
matcher.replaceAll(Matcher.quoteReplacement(str))
- “replaceFirst”方法将只替换模式的第一次出现。
- “Pattern”类有一个“split”方法,它可以用正则表达式来匹配边界,从而将输入分割成字符串数组。
// 将输入分割成标记,其中分隔符是由可选的空白字符包围的标点符号 Pattern pattern = Pattern.compile("\\s*\\p{Punct}\\s*"); String[] tokens = pattern.split(input);
- 如果有多个标记,那么可以惰性地获取它们:
Stream<String> tokens= commas.splitAsStream(input);
- 如果不关心预编译模式和惰性获取,那么可以使用“String.split”方法:
String[] tokens = input.split("\\s*,\\s*");
相关方法:
java.util.regex.Pattern 1.4 | |
---|---|
static Pattern compile(String expression) | 把正则表达式字符串编译到一个用于快速处理匹配的模式对象中。
参数:
|
static Pattern compile(String expression, int flags) | |
Matcher matcher(CharSequence input) | 返回一个matcher对象 , 你可以用它在输入中定位模式的匹配。 |
String[] split(CharSequence input) | 将输入分割成标记. 其中模式指定了分隔符的形式。 返同标记数组, 分隔符并非标记的一部分 。
参数:
|
String[] split(CharSequence input, int limit) | |
Stream<String> splitAsStream(CharSequence input) 8 | |
java.util.regex.Matcher 1.4 | |
boolean matches() | 如果输入匹配模式, 则返回true |
boolean lookingAt() | 如果输入的开头匹配模式, 则返回true。 |
boolean find() | 尝试杳找下一个匹配, 如果找到了另一个匹配, 则返回true。 参数: start 开始查找的索引位犹 |
boolean find(int start) | |
int start() | 返回当前匹配的开始索引和结尾之后的索引位置。 |
int end() | |
String group() | 返回当前的匹配。 |
int groupCount() | 返回输入模式中的群组数值。 |
int start(int grouplndex) | 返回当前匹配中给定群组的开始和结尾之后的位置。
参数:
|
int end(int grouplndex) | |
String group(int group Index) | 返回匹配给定群组的字符串。
参数:
|
String replaceAll(String replacement) | 返回从匹配器输入获得的通过将所有匹配或第一个匹配用替换字符串替换之后的字符串。
参数:
|
String replaceFirst(String replacement) | |
static String quoteReplacement(String str) 5.0 | 引用str 中的所有\和$。 |
Matcher reset() | 复位匹配器的状态。第二个方法将使匹配器作用于另一个不同的输入。这两个方法都返回this 。 |
Matcher reset (CharSequence input) |