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

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

Стаття з групи Random UA
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 - неймовірний клас. Тобто якщо ми хочемо змінити його значення, ми отримаємо нове, а старе при цьому залишиться в рядковому пулі. Якщо ми захочемо видалити 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
Інші матеріали серії:
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ