JavaRush /Blog Java /Random-PL /Analiza pytań i odpowiedzi z rozmów kwalifikacyjnych dla ...

Analiza pytań i odpowiedzi z rozmów kwalifikacyjnych dla programisty Java. Część 13

Opublikowano w grupie Random-PL
Cześć!
Dążenie do celu to przede wszystkim ruch.
Dlatego nie wystarczy tylko myśleć, że chcesz coś osiągnąć. Trzeba coś zrobić – nawet najmniejsze kroki – ale rób je codziennie, a tylko w ten sposób osiągniesz ostateczny cel. A ponieważ jesteś tutaj, aby zostać programistą Java, musisz każdego dnia zrobić przynajmniej minimalny krok w kierunku pogłębiania swojej wiedzy o Javie. Na dzisiejszy krok w Javie proponuję zapoznać się z nową częścią analizy najpopularniejszych pytań na rozmowach kwalifikacyjnych dla programistów. Analiza pytań i odpowiedzi z rozmów kwalifikacyjnych dla programisty Java.  Część 13 - 1Dziś zajmiemy się praktyczną częścią pytań dla młodszych specjalistów. Praktyczne zadanie podczas rozmowy kwalifikacyjnej nie jest rzadkością. Ważne jest, aby nie zgubić się w takiej sytuacji, zachować zimną krew i zaproponować optymalne rozwiązanie, a nawet kilka. Radziłbym również, aby nie milczeć podczas rozwiązywania problemu, ale skomentować swój tok myślenia i zapisać rozwiązanie, a po napisaniu wyjaśnić słowami, co zrobiłeś i dlaczego. Dzięki temu zjednasz sobie większą sympatię rozmówcy niż cicha decyzja. Więc zacznijmy!

111. Jak wymieniać dane pomiędzy wątkami?

Analiza pytań i odpowiedzi z rozmów kwalifikacyjnych dla programisty Java.  Część 13 - 2Do wymiany danych między wątkami można zastosować wiele różnych podejść i środków: na przykład użyć zmiennych atomowych, zsynchronizowanych kolekcji i semafora. Ale aby rozwiązać ten problem, podam przykład z Exchanger . Exchanger to klasa synchronizacji z pakietu concurrent , która ułatwia wymianę elementów pomiędzy parą wątków poprzez utworzenie wspólnego punktu synchronizacji. Jego użycie upraszcza wymianę danych pomiędzy dwoma wątkami. Zasada działania jest dość prosta: czeka, aż dwa osobne wątki wywołają metodę Exchange() . Tworzy się między nimi coś w rodzaju punktu wymiany: pierwsza nić stawia swój przedmiot i otrzymuje w zamian przedmiot drugiej, a ta z kolei otrzymuje przedmiot pierwszej i stawia swój. Oznacza to, że pierwszy wątek używa metody Exchange() i pozostaje bezczynny do czasu, aż inny wątek wywoła metodę Exchange() na tym samym obiekcie i nastąpi wymiana danych między nimi. Jako przykład rozważmy następującą implementację klasy 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();
     }
   }
 }
}
W konstruktorze wątku definiujemy obiekt Exchanger , który akceptuje obiekty typu String , a przy starcie (w metodzie uruchamiania ) używamy jego Exchange() do wymiany wiadomości z innym wątkiem, który używa tej metody w tym samym Exchangerze . Uruchommy to w trybie głównym :
Exchanger<String> exchanger = new Exchanger<>();
CustomThread first = new CustomThread("Первый ", exchanger);
first.setMessage("Сообщение первого потока");
CustomThread second = new CustomThread("Второй", exchanger);
second.setMessage("Сообщение второго потока");
first.start();
second.start();
W konsoli wyświetli się:
Pierwszy wątek otrzymał wiadomość: Wiadomość z drugiego wątku Drugi wątek otrzymał wiadomość: Wiadomość z pierwszego wątku Drugi wątek otrzymał wiadomość: Wiadomość z drugiego wątku Pierwszy wątek otrzymał wiadomość: Wiadomość z pierwszego wątku Drugi wątek wątek otrzymał wiadomość: Wiadomość pierwszego wątku Pierwszy wątek otrzymał wiadomość: Wiadomość drugiego wątku... .
Oznacza to, że wymiana danych pomiędzy wątkami przebiegła pomyślnie.

112. Jaka jest różnica pomiędzy klasą Thread a interfejsem Runnable?

Pierwszą rzeczą, na którą zauważę, jest to , że Thread jest klasą, Runnable jest interfejsem, co jest bardzo oczywistą różnicą =D Analiza pytań i odpowiedzi z rozmów kwalifikacyjnych dla programisty Java.  Część 13 - 3Powiem też, że Thread używa Runnable (kompozycji). Oznacza to, że mamy dwa sposoby:
  1. Dziedzicz z Thread , zastąp metodę run , następnie utwórz ten obiekt i rozpocznij wątek za pomocą metody start() .

  2. Zaimplementuj Runnable w określonej klasie, zaimplementuj jej metodę run() , a następnie utwórz obiekt Thread , przypisując tę ​​implementację obiektu interfejsu Runnable do jego konstruktora . Cóż, na koniec uruchom obiekt Thread za pomocą metody start() .

Co jest lepsze? Pomyślmy trochę:
  • Implementując interfejs Runnable , nie zmieniasz zachowania wątku. Zasadniczo po prostu dajesz wątkowi coś do uruchomienia. I taki jest nasz skład, co z kolei uważane jest za dobre podejście.

  • wdrożenie Runnable zapewnia większą elastyczność Twojej klasie. Jeśli odziedziczysz po Thread , wówczas akcja, którą wykonasz, będzie zawsze dotyczyć wątku. Ale jeśli zaimplementujesz Runnable, nie musi to być tylko wątek. W końcu możesz uruchomić go w wątku lub przekazać do jakiejś usługi wykonującej. No albo po prostu przekaż to gdzieś jako zadanie w aplikacji jednowątkowej.

  • Korzystanie z Runnable pozwala logicznie oddzielić wykonywanie zadań od logiki sterującej wątkami.

  • W Javie możliwe jest tylko pojedyncze dziedziczenie, więc można rozszerzyć tylko jedną klasę. Jednocześnie liczba rozszerzalnych interfejsów jest nieograniczona (no cóż, nie całkiem nieograniczona, ale 65535 , ale jest mało prawdopodobne, że kiedykolwiek przekroczysz ten limit).

Cóż, to od Ciebie zależy, czego dokładnie użyjesz ^^

113. Istnieją wątki T1, T2 i T3. Jak je wdrożyć po kolei?Analiza pytań i odpowiedzi z rozmów kwalifikacyjnych dla programisty Java.  Część 13 - 4

Pierwszą i najprostszą rzeczą, która przychodzi na myśl, jest użycie metody Join() . Zawiesza wykonywanie bieżącego wątku (zwanego metodą) do czasu, aż wątek, w którym wywołano metodę, zakończy wykonywanie. Stwórzmy własną implementację wątku:
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 + " - закончил свою работу");
 }
}
Rozpocznijmy jeden po drugim trzy takie wątki, używając metody 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();
Wyjście konsoli:
Wątek pierwszy - rozpoczął pracę Wątek pierwszy - zakończył pracę Wątek drugi - rozpoczął pracę Wątek drugi - zakończył pracę Wątek trzeci - rozpoczął pracę Wątek trzeci - zakończył pracę
Oznacza to, że wykonaliśmy nasze zadanie. Następnie przechodzimy bezpośrednio do zadań praktycznych na poziomie Junior .

Zadania praktyczne

114. Suma przekątna macierzy (problem Leetcode)

Warunek: Oblicz sumę wszystkich elementów na głównej przekątnej i wszystkich elementów na dodatkowej przekątnej, które nie są częścią głównej przekątnej. Analiza pytań i odpowiedzi z rozmów kwalifikacyjnych dla programisty Java.  Część 13 - 51. Przy macierzy w postaci: mat = [[1,2,3], [4,5,6], [7,8,9]] Na wyjściu powinno być - 25 2. Przy macierzy - mat = [[1,1 ,1,1], [1,1,1,1], [1,1,1,1], [1,1,1,1]] Wynik powinien wynosić - 8 3. Z macierz - mat = [[ 5]] Wniosek powinien być taki - 5 Przerwij czytanie i zrealizuj swoją decyzję. Moje rozwiązanie byłoby następujące:
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;
}
Wszystko dzieje się przy jednym przejściu przez tablicę, podczas którego mamy do dyspozycji dwa indeksy raportu: i - do raportowania wierszy tablicy i kolumn przekątnej głównej, j - do raportowania kolumn dodatkowej przekątnej. Jeśli komórka głównej przekątnej i dodatkowej pokrywają się, wówczas jedna z wartości jest ignorowana przy obliczaniu sumy. Sprawdźmy korzystając z macierzy z warunku:
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));
Wyjście konsoli:
25 8 5

115. Przesuń zera (wyzwanie Leetcode)

Warunek: w tablicy liczb całkowitych przesuń wszystkie zera na koniec, zachowując względną kolejność elementów niezerowych. 1. Z tablicą: [0,1,0,3,12] Wynikiem powinno być: [1,3,12,0,0] 2. Z tablicą: [0] Wynikiem powinno być: [0] Zatrzymaj się i zapisz moją decyzję... Moja decyzja:
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;// заполняем последние элементы массива нулями согласно счётчику нулей
 }
}
Badanie:
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));
Wyjście konsoli:
[1, 2, 12, 9, 0, 0] [0]

116. Lista nazw <String>. Usuń pierwszą literę z każdego imienia i obróć posortowaną listę

1. Pierwsze co przychodzi na myśl to metody klasy Collections , która zawiera wiele metod pomocniczych dla kolekcji:
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. Ponadto, jeśli używamy Java w wersji 8 i wyższej, musimy po prostu pokazać rozwiązanie za pomocą strumieni:
public static List<String> processTheList(List<String> nameList) {
 return nameList.stream()
     .map(x -> x.substring(1))
     .sorted().collect(Collectors.toList());
}
Niezależnie od wybranego rozwiązania sprawdzenie może wyglądać następująco:
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));
Wyjście konsoli:
[avid, eter, gor, mitriy, nna, ob, ohn]

117. Odwróć tablicę

Rozwiązanie 1 Ponownie pierwszą rzeczą, która przychodzi na myśl, jest użycie metod pomocniczej klasy użytkowej Collections . Ale ponieważ mamy tablicę, musimy najpierw przekonwertować ją na kolekcję (listę):
public static Integer[] reverse(Integer[] arr) {
 List<Integer> list = Arrays.asList(arr);
 Collections.reverse(list);
 return list.toArray(arr);
}
Rozwiązanie 2 Ponieważ pytanie dotyczyło tablicy, uważam, że należy pokazać rozwiązanie bez korzystania z gotowych funkcjonalności, czyli że tak powiem, według klasyków:
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;
}
Badanie:
Integer[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9};
System.out.println(Arrays.toString(reverse(arr)));
Wyjście konsoli:
[9, 8, 7, 6, 5, 4, 3, 2, 1]

118. Sprawdź, czy ciąg znaków jest palindromem

Analiza pytań i odpowiedzi z rozmów kwalifikacyjnych dla programisty Java.  Część 13 - 6Rozwiązanie 1 Warto od razu zapamiętać StringBuilder : jest bardziej elastyczny i bogaty w różne metody w porównaniu do zwykłego String . Nas szczególnie interesuje metoda odwrotna :
public static boolean isPalindrome(String string) {
 string = string.toLowerCase(); //приводит всю строку к нижнему регистру
 StringBuilder builder = new StringBuilder();
 builder.append(string);
 builder.reverse(); // перевочиваем строку методом Builder-а
 return (builder.toString()).equals(string);
}
Rozwiązanie: Następnym podejściem będzie bez wykorzystywania „luk” od razu po wyjęciu z pudełka. Porównujemy znaki z tyłu ciągu z odpowiednimi znakami z przodu:
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;
}
Sprawdzamy oba podejścia:
boolean isPalindrome = isPalindrome("Tenet");
System.out.println(isPalindrome);
Wyjście konsoli:
PRAWDA

119. Napisz prosty algorytm sortowania (bąbelkowy, selekcjonujący lub wahadłowy). Jak można to poprawić?

Jako prosty algorytm do wdrożenia wybrałem sortowanie przez wybór - Sortowanie przez wybór:
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]; // так Jak отрезок постоянно уменьшается
   arr[i] = temp; // и выпадающие из него числа будут минимальными в текущем отрезке
 } // и Jak итог - числа оставшиеся вне текущей итерации отсортированы от самого наименьшего к большему
}
Ulepszona wersja wyglądałaby tak:
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;
}
Cóż, teraz musimy się upewnić, czy sortowanie rzeczywiście się poprawiło. Porównajmy wydajność:
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 + "%");
Oba rodzaje rozpoczęły się w tym samym cyklu, ponieważ gdyby istniały oddzielne pętle, sortowanie z powyższego kodu dałoby gorszy wynik, niż gdyby zostało umieszczone na drugim miejscu. Dzieje się tak dlatego, że program „rozgrzewa się” i wtedy działa nieco szybciej. Ale odbiegnę trochę od tematu. Po pięciu uruchomieniach tego sprawdzania w konsoli zaobserwowałem wzrost wydajności o: 36,41006735635892% 51,46131097160771% 41,88918834013988% 48,091980705743566% 37,120220461591444% Jak dla mnie to całkiem nieźle wynik. Analiza pytań i odpowiedzi z rozmów kwalifikacyjnych dla programisty Java.  Część 13 - 7

120. Napisz algorytm (sekwencję działań) składający się z literału typu int z literałem typu bajt. Wyjaśnij, co dzieje się z pamięcią

  1. wartość bajtu jest konwertowana na int. Zostanie mu przydzielony nie 1 bajt pamięci, ale jak wszystkie wartości int - 4, jeśli tej wartości nie ma jeszcze na stosie int. Jeśli tak, link do niego zostanie po prostu otrzymany.

  2. Zostaną dodane dwie wartości int i uzyskana zostanie trzecia. Zostanie mu przydzielona nowa sekcja pamięci - 4 bajty (lub zostanie odebrane odwołanie ze stosu int do istniejącej wartości).

    W takim przypadku pamięć dwóch int będzie nadal zajęta, a ich wartości zostaną zapisane odpowiednio na stosie int.

Właściwie na tym kończą się pytania na poziomie Junior z naszej listy. Począwszy od następnego artykułu, zrozumiemy zagadnienia na poziomie średnim. Chciałbym zauważyć, że pytania na poziomie średnim są również aktywnie zadawane programistom na poziomie podstawowym - Junior. Bądźcie na bieżąco. Cóż, to wszystko na dziś: do zobaczenia!Analiza pytań i odpowiedzi z rozmów kwalifikacyjnych dla programisty Java.  Część 13 - 8
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION