Pattern matching in Java(翻译)

模式匹配是一种来源于函数式语言的特性,它有点像java中的switch,
然而比switch更加强大,它除了能对值进行匹配之外还能对类型进行匹配.
配合scala的模板类,它能写出非常简洁的类型、值匹配的代码.
当你需要根据对象的类型或者值的不同来进行不同的逻辑处理时,就应该使用模式匹配.
下面的内容将主要针对其中的类型匹配展开.

scala中的模式匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
object patterns {
abstract class Foo
class Bar(val bar: Int) extends Foo

class Baz(val baz: String) extends Foo

def handle(f: Foo) =
f match {
case b: Bar => b.bar
case b: Baz => b.baz
}

handle(new Bar(42)) //> res0: Any = 42
handle(new Baz("Luhrmann")) //> res1: Any = Luhrmann
}

上面定义了一个抽象类Foo和两个子类Bar,Baz(object关键字说明这个类是单例的),
两个子类各有一个名称与类型都不同的成员属性,另外有一个handle方法来处理子类对象.
scala具有类型推导,所以这里没有声明方法的返回类型.

java中的做法

instanceof

简单的做法是使用instanceof

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
abstract class BadFoo {

static class Bar extends BadFoo {
int bar;

private Bar(int bar) {
this.bar = bar;
}

}

static class Baz extends BadFoo {
String baz;

private Baz(String baz) {
this.baz = baz;
}

}

static void handle(BadFoo f) {
if (f instanceof Bar) {
Bar b = (Bar) f;
System.out.println("I have " + b.bar + " bars");
} else if (f instanceof Baz) {
Baz b = (Baz) f;
System.out.println("I have the " + b.baz + " baz");
}
}

public static void main(String[] args) {
handle(new Bar(42));
handle(new Baz("Luhrmann"));
}
}

然而这么写有几个问题,1.在那些使用动态代理的代码中,产生的对象永远不会是原始类的一个实例,
instanceof不起作用.2.无法在编译时期进行类型判断,if (f instanceof Bar) Baz b = (Baz) f;
以上代码能编译通过,但是不能运行.3.当增加了一个新的子类时,不能保证旧代码被及时更新.

Moving logic into the class

另一种做法是把逻辑放到数据对象中,就像OOP应该做的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
abstract class OoFoo {
abstract void handle();

static class Bar extends OoFoo {
private int bar;

private Bar(int bar) {
this.bar = bar;
}

@Override
void handle() {
System.out.println("I have " + bar + " bars");
}
}

static class Baz extends OoFoo {
private String baz;

private Baz(String baz) {
this.baz = baz;
}

@Override
void handle() {
System.out.println("I have the " + baz + " baz");
}
}

public static void main(String[] args) {
new Bar(42).handle();
new Baz("Luhrmann").handle();
}
}

这么做刚开始还不错,然而当类似的方法逐渐变多,你的数据对象就变成了存放业务逻辑的地方.
使用访问者模式能够保持数据对象简单,把业务逻辑放到响应的处理类中.

访问者模式

Basic Visitor

基本的访问者模式包括以下内容:

  • 一个抽象类或者接口包括一个以Visitor作为参数,返回类型为Visitor中泛型类型的方法match
  • 一个Visitor类,包括各个子类对象为参数的case方法
  • 一系列实现了match方法的子类
  • 实现各个case方法的应用代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    abstract class Foo {
    abstract <T> T match(Visitor<T> visitor);

    interface Visitor<T> {
    T caseBar(Bar b);
    T caseBaz(Baz b);
    }

    static class Bar extends Foo {
    final int bar;

    private Bar(int bar) {
    this.bar = bar;
    }

    @Override
    <T> T match(Visitor<T> visitor) {
    return visitor.caseBar(this);
    }
    }

    static class Baz extends Foo {
    final String baz;

    private Baz(String baz) {
    this.baz = baz;
    }

    @Override
    <T> T match(Visitor<T> visitor) {
    return visitor.caseBaz(this);
    }
    }

    static void handle(Foo f) {
    System.out.println(f.match(new Visitor<String>() {
    @Override
    public String caseBar(Bar b) {
    return "I have " + b.bar + " bars";
    }
    @Override
    public String caseBaz(Baz b) {
    return "I have the " + b.baz + " baz";
    }
    }));
    }

    public static void main(String[] args) {
    handle(new Bar(42));
    handle(new Baz("Luhrmann"));
    }
    }

Default visitor

有时候Visitor没必要重写全部的case方法,只重写部分方法,其他时候返回默认值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class DefaultFooVisitor<T> implements Foo.Visitor<T> {
T defaultValue;

private DefaultFooVisitor(T defaultValue) {
this.defaultValue = defaultValue;
}

@Override
public T caseBar(Bar b) {
return defaultValue;
}

@Override
public T caseBaz(Baz b) {
return defaultValue;
}

static int countBars(Foo f) {
return f.match(new DefaultFooVisitor<Integer>(0) {
@Override
public Integer caseBar(Bar b) {
return b.bar;
}
});
}
}

缺点是当增加了新的子类,新增的case方法可能会忘记重写.
解决这个问题的一种方法是先把DefaultFooVisitor中新增的case方法改为抽象的,
解决了所有的编译问题之后再来实现DefaultFooVisitor中新增的case方法.

Void or Unit return values

有时候返回值是不需要的,可以用Unit.unit或者java.lang.Void来表达空的返回类型.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class PartitionFoos {
void doStuff(Collection<Foo> foos) {
final List<Bar> bars = new ArrayList<>();
final List<Baz> bazes = new ArrayList<>();
for (Foo f : foos) {
f.match(new Visitor<Void>() {

@Override
public Void caseBar(Bar b) {
bars.add(b);
return null;
}

@Override
public Void caseBaz(Baz b) {
bazes.add(b);
return null;
}

});
}
}

}

解构模式匹配

前面的例子都只包含了类型匹配,最后的一个例子会展示java处理包含类型以及值匹配的方法.
先来看一下scala的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object patterns {
abstract class Foo
case class Bar(val bar: Int) extends Foo

case class Baz(val baz: String) extends Foo

def handle(f: Foo) =
f match {
case Bar(i) if (i > 10) => 10
case Bar(i) => i
case Baz(s) => s
}

handle(new Bar(42)) //> res0: Any = 10
handle(new Bar(3)) //> res0: Any = 3
handle(new Baz("Luhrmann")) //> res1: Any = Luhrmann
}

简洁,灵活.再来看一下java版本的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
abstract class Foo {
abstract <T> T match(Visitor<T> visitor);

interface Visitor<T> {
T caseManyBar();
T caseBar(int i);
T caseBaz(String s);
}

static class Bar extends Foo {
private int bar;

private Bar(int bar) {
this.bar = bar;
}

@Override
<T> T match(Visitor<T> visitor) {
return bar > 10 ? visitor.caseManyBar() : visitor.caseBar(bar);
}
}

static class Baz extends Foo {
private String baz;

private Baz(String baz) {
this.baz = baz;
}

@Override
<T> T match(Visitor<T> visitor) {
return visitor.caseBaz(baz);
}
}

static void handle(Foo f) {
System.out.println(f.match(new Visitor<String>() {
@Override
public String caseBar(int i) {
return "I have " + i + " bars";
}
@Override
public String caseManyBar() {
return "I have many bars";
}
@Override
public String caseBaz(String s) {
return "I have the " + s + " baz";
}
}));
}

public static void main(String[] args) {
handle(new Bar(42));
handle(new Bar(3));
handle(new Baz("Luhrmann"));
}
}

16vs58,很难说用多余的42行来模拟相同的功能是否值得.
访问者模式的好处是,让你以一种类型安全的方式来处理子类.
在你添加新的子类时,编译器会告诉你哪些代码需要修改.
当你下一次遇到ClassCastException时你应该考虑使用它.

原文:
Pattern matching in Java with the Visitor pattern