Laboratory Training 3
Use of Inheritance and Polymorphism in Java
1 Training Tasks
1.1 Individual Assignment
Create a hierarchy of classes that represent entities according to task 1.5 of the previous laboratory training. The first entity should be represented by a hierarchy of abstract and non-abstract derived classes.
The class that represents the second entity of an individual task should contain data and access
methods, overridden equals()
and hashCode()
functions, It also
should implement the Comparable
interface
for the natural comparison of objects when sorting by one of the criteria.
The base abstract class that represents the first entity of individual tasks (conventionally AbstractFirstEntity
)
should contain:
- access functions (getters and setters);
- abstract methods for access to a sequence of elements of the second abstract class;
- overridden
equals()
method for checking equality of objects; - overridden
hashCode()
method for getting hash codes.
To search for the necessary data and sort it according to the task and format the data output to the console, separate auxiliary classes should be created. The search functions should reproduce the tasks of the previous laboratory training.
Sorting criteria are determined by the student's number in the group list. Individual tasks are listed in the following table:
#
| First Criterion of Sorting | Second Criterion of Sorting |
---|---|---|
1, 17
|
By temperature (in decreasing order) | By comments in alphabetical order |
2, 18
|
By count of students (in increasing order) | By length of topic (in increasing order) |
3, 19
|
By count of passengers (in increasing order) | By comments in alphabetical order |
4, 20
|
By count of words in topics (in increasing order) | By topics in alphabetical order |
5, 21
|
By temperature (in increasing order) | By length of comments (in decreasing order) |
6, 22
|
By count of members (in increasing order) | By title in alphabetical order |
7, 23
|
By count of visitors (in increasing order) | By comments in alphabetical order |
8, 24
|
By count of passengers (in decreasing order) | By length of comments (in decreasing order) |
9, 25
|
By date (in decreasing order) | By count of visitors (in increasing order) |
10, 26
|
By count of concerts (in increasing order) | By name of city in alphabetical order |
11, 27
|
By shift index (in increasing order) | By count of computers (in increasing order) |
12, 28
|
By count of visitors (in increasing order) | By comments in alphabetical order |
13, 29
|
By count of passengers (in decreasing order) | By name in alphabetical order |
14, 30
|
By count of purchasers (in decreasing order) | By comments in alphabetical order |
15, 31
|
By date (in decreasing order) | By increasing the number of viewers |
16, 32
|
By decreasing the number of minutes of conversation | By increasing the amount of funds used to conversation |
Use the standard Arrays.sort()
function for sorting. The first sorting should be provided
by the implementation of the Comparable
interface
for the entity whose objects are stored in an array. The second sorting is provided by creating a
separate class
that implements the Comparator
interface. Use of lambda expression is recommended.
Create two derived classes from abstract class:
- with the sequence of the second entity objects represented using an array;
- with the sequence of the second entity objects represented by a manually created singly linked list (see Example 3.5).
You should create classes with meaningful names that reflect the physical nature of the individual task.
Javadoc comments should be added to the source code.
In the main()
function, create the necessary objects that represent a sequence of objects in various
ways, and call methods that implement the main task. Output the results to the console window. Testing the program
should include the task of the previous laboratory work, as well as sorting by certain criteria.
1.2 Class Hierarchy
Implement classes "Human," "Citizen", "Student", "Employee". Each
class should implement toString()
method. Create an array of references to different
objects of the class hierarchy. Create a cycle and display data that represents objects of different
types.
Note: it is necessary to create classes with meaningful names.
1.3 Minimum of a Function
Implement a program that finds minimum of some function by setting value of step and traversal of a given interval. Implement five versions:
- using abstract class and derived classes
- through the definition of the interface, the creation of a class that uses this interface as a parameter type of the minimum finding function, the creation of separate classes that implement this interface
- using previously described interface and anonymous classes
- using lambda expressions
- using references to methods.
Test the program for two different functions.
1.4 Implementation of an Array of Points through a Two-Dimensional Array
Implement the AbstractArrayOfPoints
, abstract class functionality given in Example 3.2, in
two ways:
- using a two-dimensional array of real numbers; each row of the array must correspond to the point;
- use of a one-dimensional array of real numbers; each pair of numbers in the array should correspond to the point.
Test classes.
Note: you should not make changes to the AbstractArrayOfPoints
class code (except
the package name).
1.5 The Implementation of the Comparable Interface
Create a Circle
class that implements the Comparable
interface. A circle with a
larger radius is considered to be larger. Sort the array of Circle
objects.
1.6 The Implementation of the Comparator Interface
Create a Triangle
class. The triangle is defined by the lengths of the sides.
The area
of the triangle in this case can be calculated by Heron's formula:

where a, b and care the lengths of the sides of the triangle. Sort the array of
triangles by descending the area. To determine the sort criteria, use an object that implements the
Comparator
interface.
1.7 Calculation of the Definite Integral (Additional Task)
Create an Integrable
interface that contains a description of an abstract function that
takes an argument of type double
and returns the result of the same type.
This interface should contain the integral()
method with the default implementation (with
the default
modifier) of calculating the specified integral. The method
should receive start, end of the interval, and the accuracy of the calculations as arguments. The
default implementation of computing the integral should use the method of rectangles.
Create a class that redefines the integral()
method by implementing a trapezoid method for
calculating integral.
Calculate a definite integral by using both algorithms for different functions of the
java.lang.Math
class (see example 3.3). Compare results for different algorithms and different values of computing
accuracy.
2 Instructions
2.1 Inheritance
The inheritance mechanism assumes creation of derived classes from base classes. Derived class has direct access to all public and protected members of base class. Base class and derived classes build class hierarchy.
Unlike C++, only single inheritance of classes is allowed in Java. In other words, a class can inherit implementation from one base class only. Inheritance is always public. There are no private and protected base classes in Java. Suppose there is a base class:
class Shape {private double x, y;// coordinates of the center of the shape public double getX() {return x; }public void setX(double x) {this .x = x; }public double getY() {return y; }public void setY(double y) {this .y = y; }public double distance() {// distance from the origin return Math.sqrt(x * x + y * y); } }
The syntax of inheritance is as follows:
class DerivedClassextends BaseClass {// class body }
In our case, we can create a derived class:
class Circleextends Shape {private double radius;public double getRadius() {return radius; }public void setRadius(double radius) {this .radius = radius; } }
The derived class functions have access to the members described as public
and protected
. Class members declared to be protected can be used by
descendants, as well as within their package. Private class members are not available even for its
descendants.
All Java classes are derived from java.lang.Object
directly or indirectly. The
Object
class offers useful methods, such as toString()
for retrieving the data of any
object as a string, equals()
for checking the equivalence of objects, etc. You don't need to declare
base class java.lang.Object
explicitly.
The class inherits all members of the base class, except constructors. Before the constructor of a
derived class executes, the base class constructor (the default constructor, unless explicitly called
otherwise) is called. You should not explicitly initialize the fields of the base class from the derived class constructor,
even if these fields are available (public
or protected
).
If we want to call a base class constructor with parameters, in particular if the base class does not have parameterless
constructors, there is a special mechanism for an explicit base class constructor invocation through the use of the
super
keyword.
The super
keyword is used to access members of the base class from within a derived
class:
- call a method on the base class that has been overridden by another method
- specify which base-class constructor should be called when creating instances of the derived class.
If in the previous example a single constructor with parameters is added to the Shape
class,
class Shape {private double x, y;// coordinates of the center of the shape public Shape(double x,double y) {this .x = x;this .y = y; }//... }
the code of the Circle
class will not be able to compile. This constructor must be explicitly called from the derived
class constructors using super
. Alternatively, you can add a constructor whose parameters are
needed to initialize the base part of the object and are sent directly to the base class constructor:
class Circleextends Shape {private double radius;public Circle(double radius) {super (0, 0);// call the base class constructor this .radius = radius; }public Circle(double x,double y,double radius) {super (x, y);// call the base class constructor this .radius = radius; }public Circle() {this (10);// call another constructor of the current class }// ... }
Note:
you cannot use calls of super
()
and this
()
at
the same time in the constructor body.
A base class access using the super
keyword is permitted only in constructors or
non-static methods.
Classes can be declared with final
keyword. Final classes cannot be used as base
classes. For example, you can declare a class Circle
as final:
final class Circleextends Shape {// ... }class Ellipseextends Circle {// Syntax error. // Cannot inherit from Circle }
The method declared as final
cannot be overridden. For instance:
class Shape {// ... public final double distance() {return Math.sqrt(x * x + y * y); } }class Circleextends Shape {public double distance() {// Syntax error. // distance() cannot be overridden // ... }// ... }
Reference to derived class can be implicitly converted to base class reference. In other words, derived class objects can be always used instead of base class objects.
Shape shape =new Circle();
Reverse conversion should be done explicitly:
Shape shape =new Circle(); Circle circle = (Circle) shape;
If the conversion is not possible, an exception of type ClassCastException
is thrown:
Shape shape =new Shape(1, 2); Circle circle = (Circle) shape;// ClassCastException
Thanks to type casting rules, you can create arrays of references to objects of different types within the same
inheritance hierarchy. For example, a previously created class hierarchy of Shape
and Circle
classes
can be extended with Square
class:
class Shape {private double x, y;public Shape(double x,double y) {this .x = x;this .y = y; }// ... public final double distance() {return Math.sqrt(x * x + y * y); } }class Circleextends Shape {private double radius;public Circle(double x,double y,double radius) {super (x, y);this .radius = radius; }// ... }class Squareextends Shape {private double side;public Square(double x,double y,double side) {super (x, y);this .side = side; }// ... }
Now you can put references to different shapes into an array of references to Shape
, and then calculate
and output the distance of the shapes from the origin in a loop:
Shape shapes[] =new Shape[3]; shapes[0] =new Shape(3, 4); shapes[1] =new Circle(1, 1, 0.3); shapes[2] =new Square(4, 3, 2);for (int i = 0; i < shapes.length; i++) { System.out.println(shapes[i].distance()); }
Java supports instanceof
keyword, which allows you to check whether the object is an instance
of a certain type (or derived types). The expression
objectinstanceof class
returns the value of the boolean
type, which can be used to verify whether the method of this
class can be called:
if (shapeinstanceof Circle) { ((Circle)shape).setRadius(20); }
Records (record
type) do
not support explicit inheritance.
2.2 Sealed Classes
Starting from the JDK 17 version to Java syntax was extended with the opportunity to limit the list of subclasses. This was done in order to better control the correctness of creating specific realizations of subclasses. To limit potential derived classes in previous versions, it was necessary to make the base class with package (non-public) visibility. But this approach made it impossible not only to inherit, but also any use of the class outside the package. In addition, there is sometimes a need to permit inheritance for classes located in other packages.
The new opportunity to determine such restrictions involves the use of so-called sealed classes. After the sealed class name, a list of permitted derived classes is placed:
public sealed class SealedBasepermits FirstDerived, SecondDerived {protected int data; }
Listed permitted subclasses must be accessible by the compiler. Such classes are defined with modifiers final
or sealed
.
In the latter case, an additional branch of allowed classes is created:
final class FirstDerivedextends SealedBase { }sealed class SecondDerivedextends SealedBasepermits SomeSubclass { }final class SomeSubclassextends SecondDerived { { data = 0; } }
Attempt to create other subclasses leads to an error:
class AnotherSubclassextends SealedBase {// Compiler error }
There is another modifier for the permitted derived class: non-sealed
. You can create
any subclasses from such a class:
non-sealed class SecondDerivedextends SealedBase { }class PlainSubclassextends SecondDerived { { data = 0; } }
Permitted derived classes can be located in other packages.
2.3 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 { @Overridepublic String toString() {// The toString() method is defined in the Object class return "My overridden method!"; } }
There are other examples of annotations:
@SuppressWarnings("warning_identifier")
– compiler warnings should be silenced in the annotated element@Deprecated
– the use of the annotated element is no longer desirable
You can declare your own annotations.
2.4 Polymorphism
2.4.1 Overview
A runtime polymorphism is a property that provides definition of class object behavior not during compilation but at run time. Classes that provide identical interface but implemented for specific requirements are called polymorphic classes.
Usually polymorphism is implemented by so-called late binding mechanism. Connecting a method call to a method body is called binding. When binding is performed before the program is run, it's called early binding. Such binding is typical for procedural languages (such as C and Pascal). The late binding means that the binding occurs at run time based on the type of object. Late binding is also called dynamic binding. A late binding mechanism is used to implement the polymorphism.
In object-oriented programming languages, later binding is implemented through the mechanism of virtual functions. A virtual function (virtual method) is a function defined in the base class, and is overridden in the derived classes, so that the specific implementation of the function for the call will be determined at runtime. The choice of the implementation of a virtual function depends on the actual (and not declared in the description) type of the object. Because a reference to a base type can contain the address of an object of any derived type, the behavior of previously created classes can be changed later by overriding virtual methods. Overriding provides for creation of a new virtual method with the same name, a list of parameters and an access qualifier. In fact, polymorphic classes have classes that contain virtual functions.
In C++, the virtual
modifier is used to denote a virtual function. In Java, all non-static
non-final methods are virtual methods. The virtual
keyword is not needed. Constructors and
private methods also cannot be virtual.
Starting with Java 5, the @Override
annotation can precede overridden virtual methods. This
annotation allows the compiler to make additional syntax check: new function signature must match the
signature of overridden function of the base class. Use of @Override
is desirable but not
obligatory.
All Java classes are polymorphic because java.lang.Object
is polymorphic as well. In
particular, each derived class can define its own virtual toString()
method that will be
called automatically when string representation of object is needed:
public class SingleInteger {int i = 10; @Overridepublic String toString() {return "i = " + i; }public static void main(String[] args) { SingleInteger mc =new SingleInteger(); System.out.println(mc);// i = 10; } }
2.4.2 Abstract Classes and Methods
Sometimes, a class that you define represents an abstract concept and, as such, should not be
instantiated. Such concepts can be represented by abstract classes. Java uses
abstract
keyword to declare abstract classes:
abstract class SomeConcept {// ... }
An abstract class may contain abstract methods, that is, methods with no implementation. Abstract method
has no function body. Its declaration is similar to the declaration of the C++ member function, but the
declaration must be preceded by the abstract
keyword.
For example, you can declare an abstract class Shape
to provide fields and methods that will
be used by all subclasses, such as the current position and the moveTo()
method. The
Shape
class also declares abstract methods, such as draw()
, that need to be implemented by all
subclasses, but are implemented in different ways. No default implementation in the superclass makes
sense. The Shape
class would look something like this:
abstract class Shape {int x, y;// ... void moveTo(int newX,int newY) {// ... }abstract void draw(); }
Each non-abstract subclass of Shape
, such as Circle
and Rectangle
,
would have to provide an implementation for the draw()
method.
class Circleextends Shape {void draw() {// ... } }class Rectangleextends Shape {void draw() {// ... } }
Abstract methods are similar to pure virtual functions in C++ language.
An abstract class is not required to have an abstract method in it. However, any class that has an
abstract method in it or that does not provide an implementation for any abstract methods declared in
its superclasses must be declared as an abstract class (using abstract
keyword).
2.5 Concept of Interfaces. Arranging Objects
2.5.1 Creating and Implementing Interfaces
Java introduces a concept of interface. The interface can be considered as a purely abstract class, but unlike abstract classes, the interface never contains data, only methods. These methods are generally considered public:
interface Printable {void print(String printerName); String getPreview(); }
A class may inherit from one and only one other class but can implement several interfaces. When a class implements
an interface, it either must provide implementations for all the methods declared in that interface or this class
must be declared with abstract
keyword.
Interfaces can contain data fields. Those fields are considered as final and static (compile-time constants). They must be initialized at the place of creation:
interface MathConstants {double PI = 3.14159265359;double E = 2.71828182846; }
Interfaces cannot contain constructors because there is no data other than static constants.
To indicate that a class implements an interface, the interface identifier is included in the list of the implemented
interfaces. This list starts with implements
keyword. Methods declared in an interface are public
and abstract by default and if this class implements a method, which was declared in an interface, this method must
be declared as public
:
interface Printable {void print(String printerName); String getPreview(); }class Documentimplements Printable { @Overridepublic void print(String printerName) { } @Overridepublic String getPreview() { String preview = "";//... return preview; } }
Note: Using the @Override
annotation is recommended, but not required.
Records (record
type) can implement interfaces.
An interface can have several base interfaces. Multiple inheritance of interfaces is safe in terms of data duplication and name conflicts:
interface Printable {void print(String printerName); String getPreview(); }interface Editable {void edit(); }interface Serviceableextends Printable, Editable { String getPreview();// the declaration may be repeated }
Now the class that implements the AllFunctions
interface must define three functions:
class Documentimplements Serviceable { @Overridepublic void print(String printerName) { } @Override// One implementation is used for both base and derived interfaces: public String getPreview() { String preview = "";//... return preview; } @Overridepublic void edit() { } }
A class can implement several interfaces. This is a more common way than creating a derived interface:
class AnotherDocumentimplements Printable, Editable { @Overridepublic void print(String printerName) { } @Overridepublic String getPreview() { String preview = "";//... return preview; } @Overridepublic void edit() {//... } }
Very often programmers create the reference to the interface which is initialized by object of a class that implements this interface. This approach is good practice because it allows you to easily replace one implementation of the interface with another one:
public static void main(String[] args) { Serviceable serviceable =new Document(); }
Interfaces are not derived from the java.lang.Object
class. You cannot create a new interface object.
Even for an empty interface, you need to create a class that implements it:
interface Empty { }class EmptyImplementationimplements Empty { }// ... public static void main(String[] args) { Empty empty1 =new Empty();// Error Empty empty2 =new EmptyImplementation();// Correct creation of the object }
Like classes, interfaces can be defined with public
modifier (most often) or without
a modifier. Like classes, public interfaces must be defined in separate files, whose names match the names of the
interfaces.
2.5.2 Arranging Objects
The JDK provides a large number of standard interfaces. Consider using the Comparable
and Comparator
interfaces
to sort arrays.
The simplest way to implement ascending sort is call of java.util.Array.sort()
method with reference
to array as single argument (reference to an array). You can also sort any part of array. Such methods are implemented
for all primitive types. You can also sort arrays of objects into ascending order, according to the natural ordering
of its items. All items in the array must implement the Comparable
interface. The only method
of this interface is compareTo()
:
public int compareTo(Object o)
This method should return negative value (e.g. -1) if this object is less than o
, zero value if objects
are equal, and positive value otherwise.
Standard Double
, Integer
, Long
, etc., and String
classes implement
this interface. The following program sorts an array of Integer
:
public class SortIntegers {public static void main(String[] args) { Integer[] a = {7, 8, 3, 4, -10, 0}; java.util.Arrays.sort(a); System.out.println(java.util.Arrays.toString(a)); } }
Note. The Comparable
interface name is an example of the most correct interface name. It
is desirable that the interface names end in -able
(Comparable
, Runnable
,
etc.). But this rule is very often violated even for standard interfaces.
Java 5 Comparable
is a generic interface. Creating generic classes and interfaces will be considered
later. Thanks to the generalization, function declared in the interface can get parameters of other types but not
of the type Object
only. In our case, the compareTo()
function should get an argument
of the array item type.
The following example shows how to create your own class, which implements Comparable
interface. The
program sorts array of rectangles by area:
class Rectangleimplements 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); } @Overridepublic int compareTo(Rectangle rect) {return Double.compare(area(), rect.area()); } @Overridepublic String toString() {return "[" + width + ", " + height + ", area = " + area() + ", perimeter = " + perimeter() + "]"; } }public class SortRectangles {public static void main(String[] args) { Rectangle[] a = {new Rectangle(2, 7),new Rectangle(5, 3),new Rectangle(3, 4)}; java.util.Arrays.sort(a); System.out.println(java.util.Arrays.toString(a)); } }
In this example, the static compare()
method of Double
class returns necessary integer
values which are used by sort()
method.
Sometimes we cannot (or do not want to) implement compareTo()
for particular class, or we want to
sort objects by different criteria. An alternative way of sorting is creation of a class that implements Comparator
interface.
Objects of such classes are used as arguments of sort()
methods. Java 2 declaration of such methods
(without generics) is as follows (the description is given without generics):
public static void sort(Object[] a, Comparator c); public static void sort(Object[] a,int fromIndex,int toIndex, Comparator c);
The only method of Comparator
interface is compare()
. This method should return negative
value if first object is less than second, zero value if objects are equal, and positive value otherwise.
Note. Java 5 Comparator
is also a generic interface. By its implementation, after its name,
in angle brackets it is necessary to specify the type of objects that you compare. Now the compare()
function
should take two arguments of array item type. For example
By using a class that implements the Comparator
interface, you can additionally sort by perimeter
of rectangles:
class CompareByPerimeterimplements java.util.Comparator<Rectangle> { @Overridepublic int compare(Rectangle r1, Rectangle r2) {return Double.compare(r1.perimeter(), r2.perimeter()); } }public class SortRectangles {public static void main(String[] args) { Rectangle[] a = {new Rectangle(2, 7),new Rectangle(5, 3),new Rectangle(3, 4)}; java.util.Arrays.sort(a);// sort by area System.out.println(java.util.Arrays.toString(a)); java.util.Arrays.sort(a,new CompareByPerimeter());// sort by perimeter System.out.println(java.util.Arrays.toString(a)); } }
2.6 Nested Classes
2.6.1 Overview
It is possible to place a class definition within another class definition. This is called a nested class. It can be either static nested or inner. A nested class can be used inside either of outer class or somewhere else.
class Outer {class Inner {int i; } Inner inner =new Inner(); }class Another { Outer.Inner i; }
Nested classes can be declared as public
, private
, or protected
.
Local classes are created inside blocks. There is also a special kind of local classes, the so-called anonymous classes.
2.6.2 Static Nested Classes
The use of static nested classes is similar to the nested classes in C++ and C#. It is in fact a type described within the scope of another type:
class Outer {static class Inner { } }
For example, we can create your own wrapper classes for integers and real numbers and define these classes as nested:
public class Wrappers {public static class Integer {private int value;public Integer(int value) {this .value = value; }public int get() {return value; }public void set(int value) {this .value = value; } }public static class Double {private double value;public Double(double value) {this .value = value; }public double get() {return value; }public void set(double value) {this .value = value; } }// ... }
Objects of such classes can be created both in the outer class and outside it. In the Wrappers
class:
public class Wrappers {// ... protected void test() { Integer i =new Integer(3); System.out.println(i.get()); Double d =new Double(3.6); System.out.println(d.get()); } }
In another class:
public class WrappersTest {public static void main(String[] args) { Wrappers.Integer i =new Wrappers.Integer(10); System.out.println(i.get()); Wrappers.Double d =new Wrappers.Double(10.5); System.out.println(d.get()); } }
Nested classes can be used to create a type for some part of a complex object, or part of its description. For
example, within a Circle
class, we can create a separate nested class Center
to represent
the coordinates of the center:
public class Circle {public static class Center {private double x, y;// coordinates of the center of the circle public Center() { }public Center(double x,double y) {this .x = x;this .y = y; }public double getX() {return x; }public void setX(double x) {this .x = x; }public double getY() {return y; }public void setY(double y) {this .y = y; }double distance() {return Math.sqrt(x * x + y * y); } }private double radius;private Center center =new Center();// coordinates of the center of the circle //... }
An object of Center
class is automatically created when a Circle
class object is created,
because a field of the appropriate type is present in Circle
and initialized with the required
object. Otherwise, the class would simply be a static class nested within another.
We can set and read the center coordinates outside the class. We can also create a separate object and then replace the existing center with this object:
public class CircleTest {public static void main(String[] args) { Circle circle =new Circle(); circle.getCenter().setX(1); Circle.Center center =new Circle.Center(2, 3); circle.setCenter(center); } }
Static nested classes can contain their own static members, as well as own nested static and non-static classes. Nested classes have access to the static members of outer classes, including those that have been declared private. For example:
public class Circle {private static String entityName = "Circle";public static String getEntityName() {return entityName; }public static class Center {public static String getClassInformation() {return "Inner class: Center. Outer class: " + entityName; }// ... }//... }
If there is a name conflict, you can use a prefix – the class name:
public class Circle {private static String entityName = "Circle";public static String getEntityName() {return entityName; }public static class Center {private static String entityName = "Center";public static String getClassInformation() {return "Inner class: " + entityName + ". Outer class: " + Circle.entityName; }// ... }//... }
Static nested classes do not have access to non-static members of outer classes. However, such an object can be created or passed as a parameter. Then private non-static members are also available:
public class Circle {private double radius;public static class Center {private static String entityName = "Center";public String getObjectInformation(Circle circle) {return "Coordinates of the center of the circle: " + x + ", " + y + ". Radius of the circle: " + circle.radius; }// ... } // ... }
Classes can be created inside interfaces. These classes are static by default. Inside classes, you can also create interfaces that are static by default.
2.6.3 Inner Classes
Non-static nested classes are also inner classes. The main feature of the inner classes in Java is that the objects of these classes receive a reference to the object of the outer class. This fact implies two important conclusions:
- objects of inner classes have direct access to members of outer class objects
- you cannot create object of inner class without object of outer class.
Non-static inner classes cannot contain static members.
The class Center
from the previous example can be defined as non-static. The code for this class should
be changed, in particular, the static members of the inner class should be converted to non-static:
package ua.inf.iwanoff.java.third;public class Circle {private static String entityName = "Circle";public static String getEntityName() {return entityName; }public class Center {private String entityName = "Center";public String getClassInformation() {return "Inner class: " + entityName + ". Outer class: " + Circle.entityName; }public String getObjectInformation() {return "Coordinates of the center of the circle: " + x + ", " + y + ". Radius of the circle: " + radius; }// ... then no changes }private double radius;private Center center =new Center();// coordinates of the center of the circle //... }
To create inner class objects outside outer classes, Java offers a special syntax:
outer_class_object.new Inner_class_constructor();
In our case, we will need to make changes to the function main()
:
public class CircleTest {public static void main(String[] args) { Circle circle =new Circle(); circle.getCenter().setX(1); Circle.Center center =new Circle().new Center(2, 3); circle.setCenter(center); System.out.println(center.getClassInformation()); System.out.println(center.getObjectInformation()); } }
You should keep in mind that objects of inner classes are not created automatically. Creating an object can be provided in the constructor or in any method of the outer class, and also outside it (if this class is not declared private). You can also create an array of inner class objects. Each of these objects will have access to the outer object.
Inner classes can have their own base classes. Therefore, we obtain something close to multiple inheritance:
class FirstBase {int a = 1; }class SecondBase {int b = 2; }class Outerextends FirstBase {int c = 3;class Innerextends SecondBase {void show() { System.out.println(a); System.out.println(b); System.out.println(c); } } }public class Test {public static void main(String[] args) { Outer outer =new Outer(); Outer.Inner inner = outer.new Inner(); inner.show(); } }
The following example has a purely theoretical meaning, since the multiple inheritance of classes, regardless of the methods of its implementation, is dangerous in terms of a possible name conflicts.
2.6.4 Local and Anonymous Classes
Inner classes can be also local. You cannot access local classes from the outside of the block in which they are defined. A local class definition is typically placed into function body:
void f() {class Local {int j; } Local l =new Local(); l.j = 100; System.out.println(l.j); }
You can also place local classes inside blocks.
Local classes can have their own base classes or implement certain interfaces. In the following example, a local class is created to define how to sort an array of strings (in reverse alphabetical order):
static void sortReverse(String[] a) {class StringComparerimplements Comparator<String> { @Overridepublic int compare(String s1, String s2) {return s2.compareTo(s1); } } StringComparer comparer =new StringComparer(); Arrays.sort(a, comparer); }
An anonymous class can implement an interface or extend another class. It can either override existing base class methods or introduce new ones. To create an object of an anonymous class, you should call the constructor of the base class constructor, or specify the name of the interface with parentheses, and then you place the body of an anonymous class:
new Object() { // Adding a new method: void hello() { System.out.println("Hello!"); } }.hello(); System.out.println(new Object() { // Overriding an existing method: @Override public String toString() { return "This is an anonymous class."; } });
An anonymous class is never abstract. An anonymous class is always an inner class; it is never
static
.
An anonymous class is always implicitly final
.
An anonymous class can implement an interface. In the following example, new anonymous class is created for definition of sorting order of string-type members of an array:
static void sortReverse(String[] a) { Arrays.sort(a,new Comparator<String>() {public int compare(String s1, String s2) {return s2.compareTo(s1); } }); }
An anonymous class cannot have an explicitly declared constructor. However, a default constructor is always created. If the base class does not have a constructor without parameters, the necessary parameters of the constructor are placed in brackets when you create an object:
abstract class Base {int k; Base(int k) {this .k = k; }abstract void show(); }public class Test {static void showBase(Base b) { b.show(); }public static void main(String[] args) { showBase(new Base(10) {void show() { System.out.println(k); } }); } }
You can also use initialization blocks.
In order for anonymous classes to have access to local members of external blocks, these members should
be declared as final
.
2.7 Interface Methods with Default Implementation
Java 8 provides a new opportunity to provide default implementation of methods declared in the interface.
To do this, before the corresponding function you must place the default
keyword,
after which the function can be implemented inside the interface. In the following example some
interface declares method and offers the default implementation of this method:
package ua.inf.iwanoff.java.third;public interface Greetings {default void hello() { System.out.println("Hello everybody!"); } }
The class that implements this interface may be empty. You can leave the default implementation of the
hello()
method:
package ua.inf.iwanoff.java.third;public class MyGreetingsimplements 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 MyGreetingsimplements Greetings { @Overridepublic void hello() { System.out.println("Hello to me!"); } }
Now, creating an object of this class, we get a new greeting.
If overridden method needs to call the default interface method, you can use the
super
keyword:
Greetings.super .hello();
We can offer such an example. Suppose it is necessary to print the values of some function at a certain interval with a given step. You can create an interface with one abstract method (the calculation of some function) and one method with a default implementation:
package ua.inf.iwanoff.java.third;public interface FunctionToPrint {double f(double x);default void print(double x) { System.out.printf("x = %7f f(x) = %7f%n", x, f(x)); } }
In the PrintValues
class, we create a printTable()
method. This method uses the
previously created interface.
package ua.inf.iwanoff.java.third;public class PrintValues {static void printTable(double from,double to,double step, FunctionToPrint func) {for (double x = from; x <= to; x += step) { func.print(x); } System.out.println(); }// Create an object of an anonymous class in the main() function: public static void main(String[] args) { printTable(-2, 2, 0.5,new FunctionToPrint() { @Overridepublic 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() { @Overridepublic double f(double x) {return x * x * x; } @Overridepublic 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 OldImplimplements SomeInterface { @Overridepublic 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 NewImplimplements SomeInterface { @Overridepublic void f() {// implementation } @Overridepublic void g() {// implementation } }
Without default implementation, the code built on the previous version will not be compiled.
When inheriting an interface that contains a method with a default implementation, this method is also inherited with its implementation, but it can also be re-declared as abstract, or redefined with another implementation.
In Java 8, interfaces may also contain the implementation of static methods. These methods should logically relate to this interface (for example, they can get the reference to the interface as a parameter). Most often, these are auxiliary methods. Like all interface members, these static methods are public. You can specify public explicitly, but there is no need for it.
In the example above, the printTable()
function could be placed inside the interface:
package ua.inf.iwanoff.java.third;public interface FunctionToPrint {double f(double x);default void print(double x) { System.out.printf("x = %9f f(x) = %9f%n", x, f(x)); }static void printTable(double from,double to,double step, FunctionToPrint func) {for (double x = from; x <= to; x += step) { func.print(x); } System.out.println(); } }
The function call should be performed through the interface name.
2.8 Working with Functional Interfaces in Java 8
2.8.1 Lambda Expressions and Functional Interfaces
Java interfaces very often contain declaration of the one and only abstract function (without default implementation). Such interfaces are called functional interfaces. They are generally used to implement callback mechanism, event handling, etc. Despite their simplicity, their implementation, however, requires a separate class (ordinary, nested or anonymous). Even using an anonymous class, we get cumbersome poorly readable syntax. Lambda expressions that appeared in the Java version 8, can reduce the need for anonymous classes in the source code.
In programming languages, there is the concept of functional object – an object that can be used as a function. Lambda expression is a special syntax for describing a functional object within a method. In other words, the lambda expression is a way to describe a function inside another function.
The term "lambda expression" is associated with mathematical discipline, so-called lambda calculus. Lambda calculus is a formal system developed by the American mathematician Alonzo Church for formalization and analysis of the notion of computability. Lambda calculus has become a formal basis for functional programming languages (Lisp, Scheme, etc.).
Lambda expression in Java has the following syntax:
- a list of formal parameters separated by commas and enclosed in parentheses; if we have one parameter, parentheses can be omitted; if there are no parameters, you need an empty pair of parentheses;
- arrow (pointer,
->
); - a body consisting of one expression or a block; if a block is used, the
return
statement may be inside it.
For example, this is a function with one parameter:
k -> k * k
The same with the brackets and the block:
(k) -> {return k * k; }
Function with two parameters:
(a, b) -> a + b
Function without parameters:
() -> System.out.println("First")
For example, there is a functional interface:
public interface SomeInt {int f(int x); }
When calling some function, you need to send an argument of the functional interface type. Traditionally, you can create an anonymous class:
someFunc(new SomeInt() { @Overridepublic int f(int x) {return x * x; } });
You can create a variable of object that implements the interface, and use it instead of anonymous class:
SomeInt func = k -> k * k; someFunc(func);
You can also create an anonymous object when calling a function with a functional interface type parameter:
someFunc(x -> x * x);
Since each lambda expression is associated with a specific functional interface, the parameter types and the result are determined automatically in comparison with the respective functional interface.
An example task with a function value table can be implemented using lambda expressions. The previously created interface is functional interface because it has the only abstract method declared:
package ua.inf.iwanoff.java.third;public interface FunctionToPrint {double f(double x);default void print(double x) { System.out.printf("x = %9f f(x) = %9f%n", x, f(x)); }static void printTable(double from,double to,double step, FunctionToPrint func) {for (double x = from; x <= to; x += step) { func.print(x); } System.out.println(); } }
Using the functional interface with the lambda expression:
package ua.inf.iwanoff.java.third;public class PrintWithLambda {public static void main(String[] args) { FunctionToPrint.printTable(-2.0, 2.0, 0.5, x -> x * x * x); } }
2.8.2 Using References to Methods
Very often the entire lambda-expression body consists of calling the existing method only. In this case, instead of the lambda expression, you can use the reference to this method. There are several variants of using methods references.
Kind of reference to the method | Syntax | Example |
---|---|---|
Reference to the static method | Class::staticMethodName |
String::valueOf |
Reference to a non-static method for a given object | object::nonStaticMethodName |
s::toString |
Reference to a non-static method for parameter | Class::nonStaticMethodName |
Object::toString |
Reference to the constructor | Class::new |
String::new |
For example, there are functional interfaces:
interface IntOperation {int f(int a,int b); }interface StringOperation { String g(String s); }
You can create some class:
class DifferentMethods {public int add(int a,int b) {return a + b; }public static int mult(int a,int b) {return a * b; } }
Now methods are used:
public class TestMethodReferences {static void print(IntOperation op,int a,int b) { System.out.println(op.f(a, b)); }static void print(StringOperation op, String s) { System.out.println(op.g(s)); }public static void main(String[] args) { DifferentMethods dm =new DifferentMethods(); print(dm::add, 3, 4); print(DifferentMethods::mult, 3, 4); print(String::toUpperCase, "text"); } }
2.8.3 Standard Functional Interfaces
Instead of creating new functional interfaces, in most cases it is enough to use the standard interfaces that are
described in the java.util.function
package.
Interface | Description |
---|---|
BooleanSupplier |
Represents a supplier of boolean -valued results |
DoubleBinaryOperator |
Represents an operation upon two double -valued operands and
producing a double -valued result
|
DoubleConsumer |
Represents an operation that accepts a single
double -valued
argument and returns no result
|
DoublePredicate |
Represents a predicate (boolean -valued function) of one
double -valued argument
|
DoubleSupplier |
Represents a supplier of double -valued results |
DoubleToIntFunction |
Represents a function that accepts a double -valued argument and
produces an int -valued result |
DoubleToLongFunction |
Represents a function that accepts a double -valued argument and
produces a long -valued result
|
DoubleUnaryOperator |
Represents an operation on a single double -valued operand that
produces a double -valued result
|
IntBinaryOperator |
Represents an operation upon two int -valued operands and producing
an int -valued result
|
IntConsumer |
Represents an operation that accepts a single int -valued argument
and returns no result
|
IntPredicate |
Represents a predicate (boolean -valued function) of one
int -valued argument
|
IntSupplier |
Represents a supplier of int -valued results |
IntToDoubleFunction |
Represents a function that accepts an int -valued argument and
produces a double -valued result
|
IntToLongFunction |
Represents a function that accepts an int -valued argument and
produces a long -valued result
|
IntUnaryOperator |
Represents an operation on a single int -valued operand that
produces an int -valued result
|
LongBinaryOperator |
Represents an operation upon two long -valued operands and
producing a long -valued result
|
LongConsumer |
Represents an operation that accepts a single
long -valued argument and returns no
result
|
LongPredicate |
Represents a predicate (boolean -valued function) of one
long -valued argument
|
LongSupplier |
Represents a supplier of long -valued results |
LongToDoubleFunction |
Represents a function that accepts a
long -valued argument and produces a
double -valued result
|
LongToIntFunction |
Represents a function that accepts a
long -valued argument and produces
an int -valued result
|
LongUnaryOperator |
Represents an operation on a single long -valued operand that
produces a long -valued result long
|
For example, you can manually create a functional interface SomeFunction
and use that interface:
interface SomeFunction {double func(double x); }class ValuePrinter {static void printValue(SomeFunction function,double x) { System.out.printf("x = %f, f(x) = %f", x, function.func(x)); }static void test() { printValue(Math::sqrt, 4); } }
But a more productive approach is to use the standard interface DoubleUnaryOperator
without
creating a new interface:
class ValuePrinter {static void printValue(DoubleUnaryOperator function,double x) { System.out.printf("x = %f, f(x) = %f", x, function.applyAsDouble(x)); }static void test() { printValue(Math::sqrt, 4); } }
In addition to those listed, there are useful generic functional interfaces that will be discussed later.
In addition to these, a generic Comparator
interface, a Runnable
interface
(used in multithreading programming), and many more also are functional interfaces.
2.8.4 Composition of Lambda Expressions
You can implement a composition of lambda expressions (use lambda expressions as parameters). For this purpose
the java.util.function
package interfaces provide methods with a default implementation, providing
execution of a function passed as a parameter, before or after this method. In particular, the following methods
are defined in standard functional interface:
// The before function is executed, and then the calling function is executed: Function compose(Function before)// The after function is executed after the calling function: Function andThen(Function after)
Use of these methods and their differences will be considered in this example. There is a class with a static function calc()
,
which accepts a functional interface and an argument of double
type. You can make a composition of
lambda expressions:
package ua.inf.iwanoff.java.third;import java.util.function.DoubleUnaryOperator;public class ComposeDemo {public static double calc(DoubleUnaryOperator operator,double x) {return operator.applyAsDouble(x); }public static void main(String[] args) { DoubleUnaryOperator addTwo = x -> x + 2; DoubleUnaryOperator duplicate = x -> x * 2; System.out.println(calc(addTwo.compose(duplicate), 10.0));// 22.0 System.out.println(calc(addTwo.andThen(duplicate), 10.0));// 24.0 } }
The composition may be more complex:
System.out.println(calc(addTwo.andThen(duplicate).andThen(addTwo), 10.0)); // 26.0
2.9 Object Cloning, Equivalence Checking, and Getting Hash Codes
2.9.1 Object Cloning
Sometimes there is a need to create a copy of some object, for example, to perform some actions that do not violate the original data. Simple assignment only copies references. If you need to copy an object memberwise, you should use the mechanism of the so-called cloning.
The base class java.lang.Object
implements a function called clone()
, which by
default allows you to perform memberwise copy the object. This function is also defined for arrays,
strings, and other standard classes. For example, you can get a copy of an existing array and work with
this copy:
package ua.inf.iwanoff.java.third;import java.util.Arrays;public class ArrayClone {public static void main(String[] args) {int [] a1 = { 1, 2, 3, 4 };int [] a2 = a1.clone();// copy of items System.out.println(Arrays.toString(a2));// [1, 2, 3, 4] a1[0] = 10;// change the first array System.out.println(Arrays.toString(a1));// [10, 2, 3, 4] System.out.println(Arrays.toString(a2));// [1, 2, 3, 4] } }
In order to be able to clone objects of user classes, these classes must implement the
Cloneable
interface. This interface does not declare any methods. It just indicates that
objects of this class can be cloned. Otherwise, calling the clone()
function will throw
an exception of the CloneNotSupportedException
type.
Note. The mechanism of exception handling is largely similar to the corresponding C++ language
mechanism. In Java, you should list possible thrown exceptions using the throws
keyword in the method header. The mechanism for processing exceptions will be discussed later.
For example, if we need to clone objects of the Human
class, with two fields of
String
type (name
and surname
), we'll add the implementation of
the Cloneable
interface to the class description. Then we'll generate a constructor with
two parameters and override toString()
method. In the main()
function, we'll
perform an object cloning test:
package ua.inf.iwanoff.java.third;public class Humanimplements Cloneable {private String name;private String surname;public Human(String name, String surname) {super ();this .name = name;this .surname = surname; } @Overridepublic String toString() {return name + " " + surname; }public static void main(String[] args)throws CloneNotSupportedException { Human human1 =new Human("John", "Smith"); Human human2 = (Human) human1.clone(); System.out.println(human2);// John Smith human1.name = "Mary"; System.out.println(human1);// Mary Smith System.out.println(human2);// John Smith } }
As you can see from the example, the source object can be modified after cloning. In this case, the copy does not change.
The clone()
function can be overridden with changing its result type and making it open for
ease of use. Thanks to the availability of this function, cloning will be simplified (you will not need
to convert the type every time):
@Overridepublic Human clone()throws CloneNotSupportedException {return (Human)super .clone(); }// ... Human human2 = human1.clone();
The standard cloning implemented in the java.lang.Object
class allows you to create copies
of objects whose fields are value types and String
type (as well as wrapper classes). If
the fields of the object are references to arrays or other types, it is necessary to apply the so-called
"deep" cloning. For example, some class SomeCloneableClass
contains two fields of
type double and an array of integers. "Deep" cloning will create separate arrays for different
objects.
package ua.inf.iwanoff.java.third;import java.util.Arrays;public class SomeCloneableClassimplements Cloneable {private double x, y;private int [] a;public SomeCloneableClass(double x,double y,int [] a) {super ();this .x = x;this .y = y;this .a = a; } @Overrideprotected SomeCloneableClass clone()throws CloneNotSupportedException { SomeCloneableClass scc = (SomeCloneableClass)super .clone();// copy x and y scc.a = a.clone();// now two objects work with different arrays return scc; } @Overridepublic String toString() {return " x=" + x + " y=" + y + " a=" + Arrays.toString(a); }public static void main(String[] args)throws CloneNotSupportedException { SomeCloneableClass scc1 =new SomeCloneableClass(0.1, 0.2,new int [] { 1, 2, 3 }); SomeCloneableClass scc2 = scc1.clone(); scc2.a[2] = 4; System.out.println("scc1:" + scc1); System.out.println("scc2:" + scc2); } }
2.9.2 Equivalence Checking
In order to make sure that the cloned objects are the same, it would be nice to be able to automatically
compare all the fields. The reference model of Java objects does not allow you to compare the contents
of objects using the comparison operation (==
), since the references are compared. To
compare data, it is advisable to use the equals()
function defined in the
java.lang.Object
class. For classes whose fields are value types, the class method provides a memberwise comparison. If
the fields are object references, you must explicitly override the equals()
function. A
typical implementation of the equals()
method includes examining references (if they
match), then check whether the reference is null, then check type (for example, using
instanceof
operator). If the types match, fields are checked.
Here is a complete example with the Human
class:
ppackage ua.inf.iwanoff.java.third;public class Humanimplements Cloneable {private String name;private String surname;public Human(String name, String surname) {super ();this .name = name;this .surname = surname; } @Overridepublic boolean equals(Object obj) {if (this == obj) {return true ; }if (obj ==null || !(objinstanceof Human)) {return false ; } Human h = (Human) obj;return name.equals(h.name) && surname.equals(h.surname); } @Overridepublic Human clone()throws CloneNotSupportedException {return (Human)super .clone(); } @Overridepublic String toString() {return name + " " + surname; }public static void main(String[] args)throws CloneNotSupportedException { Human human1 =new Human("John", "Smith"); Human human2 = human1.clone(); System.out.println(human2); human1.name = "Mary"; System.out.println(human1); System.out.println(human2); human2.name =new String("Mary"); System.out.println(human2); System.out.println(human1.equals(human2));// true } }
If the equals()
method had not been defined, the last comparison would have given
false
.
To compare two arrays, you should call the static function equals()
of the
Arrays
class. This function compares array memberwise (calls the equals()
method):
Arrays.equals(array1, array2);
Records (record
) automatically provide a correct the correct implementation of the equals()
method.
2.9.3 Hash Codes
The implementation of the equals()
function for large objects may require significant
calculations,
since all data members should be checked. For simplified verification for possible equality of two
objects Java
uses so-called hashing.
Hashing is the process of obtaining from an object a unique code using some formal algorithm. In a broad sense, the result is a sequence of bits of fixed length, in the particular case, this is a simple integer. This conversion is performed by a so-called hash function. The hash function should meet the following requirement: the hash function should return the same hash code each time it is applied to identical or equal objects. Unfortunately, it is not possible to ensure producing different of hash codes for different objects.
Hashing is used in some container classes of the Java Collection Framework to make it impossible to put identical elements into the collection.
All Java objects inherit the standard implementation of a hashCode()
function defined in the
Object
class.
This function returns a hash code obtained by converting an object's internal address to a number, which
ensures
the creation of a unique code for each individual object.
Specific standard classes implement their hash functions. For example, for a string, the value of the hash function is calculated by the formula:
s[0]*31n-1 + s[1]*31n-2 + ... + s[n-1]
Here s[0]
, s[1]
, etc. are codes of the corresponding characters.
The functions hashCode()
for Integer
, Double
, etc. are also
stable.
For user defined types, the hashCode()
function should be redefined. Usually, the hash code of an object
is generated from the hash codes of the fields. The simplest way is to use the static function hash()
of
the java.util.Objects
class (starting with Java 7). Suppose a class Person
is created.
The hash code can be generated using the hash()
function:
class Person { String name;int year;public Person(String name,int year) {this .name = name;this .year = year; } @Overridepublic int hashCode() {return Objects.hash(name, year); } }
You can also propose your own algorithms for obtaining hash codes.
Records (record
) automatically provide a correct hash function.
3 Sample Programs
3.1 Hierarchy of Real World Objects
Our goal is to create the following class hierarchy:
- Region
- Populated Region
- Country
- City
- Island
Several classes of this hierarchy can be used as base classes for other classes (for instance, "Uninhabited island", "National park", "Borough", etc.). The hierarchy of classes can be expanded with the classes "City" and "Island". Each class should provide its original constructor for fields' initialization. Array of references to base object can be filled with references to objects of different derived types. For each object, a string of data about it will be displayed on the screen.
To obtain string representation of some object, we'll override toString()
function.
The class hierarchy can be as follows:
package ua.inf.iwanoff.java.third;import java.util.*;// Class hierarchy class Region {private String name;private double area;public Region(String name,double area) {this .name = name;this .area = area; }public String getName() {return name; }public double getArea() {return area; }public String toString() {return "Region" + name + ".\tArea " + area + " sq.km."; } }class PopulatedRegionextends 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 Countryextends 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 Cityextends 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 Islandextends 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: @Overridepublic 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 ArrayOfPointObjectsextends AbstractArrayOfPoints {private Point[] p = { }; @Overridepublic void setPoint(int i,double x,double y) {if (i < count()) { p[i].setPoint(x, y); } } @Overridepublic double getX(int i) {return p[i].getX(); } @Overridepublic double getY(int i) {return p[i].getY(); } @Overridepublic int count() {return p.length; } @Overridepublic 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 } @Overridepublic 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 ArrayWithTwoArraysextends AbstractArrayOfPoints {private double [] ax = { };private double [] ay = { }; @Overridepublic void setPoint(int i,double x,double y) {if (i < count()) { ax[i] = x; ay[i] = y; } } @Overridepublic double getX(int i) {return ax[i]; } @Overridepublic double getY(int i) {return ay[i]; } @Overridepublic int count() {return ax.length;// or ay.length, they are the same } @Overridepublic 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; } @Overridepublic 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:
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:
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 FirstImplementationimplements FunctionWithDerivatives { @Overridepublic 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
we can define the first and second derivatives in the following way:
+
12Now the class that implements our interface can be as follows:
package ua.inf.iwanoff.java.third;public class SecondImplementationimplements FunctionWithDerivatives { @Overridepublic double f(double x) {return x * x * x - 6 * x * x + 12 * x - 9; } @Overridepublic double f1(double x) {return 3 * x * x - 12 * x + 12; } @Overridepublic 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:
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 SpecificEquationextends AbstractEquation { @Overridepublic 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 MyEquationimplements LeftSide { @Overridepublic double f(double x) {return x * x - 2; } }public class InterfaceTest {public static void main(String[] args) { System.out.println(Solver.solve(0, 2, 0.000001,new MyEquation())); } }
The program can be modified to take into account Java 8 capabilities. The root finding method can be implemented inside the interface:
package ua.inf.iwanoff.java.third;public interface FunctionToSolve {double f(double x);static double solve(double a,double b,double eps, FunctionToSolve func) {double x = (a + b) / 2;while (Math.abs(b - a) > eps) {if (func.f(a) * func.f(x) > 0) { a = x; }else { b = x; } x = (a + b) / 2; }return x; } }
Now you should call FunctionToSolve.solve()
instead of Solver.solve()
.
3.4.4 Using an Anonymous Class
If the function is only necessary to solve the equation, it can be defined in an anonymous class:
package ua.inf.iwanoff.java.third;public class SolveUsingAnonymousClass {public static void main(String[] args) { System.out.println(FunctionToSolve.solve(0, 2, 0.000001,new FunctionToSolve() { @Overridepublic double f(double x) {return x * x - 2; } })); } }
3.4.5 Using Lambda Expressions
The left part of the equation can be defined by the lambda expression (instead of an anonymous class):
package ua.inf.iwanoff.java.third;public class SolveUsingLambda {public static void main(String[] args) { System.out.println(FunctionToSolve.solve(0, 2, 0.000001, x -> x * x - 2)); } }
3.4.6 Using References to Methods
The previous task can be solved using references to methods. We can implement the function as a separate static method:
package ua.inf.iwanoff.java.third;public class SolveUsingReference {public static double f(double x) {return x * x - 2; }public static void main(String[] args) { System.out.println(FunctionToSolve.solve(0, 2, 0.000001, SolveUsingReference::f)); } }
3.5 Hierarchy of the "Country" and "Census" Classes
Assume that we need to extend a census data processing program so that we can potentially use different data structures to represent a sequence of censuses. The class representing a country should not contain any data about the sequence of censuses. This class should be abstract. The country data are the name, the area, and a sequence of references to an object of type "Census". Fields are needed to define the following functions:
- functions for accessing data;
- abstract functions for accessing the census sequence;
- overriding the
equals()
method for checking the equivalence of objects; - overriding the
hashCode()
method for obtaining hash codes of objects.
Since there are different ways for storing data about the sequence of censuses, it is advisable to create a hierarchy of classes. To demonstrate the possibilities of representing the sequence of censuses in different data structures: an array and a singly linked list.
In turn, the class representing the census should include data about the census year, population, and comments.
It is also necessary to override the methods for checking equivalence and obtaining a hash code. To ensure sorting
by increasing population, a Comparable
interface should be implemented and the
compareTo()
function should provide a "natural" comparison by population.
The program must implement the following functions:
- calculating population density according to a specific census;
- determining the census with the largest population;
- checking the occurrence of a certain word in a comment;
- sorting censuses by population;
- sorting censuses in alphabetic order of comments.
It is necessary to additionally implement the functions of obtaining the representation of the census as a string, checking the presence of words and the sequence of letters in the comments, and testing. As in the previous implementation, these functions will be presented in separate classes.
We create a new class Census
in the package ua.inf.iwanoff.java.third
.
Its code can be copied from the package ua.inf.iwanoff.java.second
, but, unfortunately, we cannot use
the previously created class directly, since we need to add methods
equals()
, hashCode()
and compareTo()
. The class code will be as follows:
package ua.inf.iwanoff.java.third;import java.util.Objects;/** * The class is responsible for presenting the census. * The census is represented by year, population and comment */ public class Censusimplements Comparable<Census> {private int year;private int population;private String comments;/** * Constructor initializes the object with default values */ public Census() { }/** * Constructor initializes the object with given values * * @param year year of census * @param population population in the specified year * @param comments the text of the comment */ public Census(int year,int population, String comments) {this .year = year;this .population = population;this .comments = comments; }/** * Returns the census year * @return year of census in the form of an integer value */ public int getYear() {return year; }/** * Sets the value of the census year * @param year year of census in the form of an integer value */ public void setYear(int year) {this .year = year; }/** * Returns the population * @return the population in the form of an integer value */ public int getPopulation() {return population; }/** * Sets the population * @param population the population in the form of an integer value */ public void setPopulation(int population) {this .population = population; }/** * Returns the comment string * @return a census comment in the form of a string */ public String getComments() {return comments; }/** * Sets the content of a comment string * @param comments a census comment in the form of a string */ public void setComments(String comments) {this .comments = comments; }/** * Indicates whether some other census object is "equal to" this one * @param obj the reference to the census object with which to compare * @return {@code true}, if this object is the same as the obj argument * {@code false} otherwise */ @Overridepublic boolean equals(Object obj) {if (this == obj) {return true ; }if (!(objinstanceof Census c)) {return false ; }return c.year == year && c.population == population && c.comments.equals(comments); }/** * Returns a hash code value for the census object * @return a hash code value for this object */ @Overridepublic int hashCode() {return Objects.hash(year, population, comments); }/** * Compares this object with the specified object for order. Returns a * negative integer, zero, or a positive integer as this object is less * than, equal to, or greater than the specified object * @param c the census object to be compared * @return the result of comparison */ @Overridepublic int compareTo(Census c) {return Integer.compare(population, c.population); } }
As can be seen from the given code, the Census
class implements the Comparable<Census>
interface.
The implementation of this interface requires the addition of a compareTo()
method, which defines a "natural" comparison
by population.
We can copy the CensusUtilities
class code the previous laboratory training's example:
package ua.inf.iwanoff.java.third;import java.util.Arrays;/** * Provides static methods for searching data in a comment */ public class CensusUtilities {/** * Checks whether the word can be found in the comment text * @param census reference to a census * @param word a word that should be found in a comment * @return {@code true}, if the word is contained in the comment text * {@code false} otherwise */ public static boolean containsWord(Census census, String word) { String[] words = census.getComments().split("\\s"); Arrays.sort(words);return Arrays.binarySearch(words, word) >= 0; }/** * Checks whether the substring can be found in the comment text * @param census reference to a census * @param substring a substring that should be found in a comment * @return {@code true}, if the substring is contained in the comment text * {@code false} otherwise */ public static boolean containsSubstring(Census census, String substring) {return census.getComments().toUpperCase().contains(substring.toUpperCase()); }/** * Static method of adding a reference to census * to an array of censuses obtained as parameter * @param arr the array to which the census is added * @param item reference that is added * @return an updated array of censuses */ public static Census[] addToArray(Census[] arr, Census item) { Census[] newArr;if (arr !=null ) { newArr =new Census[arr.length + 1]; System.arraycopy(arr, 0, newArr, 0, arr.length); }else { newArr =new Census[1]; } newArr[newArr.length - 1] = item;return newArr; } }
We cannot link to the code of the previous work directly, because the functions actually receive a reference
to the new Census
class.
The class AbstractCountry
also contains methods equals()
and hashCode()
. The AbstractCountry
class
code will be as follows:
package ua.inf.iwanoff.java.third;import java.util.Arrays;import java.util.Objects;/** * Abstract class for presenting the country in which the censuses are carried out. * Country is described by the name, area and sequence of censuses. * Access to the sequence of censuses is represented by abstract methods */ public abstract class AbstractCountry {private String name;private double area;/** * Returns country name * @return country name */ public String getName() {return name; }/** * Sets country name * @param name country name */ public void setName(String name) {this .name = name; }/** * Returns the ara of the country * @return the ara of the country in the form of a floating point value */ public double getArea() {return area; }/** * Sets the ara of the country * @param area the ara of the country in the form of a floating point value */ public void setArea(double area) {this .area = area; }/** * Returns reference to the census by index in a sequence * * <p> A subclass must provide an implementation of this method * * @param i census index * @return reference to the census with given index */ public abstract Census getCensus(int i);/** * Sets a reference to a new census within the sequence * according to the specified index. * * <p> A subclass must provide an implementation of this method * * @param i census index * @param census a reference to a new census */ public abstract void setCensus(int i, Census census);/** * Adds a reference to a new census to the end of the sequence * * <p> A subclass must provide an implementation of this method * * @param census a reference to a new census * @return {@code true} if the reference has been added * {@code false} otherwise */ public abstract boolean addCensus(Census census);/** * Creates a new census and adds a reference to it at the end of the sequence * * @param year year of census * @param population population in the specified year * @param comments the text of the comment * @return {@code true} if the reference has been added * {@code false} otherwise */ public boolean addCensus(int year,int population, String comments) { Census census =new Census(year, population, comments);return addCensus(census); }/** * Returns the number of censuses in the sequence * * <p> A subclass must provide an implementation of this method * * @return count of censuses */ public abstract int censusesCount();/** * Removes all the censuses from the sequence * * <p> A subclass must provide an implementation of this method */ public abstract void clearCensuses();/** * Puts data from an array of censuses into a sequence * * <p> A subclass must provide an implementation of this method * * @param censuses array of references to censuses */ public abstract void setCensuses(Census[] censuses);/** * Returns an array of censuses obtained from the sequence * * <p> A subclass must provide an implementation of this method * * @return array of references to censuses */ public abstract Census[] getCensuses();/** * Checks whether this country is equivalent to another * @param obj country, equivalence with which we check * @return {@code true}, if two countries are the same * * {@code false} otherwise */ @Overridepublic boolean equals(Object obj) {if (this == obj) {return true ; }if (!(objinstanceof 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 */ @Overridepublic int hashCode() {return Objects.hash(name, area, Arrays.hashCode(getCensuses())); } }
In the derived class CountryWithArray
, we use an array to represent the sequence
of censuses:
package ua.inf.iwanoff.java.third;/** * Class for presenting the country in which the censuses are carried out. * Census data are represented with an array */ public class CountryWithArrayextends 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 */ @Overridepublic 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 */ @Overridepublic 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 */ @Overridepublic boolean addCensus(Census census) {if (getCensuses() !=null ) {for (Census c : getCensuses()) {if (c.equals(census)) {return false ; } } } setCensuses(CensusUtilities.addToArray(getCensuses(), census));return true ; }/** * Returns the number of censuses in the sequence * @return count of censuses */ @Overridepublic int censusesCount() {return censuses.length; }/** * Removes all of the censuses from the sequence */ @Overridepublic void clearCensuses() { censuses =null ; }/** * Returns an array of censuses obtained from the inner array * @return array of references to censuses */ */ @Overridepublic Census[] getCensuses() {return censuses; }/** * Returns an array of censuses obtained from the sequence * @return array of references to censuses */ @Overridepublic void setCensuses(Census[] censuses) {this .censuses = censuses; } }
In another derived class, we implement the simplest version of a singly linked list for storing a sequence of censuses:
package ua.inf.iwanoff.java.third;/** * Class for presenting the country in which the censuses are carried out. * Census data is represented by a singly linked list */ public class CountryWithLinkedListextends AbstractCountry {/** * A helper class that represents a node of a linked list */ private class Node { Census census; Node next; }private int size = 0;// count of censuses private Node head =null ;// reference to the beginning of the list /** * Returns reference to the census by index in a sequence * @param i census index * @return reference to the census with given index */ @Overridepublic Census getCensus(int i) {if (i < 0 || i >= size || head ==null ) {return null ; } Node node = head;for (int j = 0; j < i; j++) { node = node.next; }return node.census; }/** * Sets a reference to a new census within the sequence * according to the specified index. * @param i census index * @param census a reference to a new census */ @Overridepublic void setCensus(int i, Census census) {if (i < 0 || i >= size || head ==null ) {return ; } Node node = head;for (int j = 0; j < i; j++) { node = node.next; } node.census = census; }/** * Adds a reference to a new census to the end of the sequence * @param census a reference to a new census * @return {@code true} if the reference has been added * {@code false} otherwise */ @Overridepublic boolean addCensus(Census census) { Node newNode =new Node(); newNode.census = census; newNode.next =null ;if (head ==null ) { head = newNode; size = 1;return true ; } Node node = head; Node previous =null ;for (int i = 0; i < size; i++) {if (node.census.equals(census)) {return false ; } previous = node; node = node.next; } node = newNode; previous.next = node; size++;return true ; }/** * Returns the number of censuses in the sequence * @return count of censuses */ @Overridepublic int censusesCount() {return size; }/** * Removes all the censuses from the sequence */ @Overridepublic void clearCensuses() { head =null ; size = 0; }/** * Puts data from an array of censuses into a sequence * @param censuses array of references to censuses */ @Overridepublic void setCensuses(Census[] censuses) { clearCensuses();for (Census census : censuses) { addCensus(census); } }/** * Returns an array of censuses obtained from the sequence * @return array of references to censuses */ @Overridepublic Census[] getCensuses() { Census[] censuses =new Census[size]; Node node = head;for (int i = 0; i < size; i++) { censuses[i] = node.census; node = node.next; }return censuses; } }
We add the sorting functions to the code of the previously created class, which should also be copied to the new package:
package ua.inf.iwanoff.java.third;import java.util.Arrays;import java.util.Comparator;/** * Provides static methods for searching censuses */ public class CountryUtilities {/** * Returns the population density for the specified year * @param country reference to a country * @param year specified year (e.g. 1959, 1979, 1989, etc.) * @return population density for the specified year */ public static double density(AbstractCountry country,int year) {for (int i = 0; i < country.censusesCount(); i++) {if (year == country.getCensus(i).getYear()) {return country.getCensus(i).getPopulation() / country.getArea(); } }return 0; }/** * Finds and returns a year with the maximum population * @param country reference to a country * @return year with the maximum population */ public static int maxYear(AbstractCountry country) { Census census = country.getCensus(0);for (int i = 1; i < country.censusesCount(); i++) {if (census.getPopulation() < country.getCensus(i).getPopulation()) { census = country.getCensus(i); } }return census.getYear(); }/** * Creates and returns an array of censuses with the specified word in the comments * @param country reference to a country * @param word a word that is found * @return array of censuses with the specified word in the comments */ public static Census[] findWord(AbstractCountry country, String word) { Census[] result =null ;for (Census census : country.getCensuses()) {if (CensusUtilities.containsWord(census, word)) { result = CensusUtilities.addToArray(result, census); } }return result; }/** * Sorts the sequence of censuses by population * * @param country reference to a country */ public static void sortByPopulation(AbstractCountry country) { Census[] censuses = country.getCensuses(); Arrays.sort(censuses); country.setCensuses(censuses); }/** * Sorts the sequence of censuses in the alphabetic order of comments * * @param country reference to a country */ public static void sortByComments(AbstractCountry country) { Census[] censuses = country.getCensuses(); Arrays.sort(censuses, Comparator.comparing(Census::getComments)); country.setCensuses(censuses); } }
A separate class contains the means for obtaining data representation in the form of strings:
package ua.inf.iwanoff.java.third;/** * A class that allows getting representation * of various application objects in the form of strings */ public class StringRepresentations {/** * Provides a census data in the form of a string * * @param census reference to a census * @return string representation of a census data */ public static String toString(Census census) {return "The census in " + getYear() + ". Population: " + getPopulation() + ". Comments: " + getComments(); }/** * Returns a string representation of the country * * @param country reference to a country * @return a string representation of the country */ public static String toString(AbstractCountry country) { StringBuilder result =new StringBuilder(country.getName() + ". Area: " + country.getArea() + " sq.km.");for (int i = 0; i < country.censusesCount(); i++) { result.append("\n").append(toString(country.getCensus(i))); }return result + ""; } }
Demonstration of class capabilities is implemented in the class CountryDemo
:
package ua.inf.iwanoff.java.third;import static ua.inf.iwanoff.java.third.CountryUtilities.*;/** * Country testing program */ public class CountryDemo {/** * Auxiliary function for filling in the data of the "Country" object * @param country country reference * @return a reference to the new "Country" object */ public static AbstractCountry setCountryData(AbstractCountry country) { country.setName("Ukraine"); country.setArea(603628);// Adding censuses: System.out.println(country.addCensus(1959, 41869000, "First census after World War II")); System.out.println(country.addCensus(1970, 47126500, "Population increases")); System.out.println(country.addCensus(1979, 49754600, "No comments")); System.out.println(country.addCensus(1989, 51706700, "The last soviet census")); System.out.println(country.addCensus(2001, 48475100, "The first census in the independent Ukraine"));// Attempt to add a census twice: System.out.println(country.addCensus(1959, 41869000, "First census after World War II"));return country; }/** * Displays census data that contains a certain word in comments * @param country reference to a country * @param word a word that is found */ public static void printWord(AbstractCountry country, String word) { Census[] result = findWord(country, word);if (result ==null ) { System.out.println("The word \"" + word + "\" is not present in the comments."); }else { System.out.println("The word \"" + word + "\" is present in the comments:");for (Census census : result) { System.out.println(StringRepresentations.toString(census)); } } }/** * Performs testing search methods * @param country reference to a country */ public static void testSearch(AbstractCountry country) { System.out.println("Population density in 1979: " + density(country, 1979)); System.out.println("The year with the maximum population: " + maxYear(country) + "\n"); printWord(country, "census"); printWord(country, "second"); }/** * Performs testing search methods * @param country reference to a country */ public static void testSorting(AbstractCountry country) { sortByPopulation(country); System.out.println("\nSorting by population:"); System.out.println(StringRepresentations.toString(country)); sortByComments(country); System.out.println("\nSorting comments alphabetically:"); System.out.println(StringRepresentations.toString(country)); }/** * Demonstration of work with a country * @param args command line arguments (not used) */ public static void main(String[] args) { AbstractCountry country = setCountryData(new CountryWithArray()); testSearch(country); testSorting(country); System.out.println("----------------------------------------"); country = setCountryData(new CountryWithLinkedList()); testSearch(country); testSorting(country); } }
Before main program output, we'll get true
(five times) and
false
(one time) as a result of addCensus()
function.
4 Exercises
- 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 themain()
function). Display items. - 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 themain()
function). Display items. - 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 themain()
function). Display items. - 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
- What is the meaning of inheritance?
- What are advantages of common base class?
- What members of the base class are not inherited?
- How can you initialize base class?
- How can you call base class method of the same name?
- How can you use
super
keyword? - How can you override method with the
final
modifier? - Is multiple inheritance of classes allowed?
- How can you cast base class reference to derived class reference?
- What makes sense to use annotations?
- What are advantages of polymorphism?
- What is the difference between virtual and non-virtual functions?
- How can you define virtual function in Java?
- Some class is defined with
final
modifier. Can you create virtual functions within this class? - Why functions with
private
modifier are non-virtual? - Can you create abstract class without abstract methods?
- Can abstract classes contain non-abstract methods?
- What are differences between abstract classes and interfaces?
- Can you define fields within interface?
- Can you declare several base interfaces for a new derived interface?
- What requirements must meet class that implements some interface?
- Can you implement several interfaces within a single class?
- What requirements should an object satisfy, so that an array of such objects can be sorted without defining a sorting flag?
- How to define a special rule for sorting array items?
- How to access local class from outside of block?
- Is it possible to define several public classes in a single source file?
- Is it possible to create an object of non-static inner class without outer class object?
- Can non-static inner classes contain static members?
- What is the difference between static nested classes and non-static inner classes?
- Can static nested classes contain non-static members?
- Is it possible to create classes within interfaces?
- Is an anonymous class static?
- Why is it not possible to create explicit constructor of an anonymous class?
- What is lambda expression?
- What is a functional interface?
- What are the benefits of lambda expressions?
- What are the references to methods used for?
- What is the process of cloning objects?
- What is the use of the override of the
equals()
function? - What is the use of the override of the
hashCode()
function?