Лабораторна робота 2

Робота з типами-посиланнями

1 Завдання на лабораторну роботу

1.1 Індивідуальне завдання

Створити двовимірний масив цілих чисел з кількістю рядків і стовпців, вказаних в таблиці. Заповнити масив випадковими додатними значеннями відповідно до правила, наведеного в таблиці. Заповнити одновимірний масив рядків (String) довжини, вказаної в таблиці. Кожен елемент містить повторення певного символу кількістю, вказаною в таблиці. Здійснити сортування масиву рядків за правилом, вказаним в таблиці.

Розміри двовимірного масиву Правило заповнення
двовимірного масиву
Кількість елементів у масиву рядків Довжина окремого рядка в масиві Критерій сортування масиву рядків
1, 17 5 × 4 випадкове парне число від 4 до 30 кількість рядків масиву чисел мінімальний елемент рядка за збільшенням довжини
2, 18 5 × 6 випадкове парне число від 0 до 20 кількість стовпців масиву чисел максимальний елемент стовпця за зменшенням довжини
3, 19 4 × 3 випадкове парне число від 20 до 30 кількість рядків масиву чисел максимальний елемент рядка за алфавітом у зворотному порядку
4, 20 4 × 3 випадкове ціле число від 4 до 29 кількість рядків масиву чисел максимальний елемент рядка за зменшенням довжини
5, 21 4 × 6 випадкове ціле число від 1 до 25 кількість стовпців масиву чисел максимальний елемент стовпця за збільшенням довжини
6, 22 6 × 4 випадкове парне число від 10 до 20 кількість рядків масиву чисел мінімальний елемент рядка за алфавітом у зворотному порядку
7, 23 4 × 6 випадкове парне число від 12 до 24 кількість стовпців масиву чисел мінімальний елемент стовпця за зменшенням довжини
8, 24 5 × 3 випадкове непарне число від 3 до 31 кількість рядків масиву чисел мінімальний елемент рядка за алфавітом у зворотному порядку
9, 25 3 × 5 випадкове ціле число від 6 до 29 кількість стовпців масиву чисел мінімальний елемент стовпця за збільшенням довжини
10, 26 4 × 5 випадкове непарне число від 1 до 23 кількість стовпців масиву чисел максимальний елемент стовпця за алфавітом у зворотному порядку
11, 27 4 × 3 випадкове парне число від 0 до 24 кількість рядків масиву чисел мінімальний елемент рядка за зменшенням довжини
12, 28 6 × 4 випадкове парне число від 8 до 26 кількість стовпців масиву чисел мінімальний елемент стовпця за збільшенням довжини
13, 29 4 × 6 випадкове непарне число від 5 до 21 кількість стовпців масиву чисел максимальний елемент стовпця за зменшенням довжини
14, 30 4 × 6 випадкове ціле число від 4 до 33 кількість рядків масиву чисел максимальний елемент рядка за збільшенням довжини
15, 31 5 × 4 випадкове непарне число від 7 до 37 кількість рядків масиву чисел максимальний елемент рядка за алфавітом у зворотному порядку
16, 32 4 × 6 випадкове непарне число від 1 до 35 кількість стовпців масиву чисел мінімальний елемент стовпця за зменшенням довжини

Вивести отриманий масив рядків.

Наприклад, припустимо двовимірний масив цілих містить такі числа:

3 7
18 4
19 2

Якщо, припустимо, кількість елементів масиву рядків (String) відповідає кількості рядків масиву чисел, а мінімальні елементи масиву – це кількість повторення певного символу, і символ, який треба повторювати, це 'N', ми отримаємо такий масив рядків:

NNN
NNNN
NN

Реалізувати два підходи: традиційний, побудований на циклах і роботі з окремими елементами й через функції класу Arrays (без циклів). Не використовувати роботу з потоками і метод Arrays.stream().

Додати до класу та окремих функцій коментарі Javadoc.

1.2 Ератосфенове решето

Заповнити масив із трьохсот цілих чисел послідовними додатними значеннями. Замінити всі значення, що не є простими числами, деяким від'ємним значенням. Для цього послідовно виключати всі числа – дільники інших чисел. Вивести на екран додатні значення, що залишилися, (прості числа).

У програмі не застосовувати ділення та знаходження залишку від ділення.

1.3 Знаходження чисел Фібоначчі

Реалізувати функцію обчислення чисел Фібоначчі (до 92-го числа включно) з використанням допоміжного масиву (статичного поля). Параметр функції – номер числа Фібоначчі. Пошук чисел Фібоначчі здійснюється за таким правилом:

F(1) = F(2) = 1; F(n) = F(n - 2) + F(n - 1)

Під час першого виклику функції масив заповнюється до необхідного числа. Під час наступних викликів число або повертається з масиву, або обчислюється з використанням останніх двох чисел, що зберігаються у масиві з подальшим заповненням масиву. Використовувати тип long для представлення чисел.

Здійснити тестування функції для різних значень номерів, що вводяться у довільному порядку.

1.4 Вирівнювання рядка

Прочитати аргумент командного рядка і додати в нього пропуски (space characters) так, щоб довжина рядка дорівнювала заданому числу. Пропуски додавати рівномірно між словами (за можливості).

2 Методичні вказівки

2.1 Типи-посилання

У Java немає типів указівників. Імена змінних непримітивних типів по суті є іменами посилань на відповідні об'єкти. Усі непримітивні типи мають назву типів-посилань (reference types). Розіменування не потрібне: звернення до примітивних типів завжди здійснюється за значенням, а до непримітивних – за посиланням. Типи-посилання ніколи не можуть бути приведені до примітивних і навпаки. У Java немає операцій розіменування (* та ->). Java також не підтримує адресної арифметики.

Спеціальне ключове слово null використовують для того, щоб показати, що змінна типу-посилання ні на що не посилається. Константа null може бути присвоєна змінній будь-якого типу-посилання.

Об'єкти, на які вказують посилання, повинні бути розміщені в динамічній пам'яті за допомогою операції new:

SomeType st = new SomeType();

Присвоювання значення одного посилання іншому не забезпечує копіювання об'єктів. Після присвоювання два посилання посилаються на один об'єкт.

SomeType a = new SomeType();
SomeType b = new SomeType();
a = b;

Об'єкт, на який раніше посилалося a, загублений.

На відміну від С++, звільнення пам'яті від непотрібних об'єктів не потрібно. У Java немає операції delete. Для звільнення пам'яті використовується спеціальний механізм, який має назву збирання сміття. Цей механізм базується на підрахунку посилань на об'єкти. Кожен об'єкт має свій лічильник посилань. Коли посилання копіюється в нову змінну типу-посилання, лічильник збільшується на одиницю. Коли посилання виходить з області видимості, чи перестає вказувати на об'єкт, лічильник зменшується на одиницю. Збирач сміття переглядає список об'єктів і видаляє з пам'яті всі об'єкти, для яких кількість посилань дорівнює 0.

Операція ==, застосована до змінних-посилань здійснює порівняння адрес, а не вмісту об'єктів.

Аргументи функцій типу-посилання передаються у функції за посиланням. Всередині функції створюється нове посилання на той самий об'єкт. Цей об'єкт можна змінити у функції та після повернення з функції використовувати його значення.

2.2 Масиви

2.2.1 Опис масивів

Найпростішим типом-посиланням у Java є масив. Масив (array) – це набір комірок зберігання даних, кожна з яких має той же тип. Окрема комірка має назву елемента масиву (array item). На відміну від інших складених типів даних, масиви завжди розташовуються в цілісному блоці пам'яті. Оскільки всі елементи займають однакові за розміром комірки пам'яті, адреса конкретного елемента завжди може бути обчислена за його номером. Номери елементів називаються індексами. Звернення до конкретних елементів здійснюють через індекс.

Як і в С++, масиви в Java індексуються починаючи з нуля. Під час опису масиву квадратні дужки ставляться після імені типу, а не імені змінної. Розміщення квадратних дужок після імені змінної допускається, але не рекомендується.

int[] a;        // посилання на масив
int n = 10;     // можна використовувати змінну
a = new int[n]; // створення масиву

У цьому прикладі описується посилання на масив, який створюється пізніше. Розмір масиву не є частиною типу, як це реалізовано в C++. Це дозволяє визначити масив необхідного розміру під час виконання програми

int[] numbers;         // посилання на масив цілих чисел довільної довжини
numbers = new int[10]; // numbers складається з 10 елементів
numbers = new int[20]; // numbers складається з 20 елементів

У Java підтримуються одно- і багатовимірні масиви (масиви масивів). Наприклад, так можна визначити посилання на двовимірний масив:

byte[][] scores;

Наведені нижче приклади показують, як створити різні масиви:

int[] numbers = new int[5];           // одновимірний масив
double[][] matrix = new double[5][4]; // прямокутний масив
byte[][] scores = new byte[5][];      // двовимірний масив з різною довжиною рядків
for (int i = 0; i < 5; i++) {
    scores[i] = new byte[i + 4];
}

Останній приклад показує як можна створити двовимірний масив з різною довжиною рядків. Розмір масиву може бути визначений як за допомогою констант, так і за допомогою змінних і виразів, що дають цілий результат.

У Java масиви містять елементи разом з їх кількістю. Кількість елементів масиву завжди можна отримати за допомогою спеціального поля length. Це поле доступне тільки для читання:

int[] a = new int[10];
System.out.println(a.length); // 10

У Java передбачений простий спосіб ініціалізації масиву списком початкових значень:

int[] numbers = new int[] { 1, 2, 3, 4, 5 };

Можна опустити операцію new:

int[] numbers = { 1, 2, 3, 4, 5 };

Аналогічно ініціалізуються багатовимірні масиви:

int[][] b = { { 1, 2, 3 },
              { 0, 0, 1 },
              { 1, 1, 11},
              { 0, 0, 0 } };

У наведеному прикладі створюється двовимірний масив з чотирьох рядків і трьох стовпців.

Так виглядає типовий цикл для обходу масиву:

for (int i = 0; i < a.length; i++) {
    a[i] = 0;
}

Аналогічно обхід усіх елементів двовимірного масиву зазвичай здійснюється так:

for (int i = 0; i < c.length; i++) {
    for (int j = 0; j < c[i].length; j++) {
        c[i][j] = 0;
    }
}

Як видно з останнього прикладу, для отримання розміру i-го рядка двовимірного масиву використовують конструкцію c[i].length.

Альтернативна форма циклу for (починаючи з JDK 1.5) дозволяє спростити повний обхід масивів. Наприклад, замість

int[] nums = { 1, 2, 3, 4, 5, 6 };
for (int i = 0; i < nums.length; i++) {
    System.out.println(nums[i]);
}

можна написати

int[] nums = { 1, 2, 3, 4, 5, 6 };
for(int n : nums) {
    System.out.println(n);
}

Альтернативна форма може бути застосована. лише для читання значень елементів, а не для їхньої модифікації.

Масиви читають з клавіатури поелементно. Наведений нижче приклад демонструє читання кількості та значень елементів з клавіатури.

package ua.inf.iwanoff.java.second;

public class ArrayTest {

    public static void main(String[] args) {
        System.out.println("Уведіть кількість елементів масиву:");
        java.util.Scanner s = new java.util.Scanner(System.in);
        int size = s.nextInt();
        double[] a = new double[size];
        System.out.println("Уведіть елементи масиву:");
        for (int i = 0; i < a.length; i++) {
            a[i] = s.nextDouble();
        }
        // Робота з масивом
        // ...
    }

}

Присвоювання імені масиву іншому імені масиву приводить тільки до копіювання посилання, але не самого масиву. Для копіювання елементів масиву необхідно організувати цикл або скористатися стандартними функціями.

2.2.2 Масиви як параметри та результат функцій

Масиви-параметри передаються у функції за посиланням. Після повернення з функції елементи можуть містити змінені значення:

package ua.inf.iwanoff.java.second;

public class SwapElements {

    static void swap(int[] a) {
        int z = a[0];
        a[0] = a[1];
        a[1] = z;
    }

    public static void main(String[] args) {
        int[] b = { 1, 2 };
        swap(b);
        System.out.println(b[0]); // 2
        System.out.println(b[1]); // 1
    }

}

Якщо параметр описано як одновимірний масив, то можна фактичним параметром вказати рядок двовимірного масиву. Також можна використовувати параметри типу багатовимірних масивів. Наприклад:

package ua.inf.iwanoff.java.second;

public class ArraysTest {

    static double sum(double[] arr1D) {
        double s = 0;
        for (double elem : arr1D) {
            s += elem;
        }
        return s;
    }

    static double sum(double[][] arr2D) {
        double s = 0;
        for (double[] line : arr2D) {
            for (double elem : line) {
                s += elem;
            }
        }
        return s;
    }

    public static void main(String[] args) {
        double[][] arr = { { 1, 2 },
                           { 3, 4 },
                           { 5, 6 } };
        System.out.printf("Сума елементів рядка з індексом 1: %f%n", sum(arr[1]));
        System.out.printf("Сума всіх елементів: %f%n", sum(arr));
    }
}

У Java 1.5 з'явилася додаткова можливість створення функцій зі змінним числом параметрів визначеного типу (Variable Arguments List, varargs). Всередині функції такі параметри інтерпретуються як масив:

static void printIntegers(int... a) {
    for (int i = 0; i < a.length; i++) {
        System.out.println(a[i]);
    }
}    

Викликати таку функцію можна у два способи: передаючи список аргументів типу елемента масиву, або передаючи масив цілком:

public static void main(String[] args) {
int[] arr = {4, 5};
printIntegers(arr);
printIntegers(1, 2, 3);
}

Такий параметр може бути лише один і обов'язково розташований останнім в списку.

Функція може повертати посилання на масив. Найчастіше такий масив створюється всередині функції. Наприклад, створюємо масив цілих, заповнений одиницями:

static int[] arrayOfOnes(int n) {
    int[] arr = new int[n];
    for (int i = 0; i < arr.length; i++) {
        arr[i] = 1;
    }
    return arr;
}

Тепер можна створити масив за допомогою нашої функції:

int[] a = arrayOfOnes(6);

Якщо потрібен тільки один елемент, для нього можна не створювати масив з ім'ям і поєднати створення масиву з його використанням:

System.out.println(arrayOfOnes(2)[0]);

Можна також повертати посилання на багатовимірні масиви.

Посилання на масиви передаються за значенням. Припустимо, ми хочемо створити функцію, яка змінює розмір наявного масиву:

static void resize(int[] arr, int newSize) {
    // збереження існуючих елементів
    arr = new int[newSize];
    // копіювання
}

Нове посилання слід повертати, інакше новий масив буде втрачено:

static int[] resize(int[] arr, int newSize) {
    // збереження існуючих елементів
    arr = new int[newSize];
    // копіювання
    return arr;
}

2.2.3 Стандартні функції для роботи з масивами

Клас System надає найпростіший шлях копіювання одного масиву в іншій – використання статичного методу arraycopy():

System.arraycopy(a, a_from, b, b_from, size);

Це еквівалентно такому циклу:

for (int i = a_from, j = b_from; i < size + a_from; i++, j++) {
    b[j] = a[i];
} 

Масив, у який здійснюється копіювання, повинен мати необхідні розміри. Функція arraycopy() не створює нового масиву. Весь масив a можна скопіювати в b таким викликом:

System.arraycopy(a, 0, b, 0, a.length);

Для роботи з масивами можна використовувати статичні методи класу Arrays, реалізованого в пакеті java.util.

Статичний метод класу Arrays Опис
void fill(тип[] a, тип val) заповнює масив вказаними значеннями
void fill(тип[] a, int fromIndex, int toIndex, тип val) заповнює частину масиву вказаними значеннями
String toString(тип[] a) повертає список елементів масиву у вигляді рядка
String deepToString(тип[][] a) подає багатовимірний масив у вигляді рядка
тип[] copyOf(тип[] a, int len) створює новий масив довжини len з копіями елементів
тип[] copyOfRange(тип[] a, int from, int to) створює новий масив з копіями елементів діапазону
void setAll(тип[] array, Оператор generator) заповнює масив за формулою, визначеною генератором
boolean equals(тип[] a, тип[] a2) перевіряє еквівалентність елементів двох масивів
boolean deepEquals(тип[][] a, тип[][] a2) перевіряє еквівалентність елементів багатовимірних масивів
void sort(тип[] a) здійснює сортування елементів за зростанням
void sort(тип[] a, int fromIndex, int toIndex) здійснює сортування частини елементів за зростанням
void sort(тип[] a, int fromIndex, int toIndex, Компаратор c) здійснює сортування елементів за критерієм, визначеним компаратором
int binarySearch(тип[] a, тип key) здійснює пошук у відсортованому масиві

У таблиці тип означає один з фундаментальних (примітивних) типів або тип Object.

Перший варіант функції fill() використовується для заповнення всього масиву, другий – частини, при чому елемент з номером toIndex не включається в послідовність. Наприклад:

package ua.inf.iwanoff.java.second;

public class FillArray {

    public static void main(String[] args) {
        int[] a = new int[6];
        java.util.Arrays.fill(a, 0, 4, 12); // Інші елементи дорівнюють 0
        for (int x : a) {
            System.out.print(x + " ");
        }
        System.out.println();
        java.util.Arrays.fill(a, 100);      // Всі елементи дорівнюють 100
        for (int x : a) {
            System.out.print(x + " ");
        }
        System.out.println();
    }

}

У попередньому прикладі для виведення елементів масиву на екран був використаний цикл. Альтернативний спосіб – використання функції toString() класу Arrays. Ця функція повертає представлення масиву у вигляді рядка, яке є зручним для більшості застосувань:

java.util.Arrays.fill(a, 100);
System.out.println(Arrays.toString(a)) // [100, 100, 100, 100, 100, 100];

Примітка: для виведення в рядок багатовимірних масивів слід використовувати функцію deepToString().

Клас Arrays надає альтернативний шлях копіювання масивів. Функція copyOf() створює новий масив копій елементів. Перший параметр – вихідний масив, другий параметр – довжина вислідного масиву. Елементи, які не помістилися, відкидаються, відсутні заповнюються нулями. Функція copyOfRange(тип[] a, int from, int to) копіює в новий масив частину масиву, включаючи початок інтервалу і не включаючи кінця інтервалу:

package ua.inf.iwanoff.java.second;

import java.util.Arrays;

public class CopyOfTest {

    public static void main(String[] args) {
        int[] a = { 1, 2, 3, 4 };
        int[] b = Arrays.copyOf(a, 3); 
        System.out.println(Arrays.toString(b));// [1, 2, 3]
        int[] c = Arrays.copyOf(a, 6); 
        System.out.println(Arrays.toString(c));// [1, 2, 3, 4, 0, 0]
        int[] d = Arrays.copyOfRange(a, 1, 3);
        System.out.println(Arrays.toString(d));// [2, 3]
    }

}

Порівняти два масиви чи частину їх можна за допомогою функцій групи equals(). Масиви порівнюються поелементно. Два масиви також вважаються еквівалентними, якщо обидва посилання – null. Наприклад:

package ua.inf.iwanoff.java.second;

import java.util.Arrays;

public class ArraysComparison {

    public static void main(String[] args) {
        double[] a = null, b = null;
        System.out.println(Arrays.equals(a, b)); // true
        a = new double[] { 1, 2, 3, 4 };
        b = new double[4];
        System.out.println(Arrays.equals(a, b)); // false
        System.arraycopy(a, 0, b, 0, a.length);
        System.out.println(Arrays.equals(a, b)); // true
        b[3] = 4.5;
        System.out.println(Arrays.equals(a, b)); // false
    }

}

Є також метод deepEquals(), використання якого аналогічне. Різниця є істотною для багатовимірних масивів. Здійснюється більш "глибока" перевірка:

int[][] a1 = { { 1, 2 } , { 3, 4 } };
int[][] a2 = { { 1, 2 } , { 3, 4 } };
System.out.println(Arrays.equals(a1, a2));    // false
System.out.println(Arrays.deepEquals(a1, a2));// true    

Функція setAll() дозволяє здійснює заповнювати масив значеннями залежно від індексу. Для заповнення застосовують механізм зворотного виклику (callback). Цей механізм у Java реалізовано за допомогою так званих функціональних інтерфейсів. Починаючи з Java 8, найпростіший варіант реалізації callback-функції це застосування так званих лямбда-виразів – альтернативної форми опису функцій. Докладно функціональні інтерфейси та лямбда-вирази будуть розглянуті пізніше. У наведеному нижче прикладі масив цілих чисел заповнюється квадратами індексів:

int[] a = new int[6];
Arrays.setAll(a, k -> k * k);
System.out.println(Arrays.toString(a)); // [0, 1, 4, 9, 16, 25]

Лямбда-вираз у цьому прикладі слід розуміти так: функція приймає аргумент k (індекс елемента) та повертає другий степінь індексу, який записується у відповідний елемент.

За допомогою функції sort() можна здійснити сортування масиву чисел за зростанням. Наприклад:

package ua.inf.iwanoff.java.second;

import java.util.Arrays;

public class ArraySort {

    public static void main(String[] args) {
        int[] a = new int[] { 11, 2, 10, 1 };
        Arrays.sort(a);            // 1 2 10 11
        for (int x : a) {
            System.out.print(x + " ");
        }
        System.out.println();
    }

}

Функція sort() реалізована для масивів усіх примітивних типів та рядків. Рядки впорядковуються за алфавітом. Можна також сортувати частину масиву. Як і для функції fill(), вказується початковий і кінцевий індекси послідовності, яку слід відсортувати. Кінцевий індекс не включається в послідовність. Наприклад:

int[] a = { 7, 8, 3, 4, -10, 0 };
java.util.Arrays.sort(a, 1, 4); // 7 3 4 8 -10 0    

У відсортованих масивах можна виконати пошук за допомогою методів класу Arrays. Група функцій binarySearch(), реалізована для всіх примітивних типів і типу Object, повертає індекс знайденого елемента або від'ємне значення, якщо елемент відсутній.

2.3 Визначення класів. Інкапсуляція

2.3.1 Поля і методи

Клас – це структурований тип даних, набір елементів даних різних типів і функцій для роботи з цими даними. Опис класу складається зі специфікаторів (наприклад, public, final), імені, імені базового класу, списку інтерфейсів і тіла у фігурних дужках.

Тіло класу містить поля (їм відповідають елементи даних у C++) і методи (функції-елементи в C++). Поля і методи разом іменуються елементами (членами) класу. Нижче наводиться приклад опису класу:

class Rectangle {
    double width;
    double height;

    double area() {
        return width * height;
    }
}

Після останньої фігурної дужки, що закривається, не слід ставити крапку з комою. Методи завжди реалізуються усередині визначення класу.

Під час створення об'єкта класу поля ініціалізуються усталеними значеннями (нулями або null для посилань). Java допускає ініціалізацію полів початковими значеннями:

class Rectangle {
    double width = 10;
    double height = 20;
    double area() {
        return width * height;
    }
}

Можна створити спеціальний блок ініціалізації усередині тіла класу. Такий блок виконуватиметься щораз під час створення нового об'єкта:

class Rectangle {
    double width;
    double height;
    {
        width = 10;
        height = 20;
    }
    double area() {
        return width * height;
    }
}

Для того, щоб працювати з полями й методами класу, необхідно створити об'єкт. Для цього спочатку створюють посилання на об'єкт, а потім за допомогою операції new створюють сам об'єкт шляхом виклику конструктора. Ці дії можна поєднати. Після цього можна викликати методи та використовувати поля:

Rectangle rect = new Rectangle(); // rect - ім'я посилання на об'єкт
double a = rect.area();           // a = 200
rect.width = 15;                  // зміна значення поля
double b = rect.area();           // b = 300

Під час виклику методів аргументи передаються за значенням.

Ключове слово this використовується як посилання на об'єкт, для якого викликаний метод. Усі нестатичні методи неявно отримують посилання на об'єкт для якого вони використані. Ключове слово this використовувати явно, наприклад, коли треба повернути з функції посилання на поточний об'єкт, або запобігти конфлікту імен.

2.3.2 Специфікатори доступу. Інкапсуляція

Java підтримує закритий (private), пакетний, захищений (protected) і відкритий (public) рівні доступу. Сам клас може бути оголошений як public. На відміну від C++, Java вимагає окремої специфікації доступу для кожного елемента, або групи полів одного типу:

public class Rectangle {
    private double width = 10;
    private double height = 20;

    public void setWidth(double width) {
        this.width = width;
    }

    public double getWidth() {
        return width;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    public double getHeight() {
        return height;
    }

    public double area() {
        return width * height;
    }
}

Доступ до закритих (private) елементів класу обмежений методами усередині класу. У Java немає ключового слова friend, яке у С++ забезпечує доступ до закритих елементів ззовні класу.

Відкриті (public) елементи відкритого класу можуть бути доступні з будь-якої функції будь-якого пакета.

Елементи класу без атрибутів доступу мають пакетну видимість. Такий доступ ще називають "дружнім". Всі інші класи цього пакета мають доступ до таких елементів як до відкритих. Ззовні пакету такі елементи взагалі недоступні.

Доступ до захищеного (protected) елемента класу, визначеного в деякому пакеті, обмежений методами цього класу і похідних класів будь-яких пакетів, а також класів цього пакету.

Інкапсуляція (приховування даних) – одна з трьох парадигм об'єктно-орієнтованого програмування. Зміст інкапсуляції полягає у приховуванні від зовнішнього користувача деталей реалізації об'єкта. Зокрема доступ до даних (полів), які зазвичай описані з модифікатором private, здійснюється через відкриті функції доступу. Як правило, це так звані сетери та гетери. Якщо поле має ім'я name, відповідні функції доступу мають імена setName та getName.

Автоматична генерація гетерів і сетерів здійснюється за допомогою функції Code | Generate... | Getter and Setter... головного меню IntelliJ IDEA.

2.3.3 Конструктори

Екземпляр класу створюється шляхом застосування операції new до конструктора.

Конструктор – це функція, яка здійснює ініціалізацію даних об'єкта. Ім'я конструктора збігається з ім'ям класу. Не можна вказувати типу результату конструктора. У класі може бути визначено кілька конструкторів. Якщо жоден конструктор явно не визначений, автоматично створюється усталений конструктор (без параметрів). Такий конструктор ініціалізує всі поля усталеними початковими значеннями. Після визначення принаймні одного конструктора усталений конструктор автоматично не створюється.

Один конструктор можна викликати з іншого з використанням слова this, після якого випливають необхідні аргументи. Спочатку здійснюється ініціалізація в місці опису, після якої значення можуть бути перевизначені в блоці ініціалізації, а потім перевизначені в конструкторі:

public class Rectangle {
    private double width = 10;
    private double height = 20;
    {
        width = 30;
        height = 40;
    }

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public Rectangle() {
        this(50, 60); // виклик іншого конструктора
    }
}

. . .

Rectangle rectangle = new Rectangle(); // width = 50, height = 60

У Java немає конструкторів копіювання і деструкторів. Можна створити спеціальний метод finalize(), який викликається збирачем сміття перед ліквідацією об'єкта. У деяких випадках об'єкт може бути не вилучений збирачем сміття ніколи (пам'яті вистачало до кінця програми), отже метод finalize() може бути ніколи не викликаний.

2.3.4 Статичні елементи. Константи

Методи й поля можуть бути оголошені з ключовим словом static. Звернення до таких полів і методів може здійснюватися без створення екземпляра класу. Статичні поля є альтернативою відсутнім у Java глобальним змінним. На відміну від С++, статичні елементи даних не потрібно окремо визначати в глобальній області видимості. Статичні поля можуть бути проініціалізовані під час створення:

class SomeClass {
    static double x = 10;
    static int    i = 20;
}

Можна створити окремий блок статичної ініціалізації:

class SomeClass {
    static double x;
    static int i;
    static {
        x = 10;
        i = 20;
    }
}

На відміну від нестатичної ініціалізації, створення й ініціалізація статичних полів здійснюється під час першого звернення до класу (створенні екземпляра класу чи зверненні до статичних елементів). Java не створює статичних полів для класів, які не використовуються.

Статичні методи не отримують посилання на об'єкт і не можуть використовувати посилання this.

Звернення до статичних елементів може здійснюватися як через ім'я класу, так і через посилання на об'єкт:

SomeClass.x = 30;
SomeClass s = new SomeClass();
s.x = 40;

Всередині класів можна визначати константи. Константи можуть бути двох видів – статичні й нестатичні. Статичну константу створює компілятор. За угодою її ім'я має містити лише великими літерами:

public static final double PI = 3.14159265;

Значення нестатичної константи слід визначити, причому один раз – у місці визначення, в блоці ініціалізації (тоді це значення буде однаковим для всіх екземплярів), або в конструкторі:

public class ConstDemo {
    public final int one = 1;
    public final int two; 
    {
        two = 2;
    }

    public final int other;

    public ConstDemo(int other) {
        this.other = other;
    }
}

Цілком безпечно визначати константи як public, оскільки компілятор не дозволить змінити їх значення.

2.4 Композиція класів

Під композицією класів розуміють створення нових класів з використанням об'єктів інших класів як полів. Java не дозволяє розміщення об'єктів усередині інших об'єктів, можна тільки описувати посилання. Композиція класів у цьому випадку припускає створення об'єктів безпосередньо (у тілі класу при оголошенні полів) чи в конструкторах.

class X {
}

class Y {
}

class Z {
    X x = new X();
    Y y;
    Z() {
        y = new Y();
    }
}

Можна також створити внутрішній об'єкт безпосередньо перед його першим використанням.

Відношення, що моделюється композицією, часто називають відношенням "has-a".

Агрегування – це різновид композиції, який передбачає, що сутність (екземпляр) міститься в іншій сутності або не може бути створена та існувати без сутності, яка її охоплює. При цьому сутність, що охоплює, може існувати без внутрішньої, тобто час життя зовнішньої та внутрішньої сутностей може не збігатися. Більш строге трактування композиції (власне композиція) передбачає, що час життя зовнішньої та внутрішньої сутностей збігається. На рівні Java агрегування передбачає можливість створення внутрішнього об'єкта перед його використанням, тоді як строга композиція передбачає створення внутрішнього об'єкта в тілі класу, в блоці ініціалізації або в конструкторі

2.5 Використання стандартних класів

2.5.1 Загальні відомості

Раніше вже використовувалися статичні засоби стандартних класів System (поля-потоки in і out), Math (стандартні математичні функції, реалізовані у вигляді статичних методів) і Arrays. Крім того, створювався об'єкт класу java.util.Scanner з викликом його методів.

Для тестування програмного забезпечення, статистичного моделювання, в задачах криптографії тощо використовують випадкові та псевдовипадкові значення, які можна отримати за допомогою класу java.util.Random.

Практично жодна програма на Java не може обійтися без об'єктів класу String: функція main() описується з параметром типу масиву рядків, дані читаються з потоків у рядки й записуються з рядків у потоки, рядки використовуються для представлення даних у візуальних компонентах графічного інтерфейсу користувача тощо. Для модифікації вмісту рядків використовуються стандартні класи StringBuffer і StringBuilder. Для поділу рядка на лексеми використовують клас StringTokenizer.

Класи-обгортки Integer, Double, Boolean, Character, Float, Byte, Short і Long використовують для зберігання даних примітивних типів у об'єктах, з якими можна працювати, як з посиланнями. Крім того, ці класи надають ряд корисних методів для перетворення даних.

2.5.2 Робота з випадковими величинами

Іноді під час тестування виникає необхідність у заповненні масивів випадковими (псевдовипадковими) значеннями. Це можна здійснити за допомогою функції random() класу Math і за допомогою спеціального класу java.util.Random. Перший варіант дає випадкове число у діапазоні від 0 до 1. Наприклад:

package ua.inf.iwanoff.java.second;

import java.util.Arrays;

public class MathRandomTest {

    public static void main(String[] args) {
        double[] a = new double[5];
        for (int i = 0; i < a.length; i++) {
            a[i] = Math.random() * 10; // випадкові значення від 0 до 10
        }
        System.out.println(Arrays.toString(a));
    }

}

Використання класу Random дозволяє отримати більш різноманітні результати. Конструктор класу Random без параметрів ініціалізує генератор псевдовипадкових чисел так, що послідовності випадкових значень практично не повторюються. Якщо для зневадження нам необхідно кожного разу отримувати однакові випадкові значення, слід скористатися конструктором з цілим параметром. Параметром може бути будь-яке ціле, яке ініціалізує генератор випадкових чисел.

У наведеній нижче таблиці представлені функції класу java.util.Random, що дозволяють отримати різні псевдовипадкові значення.

Функція Опис
nextBoolean() повертає наступне рівномірно розподілене значення типу boolean
nextDouble() повертає наступне значення типу double, рівномірно розподілене на інтервалі від 0 до 1
nextFloat() повертає наступне значення типу float, рівномірно розподілене на інтервалі від 0 до 1
nextInt() повертає наступне рівномірно розподілене значення типу int
nextInt(int n) повертає наступне значення типу int , рівномірно розподілене від 0 до n (не включаючи n)
nextLong() повертає наступне рівномірно розподілене значення типу long
nextBytes(byte[] bytes) заповнює масив цілих типу byte випадковими значеннями
nextGaussian() повертає наступне значення типу double, розподілене на інтервалі від 0 до 1 за нормальним законом

У наведеному нижче прикладі ми отримуємо псевдовипадкові цілі значення в діапазоні від 0 (включно) до 10:

package ua.inf.iwanoff.java.second;

import java.util.*;

public class UtilRandomTest {

    public static void main(String[] args) {
        int[] a = new int[15];
        Random rand = new Random(100);
        for (int i = 0; i < a.length; i++) {
            a[i] = rand.nextInt(10); // випадкові значення від 0 до 10
        }
        System.out.println(Arrays.toString(a));
    }

}

Після кожного запуску програми ми будемо отримувати однакову послідовність псевдовипадкових значень. Якщо ми хочемо, щоб значення були дійсно випадковими, треба скористатися конструктором Random() без параметрів.

2.6 Рядки

2.6.1 Використання класу String

Рядки в Java – це екземпляри класу java.lang.String. Об'єкти цього класу містять символи Unicode. Об'єкт-рядок може бути створений під час опису посилання шляхом присвоювання йому рядкового літерала:

String s = "Перший рядок"; 

Рядок можна також створити за допомогою різних конструкторів. Клас String у Java надає 15 конструкторів, які дозволяють визначити початкове значення рядка. Наприклад, можна отримати рядок з масиву символів:

char[] chars = { 'Т', 'е', 'к', 'с', 'т' };
String s1 = new String(chars); // Текст

Можна створити масив байтів і отримати з нього рядок. Перетворення байтів у символи здійснюється згідно з усталеною таблицею кодування, прийнятою в системі.

byte[] bytes = { 49, 50, 51, 52 };
String s2 = new String(bytes);
System.out.println(s2); // 1234

Можна створити рядок з іншого рядка. Слід відрізняти створення нового посилання від створення нового рядка:

String s = "текст";
String s1 = s;             // s і s1 посилаються на один рядок 
String s2 = new String(s); // s2 посилається на новий рядок - копію s

Якщо один з операндів – рядок, а інший – ні, то цей операнд подається у вигляді рядка. Альтернативний спосіб перетворення числових даних у рядки – застосування статичних функцій valueOf(). Відповідні функції реалізовані для аргументів числових типів, а також типів Object, char, boolean і масивів символів. Наприклад:

double d = 1.1;
String sd = String.valueOf(d); // "1.1"

Нижче наведені методи класу String, які використовують найбільш часто.

Метод Аргументи Повертає Опис
length () int Повертає кількість символів у рядку.
concat (String str) String Додає вказаний рядок у кінець поточного рядка
charAt (int index) char Повертає символ, що відповідає певному індексу
compareTo (String value) int Порівнює поточний рядок з аргументом-рядком. Результат від'ємний, якщо поточний рядок лексикографічно передує рядку-аргументу. Результат дорівнює 0, якщо рядки збігаються і додатний у протилежному випадку
compareToIgnoreCase (String str) int Працює як compareTo(), але ігнорує регістр
equals (String value) boolean Порівнює поточний рядок з аргументом-рядком. Повертає true, якщо рядки збігаються
equalsIgnoreCase (String str) boolean Порівнює поточний рядок з аргументом-рядком з ігноруванням регістрів. Повертає true, якщо рядки збігаються
indexOf (String substring) int Повертає індекс, який відповідає першому входженню підрядка у рядок. Якщо підрядок не входить у рядок – повертає –1
indexOf (char ch) int Повертає індекс, який відповідає першому входженню символу в рядок. Якщо символ відсутній – повертає –1
lastIndexOf (String substring) int Повертає індекс, який відповідає останньому входженню підрядка у рядок. Якщо підрядок не входить у рядок – повертає –1
lastIndexOf (char ch) int Повертає індекс, який відповідає останньому входженню символу в рядок. Якщо символ відсутній – повертає –1
repeat (int count) String Повертає рядок, в якому вихідний рядок повторюється count разів
substring (int beginindex, int endindex) String Повертає новий рядок, що є підрядком вихідного рядку
toLowerCase () String Повертає рядок, усі символи якого переведені в нижній регістр
toUpperCase () String Повертає рядок, усі символи якого переведені у верхній регістр
regionMatches (int toffset, String other, int offset, int len) boolean Перевіряє, чи збігаються дві послідовності символів у двох рядках
regionMatches (boolean ignoreCase, int toffset, String other, int ooffset, int len) boolean Перевіряє, чи збігаються дві послідовності символів у двох рядках. Додатково можна встановлювати можливість ігнорування регістра під час перевірки
toCharArray () char[] Перетворює поточний рядок у новий масив символів
getChars (int srcBegin, int srcEnd, char[] dst, int dstBegin) void Копіює символи поточного рядка у результуючий масив символів
getBytes () byte[] Повертає масив байтів, що містять коди символів з урахуванням таблиці кодів платформи (операційної системи)
trim () String Повертає копію рядка, у якій початкові і кінцеві пропуски опущені
startsWith (String prefix) boolean Перевіряє, чи починається рядок із зазначеного префікса
endsWith (String suffix) boolean Перевіряє, чи закінчується рядок зазначеним суфіксом

Наведені нижче приклади демонструють використання методів обробки рядків.

String s1 = new String("Hello World.");
int i = s1.length();   // i = 12
char c = s1.charAt(6); // c = 'W'
i = s1.indexOf('e');   // i = 1 (індекс 'e' у "Hello World.")
String s2 = "abcdef".substring(2, 5); // s2 = "cde"
int k = "AA".compareTo("AB");         // k = -1
s2 = "abc".toUpperCase();             // s2 = ABC  

Одна з найбільш типових операцій з рядками – зшивання. Для зшивання двох рядків можна застосувати функцію concat():

String s3 = s1.concat(s2);
s3 = s3.concat("додаємо текст");

Але найчастіше замість виклику функції concat() застосовують операцію +:

String s1 = "first";
String s2 = s1 + " and second";

Якщо один з операндів – рядок, а інший – ні, то цей операнд приводиться до рядкового представлення.

int n = 1;
String sn = "n дорівнює " + n; // "n дорівнює 1"
double d = 1.1;
String sd = d + ""; // "1.1"

Можна також використовувати операцію "+=" для дошивання в кінець рядка.

Можна створювати масиви рядків. Як і для інших типів-посилань, масив зберігає не рядки безпосередньо, а посилання на них. Функція sort() класу java.util.Arrays реалізована також для рядків. Рядки впорядковуються за алфавітом:

String[] a = { "dd", "ab", "aaa", "aa" };
java.util.Arrays.sort(a); // aa aaa ab dd     

Якщо ми хочемо перевизначити умову сортування, можна також скористатися лямбда-виразом. Відповідна функція повинна повертати -1, якщо елементи розташовані в необхідному порядку (перший елемент менше, ніж другий), нуль, якщо вони еквівалентні, та 1, якщо їхній порядок слід змінити на протилежний (перший елемент більше, ніж другий). Відповідний метод compareTo() класу String надає можливість отримувати такі значення.

Наприклад, визначення такого-лямбда-виразу як умови сортування нічого не змінить: сортування здійснюватиметься в природному порядку, як і без визначення умови перевірки:

String[] a = { "dd", "ab", "aaa", "aa" };
java.util.Arrays.sort(a, (s1, s2) -> s1.compareTo(s2)); // [aa, aaa, ab, dd]
System.out.println(Arrays.toString(a));
java.util.Arrays.sort(a, (s1, s2) -> -s1.compareTo(s2)); // [dd, ab, aaa, aa]
System.out.println(Arrays.toString(a));

А тепер сортування здійснюватиметься в протилежному порядку:

String[] a = { "dd", "ab", "aaa", "aa" };
java.util.Arrays.sort(a, (s1, s2) -> -s1.compareTo(s2)); // [dd, ab, aaa, aa]
System.out.println(Arrays.toString(a));

Якщо ми порівнюємо цілі значення (наприклад, довжину рядків), для отримання цих значень можна скористатися статичними функціями compare(), яку надають класи Integer, Double та інші класи-обгортки.

Для читання рядка з потоку з використанням класу java.util.Scanner рядок можна отримати за допомогою методу next() (до роздільника) або nextLine() (до кінця рядка).

Екземпляр класу String не може бути змінений після створення. Робота деяких методів та операцій ззовні нагадує модифікацію об'єкту, однак насправді створюється новий рядок.

String s = "ab"; // У пам'яті один рядок
s = s += "c";    // У пам'яті три рядки: "ab", "c" та "abc". На "abc" посилається s
// Зайві рядки потім будуть видалені збирачем сміття

2.6.2 Використання класів StringBuffer і StringBuilder

Існує спеціальний клас StringBuffer, що дозволяє модифікувати вміст рядкового об'єкта. Починаючи з Java 5, замість StringBuffer можна використовувати StringBuilder. У програмах, які не створюють окремих потоків виконання, функції цього класу виконуються більш ефективно.

Створити об'єкт типу StringBuilder можна з наявного рядка. Після модифікації можна створити новий об'єкт класу String, використовуючи об'єкт класу StringBuilder. Наприклад:

String s = "abc";
StringBuilder sb1 = new StringBuilder(s);    // Виклик конструктора
StringBuilder sb2 = new StringBuilder("cd"); // Виклик конструктора
// модифікація sb1 та sb2
// ...
String s1 = new String(sb1); // Виклик конструктора
String s2 = sb2 + "";        // Перетворення типів

Окрім деяких типових для класу String функцій, таких як length(), charAt(), indexOf(), substring(), клас StringBuilder надає низку методів для модифікації вмісту. Це такі методи, як append(), delete(), deleteCharAt(), insert(), replace(), reverse(), та setCharAt(). Розглянемо використання цих функцій на наведеному нижче прикладі:

public class StringBuilderTest {

    public static void main(String[] args) {
        String s = "abc";
        StringBuilder sb = new StringBuilder(s);
        sb.append("d");         // abcd
        sb.setCharAt(0, 'f');   // fbcd
        sb.delete(1, 3);        // fd
        sb.insert(1, "gh");     // fghd
        sb.replace(2, 3, "mn"); // fgmnd
        sb.reverse();           // dnmgf
        System.out.println(sb);
    }

}

Використання StringBuilder може підвищити ефективність роботи програми у випадках, коли певний рядок зазнає багаторазових модифікацій протягом роботи програми. Але важливо пам'ятати, що декілька посилань вказують на один об'єкт типу StringBuilder. Тому, коли ми його змінюємо, усі посилання вказуватимуть на змінений рядок.

2.6.3 Поділ рядка на лексеми

Існує кілька способів поділу рядка на лексеми. Найпростіший спосіб – використання класу java.util.StringTokenizer. Об'єкт цього класу створюється за допомогою конструктора з параметром типу String, який визначає рядок, що підлягає поділу на лексеми:

StringTokenizer st = new StringTokenizer(someString);

Після створення об'єкта можна отримати загальну кількість лексем за допомогою методу countTokens(). Клас реалізує внутрішній "поточний вказівник", який вказує на наступне слово. Функція nextToken() повертає наступну лексему з рядка. Функції nextToken() можна задати альтернативний роздільник лексем як параметр. За допомогою функції hasMoreTokens() можна перевірити, чи є ще лексеми. У наведеному нижче прикладі всі слова рядка виводяться в окремих рядках:

package ua.inf.iwanoff.java.second;

import java.util.*;

public class AllWords {

    public static void main(String[] args) {
        String s = new Scanner(System.in).nextLine();
        StringTokenizer st = new StringTokenizer(s);
        while (st.hasMoreTokens()) {
            System.out.println(st.nextToken());
        }
    }

}

Більш сучасний спосіб розбиття на лексеми – використання методу split() класу String. Параметр цього методу – так званий регулярний вираз, який визначає роздільники. Регулярні вирази дозволяють визначати шаблони для рядків. Наприклад, "\\s" – це будь-який символ-роздільник. Однак у найпростішому випадку можна використовувати безпосередньо розділовий символ – пропуск. Наприклад:

String s = "aa bb ccc";
String[] a = s.split(" ");
System.out.println(Arrays.toString(a)); // [aa, bb, ccc]

2.7 Класи-обгортки

Класи Integer, Double, Boolean, Character, Float, Byte, Short і Long дозволяють представити числові та булеві значення в об'єктах. Тому ці класи також називають класами-обгортками. Додатково ці класи надають набір методів для перетворення арифметичних значень у представлення рядком і навпаки й інші засоби для зручної роботи з цілими й дійсними числами.

Статичний метод Double.parseDouble() повертає дійсне число за представленням у вигляді рядка:

String s = "1.2";
double d = Double.parseDouble(s);

Аналогічно працюють функції Integer.parseInt(), Long.parseLong(), Float.parseFloat(), Byte.parseByte(), Short.parseShort() та Boolean.parseBoolean().

У версії Java 5 (JDK 1.5) об'єкти типу Integer можна ініціалізувати виразами цілого типу, використовувати у виразах для одержання значень (автоматичне упакування / розпакування). Цілі значення (константи) можна заносити в списки й інші контейнери. Автоматично будуть створюватися і заноситися в контейнер об'єкти типу Integer. У попередній версії Java (JDK 1.4) необхідно було писати:

Integer m = new Integer(10); // ініціалізуємо об'єкт типу Integer
int k = m.intValue() + 1;    // використовуємо значення у виразі
// створюємо масив:  
Integer[] a = {new Integer(1), new Integer(2), new Integer(3)};
a[2] = new Integer(4);       // заносимо об'єкт з новим цілим значенням
// отримаємо об'єкт типу Integer з масиву і використовуємо значення:
int i = a[1].intValue() + 2;    

Тепер усе простіше:

Integer m = 10; // ініціалізуємо об'єкт типу Integer
int k = m + 1;  // використовуємо значення у виразі
// створюємо масив:  
Integer[] a = {1, 2, 3};
a[2] = 4;       // заносимо об'єкт з новим цілим значенням
// отримаємо об'єкт типу Integer з масиву і використовуємо значення:
int i = a[1] + 2;

Примітка: автоматичне упакування і розпакування – це операція, яка вимагає додаткових ресурсів, зокрема, неявного створення об'єктів; тому, наприклад операція m++ для змінної m типу Integer у циклі виконуватиметься вкрай неефективно.

Те ж саме стосується інших типів-обгорток. Фактично об'єкти цих можуть бути використані замість змінних відповідних примітивних типів. Недоліком використання типів-обгорток є зменшення ефективності внаслідок додавання операцій розміщення у динамічній пам'яті. Але перевагою є можливість використання значення null. Наприклад, якщо значення функції не може бути обчислене, функція може повернути null:

package ua.inf.iwanoff.java.second;

import java.util.Scanner;

public class Reciprocal {

    // Зворотна величина:
    static Double reciprocal(double x) {
        if (x == 0) {
            return null;
        }
        return 1 / x;
    }

    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);
        double x = s.nextDouble();
        Double y = reciprocal(x);
        if (y == null) {
            System.out.println("Помилка");
        }
        else {
            System.out.println(y);
        }
    }

}

Клас Character – це, в першу чергу, оболонка для зберігання значення примітивного типу char (символ) в об'єкті. Об'єкт типу Character містить одне поле типу char. Крім того, цей клас надає кілька методів для визначення категорії символу (малі літери, цифри тощо) і для перетворення символів з верхнього регістру в нижній і навпаки.

Є можливість переводити окремий символ у верхній (або нижній) регістр:

char c1 = 'a';
char c2 = Character.toUpperCase(c1); // 'A'
char c3 = Character.toLowerCase(c2); // 'a'

Є функції, що дозволяють перевіряти властивості символів. Наприклад, метод Character.isLetter() повертає true, якщо символ є літерою в англійській, українській, російській, китайській, німецькій, арабській або іншій мові. Нижче наведені деякі найбільш корисні методи порівняння символів:

  • isDigit() повертає true, якщо символ є цифрою
  • isLetter() повертає true, якщо символ є літерою
  • isLetterOrDigit() повертає true, якщо символ є літерою або цифрою
  • isLowerCase() повертає true, якщо символ є літерою в нижньому регістрі
  • isUpperCase() повертає true, якщо символ є літерою у верхньому регістрі
  • isSpaceChar() повертає true, якщо символ є роздільником – символом пробілу, нового рядка або символом табуляції.

2.8 Використання аргументів командного рядка

У Java можна організувати читання аргументів з командного рядка (окремі слова, набрані в командному рядку після імені головного класу). Наприклад, наведена нижче програма виводить перший аргумент командного рядка на екран:

public static void main(String[] args) {
    System.out.println(args[0]);
}

Аргументи командного рядка у програмі представлені у вигляді масиву рядків. Для того, щоб отримати числове значення, яке представлено рядком, використовують функції класів Integer та Double. У наведеному нижче прикладі програма здійснює читання з командного рядка цілого та дійсного значень та знаходить їхню суму.

public class TestArgs {

    public static void main(String[] args) {
        int n = Integer.parseInt(args[0]);
        double x = Double.parseDouble(args[1]);
        double y = n + x;
        System.out.println(y);
    }

}

Кількість аргументів, які були уведені в командному рядку, можна отримати за допомогою виразу args.length.

Для визначення аргументів командного рядка в середовищі IntelliJ IDEA спочатку слід додати конфігурацію часу виконання (Run | Edit Configurations...). У вікні Run/Debug Configurations додаємо нову конфігурацію (Application) за допомогою кнопки +, вибираємо нашу програму (Main class), додаємо аргументи (Program arguments) і запускаємо програму. Якщо аргумент – це рядок, який містить кілька слів, його необхідно брати в лапки.

3 Приклади програм

3.1 Обчислення факторіалів

Припустимо, необхідно розробити функцію обчислення факторіалів (від 0 до 20 включно) з використанням допоміжного масиву (статичного поля). Під час першого виклику функції масив заповнюється до необхідного числа. Під час наступних викликів число або повертається з масиву, або обчислюється з використанням останнього числа, що зберігається у масиві, з подальшим заповненням масиву.

Необхідно також здійснити тестування функції для різних чисел, що вводяться у довільному порядку. Програма матиме такий вигляд:

package ua.inf.iwanoff.java.second;

import java.util.Arrays;

public class Factorial {
    private static long[] f = new long[30];
    private static int last = 0;

    static {
        f[0] = 1;
    }

    public static long factorial(int n) {
        if (n > last) {
            for (int i = last + 1; i <= n; i++) {
                f[i] = i * f[i - 1];
            }
            last = n;
        }
        return f[n];
    }
  
    public static void main(String[] args) {
        System.out.println(factorial(5));
        System.out.println(Arrays.toString(f));
        System.out.println(factorial(1));
        System.out.println(Arrays.toString(f));
        System.out.println(factorial(3));
        System.out.println(Arrays.toString(f));
        System.out.println(factorial(6));
        System.out.println(Arrays.toString(f));
        System.out.println(factorial(20));
        System.out.println(Arrays.toString(f));
    }

}

У функції main() виводяться значення факторіалів у довільному порядку. Можна також простежити поступову зміну вмісту масиву f.

3.2 Сума цифр

Припустимо, необхідно обчислити суму цифр числа. Можна запропонувати два підходи. Перший підхід використовує знаходження залишку від ділення.

package ua.inf.iwanoff.java.first;

import java.util.Scanner;

public class SumOfDigits {

    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);
        int n = s.nextInt();
        int sum = 0;
        while (n > 0) {
            sum += n % 10;
            n /= 10;
        }
        System.out.println(sum);
    }

}

Для знаходження суми цифр цілого числа можна також скористатись його представленням у вигляді рядка:

package ua.inf.iwanoff.java.first;

public class SumOfDigitsUsingStrings {

    public static void main(String[] args) {
        String n = args[0];
        int sum = 0;
        for (int i = 0; i < n.length(); i++) {
            sum += Integer.parseInt(n.charAt(i) + "");
        }
        System.out.println(sum);
    }

}

3.3 Видалення зайвих пропусків

У наведеному нижче прикладі з рядка, прочитаного з першого аргументу командного рядка, видаляються зайві пропуски (залишаємо по одному).

package ua.inf.iwanoff.java.second;

public class SpaceRemover {

    public static void main(String[] args) {
        System.out.println(args[0]);
        String s = args[0];
        while (s.indexOf("  ") >= 0) {
            s = s.replaceAll("  ", " ");
        }
        System.out.println(s); 
    }

}

Перед виконанням програми необхідне значення слід встановити в налаштуваннях конфігурації часу виконання на закладці Arguments у вікні Program arguments. Якщо рядок містить кілька слів, його необхідно брати в лапки. Якщо як аргумент заданий, наприклад, рядок

"To    be  or not to                be"

отримаємо рядок

To be or not to be

3.4 Робота з масивом рядків

Припустимо, необхідно створити програму, в якій одновимірний масив заповнюється випадковими додатними цілими числами, створюється масив рядків, довжина кожного з них визначена елементами першого масиву. В рядках відповідну кількість разів повторюється певний символ, наприклад, символ підкреслення. Потім повинно здійснюватися сортування масиву за збільшенням і за зменшенням довжини рядків.

Перша реалізація передбачає використання традиційних синтаксичних конструкцій та явних циклів:

package ua.inf.iwanoff.java.second;

import java.util.Random;

/**
 * Цей клас відповідає за демонстрацію роботи з одновимірними масивами
 * традиційними засобами
 */
public class WithoutArraysClass {
    public static void main(String[] args) {
        // Constants for a more convenient assignment of the sorting criterion:
        final boolean increaseLength = true;
        final boolean decreaseLength = false;

        // Створення та заповнення масиву цілих чисел:
        final int n = 10;
        int[] numbers = new int[n];
        Random random = new Random();
        for (int i = 0; i < numbers.length; i++) {
            numbers[i] = random.nextInt() % n + 10;
        }
        printIntArray(numbers);

        // Створення та заповнення масиву рядків:
        String[] lines = new String[n];
        for (int i = 0; i < numbers.length; i++) {
            lines[i] = "_".repeat(numbers[i]);
        }
        printStringArray(lines);
        System.out.println();

        // Сортування рядків у порядку збільшення та зменшення:
        sort(lines, increaseLength);
        printStringArray(lines);
        System.out.println();
        sort(lines, decreaseLength);
        printStringArray(lines);
    }

    /**
     * Виводить елементи масиву цілих у стандартний вихідний потік
     *
     * @param arr масив цілих, який слід вивести
     */
    public static void printIntArray(int[] arr) {
        System.out.printf("[");
        for (int i = 0; i < arr.length - 1; i++) {
            System.out.printf("%d, ", arr[i]);
        }
        System.out.printf("%d]\n", arr[arr.length - 1]);
    }

    /**
     * Виводить елементи масиву рядків у стандартний вихідний потік
     *
     * @param arr масив рядків, який слід вивести
     */
    public static void printStringArray(String[] arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }

    /**
     * Сортує масив рядків за довжиною.
     * Використовується сортування бульбашкою
     *
     * @param arr масив для сортування
     * @param increaseLength повинен бути true для сортування в зростаючому порядку
     *                       і false у протилежному випадку
     */
    public static void sort(String[] arr, boolean increaseLength) {
        boolean mustSort;// повторюємо доти, доки mustSort true
        do {
            mustSort = false;
            for (int i = 0; i < arr.length - 1; i++) {
                if (increaseLength ?
                        arr[i].length() > arr[i + 1].length() :
                        arr[i].length() < arr[i + 1].length()) {
                    // Міняємо елементи місцями:
                    String temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                    mustSort = true;
                }
            }
        }
        while (mustSort);
    }

}

Друга реалізація побудована на використанні функцій класу Arrays:

package ua.inf.iwanoff.java.second;

import java.util.Arrays;
import java.util.Random;

/**
 * Цей клас відповідає за демонстрацію роботи з одновимірними масивами
 * за допомогою методів класу Arrays і лямбда-виразів
 */
public class WithArraysClass {
    public static void main(String[] args) {
        // Створення та заповнення масиву цілих чисел:
        final int n = 10;
        int[] numbers = new int[n];
        Random random = new Random();
        Arrays.setAll(numbers, i -> random.nextInt() % n + 10);
        System.out.println(Arrays.toString(numbers));

        // Створення та заповнення масиву рядків:
        String[] lines = new String[n];
        Arrays.setAll(lines, i -> "\n" + "_".repeat(numbers[i]));
        System.out.println(Arrays.toString(lines));

        // Сортування рядків у порядку збільшення та зменшення:
        Arrays.sort(lines); // рядки, що містять однакові символи, сортуються за довжиною
        System.out.println(Arrays.toString(lines));
        Arrays.sort(lines, (s1, s2) -> -Integer.compare(s1.length(), s2.length()));
        System.out.println(Arrays.toString(lines));
    }
}

Як видно, друга реалізація значно більш прозора й компактна. Додаткові символи переведення рядків забезпечують більш наочне виведення в стандартний потік.

Недоліком реалізації з застосуванням можливостей класу Arrays є неможливість керування формою і процесом виведення елементів масиву в консольне вікно. Цей недолік можна подолати через використання так званих потоків даних (Stream API), які будуть розглянуті пізніше.

3.5 Знаходження мінімальних значень у стовпцях двовимірного масиву

Припустимо, необхідно створити двовимірний масив цілих чисел, заповнити його випадковими значеннями у визначеному діапазоні, а потім знайти й заповнити одновимірний масив мінімальними значеннями серед елементів стовпців. Бажано використовувати функції класу Arrays замість явних циклів. Програма може бути такою:

package ua.inf.iwanoff.java.second;

import java.util.Arrays;
import java.util.Random;

/**
 * Клас демонструє можливість отримання мінімальних значень стовпців
 * у двовимірному масиві
 */
public class TwoDArrayDemo {
    public static void main(String[] args) {
        // Розміри масиву:
        final int m = 3;
        final int n = 4;

        // Діапазон можливих значень елементів:
        final int from = 3;
        final int to = 8;

        // Створення та заповнення масиву:
        int[][] arr = new int[m][];
        Random random = new Random();
        Arrays.setAll(arr, i -> fillRow(random, n, from, to));
        System.out.println(Arrays.deepToString(arr));

        // Отримання масиву мінімальних значень
        int[] mins = new int[n];
        Arrays.setAll(mins, j -> getMinInColumn(arr,  j));
        System.out.println(Arrays.toString(mins));
    }

    /**
     * Створює масив цілих чисел і заповнює його значеннями із заданого діапазону
     *
     * @param random current Random type object used for filling row
     * @param size довжина масиву
     * @param from ліва межа діапазону
     * @param to права межа діапазону
     * @return масив цілих чисел із випадковими значеннями
     */
    public static int[] fillRow(Random random, int size, int from, int to) {
        int[] result = new int[size];
        Arrays.setAll(result, j -> Math.abs(random.nextInt() % (to - from)) + from);
        return result;
    }

    /**
     * Обчислює мінімальне значення елементів стовпця у двовимірному масиві
     *
     * @param arr вихідний масив
     * @param j індекс стовпця
     * @return мінімальне значення
     */
    public static int getMinInColumn(int[][] arr, int j) {
        int[] column = new int[arr.length];
        Arrays.setAll(column, i -> arr[i][j]);
        Arrays.sort(column);
        return column[0];
    }
}

Для знаходження мінімального елементу в одновимірному масиві (стовпці) здійснюється його сортування за збільшенням. Нульовий елемент такого масиву буде мінімальним.

4 Вправи для контролю

  1. Увести з клавіатури кількість елементів та елементи одновимірного масиву цілих чисел. Відсортувати масив за збільшенням елементів.
  2. Увести з клавіатури рядок (String), змінити порядок символів на зворотний та вивести рядок на екран.
  3. Увести рядок, видалити літеру "а", вивести рядок на екран.
  4. Проініціалізувати одновимірний масив рядків. Відсортувати масив за алфавітом у зворотному порядку.
  5. Створити клас з конструктором для опису точки в тривимірному просторі.
  6. Створити клас з конструктором для опису товару (зберігаються назва та ціна).
  7. Створити клас з конструктором для опису користувача (зберігаються ім'я та пароль).

5 Контрольні запитання

  1. Чим відрізняється посилання Java від указівника C++?
  2. Чим типи-посилання відрізняються від типів-значень?
  3. Як у Java здійснюється розіменування?
  4. Що є результатом присвоєння одного посилання іншому?
  5. Як у Java видалити об'єкт, який було створено за допомогою new?
  6. У чому полягає "збирання сміття"?
  7. Чи можна використовувати змінні для визначення довжини масиву?
  8. Чи можна змінити розміри масиву за допомогою поля length?
  9. Як додати новий елемент у кінець масиву?
  10. Як визначити кількість стовпців двовимірного масиву?
  11. Чи можна створити двовимірний масив з різною довжиною рядків?
  12. Чим відрізняється застосування двох різних конструкцій for для обходу елементів масиву?
  13. Чим визначається розмір масиву, який створюється функцією arraycopy()?
  14. Чи можна за допомогою arraycopy() скопіювати частину масиву?
  15. Як здійснити читання масиву з клавіатури?
  16. У які способи можна заповнити елементи масиву без циклу?
  17. Як без циклу встановити, що елементи масивів збігаються?
  18. Чи можна без циклу відсортувати частину масиву?
  19. Чи дозволяє функція binarySearch() здійснити пошук у невідсортованому масиві?
  20. Чи можна змінити значення елементів масиву за допомогою функції?
  21. Як у Java створити функцію зі змінною кількістю аргументів?
  22. З яких основних елементів складається опис класу?
  23. Чи завжди необхідно явно ініціалізувати поля класу?
  24. Чи можна в Java поза класом реалізовувати методи, оголошені всередині класу?
  25. Чим відрізняються статичні та нестатичні елементи класу?
  26. Як здійснюється ініціалізація статичних даних?
  27. Де може бути розташована конструкція ініціалізації?
  28. Як у Java визначається дружній доступ до елементів класу?
  29. Як встановити рівень доступу для групи елементів?
  30. У чому полягає зміст інкапсуляції та як вона реалізована в Java?
  31. Як можна використовувати посилання this?
  32. Як викликати конструктори з інших конструкторів?
  33. Скільки конструкторів без параметрів може бути створено в одному класі?
  34. Як створити клас, у якому немає жодного конструктора?
  35. Чому в Java немає деструкторів?
  36. Коли викликається метод finalize()?
  37. У яких випадках доцільно використовувати композицію класів?
  38. Чи можна в Java цілком розмістити один об'єкт усередині іншого об'єкта?
  39. Чи можна змінити вміст раніше створеного рядка?
  40. Як змінити конкретний символ у рядку?
  41. У чому є недоліки й переваги класу StringBuilder у порівнянні з класом String?
  42. Як перевести число в його рядкове представлення і навпаки?
  43. Які є недоліки й переваги застосування об'єктів класу Integer замість змінних типу int?