— Я расскажу тебе про «модификаторы доступа». Когда-то я уже рассказывал про них, но повторение – мать учения.
Ты можешь управлять доступом (видимостью) методов и переменных твоего класса из других классов. Модификатор доступа отвечает на вопрос «Кто может обращаться к данному методу/переменной?». Каждому методу или переменной можно указывать только один модификатор.
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 | Описание |
---|---|
|
|
|
Мы расширили видимость метода с protected до public . |
Использование | Почему это «законно» |
---|---|
|
Все отлично. Тут мы даже не знаем, что в классе-наследнике видимость метода была расширена. |
|
Тут вызывается метод, у которого расширили область видимости.
Если бы этого сделать было нельзя, всегда можно было бы объявить метод в Tiger: Т.е. ни о каком нарушении безопасности и речи нет. |
|
Если все условия подходят для вызова метода базового типа (Cat), то они уж точно подойдут для вызова типа наследника (Tiger) . Т.к. ограничения на вызов метода были ослаблены, а не усилены. |
— Не уверен, что понял полностью, но то, что так можно делать, запомню.
3) Сужение типа результата.
В переопределенном методе мы можем поменять тип результата, сузив его.
Код на Java | Описание |
---|---|
|
|
|
Мы переопределили метод getMyParent , теперь он возвращает объект типа Tiger . |
Использование | Почему это «законно» |
---|---|
|
Все отлично. Тут мы даже не знаем, что в классе наследнике тип результата метода getMyParent был сужен.
«Старый код» как работал так и работает. |
|
Тут вызывается метод, у которого сузили тип результата.
Если бы этого сделать было нельзя, всегда можно было бы объявить метод в Tiger: Т.е. ни о каком нарушении безопасности и/или контроля приведения типов нет речи. |
|
И тут все отлично работает, хотя мы расширили тип переменных до базового класса (Cat).
Нет ничего страшного при вызове метода getMyParent, т.к. его результат, хоть и класса Tiger, все равно сможет отлично присвоиться в переменную myParent базового класса (Cat). Объекты Tiger можно смело хранить как в переменных класса Tiger, так и в переменных класса Cat. |
— Ага. Я понял. Надо при переопределении методов беспокоиться о том, как все это будет работать, если мы передадим наши объекты в код, который умеет обращаться только с базовым классом, и ничего о нашем классе не знает.
— Именно! Тогда вопрос на засыпку, почему нельзя расширить тип результата при переопределении метода?
— Это же очевидно, тогда перестанет работать код в базовом классе:
Код на Java | Пояснение проблемы |
---|---|
|
|
|
Мы переопределили метод getMyParent и расширили тип его результата.
Тут все отлично. |
|
Тогда у нас перестанет работать этот код.
Метод getMyParent может вернуть любой объект типа Object, т.к. на самом деле он вызывается у объекта типа Tiger. А у нас нет проверки перед присваиванием. Тогда вполне возможно, что переменная myParent типа Cat будет хранить ссылку на строку. |
— Отличный пример, Амиго!
В Java перед вызовом метода не проверяется, есть ли такой метод у объекта или нет. Все проверки происходят во время выполнения. И [гипотетический] вызов отсутствующего метода, скорее всего, приведет к тому, что программа начнет выполнять байт-код там, где его нет. Это, в конце концов, приведет к фатальной ошибке, и операционная система принудительно закроет программу.
— Ничего себе. Буду знать.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ