JavaRush /Blog Java /Random-VI /Lõi Java. Câu hỏi phỏng vấn phần 2
Andrey
Mức độ

Lõi Java. Câu hỏi phỏng vấn phần 2

Xuất bản trong nhóm
Đối với những người lần đầu tiên nghe đến từ Java Core, đây là những nền tảng cơ bản của ngôn ngữ. Với kiến ​​thức này, bạn có thể yên tâm đi thực tập/thực tập.
Lõi Java.  Câu hỏi phỏng vấn phần 2 - 1
Những câu hỏi này sẽ giúp bạn ôn lại kiến ​​thức trước buổi phỏng vấn, hoặc học hỏi điều gì đó mới mẻ cho bản thân. Để có được kỹ năng thực tế, hãy học tại JavaRush . Bài viết gốc Link tới các phần khác: Java Core. Câu hỏi phỏng vấn, phần 1 Java Core. Những câu hỏi phỏng vấn phần 3

Tại sao nên tránh phương thức Finalize()?

Tất cả chúng ta đều biết tuyên bố rằng một phương thức finalize()được gọi bởi trình thu gom rác trước khi giải phóng bộ nhớ bị chiếm giữ bởi một đối tượng. Đây là một chương trình ví dụ chứng minh rằng lệnh gọi phương thức finalize()không được đảm bảo:
public class TryCatchFinallyTest implements Runnable {

	private void testMethod() throws InterruptedException
	{
		try
		{
			System.out.println("In try block");
			throw new NullPointerException();
		}
		catch(NullPointerException npe)
		{
			System.out.println("In catch block");
		}
		finally
		{
			System.out.println("In finally block");
		}
	}

	@Override
	protected void finalize() throws Throwable {
		System.out.println("In finalize block");
		super.finalize();
	}

	@Override
	public void run() {
		try {
			testMethod();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
public class TestMain
{
	@SuppressWarnings("deprecation")
	public static void main(String[] args) {
	for(int i=1;i< =3;i++)
	{
		new Thread(new TryCatchFinallyTest()).start();
	}
	}
}
Đầu ra: Trong khối thử Trong khối bắt Trong khối cuối cùng Trong khối thử Trong khối bắt Trong khối cuối cùng Trong khối thử Trong khối bắt Trong khối cuối cùng Đáng ngạc nhiên là phương thức này finalizekhông được thực thi cho bất kỳ luồng nào. Điều này chứng minh lời nói của tôi. Tôi nghĩ lý do là các trình hoàn thiện được thực thi bởi một luồng thu gom rác riêng biệt. Nếu Máy ảo Java kết thúc quá sớm thì trình thu gom rác sẽ không có đủ thời gian để tạo và thực thi các trình hoàn thiện. Các lý do khác để không sử dụng phương pháp này finalize()có thể là:
  1. Phương thức này finalize()không hoạt động với các chuỗi như hàm tạo. Điều này có nghĩa là khi bạn gọi hàm tạo của lớp, hàm tạo của lớp cha sẽ được gọi vô điều kiện. Nhưng trong trường hợp của phương pháp này finalize(), điều này sẽ không xảy ra. Phương thức siêu lớp finalize()phải được gọi một cách rõ ràng.
  2. Bất kỳ ngoại lệ nào được phương thức đưa ra finalizeđều bị luồng thu gom rác bỏ qua và sẽ không được truyền bá thêm, điều đó có nghĩa là sự kiện sẽ không được ghi lại trong nhật ký của bạn. Điều này rất tệ phải không?
  3. Bạn cũng sẽ bị phạt hiệu suất đáng kể nếu phương thức này finalize()có trong lớp của bạn. Trong Lập trình hiệu quả (tái bản lần thứ 2), Joshua Bloch đã nói:
    “Đúng, và một điều nữa: sẽ có một hình phạt lớn về hiệu suất khi sử dụng các công cụ hoàn thiện. Trên máy của tôi, thời gian để tạo và hủy các đối tượng đơn giản là khoảng 5,6 nano giây.
    Việc thêm bộ hoàn thiện sẽ tăng thời gian lên 2400 nano giây. Nói cách khác, việc tạo và xóa một đối tượng bằng bộ hoàn thiện sẽ chậm hơn khoảng 430 lần.”

Tại sao HashMap không nên được sử dụng trong môi trường đa luồng? Điều này có thể gây ra một vòng lặp vô hạn?

Chúng tôi biết rằng HashMapđây là bộ sưu tập không được đồng bộ hóa, bản sao được đồng bộ hóa của nó là HashTable. Vì vậy, khi bạn đang truy cập một bộ sưu tập và trong môi trường đa luồng nơi tất cả các luồng đều có quyền truy cập vào một phiên bản duy nhất của bộ sưu tập thì sẽ an toàn hơn khi sử dụng HashTablevì những lý do rõ ràng, chẳng hạn như tránh đọc sai và đảm bảo tính nhất quán của dữ liệu. Trong trường hợp xấu nhất, môi trường đa luồng này sẽ gây ra vòng lặp vô hạn. Vâng đúng vậy. HashMap.get()có thể gây ra một vòng lặp vô hạn. Hãy xem làm thế nào? Nếu bạn nhìn vào mã nguồn của phương thức HashMap.get(Object key), nó trông như thế này:
public Object get(Object key) {
    Object k = maskNull(key);
    int hash = hash(k);
    int i = indexFor(hash, table.length);
    Entry e = table[i];
    while (true) {
        if (e == null)
            return e;
        if (e.hash == hash && eq(k, e.key))
            return e.value;
        e = e.next;
    }
}
while(true)luôn có thể trở thành nạn nhân của vòng lặp vô hạn trong môi trường thời gian chạy đa luồng nếu vì lý do nào đó e.nextnó có thể trỏ đến chính nó. Điều này sẽ gây ra một vòng lặp vô tận, nhưng e.nextnó sẽ trỏ đến chính nó như thế nào (tức là tới e)? Điều này có thể xảy ra trong một phương thức void transfer(Entry[] newTable)được gọi trong khi nó HashMapđang được thay đổi kích thước.
do {
    Entry next = e.next;
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);
Đoạn mã này có xu hướng tạo ra một vòng lặp vô hạn nếu việc thay đổi kích thước xảy ra cùng lúc với một luồng khác đang cố gắng thay đổi phiên bản bản đồ ( HashMap). Cách duy nhất để tránh trường hợp này là sử dụng đồng bộ hóa trong mã của bạn hoặc tốt hơn là sử dụng bộ sưu tập được đồng bộ hóa.

Giải thích sự trừu tượng và đóng gói. Chúng được kết nối như thế nào?

Nói một cách đơn giản , “ Tính trừu tượng chỉ hiển thị những thuộc tính quan trọng của một đối tượng đối với chế độ xem hiện tại ” . Trong lý thuyết lập trình hướng đối tượng, tính trừu tượng bao gồm khả năng xác định các đối tượng đại diện cho các “tác nhân” trừu tượng có thể thực hiện công việc, thay đổi và báo cáo các thay đổi về trạng thái của chúng cũng như “tương tác” với các đối tượng khác trong hệ thống. Tính trừu tượng trong bất kỳ ngôn ngữ lập trình nào đều hoạt động theo nhiều cách. Điều này có thể được nhìn thấy từ việc tạo ra các quy trình để xác định giao diện cho các lệnh ngôn ngữ cấp thấp. Một số khái niệm trừu tượng cố gắng hạn chế phạm vi trình bày tổng thể về nhu cầu của người lập trình bằng cách ẩn hoàn toàn các khái niệm trừu tượng mà chúng được xây dựng trên đó, chẳng hạn như các mẫu thiết kế. Thông thường, sự trừu tượng hóa có thể được nhìn nhận theo hai cách: Trừu tượng hóa dữ liệu là cách tạo ra các kiểu dữ liệu phức tạp và chỉ hiển thị các hoạt động có ý nghĩa để tương tác với mô hình dữ liệu, đồng thời ẩn tất cả các chi tiết triển khai với thế giới bên ngoài. Sự trừu tượng hóa việc thực thi là quá trình xác định tất cả các câu lệnh quan trọng và hiển thị chúng dưới dạng một đơn vị công việc. Chúng ta thường sử dụng tính năng này khi tạo một phương thức để thực hiện một số công việc. Việc giới hạn dữ liệu và phương thức trong các lớp kết hợp với việc thực hiện ẩn (sử dụng kiểm soát truy cập) thường được gọi là đóng gói. Kết quả là một kiểu dữ liệu có đặc điểm và hành vi. Đóng gói về cơ bản cũng liên quan đến việc ẩn dữ liệu và ẩn triển khai. "Gói gọn mọi thứ có thể thay đổi" . Câu trích dẫn này là một nguyên tắc thiết kế nổi tiếng. Đối với vấn đề đó, trong bất kỳ lớp nào, những thay đổi về dữ liệu có thể xảy ra trong thời gian chạy và những thay đổi về cách triển khai có thể xảy ra trong các phiên bản sau này. Do đó, việc đóng gói áp dụng cho cả dữ liệu và việc triển khai. Vì vậy, chúng có thể được kết nối như thế này:
  • Trừu tượng hóa chủ yếu là những gì một lớp có thể làm [Ý tưởng]
  • Đóng gói nhiều hơn Cách đạt được chức năng này [Triển khai]

Sự khác biệt giữa giao diện và lớp trừu tượng?

Sự khác biệt chính có thể được liệt kê như sau:
  • Một giao diện không thể thực hiện bất kỳ phương thức nào, nhưng một lớp trừu tượng thì có thể.
  • Một lớp có thể triển khai nhiều giao diện nhưng chỉ có thể có một siêu lớp (trừu tượng hoặc không trừu tượng)
  • Giao diện không phải là một phần của hệ thống phân cấp lớp. Các lớp không liên quan có thể thực hiện cùng một giao diện.
Điều bạn phải nhớ là: “Khi bạn có thể mô tả đầy đủ một khái niệm dưới dạng “nó làm gì” mà không cần phải chỉ rõ “nó thực hiện nó như thế nào”, thì bạn nên sử dụng một giao diện. Nếu bạn cần bao gồm một số chi tiết triển khai thì bạn cần thể hiện khái niệm của mình trong một lớp trừu tượng." Ngoài ra, nói cách khác: Có nhiều lớp có thể được "nhóm lại với nhau" và được mô tả bằng một danh từ duy nhất không? Nếu vậy, hãy tạo một lớp trừu tượng với tên của danh từ này và kế thừa các lớp từ nó. Ví dụ: CatDogcó thể kế thừa từ lớp trừu tượng Animalvà lớp cơ sở trừu tượng này sẽ triển khai phương thức void Breathe()- thở, do đó tất cả các loài động vật sẽ thực hiện theo cùng một cách. Những động từ nào có thể áp dụng cho lớp của tôi và có thể áp dụng cho lớp khác? Tạo một giao diện cho mỗi động từ này. Ví dụ, tất cả các loài động vật đều có thể ăn nên mình sẽ tạo một giao diện IFeedablevà bắt nó Animalthực hiện giao diện đó. Chỉ đủ tốt để thực hiện một giao diện Dog( có khả năng thích tôi), nhưng không phải tất cả. Có người đã nói: sự khác biệt chính là nơi bạn muốn triển khai. Khi tạo một giao diện, bạn có thể di chuyển phần triển khai sang bất kỳ lớp nào triển khai giao diện của bạn. Bằng cách tạo một lớp trừu tượng, bạn có thể chia sẻ việc triển khai tất cả các lớp dẫn xuất ở một nơi và tránh được nhiều điều tồi tệ như sao chép mã. HorseILikeable

StringBuffer tiết kiệm bộ nhớ như thế nào?

Lớp này Stringđược triển khai như một đối tượng bất biến, nghĩa là khi ban đầu bạn quyết định đặt thứ gì đó vào đối tượng String, máy ảo sẽ phân bổ một mảng có độ dài cố định chính xác bằng kích thước của giá trị ban đầu của bạn. Sau đó, giá trị này sẽ được coi là hằng số bên trong máy ảo, giúp cải thiện hiệu suất đáng kể nếu giá trị của chuỗi không thay đổi. Tuy nhiên, nếu bạn quyết định thay đổi nội dung của chuỗi theo bất kỳ cách nào, thì công việc thực sự của máy ảo là sao chép nội dung của chuỗi gốc vào không gian tạm thời, thực hiện các thay đổi của bạn, sau đó lưu những thay đổi đó vào một mảng bộ nhớ mới. Vì vậy, việc thay đổi giá trị của chuỗi sau khi khởi tạo là một thao tác tốn kém. StringBufferMặt khác, được triển khai dưới dạng một mảng mở rộng linh hoạt bên trong máy ảo, điều đó có nghĩa là mọi thao tác sửa đổi đều có thể xảy ra trên ô nhớ hiện có và bộ nhớ mới sẽ được phân bổ khi cần. Tuy nhiên, không có cách nào để máy ảo thực hiện tối ưu hóa StringBuffervì nội dung của nó được coi là không nhất quán trên từng phiên bản.

Tại sao các phương thức chờ và thông báo được khai báo trong lớp Object thay vì Thread?

Các phương thức wait, notify, notifyAllchỉ cần thiết khi bạn muốn các luồng của mình có quyền truy cập vào các tài nguyên được chia sẻ và tài nguyên được chia sẻ có thể là bất kỳ đối tượng java nào trong vùng heap. Do đó, các phương thức này được định nghĩa trên lớp cơ sở Objectđể mỗi đối tượng có một điều khiển cho phép các luồng chờ trên màn hình của chúng. Java không có bất kỳ đối tượng đặc biệt nào được sử dụng để chia sẻ tài nguyên được chia sẻ. Không có cấu trúc dữ liệu như vậy được xác định. Vì vậy, trách nhiệm của lớp Objectlà có thể trở thành tài nguyên được chia sẻ và cung cấp các phương thức trợ giúp như wait(), notify(), notifyAll(). Java dựa trên ý tưởng về màn hình của Charles Hoare. Trong Java, tất cả các đối tượng đều có màn hình. Các thread chờ trên màn hình, vì vậy để thực hiện việc chờ chúng ta cần có hai tham số:
  • một sợi chỉ
  • giám sát (bất kỳ đối tượng nào).
Trong thiết kế Java, một luồng không thể được xác định chính xác; nó luôn là luồng hiện tại thực thi mã. Tuy nhiên, chúng ta có thể định nghĩa một màn hình (là một đối tượng mà chúng ta có thể gọi một phương thức trên đó wait). Đây là một thiết kế tốt vì nếu chúng ta có thể buộc bất kỳ thread nào khác phải chờ trên một màn hình cụ thể sẽ dẫn đến tình trạng "xâm lấn", khiến việc thiết kế/lập trình các chương trình song song trở nên khó khăn. Hãy nhớ rằng trong Java, tất cả các hoạt động can thiệp vào các luồng khác đều không được dùng nữa (ví dụ: stop()).

Viết chương trình tạo deadlock trong Java và khắc phục nó

Trong Java deadlock, đây là tình huống trong đó có ít nhất hai luồng giữ một khối trên các tài nguyên khác nhau và cả hai đều đang chờ tài nguyên khác có sẵn để hoàn thành nhiệm vụ của chúng. Và không ai trong số họ có thể để lại khóa tài nguyên đang được giữ. Lõi Java.  Câu hỏi phỏng vấn phần 2 - 2 Chương trình ví dụ:
package thread;

public class ResolveDeadLockTest {

	public static void main(String[] args) {
		ResolveDeadLockTest test = new ResolveDeadLockTest();

		final A a = test.new A();
		final B b = test.new B();

		// Thread-1
		Runnable block1 = new Runnable() {
			public void run() {
				synchronized (a) {
					try {
					// Добавляем задержку, чтобы обе нити могли начать попытки
					// блокирования ресурсов
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					// Thread-1 заняла A но также нуждается в B
					synchronized (b) {
						System.out.println("In block 1");
					}
				}
			}
		};

		// Thread-2
		Runnable block2 = new Runnable() {
			public void run() {
				synchronized (b) {
					// Thread-2 заняла B но также нуждается в A
					synchronized (a) {
						System.out.println("In block 2");
					}
				}
			}
		};

		new Thread(block1).start();
		new Thread(block2).start();
	}

	// Resource A
	private class A {
		private int i = 10;

		public int getI() {
			return i;
		}

		public void setI(int i) {
			this.i = i;
		}
	}

	// Resource B
	private class B {
		private int i = 20;

		public int getI() {
			return i;
		}

		public void setI(int i) {
			this.i = i;
		}
	}
}
Việc chạy đoạn mã trên sẽ dẫn đến bế tắc vì những lý do rất rõ ràng (đã giải thích ở trên). Bây giờ chúng ta cần giải quyết vấn đề này. Tôi tin rằng giải pháp cho mọi vấn đề nằm ở gốc rễ của vấn đề đó. Trong trường hợp của chúng tôi, mô hình truy cập vào A và B là vấn đề chính. Vì vậy, để giải quyết, chúng ta chỉ cần thay đổi thứ tự các toán tử truy cập thành các tài nguyên được chia sẻ. Sau khi thay đổi nó sẽ như thế này:
// Thread-1
Runnable block1 = new Runnable() {
	public void run() {
		synchronized (b) {
			try {
				// Добавляем задержку, чтобы обе нити могли начать попытки
				// блокирования ресурсов
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// Thread-1 заняла B но также нуждается в А
			synchronized (a) {
				System.out.println("In block 1");
			}
		}
	}
};

// Thread-2
Runnable block2 = new Runnable() {
	public void run() {
		synchronized (b) {
			// Thread-2 заняла B но также нуждается в А
			synchronized (a) {
				System.out.println("In block 2");
			}
		}
	}
};
Chạy lại lớp này và bây giờ bạn sẽ không thấy bế tắc nữa. Tôi hy vọng điều này sẽ giúp bạn tránh được bế tắc và thoát khỏi chúng nếu gặp phải.

Điều gì xảy ra nếu lớp của bạn triển khai giao diện Serializable chứa thành phần không thể tuần tự hóa? Làm thế nào để khắc phục điều này?

Trong trường hợp này, nó sẽ bị ném đi NotSerializableExceptiontrong quá trình thực thi. Để khắc phục vấn đề này, có một giải pháp rất đơn giản - hãy đánh dấu vào các ô này transient. Điều này có nghĩa là các trường đã chọn sẽ không được tuần tự hóa. Nếu bạn cũng muốn lưu trữ trạng thái của các trường này thì bạn cần xem xét các biến tham chiếu đã triển khai Serializable. Bạn cũng có thể cần sử dụng các phương thức readResolve()writeResolve(). Hãy tóm tắt:
  • Đầu tiên, làm cho trường của bạn không thể tuần tự hóa được transient.
  • Đầu tiên writeObject, hãy gọi defaultWriteObjectluồng để lưu tất cả các trường không phải là transienttrường, sau đó gọi các phương thức còn lại để tuần tự hóa các thuộc tính riêng lẻ của đối tượng không thể tuần tự hóa của bạn.
  • Trong readObject, trước tiên hãy gọi defaultReadObjectluồng để đọc tất cả transientcác trường không phải, sau đó gọi các phương thức khác (tương ứng với các phương thức bạn đã thêm vào writeObject) để giải tuần tự hóa transientđối tượng không phải của bạn.

Giải thích các từ khóa nhất thời và dễ bay hơi trong Java

"Từ khóa transientđược sử dụng để chỉ ra các trường sẽ không được sắp xếp theo thứ tự." Theo Đặc tả ngôn ngữ Java: Các biến có thể được đánh dấu bằng chỉ báo nhất thời để cho biết rằng chúng không phải là một phần của trạng thái liên tục của đối tượng. Ví dụ: bạn có thể chứa các trường bắt nguồn từ các trường khác và tốt hơn là lấy chúng theo chương trình thay vì khôi phục trạng thái của chúng thông qua tuần tự hóa. Ví dụ: trong một lớp, BankPayment.javacác trường như principal(giám đốc) và rate(tỷ lệ) có thể được tuần tự hóa và interest(lãi suất tích lũy) có thể được tính bất kỳ lúc nào, ngay cả sau khi khử lưu lượng. Nếu chúng ta nhớ, mỗi luồng trong Java có bộ nhớ cục bộ riêng và thực hiện các thao tác đọc/ghi trên bộ nhớ cục bộ này. Khi tất cả các thao tác được thực hiện, nó sẽ ghi trạng thái đã sửa đổi của biến vào bộ nhớ dùng chung, từ đó tất cả các luồng đều truy cập vào biến đó. Thông thường, đây là một luồng bình thường bên trong máy ảo. Nhưng công cụ sửa đổi dễ bay hơi cho máy ảo biết rằng quyền truy cập của một luồng vào biến đó phải luôn khớp với bản sao của biến đó với bản sao chính của biến đó trong bộ nhớ. Điều này có nghĩa là mỗi khi một thread muốn đọc trạng thái của một biến, nó phải xóa trạng thái bộ nhớ trong và cập nhật biến đó từ bộ nhớ chính. Volatilehữu ích nhất trong các thuật toán không khóa. Bạn đánh dấu một biến lưu trữ dữ liệu được chia sẻ là không ổn định, sau đó bạn không sử dụng khóa để truy cập vào biến đó và tất cả các thay đổi do một luồng thực hiện sẽ hiển thị cho những luồng khác. Hoặc nếu bạn muốn tạo mối quan hệ "xảy ra sau" để đảm bảo rằng các phép tính không bị lặp lại, hãy đảm bảo các thay đổi được hiển thị trong thời gian thực. Dễ bay hơi nên được sử dụng để xuất bản một cách an toàn các đối tượng bất biến trong môi trường đa luồng. Việc khai báo trường public volatile ImmutableObjectđảm bảo rằng tất cả các luồng luôn nhìn thấy tham chiếu hiện có cho phiên bản đó.

Sự khác biệt giữa Iterator và ListIterator?

Chúng ta có thể sử dụng , hoặc Iteratorđể lặp lại các phần tử . Nhưng nó chỉ có thể được sử dụng để lặp lại các phần tử . Những khác biệt khác được mô tả dưới đây. Bạn có thể: SetListMapListIteratorList
  1. lặp theo thứ tự ngược lại.
  2. lấy chỉ mục ở bất cứ đâu.
  3. thêm bất kỳ giá trị nào vào bất cứ đâu.
  4. đặt bất kỳ giá trị nào ở vị trí hiện tại.
Chúc may mắn với các nghiên cứu của bạn!! Tác giả bài viết Lokesh Gupta Bài viết gốc Java Core. Câu hỏi phỏng vấn, phần 1 Java Core. Những câu hỏi phỏng vấn phần 3
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION