Java 编程问题:六、Java I/O 路径、文件、缓冲区、扫描和格式化4

简介: Java 编程问题:六、Java I/O 路径、文件、缓冲区、扫描和格式化

将二进制文件读入内存


可以通过Files.readAllBytes()将整个二进制文件读入内存:

byte[] bytes = Files.readAllBytes(binaryFile);


类似的方法也存在于InputStream类中。


虽然这些方法对于相对较小的文件非常方便,但对于较大的文件来说并不是一个好的选择。尝试将大文件提取到内存中很容易出现 OOM 错误,而且显然会消耗大量内存。或者,对于大文件(例如 200GB),我们可以关注内存映射文件(MappedByteBuffer。MappedByteBuffer允许我们创建和修改巨大的文件,并将它们视为一个非常大的数组。他们看起来像是在记忆中,即使他们不是。一切都发生在本机级别:


try (FileChannel fileChannel = (FileChannel.open(binaryFile,
    EnumSet.of(StandardOpenOption.READ)))) {
  MappedByteBuffer mbBuffer = fileChannel.map(
    FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
  System.out.println("\nRead: " + mbBuffer.limit() + " bytes");
}

对于大型文件,建议在缓冲区内遍历固定大小的缓冲区,如下所示:

private static final int MAP_SIZE = 5242880; // 5 MB in bytes
try (FileChannel fileChannel = FileChannel.open(
    binaryFile, StandardOpenOption.READ)) {
  int position = 0;
  long length = fileChannel.size();
  while (position < length) {
    long remaining = length - position;
    int bytestomap = (int) Math.min(MAP_SIZE, remaining);
    MappedByteBuffer mbBuffer = fileChannel.map(
      MapMode.READ_ONLY, position, bytestomap);
    ... // do something with the current buffer
    position += bytestomap;
  }
}



写入二进制文件


写入二进制文件的一种有效方法是使用BufferedOutputStream。例如,将byte[]写入文件可以如下完成:

final byte[] buffer...;
Path classFile = Paths.get(
  "build/classes/modern/challenge/Main.class");
try (BufferedOutputStream bos = newBufferedOutputStream(
    Files.newOutputStream(classFile, StandardOpenOption.CREATE,
      StandardOpenOption.WRITE))) {
  bos.write(buffer);
}

如果您正在逐字节写入数据,请使用write(int b)方法,如果您正在写入数据块,请使用write(byte[] b, int off, int len)方法。


向文件写入byte[]的一种非常方便的方法是Files.write(Path path, byte[] bytes, OpenOption... options)。例如,让我们编写前面缓冲区的内容:


Path classFile = Paths.get(
  "build/classes/modern/challenge/Main.class");
Files.write(classFile, buffer,
  StandardOpenOption.CREATE, StandardOpenOption.WRITE);

通过MappedByteBuffer写入二进制文件可以如下完成(这对于写入大型文本文件非常有用):

Path classFile = Paths.get(
  "build/classes/modern/challenge/Main.class");
try (FileChannel fileChannel = (FileChannel) Files.newByteChannel(
    classFile, EnumSet.of(StandardOpenOption.CREATE,
      StandardOpenOption.READ, StandardOpenOption.WRITE))) {
  MappedByteBuffer mbBuffer = fileChannel
    .map(FileChannel.MapMode.READ_WRITE, 0, buffer.length);
  if (mbBuffer != null) {
    mbBuffer.put(buffer);
  }
}

最后,如果我们正在写一段数据(不是原始的二进制数据),那么我们可以依赖于DataOutputStream。这个类为不同类型的数据提供了writeFoo()方法。例如,让我们将几个浮点值写入一个文件:

Path floatFile = Paths.get("float.bin");
try (DataOutputStream dis = new DataOutputStream(
    new BufferedOutputStream(Files.newOutputStream(floatFile)))) {
  dis.writeFloat(23.56f);
  dis.writeFloat(2.516f);
  dis.writeFloat(56.123f);
}



140 在大文件中搜索

搜索和计算文件中某个字符串的出现次数是一项常见的任务。尽可能快地实现这一点是一项强制性要求,尤其是当文件很大(例如 200GB)时。


注意,以下实现假设字符串11111中只出现一次,而不是两次。此外,前三个实现依赖于第 1 章“字符串、数字和数学”节的“在另一个字符串中对字符串进行计数”的帮助方法:

private static int countStringInString(String string, String tofind) {
  return string.split(Pattern.quote(tofind), -1).length - 1;
}

既然如此,让我们来看看解决这个问题的几种方法。




基于BufferedReader的解决方案


从前面的问题中我们已经知道,BufferedReader对于读取文本文件是非常有效的。因此,我们也可以用它来读取一个大文件。在读取时,对于通过BufferedReader.readLine()获得的每一行,我们需要通过countStringInString()计算所搜索字符串的出现次数:


public static int countOccurrences(Path path, String text, Charset ch)
    throws IOException {
  int count = 0;
  try (BufferedReader br = Files.newBufferedReader(path, ch)) {
    String line;
    while ((line = br.readLine()) != null) {
      count += countStringInString(line, text);
    }
  }
  return count;
}



基于Files.readAllLines()的解决方案


如果内存(RAM)对我们来说不是问题,那么我们可以尝试将整个文件读入内存(通过Files.readAllLines()并从那里处理它。将整个文件放在内存中支持并行处理。因此,如果我们的硬件可以通过并行处理突出显示,那么我们可以尝试依赖parallelStream(),如下所示:

public static int countOccurrences(Path path, String text, Charset ch)
    throws IOException {
  return Files.readAllLines(path, ch).parallelStream()
    .mapToInt((p) -> countStringInString(p, text))
    .sum();
}

如果parallelStream()没有任何好处,那么我们可以简单地切换到stream()。这只是一个基准问题。



基于Files.lines()的解决方案


我们也可以尝试通过Files.lines()利用流。这一次,我们将文件作为一个懒惰的Stream<String>来获取。如果我们可以利用并行处理(基准测试显示出更好的性能),那么通过调用parallel()方法来并行化Stream<String>就非常简单了:

public static int countOccurrences(Path path, String text, Charset ch)
    throws IOException {
  return Files.lines(path, ch).parallel()
    .mapToInt((p) -> countStringInString(p, text))
    .sum();
}



基于扫描器的解决方案

从 JDK9 开始,Scanner类附带了一个方法,该方法返回分隔符分隔的标记流Stream<String> tokens()。如果我们将要搜索的文本作为Scanner的分隔符,并对tokens()返回的Stream的条目进行计数,则得到正确的结果:

public static long countOccurrences(
  Path path, String text, Charset ch) throws IOException {
  long count;
  try (Scanner scanner = new Scanner(path, ch)
      .useDelimiter(Pattern.quote(text))) {
    count = scanner.tokens().count() - 1;
  }
  return count;
}

JDK10 中添加了支持显式字符集的扫描器构造器。



基于MappedByteBuffer的解决方案


我们将在这里讨论的最后一个解决方案是基于 JavaNIO.2、MappedByteBuffer和FileChannel的。此解决方案从给定文件上的一个FileChannel打开一个内存映射字节缓冲区(MappedByteBuffer)。我们遍历提取的字节缓冲区并查找与搜索字符串的匹配(该字符串被转换为一个byte[]并逐字节进行搜索)。


对于小文件,将整个文件加载到内存中会更快。对于大型/大型文件,以块(例如,5 MB 的块)的形式加载和处理文件会更快。一旦我们加载了一个块,我们就必须计算所搜索字符串的出现次数。我们存储结果并将其传递给下一个数据块。我们重复这个过程,直到遍历了整个文件。


让我们看一下这个实现的核心行(看一下与本书捆绑在一起的源代码以获得完整的代码):


private static final int MAP_SIZE = 5242880; // 5 MB in bytes
public static int countOccurrences(Path path, String text)
                                          throws IOException {
  final byte[] texttofind = text.getBytes(StandardCharsets.UTF_8);
  int count = 0;
  try (FileChannel fileChannel = FileChannel.open(path,
                                   StandardOpenOption.READ)) {
    int position = 0;
    long length = fileChannel.size();
    while (position < length) {
      long remaining = length - position;
      int bytestomap = (int) Math.min(MAP_SIZE, remaining);
      MappedByteBuffer mbBuffer = fileChannel.map(
        MapMode.READ_ONLY, position, bytestomap);
      int limit = mbBuffer.limit();
      int lastSpace = -1;
      int firstChar = -1;
      while (mbBuffer.hasRemaining()) {        
        // spaghetti code omitted for brevity
        ...
      }
    }
  }
  return count;
}


这个解决方案非常快,因为文件直接从操作系统的内存中读取,而不必加载到 JVM 中。这些操作在本机级别进行,称为操作系统级别。请注意,此实现仅适用于 UTF-8 字符集,但也可以适用于其他字符集。



141 将 JSON/CSV 文件读取为对象

如今,JSON 和 CSV 文件无处不在。读取(反序列化)JSON/CSV 文件可能是一项日常任务,通常位于业务逻辑之前。编写(序列化)JSON/CSV 文件也是一项常见的任务,通常发生在业务逻辑的末尾。在读写这些文件之间,应用将数据用作对象。



将 JSON 文件读/写为对象

让我们从三个文本文件开始,它们代表典型的类似 JSON 的映射:


在melons_raw.json中,每行有一个 JSON 条目。每一行都是一段独立于前一行的 JSON,但具有相同的模式。在melons_array.json中,我们有一个 JSON 数组,而在melons_map.json中,我们有一个非常适合 JavaMap的 JSON 数组。


对于这些文件中的每一个,我们都有一个Path,如下所示:


Path pathArray = Paths.get("melons_array.json");
Path pathMap = Paths.get("melons_map.json");
Path pathRaw = Paths.get("melons_raw.json");


现在,让我们看看三个专用库,它们将这些文件的内容作为Melon实例读取:

public class Melon {
  private String type;
  private int weight;
  // getters and setters omitted for brevity
}



使用 JSON-B


JavaEE8 附带了一个类似 JAXB 的声明性 JSON 绑定,称为 JSON-B(JSR-367)。JSON-B 与 JAXB 和其他 JavaEE/SE API 保持一致。JakartaEE 将 JavaEE8 JSON-P/B 提升到了一个新的层次。其 API 通过javax.json.bind.Jsonb和javax.json.bind.JsonbBuilder类公开:


Jsonb jsonb = JsonbBuilder.create();


对于反序列化,我们使用Jsonb.fromJson(),而对于序列化,我们使用Jsonb.toJson()

  • 让我们把melons_array.json读作MelonArray
Melon[] melonsArray = jsonb.fromJson(Files.newBufferedReader(
  pathArray, StandardCharsets.UTF_8), Melon[].class);


让我们把melons_array.json读作MelonList

List<Melon> melonsList 
  = jsonb.fromJson(Files.newBufferedReader(
    pathArray, StandardCharsets.UTF_8), ArrayList.class);

   让我们把melons_map.json读作Melon的Map:

Map<String, Melon> melonsMap 
  = jsonb.fromJson(Files.newBufferedReader(
    pathMap, StandardCharsets.UTF_8), HashMap.class);


让我们把melons_raw.json逐行读成Map

Map<String, String> stringMap = new HashMap<>();
try (BufferedReader br = Files.newBufferedReader(
    pathRaw, StandardCharsets.UTF_8)) {
  String line;
  while ((line = br.readLine()) != null) {
    stringMap = jsonb.fromJson(line, HashMap.class);
    System.out.println("Current map is: " + stringMap);
  }
}


让我们把melons_raw.json逐行读成Melon

try (BufferedReader br = Files.newBufferedReader(
    pathRaw, StandardCharsets.UTF_8)) {
  String line;
  while ((line = br.readLine()) != null) {
    Melon melon = jsonb.fromJson(line, Melon.class);
    System.out.println("Current melon is: " + melon);
  }
}


让我们将一个对象写入一个 JSON 文件(melons_output.json):

Path path = Paths.get("melons_output.json");
jsonb.toJson(melonsMap, Files.newBufferedWriter(path,
  StandardCharsets.UTF_8, StandardOpenOption.CREATE, 
    StandardOpenOption.WRITE));




使用 Jackson


Jackson 是一个流行且快速的库,专门用于处理(序列化/反序列化)JSON 数据。Jackson API 依赖于com.fasterxml.jackson.databind.ObjectMapper。让我们再看一次前面的例子,但这次使用的是 Jackson:

ObjectMapper mapper = new ObjectMapper();



反序列化使用ObjectMapper.readValue(),序列化使用ObjectMapper.writeValue()

  • 让我们把melons_array.json读作MelonArray
Melon[] melonsArray 
  = mapper.readValue(Files.newBufferedReader(
    pathArray, StandardCharsets.UTF_8), Melon[].class);



   让我们把melons_array.json读作Melon的List:

List<Melon> melonsList  
  = mapper.readValue(Files.newBufferedReader(
    pathArray, StandardCharsets.UTF_8), ArrayList.class);

   让我们把melons_map.json读作Melon的Map:

Map<String, Melon> melonsMap  
  = mapper.readValue(Files.newBufferedReader(
    pathMap, StandardCharsets.UTF_8), HashMap.class);


让我们把melons_raw.json逐行读成Map

Map<String, String> stringMap = new HashMap<>();
try (BufferedReader br = Files.newBufferedReader(
    pathRaw, StandardCharsets.UTF_8)) {
  String line;
  while ((line = br.readLine()) != null) {
    stringMap = mapper.readValue(line, HashMap.class);
    System.out.println("Current map is: " + stringMap);
  }
}


让我们把melons_raw.json逐行读成Melon

try (BufferedReader br = Files.newBufferedReader(
    pathRaw, StandardCharsets.UTF_8)) {
  String line;
  while ((line = br.readLine()) != null) {
    Melon melon = mapper.readValue(line, Melon.class);
    System.out.println("Current melon is: " + melon);
  }
}


让我们将一个对象写入一个 JSON 文件(melons_output.json):

Path path = Paths.get("melons_output.json");
mapper.writeValue(Files.newBufferedWriter(path, 
  StandardCharsets.UTF_8, StandardOpenOption.CREATE, 
    StandardOpenOption.WRITE), melonsMap);



使用 Gson


Gson 是另一个专门用于处理(序列化/反序列化)JSON 数据的简单库。在 Maven 项目中,它可以作为依赖项添加到pom.xml。它的 API 依赖于类名com.google.gson.Gson。本书附带的代码提供了一组示例。


将 CSV 文件作为对象读取


最简单的 CSV 文件类似于下图中的文件(用逗号分隔的数据行):


反序列化这种 CSV 文件的简单而有效的解决方案依赖于BufferedReader和String.split()方法。我们可以通过BufferedReader.readLine()读取文件中的每一行,并通过Spring.split()用逗号分隔符将其拆分。结果(每行内容)可以存储在List<String>中。最终结果为List<List<String>>,如下所示:


public static List<List<String>> readAsObject(
    Path path, Charset cs, String delimiter) throws IOException {
  List<List<String>> content = new ArrayList<>();
  try (BufferedReader br = Files.newBufferedReader(path, cs)) {
    String line;
    while ((line = br.readLine()) != null) {
      String[] values = line.split(delimiter);
      content.add(Arrays.asList(values));
    }
  }
  return content;
}

如果 CSV 数据有 POJO 对应项(例如,我们的 CSV 是序列化一堆Melon实例的结果),那么它可以反序列化,如下例所示:

public static List<Melon> readAsMelon(
    Path path, Charset cs, String delimiter) throws IOException {
  List<Melon> content = new ArrayList<>();
  try (BufferedReader br = Files.newBufferedReader(path, cs)) {
    String line;
    while ((line = br.readLine()) != null) {
      String[] values = line.split(Pattern.quote(delimiter));
      content.add(new Melon(values[0], Integer.valueOf(values[1])));
    }
  }
  return content;
}

对于复杂的 CSV 文件,建议使用专用库(例如 OpenCSV、ApacheCommons CSV、Super CSV 等)。



142 使用临时文件/文件夹


JavaNIO.2API 支持使用临时文件夹/文件。例如,我们可以很容易地找到临时文件夹/文件的默认位置,如下所示:

String defaultBaseDir = System.getProperty("java.io.tmpdir");


通常,在 Windows 中,默认的临时文件夹是C:\Temp, %Windows%\Temp,或者是Local Settings\Temp中每个用户的临时目录(这个位置通常通过TEMP环境变量控制)。在 Linux/Unix 中,全局临时目录是/tmp和/var/tmp。前一行代码将返回默认位置,具体取决于操作系统。


在下一节中,我们将学习如何创建临时文件夹/文件。



相关文章
|
1天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第12天】 在现代软件开发中,多线程编程是提升应用程序性能和响应能力的关键手段之一。特别是在Java语言中,由于其内置的跨平台线程支持,开发者可以轻松地创建和管理线程。然而,随之而来的并发问题也不容小觑。本文将探讨Java并发编程的核心概念,包括线程安全策略、锁机制以及性能优化技巧。通过实例分析与性能比较,我们旨在为读者提供一套既确保线程安全又兼顾性能的编程指导。
|
2天前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第11天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个方面,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。我们将通过实例和代码片段来说明这些概念和技术。
3 0
|
2天前
|
Java 调度
Java并发编程:深入理解线程池
【5月更文挑战第11天】本文将深入探讨Java中的线程池,包括其基本概念、工作原理以及如何使用。我们将通过实例来解释线程池的优点,如提高性能和资源利用率,以及如何避免常见的并发问题。我们还将讨论Java中线程池的实现,包括Executor框架和ThreadPoolExecutor类,并展示如何创建和管理线程池。最后,我们将讨论线程池的一些高级特性,如任务调度、线程优先级和异常处理。
|
3天前
|
Java 开发者
Java一分钟之-Java IO流:文件读写基础
【5月更文挑战第10天】本文介绍了Java IO流在文件读写中的应用,包括`FileInputStream`和`FileOutputStream`用于字节流操作,`BufferedReader`和`PrintWriter`用于字符流。通过代码示例展示了如何读取和写入文件,强调了常见问题如未关闭流、文件路径、编码、权限和异常处理,并提供了追加写入与读取的示例。理解这些基础知识和注意事项能帮助开发者编写更可靠的程序。
13 0
|
3天前
|
Java
【JAVA基础篇教学】第十三篇:Java中I/O和文件操作
【JAVA基础篇教学】第十三篇:Java中I/O和文件操作
|
3天前
|
缓存 Java 数据库
Java并发编程学习11-任务执行演示
【5月更文挑战第4天】本篇将结合任务执行和 Executor 框架的基础知识,演示一些不同版本的任务执行Demo,并且每个版本都实现了不同程度的并发性。
24 4
Java并发编程学习11-任务执行演示
|
4天前
|
存储 安全 Java
12条通用编程原则✨全面提升Java编码规范性、可读性及性能表现
12条通用编程原则✨全面提升Java编码规范性、可读性及性能表现
|
4天前
|
缓存 Java 程序员
关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
|
4天前
|
Java
JDK环境下利用记事本对java文件进行运行编译
JDK环境下利用记事本对java文件进行运行编译
13 0
|
4天前
|
缓存 Java 数据库
Java并发编程中的锁优化策略
【5月更文挑战第9天】 在高负载的多线程应用中,Java并发编程的高效性至关重要。本文将探讨几种常见的锁优化技术,旨在提高Java应用程序在并发环境下的性能。我们将从基本的synchronized关键字开始,逐步深入到更高效的Lock接口实现,以及Java 6引入的java.util.concurrent包中的高级工具类。文中还会介绍读写锁(ReadWriteLock)的概念和实现原理,并通过对比分析各自的优势和适用场景,为开发者提供实用的锁优化策略。
6 0