МІНІСТЕРСТВО ОСВІТИ ТА НАУКИ УКРАЇНИ
НАЦІОНАЛЬНИЙ УНІВЕРСИТЕТ “ЛЬВІВСЬКА ПОЛІТЕХНІКА”
“Класи та об’єкти в Java”
Методичні вказівки
до лабораторної роботи №2
з курсу “Об’єктно-орієнтоване програмування”
для студентів базового напрямку
6.0804 “Комп’ютерні науки”
ЗАТВЕРДЖЕНО
на засіданні кафедри “Системи автоматизованого проектування” Протокол № 1від 30.08.2010
ЛЬВІВ 2010 “Класи та об’єкти Java”. Методичні вказівки до виконання лабораторної роботи №2 з курсу: “Об’єктно-орієнтоване програмування” для студентів базового напрямку 6.0804 “Комп’ютерні науки”.
Укладачі: Керницький А.Б., ст.викл., др.інж.
Каркульовський В.І., доцент, к.т.н.
Загарюк Р.В., асистент, к.т.н.
Відповідальний за випуск:
Рецензенти:
1. МЕТА РОБОТИ
Ознайомитися із сучасною парадигмою програмування — об’єктно-орієнтованим програмуванням і реалізацією цієї парадигми в мові Java. Розібратися із правилами запису класів і вивчити їхні додаткові можливості.
2.КОРОТКІ ТЕОРЕТИЧНІ ВІДОМОСТІ
2.1. ПРИНЦИПИ ОБ’ЄКТНО-ОРІЄНТОВАНОГО ПРОГРАМУВАННЯ
Об’єктно-орієнтоване програмування розвивається уже більше двадцати років. Існує декілька шкіл, кожна з яких пропонує свій набір принципів роботи з об’єктами і по-своєму викладає ці принципи. Але є декілька загальноприйнятих понять. На даній лабораторній роботі ми їх вивчемо.
2.1.1. Абстракція
Описуючи поведінку будь-якого об’єкта, наприклад автомобіля, ми будуємо його модель. Модель, як правило, не може описати об’єкт повністю оскільки реальні об’єкти досить складні. Приходиться відбирати лише ті характеристики об’єкта, які є важливими для розв’язання поставленої перед нами задачі. Для опису вантажоперевезень важливою характеристикою буде вантажопідйомність автомобіля, а для опису автомобільних перегонів вона є несуттєвою. Для моделювання перегонів обов’язково необхідно описати метод набирання швидкості даною автівкою, а для вантажоперевезеннь це не важливо.
Ми повинні абстрагуватися від деяких конкретних деталей об’єкта. Дуже важливо вибрати правильну степінь абстракції. Поверхнева степінь дасть лише приблизний опис об’єкта, не дозволить правильно моделювати його поведінку. Глибока степінь абстракції зробить модель надто складною, перевантаженою деталями, і тому непридатною.
Наприклад, можна цілком точно предбачити погоду на завтра у певному місці, але розрахунки за такою моделлю триватимуть три доби навіть на найпотужнішому комп’ютері. Модель, що запізнюється на два дні нікому не потрібна. Точність моделі, якою користуються синоптики, всім відома. Поте розрахунки за нею займають всього декілька годин.
Опис кожної моделі робиться у вигляді одного або декількох класів (classes). Клас можна вважати проектом, зліпком, кресленням, за яким потім будуть створюватися конкретні об’єкти. Клас містить опис змінних і констант, які характеризують об’єкт. Вони називаються полями класу (class fields). Процедури, які описують поведінку об’єкта, називаються методами класу (class methods). Всередині класу можна описати і вкладені класи (nested classes) і вкладені інтерфейси. Поля, методи і вкладені класи першого рівня є членами класу (class members). Різні школи об’єктно-орієнтованного програмування пропонують різні терміни. На лабораторному практикумі ми використовуватимемо термінологію, прийняту в технології Java.
Ось зразок опису автівки:
class Car {
int maxVelocity; // Поле, що містить найбільшу швидкість автівки
int speed; // Поле, що містить поточну швидкість автівки
int weight; // Поле, що містить вагу автівки
// Інші поля...
void moveTo(int x, int у){ // Метод, що моделює переміщення автівки. Параметри х і у — не поля
int а = 1; // Локальна змінна — не поле
// Тіло метода. Тут описується закон переміщення автівки у точку (х, у)
}
// Інші методи. . .
}
Після того як опис класу завершено, можна створювати конкретні об’єкти, eкземпляри (instances) описаного класу. Створення екземплярів відбувається у три етапи, подібно до опису масивів. Спочатку оголошуються посилання на об’єкти: записується назва класу, і через пробіл перераховуються екземпляри класу, точніше, посилання на них.
Car alfaRomeo156, fordFocus, fiat500;
Потім операцією new визначаються самі об’єкти, під них виділяється оперативна пам’ять, посилання отримує адресу цієї частини пам’яті в якості свого значення.
alfaRomeo156= new Car();
fordFocus = new Car();
fiat500 = new Car();
На третьому етапі відбувається ініціалізація об’єктів, задаються початкові значення. Цей етап, як правило, суміщається з другим, якраз для цього в операції new повторяється назва класу з дужками Car (). Це так званий конструктор (constructor) класу.
Оскільки назви полів, методів і вкладених класів у всіх об’єктах одинакові, вони задані в описі класу, їх треба уточняти назвою посилання на об’єкт:
alfaRomeo156.maxVelocity = 250;
fordFocus.maxVelocity = 180;
fiat500.maxVelocity = 150;
fiat500.moveTo(35, 120);
Текстовий рядок в лапках вважається в Java об’єктом класу String. Тому можна написати: int strlen = "Це об’єкт класу String".length();
Об’єкт "рядок" виконує метод length(), один із методів свого класу String, який підраховує кількість символів у рядку. У результаті одержимо значення strlen, яке дорівнює 21.
2.1.2. Ієрархія
Ієрархія об’єктів давно використовується для їх класифікації. Особливо детально вона опрацьована у біології. Всі знайомі з сімействами, родами і видами. Ми можемо зробити опис своїх домашніх тварин (pets): кішок (cats), собак (dogs), корів (cows) та інших наступним чином:
class Pet{ // Тут описуємо спільні властивості всіх домашніх тварин
Master person; // Господар тварини
int weight, age, eatTime; // Вага, вік, час годування
int eat(int food, int drink, int time){ // Процес годування
// Початкові дії...
if (time == eatTime) person.getFood(food, drink);
// Метод приймання їжі
}
void voice(); // Звуки, які видають тварини
// Інше...
}
Потім створюємо класи, які описують конкретніші об’єкти, зв’язуючи їх із спільним класом:
class Cat extends Pet{ // Описуються властивості, властиві лише кішкам:
int mouseCatched; // кількість спійманих мишей
void toMouse(); // процес ловіння мишей
// інші властивості
}
class Dog extends Pet{ // Властивості собак:
void preserve(); // охороняти
}
Зверніть увагу, що ми не повторюємо спільні властивості, описані у класі Pet. Вони наслідуються автоматично. Ми можемо визначити об’єкт класу Dog і використовувати у ньому всі властивості класу Pet так, ніби вони описані у класі Dog:
Dog Brovko = new Dog(), Artur = new Dog();
Після такого визначення можна буде написати
Brovko.age = 3;
int p = Artur.eat (30, 10, 12);
А класифікацію продовжити таким чином:
class Pointer extends Dog{ ... } // Властивості породи пойнтер
class Setter extends Dog{ ... } // Властивості сеттерів
Зверніть увагу, що на кожному наступному рівні ієрархії у клас додаються нові властивості, але жодна властивість не пропадає. Тому використовується слово extends — "розширює" і говорять, що клас Dog — розширення (extension) класу Pet. З іншої сторони, кількість об’єктів при цьому зменшується: собак менше, ніж всіх домашніх тварин. Тому часто говорять, що клас Dog — підклас (subclass) класу Pet, а клас Pet — суперклас (superclass) класу Dog.
Часто використовують генеалогічну термінологію: батьківський клас, дочірний клас, клас-нащадок, клас-предок.
У цій термінології говорять про наслідування (inheritance) класів, у нашому прикладі клас Dog наслідує клас Pet.
Опишемо господаря тварини у класі Master:
class Master{ // Власник тварини
String name; // Прізвище, ім’я
// Інші дані
void getFood(int food, int drink); // Годівля
// Інше
}
Господар і його домашні тварини постійно контактують. Їх взаємодія виражається дієсловами "гуляти", "годувати", "охороняти", "чистити", "ластитися", "проситися" та іншими. Для опису взаємодії об’єктів застосовується третій принцип об’єктно-орієнтованого програмування — обов’язок або відповідальність.
2.1.3. Відповідальність
У нашому прикладі розглядається тільки взаємодія у процесі годування, яка описується методом eat(). У цьому методі тварина звертається до господаря, вмоляючи його застосувати метод getFood(). В англомовній літературі подібне звернення описується словом message "повідомлення". Чому не використовується словосполучення "виклик методу", адже говорять: "Виклик процедури"? Тому що між цими поняттями існує, принаймні, три відмінності.
Повідомлення іде до конкретного об’єкта, що знає метод розв’язання задачі, у прикладі цей об’єкт — біжуче значення змінної person. У кожного об’єкта є свій біжучий стан, свої значення полів класу, і це може вплинути на виконання метод.
Спосіб виконання доручення, що міститься в повідомленні, залежить від об’єкта, якому воно адресоване. Один госодар поставить миску з "Chappi", другий кине кістку, третій вижене собаку на вулицю. Цю цікаву властивість називають поліморфізмом (polymorphism).
Звернення до методу відбудеться лише на етапі виконання програми, компілятор нічого не знає про метод. Це називається "пізнім зв’язуванням" на противагу "ранньому зв’язуванню", при якому процедура приєднується до програми на етапі компонування.
Отже, обєкт Brovko, виконуючи свій метод eat(), посилає повідомлення об’єкту, посилання на який міститься у змінній person, із проханням видати йому певну кількість їжі і води. Повідомлення записане у рядку person.getFood(food, drink).
Цим повідомленням заключається контракт (contract) між об’єктами, суть якого у тому, що об’єкт Brovko бере на себя відповідальність (responsibility) задати правильні параметри в повідомленні, а об’єкт — біжуче значення person — бере на себе відповідальність застосувати метод годівлі getFood(), яким би він не був.
Для того, щоб правильно реалізувати принцип відповідальності, застосовується четвертий принцип об’єктно-орієнтованого програмування — модульність (modularity).
2.1.4. Модульність
Цей принцип стверджує — кожний клас повинен складати окремий модуль. Члени класу, до яких не планується звертання зовні, повинні бути інкапсульовані. В мові Java інкапсуляция досягається додаванням модифікатора private до опису члена класу. Наприклад:
private int mouseCatched;
private String name;
private void preserve();
Ці члени класів стають закритими, ними можуть користуватися лише екземпляри того ж самого класу, наприкладр, Brovko може дати доручення
Brovko.preserve().
А якщо у класі Master ми напишемо: private void getFood(int food, int drink); тоді метод getFood() не буде знайдено, і Brovko не зможе отримати їжу.
В протилежність закритості ми можемо оголосити певні члени класу відкритими, записавши замість слова private модифікатор public, наприклад:
public void getFood(int food, int drink);
До таких членів може звернутися будь-який об’єкт будь-якого класу.
Принцип модульності предбачає відкривати члени класу тільки у випадку необхідності. Якщо потрібно звернутися до поля класу, тоді рекомендується включити у клас спеціальні методи доступу (access methods), окремо для читання цього поля (get method) і для запису у це поле (set method). Назви методів доступу рекомендується починати зі слів get і set, додаючи до цих слів назву поля. Для JavaBeans ці рекомендації введені у ранг закону.
У нашому прикладі класу Master методи доступу до поля Name у самому простому випадку можуть виглядати наступним:
public String getName(){
return name;
}
public void setName(String newName)
{
name = newName;
}
У реальних ситуаціях доступ обмежується різними перевірками, особливо у set-методах, що змінюють значення полів. Можна перевіряти тип значения, що вводиться, задавати діапазон значень, зрівнювати зі списком допустимих значень.
Крім методів доступу рекомендується створювати провірочні is-методи, які повертають логічні значення true або false. Наприклад, у клас Master можна включити метод, який перевіряє, чи задана назва господаря:
public boolean isEmpty(){
return name == null ? true : false;
}
і використовувати цей метод для перевірки при доступі до поля Name, наприклад:
if (masterOl.isEmpty()) masterOl.setName(“Керницький”);
Таким чином, ми залишаємо відкритими лише методи, необхідні для взаємодії об’єктів. При цьому зручно спланувати класи таким чином, щоб залежність між ними була найменшою, як прийнято говорити у теорії ООП, було найменше зачеплення (low coupling) між класами. Тоді структура програми сильно спрощується. Крім того, такі класи зручно використовувати як будівельні блоки для побудови інших програм.
Навпаки, члени класу повинні активно взаємодіяти один з одним, як говорять, мати тісну функціональну зв’язність (high cohesion). Для цього у клас належить включати всі методи, які описують поведінку моделюючого об’єкта. Одне із правил досягнення сильної функціональної зв’язності, введенне Карлом Ліберхером (Karl J. Lieberherr), отримало назву закон Деметра. Закон говорить: "у методі m() класу А належить використовувати тільки методи класу А, методи класів, до яких належать аргументи метода m(), і методи класів, екземпляри яких створюються всередині метода m().
Чи будуть закриті члени класу доступні його нащадкам? Якщо в класі Pet написано private Master person; то чи можна використовувати Brovko.person? Зрозуміло, що ні. Адже у протилежному випадку кожний, хто цікавиться закритими полями класу А, може розширити його класом B, і продивитися закриті поля класу А через екземпляри класу B.
Коли потрібно дозволити доступ нащадкам класу, але небажано віткривати його всьому світу, тоді у Java використовується захищений (protected) доступ, позначений модифікатором protected, наприклад, об’єкт Brovko може звернутися до поля person батьківського класу Pet, якщо у класі Pet це поле описано наступним чином:
protected Master person;
На доступ до члена класу впливає ще і пакет, у якому знаходиться клас.
Із цього загального схематичного опису принципів об’єктно-орієнтованого програмування видно, що мова Java дозволяє легко втілювати всі ці принципи.
2.2. ОПИС КЛАСІВ І ПІДКЛАСІВ
Опис класу починаеться зі слова class, після якого записується назва класу. "Code Conventions" рекомендує починать назву класу із великої літери.
Перед словом class можна записати модифікатори класу (class modifiers). Це одне із слів public, abstract, final, strictfp. Перед назвою вкладеного класу можна поставити, крім цього, модифікатори protected, private, static.
Тіло класу, в якому в будь-якому порядку перераховуються поля, методи, вкладені класи та інтерфейси, заключається у фігурні дужки.
При описі поля вказується його тип, потім, через пробіл, назву і, може бути, початкове значення після знака рівності, яке можна записати константним виразом.
Опис поля може починатися з одного або декількох необов’язкових модифікаторів public, protected, private, static, final, transient, volatile. Якщо потрібно поставити декілька модифікаторів, то JLS рекомендує перераховувати їх у вказаному порядку, оскільки деякі компілятори вимагають певного порядку запису модифікаторів.
При описі методу вказується тип значення, який він повертає або слово void, потім, через пробіл, назву метода, потім, у дужках, список параметрів. Після цього у фігурних дужках розписується виконуваний метод.
Опис методу може починатися із модифікаторів public, protected, private, abstract, static, final, synchronized, native, strictfp.
У списку параметрів через кому перераховуються тип і назва кожного параметра. Перед типом будь-якого параметра може стояти модифікатор final. Такий параметр не можна змінювати всередині метода. Список параметрів може бути відсутнім, але дужки зберігаються.
Перед початком роботи методу для кожного параметра виділяється комірка оперативної пам’яті, у яку копіюється значення параметра, задане при зверненні до метода. Такий спосіб називається передачею параметрів за значенням.
У лістинзі 2.1 показано, як можна оформити метод ділення пополам для знаходження кореня нелінійного рівняння.
Лістинг 2.1. Знаходження кореня нелінійного рівняння методом бісекцій
class Bisection2{
private static double EPS = 1e-8; // Константа
private double a = 0.0, b = 1.5, root; // Закриті поля
public double getRoot(){return root;}
// Метод доступа
public double f(double x)
{
return x*x*x - 3*x*x + 3; // Або щось інше
}
public void bisect(){ // Параметрів немає — метод працює з полями екземпляра
double y = 0.0; // Локальна змінна — не поле
do{
root = 0.5 *(a + b); y = f(root);
if (Math.abs(y) < EPS) break;
// Корінь знайдено. Виходимо з циклу.
// Якщо на кінцях відрізка [a; root] функція має різні знаки:
if (f(a) * y < 0.0) b = root;
// значить, корінь тут. Переносимо точку b у точку root
//В протилежному випадку:
else a = root;
// переносимо точку а у точку root
// Продовжуємо, поки [а; b] не стане малим
} while(Math.abs(b-a) >= EPS);
}
}
class Test{
public static void main(String[] args){
Bisection2 b2 = new Bisection2();
b2.bisect();
System.out.println("x = " +
b2.getRoot() + // Звертаємося до кореня через метод доступу
", f() = " +b2.f(b2.getRoot()));
}
}
В описі методу f() збережений старий, процедурний стиль: метод одержує аргумент, опрацьовує його і повертає результат. Опис методу bisect() виконано у дусі ООП: метод активний, він сам звертається до полів екземпляра b2 і сам заносить результат у потрібне поле.
Назва метода, кількість і типи параметрів створюють сигнатуру (signature) метода. Компілятор розрізняє методи не за їхніми назвами, а за сигнатурами. Це дозволяє записувати різні методи з однаковими назвами, що відрізняються кількістю і/або типами параметрів. Тип значення, що повертається, не входить у сигнатуру метода, значить, методи не можуть розрізнятися тільки типом результу та їх роботою.
Наприклад, у класі Automobile ми записали метод moveTo(int x, int у), позначивши пункт призначення його географічними координатами. Можна визначити ще метод moveTo (string destination) для вказівки географічної назви пункту призначення і звертатися до нього таким чином:
oka.moveTo("Полтава") ;
Таке дублювання методів називається перевантаженням (overloading). Перевантаження методів дуже зручне у використанні. Якщо виводити дані різного типу на екран методом println() використовуються різні методи з однією і тією ж назвою println. Всі ці методи потрібно ретельно спланувати і заздалегідь описати у класі. Це зроблено у класі PrintStream, де представлено близько двадцати методів print() і println():
void
println()
Terminate the current line by writing the line separator string.
void
println(boolean x)
Print a boolean and then terminate the line.
void
println(char x)
Print a character and then terminate the line.
void
println(char[] x)
Print an array of characters and then terminate the line.
void
println(double x)
Print a double and then terminate the line.
void
println(float x)
Print a float and then terminate the line.
void
println(int x)
Print an integer and then terminate the line.
void
println(long x)
Print a long and then terminate the line.
void
println(Object x)
Print an Object and then terminate the line.
void
println(String x)
Print a String and then terminate the line.
Якщо ж записати метод з тією ж назвою у підкласі, наприклад:
class Truck extends Automobile{
void moveTo(int x, int y){
// Певні дії
}
// Ще щось
}
то він перекриє метод суперкласу. Визначивши екземпляр класу Truck, наприклад:
Truck gazel = new Truck();
і записавши gazel.moveTo(25, 150), ми звернемося до метода класу Truck. Відбудеться перевизначення (overriding) метода.
При перевизначенні права доступу до метода можна лише розширити. Відкритий метод public повинен залишатися відкритим, захищений protected може стати відткритим. Усередині підкласу можна звернутися до методу суперкласу, якщо уточнити назву метода словом super, наприклад, super.moveTo(30, 40). Можна уточнити і назву метода, записаного у цьому ж класі, словом this, наприклад, this.moveTo (50, 70), але у даному випадку це вже є зайвим. Таким же способом можна уточняти і співпадаючі назви полів, а не kbit методів.
Перевизначення методів призводить до цікавих результатів. У класі Pet було описано метод voice(). Перевизначимо його у підкласах і використаємо у класі chorus, як показано у лістинзі 2.2.
Лістинг 2.2. Приклад поліморфного метода
abstract class Pet{
abstract void voice();
}
class Dog extends Pet{
int k = 10;
void voice(){
System.out.println("Hav-hav!");
}
}
class Cat extends Pet{
void voice () {
System.out.println("Miaou!");
}
}
class Cow extends Pet{
void voice(){
System.out.println("Mu-u-u!");
}
}
public class Chorus{
public static void main(String[] args){
Pet[] singer = new Pet[3];
singer[0] = new Dog();
singer[1] = new Cat();
singer[2] = new Cow();
for (int i = 0; i < singer.length; i++)
singer[i].voice();
}
}
Вся справа тут у визначенні поля singer[]. Хоча масив singer [] має тип Pet, кожний його елемент посилається на об’єкт свого типу Dog, Cat, Сow. При виконанні програми викликається метод конкретного об’єкта, а не метод класу, яким визначалася назва посилання. Так у Java реалізується поліморфізм.
У мові Java всі методи є віртуальними функціями. Клас Pet і метод voice() є абстрактними.
2.3. АБСТРАКТНІ МЕТОДИ І КЛАСИ
При описі класу Pet ми не можемо задати у методі voice () жодного корисного алгоритму, оскільки у всіх тварин різні голоси. У таких випадках ми записуємо тільки заголовок метода і ставимо після списку параметрів і закриваючої дужки крапку з комою. Цей метод буде абстрактним (abstract), що необхідно вказати компілятору модифікатором abstract.
Якщо клас містить хоча б один абстрактний метод, то створити його екземпляри, а тим більше використовувати їх не вдасця. Такий клас стає абстрактним, що обов’язково треба вказати модифікатором abstract.
Абстрактні класи використовуються тільки породжуючи від них підкласи, в яких перевизначені абстрактні методи. Звернемося до лістингу 2.2., щоби розібратися для чого потрібні абстрактні класи.
Хоча елементи масиву singer [] посилаються на підкласи Dog, Cat, Cow, але все-таки це змінні типу Pet і посилатися вони можуть тільки на поля і методи, описані у суперкласі Pet. Додаткові поля підкласу для них є недоступними. Якщо звернутися, наприклад, до поля k класу Dog, написавши singer[0].k, тоді компілятор "скаже", що він не може реалізувати таке посилання. Тому метод, який реалізується у декількох підкласах, приходиться виносити у суперклас, а якщо там його неможливо реалізувати, тоді його необхідно оголосити абстрактним. Таким чином, абстрактні класи групуються на вершині ієрархії класів.
Можна задати порожню реалізацію методу, поставивши пару фігурних дужок, нічого не написавши між ними, наприклад:
void voice(){}
Отримується повноцінний метод. Але це штучне рішення, яке заплутає структуру класу. Замкнути ж ієрархію можна остаточними класами.
2.4. ОСТАТОЧНІ ЧЛЕНИ І КЛАСИ
Помітивши метод модифікатором final, можна заборонити його перевизначення у підкласах. Це зручно з метою безпеки. Можна бути впевненим, що метод виконує ті дії, які йому задані. Саме так визначені математичні функції sin(), cos() та інші у класі Math: метод Math.cos (x) обчислює саме косинус числа х. Зрозуміло, що такий метод не може бути абстрактним.
Для повної безпеки, поля, які обробляються остаточними методами, належить зробити закритими (private).
Якщо ж помітити модифікатором final увесь клас, тоді його взагалі не можна буде розширювати. Так визначений, наприклад, класс Math:
public final class Math{ . . . }
Для змінних модифікатор final має зовсім інший зміст. Якщо помітити модифікатором final опис змінної, тоді її значення (а воно повинно бути обов’язково задане або тут, або у блоці ініціалізації або в конструкторі) не можна змінити ні в підкласах, ні у самому класі. Змінна перетворюється у константу. У мові Java визначаються константи:
public final int MIN_VALUE = -1, MAX_VALUE = 9999;
Відповідно до "Code Conventions" константи записуються великими літерами, слова у них розділяються знаком підкреслення.
На самій вершині ієрархії класів Java стоїть клас Object.
2.5. КЛАС Object
Якщо при описі класу ми не вказуємо жодне розширення, тобто не пишемо слово extends і назву класу за ним, як при описі класу Pet, тоді Java вважає цей клас розширенням класу Оbject, і компілятор дописує це за нас:
class Pet extends Object{ . . . }
Це розширення можна записати і явно.
Сам клас Оbject не є нічиїм нащадком, від нього починається ієрархія всіх класів Java. Зокрема, всі масиви — прямі нащадки класу Оbject.
Оскільки такий клас може містити тільки загальні властивості всіх класів, у нього включено лише декілька найзагальних методів, наприклад, метод equals(), що порівнює даний об’єкт на рівність з об’єктом, заданим в аргументі, і який повертає логічне значення. Його можна використати таким чином:
Object obj1 = new Dog(), obj 2 = new Cat();
if (obj1.equals(obj2)) ...
Об’єкт obj1 є активним, він сам порівнює себя с другим об’єктом. Можна, звичайно, записати і obj2.equals(obj1), зробивши активним обєкт obj2, з тим самим результатом.
Як вказувалось у попередній лабораторній роботі, посилання можна порівнювати на рівність і нерівність:
obj1 == obj2; obj1 != obj 2;
У цьому випадку співставляються адреси об’єктів, і можна взнати, чи не вказують обидва посилання на один і той самий об’єкт.
Метод equals() порівнює вміст об’єктів у їх біжучому стані, фактично він реалізований у класі Оbject як тотожність: об’єкт дорівнює лише самому собі. Тому його часто перевизначають у підкласах. Правильно спроектовані класи повинні перевизначити методи класу Оbject, якщо їх не влаштовує стандартна реалізація.
Другий метод класу Оbject, який необхідно перевизначати у підкласах, — метод toString (). Цей метод без параметрів, який намагається вміст об’єкта претворити у рядок символів і повертає об’єкт класу String.
До цього методу виконуюча система Java звертається кожного разу, коли потрібно представити об’єкт у вигляді рядка, наприклад, у методі printing.
2.6. КОНСТРУКТОРИ КЛАСУ
В операції new, що визначає екземпляри класу, повторюється назва класу з дужками. Такий "метод" називається конструктором класу (class constructor). Перечислимо особливості конструктора^
Конструктор є в будь-якому класі. Навіть якщо він не вкзаний, компілятор Java сам створить конструктор за замовчуванням (default constructor), який, між іншим, пурожній, він не робить нічого, крім виклику конструктора суперкласу.
Конструктор виконується автоматично при створенні екземпляра класу, після розподілу пам’яті і обнулювання полів, але до початку використання створюваного об’єкта.
Конструктор не повертає жодного значення. Тому в його описі не пишеться навіть слово void, але можна задати один із трьох модифікаторів public, protected або private.
Конструктор не є методом, він навіть не вважається членом класу. Тому його не можна наслідувати або перевизначати у підкласі.
Тіло конструктора може починатися:
з виклику одного із конструкторів суперкласу, для цього записується слово super() з параметрами у дужках, якщо вони потрібні;
з виклику другого конструктора цього ж класу, для цього записується слово this() з параметрами у дужках, якщо вони потрібні.
Якщо ж super() на початку конструктора не вказаний, то спочатку виконується конструктор суперкласу без аргументів, потім відбувається ініціалізація полів значеннями, вказаними при їх оголошенні, а вже потім те, що записано у конструкторі.
У всьому іншому конструктор можна вважати звичайним методом, у ньому дозволяється записувати будь-які оператори, навіть оператор return, але тільки порожній.
У класі може бути декілька конструкторів. Оскільки у них одне і те ж ім’я, що співпадає з іменем класу, тоді вони повинні відрізнятися типом і/або кількістю параметрів.
2.7. ОПЕРАЦІЯ new
Операція, що позначається словом new застосовується для виділення пам’яті масивам і об’єктам.
У першому випадку в якості операнда вказується тип елементів масиву і кількість його елементів у квадратних дужках, наприклад:
double a[] = new double[100];
У другому випадку операндом служить конструктор класу. Якщо конструктора у класі немає, то викликається конструктор за замовчуванням.
Числові поля класу одержують нульові значення, логічні поля — значення false, посилання — значення null.
Результатом операції new буде посилання на створений об’єкт. Це посилання може бути присвоєне змінній типу посилання на даний тип:
Dog k9 = new Dog () ;
але може використовуватися і безпосередньо:
new Dog().voice();
Тут після створення безіменного об’єкта одразу виконується його метод voice().
2.8. СТАТИЧНІ ЧЛЕНИ КЛАСУ
Різні екземпляри одного класу мають повністю незалежні один від одного поля, що приймають різні значення. Зміна поля в одному екземплярі жодним чином не впливає на те ж поле у другому екземплярі. У кожному екземплярі для таких полів виділяється своя комірка пам’яті. Тому такі поля називаються змінними екземпляра класу (instance variables) або змінними об’єкта.
Інколи треба визначити поле, спільне для всього класу, зміна якого в одному екземплярі потягне зміну того ж поля у всіх екземплярах. Наприклад, ми хочемо у класі Automobile відзначити порядковий заводський номер автомобіля. Такі поля називаються змінними класу (class variables). Для змінних класу виділяється тільки одна комірка пам’яті, спільна для всіх екземплярів. Змінні класу створюються у Java модифікатором static. У лістинзі 2.3 ми записуємо цей модифікатор при визначенні змінної number.
Лістинг 2.3. Статична змінна
class Automobile {
private static int number;
Automobile(){
number++;
System.out.println("From Automobile constructor:"+ " number = "+number);
}
}
public class AutomobileTest{
public static void main(String[] args){
Automobile lada2105 = new Automobile(),
fordScorpio = new Automobile(),
oka = new Automobile();
}
}
До статичних змінних можна звертатися з іменем класа, Automobile.number, а не тільки з іменем eкземпляра, lada2105.number, причому це можна робити, навіть якщо не створено жодного екземпляра класу.
Для роботи з такими статичними змінними звичайно створюються статичні методи, помічені модифікатором static. Для методів слово static має зовсім інший зміст. Виконуюча система Java завжди створює в пам’яті тільки одну копію машинного коду метода, що розділяється всіма екземплярами, незалежно від того, статичний цей метод чи ні.
Головна особливість статичних методів — вони виконуються зразу у всіх екземплярах класу. Більше того, вони можуть виконуватися, навіть якщо не створений жодний екземпляр класу. Досить уточнити назву методу назвою класу (а не назвою об’єкту), щоб метод міг працювати. Так ми користувалися методами класу Math, не створюючи його екземпляри, а просто записуючи Math.abs(x), Math.sqrt(x). Так само використовується метод System.out.println(). Методом main() користуються, взагалі не створюючи жодних об’єктів.
Статичні методи називаються методами класу (class methods), на відміну від нестатичних методів, які називаються методами екземпляру (instance methods).
Звідси витікають інші особливості статичних методів:
у статичному методі не можна використовувати посилання this і super;
у статичному методі не можна напряму, не створюючи екземплярів, посилатися на нестатичні поля і методи;
статичні методи не можуть бути абстрактними;
статичні методи перевизначаються у підкласах тільки як статичні.
Статичні змінні ініціалізуються ще до початку роботи конструктора, але при ініціалізації можна використовувати тільки константні вирази. Якщо ж ініціалізація вимагає складних обчислень, наприклад, циклів для задання значень елементам статичних масивів або звернення до методів, то ці обчислення заключають у блок, помічений словом static, який теж буде виконаний до запуску конструктора:
static int[] a = new a[10];
static {
for(int k = 0; k