1. Java集合框架进阶:可变参数与Collections工具类实战
作为一名有五年Java开发经验的工程师,我经常需要回顾集合框架的基础知识。今天我想分享两个容易被忽视但极其重要的特性:可变参数和Collections工具类。这些知识点在黑马程序员的JavaSE课程中讲解得非常透彻,但视频中的部分细节需要结合实践才能深刻理解。
2. 可变参数深度解析
2.1 基本语法与本质
可变参数(Varargs)是Java 5引入的语法糖,声明方式是在数据类型后加三个点:
java复制public static void printNames(String... names) {
// 方法体
}
关键点在于理解可变参数的本质——它实际上就是一个数组。编译器会把上面的代码处理成:
java复制public static void printNames(String[] names) {
// 方法体
}
但可变参数提供了更灵活的调用方式:
java复制printNames(); // 合法,相当于new String[0]
printNames("Alice"); // 合法
printNames("A", "B"); // 合法
printNames(new String[]{"X", "Y"}); // 也合法
2.2 使用规范与陷阱
在实际项目中,我总结出几条黄金法则:
-
位置规则:可变参数必须是方法最后一个参数。以下代码会编译错误:
java复制// 错误示例! void demo(int... nums, String name) {} -
唯一性:一个方法只能有一个可变参数。尝试声明两个可变参数会导致编译失败。
-
类型安全:要注意自动装箱拆箱的问题。比如:
java复制void process(int... nums) {} void process(Integer... nums) {} // 这两个方法不能共存,会导致编译错误 -
性能考量:频繁调用可变参数方法会生成大量临时数组,在性能敏感场景要谨慎使用。
2.3 实战技巧
一个有用的模式是将可变参数与泛型结合:
java复制@SafeVarargs
public static <T> List<T> asList(T... elements) {
List<T> list = new ArrayList<>();
Collections.addAll(list, elements);
return list;
}
注意这里使用了@SafeVarargs注解来抑制警告,这是Java 7引入的专门用于可变参数的注解。
3. Collections工具类详解
3.1 核心方法剖析
java.util.Collections这个工具类提供了许多静态方法,我重点介绍几个最常用的:
3.1.1 addAll方法
java复制List<String> list = new ArrayList<>();
Collections.addAll(list, "A", "B", "C");
这个方法比直接调用list.add("A")等更高效,特别是在批量添加元素时。它的实现利用了System.arraycopy,性能更好。
3.1.2 shuffle方法
洗牌算法在实际应用中非常有用,比如:
- 随机展示商品
- 考试题目乱序
- 游戏卡牌随机排序
java复制List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Collections.shuffle(numbers); // 随机打乱
注意:shuffle使用的是默认的随机源。如果需要可重复的随机序列,可以传入Random对象:
java复制Collections.shuffle(list, new Random(123)); // 固定种子
3.1.3 sort方法
排序是最常用的操作之一:
java复制List<String> names = Arrays.asList("Bob", "Alice", "Charlie");
Collections.sort(names); // 自然顺序排序
对于自定义对象,需要实现Comparable接口或提供Comparator:
java复制Collections.sort(students, (s1, s2) ->
s1.getScore() != s2.getScore() ?
s2.getScore() - s1.getScore() : // 分数降序
s1.getName().compareTo(s2.getName()) // 姓名升序
);
3.2 性能考量与替代方案
虽然Collections方法很方便,但在大数据量时需要注意:
-
sort方法:对于ArrayList,它使用TimSort算法,时间复杂度O(n log n),空间复杂度O(n)。对于LinkedList,性能会差很多,因为随机访问效率低。
-
shuffle方法:使用Fisher-Yates算法,时间复杂度O(n)。对于不可变集合,会先拷贝一份数据。
在Java 8+中,可以考虑使用Stream API作为替代:
java复制List<String> sorted = list.stream()
.sorted(Comparator.comparing(String::length))
.collect(Collectors.toList());
4. 斗地主案例实战
4.1 扑克牌建模
首先定义Card类,关键点在于:
- 使用size属性表示牌的大小顺序
- 重写toString()方便输出
- 提供必要的getter/setter
java复制public class Card {
private String number; // 3-10,J,Q,K,A,2
private String color; // ♠,♥,♣,♦
private int size; // 比较大小用
// 构造器、getter/setter省略
@Override
public String toString() {
return color + number;
}
}
4.2 牌局初始化
Room类的构造函数负责初始化54张牌:
java复制public Room() {
String[] numbers = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"};
String[] colors = {"♠", "♥", "♣", "♦"};
int size = 0;
// 生成普通牌
for (String number : numbers) {
size++;
for (String color : colors) {
allCards.add(new Card(number, color, size));
}
}
// 添加大小王
allCards.add(new Card("", "🃏", ++size)); // 小王
allCards.add(new Card("", "👑", ++size)); // 大王
}
4.3 核心游戏逻辑
游戏流程包括洗牌、发牌、排序和抢地主:
java复制public void start() {
// 1. 洗牌
Collections.shuffle(allCards);
// 2. 初始化玩家手牌
List<Card> player1 = new ArrayList<>();
List<Card> player2 = new ArrayList<>();
List<Card> player3 = new ArrayList<>();
// 3. 发牌(留3张底牌)
for (int i = 0; i < 51; i += 3) {
player1.add(allCards.get(i));
player2.add(allCards.get(i+1));
player3.add(allCards.get(i+2));
}
// 4. 排序(从大到小)
sortCards(player1);
sortCards(player2);
sortCards(player3);
// 5. 抢地主(假设player1抢到)
List<Card> lastThree = allCards.subList(51, 54);
player1.addAll(lastThree);
sortCards(player1);
}
4.4 排序算法实现
使用Comparator实现从大到小排序:
java复制private void sortCards(List<Card> cards) {
cards.sort((c1, c2) -> {
// 先比较大小
int sizeCompare = c2.getSize() - c1.getSize();
if (sizeCompare != 0) return sizeCompare;
// 大小相同比较花色(假设黑桃>红心>梅花>方片)
return compareColor(c2.getColor(), c1.getColor());
});
}
private int compareColor(String c1, String c2) {
// 实现花色比较逻辑
}
5. 常见问题与优化建议
5.1 可变参数问题排查
-
空指针问题:
java复制void printLength(String... strs) { System.out.println(strs.length); // 即使不传参数,strs也不是null }调用
printLength()会输出0,而不是NPE。 -
重载混淆:
java复制void process(String s1, String s2) {} void process(String... strs) {}调用
process("A", "B")会优先匹配第一个方法。
5.2 Collections工具类陷阱
-
不可变集合:
java复制List<String> list = Collections.emptyList(); list.add("test"); // 抛出UnsupportedOperationException -
同步集合:
java复制List<String> syncList = Collections.synchronizedList(new ArrayList<>()); // 需要手动同步迭代操作 synchronized(syncList) { for (String s : syncList) {...} }
5.3 斗地主案例优化方向
-
使用枚举改进Card类:
java复制public enum Suit { SPADE, HEART, CLUB, DIAMOND } public enum Rank { THREE, FOUR, ..., TWO } -
引入Game和Player类,增强扩展性
-
实现完整的游戏规则,如出牌验证、胜负判断等
-
添加网络功能,支持多人在线对战
6. 性能优化实战
6.1 集合初始化优化
避免频繁扩容:
java复制// 不好的做法
List<Card> cards = new ArrayList<>(); // 默认容量10
for (int i = 0; i < 54; i++) { // 需要扩容多次
cards.add(new Card(...));
}
// 优化方案
List<Card> cards = new ArrayList<>(54); // 指定初始容量
6.2 排序优化
对于固定排序规则,可以使用静态Comparator:
java复制private static final Comparator<Card> CARD_COMPARATOR =
Comparator.comparingInt(Card::getSize).reversed()
.thenComparing(Card::getColor, this::compareColor);
cards.sort(CARD_COMPARATOR); // 复用Comparator对象
6.3 并行处理
Java 8+可以使用并行流提高性能:
java复制List<Card> shuffled = allCards.parallelStream()
.sorted(Comparator.comparingInt(c -> ThreadLocalRandom.current().nextInt()))
.collect(Collectors.toList());
7. 测试与验证
7.1 单元测试示例
使用JUnit测试可变参数方法:
java复制@Test
void testVarargs() {
assertEquals(0, StringUtils.join().length());
assertEquals("A,B", StringUtils.join("A", "B"));
assertEquals("X", StringUtils.join(new String[]{"X"}));
}
7.2 斗地主案例测试
验证发牌的正确性:
java复制@Test
void testDealCards() {
Room room = new Room();
room.start();
List<List<Card>> hands = room.getPlayerHands();
assertEquals(3, hands.size());
assertEquals(17, hands.get(0).size()); // 初始每人17张
assertEquals(3, room.getLastThreeCards().size());
}
8. 扩展思考
8.1 可变参数在日志中的应用
日志框架通常大量使用可变参数:
java复制logger.debug("User {} has {} orders", userId, orderCount);
8.2 集合工具类的线程安全方案
除了Collections.synchronizedXXX,还可以考虑:
- CopyOnWriteArrayList
- ConcurrentHashMap
- Java 9的List.of()创建的不可变集合
8.3 函数式编程结合
Java 8+中,可以结合Stream API和Collections:
java复制List<String> filtered = list.stream()
.filter(s -> s.length() > 3)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
));
9. 总结回顾
通过这个复习,我重新梳理了几个关键点:
- 可变参数是语法糖,本质是数组,但要记住它的特殊规则
- Collections工具类提供了许多现成算法,避免重复造轮子
- 集合操作要注意性能影响,特别是大数据量时
- 斗地主案例展示了集合的综合运用,可以进一步扩展为完整游戏
这些知识点看似基础,但在实际开发中经常用到。建议读者自己动手实现一个完整的扑克牌游戏,会遇到很多值得思考的问题。