Navigation
Recherche
|
Basic and advanced pattern matching in Java
jeudi 31 juillet 2025, 11:00 , par InfoWorld
Pattern matching is a way to simplify your code by checking if a value fits a certain structure or type, without having to write messy, repetitive checks. Instead of using multiple if statements and manual type casting, you can simply let Java do the heavy lifting.
Developers use pattern matching to reduce unnecessary work in their code. For example, you can use pattern matching to find out if a Java Object is a String. Here are some advantages of pattern matching: Less boilerplate code: No more writing extra instanceof checks followed by casting. Pattern matching results in code that is shorter and cleaner. More readable code: The code’s logic is easier to follow because the pattern performs type checking and variable binding in one step. Fewer bugs: Since Java handles the type casting automatically, you avoid mistakes like ClassCastExceptions. In short, pattern matching makes your Java code simpler, safer, and easier to read. If you’re using Java 16 or later, you should start using it today. What Java editions support pattern matching? Java 16 introduced basic pattern matching with the finalized instanceof method. For more advanced features, like pattern matching in switch, record patterns, and guarded cases, you need Java 21+. Pattern matching basics Pattern matching simplifies your Java code by allowing you to check an object’s type and use it in one clean step. No more messy type checking and casting; Java does it for you automatically. Remember how bad class casting looks in your code? Pattern matching solves this problem gracefully. Instead of writing: if (object instanceof String) { String s = (String) object; System.out.println(s.length()); } You can just write: if (object instanceof String s) { System.out.println(s.length()); } The difference is clear: No redundant casting: The variable s is ready to use as a String. Cleaner logic: Combines type-checking and assignment in one line. Fewer bugs: Removes the risk of a ClassCastException from manual casting. The cast is created, and then you declare the variable with the desired type in the same line of code. Pattern matching for switch (Java 21+) In older Java versions, switch statements could only compare simple values, such as numbers or enums. But with pattern matching, switch can check an object’s type directly, making your code cleaner and more powerful. Let’s take a look. Type checking without pattern matching requires verbose code: if (obj instanceof String) { String s = (String) obj; System.out.println('It's a String: ' + s); } else if (obj instanceof Integer) { Integer i = (Integer) obj; System.out.println('It's an Integer: ' + i); } else { System.out.println('Unknown type'); } With switch and pattern matching, you can match types and extract variables in one step: switch (obj) { case String s -> System.out.println('It's a String: ' + s); case Integer i -> System.out.println('It's an Integer: ' + i); default -> System.out.println('Unknown type'); } This is a significant improvement over the old way, where switch could only match constants: switch (number) { case 1 -> System.out.println('One'); // Old: only constants case 2 -> System.out.println('Two'); } Some key takeaways from this example: Pattern matching works with any object type: This means no more long, if-else chains for type checks. It supports auto-casting: Variables (s, i) are ready to use with the correct type. The logic is clearer: With pattern matching, the structure of the code is flat and easy to read. Advanced features of pattern matching Java 16 introduced basic pattern matching with instanceof, but more advanced capabilities arrived in Java 21. This section shows you how to use record patterns, nested record patterns, and guarded patterns in Java programs. Record patterns Record patterns allow you to directly access the components of a record in one concise operation. This feature introduces powerful deconstruction capabilities to Java, similar to those found in functional programming languages. Record patterns also let you deconstruct records (Java’s immutable data classes) directly within pattern-matching contexts like instanceof checks and switch statements. Instead of retrieving each field separately, you can extract all the components in one step. Here’s an example of using a record pattern with instanceof: record Point(int x, int y) {} // Without record patterns if (p instanceof Point) { Point point = (Point) p; int x = point.x(); int y = point.y(); System.out.println(x + ', ' + y); } // With record patterns if (p instanceof Point(int x, int y)) { System.out.println(x + ', ' + y); // x and y are directly available } Record patterns shine even more when used in switch statements: Object obj = new Point(10, 20); switch (obj) { case Point(int x, int y) when x > 0 && y > 0 -> System.out.println('Point in first quadrant: ' + x + ', ' + y); case Point(int x, int y) -> System.out.println('Point elsewhere: ' + x + ', ' + y); default -> System.out.println('Not a point'); } Nested record patterns Nested record patterns let you look inside records that contain other records, all in one step. This makes working with complex data much simpler. You can use nested patterns to match and extract values from multiple levels of data structures at once. Instead of navigating through each level separately, you can directly access the inner components: // Define two simple records record Address(String city, String country) {} record Person(String name, Address address) {} // Create a person with an address Person person = new Person('Rafael', new Address('Sao Paulo', 'Brazil')); For contrast, here’s an example of matching and extracting values without nested patterns: // Multiple steps required if (person instanceof Person) { String name = person.name(); Address address = person.address(); String city = address.city(); String country = address.country(); System.out.println(name + ' lives in ' + city + ', ' + country); } And here’s the same code with nested patterns: // One clean pattern if (person instanceof Person(String name, Address(String city, String country))) { // All variables are immediately available System.out.println(name + ' lives in ' + city + ', ' + country); } Like regular record patterns, nested record patterns can be used in switch statements. In the first case clause of the following code, we have the String “Ireland”, which stipulates that the condition will be met only if the person’s country is Ireland; if not, the other condition will be met: switch (person) { case Person(String name, Address(String city, 'Ireland')) -> System.out.println(name + ' lives in ' + city + ', Ireland'); case Person(String name, Address(String city, String country)) -> System.out.println(name + ' lives in ' + city + ', ' + country); } Once again, the benefits are clear: Less boilerplate: Nested record patterns let you extract all the components in a single operation. Improved readability: The pattern clearly shows what you are extracting. Type safety: The compiler ensures correct types for extracted variables. Nested deconstruction: The pattern seamlessly handles complex data structures. Nested patterns make your code more readable and maintainable, especially when working with complex data structures like configuration objects, domain models, or API responses. Guarded patterns (when clauses) Guarded patterns enhance pattern matching by letting you add specific conditions using the when keyword. This feature allows you to match not just by type or structure, but also by the values contained within the matched object. As shown here, a guarded pattern combines a regular pattern with a Boolean condition: switch (obj) { case String s when s.length() > 5 -> System.out.println('Long string: ' + s); case String s -> System.out.println('Short string: ' + s); default -> System.out.println('Not a string'); } Here’s how this code works: Java checks if the object matches the pattern (e.g., is it a String?). If it matches, Java binds the variable (s). Java evaluates the condition after when. If the condition is true, the case is selected. Let’s look at a couple of practical examples. In this first example, we use a guarded pattern for testing String content: switch (input) { case String s when s.startsWith(' -> System.out.println('HTTP URL'); case String s when s.startsWith('https://') -> System.out.println('Secure HTTPS URL'); case String s -> System.out.println('Not a URL'); } And here, we’re using the pattern with numbers: switch (num) { case Integer i when i < 0 -> System.out.println('Negative'); case Integer i when i == 0 -> System.out.println('Zero'); case Integer i when i > 0 -> System.out.println('Positive'); default -> System.out.println('Not an integer'); } The benefits of guarded patterns are: More precise matching: Target specific value conditions. Reduced code: Combine type checking and value testing in one step. Better readability: Clear, concise expression of complex conditions. Pattern matching with sealed classes Combining sealed classes with pattern matching is a powerful way to bring compile-time safety to your code. Examples in this section will show you why this combination is a game-changer. What are sealed classes? Introduced in Java 17, sealed classes are classes that explicitly define which other classes can extend or implement them. Think of a sealed class as creating a “closed club” of related types: sealed interface JavaMascot permits Duke, Juggy { } record Duke(String color, int yearIntroduced) implements JavaMascot { } record Juggy(String color, boolean isFlying) implements JavaMascot { } At first glance, this code doesn’t seem to help much, but the real benefit starts when we try something like the following: record Moby(String color, double tentacleLength) implements JavaMascot { } // Compilation error here Because the sealed interface JavaMascot only permits Duke and Juggy to implement it, the code above won’t compile! By restricting the code in that way, we avoid using a class we shouldn’t to implement the JavaMascot interface. This decreases the chances of bugs in our code. Remember, the more you restrict the code, the less likely you are to create bugs! Using sealed classes with switch When you combine sealed classes with switch in pattern matching, the compiler gains complete knowledge of all possible types. There are two major benefits to the compiler having such knowledge: (1) The compiler verifies you’ve handled all possible subtypes; (2) since all possibilities are known, you don’t need a default case. Do you see how sealed classes and switch work together in the following code? String describeMascot(JavaMascot mascot) { return switch (mascot) { case Duke(String color, int yearIntroduced) -> 'Duke (' + color + ') from ' + yearIntroduced; case Juggy(String color, boolean isFlying) -> 'Juggy (' + color + ')' + (isFlying? ' flying high': ''); // No default needed! The compiler knows these are all possibilities }; } Primitive type pattern matching in Java 23+ Java’s newer versions introduced a powerful preview feature that lets us check compatibility between primitive types using pattern matching. We can explore this feature with a few examples. Just remember that, since primitive type pattern matching is still a preview feature, it could be changed or removed in a future release. To execute the following examples, you need to turn on enable preview: java --enable-preview YourClassNameHere What is primitive type pattern matching? This feature enables us to determine if a value of one primitive type can be accurately represented in another primitive type, binding the value to a new variable if the test is successful. We can see this feature at work in the following integer compatibility example: int count = 98; if (count instanceof byte smallCount) { // This executes only if count fits in a byte's range System.out.println('Small enough: ' + smallCount); } else { System.out.println('Number too large for byte storage'); } Here, we’re checking if 98 can be stored in a byte. Since it’s between -128 and 127, the condition succeeds. Consider another example, this one evaluating decimal precision: double measurement = 17.5; if (measurement instanceof float simpleMeasurement) { System.out.println('No precision loss: ' + simpleMeasurement); } else { System.out.println('Requires double precision'); } This verifies if the double value can be represented as a float without precision loss. Here’s an example using primitive type pattern matching with text characters: int codePoint = 90; if (codePoint instanceof char symbol) { System.out.println('This represents: '' + symbol + '''); } else { System.out.println('Not a valid character code'); } The output from this code would be: This represents: ‘Z’, because 90 is the ASCII/Unicode value for Z. Finally, here’s a demonstration showing multiple type compatibility checks: void examineNumber(long input) { System.out.println('Examining: ' + input); if (input instanceof byte b) System.out.println('Fits in a byte variable: ' + b); if (input instanceof short s) System.out.println('Fits in a short variable: ' + s); if (input >= 0 && input
https://www.infoworld.com/article/4026690/basic-and-advanced-pattern-matching-in-java.html
Voir aussi |
56 sources (32 en français)
Date Actuelle
sam. 2 août - 07:28 CEST
|