本文内容非原创,你可以点击此处查看内容来源声明
输入/输出流
在Java API中,可以从其中读出一个字节序列的对象叫做输入流,可以从其中写入一个字节序列的对象叫做输出流。这些字节序列的来源地和目的地可以是文件,也可以是网络连接,甚至是内存块
抽象类InputStream和OutputStream构成了输入输出(I/O)类层次结构的基础。
读写字节
InpuStream类有一个抽象方法:
abstract int read()
这个方法将读入一个字节,并返回读入的字节,或者在遇到输入源结尾时返回-1。在设计具体的输入流类时,必须覆盖这个方法已提供适当的功能。
InputStream类还有若干个非抽象防范,可以读入一个字节数组,或者跳过大量的字节。这些方法都要调用抽象的read方法,因此,各个子类都只需覆盖这一个方法。
与此类似,OutputStream类定义了下面的抽象方法:
abstract void write(int b)
它可以向某个输出位置写出一个字节。
read和write方法在执行时都将阻塞,直至 字节确实被读入或写出。这就意味着如果流不能被立即访问,那么当前的线程将被阻塞。这使得这两个防范等待制定的流变为可用 的这段时间里,其他的线程就有机会去执行有用的工作。
available方法使我们可以去见检查当前可读入的字节数量,这意味着下面的代码片段不会被阻塞:
int bytesAvailable = in.available();
if(bytesAvailable > 0){
byte[] data = new byte[bytesAvailable];
in.read(data);
}
当你完成对输入输出流的读写时,英国通过调用close方法来关闭它。
即使某个输入输出流类提供了原生的read和write功能的某些具体方法,应用系统的程序员还是很少使用他们,因为大家感兴趣的数据可能包含数字、字符串和对象,而不是原生字节。
我们可以使用众多的从基本的InputStream和OutputStream类导出的某个输入/输出类,而不只是直接使用字节。
完整的流家族
组合输入/输出流过滤器
Tips:
所有在java.io中的类都将被相对路径名解释为以用户工作目录开始,可以调用System.getProperty("user.dir");
Warning
由于反斜杠字符在java字符串中是转义字符,因此要确保在Windows风格的路径名中使用\(eg:C:\Windows\win.ini)。在Windows中,可以使用单斜杠字符(C:/Windows/win.ini),因为大部分Windows文件处理的系统调用都会将斜杠解释成文件分隔符。但是并不推荐这么做,因为Windows系统函数的行为会因与时俱进而发生变化。因此对于可移植的程序来说,应该使用程序所运行平台的文件分隔符,可以通过个常量字符串java.io.File.separator获得。
FileInputStream和FileOutputStream可以提供附着在一个磁盘文件上的输入流和输出流,而你只需向构造器提供文件名或文件的完整路径名。
FileInput fin = new FileInputStream("employee.dat");
与抽象类InutStream和Outputstream一样,这些类只支持在字节级别上的读写。也就是说,我们只能从fin对象中读入字节和字节数组。
byte b = (byte) fin.read();
DataInputStream,就只能读入数值类型:
DataInputStream din = ...;
double x = din.readDouble();
正如FileInputStream没有任何读入数值的方法一样,DataInputStream也没有任何从文件中获取数据的方法。
Java使用了一种灵巧的机制来分离这两种职责。某些输入流(例如FileInputStream和由URL类的openStream方法返回的输入流)可以从文件和其他更外部的位置上获取字节,而其他的输入流(例如DataInputStream)可以将字节组装到更有用的数据类型中。Java程序员必须对二者进行组合。例如,为了从文件中读入数字,首先需要创建一个FileInputStream,然后将其传递给DataInputStream的构造器:
FileInputStream fin = new FileInputStream("employee.dat");
DataInputStream din = new DataInputStream(fin);
double x = din.readDouble();
FilterInputStream和FilterOutputStream这些类的子类用于向处理字节的输入/输出流添加额外的功能。
可以通过嵌套过滤器添加多重功能。例如,输入流在默认情况下是不被缓冲区缓存的。每个对read的调用都会请求操作系统再分发一个字节。相比之下,请求一个数据块并将置于缓冲区会显得更加高效。如果使用缓冲机制,以及用于文件的数据输入方法,就需要使用下面这种相当恐怖的构造器序列:
DataInputStream din = new DataInputStream(
new BufferedInputStream(
new FileInputStream("employee.dat")));
注意,把DataInputStream置于构造器的最后,是因为希望使用DataInputStream的方法,并且希望使用它们能够使用带缓冲机制的read方法。
有时当多个输入流链接在一起,需要跟踪各个中介输入流(intermediate inputstream)。例如,当读入输入流时,经常需要预览下一个字节,以了解它是否是想要的值。Java提供了用于此目的的PushbackInputStream:
PushbackInputStream pbin = new PushbackInputStream(
new BuffererInputStream(
new FileInputStream("employee.dat")));
现在可以预读下一个字节:
int b = pbin.read();
并且在它并非你所期望的值时将其推回流中。
if(b != '<') pbin.unread(b);
但是读入和推回是可应用于可推回(pushable)输入流的仅有的方法。如果希望能够预先浏览并且可以读入数字,就需要一个既是可推回输入流,又是一个数据输入流的引用。
DataInputStream din = new DataInputStream(
pbin = new PushbackInputStream(
new BufferedInputStream(
new FileInputStream("employee.dat")))
);
当然在其它编程语言的输入输出流类库中,诸如缓冲机制和预览等细节都是自动处理的。
文本输入与输出
在保存数据时,可以选择二进制格式或文本格式。
在存储文本字符串时,需要考虑编码方式,在Java内部使用的是UTF-16,字符串“1234”编码为 00 31 00 32 00 33 0034 十六进制。但是,许多程序都希望文本文件按照其他编码方式编码。在UTF-8这种在互联网上最常用的编码方式中,这个字符串将写出为 4A 6F 73 C3 A9,其中并没有用于前3个字母的任何0字节,而字符e(e头上加一个二声)占用了两个字节。(最后一句话没看懂 ,中文书上就这么翻译的,可能翻译错了吧。。。)
OutputStreamWriter类将使用选定的字符编码方式,把Unicode码元的输出流转换为字节流。
InputStreamReader类将包含字节(用某种字符编码方式表示的字符)的输入流转换为可以产生Unicode码元的读入器。
如何写出文本输出
对于文本输出可以使用PrinterWriter。这个类拥有以文本格式打印字符串和数字的方法,还有一个将Printerwriter链接到FileWriter的便捷方法,下面的语句:
PrintWriter out = new PrinterWriter("employee.txt","UTF-8");
等同于
PrintWriter out = new PrinterWriter(new FileOutStream("employee.txt"),"UTF-8");
为了输出到打印写出的器,需要使用与使用System.out时相同的print,println和printf方法。
如何读入文本输入
最简单的处理任意文本的方式是使用广泛使用的Scanner类,可以从任何输入流中构建Scanner对象。
或者,可以将短小的文本文件像下面这样读入一个字符串中:
Strng content = new String(File.readAllBytes(path),charset);
如果要将文件一行一行地读入,可以调用:
List<String> lines = Files.readAllLiines(path,charset);
如果文件太大,可以将行惰性处理为一个Stream<String>对象:
try (Stream<String> lines = Files.lines(path,charset)){...}
在早期的Java版本中,处理文本的唯一输入方式是通过BufferedReader类。它的readLine方法会产生一行文本,或者在无法获得更多的输入时返回null。典型的输入循环看起来像下面这样:
InputStream inputStream = ...;
try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream,StandardCharset.UTF_8))){
String line;
while((line = in.readLine() != null)){
do Something
}
}
如今,BufferedReader类又有了一个lines方法,可以产生一个Stream<String>对象。但是,与Scanner不同,BufferedReader没有用于任何读入数字的方法。
以文本格式存储对象
字符编码方式
输入和输出流都是用于处理字节序列的,但是在许多情况下,我们希望操作的是文本,即字符序列。于是,字符如何编码成为字节就成了问题。
Java针对字符使用的是Unicode标准。每个字符或“编码点”都具有一个21位的整数。有多种不同的字符编码方式,就是说,将这些21位数字包装成字节的方法有多种。
最常见的编码方式是UTF-8,它会将每个Unicode编码点编码为1到4个字节 的序列(如下表)。UTF-8的好处是传统的包含了英语中用到的所有字符 的ASCII字符集中的每个字符都会占用一个字节。
UTF-8编码方式
字符范围 | 编码方式 |
---|---|
0...7F | 0a6a5a4a3a2a1a0 |
80...7FF | 110a10a9a8a7a6 a5a4a3a2a1a0 |
800...FFFF | 1110a15a14a13a12a11a10a9a8a7a6 a5a4a3a2a1a0 |
10000...10FFFF | 11110a20a19a18a17a16a15a14a13a12a11a10a9a8a7a6 a5a4a3a2a1a0 |
另一种常用的编码方式是UTF-16,他会将每个Unicode编码点编码为1个或2个16位值。
(待补充。。。)
StandardCharsets类具有类型为Charset的静态变量,用于表示每种Java虚拟机都必须支持的字符编码方式:
StandardCharsets.UTF_8
StandardCharsets.UTF_16
StandardCharsets.UTF_16BE
StandardCharsets.UTF_16LE
StandardCharsets.UTF_8859_1
StandardCharsets.UTF_US_ASCII
为了获得另一种编码方式的Charset,可以使用静态的forName方法:
Charset shiftJIS = Charset.forName("Sift-JIS");
在读入或写出文本时,应该使用Charset对象。可以像下面这样将一个字节数组转换为字符串:
String str = new String(bytes,StandardCharsets.UTF_8);
读写二进制数据
文本格式对于测试和调试而言很方便,但是并不像二进制格式传递数据那样高效。
DadaInput 和 DataOutput接口
DataInput接口定义了下面用于二进制格式写数组、字符、Boolean值和字符串的方法:
writeCharts
writeByte
writeInt
writeShort
writeLong
writeFloat
writeDouble
writeChar
writeBoolean
writeUTF
例如,writeInt总是将一个整数写出为4字节的二进制数量值,而不管它有多少位,writeDouble总是将一个double值写出为8字节的二进制数量值。这样产生的结果并非人可阅读的,但是对于每个给定的每个值,所需的空间都是相同的,而且将其读回也比解析文本要快。
writeUTF方法使用修订版的8位Unicode转换格式写出字符串。这种方式与直接使用标准的UTF-8编码方式不同,其中,Unicode码元序列首先用UTF-16表示,其结果之后使用UTF-8规则进行编码。修订后的编码方式对于编码大于oxFFF的字符的处理有所不同,这是为了向后兼容在Unicode还没有超过16位时构建的虚拟机。
为了读回数据,可以使用DataInput接口中定义 的下列方法:
readInt
readShort
readLong
readFloat
readDouble
readChar
readBoolean
readUTF
DataInputStream类实现了DataInput接口,为了从文件中读入二进制数据,可以将DataInputStream与某个字节源相结合,例如FileInputStream:
DataInputStream in = new DataInputStream(new FileInputStream("employee.txt"));
与此类似,想要写出二进制数据,可以使用实现了DataOutput接口的DataOutputStream类:
DataOutputStream out = new DataOutputStream(new FileOutputStream("employee.txt"));
随机访问文件
RandomAccessFile类可以在文件中的任何位置查找或写入数据。磁盘文件都是随机访问的,但是与网络套接字通信的输入/输出流却不是。可以打开一个随机访问文件,只是用于读入或同时用于读写,可以通过使用字符串"r"(用于读入访问)或"rw"(用于读入/写出访问)作为构造器的第二个参数来指定这个选项。
RandomAccessFile in = new RandomAccesFiel("employee.txt","r");
RandomeAccessFile inOut = new RandomAcce("employee.txt","rw");
当你将已有文件作为RandomAccessFile打开时,这个文件不会被删除。
随机访问已有文件有一个表示下一个将被读入或写出的字节所处位置 的文件指针,seek方法可以用来将这个文件指针设置到文件中的任意字节位置,seek的参数是一个long类型的整数,它的值位于0到文件按照字节来度量的长度之间。
getFilePointer方法将返回文件指针的当前位置。
RandomAccessFile类同时实现了DataInput和DataOutput接口。为了读写随机访问文件,可以使用在前面讨论过的readInt/writeInt/和readChar/writeChar之类的方法。
(待补充。。。)
ZIP文档
ZIP文档通常以压缩格式存储了一个或多个文件,每个ZIP文档都有一个头,包含诸如每个文件名和使用的压缩方法等信息。在Java中,可以使用ZipInputStream来读入ZIP文档。可以需要浏览文档中的每个单独的项,getNextEntry方法可以返回一个描述这些项的ZipEntry类型的对象。向ZipInputStream的getInputStream方法传递该项可以获取用于读取该项的输入流。然后调用closeEntry来读入下一个项。下面是典型的通读ZIP文件的代码序列:
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();
要写出到ZIP文件,可以使用ZipOutputStream,而对于你希望放入到ZIP文件中的每一项,都应该创建一个ZipEntry对象,并将文件名传递给ZipEntry的构造器,它将设置其他诸如文件日期和解压缩方法等参数。如果需要,你可以 覆盖这些设置。然后,你需要调用ZipOutputStream的putNextEntry方法来开始写出新文件,并将文件数据发送到ZIP 输出流中。完成时,需要调用closeEntry。然后,你需要对所有你希望存储的文件都重复这个过程。下面是代码框架:
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();
JAR文件只是带一个特殊项的ZIP文件,这个项称作清单。可以使用JarInputStream和JarOutputStream类来读写清单项。
ZIP输入流是一个能够展示流的抽象化的强大之处的实例。当你读入以压缩格式存储的数据时,不必担心边请求边解压数据的问题,而且ZIP格式的字节源并非是文件,也可以是来自网络连接的ZIP数据。事实上,当Applet的类加载器读入JAR文件时,它就是在读入和解压来自网络的数据。
对象输入/输出流与序列化
当你需要存储相同类型的数据时,使用固定长度的记录格式是一个不错的选择。但是,在面向对象程序中创建的对象很少全部都具有相同的类型。例如,你可能有一个称为staff的数组,它名义是一个Employee记录数组,但是实际上却包含诸如Manager这样的子类实例。
Java语言支持一种称为对象序列化的非常通用的机制,它可以将任何对象写出到输出流中,并在之后将其读回。
保存和加载序列化对象
为了保存对象数据,需要打开一个ObjectOupputStream对象:
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt"));
为了保存对象,可以使用ObjectOutputStream的writeObject方法:
Employee harry = new Employee("Harry Hacker",5000,1998,10,1);
Manager boss = new Manager("Carl Cracker",8000,1987,12,25);
out.writeObject(harry);
out.writeObbject(boss);
为了将这些对象读回,首先需要获得一个ObjectInputStream对象:
ObjectInputStream in = new ObjectInputStream(new FileInputStream("employee.txt"));
然后,用readObject方法以这些对象被写出时的顺序获得他们:
Employee e1 = (Employee) in.readObject();
Employee e2 = (Employee) in.readObject();
但是对希望在对象输出流中存储或从对象输入流中恢复的所有类都应进行一些修改,这些类必须实现Serializable接口:
class Employee implements Serializable {... }
Serializable接口没有任何方法,因此不需要对这些类做任何改动。在这一点上,与Cloneable接口很相似。但是,为了使类可克隆,仍需要覆盖object类中的clone方法,而为了使类序列化,不需要做任何事。
只有需要写出对象时才使用witeObject/readObject,对于基本类型值,使用诸如writeInt/readInt或writeDouble/readDouble (对象流类都实现了DataInput/DataOutput接口)
在幕后,是ObjectOutputStream在浏览对象的所有域,并存储他们的内容。
此处的序列化是将对象集合保存到文件中,并按照它们被存储的样子获取他们。序列化的另一种非常重要的应用是通过网络将对象集合传送到另一台计算机上。正如在文件中保存原生的内存地址无意义一样,这些地址对于不同的处理器之间的通信也是毫无意义的。因为序列化用序列号代替了内存地址,所有它允许对象集合从一台机器传送到另一台机器。
理解对象序列化的文件格式
参见原书
修改默认的序列化机制
某些数据域是不可序列化的,例如只对本地方法有意义的存储文件句柄或窗口句柄的整数值,这种信息在稍后重新加载对象或将其传送到其他机器上时都是没用的。事实上,这种域的值如果不恰当,还会引起本地方法崩溃。Java拥有一种很简单的机制来防止这种域被序列化,那就是将它们标记成transient的。如果这些域属于不可序列化的类,也需要将它们标记成transient的。瞬时的域在对象被序列化时总是被跳过的。
序列化机制为单个的类提供了一种方式,去向默认的读写行为添加验证或任何其他想要的行为。可序列化的类可以定义具有下列签名的方法:
private void readObject(ObjectInputStream in) throws IOException,ClassNoutFoundException;
private void writeObject(ObjectOutputStream out) throws IOException;
之后数据域就再不会被自动序列化,取而代之的是调用这些方法。
除了让序列化机制来保存和恢复对象数据,类还可以定义它自己的机制。为了做到这一点,这个类必须实现Externalizable接口,这需要定义两个方法:
public void readExternal(ObjectInputStream in) throws IOException ,ClassNotFoundException;
Public void writeExternal(ObjectOutputStream out) throws IOException;
序列化单例和实例安全的枚举
在序列化和反序列化时,如果目标对象时唯一的,必须加倍小心。
如果使用Java语言的enum结构,就不必担心序列化。但是假设在维护遗留代码,其中包含下面这样的枚举类型:
public class Orientation {
public static final Orientation HORIZONTAL = new Orientation(1);
public static final Orientation VERTICAL = new Orientation(2);
}
为了解决这个问题,需要定义另一种称为readResolve的特殊序列化方法。如果定义了该方法,在对象被序列化后就会调用它。它必须返回一个对象,而该对象之后会成为一个readObject的返回值。在上面的情况下,readResolve方法将检查value域并返回恰当的枚举常量:
protected Object readResolve() throws ObjectStreamException{
if(value == 1) return Orientation.HORIZONTAL;
if(value == 2) return Orientation.VERTICAL;
throw new ObjectStreamException();
}
为克隆使用序列化
序列化机制是一种很有趣的用法:即提供了一种克隆对象的渐变途径,只要对应的类是可序列化的即可。做法很简单:直接将对象序列化到输出流中,然后将其读回。这样产生的新对象是对现有对象的一个深拷贝。在此过程中,我们不必将对象写出到文件中,因为可以用ByteArrayOutputStream将数据保存到字节数组中。
操作文件
Path接口和Files类封装了在用户机器上处理文件系统所需的所有功能,他们是在JavaSE7中新添加进来的,比自JDK1.0以来一直使用的File类方便很多。
Path
Path表示的是一个目录名序列,其后还可以跟着一个文件名。路径中的第一个部件可以是根部件,例如 / 或 C:\ ,而允许访问的根部件取决于文件系统。
Path absolute = Paths.get("/home","harry");
Path relative = Paths.get("myprog","conf","user.properties");
静态的Paths.get方法接受一个或多个字符串,并将它们用默认的文件系统路径分隔符(/,\)连接起来,然后解析连接起来的结果,如果表示的不是给定文件系统的合法路径,就抛出InvalidPathException异常。这个连接起来的结果就是一个Path对象。
get方法可以获取多个部件构成的单个字符串。
组合或解析路径是常见的操作,调用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 。例如,以”/home/cay"为目标对"/home/fred/myprog"进行相对化操作,会产生“../fred/yprog",其中假设..表示的是文件系统的父目录。
normalize方法将移除所有冗余的.和..部件(或文件系统认为冗余的部件)
toAbsolutePath方法将产生给定路径的绝对路径,该绝对路径从根部件开始。
Path类有许多有用的方法用来将路径断开。
Path p = Paths.get("/home","fred","myproperties");
Path parent = p.getParent();
Path file = p.getFileName();
Path root = p.getRoot();
还可以从Path对象中构建Scanner对象:
Scanner in = new Scanner(Paths.get("/home/fred/input.txt"));
需要与遗留系统交互时,可以使用Path的toFile方法,或者File类的toPath方法
读写文件
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.createDirectory(path);
其中路径中除最后一个部件外,其他部分都必须是已存在的。要创建路径中的中间目录,应该使用:
Files.createDirectories(path);
可以使用下面的语句创建一个空文件:
Files.createFile(path);
如果文件已存在,那么这个调用就会异常。
有些便捷方法可以用来在指定位置或者系统指定位置创建临时文件和临时目录:
Path newPath = Files.createTempFile(dir,prefix,suffix);
Pah newPath = Files.createTempFile(prefix,suffix);
Pah newPath = Files.createTempDirectory(dir,prefix);
Pah newPath = Files.createTempDirectory(prefix);
其中,dir是一个Path对象,prefix和suffix是可以为null的字符串。
创建文件或目录,可以制定属性,例如文件的拥有者和权限。。
复制、移动和删除文件
将文件从一个位置复制到另一个位置可以直接调用
Files.copy(fromPath,toPath);
移动文件可以调用:
Files.move(fromPath,toPath);
如果目标路径已经存在,那么复制或移动将失败。如果想要覆盖已有的目标路径,可以使用REPLACE_EXISTING选项。如果想要复制所有的文件属性,可以使用COPY_ATTRIBUTES选项。可同时选择这两个选项。
可以使用ATOMIC_MOVE 将移动操作定义为原子性的。
还可以将一个输入流复制到Path中,这表示你想要将该输入流存储到硬盘上。类似地,可以将一个Path复制到输出流中。可以使用下面的调用:
Files.copy(inputStream,toPath);
Files.copy(fromPath,outputStream);
删除文件可以调用
Files.delete(path);
如果要删除的文件不存在,这个方法就会抛出异常。因此可转而使用:
boolean deleted = Files.deleteIfExits(path);
获取文件信息
下面的静态方法将返回boolean值,表示检查路径的某个属性的结果:
- exists
- isHidden
- isReadadble,isWritable,isExecuutable
- isRegularFile, isDirectory, isSymbolicLink
size方法将返回文件的字节数:
long filesize = Files.size(path);
getOwner方法将文件的拥有者作为java.nio.file.attribute.UserPrincipal的一个实例返回。
所有的文件兄台那个都会报告一个基本属性集,他们被封装在BasicFileAttributes接口中,这些属性与上述信息有部分重叠。基本文件属性包括:
- 创建文件、最后一次访问、最后一次修改的时间,这些时间都表示成java.nio.file.attribute.FileTime
- 文件是常规文件、目录还是符号链接,或者都不是
- 文件尺寸
- 文件主键,这是某种类的对象,具体所属类与文件系统相关,有可能是文件的唯一标识符,也可能不是
要获取这些属性,可以调用:
BasicFileAttributes attributes = Files .readAttributes(path,BasicFileAttributes.class);
如果了解到用户的文件兄台那个兼容POSIX,你可以获取一个PosixFileAttributes实例:
PosixFileAttributes attributes = Files .readAttributes(path,PosixFilesAttributes.class);
然后从中找到拥有者,以及文件的拥有者、组或访问权限。
访问目录的项
静态的Files.list方法会返回一个可以读取目录中各个项的Stream<Path>对象。目录是被惰性读取的,这使得处理具有大量项的目录可以变得更高效。
因为读取目录设涉及需要关闭的系统资源,所以应使用try块:
try( Stream<Path> entries = Files.list(pathToDirectory){...}
list 方法不会进入子目录。为了处理目录中的所有子目录,需要使用File.walk方法。
使用目录流
Files.walk方法会产生一个可以遍历目录中所有子目录的Stream<Path>对象。有时需要对遍历过程进行更加细粒度的控制。在这种情况下,应该使用File.newDirectoryStream对象,它会产生一个DirectoryStream。注意它不是java.util.stream.Stream的子接口,而是专门用于目录遍历的接口。它是Iterable的子接口,可以在增强的for循环中使用目录流。
try (DirectoryStream<Path> entries = Files.newDirectoryStream(dir)){
for (Path entry: entries){
Process entries
}}
try语句块用来确保目录流可以被正确关闭。访问目录中的 项并没有具体的顺序。可以用glob模式来过滤文件:
try (DirectoryStream<Path> entries = Files.newDirectoryStream(dir,"*.java"))
glob模式待完善。。
ZIP文件系统
Paths类会在默认文件系统中查找路径,即在用户本地磁盘中的文件,最有用之一的是ZIP文件系统。如果zipname 是某个ZIP文件的名字,那么下面的调用
FileSystem fs = FileSystem.newFileSystem(Path.get(zipname),null);
将建立一个文件系统,它包含ZIP文档中的所有文件。如果知道文件名,那么从ZIP文档中复制出这个文件就会变得很容易:
Files.copy(fs.getPath(soureceName),targetPath);
其中,fs.getPath对于任意文件系统来说,都与Paths.get类似
要列出ZIP文档中的所有文件,可以遍历文件树:
FileSystem fs = FileSystem.newFileSystem(Path.get(zipname),null);
Files.walkFileTreee(fs.getPath("/"),new SimpleFileVisitor<Path>(){
public FileVisitResult visitFile(Path file,BasicFileAttributes attrs) throws IOException{
System.out.pintln(file);
return FileVisitResult.CONTINUE;}
});
内存映射文件
大多数操作系统都可以利用虚拟内存实现来将一个文件或文件的一部分“映射”到内存中。然后,这个文件就可以当作是内存数组一样访问,这比传统的文件操作要快得多。
内存映射文件的性能
与随机访问相比,性能提高总是很明显的。另一方面,对于中等尺寸文件的顺序读入则没有必要使用内存映射。
java.nio包使用内存映射变得十分简单。
首先,从文件中获得一个通道(channel),通道是用于磁盘文件的一种抽象,它使我们可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。
FileChannel channel = FileChannel.open(path,optios);
然后,通过调用FileChannel类的map方法从这个通道中获得一个ByteBuffer。可以指定想要映射的文件区域与映射模式,支持的模式有三种:
- FileChannel.MapMode.READ_ONLY: 所产生的缓冲区是只读的,任何对缓冲区写入的尝试都会导致ReadOnlyBufferedException异常。
- FileChannel.MapMode.READ_WRTE:所产生的缓冲区是可写的,任何修改都会在某个时刻写回到文件中。注意:其他映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的确切行为是依赖于操作系统的。
- FileChannel.MapMode.PRIVATE: 所产生的缓冲区是科协的,但是任何修改过这个缓冲区来说是私有的,不会传播到文件中。
一旦有了缓冲区,就可以使用ByteBuffer类和Buffer超类的方法读写数据了。
缓冲区支持顺序和随机访问数据访问,它有一个可以通过get和put操作来移动的位置。例如,可以像下面这样顺序遍历缓冲区的所有字节:
while (buffer.hasRemaining()){
byte b = buffer.get();
}
或者下下面这样进行随机访问:
for ( int i=0;i < buffer.limit(); i++){
byte b = buffer.get(i);
}
可以用下面 的方法读写字节数组:
get(byte[] byte)
get(byte[], int offset, int length)
最后,还有下面的方法:
getInt
getLong
getShort
getChar
getFloat
getDouble
用来读入文件中存储为二进制的基本类型值。
Java对二进制数据使用高位在前 的排序机制,但是,如果需要以地位在前的排序方式处理包含二进制数字的文件,只需调用
buffer.order(ByteOrder.LITTLE_ENDIAN);
要查询缓冲区内当前的字节顺序,可以调用:
ByteOrder b = buffer.order();
要向缓冲区写数字,可以使用下列方法:
putInt
putLong
putShort
putChar
putFloat
putDouble
在恰当的时候,以及当通道关闭时,会将这些修改写回到文件中。
缓冲区数据结构
本节简要介绍Buffer对象上的基本操作。
缓冲区是由具有相同类型的数值构成的数组,Buffer类是一个抽象类,它由众多的具体子类,包括ByteBufer,CharBuffer,DoubleBuffer,IntBuffer,LongBuffer和ShortBuffer。
StringBuffer类与这些缓冲区没有关系。
在实践中,最常用的是ByteBuffer和CharBuffer,如图,每个缓冲区都具有:
- 一个容量,它永远不能改变
- 一个读写位置,下一个值将在此进行读写
- 一个界限,超过它进行读写没有意义
- 一个可选的标记,用于重复一个读入或写出操作
这些值满足下面的条件:
0<=标记<=位置<=界限<=容量
使用缓冲区的主要目的是执行“写,然后读入”循环。假设我们有一个缓冲区,在一开始,他的位置为0,界限等于容量。我们不断地调用put将值添加到这个缓冲区中,当我们耗尽所有的数据或者写出的数据量达到容量大小时,就该切换到读入操作了。
这时调用flip方法将界限设置到当前位置,并把位置复位到0.现在在remaining方法返回正整数时,不断地调用get。在我们缓冲区素养的值都读入之后,调用clear使缓冲区为下一次写循环做好准备。clear方法将位置复位到0,并将界限复位到容量。
如果想重读缓冲区,可以使用rewined或mark/reset方法。
要获取缓冲区,可以调用诸如ByteBuffer.allocate或ByteBuffer.wrap这样的静态方法。
然后,可以用来自某个通道的数据填充缓冲区,或者将缓冲区的内容写出通道中。例如:
ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
channel.read(buffer);
channel.position(newpos);
buffer.flip();
channel.write(buffer);
这是一种非常有用的方法,可以替代随机访问文件。
文件加锁机制
考虑一下多个同时执行的程序需要修改同一个文件的情形,很明显,这些程序需要以某种方式进行通信,不然这个文件很容易被损坏。文件锁可以解决问题,它可以控制对文件或文件中某个范围的字节的访问。
假设你的应用程序将用户的偏好存储在一个配置文件中,当用户调用这个应用的两个实例时,这两个实例就有可能会同时希望写这个配置文件。这种情况下,第一个实例应该锁定这个文件,当第二个实例发现这个文件被锁定时,它必须决策是等待直至这个文件解锁,还是直接跳过这个文件写操作。
要锁定一个问价,可以调用FileChannel类的lock和tryLock方法:
FileChannel = FileChannel.open(path);
FileLock lock = channel.lock();
或
FileLock lock = channel.rtyLock();
第一个调用会阻塞直至可获得锁,而第二个调用将立即返回,要么返回锁,要么在锁不可获得的情况下返回null。这个文件将保持锁定状态,直至这个通道关闭,或者在锁上调用了release方法。
还可以通过下面的调用锁定文件的一部分:
FileLock lock (long start,long size,boolean shared)
或
FileLock tryLock(long start,long size,boolean shared)
如果shared标志为false,则锁定文件的目的是读写,而如果为true,则这是一个共享锁,它允许多个进程从文件中读入,并阻止任何进程获得独占的锁。并非所有的操作系统都支持共享锁,因此你可能会在请求共享锁的时候得到的是独占的锁。
调用FileLock类的isShared方法可以查询所持有的锁的类型。
如果锁定了文件的尾部,而这个文件的长度随后增长了超过锁定的部分,那么增长出来的额外区域是未锁定的,要想锁定所有字节,可以使用Long.MAX_VALUE来表示尺寸。
要确保在操作完成时释放锁,与往常一样,最好在一个try语句中执行释放锁的操8作:
try(FileLock lock = channel.lock()){
access the locked file or segment
}
文件加锁机制时依赖操作系统的
- 在某些系统中,文件加锁仅仅是建议性的,如果是一个应用未能得到锁,它仍旧可以向被另一个应用并发锁定的文件执行写操作。
- 在某些系统中,不能在锁定一个文件的同时将其映射到内存中。
文件是由整个Java虚拟机持有的。如果有两个程序是由同一个虚拟机启动的,那么它们不可能每一个都获得一个在同一个文件上的锁。当调用lock和tryLock方法时,如果虚拟机已经在同一个文件上持有了另一个重叠的锁,那么这两个方法将抛出OverlappingFileLockException。 - 在一些系统中,关闭一个通道会释放由Java虚拟机持有的底层文件上的所有锁。因此,在同一个锁定文件山海关应避免使用多个通道。
- 在网络文件系统上锁定文件时高度依赖与系统的,因此应尽量避免。
正则表达式
这部分单独作为一个章节列出。
网友评论