这篇文章将解决在Java中实现有限状态机的问题。 如果您不知道什么是FSM或在什么地方可以使用FSM,您可能会热衷于阅读此 , 这个和这个 。
如果您发现自己在设计上使用FSM的情况,则可能已经开始为实现相同接口的每个状态编写类。 一个好的设计可能是:
interface State { }
class State_1 implements State {}
class State_2 implements State {}
...
您可能有一个类可以管理这些状态以及它们之间的过渡,而另一个可以实现FSM的上下文(输入带状区域),另一个用于起始状态的接口,另一个用于结束状态的接口,依此类推。 许多类分散在许多文件中,使您无法快速跟踪它们。
还有另一种方法:使用枚举。
枚举本质上是类的列表,并且枚举的每个成员可能具有不同的实现。 假设我们要实现以下状态机:
初始化-('a')-> A
初始化-(else)->失败
A-('b')-> B A-('c')-> C A-('a')-> A A-(”)->结束 A-(其他)->失败 B-('c')-> C B-('b')-> B B-(”)->结束 B-(其他)->失败 C-('c')-> C C-(”)->结束 C-(其他)->失败
该FSM将验证以下正则表达式:^(a +)(b *)(c *)$。 我们可以将状态写成枚举状态的元素,如下所示:
interface State {
public State next();
}
class Input {
private String input;
private int current;
public Input(String input) {this.input = input;}
char read() { return input.charAt(current++); }
}
enum States implements State {
Init {
@Override
public State next(Input word) {
switch(word.read()) {
case 'a': return A;
default: return Fail;
}
}
},
A {
@Override
public State next(Input word) {
switch(word.read()) {
case 'a': return A;
case 'b': return B;
case 'c': return C;
case '': return null;
default: return Fail;
}
}
},
B {
@Override
public State next(Input word) {
switch(word.read()) {
case 'b': return B;
case 'c': return C;
case '': return null;
default: return Fail;
}
}
},
C {
@Override
public State next(Input word) {
switch(word.read()) {
case 'c': return C;
case '': return null;
default: return Fail;
}
}
},
Fail {
@Override
public State next(Input word) {
return Fail;
}
};
public abstract State next(Input word);
}
我们要做的是定义每个枚举中每个状态的转换。 每个过渡都会返回一个新状态,因此我们可以更有效地循环遍历它们:
State s;
Input in = new Input("aabbbc");
for(s = States.Init; s != null || s != States.Fail; s = s.next(in)) {}
if(s == null) {System.out.printlin("Valid!");}
else {System.out.println("Failed");}
此时,我们要么验证了字符串,要么失败了。 这是一个简单而优雅的设计。
我们可以通过将最终状态与主要状态分开来进一步改善实现,以简化遍历的退出条件。 我们将定义另一个名为FinalState的接口,以及一个将包含所需退出状态的第二枚举(相应地更改States枚举):
interface FinalState extends State {}
enum FinalStates implements FinalState {
Fail {
@Override
public State next(Input word) {
return Fail;
}
},
Ok {
@Override
public State next(Input word) {
return End;
}
}
}
这样,遍历将有所简化:
for(s = States.Init; !(s instanceof FinalState); s = s.next(in)) {}
switch(s) {
case Fail: System.out.printlin("Failed"); break;
case Ok: System.out.println("Valid!"); break;
default: System.out.println("Undefined"); break;
}
优点是(除了更简单的遍历之外),我们可以指定两个以上的最终状态。 在大多数情况下,FSM会有多个出口点,因此建议从长远来看最后一种改进。 您还可以将所有这些枚举和接口放在同一源文件中,并将整个逻辑放在一个位置,而不是浏览多个选项卡,以便了解流程的工作原理。
总之,使用枚举可以更紧凑,更有意义地实现FSM。 您将所有逻辑都放在一个文件中,并且所有遍历都是轻松的。 您还将获得更轻松的调试体验,因为已转换的状态的名称将显示在调试器中(变量s将相应地更改其值,并带有新状态的名称),而不必弄清楚您刚刚上过什么课。 总而言之,我认为这是一个好技术。
参考:我们的JCG合作伙伴 Attila-Mihaly Balazs 在Java中实现自动机 在Transylvania JUG博客上。
翻译自: https://www.javacodegeeks.com/2012/03/automaton-implementation-in-java.html