0%

Java 补丁-输入输出

输入/输出流

读写字节

若一个流(对象)不能被立即访问,read 和 write 方法在执行时将被阻塞,在等待流可用的时间里,时间片分给其他线程。available 方法检查当前可读入的字节数量,可以避免阻塞:

1
2
3
4
5
int bytesAvailable = in.available();
if (bytesAvailable > 0) {
byte[] data = new byte[byteAvailable];
in.read(data);
}

完成读写后应该调用 close 方法关闭,close 方法释放资源的同时,会送出输出流缓冲区中的字节。人为冲刷缓冲区用 flush 方法。

组合输入输出流过滤器

通过嵌套过滤器添加功能,例:用 DataInputStream 使用缓冲机制从文件中读取。输入流默认不被缓冲区缓存( read 每次请求操作系统分发一个字节),DataInputStream 没有从文件中获取数据的方法:

1
2
3
DataInputStream din = new DataInputStream(
new BufferedInputStream(
new FileInputStream("test.dat")));

PushbackInputStream 用于预读下一个字节 int b = pbin.read; ,并退回非期望的值 if(b != '<') pbin.unread(b); :

1
2
3
4
DataInputStream din = new DataInputStream(
pbin = new PushbackInputStream(
new BufferedInputStream(
new FileInputStream("test.dat"))));

读写文件

文本输出

使用 PrintWrite 类建立写出器对象,用 print,println,printf 方法输出到写出器:

1
2
3
PrintfWrite out = new PrintWrite("emp.txt", "UTF-8");
String str = "abc";
out.print(str);

println 通过 System.getProperty("line.separator") 获取当前系统的换行符。
自动冲刷:PrintWriter(Writer out, Boolean autoFlush) ,开启后每次调用 println 都会发送缓冲区中的所有字符。默认关闭。

文本输入

从任何输入流构建 Scanner 对象处理文本。

或者:

1
2
3
4
5
6
7
8
String content = new String(Files.readAllBytes(path), charset);
// 处理为字符串

List<String> lines = Files.readAllLines(path, charset);
// 按行读入

try (Stream<String> lines = Files.lines(path, charset)) { ... }
// 处理为 Stream<String> 对象

早期版本通过 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
2
3
4
long n = 3;
in.seek((n-1) * SIZE); // 第三个处即第二个末尾
Employee e = new Employee();
e.readDate(in);

在文件中(非尾部)进行修改只允许进行替换:

1
2
in.seek((n-1) * RECORD_SIZE);
e.wirteDate(out);

length 方法可以确认文件中的字节总数。

ZIP 文档

ZIP 文档头包含文件名和压缩方法等信息,使用 ZipInputStream 读入 ZIP 文档,getNextEntry 方法返回描述文档头项目的 ZipEntry 对象,将该项传递给 ZipInputStream 的 getInputStream 方法以获取读取该项的输入流。调用 closeEntry 读入下一项:

1
2
3
4
5
6
7
8
ZipInputStream zin = new ZipInputStream(new FileInputStream(zipname));
ZipEntry entry;
while((entry = zin.getNextEntry()) != null) {
InputStream in = zin.getInputStream(entry);
...
zin.closeEntry();
}
zin.close();

使用 ZipOutputStream 写出到 ZIP 文件,对要写入的每一项都应该创建一个 ZipEntry 对象,并将文件名传递给 ZipEntry 的构造器,其自动设置如文件日期解压缩方法等参数(也可自定义进行覆盖)。调用 ZipOutputStream 的 putNextEntry 方法将文件数据发送到 ZIP 输出流中,完成时 closeEntry。其后对其他文件重复此过程:

1
2
3
4
5
6
7
8
9
FileOutputStream fout = new FileOutputStream("test.zip");
ZipOutputStream zout = new ZipOutputStream(fout);
// for all files :
ZipEntry ze = new ZipEntry(filename);
zout.putNextEntry(ze);
...
zout.closeEntry();

zout.close;

JAR 文件使用 JarInputStream 和 JarOutputStream 类读写,是带有特殊项的 ZIP 文档。

对象输入输出流与序列化

保存和加载序列化对象

对象序列化:将对象写出到输出流并可以再读回。类必须实现 Serializable 接口。

首先创建 ObjectOutputStream 对象:

1
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test.dat"));

ObjectOutputStreamwriteObject 方法保存对象:

1
2
Employee harry = new Employee("harry");
out,writeObject(harry)

若要读回,获取一个 ObjectInputStream 对象,然后用 readObject 方法以被写出时的顺序获得对象:

1
2
ObjectInputStream in = new ObjectInputStream(new FileInputStream("test.dat"));
Employee e = (Employee) in.readObject();

每个对象是用一个序列号保存的:

  • 对每个对象引用都关联一个序列号
  • 对每个对象,第一次遇到时保存到输出流
  • 若之前已经保存过,则写出类似“与序列号 x 相同”

读入时:

  • 对于输入流中的对象,第一次遇到其序列号时构建并初始化
  • 遇到“与序列号 x 相同”时获取对象引用

修改默认的序列化机制

对于只对本地方法有意义的数据域,使用 transient 标记以避免序列化。

为默认的读写方法添加其他行为,序列化的类中定义:

1
2
3
4
5
6
7
8
9
private void readObject (ObjectInputStream in) 
throws IOException, ClassNotFoundException {
in.defaultReadObject();
...
}
private void writeObject (ObjectOutputStream out)
throws IOException {
out.defaultWriteObject();
}

自定义序列化机制,必须实现 Externalizable 接口:

1
2
3
4
public void readExternal (ObjectInputStream in)
throws IOException, ClassNotFoundException;
private void writeExternal (ObjectOutputStream out)
throws IOException;

这两个方法对包含超类数据在内的整个对象负责,序列化机制在读入时用无参构造器创建一个对象,然后调用 readExternal 方法。

序列化单例和类型安全的枚举

类型安全的枚举实现 Serializable 接口时不适用默认的序列化机制,即使构造器私有,序列化机制也可以创建新对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Orientation {
public static final Orientation HORTZONTAL = new Orientation(1);
public static final Orientation VERTICAL = new Orientation(2);
private int value;

private Orientation(int v) {
value = v;
}
}

Orientation = Orientation.HORTZONTAL;
// orientation == Orientation.HORTZONTAL -> true

ObjectOutputStream out = ... ;
out.write(original);
out.close();
ObjectInputStream in = ...;
Orientation saved = (Orientation) in.read();
// saved == Orientation.HORTZONTAL -> false

需要定义 readResolve 特殊序列化方法,其在对象被序列化之后调用。该方法必须返回一个对象,并会成为 readObject 的返回值:

1
2
3
4
protected Object readResolve() throws ObjectStreamException {
if (value == 1) return Orientation.HORTZONTAL;
if (value == 2) return Orientation.VERTICAL;
}

克隆

将对象序列化到输出流中,然后将其读回,产生的新对象是对现有对象的深拷贝,可以用 ByteArrayOutputStream 将数据保存在数组中而不必写出到文件。

操作文件

Path

Paths.get 方法接受一个或多个字符串,并用默认路径分隔符连接(\ /),然后返回 Path 对象。路径不必实际存在,其仅是抽象名字序列,先创建一个路径再调用方法创建对应文件。

resolve 方法产生子目录:

1
2
3
4
Path workRelative = Paths.get("work");
Path workPath = basePath.resolve(workRelative);
// 或者
Path workPath = basePath.resolve("work");

resolveSibling 方法产生兄弟路径:

1
Path tempPath = workPath.resolveSibling("temp");

相对化路径 relativize 方法,normalize 移除冗余,toAbsolutePath 产生给定路径的绝对地址。及诸如 getParent() getFilename 等。

读写文件

1
2
3
4
5
6
7
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, context.getBytes(charset), StandardOpenOption.APPEND);
// 追加内容
Files.write(path, lines); // 写出行集合

适合中等长度的文本文件,若文件长度较大或为二进制,应当使用输入输出流或读入写入器:

1
2
InputStream in = Files.newInputStream(path);
Reader in = Files.newBufferedReader(path, charset);

创建文件和目录

路径中除最后一个,都已存在:

1
Files.createDirctory(path);

创建路径中的中间目录:

1
Files.createFile(path);

createTempFile()createTempDirectory() 用于创建临时文件和目录。

复制移动删除文件

1
2
3
4
5
6
7
8
9
10
11
Files.copy(fromPath, toPath); // 已存在则失败
Files.move(fromPath, toPath); // 已存在则失败
Files.copy(fromPath, toPath, StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
// 覆盖目标,复制所有文件属性
Files.move(fromPath, toPath, StandardCopyOption.ATOMIC_MOVE);
// 原子移动,完成或保持原位
Files.copy(inputStream, toPath);
Files.copy(fromPath, outputStream);
Files.delete(path); // 失败抛异常
Files.deleteIfExists(path); // 返回 bool 值,可作为 delete 替代

访问目录中的项

Files.list 方法返回一个可以读取目录中项的 Stream 对象,若要进入子目录,使用 File.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
2
3
4
5
6
7
8
9
10
11
12
Files.walkFileTree(Paths.get("/"), new SimpleFileVisitor<Path>()
{
public FileVisitResult visitFile(Path file, BasicFileAtttributes 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;
}
})

覆盖 postVisitDirectory 方法,防止遇到不允许打开的目录或不允许访问的文件时立刻失败。

ZIP 文件系统

建立一个包含 ZIP 文档所有文件的文件系统:

1
2
FileSystem fs = FileSystems.newFileSystem(Paths.get(zipname), null);
Files.copy(fs.getPath(sourceName), targetPAth);

内存映射文件

概述

首先从文件中获取一个通道(磁盘文件的抽象),以获得访问内存映射等特性:

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
2
3
while (buffer.hasRemaining()) {
byte b = buffer.get();
}

随机访问:

1
2
3
for (int i = 0; i< buffer.limit(); i++) {
byte b = buffer.get(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 类的 locktrylock 方法,前者会阻塞至可获得锁,后者将立刻返回锁或 null。文件将锁定至通道关闭或在锁上调用 release 方法。

锁定文件的一部分:lock(long start, long size, boolean shared)trylock(long start, long size, boolean shared) ,share 标志为 false 则锁定文件的目的是读写,若为 true 允许多个进程从文件读入并阻止进程获得独占的锁。注意这依赖于操作系统。