JDK 17 - Switch Case Performance
The new switch statement is going to be very useful but if we can write our code both ways e.g., old and new, which will be preferred? Let's compare the performance of both implementations.
Join the DZone community and get the full member experience.
Join For FreeSwitch statements in Java have evolved over time. Switch statements supported primitive types at the beginning, then came along String, now JDK17 stable release is out and we have pattern matching which allows us to pass different object types to the switch statement.
We can now have switch statements like this: (example taken from Oracle JavaSE 17 docs)
record Point(int x, int y) { }
enum Color { RED, GREEN, BLUE; }
...
static void typeTester(Object obj) {
switch (obj) {
case null -> System.out.println("null");
case String s -> System.out.println("String");
case Color c -> System.out.println("Color with " + c.values().length + " values");
case Point p -> System.out.println("Record class: " + p.toString());
case int[] ia -> System.out.println("Array of int values of length" + ia.length);
default -> System.out.println("Something else");
}
}
It's obvious that the new switch statement will be very useful. It will change how we code, make the code cleaner and shorter, make it more readable in some cases, and so on. However, if we come to a point where we can write our code both ways (the old way with primitives and so on, and the new way with pattern matching), which one will be preferred? Coding time, readability, memory usage, and performance are the criteria we should consider. Let's look at the performance of the two implementation ways and compare them.
There will be unlimited types of implementations with the new pattern matching switch but we will try to focus on the switch case by creating a simple scenario.
Let's have four classes (ClassAddition, ClassSubtraction, ClassMultiplication, ClassDivision) extended from abstract class ClassOperation which has a number of variables and typeId representing the operation (1: add, 2:subtract, 3:multiply, 4:divide) that will be done.
The code for this looks like this:
public abstract class ClassOperation {
private int typeId;
private double number;
public int getTypeId() {return typeId;}
public void setTypeId(int typeId) {this.typeId = typeId;}
public double getNumber() {return number;}
public void setNumber(double number) {this.number = number;}
}
public class ClassAddition extends ClassOperation {
public ClassAddition(double number){
setTypeId(OperationTypeEnum.ADDITION.getId());
setNumber(number);
}
}
public class ClassSubtraction extends ClassOperation {
public ClassSubtraction(double number){
setTypeId(OperationTypeEnum.SUBTRACTION.getId());
setNumber(number);
}
}
public class ClassMultiplication extends ClassOperation{
public ClassMultiplication(double number){
setTypeId(OperationTypeEnum.MULTIPLICATION.getId());
setNumber(number);
}
}
public class ClassDivision extends ClassOperation{
public ClassDivision(double number){
setTypeId(OperationTypeEnum.DIVISION.getId());
setNumber(number);
}
}
public enum OperationTypeEnum {
ADDITION(1),
SUBTRACTION(2),
MULTIPLICATION(3),
DIVISION(4);
OperationTypeEnum(int id){
this.id = id;
}
public static OperationTypeEnum findById(int id){
for (OperationTypeEnum ot : OperationTypeEnum.values()){
if(ot.getId() == id){
return ot;
}
}
return null;
}
private int id;
public int getId(){
return id;
}
}
Now let's create the methods that will fill a list randomly with these classes with random values.
private static void prepareData(long listSize){
operationList.clear();
for (long i = 0; i < listSize; i++){
int typeId = (int) Math.floor(Math.random() * 4 + 1);
double number = Math.floor(Math.random()*5 + 1);
operationList.add(getObject(typeId, number));
}
}
private static Object getObject(int typeId, double number){
OperationTypeEnum ot = OperationTypeEnum.findById(typeId);
switch (ot){
case ADDITION:
return new ClassAddition(number);
case SUBTRACTION:
return new ClassSubtraction(number);
case MULTIPLICATION:
return new ClassMultiplication(number);
case DIVISION:
return new ClassDivision(number);
default:
return null;
}
}
Afterward, let's calculate the total of these lists in both switch statement styles.
The New Switch:
private static void calculateUsingNewSwitch(){
BigDecimal total = BigDecimal.ZERO;
long startTime = System.nanoTime();
for (Object obj : operationList){
switch(obj){
case ClassAddition ca -> total = total.add(new BigDecimal(ca.getNumber()));
case ClassSubtraction cs -> total = total.subtract(new BigDecimal(cs.getNumber()));
case ClassMultiplication cm -> total = total.multiply(new BigDecimal(cm.getNumber()));
case ClassDivision cd -> total = total.divide(new BigDecimal(cd.getNumber()), 2, RoundingMode.HALF_UP);
default -> System.out.println("unknown type for new switch");
};
}
long endTime = System.nanoTime();
double milliseconds = (double) (endTime - startTime) / 1_000_000;
System.out.println (operationList.size() + " -> new switch -> took: " + milliseconds);
}
The Old Switch:
private static void calculateUsingOldSwitch(){
BigDecimal total = BigDecimal.ZERO;
long startTime = System.nanoTime();
for (Object obj : operationList){
OperationTypeEnum ot = OperationTypeEnum.findById(((ClassOperation)obj).getTypeId());
switch(ot){
case ADDITION:
total = total.add(new BigDecimal(((ClassAddition) obj).getNumber()));
break;
case SUBTRACTION:
total = total.subtract(new BigDecimal(((ClassSubtraction)obj).getNumber()));
break;
case MULTIPLICATION:
total = total.multiply(new BigDecimal(((ClassMultiplication)obj).getNumber()));
break;
case DIVISION:
total = total.divide(new BigDecimal(((ClassDivision)obj).getNumber()), 2, RoundingMode.HALF_UP);
break;
default: System.out.println("unknown type for old switch");
break;
};
}
long endTime = System.nanoTime();
double milliseconds = (double) (endTime - startTime) / 1_000_000;
System.out.println (operationList.size() + " -> old switch -> took: " + milliseconds);
}
Now, let's run our tests 5 times for 5 different sets:
- 10.000
- 100.000
- 1.000.000
- 10.000.000
- 100.000.000
public static List<Object> operationList;
public static void main(String[] args) {
operationList = new ArrayList<>();
for (int t = 1; t <= 5; t++) {
long size = 10000;
for (int i = 1; i <= 5; i++) {
try {
runTest(size);
} catch (Exception e) {
System.out.println(e.toString());
}
size = size * 10;
}
}
}-
And the results are:
RUN2 | RUN3 | RUN4 | RUN5 | AVG | ||||||
NEW | OLD | NEW | OLD | NEW | OLD | NEW | OLD | NEW | OLD | |
10.000 | 6,7 | 3,1 | 4,2 | 3,0 | 3,0 | 1,8 | 2,2 | 2,5 | 4,0 | 2,6 |
100.000 | 41,1 | 36,9 | 38,9 | 36,4 | 29,8 | 34,6 | 18,6 | 18,5 | 32,1 | 31,6 |
1.000.000 | 367,5 | 387,6 | 355,4 | 247,0 | 499,2 | 562,8 | 341,3 | 345,7 | 390,9 | 385,8 |
10.000.000 | 5.512,8 | 5.070,6 | 4.750,4 | 4.192,6 | 6.940,2 | 6.836,5 | 4.450,2 | 3.735,6 | 5.413,4 | 4.958,8 |
100.000.000 | 95.391,9 | 95.331,8 | 101.763,1 | 100.890,0 | 106.325,7 | 102.622,2 | 182.330,1 | 186.933,0 | 121.452,7 | 121.444,2 |
As you may have noticed, typecast in the old switch was not necessary. They were done in order to have the same operations being executed on both examples so the only difference would be the switch statement itself.
Let's make the following changes get rid of extra type casts and re-run the test:
- Change the type of the List from Object to ClassOperation
- Change the return type of getObject from Object to ClassOperation
- Remove the ClassOperation type casts from both switches
- Remove the remaining type casts from the old switch
After these steps, our old switch calculation method would look like this:
private static void calculateUsingOldSwitch(){
BigDecimal total = BigDecimal.ZERO;
long startTime = System.nanoTime();
for (ClassOperation obj : operationList){
OperationTypeEnum ot = OperationTypeEnum.findById(obj.getTypeId());
switch(ot){
case ADDITION:
total = total.add(new BigDecimal(obj.getNumber()));
break;
case SUBTRACTION:
total = total.subtract(new BigDecimal(obj.getNumber()));
break;
case MULTIPLICATION:
total = total.multiply(new BigDecimal(obj.getNumber()));
break;
case DIVISION:
total = total.divide(new BigDecimal(obj.getNumber()), 2, RoundingMode.HALF_UP);
break;
default: System.out.println("unknown type for old switch");
break;
};
}
long endTime = System.nanoTime();
double milliseconds = (double) (endTime - startTime) / 1_000_000;
System.out.println (operationList.size() + " -> old switch -> took: " + milliseconds);
}
If we re-run the tests, the result is:
RUN2 | RUN3 | RUN4 | RUN5 | AVG | ||||||
NEW | OLD | NEW | OLD | NEW | OLD | NEW | OLD | NEW | OLD | |
10.000 | 1,5 | 1,5 | 1,7 | 1,7 | 1,7 | 1,7 | 1,6 | 1,6 | 1,6 | 1,6 |
100.000 | 21,6 | 21,4 | 23,6 | 22,1 | 23,0 | 22,5 | 25,1 | 24,5 | 23,3 | 22,6 |
1.000.000 | 244,7 | 229,6 | 271,8 | 268,5 | 278,9 | 255,4 | 282,7 | 235,5 | 269,5 | 247,2 |
10.000.000 | 6.792,8 | 6.293,1 | 4.858,4 | 4.563,7 | 5.288,4 | 4.384,5 | 4.994,7 | 4.541,1 | 5.483,6 | 4.945,6 |
100.000.000 | 103.260,7 | 102.734,4 | 132.283,3 | 132.437,4 | 227.572,2 | 223.576,9 | 74.045,6 | 71.794,5 | 134.290,5 | 132.635,8 |
As you can see from the results, the performance of the new switch and the old switch with type casts can be considered as same. The performance of the old switch without typecast is slightly better.
When it comes to a point where we can choose one of the two ways, which one we choose won't make a big difference as performance. Of course, our result can not imply that it will always be the same for every usage but it can give an idea.
Opinions expressed by DZone contributors are their own.
Comments