→ Пошук по сайту       Увійти / Зареєструватися
Знання Програмування на Java Навчальний курс по JAVA

Об'єктна модель в JAVA

 Java

Статичні елементи

До цього моменту під полями об'єкту ми завжди розуміли значення, які мають сенс тільки в контексті деякого екземпляра класу. Наприклад:

 class Human {
   private String name;
} 

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

Але бувають дані іншого характеру. Припустимо, необхідно зберігати кількість всіх людей (примірників класу Human, існуючих в системі). Зрозуміло, що загальна кількість людей не є характеристикою якоїсь однієї людини, вона відноситься до всього типу в цілому. Звідси з'являється назва "поле класу", на відміну від "поля об'єкта". Оголошуються такі поля за допомогою модифікатора static:

 class Human {
   public static int totalCount;
} 

Щоб звернутися до такого поля, посилання на об'єкт не потрібно, цілком достатньо імені класу:

Human.totalCount + +;

  // Народження ще однієї людини 

Для зручності дозволено звертатися до статичних полів і через посилання:

 Human h = new Human ();
h.totalCount = 100; 

Однак таке звернення конвертується компілятором. Воно використовує тип посилання, в даному випадку змінна h оголошена як Human, тому останній рядок буде неявно перетворена в:

 Human.totalCount = 100; 

У цьому можна переконатися на наступному прикладі:

 Human h = null;
h.totalCount + = 10; 

Значення посилання null, але це не має значення в силу описаної конвертації. Даний код успішно скомпіліруется і коректно виповниться. Таким чином, в наступному прикладі

 Human h1 = new Human (), h2 = new Human ();
Human.totalCount = 5;
h1.totalCount + +;
System.out.println (h2.totalCount); 

всі звернення до змінної totalCount призводять до одного єдиного поля, і результатом роботи такої програми буде 6. Це поле буде існувати в єдиному екземплярі незалежно від того, скільки об'єктів було породжене від даного класу, і чи було взагалі створено хоч один об'єкт.

Аналогічно оголошуються статичні методи.

 class Human {
   private static int totalCount;
   public static int getTotalCount () {
      return totalCount;
   }
} 

Для виклику статичного методу посилання на об'єкт не потрібно.

 Human.getTotalCount (); 

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

 Human h = null;
h.getTotalCount (); // два еквівалентних
Human.getTotalCount (); // звернення до одного і того ж методу 

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

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

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

 class Human {
   static {
      System.out.println ("Class loaded");
   }
} 

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

 class Test {
   static int a;
   static {
      a = 5;
      // B = 7; // Не можна використовувати до оголошення!
   }
   static int b = a;
} 

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

 class Test {
   static int b = Test.a;
   static int a = 3;
   static {
      System.out.println ("a =" + a + ", b =" + b);
   }
} 

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

 a = 3, b = 0 

Видно, що поле b при ініціалізації одержало значення за замовчуванням поля a, тобто 0. Потім полю a було присвоєно значення 3.

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

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

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

Навпаки, зі статичним контекстом асоційованих об'єктів немає. Наприклад, як вже вказувалося, стартовий метод main () викликається в той момент, коли жоден об'єкт ще не створений. При зверненні до статичного методу, наприклад, MyClass.staticMethod (), також може не бути жодного примірника MyClass. Звертатися до статичних методів класу Math можна, а створювати його екземпляри ні.

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

 class Test {
   public void process () {
   }
   public static void main (String s []) {
      // Process (); - помилка!
      // У якого об'єкта його викликати?
      Test test = new Test ();
      test.process (); // так правильно
   }
}

Ключові слова this і super

Ці ключові слова вже згадувалися, розглядалися і деякі випадки їхнього застосування. Тут вони будуть описані більш детально.

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

 class Test {
   public Object getThis () {
      return this;
      // Перевіримо, куди вказує це посилання
   }
   public static void main (String s []) {
      Test t = new Test ();
      System.out.println (t.getThis () == t);
// Порівняння
   }
} 

Результатом роботи програми буде:

 true 

Тобто всередині методів слово this повертає посилання на об'єкт, у якого цей метод викликані. Воно необхідне, якщо потрібно передати аргумент, рівний посиланням на даний об'єкт, у який-небудь метод.

 class Human {
   public static void register (Human h) {
      System.out.println (h.name +
        "Is registered.");
   }
   private String name;
   public Human (String s) {
      name = s;
      register (this); // самореєстрація
   }
   public static void main (String s []) {
      new Human ("John");
   }
} 

Результатом буде:

 John is registered. 

Інше застосування this розглядалося у разі "затінюючих" оголошень:

 class Human {
   private String name;
   public void setName (String name) {
      this.name = name;
   }
} 

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

 class Test {
   // Int b = a; не можна звертатися до
   // Неоголошеного поля!
   int b = this.a;
   int a = 5;
   {
      System.out.println ("a =" + a + ", b =" + b);
   }
   public static void main (String s []) {
      new Test ();
   }
} 

Результатом роботи програми буде:

 a = 5, b = 0

Все відбувається так само, як і для статичних полів - b отримує значення за замовчуванням для a, тобто нуль, а потім a ініціалізується значенням 5.

Нарешті, слово this застосовується в конструкторах для явного виклику в першому рядку іншого конструктора цього ж класу. Там же може застосовуватися і слово super, але вже для звернення до конструктора батьківського класу.

Інші застосування слова super також пов'язані зі зверненням до батьківського типу об'єкта. Наприклад, воно може знадобитися у разі перевизначення (overriding) батьківського методу.

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

 class Parent {
   public int getValue () {
      return 5;
   }
}
class Child extends Parent {
   // Перевизначення методу
   public int getValue () {
     return 3;
   }
   public static void main (String s []) {
     Child c = new Child ();
     // Приклад виклику перевизначеного методу
     System.out.println (c.getValue ());
   }
} 

Виклик перевизначеного методу використовує механізм поліморфізму, який детально розглядається в кінці цієї лекції. Однак ясно, що результатом виконання прикладу буде значення 3. Неможливо, використовуючи посилання типу Child, отримати з методу getValue () значення 5, батьківський метод перекритий і вже недоступний.

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

 class Parent {
   public int getValue () {
      return 5;
   }
}
class Child extends Parent {
   // Перевизначення методу
   public int getValue () {
      // Звернення до методу батька
      return super.getValue () +1;
   }
   public static void main (String s []) {
      Child c = new Child ();
      System.out.println (c.getValue ());
   }
} 

Результатом роботи програми буде значення 6.

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

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

Ключове слово abstract

Наступне важливе поняття, яке необхідно розглянути, - ключове слово abstract.

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

Розглянемо приклад. Припустимо, необхідно створити набір графічних елементів, неважливо, яких саме. Наприклад, вони можуть представляти собою геометричні фігури - коло, квадрат, зірка і т.д.; або елементи призначеного для користувача інтерфейсу - кнопки, поля введення і т.д. Зараз це не має вирішального значення. Крім того, існує спеціальний контейнер, який займається їх промальовкою. Зрозуміло, що зовнішній вигляд кожної компоненти унікальний, а значить, відповідний метод (назвемо його paint ()) буде реалізований в різних елементах по-різному.

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

Але як вчинити з методом відтворення? Адже батьківський клас не представляє собою будь-яку фігуру, у нього немає візуального представлення. Можна оголосити метод paint () в кожній компоненті незалежно. Але тоді контейнер повинен буде володіти складною функціональністю, щоб аналізувати, яка саме компонента зараз обробляється, виконувати приведення типу і лише після цього викликати потрібний метод.

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

Наведемо спрощений приклад:

 // Базова арифметична операція
abstract class Operation {
  public abstract int calculate (int a, int b);
}
// Додавання
class Addition extends Operation {
  public int calculate (int a, int b) {
    return a + b;
   }
}
// Віднімання
class Subtraction extends Operation {
   public int calculate (int a, int b) {
      return a-b;
   }
}
class Test {
   public static void main (String s []) {
      Operation o1 = new Addition ();
      Operation o2 = new Subtraction ();
      o1.calculate (2, 3);
      o2.calculate (3, 5);
   }
} 

Видно, що виконання операцій додавання і віднімання в методі main () записуються однаково.

Зверніть увагу - оскільки абстрактний метод не має тіла, після опису його заголовка ставиться крапка з комою. А раз у нього немає тіла, то до нього не можна звертатися, поки його спадкоємці не опишуть реалізацію. Це означає, що не можна створювати екземпляри класу, у якого є абстрактні методи. Такий клас сам оголошується абстрактним.

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

Звичайно, клас не може бути одночасно abstract і final. Це ж вірно і для методів. Крім того, абстрактний метод не може бути private, native, static.

Сам клас може без обмежень користуватися своїми абстрактними методами.

 abstract class Test {
   public abstract int getX ();
   public abstract int getY ();
   public double getLength () {
      return Math.sqrt (getX () * getX () +
                       getY () * getY ());
   }
} 

Це коректно, оскільки метод getLength () може бути викликаний тільки у об'єкту. Об'єкт може бути породжений тільки від неабстрактного класу, який є спадкоємцем від Test, і повинен був реалізувати всі абстрактні методи.

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

Інтерфейси

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

Всі ці проблеми не виникають у тому випадку, якщо успадковуються тільки абстрактні методи від декількох батьків. Навіть якщо успадковано кілька однакових методів, все одно у них немає реалізації і можна один раз описати тіло методу, яке буде використовуватися при виклику будь-якого з цих методів.

Саме так влаштовані інтерфейси в Java. Від них не можна породжувати об'єкти, але інші класи можуть реалізовувати їх.

Оголошення інтерфейсів

Оголошення інтерфейсів дуже схоже на спрощене оголошення класів.

Воно починається із заголовка. Спочатку вказуються модифікатори. Інтерфейс може бути оголошений як public і тоді він буде доступний для загального використання, або модифікатор доступу може не вказуватися, в цьому випадку інтерфейс доступний тільки для типів свого пакету. Модифікатор abstract для інтерфейсу не потрібно, оскільки всі інтерфейси є абстрактними. Його можна вказати, але робити цього не рекомендується, щоб не захаращувати код.

Далі записується ключове слово interface та ім'я інтерфейсу.

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

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

Потім у фігурних дужках записується тіло інтерфейсу.

 public interface Drawable extends Colorable,
   Resizable {
} 

Тіло інтерфейсу складається з оголошення елементів, тобто полів-констант і абстрактних методів. Всі поля інтерфейсу повинні бути public final static, так що ці модифікатори вказувати необов'язково і навіть небажано, щоб не захаращувати код. Оскільки поля оголошуються фінальними, необхідно їх відразу ініціалізувати.

 public interface Directions {
   int RIGHT = 1;
   int LEFT = 2;
   int UP = 3;
   int DOWN = 4;
} 

Всі методи інтерфейсу є public abstract і ці модифікатори також необов'язкові.

 public interface Moveable {
   void moveRight ();
   void moveLeft ();
   void moveUp ();
   void moveDown ();
} 

Як ми бачимо, опис інтерфейсу набагато простіше, ніж оголошення класу.

Реалізація інтерфейсу

Кожен клас може реалізовувати будь-які доступні інтерфейси. При цьому в класі повинні бути реалізовані всі абстрактні методи, що з'явилися при спадкуванні від інтерфейсів або батьківського класу, щоб новий клас міг бути оголошений неабстрактним.

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

 interface A {
  int getValue ();
}
interface B {
  double getValue ();
} 

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

Подібний конфлікт з полями-константами не настільки критичне:

 interface A {
  int value = 3;
}
interface B {
  double value = 5.4;
}
class C implements A, B {
  public static void main (String s []) {
    C c = new C ();
    // System.out.println (c.value); - помилка!
    System.out.println (((A) c). Value);
    System.out.println (((B) c). Value);
  }
} 

Як видно з прикладу, звертатися до такого поля через сам клас не можна, компілятор не зможе зрозуміти, яке з двох полів потрібно використовувати. Але можна з допомогою явного приведення послатися на одне з них.

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

Якщо інтерфейс A успадковується від інтерфейсу B, а клас реалізує A, то вважається, що інтерфейс B також реалізується цим класом з тієї ж причини - всі елементи передаються у спадщину в два етапи - спочатку інтерфейсу A, потім класу.

Нарешті, якщо клас C1 успадковується від класу C2, клас C2 реалізує інтерфейс A1, а інтерфейс A1 успадковується від інтерфейсу A2, то клас C1 також реалізує інтерфейс A2.

Все це дозволяє стверджувати, що змінні типу інтерфейс також допустимі. Вони можуть мати значення null, або посилатися на об'єкти, породжені від класів, що реалізують цей інтерфейс. Оскільки об'єкти породжуються тільки від класів, а всі вони успадковуються від Object, це означає, що значення типу інтерфейс володіють усіма елементами класу Object.

Застосування інтерфейсів

До цих пір інтерфейси розглядалися з технічної точки зору - як їх оголошувати, які конфлікти можуть виникати, як їх вирішувати. Однак важливо розуміти, як застосовуються інтерфейси з концептуальної точки зору.

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

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

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

Можна було б оголосити відповідний метод (скажімо, consumeInsect (Insect)) в кожного представника незалежно. Але якщо завдання полягає в моделюванні, наприклад, зоопарку, то однотипну процедуру - годування комахами - довелося б описувати для кожного виду окремо, що істотно ускладнило б код, причому без будь-якої користі.

Java пропонує інше рішення. Оголошується інтерфейс InsectConsumer:

public interface InsectConsumer {
   void consumeInsect (Insect i);
}  

Його реалізують всі підходящі тварини та рослини:

 // Росичка розширює клас рослина
public class Sundew extends
   Plant implements InsectConsumer {
      public void consumeInsect (Insect i) {
      ...
      }
}
// Ластівка розширює клас птах
public class Swallow extends
   Bird implements InsectConsumer {
      public void consumeInsect (Insect i) {
      ...
      }
}
// Мурахоїд розширює клас ссавець
public class AntEater extends
   Mammal implements InsectConsumer {
      public void consumeInsect (Insect i) {
      ...
      }
} 

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

 // Службовець, відповідальний за годування,
// Розширює клас службовець
class FeedWorker extends Worker {
   // За допомогою цього методу можна нагодувати
   // І росичку, і ластівку, і мурахоїда
   public void feedOnInsects (InsectConsumer
                             consumer) {
      ...
      consumer.consumeInsect (insect);
      ...
   }
} 

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

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

Поліморфізм

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

Поля

Почнемо з полів, які можуть бути статичними або динамічними. Розглянемо приклад:

 class Parent {
   int a = 2;
}
class Child extends Parent {
   int a = 3;
} 

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

 Child c = new Child ();
System.out.println (c.a);
Parent p = c;
System.out.println (p.a); 

Обидва посилання вказують на один і той самий об'єкт, породжений від класу Child, але одне з них має такий же тип, а інше - Parent. Звідси випливають і результати:

 3
2  

Оголошення поля в класі-спадкоємця "приховало" батьківське поле. Дане оголошення так і називається - "приховуюче" (hiding). Це особливий випадок перекриття областей видимості, відмінний від "затінення" (shadowing) і "затулення" (obscuring) оголошень. Тим не менш, батьківське поле продовжує існувати. До нього можна звернутися і явно:

 class Child extends Parent {
   int a = 3;
      // Приховує оголошення
   int b = ((Parent) this). a;
      // Більш громіздке оголошення
   int c = super.a;
      // Простіше
} 

Змінні b і c отримають значення, які зберігаються в батьківському полі a. Хоча вираз з super більш простий, він не дозволить звернутися на два рівня вгору по дереву наслідування. Адже цілком можливо, що в батьківському класі це поле також було приховане і в батьку батька зберігається ще одне значення. До нього можна звернутися явним приведенням, як це робиться для b.

Розглянемо наступний приклад:

class Parent {
   int x = 0;
   public void printX () {
      System.out.println (x);
   }
}
class Child extends Parent {
   int x =- 1;
}

 Який буде результат наступних рядків?

new Child (). printX ();  

Значення якого поля буде роздруковано? Метод викликається за допомогою посилання типу Child, але це не зіграє ніякої ролі. Викликається метод, визначений у класі Parent, і компілятор, звичайно, розцінив звернення до поля x в цьому методі саме як до поля класу Parent. Тому результатом буде 0.

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

Розглянемо приклад:

 class Parent {
   static int a = 2;
}
class Child extends Parent {
   static int a = 3;
} 

Який буде результат наступних рядків?

 Child c = new Child ();
System.out.println (c.a);
Parent p = c;
System.out.println (p.a); 

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

Тому розглянутий приклад еквівалентний:

 System.out.println (Child.a)
System.out.println (Parent.a) 

А його результат сумнівів уже не викликає:

 3
2 

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

Зверніть увагу на наступний приклад:

  class Parent {
   static int a;
} 
class Child extends Parent {
} 

Який буде результат наступних рядків?

 Child.a = 10;
Parent.a = 5;
System.out.println (Child.a); 

У цьому прикладі поле a не було приховано і передалося у спадок класу Child. Однак результат показує, що це все ж одне поле:

 5 

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

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

Методи

Розглянемо випадок перевизначення (overriding) методів:

 class Parent {
   public int getValue () {
      return 0;
   }
}
class Child extends Parent {
   public int getValue () {
      return 1;
   }
} 

І рядки, що демонструють роботу з цими методами:

 Child c = new Child ();
System.out.println (c.getValue ());
Parent p = c;
System.out.println (p.getValue ()); 

Результатом буде:

 1
1 

Можна бачити, що батьківський метод повністю перекритий, значення 0 ніяк не можна отримати через посилання, що вказує на об'єкт класу Child. У цьому є ключова особливість поліморфізму - спадкоємці можуть змінювати батьківське поводження, навіть якщо звернення до них проводиться за посиланням батьківського типу. Нагадаємо, що, хоч старий метод зовні уже недоступний, всередині класу-спадкоємця до нього все ж можна звернутися за допомогою super.

Розглянемо більш складний приклад:

 class Parent {
   public int getValue () {
      return 0;
   }
   public void print () {
      System.out.println (getValue ());
   }
}
class Child extends Parent {
   public int getValue () {
      return 1;
   }
} 

Що з'явиться на консолі після виконання наступних рядків?

 Parent p = new Child ();
p.print (); 

За допомогою посилання типу Parent викликається метод print (), оголошений в класі Parent. З цього методу робиться звернення до getValue (), яке в класі Parent повертає 0. Але компілятор вже не може передбачити, до динамічного методу якого класу відбудеться звернення під час роботи програми. Це визначає віртуальна машина на основі об'єкта, на який вказує посилання. І якраз цей об'єкт породжений від Child, то існує лише один метод getValue ().

Результатом роботи прикладу буде:

 1

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

Згадаймо, що заголовок методу складається з модифікаторів, значень, що повертається, сигнатур і throws-виразів. Сигнатура (ім'я і набір аргументів) залишається незмінною, якщо говорити про перевизначення. Значення, що повертається також не може змінюватися, інакше це призведе до появи двох різних методів з однаковими сигнатурами.

Розглянемо модифікатори доступу.

 class Parent {
   protected int getValue () {
      return 0;
   }
}
class Child extends Parent {
   /*? */ Protected int getValue () {
      return 1;
   }
} 

Нехай батьківський метод був оголошений як protected. Зрозуміло, що метод спадкоємця можна залишити з таким же рівнем доступу, але чи можна його розширити (public), або звузити (доступ за замовчуванням)? Кілька рядків для перевірки:

 Parent p = new Child ();
p.getValue (); 

Звернення до методу здійснюється за допомогою посилання типу Parent. Саме компілятор виконує перевірку рівня доступу, і він буде орієнтуватися на батьківський клас. Але посилання-то вказує на об'єкт, породжений від Child, і за правилами поліморфізму виконуватися буде метод саме цього класу. А значить, доступ до перевизначення методів не може бути більш обмеженим, ніж до вихідного. Отже, методи з доступом за замовчуванням можна перевизначати з таким же доступом, або protected або public. Protected-методи перевизначаються такими ж, або public, а для public міняти модифікатор доступу і зовсім не можна.

Що стосується private-методів, то вони визначені тільки всередині класу, з зовні їх не видно, а тому спадкоємці можуть без обмежень оголошувати методи з такими ж сигнатурами і довільними повертаються значеннями, модифікаторами доступу і т.д.

Аналогічні обмеження накладаються і на throws-вираз, які будуть розглянуті в наступних лекціях.

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

Перейдемо до статичних методів. Розглянемо приклад:

 class Parent {
   static public int getValue () {
      return 0;
   }
}
class Child extends Parent {
   static public int getValue () {
      return 1;
   }
} 

І рядки, що демонструють роботу з цими методами:

 Child c = new Child ();
System.out.println (c.getValue ());
Parent p = c;
System.out.println (p.getValue ()); 

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

  System.out.println (Child.getValue ()); System.out.println (Parent.getValue ()); 
1
0  

Тобто статичні методи, ніби статичні поля, належать класу і поява спадкоємців на них не позначається.

Статичні методи не можуть перекривати звичайні, і навпаки.

Поліморфізм і об'єкти

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

По-перше, тепер можна точно сформулювати, що є елементами вказівного типу. Вказівний тип має такі елементами:

  • безпосередньо оголошені в його тілі;
  • оголошені в його батьківському класі і реалізуємих інтерфейсами, крім:
  • private-елементів;
  • "Прихованих" елементів (полів та статичних методів, прихованих однойменними елементами);
  • перевизначених (динамічних) методів.

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

Зведемо ці дані в таблицю.

Таблиця 8.1. Взаємозв'язок типу змінної і типів її можливих значень.

Тип змінної

Допустимі значення

Абстрактний клас

  • null;
  • неабстрактний наслідний.

Інтерфейс

  • null;
  • класи реалізовуючі інтерфейс:
    • реалізуючі напряму (заголовок містить implement);
    • наслідуємі від реалізуючих класів;
    • реалізуючі нащадків цього інтерфейсу;
    • змішаний випадок – наслідування від класу, реалізуючого нащадка інтерфейсу.

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

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

Висновок

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

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

Далі розглядалося використання ключових слів this і super. Вираз this надає посилання, що вказує на об'єкт, в контексті якого воно зустрічається. Ця конструкція допомагає уникати конфліктів імен, а також застосовується в конструкторах.

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

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

Наступне важливе поняття - особливий тип в Java, інтерфейс. Його ще називають повністю абстрактним класом, тому що всі його методи обов'язково абстрактні, а поля final static. Відповідно, на основі інтерфейсів неможливо створювати об'єкти.

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

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

загрузка...
Теми розділу
Сторінки, близькі за змістом