这是一个新的工程。我们创建一个接口叫 `PopulationNode`,它有一个方法 `computePopulation()`,表示可以计算某个区域的人口数量。
哪些对象可以具有“人口”这个概念?国家、城市、区、小区、甚至一栋楼,都可以是一个人口节点。我们用组合模式来统一处理这种树形结构。
我们先写一个 `City` 类,实现 `PopulationNode` 接口。`City` 有名字,并且包含多个“区”(`District`)。每个 `District` 也实现了 `PopulationNode`,表示一个行政区域,它也有名字和固定的人口数。
```java
public class District implements PopulationNode {
private final String name;
private final int population;
public District(String name, int population) {
this.name = name;
this.population = population;
}
@Override
public int computePopulation() {
return population;
}
}
```
`City` 持有一个 `List<District>`,并提供 `addDistrict(District)` 方法来添加区。它的 `computePopulation()` 实现为所有区的人口之和:
```java
public class City implements PopulationNode {
private final String name;
private final List<District> districts = new ArrayList<>();
public void addDistrict(District district) {
districts.add(district);
}
@Override
public int computePopulation() {
return districts.stream()
.mapToInt(PopulationNode::computePopulation)
.sum();
}
}
```
同理,我们再写一个 `Province`(省)类,它不直接管理区,而是管理多个城市:
```java
public class Province implements PopulationNode {
private final String name;
private final List<City> cities = new ArrayList<>();
public void addCity(City city) {
cities.add(city);
}
@Override
public int computePopulation() {
return cities.stream()
.mapToInt(PopulationNode::computePopulation)
.sum();
}
}
```
在 `main` 函数中,我们可以这样组装数据:
```java
Province heilongjiang = new Province("黑龙江");
City harbin = new City("哈尔滨");
harbin.addDistrict(new District("南岗区", 1000000));
harbin.addDistrict(new District("道里区", 800000));
heilongjiang.addCity(harbin);
City jiamusi = new City("佳木斯");
jiamusi.addDistrict(new District("向阳区", 300000));
heilongjiang.addCity(jiamusi);
System.out.println(heilongjiang.computePopulation()); // 输出总人口
```
这就是组合模式的典型应用:**无论是叶子节点(如 `District`)还是容器节点(如 `City`、`Province`),都实现同一个接口 `PopulationNode`**。客户端无需关心当前操作的是单个对象还是组合对象,统一调用 `computePopulation()` 即可。
我们打个断点观察数据结构:`Province` 包含多个 `City`,每个 `City` 又包含多个 `District`。整个结构像一棵树,根节点是省,中间节点是城市,叶子节点是区。这正是组合模式的核心思想——树形结构的统一处理。
---
我们再看另一个例子:计算器。我们想计算表达式 `1 + 2 * 3 + 4`,甚至更复杂的带括号的表达式。
我们可以把表达式看作一棵树。比如 `1 + 2` 是一个加法节点,左右子节点分别是数字 1 和 2。`2 * 3` 是乘法节点,以此类推。
我们定义一个接口 `Expression`,它有一个方法 `getValue()`:
```java
public interface Expression {
int getValue();
}
```
叶子节点是 `NumberExpression`,表示一个数字:
```java
public class NumberExpression implements Expression {
private final int value;
public NumberExpression(int value) {
this.value = value;
}
@Override
public int getValue() {
return value;
}
}
```
非叶子节点是操作符表达式。由于加减乘除都是二元操作符,我们先写一个抽象基类 `BinaryOperatorExpression`:
```java
public abstract class BinaryOperatorExpression implements Expression {
protected final Expression left;
protected final Expression right;
protected BinaryOperatorExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
}
```
然后具体实现 `AddExpression`、`SubtractExpression`、`MultiplyExpression`、`DivideExpression`:
```java
public class AddExpression extends BinaryOperatorExpression {
public AddExpression(Expression left, Expression right) {
super(left, right);
}
@Override
public int getValue() {
return left.getValue() + right.getValue();
}
}
```
其他操作符类似。
现在我们可以手动构建表达式树:
```java
Expression one = new NumberExpression(1);
Expression two = new NumberExpression(2);
Expression three = new NumberExpression(3);
Expression four = new NumberExpression(4);
Expression add1 = new AddExpression(one, two); // 1 + 2
Expression mul = new MultiplyExpression(two, three); // 2 * 3
Expression add2 = new AddExpression(add1, mul); // (1+2) + (2*3)
Expression total = new AddExpression(add2, four); // 上述结果 + 4
System.out.println(total.getValue());
```
打个断点可以看到,整个表达式是一棵由 `Expression` 节点构成的树。组合模式让这种结构天然成立。
---
但问题来了:如何把一个字符串表达式(如 `"1 + 2 * (3 + 4)"`)自动解析成这棵树?
这就涉及到表达式解析。我们知道:
- **中缀表达式**:运算符在中间,如 `1 + 2`,人类易读,计算机难解析。
- **后缀表达式(逆波兰式)**:运算符在后面,如 `1 2 +`,计算机易处理。
计算机通常将中缀转为后缀,再计算。例如:
中缀:`(1 + 2) * (3 + 4)`
后缀:`1 2 + 3 4 + *`
后缀表达式的计算很简单:用一个栈,从左到右扫描:
- 遇到数字,入栈;
- 遇到操作符,弹出两个数进行运算,结果再入栈;
- 最后栈中只剩一个数,就是结果。
那么如何将中缀转后缀?规则如下:
1. 遇到操作数,直接加入输出;
2. 遇到 `(`,入栈;
3. 遇到 `)`,将栈中元素弹出到输出,直到遇到 `(`;
4. 遇到操作符,比较优先级:若栈顶操作符优先级 ≥ 当前操作符,则弹出栈顶到输出,直到不满足;然后将当前操作符入栈;
5. 扫描结束后,将栈中剩余操作符全部弹出到输出。
我们实现一个 `ExpressionParser` 类:
```java
public class ExpressionParser {
private final String infixExpression;
public ExpressionParser(String expr) {
this.infixExpression = expr.replaceAll("\\s+", ""); // 去空格
}
public List<String> toPostfix() {
List<String> output = new ArrayList<>();
Deque<String> stack = new ArrayDeque<>();
int i = 0;
while (i < infixExpression.length()) {
char c = infixExpression.charAt(i);
if (Character.isDigit(c)) {
StringBuilder num = new StringBuilder();
while (i < infixExpression.length() && Character.isDigit(infixExpression.charAt(i))) {
num.append(infixExpression.charAt(i++));
}
output.add(num.toString());
i--; // 因为 for 循环会 ++i
} else if (c == '(') {
stack.push(String.valueOf(c));
} else if (c == ')') {
while (!stack.isEmpty() && !stack.peek().equals("(")) {
output.add(stack.pop());
}
stack.pop(); // 移除 '('
} else if (isOperator(c)) {
while (!stack.isEmpty() && precedence(stack.peek()) >= precedence(String.valueOf(c))) {
output.add(stack.pop());
}
stack.push(String.valueOf(c));
} else {
throw new IllegalArgumentException("非法字符: " + c);
}
i++;
}
while (!stack.isEmpty()) {
output.add(stack.pop());
}
return output;
}
private boolean isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
private int precedence(String op) {
return switch (op) {
case "+", "-" -> 1;
case "*", "/" -> 2;
default -> 0;
};
}
}
```
接下来,我们将后缀表达式转换为 `Expression` 对象树:
```java
public Expression parse() {
List<String> postfix = toPostfix();
Deque<Expression> stack = new ArrayDeque<>();
for (String token : postfix) {
if (isOperator(token.charAt(0))) {
Expression right = stack.pop();
Expression left = stack.pop();
Expression expr = switch (token) {
case "+" -> new AddExpression(left, right);
case "-" -> new SubtractExpression(left, right);
case "*" -> new MultiplyExpression(left, right);
case "/" -> new DivideExpression(left, right);
default -> throw new IllegalArgumentException();
};
stack.push(expr);
} else {
int value = Integer.parseInt(token);
stack.push(new NumberExpression(value));
}
}
return stack.pop();
}
```
使用方式:
```java
ExpressionParser parser = new ExpressionParser("1 + 2 * (3 + 4)");
Expression expr = parser.parse();
System.out.println(expr.getValue()); // 输出 15
```
打个断点可以看到,最终生成的是一棵完整的表达式树,完全由组合模式构建。
---
思考题:
你能实现一个支持“上下文”的计算器吗?比如支持变量定义(`x = 5`)、函数调用(`sqrt(16)`),甚至记忆上次计算结果?这需要扩展 `Expression` 接口,引入环境上下文(`Map<String, Integer>`)作为 `getValue(Context context)` 的参数。
Comments NOTHING