java-容器-ArrayList
工作中经常会用到Java的集合类,最近不忙了,把相关知识总结一下,便于理解记忆。
打开java.util.ArrayList的源代码,首先映入眼帘的是@author Josh Bloch(相对于源码,本人更喜欢看故事,每次看到一份源码,先看看作者是谁。
言归正传,看一个类的时候首先要看看这个类能干什么,有什么特性。这些都可以在这个类实现的接口上体现了(废话。。。)。好,直接从最顶级的接口看吧。首先是接口java.lang.Iterable:
package java.lang;
import java.util.Iterator;
public interface Iterable<T> {
Iterator<T> iterator();
}
由于篇幅的关系,去掉了注释。注释上说明,实现了这个接口的类,可以使用"foreach"语法。
接下来是接口java.util.Collection:
package java.util;
public interface Collection<E> extends Iterable<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
}
里面的注释很详细,反正大概意思是说(英语不太好),这是集合层次的顶级接口,代表了一组对象blablablabla,所有通用的实现应该提供两个"标准"的构造方法:无参的构造方法和拥有一个集合类型参数的构造方法blablablabla,总之这个接口对集合进行了高度滴、抽象滴定义:)
接下来就是java.util.List接口了。
public interface List<E> extends Collection<E> {
//和Collection重复的方法就不贴了,但必须知道有这些方法
boolean addAll(int index, Collection<? extends E> c);
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
List<E> subList(int fromIndex, int toIndex);
}
list代表了一个有序的集合,可以通过index来访问和查找集合内的元素,集合内元素是可重复的,因为index(集合中的位置)不同。
还有一个ArrayList实现的接口是java.util.RandomAccess:
package java.util;
public interface RandomAccess {
}
一看没有方法的接口,就知道这大概是个标记接口喽。实现这个接口的集合类会在随机访问元素上提供很好的性能。比如说,ArrayList实现了RandomAccess而LinkedList没有,那么当我们拿到一个集合时(假设这个集合不是ArrayList就是LinkedList),如果这个集合时ArrayList,那么我们遍历集合元素可以使用下标遍历,如果是LinkedList,就使用迭代器遍历。比如集合工具类Collections中提供的对集合中元素进行二分查找的方法,代码片段如下:
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
还有java.lang.Cloneable和java.io.Serializable两个标记接口,表示ArrayList是可克隆的、可序列化的。
还要看一下AbstractCollection和AbstractList,这两个抽象类里实现了一部分公共的功能,代码都还比较容易看懂。
需要注意的地方是AbstractList中的一个属性modCount。这个属性主要由集合的迭代器来使用,对于List来说,可以调用iterator()和listIterator()等方法来生成一个迭代器,这个迭代器在生成时会将List的modCount保存起来(迭代器实现为List的内部类),在迭代过程中会去检查当前list的modCount是否发生了变化(和自己保存的进行比较),如果发生变化,那么马上抛出java.util.ConcurrentModificationException异常,这种行为就是fail-fast。
好了,终于可以看ArrayList了,在看之前可以先思考一下,如果我们自己来实现ArrayList,要怎么实现。我们都会把ArrayList简单的看作动态数组,说明我们使用它的时候大都是当做数组来使用的,只不过它的长度是可改变的。那我们的问题就变成了一个实现长度可变化的数组的问题了。那么首先,我们会用一个数组来做底层数据存储的结构,新建的时候会提供一个默认的数组长度。当我们添加或删除元素的时候,会改变数据的长度(当默认长度不够或者其他情况下),这里涉及到数据的拷贝,我们可以使用系统提供的System.arraycopy来进行数组拷贝。如果我们每次添加或者删除元素时都会因为改变数组长度而拷贝数组,那也太2了,可以在当数组长度不够的时候,扩展一定的空间,比如可以扩展到原来的2倍,这样会提高一点性能(如果不是在数组末尾添加或删除元素的话,还是会进行数组拷贝),但同时浪费了一点空间。再看看上面的接口,比如我们要实现size方法,那么就不能直接返回数组的长度了(由于上面的原因),也没办法去遍历数组得到长度(因为可能存在null元素),我们会想到利用一个私有变量来保存长度,在添加删除等方法里面去相应修改这个变量(这里不考虑线程安全问题,因为ArrayList本身就不是线程安全的)。
带着这些问题,来看一下ArrayList的代码。 首先,真正存储元素的是一个数组。
private transient Object[] elementData;
那么这个数组该怎么初始化呢?数组的长度该怎么确定呢?看一下构造方法。
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
public ArrayList() {
this(10);
}
可以看到,我们可以选择使用一个参数作为ArrayList内部数组的容量来构造一个ArrayList;或者使用无参的构造方法,这样会默认使用10作为数组容量。我们在实际应用中可以根据具体情况来选择使用哪个构造方法,例如在某种特定逻辑下,我们需要存储200个元素(固定值)到一个ArrayList里,那我们就可以使用带一个int型参数的构造方法,这样就避免了在数组长度不够,进行扩容时,内部数组的拷贝。
当然,根据接口java.util.Collection的规范(非强制),ArrayList也提供用一个集合做为参数的构造方法。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
上面说到说考虑用一个私有变量来存储size(集合中实际原始的个数)。在ArrayList也有这样一个变量。
private int size;
有了这个变量,一些方法实现起来就非常简单了。
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
由于内部数组的存在,一些基于下标的方法实现起来也非常简单。
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
public E set(int index, E element) {
RangeCheck(index);
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
public boolean add(E e) {
ensureCapacity(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
ensureCapacity(size+1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
有几个要注意的地方,首先是RangeCheck方法,顾名思义是做边界检查的,看看方法实现。
private void RangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
}
异常信息是不是很眼熟,呵呵。
还有就是在"添加"方法中的ensureCapacity方法,上面也考虑到扩容的问题,这个方法其实就干了这件事情。
public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
注意当数组长度不够的时候,会进行数组的扩展,扩展到原来长度的1.5倍(注意整型的除法)加1,比如原来是2,会扩展到4,原来是30,会扩展到46。当数组的长度大到算出的新容量超出了整型最大范围,那么新容量就等于传入的"最小容量"。
最后再看一下另一个删除方法。
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
}
可以看到,像size、isEmpty、get、set这样的方法时间复杂度为O(1),而像indexOf、add、remove等方法,最坏的情况下(如添加元素到第一个位置,删除第一个位置的元素,找最后一个元素的下标等)时间复杂度为O(n)。
一般情况下内部数组的长度总是大于集合中元素总个数的,ArrayList也提供了一个释放多余空间的方法,我们可以适时调用此方法来减少内存占用。
public void trimToSize() {
modCount++;
int oldCapacity = elementData.length;
if (size < oldCapacity) {
elementData = Arrays.copyOf(elementData, size);
}
}
基本上ArrayList的内容总结的差不多啦。最后,别忘了它是线程不安全的。
java-容器-ArrayList的更多相关文章
- 数组容器(ArrayList)设计与Java实现,看完这个你不懂ArrayList,你找我!!!
数组容器(ArrayList)设计与Java实现 本篇文章主要跟大家介绍我们最常使用的一种容器ArrayList.Vector的原理,并且自己使用Java实现自己的数组容器MyArrayList,让自 ...
- Java容器解析系列(4) ArrayList Vector Stack 详解
ArrayList 这里关于ArrayList本来都读了一遍源码,并且写了一些了,突然在原来的笔记里面发现了收藏的有相关博客,大致看了一下,这些就是我要写的(╹▽╹),而且估计我还写不到博主的水平,这 ...
- 【Java心得总结七】Java容器下——Map
我将容器类库自己平时编程及看书的感受总结成了三篇博文,前两篇分别是:[Java心得总结五]Java容器上——容器初探和[Java心得总结六]Java容器中——Collection,第一篇从宏观整体的角 ...
- 【Java心得总结六】Java容器中——Collection
在[Java心得总结五]Java容器上——容器初探这篇博文中,我对Java容器类库从一个整体的偏向于宏观的角度初步认识了Java容器类库.而在这篇博文中,我想着重对容器类库中的Collection容器 ...
- 【Java心得总结五】Java容器上——容器初探
在数学中我们有集合的概念,所谓的一个集合,就是将数个对象归类而分成为一个或数个形态各异的大小整体. 一般来讲,集合是具有某种特性的事物的整体,或是一些确认对象的汇集.构成集合的事物或对象称作元素或是成 ...
- Java 容器(list, set, map)
java容器类库的简化图: (虚线框表示接口, 实线框表示普通的类, 空心箭头表示特定的类实现了接口, 实心箭头表示某个类可以生成箭头所指的类对象) 继承Collection的主要有Set 和 Lis ...
- Java - 容器详解
一.ArrayList 长度可变数组,类似于c++ STL中的vector. 元素以线性方式连续存储,内部允许存放重复元素. 允许对元素进行随机的快速访问,但是向ArrayList中插入和删除元素的速 ...
- java容器学习
容器是java中重要的一部分,其接口的结构如下 Collection | ------------------ Map | | | Set List HashMap | HashSet 顾名思义,容器 ...
- Java 容器:Collection 初探之 List
1 ///: JavaBasic//com.cnblogs.pattywgm.day1//CollectionTest.java 2 3 package com.cnblogs.pattywgm.da ...
- java容器---集合总结
思考为什么要引入容器这个概念? Java有多种方式保存对象(应该是对象的引用),例如使用数组时保存一组对象中的最有效的方式,如果你想保存一组基本类型的数据,也推荐使用这种方式,但大家知道数组是具有固定 ...
随机推荐
- javascript中addEventListener(attachEvent)具体解释
addEventListener 有三个參数:第一个參数表示事件名称(不含 on,如 "click").第二个參数表示要接收事件处理的函数:第三个參数为 useCapture.样例 ...
- Mathematica 文本界面获得之前的结果
使用%号做标记.获得文本界面之前的运算结果:
- yii源码二 -- interfaces
path:framework/base/interfaces.php overview:This file contains core interfaces for Yii framework. in ...
- Reshaper cannot resolve symbol
问题 不知道出了什么问题,在代码视图发现有些关键词显示为红色,并且Reshaper提示消息为Reshaper cannot resolve symbol XXX ,但编辑不会报错. 虽然不影响使用,但 ...
- Android 之 SharedPreferences应用
Android 平台给我们提供了一个 SharedPreferences 类,它是一个轻量级的存储类,特别适合用于保存共享数据.使用SharedPreferences保存数据,其背后是用xml文件存放 ...
- MVC 之 缓存机制(二)
八.应用程序缓存 应用程序缓存提供了一种编程方式,可通过键/值对将任意数据存储在内存中. 使用应用程序缓存与使用应用程序状态类似. 但是,与应用程序状态不同的是,应用程序缓存中的数据是易失的, 即数据 ...
- SQL到NoSQL概览性总结之一 数据库应用场景选型
数据库类型与实例 适合场景 不适合场景 场景举例 关系数据库 基于集合理论,具有行和列的二维表,严格使用类型 开源MySQL/MariaDB, PostgreSQL 商业:Oracle,DB2,SQL ...
- MySQL快速建立测试表
1:只要已经存在表结构的 第一种方式: CREATE TABLE T1 SELECT * FROM mysql.user ; 第二种方式: CREATE TABLE T2 LIKE mysql.use ...
- sell 项目 订单详情表 设计 及 创建
1.数据库设计 2.订单详情表 创建 /** * 订单详情表 */ create table `order_detail` ( `detail_id` varchar(32) not null, `o ...
- Influxdb数据压缩
环境: CentOS6.5_x64InfluxDB版本:1.1.0 数据压缩可以参考: https://docs.influxdata.com/influxdb/v1.1/concepts/stora ...