Hey everyone! Программирование полно подводных камней. И нет практически ни одной темы, в которой вы не споткнетесь и не набьете шишки. Особенно это касается новичков. Уменьшить количество этого можно лишь одним способом — учиться. В частности это касается подробных разборов самых базовых тем. Сегодня продолжаю разбор вопросов 250+ с собеседований на Java-разработчика, которые хорошо охватывают базовые темы. Отмечу, что в списке есть и не совсем стандартные вопросы, позволяющие взглянуть на обычные темы под другим углом.Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 7 - 1

62. Что такое строковый пул и зачем он нужен?

В памяти в Java (Heap, о которой мы ещё поговорим) есть область — String pool, или строковый пул. Она предназначена для хранения строковых значений. Другими словами когда вы создаете некую строку, например через двойные кавычки:

String str = "Hello world";
происходит проверка того, имеет ли строковой пул данное значение. Если имеет, переменной str присваивается ссылка на это значение в пуле. Если же не имеет, создастся новое значение в пуле, и ссылка на него будет присвоена переменной str. Рассмотрим пример:

String firstStr = "Hello world";
String secondStr = "Hello world";
System.out.println(firstStr == secondStr);
На экран будет выведено true. Мы помним, что == сравнивает именно ссылки — значит эти две ссылки ссылаются на одно и то же значение из строкового пула. Это сделано для того, чтобы не плодить множество одинаковых объектов типа String в памяти, ведь как мы помним, String — неизменяемый класс, и если у нас будет множество ссылок на одно и то же значение, плохого в этом ничего нет. Теперь невозможна ситуация, при которой изменение значения в одном месте приводит к изменениям сразу для нескольких других ссылок. Но тем не менее, если мы создадим строку через new:

String str = new String("Hello world");
создастся отдельный объект в памяти, который будет хранить данное строковое значение (и не важно, есть ли у нас уже такое значение в строковом пуле). В качестве подтверждения:

String firstStr = new String("Hello world");
String secondStr = "Hello world";
String thirdStr = new String("Hello world");
System.out.println(firstStr == secondStr);
System.out.println(firstStr == thirdStr);
Мы получим два false, и это значит, что у нас тут три разные значения, на которые ссылаются ссылки. Собственно, поэтому рекомендуется создавать строки просто через двойные кавычки. Тем не менее, можно сложить (или получать ссылку) значения в строковой пул и при создании объекта через new. Для этого используем метод класса строки — intern(). Данный метод принудительно создает значение в строковом пуле, ну или получает ссылку на него, если оно уже хранится там. Вот пример:

String firstStr = new String("Hello world").intern();
String secondStr = "Hello world";
String thirdStr = new String("Hello world").intern();
System.out.println(firstStr == secondStr);
System.out.println(firstStr == thirdStr);
System.out.println(secondStr == thirdStr);
в результате мы получим в консоли три значения true, а значит, все три переменные ссылаются на одну и ту же строку.Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 7 - 2

63. Какие GOF-шаблоны применяются в строковом пуле?

В строковом пуле явно прослеживается GOF шаблон — легковес (flyweight), иначе его называют поселенец. Если же вы увидели тут ещё какой-то шаблон — делитесь в комментарии. Ну а мы поговорим о шаблоне легковесе. Легковес — структурный шаблон проектирования, при котором объект, представляющий себя как уникальный экземпляр в разных местах программы, по факту не является таковым. Легковес экономит память, разделяя общее состояние объектов между собой, вместо хранения одинаковых данных в каждом объекте. Для понимания сути рассмотрим самый простой пример. Предположим, у нас есть интерфейс сотрудника:

public interface Employee {
   void work();
}
И есть некоторые реализации, например, юрист:

public class Lawyer implements Employee {
 
   public Lawyer() {
       System.out.println("Юрист взят в штат.");
   }
 
   @Override
   public void work() {
       System.out.println("Решение юридических вопросов...");
   }
}
И бухгалтер:

public class Accountant implements Employee{
 
   public Accountant() {
       System.out.println("Бухгалтер взят в штат.");
   }
 
   @Override
   public void work() {
       System.out.println("Ведение бухгалтерского отчёта....");
   }
}
Методы весьма условны: нам всего лишь нужно видеть, что они выполняются. Такая же ситуация и с конструктором. Благодаря выводу в консоли мы будем видеть, когда создаются новые объекты. Также у нас есть отдел сотрудников, задача которого — выдавать запрашиваемого сотрудника, если же его нет — нанимать в штат и выдавать в ответ на запрос:

public class StaffDepartment {
   private Map<String, Employee> currentEmployees = new HashMap<>();
 
   public Employee receiveEmployee(String type) throws Exception {
       Employee result;
       if (currentEmployees.containsKey(type)) {
           result = currentEmployees.get(type);
       } else {
           switch (type) {
               case "Бухгалтер":
                   result = new Accountant();
                   currentEmployees.put(type, result);
                   break;
               case "Юрист":
                   result = new Lawyer();
                   currentEmployees.put(type, result);
                   break;
               default:
                   throw new Exception("Данный сотрудник в штате не предусмотрен!");
           }
       }
       return result;
   }
}
То есть логика простая: если есть данная единица — верни её, если нет — создай, помести в хранилище (что-то вроде кеша) и отдай назад. А теперь давайте посмотрим, как это всё работает:

public static void main(String[] args) throws Exception {
   StaffDepartment staffDepartment = new StaffDepartment();
   Employee empl1  = staffDepartment.receiveEmployee("Юрист");
   empl1.work();
   Employee empl2  = staffDepartment.receiveEmployee("Бухгалтер");
   empl2.work();
   Employee empl3  = staffDepartment.receiveEmployee("Юрист");
   empl1.work();
   Employee empl4  = staffDepartment.receiveEmployee("Бухгалтер");
   empl2.work();
   Employee empl5  = staffDepartment.receiveEmployee("Юрист");
   empl1.work();
   Employee empl6  = staffDepartment.receiveEmployee("Бухгалтер");
   empl2.work();
   Employee empl7  = staffDepartment.receiveEmployee("Юрист");
   empl1.work();
   Employee empl8  = staffDepartment.receiveEmployee("Бухгалтер");
   empl2.work();
   Employee empl9  = staffDepartment.receiveEmployee("Юрист");
   empl1.work();
   Employee empl10  = staffDepartment.receiveEmployee("Бухгалтер");
   empl2.work();
}
И в консоли, соответственно, будет вывод:
Юрист взят в штат. Решение юридических вопросов... Бухгалтер взят в штат. Ведение бухгалтерского отчёта.... Решение юридических вопросов... Ведение бухгалтерского отчёта.... Решение юридических вопросов... Ведение бухгалтерского отчёта.... Решение юридических вопросов... Ведение бухгалтерского отчёта.... Решение юридических вопросов... Ведение бухгалтерского отчёта...
Как вы видите, всего было создано лишь два объекта, которые при этом многократно переиспользовались. Пример весьма простой, но он наглядно демонстрирует как применение данного шаблона может сэкономить наши ресурсы. Ну и как вы заметили, логика данного паттерна уж больно похожа на логику работы страхового пула. Подробнее о разновидностях GOF паттернов вы можете почитать в этой статье.Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 7 - 3

64. Как разделить строку на части? Приведите пример соответствующего кода

Очевидно, что в данном вопросе речь идёт о методе split. У класса String есть две вариации данного метода:

String split(String regex);
и

String split(String regex);
regex — это разделитель строки — некоторое регулярное выражение, по которому производится разделение строки на массив строк, например:

String str = "Hello, world it's Amigo!";
String[] arr = str.split("\\s");
for (String s : arr) {
  System.out.println(s);
}
В консоль будет выведено:
Hello, world it's Amigo!
То есть, наше строковое значение было разбито на массив строк и разделителем послужил пробел (для разделения можно было использовать и не регулярное выражение пробела "\\s" и просто строковое выражение " "). Второй, перегруженный метод имеет дополнительный аргумент — limit. limit — максимально допустимое значение получаемого массива. То есть, когда строка будет уже разбита на предельное допустимое количество подстрок, дальнейшей разбивки не будет, и у последнего элемента будет “остаток” от возможно недоразбитой строки. Пример:

String str = "Hello, world it's Amigo!";
String[] arr = str.split(" ", 2);
for (String s : arr) {
  System.out.println(s);
}
Вывод в консоли:
Hello, world it's Amigo!
Как мы видим, если бы не ограничение limit = 2, последний элемент массива можно было бы разбить на три подстроки.Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 7 - 4

65. Почему массив символов лучше строки для сохранения пароля?

Причин для предпочтения массива строке при сохранении пароля несколько: 1. Строковой пул и неизменяемость строк. При использовании массива (char[]) мы можем явно стереть данные после того, как закончили работу с ним. Также мы можем сколько угодно переписывать массив, и действительного пароля нигде не будет в системе, даже до сбора мусора (достаточно изменить пару ячеек на недействительные значения). В то самое время String — immutable класс. То есть, если мы хотим изменить его значение, мы получим новое, а старое при этом останется в строковом пуле. Если мы захотим удалить String значение пароля, это может быть весьма сложным занятием, так как нужно, чтобы сборщик мусора удалил именно значение из String pool-а, и существует большая вероятность, что это String значение останется там надолго. То есть, в данной ситуации String уступает массиву char в безопасности хранения данных. 2. При случайном выводе в консоли (или в логи) значения String выведется само значение:

String password = "password";
System.out.println("Пароль - " + password);
Вывод в консоли:
Пароль - password
В то же время, если случайно вывести в консоль массив:

char[] arr = new char[]{'p','a','s','s','w','o','r','d'};
System.out.println("Пароль - " + arr);
в консоли будет непонятная абракадабра:
Пароль - [C@7f31245a
На самом деле не абракадабра, а: [C — имя класса — массив char, @ — разделитель, после которого — 7f31245a — шестнадцатеричный хешкод. 3. Официальный документ, руководство по архитектуре криптографии Java прямо указывает на хранение паролей в char[] вместо String: “Казалось бы, логично собрать и сохранить пароль в объекте типа java.lang.String. Однако здесь есть предостережение: объекты типа String неизменяемы, т. е. Не определены методы, позволяющие изменять (перезаписывать) или обнулять содержимое объекта String после использования. Эта функция делает String объекты непригодными для хранения конфиденциальной информации, такой как пароли пользователей. Вместо этого вы всегда должны собирать и хранить конфиденциальную информацию о безопасности в массиве символов.”Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 7 - 5

Enum

66. Дайте краткую характеристику Enum в Java

Enum — перечисление, набор строковых констант, объединенных общим типом. Объявляется через ключевое слово — enum. Вот пример с enum — допустимые роли в некоторой школе:

public enum Role {
   STUDENT,
   TEACHER,
   DIRECTOR,
   SECURITY_GUARD
}
Слова, написанные большими буквами, и есть те самые константы перечисления, которые объявляются упрощенно, без использования оператора new. Использование перечислений заметно упрощает жизнь, так как они помогают избежать ошибок и путаницы в наименованиях (так как может быть только определенный перечень значений). Лично для меня они очень удобны при использовании в логической конструкции Switch.

67. Может Enum реализовывать (implements) интерфейсы?

Да. Ведь перечисления должны представлять не просто пассивные наборы (как например, роли). В Java они могут представлять более сложные объекты с некоторым функционалом, поэтому вам, возможно, понадобится добавить к ним дополнительный функционал. Также это позволит использовать возможности полиморфизма, подставляя значение enum в места, где необходим тип имплементируемого интерфейса.

68. Может Enum расширять (extends) класс?

Нет, не может, так как перечисление — это подкласс по умолчанию универсального класса Enum <T>, где T представляет универсальный тип перечисления. Это не что иное, как общий базовый класс для всех типов перечисления языка Java. Преобразование enum в класс выполняется компилятором Java во время компиляции. Это расширение явно в коде не указывается, но всегда незримо присутствует.

69. Можно ли создать Enum без экземпляров объектов?

Как по мне, вопрос немного странный, ну или я его не до конца понял. У меня есть две интерпретации: 1. Может ли быть enum без значений — да, конечно, это будет что-то вроде пустого класса — бессмысленно:

public enum Role {
}
И вызвав:

var s = Role.values();
System.out.println(s);
Мы получим в консоли:
[Lflyweight.Role;@9f70c54
(пустой массив значений Role) 2. Можно ли создать enum без оператора new — да, конечно. Как я выше уже сказал, для значений (перечислений) enum не нужно использовать оператор new, так как это — статические значения.

70. Можно ли мы переопределить метод toString() для Enum?

Да, конечно вы можете переопределить метод toString(), чтобы определить конкретный способ отображения вашего enum при вызове метода toString (при переводе enum в обычную строку, например, для вывод в консоль или логи).

public enum Role {
   STUDENT,
   TEACHER,
   DIRECTOR,
   SECURITY_GUARD;

   @Override
   public String toString() {
       return "Выбрана роль - " + super.toString();
   }
}
На этом на сегодня у меня всё, до следующей части!Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 7 - 6
Другие материалы серии: