これは、Java がプロジェクトに List を使用できるシナリオの概要です。

この記事は、Nuggets コミュニティによって署名された最初の記事です. 14 日以内の転載は禁止されています. 14 日を過ぎた無断転載は禁止されています. 侵害について調査する必要があります!

リストは要素のセットを順番に表します. 順序とは, 要素が順番にトラバースされることを意味します. 最初にリストに入れられた要素が最初にトラバースされます. これは配列に非常に似ていますが, 配列とは異なり, リストはサイズ制限なしです。

List は Java でプログラムを作成する際に最も頻繁に使用されるデータ構造です. 今日, この記事では, List を使用する主な方法をよく見ていきます. 後半では, いくつかの便利な関数も紹介します.開発効率化の事例として、記事全体の概要は次のとおりです。

リスト インターフェイス

List インターフェースの完全な名前は java.util.List で、そこで定義されているメソッドを次の図に示します。画像.pngこれは、クラスがこれらのメソッドを実装している限り、List インターフェースの実装クラスであることを意味します。

Java リストに含まれる要素は、Java リスト内に表示される順序に従って、挿入、アクセス、反復、および削除できます。要素の順序が、このデータ構造がリストと呼ばれる理由です。Java リストの各要素にはインデックスがあり、リストの最初の要素のインデックスは 0、2 番目の要素のインデックスは 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 の動的展開と検索の実装は非常にわかりにくく、まったくパフォーマンスがありません。幸いなことに、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 の 2 種類のデータ構造があり、次に 2 種類の紹介があります。

実際、これらの構造と Stream 操作、Stream と Lambda は密接に組み合わされており、一緒に使用されることがよくありますが、心配する必要はありません。後でそれらを 1 つずつ整理して接続します。

おすすめ

転載: juejin.im/post/7147624805313806343