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

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

Статья из группы Random
Привет!
Движение к цели — это прежде всего движение.
Поэтому мало лишь думать о том, что ты хочешь чего-то достигнуть. Нужно что-то делать — даже самые маленькие шаги, — но делать их каждый день, и только так вы достигнете конечной цели. А так как вы здесь, чтобы стать 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.091980705743566% 37.120220461591444% Как по мне, это довольно-таки хороший результат. Разбор вопросов и ответов с собеседований на 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
Другие материалы серии:
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
the10or Уровень 35
8 августа 2022
117. Переверните массив Решение 1 не работает же. Arrays.asList() принимает массив интеджеров, а у нас массив интов. автоупаковка здесь не срабатывает, надо ручками.
hidden #2460969 Уровень 2
9 августа 2021
115. Move Zeroes (задача с Leetcode) Условие: В целочисленном массиве переместите все 0 в конец, сохраняя относительный порядок ненулевых элементов. 1. При массиве: [0,1,0,3,12] Вывод должен быть: [1,3,12,0,0]

public static void moveZeroes(int[] nums) {
    int[] sortedNums = new int[nums.length];
    int index = 0;
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] != 0) {
            sortedNums[index++] =  nums[i];
        }
    }
    System.arraycopy(sortedNums, 0, nums, 0, nums.length);
}
а что значит поверните? 116. Given List <String> names. Удалите первую букву из каждого имени и поверните отсортированный список