Laboratory Training 3
Use of Inheritance and Polymorphism in Java
1 Training Tasks
1.1 Individual Assignment
Create a hierarchy of classes that represent entities according to individual assignment. Particular function is given in the individual task according to your own index in the group students list (index of variant).
Each class should be separately tested. The main()
function of the first class should
contain creation of necessary object and invocation of methods implementing an individual assignment.
Results should be shown on console window.
#
|
First Entity
|
Second Entity
|
Main Assignment:
Find and show the following data: |
||
---|---|---|---|---|---|
Entity
|
Obligatory Fields
|
Entity
|
Obligatory Fields
|
||
1, 17 | Weather | Season, comments | Day | Date, temperature, comments | Average temperature, day with the maximum temperature, day with the longest comment |
2, 18 | Course of studies | Title, presence of an exam | Practical training | Date, topic, count of students | Average count of students, lesson with maximum students present, list of topics with particular word in a title |
3, 19 | Tram stop | Name, list of tram lines | Hour | Count of passengers, comments | Total count of passengers, stop with the minimum count of passengers, the longest comment |
4, 20 | Course of studies | Title, surname of lecturer | Lecture | Date, topic, count of students | Lecture with maximum students present, list of topics with particular word in a title, the last letter in the lecturer's surname |
5, 21 | Weather | Year, comments | Measurement of temperature | Date, temperature, comments | Measurements with the minimum temperature, with the maximum count of words in measurement comments, the last letter in a weather comment |
6, 22 | Conference | Title, place | Session | Date, topic, count of members | Average count of members, session with the maximum count of members, length of title |
7, 23 | Exhibition | Title, surname of painter | Day | Count of visitors, comments | Total count of visitors, day with the minimum count of visitors, list of comments
with the particular word |
8, 24 | Subway station | Name, year of opening | Hour | Count of passengers, comments | Total count of passengers, hours with the minimum count of passengers, hours the maximum count of words in comments |
9, 25 | Doctor | Surname, specialization | Reception | Day, shift, count of visitors | Total count of visitors, reception with the minimum count of visitors, surname length |
10, 26 | Music band | Name of band, surname of leader | Professional tour | City, year, count of concerts | Professional tour with maximum count of concerts, list of tours to particular city,
the last letter in the surname of leader |
11, 27 | Workshop | Name, address | Shift | Count of repaired computers | Total count of computers, shift with the maximum count of repaired computers, length of street name |
12, 28 | Doctor | Surname, length of service | Reception | Day, count of visitors, comments | Average count of visitors, reception with the minimum count of visitors, reception with the largest comments |
13, 29 | Tram line | Number, average traffic interval | Tram stop | Name, count of passengers | Total count of passengers, stops with the minimum count of passengers, stops with the longest name |
14, 30 | Twenty-four-hour kiosk | Denomination, address | Hour | Count of purchasers, comments | Total count of buyers, hour the minimum count of buyers, comments with particular words |
15, 31 | Musician | Surname, genre | Concert | Date, gate | Common gate, concert with the minimum gate, count of words in genre name |
16, 32 | Phone number | Number, operator | Calls | Date, duration of calls, funds used for calls | Average salary per day for the period; the number of days when the cost of a minute
of conversation exceeded the set value; Days when the number of minutes of conversation was even |
The class that represents the second entity of an individual task should contain data and access
methods, overridden toString()
, equals()
, and hashCode()
functions, as
well as the implementation of the functions defined in the previous assignments (if necessary). This
class also
should implement the Comparable
interface
for the natural comparison of objects when sorting by one of the criteria.
The base abstract class that represents the first entity of individual tasks (conventionally AbstractFirstEntity
)
should contain:
- access functions (getters and setters);
- abstract methods for access to a sequence of elements of the second abstract class;
- overridden
toString()
method for output object data; - overridden
equals()
method for checking equality of objects; - overridden
hashCode()
method for getting hash codes; - implementation of search methods according to specific criteria;
- implementation of the function of sorting items of the sequence on the first criterion of an individual task by the Bubble method;
- implementation of the function of sorting items of the sequence on the second criterion of the individual task by the Insertion method;
- implementation of testing the classes' functionality.
Sorting criteria are determined by the student's number in the group list. Search functions should return
arrays
of objects (or null
,
if no search results). Individual tasks are listed in the following table:
#
| First Criterion of Sorting | Second Criterion of Sorting |
---|---|---|
1, 17
|
By temperature (in decreasing order) | By comments in alphabetical order |
2, 18
|
By count of students (in increasing order) | By length of topic (in increasing order) |
3, 19
|
By count of passengers (in increasing order) | By comments in alphabetical order |
4, 20
|
By count of words in topics (in increasing order) | By topics in alphabetical order |
5, 21
|
By temperature (in increasing order) | By length of comments (in decreasing order) |
6, 22
|
By count of members (in increasing order) | By title in alphabetical order |
7, 23
|
By count of visitors (in increasing order) | By comments in alphabetical order |
8, 24
|
By count of passengers (in decreasing order) | By length of comments (in decreasing order) |
9, 25
|
By date (in decreasing order) | By count of visitors (in increasing order) |
10, 26
|
By count of concerts (in increasing order) | By name of city in alphabetical order |
11, 27
|
By shift index (in increasing order) | By count of computers (in increasing order) |
12, 28
|
By count of visitors (in increasing order) | By comments in alphabetical order |
13, 29
|
By count of passengers (in decreasing order) | By name in alphabetical order |
14, 30
|
By count of purchasers (in decreasing order) | By comments in alphabetical order |
15, 31
|
By date (in decreasing order) | By increasing the number of viewers |
16, 32
|
By decreasing the number of minutes of conversation | By increasing the amount of funds used to conversation |
Derived class from the created abstract classes (for convenience FirstEntityWithArray
)
should contain fields of appropriate types, in particular, the sequence of elements of the second entity
should
be represented as an array.
Create another class (conventionally FirstEntityWithSorting
) derived from the previously
created FirstEntityWithArray
class.
This class should override sorting methods by use the standard Arrays.sort()
function
instead of
bubble sorting and insertion sorting. The first sorting should be provided by the implementation of the
Comparable
interface
for the entity whose objects are stored in an array. The second sorting is provided by creating a
separate class
that implements the Comparator
interface. Use of lambda expression is recommended.
Testing of the program should include the previously implemented task, as well as sorting according to specific criteria.
Note: you should create classes with meaningful names that reflect the physical nature of the
individual task, not FirstEntity
, SecondEntity
, etc.
Javadoc comments should be added to the source code.
1.2 Class Hierarchy
Implement classes "Human," "Citizen", "Student", "Employee". Each
class should implement toString()
method. Create an array of references to different
objects of the class hierarchy. Create a cycle and display data that represents objects of different
types.
Note: it is necessary to create classes with meaningful names.
1.3 Minimum of a Function
Implement a program that finds minimum of some function by setting value of step and traversal of a given interval. Implement five versions:
- using abstract class and derived classes
- through the definition of the interface, the creation of a class that uses this interface as a parameter type of the minimum finding function, the creation of separate classes that implement this interface
- using previously described interface and anonymous classes
- using lambda expressions
- using references to methods.
Test the program for two different functions.
1.4 Implementation of an Array of Points through a Two-Dimensional Array
Implement the AbstractArrayOfPoints
, abstract class functionality given in Example 3.2, in
two ways:
- using a two-dimensional array of real numbers; each row of the array must correspond to the point;
- use of a one-dimensional array of real numbers; each pair of numbers in the array should correspond to the point.
Test classes.
Note: you should not make changes to the AbstractArrayOfPoints
class code (except
the package name and implementation of the sortByY()
method).
1.5 The Implementation of the Comparable Interface
Create a Circle
class that implements the Comparable
interface. A circle with a
larger radius is considered to be larger. Sort the array of Circle
objects.
1.6 The Implementation of the Comparator Interface
Create a Triangle
class. The triangle is defined by the lengths of the sides.
The area
of the triangle in this case can be calculated by Heron's formula:
where a, b and care the lengths of the sides of the triangle. Sort the array of
triangles by descending the area. To determine the sort criteria, use an object that implements the
Comparator
interface.
1.7 Calculation of the Definite Integral (Additional Task)
Create an Integrable
interface that contains a description of an abstract function that
takes an argument of type double
and returns the result of the same type.
This interface should contain the integral()
method with the default implementation (with
the default
modifier) of calculating the specified integral. The method
should receive start, end of the interval, and the accuracy of the calculations as arguments. The
default implementation of computing the integral should use the method of rectangles.
Create a class that redefines the integral()
method by implementing a trapezoid method for
calculating integral.
Calculate a definite integral by using both algorithms for different functions of the
java.lang.Math
class (see example 3.3). Compare results for different algorithms and different values of computing
accuracy.
2 Instructions
2.1 Inheritance
The inheritance mechanism assumes creation of derived classes from base classes. Derived class has direct access to all public and protected members of base class. Base class and derived classes build class hierarchy.
Unlike C++, only single inheritance of classes is allowed in Java. In other words, a class can inherit implementation from one base class only. Inheritance is always public. There are no private and protected base classes in Java. The syntax of inheritance is as follows:
class DerivedClass extends BaseClass { // class body }
The derived class functions have access to the members described as public
and protected
. Class members declared to be protected can be used by
descendants, as well as within their package. Private class members are not available even for its
descendants.
All Java classes are derived from java.lang.Object
directly or indirectly. The
Object
class offers useful methods, such as toString()
. You don't need to
declare base class java.lang.Object
explicitly.
The class inherits all members of the base class, except constructors. Before the constructor of a derived class executes, the base class constructor (the default constructor, unless explicitly called otherwise) is called.
The super
keyword is used to access members of the base class from within a derived
class:
- call a method on the base class that has been overridden by another method
- specify which base-class constructor should be called when creating instances of the derived class.
Here are examples of using super
keyword:
class BaseClass { int i, j; BaseClass(int i, int j) { this.i = i; this.j = j; } } class DerivedClass extends BaseClass { int k; DerivedClass(int i, int j, int k) { super(i, j); this.k = k; } }
A base class access using the super
keyword is permitted only in constructors or
non-static methods.
Classes can be declared with final
keyword. Final classes cannot be used as base
classes. The method declared as final
cannot be overridden. For instance:
final class A { void f() { } } class B { final void g() { } } class C extends A { // Error! Cannot inherit from A } class D extends B { void g() { } // Error! g() cannot be overridden }
Reference to derived class can be implicitly converted to base class reference. In other words, derived class objects can be always used instead of base class objects.
class Base { static void f(Base b) { } } class Derived extends Base { public static void main(String[] args) { Base b; b = new Derived(); // Implicit type conversion Derived d = new Derived(); f(d); // Implicit type conversion } }
Reverse conversion should be done explicitly:
Base b = new Base(); Derived d = (Derived) b;
Java supports instanceof
keyword, which allows you to check whether the object is an instance
of a certain type (or derived types). The expression
object instanceof class
returns the value of the boolean
type, which can be used to verify whether the method of this
class can be called:
if (x instanceof SomeClass) ((SomeClass)x).someMethod();
2.2 Sealed Classes
Starting from the JDK 17 version to Java syntax was extended with the opportunity to limit the list of subclasses. This was done in order to better control the correctness of creating specific realizations of subclasses. To limit potential derived classes in previous versions, it was necessary to make the base class with package (non-public) visibility. But this approach made it impossible not only to inherit, but also any use of the class outside the package. In addition, there is sometimes a need to permit inheritance for classes located in other packages.
The new opportunity to determine such restrictions involves the use of so-called sealed classes. After the sealed class name, a list of permitted derived classes is placed:
public sealed class SealedBase permits FirstDerived, SecondDerived { protected int data; }
Listed permitted subclasses must be accessible by the compiler. Such classes are defined with modifiers final
or sealed
.
In the latter case, an additional branch of allowed classes is created:
final class FirstDerived extends SealedBase { } sealed class SecondDerived extends SealedBase permits SomeSubclass { } final class SomeSubclass extends SecondDerived { { data = 0; } }
Attempt to create other subclasses leads to an error:
class AnotherSubclass extends SealedBase { // Помилка компіляції }
There is another modifier for the permitted derived class: non-sealed
. You can create
any subclasses from such a class:
non-sealed class SecondDerived extends SealedBase { } class PlainSubclass extends SecondDerived { { data = 0; } }
Permitted derived classes can be located in other packages.
2.3 Annotations (Metadata)
Annotations can contain additional information, which cannot be set using language constructs.
Annotations start from @ character. The most used annotation is @Override
. Thanks to this
annotation, the compiler can check whether the corresponding method was actually declared in the base
classes.
public class MyClass { @Override public String toString() { return "My overridden method!"; } }
There are other examples of annotations:
@SuppressWarnings("warning_identifier")
– compiler warnings should be silenced in the annotated element@Deprecated
– the use of the annotated element is no longer desirable
You can declare your own annotations.
2.4 Polymorphism
2.4.1 Overview
A runtime polymorphism is a property that provides definition of class object behavior not during compilation but at run time. Classes that provide identical interface but implemented for specific requirements are called polymorphic classes.
Usually polymorphism is implemented by so-called late binding mechanism. Connecting a method call to a method body is called binding. When binding is performed before the program is run, it's called early binding. Such binding is typical for procedural languages (such as C and Pascal). The late binding means that the binding occurs at run time based on the type of object. Late binding is also called dynamic binding. A late binding mechanism is used to implement the polymorphism.
In object-oriented programming languages, later binding is implemented through the mechanism of virtual functions. A virtual function (virtual method) is a function defined in the base class, and is overridden in the derived classes, so that the specific implementation of the function for the call will be determined at runtime. The choice of the implementation of a virtual function depends on the actual (and not declared in the description) type of the object. Because a reference to a base type can contain the address of an object of any derived type, the behavior of previously created classes can be changed later by overriding virtual methods. Overriding provides for creation of a new virtual method with the same name, a list of parameters and an access qualifier. In fact, polymorphic classes have classes that contain virtual functions.
In C++, late binding is implemented through virtual functions mechanism. In Java, all non-static
non-final methods are virtual methods. The virtual
keyword is not needed. Constructors and
private methods also cannot be virtual.
Starting with Java 5, the @Override
annotation can precede overridden virtual methods. This
annotation allows the compiler to make additional syntax check: new function signature must match the
signature of overridden function of the base class. Use of @Override
is desirable but not
obligatory.
All Java classes are polymorphic because java.lang.Object
is polymorphic as well. In
particular, each derived class can define its own virtual toString()
method that will be
called automatically when string representation of object is needed:
public class MyClass { int i = 10; @Override public String toString() { return "i = " + i; } public static void main(String[] args) { MyClass mc = new MyClass(); System.out.println(mc); // i = 10; } }
2.4.2 Abstract Classes and Methods
Sometimes, a class that you define represents an abstract concept and, as such, should not be
instantiated. Such concepts can be represented by abstract classes. Java uses
abstract
keyword to declare abstract classes:
abstract class SomeConcept { . . . }
An abstract class may contain abstract methods, that is, methods with no implementation. Abstract method
has no function body. Its declaration is similar to the declaration of the C++ member function, but the
declaration must be preceded by the abstract
keyword.
For example, you can declare an abstract class Shape
to provide fields and methods that will
be used by all subclasses, such as the current position and the moveTo()
method. The
Shape
class also declares abstract methods, such as draw()
, that need to be implemented by all
subclasses, but are implemented in different ways. No default implementation in the superclass makes
sense. The Shape
class would look something like this:
abstract class Shape { int x, y; . . . void moveTo(int newX, int newY) { . . . } abstract void draw(); }
Each non-abstract subclass of Shape
, such as Circle
and Rectangle
,
would have to provide an implementation for the draw()
method.
class Circle extends Shape { void draw() { . . . } } class Rectangle extends Shape { void draw() { . . . } }
Abstract methods are similar to pure virtual functions in C++ language.
An abstract class is not required to have an abstract method in it. However, any class that has an
abstract method in it or that does not provide an implementation for any abstract methods declared in
its superclasses must be declared as an abstract class (using abstract
keyword).
2.5 Concept of Interfaces. Arranging Objects
Java introduces a concept of interface. The interface can be considered as a purely abstract class, but unlike abstract classes, the interface never contains data, only methods. These methods are generally considered public:
interface SomeFunctions { void f(); int g(int x); }
A class may inherit from one and only one other class but can implement several interfaces. When a class
implements an interface, it either must provide implementations for all the methods declared in that
interface or this class must be declared with abstract
keyword.
Interfaces can contain data fields. Those fields are considered as final and static (compile-time constants). They must be initialized at the place of creation. Interfaces cannot contain constructors because there is no data other than static constants.
To indicate that a class implements an interface, the interface identifier is included in the list of the
implemented interfaces. This list starts with implements
keyword. Methods declared
in an interface are public and abstract by default and if this class implements a method, which was
declared in an interface, this method must be declared as public
:
interface SomeFunctions { void f(); int g(int x); } class SomeClass implements SomeFunctions { @Override public void f() { } @Override public int g(int x) { return x; } }
Note: Using the @Override
annotation is recommended, but not required.
An interface can have several base interfaces. Multiple inheritance of interfaces is safe in terms of data duplication and name conflicts:
interface SomeFunctions { void f(); int g(int x); } interface AnotherFunction { void h(int z); } interface AllFunctions extends SomeFunctions, AnotherFunction { int g(int x); // the declaration may be repeated }
Now the class that implements the AllFunctions
interface must define three functions:
class Implementation implements AllFunctions { @Override public void f() { } @Override // One implementation is used for both base and derived interfaces: public int g(int x) { return x; } @Override public void h(int z) { } }
A class can implement several interfaces. This is a more common way than creating a derived interface:
class AnotherImplementation implements SomeFunctions, AnotherFunction { @Override public void f() { } @Override public int g(int x) { return x; } @Override public void h(int z) { } }
Very often programmers create the reference to the interface which is initialized by object of a class that implements this interface. This approach is good practice because it allows you to easily replace one implementation of the interface with another one.
Interfaces are not derived from the java.lang.Object
class. You cannot create a new
interface object. Even for an empty interface, you need to create a class that implements it:
interface Empty { } class EmptyImplementation implements Empty { } ... public static void main(String[] args) { Empty empty1 = new Empty(); // Error Empty empty2 = new EmptyImplementation(); // Correct creation of the object }
The JDK provides a large number of standard interfaces. Consider using the Comparable
and
Comparator
interfaces to sort arrays.
The simplest way to implement ascending sort is call of java.util.Array.sort()
method with
reference to array as single argument (reference to an array). You can also sort any part of array. Such
methods are implemented for all primitive types. You can also sort arrays of objects into ascending
order, according to the natural ordering of its elements. All elements in the array must implement the
Comparable
interface. The only method of this interface is compareTo()
:
public int compareTo(Object o)
This method should return negative value (e.g. -1) if this object is less than o
, zero value
if objects are equal, and positive value otherwise.
Standard Double
, Integer
, Long
, etc, and String
classes implement this interface. The following program sorts an array of Integer
:
public class SortIntegers { public static void main(String[] args) { Integer[] a = {7, 8, 3, 4, -10, 0}; java.util.Arrays.sort(a); System.out.println(java.util.Arrays.toString(a)); } }
Note. The Comparable
interface name is an example of the most correct interface
name. It is desirable that the interface names end in -able
(Comparable
,
Runnable
, etc.). But this rule is very often violated even for standard interfaces.
Java 5 Comparable
is a generic interface. Creating generic classes and interfaces will be
considered later. Thanks to the generalization, function declared in the interface can get parameters of
other types but not of the type Object
only. In our case, the compareTo()
function should get an argument of the array item type.
The following example shows how to create your own class, which implements Comparable
interface. The program sorts array of rectangles by area:
class Rectangle implements Comparable<Rectangle> { private double width, height; public Rectangle(double width, double height) { this.width = width; this.height = height; } public double area() { return width * height; } public double perimeter() { return 2 * (width + height); } @Override public int compareTo(Rectangle rect) { return Double.compare(area(), rect.area()); } @Override public String toString() { return "[" + width + ", " + height + ", area = " + area() + ", perimeter = " + perimeter() + "]"; } } public class SortRectangles { public static void main(String[] args) { Rectangle[] a = {new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4)}; java.util.Arrays.sort(a); System.out.println(java.util.Arrays.toString(a)); } }
In this example, the static compare()
method of Double
class returns necessary
integer values which are used by sort()
method.
Sometimes we cannot (or do not want to) implement compareTo()
for particular class, or we
want to sort objects by different criteria. An alternative way of sorting is creation of a class that
implements Comparator
interface. Objects of such classes are used as arguments of
sort()
methods. Java 2 declaration of such methods (without generics) is as follows:
public static void sort(Object[] a, Comparator c) public static void sort(Object[] a, int fromIndex, int toIndex, Comparator c) // sort some part of items
The only method of Comparator
interface is compare()
. This method should return
negative value if first object is less than second, zero value if objects are equal, and positive value
otherwise.
Note. Java 5 Comparator
is also a generic interface. By its implementation, after
its name, in angle brackets it is necessary to specify the type of objects that you compare. Now the
compare()
function should take two arguments of array item type. For example
By using a class that implements the Comparator
interface, you can additionally sort by
perimeter of rectangles:
class CompareByPerimeter implements java.util.Comparator<Rectangle> { @Override public int compare(Rectangle r1, Rectangle r2) { return Double.compare(r1.perimeter(), r2.perimeter()); } } public class SortRectangles { public static void main(String[] args) { Rectangle[] a = {new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4)}; java.util.Arrays.sort(a); // sort by area System.out.println(java.util.Arrays.toString(a)); java.util.Arrays.sort(a, new CompareByPerimeter()); // sort by perimeter System.out.println(java.util.Arrays.toString(a)); } }
2.6 Nested Classes
2.6.1 Overview
It is possible to place a class definition within another class definition. This is called a nested class. It can be either static nested or inner. A nested class can be used inside either of outer class or somewhere else.
class Outer { class Inner { int i; }; Inner inner = new Inner(); } class Another { Outer.Inner i; }
Nested classes can be declared as public
, private
, or protected
.
Local classes are created inside blocks. There is also a special kind of local classes, the so-called anonymous classes.
A separate category is static nested classes, the use of which is similar to the nested classes in C++ and C#.
2.6.2 Inner Classes
Non-static nested classes are also inner classes. The main feature of the inner classes in Java is that the objects of these classes receive a reference to the object of the outer class. This fact implies two important conclusions:
- objects of inner classes have direct access to elements of outer class objects
- you cannot create object of inner class without object of outer class.
In this regard, Java offers a special mechanism for creating objects of the inner classes. This mechanism is illustrated in the following example:
class Outer { int k = 100; class Inner { void show() { System.out.println(k); } } } public class Test { public static void main(String[] args) { Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); inner.show(); } }
Non-static inner classes cannot contain static members.
You should keep in mind that objects of inner classes are not created automatically. Creating an object can be provided in the constructor or in any method of the outer class, and also outside it (if this class is not declared private). You can also create an array of inner class objects. Each of these objects will have access to the outer object.
Inner classes can have their own base classes. Therefore, we obtain something close to multiple inheritance:
class FirstBase { int a = 1; } class SecondBase { int b = 2; } class Outer extends FirstBase { int c = 3; class Inner extends SecondBase { void show() { System.out.println(a); System.out.println(b); System.out.println(c); } } } public class Test { public static void main(String[] args) { Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); inner.show(); } }
The following example has a purely theoretical meaning, since the multiple inheritance of classes, regardless of the methods of its implementation, is dangerous in terms of a possible name conflicts.
2.6.3 Local and Anonymous Classes
Inner classes can be also local. You cannot access local classes from the outside of the block in which they are defined. A local class definition is typically placed into function body:
void f() { class Local { int j; } Local l = new Local(); l.j = 100; System.out.println(l.j); }
You can also place local classes inside blocks.
An anonymous class can implement an interface or extend another class. It can either override existing base class methods or introduce new ones. To create an object of an anonymous class, you should call the constructor of the base class constructor, or specify the name of the interface with parentheses, and then you place the body of an anonymous class:
new Object() { // Adding a new method: void hello() { System.out.println("Hello!"); } }.hello(); System.out.println(new Object() { // Overriding an existing method: @Override public String toString() { return "This is an anonymous class."; } });
An anonymous class is never abstract. An anonymous class is always an inner class; it is never
static
.
An anonymous class is always implicitly final
. In the following example, new
anonymous class is created for definition of sorting order of string-type elements of an array:
void sortByABC(String[] a) { Arrays.sort(a, new Comparator<String>() { public int compare(String s1, String s2) { return (s1).compareTo(s2); } }); }
An anonymous class cannot have an explicitly declared constructor. However, a default constructor is always created. If the base class does not have a constructor without parameters, the necessary parameters of the constructor are placed in brackets when you create an object:
abstract class Base { int k; Base(int k) { this.k = k; } abstract void show(); } public class Test { static void showBase(Base b) { b.show(); } public static void main(String[] args) { showBase(new Base(10) { void show() { System.out.println(k); } }); } }
You can also use initialization blocks.
In order for anonymous classes to have access to local elements of external blocks, these elements should
be declared as final
.
2.6.4 Static Nested Classes
Static nested classes have access only to static members of outer classes. Objects of such classes can be created without the creation of objects of outer classes:
class Outer { int k = 100; static int m = 200; static class Inner { void show() { // k unavailable System.out.println(m); } } } public class Test { public static void main(String[] args) { Outer.Inner inner = new Outer.Inner(); inner.show(); } }
Static nested classes can contain their own static elements, as well as own nested static and non-static classes.
Classes can be created inside interfaces. These classes are static by default. Inside classes, you can also create interfaces that are static by default.
2.7 Interface Methods with Default Implementation
Java 8 provides a new opportunity to provide default implementation of methods declared in the interface.
To do this, before the corresponding function you must place the default
keyword,
after which the function can be implemented inside the interface. In the following example some
interface declares method and offers the default implementation of this method:
package ua.inf.iwanoff.java.third; public interface Greetings { default void hello() { System.out.println("Hello everybody!"); } }
The class that implements this interface may be empty. You can leave the default implementation of the
hello()
method:
package ua.inf.iwanoff.java.third; public class MyGreetings implements Greetings { }
During testing we receive a default greeting.
package ua.inf.iwanoff.java.third; public class GreetingsTest { public static void main(String[] args) { new MyGreetings().hello(); // Hello everybody! } }
The same can be obtained by using anonymous class. Its body will also be empty:
package ua.inf.iwanoff.java.third; public class GreetingsTest { public static void main(String[] args) { new Greetings() { }.hello(); // Hello everybody! } }
The presence of default implementation methods makes the interfaces even more similar to abstract (and
even non-abstract) classes. But the fundamental difference is that the interface cannot be directly used
to create objects. All classes are directly or indirectly derived from the base type
java.lang.Object
,
which contains the data and functions required to function all, even the simplest objects. Interfaces
are not classes and are from java.lang.Object
. An interface is just a declaration of a
certain behavior that can be supplemented with assistive tools (methods with default implementation).
The fields described in the interface are not the actual data of the object but the compile-time
constants. To call methods with default implementation, an object of a class that implements the
interface is required. Because of this the anonymous class object is created
new Greetings() { }.hello();
but not the interface instance
new Greetings().hello(); // Syntax error!
The method with default implementation can be overridden:
package ua.inf.iwanoff.java.third; public class MyGreetings implements Greetings { @Override public void hello() { System.out.println("Hello to me!"); } }
Now, creating an object of this class, we get a new greeting.
If overridden method needs to call the default interface method, you can use the
super
keyword:
Greetings.super.hello();
We can offer such an example. Suppose it is necessary to print the values of some function at a certain interval with a given step. You can create an interface with one abstract method (the calculation of some function) and one method with a default implementation:
package ua.inf.iwanoff.java.third; public interface FunctionToPrint { public double f(double x); default void print(double x) { System.out.printf("x = %7f f(x) = %7f%n", x, f(x)); } }
In the PrintValues
class, we create a printTable()
method. This method uses the
previously created interface.
package ua.inf.iwanoff.java.third; public class PrintValues { static void printTable(double from, double to, double step, FunctionToPrint func) { for (double x = from; x <= to; x += step) { func.print(x); } System.out.println(); } // Create an object of an anonymous class in the main() function: public static void main(String[] args) { printTable(-2, 2, 0.5, new FunctionToPrint() { @Override public double f(double x) { return x * x * x; } }); } }
Assume the accuracy of the values is not satisfactory. In this case, you can also override the
print()
method:
public static void main(String[] args) { printTable(-2, 2, 0.5, new FunctionToPrint() { @Override public double f(double x) { return x * x * x; } @Override public void print(double x) { System.out.printf("x = %9f f(x) = %9f%n", x, f(x)); } }); }
The main advantage of interfaces with default implementation is the ability to extend the interfaces from version to version, ensuring compatibility with the old code. Suppose, an interface was previously defined in some library:
public interface SomeInterface { void f(); }
This interface was implemented by some class:
public class OldImpl implements SomeInterface { @Override public void f() { // implementation } }
Now, when updating the library, a new version of the interface was created with the new method:
interface SomeInterface { void f(); default void g() { // implementation } }
This method will be implemented by new classes:
public class NewImpl implements SomeInterface { @Override public void f() { // implementation } @Override public void g() { // implementation } }
Without default implementation, the code built on the previous version will not be compiled.
When inheriting an interface that contains a method with a default implementation, this method is also inherited with its implementation, but it can also be re-declared as abstract, or redefined with another implementation.
In Java 8, interfaces may also contain the implementation of static methods. These methods should logically relate to this interface (for example, they can get the reference to the interface as a parameter). Most often, these are auxiliary methods. Like all interface members, these static methods are public. You can specify public explicitly, but there is no need for it.
In the example above, the printTable()
function could be placed inside the interface:
package ua.inf.iwanoff.java.third; public interface FunctionToPrint { public double f(double x); default void print(double x) { System.out.printf("x = %9f f(x) = %9f%n", x, f(x)); } static void printTable(double from, double to, double step, FunctionToPrint func) { for (double x = from; x <= to; x += step) { func.print(x); } System.out.println(); } }
The function call should be performed through the interface name.
2.8 Working with Functional Interfaces in Java 8
2.8.1 Lambda Expressions and Functional Interfaces
Java interfaces very often contain declaration of the one and only abstract function (without default implementation). Such interfaces are called functional interfaces. They are generally used to implement callback mechanism, event handling, etc. Despite their simplicity, their implementation, however, requires a separate class (ordinary, nested or anonymous). Even using an anonymous class, we get cumbersome poorly readable syntax. Lambda expressions that appeared in the Java version 8, can reduce the need for anonymous classes in the source code.
In programming languages, there is the concept of functional object – an object that can be used as a function. Lambda expression is a special syntax for describing a functional object within a method. In other words, the lambda expression is a way to describe a function inside another function.
The term "lambda expression" is associated with mathematical discipline, so-called lambda calculus. Lambda calculus is a formal system developed by the American mathematician Alonzo Church for formalization and analysis of the notion of computability. Lambda calculus has become a formal basis for functional programming languages (Lisp, Scheme, etc.).
Lambda expression in Java has the following syntax:
- a list of formal parameters separated by commas and enclosed in parentheses; if we have one parameter, parentheses can be omitted; if there are no parameters, you need an empty pair of parentheses;
- arrow (pointer,
->
); - a body consisting of one expression or a block; if a block is used, the
return
statement may be inside it.
For example, this is a function with one parameter:
k -> k * k
The same with the brackets and the block:
(k) -> { return k * k; }
Function with two parameters:
(a, b) -> a + b
Function without parameters:
() -> System.out.println("First")
For example, there is a functional interface:
public interface SomeInt { int f(int x); }
When calling some function, you need to send an argument of the functional interface type. Traditionally, you can create an anonymous class:
someFunc(new SomeInt() { @Override public int f(int x) { return x * x; } });
You can create a variable of object that implements the interface, and use it instead of anonymous class:
SomeInt func = k -> k * k; someFunc(func);
You can also create an anonymous object when calling a function with a functional interface type parameter:
someFunc(x -> x * x);
Since each lambda expression is associated with a specific functional interface, the parameter types and the result are determined automatically in comparison with the respective functional interface.
An example task with a function value table can be implemented using lambda expressions. The previously created interface is functional interface because it has the only abstract method declared:
package ua.inf.iwanoff.java.third; public interface FunctionToPrint { public double f(double x); default void print(double x) { System.out.printf("x = %9f f(x) = %9f%n", x, f(x)); } static void printTable(double from, double to, double step, FunctionToPrint func) { for (double x = from; x <= to; x += step) { func.print(x); } System.out.println(); } }
Using the functional interface with the lambda expression:
package ua.inf.iwanoff.java.third; public class PrintWithLambda { public static void main(String[] args) { FunctionToPrint.printTable(-2.0, 2.0, 0.5, x -> x * x * x); } }
2.8.2 Using References to Methods
Very often the entire lambda-expression body consists of calling the existing method only. In this case, instead of the lambda expression, you can use the reference to this method. There are several variants of using methods references.
Kind of reference to the method | Syntax | Example |
---|---|---|
Reference to the static method | Class::staticMethodName |
String::valueOf |
Reference to a non-static method for a given object | object::nonStaticMethodName |
s::toString |
Reference to a non-static method for parameter | Class::nonStaticMethodName |
Object::toString |
Reference to the constructor | Class::new |
String::new |
For example, there are functional interfaces:
interface IntOperation { int f(int a, int b); } interface StringOperation { String g(String s); }
You can create some class:
class DifferentMethods { public int add(int a, int b) { return a + b; } public static int mult(int a, int b) { return a * b; } }
Now methods are used:
public class TestMethodReferences { static void print(IntOperation op, int a, int b) { System.out.println(op.f(a, b)); } static void print(StringOperation op, String s) { System.out.println(op.g(s)); } public static void main(String[] args) { DifferentMethods dm = new DifferentMethods(); print(dm::add, 3, 4); print(DifferentMethods::mult, 3, 4); print(String::toUpperCase, "text"); } }
2.8.3 Standard Functional Interfaces
Instead of creating new functional interfaces, in most cases it is enough to use the standard generic
interfaces that are described in the java.util.function
package.
Interface | Description |
---|---|
BiConsumer<T,U> |
Represents an operation that accepts two input arguments and returns no result |
BiFunction<T,U,R> |
Represents a function that accepts two arguments and produces a result |
BinaryOperator<T> |
Represents an operation upon two operands of the same type, producing a result of the same type as the operands |
BiPredicate<T,U> |
Represents a predicate (boolean -valued function) of two arguments
|
BooleanSupplier |
Represents a supplier of boolean -valued results |
Consumer<T> |
Represents an operation that accepts a single input argument and returns no result |
DoubleBinaryOperator |
Represents an operation upon two double -valued operands and
producing a double -valued result
|
DoubleConsumer |
Represents an operation that accepts a single
double -valued
argument and returns no result
|
DoubleFunction<R> |
Represents a function that accepts a double -valued argument and
produces a result
|
DoublePredicate |
Represents a predicate (boolean -valued function) of one
double -valued argument
|
DoubleSupplier |
Represents a supplier of double -valued results |
DoubleToIntFunction |
Represents a function that accepts a double -valued argument and
produces an int -valued result |
DoubleToLongFunction |
Represents a function that accepts a double -valued argument and
produces a long -valued result
|
DoubleUnaryOperator |
Represents an operation on a single double -valued operand that
produces a double -valued result
|
Function<T,R> |
Represents a function that accepts one argument and produces a result |
IntBinaryOperator |
Represents an operation upon two int -valued operands and producing
an int -valued result
|
IntConsumer |
Represents an operation that accepts a single int -valued argument
and returns no result
|
IntFunction<R> |
Represents a function that accepts an int -valued argument and
produces a result
|
IntPredicate |
Represents a predicate (boolean -valued function) of one
int -valued argument
|
IntSupplier |
Represents a supplier of int -valued results |
IntToDoubleFunction |
Represents a function that accepts an int -valued argument and
produces a double -valued result
|
IntToLongFunction |
Represents a function that accepts an int -valued argument and
produces a long -valued result
|
IntUnaryOperator |
Represents an operation on a single int -valued operand that
produces an int -valued result
|
LongBinaryOperator |
Represents an operation upon two long -valued operands and
producing a long -valued result
|
LongConsumer |
Represents an operation that accepts a single
long -valued argument and returns no
result
|
LongFunction<R> |
Represents a function that accepts a long -valued argument and
produces a result
|
LongPredicate |
Represents a predicate (boolean -valued function) of one
long -valued argument
|
LongSupplier |
Represents a supplier of long -valued results |
LongToDoubleFunction |
Represents a function that accepts a
long -valued argument and produces a
double -valued result
|
LongToIntFunction |
Represents a function that accepts a
long -valued argument and produces
an int -valued result
|
LongUnaryOperator |
Represents an operation on a single long -valued operand that
produces a long -valued result long
|
ObjDoubleConsumer<T> |
Represents an operation that accepts a T -valued and a
double -valued argument, and returns no result
|
ObjIntConsumer<T> |
Represents an operation that accepts a T -valued and a
int -valued argument, and returns no result
|
ObjLongConsumer<T> |
Represents an operation that accepts a T -valued and a
long -valued argument, and returns no result
|
Predicate<T> |
Represents a predicate (boolean -valued function) of one argument
|
Supplier<T> |
Represents a supplier of results |
ToDoubleBiFunction<T,U> |
Represents a function that accepts two arguments and produces a
double -valued result
|
ToDoubleFunction<T> |
Represents a function that produces a double -valued result |
ToIntBiFunction<T,U> |
Represents a function that accepts two arguments and produces an
int -valued result
|
ToIntFunction<T> |
Represents a function that produces an
int -valued result
|
ToLongBiFunction<T,U> |
Represents a function that accepts two arguments and produces a
long -valued result
|
ToLongFunction<T> |
Represents a function that produces a long -valued result |
UnaryOperator<T> |
Represents an operation on a single operand that produces a result of the same type as its operand |
In addition to these, a generic Comparator
interface, a Runnable
interface
(used in multithreading programming), and many more also are functional interfaces.
2.8.4 Composition of Lambda Expressions
You can implement a composition of lambda expressions (use lambda expressions as parameters). For this
purpose the java.util.function
package interfaces provide methods with a default
implementation, providing execution of a function passed as a parameter, before or after this method. In
particular, the following methods are defined in the Function
interface:
// The before function is executed, and then the calling function is executed: Function compose(Function before) // The after function is executed after the calling function: Function andThen(Function after)
Use of these methods and their differences will be considered in this example. There is a class with a
static function calc()
, which accepts a functional interface and an argument of Double
type. You can make a composition of lambda expressions:
package ua.inf.iwanoff.java.third; import java.util.function.Function; public class ComposeDemo { public static Double calc(Function<Double , Double> operator, Double x) { return operator.apply(x); } public static void main(String[] args) { Function<Double , Double> addTwo = x -> x + 2; Function<Double , Double> duplicate = x -> x * 2; System.out.println(calc(addTwo.compose(duplicate), 10.0)); // 22.0 System.out.println(calc(addTwo.andThen(duplicate), 10.0)); // 24.0 } }
The composition may be more complex:
System.out.println(calc(addTwo.andThen(duplicate).andThen(addTwo), 10.0)); // 26.0
2.9 Object Cloning, Equivalence Checking, and Getting Hash Codes
2.9.1 Object Cloning
Sometimes there is a need to create a copy of some object, for example, to perform some actions that do not violate the original data. Simple assignment only copies references. If you need to copy an object elementwise, you should use the mechanism of the so-called cloning.
The base class java.lang.Object
implements a function called clone()
, which by
default allows you to perform elementwise copy the object. This function is also defined for arrays,
strings, and other standard classes. For example, you can get a copy of an existing array and work with
this copy:
package ua.inf.iwanoff.java.third; import java.util.Arrays; public class ArrayClone { public static void main(String[] args) { int[] a1 = { 1, 2, 3, 4 }; int[] a2 = a1.clone(); // copy of items System.out.println(Arrays.toString(a2)); // [1, 2, 3, 4] a1[0] = 10; // change the first array System.out.println(Arrays.toString(a1)); // [10, 2, 3, 4] System.out.println(Arrays.toString(a2)); // [1, 2, 3, 4] } }
In order to be able to clone objects of user classes, these classes must implement the
Cloneable
interface. This interface does not declare any methods. It just indicates that
objects of this class can be cloned. Otherwise, calling the clone()
function will generate
an exception of the CloneNotSupportedException
type.
Note. The mechanism of exception handling is largely similar to the corresponding C++ language
mechanism. In Java, you should list possible thrown exceptions using the throws
keyword in the method header. The mechanism for processing exceptions will be discussed later.
For example, if we need to clone objects of the Human
class, with two fields of
String
type (name
and surname
), we'll add the implementation of
the Cloneable
interface to the class description. Then we'll generate a constructor with
two parameters and override toString()
method. In the main()
function, we'll
perform an object cloning test:
package ua.inf.iwanoff.java.third; public class Human implements Cloneable { private String name; private String surname; public Human(String name, String surname) { super(); this.name = name; this.surname = surname; } @Override public String toString() { return name + " " + surname; } public static void main(String[] args) throws CloneNotSupportedException { Human human1 = new Human("John", "Smith"); Human human2 = (Human) human1.clone(); System.out.println(human2); // John Smith human1.name = "Mary"; System.out.println(human1); // Mary Smith System.out.println(human2); // John Smith } }
As you can see from the example, the source object can be modified after cloning. In this case, the copy does not change.
The clone()
function can be overridden with changing its result type and making it open for
ease of use. Thanks to the availability of this function, cloning will be simplified (you will not need
to convert the type every time):
@Override public Human clone() throws CloneNotSupportedException { return (Human) super.clone(); } . . . Human human2 = human1.clone();
The standard cloning implemented in the java.lang.Object
class allows you to create copies
of objects whose fields are value types and String
type (as well as wrapper classes). If
the fields of the object are references to arrays or other types, it is necessary to apply the so-called
"deep" cloning. For example, some class SomeCloneableClass
contains two fields of
type double and an array of integers. "Deep" cloning will create separate arrays for different
objects.
package ua.inf.iwanoff.java.third; import java.util.Arrays; public class SomeCloneableClass implements Cloneable { private double x, y; private int[] a; public SomeCloneableClass(double x, double y, int[] a) { super(); this.x = x; this.y = y; this.a = a; } @Override protected SomeCloneableClass clone() throws CloneNotSupportedException { SomeCloneableClass scc = (SomeCloneableClass) super.clone(); // copy x and y scc.a = a.clone(); // now two objects work with different arrays return scc; } @Override public String toString() { return " x=" + x + " y=" + y + " a=" + Arrays.toString(a); } public static void main(String[] args) throws CloneNotSupportedException { SomeCloneableClass scc1 = new SomeCloneableClass(0.1, 0.2, new int[] { 1, 2, 3 }); SomeCloneableClass scc2 = scc1.clone(); scc2.a[2] = 4; System.out.println("scc1:" + scc1); System.out.println("scc2:" + scc2); } }
2.9.2 Equivalence Checking
In order to make sure that the cloned objects are the same, it would be nice to be able to automatically
compare all the fields. The reference model of Java objects does not allow you to compare the contents
of objects using the comparison operation (==
), since the references are compared. To
compare data, it is advisable to use the equals()
function defined in the
java.lang.Object
class. For classes whose fields are value types, the class method provides an elementwise comparison. If
the fields are object references, you must explicitly override the equals()
function. A
typical implementation of the equals()
method includes examining references (if they
match), then check whether the reference is null, then check type (for example, using
instanceof
operator). If the types match, fields are checked.
Here is a complete example with the Human
class:
package ua.inf.iwanoff.java.third; public class Human implements Cloneable { private String name; private String surname; public Human(String name, String surname) { super(); this.name = name; this.surname = surname; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || !(obj instanceof Human)) { return false; } Human h = (Human) obj; return name.equals(h.name) && surname.equals(h.surname); } @Override public Human clone() throws CloneNotSupportedException { return (Human) super.clone(); } @Override public String toString() { return name + " " + surname; } public static void main(String[] args) throws CloneNotSupportedException { Human human1 = new Human("John", "Smith"); Human human2 = human1.clone(); System.out.println(human2); human1.name = "Mary"; System.out.println(human1); System.out.println(human2); human2.name = new String("Mary"); System.out.println(human2); System.out.println(human1.equals(human2)); // true } }
If the equals()
method had not been defined, the last comparison would have given
false
.
To compare two arrays, you should call the static function equals()
of the
Arrays
class. This function compares array elementwise (calls the equals()
method):
Arrays.equals(array1, array2);
2.9.3 Hash Codes
The implementation of the equals()
function for large objects may require significant
calculations,
since all data members should be checked. For simplified verification for possible equality of two
objects Java
uses so-called hashing.
Hashing is the process of obtaining from an object a unique code using some formal algorithm. In a broad sense, the result is a sequence of bits of fixed length, in the particular case, this is a simple integer. This conversion is performed by a so-called hash function. The hash function should meet the following requirement: the hash function should return the same hash code each time it is applied to identical or equal objects. Unfortunately, it is not possible to ensure producing different of hash codes for different objects.
Hashing is used in some container classes of the Java Collection Framework to make it impossible to put identical elements into the collection.
All Java objects inherit the standard implementation of a hashCode()
function defined in the
Object
class.
This function returns a hash code obtained by converting an object's internal address to a number, which
ensures
the creation of a unique code for each individual object.
Specific standard classes implement their hash functions. For example, for a string, the value of the hash function is calculated by the formula:
s[0]*31n-1 + s[1]*31n-2 + ... + s[n-1]
Here s[0]
, s[1]
, etc. are codes of the corresponding characters.
The functions hashCode()
for Integer
, Double
, etc. are also
stable.
For user defined types, the hashCode()
function should be redefined.
3 Sample Programs
3.1 Hierarchy of Real World Objects
Our goal is to create the following class hierarchy:
- Region
- Populated Region
- Country
- City
- Island
Several classes of this hierarchy can be used as base classes for other classes (for instance, "Uninhabited island", "National park", "Borough", etc.). The hierarchy of classes can be expanded with the classes "City" and "Island". Each class should provide its original constructor for fields' initialization. Array of references to base object can be filled with references to objects of different derived types. For each object, a string of data about it will be displayed on the screen.
To obtain string representation of some object, we'll override toString()
function.
The class hierarchy can be as follows:
package ua.inf.iwanoff.java.third; import java.util.*; // Class hierarchy class Region { private String name; private double area; public Region(String name, double area) { this.name = name; this.area = area; } public String getName() { return name; } public double getArea() { return area; } public String toString() { return "Region" + name + ".\tArea " + area + " sq.km."; } } class PopulatedRegion extends Region { private int population; public PopulatedRegion(String name, double area, int population) { super(name, area); this.population = population; } public int getPopulation() { return population; } public int density() { return (int) (population / getArea()); } public String toString() { return "Populated Region" + getName() + ".\tArea " + getArea() + " sq.km. \tPopulation " + population + " inhab.\tDensity" + density() + " inhab. per sq.km."; } } class Country extends PopulatedRegion { private String capital; public Country(String name, double area, int population, String capital) { super(name, area, population); this.capital = capital; } public String getCapital() { return capital; } public String toString() { return "Country " + getName() + ".\tArea " + getArea() + " sq.km. \tPopulation " + getPopulation() + " inhab.\tDensity " + density() + " inhab. per sq.km.\tCapital " + capital; } } class City extends PopulatedRegion { private int boroughs; // count of boroughs public City(String name, double area, int population, int boroughs) { super(name, area, population); this.boroughs = boroughs; } public int getBoroughs() { return boroughs; } public String toString() { return "City " + getName() + ".\tArea " + getArea() + " sq.km. \tPopulation " + getPopulation() + " inhab.\tDensity " + density() + " inhab. per sq.km.\tBoroughs: " + boroughs; } } class Island extends PopulatedRegion { private String sea; public Island(String name, double area, int population, String sea) { super(name, area, population); this.sea = sea; } public String getSea() { return sea; } public String toString() { return "Island " + getName() + ".\tArea " + getArea() + " sq.km. \tPopulation " + getPopulation() + " inhab.\tDensity " + density() + " inhab. per sq.km.\tSea - " + sea; } } public class Regions { public static void main(String[] args) { Region[] a = { new City("Kiev", 839, 2679000, 10), new Country("Ukraine", 603700, 46294000, "Kiev"), new City("Kharkiv", 310, 1461000, 9), new Island("Zmiiny", 0.2, 30, "Black Sea") }; for (Region region : a) { System.out.println(region); } } }
3.2 Class for Representing an Array of Points
3.2.1 Problem Statement and Creating Abstract Class
Suppose we want to develop a class to represent an array of points. Each point is represented by two
numbers of the double
type: x
and y
. It is necessary to
provide setting point coordinates, getting coordinates of the specific point and the total number of
points, adding a point to the end of an array, and removing the last point. In addition, it is necessary
to organize the sorting of the array by increasing the specified coordinate and output the coordinates
of points into a string.
The simplest, but not the only solution is to create a Point
class with two fields and
creating an array of references to the Point
type. Such a solution is correct in terms of
organizing the data structure, but not sufficiently effective, since it involves placing both the array
itself and individual items in free store. Alternative variants are use of two arrays, two-dimensional
array, etc.
The final decision on the structure of data can be taken only in the context of a specific task. Polymorphism allows us to implement the necessary algorithms without binding to a specific data structure. To do this, we create an abstract class in which the access functions are declared as abstract, sorting and output algorithms are implemented using abstract access functions. In addition, we can define a function for testing. The corresponding abstract class will be as follows:
package ua.inf.iwanoff.java.third; public abstract class AbstractArrayOfPoints { // Recording of new point coordinates: public abstract void setPoint(int i, double x, double y); // Getting X of the i-th point: public abstract double getX(int i); // Getting Y of the i-th point: public abstract double getY(int i); // Getting the number of points: public abstract int count(); // Adding a point to the end of an array: public abstract void addPoint(double x, double y); // Deleting the last point: public abstract void removeLast(); // Sorting by X: public void sortByX() { boolean mustSort; // repeat while // the mustSort is true do { mustSort = false; for (int i = 0; i < count() - 1; i++) { if (getX(i) > getX(i + 1)) { // exchange items: double x = getX(i); double y = getY(i); setPoint(i, getX(i + 1), getY(i + 1)); setPoint(i + 1, x, y); mustSort = true; } } } while (mustSort); } // The sortByY() function can be implemented in the similar way // Getting a string representation: @Override public String toString() { String s = ""; for (int i = 0; i < count(); i++) { s += "x = " + getX(i) + " \ty = " + getY(i) + "\n"; } return s + "\n"; } // Testing sorting on four points: public void test() { addPoint(22, 45); addPoint(4, 11); addPoint(30, 5.5); addPoint(-2, 48); sortByX(); System.out.println(this); } }
Now we can implement different forms of the data structure.
3.2.2 Implementation through an Array of Point objects
The first possible implementation would be the creation of a Point
class and use an array of
Point
references. A class for point representation can be added to the same package. The
Point
class will contain two fields and a constructor:
package ua.inf.iwanoff.java.third; public class Point { private double x, y; public Point(double x, double y) { this.x = x; this.y = y; } public double getX() { return x; } public double getY() { return y; } public void setPoint(double x, double y) { this.x = x; this.y = y; } }
In the ArrayOfPointObjects
class, we create a field: a reference to the Point
array. We initialize this field with an empty array. The implementation of most of the functions is
obvious. The greatest difficulty is concerned with the implementation of adding and removing points. In
both cases, you need to create a new array of the correct length and overwrite the contents of the old
one. In the main()
function, we perform testing. All code of the
AbstractArrayOfPoints.java
file will look like this:
package ua.inf.iwanoff.java.third; public class ArrayOfPointObjects extends AbstractArrayOfPoints { private Point[] p = { }; @Override public void setPoint(int i, double x, double y) { if (i < count()) { p[i].setPoint(x, y); } } @Override public double getX(int i) { return p[i].getX(); } @Override public double getY(int i) { return p[i].getY(); } @Override public int count() { return p.length; } @Override public void addPoint(double x, double y) { // Create an array larger by one item: Point[] p1 = new Point[p.length + 1]; // Copy all items: System.arraycopy(p, 0, p1, 0, p.length); // Write a new point to the last item: p1[p.length] = new Point(x, y); p = p1; // now p points to a new array } @Override public void removeLast() { if (p.length == 0) { return; // the array is already empty } // Create an array smaller by one item: Point[] p1 = new Point[p.length - 1]; // Copy all items except the last one: System.arraycopy(p, 0, p1, 0, p1.length); p = p1; // now p points to a new array } public static void main(String[] args) { // An anonymous object can be created: new ArrayOfPointObjects().test(); } }
As a result, we get points sorted by coordinate X.
3.2.3 Implementation through Two Arrays
An alternative implementation involves creating two arrays to separately store the values of
X
and Y
. We create an ArrayWithTwoArrays
class using similar
options. In the ArrayWithTwoArrays
class, we create two fields (references to arrays of
real numbers) and initialize them with empty arrays. The implementation of functions is similar to the
previous version. In the main()
function, we perform testing:
package ua.inf.iwanoff.java.third; public class ArrayWithTwoArrays extends AbstractArrayOfPoints { private double[] ax = { }; private double[] ay = { }; @Override public void setPoint(int i, double x, double y) { if (i < count()) { ax[i] = x; ay[i] = y; } } @Override public double getX(int i) { return ax[i]; } @Override public double getY(int i) { return ay[i]; } @Override public int count() { return ax.length; // or ay.length, they are the same } @Override public void addPoint(double x, double y) { double[] ax1 = new double[ax.length + 1]; System.arraycopy(ax, 0, ax1, 0, ax.length); ax1[ax.length] = x; ax = ax1; double[] ay1 = new double[ay.length + 1]; System.arraycopy(ay, 0, ay1, 0, ay.length); ay1[ay.length] = y; ay = ay1; } @Override public void removeLast() { if (count() == 0) { return; } double[] ax1 = new double[ax.length - 1]; System.arraycopy(ax, 0, ax1, 0, ax1.length); ax = ax1; double[] ay1 = new double[ay.length - 1]; System.arraycopy(ay, 0, ay1, 0, ay1.length); ay = ay1; } public static void main(String[] args) { new ArrayWithTwoArrays().test(); } }
The results must be identical.
3.3 Using Interfaces with Default Methods Implementation
Suppose it is necessary to find the root of the equation using Tangent method (Newton method). This method involves the use of first and second derivative of the function for finding the root. The approximate value of the first derivative of any function can be found by the formula:
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 FirstImplementation implements FunctionWithDerivatives { @Override public double f(double x) { return Math.sin(x - 0.5); } public static void main(String[] args) { System.out.println(Newton.solve(0, 1, 0.000001, new FirstImplementation())); } }
For functions, we can redefine the mechanism for calculating the first and second derivatives. For example, for a cubic polynomial
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 SecondImplementation implements FunctionWithDerivatives { @Override public double f(double x) { return x * x * x - 6 * x * x + 12 * x - 9; } @Override public double f1(double x) { return 3 * x * x - 12 * x + 12; } @Override public double f2(double x) { return 6 * x - 12; } public static void main(String[] args) { System.out.println(Newton.solve(0, 1, 0.000001, new SecondImplementation())); } }
The explicit definition of derivatives can improve the efficiency of the algorithm.
3.4 Solving Equation using Dichotomy Method
3.4.1 Problem Statement
Assume that we want to solve some equation using dichotomy (bisection) method. Generally, equation can be as follows:
The dichotomy method allows us to find the only root of the equation. If there are no roots, or more than one, the results cannot be reliable.
All numeric methods require calculation of a left-hand member of an equation in different points. However, f(x) can be different in different projects. A special mechanism of transferring data about this function is required.
3.4.2 Using Abstract Class
The first approach uses concept of abstract classes. An abstract class called
AbstractEquation
defines an abstract f()
function, as well as non-abstract
method that solves an equation (solve()
):
package ua.inf.iwanoff.java.third; public abstract class AbstractEquation { public abstract double f(double x); public double solve(double a, double b, double eps) { double x = (a + b) / 2; while (Math.abs(b - a) > eps) { if (f(a) * f(x) > 0) { a = x; } else { b = x; } x = (a + b) / 2; } return x; } }
Now we can create a new class with particular f()
function:
package ua.inf.iwanoff.java.third; public class SpecificEquation extends AbstractEquation { public double f(double x) { return x * x - 2; } public static void main(String[] args) { SpecificEquation se = new SpecificEquation(); System.out.println(se.solve(0, 2, 0.000001)); } }
3.4.3 Using the Interface and the Class that Implements It
Interfaces provide another approach to this problem. We can declare interface for representation of the left side of an equation. To create a new interface within Eclipse environment, use New | Interface function.
package ua.inf.iwanoff.java.third; public interface LeftSide { double f(double x); }
The Solver
class provides a static method that solves an equation:
package ua.inf.iwanoff.java.third; public class Solver { static double solve(double a, double b, double eps, LeftSide ls) { double x = (a + b) / 2; while (Math.abs(b - a) > eps) { if (ls.f(a) * ls.f(x) > 0) { a = x; } else { b = x; } x = (a + b) / 2; } return x; } }
The class that implements the interface contains a specific implementation of the f()
function:
package ua.inf.iwanoff.java.third; class MyEquation implements LeftSide { public double f(double x) { return x * x - 2; } } public class InterfaceTest { public static void main(String[] args) { System.out.println(Solver.solve(0, 2, 0.000001, new MyEquation())); } }
The program can be modified to take into account Java 8 capabilities and functional interfaces. The root finding method can be implemented inside the interface:
package ua.inf.iwanoff.java.third; public interface FunctionToSolve { double f(double x); static double solve(double a, double b, double eps, FunctionToSolve func) { double x = (a + b) / 2; while (Math.abs(b - a) > eps) { if (func.f(a) * func.f(x) > 0) { a = x; } else { b = x; } x = (a + b) / 2; } return x; } }
Now you should call FunctionToSolve.solve()
instead of Solver.solve()
.
3.4.4 Using an Anonymous Class
If the function is only necessary to solve the equation, it can be defined in an anonymous class:
package ua.inf.iwanoff.java.third; public class SolveUsingAnonymousClass { public static void main(String[] args) { System.out.println(FunctionToSolve.solve(0, 2, 0.000001, new FunctionToSolve() { @Override public double f(double x) { return x * x - 2; } })); } }
3.4.5 Using Lambda Expressions
The left part of the equation can be defined by the lambda expression (instead of an anonymous class):
package ua.inf.iwanoff.java.third; public class SolveUsingLambda { public static void main(String[] args) { System.out.println(FunctionToSolve.solve(0, 2, 0.000001, x -> x * x - 2)); } }
3.4.6 Using References to Methods
The previous task can be solved using references to methods. We can implement the function as a separate static method:
package ua.inf.iwanoff.java.third; public class SolveUsingReference { public static double f(double x) { return x * x - 2; } public static void main(String[] args) { System.out.println(FunctionToSolve.solve(0, 2, 0.000001, SolveUsingReference::f)); } }
3.5 Hierarchy of the "Country" and "Census" Classes
Assume that we need to develop a program that provides classes for representation of a country and a
census. Data members of Country
class are name, area, and a sequence of references to
Census
object.
The Census
class includes data about year, population, and comments.
The following methods should be implemented:
- calculation of population density for a given year
- finding census with the maximum population
- finding particular word in a comment
- sorting censuses by population
- sorting censuses in alphabetic order of comments.
The Census
class will implement the functions of obtaining a string representation, checking
the
equivalence, getting hash codes, checking the presence of words and the sequences of letters in the
comments and
testing. In order to ensure sorting for the population increase, the Comparable
interface
should be
implemented and the compareTo()
function
should provide a "natural" comparison of the population.
Given the different options for storing the sequence of censuses, it is advisable to create a hierarchy of classes.
Basic abstract AbstractCountry
class that represent country should not contain data about the sequence
of censuses, only declaration of access functions and sorting methods should be declared.
It is necessary to implement functions of getting string representation, getting hash codes, checking of equivalence,
calculation of population density in for a certain census, the search for a census with the maximum population, as
well as checking whether the comments contain a certain word.
The derived class representing the country will use an array to store the sequence of censuses. It is necessary to implement census sorting functions using standard array sorting tools.
We create a new Census
class in ua.inf.iwanoff.java.third
package.
Its code will be as follows:
package ua.inf.iwanoff.java.third; import java.util.StringTokenizer; /** * The class is responsible for presenting the census. * The census is represented by year, population and comments */ public class Census implements Comparable<Census> { private int year; private int population; private String comments; /** * Constructor initializes the object with default values */ public Census() { } /** * Constructor initializes the object with given values * * @param year year of census * @param population population in the specified year * @param comments the text of the comment */ public Census(int year, int population, String comments) { this.year = year; this.population = population; this.comments = comments; } /** * Returns the census year * @return year of census in the form of an integer value */ public int getYear() { return year; } /** * Sets the value of the census year * @param year year of census in the form of an integer value */ public void setYear(int year) { this.year = year; } /** * Returns the population * @return the population in the form of an integer value */ public int getPopulation() { return population; } /** * Sets the population * @param population the population in the form of an integer value */ public void setPopulation(int population) { this.population = population; } /** * Returns the comment string * @return a census comment in the form of a string */ public String getComments() { return comments; } /** * Sets the content of a comment string * @param comments a census comment in the form of a string */ public void setComments(String comments) { this.comments = comments; } /** * Provides a census data in the form of a string * * @return string representation of a census data */ @Override public String toString() { return "The census in " + getYear() + ". Population: " + getPopulation() + ". Comments: " + getComments(); } /** * Indicates whether some other census object is "equal to" this one * @param obj the reference to the census object with which to compare * @return {@code true}, if this object is the same as the obj argument * {@code false} otherwise */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof Census c)) { return false; } return c.getYear() == getYear() && c.getPopulation() == getPopulation() && c.getComments().equals(getComments()); } /** * Returns a hash code value for the census object * @return a hash code value for this object */ @Override public int hashCode() { return (int) (year * population * getComments().hashCode()); } /** * Compares this object with the specified object for order. Returns a * negative integer, zero, or a positive integer as this object is less * than, equal to, or greater than the specified object * @param c the census object to be compared * @return the result of comparison */ @Override public int compareTo(Census c) { return Integer.compare(getPopulation(), c.getPopulation()); } /** * Checks whether the word can be found in the comment text * @param word a word that should be found in a comment * @return {@code true}, if the word is contained in the comment text * {@code false} otherwise */ public boolean containsWord(String word) { StringTokenizer st = new StringTokenizer(getComments()); String s; while (st.hasMoreTokens()) { s = st.nextToken(); if (s.equalsIgnoreCase(word)) { return true; } } Object dd; return false; } /** * Checks whether the substring can be found in the comment text * @param substring a substring that should be found in a comment * @return {@code true}, if the substring is contained in the comment text * {@code false} otherwise */ public boolean containsSubstring(String substring) { return getComments().toUpperCase().indexOf(substring.toUpperCase()) >= 0; } /** * Display the test results whether the word or substring * can be found in the comment text * @param word the word or substring for which the verification is performed */ private void testWord(String word) { if (containsWord(word)) { System.out.println("Comment contains the word \"" + word + "\""); } else { System.out.println("Comment does not contain the word \"" + word + "\""); } if (containsSubstring(word)) { System.out.println("Comment contains the text \"" + word + "\""); } else { System.out.println("Comment does not contain the text \"" + word + "\""); } } /** * Tests checking of the presence of a word or substring * in a comment to the census */ protected void testCensus() { setYear(2001); setPopulation(48475100); setComments("The first census in the independent Ukraine"); System.out.println(this); testWord("Ukraine"); testWord("Kraine"); testWord("Ukraina"); } /** * program shows results of testing presence of some word * or substring in the comment text * @param args command line arguments (not used) */ public static void main(String[] args) { new Census().testCensus(); } }
As can be seen from the given code, the Census
class implements the
Comparable<Census>
interface.
The implementation of this interface requires the addition of a compareTo()
method, which
defines a "natural" comparison
by population.
The AbstractCountry
class also contains equals()
, hashCode()
and
toString()
methods.
We also implement an auxiliary static addToArray()
function, which adds a new item
to the array. We implement all functions that not depend on the internal representation of the sequence
of censuses.
The AbstractCountry
class code will as
follows:
package ua.inf.iwanoff.java.third; import java.util.Arrays; /** * Abstract class for presenting the country in which the censuses are carried out. * Country is described by the name, area and sequence of censuses. * Access to the sequence of censuses is represented by abstract methods */ public abstract class AbstractCountry { private String name; private double area; /** * Returns country name * @return country name */ public String getName() { return name; } /** * Sets country name * @param name country name */ public void setName(String name) { this.name = name; } /** * Returns the ara of the country * @return the ara of the country in the form of a floating point value */ public double getArea() { return area; } /** * Sets the ara of the country * @param area the ara of the country in the form of a floating point value */ public void setArea(double area) { this.area = area; } /** * Returns reference to the census by index in a sequence * * <p> A subclass must provide an implementation of this method * * @param i census index * @return reference to the census with given index */ public abstract Census getCensus(int i); /** * Sets a reference to a new census within the sequence * according to the specified index. * * <p> A subclass must provide an implementation of this method * * @param i census index * @param census a reference to a new census */ public abstract void setCensus(int i, Census census); /** * Adds a reference to a new census to the end of the sequence * * <p> A subclass must provide an implementation of this method * * @param census a reference to a new census * @return {@code true} if the reference has been added * {@code false} otherwise */ public abstract boolean addCensus(Census census); /** * Creates a new census and adds a reference to it at the end of the sequence * * <p> A subclass must provide an implementation of this method * * @param year year of census * @param population population in the specified year * @param comments the text of the comment * @return {@code true} if the reference has been added * {@code false} otherwise */ public abstract boolean addCensus(int year, int population, String comments); /** * Returns the number of censuses in the sequence * * <p> A subclass must provide an implementation of this method * * @return count of censuses */ public abstract int censusesCount(); /** * Removes all of the censuses from the sequence * * <p> A subclass must provide an implementation of this method */ public abstract void clearCensuses(); /** * Sorts the sequence of censuses by population * * <p> A subclass must provide an implementation of this method */ public abstract void sortByPopulation(); /** * Sorts the sequence of censuses in the alphabetic order of comments * * <p> A subclass must provide an implementation of this method */ public abstract void sortByComments(); /** * Puts data from an array of censuses into a sequence * * <p> A subclass must provide an implementation of this method * * @param censuses array of references to censuses */ public abstract void setCensuses(Census[] censuses); /** * Returns an array of censuses obtained from the sequence * * <p> A subclass must provide an implementation of this method * * @return array of references to censuses */ public abstract Census[] getCensuses(); /** * Static method of adding a reference to census * to an array of censuses obtained as parameter * @param arr the array to which the census is added * @param item reference that is added * @return an updated array of censuses */ public static Census[] addToArray(Census[] arr, Census item) { Census[] newArr; if (arr != null) { newArr = new Census[arr.length + 1]; System.arraycopy(arr, 0, newArr, 0, arr.length); } else { newArr = new Census[1]; } newArr[newArr.length - 1] = item; return newArr; } /** * Checks whether this country is equivalent to another * @param obj country, equivalence with which we check * @return {@code true}, if two countries are the same * * {@code false} otherwise */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof AbstractCountry c)) { return false; } if (!getName().equals(c.getName()) || getArea() != c.getArea()) { return false; } return Arrays.equals(getCensuses(), c.getCensuses()); } /** * Returns a hash code value for the country * @return a hash code value */ @Override public int hashCode() { return (int) (getName().hashCode() * area * Arrays.hashCode(getCensuses())); } /** * Returns a string representation of the country * * @return a string representation of the country */ @Override public String toString() { StringBuilder result = new StringBuilder(getName() + ". Area: " + getArea() + " sq.km."); for (int i = 0; i < censusesCount(); i++) { result.append("\n").append(getCensus(i)); } return result + ""; } /** * Returns the population density for the specified year * @param year specified year (e.g. 1959, 1979, 1989, etc.) * @return population density for the specified year */ public double density(int year) { for (int i = 0; i < censusesCount(); i++) { if (year == getCensus(i).getYear()) { return getCensus(i).getPopulation() / getArea(); } } return 0; } /** * Finds and returns a year with the maximum population * @return year with the maximum population */ public int maxYear() { Census census = getCensus(0); for (int i = 1; i < censusesCount(); i++) { if (census.getPopulation() < getCensus(i).getPopulation()) { census = getCensus(i); } } return census.getYear(); } /** * Creates and returns an array of censuses with the specified word in the comments * @param word a word that is found * @return array of censuses with the specified word in the comments */ public Census[] findWord(String word) { Census[] result = null; for (Census census : getCensuses()) { if (census.containsWord(word)) { result = addToArray(result, census); } } return result; } /** * Displays census data that contains a certain word in comments * @param word a word that is found */ private void printWord(String word) { Census[] result = findWord(word); if (result == null) { System.out.println("The word \"" + word + "\" is not present in the comments."); } else { System.out.println("The word \"" + word + "\" is present in the comments:"); for (AbstractCensus census : result) { System.out.println(census); } } } /** * Auxiliary function of creating a new country object * @return reference to the new country object */ public AbstractCountry createCountry() { setName("Ukraine"); setArea(603628); // Adding censuses with the output of the result (false / true): System.out.println(addCensus(1959, 41869000, "The first postwar census")); System.out.println(addCensus(1970, 47126500, "Population increases")); System.out.println(addCensus(1979, 49754600, "No comments")); System.out.println(addCensus(1989, 51706700, "The last soviet census")); System.out.println(addCensus(2001, 48475100, "The first census in the independent Ukraine")); System.out.println(addCensus(2001, 48475100, "The first census in the independent Ukraine")); return this; } /** * Performs testing class methods */ public void testCountry() { System.out.println("Population density in 1979: " + density(1979)); System.out.println("The year with the maximum population: " + maxYear() + "\n"); printWord("census"); printWord("second"); sortByPopulation(); System.out.println("\nSorting by population:"); System.out.println(this); sortByComments(); System.out.println("\nSorting by comments:"); System.out.println(this); } }
In the derived class, we use an array to represent the sequence of censuses. To compare the censuses, the
reference
to the java.util.Comparator.comparing()
method is used, which provides the required values
by comparison. CountryWithArray
class
code will be the following:
package ua.inf.iwanoff.java.third; import java.util.Arrays; import java.util.Comparator; /** * Class for presenting the country in which the censuses are carried out. * Census data are represented with an array */ public class CountryWithArray extends AbstractCountry { private Census[] censuses; /** * Returns reference to the census by index in a sequence * @param i census index * @return reference to the census with given index */ @Override public Census getCensus(int i) { return censuses[i]; } /** * Sets a reference to a new census within the sequence * according to the specified index. * @param i census index * @param census a reference to a new census */ @Override public void setCensus(int i, Census census) { censuses[i] = census; } /** * Adds a reference to a new census to the end of the sequence * @param census a reference to a new census * @return {@code true} if the reference has been added * {@code false} otherwise */ @Override public boolean addCensus(Census census) { if (getCensuses() != null) { for (Census c : getCensuses()) { if (c.equals(census)) { return false; } } } setCensuses(addToArray(getCensuses(), census)); return true; } /** * Creates a new census and adds a reference to it at the end of the sequence * @param year year of census * @param population population in the specified year * @param comments the text of the comment * @return {@code true} if the reference has been added * {@code false} otherwise */ @Override public boolean addCensus(int year, int population, String comments) { Census census = new Census(year, population, comments); return addCensus(census); } /** * Returns the number of censuses in the sequence * @return count of censuses */ @Override public int censusesCount() { return censuses.length; } /** * Removes all of the censuses from the sequence */ @Override public void clearCensuses() { censuses = null; } /** * Returns an array of censuses obtained from the sequence * @return array of references to censuses */ @Override public Census[] getCensuses() { return censuses; } /** * Puts data from an array of censuses into a sequence * @param censuses array of references to censuses */ @Override public void setCensuses(Census[] censuses) { this.censuses = censuses; } /** * Sorts the sequence of censuses by population */ @Override public void sortByPopulation() { Arrays.sort(censuses); } /** * Sorts the sequence of censuses in the alphabetic order of comments */ @Override public void sortByComments() { Arrays.sort(censuses, Comparator.comparing(Census::getComments)); } /** * Demonstration of functions' work * @param args command line arguments (not used) */ public static void main(String[] args) { new CountryWithArray().createCountry().testCountry(); } }
Before main program output, we'll get true
(five times) and
false
(one time) as a result of addCensus()
function.
4 Exercises
- 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 elements 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 a local class static?
- 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? - How to compare array items?