Лабораторна робота 5
Метапрограмування. Паралельне та декларативне програмування
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
Створити програму графічного інтерфейсу користувача, яка призначена для побудови графіку довільних функцій. Користувач повинен увести дійсні значення a і b, функції f(x) і g(x) у вигляді рядків, які відповідають синтаксису Java. У програмі здійснюється обчислення функції h(x) відповідно до індивідуального завдання:
Номери варіантів |
Функція h(x) |
Номери варіантів |
Функція 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) |
Після введення необхідних функцій, діапазону відображення графіку і натиснення відповідної кнопки здійснюється побудова графіку. Слід також передбачити функцію очищення рядків уведення й графіку.
Для програмної реалізації обробки введених виразів слід застосувати динамічну компіляцію коду. Для створення застосунку графічного інтерфейсу користувача слід використати засоби JavaFX. Рекомендований підхід - використання компоненту LineChart
.
1.2 Перегляд всіх полів класу
Створити консольний застосунок, в якому користувач вводить ім'я класу і отримує інформацію про всі поля цього класу (включаючи закриті і захищені).
1.3 Створення застосунку графічного інтерфейсу користувача для отримання простих множників чисел
За допомогою засобів JavaFX розробити застосунок графічного інтерфейсу користувача, в якому користувач вводить діапазон чисел (від і до), а у вікні відображаються числа і їх прості множники. Реалізувати можливість призупинення, відновлення потоку, а також повного припинення і повторного обчислення з новими даними.
1.4 Робота з BlockingQueue
Створити консольну програму, в якій один потік виконання додає цілі числа до черги BlockingQueue
,а інший обчислює їх середнє арифметичне.
1.5 Виклик функції для обраного класу (додаткове завдання)
Створити класи з однойменними методами. Вибрати клас за ім'ям і викликати його метод.
1.6 Інтерпретація математичних виразів (додаткове завдання)
Створити консольний застосунок, який дозволяє вводити математичні вирази, обчислювати і виводити результат. Вираз може складатися з констант, математичних операцій і дужок. Для реалізації використовувати засоби пакету javax.script
.
Примітка. Синтаксис математичних виразів JavaScript аналогічний Java. Результат можна виводити за допомогою функції print()
без створення додаткових змінних.
1.7 Обчислення π в окремому потоці виконання (додаткове завдання)
Реалізувати програму обчислення π с точністю до заданого ε як суму послідовності:
Обчислення здійснювати в окремому потоці виконання. Під час виконання обчислення надавати користувачеві можливість уводити запит про кількість обчислених елементів суми.
2 Методичні вказівки
2.1 Використання RTTI
Механізм ідентифікації типу часу виконання (RTTI – run-time type identification) дозволяє визначити тип об'єкта під час виконання програми. Механізм RTTI підтримує більшість мов об'єктно-орієнтованого програмування. Цей механізм, як зазвичай, застосовується до так званих поліморфних типів (класів або структур, які надають віртуальні функції). RTTI дозволяє під час виконання програми визначити точний тип об'єкта за посиланням (або вказівником для мов, що підтримує вказівники). Завдяки розширенню правил сумісності типів посилання на базовий тип може фактично вказувати на об'єкти похідних типів, що є основою поліморфізму. У деяких випадках необхідно явно отримати реальний тип об'єкта, або перевірити, чи ми маємо справу з об'єктом типу, що нас цікавить.
У Java всі типи-посилання є поліморфними. RTTI реалізований через інформацію про тип, який зберігається в кожному об'єкті. Ключове слово instanceof
дозволяє перевірити, чи є об'єкт екземпляром конкретного типу. Вираз
об'єкт instanceof клас
повертає значення типу boolean
, яке може бути використане перед викликом методів, характерних для конкретного класу:
if(x instanceof SomeClass) ((SomeClass)x).someMethod();
Без такої перевірки приведення може завершиться генерацією винятку ClassCastException
.
У наведеному нижче прикладі залежно від числа, введеного користувачем, створюються об'єкти різних типів:
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 Використання завантажувачів класів
Однією з основних особливостей платформи Java є модель динамічного завантаження класів. Завантажувачі класів – це спеціальні об'єкти. які забезпечують завантаження коду класів під час роботи програми.
Динамічне завантаження класів в Java має ряд особливостей:
- відкладене (lazy) завантаження – класи завантажуються тільки в разі потреби, що дозволяє заощаджувати ресурси і поліпшити розподіл навантаження;
- контроль коректності коду (використання типів) проводиться під час завантаження класу, а не під час виконання коду;
- можливість створення користувацьких завантажувачів, які повністю контролюють процес отримання запитаного класу;
- можливість контролювати надійність і безпеку коду через так звані атрибути безпеки.
З кожним завантажувачем пов'язаний свій простір імен для створюваних класів. Класи, створені двома різними завантажувачами на основі загального байт-коду, в системі будуть відрізнятися.
Під час завантаження класу створюється об'єкт типу java.lang.Class
. Тип Class
– це клас, екземпляри якого представляють класи, інтерфейси та інші типи під час виконання Java-програми. Примітивні типи Java (boolean
, byte
, char
, short
, int
, long
, float
і double
) і void
також представлені у вигляді об'єктів типу Class
. Об'єкт типу Class
може бути отриманий для існуючих типів (в тому числі і примітивних) з використанням ключового слова class
:
Class<?> c1 = String.class; Class<?> c2 = double.class;
Як видно з попереднього прикладу, клас Class
– узагальнений. Найбільш часто його використовують з параметром <?>
(значення типу визначається автоматично).
Можна також отримати об'єкт Class
для раніше створеного об'єкта типу посилання:
Integer i = new Integer(100); Class<?> c3 = i.getClass();
Об'єкт типу Class
дозволяє отримати різноманітну інформацію про тип, яка може бути використана під час виконання програми і є основою рефлексії. Ці можливості будуть розглянуті нижче.
Далі термін "завантаження класів" передбачатиме завантаження різних типів.
Розрізняють три види завантажувачів у Java:
- базовий завантажувач (bootstrap) – завантажує базові (системні) класи, які зазвичай знаходяться в jar-файлах в директорії
jre/lib
; базовий завантажувач не доступний програмно; - завантажувач розширень (Extension Classloader) – завантажує класи різних пакетів розширень, які зазвичай містяться в
jre/lib/ext
; - системний завантажувач (System Classloader) – реалізує стандартний алгоритм завантаження з каталогів і JAR-файлів, перелічених у змінній
CLASSPATH
.
Абстрактний клас java.lang.ClassLoader
використовується для створення типів об'єктів, що відповідають за завантаження класів. Кожен об'єкт типу Class
містить посилання на ClassLoader
. Для кожного класу визначений поточний завантажувач. Для того, щоб отримати завантажувач, яким був завантажений клас SomeClass
, необхідно скористатися методом SomeClass.class.getClassLoader()
. Java дозволяє створювати користувацькі завантажувачі.
Під час завантаження класів використовують спеціальну ієрархію об'єктів-завантажувачів (базовий завантажувач / завантажувач розширень / системний завантажувач). Алгоритм завантаження зазвичай такий:
- здійснюється пошук в списку раніше завантажених класів (щоб клас не був завантажений двічі);
- якщо клас не був завантажений, здійснюється делегування батьківському завантажувачу;
- якщо батьківський завантажувач не зміг завантажити запитаний клас, поточний завантажувач намагається сам завантажити необхідний клас.
При цьому слід врахувати, що всі "системні" класи (java.lang.Object
, java.lang.String
тощо) завантажуються тільки базовим завантажувачем. Для них getClassLoader()
повертає null
.
2.3 Рефлексія
Розглянутий раніше механізм RTTI базується на інформації, яка може бути отримана під час компіляції. Поряд з RTTI, Java надає більш складний і потужний механізм – рефлексію. Цей механізм використовує інформацію, яка може бути недоступна під час компіляції.
Відображення або рефлексія (reflection) – це механізм, який дозволяє програмі відстежувати й модифікувати власну структуру та поведінку під час виконання.
Безпосередня інформація про тип зберігається в об'єкті типу java.lang.Class
. Пакет java.lang.reflect
містить класи, які надають інформацію про класи та об'єкти та забезпечують доступ до елементів класів та інтерфейсів.
За допомогою можливостей типу Class
можна створити об'єкт класу з певним ім'ям. Для одержання об'єкта типу Class
використовується статичний метод forName()
.
String name = "SomeClass"; Class c = Class.forName(name);
У наведеному нижче прикладі ім'я необхідного класу вводиться з клавіатури, після чого створюється об'єкт за допомогою функції newInstance()
:
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(); } } }
Об'єкт типу Class
містить, зокрема, інформацію про ім'я типу, що у наведеному вище прикладі можна було б одержати в такий спосіб:
System.out.println(o.getClass().getName());
Інформація про типи може бути представлена в зручному вигляді за допомогою функцій getSimpleName()
або getCanonicalName()
.
Об'єкт типу Class
може бути також отриманий для інтерфейсів і навіть для безіменних класів. Метод isInterface()
для інтерфейсів повертає true
, а метод isAnonymousClass()
повертає true
для безіменних класів. Можна також отримати масив вкладених типів, клас, в який вкладено даний тощо.
Метод getSuperclass()
повертає базовий клас. Для класу Object
цей метод повертає null
. Метод getInterfaces()
повертає масив реалізованих інтерфейсів (також об'єктів типу Class
).
Можна отримати список методів класу - масив об'єктів класу java.lang.reflect.Method
. Цей масив буде містити посилання на відкриті методи даного класу і всіх його предків:
for (int i = 0; i < o.getClass().getMethods().length; i++) { System.out.println(o.getClass().getMethods()[i].getName()); }
Функція getMethod()
дозволяє отримати метод за ім'ям.
Функція getDeclaredMethods()
дозволяє отримати список всіх методів (відкритих, захищених, пакетних і закритих), але тільки визначених у цьому класі (а не в базових класах). Аналогічно викликають функцію getDeclaredMethod()
, яка дозволяє отримати будь-який метод за ім'ям.
Для методу можна отримати інформацію про клас, в якому він оголошений (getDeclaringClass()
), про ім'я (getName()
), тип результату (getReturnType()
). Можна також отримати масив типів параметрів (об'єктів типу Class<?>
) за допомогою методу getParameterTypes()
. Масив, що отримують за допомогою функції getExceptionTypes()
, містить типи можливих винятків. Перевизначена функція toString()
повертає повну сигнатуру методу зі списком винятків.
Інформація про модифікатори методу може бути отримана за допомогою функції getModifiers()
. Ця функція повертає ціле число, яке може бути використано, наприклад, для функції charAt()
класу String
:
Class<?> c = String.class; String methodName = "charAt"; Method m = c.getDeclaredMethod(methodName, int.class); // Визначаємо метод з цілим параметром 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 "); }
Метод можна викликати за допомогою функції invoke()
. Перший параметр цього методу – посилання на об'єкт класу, для якого викликають метод. Для статичних методів – це null
. Далі вказують необхідні фактичні параметри. Оскільки кілька методів може відрізнятися тільки списком параметрів, під час виклику getMethod()
крім імені обов'язково потрібно вказувати типи параметрів (масив об'єктів типу Class
). Якщо список параметрів порожній, цей масив можна опустити.
Наприклад, так можна викликати статичну функцію без параметрів для класу, заданого ім'ям:
method = Class.forName(className).getMethod(methodName); method.invoke(null);
У наведеному нижче прикладі здійснюється виклик нестатичного методу:
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)); } }
Отримання інформації про поля здійснюється аналогічно. Клас java.lang.reflect.Field
представляє дані про поле класу – імені (getName()
), типу (getType()
), клас, в якому визначено поле (getDeclaringClass()
), модифікатори доступу (getModifiers()
) тощо. Для отримання значення, яке зберігається в поле, використовують метод get()
, який повертає результат типу Object
. За допомогою методу set()
можна встановити нове значення.
Якщо ми хочемо отримати доступ до закритих або захищених полів, але визначених саме в цьому класі, використовують методи getDeclaredField()
(за ім'ям) і getDeclaredFields()
(для отримання масиву полів).
Через механізм рефлексії можна обійти обмеження доступу до елементів класу. Закритий метод, отриманий за допомогою getDeclaredMethod()
або як елемент масиву getDeclaredMethods()
, не може бути безпосередньо викликаний. Однак для його виклику можна дозволити доступ за допомогою функції setAccessible()
з параметром true
. Так само можна дістатись закритих полів. Наведений нижче приклад демонструє можливості роботи з закритими елементами класу:
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("Закритий метод. k = " + k); } } public class AccessTest { public static void main(String[] args) throws Exception { // Отримуємо клас: Class<?> c = Class.forName("ua.inf.iwanoff.oop.fifth.TestClass"); // Створюємо об'єкт: Object obj = c.newInstance(); // Доступ до закритого поля: Field f = c.getDeclaredField("k"); f.setAccessible(true); f.set(obj, 1); // Доступ до закритого методу: Method m = c.getDeclaredMethod("testPrivate"); m.setAccessible(true); m.invoke(obj); } }
Механізм рефлексії слід застосовувати з обережністю, оскільки він надає можливості обходу інкапсуляції.
2.4 Використання метапрограмування
2.4.1 Загальні концепції. Засоби виконання сценаріїв
Метапрограмування – це процес створення програм, які породжують інші програми. Найважливішим елементом метапрограмування є автоматична генерація коду. Можна запропонувати два підходи – з використанням скриптових мов і з використанням компіляції "на льоту".
Багато мов сценаріїв і динамічно типізованих мов дозволяють генерувати байт-код Java, так що програми можуть виконуватися на платформі Java, так само як програми Java. Засоби пакету javax.script
, доданого в Java SE 6, дозволяють інтерпретувати вирази, написані на скриптових мовах (AppleScript, Groovy, JavaScript, Jelly, PHP, Python, Ruby тощо). Наприклад, таким чином можна інтерпретувати код, написаний на 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')"); } }
Як видно з прикладу, клас ScriptEngineManager
дозволяє створити движок JavaScript, а екземпляр класу ScriptEngine
здійснює інтерпретацію виразів. Вираз, або його частина, може бути прочитано з потоку введення, наприклад, з клавіатури.
Клас ScriptEngine
також дозволяє створювати змінні скриптової мови (метод put()
), викликати її функції (метод invokeFunction()
) тощо.
Для передачі інформації з Java в скрипт використовується інтерфейс зв'язування Bindings
:
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 Динамічна генерація коду
Однією з можливостей Java є виклик Java-компілятора у вихідному коді. З цією метою використовують пакет javax.tools
. Клас javax.tools.JavaCompiler
забезпечує компіляцію зазначеного вихідного коду в файл .class
. Створити екземпляр JavaCompiler
можна за допомогою фабричного методу getSystemJavaCompiler()
класу javax.tools.ToolProvider
:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
Припустимо, в каталозі c:\java
є підкаталог (пакет) test
, а в ньому – файл Test.java
:
package test; public class Test { public static void main(String[] args) { System.out.println("Hello, Dynamic Compiling!"); } }
За допомогою виклику функції run()
об'єкта compiler
можна здійснити компіляцію вихідного коду. Перші три параметри функції run()
– відповідно потоки введення, виведення даних і виведення помилок. Ці потоки використовуються у процесі компіляції. Якщо значення параметрів встановити null
, будуть використовуватися відповідно System.in
, System.out
і System.err
.У четвертому і подальших параметрах вказуються аргументи і опції компіляції. У найпростішому випадку необхідно тільки вказати повне ім'я файлу з вихідним кодом. Наприклад:
compiler.run(null, null, null, "c:/java/test/Test.java");
Тепер скомпільовану програму можна завантажити на виконання. Для того, щоб завантажити клас, не зазначений у змінній CLASSPATH
, необхідно використовувати спеціальний завантажувач класів – java.net.URLClassLoader
. Параметр конструктора цього класу – масив об'єктів класу URL
. Клас URL
представляє уніфікований визначник місцезнаходження ресурсу (Uniform Resource Locator), вказівник на "ресурс" у World Wide Web. Ресурс може бути чимось простим, типу файлу або каталогу, або це може бути посилання на більш складний об'єкт, такий як запит до бази даних або до пошукової системи. У нашому випадку такий масив буде складатися з одного елемента. Отримати такий елемент можна з об'єкта типу java.net.URI
(уніфікований ідентифікатор ресурсів, Uniform Resource Identifier), який, у свою чергу, може бути отриманий з об'єкта java.io.File
:
URLClassLoader classLoader = new URLClassLoader( new URL[] { new File("c:/java").toURI().toURL() });
Отриманий об'єкт classLoader
можна використовувати для завантаження класу:
Class<?> cls = Class.forName("test.Test", true, classLoader);
Викликаємо метод main()
:
Method m = cls.getMethod("main", String[].class); m.invoke(null, new Object[] { new String[] { } });
Примітка: як видно з прикладу, для того, щоб передати параметр-масив, треба зробити інший масив типу Object
з масивів.
У більш складному випадку крім класу JavaCompiler
необхідно залучати інші засоби пакету javax.tools
. Зокрема:
JavaCompiler.CompilationTask
– клас, який представляє завдання компіляції; виконання компіляції здійснюється методомcall()
;JavaFileManager
управляє читанням і записом в файли при компіляції;JavaFileObject
– файловий об'єкт, який дозволяє абстрагуватися від вихідного коду Java і файлів класів;DiagnosticListener
– слухач діагностичних подій компіляції.
Якщо сирцевий код береться не з файлової системи, то необхідно створити клас, який реалізує інтерфейс JavaFileObject
. Java надає просту реалізацію у вигляді класу SimpleJavaFileObject
. Можна створити похідний клас і перекрити методи відповідно до необхідності.
2.5 Робота з потоками виконання
2.5.1 Загальні концепції
Процес (process) – це екземпляр комп'ютерної програми, яка завантажена в пам'ять виконується. В сучасних операційних системах процес може виконуватися паралельно з іншими процесами. Процесу виділяється окремий адресний простір, причому цей простір фізично недоступний для інших процесів.
Нитка (thread), або потік виконання (потік управління) - це окрема підзадача, яка може виконуватися паралельно з іншими підзадачами (нитками) в межах одного процесу. Кожен процес містить як мінімум один потік виконання, іменований головним (main thread). Всі потоки виконання, створені процесом, виконуються в адресному просторі цього процесу і мають доступ до ресурсів процесу. Створення потоків виконання – істотно менше ресурсномістка операція, ніж створення нових процесів. Потоки виконання іноді називають легковажними процесами (lightweight processes).
Якщо процес створив кілька потоків, то всі вони виконуються паралельно, при чому час центрального процесора (або декількох центральних процесорів в мультипроцесорних системах) розподіляється між цими потоками. Розподілом часу центрального процесора займається спеціальний модуль операційної системи - планувальник (scheduler). Планувальник по черзі передає управління окремим потокам, так що навіть в однопроцесорній системі створюється ілюзія паралельної роботи запущених потоків. Розподіл часу виконується за перериваннями системного таймера.
Потоки виконання використовують для реалізації незалежних підзадач у межах одного процесу з метою реалізації фонових процесів, моделювання паралельного виконання певних дій або підвищення зручності користувацького інтерфейсу.
2.5.2 Низькорівневі засоби роботи з потокам виконання
Є два підходи до створення об'єкта-нитки з використанням класу Thread
:
- успадкування нового класу від класу
java.lang.Thread
і створення об'єкта нового класу; - створення класу, який реалізує інтерфейс
java.lang.Runnable
; об'єкт такого класу передається конструктору класуjava.lang.Thread
.
Під час створення похідного класу від Thread
необхідно перекрити його метод run()
. Після створення об'єкта-потоку його треба запустити за допомогою методу start()
. Цей метод здійснює певну ініціалізацій роботу і викликає run()
. У наведеному нижче прикладі створюється окремий потік виконання, який здійснює виведення чергового цілого числа від 1 до 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); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } } public static void main(String[] args) { new ThreadTest().start(); // Сюди можна додати дії, що виконуються паралельно з методом run() } }
Виклик методу sleep()
зумовлює припинення потоку на вказану кількість мілісекунд. Виклик методу sleep()
вимагає перехоплення винятку InterruptedException
, який генерується в разі переривання цього потоку виконання іншим потоком. Аналогічний приклад зі створенням об'єкта класу, що реалізує інтерфейс Runnable
. У цьому випадку також необхідно запустити потік за допомогою методу start()
:
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(); // Сюди можна додати дії, що виконуються паралельно з методом run() } }
Другий підхід є кращим, так як в цьому випадку ми вільні у виборі базового класу.
Окремим потокам можна давати імена (другий параметр конструктора) і отримувати ці імена за допомогою функції getName()
.
Будь який потік виконання може знаходитися в декількох стандартних станах. Стан "новий" (Thread.State.NEW
) потік отримує, коли створюється об'єкт потоку. Виклик методу start()
переводить потік зі стану "новий" в стан "працює" (Thread.State.RUNNABLE
). Існують стани "заблокований" (Thread.State.BLOCKED
), "заблокований за часом", або "в режимі очікування" (Thread.State.TIMED_WAITING
), "очікує", або "непрацездатний" (Thread.State.WAITING
) і "завершений" (Thread.State.TERMINATED
). Під час створення потоку він отримує стан "новий" і не виконується. Отримати значення стану потоку можна викликом методу getState()
. Наведений нижче приклад демонструє деякі стани потоку в межах його життєвого циклу:
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(); // перериваємо потік Thread.sleep(100); // потрібен час для завершення потоку System.out.println(testThread.getState()); // TERMINATED } }
Метод wait(long timeout)
, як і метод sleep(long timeout),
дозволяє призупинити роботу потоку на вказану кількість мілісекунд. Під час виконання цього методу також може бути згенеровано виняток InterruptedException
. Застосування цих методів дозволяє перевести потік в режим очікування (TIMED_WAITING
). На відміну від sleep()
, працездатність після виклику методу wait()
можна відновити методами notify()
або notifyAll()
.
Метод wait()
може бути викликаний без параметрів. Потік при цьому переходить в стан "непрацездатний" (WAITING
).
Менш надійним альтернативним способом припинення роботи потоку є виклик методу yield(),
який повинен призводити до припинення потоку на деякий квант часу, для того щоб інші потоки могли виконувати свої дії.
Після завершення виконання методу run()
потік закінчує свою роботу, потік переходить в пасивний стан (TERMINATED
).
У ранніх версіях Java для примусової зупинки, а також для тимчасового призупинення роботи потоку з подальшим відновленням передбачалося використання методів класу Thread
- stop()
, suspend()
і resume()
. Методи stop()
, suspend()
і resume()
вважаються небажаними для використання (deprecated-методи), оскільки вони провокують створення ненадійного коду, який важко зневаджувати. Крім того, використання suspend()
і resume()
може спровокувати взаємні блокування (deadlock).
Перервати потік, який знаходиться в стані очікування (Thread.State.TIMED_WAITING
), можна за допомогою виклику функції interrupt()
. В цьому випадку генерується виняток InterruptedException
. Можна модифікувати попередній приклад так, щоб він дозволяв перервати потік виконання:
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("Потік перерваний."); break; } System.out.println(counter); } } @SuppressWarnings("resource") public static void main(String[] args) { Thread thread = new Thread(new InterruptTest()); thread.start(); System.out.println("Для переривання натисніть Enter"); new Scanner(System.in).nextLine(); thread.interrupt(); } }
Метод join()
дозволяє одному потоку дочекатися завершення іншого. Виклик цього методу всередині потоку t1
для потоку t2
призведе до припинення поточного потоку (t1
) до завершення t2
, як показано в наведеному нижче прикладі:
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("Потік First запущений."); Thread.sleep(1000); System.out.println("Основна робота потоку First завершена."); t2.join(); System.out.println("Потік First завершено."); } catch (InterruptedException e) { e.printStackTrace(); } } } static class SecondThread implements Runnable { @Override public void run() { try { System.out.println("Потік Second запущений."); Thread.sleep(3000); System.out.println("Потік Second завершено."); } 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(); } }
Порядок виведення повідомлень буде таким:
Потік First запущений. Потік Second запущений. Основна робота потоку First завершена. Потік Second завершено. Потік First завершено.
Як і sleep()
, join()
реагує на переривання, отже, необхідно перехоплювати виняток InterruptedException
.
Можна створити потік виконання, робота якого переривається при завершенні потоку, який його породив. Для того, щоб створити такий потік, викликається метод setDaemon setDaemon()
з параметром true
. Метод setDaemon()
необхідно викликати після створення потоку, але до моменту його запуску, тобто перед викликом методу start()
.За допомогою методу isDaemon()
можна перевірити, є потік демоном, чи ні. Якщо потік-демон створює інші потоки, то вони також отримають статус потоку-демона.
package ua.inf.iwanoff.oop.fifth; public class Th extends Thread { public void run() { try { if (isDaemon()) { System.out.println("старт потоку-демона"); sleep(1000); } else { System.out.println("старт звичайного потоку"); } } catch (InterruptedException e) { System.err.print("Error" + e); } finally { if (!isDaemon()) System.out.println("завершення звичайного потоку"); else System.out.println("завершення потоку-демона"); } } public static void main(String[] args) { Th usual = new Th(); Th daemon = new Th(); daemon.setDaemon(true); daemon.start(); usual.start(); System.out.println("останній рядок main"); } }
Після компіляції і запуску, можливо, буде виведено:
старт потоку-демона останній рядок main старт звичайного потоку завершення звичайного потоку
Потік-демон (через виклик методу sleep(10000)
) не встиг завершити виконання свого коду до завершення основного потоку застосунку, пов'язаного з методом main()
. Базова властивість потоків-демонів полягає в можливості основного потоку застосунку завершити виконання з закінченням коду методу main()
, не звертаючи уваги на те, що потік-демон ще працює. Якщо поставити час затримки також для потоку main()
, то потік-демон може встигнути завершити своє виконання до закінчення роботи основного потоку:
старт потоку-демона старт звичайного потоку завершення звичайного потоку завершення потоку-демона останній рядок main
Якщо запущено декілька потоків виконання, потрібен спосіб для виключення можливості використання одного ресурсу двома потоками. Якщо елементи даних класу оголошені як private
і доступ до цієї області пам'яті можливий тільки за допомогою методів, то можна уникнути колізій, оголосивши ці методи як synchronized
. Одночасно тільки один процес може викликати synchronized
метод для певного об'єкта. Такий метод або фрагмент коду іменується критичної секцією.
Для забезпечення доступу до такого коду використовується концепція монітора. Під монітором розуміють деякий об'єкт, що забезпечує блокування коду під час виконання його деяким потоком. Якщо ключове слово synchronized
розташоване перед заголовком функції, монітором є об'єкт, для якого викликаний цей метод (this
). Після того, як викликаний хоча б один синхронізований метод, блокування поточним об'єктом поширюється на всі методи з модифікатором synchronized
.
Розглянемо такий приклад. Клас Adder
дозволяє додавати цілі числа до деякого накопичувача. Клас AdderThread
реалізує потік виконання, що забезпечує додавання послідовних п'яти цілих значень. У функції main()
класу AdderTest
створюємо два потоки і запускаємо їх:
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(); } }
Завантажена декілька разів на виконання програма буде продукувати різні проміжні результати, що пов'язано з некоректною роботою двох потоків з одним об'єктом. Для виправлення ситуації перед методом add()
слід додати модифікатор synchronized
:
synchronized public void add(long value) { this.sum += value; System.out.println(Thread.currentThread().getName() + " " + sum); }
Іноді оголошувати весь метод як synchronized
незручно, оскільки одночасно інші нитки могли б виконувати інші заблоковані методи. Для вирішення даної проблеми використовують блок, що починається зі слова synchronized
і містить після цього слова в дужках ім'я об'єкта, який відповідає за блокування. Монітором може бути будь-який об'єкт, похідний від java.lang.Object
, що підтримує необхідні для цього методи. У наведеному нижче прикладі створюється спеціальний об'єкт для блокування:
public void add(long value) { Object lock = new Object(); synchronized (lock) { this.sum += value; System.out.println(Thread.currentThread().getName() + " " + sum); } }
Синхронізації не вимагають процеси запису і читання об'єктів, розміри яких не перевищують 32 біт. Такі об'єкти називаються атомарними.
Під час обміну даних через поля слід використовувати ключове слово volatile
перед описом відповідного поля. Це пов'язано з наявністю механізму кешування даних окремо для кожного потоку, і значення з цього кешу може відрізнятися для кожного з них. Оголошення поля з ключовим словом volatile
відключає для нього таке кешування:
volatile long sum = 0;
Метод wait()
, викликаний всередині синхронізованого блоку або методу, зупиняє виконання поточного потоку і звільняє від блокування захоплений об'єкт, зокрема об'єкт lock
. Повернути блокування об'єкта потоку можна викликом методу notify()
для конкретного потоку або notifyAll()
для всіх потоків. Виклик може бути здійснений тільки з іншого потоку, який заблокував, в свою чергу, зазначений об'єкт.
За допомогою методу setPriority()
можна змінювати пріоритети потоків. Не рекомендується встановлювати вищий пріоритет (константа Thread.MAX_PRIORITY
).
2.5.3 Використання потоків виконання у JavaFX-застосунках
Робота застосунків графічного інтерфейсу користувача зазвичай пов'язана з потоками виконання. Після завантаження JavaFX-застосунку на виконання автоматично створюється потік застосунку (application thread), в якому здійснюється обробка подій. Тільки цей потік може мати взаємодію з вікнами й візуальними компонентами. Спроба двох потоків одночасно керувати зовнішнім виглядом компонентів та інформацією у вікні може призвести до хаотичного вигляду й непередбачуваної поведінки візуальних компонентів. Разом з тим, саме завдяки багатопотоковості можна забезпечити керованість застосунком. Крім того, доцільно деякі дії, які виконуються тривалий час, запускати в окремому потоці виконання, що забезпечить можливість відслідковувати процес, призупиняти й продовжувати його, змінювати налаштування тощо.
Засоби JavaFX надають можливість виконувати код в потоці виконання, який відповідає за отримання і обробку подій. Метод javafx.application.Platform.runLater()
отримує об'єкт, який реалізує функціональний інтерфейс Runnable
. Цей об'єкт отримує потік обробки подій, де об'єкт стає в чергу і, коли виникає можливість, виконується його метод run()
. Саме в цьому методі доцільно розташувати взаємодію з візуальними компонентами. Приклад 3.6 демонструє можливість виклику цього методу з дочірнього потоку.
Існують також високорівневі механізми роботи з потоками виконання у JavaFX, представлені в пакеті javafx.concurrent
: абстрактний клас Task, похідний від java.util.concurrent.FutureTask
, в свою черг передбачає створення похідного класу, який реалізує метод call(), в якому реалізована взаємодія з візуальними компонентами. Крім того, цей клас надає можливість безпосереднього зв'язування деяких компонентів (наприклад, ProgressBar
) з задачею (Task
).
2.6 Використання колекцій, безпечних з точки зору багатопотоковості
2.6.1 Використання синхронізованих аналогів стандартних колекцій
Засобами класу java.util.Collections
можна отримати безпечні з точки зору багатопотоковості клони існуючих колекцій. Це такі статичні функції, як synchronizedCollection()
, synchronizedList()
, synchronizedMap()
, synchronizedSet()
, synchronizedSortedMap()
і synchronizedSortedSet()
. Кожна з цих функцій приймає як параметр відповідну несинхронізовану колекцію і повертає колекцію такого ж типу, всі методи якої (крім ітераторів) визначені як synchronized
. Відповідно всі операції, включаючи читання, використовують блокування, що знижує ефективність роботи і обмежує використання відповідних колекцій.
Ітератори стандартних колекцій, реалізованих у java.util
, реалізують так звану fail-fast поведінку: у випадку зміни об'єкта в процесі обходу колекції ітератором генерується виняток ConcurrentModificationException
, причому в умовах багатопотоковості поведінка ітератора може бути непередбачуваною - виняток може бути згенеровано не відразу або не згенеровано взагалі. Надійну і роботу з елементами колекції забезпечують спеціальні контейнерні класи, визначені в java.util.concurrent
. Це такі типи, як CopyOnWriteArrayList
, CopyOnWriteArraySet
, ConcurrentMap
(інтерфейс), ConcurrentHashMap
(реалізація), ConcurrentNavigableMap
(интерфейс) і ConcurrentSkipListMap
(інтерфейс).
Клас CopyOnWriteArrayList
, який реалізує інтерфейс List
, для всіх операцій зміни значення елементів створює копію масиву. Ітератор також працює зі своєю копією. Така поведінка ітератора має назву fail-safe поведінки.
Класи ConcurrentHashMap
і ConcurrentSkipListMap
забезпечують роботу з асоціативними масивами, яка не потребує блокування всіх операцій. Клас ConcurrentHashMap
організовує структуру даних, розділену на сегменти. Блокування здійснюється на рівні одного сегмента. За рахунок поліпшеного хеш-функції ймовірність звернення двох ниток до одного сегмента істотно знижується. Клас ConcurrentSkipListMap
реалізує зберігання з використанням спеціальної структури даних - skip list (Список з пропусками). Елементи асоціативного масиву впорядковані за ключами. У прикладі 2.2 продемонстровано роботу з ConcurrentSkipListMap
.
2.6.2 Робота зі спеціальними колекціями, які використовують блокування
Пакет java.util.concurrent
надає набір колекцій, безпечних з точки зору багатопотоковості. Це такі узагальнені типи, як BlockingQueue
(інтерфейс) і його стандартні реалізації (ArrayBlockingQueue
, LinkedBlockingQueue
, PriorityBlockingQueue
, SynchronousQueue
, DelayQueue
), похідні від нього інтерфейси TransferQueue
(реалізація LinkedTransferQueue
) і LinkedBlockingDeque
(стандартна реалізація – LinkedBlockingDeque
). Колекції реалізовані в пакеті java.util.concurrent
.
Інтерфейс BlockingQueue
представляє чергу, яка дозволяє додавати і вилучати елементи безпечно з точки зору потоків. Типове використання BlockingQueue
– занесення об'єктів одним потоком, а вилучення – іншим.
Потік, який додає об'єкти (перший потік), продовжує додавати їх в чергу, доки не буде досягнута деяка верхня межа припустимої кількості елементів. У цьому випадку під час спроби додати нові об'єкти перший потік блокується, поки потік виконання, який отримує елементи (другий потік) не вилучить з черги принаймні один елемент. Другий потік може вилучати і використовувати об'єкти з черги. Якщо, проте, другий потік спробує отримати об'єкт з порожньої черги, цей потік блокується, поки перший потік не додасть новий об'єкт.
Iнтерфейс BlockingQueue
походить від java.util.Queue
, тому він підтримує методи add()
, remove()
, element()
(з генерацією винятку), а також offer()
, poll()
и peek()
(з поверненням спеціального значення). Крім того, визначені методи, які блокують - put()
(для додавання) і take()
(для вилучення). Є також методи, які використовують блокування з часовим інтервалом – offer()
і poll()
з додатковими параметрами (інтервалом часу і одиницею часу).
До BlockingQueue
не можна додати значення null
. Така спроба призводить до генерації винятку NullPointerException
.
Найбільш популярна реалізація BlockingQueue
– це ArrayBlockingQueue
. Для зберігання посилань на об'єкти в цій реалізації використовують звичайний масив обмеженої довжини. Ця довжина після створення об'єкта не може бути змінена. Всі конструктори класу першим параметром отримують цю довжину масиву. У наведеному нижче прикладі створюються два потоки - виробник (Producer
) і споживач (Consumer
). Виробник додає цілі числа з затримкою між додаваннями, а споживач їх вилучає.
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() { // Намагаємося додавати числа: 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() { // Вилучаємо числа: 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 { // Створюємо чергу з 10 елементів: BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); // Створюємо і запускаємо два потоки - для запису і читання: Thread producerThread = new Thread(new Producer(queue, 100)); Thread consumerThread = new Thread(new Consumer(queue, 4)); producerThread.start(); consumerThread.start(); // Чекаємо 10 секунд і перериваємо перший потік: Thread.sleep(10000); producerThread.interrupt(); } }
Під час запуску програми видно, що потік-виробник послідовно додає числа в чергу. Перші чотири числа вилучаються потоком-споживачем, причому потік-споживач кожен раз блокується, чекаючи наступне число. Оскільки в черзі може розміститися лише 10 чисел, після того, як потік-споживач припиняє вилучати елементи, черга швидко переповнюється. За десять секунд головний потік припиняє потік-виробник.
Реалізація LinkedBlockingQueue
відрізняється від ArrayBlockingQueue
внутрішнім поданням – у вигляді зв'язного списку. При цьому максимальний розмір вже не суттєвий (усталене значення – Integer.MAX_VALUE
), однак він також може бути заданий в конструкторі.
Клас PriorityBlockingQueue
використовує ті ж правила впорядкування, що й java.util.PriorityQueue
.
Клас SynchronousQueue
представляє чергу, яка може містити лише один елемент. Потік виконання, який додав елемент, блокується, поки інший потік не отримає його з черги. Якщо другий потік не може отримати елемент (черга порожня), цей потік також блокується, поки новий елемент не буде додано першим потоком.
Реалізація DelayQueue
дозволяє вилучати тільки ті елементи, для яких закінчився деякий проміжок часу (затримка). Елементи цієї черги повинні реалізовувати інтерфейс java.util.concurrent.Delayed
. Цей інтерфейс визначає метод getDelay()
, який повертає затримку, яка залишилася для об'єкта. З черги в першу чергу вилучається елемент, для якого час затримки минув раніше інших. Якщо у жодного з елементів не закінчилася затримка, метод poll()
поверне null
.
Інтерфейс BlockingDeque
, похідний від BlockingQueue
, додатково підтримує операції addFirst()
, addLast()
, takeFirst()
і takeLast()
, характерні для черг з двома кінцями. Клас LinkedBlockingDeque
пропонує стандартну реалізацію цього інтерфейсу.
2.7 Робота з контейнерами і потоками даних Java 8
2.7.1 Використання класу Optional
Опціональні значення - це контейнери для значень, які можуть іноді бути порожніми. Традиційно для невизначених величин використовували значення null
. Використання константи null
може бути незручним, оскільки воно може призвести до генерації винятку NullPointerException
, що ускладнює налагодження і супровід програми.
Узагальнений клас Optional
дозволяє зберегти значення посилання на деякий об'єкт, а також перевірити, чи встановлено значення. Наприклад, деякий метод повертає числове значення, але для деяких значень аргументу не може повернути щось певне. Такий метод може повернути об'єкт типу Optional
і це значення потім може бути використано в викликає функції. Припустимо, деяка функція обчислює і повертає зворотну величину і повертає "порожній" об'єкт, якщо аргумент дорівнює 0. У функції main()
здійснюємо обчислення зворотних величин для чисел з масиву:
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("Значення не може бути розраховане%n"); } } } }
Якщо не здійснювати перевірку на наявність значення (isPresent()
), при спробі виклику функції get()
для "порожнього" значення буде згенеровано виняток java.util.NoSuchElementException
. Його можна перехоплювати замість виклику функції isPresent()
.
У деяких випадках значення null
не повинно зберігатися як можливе допустиме. В цьому випадку для збереження значення слід використовувати ofNullable()
. Наприклад:
Integer k = null; Optional<Integer> opt = Optional.ofNullable(k); System.out.println(opt.isPresent()); // false
Припустимо, якщо описана раніше функція reciprocal()
не повертає значення в разі ділення на нуль, змінній y слід присвоїти 0. Традиційно в цьому випадку використовують конструкцію if
... else
else або умовну операцію:
y = reciprocal(x); double z = y.isPresent() ? y.get() : 0;
Метод orElse()
дозволяє зробити код більш компактним:
double z = reciprocal(x).orElse(0.0);
Крім узагальненого класу Optional
можна також використовувати класи, оптимізовані для примітивних типів - OptionalInt
, OptionalLong
, OptionalBoolean
тощо. Попередній приклад з обчисленням зворотної величини можна було б реалізувати таким чином (з використанням 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("Значення не може бути розраховане%n"); } } } }
Як видно з прикладу, результат слід отримувати за допомогою методу getAsDouble()
замість get()
.
2.7.2 Додаткові можливості роботи зі стандартними контейнерами
Стандартні інтерфейси пакету java.util
доповнені методами, орієнтованими на використання лямбда-виразів і посилань на методи. Для забезпечення сумісності з попередніми версіями Java нові методи інтерфейсів представлені з усталеною реалізацією. Зокрема, інтерфейс Iterable
визначає метод forEach()
, який дозволяє виконати в циклі деякі дії, що не змінюють елементів колекції. Дію можна задати лямбда-виразом або посиланням на метод. Наприклад:
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); } }
У наведеному вище прикладі здійснюється сумування елементів колекції. Змінна-сума описана як статичне поле класу, оскільки лямбда-вирази не можуть змінювати локальні змінні.
Інтерфейс Collection
визначає метод removeIf()
, який дозволяє видалити з колекції дані, відповідні деякому правилу-фільтру. У наступному прикладі з колекції цілих чисел видаляються непарні елементи. Метод forEach()
використовується для виведення елементів колекції в стовпчик:
Collection<Integer> c = new ArrayList(Arrays.asList(2, 4, 11, 8, 12, 3)); c.removeIf(k -> k % 2 != 0); // Решта елементів виводиться в стовпчик: c.forEach(System.out::println);
Інтерфейс List
надає методи replaceAll()
і sort()
. Останній можна використовувати замість аналогічного статичного методу класу Collections
, проте визначення ознаки сортування є обов'язковим:
List<Integer> list = new ArrayList(Arrays.asList(2, 4, 11, 8, 12, 3)); list.replaceAll(k -> k * k); // замінюємо числа їхніми квадратами 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]
Найбільш суттєві зміни торкнулися інтерфейсу Map
. Додані методи, наведені в таблиці:
Метод | Опис |
---|---|
V getOrDefault(Object key, V& defaultValue) |
Повертає значення, або усталене значення, якщо ключ відсутній |
V putIfAbsent(K key, V value) |
Додає пару, якщо ключ відсутній, і повертає значення |
boolean remove(Object key, Object value) |
Видаляє пару, якщо вона присутня |
boolean replace(K key, V oldValue, V newValue) |
Замінює значення на нове, якщо пара присутня |
V replace(K key, V value) |
Замінює значення; якщо ключ є, повертає старе значення |
V compute(K key, BiFunction<?& super K, super V, ? extends V> remappingFunction) |
Викликає функцію для побудови нового значення. Вводиться нова пара, видаляється пара, яка існувала раніше, і повертається нове значення |
V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) |
Якщо присутній вказаний ключ, для створення нового значення викликається задана функція і нове значення замінює колишнє. |
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) |
Повертає значення за ключем. Якщо ключ відсутній, додається нова пара, значення обчислюється за функцією |
V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) |
Якщо ключ відсутній, то вводиться нова пара і повертається значення v . В іншому випадку задана функція повертає нове значення, виходячи з колишнього значення і ключ оновлюється для доступу до цього значення. а потім воно повертається |
void forEach(BiConsumer<? super K, ? super V> action) |
Виконує задану дію (action ) над кожним елементом |
Наведений нижче приклад демонструє використання деяких із зазначених методів:
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); // порядкове виведення 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); // нічого не змінилося 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); // нічого не змінилося map.computeIfPresent(2, (i, s) -> s.toLowerCase()); System.out.println(map); // {1=one, 2=two, 4=4, 7=seven, 8=EIGHT} // Уводиться нова пара: map.merge(9, "nine", (value, newValue) -> value.concat(newValue)); System.out.println(map.get(9)); // nine // Текст зшивається з попереднім: map.merge(9, " as well", (value, newValue) -> value.concat(newValue)); System.out.println(map.get(9)); // nine as well } }
2.7.3 Використання Stream API Java 8
Потоки для роботи з колекціями, або потоки елементів, потоки даних (Stream API) призначені для високорівневої обробки даних, що зберігаються в контейнерах. Їх не слід плутати з потоками введення-виведення (input / output streams) і потоками управління (threads).
Потоковий API використовують для пошуку, фільтрації, перетворення, знаходження мінімальних і максимальних значень, а також іншого маніпулювання даними. Важливою перевагою Stream API є можливість надійної і ефективної роботи в багатопотоковому (multithreading) оточенні.
Потоки слід розуміти не як новий вид колекцій, а як канал передачі і обробки даних. Потік даних працює з деяким джерелом даних, наприклад масивом або колекцією. Потік не зберігає даних безпосередньо, а виконує переміщення, фільтрацію, сортування тощо. Дії, що виконуються потоком, не змінюють даних джерела. Наприклад, сортування даних в потоці не змінює їх порядок в джерелі, а створюється окрема результуюча колекція.
Можна створювати послідовні (sequential) і паралельні (parallel) потоки елементів. Паралельні потоки безпечні з точки зору багатопотоковості. З наявного паралельного потоку можна отримати послідовний і навпаки.
Для роботи з потоками в Java 8 реалізований пакет java.util.stream
, що надає набір інтерфейсів і класів, що забезпечують операції над потоками елементів в стилі функціонального програмування. Потік представлений об'єктом, що реалізує інтерфейс java.util.stream.Stream
. У свою чергу, цей інтерфейс успадковує методи узагальненого інтерфейсу java.util.stream.BaseStream
.
Операції над потоками (методи), визначені в інтерфейсах BaseStream
, Stream
, та інших, похідних від BaseStream
, діляться на проміжні та кінцеві. Проміжні операції отримують і генерують потоки даних і служать для створення так званих конвеєрів (pipeline), в яких над послідовністю виконується ряд дій. Кінцеві операції дають остаточний результат і при цьому "споживають" вихідний потік. Це означає, що вихідний потік не може бути використаний повторно і в разі необхідності повинен бути створений заново.
Найбільш істотні методи узагальненого інтерфейсу java.util.stream.BaseStream
наведені в таблиці (S
- тип потоку, E
- тип елемента, R
- тип контейнера):
Метод | Опис | Примітка |
---|---|---|
S parallel() |
повертає паралельний потік даних, отриманий з поточного | проміжна операція |
S sequential() |
повертає послідовний потік даних, отриманий з поточного | проміжна операція |
boolean isParallel() |
повертає true , якщо викликає потік даних є паралельним, або false , якщо він є послідовним |
кінцева операція |
S unordered() |
повертає невпорядкований потік даних, отриманий з поточного | проміжна операція |
Iterator<T> iterator() |
повертає посилання на ітератор потоку даних | кінцева операція |
Spliterator<T> spliterator() |
повертає посилання на ітератор-роздільник потоку даних | кінцева операція |
Використання ітераторів потоків буде розглянуто нижче.
Інтерфейс Stream
розширює набір методів для роботи з потоками елементів. Це також узагальнений інтерфейс і він придатний для роботи з будь-якими посилальними типами. Нижче наведені найбільш вживані методи інтерфейсу Stream
:
Метод | Опис | Примітка |
---|---|---|
void forEach(Consumer<? super T> action) |
виконує код, заданий дією (action ) для кожного елемента потоку |
кінцева операція |
Stream<T> filter(Predicate<? super T> pred) |
повертає потік елементів, що задовольняють предикату | проміжна операція |
Stream<T> sorted() |
повертає потік елементів, розсортованих в природному порядку | проміжна операція |
Stream<T> sorted(Comparator<? super T> comparator) |
повертає потік елементів, розсортованих в зазначеному порядку | проміжна операція |
|
застосовує задану функцію до елементів потоку і повертає новий потік | проміжна операція |
Optional<T> min(Comparator<? super T> comp) |
повертає мінімальне значення з використанням заданого порівняння | кінцева операція |
Optional<T> max(Comparator<? super T> comp) |
повертає максимальне значення з використанням заданого порівняння | кінцева операція |
long count() |
повертає кількість елементів в потоці | кінцева операція |
Stream<T> distinct() |
повертає потік різних елементів | проміжна операція |
Optional<T> reduce(BinaryOperator<T> accumulator) |
повертає скалярний результат, обчислений за значеннями елементів | кінцева операція |
Object[] toArray() |
створює і повертає масив елементів потоку | кінцева операція |
Існує кілька способів створення потоку. Можна скористатися "фабричними" методами, що були додані до інтерфейсу Collection
(з усталеними реалізаціями) - відповідно stream()
(для послідовної роботи) и parallelStream()
(для багатопотокового роботи):
List<Integer> intList = Arrays.asList(3, 4, 1, 2); Stream<Integer> sequential = intList.stream(); Stream<Integer> parallel = intList.parallelStream();
Можна створити потік з масиву:
Integer[] a = { 1, 2, 3 }; Stream<Integer> fromArray = Arrays.stream(a);
Можна створити джерело даних із зазначеними елементами. Для цього слід скористатися "фабричним" методом of()
:
Stream<Integer> newStream = Stream.of(4, 5, 6);
Потоки елементів можна створити з потоків введення (BufferedReader.lines()
), заповнити випадковими значеннями (Random.ints()
), а також отримати з архівів, бітових наборів тощо.
Проміжні операції характеризуються так званою відкладеною поведінкою (lazy behaviour): вони виконуються не миттєво, а в міру необхідності - коли кінцева операція працює з новим потоком даних. Відкладена поведінка підвищує ефективність роботи з потоками елементів.
Більшість операцій реалізовані так, що дії над окремими елементами не залежать від інших елементів. Такі операції називаються операціями без збереження стану. Інші операції, що вимагають роботи одразу над всіма елементами (наприклад, sorted()
), називаються операціями зі збереженням стану.
З потоку можна отримати масив за допомогою методу toArray()
. У наведеному нижче прикладі створюється потік, а потім виводиться на консоль через створення масиву і приведення до строковому поданням за допомогою статичного методу Arrays.toString()
:
s = Stream.of (1, -2, 3); System.out.println(Arrays.toString(s.toArray())); // [1, -2, 3]
Потоки елементів надають ітератори. Метод iterator()
інтерфейсу Stream
повертає об'єкт, який реалізує інтерфейс java.util.Iterator
. Ітератор можна використовувати в явному вигляді:
s = Stream.of(11, -2, 3); Iterator<Integer> it = s.iterator(); while (it.hasNext()) { System.out.println(it.next()); }
Існує також спеціальний вид ітератора - ітератор-роздільник (реалізований інтерфейсом Spliterator
). Він, зокрема, дозволяє обійти частину елементів.
Потоки забезпечують ітерацію за елементами даних за допомогою методу forEach()
. Параметр функції - стандартний функціональний інтерфейс Consumer
, що визначає метод з одним параметром і типом результату void
. Наприклад:
Stream<Integer> s = Stream.of(4, 5, 6, 1, 2, 3); s.forEach(System.out::println);
Найбільш проста операція з потоками - фільтрація. Проміжна операція filter()
повертає фільтрований потік, приймаючи параметр типу Predicate
. Тип Predicate
- це функціональний інтерфейс, що описує метод з одним параметром і типом результату boolean
. Наприклад, можна відфільтрувати з потоку s
тільки парні числа:
s.filter(k -> k % 2 == 0).forEach(System.out::println);
Попередній приклад ілюструє використання лямбда-виразів під час роботи з потоками, а також невеличкий конвеєр, що включає одну проміжну операцію.
Проміжна операція sorted()
повертає відсортоване представлення потоку. Елементи упорядковано в природному порядку (якщо він визначений). В інших випадках слід реалізовувати інтерфейс Comparator
, наприклад, за допомогою лямбда-виразу:
// Сортуємо за зростанням: Stream<Integer> s = Stream.of(4, 5, 6, 1, 2, 3); s.sorted().forEach(System.out::println); // Сортуємо в порядку зменшення: s = Stream.of(4, 5, 6, 1, 2, 3); s.sorted((k1, k2) -> Integer.compare(k2, k1)).forEach(System.out::println);
Останній приклад показує, що після кожного виклику кінцевої операції потік потрібно створювати знову.
Проміжна операція map()
отримує функціональний інтерфейс, що задає деяку функцію перетворення і формування нового потоку з отриманих перетворених елементів. Наприклад, обчислюємо квадрати чисел:
s = Stream.of(1, 2, 3); s.map(x -> x * x).forEach(System.out::println);
За допомогою методу distinct()
з колекції можна отримати потік, що містить тільки різні елементи. Наприклад:
s = Stream.of(1, 1, -2, 3, 3); System.out.println(Arrays.toString(s.distinct().toArray())); // [1, -2, 3]
Кінцева операція count()
з типом результату long
повертає кількість елементів в потоці:
s = Stream.of(4, 5, 6, 1, 2, 3); System.out.println(s.count()); // 6
Кінцеві операції min()
і max()
повертають об'єкти Optional
з відповідно мінімальним і максимальним значенням. Для порівняння використовується параметр типу Comparator
. Наприклад:
s = Stream.of(11, -2, 3); System.out.println(s.min(Integer::compare).get()); // -2
За допомогою кінцевої операції reduce()
можна обчислити деяке скалярне значення. Операція reduce()
у своїй простій формі здійснює вказану дію з двома операндами, перший з яких - результат виконання дії над попередніми елементами, а другий - поточний елемент. У наступному прикладі ми знаходимо суму елементів потоку даних:
s = Stream.of(1, 1, -2, 3, 3); Optional<Integer> sum = s.reduce((s1, s2) -> s1 + s2); sum.ifPresent(System.out::println); // 6
Операції min()
, max()
і reduce()
отримують скалярне значення з потоку, тому вони мають назву операцій зведення.
Існують також потоки для роботи з примітивними типами - IntStream
, LongStream
и DoubleStream
.
2.8 Стандартні засоби створення графіків і діаграм
Стандартні засоби JavaFX надають готові компоненти для побудови графіків і діаграм. Абстратний клас Chart
є базовим для всіх графіків і діаграм і об'єднує в собі властивості всіх компонентів, призначених для візуалізації числових даних. Безпосередньо похідними від нього є класи PieChart
(кругова діаграма) та XYChart
. Останній також є абстрактним і виступає базовим для всіх інших діаграм: AreaChart
, BarChart
, BubbleChart
, LineChart
, ScatterChart
, StackedAreaChart
і StackedBarChart
.
Конструктори класів, похідних від XYChart
, вимагають визначення двох об'єктів типу Axis
(вісь). Axis
- це абстрактний клас для представлення осі на графіку або діаграмі. Похідними від нього є клас CategoryAxis
, об'єкти якого використовують, коли вздовж осі розташовують мітки у вигляді набору рядків, а також абстрактний клас ValueAxis
. Клас NumberAxis
, похідний від ValueAxis
, використовують, коли вісь представляє шкалу числових значень.
У наведеному нижче прикладі здійснюється демонстрація можливостей побудови деяких діаграм:
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))); // Використовує той самий набір даних 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); } }
Головне вікно застосунку виглядатиме так:
До діаграм, похідних від XYChart
, можна визначити кілька наборів даних (Series
) для візуального порівняння різних залежностей.
Особливу цікавість представляє діаграма LineChart
, яка може бути використана для побудови графіків функцій. Для побудови графіку спочатку слід підготувати дані залежності у вигляді набору точок. Точок повинно бути достатньо багато, щоб крива була гладкою, але не надто багато, щоб не уповільнити виконання програми. У наведеній вище програмі здійснюється побудова двох тригонометричних функцій на одному графіку:
package ua.inf.iwanoff.oop.fifth; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.stage.Stage; import java.util.function.DoubleUnaryOperator; public class LineChartDemo extends Application { @Override public void start(Stage primaryStage) { primaryStage.setTitle("Тригонометричні функції"); Scene scene = new Scene(fgGraph(-4, 4, 1, -1.5, 1.5, 1, "Синус", Math::sin, "Косинус", Math::cos), 800, 350); primaryStage.setScene(scene); primaryStage.show(); } private LineChart<Number, Number> fgGraph(double xFrom, double xTo, double xStep, double yFrom, double yTo, double yStep, String fName, DoubleUnaryOperator f, String gName, DoubleUnaryOperator g) { NumberAxis xAxis = new NumberAxis(xFrom, xTo, xStep); NumberAxis yAxis = new NumberAxis(yFrom, yTo, yStep); LineChart<Number, Number> graphChart = new LineChart<>(xAxis, yAxis); // Вимикаємо "символи" в точках graphChart.setCreateSymbols(false); double h = (xTo - xFrom) / 100; // Додаємо ім'я і точки першої функції: XYChart.Series<Number, Number> fSeries = new XYChart.Series<>(); fSeries.setName(fName); for (double x = xFrom; x <= xTo; x += h) { fSeries.getData().add(new XYChart.Data<>(x, f.applyAsDouble(x))); } // Додаємо ім'я і точки другої функції: XYChart.Series<Number, Number> gSeries = new XYChart.Series<>(); gSeries.setName(gName); for (double x = xFrom; x <= xTo; x += h) { gSeries.getData().add(new XYChart.Data<>(x, g.applyAsDouble(x))); } // Додаємо обидві функції graphChart.getData().addAll(fSeries, gSeries); return graphChart; } public static void main(String[] args) { launch(args); } }
3 Приклади програм
3.1 Отримання даних про методи класу
Припустимо, необхідно створити програму, яка виводить дані про методи деякого класу – ім'я, тип результату і типи параметрів. Програма матиме такий вигляд:
package ua.inf.iwanoff.oop.fifth; import java.lang.reflect.Method; import java.util.Scanner; public class ShowAllMethods { @SuppressWarnings("resource") public static void main(String[] args) { System.out.print("Уведіть повне ім\'я класу: "); String className = new Scanner(System.in).next(); try { Class<?> c = Class.forName(className); for (Method m : c.getMethods()) { System.out.printf("Ім'я: %s Тип результату: %s%n", m.getName(), m.getReturnType().getCanonicalName()); for (Class<?> type : m.getParameterTypes()) { System.out.printf(" Тип параметра: %s%n", type.getCanonicalName()); } } } catch (ClassNotFoundException e) { System.err.println("Помилка введення імені класу!"); } } }
3.2 Обчислення значень функцій класу Math
У наступному прикладі функції getMethod()
та invoke()
використана для виклику статичних методів класу java.lang.Math
(можуть бути викликані методи з одним параметром):
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) { // Створюємо об'єкт типу Method, якщо його не було створено раніше. // Метод повинен приймати один аргумент типу double: method = Class.forName(className).getMethod(methodName, double.class); } // Викликаємо метод: return (Double) method.invoke(null, x); } @SuppressWarnings("resource") public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("Уведіть ім'я статичного методу класу Math:"); SomeFunction someFunction = new SomeFunction("java.lang.Math", scanner.next()); System.out.println("Уведіть початок, кінець інтервалу та крок:"); 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("Помилка введення даних"); } catch (Exception e) { System.out.println("Помилка виклику функції"); } } }
3.3 Обчислення значення виразу з рядком
Під час виконання програми користувач вводить вираз, в якому використовується деякий рядок, і отримує результат цього виразу на екрані. Можна використовувати методи класу String
. Залежно від мети використання програми можна запропонувати два варіанти її реалізації.
Перший варіант
Перший варіант передбачає, що генерація коду з уведеним користувачем рядком під час виконання програми здійснюватиметься один раз. Припустимо, в середовищі Eclipse ми створили проект StringProcessor
, пакет ua.inf.iwanoff.oop.fifth
і клас StringProcessor
. Код може бути таким:
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) {"); // Додаємо порожній рядок, щоб перетворити на рядок результат будь-якого типу: 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("Уведіть вираз, який треба виконати над рядком s:"); String expression = scan.nextLine().replaceAll("\"", "\\\""); sp.genSource(expression); try { if (sp.compile()) { System.out.println("Введіть рядок 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("Помилка введення виразу!"); } } catch (Exception e) { e.printStackTrace(); } } }
Примітка: у середовищі IntelliJ IDEA для відповідного проекту з ім'ям StringProcessor
ініціалізація змінної sourceFile
буде такою:
final String sourceFile = "out/production/StringProcessor/ua/inf/iwanoff/oop/fifth/StrFun.java";
Другий варіант
Більш складний випадок передбачає багаторазове уведення виразу і, відповідно, багаторазову геренацію коду. Проблема полягає в тому, що після завантаження класу його треба видалити (unload), для того, щоб завантажити нову версію з таким же ім'ям. Це нетривіальна задача, яка передбачає створення власного завантажувача. Більш простий підхід полягає у створенні класів з різними іменами. Наприклад, до імен класів можна додавати послідовні цілі числа. Тепер програма буде такою:
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("Уведіть вираз, який треба виконати над рядком s " + "(порожній рядок для закінчення):"); expression = scan.nextLine().replaceAll("\"", "\\\""); if (expression.length() == 0) { break; } sp.genSource(expression, ++number); try { if (sp.compile(number)) { System.out.println("Введіть рядок 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("Помилка введення виразу!"); } } catch (Exception e) { e.printStackTrace(); } } while (expression.length() > 0); } }
Примітка: у середовищі IntelliJ IDEA теж слід скоротити значення рядка StringProcessor
.
3.4 Робота з контейнером ConcurrentSkipListMap
У наведеній нижче програмі один потік виконання здійснює заповнення ConcurrentSkipListMap
парами число / список простих множників, а інший потік знаходить в цьому списку прості числа (числа з одним простим співмножником) і заносить їх до списку. Числа перевіряються в заданому діапазоні.
Клас PrimeFactorization
(розкладання на прості множники):
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; } // Отримання списку дільників заданого числа: 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; } }
Клас Primes
(прості числа):
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 { // намагаємося отримати наступний набір чисел: factors = pf.table.get(last); Thread.sleep(1); } while (factors == null); // Найдено простое число: if (factors.size() == 1) { result.add(last); System.out.println(this); } } } catch (InterruptedException e) { e.printStackTrace(); } } @Override public String toString() { return " Prime numbers: " + result; } }
Клас PrimesTest
:
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 Отримання таблиці простих чисел за допомогою потоків даних
Наведена нижче програма дозволяє отримати таблицю простих чисел в заданому діапазоні. Для отримання простих чисел доцільно використовувати потік IntStream
:
package ua.inf.iwanoff.topic10; 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); } }
Метод isPrime()
перевіряє, чи є число n простим. Для цього для чисел, більших 1, формується набір послідовних цілих чисел, для кожного з яких перевіряється, чи ділиться n на це число. У методі printAllPrimes()
формуємо потік простих цілих чисел з використанням фільтру і виводимо числа за допомогою методу forEach()
.
3.6 Створення застосунку графічного інтерфейсу користувача для обчислення і відображення простих чисел
Припустимо, необхідно розробити програму отримання простих чисел у діапазоні від 1 до певного значення, яке може бути досить великим. Для знаходження простих чисел будемо застосовувати найпростіший алгоритм послідовної перевірки усіх чисел від 2 до квадратного кореня з числа, що перевіряється. Така перевірка може зайняти багато часу. Для створення зручного інтерфейсу користувача доцільно використовувати окремий потік виконання, у якому здійснюється перевірка. Завдяки цьому можна буде призупиняти та відновлювати пошук, виконувати стандартні маніпуляції з вікном, включаючи його закриття до завершення процесу пошуку.
Кореневою панеллю нашого застосунку буде BorderPane
. Сирцевий код JavaFX-застосунку буде таким:
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.FileInputStream; 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(new FileInputStream("PrimeAppWindow.fxml")); Scene scene = new Scene(root, 700, 500); primaryStage.setScene(scene); primaryStage.setTitle("Прості числа"); primaryStage.show(); } catch (IOException e) { e.printStackTrace(); } } }
Інтерфейс користувача включатиме верхню панель (HBox
), до якої послідовно додано мітку з текстом "До:", поле введення тексту textFieldTo
, а також чотири кнопки buttonStart
, buttonSuspend
, buttonResume
і buttonStop
з текстом відповідно "Стартувати", "Призупинити","Продовжити" і "Завершити". На початку виконання програми лише кнопка buttonStart
буде доступною. Середню частину займатиме область введення textAreaResults
, для якої вимкнене редагування, нижню частину займатиме компонент progressBar
, у якому відображатиметься доля виконання процесу пошуку простих чисел. Файл FXML-документу буде таким:
<?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="До:"/> <TextField fx:id="textFieldTo" text="1000"/> <Button fx:id="buttonStart" minWidth="80.0" text="Стартувати" onAction="#startClick"/> <Button fx:id="buttonSuspend" minWidth="80.0" disable="true" text="Призупинити" onAction="#suspendClick"/> <Button fx:id="buttonResume" minWidth="80.0" disable="true" text="Продовжити" onAction="#resumeClick"/> <Button fx:id="buttonStop" minWidth="80.0" disable="true" text="Завершити" 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>
Можна згенерувати порожній клас контролера, додати до нього поля, пов'язані з візуальними компонентами й методи обробки подій:
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) { } }
Після завантаження на виконання вікно програми буде таким:
Тепер окремо від засобів графічного інтерфейсу користувача можна створити клас, який відповідатиме за обчислення простих чисел в окремому потоці. Клас PrimeNumbers
реалізуватиме інтерфейс Runnable
. Основні "робочі" функції (start()
, suspend()
, resume()
і stop()
), які викликатимуться з контролера, змінюють стан об'єкта (поля suspended
і stopped
). Для забезпечення гнучкості програмі здійснюється оновлення даних про прості числа і відсоток виконання процесу через механізм зворотного виклику - передбачені поля класу типу інтерфейсу Runnable
. Для того, щоб виконання методів run()
здійснювалося б в іншому потоці, ці функції викликаються за допомогою Platform.runLater()
. Сирцевий код класу PrimeNumbers
матиме такий вигляд:
package ua.inf.iwanoff.oop.fifth; import javafx.application.Platform; public class PrimeNumbers implements Runnable { private Thread primesThread; // нитка обчислення простих чисел private int to; // кінець діапазону обчислення простих чисел private int lastFound; // останнє знайдене просте число private Runnable displayFunc; // функція, яка викликається для виведення знайденого числа private Runnable percentageFunc; // функція, яка оновлює кількість відсотків виконаного процесу private Runnable finishFunc; // функція, яка викликається після закінчення 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); // Оновлюємо кількість відсотків: 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); // Відображаємо знайдене просте число: if (displayFunc != null) { displayFunc.run(); } } } catch (InterruptedException e) { // залежно від стану об'єкта чекаємо на продовження або завершуємо пошук: while (isSuspended()) { try { Thread.sleep(100); } catch (InterruptedException e1) { // Перервали у стані очікування: 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(); } }
Як видно з коду, методи доступу до даних реалізовані з модифікатором synchronized
, що унеможливлює одночасний запис і читання недописаних даних з різних потоків. Тому всі звертання до полів класу з його методів здійснюються тільки через функції доступу.
Тепер можна реалізувати код контролера:
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("Помилка"); alert.setHeaderText("Хибний діапазон!"); 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); } }
Методи addToTextArea()
, setProgress()
і finish()
призначені не для безпосереднього виклику з контролера, а для зворотного виклику. Посилання на ці функції передаються в конструктор об'єкта типу PrimeNumbers
. Оброблювачі подій викликають відповідні методи класу PrimeNumbers
і змінюють доступність кнопок.
4 Вправи для контролю
- Створити консольний застосунок, в якому користувач вводить ім'я класу і отримує інформацію про всі відкриті поля цього класу.<
- Створити функцію, яка обчислює квадратний корінь, якщо це можливо і повертає об'єкт типу
OptionalDouble
. - Створити консольну програму, в якій виводяться всі додатні цілі числа, сума яких дорівнює заданому значенню. Використати потоки даних. Не використовувати явні цикли.
5 Контрольні запитання
- У чому суть RTTI?
- Для чого використовується ключове слово
instanceof
? - Дайте визначення рефлексії.
- Що за інформація зберігається в полі
class
? - Як здійснюється завантаження класів в Java?
- Які існують види завантажувачів класів?
- Як завантажити клас за ім'ям?
- Для чого створюються користувацькі завантажувачі?
- Як отримати інформацію про методи класу?
- Як викликати метод за ім'ям?
- Як викликати статичний метод?
- Як отримати інформацію про поля класу?
- Як в код на Java включити код на JavaScript?
- Як програмно здійснити компіляцію вихідного коду?
- Дайте визначення процесу і потоку виконання.
- У чому переваги використання потоків виконання?
- Які є способи створення потоків виконання в Java?
- В яких станах може перебувати потік?
- Коли здійснюється припинення і призупинення виконання роботи потоку?
- Якими засобами можна здійснити синхронізацію потоків?
- У яких випадках робота потоку блокується?
- Для чого використовується модифікатор
synchronized
? - Які контейнерні класи забезпечують безпеку з точки зору потоків?
- Для чого використовують клас
Optional
? - Чи можна використовувати клас
Optional
з примітивними типами? - Які додаткові можливості роботи зі стандартними контейнерами передбачені в Java 8?
- У чому переваги і особливості Stream API?
- Як отримати потік з колекції?
- Як отримати потік з масиву?
- Чим відрізняються проміжні і кінцеві операції?