Java 黑马程序员学习笔记(进阶篇24) - 实践

一、综合练习 1

1. 递归复制目录及其内容

请编写一个 Java 程序,实现以下功能:

(1) 复制指定源目录(包含所有子目录和文件)到目标目录,保持原目录结构不变。

(2) 具体要求:

① 源目录路径为 D:\\aaa\\src,目标目录路径为 D:\\aaa\\dest

② 若目标目录不存在,需自动创建(包括所有必要的父目录)。

③ 遍历源目录下的所有元素:

  • 若元素是文件,使用字节输入流(FileInputStream)和字节输出流(FileOutputStream)复制该文件到目标目录下(文件名与源文件相同);
  • 若元素是子目录,递归调用复制方法,将子目录及其内容复制到目标目录下(子目录名与源子目录相同)。

④ 文件复制时,使用长度为 1024 的字节数组作为缓冲区,通过循环批量读取和写入(read(byte[]) 和 write(byte[], int, int) 方法)。

⑤ 复制完成后,关闭所有流资源。

(3) 程序需声明抛出 IOException(无需捕获,直接通过 throws 声明)。

(4) 类名定义为 test3,包名为 demo1;复制方法定义为 private static void copydir(File src, File dest) throws IOException

package demo1;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class test3 {
    public static void main(String[] args) throws IOException {
        File src = new File("D:\\aaa\\src");
        File dest = new File("D:\\aaa\\dest");
        copydir(src,dest);
    }
    private static void copydir(File src, File dest) throws IOException {
        dest.mkdirs();
        File[] files = src.listFiles();
        for (File file : files) {
            if(file.isFile()) {
                FileInputStream fis = new FileInputStream(file);
                FileOutputStream fos = new FileOutputStream(new File(dest,file.getName()));  // 不太理解
                byte[] bytes = new byte[1024];
                int len;
                while((len = fis.read(bytes)) != -1) {
                    fos.write(bytes,0,len);
                }
                fos.close();
                fis.close();
            } else {
                copydir(file,new File(dest,file.getName()));
            }
        }
    }
}
关键逻辑:new File(dest, file.getName())

代码场景:当遍历到源目录下的文件file.isFile() == true)时,需要将该文件复制到目标目录。

① 拆解:
  • file:当前正在处理的源文件(比如 D:\aaa\src\a.txt);
  • file.getName():获取源文件的 “文件名”(比如 a.txt);
  • dest:目标目录(D:\aaa\dest);
  • new File(dest, file.getName()):创建一个 “目标文件” 对象,路径是 目标目录 + 源文件名(比如 D:\aaa\dest\a.txt)。
② 作用:

确保复制后的文件存放在目标目录下,且文件名与源文件相同,避免文件名冲突或路径错误。

2. 使用字节流对文件内容进行异或运算转换

请编写一个 Java 程序,实现以下功能:

(1) 使用字节输入流(FileInputStream)读取当前项目路径下的 a.txt 文件内容(逐个字节读取)。

(2) 对读取到的每个字节执行异或(^)运算(与整数 2 进行异或)。

(3) 使用字节输出流(FileOutputStream)将运算后的字节写入当前项目路径下的 b.txt 文件。

(4) 循环读取和写入,直到文件末尾(read() 方法返回 -1 时终止循环)。

(5) 操作完成后,关闭输入流资源。

(6) 程序需声明抛出 IOException(无需捕获,直接通过 throws 声明)。

要求:

  • 源文件为 a.txt,目标文件为 b.txt(均使用相对路径,位于项目根目录)。
  • 必须使用 while 循环结合 read() 方法逐个字节读取,使用 write(int b) 方法写入运算后的字节。
package demo1;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class test4 {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("a.txt");
        FileOutputStream fos = new FileOutputStream("b.txt");
        int b;
        while ((b = fis.read()) != -1) {
            fos.write(b ^ 2);  // 不太理解
        }
        fos.close();
        fis.close();
    }
}
关键逻辑:为什么这叫 “加密”?核心是 “可逆性”

① 加密的关键是 “能加密也能解密”,而异或运算有个特殊性质:对同一个数异或两次,结果会还原成原来的数,即 (b ^ 密钥) ^ 密钥 = b

② 用上面的例子验证:

  • 加密后的值是 9997 ^ 2);
  • 解密时再用 99 ^ 2
  01100011 (99,加密后)
^ 00000010 (2,密钥)
--------------------
  01100001 (97,'a')→ 还原成原始值

③ 这意味着:

  • 用你的代码处理 a.txt 得到 b.txt(加密,明文→密文);
  • 再用同样的代码(把 a.txt 换成 b.txtb.txt 换成 c.txt)处理一次,c.txt 会和 a.txt 完全一样(解密,密文→明文)。

3. 读取文件中数字并排序后写回

请编写一个 Java 程序,实现以下功能:

(1) 使用字符输入流(FileReader)读取当前项目路径下 d.txt 文件的内容(文件中内容为用 - 分隔的整数,例如 3-1-5-2)。

(2) 将读取到的内容拼接为字符串,按 - 分割为字符串数组,再转换为整数并存储到 ArrayList<Integer> 集合中。

(3) 使用 Collections.sort() 方法对集合中的整数进行升序排序。

(4) 使用字符输出流(FileWriter)将排序后的整数写回 d.txt 文件,格式要求:

  • 整数之间用 - 分隔;
  • 最后一个整数后面不添加 -(例如排序后结果应为 1-2-3-5)。

(5) 操作完成后,关闭输入流和输出流资源。

(6) 程序需声明抛出 IOException(无需捕获,直接通过 throws 声明)。

要求:

  • 源文件和目标文件均为 d.txt(使用相对路径,位于项目根目录)。
  • 必须使用 StringBuilder 拼接读取的字符内容。
package demo1;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
public class test6 {
    public static void main(String[] args) throws IOException {
        FileReader fr = new FileReader("d.txt");
        StringBuilder sb = new StringBuilder();
        int ch;
        while((ch = fr.read()) != -1) {
            sb.append((char)ch);
        }
        fr.close();
        System.out.println(sb);
        String str = sb.toString();
        String[] arrStr = str.split("-");
        ArrayList list = new ArrayList();
        for (String s : arrStr) {
            int i = Integer.parseInt(s);
            list.add(i);
        }
        Collections.sort(list);
        FileWriter fw = new FileWriter("d.txt");
        for (int i = 0; i < list.size(); i++) {
            if(i == list.size() - 1) {
                fw.write(list.get(i) + "");  // 不太理解
            } else {
                fw.write(list.get(i) + "-");
            }
        }
        fw.close();
    }
}
关键逻辑:fw.write(list.get(i) + "") 的作用
+ "" 的作用:将整数转为字符串

① FileWriter 的 write 方法有两个常用重载:

  • write(int c):写入的是整数 c 对应的ASCII 字符(如 write(97) 会写入 'a');
  • write(String str):写入字符串(如 write("5") 会写入字符 '5')。

② 代码中需要写入的是 “数字的字符形式”(如整数 5 要写成字符 '5'),而不是其 ASCII 对应的字符(5 的 ASCII 对应不可见字符,不是我们要的)。

③ 因此,list.get(i) + "" 是通过 “整数 + 空字符串” 的方式,将整数自动转换为字符串(如 5 + "" 结果是 "5"),这样 write 方法会调用 write(String str) 重载,正确写入数字字符。

二、缓冲流

1. 缓冲字节流:BufferedInputStream/BufferedOutputStream

普通字节流(FileInputStream/FileOutputStream)每次读写都直接操作磁盘,效率低。缓冲字节流内部维护一个 8KB 的缓冲区(字节数组),通过 “批量读写缓冲区” 减少磁盘 IO 次数,大幅提高效率。

(1) 核心特点
  • 构造方法:需传入 “普通字节流” 作为参数(装饰者模式,增强普通字节流的功能);
  • 缓冲区:默认 8KB,也可手动指定大小(如new BufferedInputStream(fis, 1024*16) → 16KB 缓冲区);
  • 方法:与普通字节流完全一致(read()write()close()),无需学习新方法。
(2) 案例:用缓冲字节流复制大文件(效率对比)
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class BufferedStreamDemo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        try (// 1. 创建普通字节流
             FileInputStream fis = new FileInputStream("D:\\large.mp4"); // 大视频文件
             FileOutputStream fos = new FileOutputStream("D:\\copy_large.mp4");
             // 2. 用缓冲流包装普通流(增强效率)
             BufferedInputStream bis = new BufferedInputStream(fis);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            byte[] buffer = new byte[1024 * 8];
            int readLen;
            while ((readLen = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, readLen);
            }
            // 缓冲流的flush():若需立即写入,可手动调用(close()会自动刷新)
            bos.flush();
            System.out.println("缓冲流复制完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("缓冲流复制耗时:" + (end - start) + "毫秒");
    }
}
① 效率对比
  • 普通字节流复制 1GB 视频:约 10-20 秒;
  • 缓冲字节流复制 1GB 视频:约 1-3 秒(效率提升 10 倍以上)。
② 注意

缓冲流关闭时,只需关闭外层的缓冲流(bis.close()bos.close()),内层的普通流会被自动关闭(try-with-resources 也会自动处理)。

2. 缓冲字符流:BufferedReader/BufferedWriter

与缓冲字节流类似,缓冲字符流内部有缓冲区(默认 8KB 字符数组),提高文本读写效率,且提供特有方法(普通字符流没有)。

(1) BufferedReader(缓冲字符输入流)
  • 特有方法:String readLine() → 读取一整行文本(不包含换行符\n),到文件末尾返回null
  • 构造方法:new BufferedReader(new FileReader("D:\\test.txt"))
(2) BufferedWriter(缓冲字符输出流)
  • 特有方法:void newLine() → 写入跨平台换行符(Windows 是\r\n,Linux 是\n,Mac 是\r),避免手动写\r\n不跨平台;
  • 构造方法:new BufferedWriter(new FileWriter("D:\\test.txt"))
(3) 案例:用缓冲字符流读写文本文件(按行读写)
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedCharStreamDemo {
    public static void main(String[] args) {
        // 需求:读取D:\\source.txt的内容,按行写入D:\\target.txt(追加模式)
        try (// 1. 创建缓冲字符输入流(读源文件)
             BufferedReader br = new BufferedReader(new FileReader("D:\\source.txt"));
             // 2. 创建缓冲字符输出流(写目标文件,追加模式)
             BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\target.txt", true))) {
            String line; // 存储每行读取的内容
            // 按行读取:readLine()返回null表示文件末尾
            while ((line = br.readLine()) != null) {
                bw.write(line); // 写入该行内容
                bw.newLine(); // 写入跨平台换行符(避免手动写"\r\n")
            }
            bw.flush(); // 字符流必须flush
            System.out.println("按行读写完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

核心优势

  • readLine():比普通字符流的read(char[])更方便,无需处理缓冲数组的截取;
  • newLine():实现跨平台换行,代码在 Windows 和 Linux 上都能正常运行。

三、综合练习 2

1. 文件内容按前缀数字排序                                                                                 

现有一个文本文件 f.txt,文件中每行内容的格式为「数字。任意字符」(例如:3.test1.demo5.example)。请编写一个 Java 程序,实现以下功能:

(1) 读取 f.txt 中的所有内容(每行作为一个独立的字符串);

(2) 按照每行中「.」前面的数字进行升序排序(例如上述示例应排序为 1.demo3.test5.example);

(3) 将排序后的内容写入到新文件 h.txt 中,要求每行内容与原文件保持一致(仅调整顺序),且每行末尾需保留换行。

注意:使用缓冲流(BufferedReaderBufferedWriter)进行文件读写,以提高效率。

方法一:
package demo3;
import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class test4 {
    public static void main(String[] args) throws IOException {
        // 1. 使用BufferedReader读取f.txt内容
        BufferedReader br = new BufferedReader(new FileReader("f.txt"));
        String line;
        ArrayList list = new ArrayList<>();
        // 循环读取每行内容,直到文件末尾(line为null)
        while((line = br.readLine()) != null) {
            list.add(line); // 将每行内容存入集合
        }
        br.close(); // 关闭输入流
        // 2. 按「.」前面的数字升序排序
        Collections.sort(list, new Comparator() {
            @Override
            public int compare(String o1, String o2) {
                // 分割字符串,获取「.」前面的部分并转为整数
                int i1 = Integer.parseInt(o1.split("\\.")[0]);
                int i2 = Integer.parseInt(o2.split("\\.")[0]);
                // 按整数升序排序(i1 - i2为负数则o1在前,正数则o2在前)
                return i1 - i2;
            }
        });
        // 3. 使用BufferedWriter写入h.txt
        BufferedWriter bw = new BufferedWriter(new FileWriter("h.txt"));
        for (String s : list) {
            bw.write(s); // 写入每行内容
            bw.newLine(); // 写入换行符(适配不同系统)
        }
        bw.close(); // 关闭输出流
    }
}
关键逻辑:ArrayList 是怎么排序的?

在 test4 中,我们用的是ArrayList<String>存储数据,它本身是 “无序集合”,所以需要通过Collections.sort(list, new Comparator(...))手动传入一个比较器,告诉程序 “按照什么规则排序”(比如按.前面的数字升序)。

方法二:
package demo3;
import java.io.*;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public class test5 {
    public static void main(String[] args) throws IOException {
        // 1. 使用BufferedReader读取f.txt内容
        BufferedReader br = new BufferedReader(new FileReader("f.txt"));
        String line;
        // TreeMap会自动按key(整数)升序排序
        TreeMap tm = new TreeMap<>();
        // 循环读取每行,处理后存入TreeMap
        while ((line = br.readLine()) != null) {
            // 按「.」分割每行内容,得到数组([0]为前缀数字,[1]为具体内容)
            String[] arr = line.split("\\.");
            // 前缀数字转为Integer作为key,具体内容作为value存入TreeMap
            tm.put(Integer.parseInt(arr[0]), arr[1]);
        }
        br.close(); // 关闭输入流
        // 2. 使用BufferedWriter将排序后的具体内容写入i.txt
        BufferedWriter bw = new BufferedWriter(new FileWriter("i.txt"));
        // 获取TreeMap中所有键值对
        Set> entries = tm.entrySet();
        // 遍历键值对,写入value(具体内容)
        for (Map.Entry entry : entries) {
            String value = entry.getValue();
            bw.write(value);
            bw.newLine(); // 写入换行符
        }
        bw.close(); // 关闭输出流
    }
}
关键逻辑:tm.put(Integer.parseInt(arr[0]), arr[1]);
解析 tm.put(key, value)

tm 是 TreeMap<Integer, String> 类型的集合,它的特点是:会自动根据 key 的自然顺序(这里是整数的升序)对键值对进行排序,无需手动调用排序方法。

tm.put(Integer.parseInt(arr[0]), arr[1]) 的作用是:

  • 把转换后的整数(arr[0] 转成的 int)作为 key
  • 把「.」后面的具体内容(arr[1])作为 value
  • 将这组键值对存入 TreeMap 中。

2. 软件使用次数限制

现有一个文本文件 count.txt,文件中仅包含一个整数(表示软件已被使用的次数,初始可能为 0)。请编写一个 Java 程序,实现以下功能:

(1) 读取 count.txt 中的整数,作为当前已使用次数;

(2) 将已使用次数加 1,得到本次使用的次数;

(3) 根据本次使用次数输出提示信息:

  • 若次数 ≤ 3:输出 “欢迎使用本软件,第 X 次使用免费”(X 为本次次数);
  • 若次数 > 3:输出 “本软件只能免费使用 3 次,欢迎您注册会员后免费使用”;

(4) 将更新后的次数(即本次使用次数)写入 count.txt 中,覆盖原有内容。

要求:使用缓冲流(BufferedReaderBufferedWriter)进行文件的读写操作。

package demo3;
import java.io.*;
public class test6 {
    public static void main(String[] args) throws IOException {
        // 读取count.txt中的已使用次数
        BufferedReader br = new BufferedReader(new FileReader("count.txt"));
        String line = br.readLine(); // 读取文件中的唯一一行内容(整数的字符串形式)
        br.close(); // 关闭输入流
        // 将读取到的字符串转为整数,计算本次使用次数
        int count = Integer.parseInt(line);
        count++;
        // 根据本次次数输出提示信息
        if(count <= 3) {
            System.out.println("欢迎使用本软件,第" + count + "次使用免费");
        } else {
            System.out.println("本软件只能免费使用3次,欢迎您注册会员后免费使用");
        }
        // 将更新后的次数写回count.txt
        BufferedWriter bw = new BufferedWriter(new FileWriter("count.txt"));
        bw.write(count + ""); // 将整数转为字符串写入
        bw.close(); // 关闭输出流
    }
}
关键逻辑:String line = br.readLine();

String line = br.readLine(); 的作用就是:把文件中记录的 “已使用次数”(字符串形式)读取出来,存到 line 变量中,为后续转换为整数、计算本次使用次数做准备。

posted on 2025-12-01 18:52  ljbguanli  阅读(2)  评论(0)    收藏  举报