javajava-22

Pattern matching on enum type in Java 22


I have a java enum:

public enum PaxosRole {
    FOLLOWER,
    RECOVERER,
    LEADER
}

I wish to use it in a record such as:

public record PaxosEvent(PaxosRole role, PaxosMessage message) {}

When I try to pattern match it with:

        final var paxosEvent = new PaxosEvent(role, msg);
        switch (paxosEvent){
            case PaxosEvent(PaxosRole.FOLLOWER, var msg) -> {
                // do something
            }
            case PaxosEvent(PaxosRole.RECOVERER, var msg) -> {
                // do something else
            }
            case PaxosEvent(PaxosRole.LEADER, var msg) -> {
                // do something distinct
            }

        }

My IDE says Unknown class for the enum in the pattern matches.

Trying to swap the enum for a complex sealed class:

sealed interface PaxosRole {
    record Follower() implements PaxosRole {};
    record Recoverer() implements PaxosRole {};
    record Leader() implements PaxosRole {};

    Follower FOLLOWER = new Follower();
    Recoverer RECOVERER = new Recoverer();
    Leader LEADER = new Leader();
}

Then the following does not work:

        switch (paxosEvent){
            case PaxosEvent(PaxosRole.Follower _, var msg) -> {
                throw new AssertionError("follower role not implemented");
            }
            case PaxosEvent(PaxosRole.Recoverer _, var msg) -> {
                throw new AssertionError("follower role not implemented");
            }
            case PaxosEvent(PaxosRole.Leader _, var msg) -> {
                throw new AssertionError("follower role not implemented");
            }

        }

My IDE says that only the first branch will match; the other two branches are the same class. I am guessing it thinks all are of type PaxosRole. Even if it was to work, it looks pretty verbose.

I can use a with to check the enum:

        switch (paxosEvent){
            case PaxosEvent(var role, var m) when role.equals(PaxosRole.FOLLOWER)-> {
                throw new AssertionError("follower role not implemented");
            }
            case PaxosEvent(var role, var m) when role.equals(PaxosRole.RECOVERER)-> {
                throw new AssertionError("follower role not implemented");
            }
            case PaxosEvent(var role, var m) when role.equals(PaxosRole.LEADER)-> {
                throw new AssertionError("follower role not implemented");
            }
        }

Yet my IDE then tells me that the match is not exhaustive. So, it wants me to add a default branch. Yet it is exhaustive; all three enums are covered in the with statements.

Is there a way to use a simple enum in a switch in Java 22 without a default and without being so verbose?


Solution

  • Mixing value cases and record/type patterns is not supported yet.

    Your work-around

    sealed interface PaxosRole {
        record Follower() implements PaxosRole {};
        record Recoverer() implements PaxosRole {};
        record Leader() implements PaxosRole {};
    
        Follower FOLLOWER = new Follower();
        Recoverer RECOVERER = new Recoverer();
        Leader LEADER = new Leader();
    }
    
    switch(paxosEvent){
        case PaxosEvent(PaxosRole.Follower _, var msg) -> {
            throw new AssertionError("follower role not implemented");
        }
        case PaxosEvent(PaxosRole.Recoverer _, var msg) -> {
            throw new AssertionError("recoverer role not implemented");
        }
        case PaxosEvent(PaxosRole.Leader _, var msg) -> {
            throw new AssertionError("leader role not implemented");
        }
    }
    

    does work with javac; any problems in your IDE are related to the IDE.

    Alternatively, you can use record patterns instead of type patterns:

    switch(paxosEvent) {
        case PaxosEvent(PaxosRole.Follower(), var msg) -> {
            throw new AssertionError("follower role not implemented");
        }
        case PaxosEvent(PaxosRole.Recoverer(), var msg) -> {
            throw new AssertionError("recoverer role not implemented");
        }
        case PaxosEvent(PaxosRole.Leader(), var msg) -> {
            throw new AssertionError("leader role not implemented");
        }
    }
    

    But there is no need for pattern matching at all with your original definition of PaxosRole as an enum type. You could simply use

    switch(paxosEvent.role()) {
        case FOLLOWER -> {
            throw new AssertionError("follower role not implemented");
        }
        case RECOVERER -> {
            throw new AssertionError("recoverer role not implemented");
        }
        case LEADER -> {
            throw new AssertionError("leader role not implemented");
        }
    }
    

    This would require accessing paxosEvent.message() manually when needed. If you really need to avoid that, you can nest switch statements:

    switch(paxosEvent) {
        case PaxosEvent(var role, var msg) -> {
            switch(role) {
                case FOLLOWER -> {
                    throw new AssertionError("follower role not implemented");
                }
                case RECOVERER -> {
                    throw new AssertionError("recoverer role not implemented");
                }
                case LEADER -> {
                    throw new AssertionError("leader role not implemented");
                }
            }
        }
    }
    

    This can still be quite compact. E.g. as an expression:

    int i = switch(paxosEvent) {
        case PaxosEvent(var role, var msg) -> switch(role) {
            case FOLLOWER -> 1;
            case RECOVERER -> 2;
            case LEADER -> 3;
        };
    };
    

    And pattern matching will evolve. Patterns like in your first attempt might be possible in the future…