当前位置: 首页 > news >正文

这一文,关于Java泛型的点点滴滴 一

作为一个 Java 程序员,用到泛型最多的,我估计应该就是这一行代码:

List<String> list = new ArrayList<>();

这也是所有 Java 程序员的泛型之路开始的地方啊。

不过本文讲泛型,先不从这里开始讲,而是再往前看一下,看一看没有泛型的时候,Java 代码是怎么写的,然后我们才会知道为什么要加入泛型,泛型代码该怎么写。

这里插播一下我的微信公众号,希望大家能够多多关注,我会不定期更新优秀的技术文章:

接下来,开始我们的正文。

为什么要设计泛型

提高代码重用性

没有泛型之前,我们写一个两数相加的函数:

public static int add(int a, int b) {return a + b;
}

看似没问题,对吧。不过这个时候我们想计算 float 类型的加法,那这个函数就不行了,因为他只能计算 int 值。此时就只能再加入一个相同的函数了:

public static float add(float a, float b) {return a + b;
}

现在我们有两个方法能够计算 int 和 float 类型的加法。那现在如果要计算 String 类型的加法呢,这两个方法就又不够用了。面对这样的需求,在没有泛型的支持下,我们只能不断地增加逻辑基本相同的方法,代码重用性极低。
这就是泛型要解决的第一个问题:提高代码重用性。
那在泛型的加持下,我们如何编写这个函数呢?

public static <T extends Number> double add(T a, T b) {return a.doubleValue() + b.doubleValue();
}

这个方法使用了泛型,它能够处理任何类型的数字相加,不需要针对每个类型编写各自的加法方法。这就大大提高了代码的重用性,有了这个方法,那些固定类型的方法就都可以删了。
特别是一些逻辑相同的代码,使用泛型不仅能够提高代码重用性,还能够提高可读性。比如说下面这段代码,真是的是非常好用:

public static <T> void printArray(T[] array) {for (T element : array) {System.out.println(element);}
}

泛型的这个特性虽然很牛了,但是这还不是 Java 要设计泛型的全部原因。因为泛型还有一个作用,那就是保证类型安全。

保证类型安全

在说泛型的这个作用之前,先问大家一个问题,咱们常用的集合 ArrayList 是 Java 哪个版本加入的呢?泛型又是 Java 哪个版本加入的呢?

答案:ArrayList 是 Java 1.2 版本加入的,而泛型是 Java 1.5 加入的。

也就是说,有一段时时间,ArrayList 不是大家普遍认识的带泛型的 ArrayList<T> 这种形式,而是一个只能存放 Object 的列表。

在那一段泛型之光没有照耀到 Java 的日子里,保证类型安全成为了 Java 程序员在使用集合时不得不考虑的事情,考虑下面这一段代码:

ArrayList list = new ArrayList();
list.add("123");
// do some work......
Integer num = (Integer) list.get(0);

这段代码没有使用泛型来使用 ArrayList,我们加入了字符串 "123",但是在使用时,我们假定程序员忘记了加入的类型,他只记得好像应该是数字,于是在获取时就直接使用了 Integer 类强转。

这样的代码是能通过编译的,但是在运行的时候,会崩溃:

 Caused by: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer

这是一个典型的未使用泛型,而导致的类型安全无法保证,引发的崩溃。让程序员去保证类型安全,本身是不靠谱的做法,特别是在这种都是 Object 对象的列表中,鬼都不会知道存着的是个什么鬼。

这个时候就需要泛型出场了,泛型能够在编译时保证类型安全。例如上面的代码,我们加入泛型:

ArrayList<String> list = new ArrayList<>();
list.add("123");
Integer num = (Integer) list.get(0);

首先,ArrayList 加入泛型后,我们就知道这个列表是只能存入 String 类型的,也就不会将其转换为 Integer。那如果我非要转换呢,javac 编译器就会报错:

错误: 不兼容的类型: String无法转换为IntegerInteger num = (Integer) list.get(0);

这样类型安全就可以在编译时得到保证,不会出现在运行时的崩溃。

例子的代码很简单,大家可能看不到这一点对于软件开发有多重要,在大型复杂的项目中,这种类型安全的保证,是能减少很多运行时的崩溃的。特别是,一般像这种类型不一致的崩溃很多都是偶现的,偶现的 BUG 是最恶心的,因此使用泛型保证类型安全是十分必要的。

消除强制类型转换

泛型的这个作用其实就是上面保证类型安全这一点带来的。没有用泛型时,需要我们使用强制类型转化,但是加入泛型后,编译器已经能够知道我们存入的是什么类型,因此也就不需要我们进行强制类型转换了。

既然泛型有那么大的作用,那我们就赶紧把泛型用起来吧。

使用泛型

这一节,我们来看看如何使用系统提供的泛型类,以及其中需要注意的事项。

最常用到泛型的地方便是集合了,使用这些泛型集合类时,只需要把具体泛型参数 <T> 替换为需要的类型即可,例如 ArrayList<String>ArrayList<Number>Map<String, Integer> 等。

如果在使用泛型类时不指定类型参数,编译器会给出警告,且只能将 <T> 视为 Object 类型。这个时候就需要程序员自己去保证类型安全了,因此强烈不建议这么做,因为这样容易将类型转换异常带到运行时中去。

使用泛型基本就需要注意以上两点,下面介绍一下在使用泛型时的注意事项,这也是大家很少关注到的向上转型的问题。

在 Java 中,ArrayList<T> 是实现了 List<T> 接口,也就是说它可以向上转型为 List<T>

public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable

那么问题就来了,当泛型参数不同时,还能向上转型么,说具体一点,ArrayList<String> 能转型为 List<Number> 么?

答案是不行的:

ArrayList List = new ArrayList<String>();    //Raw use of parameterized class 'ArrayList'
List<Integer> list = new ArrayList<String>();    //直接报错

为什么 Java 不允许这么转型呢?因为运行转型的话,那么对于一个 ArrayList<String> 的容器,我将其转型为 ArrayList<Integer> 就可以往里面加入 Integer 对象了,这明显会造成 ClassCastException。泛型的存在用于限定类型的,这么一搞,泛型就失去了其作用。

这里,大家可以简单理解为,当泛型参数不一样时,两个类就没有太大关系了。例如 ArrayList<Integer>List<Number> 两者完全没有继承关系。

编写泛型

知道怎么使用系统的泛型之后,我们现在就来看看如何编写自己的泛型类。

泛型作为对类型进行限制的一种方式,我们编写泛型代码,也就是对使用我们代码的人进行一种限制。在这种情况下,我们是作为其他程序员的底层,向上提供某种框架代码,让其他程序员能够在我们设定的框架中更容易地编写代码实现功能。这有点类似于库的开发者,或是框架开发者,作为这种角色,写好泛型代码就更显得尤为重要了。毕竟,你也不想让别人说,这代码写得就跟一坨屎一样吧。

编写泛型类

编写泛型类,是比普通类要复杂的。这里我们就用 Pair<F, S> 这个类作为目标,一步一步编写出一个合格的泛型类。Pair 类是 Android 开发中一个简单的使用工具类,用于存储一对相关联的对象。

我们的第一版 Pair 只能使用没有使用泛型:

public class Pair {public final String first;public final String second;
}

那这肯定是不行的,因为这个 Pair 只能存放 String 类型的 first 和 second,那了能够存放所有类型,我们就使用泛型 <T>

public class Pair<T> {public final T first;public final T second;
}

我们把 firstsecondT 来修饰,表示其这两个成员变量是 T 类型的。而这个 T 类型,Java 是不知道的,我们必须声明告诉 Java 这是一个类型,因此类名从 Pair 变成了 Pair<T>,后面的 <T> 就是我们的泛型类型声明。

上面的代码看上去没问题,但是这个 Pair<T> 只能存放的 firstsecond 必须是相同的类型 T,那不同类型的怎么办呢?这时候我们再加一个泛型不就行了:

public class Pair<F, S> {public final F first;public final S second;
}

在加入两个泛型之后,firstsecond 的类型对应不同的泛型,这样就可以表示不同的类型了,注意 FS 这两个不同的泛型都需要在类上进行声明。

我们在为 Pair<F, S> 添加个构造方法:

public class Pair<F, S> {public final F first;public final S second;public Pair(F first, S second) {this.first = first;this.second = second;}
}

这算是一个简单的泛型类,那接下来,我们再为它编写一个泛型方法。

编写泛型方法

此处的泛型方法是指静态方法,而不是成员方法。这两种方法在使用泛型时是有一些区别的,其中最重要的一点就是,静态方法是不能使用类上声明的泛型类型,必须得自己声明泛型类型。例如,下面的代码将编译错误:

public static class Pair<F, S> {public final F first;public final S second;//编译错误,F、S 类型不能在 static 方法上使用public static Pair<F, S> create(F a, S b) {return new Pair<F, S>(a, b);}
}

可以想一想,为什么静态方法不能使用类上已经声明的泛型类型呢?

在回答这个问题之前,我们可以先想一下,类上的泛型类型,是在什么时候确定下来的呢?是在类创建的时候,我们在 new 的时候是需要提供具体类型的,这个时候泛型就被具体化为某个特定类型。不同的对象可能被创建为不同的类型,而静态方法只跟类相关,跟具体对象无关,而这些泛型又是跟具体对象相关的。所以静态对象不能使用类上声明的泛型也就变得合理了。

那要想使静态方法使用泛型,那就必须这个静态方法自己声明泛型:

public static <F, S> Pair <F, S> create(F a, S b) {return new Pair<F, S>(a, b);
}

这个静态方法在函数名前使用 <F, S> 来声明了两个泛型,那么后续这两个泛型就可以在这个函数中使用了。此时注意,这里的 FS 虽然与 Pair 上的 FS 泛型看似相同,实际上是没有任何关系的。所以为了避免产生误会,一般都会使用不同的泛型名,例如将这个方法的 <F, S> 变成 <A, B>

public static class Pair<F, S> {public final F first;public final S second;public static <A, B> Pair <A, B> create(A a, B b) {return new Pair<A, B>(a, b);}
}

这样才能够清楚地将静态方法的泛型类型和实例类型的泛型类型区分开。

在使用时,我们可以使用如下代码创建一个 Pair<F, S> 实例:

Pair<String, Integer> pair = Pair.create("123", 123);

这里总结一下编写泛型需要注意的几点:

  • 编写泛型时,需要定义泛型类型 <T>
  • 静态方法不能引用类上的泛型类型 <T>,必须定义自己方法特有的泛型类型;
  • 泛型可以同时定义多个,例如 <F, S><F、S、T>

在这里我们需要注意泛型的一个限制,那就是不能使用泛型类型直接创建对象。这一点也好理解,T 是什么类型只有在使用时,指定了泛型的具体类型才能确定。T 类型是一个抽象的类型,它是无法直接 new 出来的,就像你无法直接 new 一个 interface 一样。例如下面的代码是错误的:

public static class Pair<F, S> {public final F first;public final S second;public Pair(F first, S second) {this.first = new F();        //错误this.second = new S();       //错误}
}

这里使用 F 类型的默认构造,设想一下假如这个类型被确定为一个没有默认构造方法的类型呢。所以使用泛型类型创建对象是不行的。

Java 的泛型实现方式:类型擦除

上面的几节介绍了泛型的好处,泛型的使用,那这一节我们就来看看 Java 是如何实现泛型技术的。

首先,泛型编程并非 Java 特有的,在其他语言 C++、C# 上都有类似的技术,只不过名称不同而已,例如 C++ 上叫模版。在这些技术的加持下,程序员可以编写与具体类型无关的代码,只需要在使用时指定具体类型,从而提高代码的复用性;并且在编译时进行类型检查,减少运行时错误。

Java 的泛型是通过类型擦除(Type Erasure)来实现的。也就是说在编译时将泛型类型擦除,替换为其上限类型(通常为 Object),并在必要时插入类型转换。这种机制在编译时处理泛型类型,而在运行时移除了所有的泛型信息,因此叫做类型擦除。

这也就意味着,Java 的泛型是由编译器实现的,在编译成 class 文件时类型信息已经被擦除了,因此运行时,Java 虚拟机是没有任何泛型信息的。

例如上面我们编写的 Pair 的这个类,在我们看来,它是这样的,在源代码阶段,里面是包含泛型信息的:

public static class Pair<F, S> {public final F first;public final S second;public Pair(F first, S second) {this.first = first;this.second = second;}public static <A, B> Pair<A, B> create(A a, B b) {return new Pair<A, B>(a, b);}
}

那么在虚拟机的视角,它是这样的:

public class Pair {private Object first;private Object last;public Pair(Object first, Object last) {this.first = first;this.last = last;}
}

从这里就能看到,这个 Pair 在运行时已经没有泛型信息了,所有的泛型类型都被替换为了 Object

那么既然我们定义的泛型类型最终都变成了 Object,那我们就知道了 Java 泛型的一个局限:泛型类型 <T> 不能是基本类型。
因为像 intfloat 这些基本类型不是 Object 的子类,所以我们必须使用包装类:

Pair<float, int> pair = Pair.create(3.15, 123);    //编译错误
Pair<Float, Integer> pair = Pair.create(3.15F, 123);    //编译通过

尽管 Java 的泛型在编译时通过类型擦除机制移除了泛型类型信息,但 Java 编译器会在 class 文件中保留一些泛型信息,以便工具和开发人员能够利用这些信息进行反射和调试。所以如果大家把这个类编译为 class 文件之后,再查看它的反编译的内容,会发现它是有一些泛型信息的。但这并不意味着 JVM 在运行时会携带这些类型信息,既然是类型擦除,也就是说泛型类型参数被擦除并替换为其边界类型,如果没有指定边界,则默认为 Object

这里又引入了边界类型这个概念,在下一篇文章中,我们就来详细聊聊这个边界类型,这也是泛型中比较重要和难的点。

相关文章:

这一文,关于Java泛型的点点滴滴 一

作为一个 Java 程序员&#xff0c;用到泛型最多的&#xff0c;我估计应该就是这一行代码&#xff1a; List<String> list new ArrayList<>();这也是所有 Java 程序员的泛型之路开始的地方啊。 不过本文讲泛型&#xff0c;先不从这里开始讲&#xff0c;而是再往前…...

微信小程序之调查问卷

一、设计思路 1、界面 调查问卷又称调查表&#xff0c;是以问题的形式系统地记载调查内容的一种形式。微信小程序制作的调查问卷&#xff0c;可以在短时间内快速收集反馈信息。具体效果如下所示&#xff1a; 2、思路 此调查问卷采用服务器客户端的方式进行设计&#xff0c;服…...

基于Qt的视频剪辑

在Qt中进行视频剪辑可以通过多种方式实现&#xff0c;但通常需要使用一些额外的库来处理视频数据。以下是一些常见的方法和步骤&#xff1a; 使用FFmpeg FFmpeg是一个非常强大的多媒体框架&#xff0c;可以用来处理视频和音频数据。你可以使用FFmpeg的命令行工具或者其库来实现…...

electron 网页TodoList工具打包成win桌面应用exe

参考&#xff1a; electron安装&#xff08;支持win、mac、linux桌面应用&#xff09; https://blog.csdn.net/weixin_42357472/article/details/140643624 TodoList工具 https://blog.csdn.net/weixin_42357472/article/details/140618446 electron打包过程&#xff1a; 要将…...

数据结构之判断二叉树是否为搜索树(C/C++实现)

文章目录 判断二叉树是否为搜索树方法一&#xff1a;递归法方法二&#xff1a;中序遍历法总结 二叉树是一种非常常见的数据结构&#xff0c;它在计算机科学中有着广泛的应用。二叉搜索树&#xff08;Binary Search Tree&#xff0c;简称BST&#xff09;是二叉树的一种特殊形式&…...

golang长连接的误用

误用一&#xff1a;忘记读取响应的body 由于忘记读取响应的body导致创建大量处于TIME_WAIT状态的连接&#xff08;同时产生大量处于transport.go的readLoop和writeLoop的协程&#xff09; 在linux下运行下面的代码: package mainimport ("fmt""html"&qu…...

Springboot @Validate @Valid 基于复杂嵌套对象的参数校验示例

Springboot Validate Valid 基于复杂嵌套对象的参数校验示例 复杂对象 Data public class Object1 {Length(max 50,message "长度不能超过50位字符")NotBlank(message "名称不能为空")private String name;NotNull(message "不能为空")pri…...

算力共享下的,分级路由转发报文协议与通告

目录 网络双 SLA 约束 一、双SLA约束的定义与背景 二、双SLA约束的应用场景 三、双SLA约束的管理与实施 四、双SLA约束的优势与挑战 算力共享下的,分级路由转发报文协议与通告 基础设施即服务(IaaS)类 型算力资源 函数即服务(FaaS)类型算力服务 软件即服务(SaaS…...

滚动数组详解

滚动数组详解 何为滚动数组&#xff1f;滚动数组是如何优化空间的&#xff1f;交替滚动例题&#xff1a;来自某某轮廓线DP的题目 自我滚动(~~不如交替~~ 完结&#xff01;&#xff01;&#xff01; ( 宇宙免责任书&#xff1a;我用的是C) 何为滚动数组&#xff1f; 什么是滚动…...

C 语言动态链表

线性结构->顺序存储->动态链表 一、理论部分 从起源中理解事物&#xff0c;就是从本质上理解事物。 -杜勒鲁奇 动态链表是通过结点&#xff08;Node&#xff09;的集合来非连续地存储数据&#xff0c;结点之间通过指针相互连接。 动态链表本身就是一种动态分配内存的…...

【Leetcode】二十、记忆化搜索:零钱兑换

文章目录 1、记忆化搜索2、leetcode509&#xff1a;斐波那契数列3、leetcode322&#xff1a;零钱兑换 1、记忆化搜索 也叫备忘录&#xff0c;即把已经计算过的结果存下来&#xff0c;下次再遇到&#xff0c;就直接取&#xff0c;不用重新计算。目的是以减少重复计算。 以前面提…...

json数据格式 继续学习

1.定义 轻量级的数据交互格式&#xff0c;可以按照json数据格式去组织和封装数据。 本质是一个带有特定格式的字符串。 2.功能 负责不同编程语言中的数据传递和交互。 3.json数据格式转化 """ 演示json数据和python字典之间的转换 """ impor…...

gradle 构建项目添加版本信息

gradle 构建项目添加版本信息&#xff0c;打包使用 spring boot 的打包插件 build.gradle 配置文件 bootJar {manifest {attributes(Project-Name: project.name,Project-Version: project.version,"project-Vendor": "XXX Corp","Built-By": &…...

vue3 学习笔记17 -- 基于el-menu封装菜单

vue3 学习笔记17 – 基于el-menu封装菜单 前提条件&#xff1a;组件创建完成 配置路由 // src/router/index.ts import { createRouter, createWebHashHistory } from vue-router import type { RouteRecordRaw } from vue-router export const Layout () > import(/lay…...

使用 Redis 实现验证码、token 的存储,用自定义拦截器完成用户认证、并使用双重拦截器解决 token 刷新的问题

可以看一下我以前做过的笔记&#xff1a;黑马点评 短信登录部分 基于session实现登录流程 1.发送验证码 用户在提交手机号后&#xff0c;会校验手机号是否合法&#xff0c;如果不合法&#xff0c;则要求用户重新输入手机号 如果手机号合法&#xff0c;后台此时生成对应的验…...

反转链表 - 力扣(LeetCode)C语言

206. 反转链表 - 力扣&#xff08;LeetCode&#xff09;( 点击前面链接即可查看题目) /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ struct ListNode* reverseList(struct ListNode* head) {if(head NULL)…...

【Linux】进程间通信(1):进程通信概念与匿名管道

人与人之间是如何通信的&#xff1f;举个简单的例子&#xff0c;假如我是月老&#xff0c;我要为素不相识的但又渴望爱情的男女两方牵红线。我需要收集男方的信息告诉女方&#xff0c;收集女方的信息告诉男方&#xff0c;然后由男女双方来决定是否继续。对于他们而言&#xff0…...

Spring从入门到精通 01

文章目录 1. 依赖注入 (Dependency Injection, DI)2. 面向切面编程 (Aspect-Oriented Programming, AOP)3. 事务管理4. 简化 JDBC 开发5. 集成各种框架和技术6. 模块化和扩展性&#xff1a;主要的 Spring 模块&#xff1a;Core Container&#xff1a;AOP 模块&#xff1a;Data …...

C语言经典习题25

冒泡排序 对一维数组进行升序排序&#xff0c;然后在数组中输入20个数&#xff0c;将排序后的结果打印输出。 #include<stdio.h> #define N 20 int main() {int a[N];int i;for(i0;i<N;i) //初始化数组的数 {scanf("%d",&a);}for(i0;…...

2-47 基于matlab的时域有限差分法(FDTD法)拉夫等效原理进行时谐场外推

基于matlab的时域有限差分法(FDTD法)拉夫等效原理进行时谐场外推。外推边界距离吸收边界的距离、电磁场循环、傅立叶变换提起幅值和相位、各远区剖分点电场、方向系数计算等操作&#xff0c;得出可视化结果。程序已调通&#xff0c;可直接运行。 2-47 时域有限差分法(FDTD法) 拉…...

C++:std::is_convertible

C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

Xshell远程连接Kali(默认 | 私钥)Note版

前言:xshell远程连接&#xff0c;私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...

AI Agent与Agentic AI:原理、应用、挑战与未来展望

文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例&#xff1a;使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例&#xff1a;使用OpenAI GPT-3进…...

服务器硬防的应用场景都有哪些?

服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式&#xff0c;避免服务器受到各种恶意攻击和网络威胁&#xff0c;那么&#xff0c;服务器硬防通常都会应用在哪些场景当中呢&#xff1f; 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...

vue3 定时器-定义全局方法 vue+ts

1.创建ts文件 路径&#xff1a;src/utils/timer.ts 完整代码&#xff1a; import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...

浅谈不同二分算法的查找情况

二分算法原理比较简单&#xff0c;但是实际的算法模板却有很多&#xff0c;这一切都源于二分查找问题中的复杂情况和二分算法的边界处理&#xff0c;以下是博主对一些二分算法查找的情况分析。 需要说明的是&#xff0c;以下二分算法都是基于有序序列为升序有序的情况&#xf…...

Typeerror: cannot read properties of undefined (reading ‘XXX‘)

最近需要在离线机器上运行软件&#xff0c;所以得把软件用docker打包起来&#xff0c;大部分功能都没问题&#xff0c;出了一个奇怪的事情。同样的代码&#xff0c;在本机上用vscode可以运行起来&#xff0c;但是打包之后在docker里出现了问题。使用的是dialog组件&#xff0c;…...

用机器学习破解新能源领域的“弃风”难题

音乐发烧友深有体会&#xff0c;玩音乐的本质就是玩电网。火电声音偏暖&#xff0c;水电偏冷&#xff0c;风电偏空旷。至于太阳能发的电&#xff0c;则略显朦胧和单薄。 不知你是否有感觉&#xff0c;近两年家里的音响声音越来越冷&#xff0c;听起来越来越单薄&#xff1f; —…...

面向无人机海岸带生态系统监测的语义分割基准数据集

描述&#xff1a;海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而&#xff0c;目前该领域仍面临一个挑战&#xff0c;即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...