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。前一行代码将返回默认位置,具体取决于操作系统。


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



相关文章
|
19天前
|
安全 Java 数据库连接
2025 年最新 Java 学习路线图含实操指南助你高效入门 Java 编程掌握核心技能
2025年最新Java学习路线图,涵盖基础环境搭建、核心特性(如密封类、虚拟线程)、模块化开发、响应式编程、主流框架(Spring Boot 3、Spring Security 6)、数据库操作(JPA + Hibernate 6)及微服务实战,助你掌握企业级开发技能。
172 3
|
24天前
|
存储 Java
Java LocalDateTime与hutool DateUtil实现秒转换为时分秒格式化展示。
注意:以上代码示例仅适合于处理小于24小时内(86400s) 总积 因 LocalDateTime 和 Local Time 不支持超过24小时表达而 huo tool示例虽然理论上支持但未考虑超过24小时情况下可能出现负值等异常情况处理细节需用户自行添加相关逻辑以确保正确性.
126 7
|
1月前
|
Java
Java编程:理解while循环的使用
总结而言, 使用 while 迴圈可以有效解决需要多次重复操作直至特定條件被触发才停止執行任务场景下问题; 它简单、灵活、易于实现各种逻辑控制需求但同时也要注意防止因邏各错误导致無限迁璇発生及及時處理可能発生异常以确保程序稳定运作。
154 0
|
1月前
|
安全 Cloud Native Java
Java:历久弥新的企业级编程基石
Java:历久弥新的企业级编程基石
|
1月前
|
移动开发 Cloud Native Java
Java:历久弥新的企业级编程基石
Java:历久弥新的企业级编程基石
|
2月前
|
设计模式 Java 数据库连接
Java编程的知识体系 | Java编程精要
Java是一种广泛使用的通用编程语言,具备面向对象、跨平台、安全简单等优势,适用于桌面、企业、Web、移动及大数据等多个领域。它功能强大且易于学习,是程序设计入门和面向对象思想学习的优选语言。本书系统讲解Java编程知识,涵盖技术核心与应用拓展两大模块,内容包括基础语法、面向对象设计、GUI、数据库、多线程、网络编程及Web开发等,帮助读者全面掌握Java开发技能。
70 0
|
2月前
|
安全 Java
Java编程探究:深入解析final关键字
1. **使用限制**: 对于 `final` 方法和类,可以限制其他开发人员对代码的使用,确保其按设计的方式工作而不会被子类意外改变。
84 0
|
Java
java中I/O流之字节流和字符流学习总结(下)
java中I/O流之字节流和字符流学习总结(下)
155 0
java中I/O流之字节流和字符流学习总结(下)
|
移动开发 Java 数据库
java中I/O流之字节流和字符流学习总结(上)
java中I/O流之字节流和字符流学习总结(上)
169 0
java中I/O流之字节流和字符流学习总结(上)
|
移动开发 Java
Java I/O学习(附实例和详解)
版权声明:本文为博主原创文章,转载注明出处http://blog.csdn.net/u013142781 目录(?)[+] 一、Java I/O类结构以及流的基本概念 在阅读Java I/O的实例之前我们必须清楚一些概念,我们先看看Java I/O的类结构图: Java I/O主要以流的形式进行读写数据。
2576 0