Reader small image

You're reading from  Java Coding Problems - Second Edition

Product typeBook
Published inMar 2024
PublisherPackt
ISBN-139781837633944
Edition2nd Edition
Right arrow
Author (1)
Anghel Leonard
Anghel Leonard
author image
Anghel Leonard

Anghel Leonard is a Chief Technology Strategist and independent consultant with 20+ years of experience in the Java ecosystem. In daily work, he is focused on architecting and developing Java distributed applications that empower robust architectures, clean code, and high-performance. Also passionate about coaching, mentoring and technical leadership. He is the author of several books, videos and dozens of articles related to Java technologies.
Read more about Anghel Leonard

Right arrow

66. Dealing with completeness (type coverage) in pattern labels for switch

In a nutshell, switch expressions and switch statements that use null and/or pattern labels should be exhaustive. In other words, we must cover with explicit switch case labels all the possible values. Let’s consider the following example:

class Vehicle {}
class Car extends Vehicle {}
class Van extends Vehicle {}
private static String whatAmI(Vehicle vehicle) {
  return switch(vehicle) {
    case Car car -> "You're a car";
    case Van van -> "You're a van";
  };
}

This snippet of code doesn’t compile. The error is clear: The switch expression does not cover all possible input values. The compiler complains because we don’t have a case pattern label for Vehicle. This base class can be legitimately used without being a Car or a Van, so it is a valid candidate for our switch. We can add a case Vehicle or a default label. If you know that Vehicle will remain an empty base class, then you’ll probably go for a default label:

return switch(vehicle) {
    case Car car -> "You're a car";
    case Van van -> "You're a van";
    default -> "I have no idea ... what are you?";
  };

If we continue by adding another vehicle such as class Truck extends Vehicle {}, then this will be handled by the default branch. If we plan to use Vehicle as an independent class (for instance, to enrich it with methods and functionalities), then we will prefer to add a case Vehicle as follows:

return switch(vehicle) {
    case Car car -> "You're a car";
    case Van van -> "You're a van";
    case Vehicle v -> "You're a vehicle"; // total pattern
};

This time, the Truck class will match the case Vehicle branch. Of course, we can add a case Truck as well.

Important note

The Vehicle v pattern is named a total type pattern. There are two labels that we can use to match all possible values: the total type pattern (for instance, a base class or an interface) and the default label. Generally speaking, a total pattern is a pattern that can be used instead of the default label.

In the previous example, we can accommodate all possible values via the total pattern or the default label but not both. This makes sense since the whatAmI(Vehicle vehicle) method gets Vehicle as an argument. So, in this example, the selector expression can be only Vehicle or a subclass of Vehicle. How about modifying this method as whatAmI(Object o)?

private static String whatAmI(Object o) {
  return switch(o) {
    case Car car -> "You're a car";
    case Van van -> "You're a van";
    case Vehicle v -> "You're a vehicle"; // optional
    default -> "I have no idea ... what are you?";
  };
}

Now, the selector expression can be any type, which means that the total pattern Vehicle v is not total anymore. While Vehicle v becomes an optional ordinary pattern, the new total pattern is case Object obj. This means that we can cover all possible values by adding the default label or the case Object obj total pattern:

return switch(o) {
  case Car car -> "You're a car";
  case Van van -> "You're a van";
  case Vehicle v -> "You're a vehicle";  // optional
  case Object obj -> "You're an object"; // total pattern
};

I think you get the idea! How about using an interface for the base type? For instance, here is an example based on the Java built-in CharSequence interface:

public static String whatAmI(CharSequence cs) {
  return switch(cs) { 
    case String str -> "You're a string";
    case Segment segment -> "You're a Segment";
    case CharBuffer charbuffer -> "You're a CharBuffer";
    case StringBuffer strbuffer -> "You're a StringBuffer";
    case StringBuilder strbuilder -> "You're a StringBuilder";
  };
}

This snippet of code doesn’t compile. The error is clear: The switch expression does not cover all possible input values. But, if we check the documentation of CharSequence, we see that it is implemented by five classes: CharBuffer, Segment, String, StringBuffer, and StringBuilder. In our code, each of these classes is covered by a pattern label, so we have covered all possible values, right? Well, yes and no… “Yes” because we cover all possible values for the moment, and “no” because anyone can implement the CharSequence interface, which will break the exhaustive coverage of our switch. We can do this:

public class CoolChar implements CharSequence { ... }

At this moment, the switch expression doesn’t cover the CoolChar type. So, we still need a default label or the total pattern, case CharSequence charseq, as follows:

return switch(cs) { 
  case String str -> "You're a string";
  ...
  case StringBuilder strbuilder -> "You're a StringBuilder";
  // we have created this
  case CoolChar cool -> "Welcome ... you're a CoolChar";
  // this is a total pattern
  case CharSequence charseq -> "You're a CharSequence";
  // can be used instead of the total pattern
  // default -> "I have no idea ... what are you?";
};

Okay, let’s tackle this scenario on the java.lang.constant.ClassDesc built-in interface:

private static String whatAmI(ConstantDesc constantDesc) {
  return switch(constantDesc) { 
    case Integer i -> "You're an Integer";
    case Long l -> "You're a Long";
    case Float f -> " You're a Float";
    case Double d -> "You're a Double";
    case String s -> "You're a String";
    case ClassDesc cd -> "You're a ClassDesc";
    case DynamicConstantDesc dcd -> "You're a DCD";
    case MethodHandleDesc mhd -> "You're a MethodHandleDesc";
    case MethodTypeDesc mtd -> "You're a MethodTypeDesc";
  };
}

This code compiles! There is no default label and no total pattern but the switch expression covers all possible values. How so?! This interface is declared as sealed via the sealed modifier:

public sealed interface ClassDesc
  extends ConstantDesc, TypeDescriptor.OfField<ClassDesc>

Sealed interfaces/classes were introduced in JDK 17 (JEP 409) and we will cover this topic in Chapter 8. However, for now, it is enough to know that sealing allows us to have fine-grained control of inheritance so classes and interfaces define their permitted subtypes. This means that the compiler can determine all possible values in a switch expression. Let’s consider a simpler example that starts as follows:

sealed interface Player {}
final class Tennis implements Player {}
final class Football implements Player {}
final class Snooker implements Player {}

And, let’s have a switch expression covering all possible values for Player:

private static String trainPlayer(Player p) { 
  return switch (p) {
    case Tennis t -> "Training the tennis player ..." + t;
    case Football f -> "Training the football player ..." + f;
    case Snooker s -> "Training the snooker player ..." + s;
  };
}

The compiler is aware that the Player interface has only three implementations and all of them are covered via pattern labels. We can add a default label or the total pattern case Player player, but you most probably don’t want to do that. Imagine that we add a new implementation of the sealed Player interface named Golf:

final class Golf implements Player {}

If the switch expression has a default label, then Golf values will be handled by this default branch. If we have the total pattern Player player, then this pattern will handle the Golf values. On the other hand, if none of the default labels or total patterns are present, the compiler will immediately complain that the switch expression doesn’t cover all possible values. So, we are immediately informed, and once we add a case Golf g, the error disappears. This way, we can easily maintain our code and have a guarantee that our switch expressions are always up to date and cover all possible values. The compiler will never miss the chance to inform us when a new implementation of Player is available.

A similar logic applies to Java enums. Consider the following enum:

private enum PlayerTypes { TENNIS, FOOTBALL, SNOOKER }

The compiler is aware of all the possible values for PlayerTypes, so the following switch expression compiles successfully:

private static String createPlayer(PlayerTypes p) { 
  return switch (p) {
    case TENNIS -> "Creating a tennis player ...";
    case FOOTBALL -> "Creating a football player ...";
    case SNOOKER -> "Creating a snooker player ...";
  };
}

Again, we can add a default label or the total pattern, case PlayerTypes pt. But, if we add a new value in the enum (for instance, GOLF), the compiler will delegate the default label or the total pattern to handle it. On the other hand, if none of these are available, the compiler will immediately complain that the GOLF value is not covered, so we can add it (case GOLF g) and create a golf player whenever required.

So far, so good! Now, let’s consider the following context:

final static class PlayerClub implements Sport {};
private enum PlayerTypes implements Sport
  { TENNIS, FOOTBALL, SNOOKER }
sealed interface Sport permits PlayerTypes, PlayerClub {};

The sealed interface Sport allows only two subtypes: PlayerClub (a class) and PlayerTypes (an enum). If we write a switch that covers all possible values for Sport, then it will look as follows:

private static String createPlayerOrClub(Sport s) { 
  return switch (s) {
    case PlayerTypes p when p == PlayerTypes.TENNIS
      -> "Creating a tennis player ...";
    case PlayerTypes p when p == PlayerTypes.FOOTBALL
      -> "Creating a football player ...";
    case PlayerTypes p -> "Creating a snooker player ...";
    case PlayerClub p -> "Creating a sport club ...";
  };
}

We immediately observe that writing case PlayerTypes p when p == PlayerTypes.TENNIS is not quite neat. What we actually want is case PlayerTypes.TENNIS but, until JDK 21, this is not possible since qualified enum constants cannot be used in case labels. However, starting with JDK 21, we can use qualified names of enum constants as labels, so we can write this:

private static String createPlayerOrClub(Sport s) {
  return switch (s) {
    case PlayerTypes.TENNIS
      -> "Creating a tennis player ...";
    case PlayerTypes.FOOTBALL
      -> "Creating a football player ...";
    case PlayerTypes.SNOOKER
      -> "Creating a snooker player ...";
    case PlayerClub p 
      -> "Creating a sport club ...";
  };
}

Done! Now you know how to deal with type coverage in switch expressions.

Previous PageNext Page
You have been reading a chapter from
Java Coding Problems - Second Edition
Published in: Mar 2024Publisher: PacktISBN-13: 9781837633944
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at ₹800/month. Cancel anytime

Author (1)

author image
Anghel Leonard

Anghel Leonard is a Chief Technology Strategist and independent consultant with 20+ years of experience in the Java ecosystem. In daily work, he is focused on architecting and developing Java distributed applications that empower robust architectures, clean code, and high-performance. Also passionate about coaching, mentoring and technical leadership. He is the author of several books, videos and dozens of articles related to Java technologies.
Read more about Anghel Leonard