Android泛型详解
参考文献:https://pingfangx.github.io/java-tutorials/java/generics/types.html
1,什么是泛型?
Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了 编译时类型安全检测机制, 该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数类型,也就是说所操作的数据类型被指定为一个参数。 泛型不存在于JVM虚拟机。
通俗点讲,就是将类型参数化,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这样提高了代码的类型安全性,使你在编译时可以检测到更多错误。
2,为什么要使用泛型?
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);//需要强制转换list.add(1);
s =(String) list.get(1); //在编译时期没有任何错误提示 在运行时期会报错
使用泛型的代码如下:
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); //不需要进行强制转换list.add(1);
s =list.get(0); //会在编译期就报错 Required type:String Provided:int
通过上面官方例子,我们不难发现,与非泛型代码相比,使用泛型的代码具有以下优点:
1,在编译时进行更强的类型检查。Java编译器将强类型检查应用于通用代码,如果代码违反类型安全,则会发出错误。修复编译时错误比修复运行时错误容易,后者可能很难找到。
2,消除类型转换。当使用泛型重写时,代码不需要强制转换。
3,使程序员能够实现通用算法。通过使用泛型,程序员可以实现对不同类型的集合进行工作,可以自定义并且类型安全且易于阅读的泛型算法。
3,泛型类的创建
泛型类是通过类型进行参数化的通用类。
泛型类的定义格式如下:
class 类名<T1, T2, ..., Tn> {private 泛型标识 变量名;}
//例如:
class Student<T> {private T t;public void set(T t) { this.t = t; }public T get() { return t; }}
上面T可以随便定义,只要是在<>中,你甚至可以定义为dnadnandn都行,注意:尽量不要定义为关键字,以免引起冲突。
注意:类型参数与类型变量的区别:
Student<T>中的T为类型参数
Student<String>中的String为类型变量
常用的类型参数名称是:
T - Type类型 最常用
E - 元素或Exception异常
K - Key
V - Value
N - 数字
S,U,V 等:第二,第三,第四个类型
下面看一个简单的例子
泛型类:
public class MyList<T> {private List<T> list;public void add(T t){list.add(t);}public T get(int i){return list.get(i);}public int size(){return list.size();}}
不使用泛型的类:
public class MyList1 {private List<Object> list;public void add(Object t){list.add(t);}public Object get(int i){return list.get(i);}public int size(){return list.size();}
}
在使用时:
//泛型类的使用MyList<String> myList =new MyList<>();myList.add("aaa");//myList.add(1); 在编译时就报错for (int i = 0; i < myList.size(); i++) {String s1 = myList.get(i);System.out.println("s1="+s1);}//非泛型类的使用MyList1 myList1 =new MyList1();myList1.add(11);//不会报错myList1.add("111");for (int i = 0; i < myList1.size(); i++) {String s1 = (String) myList1.get(i);//1,需要强转 2,在运行时会报错System.out.println("s1="+s1);}
泛型类还可以传入多个泛型参数,下面看一个 两个参数的泛型类:
public class People<K,V> {private K name;private V achievement;public People(K name, V achievement) {this.name = name;this.achievement = achievement;}public void setName(K name) {this.name = name;}public void setAchievement(V achievement) {this.achievement = achievement;}public K getName() {return name;}public V getAchievement() {return achievement;}
}
People<String,Integer> p1 =new People<>("张三",98);
People<String,Integer> p2 =new People<>("赵武",99);
// People<String,Integer> p3 =new People<>("赵武","100");//编译阶段报错
需要注意的是泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数,例如:
public class Test<T> {private static T name;//编译时报错'com.yuanzhen.Test.this' cannot be referenced from a static contextpublic static T getName(){//编译时报错'com.yuanzhen.Test.this' cannot be referenced from a static contextreturn name;}
}
原因是泛型在对象创建时才知道是什么类型,但是静态方法属于类,调用getName方法实际调用的Test类的方法,而类在编译阶段就存在了,所以虚拟机根本不知道方法中引用的泛型是什么类型
初始化时:对象创建的代码执行先后顺序是static的部分,然后才是构造函数等等,所以在对象初始化之前static的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西
4,泛型接口的创建
泛型接口的定义和泛型类的定义一样,区别就在于一个是接口需要实现各个接口方法,一个是类,需要实现对应的抽象方法。
泛型接口的定义格式如下:
public interface 接口名<类型参数> {...
}
//例如
public interface IPeople<T> {T getName();void setName(T t);
}
泛型的具体使用方式:
①直接在实现类中指定泛型的具体类型
public class Student<String> implements IPeople<String>{@Overridepublic String getName() {return null;}@Overridepublic void setName(String string) {}
}
②在实现类中继续使用泛型,在实例化实现类对象的时候指定泛型的具体类型
public class Teacher<T> implements IPeople<T>{@Overridepublic T getName() {return null;}@Overridepublic void setName(T t) {}
}
Teacher<String> teacher =new Teacher<>();
teacher.setName("张三");
③在接口继承接口中指定泛型的具体类型。
public interface ITeacher<String> extends IPeople<String>{}
5,泛型方法的创建
public <类型参数1,类型参数2...> 返回类型 方法名(类型参数1 变量名1,类型参数2 变量名2 ...) {...
}
//例如:
public class Doctor {/*** 泛型方法*/public <T> void doWork(T t){}
}
注意:只有在方法声明中声明了<>的方法才是泛型方法
下面看个例子:
public class Doctor<T> {private T name;/** 不是泛型方法 只是普通方法* */public Doctor(T name) {this.name = name;}/** 不是泛型方法 只是普通方法* */public void setName(T name) {this.name = name;}/** 不是泛型方法 只是普通方法* */public T getName() {return name;}/** 泛型方法 因为它定义了自己的<T>* */public <T> void doWork(T t){}
}
调用泛型方法:
Doctor<String> doctor =new Doctor<>("张三");
doctor.<String>doWork("看病");//调用泛型方法的完整语法
doctor.doWork("看病");//因为类型推断,所以可以省略<String>
类型推断:上文中可以看出,因为类型推断,可以省略<>,那么什么是类型推断呢?
类型推断(Type Inference)是指 Java 编译器能查看每个方法的调用和相应声明,以确定调用合适的类型参数(Type Argument)或参数。推断算法决定参数的类型,如果可用,则指定被赋值的类型或返回的类型。最后,推断算法试图在一起工作的所有参数中找到最具体的类型。
引入类型推断的泛型方法,能够让你像调用普通方法一样调用泛型方法,而不需要在尖括号中指定类型。
5,限定类型参数
<T extends B1> //单个限定
<T extends B1 & B2 & B3> //多重限定
举例如下:
public class Teacher<T extends Number>{private T age;public T getAge() {return age;}public void setAge(T t) {}
}
Teacher<String> teacher1 =new Teacher<>();//报错 因为已经限定了类型必须是Number及其子类
teacher1.setAge("张三");
//正确用法
Teacher<Integer> teacher2 =new Teacher<>();
teacher2.setAge(20);
6,通配符
①上限通配符
格式如下:
<? extends T>
//例如:
<? extends Foo>
//其中,Foo可以是任何类型,匹配Foo和Foo的任何子类型
下面来看一个具体的使用案例:
public class Fruit {@Overridepublic String toString() {return "水果";}
}
public class Apple extends Fruit {@Overridepublic String toString() {return "苹果";}
}
ArrayList<Fruit> fruits =new ArrayList<Apple>();//这样会在编译期报错
这样为什么会报错呢?Java不是可以将子类对象赋值给一个父类对象的引用吗?
下来再来看一下这段代码:
public class Banana extends Fruit {@Overridepublic String toString() {return "香蕉";}
}
ArrayList<Apple> apples =new ArrayList<Apple>();
Apple apple =new Apple();
apples.add(apple);
//----------------上面的代码不会报错 是正常逻辑------------
ArrayList<Fruit> fruits =apples;//假如这行代码在编译期不报错的话
fruits.add(new Banana());
Apple a =fruits.get(1);//这行代码在运行期间就会报类型转换异常的错误
所以泛型是不支持这种向上转型的。
但是如果我们一定要这么写呢?也不是不可以,用通配符就可以做到
ArrayList<? extends Fruit> fruits =new ArrayList<Apple>();//编译期不会报错
Apple apple =new Apple();
fruits.add(apple);//但是在添加的时候会在编译期报错
大家想一想为什么会在添加的时候会在编译期报错呢?
因为如果放开的话,我还是同样的可以添加香蕉,苹果等子类,这样在运行期间就可能会出现更大的错误,所以编译器直接就不让你添加了,这样就不会有问题了。
但是这样做有什么意义呢?
当你只想让用户往外取值,不想让用户进行写入时,<? extends Fruit>的意义就体现出来了,简而言之就是只能读取不能写入
②下限通配符
格式如下:
<? super T>
//例如:
<? supper Zoo>
//其中,Zoo可以是任何类型,匹配Zoo和Zoo的任何超类
下面来看一个例子:
ArrayList<Apple> fruits =new ArrayList<Fruit>();//会在编译期报错
Apple apple =new Apple();
fruits.add(apple);
为什么会在编译期报错呢?同上限通配符一样的道理,泛型也不支持这种转型
那么需要怎么做呢?请看下面:
ArrayList<? super Apple> fruits =new ArrayList<Fruit>();//不报错 正常运行
Apple apple =new Apple();
fruits.add(apple);
Object object = fruits.get(0);
使用下限通配符为什么可以添加呢? 因为往里面添加的都是apple的父类,其归根结底,都会用一个最终的父类表示,所以不会有问题。但是往外读的时候,我不知道是苹果还是水果,所以不建议读取,虽然可以用Object接收。
简而言之,与上限通配符相反,下线通配符只能写入,不能读取
③通配符使用准则
在开发中,我们应该在什么时候使用上限通配符和下限通配符呢?
通配符准则:
使用上限通配符定义输入变量,使用 extends 关键字。
使用下限通配符定义输出变量,使用 super 关键字。
如果可以使用 Object 类中定义的方法访问输入变量,请使用无界通配符( ? )。
如果代码需要同时使用输入和输出变量来访问变量,则不要使用通配符。
7,类型擦除
下面来看几个例子:
public class Node<T> {private T data;private Node<T> next;public Node(T data, Node<T> next) {this.data = data;this.next = next;}public T getData() { return data; }}
public class Node {private Object data;private Node next;public Node(Object data, Node next) {this.data = data;this.next = next;}public Object getData() { return data; }
}
public class Node<T extends Comparable<T>> {private T data;private Node<T> next;public Node(T data, Node<T> next) {this.data = data;this.next = next;}public T getData() { return data; }
}
public class Node {private Comparable data;private Node next;public Node(Comparable data, Node next) {this.data = data;this.next = next;}public Comparable getData() { return data; }
}
8,对泛型的限制
①无法实例化具有基本类型的泛型类型
请看下面例子:
class Pair<K, V> {private K key;private V value;public Pair(K key, V value) {this.key = key;this.value = value;}
}
创建对对象时,不能用基本类型替换类型参数K或V:
Pair<int, char> p = new Pair<>(8, 'a');//编译错误
你只能将非基本类型替换为类型参数K和V:
Pair<Integer, Character> p = new Pair<>(8, 'a');
②无法创建类型参数的实例
例如,以下代码会导致编译时错误:
public static <E> void append(List<E> list) {E elem = new E(); //编译期错误list.add(elem);
}
解决方法是,可以通过反射创建类型参数的对象:
public static <E> void append(List<E> list, Class<E> cls) throws Exception {E elem = cls.newInstance(); // OKlist.add(elem);
}
③无法声明类型为类型参数的静态字段
类的静态字段是该类的所有非静态对象共享的类级别变量。因此,不允许使用类型参数的静态字段
public class MobileDevice<T> {private static T os;
}
④无法将Casts或instanceof与参数化类型一起使用
public static <E> void rtti(List<E> list) {if (list instanceof ArrayList<Integer>) { // 编译器错误// ...}
}
⑤无法创建参数化类型的数组
List<Integer>[] arrayOfLists = new List<Integer>[2];//编译期报错
⑥无法创建,捕获或抛出参数化类型的对象
class MathException<T> extends Exception { /* ... */ } // 编译期错误
class QueueFullException<T> extends Throwable { /* ... */ }// 编译器错误
public static <T extends Exception, J> void execute(List<J> jobs) {try {for (J job : jobs)} catch (T e) { //编译期错误}
}
class Parser<T extends Exception> {public void parse(File file) throws T { // OK}
}
⑦无法重载每个重载的形式参数类型都擦除为相同原始(raw)类型的方法
public class Example {public void print(Set<String> strSet) { }public void print(Set<Integer> intSet) { }
}
9,在安卓中的使用
那么在了解了泛型的知识之后,我们在安卓开发中,到底用到了哪些泛型呢?
①网络请求
最常用的地方,就是解析http请求下来的json数据。因为http请求下来的数据,我们不知道需要转换成什么类型,只有在调用的地方才知道,所以我们采用泛型。
先定义一个回调接口:
public interface CallBack<T> {void onError(Exception e);void onSuccess(T t);
}
在实际请求中传入匿名内部类:
OmniHttp.get(getHOST() + "/app-wn/login").syncRequest(true).execute(new CallBack<LoginBean>() {@Overridepublic void onError(ApiException e) {}@Overridepublic void onSuccess(LoginBean loginBean) {}
});
这只是简单的写了一个小例子,可以尝试在自己写一下
②findViewById()
/*** FindViewById的源码,泛型封装,减少强转代码*/public <T extends View> T findViewById(@IdRes int id) {return getDelegate().findViewById(id);}
//使用前
ImageView item = (ImageView)findViewById(R.id.item);
//使用后
ImageView item =findViewById(R.id.item);
③BaseAdapter实现封装的Adapter
有时间补充
④MVP架构
有时间补充
10,总结
花了一天时间整理了下泛型的相关知识点,在android中的应用后续还会继续补充。
相关文章:
Android泛型详解
参考文献:https://pingfangx.github.io/java-tutorials/java/generics/types.html 1,什么是泛型? Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了 编译时类型安全检测机制, 该机制允许程序员在编译时检测到…...

C++信息学奥赛1178:成绩排序
#include<bits/stdc.h> using namespace std; int main(){int n;cin>>n; // 输入整数 n,表示数组的大小int arr[n]; // 创建大小为 n 的整型数组 arrstring brr[n]; // 创建大小为 n 的字符串数组 brrfor(int i0;i<n;i) cin>>brr[i]>>ar…...
【计算机视觉 | 目标检测】目标检测常用数据集及其介绍(七)
文章目录 一、Cops-Ref二、FAT (Falling Things)三、GEN1 Detection (Prophesee GEN1 Automotive Detection Dataset)四、RIT-18五、AGAR (Annotated Germs for Automated Recognition)六、EuroCity Persons七、Freiburg Groceries八、Lytro Illum九、PFN-PIC (PFN Picking Ins…...

100天精通Golang(基础入门篇)——第20天:Golang 接口 深度解析☞从基础到高级
🌷🍁 博主猫头虎🐅🐾 带您进入 Golang 语言的新世界✨✨🍁 🦄 博客首页——🐅🐾猫头虎的博客🎐 🐳 《面试题大全专栏》 🦕 文章图文并茂…...

ESXi 6.7添加螃蟹2.5g网卡支持
安装了ESXi 6.7,结果机器两块网卡只能识别一块,然后想着不能让另一块浪费啊,开始折腾,看着网上都是找的驱动然后封装进iso,可是我已经装完了,怎么办,然后找到了下面解决方法 1.找驱动 下载RTL81…...

机器学习笔记之最优化理论与方法(四) 凸函数:定义与基本性质
机器学习笔记之最优化理论与方法——再回首:凸函数定义与基本性质 引言凸函数的定义严格凸函数凸函数的推论:凹函数 常见凸函数凸函数的基本性质几种保持函数凸性的运算凸集与凸函数之间的关联关系 引言 本节将介绍凸函数定义及其基本性质。 本文是关于…...
【Git】git tag 查看版本号 | 删除本地 | 删除远程仓库| 批量删除
一、删除指定tag 使用场景:比如我们在本地git tag了一个错误的版本号,但是还没有push,想直接删掉避免污染远程仓库 1、删除指令 要删除指定的Git标签(版本号),您可以使用以下命令: git tag -d 标…...
thinkphp:数据库查询,嵌套别的表的查询(别的表做子查询)
例子 从 vendors 表中选择记录。在 vendors 表中,筛选出具有满足以下条件的 vendor_code 值: 对应的采购订单(在 po_headers_all 表中)存在未完全接收的采购行(在 po_lines_all 表中)。相应的采购订单状态…...
《Linux 系统命令及Shell脚本实践指南》
Linux 系统命令及Shell脚本实践指南 《Linux 系统命令及Shell脚本实践指南》该书从结构上分为三部分:第一部分1.1Linux的历史发展1.2用户管理1.3任务管理单一时刻执行一次任务使用at周期性任务使用:cron表达式,命令crontab 1.4文件管理1.4.1 Linux shell…...
代码随想录算法训练营第三十八天 | ● 理论基础 ● 509. 斐波那契数 ● 70. 爬楼梯 ● 746. 使用最小花费爬楼梯
题目链接:509. 斐波那契数 代码随想录 视频:手把手带你入门动态规划 | LeetCode:509.斐波那契数_哔哩哔哩_bilibili 看完代码随想录之后的想法: 我们要知道动态规划的五部曲; 1,确定dp数组的含义&#x…...

Java分别用BIO、NIO实现简单的客户端服务器通信
分别用BIO、NIO实现客户端服务器通信 BIONIONIO演示(无Selector)NIO演示(Selector) 前言: Java I/O模型发展以及Netty网络模型的设计思想 BIO Java BIO是Java平台上的BIO(Blocking I/O)模型&a…...
React Portals
什么是React Portals React Portals(React 门户)是 React 提供的一种机制,用于将组件渲染到 DOM 树中的不同位置,而不受组件层次结构的限制。它允许你将一个组件的渲染内容“传送”到 DOM 结构中的任何位置,通常用于处…...

Python基础之高级函数
异常捕获 Python中,使用trycatch两个关键字来实现对异常的处理。在我们平时的工作中,异常的出现是在所难免的,但是异常一旦出现,极有可能会直接导致程序崩溃,无法正常运行,所以异常一定要及时的做出对应的…...

CSS3常用的新功能总结
CSS3常用的新功能包括圆角、阴渐变、2D变换、3D旋转、动画、viewpor和媒体查询。 圆角、阴影 border-redius 对一个元素实现圆角效果,是通过border-redius完成的。属性为两种方式: 一个属性值,表示设置所有四个角的半径为相同值ÿ…...

Lvs+KeepAlived高可用高性能负载均衡
目录 1.环境介绍 2.配置keepalived 3.测试 1.测试负载均衡 2.测试RS高可用 3.测试LVS高可用 3.1测试lvs主服务宕机 3.2.测试lvs主服务器恢复 4.我在实验中遇到的错误 1.环境介绍 环境:centos7 RS1---RIP1:192.168.163.145 VIP 192.168.163.200 RS2---RIP2…...

无涯教程-Android Online Test函数
Android在线测试模拟了真正的在线认证考试。您将看到基于 Android概念的多项选择题(MCQ),将为您提供四个options。您将为该问题选择最合适的答案,然后继续进行下一个问题,而不会浪费时间。完成完整的考试后,您将获得在线考试分数。 总问题数-20 最长时间-20分钟 Start Test …...

蓝桥杯打卡Day1
文章目录 全排列八皇后 一、全排列IO链接 本题思路:本题是一道经典的全排列问题,深度优先搜索即可解决。 #include <bits/stdc.h>constexpr int N10;std::string s; std::string ans; int n; bool st[N];void dfs(int u) {if(un){std::cout<<ans<…...
zipkin2.24.2源码install遇见的问题
1、idea导入项目后将Setting中的关于Maven和Java Compile相关的配置改为jdk11,同时Project Structure改为jdk11 2、将pom配置中的fork标签注释 标题未修改以上配置产生的问题 Compilation failure javac: Ч ı : --release : javac <options> <source files&g…...

yapi密码是如何生成的
yapi密码是如何生成的 关闭yapi注册功能后,想要通过手动插入用户数据到db中,那么密码是如何生成的呢? exports.generatePassword (password, passsalt) > { return sha1(password sha1(passsalt)); }; 所以如果想要创建一个用户&#x…...

2023-09-02 LeetCode每日一题(最多可以摧毁的敌人城堡数目)
2023-09-02每日一题 一、题目编号 2511. 最多可以摧毁的敌人城堡数目二、题目链接 点击跳转到题目位置 三、题目描述 给你一个长度为 n ,下标从 0 开始的整数数组 forts ,表示一些城堡。forts[i] 可以是 -1 ,0 或者 1 ,其中&…...
day52 ResNet18 CBAM
在深度学习的旅程中,我们不断探索如何提升模型的性能。今天,我将分享我在 ResNet18 模型中插入 CBAM(Convolutional Block Attention Module)模块,并采用分阶段微调策略的实践过程。通过这个过程,我不仅提升…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序
一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...
LLM基础1_语言模型如何处理文本
基于GitHub项目:https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken:OpenAI开发的专业"分词器" torch:Facebook开发的强力计算引擎,相当于超级计算器 理解词嵌入:给词语画"…...

MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...

STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...
嵌入式常见 CPU 架构
架构类型架构厂商芯片厂商典型芯片特点与应用场景PICRISC (8/16 位)MicrochipMicrochipPIC16F877A、PIC18F4550简化指令集,单周期执行;低功耗、CIP 独立外设;用于家电、小电机控制、安防面板等嵌入式场景8051CISC (8 位)Intel(原始…...
云原生周刊:k0s 成为 CNCF 沙箱项目
开源项目推荐 HAMi HAMi(原名 k8s‑vGPU‑scheduler)是一款 CNCF Sandbox 级别的开源 K8s 中间件,通过虚拟化 GPU/NPU 等异构设备并支持内存、计算核心时间片隔离及共享调度,为容器提供统一接口,实现细粒度资源配额…...

react菜单,动态绑定点击事件,菜单分离出去单独的js文件,Ant框架
1、菜单文件treeTop.js // 顶部菜单 import { AppstoreOutlined, SettingOutlined } from ant-design/icons; // 定义菜单项数据 const treeTop [{label: Docker管理,key: 1,icon: <AppstoreOutlined />,url:"/docker/index"},{label: 权限管理,key: 2,icon:…...