Deterministic finite automaton in Java
Java 8 constructs

The following

function.putIfAbsent(startState, new HashMap&lt;&gt;());
function.get(startState).put(character, goalState);


can be simplified by using the computeIfAbsent(key, mappingFunction) method. This method will return the current value associated with the given key or compute a new value for that key and return it. As such, you could have:

function.computeIfAbsent(startState, k -&gt; new HashMap&lt;&gt;()).put(character, goalState);


Also, the following

if (!function.containsKey(startState)) {
    return null;
}
return function.get(startState).get(character);


can be also written more simply with the getOrDefault(key, defaultValue) method. This will return the value associated with the given key or else return the default value. You could have:

return function.getOrDefault(startState, Collections.emptyMap()).get(character);


When the map doesn't contain the key startState, an empty map (that was already instantiated, it is a cached value) is returned with Collections.emptyMap().

Code structure

You currently have a class TransitionFunction that holds all transitions and offers a method to go from one state to another for a particular character.

I find the name TransitionFunction a bit misleading because it would imply that the class is in fact a Function in that it is a functional interface taking a parameter and returing a value. But it is more than that since it is the class that holds all possible transitions.

Why not rename the class Transitions and:


Rename the method setTransition to addTransition to better reflect the fact that this method adds a transition (instead of setting it).
Move the code for processing all transitions, that is currently placed inside the matches method, in this class. This method would be called processAll(Integer startState, char[] characters) and would process all the characters given starting at the given state.


This is what the class could look like

public class Transitions {

    private final Map&lt;Integer, Map&lt;Character, Integer&gt;&gt; function = new HashMap&lt;&gt;();

    public void addTransition(Integer startState, Integer goalState, char character) {
        function.computeIfAbsent(startState, k -&gt; new HashMap&lt;&gt;()).put(character, goalState);
    }

    public Integer process(Integer startState, char character) {
        return function.getOrDefault(startState, Collections.emptyMap()).get(character);
    }

    public Integer processAll(Integer startState, char[] characters) {
        return IntStream.range(0, characters.length)
                        .boxed()
                        .reduce(startState, (s, i) -&gt; s == null ? null : process(s, characters[i]));
    }

}


Notice that the processAll method leverages an IntStream over the indexes of the input array. The final state is calculating by reducing from the starting state and calling process each time on the accumulated result. The difference between your initial code is that it is not short-circuiting; if you wish to keep that, you can keep your for loop as-is.

With this change, the matches method would just be:

public boolean matches(String text) {
    Integer finalState = transitionFunction.processAll(startState, text.toCharArray());
    return finalState != null &amp;&amp; acceptingStates.contains(finalState);
}


Using Optional

You are currently relying on null values to mean "no state". Using an Optional would remove all that. We could make the methods process and processAll return an Optional&lt;Integer&gt; with

public Optional&lt;Integer&gt; process(Integer startState, char character) {
    return Optional.ofNullable(function.getOrDefault(startState, Collections.emptyMap()).get(character));
}

public Optional&lt;Integer&gt; processAll(Integer startState, char[] characters) {
    return IntStream.range(0, characters.length)
                    .boxed()
                    .reduce(
                      Optional.of(startState),
                      (o, i) -&gt; o.flatMap(s -&gt; process(s, characters[i])),
                      (o1, o2) -&gt; o1.map(Optional::of).orElse(o2)
                    );
}


process makes a call to Optional.ofNullable to wrap the value. The reducing part works on the optional values. The combiners combines two optional into a single one.

Then, the matches method becomes

public boolean matches(String text) {
    return transitionFunction.processAll(startState, text.toCharArray())
                             .map(acceptingStates::contains)
                             .orElse(false);
}