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 individual assignment. Particular function is given in the individual task according to your own index in the group students list (index of variant).

Each class should be separately tested. The main() function of the first class should contain creation of necessary object and invocation of methods implementing an individual assignment. Results should be shown on console window.

#
First Entity
Second Entity
Main Assignment:
Find and show the following data:
Entity
Obligatory Fields
Entity
Obligatory Fields
1, 17 Weather Season, comments Day Date, temperature, comments Average temperature, day with the maximum temperature, day with the longest comment
2, 18 Course of studies Title, presence of an exam Practical training Date, topic, count of students Average count of students, lesson with maximum students present, list of topics with particular word in a title
3, 19 Tram stop Name, list of tram lines Hour Count of passengers, comments Total count of passengers, stop with the minimum count of passengers, the longest comment
4, 20 Course of studies Title, surname of lecturer Lecture Date, topic, count of students Lecture with maximum students present, list of topics with particular word in a title, the last letter in the lecturer's surname
5, 21 Weather Year, comments Measurement of temperature Date, temperature, comments Measurements with the minimum temperature, with the maximum count of words in measurement comments, the last letter in a weather comment
6, 22 Conference Title, place Session Date, topic, count of members Average count of members, session with the maximum count of members, length of title
7, 23 Exhibition Title, surname of painter Day Count of visitors, comments Total count of visitors, day with the minimum count of visitors, list of comments with the particular word
8, 24 Subway station Name, year of opening Hour Count of passengers, comments Total count of passengers, hours with the minimum count of passengers, hours the maximum count of words in comments
9, 25 Doctor Surname, specialization Reception Day, shift, count of visitors Total count of visitors, reception with the minimum count of visitors, surname length
10, 26 Music band Name of band, surname of leader Professional tour City, year, count of concerts Professional tour with maximum count of concerts, list of tours to particular city, the last letter in the surname of leader
11, 27 Workshop Name, address Shift Count of repaired computers Total count of computers, shift with the maximum count of repaired computers, length of street name
12, 28 Doctor Surname, length of service Reception Day, count of visitors, comments Average count of visitors, reception with the minimum count of visitors, reception with the largest comments
13, 29 Tram line Number, average traffic interval Tram stop Name, count of passengers Total count of passengers, stops with the minimum count of passengers, stops with the longest name
14, 30 Twenty-four-hour kiosk Denomination, address Hour Count of purchasers, comments Total count of buyers, hour the minimum count of buyers, comments with particular words
15, 31 Musician Surname, genre Concert Date, gate Common gate, concert with the minimum gate, count of words in genre name
16, 32 Phone number Number, operator Calls Date, duration of calls, funds used for calls Average salary per day for the period; the number of days when the cost of a minute of conversation exceeded the set value;
Days when the number of minutes of conversation was even

The class that represents the second entity of an individual task should contain data and access methods, overridden toString(), equals(), and hashCode() functions, as well as the implementation of the functions defined in the previous assignments (if necessary). This class 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 toString() method for output object data;
  • overridden equals() method for checking equality of objects;
  • overridden hashCode() method for getting hash codes;
  • implementation of search methods according to specific criteria;
  • implementation of the function of sorting items of the sequence on the first criterion of an individual task by the Bubble method;
  • implementation of the function of sorting items of the sequence on the second criterion of the individual task by the Insertion method;
  • implementation of testing the classes' functionality.

Sorting criteria are determined by the student's number in the group list. Search functions should return arrays of objects (or null, if no search results). 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

Derived class from the created abstract classes (for convenience FirstEntityWithArray) should contain fields of appropriate types, in particular, the sequence of elements of the second entity should be represented as an array.

Create another class (conventionally FirstEntityWithSorting) derived from the previously created FirstEntityWithArray class. This class should override sorting methods by use the standard Arrays.sort() function instead of bubble sorting and insertion 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.

Testing of the program should include the previously implemented task, as well as sorting according to specific criteria.

Note: you should create classes with meaningful names that reflect the physical nature of the individual task, not FirstEntity, SecondEntity, etc.

Javadoc comments should be added to the source code.

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 and implementation of the sortByY() method).

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. The syntax of inheritance is as follows:

class DerivedClass extends BaseClass {
    // class body
}

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(). 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.

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.

Here are examples of using super keyword:

class BaseClass {
    int i, j;
    BaseClass(int i, int j) {
        this.i = i;
        this.j = j;
    }
}

class DerivedClass extends BaseClass {
    int k;
    DerivedClass(int i, int j, int k) {
        super(i, j);
        this.k = k;
    }
}

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. The method declared as final cannot be overridden. For instance:

final class A {
    void f() { }
}

class B {
    final void g() { } 
}

class C extends A { // Error! Cannot inherit from A
  
}

class D extends B {
    void g() { }    // Error! g() 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.

class Base {
    static void f(Base b) { }
}

class Derived extends Base {
 
    public static void main(String[] args) {
        Base b;
        b = new Derived(); // Implicit type conversion
        Derived d = new Derived();
        f(d);              // Implicit type conversion
    }
}    

Reverse conversion should be done explicitly:

Base b = new Base();
Derived d = (Derived) b;

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 (x instanceof SomeClass)
    ((SomeClass)x).someMethod();

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 { // Помилка компіляції

}

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() {
        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++, late binding is implemented through virtual functions mechanism. 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 MyClass {
    int i = 10;
	
    @Override
    public String toString() {
        return "i = " + i; 
    }

    public static void main(String[] args) {
        MyClass mc = new MyClass();
        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

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 SomeFunctions {
    void f();
    int g(int x);
}

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. 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 SomeFunctions {
    void f();
    int g(int x);
}

class SomeClass implements SomeFunctions {
    @Override
    public void f() {

    }

    @Override
    public int g(int x) {
        return x;
    }
}

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

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

interface SomeFunctions {
    void f();
    int g(int x);
}

interface AnotherFunction {
    void h(int z);
}

interface AllFunctions extends SomeFunctions, AnotherFunction {
    int g(int x); // the declaration may be repeated
}

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

class Implementation implements AllFunctions {
    @Override
    public void f() {

    }

    @Override 
    // One implementation is used for both base and derived interfaces:
    public int g(int x) {
        return x;
    }

    @Override
    public void h(int z) {

    }
}

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

class AnotherImplementation implements SomeFunctions, AnotherFunction {
    @Override
    public void f() {

    }

    @Override
    public int g(int x) {
        return x;
    }

    @Override
    public void h(int z) {

    }
}

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.

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
}

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 elements. All elements 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:

public static void sort(Object[] a, Comparator c)
public static void sort(Object[] a, int fromIndex, int toIndex, Comparator c) // sort some part of items

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.

A separate category is static nested classes, the use of which is similar to the nested classes in C++ and C#.

2.6.2 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 elements of outer class objects
  • you cannot create object of inner class without object of outer class.

In this regard, Java offers a special mechanism for creating objects of the inner classes. This mechanism is illustrated in the following example:

class Outer {
    int k = 100;

    class Inner {
        void show() {
            System.out.println(k);
        }
    }

}

public class Test {

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

}

Non-static inner classes cannot contain static members.

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

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. In the following example, new anonymous class is created for definition of sorting order of string-type elements of an array:

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

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 elements of external blocks, these elements should be declared as final.

2.6.4 Static Nested Classes

Static nested classes have access only to static members of outer classes. Objects of such classes can be created without the creation of objects of outer classes:

class Outer {
    int k = 100;
    static int m = 200;

    static class Inner {
        void show() {
            // k unavailable
            System.out.println(m);
        }
    }
}

public class Test {

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

Static nested classes can contain their own static elements, as well as own nested static and non-static classes.

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.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 {
    public 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 {
    public 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 {
    public 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 generic interfaces that are described in the java.util.function package.

Interface Description
BiConsumer<T,U> Represents an operation that accepts two input arguments and returns no result
BiFunction<T,U,R> Represents a function that accepts two arguments and produces a result
BinaryOperator<T> Represents an operation upon two operands of the same type, producing a result of the same type as the operands
BiPredicate<T,U> Represents a predicate (boolean-valued function) of two arguments
BooleanSupplier Represents a supplier of boolean-valued results
Consumer<T> Represents an operation that accepts a single input argument and returns no result
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
DoubleFunction<R> Represents a function that accepts a double-valued argument and produces a 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
Function<T,R> Represents a function that accepts one argument and produces a 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
IntFunction<R> Represents a function that accepts an int-valued argument and produces a 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
LongFunction<R> Represents a function that accepts a long-valued argument and produces a 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
ObjDoubleConsumer<T> Represents an operation that accepts a T-valued and a double-valued argument, and returns no result
ObjIntConsumer<T> Represents an operation that accepts a T-valued and a int-valued argument, and returns no result
ObjLongConsumer<T> Represents an operation that accepts a T-valued and a long-valued argument, and returns no result
Predicate<T> Represents a predicate (boolean-valued function) of one argument
Supplier<T> Represents a supplier of results
ToDoubleBiFunction<T,U> Represents a function that accepts two arguments and produces a double-valued result
ToDoubleFunction<T> Represents a function that produces a double-valued result
ToIntBiFunction<T,U> Represents a function that accepts two arguments and produces an int-valued result
ToIntFunction<T> Represents a function that produces an int-valued result
ToLongBiFunction<T,U> Represents a function that accepts two arguments and produces a long-valued result
ToLongFunction<T> Represents a function that produces a long-valued result
UnaryOperator<T> Represents an operation on a single operand that produces a result of the same type as its operand

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 the Function 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.Function;

public class ComposeDemo {
  
    public static Double calc(Function<Double , Double> operator, Double x) {
        return operator.apply(x);
    }
  
    public static void main(String[] args) {
        Function<Double , Double> addTwo = x -> x + 2;
        Function<Double , Double> 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 elementwise, 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 elementwise 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 generate 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 an elementwise 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:

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 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 elementwise (calls the equals() method):

Arrays.equals(array1, array2);

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.

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 {
    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 {
    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 and functional interfaces. 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 develop a program that provides classes for representation of a country and a census. Data members of Country class are name, area, and a sequence of references to Census object.

The Census class includes data about year, population, and comments.

The following methods should be implemented:

  • calculation of population density for a given year
  • finding census with the maximum population
  • finding particular word in a comment
  • sorting censuses by population
  • sorting censuses in alphabetic order of comments.

The Census class will implement the functions of obtaining a string representation, checking the equivalence, getting hash codes, checking the presence of words and the sequences of letters in the comments and testing. In order to ensure sorting for the population increase, the Comparable interface should be implemented and the compareTo() function should provide a "natural" comparison of the population.

Given the different options for storing the sequence of censuses, it is advisable to create a hierarchy of classes. Basic abstract AbstractCountry class that represent country should not contain data about the sequence of censuses, only declaration of access functions and sorting methods should be declared. It is necessary to implement functions of getting string representation, getting hash codes, checking of equivalence, calculation of population density in for a certain census, the search for a census with the maximum population, as well as checking whether the comments contain a certain word.

The derived class representing the country will use an array to store the sequence of censuses. It is necessary to implement census sorting functions using standard array sorting tools.

We create a new Census class in ua.inf.iwanoff.java.third package. Its code will be as follows:

package ua.inf.iwanoff.java.third;

import java.util.StringTokenizer;

/**
 * The class is responsible for presenting the census.
 * The census is represented by year, population and comments
 */
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;
    }

    /**
     * Provides a census data in the form of a string
     *
     * @return string representation of a census data
     */
    @Override
    public String toString() {
        return "The census in " + getYear() + ". Population: " + getPopulation() + 
               ". Comments: " + getComments();
    }

    /**
     * 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.getYear() == getYear() &&
               c.getPopulation() == getPopulation() &&
               c.getComments().equals(getComments());
    }

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

    /**
     * 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(getPopulation(), c.getPopulation());
    }

    /**
     * Checks whether the word can be found in the comment text
     * @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 boolean containsWord(String word) {
        StringTokenizer st = new StringTokenizer(getComments());
        String s;
        while (st.hasMoreTokens()) {
            s = st.nextToken();
            if (s.equalsIgnoreCase(word)) {
                return true;
            }
        }
        Object dd;
        return false;
    }

    /**
     * Checks whether the substring can be found in the comment text
     * @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 boolean containsSubstring(String substring) {
        return getComments().toUpperCase().indexOf(substring.toUpperCase()) >= 0;
    }

    /**
     * Display the test results whether the word or substring
     * can be found in the comment text
     * @param word the word or substring for which the verification is performed
     */
    private void testWord(String word) {
        if (containsWord(word)) {
            System.out.println("Comment contains the word \"" + word + "\"");
        }
        else {
            System.out.println("Comment does not contain the word \"" + word + "\"");
        }
        if (containsSubstring(word)) {
            System.out.println("Comment contains the text \"" + word + "\"");
        }
        else {
            System.out.println("Comment does not contain the text \"" + word + "\"");
        }
    }

    /**
     * Tests checking of the presence of a word or substring
     * in a comment to the census
     */
    protected void testCensus() {
        setYear(2001);
        setPopulation(48475100);
        setComments("The first census in the independent Ukraine");
        System.out.println(this);
        testWord("Ukraine");
        testWord("Kraine");
        testWord("Ukraina");
    }

    /**
     * program shows results of testing presence of some word
     * or substring in the comment text
     * @param args command line arguments (not used)
     */
    public static void main(String[] args) {
        new Census().testCensus();
    }
}

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.

The AbstractCountry class also contains equals(), hashCode() and toString() methods. We also implement an auxiliary static addToArray() function, which adds a new item to the array. We implement all functions that not depend on the internal representation of the sequence of censuses. The AbstractCountry class code will as follows:

package ua.inf.iwanoff.java.third;

import java.util.Arrays;

/**
 * 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
     *
     * <p> A subclass must provide an implementation of this method
     *
     * @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 abstract boolean addCensus(int year, int population, String comments);

    /**
     * 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 of the censuses from the sequence
     *
     * <p> A subclass must provide an implementation of this method
     */
    public abstract void clearCensuses();

    /**
     * Sorts the sequence of censuses by population
     *
     * <p> A subclass must provide an implementation of this method
     */
    public abstract void sortByPopulation();

    /**
     * Sorts the sequence of censuses in the alphabetic order of comments
     *
     * <p> A subclass must provide an implementation of this method
     */
    public abstract void sortByComments();

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

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

    /**
     * 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 (int) (getName().hashCode() * area * Arrays.hashCode(getCensuses()));
    }

    /**
     * Returns a string representation of the country
     *
     * @return a string representation of the country
     */
    @Override
    public String toString() {
        StringBuilder result = new StringBuilder(getName() + ". Area: " + getArea() + " sq.km.");
        for (int i = 0; i < censusesCount(); i++) {
            result.append("\n").append(getCensus(i));
        }
        return result + "";
    }

    /**
     * Returns the population density for the specified year
     * @param year specified year (e.g. 1959, 1979, 1989, etc.)
     * @return population density for the specified year
     */
    public double density(int year) {
        for (int i = 0; i < censusesCount(); i++) {
            if (year == getCensus(i).getYear()) {
                return getCensus(i).getPopulation() / getArea();
            }
        }
        return 0;
    }

    /**
     * Finds and returns a year with the maximum population
     * @return year with the maximum population
     */
    public int maxYear() {
        Census census = getCensus(0);
        for (int i = 1; i < censusesCount(); i++) {
            if (census.getPopulation() < getCensus(i).getPopulation()) {
                census = getCensus(i);
            }
        }
        return census.getYear();
    }

    /**
     * Creates and returns an array of censuses with the specified word in the comments
     * @param word a word that is found
     * @return array of censuses with the specified word in the comments
     */
    public Census[] findWord(String word) {
        Census[] result = null;
        for (Census census : getCensuses()) {
            if (census.containsWord(word)) {
                result = addToArray(result, census);
            }
        }
        return result;
    }

    /**
     * Displays census data that contains a certain word in comments
     * @param word a word that is found
     */
    private void printWord(String word) {
        Census[] result = findWord(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 (AbstractCensus census : result) {
                System.out.println(census);
            }
        }
    }

    /**
     * Auxiliary function of creating a new country object
     * @return reference to the new country object
     */
    public AbstractCountry createCountry() {
        setName("Ukraine");
        setArea(603628);
        // Adding censuses with the output of the result (false / true):
        System.out.println(addCensus(1959, 41869000, "The first postwar census"));
        System.out.println(addCensus(1970, 47126500, "Population increases"));
        System.out.println(addCensus(1979, 49754600, "No comments"));
        System.out.println(addCensus(1989, 51706700, "The last soviet census"));
        System.out.println(addCensus(2001, 48475100, "The first census in the independent Ukraine"));
        System.out.println(addCensus(2001, 48475100, "The first census in the independent Ukraine"));
        return this;
    }

    /**
     * Performs testing class methods
     */
     public void testCountry() {
        System.out.println("Population density in 1979: " + density(1979));
        System.out.println("The year with the maximum population: " + maxYear() + "\n");

        printWord("census");
        printWord("second");

        sortByPopulation();
        System.out.println("\nSorting by population:");
        System.out.println(this);

        sortByComments();
        System.out.println("\nSorting by comments:");
        System.out.println(this);
    }
}

In the derived class, we use an array to represent the sequence of censuses. To compare the censuses, the reference to the java.util.Comparator.comparing() method is used, which provides the required values by comparison. CountryWithArray class code will be the following:

package ua.inf.iwanoff.java.third;

import java.util.Arrays;
import java.util.Comparator;

/**
 * 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(addToArray(getCensuses(), census));
        return true;
    }

    /**
     * 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
     */
    @Override
    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
     * @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 sequence
     * @return array of references to censuses
     */
    @Override
    public Census[] getCensuses() {
        return censuses;
    }

    /**
     * Puts data from an array of censuses into a sequence
     * @param censuses array of references to censuses
     */
    @Override
    public void setCensuses(Census[] censuses) {
        this.censuses = censuses;
    }

    /**
     * Sorts the sequence of censuses by population
     */
    @Override
    public void sortByPopulation() {
        Arrays.sort(censuses);
    }

    /**
     * Sorts the sequence of censuses in the alphabetic order of comments
     */
    @Override
    public void sortByComments() {
        Arrays.sort(censuses, Comparator.comparing(Census::getComments));
    }

    /**
     * Demonstration of functions' work
     * @param args command line arguments (not used)
     */
    public static void main(String[] args) {
        new CountryWithArray().createCountry().testCountry();
    }
}

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 elements 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 a local class static?
  33. Is an anonymous class static?
  34. Why is it not possible to create explicit constructor of an anonymous class?
  35. What is lambda expression?
  36. What is a functional interface?
  37. What are the benefits of lambda expressions?
  38. What are the references to methods used for?
  39. What is the process of cloning objects?
  40. What is the use of the override of the equals() function?
  41. What is the use of the override of the hashCode() function?
  42. How to compare array items?

 

up