ArrayList源码阅读

news2024/9/25 13:17:07

文章目录

  • 简介
  • 例子
  • 继承结构概览
  • 代码分析
    • 成员变量
    • 方法
      • 迭代器
      • 子列表
  • 总结
  • 参考链接

本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习

本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,否则你会不知道我在说什么

简介

ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。

例子

ArrayList的API使用起来还是比较简单的,例子的话简单看看就行。

ArrayList<String> sites = new ArrayList<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Weibo");
System.out.println(sites); // 输出[Google, Runoob, Taobao, Weibo]
System.out.println(sites.get(1));  // 访问第二个元素,输出Runoob
sites.set(2, "Wiki"); // 修改第三个元素为Wiki
sites.remove(3); // 删除第四个元素
System.out.println(sites.size()); // 得到元素个数
// 使用for-each遍历
for (String i : sites) {
  System.out.println(i);
}

继承结构概览

当开始分析一个类之前,肯定得知道它继承了什么类或者接口,因为在该类中可能会调用父类的方法,因此有必要先去了解这个类的继承体系。ArrayList继承于AbstractList,AbstractList继承于AbstractCollection并实现List,AbstractCollection又实现Collection。用图表示如下,隐藏了不太算JCF相关的接口,比如Cloneable,Object这些,只保留集合相关的类和接口:

image-20231229175147152

其中的AbstractCollection用于提供一个实现了Collection的抽象类,提供了一些在实现Collection接口时某些方法的通用实现,类似一个骨架,重复性工作都给你做好了,想要实现Collection的类可以首先考虑继承AbstractCollection。

类似地,AbstractList是List的骨架类,提供了一些在实现List接口时某些方法的通用实现。

不熟悉JCF的基础接口和类的话,可以先看这篇:JCF相关基础类接口/抽象类源码阅读

整明白JCF继承结构之后,看一下完整的继承结构:

image-20240107142510975

也就多了三个标记接口:RandomAccess、Cloneabl、Serializable,标记接口指的是没有任何方法的空接口,比如RandomAccess只是用来给使用ArrayList的用户说明这个类可以高效地实现随机访问。这些标记接口不影响我们分析类,想进一步了解可以参考我的JCF基础的博客,这里不再赘述。

代码分析

成员变量

ArrayList的成员变量很少,只有底层数组和size:

transient Object[] elementData;
private int size;

因此ArrayList其实就是一个对原始数组的封装,并且内部通过强制类型转换实现Object到泛型的转换。

因为是用数组实现的,数组本身的大小是固定的,当大小不足容下更多的元素,只能重新创建更大的新数组,而且为了避免频繁扩容带来的开销,扩容后的大小总是会 >= 实际元素的个数。因此引出两个概念:capacity和size,前者是数组的大小,即elementData.length,后者则是ArrayList实际存储了的元素个数,存于size变量中。

另外还提前声明了两个静态空数组,作用如下:

// capacity为0时直接用它,避免重复创建空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于无参构造函数构造时,capacity为0,之后第一次扩容至少要到DEFAULT_CAPACITY。与EMPTY_ELEMENTDATA区分开来
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

方法

方法这块看起来讲得比较乱,其实顺序是按照源码从上到下的顺序讲的,并且函数之间的调用我会先讲被调用的函数,或者串讲。

ArrayList有三种构造函数:

// 空列表,capacity和size都为0,第一次扩容至少要扩容到DEFAULT_CAPACITY
public ArrayList();

// capacity为initalCapacity,size为0
public ArrayList(int initialCapacity);

// capacity和size都为c.length,保存的元素与c中的相同
public ArrayList(Collection<? extends E> c);

比较直观,不列实现代码了。下面看下其他方法,其中一些实现简单的方法就不细讲

// 将capacity缩小到刚好能容纳所有元素,具体是通过elementData = Arrays.copyOf(elementData, size)
public void trimToSize();

// 确保数组大小至少为minCapacity
public void ensureCapacity(int minCapacity) {
  // 第二个条件:当elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,至少会扩容到DEFAULT_CAPACITY,因此minCapacity小于DEFAULT_CAPACITY的话,直接忽略本次扩容
  if (minCapacity > elementData.length
    && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
         && minCapacity <= DEFAULT_CAPACITY)) {
    modCount++;
    grow(minCapacity);
  }
}

// 将数组扩容到至少minCapacity
private Object[] grow(int minCapacity) {
  int oldCapacity = elementData.length;
  if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    // 如果数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    // 扩容为oldCapacity的1.5倍(希望扩容的大小),否则扩容到minCapacity
    int newCapacity = ArraysSupport.newLength(oldCapacity,
      minCapacity - oldCapacity, /* minimum growth */
      oldCapacity >> 1           /* preferred growth */);
    return elementData = Arrays.copyOf(elementData, newCapacity);
  } else {
    // 如果数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,至少扩容到DEFAULT_CAPACITY
    return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
  }
}

这里grow(minCapacity)方法中,ArraysSupport.newLength是个什么的呢?简单来说这个函数是基于当前数组长度、希望增长的长度、最少增长的长度,来计算新的数组长度的。当然,里面还有更加细致的考量,了解一下即可:

// 基于当前数组的长度,计算新的长度
// minGrowth是最少要增长的长度
// prefGrowth是希望增长的长度
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
  int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
  if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
    // 如果希望增长的长度合适,则直接返回
    return prefLength;
  } else {
		// 希望增长的长度不适合,继续计算
    return hugeLength(oldLength, minGrowth);
  }
}

private static int hugeLength(int oldLength, int minGrowth) {
  int minLength = oldLength + minGrowth;
  if (minLength < 0) {
    // minLength > Integer.MAX
    throw new OutOfMemoryError(
        "Required array length " + oldLength + " + " + minGrowth + " is too large");
  } else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
    // minLength <= SOFT_MAX_ARRAY_LENGTH,说明prefGrowth > minGrowth但是prefGrowth会导致prefLength > SOFT_MAX_ARRAY_LENGTH,那么让结果尽量接近prefGrowth,直接返回SOFT_MAX_ARRAY_LENGTH
    return SOFT_MAX_ARRAY_LENGTH;
  } else {
    // minLength > SOFT_MAX_ARRAY_LENGTH 并且 minLength < Integer.MAX,倒也可以返回minLength。SOFT_MAX_ARRAY_LENGTH实际上是照顾JVM的实现,因为有的JVM对最大数组大小的限制在Integer.MAX附近,而SOFT_MAX_ARRAY_LENGTH可以保证不会超出大部分JVM最大数组的限制
    return minLength;
  }
}

继续看其他的方法:

// ArrayList继承了Cloneable,支持克隆一个新的ArrayList。底层数组与克隆的底层数组是两个不同的实例,但里面的元素都是一样的
public Object clone() {
  try {
    // 获取浅拷贝
    ArrayList<?> v = (ArrayList<?>) super.clone();
    // 创建新数组
    v.elementData = Arrays.copyOf(elementData, size);
    // 新的ArrayList,modCount(结构性修改次数)自然是0
    v.modCount = 0;
    return v;
  } catch (CloneNotSupportedException e) {
    // this shouldn't happen, since we are Cloneable
    throw new InternalError(e);
  }
}

// 将元素插入index为下标的位置,原来[index, size)的元素都往后移动
public void add(int index, E element) {
  rangeCheckForAdd(index);
  modCount++;
  final int s;
  Object[] elementData;
  if ((s = size) == (elementData = this.elementData).length)
    elementData = grow();
  // 将从index开始到数组末尾的元素,整体复制到index+1开始的位置
  // remove(index)的实现也是用这个System.arraycopy,只不过是index+1的位置复制到index而已
  System.arraycopy(elementData, index,
                   elementData, index + 1,
                   s - index);
  elementData[index] = element;
  size = s + 1;
}

// 当两个List拥有相同size并且每个元素都equals时返回true
// 如果o不是ArrayList(但仍是List)则使用迭代器遍历,如果是ArrayList的话,使用fori遍历
public boolean equals(Object o) {
  if (o == this) {
    return true;
  }

  if (!(o instanceof List)) {
    return false;
  }

  final int expectedModCount = modCount;
  boolean equal = (o.getClass() == ArrayList.class)
      ? equalsArrayList((ArrayList<?>) o)
      : equalsRange((List<?>) o, 0, size);

  checkForComodification(expectedModCount);
  return equal;
}

// clear名为删除所有元素,在ArrayList中实现为将数组每个引用置空,使得将来可以复用数组
public void clear() {
  modCount++;
  final Object[] es = elementData;
  for (int to = size, i = size = 0; i < to; i++)
    es[i] = null;
}

还有几个可能稍微难理解一点的方法:

// 相当于删除在[lo, hi)间的元素
private void shiftTailOverGap(Object[] es, int lo, int hi) {
  System.arraycopy(es, hi, es, lo, size - hi);
  for (int to = size, i = (size -= hi - lo); i < to; i++)
    es[i] = null;
}

// 当complement=false,删除在[from, end)间,且存在于c中的元素
// 当complement=true,删除在[from, end)间,且不存在于c中的元素
boolean batchRemove(
  Collection<?> c, boolean complement,
  final int from, final int end) {
  Objects.requireNonNull(c);
  final Object[] es = elementData;
  int r;
  // 确定从哪开始删除:这个for循环完了之后,r保存第一个将被删除元素的下标
  for (r = from;; r++) {
    if (r == end)
      return false;
    if (c.contains(es[r]) != complement)
      break;
  }
  // w保存将要保留的元素的下标
  // 这是实现删除数组中某些元素的时候常用的双指针算法,目的是保持数组元素连续:w和r分别为两个指针,r负责遍历(r无条件自增),当r指向的元素符合条件的时候存到w的位置,w自增(不符合条件的话w保持当前值)
  int w = r++;
  try {
    for (Object e; r < end; r++)
      if (c.contains(e = es[r]) == complement) // 遍历到要删除的元素
        es[w++] = e;
  } catch (Throwable ex) {
    // 如果contains抛异常,为了避免数组出现空洞做的善后处理
    // 放弃遍历[r, end)这堆没遍历完的元素,直接把他们前移到w指向的位置
    System.arraycopy(es, r, es, w, end - r);
    w += end - r;
    throw ex;
  } finally {
    modCount += end - w;
    // 处理完元素之后[w, end)这段会出现空洞,把[end, size)前移进来
    shiftTailOverGap(es, w, end);
  }
  return true;
}

// 删除[i, end)区间中所有满足filter的元素
// 与batchRemove大同小异,不同之处只是在于条件通过filter.test判断
boolean removeIf(Predicate<? super E> filter, int i, final int end) {
  Objects.requireNonNull(filter);
  int expectedModCount = modCount;
  final Object[] es = elementData;
  // 确定从哪开始删除:这个for循环完了之后,i保存第一个将被删除元素的下标
  for (; i < end && !filter.test(elementAt(es, i)); i++)
    ;
  if (i < end) {
    // 与batchRemove的不同:先标记所有符合条件(将被删除)的元素,然后才删除
    // 这样做的原因在于:filter.test传入的是当前遍历到的列表元素,但其可能不仅依赖于传入的元素来决定返回值为true还是false,而是依赖于整个列表(也就是可能会依赖于其他的元素)。因此在要保证在filter遍历的过程中列表是可重入读的,即不能遇到filter.test返回true就立刻删除,这样对于下一次filter.test来说列表已经发生了改变。而是用deathRow先标记将要删除的元素,遍历完之后再一次性删除。
    final int beg = i;
    final long[] deathRow = nBits(end - beg);
    deathRow[0] = 1L;
    for (i = beg + 1; i < end; i++)
      if (filter.test(elementAt(es, i)))
        setBit(deathRow, i - beg);
    if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
    modCount++;
    int w = beg;
    // 双指针w和i
    for (i = beg; i < end; i++)
      if (isClear(deathRow, i - beg))
        es[w++] = es[i];
    shiftTailOverGap(es, w, end);
    return true;
  } else {
    if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
    return false;
  }
}

第一个函数shiftTailOverGap不用多说

第二个函数batchRemove简单总结就是用来移除数组中包含于c/不包含于c中的元素,并且函数退出之后,数组不能出现空洞,即数组留下来的元素得保持连续,不然的话之后这个数组还怎么使用hhhhh。

第三个函数removeIf与batchRemove是一样的思想,只不过移除的是“符合条件的元素”,用途更为广泛一点。batchRemove相当于是其的一个特例,条件为“是否包含于/不包含于c”。然后还得保证predict(filter)的可重入性,也就是该方法对于调用者有一个规范/假设:对每个元素进行predict的时,列表结构不能发生改变,更具体的解释可以看注释。

至此,基本的方法介绍得差不多了,都比较简单,下面来看一下迭代器和子列表相关的东西。ArrayList没有使用AbstractList提供的默认迭代器类和子列表类,而是自己重新实现了一套,至于为什么,我想是为了直接操纵底层数组提高运行效率(注释也说了An optimized version of AbstractList.Itr)。

先看迭代器:

迭代器

private class Itr implements Iterator<E> {
  int cursor;       // index of next element to return
  int lastRet = -1; // index of last element returned; -1 if no such
  int expectedModCount = modCount;
  
  // ...
}

private class ListItr extends Itr implements ListIterator<E> {
  // ...
}

还是熟悉的配方。。。参考我JCF博客关于AbstractList.Itr和AbstractList.ListItr的分析就行了,大同小异。不同之处只是ArrayList直接访问底层数组,而AbstractList借助get方法访问。

迭代器是fail-fast的,意思是会在运行过程中检测结构性修改,如果有的话直接抛出ConcurrentModificationException异常,而不是继续迭代遍历。这一点其实在AbstractList.Itr也是一样的。

但注意ArrayList不是并发安全的,因此用户不能完全依赖于fail-test来判断有没有发生结构性修改,可能在某个时间点检查是正常的,在这个时间点之后发生了结构性修改无法再检测出来,fail-fast只是尽最大努力去发现结构性修改。总结下来即:抛异常则一定发生了结构性修改,但没抛异常不一定意味着没发生结构性修改,因此这个特性只能用来debug,或者说这个是一个heuristic的功能(启发式的,意味着不一定准确)。

子列表

private static class SubList<E> extends AbstractList<E> implements RandomAccess {
  private final ArrayList<E> root;
  private final SubList<E> parent;
  private final int offset;
  private int size;
}

成员变量还是熟悉的配方,里面的方法都与AbstractList.SubList的大同小异,基本上类似根据菜谱做菜(先放油、再放菜、放盐、放葱)那样,有的方法看着有点长,其实就是步骤比较多:先访问元素、修改指针、检查并发访问…就不再介绍了。

总结

整体来说ArrayList没什么难点,掌握核心思想,即:ArrayList就是对数组进行封装的类,并通过扩容机制(每次扩容1.5倍)实现了动态修改大小的特性。其中有的方法看起来很长,其实是维护变量罢了,理解核心思想的话,并不难分析。还有几个方法比如batchRemove、removeIf可能有些让人疑惑的点,比如双指针算法、标记删除法,读懂就好了。

最后ArrayList不是并发安全的,进一步来说,里面有的方法可能会抛出ConcurrentModificationException,但是不抛出这个异常的情况下,不代表着没有“异常”。

参考链接

「博客园」JCF相关基础类接口/抽象类源码阅读

「博客园」J源码中transient的用途

「Java全栈知识体系」Collection - ArrayList 源码解析

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

activiti流程图+动态表单

使用技术 jeecg-bootactivitivue3form-create 简单效果展示 流程图绘制 审批人配置 动态表单配置 流程审批 流程审批记录 填写表单信息 源码地址 后台&#xff1a;https://gitee.com/houshixin/jmg-boot前端&#xff1a;https://gitee.com/houshixin/jmg-ui

【MySQL】导入导出SQL脚本及远程备份---超详细介绍

目录 前言&#xff1a; 一 navcat导入导出 1.1 导入 1.2 导出 二 mysqldump 导入导出 2.1 导入 2.2 导出 三 load data infile命令导入导出 3.1 导入 3.2 导出 四 远程备份 五 思维导图 前言&#xff1a; 随着当今企业发展&#xff0c;数据库的数据越来越多&…

HTML标签(一)

目录 HTML语法规范 基本语法概述 标签关系 HTML基本结构标签 第一个HTML网页 开发工具 VSCode的使用&#xff1a; VScode工具生成骨架标签新增代码 文档类型声明标签 lang语言种类 字符集 总结 HTML常用标签 标签语义 标题标签 段落标签 换行标签 文本格式化…

vue前端开发自学,异步加载组件,提升用户端的客户体验度

vue前端开发自学,异步加载组件,提升用户端的客户体验度&#xff01;现实项目开发时&#xff0c;组件的数量非常庞大&#xff0c;如果都是一口气加载完&#xff0c;对手机用户来说&#xff0c;体验度会很差。因此&#xff0c;非常有必要使用异步加载。 那就是&#xff0c;用到了…

【贪心】重构字符串

/*** 思路&#xff1a;如果s长度小于2&#xff0c;直接返回s&#xff0c;假设字符串s的长度为n。* n为偶数&#xff0c;如果字符串中的某个字符数量超过 n/2 则肯定会存在相邻的字符。* n为奇数&#xff0c;如果字符串中的某个字符的数量超过 &#xff08;n1&am…

强力推荐:本地文件加密软件—超详细加密步骤来了!

在数字化时代&#xff0c;数据安全问题日益受到人们的关注。 为了保护个人和企业的重要信息不被泄露&#xff0c;越来越多的人开始使用文件加密软件。 尤其是常常会有数据泄露风险的企业更是需要一款非常给力的加密工具来保护企业数据安全。 一、选择合适的加密软件 在选择加…

HackTheBox - Medium - Linux - Shared

Shared Shared 是一台中等难度的 Linux 机器&#xff0c;它具有通向立足点的 Cookie SQL 注入&#xff0c;然后通过对 Golang 二进制文件进行逆向工程并利用两个 CVE 来获得 root shell 来提升权限。 外部信息收集 端口扫描 循例nmap Web枚举 查看证书 看到这个扫了一下vhos…

DNS解析和它的三个实验

一、DNS介绍 DNS&#xff1a;domain name server 7层协议 名称解析协议 tcp /53 主从之间的同步 udp/53 名字解析 DNS作用&#xff1a;将域名转换成IP地址的协议 1.1DNS的两种实现方式 1.通过hosts文件&#xff08;优先级最高&#xff09; 分散的管理 linux /etc/hos…

如何高效进行项目文档的编制及管理?

“做完一个项目到底会产出多少份文档&#xff1f;” 今天看到这样一个吐槽贴&#xff1a;小李作为刚入行的项目经理&#xff0c;每天上班期间电话、会议、邮件各种不停歇&#xff0c;晚上还要加班做各种文档&#xff1b;由于经验不足&#xff0c;熬到十一二点还做不完是常态。…

jmeter请求发送加密参数

最近在做http加密接口&#xff0c;请求头的uid参数及body的请求json参数都经过加密再发送请求&#xff0c;加密方式为&#xff1a;ase256。所以&#xff0c;jmeter发送请求前也需要对uid及json参数进行加密。我这里是让开发写了个加密、解密的jar&#xff0c;jmeter直接调用这个…

模板与STL(C++)

七、模板与STL&#xff08;泛型编程&#xff09;7.1 模板的概念7.2 函数模板7.2.1 函数模板的定义7.2.2 函数模板的实例化7.2.3 模板参数7.2.4 函数模板的特化 7.3 类模板7.3.1 类模板的定义7.3.2 类模板实例化7.3.3 类模板特化 七、模板与STL&#xff08;泛型编程&#xff09;…

获得利润和成长,应采取什么步骤, 澳福认为只需坚持一点

大多数交易者通常会考虑在外汇交易中获取利润&#xff0c;但只有少数人会思考这样一个问题:为了获得利润和专业成长&#xff0c;应该采取什么步骤。像“外汇交易怎么赢利”这样的文章很受市场欢迎&#xff0c;但是很少有人在交易中使用这些文章中给出的建议&#xff0c;因为在生…

python设计模式有哪几种

Python 中常见的设计模式有以下几种 一 单例模式&#xff08;Singleton Pattern&#xff09;&#xff1a;确保一个类只有一个实例&#xff0c;并提供全局访问点。 二 工厂模式&#xff08;Factory Pattern&#xff09;&#xff1a;使用工厂方法来创建对象&#xff0c;而不是直…

InternLM第4次课笔记

XTuner 大模型单卡低成本微调实战 1 Finetune介绍 2 XTuner介绍 XTuner中微调的技术&#xff1a; 3 8GB显卡玩转LLM 4 动手实战环节

Vmware虚拟机问题解决方案 运行虚拟机系统蓝屏 运行虚拟机时报错VT-x

1. 运行虚拟机系统蓝屏 可能的原因有两个: 1). 虚拟机所在磁盘的空间不足 ; -------> 清理磁盘空间 。 2). 操作系统版本高, 需要适配新版本的Vmware ; ------> 卸载Vmware15版本, 安装Vmware16版本 。 2. 卸载Vmware步骤 1). 卸载已经安装的VMware 弹出确认框, 点击…

解决“Ubuntu系统与windows系统之间不能执行复制粘贴”之问题

在win11中&#xff0c;发现“Ubuntu系统与windows系统之间不能互相复制粘贴”&#xff0c;只能通过“FPT客户端FileZilla”才能交换文件&#xff0c;但遇到字符串&#xff0c;就没法实现了&#xff0c;因此&#xff0c;在两个系统之间实现互相复制和粘贴字符串&#xff0c;就很…

绘图工具用的好,头发掉的少

程序员不管是在学习&#xff0c;还是工作过程中&#xff0c;很多时候都需要画图&#xff0c;如产品分析、架构设计、方案选型等&#xff0c;良好的绘图不仅可以让绘图者的思路清晰&#xff0c;也可以让聆听者更好的理解。用好画图&#xff0c;升职加薪少不了&#xff01;今天介…

爬虫案例—表情党图片data-src抓取

爬虫案例—表情党图片data-src抓取 表情党网址&#xff1a;https://qq.yh31.com 抓取心情板块的图片data-src 由于此页面采用的是懒加载技术&#xff0c;为了节省网络带宽和减轻服务器压力。不浏览的图片&#xff0c;页面不加载&#xff0c;统一显示LOADING…。如下图&#x…

C++学习笔记——继承和动态内存分配

目录 一、继承 二、动态内存分配 三、继承的细节 四、动态内存分配细节 五、一个动物园管理系统 继承和动态内存分配是C中两个重要的概念 一、继承 继承是C中面向对象编程的一个重要特性&#xff0c;它允许我们创建一个新类&#xff0c;该类从现有的类中继承属性和方法&…

多区域isis配置实验

一、预习&#xff1a; IS-IS&#xff1a;Intermediate System to Intermediate System&#xff0c;中间系统到中间系统&#xff0c;是ISO为它的CLNP&#xff08;ConnectionLess Network Protocol&#xff09;设计的一种动态路由协议&#xff0c;后来为了提供对IP路由的支持&…