Java 做项目能用到 List 的场景,这篇总结全了

本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

List 代表有顺序的一组元素,顺序代表遍历元素时是有顺序的,先放进 List 的元素会先被遍历到,这点很像数组,但是跟数组很不一样的是 List 对大小没有限制。

List 是使用 Java 编写程序的时候,最高频使用的数据结构,今天这篇文章我们就来好好地把List主要的使用方法过一遍,在后半部分还会给出多个特遍有用的能提高我们开发效率的实践示例,整个文章的大纲如下:

List 接口

List 接口的全名叫 java.util.List,其中定义的方法如下图所示: image.png 这就意味着,只要类实现了这些方法,那么它就是一个 List 接口的实现类。

Java List 中包含的元素可以根据它们在 Java List 内部出现的顺序进行插入、访问、迭代和删除。元素的顺序是这个数据结构被称为 List 的原因。 Java List 中的每个元素都有一个索引,列表中的第一个元素的索引为 0,第二个元素的索引为 1,以此类推。索引的意思是“距离列表开头有多少个元素”。

下面我们用数组作为底层存储的结构,自己实现一个List类。

package com.example.learncollection;

import java.util.*;

public class MyArrayList implements List {

    private Object[] elements;

    private int curr;
    // 先给数组分配16个长度
    public MyArrayList() {
        elements = new Object[16];
        curr = 0;
    }

    @Override
    public int size() {
        return curr;
    }

    @Override
    public boolean isEmpty() {
        return curr == 0;
    }

    @Override
    public boolean contains(Object o) {

        for (Object ele : elements) {
            if (Objects.equals(ele, o)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public void clear() {
        curr = 0;
    }

    @Override
    public Object get(int index) {
        if (index > curr || index < 0) {
            throw new IndexOutOfBoundsException("out of bound " + curr + " for " + index);
        }
        return elements[index];
    }

    @Override
    public boolean add(Object o) {
        if (curr == elements.length - 1) {
            // 数组满了,扩容一倍,把数据拷贝到新数组里。
            Object[] temp = new Object[elements.length * 2];
            System.arraycopy(elements, 0, temp, 0, elements.length);
            elements = temp;
        }
        elements[curr] = o;
        curr++;
        return true;
    }

    @Override
    public Iterator iterator() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object[] toArray() {
        throw new UnsupportedOperationException();
    }

    ...... // 其他方法都抛出 UnsupportedOperationException 异常实现,这里省略。
}

复制代码

上面只实现了几个基础的方法,其他操作的方法都按抛出 UnsupportedOperationException 异常来实现,在例程里省略掉了。

package com.example.learncollection;

import java.util.List;

public class UseListAppMain {

    public static void main(String[] args) {
		List myList = new MyArrayList();
        for (int i = 0; i < 10; i++) {
            myList.add("str" + (i % 5));
        }
        
        System.out.println();
        System.out.println("输出" + myList.getClass() + "中的元素,共" + myList.size() + "个");
        
        for (List element : myList) {
            System.out.println(element);
        }
    }
}

复制代码

Java 提供的List实现类

上面解释 List 接口的时候,尝试自己写了一个实现类,能观察到需要实现的方法有很多,且 List 的动态扩容和查找实现的都很晦涩,也毫无性能可言。好在 Java 里给我们提供了很多完备的 List 实现类,我们直接用就行了,不必自己编写类实现 List 接口。

可以在 Java Collections API 中的以下 List 实现之间进行选择:

  • java.util.ArrayList java
  • util.LinkedList
  • java.util.Vector
  • java.util.Stack

在这些实现中,ArrayList 是最常用的。 java.util.concurrent 包中还有并发 List 的实现。这部分内容等到并发相关的章节再详细解释。

创建列表实例

可以通过实例化实现了List接口的类,创建一个列表实例。

List listA = new ArrayList();
List listB = new LinkedList();
List listC = new Vector();
List listD = new Stack();
复制代码

大多数情况下我们都会使用 ArrayList 类,但在某些情况下,使用其他实现之一可能更有意义。

默认情况下,可以往 List 中放入任何对象。但是从 Java 5 开始,使用 Java 泛型可以限制插入到列表中的对象类型。下面是一个例子:

List<MyObject> list = new ArrayList<ObjectType>();
复制代码

现在这个列表只允许插入 MyObject 类的实例,这样一来访问和迭代列表元素的时候就不需要再对其进行强制类型转换了。

List<MyObject> list = new ArrayList<MyObject>();

list.add(new MyObject("First MyObject"));

MyObject myObject = list.get(0);

for(MyObject obj : list){
   ......
}
复制代码

如果不使用泛型对列表元素类型进行限制,上面这个例子会变成

List list = new ArrayList();   // 没有泛型类型约束,列表元素默认是Object类型的

list.add(new MyObject("First MyObject"));

MyObject myObject = (MyObject) list.get(0);  // 使用列表元素时需要进行类型转换

for(Object obj : list){
    // 使用前进行类型转换
    MyObject theMyObject = (MyObject) anObject;

   ......
}
复制代码

如果没有在 List 变量声明上设置泛型类型,Java 编译器只知道 List 中存放的是 Object 实例。因此,在使用时需要将它们转换为对象所属的具体类(或接口)。

为 List 变量指定泛型类型,可以帮助避免将错误类型的对象插入到列表中,使我们从 List 中检索对象时无需再做类型转换。并且 它可以帮助代码的阅读者了解 List 应该包含什么类型的对象。除非有充分的理由不对 List 使用泛型约束,否则创建 List 实例时应始终使用泛型约束。

下面通过 ArrayList 我们详细介绍一下 Java List 的使用方法。

向列表中插入元素

使用 add() 方法将元素(对象)插入到 Java 列表中。

List<String> strList = new ArrayList<>();

strList.add("element 1");
strList.add("element 2");
strList.add("element 3");
复制代码

add 方法加元素插入到列表的尾部。注意,列表是允许 null 值插入的。

Object element = null;

List<Object> list = new ArrayList<>();

list.add(element);
复制代码

add 方法还支持向指定索引位置插入元素。

strList.add(0, "element 4");
复制代码

如果 List 的索引位置上已经包含元素,原有元素将在 List 的内部序列中进一步向下推,比如这个例子里在新元素插入到索引 0 之前,原来索引为 0 的元素将被挪到索引 1 的位置上。

把一个列表的全部元素插入到另外一个列表

可以将一个 List 中的所有元素添加到另一个 List 中,使用 List 的 addAll() 方法就能执行此操作。结果列表是两个列表的并集。

List<String> listSource = new ArrayList<>();

listSource.add("123");
listSource.add("456");

List<String> listDest   = new ArrayList<>();

listDest.addAll(listSource);
复制代码

addAll() 方法的类型参数是 Collection,因此可以传递 List 或 Set 作为参数。换句话说,可以使用 addAll() 将 List 或 Set 中的所有元素添加到 List 中。

从列表中读取一个元素

可以使用元素的索引从 Java 列表中获取元素。使用 get(int index) 方法执行此操作。以下是使用元素索引访问列表元素的示例:

List<String> listA = new ArrayList<>();

listA.add("element 0");
listA.add("element 1");
listA.add("element 2");

//access via index
String element0 = listA.get(0);
String element1 = listA.get(1);
String element3 = listA.get(2);
复制代码

从列表中查找一个元素

在 List 中查找一个元素,可以使用以下两个方法

  • indexOf
  • lastIndexOf

indexOf 方法返回给定元素在 List 中第一次出现的索引

List<String> list = new ArrayList<>();

String element1 = "element 1";
String element2 = "element 2";

list.add(element1);
list.add(element2);

int index1 = list.indexOf(element1);
int index2 = list.indexOf(element2);

System.out.println("index1 = " + index1);
System.out.println("index2 = " + index2);
复制代码

上面例程的返回结果是

index1 = 0
index2 = 1
复制代码

lastIndexOf 方法返回给定元素在 List 中最后一次出现的索引。

List<String> list = new ArrayList<>();

String element1 = "element 1";
String element2 = "element 2";

list.add(element1);
list.add(element2);
list.add(element1);

int lastIndex = list.lastIndexOf(element1);
System.out.println("lastIndex = " + lastIndex);
复制代码

上面例程的返回结果是

lastIndex = 2
复制代码

如果列表中不存在给定元素,这两个方法的返回结果都是 -1

检查列表中是否存在给定元素

使用 List contains() 方法检查 Java List 是否包含给定元素。

List<String> list = new ArrayList<>();

String element1 = "element 1";

list.add(element1);

boolean containsElement = list.contains("element 1");

System.out.println(containsElement); // 输出 true
复制代码

为了确定 List 是否包含给定元素,List 将在内部迭代其元素并将每个元素与 contains 参数指定的对象进行比较。比较使用元素的 equals 方法来检查元素是否等于参数。 由于可以向 List 添加空值,因此实际上可以检查 List 是否包含空值。以下是检查 List 是否包含空值的方法:

list.add(null);

containsElement = list.contains(null);

System.out.println(containsElement);
复制代码

显然,如果 contains() 的输入参数为 null,则 contains() 方法不会使用 equals() 方法来比较每个元素,而是使用 == 运算符。

从列表中移除一个元素

可以通过以下两种方法从 Java 列表中删除元素。

  • remove(Object element)
  • remove(int index)

remove(Object element) 从列表中的删除参数 element 指定的该元素(如果存在)。删除后列表中的所有后续元素会在列表中向上移动,索引减 1。

List<String> list = new ArrayList<>();

String element = "first element";
list.add(element);

list.remove(element);
复制代码

remove(int index) 方法删除给定索引处的元素。删除后,列表中的所有后续元素会在列表中向上移动,索引减 1。

List<String> list = new ArrayList<>();

list.add("element 0");
list.add("element 1");
list.add("element 2");

list.remove(0);
复制代码

从列表中移除所有元素

clear() 方法从列表中删除所有元素

List<String> list = new ArrayList<>();

list.add("object 1");
list.add("object 2");

list.clear();
复制代码

获得两个列表的交集

List 的 reatinAll 方法可以获取两个 List 的交集。

List<String> list      = new ArrayList<>();
List<String> otherList = new ArrayList<>();

String element1 = "element 1";
String element2 = "element 2";
String element3 = "element 3";
String element4 = "element 4";

list.add(element1);
list.add(element2);
list.add(element3);

otherList.add(element1);
otherList.add(element3);
otherList.add(element4);

list.retainAll(otherList);
复制代码

上面的例程执行完后,list 列表里将只会存在,list 和 otherList 两个列表中共有的元素。即执行完后 list 中只剩下 "element1","element3" 两个元素。

返回列表的大小

通过调用 size() 方法获取 List 中的元素数量。

List<String> list = new ArrayList<>();

list.add("object 1");
list.add("object 2");

int size = list.size(); // 长度为2
复制代码

获取列表子集

List 接口有一个名为 subList() 的方法,该方法可以使用原始 List 中的元素子集创建一个新 List。 subList() 方法接受 2 个参数:开始索引和结束索引。起始索引是原始列表中要包含在子列表中的第一个元素的索引。结束索引是子列表的最后一个索引,结束索引处的元素不包含在子列表中。

类似于 Java 字符串子字符串 substring 方法的工作方式 。

List<String> list = new ArrayList<>();

list.add("element 1");
list.add("element 2");
list.add("element 3");
list.add("element 4");

List<String> sublist = list.subList(1, 3);
复制代码

上面的例程把原始列表的 索引1 和 索引2 作为子集赋值给了 sublist 。

把列表转换为Set

可以通过创建一个新的 Set 并将 List 中的所有元素添加到其中,来将 List 转换为 Set。 转换为 Set 后将删除 List 中的所有重复项。

List<String> list = new ArrayList<>();

list.add("element 1");
list.add("element 2");
list.add("element 3");
list.add("element 3");

Set<String> set = new HashSet<>();
set.addAll(list);
复制代码

把列表转换为数组

可以使用 List 的 toArray() 方法将列表转换为数组。

List<String> list = new ArrayList<>();

list.add("element 1");
list.add("element 2");
list.add("element 3");
list.add("element 3");

Object[] objects = list.toArray();
复制代码

可以将 List 转换为特定类型的数组。

List<String> list = new ArrayList<>();

list.add("element 1");
list.add("element 2");
list.add("element 3");
list.add("element 3");

String[] objects1 = list.toArray(new String[0]);
复制代码

注意,即使我们将大小为 0 的 String 数组传递给 toArray() 方法,返回的数组也将包含 List 中的所有元素,它将具有与 List 相同数量的元素。

把数组转换成List

使用 java.util.Arrays 的 asList 方法可以将 Java 数组转换为 List。

String[] values = new String[]{ "one", "two", "three" };

List<String> list = (List<String>) Arrays.asList(values);
复制代码

排序列表

排序可排序对象的列表

如果 List 中包含的是实现了 Comparable 接口 (java.lang.Comparable) 的对象,这些对象可以相互比较。在这种情况下,可以像这样对 List 进行排序:

List<String> list = new ArrayList<>();

list.add("c");
list.add("b");
list.add("a");

Collections.sort(list);
复制代码

Java 的 String 类实现了 Comparable 接口,可以使用 Collections 接口的 sort() 方法按自然顺序对它们进行排序。

使用比较器(Comparator)对列表进行排序

如果 List 中的对象没有实现 Comparable 接口,或者如果想以不同于对象的 compare() 实现的顺序对对象进行排序,那么需要使用 Comparator 实现 (java.util.Comparator)。

public class Car{
    public String brand;
    public String numberPlate;
    public int noOfDoors;

    public Car(String brand, String numberPlate, int noOfDoors) {
        this.brand = brand;
        this.numberPlate = numberPlate;
        this.noOfDoors = noOfDoors;
    }
}
复制代码

这是对上述 Car 对象的 Java 列表进行排序的代码

List<Car> list = new ArrayList<>();

list.add(new Car("Volvo V40" , "XYZ 201845", 5));
list.add(new Car("Citroen C1", "ABC 164521", 4));
list.add(new Car("Dodge Ram" , "KLM 845990", 2));

Comparator<Car> carBrandComparator = new Comparator<Car>() {
    @Override
    public int compare(Car car1, Car car2) {
        return car1.brand.compareTo(car2.brand);
    }
};

Collections.sort(list, carBrandComparator);
复制代码

上面示例中的 Comparator 实现,仅比较 Car 对象的 brand 字段。我们可以创建另一个比较器实现来比较车牌号,甚至是汽车门的数量。

迭代列表

可以通过多种不同的方式迭代 Java 列表。最常见的三种方式是:

  • 使用迭代器
  • 使用for each 循环
  • 使用Java Stream API

使用迭代器

通过调用 List 接口的 iterator() 方法获得一个 Iterator。 一旦你获得了一个迭代器,你就可以继续调用它的 hasNext() 方法,直到它返回 false。

List<String> list = new ArrayList<>();

list.add("first");
list.add("second");
list.add("third");
    
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
    String obj = iterator.next();
}
复制代码

如你所见,调用 hasNext() 是在 while 循环内完成的。 在 while 循环中,调用 Iterator 接口的 next() 方法来获取 Iterator 指向的下一个元素。

使用for each循环

迭代 List 的第二种方法是使用 Java 5 中新增的 for each循环

List<String> list = new ArrayList<String>();

list.add("first");
list.add("second");
list.add("third");

for(String element : list) {
    System.out.println(element);
}
复制代码

当然,用普通的 for 循环也是能迭代 List。

List<String> list = new ArrayList<String>();

list.add("first");
list.add("second");
list.add("third");
    
for(int i=0; i < list.size(); i++) {
    String element = list.get(i);
}
复制代码

使用Java Sream API 迭代 List

必须首先从列表中获取 Stream。在 Java 中从 List 中获取 Stream 是通过调用 List 的 stream() 方法来完成的。

List<String> stringList = new ArrayList<String>();

stringList.add("abc");
stringList.add("def");

Stream<String> stream = stringList.stream();
复制代码

从列表中获取流后,可以通过调用其 forEach() 方法来迭代流。以下是使用 forEach() 方法迭代 List 元素的示例:

List<String> stringList = new ArrayList<String>();

stringList.add("one");
stringList.add("two");
stringList.add("three");

Stream<String> stream = stringList.stream();
stream.forEach( element -> { System.out.println(element); });
复制代码

调用 forEach() 方法将使 Stream 在内部迭代 Stream 的所有元素,并为 Stream 中的每个元素调用作为参数传递给 forEach() 方法的 Consumer。

对象List的常用操作

有两个对象集合 aList 和 bList。

List<A> aList = new ArrayList<>(Arrays.asList(
    new A("1", "张三"),
    new A("2", "李四"),
    new A("3", "王五")
));

List<A> bList = new ArrayList<>(Arrays.asList(
    new A("2", "李四"),
    new A("3", "王五"),
    new A("4", "赵六")
));
复制代码

Class A 的声明如下:

// 静态内部类
static class A {
    String id;
    String nickName;

    public A(String id, String nickName) {
        this.id = id;
        this.nickName = nickName;
    }

    @Override
    public String toString() {
        return "A{" +
            "id='" + id + '\'' +
            ", nickName='" + nickName + '\'' +
            '}';
    }

    public String getId() {
        return id;
    }

    public String getNickName() {
        return nickName;
    }
}
复制代码

求两个对象List的交集

根据集合对象里的ID,求两个集合的交集

// aList 与 bList 的交集 (在两个集合中都存在的元素)
List<A> intersections = aList
    .stream() //获取第一个集合的Stream1
    .filter(  //取出Stream1中符合条件的元素组成新的Stream2,lambda表达式1返回值为true时为符合条件
        a ->  //lambda表达式1,a为lambda表达式1的参数,是Stream1中的每个元素
        bList.stream() //获取第二个集合的Stream3
        .map(A::getId) //将第二个集合每个元素的id属性取出来,映射成新的一个Stream4
        .anyMatch( //返回值(boolean):Stream4中是否至少有一个元素使lambda表达式2返回值为true
            id -> //lambda表达式2,id为lambda表达式2的参数,是Stream4中的每个元素
            Objects.equals(a.getId(), id) //判断id的值是否相等
        )
    )
    .collect(Collectors.toList()); //将Stream2转换为List
System.out.println("----------bList 与 aList 的交集为:");
System.out.println(intersections);
复制代码

求两个对象List的差集

根据集合对象里的ID,求bList 与 aList的差集

// 求bList 与 aList的差集(在bList中不在aList中)
List<A> differences = bList.stream().filter(b -> aList.stream().map(A::getId).noneMatch(id -> Objects.equals(b.getId(), id))).collect(Collectors.toList());
System.out.println("----------bList 与 aList 的差集为:");
System.out.println(differences);
复制代码

高效版求差集

上面的执行效率不高,每个bList 的元素都要在noneMatch里判断在aList里有没有跟它ID重复的对象,相当于整个筛选是O(N²)的复杂度,所以可以先把 aList 转化成以id为 key 的 Map,这样noneMatch里的操作只需要判断一次key存不存在即可,整个筛选变成了O(N)的复杂度。

Map<String, A> aMap = aList.stream().collect(Collectors.toMap(A::getId, Function.identity())) ;
List<A> diffEffective = bList.stream().filter(b -> !aMap.containsKey(b.getId())).collect(Collectors.toList());
System.out.println("----高效版------bList 与 aList 的差集为:");
System.out.println(diffEffective);
复制代码

�同理,求两个 List 的交集也能这么优化。

迭代中删除List元素

如果直接在迭代的时候删除List 元素,程序会抛出--异常 ConcurrentModificationException,那么如果想在迭代的过程中把不满足条件的元素删除,有两种方式来实现。

第一种是让程序避免在迭代中删除元素,把要删除的元素暂存在一个新List里,然后使用removeAll进行删除,避免在迭代中修改List。

假设 List 的元素是下面 Book 类的对象

static class ISBN {
    private String ISBNCode;
    public ISBN(String isbn) {
        this.ISBNCode = isbn;
    }

    public String getCode() {
        return this.ISBNCode;
    }
}

static class Book {
    private ISBN isbn;
    public Book(ISBN isbn) {
        this.isbn = isbn;
    }

    public ISBN getISBN() {
        return this.isbn;
    }
}
复制代码

那么按照第一种方法,下面例子会在迭代后删除指定 List,其实跟使用Stream API 的 filter 操作优点像,不过 Stream 还没学,先看下面这个实现。

public static void useListRemoveAll() {
    List<Book> books = new ArrayList<>();
    books.add(new Book(new ISBN("0-201-63361-2")));
    books.add(new Book(new ISBN("0-201-63361-3")));
    books.add(new Book(new ISBN("0-201-63361-4")));

    ISBN isbn = new ISBN("0-201-63361-2");
    List<Book> found = new ArrayList<>();
    for(Book book : books){
        if(book.getISBN().getCode().equals(isbn.getCode())){
            found.add(book);
        }
    }
    books.removeAll(found);
    System.out.println(books);
}
复制代码

除此之外,在 Java 8及以上的版本,还可以使用 Collection 提供的 removeIf 方法,真正实现在迭代过程中删除元素。

public static void useListRemoveIf() {
    List<Book> books = new ArrayList<>();
    books.add(new Book(new ISBN("0-201-63361-2")));
    books.add(new Book(new ISBN("0-201-63361-3")));
    books.add(new Book(new ISBN("0-201-63361-4")));
    ISBN isbn = new ISBN("0-201-63361-2");

    books.removeIf(book -> book.getISBN().getCode().equals(isbn.getCode()));

    System.out.println(books);
}
复制代码

总结

本篇文章把Java List 的各种操作和实战应用给大家梳理了一下,List 是开发中使用非常高频的结果,操作又多,所以即使完全记不住也没事,多回来参考。 集合框架中还有两类数据结构 Set 和 Map 接下来会分两种介绍。

其实这些结构和 Stream 操作,Stream 和 Lambda 结合紧密经常一起使用,不过不要着急,后面会一一给大家梳理串联起来。

来源;https://juejin.cn/post/7147624805313806343
posted @ 2022-12-24 00:58  程序员小明1024  阅读(250)  评论(0编辑  收藏  举报