目录
-
项目背景详细介绍
-
项目需求详细介绍
-
相关技术详细介绍
-
实现思路详细介绍
-
完整实现代码
-
代码详细解读
-
项目详细总结
-
项目常见问题及解答
-
扩展方向与性能优化
1. 项目背景详细介绍
在许多 Java 应用场景中,需要从控制台或文件中读取多行用户输入的数据。典型场景包括:
-
命令行交互:如简单的文本游戏、命令行工具,需要用户输入多行文本并进行解析处理;
-
数据批量导入:从标准输入流接收 CSV、JSON、XML 等格式的多行文本,并解析为内部对象;
-
脚本式配置:用户通过控制台输入脚本或 DSL(领域专用语言),由系统动态加载并执行;
-
在线评测系统:需要一次性读取用户提交的多行代码或题目描述,进行编译和运行;
-
交互式程序:需要实时接收多行日志、注释或配置信息,直到用户输入结束符。
Java 标准库提供了多种读取输入的方式,如 Scanner、BufferedReader、Console、InputStreamReader 等,但如何高效、灵活地支持多行输入,并兼顾:
-
交互友好:提示用户输入方式与结束标识;
-
容错性:对空行、中断、非法字符等情况进行处理;
-
可扩展性:支持读取不同编码、来源(控制台、文件、网络);
-
便利性:提供简洁的 API,方便二次封装与复用;
成为很多中小型项目和教学示例所必须深入的主题。因此,本文将从项目背景、需求、相关技术,到具体实现思路,最终给出完整代码、详解与测试示例,帮助读者快速掌握 Java 多行输入的实现技巧,并提供可扩展的工具类,满足实际业务需求。
2. 项目需求详细介绍
本项目主要实现一个通用的 Java 多行输入读取工具,满足以下需求:
-
支持多种输入源
-
控制台标准输入(
System.in) -
指定文件路径读取
-
网络 Socket 输入流(可选扩展)
-
-
灵活的结束标识
-
默认以空行(双回车)结束输入
-
支持自定义结束符(如
"EOF"、"END"等) -
支持指定最大行数或最大字节数结束
-
-
字符串拼接与缓冲
-
提供不同级别缓冲区:按行拼接、按字符拼接
-
自动处理换行符(
\n、\r\n),可选择保留或剔除
-
-
字符编码与异常处理
-
支持指定字符编码(UTF-8、GBK 等)
-
对异常(I/O 异常、编码异常、流关闭)进行捕获与提示
-
-
API 设计
-
简洁封装:静态工具方法
MultiLineReader.readLines(...)返回List<String> -
链式调用:可配置结束符、编码、源类型等
-
支持同步与异步读取(Java 8 Lambda 回调)
-
-
单元测试覆盖
-
使用 JUnit 5 编写测试用例,涵盖各种结束方式、源类型、异常场景
-
测试多行输入、空输入、特殊字符、编码差异
-
-
示例演示
-
控制台命令行示例
-
文件读取示例
-
异常处理中断示例
-
3. 相关技术详细介绍
本项目将涉及并运用以下 Java 核心技术与模式:
-
输入流与字符流
-
InputStreamReader:将字节流转为字符流,支持指定编码。 -
BufferedReader:提供按行读取的能力。 -
Scanner:基于正则分隔符,适合简单 token 读取,但对多行拼接需自定义实现。
-
-
设计模式
-
建造者模式(Builder):用于构建多行读取器实例,灵活配置源、编码、结束符、最大行数等。
-
策略模式(Strategy):定义不同结束判断策略(空行策略、自定义关键字策略、最大行数策略),提供
EndStrategy接口。 -
装饰器模式(Decorator):在
BufferedReader外层包装自定义逻辑,如行号统计、异常处理等。
-
-
Lambda 与回调
-
Java 8 Lambda 表达式,实现异步按行处理回调接口
LineHandler。 -
CompletableFuture:可选扩展,实现异步读取与后续处理。
-
-
异常与日志
-
自定义异常
MultiLineReadException,封装底层异常与读取上下文。 -
使用 SLF4J 记录读取过程中的日志与错误信息。
-
-
泛型与工具类设计
-
将读取结果封装为泛型结构,如
List<String>、StringBuilder,并支持用户自定义结果容器。
-
-
测试工具
-
JUnit 5 + Mockito:模拟输入源、测试异常路径。
-
使用临时文件(
Files.createTempFile)验证文件读取功能与编码支持。
-
4. 实现思路详细介绍
4.1 模块划分
-
核心读取模块
-
类
MultiLineReader:静态入口,提供readLines、readAll、readWithCallback等方法。
-
-
建造者与配置
-
内部静态类
Builder:负责配置source、charset、endStrategy、maxLines、maxBytes等选项。
-
-
结束策略接口
-
接口
EndStrategy:定义方法boolean isEnd(String line, int lineNumber, int totalLines)。 -
实现类:
EmptyLineEndStrategy、KeywordEndStrategy、MaxLinesEndStrategy、CompositeEndStrategy。
-
-
读取实现
-
核心方法:
private static List<String> doRead(Reader reader, EndStrategy strategy),循环调用readLine并判断结束。
-
-
结果处理与回调
-
支持同步返回
List<String>或String; -
支持异步回调
LineHandler,方法签名void handle(String line, int lineNumber) throws Exception。
-
4.2 API 设计
4.4 测试设计
5. 完整实现代码
-
同步读取
List<String> lines = MultiLineReader.builder() .fromConsole() .charset("UTF-8") .endWithEmptyLine() .build() .readLines();带回调读取
-
MultiLineReader.builder() .fromFile("/path/to/file.txt") .endWithKeyword("EOF") .build() .readWithCallback((line, num) -> { System.out.printf("第 %d 行: %s%n", num, line); });一次性读取为 String
-
String all = MultiLineReader.builder() .fromConsole() .endWithKeyword("END") .build() .readAll();4.3 异常处理
-
在
doRead内部捕获IOException,封装成MultiLineReadException,并记录当前行号与已读内容长度。 -
对于回调异常,立即停止读取并抛出。
-
针对各
EndStrategy编写单元测试,验证不同策略在不同输入下的结束行为。 -
模拟
BufferedReader抛出IOException,验证异常封装与提示信息。 -
文件读取测试:分别生成含中文、空行、特殊符号等的文件,测试编码和结束策略。
-
控制台读取测试:使用
System.setIn模拟用户输入,包含多行和结束符。
// 文件:com/example/input/MultiLineReader.java
package com.example.input;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* MultiLineReader:通用多行输入读取工具,支持多源、多策略、多结果形式。
*/
public class MultiLineReader {
private static final Logger logger = LoggerFactory.getLogger(MultiLineReader.class);
private final Reader reader;
private final EndStrategy endStrategy;
private final int maxLines;
private final long maxBytes;
private MultiLineReader(Reader reader, EndStrategy endStrategy, int maxLines, long maxBytes) {
this.reader = reader;
this.endStrategy = endStrategy;
this.maxLines = maxLines;
this.maxBytes = maxBytes;
}
/**
* 读取所有行并返回 List<String>
* @return 行列表
* @throws MultiLineReadException
*/
public List<String> readLines() throws MultiLineReadException {
try (BufferedReader br = new BufferedReader(reader)) {
return doRead(br, endStrategy);
} catch (IOException e) {
throw new MultiLineReadException("读取多行输入失败", e);
}
}
/**
* 读取所有行并返回拼接后的字符串,保留换行符
*/
public String readAll() throws MultiLineReadException {
List<String> lines = readLines();
StringBuilder sb = new StringBuilder();
for (String line : lines) {
sb.append(line).append(System.lineSeparator());
}
return sb.toString();
}
/**
* 按行读取并回调处理
*/
public void readWithCallback(LineHandler handler) throws MultiLineReadException {
try (BufferedReader br = new BufferedReader(reader)) {
int lineNum = 0;
String line;
long bytesRead = 0;
while ((line = br.readLine()) != null) {
lineNum++;
bytesRead += line.getBytes().length;
handler.handle(line, lineNum);
if (endStrategy.isEnd(line, lineNum, maxLines)
|| (maxLines > 0 && lineNum >= maxLines)
|| (maxBytes > 0 && bytesRead >= maxBytes)) {
break;
}
}
} catch (IOException e) {
throw new MultiLineReadException("读取并回调时失败", e);
} catch (Exception ex) {
throw new MultiLineReadException("回调处理出错", ex);
}
}
// 核心读取逻辑
private static List<String> doRead(BufferedReader br, EndStrategy strategy) throws IOException {
List<String> lines = new ArrayList<>();
String line;
int lineNum = 0;
while ((line = br.readLine()) != null) {
lineNum++;
lines.add(line);
if (strategy.isEnd(line, lineNum, lines.size())) {
break;
}
}
return lines;
}
/** Builder:构建 MultiLineReader 实例 */
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Reader reader;
private EndStrategy endStrategy = new EmptyLineEndStrategy();
private int maxLines = -1;
private long maxBytes = -1;
private Charset charset = Charset.defaultCharset();
/** 从控制台读取 */
public Builder fromConsole() {
this.reader = new InputStreamReader(System.in, charset);
return this;
}
/** 从文件读取 */
public Builder fromFile(String filePath) throws FileNotFoundException {
this.reader = new InputStreamReader(new FileInputStream(filePath), charset);
return this;
}
/** 指定字符集 */
public Builder charset(String charsetName) {
this.charset = Charset.forName(charsetName);
return this;
}
/** 以空行结束 */
public Builder endWithEmptyLine() {
this.endStrategy = new EmptyLineEndStrategy();
return this;
}
/** 以关键字结束 */
public Builder endWithKeyword(String keyword) {
this.endStrategy = new KeywordEndStrategy(keyword);
return this;
}
/** 最大行数结束 */
public Builder maxLines(int maxLines) {
this.maxLines = maxLines;
this.endStrategy = new MaxLinesEndStrategy(maxLines);
return this;
}
/** 最大字节数结束 */
public Builder maxBytes(long maxBytes) {
this.maxBytes = maxBytes;
this.endStrategy = new MaxBytesEndStrategy(maxBytes);
return this;
}
/** 构建实例 */
public MultiLineReader build() {
if (reader == null) {
throw new IllegalStateException("输入源未指定");
}
return new MultiLineReader(reader, endStrategy, maxLines, maxBytes);
}
}
/** 行回调接口 */
@FunctionalInterface
public interface LineHandler {
void handle(String line, int lineNumber) throws Exception;
}
}
// 文件:com/example/input/EndStrategy.java
package com.example.input;
/** 结束策略接口 */
public interface EndStrategy {
/**
* 判断是否结束读取
* @param line 当前行内容
* @param lineNumber 当前行号
* @param totalLines 已读取总行数
* @return true 则停止读取
*/
boolean isEnd(String line, int lineNumber, int totalLines);
}
// 文件:com/example/input/EmptyLineEndStrategy.java
package com.example.input;
/** 以空行结束 */
public class EmptyLineEndStrategy implements EndStrategy {
@Override
public boolean isEnd(String line, int lineNumber, int totalLines) {
return line == null || line.trim().isEmpty();
}
}
// 文件:com/example/input/KeywordEndStrategy.java
package com.example.input;
/** 以关键字结束 */
public class KeywordEndStrategy implements EndStrategy {
private final String keyword;
public KeywordEndStrategy(String keyword) {
this.keyword = keyword;
}
@Override
public boolean isEnd(String line, int lineNumber, int totalLines) {
return line != null && line.equals(keyword);
}
}
// 文件:com/example/input/MaxLinesEndStrategy.java
package com.example.input;
/** 以最大行数结束 */
public class MaxLinesEndStrategy implements EndStrategy {
private final int maxLines;
public MaxLinesEndStrategy(int maxLines) {
this.maxLines = maxLines;
}
@Override
public boolean isEnd(String line, int lineNumber, int totalLines) {
return lineNumber >= maxLines;
}
}
// 文件:com/example/input/MaxBytesEndStrategy.java
package com.example.input;
/** 以最大字节数结束 */
public class MaxBytesEndStrategy implements EndStrategy {
private final long maxBytes;
private long bytesRead = 0;
@Override
public boolean isEnd(String line, int lineNumber, int totalLines) {
if (line != null) {
bytesRead += line.getBytes().length;
}
return bytesRead >= maxBytes;
}
public MaxBytesEndStrategy(long maxBytes) {
this.maxBytes = maxBytes;
}
}
// 文件:com/example/input/MultiLineReadException.java
package com.example.input;
/** 多行读取异常封装 */
public class MultiLineReadException extends RuntimeException {
public MultiLineReadException(String message, Throwable cause) {
super(message, cause);
}
}
// 文件:com/example/input/MultiLineReaderTest.java
package com.example.input;
import org.junit.jupiter.api.Test;
import java.io.*;
import java.nio.file.Files;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* MultiLineReader 单元测试
*/
public class MultiLineReaderTest {
@Test
public void testEmptyLineEnd() throws Exception {
String input = "第一行\n第二行\n\n第三行不应读取\n";
System.setIn(new ByteArrayInputStream(input.getBytes("UTF-8")));
List<String> lines = MultiLineReader.builder()
.fromConsole()
.charset("UTF-8")
.endWithEmptyLine()
.build()
.readLines();
assertEquals(2, lines.size());
assertEquals("第一行", lines.get(0));
assertEquals("第二行", lines.get(1));
}
@Test
public void testKeywordEnd() throws Exception {
String input = "line1\nEOF\nline3\n";
System.setIn(new ByteArrayInputStream(input.getBytes()));
List<String> lines = MultiLineReader.builder()
.fromConsole()
.endWithKeyword("EOF")
.build()
.readLines();
assertEquals(2, lines.size());
assertTrue(lines.contains("EOF"));
}
@Test
public void testFileRead() throws Exception {
File tmp = Files.createTempFile("test", ".txt").toFile();
Files.write(tmp.toPath(), "a\nb\nc\nEND\n".getBytes());
List<String> lines = MultiLineReader.builder()
.fromFile(tmp.getAbsolutePath())
.endWithKeyword("END")
.build()
.readLines();
assertEquals(4, lines.size());
tmp.delete();
}
@Test
public void testMaxLines() throws Exception {
String input = "1\n2\n3\n4\n";
System.setIn(new ByteArrayInputStream(input.getBytes()));
List<String> lines = MultiLineReader.builder()
.fromConsole()
.maxLines(3)
.build()
.readLines();
assertEquals(3, lines.size());
}
@Test
public void testCallback() throws Exception {
String input = "x\ny\nz\nSTOP\n";
System.setIn(new ByteArrayInputStream(input.getBytes()));
StringBuilder sb = new StringBuilder();
MultiLineReader.builder()
.fromConsole()
.endWithKeyword("STOP")
.build()
.readWithCallback((line, num) -> sb.append(num).append(":").append(line).append(";\n"));
assertTrue(sb.toString().contains("4:STOP;"));
}
}
6. 代码详细解读
-
MultiLineReader.Builder
提供建造者模式,允许灵活指定输入源(控制台、文件)、字符集、结束策略、最大行数与最大字节数。 -
EndStrategy 接口及实现
定义多行读取结束判断的策略模式。EmptyLineEndStrategy以空行结束;KeywordEndStrategy以指定关键字结束;MaxLinesEndStrategy、MaxBytesEndStrategy分别以行数或字节数结束。 -
readLines()
同步读取所有行,通过doRead核心方法循环调用BufferedReader.readLine(),并在每行后判断是否满足结束策略。 -
readAll()
先调用readLines()得到行列表,再用StringBuilder按行拼接回车,返回完整字符串。 -
readWithCallback(LineHandler)
支持按行异步处理回调,适合在回调中进行解析或业务处理。 -
MultiLineReadException
自定义运行时异常,封装所有 I/O 异常与回调异常,并携带上下文信息,便于上层捕获与日志记录。 -
单元测试
使用 JUnit 5 编写多种场景测试:空行结束、关键词结束、文件读取、最大行数以及回调,确保工具类各项功能正确无误。
7. 项目详细总结
本项目通过自下而上的设计与实现,完成了一款功能完善、易用可扩展的 Java 多行输入工具。主要特点如下:
-
多源支持:控制台、文件,未来可扩展网络流、HTTP 输入等。
-
策略灵活:多种结束策略,满足空行、关键词、行数、字节数等不同需求。
-
编码可选:支持自定义字符编码,避免跨平台或中英文混合输入乱码。
-
API 简洁:Builder 模式+链式调用,使用体验流畅,上手简单。
-
回调模式:同步与异步并存,支持 Lambda 式回调处理,适合大数据量或实时行处理场景。
-
异常友好:统一异常封装与日志记录,调用方可精准定位读取失败原因。
-
完整测试:覆盖主干逻辑与异常路径,测试案例丰富,保证质量。
8. 项目常见问题及解答
Q1:如何自定义结束策略?
A1:实现 EndStrategy 接口,并在 Builder 中通过 builder.endStrategy(yourStrategy) 注入即可。
Q2:能否支持网络 Socket 多行输入?
A2:可在 Builder 中新增 fromSocket(Socket socket) 方法,将 socket.getInputStream() 包装为 InputStreamReader,其余逻辑复用。
Q3:readAll 会不会内存溢出?
A3:对超大输入,readAll 可能占用大量内存,建议使用 readWithCallback 分行处理或指定 maxLines/maxBytes 限制。
Q4:Builder 线程安全吗?
A4:Builder 仅供构造时使用,非线程安全;构建完成的 MultiLineReader 可在单线程或并发场景下安全调用,但建议每个线程各自构建实例。
Q5:如何在回调中处理异常?
A5:readWithCallback 会将回调异常封装为 MultiLineReadException 抛出,上层可在调用处捕获并获取根异常进行处理。
9. 扩展方向与性能优化
-
异步非阻塞 I/O
-
基于 NIO
AsynchronousChannel或 Netty 实现高性能异步多行读取应用。
-
-
增加超时中断
-
在阻塞读取时,通过另起守护线程监控超时,达到超时中断读取。
-
-
大文件分片读取
-
对超大文件按块读取,结合异步回调处理,提高吞吐并避免内存峰值。
-
-
压缩与加密支持
-
在读取前后增加解压或解密装饰器,为读取压缩文件或加密传输数据提供支持。
-
-
UI 交互增强
-
在控制台或 Swing/JavaFX 界面中实时显示读取进度、行数统计及日志。
-
-
结果序列化
-
提供读取结果直接序列化为 JSON、XML、对象集合等,方便后续业务处理。
-
-
流式处理
-
将
List<String>返回改为Stream<String>,结合 Stream API 实现懒加载与延迟计算。
-
1435

被折叠的 条评论
为什么被折叠?



