type
status
date
slug
summary
tags
category
icon
password
Property
在集合类中,
List
是最基础的一种集合:它是一种有序列表。List
的行为和数组几乎完全相同:List
内部按照放入元素的先后顺序存放,每个元素都可以通过索引确定自己的位置,List
的索引和数组一样,从0
开始。数组和
List
类似,也是有序结构,如果使用数组,在添加和删除元素的时候,会非常不方便。例如,从一个已有的数组{'A', 'B', 'C', 'D', 'E'}
中删除索引为2
的元素:这个“删除”操作实际上是把
'C'
后面的元素依次往前挪一个位置,而“添加”操作实际上是把指定位置以后的元素都依次向后挪一个位置,腾出来的位置给新加的元素。这两种操作,用数组实现非常麻烦。因此,在实际应用中,需要增删元素的有序列表,使用最多的是
ArrayList
。实际上,ArrayList
在内部使用了数组来存储所有元素。例如,一个ArrayList
拥有5个元素,实际数组大小为6
(即有一个空位):当添加一个元素并指定索引到
ArrayList
时,ArrayList
自动移动需要移动的元素:然后,往内部指定索引的数组位置添加一个元素,然后把
size
加1
:继续添加元素,但是数组已满,没有空闲位置的时候,
ArrayList
先创建一个更大的新数组,然后把旧数组的所有元素复制到新数组,紧接着用新数组取代旧数组:现在,新数组就有了空位,可以继续添加一个元素到数组末尾,同时
size
加1
:可见,
ArrayList
把添加和删除的操作封装起来,操作List
类似于操作数组,却不用关心内部元素如何移动。考察
List<E>
接口,可以看到几个主要的接口方法:- 在末尾添加一个元素:
boolean add(E e)
- 在指定索引添加一个元素:
boolean add(int index, E e)
- 删除指定索引的元素:
E remove(int index)
- 删除某个元素:
boolean remove(Object e)
- 获取指定索引的元素:
E get(int index)
- 获取链表大小(包含元素的个数):
int size()
但是,实现
List
接口并非只能通过数组(即ArrayList
的实现方式)来实现,另一种LinkedList
通过“链表”也实现了List接口。在LinkedList
中,它的内部每个元素都指向下一个元素:比较
ArrayList
和LinkedList
:通常情况下优先使用
ArrayList
。List的特点
使用
List
时,要关注List
接口的规范。List
接口允许添加重复的元素,即List
内部的元素可以重复:List
还允许添加null
:创建List
除了使用
ArrayList
和LinkedList
,还可以通过List
接口提供的of()
方法,根据给定元素快速创建List
:但是
List.of()
方法不接受null
值,如果传入null
,会抛出NullPointerException
异常。遍历List
和数组类型,我们要遍历一个
List
,完全可以用for
循环根据索引配合get(int)
方法遍历:但这种方式并不推荐,一是代码复杂,二是因为
get(int)
方法只有ArrayList
的实现是高效的,换成LinkedList
后,索引越大,访问速度越慢。所以要始终坚持使用迭代器
Iterator
来访问List
。Iterator
本身也是一个对象,但它是由List
的实例调用iterator()
方法的时候创建的。Iterator
对象知道如何遍历一个List
,并且不同的List
类型,返回的Iterator
对象实现也是不同的,但总是具有最高的访问效率。Iterator
对象有两个方法:boolean hasNext()
判断是否有下一个元素,E next()
返回下一个元素。因此,使用Iterator
遍历List
代码如下:通过
Iterator
遍历List
永远是最高效的方式。并且,由于Iterator
遍历是如此常用,所以,Java的for each
循环本身就可以帮我们使用Iterator
遍历:上述代码就是我们编写遍历
List
的常见代码。实际上,只要实现了
Iterable
接口的集合类都可以直接用for each
循环来遍历,Java编译器本身并不知道如何遍历集合对象,但它会自动把for each
循环变成Iterator
的调用,原因就在于Iterable
接口定义了一个Iterator<E> iterator()
方法,强迫集合类必须返回一个Iterator
实例。List和Array转换
把
List
变为Array
有三种方法,第一种是调用toArray()
方法直接返回一个Object[]
数组:这种方法会丢失类型信息,所以实际应用很少。
第二种方式是给
toArray(T[])
传入一个类型相同的Array
,List
内部自动把元素复制到传入的Array
中:注意到这个
toArray(T[])
方法的泛型参数<T>
并不是List
接口定义的泛型参数<E>
,所以,我们实际上可以传入其他类型的数组,例如我们传入Number
类型的数组,返回的仍然是Number
类型:但是,如果我们传入类型不匹配的数组,例如,
String[]
类型的数组,由于List
的元素是Integer
,所以无法放入String
数组,这个方法会抛出ArrayStoreException
。如果我们传入的数组大小和
List
实际的元素个数不一致怎么办?根据List接口的文档,我们可以知道:如果传入的数组不够大,那么
List
内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比List
元素还要多,那么填充完元素后,剩下的数组元素一律填充null
。实际上,最常用的是传入一个“恰好”大小的数组:
最后一种更简洁的写法是通过
List
接口定义的T[] toArray(IntFunction<T[]> generator)
方法:反过来,把
Array
变为List
就简单多了,通过List.of(T...)
方法最简单:对于JDK 11之前的版本,可以使用
Arrays.asList(T...)
方法把数组转换成List
。要注意的是,返回的
List
不一定就是ArrayList
或者LinkedList
,因为List
只是一个接口,如果我们调用List.of()
,它返回的是一个只读List
:对只读
List
调用add()
、remove()
方法会抛出UnsupportedOperationException
。equals
我们知道
List
是一种有序链表:List
内部按照放入元素的先后顺序存放,并且每个元素都可以通过索引确定自己的位置。List
还提供了boolean contains(Object o)
方法来判断List
是否包含某个指定元素。此外,int indexOf(Object o)
方法可以返回某个元素的索引,如果元素不存在,就返回-1
。我们来看一个例子:
我们往
List
中添加的"C"
和调用contains("C")
传入的"C"
是不是同一个实例?如果这两个
"C"
不是同一个实例,这段代码是否还能得到正确的结果?我们可以改写一下代码测试一下:因为我们传入的是
new String("C")
,所以一定是不同的实例。结果仍然符合预期,这是为什么呢?因为
List
内部并不是通过==
判断两个元素是否相等,而是使用equals()
方法判断两个元素是否相等,例如contains()
方法可以实现如下:因此,要正确使用
List
的contains()
、indexOf()
这些方法,放入的实例必须正确覆写equals()
方法,否则,放进去的实例,查找不到。我们之所以能正常放入String
、Integer
这些对象,是因为Java标准库定义的这些类已经正确实现了equals()
方法。以
Person
对象为例:不出意外,虽然放入了
new Person("Bob")
,但是用另一个new Person("Bob")
查询不到,原因就是Person
类没有覆写equals()
方法。编写equals
如何正确编写
equals()
方法?equals()
方法要求我们必须满足以下条件:- 自反性(Reflexive):对于非
null
的x
来说,x.equals(x)
必须返回true
;
- 对称性(Symmetric):对于非
null
的x
和y
来说,如果x.equals(y)
为true
,则y.equals(x)
也必须为true
;
- 传递性(Transitive):对于非
null
的x
、y
和z
来说,如果x.equals(y)
为true
,y.equals(z)
也为true
,那么x.equals(z)
也必须为true
;
- 一致性(Consistent):对于非
null
的x
和y
来说,只要x
和y
状态不变,则x.equals(y)
总是一致地返回true
或者false
;
- 对
null
的比较:即x.equals(null)
永远返回false
。
上述规则看上去似乎非常复杂,但其实代码实现
equals()
方法是很简单的,以Person
类为例:首先,我们要定义“相等”的逻辑含义。对于
Person
类,如果name
相等,并且age
相等,我们就认为两个Person
实例相等。因此,编写
equals()
方法如下:对于引用字段比较,我们使用
equals()
,对于基本类型字段的比较,我们使用==
。如果
this.name
为null
,那么equals()
方法会报错,因此,需要继续改写如下:如果
Person
有好几个引用类型的字段,上面的写法就太复杂了。要简化引用类型的比较,我们使用Objects.equals()
静态方法:因此,我们总结一下
equals()
方法的正确编写方法:- 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
- 用
instanceof
判断传入的待比较的Object
是不是当前类型,如果是,继续比较,否则,返回false
;
- 对引用类型用
Objects.equals()
比较,对基本类型直接用==
比较。
使用
Objects.equals()
比较两个引用类型是否相等的目的是省去了判断null
的麻烦。两个引用类型都是null
时它们也是相等的。如果不调用
List
的contains()
、indexOf()
这些方法,那么放入的元素就不需要实现equals()
方法。