Laboratory Training 1
Inheritance and Polymorphism
1 Training Tasks
1.1 Individual Task
Create a hierarchy of classes that represent main entities of the Laboratory training #5 of the course "Programming Basics p. 2" of the previous semester. The base abstract class that represents the second entity of an individual task (conventionally SecondAbstractEntity
) should not contain data, only abstract access methods, overridden toString()
and equals()
functions, as well as the implementation of the functions defined in the previous assignments. 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 FirstAbstractEntity
) should contain:
- abstract access functions (getters and setters);
- abstract functions 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; - 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), instead of directly to display these results.
Derived classes from the created abstract classes (conventionally FirstEntityWithArray
and SecondEntityWithData
) 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 of 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.
Testing of the program should include the previously implemented task, as well as sorting according to specific criteria.
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 |
Note: you should create classes with meaningful names that reflect the physical nature of the individual task, not FirstEntity
, SecondEntity
, etc.
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 IntelliJ IDEA Integrated Development Environment
2.1.1 Installing the IntelliJ IDEA IDE and Creating the First Project
The IntelliJ IDEA integrated development environment is an application written entirely in Java. To run the IDE, Java should be pre-installed on your computer. To compile Java classes, you should install a JDK.
There are two IDE variants that support different subsets of Java technologies. The free version (Community Edition) provides a complete set of development tools for the Java SE platform in Java, Kotlin, Groovy and Scala, project management tools, working with repository, testing and debugging. Ultimate Edition supports a full range of technologies, including Java EE, Spring Framework, special database tools, support for JavaScript and TypeScript development, and more.
The IntelliJ IDEA software installer can be downloaded from the JetBrains download page. Choose the Community Edition option. After downloading the installer, you need to run it. Clicking the Next button, go through the pages of the installation wizard..
- On the Choose Install Location page, select the folder to install (you can leave it unchanged).
- On the Installation Options page, leave other options unchanged.
- On the Choose Start Menu Folder page, select a group from the Start menu and click Install.
- After installing IntelliJ IDEA on the last page of the wizard, you can immediately select the option Run IntelliJ IDEA Community Edition and click Finish.
After the first launch of the environment, you should accept the terms of the IntelliJ IDEA User Agreement. Then you are offered a window in which you can choose to import the settings of the previous version. If you are installing IntelliJ IDEA for the first time, you do not need to import any options.
After the first launch of the environment, you should you are offered a window in which you can choose to import the settings of the previous version. If you are installing IntelliJ IDEA for the first time, you do not need to import any options.
In the right part of the Welcome to IntelliJ IDEA window you can find four menu items: Projects, Customize, Plugins and Learn IntelliJ IDEA. In particular, the position allows you to make general settings for environment styles. There are four color themes (Color theme) – Dracula, IntelliJ Light, Windows 10 Light, and High contrast. The theme chosen at will. You can also change the font size.
In the Welcome to IntelliJ IDEA window, the Projects position is selected by default. It is advisable to start from this position at the first start. Choose the creation of a new project (New Project button). On the first page of the wizard for a new Java project (Java position in the left menu), select Project SDK. IntelliJ IDEA is trying to find the installed JDK. If this fails, you can select the Add JDK... function and specify the path to the previously installed JDK. On the next page of the New Project wizard, select the Create project from template option, and then select the Command Line App template (console application). On the next page, enter the project name, the project location folder, and the base package name (Base package). As the name of the base package, it is best to choose a reverse domain name (the author uses ua.inf.iwanoff
). Then click Finish. The main IDE window opens with two tabs in the editing area. The What's New in IntelliJ IDEA tab can be closed after reading. On the Main.java tab you can see the following code:
package ua.inf.iwanoff; public class Main { public static void main(String[] args) { // write your code here } }
In the body of the main()
function, you can place the required program code instead of a comment.
To run the program for execution, you can use the Run | Run 'Main' main menu function, the green arrow in the toolbar or the Shift+F10 keyboard shortcut. If the compilation is successful, the program will execute. At the bottom of the main window there is a special area that simulates the work in the command window.
2.1.2 Elements of the IDE IntelliJ IDEA Graphical User Interface. Code Templates and Hotkeys
The main IDE window includes both traditional for all modern integrated environments UI elements (menu, toolbar, main editor window, status bar) and IntelliJ IDEA-specific set of tool windows. Tool windows are associated with call buttons located around the perimeter of the workspace, with a icon, signature and numerical designation (last not necessarily). If you click on these buttons, windows with some auxiliary functionality will open next to them.
There is no save function in the standard main menu. Autosave occurs at program run or exit.
The submenus of the main menu (Navigate, Code, Analyze, and Refactor) offer powerful code management tools. Some functions for working with the code and the corresponding keyboard shortcuts are given in a table:
Menu Function | Description | Keyboard shortcut |
---|---|---|
Navigate | Back | Go to the previous view or editing place | Ctrl+Alt+Left |
Navigate | Forward | Go to the next view or editing place | Ctrl+Alt+Right |
Navigate | Previous Highlichted Error | Go to the previous error found in the code | Shift+F2 |
Navigate | Next Highlichted Error | Go to the next error found in the code | F2 |
Code | Override methods | Override the base class method | Ctrl+O |
Code | Implement methods | Implement an abstract class or interface method | Ctrl+I |
Code | Generate... | Generate code snippet (constructor, getters and setters, overridden methods, etc.) | Alt+Insert |
The IntelliJ IDEA editor supports a large number of keyboard shortcuts for editing. In addition to standard buffer operation, it allows you to delete a line (Ctrl-Y), to duplicate a line or block (Ctrl-D), go to breakpoints (Ctrl+breakpoint_number), comment and uncommend a block (Ctrl+/), and so on.
The Ctrl-space shortcut allows you to get a list of possible object items, method parameters, and so on. The Ctrl-Shift-space
key shortcut filters the list, leaving only the variants of the expected type.
Using Ctrl+Alt+L, you can format the code.
A useful Alt-Enter keyboard shortcut allows you to get a set of options to fix an error that has occurred (for example, add an import directive, generate an empty method, etc.).
A list of all keyboard shortcuts can be found in the Settings window, then Keymap (File | Settings...) .
The line immediately after the menu bar shows the path to the file we are editing (inside the project), as well as the most commonly used functions (selecting a program to run, start and debug buttons, getting the project structure, and search).
The IntelliJ IDEA environment makes it much easier to type source code using Live Templates. You can call up the corresponding function via the main menu (Code | Insert Live Template...) or by pressing Ctrl-J. A list of code templates appears. The list depends on the context (location of the cursor in the editor window). The most useful code templates are given in a table:
Sequence of Characters | Program Code |
---|---|
psvm |
public static void main(String[] args) |
St |
String |
psf |
public static final |
fori |
for (int i = 0; i < ; i++) |
itar |
for (int i = 0; i < args.length; i++) { String arg = args[i]; } |
ifn |
if ( == null) { } |
iter |
for (Object o : ) { } |
sout |
System.out.println(); |
Templates can also simply be typed in code with the appropriate sequence of letters. After the prompt window appears, confirm the entry and get the corresponding text in the editor window.
2.1.3 Using Debugger
The debugger program can be started using the Run | Debug 'Main' main menu function, using button with the green bug in the toolbar or using the keyboard shortcut Shift+F9. In the simplest case, to debug the program, simply specify the breakpoint (click on the vertical gray bar to the left of the desired line) and start the debug process. The execution of the program will stop at the selected line, after which the intermediate values of the variables can be seen in the Variables output area or simply by placing the mouse cursor over the variable in the program code. In the Run | Debugging Actions submenu you can choose various versions of the stepwise program running:
- Step into (F7) – stepwise execution of called functions
- Step over (F8) – stepwise execution without entering called functions.
2.1.4 Project Structure
A separate folder is created for each project (with the name of the project, e. g. Project
). The project root folder files contain information about the project as a whole and about the modules (the project may include several separate modules).
The src
folder contains source code packages of the project. The out
folder contains the result of compiling the source code. Inside the out
folder is the production
subdirectory with Project
subfolder in it, then the folder structure will repeat the similar folder structure of the src
subdirectory.
Additional project options related to compilation, encoding, etc. are allocated in the hidden .idea
subfolder.
2.2 Using the GitHub Service repository
During the development of large projects, there is a need for additional means of control over different versions of artifacts, in particular, the source code. You must be able to access previous versions of documents to trace changes. To solve these problems, as well as to provide collective access to the project, use version control systems – special software for managing changing documents and providing access to these documents. The daily cycle of interaction with the version control system includes updating the working copy, modifying the project and fixing changes. While working on the project, you can create several branches (forks) for different solutions, and then merge the versions.
Version control systems can be centralized and distributed. In centralized systems, version storage is performed on a special server. An example of a centralized system is Subversion (SVN). Distributed systems have a local copy of the repository and ensure that the data is reconciled with the repository on the remote computer. Git is an open version control system. For open source projects, using Git is free.
GitHub is a social repository for open source projects that use Git to control source versions. To create repositories, register at https://github.com.
Both implementations of IntelliJ IDEA support integrated work with version control systems (VCS submenu of the main menu). To work with GitHub in IntelliJ IDEA, you must first install the Git system. You can download the required software at https://git-scm.com/downloads for your operating system. You can retain unchanged selected options on the installation wizard pages.
After installing the necessary software, you need to make some settings. To do this, run the program Git Bash
, in the command line set the name and address of the user, specified earlier during registration on GitHub:
git config --global user.name "user_name" git config --global user.email user_address@mail
A .gitconfig
file is created containing the appropriate settings.
In the IntelliJ IDEA environment you should configure Git (File | Settings..., then Version Control | Git). You should specify the path to the git.exe
file, for example, C:\Program Files\Git\bin\git.exe
. In the GitHub settings (Version Control | GitHub) add an account with the + button and carry out an authorization.
In order to add a previously created project in IntelliJ IDEA, you should first allow use of VCS: VCS | Enable Version Control Integration and select Git in the list of proposed VCS. Now in the main menu, instead of the VCS submenu will appear Git. A similar result can be obtained if you use the menu function VCS | Create Git Repository... and choose a project that interests you.
Note. GIT system can be installed from IntelliJ IDEA, if in the main menu select VCS | Enable Version Control Integration, then select git in the list of different VCS. In the lower right corner of the window there is a popup menu, which will be prompted to download Git.
You can now copy the project to GitHub using the Git | GitHub | Share Project on GitHub menu function.
If you have a project that was previously added to Git and want to add new files, such as classes, IntelliJ IDEA offers to add these files to a repository through the Add File to Git dialog box.
If you have previously added to GIT, add new files, such as classes, IntelliJ IDEA offers to add these files to a repository through the Add File to Git dialog box.
After making changes to the code, you should update the project in the local repository using the menu function Git | Commit.... You can individually update files through the context menu (Git | Commit File...) . The project update should be carried out after making changes related to the execution of a certain code of code modification. The meaning of this problem should be described in the Commit Message of Commit Changes dialog box.
After updating the project in the local repository using Commit function, the changes made can also be transferred to GitHub repository using the Git | Push... menu function (or similar to the function of the context menu).
At any other time, after closing all projects, you can use the Get from VCS function on the IntelliJ IDEA start window. Next, select GitHub, specify the folder for placing the project, and then confirm its opening.
2.3 Class Composition
Class composition assumes creation of classes, which contain objects of other classes as class fields. In Java, you cannot place objects into another object. Only references are allowed here. You can create objects direct after the declaration or in constructors.
class X { } class Y { } class Z { X x = new X(); Y y; Z() { y = new Y(); } }
You can also create an inner object just before its first use.
The relationship that is modeled by composition is often called the "has-a" relation.
Aggregation is a kind of composition that provides that the entity (instance) is contained in another entity or cannot be created and cannot exist without the entity that covers it. At the same time, the outer entity can exist without inner, that is, the lifetime of the external and internal entities may not be the same. A more strict form of the composition (the composition itself) provides that the lifetime of the outer and inner essences coincides. At the Java level, aggregation allows for the creation of an internal object before using it, whereas a strict composition involves creating an internal object in the body of the class, in the initialization block, or in the constructor.
2.4 Inheritance
The inheritance mechanism assumes creation of derived classes from base classes. Derived class has direct access to all public and protected elements 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;
2.5 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.6 Polymorphism
2.6.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.
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.6.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.
2.7 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 whether 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.8 Nested Classes
2.8.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.8.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.8.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.8.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.9 Default Implementation of Interface Methods
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.oop.first; 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.oop.first; public class MyGreetings implements Greetings { }
During testing we receive a default greeting.
package ua.inf.iwanoff.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; 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.10 Working with Functional Interfaces in Java 8
2.10.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.oop.first; 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.oop.first; public class PrintWithLambda { public static void main(String[] args) { FunctionToPrint.printTable(-2.0, 2.0, 0.5, x -> x * x * x); } }
2.10.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::nonSstaticMethodName |
s::toString |
Reference to a non-static method for parameter | Class::nonSstaticMethodName |
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.10.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 multithreaded programming), and many more also are functional interfaces.
2.10.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.oop.first; 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.11 Object Cloning and Equivalence Checking
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 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.oop.first; 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.oop.first; 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.oop.first; 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); } }
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.oop.first; 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);
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.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; 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:
+
12 Now the class that implements our interface can be as follows:
package ua.inf.iwanoff.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; public interface LeftSide { double f(double x); }
The Solver
class provides a static method that solves an equation:
package ua.inf.iwanoff.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; 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.oop.first; 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
Suppose it is necessary to design a hierarchy of classes in which the classes for the representation of the country and the census of the population are described. Given the different options for storing data on countries and censuses, it is advisable to create a hierarchy of classes. Basic abstract classes that represent country and census should not contain any data.
The AbstractCensus
class will declare abstract access methods. In this class, we can implement the functions of obtaining a string representation, checking the equivalence, 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. The derived class that represents the census must contain data fields.
In the AbstractCountry
class, which also does not contain data, abstract access and sorting methods should be declared. It is necessary to implement functions of getting string representation, 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 project and add a new package ua.inf.iwanoff.oop.first
. We also create an AbstractCensus
abstract class. Its code (partially borrowed from the previously created Census
class of the example of Laboratory training # 5 of the course "Fundamentals of programming" of the previous semester) will be as follows:
package ua.inf.iwanoff.oop.first; import java.util.StringTokenizer; public abstract class AbstractCensus implements Comparable<AbstractCensus> { @Override public String toString() { return "The census in " + getYear() + ". Population: " + getPopulation() + ". Comments: " + getComments(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || !(obj instanceof AbstractCensus)) { return false; } AbstractCensus c = (AbstractCensus) obj; return c.getYear() == getYear() && c.getPopulation() == getPopulation() && c.getComments().equals(getComments()); } @Override public int compareTo(AbstractCensus c) { return Integer.compare(getPopulation(), c.getPopulation()); } public abstract String getComments(); public abstract void setComments(String comments); public abstract int getPopulation(); public abstract void setPopulation(int population); public abstract int getYear(); public abstract void setYear(int year); public boolean containsWord(String word) { StringTokenizer st = new StringTokenizer(getComments()); String s; while (st.hasMoreTokens()) { s = st.nextToken(); if (s.equalsIgnoreCase(word)) { return true; } } return false; } public boolean containsSubstring(String substring) { return getComments().toUpperCase().indexOf(substring.toUpperCase()) >= 0; } 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 + "\""); } } 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"); } }
As can be seen from the given code, the AbstractCensus
class implements the Comparable<AbstractCensus>
interface. The implementation of this interface requires the addition of a compareTo()
method, which defines a "natural" comparison by population.
The derived class CensusWithData
contains a definition of data fields and an implementation of access methods, as well as the main()
method:
package ua.inf.iwanoff.oop.first; public class CensusWithData extends AbstractCensus { private int year; private int population; private String comments; public CensusWithData() { } public CensusWithData(int year, int population, String comments) { this.year = year; this.population = population; this.comments = comments; } @Override public int getYear() { return year; } @Override public void setYear(int year) { this.year = year; } @Override public int getPopulation() { return population; } @Override public void setPopulation(int population) { this.population = population; } @Override public String getComments() { return comments; } @Override public void setComments(String comments) { this.comments = comments; } public static void main(String[] args) { new CensusWithData().testCensus(); } }
The AbstractCountry
class also contains the equals()
and toString()
methods. In the class, we have a helper static addToArray()
function that adds a new item to the array. We can implement all functions that are independent of the internal representation of data. The AbstractCountry
class code will as follows:
package ua.inf.iwanoff.oop.first; import java.util.Arrays; public abstract class AbstractCountry { public abstract String getName(); public abstract void setName(String name); public abstract double getArea(); public abstract void setArea(double area); public abstract AbstractCensus getCensus(int i); public abstract void setCensus(int i, AbstractCensus census); public abstract boolean addCensus(AbstractCensus census); public abstract boolean addCensus(int year, int population, String comments); public abstract int censusesCount(); public abstract void clearCensuses(); public abstract void sortByPopulation(); public abstract void sortByComments(); public abstract void setCensuses(AbstractCensus[] censuses); public abstract AbstractCensus[] getCensuses(); public static AbstractCensus[] addToArray(AbstractCensus[] arr, AbstractCensus item) { AbstractCensus[] newArr; if (arr != null) { newArr = new AbstractCensus[arr.length + 1]; System.arraycopy(arr, 0, newArr, 0, arr.length); } else { newArr = new AbstractCensus[1]; } newArr[newArr.length - 1] = item; return newArr; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || !(obj instanceof AbstractCountry)) { return false; } AbstractCountry c = (AbstractCountry) obj; if (!getName().equals(c.getName()) || getArea() != c.getArea()) { return false; } return Arrays.equals(getCensuses(), c.getCensuses()); } @Override public String toString() { String result = getName() + ". Area: " + getArea() + " sq.km."; for (int i = 0; i < censusesCount(); i++) { result += "\n" + getCensus(i); } return result; } public double density(int year) { for (int i = 0; i < censusesCount(); i++) { if (year == getCensus(i).getYear()) { return getCensus(i).getPopulation() / getArea(); } } return 0; } public int maxYear() { AbstractCensus census = getCensus(0); for (int i = 1; i < censusesCount(); i++) { if (census.getPopulation() < getCensus(i).getPopulation()) { census = getCensus(i); } } return census.getYear(); } public AbstractCensus[] findWord(String word) { AbstractCensus[] result = null; for (AbstractCensus census : getCensuses()) { if (census.containsWord(word)) { result = addToArray(result, census); } } return result; } private void printWord(String word) { AbstractCensus[] 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); } } } 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; } 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); } }
To provide comparison of censuses during sorting, we create a separate class CompareByComments
:
package ua.inf.iwanoff.oop.first; import java.util.Comparator; public class CompareByComments implements Comparator<AbstractCensus> { public int compare(AbstractCensus c1, AbstractCensus c2) { return c1.getComments().compareTo(c2.getComments()); } }
In the derived class, we use an array to represent the sequence of censuses. CountryWithArray
class code will be the following:
package ua.inf.iwanoff.oop.first; import java.util.Arrays; public class CountryWithArray extends AbstractCountry { private String name; private double area; private AbstractCensus[] censuses; @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @Override public double getArea() { return area; } @Override public void setArea(double area) { this.area = area; } @Override public AbstractCensus getCensus(int i) { return censuses[i]; } @Override public void setCensus(int i, AbstractCensus census) { censuses[i] = census; } @Override public boolean addCensus(AbstractCensus census) { if (getCensuses() != null) { for (AbstractCensus c : getCensuses()) { if (c.equals(census)) { return false; } } } setCensuses(addToArray(getCensuses(), census)); return true; } @Override public boolean addCensus(int year, int population, String comments) { AbstractCensus census = new CensusWithData(year, population, comments); return addCensus(census); } @Override public int censusesCount() { return censuses.length; } @Override public void clearCensuses() { censuses = null; } @Override public AbstractCensus[] getCensuses() { return censuses; } @Override public void setCensuses(AbstractCensus[] censuses) { this.censuses = censuses; } @Override public void sortByPopulation() { Arrays.sort(censuses); } @Override public void sortByComments() { Arrays.sort(censuses, new CompareByComments()); } 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
- In what cases is it advisable to use class composition?
- How can you place whole Java object into another object?
- 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? - How to compare array items?