Java Strings
Các câu hỏi phỏng vấn Java String được đưa ra bao gồm từ các phương thức chuỗi, tính bất biến của chuỗi và các vấn đề rò rỉ bộ nhớ cho đến các ví dụ và trường hợp sử dụng đơn giản.
Chuỗi (String) có phải là từ khóa trong Java không?
Không. Chuỗi (String) không phải là từ khóa dự trữ trong Java. Nó là một kiểu dữ liệu dẫn xuất, tức là nó là một lớp (class).
Tại sao Chuỗi (String) lại bất biến?
Tất cả chúng ta đều biết rằng các chuỗi trong Java là bất biến. Nếu bạn muốn biết bất biến là gì và nó được thực hiện như thế nào, hãy theo dõi bài viết này: Làm thế nào để tạo một lớp Java bất biến?
Câu hỏi ở đây là TẠI SAO? Tại sao lại bất biến? Hãy cùng phân tích.
Lý do đầu tiên là để cải thiện hiệu suất. Ngôn ngữ Java được phát triển nhằm tăng tốc độ phát triển ứng dụng, vì nó không thực sự nhanh trong các ngôn ngữ trước đó. Các nhà thiết kế JVM chắc hẳn đủ thông minh để nhận ra rằng các ứng dụng thực tế sẽ chủ yếu bao gồm các chuỗi dưới dạng nhãn, thông điệp, cấu hình, đầu ra và các dạng khác. Nhìn thấy sự sử dụng quá mức như vậy, họ đã hình dung ra việc sử dụng chuỗi không đúng cách có thể nguy hiểm như thế nào. Vì vậy, họ đã đưa ra khái niệm về String pool (vùng nhớ đệm chuỗi). Vùng nhớ đệm chuỗi không gì khác ngoài một tập hợp các chuỗi phần lớn là duy nhất. Ý tưởng cơ bản đằng sau String pool là tái sử dụng chuỗi một khi nó đã được tạo. Bằng cách này, nếu một chuỗi cụ thể được tạo 20 lần trong mã, ứng dụng chỉ có một thể hiện duy nhất.
Lý do thứ hai tôi nhận thấy là các vấn đề bảo mật. Chuỗi là loại tham số được sử dụng nhiều nhất trong mọi khía cạnh của lập trình Java. Dù là tải một trình điều khiển (driver) hay mở một kết nối URL, bạn truyền thông tin dưới dạng chuỗi. Nếu chuỗi không được thiết kế là bất biến (final), chúng đã mở ra một hộp Pandora chứa đầy các vấn đề bảo mật.
String Constant Pool là gì?
Vùng nhớ đệm chuỗi hằng (String Constant Pool) là một khu vực bộ nhớ đặc biệt trong bộ nhớ heap thông thường, nơi các chuỗi hằng số được lưu trữ. Các hằng số này được tham chiếu bởi các biến chuỗi trong suốt vòng đời của ứng dụng.
Trong Java, chúng ta có thể tạo một chuỗi theo nhiều cách. Ví dụ, sử dụng chuỗi ký tự (string literals) hoặc sử dụng từ khóa new
.
Khi sử dụng chuỗi ký tự (literal), JVM sẽ kiểm tra xem đã có chuỗi “abc” (cùng dãy ký tự) tồn tại hay chưa. Nếu chuỗi đó đã tồn tại, JVM sẽ gán tham chiếu của đối tượng hiện có cho biến str1
; ngược lại, một đối tượng “abc” mới sẽ được tạo, và tham chiếu của nó sẽ được gán cho biến str1
.
Khi sử dụng từ khóa new
, Java sẽ tạo một đối tượng String
mới trong bộ nhớ heap thông thường. Chúng ta phải sử dụng phương thức intern()
để chuyển chuỗi từ bộ nhớ heap sang String Constant Pool.
Để tối ưu hóa việc sử dụng bộ nhớ và cải thiện hiệu suất tổng thể, việc sử dụng chuỗi ký tự (string literals) để tạo chuỗi được khuyến nghị. Trừ khi cần một bản sao rõ ràng của chuỗi ban đầu, việc sử dụng hàm tạo (constructor) là không cần thiết vì chuỗi trong Java là bất biến.
Phương thức String intern()
là gì?
String intern()
là gì?
Khi phương thức intern()
của chuỗi được gọi, nếu String Constant Pool đã chứa một chuỗi có cùng nội dung với chuỗi này (được xác định bởi phương thức equals()
), thì tham chiếu từ vùng nhớ đệm sẽ được trả về. Ngược lại, đối tượng String
này sẽ được thêm vào vùng nhớ đệm, và một tham chiếu đến đối tượng String
mới trong vùng nhớ đệm sẽ được trả về.
Nói đơn giản, string interning là quá trình chuyển một đối tượng String
từ bộ nhớ heap thông thường sang String Constant Pool và sử dụng tham chiếu của đối tượng từ vùng nhớ đệm đó.
Đối với phương thức intern()
, với bất kỳ hai chuỗi s1
và s2
, s1.intern() == s2.intern()
chỉ đúng nếu và chỉ nếu s1.equals(s2)
đúng.
Điều này có nghĩa là nếu s1
và s2
là các đối tượng chuỗi khác nhau nhưng có cùng dãy ký tự, thì việc gọi intern()
trên cả hai sẽ dẫn đến một chuỗi ký tự duy nhất trong String Constant Pool được tham chiếu bởi cả hai biến.
Theo mặc định, hãy nhớ rằng tất cả các chuỗi ký tự (literal) và các biểu thức hằng số có giá trị chuỗi đều được intern tự động.
Làm thế nào để tìm chuỗi khớp với biểu thức chính quy?
Chúng ta có thể sử dụng API Pattern
và Matcher
để khớp biểu thức chính quy. Lớp String
cung cấp phương thức riêng của nó là matches()
. Chúng ta nên sử dụng trực tiếp phương thức matches()
. Phương thức này cũng sử dụng Pattern.matches()
bên trong định nghĩa hàm.
Giải thích:
Trong ví dụ trên,
str.matches("(.*)java(.*)")
kiểm tra xem chuỗistr
có chứa từ "java" hay không. Biểu thức(.*)
đại diện cho bất kỳ ký tự nào (bao gồm không có ký tự) trước hoặc sau "java". Kết quả làtrue
vì chuỗi chứa "java".Tương tự,
str.matches("(.*)python(.*)")
kiểm tra xem chuỗi có chứa "python" hay không. Kết quả làfalse
vì "python" không xuất hiện trong chuỗi.
Làm thế nào để so sánh hai chuỗi?
Đây là một lĩnh vực thường được yêu thích trong các cuộc phỏng vấn. Nói chung, có hai cách để so sánh các đối tượng:
Sử dụng toán tử
==
Sử dụng phương thức
equals()
Toán tử ==
so sánh tham chiếu của các đối tượng. Vì vậy, nếu hai đối tượng chuỗi tham chiếu đến cùng một chuỗi ký tự (literal) trong String Constant Pool hoặc cùng một đối tượng chuỗi trong heap, thì s == t
sẽ trả về true
, ngược lại trả về false
.
Phương thức equals()
được ghi đè trong lớp String
và kiểm tra dãy ký tự được lưu trữ bởi các đối tượng String
. Nói cách khác, phương thức equals()
so sánh giá trị của các đối tượng chuỗi. Nếu chúng chứa cùng dãy ký tự, thì s.equals(t)
sẽ trả về true
, ngược lại trả về false
.
Ví dụ minh họa:
Chuỗi (String) hoạt động như thế nào trong Java?
Giống như bất kỳ ngôn ngữ lập trình nào khác, String
trong Java là một dãy các ký tự. Nó giống như một lớp tiện ích để thao tác trên dãy ký tự đó. Dãy ký tự này được duy trì trong một biến kiểu mảng char
sau đây:
Các phương thức khác nhau của chuỗi thao tác trên mảng này trong các tình huống khác nhau, sử dụng các biến sau để duy trì vị trí trong mảng:
Giải thích:
value[]
: Là mảngchar
lưu trữ dãy ký tự thực tế của chuỗi. Vì nó được khai báo làfinal
, nội dung của mảng không thể thay đổi sau khi chuỗi được tạo, điều này góp phần làm choString
bất biến.offset
: Xác định vị trí bắt đầu của dãy ký tự trong mảngvalue
.count
: Xác định số lượng ký tự hợp lệ trong chuỗi.
Những thành phần này cho phép lớp String
quản lý và thao tác hiệu quả trên dãy ký tự, đồng thời đảm bảo tính bất biến và hiệu suất tối ưu trong Java.
Làm thế nào để kiểm tra một chuỗi Palindrome?
Một chuỗi được gọi là Palindrome nếu giá trị của nó vẫn giữ nguyên khi bị đảo ngược. Để kiểm tra xem một chuỗi có phải là Palindrome hay không, hãy đảo ngược chuỗi đó và so sánh nó với chuỗi ban đầu.
Nếu chuỗi ban đầu và chuỗi đảo ngược giống nhau, thì chuỗi đã cho là một Palindrome.
Giải thích:
originalString
: Chuỗi ban đầu cần kiểm tra, ở đây là"abcdcba"
.StringBuilder
: Được sử dụng để tạo một bản sao của chuỗi và cung cấp phương thứcreverse()
để đảo ngược chuỗi.reverseString
: Chuỗi sau khi đảo ngược, ở đây là"abcdcba"
.equals()
: So sánh giá trị của chuỗi ban đầu và chuỗi đảo ngược. Nếu chúng bằng nhau, kết quả làtrue
, nghĩa là chuỗi là Palindrome.
Trong ví dụ này, "abcdcba"
khi đảo ngược vẫn là "abcdcba"
, nên kết quả trả về là true
.
Làm thế nào để xóa hoặc thay thế ký tự trong một chuỗi?
Để thay thế hoặc xóa các ký tự, bạn có thể sử dụng String.replace()
hoặc String.replaceAll()
.
Cả hai phương thức đều nhận hai đối số:
Đối số đầu tiên là ký tự cần thay thế.
Đối số thứ hai là ký tự mới sẽ được đặt vào chuỗi.
Nếu bạn muốn xóa ký tự, hãy truyền một chuỗi rỗng (""
) vào đối số thứ hai.
Giải thích:
replace("h", "H")
: Thay thế ký tự"h"
đầu tiên (hoặc tất cả"h"
) bằng"H"
.replaceAll("o", "O")
: Thay thế tất cả các ký tự"o"
bằng"O"
. (replaceAll()
hỗ trợ cả biểu thức chính quy, nhưng ở đây chỉ thay ký tự đơn giản).replace("h", "")
: Xóa ký tự"h"
bằng cách thay nó bằng chuỗi rỗng.replace("o", "")
: Xóa tất cả các ký tự"o"
trong chuỗi.
Lưu ý: replace()
thay thế tất cả các lần xuất hiện của ký tự hoặc chuỗi con, trong khi replaceAll()
thường được dùng với biểu thức chính quy, nhưng ở đây cả hai đều hoạt động tương tự với ký tự đơn.
Làm thế nào để chuyển đổi một chuỗi thành chữ in hoa hoặc chữ thường?
Sử dụng phương thức String.toLowerCase()
và String.toUpperCase()
để chuyển đổi chuỗi sang chữ thường hoặc chữ in hoa.
Giải thích:
toLowerCase()
: Chuyển tất cả các ký tự trong chuỗi thành chữ thường.toUpperCase()
: Chuyển tất cả các ký tự trong chuỗi thành chữ in hoa.
Các phương thức này không thay đổi chuỗi gốc (vì String
là bất biến), mà trả về một chuỗi mới với định dạng đã chuyển đổi.
Chúng ta có thể sử dụng String
trong câu lệnh switch
không?
String
trong câu lệnh switch
không?
Có, chúng ta có thể sử dụng lớp String
trong câu lệnh switch
kể từ Java 7. Trước Java 7, điều này là không thể, và chúng ta phải sử dụng câu lệnh if-else
để đạt được hành vi tương tự.
Giải thích:
Trong ví dụ trên, biến
number
là mộtString
có giá trị"1"
.Câu lệnh
switch
so sánh giá trị củanumber
với cáccase
bằng cách sử dụng phương thứcequals()
nội bộ (không phải==
), vì vậy nó kiểm tra giá trị chuỗi chứ không phải tham chiếu.Khi khớp với
case "1"
, chương trình in ra"One"
và thoát khỏiswitch
nhờbreak
.Nếu không có giá trị nào khớp, khối
default
sẽ được thực thi.
Tính năng này được thêm vào từ Java 7 để tăng tính linh hoạt và tiện lợi khi làm việc với chuỗi trong các cấu trúc điều khiển.
Làm thế nào để in tất cả các hoán vị của một chuỗi?
Hoán vị là sự sắp xếp lại các ký tự sao cho mỗi cách sắp xếp là duy nhất so với các cách sắp xếp khác. Ví dụ, dưới đây là các hoán vị của chuỗi “ABC”: ABC, ACB, BAC, BCA, CBA, CAB.
Một chuỗi có độ dài N sẽ có N! (N giai thừa) hoán vị.
Giải thích:
Phương thức
getPermutations()
sử dụng cách tiếp cận đệ quy để tạo tất cả các hoán vị của chuỗi:Lấy ký tự đầu tiên (
initial
) của chuỗi.Lấy phần còn lại của chuỗi (
rem
) không bao gồm ký tự đầu tiên.Gọi đệ quy để lấy các hoán vị của phần còn lại (
wordSet
).Chèn ký tự đầu tiên vào mọi vị trí có thể trong từng hoán vị của
wordSet
bằng cách sử dụngcharInsertAt()
.
charInsertAt()
chèn một ký tự vào vị trí xác định trong chuỗi.HashSet
được sử dụng để lưu trữ các hoán vị, đảm bảo không có trùng lặp.
Ví dụ:
Với
"ABC"
:Bước 1:
initial = 'A'
,rem = "BC"
.Bước 2: Gọi đệ quy cho
"BC"
→[BC, CB]
.Bước 3: Chèn
'A'
vào các vị trí của"BC"
và"CB"
:"BC"
: ABC, BAC, BCA."CB"
: ACB, CAB, CBA.
Kết quả:
[ABC, ACB, BAC, BCA, CBA, CAB]
.
Làm thế nào để đảo ngược từng từ trong một chuỗi?
Để đảo ngược từng từ một cách riêng lẻ, hãy tách chuỗi thành các từ và lưu chúng vào một mảng. Sau đó, áp dụng logic đảo ngược cho từng từ, và cuối cùng, nối tất cả các từ lại với nhau.
Giải thích:
split(" ")
: Tách chuỗiblogName
thành một mảng các từ, sử dụng khoảng trắng làm điểm phân tách. Với"how to do in java dot com"
, mảngtokens
sẽ là["how", "to", "do", "in", "java", "dot", "com"]
.StringBuffer
: Được sử dụng để lưu trữ chuỗi kết quả và hỗ trợ phương thứcreverse()
để đảo ngược từng từ.Vòng lặp
for
:Với mỗi từ (
token
), tạo mộtStringBuffer
từ từ đó, đảo ngược nó bằngreverse()
, rồi chuyển lại thànhString
bằngtoString()
.Nối từ đã đảo ngược vào
finalString
và thêm một khoảng trắng (" "
) để giữ cấu trúc câu.
Kết quả cuối cùng:
"woh ot od ni avaj tod moc "
.
Lưu ý: Nếu không muốn khoảng trắng thừa ở cuối, bạn có thể dùng finalString.toString().trim()
để loại bỏ nó, kết quả sẽ là "woh ot od ni avaj tod moc"
.
Làm thế nào để tách một chuỗi?
Sử dụng phương thức String.split()
để chia một chuỗi đã cho dựa trên các khớp của biểu thức chính quy được cung cấp. Phương thức này còn được gọi là lấy các token của chuỗi dựa trên một ký tự phân tách (delimiter).
Phương thức split()
trả về một mảng các chuỗi. Mỗi chuỗi trong mảng là một token riêng lẻ.
Giải thích:
numbers.split(",")
: Tách chuỗi"1,2,3,4,5,6,7"
tại mỗi dấu phẩy (,
), tạo ra một mảng các chuỗi con.Kết quả
numArray
là["1", "2", "3", "4", "5", "6", "7"]
.Arrays.toString()
: Được sử dụng để in mảng dưới dạng dễ đọc thay vì chỉ hiển thị địa chỉ bộ nhớ của mảng.
Lưu ý:
Nếu chuỗi có ký tự phân tách liên tiếp (ví dụ:
"1,,2"
),split()
sẽ tạo ra các phần tử rỗng trong mảng (kết quả:["1", "", "2"]
).split()
chấp nhận biểu thức chính quy, vì vậy bạn có thể dùng các mẫu phức tạp hơn thay vì chỉ một ký tự đơn như","
. Ví dụ:split("\\s+")
để tách dựa trên một hoặc nhiều khoảng trắng.
Tại sao chúng ta không nên sử dụng String
để lưu trữ mật khẩu?
String
để lưu trữ mật khẩu?Chúng ta biết rằng các chuỗi trong Java được lưu trữ trong String Constant Pool. Một khi một chuỗi được tạo trong vùng nhớ đệm này, nó sẽ tồn tại ở đó cho đến khi được thu gom rác (garbage collected). Trong khoảng thời gian này, bất kỳ chương trình độc hại nào cũng có thể truy cập vào khu vực bộ nhớ vật lý và lấy dữ liệu chuỗi.
Nếu chúng ta lưu trữ mật khẩu dưới dạng String
, nó sẽ được giữ trong String Pool và tồn tại trong bộ nhớ lâu hơn mức cần thiết, bởi vì chu kỳ thu gom rác là không thể dự đoán. Điều này khiến các chuỗi mật khẩu nhạy cảm dễ bị tấn công và đánh cắp dữ liệu.
Liệu có thể lập luận rằng ta nên làm trống chuỗi String
sau khi sử dụng không? Không, điều đó là không thể. Chúng ta biết rằng một khi String
được tạo, nó không thể bị thay đổi, ví dụ: bạn không thể sửa đổi nội dung của nó. String
là bất biến (immutable) và có thuộc tính final
.
Tuy nhiên, mảng char
thì có thể thay đổi (mutable), chúng ta có thể ghi đè nội dung của nó sau khi sử dụng. Vì vậy, ứng dụng của chúng ta nên sử dụng char[]
để lưu trữ mật khẩu, và sau khi sử dụng mật khẩu, thay thế nội dung mảng bằng ký tự trống.
Giải thích:
String password = "123456"
: Cách này không an toàn vì chuỗi"123456"
sẽ tồn tại trong String Pool cho đến khi thu gom rác, làm tăng nguy cơ bị truy cập trái phép.char[] passwordChars
: Mảng ký tự có thể lưu trữ mật khẩu (ví dụ: lấy từ cơ sở dữ liệu). Sau khi sử dụng, bạn có thể ghi đè từng phần tử trong mảng để xóa dữ liệu nhạy cảm.Vòng lặp
for
: Thử thay thế từng ký tự trongpasswordChars
bằng ký tự trống (' '
). Tuy nhiên, đoạn code mẫu này có lỗi nhỏ: trong Java, vòng lặpfor-each
không thay đổi trực tiếp giá trị trong mảng. Để sửa, cần dùng vòng lặp chỉ số:
Kết luận: Sử dụng char[]
thay vì String
để lưu mật khẩu giúp kiểm soát vòng đời dữ liệu nhạy cảm tốt hơn, giảm nguy cơ bị rò rỉ thông tin. Sau khi dùng xong, bạn có thể chủ động xóa nội dung mảng.
Chuỗi (String) có an toàn với luồng (thread-safe) không?
Có, chuỗi là an toàn với luồng vì chúng bất biến (immutable).
Hãy nhớ rằng tất cả các thể hiện bất biến đều an toàn với luồng theo thiết kế.
Giải thích:
Tính bất biến của
String
nghĩa là một khi đối tượng chuỗi được tạo, nội dung của nó không thể thay đổi. Điều này loại bỏ khả năng xảy ra xung đột hoặc tình trạng cạnh tranh (race condition) khi nhiều luồng truy cập cùng một đối tượngString
.Vì không có thao tác ghi (write) nào có thể thay đổi trạng thái của
String
, các luồng chỉ có thể đọc dữ liệu, và việc đọc đồng thời từ nhiều luồng là an toàn.Do đó, bạn không cần sử dụng cơ chế đồng bộ hóa (synchronization) khi làm việc với
String
trong môi trường đa luồng.
Tại sao String
là lựa chọn tuyệt vời cho khóa của HashMap
?
String
là lựa chọn tuyệt vời cho khóa của HashMap
?
Trong Java, một khóa của Map
cần phải bất biến (immutable) và tuân thủ hợp đồng giữa phương thức equals()
và hashCode()
. Lớp String
đáp ứng cả hai điều kiện này.
Ngoài ra, lớp String
cung cấp nhiều phương thức hữu ích để so sánh, sắp xếp, tách token hoặc chuyển đổi giữa chữ hoa và chữ thường. Những phương thức này có thể được sử dụng khi thực hiện các thao tác CRUD (tạo, đọc, cập nhật, xóa) trên Map
.
Tất cả những điều này khiến String
trở thành một lớp rất hữu dụng để sử dụng trong Map
, thay vì phải tự tạo một lớp riêng của chúng ta.
Giải thích:
Bất biến: Vì
String
là bất biến, giá trị của nó không thể thay đổi sau khi được sử dụng làm khóa. Điều này đảm bảo tính nhất quán của khóa trongHashMap
, tránh việc thay đổi giá trị làm phá vỡ cấu trúc dữ liệu nội bộ (dựa trên giá trị băm).Hợp đồng
equals()
vàhashCode()
:String
đã ghi đè hai phương thức này một cách chính xác, đảm bảo rằng hai chuỗi có nội dung giống nhau sẽ có cùng giá trị băm và được coi là bằng nhau. Điều này rất quan trọng đểHashMap
tra cứu và lưu trữ khóa hiệu quả.Các phương thức tiện ích: Các phương thức như
compareTo()
,split()
,toLowerCase()
,toUpperCase()
, v.v., giúp xử lý khóa dễ dàng hơn trong các tình huống thực tế, chẳng hạn như chuẩn hóa dữ liệu hoặc tìm kiếm.Hiệu quả: Sử dụng
String
tiết kiệm thời gian và công sức so với việc tự định nghĩa một lớp tùy chỉnh, đồng thời tận dụng được tối ưu hóa sẵn có của Java.
Do đó, String
là lựa chọn lý tưởng và phổ biến cho khóa trong HashMap
hoặc các cấu trúc dữ liệu tương tự như HashSet
.
Last updated