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

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

Статья из группы Random
Привет-привет! Как много нужно знать Java разработчику? Можно долго спорить по этому вопросу, но правда в том, что на собеседовании вас будут гонять по теории в полный рост. Даже по тем областям знаний, которым вам не доведется воспользоваться в работе. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 15 - 1Ну а если вы новичок, по вашим теоретическим знаниям пройдутся очень серьезно. Раз опыта и больших достижений еще нет, остается только проверить прочность базы знаний. Сегодня мы продолжим заниматься укреплением этой самой базы, разбирая самые популярные вопросы на собеседованиях для Java-разработчиков. Полетели!

Java Core

9. В чем разница между статическим и динамическим связыванием в Java?

На данный вопрос я уже ответил в этой статье в 18 вопросе про статический и динамический полиморфизм, советую ознакомиться.

10. Можно ли использовать private или protected переменные в interface?

Нет, нельзя. Так как когда вы объявляете интерфейс, компилятор Java автоматически добавляет ключевые слова public и abstract перед методами интерфейса и ключевые слова public, static и final перед членами данных. Собственно, если вы добавите private или protected, возникнет конфликт, и компилятор будет ругаться на модификатор доступа сообщением: “Modifier ‘<выбранный модификатор>’ not allowed here” Почему же компилятор добавляет public, static и final переменным в интерфейсе? Давайте разберёмся:
  • public — интерфейс предоставляет возможность клиенту взаимодействовать с объектом. Если бы переменные не были общедоступными, у клиентов не было бы к ним доступа.
  • static — интерфейсы не могут быть созданы (а точнее, их объекты), поэтому переменная статична.
  • final — так как интерфейс используется для достижения 100% абстракции, переменная имеет свой конечный вид (и не будет изменена).

11. Что такое Classloader и для чего используется?

Classloader — или Загрузчик классов — обеспечивает загрузку классов Java. А точнее, обеспечивают загрузку его наследники — конкретные загрузчики классов, т.к. сам ClassLoader абстрактен. Каждый раз, когда загружается какой-либо .class-файл, например, после обращения к конструктору или статическому методу соответствующего класса, это действие выполняет один из наследников класса ClassLoader. Есть три вида наследников:
  1. Bootstrap ClassLoader — базовый загрузчик, реализован на уровне JVM и не имеет обратной связи со средой выполнения, так как является частью ядра JVM и написан в машинном коде. Данный загрузчик служит родительским элементом для всех других экземпляров ClassLoader.

    В основном отвечает за загрузку внутренних классов JDK, обычно rt.jar и других основных библиотек, расположенных в каталоге $ JAVA_HOME / jre / lib. У разных платформ могут быть разные реализации этого загрузчика классов.

  2. Extension Classloader — загрузчик расширений, потомок класса базового загрузчика. Заботится о загрузке расширения стандартных базовых классов Java. Загружается из каталога расширений JDK, обычно — $ JAVA_HOME / lib / ext или любого другого каталога, упомянутого в системном свойстве java.ext.dirs (с помощью данной опции можно управлять загрузкой расширений).

  3. System ClassLoader — системный загрузчик, реализованный на уровне JRE, который заботится о загрузке всех классов уровня приложения в JVM. Он загружает файлы, найденные в переменном окружении классов -classpath или -cp опции командной строки.

Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 15 - 2Загрузчики классов — это часть среды выполнения Java. В тот момент когда JVM запрашивает класс, загрузчик классов пытается найти класс и загрузить определение класса в среду выполнения, используя полное имя класса. Метод java.lang.ClassLoader.loadClass() отвечает за загрузку определения класса во время выполнения. Он пытается загрузить класс на основе полного имени. Если класс еще не загружен, он делегирует запрос загрузчику родительского класса. Этот процесс происходит рекурсивно выглядит так:
  1. System Classloader пытается найти класс в своем кеше.

    • 1.1. Если класс найден, загрузка успешно завершена.

    • 1.2. Если класс не найден, загрузка делегируется к Extension Classloader-у.

  2. Extension Classloader пытается найти класс в собственном кеше.

    • 2.1. Если класс найден — успешно завершена.

    • 2.2. Если класс не найден, загрузка делегируется Bootstrap Classloader-у.

  3. Bootstrap Classloader пытается найти класс в собственном кеше.

    • 3.1. Если класс найден, загрузка успешно завершена.

    • 3.2. Если класс не найден, базовый Bootstrap Classloader попытается его загрузить.

  4. Если загрузка:

    • 4.1. Прошла успешно — загрузка класса завершена.

    • 4.2. Не прошла успешно — управление передается к Extension Classloader.

  5. 5. Extension Classloader пытается загрузить класс, и если загрузка:

    • 5.1. Прошла успешно — загрузка класса завершена.

    • 5.2. Не прошла успешно — управление передается к System Classloader.

  6. 6. System Classloader пытается загрузить класс, и если загрузка:

    • 6.1. Прошла успешно — загрузка класса завершена.

    • 6.2. Не прошла успешно — генерируется исключение — ClassNotFoundException.

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

12. Что такое Run-Time Data Areas?

Run-Time Data Ares — области данных среды выполнения JVM. JVM определяет некоторые области данных времени выполнения, необходимые во время выполнения программы. Одни из них создаются при запуске JVM. Другие являются локальными по отношению к потокам и создаются только при создании потока (и уничтожаются, когда поток уничтожается). Области данных среды выполнения JVM выглядят так: Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 15 - 3
  • PC Register — регистр ПК — локален для каждого потока и содержит адрес инструкции JVM, которую поток выполняет в данный момент.

  • JVM Stack — область памяти, которая используется как хранилище для локальных переменных и временных результатов. У каждого потока есть свой отдельный стек: как только поток завершается, этот стек также уничтожается. Стоит отметить, что преимуществом stack над heap является производительность, в то время как heap безусловно имеет преимущество в масштабе хранилища.

  • Native Method Stack — область данных для каждого потока, в которой хранятся элементы данных, аналогичные стеку JVM, для выполнения собственных (не Java) методов.

  • Heap — используется всеми потоками как хранилище которое содержит объекты, метаданные классов, массивы и т. д., которые создаются во время выполнения. Данная область создается при запуске JVM и уничтожается при завершении ее работы.

  • Method area — область метода — эта область времени выполнения общая для всех потоков и создается при запуске JVM. Он хранит структуры для каждого класса, такие как пул констант (Runtime Constant Pool — пул для хранения констант), код для конструкторов и методов, данные метода и т. д.

13. Что такое immutable object?

В данной части статьи в 14 и 15 вопросе уже есть ответ на этот вопрос, поэтому ознакамливаетесь не теряя времени зря.

14. В чем особенность класса String?

Ранее в разборе мы неоднократно говорили про те или иные особенности String (для этого был отдельный раздел). Сейчас же подведем итог по особенностям String:
  1. Это самый популярный объект в Java, который применяют для разнообразных целей. По частоте использования он не уступает даже примитивным типам.

  2. Объект данного класса можно создать без использования ключевого слова new — непосредственно через кавычки String str = “строка”;.

  3. String — это immutable класс: при создании объекта данного класса его данные нельзя изменить (когда вы к некоторой строке добавляете + “другую строку”, как результат вы получите новую, третью строку). Неизменность класса String делает его потокобезопасным.

  4. Класс String финализирован (имеет модификатор final), поэтому его наследование невозможно.

  5. У String есть свой пул строк, область памяти в heap, которая кеширует создаваемые строковые значения. В этой части серии, в 62 вопросе, я описывал строковой пул.

  6. В Java присутствуют аналоги String, также предназначенные для работы с строками — StringBuilder и StringBuffer, но с тем отличием, что они изменяемые. Подробнее о них вы можете почитать в этой статье.

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

15. Что такое ковариантность типов?

Для понимания ковариантности мы рассмотрим пример. Предположим, у нас есть класс животного:

public class Animal {
 void voice() {
   System.out.println("*тишина*");
 }
}
И некоторый расширяющий его класс Dog:

public class Dog extends Animal {
 
 @Override
 public void voice() {
   System.out.println("Гав, гав, гав!!!");
 }
}
Как мы помним, родительскому типу мы можем без проблем присваивать объекты типа наследника:

Animal animal = new Dog();
Это у нас будет ничто иное как полиморфизм. Удобно, гибко не так ли? Ну а в случае со списком животных? Сможем ли мы задать списку с дженериком Animal список с объектами Dog?

List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs;
В таком случае строка присвоения списку животных списка собак будет подчеркнута красным, т.е. компилятор не пропустит данный код. Несмотря на то, что вроде как это присваивание весьма логично (ведь переменной типа Animal мы можем присвоить объект Dog) его сделать нельзя. Это происходит потому, что если бы это было допустимо, в список, который изначально предназначен для Dog, мы сможем положить объект Animal, при этом думая, что в списке у нас только Dogs. И потом, к примеру, возьмём с помощью метода get() объект у того списка dogs, думая, что это собака, и вызовем у него некоторый метод объекта Dog, которого нет у Animal. И как вы понимаете, это невозможно — упадет ошибка. Но, к счастью, компилятор не пропускает данную логическую ошибку с присвоением списка потомков, списку родителей (и наоборот). В Java возможно присвоение объектов списков лишь переменным списков с совпадающими дженериками. Это и называется инвариацией. Если бы могли это сделать, это называлось бы и называлось ковариацией. То есть, ковариация — это если бы мы могли переменной типа List<Animal> задать объект типа ArrayList<Dog>. Получается что в Java ковариантность не поддерживается? Как бы не так! Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 15 - 5Но это делается своим, особым путем. Для этого используется конструкция ? extends Animal. Она ставится дженериком переменной, которой мы хотим задать объект списка, с дженериком потомка. Эта конструкция дженерика значит, что подойдёт любой тип, который является потомком типа Animal (и тип Animal также попадает под это обобщение). В свою очередь, Animal может быть не только классом, но и интерфейсом (и пусть вас не вводит в заблуждение ключевое слово extends). Наше предыдущее присваивание мы можем выполнить следующим образом:

List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
В результате вы увидите в IDE, что компилятор не будет ругаться на данную конструкцию. Давайте проверим работоспособность данной конструкции. Предположим, у нас есть метод, который заставляет всех переданных ему животных издать звуки:

public static void animalsVoice(List<? extends Animal> animals) {
 for (Animal animal : animals) {
   animal.voice();
 }
}
Передадим ему список с собаками:

List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
animalsVoice(dogs);
В консоли мы увидим следующий вывод:
Гав, гав, гав!!! Гав, гав, гав!!! Гав, гав, гав!!!
А значит данный подход к ковариантности успешно работает. Отмечу, что в список с данным дженериком ? extends Animal мы не можем вставить новые данные никакого типа: ни типа Dog, ни даже типа Animal:

List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
animals.add(new Dog());
dogs.add(new Animal());
Собственно, в последних двух строках компилятор будет подчеркивать красным вставку объектов. Это связано с тем, что мы не можем быть на сто процентов уверены, список с объектами какого типа будет присвоен списку с данных дженериком <? extends Animal>. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 15 - 6Хотелось бы ещё рассказать про контравариантность, так как обычно это понятие идет всегда вместе с ковариантностью, и как правило спрашивают о них вместе. Это понятие — некоторая противоположность ковариантности, так как для данной конструкции используется тип наследника. Предположим, нам нужен список, которому можно будет присвоить список с типом объектов, не являющихся предками объекта Dog. При этом мы заранее не знаем, что это будут за конкретные типы. В таком случае нас может выручить конструкция вида ? super Dog, для которой подходят все типы — прародители класса Dog:

List<Animal> animals = new ArrayList<>();
List<? super Dog> dogs = animals;
dogs.add(new Dog());
dogs.add(new Dog());
Мы можем смело добавлять в список с таким дженериком объекты типа Dog, ведь у него в любом случае присутствуют все реализованные методы любого его прародителя. Но мы не сможем добавить объект типа Animal, так как нет уверенности, что внутри будут именно объекты этого типа, а не, например, Dog. Ведь мы можем запросить у элемента данного списка метод класса Dog, которого не будет в наличии у Animal. В таком случае возникнет ошибка компиляции. Также, если бы мы захотели реализовать предыдущий метод, но уже с данным дженериком:

public static void animalsVoice(List<? super Dog> dogs) {
 for (Dog dog : dogs) {
   dog.voice();
 }
}
мы бы получили ошибку компиляции в цикле for, так как мы не можем быть уверены, что пришедший список содержит объекты типа Dog и свободно использовать его методы. Если у данного списка мы вызовем метод dogs.get(0); — мы получим объект типа Object. То есть для работы метода animalsVoice() нам как минимум нужно добавить небольшие манипуляции с сужением данных вида:

public static void animalsVoice(List<? super Dog> dogs) {
 for (Object obj : dogs) {
   if (obj instanceof Dog) {
     Dog dog = (Dog) obj;
     dog.voice();
   }
 }
}
Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 15 - 7

16. Как есть методы в классе Object?

В данной части серии, в 11 пункте, я уже ответил на данный вопрос, поэтому настоятельно советую ознакомиться, если вы до сих пор этого не сделали. На этом на сегодня и закончим. До встречи в следующей части! Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 15 - 8
Другие материалы серии:
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Justinian Уровень 41 Master
4 октября 2021

10. Можно ли использовать private или protected переменные в interface?
в этом вопросе упоминаются методы, хотел бы акцентировать на разграничении переменных и методов в рамках интерфейсов: -переменные приватными быть не могут - дефолтные методы - приватными быть не могут (это часть АПИ интерфейса - конструкция 2 в 1, И публичный метод, и его реализация по умолчанию) - абстрактные методы - приватными быть не могут - они часть АПИ интерфейса, его контракта. - хелпер методы с реализацией быть приватными МОГУТ начиная с 9-й джавы:

interface Calc {
    default int multiplyNumbers(int a, int b) {
        int result = multiply(a, b);
        return result;
    }

    private static int multiply(int a, int b) {
        return a * b;
    }
}

class Calculator implements Calc {
}
...
new Calculator().multiplyNumbers(1, 2);