65. Dealing with pattern label dominance in switch
The compiler matches the selector expression against the available pattern labels by testing the selector expression against each label starting from top to bottom (or, from the first to the last) in the exact order in which we wrote them in the switch
block. This means that the first match wins. Let’s assume that we have the following base class (Pill
) and some pills (Nurofen
, Ibuprofen
, and Piafen
):
abstract class Pill {}
class Nurofen extends Pill {}
class Ibuprofen extends Pill {}
class Piafen extends Pill {}
Hierarchically speaking, Nurofen
, Ibuprofen
, and Piafen
are three classes placed at the same hierarchical level since all of them have the Pill
class as the base class. In an IS-A inheritance relationship, we say that Nurofen
is a Pill
, Ibuprofen
is a Pill
, and Piafen
is also a Pill
. Next, let’s use a switch
to serve our clients the proper headache pill:
private static String headache(Pill o) {
return switch(o) {
case Nurofen nurofen -> "Get Nurofen ...";
case Ibuprofen ibuprofen -> "Get Ibuprofen ...";
case Piafen piafen -> "Get Piafen ...";
default -> "Sorry, we cannot solve your headache!";
};
}
Calling headache(new Nurofen())
will match the first pattern label, Nurofen nurofen
. In the same manner, headache(new Ibuprofen())
matches the second pattern label, and headache(new Piafen())
matches the third one. No matter how we mix the order of these label cases, they will work as expected because they are on the same level and none of them dominate the others.
For instance, since people don’t want headaches, they order a lot of Nurofen, so we don’t have any anymore. We represent this by removing/comment the corresponding case:
return switch(o) {
// case Nurofen nurofen -> "Get Nurofen ...";
case Ibuprofen ibuprofen -> "Get Ibuprofen ...";
case Piafen piafen -> "Get Piafen ...";
default -> "Sorry, we cannot solve your headache!";
};
So, what happens when a client wants Nurofen? You’re right … the default
branch will take action since Ibuprofen
and Piafen
don’t match the selector expression.
But, what will happen if we modify the switch
as follows?
return switch(o) {
case Pill pill -> "Get a headache pill ...";
case Nurofen nurofen -> "Get Nurofen ...";
case Ibuprofen ibuprofen -> "Get Ibuprofen ...";
case Piafen piafen -> "Get Piafen ...";
};
Adding the Pill
base class as a pattern label case allows us to remove the default
branch since we cover all possible values (this is covered in detail in Problem 66). This time, the compiler will raise an error to inform us that the Pill
label case dominates the rest of the label cases. Practically, the first label case Pill pill
dominates all other label cases because every value that matches any of the Nurofen nurofen
, Ibuprofen ibuprofen
, Piafen piafen
patterns also matches the pattern Pill pill
. So, Pill pill
always wins while the rest of the label cases are useless. Switching Pill pill
with Nurofen nurofen
will give a chance to Nurofen nurofen
, but Pill pill
will still dominate the remaining two. So, we can eliminate the dominance of the base class Pill
by moving its label case to the last position:
return switch(o) {
case Nurofen nurofen -> "Get Nurofen ...";
case Ibuprofen ibuprofen -> "Get Ibuprofen ...";
case Piafenpiafen -> "Get Piafen ...";
case Pill pill -> "Get a headache pill ...";
};
Now, every pattern label has a chance to win.
Let’s have another example that starts from this hierarchy:
abstract class Drink {}
class Small extends Drink {}
class Medium extends Small {}
class Large extends Medium {}
class Extra extends Medium {}
class Huge extends Large {}
class Jumbo extends Extra {}
This time, we have seven classes disposed of in a multi-level hierarchy. If we exclude the base class Drink
, we can represent the rest of them in a switch
as follows:
private static String buyDrink(Drink o) {
return switch(o) {
case Jumbo j: yield "We can give a Jumbo ...";
case Huge h: yield "We can give a Huge ...";
case Extra e: yield "We can give a Extra ...";
case Large l: yield "We can give a Large ...";
case Medium m: yield "We can give a Medium ...";
case Small s: yield "We can give a Small ...";
default: yield "Sorry, we don't have this drink!";
};
}
The order of pattern labels is imposed by the class hierarchy and is quite strict, but we can make some changes without creating any dominance issues. For instance, since Extra
and Large
are subclasses of Medium
, we can switch their positions. Some things apply to Jumbo
and Huge
since they are both subclasses of Medium
via Extra
, respectively Large
.
In this context, the compiler evaluates the selection expression by trying to match it against this hierarchy via an IS-A inheritance relationship. For instance, let’s order a Jumbo
drink while there are no more Jumbo
and Extra
drinks:
return switch(o) {
case Huge h: yield "We can give a Huge ...";
case Large l: yield "We can give a Large ...";
case Medium m: yield "We can give a Medium ...";
case Small s: yield "We can give a Small ...";
default: yield "Sorry, we don't have this drink!";
};
If we order Jumbo
(o
is Jumbo
), then we will get Medium
. Why? The compiler matches Jumbo
against Huge
without success. The same result is obtained while matching Jumbo
against Large
. However, when it matches Jumbo
against Medium
, it sees that Jumbo
is a Medium
subclass via the Extra
class. So, since Jumbo
is Medium
, the compiler chooses the Medium m
pattern label. At this point, Medium
matches Jumbo
, Extra
, and Medium
. So, soon we will be out of Medium
as well:
return switch(o) {
case Huge h: yield "We can give a Huge ...";
case Large l: yield "We can give a Large ...";
case Small s: yield "We can give a Small ...";
default: yield "Sorry, we don't have this drink!";
};
This time, any request for Jumbo
, Extra
, Medium
, or Small
will give us a Small
. I think you get the idea.
Let’s take a step further, and analyze this code:
private static int oneHundredDividedBy(Integer value) {
return switch(value) {
case Integer i -> 100/i;
case 0 -> 0;
};
}
Have you spotted the problem? A pattern label case dominates a constant label case, so the compiler will complain about the fact that the second case (case 0
) is dominated by the first case. This is normal, since 0 is an Integer
as well, so it will match the pattern label. The solution requires switching the cases:
return switch(value) {
case 0 -> 0;
case Integer i -> 100/i;
};
Here is another case to enforce this type of dominance:
enum Hero { CAPTAIN_AMERICA, IRON_MAN, HULK }
private static String callMyMarvelHero(Hero hero) {
return switch(hero) {
case Hero h -> "Calling " + h;
case HULK -> "Sorry, we cannot call this guy!";
};
}
In this case, the constant is HULK
and it is dominated by the Hero h
pattern label case. This is normal, since HULK
is also a Marvel hero, so Hero h
will match all Marvel heroes including HULK
. Again, the fix relies on switching the cases:
return switch(hero) {
case HULK -> "Sorry, we cannot call this guy!";
case Hero h -> "Calling " + h;
};
Okay, finally, let’s tackle this snippet of code:
private static int oneHundredDividedByPositive(Integer value){
return switch(value) {
case Integer i when i > 0 -> 100/i;
case 0 -> 0;
case Integer i -> (-1) * 100/i;
};
}
You may think that if we enforce the Integer i
pattern label with a condition that forces i
to be strictly positive, then the constant label will not be dominated. But, this is not true; a guarded pattern label still dominates a constant label. The proper order places the constant labels first, followed by guarded pattern labels, and finally, by non-guarded pattern labels. The next code fixes the previous one:
return switch(value) {
case 0 -> 0;
case Integer i when i > 0 -> 100/i;
case Integer i -> (-1) * 100/i;
};
Okay, I think you get the idea. Feel free to practice all these examples in the bundled code.