在算法竞赛和日常开发中,Java的集合框架和字符串处理能力是解决问题的利器。Set、HashMap和String这三个类作为Java标准库中最常用的工具,掌握它们的高效使用方法能显著提升编码效率。本文将从实际应用场景出发,深入剖析这些类的核心方法、底层原理和使用技巧。
提示:本文所有代码示例基于Java 8+环境,部分方法如String.strip()需要JDK11+支持
HashSet基于哈希表实现,提供O(1)时间复杂度的插入、删除和查找操作,但不保证元素顺序。其底层实际上是使用HashMap存储元素,将元素作为HashMap的key,用一个固定的Object对象作为value。
java复制// HashSet底层实现简析
public class HashSet<E> {
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
}
TreeSet基于红黑树实现,元素按照自然顺序或Comparator指定的顺序排序,所有操作的时间复杂度为O(log n)。它实际上是使用TreeMap作为存储容器。
java复制// TreeSet的排序特性示例
Set<Integer> treeSet = new TreeSet<>((a, b) -> b - a); // 自定义降序排序
treeSet.addAll(Arrays.asList(3,1,4,1,5));
System.out.println(treeSet); // 输出[5,4,3,1]
contains()方法在算法题中常用于快速判断元素存在性。例如两数之和问题:
java复制public int[] twoSum(int[] nums, int target) {
Set<Integer> set = new HashSet<>();
for (int num : nums) {
if (set.contains(target - num)) {
return new int[]{num, target - num};
}
set.add(num);
}
return new int[0];
}
remove()方法常配合迭代器使用,避免并发修改异常:
java复制Set<Integer> set = new HashSet<>(Arrays.asList(1,2,3,4,5));
Iterator<Integer> it = set.iterator();
while (it.hasNext()) {
if (it.next() % 2 == 0) {
it.remove(); // 安全删除
}
}
lower()和higher()方法在解决范围查询问题时非常高效。例如最近的会议室时间安排:
java复制TreeSet<Integer> meetingTimes = new TreeSet<>(Arrays.asList(900, 1000, 1100));
int newMeeting = 1030;
Integer prevMeeting = meetingTimes.lower(newMeeting); // 1000
Integer nextMeeting = meetingTimes.higher(newMeeting); // 1100
first()和last()方法在滑动窗口问题中也有应用:
java复制// 保持窗口内元素有序并快速获取极值
TreeSet<Integer> window = new TreeSet<>();
window.add(5); window.add(2); window.add(8);
int minInWindow = window.first(); // 2
int maxInWindow = window.last(); // 8
初始容量设置:对于已知大小的集合,建议初始化时指定容量避免扩容开销
java复制Set<Integer> largeSet = new HashSet<>(10000); // 避免频繁扩容
对象相等性:自定义对象作为Set元素时,必须正确重写equals()和hashCode()
java复制class Student {
String id;
// 必须重写equals和hashCode
@Override public int hashCode() { return id.hashCode(); }
}
遍历性能:HashSet的遍历比ArrayList慢约30%,在需要频繁遍历的场景慎用
内存占用:HashSet的内存开销大约是存储元素的2-3倍,内存敏感场景需注意
Java 8中的HashMap采用数组+链表+红黑树的混合结构。当链表长度超过8时转为红黑树,提升最坏情况下的性能。负载因子默认为0.75,当元素数量超过容量*负载因子时会触发扩容。
java复制// HashMap内部结构简析
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
getOrDefault()和merge()方法是频率统计的利器:
java复制// 传统写法
Map<String, Integer> freq = new HashMap<>();
for (String word : words) {
if (freq.containsKey(word)) {
freq.put(word, freq.get(word) + 1);
} else {
freq.put(word, 1);
}
}
// 优雅写法
for (String word : words) {
freq.put(word, freq.getOrDefault(word, 0) + 1);
}
// Java8+写法
for (String word : words) {
freq.merge(word, 1, Integer::sum);
}
当需要多字段组合作为key时,可以:
java复制// 方法1:使用字符串拼接(简单但可能冲突)
map.put(x + "," + y, value);
// 方法2:使用自定义对象(推荐)
class CompositeKey {
int x; int y;
// 必须重写equals和hashCode
}
map.put(new CompositeKey(x, y), value);
java复制Map<String, Integer> map = new HashMap<>();
// 1. 遍历entrySet(最推荐)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
entry.getKey();
entry.getValue();
}
// 2. 遍历keySet(需要额外get)
for (String key : map.keySet()) {
map.get(key);
}
// 3. 遍历values(仅需要值时)
for (Integer value : map.values()) {
// 使用value
}
// 4. Java8+ forEach
map.forEach((k, v) -> System.out.println(k + ": " + v));
初始化容量:预估元素数量设置初始容量,避免多次扩容
java复制// 预期存储1000个元素
Map<String, Integer> map = new HashMap<>(1333); // 1000/0.75
使用原始类型特化类:考虑使用FastUtil或Eclipse Collections等库的原始类型map
java复制Int2IntOpenHashMap fastMap = new Int2IntOpenHashMap();
避免包装类型:Java8引入的Map.computeIfAbsent可以延迟创建复杂value对象
java复制Map<String, List<Integer>> map = new HashMap<>();
map.computeIfAbsent("key", k -> new ArrayList<>()).add(123);
Java字符串的不可变性带来安全性优势,但也需要注意性能问题:
java复制// 低效的字符串拼接
String result = "";
for (int i = 0; i < 100; i++) {
result += i; // 每次循环创建新对象
}
// 改进方案1:使用StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append(i);
}
// 改进方案2:Java8+的StringJoiner
StringJoiner sj = new StringJoiner(",");
for (int i = 0; i < 100; i++) {
sj.add(String.valueOf(i));
}
java复制String s = "a".repeat(1000) + "b";
// 1. 查找单个字符
long start = System.nanoTime();
s.indexOf('b');
// 耗时约2000ns
// 2. 查找短字符串
s.indexOf("aab");
// 耗时约5000ns
// 3. 正则表达式匹配
s.matches(".*aab.*");
// 耗时约50000ns(最慢)
java复制// 简单分隔优先使用字符而非正则
String[] parts = "a,b,c".split(","); // 比split("\\,")快2倍
// 大量替换使用StringBuilder
StringBuilder sb = new StringBuilder(s);
while (sb.indexOf("old") != -1) {
sb.replace(sb.indexOf("old"), sb.indexOf("old")+3, "new");
}
java复制// 正确比较Unicode字符串
String s1 = "café";
String s2 = "cafe\u0301"; // 组合字符形式
System.out.println(s1.equals(s2)); // false
System.out.println(s1.normalize().equals(s2.normalize())); // true
// 字节转换指定编码
byte[] utf8Bytes = "中文".getBytes(StandardCharsets.UTF_8);
String recovered = new String(utf8Bytes, StandardCharsets.UTF_8);
java复制// 陷阱1:==比较对象而非内容
String a = "hello";
String b = new String("hello");
System.out.println(a == b); // false
// 陷阱2:null检查顺序
String s = null;
if (s.equals("test")) {} // NullPointerException
if ("test".equals(s)) {} // 安全
// 陷阱3:大小写敏感比较
System.out.println("Hello".equals("hello")); // false
System.out.println("Hello".equalsIgnoreCase("hello")); // true
java复制// 找出数组中的所有重复元素
public List<Integer> findDuplicates(int[] nums) {
Set<Integer> seen = new HashSet<>();
List<Integer> duplicates = new ArrayList<>();
for (int num : nums) {
if (!seen.add(num)) { // add返回false表示已存在
duplicates.add(num);
}
}
return duplicates;
}
java复制// 斐波那契数列记忆化搜索
Map<Integer, Integer> memo = new HashMap<>();
public int fib(int n) {
if (n <= 1) return n;
if (memo.containsKey(n)) return memo.get(n);
int res = fib(n-1) + fib(n-2);
memo.put(n, res);
return res;
}
通过JMH测试不同集合类的性能(ops/ms):
| 操作 | ArrayList | HashSet | TreeSet |
|---|---|---|---|
| 插入1000元素 | 1250 | 980 | 320 |
| 查询1000次 | 1500 | 1200 | 450 |
| 遍历所有元素 | 2000 | 800 | 600 |
Set选择原则:
HashMap优化要点:
字符串处理准则:
线程安全场景:
问题1:自定义对象在Set中出现重复元素
问题2:TreeSet抛出ClassCastException
问题1:HashMap死循环(Java7及之前)
问题2:内存泄漏
问题1:substring内存泄漏(Java6及之前)
java复制String newStr = new String(oldStr.substring(0,5));
问题2:正则表达式拒绝服务(ReDoS)
java复制// 不安全的正则示例
String regex = "(a+)+b";
String input = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaac";
input.matches(regex); // 可能导致长时间运行
java复制// 使用Stream统计词频
Map<String, Long> freq = words.stream()
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
));
// 筛选出现频率最高的10个单词
List<String> top10 = freq.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(10)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
java复制// 当value不存在时计算(避免重复计算)
Map<String, Integer> map = new HashMap<>();
String key = "someKey";
// 传统方式
if (!map.containsKey(key)) {
map.put(key, computeExpensiveValue(key));
}
// Java8改进方式
map.computeIfAbsent(key, k -> computeExpensiveValue(k));
// 合并统计场景
map.merge(key, 1, Integer::sum);
java复制// 多行字符串文本(Java15+)
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
// 字符串增强方法
String s = " hello ";
s.repeat(3); // " hello hello hello "
s.strip(); // "hello" (去首尾空白)
s.stripLeading(); // "hello " (去首部空白)
s.stripTrailing();// " hello" (去尾部空白)
s.isBlank(); // false (检查是否空白)
java复制// 不可变集合
ImmutableSet<String> immutableSet = ImmutableSet.of("a", "b", "c");
// 多值Map
Multimap<String, Integer> multimap = ArrayListMultimap.create();
multimap.put("key", 1);
multimap.put("key", 2);
// 双向Map
BiMap<String, Integer> biMap = HashBiMap.create();
biMap.put("one", 1);
biMap.inverse().get(1); // "one"
java复制// 字符串处理
StringUtils.isBlank(" "); // true
StringUtils.substringBetween("<tag>content</tag>", "<tag>", "</tag>"); // "content"
// 集合工具
CollectionUtils.isEmpty(collection); // 安全判空
ListUtils.union(list1, list2); // 合并列表
java复制// FastUtil的IntSet
IntOpenHashSet intSet = new IntOpenHashSet(new int[]{1,2,3});
// Eclipse Collections的Primitive Map
IntIntMap intMap = new IntIntHashMap();
intMap.put(1, 100);
intMap.get(1); // 100
java复制// 显式使用字符串池
String s1 = "hello";
String s2 = new String("hello").intern(); // 复用常量池中的"hello"
System.out.println(s1 == s2); // true
// 自定义对象池模式
class ObjectPool {
private static final Map<Key, HeavyObject> pool = new HashMap<>();
public static HeavyObject get(Key key) {
return pool.computeIfAbsent(key, k -> createExpensiveObject(k));
}
}
java复制// 创建只读视图
Set<String> unmodifiableSet = Collections.unmodifiableSet(originalSet);
// 类型安全视图
List<Integer> intList = Collections.checkedList(rawList, Integer.class);
// 同步视图
Map<String, Integer> syncMap = Collections.synchronizedMap(originalMap);
java复制// 动态排序策略
Set<String> set = new TreeSet<>(createComparator(sortType));
private Comparator<String> createComparator(SortType type) {
switch (type) {
case LENGTH: return Comparator.comparing(String::length);
case ALPHABETIC: return String::compareTo;
default: throw new IllegalArgumentException();
}
}
java复制// 使用jol工具分析对象内存
System.out.println(ClassLayout.parseInstance(new HashMap<>(1000)).toPrintable());
// 典型集合内存开销(64位JVM,压缩指针开启)
// ArrayList: 24字节头 + 4字节size + 4字节modCount + 8字节数组引用
// HashMap: 48字节头 + 8字节字段*6 + 数组和节点开销
java复制// 减少字符串内存占用的技巧
// 1. 使用String.intern()共享相同字符串
String s1 = new String("hello").intern();
// 2. 大文本处理使用StringReader/StringWriter流式处理
try (StringWriter writer = new StringWriter()) {
writer.write(largeText);
}
// 3. 考虑使用char[]直接操作
char[] buffer = new char[1024];
java复制// 集合使用后及时清空
List<HeavyObject> tempList = new ArrayList<>();
// 使用后
tempList.clear(); // 或者直接赋null
// 使用弱引用Map缓存
WeakHashMap<Key, Value> cache = new WeakHashMap<>();
// 避免在热点路径上创建临时集合
// 不好的做法
for (String s : list) {
new HashSet<>(Arrays.asList(s)); // 每次循环创建新集合
}