企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
### 集合元素在内存如何存放 数据元素在内存中,主要有2种存储方式: 1、顺序存储,Random Access(或直接存储,Direct Access): 这种方式,相邻的数据元素存放于相邻的内存地址中,整块内存地址是连续的。可以根据元素的位置直接计算出内存地址,直接进行读取。读取一个特定位置元素的平均时间复杂度为O(1)。这种数据结构插入和删除时比较麻烦,查询比较方便。正常来说,只有基于数组实现的集合,才有这种特性。Java中以ArrayList为代表; 2、链式存储,Sequential Access: 这种方式是将数据元素放在独立的空间中,在内存中每个元素的内存地址都不要求是相邻的。所以每个元素中需要保存下一个元素的索引,即下一个元素的内存地址。所以这种数据结构插入和删除比较方便,但是查找很麻烦,要从第一个开始遍历,因为它无法根据元素的位置直接计算出内存地址,只能按顺序读取元素。读取一个特定位置元素的平均时间复杂度为O(n)。主要以链表为代表。Java中以LinkedList为代表; ### Java集合遍历方式 1)传统的for循环遍历基于计数器 遍历者自己在集合外部维护一个计数器,然后依次读取每一个位置的元素,直到读取到最后一个元素后。主要就是需要按元素的位置来读取元素,适合于遍历顺序存储的数据结构; ``` for (int i = 0; i < list.size(); i++) { list.get(i); } ``` 2)迭代器遍历Iterator Iterator本来是OO的一个设计模式,主要目的就是屏蔽不同数据集合的特点,统一遍历集合的接口。从结构上可以看出,迭代器模式在客户与容器之间加入了迭代器角色。迭代器角色的加入,就可以很好的避免容器内部细节的暴露,而且也使得设计符号“单一职责原则”; 每一个具体实现的数据集合,一般都需要提供相应的Iterator。相比于传统for循环,Iterator取代了显式的遍历计数器。所以基于顺序存储集合的Iterator可以直接按位置访问数据。而基于链式存储集合的Iterator,正常的实现都是需要保存当前遍历的位置,然后根据当前位置来向前或者向后移动指针 迭代器模式的写法如下: ``` Iterator iterator = list.iterator(); while (iterator.hasNext()) { iterator.next(); } ``` 3)foreach循环遍历 foreach内部也是采用了Iterator的方式实现,根据反编译的字节码可以发现,foreach内部也是采用了Iterator的方式实现,只不过Java编译器帮我们生成了这些代码 优点:代码简洁,不易出错 缺点:只能做简单的遍历,不能在遍历过程中操作(删除、替换)数据集合 ``` for (ElementType element : list) { } ``` ### 遍历性能 1)传统的for循环遍历基于计数器 ArrayList按位置读取的代码:直接按元素位置读取 ``` public E get(int index) { rangeCheck(index); return elementData(index); } E elementData(int index) { return (E) elementData[index]; } ``` LinkedList按位置读取的代码:如果index小于容量的一半,则从头开始查找,否则从尾部开始查找 ``` public E get(int index) { checkElementIndex(index); return node(index).item; } Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } } ``` 2)迭代器遍历Iterator 对于RandomAccess类型的集合来说,如ArrayList迭代器遍历反而因为一些额外的操作,还会增加额外的运行时间; 对于Sequential Access的集合来说,因为Iterator内部维护了当前遍历的位置,所以每次遍历,读取下一个位置并不需要从集合的第一个元素开始查找,只要把指针向后移一位就行了,这样一来遍历整个集合的时间复杂度就降低为O(n);以LinkedList的迭代器为例,其内部实现,就是维护当前遍历的位置,然后操作指针移动就可以了 3)foreach循环遍历 分析Java字节码可知,foreach内部实现原理是通过Iterator实现的,只不过这个Iterator是Java编译器帮我们生成的,所以我们不需要再手动去编写。但是因为每次都要做类型转换检查,所以花费的时间比Iterator略长,而时间复杂度和Iterator是一样 ### 使用场景 1)传统的for循环遍历基于计数器 适用于遍历顺序存储集合,读取性能比较高如ArrayList;不适用于遍历链式存储的集合如LinkedList,时间复杂度太大; 2)迭代器遍历Iterator 对于顺序存储的数据结构,如果不是太在意时间,推荐选择此方式,毕竟代码更加简洁,也防止了Off-By-One的问题。 链式存储:推荐此种遍历方式,平均时间复杂度降为O(n) 3)foreach循环遍历 foreach让代码更加简洁,缺点就是遍历过程中不能操作数据集合(删除等),所以有些场合不使用。而且它本身就是基于Iterator实现的,但是由于类型转换的问题,所以会比直接使用Iterator慢一点,性能上相差不大