Laboratory Training 3

Use of Inheritance and Polymorphism in Java

1 Training Tasks

1.1 Individual Assignment

Create a hierarchy of classes that represent entities according to task 1.5 of the previous laboratory training. The first entity should be represented by a hierarchy of abstract and non-abstract derived classes.

The class that represents the second entity of an individual task should contain data and access methods, overridden equals() and hashCode() functions, It also should implement the Comparable interface for the natural comparison of objects when sorting by one of the criteria.

The base abstract class that represents the first entity of individual tasks (conventionally AbstractFirstEntity) should contain:

  • access functions (getters and setters);
  • abstract methods for access to a sequence of elements of the second abstract class;
  • overridden equals() method for checking equality of objects;
  • overridden hashCode() method for getting hash codes.

To search for the necessary data and sort it according to the task and format the data output to the console, separate auxiliary classes should be created. The search functions should reproduce the tasks of the previous laboratory training.

Sorting criteria are determined by the student's number in the group list. Individual tasks are listed in the following table:

#
First Criterion of Sorting Second Criterion of Sorting
1, 17
By temperature (in decreasing order) By comments in alphabetical order
2, 18
By count of students (in increasing order) By length of topic (in increasing order)
3, 19
By count of passengers (in increasing order) By comments in alphabetical order
4, 20
By count of words in topics (in increasing order) By topics in alphabetical order
5, 21
By temperature (in increasing order) By length of comments (in decreasing order)
6, 22
By count of members (in increasing order) By title in alphabetical order
7, 23
By count of visitors (in increasing order) By comments in alphabetical order
8, 24
By count of passengers (in decreasing order) By length of comments (in decreasing order)
9, 25
By date (in decreasing order) By count of visitors (in increasing order)
10, 26
By count of concerts (in increasing order) By name of city in alphabetical order
11, 27
By shift index (in increasing order) By count of computers (in increasing order)
12, 28
By count of visitors (in increasing order) By comments in alphabetical order
13, 29
By count of passengers (in decreasing order) By name in alphabetical order
14, 30
By count of purchasers (in decreasing order) By comments in alphabetical order
15, 31
By date (in decreasing order) By increasing the number of viewers
16, 32
By decreasing the number of minutes of conversation By increasing the amount of funds used to conversation

Use the standard Arrays.sort() function for sorting. The first sorting should be provided by the implementation of the Comparable interface for the entity whose objects are stored in an array. The second sorting is provided by creating a separate class that implements the Comparator interface. Use of lambda expression is recommended.

Create two derived classes from abstract class:

  • with the sequence of the second entity objects represented using an array;
  • with the sequence of the second entity objects represented by a manually created singly linked list (see Example 3.5).

You should create classes with meaningful names that reflect the physical nature of the individual task.

Javadoc comments should be added to the source code.

In the main() function, create the necessary objects that represent a sequence of objects in various ways, and call methods that implement the main task. Output the results to the console window. Testing the program should include the task of the previous laboratory work, as well as sorting by certain criteria.

1.2 Class Hierarchy

Implement classes "Human," "Citizen", "Student", "Employee". Each class should implement toString() method. Create an array of references to different objects of the class hierarchy. Create a cycle and display data that represents objects of different types.

Note: it is necessary to create classes with meaningful names.

1.3 Minimum of a Function

Implement a program that finds minimum of some function by setting value of step and traversal of a given interval. Implement five versions:

  • using abstract class and derived classes
  • through the definition of the interface, the creation of a class that uses this interface as a parameter type of the minimum finding function, the creation of separate classes that implement this interface
  • using previously described interface and anonymous classes
  • using lambda expressions
  • using references to methods.

Test the program for two different functions.

1.4 Implementation of an Array of Points through a Two-Dimensional Array

Implement the AbstractArrayOfPoints, abstract class functionality given in Example 3.2, in two ways:

  • using a two-dimensional array of real numbers; each row of the array must correspond to the point;
  • use of a one-dimensional array of real numbers; each pair of numbers in the array should correspond to the point.

Test classes.

Note: you should not make changes to the AbstractArrayOfPoints class code (except the package name).

1.5 The Implementation of the Comparable Interface

Create a Circle class that implements the Comparable interface. A circle with a larger radius is considered to be larger. Sort the array of Circle objects.

1.6 The Implementation of the Comparator Interface

Create a Triangle class. The triangle is defined by the lengths of the sides. The area of the triangle in this case can be calculated by Heron's formula:

Heron

where a, b and care the lengths of the sides of the triangle. Sort the array of triangles by descending the area. To determine the sort criteria, use an object that implements the Comparator interface.

1.7 Calculation of the Definite Integral (Additional Task)

Create an Integrable interface that contains a description of an abstract function that takes an argument of type double and returns the result of the same type. This interface should contain the integral() method with the default implementation (with the default modifier) of calculating the specified integral. The method should receive start, end of the interval, and the accuracy of the calculations as arguments. The default implementation of computing the integral should use the method of rectangles.

Create a class that redefines the integral() method by implementing a trapezoid method for calculating integral.

Calculate a definite integral by using both algorithms for different functions of the java.lang.Math class (see example 3.3). Compare results for different algorithms and different values of computing accuracy.

2 Instructions

2.1 Inheritance

The inheritance mechanism assumes creation of derived classes from base classes. Derived class has direct access to all public and protected members of base class. Base class and derived classes build class hierarchy.

Unlike C++, only single inheritance of classes is allowed in Java. In other words, a class can inherit implementation from one base class only. Inheritance is always public. There are no private and protected base classes in Java. Suppose there is a base class:

class Shape {
    private double x, y;  // coordinates of the center of the shape

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }

    public double distance() {// distance from the origin
        return Math.sqrt(x * x + y * y);
    }
}

The syntax of inheritance is as follows:

class DerivedClass extends BaseClass {
    // class body
}

In our case, we can create a derived class:

class Circle extends Shape {
    private double radius;

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }
}

The derived class functions have access to the members described as public and protected. Class members declared to be protected can be used by descendants, as well as within their package. Private class members are not available even for its descendants.

All Java classes are derived from java.lang.Object directly or indirectly. The Object class offers useful methods, such as toString() for retrieving the data of any object as a string, equals() for checking the equivalence of objects, etc. You don't need to declare base class java.lang.Object explicitly.

The class inherits all members of the base class, except constructors. Before the constructor of a derived class executes, the base class constructor (the default constructor, unless explicitly called otherwise) is called. You should not explicitly initialize the fields of the base class from the derived class constructor, even if these fields are available (public or protected).

If we want to call a base class constructor with parameters, in particular if the base class does not have parameterless constructors, there is a special mechanism for an explicit base class constructor invocation through the use of the super keyword. The super keyword is used to access members of the base class from within a derived class:

  • call a method on the base class that has been overridden by another method
  • specify which base-class constructor should be called when creating instances of the derived class.

If in the previous example a single constructor with parameters is added to the Shape class,

class Shape {
    private double x, y;  // coordinates of the center of the shape

    public Shape(double x, double y) {
        this.x = x;
        this.y = y;
    }

    //...
}

the code of the Circle class will not be able to compile. This constructor must be explicitly called from the derived class constructors using super. Alternatively, you can add a constructor whose parameters are needed to initialize the base part of the object and are sent directly to the base class constructor:

class Circle extends Shape {

    private double radius;

    public Circle(double radius) {
        super(0, 0); // call the base class constructor
        this.radius = radius;
    }

    public Circle(double x, double y, double radius) {
        super(x, y); // call the base class constructor
        this.radius = radius;
    }

    public Circle() {
        this(10); // call another constructor of the current class
    }

    // ...
}

Note: you cannot use calls of super() and this() at the same time in the constructor body.

A base class access using the super keyword is permitted only in constructors or non-static methods.

Classes can be declared with final keyword. Final classes cannot be used as base classes. For example, you can declare a class Circle as final:

final class Circle extends Shape {
    // ...
}

class Ellipse extends Circle { // Syntax error.
                               // Cannot inherit from Circle
}

The method declared as final cannot be overridden. For instance:

class Shape {
    
    // ...

    public final double distance() {
        return Math.sqrt(x * x + y * y);
    }
}

class Circle extends Shape {
    public double distance() { // Syntax error.
                               // distance() cannot be overridden
        // ...
    }

    // ...
}

Reference to derived class can be implicitly converted to base class reference. In other words, derived class objects can be always used instead of base class objects.

Shape shape = new Circle();

Reverse conversion should be done explicitly:

Shape shape = new Circle();
Circle circle = (Circle) shape;

If the conversion is not possible, an exception of type ClassCastException is thrown:

Shape shape = new Shape(1, 2);
Circle circle = (Circle) shape; // ClassCastException

Thanks to type casting rules, you can create arrays of references to objects of different types within the same inheritance hierarchy. For example, a previously created class hierarchy of Shape and Circle classes can be extended with Square class:

class Shape {
    private double x, y;

    public Shape(double x, double y) {
        this.x = x;
        this.y = y;
    }

    // ...

    public final double distance() {
        return Math.sqrt(x * x + y * y);
    }
}

class Circle extends Shape {

    private double radius;

    public Circle(double x, double y, double radius) {
        super(x, y);
        this.radius = radius;
    }

    // ...
}

class Square extends Shape {
    private double side;

    public Square(double x, double y, double side) {
        super(x, y);
        this.side = side;
    }

    // ...
}

Now you can put references to different shapes into an array of references to Shape, and then calculate and output the distance of the shapes from the origin in a loop:

Shape shapes[] = new Shape[3];
shapes[0] = new Shape(3, 4);
shapes[1] = new Circle(1, 1, 0.3);
shapes[2] = new Square(4, 3, 2);
for (int i = 0; i < shapes.length; i++) {
    System.out.println(shapes[i].distance());
}

Java supports instanceof keyword, which allows you to check whether the object is an instance of a certain type (or derived types). The expression

object instanceof class

returns the value of the boolean type, which can be used to verify whether the method of this class can be called:

if (shape instanceof Circle) {
    ((Circle)shape).setRadius(20);
}

Records (record type) do not support explicit inheritance.

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 Annotations (Metadata)

Annotations can contain additional information, which cannot be set using language constructs. Annotations start from @ character. The most used annotation is @Override. Thanks to this annotation, the compiler can check whether the corresponding method was actually declared in the base classes.

public class MyClass {
  
    @Override
    public String toString() {  // The toString() method is defined in the Object class
        return "My overridden method!";
    }

}

There are other examples of annotations:

  • @SuppressWarnings("warning_identifier") – compiler warnings should be silenced in the annotated element
  • @Deprecated – the use of the annotated element is no longer desirable

You can declare your own annotations.

2.4 Polymorphism

2.4.1 Overview

A runtime polymorphism is a property that provides definition of class object behavior not during compilation but at run time. Classes that provide identical interface but implemented for specific requirements are called polymorphic classes.

Usually polymorphism is implemented by so-called late binding mechanism. Connecting a method call to a method body is called binding. When binding is performed before the program is run, it's called early binding. Such binding is typical for procedural languages (such as C and Pascal). The late binding means that the binding occurs at run time based on the type of object. Late binding is also called dynamic binding. A late binding mechanism is used to implement the polymorphism.

In object-oriented programming languages, later binding is implemented through the mechanism of virtual functions. A virtual function (virtual method) is a function defined in the base class, and is overridden in the derived classes, so that the specific implementation of the function for the call will be determined at runtime. The choice of the implementation of a virtual function depends on the actual (and not declared in the description) type of the object. Because a reference to a base type can contain the address of an object of any derived type, the behavior of previously created classes can be changed later by overriding virtual methods. Overriding provides for creation of a new virtual method with the same name, a list of parameters and an access qualifier. In fact, polymorphic classes have classes that contain virtual functions.

In C++, the virtual modifier is used to denote a virtual function. In Java, all non-static non-final methods are virtual methods. The virtual keyword is not needed. Constructors and private methods also cannot be virtual.

Starting with Java 5, the @Override annotation can precede overridden virtual methods. This annotation allows the compiler to make additional syntax check: new function signature must match the signature of overridden function of the base class. Use of @Override is desirable but not obligatory.

All Java classes are polymorphic because java.lang.Object is polymorphic as well. In particular, each derived class can define its own virtual toString() method that will be called automatically when string representation of object is needed:

public class SingleInteger {
    int i = 10;
	
    @Override
    public String toString() {
        return "i = " + i; 
    }

    public static void main(String[] args) {
        SingleInteger mc = new SingleInteger();
        System.out.println(mc); // i = 10;
    }
}

2.4.2 Abstract Classes and Methods

Sometimes, a class that you define represents an abstract concept and, as such, should not be instantiated. Such concepts can be represented by abstract classes. Java uses abstract keyword to declare abstract classes:

abstract class SomeConcept {
    // ...
}

An abstract class may contain abstract methods, that is, methods with no implementation. Abstract method has no function body. Its declaration is similar to the declaration of the C++ member function, but the declaration must be preceded by the abstract keyword.

For example, you can declare an abstract class Shape to provide fields and methods that will be used by all subclasses, such as the current position and the moveTo() method. The Shape class also declares abstract methods, such as draw(), that need to be implemented by all subclasses, but are implemented in different ways. No default implementation in the superclass makes sense. The Shape class would look something like this:

abstract class Shape {
    int x, y;
    // ...
    void moveTo(int newX, int newY) {
        // ...
    }
    abstract void draw();
}

Each non-abstract subclass of Shape, such as Circle and Rectangle, would have to provide an implementation for the draw() method.

class Circle extends Shape 
{
    void draw() {
        // ...
    }
}

class Rectangle extends Shape {
    void draw() {
        // ...
    }
}

Abstract methods are similar to pure virtual functions in C++ language.

An abstract class is not required to have an abstract method in it. However, any class that has an abstract method in it or that does not provide an implementation for any abstract methods declared in its superclasses must be declared as an abstract class (using abstract keyword).

2.5 Concept of Interfaces. Arranging Objects

2.5.1 Creating and Implementing Interfaces

Java introduces a concept of interface. The interface can be considered as a purely abstract class, but unlike abstract classes, the interface never contains data, only methods. These methods are generally considered public:

interface Printable {
    void print(String printerName);
    String getPreview();
}

A class may inherit from one and only one other class but can implement several interfaces. When a class implements an interface, it either must provide implementations for all the methods declared in that interface or this class must be declared with abstract keyword.

Interfaces can contain data fields. Those fields are considered as final and static (compile-time constants). They must be initialized at the place of creation:

interface MathConstants {
    double PI = 3.14159265359;
    double E = 2.71828182846;
}

Interfaces cannot contain constructors because there is no data other than static constants.

To indicate that a class implements an interface, the interface identifier is included in the list of the implemented interfaces. This list starts with implements keyword. Methods declared in an interface are public and abstract by default and if this class implements a method, which was declared in an interface, this method must be declared as public:

interface Printable {
    void print(String printerName);
    String getPreview();
}

class Document implements Printable {
    @Override
    public void print(String printerName) {

    }

    @Override
    public String getPreview() {
        String preview = "";
        //...
        return preview;
    }
}

Note: Using the @Override annotation is recommended, but not required.

Records (record type) can implement interfaces.

An interface can have several base interfaces. Multiple inheritance of interfaces is safe in terms of data duplication and name conflicts:

interface Printable {
    void print(String printerName);
    String getPreview();
}

interface Editable {
    void edit();
}

interface Serviceable extends Printable, Editable {
    String getPreview(); // the declaration may be repeated
}

Now the class that implements the AllFunctions interface must define three functions:

class Document implements Serviceable {
    @Override
    public void print(String printerName) {

    }

    @Override
    // One implementation is used for both base and derived interfaces:
    public String getPreview() {
        String preview = "";
        //...
        return preview;
    }

    @Override public void edit() {

    }
}

A class can implement several interfaces. This is a more common way than creating a derived interface:

class AnotherDocument implements Printable, Editable {
    @Override
    public void print(String printerName) {

    }

    @Override
    public String getPreview() {
        String preview = "";
        //...
        return preview;
    }

    @Override public void edit() {
        //...
    }
}

Very often programmers create the reference to the interface which is initialized by object of a class that implements this interface. This approach is good practice because it allows you to easily replace one implementation of the interface with another one:

public static void main(String[] args) {
    Serviceable serviceable = new Document(); 
}

Interfaces are not derived from the java.lang.Object class. You cannot create a new interface object. Even for an empty interface, you need to create a class that implements it:

interface Empty {

}

class EmptyImplementation implements Empty {

}

// ...

public static void main(String[] args) {
    Empty empty1 = new Empty(); // Error
    Empty empty2 = new EmptyImplementation(); // Correct creation of the object
}

Like classes, interfaces can be defined with public modifier (most often) or without a modifier. Like classes, public interfaces must be defined in separate files, whose names match the names of the interfaces.

2.5.2 Arranging Objects

The JDK provides a large number of standard interfaces. Consider using the Comparable and Comparator interfaces to sort arrays.

The simplest way to implement ascending sort is call of java.util.Array.sort() method with reference to array as single argument (reference to an array). You can also sort any part of array. Such methods are implemented for all primitive types. You can also sort arrays of objects into ascending order, according to the natural ordering of its items. All items in the array must implement the Comparable interface. The only method of this interface is compareTo():

public int compareTo(Object o)

This method should return negative value (e.g. -1) if this object is less than o, zero value if objects are equal, and positive value otherwise.

Standard Double, Integer, Long, etc., and String classes implement this interface. The following program sorts an array of Integer:

public class SortIntegers {
    public static void main(String[] args) {
        Integer[] a = {7, 8, 3, 4, -10, 0};
        java.util.Arrays.sort(a);
        System.out.println(java.util.Arrays.toString(a));
    }
}

Note. The Comparable interface name is an example of the most correct interface name. It is desirable that the interface names end in -able (Comparable, Runnable, etc.). But this rule is very often violated even for standard interfaces.

Java 5 Comparable is a generic interface. Creating generic classes and interfaces will be considered later. Thanks to the generalization, function declared in the interface can get parameters of other types but not of the type Object only. In our case, the compareTo() function should get an argument of the array item type.

The following example shows how to create your own class, which implements Comparable interface. The program sorts array of rectangles by area:

class Rectangle implements Comparable<Rectangle> {
    private double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double area() {
        return width * height;
    }

    public double perimeter() {
        return 2 * (width + height);
    }
    
    @Override
    public int compareTo(Rectangle rect) {
        return Double.compare(area(), rect.area());
    }

    @Override
    public String toString() {
        return "[" + width + ", " + height + ", area = " + area() + ", perimeter = " + perimeter() + "]";
    }

}

public class SortRectangles {

    public static void main(String[] args) {
        Rectangle[] a = {new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4)};
        java.util.Arrays.sort(a);
        System.out.println(java.util.Arrays.toString(a));
    }

}

In this example, the static compare() method of Double class returns necessary integer values which are used by sort() method.

Sometimes we cannot (or do not want to) implement compareTo() for particular class, or we want to sort objects by different criteria. An alternative way of sorting is creation of a class that implements Comparator interface. Objects of such classes are used as arguments of sort() methods. Java 2 declaration of such methods (without generics) is as follows (the description is given without generics):

public static void sort(Object[] a, Comparator c);
public static void sort(Object[] a, int fromIndex, int toIndex, Comparator c);

The only method of Comparator interface is compare(). This method should return negative value if first object is less than second, zero value if objects are equal, and positive value otherwise.

Note. Java 5 Comparator is also a generic interface. By its implementation, after its name, in angle brackets it is necessary to specify the type of objects that you compare. Now the compare() function should take two arguments of array item type. For example

By using a class that implements the Comparator interface, you can additionally sort by perimeter of rectangles:

class CompareByPerimeter implements java.util.Comparator<Rectangle>
{

    @Override
    public int compare(Rectangle r1, Rectangle r2) {
        return Double.compare(r1.perimeter(), r2.perimeter());
    }
}

public class SortRectangles {

    public static void main(String[] args) {
        Rectangle[] a = {new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4)};
        java.util.Arrays.sort(a);                           // sort by area
        System.out.println(java.util.Arrays.toString(a));
        java.util.Arrays.sort(a, new CompareByPerimeter()); // sort by perimeter
        System.out.println(java.util.Arrays.toString(a));
    }

}

2.6 Nested Classes

2.6.1 Overview

It is possible to place a class definition within another class definition. This is called a nested class. It can be either static nested or inner. A nested class can be used inside either of outer class or somewhere else.

class Outer {
    class Inner {
        int i;
    }

    Inner inner = new Inner();
}

class Another {
    Outer.Inner i;
}

Nested classes can be declared as public, private, or protected.

Local classes are created inside blocks. There is also a special kind of local classes, the so-called anonymous classes.

2.6.2 Static Nested Classes

The use of static nested classes is similar to the nested classes in C++ and C#. It is in fact a type described within the scope of another type:

class Outer {
    static class Inner {
    }
}

For example, we can create your own wrapper classes for integers and real numbers and define these classes as nested:

public class Wrappers {
    public static class Integer {
        private int value;

        public Integer(int value) {
            this.value = value;
        }

        public int get() {
            return value;
        }

        public void set(int value) {
            this.value = value;
        }
    }

    public static class Double {
        private double value;

        public Double(double value) {
            this.value = value;
        }

        public double get() {
            return value;
        }

        public void set(double value) {
            this.value = value;
        }
    }
    
    // ...

}

Objects of such classes can be created both in the outer class and outside it. In the Wrappers class:

public class Wrappers {
    
    // ...
    
    protected void test() {
        Integer i = new Integer(3);
        System.out.println(i.get());
        Double d = new Double(3.6);
        System.out.println(d.get());
    }
}

In another class:

public class WrappersTest {
    public static void main(String[] args) {
        Wrappers.Integer i = new Wrappers.Integer(10);
        System.out.println(i.get());
        Wrappers.Double d = new Wrappers.Double(10.5);
        System.out.println(d.get());
    }
}

Nested classes can be used to create a type for some part of a complex object, or part of its description. For example, within a Circle class, we can create a separate nested class Center to represent the coordinates of the center:

public class Circle {
    public static class Center {
        private double x, y; // coordinates of the center of the circle

        public Center() {
        }

        public Center(double x, double y) {
            this.x = x;
            this.y = y;
        }

        public double getX() {
            return x;
        }

        public void setX(double x) {
            this.x = x;
        }

        public double getY() {
            return y;
        }

        public void setY(double y) {
            this.y = y;
        }

        double distance() {
            return Math.sqrt(x * x + y * y);
        }
    }

    private double radius;
    private Center center = new Center(); // coordinates of the center of the circle

    //...

}

An object of Center class is automatically created when a Circle class object is created, because a field of the appropriate type is present in Circle and initialized with the required object. Otherwise, the class would simply be a static class nested within another.

We can set and read the center coordinates outside the class. We can also create a separate object and then replace the existing center with this object:

public class CircleTest {
    public static void main(String[] args) {
        Circle circle = new Circle();
        circle.getCenter().setX(1);
        Circle.Center center = new Circle.Center(2, 3);
        circle.setCenter(center);
    }
}

Static nested classes can contain their own static members, as well as own nested static and non-static classes. Nested classes have access to the static members of outer classes, including those that have been declared private. For example:

public class Circle {
    private static String entityName = "Circle";

    public static String getEntityName() {
        return entityName;
    }

    public static class Center {

        public static String getClassInformation() {
            return "Inner class: Center. Outer class: " + entityName;
        }
        
        // ...
    }

    //...
}

If there is a name conflict, you can use a prefix – the class name:

public class Circle {
    private static String entityName = "Circle";

    public static String getEntityName() {
        return entityName;
    }

    public static class Center {
        private static String entityName = "Center";

        public static String getClassInformation() {
            return "Inner class: " + entityName + ". Outer class: " + Circle.entityName;
        }

        // ...
    }

    //...
}

Static nested classes do not have access to non-static members of outer classes. However, such an object can be created or passed as a parameter. Then private non-static members are also available:

public class Circle {
    private double radius;
    
    public static class Center {
        private static String entityName = "Center";

        public String getObjectInformation(Circle circle) {
            return "Coordinates of the center of the circle: " + x + ", " + y +
                   ". Radius of the circle: " + circle.radius;
        }

        // ...
    }

    // ...
}

Classes can be created inside interfaces. These classes are static by default. Inside classes, you can also create interfaces that are static by default.

2.6.3 Inner Classes

Non-static nested classes are also inner classes. The main feature of the inner classes in Java is that the objects of these classes receive a reference to the object of the outer class. This fact implies two important conclusions:

  • objects of inner classes have direct access to members of outer class objects
  • you cannot create object of inner class without object of outer class.

Non-static inner classes cannot contain static members.

The class Center from the previous example can be defined as non-static. The code for this class should be changed, in particular, the static members of the inner class should be converted to non-static:

package ua.inf.iwanoff.java.third;

public class Circle {
    private static String entityName = "Circle";

    public static String getEntityName() {
        return entityName;
    }

    public class Center {
        private String entityName = "Center";

        public String getClassInformation() {
            return "Inner class: " + entityName + ". Outer class: " + Circle.entityName;
        }

        public String getObjectInformation() {
            return "Coordinates of the center of the circle: " + x + ", " + y +
                   ". Radius of the circle: " + radius;
        }

        // ... then no changes
    }

    private double radius;
    private Center center = new Center(); // coordinates of the center of the circle

    //...
}

To create inner class objects outside outer classes, Java offers a special syntax:

outer_class_object.new Inner_class_constructor();

In our case, we will need to make changes to the function main():

public class CircleTest {
    public static void main(String[] args) {
        Circle circle = new Circle();
        circle.getCenter().setX(1);
        Circle.Center center = new Circle().new Center(2, 3);
        circle.setCenter(center);
        System.out.println(center.getClassInformation());
        System.out.println(center.getObjectInformation());
    }
}

You should keep in mind that objects of inner classes are not created automatically. Creating an object can be provided in the constructor or in any method of the outer class, and also outside it (if this class is not declared private). You can also create an array of inner class objects. Each of these objects will have access to the outer object.

Inner classes can have their own base classes. Therefore, we obtain something close to multiple inheritance:

class FirstBase {
    int a = 1;
}

class SecondBase {
    int b = 2;
}

class Outer extends FirstBase {
    int c = 3;

    class Inner extends SecondBase {
        void show() {
            System.out.println(a);
            System.out.println(b);
            System.out.println(c);
        }
    }
}

public class Test {

    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.show();
    }
}

The following example has a purely theoretical meaning, since the multiple inheritance of classes, regardless of the methods of its implementation, is dangerous in terms of a possible name conflicts.

2.6.4 Local and Anonymous Classes

Inner classes can be also local. You cannot access local classes from the outside of the block in which they are defined. A local class definition is typically placed into function body:

void f() {
    class Local {
        int j;
    }
    Local l = new Local();
    l.j = 100;
    System.out.println(l.j);
}

You can also place local classes inside blocks.

Local classes can have their own base classes or implement certain interfaces. In the following example, a local class is created to define how to sort an array of strings (in reverse alphabetical order):

static void sortReverse(String[] a) {
    class StringComparer implements Comparator<String> {
        @Override
        public int compare(String s1, String s2) {
            return s2.compareTo(s1);
        }
    }
    StringComparer comparer = new StringComparer();
    Arrays.sort(a, comparer);
}

An anonymous class can implement an interface or extend another class. It can either override existing base class methods or introduce new ones. To create an object of an anonymous class, you should call the constructor of the base class constructor, or specify the name of the interface with parentheses, and then you place the body of an anonymous class:

new Object() {
    // Adding a new method:
    void hello() {
        System.out.println("Hello!");
    }
}.hello();

System.out.println(new Object() {
    // Overriding an existing method:
    @Override public String toString() {
        return "This is an anonymous class.";
    }
});

An anonymous class is never abstract. An anonymous class is always an inner class; it is never static. An anonymous class is always implicitly final.

An anonymous class can implement an interface. In the following example, new anonymous class is created for definition of sorting order of string-type members of an array:

static void sortReverse(String[] a) {
    Arrays.sort(a, new Comparator<String>() {
        public int compare(String s1, String s2) {
            return s2.compareTo(s1);
        }
    });
}

An anonymous class cannot have an explicitly declared constructor. However, a default constructor is always created. If the base class does not have a constructor without parameters, the necessary parameters of the constructor are placed in brackets when you create an object:

abstract class Base {
    int k;

    Base(int k) {
        this.k = k;
    }

    abstract void show();
}

public class Test {

    static void showBase(Base b) {
        b.show();
    }

    public static void main(String[] args) {
        showBase(new Base(10) {
            void show() {
                System.out.println(k);
            }
        });
    }
}

You can also use initialization blocks.

In order for anonymous classes to have access to local members of external blocks, these members should be declared as final.

2.7 Interface Methods with Default Implementation

Java 8 provides a new opportunity to provide default implementation of methods declared in the interface. To do this, before the corresponding function you must place the default keyword, after which the function can be implemented inside the interface. In the following example some interface declares method and offers the default implementation of this method:

package ua.inf.iwanoff.java.third;

public interface Greetings {
    default void hello() {
        System.out.println("Hello everybody!");
    }
}

The class that implements this interface may be empty. You can leave the default implementation of the hello() method:

package ua.inf.iwanoff.java.third;

public class MyGreetings implements Greetings {

}

During testing we receive a default greeting.

package ua.inf.iwanoff.java.third;

public class GreetingsTest {

    public static void main(String[] args) {
        new MyGreetings().hello(); // Hello everybody!
    }

}

The same can be obtained by using anonymous class. Its body will also be empty:

package ua.inf.iwanoff.java.third;

public class GreetingsTest {

    public static void main(String[] args) {
        new Greetings() { }.hello(); // Hello everybody!
    }

}

The presence of default implementation methods makes the interfaces even more similar to abstract (and even non-abstract) classes. But the fundamental difference is that the interface cannot be directly used to create objects. All classes are directly or indirectly derived from the base type java.lang.Object, which contains the data and functions required to function all, even the simplest objects. Interfaces are not classes and are from java.lang.Object. An interface is just a declaration of a certain behavior that can be supplemented with assistive tools (methods with default implementation). The fields described in the interface are not the actual data of the object but the compile-time constants. To call methods with default implementation, an object of a class that implements the interface is required. Because of this the anonymous class object is created

new Greetings() { }.hello();

but not the interface instance

new Greetings().hello(); // Syntax error!

The method with default implementation can be overridden:

package ua.inf.iwanoff.java.third;

public class MyGreetings implements Greetings {

    @Override
    public void hello() {
        System.out.println("Hello to me!");
    }

}

Now, creating an object of this class, we get a new greeting.

If overridden method needs to call the default interface method, you can use the super keyword:

Greetings.super.hello();

We can offer such an example. Suppose it is necessary to print the values of some function at a certain interval with a given step. You can create an interface with one abstract method (the calculation of some function) and one method with a default implementation:

package ua.inf.iwanoff.java.third;

public interface FunctionToPrint {
    double f(double x);
    default void print(double x) {
        System.out.printf("x = %7f f(x) = %7f%n", x, f(x));
    }
}

In the PrintValues class, we create a printTable() method. This method uses the previously created interface.

package ua.inf.iwanoff.java.third;

public class PrintValues {

    static void printTable(double from, double to, 
                      double step, FunctionToPrint func) {
        for (double x = from; x <= to; x += step) {
            func.print(x);
        }
        System.out.println();
    }

    // Create an object of an anonymous class in the main() function:
    public static void main(String[] args) {
        printTable(-2, 2, 0.5, new FunctionToPrint() {
            @Override
            public double f(double x) {
                return x * x * x;
            }
        });
    }

}

Assume the accuracy of the values is not satisfactory. In this case, you can also override the print() method:

   public static void main(String[] args) {
        printTable(-2, 2, 0.5, new FunctionToPrint() {
            @Override
            public double f(double x) {
                return x * x * x;
            }

            @Override
            public void print(double x) {
                System.out.printf("x = %9f f(x) = %9f%n", x, f(x));
            }
            
        });
    }

The main advantage of interfaces with default implementation is the ability to extend the interfaces from version to version, ensuring compatibility with the old code. Suppose, an interface was previously defined in some library:

public interface SomeInterface {
    void f();
}

This interface was implemented by some class:

public class OldImpl implements SomeInterface {
    @Override
    public void f() {
        // implementation
    }
}

Now, when updating the library, a new version of the interface was created with the new method:

interface SomeInterface {
    void f();
    default void g() {
        // implementation
    }
}

This method will be implemented by new classes:

public class NewImpl implements SomeInterface {

    @Override
    public void f() {
        // implementation
    }

    @Override
    public void g() {
        // implementation
    }
}

Without default implementation, the code built on the previous version will not be compiled.

When inheriting an interface that contains a method with a default implementation, this method is also inherited with its implementation, but it can also be re-declared as abstract, or redefined with another implementation.

In Java 8, interfaces may also contain the implementation of static methods. These methods should logically relate to this interface (for example, they can get the reference to the interface as a parameter). Most often, these are auxiliary methods. Like all interface members, these static methods are public. You can specify public explicitly, but there is no need for it.

In the example above, the printTable() function could be placed inside the interface:

package ua.inf.iwanoff.java.third;

public interface FunctionToPrint {
    double f(double x);
    default void print(double x) {
        System.out.printf("x = %9f f(x) = %9f%n", x, f(x));
    }
    static void printTable(double from, double to, double step, FunctionToPrint func) {
        for (double x = from; x <= to; x += step) {
            func.print(x);
        }
        System.out.println();
    }
}

The function call should be performed through the interface name.

2.8 Working with Functional Interfaces in Java 8

2.8.1 Lambda Expressions and Functional Interfaces

Java interfaces very often contain declaration of the one and only abstract function (without default implementation). Such interfaces are called functional interfaces. They are generally used to implement callback mechanism, event handling, etc. Despite their simplicity, their implementation, however, requires a separate class (ordinary, nested or anonymous). Even using an anonymous class, we get cumbersome poorly readable syntax. Lambda expressions that appeared in the Java version 8, can reduce the need for anonymous classes in the source code.

In programming languages, there is the concept of functional object – an object that can be used as a function. Lambda expression is a special syntax for describing a functional object within a method. In other words, the lambda expression is a way to describe a function inside another function.

The term "lambda expression" is associated with mathematical discipline, so-called lambda calculus. Lambda calculus is a formal system developed by the American mathematician Alonzo Church for formalization and analysis of the notion of computability. Lambda calculus has become a formal basis for functional programming languages (Lisp, Scheme, etc.).

Lambda expression in Java has the following syntax:

  • a list of formal parameters separated by commas and enclosed in parentheses; if we have one parameter, parentheses can be omitted; if there are no parameters, you need an empty pair of parentheses;
  • arrow (pointer, ->);
  • a body consisting of one expression or a block; if a block is used, the return statement may be inside it.

For example, this is a function with one parameter:

k -> k * k

The same with the brackets and the block:

(k) -> { return k * k; }

Function with two parameters:

(a, b) -> a + b

Function without parameters:

() -> System.out.println("First")

For example, there is a functional interface:

public interface SomeInt {
    int f(int x);
}

When calling some function, you need to send an argument of the functional interface type. Traditionally, you can create an anonymous class:

someFunc(new SomeInt() {
    @Override
    public int f(int x) {
        return x * x;
    }
});

You can create a variable of object that implements the interface, and use it instead of anonymous class:

SomeInt func = k -> k * k;
someFunc(func);

You can also create an anonymous object when calling a function with a functional interface type parameter:

someFunc(x -> x * x);

Since each lambda expression is associated with a specific functional interface, the parameter types and the result are determined automatically in comparison with the respective functional interface.

An example task with a function value table can be implemented using lambda expressions. The previously created interface is functional interface because it has the only abstract method declared:

package ua.inf.iwanoff.java.third;

public interface FunctionToPrint {
    double f(double x);
    default void print(double x) {
        System.out.printf("x = %9f f(x) = %9f%n", x, f(x));
    }
    static void printTable(double from, double to, double step, FunctionToPrint func) {
        for (double x = from; x <= to; x += step) {
            func.print(x);
        }
        System.out.println();
    }
}

Using the functional interface with the lambda expression:

package ua.inf.iwanoff.java.third;

public class PrintWithLambda {
    public static void main(String[] args) {
        FunctionToPrint.printTable(-2.0, 2.0, 0.5, x -> x * x * x);
    }
}

2.8.2 Using References to Methods

Very often the entire lambda-expression body consists of calling the existing method only. In this case, instead of the lambda expression, you can use the reference to this method. There are several variants of using methods references.

Kind of reference to the method Syntax Example
Reference to the static method Class::staticMethodName String::valueOf
Reference to a non-static method for a given object object::nonStaticMethodName s::toString
Reference to a non-static method for parameter Class::nonStaticMethodName Object::toString
Reference to the constructor Class::new String::new

For example, there are functional interfaces:

interface IntOperation {
    int f(int a, int b);
}

interface StringOperation {
    String g(String s);
}

You can create some class:

class DifferentMethods
{
    public int add(int a, int b) {
        return a + b;
    }

    public static int mult(int a, int b) {
        return a * b;
    }
}

Now methods are used:

public class TestMethodReferences {

    static void print(IntOperation op, int a, int b) {
        System.out.println(op.f(a, b));
    }
  
    static void print(StringOperation op, String s) {
        System.out.println(op.g(s));
    }
  
    public static void main(String[] args) {
        DifferentMethods dm = new DifferentMethods();
        print(dm::add, 3, 4);
        print(DifferentMethods::mult, 3, 4);
        print(String::toUpperCase, "text");    
    }
}

2.8.3 Standard Functional Interfaces

Instead of creating new functional interfaces, in most cases it is enough to use the standard interfaces that are described in the java.util.function package.

Interface Description
BooleanSupplier Represents a supplier of boolean-valued results
DoubleBinaryOperator Represents an operation upon two double-valued operands and producing a double-valued result
DoubleConsumer Represents an operation that accepts a single double-valued argument and returns no result
DoublePredicate Represents a predicate (boolean-valued function) of one double-valued argument
DoubleSupplier Represents a supplier of double-valued results
DoubleToIntFunction Represents a function that accepts a double-valued argument and produces an int-valued result
DoubleToLongFunction Represents a function that accepts a double-valued argument and produces a long-valued result
DoubleUnaryOperator Represents an operation on a single double-valued operand that produces a double-valued result
IntBinaryOperator Represents an operation upon two int-valued operands and producing an int-valued result
IntConsumer Represents an operation that accepts a single int-valued argument and returns no result
IntPredicate Represents a predicate (boolean-valued function) of one int-valued argument
IntSupplier Represents a supplier of int-valued results
IntToDoubleFunction Represents a function that accepts an int-valued argument and produces a double-valued result
IntToLongFunction Represents a function that accepts an int-valued argument and produces a long-valued result
IntUnaryOperator Represents an operation on a single int-valued operand that produces an int-valued result
LongBinaryOperator Represents an operation upon two long-valued operands and producing a long-valued result
LongConsumer Represents an operation that accepts a single long-valued argument and returns no result
LongPredicate Represents a predicate (boolean-valued function) of one long-valued argument
LongSupplier Represents a supplier of long-valued results
LongToDoubleFunction Represents a function that accepts a long -valued argument and produces a double-valued result
LongToIntFunction Represents a function that accepts a long -valued argument and produces an int-valued result
LongUnaryOperator Represents an operation on a single long-valued operand that produces a long-valued result long

For example, you can manually create a functional interface SomeFunction and use that interface:

interface SomeFunction {
    double func(double x);
}

class ValuePrinter {
    static void printValue(SomeFunction function, double x) {
        System.out.printf("x = %f, f(x) = %f", x, function.func(x));
    }

    static void test() {
        printValue(Math::sqrt, 4);
    }
}

But a more productive approach is to use the standard interface DoubleUnaryOperator without creating a new interface:

class ValuePrinter {
    static void printValue(DoubleUnaryOperator function, double x) {
        System.out.printf("x = %f, f(x) = %f", x, function.applyAsDouble(x));
    }

    static void test() {
        printValue(Math::sqrt, 4);
    }
}

In addition to those listed, there are useful generic functional interfaces that will be discussed later.

In addition to these, a generic Comparator interface, a Runnable interface (used in multithreading programming), and many more also are functional interfaces.

2.8.4 Composition of Lambda Expressions

You can implement a composition of lambda expressions (use lambda expressions as parameters). For this purpose the java.util.function package interfaces provide methods with a default implementation, providing execution of a function passed as a parameter, before or after this method. In particular, the following methods are defined in standard functional interface:

// The before function is executed, and then the calling function is executed:
Function compose(Function before)
// The after function is executed after the calling function:
Function andThen(Function after)

Use of these methods and their differences will be considered in this example. There is a class with a static function calc(), which accepts a functional interface and an argument of double type. You can make a composition of lambda expressions:

package ua.inf.iwanoff.java.third;

import java.util.function.DoubleUnaryOperator;

public class ComposeDemo {

    public static double calc(DoubleUnaryOperator operator, double x) {
        return operator.applyAsDouble(x);
    }

    public static void main(String[] args) {
        DoubleUnaryOperator addTwo = x -> x + 2;
        DoubleUnaryOperator duplicate = x -> x * 2;
        System.out.println(calc(addTwo.compose(duplicate), 10.0)); // 22.0
        System.out.println(calc(addTwo.andThen(duplicate), 10.0)); // 24.0
    }
}

The composition may be more complex:

System.out.println(calc(addTwo.andThen(duplicate).andThen(addTwo), 10.0)); // 26.0

2.9 Object Cloning, Equivalence Checking, and Getting Hash Codes

2.9.1 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.

Note. The mechanism of exception handling is largely similar to the corresponding C++ language mechanism. In Java, you should list possible thrown exceptions using the throws keyword in the method header. The mechanism for processing exceptions will be discussed later.

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.9.2 Equivalence Checking

In order to make sure that the cloned objects are the same, it would be nice to be able to automatically compare all the fields. The reference model of Java objects does not allow you to compare the contents of objects using the comparison operation (==), since the references are compared. To compare data, it is advisable to use the equals() function defined in the java.lang.Object class. For classes whose fields are value types, the class method provides a memberwise comparison. If the fields are object references, you must explicitly override the equals() function. A typical implementation of the equals() method includes examining references (if they match), then check whether the reference is null, then check type (for example, using instanceof operator). If the types match, fields are checked.

Here is a complete example with the Human class:

ppackage 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 boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || !(obj instanceof Human)) {
            return false;
        }
        Human h = (Human) obj;
        return name.equals(h.name) && surname.equals(h.surname);
    }

    @Override
    public Human clone() throws CloneNotSupportedException {
        return (Human) super.clone();
    }

    @Override
    public String toString() {
        return name + " " + surname;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Human human1 = new Human("John", "Smith");
        Human human2 = human1.clone();
        System.out.println(human2);
        human1.name = "Mary";
        System.out.println(human1);
        System.out.println(human2);
        human2.name = new String("Mary");
        System.out.println(human2);
        System.out.println(human1.equals(human2)); // true
    }
}

If the equals() method had not been defined, the last comparison would have given false.

To compare two arrays, you should call the static function equals() of the Arrays class. This function compares array memberwise (calls the equals() method):

Arrays.equals(array1, array2);

Records (record) automatically provide a correct the correct implementation of the equals() method.

2.9.3 Hash Codes

The implementation of the equals() function for large objects may require significant calculations, since all data members should be checked. For simplified verification for possible equality of two objects Java uses so-called hashing.

Hashing is the process of obtaining from an object a unique code using some formal algorithm. In a broad sense, the result is a sequence of bits of fixed length, in the particular case, this is a simple integer. This conversion is performed by a so-called hash function. The hash function should meet the following requirement: the hash function should return the same hash code each time it is applied to identical or equal objects. Unfortunately, it is not possible to ensure producing different of hash codes for different objects.

Hashing is used in some container classes of the Java Collection Framework to make it impossible to put identical elements into the collection.

All Java objects inherit the standard implementation of a hashCode() function defined in the Object class. This function returns a hash code obtained by converting an object's internal address to a number, which ensures the creation of a unique code for each individual object.

Specific standard classes implement their hash functions. For example, for a string, the value of the hash function is calculated by the formula:

s[0]*31n-1 + s[1]*31n-2 + ... + s[n-1]

Here s[0], s[1], etc. are codes of the corresponding characters.

The functions hashCode() for Integer, Double, etc. are also stable. For user defined types, the hashCode() function should be redefined. Usually, the hash code of an object is generated from the hash codes of the fields. The simplest way is to use the static function hash() of the java.util.Objects class (starting with Java 7). Suppose a class Person is created. The hash code can be generated using the hash() function:

class Person {
    String name;
    int year;

    public Person(String name, int year) {
        this.name = name;
        this.year = year;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, year);
    }
}

You can also propose your own algorithms for obtaining hash codes.

Records (record) automatically provide a correct hash function.

3 Sample Programs

3.1 Hierarchy of Real World Objects

Our goal is to create the following class hierarchy:

  • Region
  • Populated Region
  • Country
  • City
  • Island

Several classes of this hierarchy can be used as base classes for other classes (for instance, "Uninhabited island", "National park", "Borough", etc.). The hierarchy of classes can be expanded with the classes "City" and "Island". Each class should provide its original constructor for fields' initialization. Array of references to base object can be filled with references to objects of different derived types. For each object, a string of data about it will be displayed on the screen.

To obtain string representation of some object, we'll override toString() function.

The class hierarchy can be as follows:

package ua.inf.iwanoff.java.third;

import java.util.*;

// Class hierarchy
class Region {
    private String name;
    private double area;

    public Region(String name, double area) {
        this.name = name;
        this.area = area;
    }

    public String getName() {
        return name;
    }

    public double getArea() {
        return area;
    }

    public String toString() {
        return "Region" + name + ".\tArea " + area + " sq.km.";
    }

}

class PopulatedRegion extends Region {
    private int population;

    public PopulatedRegion(String name, double area, int population) {
        super(name, area);
        this.population = population;
    }

    public int getPopulation() {
        return population;
    }

    public int density() {
        return (int) (population / getArea());
    }

    public String toString() {
        return "Populated Region" + getName() + ".\tArea " + getArea() +
               " sq.km.   \tPopulation " + population + 
               " inhab.\tDensity" + density() + " inhab. per sq.km.";
    }

}

class Country extends PopulatedRegion {
    private String capital;

    public Country(String name, double area, int population, String capital) {
        super(name, area, population);
        this.capital = capital;
    }

    public String getCapital() {
        return capital;
    }

    public String toString() {
        return "Country " + getName() + ".\tArea " + getArea() +
               " sq.km.   \tPopulation " + getPopulation() + 
               " inhab.\tDensity " + density() + 
               " inhab. per sq.km.\tCapital " + capital;
    }

}

class City extends PopulatedRegion {
    private int boroughs; // count of boroughs

    public City(String name, double area, int population, int boroughs) {
        super(name, area, population);
        this.boroughs = boroughs;
    }

    public int getBoroughs() {
        return boroughs;
    }

    public String toString() {
        return "City " + getName() + ".\tArea " + getArea() +
               " sq.km.   \tPopulation " + getPopulation() + 
               " inhab.\tDensity " + density() + 
               " inhab. per sq.km.\tBoroughs: " + boroughs;
    }
  
}

class Island extends PopulatedRegion {
    private String sea;

    public Island(String name, double area, int population, String sea) {
        super(name, area, population);
        this.sea = sea;
    }

    public String getSea() {
        return sea;
    }

    public String toString() {
        return "Island " + getName() + ".\tArea " + getArea() +
               " sq.km.   \tPopulation " + getPopulation() + 
               " inhab.\tDensity " + density() + 
               " inhab. per sq.km.\tSea - " + sea;
    }  
}

public class Regions {
  
    public static void main(String[] args) {
        Region[] a = { new City("Kiev", 839, 2679000, 10),
                       new Country("Ukraine", 603700, 46294000, "Kiev"),
                       new City("Kharkiv", 310, 1461000, 9),
                       new Island("Zmiiny", 0.2, 30, "Black Sea") };
        for (Region region : a) {
            System.out.println(region);
        }
    }

}

3.2 Class for Representing an Array of Points

3.2.1 Problem Statement and Creating Abstract Class

Suppose we want to develop a class to represent an array of points. Each point is represented by two numbers of the double type: x and y. It is necessary to provide setting point coordinates, getting coordinates of the specific point and the total number of points, adding a point to the end of an array, and removing the last point. In addition, it is necessary to organize the sorting of the array by increasing the specified coordinate and output the coordinates of points into a string.

The simplest, but not the only solution is to create a Point class with two fields and creating an array of references to the Point type. Such a solution is correct in terms of organizing the data structure, but not sufficiently effective, since it involves placing both the array itself and individual items in free store. Alternative variants are use of two arrays, two-dimensional array, etc.

The final decision on the structure of data can be taken only in the context of a specific task. Polymorphism allows us to implement the necessary algorithms without binding to a specific data structure. To do this, we create an abstract class in which the access functions are declared as abstract, sorting and output algorithms are implemented using abstract access functions. In addition, we can define a function for testing. The corresponding abstract class will be as follows:

package ua.inf.iwanoff.java.third;

public abstract class AbstractArrayOfPoints {
    // Recording of new point coordinates:
    public abstract void setPoint(int i, double x, double y);

    // Getting X of the i-th point:
    public abstract double getX(int i);

    // Getting Y of the i-th point:
    public abstract double getY(int i);

    // Getting the number of points:
    public abstract int count();

    // Adding a point to the end of an array:
    public abstract void addPoint(double x, double y);

    // Deleting the last point:
    public abstract void removeLast();

    // Sorting by X:
    public void sortByX() {
        boolean mustSort; // repeat while
                          // the mustSort is true
        do {
            mustSort = false;
            for (int i = 0; i < count() - 1; i++) {
                if (getX(i) > getX(i + 1)) {
                    // exchange items:
                    double x = getX(i);
                    double y = getY(i);
                    setPoint(i, getX(i + 1), getY(i + 1));
                    setPoint(i + 1, x, y);
                    mustSort = true;
                }
            }
        }
        while (mustSort);
    }

    // The sortByY() function can be implemented in the similar way

    // Getting a string representation:
    @Override
    public String toString() {
        String s = "";
        for (int i = 0; i < count(); i++) {
            s += "x = " + getX(i) + " \ty = " + getY(i) + "\n";
        }
        return s + "\n";
    }

    // Testing sorting on four points:
    public void test() {
        addPoint(22, 45);
        addPoint(4, 11);
        addPoint(30, 5.5);
        addPoint(-2, 48);
        sortByX();
        System.out.println(this);
    }

}

Now we can implement different forms of the data structure.

3.2.2 Implementation through an Array of Point objects

The first possible implementation would be the creation of a Point class and use an array of Point references. A class for point representation can be added to the same package. The Point class will contain two fields and a constructor:

package ua.inf.iwanoff.java.third;

public class Point {
    private double x, y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    public void setPoint(double x, double y) {
        this.x = x;
        this.y = y;
    }
  
}

In the ArrayOfPointObjects class, we create a field: a reference to the Point array. We initialize this field with an empty array. The implementation of most of the functions is obvious. The greatest difficulty is concerned with the implementation of adding and removing points. In both cases, you need to create a new array of the correct length and overwrite the contents of the old one. In the main() function, we perform testing. All code of the AbstractArrayOfPoints.java file will look like this:

package ua.inf.iwanoff.java.third;

public class ArrayOfPointObjects extends AbstractArrayOfPoints {
    private Point[] p = { };
  
    @Override
    public void setPoint(int i, double x, double y) {
        if (i < count()) {
            p[i].setPoint(x, y);
        }
    }

    @Override
    public double getX(int i) {
        return p[i].getX();
    }

    @Override
    public double getY(int i) {
        return p[i].getY();
    }

    @Override
    public int count() {
        return p.length;
    }

    @Override
    public void addPoint(double x, double y) {
        // Create an array larger by one item:
        Point[] p1 = new Point[p.length + 1];
        // Copy all items:
        System.arraycopy(p, 0, p1, 0, p.length);
        // Write a new point to the last item:
        p1[p.length] = new Point(x, y);
        p = p1; // now p points to a new array
    }

    @Override
    public void removeLast() {
        if (p.length == 0) {
            return; // the array is already empty
        }
        // Create an array smaller by one item:
        Point[] p1 = new Point[p.length - 1];
        // Copy all items except the last one:
        System.arraycopy(p, 0, p1, 0, p1.length);
        p = p1; // now p points to a new array
    }

    public static void main(String[] args) {
        // An anonymous object can be created:
        new ArrayOfPointObjects().test();
    }

}

As a result, we get points sorted by coordinate X.

3.2.3 Implementation through Two Arrays

An alternative implementation involves creating two arrays to separately store the values of X and Y. We create an ArrayWithTwoArrays class using similar options. In the ArrayWithTwoArrays class, we create two fields (references to arrays of real numbers) and initialize them with empty arrays. The implementation of functions is similar to the previous version. In the main() function, we perform testing:

package ua.inf.iwanoff.java.third;

public class ArrayWithTwoArrays extends AbstractArrayOfPoints {
    private double[] ax = { };
    private double[] ay = { };
  
    @Override
    public void setPoint(int i, double x, double y) {
        if (i < count()) {
            ax[i] = x;
            ay[i] = y;
        }
    }

    @Override
    public double getX(int i) {
        return ax[i];
    }

    @Override
    public double getY(int i) {
        return ay[i];
    }

    @Override
    public int count() {
        return ax.length; // or ay.length, they are the same
    }

    @Override
    public void addPoint(double x, double y) {
        double[] ax1 = new double[ax.length + 1];
        System.arraycopy(ax, 0, ax1, 0, ax.length);
        ax1[ax.length] = x;
        ax = ax1;
        double[] ay1 = new double[ay.length + 1];
        System.arraycopy(ay, 0, ay1, 0, ay.length);
        ay1[ay.length] = y;
        ay = ay1;
    }

    @Override
    public void removeLast() {
        if (count() == 0) {
            return;
        }
        double[] ax1 = new double[ax.length - 1];
        System.arraycopy(ax, 0, ax1, 0, ax1.length);
        ax = ax1;
        double[] ay1 = new double[ay.length - 1];
        System.arraycopy(ay, 0, ay1, 0, ay1.length);
        ay = ay1;
    }

    public static void main(String[] args) {
        new ArrayWithTwoArrays().test();
    }

}

The results must be identical.

3.3 Using Interfaces with Default Methods Implementation

Suppose it is necessary to find the root of the equation using Tangent method (Newton method). This method involves the use of first and second derivative of the function for finding the root. The approximate value of the first derivative of any function can be found by the formula:

f '(x) = (f(x + dx) - f(x)) / dx

The smaller dx, the more accurate the derivative will be found. The second derivative can be found as the derivative of the first derivative.

The algorithm is as follows: on the given search interval we find the initial approximation. This will be the beginning of the interval (if the sign of the function and the second derivative at this point are the same) or the end of the interval (otherwise). Next, we calculate the following approximations by the formula:

xn+1 = xn - f(xn) / f '(xn)

Describe the interface. The first and second derivatives are calculated using methods with a default implementation:

package ua.inf.iwanoff.java.third;

public interface FunctionWithDerivatives {
    double DX = 0.001;
  
    double f(double x);
  
    default double f1(double x) {
        return (f(x + DX) - f(x)) / DX;
    }
  
    default double f2(double x) {
        return (f1(x + DX) - f1(x)) / DX;
    }
}

We implement a class with a static method for solving the equation:

package ua.inf.iwanoff.java.third;

public class Newton {
  
    public static double solve(double from, double to, double eps, FunctionWithDerivatives func) {
        double x = from;
        if (func.f(x) * func.f2(x) < 0) { // signs are different
            x = to;
        }
        double d;
        do {
            d = func.f(x) / func.f1(x);
            x -= d;
        }
        while (Math.abs(d) > eps);
        return x;
    }
}

We create a class that implements the interface, and solve equation:

package ua.inf.iwanoff.java.third;

public class FirstImplementation implements FunctionWithDerivatives {

    @Override
    public double f(double x) {
        return Math.sin(x - 0.5);
    }
  
    public static void main(String[] args) {
        System.out.println(Newton.solve(0, 1, 0.000001, new FirstImplementation()));
    }
}

For functions, we can redefine the mechanism for calculating the first and second derivatives. For example, for a cubic polynomial

f(x) = x36x2 + 12x – 9

we can define the first and second derivatives in the following way:

f '(x) = 3x2 – 12x + 12
f ''(x) = 6x – 12

Now the class that implements our interface can be as follows:

package ua.inf.iwanoff.java.third;

public class SecondImplementation implements FunctionWithDerivatives {

    @Override
    public double f(double x) {
        return x * x * x - 6 * x * x + 12 * x - 9;
    }

    @Override
    public double f1(double x) {
        return 3 * x * x - 12 * x + 12;
    }

    @Override
    public double f2(double x) {
        return 6 * x - 12;
    }

    public static void main(String[] args) {
        System.out.println(Newton.solve(0, 1, 0.000001, new SecondImplementation()));
    }
}

The explicit definition of derivatives can improve the efficiency of the algorithm.

3.4 Solving Equation using Dichotomy Method

3.4.1 Problem Statement

Assume that we want to solve some equation using dichotomy (bisection) method. Generally, equation can be as follows:

f(x) = 0

The dichotomy method allows us to find the only root of the equation. If there are no roots, or more than one, the results cannot be reliable.

All numeric methods require calculation of a left-hand member of an equation in different points. However, f(x) can be different in different projects. A special mechanism of transferring data about this function is required.

3.4.2 Using Abstract Class

The first approach uses concept of abstract classes. An abstract class called AbstractEquation defines an abstract f() function, as well as non-abstract method that solves an equation (solve()):

package ua.inf.iwanoff.java.third;

public abstract class AbstractEquation {
    public abstract double f(double x);
  
    public double solve(double a, double b, double eps) {
        double x = (a + b) / 2;
        while (Math.abs(b - a) > eps) {
            if (f(a) * f(x) > 0) {
                a = x;
            }
            else {
                b = x;
            }
            x = (a + b) / 2;
        }
        return x;
    }
}

Now we can create a new class with particular f() function:

package ua.inf.iwanoff.java.third;

public class SpecificEquation extends AbstractEquation {
    @Override
    public double f(double x) {
        return x * x - 2;
    }

    public static void main(String[] args) {
        SpecificEquation se = new SpecificEquation();
        System.out.println(se.solve(0, 2, 0.000001));
    }
}

3.4.3 Using the Interface and the Class that Implements It

Interfaces provide another approach to this problem. We can declare interface for representation of the left side of an equation. To create a new interface within Eclipse environment, use New | Interface function.

package ua.inf.iwanoff.java.third;

public interface LeftSide {
    double f(double x);
}

The Solver class provides a static method that solves an equation:

package ua.inf.iwanoff.java.third;

public class Solver {
    static double solve(double a, double b, double eps, LeftSide ls) {
        double x = (a + b) / 2;
        while (Math.abs(b - a) > eps) {
          if (ls.f(a) * ls.f(x) > 0) {
                a = x;
            }
            else {
                b = x;
            }
            x = (a + b) / 2;
        }
        return x;
    }
}

The class that implements the interface contains a specific implementation of the f() function:

package ua.inf.iwanoff.java.third;

class MyEquation implements LeftSide {
    @Override
    public double f(double x) {
        return x * x - 2;
    }
}

public class InterfaceTest {

    public static void main(String[] args) {
        System.out.println(Solver.solve(0, 2, 0.000001, new MyEquation()));
    }

}

The program can be modified to take into account Java 8 capabilities. The root finding method can be implemented inside the interface:

package ua.inf.iwanoff.java.third;

public interface FunctionToSolve {
    double f(double x);

    static double solve(double a, double b, double eps, FunctionToSolve func) {
        double x = (a + b) / 2;
        while (Math.abs(b - a) > eps) {
            if (func.f(a) * func.f(x) > 0) {
                a = x;
            }
            else {
                b = x;
            }
            x = (a + b) / 2;
        }
        return x;
    }
}

Now you should call FunctionToSolve.solve() instead of Solver.solve().

3.4.4 Using an Anonymous Class

If the function is only necessary to solve the equation, it can be defined in an anonymous class:

package ua.inf.iwanoff.java.third;

public class SolveUsingAnonymousClass {

    public static void main(String[] args) {
        System.out.println(FunctionToSolve.solve(0, 2, 0.000001, new FunctionToSolve() {
            @Override
            public double f(double x) {
                return x * x - 2;
            }
        }));
    }

}

3.4.5 Using Lambda Expressions

The left part of the equation can be defined by the lambda expression (instead of an anonymous class):

package ua.inf.iwanoff.java.third;

public class SolveUsingLambda {

    public static void main(String[] args) {
        System.out.println(FunctionToSolve.solve(0, 2, 0.000001, x -> x * x - 2));
    }

}

3.4.6 Using References to Methods

The previous task can be solved using references to methods. We can implement the function as a separate static method:

package ua.inf.iwanoff.java.third;

public class SolveUsingReference {

    public static double f(double x) {
        return x * x - 2;
    }
  
    public static void main(String[] args) {
        System.out.println(FunctionToSolve.solve(0, 2, 0.000001, SolveUsingReference::f));
    }

}

3.5 Hierarchy of the "Country" and "Census" Classes

Assume that we need to extend a census data processing program so that we can potentially use different data structures to represent a sequence of censuses. The class representing a country should not contain any data about the sequence of censuses. This class should be abstract. The country data are the name, the area, and a sequence of references to an object of type "Census". Fields are needed to define the following functions:

  • functions for accessing data;
  • abstract functions for accessing the census sequence;
  • overriding the equals() method for checking the equivalence of objects;
  • overriding the hashCode() method for obtaining hash codes of objects.

Since there are different ways for storing data about the sequence of censuses, it is advisable to create a hierarchy of classes. To demonstrate the possibilities of representing the sequence of censuses in different data structures: an array and a singly linked list.

In turn, the class representing the census should include data about the census year, population, and comments. It is also necessary to override the methods for checking equivalence and obtaining a hash code. To ensure sorting by increasing population, a Comparable interface should be implemented and the compareTo() function should provide a "natural" comparison by population.

The program must implement the following functions:

  • calculating population density according to a specific census;
  • determining the census with the largest population;
  • checking the occurrence of a certain word in a comment;
  • sorting censuses by population;
  • sorting censuses in alphabetic order of comments.

It is necessary to additionally implement the functions of obtaining the representation of the census as a string, checking the presence of words and the sequence of letters in the comments, and testing. As in the previous implementation, these functions will be presented in separate classes.

We create a new class Census in the package ua.inf.iwanoff.java.third. Its code can be copied from the package ua.inf.iwanoff.java.second, but, unfortunately, we cannot use the previously created class directly, since we need to add methods equals(), hashCode() and compareTo(). The class code will be as follows:

package ua.inf.iwanoff.java.third;

import java.util.Objects;

/**
 * The class is responsible for presenting the census.
 * The census is represented by year, population and comment
 */
public class Census implements Comparable<Census> {
    private int year;
    private int population;
    private String comments;

    /**
     * Constructor initializes the object with default values
     */
    public Census() {
    }

    /**
     * Constructor initializes the object with given values
     *
     * @param year year of census
     * @param population population in the specified year
     * @param comments the text of the comment
     */
    public Census(int year, int population, String comments) {
        this.year = year;
        this.population = population;
        this.comments = comments;
    }

    /**
     * Returns the census year
     * @return year of census in the form of an integer value
     */
    public int getYear() {
        return year;
    }

    /**
     * Sets the value of the census year
     * @param year year of census in the form of an integer value
     */
    public void setYear(int year) {
        this.year = year;
    }

    /**
     * Returns the population
     * @return the population in the form of an integer value
     */
    public int getPopulation() {
        return population;
    }

    /**
     * Sets the population
     * @param population the population in the form of an integer value
     */
    public void setPopulation(int population) {
        this.population = population;
    }

    /**
     * Returns the comment string
     * @return a census comment in the form of a string
     */
    public String getComments() {
        return comments;
    }

    /**
     * Sets the content of a comment string
     * @param comments a census comment in the form of a string
     */
    public void setComments(String comments) {
        this.comments = comments;
    }
    /**
     * Indicates whether some other census object is "equal to" this one
     * @param obj the reference to the census object with which to compare
     * @return {@code true}, if this object is the same as the obj argument
     *         {@code false} otherwise
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Census c)) {
            return false;
        }
        return c.year == year &&
               c.population == population &&
               c.comments.equals(comments);
    }

    /**
     * Returns a hash code value for the census object
     * @return a hash code value for this object
     */
    @Override
    public int hashCode() {
        return Objects.hash(year, population, comments);
    }

    /**
     * Compares this object with the specified object for order. Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object
     * @param c the census object to be compared
     * @return the result of comparison
     */
    @Override
    public int compareTo(Census c) {
        return Integer.compare(population, c.population);
    }
}

As can be seen from the given code, the Census class implements the Comparable<Census> interface. The implementation of this interface requires the addition of a compareTo() method, which defines a "natural" comparison by population.

We can copy the CensusUtilities class code the previous laboratory training's example:

package ua.inf.iwanoff.java.third;

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.getComments().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.getComments().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
     * @param item reference that is added
     * @return an updated array of censuses
     */
    public static Census[] addToArray(Census[] arr, Census item) {
        Census[] newArr;
        if (arr != null) {
            newArr = new Census[arr.length + 1];
            System.arraycopy(arr, 0, newArr, 0, arr.length);
        }
        else {
            newArr = new Census[1];
        }
        newArr[newArr.length - 1] = item;
        return newArr;
    }
}

We cannot link to the code of the previous work directly, because the functions actually receive a reference to the new Census class.

The class AbstractCountry also contains methods equals() and hashCode(). The AbstractCountry class code will be as follows:

package ua.inf.iwanoff.java.third;

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 class AbstractCountry {
    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 sequence
     *
     * @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 censuses array of references to censuses
     */
    public abstract void setCensuses(Census[] censuses);


    /**
     * 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[] getCensuses();

    /**
     * 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 AbstractCountry c)) {
            return false;
        }
        if (!getName().equals(c.getName()) || getArea() != c.getArea()) {
            return false;
        }
        return Arrays.equals(getCensuses(), c.getCensuses());
    }

    /**
     * Returns a hash code value for the country
     * @return a hash code value
     */
    @Override
    public int hashCode() {
        return Objects.hash(name, area, Arrays.hashCode(getCensuses()));
    }
}

In the derived class CountryWithArray, we use an array to represent the sequence of censuses:

package ua.inf.iwanoff.java.third;

/**
 * Class for presenting the country in which the censuses are carried out.
 * Census data are represented with an array
 */
public class CountryWithArray extends AbstractCountry {
    private Census[] censuses;

    /**
     * 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 censuses[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) {
        censuses[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 (getCensuses() != null) {
            for (Census c : getCensuses()) {
                if (c.equals(census)) {
                    return false;
                }
            }
        }
        setCensuses(CensusUtilities.addToArray(getCensuses(), census));
        return true;
    }

    /**
     * Returns the number of censuses in the sequence
     * @return count of censuses
     */
    @Override
    public int censusesCount() {
        return censuses.length;
    }

    /**
     * Removes all of the censuses from the sequence
     */
    @Override
    public void clearCensuses() {
        censuses = null;
    }

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

    /**
     * Returns an array of censuses obtained from the sequence
     * @return array of references to censuses
     */
    @Override
    public void setCensuses(Census[] censuses) {
        this.censuses = censuses;
    }
}

In another derived class, we implement the simplest version of a singly linked list for storing a sequence of censuses:

package ua.inf.iwanoff.java.third;

/**
 * Class for presenting the country in which the censuses are carried out.
 * Census data is represented by a singly linked list
 */
public class CountryWithLinkedList extends AbstractCountry {
    /**
     * A helper class that represents a node of a linked list
     */
    private class Node {
        Census census;
        Node next;
    }

    private int size = 0;     // count of censuses
    private Node head = null; // reference to the beginning of the list

    /**
     * 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) {
        if (i < 0 || i >= size || head == null) {
            return null;
        }
        Node node = head;
        for (int j = 0; j < i; j++) {
            node = node.next;
        }
        return node.census;
    }

    /**
     * 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) {
        if (i < 0 || i >= size || head == null) {
            return;
        }
        Node node = head;
        for (int j = 0; j < i; j++) {
            node = node.next;
        }
        node.census = 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) {
        Node newNode = new Node();
        newNode.census = census;
        newNode.next = null;
        if (head == null) {
            head = newNode;
            size = 1;
            return true;
        }
        Node node = head;
        Node previous = null;
        for (int i = 0; i < size; i++) {
            if (node.census.equals(census)) {
                return false;
            }
            previous = node;
            node = node.next;
        }
        node = newNode;
        previous.next = node;
        size++;
        return true;
    }

    /**
     * Returns the number of censuses in the sequence
     * @return count of censuses
     */
    @Override
    public int censusesCount() {
        return size;
    }

    /**
     * Removes all the censuses from the sequence
     */
    @Override
    public void clearCensuses() {
        head = null;
        size = 0;
    }

    /**
     * Puts data from an array of censuses into a sequence
     * @param censuses array of references to censuses
     */
    @Override
    public void setCensuses(Census[] censuses) {
        clearCensuses();
        for (Census census : censuses) {
            addCensus(census);
        }
    }

    /**
     * Returns an array of censuses obtained from the sequence
     * @return array of references to censuses
     */
    @Override
    public Census[] getCensuses() {
        Census[] censuses = new Census[size];
        Node node = head;
        for (int i = 0; i < size; i++) {
            censuses[i] = node.census;
            node = node.next;
        }
        return censuses;
    }
}

We add the sorting functions to the code of the previously created class, which should also be copied to the new package:

package ua.inf.iwanoff.java.third;

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(AbstractCountry country, int year) {
        for (int i = 0; i < country.censusesCount(); i++) {
            if (year == country.getCensus(i).getYear()) {
                return country.getCensus(i).getPopulation() / 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(AbstractCountry country) {
        Census census = country.getCensus(0);
        for (int i = 1; i < country.censusesCount(); i++) {
            if (census.getPopulation() < country.getCensus(i).getPopulation()) {
                census = country.getCensus(i);
            }
        }
        return census.getYear();
    }

    /**
     * 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(AbstractCountry country, String word) {
        Census[] result = null;
        for (Census census : country.getCensuses()) {
            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(AbstractCountry country) {
        Census[] censuses = country.getCensuses();
        Arrays.sort(censuses);
        country.setCensuses(censuses);
    }

    /**
     * Sorts the sequence of censuses in the alphabetic order of comments
     *
     * @param country reference to a country
     */
    public static void sortByComments(AbstractCountry country) {
        Census[] censuses = country.getCensuses();
        Arrays.sort(censuses, Comparator.comparing(Census::getComments));
        country.setCensuses(censuses);
    }
}

A separate class contains the means for obtaining data representation in the form of strings:

package ua.inf.iwanoff.java.third;

/**
 * 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 " + getYear() + ". Population: " + getPopulation() + 
               ". Comments: " + getComments();
    }

    /**
     * Returns a string representation of the country
     * 
     * @param country reference to a country
     * @return a string representation of the country
     */
    public static String toString(AbstractCountry 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 + "";
    }
}

Demonstration of class capabilities is implemented in the class CountryDemo:

package ua.inf.iwanoff.java.third;

import static ua.inf.iwanoff.java.third.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 AbstractCountry setCountryData(AbstractCountry 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(AbstractCountry country, String word) {
        Census[] result = findWord(country, word);
        if (result == null) {
            System.out.println("The word \"" + word + "\" is not present in the comments.");
        }
        else {
            System.out.println("The word \"" + word + "\" is present in the comments:");
            for (Census census : result) {
                System.out.println(StringRepresentations.toString(census));
            }
        }
    }

    /**
     * Performs testing search methods 
     * @param country reference to a country
     */
    public static void testSearch(AbstractCountry country) {
        System.out.println("Population density in 1979: " + density(country, 1979));
        System.out.println("The year with the maximum population: " + maxYear(country) + "\n");
        printWord(country, "census");
        printWord(country, "second");
    }

    /**
     * Performs testing search methods 
     * @param country reference to a country
     */
    public static void testSorting(AbstractCountry 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
     * @param args command line arguments (not used)
     */
    public static void main(String[] args) {
        AbstractCountry country = setCountryData(new CountryWithArray());
        testSearch(country);
        testSorting(country);
        System.out.println("----------------------------------------");
        country = setCountryData(new CountryWithLinkedList());
        testSearch(country);
        testSorting(country);
    }
}

Before main program output, we'll get true (five times) and false (one time) as a result of addCensus() function.

4 Exercises

  1. Create a hierarchy of classes: Book and Manual. Implement constructors and access methods. Override toString() method. Create an array that contains items of different types (in the main() function). Display items.
  2. Create a hierarchy of classes: Movie and TV Series. Implement constructors and access methods. Override toString() method. Create an array that contains items of different types (in the main() function). Display items.
  3. Create a hierarchy of classes: City and Capital. Implement constructors and access methods. Override toString() method. Create an array that contains items of different types (in the main() function). Display items.
  4. Create a class to represent a named matrix with a string-type field: the name of the matrix and the field that represents the two-dimensional array. Implement methods of cloning, equivalence checking and obtaining string representation. Carry out testing.

5 Quiz

  1. What is the meaning of inheritance?
  2. What are advantages of common base class?
  3. What members of the base class are not inherited?
  4. How can you initialize base class?
  5. How can you call base class method of the same name?
  6. How can you use super keyword?
  7. How can you override method with the final modifier?
  8. Is multiple inheritance of classes allowed?
  9. How can you cast base class reference to derived class reference?
  10. What makes sense to use annotations?
  11. What are advantages of polymorphism?
  12. What is the difference between virtual and non-virtual functions?
  13. How can you define virtual function in Java?
  14. Some class is defined with final modifier. Can you create virtual functions within this class?
  15. Why functions with private modifier are non-virtual?
  16. Can you create abstract class without abstract methods?
  17. Can abstract classes contain non-abstract methods?
  18. What are differences between abstract classes and interfaces?
  19. Can you define fields within interface?
  20. Can you declare several base interfaces for a new derived interface?
  21. What requirements must meet class that implements some interface?
  22. Can you implement several interfaces within a single class?
  23. What requirements should an object satisfy, so that an array of such objects can be sorted without defining a sorting flag?
  24. How to define a special rule for sorting array items?
  25. How to access local class from outside of block?
  26. Is it possible to define several public classes in a single source file?
  27. Is it possible to create an object of non-static inner class without outer class object?
  28. Can non-static inner classes contain static members?
  29. What is the difference between static nested classes and non-static inner classes?
  30. Can static nested classes contain non-static members?
  31. Is it possible to create classes within interfaces?
  32. Is an anonymous class static?
  33. Why is it not possible to create explicit constructor of an anonymous class?
  34. What is lambda expression?
  35. What is a functional interface?
  36. What are the benefits of lambda expressions?
  37. What are the references to methods used for?
  38. What is the process of cloning objects?
  39. What is the use of the override of the equals() function?
  40. What is the use of the override of the hashCode() function?

 

up