JDK源码解析-ArrayList

1. ArrayList类

1.1 ArrayList类结构图

ArrayList 是一个用数组实现的集合,支持随机访问,元素有序且可以重复。

(1)ArrayList 是一种变长的集合类,基于定长数组实现。

(2)ArrayList 允许空值和重复元素,当往 ArrayList 中添加的元素数量大于其底层数组容量时,其会通过扩容机制重新生成一个更大的数组。

(3)ArrayList 底层基于数组实现,所以其可以保证在 O(1) 复杂度下完成随机查找操作。

(4)ArrayList 是非线程安全类,并发环境下,多个线程同时操作 ArrayList,会引发不可预知的异常或错误。

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

'xxs'

①、实现 RandomAccess 接口

这是一个标记接口,一般此标记接口用于 List实现,以表明它们支持快速(通常是恒定时间)的随机访问

②、实现 Cloneable 接口

Cloneable 和 RandomAccess 接口一样也是一个标记接口,接口内无任何方法体和常量的声明,也就是说如果想克隆对象,必须要 实现 Cloneable 接口,表明该类是可以被克隆的。

③、实现 Serializable 接口

标记接口,表示能被序列化

④、实现 List 接口

这个接口是 List 类集合的上层接口,定义了实现该接口的类都必须要实现的一组方法

'xxs'

1.2 字段属性

 //集合的默认大小private static final int DEFAULT_CAPACITY = 10;//空的数组实例private static final Object[] EMPTY_ELEMENTDATA = {};//这也是一个空的数组实例,和EMPTY_ELEMENTDATA空数组相比是用于了解添加元素时数组膨胀多少private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//存储 ArrayList集合的元素,集合的长度即这个数组的长度//1、当 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时将会清空 ArrayList//2、当添加第一个元素时,elementData 长度会扩展为 DEFAULT_CAPACITY=10transient Object[] elementData;//表示集合的长度private int size;

1.3 类构造器

1. 无参构造:

public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

此无参构造函数将创建一个 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 声明的数组,注意此时初始容量是0,而不是大家以为的 10。

注意:根据默认构造函数创建的集合,ArrayList list = new ArrayList();此时集合长度是0.

2. 重载:有参构造ArrayList(int initialCapacity)

public ArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}
}

初始化集合大小创建 ArrayList 集合。当大于0时,给定多少那就创建多大的数组;当等于0时,创建一个空数组;当小于0时,抛出异常。

3.重载:ArrayList(Collection<? extends E> c)

public ArrayList(Collection<? extends E> c) {elementData = c.toArray();if ((size = elementData.length) != 0) {// c.toArray might (incorrectly) not return Object[] (see 6260652)if (elementData.getClass() != Object[].class)elementData = Arrays.copyOf(elementData, size, Object[].class);} else {// replace with empty array.this.elementData = EMPTY_ELEMENTDATA;}
}

将已有的集合复制到 ArrayList 集合中

思考:无参构造和0长度构造有什么区别?

//两种方式构建list,有什么区别?
ArrayList list1 = new ArrayList();
ArrayList list2 = new ArrayList(0);
@Test
public void test2(){//两种方式构建list,有什么区别?ArrayList list1 = new ArrayList();ArrayList list2 = new ArrayList(0);//打印对象头System.out.println(ClassLayout.parseInstance(list1).toPrintable());System.out.println(ClassLayout.parseInstance(list2).toPrintable());System.out.println("========");//add一个元素之后再来打印试试list1.add(1);list2.add(1);System.out.println(ClassLayout.parseInstance(list1).toPrintable());System.out.println(ClassLayout.parseInstance(list2).toPrintable());
}

'xxs'

原理:

//calculateCapacity
//每次元素变动,比如add,会调用该函数判断容量情况
private static int calculateCapacity(Object[] elementData, int minCapacity) {//定义default empty数组的意义就在这里!用于扩容时判断当初采用的是哪种构造函数if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//如果是无参的构造函数,用的就是该default empty//那么第一次add时候,容量取default和min中较大者return Math.max(DEFAULT_CAPACITY, minCapacity);}//如果是另外两个构造函数,比如指定容量为5,或者初始参数collection为5//那就直接返回5,一定程度上,节约了内存空间return minCapacity;
}

1.4 添加元素

// 思考:List集合底层是数组,为什么能添加到任意多个元素?
list1.add(1);

源码:

public boolean add(E e) {ensureCapacityInternal(size + 1);  //添加元素之前,首先要确定集合的大小(是否需要扩容)elementData[size++] = e;return true;
}

如上所示,在通过调用 add 方法添加元素之前,要首先调用 ensureCapacityInternal 方法来确定集合的大小,如果集合满了,则要进行扩容操作。

private void ensureCapacityInternal(int minCapacity) {//这里的minCapacity 是集合当前大小+1//elementData 是实际用来存储元素的数组,注意数组的大小和集合的大小不是相等的,前面的size是指集合大小ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//如果数组为空,则从size+1的值和默认值10中取最大的return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;//不为空,则返回size+1
}
private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);
}

在 ensureExplicitCapacity 方法中,首先对修改次数modCount加一,这里的modCount给ArrayList的迭代器使用的,在并发操作被修改时,提供快速失败行为(保证modCount在迭代期间不变,否则抛出ConcurrentModificationException异常,可以查看源码865行),接着判断minCapacity是否大于当前ArrayList内部数组长度,大于的话调用grow方法对内部数组elementData扩容,grow方法代码如下:

private void grow(int minCapacity) {int oldCapacity = elementData.length;//得到原始数组的长度int newCapacity = oldCapacity + (oldCapacity >> 1);//新数组的长度等于原数组长度的1.5倍if (newCapacity - minCapacity < 0)//当新数组长度仍然比minCapacity小,则为保证最小长度,新数组等于minCapacitynewCapacity = minCapacity;//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 = 2147483639if (newCapacity - MAX_ARRAY_SIZE > 0)//当得到的新数组长度比 MAX_ARRAY_SIZE 大时,调用 hugeCapacity 处理大数组newCapacity = hugeCapacity(minCapacity);//调用 Arrays.copyOf 将原数组拷贝到一个大小为newCapacity的新数组(注意是拷贝引用)elementData = Arrays.copyOf(elementData, newCapacity);
}private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) //throw new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ? //minCapacity > MAX_ARRAY_SIZE,则新数组大小为Integer.MAX_VALUEInteger.MAX_VALUE :MAX_ARRAY_SIZE;
}

扩容的核心方法就是调用前面讲过的Arrays.copyOf 方法,创建一个更大的数组,然后将原数组元素拷贝过去即可

对于 ArrayList 集合添加元素,总结一下:

①、当通过 ArrayList() 构造一个空集合,初始长度是为0的,第 1 次添加元素,会创建一个长度为10的数组,并将该元素赋值到数组的第一个位置。

②、第 2 次添加元素,集合不为空,而且由于集合的长度size+1是小于数组的长度10,所以直接添加元素到数组的第二个位置,不用扩容。

③、第 11 次添加元素,此时 size+1 = 11,而数组长度是10,这时候创建一个长度为10+10*0.5 = 15 的数组(扩容1.5倍),然后将原数组元素引用拷贝到新数组。并将第 11 次添加的元素赋值到新数组下标为10的位置。

④、第 Integer.MAX_VALUE - 8 = 2147483639,然后 2147483639%1.5=1431655759(这个数是要进行扩容) 次添加元素,为了防止溢出,此时会直接创建一个 1431655759+1 大小的数组,这样一直,每次添加一个元素,都只扩大一个范围。

⑤、第 Integer.MAX_VALUE - 7 次添加元素时,创建一个大小为 Integer.MAX_VALUE 的数组,在进行元素添加。

⑥、第 Integer.MAX_VALUE + 1 次添加元素时,抛出 OutOfMemoryError 异常。

注意:能向集合中添加 null 的,因为数组可以有 null 值存在。

Object[] obj = {null,1};ArrayList list = new ArrayList();
list.add(null);
list.add(1);
System.out.println(list.size());//2

1.5 删除元素

public E remove(int index) {rangeCheck(index);//判断给定索引的范围,超过集合大小则抛出异常modCount++;E oldValue = elementData(index);//得到索引处的删除元素int numMoved = size - index - 1;if (numMoved > 0)//size-index-1 > 0 表示 0<= index < (size-1),即索引不是最后一个元素//通过 System.arraycopy()将数组elementData 的下标index+1之后长度为 numMoved的元素拷贝到从index开始的位置System.arraycopy(elementData, index+1, elementData, index,numMoved);elementData[--size] = null; //将数组最后一个元素置为 null,便于垃圾回收return oldValue;
}

remove(int index) 方法表示删除索引index处的元素,首先通过 rangeCheck(index) 方法判断给定索引的范围,超过集合大小则抛出异常;接着通过 System.arraycopy 方法对数组进行自身拷贝

'xxs'

附:

/*
* src:源数组srcPos:源数组要复制的起始位置dest:目的数组destPos:目的数组放置的起始位置length:复制的长度注意:src 和 dest都必须是同类型或者可以进行转换类型的数组。
*/
public static native void arraycopy(Object src,  int  srcPos,Object dest, int destPos,int length);

1.6 修改元素

通过调用 set(int index, E element) 方法在指定索引 index 处的元素替换为 element。并返回原数组的元素。

public E set(int index, E element) {rangeCheck(index);//判断索引合法性E oldValue = elementData(index);//获得原数组指定索引的元素elementData[index] = element;//将指定所引处的元素替换为 elementreturn oldValue;//返回原数组索引元素
}

通过调用 rangeCheck(index) 来检查索引合法性

 private void rangeCheck(int index) {if (index >= size)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}

当索引为负数时,会抛出 java.lang.ArrayIndexOutOfBoundsException 异常。当索引大于集合长度时,会抛出 IndexOutOfBoundsException 异常。

1.7 查找元素

public E get(int index) {rangeCheck(index);return elementData(index);
}

同理,首先还是判断给定索引的合理性,然后直接返回处于该下标位置的数组元素。

1.8 遍历集合

①、普通 for 循环遍历

前面介绍查找元素时,知道可以通过get(int index)方法,根据索引查找元素,那么遍历同理:

ArrayList list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
for(int i = 0 ; i < list.size() ; i++){System.out.print(list.get(i)+" ");
}

②、迭代器 iterator

ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
Iterator<String> it = list.iterator();
while(it.hasNext()){String str = it.next();System.out.print(str+" ");
}

在介绍 ArrayList 时,知道该类实现了 List 接口,而 List 接口又继承了 Collection 接口,Collection 接口又继承了 Iterable 接口,该接口有个 Iterator iterator() 方法,能获取 Iterator 对象,能用该对象进行集合遍历,为什么能用该对象进行集合遍历?再看看 ArrayList 类中的该方法实现:

public Iterator<E> iterator() {return new Itr();
}

该方法是返回一个 Itr 对象,这个类是 ArrayList 的内部类。

private class Itr implements Iterator<E> {int cursor;       //游标, 下一个要返回的元素的索引int lastRet = -1; // 返回最后一个元素的索引; 如果没有这样的话返回-1.int expectedModCount = modCount;//通过 cursor != size 判断是否还有下一个元素public boolean hasNext() {return cursor != size;}@SuppressWarnings("unchecked")public E next() {checkForComodification();//迭代器进行元素迭代时同时进行增加和删除操作,会抛出异常int i = cursor;if (i >= size)throw new NoSuchElementException();Object[] elementData = ArrayList.this.elementData;if (i >= elementData.length)throw new ConcurrentModificationException();cursor = i + 1;//游标向后移动一位return (E) elementData[lastRet = i];//返回索引为i处的元素,并将 lastRet赋值为i}public void remove() {if (lastRet < 0)throw new IllegalStateException();checkForComodification();try {ArrayList.this.remove(lastRet);//调用ArrayList的remove方法删除元素cursor = lastRet;//游标指向删除元素的位置,本来是lastRet+1的,这里删除一个元素,然后游标就不变了lastRet = -1;//lastRet恢复默认值-1expectedModCount = modCount;//expectedModCount值和modCount同步,因为进行add和remove操作,modCount会加1} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}@Override@SuppressWarnings("unchecked")public void forEachRemaining(Consumer<? super E> consumer) {//便于进行forEach循环Objects.requireNonNull(consumer);final int size = ArrayList.this.size;int i = cursor;if (i >= size) {return;}final Object[] elementData = ArrayList.this.elementData;if (i >= elementData.length) {throw new ConcurrentModificationException();}while (i != size && modCount == expectedModCount) {consumer.accept((E) elementData[i++]);}// update once at end of iteration to reduce heap write trafficcursor = i;lastRet = i - 1;checkForComodification();}//前面在新增元素add() 和 删除元素 remove() 时,可以看到 modCount++。修改set() 是没有的//也就是说不能在迭代器进行元素迭代时进行增加和删除操作,否则抛出异常final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}
}

注意在进行 next() 方法调用的时候,会进行 checkForComodification() 调用,该方法表示迭代器进行元素迭代时,如果同时进行增加和删除操作,会抛出 ConcurrentModificationException 异常。比如:

ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
Iterator<String> it = list.iterator();
while(it.hasNext()){String str = it.next();System.out.print(str+" ");list.remove(str);//集合遍历时进行删除或者新增操作,都会抛出 ConcurrentModificationException 异常//list.add(str);list.set(0, str);//修改操作不会造成异常
}

'xxs'

解决办法是不调用 ArrayList.remove() 方法,转而调用 迭代器的 remove() 方法:

Iterator<String> it = list.iterator();
while(it.hasNext()){String str = it.next();System.out.print(str+" ");//list.remove(str);//集合遍历时进行删除或者新增操作,都会抛出 ConcurrentModificationException 异常it.remove();
}

注意:迭代器只能向后遍历,不能向前遍历,能够删除元素,但是不能新增元素。

③、迭代器的变种 forEach

ArrayList<String> list = new ArrayList<>();list.add("a");list.add("b");list.add("c");
for(String str : list){System.out.print(str + " ");
}

这种语法可以看成是 JDK 的一种语法糖,通过反编译 class 文件,可以看到生成的 java 文件,其具体实现还是通过调用 Iterator 迭代器进行遍历的。如下:

ArrayList list = new ArrayList();list.add("a");list.add("b");list.add("c");String str;for (Iterator iterator1 = list.iterator(); iterator1.hasNext(); System.out.print((new StringBuilder(String.valueOf(str))).append(" ").toString()))str = (String)iterator1.next();

总结:

  • arrayList可以存放null。
  • arrayList本质上就是一个elementData数组。
  • arrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是gorw()方法。
  • arrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,有移动很多数据才能达到应有的效果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/113672.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝牙资讯|苹果AirPods耳机或可以支持隔空手势交互

根据美国商标和专利局&#xff08;USPTO&#xff09;公示的清单&#xff0c;苹果近期获得了一项关于 AirPods 耳机的最新专利&#xff0c;其亮点在于传统触控操作之外&#xff0c;还支持隔空手势操作。 其中图 4A 展示了一位佩戴 AirPods 耳机的用户&#xff1b;图 4B 展示了…

十年网络安全工程师整理:渗透测试工具使用方法介绍

渗透测试&#xff0c;是为了证明网络防御按照预期计划正常运行而提供的一种机制。 渗透人员在不同的位置&#xff08;比如从内网、从外网等位置&#xff09;利用各种手段对某个特定网络进行测试&#xff0c;以期发现和挖掘系统中存在的漏洞&#xff0c;然后输出渗透测试报告&a…

说说我最近筛简历和面试的感受。。

大家好&#xff0c;我是鱼皮。 都说现在行情不好、找工作难&#xff0c;但招人又谈何容易&#xff1f;&#xff01; 最近我们公司在招开发&#xff0c;实习社招都有。我收到的简历很多&#xff0c;但认真投递的、符合要求的却寥寥无几&#xff0c;而且都是我自己看简历、选人…

图像化数据库工具DBeaver远程连接云服务器的MySQL数据库

一、安装宝塔面板 使用xshell、electerm、SecureCRT等远程终端连接登陆上云服务器&#xff0c;在Linux宝塔面板使用脚本安装 安装后&#xff0c;如下图&#xff1a;按照提示&#xff0c;在云服务器防火墙/安全组放行Linux宝塔面板的端口 在浏览器打开上述网址&#xff0c;…

Java“牵手”天猫整店商品API接口数据,通过店铺ID获取整店商品详情数据,天猫店铺所有商品API申请指南

天猫平台店铺所有商品数据接口是开放平台提供的一种API接口&#xff0c;通过调用API接口&#xff0c;开发者可以获取天猫整店的商品的标题、价格、库存、月销量、总销量、库存、详情描述、图片、价格信息等详细信息 。 获取店铺所有商品接口API是一种用于获取电商平台上商品详…

设计模式--装饰者模式(Decorator Pattern)

一、什么是装饰者模式&#xff08;Decorator Pattern&#xff09; 装饰者模式&#xff08;Decorator Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许你在不修改现有对象的情况下&#xff0c;动态地将新功能附加到对象上。这种模式通过创建一个包装类&#xff0c;…

西北大学计算机考研844高分经验分享

西北大学计算机考研844经验分享 个人介绍 ​ 本人是西北大学22级软件工程研究生&#xff0c;考研专业课129分&#xff0c;过去一年里在各大辅导机构任职&#xff0c;辅导考研学生专业课844&#xff0c;辅导总时长达288小时&#xff0c;帮助多名学生专业课高分上岸。 前情回顾…

ReactPy:使用 Python 构建动态前端应用程序

在 Web 开发领域,ReactJS 已成为主导者,为开发人员提供了用于创建动态和交互式用户界面的强大工具集。但是,如果您更喜欢 Python 的多功能性和简单性作为后端,并且希望在前端也利用它的功能,该怎么办?ReactPy 是一个 Python 库,它将熟悉的 ReactJS 语法和灵活性带入了 P…

【Spring+SpringMVC+Mybatis】SSM框架的整合、思想、工作原理和优缺点的略微讲解

&#x1f680;欢迎来到本文&#x1f680; &#x1f349;个人简介&#xff1a;陈童学哦&#xff0c;目前学习C/C、算法、Python、Java等方向&#xff0c;一个正在慢慢前行的普通人。 &#x1f3c0;系列专栏&#xff1a;陈童学的日记 &#x1f4a1;其他专栏&#xff1a;CSTL&…

OpenLayers7官方文档翻译,OpenLayers7中文文档,OpenLayers快速入门

快速入门 这个入门文档向您展示如何放一张地图在web网页上。 开发设置使用 NodeJS&#xff08;至少需要Nodejs 14 或更高版本&#xff09;&#xff0c;并要求安装 git。 设置新项目 开始使用OpenLayers构建项目的最简单方法是运行&#xff1a;npm create ol-app npm create…

分类模型评估指标——准确率、精准率、召回率、F1、ROC曲线、AUC曲线

机器学习模型需要有量化的评估指标来评估哪些模型的效果更好。 本文将用通俗易懂的方式讲解分类问题的混淆矩阵和各种评估指标的计算公式。将要给大家介绍的评估指标有&#xff1a;准确率、精准率、召回率、F1、ROC曲线、AUC曲线。 机器学习评估指标大全 所有事情都需要评估好…

RunnerGo:轻量级、全栈式、易用性和高效性的测试工具

随着软件测试的重要性日益凸显&#xff0c;市场上的测试工具也日益丰富。RunnerGo作为一款基于Go语言研发的开源测试平台&#xff0c;以其轻量级、全栈式、易用性和高效性的特点&#xff0c;在测试工具市场中逐渐脱颖而出。 RunnerGo是一款轻量级的测试工具&#xff0c;使用Go…

Vector 动态数组(迭代器)

C数据结构与算法 目录 本文前驱课程 1 C自学精简教程 目录(必读) 2 Vector<T> 动态数组&#xff08;模板语法&#xff09; 本文目标 1 熟悉迭代器设计模式&#xff1b; 2 实现数组的迭代器&#xff1b; 3 基于迭代器的容器遍历&#xff1b; 迭代器语法介绍 对迭…

java入门第三节

java入门第三节 一.什么是oop 1.pop与oop (1).面向过程编程&#xff1a;&#xff08;POP&#xff1a;Procedure Oriented Programming&#xff09; 1.步骤清晰简单&#xff0c;第一步做什么&#xff0c;第二步做什么&#xff0c;按照顺序&#xff1b; 2.代码线性&#xff0…

一个简单的vim例子

一.欢迎来到我的酒馆 在本章节介绍vim工具。 目录 一.欢迎来到我的酒馆二.什么是vim三.开始使用vim 二.什么是vim 2.1什么是vim vim是一种Linux命令行类型的文本编辑器。vim指的是"vi improved"&#xff0c;意思是vi工具的升级版。vim是基于vi实现的&#x…

Scrum Master 面试问题- ChatGPT 版

之前&#xff0c;我测试了 ChatGPT 如何回答《Scrum Master 面试指南》中的问题&#xff1b;见下文。早在2023 年 1 月&#xff0c;我就不会在 Scrum Master 面试过程中采取下一步&#xff0c;邀请ChatGPT与几名Scrum团队成员进行全方位的面试。 那么&#xff0c;如果 GPT 3.5…

【安全】原型链污染 - Hackit2018

目录 准备工作 解题 代码审计 Payload 准备工作 将这道题所需依赖模块都安装好后 运行一下&#xff0c;然后可以试着访问一下&#xff0c;报错是因为里面没内容而已&#xff0c;不影响,准备工作就做好了 解题 代码审计 const express require(express) var hbs require…

考生作弊行为分析算法

考生作弊行为分析系统利用pythonyolo系列网络模型算法框架&#xff0c;考生作弊行为分析算法利用图像处理和智能算法对考生的行为进行分析和识别&#xff0c;经过算法服务器的复杂计算和逻辑判断&#xff0c;算法将根据考生行为的特征和规律&#xff0c;判定是否存在作弊行为。…

appium+python自动化测试

获取APP的包名 1、aapt即Android Asset Packaging Tool&#xff0c;在SDK的build-tools目录下。该工具可以查看apk包名和launcherActivity 2、在android-sdk里面双击SDK-manager,下载buidl-tools 3、勾选build-tools&#xff0c;随便选一个版本&#xff0c;我这里选的是24的版…

windows Etcd的安装与使用

一、简介 etcd是一个分布式一致性键值存储&#xff0c;其主要用于分布式系统的共享配置和服务发现。 etcd由Go语言编写 二、下载并安装 1.下载地址&#xff1a; https://github.com/coreos/etcd/releases 解压后的目录如下&#xff1a;其中etcd.exe是服务端&#xff0c;e…