27、Java 文件读写操作全解析

Java 文件读写操作全解析

1. 数据表示与流的概念

在计算机中,有多种方式来表示数据。通过创建对象,我们已经接触过一种方式,对象包含以变量形式存在的数据以及对其他对象的引用,还包含利用这些数据完成任务的方法。

为了处理其他类型的数据,比如硬盘上的文件和 Web 服务器上的文档,我们可以使用 java.io 包中的类。该包名中的 “IO” 代表 “输入/输出”,这些类用于访问各种数据源,如硬盘、CD - ROM 或计算机内存。

我们可以通过一种称为流(streams)的通信系统将数据引入程序或从程序中输出。流是一种将信息从一个地方传输到另一个地方的对象。

在接下来的操作中,我们将使用流来完成以下任务:
- 从文件中读取字节到程序中
- 在计算机上创建新文件
- 将字节数组保存到文件中
- 修改文件中存储的数据

流可以分为两种类型:
| 流类型 | 描述 |
| ---- | ---- |
| 输入流(Input streams) | 从数据源读取数据 |
| 输出流(Output streams) | 向数据源写入数据 |

所有的输入和输出流都是由字节组成的,字节是取值范围在 0 到 255 之间的单个整数。我们可以使用这种格式来表示各种数据,如可执行程序、文字处理文档和 MP3 音乐文件等。Java 类文件以字节码(bytecode)的形式存储为字节,Java 解释器可以运行字节码,这些字节码不一定由 Java 语言生成,也可以是其他语言(如 NetRexx 和 JPython)编译生成的。

除了字节流,还有一种更专门的数据处理方式是以字符形式,即单个字母、数字、标点符号等。当读写文本文件、文字处理文档、网页等时,可以使用字符流。

无论处理的是字节流、字符流还是其他类型的信息,总体过程都是相似的:
1. 创建与数据关联的流对象。
2. 调用流的方法将信息放入流中或从流中取出信息。
3. 调用对象的 close() 方法关闭流。

2. Java 中的文件表示

在 Java 中,文件由 File 类表示,该类也是 java.io 包的一部分。文件可以从硬盘、软盘、CD - ROM 等存储设备中读取。

File 对象可以表示已存在的文件和想要创建的文件。创建 File 对象时,可以使用文件名作为构造函数的参数,例如:

File bookName = new File("address.dat");

这将在当前文件夹中为名为 address.dat 的文件创建一个对象。也可以在文件名中包含路径:

File bookName = new File("data\\address.dat");

在 Windows 系统中,路径和文件名使用反斜杠( \ )作为分隔符,而 Linux 和其他基于 Unix 的系统使用正斜杠( / )。为了编写一个能在不同操作系统上都能正确引用文件的 Java 程序,可以使用 File.pathSeparator 类变量,例如:

File bookName = new File("data" + File.pathSeparator + "address.dat");

创建 File 对象后,可以调用一些有用的方法:
| 方法 | 描述 |
| ---- | ---- |
| exists() | 如果文件存在返回 true ,否则返回 false |
| getName() | 以字符串形式返回文件的名称 |
| length() | 以长整型值返回文件的大小 |
| createNewFile() | 如果文件不存在,则创建同名文件 |
| delete() | 如果文件存在,则删除该文件 |
| renameTo(File) | 使用指定的 File 对象的名称重命名文件 |

此外, File 对象还可以表示系统中的文件夹。可以在 File 构造函数中指定文件夹名称,该名称可以是绝对路径(如 “C:\MyDocuments\”)或相对路径(如 “java\database”)。创建表示文件夹的对象后,可以调用其 listFiles() 方法查看文件夹内的内容,该方法返回一个 File 对象数组,代表文件夹中包含的每个文件和子文件夹。

3. 从流中读取数据

第一个任务是使用输入流从文件中读取数据。我们可以使用 FileInputStream 类,它表示从文件中以字节形式读取的输入流。

创建文件输入流时,可以将文件名或 File 对象作为 FileInputStream() 构造函数的参数。在创建文件输入流之前,文件必须存在,否则在尝试创建流时会抛出 IOException 。为了处理这个异常,通常将涉及文件的所有语句放在一个 try - catch 块中,例如:

try {
    File cookie = new File("cookie.web");
    FileInputStream file = new FileInputStream(cookie);
    System.out.println("Length of file: " + cookie.length());
} catch (IOException e) {
    System.out.println("Could not read file.");
}

文件输入流以字节形式读取数据。我们可以通过调用流的 read() 方法(无参数)来读取单个字节。如果已经到达文件末尾,没有更多字节可用,则返回字节值 - 1。当读取输入流时,从流的第一个字节开始,例如文件的第一个字节。我们可以通过调用流的 skip() 方法并传入一个整数参数(表示要跳过的字节数)来跳过流中的一些字节,例如:

scanData.skip(1024);

如果想一次读取多个字节,可以按以下步骤操作:
1. 创建一个大小与要读取的字节数完全相同的字节数组。
2. 调用流的 read() 方法并传入该数组作为参数,数组将被填充从流中读取的字节。

下面是一个读取 MP3 音频文件中 ID3 数据的示例。由于 MP3 是一种流行的音乐文件格式,通常会在 ID3 文件末尾添加 128 个字节来保存歌曲的相关信息,如标题、艺术家和专辑等。

以下是 ReadID3.java 的完整代码:

import java.io.*;

public class ReadID3 {
    public static void main(String[] arguments) {
        try {
            File song = new File(arguments[0]);
            FileInputStream file = new FileInputStream(song);
            int size = (int)song.length();
            file.skip(size - 128);
            byte[] last128 = new byte[128];
            file.read(last128);
            String id3 = new String(last128);
            String tag = id3.substring(0, 3);
            if (tag.equals("TAG")) {
                System.out.println("Title: " + id3.substring(3, 32));
                System.out.println("Artist: " + id3.substring(33, 62));
                System.out.println("Album: " + id3.substring(63, 91));
                System.out.println("Year: " + id3.substring(93, 97));
            } else
                System.out.println(arguments[0] + " does not contain" + " ID3 info.");
            file.close();
        } catch (Exception e) {
            System.out.println("Error — " + e.toString());
        }
    }
}

编译这个源文件后,会创建一个 ReadID3 类文件。可以将其作为应用程序运行,并在命令行中指定系统上的一个 MP3 文件作为参数,例如:

java ReadID3 Everlong.mp3

如果系统上有 Everlong.mp3 文件,运行该应用程序可能会输出如下信息:

Title: Everlong
Artist: Foo Fighters
Album: The Colour and the Shape
Year: 1997
4. 缓冲输入流

提高读取输入流程序性能的一种方法是对输入进行缓冲。缓冲是将数据保存在内存中,以便程序需要时使用的过程。当 Java 程序需要从缓冲输入流中获取数据时,会先查看缓冲区,这比直接从文件等数据源读取要快。

要使用缓冲输入流,首先创建一个输入流(如 FileInputStream 对象),然后使用该对象创建缓冲流。调用 BufferedInputStream(InputStream) 构造函数,将输入流作为唯一参数。数据在从输入流读取时会被缓冲。

从缓冲流中读取数据时,调用其 read() 方法(无参数),会返回一个 0 到 255 之间的整数,表示流中的下一个字节数据。如果没有更多字节可用,则返回 - 1。

下面是一个使用缓冲流实现控制台输入的示例。 System 类中有一个类变量 in ,它表示一个 InputStream 对象,该对象从键盘接收输入并将其作为流提供。

以下是 ReadConsole.java 的完整代码:

import java.io.*;

public class ReadConsole {
    public static String readLine() {
        StringBuffer response = new StringBuffer();
        try {
            BufferedInputStream bin = new BufferedInputStream(System.in);
            int in = 0;
            char inChar;
            do {
                in = bin.read();
                inChar = (char) in;
                if (in != -1) {
                    response.append(inChar);
                }
            } while ((in != -1) & (inChar != '\n'));
            bin.close();
            return response.toString();
        } catch (IOException e) {
            System.out.println("Exception: " + e.getMessage());
            return null;
        }
    }

    public static void main(String[] arguments) {
        System.out.print("You are standing at the end of the road ");
        System.out.print("before a small brick building. Around you ");
        System.out.print("is a forest. A small stream flows out of ");
        System.out.println("the building and down a gully.\n");
        System.out.print("> ");
        String input = ReadConsole.readLine();
        System.out.println("That’s not a verb I recognize.");
    }
}

编译并运行该应用程序时,输出可能如下:

You are standing at the end of the road before a small brick building.
Around you is a forest. A small stream flows out of the building and
down a gully.
> go north
That’s not a verb I recognize.

ReadConsole 类包含一个类方法 readLine() ,用于从控制台接收字符。当按下回车键时, readLine() 会返回一个包含所有接收到字符的 String 对象。如果将 ReadConsole 类保存在 CLASSPATH 环境变量列出的文件夹中,就可以在编写的任何 Java 程序中调用 ReadConsole.readLine() 方法。

5. 向流中写入数据

java.io 包中,处理流的类是成对出现的。例如,有用于处理字节流的 FileInputStream FileOutputStream 类,用于处理字符流的 FileReader FileWriter 类等。

要开始从字节流中读取数据,首先要创建一个与输出流关联的 File 对象,这个文件不一定需要在系统中存在。

创建 FileOutputStream 有两种方式:
- 如果要将字节追加到现有文件中,调用 FileOutputStream() 构造函数,传入两个参数:表示文件的 File 对象和布尔值 true 。写入流的字节将被添加到文件末尾。
- 如果要将字节写入新文件,调用 FileOutputStream() 构造函数,将 File 对象作为唯一参数。

创建输出流后,可以调用不同的 write() 方法向流中写入字节:
- 调用 write() 方法,传入一个字节作为唯一参数,将该字节写入流中。
- 调用 write() 方法,传入一个字节数组作为唯一参数,将数组中的所有字节写入流中。
- 调用 write() 方法,传入三个参数:一个字节数组、表示要写入流的数组的第一个元素的整数以及要写入的字节数。

以下是一个创建包含 10 个字节的字节数组并将最后 5 个字节写入输出流的示例:

File dat = new File("data.dat");
FileOutputStream datStream = new FileOutputStream(dat);
byte[] data = { 5, 12, 4, 13, 3, 15, 2, 17, 1, 18 };
datStream.write(data, 5, 5);

完成向流中写入字节后,调用流的 close() 方法关闭流。

6. 向 MP3 文件写入字节的示例

下面是一个将 MP3 文件中的部分字节替换为修改后数据的示例。

以下是 TrashMP3.java 的完整代码:

import java.io.*;

public class TrashMP3 {
    static int INTERFERENCE = 500;
    static int RANDOMBYTE = 5;

    public static void main(String[] arguments) {
        try {
            File song = new File(arguments[0]);
            FileInputStream file = new FileInputStream(song);
            File trashedSong = new File("trashed.mp3");
            FileOutputStream trash = new FileOutputStream(trashedSong);
            boolean eof = false;
            int count = 0;
            System.out.println("Creating file ...");
            while (!eof) {
                int input = file.read();
                if (input == -1)
                    eof = true;
                else {
                    count++;
                    if (count % INTERFERENCE == 0) {
                        int newInput = input + RANDOMBYTE;
                        if (newInput > 255)
                            newInput = newInput - 255;
                        trash.write(newInput);
                    } else
                        trash.write(input);
                }
            }
            file.close();
            trash.close();
            System.out.println("Done");
        } catch (Exception e) {
            System.out.println("Error — " + e.toString());
        }
    }
}

编译该文件后,运行应用程序时需要指定一个 MP3 文件作为命令行参数,例如:

java TrashMP3 MuskratLove.mp3

该程序会读取 MP3 文件中的所有数据,并将其中一小部分字节替换为修改后的数据,最终生成一个新的 MP3 文件。

通过以上内容,我们详细介绍了 Java 中文件读写的相关操作,包括流的概念、文件的表示、数据的读写以及缓冲流的使用等。希望这些内容能帮助你更好地处理 Java 中的文件操作。

Java 文件读写操作全解析

7. 操作流程总结

为了更清晰地理解 Java 文件读写操作,我们可以将整个过程总结为以下流程图:

graph TD;
    A[开始] --> B[创建流对象];
    B --> C{流类型};
    C -->|输入流| D[读取数据];
    C -->|输出流| E[写入数据];
    D --> F[处理数据];
    E --> G[完成写入];
    F --> H[关闭流];
    G --> H;
    H --> I[结束];

这个流程图展示了 Java 文件读写的基本操作流程。首先创建流对象,根据流的类型进行读取或写入操作,处理读取的数据或完成写入后,关闭流以释放资源。

8. 不同场景下的应用

在实际开发中,Java 文件读写操作有多种应用场景,以下是一些常见的场景及对应的操作示例。

8.1 配置文件读取

在很多 Java 应用程序中,我们需要读取配置文件来获取应用程序的配置信息。例如,我们可以创建一个配置文件 config.properties ,内容如下:

database.url=jdbc:mysql://localhost:3306/mydb
database.username=root
database.password=123456

以下是读取该配置文件的 Java 代码:

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class ConfigReader {
    public static void main(String[] args) {
        Properties properties = new Properties();
        try (FileInputStream fis = new FileInputStream("config.properties")) {
            properties.load(fis);
            String url = properties.getProperty("database.url");
            String username = properties.getProperty("database.username");
            String password = properties.getProperty("database.password");
            System.out.println("Database URL: " + url);
            System.out.println("Username: " + username);
            System.out.println("Password: " + password);
        } catch (IOException e) {
            System.out.println("Error reading config file: " + e.getMessage());
        }
    }
}

在这个示例中,我们使用 Properties 类来读取配置文件,通过 FileInputStream 打开文件并加载到 Properties 对象中,然后可以根据键获取相应的值。

8.2 日志文件写入

在 Java 应用程序中,记录日志是非常重要的。我们可以将日志信息写入日志文件中。以下是一个简单的日志写入示例:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.time.LocalDateTime;

public class Logger {
    public static void log(String message) {
        try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream("app.log", true)))) {
            String timestamp = LocalDateTime.now().toString();
            writer.println(timestamp + " - " + message);
        } catch (IOException e) {
            System.out.println("Error writing log: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        log("Application started");
        log("Performing some operation");
        log("Application ended");
    }
}

在这个示例中,我们使用 PrintWriter 将日志信息追加到 app.log 文件中,同时记录了日志的时间戳。

9. 异常处理与资源管理

在进行 Java 文件读写操作时,异常处理和资源管理是非常重要的。如前面提到的,很多文件操作方法会抛出 IOException ,我们需要捕获并处理这些异常。同时,为了避免资源泄漏,我们应该确保在使用完流后关闭它们。

在 Java 7 及以后的版本中,我们可以使用 try-with-resources 语句来自动关闭实现了 AutoCloseable 接口的资源。例如,前面的 ConfigReader 示例中就使用了 try-with-resources 语句:

try (FileInputStream fis = new FileInputStream("config.properties")) {
    properties.load(fis);
    // 处理数据
} catch (IOException e) {
    System.out.println("Error reading config file: " + e.getMessage());
}

在这个语句中, FileInputStream 对象会在 try 块执行完毕后自动关闭,无需手动调用 close() 方法。

10. 性能优化建议

为了提高 Java 文件读写操作的性能,我们可以采取以下一些建议:
- 使用缓冲流 :如前面介绍的缓冲输入流和缓冲输出流,它们可以减少与数据源的交互次数,提高读写效率。
- 批量读写 :尽量一次读取或写入多个字节或字符,而不是一个一个地操作。例如,使用 read(byte[] b) 方法读取多个字节。
- 选择合适的流类型 :根据数据的类型选择合适的流,如处理文本数据时使用字符流,处理二进制数据时使用字节流。

11. 总结与展望

通过本文的介绍,我们全面了解了 Java 文件读写操作的相关知识,包括流的概念、文件的表示、不同类型流的使用、异常处理和性能优化等方面。这些知识对于开发 Java 应用程序时处理文件操作非常重要。

在未来的 Java 开发中,随着技术的不断发展,文件读写操作可能会有更多的优化和改进。例如,可能会出现更高效的流处理方式,或者对不同文件格式的支持更加完善。我们需要不断学习和关注这些变化,以提高我们的开发能力和应用程序的性能。

希望本文能帮助你更好地掌握 Java 文件读写操作,在实际开发中能够灵活运用这些知识解决各种文件处理问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值