Book Image

Java Coding Problems - Second Edition

By : Anghel Leonard
Book Image

Java Coding Problems - Second Edition

By: Anghel Leonard

Overview of this book

The super-fast evolution of the JDK between versions 12 and 21 has made the learning curve of modern Java steeper, and increased the time needed to learn it. This book will make your learning journey quicker and increase your willingness to try Java’s new features by explaining the correct practices and decisions related to complexity, performance, readability, and more. Java Coding Problems takes you through Java’s latest features but doesn’t always advocate the use of new solutions — instead, it focuses on revealing the trade-offs involved in deciding what the best solution is for a certain problem. There are more than two hundred brand new and carefully selected problems in this second edition, chosen to highlight and cover the core everyday challenges of a Java programmer. Apart from providing a comprehensive compendium of problem solutions based on real-world examples, this book will also give you the confidence to answer questions relating to matching particular streams and methods to various problems. By the end of this book you will have gained a strong understanding of Java’s new features and have the confidence to develop and choose the right solutions to your problems.
Table of Contents (16 chapters)
1
Text Blocks, Locales, Numbers, and Math
Free Chapter
2
Objects, Immutability, Switch Expressions, and Pattern Matching
14
Other Books You May Enjoy
15
Index

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.