|
|
ОБ’ЄКТНО ОРІЄНТОВАНЕ ПРОГРАМУВАННЯ Електронний посібник |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Модуль 3 |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3. Інкапсуляція. успадкування та повторне
використання коду 3.1. Інкапсуляція та конструктори 3.2. Створення та використання масивів
Одною із переваг
ООП є те, що користувачі класів можуть використовувати їх, майже не знаючи,
як вони реалізовані, використовуючи лише кілька методів для роботи з
конкретним об’єктом. Таке приховування
реалізації класу слугує
також і певним захистом від неправильної роботи з даними. Такий принцип
реалізації називається "інкапсуляцією". Якщо візьмемо реальний світ, то, наприклад, ви не
знаєте як влаштований телевізор, проте ви можете його увімкнути та за
допомогою кнопок переключати канали. Сама ж схемотехнічна
реалізація прикрита корпусом телевізора і переважно невідома глядачам ТВ.
Аналогічно і класи розробляються за схожим принципом. Створюються певні
методи (інтерфейс класу), через які відбувається доступ до полів класу
(змінних) та його методів. Всі інші класи, методи, поля, які слугують лише
для обслуговування внутрішнього функціонування класу, намагаються захистити,
щоб до них не було доступу без крайньої на те потреби. Це в свою чергу
зменшує кількість помилок в програмі через невмілі дії користувача цього
класу.
Йдеться про те, що
об’єкт вміщує не тільки дані, але і правила їх обробки, оформлені у вигляді
виконуваних фрагментів (методів). Доступ до стану об'єкта напряму заборонено
і ззовні з ним можна взаємодіяти виключно через заданий інтерфейс, що
дозволяє контролювати правильність звернення до полів та їхню ініціалізацію.
Також інкапсулюються методи, які виконують
допоміжну роль і не бажано, щоб користувач-програміст мав доступ до них.
Оскільки користувачі-програмісти працюють лише через відкриті елементи класів,
то розробники класу можуть як завгодно змінювати всі закриті елементи і
навіть перейменовувати та видаляти їх, не турбуючись, що десь хтось їх
використовує у своїх програмах. Наприклад, вам
потрібно зробити певну програму по роботі зі списком автомобілів.
У вищенаведеному
прикладі для того, щоб рахувати кількість об'єктів, ми створюємо статичну
змінну count, яка буде спільною для всіх об'єктів car.
В методі main ми створюємо екземпляри класу Car
двома способами. Спочатку вручну ініціалізуємо
кожне поле car1, а поля car2 ініціалізуємо
через конструктор. Крім того, що другий спосіб є простішим, перший спосіб може
слугувати ще й джерелом низки помилок. Наприклад, ми можемо забути ініціалізувати певне поле. Крім того, ініціалізуючи
через конструктор, ми можемо здійснити попередню перевірку на правильність
введення даних. Наприклад, на правильність введення назви виробника тощо.
Також маємо два службових поля: count та id, які ініціалізуються в
конструкторах, проте ми можемо задати значення і напряму, що може зашкодити логіці функціонування об'єкту класу. Ми запросто можемо
вказати, що є 100 автомобілів, хоча насправді їх 66. І якщо будемо десь
використовувати цикл з перебору автомобілів з врахування змінної count,
це викличе помилку. Тому в ООП і придумано
інкапсуляцію – приховування внутрішньої реалізації класу. Рекомендується
оголошувати поля та методи з самого початку закритими і лише за необхідності
надавати до них більший доступ.
Як бачимо, усі поля
у нас тепер приватні. Робота з закритими полями можлива лише через відповідні
методи. Для доступу до полів count та
id створено методи getCount та getId. За необхідності можна створити методи для доступу до
інших полів. Також можна створити певні методи модифікації окремих полів (setId
та ін.) та передбачити в них попередню перевірку значень, що вводяться. Тож
ми усунули можливість появи помилок у програмуванні, пов'язаних з
неправильним використанням полів та методів класу. Іншим програмістам буде
значно легше використовувати клас Car, їм не потрібно вникати в особливості реалізації
цього класу. Необхідно
зазначити, що методи, які читають значення полів, прийнято називати з
використанням префіксу get з наступним
вказанням назви змінної, а для тих, що модифікують значення, використовують
префікс set. В англомовній термінології
можна зустріти терміни getter та setter методи або метод accesor
та метод mutator. Зверніть увагу, як
здійснюється звернення до змінної count. Насправді до методу можна звернутися з використанням
об'єкта car[i].getCount(), але оскільки цей метод та
змінна є статичними, тобто вона та метод спільно використовуються усіма
об'єктами, то логічнішим є зверненням до неї через назву клас Car.getCount(). Цей метод можна викликати до створення будь-яких
об'єктів. В такому разі ми просто отримаємо 0. Якщо б змінна не була
приватною, то доступ до неї можна було б робити без застосування методу,
безпосередньо: Car.count. Згадаємо тепер
приклад з сейфами з попередньої лекції. Замість методу safeValue(), який в нас заповнює змінні об’єкта значеннями, ми
можемо створити метод, який буде мати назву таку ж, як і клас, тобто Safe(pWidth, pHeight, pDepth). Це дасть нам
можливість ще скоротити програму, оскільки при створенні об’єкта ми зможемо
зразу ж задавати розміри сейфу.
Такі методи носять
назву конструктор класу. Коли ми писали
просто new Safe(), то віртуальна машина використовувала конструктор за
замовчуванням без параметрів, який практично нічого корисного для нас не
робив. Тепер же ми можемо використовувати новий створений нами конструктор.
У наведеному
прикладі, щоб обчислити об’єм нового сейфу, нам потрібно додати лише два
рядки тексту (дві інструкції). Щоправда навіть це можна автоматизувати за
допомогою використання іншого типу даних – масивів, які будуть розглядатися пізніше.
Можна також додати клас Coin, в якому був би метод для обчислення сукупного об’єму
різноманітних монет. Ви можете спробувати зробити таку програму зараз. Це
буде корисним для засвоєння викладеного матеріалу. Тема класів та
методів значно комплексніша, тому основне, що ви маєте винести з цього
заняття, – це розуміння того, як використовуються методи (зокрема
конструктори) класів. Java надає великий набір уже готових класів та методів,
які значно спрощують роботу програміста.
Це доволі зручний
засіб групування інформації. Масиви можна створювати з елементів будь-якого
типу. До конкретного елемента в масиві звертаються за індексом (номером).
Вони можуть бути як одновимірні, так і багатовимірні.
Наприклад:
Існує також інша
форма оголошення масиву:
Проте для того, щоб
масив почав існувати, необхідно виділити під нього пам'ять за допомогою
операції new.
де розмір –
планована кількість елементів у масиві.
або зразу ж:
Таким чином
відбувається виділення пам'яті під масив і ініціалізація елементів масиву
нулями. В подальшому можна напряму звертатися до елементів масиву, вказуючи
індекс у квадратних дужках. Нумерація елементів у масиві в Java відбувається
з нуля. Тобто в наведеному прикладі звернення до першого (нульового) елемента
– month_days[0], а до останнього – month_days[11]. Java не дозволить програмі звернутися поза межі масиву,
щоправда помилка буде вказана лише на етапі виконання програми через
викидання винятку (виключення, exception).
Масиви також можна ініціалізувати зразу ж при їхньому оголошенні, не
використовуючи операції new, аналогічно як
це відбувається під час роботи з простими типами даних. Наступний приклад
зразу ж при оголошенні ініціалізує масив month_days[] кількістю
днів у місяцях.
Результат виконання
на екрані: Травень має 31 день Наступний приклад
демонструє знаходження максимального числа в одновимірному масиві.
Як бачимо, спочатку
змінній max присвоюється значення нульового
елемента масиву, після чого в циклі іде послідовне порівняння з кожним
наступним числом до останнього. Якщо при порівнянні чергове значення в масиві
більше за максимальне в змінній max, то
змінній max присвоюється це значення. Як ви
уже зрозуміли, після закінчення циклу у змінній max
міститиметься максимальне значення, яке і буде виведене на консоль: Максимальне число в масиві: 3.2 В Java масиви є об’єктами (про об’єкти ми вже
говорили раніше), що забезпечує деяку додаткову функціональність масивам.
Зокрема можна дізнатися довжину масиву наступним чином array.length.
Для вищенаведеного прикладу можна замінити рядок з циклом таким чином:
Робота з
багатовимірними масивами подібна до роботи з одновимірними. Відмінність лише в
тому, що використовуються додаткові квадратні дужки. Переважно
використовуються двовимірні масиви, які використовують для роботи з
табличними даними, та тривимірні масиви. Двовимірний та тривимірний масиви
можна оголосити наступним чином:
Для двовимірного
лівий індекс означає номер рядка, а правий – номер стовпця. Це можна уявити
наступним чином:
Тривимірний масив
можна уявити у вигляді куба. Крім номера рядка і номера стовпця, додається ще
індекс елемента вглибину.
У наведеному
прикладі в кожному рядку однакова кількість елементів (стовпців). У Java можна створити двовимірні масиви з різною кількістю елементів
у рядках (зазубрені або зубчасті масиви, jagged arrays). Використання таких нерівних
(нерегулярних) масивів не рекомендується, оскільки з ними важче працювати
і можна припуститися низки помилок, але в деяких ситуаціях вони можуть бути
доволі корисними.
Клас, на базі якого
створюється підклас, називають надкласом, суперкласом або ж
батьківським класом. Новий клас, що розширює (extends)
батьківський клас називають підкласом або дочірнім класом. На відміну від
С++, де клас може мати кілька батьківських класів, мова програмування Java підтримує лише одинарне
успадкування, тобто може бути
лише один безпосередній надклас. У надкласу звичайно може бути свій надклас,
проте також лише один безпосередній. Множинне успадкування доволі складне у
застосуванні і вимагає обережного підходу, тому творці Java
вирішили відмовитися від нього.
Тепер нехай вам
необхідний клас, який би містив ще й інформацію про висоту кімнати й
обчислював ще й об'єм кімнати. Можна створити повністю новий клас, можна
модифікувати вже існуючий клас, а можна створити клас, розширивши базовий клас SimpleRoom. Якщо клас SimpleRoom вже використовується в інших програмах, то змінювати
його потрібно обережно. Для цього прикладу доведеться створити ще один
конструктор, а не модифікувати існуючий. У складніших випадках може знадобитися
набагато більше дій. Крім того, може бути, що клас SimpleRoom вже
стандартизований, задокументований, його використовує чимало інших
розробників і змінювати його просто так ви не маєте права. Тож виходом є
створення нового класу під ваші потреби. Завдяки ж можливості успадкування,
нам не потрібно повністю переписувати клас. На основі класу SimpleRoom можна створити новий клас SimpleRoom2. Для цього достатньо вказати ключове слово extends
(що означає "розширює") і вказати назву батьківського класу.
Розберемо
вищенаведений приклад. При створенні класу ми зазначили, який клас ми
розширюємо. В класі введене нове поле height. Далі ми створили конструктор, в якому іде звернення
до батьківського конструктора. Якщо б не було конструкторів з параметрами, то
неявні виклики конструкторів викликалися б у такій же послідовності. Спочатку
викликається конструктор дочірнього класу, з нього викликається батьківський
конструктор, створюється батьківський об'єкт, далі іде завершення
конструктора дочірнього класу і створюється дочірній об'єкт. Для виклику
конструктора суперкласу ми скористалися методом super з відповідними
аргументами для батьківського конструктора:
Також зверніть
увагу як відбувається звернення до полів батьківського класу. Якщо поля не
приватні, то вони доступні з дочірнього класу. Тож до них можна звертатися
безпосередньо за іменем, або ж скористатися для доступу ключовим словом super (замість об'єктної змінної).
Якщо б у нашому дочірньому класі існували б однойменні поля з батьківським
класом (наприклад, і там, і там width), то поля
батьківського класу були б доступні лише з допомогою super. Результат виконання SimpleRoom2:
Зверніть увагу, що
ми без проблем через об'єктну змінну класу SimpleRoom2 викликаємо метод info
класу SimpleRoom:
Тож, крім полів,
дочірньому класу також доступні неприватні методи батьківського класу.
Інколи може
виникнути необхідність заборонити можливість здійснення успадкування.
Тобто можливість створення нових класів на базі певного класу. Тоді клас
можна оголосити як final. Також можна заборонити заміщення методу у
класах-нащадках, використавши при оголошенні методу той же модифікатор final. Як уже мабуть ви знаєте, цей же модифікатор final
також застосовується для оголошення констант – змінних, які ініціалізуються лише раз. Наприклад, у Java
фінальним оголошено клас String.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||