一.什么是组合模式
组合模式将对象组合成树形结构以表示"部分-整体"的层次结构。它使得用户对单个对象和组合对象的使用具有一致性,增强了代码的灵活性和可维护性。这种模式特别适用于需要表示层次结构的数据模型,如文件系统、组织结构图等。
二.类图
部分:left,不可分割,原子性
整体:composite,由部分组成
存放它们的容器:component。包含部分和整体
component可以调用left和compsite实例。

三.实例
参考图解设计模式这本书
这里用到了向上转型这种方式。向上转型在这里的作用是:将子类对象(File/Directory)当作父类类型(Entry)来统一处理,从而让容器可以混合存储不同子类型,并在遍历时以统一的父类视角操作它们。类图如图3-1所示。

图3-1
这里模拟创建文件,打印出所有文件的路径以及对应的文件存储大小。例如图3-2这样的文件结构

图3-2
打印出的效果如图3-3所示

图3-3
具体实现代码如下:
1.Entry.java
java
package composite;
import java.util.*;
public abstract class Entry {
public abstract String getName();
public abstract int getSize();
public Entry add(Entry entry) throws FileTreatmentException{
throw new FileTreatmentException();
}
public void printList() {
printList("root");
}
protected abstract void printList(String prefix);
public String toString() {
return getName()+"("+getSize()+")";
}
}
这里的printList有两个,这就是重载。重载的定义就是在同一个类中,方法名相同,参数列表不同(类型、个数、顺序),与返回类型无关(仅改变返回类型不能 构成重载,编译器会报错)。重载的出现,体现了Java的编译时多态(也叫静态多态、早绑定)。下面的Directory类解说里面会提到具体的用法。
2.Directory.java
java
package composite;
import java.util.Iterator;
import java.util.ArrayList;
public class Directory extends Entry{
private String name;
private ArrayList directory=new ArrayList();
public Directory(String name) {
this.name=name;
}
@Override
public String getName() {//获取名字
return name;
}
@Override
public int getSize() {//获取大小
int size=0;
Iterator it=directory.iterator();
while(it.hasNext()) {
Entry entry=(Entry)it.next();
size+=entry.getSize();
}
return size;
}
@Override
protected void printList(String prefix) {//显示条目目录一览prefix前缀
System.out.println(prefix+"/"+this);
Iterator it=directory.iterator();
while(it.hasNext()) {
Entry entry=(Entry)it.next();
entry.printList(prefix+"/"+name);
}
}
public Entry add(Entry entry) {//增加条目
directory.add(entry);
return this;
}
}
在getSize方法里面创建了一个迭代器it,用来遍历文件夹下面的每一个文件/文件夹,计算存储大小之和。在printList方法里面用了同样的方式遍历,打印出文件的路径,这里的 printList(String) 方法重写了父类 Entry 中同名的有参方法(参数列表必须完全相同)。方法内首先打印当前节点的路径:prefix + "/" + this,其中 this 是调用该方法的对象引用。若未重写 toString(),默认打印 类名@哈希码;但这里继承了 Entry 的 toString() 方法,以 对象名+(大小) 的格式输出。然后通过 Iterator 遍历 directory 集合中的每个子节点(Entry 类型)。对每个子节点递归调用 printList(prefix + "/" + name),其中 name 是当前节点(this)的名称,用于拼接下一级路径。由于 Entry 中的 printList(String) 是抽象方法(或有默认实现),子类 Directory 必须(或可以)重写该方法。递归调用时会根据子节点的实际类型(File 或 Directory)多态地执行对应的 printList 逻辑,最终打印出完整的目录树结构。
重写强烈建议加 @override,存在于继承关系的子类中,方法名必须相同,参数列表也必须完全相同。返回类型 :必须相同,或是原返回类型的子类 (协变返回类型)。重写的出现体现了运行时多态(也叫动态多态、晚绑定)。
3.File.java
java
package composite;
public class File extends Entry{
private String name;
private int size;
public File(String name,int size) {
this.name=name;
this.size=size;
}
public String getName() {
return name;
}
public int getSize() {
return size;
}
protected void printList(String prefix) {
System.out.println(prefix+"/"+this);
}
}
4.FileTreatmentException.java
java
public class FileTreatmentException extends RuntimeException {
// 无参构造器:什么都不做,只是调用父类无参构造器
public FileTreatmentException() {
super(); // 隐式存在,调用 RuntimeException()
}
// 有参构造器:接收消息,传给父类存储
public FileTreatmentException(String msg) {
super(msg); // 调用 RuntimeException(String msg)
}
}
这个 FileTreatmentException 是一个运行时异常 ,在这里,当你对叶子节点(File)执行了只允许容器节点(Directory)执行的操作时抛出。

抛出异常测试代码
java
FileTreatmentException e = new FileTreatmentException("文件操作非法");
// 这些都是从父类继承来的方法,你没有写任何代码
System.out.println(e.getMessage()); // 输出:文件操作非法
e.printStackTrace(); // 打印完整堆栈
System.out.println(e.toString()); // 输出:composite.FileTreatmentException: 文件操作非法
// 查看继承的方法
Method[] methods = e.getClass().getMethods();
for (Method m : methods) {
System.out.println(m.getName());
}
// 会看到 getMessage, printStackTrace, getCause... 等20+个方法
这里的e.getClass就是反射的应用。
getClass()是java.lang.Object类中的方法,它返回一个Class<?>对象 ,这个对象包含了该实例的运行时类的元数据信息。这属于Java反射机制 的核心组成部分。反射就是在运行时动态地获取类的信息(如方法、字段、构造器)以及动态调用对象的能力。getClass()就是反射的入口方法之一 。这里的getMethods()→ 反射获取该类的所有public方法(包括从父类继承的)
5.Main.java
java
package composite;
public class Main{
public static void main(String []args) {
try {
Entry f1=new File("f1",10);
Directory d1=new Directory("d1");
Directory d2=new Directory("d2");
d1.add(d2);
File f2=new File("f2",20);
d2.add(f2);
f1.printList();
d1.printList();
System.out.println("**************************\n");
d1.printList("");
}
catch(FileTreatmentException e){
e.printStackTrace();
}
}
}
这里打印有两种方式,printList()和printList(string)这两种方式
如果你使用无参函数,调用流程如下:
java
// 第1步:你调用
d1.printList(); // 无参
// 第2步:进入 Entry 的无参方法
public void printList() {
printList(" "); // 内部主动调用有参版本,传入空格
}
// 第3步:进入有参方法,开始递归
protected void printList(String prefix) {
System.out.println(prefix + "/" + this);
// ... 递归调用子节点的有参方法
}
要是你使用带参数的函数,就会少了上面代码块的的第一步。
为什么父类要设计两个方法?
这是经典的模板方法模式:
java// 对外公共API(用户友好,不需要传参) public void printList() { printList(""); // 提供默认的初始前缀 } // 内部递归实现(需要参数,但用户不需要知道) protected void printList(String prefix) { // 递归逻辑 }
运行结果如下图所示:

四.习题

习题11-1:
树形结构,有递归的地方都适用组合模式,例如个体-班级-学校,页面组件-页面-页面容器。
习题11-12:
目前上面的printList只是查看节点之后是否还有文件或文件夹,如果有就打印出来。所以起点就是该节点,该节点之前的文件不清楚,我们要打印文件的路径,需要知道该节点之前的文件。思路是在每次加一个文件夹的时候,前缀就变成该节点名称+"/",那怎么实现呢?具体是添加一个parent父节点作为零时存储节点,每次add的时候就把"前缀+该节点的文件名"作为新的前缀存储进parent节点的name里面。也就是parent的文件名就是前缀。
实现代码如下:
1.Entry.java
首先加一个entry对象parent,再编写一个getFullName方法,在getFullName方法里面,定义一个StringBuffer对象,将调用getFullName方法的节点存储在entry里面,通过循环向前寻找父节点,不断增加前缀,直到前面没有节点位置。此时就是完整的路径,最后通过fullname.tostring方法返回字符串。
java
package composite;
import java.util.*;
public abstract class Entry {
public abstract String getName();
public abstract int getSize();
public Entry parent;
public Entry add(Entry entry) throws FileTreatmentException{
throw new FileTreatmentException();
}
public void printList() {
printList("root");
}
protected abstract void printList(String prefix);
public String toString() {
return getName()+"("+getSize()+")";
}
public String getFullName() {
StringBuffer fullname=new StringBuffer();
Entry entry=this;
do {
fullname.insert(0, "/"+entry.getName());
entry=entry.parent;
}while(entry!=null);
return fullname.toString();
}
}
2.Directory.java
文件夹才可以add文件或文件夹,因此,add方法写在文件夹类里面,但是为了表示文件不能使用add方法,所以在entry里面添加add方法用来抛出异常,这样用户在使用文件add或是entry来add的时候会抛出异常,就方便理解。在add的时候就要把该节点存储在parent里面,这样每次add的时候,parent的文件名(即前缀)就会改变。
java
package composite;
import java.util.Iterator;
import java.util.ArrayList;
public class Directory extends Entry{
private String name;
private ArrayList directory=new ArrayList();
public Directory(String name) {
this.name=name;
}
@Override
public String getName() {//获取名字
return name;
}
@Override
public int getSize() {//获取大小
int size=0;
Iterator it=directory.iterator();
while(it.hasNext()) {
Entry entry=(Entry)it.next();
size+=entry.getSize();
}
return size;
}
@Override
protected void printList(String prefix) {//显示条目目录一览prefix前缀
System.out.println(prefix+"/"+this);
Iterator it=directory.iterator();
while(it.hasNext()) {
Entry entry=(Entry)it.next();
entry.printList(prefix+"/"+name);
}
}
public Entry add(Entry entry) {//增加条目
directory.add(entry);
entry.parent=this;
return this;
}
}
我实现了打印一个文件夹下的所有文件功能getFiles函数。仿照printList写的,供大家了解一下。
1.entry.java
java
package composite;
import java.util.*;
public abstract class Entry {
public abstract String getName();
public abstract int getSize();
public Entry add(Entry entry) throws FileTreatmentException{
throw new FileTreatmentException();
}
public void printList() {
printList("root");
}
protected abstract void printList(String prefix);
protected abstract String getFiles(String path);
public void getFiles() {
String path="";
path=getFiles(path);//如果一个无返回值函数(void)调用一个递归有返回值函数,必须要有变量接收返回值,否则返回值会丢失。
printPath(path);
}
public void printPath(String path) {
System.out.println(path);
}
public String toString() {
return getName()+"("+getSize()+")";
}
}
2.Directory.java
java
package composite;
import java.util.Iterator;
import java.util.ArrayList;
public class Directory extends Entry{
private String name;
private ArrayList directory=new ArrayList();
public Directory(String name) {
this.name=name;
}
@Override
public String getName() {//获取名字
return name;
}
@Override
public int getSize() {//获取大小
int size=0;
Iterator it=directory.iterator();
while(it.hasNext()) {
Entry entry=(Entry)it.next();
size+=entry.getSize();
}
return size;
}
@Override
protected void printList(String prefix) {//显示条目目录一览prefix前缀
System.out.println(prefix+"/"+this);
Iterator it=directory.iterator();
while(it.hasNext()) {
Entry entry=(Entry)it.next();
entry.printList(prefix+"/"+name);
}
}
public Entry add(Entry entry) {//增加条目
directory.add(entry);
return this;
}
@Override
protected String getFiles(String path) {
// TODO Auto-generated method stub
Iterator it=directory.iterator();
String currentPath=path+"/"+this;
while(it.hasNext()) {
Entry entry=(Entry)it.next();
currentPath=entry.getFiles(currentPath);
}
return currentPath;
}
}
3.File.java
java
package composite;
public class File extends Entry{
private String name;
private int size;
public File(String name,int size) {
this.name=name;
this.size=size;
}
public String getName() {
return name;
}
public int getSize() {
return size;
}
protected void printList(String prefix) {
System.out.println(prefix+"/"+this);
}
@Override
protected String getFiles(String path) {
// TODO Auto-generated method stub
return path+"/"+this;
}
}
4.Main.java
java
package composite;
public class Main{
public static void main(String []args) {
try {
Entry f1=new File("f1",10);
Directory d1=new Directory("d1");
Directory d2=new Directory("d2");
d1.add(d2);
File f2=new File("f2",20);
File f3=new File("f3",30);
d2.add(f2);
d2.add(f3);
d2.getFiles();
f1.getFiles();
}
catch(FileTreatmentException e){
e.printStackTrace();
}
}
}