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.
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.
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);
Based on the comment concerning performance penalty, a benchmark is performed to compare following scenarios:
To see
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.
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 {
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With