输入/输出流
读写字节
若一个流(对象)不能被立即访问,read 和 write 方法在执行时将被阻塞,在等待流可用的时间里,时间片分给其他线程。available 方法检查当前可读入的字节数量,可以避免阻塞:
1 | int bytesAvailable = in.available(); |
完成读写后应该调用 close 方法关闭,close 方法释放资源的同时,会送出输出流缓冲区中的字节。人为冲刷缓冲区用 flush 方法。
组合输入输出流过滤器
通过嵌套过滤器添加功能,例:用 DataInputStream 使用缓冲机制从文件中读取。输入流默认不被缓冲区缓存( read 每次请求操作系统分发一个字节),DataInputStream 没有从文件中获取数据的方法:
1 | DataInputStream din = new DataInputStream( |
PushbackInputStream 用于预读下一个字节 int b = pbin.read;
,并退回非期望的值 if(b != '<') pbin.unread(b);
:
1 | DataInputStream din = new DataInputStream( |
读写文件
文本输出
使用 PrintWrite 类建立写出器对象,用 print,println,printf 方法输出到写出器:
1 | PrintfWrite out = new PrintWrite("emp.txt", "UTF-8"); |
println 通过 System.getProperty("line.separator")
获取当前系统的换行符。
自动冲刷:PrintWriter(Writer out, Boolean autoFlush)
,开启后每次调用 println 都会发送缓冲区中的所有字符。默认关闭。
文本输入
从任何输入流构建 Scanner 对象处理文本。
或者:
1 | String content = new String(Files.readAllBytes(path), charset); |
早期版本通过 BufferedReader 类,其 readLine 方法返回一行文本或 null,lines 方法可以产生一个 Stream
读写二进制数据
Java 中所有值按高位在前的模式写出,以保证 Java 文件独立于平台。
DataInput 和 DataOutput 接口
DateOutput 接口定义了以二进制格式写数组,字符,布尔值和字符串的方法:writeChars
writeByte
writeInt
writeShort
writeLong
writeFloat
等。虽然结果非人可读,但对于给定类型的每个值,其所需空间相同,读回也比文本更快。其中 writeUTF 方法先用 UTF-16 表示码元序列,再用 UTF-8 规则编码,所以对于编码大于 0xFFFF 的字符的处理有所不同(为了兼容)。用于虚拟机的字符串才需要 writeUTF 方法(如生成字节码的程序),其他情况应该用 writeChars。
DateInput 接口定义了读回数据的方法。DataInputStream 类通过与字节源相组合,从文件中读入二进制数据:
1 | DataInputStream in = new DataInputStream(new FileInputStream("test.dat")); |
DataOutputStream 与此类似:
1 | DataOutputStream in = new DataOutputStream(new FileOutputStream("test.dat")); |
随机访问文件
随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。存在指向该隐含数组的光标或索引,称为文件指针;
输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针。如果随机访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。隐含数组的当前末尾之后的输出操作导致该数组扩展。
RandomAccessFile
类,磁盘文件都可以随机访问,注意套接字通信的流不是。使用字符串 “r” 或 “rw” 作为第二个参数指定只读或可读写:
1 | RandomAccessFile in = new RandomAccessFile("file.dat", "r"); |
seek
方法用来设置文件指针到文件的任意位置,其参数是一个 long 型整数,getFilePointer
方法返回指针的当前位置。
例,从第三个开始读:
1 | long n = 3; |
在文件中(非尾部)进行修改只允许进行替换:
1 | in.seek((n-1) * RECORD_SIZE); |
length
方法可以确认文件中的字节总数。
ZIP 文档
ZIP 文档头包含文件名和压缩方法等信息,使用 ZipInputStream 读入 ZIP 文档,getNextEntry
方法返回描述文档头项目的 ZipEntry 对象,将该项传递给 ZipInputStream 的 getInputStream 方法以获取读取该项的输入流。调用 closeEntry 读入下一项:
1 | ZipInputStream zin = new ZipInputStream(new FileInputStream(zipname)); |
使用 ZipOutputStream 写出到 ZIP 文件,对要写入的每一项都应该创建一个 ZipEntry 对象,并将文件名传递给 ZipEntry 的构造器,其自动设置如文件日期解压缩方法等参数(也可自定义进行覆盖)。调用 ZipOutputStream 的 putNextEntry 方法将文件数据发送到 ZIP 输出流中,完成时 closeEntry。其后对其他文件重复此过程:
1 | FileOutputStream fout = new FileOutputStream("test.zip"); |
JAR 文件使用 JarInputStream 和 JarOutputStream 类读写,是带有特殊项的 ZIP 文档。
对象输入输出流与序列化
保存和加载序列化对象
对象序列化:将对象写出到输出流并可以再读回。类必须实现 Serializable 接口。
首先创建 ObjectOutputStream
对象:
1 | ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test.dat")); |
ObjectOutputStream
的 writeObject
方法保存对象:
1 | Employee harry = new Employee("harry"); |
若要读回,获取一个 ObjectInputStream 对象,然后用 readObject 方法以被写出时的顺序获得对象:
1 | ObjectInputStream in = new ObjectInputStream(new FileInputStream("test.dat")); |
每个对象是用一个序列号保存的:
- 对每个对象引用都关联一个序列号
- 对每个对象,第一次遇到时保存到输出流
- 若之前已经保存过,则写出类似“与序列号 x 相同”
读入时:
- 对于输入流中的对象,第一次遇到其序列号时构建并初始化
- 遇到“与序列号 x 相同”时获取对象引用
修改默认的序列化机制
对于只对本地方法有意义的数据域,使用 transient 标记以避免序列化。
为默认的读写方法添加其他行为,序列化的类中定义:
1 | private void readObject (ObjectInputStream in) |
自定义序列化机制,必须实现 Externalizable 接口:
1 | public void readExternal (ObjectInputStream in) |
这两个方法对包含超类数据在内的整个对象负责,序列化机制在读入时用无参构造器创建一个对象,然后调用 readExternal 方法。
序列化单例和类型安全的枚举
类型安全的枚举实现 Serializable 接口时不适用默认的序列化机制,即使构造器私有,序列化机制也可以创建新对象:
1 | public class Orientation { |
需要定义 readResolve
特殊序列化方法,其在对象被序列化之后调用。该方法必须返回一个对象,并会成为 readObject 的返回值:
1 | protected Object readResolve() throws ObjectStreamException { |
克隆
将对象序列化到输出流中,然后将其读回,产生的新对象是对现有对象的深拷贝,可以用 ByteArrayOutputStream
将数据保存在数组中而不必写出到文件。
操作文件
Path
Paths.get
方法接受一个或多个字符串,并用默认路径分隔符连接(\ /),然后返回 Path 对象。路径不必实际存在,其仅是抽象名字序列,先创建一个路径再调用方法创建对应文件。
resolve
方法产生子目录:
1 | Path workRelative = Paths.get("work"); |
resolveSibling
方法产生兄弟路径:
1 | Path tempPath = workPath.resolveSibling("temp"); |
相对化路径 relativize
方法,normalize
移除冗余,toAbsolutePath
产生给定路径的绝对地址。及诸如 getParent()
getFilename
等。
读写文件
1 | byte[] bytes = Files.readAllBytes(path); // 读入 |
适合中等长度的文本文件,若文件长度较大或为二进制,应当使用输入输出流或读入写入器:
1 | InputStream in = Files.newInputStream(path); |
创建文件和目录
路径中除最后一个,都已存在:
1 | Files.createDirctory(path); |
创建路径中的中间目录:
1 | Files.createFile(path); |
createTempFile()
和 createTempDirectory()
用于创建临时文件和目录。
复制移动删除文件
1 | Files.copy(fromPath, toPath); // 已存在则失败 |
访问目录中的项
Files.list
方法返回一个可以读取目录中项的 StreamFile.walk
方法,File.walk(pathToRoot, depth) 限制树的深度。涉及系统资源要 try 块。
目录流
比 walk 更细粒的控制,File.newDirectoryStream
是 Iterable 的子接口,可以用于增强 for 循环。
使用 glob 模式过滤文件:
1 | try (DirectoryStream<Path> entries = Files.newDirectoryStream(dir, "*.java")); |
模式 | 描述 | 例 |
---|---|---|
* | 匹配路径中 0 或多个字符 | 当前目录中匹配 |
** | 匹配跨目录的 0 或多个字符 | 子目录中匹配 |
? | 匹配一个字符 | |
[…] | 匹配字符集和 | |
{…} | 匹配 , 隔开的可选项之一 | |
\ | 转译 | * 带 * 的文件 |
若要访问某个目录的所有子孙成员,调用 walkFileTree
方法,并传递一个 FileVisitor 类型的对象,
遇到文件或目录:
FileVisitResult visitFile(T path, BasicFileAtttributes attrs)
在一个目录被处理前:
FileVisitResult perVisitDirectory(T dir, IOException ex)
在一个目录被处理后:
FileVisitResult postVisitDirectory(T dir, IOException ex)
试图访问时发生错误:
FileVisitResult visitFileFailed(path, IOException)
以上情况可用的操作:
- 继续访问下一个:
FileVisitResult.CONTINUE
- 继续访问,但不再访问这个目录下的任何项:
FileVisitResult.SKIP_SUBBTREE
- 继续访问,但不再访问这个文件的兄弟文件:
FileVisitResult.SKIP_SIBLINGS
- 终止访问:
FileVisitResult.TERMINATE
任何方法抛出异常将终止访问,并从 walkFileTree 方法抛出。
SimpleFileVisitor
实现了 FileVisitor 接口,除 visitFileFailed 方法外不做任何处理即直接访问。由 visitFileFailed 方法抛出由失败导致的异常,并终止访问。
例,删除目录树:
1 | Files.walkFileTree(Paths.get("/"), new SimpleFileVisitor<Path>() |
覆盖 postVisitDirectory 方法,防止遇到不允许打开的目录或不允许访问的文件时立刻失败。
ZIP 文件系统
建立一个包含 ZIP 文档所有文件的文件系统:
1 | FileSystem fs = FileSystems.newFileSystem(Paths.get(zipname), null); |
内存映射文件
概述
首先从文件中获取一个通道(磁盘文件的抽象),以获得访问内存映射等特性:
1 | FileChannel channel = FileCHannel.open(path, options); |
调用 FileChannel
类的 map 方法获得一个 ByteBuffer,可以指定模式:
- FileChannel.MapMode.READ_ONLY
- FileChannel.MapMode.READ_WRITE
- FileChannel.MapMode.PRIVATE 可写,但不会写回进文件
然后使用 ByteBuffer 和 Buffer 超类方法读写。缓冲区支持顺序和随机访问,有一个通过 get put 操作移动的位置。
顺序遍历:
1 | while (buffer.hasRemaining()) { |
随机访问:
1 | for (int i = 0; i< buffer.limit(); i++) { |
读写字节数组:
- get(byte[] bytes)
- get(bytes[], int offset, int length)
读入二进制基本类型:
- getInt
- getLong
- getChar
- …
写入数字:
- putInt
- putLong
- …
缓冲区数据结构
缓冲区是由具有相同类型的数值构成的数组,Buffer 是一个抽象类,其具体子类有 ByteBuffer Charbuffer DoubleBuffer 等(没有 StringBuffer !)。
0 <= 标记 <= 位置 <= 界限 <= 容量
缓冲区执行写读循环,假设不断 put 填充缓冲区,当耗尽所有数据或达到容量大小切换读入操作;
调用 flip 方法将界限设置到当前位置,然后位子复位到 0,当 remaining 方法返回正数就不断调用 get,当读入所有数值后,用 clear 复位位置到 0,恢复界限到容量,等待下次写循环。
获取缓冲区有 ByteBuffer.allocate 或 ByteBuffer.warp 等方法,使用某个通道的数据填充。
文件加锁
锁定一个文件使用 FileChannel 类的 lock
或 trylock
方法,前者会阻塞至可获得锁,后者将立刻返回锁或 null。文件将锁定至通道关闭或在锁上调用 release 方法。
锁定文件的一部分:lock(long start, long size, boolean shared)
或 trylock(long start, long size, boolean shared)
,share 标志为 false 则锁定文件的目的是读写,若为 true 允许多个进程从文件读入并阻止进程获得独占的锁。注意这依赖于操作系统。