Switch expression with void return type

5.5k views Asked by At

Is there any way to force an exhaustive check of all enum values when the switch branches call methods with void return type? It's quite ugly to hard-code a yield just to coax the compiler to demand exhaustiveness.

This is my current pattern (the handle methods have void return type)

int unused = switch (event.getEventType()) {
    case ORDER   -> { handle((OrderEvent) event); yield 0; }
    case INVOICE -> { handle((InvoiceEvent) event); yield 0; }
    case PAYMENT -> { handle((PaymentEvent) event); yield 0; }
};

The reason I want to use an expression is to get a compilation error when a new enum value is added and is not handled.

5

There are 5 answers

6
samabcde On BEST ANSWER

Maybe yield a Consumer of Event, so you yield something useful, the trade off is one more line for consumer.accept.

Consumer<Event> consumer = switch (event.getEventType()) {
    case ORDER -> e -> handle((OrderEvent) e);
    case INVOICE -> e -> handle((InvoiceEvent) e);
    case PAYMENT -> e -> handle((PaymentEvent) e);
};
consumer.accept(event);

Continue if you concern performance

Based on the comment concerning performance penalty, a benchmark is performed to compare following scenarios:

  1. Using consumer and handle is instance method
  2. Using consumer and handle is static method
  3. Not using consumer and handle is instance method
  4. Not using consumer and handle is static method

To see

  • Is using Consumer has large performance impact?
  • Is there any difference for static and instance handle method?

And the result is:

# Run complete. Total time: 00:20:30

Benchmark                                          Mode  Cnt      Score     Error   Units
SwitchExpressionBenchMark.consumerHandle          thrpt  300  49343.496 ±  91.324  ops/ms
SwitchExpressionBenchMark.consumerStaticHandle    thrpt  300  49312.273 ± 112.630  ops/ms
SwitchExpressionBenchMark.noConsumerHandle        thrpt  300  49353.232 ± 106.522  ops/ms
SwitchExpressionBenchMark.noConsumerStaticHandle  thrpt  300  49496.614 ± 122.916  ops/ms

By observing the result, there is no much different between the 4 scenarios.

  • Using Consumer does not have significant performance impact.
  • The performance different between static and instance handle method is neglectable.

The benchmark is performed with:
CPU: Intel(R) Core(TM) i7-8750H
Memory: 16G
JMH version: 1.19
VM version: JDK 15.0.2

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@Warmup(iterations = 30, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 30, time = 500, timeUnit = TimeUnit.MILLISECONDS)
public class SwitchExpressionBenchMark {
    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }

    @Benchmark
    public void consumerStaticHandle(Blackhole blackhole, InvoiceEvent invoiceEvent) {
        Event event = invoiceEvent;
        Consumer<Event> consumer = switch (event.getEventType()) {
            case ORDER -> e -> staticHandle((OrderEvent) e);
            case INVOICE -> e -> staticHandle((InvoiceEvent) e);
            case PAYMENT -> e -> staticHandle((PaymentEvent) e);
        };
        consumer.accept(event);
    }

    @Benchmark
    public void consumerHandle(Blackhole blackhole, InvoiceEvent invoiceEvent) {
        Event event = invoiceEvent;
        Consumer<Event> consumer = switch (event.getEventType()) {
            case ORDER -> e -> this.handle((OrderEvent) e);
            case INVOICE -> e -> this.handle((InvoiceEvent) e);
            case PAYMENT -> e -> this.handle((PaymentEvent) e);
        };
        consumer.accept(event);
    }

    @Benchmark
    public void noConsumerHandle(Blackhole blackhole, InvoiceEvent invoiceEvent) {
        Event event = invoiceEvent;
        int unused = switch (event.getEventType()) {
            case ORDER -> {
                this.handle((OrderEvent) event);
                yield 0;
            }
            case INVOICE -> {
                this.handle((InvoiceEvent) event);
                yield 0;
            }
            case PAYMENT -> {
                this.handle((PaymentEvent) event);
                yield 0;
            }
        };
    }

    @Benchmark
    public void noConsumerStaticHandle(Blackhole blackhole, InvoiceEvent invoiceEvent) {
        Event event = invoiceEvent;
        int unused = switch (event.getEventType()) {
            case ORDER -> {
                staticHandle((OrderEvent) event);
                yield 0;
            }
            case INVOICE -> {
                staticHandle((InvoiceEvent) event);
                yield 0;
            }
            case PAYMENT -> {
                staticHandle((PaymentEvent) event);
                yield 0;
            }
        };
    }

    private static void staticHandle(PaymentEvent event) {
        doSomeJob();
    }

    private static void staticHandle(InvoiceEvent event) {
        doSomeJob();
    }

    private static void staticHandle(OrderEvent event) {
        doSomeJob();
    }

    private void handle(PaymentEvent event) {
        doSomeJob();
    }

    private void handle(InvoiceEvent event) {
        doSomeJob();
    }

    private void handle(OrderEvent event) {
        doSomeJob();
    }

    private static void doSomeJob() {
        Blackhole.consumeCPU(16);
    }

    private enum EventType {
        ORDER, INVOICE, PAYMENT
    }

    public static class Event {
        public EventType getEventType() {
            return eventType;
        }

        public void setEventType(EventType eventType) {
            this.eventType = eventType;
        }

        private EventType eventType;

        public double getD() {
            return d;
        }

        public void setD(double d) {
            this.d = d;
        }


        private double d;
    }

    public static class OrderEvent extends Event {
    }

    @State(Scope.Thread)
    public static class InvoiceEvent extends Event {
        @Setup(Level.Trial)
        public void doSetup() {
            this.setEventType(EventType.INVOICE);
        }
    }

    public static class PaymentEvent extends Event {
    }
}
3
Brian Goetz On

The statement of the question is a bit of an "XY problem"; what you want is totality checking, but you're asking for it to be treated as an expression, not because you want an expression, but because you want the totality checking that comes with expression-hood.

One of the items of "technical debt" left from the addition of switch expressions is the ability for switch statements to opt into the same totality checking that switch expressions get. We could not retroactively change this about switch statements -- switch statements have always been allowed to be partial -- but you are correct that it would be nice to be able to get this sort of type checking. As you surmise, turning it into a void expression switch is one way to get there, but it is indeed ugly, and worse, will not be easily discoverable. It is on our list to find a way to allow you to opt back into totality checking for switch statements. There have been discussions on the amber-spec-experts list about this; it is related to several other possible features, and design discussions are still ongoing.

2
DuncG On

If you have test classes (say JUNIT test cases) which you build and run before releasing your main code, then you could drop a simple guard function into any existing test class for each enum you want to watch:

String checkForEnumChanged(YourEnum guard) {
    return switch (guard) {
        case ORDER -> "OK";
        case INVOICE -> "OK";
        case PAYMENT -> "OK";
    };
}

This means you can keep your main application code clear of the yield 0; style of switch and get a compile error in the test classes when the enum values are edited.

0
Thiyanesh On

Add a delegate

Add a delegate method to forward the request and return a Void type

public class SwitchTest {
    
    enum EventType {
        ORDER,
        INVOICE,
        PARCELDELIVERY
    }

    interface Event {

        EventType getType();
    }

    static class OrderType implements Event {

        @Override
        public EventType getType() {
            return EventType.ORDER;
        }
    }

    static class InvoiceType implements Event {

        @Override
        public EventType getType() {
            return EventType.INVOICE;
        }
    }

    static void handle(Event e) {
        System.out.println(e.getType());
    }

    static Void switchExpressionDelegate(Event e) {
        handle(e);
        return null;
    }

    public static void main(String[] args) {
        Event event = new OrderType();
        Void nullNoop = switch (event.getType()) {
            case ORDER -> switchExpressionDelegate(event);
            case INVOICE -> switchExpressionDelegate(event);
            case PARCELDELIVERY -> switchExpressionDelegate(event);
        };
    }
}

Exact type

Assuming the handle method has exact type, then a parallel hierarchy of delegate methods have to be added. (this does not look good though)


    static Void switchExpressionDelegate(OrderType e) {
        handle(e);
        return null;
    }

    static Void switchExpressionDelegate(InvoiceType e) {
        handle(e);
        return null;
    }

    public static void main(String[] args) {
        Event event = new OrderType();
        Void nullNoop = switch (event.getType()) {
            case ORDER -> switchExpressionDelegate((OrderType) event);
            case INVOICE -> switchExpressionDelegate((InvoiceType) event);
            case PARCELDELIVERY -> switchExpressionDelegate((OrderType) event); // can throw error in an actual implementation
        };
    }

Adapter

If adding new classes is an option, then adapter classes can be added

All the above looks round about

As pointed by other answer by sambabcde, the best option seems to be using a Consumer

    public static void main(String[] args) {
        Event event = new OrderType();
        Consumer<Void> nullNoop = switch (event.getType()) {
            case ORDER -> e -> handle((OrderType) event);
            case INVOICE -> e -> handle((InvoiceType) event);
            case PARCELDELIVERY -> e -> handle((OrderType) event);
        };
        nullNoop.accept(null);
    }
0
Aliaksei On

How about Runnable :

Runnable limitOperationRunnable = switch (limitOperation) {
  case INSERT -> () -> ...;
  case UPDATE -> () -> ...;
  case DELETE -> () -> ...;
};
limitOperationRunnable.run();