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。
② 用上面的例子验证:
- 加密后的值是
99(97 ^ 2); - 解密时再用
99 ^ 2:
01100011 (99,加密后)
^ 00000010 (2,密钥)
--------------------
01100001 (97,'a')→ 还原成原始值
③ 这意味着:
- 用你的代码处理
a.txt得到b.txt(加密,明文→密文); - 再用同样的代码(把
a.txt换成b.txt,b.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.test、1.demo、5.example)。请编写一个 Java 程序,实现以下功能:
(1) 读取 f.txt 中的所有内容(每行作为一个独立的字符串);
(2) 按照每行中「.」前面的数字进行升序排序(例如上述示例应排序为 1.demo、3.test、5.example);
(3) 将排序后的内容写入到新文件 h.txt 中,要求每行内容与原文件保持一致(仅调整顺序),且每行末尾需保留换行。
注意:使用缓冲流(BufferedReader、BufferedWriter)进行文件读写,以提高效率。
方法一:
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 中,覆盖原有内容。
要求:使用缓冲流(BufferedReader、BufferedWriter)进行文件的读写操作。
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 变量中,为后续转换为整数、计算本次使用次数做准备。
浙公网安备 33010602011771号