← 返回题目列表

如何手写一个组合模式?Java 代码怎么实现?

高频 中等 第 3 / 25 题 更新于 2026/07/28
组合模式Java代码实现文件系统树形结构

简化版

手写组合模式可以先定义统一接口 Component,再实现叶子类 File 和组合类 Directory。组合类内部维护 List<Component>,对外实现同样的方法,内部递归调用子节点。

详细版

以文件系统为例:

interface FileNode {
    long size();
    void print(String indent);
}

文件是叶子:

class FileLeaf implements FileNode {
    public long size() { return size; }
}

目录是组合:

class Directory implements FileNode {
    private List<FileNode> children = new ArrayList<>();

    public long size() {
        return children.stream().mapToLong(FileNode::size).sum();
    }
}

客户端调用:

root.size();
root.print("");

不需要关心 root 下面有多少层目录和文件。

完整版教学

一、定义公共接口

interface FileNode {
    String name();
    long size();
    void print(String indent);
}

这里定义的是文件和目录都具备的能力。

文件有名称和大小,目录也有名称和总大小。

二、实现叶子节点 File

class FileLeaf implements FileNode {
    private final String name;
    private final long size;

    FileLeaf(String name, long size) {
        this.name = name;
        this.size = size;
    }

    public String name() {
        return name;
    }

    public long size() {
        return size;
    }

    public void print(String indent) {
        System.out.println(indent + "- " + name + " (" + size + ")");
    }
}

叶子节点没有子节点,所以 size() 直接返回自身大小。

三、实现组合节点 Directory

class Directory implements FileNode {
    private final String name;
    private final List<FileNode> children = new ArrayList<>();

    Directory(String name) {
        this.name = name;
    }

    void add(FileNode node) {
        children.add(node);
    }

    public String name() {
        return name;
    }

    public long size() {
        long total = 0;
        for (FileNode child : children) {
            total += child.size();
        }
        return total;
    }

    public void print(String indent) {
        System.out.println(indent + "+ " + name);
        for (FileNode child : children) {
            child.print(indent + "  ");
        }
    }
}

目录的大小来自子节点大小之和。

如果子节点也是目录,它会继续递归计算。

四、客户端统一使用

Directory root = new Directory("root");
root.add(new FileLeaf("a.txt", 10));

Directory images = new Directory("images");
images.add(new FileLeaf("logo.png", 200));
root.add(images);

root.print("");
System.out.println(root.size());

客户端统一操作 FileNode,不需要写一堆类型判断。

五、工程里要注意访问控制

上面为了演示简单,把 add() 放在 Directory 上。

真实项目中,要考虑:

  • 是否允许外部随便添加子节点;
  • 是否需要删除子节点;
  • 是否需要父节点引用;
  • 是否防止形成环;
  • 是否需要缓存计算结果;
  • 是否线程安全。

组合模式只是结构,树的维护规则仍然要设计清楚。

六、常见误区与追问

实现验收要同时检查统一操作和结构约束。构造1个目录并加入2个文件后,调用 size() 应递归得到两者之和;尝试把目录自身加入子节点必须被拒绝,否则遍历会无限循环。子节点集合还应封装为只读视图,避免客户端绕过校验直接修改。

检查维度判定依据
Component声明叶子与组合共有操作
Composite维护子节点并递归聚合结果

易错点:统一业务操作不代表必须把 add/remove 也塞进所有叶子。

  • 误区:Component 必须是抽象类。 接口或抽象类都可以,取决于是否需要共享状态和默认实现。
  • 追问:子节点集合为什么不能直接暴露? 外部修改会绕过环检测、父指针和权限等结构不变量。
  • 误区:递归方法只要能跑就正确。 还要处理环、深度、异常节点和大树性能。
  • 追问:父指针是否必须? 不是;需要向上导航或快速移除时才增加,同时要维护双向一致性。
  • 追问:如何测试新增节点类型? 把它作为 Component 放进现有树,验证客户端和 Composite 无需类型分支。

七、加强记忆

手写组合模式抓住三个点:公共接口表示统一能力,叶子节点直接执行,组合节点保存 List<Component> 并递归调用子节点。文件系统是最经典例子:文件是叶子,目录是组合。