組合模式:將對象組合成樹形結構以表示「部分-整體」層次結構,使客戶端對單個對象和組合對象的使用具有一致性 (統一接口)。核心價值在於遞歸操作的統一抽象,讓客戶端無需區分葉子節點與容器節點。
| 角色 | 職責 | 例子 |
|---|---|---|
| Component (抽象構件) | 定義統一接口,聲明增刪改查子節點、操作方法 | FileSystemNode 接口 |
| Leaf (葉子構件) | 無子節點,實現具體業務邏輯 | File 文件 |
| Composite (容器構件) | 包含子節點列表,實現子節點管理,操作委託遞歸子節點 | Directory 目錄 |
| Client (客戶端) | 通過 Component 接口操作樹結構 | 文件掃描器、備份工具 |
// 1. 抽象構件
interface FileSystemNode {
String getName();
long getSize();
void printTree(String indent);
}
// 2. 葉子構件:文件
class FileNode implements FileSystemNode {
private final String name;
private final long size;
public FileNode(String name, long size) {
this.name = name;
this.size = size;
}
@Override public String getName() { return name; }
@Override public long getSize() { return size; }
@Override
public void printTree(String indent) {
System.out.println(indent + "📄 " + name + " (" + formatSize(size) + ")");
}
}
// 3. 容器構件:目錄
class DirectoryNode implements FileSystemNode {
private final String name;
private final List children = new ArrayList<>();
public DirectoryNode(String name) { this.name = name; }
public void add(FileSystemNode node) { children.add(node); }
public void remove(FileSystemNode node) { children.remove(node); }
public List getChildren() { return Collections.unmodifiableList(children); }
@Override public String getName() { return name; }
@Override
public long getSize() {
return children.stream().mapToLong(FileSystemNode::getSize).sum();
}
@Override
public void printTree(String indent) {
System.out.println(indent + "📁 " + name + " (" + formatSize(getSize()) + ")");
for (FileSystemNode child : children) {
child.printTree(indent + " ");
}
}
}
// 4. 工具方法
private static String formatSize(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
return String.format("%.1f MB", bytes / (1024.0 * 1024));
}
// 5. 客戶端使用
public class FileScanner {
public static void main(String[] args) {
// 構建樹
DirectoryNode root = new DirectoryNode("項目根目錄");
DirectoryNode src = new DirectoryNode("src");
DirectoryNode main = new DirectoryNode("main");
DirectoryNode java = new DirectoryNode("java");
java.add(new FileNode("Main.java", 2048));
java.add(new FileNode("UserService.java", 5120));
main.add(java);
src.add(main);
src.add(new FileNode("README.md", 1024));
root.add(src);
DirectoryNode test = new DirectoryNode("test");
test.add(new FileNode("UserServiceTest.java", 3072));
root.add(test);
// 統一操作:打印樹、計算總大小
root.printTree("");
System.out.println("總大小: " + formatSize(root.getSize()));
}
}
📁 項目根目錄 (11.3 KB)
📁 src (8.2 KB)
📁 main (7.0 KB)
📁 java (7.0 KB)
📄 Main.java (2.0 KB)
📄 UserService.java (5.0 KB)
📄 README.md (1.0 KB)
📁 test (3.0 KB)
📄 UserServiceTest.java (3.0 KB)
總大小: 11.3 KB
// 為節點添加 accept 方法
interface FileSystemNode {
void accept(FileVisitor visitor);
// ... 其他方法
}
interface FileVisitor {
void visit(FileNode file);
void visit(DirectoryNode dir);
}
// 具體訪問器:查找大文件
class LargeFileFinder implements FileVisitor {
private final long threshold;
private final List result = new ArrayList<>();
public LargeFileFinder(long thresholdKB) { this.threshold = thresholdKB * 1024; }
@Override public void visit(FileNode file) {
if (file.getSize() > threshold) result.add(file);
}
@Override public void visit(DirectoryNode dir) {
for (FileSystemNode child : dir.getChildren()) child.accept(this);
}
public List getResult() { return result; }
}
// 使用
LargeFileFinder finder = new LargeFileFinder(4); // 4KB
root.accept(finder);
finder.getResult().forEach(f -> System.out.println("大文件: " + f.getName()));
| API | Component | Leaf | Composite |
|---|---|---|---|
| AWT/Swing | Component | Button, Label, TextField | Container, Panel, JFrame |
| XML DOM | Node | Text, Comment, Attr | Element, Document |
| NIO.2 | Path | 文件 Path | 目錄 Path (Files.walkFileTree) |
ParentNodeStackOverflowError,深層樹需改用迭代或尾遞歸優化// 使用 Stream API 替代顯式遞歸
public long calculateTotalSize() {
return StreamSupport.stream(
Spliterators.spliteratorUnknownSize(iterator(), Spliterator.ORDERED), false)
.mapToLong(FileSystemNode::getSize).sum();
}
// 深度優先迭代器 (避免棧溢出)
public Iterator iterator() {
return new Iterator<>() {
private final Deque> stack = new ArrayDeque<>();
{ stack.push(Collections.singletonList(this).iterator()); }
@Override public boolean hasNext() {
while (!stack.isEmpty()) {
Iterator it = stack.peek();
if (it.hasNext()) return true;
stack.pop();
}
return false;
}
@Override public FileSystemNode next() {
Iterator it = stack.peek();
FileSystemNode node = it.next();
if (node instanceof DirectoryNode dir) {
stack.push(dir.getChildren().iterator());
}
return node;
}
};
}
組合模式通過統一接口將樹形結構的遞歸操作封裝,讓客戶端代碼極其簡潔。核心在於 Composite 內部持有 List<Component> 並在操作方法中遞歸委託子節點。現代 Java 可結合 Stream API、迭代器模式、訪問者模式 進一步增強靈活性與性能。適用於一切「部分-整體」層次結構場景。