HashMap 面试必会知识点详解(JDK 8)

面向 Java / Android 高级开发岗位的复习文档。本文以 JDK 8 的 java.util.HashMap 为主线,重点讲清数据结构、hash 扰动、put/get、冲突处理、扩容、红黑树、线程安全与高频追问。

版本提醒: 不同 JDK 版本的实现细节可能变化。面试回答时最好明确"以 JDK 8 为例"。


目录

  1. [HashMap 是什么](#HashMap 是什么)
  2. 底层数据结构
  3. [Node 节点结构](#Node 节点结构)
  4. [hash 扰动函数](#hash 扰动函数)
  5. [为什么容量通常是 2 的幂](#为什么容量通常是 2 的幂)
  6. [put 的完整流程](#put 的完整流程)
  7. [hash 冲突与 equals](#hash 冲突与 equals)
  8. [get 的查找流程](#get 的查找流程)
  9. 扩容条件、负载因子与阈值
  10. [resize 扩容迁移原理](#resize 扩容迁移原理)
  11. 链表为什么转红黑树
  12. [JDK 7 与 JDK 8 的主要区别](#JDK 7 与 JDK 8 的主要区别)
  13. [HashMap 为什么线程不安全](#HashMap 为什么线程不安全)
  14. [HashMap 的 null 规则](#HashMap 的 null 规则)
  15. 时间复杂度与性能
  16. [equals 和 hashCode 的契约](#equals 和 hashCode 的契约)
  17. [迭代器与 fail-fast](#迭代器与 fail-fast)
  18. 如何正确初始化容量
  19. [与 ConcurrentHashMap 的区别](#与 ConcurrentHashMap 的区别)
  20. 高频面试题速答
  21. 源码阅读路线与总结

1. HashMap 是什么

HashMap<K, V> 是 Java 集合框架中的键值映射容器。它根据 key 的哈希值定位存储桶,再通过 key 的相等性判断找到具体映射。

java 复制代码
Map<String, Integer> scores = new HashMap<>();
scores.put("Tom", 90);
scores.put("Jerry", 85);

Integer score = scores.get("Tom"); // 90

主要特点(以常见 JDK 8 实现为准):

  • 不保证 key 的迭代顺序。
  • 允许一个 null key,允许多个 null value。
  • 非线程安全;多个线程并发修改需要额外同步或使用并发容器。
  • 平均情况下,put、get、remove 接近 O(1)。
  • 容量不足时会扩容;冲突严重的桶可能树化。

面试回答模板:

HashMap 是基于哈希表的 Map 实现。JDK 8 中底层是 Node 数组,桶内元素通过链表或红黑树组织。它先对 key 的 hashCode 做扰动,再通过 (n - 1) & hash 定位桶;发生冲突时比较 hash 并结合 ==、equals() 判断 key 是否相同。元素数量超过阈值时会扩容。HashMap 不保证顺序,也不是线程安全容器。

2. 底层数据结构

JDK 8 的主要结构:

text 复制代码
HashMap
  |
  +-- table: Node<K,V>[]
        |
        +-- bucket[0] -> null
        +-- bucket[1] -> Node -> Node -> ...
        +-- bucket[2] -> TreeNode(树化后)
        +-- ...
  • 数组 table:根据 hash 快速定位桶。
  • 链表:多个元素映射到同一桶时,且该桶尚未树化,就通过链表保存。
  • 红黑树:桶内冲突严重且满足条件时,用平衡树降低查找复杂度。

为什么采用这种组合?

  • 单纯链表查找可能退化到 O(n)。
  • 单纯红黑树会增加节点结构和维护成本。
  • 数组 + 链表 + 红黑树在常规场景下兼顾效率和空间。

3. Node 节点结构

JDK 8 中普通节点的核心结构可概括为:

java 复制代码
static class Node<K, V> implements Map.Entry<K, V> {
    final int hash;
    final K key;
    V value;
    Node<K, V> next;

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final K getKey() { return key; }
    public final V getValue() { return value; }
    public final V setValue(V newValue) {
        V old = value;
        value = newValue;
        return old;
    }
}

重点:

  • hash:插入时计算并保存的扰动后哈希值。
  • key:声明为 final,节点创建后不能换成另一个 key 引用。
  • value:可以更新。
  • next:链表中的下一个节点;树化节点使用 TreeNode 扩展结构。

同一个 key 再次 put 时,一般是更新已有节点的 value,而不是插入重复 key。

4. hash 扰动函数

JDK 8 的核心逻辑:

java 复制代码
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

4.1 为什么不直接使用 key.hashCode()?

桶索引计算为:

java 复制代码
index = (n - 1) & hash;

当容量 n = 16 时,n - 1 = 15,二进制是 0000...1111。与它做 AND,实际上主要取 hash 的低 4 位。

如果多个 key 的低位相同,即使高位不同,也可能被放进同一个桶。

4.2 (h >>> 16) 是什么?

  • >>> 是无符号右移。
  • 对 32 位 int 右移 16 位,相当于把原来的高 16 位移到低 16 位。
  • 再执行 h ^ (h >>> 16),让原 hash 的高位信息参与低位计算。

简化示意:

text 复制代码
原 h:       [ 高 16 位 ][ 低 16 位 ]
h >>> 16:   [    0     ][ 原高16位 ]
异或结果:   [ 高 16 位 ][ 低位与高位混合 ]

目的不是保证绝不冲突,而是让 hash 的低位分布更充分,降低部分冲突概率。

5. 为什么容量通常是 2 的幂

HashMap 用下面的方式计算桶索引:

java 复制代码
index = (n - 1) & hash;

当 n 是 2 的幂时,n - 1 的二进制低位全为 1。例如:

text 复制代码
n     = 16  -> 00010000
n - 1 = 15  -> 00001111

这样可以通过位运算快速取出低位作为索引,避免使用取模运算。

JDK 8 在构造时传入非 2 的幂容量,通常会通过 tableSizeFor() 将其调整为大于等于目标值的最小 2 的幂(受最大容量限制)。

注意:容量为 2 的幂是为了让索引计算与扩容优化成立;不是说只要容量为 2 的幂就能完全避免哈希冲突。

6. put 的完整流程

调用:

java 复制代码
map.put(key, value);

总体流程:

text 复制代码
put(key, value)
   |
   v
计算扰动后的 hash
   |
   v
table 是否初始化?-- 否 --> 初始化数组
   |
   v
index = (n - 1) & hash
   |
   +-- 桶为空:直接创建节点
   |
   +-- 桶非空:
         |
         +-- 首节点 key 相同:更新 value
         |
         +-- 桶是红黑树:调用树插入逻辑
         |
         +-- 桶是链表:遍历查找
                 |
                 +-- 找到相同 key:更新 value
                 |
                 +-- 没找到:追加新节点
                           |
                           +-- 达到树化判断条件:尝试树化
   |
   v
若是新映射:size++,modCount++
   |
   v
size > threshold?-- 是 --> resize()

6.1 延迟初始化

java 复制代码
if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

new HashMap<>() 通常不会立即分配默认的 16 个桶。第一次插入时才初始化内部数组,这叫延迟初始化。

6.2 桶为空

java 复制代码
tab[i] = newNode(hash, key, value, null);

直接将新节点放到该桶。

6.3 首节点是否为相同 key

java 复制代码
if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k)))) {
    e = p;
}

判断顺序:

  1. 先比较 hash,hash 不同则一定不是同一个映射 key。
  2. 再判断引用是否相同 ==。
  3. 最后用 equals() 判断逻辑相等。

6.4 链表遍历

遍历链表:

  • 如果找到 hash 相同且 key 相等的节点,更新其 value。
  • 如果走到链表末尾仍未找到,则创建新节点追加。
  • 新节点插入后,可能触发树化判断。

6.5 更新 value 的返回值

put 返回旧 value;如果之前没有该 key,则返回 null。因此,返回 null 不一定能区分"原来没有 key"和"原来的 value 就是 null"。

7. hash 冲突与 equals

7.1 什么是 hash 冲突?

不同 key 的 hash 值可能相同;即使 hash 值不同,也可能在取桶索引时得到同一个 index。

text 复制代码
keyA -> hashA -> bucket[3]
keyB -> hashB -> bucket[3]

这就是桶冲突。哈希值是有限位数,而 key 的可能数量远远更多,因此冲突不可避免。

7.2 为什么必须调用 equals?

hashCode() 不是唯一标识。HashMap 不能仅凭 hash 相同就认定 key 相同。

java 复制代码
String a = new String("abc");
String b = new String("abc");

System.out.println(a == b);        // false:不同对象
System.out.println(a.equals(b));   // true:内容相等
System.out.println(a.hashCode() == b.hashCode()); // true

HashMap 通过 hash 缩小范围,再通过 == / equals() 精确判断。

7.3 自定义 key 示例

java 复制代码
final class UserKey {
    private final int id;

    UserKey(int id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        return 1; // 故意制造冲突,仅用于演示
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof UserKey)) return false;
        UserKey other = (UserKey) obj;
        return id == other.id;
    }
}
java 复制代码
Map<UserKey, String> map = new HashMap<>();
map.put(new UserKey(1), "A");
map.put(new UserKey(2), "B");

System.out.println(map.size()); // 2:hash 相同,但 equals 为 false
map.put(new UserKey(1), "C");
System.out.println(map.size()); // 2:逻辑上相同的 key,更新 value

8. get 的查找流程

java 复制代码
V get(Object key)

核心步骤:

  1. 计算 key 的扰动 hash。
  2. 根据 (n - 1) & hash 找到桶。
  3. 检查桶的第一个节点。
  4. 如果首节点不匹配:
    • 若为树节点,按红黑树逻辑查找;
    • 否则遍历链表。
  5. 找到 hash 相同且 key 相等的节点后返回 value;找不到返回 null。

逻辑示意:

text 复制代码
get(key)
  |
计算 hash 与 index
  |
定位 table[index]
  |
比较 hash + == / equals
  |
链表遍历或红黑树查找
  |
返回 value / null

9. 扩容条件、负载因子与阈值

常见默认值:

java 复制代码
DEFAULT_INITIAL_CAPACITY = 16;
DEFAULT_LOAD_FACTOR = 0.75f;

阈值通常为:

java 复制代码
threshold = capacity * loadFactor;

默认容量 16 时,阈值通常是 12。新映射插入后,如果 size > threshold,会触发扩容。

9.1 负载因子是什么?

负载因子控制数组空间利用率和冲突程度之间的折中:

  • 较低:更早扩容,通常冲突更少,但占用更多数组空间。
  • 较高:空间利用率更高,但桶冲突可能更多。

0.75 是 JDK 的常用默认折中,并不是所有业务下的数学最优值。

9.2 是不是每次 put 都可能扩容?

不是。通常只有新增映射后 size 超过 threshold 才会触发容量扩展;更新已有 key 的 value 不增加 size,不会因为这次更新而触发该扩容判断。

9.3 构造参数注意事项

java 复制代码
new HashMap<>(64);

参数表示初始容量需求,不是"插入 64 个元素才扩容"的阈值。内部容量会调整为合适的 2 的幂,阈值再根据负载因子计算。

10. resize 扩容迁移原理

这是 JDK 8 的高频重点。

10.1 容量翻倍

常规情况下:

text 复制代码
16 -> 32 -> 64 -> 128

源码通过:

java 复制代码
newCap = oldCap << 1;

实现容量翻倍。

10.2 为什么元素只会留在原位置或移动 oldCap?

旧索引:

java 复制代码
oldIndex = hash & (oldCap - 1);

新容量为 newCap = oldCap * 2,新索引:

java 复制代码
newIndex = hash & (newCap - 1);

新掩码相比旧掩码只多出 oldCap 对应的那一位。因此新索引只有两种可能:

text 复制代码
newIndex = oldIndex
或
newIndex = oldIndex + oldCap

10.3 (e.hash & oldCap) == 0 是什么含义?

java 复制代码
if ((e.hash & oldCap) == 0) {
    // 留在原索引 j
} else {
    // 移到 j + oldCap
}

oldCap 本身是 2 的幂,它对应扩容时新增的那一位。

  • 与运算结果为 0:新增位为 0,留在 j。
  • 与运算结果非 0:新增位为 1,移动到 j + oldCap。

10.4 示例:16 扩到 32

旧桶索引为 5,扩容后只可能去 5 或 21:

text 复制代码
原位置:5
新位置:5 + 16 = 21

源码通过 hash & 16 判断,不需要重新调用 key.hashCode(),也不必重新计算完整 hash。

10.5 链表拆分

JDK 8 将旧桶链表拆成两条:

  • lo 链表:留在原索引 j。
  • hi 链表:移动到 j + oldCap。

通过 head/tail 方式拼接,保持各自链表中节点的相对顺序,并在结尾将尾节点的 next 置为 null。

10.6 扩容成本

扩容需要创建新数组并迁移已有节点,因此一次扩容是 O(n) 级别操作。不过在容量逐步翻倍的情况下,普通 put 的摊还时间复杂度仍通常为 O(1)。

11. 链表为什么转红黑树

JDK 8 常见常量:

java 复制代码
TREEIFY_THRESHOLD = 8;
UNTREEIFY_THRESHOLD = 6;
MIN_TREEIFY_CAPACITY = 64;

需要理解的细节:

  • TREEIFY_THRESHOLD = 8 是触发树化判断的阈值,不应简单说成"链表一到 8 个节点就一定树化"。
  • 在 putVal 的链表插入逻辑中,binCount 的计数方式会影响触发时机。
  • 如果 table 容量小于 64,treeifyBin() 通常优先尝试扩容,而不是立即树化。
  • 扩容拆分树桶时,如果拆分后的某一侧节点数较少,可能退化为链表;UNTREEIFY_THRESHOLD 常用于相关判断。

为什么红黑树可以改善性能?

  • 链表查找最坏 O(n)。
  • 红黑树保持近似平衡,查找通常为 O(log n)。

引入树结构也有额外内存和维护成本,所以只在冲突严重时采用。

12. JDK 7 与 JDK 8 的主要区别

对比点 JDK 7(典型实现) JDK 8
桶内结构 数组 + 链表 数组 + 链表 + 红黑树
冲突严重时 链表可能很长 满足条件时树化
插入链表 典型实现使用头插 使用尾部追加,便于保持拆分顺序
扩容迁移 重新计算/定位元素位置 利用 hash & oldCap 将节点拆到原位置或 原位置 + oldCap
并发安全 不安全 仍然不安全

不要把"JDK 8 解决了 HashMap 线程安全问题"当成结论:JDK 8 的 HashMap 依旧不是线程安全容器。

13. HashMap 为什么线程不安全

HashMap 没有为普通并发读写提供完整的同步保障。

多个线程同时修改可能出现:

  • 更新覆盖、数据丢失。
  • size 等内部状态不符合调用方预期。
  • 并发扩容或链表修改期间出现不一致的结构与读取结果。
  • 读线程可能看不到另一个线程刚写入的值;没有同步关系时也不具备所需的可见性保证。

JDK 7 的某些并发扩容交错可能导致链表成环、读取时循环;JDK 8 改了迁移实现,但不代表并发修改变安全。

解决方式:

  • 有同步需求时,使用 ConcurrentHashMap。
  • 简单场景可用 Collections.synchronizedMap(new HashMap<>()),但遍历时也要按其要求同步。
  • 多线程只读且安全发布、无后续修改的场景,需要根据具体对象生命周期判断。

14. HashMap 的 null 规则

HashMap 允许:

java 复制代码
map.put(null, "A"); // 允许
map.put("name", null); // 允许
map.put("age", null);  // 允许
  • null key 最多一个,因为 Map 中 key 不能重复。
  • null value 可以有多个。
  • JDK 8 的 hash(null) 返回 0,null key 会映射到索引 0(因为索引计算为 (n - 1) & 0)。

注意:get(key) == null 可能表示 key 不存在,也可能表示该 key 对应的 value 就是 null。可以用 containsKey(key) 区分。

15. 时间复杂度与性能

操作 / 情况 常见复杂度 说明
get / put 平均情况 O(1) hash 分布较好、冲突较少
链表桶查找 O(k) k 为该桶链表长度
树化桶查找 通常 O(log k) 红黑树结构
扩容 O(n) 需要迁移已有节点
全量遍历 O(capacity + size) 需要扫描 table 与元素

面试时不要无条件回答"HashMap 查询永远是 O(1)"。更准确的说法是:平均 O(1),具体取决于 hash 分布、桶结构和扩容等情况。

16. equals 和 hashCode 的契约

Java 的基本约定:

  1. 如果 a.equals(b) 为 true,则 a.hashCode() == b.hashCode() 必须成立。
  2. hashCode() 相同,不要求 equals() 为 true。
  3. 在参与比较的字段未改变时,多次调用 hashCode() 应保持一致。

错误示例:只重写 equals(),却没有正确重写 hashCode()。逻辑上相等的对象可能被分配到不同桶,导致 Map 查找异常。

更重要的实践建议:不要修改已经作为 HashMap key 的对象中参与 equals() / hashCode() 计算的字段。

例如,插入时 id = 1,后来把 id 改成 2,查找时新 hash 可能定位到另一个桶,原映射就可能难以通过该 key 找到。

17. 迭代器与 fail-fast

HashMap 的迭代器通常使用 modCount 检测结构性修改。

  • 新增或删除映射通常属于结构性修改。
  • 只更新已有 key 的 value 通常不属于结构性修改。
  • 迭代过程中检测到未通过迭代器自身允许的结构性修改,可能抛出 ConcurrentModificationException。

这叫 fail-fast(快速失败)。

重要:fail-fast 是尽力而为的错误检测机制,不能作为线程同步手段,也不保证每次并发修改都一定抛异常。

18. 如何正确初始化容量

如果预估要放入 expectedSize 个元素,且希望在插入期间尽量避免扩容,可以按负载因子估算所需容量,再让 HashMap 调整到合适的 2 的幂。

java 复制代码
int expectedSize = 100;
float loadFactor = 0.75f;
int initialCapacity = (int) (expectedSize / loadFactor) + 1;

Map<Integer, String> map = new HashMap<>(initialCapacity);

这只是常见估算方式;极大容量要注意整数溢出和最大容量限制。不要简单认为 new HashMap<>(100) 就一定能无扩容存放 100 个元素,因为构造参数和最终阈值不是同一个概念。

19. 与 ConcurrentHashMap 的区别

对比点 HashMap ConcurrentHashMap(JDK 8)
线程安全 否 是,支持并发访问
并发控制 无内置完整同步 CAS、synchronized 等机制
null key/value 允许 不允许
冲突结构 链表 / 红黑树 链表 / 红黑树
扩容 普通单线程迁移逻辑 可由多个线程协助扩容
适用场景 无并发修改或外部同步 多线程共享映射

ConcurrentHashMap 不是简单地给整个 HashMap 加一把大锁。JDK 8 通过 CAS、桶级同步以及协助扩容等机制提高并发度。

20. 高频面试题速答

Q1:HashMap 底层是什么?

JDK 8 中是 Node 数组,桶内使用链表或红黑树。数组用于快速定位桶,链表和红黑树处理冲突。

Q2:(h = key.hashCode()) ^ (h >>> 16) 是什么?

hash 扰动函数。将高 16 位信息混入低位,改善低位分布,降低部分冲突概率。

Q3:为什么用 (n - 1) & hash?

容量为 2 的幂时,n - 1 的低位全为 1,可以用位运算快速得到桶索引。

Q4:hash 相同就认为 key 相同吗?

不是。hash 只用于定位或缩小范围;还要通过引用相同 == 或 equals() 判断 key 是否相同。

Q5:为什么要重写 equals 和 hashCode?

让逻辑相等的对象具有一致的 hash 值,并能按业务定义判断相等,否则 Map 的查找和去重行为可能不符合预期。

Q6:默认容量和负载因子是多少?

常见默认值是容量 16、负载因子 0.75;内部 table 通常延迟到首次插入时初始化。

Q7:什么时候扩容?

新映射插入后,如果 size > threshold,通常触发扩容。默认容量 16、负载因子 0.75 时,初始阈值通常为 12。

Q8:JDK 8 扩容为什么看 hash & oldCap?

容量翻倍只新增一个索引位。该位为 0,节点留在原位置;为 1,节点移动到 原位置 + oldCap。

Q9:链表达到 8 个节点就一定树化吗?

不一定。树化触发判断还与插入计数有关;若数组容量小于 64,通常先尝试扩容。

Q10:HashMap 线程安全吗?

不安全。并发修改可能导致更新丢失、状态不一致等问题;共享并发场景应考虑 ConcurrentHashMap 或适当同步。

Q11:HashMap 为什么允许一个 null key?

JDK 8 对 null key 的 hash 直接返回 0,因此会映射到索引 0。Map 的 key 唯一性规则仍然适用,所以最多一个 null key。

Q12:get() 返回 null 代表什么?

可能 key 不存在,也可能 key 存在但 value 为 null。使用 containsKey() 可进一步判断。

Q13:HashMap 的迭代顺序稳定吗?

不保证。不要依赖它的遍历顺序;需要可预测顺序时,考虑 LinkedHashMap 或 TreeMap,具体按需求选择。

Q14:扩容时为什么不用重新调用 key.hashCode()?

节点已经保存了扰动后的 hash 字段。JDK 8 可以利用这个 hash 和新增索引位完成迁移,不需要再次调用 key 的 hashCode()。

Q15:把可变对象当 key 有什么风险?

如果参与 equals() / hashCode() 的字段在插入后发生变化,查找时可能定位到不同桶,从而无法按预期找到原映射。

21. 源码阅读路线与总结

建议按以下顺序阅读 JDK 8 的 HashMap:

  1. hash(Object key):看扰动函数。
  2. put() / putVal():看桶定位、key 相等判断、链表与树插入。
  3. get() / getNode():看查找。
  4. resize():看扩容与链表拆分。
  5. treeifyBin():看树化条件。
  6. TreeNode 相关方法:了解红黑树插入、查找、拆分。
  7. HashIterator:看迭代器和 modCount。

最后把整个过程背成一条链:

text 复制代码
put(key, value)
   |
   v
hashCode()
   |
   v
h ^ (h >>> 16)
   |
   v
(n - 1) & hash
   |
   v
定位 table[index]
   |
   +-- 空桶:创建节点
   |
   +-- 冲突:hash + == / equals
                 |
                 +-- 相同 key:更新 value
                 |
                 +-- 不同 key:链表 / 红黑树插入
   |
   v
新映射使 size 增加
   |
   v
size > threshold?
   |
   +-- 是:resize()
              |
              v
       hash & oldCap
              |
              +-- 0:留在原位置
              |
              +-- 非 0:移动到原位置 + oldCap

面试优先级建议:

  • 必须讲熟: 底层结构、hash 扰动、索引计算、putVal()、equals/hashCode、扩容。
  • 高级开发要会: 红黑树条件、负载因子、JDK 7/8 区别、null 规则、fail-fast。
  • 专家岗继续深挖: resize() 的链表拆分、红黑树退化、并发问题、ConcurrentHashMap 的 CAS / synchronized / 协助扩容。

复习时尽量结合源码自己手推一个例子:容量从 16 扩到 32,某个桶中的多个节点分别根据 hash & 16 留在原位置或移动到 原位置 + 16。能解释清楚这一步,就真正理解了 JDK 8 HashMap 扩容的关键设计。

相关推荐
伯恩bourne3 小时前
Spring Boot Bean 注册的两种方式:@Component 家族 vs @Configuration + @Bean
java·spring boot·后端
silvamux_3 小时前
curl 测试 OpenAI 兼容 API 的完整流程
java·人工智能·缓存·ai·oneapi
省钱兄--zs4 小时前
深圳24小时自助健身房软硬件解决方案实战指南与开发流程详解
java·数据仓库·spring boot·小程序·架构
顽疲4 小时前
Java vue 养老系统源码详解:月账单 care_month_bill 与 Quartz 扣费 Job 实践
java·开发语言·vue.js·vue养老·springboot养老·java养老
Wang's Blog4 小时前
Java 项目实战: 交友项目 - 安装开发工具与环境准备
java·开发语言·交友
用户21296835261684 小时前
开源了一个 Java 多智能体 LLM 框架 Baton:一个依赖,把 Agent 循环、工具调用、记忆、编排全配齐
java·llm
helmsail4 小时前
05 管理活动与选品
java
天远数科5 小时前
Java数据工程:利用天远人车核验加强版优化智慧高速ETC发行合规体验
java·人工智能
.Bryce.5 小时前
在智能体中使用A2A工具:让大模型自己决定“该找谁帮忙“
android·java·服务器·redis·后端·agent·a2a