Xử lý String nâng cao
Bài trước: String cơ bản — Đã học về String là object, String Pool, immutability, và các methods cơ bản. Bài này sẽ học cách xử lý String hiệu quả với StringBuilder, formatting, và Regular Expressions.
Sau bài này, bạn sẽ:
- Sử dụng StringBuilder/StringBuffer để nối String hiệu quả trong vòng lặp
- Áp dụng String.format() và printf() cho formatted output
- Sử dụng Regular Expressions (regex) để tìm kiếm và validate patterns
- Hiểu sự khác biệt giữa StringBuilder (fast) và StringBuffer (thread-safe)
- Áp dụng các pattern validation phổ biến: email, phone, password, URL
Trong bài này, chúng ta sẽ học các kỹ thuật xử lý String hiệu quả hơn, bao gồm StringBuilder, StringBuffer, formatting, và Regular Expressions.
StringBuilder và StringBuffer
Vấn đề với String Immutability
// Ví dụ về hiệu năng kém khi dùng String
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // Tạo ra 10,000 String objects mới!
}
// Mỗi lần += tạo ra 1 String object mới
// -> Tốn bộ nhớ và thời gian
Mỗi lần nối String với +, Java tạo ra một String object hoàn toàn mới trong bộ nhớ. Với 10,000 lần lặp, có thể tạo ra hàng chục nghìn objects không cần thiết!
StringBuilder - Mutable String
StringBuilder là một class cung cấp mutable (có thể thay đổi) string buffer.
// Sử dụng StringBuilder - HIỆU QUẢ hơn nhiều
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i); // Chỉ sửa đổi 1 object duy nhất
}
String result = sb.toString();
Các phương thức của StringBuilder
public class StringBuilderDemo {
public static void main(String[] args) {
// 1. Khởi tạo
StringBuilder sb1 = new StringBuilder(); // Capacity mặc định: 16
StringBuilder sb2 = new StringBuilder(50); // Capacity: 50
StringBuilder sb3 = new StringBuilder("Hello"); // Khởi tạo với String
// 2. append() - Thêm vào cuối
StringBuilder sb = new StringBuilder("Java");
sb.append(" Programming");
sb.append(" ").append(2024); // Method chaining
System.out.println(sb); // "Java Programming 2024"
// 3. insert() - Chèn vào vị trí chỉ định
sb = new StringBuilder("Java");
sb.insert(0, "Hello "); // "Hello Java"
sb.insert(11, " World"); // "Hello Java World"
System.out.println(sb);
// 4. delete() và deleteCharAt()
sb = new StringBuilder("Hello World");
sb.delete(5, 11); // "Hello" (xóa từ index 5 đến 10)
sb.deleteCharAt(0); // "ello"
System.out.println(sb);
// 5. replace()
sb = new StringBuilder("Hello World");
sb.replace(6, 11, "Java"); // "Hello Java"
System.out.println(sb);
// 6. reverse()
sb = new StringBuilder("Java");
sb.reverse(); // "avaJ"
System.out.println(sb);
// 7. substring() - Giống String
sb = new StringBuilder("Hello Java");
String sub = sb.substring(6); // "Java"
System.out.println(sub);
// 8. capacity() và length()
sb = new StringBuilder();
System.out.println("Length: " + sb.length()); // 0
System.out.println("Capacity: " + sb.capacity()); // 16
sb.append("Hello");
System.out.println("Length: " + sb.length()); // 5
System.out.println("Capacity: " + sb.capacity()); // 16
// 9. ensureCapacity() - Đảm bảo capacity tối thiểu
sb.ensureCapacity(50);
System.out.println("Capacity: " + sb.capacity()); // >= 50
// 10. trimToSize() - Giảm capacity về đúng bằng length
sb.trimToSize();
System.out.println("Capacity: " + sb.capacity()); // 5
}
}
StringBuilder vs StringBuffer
| Đặc điểm | StringBuilder | StringBuffer |
|---|---|---|
| Thread-safe | Không | Có (synchronized) |
| Hiệu năng | Nhanh hơn | Chậm hơn (do synchronization) |
| Khi nào dùng | Single-threaded | Multi-threaded |
| Từ Java version | Java 5 | Java 1.0 |
// StringBuilder - Dùng trong single-threaded (99% trường hợp)
StringBuilder sb = new StringBuilder();
sb.append("Fast");
// StringBuffer - Dùng khi nhiều threads cùng sửa đổi
StringBuffer sbf = new StringBuffer();
sbf.append("Thread-safe");
Luôn ưu tiên dùng StringBuilder trừ khi bạn thực sự cần thread-safety. StringBuffer chậm hơn đáng kể do overhead của synchronization.
So sánh hiệu năng
public class PerformanceComparison {
public static void main(String[] args) {
int iterations = 10000;
// 1. String concatenation
long start = System.currentTimeMillis();
String str = "";
for (int i = 0; i < iterations; i++) {
str += "a";
}
long end = System.currentTimeMillis();
System.out.println("String: " + (end - start) + "ms");
// 2. StringBuilder
start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < iterations; i++) {
sb.append("a");
}
String result = sb.toString();
end = System.currentTimeMillis();
System.out.println("StringBuilder: " + (end - start) + "ms");
// 3. StringBuffer
start = System.currentTimeMillis();
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < iterations; i++) {
sbf.append("a");
}
result = sbf.toString();
end = System.currentTimeMillis();
System.out.println("StringBuffer: " + (end - start) + "ms");
}
}
// Kết quả ví dụ (thời gian thực tế phụ thuộc vào máy):
// String: 350ms
// StringBuilder: 2ms
// StringBuffer: 3ms
String Formatting
1. String.format()
String.format() giống printf() trong C, cho phép tạo formatted strings.
public class StringFormatDemo {
public static void main(String[] args) {
// 1. Định dạng số nguyên
String s1 = String.format("Number: %d", 42);
System.out.println(s1); // "Number: 42"
// 2. Định dạng số thực
String s2 = String.format("Pi: %.2f", Math.PI);
System.out.println(s2); // "Pi: 3.14"
// 3. Định dạng chuỗi
String name = "Java";
String s3 = String.format("Language: %s", name);
System.out.println(s3); // "Language: Java"
// 4. Nhiều tham số
String s4 = String.format("Name: %s, Age: %d, Height: %.2f",
"John", 25, 1.75);
System.out.println(s4); // "Name: John, Age: 25, Height: 1.75"
// 5. Căn lề và padding
String s5 = String.format("|%10s|", "Java"); // "| Java|" (right-aligned)
String s6 = String.format("|%-10s|", "Java"); // "|Java |" (left-aligned)
System.out.println(s5);
System.out.println(s6);
// 6. Số nguyên với padding
String s7 = String.format("%05d", 42); // "00042"
System.out.println(s7);
// 7. Dấu phân cách hàng nghìn
String s8 = String.format("%,d", 1000000); // "1,000,000"
System.out.println(s8);
// 8. Định dạng date/time
import java.util.Date;
String s9 = String.format("Current date: %tF", new Date());
String s10 = String.format("Current time: %tT", new Date());
System.out.println(s9); // "Current date: 2024-01-15"
System.out.println(s10); // "Current time: 14:30:45"
}
}
Format Specifiers chính
| Specifier | Mô tả | Ví dụ |
|---|---|---|
%s | String | String.format("%s", "text") |
%d | Decimal integer | String.format("%d", 42) |
%f | Floating point | String.format("%.2f", 3.14159) → "3.14" |
%e | Scientific notation | String.format("%e", 1000.0) → "1.000000e+03" |
%x | Hexadecimal | String.format("%x", 255) → "ff" |
%o | Octal | String.format("%o", 8) → "10" |
%b | Boolean | String.format("%b", true) → "true" |
%c | Character | String.format("%c", 65) → "A" |
%n | Newline | Platform-independent newline |
2. printf() và System.out.format()
// printf() tương đương format() nhưng in trực tiếp
System.out.printf("Name: %s, Age: %d%n", "John", 25);
// System.out.format() tương tự
System.out.format("Price: $%.2f%n", 19.99);
3. Text Blocks (Java 15+)
Text blocks giúp viết multi-line strings dễ dàng hơn.
public class TextBlocksDemo {
public static void main(String[] args) {
// Cách cũ - phải escape và concat
String html1 = "<html>\n" +
" <body>\n" +
" <h1>Hello</h1>\n" +
" </body>\n" +
"</html>";
// Text blocks - Dễ đọc hơn nhiều!
String html2 = """
<html>
<body>
<h1>Hello</h1>
</body>
</html>
""";
System.out.println(html2);
// Text blocks với formatting
String name = "Java";
int version = 21;
String message = """
Welcome to %s!
Current version: %d
""".formatted(name, version);
System.out.println(message);
}
}
Regular Expressions (Regex)
Regular Expressions là công cụ mạnh mẽ để tìm kiếm và xử lý patterns trong String.
Cơ bản về Regex
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class RegexBasics {
public static void main(String[] args) {
// 1. matches() - Kiểm tra toàn bộ String khớp pattern
String text = "Java123";
boolean matches = text.matches("[A-Za-z]+\\d+"); // Chữ + số
System.out.println(matches); // true
// 2. Pattern và Matcher
String input = "Java is fun. Java is powerful.";
Pattern pattern = Pattern.compile("Java");
Matcher matcher = pattern.matcher(input);
// Tìm tất cả matches
while (matcher.find()) {
System.out.println("Found at: " + matcher.start());
}
// Found at: 0
// Found at: 13
// 3. replaceAll() với regex
String result = input.replaceAll("Java", "Python");
System.out.println(result);
// 4. replaceFirst() với regex
result = input.replaceFirst("Java", "Python");
System.out.println(result);
// 5. split() với regex
String data = "apple,banana;orange:grape";
String[] fruits = data.split("[,;:]"); // Tách bởi , hoặc ; hoặc :
for (String fruit : fruits) {
System.out.println(fruit);
}
}
}
Regex Pattern Syntax
| Pattern | Mô tả | Ví dụ |
|---|---|---|
. | Bất kỳ ký tự nào | a.c matches "abc", "a1c" |
\d | Chữ số [0-9] | \d\d matches "42" |
\D | Không phải chữ số | \D matches "a", "!" |
\w | Word character [a-zA-Z0-9_] | \w+ matches "hello_123" |
\W | Không phải word character | \W matches "!", "@" |
\s | Whitespace | \s+ matches " " |
\S | Không phải whitespace | \S+ matches "hello" |
^ | Đầu string | ^Java matches "Java..." |
$ | Cuối string | Java$ matches "...Java" |
* | 0 hoặc nhiều lần | a* matches "", "a", "aaa" |
+ | 1 hoặc nhiều lần | a+ matches "a", "aaa" |
? | 0 hoặc 1 lần | colou?r matches "color", "colour" |
{n} | Chính xác n lần | \d{3} matches "123" |
{n,} | Ít nhất n lần | \d{3,} matches "123", "1234" |
{n,m} | Từ n đến m lần | \d{3,5} matches "123", "12345" |
[abc] | a, b, hoặc c | [aeiou] matches vowels |
[^abc] | Không phải a, b, c | [^0-9] matches non-digits |
[a-z] | Từ a đến z | [a-zA-Z] matches letters |
(x|y) | x hoặc y | (cat|dog) matches "cat", "dog" |
Common Validation Patterns
public class ValidationPatterns {
// Email validation
public static boolean isValidEmail(String email) {
String regex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
return email.matches(regex);
}
// Phone number (Vietnam format)
public static boolean isValidPhone(String phone) {
// 0123456789 hoặc 0123-456-789
String regex = "^0\\d{9}$|^0\\d{3}-\\d{3}-\\d{3}$";
return phone.matches(regex);
}
// Password strength (8+ chars, chữ hoa, chữ thường, số)
public static boolean isStrongPassword(String password) {
String regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$";
return password.matches(regex);
}
// Date format (DD/MM/YYYY)
public static boolean isValidDate(String date) {
String regex = "^(0[1-9]|[12][0-9]|3[01])/(0[1-9]|1[0-2])/\\d{4}$";
return date.matches(regex);
}
// URL validation
public static boolean isValidURL(String url) {
String regex = "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$";
return url.matches(regex);
}
public static void main(String[] args) {
// Test email
System.out.println(isValidEmail("[email protected]")); // true
System.out.println(isValidEmail("invalid.email")); // false
// Test phone
System.out.println(isValidPhone("0123456789")); // true
System.out.println(isValidPhone("0123-456-789")); // true
System.out.println(isValidPhone("123456")); // false
// Test password
System.out.println(isStrongPassword("Pass123")); // true
System.out.println(isStrongPassword("weak")); // false
// Test date
System.out.println(isValidDate("15/01/2024")); // true
System.out.println(isValidDate("32/13/2024")); // true (lưu ý: regex không kiểm tra logic date)
// Test URL
System.out.println(isValidURL("https://example.com")); // true
System.out.println(isValidURL("not-a-url")); // false
}
}
Ví dụ: Extract thông tin từ text
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexExtraction {
public static void main(String[] args) {
String text = """
Contact us:
Email: [email protected]
Phone: 0123-456-789
Website: https://example.com
""";
// Extract email
Pattern emailPattern = Pattern.compile("[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}");
Matcher emailMatcher = emailPattern.matcher(text);
if (emailMatcher.find()) {
System.out.println("Email: " + emailMatcher.group());
}
// Extract phone
Pattern phonePattern = Pattern.compile("0\\d{3}-\\d{3}-\\d{3}");
Matcher phoneMatcher = phonePattern.matcher(text);
if (phoneMatcher.find()) {
System.out.println("Phone: " + phoneMatcher.group());
}
// Extract URL
Pattern urlPattern = Pattern.compile("https?://[\\w\\-]+(\\.[\\w\\-]+)+");
Matcher urlMatcher = urlPattern.matcher(text);
if (urlMatcher.find()) {
System.out.println("URL: " + urlMatcher.group());
}
}
}
String Conversion
1. Primitive to String
// valueOf() - Recommended
String s1 = String.valueOf(42);
String s2 = String.valueOf(3.14);
String s3 = String.valueOf(true);
String s4 = String.valueOf('A');
// toString() cho objects
Integer num = 42;
String s5 = num.toString();
// Concatenation với empty string (không khuyến khích)
String s6 = 42 + "";
2. String to Primitive
// parseInt(), parseDouble(), etc.
String s1 = "42";
int num1 = Integer.parseInt(s1);
String s2 = "3.14";
double num2 = Double.parseDouble(s2);
String s3 = "true";
boolean flag = Boolean.parseBoolean(s3);
// Xử lý exception
try {
int num = Integer.parseInt("abc"); // NumberFormatException
} catch (NumberFormatException e) {
System.out.println("Invalid number format");
}
3. Wrapper Classes
// valueOf() returns wrapper object
Integer num1 = Integer.valueOf("42");
Double num2 = Double.valueOf("3.14");
Boolean flag = Boolean.valueOf("true");
// Auto-unboxing
int primitiveNum = num1; // Integer -> int tự động
StringJoiner (Java 8+)
StringJoiner giúp nối các strings với delimiter, prefix, và suffix.
import java.util.StringJoiner;
public class StringJoinerDemo {
public static void main(String[] args) {
// 1. Delimiter only
StringJoiner sj1 = new StringJoiner(", ");
sj1.add("Apple").add("Banana").add("Orange");
System.out.println(sj1); // "Apple, Banana, Orange"
// 2. Delimiter + Prefix + Suffix
StringJoiner sj2 = new StringJoiner(", ", "[", "]");
sj2.add("Java").add("Python").add("C++");
System.out.println(sj2); // "[Java, Python, C++]"
// 3. String.join() - Simpler alternative
String result = String.join(", ", "Red", "Green", "Blue");
System.out.println(result); // "Red, Green, Blue"
// 4. Join array
String[] languages = {"Java", "Python", "JavaScript"};
String joined = String.join(" | ", languages);
System.out.println(joined); // "Java | Python | JavaScript"
// 5. Join with Stream (Java 8+)
import java.util.Arrays;
import java.util.stream.Collectors;
String streamJoin = Arrays.stream(languages)
.collect(Collectors.joining(", ", "{", "}"));
System.out.println(streamJoin); // "{Java, Python, JavaScript}"
}
}
StringBuilder Internal: Capacity Growth
Khi append() vượt quá capacity hiện tại, StringBuilder tự động mở rộng:
newCapacity = (oldCapacity << 1) + 2 // gấp đôi + 2
StringBuilder sb = new StringBuilder(); // capacity = 16
sb.append("Hello World!!!!!"); // 16 chars → vừa đủ capacity 16
System.out.println(sb.capacity()); // 16
sb.append("X"); // 17 chars → vượt capacity!
// newCapacity = (16 << 1) + 2 = 34
System.out.println(sb.capacity()); // 34
Nếu biết trước kích thước xấp xỉ, hãy set capacity ban đầu để tránh resize:
// ❌ Resize nhiều lần nếu append nhiều
StringBuilder sb = new StringBuilder();
// ✅ Set capacity phù hợp ngay từ đầu
StringBuilder sb = new StringBuilder(1000);
Pattern.compile() Flags
Pattern.compile() nhận flags để điều chỉnh cách regex hoạt động:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
// CASE_INSENSITIVE: Bỏ qua chữ hoa/thường
Pattern p1 = Pattern.compile("java", Pattern.CASE_INSENSITIVE);
Matcher m1 = p1.matcher("I love Java and JAVA");
while (m1.find()) {
System.out.println("Found: " + m1.group()); // "Java", "JAVA"
}
// MULTILINE: ^ và $ match đầu/cuối mỗi dòng (không chỉ đầu/cuối string)
String text = "Line1\nLine2\nLine3";
Pattern p2 = Pattern.compile("^Line\\d", Pattern.MULTILINE);
Matcher m2 = p2.matcher(text);
while (m2.find()) {
System.out.println(m2.group()); // "Line1", "Line2", "Line3"
}
// DOTALL: Dấu . match cả newline
Pattern p3 = Pattern.compile("start.*end", Pattern.DOTALL);
Matcher m3 = p3.matcher("start\nhello\nend");
System.out.println(m3.find()); // true (. match cả \n)
// Kết hợp nhiều flags bằng |
Pattern p4 = Pattern.compile("pattern",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Regex Backreferences trong replaceAll
Dùng $1, $2... để tham chiếu đến capturing groups (nhóm trong ngoặc ()) trong chuỗi thay thế:
// Đổi "firstName lastName" → "lastName, firstName"
String name = "John Doe";
String reversed = name.replaceAll("(\\w+) (\\w+)", "$2, $1");
System.out.println(reversed); // "Doe, John"
// Format số điện thoại: 0912345678 → 091-234-5678
String phone = "0912345678";
String formatted = phone.replaceAll("(\\d{3})(\\d{3})(\\d{4})", "$1-$2-$3");
System.out.println(formatted); // "091-234-5678"
// Loại bỏ từ trùng lặp liên tiếp: "the the" → "the"
String text = "the the quick quick fox";
String clean = text.replaceAll("\\b(\\w+)\\s+\\1\\b", "$1");
System.out.println(clean); // "the quick fox"
Cảnh báo: ReDoS (Regular Expression Denial of Service)
Một số regex pattern có thể gây catastrophic backtracking — regex engine thử hàng triệu combinations, khiến chương trình "treo":
// ❌ NGUY HIỂM: Nested quantifiers
String evilRegex = "(a+)+b";
String input = "aaaaaaaaaaaaaaaaac"; // Không có 'b' ở cuối
// Regex engine thử 2^n combinations trước khi kết luận "không match"
boolean match = input.matches(evilRegex); // Có thể mất HÀNG GIỜ!
Cách phòng tránh:
- Tránh nested quantifiers:
(a+)+,(a*)*,(a+)* - Set timeout cho regex: dùng
MatchervớiThread.interrupt() - Test regex với input dài trước khi deploy
Bài tập: Word Counter
Viết chương trình đếm số lượng từ, ký tự, câu trong một đoạn văn.
import java.util.HashMap;
import java.util.Map;
public class TextAnalyzer {
public static void analyzeText(String text) {
if (text == null || text.trim().isEmpty()) {
System.out.println("Empty text!");
return;
}
// Đếm ký tự (không kể khoảng trắng)
int charCount = text.replaceAll("\\s+", "").length();
// Đếm từ
String[] words = text.trim().split("\\s+");
int wordCount = words.length;
// Đếm câu (kết thúc bằng ., !, ?)
String[] sentences = text.split("[.!?]+");
int sentenceCount = sentences.length;
// Đếm dòng
String[] lines = text.split("\n");
int lineCount = lines.length;
// Tìm từ dài nhất
String longestWord = "";
for (String word : words) {
String cleanWord = word.replaceAll("[^a-zA-Z]", "");
if (cleanWord.length() > longestWord.length()) {
longestWord = cleanWord;
}
}
// Đếm tần suất từ
Map<String, Integer> wordFrequency = new HashMap<>();
for (String word : words) {
String cleanWord = word.toLowerCase().replaceAll("[^a-z]", "");
if (!cleanWord.isEmpty()) {
wordFrequency.put(cleanWord, wordFrequency.getOrDefault(cleanWord, 0) + 1);
}
}
// In kết quả
System.out.println("=== TEXT ANALYSIS ===");
System.out.println("Characters (no spaces): " + charCount);
System.out.println("Words: " + wordCount);
System.out.println("Sentences: " + sentenceCount);
System.out.println("Lines: " + lineCount);
System.out.println("Longest word: " + longestWord + " (" + longestWord.length() + " chars)");
System.out.println("\n=== WORD FREQUENCY (Top 5) ===");
wordFrequency.entrySet().stream()
.sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue()))
.limit(5)
.forEach(e -> System.out.printf("%s: %d%n", e.getKey(), e.getValue()));
}
public static void main(String[] args) {
String text = """
Java is a popular programming language.
Java is used for web development, mobile apps, and more.
Learning Java is fun and rewarding!
""";
analyzeText(text);
}
}
Bài tập thực hành
Bài 1: Caesar Cipher
Implement Caesar Cipher encryption (dịch mỗi chữ cái đi n vị trí).
public class CaesarCipher {
public static String encrypt(String text, int shift) {
// TODO: Implement encryption
// A -> D (shift=3), Z -> C (shift=3)
return "";
}
public static String decrypt(String text, int shift) {
// TODO: Implement decryption
return "";
}
public static void main(String[] args) {
String original = "HELLO";
String encrypted = encrypt(original, 3);
System.out.println("Encrypted: " + encrypted); // "KHOOR"
System.out.println("Decrypted: " + decrypt(encrypted, 3)); // "HELLO"
}
}
Bài 2: HTML Tag Remover
Viết chương trình loại bỏ tất cả HTML tags từ một string.
public class HTMLTagRemover {
public static String removeTags(String html) {
// TODO: Sử dụng regex để remove <tag>...</tag>
return "";
}
public static void main(String[] args) {
String html = "<p>Hello <b>World</b>!</p>";
System.out.println(removeTags(html)); // "Hello World!"
}
}
Bài 3: URL Parser
Parse URL và extract protocol, domain, path, query parameters.
public class URLParser {
public static void parseURL(String url) {
// TODO: Extract và in ra:
// - Protocol (http, https)
// - Domain
// - Path
// - Query parameters
}
public static void main(String[] args) {
parseURL("https://example.com/path/to/page?id=123&name=Java");
// Expected output:
// Protocol: https
// Domain: example.com
// Path: /path/to/page
// Query: id=123, name=Java
}
}
Tổng kết
- StringBuilder cho mutable strings - hiệu quả hơn nhiều khi nối chuỗi nhiều lần
- StringBuilder (single-thread) vs StringBuffer (thread-safe)
- String.format() và printf() cho formatted output
- Text blocks (Java 15+) giúp viết multi-line strings dễ dàng
- Regular Expressions là công cụ mạnh mẽ cho pattern matching
- StringJoiner và String.join() cho việc nối strings với delimiter
- Các pattern validation phổ biến: email, phone, password, URL