1、目标:输出类似 Windows 的命令行工具中 tree /F 命令所得到的目录树:
2、语言、算法:
所使用的计算机语言:Java
涉及算法:递归算法
涉及的类库:java.io.File、java.util.ArrayList
3、思路分析:
自定义一个简单的链表类,作为存储目录树的结构;
写一个递归算法先把文件夹目录结构存储在目录树中;
再写一个目录树遍历方法,传入一个根节点,按照目录树的结构输出整个目录树;
分别使用单线程、多线程实现此功能;
在线程开始时和线程结束时获取时间戳,得到程序运行所花费的时间。
4、代码实现:
链表类及目录树的遍历方法:
定义一个 Node 节点类,其对象作为目录树中的节点,其中的 value 则存储当前目录所对应的 File 对象,nodes 则表示当前目录下所包含的子目录或文件;
定义一个静态的 layoutCount 作为输出目录树时的所到达的层级数;
代码:
import java.io.File;
import java.util.ArrayList;
public class Node {
public Node(File file) { // 传绝对路径
this.value = file;
nodes = new ArrayList<>();
}
File value;
ArrayList<Node> nodes;
static int layoutCount = 0;
public static void travelTree(Node node) {
for (int i = 0; i < layoutCount; i++) {
System.out.print(" ");
}
System.out.println(node.value.getName());
ArrayList<Node> files = node.nodes;
for (int i = 0; i < files.size(); i++) {
layoutCount++;
travelTree(files.get(i));
layoutCount--;
}
}
public String toString() {
return this.value.getName();
}
}
单线程下的目录树生成代码:
递归算法 travelDirectory 的思路:travelDirectory 方法需要一个 Node 参数,进入递归算法之后,先把参数 node 中的 File 对象引用赋给 file,如果 file 是一个文件,则将 file 添加进 node 中的动态数组中,作为node 目录之下的一个文件;如果 file 是一个文件夹,则调用递归算法自身遍历 file 所代表的的路径之下的文件列表,进入递归。
在 main 方法中,首先获取一下当前的时间戳;
定义一个 root 作为根目录的节点,调用 travelDirectory 方法递归遍历 path 目录;
最后调用 Node.travelTree(node); 方法输出目录树之后再获取当前时间戳,计算得到所花费的时间。
代码:
import java.io.File;
public class Tree {
static String path = "D:\\IDEA-WorkSpace";
public static void main(String[] args) {
long start = System.currentTimeMillis();
Node root = new Node(new File(path));
Tree.travelDirectory(root);
Node.layoutCount = 0;
Node.travelTree(root);
long end = System.currentTimeMillis();
System.out.println("时间为:" + (end - start));
}
public static void travelDirectory(Node node) {
File file = node.value;
if (file.isFile()) {
node.nodes.add(new Node(file));
}
if (file.isDirectory()) {
File[] files = file.listFiles();
for (int i = 0; i < files.length; i++) {
Node child = new Node(files[i]);
node.nodes.add(child);
if (child.value.isDirectory()) {
travelDirectory(child);
}
}
}
}
}
多线程下的目录树生成代码:
既然是多线程则写一个继承自 Thread 的类:TreeThread,此类代表遍历一个文件夹所经过的过程,类中 node 变量表示当前所进入到的目录,线程执行的时候,先判断当前目录是不是一个文件,如果是一个文件则退出线程,如果是一个文件夹,则获取文件夹里面所有的文件及子文件夹,并把文件及文件夹都添加进自身的 node.nodes,并创建线程继续遍历这些文件及文件夹。
在 main 方法中,先拿到方法开始执行时的时间戳,然后创建第一个线程开始遍历 file 所代表的路径,直接让线程开始运行即可,线程运行过程中会自动再创建线程运行当前路径之下的文件及文件夹遍历操作。为了避免多线程还没遍历完就已经执行输出方法导致输出错误,所以先等待 1000 毫秒之后才执行输出方法,再最后获取结束时间后再减多这等待所花费的 1000 毫秒即可得到多线程运行所花费的实际时间。
代码:
import java.io.File;
public class MulTree {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
File file = new File(Tree.path);
Node node = new Node(file);
// 使用多线程生成目录树
TreeThread thread = new TreeThread(node);
thread.start();
// 为了避免多线程的影响,所以等待1000毫秒之后再执行遍历目录树的代码
Thread.sleep(1000);
long end ;
// 输出目录树
Node.layoutCount = 0;
Node.travelTree(node);
end = System.currentTimeMillis();
// 输出的时间则减去 1000 毫秒
System.out.println("时间为:" + (end - start - 1000));
System.out.println("线程总数:" + TreeThread.count);
}
}
class TreeThread extends Thread {
static int count = 0;
Node node; // node 包含了当前要遍历的根目录
TreeThread(Node node) {
this.node = node;
count++;
}
@Override
public void run() {
if (this.node.value.isFile()) {
return;
}
File[] files = this.node.value.listFiles();
for (int i = 0; i < files.length; i++) {
Node newNode = new Node(files[i]);
this.node.nodes.add(newNode);
TreeThread thread = new TreeThread(newNode);
thread.start();
}
}
}
5、分析单线程和多线程程序的运行结果,可知两种遍历程序已初步完毕,接下来只需要提供一个更复杂的文件路径即可。
花费时间(ms) |
单线程 |
多线程 |
第一次 |
8560 |
296 |
第二次 |
640 |
209 |
第三次 |
619 |
212 |
第四次 |
653 |
291 |
第五次 |
684 |
269 |
第六次 |
675 |
268 |
需注意的是,如果提供了一个从未运行过的路径,第一次运行程序所花的实际较长,第二次运行所花的实际就已经大大降低了,本人未去研究这个问题,推测导致如此结果的原因可能是 Windows 或 JVM 自己会记录有缓存,使得下一次遍历目录时所花费的时间大大降低。
由于存在这样的情况,所以采取运行多次的办法,本文总共运行了6次,取第二到第五次的结果对比,由结果可知,多线程输出目录树的速度要更快些。
当遍历一个复杂的路径时,应该需要对比两种方式输出的目录树是否有差别,本文已自行对比过了,两种方式输出的目录树皆符合实际情况(cmd 工具中输出的目录树)。
6、改进:
本文所遍历的路径是 IDEA 的工作空间,文件数量、文件夹层次适中,当遍历更复杂的目录时可能会导致单线程、多线程所花费的时间相差不大,推测是由于频繁创建线程对象而导致开销过大,所以可以使用线程池来避免这个情况,另外线程池的数量也需要足够大,比如 1000 个或以上。