1. Java基础输入输出方法解析
作为Java开发者,输入输出操作是我们日常开发中最基础也最频繁使用的功能之一。虽然看似简单,但其中包含了许多值得深入探讨的细节和技巧。让我们从最基础的System.out开始,逐步深入Java的I/O世界。
1.1 标准输出:System.out的三剑客
Java的标准输出主要通过System类提供的out对象实现,其中最常用的三个方法是:
System.out.print("Hello"); // 不换行输出 System.out.println("World"); // 输出后换行 System.out.printf("PI=%.2f", Math.PI); // 格式化输出println方法在内部实际上是print加换行符的组合。在Java 8之前,println的实现会同步锁住System.out对象,这在多线程环境下可能成为性能瓶颈。Java 8对此进行了优化,现在println内部使用了更高效的缓冲机制。
提示:在需要大量输出日志的场景下,使用StringBuilder构建完整字符串后一次性输出,比多次调用print/println性能更好。
1.2 格式化输出的艺术
printf方法提供了强大的格式化功能,其核心是格式说明符:
System.out.printf("%-10s %5d %8.2f%n", "Item", 123, 45.678); // 输出:Item 123 45.68常用格式说明符包括:
%s:字符串%d:十进制整数%f:浮点数%n:平台无关的换行符%t:日期时间
格式说明符还可以包含各种标志:
-:左对齐0:数字前补零,:使用千位分隔符+:显示正负号
对于更复杂的格式化需求,可以使用java.util.Formatter类:
Formatter formatter = new Formatter(); formatter.format("%,.2f", 10000.0/3.0); System.out.println(formatter); // 输出:3,333.331.3 Scanner输入类的深度使用
Scanner是Java 5引入的实用工具类,大大简化了输入操作。其核心用法是:
Scanner scanner = new Scanner(System.in); System.out.print("请输入姓名:"); String name = scanner.nextLine(); System.out.print("请输入年龄:"); int age = scanner.nextInt();Scanner使用正则表达式在后台进行分词处理,默认使用空白字符作为分隔符。我们可以通过useDelimiter方法自定义分隔符:
Scanner scanner = new Scanner("one,two,three").useDelimiter(","); while(scanner.hasNext()) { System.out.println(scanner.next()); }Scanner提供了丰富的方法来读取不同类型的数据:
- nextLine():读取整行
- nextInt()/nextDouble():读取数字
- next():读取下一个标记
- hasNextXxx():检查是否有指定类型的输入
注意:nextInt()等方法不会消耗行尾的换行符,如果后面跟着nextLine(),可能会读取到空行。解决方法是在nextInt()后调用一次nextLine()消耗换行符。
2. 文件输入输出操作
2.1 传统的FileInputStream/FileOutputStream
最基本的文件IO操作是通过字节流实现的:
// 文件复制示例 try (InputStream in = new FileInputStream("source.txt"); OutputStream out = new FileOutputStream("target.txt")) { byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } }Java 7引入的try-with-resources语法可以自动关闭资源,比传统的try-catch-finally更简洁安全。
2.2 字符流Reader/Writer
对于文本文件,使用字符流效率更高:
// 文本文件读写 try (BufferedReader reader = new BufferedReader(new FileReader("input.txt")); BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) { String line; while ((line = reader.readLine()) != null) { writer.write(line); writer.newLine(); // 写入平台相关的换行符 } }BufferedReader的readLine()方法可以高效读取文本行,而BufferedWriter提供了newLine()方法来写入平台相关的换行符。
2.3 NIO文件操作
Java 7引入了NIO.2 API,提供了更现代的文件操作方式:
Path source = Paths.get("source.txt"); Path target = Paths.get("target.txt"); Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); // 读取所有行 List<String> lines = Files.readAllLines(source, StandardCharsets.UTF_8); // 写入所有行 Files.write(target, lines, StandardCharsets.UTF_8);Files类提供了丰富的静态方法,可以简化常见文件操作:
- readAllBytes()/readAllLines():读取全部内容
- write():写入内容
- createDirectories():创建目录
- walkFileTree():遍历目录树
3. 高级IO技巧与性能优化
3.1 缓冲的重要性
未缓冲的IO操作性能通常很差:
// 无缓冲的复制(性能差) try (InputStream in = new FileInputStream("large.bin"); OutputStream out = new FileOutputStream("copy.bin")) { int b; while ((b = in.read()) != -1) { // 每次只读一个字节 out.write(b); } }添加缓冲后性能可提升数十倍:
// 带缓冲的复制 try (InputStream in = new BufferedInputStream(new FileInputStream("large.bin")); OutputStream out = new BufferedOutputStream(new FileOutputStream("copy.bin"))) { byte[] buffer = new byte[8192]; // 8KB缓冲区 int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } }3.2 内存映射文件
对于超大文件,内存映射(MappedByteBuffer)可以提供最佳性能:
try (RandomAccessFile file = new RandomAccessFile("huge.bin", "rw")) { FileChannel channel = file.getChannel(); MappedByteBuffer buffer = channel.map( FileChannel.MapMode.READ_WRITE, 0, channel.size()); // 直接操作buffer,就像操作内存数组一样 for (int i = 0; i < buffer.limit(); i++) { byte b = buffer.get(i); buffer.put(i, (byte)(b * 2)); } }内存映射文件直接将文件映射到虚拟内存空间,避免了用户空间和内核空间之间的数据拷贝。
3.3 使用Files类的快捷方法
Java 7+的Files类提供了许多便利方法:
// 读取小文件的最简单方式 byte[] bytes = Files.readAllBytes(Paths.get("small.bin")); List<String> lines = Files.readAllLines(Paths.get("text.txt")); // 使用Stream处理大文件 try (Stream<String> stream = Files.lines(Paths.get("large.txt"))) { stream.filter(line -> !line.isEmpty()) .forEach(System.out::println); } // 创建临时文件 Path tempFile = Files.createTempFile("prefix", ".suffix"); // 创建目录(包括不存在的父目录) Files.createDirectories(Paths.get("path/to/new/dir"));4. 常见问题与最佳实践
4.1 字符编码问题
文本IO中最常见的问题是字符编码处理不当:
// 错误的编码处理(平台默认编码) try (Reader reader = new FileReader("text.txt")) { // 可能因编码问题导致乱码 } // 正确的做法:明确指定编码 try (Reader reader = new InputStreamReader( new FileInputStream("text.txt"), StandardCharsets.UTF_8)) { // 正确处理UTF-8编码文本 }最佳实践是:
- 总是明确指定字符编码(推荐UTF-8)
- 避免使用FileReader/FileWriter,因为它们使用平台默认编码
- 使用InputStreamReader/OutputStreamWriter并明确指定编码
4.2 资源泄漏防护
IO操作必须确保资源被正确关闭:
// 传统方式(冗长易错) InputStream in = null; try { in = new FileInputStream("file.txt"); // 使用in } catch (IOException e) { // 处理异常 } finally { if (in != null) { try { in.close(); } catch (IOException e) { // 关闭异常处理 } } } // 现代方式(推荐) try (InputStream in = new FileInputStream("file.txt")) { // 使用in } catch (IOException e) { // 处理异常 }4.3 性能调优技巧
- 缓冲区大小选择:通常8KB是一个不错的起点,但应根据实际情况测试调整
- 批量操作:尽量批量读写数据,减少IO次数
- 使用NIO:对于高并发IO,考虑使用NIO的非阻塞模式
- 减少系统调用:如检查文件是否存在后再操作,这种模式存在竞态条件,应该直接尝试操作并处理异常
4.4 跨平台注意事项
- 路径分隔符:使用
File.separator或Paths.get()代替硬编码的/或\ - 换行符:使用
System.lineSeparator()或BufferedWriter.newLine() - 文件系统差异:注意Windows和Unix-like系统在文件锁定、符号链接等方面的差异
在实际项目中,我经常遇到的一个坑是文件路径问题。特别是在跨平台部署时,硬编码的路径分隔符常常导致问题。现在我养成了使用Paths.get()和相对路径的习惯:
// 不好的做法 File file = new File("src/main/resources/config.properties"); // 更好的做法 Path configPath = Paths.get("src", "main", "resources", "config.properties");另一个实用技巧是在处理用户输入路径时进行规范化:
Path userPath = Paths.get(userInput).normalize(); if (!userPath.startsWith(BASE_DIR)) { throw new SecurityException("禁止访问基目录之外的文件"); }这样可以防止路径遍历攻击(如用户输入"../../../etc/passwd")。