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

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

Стаття з групи Random UA
Вітання!
Рух до мети – це передусім рух.
Тому мало лише думати про те, що ти хочеш чогось досягти. Потрібно щось робити — навіть найменші кроки, — але робити їх щодня, і тільки так ви досягнете кінцевої мети. А так як ви тут, щоб стати Java-розробниками, вам потрібно щодня робити хоча б мінімальний крок у бік поглиблень знань з Java. Як сьогоднішній Java-кроку пропоную ознайомитися з новою частиною аналізу найпопулярніших питань на співбесідах для розробників. Розбір запитань та відповідей із співбесід на Java-розробника.  Частина 13 - 1Сьогодні ми пройдемося з практичної частини питань для Junior-фахівців. Практичне завдання на співбесіді – не рідкість. Важливо не губитися в такій ситуації, постаратися зберегти холодну голову та запропонувати оптимальне рішення, а то й дещо. Також я б порекомендував не мовчати при вирішенні завдання, а коментувати хід своїх думок та написання рішення, ну або після написання пояснити на словах, що й навіщо ви зробабо. Це набагато більше розташує інтерв'юера до вас, ніж мовчазне рішення. Отже, почнемо!

111. Як між потоками обмінюватись даними?

Розбір запитань та відповідей із співбесід на Java-розробника.  Частина 13 - 2Для обміну даними між потоками можна використовувати багато різних підходів та засобів: наприклад, скористатися атомарними змінними, синхронізованими колекціями, семафором. Але для вирішення цього завдання я наведу приклад з Exchanger . Exchanger - це клас синхронізації з concurrent пакета, який полегшує обмін елементами між парою потоків за рахунок створення спільної точки синхронізації. Його використання полегшує обмін даними між двома потоками. Механізм його роботи дуже простий: він чекає, доки два окремі потоки не викличуть його метод exchange(). Між ними створюється щось подібне до точки обміну: перший потік кладе свій об'єкт і отримує замість об'єкт іншого, а той у свою чергу отримує об'єкт першого і кладе свій. Тобто, перший потік використовує метод exchange() і не діє до тих пір, поки інший потік не викличе метод exchange() цього ж об'єкта і між ними не відбудеться обмін даними. Як приклад розглянемо таку реалізацію класу Thread :
public class CustomThread extends Thread {
 private String threadName;
 private String message;
 private Exchanger<String> exchanger;

 public CustomThread(String threadName, Exchanger<String> exchanger) {
   this.threadName = threadName;
   this.exchanger = exchanger;
 }

 public void setMessage(final String message) {
   this.message = message;
 }

 @Override
 public void run() {
   while (true) {
     try {
       message = exchanger.exchange(message);
       System.out.println(threadName + " поток получил сообщение: " + message);
       Thread.sleep(1000);
     } catch (Exception e) {
       e.printStackTrace();
     }
   }
 }
}
У конструкторі потоку ми задаємо об'єкт Exchanger , приймаючий об'єкти типу String , а запуску (у методі run ) використовуємо його exchange() обмінюватися повідомленням з іншим потоком, використовуючи цей метод у тому Exchanger . Давайте запустимо його в main :
Exchanger<String> exchanger = new Exchanger<>();
CustomThread first = new CustomThread("Первый ", exchanger);
first.setMessage("Сообщение первого потока");
CustomThread second = new CustomThread("Второй", exchanger);
second.setMessage("Сообщение второго потока");
first.start();
second.start();
У консолі буде виведено:
Перший потік отримав повідомлення: Повідомлення другого потоку Другий потік отримав повідомлення: Повідомлення першого потоку Другий потік отримав повідомлення: Повідомлення другого потоку Перший потік отримав повідомлення: Повідомлення першого потоку Перший потік отримав повідомлення: Повідомлення другого потоку .. .
Це означає, що обмін даними між потоками відбувається успішно.

112. У чому полягає відмінність класу Thread від інтерфейсу Runnable?

Перше, що відзначу, Thread – це клас, Runnable – інтерфейс, що дуже очевидна відмінність = D Розбір запитань та відповідей із співбесід на Java-розробника.  Частина 13 – 3Також скажу, що Thread використовує Runnable (композиція). Тобто у нас є два шляхи:
  1. Наслідувати Thread , перевизначити метод run, після чого створити даний об'єкт і запустити потік через метод start() .

  2. Реалізувати Runnable у певному класі, реалізувати його метод run() , після чого створити об'єкт Thread , задавши йому конструктор цей об'єкт-реалізацію інтерфейсу Runnable . Ну і наприкінці запустити об'єкт Thread за допомогою методу start() .

Що ж краще? Давайте трохи поміркуємо:
  • при реалізації інтерфейсу Runnable ви не змінюєте поведінку потоку. По суті, ви просто даєте потоку щось запустити. А це у нас композиція, що, у свою чергу, вважається хорошим підходом.

  • реалізація Runnable дає більше гнучкості вашому класу. Якщо ви успадкуєте від Thread , то дія, яку ви виконуєте, завжди буде в потоці. Але якщо ви реалізуєте Runnable , це не обов'язково буде просто потік. Адже ви можете як запустити його в потоці, так і передати будь-якій службі-виконавцю. Ну чи просто передати його кудись як завдання в однопотоковому додатку.

  • Використання Runnable дозволяє логічно відокремити виконання завдання від логіки управління потоками.

  • у Java можливе лише одиночне успадкування, тому можна розширити лише один клас. У той же час кількість інтерфейсів, що розширюються, необмежена (ну не зовсім необмежена, а 65535 , але навряд чи ви колись упреєтеся в цей ліміт).

Ну а що саме краще використовувати, вирішувати вже вам ^^

113. Є потоки Т1, Т2 та Т3. Як реалізувати їхнє послідовне виконання?Розбір запитань та відповідей із співбесід на Java-розробника.  Частина 13 – 4

Найперше і найпростіше, що спадає на думку - це використання методу join() . Він зупиняє виконання поточного (що викликав даний метод) потоку до того часу, поки потік, у якому викликаний метод, закінчить своє виконання. Створимо свою реалізацію потоку:
public class CustomThread extends Thread {
private String threadName;

 public CustomThread(final String  threadName){
   this.threadName = threadName;
 }

 @Override
 public void run() {
   System.out.println(threadName + " - начал свою работу");
   try {
     // происходит некая логика
     Thread.sleep(1000);
   } catch (InterruptedException e) {
     e.printStackTrace();
   }

   System.out.println(threadName + " - закончил свою работу");
 }
}
Запустимо три таких потоки по черзі, використовуючи join() :
CustomThread t1 = new CustomThread("Первый поток");
t1.start();
t1.join();
CustomThread t2 = new CustomThread("Второй поток");
t2.start();
t2.join();
CustomThread t3 = new CustomThread("Третий поток");
t3.start();
t3.join();
Висновок у консолі:
Перший потік - почав свою роботу Перший потік - закінчив свою роботу Другий потік - почав свою роботу Другий потік - закінчив свою роботу Третій потік - почав свою роботу Третій потік - закінчив свою роботу
Це означає, що ми впоралися з нашим завданням. Далі переходимо безпосередньо до практичних завдань рівня Junior .

Практичні завдання

114. Matrix Diagonal Sum (завдання з Leetcode)

Умова: Підрахуйте суму всіх елементів на основній діагоналі та всіх елементів на додатковій діагоналі, які не є частиною основної діагоналі. Розбір запитань та відповідей із співбесід на Java-розробника.  Частина 13 – 51. При матриці виду: mat = [[1,2,3], [4,5,6], [7,8,9]] Висновок має бути - 25 2. При матриці - mat = [[1,1 ,1,1], [1,1,1,1], [1,1,1,1], [1,1,1,1]] Висновок має бути - 8 3. При матриці - mat = [[ 5]] Висновок має бути - 5 Зробіть паузу в прочитанні і реалізуйте своє рішення. Моє ж рішення буде наступним:
public static int countDiagonalSum(int[][] matrix) {
 int sum = 0;
 for (int i = 0, j = matrix.length - 1; i < matrix.length; i++, j--) {
   sum += matrix[i][i];
   if (j != i) {
     sum += matrix[i][j];
   }
 }
 return sum;
}
Все відбувається за допомогою одного проходу масивом, під час якого у нас є два індекси для звіту: i — для звіту рядків масиву і колонок основної діагоналі, j — для звіту колонок додаткової діагоналі. Якщо ж комірка основної діагоналі та додаткової збігаються, то одне із значень ігнорується при підрахунку суми. Перевіримо, використовуючи матриці з умови:
int[][] arr1 = {
   {1, 2, 3},
   {4, 5, 6},
   {7, 8, 9}};
System.out.println(countDiagonalSum(arr1));

int[][] arr2 = {
   {1, 1, 1, 1},
   {1, 1, 1, 1},
   {1, 1, 1, 1},
   {1, 1, 1, 1}};
System.out.println(countDiagonalSum(arr2));

int[][] arr3 = {{5}};
System.out.println(countDiagonalSum(arr3));
Висновок у консолі:
25 8 5

115. Move Zeroes (завдання з Leetcode)

Умова: У цілісному масиві перемістіть усі 0 на кінець, зберігаючи відносний порядок ненульових елементів. 1. При масиві: [0,1,0,3,12] Висновок має бути: [1,3,12,0,0] 2. При масиві: [0] Висновок повинен бути: [0] Зробіть паузу та напишіть своє рішення ... Моє рішення:
public static void moveZeroes(int[] nums) {
 int counterWithoutNulls = 0;
 int counterWithNulls = 0;
 int length = nums.length;
 while (counterWithNulls < length) {
   if (nums[counterWithNulls] == 0) {// находим нулевые элементы и увеличиваем счётчик
     counterWithNulls++;
   } else { // сдвигаем элементы на количество найденных нулевых элементов слева
     nums[counterWithoutNulls++] = nums[counterWithNulls++];
   }
 }
 while (counterWithoutNulls < length) {
   nums[counterWithoutNulls++] = 0;// заполняем последние элементы массива нулями согласно счётчику нулей
 }
}
Перевірка:
int[] arr1 = {1, 2, 0, 0, 12, 9};
moveZeroes(arr1);
System.out.println(Arrays.toString(arr1));

int[] arr2 = {0};
moveZeroes(arr2);
System.out.println(Arrays.toString(arr2));
Виведення в консоль:
[1, 2, 12, 9, 0, 0] [0]

116. Given List <String> names. Видаліть першу літеру з кожного імені та поверніть відсортований список

1. Перше, що спадає на думку, це методи класу Collections , що зберігає в собі безліч допоміжних методів для колекцій:
public static List<String> processTheList(List<String> nameList) {
 for (int i = 0; i < nameList.size(); i++) {
   nameList.set(i, nameList.get(i).substring(1));
 }
 Collections.sort(nameList);
 return nameList;
}
2. Також якщо ми використовуємо Java версії 8 і вище, ми просто зобов'язані показати рішення через стрими:
public static List<String> processTheList(List<String> nameList) {
 return nameList.stream()
     .map(x -> x.substring(1))
     .sorted().collect(Collectors.toList());
}
Незалежно від обраного рішення, перевірка може бути такою:
List<String> nameList = new ArrayList();
nameList.add("John");
nameList.add("Bob");
nameList.add("Anna");
nameList.add("Dmitriy");
nameList.add("Peter");
nameList.add("David");
nameList.add("Igor");

System.out.println(processTheList(nameList));
Висновок у консолі:
[avid, eter, gor, mitriy, nna, ob, ohn]

117. Переверніть масив

Рішення 1 Знову ж таки, перше, що спадає на думку — використовувати методи допоміжного, утилітного класу Collections . Але так як у нас масив, спочатку потрібно перетворити його на колекцію (список):
public static Integer[] reverse(Integer[] arr) {
 List<Integer> list = Arrays.asList(arr);
 Collections.reverse(list);
 return list.toArray(arr);
}
Рішення 2 Так як питання було про масив, думаю, необхідно показати рішення і без використання готового функціоналу з коробки, а так би мовити, за класикою:
public static Integer[] reverse(Integer[] arr) {
 for (int i = 0; i < arr.length / 2; i++) {
   int temp = arr[i];
   arr[i] = arr[arr.length - 1 - i];
   arr[arr.length - 1 - i] = temp;
 }
 return arr;
}
Перевірка:
Integer[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9};
System.out.println(Arrays.toString(reverse(arr)));
Висновок у консолі:
[9, 8, 7, 6, 5, 4, 3, 2, 1]

118. Перевірити, чи є рядок паліндромом

Розбір запитань та відповідей із співбесід на Java-розробника.  Частина 13 - 6Рішення 1 Варто відразу згадати про StringBuilder : він більш гнучкий і насичений різними методами порівняно із звичайним String . Нас особливо цікавить метод reverse :
public static boolean isPalindrome(String string) {
 string = string.toLowerCase(); //приводит всю строку к нижнему регистру
 StringBuilder builder = new StringBuilder();
 builder.append(string);
 builder.reverse(); // перевочиваем строку методом Builder-а
 return (builder.toString()).equals(string);
}
Рішення: Наступний підхід буде без використання лазівок з коробки. Порівнюємо символи із задньої частини рядка з відповідними символами із передньої:
public static boolean isPalindrome(String string) {
  string = string.toLowerCase();
 int length = string.length();
 int fromBeginning = 0;
 int fromEnd = length - 1;
 while (fromEnd > fromBeginning) {
   char forwardChar = string.charAt(fromBeginning++);
   char backwardChar = string.charAt(fromEnd--);
   if (forwardChar != backwardChar)
     return false;
 }
 return true;
}
І перевіряємо обидва підходи:
boolean isPalindrome = isPalindrome("Tenet");
System.out.println(isPalindrome);
Висновок у консолі:
true

119. Написати простий алгоритм сортування (Bubble, Selection чи Shuttle). Як його можна покращити?

Як просто алгоритм для реалізації я вибрав сортування вибором - Selection Sort:
public static void selectionSorting(int[] arr) {
 for (int i = 0; i < arr.length - 1; i++) {
   int min = i;
   for (int j = i + 1; j < arr.length; j++) {
     if (arr[j] < arr[min]) {
       min = j; // выбираем минимальный элемент в текущем числовом отрезке
     }
   }
   int temp = arr[min]; // меняем местами минимальный элемент с элементом под индексом i
   arr[min] = arr[i]; // так як отрезок постоянно уменьшается
   arr[i] = temp; // и выпадающие из него числа будут минимальными в текущем отрезке
 } // и як итог - числа оставшиеся вне текущей итерации отсортированы от самого наименьшего к большему
}
Покращений варіант виглядатиме так:
public static void improvedSelectionSorting(int[] arr) {
 for (int i = 0, j = arr.length - 1; i < j; i++, j--) { // рассматриваемый отрезок с каждой итерацией
   // будет уменьшаться с ДВУХ сторон по одному элементу
   int min = arr[i];
   int max = arr[i];
   int minIndex = i;
   int maxIndex = i;
   for (int n = i; n <= j; n++) { // выбираем min и max на текущем отрезке
     if (arr[n] > max) {
       max = arr[n];
       maxIndex = n;
     } else if (arr[n] < min) {
       min = arr[n];
       minIndex = n;
     }
   }
   // меняем найденный минимальный элемент с позиции с индексом min на позицию с индексом i
   swap(arr, i, minIndex);

   if (arr[minIndex] == max) {// срабатывает, если элемент max оказался смещен предыдущей перестановкой -
     swap(arr, j, minIndex); // на старое место min, поэтому с позиции с индексом min смещаем его на позицию j
   } else {
     swap(arr, j, maxIndex); // простое обмен местами элементов с индексами max и j
   }
 }
}

static int[] swap(int[] arr, int i, int j) {
 int temp = arr[i];
 arr[i] = arr[j];
 arr[j] = temp;
 return arr;
}
Ну а тепер нам потрібно переконатися, чи правда сортування покращало. Давайте порівняємо продуктивність:
long firstDifference = 0;
long secondDifference = 0;
long primaryTime;
int countOfApplying = 10000;
for (int i = 0; i < countOfApplying; i++) {
 int[] arr1 = {234, 33, 123, 4, 5342, 76, 3, 65,
     3, 5, 35, 75, 255, 4, 46, 48, 4658, 44, 22,
     678, 324, 66, 151, 268, 433, 76, 372, 45, 13,
     9484, 499959, 567, 774, 473, 3, 32, 865, 67, 43,
     63, 332, 24, 1};
 primaryTime = System.nanoTime();
 selectionSorting(arr1);
 firstDifference += System.nanoTime() - primaryTime;

 int[] arr2 = {234, 33, 123, 4, 5342, 76, 3, 65,
     3, 5, 35, 75, 255, 4, 46, 48, 4658, 44, 22,
     678, 324, 66, 151, 268, 433, 76, 372, 45, 13,
     9484, 499959, 567, 774, 473, 3, 32, 865, 67, 43,
     63, 332, 24, 1};
 primaryTime = System.nanoTime();
 improvedSelectionSorting(arr2);
 secondDifference += System.nanoTime() - primaryTime;
}

System.out.println(((double) firstDifference / (double) secondDifference - 1) * 100 + "%");
Обидві сортування запустабося у тому самому циклі, т.к. якби були окремі цикли, сортування з у коді вище показувало б гірший результат, ніж якщо її поставити другий. Це з тим, що програма хіба що “розігрівається” і далі працює трохи швидше. Але я трохи відійшов від теми. Після п'яти запусків цієї перевірки в консолі я побачив збільшення продуктивності на: 36.41006735635892% 51.46131097160771% 41.88918834013988% 48.09198070442 % Як на мене, це досить хороший результат. Розбір запитань та відповідей із співбесід на Java-розробника.  Частина 13 – 7

120. Напишіть алгоритм (послідовність дій) складання літералу типу int із літералом типу byte. Поясніть, що відбувається з пам'яттю

  1. byte значення наводиться до int. Для нього буде виділено не 1 байт пам'яті, а як і для всіх int значень - 4, якщо цього значення ще немає в int стеку. Якщо ж є, просто буде отримано посилання на нього.

  2. Два int значення будуть складені та вийде третє. Під нього виділиться нова ділянка пам'яті - 4 байти (або буде отримано посилання з int стека на існуюче значення).

    При цьому пам'ять двох int все ще буде зайнята, і їх значення зберігатимуться в int стеку відповідно.

Власне, на цьому й закінчуються питання рівня Junior із нашого списку. Починаючи з наступної статті ми розбиратимемося з питань рівня Middle. Зазначу, що питання Middle-рівня активно ставлять і розробникам початкового рівня – Junior. Тож слідкуйте за оновленнями. Ну, а на сьогодні все: до зустрічі!Розбір запитань та відповідей із співбесід на Java-розробника.  Частина 13 – 8
Інші матеріали серії:
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ