【问题标题】:How to implement a FSM - Finite State Machine in Java如何在 Java 中实现 FSM - 有限状态机
【发布时间】:2012-11-04 17:46:07
【问题描述】:

我有工作要做,需要你的帮助。 我们想实现一个FSM - Finite State Machine,来识别字符序列(比如:A、B、C、A、C),并判断它是否被接受。

我们想实现三个类:StateEventMachinestate 类在FSM 中表示一个节点,我们想用State design pattern 来实现它,每个节点都将从抽象类状态扩展,每个类将处理不同类型的事件并指示转换到新状态。您认为这是个好主意吗?

第二件事,我们不知道如何保存所有的过渡。我们再次考虑使用某种map 来实现它,它保持起点并获得带有下一个状态的某种向量,但我不确定这是一个好主意。

我很乐意了解如何实现它,或者您可以给我一些起点。

我应该如何保存 FSM,这意味着我应该如何在程序开始时构建树? 我用谷歌搜索了很多例子,但没有任何帮助。

非常感谢。

【问题讨论】:

  • 当然是个好主意,但如果你要识别字符串,你最好使用例如正则表达式?
  • 也许你可以试试二维数组。
  • 我有一个你可能想看的库:github.com/mtimmerm/dfalex

标签: java design-patterns state-machine fsm state-pattern


【解决方案1】:

状态机的核心是转换表,它将状态和符号(您所称的事件)带到新状态。这只是一个包含两个索引的状态数组。为了理智和类型安全,将状态和符号声明为枚举。我总是以某种方式(特定于语言)添加一个“长度”成员来检查数组边界。当我手动编码 FSM 时,我使用空白摆弄将代码格式化为行和列格式。状态机的其他元素是初始状态和接受状态集。接受状态集的最直接实现是由状态索引的布尔数组。然而,在 Java 中,枚举是类,您可以在声明中为每个枚举值指定一个“接受”参数,并在枚举的构造函数中对其进行初始化。

对于机器类型,您可以将其编写为泛型类。它需要两个类型参数,一个用于状态,一个用于符号,一个用于转换表的数组参数,一个用于初始状态的单个状态。唯一的其他细节(尽管很关键)是您必须调用 Enum.ordinal() 以获得适合索引转换数组的整数,因为您没有直接声明具有枚举索引的数组的语法(尽管应该是)。

为了抢占一个问题,EnumMap 不适用于转换表,因为所需的键是一对枚举值,而不是单个。

enum State {
    Initial( false ),
    Final( true ),
    Error( false );
    static public final Integer length = 1 + Error.ordinal();

    final boolean accepting;

    State( boolean accepting ) {
        this.accepting = accepting;
    }
}

enum Symbol {
    A, B, C;
    static public final Integer length = 1 + C.ordinal();
}

State transition[][] = {
    //  A               B               C
    {
        State.Initial,  State.Final,    State.Error
    }, {
        State.Final,    State.Initial,  State.Error
    }
};

【讨论】:

  • 这真是个好主意。我需要你详细说明。 状态的双索引数组是什么意思,如果我有来自一个状态的几条路线?对于状态类,我们有一个抽象类,每个派生类都是(开始状态、常规状态和接受状态),这样我们每个都有一个类型。您认为我们需要机器类作为通用类吗?最后一件事,你有什么链接可以告诉我你的方法已经介绍了吗?非常感谢您的帮助。
  • 双索引数组是二维数组。见例子。开始状态和接受状态常规状态。您不需要甚至不需要为这些单独的类。接受是状态的布尔谓词。初始状态是状态机的属性,而不是状态本身的属性。机器本身不需要是通用的,尽管我发现这样做是个好习惯。特别是,它允许您对状态机代码进行单元测试,而与您想使用的特定状态机无关,这将获得自己的一组测试。
  • 感谢您的帮助。到目前为止,我所做的是为状态创建一个抽象类,派生类来自不同的类型(开始、标准、结束和错误)。每个状态都有一个 hashTable 保存他的孩子,或者在 null 的情况下是错误状态。我现在要做的是为不同的序列打印不同的消息。您对如何实现这一目标有任何建议吗?谢谢。
  • 如果您想拥有特定于 sequences 的消息,那么您需要一个表示该序列的状态。当你进入状态时,你可以触发你的动作。但是这个问题是如何设计机器而不是实现它,这是你最初的问题。
  • 这种方式不需要在Java中声明静态length。请改用State.values().lengthSymbol.values().length
【解决方案2】:

EasyFSM 是一个动态 Java 库,可用于实现 FSM。

您可以在以下位置找到相同的文档: Finite State Machine in Java

此外,您可以在以下位置下载库: Java FSM Library : DynamicEasyFSM

【讨论】:

    【解决方案3】:

    您可以通过两种不同的方式实现有限状态机。

    选项 1:

    具有预定义工作流的有限状态机:如果您提前知道所有状态并且状态机几乎是固定的且未来没有任何更改,建议您使用

    1. 识别应用程序中所有可能的状态

    2. 识别应用程序中的所有事件

    3. 确定您的应用程序中所有可能导致状态转换的条件

    4. 事件的发生可能会导致状态转换

    5. 通过决定状态和转换的工作流程来构建有限状态机。

      例如,如果状态 1 发生事件 1,则状态将被更新,机器状态可能仍处于状态 1。

      如果在状态 1 发生事件 2,在某些条件评估下,系统将从状态 1 移动到状态 2

    此设计基于 StateContext 模式。

    看看Finite State Machine原型类。

    选项 2:

    行为树:如果状态机工作流程经常更改,建议使用。您可以在不破坏树的情况下动态添加新行为。

    Task 基类为所有这些任务提供了一个接口,leaf tasks 就是刚才提到的那些,而父任务是决定哪个任务的内部节点接下来执行。

    任务只有他们需要做的事情的逻辑,任务是否开始,是否需要更新,是否完成的所有决策逻辑with success 等被分组到 TaskController 类中,并通过组合添加。

    装饰器是通过包装另一个类并赋予其额外逻辑来“装饰”另一个类的任务。

    最后,Blackboard 类是父 AI 拥有的类,每个任务都引用它。它作为所有叶子任务的知识数据库

    请查看 Jaime Barrachina Verdia 的 article 了解更多详情

    【讨论】:

    • 很好的答案!有限状态机的链接可能被替换为:github.com/eclipse-ee4j/orb-gmbal-pfl/tree/master/pfl-basic/src/… 这些是相同的类,但是它们不是专有 Oracle JDK 源的一部分(另请参见源中的机密标题),它们是 Eclipse 的一部分,并在 3 条款下发布BSD 许可证是 Oracle 向 Eclipse 基金会捐赠的 Java EE 的一部分。
    【解决方案4】:

    嗯,我建议你使用享元来实现状态。目的:避免大量小对象的内存开销。状态机可以变得非常非常大。

    http://en.wikipedia.org/wiki/Flyweight_pattern

    我不确定是否需要使用设计模式状态来实现节点。状态机中的节点是无状态的。它们只是将当前输入符号与当前状态的可用转换相匹配。也就是说,除非我完全忘记了它们是如何工作的(这是绝对可能的)。

    如果我在编写代码,我会这样做:

    interface FsmNode {
      public boolean canConsume(Symbol sym);
      public FsmNode consume(Symbol sym);
      // Other methods here to identify the state we are in
    }
    
      List<Symbol> input = getSymbols();
      FsmNode current = getStartState();
      for (final Symbol sym : input) {
        if (!current.canConsume(sym)) {
          throw new RuntimeException("FSM node " + current + " can't consume symbol " + sym);
        }
        current = current.consume(sym);
      }
      System.out.println("FSM consumed all input, end state is " + current);
    

    在这种情况下享元会做什么?好吧,在 FsmNode 下可能会有这样的东西:

    Map<Integer, Map<Symbol, Integer>> fsm; // A state is an Integer, the transitions are from symbol to state number
    FsmState makeState(int stateNum) {
      return new FsmState() {
        public FsmState consume(final Symbol sym) {
          final Map<Symbol, Integer> transitions = fsm.get(stateNum);
          if (transisions == null) {
            throw new RuntimeException("Illegal state number " + stateNum);
          }
          final Integer nextState = transitions.get(sym);  // May be null if no transition
          return nextState;
        }
        public boolean canConsume(final Symbol sym) {
          return consume(sym) != null;
        }
      }
    }
    

    这会在需要使用的基础上创建状态对象,它允许您使用更有效的底层机制来存储实际的状态机。我在这里使用的那个 (Map(Integer, Map(Symbol, Integer))) 不是特别有效。

    请注意,Wikipedia 页面侧重于许多有些相似的对象共享相似数据的情况,例如 Java 中的 String 实现。在我看来,享元更通用一点,涵盖了任何按需创建生命周期较短的对象(使用更多 CPU 来节省更高效的底层数据结构)。

    【讨论】:

    • 感谢您的帮助。到目前为止,我所做的是为状态创建一个抽象类,派生类来自不同的类型(开始、标准、结束和错误)。每个状态都有一个 hashTable 保存他的孩子,或者在 null 的情况下是错误状态。我现在要做的是为不同的序列打印不同的消息。您对如何实现这一目标有任何建议吗?谢谢。
    • 当然。您可以通过以下几种方式执行此操作:如果消息应该取决于到达结束符号的路径,您要么需要让节点在其消费方法中打印部分消息,要么将路径记录在消费时维护的某个列表中节点。如果消息取决于作为最终节点到达的终端节点,则只需让该节点将其打印为其消费方法的一部分。这有意义吗?
    • 这个实现,来自某种采访。他们告诉我,我有一个 State 抽象类,派生类应该实现 FSM 的逻辑。我认为每个派生类应该来自不同的类型,就像我说的,例如开始、标准、结束和错误。您认为这是正确的选择,还是您可以考虑其他想法?谢谢。
    • 我不确定您是否需要不同的类。你可以用很多其他方式来处理它。基本上,如果您发现自己检查了某个引用的确切类型,那么您可能在调用代码中做了一些应该由对象处理的事情(参见 Liskov 替换原则)。如果您真的需要它(例如,检查您是否处于结束状态),请在公共接口上实现一个方法(isEndState())。在我的代码中,我可能有一个可以返回而不是 null 的 Error 类。每当调用任何方法时,该实现基本上都会失败。
    • 那么在这种情况下,你认为我应该如何使用抽象类呢?我真的不知道如何继续这个。我看到了很多让我感到困惑的例子。
    【解决方案5】:

    考虑一下简单、轻量级的 Java 库 EasyFlow。从他们的文档中:

    使用 EasyFlow,您可以:

    • 实现复杂的逻辑,但保持代码简洁干净
    • 轻松优雅地处理异步调用
    • 使用事件驱动的编程方法避免并发
    • 通过避免递归来避免 StackOverflow 错误
    • 简化复杂 Java 应用程序的设计、编程和测试

    【讨论】:

      【解决方案6】:

      我用 java 设计并实现了一个简单的有限状态机示例。

      IFiniteStateMachine:管理有限状态机的公共接口
      例如向有限状态机添加新状态或通过特定操作转移到下一个状态。

      interface IFiniteStateMachine {
          void setStartState(IState startState);
      
          void setEndState(IState endState);
      
          void addState(IState startState, IState newState, Action action);
      
          void removeState(String targetStateDesc);
      
          IState getCurrentState();
      
          IState getStartState();
      
          IState getEndState();
      
          void transit(Action action);
      }
      

      IState:获取状态相关信息的公共接口
      例如状态名称和连接状态的映射。

      interface IState {
          // Returns the mapping for which one action will lead to another state
          Map<String, IState> getAdjacentStates();
      
          String getStateDesc();
      
          void addTransit(Action action, IState nextState);
      
          void removeTransit(String targetStateDesc);
      }
      

      Action:会导致状态转换的类。

      public class Action {
          private String mActionName;
      
          public Action(String actionName) {
              mActionName = actionName;
          }
      
          String getActionName() {
              return mActionName;
          }
      
          @Override
          public String toString() {
              return mActionName;
          }
      
      }
      

      StateImpl:IState的实现。我应用了 HashMap 等数据结构来保持 Action-State 映射。

      public class StateImpl implements IState {
          private HashMap<String, IState> mMapping = new HashMap<>();
          private String mStateName;
      
          public StateImpl(String stateName) {
              mStateName = stateName;
          }
      
          @Override
          public Map<String, IState> getAdjacentStates() {
              return mMapping;
          }
      
          @Override
          public String getStateDesc() {
              return mStateName;
          }
      
          @Override
          public void addTransit(Action action, IState state) {
              mMapping.put(action.toString(), state);
          }
      
          @Override
          public void removeTransit(String targetStateDesc) {
              // get action which directs to target state
              String targetAction = null;
              for (Map.Entry<String, IState> entry : mMapping.entrySet()) {
                  IState state = entry.getValue();
                  if (state.getStateDesc().equals(targetStateDesc)) {
                      targetAction = entry.getKey();
                  }
              }
              mMapping.remove(targetAction);
          }
      
      }
      

      FiniteStateMachineImpl:IFiniteStateMachine 的实现。我使用 ArrayList 来保存所有的状态。

      public class FiniteStateMachineImpl implements IFiniteStateMachine {
          private IState mStartState;
          private IState mEndState;
          private IState mCurrentState;
          private ArrayList<IState> mAllStates = new ArrayList<>();
          private HashMap<String, ArrayList<IState>> mMapForAllStates = new HashMap<>();
      
          public FiniteStateMachineImpl(){}
          @Override
          public void setStartState(IState startState) {
              mStartState = startState;
              mCurrentState = startState;
              mAllStates.add(startState);
              // todo: might have some value
              mMapForAllStates.put(startState.getStateDesc(), new ArrayList<IState>());
          }
      
          @Override
          public void setEndState(IState endState) {
              mEndState = endState;
              mAllStates.add(endState);
              mMapForAllStates.put(endState.getStateDesc(), new ArrayList<IState>());
          }
      
          @Override
          public void addState(IState startState, IState newState, Action action) {
              // validate startState, newState and action
      
              // update mapping in finite state machine
              mAllStates.add(newState);
              final String startStateDesc = startState.getStateDesc();
              final String newStateDesc = newState.getStateDesc();
              mMapForAllStates.put(newStateDesc, new ArrayList<IState>());
              ArrayList<IState> adjacentStateList = null;
              if (mMapForAllStates.containsKey(startStateDesc)) {
                  adjacentStateList = mMapForAllStates.get(startStateDesc);
                  adjacentStateList.add(newState);
              } else {
                  mAllStates.add(startState);
                  adjacentStateList = new ArrayList<>();
                  adjacentStateList.add(newState);
              }
              mMapForAllStates.put(startStateDesc, adjacentStateList);
      
              // update mapping in startState
              for (IState state : mAllStates) {
                  boolean isStartState = state.getStateDesc().equals(startState.getStateDesc());
                  if (isStartState) {
                      startState.addTransit(action, newState);
                  }
              }
          }
      
          @Override
          public void removeState(String targetStateDesc) {
              // validate state
              if (!mMapForAllStates.containsKey(targetStateDesc)) {
                  throw new RuntimeException("Don't have state: " + targetStateDesc);
              } else {
                  // remove from mapping
                  mMapForAllStates.remove(targetStateDesc);
              }
      
              // update all state
              IState targetState = null;
              for (IState state : mAllStates) {
                  if (state.getStateDesc().equals(targetStateDesc)) {
                      targetState = state;
                  } else {
                      state.removeTransit(targetStateDesc);
                  }
              }
      
              mAllStates.remove(targetState);
      
          }
      
          @Override
          public IState getCurrentState() {
              return mCurrentState;
          }
      
          @Override
          public void transit(Action action) {
              if (mCurrentState == null) {
                  throw new RuntimeException("Please setup start state");
              }
              Map<String, IState> localMapping = mCurrentState.getAdjacentStates();
              if (localMapping.containsKey(action.toString())) {
                  mCurrentState = localMapping.get(action.toString());
              } else {
                  throw new RuntimeException("No action start from current state");
              }
          }
      
          @Override
          public IState getStartState() {
              return mStartState;
          }
      
          @Override
          public IState getEndState() {
              return mEndState;
          }
      }
      

      示例

      public class example {
      
          public static void main(String[] args) {
              System.out.println("Finite state machine!!!");
              IState startState = new StateImpl("start");
              IState endState = new StateImpl("end");
              IFiniteStateMachine fsm = new FiniteStateMachineImpl();
              fsm.setStartState(startState);
              fsm.setEndState(endState);
              IState middle1 = new StateImpl("middle1");
              middle1.addTransit(new Action("path1"), endState);
              fsm.addState(startState, middle1, new Action("path1"));
              System.out.println(fsm.getCurrentState().getStateDesc());
              fsm.transit(new Action(("path1")));
              System.out.println(fsm.getCurrentState().getStateDesc());
              fsm.addState(middle1, endState, new Action("path1-end"));
              fsm.transit(new Action(("path1-end")));
              System.out.println(fsm.getCurrentState().getStateDesc());
              fsm.addState(endState, middle1, new Action("path1-end"));
          }
      
      }
      

      Full example on Github

      【讨论】:

        【解决方案7】:

        这是一个超级简单的 FSM 实现/示例,它仅使用“if-else”避免了上述所有子类化答案(取自Using Finite State Machines for Pattern Matching in Java,他正在寻找以“@”结尾的字符串后跟数字后跟“#”--参见状态图here):

        public static void main(String[] args) {
            String s = "A1@312#";
            String digits = "0123456789";
            int state = 0;
            for (int ind = 0; ind < s.length(); ind++) {
                if (state == 0) {
                    if (s.charAt(ind) == '@')
                        state = 1;
                } else {
                    boolean isNumber = digits.indexOf(s.charAt(ind)) != -1;
                    if (state == 1) {
                        if (isNumber)
                            state = 2;
                        else if (s.charAt(ind) == '@')
                            state = 1;
                        else
                            state = 0;
                    } else if (state == 2) {
                        if (s.charAt(ind) == '#') {
                            state = 3;
                        } else if (isNumber) {
                            state = 2;
                        } else if (s.charAt(ind) == '@')
                            state = 1;
                        else
                            state = 0;
                    } else if (state == 3) {
                        if (s.charAt(ind) == '@')
                            state = 1;
                        else
                            state = 0;
                    }
                }
            } //end for loop
        
            if (state == 3)
                System.out.println("It matches");
            else
                System.out.println("It does not match");
        }
        

        P.S:不直接回答您的问题,而是向您展示如何在 Java 中非常轻松地实现 FSM。

        【讨论】:

          猜你喜欢
          • 2011-04-16
          • 2015-10-14
          • 2012-06-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-11-18
          • 2019-02-12
          • 2012-06-19
          相关资源
          最近更新 更多