JavaRush /Java блог /Random /Разбор вопросов и ответов с собеседований на Java-разрабо...
Константин
36 уровень

Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14

Статья из группы Random
Салют! Мир постоянно движется и постоянно движемся мы. Раньше, для того чтобы стать Java-разработчиком, было достаточно немного знать синтаксис Java, ну а остальное придет. Со временем уровень знаний, необходимый для становления Java-разработчика, значительно вырос, как и конкуренция, которая продолжает двигать нижнюю планку необходимых знаний вверх. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14 - 1Если вы действительно хотите стать разработчиком, это нужно принять как данность и тщательно подготовиться, чтобы выделяться среди таких же, как и вы, новичков.Чем мы сегодня и займемся, а именно продолжим разбор вопросов 250+. В предыдущих статьях мы разобрали все вопросы уровня джун, и сегодня возьмемся за вопросы уровня мидл. Хотя я отмечу, что они не являются стопроцентными вопросами уровня мидла, большинство из них вы можете встретить на собеседовании джуниор уровня, ибо именно на таких собеседованиях идет подробное прощупывание вашей теоретической базы, в то время как у мидла уже вопросы более ориентированы на прощупывание его опыта. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14 - 2Но, без лишнего предисловия, давайте начинать.

Middle

Общие

1. В чем преимущества и недостатки ООП, если сравнивать с процедурным / функциональным программированием?

В разборе вопросов для Juinior был данный вопрос, и соответственно я уже ответил на него. Ищите этот вопрос и ответ на него в данной части статьи, вопросы 16 и 17.

2. Чем отличается агрегация от композиции?

В ООП существует несколько видов взаимодействия объектов, объединенных под общим понятием "Has-A Relationship". Это отношение указывает на то, что один объект является составной частью другого объекта. При этом существует два подвида этого отношения: Композиция — один объект создает другой объект и время жизни другого объекта зависит от времени жизни создавшего. Агрегация — объект получает ссылку (указатель) на другой объект в процессе конструирования (при этом время жизни другого объекта не зависит от времени жизни создавшего). Для большего понимания, давайте рассмотрим конкретный пример. У нас есть некоторый класс машины — Car, у которого в свою очередь есть внутренние поля типа — двигатель — Engine и список пассажиров — List<Passenger>, также у него есть метод начала движения — startMoving():

public class Car {
 
 private Engine engine;
 private List<Passenger> passengers;
 
 public Car(final List<Passenger> passengers) {
   this.engine = new Engine();
   this.passengers = passengers;
 }
 
 public void addPassenger(Passenger passenger) {
   passengers.add(passenger);
 }
 
 public void removePassengerByIndex(Long index) {
   passengers.remove(index);
 }
 
 public void startMoving() {
   engine.start();
   System.out.println("Машина начала своё движение");
   for (Passenger passenger : passengers) {
     System.out.println("В машине есть пассажир - " + passenger.getName());
   }
 }
}
В данном случае Композицией является связь между Car и Engine, так как работоспособность машины напрямую зависит от наличия объекта двигателя, ведь если engine = null, то мы получим NullPointerException. В свою очередь, и двигатель не может существовать без машины (зачем нам двигатель без машины?) и не может принадлежать нескольким машинам в один момент времени. Это значит то, что если мы удалим объект Car, то на объект Engine не будет больше ссылок, и его вскоре удалит Garbage Collector. Как вы видите, данная связь является весьма строгой (сильной). Агрегацией является связь между Car и Passenger, так как работоспособность Car никоим образом не зависит от объектов типа Passenger и их количества. Они могут как выходить из машины — removePassengerByIndex(Long index), так и заходить новые — addPassenger(Passenger passenger), несмотря на это, функционирование машины продолжится должным образом. В свою очередь, объекты Passenger могут существовать и без объекта Car. Как вы понимаете, это гораздо более слабая связь, нежели мы видим у композиции. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14 - 3Но это еще не все, объект, который связан агрегацией с другим, может также иметь данную связь с другими объектами в один и тот же момент времени. Например, вы как Java-студент, записаны на курсы английского, ООП и логарифмы в один и тот же момент времени, но при этом вы не являетесь критически необходимой частью их, без которой невозможно нормальное функционирование (как например, преподаватель).

3. Какие паттерны GoF вы использовали на практике? Приведите примеры.

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

4. Что такое прокси-объект? Приведите примеры

Прокси — это структурный паттерн проектирования, позволяющий подставлять вместо реальных объектов специальные объекты-заменители, или другими словами — прокси-объекты. Эти прокси-объекты перехватывают вызовы к изначальному объекту, позволяя вклинить некоторую логику до или после передачи вызова оригиналу. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14 - 4Примеры использования объекта-прокси:
  • Как удаленный прокси — используется, когда нам нужен удаленный объект (объект в другом адресном пространстве), который необходимо представить локально. В этом случае прокси будет заниматься созданием соединения, кодирования, декодированием и прочим, в то время как клиент будет использовать его, как если бы изначальный объект, находящийся в локальном пространстве.

  • Как виртуальный прокси — используется, когда нужен ресурсоемкий объект. В таком случае объект-прокси служит чем-то вроде изображения реального объекта, которого на самом деле еще нет. Когда к же данному объекту отправляется реальный запрос (вызов метода), лишь тогда происходит загрузка оригинального объекта и выполнение метода. Данный подход еще называется отложенной инициализацией, это бывает весьма удобно, ведь в некоторых ситуациях оригинальный объект может и не пригодиться, тогда и затрат на его создание не будет.

  • Как защитный прокси — используется, когда нужно контролировать доступ к некоторому объекту на основе прав клиента. То есть если клиент с недостающими правами доступа попытается обратиться к оригинальному объекту, прокси его перехватит и не пустит.

Рассмотрим пример виртуального прокси: У нас есть некоторый интерфейс обработчика:

public interface Processor {
 void process();
}
Реализация которого задействует слишком много ресурсов, но при этом он может быть использован не при каждом запуске приложения:

public class HiperDifficultProcessor implements Processor {
 @Override
 public void process() {
   // некоторый сверхсложная обработка данных
 }
}
Класс прокси:

public class HiperDifficultProcessorProxy implements Processor {
private HiperDifficultProcessor processor;
 
 @Override
 public void process() {
   if (processor == null) {
     processor = new HiperDifficultProcessor();
   }
   processor.process();
 }
}
Запустим его в main:

Processor processor = new HiperDifficultProcessorProxy();
// тут тяжеловсеного оригинального объекта, ещё не сущетсвует
// но при этом есть объект, который его представляет и у которого можно вызывать его методы
processor.process(); // лишь теперь, объект оригинал был создан
Отмечу, что проксирование используют многие фреймворки, а для Spring это и вовсе ключевой паттерн (Spring прошит им вдоль и поперек). Подробнее о данном паттерне читайте вот тут. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14 - 5

5. Какие нововведения анонсировано в Java 8?

Нововведения принесенные Java 8 следующие:
  • Добавлены функциональные интерфейсы, о том что это за зверь читайте тут.

  • Лямбда-выражения, которые тесно связаны с функциональными интерфейсами, подробнее об их использовании вот тут.

  • Добавлено Stream API для удобной обработки коллекций данных, подробнее читайте тут.

  • Добавлены ссылки на методы.

  • В Iterable интерфейс добавлен метод forEach().

  • Добавлен новый API даты и времени в пакете java.time, подробный разбор тут.

  • Улучшили Concurrent API.

  • Добавление класса обертки Optional, который используется для корректной обработки нулевых значений, отличную статью на данную тему вы найдете вот тут.

  • Добавление интерфейсам возможности использовать static и default методы (что, по сути, приближает Java к множественному наследованию), подробнее тут.

  • Добавили новые методы в класс Collection(removeIf(), spliterator()).

  • Мелкие улучшения Java Core.

Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14 - 6

6. Что такое High Cohesion и Low Coupling? Приведите примеры.

High Cohesion или Высокая связанность — это понятие, когда некоторый класс содержит элементы, которые тесно связаны друг с другом и объединены по своему предназначению. Например, все методы в классе User должны представлять поведение пользователя. Класс имеет низкую связность, если он содержит несвязанные элементы. Например, класс User, содержащий метод валидации адреса электронной почты:

 public class User {
private String name;
private String email;
 
 public String getName() {
   return this.name;
 }
 
 public void setName(final String name) {
   this.name = name;
 }
 
 public String getEmail() {
   return this.email;
 }
 
 public void setEmail(final String email) {
   this.email = email;
 }
 
 public boolean isValidEmail() {
   // некоторая логика валидации емейла
 }
}
Класс пользователя может нести ответственность за хранение адреса электронной почты пользователя, но никак не за его проверку или отправку электронного письма. Поэтому, чтобы достичь высокой связности, выносим метод валидации в отдельный класс утилиту:

public class EmailUtil {
 public static boolean isValidEmail(String email) {
   // некоторая логика валидации емейла
 }
}
И используем по мере надобности (например, перед сохранением user-у). Low Coupling или Низкое зацепление — это понятие описывающее низкую взаимозависимость между программными модулями. По сути, взаимозависимость заключается в том, как изменение одного требует изменения другого. Два класса имеют сильную связь (или плотную связь), если они тесно связаны. Например, два конкретных класса, хранящие ссылки друг на друга и вызывающие методы друг друга. Слабосвязанные классы более просты в разработке и поддержке. Поскольку они независимы друг от друга, их можно разрабатывать и тестировать их параллельно. Кроме того, они могут быть изменены и обновлены, не влияя друг на друга. Рассмотрим пример сильно связанных классов. У нас есть некоторый класс студента:

public class Student {
 private Long id;
 private String name;
 private List<Lesson> lesson;
}
Который в себе содержит список уроков:

public class Lesson {
 private Long id;
 private String name;
 private List<Student> students;
}
Каждый урок содержит ссылку на посещающих студентов. Невероятно сильное сцепление, вам так не кажется? Как же можно снизить его? Во-первых, сделаем так, чтобы студенты имели не список предметов, а список их идентификаторов:

public class Student {
 private Long id;
 private String name;
 private List<Long> lessonIds;
}
Во-вторых, классу урока вовсе не обязательно знать о всех студентах, поэтому вовсе удалим их список:

public class Lesson {
 private Long id;
 private String name;
}
Так стало гораздо проще, и связь стала гораздо слабее, вы не находите? Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14 - 7

ООП

7. Каким образом можно реализовать множественное наследование в Java?

Множественное наследование — это особенность объектно-ориентированной концепции, когда класс может наследовать свойства более чем одного родительского класса. Проблема возникает, когда существуют методы с одинаковой сигнатурой как в супер классах, так и в подклассе. При вызове метода компилятор не может определить, какой метод класса должен быть вызван, и даже при вызове метода класса, который получает приоритет. Поэтому множественное наследование Java не поддерживает! Но есть своего рода лазейка, о которой мы и поговорим далее. Как я и упомянул ранее, с выходом Java 8, интерфейсам была добавлена возможность иметь методы по умолчанию — default методы. Если имплементирующий интерфейс класс не переопределяет данный метод, то будет использована данная реализация по умолчанию (переопределять дефолтный метод не обязательно, как например имплементировать абстрактный). В таком случае, возможно имплементация различных интерфейсов одним классом и использование их методов по умолчанию. Рассмотрим пример. У нас есть некоторый интерфейс летуна, с default методом fly():

public interface Flyer {
 default void fly() {
   System.out.println("Я лечу!!!");
 }
}
Интерфейс ходуна, с default методом walk():

public interface Walker {
 default void walk() {
   System.out.println("Я хожу!!!");
 }
}
Интерфейс плавуна, с методом swim():

public interface Swimmer {
 default void swim() {
   System.out.println("Я плыву!!!");
 }
}
Ну а теперь имплементируем все это в одном классе утки:

public class Duck implements Flyer, Swimmer, Walker {
}
И запустим, все методы нашей утки:

Duck donald = new Duck();
donald.walk();
donald.fly();
donald.swim();
В консоли мы получим:
Я хожу!!! Я лечу!!! Я плыву!!!
А это значит, что мы верно изобразили множественное наследование, хоть это и не оно. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14 - 8Также отмечу, если класс будет имплементировать интерфейсы с дефолтными методами, которые имеют одинаковые названия методов и одинаковые аргументы в этих методах, то компилятор начнет ругаться на несовместимость, так как он не понимает какой метод действительно нужно применять. Выходов тут несколько:
  • Переименовать методы в интерфейсах, чтобы они различались между собой.
  • Переопределить такие спорные методы в классе имплементации.
  • Наследоваться от класса, который реализует данные спорные методы (тогда ваш класс будет использовать именно его реализацию).

8. Какая разница между методами final, finally и finalize()?

final — это ключевое слово, который используется для наложения ограничений на класс, метод или переменную, ограничение означающее:
  • Для переменной — после первичной инициализации, переменную переопределить нельзя.
  • Для метода — метод не может быть переопределен в подклассе (классе наследнике).
  • Для класса — класс не может быть унаследован.
finally — это ключевое слово перед блоком с кодом, используется при обработке исключений, совместно с блоком try и совместно (или взаимозаменяемо) с блоком catch. Код в данном блоке выполняется в любом случае, независимо от того, будет ли выброшено исключение или нет. В данной части статьи, в 104-м вопросе разобраны исключительные ситуации в которых данный блок выполнен не будет. finalize() — метод класса Object, вызывается перед удалением каждого объекта сборщиком мусора, будет вызван этот метод (напоследок), используется для очистки занимаемых ресурсов. Подробнее об методах класса Object, которые наследует каждый объект смотрите в данной части статьи в 11-м вопросе. Ну а на этом мы сегодня и закончим. До встречи в следующей части! Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14 - 9
Другие материалы серии:
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ