读取文件内容,然后进行处理,在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类提供了逐行处理文件的方法,我们还可以在文件处理过程中利用哈希来存储数据,这有助于节省内存。

欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源

如何在Java中读取超过内存大小的文件的更多相关文章

  1. 关于c# 中读取系统内存大小的问题。

    在程序中,调用WMI的时候,出现一个问题,就是我系统有插了两条内存条,共4G.然而自己只能在程序中查到安装内存为2G,感觉有点不淡定.这是之前的代码. static ManagementObjectS ...

  2. 用代码说话:如何在Java中实现线程

    并发编程是Java语言的重要特性之一,"如何在Java中实现线程"是学习并发编程的入门知识,也是Java工程师面试必备的基础知识.本文从线程说起,然后用代码说明如何在Java中实现 ...

  3. 如何在Java中测试类是否是线程安全的

    通过优锐课的java核心笔记中,我们可以看到关于如何在java中测试类是否线程安全的一些知识点汇总,分享给大家学习参考. 线程安全性测试与典型的单线程测试不同.为了测试一个方法是否是线程安全的,我们需 ...

  4. Java解决大文件读取的内存问题以及文件流的比较

    Java解决大文件读取的内存问题以及文件流的比较 传统方式 读取文件的方式一般是是从内存中读取,官方提供了几种方式,如BufferedReader, 以及InputStream 系列的,也有封装好的如 ...

  5. 如何在 Java 中实现 Dijkstra 最短路算法

    定义 最短路问题的定义为:设 \(G=(V,E)\) 为连通图,图中各边 \((v_i,v_j)\) 有权 \(l_{ij}\) (\(l_{ij}=\infty\) 表示 \(v_i,v_j\) 间 ...

  6. 4种方法教你如何查看java对象所占内存大小

    摘要:本文讲述4种查看java对象所占内存大小的方法 本文分享自华为云社区<查看java对象所占内存大小>,作者:xiewenci. 计算java对象所占内存大小 1.使用jdk8自带AP ...

  7. 如何在JAVA中实现一个固定最大size的hashMap

    如何在JAVA中实现一个固定最大size的hashMap 利用LinkedHashMap的removeEldestEntry方法,重载此方法使得这个map可以增长到最大size,之后每插入一条新的记录 ...

  8. 如何在java中使用sikuli进行自动化测试

    很早之前写过一篇介绍sikuli的文章.本文简单介绍如何在java中使用sikuli进自动化测试. 图形脚本语言sikuli sikuli IDE可以完成常见的单击.右击.移动到.拖动等鼠标操作,ja ...

  9. Java中读取文件

    Java中读取文件,去除一些分隔符,保存在多维数组里面 public void readFile(String filePath) { File file=new File(filePath); Ar ...

  10. Java宝典(四)------Java中也存在内存泄露。

    --Java中会存在内存泄露吗? --如果你想当然的以为Java里有了垃圾回收机制就不会存在内存泄露,那你就错了. Java里也会存在内存泄露! 我们慢慢来分析. 所谓内存泄露就是指一个不再被程序使用 ...

随机推荐

  1. spring boot 2.0集成并使用redis

    项目地址:https://gitee.com/indexman/spring_boot_in_action 前面一章介绍了spring boot自带的缓存,下面讲一下如何在2.0版本中集成并使用red ...

  2. C++ sentry 如何压缩日志文件

    项目中在使用 sentry 上传事件的 attachment 函数过程中发现,附带的 log 文件是未压缩的,于是有了需求,即需要在 sentry 内部将未压缩的文件流压缩后再上传给服务器 这个需求看 ...

  3. C++ 多线程的错误和如何避免(8)

    不要重复获取同一个锁 问题:在获得一个锁并且没有释放该锁的前提下,再次尝试获取该锁会报错. 比如, #include <iostream> #include <thread> ...

  4. heapq.merge()高效合并有序序列

    import heapq import random # 针对较大的数据集,采用heapq中的merge()方法 # merge()不是对整个合并后的序列排序,而是使用一个堆一次一个元素的生成一个新序 ...

  5. 百亿节点、毫秒级延迟,携程金融基于 NebulaGraph 的大规模图应用实践

    作者:霖雾,携程数据开发工程师,关注图数据库等领域. 0. 背景 2017 年 9 月携程金融成立,在金融和风控业务中,有多种场景需要对图关系网络进行分析和实时查询,传统关系型数据库难以保证此类场景下 ...

  6. .NET周刊【3月第1期 2024-03-03】

    国内文章 推荐10款C#开源好用的Windows软件 https://www.cnblogs.com/Can-daydayup/p/18035760 DevToys.Microsoft PowerTo ...

  7. CTF中常见编码

    ASCII编码 ASCII HEX DEC flag{hello_ctfer} 66 6c 61 67 7b 68 65 6c 6c 6f 5f 63 74 66 65 72 7d 102 108 9 ...

  8. MySQL学习之初识数据库

    •数据库的相关概念 DB : 数据库,保存一组有组织的数据的容器 DBMS : 数据库管理系统,又称为数据库软件(产品),用于管理 DB 中的数据 SQL : 结构化查询语言,用于和 DBMS 通信的 ...

  9. 【技术积累】MySQL优化及进阶

    MySql优化及进阶 一.MySQL体系结构 连接层:是一些客户端和链接服务,包含本地sock 通信和大多数基于客户端/服务端工具实现的类似于 TCP/IP的通信 服务层:大多数的核心服务功能,如SQ ...

  10. Python面向对象之派生和组合

    [一]什么是派生 派生是指,子类继承父类,派生出自己的属性与方法,并且重用父类的属性与方法 [二]派生的方法 子类可以派生出自己新的属性,在进行属性查找时,子类中的属性名会优先于父类被查找 例如每个老 ...