容器(二):Collection三足鼎立之List
标签: Java编程思想
容器类中Collection接口和Map接口双雄并起,在Collection家族中也呈现着三足鼎立的局面:List接口、Set接口、Queue接口。
在 《容器(一):容器双雄之Collection》中介绍了Collection的相关知识,这篇笔记来学习 Collection 的子接口之一 List 接口
。
List接口
一个List是一个元素有序的、可以重复、可以为null的集合(有时候我们也叫它“序列”)。集合中的每个元素都有其对应的顺序索引。List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
为什么 List 中的元素 “有序”、“可以重复”呢?
首先,List 的数据结构就是一个序列,存储内容时直接在内存中开辟一块连续的空间,然后将空间地址与索引对应。
在JDK API中:
有序的 collection(也称为序列)。此接口的用户可以对列表中每个元素的插入位置进行精确地控制。用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。
可以看出,List 接口的实现类在实现插入元素时,都会根据索引进行排列。比如:ArrayList是实现了基于动态数组的数据结构,LinkedList基于双链表的数据结构。
由于 List 的元素在存储时互不干扰,没有什么依赖关系,自然可以重复(这点与 Set 有很大区别)。
List接口的方法
与Collection接口类似:List接口中的方法并没有实现,而是交给其子接口去具体实现
List 接口中除了继承 Collection 接口中的方法以外,还提供以下类型的操作:
位置相关: List 和 数组一样,都是从 0 开始,我们可以根据元素在 list 中的位置进行操作,比如说 get, set, add, addAll, remove;
搜索相关: 从 list 中查找某个对象的位置,比如 indexOf, lastIndexOf;
特殊迭代: 使用 Iterator 的拓展版迭代器 ListIterator 进行迭代操作;
范围操作: 使用 subList 方法对 list 进行任意范围的操作。
下面我们逐一的研究一下List特有的这些方法:
测试代码
public static void testList1() {
List list = new ArrayList();
list.add(123);
list.add(456);
list.add(new String("AA"));
list.add(new String("BB"));
System.out.println(list); // [123, 456, AA, BB]
list.add(0,555);
System.out.println(list); // [555, 123, 456, AA, BB]
Object obj = list.get(1);
System.out.println(obj); // 123
list.remove(0);
System.out.println(list.get(0)); // 123
list.set(0,111);
System.out.println(list.get(0)); // 111
list.add(111);
System.out.println(list.indexOf(111)); // 0
System.out.println(list.lastIndexOf(111)); // 4
List list1 = list.subList(0,3);
System.out.println(list1); // [111, 456, AA]
}
位置相关
List接口提供了一些与位置相关的方法:比如在指定位置添加元素、添加其他容器、用来获取和更改的get() set()方法,以及remove方法等。
void add(int index,E element)
:在列表的指定位置插入指定元素(可选操作)。将当前处于该位置的元素(如果有的话)和所有后续元素向右移动(在其索引中加 1)。
参数:
index - 要在其中插入指定元素处的索引
element - 要插入的元素boolean addAll(int index,Collection<? extends E> c)
:将指定 collection中的所有元素都插入到列表中的指定位置(可选操作)。将当前处于该位置的元素(如果有的话)和所有后续元素向右移动(增加其索引)。
参数:
index - 将指定 collection 的第一个元素所插入位置的索引
c - 包含要添加到此列表的元素的 collection
返回:
如果此列表由于调用而发生更改,则返回 trueE get(int index)
:返回列表中指定位置的元素。
参数:
index - 要返回的元素的索引
返回:
列表中指定位置的元素E set(int index,E element)
:用指定元素替换列表中指定位置的元素(可选操作)。
参数:
index - 要替换的元素的索引
element - 要在指定位置存储的元素
返回:
以前在指定位置的被覆盖的元素E remove(int index)
:移除列表中指定位置的元素(可选操作)。将所有的后续元素向左移动(将其索引减 1)。返回从列表中移除的元素。
参数:
index - 要移除的元素的索引
返回:
以前在指定位置的被移除的元素boolean removeAll(Collection<?> c)
:从列表中移除指定 collection 中包含的其所有元素(可选操作)。
指定者:
接口 Collection 中的 removeAll
参数:
c - 包含从此列表中移除的元素的 collection
返回:
如果此列表由于调用而发生更改,则返回 true
下面是一个简单的 List 中的元素交换方法:
public static <E> void swap(List<E> a, int i, int j) {
E tmp = a.get(i);
a.set(i, a.get(j));
a.set(j, tmp);
}
搜索相关
提供了搜索某一元素的方法,并返回该元素的索引。
int indexOf(Object o)
:返回此列表中第一次出现的指定元素的索引;如果此列表不包含该元素,则返回 -1。更确切地讲,返回满足 (o==null ? get(i)==null : o.equals(get(i))) 的最低索引 i;如果没有这样的索引,则返回 -1。
参数:
o - 要搜索的元素
返回:
此列表中第一次出现的指定元素的索引,如果列表不包含该元素,则返回 -1int lastIndexOf(Objecto)
:返回此列表中最后出现的指定元素的索引;如果列表不包含此元素,则返回 -1。更确切地讲,返回满足 (o==null ? get(i)==null : o.equals(get(i))) 的最高索引 i;如果没有这样的索引,则返回 -1。
参数:
o - 要搜索的元素
返回:
列表中最后出现的指定元素的索引;如果列表不包含此元素,则返回 -1
特殊迭代
关于ListIterator要另写一篇笔记
ListIterator<E> listIterator()
:返回此列表元素的列表迭代器(按适当顺序)。
返回:
此列表元素的列表迭代器(按适当顺序)ListIterator<E> listIterator(int index)
:返回列表中元素的列表迭代器(按适当顺序),从列表的指定位置开始。指定的索引表示 next 的初始调用所返回的第一个元素。previous 方法的初始调用将返回索引比指定索引少 1 的元素。
参数:
index - 从列表迭代器返回的第一个元素的索引(通过调用 next 方法)
返回:
此列表中元素的列表迭代器(按适当顺序),从列表中的指定位置开始范围操作
List.subList(int fromIndex, int toIndex)
方法返回 List 在 fromIndex 与 toIndex 范围内的子集。注意是左闭右开,[fromIndex,toIndex)。
List<E> subList(int fromIndex,int toIndex)
:
返回列表中指定的 fromIndex(包括 )和 toIndex(不包括)之间的部分。(如果 fromIndex 和 toIndex 相等,则返回的列表为空)。返回的列表由此列表支持,因此返回列表中的非结构性更改将反映在此列表中,反之亦然。返回的列表支持此列表支持的所有可选列表操作。
参数:
fromIndex - subList 的低端(包括)
toIndex - subList 的高端(不包括)
返回:
列表中指定范围的视图
注意:
List.subList 方法并没有像我们想的那样:创建一个新的 List,然后把旧 List 的指定范围子元素拷贝进新 List。
真相是: subList 返回的仍是 List 原来的引用,只不过把开始位置 offset 和 size 改了下,
见 List.subList() 在 AbstractList 抽象类中的实现:
public List<E> subList(int start, int end) {
if (start >= 0 && end <= size()) {
if (start <= end) {
if (this instanceof RandomAccess) {
return new SubAbstractListRandomAccess<E>(this, start, end);
}
return new SubAbstractList<E>(this, start, end);
}
throw new IllegalArgumentException();
}
throw new IndexOutOfBoundsException();
}
SubAbstractListRandomAccess 最终也是继承 SubAbstractList,直接看 SubAbstractList:
SubAbstractList(AbstractList<E> list, int start, int end) {
fullList = list;
modCount = fullList.modCount;
offset = start;
size = end - start;
}
可以看到,的确是保持原来的引用。
所以,重点来了!
由于 subList 持有 List 同一个引用,所以对 subList 进行的操作也会影响到原有 List,举个栗子:
List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
List subList = list.subList(0,2).clear();
Sysout.out,println(list);
运行结果是:
[c,d]
注意这行代码:List.subList(fromIndex, toIndex).clear();
一句话实现了删除 List 中 subList 部分元素的操作。
还可以查找某元素在局部范围内的位置:
int i = list.subList(fromIndex, toIndex).indexOf(o);
int j = list.subList(fromIndex, toIndex).lastIndexOf(o);
List 与 Array 区别
List 在很多方面跟 Array 数组感觉很相似,尤其是 ArrayList,那 List 和数组究竟哪个更好呢?
相似之处:
- 都可以表示一组同类型的对象
- 都使用下标进行索引
不同之处:
- 数组可以存任何类型元素
- List 不可以存基本数据类型,必须要包装
- 数组容量固定不可改变;List 容量可动态增长
- 数组效率高; List 由于要维护额外内容,效率相对低一些
- 容量固定时优先使用数组,容纳类型更多,更高效。
在容量不确定的情景下, List 更有优势,ArrayList 和 LinkedList 中都实现了容量动态增长,在专门的笔记中记录
List 与 Array 之间的转换
在 List 中有两个转换成数组的方法:
Object[] toArray()
返回一个包含 List 中所有元素的数组;T[] toArray(T[] array)
作用同上,不同的是当 参数 array 的长度比 List 的元素大时,会使用参数 array 保存 List 中的元素;否则会创建一个新的 数组存放 List 中的所有元素;
数组工具类 Arrays 提供了数组转成 List 的方法 asList :
@SafeVarargs
public static <T> List<T> asList(T... array) {
return new ArrayList<T>(array);
}
使用的是 Arrays 内部创建的 ArrayList 的转换构造函数:
private final E[] a;
ArrayList(E[] storage) {
if (storage == null) {
throw new NullPointerException("storage == null");
}
//直接复制
a = storage;
}
ps:用心学习,喜欢的话请点赞 (在左侧哦)