Additional Task
Using Records, Sealed Classes, and Pattern Matching
1 Training Task
Modify the previously created program for the individual assignment of laboratory trainings No. 3 and No. 4 by building the code using records, sealed classes, pattern matching, and unnamed variables. Limit yourself to implementing two derived classes on your own.
2 Instructions
2.1 Records
Starting with JDK 14, a new construct has been added to the Java syntax, the so-called records. In fact, it is a simplified class designed to store read-only data. Special syntax is used to create this class:
record PairRec(list of fields that are constructor's formal parameters) { }
Records provide constructor with parameters, access methods for reading, as well as toString(), equals() and hashCode() methods.
The record allows the programmer to explicitly implement necessary constructors and other methods, as well as static
functions. Records can also contain static fields.
The traditional approach to creating a class with read-only data involves creating the necessary fields, getters, and a constructor:
public class CircleReadOnly {private double radius;public CircleReadOnly(double radius) {this .radius = radius; }public double getRadius() {return radius; }public double area() {return Math.PI * radius * radius; }public double perimeter() {return 2 * Math.PI * radius; } }
Creating a record allows you to reduce the required code:
public record CircleRecord(double radius) {// radius field created automatically public double area() {return Math.PI * radius * radius; }public double perimeter() {return 2 * Math.PI * radius; } }
The following program demonstrates both implementations. When using a record, an automatically generated method radius() is
used instead of a getter:
public class TestReadOnly {public static void main(String[] args) {// Working with the class: CircleReadOnly circle1 =new CircleReadOnly(10); System.out.println(circle1.getRadius()); System.out.println(circle1.area()); System.out.println(circle1.perimeter());// Working with the record CircleRecord circle2 =new CircleRecord(10); System.out.println(circle2.radius()); System.out.println(circle2.area()); System.out.println(circle2.perimeter()); } }
If necessary, you can add a parameterless constructor to the record. It should call the automatically generated constructor:
public record CircleRecord(double radius) {public CircleRecord() {this (20); }// ... }
If there is more than one field, you can add constructors with fewer parameters. Such constructors should also call the automatically generated constructor.
Records (record type) do not support explicit inheritance, but they can implement
interfaces. Automatic generation of methods toString(), equals() and hashCode() provides
additional benefits and significantly reduces the minimum required code.
Records are most often used to implement the DTO (Data Transfer Object) design pattern, which is used to transfer structured data between different layers of an application. Data Transfer Objects (DTOs) contain no logic (only data). Read-only objects are reliable from a multithreading perspective. Records are often used in database applications to map rows of relational tables to Java objects.
2.2 Sealed Classes
Starting from the JDK 17 version to Java syntax was extended with the opportunity to limit the list of subclasses. This was done in order to better control the correctness of creating specific realizations of subclasses. To limit potential derived classes in previous versions, it was necessary to make the base class with package (non-public) visibility. But this approach made it impossible not only to inherit, but also any use of the class outside the package. In addition, there is sometimes a need to permit inheritance for classes located in other packages.
The new opportunity to determine such restrictions involves the use of so-called sealed classes. After the sealed class name, a list of permitted derived classes is placed:
public sealed class SealedBasepermits FirstDerived, SecondDerived {protected int data; }
Listed permitted subclasses must be accessible by the compiler. Such classes are defined with modifiers final or sealed.
In the latter case, an additional branch of allowed classes is created:
final class FirstDerivedextends SealedBase { }sealed class SecondDerivedextends SealedBasepermits SomeSubclass { }final class SomeSubclassextends SecondDerived { { data = 0; } }
Attempt to create other subclasses leads to an error:
class AnotherSubclassextends SealedBase {// Compiler error }
There is another modifier for the permitted derived class: non-sealed. You can create
any subclasses from such a class:
non-sealed class SecondDerivedextends SealedBase { }class PlainSubclassextends SecondDerived { { data = 0; } }
Permitted derived classes can be located in other packages.
2.3 Object Cloning
Sometimes there is a need to create a copy of some object, for example, to perform some actions that do not violate the original data. Simple assignment only copies references. If you need to copy an object memberwise, you should use the mechanism of the so-called cloning.
The base class java.lang.Object implements a function called clone(), which by default
allows you to perform memberwise copy the object. This function is also defined for arrays, strings, and other standard
classes. For example, you can get a copy of an existing array and work with this copy:
package ua.inf.iwanoff.java.third;import java.util.Arrays;public class ArrayClone {public static void main(String[] args) {int [] a1 = { 1, 2, 3, 4 };int [] a2 = a1.clone();// copy of items System.out.println(Arrays.toString(a2));// [1, 2, 3, 4] a1[0] = 10;// change the first array System.out.println(Arrays.toString(a1));// [10, 2, 3, 4] System.out.println(Arrays.toString(a2));// [1, 2, 3, 4] } }
In order to be able to clone objects of user classes, these classes must implement the Cloneable interface.
This interface does not declare any methods. It just indicates that objects of this class can be cloned. Otherwise,
calling the clone() function will throw an exception of the CloneNotSupportedException type.
For example, if we need to clone objects of the Human class, with two fields of String type
(name and surname), we'll add the implementation of the Cloneable interface
to the class description. Then we'll generate a constructor with two parameters and override toString() method.
In the main() function, we'll perform an object cloning test:
package ua.inf.iwanoff.java.third;public class Humanimplements Cloneable {private String name;private String surname;public Human(String name, String surname) {super ();this .name = name;this .surname = surname; } @Overridepublic String toString() {return name + " " + surname; }public static void main(String[] args)throws CloneNotSupportedException { Human human1 =new Human("John", "Smith"); Human human2 = (Human) human1.clone(); System.out.println(human2);// John Smith human1.name = "Mary"; System.out.println(human1);// Mary Smith System.out.println(human2);// John Smith } }
As you can see from the example, the source object can be modified after cloning. In this case, the copy does not change.
The clone() function can be overridden with changing its result type and making it open for ease of
use. Thanks to the availability of this function, cloning will be simplified (you will not need to convert the type
every time):
@Overridepublic Human clone()throws CloneNotSupportedException {return (Human)super .clone(); }// ... Human human2 = human1.clone();
The standard cloning implemented in the java.lang.Object class allows you to create copies of objects
whose fields are value types and String type (as well as wrapper classes). If the fields of the object
are references to arrays or other types, it is necessary to apply the so-called "deep" cloning. For example,
some class SomeCloneableClass contains two fields of type double and an array of integers. "Deep" cloning
will create separate arrays for different objects.
package ua.inf.iwanoff.java.third;import java.util.Arrays;public class SomeCloneableClassimplements Cloneable {private double x, y;private int [] a;public SomeCloneableClass(double x,double y,int [] a) {super ();this .x = x;this .y = y;this .a = a; } @Overrideprotected SomeCloneableClass clone()throws CloneNotSupportedException { SomeCloneableClass scc = (SomeCloneableClass)super .clone();// copy x and y scc.a = a.clone();// now two objects work with different arrays return scc; } @Overridepublic String toString() {return " x=" + x + " y=" + y + " a=" + Arrays.toString(a); }public static void main(String[] args)throws CloneNotSupportedException { SomeCloneableClass scc1 =new SomeCloneableClass(0.1, 0.2,new int [] { 1, 2, 3 }); SomeCloneableClass scc2 = scc1.clone(); scc2.a[2] = 4; System.out.println("scc1:" + scc1); System.out.println("scc2:" + scc2); } }
2.4 Pattern Matching
The pattern matching mechanism available in modern languages allows you to combine type checking and variable creation of the required type in a single expression. In fact, three actions are performed simultaneously:
- checking whether an object corresponds to a certain type;
- casting the object to the desired type if the check is successful and creating a local variable;
- using a variable (performing actions provided by the corresponding type).
The pattern matching mechanism was first introduced in Java 14. In subsequent versions, this mechanism was supplemented with additional features. In general, this mechanism allows you to make your code more compact and expressive. In addition, the code becomes more declarative and safe by combining three separate operations into one step.
Suppose a variable was created in the program, and then a reference to a string was written to it:
Object obj;// ... obj =new String("Some text");
If this string needs to be obtained, it is necessary to perform a type cast, which without proper checking leads
to the throwing of ClassCastException. Therefore, it is advisable to first check the type, then
perform the type cast, and then perform certain actions. The implementation of the traditional approach involves
manually performing these actions:
// ... if (objinstanceof String) {// type check String s = (String) obj;// cast the object to the desired type if (s.length() > 5) {// usage System.out.println(s); } }
The new approach involves combining these actions in a single expression:
if (objinstanceof String s && s.length() > 5) { System.out.println(s);// variable s is ready }
In addition to instanceof, pattern matching is used in switches to check the actual
type of objects. For example, you can not only perform different actions with a certain object, but also use the
value that refers to obj:
switch (obj) {case String s: System.out.println("String: " + s);break ;case Integer i: System.out.println("Number: " + i);break ;default : System.out.println("Unknown type"); }
You can additionally check for conditions related to specific types. This is done using the context-sensitive
keyword when:
switch (obj) {case Integer iwhen i > 0: System.out.println("Positive number: " + i);break ;case Integer iwhen i < 0: System.out.println("Negative number: " + i);break ;default : System.out.println("Unknown type or zero"); }
Java 25 has an additional feature that allows you to use primitive types for this type of checking (primitive patterns).
This allows you to work safely with numbers of different types. The following getGrade() function allows
you to get a score based on the number of points:
String getGrade(Number n) {
return switch (n) {
case int i when i >= 90 -> "A";
case int i when i >= 82 -> "B";
case int i when i >= 75 -> "C";
case int i when i >= 64 -> "D";
case int i when i >= 60 -> "E";
case double d when d >= 59.5 -> "E (rounded)";
default -> "F/FX";
};
}
void main() {
int mark = 88;
System.out.println(getGrade(mark)); // B
double roundedMark = 59.9;
System.out.println(getGrade(roundedMark)); // E (rounded)
}
The following advantages of using pattern matching can be listed:
- Type safety: the compiler guarantees that a variable will only be accessible where it is exactly initialized; an exception ClassCastException will not be thrown.
- Conciseness: the amount of boilerplate code is significantly reduced.
- Exhaustiveness: In conjunction with sealed classes, the compiler checks whether all types have been checked in the switch.
Record pattern matching allows you to extract the required record fields (unpack the record) directly
in the instanceof and switch constructs, without calling getters. For example, we have the following record:
record Pair(double x,double y) { }
Suppose a reference to and initialized with a record type object was created:
Object obj = new Pair(1, 2);
The traditional approach to getting values involves checking the reference type, creating a new variable, and casting the type:
if (objinstanceof Pair) { Pair p = (Pair) obj;double x = p.x();double y = p.y(); System.out.printf("x = %f y = %f\n", x, y); }
This code can be partially shortened using a pattern matching mechanism:
if (objinstanceof Pair p) {double x = p.x();double y = p.y(); System.out.printf("x = %f y = %f\n", x, y); }
But the most compact and expressive solution is one that uses a special pattern matching syntax for records:
if (objinstanceof Pair(double x,double y)) { System.out.printf("x = %f y = %f\n", x, y); }
Similarly, pattern matching can be applied to records in the switch construct:
record Pair(double x,double y) { }// ... public void printData(Object obj) {switch (obj) {case Pair(double x,double y) -> System.out.println("Pair: " + x + ", " + y);case String s -> System.out.println("String: " + s);default -> System.out.println("Unknown object"); } }
2.5 Unnamed Variables
When creating programs, situations very often arise when the syntax requires the creation of a variable (for example, an exception object, pattern matching, and a formal parameter of a lambda expression). Since during code analysis, the name of a variable implies its use, the presence of named variables.
Unnamed variables and patterns, denoted by the underscore character _, were introduced starting with
Java 21 to prevent code from being cluttered with variables that are technically needed but not logically needed.
You can use anonymous fields in record destructuring if they are not needed in a specific context, for example:
if (objinstanceof Pair(double x,double _)) { System.out.println("x = " + x); }
Unnamed variables are often useful for describing an exception object when we are not interested in the object itself, but only its type. For example:
String s =new Scanner(System.in).next();try {double d = Double.parseDouble(s); System.out.println(d); }catch (NumberFormatException _) { System.out.println("Wrong data!"); }
If a lambda function takes two arguments but only uses one, the second can be replaced with _.
// Ignore the value, use only the key in the map map.forEach((key, _) -> System.out.println("Key: " + key));// Or in loops where the current value is not important for (var _ : list) { count++; }
Overall, the code becomes cleaner, the number of compiler warnings is reduced, and you can't accidentally use
a variable _ because the compiler knows it doesn't have a name.
3 Sample Program
We previously considered the task of processing data about the country and the population census. The previously
created code can be modified using records, sealed classes, pattern matching, and unnamed variables. We can limit
ourselves to implementing two derived classes that will use a regular array and ArrayList.
For a population census, we can suggest a record instead of a class. This will significantly shorten the source code:
package ua.inf.iwanoff.java.additional;/** * The record is responsible for presenting the census. * The census is represented by year, population and comments */ public record Census(int year,int population, String comments)implements Comparable <Census> { @Overridepublic int compareTo(Census census) {return Integer.compare(population, census.population); } }
The separate class with the functions for searching data in comments has undergone minimal changes. Instead getComments() we use an automatically generated method comments():
package ua.inf.iwanoff.java.additional;import java.util.Arrays;/** * Provides static methods for searching data in a comment */ public class CensusUtilities {/** * Checks whether the word can be found in the comment text * @param census reference to a census * @param word a word that should be found in a comment * @return {@code true}, if the word is contained in the comment text * {@code false} otherwise */ public static boolean containsWord(Census census, String word) { String[] words = census.comments().split("\\s"); Arrays.sort(words);return Arrays.binarySearch(words, word) >= 0; }/** * Checks whether the substring can be found in the comment text * @param census reference to a census * @param substring a substring that should be found in a comment * @return {@code true}, if the substring is contained in the comment text * {@code false} otherwise */ public static boolean containsSubstring(Census census, String substring) {return census.comments().toUpperCase().contains(substring.toUpperCase()); }/** * Static method of adding a reference to census * to an array of censuses obtained as parameter * @param arr the array to which the census is added (must be not null) * @param item reference that is added * @return an updated array of censuses */ public static Census[] addToArray(Census[] arr, Census item) { Census[] newArr = Arrays.copyOf(arr, arr.length + 1); newArr[newArr.length - 1] = item;return newArr; } }
We will reimplement the base abstract class Country so that it allows the creation of only two derived
classes: CountryWithArray and CountryWithArrayList. The Country class code
could be as follows:
package ua.inf.iwanoff.java.additional;import java.util.Arrays;import java.util.Objects;Country /** * Abstract class for presenting the country in which the censuses are carried out. * Country is described by the name, area and sequence of censuses. * Access to the sequence of censuses is represented by abstract methods */ public abstract sealed class permits CountryWithArray, CountryWithArrayList {private String name;private double area;/** * Returns country name * @return country name */ public String getName() {return name; }/** * Sets country name * @param name country name */ public void setName(String name) {this .name = name; }/** * Returns the ara of the country * @return the ara of the country in the form of a floating point value */ public double getArea() {return area; }/** * Sets the ara of the country * @param area the ara of the country in the form of a floating point value */ public void setArea(double area) {this .area = area; }addCensus( /** * Returns reference to the census by index in a sequence * * <p> A subclass must provide an implementation of this method * * @param i census index * @return reference to the census with given index */ public abstract Census getCensus(int i);/** * Sets a reference to a new census within the sequence * according to the specified index. * * <p> A subclass must provide an implementation of this method * * @param i census index * @param census a reference to a new census */ public abstract void setCensus(int i, Census census);/** * Adds a reference to a new census to the end of the sequence * * <p> A subclass must provide an implementation of this method * * @param census a reference to a new census * @return {@code true} if the reference has been added * {@code false} otherwise */ public abstract boolean addCensus(Census census);/** * Creates a new census and adds a reference to it at the end of the array * @param year year of census * @param population population in the specified year * @param comments the text of the comment * @return {@code true}, if the reference has been added * {@code false} otherwise */ public booleanint year,int population, String comments) { Census census =new Census(year, population, comments);return addCensus(census); }/** * Returns the number of censuses in the sequence * * <p> A subclass must provide an implementation of this method * * @return count of censuses */ public abstract int censusesCount();/** * Removes all the censuses from the sequence * * <p> A subclass must provide an implementation of this method */ public abstract void clearCensuses();/** * Puts data from an array of censuses into a sequence * * <p> A subclass must provide an implementation of this method * * @param censusesArray array of references to censuses */ public abstract void setCensusesArray(Census[] censusesArray);/** * Returns an array of censuses obtained from the sequence * * <p> A subclass must provide an implementation of this method * * @return array of references to censuses */ public abstract Census[] getCensusesArray();/** * Checks whether this country is equivalent to another * @param obj country, equivalence with which we check * @return {@code true}, if two countries are the same * {@code false} otherwise @Overridepublic boolean equals(Object obj) {if (this == obj) {return true ; }if (!(objinstanceof Country c)) {return false ; }if (!getName().equals(c.getName()) || getArea() != c.getArea()) {return false ; }return Arrays.equals(getCensusesArray(), c.getCensusesArray()); }/** * Returns a hash code value for the country * @return a hash code value */ @Overridepublic int hashCode() {return Objects.hash(name, area, Arrays.hashCode(getCensusesArray())); } }
The class CountryWithArray must now be final:
package ua.inf.iwanoff.java.additional;CountryWithArray /** * Class for presenting the country in which the censuses are carried out. * Census data are represented with an array */ public final class extends Country {private Census[] censusesArray = {};/** * Returns reference to the census by index in a sequence * @param i census index * @return reference to the census with given index */ @Overridepublic Census getCensus(int i) {return censusesArray[i]; }/** * Sets a reference to a new census within the sequence * according to the specified index. * @param i census index * @param census a reference to a new census */ @Overridepublic void setCensus(int i, Census census) { censusesArray[i] = census; }/** * Adds a reference to a new census to the end of the sequence * @param census a reference to a new census * @return {@code true} if the reference has been added * {@code false} otherwise */ @Overridepublic boolean addCensus(Census census) {if (getCensusesArray() !=null ) {for (Census c : getCensusesArray()) {if (c.equals(census)) {return false ; } } } setCensusesArray(CensusUtilities.addToArray(getCensusesArray(), census));return true ; }/** * Returns the number of censuses in the sequence * @return count of censuses */ @Overridepublic int censusesCount() {return censusesArray.length; }/** * Removes all the censuses from the sequence */ @Overridepublic void clearCensuses() { censusesArray =new Census[0]; }/** * Returns an array of censuses obtained from the inner array * @return array of references to censuses */ @Overridepublic Census[] getCensusesArray() {return censusesArray; }/** * Puts data from an array of censuses into a sequence * @param censusesArray array of references to censuses */ @Overridepublic void setCensusesArray(Census[] censusesArray) {this .censusesArray = censusesArray; } }
The class CountryWithArrayList must also be final:
package ua.inf.iwanoff.java.additional;import java.util.ArrayList;import java.util.Arrays;import java.util.List;CountryWithArrayList /** * Class for presenting the country in which the censuses are carried out. * Census data are represented with ArrayList */ public final class extends Country {private List<Census> censusesList =new ArrayList<>();/** * Returns reference to the census by index in a sequence * @param i census index * @return reference to the census with given index */ @Overridepublic Census getCensus(int i) {return censusesList.get(i); }/** * Sets a reference to a new census within the sequence * according to the specified index. * @param i census index * @param census a reference to a new census */ @Overridepublic void setCensus(int i, Census census) { censusesList.set(i, census); }/** * Adds a reference to a new census to the end of the sequence * @param census a reference to a new census * @return {@code true} if the reference has been added * {@code false} otherwise */ @Overridepublic boolean addCensus(Census census) {if (censusesList.contains(census)) {return false ; }return censusesList.add(census); }/** * Returns the number of censuses in the sequence * @return count of censuses */ @Overridepublic int censusesCount() {return censusesList.size(); }/** * Removes all the censuses from the sequence */ @Overridepublic void clearCensuses() { censusesList.clear(); }/** * Puts data from an array of censuses into a sequence * @param censusesArray array of references to censuses */ @Overridepublic void setCensusesArray(Census[] censusesArray) { censusesList =new ArrayList<>(Arrays.asList(censusesArray)); }/** * Returns an array of censuses obtained from the sequence * @return array of references to censuses */ @Overridepublic Census[] getCensusesArray() {return censusesList.toArray(new Census[0]); }List<Census> getCensusesList() { /** * Returns a list of censuses * @return list of references to censuses */ public return censusesList; }setCensusesList(List<Census> censusesList) { /** * Puts a list of censuses into the object * @param censusesList arbitrary list of censuses */ public voidthis .censusesList = censusesList; } }
The class CountryUtilities practically does not need any changes. Only when working with censuses,
instead of getYear() and getPopulation() we use year() and population():
package ua.inf.iwanoff.java.additional;import java.util.Arrays;import java.util.Comparator;/** * Provides static methods for searching censuses */ public class CountryUtilities {/** * Returns the population density for the specified year * @param country reference to a country * @param year specified year (e.g. 1959, 1979, 1989, etc.) * @return population density for the specified year */ public static double density(Country country,int year) {for (int i = 0; i < country.censusesCount(); i++) {if (year == country.getCensus(i).year()) {return country.getCensus(i).population() / country.getArea(); } }return 0; }/** * Finds and returns a year with the maximum population * @param country reference to a country * @return year with the maximum population */ public static int maxYear(Country country) { Census census = country.getCensus(0);for (int i = 1; i < country.censusesCount(); i++) {if (census.population() < country.getCensus(i).population()) { census = country.getCensus(i); } }return census.year(); }/** * Creates and returns an array of censuses with the specified word in the comments * @param country reference to a country * @param word a word that is found * @return array of censuses with the specified word in the comments */ public static Census[] findWord(Country country, String word) { Census[] result = {};for (Census census : country.getCensusesArray()) {if (CensusUtilities.containsWord(census, word)) { result = CensusUtilities.addToArray(result, census); } }return result; }/** * Sorts the sequence of censuses by population * * @param country reference to a country */ public static void sortByPopulation(Country country) { Census[] censuses = country.getCensusesArray(); Arrays.sort(censuses); country.setCensusesArray(censuses); }/** * Sorts the sequence of censuses in the alphabetic order of comments * * @param country reference to a country */ public static void sortByComments(Country country) { Census[] censuses = country.getCensusesArray(); Arrays.sort(censuses, Comparator.comparing(Census::comments)); country.setCensusesArray(censuses); } }
The class StringRepresentations also requires almost no changes (except for the getters for the censuses):
package ua.inf.iwanoff.java.additional;/** * A class that allows getting representation * of various application objects in the form of strings */ public class StringRepresentations {/** * Provides a census data in the form of a string * * @param census reference to a census * @return string representation of a census data */ public static String toString(Census census) {return "The census in " + census.year() + ". Population: " + census.population() + ". Comments: " + census.comments(); }/** * Returns a string representation of the country * * @param country reference to a country * @return a string representation of the country */ public static String toString(Country country) { StringBuilder result =new StringBuilder(country.getName() + ". Area: " + country.getArea() + " sq.km.");for (int i = 0; i < country.censusesCount(); i++) { result.append("\n").append(toString(country.getCensus(i))); }return result + ""; } }
The code of the class CountryDemo with the function main() will be similar to the code
of the class PolymorphismDemo from the example of laboratory training No. 3. But instead of
CountryWithLinkedList we will use the CountryWithArrayList. The main differences will
concern the implementation of the function main(), in which, depending on the integer entered by the
user (1 or 2), we use one of the previously created derived classes. If the user enters an invalid integer, he receives
the message "Invalid
number!". If instead
of an integer, a string is entered that cannot be converted to an integer value, an exception occurs, the processing
of which includes outputting the message "Invalid
characters!".
package ua.inf.iwanoff.java.additional;import java.util.InputMismatchException;import java.util.Scanner;import static ua.inf.iwanoff.java.additional.CountryUtilities.*;CountryDemo { /** * Country testing program */ public class /** * Auxiliary function for filling in the data of the "Country" object * @param country country reference * @return a reference to the new "Country" object */ public static Country setCountryData(Country country) { country.setName("Ukraine"); country.setArea(603628);// Adding censuses: System.out.println(country.addCensus(1959, 41869000, "First census after World War II")); System.out.println(country.addCensus(1970, 47126500, "Population increases")); System.out.println(country.addCensus(1979, 49754600, "No comments")); System.out.println(country.addCensus(1989, 51706700, "The last soviet census")); System.out.println(country.addCensus(2001, 48475100, "The first census in the independent Ukraine"));// Attempt to add a census twice: System.out.println(country.addCensus(1959, 41869000, "First census after World War II"));return country; }/** * Displays census data that contains a certain word in comments * @param country reference to a country * @param word a word that is found */ public static void printWord(Country country, String word) { Census[] result = findWord(country, word);if (result.length == 0) { System.out.printf("The word \"%s\" is not present in the comments.", word); }else { System.out.printf("The word \"%s\" is present in the comments:", word);for (Census census : result) { System.out.println(StringRepresentations.toString(census)); } } }/** * Performs testing search methods * @param country reference to a country */ public static void testSearch(Country country) { System.out.printf("Population density in 1979: %5.1f\n", density(country, 1979)); System.out.printf("The year with the maximum population: %d\n\n", maxYear(country)); printWord(country, "census"); printWord(country, "second"); }/** * Performs testing sorting methods * @param country reference to a country */ public static void testSorting(Country country) { sortByPopulation(country); System.out.println("\nSorting by population:"); System.out.println(StringRepresentations.toString(country)); sortByComments(country); System.out.println("\nSorting comments alphabetically:"); System.out.println(StringRepresentations.toString(country)); }main() { System.out.print("Enter your choice: 1 - CountryWithArray, 2 - CountryWithArrayList "); Scanner scanner = /** * Demonstration of work with a country */ static voidnew Scanner(System.in);try {int i = scanner.nextInt(); Country country =switch (i) {case 1 -> setCountryData(new CountryWithArray());case 2 -> setCountryData(new CountryWithArrayList());default ->null ; };if (country ==null ) { System.out.println("Invalid number!");return ; } String s =switch (country) {case CountryWithArray _ -> "------ Country With Array ------";case CountryWithArrayList _ -> "------ Country With Array List ------"; }; System.out.println(s); testSearch(country); testSorting(country); }catch (InputMismatchException _) { System.out.println("Invalid characters!"); } } }
In the second switch, we used pattern matching and unnamed variables. An unnamed
variable was also used in the exception handling.
4 Quiz
- What are the advantages of records over classes intended for read-only objects?
- Is it possible to create multiple constructors in a record?
- What are the syntactic constraints associated with records?
- What is the purpose of creating sealed classes?
- Explain the use of the keyword non-sealed.
- Why is it necessary to clone objects?
- Which
Cloneableinterface methods must be defined? - When should you manually implement a method
clone()? - What is the idea behind pattern matching?
- In which language constructions can you use pattern matching?
- What is the point of pattern matching for records?
- How and why are unnamed variables used?