List
# List
在集合类中,List
是最基础的一种集合,可以理解为一种有序的线性表。
在数据结构里我们学过线性表,如果我们要往数组里添加或删除某个元素,需要挪动其他位置的元素;如果空间不够了,还要扩容。List 已经自动帮我们实现了这些,我们直接用就可以了,不用关心其内部怎么实现。
List 只是一个接口,常见的实现类有 ArrayList
(用的最多,也推荐用)和 LinkedList
,LinkedList
就相当于链表,两者的区别跟 数组和链表的区别一样。
List
接口允许我们添加重复的元素,还运行添加 null。
# List 常用方法
- 在末尾添加一个元素:
boolean add(E e)
- 在指定索引添加一个元素:
boolean add(int index, E e)
- 删除指定索引的元素:
E remove(int index)
- 删除某个元素:
boolean remove(Object e)
- 获取指定索引的元素:
E get(int index)
- 获取链表大小(包含元素的个数):
int size()
- 是否含有某个元素:
boolean contains(Object e)
- 返回某个元素的索引:
int indexOf(Object o)
。如果元素不存在,就返回-1
。
List<String> myWaves = new ArrayList<>();
myWaves.add("爱莉希亚");
myWaves.add("布洛尼娅");
myWaves.add(0,"梅比乌斯");
String ss = myWaves.get(0);
System.out.println(ss);
System.out.println("myWaves.size(): " + myWaves.size());
2
3
4
5
6
7
运行结果:
梅比乌斯
myWaves.size(): 3
2
# of
方法
自 Java9 之后,可以通过 List
接口提供的 of()
方法,根据给定元素快速创建 List
:
List<Integer> list = List.of(1, 2, 5);
该方法最多支持 10 个元素。注意事项:
- 返回的只是一个只读
List
,调用add()
、remove()
方法会抛出UnsupportedOperationException
。 List.of()
方法不接受null
值,如果传入null
,会抛出NullPointerException
异常。
如果使用 Java8,可以使用 Arrays.asList 方法来快速创建:
List<Integer> list = Arrays.asList(1, 2, 3);
Arrays.asList()
方法是 Arrays 的静态方法。这种方式构造的 List 是固定长度的,如果调用 add 方法增加新的元素时会报异常 java.lang.UnsupportedOperationException。这种方式仅适用于构造静态不变的 List。
# 遍历 List
遍历可以说是集合类最常用的方法了。我们可以用之前学过的遍历数组的方法来遍历:
for (int i = 0; i < myWaves.size(); i++)
System.out.println(myWaves.get(i));
2
但这种方式并不推荐,一是代码复杂,二是因为 get(int)
方法只有 ArrayList
的实现是高效的,换成 LinkedList
后,索引越大,访问速度越慢。
所以我们要始终坚持使用迭代器 Iterator
来访问 List
。Iterator
本身也是一个对象,但它是由 List
的实例调用 iterator()
方法的时候创建的。Iterator
对象知道如何遍历一个 List
,并且不同的 List
类型,返回的 Iterator
对象实现也是不同的,但总是具有最高的访问效率。
Iterator
对象有两个方法:boolean hasNext()
判断是否有下一个元素,E next()
返回下一个元素。因此,使用 Iterator
遍历 List
代码如下:
for(Iterator<String> it = myWaves.iterator(); it.hasNext(); ){
System.out.println(it.next());
}
2
3
有童鞋可能觉得使用 Iterator
访问 List
的代码比使用索引更复杂。但是,要记住,通过 Iterator
遍历 List
永远是最高效的方式。并且,由于 Iterator
遍历是如此常用,所以,Java 的 for each
循环本身就可以帮我们使用 Iterator
遍历。把上面的代码再改写如下:
for (String s : myWaves) {
System.out.println(s);
}
2
3
实际上,只要实现了 Iterable
接口的集合类都可以直接用 for each
循环来遍历,Java 编译器本身并不知道如何遍历集合对象,但它会自动把 for each
循环变成 Iterator
的调用,原因就在于 Iterable
接口定义了一个 Iterator<E> iterator()
方法,强迫集合类必须返回一个 Iterator
实例。
# List 和 Array 转换
把 List
变为 Array
有三种方法,
- 调用
Object[] toArray()
方法直接返回一个数组:Object[] array = list.toArray();
。但这样会丢失类型信息,较少用。 - 给
toArray()
传入一个类型相同的 Array:Integer[] array = list.toArray(new Integer[3]);
如果我们传入的数组大小和 List
实际的元素个数不一致怎么办?根据 List 接口 (opens new window)的文档,我们可以知道:
如果传入的数组不够大,那么 List
内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比 List
元素还要多,那么填充完元素后,剩下的数组元素一律填充 null
。
实际上,最常用的是传入一个“恰好”大小的数组:
Integer[] array = list.toArray(new Integer[list.size()]);
把 Array
变为 List
就简单多了,通过 List.of(T...)
方法最简单:
Integer[] array = { 1, 2, 3 };
List<Integer> list = List.of(array);
2
对于 JDK 11 之前的版本,可以使用 Arrays.asList(T...)
方法把数组转换成 List
。
# equals 方法
前面我们简单演示了下 contains()
方法,其内部是用 equals()
方法判断两个元素是否相等(indexOf()
也是用 equals()
来查找的)。如果我们存放的类型没有实现 equals()
方法,那么 contains()
方法是无效的,我们来演示下。
首先创建一个 Person 类,里面没有实现 equals 方法:
class Person {
public String name;
public int age;
Person(String name, int age){
this.name = name;
this.age = age;
}
}
2
3
4
5
6
7
8
9
然后测试 contains()
:
List<Person> myWaves = new ArrayList<>();
myWaves.add(new Person("雷姆", 18));
myWaves.add(new Person("拉姆", 18));
myWaves.add(new Person("艾米莉雅", 18));
boolean b = myWaves.contains(new Person("雷姆", 18));
System.out.println(b); //false
2
3
4
5
6
7
可以看到,虽然我们创建了一样的 Person 类(name 和 age 相同),但是 contains()
返回的是 false
因此,要正确使用 List
的 contains()
、indexOf()
这些方法,放入的实例必须正确覆写 equals()
方法,否则,放进去的实例,查找不到
至于实例是否 equals 是否相同,则是我们自己定义的逻辑。我们可以认为名字相同既是同一人,也可以认为名字和年龄都一样才是同一人。我们完善 Person 类如下:
class Person {
public String name;
public int age;
Person(String name, int age){
this.name = name;
this.age = age;
}
public boolean equals(Object o){
if(o instanceof Person){
Person p = (Person) o;
return this.name.equals(p.name) && this.age == p.age;
}
return false;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
此时我们再使用 contains()
方法,就可以判断是否存在某个实例了。
不过,此时的方法还不完善:
- 如果 this.name 为 null,会报错空指针异常
- 如果有多个引用类型,那么判空的代码就更多了,可以使用
Objects.equals()
静态方法。