java 流、文件、IO

每日一言

All of life’s journeys come with meetings, partings, and reunions. – Meryl Stryfe
from Trigun

控制台input

BufferedReader & System.in

System.in 是java中的一个静态成员变量,属于 java.lang.System 类,它是一个 InputStream,绑定到键盘,直接使用它进行读取有许多不方便之处:

  • 它只能以字节为单位读取数据。
  • 读取数据需要手动处理字节到字符的转换,以及处理不同数据类型(例如整数、浮点数等)。
  • 中文字符通常占用多个字节,如果一次读取的字节数不足以包含一个完整的中文字符,则需要多次读取并将结果拼接起来。
  • 需要手动处理异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.IOException;

public class PrintSystemIn {
public static void main(String[] args) {
try {
System.out.println("Enter a character:");
int byteRead = System.in.read();
if (byteRead != -1) {
System.out.println("You entered: " + (char) byteRead);
}
} catch (IOException e) {
System.err.println("Error reading input: " + e.getMessage());
}
}
}

1737644314039

所以,直接通过System.in来进行程序输入是极为不方便的。

所以我们通常将System.in包装在一个InputStreamReader类中,将读取的字节流转换为字符流。在java中,char类型占用两个字节的空间,对于文本数据,我们可以通过InputStreamReader类来将原始的字节流合并为字符流,将两个字节的内容合并的一个char类型数据。

再将整体包装在一个BuffereredReader中,实现缓冲读取字节流转化为字符流的过程。

需要引用以下头文件:

1
2
3
4
5
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

import java.io.*; //可以用这一个代替上面三个

创建方式如下:

1
2
BufferedReader br = new BufferedReader(new 
InputStreamReader(System.in));

BufferedReader 对象创建后,我们便可以使用 read() 方法从控制台读取一个字符,或者用 readLine() 方法读取一个字符串。

但是在使用BufferedReader进行读取之前,我们还需要了解一点东西。如,我们使用如下的方式进行输入时:

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class BfReader {
public static void main(String[] args) {
System.out.println("请输入一个字符:");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
char c = (char)br.read();
System.out.println("您输入的字符是:" +c);
}
}

编译器会进行如下报错:

1737644699176

Unhandled exception type IOException:未处理的 IOException 异常类型

BufferedReader.read()方法会抛出一个异常 IOException,因此我们需要使用 try-catch 来进行异常处理,这也就是我们之前引用的第三个头文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

public class BfReader {
public static void main(String[] args) {
System.out.println("请输入一个字符:");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
try {
char c = (char) br.read();
System.out.println("您输入的字符是:" + c);
} catch (IOException e) {
System.err.println("读取字符时发生错误: " + e.getMessage());
}
}
}

此时就能够正常的读取并打印中文了:

1737645023294

稍作修改就可以改为读取字符串:

注意,read()返回的是一个char类型,readline()返回的是一个String类型。

1
2
3
4
5
6
7
8
9
public class BfReader {
public static void main(String[] args) throws IOException {
System.out.println("请输入字符串:");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str = br.readLine();
System.out.println("您输入的字符是:" + str);
}
}

1737645305774

此处我们展示了第二种异常处理的方式:这种异常处理方式并不直接在main()的内部进行异常处理,而是将异常处理交给更高层,让代码看起来更加简洁,关于异常处理的详细内容,我们后续再来详细了解。

读取int、float、double类型数据

通过BufferedReader读取int、float、double类型需要借助三个java静态函数,分别是:

1
2
3
int intValue = Integer.parseInt(bf.readLine());
float floatValue = Float.parseFloat(bf.readLine());
double doubleValue = Double.parseDouble(bf.readLine());

这三个函数的参数传入参数都是String类型,所以一定要通过 readLine()方法进行读取,读取的字符串形式一定为一个标准的整数、小数。否则会触发 NumberFormatException的异常。

Xxx.parseXxx()函数会将读取的字符串形式的数据转化为对应的数据类型返回。

读取代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ReadInput {

public static void main(String[] args) {
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));

try {
System.out.println("请输入一个字符串:");
String text = bf.readLine();
System.out.println("你输入的字符串是: " + text);

System.out.println("请输入一个整数:");
int intValue = Integer.parseInt(bf.readLine());
System.out.println("你输入的整数是: " + intValue);

System.out.println("请输入一个浮点数:");
float floatValue = Float.parseFloat(bf.readLine());
System.out.println("你输入的浮点数是: " + floatValue);

System.out.println("请输入一个双精度浮点数:");
double doubleValue = Double.parseDouble(bf.readLine());
System.out.println("你输入的双精度浮点数是: " + doubleValue);

} catch (IOException e) {
System.err.println("读取输入时发生错误: " + e.getMessage());
} catch (NumberFormatException e) {
System.err.println("输入的数字格式不正确: " + e.getMessage());
}
}
}

这种读取方式只允许我们一行输入一个数据,比较繁琐。

1737809270237

Scanner类

java.util.Scanner 是 Java5 的新特征,我们可以通过 Scanner 类来获取用户的输入。

引头文件

1
import java.util.Scanner; 

创建方法:

1
Scanner s = new Scanner(System.in);

next()读取

在使用next读取前,我们通常使用hasNext()方法来检测是否还有内容未读取。

next()方法会以空格,回车等为分割符,一次只会读取一个单词,hasNext()方法会检测是否还有未读的单词,直到读入EOF返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.Scanner; 

public class ScannerDemo {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
// 从键盘接收数据

// next方式接收字符串
System.out.println("next方式接收:");
// 判断是否还有输入
while (scan.hasNext()) {
String str1 = scan.next();
System.out.println("输入的数据为:" + str1);
}
System.out.println("next方式接收结束");
scan.close();
}
}

1737646823318

可以通过按 Ctrl + Z 然后按 Enter 键来发送 EOF(End Of File)信号,从而结束输入。

nextLine()读取

在使用nextLine读取前,我们通常使用hasNextLine()方法来检测是否还有内容未读取。

nextLine()方法将以换行符为分割符,一次读取一行的内容。hasNextLine()方法会检测是否还有未读的行,直到读入EOF返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.Scanner; 

public class ScannerDemo {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
// 从键盘接收数据

// nextLine方式接收字符串
System.out.println("nextLine方式接收:");
// 判断是否还有输入
while (scan.hasNextLine()) {
String str1 = scan.nextLine();
System.out.println("输入的数据为:" + str1);
}
System.out.println("nextLine方式接收结束");
scan.close();
}
}

1737647091268

读取int、float、double类型数据

Scanner类同样对于int、float、double类型的数据提供了支持。但是在读取之前同样建议通过函数 hasNextXxx()进行验证,再通过nextXxx()来读取。

读取示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.util.Scanner;

public class ReadInput {

public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);

try {
System.out.println("请输入一个字符串:");
if (scanner.hasNextLine()) {
String text = scanner.nextLine();
System.out.println("你输入的字符串是: " + text);
}
else {
System.out.println("你没有输入任何字符串");
}

System.out.println("请输入一个整数、一个浮点数、一个双精度浮点数:");
if (scanner.hasNextInt()) {
int intValue = scanner.nextInt();
System.out.println("你输入的整数是: " + intValue);
}
else {
System.out.println("输入的不是整数!");
}

if(scanner.hasNextFloat()) {
float floatValue = scanner.nextFloat();
System.out.println("你输入的浮点数是: " + floatValue);
}
else {
System.out.println("输入的不是浮点数!");
}

if(scanner.hasNextDouble()) {
double doubleValue = scanner.nextDouble();
System.out.println("你输入的双精度浮点数是: " + doubleValue);
}
else {
System.out.println("输入的不是双精度浮点数!");
}

} catch (Exception e) {
System.err.println("读取输入时发生错误: " + e.getMessage());
} finally {
scanner.close();
}
}
}

1737809496043

这样我们就无需对于每一行的数据格式进行严格要求,只要通过空格将我们想要输入的数据隔开就好。

控制台output

java中的控制台输出,通过 System.out对象来实现。System.out java.io.PrintStream的一个指向控制台的实例。

我们这里先介绍三种标准的输出函数

System.out.print(data)

作用:将指定的数据输出到控制台, 不换行

1
2
3
4
5
6
7
8
9
public class PrintExample {
public static void main(String[] args) {
System.out.print("Hello, ");
System.out.print("World!");
System.out.print(123);
System.out.print(3.14);
}
}

运行结果:

1737816475456

System.out.println(data)

作用:将指定的数据输出到控制台,每次输出换行。

1
2
3
4
5
6
7
8
9
public class PrintlnExample {
public static void main(String[] args) {
System.out.println("Hello, ");
System.out.println("World!");
System.out.println(123);
System.out.println(3.14);
}
}

运行结果:

1737816601031

对于以上两函数的data可以是:

  • int,long,double等数字类型
  • char类型
  • String类型
  • boolean类型

也可以是:

  • 对象(类的实例)

第一种情况输出的内容就是变量的值。对于boolean类型输出的就是 truefalse

我们详细讲解一下当传入参数为对象时,程序会输出什么。

首先我们写这样一个简单的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PrintlnExample {
public static void main(String[] args) {
MyClass mc1 = new MyClass(10);
System.out.println(mc1);
}
}

class MyClass{
int a;
String str = "It's my class, a = " + a;
public MyClass(int x){
a = x;
}
}

我们创建了一个对象,并且用println打印它。

运行结果:

1737817212012

打印的结果为:类名+ @ + 对象的哈希码的十六进制表示

好,现在又出现了一个陌生的名词:“对象的哈希码的十六进制表示”。我们先暂时忽视这一概念的内部实现,只需知道该值是根据对象的内部状态计算出来的。

现在我们给MyClass类添加一个名为:toString()的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PrintlnExample {
public static void main(String[] args) {
MyClass mc1 = new MyClass(10);
System.out.println(mc1);
}
}

class MyClass{
int a;
String str = "It's my class, a = " + a;
public MyClass(int x){
a = x;
}
public String toString(){
return str;
}
}

运行结果:

1737817846400

打印结果变为了我们编写的toString()函数的返回值。

由此,我们可以总结打印对象的过程如下:

  1. 检查对象的toString()方法
    • System.out.println() 会首先检查传入的对象是否定义了 toString() 方法。
    • 如果对象所属的类重写了 toString() 方法,那么 System.out.println() 将调用这个重写后的 toString() 方法,并打印该方法返回的字符串。
    • 如果对象所属的类没有重写 toString() 方法,那么 System.out.println() 将调用该对象从 java.lang.Object 类继承来的默认 toString() 方法。
  2. 默认 toString() 方法:
  • java.lang.Object 类提供的默认 toString() 方法会返回一个字符串,该字符串包含以下信息:
    • 对象的类名(包括包名,如果存在)。
    • @ 符号。
    • 对象的哈希码的十六进制表示。
  • 例如,默认的输出格式可能类似于 com.example.MyClass@1a2b3c4d

System.err.println()

System.err.println 用于将文本输出到标准错误流(standard error stream)的方法,它与System.out.println的及其相似,但是输出目标不同。

该方法主要目的是将error信息与正常output的输出进行区分,当他们可以分别被重定向到不同位置。

1
2
3
4
5
try {

} catch (Exception e) {
System.err.println("错误信息");
}

对于文件的input和output

控制台的输入输出只涉及到字符流的输入输出,但是对于文件的输入输出,可能涉及字符流和字节流两种情况。

字符流的文件输入

字符流的文件输入我们依然可以使用控制台input中的两种方法,只需要将System.in改为对应的文件即可。

假设在如下路径下的文件

1
D:\windows\desktop\text.txt

用Scanner读入:

1
2
3
ring FilePath = "D:\windows\desktop\text.txt"
File file = new File(FliePath);
Scanner sc = new Scanner(file);

BufferedReader 读入:

1
BufferedReader bf = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)));

与Scanner直接将 System.in替换为 File 对象不同,BufferedReader需要将 System.in替换为 FileInputStream对象。

或者可以直接使用:

1
BufferedReader br = new BufferedReader(new java.io.FileReader(filePath))

差距对比:

特性 BufferedReader(new FileReader(filePath)) BufferedReader(new InputStreamReader(new FileInputStream(filePath)))
底层输入流 FileReader FileInputStream
字符编码处理 使用系统默认编码 需要显式指定编码
编码问题 可能出现乱码 可以通过指定编码避免乱码
代码复杂度 简单 稍复杂
性能 相对较低 相对较高
适用场景 读取文本文件,简单场景 读取文本文件,需要控制编码,性能要求较高场景

字符流的文件输出

字符流的文件输出我们可以通过对于System.out的重定向来实现。

引入头文件:

1
2
import java.io.FileOutputStream;
import java.io.PrintStream;
  1. 创建一个FileOutputStream类,指定输出文件

    1
    FileOutputStream fos = new FileOutputStream("Output.txt");
  2. 创建PrintStream,将FileOutputStream包裹

    1
    PrintStream ps = new PrintStream(fos);
  3. 使用System.setOut(),将字符流输出重定向到文件

    1
    System.setOut(ps);

之后就可以使用System.out.print等函数进行文件输出。

针对System.err的重定向则使用方法:System.setErr()

  1. 使用完毕后关闭文件流:

    1
    2
    ps.close
    fos.close
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;

public class RedirectSystemOutToFile {
public static void main(String[] args){
try {
// 1. 创建 FileOutputStream,指定输出文件
FileOutputStream fos = new FileOutputStream("output.txt");

// 2. 创建 PrintStream,将 FileOutputStream 包裹
PrintStream ps = new PrintStream(fos);

// 3. 使用 System.setOut() 重定向 System.out
System.setOut(ps);

System.out.println("成功定向到文件!! 恭喜你!!");

// 4. 关闭 PrintStream
ps.close();
fos.close();
} catch (FileNotFoundException e) {
System.err.println("Error creating output file: " + e.getMessage());
} catch (Exception e) {
System.err.println("Error closing stream: " + e.getMessage());
}
}
}

恢复控制台输出

如果在文件输出结束后,你想要恢复文件输出,则需要在更改为文件输出之前,保存原本的输出流

1
PrintStream originalOut = System.out; // 保存原始的 System.out

恢复:

1
System.setOut(originalOut); // 恢复 System.out 到控制台

将你从控制台输入的字符转存到文件保存起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.Scanner;
public class RecordYourInput {
public static void main(String[] args){
try {
File file = new File("input.txt");
Scanner sc = new Scanner(System.in);
PrintStream os = System.out;
FileOutputStream fos = new FileOutputStream(file);
PrintStream ps = new PrintStream(fos);
System.setOut(ps);
while(sc.hasNextLine()){
String input = sc.nextLine();
System.out.println(input);
if(input.equals("exit")){
break;
}
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
}

我们运行程序输入如下字符:

1738554376618

可以看到工作目录下生成input.txt,内容如下:

1738554425640

字节流的文件输入

对于很多文件,如图片、视频等文件,他们的内容不是以字符编码进行存放,而是以字节为单位进行读取,对于这些文件,我们就不能像读取 .txt文件一样通过字符流读取他们,而是需要通过字节流的方式进行读取。

字节流的input通过 FileInputStream类来实现,创建一个字节输入流:

1
2
File f = new File("C:/java/hello");
InputStream in = new FileInputStream(f);
方法 描述 示例代码
int read() 读取一个字节的数据,返回值为 0 到 255 之间的整数。如果到达流的末尾,返回 -1。 int data = in.read();
int read(byte[] b) 从输入流中读取字节,并将其存储在字节数组 b 中,返回实际读取的字节数。如果到达流的末尾,返回 -1。 byte[] buffer = new byte[1024]; int bytesRead = in.read(buffer);
int read(byte[] b, int off, int len) 从输入流中读取最多 len 个字节,并将它们存储在字节数组 boff 偏移位置,返回实际读取的字节数。如果到达流的末尾,返回 -1。 byte[] buffer = new byte[1024]; int bytesRead = in.read(buffer, 0, buffer.length);
long skip(long n) 跳过并丢弃输入流中的 n 个字节,返回实际跳过的字节数。 long skippedBytes = in.skip(100);
int available() 返回可以读取的字节数(不阻塞)。 int availableBytes = in.available();
void close() 关闭输入流并释放与该流相关的所有资源。 in.close();
void mark(int readlimit) 在流中的当前位置设置标记,readlimit 是可以读取的字节数上限。 in.mark(1024);
void reset() 将流重新定位到上次标记的位置,如果没有标记或标记失效,抛出 IOException in.reset();
boolean markSupported() 检查当前输入流是否支持 mark()reset() 操作。 boolean isMarkSupported = in.markSupported();

字节流的文件输出

字节流的文件输出通过 FileOutputStream类来实现,创建一个文件输出流:

1
2
File f = new File("C:/java/hello");
OutputStream fOut = new FileOutputStream(f);
方法 描述 示例代码
void write(int b) 将指定的字节写入输出流,b 的低 8 位将被写入流中。 fOut.write(255);
void write(byte[] b) 将字节数组 b 中的所有字节写入输出流。 byte[] data = "Hello".getBytes(); fOut.write(data);
void write(byte[] b, int off, int len) 将字节数组 b 中从偏移量 off 开始的 len 个字节写入输出流。 byte[] data = "Hello".getBytes(); fOut.write(data, 0, data.length);
void flush() 刷新输出流并强制写出所有缓冲的数据,确保数据被立即写入目标输出。 fOut.flush();
void close() 关闭输出流并释放与该流相关的所有资源。关闭后不能再写入。 fOut.close();

java程序更换win11壁纸

在本地路径下存有一张好看的图片:D:\OneDrive\图片\picture\19.png

1737898558036

我希望通过一个java程序来帮我将图片替换为我电脑的壁纸。

首先我们需要知道:

  • win11壁纸路径为:C:\Users\%username%\AppData\Roaming\Microsoft\Windows\Themes 目录下的 TranscodedWallpaper 文件,并无文件后缀。

所以我们只需要读取我们喜欢的图片的数据,再覆盖原本的壁纸文件即可。

这是我现在的桌面:

1737900730120

我们执行下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.*;

public class WallPaperChange {
public static void main(String[] args){
String sourcePath = "D:\\OneDrive\\图片\\picture\\19.png";
String targetPath = "C:\\Users\\XXY\\AppData\\Roaming\\Microsoft\\Windows\\Themes\\TranscodedWallpaper";
File sourceFile = new File(sourcePath);
File targetFile = new File(targetPath);
try (InputStream s = new FileInputStream(sourceFile);
OutputStream t = new FileOutputStream(targetFile)) {
byte[] b = new byte[s.available()];
s.read(b);
t.write(b);
System.out.println("Wallpaper changed successfully");
} catch (FileNotFoundException e) {
System.err.println("File not found: " + e.getMessage());
} catch (IOException e) {
System.err.println("An I/O error occurred: " + e.getMessage());
}
}
}

现在我们通过任务管理器将Windows资源管理器重新启动;

1737900995934

可以看到壁纸已经更换为我们想要的壁纸:

1737901053139


java 流、文件、IO
http://blog.ulna520.com/2025/01/23/java 流、文件、IO_20250123_220927/
Veröffentlicht am
January 23, 2025
Urheberrechtshinweis