— Я розповім тобі про «модифікатори доступу». Колись я вже розповідав про них, але повторення – мати навчання.

Ти можеш керувати доступом (видимістю) методів та змінних твого класу з інших класів. Модифікатор доступу відповідає на питання «Хто може звертатися до даного методу/змінної?». Кожному методу або змінній можна вказувати лише один модифікатор.

1) Модифікатор «public».

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

2) Модифікатор «private».

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

3) «Модифікатор «за замовчуванням».

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

4) Модифікатор «protected».

Цей рівень доступу трохи ширший, ніж package. До змінної, методу чи класу, позначеного модифікатором protected, можна звертатися з його ж пакету (як package), але й також з усіх класів, що успадковані від поточного.

Таблиця з поясненням:

Тип видимості Ключове слово Доступ
Свій клас Свій пакет Клас — спадкоємець Усі класи
Закритий private Є Немає Немає Немає
Пакет (немає модифікатора) Є Є Немає Немає
Захищений protected Є Є Є Немає
Відкритий public Є Є Є Є

Є спосіб легко запам'ятати цю таблицю. Уяви собі, що ти складаєш заповіт і ділиш усі речі на чотири категорії. Хто може скористатися твоїми речами?

Хто має доступ Модифікатор Приклад
Тільки я сам private Особистий щоденник
Сім'я (немає модифікатора) Сімейні фотографії
Сім'я та спадкоємці protected Родовий маєток
Усі public Мемуари

— Якщо уявити, що класи, що лежать в одному пакеті, – це одна сім'я, то дуже навіть схоже.

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

1) Неявна реалізація абстрактного методу.

Припустимо, що в тебе є код:

Код
class Cat
{
 public String getName()
 {
  return "Васько";
 }
}

І ти вирішив успадкувати від нього клас тигр і додати новому класу інтерфейс

Код
class Cat
{
 public String getName()
 {
   return "Васько";
 }
}
interface HasName
{
 String getName();
 int getWeight();
}
class Tiger extends Cat implements HasName
{
 public int getWeight()
 {
  return 115;
 }

}

Якщо ти просто реалізуєш всі методи, які тобі підкаже Intellij IDEA, то можеш потім довго шукати помилку.

Виявляється, що в класі Tiger є успадкований від Cat метод getName, який і вважатиметься реалізацією методу getName для інтерфейсу HasName.

— Не бачу в цьому нічого страшного.

— Це не дуже погано, це радше потенційне місце для помилок.

Але може бути ще гірше:

Код
interface HasWeight
{
 int getValue();
}
interface HasSize
{
 int getValue();
}
class Tiger extends Cat implements HasWeight, HasSize
{
 public int getValue()
 {
  return 115;
 }
}

Виявляється, ти не завжди можеш успадковуватись від кількох інтерфейсів. Точніше успадкуватися можеш, а ось коректно їх реалізувати – ні. Поглянь на приклад, обидва інтерфейси вимагають, щоб ти реалізував метод getValue(), і не зрозуміло, що він повинен повертати: вагу (weight) чи розмір (size). Це досить неприємна річ, якщо тобі доведеться з нею зіткнутися.

— Так, згоден. Хочеш реалізувати метод, а не можеш. Раптом ти вже успадкував метод з такою самою назвою від базового класу. Обламайся.

— Але є й приємні новини.

2) Розширення видимості. При перевизначенні типу дозволяється розширити видимість способу. Ось як це виглядає:

Код на Java Опис
class Cat
{
 protected String getName()
 {
  return "Васько";
 }
}
class Tiger extends Cat
{
 public String getName()
 {
  return "Василь Тигранович";
 }
}
Ми розширили видимість методу зprotected до public.
Використання Чому це «законно»
public static void main(String[] args)
{
 Cat cat = new Cat();
 cat.getName();
}
Все відмінно. Тут ми навіть не знаємо, що в класі-спадкоємці видимість методу була розширена.
public static void main(String[] args)
{
 Tiger tiger = new Tiger();
 tiger.getName();
}
Тут викликається метод, у якого розширили область видимості.

Якби цього зробити було не можна, завжди можна було б оголосити метод Tiger:
public String getPublicName()
{
super.getName(); // виклик protected методу
}

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

public static void main(String[] args)
{
 Cat catTiger = new Tiger();
 catTiger.getName();
}
Якщо всі умови підходять для виклику методу базового типу (Cat), то вони точно підійдуть для виклику типу спадкоємця (Tiger). Так як обмеження на виклик методу були послаблені, а не посилені.

— Не впевнений, що зрозумів повністю, але те, що так можна робити, запам'ятаю.

3) Звуження типу результату.

У перевизначеному методі ми можемо змінити тип результату, звузивши його.

Код на Java Опис
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Tiger getMyParent()
 {
  return (Tiger) this.parent;
 }
}
Ми перевизначили метод getMyParent, тепер він повертає об'єкт типу Tiger.
Використання Чому це «законно»
public static void main(String[] args)
{
 Cat parent = new Cat();

 Cat me = new Cat();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
Все відмінно. Тут ми навіть не знаємо, що в класі-спадкоємці тип результату методу getMyParent був звужений.

«Старий код» як працював, так і працює.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Tiger me = new Tiger();
 me.setMyParent(parent);
 Tiger myParent = me.getMyParent();
}
Тут викликається метод, у якого звузили тип результату.

Якби цього зробити було не можна, завжди можна було б оголосити метод Tiger:
public Tiger getMyTigerParent()
{
return (Tiger) this.parent;
}

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

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
І тут все добре працює, хоча ми розширили тип змінних до базового класу (Cat).

Немає нічого страшного при виклику методу getMyParent, т.я. його результат, хоч і класу Tiger, все рівно зможе бути чудово присвоєним в змінну myParent базового класу (Cat).

Об'єкти Tiger можна сміливо зберігати як у змінних класу Tiger, так і в змінних класу Cat.

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

— Отож-бо! Тоді питання на засипку, чому не можна розширити тип результату при перевизначенні методу?

— Це ж очевидно, тоді перестане працювати код у базовому класі:

Код на Java Пояснення проблеми
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Object getMyParent()
 {
  if (this.parent != null)
   return this.parent;
  else
   return "я - сирота";
 }
}
Ми перевизначили метод getMyParent та розширили тип його результату.

Тут все відмінно.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 Cat myParent = me.getMyParent();
}
Тоді в нас перестане працювати цей код.

Метод getMyParent може повернути будь-який об'єкт типу Object, т.я. насправді він викликається в об'єкта типу Tiger.

А в нас немає перевірки перед присвоєнням. Тоді цілком можливо, що змінна myParent типу Cat буде зберігати посилання на рядок.

— Чудовий приклад, Аміго!

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

— Нічого собі. Буду знати.