集合-List列表
列表List
List接口在Collection的基础上添加了许多方法,允许在list中间插入和删除元素。
List主要有两种类型的具体实现:
- ArrayList:底层数组实现,动态调整集合的大小,擅长随机访问,但在ArrayList中插入和删除元素速度较慢。
- LinkedList:底层链表实现,擅长插入和删除,对于随机访问来说相对较慢。
List
下面以一个例子来介绍List接口中方法的使用:首先定义一个外部类Phone,提供一个静态方法list返回一组Phone集合。
class Phone {
String name;
static List<Phone> list() {
// Exception in thread "main" java.lang.UnsupportedOperationException
//return Arrays.asList(new HuaWei(), new Nova(), new P40(), new Honor(), new IPhone(), new OnePlus());
return new ArrayList<>(Arrays.asList(new HuaWei(), new Nova(), new P40(), new Honor(), new IPhone(), new OnePlus()));
}
@Override
public String toString() {
return name;
}
}
class HuaWei extends Phone{public HuaWei() {super.name = "HuaWei";}}
class Nova extends Phone{public Nova() {super.name = "Nova";}}
class P40 extends Phone{public P40() {super.name = "P40";}}
class Honor extends Phone{public Honor() {super.name = "Honor";}}
class Honor20 extends Phone{public Honor20() {super.name = "Honor20";}}
class IPhone extends Phone{public IPhone() {super.name = "IPhone";}}
class OnePlus extends Phone{public OnePlus() {super.name = "OnePlus";}}
public class ListTest {
public static void main(String[] args) {
List<Phone> phones = Phone.list();
System.out.println(phones);
//=================[1]====================
Honor20 honor20 = new Honor20();
phones.add(honor20); // Automatically resizes
System.out.println("insert honor20->" + phones);
System.out.println("contains honor20->" + phones.contains(honor20));
phones.remove(honor20); // Remove by Object
phones.remove(1); // Remove by index
System.out.println("remove honor20 and object in 1 index->" + phones);
/*
输出:
[HuaWei, Nova, P40, Honor, IPhone, OnePlus]
insert honor20->[HuaWei, Nova, P40, Honor, IPhone, OnePlus, Honor20]
contains honor20->true
remove honor20 and object in 1 index->[HuaWei, P40, Honor, IPhone, OnePlus]
*/
//=================[2]====================
Phone p = phones.get(0);
System.out.println(p + " index:" + phones.indexOf(p));
HuaWei huaWei = new HuaWei();
// 因为集合中存有一个HuaWei对象
// 在没有将新对象huaWei加入到集合中之前,删除这个新对象,查看是否会影响集合中的HuaWei对象
System.out.println(phones.indexOf(huaWei));
System.out.println(phones.remove(huaWei));
// 删除集合中的HuaWei对象
System.out.println(phones.remove(p));
System.out.println(phones);
phones.add(0, new HuaWei()); // 在指定索引处插入对象
System.out.println(phones);
/*
输出:
HuaWei index:0
-1
false
true
[P40, Honor, IPhone, OnePlus]
[HuaWei, P40, Honor, IPhone, OnePlus]
*/
//=================[3]====================
List<Phone> sub = phones.subList(1, 4);// 求子集范围[1,4),4是开区间
System.out.println("subList: " + sub);
System.out.println("before shuffled containsAll->" + phones.containsAll(sub));
Collections.shuffle(phones); // 打乱集合
System.out.println("shuffled subList: " + sub);
System.out.println("after shuffled containsAll->" + phones.containsAll(phones)); //集合元素的顺序不影响containsAll的结果
ArrayList<Phone> copy = new ArrayList<>(phones);//[3.1]
sub = Arrays.asList(phones.get(1), phones.get(4));//[3.2]
System.out.println("copy: " + copy + " sub: " + sub);
copy.retainAll(sub); //求交集
System.out.println("retainAll(求交集之后)的copy: " + copy);
/*
输出:
subList: [P40, Honor, IPhone]
before shuffled containsAll->true
shuffled subList: [OnePlus, Honor, HuaWei]
after shuffled containsAll->true
copy: [IPhone, OnePlus, Honor, HuaWei, P40] sub: [OnePlus, P40]
retainAll(求交集之后)的copy: [OnePlus, P40]
*/
//=================[4]====================
copy = new ArrayList<>(phones);
copy.removeAll(sub);
System.out.println(copy);
copy.set(1, new Honor()); // replace an element
copy.addAll(2, sub); // 在指定索引处插入集合
System.out.println("before clear phones is empty:" + phones.isEmpty());
phones.clear();
System.out.println("clear phones->" + phones);
System.out.println("after clear phones is empty:" + phones.isEmpty());
phones.addAll(Phone.list());
Object[] objects = phones.toArray();
System.out.println(objects[3]);
Phone[] ph = phones.toArray(new Phone[0]);
System.out.println(ph[3]);
/*
输出:
[IPhone, Honor, HuaWei]
before clear phones is empty:false
clear phones->[]
after clear phones is empty:true
Honor
Honor
*/
}
}
[1]:当向List的实现ArrayList集合中插入元素时,能够动态增减大小(自动扩容调整索引),contains方法判断指定的对象是否在集合内,remove是一个重载方法,可以根据对象删除,也可以根据索引删除。
[2]:如果集合中已存在一个HuaWei对象,在没有将新对象HuaWei加入到集合中之前,删除这个新对象,查看是否会影响集合中的HuaWei对象,这是不会影响原集合的,尽管在认知上认为是同一个。contains行为依赖于equals方法。下面会介绍依赖于equals()
的点。
[3]:subList()
方法可以轻松地从更大的列表中创建切片,注意这里不包括边界,当将切片结果传递给原来这个较大的列表的 containsAll()
方法时,很自然地会得到 true。请注意,顺序并不重要,在 sub 上调用直观命名的 Collections.sort()
和 Collections.shuffle()
方法,不会影响 containsAll()
的结果。 subList()
所产生的列表的幕后支持就是原始列表,sub只持有原始列表的部分引用。
retainAll()
方法实际上是一个“集合交集”操作,在本例中,它保留了同时在 copy 和 sub 中的所有元素。请再次注意,所产生的结果行为依赖于 equals()
方法。
[3.1]、[3.2]处的代码,展示了集合是“持有对象引用”的,集合对象变了,但是集合中数据元素的对象引用并没有发生变化,copy、sub集合里面的对象引用和phone中的对象引用是相同的。
[4]:removeAll()
方法也是基于 equals()
方法运行的。 顾名思义,它会从 List 中删除在参数 List 中的所有元素。可以通过set()
方法替换指定索引处的元素值,clear()
用于清空集合中的元素(清空了集合中持有的对象引用),isEmpty()
判断集合中是否含有对象引用(元素)。对于 List ,有一个重载的 addAll()
方法可以将新列表插入到原始列表的中间位置,而不是仅能用 Collection 的 addAll()
方法将其追加到列表的末尾。
toArray()
方法将任意的 Collection 转换为数组。这是一个重载方法,其无参版本返回一个 Object 数组,但是如果将目标类型的数组传递给这个重载版本,那么它会生成一个指定类型的数组(假设它通过了类型检查)。如果参数数组太小而无法容纳 List 中的所有元素(就像本例一样),则 toArray()
会创建一个具有合适尺寸的新数组
依赖于equals方法?持有对象引用?集合常见误区
1、依赖于equals
方法
当确定元素是否是属于某个 List ,寻找某个元素的索引,以及通过引用从 List 中删除元素时,都会用到 equals()
方法(根类 Object 的一个方法),如List.indexOf(Object obj)、List.contains(Object obj)、List.containsAll(List lists)、List.remove(Object obj)、List.removeAll(List lists)、List.retainAll(List lists)
。上面的HuaWei的例子也可以说明,新生成的HuaWei对象,当调用contains()
方法时,怎么知道集合是否包含HuaWei对象,底层就是通过调用对象的equals()
判断是否包含,因为类中都没有重写equals()
方法,所以默认调用的是父类中的equals()
(判断地址),所以当对新实例HuaWei调用indexOf时,就会返回 -1 (表示未找到),或者调用remove就会返回false。如果我们重写了 equals()
,那么结果就会有所不同。
对于其他类,
equals()
的定义可能有所不同。例如,如果两个 String 的内容相同,则这两个 String 相等。因此,为了防止出现意外,请务必注意 List 行为会根据equals()
行为而发生变化。
@Test
public void testList() {
List<String> strings = new ArrayList<>(Arrays.asList("Long", "Abc", "Qwe"));
strings.add("Long");
System.out.println(Arrays.toString(strings.toArray()));
System.out.println(strings.remove("Long"));
//System.out.println(strings.remove(new String("Long"))); 等同于上面
System.out.println(Arrays.toString(strings.toArray()));
System.out.println(strings.remove("Long"));
System.out.println(Arrays.toString(strings.toArray()));
/*
输出:
[Long, Abc, Qwe, Long]
true
[Abc, Qwe, Long]
true
[Abc, Qwe]
*/
}
从上面结果我们就可以看出,一String类重写了equals方法,所以remove方法看的效果和之前是不一样的;二因为集合中有两个与"Long"相等的数据元素,默认是从第一个开始处理的,不仅仅是对于remove
还有contains
等等都会处理第一个出现的元素。
2、持有对象引用
public static void main(String[] args) {
// 持有对象引用思想
List<Phone> phones = Phone.list();
System.out.println(phones);
// copy集合也保存了phones集合中的所持有的对象引用,
// 注意仅仅是保存了一组地址值在集合中,并不是保存了数据对象。
// 注意理解 对象引用的概念。
ArrayList<Phone> copy = new ArrayList<>(phones);
copy.clear();
System.out.println("copy->" + copy);
// copy仅仅是清空了集合中保存的地址值,并没有销毁对象,只是不在持有对象引用
// phones并没有清空引用值,所以说phone还是保留着手机对象的引用值。
System.out.println("phones->" + phones);
// 还是对象引用的概念,这里不再阐述。
Phone phone = phones.get(0);
System.out.println(phone.name);
copy = new ArrayList<>(phones);
Phone copyPhone = copy.get(0);
copyPhone.name = "Nokia";
System.out.println(phone.name);
}
3、集合常见误区?
Phone类中存在一个静态方法
static List<Phone> list() {
// Exception in thread "main" java.lang.UnsupportedOperationException
//return Arrays.asList(new HuaWei(), new Nova(), new P40(), new Honor(), new IPhone(), new OnePlus());[1]
return new ArrayList<>(Arrays.asList(new HuaWei(), new Nova(), new P40(), new Honor(), new IPhone(), new OnePlus()));
}
当使用[1]构造的集合列表,若之后对该集合列表进行add或者remove操作就会引发java.lang.UnsupportedOperationException
,这是为什么呢?来看一下Arrays.asList(T... a)
底层源码:
@SafeVarargs
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
方法返回的ArrayList,ArrayList集合不可以动态扩容吗?这就很奇怪了,当仔细观察,发现ArrayList并不是java.util.ArrayList
,而是java.util.Arrays.ArrayList
,属于Arrays
的一个私有内部类,继承了AbstractList
并重写了一些方法,add和remove方法并没有重写,那么默认会调用父类AbstractList
的方法,AbstractList
抽象类的add和remove的方法体就是抛出异常,所以说这就是为什么对Arrays.asList(T... a)
的结果进行写操作时会引发异常。另一方面来说,java.util.Arrays.ArrayList
底层是数组来存储值的,由于add和remove这两个方法会尝试修改数组大小,所以会在运行时得到“Unsupported Operation(不支持的操作)”错误:
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
LinkedList(链表)
数组和数组列表都有一个重大的缺陷,当从数组的中间位置删除一个元素要付出很大的代价,因为数组中处于被删除元素之后的所有元素都要向数组的前端移动,如果数据量大的话,这是十分耗时的。Java中的链表解决了这个问题,链表将对象存放在独立的结点中,每个结点保留着下一个结点的引用。
LinkedList底层结构就是链表,它实现了基本的List接口,它在List中间执行插入和删除时比ArrayList更高效,但随机访问操作效率不及ArrayList。
在Java中,所有链表实际上都是双向链表(doubly linked)—每个结点还存放着指向前驱结点的引用。
LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque) 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 Queue 中)。例如:
getFirst()
和element()
是相同的,element()
底层就是调用的getFirst()
,它们都返回列表的头部(第一个元素)而并不删除它,如果 List 为空,则抛出 NoSuchElementException 异常。peek()
方法与这两个方法只是稍有差异,它在列表为空时返回 null 。removeFirst()
和remove()
也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 NoSuchElementException 异常。poll()
稍有差异,它在列表为空时返回 null 。addFirst()
在列表的开头插入一个元素。offer()
与add()
和addLast()
相同。 它们都在列表的尾部(末尾)添加一个元素。removeLast()
删除并返回列表的最后一个元素。
示例:
public class LinkedListTest {
public static void main(String[] args) {
LinkedList<Phone> phones = new LinkedList<>(Phone.list());
System.out.println(phones);
// 获取第一个元素,不同点是对empty-list的行为不同
System.out.println("getFirst:" + phones.getFirst());
System.out.println("element:" + phones.element());
System.out.println("peek:" + phones.peek());
// 删除并返回删除的元素
System.out.println("phones.remove():" + phones.remove());// 底层通过removeFirst删除
System.out.println("phones.removeFirst():" + phones.removeFirst());
System.out.println("phones.poll():" + phones.poll());
System.out.println(phones);
// 在列表头插入一个元素
phones.addFirst(Phone.get());
System.out.println("After addFirst():" + phones);
// 在列表尾插入元素 offer add addLast
phones.offer(Phone.get());
System.out.println("After offer():" + phones);
phones.add(Phone.get());
System.out.println("After add():" + phones);
phones.addLast(new Honor20());
System.out.println("After addLast(): " + phones);
/*
输出:
[HuaWei, Nova, P40, Honor, IPhone, OnePlus]
getFirst:HuaWei
element:HuaWei
peek:HuaWei
phones.remove():HuaWei
phones.removeFirst():Nova
phones.poll():P40
[Honor, IPhone, OnePlus]
After addFirst():[OnePlus, Honor, IPhone, OnePlus]
After offer():[OnePlus, Honor, IPhone, OnePlus, IPhone]
After add():[OnePlus, Honor, IPhone, OnePlus, IPhone, IPhone]
After addLast(): [OnePlus, Honor, IPhone, OnePlus, IPhone, IPhone, Honor20]
*/
}
}
Iterator
迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。另外迭代器通常称为轻量级对象(lightweight object):创建它的代价小。Java的Iterator只能单向移动。
Iterator接口源码:
public interface Iterator<E> {
/*检查序列中是否还有元素*/
boolean hasNext();
/*获得序列中的下一个元素*/
E next();
/*将迭代器最近返回的那个元素删除*/
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
Iterator的简单使用
示例一:
public class IteratorTest {
public static void main(String[] args) {
// Iterator遍历元素
List<Phone> phones = Phone.list();
Iterator<Phone> it = phones.iterator();
while (it.hasNext()) {
Phone p = it.next();
System.out.print(p + " ");
}
System.out.println();
// for-each增强for循环,Collection接口扩展了Iterable接口,
// 对于任何实现了Collection接口的类都使用for-each循环
for (Phone p : phones) {
System.out.print(p + " ");
}
System.out.println();
// 利用Iterator删除元素
it = phones.iterator();
for (int i = 0; i < 3; i++) {
it.next();
it.remove();
}
System.out.println(phones);
/*
输出
HuaWei Nova P40 Honor IPhone OnePlus
HuaWei Nova P40 Honor IPhone OnePlus
[Honor, IPhone, OnePlus]
*/
}
}
根据示例一,可得知,有了Iterator,遍历元素时,我们不在关心集合的数量,会由hasNext()
和next()
帮我们处理。Iterator可以删除next()
生成的最后一个元素,需要注意,必须在next之后调用remove()
,至于为什么,下面会介绍。
示例二:
public class IteratorTestTwo {
public static void display(Iterator<Phone> it) {
while(it.hasNext()) {
Phone p = it.next();
System.out.print(p + " ");
}
System.out.println();
}
// 更通用的方法
public static void display(Iterable<Phone> iterable) {
iterable.forEach(System.out::print);
}
public static void main(String[] args) {
List<Phone> phones = Phone.list();
LinkedList<Phone> phonesLL = new LinkedList<>(phones);
HashSet<Phone> phonesHS = new HashSet<>(phones);
// 注意这里需要之前的Phone类实现Comparable接口,因为TreeSet需要比较然后按元素顺序排序
TreeSet<Phone> phonesTS = new TreeSet<>(phones);
display(phones.iterator());
display(phonesLL.iterator());
display(phonesHS.iterator());
display(phonesTS.iterator());
/*
输出:
HuaWei Nova P40 Honor IPhone OnePlus
HuaWei Nova P40 Honor IPhone OnePlus
P40 OnePlus IPhone HuaWei Honor Nova
Honor HuaWei IPhone Nova OnePlus P40
*/
display(phones); // List间接继承了Iterable接口,对于其他集合序列也一样
}
}
示例二展示了我们无需知道具体序列的类型,Iterator将遍历序列的操作与该序列的底层结构分离,或者说迭代器统一了对集合的访问方式。另外display
是一个重载方法,形参是Iterable
类型的,Iterable
可以产生Iterator
的任何方法,并且它还有一个forEach
默认方法。使用它对集合的访问显得更简单,可以直接通过display(phones)
就可以访问,因为集合都实现了Collection
,而它又扩展了Iterable
,间接继承。
Collection
类扩展了Iterable
接口,而Iterable
接口提供了获取一个Iterator
对象的方法,所以对于任何集合,都可以获取它的Iterator
对象。
ListIterator
ListIterator是一个更强大的Iterator子类型,它只能由各种List类生成。Iterator 只能向前移动,而 ListIterator 可以双向移动。它可以生成迭代器在列表中指向位置的后一个和前一个元素的索引,并且支持修改集合中的元素。可以通过调用集合实现类中的 listIterator()
方法来生成指向 List 开头处的 ListIterator ,还可以通过调用 listIterator(n)
创建一个一开始就指向列表索引号为 n 的元素处的 ListIterator 。
ListIterator源码
public interface ListIterator<E> extends Iterator<E> {
// Query Operations
boolean hasNext();
E next();
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
// Modification Operations
void remove();
/*set方法用一个新元素取代调用next或previous方法返回的上一个元素*/
void set(E e);
void add(E e);
}
示例:
public static void main(String[] args) {
List<Phone> phones = Phone.list();
ListIterator<Phone> it = phones.listIterator();
while (it.hasNext()) {
System.out.println(it.next() + ",nextIndex:" + it.nextIndex() + ",previousIndex:" + it.previousIndex()+";");
}
// 从后往前遍历
System.out.print("reverse traverse->" );
while (it.hasPrevious()) {
System.out.print(it.previous() + " ");
}
System.out.println();
System.out.println(phones);
it = phones.listIterator(3);
// 获得从索引3处开始的ListIterator对象
while (it.hasNext()) {
it.next();
// get()会随机得到一个Phone对象
it.set(Phone.get());
}
// 在集合尾部添加一个元素
it.add(Phone.get());
System.out.println(phones);
/*
输出:
HuaWei,nextIndex:1,previousIndex:0;
Nova,nextIndex:2,previousIndex:1;
P40,nextIndex:3,previousIndex:2;
Honor,nextIndex:4,previousIndex:3;
IPhone,nextIndex:5,previousIndex:4;
OnePlus,nextIndex:6,previousIndex:5;
reverse traverse->OnePlus IPhone Honor P40 Nova HuaWei
[HuaWei, Nova, P40, Honor, IPhone, OnePlus]
[HuaWei, Nova, P40, OnePlus, IPhone, P40, Honor]
*/
}
ListIterator
是一个接口,ArrayList
没有实现却能返回这个接口的对象?
底层发现ArrayList
并没有直接实现ListIterator
,有点和Arrays类似,也是通过一个私有匿名内部类间接实现ListIterator
,所以说就能获得该对象。
看源码!
迭代器注意点
迭代器解析
Java迭代器的查找操作和位置变更是紧密相连的。只能顺序next()
或者反序previous()
依次遍历。不能像get(index)那样随机访问。
因此,应该讲Java迭代器认为是位于两个元素之间。当调用next或者previous,迭代器就越过下一个元素或者上一个元素,并返回刚刚越过的那个元素的引用。
Iterator
的next方法和remove方法的调用具有相互依赖性。如果调用remove之前没有调用next将是不合法的。否则就会抛出IllegalStateException
。对于previous同样道理。
List<String> strings = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
ListIterator<String> it = strings.listIterator(3);
while (it.hasPrevious()) {
it.hasPrevious();
it.remove();
// 不可再次调用,只消耗刚刚返回的那个元素
// it.remove();
}
这样做有什么好处,我所理解到的是避免了一定的死循环,比如
List<String> strings = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
ListIterator<String> it = strings.listIterator();
// [1]没有死循环
while (it.hasNext()) {
it.next();
it.add("11");
}
System.out.println(Arrays.toString(strings.toArray()));
// 输出:[aaa, 11, bbb, 11, ccc, 11]
// [2]下面就会出现死循环
while (it.hasPrevious()) {
it.previous();
it.add("22");
}
System.out.println(Arrays.toString(strings.toArray()));
// 死循环
[1]如果it指针指向了当前索引而不是当前元素和下一个元素的中间位置,那么上面就会造成死循环。因为遍历的总是插入的前一个元素
[2]为什么会死循环?看源码给予了解答:
/*
*Inserts the specified element into the list (optional operation).
* The element is inserted immediately before the element that
* would be returned by {@link #next}, if any, and after the element
* that would be returned by {@link #previous}, if any
*/
插入元素在调用next()
方法返回的元素之前(如果有的话),或者调用previous()
在返回的元素之后插入(如果有的话)。当调用previous()
后,然后add()
总是会在光标(指针所在位置)之后插入,所以就会导致插入的元素总是在光标之后,从而导致了死循环。下图解释了这一现象:
多个迭代器修改访问异常
List<String> strings = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
ListIterator<String> it1 = strings.listIterator();
ListIterator<String> it2 = strings.listIterator();
it1.next();
it1.remove();
it2.next();
// Exception in thread "main" java.util.ConcurrentModificationException
1)如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状态。如上,如果一个迭代器指向另一个迭代器刚刚删除的元素前,现在这个迭代器就是无效的,并且不应该在使用。否则抛出ConcurrentModificationException
。
2)在迭代元素的时候不能通过集合的方法删除元素, 否则会抛出ConcurrentModificationException 异常. 但是可以通过Iterator接口中的remove()方法进行删除.
fail-fast&fail-safe
快速失败机制和安全失败机制的区别?
Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。