Java Basic
Trang này chứa các mẹo và tài nguyên để chuẩn bị cho các cuộc phỏng vấn Java cơ bản.
Java là gì? Các đặc điểm của Java là gì?
Java là một ngôn ngữ lập trình cấp cao và độc lập nền tảng.
Java cũng là một ngôn ngữ hướng đối tượng. Tuy nhiên, mọi thứ (ngoại trừ các kiểu dữ liệu cơ bản) đều là đối tượng trong Java.
Độc lập nền tảng (Platform independent): Một chương trình duy nhất có thể chạy trên nhiều nền tảng khác nhau mà không cần sửa đổi.
Hiệu năng cao (High Performance): Trình biên dịch JIT (Just In Time compiler) cho phép Java đạt hiệu năng cao. JIT chuyển đổi mã bytecode thành mã máy và sau đó JVM bắt đầu thực thi.
Đa luồng (Multi-threaded): Chúng ta có thể viết các chương trình Java xử lý nhiều tác vụ cùng lúc bằng cách định nghĩa nhiều luồng.
Java không hỗ trợ đa kế thừa thông qua lớp. Điều này có thể đạt được thông qua các interface trong Java.
Bảo mật (Secured): Java được bảo mật vì nó không sử dụng con trỏ tường minh (explicit pointers).
Java là một ngôn ngữ lập trình mạnh mẽ vì nó sử dụng quản lý bộ nhớ mạnh mẽ. Các khái niệm như thu gom rác tự động (Automatic garbage collection).
2. Các khái niệm Lập trình Hướng đối tượng (OOP) là gì?
Các khái niệm OOP bao gồm:
Tính kế thừa (Inheritance)
Tính kế thừa có nghĩa là một lớp (class) có thể mở rộng từ một lớp khác. Nhờ đó, mã nguồn (code) có thể được tái sử dụng từ lớp này sang lớp khác. Lớp hiện có được gọi là lớp Cha (Super class), trong khi lớp được dẫn xuất gọi là lớp Con (Sub class). Tính kế thừa chỉ áp dụng cho các thành viên công khai (public) và được bảo vệ (protected). Các thành viên riêng tư (private) không thể được kế thừa.
Tính đóng gói (Encapsulation)
Bảo vệ mã nguồn khỏi sự truy cập từ bên ngoài (bảo vệ dữ liệu). Tăng khả năng bảo trì mã nguồn. Để thực hiện đóng gói, chúng ta cần đặt tất cả các biến thể hiện (instance variables) thành
private
và tạo các phương thứcsetter
vàgetter
cho các biến đó. Điều này sẽ buộc các đối tượng khác phải gọi các phương thứcsetter
/getter
thay vì truy cập trực tiếp vào dữ liệu.
Tính đa hình (Polymorphism)
Một đối tượng duy nhất có thể tham chiếu đến lớp Cha hoặc lớp Con tùy thuộc vào kiểu tham chiếu, đó được gọi là tính đa hình. Tính đa hình được áp dụng thông qua việc ghi đè phương thức (method overriding).
Tính trừu tượng (Abstraction)
Tính trừu tượng trong lập trình hướng đối tượng có nghĩa là che giấu các chi tiết phức tạp bên trong, chỉ hiển thị ra các đặc tính và hành vi cần thiết phù hợp với ngữ cảnh sử dụng.
Giao diện (Interface)
Đa kế thừa (Multiple inheritance - kế thừa từ nhiều lớp cha) không thể thực hiện được trong Java. Để khắc phục vấn đề này, khái niệm Interface được giới thiệu. Interface là một khuôn mẫu (template) chỉ chứa các khai báo phương thức (method declarations) mà không có phần triển khai phương thức (method implementation).
3. Các phạm vi truy cập khác nhau trong Java là gì?
Bộ truy cập mặc định (Default):
Được áp dụng khi không có từ khóa chỉ định truy cập nào được sử dụng.
Các thành viên dữ liệu, lớp và phương thức được truy cập trong cùng gói.
Bộ truy cập riêng tư (Private):
Được đánh dấu bằng từ khóa
private
.Chỉ được truy cập trong phạm vi lớp, và không thể truy cập bởi các lớp khác, ngay cả trong cùng gói.
Bộ truy cập được bảo vệ (Protected):
Có thể được truy cập trong cùng gói hoặc bởi các lớp con từ các gói khác.
Bộ truy cập công khai (Public):
Có thể được truy cập từ mọi nơi.
4. Ý nghĩa của SOLID là gì?
SOLID đại diện cho năm nguyên tắc thiết kế hướng đối tượng trong Java, giúp tạo ra phần mềm dễ bảo trì, mở rộng và linh hoạt:
S: Nguyên tắc đơn nhiệm (Single Responsibility Principle):
Một lớp chỉ nên có một và chỉ một trách nhiệm duy nhất.
O: Nguyên tắc mở đóng (Open-Closed Principle):
Các thành phần phần mềm nên mở cho việc mở rộng, nhưng đóng cho việc sửa đổi. Ví dụ, trình duyệt là một ví dụ điển hình về chức năng mở rộng nhưng đóng cho việc sửa đổi.
L: Nguyên tắc thay thế Liskov (Liskov Substitution Principle):
Các kiểu dữ liệu con phải có khả năng thay thế hoàn toàn cho các kiểu dữ liệu cha của chúng.
I: Nguyên tắc phân tách giao diện (Interface Segregation Principle):
Client không nên bị ép buộc phải triển khai các phương thức không cần thiết mà họ sẽ không sử dụng.
D: Nguyên tắc đảo ngược phụ thuộc (Dependency Inversion Principle):
Phụ thuộc vào các trừu tượng, không phụ thuộc vào các cài đặt cụ thể. Theo nguyên tắc này, module cấp cao không bao giờ được phụ thuộc vào module cấp thấp. Ví dụ, khi bạn đến cửa hàng địa phương để mua một món đồ và quyết định thanh toán bằng thẻ ghi nợ, bạn đưa thẻ cho nhân viên thu ngân. Nhân viên không cần quan tâm đến loại thẻ bạn đưa. Loại thẻ tín dụng hoặc thẻ ghi nợ bạn dùng để thanh toán không quan trọng; họ chỉ cần quẹt thẻ.
5. Dung lượng của các kiểu dữ liệu?
byte - 1 byte (8 bits). Min -2^7 Max 2^7-1
short - 2 byte (16 bits). Min -2^15 Max 2^15-1
char - 2 byte (16 bits). 2^16-1
int - 4 byte (32 bits). Min -2^31-1 Max 2^31
long - 8 byte (64 bits). Min -2^63-1 Max 2^63
float - 4 byte (32 bits). Min -2^31-1 Max 2^31
double - 8 byte (64 bits). Min -2^63-1 Max 2^63
boolean - NA usualy 1 byte
6. Thứ tự thực thi của các khối khởi tạo và hàm tạo trong Java:
Khối khởi tạo tĩnh (Static initialization blocks):
Sẽ chạy khi lớp được tải lần đầu tiên vào JVM.
Khối khởi tạo (Initialization blocks):
Chạy theo đúng thứ tự xuất hiện trong chương trình.
Khối khởi tạo đối tượng (Instance Initialization blocks):
Được thực thi mỗi khi lớp được khởi tạo và trước khi các hàm tạo được gọi.
Thông thường được đặt phía trên các hàm tạo trong dấu ngoặc nhọn
{}
.
7. Sự khác biệt giữa Biểu thức Lambda và Closure trong Java:
Java hỗ trợ biểu thức Lambda nhưng không hỗ trợ Closure.
Biểu thức Lambda là một hàm ẩn danh và có thể được định nghĩa như một tham số.
Closure là các đoạn mã hoặc khối mã có thể được sử dụng mà không cần phải là một phương thức hoặc một lớp. Điều này có nghĩa là Closure có thể truy cập các biến không được định nghĩa trong danh sách tham số của nó và cũng có thể gán nó cho một biến.
8. Danh sách các phương thức của lớp Object trong Java:
clone()
- Tạo và trả về một bản sao của đối tượng này.equals()
- Cho biết liệu một đối tượng khác có "bằng" đối tượng này hay không.finalize()
- Được trình thu gom rác gọi trên một đối tượng khi trình thu gom rác xác định rằng không còn tham chiếu nào đến đối tượng đó.getClass()
- Trả về lớp thời gian chạy của một đối tượng.hashCode()
- Trả về giá trị mã băm cho đối tượng.notify()
- Đánh thức một luồng đơn lẻ đang chờ trên monitor của đối tượng này.notifyAll()
- Đánh thức tất cả các luồng đang chờ trên monitor của đối tượng này.toString()
- Trả về biểu diễn chuỗi của đối tượng.wait()
- Làm cho luồng hiện tại chờ cho đến khi một luồng khác gọi phương thứcnotify()
hoặc phương thứcnotifyAll()
cho đối tượng này.
9. Sự khác biệt giữa atomic, volatile, synchronized là gì?
Để hiểu rõ sự khác biệt, ta cần xem xét vấn đề đồng bộ hóa trong môi trường đa luồng.
Vấn đề:
Trong môi trường đa luồng, các thao tác như tăng giá trị biến (
i++
) có thể gặp vấn đề về race condition và tính khả kiến (visibility).Các luồng có thể đọc giá trị cùng lúc, dẫn đến việc ghi đè lẫn nhau.
Giá trị có thể được lưu trữ trong bộ nhớ cache cục bộ của CPU, không được cập nhật ngay lập tức cho các CPU/luồng khác.
volatile:
Khai báo biến
volatile
đảm bảo rằng việc sửa đổi giá trị sẽ ảnh hưởng trực tiếp đến bộ nhớ chính.Trình biên dịch không thể tối ưu hóa các tham chiếu đến biến này.
Điều này đảm bảo rằng khi một luồng sửa đổi biến, tất cả các luồng khác sẽ thấy giá trị mới ngay lập tức.
volatile
chỉ đảm bảo tính khả kiến, không đảm bảo tính nguyên tử của thao tác.
AtomicInteger (và các lớp Atomic khác):
Lớp
AtomicInteger
sử dụng các thao tác CPU cấp thấp CAS (compare-and-swap), không cần đồng bộ hóa.Khai báo biến atomic đảm bảo rằng các thao tác trên biến xảy ra một cách nguyên tử, tức là tất cả các bước con của thao tác được hoàn thành trong luồng mà chúng được thực thi và không bị gián đoạn bởi các luồng khác.
AtomicInteger
đảm bảo tính nguyên tử của một số thao tác nhất định, và cũng đảm bảo tính khả kiến.
synchronized:
Đồng bộ hóa tất cả các truy cập vào một biến chỉ cho phép một luồng truy cập biến tại một thời điểm và buộc tất cả các luồng khác phải chờ luồng truy cập đó giải phóng quyền truy cập vào biến.
Đồng bộ hóa xảy ra trên một đối tượng.
Gọi một phương thức
synchronized
của một lớp sẽ khóa đối tượngthis
của lệnh gọi.Các phương thức
synchronized
tĩnh sẽ khóa chính đối tượngClass
.Tương tự, việc vào một khối
synchronized
yêu cầu khóa đối tượngthis
của phương thức.synchronized
đảm bảo tính nguyên tử và tính khả kiến, nhưng có thể gây ra hiệu suất kém hơn so vớivolatile
vàAtomicInteger
.Một phương thức (hoặc khối)
synchronized
có thể được thực thi trong nhiều luồng cùng lúc nếu chúng khóa trên các đối tượng khác nhau, nhưng chỉ một luồng có thể thực thi một phương thức (hoặc khối)synchronized
tại một thời điểm cho bất kỳ đối tượng đơn lẻ nào.
Tóm tắt:
volatile
: Đảm bảo tính khả kiến, không đảm bảo tính nguyên tử.Atomic
: Đảm bảo tính nguyên tử và tính khả kiến cho các thao tác đơn lẻ.synchronized
: Đảm bảo cả tính nguyên tử và tính khả kiến, nhưng có thể gây ra hiệu suất kém hơn.
10. Giải thích về quá trình hoàn thiện đối tượng (Object Finalization):
Quá trình hoàn thiện đối tượng (object finalization) trong Java là một cơ chế cho phép một đối tượng thực hiện các thao tác dọn dẹp cuối cùng trước khi bị trình thu gom rác (garbage collector) thu hồi.
Khi nào nó được gọi:
Phương thức
finalize()
của một đối tượng được gọi bởi trình thu gom rác khi nó xác định rằng không còn tham chiếu nào đến đối tượng đó nữa.Điều này có nghĩa là đối tượng không thể truy cập được từ bất kỳ đối tượng đang hoạt động nào trong chương trình.
Khi một đối tượng không thể được truy cập từ bất kỳ đối tượng đang hoạt động nào, điều đó có nghĩa là nó có thể được thu gom rác một cách an toàn.
Mục đích:
Mục đích chính của
finalize()
là cho phép đối tượng giải phóng các tài nguyên không phải Java (native resources), chẳng hạn như kết nối cơ sở dữ liệu hoặc xử lý tệp, mà trình thu gom rác không thể tự động quản lý.Tuy nhiên, việc sử dụng
finalize()
không được khuyến khích vì nó có thể gây ra các vấn đề về hiệu suất và tính không thể đoán trước.
Lưu ý quan trọng:
Không có gì đảm bảo rằng
finalize()
sẽ được gọi cho mọi đối tượng.Trình thu gom rác có thể chọn không gọi
finalize()
nếu hệ thống đang chịu tải nặng.Việc thực thi
finalize()
có thể bị trì hoãn, điều này có thể dẫn đến việc giải phóng tài nguyên bị trì hoãn.Kể từ java 9, finalize method has been deprecated.
Thay vào đó, sử dụng try-with-resources để đảm bảo các tài nguyên được giải phóng đúng cách.
Tóm lại, finalize()
là một cơ chế để dọn dẹp tài nguyên trước khi đối tượng bị thu gom rác, nhưng việc sử dụng nó nên được tránh nếu có thể.
11. Serialization trong Java là gì?
Serialization (tuần tự hóa) đối tượng trong Java là quá trình chuyển đổi một đối tượng thành định dạng nhị phân, cho phép lưu trữ đối tượng trên đĩa hoặc gửi qua mạng đến một máy ảo Java (JVM) đang chạy khác. Quá trình ngược lại, tạo đối tượng từ luồng nhị phân, được gọi là deserialization (giải tuần tự hóa) trong Java.
Java cung cấp API Serialization để tuần tự hóa và giải tuần tự hóa đối tượng, bao gồm java.io.Serializable
, java.io.Externalizable
, ObjectInputStream
và ObjectOutputStream
, v.v.
Serializable
là interface đánh dấu (marker interface).Interface đánh dấu trong Java là interface không có trường hoặc phương thức nào, hay nói cách khác, interface trống trong Java được gọi là interface đánh dấu.
Làm cho một lớp có thể tuần tự hóa (Serializable) trong Java rất dễ dàng.
Lớp Java của bạn chỉ cần triển khai interface
java.io.Serializable
và JVM sẽ lo việc tuần tự hóa các đối tượng theo định dạng mặc định.
serialVersionUID
:Nếu bạn không khai báo rõ ràng
serialVersionUID
, JVM sẽ tạo nó dựa trên cấu trúc của lớp, phụ thuộc vào các interface mà lớp triển khai và một số yếu tố khác có thể thay đổi.serialVersionUID
được sử dụng để đảm bảo rằng cùng một lớp (được sử dụng trong quá trình tuần tự hóa) được tải trong quá trình giải tuần tự hóa.serialVersionUID
được sử dụng để kiểm soát phiên bản của đối tượng.
Tùy chỉnh quá trình tuần tự hóa:
Bạn có thể tùy chỉnh quá trình tuần tự hóa bằng cách định nghĩa các phương thức
writeObject
vàreadObject
.
Loại trừ biến khỏi quá trình tuần tự hóa:
Để tránh tuần tự hóa một số biến, bạn có thể đánh dấu biến đó là
static
hoặctransient
.
Externalizable
interface:Interface
Externalizable
có nhiều quyền kiểm soát hơn đối với quá trình tuần tự hóa và bắt buộc phải ghi đè các phương thứcwriteExternal
vàreadExternal
.
Tóm lại, Serialization trong Java là một cơ chế cho phép chuyển đổi đối tượng thành dạng nhị phân để lưu trữ hoặc truyền tải, và Deserialization là quá trình ngược lại.
12. Khái niệm nhân bản đối tượng (Cloning) trong Java:
Java hỗ trợ hai loại nhân bản: nhân bản nông (shallow cloning) và nhân bản sâu (deep cloning). Theo mặc định, Java sử dụng nhân bản nông. Lớp Object
có một phương thức clone()
thực hiện nhân bản nông.
Nhân bản nông (Shallow Cloning):
Nhân bản nông sao chép càng ít càng tốt. Việc sử dụng bộ nhớ thấp hơn.
Nếu lớp chỉ có các thành viên kiểu dữ liệu nguyên thủy, thì một bản sao hoàn toàn mới của đối tượng sẽ được tạo và tham chiếu đến bản sao đối tượng mới sẽ được trả về.
Nếu lớp chứa các thành viên thuộc bất kỳ kiểu lớp nào, thì chỉ các tham chiếu đối tượng đến các thành viên đó được sao chép, do đó, các tham chiếu thành viên trong cả đối tượng gốc cũng như đối tượng được nhân bản đều tham chiếu đến cùng một đối tượng.
Nhân bản sâu (Deep Cloning):
Nhân bản sâu là bản sao của chính đối tượng. Một bộ nhớ mới được cấp phát cho đối tượng và nội dung được sao chép. Khi một bản sao sâu của đối tượng được thực hiện, các tham chiếu mới được tạo.
Một giải pháp là chỉ cần triển khai phương thức tùy chỉnh của riêng bạn (ví dụ:
deepCopy()
) trả về một bản sao sâu của một thể hiện của một trong các lớp của bạn.Giải pháp phổ biến khác cho vấn đề sao chép sâu là sử dụng Tuần tự hóa đối tượng Java (Java Object Serialization).
Tóm lại, nhân bản nông chỉ sao chép các tham chiếu, trong khi nhân bản sâu sao chép toàn bộ đối tượng, bao gồm cả các đối tượng được tham chiếu.
13. StringBuffer vs StringBuilder:
StringBuffer:
Được đồng bộ hóa (synchronized), nghĩa là nó an toàn luồng (thread-safe).
Điều này có nghĩa là nhiều luồng có thể thao tác trên một đối tượng
StringBuffer
cùng một lúc mà không gây ra vấn đề về tính nhất quán dữ liệu.Do cơ chế đồng bộ hóa, hiệu suất của
StringBuffer
chậm hơn so vớiStringBuilder
.
StringBuilder:
Không được đồng bộ hóa (unsynchronized), nghĩa là nó không an toàn luồng (not thread-safe).
Điều này có nghĩa là nếu nhiều luồng thao tác trên một đối tượng
StringBuilder
cùng một lúc, có thể xảy ra vấn đề về tính nhất quán dữ liệu.Vì không có cơ chế đồng bộ hóa, hiệu suất của
StringBuilder
nhanh hơnStringBuffer
.
Tóm lại:
Nếu bạn cần thao tác chuỗi trong môi trường đa luồng, hãy sử dụng
StringBuffer
.Nếu bạn chỉ thao tác chuỗi trong một luồng đơn lẻ hoặc không quan tâm đến vấn đề đồng bộ hóa, hãy sử dụng
StringBuilder
để có hiệu suất tốt hơn.
14. Kể tên một số trình phân tích cú pháp thường được sử dụng để phân tích cú pháp tài liệu XML.
DOM Parser (Document Object Model Parser):
Phân tích cú pháp tài liệu bằng cách tải toàn bộ nội dung của tài liệu và tạo cây phân cấp hoàn chỉnh của tài liệu trong bộ nhớ.
Sử dụng DOM parser, chúng ta có thể phân tích cú pháp, sửa đổi hoặc tạo tài liệu XML.
DOM phù hợp với các tài liệu XML nhỏ và trung bình, khi cần truy cập ngẫu nhiên vào các phần của tài liệu.
SAX Parser (Simple API for XML Parser):
Phân tích cú pháp tài liệu dựa trên các sự kiện kích hoạt.
Không tải toàn bộ tài liệu vào bộ nhớ.
Phù hợp khi xử lý các tài liệu XML rất lớn mà cây DOM sẽ tiêu thụ quá nhiều bộ nhớ.
Chúng ta không có quyền truy cập ngẫu nhiên vào tài liệu XML vì nó được xử lý theo hướng chỉ tiến (forward-only).
Sử dụng SAX parser, chúng ta chỉ có thể phân tích cú pháp hoặc sửa đổi tài liệu XML.
StAX Parser (Streaming API for XML Parser):
Phân tích cú pháp tài liệu theo cách tương tự như SAX parser, nhưng StAX là API kéo (PULL API) trong khi SAX là API đẩy (PUSH API).
Điều này có nghĩa là trong trường hợp StAX parser, ứng dụng client cần yêu cầu StAX parser lấy thông tin từ XML bất cứ khi nào cần, nhưng trong trường hợp SAX parser, ứng dụng client được yêu cầu lấy thông tin khi SAX parser thông báo cho ứng dụng client rằng thông tin có sẵn.
Sử dụng StAX parser, chúng ta có thể phân tích cú pháp, sửa đổi và tạo tài liệu XML.
StAX cung cấp sự kiểm soát tốt hơn so với SAX, vì ứng dụng client có thể kiểm soát luồng phân tích cú pháp.
Tóm lại:
DOM: Tải toàn bộ tài liệu vào bộ nhớ, phù hợp cho tài liệu nhỏ và trung bình, truy cập ngẫu nhiên.
SAX: Phân tích cú pháp dựa trên sự kiện, không tải toàn bộ tài liệu vào bộ nhớ, phù hợp cho tài liệu lớn, chỉ tiến.
StAX: Giống SAX nhưng là API kéo, cho phép nhiều kiểm soát hơn SAX, và phù hợp với việc phân tích các tài liệu lớn.
15. Những tính năng mới nào được thêm vào Java 8?
Java 8 đi kèm với một số tính năng mới, nhưng những tính năng quan trọng nhất là:
Biểu thức Lambda (Lambda Expressions):
Một tính năng ngôn ngữ mới cho phép chúng ta coi các hành động như các đối tượng.
Tham chiếu phương thức (Method References):
Cho phép chúng ta định nghĩa Biểu thức Lambda bằng cách tham chiếu trực tiếp đến các phương thức bằng tên của chúng.
Optional:
Lớp wrapper đặc biệt được sử dụng để thể hiện tính tùy chọn (optionality).
Functional Interface (Giao diện hàm):
Một interface với tối đa một phương thức trừu tượng; việc triển khai có thể được cung cấp bằng Biểu thức Lambda.
Phương thức mặc định (Default methods):
Cho phép chúng ta thêm các triển khai đầy đủ trong các interface bên cạnh các phương thức trừu tượng.
Nashorn, JavaScript Engine:
Công cụ dựa trên Java để thực thi và đánh giá mã JavaScript.
Stream API:
Một lớp iterator đặc biệt cho phép chúng ta xử lý các tập hợp đối tượng theo cách hàm (functional).
Date API:
Một API Date được cải thiện, bất biến, lấy cảm hứng từ JodaTime.
16. Những tính năng mới nào được thêm vào Java 11?
Java 11 đi kèm với một số tính năng mới, trong đó những tính năng quan trọng nhất là:
Các phương thức mới cho lớp String:
isBlank()
: Kiểm tra xem chuỗi có trống hoặc chỉ chứa các ký tự khoảng trắng hay không.lines()
: Trả về một luồng các chuỗi, được chia bởi các dấu ngắt dòng.strip()
: Loại bỏ khoảng trắng ở đầu và cuối chuỗi.stripLeading()
: Loại bỏ khoảng trắng ở đầu chuỗi.stripTrailing()
: Loại bỏ khoảng trắng ở cuối chuỗi.repeat(int count)
: Lặp lại chuỗi một số lần nhất định.
Các phương thức File mới:
readString()
vàwriteString()
: Các phương thức tĩnh từ lớpFiles
để đọc và ghi chuỗi từ/vào tệp.
HTTP Client mới:
Cải thiện hiệu suất tổng thể và hỗ trợ cả HTTP/1.1 và HTTP/2.
Hệ thống Mô-đun (Modular System):
Được giới thiệu từ Java 9, nhưng tiếp tục được cải thiện trong Java 11.
17. Biểu thức Lambda là gì và nó được sử dụng để làm gì?
Nói một cách đơn giản, biểu thức Lambda là một hàm mà chúng ta có thể tham chiếu và truyền xung quanh như một đối tượng.
Đặc điểm:
Biểu thức Lambda giới thiệu phong cách xử lý hàm (functional style processing) trong Java, giúp viết mã gọn gàng và dễ đọc hơn.
Do đó, biểu thức Lambda là sự thay thế tự nhiên cho các lớp ẩn danh (anonymous classes), chẳng hạn như các đối số phương thức.
Sử dụng:
Một trong những công dụng chính của chúng là định nghĩa các triển khai nội tuyến (inline implementations) của các giao diện hàm (functional interfaces).
Tóm lại, Biểu thức Lambda giúp viết mã ngắn gọn hơn, đặc biệt khi làm việc với các hàm xử lý dữ liệu như Stream API, và thường được sử dụng để thay thế các lớp ẩn danh.
18. Exception là gì?
Exception (ngoại lệ) là một sự kiện bất thường xảy ra trong quá trình thực thi chương trình và làm gián đoạn luồng thực thi bình thường của các lệnh chương trình.
throws
keyword:Từ khóa
throws
được sử dụng để chỉ định rằng một phương thức có thể phát sinh ngoại lệ trong quá trình thực thi của nó.
throw
keyword:Từ khóa
throw
cho phép chúng ta ném một đối tượng ngoại lệ để làm gián đoạn luồng thực thi bình thường của chương trình.
Xử lý ngoại lệ bằng
try-catch-finally
:Chúng ta có thể xử lý ngoại lệ bằng cách sử dụng câu lệnh
try-catch-finally
.Khối mã mà trong đó một ngoại lệ có thể xảy ra được đặt trong khối
try
. Khối này còn được gọi là mã "được bảo vệ" (protected) hoặc "được canh giữ" (guarded).Nếu một ngoại lệ xảy ra, khối
catch
phù hợp với ngoại lệ đang được ném sẽ được thực thi, nếu không, tất cả các khốicatch
sẽ bị bỏ qua.Khối
finally
luôn được thực thi sau khi khốitry
thoát ra, bất kể có ngoại lệ nào được ném bên trong nó hay không.
Checked vs Unchecked Exception:
Một ngoại lệ được kiểm tra (checked exception) phải được xử lý trong khối
try-catch
hoặc được khai báo trong mệnh đềthrows
; trong khi một ngoại lệ không được kiểm tra (unchecked exception) không bắt buộc phải được xử lý hoặc khai báo.
Exception vs Error:
Một ngoại lệ (exception) là một sự kiện đại diện cho một điều kiện mà từ đó có thể phục hồi, trong khi lỗi (error) đại diện cho một tình huống bên ngoài thường không thể phục hồi.
Tóm lại, Exception là cơ chế của Java để xử lý các sự kiện bất thường trong quá trình chạy chương trình, cho phép chương trình tiếp tục thực thi một cách an toàn.
19. Làm thế nào để tạo ra OutOfMemoryError và StackOverflowException?
OutOfMemoryError (Lỗi hết bộ nhớ):
Đoạn mã này tạo ra một
LinkedList
và liên tục thêm các mảnglong
lớn vào danh sách.Do vòng lặp vô hạn, bộ nhớ heap sẽ dần đầy lên, dẫn đến
OutOfMemoryError
.Adding Thread.sleep() within the loop, will slow down the process, so that the user can observe the memory rising slowly.
StackOverflowException (Lỗi tràn ngăn xếp):
Đoạn mã này định nghĩa một phương thức đệ quy
recursivePrint
mà không có điều kiện dừng thích hợp.Mỗi lần phương thức được gọi đệ quy, một khung ngăn xếp mới được tạo.
Vì không có điều kiện dừng, ngăn xếp cuộc gọi sẽ dần đầy lên, dẫn đến
StackOverflowException
.
Giải thích:
OutOfMemoryError:
Xảy ra khi JVM không thể cấp phát bộ nhớ cho một đối tượng mới vì không còn đủ bộ nhớ heap.
Nguyên nhân có thể là do rò rỉ bộ nhớ, kích thước heap quá nhỏ hoặc tạo ra quá nhiều đối tượng lớn.
StackOverflowException:
Xảy ra khi ngăn xếp cuộc gọi bị tràn do quá nhiều lệnh gọi phương thức đệ quy hoặc lệnh gọi phương thức sâu.
Mỗi lệnh gọi phương thức sẽ chiếm một phần bộ nhớ trên ngăn xếp.
When a program has a very deep recursive call, or a recursive call that never ends, the stack memory will be full.
20. Annotations là gì? Các trường hợp sử dụng điển hình của chúng là gì?
Annotations (chú thích) là siêu dữ liệu (metadata) được gắn với các phần tử của mã nguồn của một chương trình và không ảnh hưởng đến hoạt động của mã mà chúng tác động.
Các trường hợp sử dụng điển hình:
Thông tin cho trình biên dịch (Information for the compiler): Với chú thích, trình biên dịch có thể phát hiện lỗi hoặc bỏ qua cảnh báo.
Xử lý thời gian biên dịch và thời gian triển khai (Compile-time and deployment-time processing): Các công cụ phần mềm có thể xử lý chú thích và tạo mã, tệp cấu hình, v.v.
Xử lý thời gian chạy (Runtime processing): Chú thích có thể được kiểm tra tại thời gian chạy để tùy chỉnh hành vi của một chương trình.
Định nghĩa Annotation:
Annotations là một dạng interface, trong đó từ khóa
interface
được đặt trước bởi@
, và phần thân của nó chứa các khai báo phần tử kiểu chú thích trông rất giống với các phương thức:
Sử dụng Annotation:
Sau khi chú thích được định nghĩa, bạn có thể bắt đầu sử dụng nó trong mã của mình:
Tóm lại, Annotations cung cấp một cách để thêm siêu dữ liệu vào mã Java, giúp cải thiện khả năng đọc, bảo trì và mở rộng của mã. Chúng được sử dụng rộng rãi trong các framework và thư viện Java để cấu hình, kiểm tra và xử lý mã.
21. Lớp bất biến (immutable class) là gì?
Các đối tượng bất biến là những đối tượng mà trạng thái của chúng không thể thay đổi sau khi được tạo. Lớp mà các đối tượng của nó có đặc điểm này có thể được gọi là lớp bất biến. Ví dụ: String
, Integer
.
Tính an toàn luồng (thread safety):
Các lớp bất biến an toàn luồng vì bạn không thể thay đổi trạng thái của các đối tượng bất biến.
Do đó, ngay cả khi hai luồng truy cập đối tượng bất biến song song, nó cũng sẽ không tạo ra bất kỳ vấn đề nào.
Các bước để tạo một lớp bất biến trong Java:
Khai báo lớp là
final
: Để ngăn lớp không thể được kế thừa.Đặt tất cả các trường là
private
: Để không cho phép truy cập trực tiếp.Không cung cấp các phương thức setter cho các biến: Để ngăn thay đổi trạng thái.
Đặt tất cả các trường có thể thay đổi (mutable) là
final
: Để giá trị của chúng chỉ có thể được gán một lần.Khởi tạo tất cả các trường thông qua một hàm tạo (constructor) thực hiện sao chép sâu (deep copy): Để đảm bảo rằng các đối tượng nội bộ cũng là bất biến.
Thực hiện sao chép (cloning) các đối tượng trong các phương thức getter để trả về một bản sao thay vì trả về tham chiếu đối tượng thực tế: Để ngăn chặn các thay đổi từ bên ngoài ảnh hưởng đến trạng thái bên trong của đối tượng.
Tóm lại, lớp bất biến là một lớp mà các đối tượng của nó không thể thay đổi sau khi được tạo. Điều này giúp đảm bảo tính an toàn luồng và đơn giản hóa việc lập trình trong môi trường đa luồng.
Khi sử dụng annotation trong Java, quy trình thực thi có thể khác nhau tùy thuộc vào mục đích của annotation và cách chúng được xử lý. Tuy nhiên, có một số bước chung liên quan đến việc xử lý annotation:
1. Định nghĩa Annotation:
Annotation được định nghĩa bằng cú pháp
@interface
, tương tự như định nghĩa interface thông thường.Annotation có thể chứa các phần tử (elements) giống như các phương thức trong interface, nhưng chúng được sử dụng để định nghĩa các thuộc tính của annotation.
2. Áp dụng Annotation:
Annotation được áp dụng cho các phần tử mã nguồn như lớp, phương thức, trường, tham số, v.v.
Cú pháp áp dụng annotation là
@AnnotationName(attribute1 = value1, attribute2 = value2, ...)
.
3. Xử lý Annotation:
Cách annotation được xử lý phụ thuộc vào chính sách lưu giữ (retention policy) của nó:
SOURCE: Annotation chỉ tồn tại trong mã nguồn và bị loại bỏ bởi trình biên dịch.
CLASS: Annotation được lưu trữ trong tệp lớp đã biên dịch, nhưng không khả dụng tại thời gian chạy.
RUNTIME: Annotation được lưu trữ trong tệp lớp đã biên dịch và khả dụng tại thời gian chạy.
Xử lý tại thời gian biên dịch:
Trình biên dịch có thể sử dụng annotation để kiểm tra mã nguồn, tạo mã bổ sung hoặc tạo tệp cấu hình.
Các công cụ xử lý annotation (annotation processors) có thể được sử dụng để xử lý annotation tại thời gian biên dịch.
Xử lý tại thời gian chạy:
Reflection API có thể được sử dụng để truy xuất annotation tại thời gian chạy.
Các thư viện và framework có thể sử dụng annotation để cấu hình, kiểm tra hoặc sửa đổi hành vi của chương trình.
4. Sử dụng Annotation:
Annotation có thể được sử dụng cho nhiều mục đích khác nhau, bao gồm:
Cung cấp thông tin cho trình biên dịch.
Tạo mã hoặc tệp cấu hình.
Tùy chỉnh hành vi của chương trình tại thời gian chạy.
Kiểm tra mã nguồn.
Ví dụ về quy trình thực thi với Annotation @Override:
Định nghĩa:
@Override
là một annotation tích hợp sẵn trong Java.
Áp dụng:
@Override
được áp dụng cho một phương thức trong lớp con để chỉ ra rằng phương thức đó ghi đè một phương thức từ lớp cha.
Xử lý:
Trình biên dịch Java sử dụng
@Override
để kiểm tra xem phương thức được chú thích có thực sự ghi đè một phương thức từ lớp cha hay không.Nếu không, trình biên dịch sẽ báo lỗi.
Sử dụng:
@Override
giúp đảm bảo rằng các phương thức ghi đè được khai báo chính xác, tránh lỗi do sai sót trong quá trình đặt tên phương thức.
Tóm lại, quy trình thực thi annotation có thể khác nhau, nhưng nó thường liên quan đến việc định nghĩa, áp dụng, xử lý và sử dụng annotation để đạt được mục đích mong muốn.
Last updated