Надеюсь, последняя статья была не слишком сложной! Поздравляем, если вы справились с этим, поскольку концепции, о которых мы говорили, были одними из самых важных в программировании. На этот раз мы продолжим с того места, на котором остановились, и объясним, что означает «публичный». Однако перед этим мы должны поговорить о концепции наследования.

Наследование

Наследование — это способ избежать повторения кода и указать отношения родитель-потомок между классами. Чтобы лучше понять, давайте рассмотрим несколько примеров.

телевизоры

Вспомните телевизионный класс, который мы сделали несколько статей назад. Вернемся к нему:

public class TV {
  int year;
  String make;
  String model;
  String resolution;
  boolean HDR;
}

Конечно, хорошо иметь какой-то общий телевизионный объект… Но что, если мы хотим быть более конкретными? Помните, как мы подарили мне телевизор Sony с разрешением 8K, а Даниэлю — Toshiba с разрешением 1080P? Что, если в телевизорах Sony есть некоторые эксклюзивные функции, которых нет в телевизорах Toshiba, и наоборот? Конечно, мы можем сделать следующее:

public class SonyTV {
  int year;
  String make;
  String model;
  String resolution;
  boolean HDR;
  boolean someSonyExclusiveFeature;
}
public class ToshibaTV {
  int year;
  String make;
  String model;
  String resolution;
  boolean HDR;
  boolean someToshibaExclusiveFeature;
}

Обратите внимание, что каждый общедоступный класс должен быть отдельным файлом в Java.

Это правильно, и это работает, но много повторяющегося кода. Существует не менее 50 телевизионных брендов, и представьте, сколько повторений они приносят! Хуже того, если мы захотим добавить какой-нибудь базовый атрибут ко всем телевизорам, будет кошмаром проходить каждый отдельный класс. Давайте воспользуемся наследованием, чтобы упростить код:

public class TV {
  int year;
  String make;
  String model;
  String resolution;
  boolean HDR;
}
public class SonyTV extends TV {
  boolean someSonyExclusiveFeature;
}
public class ToshibaTV extends TV {
  boolean someToshibaExclusiveFeature;
}

Обратите внимание, что в Java каждый общедоступный класс должен находиться в своем собственном файле. Нам пришлось бы создать SonyTV.java и ToshibaTV.java, а затем поместить в эти файлы новые классы.

extends — это волшебное слово для наследования. SonyTV extends TV означает, что SonyTV является потомком или подклассом TV. SonyTV унаследует все публичные и защищенные переменные и методы класса TV, а значит, мы можем сделать так:

SonyTV stv = new SonyTV();
stv.HDR = true; // Variable declared in TV class
stv.someSonyExclusiveFeature = true; // Variable declared in SonyTV class

А если мы объявим одноименную переменную в дочернем классе:

public class TV {
  int year;
  String make;
  String model;
  String resolution;
  boolean HDR;
}
public class SonyTV extends TV {
  boolean someSonyExclusiveFeature;
  boolean HDR;
}

Мы можем различить их, используя ключевое слово super внутри дочернего класса:

System.out.println(HDR); // Refers to the newly declared HDR variable
System.out.println(super.HDR); // Refers to the HDR variable in the TV class

Мы также можем получить доступ к обеим переменным HDR извне путем приведения типов.

Кастинг

Приведение означает, что объект относится к другому типу. Существует два типа приведения: приведение вниз и приведение вверх. Приведение вверх, как правило, безвредно, но приведение вниз может быть опасным. Рассмотрим следующее утверждение:

Человек — это либо мужчина, либо женщина.

Допустим, это правда. Чтобы перевести это в код:

class Human {}
class Man extends Human {}
class Woman extends Human {}

Теперь рассмотрим следующее предложение:

Если ты мужчина, то ты человек.

Это кастинг. Мы предполагаем, что дочерний класс имеет родительский тип. Это считается безопасным и всегда верно. В коде, если у нас есть объект Man с именем m, мы можем привести его к объекту Human, выполнив (Human)m.

Однако рассмотрим следующее предложение:

Если ты человек, то ты мужчина.

Это, очевидно, более шатко, потому что, скорее всего, вы женщина. Это нисходящее приведение, когда мы предполагаем, что родительский класс является его дочерним классом. Это намного опаснее, потому что, если мы не уверены, что этот человек действительно мужчина, приведение к неправильному типу обычно заканчивается крахом.

Вернемся к примеру с телевизором с нашим объектом SonyTV. Рассмотрим следующий фрагмент кода:

SonyTV stv = new ToshibaTV();
((TV)stv).HDR = true; // This will set the HDR variable in the parent class to true by temporarily casting the child object into a parent object.
stv.HDR = false; // This will set the HDR variable in the child class to false.
System.out.println(stv.HDR);
System.out.println(((TV)stv).HDR);

Множественное наследование

Обратите внимание, что хотя это и возможно в некоторых других языках, Java не поддерживает множественное наследование, то есть каждый дочерний класс может иметь не более одного родителя. Однако мы можем связать наследование:

public class Appliance {}
public class TV extends Appliance {}
public class SonyTV extends TV {}

Но подождите... Мы постоянно упоминаем слово public, что это такое?

Видимость

Видимость относится… ну, к видимости переменной или метода. В Java есть три ключевых слова видимости: public, private, protected. Давайте используем пример, чтобы лучше понять их:

public class Parent {
  int ID;
  private int privateFunds;
  protected int sugarMoney;
 
  public void earnMoney(int password) {
    if (password != 1234) { return; } // Immediately exits the method without further execution
    privateFunds += 100;
    sugarMoney += 50;
  }
}
public class Child extends Parent {
  public void spendAllMoney() {
   // privateFunds = 0; -> Invalid
   sugarMoney = 0;
  }
}
public class Main {
  public static void main(String[] args) {
    Child child = new Child();
    System.out.println(child.ID); // Valid
    // System.out.println(child.privateFunds); -> Invalid
    // System.out.println(child.sugarMoney); -> Invalid
    child.earnMoney(1234); // Valid
    child.spendAllMoney(); // Valid
  }
}

В классе Father:
ID: общедоступный, то есть любой может получить к нему доступ. По умолчанию, если мы объявляем переменную без модификатора видимости, переменная становится общедоступной. Поэтому мы можем получить к нему доступ в основном методе.
privateFunds: Частный, что означает, что он доступен только внутри класса. Мы не можем получить к нему доступ где-либо еще, даже в его дочернем элементе.
sugarMoney: защищенный, что означает, что он доступен только себе и своим дочерним элементам. Следовательно, мы можем потратить сахарные деньги в Child, но не можем распечатать их напрямую в основном методе.
earnMoney(int password): Public. Обратите внимание, что пока переменная видима, мы можем ее изменить. Здесь, несмотря на то, что метод является общедоступным, он может изменять приватную переменную. Мы можем использовать эту технику для косвенного изменения закрытых переменных или для наложения ограничений, как показано здесь. Если пароль неверный, мы не сможем заработать деньги.

В классе Child:
spendAllMoney(): мы можем изменить только sugarMoney, потому что его видимость защищена, что делает его видимым для всех дочерних элементов Father. Однако доступ к privateFunds недоступен, так как он является частным.

Обратите внимание, что у классов также есть видимость. Без каких-либо модификаторов видимости класс является видимым для пакета, что означает, что только другие классы в том же пакете могут получить к нему доступ. Только public разрешает классам в других пакетах доступ к нашему классу. На данный момент все наши классы находятся в одном пакете, так что это еще не проблема.

Хороший! Теперь мы мастера наследования и видимости. Однако, глядя на классы TV и Parent, вы можете задаться вопросом, как значения могут быть инициализированы прямо при создании объекта? Кроме того, когда мы создаем новый объект:

TV someTV = new TV();

Что такое TV()? Вроде вызываем функцию, а в классе TV такой функции нет?

В следующей статье мы ответим на эти вопросы, взглянув на конструкторы.