Сьогодні ми пройдемося з практичної частини питань для Junior-фахівців. Практичне завдання на співбесіді – не рідкість. Важливо не губитися в такій ситуації, постаратися зберегти холодну голову та запропонувати оптимальне рішення, а то й дещо. Також я б порекомендував не мовчати при вирішенні завдання, а коментувати хід своїх думок та написання рішення, ну або після написання пояснити на словах, що й навіщо ви зробабо. Це набагато більше розташує інтерв'юера до вас, ніж мовчазне рішення. Отже, почнемо!
111. Як між потоками обмінюватись даними?
Для обміну даними між потоками можна використовувати багато різних підходів та засобів: наприклад, скористатися атомарними змінними, синхронізованими колекціями, семафором. Але для вирішення цього завдання я наведу приклад з 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
Також скажу, що Thread використовує Runnable (композиція). Тобто у нас є два шляхи:
-
Наслідувати Thread , перевизначити метод run, після чого створити даний об'єкт і запустити потік через метод start() .
-
Реалізувати 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(); Висновок у консолі:
Практичні завдання
114. Matrix Diagonal Sum (завдання з Leetcode)
Умова: Підрахуйте суму всіх елементів на основній діагоналі та всіх елементів на додатковій діагоналі, які не є частиною основної діагоналі.
1. При матриці виду: 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)); Висновок у консолі:
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)); Виведення в консоль:
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)); Висновок у консолі:
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))); Висновок у консолі:
118. Перевірити, чи є рядок паліндромом
Рішення 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); Висновок у консолі:
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 % Як на мене, це досить хороший результат.
120. Напишіть алгоритм (послідовність дій) складання літералу типу int із літералом типу byte. Поясніть, що відбувається з пам'яттю
-
byte значення наводиться до int. Для нього буде виділено не 1 байт пам'яті, а як і для всіх int значень - 4, якщо цього значення ще немає в int стеку. Якщо ж є, просто буде отримано посилання на нього.
-
Два int значення будуть складені та вийде третє. Під нього виділиться нова ділянка пам'яті - 4 байти (або буде отримано посилання з int стека на існуюче значення).
При цьому пам'ять двох int все ще буде зайнята, і їх значення зберігатимуться в int стеку відповідно.

ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ