JavaRush /Blog Java /Random-VI /Phân tích các câu hỏi và câu trả lời từ các cuộc phỏng vấ...

Phân tích các câu hỏi và câu trả lời từ các cuộc phỏng vấn dành cho nhà phát triển Java. Phần 13

Xuất bản trong nhóm
Xin chào!
Chuyển động hướng tới mục tiêu trước hết là chuyển động.
Vì vậy, chỉ nghĩ rằng bạn muốn đạt được điều gì đó là chưa đủ. Bạn cần phải làm điều gì đó - ngay cả những bước nhỏ nhất - nhưng hãy thực hiện chúng hàng ngày và chỉ bằng cách này, bạn mới đạt được mục tiêu cuối cùng. Và vì bạn ở đây để trở thành nhà phát triển Java, nên bạn cần thực hiện ít nhất một bước tối thiểu để nâng cao kiến ​​thức về Java của mình mỗi ngày. Đối với bước Java hôm nay, tôi khuyên bạn nên tự làm quen với phần mới của việc phân tích các câu hỏi phỏng vấn phổ biến nhất dành cho nhà phát triển. Phân tích các câu hỏi và câu trả lời từ các cuộc phỏng vấn dành cho nhà phát triển Java.  Phần 13 - 1Hôm nay chúng ta sẽ đi qua phần thực hành của các câu hỏi dành cho chuyên gia Junior. Một nhiệm vụ thực tế tại một cuộc phỏng vấn không phải là hiếm. Điều quan trọng là không được lạc lối trong tình huống như vậy, hãy cố gắng giữ một cái đầu lạnh và đưa ra giải pháp tối ưu, hoặc thậm chí là một số giải pháp. Tôi cũng khuyên bạn không nên giữ im lặng khi giải quyết vấn đề mà hãy bình luận về dòng suy nghĩ của bạn và viết ra giải pháp, hoặc sau khi viết, hãy giải thích bằng lời những gì bạn đã làm và tại sao. Điều này sẽ khiến bạn được người phỏng vấn quý mến hơn là một quyết định im lặng. Vậy hãy bắt đầu!

111. Làm thế nào để trao đổi dữ liệu giữa các luồng?

Phân tích các câu hỏi và câu trả lời từ các cuộc phỏng vấn dành cho nhà phát triển Java.  Phần 13 - 2Để trao đổi dữ liệu giữa các luồng, bạn có thể sử dụng nhiều cách tiếp cận và phương tiện khác nhau: ví dụ: sử dụng các biến nguyên tử, bộ sưu tập được đồng bộ hóa và semaphore. Nhưng để giải quyết vấn đề này mình sẽ đưa ra ví dụ với Exchanger . Exchanger là một lớp đồng bộ hóa từ gói đồng thời tạo điều kiện thuận lợi cho việc trao đổi các phần tử giữa một cặp luồng bằng cách tạo một điểm đồng bộ hóa chung. Việc sử dụng nó giúp đơn giản hóa việc trao đổi dữ liệu giữa hai luồng. Cách thức hoạt động của nó khá đơn giản: nó đợi hai luồng riêng biệt gọi phương thức Exchange() của nó . Một cái gì đó giống như một điểm trao đổi được tạo ra giữa chúng: luồng đầu tiên đặt đối tượng của nó và nhận lại đối tượng của đối tượng kia, và chuỗi sau lần lượt nhận đối tượng của đối tượng đầu tiên và đặt đối tượng của mình. Nghĩa là, luồng đầu tiên sử dụng phương thức Exchange() và không hoạt động cho đến khi một luồng khác gọi phương thức Exchange() trên cùng một đối tượng và dữ liệu được trao đổi giữa chúng. Ví dụ, hãy xem xét việc triển khai lớp Thread sau đây :
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();
     }
   }
 }
}
Trong hàm tạo luồng, chúng ta xác định một đối tượng Exchanger chấp nhận các đối tượng thuộc loại String và khi khởi động (trong phương thức chạy ), chúng ta sử dụng Exchange() của nó để trao đổi thông báo với một luồng khác sử dụng phương thức này trong cùng Exchanger . Hãy chạy nó trong main :
Exchanger<String> exchanger = new Exchanger<>();
CustomThread first = new CustomThread("Первый ", exchanger);
first.setMessage("Сообщение первого потока");
CustomThread second = new CustomThread("Второй", exchanger);
second.setMessage("Сообщение второго потока");
first.start();
second.start();
Bảng điều khiển sẽ hiển thị:
Luồng đầu tiên đã nhận được tin nhắn: Tin nhắn của luồng thứ hai Luồng thứ hai đã nhận được tin nhắn: Tin nhắn của luồng đầu tiên Luồng thứ hai đã nhận được tin nhắn: Tin nhắn của luồng thứ hai Luồng đầu tiên đã nhận được tin nhắn: Tin nhắn của luồng đầu tiên Luồng thứ hai thread đã nhận được tin nhắn: Tin nhắn của thread đầu tiên Thread đầu tiên đã nhận được tin nhắn: Tin nhắn của thread thứ hai... .
Điều này có nghĩa là việc trao đổi dữ liệu giữa các luồng thành công.

112. Sự khác biệt giữa lớp Thread và giao diện Runnable là gì?

Điều đầu tiên tôi sẽ lưu ý là Thread là một lớp, Runnable là một giao diện, đây là một sự khác biệt rất rõ ràng =D Phân tích các câu hỏi và câu trả lời từ các cuộc phỏng vấn dành cho nhà phát triển Java.  Phần 13 - 3Tôi cũng sẽ nói rằng Thread sử dụng Runnable (composition). Tức là chúng ta có hai cách:
  1. Kế thừa từ Thread , ghi đè phương thức run, sau đó tạo đối tượng này và bắt đầu luồng thông qua phương thức start() .

  2. Triển khai Runnable trong một lớp nhất định, triển khai phương thức run() của nó , sau đó tạo một đối tượng Thread , gán việc triển khai đối tượng này của giao diện Runnable cho hàm tạo của nó . Chà, cuối cùng, hãy khởi chạy đối tượng Thread bằng phương thức start() .

Điều gì là tốt hơn? Chúng ta hãy suy nghĩ một chút:
  • Khi bạn triển khai giao diện Runnable , bạn không thay đổi hành vi của luồng. Về cơ bản, bạn chỉ đang cung cấp cho luồng thứ gì đó để chạy. Và đây là thành phần của chúng tôi, do đó được coi là một cách tiếp cận tốt.

  • việc triển khai Runnable mang lại sự linh hoạt hơn cho lớp của bạn. Nếu bạn kế thừa từ Thread thì hành động bạn thực hiện sẽ luôn nằm trên luồng. Nhưng nếu bạn triển khai Runnable thì nó không nhất thiết chỉ là một luồng. Rốt cuộc, bạn có thể chạy nó trong một luồng hoặc chuyển nó đến một số dịch vụ thực thi. Chà, hoặc chỉ chuyển nó đến đâu đó dưới dạng một tác vụ trong một ứng dụng đơn luồng.

  • Việc sử dụng Runnable cho phép bạn tách biệt một cách logic việc thực thi tác vụ khỏi logic điều khiển luồng.

  • Trong Java, chỉ có thể kế thừa một lần, do đó chỉ có thể mở rộng một lớp. Đồng thời, số lượng giao diện có thể mở rộng là không giới hạn (tốt, không hoàn toàn không giới hạn, nhưng 65535 , nhưng bạn khó có thể đạt đến giới hạn này).

Chà, tùy bạn quyết định chính xác nên sử dụng cái gì ^^

113. Có chủ đề T1, T2 và T3. Làm thế nào để thực hiện chúng một cách tuần tự?Phân tích các câu hỏi và câu trả lời từ các cuộc phỏng vấn dành cho nhà phát triển Java.  Phần 13 - 4

Điều đầu tiên và đơn giản nhất bạn nghĩ đến là sử dụng phương thức join() . Nó tạm dừng việc thực thi luồng hiện tại (được gọi là phương thức) cho đến khi luồng mà phương thức được gọi kết thúc việc thực thi. Hãy tạo triển khai chủ đề của riêng chúng ta:
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 + " - закончил свою работу");
 }
}
Hãy bắt đầu lần lượt ba chủ đề như vậy bằng cách sử dụng 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();
Đầu ra của bảng điều khiển:
Luồng đầu tiên - bắt đầu công việc Luồng đầu tiên - kết thúc công việc Luồng thứ hai - bắt đầu công việc Luồng thứ hai - kết thúc công việc Luồng thứ ba - bắt đầu công việc Luồng thứ ba - kết thúc công việc
Điều này có nghĩa là chúng ta đã hoàn thành nhiệm vụ của mình. Tiếp theo, chúng ta chuyển thẳng sang các nhiệm vụ thực tế ở cấp Junior .

Nhiệm vụ thực tế

114. Ma trận tổng chéo (vấn đề Leetcode)

Điều kiện: Tính tổng tất cả các phần tử trên đường chéo chính và tất cả các phần tử trên đường chéo phụ không thuộc đường chéo chính. Phân tích các câu hỏi và câu trả lời từ các cuộc phỏng vấn dành cho nhà phát triển Java.  Phần 13 - 51. Với ma trận có dạng: mat = [[1,2,3], [4,5,6], [7,8,9]] Kết quả đầu ra phải là - 25 2. Với ma trận - mat = [[1,1 ,1,1], [1,1,1,1], [1,1,1,1], [1,1,1,1]] Đầu ra phải là - 8 3. Với a ma trận - mat = [[ 5]] Kết luận phải là - 5 Tạm dừng đọc và thực hiện quyết định của bạn. Giải pháp của tôi sẽ như sau:
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;
}
Mọi thứ xảy ra chỉ với một lần duyệt qua mảng, trong đó chúng ta có hai chỉ mục cho báo cáo: i - để báo cáo các hàng của mảng và các cột của đường chéo chính, j - để báo cáo các cột của đường chéo bổ sung. Nếu ô của đường chéo chính và ô bổ sung trùng nhau thì một trong các giá trị sẽ bị bỏ qua khi tính tổng. Hãy kiểm tra bằng cách sử dụng ma trận từ điều kiện:
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));
Đầu ra của bảng điều khiển:
25 8 5

115. Di chuyển số 0 (Thử thách Leetcode)

Điều kiện: Trong một mảng số nguyên, di chuyển tất cả các số 0 về cuối, duy trì thứ tự tương đối của các phần tử khác 0. 1. Với một mảng: [0,1,0,3,12] Đầu ra phải là: [1,3,12,0,0] 2. Với một mảng: [0] Đầu ra phải là: [0] Tạm dừng và viết quyết định của tôi... Quyết định của tôi:
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;// заполняем последние элементы массива нулями согласно счётчику нулей
 }
}
Bài kiểm tra:
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));
Đầu ra của bảng điều khiển:
[1, 2, 12, 9, 0, 0] [0]

116. Danh sách đã cho tên <String>. Xóa chữ cái đầu tiên của mỗi tên và xoay danh sách đã sắp xếp

1. Điều đầu tiên bạn nghĩ đến là các phương thức của lớp Collections , chứa nhiều phương thức phụ trợ cho các bộ sưu tập:
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. Ngoài ra, nếu chúng tôi sử dụng Java phiên bản 8 trở lên, chúng tôi chỉ cần hiển thị giải pháp qua các luồng:
public static List<String> processTheList(List<String> nameList) {
 return nameList.stream()
     .map(x -> x.substring(1))
     .sorted().collect(Collectors.toList());
}
Bất kể giải pháp được chọn là gì, việc kiểm tra có thể như sau:
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));
Đầu ra của bảng điều khiển:
[ khao khát, eter, gor, mitriy, nna, ob, ồ]

117. Lật mảng

Giải pháp 1 Một lần nữa, điều đầu tiên bạn nghĩ đến là sử dụng các phương thức của lớp tiện ích phụ trợ Collections . Nhưng vì chúng ta có một mảng nên trước tiên chúng ta cần chuyển đổi nó thành một bộ sưu tập (danh sách):
public static Integer[] reverse(Integer[] arr) {
 List<Integer> list = Arrays.asList(arr);
 Collections.reverse(list);
 return list.toArray(arr);
}
Giải pháp 2 Vì câu hỏi là về một mảng, tôi nghĩ cần phải hiển thị giải pháp mà không sử dụng chức năng có sẵn, và có thể nói, theo kinh điển:
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;
}
Bài kiểm tra:
Integer[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9};
System.out.println(Arrays.toString(reverse(arr)));
Đầu ra của bảng điều khiển:
[9, 8, 7, 6, 5, 4, 3, 2, 1]

118. Kiểm tra xem một chuỗi có phải là một chuỗi palindrome không

Phân tích các câu hỏi và câu trả lời từ các cuộc phỏng vấn dành cho nhà phát triển Java.  Phần 13 - 6Giải pháp 1 Cần phải nhớ ngay StringBuilder : nó linh hoạt hơn và có nhiều phương thức khác nhau so với String thông thường . Chúng tôi đặc biệt quan tâm đến phương pháp đảo ngược :
public static boolean isPalindrome(String string) {
 string = string.toLowerCase(); //приводит всю строку к нижнему регистру
 StringBuilder builder = new StringBuilder();
 builder.append(string);
 builder.reverse(); // перевочиваем строку методом Builder-а
 return (builder.toString()).equals(string);
}
Giải pháp: Cách tiếp cận tiếp theo sẽ là không sử dụng những “sơ hở” sẵn có. Chúng ta so sánh các ký tự ở phía sau chuỗi với các ký tự tương ứng ở phía trước:
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;
}
Và chúng tôi kiểm tra cả hai cách tiếp cận:
boolean isPalindrome = isPalindrome("Tenet");
System.out.println(isPalindrome);
Đầu ra của bảng điều khiển:
ĐÚNG VẬY

119. Viết thuật toán sắp xếp đơn giản (Bubble, Selection hoặc Shuttle). Làm thế nào nó có thể được cải thiện?

Là một thuật toán đơn giản để thực hiện, tôi đã chọn sắp xếp lựa chọn - Sắp xếp lựa chọn:
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]; // так How отрезок постоянно уменьшается
   arr[i] = temp; // и выпадающие из него числа будут минимальными в текущем отрезке
 } // и How итог - числа оставшиеся вне текущей итерации отсортированы от самого наименьшего к большему
}
Phiên bản cải tiến sẽ trông như thế này:
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;
}
Bây giờ chúng ta cần đảm bảo rằng việc sắp xếp đã thực sự được cải thiện hay chưa. Hãy so sánh hiệu suất:
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 + "%");
Cả hai loại đều bắt đầu trong cùng một chu kỳ, bởi vì nếu có các vòng lặp riêng biệt, việc sắp xếp từ mã ở trên sẽ cho kết quả kém hơn so với việc nó được đặt ở vị trí thứ hai. Điều này là do chương trình "khởi động" và sau đó hoạt động nhanh hơn một chút. Nhưng tôi đang đi hơi xa chủ đề. Sau năm lần kiểm tra này trong bảng điều khiển, tôi thấy hiệu suất tăng lên: 36.41006735635892% 51.46131097160771% 41.88918834013988% 48.091980705743566% 37.120220461591444% Đối với tôi, đây là một điều khá tốt kết quả. Phân tích các câu hỏi và câu trả lời từ các cuộc phỏng vấn dành cho nhà phát triển Java.  Phần 13 - 7

120. Viết một thuật toán (chuỗi các hành động) để soạn một chữ kiểu int với một chữ kiểu byte. Giải thích điều gì xảy ra với trí nhớ

  1. giá trị byte được chuyển đổi thành int. Không phải 1 byte bộ nhớ sẽ được phân bổ cho nó, nhưng giống như tất cả các giá trị int - 4, nếu giá trị này chưa có trong ngăn xếp int. Nếu có, một liên kết đến nó sẽ được nhận.

  2. Hai giá trị int sẽ được thêm vào và giá trị thứ ba sẽ được lấy. Một phần bộ nhớ mới sẽ được phân bổ cho nó - 4 byte (hoặc một tham chiếu sẽ được nhận từ ngăn xếp int đến giá trị hiện có).

    Trong trường hợp này, bộ nhớ của hai int vẫn sẽ bị chiếm dụng và giá trị của chúng sẽ được lưu tương ứng trên ngăn xếp int.

Trên thực tế, đây là nơi kết thúc các câu hỏi dành cho cấp độ Junior trong danh sách của chúng tôi. Bắt đầu từ bài viết tiếp theo chúng ta sẽ hiểu các vấn đề về cấp độ Trung cấp. Tôi muốn lưu ý rằng các câu hỏi cấp độ Trung cấp cũng được tích cực hỏi đối với các nhà phát triển trình độ đầu vào - Junior. Vậy nên hãy chờ trong giây lát. Vâng, đó là tất cả cho ngày hôm nay: hẹn gặp lại!Phân tích các câu hỏi và câu trả lời từ các cuộc phỏng vấn dành cho nhà phát triển Java.  Phần 13 - 8
Các tài liệu khác trong loạt bài:
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION