流与文件
1.1 流
抽象类InputStream和OutputStream构成了输入输出类层次结构的基础。
由于面向字节的流不便于处理Unicode形式存储的信息(Unicode每个字符都使用了多个字节表示),所以从抽象类Reader和Writer中继承出来了一个专门用于处理Unicode字符的单独的类层次结构。
读写字节
- InputStream类有一个抽象方法:read() 用于读入一个字节并返回读入的字节,或者在遇到输入源结尾时返回-1.在设计具体的输入流类时,必须覆盖这个方法以提供使用的功能。如,从某种输入源读入字节。InputStream类还有若干个非抽象的方法,它们可以读入一个字节数组,或者跳过大量字节。这些方法都要调用read()方法。因此,各个子类都只需覆盖这个方法。
- OutputStream类定义了抽象方法:write()。
- read和write方法在执行时都将阻塞,直至字节确实被读入或写出。
- available方法可以用于检查当前可读入的字节数量。
- 完成对流的读写时,应该调用close来关闭它。这个调用会释放十分有限的操作系统资源。如果一个应用程序打开了过多的流而没有关闭,那么系统资源将被耗尽。关闭一个输出流的同时还会冲刷用于该输出流的缓冲区。
完整的流家族
- 处理字节:InputStream、OutputStream、DataInputStream、DataOutputStream(以二进制格式读写所有的Java基本类型)、ZipInputStream、ZipOutputStream(以常见的zip压缩格式读写文件)
- 处理字符:使用抽象类Reader和Writer类的子类
- 还有四个附加的接口:Closeable、Flushable、Readable、Appendable。分别拥有close、flush、read、append方法。
组合流过滤器
- FileInputStream和FileOutputStream提供附着在一个磁盘文件的输入流和输出流。只需向其构造器提供文件名或文件的完整路径名。如:
FileInputStream in=new FileInputStream("data.txt");
- Java使用某些流从文件或外部位置获取字节,而用其他流将字节组装到更有用的数据类型中。
- 可以使用嵌套过滤器来添加多重功能。例如,流在默认情况下是不被缓冲区缓存的,也就是说,每个对read的调用都会请求操作系统再分发一个字节。相比之下,请求一个数据块并将其置于缓冲区会显得更加高效。可以将BufferedInputStream和FileInputStream链接在一起。
- 有时,当多个流链接在一起时,需要跟踪各个中介流(intermediate stream)。例如,当读取输入时,经常需要浏览下一字节,以了解是否是想要的值。可以通过PushbackInputStream实现:
PushbackInputStream pbin=new PushbackInputStream(new BufferedInputStream(new FileInputStream("data.dat")));
可以预读下一字节:int b=pbin.read();
并在它并非期望的值时推回流中:if(b!='<') pbin.unread(b);
1.2 文本输入与输出
在保存数据时,可以选择二进制格式或文本格式。在存储文本字符串时,需要考虑字符编码。OutputStreamWriter类将使用选定的字符编码方式,把Unicode字符流转换成字节流。而InputStreamReader类将包含字节(用某种字符编码方式表示的字符)的输入流转为可以可以产生Unicode码元的读入。
如何写出文本输出
- 文本输出可以使用PrintWriter。这个类拥有以文本格式打印字符串和数字的方法。
- 为了输出到打印写出器,需要使用与使用System.out时相同的print、println和printf方法。println方法在行中添加了对目标系统来说恰当的行结束符(Windows系统是“\r\n”,UNIX系统是“\n”,可以通过调用System.getProperty(“line.separator”)获得)
- print方法不抛出异常,可以调用checkError方法查看流是否出了某些错误。
如何读入文本输入
- 以二进制格式写出数据,需要DataOutputStream。以文本格式写出数据,需要使用PrintWriter。
- 但使用Scanner读取文本输入。
字符集
- 可以调用静态的availableCharsets方法确定某个特定实现下可用的字符集:
Map<String,Charset> charsets=Charset.availableCharsets();
- 编码字符串:
Charset cset=Charset.forName("utf-8");
(字符集名字是大小写不敏感的)String str=...
ByteBuffer buffer=cset.encode(str);
byte[] bytes=buffer.array();
- 相反,要解码字节序列,需要有字节缓冲区。使用ByteBuffer数组的静态方法wrap可以将一个字节数组转换成一个字节缓冲区。decode方法的结果是一个CharBuffer,调用它的toString方法可以获得一个字符串。
byte[] bytes=...
ByteBuffer bbuf=ByteBuffer.wrap(bytes,offset,length);
CharBuffer cbuf=cset.decode(bbuf);
String str=cbuf.toString();
1.3 读写二进制数据
- DataOutput接口定义了以二进制格式写数组、字符、boolean值和字符串的方法:writeChars、writeBytes、writeInt、writeShort… 例如,writeInt总是把一个整数写出为4个字节的二进制数量值,而不管它有多少位。这样产生的结果并非人可以阅读的,但是对于给定类型的每个值,所需空间都是相同的,而且将其读回也比解析文本要更快。
- writeUTF方法使用修订版的8为位Unicode转换格式写出字符串。由于没有其他方法会使用UTF-8的修订版,所以应该在只有写出用于Java虚拟机的字符串时才使用writeUTF方法。
- 为了读回数据,可以使用DataInput接口中定义的下列方法:readInt、 readShort、readChar…
随机访问文件
- RandomAccessFile类可以在文件中的任何位置查找或写入数据。磁盘文件都是随机访问的,但从网络而来的数据流却不是。可以打开一个随机访问文件,只用于读入或同时用于读写,使用”r”或”rw”作为构造器的第二个参数来指定:
RandomAccessFile inOut=new RandomAccessFile("data.dat","rw");
- 随机访问文件有一个表示下一个将被读入或写出的字节所处位置的文件指针,seek方法可以将这个指针设置到文件中的任意字节位置,seek的参数是一个long类型的整数,它位于0到文件按照字节来度量的长度之间。
- getFilePointer方法将返回文件指针的当前位置。
- 假如希望将文件指针置于第三条雇员记录处,则只需将文件指针置于合适的字节位置,然后就可以开始读入了:
long n=3;
in.seek((n-1)*RECORD_SIZE);
Employee e=new Employee();
e.readData(in);
1.4 ZIP文档
- ZIP文档通常以压缩格式存储了一个或多个文件,每个ZIP文档都有一个头,包含诸如每个文件名字和所使用的压缩方法等信息。在Java中,可以使用ZipInputStream读入ZIP文档。你可能需要浏览文档中每个单独的项,getNextEntry方法就可以返回一个描述这些项的ZipEntry对象。ZipInputStream的read方法被修改为在碰到当前项的结尾时返回-1(而不是ZIP文件末尾),然后必须调用closeEntry读入下一项。
/* 通读ZIP文件 */
ZipInputStream zin=new ZipInputStream(new FileInputStream("ziptest.zip"));
ZipEntry entry;
while((entry=zin.getNextEntry())!=null){
do something
zin.closeEntry();
}
zin.close();
- 当希望读入某个ZIP项的内容时,可能并不想使用原生的read方法。通常,使用某个更能胜任的流过滤器的方法。如,为了读入ZIP文件内部的一个文本文件,可以使用下面的循环:
Scanner in=new Scanner(zin);
while(in.hasNextLine())
do something with in.nextLine()
- 要写出到ZIP文件可以使用ZipOutputStream。而对于希望放入到ZIP文件中的每一项,都应该创建一个ZipEntry对象,并将文件名传递给ZipEntry的构造器,它将设置其他诸如文件日期和解压缩方法等参数。如果需要,可以覆盖这些设置。然后调用ZipOutputStream的putNextEntry来写出新文件,并将文件数据发送到ZIP流中。当完成时,调用closeEntry。
/* 写出到ZIP文件 */
FileOutputStream 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();
}
zout.close();
1.5 对象流与序列化
- 当需要存储相同类型的数据时,使用固定长度的记录格式是一个不错的选择。但面向对象程序中创建的对象很少全部都具有相同的类型。如,staff数组名义上是一个Employee数组,但是包含了Manager这样的子类实例。Java语言支持一种称为对象序列化(object seriallization)的非常通用的机制,可以将任何对象写出到流中,并在之后将其读回。
- 为了保存对象数据,首先需要打开一个ObjectOutputStream对象:
ObjectOutputStream out= new ObjectOutputStream(new FileOutputStream("employee.dat"));
可以直接使用ObjectOutputStream的writeObject方法:Employee harry=new Employee("Harry Hacker",50000);
out.writeObject(harry);
(对于对象才需要使用writeObject方法,对于基本类型可以直接使用writeInt等方法) - 为了将这些对象读回,需要获得一个ObjectOutputStream对象:
ObjectInputStream in=new ObjectInputStream(new FileInputStream("employee.dat"));
然后用readObject方法以这些对象被写出的顺序获得它们。 - 对希望在对象流中存储或恢复的所有类都应进行一下修改,实现Serializable接口。Serializable接口没有任何方法,因此不需要对这些类做任何改动。
对象序列化的算法:
- 对每个对象引用都关联一个序列号
- 对于每个对象,第一次遇到时,保存其对象数据到流中
- 如果某个对象之前已经被保存过,那么只写出“与之前保存过的序列号为x的对象相同”。在读回对象时,整个过程是反过来的。
- 对于流中的对象,在第一次遇到其序列号时,构建它,并使用流中数据来初始化它,然后记录这个顺序号和新对象之间的关联。
- 当遇到“与之前保存过的序列号为x的对象相同”标记时,获取与这个顺序号相关联的对象引用。
理解对象序列化的文件格式
- 对象序列化是以特殊的文件格式存储对象数据的。研究这种数据格式对于洞察对象流化的处理过程非常有益。
- 每个文件都是以”AC ED”这两个字节的魔幻数字开始的。后面紧跟着对象序列化格式的版本号,目前是”00 05”(本部分统一使用十六进制来表示字节)。然后是它包含的对象序列,顺序即它们被存储的顺序。
- 字符串对象被保存为:
74 两字节表示的字符串长度 所有字符
如,字符串”harry”被存为:74 00 05 harry
- 当存储一个对象时,这个对象所属的类也必须存储。这个类的描述包括:类名、序列化的版本唯一的ID(它是数据域类型和方法签名的指纹)、描述序列化方法的标志集、对数据域的描述。指纹是通过对类、 超类、接口、域类型的和方法前面按规范方式排序,然后将安全散列算法(SHA)应用于这些数据而获得的。SHA是一种可以为较大信息块提供指纹的快速算法,不论最初的数据块尺寸有多大,这种指纹总是20个字节的数据包。 序列化机制只使用了SHA码的前8个字节作为类的指纹。即使这样,当类的数据域或方法发生变化时,其指纹跟着变化的可能性还是非常大。类标识符的存储:
72 2字节的类名长度 类名 8字节长的指纹 1字节长的标志 2字节长的数据域描述符的数量 数据域描述符 78(结束标记) 超类类型(如果没有就是70)
- 每个数据域描述符格式:
1字节长的类型编码 2字节长的域名长度 域名 类名(如果域是对象)
类型编码中,B为byte C为char J为long L为对象 [为数组 等等。类名和域名字符串不是以字符串编码74开头的,但域类型是。域类型使用的是与域名稍有不同的编码机制,即本地方法使用的格式。例如,Employee类的薪水域被编码为:D 00 06 salary
- 数组总是被存储成下面的格式:
75 类描述符 4字节长的数组项的数量 数组项
。注意,Employee对象数组的指纹与Employee类自身的指纹并不一相同。 - 所有对象(包括数组和字符串)和所有的类描述符在存储到输入文件时都被赋予了一个序列号,这个数字以
00 7E 00 00
开头。任何给定的类其完整的类描述符只保存一次,后续的描述符将引用它。如对Date的重复引用被编码为:71 00 7E 00 00
相同的机制还用于对象。如果要写出一个对之前存储过的对象的引用,那么这个引用也将用完全相同的方式存储,即71后面跟随序列号。 - 空引用被存储为:
70
- 应该记住:对象流输出中包含所有对象的类型和数据域; 每个对象都被赋予一个序列号; 相同对象的重复出现将被存储为对这个对象的序列号的引用
修改默认的序列化机制
- 某些数据域是不可序列化的,如,只对本地方法有意义的存储文件句柄或窗口句柄的整数值,这种信息在稍后重新加载对象或将其传送到其他机器上都是没有用处的。事实上,如果这种域的值不恰当,还会引起本地方法崩溃。Java使用标记transient(标在域类型前)防止这种域被序列化。如果这些域属于不可序列化的类,也需要标记成transient。
- 序列化机制为单个的类提供了一种方式,向默认的读写行为添加验证或任何其他想要的行为。可序列化的类可以定义以下方法:
private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException;
private void writeObject(ObjectOutputStream out) throws IOException;
之后,数据域就再也不会被自动序列化,取而代之的是调用这些方法。这两个方法只需要保存和加载他们的数据域,而不需要关心超类数据和其他任何数据。 - 通过实现Externalizable接口,类可以定义它自己的机制。实现接口需要定义readExternal和writeExternal两个方法。与readObject和writeObject方法不同,这些方法对包含超类数据在内的整个对象的存储和恢复负责,而序列化机制在流中仅仅是记录该对象所属的类。在读入可外部化的类时,对象流用无参构造器创建一个对象,然后调用readExternal方法。
- readObject、writeObject方法都是私有的,并且只能被序列化机制调用。但readExternal、writeExternal方法都是公共的。特别是,readExternal还潜在地允许修改现有对象的状态。
序列化单例和类型安全的枚举
- 在序列化和反序列化时,如果目标对象是唯一的,那么必须多加小心,这通常会在实现单例和类型安全的枚举时发生。
- 如果使用enum结构,就不必担心序列化,它能够正常工作。但是在维护遗留代码时,如果使用用私有构造器构造枚举对象,这时默认的序列化机制是不适用的。因为即使构造器是私有的,序列化机制也可以创建新的对象。而这个对象与任何预定义的常量都不等同。
- 需要另外定义一个称为readResolve的特殊序列化方法。如果定义了readResolve方法,在对象被序列化之后就会调用它。它必须返回一个对象,而该对象之后会成为readObejct的返回值。如在上述枚举对象中,readResolve方法将检查value域并返回恰当的枚举常量:
protected Object readResolve() throws ObjectStreamException{
if(value==1) return ...
if(value==2) return ..
return null;//一般情况下不应该发生
}
- 记住要向遗留代码中所有类型安全的枚举以及向支持单例模式的类中添加readResolve方法。
版本管理
- 如果使用序列化来保存对象,就需要考虑程序在演化时会有什么问题。例如,旧版本是否可以读入新版本产生的文件。如果对象文件可以处理类的演化问题,那么它正是我们想要的。
- 类可以表明它对其早期的版本保持兼容。首先需要获得这个类的早期版本的指纹。可以使用JDK中的单机程序serialver来获得这个数字。运行:
serialver Employee
会得到Employee类的serialVersionUID号。然后把这个类的所有较新版本把serialVersionUID常量定义为与最初版本的指纹相同(在类中添加一行:public static final long serialVersionUID=.....;
) 如果一个类有名为serialVersionUID的静态数据成员,它就不需要再人工地计算其指纹,而直接使用这个值。一旦这个静态数据成员被置于某个类的内部,那么序列化系统就可以读入这个类的对象的不同版本。 - 如果这个类只有方法产生了变化,那么在读入新对象数据时不会有任何问题。但如果数据域产生了变化,可能会有问题。这时,对象流将尽力将流对象转换为这个类当前的版本。如果这两部分数据域之间名字匹配而类型不匹配,那么对象流不会尝试将一种类型转换为另一种类型,因为这两个对象不兼容;如果流中的对象具有在当前版本中所没有的数据域,那么对象流会忽略这些额外的数据;如果当前版本具有在流化对象中所没有的数据域,那么这些新添加的域将被设为它们的默认值。
- 这样的处理是否安全取决于类设计者是否能够在readObject方法中实现额外的代码订正版本不兼容问题,或者是否能够确保所有的方法在处理null数据时都足够健壮。
为克隆使用序列化
- 序列化提供了一种克隆对象的简便途径,只要对应的类是可序列化的即可。做法很简单:直接把对象序列化到流中,然后将其读回。这样产生的新对象是对现有对象的深拷贝(deep copy)。在此过程中,不必将对象写出到文件,可以用ByteArrayOutputStream将数据保存在字节数组中。
- 需要注意的是,虽然这个方法灵巧,但通常会比显式地构建新对象并复制或克隆数据域的克隆方法慢得多。
1.6 操作文件
Path
- Path表示的是一个目录名序列,后面还可以跟着一个文件名。路径中的第一个部件可以是根部件,例如/或C:\。静态的Path.get方法接受一个或多个字符串,并将它们用默认文件系统的路径分隔符连接起来。如果其表示的不是给定文件系统中的合法路径,会抛出InvalidPathException。连接起来的结果是一个Path对象。
- 调用p.resolve(q)将按照规则返回一个路径:如果q是绝对路径,则返回q;否则,将返回“p后面跟着q”。这个方法可以用于生成完整路径。resolve方法有一种快捷方式,可以接受一个字符串。
Path workPath=basePath.resolve("work");
还有一个很方便的方法resolveSibling,它通过解析指定路径的父路径产生其兄弟路径。例如,如果workPath是/opt/myapp/work
,那么下面的调用:Path tempPath=workPath.resolveSibling("temp");
将创建/opt/myapp/temp
。 - resolve的对立面是relativize。即调用p.relativize(r)将产生路径q,对q进行解析会产生r。
- toAbsolutePath方法将产生给定路径的绝对路径。
- getParent方法将返回父路径。
- getFileName方法返回文件名。
读写文件
- Files类可以使得普通文件操作变得快捷。使用下面的方法可以很容易地读取文件的所有内容:
byte[] bytes=Files.readAllBytes(path);
如果想将文件当做字符串读入,可以在调用readAllBytes之后执行下面的代码: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.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);
- 删除文件:
Files.delete(path);
如果要删除的文件不存在就会抛出异常,可以先调用boolean deleted=Files.deleteIfExists(path);
该删除方法也可以用来移除空目录。
创建文件和目录
- 创建新目录:
Files.createDirectory(path);
其中,路径除了最后一个部件外,其他部分都必须是已经存在的。要创建路径的中间目录,应该使用Files.createDirectories(path);
- 创建文件:
Files.createFile(path);
如果文件已经存在,则会抛出异常。使用前应该先检查文件是否存在。
获取文件信息
- 可以获取exists、isHidden、isReadable、isWritable、isExecutable、isRegularFile、isDirectory等文件信息。
- size方法将返回文件字节数:
long fileSize=Files.size(path);
- 所有文件系统都会报告一个基本属性集,它们被封装在BasicFileAttributes接口中。基本文件属性包括:创建文件、最后一次访问和最后一次修改时间、文件类型、文件尺寸、文件主键。 要获取这些属性,可以调用
BasicFileAttributes attributes=files.readAttributes(path,BasicFileAttribtutes.class);
迭代目录中的文件
- 旧的File类有一个方法,可以用来获取一个目录中的所有文件构成的数组,但是当目录中包含大量文件时,这个方法的性能非常低。Files类设计了一个方法,可以产生一个Iterable对象。
try(DirectoryStream<Path> entries=Files.newDirectoryStream(dir)){
for(Path entry: entries)
process entries
}
- try语句块用来确保目录流可以被正常关闭。访问目录中的项并没有具体的顺序。
- 可以使用glob模式过滤文件:
DirectoryStream<Path> entries=Files.newDirectoryStream(dir,"*.java");
注,如果使用windows的glob语法,必须对反斜杠转义两次:一次为glob转义,一次为Java字符串转义。 要想访问某个目录的所有子孙成员,调用walkFileTree方法,并向其传递一个FileVisitor类型的对象,这个对象会得到下列通知:
在遇到一个文件或目录时:FileVisitResult visitFile(..)
在一个目录被处理前:FileVisitResult preVisitDirectory(…)
在一个目录被处理后:FileVisitResult postVisitDirectory(…)
在试图访问文件或目录时发生错误:FileVisitResult visitFailed(…)对于上述每种情况,都可以指定是否希望执行下面的操作:
继续访问下一个文件:FileVisitResult.CONTINUE
继续访问,但不再访问这个目录下的任何项:FileVisitResult.SKIP_SUBTREE
继续访问,但不再访问同一目录下的文件:FileVisitResult.SKIP_SIBLINGS
终止访问:FileVisitResult.TERMINATE
ZIP文件系统
- 建立文件系统,包括ZIP文档中所有文件:
FileSystem fs=FileSystems.newFileSystem(Paths.get("D:\\workspace\\ziptest.zip"), null);
- 复制ZIP文档中的文件:
Files.copy(fs.getPath(sourceName), targetPath);
- 可以用walkFileTree遍历文件系统
1.7 内存映射文件
大多数操作系统都可以利用虚拟内存实现将一个文件或文件一部分映射到内存中。然后这个文件就可以被当作内存数组一样使用,这比传统的文件操作和带缓冲区的操作都快。
使用java.nio包实现内存映射:
- 首先从文件中获得一个通道(channel)。通道是用于磁盘文件的一种抽象,使我们可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。
FileChannel channel=FileChannel.open(path,options);
然后调用FileChannel类的map方法从这个通道中获得一个ByteBuffer。可以指定想要映射的文件区域和与映射模式:
FileChannel.MapMode.READ_ONLY:所产生的缓冲区是只读的
FileChannel.MapMode.READ_WRITE:所产生的缓冲区是可写的,任何修改都会在某个时刻写入文件中
FileChannel.MapMode.PRIVATE:所产生的缓冲区是可写的,但是任何修改对这个缓冲区来说都是私有的,不会传播到文件中顺序访问:
while(buffer.hasRemaining){ byte b=buffer.get();}
- 随机数据访问:
for(int i=0;i>buffer.limit();i++){byte b=buffer.get(i);}
- 要向缓冲区中写入:putInt、putLong、putShort…
缓冲区数据结构
- 缓冲区是由具有相同类型的数值构成的数组。Buffer类是一个抽象类,它有众多的子类,包括ByteBuffer、CharBuffer、DoubleBuffer等。(StringBuffer类和这些类没有关系)最常用是ByteBuffer和CharBuffer。
- 每个缓冲区都有一个容量,永远不能改变。
- 一个读写位置,下一个值将在此进行读写。
- 一个界限,超过它进行读写是没有意义的。
- 一个可选的标记,用于重复一个读入或写出操作。
- 这些值满足条件:0<=标记<=位置<=界限<=容量
- 使用缓冲区:一开始位置为0,界限等于容量。不断调用put将值添加到缓冲区。当耗尽素所有数据或写出的数据量达到容量大小,就切换到读入操作。这时调用flip方法将界限设置到当前位置并把位置复位到0.在remaining方法返回正数(返回的值时界限-位置),不断调用get。将缓冲区中所有值都读入后,调用clear为缓冲区下一次读写做好准备。clear将位置复位到0,并将界限复位到容量。
- 重读缓冲区:使用rewind或mark/reset方法
- 获取缓冲区:ByteBuffer.allocate或wrap这样的静态方法。
- 可以用来自某个通道的数据填充缓冲区,或者把缓冲区的数据写入通道。这是一种非常有用的方法,可以替代随机访问文件。
ByteBuffer buffer=ByteBuffer.allocate(RECORD_SIZE);
channel.read(buffer);
channel.position(newpos);
buffer.flip();
channel.write(buffer);
文件加锁机制
- 文件锁可以解决多个程序同时修改同一个文件的问题,它可以控制对文件或文件中某个范围的字节的访问。
- 锁定一个文件,可以调用FileChannel的lock或tryLock方法:
FileChannel channel=FileChannel.open(path);
FileLock lock=channel.lock;
或FileLock lcok=channel.tryLock();
第一个调用会阻塞直至可获得锁,第二个调用会立即返回,要么返回锁,要么在锁不可获得的情况下返回null。这个文件将保持锁定状态,直到通道关闭,或在锁上调用release方法。 - 锁定文件的一部分:
FileLock lock(long start,long size,boolean shared)
或FileLock lock(long start,long size,boolean shared)
如果shared为false,则锁定文件的目的是读写。如果为true,则这是一个共享锁,它允许多个进程读入文件,并阻止任何进程获得独占的锁。并非所有操作系统都支持共享锁,因此可能在请求获得共享锁时得到的是独占的锁。调用FileLock类的isShared方法可以查询所持有的锁的类型。 - 释放锁:最好在一个try语句中执行释放锁的操作
try(FileLock lock=channel.lock()){..}
- 注意,文件加锁机制是依赖于操作系统的。
- 在某些系统中,文件加锁仅仅是建议性的。如果一个应用未能得到锁,它仍然可以向被另一个应用并发锁定的文件执行写操作。
- 在某些系统中,不能在锁定一个文件的同时将其映射到内存中。
- 文件锁是整个Java虚拟机持有的。如果有两个程序是由同一个虚拟机启动的,不可能都获得在同一个文件上的锁,会抛出异常。
- 在一些系统中,关闭一个通道会释放由Java虚拟机持有的底层文件上的所有锁。因此,在同一个锁定文件上应避免使用多个通道。
- 在网络文件系统上锁定文件是高度依赖于系统的,应该避免。
1.8 正则表达式
- 用正则表达式的字符串构建一个pattern对象:
Pattern pattern=Pattern.compile(patternString);
- 获得一个matcher:
Matcher matcher=pattern.matcher(input);
输入可以是任何实现了CharSequence接口的类的对象,可以是String,StringBuilder和CharBuffer。 编译模式时可以设置一个或多个标志,如
Pattern pattern=Pattern.compile(patternString,Pattern.CASE_INSENSITIVE+Pattern.UNICODE_CASE);
总共有6个标志:CASE_INSENSITIVE:匹配时忽略大小写。默认情况下只考虑ascⅡ
UNICODE_CASE:与CASE_INSENSITIVE组合时,用Unicode字母的大小写匹配。
MULTILINE:^和 匹配行的开头和结尾而不是整个输入的开头和结尾。UNIXLINES:在多行模式中匹配和 时,只有’\n’被识别为行终止符。
DOTALL:.号匹配所有字符,包括行终止符。
CANON_EQ:考虑Unicode字符规范的等价性。如,u后面跟随··匹配ü。如果正则表达式包含群组,Matcher对象可以揭示群组的边界。
matcher.group(1)
返回第一个群组- 通常,不希望用正则表达式匹配所有输入,而只是想找出输入中一个或多个匹配的子字符串。这时可以用Matcher类的find方法查找匹配内容。如果返回true,再用start和end查找匹配内容。
while(matcher.find()){ String match=input.substring(matcher.start,matcher.end);}
- replaceAll方法将匹配的部分替换成指定内容。替换字符串可以包含对群组的引用:
$n
表示替换成第n个群组。因此替换文本中包含$
要用\$
表示。 - Pattern类有一个split方法,可以用正则表达式匹配边界,从而将输入分割成字符串数组。
String[] tokens=pattern.split(input);