ua

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 SealedBase permits 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 FirstDerived extends SealedBase {

}

sealed class SecondDerived extends SealedBase permits SomeSubclass {

}

final class SomeSubclass extends SecondDerived {
    {
        data = 0;
    }
}

Attempt to create other subclasses leads to an error:

class AnotherSubclass extends 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 SecondDerived extends SealedBase {

}

class PlainSubclass extends 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 Human implements Cloneable {
    private String name;
    private String surname;
  
    public Human(String name, String surname) {
        super();
        this.name = name;
        this.surname = surname;
    }

    @Override
    public 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):

@Override
public 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 SomeCloneableClass implements 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;
    }

    @Override
    protected 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;
    }

    @Override
    public 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 (obj instanceof 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 (obj instanceof 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 i when i > 0:
        System.out.println("Positive number: " + i);
        break;
    case Integer i when 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 (obj instanceof 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 (obj instanceof 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 (obj instanceof 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 (obj instanceof 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> {

    @Override
    public 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;

/**
 * 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 Country
        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;
    }

    /**
     * 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 boolean addCensus(int 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
     @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof 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
     */
    @Override
    public int hashCode() {
        return Objects.hash(name, area, Arrays.hashCode(getCensusesArray()));
    }
}

The class CountryWithArray must now be final:

package ua.inf.iwanoff.java.additional;

/**
 * Class for presenting the country in which the censuses are carried out.
 * Census data are represented with an array
 */
public final class CountryWithArray 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
     */
    @Override
    public 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
     */
    @Override
    public 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
     */
    @Override
    public 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
     */
    @Override
    public int censusesCount() {
        return censusesArray.length;
    }

    /**
     * Removes all the censuses from the sequence
     */
    @Override
    public void clearCensuses() {
        censusesArray = new Census[0];
    }

    /**
     * Returns an array of censuses obtained from the inner array
     * @return array of references to censuses
     */
    @Override
    public Census[] getCensusesArray() {
        return censusesArray;
    }

    /**
     * Puts data from an array of censuses into a sequence
     * @param censusesArray array of references to censuses
     */
    @Override
    public 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;

/**
 * Class for presenting the country in which the censuses are carried out.
 * Census data are represented with ArrayList
 */
public final class CountryWithArrayList 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
     */
    @Override
    public 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
     */
    @Override
    public 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
     */
    @Override
    public 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
     */
    @Override
    public int censusesCount() {
        return censusesList.size();
    }

    /**
     * Removes all the censuses from the sequence
     */
    @Override
    public void clearCensuses() {
        censusesList.clear();
    }

    /**
     * Puts data from an array of censuses into a sequence
     * @param censusesArray array of references to censuses
     */
    @Override
    public 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
     */
    @Override
    public Census[] getCensusesArray() {
        return censusesList.toArray(new Census[0]);
    }

    /**
     * Returns a list of censuses
     * @return list of references to censuses
     */
    public List<Census> getCensusesList() {
        return censusesList;
    }

    /**
     * Puts a list of censuses into the object
     * @param censusesList arbitrary list of censuses
     */
    public void setCensusesList(List<Census> censusesList) {
        this.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.*;

/**
 * Country testing program
 */
public class CountryDemo {
    /**
     * 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));
    }

    /**
     * Demonstration of work with a country
     */
    static void main() {
        System.out.print("Enter your choice: 1 - CountryWithArray, 2 - CountryWithArrayList ");
        Scanner scanner = new 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

  1. What are the advantages of records over classes intended for read-only objects?
  2. Is it possible to create multiple constructors in a record?
  3. What are the syntactic constraints associated with records?
  4. What is the purpose of creating sealed classes?
  5. Explain the use of the keyword non-sealed.
  6. Why is it necessary to clone objects?
  7. Which Cloneable interface methods must be defined?
  8. When should you manually implement a method clone()?
  9. What is the idea behind pattern matching?
  10. In which language constructions can you use pattern matching?
  11. What is the point of pattern matching for records?
  12. How and why are unnamed variables used?

 

up