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


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



相关文章
|
4天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
7天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
9天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
6天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
8天前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
20 2
|
9天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
9天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
37 1
|
9天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
18天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
6天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9