Java 基础之 List 深度探秘
List 是什么?
List 集合是 Java 集合框架中的一种有序、可重复的数据结构,它继承自 Collection 接口,允许存储多个元素。与数组不同,List 集合的大小是动态可变的,可以根据需要动态地添加或删除元素。
List 集合中的元素允许重复,各元素的顺序是对象插入的顺序,用户可通过使用索引(元素在集合中的位置)来访问集合中的元素。Java 集合就像一个容器,可以存储任何类型的数据,也可以结合泛型来存储具体的类型对象。在程序运行时,Java 集合可以动态的进行扩展,随着元素的增加而扩大。在 Java 中,集合类通常存在于 java.util 包中。Java 集合主要由 2 大体系构成,分别是 Collection 体系和 Map 体系,其中 Collection 和 Map 分别是 2 大体系中的顶层接口。Collection 主要有三个子接口,分别为 List(列表)、Set(集)、Queue(队列)。其中,List、Queue 中的元素有序可重复,而 Set 中的元素无序不可重复。List 中主要有 ArrayList、LinkedList 两个实现类;Set 中则是有 HashSet 实现类;而 Queue 是在 JDK1.5 后才出现的新集合,主要以数组和链表两种形式存在。Map 同属于 java.util 包中,是集合的一部分,但与 Collection 是相互独立的,没有任何关系。Map 中都是以 key-value 的形式存在,其中 key 必须唯一,主要有 HashMap、HashTable、treeMap 三个实现类。
在 Collection 中,List 集合是有序的,可对其中每个元素的插入位置进行精确地控制,可以通过索引来访问元素,遍历元素。在 List 集合中,我们常用到 ArrayList 和 LinkedList 这两个类。
- ArrayList 集合
-
- ArrayList 底层通过数组实现,随着元素的增加而动态扩容。ArrayList 是 Java 集合框架中使用最多的一个类,是一个数组队列,线程不安全集合。它继承于 AbstractList,实现了 List, RandomAccess, Cloneable, Serializable 接口。
-
- ArrayList 实现 List,得到了 List 集合框架基础功能;实现 RandomAccess,获得了快速随机访问存储元素的功能,RandomAccess 是一个标记接口,没有任何方法;实现 Cloneable,得到了 clone() 方法,可以实现克隆功能;实现 Serializable,表示可以被序列化,通过序列化去传输,典型的应用就是 hessian 协议。
-
- ArrayList 集合的特点:容量不固定,随着容量的增加而动态扩容(阈值基本不会达到);有序集合(插入的顺序 == 输出的顺序);插入的元素可以为 null;增删改查效率更高(相对于 LinkedList 来说);线程不安全。
-
- ArrayList 的底层数据结构:数组。
- LinkedList 集合
-
- LinkedList 底层通过链表来实现,随着元素的增加不断向链表的后端增加节点。LinkedList 是一个双向链表,每一个节点都拥有指向前后节点的引用。相比于 ArrayList 来说,LinkedList 的随机访问效率更低。
-
- 它继承 AbstractSequentialList,实现了 List, Deque, Cloneable, Serializable 接口。实现 List,得到了 List 集合框架基础功能;实现 Deque,Deque 是一个双向队列,也就是既可以先入先出,又可以先入后出,说简单点就是既可以在头部添加元素,也可以在尾部添加元素;实现 Cloneable,得到了 clone() 方法,可以实现克隆功能;实现 Serializable,表示可以被序列化,通过序列化去传输,典型的应用就是 hessian 协议。
-
- LinkedList 集合的底层数据结构:链表。
二、常见的 List 实现类
1. ArrayList
- 基于动态数组实现,支持快速随机访问,适用于读取操作频繁的场景。
ArrayList 是 Java 集合框架中的一个类,它实现了 List 接口,底层基于数组实现。数据在内存中是连续存储的,因此支持随机访问非常快速。在列表末尾添加/删除元素非常快,但在列表中间插入/删除元素可能需要移动元素,较慢。每次自动增长时,数组大小增加 50%。
2. LinkedList
- 基于双向链表实现,支持高效的插入和删除操作,适用于频繁插入、删除元素的场景。
LinkedList 也是 Java 集合框架中的一个类,同样实现了 List 接口,但底层基于链表实现。每个元素都包含指向前后元素的指针,插入和删除操作非常高效。但随机访问性能较差,需要从头或尾开始遍历链表。提供了额外的方法和接口,如 Deque,可以作为队列、双端队列或栈使用。
3. Vector
- 类似 ArrayList,但是线程安全,性能相对较低,一般不推荐使用。
Vector 的底层与 ArrayList 类似,都是以动态数组的方式进行对象的存储。但 Vector 是线程同步操作安全的,很多对外的方法都用 Synchronized 关键字进行修饰,所以通过 vector 进行操作性能并不高。每次自动增长时,数组大小增加一倍,或者增加指定的大小。在工作中,如果需要将集合(ArrayList 与 LinkedList)转换为线程安全,可以用到 Collection 工具类。但有更好的实现线程安全的替代方法,不建议使用 Vector。
三、List 的常见用法
1. 创建和基本操作
- 通过示例展示如何创建一个 ArrayList,以及进行添加、获取、删除元素等基本操作。
在 Java 中,创建一个 ArrayList 可以按照以下步骤进行:
- 导入包:import java.util.ArrayList;。
- 创建引用类型的变量:数据类型<集合存储的数据类型> 变量名 = new 数据类型<集合存储的数据类型>();。例如,创建一个存储整数的 ArrayList:ArrayList<Integer> arr = new ArrayList<Integer>();。注意,集合存储的数据类型必须是引用类型,不能是基本数据类型,如果要存储整数,可以使用Integer而不是int。
- 使用变量名.方法进行操作。
添加元素:可以使用add(参数)方法向集合中添加元素。例如,arr.add(1);arr.add(2);arr.add(3);。还可以使用add(int 索引,存储的元素)将元素添加到指定的索引上。比如,arr.add(1, 10);会将数字 10 添加到索引为 1 的位置。
获取元素:使用get(int index)方法取出集合中的元素。参数为索引,例如System.out.println(arr.get(0));会输出集合中索引为 0 的元素。
删除元素:有多种方法可以删除元素。
- remove(int 索引)可以删除指定索引上的元素。例如,arr.remove(1);会删除索引为 1 的元素。
- remove(Object element)可以删除集合中第一次出现的指定元素。例如,boolean s1 = arr.remove("a");如果集合中有字符串"a",则会删除它并返回true。
集合的遍历:可以使用多种方式遍历 ArrayList。
- for循环:for(int i=0;i<arr.size();i++){System.out.println(arr.get(i));}。
- 加强for循环(也称为foreach循环):for(Integer element : arr){System.out.println(element);}。
此外,ArrayList 还有一些补充方法:
- set(int 索引,修改后的元素)可以将指定索引的元素进行修改,并返回被覆盖的元素。
- clear()可以清空集合中的所有元素。
四、List 集合的高级操作
1. 迭代操作
- 介绍使用迭代器(Iterator)和增强 for 循环对 List 进行迭代的方法。
在 Java 中,对 List 集合进行迭代有多种方法,其中包括使用迭代器(Iterator)和增强 for 循环。
使用迭代器(Iterator)进行迭代时,可以通过调用 List 的 iterator()方法获取一个迭代器。迭代器提供了一种统一的方式来遍历集合,它具有 hasNext()方法用来检测集合中是否还有元素,以及 next()方法用于获取下一个元素。例如:
Iterator i = list.iterator();
while (i.hasNext()) {
Object obj = i.next();
if (obj.equals(2)) {
i.remove();
}
}
System.out.println(list);
增强 for 循环(也称为 foreach 循环)会自动迭代集合中的每个元素,使代码更加简洁。但是增强 for 循环不允许遍历时对集合进行操作,如果进行操作(如删除),会抛出异常 ConcurrentModificationException。操作一次后必须 break 结束循环,否则会抛出异常。例如:
for (Object obj : list) {
if (obj.equals(2)) {
list.remove(obj);
break;
}
}
System.out.println(list);
此外,还可以使用普通的 for 循环来遍历 List 集合,通过索引访问每个元素。但在使用 for 循环遍历删除元素后,集合长度发生了变化,i 一直向后递增,需要注意处理。例如:
for(int i =0; i < list.size(); i++){
list.remove(i);
}
System.out.println(list);
2. 列表操作
- 演示如何进行添加、删除、替换元素等列表操作。
在 Java 中,对 List 集合可以进行多种列表操作。
添加元素:可以使用add(参数)方法向集合中添加元素。例如,list.add(1);list.add(2);list.add(3);。还可以使用add(int 索引,存储的元素)将元素添加到指定的索引上。比如,list.add(1, 10);会将数字 10 添加到索引为 1 的位置。
删除元素:有多种方法可以删除元素。
- remove(int 索引)可以删除指定索引上的元素。例如,list.remove(1);会删除索引为 1 的元素。
- remove(Object element)可以删除集合中第一次出现的指定元素。例如,boolean s1 = list.remove("a");如果集合中有字符串"a",则会删除它并返回true。
替换元素:在集合中想使用某个元素替换另外的一个元素时,可能会出现一些常见错误。正常的替换(当替换的元素的索引小于需要替换的索引的时候)应该替换的位置,出现了不正常的替换(替换的元素的索引大于需要替换的索引),出现的结果就是使得替换的是替换的下一个索引的位置的元素,使得替换发生错误。还有可能出现越界异常(替换的索引到达集合的最大索引,当 remove 需要替换的元素的时候使得集合的长度 -1,当再次使用这个索引的时候会出现越界异常)。解决办法是在替换之前赋值,将替换的元素赋值到一个变量中,然后删除需要删除的元素,最后用这个变量替换到原来需要替换的位置。例如:
public void copy(){
System.out.println("被淘汰的为"+minfitnesspos+" 被复制的染色体为"+maxfitnesspos);
List list = community.get(maxfitnesspos);
community.remove(minfitnesspos);
community.add(minfitnesspos,list);
for (int i =0; i < chromosomesize; i++) {
chromosomepos.get(minfitnesspos)[i] = chromosomepos.get(maxfitnesspos)[i];
}
3. 使用 Collections 工具类排序
- 展示如何使用 Collections 工具类对 List 进行排序。
Java 集合的工具类 Collections 中提供了两种对 List 集合进行排序的方法。
第一种称为自然排序,参与排序的对象需实现 comparable 接口,重写其 compareTo()方法,方法体中实现对象的比较大小规则。例如:
package test;
public class Emp implements Comparable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Emp() {
super();
}
public Emp(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Emp [name=" + name + ", age=" + age + "]";
}
@Override
public int compareTo(Object o) {
if (o instanceof Emp) {
Emp emp = (Emp) o;
// return this.age-emp.getAge();//按照年龄升序排序
return this.name.compareTo(emp.getName());//换姓名升序排序
}
throw new ClassCastException("不能转换为 Emp 类型的对象...");
}
}
测试类中:
package test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.BeforeClass;
import org.junit.Test;
public class TestSort {
static List list = new ArrayList();
@BeforeClass
public static void init(){
list.add(new Emp("tom",18));
list.add(new Emp("jack",20));
list.add(new Emp("rose",15));
list.add(new Emp("jerry",17));
System.out.println("排序前:");
for(Object o : list){
System.out.println(o);
}
}
@Test
public void testSortName(){
Collections.sort(list);
System.out.println("自然排序按 name 升序排序后:");
for(Object o : list){
System.out.println(o);
}
}
}
第二种叫定制排序,或自定义排序,需编写匿名内部类,先 new 一个 Comparator 接口的比较器对象 c,同时实现 compare()其方法;然后将比较器对象 c 传给 Collections.sort()方法的参数列表中,实现排序功能。例如:
@Test
public void testComparatorSortAge(){
Collections.sort(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof Emp && o2 instanceof Emp) {
Emp e1 = (Emp) o1;
Emp e2 = (Emp) o2;
return e1.getAge() - e2.getAge();
}
throw new ClassCastException("不能转换为 Emp 类型");
}
});
System.out.println("使用 Comparator 比较器按 age 升序排序后:");
for(Object o : list){
System.out.println(o);
}
}
此外,还可以使用 Collections 工具类对包含基本数据类型的 List 进行排序。例如:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SortList {
public static void sort1(){
List list = new ArrayList();
list.add(1);
list.add(3);
list.add(5);
list.add(3);
list.add(61);
list.add(11);
list.add(1);
list.add(2);
System.out.println(list);
Collections.sort(list);
System.out.println(list);
}
}
五、List 集合的高级特性
1. 同步性
- 讲解在多线程环境中使用 Collections.synchronizedList 方法创建同步 List 的方法。
在 Java 多线程环境中,为了确保 List 集合的线程安全,可以使用Collections.synchronizedList方法来创建同步的 List。语法如下:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SynchronizedListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("IND");
list.add("AUS");
list.add("WI");
list.add("NZ");
list.add("ENG");
List<String> synlist = Collections.synchronizedList(list);
synchronized(synlist) {
Iterator<String> itr = synlist.iterator();
while(itr.hasNext()) {
String str = itr.next();
System.out.println(str);
}
}
}
}
使用Collections.synchronizedList方法时,为了保证串行访问,对后备列表的所有访问都必须通过返回的同步列表来完成。在迭代返回的列表时,用户必须手动同步它,因为在执行add()等方法的时候是加了synchronized关键字的,但是iterator()却没有加。
2. 使用 ListIterator
- 介绍 ListIterator 的强大功能,如在迭代过程中进行元素的修改、添加、删除等操作。
ListIterator是一个功能更强大的迭代器,它继承自Iterator接口,只能用于各种List类型的访问。ListIterator不仅可以像Iterator一样向后遍历,还可以向前遍历,并且可以在遍历的同时修改List的元素、获取遍历时游标的索引。
ListIterator常用的方法有:
- hasNext():判断是否有下一个元素。
- next():返回下一个元素并移动游标。
- remove():删除当前游标指向的元素。
- hasPrevious():判断是否有上一个元素。
- previous():返回上一个元素并移动游标。
- add(E e):往集合中添加指定的元素。
- set(E e):修改集合中指定元素的值。
例如:
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class ListIteratorTest {
public static void main(String[] args) {
ArrayList<String> list_test = new ArrayList<>();
list_test.add("aaa");
list_test.add("bbb");
list_test.add("ccc");
System.out.println("Before iterate : " + list_test);
ListIterator<String> it = list_test.listIterator();
while (it.hasNext()) {
System.out.println(it.next() + ", " + it.previousIndex() + ", " + it.nextIndex());
}
while (it.hasPrevious()) {
System.out.print(it.previous() + " ");
}
System.out.println();
it = list_test.listIterator(1);
while (it.hasNext()) {
String t = it.next();
System.out.println("it.next:" + t);
if ("ccc".equals(t)) {
it.set("nnn");
} else {
it.add("kkk");
}
}
System.out.println("After iterate : " + list_test);
}
}
3. 使用 subList 方法
- 讲解如何使用 List 的 subList 方法截取原列表的一部分,形成一个新的子列表。
List的subList方法可以截取原列表的一部分,形成一个新的子列表。语法如下:
public List<E> subList(int fromIndex, int toIndex);
该方法返回包含从索引fromIndex(包括)到索引toIndex(不包括)元素的List集合。
例如:
import java.util.ArrayList;
import java.util.List;
public class SubListTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
List<Integer> subList = list.subList(3, 8);
System.out.println("subList: " + subList);
}
}
在新的子列表中添加或删除元素时,原列表也会发生相应改变。但是如果在原列表中删除或添加元素,调用原列表中的方法没问题,当调用subList方法生成的集合的方法时就会产生异常。
如果要生成一个独立的子列表,可以使用硬复制的方式,而不是直接使用subList方法。例如:
import java.util.ArrayList;
public class SubListTest {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
ArrayList<String> arrayList_sublist = new ArrayList<>();
for(int i = 1; i <= 2; i++){
arrayList_sublist.add(arrayList.get(i));
}
System.out.println(arrayList_sublist);
}
}