Java对象数组与集合类使用技巧

Java中对象数组不能直接用Arrays.asList转List,因其将整个数组作为单个元素封装;正确做法是用Arrays.stream().toList()或防御性拷贝。

Java中对象数组不能直接用Arrays.asList转成List

这是新手最常踩的坑:把Person

[]传给Arrays.asList(),结果发现修改返回的List不影响原数组,或者调用add()直接抛UnsupportedOperationException

根本原因是Arrays.asList()对基本类型数组和对象数组处理不同——它把对象数组整体当做一个元素封装进List,而不是展开成元素列表。比如Arrays.asList(new Person[]{p1, p2})返回的是一个只含1个元素(即那个数组引用)的List

  • 正确做法是用Stream
    List list = Arrays.stream(personArray).toList();
  • 或手动构造:
    List list = new ArrayList<>(Arrays.asList(personArray));
    (注意这行实际会触发上面说的“包数组”行为,所以必须先确保personArray是泛型安全的;更稳妥写法是用ArrayList::new配合Arrays.stream
  • 如果只是临时遍历,直接用for (Person p : personArray)比转集合更轻量

ArrayList与LinkedList在频繁插入/删除时性能差异明显

很多人以为“链表适合增删”,但没注意前提:必须是在**中间位置随机插入/删除**,且已持有目标位置的引用(比如迭代器)。否则,光是定位就抵消了结构优势。

常见误用场景:用LinkedList做队列但反复调用list.get(i)查中间元素;或用ArrayList在首部add(0, item)——这会导致所有后续元素位移,O(n)时间。

  • 首尾操作:两者都快(ArrayList首部add(0)慢,尾部add()均摊O(1);LinkedList首尾addFirst/addLast都是O(1))
  • 按索引访问:ArrayList.get(i)是O(1),LinkedList.get(i)是O(n)
  • 按值查找:contains()两者都是O(n),无本质区别
  • 真实建议:默认用ArrayList;只有明确需要在迭代中频繁remove()当前元素(用Iterator.remove()),才考虑LinkedList

HashMap的key为自定义对象时必须重写hashCode()和equals()

否则两个逻辑相等的对象(比如new Person("Alice", 30)new Person("Alice", 30))会被当作不同key存入,导致get()查不到、containsKey()返回false

关键点不在“要不要重写”,而在于**必须同时重写且逻辑一致**:如果equals()认为a和b相等,它们的hashCode()必须返回相同值;反之不成立。

  • IDE生成的hashCode()/equals()通常够用,但要注意字段是否可变——如果用可变字段参与计算,之后修改该字段会导致哈希桶错位,再也找不到这个key
  • 推荐用record(Java 14+):
    public record Person(String name, int age) {}
    自动带不可变语义和正确的hashCode()/equals()
  • 如果必须用普通类,把参与hashCode()计算的字段设为final,避免运行时被修改

使用Collections.unmodifiableList()后仍可能被内部修改

Collections.unmodifiableList()返回的只是原始List的一个只读包装,它不阻止你通过原始引用去改内容。也就是说,如果还有别的变量指向同一个ArrayList,那它依然能被add()clear()等操作修改,而只读视图也会跟着变。

  • 真正安全的做法是“防御性拷贝”:
    List safeList = Collections.unmodifiableList(new ArrayList<>(originalList));
  • 更彻底的方式是用List.copyOf()(Java 10+):
    List safeList = List.copyOf(originalList); // 返回不可变且独立副本
  • 注意:unmodifiableList()包装后的对象,调用set()add()等会立即抛UnsupportedOperationException,但get()size()正常工作
真实项目里最容易被忽略的,是对象数组转集合时的语义陷阱,以及不可变包装背后共享引用的问题——这两处错误往往不会立刻报错,而是让数据在某个深夜批量处理时悄悄错乱。