Trọn bộ câu hỏi phỏng vấn Java Core 2025 – Phiên bản dài, rất dàiii

Mục lục

Sự khác biệt giữa JRE, JVM và JDK là gì?

JVM, Java Virtual Machine, là phần cốt lõi của Java Runtime Environment (JRE). Java Virtual Machine thực thi Java bytecode, được tạo ra lần đầu từ mã nguồn Java bởi Java compiler. JVM cũng có thể được sử dụng để thực thi các chương trình viết bằng các ngôn ngữ lập trình khác.

JRE, Java Runtime Environment, là triển khai tối thiểu cần thiết của máy ảo để thực thi các ứng dụng Java. Nó bao gồm JVM và một tập hợp các Java class libraries tiêu chuẩn.

JDK, Java Development Kit, là JRE và một tập hợp các công cụ để phát triển ứng dụng bằng Java, bao gồm Java compiler, các Java class libraries tiêu chuẩn, ví dụ, tài liệu và các tiện ích khác.

Tóm lại: JDK là môi trường để phát triển chương trình Java, bao gồm JRE – môi trường để chạy chương trình Java, và JRE lại chứa JVM – trình thông dịch cho mã chương trình Java.

 

Các access modifier nào tồn tại?

private: các thành viên của lớp chỉ có thể truy cập được trong phạm vi lớp đó. Từ khóa private được sử dụng để chỉ định.

default, package-private, package level: khả năng hiển thị của lớp/thành viên lớp chỉ trong phạm vi package. Đây là access modifier mặc định – không cần chỉ định đặc biệt.

protected: các thành viên của lớp có thể truy cập được trong phạm vi package và bởi các subclasses. Từ khóa protected được sử dụng để chỉ định.

public: lớp/thành viên lớp có thể truy cập được bởi mọi người.

Thứ tự các modifier theo mức độ hạn chế tăng dần: private, default, protected, public.

Trong quá trình kế thừa, access modifier có thể được thay đổi theo hướng hiển thị rộng hơn (để duy trì tính nhất quán với Liskov Substitution Principle).

Từ khóa final có nghĩa là gì?

Modifier final có thể áp dụng cho các biến, tham số phương thức, các trường và phương thức của lớp, hoặc chính các lớp.

  • Một class không thể có subclasses;
  • Một method không thể bị overridden trong subclasses;
  • Một field không thể thay đổi giá trị sau khi khởi tạo;
  • Tham số phương thức không thể thay đổi giá trị trong phương thức;
  • Các biến local không thể thay đổi sau khi được gán giá trị.

 

Giá trị mặc định cho các biến là gì?

  • byte(byte)0;
  • short(short)0;
  • int0;
  • long0L;
  • float0f;
  • double0d;
  • char\u0000;
  • booleanfalse;
  • Objects (bao gồm String) — null.

 

Bạn biết gì về hàm main()?

Phương thức main() là điểm bắt đầu của một chương trình. Một ứng dụng có thể có nhiều phương thức như vậy. Nếu phương thức này không có, việc compilation có thể thực hiện được, nhưng khi chạy, sẽ xảy ra lỗi Error: Main method not found.

public static void main(String[] args) {}

 

Bạn biết những phép toán và toán tử logic nào?

  • &: Logical AND;
  • &&: Short-circuit AND;
  • |: Logical OR;
  • ||: Short-circuit OR;
  • ^: Logical XOR;
  • !: Logical unary NOT;
  • &=: AND assignment;
  • |=: OR assignment;
  • ^=: XOR assignment;
  • ==: Equal to;
  • !=: Not equal to;
  • ?:: Ternary conditional operator.

 

Toán tử chọn lựa tam ngôi là gì?

Toán tử điều kiện tam ngôi ?: là một toán tử có thể thay thế một số cấu trúc if-then-else.

Biểu thức được viết dưới dạng sau:

condition ? expression1 : expression2

Nếu condition là true, thì expression1 được đánh giá, và kết quả của nó trở thành kết quả của toàn bộ toán tử. Nếu conditionfalse, thì expression2 được đánh giá, và giá trị của nó trở thành kết quả của toán tử. Cả hai toán hạng expression1expression2 phải trả về giá trị cùng kiểu (hoặc kiểu tương thích).

 

Bạn biết những phép toán bitwise nào?

  • ~: Bitwise unary NOT operator;
  • &: Bitwise AND;
  • &=: Bitwise AND assignment;
  • |: Bitwise OR;
  • |=: Bitwise OR assignment;
  • ^: Bitwise exclusive XOR;
  • ^=: Bitwise exclusive XOR assignment;
  • >>: Signed right shift (chia cho 2 mũ của số lần shift);
  • >>=: Signed right shift assignment;
  • >>>: Unsigned right shift;
  • >>>=: Unsigned right shift assignment;
  • <<: Signed left shift (nhân với 2 mũ của số lần shift);
  • <<=: Signed left shift assignment.

 

Modifier abstract được sử dụng ở đâu và tại sao?

Một lớp được đánh dấu bằng modifier abstract được gọi là abstract class. Các lớp như vậy chỉ có thể dùng làm superclasses cho các lớp khác. Không được phép tạo instance của chính abstract class. Tuy nhiên, subclasses của một abstract class có thể là các abstract class khác hoặc các lớp cho phép tạo object.

Một phương thức được đánh dấu bằng từ khóa abstract là một abstract method, tức là một phương thức không có implementation. Nếu một lớp chứa ít nhất một abstract method, thì toàn bộ lớp phải được khai báo là abstract.

Việc sử dụng các abstract class và phương thức cho phép mô tả một template của một object phải được triển khai trong các lớp khác. Chỉ một số hành vi chung, phổ biến cho tất cả các lớp con, được mô tả trong chính abstract class.

 

Định nghĩa khái niệm _«interface»_. Các modifier mặc định của các trường và phương thức trong interface là gì?

Từ khóa interface được sử dụng để tạo các kiểu hoàn toàn trừu tượng. Mục đích chính của interface là định nghĩa cách một class triển khai nó có thể được sử dụng. Người tạo interface định nghĩa tên phương thức, danh sách đối số và kiểu trả về, nhưng không triển khai hành vi của chúng. Tất cả các phương thức đều được ngầm định khai báo là public abstract.

Bắt đầu từ Java 8, các phương thức default (default) và phương thức static (static) được phép trong interface.

Một interface cũng có thể chứa các fields. Trong trường hợp này, chúng tự động là public, staticfinal.

 

Lớp abstract khác gì với interface? Trong trường hợp nào bạn nên sử dụng lớp abstract và trường hợp nào nên sử dụng interface?

  • Trong Java, một class có thể triển khai nhiều interface cùng lúc, nhưng chỉ có thể kế thừa từ một class.
  • Abstract class chỉ được sử dụng khi có mối quan hệ “is a”. Interface có thể được triển khai bởi các class không liên quan đến nhau.
  • Abstract class là phương tiện để tránh viết code lặp lại, một công cụ để triển khai hành vi một phần. Interface là phương tiện để thể hiện ngữ nghĩa của lớp, một contract mô tả các khả năng. Tất cả các phương thức interface đều được ngầm định khai báo là public abstract hoặc (bắt đầu từ Java 8) các phương thức default với implementation mặc định, và các fields là public static final.
  • Interface cho phép tạo cấu trúc kiểu không có hierarchy.
  • Khi kế thừa từ một abstract class, một class “hòa tan” tính cá nhân của chính nó. Bằng cách triển khai một interface, nó mở rộng chức năng của chính nó.

Abstract class chứa một phần implementation được bổ sung hoặc mở rộng trong subclasses. Trong trường hợp này, tất cả các subclasses đều tương tự nhau ở phần implementation kế thừa từ abstract class và chỉ khác nhau ở phần implementation riêng của chúng đối với các abstract method của lớp cha. Do đó, abstract class được sử dụng trong trường hợp xây dựng một hierarchy của các lớp tương tự, rất giống nhau. Trong trường hợp này, kế thừa từ một abstract class triển khai hành vi mặc định của một object có thể hữu ích, vì nó tránh viết code lặp lại. Trong tất cả các trường hợp khác, tốt hơn là sử dụng interface.

 

Tại sao một số interface không định nghĩa bất kỳ phương thức nào cả?

Đây là cái gọi là marker interfaces. Chúng chỉ đơn giản là chỉ ra rằng một class thuộc một kiểu nhất định. Một ví dụ là interface Cloneable, chỉ ra rằng class hỗ trợ cơ chế cloning.

 

Tại sao phương thức của interface không thể được khai báo với modifier final?

Trong trường hợp interface, việc chỉ định modifier final là vô nghĩa, vì tất cả các phương thức interface đều được ngầm định khai báo là abstract, nghĩa là chúng không thể được thực thi mà không được triển khai ở nơi khác, và điều này không thể thực hiện được nếu method identifier là final.

 

Cái nào có mức độ trừu tượng cao hơn – _class_, _abstract class_, hay _interface_?

Interface.

 

Một object có thể truy cập thành viên lớp được khai báo là private không? Nếu có, bằng cách nào?

  • Bên trong class, việc truy cập biến private là không hạn chế;
  • Một nested class có toàn quyền truy cập vào tất cả các thành viên (bao gồm cả private) của lớp bao ngoài;
  • Việc truy cập các biến private từ bên ngoài có thể được tổ chức thông qua các phương thức không phải private được cung cấp bởi nhà phát triển class. Ví dụ: getX()setX().
  • Thông qua cơ chế Reflection (Reflection API):
class Victim {
    private int field = 42;
}
//...
Victim victim = new Victim();
Field field = Victim.class.getDeclaredField("field");
field.setAccessible(true);
int fieldValue = (int) field.get(victim);
//...

 

Thứ tự gọi constructor và khối khởi tạo như thế nào khi xem xét hệ thống phân cấp lớp?

Đầu tiên, tất cả các static blocks được gọi theo thứ tự từ static block đầu tiên của tổ tiên gốc và đi lên theo chuỗi hệ thống phân cấp đến các static blocks của chính class đó.

Sau đó, các non-static initialization blocks của tổ tiên gốc được gọi, constructor của tổ tiên gốc, v.v., cho đến các non-static blocks và constructor của chính class đó.

Parent static block(s) → Child static block(s) → Grandchild static block(s)

→ Parent non-static block(s) → Parent constructor →

→ Child non-static block(s) → Child constructor →

→ Grandchild non-static block(s) → Grandchild constructor

Ví dụ 1:

public class MainClass {

    public static void main(String args[]) {
        System.out.println(TestClass.v);
        new TestClass().a();
    }

}
public class TestClass {

    public static String v = "Some val";

    {
        System.out.println("!!! Non-static initializer");
    }

    static {
        System.out.println("!!! Static initializer");
    }

    public void a() {
        System.out.println("!!! a() called");
    }

}

Kết quả thực thi:

!!! Static initializer
Some val
!!! Non-static initializer
!!! a() called

Ví dụ 2:

public class MainClass {

    public static void main(String args[]) {
        new TestClass().a();
    }

}
public class TestClass {

    public static String v = "Some val";

    {
        System.out.println("!!! Non-static initializer");
    }

    static {
        System.out.println("!!! Static initializer");
    }

    public void a() {
        System.out.println("!!! a() called");
    }

}

Kết quả thực thi:

!!! Static initializer
!!! Non-static initializer
!!! a() called

 

Tại sao cần khối khởi tạo và có những loại nào?

Khối khởi tạo là mã được đặt trong dấu ngoặc nhọn và đặt bên trong một class bên ngoài các khai báo phương thức hoặc constructor.

  • Có các static và non-static initialization blocks.
  • Một initialization block được thực thi trước khi class được class loader khởi tạo hoặc trước khi một object của class được tạo bằng constructor.
  • Nhiều initialization blocks được thực thi theo thứ tự chúng xuất hiện trong mã class.
  • Một initialization block có thể tạo ra exceptions nếu khai báo của chúng được liệt kê trong throws clause của tất cả các constructor của class.
  • Một initialization block cũng có thể được tạo trong một anonymous class.

 

Khối khởi tạo static được sử dụng cho mục đích gì trong Java?

Các static initialization blocks được sử dụng để thực thi mã nên chạy một lần khi class được class loader khởi tạo, vào thời điểm trước khi tạo các object của class này bằng constructor. Một khối như vậy (không giống như non-static blocks thuộc về một object cụ thể của class) chỉ thuộc về chính class (đối tượng Class metaclass).

 

Điều gì xảy ra nếu có ngoại lệ xảy ra trong khối khởi tạo?

Đối với non-static initialization blocks, nếu một ngoại lệ được ném ra một cách rõ ràng, thì yêu cầu các ngoại lệ này phải được liệt kê trong throws clause của tất cả các constructor của class. Nếu không, sẽ xảy ra lỗi compilation. Đối với một static block, việc ném ra một ngoại lệ một cách rõ ràng sẽ dẫn đến lỗi compilation.

Trong các trường hợp khác, việc xử lý ngoại lệ sẽ giống như ở bất kỳ nơi nào khác. Class sẽ không được khởi tạo nếu xảy ra lỗi trong một static block, và một object của class sẽ không được tạo nếu xảy ra lỗi trong một non-static block.

 

Ngoại lệ nào được ném ra khi xảy ra lỗi trong khối khởi tạo lớp?

Nếu ngoại lệ là hậu duệ của RuntimeException:

  • đối với static initialization blocks, java.lang.ExceptionInInitializerError sẽ được ném ra;
  • đối với non-static blocks, ngoại lệ gốc sẽ được ném lại.

Nếu ngoại lệ là hậu duệ của Error, thì trong cả hai trường hợp, java.lang.Error sẽ được ném ra. Ngoại lệ: java.lang.ThreadDeath – luồng chết. Trong trường hợp này, sẽ không có ngoại lệ nào được ném ra.

 

Phương thức static có thể bị override hoặc overload không?

Overload – có. Nó hoạt động chính xác như với các phương thức thông thường – hai static methods có thể có cùng tên nếu số lượng tham số hoặc kiểu của chúng khác nhau.

Override – không. Việc chọn static method để gọi xảy ra trong quá trình early binding (tại compile time, không phải runtime), và phương thức của lớp cha sẽ luôn được thực thi, mặc dù về mặt cú pháp, việc override một static method là một cấu trúc ngôn ngữ hoàn toàn hợp lệ.

Nói chung, nên truy cập static fields và methods thông qua tên class, không phải object.

 

Các phương thức non-static có thể overload phương thức static không?

Có. Kết quả sẽ là hai phương thức khác nhau. Phương thức static sẽ thuộc về class và có thể truy cập thông qua tên của nó, còn phương thức non-static sẽ thuộc về một object cụ thể và có thể truy cập thông qua việc gọi phương thức trên object đó.

 

Có thể thu hẹp mức truy cập/kiểu trả về khi override phương thức không?

  • Khi override một phương thức, bạn không thể thu hẹp access modifier (ví dụ: từ public trong MainClass thành private trong Class extends MainClass).
  • Bạn không thể thay đổi kiểu trả về khi override một phương thức; nó sẽ dẫn đến lỗi “attempting to use incompatible return type”.
  • Bạn có thể thu hẹp giá trị trả về nếu chúng tương thích.

Ví dụ:

public class Animal {

    public Animal eat() {
        System.out.println("animal eat");
        return null;
    }

    public Long calc() {
        return null;
    }

}
public class Dog extends Animal {

    public Dog eat() {
        return new Dog();
    }
/*attempting to use incompatible return type
    public Integer calc() {
        return null;
    }
*/
}

 

Khi override một phương thức, có thể thay đổi: access modifier, kiểu trả về, kiểu đối số hoặc số lượng, tên đối số hoặc thứ tự; loại bỏ, thêm, thay đổi thứ tự các phần tử trong phần throws không?

Khi override một phương thức, không được phép thu hẹp access modifier, vì điều đó sẽ vi phạm Liskov Substitution Principle. Mở rộng mức truy cập là có thể.

Bạn có thể thay đổi mọi thứ không ngăn compiler hiểu phương thức nào của lớp cha đang được đề cập:

  • Thay đổi kiểu trả về khi override một phương thức chỉ được phép theo hướng thu hẹp kiểu (thay vì lớp cha – là subclass).
  • Nếu bạn thay đổi kiểu, số lượng, hoặc thứ tự đối số, nó sẽ dẫn đến overloading phương thức thay vì overriding.
  • Phần throws của một phương thức có thể bị bỏ qua, nhưng hãy nhớ rằng nó vẫn hợp lệ nếu đã được định nghĩa trong phương thức của lớp cha. Ngoài ra, có thể thêm các ngoại lệ mới là hậu duệ của những ngoại lệ đã khai báo hoặc các ngoại lệ RuntimeException. Thứ tự của các phần tử như vậy khi overriding không quan trọng.

 

Làm thế nào để truy cập các phương thức của lớp cha đã bị override?

Sử dụng từ khóa super, chúng ta có thể truy cập bất kỳ thành viên nào của lớp cha – phương thức hoặc trường – nếu chúng không được khai báo với modifier private.

super.method();

 

Một phương thức có thể được khai báo đồng thời là abstract và static không?

Không. Trong trường hợp này, compiler sẽ báo lỗi: “Illegal combination of modifiers: ‘abstract’ and ‘static’”. Modifier abstract chỉ ra rằng phương thức sẽ được triển khai trong một class khác, trong khi static, ngược lại, chỉ ra rằng phương thức này sẽ có thể truy cập bằng tên class.

 

Sự khác biệt giữa thành viên instance và thành viên static của một lớp là gì?

Modifier static chỉ ra rằng phương thức hoặc trường này thuộc về chính class và có thể truy cập ngay cả khi không tạo instance của class. Các trường được đánh dấu static được khởi tạo khi class được khởi tạo. Các phương thức được khai báo là static có một số hạn chế:

  • Chúng chỉ có thể gọi các static methods khác.
  • Chúng chỉ được truy cập các static variables.
  • Chúng không thể tham chiếu đến các thành viên kiểu this hoặc super.

Không giống như static fields, instance fields thuộc về một object cụ thể và có thể có giá trị khác nhau cho mỗi object. Việc gọi một instance method chỉ có thể thực hiện sau khi tạo một object của class.

Ví dụ:

public class MainClass {

	public static void main(String args[]) {
		System.out.println(TestClass.v);
		new TestClass().a();
		System.out.println(TestClass.v);
	}

}
public class TestClass {

	public static String v = "Initial val";

	{
		System.out.println("!!! Non-static initializer");
		v = "Val from non-static";
	}

	static {
		System.out.println("!!! Static initializer");
		v = "Some val";
	}

	public void a() {
		System.out.println("!!! a() called");
	}

}

Kết quả:

!!! Static initializer
Some val
!!! Non-static initializer
!!! a() called
Val from non-static

 

Việc khởi tạo các trường static/non-static được phép ở đâu?

  • Static fields có thể được khởi tạo ngay khi khai báo, trong static hoặc non-static initialization block.
  • Non-static fields có thể được khởi tạo ngay khi khai báo, trong non-static initialization block, hoặc trong constructor.

 

Có những loại class nào trong Java?

  • Top level class:
    • Abstract class;
    • Final class.
  • Interfaces.
  • Enum.
  • Nested class:
    • Static nested class;
    • Member inner class;
    • Local inner class;
    • Anonymous inner class.

 

Hãy nói về các nested class. Chúng được sử dụng trong trường hợp nào?

Một class được gọi là nested nếu nó được định nghĩa bên trong một class khác. Một nested class chỉ nên được tạo ra để phục vụ class bao ngoài của nó. Nếu một nested class hóa ra hữu ích trong bất kỳ ngữ cảnh nào khác, nó nên trở thành một top-level class. Nested class có quyền truy cập vào tất cả các trường và phương thức (bao gồm cả private) của class bao ngoài, nhưng không ngược lại. Vì sự cho phép này, việc sử dụng nested class dẫn đến một số vi phạm về encapsulation.

Có bốn loại nested class:

  • Static nested class;
  • Member inner class;
  • Local inner class;
  • Anonymous inner class.

Các loại class này, trừ loại đầu tiên, còn được gọi là inner classes. Inner classes không liên kết với class bao ngoài, mà với một instance của class bao ngoài.

Mỗi loại có các khuyến nghị về cách sử dụng của nó. Nếu nested class cần hiển thị bên ngoài một phương thức duy nhất, hoặc nếu nó quá dài để đặt gọn gàng trong giới hạn của một phương thức duy nhất, và nếu mỗi instance của class như vậy cần một tham chiếu đến instance bao ngoài của nó, thì sử dụng non-static inner class. Nếu không yêu cầu tham chiếu đến class bao ngoài, tốt hơn là biến class đó thành static. Nếu class chỉ cần bên trong một số phương thức nào đó và các instance của class này chỉ cần tạo trong phương thức đó, thì sử dụng local class. Và, nếu việc sử dụng class chỉ giới hạn ở một nơi duy nhất và một type đặc trưng cho class này đã tồn tại, thì nên biến nó thành một anonymous class.

 

_«static class»_ là gì?

Đây là một nested class được khai báo sử dụng từ khóa static. Modifier static không áp dụng cho top-level classes.

 

Các đặc điểm khi sử dụng nested class: static và inner là gì? Sự khác biệt giữa chúng là gì?

  • Nested class có thể truy cập tất cả các thành viên của class bao ngoài, bao gồm cả private.
  • Để tạo một object của static nested class, không yêu cầu object của class bao ngoài.
  • Từ một object của static nested class, bạn không thể truy cập trực tiếp các thành viên non-static của class bao ngoài, chỉ thông qua tham chiếu đến một instance của class ngoài.
  • Các regular inner class không thể chứa static methods, initialization blocks, hoặc classes. Static nested class thì có thể.
  • Một object của regular inner class lưu trữ tham chiếu đến object của class ngoài. Bên trong một static class, không có tham chiếu như vậy. Truy cập đến instance của class bao ngoài được thực hiện bằng cách chỉ định .this sau tên của nó. Ví dụ: Outer.this.

 

_«local class»_ là gì? Đặc điểm của nó là gì?

Local inner class là một nested class có thể được khai báo trong bất kỳ block nào cho phép khai báo biến. Giống như simple inner classes (Member inner class), local class có tên và có thể sử dụng nhiều lần. Giống như anonymous classes, chúng chỉ có instance bao ngoài khi được sử dụng trong ngữ cảnh non-static.

Local class có các đặc điểm sau:

  • Chỉ hiển thị trong phạm vi block mà chúng được khai báo;
  • Không thể được khai báo là private/public/protected hoặc static;
  • Không thể có khai báo static method hoặc class bên trong, nhưng có thể có final static fields được khởi tạo bằng một hằng số;
  • Có quyền truy cập vào các trường và phương thức của class bao ngoài;
  • Có thể truy cập local variables và tham số phương thức nếu chúng được khai báo với modifier final.

 

_«anonymous classes»_ là gì? Chúng được sử dụng ở đâu?

Đây là một nested local class không có tên, có thể được khai báo ở bất kỳ đâu trong class bao ngoài cho phép các biểu thức. Việc tạo một instance của anonymous class xảy ra đồng thời với việc khai báo nó. Tùy thuộc vào vị trí của nó, anonymous class hoạt động như một static hoặc non-static nested class – trong ngữ cảnh non-static, nó có một instance bao ngoài.

Anonymous class có một số hạn chế:

  • Việc sử dụng chúng chỉ được phép ở một nơi duy nhất trong chương trình – nơi tạo ra nó;
  • Việc sử dụng chỉ có thể thực hiện nếu không cần tham chiếu đến instance sau khi tạo ra nó;
  • Nó chỉ triển khai các phương thức của interface hoặc superclass của nó, tức là nó không thể khai báo bất kỳ phương thức mới nào, vì không có named type nào để truy cập chúng.

Anonymous class thường được sử dụng cho:

  • tạo một function object, ví dụ, triển khai interface Comparator;
  • tạo một process object, chẳng hạn như các instance của class Thread, Runnable và tương tự;
  • trong một static factory method;
  • khởi tạo một public static final field tương ứng với một complex enumeration of types, trong đó mỗi instance trong enumeration yêu cầu một subclass riêng biệt.

 

Làm thế nào một nested class có thể truy cập trường của lớp ngoài?

Một static nested class chỉ có quyền truy cập trực tiếp vào các static fields của class bao ngoài.

Một simple inner class có thể truy cập trực tiếp bất kỳ trường nào của class ngoài. Nếu nested class đã có một trường cùng tên, thì việc truy cập trường đó phải thông qua tham chiếu đến instance của nó. Ví dụ: Outer.this.field.

 

Toán tử assert được sử dụng để làm gì?

Assert là một cấu trúc đặc biệt cho phép kiểm tra các giả định về giá trị của dữ liệu tùy ý tại bất kỳ điểm nào trong chương trình. Một assertion có thể tự động báo hiệu việc phát hiện dữ liệu không chính xác, điều này thường dẫn đến chương trình kết thúc bất thường với chỉ dẫn về nơi tìm thấy dữ liệu không chính xác.

Assertions giúp đơn giản hóa đáng kể việc định vị lỗi trong mã. Ngay cả việc kiểm tra kết quả thực thi của mã rõ ràng cũng có thể hữu ích trong quá trình refactoring sau này, sau đó mã có thể trở nên ít rõ ràng hơn và có thể có lỗi xảy ra.

Assertions thường được bật trong quá trình phát triển và kiểm thử chương trình, nhưng bị tắt trong các phiên bản release của chương trình.

Vì assertions có thể bị xóa trong quá trình compilation hoặc runtime, chúng không được làm thay đổi hành vi của chương trình. Nếu hành vi của chương trình có thể thay đổi do việc xóa một assertion, đó là dấu hiệu rõ ràng của việc sử dụng assert không đúng. Do đó, trong một assert, bạn không thể gọi các phương thức làm thay đổi trạng thái của chương trình hoặc môi trường bên ngoài của chương trình.

Trong Java, việc kiểm tra assertion được triển khai bằng cách sử dụng toán tử assert, có dạng:

assert [Boolean expression]; hoặc assert [Boolean expression] : [Expression of any type except void];

Trong quá trình thực thi chương trình, nếu việc kiểm tra assertion được bật, biểu thức boolean được đánh giá, và nếu kết quả là false, một ngoại lệ java.lang.AssertionError sẽ được tạo ra. Trong trường hợp dạng thứ hai của toán tử assert, biểu thức sau dấu hai chấm chỉ định một thông báo chi tiết về lỗi xảy ra (biểu thức được đánh giá sẽ được chuyển đổi thành chuỗi và truyền vào constructor của AssertionError).

 

Bộ nhớ _Heap_ và _Stack_ trong Java là gì? Chúng khác nhau như thế nào?

Heap được sử dụng bởi Java Runtime để cấp phát bộ nhớ cho các objects và classes. Việc tạo một object mới cũng xảy ra trên heap. Đây cũng là khu vực mà garbage collector hoạt động. Bất kỳ object nào được tạo trên heap đều có quyền truy cập toàn cầu và có thể được tham chiếu từ bất kỳ phần nào của ứng dụng.

Stack là một khu vực lưu trữ dữ liệu cũng nằm trong bộ nhớ chính (RAM). Mỗi khi một phương thức được gọi, một khối mới được tạo trong bộ nhớ stack, chứa các primitives và các tham chiếu đến các objects khác trong phương thức. Ngay sau khi phương thức kết thúc thực thi, khối đó cũng ngừng được sử dụng, do đó cung cấp quyền truy cập cho phương thức tiếp theo.

Kích thước bộ nhớ stack nhỏ hơn nhiều so với lượng bộ nhớ trên heap. Stack trong Java hoạt động theo nguyên tắc LIFO (Last-In-First-Out).

Sự khác biệt giữa bộ nhớ HeapStack:

  • Heap được sử dụng bởi tất cả các phần của ứng dụng, trong khi stack chỉ được sử dụng bởi một luồng thực thi.
  • Bất cứ khi nào một object được tạo, nó luôn được lưu trữ trên heap, và bộ nhớ stack chỉ chứa tham chiếu đến nó. Bộ nhớ stack chỉ chứa các local variables của kiểu primitive và các tham chiếu đến objects trên heap.
  • Objects trên heap có thể truy cập từ bất kỳ điểm nào trong chương trình, trong khi bộ nhớ stack không thể được truy cập bởi các luồng khác.
  • Bộ nhớ stack chỉ tồn tại trong một khoảng thời gian nhất định của quá trình thực thi chương trình, trong khi bộ nhớ heap tồn tại từ rất sớm cho đến khi kết thúc quá trình thực thi chương trình.
  • Nếu bộ nhớ stack bị đầy hoàn toàn, Java Runtime sẽ ném ra java.lang.StackOverflowError. Nếu bộ nhớ heap đầy, ngoại lệ java.lang.OutOfMemoryError: Java Heap Space sẽ được ném ra.
  • Kích thước bộ nhớ stack nhỏ hơn nhiều so với bộ nhớ heap.
  • Do sự đơn giản của việc cấp phát bộ nhớ, bộ nhớ stack nhanh hơn nhiều so với bộ nhớ heap.

Các tùy chọn JVM -Xms-Xmx được sử dụng để định nghĩa kích thước ban đầu và tối đa của bộ nhớ heap. Đối với stack, kích thước bộ nhớ có thể được định nghĩa bằng tùy chọn -Xss.

 

Có đúng là các kiểu dữ liệu nguyên thủy luôn được lưu trên stack, còn instance của các kiểu dữ liệu tham chiếu trên heap không?

Không hoàn toàn. Một primitive field của một class instance không được lưu trữ trên stack, mà trên heap. Bất kỳ object nào (bất cứ thứ gì được tạo một cách rõ ràng hoặc ngầm định bằng cách sử dụng toán tử new) đều được lưu trữ trên heap.

 

Các biến được truyền vào phương thức theo giá trị hay tham chiếu?

Trong Java, các tham số luôn chỉ được truyền theo giá trị, được định nghĩa là “copy giá trị và truyền bản sao”. Với primitives, đây sẽ là một bản sao của nội dung. Với references – cũng là một bản sao của nội dung, tức là bản sao của tham chiếu. Tuy nhiên, các thành viên nội bộ của kiểu tham chiếu có thể được thay đổi thông qua bản sao như vậy, nhưng chính tham chiếu, trỏ đến instance, thì không thể.

 

Garbage collector dùng để làm gì?

Công việc của Garbage Collector là thực hiện hai việc:

  • Tìm garbage – các object không sử dụng. (Một object được coi là không sử dụng nếu không có thực thể nào trong mã đang thực thi giữ references đến nó, hoặc nếu chuỗi references có thể kết nối object với một thực thể ứng dụng nào đó bị phá vỡ);
  • Giải phóng bộ nhớ khỏi garbage.

Có hai cách tiếp cận để phát hiện garbage:

  • Reference counting;
  • Tracing

Reference counting. Bản chất của cách tiếp cận này là mỗi object có một bộ đếm. Bộ đếm lưu trữ thông tin về số lượng references trỏ đến object. Khi một reference bị hủy, bộ đếm giảm. Nếu giá trị bộ đếm là 0, object có thể được coi là garbage. Nhược điểm chính của cách tiếp cận này là khó đảm bảo độ chính xác của bộ đếm. Ngoài ra, cách tiếp cận này làm khó phát hiện các cyclic dependencies (khi hai object trỏ vào nhau, nhưng không có object “live” nào tham chiếu đến chúng), dẫn đến memory leaks.

Ý tưởng chính của cách tiếp cận Tracing là khẳng định rằng chỉ những object có thể truy cập được từ GC Roots và những object có thể truy cập được từ một object live mới được coi là live. Tất cả những thứ khác là garbage.

Có 4 loại root points:

  • Các biến local và tham số phương thức;
  • Các luồng (Threads);
  • Các biến static;
  • Các references từ JNI.

Một ứng dụng Java đơn giản nhất sẽ có các root points:

  • Các biến local bên trong phương thức main() và các tham số của phương thức main();
  • Luồng thực thi main();
  • Các biến static của class chứa phương thức main().

Như vậy, nếu chúng ta biểu diễn tất cả các objects và các references giữa chúng như một cây, chúng ta cần duyệt từ các nút (điểm) gốc dọc theo tất cả các cạnh. Trong trường hợp này, các nút mà chúng ta có thể truy cập không phải là garbage, tất cả các nút khác là garbage. Với cách tiếp cận này, cyclic dependencies được phát hiện dễ dàng. HotSpot VM sử dụng chính cách tiếp cận này.


Có hai phương pháp chính để giải phóng bộ nhớ khỏi garbage:

  • Copying collectors
  • Mark-and-sweep

Trong cách tiếp cận copying collectors, bộ nhớ được chia thành hai phần “from-space” và “to-space”, và nguyên tắc hoạt động như sau:

  • Objects được tạo trong “from-space”;
  • Khi “from-space” đầy, ứng dụng tạm dừng;
  • Garbage collector chạy. Các object live trong “from-space” được tìm thấy và sao chép sang “to-space”;
  • Khi tất cả các objects được sao chép, “from-space” được làm sạch hoàn toàn;
  • “to-space” và “from-space” đổi vai trò cho nhau.

Ưu điểm chính của cách tiếp cận này là objects lấp đầy bộ nhớ một cách dày đặc. Nhược điểm của cách tiếp cận:

  1. Ứng dụng phải tạm dừng trong thời gian cần thiết cho chu kỳ garbage collection đầy đủ;
  2. Trong trường hợp xấu nhất (khi tất cả objects đều live), “from-space” và “to-space” phải có kích thước bằng nhau.

Thuật toán mark-and-sweep có thể được mô tả như sau:

  • Objects được tạo trong bộ nhớ;
  • Khi cần chạy garbage collection, ứng dụng tạm dừng;
  • Collector duyệt cây object, đánh dấu các object live;
  • Collector duyệt qua toàn bộ bộ nhớ, tìm tất cả các phần bộ nhớ chưa được đánh dấu và lưu chúng vào “free list”;
  • Khi các object mới bắt đầu được tạo, chúng được tạo trong bộ nhớ có sẵn trong “free list”.

Nhược điểm của phương pháp này:

  1. Ứng dụng không chạy trong khi garbage collection đang diễn ra;
  2. Thời gian tạm dừng phụ thuộc trực tiếp vào kích thước bộ nhớ và số lượng objects;
  3. Nếu không sử dụng “compacting”, bộ nhớ sẽ được sử dụng không hiệu quả.

Các garbage collector của HotSpot VM sử dụng cách tiếp cận kết hợp, Generational Garbage Collection, cho phép sử dụng các thuật toán khác nhau cho các giai đoạn khác nhau của garbage collection. Cách tiếp cận này dựa trên thực tế rằng:

  • hầu hết các object được tạo nhanh chóng trở thành garbage;
  • có ít liên kết giữa các object đã được tạo trong quá khứ và các object vừa được tạo.

 

Garbage collector hoạt động như thế nào?

Cơ chế garbage collection là quá trình giải phóng không gian trên heap để cho phép thêm các object mới. Objects được tạo bằng cách sử dụng toán tử new, do đó gán một reference cho object. Để kết thúc làm việc với một object, chỉ cần ngừng tham chiếu đến nó, ví dụ, bằng cách gán một reference đến một object khác hoặc null cho biến; hoặc bằng cách kết thúc thực thi một phương thức để các local variables của nó không còn tồn tại một cách tự nhiên. Các object không có references nào được gọi là garbage và sẽ bị xóa.

Java virtual machine, sử dụng cơ chế garbage collection, đảm bảo rằng bất kỳ object nào có references vẫn còn trong bộ nhớ – tất cả các object không thể truy cập được từ mã đang thực thi, do không có references đến chúng, sẽ bị xóa, giải phóng bộ nhớ đã cấp phát cho chúng. Chính xác hơn, một object không nằm trong phạm vi xử lý của garbage collection nếu nó có thể truy cập được thông qua một chuỗi references, bắt đầu từ một reference GC Root, tức là một reference tồn tại trực tiếp trong mã đang thực thi.

Bộ nhớ được giải phóng bởi garbage collector theo “quyền quyết định” của nó. Chương trình có thể hoàn thành công việc thành công mà không sử dụng hết bộ nhớ trống có sẵn hoặc thậm chí không đến gần giới hạn này, và do đó sẽ không bao giờ yêu cầu “dịch vụ” của garbage collector.

Garbage được thu gom tự động bởi hệ thống, không có sự can thiệp của người dùng hoặc lập trình viên, nhưng điều này không có nghĩa là quá trình này không cần chú ý chút nào. Việc cần tạo và xóa số lượng lớn objects ảnh hưởng đáng kể đến hiệu suất ứng dụng, và nếu tốc độ chương trình là một yếu tố quan trọng, các quyết định liên quan đến việc tạo object nên được xem xét cẩn thận – điều này, lần lượt, sẽ giảm lượng garbage cần xử lý.

 

Các loại garbage collector nào được triển khai trong máy ảo HotSpot?

Java HotSpot VM cung cấp cho nhà phát triển bốn loại garbage collector khác nhau:

  • Serial – tùy chọn đơn giản nhất cho các ứng dụng có lượng dữ liệu nhỏ và không yêu cầu về latency. Hiện tại được sử dụng tương đối ít, nhưng trên các máy tính yếu hơn, nó có thể được máy ảo chọn làm collector mặc định. Sử dụng Serial GC được bật bằng tùy chọn -XX:+UseSerialGC.
  • Parallel – kế thừa các cách tiếp cận thu gom từ serial collector nhưng bổ sung song song hóa cho một số hoạt động, cũng như khả năng tự động điều chỉnh để đáp ứng các tham số hiệu suất yêu cầu. Parallel collector được bật bằng tùy chọn -XX:+UseParallelGC.
  • Concurrent Mark Sweep (CMS) – nhằm mục đích giảm latency tối đa bằng cách thực hiện một số tác vụ garbage collection đồng thời với các luồng ứng dụng chính. Phù hợp cho làm việc với lượng lớn dữ liệu trong bộ nhớ. Sử dụng CMS GC được bật bằng tùy chọn -XX:+UseConcMarkSweepGC.
  • Garbage-First (G1) – được tạo ra để thay thế CMS, đặc biệt trong các ứng dụng server chạy trên các server đa bộ xử lý và hoạt động với lượng lớn dữ liệu. G1 được bật bằng tùy chọn Java -XX:+UseG1GC.

 

Mô tả thuật toán của một garbage collector nào đó được triển khai trong máy ảo HotSpot.

Serial Garbage Collector là một trong những garbage collector đầu tiên trong HotSpot VM. Trong quá trình hoạt động của collector này, ứng dụng tạm dừng và chỉ tiếp tục hoạt động sau khi quá trình garbage collection hoàn tất.

Bộ nhớ ứng dụng được chia thành ba không gian:

  • Young generation. Objects được tạo trong khu vực bộ nhớ này.
  • Old generation. Các object sống sót sau “minor garbage collection” được chuyển đến khu vực bộ nhớ này.
  • Permanent generation. Khu vực này lưu trữ metadata về objects, Class data sharing (CDS), String pool. Khu vực Permanent được chia thành hai: read-only và read-write. Rõ ràng, trong trường hợp này, khu vực read-only không bao giờ được làm sạch bởi garbage collector.

Khu vực bộ nhớ Young generation bao gồm ba khu vực: Eden và hai Survivor spaces nhỏ hơn – To spaceFrom space. Hầu hết objects được tạo trong khu vực Eden, ngoại trừ các object rất lớn không thể đặt trong đó và do đó được đặt ngay lập tức vào Old generation. Các object đã sống sót ít nhất một lần garbage collection nhưng chưa đạt đến tenuring threshold để chuyển sang Old generation sẽ được chuyển sang Survivor spaces.

Khi Young generation đầy, quá trình minor collection bắt đầu trong khu vực này, trái ngược với quá trình collection được thực hiện trên toàn bộ heap (full collection). Nó xảy ra như sau: lúc đầu, một trong các Survivor space – To space – trống, trong khi space kia – From space – chứa các object sống sót sau các collection trước đó. Garbage collector tìm kiếm các object live trong Eden và sao chép chúng sang To space, sau đó cũng sao chép các object “young” live (tức là những object chưa sống sót đủ số lần garbage collection được chỉ định) từ From space sang đó. Các object “old” từ From space được chuyển sang Old generation. Sau minor collection, From space và To space đổi vai trò cho nhau, khu vực Eden trở nên trống, và số lượng object trong Old generation tăng lên.

Nếu To space bị tràn trong quá trình sao chép các object live, các object live còn lại từ Eden và From space không vừa trong To space sẽ được chuyển sang Old generation, bất kể chúng đã sống sót bao nhiêu lần garbage collection.

Vì thuật toán này chỉ đơn giản là sao chép tất cả các object live từ khu vực bộ nhớ này sang khu vực bộ nhớ khác, garbage collector này được gọi là copying. Rõ ràng, để một copying garbage collector hoạt động, ứng dụng phải luôn có một khu vực bộ nhớ trống mà các object live sẽ được sao chép sang, và thuật toán như vậy có thể được áp dụng cho các khu vực bộ nhớ tương đối nhỏ so với tổng kích thước bộ nhớ của ứng dụng. Young generation chính xác thỏa mãn điều kiện này (mặc định, trên các máy client-type, khu vực này chiếm khoảng 10% heap (giá trị có thể thay đổi tùy thuộc vào nền tảng)).

Tuy nhiên, một thuật toán khác được sử dụng cho garbage collection trong Old generation, chiếm phần lớn toàn bộ bộ nhớ.

Trong Old generation, garbage collection xảy ra sử dụng thuật toán mark-sweep-compact, bao gồm ba giai đoạn. Trong giai đoạn Mark, garbage collector đánh dấu tất cả các object live, sau đó, trong giai đoạn Sweep, tất cả các object chưa được đánh dấu sẽ bị xóa, và trong giai đoạn Compact, tất cả các object live được di chuyển đến đầu Old generation, kết quả là bộ nhớ trống sau khi làm sạch là một khu vực liên tục. Giai đoạn compacting được thực hiện để tránh fragmentation và đơn giản hóa quá trình cấp phát bộ nhớ trong Old generation.

Khi bộ nhớ trống là một khu vực liên tục, thuật toán rất nhanh bump-the-pointer (khoảng một tá chỉ thị máy) có thể được sử dụng để cấp phát bộ nhớ cho object đang được tạo: địa chỉ bắt đầu của bộ nhớ trống được lưu trữ trong một con trỏ đặc biệt, và khi yêu cầu tạo một object mới đến, mã kiểm tra xem có đủ không gian cho object mới không, và nếu có, chỉ cần tăng con trỏ lên bằng kích thước của object.

Serial garbage collector rất tốt cho hầu hết các ứng dụng sử dụng tới 200 megabyte heap, chạy trên các máy client-type và không có yêu cầu nghiêm ngặt về độ dài các tạm dừng dành cho garbage collection. Đồng thời, mô hình “stop-the-world” có thể gây ra các tạm dừng dài trong ứng dụng khi sử dụng lượng lớn bộ nhớ. Hơn nữa, thuật toán serial không cho phép sử dụng tối ưu tài nguyên tính toán của máy tính, và serial garbage collector có thể trở thành bottleneck khi ứng dụng chạy trên các máy đa bộ xử lý.

 

«string pool» là gì?

String pool là một tập hợp các chuỗi được lưu trữ trên Heap.

  • String pool khả thi là nhờ vào tính bất biến của chuỗi trong Java và việc triển khai string interning;
  • String pool giúp tiết kiệm bộ nhớ, nhưng vì lý do tương tự, việc tạo một chuỗi mất nhiều thời gian hơn;
  • Khi sử dụng " để tạo một chuỗi, pool trước tiên sẽ được tìm kiếm một chuỗi có cùng giá trị; nếu tìm thấy, chỉ đơn giản là trả về một reference đến nó, ngược lại, một chuỗi mới được tạo trong pool, và sau đó một reference đến nó được trả về;
  • Sử dụng toán tử new tạo ra một object String mới. Sau đó, sử dụng phương thức intern(), chuỗi này có thể được đặt trong pool hoặc có thể truy xuất một reference đến một object String khác có cùng giá trị từ pool;
  • String pool là một ví dụ của design pattern Flyweight.

 

Tại sao String là một class immutable và final?

Có một số ưu điểm khi chuỗi bất biến:

  • String pool chỉ khả thi vì chuỗi là immutable, do đó máy ảo tiết kiệm được nhiều không gian trống trên Heap hơn, vì các biến chuỗi khác nhau trỏ đến cùng một biến trong pool. Nếu chuỗi có thể thay đổi, việc string interning sẽ không khả thi vì việc thay đổi giá trị của một biến cũng sẽ ảnh hưởng đến các biến khác tham chiếu đến chuỗi đó.
  • Nếu chuỗi có thể thay đổi, nó sẽ gây ra mối đe dọa bảo mật nghiêm trọng cho ứng dụng. Ví dụ, tên người dùng và mật khẩu cơ sở dữ liệu được truyền dưới dạng chuỗi để lấy kết nối cơ sở dữ liệu, và trong lập trình socket, thông tin xác thực máy chủ và cổng được truyền dưới dạng chuỗi. Vì chuỗi là immutable, giá trị của nó không thể thay đổi; nếu không, kẻ tấn công có thể thay đổi giá trị tham chiếu và gây ra các vấn đề bảo mật trong ứng dụng.
  • Tính bất biến tránh synchronization: chuỗi là thread-safe và một instance của chuỗi có thể được chia sẻ bởi các luồng khác nhau.
  • Chuỗi được sử dụng bởi classloader và tính bất biến đảm bảo việc tải class chính xác.
  • Vì chuỗi là immutable, hashCode() của nó được cache tại thời điểm tạo, và không cần tính toán lại cho lần sử dụng tiếp theo. Điều này làm cho chuỗi trở thành một ứng viên tuyệt vời cho key trong một HashMap vì việc xử lý của nó nhanh hơn.

 

Tại sao char[] được ưu tiên hơn String để lưu trữ mật khẩu?

Một khi được tạo, một chuỗi vẫn còn trong pool cho đến khi nó bị garbage collected. Do đó, ngay cả sau khi bạn sử dụng xong mật khẩu, nó vẫn tiếp tục có sẵn trong bộ nhớ trong một thời gian, và không có cách nào để tránh điều này. Điều này gây ra một rủi ro bảo mật nhất định, vì ai đó có quyền truy cập bộ nhớ có thể tìm thấy mật khẩu dưới dạng văn bản.

Khi sử dụng một character array để lưu trữ mật khẩu, có thể xóa nó ngay sau khi làm việc xong với mật khẩu, tránh rủi ro bảo mật cố hữu trong chuỗi.

 

Tại sao String là key phổ biến trong HashMap trong Java?

Vì chuỗi là immutable, hash code của chúng được tính toán và cache tại thời điểm tạo, không yêu cầu tính toán lại cho lần sử dụng tiếp theo. Do đó, làm key HashMap, chúng sẽ được xử lý nhanh hơn.

 

Phương thức intern() trong class String làm gì?

Phương thức intern() được sử dụng để lưu trữ một chuỗi trong string pool hoặc truy xuất một reference nếu chuỗi đó đã có trong pool.

 

String có thể được sử dụng trong câu lệnh switch không?

Có, bắt đầu từ Java 7, chuỗi có thể được sử dụng trong câu lệnh switch; các phiên bản Java trước không hỗ trợ điều này. Trong trường hợp này:

  • các chuỗi liên quan là case-sensitive;
  • phương thức equals() được sử dụng để so sánh giá trị nhận được với các giá trị case, do đó để tránh NullPointerException, bạn nên kiểm tra null.
  • theo tài liệu cho Java 7 strings trong switch, Java compiler tạo ra bytecode hiệu quả hơn cho chuỗi trong câu lệnh switch so với các điều kiện ifelse nối tiếp.

 

Sự khác biệt chính giữa String, StringBuffer, StringBuilder là gì?

Class Stringimmutable – bạn không thể sửa đổi một object của class này; bạn chỉ có thể thay thế nó bằng cách tạo một instance mới.

Class StringBuffermutable – bạn nên sử dụng StringBuffer khi cần sửa đổi nội dung thường xuyên.

Class StringBuilder được thêm vào Java 5 và nó giống hệt class StringBuffer về mọi mặt ngoại trừ việc nó không được synchronized và do đó các phương thức của nó thực thi nhanh hơn đáng kể.

 

Class Object là gì? Nó có những phương thức nào?

Object là base class cho tất cả các objects khác trong Java. Bất kỳ class nào cũng kế thừa từ Object và do đó kế thừa các phương thức của nó:

  • public boolean equals(Object obj) – được sử dụng để so sánh objects theo value;
  • int hashCode() – trả về hash code cho một object;
  • String toString() – trả về string representation của một object;
  • Class getClass() – trả về runtime class của object;
  • protected Object clone() – tạo và trả về một copy của object;
  • void notify() – đánh thức một luồng duy nhất đang chờ trên monitor của object này;
  • void notifyAll() – đánh thức tất cả các luồng đang chờ trên monitor của object này;
  • void wait() – tạm dừng luồng hiện tại cho đến khi một luồng khác gọi phương thức notify() hoặc notifyAll() cho object này;
  • void wait(long timeout) – tạm dừng luồng hiện tại trong một khoảng thời gian chỉ định hoặc cho đến khi một luồng khác gọi phương thức notify() hoặc notifyAll() cho object này;
  • void wait(long timeout, int nanos) – tạm dừng luồng hiện tại trong một khoảng thời gian chỉ định hoặc cho đến khi một luồng khác gọi phương thức notify() hoặc notifyAll() cho object này;
  • protected void finalize() – có thể được gọi bởi garbage collector khi thu gom garbage.

 

Định nghĩa khái niệm «constructor».

Một constructor là một phương thức đặc biệt không có return type và có cùng tên với class mà nó được sử dụng. Constructor được gọi khi một object mới của class được tạo và định nghĩa các hành động cần thiết để khởi tạo nó.

 

_«default constructor»_ là gì?

Nếu một class không định nghĩa constructor nào, compiler sẽ tạo một constructor không có đối số – cái gọi là «default constructor».

public ClassName() {}

Nếu một class đã có bất kỳ constructor nào được định nghĩa, default constructor sẽ không được tạo, và nếu cần, nó phải được mô tả rõ ràng.

 

Constructor mặc định, copy constructor và constructor có tham số khác nhau như thế nào?

Constructor mặc định không có đối số. Một copy constructor nhận một object hiện có của class làm đối số để sau đó tạo ra bản sao của nó. Một parameterized constructor có các đối số trong signature của nó (thường được yêu cầu để khởi tạo các trường của lớp).

 

Private constructor có thể được sử dụng ở đâu và như thế nào?

Một private constructor (được đánh dấu bằng từ khóa private) có thể được sử dụng bởi một public static factory method để tạo các object của class đó. Nó cũng có thể truy cập được đối với nested classes và có thể được sử dụng cho nhu cầu của chúng.

 

Hãy nói về classloader và dynamic class loading.

Nền tảng của việc làm việc với các class trong Java là các classloader, là các Java object thông thường cung cấp một interface để tìm và tạo một class object theo tên của nó trong quá trình application runtime.

Khi chương trình bắt đầu, ba classloader chính được tạo:

  • Bootstrap/primordial classloader. Tải các class hệ thống cốt lõi và các class nội bộ của JDK (Core API – các packages java.* (rt.jari18n.jar)). Điều quan trọng cần lưu ý là bootstrap classloader là classloader “Primordial” hoặc “Root” và là một phần của JVM, do đó không thể tạo nó trong mã chương trình.
  • Extension classloader. Tải các extension packages khác nhau nằm trong thư mục /lib/ext hoặc một thư mục khác được chỉ định trong tham số hệ thống java.ext.dirs. Điều này cho phép cập nhật và thêm các extension mới mà không cần sửa đổi cài đặt của các ứng dụng được sử dụng. Extension classloader được triển khai bởi class sun.misc.Launcher$ExtClassLoader.
  • System/application classloader. Tải các class có đường dẫn được chỉ định trong biến môi trường CLASSPATH hoặc các đường dẫn được chỉ định trên dòng lệnh JVM sau các cờ -classpath hoặc -cp. System classloader được triển khai bởi class sun.misc.Launcher$AppClassLoader.

Các classloader có tính hierarchy: mỗi classloader (ngoại trừ bootstrap) có một parent classloader, và trong hầu hết các trường hợp, trước khi cố gắng tải class đó, nó sẽ gửi yêu cầu đến parent classloader để tải class được chỉ định. Việc ủy quyền này cho phép các class được tải bởi classloader gần nhất với bootstrap trong hierarchy ủy quyền. Do đó, các class sẽ được tìm kiếm trong các nguồn theo thứ tự đáng tin cậy của chúng: trước tiên trong thư viện Core API, sau đó trong thư mục extension, sau đó trong các tệp local trong CLASSPATH.

Quá trình tải một class bao gồm ba phần:

  • Loading – Trong giai đoạn này, tệp class được tìm kiếm và được tải vật lý từ một nguồn cụ thể (tùy thuộc vào classloader). Quá trình này xác định representation cơ bản của class trong bộ nhớ. Ở giai đoạn này, các khái niệm như “methods”, “fields”, v.v., chưa được biết đến.
  • Linking – Một quá trình có thể được chia thành 3 phần:
    • Bytecode verification – Kiểm tra bytecode để tuân thủ các yêu cầu được định nghĩa trong JVM specification.
    • Class preparation – Tạo và khởi tạo các cấu trúc cần thiết được sử dụng để biểu diễn fields, methods, implemented interfaces, v.v., được định nghĩa trong class đang được tải.
    • Resolving – Tải tập hợp các class được tham chiếu bởi class đang được tải.
  • Initialization – Gọi các static initialization blocks và gán giá trị mặc định cho các class fields.

Dynamic class loading trong Java có một số đặc điểm:

  • Lazy loading và linking của các class. Các class chỉ được tải khi cần thiết, giúp tiết kiệm tài nguyên và phân bổ tải.
  • Verification về tính đúng đắn của mã được tải (type safeness). Tất cả các hành động liên quan đến kiểm tra kiểu chỉ được thực hiện trong quá trình tải class, tránh chi phí phụ thêm trong quá trình thực thi mã.
  • Programmable loading. Một classloader tùy chỉnh có toàn quyền kiểm soát quá trình lấy class được yêu cầu – liệu có tìm kiếm bytecode và tạo class đó hay ủy quyền việc tạo cho classloader khác. Ngoài ra, có thể đặt các security attributes khác nhau cho các class được tải, do đó cho phép làm việc với mã từ các nguồn không đáng tin cậy.
  • Nhiều namespaces. Mỗi classloader có namespace riêng cho các class mà nó tạo ra. Theo đó, các class được tải bởi hai classloader khác nhau dựa trên cùng một bytecode sẽ được phân biệt trong hệ thống.

Có một số cách để bắt đầu tải một class cần thiết:

  • Rõ ràng (Explicit): gọi ClassLoader.loadClass() hoặc Class.forName() (mặc định, classloader đã tạo class hiện tại được sử dụng, nhưng có thể chỉ định rõ classloader);
  • Ngầm định (Implicit): khi một class chưa được sử dụng trước đó cần thiết cho hoạt động tiếp theo của ứng dụng, JVM sẽ bắt đầu tải nó.

 

_Reflection_ là gì?

Reflection là một cơ chế để lấy dữ liệu về một chương trình trong quá trình thực thi của nó (runtime). Trong Java, Reflection được thực hiện bằng cách sử dụng Java Reflection API, bao gồm các class trong các packages java.langjava.lang.reflect.

Các khả năng của Java Reflection API:

  • Xác định class của một object;
  • Lấy thông tin về class modifiers, fields, methods, constructors và superclasses;
  • Xác định các interface được triển khai bởi một class;
  • Tạo một instance của một class;
  • Lấy và đặt giá trị của các object fields;
  • Gọi các object methods;
  • Tạo một array mới.

 

Tại sao cần equals()? Nó khác toán tử == như thế nào?

Phương thức equals() định nghĩa mối quan hệ equivalence của các objects.

Khi so sánh các objects sử dụng ==, việc so sánh chỉ xảy ra giữa các references. Khi so sánh sử dụng phương thức equals() được nhà phát triển override, việc so sánh dựa trên internal state của các objects.

 

Nếu bạn muốn override equals(), những điều kiện nào phải được đáp ứng?

Những thuộc tính nào của mối quan hệ equivalence được tạo bởi equals() có?

  • Reflexivity: Đối với bất kỳ giá trị reference không null x nào, x.equals(x) phải trả về true.
  • Symmetry: Đối với bất kỳ giá trị reference không null xy nào, x.equals(y) phải trả về true nếu và chỉ nếu y.equals(x) trả về true.
  • Transitivity: Đối với bất kỳ giá trị reference không null x, yz nào, nếu x.equals(y) trả về truey.equals(z) trả về true, thì x.equals(z) phải trả về true.
  • Consistency: Đối với bất kỳ giá trị reference không null xy nào, nhiều lần gọi x.equals(y) liên tục trả về true hoặc liên tục trả về false, với điều kiện không có thông tin nào được sử dụng trong các phép so sánh equals trên các objects bị sửa đổi.

Đối với bất kỳ giá trị reference không null x nào, x.equals(null) phải trả về false.

 

Các quy tắc để override phương thức Object.equals().

  1. Sử dụng toán tử == để kiểm tra xem đối số có phải là reference đến chính object đó không. Nếu có, trả về true. Nếu object được so sánh là == null, trả về false.
  2. Sử dụng lệnh gọi phương thức getClass() để kiểm tra xem đối số có kiểu đúng không. Nếu không, trả về false.
  3. Cast đối số sang kiểu đúng. Vì hoạt động này sau khi kiểm tra instanceof, nên đảm bảo thành công.
  4. Duyệt qua tất cả các significant fields của class và kiểm tra xem giá trị của trường trong object hiện tại và giá trị của cùng trường đó trong đối số đang được kiểm tra equality có khớp không. Nếu tất cả các kiểm tra trường thành công, trả về true, ngược lại trả về false.

Sau khi override phương thức equals(), kiểm tra xem mối quan hệ equivalence kết quả có reflexive, symmetric, transitive và consistent không. Nếu câu trả lời là không, phương thức nên được sửa lại cho phù hợp.

 

Mối quan hệ giữa hashCode()equals() là gì?

Nếu equals() bị override, có phương thức nào khác cần được override không?

Các object bằng nhau phải trả về các hash code bằng nhau. Khi override equals(), bạn cũng phải override phương thức hashCode().

 

Điều gì xảy ra nếu equals() bị override mà không override hashCode()? Những vấn đề nào có thể phát sinh?

Các class và phương thức dựa vào các quy tắc của contract này có thể hoạt động không chính xác. Ví dụ, đối với một HashMap, điều này có thể dẫn đến việc một cặp “key-value” được chèn bằng một instance mới của key không được tìm thấy trong đó.

 

Các phương thức hashCode()equals() được triển khai như thế nào trong class Object?

Việc triển khai phương thức Object.equals() gói gọn trong việc kiểm tra sự bằng nhau của hai references:

public boolean equals(Object obj) {
  return (this == obj);
}

Việc triển khai phương thức Object.hashCode() được mô tả là native, nghĩa là nó không được định nghĩa bằng mã Java và nói chung phụ thuộc vào JVM implementation:

public native int hashCode();

Trong HotSpot JVM, hash code mặc định được tính toán sử dụng thuật toán Xorshift, một thuật toán tạo số đơn giản.

 

Phương thức hashCode() dùng để làm gì?

Phương thức hashCode() là cần thiết để tính toán hash code của một object được truyền làm input. Trong Java, đây là một số nguyên; theo nghĩa rộng hơn, nó là một chuỗi bit có độ dài cố định được lấy từ một mảng có độ dài tùy ý. Phương thức này được triển khai sao cho đối với cùng một object input, hash code sẽ luôn giống nhau. Cần hiểu rằng trong Java, tập hợp các hash code có thể có bị giới hạn bởi kiểu int, trong khi tập hợp các objects là không giới hạn. Do đó, rất có thể các hash code của các object khác nhau có thể trùng nhau:

  • nếu các hash code khác nhau, thì các object được đảm bảo là khác nhau;
  • nếu các hash code bằng nhau, các object không nhất thiết phải bằng nhau (chúng có thể khác nhau).

 

Các quy tắc để override phương thức Object.hashCode() là gì?

Có khuyến nghị nào về việc nên sử dụng trường nào khi tính toán hashCode() không?

Lời khuyên chung là chọn các trường có khả năng khác biệt đáng kể. Đối với điều này, nên sử dụng các trường độc đáo, ưu tiên là primitive fields như id, uuid. Tuy nhiên, bạn phải tuân theo quy tắc: nếu các trường được sử dụng trong tính toán hashCode(), chúng cũng phải được sử dụng trong equals().

 

Các object khác nhau có thể có cùng hashCode() không?

Có, chúng có thể. Phương thức hashCode() không đảm bảo tính duy nhất của giá trị trả về. Tình huống các object khác nhau có cùng hash code được gọi là collision. Xác suất xảy ra collision phụ thuộc vào thuật toán tạo hash code được sử dụng.

 

Nếu class Point{int x, y;}` triển khai phương thức `equals(Object that) {return this.x == that.x && this.y == that.y;}` nhưng hash code là `int hashCode() {return x;}`, liệu những điểm như vậy có được chèn và truy xuất đúng từ <code>HashSet không?

HashSet sử dụng HashMap để lưu trữ các phần tử. Khi thêm một phần tử vào HashMap, hash code được tính toán, xác định vị trí trong array nơi phần tử mới sẽ được chèn. Đối với tất cả các instance của class Point, hash code sẽ giống nhau cho tất cả các object có cùng x, điều này sẽ dẫn đến hash table bị thoái hóa thành một list.

Khi xảy ra collision trong HashMap, nó kiểm tra sự hiện diện của phần tử trong list: e.hash == hash && ((k = e.key) == key || key.equals(k)). Nếu phần tử được tìm thấy, giá trị của nó sẽ bị ghi đè. Trong trường hợp của chúng ta, phương thức equals() sẽ trả về false cho các object khác nhau. Theo đó, phần tử mới sẽ được thêm thành công vào HashSet. Việc truy xuất phần tử cũng sẽ thành công. Nhưng hiệu suất của mã như vậy sẽ thấp do sự kém hiệu quả của hash function, có thể tạo ra số lượng lớn collisions.

 

Hai object khác nhau (ref0 != ref1) có thể có ref0.equals(ref1) == true không?

Có, chúng có thể. Để điều này xảy ra, phương thức equals() phải được override trong class của các object này.

Nếu phương thức Object.equals() được sử dụng, thì đối với hai references xy, phương thức sẽ trả về true nếu và chỉ nếu cả hai references trỏ đến cùng một object (tức là x == y trả về true).

 

Hai tham chiếu khác nhau đến cùng một object (ref0 == ref1) có thể có ref0.equals(ref1) == false không?

Nói chung – có, nếu phương thức equals() được triển khai không chính xác và không thỏa mãn thuộc tính reflexivity: đối với bất kỳ reference không null x nào, phương thức x.equals(x) phải trả về true.

 

Phương thức equals(Object that) {return this.hashCode() == that.hashCode();}` có thể được triển khai như thế này không?

Nói một cách chặt chẽ, không, bởi vì phương thức hashCode() không đảm bảo giá trị duy nhất cho mỗi object. Tuy nhiên, đối với việc so sánh các instance của class Object, mã như vậy là có thể chấp nhận, vì phương thức hashCode() trong class Object trả về các giá trị duy nhất cho các object khác nhau (việc tính toán của nó dựa trên việc sử dụng thuật toán tạo số ngẫu nhiên).

 

Trong equals(), cần kiểm tra rằng đối số của equals(Object that) có cùng kiểu với object đó. Sự khác biệt giữa this.getClass() == that.getClass()that instanceof MyClass là gì?

Toán tử instanceof so sánh một object và một kiểu được chỉ định. Nó có thể được sử dụng để kiểm tra xem một object được cho có phải là một instance của một class nhất định, hoặc một instance của class con của nó, hoặc một instance của class triển khai interface được chỉ định hay không.

this.getClass() == that.getClass() kiểm tra hai class về tính đồng nhất, do đó để triển khai chính xác contract của phương thức equals(), cần sử dụng so sánh chính xác bằng phương thức getClass().

 

Phương thức equals() của class MyClass có thể được triển khai như thế này không: class MyClass {public boolean equals(MyClass that) {return this == that;}}?

Nó có thể được triển khai, nhưng phương thức này không override phương thức equals() của class Object; nó overload nó.

 

Có một class Point{int x, y;. Tại sao hash code như 31 * x + y lại được ưu tiên hơn x + y?

Hệ số nhân tạo ra sự phụ thuộc của giá trị hash code vào thứ tự các trường được xử lý, điều này cuối cùng tạo ra một hash function tốt hơn.

 

Hãy nói về object cloning.

Việc sử dụng toán tử gán không tạo ra một object mới, mà chỉ đơn thuần sao chép một reference đến object đó. Do đó, hai references trỏ đến cùng một khu vực bộ nhớ, đến cùng một object. Để tạo một object mới có cùng trạng thái, object cloning được sử dụng.

Class Object chứa một phương thức protected clone() thực hiện sao chép từng bit của một object class con. Tuy nhiên, trước tiên cần override phương thức clone()public để cho phép gọi nó. Trong phương thức được override, bạn nên gọi phiên bản cơ bản của phương thức super.clone(), phương thức này thực sự thực hiện việc cloning.

Để làm cho một object thực sự có thể clone được, class phải triển khai interface Cloneable. Interface Cloneable không chứa phương thức nào và thuộc về marker interfaces; việc triển khai nó đảm bảo rằng phương thức clone() của class Object sẽ trả về một bản sao chính xác của object đã gọi nó, sao chép giá trị của tất cả các trường của nó. Ngược lại, phương thức sẽ tạo ra ngoại lệ CloneNotSupportedException. Cần lưu ý rằng khi sử dụng cơ chế này, object được tạo mà không gọi constructor.

Giải pháp này chỉ hiệu quả nếu các trường của object được clone là các kiểu primitive và các wrapper của chúng hoặc các kiểu object immutable. Nếu một trường của kiểu được clone là kiểu reference mutable, cần có cách tiếp cận khác để cloning chính xác. Lý do là khi trường được sao chép, bản gốc và bản sao đại diện cho reference đến cùng một object. Trong tình huống này, trường object của chính class cũng nên được cloned.

Việc cloning như vậy chỉ có thể thực hiện nếu kiểu của thuộc tính class cũng triển khai interface Cloneable và override phương thức clone(). Bởi vì nếu không, việc gọi phương thức là không thể do tính không truy cập được của nó. Từ đó suy ra rằng nếu một class có superclass, thì để triển khai cơ chế cloning của subclass hiện tại, việc triển khai chính xác cơ chế đó trong superclass là cần thiết. Trong trường hợp này, nên tránh sử dụng khai báo final cho các trường kiểu object vì giá trị của chúng không thể thay đổi trong quá trình triển khai cloning.

Ngoài cơ chế cloning được tích hợp sẵn trong Java, bạn có thể sử dụng các cách sau để cloning object:

  • Một specialized copy constructor – một constructor được mô tả trong class nhận một object cùng class và khởi tạo các trường của object được tạo bằng các giá trị trường của object được truyền.
  • Một factory method – một static method trả về một instance của class của nó.
  • Cơ chế Serialization – lưu và sau đó khôi phục một object sang/từ một byte stream.

 

Sự khác biệt giữa cloning _shallow_ và _deep_ là gì?

Shallow copying sao chép ít thông tin nhất có thể về object. Mặc định, cloning trong Java là shallow, nghĩa là class Object không biết cấu trúc của class mà nó đang sao chép. Loại cloning này được thực hiện bởi JVM theo các quy tắc sau:

  • Nếu class chỉ có các thành viên kiểu primitive, một bản sao hoàn toàn mới của object sẽ được tạo và một reference đến object này sẽ được trả về.
  • Nếu class, ngoài các thành viên primitive, còn chứa các thành viên kiểu reference, thì các references đến các object của các class này được sao chép. Do đó, cả hai objects sẽ có các references giống hệt nhau.

Deep copying sao chép hoàn toàn tất cả thông tin của object:

  • Không cần sao chép dữ liệu primitive riêng biệt;
  • Tất cả các thành viên kiểu reference trong class gốc phải hỗ trợ cloning. Đối với mỗi thành viên như vậy, super.clone() phải được gọi khi override phương thức clone();
  • Nếu bất kỳ thành viên nào của class không hỗ trợ cloning, một instance mới của class đó phải được tạo trong phương thức cloning, và mỗi thành viên của nó với tất cả các thuộc tính phải được sao chép vào object class mới, từng cái một.

 

Phương thức cloning nào được ưu tiên hơn?

Phương thức cloning an toàn nhất và do đó được ưu tiên là sử dụng specialized copy constructor:

  • Không có lỗi kế thừa (không cần lo lắng rằng các lớp con sẽ có các trường mới mà sẽ không được clone thông qua phương thức clone());
  • Các trường để cloning được chỉ định rõ ràng;
  • Khả năng clone ngay cả các trường final.

 

Tại sao phương thức clone() được khai báo trong class Object mà không phải trong interface Cloneable?

Phương thức clone() được khai báo trong class Object với modifier native để cung cấp quyền truy cập vào cơ chế chuẩn cho shallow copying objects. Đồng thời, nó cũng được khai báo là protected để phương thức này không thể được gọi trên các objects chưa override nó. Bản thân interface Cloneable là một marker interface (nó không chứa khai báo phương thức nào) và chỉ cần thiết để chỉ ra rằng object sẵn sàng được clone. Việc gọi phương thức clone() được override trên một object không Cloneable sẽ ném ra ngoại lệ CloneNotSupportedException.

 

Mô tả hệ thống phân cấp ngoại lệ.

Các ngoại lệ được chia thành nhiều loại, nhưng tất cả chúng đều có một tổ tiên chung – class Throwable, mà các hậu duệ của nó là các class ExceptionError.

Errors đại diện cho các vấn đề nghiêm trọng hơn mà, theo Java specification, không nên được xử lý trong chương trình của riêng bạn, vì chúng liên quan đến các vấn đề cấp JVM. Ví dụ, các ngoại lệ loại này xảy ra nếu máy ảo hết bộ nhớ có sẵn.

Exceptions là kết quả của các vấn đề trong chương trình mà về nguyên tắc có thể giải quyết được, có thể dự đoán được và hậu quả của chúng có thể được khắc phục trong chương trình. Ví dụ, xảy ra lỗi chia số nguyên cho 0.

 

Bạn biết những loại ngoại lệ nào trong Java, chúng khác nhau như thế nào?

_checked_ và _unchecked exceptions_ là gì?

Trong Java, tất cả các ngoại lệ được chia thành hai loại:

  • checked exceptions phải được xử lý bởi một block catch hoặc được khai báo trong method signature (ví dụ: throws IOException). Sự hiện diện của handler/modifier như vậy trong method signature được kiểm tra tại compile time;
  • unchecked exceptions, bao gồm Error (ví dụ: OutOfMemoryError), không được khuyến nghị xử lý, và các runtime exceptions được biểu diễn bởi class RuntimeException và các hậu duệ của nó (ví dụ: NullPointerException), có thể không được xử lý bởi block catch và không được khai báo trong method signature.

 

Toán tử nào cho phép bạn ném (throw) ngoại lệ một cách rõ ràng?

Đây là toán tử throw:

throw new Exception();

 

Từ khóa throws có nghĩa là gì?

Modifier throws được viết trong method signature và chỉ ra rằng phương thức có khả năng ném ra một ngoại lệ của kiểu được chỉ định.

 

Làm thế nào để viết ngoại lệ của riêng bạn («custom»)?

Bạn cần kế thừa từ base class của loại ngoại lệ cần thiết (ví dụ: từ Exception hoặc RuntimeException).

class CustomException extends Exception {
    public CustomException() {
        super();
    }

    public CustomException(final String string) {
        super(string + " is invalid");
    }

    public CustomException(final Throwable cause) {
        super(cause);
    }
}

 

Những _unchecked exceptions_ nào tồn tại?

Các loại phổ biến nhất là: ArithmeticException, ClassCastException, ConcurrentModificationException, IllegalArgumentException, IllegalStateException, IndexOutOfBoundsException, NoSuchElementException, NullPointerException, UnsupportedOperationException.

 

Các lỗi class Error đại diện cho điều gì?

Các lỗi class Error đại diện cho các vấn đề nghiêm trọng nhất ở cấp JVM. Ví dụ, các ngoại lệ loại này xảy ra nếu máy ảo hết bộ nhớ có sẵn. Việc xử lý các lỗi như vậy không bị cấm, nhưng không được khuyến nghị.

 

Bạn biết gì về OutOfMemoryError?

OutOfMemoryError được ném ra khi Java virtual machine không thể tạo (allocate) một object do không đủ bộ nhớ, và garbage collector không thể giải phóng đủ.

Khu vực bộ nhớ bị chiếm bởi một quá trình Java bao gồm nhiều phần. Kiểu của OutOfMemoryError phụ thuộc vào phần nào đã hết dung lượng:

  • java.lang.OutOfMemoryError: Java heap space: Không đủ không gian trên heap, cụ thể là trong khu vực bộ nhớ nơi các object được tạo theo chương trình trong ứng dụng được đặt. Vấn đề thường nằm ở memory leak. Kích thước được đặt bởi các tham số -Xms-Xmx.
  • java.lang.OutOfMemoryError: PermGen space: (cho đến Java 8) Lỗi này xảy ra khi không đủ không gian trong khu vực Permanent, kích thước của nó được đặt bởi các tham số -XX:PermSize-XX:MaxPermSize.
  • java.lang.OutOfMemoryError: GC overhead limit exceeded: Lỗi này có thể xảy ra khi tràn cả khu vực đầu tiên và thứ hai. Nó liên quan đến việc còn ít bộ nhớ, và garbage collector liên tục hoạt động, cố gắng giải phóng một ít không gian. Lỗi này có thể bị tắt bằng tham số -XX:-UseGCOverheadLimit.
  • java.lang.OutOfMemoryError: unable to create new native thread: Được ném ra khi không thể tạo các luồng mới.

 

Mô tả hoạt động của khối _try-catch-finally_.

try – từ khóa này được sử dụng để đánh dấu sự bắt đầu của một khối mã có thể tiềm ẩn nguy cơ dẫn đến lỗi.

catch – từ khóa để đánh dấu sự bắt đầu của một khối mã được thiết kế để chặn và xử lý các ngoại lệ nếu chúng xảy ra.

finally – từ khóa để đánh dấu sự bắt đầu của một khối mã tùy chọn. Khối này được đặt sau khối catch cuối cùng. Luồng điều khiển sẽ chuyển đến khối finally trong mọi trường hợp, bất kể ngoại lệ có được ném ra hay không.

Cấu trúc chung để xử lý một tình huống ngoại lệ như sau:

try {
    // mã có thể tiềm ẩn nguy cơ dẫn đến tình huống ngoại lệ
}
catch(SomeException e ) { // chỉ định class của lỗi cụ thể mong muốn trong ngoặc đơn
    // mã xử lý tình huống ngoại lệ
}
finally {
    // khối tùy chọn, mã của nó luôn được thực thi
}

 

Cơ chế _try-with-resources_ là gì?

Cấu trúc này, xuất hiện trong Java 7, cho phép sử dụng một block try-catch mà không cần lo lắng về việc đóng các resource được sử dụng trong đoạn mã này.

Các resource được khai báo trong ngoặc đơn ngay sau try, và compiler ngầm định tạo một phần finally nơi các resource bị chiếm dụng trong block được giải phóng. Resource đề cập đến các thực thể triển khai interface java.lang.Autocloseable.

Cấu trúc chung:

try(/*khai báo resource*/) {
    //...
} catch(Exception ex) {
    //...
} finally {
    //...
}

Cần lưu ý rằng các block catchfinally rõ ràng thực thi sau khi các resource trong finally ngầm định được đóng.

 

Có thể sử dụng khối _try-finally_ (mà không có catch) không?

Cách viết như vậy là hợp lệ, nhưng không có nhiều ý nghĩa; tốt hơn vẫn nên có một khối catch nơi ngoại lệ cần thiết sẽ được xử lý.

 

Một khối catch có thể bắt nhiều ngoại lệ cùng lúc không?

Trong Java 7, một cấu trúc ngôn ngữ mới đã khả dụng, cho phép bắt nhiều ngoại lệ bằng một khối catch duy nhất:

try {
    //...
} catch(IOException | SQLException ex) {
    //...
}

 

Khối finally có luôn thực thi không?

Mã trong khối finally sẽ luôn được thực thi, bất kể ngoại lệ có được ném ra hay không.

 

Có tình huống nào mà khối finally sẽ không được thực thi không?

Ví dụ, khi JVM “chết” – trong tình huống như vậy, finally không thể truy cập được và sẽ không được thực thi, vì có sự thoát khỏi chương trình một cách bắt buộc của hệ thống:

try {
    System.exit(0);
} catch(Exception e) {
    e.printStackTrace();
} finally { }

 

Phương thức _main()_ có thể ném ngoại lệ ra bên ngoài không, và nếu có, ngoại lệ này sẽ được xử lý ở đâu?

Có, nó có thể, và nó sẽ được truyền đến Java virtual machine (JVM).

 

Giả sử có một phương thức có thể ném ra IOExceptionFileNotFoundException. Các khối catch nên theo thứ tự nào? Có bao nhiêu khối catch sẽ được thực thi?

Quy tắc chung: các ngoại lệ nên được xử lý từ “more specific” đến “more general”. Tức là, bạn không thể đặt catch(Exception ex) {} trong khối đầu tiên, nếu không tất cả các khối catch() tiếp theo sẽ không thể xử lý được gì, vì bất kỳ ngoại lệ nào cũng sẽ khớp với handler catch(Exception ex).

Do đó, với giả định rằng FileNotFoundException extends IOException, FileNotFoundException phải được xử lý trước, và sau đó là IOException:

void method() {
    try {
        //...
    } catch (FileNotFoundException ex) {
        //...
    } catch (IOException ex) {
        //...
    }
}

 

_Generics_ là gì?

Generics là một thuật ngữ kỹ thuật đề cập đến một tập hợp các tính năng ngôn ngữ cho phép định nghĩa và sử dụng parameterized types và methods. Parameterized types hoặc methods khác với loại thông thường ở chỗ chúng có type parameters.

Một ví dụ về việc sử dụng parameterized types là Java Collection Framework. Class LinkedList là một parameterized type điển hình. Nó chứa tham số E, đại diện cho kiểu của các phần tử sẽ được lưu trữ trong collection. Việc tạo các object của parameterized types được thực hiện bằng cách thay thế các parameterized types bằng actual data types. Thay vì chỉ sử dụng LinkedList mà không chỉ định kiểu phần tử trong list, người ta đề xuất sử dụng chỉ định kiểu cụ thể như LinkedList, LinkedList, v.v.

 

_«internationalization»_ và _«localization»_ là gì?

Internationalization là cách tạo các ứng dụng sao cho chúng có thể dễ dàng thích ứng cho các đối tượng khác nhau nói các ngôn ngữ khác nhau.

Localization là việc điều chỉnh interface ứng dụng cho nhiều ngôn ngữ. Việc thêm một ngôn ngữ mới có thể gây ra một số phức tạp cho localization interface.

Mục lục

Chỉ mục