Java读取超过内存大小的文件

2024-03-30 05:04

本文主要是介绍Java读取超过内存大小的文件,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

读取文件内容,然后进行处理,在Java中我们通常利用 Files 类中的方法,将可以文件内容加载到内存,并流顺利地进行处理。但是,在一些场景下,我们需要处理的文件可能比我们机器所拥有的内存要大。此时,我们则需要采用另一种策略:部分读取它,并具有其他结构来仅编译所需的数据。

接下来,我们就来说说这一场景:当遇到大文件,无法一次载入内存时候要如何处理。

模拟场景

假设,当前我们需要开发一个程序来分析来自服务器的日志文件,并生成一份报告,列出前 10 个最常用的应用程序。

每天,都会生成一个新的日志文件,其中包含时间戳、主机信息、持续时间、服务调用等信息,以及可能与我们的特定方案无关的其他数据。

2024-02-25T00:00:00.000+GMT host7 492 products 0.0.3 PUT 73.182.150.152 eff0fac5-b997-40a3-87d8-02ff2f397b44
2024-02-25T00:00:00.016+GMT host6 123 logout 2.0.3 GET 34.235.76.94 8b97acae-dd36-4e83-b423-12905a4ab38d
2024-02-25T00:00:00.033+GMT host6 50 payments/:id 0.4.6 PUT 148.241.146.59 ac3c9064-4782-46d9-a0b6-69e4d55a5b38
2024-02-25T00:00:00.050+GMT host2 547 orders 1.5.0 PUT 6.232.116.248 2285a81e-c511-41b9-b0ea-a475a0a45805
2024-02-25T00:00:00.067+GMT host4 400 suggestions 0.8.6 DELETE 149.138.227.154 8031b639-700e-4a7c-b257-fcbed0d029ce
2024-02-25T00:00:00.084+GMT host2 644 login 6.90 GET 208.158.145.204 3906a28c-56e4-4e5f-b548-591eab737aa7
2024-02-25T00:00:00.101+GMT host5 339 suggestions 0.8.9 PUT 173.109.21.97 c7dfec8a-5ca8-4d0d-b903-aaf65629fdd0
2024-02-25T00:00:00.118+GMT host9 87 products 2.6.3 POST 220.252.90.140 e5ceef67-2f0f-4c2d-a6d2-c698598aaef2
2024-02-25T00:00:00.134+GMT host0 845 products 9.4.6 GET 136.79.178.188 f28578c1-c37c-47a3-a473-4e65371e0245
2024-02-25T00:00:00.151+GMT host4 675 login 0.89 DELETE 32.159.65.239 d27ff353-e501-43e6-bdce-680d79a07c36

我们的代码将收到日志文件列表,我们的目标是编制一份报告,列出最常用的 10 个服务。但是,要包含在报告中,服务必须在提供的每个日志文件中至少有一个条目。简而言之,一项服务必须每天使用才有资格包含在报告中。

基础实现

解决这个问题的最初方法是考虑业务需求并创建以下代码:

public void processFiles(final List<File> fileList) {final Map<LocalDate, List<LogLine>> fileContent = getFileContent(fileList);final List<String> serviceList = getServiceList(fileContent);final List<Statistics> statisticsList = getStatistics(fileContent, serviceList);final List<Statistics> topCalls = getTop10(statisticsList);print(topCalls);
}

该方法接收文件列表作为参数,核心流程如下:

  • 创建一个包含每个文件条目的映射,其中Key是 LocalDate,Value是文件行列表。
  • 使用所有文件中的唯一服务名称创建字符串列表。
  • 生成所有服务的统计信息列表,将文件中的数据组织到结构化地图中。
  • 筛选统计信息,获取排名前 10 的服务调用。
  • 打印结果。

可以注意到,这种方法将太多数据加载到内存中,不可避免地会导致 OutOfMemoryError

改进实现

就如文章开头说的,我们需要采用另一种策略:逐行处理文件的模式。

private void processFiles(final List<File> fileList) {final Map<String, Counter> compiledMap = new HashMap<>();for (int i = 0; i < fileList.size(); i++) {processFile(fileList, compiledMap, i);}final List<Counter> topCalls =compiledMap.values().stream().filter(Counter::allDaysSet).sorted(Comparator.comparing(Counter::getNumberOfCalls).reversed()).limit(10).toList();print(topCalls);
}
  • 首先,它声明一个Map(compiledMap),其中一个String作为键,代表服务名称,以及一个Counter对象(稍后解释),它将存储统计信息。
  • 接下来,它逐一处理这些文件并相应地更新compileMap。
  • 然后,它利用流功能来: 仅过滤具有全天数据的计数器;按调用次数排序;最后,检索前 10 名。

在看整个处理的核心processFile方法之前,我们先来分析一下Counter类,它在这个过程中也起到了至关重要的作用:

public class Counter {@Getter private String serviceName;@Getter private long numberOfCalls;private final BitSet daysWithCalls;public Counter(final String serviceName, final int numberOfDays) {this.serviceName = serviceName;this.numberOfCalls = 0L;daysWithCalls = new BitSet(numberOfDays);}public void add() {numberOfCalls++;}public void setDay(final int dayNumber) {daysWithCalls.set(dayNumber);}public boolean allDaysSet() {return daysWithCalls.stream().mapToObj(index -> daysWithCalls.get(index)).reduce(Boolean.TRUE, Boolean::logicalAnd);}
}
  • 它包含三个属性:serviceName、numberOfCalls 和 daysWithCalls
  • numberOfCalls 属性通过 add 方法递增,该方法为 serviceName 的每个处理行调用。
  • daysWithCalls 属性是一个 Java BitSet,一种用于存储布尔属性的内存高效结构。它使用要处理的天数进行初始化,每个位代表一天,初始化为 false。
  • setDay 方法将 BitSet 中与给定日期位置相对应的位设置为 true。

allDaysSet 方法负责检查 BitSet 中的所有日期是否都设置为 true。它通过将 BitSet 转换为布尔流,然后使用逻辑 AND 运算符减少它来实现此目的。

private void processFile(final List<File> fileList, final Map<String, Counter> compiledMap, final int dayNumber) {try (Stream<String> lineStream = Files.lines(fileList.get(dayNumber).toPath())) {lineStream.map(this::toLogLine).forEach(logLine -> {Counter counter = compiledMap.get(logLine.serviceName());if (counter == null) {counter = new Counter(logLine.serviceName(), fileList.size());compiledMap.put(logLine.serviceName(), counter);}counter.add();counter.setDay(dayNumber);});} catch (final IOException e) {throw new RuntimeException(e);}
}
  • 该过程使用Files类的lines方法逐行读取文件,并将其转换为流。这里的关键特征是lines方法是惰性的,这意味着它不会立即读取整个文件;相反,它会在流被消耗时读取文件。
  • toLogLine 方法将每个字符串文件行转换为具有用于访问日志行信息的属性的对象。
  • 处理文件行的主要过程比预期的要简单。它从与serviceName关联的compileMap中检索(或创建)Counter,然后调用Counter的add和setDay方法。

正如我们所看到的,在 Java 中处理大文件而不将整个文件加载到内存中并不是什么复杂的事情。 Files类提供了逐行处理文件的方法,我们还可以在文件处理过程中利用哈希来存储数据,这有助于节省内存。

这篇关于Java读取超过内存大小的文件的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/860539

相关文章

JavaWeb-WebSocket浏览器服务器双向通信方式

《JavaWeb-WebSocket浏览器服务器双向通信方式》文章介绍了WebSocket协议的工作原理和应用场景,包括与HTTP的对比,接着,详细介绍了如何在Java中使用WebSocket,包括配... 目录一、概述二、入门2.1 POM依赖2.2 编写配置类2.3 编写WebSocket服务2.4 浏

配置springboot项目动静分离打包分离lib方式

《配置springboot项目动静分离打包分离lib方式》本文介绍了如何将SpringBoot工程中的静态资源和配置文件分离出来,以减少jar包大小,方便修改配置文件,通过在jar包同级目录创建co... 目录前言1、分离配置文件原理2、pom文件配置3、使用package命令打包4、总结前言默认情况下,

Java文件与Base64之间的转化方式

《Java文件与Base64之间的转化方式》这篇文章介绍了如何使用Java将文件(如图片、视频)转换为Base64编码,以及如何将Base64编码转换回文件,通过提供具体的工具类实现,作者希望帮助读者... 目录Java文件与Base64之间的转化1、文件转Base64工具类2、Base64转文件工具类3、

java获取图片的大小、宽度、高度方式

《java获取图片的大小、宽度、高度方式》文章介绍了如何将File对象转换为MultipartFile对象的过程,并分享了个人经验,希望能为读者提供参考... 目China编程录Java获取图片的大小、宽度、高度File对象(该对象里面是图片)MultipartFile对象(该对象里面是图片)总结java获取图片

Java通过反射获取方法参数名的方式小结

《Java通过反射获取方法参数名的方式小结》这篇文章主要为大家详细介绍了Java如何通过反射获取方法参数名的方式,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1、前言2、解决方式方式2.1: 添加编译参数配置 -parameters方式2.2: 使用Spring的内部工具类 -

Java如何获取视频文件的视频时长

《Java如何获取视频文件的视频时长》文章介绍了如何使用Java获取视频文件的视频时长,包括导入maven依赖和代码案例,同时,也讨论了在运行过程中遇到的SLF4J加载问题,并给出了解决方案... 目录Java获取视频文件的视频时长1、导入maven依赖2、代码案例3、SLF4J: Failed to lo

如何使用Spring boot的@Transactional进行事务管理

《如何使用Springboot的@Transactional进行事务管理》这篇文章介绍了SpringBoot中使用@Transactional注解进行声明式事务管理的详细信息,包括基本用法、核心配置... 目录一、前置条件二、基本用法1. 在方法上添加注解2. 在类上添加注解三、核心配置参数1. 传播行为(

在Java中使用ModelMapper简化Shapefile属性转JavaBean实战过程

《在Java中使用ModelMapper简化Shapefile属性转JavaBean实战过程》本文介绍了在Java中使用ModelMapper库简化Shapefile属性转JavaBean的过程,对比... 目录前言一、原始的处理办法1、使用Set方法来转换2、使用构造方法转换二、基于ModelMapper

JAVA调用Deepseek的api完成基本对话简单代码示例

《JAVA调用Deepseek的api完成基本对话简单代码示例》:本文主要介绍JAVA调用Deepseek的api完成基本对话的相关资料,文中详细讲解了如何获取DeepSeekAPI密钥、添加H... 获取API密钥首先,从DeepSeek平台获取API密钥,用于身份验证。添加HTTP客户端依赖使用Jav

Java实现状态模式的示例代码

《Java实现状态模式的示例代码》状态模式是一种行为型设计模式,允许对象根据其内部状态改变行为,本文主要介绍了Java实现状态模式的示例代码,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来... 目录一、简介1、定义2、状态模式的结构二、Java实现案例1、电灯开关状态案例2、番茄工作法状态案例