简介
在List中最常用的两个类就数ArrayList和LinkedList。他们两个的实现代表着数据结构中的两种种典型:线性表和链表。在这里,这个线性表是可以根据需要自动增长的。Java的库里面默认没有实现单链表,LinkedList实际上是一个双链表。这些具体的细节我们会在后续的代码里分析。
实际上,ArrayList和LinkedList他们之间的整体类关系图如下:
有了这个图作为参照,我们在后面就可以很容易的来分析他们的具体实现。前面的图片省略了部分实现接口的细节。
ArrayList
如果我们看过前面一篇文章关于stack和vector的分析的话,这里对ArrayList的分析就显得比较简单。从某种角度来说,ArrayList和Vector实在是太相似了。他们都是基于数组的线性表实现。在它的内部,都是使用一个private transient Object[] elementData;这样的数组来保存元素。在ArrayList中间还采用了一个private int size;这样的变量来保存当前list的长度。
构造
ArrayList有两个构造函数,分别如下:
public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } /** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this(10); } 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); }
这两个构造函数的实现比较简单。第一个指定一个无参数的构造函数,设置里面数组的默认长度为10. 第二个函数通过将一个Collection类型的参数传递进来转换为内部保存元素的数组。
增加元素
增加元素的方法有若干个,包括有add(E e) , add(int index, E element), addAll()的两个重载的方法。在讨论这几个方法的具体实现之前,我们先考虑一下他们实现的基础。考虑一个比较典型的情况,我们在很多情况下如果增加元素到数组里会导致里面保存元素的数组长度不够了,那么就需要调整数组的长度,保证它能够包含新增加进来的元素。同时,考虑到我们具体的限制,如果要增加的元素太多了,导致数组不可能增长到它极限长度Integer.MAX_VALUE。这个时候我们要考虑进行异常处理。下面是扩展底层数组长度相关的源代码:
public void ensureCapacity(int minCapacity) { if (minCapacity > 0) ensureCapacityInternal(minCapacity); } private void ensureCapacityInternal(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
在我们原来的数组长度不能满足要求的时候,我们首先考虑将数组增加到原来的1.5倍,如 int newCapacity = oldCapacity + (oldCapacity >> 1);这句所描述。当然,如果增加到原来1.5倍发现长度还是不能满足需要的时候,我们就进行了后续的比较。首先将数组长度设置为期望的参数长度,然后和我们设置的最大长度比较。如果没有这个最大长度MAX_ARRAY_SIZE大的话,就直接分配这么长的数组。否则就分配最大长度的数组来尝试,这个最大的长度就是Integer.MAX_VALUE。
有了这部分的讨论,我们再来看添加元素的实现时就毫无压力了:
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
我们每次增加元素前只要调用一下ensureCapacityInternal这个方法。它就帮我们把底层的数组调整全都做了。这里要做的无非就是把数组元素位置调整一下,然后长度加个1.addAll的两个方法也类似,无非就是传进来的是一个集合类型,我们要保证新传进来的数组长度加上原来元素的长度都在数组范围内。剩下的事情就是一通拷贝。显得一点技术含量都没有,这里就不再赘述了。
删除元素
在ArrayList里面删除元素主要包含有3种情形,一种是删除一个元素,一种是删除一个区段范围内的元素,还有一种是删除所有的元素。他们的总体情况如下表:
方法名称 | 方法签名 | 种类 |
remove | public E remove(int index) | 删除单个元素 |
remove | public boolean remove(Object o) | 删除单个元素 |
removeRange | protected void removeRange(int fromIndex, int toIndex) | 删除区段内元素 |
removeAll | public boolean removeAll() | 删除所有元素 |
clear | public void clear() | 删除所有元素 |
remove()方法的两种重载方法。具体的函数签名如下:
public E remove(int index) public boolean remove(Object o)
他们分别表示删除指定索引位置的元素和删除指定的元素。
还有一个是clear和removeRange方法,clear负责将数组里所有的元素删除。removeRange方法则删除指定区段范围内的元素。我们先把他们的实现代码贴出来,然后再来一一分析。
public E remove(int index) { rangeCheck(index); modCount++; E oldValue = 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; } 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 } public void clear() { modCount++; // Let gc do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; } protected void removeRange(int fromIndex, int toIndex) { modCount++; int numMoved = size - toIndex; System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved); // Let gc do its work int newSize = size - (toIndex-fromIndex); while (size != newSize) elementData[--size] = null; }
我们先看remove(int index)这个方法的实现。在删除元素之前,我们先用rangeCheck方法判断指定的参数是否合法。因为有可能我们传入的参数不是在0到数组长度之间的。在找到要删除的元素之后,从它开始后面的元素向前移一位来实现元素的删除。这里int numMoved = size - index - 1;这一句指定了数组拷贝的长度。因为是要从index + 1这个元素到size - 1之间包括头尾这两个元素的一段往前挪一位,所以要移动的元素数目为size - index -1。
remove(Object o)这个方法的实现考量是不一样的。因为它是要从数组中删除一个指定的元素,所以要删除它之前我们必须要遍历整个数组来找到它。如果根本就找不到,那根本就什么都不需要做了。如果找到这个要删除的元素,则和前面一个方法的处理流程一样,后面一个元素到末尾的都往前移一位。
remove这两个重载的函数一般情况下不会混用,但是在一定情况下如果不理解清楚也会带来一些奇怪的问题。在我的这一篇文章中进行过详细的描述。
clear方法显然非常的简单。它将数组里所有引用都置为null,这样做是防止内存泄露,然后再将数组长度设置为0.
removeRange方法的参数指定了起始到终止的数据范围,所以实现的方法只要把终止元素到数组末尾的元素挪到原来起始位置的后面,再把空缺的那部分置空就可以了。
查找元素
查找元素主要包含有如下几个方法:contains, indexOf, lastIndexOf。最核心的就是indexOf方法:
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; }
这部分的代码非常简单,就是从头往后一直遍历和比较元素,找到相等的就返回。只是要考虑目的元素为null的情况。
lastIndexOf也很类似,只是从数组的末尾往前遍历,寻找目的元素。
LinkedList
我们在书本上看到的介绍也知道,如果我们要建立一个更加通用一点的链表结构的话,需要定义一个专门的节点数据类型。在jdk里,专门定义了一个内部类,它也决定了LinkedList是一个双向链表:
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
这是在LinkedList内部使用的数据节点类型。实际上因为它也仅限与在LinkedList内部使用,所以才被定义成内部的private static class。我们实际上在构造LinkedList的时候不需要自己来构造Node对象,因为在其内部定义的构造函数已经做了,我们只需要指定E这个参数的类型就可以了。
LinkedList里面包含了3个元素:
transient int size = 0; /** * Pointer to first node. * Invariant: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient Node<E> first; /** * Pointer to last node. * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last;
从注释就可以直观的看到,分别代表链表的长度,指向链表头和尾部的引用。
linkedList和ArrayList有很多在元素的添加、删除查找上面接口相同的方法。和ArrayList比较起来的区别在于它是通过内部遍历链表再来进行操作的。这部分就针对几个典型的操作挑几个来分析。
添加元素
添加元素里面比较典型的有linkFirst, linkLast。表示在链表头之前添加元素和链表尾部后面添加元素。他们的实现过程如下:
public void addFirst(E e) { linkFirst(e); } private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; } public void addLast(E e) { linkLast(e); } void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
这里要考虑的一个点就是不管是在头之前还是在尾之后添加元素,要注意判断头尾节点为空的情况。同时,在更新完之后对长度值加1.LinkedList有一个比ArrayList好的地方,因为它可以很灵活的添加元素而不需要做大的调整。在ArrayList里面如果元素数量足够大了要调整和拷贝元素。
删除元素
删除元素里面典型的两个操作是removeFirst()和removeLast()。
public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; final Node<E> next = f.next; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; } public E removeLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return unlinkLast(l); } private E unlinkLast(Node<E> l) { // assert l == last && l != null; final E element = l.item; final Node<E> prev = l.prev; l.item = null; l.prev = null; // help GC last = prev; if (prev == null) first = null; else prev.next = null; size--; modCount++; return element; }
在移除元素的时候,首先要判断first和last是否为空。当他们为空的时候意味着整个链表都是空的。还有一种需要考虑的情况就是如果整个链表只有一个元素,在删除了之后,实际上链表就空了。这里需next == null;和 prev == null;这两句就是分别在删除头和尾元素时判断删除后链表为空的条件。
LinkedList和ArrayList很多相同的部分比如索引数据,他们的实现都比较简单,这里就不再一一列举。
其他特性
如果我们看LinkedList的类声明:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
它实现了一个Deque的接口,这个接口定义了队列的实现规范。在后面一篇文章里讨论队列实现的时候,我们会重点讲述,这里就暂时省略。
另外,LinkedList还有一个有意思的特性,它本身也实现了一个栈的规范。和我们前面通过继承vector来实现Stack不一样。它采用每次直接在链表头之前添加元素来实现push方法,删除链表头元素实现pop方法。和栈实现相关的方法实现如下:
public E peekFirst() { final Node<E> f = first; return (f == null) ? null : f.item; } public E peekLast() { final Node<E> l = last; return (l == null) ? null : l.item; } public void push(E e) { addFirst(e); } public E pop() { return removeFirst(); }
这样,以后我们如果要使用栈的话,除了声明Stack类以外,把LinkedList当成Stack使也是可行的。
总结
要实现一个简单的ArrayList和LinkedList可以说是比较简单直观的事情,一个是只要声明一个数组,然后做索引访问就可以了。而另外一个是声明一个实体对象,并指定一个指向同类型的引用来建立链表。但是如果要实现一个比较实用的List需要考虑的地方就比较多。参照jdk里面的实现,我们可以看到很多实际工程中的限制在具体的实现中带来了哪些考量。另外,通过观察详细的实现,我们也可以澄清很多想当然的用法。
参考资料
http://docs.oracle.com/javase/7/docs/api/java/util/ArrayList.html
http://docs.oracle.com/javase/7/docs/api/java/util/LinkedList.html
相关推荐
本篇文章将深入探讨如何使用Java反射来获取一个类的所有属性、方法,并处理List集合类。 首先,让我们了解Java反射的基础概念。在Java中,`java.lang.Class`类代表运行时的类信息。我们可以使用`Class.forName()`...
3. 集合类的实现:分析List类和Map类的内部结构,理解它们如何存储和操作数据。 4. 面向接口编程:虽然易语言没有像Java那样的接口概念,但可以通过模拟接口的实现,提供类似的功能。 5. 键值对操作:了解如何在Map...
本篇将深入探讨Collection接口及其子接口,包括List和Set,以及相关的实例分析。 首先,Collection接口是所有单列集合的父接口,它定义了集合的基本操作,如添加元素(add),删除元素(remove)和判断是否包含某个元素...
这个“一个讲解很清晰的Java集合框架PPT”显然是一个对外公开的教育资源,旨在帮助学习者深入理解Java集合的概念、结构以及实际应用。 在Java中,集合框架主要包括四大接口:List、Set、Queue和Map。每个接口都有...
本篇文章将深入探讨Java集合框架的各个方面,帮助开发者从基础到高级全面掌握这一关键知识。 首先,我们要理解Java集合框架的基础概念。集合是对象的容器,可以容纳多个对象,而框架则是一组接口和实现这些接口的类...
本文将深入探讨Java中的两种重要容器类——`List`和`Set`,并分析它们之间的区别以及各自的适用场景。 #### 二、Java容器类List详解 **1. List接口简介** - `List`接口是`Collection`接口的一个子接口,主要特点...
以下是一些关于Java集合类的重要知识点: 1. **Collection与Collections的区别** - **Collection** 是所有单列集合的父接口,包括 List、Set 和 Queue。它定义了集合的基本操作,如增加元素、删除元素、遍历元素等...
Java集合框架是Java编程语言中的一个核心部分,它为数据结构和对象的存储、管理和操作提供了统一...通过学习和分析`chapter3.html`这样的文档,我们可以深入了解每个集合类的内部工作原理,进一步提升我们的编程技能。
首先,Java集合框架包括List、Set、Queue等接口,以及ArrayList、LinkedList、HashSet、HashMap等实现类。这些接口和类提供了存储、检索、遍历和修改元素的方法。例如,ArrayList是以动态数组为基础的列表实现,适合...
5. **PowerDesigner格式文件**:`.oom` 文件是PowerDesigner创建的对象模型文件,这可能包含了更详细的类图和关系图,可以用来深入分析Java集合类的内部结构和实现细节,比如类的属性、方法、以及它们之间的关联。...
3. **后续学习建议**:为了更深入地掌握Java集合类,建议继续学习其他类型的集合类如`LinkedList`、`HashSet`、`HashMap`等,并尝试解决更复杂的编程问题。此外,还可以探索Java 8中引入的流式处理(Stream API),...
本章PDF课件详细讲解了这些概念,并可能包括实例代码演示、性能分析以及各种操作的注意事项,旨在帮助学习者深入理解并熟练运用Java集合框架。通过学习,开发者可以更高效地管理内存资源,优化程序性能,解决实际...
本篇文章将深入探讨如何使用Java实现这些操作,并基于给出的`GroupFilterSortUtils.java`文件,我们可以推断这是一个工具类,它可能包含了分组、过滤和排序功能。 首先,让我们了解Java中的排序。在Java的`java....
博客中提到的“源码”标签可能意味着文章会深入到Java集合框架的内部实现,讨论如何优化过滤操作的性能,或者分析`List`接口及流API的源代码,帮助读者理解其工作原理。 至于“工具”标签,可能暗示文章介绍了某些...
这个"深入理解Java集合框架.zip"文件很可能是包含了一系列关于Java集合框架的详细资料,比如源码分析、设计模式以及最佳实践。下面将详细探讨Java集合框架的关键知识点。 1. **集合接口**:Java集合框架的核心接口...
总结来说,Java集合框架提供了丰富的数据结构和算法,能够适应各种编程需求。HashMap和IdentityHashMap满足了快速存取的需求,SortedMap则提供了有序存储的可能性。自定义类在Map中的使用允许我们根据业务逻辑进行...
这篇博文将深入探讨Java集合框架,包括其基本概念、常见类、接口和实现方式,以及如何进行有效的集合操作。以下是对这些知识点的详细说明: 1. **集合框架**: Java集合框架是一组接口和类,它们提供了在程序中...
5. **集合框架**:Java集合框架包括List、Set、Queue和Map等接口,以及ArrayList、LinkedList、HashSet、HashMap等实现类。实例会展示如何存储、操作和遍历这些数据结构。 6. **IO流**:Java的IO流处理涵盖了读写...
在这个主题中,我们将深入分析集合框架的源码,理解其内部工作原理,以便更好地利用这些工具进行开发。 1. **接口与实现** Java集合框架主要包括`Collection`、`List`、`Set`和`Map`四大接口。`Collection`是最...