Laboratory Training 5
Metaprogramming. Parallel and Declarative Programming
1 Training Tasks
1.1 Individual Task
Create a graphical user interface application that is designed for plotting arbitrary functions. The user enters the real values of a and b, the functions f(x) and g(x) in the form of strings corresponding to the Java syntax. The program calculates the function h(x) according to the individual task:
Numbers of Variants |
Function h(x) |
Numbers of Variants |
Function h(x) |
||
---|---|---|---|---|---|
1 |
13 |
a∙f(x) – b∙g(x) | 7 |
19 |
f(a + x) + b∙g(x) |
2 |
14 |
f(x + a) + g(x – b) | 8 |
20 |
f(a / x) – g(b∙x) |
3 |
15 |
(a-f(x))(b + g(x)) | 9 |
21 |
f(x – a) ∙g(x + b) |
4 |
16 |
f(a∙x) – g(b∙x) | 10 |
22 |
f(a / x) + g(b / x) |
5 |
17 |
f(x / a) ∙g(x + b) | 11 |
23 |
a∙f(x) + b∙g(x) |
6 |
18 |
f(a / x) – g(b / x) | 12 |
24 |
a∙f(x) – g(b∙x) |
After entering the necessary functions, the range of the graph display and pressing the corresponding button, the chart is plotted. You should also consider the function of clearing input lines and graphics.
To implement the processing of entered expressions, you must apply a dynamic code compilation. You should use JavaFX tools to create graphical user interface application. The recommended approach is to use the LineChart
component.
1.2 View All Class Fields
Create a console application in which the user enters the class name and receives information about all the fields (including private and protected).
1.3 Creating a GUI Application for Getting Prime Factors of Numbers
Implement JavaFX GUI application, in which the user inputs a range of numbers (from and to), and in the window the numbers and their prime factors are displayed. Realize the ability to pause, resume the stream, and also complete termination and re-calculation with new data.
1.4 Working with BlockingQueue
Create a console program in which one thread adds integers to the BlockingQueue
queue, and the other calculates their arithmetic mean.
1.5 Calling Function for the Selected Class (Advanced Task)
Create classes with methods whose names match. Select particular class by name and call its method.
1.6 Interpretation of Mathematical Expressions (Advanced Task)
Create a console application that allows you to enter mathematical expressions, calculate and output the result. An expression can consist of constants, mathematical operations, and brackets. To implement the program, use the tools of the javax.script
package.
Note. The syntax of mathematical expressions of JavaScript is similar to Java. The result can be output using the print()
function without creating additional variables.
1.7 Calculating π in a Separate Thread (Additional Task)
Implement the program of calculation π with accuracy up to a given ε as the sum of a sequence:
The calculation should be done in a separate thread. When performing the calculation, you should give the user the opportunity to enter a request for the number of calculated elements of the sum.
2 Instructions
2.1 Using RTTI
The Runtime Type Identification mechanism (RTTI) allows you to determine the type of object at runtime. Most object-oriented programming languages support the RTTI mechanism. This mechanism, as a rule, applies to so-called polymorphic types (classes or structures that provide virtual functions). RTTI allows to determine the exact type of object by reference (or by pointer for languages that support pointers). Due to the extension of type compatibility rules for types, a reference to a base type can actually point to objects of derived types, which is the basis of polymorphism. In some cases, you must explicitly get the actual object type, or check whether the type of object is the one that interests you.
In Java, all reference types are polymorphic. RTTI is implemented through information about the type that is stored in each object. The instanceof
keyword allows you to check whether an object is an instance of a particular type. The expression
object instanceof class
returns the value of the boolean
type that can be used before calling class-specific methods:
if(x instanceof SomeClass) ((SomeClass)x).someMethod();
Without this check, the type cast leads to throwing ClassCastException
.
In the following example, depending on the number entered by the user, objects of different types are created:
package ua.inf.iwanoff.oop.fifth; import java.util.Scanner; class First { void first() { System.out.println("First"); } } class Second { void second() { System.out.println("Second"); } } public class InstanceOf { public static void main(String[] args) { try { Object o; int n = new Scanner(System.in).nextInt(); switch (n) { case 1: o = new First(); break; case 2: o = new Second(); break; default: return; } if (o instanceof First) { ((First) o).first(); } if (o instanceof Second) { ((Second) o).second(); } } catch (Exception e) { e.printStackTrace(); } } }
2.2 Using Class Loaders
One of the main features of the Java platform is a model of dynamic class loading. Class loaders (classloaders) are special objects that allow you to load class code while the program is running.
Dynamic class loading in Java has several features:
- delayed (lazy) loading: classes are loaded only if necessary, which saves resources and improve load distribution;
- code validation (type usage checking) is performed during class load, not during code execution;
- the ability to create custom loaders that fully control the process of obtaining the requested class;
- the ability to control the reliability and security of the code through the so-called security attributes.
Each loader has its own namespace associated with classes created by it. Classes created by two different loaders based on a common bytecode are treated as different.
When loading a class, an object lof type java.lang.Class
is created. The Class
type is a class whose instances are classes, interfaces, and other types when executing a Java application. Primitive Java types (boolean
, byte
, char
, short
, int
, long
, float
, double
) and void
are also represented as objects of Class
type. A Class
object can be obtained for existing types (including primitive ones) using the class
keyword:
Class<?> c1 = String.class; Class<?> c2 = double.class;
As can be seen from the previous example, the Class
class is generic. Most often it is used with parameter <?>
(particular type is determined automatically).
You can also get a Class
object for a previously created reference type object:
Integer i = new Integer(100); Class<?> c3 = i.getClass();
The bject of Class
type provides a variety of information about the type that can be used at runtime and is the basis of reflection. These features will be considered below.
Further, the term "load classes" will mean loading different types.
There are three types of class loaders in Java:
- Bootstrap class loader loads basic classes that are usually found in jar files in the
jre/lib
directory; bootstrap class loader is not available programmatically; - Extension class loader loads classes of different extensions packages that are usually contained in
jre/lib/ext
; - System class loader implements a standard class loading algorithm from the directories and JARs listed in the
CLASSPATH
variable.
An abstract class java.lang.ClassLoader
is used to create the types of objects responsible for loading classes. Each Class
object has a reference to ClassLoader
. For each class, the current loader is defined. In order to get the loader that was used for loading the SomeClass
class, you need to use the SomeClass.class.getClassLoader()
method. Java allows you to create custom class loaders.
When loading classes, a special hierarchy of loaders (bootstrap class loader / extension class loader / system class loader) is used. The loading algorithm is usually as follows:
- search for previously loaded classes (so that the class was not loaded twice);
- if the class was not loaded, its loading is delegated to the parent loader;
- If the parent loader could not load the requested class, the current loader tries to load the required class itself.
It should be noted that all "system" classes (java.lang.Object
, java.lang.String
, etc.) are loaded only by the base bootstrap loader. Therefore, getClassLoader()
returns null
for these classes.
2.3 Reflection
The previously discussed RTTI mechanism is based on information that can be obtained during compilation. Along with RTTI, Java provides a more complex and powerful mechanism, so-called reflection. This mechanism uses information that may not be available during compilation.
Reflection is a mechanism that allows the program to track and modify its own structure and behaviour at runtime.
Direct information about the type stored in an object of type java.lang.Class
. The java.lang.reflect
package contains classes that provide information about classes and objects and allow access to the members of classes and interfaces.
With capabilities of Class
type, you can create a Class
object with a specific name. To obtain a Class
object, a static method forName()
is used.
String name = "SomeClass"; Class c = Class.forName(name);
In the example below, the name of the required class is entered from the keyboard, and then the object is created using the newInstance()
function:
package ua.inf.iwanoff.oop.fifth; import java.util.Scanner; class One { void one() { System.out.println("one"); } } public class NewInstance { public static void main(String[] args) { try { String name = new Scanner(System.in).next(); Object o = Class.forName(name).newInstance(); if (o instanceof One) { ((One) o).one(); } } catch (Exception e) { e.printStackTrace(); } } }
The Class
object contains, in particular, the type name information that could be obtained in the example above:
System.out.println(o.getClass().getName());
Information about types can be presented in a convenient form using the functions getSimpleName()
or getCanonicalName()
.
A Class
object can also be obtained for interfaces and even for anonymous classes. The isInterface()
method returns true
for interfaces, and the isAnonymousClass()
method returns true
for anonymous classes. You can also get an array of nested types, and so on.
The getSuperclass()
method returns the base class. For the Object
class, this method returns null
. The getInterfaces()
method returns an array of implemented interfaces (also objects of Class
type).
You can get a list of class methods - an array of java.lang.reflect.Method
class objects. This array will contain references to public methods of this class and all its ancestors:
for (int i = 0; i < o.getClass().getMethods().length; i++) { System.out.println(o.getClass().getMethods()[i].getName()); }
The getMethod()
function allows you to get some method by its name.
The getDeclaredMethods()
function allows you to get a list of all methods (public, protected, package, and private), but only defined in this class (not in the base classes). Similarly, the getDeclaredMethod()
function allows you to get any method by name.
You can get information about the class in which method was declared ((getDeclaringClass()
), the method name (getName()
), as well as the result type (getReturnType()
). You can also get an array of parameter types (objects of type Class<?>
) using the getParameterTypes()
method. The array received using the getExceptionTypes()
function contains types of possible exceptions. The overridden toString()
function returns the full signature of the method with the list of exceptions.
Information about method modifiers can be obtained using getModifiers()
function. This function returns an integer that can be used, for example, for the charAt()
function of the String
class:
Class<?> c = String.class; String methodName = "charAt"; Method m = c.getDeclaredMethod(methodName, int.class); // a method with an integer parameter int mod = m.getDeclaringClass().getModifiers(); if (Modifier.isAbstract(mod)) { System.out.print("abstract "); } if (Modifier.isPublic(mod)) { System.out.print("public "); } if (Modifier.isPrivate(mod)) { System.out.print("private "); } if (Modifier.isProtected(mod)) { System.out.print("protected "); } if (Modifier.isFinal(mod)) { System.out.print("final "); }
Method can be invoked using the function invoke()
. The first parameter of this function is the reference to the class object for which the method is called. For static methods, this is null
. Then actual parameters are specified. Since several methods may differ only in the list of parameters, you must specify the types of parameters (an array of objects of type Class
). If the list of parameters is empty, this array can be omitted.
For example, you can call a static function without the parameters for the class specified by the name:
method = Class.forName(className).getMethod(methodName); method.invoke(null);
In the example below, a non-static method is called:
package ua.inf.iwanoff.oop.fifth; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; public class ListSize { public static void main(String[] args) throws Exception { List<String> list = Arrays.asList("one", "two"); Method method = Class.forName("java.util.List").getMethod("size"); System.out.println(method.invoke(list)); } }
Getting information about fields is carried out similarly. The java.lang.reflect.Field
class represents data about the field: name (getName()
), type (getType()
), the class in which the field is defined (getDeclaringClass()
), access modifiers (getModifiers()
), etc. To get the value stored in the field, you can apply the get()
method, which returns the result of the Object
type. Using the set()
method, you can set a new value.
If you want to access private or protected fields, but defined exactly in this class, getDeclaredField()
(by name) and getDeclaredFields()
methods (returns an array of fields) are used.
The reflection mechanism allows you to bypass the restrictions on access to the elements of the class. A private method obtained with getDeclaredMethod()
or as an array item returned by getDeclaredMethods()
cannot be directly called. However, to call it, you can allow access using the function setAccessible()
sending true
as actual argument. You can also get private fields. The following example shows the ability to work with private class members:
package ua.inf.iwanoff.oop.fifth; import java.lang.reflect.Field; import java.lang.reflect.Method; class TestClass { private int k = 0; private void testPrivate() { System.out.println("Private method. k = " + k); } } public class AccessTest { public static void main(String[] args) throws Exception { // Get a class: Class<?> c = Class.forName("ua.inf.iwanoff.oop.fifth.TestClass"); // Create an object: Object obj = c.newInstance(); // Access to the private field: Field f = c.getDeclaredField("k"); f.setAccessible(true); f.set(obj, 1); // Access to the private method: Method m = c.getDeclaredMethod("testPrivate"); m.setAccessible(true); m.invoke(obj); } }
The reflection mechanism should be used with caution, since it provides the ability to bypass encapsulation.
2.4 Using Metaprogramming
2.4.1 Overview. Scripting Tools
Metaprogramming is the process of creating programs that generate other programs. The most important element of metaprogramming is automatic code generation. Java offers two approaches: using scripting languages and using compilation "on the fly."
Many scripting languages and dynamically typed languages allow you to generate Java bytecodes, so that applications can run on a Java platform, just like Java programs. The javax.script
package, introduced in Java SE 6, allows you to interpret expressions written in scripting languages (AppleScript, Groovy, JavaScript, Jelly, PHP, Python, Ruby, etc.). For example, in this way you can interpret the code written in JavaScript:
package ua.inf.iwanoff.oop.fifth; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; public class EvalScript { public static void main(String[] args) throws Exception { ScriptEngineManager factory = new ScriptEngineManager(); ScriptEngine engine = factory.getEngineByName("JavaScript"); engine.eval("print('Hello, World')"); } }
As can be seen from the example, the ScriptEngineManager
class allows you to create a JavaScript engine, and an instance of the ScriptEngine
class interprets the expressions. An expression, or part of it, can be read from the input stream, for example, from the keyboard.
The ScriptEngine
class also allows you to create script language variables (put()
method), call its functions (the invokeFunction()
method), etc.
To transfer information from Java to the script, the binding interface Bindings
is used:
import javax.script.Bindings; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; public class BindingDemo { public static void main(String[] args) throws Exception { ScriptEngineManager factory = new ScriptEngineManager(); ScriptEngine engine = factory.getEngineByName("JavaScript"); engine.put("a", 1); engine.put("b", 5); Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); Object a = bindings.get("a"); Object b = bindings.get("b"); System.out.println("a = " + a); System.out.println("b = " + b); Object result = engine.eval("c = a + b;"); System.out.println("a + b = " + result); } }
2.4.2 Dynamic Code Generation
One of Java's capabilities is to call the Java compiler in the source code. For this purpose, package javax.tools
is used. The javax.tools.JavaCompiler
class compiles the specified source code into a .class
file. You can create an instance of JavaCompiler
using the factory method getSystemJavaCompiler()
of the javax.tools.ToolProvider
class:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
For example, the directory c:\java
contains a subdirectory (package) called test
, with the file Test.java
inside:
package test; public class Test { public static void main(String[] args) { System.out.println("Hello, Dynamic Compiling!"); } }
By calling the run()
function of the compiler object, you can compile the source code. The first three parameters of the run()
function are, respectively, the input, output, and error outputs. These streams are used in the compilation process. If the values of the parameters are set to null
, System.in
, System.out
and System.err
respectively will be used. The fourth and subsequent parameters will indicate arguments and compilation options. In the simplest case, you only need to specify the full name of the file with the source code. Example:
compiler.run(null, null, null, "c:/java/test/Test.java");
Now the compiled program can be loaded for execution. In order to load a class not specified in the CLASSPATH
variable, you should use a special class loader, the so-called java.net.URLClassLoader
. The constructor of this class requires as argument an array of URL
objects. The URL
class represents a Uniform Resource Locator, a pointer to a "resource" in the World Wide Web. A resource can be something simple, such as a file or directory, or it can be a reference to a more complex object, such as a query to a database or to a search engine. In our case, this array will consist of one element. You can get such an item from the java.net.URI
object (Uniform Resource Identifier), which, in turn, can be obtained from the java.io.File
object:
URLClassLoader classLoader = new URLClassLoader( new URL[] { new File("c:/java").toURI().toURL() });
Obtained classLoader
object can be used to load the class:
Class<?> cls = Class.forName("test.Test", true, classLoader);
Calling the main()
method:
Method m = cls.getMethod("main", String[].class); m.invoke(null, new Object[] { new String[] { } });
Note: as can be seen from the example, in order to pass the array parameter, it is necessary to make another array of Object
type from arrays.
In a more complex case, in addition to the JavaCompiler
class, you should involve other features of the javax.tools
package. In particular:
JavaCompiler.CompilationTask
– a class that represents a compilation task; compilation is carried out by invocation ofcall()
method;JavaFileManager
controls the reading and writing of files when compiling;JavaFileObject
– a file object that allows abstraction from Java source code and class files;DiagnosticListener
– listener for diagnostic compilation events.
If the source code is not taken from the file system, you need to create a class that implements the JavaFileObject
interface. Java provides a simple implementation of this interface (SimpleJavaFileObject
class). You can create a derived class and override methods as needed.
2.5 Working with Threads
2.5.1 Overview
A process is a copy of a computer program that is loaded in memory is executed. In modern operating systems, a process can be executed in parallel with other processes. A process has its own address space, and this space is physically unavailable for other processes.
A thread (execution thread) is a separate subtask that can be executed in parallel with other subtasks (threads) within a single process. Each process contains at least one thread called the main thread. All threads created by the process are executed in the address space of this process and have access to process resources. Creating threads is essentially less resource-intensive operation than creating new processes. Threads are sometimes referred to as lightweight processes.
If the process has created multiple threads, then all of them are executed in parallel, while the time of the central processor (or several central processors in multiprocessor systems) is distributed among these threads. Distribution of the CPU time is performed by a special module of the operating system, the so-called scheduler. The scheduler in turn handles the management of individual thread, so even in the one-processor system an illusion of parallel running of threads is created. Time distribution is performed by interruptions of the system timer.
Threads are used to implement independent subtasks within a single process in order to create background processes, simulate the parallel execution of certain actions, or enhance the user interface's convenience.
2.5.2 Low-Level Thread Tools
There are two ways to create a thread object using the Thread
class:
- creation of a new class derived from the
java.lang.Thread
class and creating an object of a new class; - creation of a class that implements the
java.lang.Runnable
interface; an object of this class is passed to thejava.lang.Thread
class constructor as an argument.
When creating a derived class from Thread
, it is necessary to override its run()
method. After creating the thread object, it should be started using the start()
method. This method performs some initialization and then calls run()
. The following example creates a separate execution thread that outputs the next integer from 1 to 40:
package ua.inf.iwanoff.oop.fifth; public class ThreadTest extends Thread { @Override public void run() { for (int counter = 1; counter <= 40; counter++) { try { Thread.sleep(1000); // Delay time in milliseconds } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } } public static void main(String[] args) { new ThreadTest().start(); // You can add actions that run in parallel with the run() method. } }
Calling the sleep()
method causes the thread to be suspended at the specified number of milliseconds. Calling the sleep() method requires catching of the InterruptedException
exception that is thrown if this thread is interrupted by another thread. A similar example is with the creation of a class object that implements the Runnable
interface. In this case, you also need to run the thread using the start()
method:
package ua.inf.iwanoff.oop.fifth; public class RunnableTest implements Runnable { @Override public void run() { for (int counter = 1; counter <= 40; counter++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } } public static void main(String[] args) { new Thread(new RunnableTest()).start(); // You can add actions that run in parallel with the run () method } }
The second approach is better, since in this case we are free to choose the base class.
You can assign names to individual threads (the second parameter of the constructor) and get these names using the getName()
function.
Any thread can be in several standard states. The thread receives the state "new" (Thread.State.NEW
) when the thread object is created. Calling the start()
method moves the thread to the state "runnable" (Thread.State.RUNNABLE
). There are states "blocked" (Thread.State.BLOCKED
), "blocked in time", or "in the standby mode" (Thread.State.TIMED_WAITING
), "waiting", or "not working" (Thread.State.WAITING
) and "complete" (Thread.State.TERMINATED
). When creating a thread, it gets a "new" state and is not executed. You can call the getState()
method to get the state of a thread. The following example shows some states of the thread within its life cycle:
package ua.inf.iwanoff.oop.fifth; public class StateTest { public static void main(String[] args) throws InterruptedException { Thread testThread = new Thread(); System.out.println(testThread.getState()); // NEW testThread.start(); System.out.println(testThread.getState()); // RUNNABLE testThread.interrupt(); // interrupt the thread Thread.sleep(100); // it takes time to complete the thread System.out.println(testThread.getState()); // TERMINATED } }
The wait(long timeout)
method, like sleep(long timeout)
method, allows you to pause the thread for a specified number of milliseconds. When executing this method, InterruptedException
can also be thrown. The use of these methods allows you to switch the thread to standby (TIMED_WAITING
). Unlike sleep()
, the ability to work after calling the wait()
method can be restored using notify()
or notifyAll()
methods.
The wait()
method can be called without parameters. The thread thus gets into the "disable" state (WAITING
).
A less reliable alternative way to stop a thread for a while is calling the yield()
method, which should stop the thread for a certain amount of time, so that other threads can perform their actions.
After the run()
method is completed, the thread finishes its work and passes to the passive state (TERMINATED
).
In earlier versions of Java, for the forced stop, as well and temporary suspension of the thread and subsequent resumption, the use class Thread
methods stop()
, suspend()
and resume()
was provided. The stop()
, suspend()
and resume()
methods are considered undesirable to use (deprecated methods), since they provoke the creation of unreliable and difficult to debug code. Furthermore, the use of suspend()
and resume()
can trigger deadlocks.
You can interrupt the thread that is in the standby state (Thread.State.TIMED_WAITING
) by calling the interrupt()
method. In this case, the InterruptedException
is thrown. We can modify the previous example so that it allows us to interrupt the thread:
package ua.inf.iwanoff.oop.fifth; import java.util.Scanner; public class InterruptTest implements Runnable { @Override public void run() { for (int counter = 1; counter <= 40; counter++) { try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("Thread interrupted."); break; } System.out.println(counter); } } @SuppressWarnings("resource") public static void main(String[] args) { Thread thread = new Thread(new InterruptTest()); thread.start(); System.out.println("Press Enter to interrupt"); new Scanner(System.in).nextLine(); thread.interrupt(); } }
The join()
method allows one thread to wait for the completion of another. Calling this method inside thread t1
for thread t2
will stop the current thread (t1
) until the completion of t2
, as shown in the following example:
package ua.inf.iwanoff.oop.fifth; public class JoinTest { static Thread t1, t2; static class FirstThread implements Runnable { @Override public void run() { try { System.out.println("The first thread started."); Thread.sleep(1000); System.out.println("The primary work of the first thread has been completed."); t2.join(); System.out.println("The first thread completed."); } catch (InterruptedException e) { e.printStackTrace(); } } } static class SecondThread implements Runnable { @Override public void run() { try { System.out.println("The second thread started."); Thread.sleep(3000); System.out.println("The second thread completed."); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { t1 = new Thread(new FirstThread()); t2 = new Thread(new SecondThread()); t1.start(); t2.start(); } }
The order of message output will be as follows:
The first thread started. The second thread started. The primary work of the first thread has been completed. The second thread completed. The first thread completed.
Like sleep()
, join()
responds to interrupt, therefore, it is necessary to catch InterruptedException
.
You can create a thread whose work is interrupted when the thread that created it terminates. In order to create such a thread, the setDaemon()
method is called with the true
parameter. The setDaemon()
method must be called after the thread was created, but before the start()
method is called. You can check whether there is a thread by the daemon or not using isDaemon()
method. If the daemon thread creates other threads, they will also receive the status of the daemon thread.
package ua.inf.iwanoff.oop.fifth; public class Th extends Thread { public void run() { try { if (isDaemon()) { System.out.println("start of the daemon thread"); sleep(1000); } else { System.out.println("start of the regular thread"); } } catch (InterruptedException e) { System.err.print("Error" + e); } finally { if (!isDaemon()) System.out.println("regular thread termination"); else System.out.println("daemon thread termination"); } } public static void main(String[] args) { Th usual = new Th(); Th daemon = new Th(); daemon.setDaemon(true); daemon.start(); usual.start(); System.out.println("the last line of the main() method"); } }
After compiling and running, it is possible that the following message output sequence will be obtained:
start of the daemon thread the last line of the main() method start of the regular thread regular thread termination
The daemon thread (due to the call to the sleep(10000)
method) did not have time to complete the execution of its code before the main application thread associated with the main()
method was completed. The basic property of daemon threads is the ability of the main application thread to complete execution with the end of the code of the main()
method, ignoring the fact that the daemon thread is still running. If you set the delay time also for the main()
thread, then the daemon thread can have time to complete its execution before the main thread ends.
start of the daemon thread
start of the regular thread
regular thread termination
daemon thread termination
the last line of the main() method
In the case of multiple threads, it is important to eliminate the possibility of using one resource by two threads. If the data members of a class are declared as private and access to this memory area is possible only through methods, then collisions can be avoided by declaring these methods as synchronized
. At the same time, only one thread can call a synchronized method for a specific object. Such a method or code fragment is referred to as a critical section.
The concept of the monitor is used to provide access to such code. A monitor is a certain object that blocks a code during execution by a certain thread. If the synchronized
keyword is located before of a function name, the object for which this method is called (this
) considered as monitor. In this case, after at least one synchronized method is called, all methods with the synchronized
modifier are blocked as well.
Consider an example. The Adder
class allows you to add integers to some accumulator. The AdderThread
class, which implements the execution thread, provides the addition of consecutive five integer values. In the main()
function of the AdderTest
class, we create two threads and run them:
package ua.inf.iwanoff.oop.fifth; class Adder { long sum = 0; public void add(long value) { this.sum += value; System.out.println(Thread.currentThread().getName() + " " + sum); } } class AdderThread extends Thread { private Adder counter = null; public AdderThread(String name, Adder counter) { super(name); this.counter = counter; } public void run() { try { for (int i = 0; i < 5; i++) { counter.add(i); Thread.sleep(10); } } catch (InterruptedException e) { e.printStackTrace(); } } } public class AdderTest { public static void main(String[] args) { Adder adder = new Adder(); Thread threadA = new AdderThread("A", adder); Thread threadB = new AdderThread("B", adder); threadA.start(); threadB.start(); } }
The program that was started several times will produce various intermediate results due to the incorrect operation of two threads with the same object. To fix the situation, the synchronized
modifier should be added before the add()
method:
synchronized public void add(long value) { this.sum += value; System.out.println(Thread.currentThread().getName() + " " + sum); }
Sometimes declaring the entire method as synchronized
is inconvenient, since in this case other threads could execute other blocking methods that are not related to this one. To solve this problem, use a block that starts with the word synchronized
and contains in brackets the name of the object responsible for the lock. Any object derived from java.lang.Object
(that supports the necessary methods) can act as a monitor. The following example creates a special object for blocking:
public void add(long value) { Object lock = new Object(); synchronized (lock) { this.sum += value; System.out.println(Thread.currentThread().getName() + " " + sum); } }
If the amount of memory occupied by the object does not exceed 32 bits, writing and reading do not require synchronization. Such objects are called atomic.
When exchanging data through fields, the volatile
keyword should be used before describing the corresponding field. This is due to the presence of a data caching mechanism for each thread separately, and the value from this cache may differ for each of them. Declaring a field with the volatile
keyword disables such caching for it:
volatile long sum = 0;
The wait()
method, called inside a synchronized block or method, stops the execution of the current thread and releases the captured object, including the lock object, from the lock. You can return blocking of a thread object by calling the notify()
method for a specific thread or by calling notifyAll()
for all threads. A call can only be made from another thread that blocked, in turn, the specified object.
The setPriority()
function allows you to modify priority of a thread. It is not recommended to set highest priority (Thread.MAX_PRIORITY
constant).
2.5.3 Use Thread Execution in JavaFX Applications
The work of graphical user interface applications is usually associated with threads. After starting the JavaFX application, the application thread is automatically created to handle the events. Only this thread can interact with windows and visual components. Attempting two threads simultaneously to control the visualization of components and information on the window's surface can lead to chaotic appearance and unpredictable behavior of the visual components. However, thanks to the multithreading, you can provide better controllability of the application. In addition, it is advisable to take in a separate thread some actions that are performed for a long time. This will provide an opportunity to monitor the process, pause and continue it, change settings, etc.
JavaFX tools allow you to execute code in the thread, which is responsible for receiving and processing events. The javafx.application.Platform.runLater()
method receives an object that implements the Runnable
functional interface. This object enters the event processing thread, where the object becomes in the queue and, when the opportunity arises, its run()
method is executed. In this method, it is advisable to implement interaction with visual components. Example 3.6 demonstrates the possibility of calling this method from a child thread.
There are also high-level mechanisms for working with threads in JavaFX, presented in the javafx.concurrent
package: the abstract Task
class, derived from java.util.concurrent.FutureTask
, in turn provides for the creation of a derived class that implements the call()
method. This method implements interaction with visual components. In addition, this class provides the ability to directly bind some components (for example, ProgressBar
) to the task.
2.6 Use of Thread-Safe Collections
2.6.1 Use of Synchronized Analogues of Standard Collections
The java.util.Collections
class can be used to obtain thread-safe clones of existing collections. These are static functions: synchronizedCollection()
, synchronizedList()
, synchronizedMap()
, synchronizedSet()
, synchronizedSortedMap()
, and synchronizedSortedSet()
. Each of these functions takes as a parameter the corresponding non-synchronized collection and returns a collection of the same type, all methods of which (except for iterators) are defined as synchronized
. Accordingly, all operations, including reading, use a lock that reduces performance and restricts the use of the appropriate collections.
Iterators of standard collections implemented in java.util
implement a so-called fail-fast behavior: if some object was changed bypassing the collection iterator throws the ConcurrentModificationException
exception, and in terms of multithreading, the behavior of the iterator may be unpredictable: an exception may not be thrown immediately or not thrown at all. Reliable and work with collection elements are provided by special container classes defined in java.util.concurrent
. These are types such as CopyOnWriteArrayList
, CopyOnWriteArraySet
, ConcurrentMap
(interface), ConcurrentHashMap
(implementation), ConcurrentNavigableMap
(interface), and ConcurrentSkipListMap
(implementation).
The CopyOnWriteArrayList
class that implements the List interface, creates a copy of the array for all operations that change the value of items. The iterator also works with its copy. Such behavior of the iterator is called fail-safe behavior.
The ConcurrentHashMap
and ConcurrentSkipListMap
classes provide associative arrays that do not require blocking of all operations. The ConcurrentHashMap
class organizes a data structure divided into segments. Blocking is done at the level of one segment. Due to the improved hash function, the probability of applying two threads to one segment is significantly reduced. The ConcurrentSkipListMap
class implements storage using a special data structure, the skip list. The elements of the associative array are sorted by the keys. Example 2.2 demonstrates work with ConcurrentSkipListMap
.
2.6.2 Work with Special Collections that Use Locks
The java.util.concurrent
package provides a collection of multi-threaded security collections. These are generic types such as BlockingQueue
(interface) and its standard implementations (ArrayBlockingQueue
, LinkedBlockingQueue
, PriorityBlockingQueue
, SynchronousQueue
, DelayQueue
), as well as derived from it TransferQueue
interface (LinkedTransferQueue
implementation) and LinkedBlockingDeque
interface (standard implementation is LinkedBlockingDeque
).
The BlockingQueue
interface represents a queue that allows you to add and get items safely from the perspective of multithreading. A typical use of the BlockingQueue
is adding objects in one thread, and getting objects in another thread.
The thread that adds objects (first thread) continues to add them to the queue until some upper limit of the permissible number of elements is reached. In this case, when trying to add new objects, the first thread will be blocked until the thread that receives the elements (second thread) does not remove at least one item from the queue. The second thread can remove and use objects from the queue. If, however, the second thread tries to get an empty queue object, this thread will be blocked until the first thread adds a new object.
The BlockingQueue
interface inherits java.util.Queue
, so it supports add()
, remove()
, element()
(with exception throwing), plus offer()
, poll()
and peek()
(with special returning value). In addition, blocking methods are put()
(for adding) and take()
(for extraction) are declared. There are also methods that use time-locking: offer()
and poll()
with additional parameters (time interval and time unit).
You cannot add null
to BlockingQueue
. This attempt leads to the throw of NullPointerException
.
The most popular implementation of BlockingQueue
is ArrayBlockingQueue
. In order to store references to objects in this implementation, a conventional array of limited length is used. This length cannot be changed after creating a queue. All class constructors receive this array length as the first parameter. In the example below, two threads are created - Producer
and Consumer
. The producer adds integers with a delay between additions, and the consumer withdraws them
package ua.inf.iwanoff.oop.fifth; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; class Producer implements Runnable { private BlockingQueue<Integer> queue; int countToAdd; public Producer(BlockingQueue<Integer> queue, int countToAdd) { this.queue = queue; this.countToAdd = countToAdd; } public void run() { // We try to add numbers: try { for (int i = 1; i <= countToAdd; i++) { queue.put(i); System.out.printf("Added: %d%n", i); Thread.sleep(100); } } catch (InterruptedException e) { System.out.println("Producer interrupted"); } } } class Consumer implements Runnable { private BlockingQueue<Integer> queue; int countToTake; public Consumer(BlockingQueue<Integer> queue, int countToTake) { this.queue = queue; this.countToTake = countToTake; } public void run() { // We withdraw numbers: try { for (int i = 1; i <= countToTake; i++) { System.out.printf("Taken by customer: %d%n", queue.take()); } } catch (InterruptedException e) { System.out.println("Consumer interrupted"); } } } public class BlockingQueueDemo { public static void main(String[] args) throws InterruptedException { // Create a queue of 10 items: BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); // Create and launch two threads - for writing and reading: Thread producerThread = new Thread(new Producer(queue, 100)); Thread consumerThread = new Thread(new Consumer(queue, 4)); producerThread.start(); consumerThread.start(); // Wait 10 seconds and interrupt the first thread: Thread.sleep(10000); producerThread.interrupt(); } }
When you start the program, you can see that the producer thread consistently adds numbers to the queue. The first four numbers are withdrawn by the consumer thread, while the consumer thread is blocked each time, waiting for the next number. Since only 10 numbers can fit in the queue, the queue quickly becomes full after the consumer thread stops withdrawing items. After ten seconds, the main thread interrupts the producer thread.
The implementation of LinkedBlockingQueue
differs from ArrayBlockingQueue
in its internal representation: a linked list is used. The maximum size is no longer significant (Integer.MAX_VALUE
by default), but it can also be specified in the constructor.
The PriorityBlockingQueue
class uses the same ordering rules as java.util.PriorityQueue
.
The SynchronousQueue
class represents a queue that can contain only one element. The thread that added the item is blocked until another thread receives it from the queue. If the second thread cannot get an element (the queue is empty), this thread is also blocked until a new element is added by the first thread.
The implementation of DelayQueue
allows you to remove only those elements that have been expired for some time (delay). The elements of this queue should implement the interface java.util.concurrent.Delayed
. This interface defines the getDelay()
method, which returns the delay remaining for the object. From the queue, the element for which the delay time has passed before the other is removed in the first place. If none of the elements is delayed, the poll()
method returns null
.
The BlockingDeque
interface, derived from the BlockingQueue
, additionally supports the aaddFirst()
, addLast()
, takeFirst()
, and takeLast()
operations, which are inherent in double-ended queues. The LinkedBlockingDeque
class offers a standard implementation of this interface.
2.7 Working with Containers and Stream API in Java 8
2.7.1 Use of Optional Class
Optional values are containers for values that can sometimes be empty. Traditionally, for uncertain values, the value null
was used. Using the null
constant can be inconvenient because it can lead to the throwing of the NullPointerException
exception, which complicates debugging and maintenance of the program.
A generic Optional
class allows you to save the reference value to some object, and also to check if the value is set. For example, some method returns a numeric value, but for some arguments the argument cannot return something definite. This method can return an Optional
type object and this value can then be used in the calling function. Suppose some function calculates and returns the reciprocal value and returns an "empty" object if the argument is equal to 0. The main()
function performs the calculation for the reciprocal numbers for array items:
package ua.inf.iwanoff.oop.fifth; import java.util.Optional; public class OptionalDemo { static Optional<Double> reciprocal(double x) { Optional<Double> result = Optional.empty(); if (x != 0) { result = Optional.of(1 / x); } return result; } public static void main(String[] args) { double[] arr = { -2, 0, 10 }; Optional<Double> y; for (double x : arr) { System.out.printf("x = %6.3f ", x); y = reciprocal(x); if (y.isPresent()) { System.out.printf("y = %6.3f%n", y.get()); } else { System.out.printf("The value cannot be calculated%n"); } } } }
If you do not check for the presence of a value (isPresent()
), a java.util.NoSuchElementException
will be thrown when you try to call the get()
function for the "empty" value. It can be caught instead of calling the isPresent()
function.
In some cases, the null
value should not be considered as possible. In this case, use ofNullable()
to store the value. For example:
Integer k = null; Optional<Integer> opt = Optional.ofNullable(k); System.out.println(opt.isPresent()); // false
Assume that if the reciprocal()
function described before does not return a value in the case of division by zero, the y variable should be assigned to 0. Traditionally, in this case, the if
... else
statement or conditional operation is used:
y = reciprocal(x); double z = y.isPresent() ? y.get() : 0;
The orElse()
method allows you to make the code more compact:
double z = reciprocal(x).orElse(0.0);
In addition to the generic Optional
class, you can also use classes optimized for primitive types: OptionalInt
, OptionalLong
, OptionalBoolean
, etc. The previous example (calculation of the reciprocal value) could be realized in this way (using OptionalDouble
):
package ua.inf.iwanoff.oop.fifth; import java.util.OptionalDouble; public class OptionalDoubleDemo { static OptionalDouble reciprocal(double x) { OptionalDouble result = OptionalDouble.empty(); if (x != 0) { result = OptionalDouble.of(1 / x); } return result; } public static void main(String[] args) { double[] arr = {-2, 0, 10}; OptionalDouble y; for (double x : arr) { System.out.printf("x = %6.3f ", x); y = reciprocal(x); if (y.isPresent()) { System.out.printf("y = %6.3f%n", y.getAsDouble()); } else { System.out.printf("The value cannot be calculated%n"); } } } }
As can be seen from the example, the result can be obtained using the getAsDouble()
method instead of get()
.
2.7.2 Additional Features for Standard Containers
Standard interfaces of the java.util
package are supplemented with methods that focus on using lambda expressions and references to methods. To ensure compatibility with previous versions, new interfaces provide the default implementation of the new methods. In particular, the Iterable
interface defines the forEach()
method, which allows you to perform some actions in the loop that do not change the elements of the collection. You can specify an action using a lambda expression or a reference to a method. For example:
public class ForEachDemo { static int sum = 0; public static void main(String[] args) { Iterable<Integer> numbers = new ArrayList(Arrays.asList(2, 3, 4)); numbers.forEach(n -> sum += n); System.out.println(sum); } }
In the above example, the sum of collection elements is calculated. The variable that holds the sum is described as a static class field, since lambda expressions cannot change local variables.
The Collection
interface defines the removeIf()
method, which allows you to remove items from the collection if items match a certain filter rule. In the following example, odd items are removed from the collection of integers. The forEach()
method is used for columnwise output the collection items:
Collection<Integer> c = new ArrayList(Arrays.asList(2, 4, 11, 8, 12, 3)); c.removeIf(k -> k % 2 != 0); // The rest of the items are displayed columnwise: c.forEach(System.out::println);
The List
interface provides methods replaceAll()
and sort()
. The second one can be used instead of the analogous static method of the Collections
class, but the definition of the sorting feature is obligatory:
List<Integer> list = new ArrayList(Arrays.asList(2, 4, 11, 8, 12, 3)); list.replaceAll(k -> k * k); // replace the numbers with their second powers System.out.println(list); // [4, 16, 121, 64, 144, 9] list.sort(Integer::compare); System.out.println(list); // [4, 9, 16, 64, 121, 144] list.sort((i1, i2) -> Integer.compare(i2, i1)); System.out.println(list); // [144, 121, 64, 16, 9, 4]
The most significant changes have affected the Map
interface. The added methods listed in the table:
Method | Description |
---|---|
V getOrDefault(Object key, V& defaultValue) |
Returns a value, or a default value, if the key is missing |
V putIfAbsent(K key, V value) |
Adds a pair if the key is missing and returns the value |
boolean remove(Object key, Object value) |
Removes a pair if it is present |
boolean replace(K key, V oldValue, V newValue) |
Replaces value with the new one if pair is present |
V replace(K key, V value) |
Replaces the value if the key is present, returns the old value |
V compute(K key, BiFunction<?& super K, super V, ? extends V> remappingFunction) |
Invokes the function to construct a new value. A new pair is added, a pair that existed before is deleted, and a new value is returned |
V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) |
If a specified key is present, a new function is called to create a new value, and the new value replaces the previous one. |
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) |
Returns the value by the key. If the key is missing, a new pair is added, the value is calculated by function |
V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) |
If the key is absent, then a new pair is entered and the value v is returned. Otherwise, the given function returns a new value based on the previous value and the key is updated to access this value. and then it returns |
void forEach(BiConsumer<? super K, ? super V> action) |
Performs a given action on each element |
The following example demonstrates the use of some of these methods:
package ua.inf.iwanoff.oop.fifth; import java.util.HashMap; import java.util.Map; public class MapDemo { static void print(Integer i, String s) { System.out.printf("%3d %10s %n", i, s); } public static void main(String[] args) { Map<Integer, String> map = new HashMap<>(); map.put(1, "one"); map.put(2, "two"); map.put(7, "seven"); map.forEach(MapDemo::print); // columnwise output System.out.println(map.putIfAbsent(7, "eight")); // seven System.out.println(map.putIfAbsent(8, "eight")); // null System.out.println(map.getOrDefault(2, "zero")); // two System.out.println(map.getOrDefault(3, "zero")); // zero map.replaceAll((i, s) -> i > 1 ? s.toUpperCase() : s); System.out.println(map); // {1=one, 2=TWO, 7=SEVEN, 8=EIGHT} map.compute(7, (i, s) -> s.toLowerCase()); System.out.println(map); // {1=one, 2=TWO, 7=seven, 8=EIGHT} map.computeIfAbsent(2, (i) -> i + ""); System.out.println(map); // nothing changed map.computeIfAbsent(4, (i) -> i + ""); System.out.println(map); // {1=one, 2=TWO, 4=4, 7=seven, 8=EIGHT} map.computeIfPresent(5, (i, s) -> s.toLowerCase()); System.out.println(map); // nothing changed map.computeIfPresent(2, (i, s) -> s.toLowerCase()); System.out.println(map); // {1=one, 2=two, 4=4, 7=seven, 8=EIGHT} // Adding a new pair: map.merge(9, "nine", (value, newValue) -> value.concat(newValue)); System.out.println(map.get(9)); // nine // The text is concatenated with the previous one: map.merge(9, " as well", (value, newValue) -> value.concat(newValue)); System.out.println(map.get(9)); // nine as well } }
2.7.3 Using the Java 8 Stream API
Streams for work with collections, or streams of elements, data streams (Stream API) are designed for high-level processing of data stored in containers. They should not be confused with input / output streams.
The Stream API is used to search, filter, transform, find the minimum and maximum values, as well as other data manipulation. An important advantage of the Stream API is the ability to work reliably and efficiently in a multithreading environment.
Streams should be understood not as a new kind of collections, but as a channel for transmission and processing of data. The stream of elements works with some data source, such as an array or collection. The stream does not store data directly, but performs transferring, filtering, sorting, etc. The actions performed by the stream do not change the source data. For example, sorting data in a stream does not change their order in the source, but creates a separate resulting collection.
You can create sequential and parallel streams of elements. Parallel streams are secure in terms of multithreading. From the available parallel stream you can get sequential one and vice versa.
To work with streams Java 8 java.util.stream
package provides a set of interfaces and classes that provide operations on a stream of elements in the style of functional programming. The stream is represented by an object that implements the java.util.stream.Stream
interface. In turn, this interface inherits the methods of the general interface java.util.stream.BaseStream
.
Stream operations (methods) defined in the BaseStream
, Stream
, and other derived interfaces are divided into intermediate and terminal. Intermediate operations receive and generate data streams and serve to create so-called pipelines, in which a sequence of actions is performed over a sequence. Terminal operations give the final result and thus "consume" the output stream. This means that the output stream cannot be reused and, if necessary, must be re-created.
The most significant methods of the generic java.util.stream.BaseStream
interface are given in the table (S
- type of the stream, E
- type of the element, R
- container type):
Method | Description | Note |
---|---|---|
S parallel() |
returns a parallel stream received from the current one | intermediate operation |
S sequential() |
returns a sequential stream received from the current one | intermediate operation |
boolean isParallel() |
returns true if the stream is parallel or false if it is sequential |
|
S unordered() |
returns an unordered data stream obtained from the current | intermediate operation |
Iterator<T> iterator() |
returns an iterator for the elements of this stream | terminal operation |
Spliterator<T> spliterator() |
returns a spliterator (split iterator) for the elements of this stream. | terminal operation |
The use of stream iterators will be discussed later.
The Stream
interface extends the set of methods for working with streaming elements. It is also a generic interface and is suitable for working with any reference types. The following are the most commonly used Stream
interface methods:
Method | Description | Note |
---|---|---|
void forEach(Consumer<? super T> action) |
executes the code specified by the action for each element of the stream |
terminal operation |
Stream<T> filter(Predicate<? super T> pred) |
returns a stream of elements satisfying the predicate | intermediate operation |
Stream<T> sorted() |
returns a stream of elements sorted in natural order | intermediate operation |
Stream<T> sorted(Comparator<? super T> comparator) |
returns a stream of elements sorted in the specified order | intermediate operation |
|
applies the given function to the elements of the stream and returns a new stream | intermediate operation |
Optional<T> min(Comparator<? super T> comp) |
returns the minimum value using the specified comparison | terminal operation |
Optional<T> max(Comparator<? super T> comp) |
returns the maximum value using the specified comparison | terminal operation |
long count() |
returns the number of elements in the stream |
terminal operation |
Stream<T> distinct() |
returns a stream of differing elements | intermediate operation |
Optional<T> reduce(BinaryOperator<T> accumulator) |
returns the scalar result calculated by the values of the elements | terminal operation |
Object[] toArray() |
creates and returns an array of stream elements | terminal operation |
There are several ways to create a stream. You can use the factory methods added to the Collection
interface (with default implementations), respectively stream()
(for synchronous work) and parallelStream()
(for asynchronous work):
List<Integer> intList = Arrays.asList(3, 4, 1, 2); Stream<Integer> sequential = intList.stream(); Stream<Integer> parallel = intList.parallelStream();
You can create a stream from an array:
Integer[] a = { 1, 2, 3 }; Stream<Integer> fromArray = Arrays.stream(a);
You can create a data source with the specified items. To do this, use the "factory" method of()
:
Stream<Integer> newStream = Stream.of(4, 5, 6);
Streams of items can be created from input streams (BufferedReader.lines()
), filled with random values (Random.ints()
), and also obtained from archives, bit sets, etc.
Intermediate operations are characterized by so-called lazy behavior: they are performed not instantaneously, but as the need arises - when the final operation is working with a new data stream. Lazy behavior increases the efficiency of work with the stream of elements.
Most operations are implemented in such a way that actions on individual elements do not depend on other elements. Such operations are called stateless operations. Other operations that require working on all elements at once (for example, sorted()
) are called stateful operations.
You can get an array from a stream using the toArray()
method. The following example creates a stream and then outputs to the console by creating an array and obtaining a string representation using the static Arrays.toString()
method:
s = Stream.of (1, -2, 3); System.out.println(Arrays.toString(s.toArray())); // [1, -2, 3]
Streams provide iterators. The iterator()
method of the Stream
interface returns an object that implements the java.util.Iterator
interface. The iterator can be used explicitly:
s = Stream.of(11, -2, 3); Iterator<Integer> it = s.iterator(); while (it.hasNext()) { System.out.println(it.next()); }
There is also a special type of iterator, a split iterator (implemented by the Spliterator
interface). It, in particular, allows you to bypass some part of the elements.
Streams provide iteration over data elements using the forEach()
method. The function parameter is the standard Consumer
functional interface, which defines a method with a single parameter and a void
result type. For example:
Stream<Integer> s = Stream.of(4, 5, 6, 1, 2, 3); s.forEach(System.out::println);
The simplest stream operation is filtering. The intermediate filter()
operation returns a filtered stream, taking a parameter of Predicate
type. The Predicate
type is a functional interface that describes a method with a single parameter and boolean
result type. For example, you can filter out only even numbers from the stream s
:
s.filter(k -> k % 2 == 0).forEach(System.out::println);
The previous example illustrates the use of lambda expressions when working with streams, as well as a small conveyor that includes one intermediate operation.
The intermediate sorted()
operation returns the sorted representation of the stream. Elements are ordered in the natural order (if it is defined). In other cases, the Comparator
interface should be implemented, for example, using the lambda expression:
// Sort ascending: Stream<Integer> s = Stream.of(4, 5, 6, 1, 2, 3); s.sorted().forEach(System.out::println); // Sort descending: s = Stream.of(4, 5, 6, 1, 2, 3); s.sorted((k1, k2) -> Integer.compare(k2, k1)).forEach(System.out::println);
The last example shows that after each call to the terminal operation, the stream should be recreated.
The intermediate operation map()
receives a functional interface that defines a certain function for transforming and forming a new stream from the resulting transformed elements. For example, we calculate the squares of numbers:
s = Stream.of(1, 2, 3); s.map(x -> x * x).forEach(System.out::println);
Using the distinct()
method, you can get a stream containing only different elements of the collection. For example:
s = Stream.of(1, 1, -2, 3, 3); System.out.println(Arrays.toString(s.distinct().toArray())); // [1, -2, 3]
The end operation count()
with the resulting type long
returns the number of elements in the stream:
s = Stream.of(4, 5, 6, 1, 2, 3); System.out.println(s.count()); // 6
The terminal operations min()
and max()
return Optional
objects with a minimum and maximum value, respectively. A Comparator
type parameter is used for comparison. For example:
s = Stream.of(11, -2, 3); System.out.println(s.min(Integer::compare).get()); // -2
Using a terminal reduce()
operation, we can calculate a scalar value. The reduce()
operation in its simplest form performs the specified action with two operands, the first of which is the result of performing the action on the previous elements, and the second is the current element. In the following example, we find the sum of the elements of the data stream:
s = Stream.of(1, 1, -2, 3, 3); Optional<Integer> sum = s.reduce((s1, s2) -> s1 + s2); sum.ifPresent(System.out::println); // 6
The min()
, max()
, and reduce()
operations get a scalar value from the stream, so they are called reduction operations.
There are also streams for working with primitive types: IntStream
, LongStream
and DoubleStream
.
2.8 Standard Tools for Creating Graphs and Diagrams
The standard JavaFX tools provide ready-made components for plotting and charting. The Abstract Class Chart is a base for all charts and diagrams and combines the properties of all components intended to visualize numeric data. Directly derived from it are the PieChart
and XYChart
classes. The XYChart
class is also abstract and serves as the base class for all other charts: AreaChart
, BarChart
, BubbleChart
, LineChart
, ScatterChart
, StackedAreaChart
, and StackedBarChart
.
The constructors of classes derived from XYChart
require the definition of two objects of the Axis
type. Axis
is an abstract class for representing an axis on a chart or diagram. The objects of derived CategoryAxis
class are used when some strings are allocated along the axis. Another derived type is an abstract ValueAxis
class. The NumberAxis
class derived from ValueAxis
is used when the axis represents a scale of numeric values.
The following example demonstrates the possibilities of constructing some diagrams:
package ua.inf.iwanoff.oop.fifth; import javafx.application.Application; import javafx.collections.FXCollections; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.chart.*; import javafx.scene.chart.XYChart.Series; import javafx.scene.layout.HBox; import javafx.stage.Stage; public class ChartDemo extends Application { @Override public void start(Stage primaryStage) { primaryStage.setTitle("Chart Demo"); PieChart pieChart = new PieChart(); pieChart.setMinWidth(300); pieChart.setTitle("Pie Chart"); pieChart.getData().addAll(new PieChart.Data("two", 2), new PieChart.Data("three", 3), new PieChart.Data("one", 1)); XYChart.Data<String, Number>[] barData = new XYChart.Data[] { new XYChart.Data<>("one", 2.0), new XYChart.Data<>("two", 3.0), new XYChart.Data<>("three", 1.0) }; BarChart<String, Number> barChart = new BarChart<>(new CategoryAxis(), new NumberAxis()); barChart.setTitle("Bar Chart"); barChart.getData().addAll(new Series<>("Bars", FXCollections.observableArrayList(barData))); XYChart.Data<Number, Number>[] xyData = new XYChart.Data[] { new XYChart.Data<>(1.0, 2.0), new XYChart.Data<>(2.0, 3.0), new XYChart.Data<>(3.0, 1.0) }; AreaChart<Number, Number> areaChart = new AreaChart<>(new NumberAxis(), new NumberAxis()); areaChart.setTitle("Area Chart"); areaChart.getData().addAll(new Series<>("Points", FXCollections.observableArrayList(xyData))); // Uses the same data set BubbleChart<Number, Number> bubbleChart = new BubbleChart<>( new NumberAxis(0, 4, 1), new NumberAxis(0, 5, 1)); bubbleChart.setTitle("Bubble Chart"); bubbleChart.getData().addAll(new Series<>("Bubbles", FXCollections.observableArrayList(xyData))); HBox hBox = new HBox(); hBox.getChildren().addAll(pieChart, barChart, areaChart, bubbleChart); hBox.setSpacing(10); hBox.setPadding(new Insets(10, 10, 10, 10)); Scene scene = new Scene(hBox, 1000, 350); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
The main window of the application will look like this:
For charts derived from XYChart
, you can define multiple data sets (Series
) to visually compare different dependencies.
3 Sample Programs
3.1 Obtaining Data about Class Methods
Suppose we need to create a program that displays data about methods of a class: name, resulting type and types of parameters. The program will look like this:
package ua.inf.iwanoff.oop.fifth; import java.lang.reflect.Method; import java.util.Scanner; public class ShowAllMethods { public static void main(String[] args) { System.out.print("Enter the full name of the class: "); String className = new Scanner(System.in).next(); try { Class<?> c = Class.forName(className); for (Method m : c.getMethods()) { System.out.printf("Name: %s Resulting type: %s%n", m.getName(), m.getReturnType().getCanonicalName()); for (Class<?> type : m.getParameterTypes()) { System.out.printf(" Parameter type: %s%n", type.getCanonicalName()); } } } catch (ClassNotFoundException e) { System.err.println("Error entering class name!"); } } }
3.2 Calculating the Results of Math Class Functions
In the following example, functions getMethod()
and invoke()
are used to call static methods of java.lang.Math
class (only methods with one parameter can be called):
package ua.inf.iwanoff.oop.fifth; import java.lang.reflect.Method; import java.util.InputMismatchException; import java.util.Scanner; public class SomeFunction { private Method method = null; private String methodName = null; private String className = null; public SomeFunction(String className, String methodName) { this.className = className; this.methodName = methodName; } public double getY(double x) throws Exception { if (method == null) { // Create an object of type Method, if it was not created before. // The method must accept one double argument: method = Class.forName(className).getMethod(methodName, double.class); } // Call the method: return (Double) method.invoke(null, x); } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("Enter the name of the static class Math method:"); SomeFunction someFunction = new SomeFunction("java.lang.Math", scanner.next()); System.out.println("Enter the beginning, end of the interval, and step:"); try { double from = scanner.nextDouble(); double to = scanner.nextDouble(); double step = scanner.nextDouble(); for (double x = from; x <= to; x += step) { System.out.println(x + "\t" + someFunction.getY(x)); } } catch (InputMismatchException e) { System.out.println("Error entering data"); } catch (Exception e) { System.out.println("Error calling function"); } } }
3.3 Calculating the Value of an Expression with a String
When executing the next program, the user enters an expression that uses some string, and receives the result of this expression on the screen. The methods of the String
class can be used. Depending on the purpose of the program, we can propose two variants of its implementation.
First Version
The first approach implies that the generation of the code will be carried out once. In the Eclipse environment, we create the StringProcessor
project, the ua.inf.iwanoff.oop.fifth
package, and the StringProcessor
class. The code can be as follows:
package ua.inf.iwanoff.oop.fifth; import java.io.*; import java.lang.reflect.Method; import java.util.Scanner; import javax.tools.*; public class StringProcessor { final String sourceFile = "bin/ua/inf/iwanoff/oop/fifth/StrFun.java"; void genSource(String expression) { try (PrintWriter out = new PrintWriter(sourceFile)) { out.println("package ua.inf.iwanoff.oop.fifth;"); out.println("public class StrFun {"); out.println(" public static String transform(String s) {"); // Add an empty string to convert the result of any type to a string: out.println(" return " + expression + "\"\";"); out.println(" }"); out.println("}"); } catch (IOException e) { e.printStackTrace(); } } boolean compile() { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); return compiler.run(null, null, null, sourceFile) == 0; } @SuppressWarnings("resource") public static void main(String[] args) { StringProcessor sp = new StringProcessor(); Scanner scan = new Scanner(System.in); System.out.println("Enter the expression that you want to execute over the s string:"); String expression = scan.nextLine().replaceAll("\"", "\\\""); sp.genSource(expression); try { if (sp.compile()) { System.out.println("Enter s:"); String s = scan.nextLine(); Class<?> cls = Class.forName("ua.inf.iwanoff.oop.fifth.StrFun"); Method m = cls.getMethod("transform", String.class); System.out.println(m.invoke(null, new Object[] { s })); } else { System.out.println("Error entering the expression!"); } } catch (Exception e) { e.printStackTrace(); } } }
Note: in IntelliJ IDEA environment for the corresponding project called StringProcessor
, initialization of the sourceFile
variable will be as follows:
final String sourceFile = "out/production/StringProcessor/ua/inf/iwanoff/oop/fifth/StrFun.java";
Second Version
A more complex case assumes repeated input of the expression and, accordingly, multiple code generation. The problem is that after loading the class, it needs to be unloaded in order to load a new version with the same name. This is a non-trivial task that involves creating your own class loader. A simpler approach is to create classes with different names. For example, you can add successive integers to class names. Now the program will be as follows:
package ua.inf.iwanoff.oop.fifth; import java.io.*; import java.lang.reflect.Method; import java.util.Scanner; import javax.tools.*; public class StringProcessor { final String sourceFile = "bin/ua/inf/iwanoff/oop/fifth/StrFun"; void genSource(String expression, int number) { try (PrintWriter out = new PrintWriter(sourceFile + number + ".java")) { out.println("package ua.inf.iwanoff.oop.fifth;"); out.println("public class StrFun" + number + "{"); out.println(" public static String transform(String s) {"); out.println(" return " + expression + " + \"\";"); out.println(" }"); out.println("}"); } catch (IOException e) { e.printStackTrace(); } } boolean compile(int number) { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); return compiler.run(null, null, null, sourceFile + number + ".java") == 0; } @SuppressWarnings("resource") public static void main(String[] args) { int number = 0; StringProcessor sp = new StringProcessor(); Scanner scan = new Scanner(System.in); String expression; do { System.out.println("Enter the expression that you want to execute over the s string " + "(empty string to finish):"); expression = scan.nextLine().replaceAll("\"", "\\\""); if (expression.length() == 0) { break; } sp.genSource(expression, ++number); try { if (sp.compile(number)) { System.out.println("Enter s:"); String s = scan.nextLine(); Class<?> cls = Class.forName("ua.inf.iwanoff.oop.fifth.StrFun" + number); Method m = cls.getMethod("transform", String.class); System.out.println(m.invoke(null, new Object[]{s})); } else { System.out.println("Error entering the expression!"); } } catch (Exception e) { e.printStackTrace(); } } while (expression.length() > 0); } }
Note: in IntelliJ IDEA, the StringProcessor
string should also be reduced.
3.4 Working with the ConcurrentSkipListMap Container
In the following program, one thread fills ConcurrentSkipListMap
with a number / list of simple factors pairs, and another thread finds prime numbers (numbers with the only simple factor) and lists them. Numbers are checked in a given range.
The PrimeFactorization
class (getting simple factors):
package ua.inf.iwanoff.oop.fifth; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.concurrent.ConcurrentSkipListMap; public class PrimeFactorization implements Runnable { static volatile Map<Long, List<Long>> table = new ConcurrentSkipListMap<>(); private long from, to; public long getFrom() { return from; } public long getTo() { return to; } public void setRange(long from, long to) { this.from = from; this.to = to; } // Getting a list of dividers of a given number: private List<Long> factors(long n) { String number = n + "\t"; List<Long> result = new ArrayList<>(); for (long k = 2; k <= n; k++) { while (n % k == 0) { result.add(k); n /= k; } } System.out.println(number + result); return result; } @Override public void run() { try { for (long n = from; n <= to; n++) { table.put(n, factors(n)); Thread.sleep(1); } } catch (InterruptedException e) { e.printStackTrace(); } } @Override public String toString() { String result = table.entrySet().size() + " numbers\n"; for (Map.Entry<?, ?> e : table.entrySet()) { result += e + "\n"; } return result; } }
The Primes
class (prime numbers):
package ua.inf.iwanoff.oop.fifth; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; public class Primes implements Runnable { PrimeFactorization pf; Set<Long> result; public Primes(PrimeFactorization pf) { this.pf = pf; } @Override public void run() { result = new LinkedHashSet<>(); try { for (long last = pf.getFrom(); last <= pf.getTo(); last++) { List<Long> factors; do { // trying to get the next set of numbers: factors = pf.table.get(last); Thread.sleep(1); } while (factors == null); // found a prime number: if (factors.size() == 1) { result.add(last); System.out.println(this); } } } catch (InterruptedException e) { e.printStackTrace(); } } @Override public String toString() { return " Prime numbers: " + result; } }
The PrimesTest
class:
package ua.inf.iwanoff.oop.fifth; public class PrimesTest { public static void main(String[] args) throws InterruptedException { PrimeFactorization pf = new PrimeFactorization(); Primes primes = new Primes(pf); pf.setRange(100000L, 100100L); new Thread(pf).start(); Thread primesThread = new Thread(primes); primesThread.start(); primesThread.join(); System.out.println(pf); System.out.println(primes); } }
3.5 Getting a Table of Prime Numbers using Data Streams
The following program allows getting a table of primes in a given range. For prime numbers, it is advisable to use IntStream
:
package ua.inf.iwanoff.oop.fifth; import java.util.stream.IntStream; public class PrimeFinder { private static boolean isPrime(int n) { return n > 1 && IntStream.range(2, n - 1).noneMatch(k -> n % k == 0); } public static void printAllPrimes(int from, int to) { IntStream primes = IntStream.range(from, to + 1).filter(PrimeFinder::isPrime); primes.forEach(System.out::println); } public static void main(String[] args) { printAllPrimes(6, 199); } }
The isPrime()
method checks whether n
is a prime number. For numbers greater than 1, a sequence of consecutive integers is formed, for each of which is checked, whether n is divided by this number. In the printAllPrimes()
method, we generate a stream of prime numbers using a filter and output the numbers using the forEach()
method.
3.6 Creating a GUI Application for Calculating and Displaying Prime Numbers
Suppose it is necessary to develop a program for obtaining primes in the range from 1 to a certain value, which can be quite large. To find primes we will use the simplest algorithm for sequential checking of all numbers from 2 to the square root of the verifiable number. Such a check may take a long time. To create a user-friendly interface, it's advisable to use a separate execution thread for checking numbers. This will allow user to pause and resume searches, perform standard window manipulations, including closing window before the search will be finished.
The root pane of our application is BorderPane
. The source code of the JavaFX application will be as follows:
package ua.inf.iwanoff.oop.fifth; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.layout.BorderPane; import javafx.stage.Stage; import java.io.IOException; public class PrimeApp extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { FXMLLoader loader = new FXMLLoader(); try { BorderPane root = loader.load(getClass().getResource("PrimeAppWindow.fxml")); Scene scene = new Scene(root, 700, 500); primaryStage.setScene(scene); primaryStage.setTitle("Prime Numbers"); primaryStage.show(); } catch (IOException e) { e.printStackTrace(); } } }
The user interface will include the top panel (HBox
), on which will be successively allocated a label with the text "To:", a textFieldTo
input field, and four buttons: buttonStart
, buttonSuspend
, buttonResume
and buttonStop
with the text "Start", "Suspend", "Resume" and "Stop". After starting a program, only the buttonStart
button is available. The middle part will be occupied by the textAreaResults
input area, for which editing is disabled. The lower part will be occupied by the progressBar
component, in which the percentage of the search process for primes will be displayed. The FXML document file will look like this:
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.geometry.Insets?> <?import javafx.scene.control.Button?> <?import javafx.scene.control.Label?> <?import javafx.scene.control.TextArea?> <?import javafx.scene.control.TextField?> <?import javafx.scene.layout.BorderPane?> <?import javafx.scene.layout.HBox?> <?import javafx.scene.control.ProgressBar?> <BorderPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ua.inf.iwanoff.oop.fifth.PrimeAppController"> <top> <HBox prefHeight="3.0" prefWidth="600.0" spacing="10.0" BorderPane.alignment="CENTER"> <padding> <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/> </padding> <Label text="To:"/> <TextField fx:id="textFieldTo" text="1000"/> <Button fx:id="buttonStart" minWidth="80.0" text="Start" onAction="#startClick"/> <Button fx:id="buttonSuspend" minWidth="80.0" disable="true" text="Suspend" onAction="#suspendClick"/> <Button fx:id="buttonResume" minWidth="80.0" disable="true" text="Resume" onAction="#resumeClick"/> <Button fx:id="buttonStop" minWidth="80.0" disable="true" text="Stop" onAction="#stopClick"/> </HBox> </top> <center> <TextArea fx:id="textAreaResults" editable="false" wrapText="true" BorderPane.alignment="CENTER"/> </center> <bottom> <ProgressBar fx:id="progressBar" maxWidth="Infinity" progress="0"/> </bottom> </BorderPane>
We can generate an empty controller class, add fields associated with visual components and methods of handling events:
package ua.inf.iwanoff.oop.fifth; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.*; public class PrimeAppController { @FXML private TextField textFieldTo; @FXML private Button buttonStart; @FXML private Button buttonSuspend; @FXML private Button buttonResume; @FXML private Button buttonStop; @FXML private ProgressBar progressBar; @FXML private TextArea textAreaResults; @FXML private void startClick(ActionEvent actionEvent) { } @FXML private void suspendClick(ActionEvent actionEvent) { } @FXML private void resumeClick(ActionEvent actionEvent) { } @FXML private void stopClick(ActionEvent actionEvent) { } }
After starting the program an application window will be the following:
Now, separately from the GUIs, you can create a class that will be responsible for calculating primes in a separate thread. The PrimeNumbers
class will implement the Runnable
interface. The main functions (start()
, suspend()
, resume()
and stop()
), which will be called from the controller, change the state of the object (suspended
and stopped
fields). To provide flexibility, the program updates data about simple numbers and percentage of process execution through the callback mechanism. The fields of type Runnable
store appropriate objects. To launch run()
methods in another thread, these functions are called using Platform.runLater()
method. The PrimeNumbers
class cheat code will look like this:
package ua.inf.iwanoff.oop.fifth; import javafx.application.Platform; public class PrimeNumbers implements Runnable { private Thread primesThread; // the thread of the calculation of primes private int to; // the end of the range of the calculation of primes private int lastFound; // last found prime private Runnable displayFunc; // the function that is called to output the found number private Runnable percentageFunc; // a function that updates the percentage of the completed process private Runnable finishFunc; // a function that is called after the finishing private double percentage; private boolean suspended; private boolean stopped; public PrimeNumbers(Runnable addFunc, Runnable percentageFunc, Runnable finishFunc) { this.displayFunc = addFunc; this.percentageFunc = percentageFunc; this.finishFunc = finishFunc; } public int getTo() { return to; } public void setTo(int to) { this.to = to; } public synchronized int getLastFound() { return lastFound; } private synchronized void setLastFound(int lastFound) { this.lastFound = lastFound; } public synchronized double getPercentage() { return percentage; } private synchronized void setPercentage(double percentage) { this.percentage = percentage; } public synchronized boolean isSuspended() { return suspended; } private synchronized void setSuspended(boolean suspended) { this.suspended = suspended; } public synchronized boolean isStopped() { return stopped; } private synchronized void setStopped(boolean stopped) { this.stopped = stopped; } @Override public void run() { for (int n = 2; n <= to; n++) { try { setPercentage(n * 1.0 / to); // Update the percentage: if (percentageFunc != null) { Platform.runLater(percentageFunc); } boolean prime = true; for (int i = 2; i * i <= n; i++) { if (n % i == 0) { prime = false; break; } } Thread.sleep(20); if (prime) { setLastFound(n); // Display prime number found: if (displayFunc != null) { displayFunc.run(); } } } catch (InterruptedException e) { // Depending on the state of the object, // wait for the continuation or complete the search: while (isSuspended()) { try { Thread.sleep(100); } catch (InterruptedException e1) { // Interrupted by waiting: if (isStopped()) { break; } } } if (isStopped()) { break; } } } if (finishFunc != null) { Platform.runLater(finishFunc); } } public void start() { primesThread = new Thread(this); setSuspended(false); setStopped(false); primesThread.start(); } public void suspend() { setSuspended(true); primesThread.interrupt(); } public void resume() { setSuspended(false); primesThread.interrupt(); } public void stop() { setStopped(true); primesThread.interrupt(); } }
As you can see from the code, data access methods are implemented with the synchronized
modifier, which prevents simultaneous writing and reading of partially completed data from different threads. Therefore, all references to class fields from its methods are carried out only through the access functions.
Now we can implement the controller code:
package ua.inf.iwanoff.oop.fifth; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.*; public class PrimeAppController { @FXML private TextField textFieldTo; @FXML private Button buttonStart; @FXML private Button buttonSuspend; @FXML private Button buttonResume; @FXML private Button buttonStop; @FXML private ProgressBar progressBar; @FXML private TextArea textAreaResults; private PrimeNumbers primeNumbers = new PrimeNumbers(this::addToTextArea, this::setProgress, this::finish); @FXML private void startClick(ActionEvent actionEvent) { try { primeNumbers.setTo(Integer.parseInt(textFieldTo.getText())); textAreaResults.setText(""); progressBar.setProgress(0); buttonStart.setDisable(true); buttonSuspend.setDisable(false); buttonResume.setDisable(true); buttonStop.setDisable(false); primeNumbers.start(); } catch (NumberFormatException e) { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle("Error"); alert.setHeaderText("Wrong range!"); alert.showAndWait(); } } @FXML private void suspendClick(ActionEvent actionEvent) { primeNumbers.suspend(); buttonStart.setDisable(true); buttonSuspend.setDisable(true); buttonResume.setDisable(false); buttonStop.setDisable(false); } @FXML private void resumeClick(ActionEvent actionEvent) { primeNumbers.resume(); buttonStart.setDisable(true); buttonSuspend.setDisable(false); buttonResume.setDisable(true); buttonStop.setDisable(false); } @FXML private void stopClick(ActionEvent actionEvent) { primeNumbers.stop(); } private void addToTextArea() { textAreaResults.setText(textAreaResults.getText() + primeNumbers.getLastFound() + " "); } private void setProgress() { progressBar.setProgress(primeNumbers.getPercentage()); } private void finish() { buttonStart.setDisable(false); buttonSuspend.setDisable(true); buttonResume.setDisable(true); buttonStop.setDisable(true); } }
The addToTextArea()
, setProgress()
and finish()
methods are not intended ffor direct call from the controller, but for a callback. References to these functions are passed to the PrimeNumbers
constructor. Event handlers call up the appropriate PrimeNumbers
class methods and change the accessibility of the buttons.
4 Exercises
- Create a console application in which the user enters the class name and obtains information about all public fields of this class.
- Create a function that calculates the square root, if possible, and returns an
OptionalDouble
object. - Create a program in which all positive integers of the specified range are printed, if the sum of digits is equal to the specified value. Use data streams. Do not use explicit loops.
5 Quiz
- What is the essence of RTTI?
- Why use the keyword
instanceof
? - Give a definition of reflection.
- What information is stored in the
class
field? - How to load classes in Java?
- What are the different types of class loaders?
- How to load a class by name?
- What are custom loaders for?
- How to get information about class methods?
- How to call a method by name?
- How to call a static method?
- How to get information about class fields?
- How to include JavaScript code into Java code?
- How to compile source code programmatically?
- Give a definition of the process and the thread.
- What are the benefits of using threads?
- What are the ways to create threads in Java?
- What are the states of the thread object?
- When threads are terminated or suspended?
- What means are used for threads synchronization?
- In which cases is the thread blocked?
- What is the usage of the
synchronized
modifier? - Which container classes are thread-safe?
- What is the usage of
Optional
class? - Is it possible to use the
Optional
class with primitive types? - What are the additional capabilities of working with standard containers provided in Java 8?
- What are the benefits and features of the Stream API?
- How to get a stream from the collection?
- How to get a stream from an array?
- What is the difference between intermediate and terminal operations?