Skip to content

Latest commit

 

History

History
1313 lines (940 loc) · 61.6 KB

File metadata and controls

1313 lines (940 loc) · 61.6 KB

十四、管理集合和数组

我们将在本章中讨论的类允许我们创建、初始化和修改 Java 集合和数组的对象。它们还允许创建不可修改和不可变的集合。其中一些类属于 Java 标准库,另一些属于流行的 Apache Commons 库。了解这些类并熟悉它们的方法对于任何 Java 程序员来说都是必不可少的。

我们将介绍以下功能领域:

  • 管理收藏
  • 管理阵列

概述类的列表包括:

  • java.util.Collections
  • org.apache.commons.collections4.CollectionUtils
  • java.util.Arrays
  • org.apache.commons.lang3.ArrayUtils

管理收藏

在本节中,我们将回顾如何创建和初始化集合对象,什么是不可变集合,以及如何对集合执行基本操作(例如,复制、排序和洗牌)。

初始化集合

我们已经看到了一些没有参数的集合构造函数的示例。现在,我们将看到创建和初始化集合对象的其他方法。

集合构造函数

每个集合类都有一个接受相同类型元素集合的构造函数。例如,下面是如何使用ArrayList(Collection collection)构造函数创建ArrayList类的对象,以及如何使用HashSet<Collection collection)构造函数创建HashSet类的对象:

List<String> list1 = new ArrayList<>();
list1.add("s1");
list1.add("s1");

List<String> list2 = new ArrayList<>(list1);
System.out.println(list2);      //prints: [s1, s1]

Set<String> set = new HashSet<>(list1);
System.out.println(set);        //prints: [s1]

List<String> list3 = new ArrayList<>(set);
System.out.println(list3);      //prints: [s1]

稍后我们将在使用其他对象和流小节中展示更多使用此类构造函数的示例。

实例初始值设定项(双大括号)

可以使用双大括号初始值设定项进行集合初始化。当集合是实例字段的值时,它特别适合,因此它在对象创建期间自动初始化。以下是一个例子:

public class ManageCollections {
  private List<String> list = new ArrayList<>() {
        {
            add(null);
            add("s2");
            add("s3");
        }
  };
  public List<String> getThatList(){
      return this.list;
  }
  public static void main(String... args){
    ManageCollections mc = new ManageCollections();
    System.out.println(mc.getThatList());    //prints: [null, s2, s3]
  }
}

我们添加了一个 getter,并在main()方法运行时使用它。遗憾的是,与构造函数中的传统集合初始化相比,双大括号初始值设定项不节省任何键入时间:

public class ManageCollections {
  private List<String> list = new ArrayList<>();
  public ManageCollections(){
        list.add(null);
        list.add("s2");
        list.add("s3");
  }
  public List<String> getThatList(){
      return this.list;
  }
  public static void main(String... args){
    ManageCollections mc = new ManageCollections();
    System.out.println(mc.getThatList());    //prints: [null, s2, s3]
  }
}

唯一的区别是您需要为add()方法的每次调用键入list变量。此外,双大括号初始值设定项有一个开销,即创建一个匿名类时只使用实例初始值设定项和对封闭类的引用。它还可能有更多的问题,因此应该避免。

好消息是,有一种更短、更方便的方法可以将集合初始化为字段值或局部变量值:

private List<String> list = Arrays.asList(null, "s2", "s3");

java.util.Arrays类的静态方法asList()非常流行(稍后我们将更详细地讨论Arrays类)。唯一的潜在缺点是,此类列表不允许添加元素:

List<String> list = Arrays.asList(null, "s2", "s3");
list.add("s4");    // throws UnsupportedOperationException

但是,我们始终可以通过将初始化列表传递到构造函数来创建新集合:

List<String> list = new ArrayList(Arrays.asList(null, "s2", "s3"));
list.add("s4");   //works just fine

Set<String> set = new HashSet<>(Arrays.asList(null, "s2", "s3"));
set.add("s4");   //works just fine as well

请注意,集合类的构造函数接受实现Collection接口的任何对象。它允许从集合创建列表,反之亦然。但是,Map接口没有扩展Collection,所以Map实现只允许从另一个映射创建映射:

Map<Integer, String> map = new HashMap<>();
map.put(1, null);
map.put(2, "s2");
map.put(3, "s3");

Map<Integer, String> anotherMap = new HashMap<>(map);

新映射的键和值的类型必须与所提供映射中的键和值相同,或者必须是所提供映射类型的父级:

class A{}
class B extends A{}
Map<Integer, B> mb = new HashMap<>();
Map<Integer, A> ma = new HashMap<>(mb);

例如,这是一项可接受的任务:

Map<Integer, String> map1 = new HashMap<>();
Map<Integer, Object> map2 = new HashMap<>(map1);

这是因为HashMap构造函数仅将类型限制为映射元素的子元素:

HashMap(Map<? extends K,? extends V> map)

以下代码也存在类似问题:

class A {}
class B extends A {}
List<A> l1 = Arrays.asList(new B());
List<B> l2 = Arrays.asList(new B());
//List<B> l3 = Arrays.asList(new A()); //compiler error

前面的代码有道理,不是吗?class B拥有(继承)class A的所有非私有方法和字段,但可以拥有class A中不可用的其他非私有方法和字段。即使今天这两个类都是空的,如我们的示例所示,明天我们可能会决定向class B添加一些方法。因此,编译器保护我们不受这种情况的影响,并且不允许将包含父类型元素的集合分配给子集合。这就是以下构造函数定义中泛型的含义,正如您在 Java 标准库 API 的java.util包中看到的:

ArrayList(Collection<? extends E> collection) HashSet(Collection<? extends E> collection) HashMap(Map<? extends K,? extends V> map)

我们希望到现在为止,您已经对此类泛型更加熟悉。如果有疑问,请阅读上一章中有关泛型的部分。

静态初始化块

静态字段初始化也有类似的解决方案。静态块可以包括生成静态字段初始化所需值所需的代码:

class SomeClass{
   public String getThatString(){
      return "that string";
   }
}
public class ManageCollections {
  private static Set<String> set = new HashSet<>();
   static {
        SomeClass someClass = new SomeClass();
        set.add(someClass.getThatString());
        set.add("another string");
  }
  public static void main(String... args){
    System.out.println(set); //prints: [that string, another string]
  }
}

由于set是一个静态字段,它不能在构造函数中初始化,因为只有在创建实例时才会调用构造函数,而在不创建实例的情况下可以访问静态字段。我们还可以将前面的代码重写如下:

private static Set<String> set = 
    new HashSet<>(Arrays.asList(new SomeClass().getThatString(), 
                                                "another string"));

但是,你可以说,它看起来有点尴尬和难以阅读。因此,如果静态初始化块允许编写更可读的代码,那么它可能是更好的选择。

工厂方法()

自 Java 9 以来,在每个接口中都提供了创建和初始化集合的另一个选项,包括Map-of()工厂方法。他们被称为工厂,因为他们生产物品。有十一种这样的方法,它们接受 0 到 10 个参数,每个参数都是必须添加到集合中的元素,例如:

List<String> iList0 = List.of();
List<String> iList1 = List.of("s1");
List<String> iList2 = List.of("s1", "s2");
List<String> iList3 = List.of("s1", "s2", "s3");

Set<String> iSet1 = Set.of("s1", "s2", "s3", "s4");
Set<String> iSet2 = Set.of("s1", "s2", "s3", "s4", "s5");
Set<String> iSet3 = Set.of("s1", "s2", "s3", "s4", "s5", "s6", 
                                              "s7", "s8", "s9", "s10");

Map<Integer, String> iMap = Map.of(1, "s1", 2, "s2", 3, "s3", 4, "s4");

请注意映射是如何构造的:从一对值到 10 对这样的值。

我们决定用“i”启动上述变量的标识符,以表明这些集合是不可变的。我们将在下一节讨论这一点。

这些工厂方法的另一个特点是它们不允许null作为元素值。如果添加,null元素将在运行时导致错误(NullPointerException。不允许使用null的原因是,很久以前,大多数收藏品都禁止使用null。这个问题对于Set特别重要,因为集合提供Map的键,而null键没有多大意义,是吗?例如,请查看以下代码:

Map<Integer, String> map = new HashMap<>();
map.put(null, "s1");
map.put(2, "s2");
System.out.println(map.get(null));     //prints: s1

您可能还记得,Map接口的put()方法在没有与提供的键相关联的值或者旧值为null时返回null。这种含糊不清很烦人,不是吗?

这就是 Java9 的作者决定从集合中挤出null的原因。可能总会有允许null的集合的特殊实现,但最常用的集合最终将不允许null,我们现在描述的工厂方法是朝这个方向迈出的第一步。

与这些工厂方法一起添加的另一个等待已久的特性是集合元素顺序的随机化。这意味着每次执行相同的集合创建时,顺序都不同。例如,如果我们运行以下行:

Set<String> iSet3 = Set.of("s1", "s2", "s3", "s4", "s5", "s6", 
                                       "s7", "s8", "s9", "s10");
System.out.println(iSet3);

输出可能如下所示:

但是,如果我们再次运行相同的两行,输出将不同:

每次执行集合创建都会导致其元素的顺序不同。这就是随机化在起作用。它有助于及早发现错误的程序员在顺序无法保证的地方依赖某个元素的顺序。

使用其他对象和流

构造函数小节中,我们演示了如何使用List<T> Arrays.asList(T...a)方法生成一个值列表,然后将其传递给实现Collection接口的任何类的构造函数(或扩展Collection的任何接口,例如ListSet)。提醒一下,(T...a)符号称为 varargs,表示可以通过以下两种方式传递参数:

  • 作为类型为 T 的值的无限逗号分隔序列
  • 作为任意大小的 T 型数组

因此,以下两条语句创建的列表相等:

List<String> x1 = Arrays.asList(null, "s2", "s3");
String[] array = {null, "s2", "s3"};
List<String> x2 = Arrays.asList(array);
System.out.println(x1.equals(x2));       //prints: true

另一种创建集合的方法是 Java8,它引入了流。下面是列表和集合对象生成的一个可能示例(我们将在第 18 章流和管道中进一步讨论流):

List<String> list2 = Stream.of(null, "s2", "s3")
                           .collect(Collectors.toList());
System.out.println(list2);               //prints: [null, s2, s3]

Set<String> set2 = Stream.of(null, "s2", "s3")
                         .collect(Collectors.toSet());
System.out.println(set2);               //prints: [null, s2, s3]

如果您阅读了关于Collectors.toList()Collectors.toSet()方法的文档,您会发现它说:“对于返回的列表的类型、可变性、可序列化性或线程安全性没有保证;如果需要对返回的列表进行更多控制,请使用 toCollection(Supplier)。它们指的是toCollection(Supplier<C> collectionFactory)``Collectors类的方法。

Supplier<C>表示法描述了一个函数,该函数不接受任何参数,并生成一个类型为C的值,因此得名。

在许多情况下(如果不是大多数的话),我们并不关心返回哪个类(实现ListSet)。这正是为接口编码的美妙之处。但如果我们这样做,下面是一个如何使用toCollection()方法的示例,根据前面的建议,该方法比toList()toSet()更好:

List<String> list3 = Stream.of(null, "s2", "s3")
               .collect(Collectors.toCollection(ArrayList::new));
System.out.println(list3);               //prints: [null, s2, s3]

Set<String> set3 = Stream.of(null, "s2", "s3")
                 .collect(Collectors.toCollection(HashSet::new));
System.out.println(set3);               //prints: [null, s2, s3]

如果您觉得我们创建一个集合很奇怪,那么流式传输它并再次复制同一个集合,但是请记住,在现实编程中,您可能只获得Stream对象,而我们创建流是为了让示例工作,并向您显示期望的值。

Map的情况下,以下代码在文档中也被提及为“不保证类型

Map<Integer, String> m = new HashMap<>();
m.put(1, null);
m.put(2, "s2");
Map<Integer, String> map2 = m.entrySet().stream()
  .map(e -> e.getValue() == null ? Map.entry(e.getKey(), "") : e)
  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println(map2);    //prints: {1=, 2=s2} 

请注意我们是如何处理null的,将其替换为一个空的String文本“”,以避免可怕的NullPointerException。这是一段代码,与前面的toCollection()方法类似,它使用我们选择的HashMap类实现生成一个映射:

Map<Integer, String> map3 = m.entrySet().stream()
   .map(e -> e.getValue() == null ? Map.entry(e.getKey(), "") : e)
   .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue(),
                                         (k,v) -> v, HashMap::new));
System.out.println(map3);    //prints: {1=, 2=s2}

如果所提供的示例对您来说过于复杂,那么您是正确的;即使对于有经验的程序员来说,它们也很复杂。这有两个原因:

  • 函数式编程是一种不同于 Java 在其存在的头二十年中使用的编写代码的方式
  • 它是最近才在 Java 中引入的,并没有很多实用方法围绕它构建,以使代码看起来更简单

好消息是,一段时间后,您将习惯它,流和函数式编程对您来说将开始变得简单。与传统的面向对象代码相比,您甚至更喜欢它,因为使用函数和流可以使代码更紧凑、更强大、更干净,尤其是在必须高效处理大量数据(大数据)的情况下,这似乎是当前的趋势,并延伸到遥远的未来。

我们将在第 17 章Lambda 表达式和函数编程中对此进行详细介绍;第 18 章中的**流和管道;在第 19 章反应系统

不变集合

在日常语言中,形容词不可变不可变可以互换使用。但是对于 Java 集合,可以更改不可修改的集合。这也取决于你对改变这个词的理解。这就是我们的意思。

不变与不可修改

Collections类中有八个静态方法使得集合不可修改

  • Set<T> unmodifiableSet(Set<? extends T> set)
  • List<T> unmodifiableList(List<? extends T> list)
  • Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> map)
  • Collection<T> unmodifiableCollection (Collection<? extends T> collection)
  • SortedSet<T> unmodifiableSortedSet(SortedSet<T> sortdedSet)
  • SortedMap<K,V> unmodifiableSortedMap(SortedMap<K,? extends V> sortedMap)
  • NavigableSet<T> unmodifiableNavigableSet(NavigableSet<T> navigableSet)
  • NavigableMap<K,V> unmodifiableNavigableMap(NavigableMap<K,? extends V> navigableMap)

下面是创建不可修改列表的代码示例:

List<String> list = Arrays.asList("s1", "s1");
System.out.println(list);          //prints: [s1, s1]

List<String> unmodfifiableList = Collections.unmodifiableList(list);
//unmodfifiableList.set(0, "s1"); //UnsupportedOperationException
//unmodfifiableList.add("s2");    //UnsupportedOperationException

正如您可能预期的那样,我们既不能更改元素的值,也不能将新元素添加到不可修改的列表中。然而,我们可以更改基础列表,因为我们仍然保留对它的引用。而此更改将由先前创建的不可修改列表拾取:

System.out.println(unmodfifiableList);      //prints: [s1, s1]
list.set(0, "s0");
//list.add("s2");       //UnsupportedOperationException
System.out.println(unmodfifiableList);      //prints: [s0, s1] 

如您所见,通过更改原始列表,我们已经成功地更改了前面创建的不可修改列表中元素的值。这就是这种创建不可修改集合的方法的弱点,因为它们基本上只是普通集合的包装。

工厂方法的of()集合没有这个弱点,因为它们没有不可修改集合的两步集合创建。这就是为什么无法更改由of工厂方法创建的集合。无法更改集合的组成或其任何元素。以这种方式创建的集合称为“不可变的”。这就是 Java 集合世界中不可修改的和不可变的之间的区别。

不带()方法的不可变

公平地说,即使不使用of()工厂方法,也有一些方法可以创建不可变的集合。以下是一种方法:

List<String> iList =
        Collections.unmodifiableList(new ArrayList<>() {{
            add("s1");
            add("s1");
        }});
//iList.set(0, "s0");       //UnsupportedOperationException
//iList.add("s2");          //UnsupportedOperationException
System.out.println(iList);  //prints: [s1, s1]

诀窍是不要引用用于创建不可修改集合的原始集合(值的源),因此不能使用它来更改基础源。

下面是另一种不使用of()工厂方法创建不可变集合的方法:

String[] source = {"s1", "s2"};
List<String> iList2 =
        Arrays.stream(source).collect(Collectors.toList());
System.out.println(iList2);      //prints: [s1, s2]

source[0]="s0";
System.out.println(iList2);      //prints: [s1, s2] 

看起来我们这里有source对原始值的引用。但是,流不维护值与其源之间的引用。它在处理每个值之前复制它们,从而断开该值与其源的连接。这就是为什么我们试图通过改变source数组的一个元素来改变iList2的元素没有成功。我们将在第 18 章流和管道中详细介绍流。

对不可变集合的需求源于在将集合对象作为参数传递到方法中时保护其不被修改的努力。正如我们已经提到的,这样的修改将是一个副作用,可能会引入意外和难以跟踪的缺陷。

请注意,没有参数的of()工厂方法会创建空的不可变集合。当您需要调用一个需要集合作为参数的方法,但您没有该方法的数据,也不想给该方法修改传入集合的机会时,也可能需要它们。

Collections类中还有三个常量提供不可变的空集合:

List<String> list1 = Collections.EMPTY_LIST;
//list1.add("s1");       //UnsupportedOperationException
Set<String> set1 = Collections.EMPTY_SET;
Map<Integer, String> map1 = Collections.EMPTY_MAP;

此外,Collections类中还有七个方法可以创建不可变的空集合:

List<String> list2 = Collections.emptyList();
//list2.add("s1");       //UnsupportedOperationException
Set<String> set2 = Collections.emptySet();
Map<Integer, String> map2 = Collections.emptyMap();

SortedSet<String> set3 = Collections.emptySortedSet();
Map<Integer, String> map3 = Collections.emptySortedMap();
NavigableSet<String> set4 = Collections.emptyNavigableSet();
NavigableMap<Integer, String> map4 = Collections.emptyNavigableMap();

Collections类的以下方法仅使用一个元素创建不可变集合:

  • Set<T> singleton(T object)
  • List<T> singletonList(T object)
  • Map<K,V> singletonMap(K key, V value)

您可以在以下代码段中看到它的工作原理:

List<String> singletonS1 = Collections.singletonList("s1");
System.out.println(singletonS1);
//singletonS1.add("s1");        //UnsupportedOperationException

所有这些都可以使用of()工厂方法完成。我们已经描述了它,只是为了让您对不可变集合创建可用的选项有一个完整的了解。

但是Collections类的List<T> nCopies(int n, T object)方法以比of()方法更紧凑的方式创建同一对象的n副本的不可变列表:

List<String> nList = Collections.nCopies(3, "s1");
System.out.println(nList);
//nList.add("s1");        //UnsupportedOperationException

使用of()方法的类似代码更详细:

List<String> nList = List.of("s1", "s1", "s1");

如果这对您来说还不算太糟糕,那么想象一下您需要创建一个包含 100 个相同对象的列表。

方法 add()和 put()混淆

不可变集合使用的一个方面偶尔会引起混淆。您已经从我们的示例中看到,与任何 Java 集合一样,不可变集合具有add()put()方法。编译器不会生成错误,只有 JVM 在运行时才会生成错误。因此,应该对使用不可变集合的代码进行良好测试,以避免在生产中出现此类错误。

java.util.Collections 类

java.util.Collections类的所有方法都是静态和无状态的。后者意味着它们不在任何地方维护任何状态,并且它们的结果不依赖于调用的历史记录,而只依赖于作为参数传入的值。

Collections类中有许多方法,您已经在上一节中看到了其中的一些方法。我们鼓励您查阅本课程的在线文档。在这里,为了方便您,我们对其中一些方法进行了分组,这样您可以更好地了解Collections类的方法。

复制

void copy(List<T> dest, List<T> src)方法将src列表中的元素复制到dest列表中,并保留元素顺序。如果需要将一个列表作为另一个列表的子列表,此方法非常有用:

List<String> list1 = Arrays.asList("s1","s2");
List<String> list2 = Arrays.asList("s3", "s4", "s5");
Collections.copy(list2, list1);
System.out.println(list2);    //prints: [s1, s2, "s5"]

执行此操作时,copy()方法不会消耗额外的内存—它只是在已分配的内存上复制值。这种方法有助于不接受复制相同大小列表的传统方法的情况:

List<String> list1 = Arrays.asList("s1","s2");
List<String> list2 = Arrays.asList("s3", "s4");
list2 = new ArrayList(list1);
System.out.println(list2);    //prints: [s1, s2]

此代码放弃最初分配给list2的值,并为list2分配新内存以保存list1值的副本。被放弃的值一直保存在内存中,直到垃圾收集器将其删除并允许重用内存。假设这些列表的大小很大,您可以理解使用Collections.copy(),在本例中,这会减少很多开销。这也有助于避免OutOfMemory异常。

排序与等于()

Collections类的两种静态排序方法为:

  • void sort(List<T> list)
  • void sort(List<T> list, Comparator<T> comparator)

第一个sort(List<T>)方法只接受包含实现Comparable接口的元素的列表,这需要实现compareTo(T)方法。由每个元素实现的compareTo(T)方法建立的顺序称为自然顺序

第二种sort()方法不需要列表元素来实现任何特定的接口。它使用类Comparator的传入对象,使用Comparator.compare(T o1, T o2)方法建立所需的顺序。如果列表中的元素实现了Comparable,则忽略它们的方法compareTo(T),仅通过方法Comparator.compare(T o1, T o2)建立顺序。

Comparator对象(方法compare(T o1, T o2)定义的顺序覆盖Comparable接口(方法compareTo(T)定义的自然顺序)。

例如,String类是如何实现Comparable接口的:

List<String> no = Arrays.asList("a","b", "Z", "10", "20", "1", "2");
Collections.sort(no);
System.out.println(no);     //prints: [1, 10, 2, 20, Z, a, b]

对许多人来说,10放在2前面,大写字母Z放在小写字母a前面这一事实可能看起来不太自然,但这一术语并非基于人类的感知。它基于在没有提供比较器的情况下如何对对象进行排序。在这种情况下,根据实现的方法compareTo(T)进行订购。这种实现方法可以被认为是内置在元素中。这就是为什么这种排序被称为自然

自然排序是通过接口Comparable(方法compareTo(T)的实现定义的排序。

尽管这对人类来说有些意外,compareTo(T)方法的String实现在许多排序情况下都非常有用。例如,我们可以用它来实现我们类Person中的Comparable接口:

class Person implements Comparable<Person>{
    private String firstName = "", lastName = "";
    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    @Override
    public int compareTo(Person person){
        int result = this.firstName.compareTo(person.firstName);
        if(result == 0) {
            return this.lastName.compareTo(person.lastName);
        }
        return result;
    }
}

我们先比较名字,如果他们相等,就比较姓氏。这意味着我们希望Person对象按名字排序,然后按姓氏排序。

compareTo(T)方法的String实现返回第一个(或这个)和第二个对象的排序位置之间的差异。例如,ac的排序位置之间的差异为2,下面是它们的比较结果:

System.out.println("a".compareTo("c"));   //prints: -2
System.out.println("c".compareTo("a"));   //prints: 2

这是有道理的:a放在c之前,所以从左到右计数时,它的位置更小。

但是请注意,compareTo(T)Integer实现并没有返回排序位置的差异。相反,当对象相等时返回0,当该对象小于方法参数时返回-1,否则返回1

System.out.println(Integer.valueOf(3)
                          .compareTo(Integer.valueOf(3))); //prints: 0
System.out.println(Integer.valueOf(3)
                          .compareTo(Integer.valueOf(4))); //prints: -1
System.out.println(Integer.valueOf(3)
                          .compareTo(Integer.valueOf(5))); //prints: -1
System.out.println(Integer.valueOf(5)
                          .compareTo(Integer.valueOf(4))); //prints: 1
System.out.println(Integer.valueOf(5)
                          .compareTo(Integer.valueOf(3))); //prints: 1

我们使用Comparator及其方法compare(T o1, T o2)得到了相同的结果:

Comparator<String> compStr = Comparator.naturalOrder();
System.out.println(compStr.compare("a", "c"));  //prints: -2

Comparator<Integer> compInt = Comparator.naturalOrder();
System.out.println(compInt.compare(3, 5));     //prints: -1

但是,请注意,方法Comparable.compareTo(T)Compartor.compare(T o1, T o2)的文档仅定义了以下返回:

  • 0当物体相等时
  • -1当第一个物体小于第二个物体时
  • 1当第一个物体大于第二个物体时

String的情况下,smallerbigger是根据它们的排序位置定义的,在排序列表中,较小的放在较大的前面。如您所见,API 文档并不保证为所有类型的对象返回排序位置的差异。

重要的是要确保方法equals()与方法Comparable.compareTo(T)对齐,以便对于相同的对象,方法Comparable.compareTo(T)返回 0。否则,可能会得到不可预测的排序结果。

这就是为什么我们在类Person中添加以下方法equals()

@Override
public boolean equals(Object other) {
    if (other == null) return false;
    if (this == other) return true;
    if (!(other instanceof Person)) return false;
    final Person that = (Person) other;
    return this.firstName.equals(that.getFirstName()) &&
            this.lastName.equals(that.getLastName());
}

现在方法equals()与方法compareTo(T)对齐,因此compareTo(T)用于相等的Person对象时返回 0:

Person joe1 = new Person("Joe", "Smith");
Person joe2 = new Person("Joe", "Smith");
Person bob = new Person("Bob", "Smith");

System.out.println(joe1.equals(joe2));    //prints: true
System.out.println(joe1.compareTo(joe2)); //prints: 0

System.out.println(joe1.equals(bob));     //prints: false
System.out.println(joe1.compareTo(bob));  //prints: 8
System.out.println(joe2.compareTo(bob));  //prints: 8

返回值8,因为这是按字母顺序排列的BJ位置之间的差异。

我们也在Person类中添加了以下toString()方法:

@Override
public String toString(){
    return this.firstName + " " + this.lastName;
}

这将使我们能够更好地展示排序结果,这就是我们现在要做的。以下是演示代码:

Person p1 = new Person("Zoe", "Arnold");
Person p2 = new Person("Alex", "Green");
Person p3 = new Person("Maria", "Brown");
List<Person> list7 = Arrays.asList(p1, p2, p3);
System.out.println(list7);  //[Zoe Arnold, Alex Green, Maria Brown]
Collections.sort(list7);
System.out.println(list7);  //[Alex Green, Maria Brown, Zoe Arnold]

如您所见,排序后元素的顺序(上一个示例的最后一行)与compareTo(T)方法中定义的顺序匹配。

现在,让我们创建一个比较器,对Person类的对象进行不同的排序:

class OrderByLastThenFirstName implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2){
        return (p1.getLastName() + p1.getFirstName())
                .compareTo(p2.getLastName() + p2.getFirstName());
    }
}

如您所见,前面的比较器首先基于姓氏的自然顺序,然后基于姓氏的自然顺序建立顺序。如果我们将此比较器用于相同的列表和对象,我们将得到以下结果:

Collections.sort(list7, new OrderByLastThenFirstName());
System.out.println(list7);  //[Zoe Arnold, Maria Brown, Alex Green]

正如预期的那样,compareTo(T)方法被忽略,传入的Comparator对象的顺序被强制执行。

倒转

Collections类中有三种静态反向相关方法和一种旋转相关方法:

  • void reverse(List<?> list):反转元素的当前顺序
  • void rotate(List<?> list, int distance) :通过将每个元素向右移动指定数量的位置(距离),旋转元素的顺序
  • Comparator<T> reverseOrder():返回一个比较器,该比较器创建的顺序与自然顺序相反;仅适用于实现Comparable接口的元素
  • Comparator<T> reverseOrder(Comparator<T> comparator):返回一个比较器,该比较器反转传入比较器定义的顺序

下面是演示所列方法的代码:

Person p1 = new Person("Zoe", "Arnold");
Person p2 = new Person("Alex", "Green");
Person p3 = new Person("Maria", "Brown");
List<Person> list7 = Arrays.asList(p1,p2,p3);
System.out.println(list7);  //[Zoe Arnold, Alex Green, Maria Brown]

Collections.reverse(list7);
System.out.println(list7);  //[Maria Brown, Alex Green, Zoe Arnold]

Collections.rotate(list7, 1);
System.out.println(list7);  //[Zoe Arnold, Maria Brown, Alex Green]

Collections.sort(list7, Collections.reverseOrder());
System.out.println(list7);  //[Zoe Arnold, Maria Brown, Alex Green]

Collections.sort(list7, new OrderByLastThenFirstName());
System.out.println(list7);  //[Zoe Arnold, Maria Brown, Alex Green]

Collections.sort(list7, 
         Collections.reverseOrder(new OrderByLastThenFirstName()));
System.out.println(list7);  //[Alex Green, Maria Brown, Zoe Arnold]

搜索和等于()

Collections类中有五种与静态搜索相关的方法:

  • int binarySearch(List<Comparable<T>> list, T key)
  • int binarySearch(List<T> list, T key, Comparator<T> comparator)
  • int indexOfSubList(List<?> source, List<?> target)
  • int lastIndexOfSubList(List<?> source, List<?> target)
  • int frequency(Collection<?> collection, Object object)

binarySearch()方法在提供的列表中搜索key值。需要注意的重要一点是,由于二进制搜索的性质,所提供的列表必须按照升序顺序排序。该算法将密钥与列表的中间元素进行比较;如果它们属于不相等的半个元素,则不可忽略中间的半个元素。搜索将继续,直到找到与键相等的元素,或者只剩下一个元素可供搜索,但该元素不等于键。

indexOfSubList()lastIndexOfSubList()方法返回所提供子列表在所提供列表中的位置:

List<String> list1 = List.of("s3","s5","s4","s1");
List<String> list2 = List.of("s4","s5");
int index = Collections.indexOfSubList(list1, list2);
System.out.println(index);  //prints: -1

List<String> list3 = List.of("s5","s4");
index = Collections.indexOfSubList(list1, list3);
System.out.println(index);   //prints: 1

请注意,子列表的顺序应该完全相同。否则,将找不到它。

最后一个方法frequency(Collection, Object)返回所提供对象在所提供集合中出现的次数:

List<String> list4 = List.of("s3","s4","s4","s1");
int count = Collections.frequency(list4, "s4");
System.out.println(count);         //prints: 2

如果要使用这些方法(或搜索集合的任何其他方法),并且集合包含自定义类的对象,则必须实现方法equals()。典型的搜索算法使用equals()方法识别对象。如果您的方法equals()中的【要实现的】字段不被使用,那么【要实现的】字段中的【要实现的】字段将被使用。以下是此行为的演示:

class A{}
class B extends A{}

List<A> list5 = List.of(new A(), new B());
int c = Collections.frequency(list5, new A());
System.out.println(c);         //prints: 0

A a = new A();
List<A> list6 = List.of(a, new B());
c = Collections.frequency(list6, a);
System.out.println(c);         //prints: 1

如您所见,A类的对象只有在字面上是同一个对象时才能找到。但是如果我们实现了方法equals(),那么根据我们在方法equals()实现中提出的标准,就会找到 A 类的对象:

class A{
    @Override
    public boolean equals(Object o){
        if (o == null) return false;
        return (o instanceof A);
    }
}
class B extends A{}

List<A> list5 = List.of(new A(), new B());
int c = Collections.frequency(list5, new A());
System.out.println(c);         //prints: 2

A a = new A();
List<A> list6 = List.of(a, new B());
c = Collections.frequency(list6, a);
System.out.println(c);         //prints: 2

现在,每种情况下的对象A计数为2,因为B扩展了A,因此有两种类型BA

如果我们希望准确地通过当前类名识别对象,而不考虑其父类,那么我们应该以不同的方式实现方法equals()

class A{
    @Override
    public boolean equals(Object o){
        if (o == null) return false;
        return o.getClass().equals(this.getClass());
    }
}
class B extends A{}

List<A> list5 = List.of(new A(), new B());
int c = Collections.frequency(list5, new A());
System.out.println(c);         //prints: 1

A a = new A();
List<A> list6 = List.of(a, new B());
c = Collections.frequency(list6, a);
System.out.println(c);         //prints: 1

方法getClass()返回操作符new创建对象时使用的类名。这就是为什么这两种情况下的计数现在都是1

在本章的其余部分中,我们将假设方法equals()是在集合和数组的元素中实现的。大多数情况下,我们将在示例中使用类String的对象。正如我们前面在第 9 章运算符、表达式和语句中提到的,类String有一个基于字符串文本值的equals()方法实现,而不仅仅是基于对象引用。而且,正如我们在上一小节中所解释的,类String还实现了接口Comparable,因此它提供了自然的顺序。

比较两个集合

Collections类中有一个简单的静态方法用于比较两个集合:

boolean disjoint(Collection<?> c1, Collection<?> c2):如果一个集合的元素都不等于另一个集合的元素,则返回true

正如您可能猜到的,此方法使用方法equals()来识别相等的元素。

最小和最大元素

可以使用以下Collections类方法选择所提供集合的最大最小元素:

  • T min(Collection<? extends T> collection)
  • T max(Collection<? extends T>collection)
  • T min(Collection<? extends T>collection, Comparator<T> comparator)
  • T max(Collection<? extends T>collection, Comparator<T> comparator)

前两种方法要求集合元素实现Comparable(方法compareTo(T)),而其他两种方法使用类Comparator的对象来比较元素。

最小的元素是排序列表中的第一个元素;最大的在排序列表的另一端。下面是演示代码:

Person p1 = new Person("Zoe", "Arnold");
Person p2 = new Person("Alex", "Green");
Person p3 = new Person("Maria", "Brown");
List<Person> list7 = Arrays.asList(p1,p2,p3);
System.out.println(list7);  //[Zoe Arnold, Alex Green, Maria Brown]

System.out.println(Collections.min(list7)); //prints: Alex Green
System.out.println(Collections.max(list7)); //prints: Zoe Arnold

Person min = Collections.min(list7, new OrderByLastThenFirstName());
System.out.println(min);                    //[Zoe Arnold]

Person max = Collections.max(list7, new OrderByLastThenFirstName());
System.out.println(max);                    //[Alex Green]

前两种方法使用自然排序来建立顺序,而后两种方法使用作为参数传入的比较器。

添加和替换元素

下面是类Collections中添加或替换集合中元素的三个静态方法:

  • boolean addAll(Collection<T> c, T... elements):将所有提供的元素添加到提供的集合中;如果提供的元素为Set,则只添加唯一的元素。它的执行速度明显快于相应集合类型的addAll()方法。
  • boolean replaceAll(List<T> list, T oldVal, T newVal):将所提供列表中等于oldValue的所有元素替换为newValue;当oldValuenull时,该方法将提供列表中的每个null值替换为newValue。如果至少有一个元素被替换,则返回true
  • void fill(List<T> list, T object):用提供的对象替换提供列表中的每个元素。

洗牌和交换元素

所提供列表中Collections类洗牌和交换元素的以下三种静态方法:

  • void shuffle(List<?> list):使用默认的随机性源来洗牌所提供列表中元素的位置
  • void shuffle(List<?> list, Random random):使用提供的随机性源(我们将在后面的相应章节中讨论这些源)来洗牌所提供列表中元素的位置
  • void swap(List<?> list, int i, int j):将所提供列表中i位置的元素与 j 位置的元素交换

转换为选中的集合

Collections的以下九个静态方法将提供的集合从原始类型(没有泛型的)转换为特定类型的元素。名称勾选表示转换后,将检查每个新增元素的类型:

  • Set<E> checkedSet(Set<E> s, Class<E> type)
  • List<E> checkedList(List<E> list, Class<E> type)
  • Queue<E> checkedQueue(Queue<E> queue, Class<E> type)
  • Collection<E> checkedCollection(Collection<E> collection, Class<E> type)
  • Map<K,V> checkedMap(Map<K,V> map, Class<K> keyType, Class<V> valueType)
  • SortedSet<E> checkedSortedSet(SortedSet<E> set, Class<E> type)
  • NavigableSet<E> checkedNavigableSet(NavigableSet<E> set, Class<E> type)
  • SortedMap<K,V> checkedSortedMap(SortedMap<K,V> map, Class<K> keyType, Class<V> valueType)
  • NavigableMap<K,V> checkedNavigableMap(NavigableMap<K,V> map, Class<K> keyType, Class<V> valueType)

下面是演示代码:

List list = new ArrayList();
list.add("s1");
list.add("s2");
list.add(42);
System.out.println(list);    //prints: [s1, s2, 42]

List cList = Collections.checkedList(list, String.class);
System.out.println(list);   //prints: [s1, s2, 42]

list.add(42);
System.out.println(list);   //prints: [s1, s2, 42, 42]

//cList.add(42);           //throws ClassCastException

您可以观察到转换不会影响集合的当前元素。我们已经将String类的对象和Integer类的对象添加到同一个列表中,并且能够毫无问题地将其转换为选中列表cList。我们可以继续向原始列表添加不同类型的对象,但尝试向选中列表添加非字符串对象会在运行时生成ClassCastException

转换为线程安全集合

Collections中有八个静态方法可以将常规集合转换为线程安全的集合:

  • Set<T> synchronizedSet(Set<T> set)
  • List<T> synchronizedList(List<T> list)
  • Map<K,V> synchronizedMap(Map<K,V> map)
  • Collection<T> synchronizedCollection(Collection<T> collection)
  • SortedSet<T> synchronizedSortedSet(SortedSet<T> set)
  • SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> map)
  • NavigableSet<T> synchronizedNavigableSet(NavigableSet<T> set)
  • NavigableMap<K,V> synchronizedNavigableMap(NavigableMap<K,V> map)

构造线程安全集合时,两个应用程序线程只能按顺序修改它,而不会影响彼此的临时结果。但是,多线程处理不在本书的范围之内,所以我们就到此为止。

转换为其他集合类型

将一种类型的集合转换为另一种类型的四种静态方法包括:

  • ArrayList<T> list(Enumeration<T> e)
  • Enumeration<T> enumeration(Collection<T> c)
  • Queue<T> asLifoQueue(Deque<T> deque)
  • Set<E> newSetFromMap(Map<E,Boolean> map)

接口java.util.Enumeration是 Java 1 附带的遗留接口,以及使用它的遗留类java.util.Hashtablejava.util.Vector。与Iterator接口非常相似。事实上,通过使用Enumeration.asIterator()方法,可以将Enumeration类型的对象转换为Iterator类型。

所有这些方法很少在主流编程中使用,因此我们在这里列出它们只是为了完整性。

创建枚举和迭代器

以下也不是经常使用的静态方法,它们允许创建空的EnumerationIteratorListIterator-都是java.util包的接口:

  • Iterator<T> empty iterator``()
  • ListIterator<T> emptyListIterator()
  • Enumeration<T> emptyEnumeration()

类 collections4.CollectionUtils

Apache Commons 项目中的类org.apache.commons.collections4.CollectionUtils包含静态无状态方法,这些方法补充了类java.util.Collections的方法。它们有助于搜索、处理和比较 Java 集合。要使用此类,您需要将以下依赖项添加到 Maven 配置文件pom.xml

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-collections4</artifactId>
  <version>4.1</version>
</dependency>

这个类中有很多方法,随着时间的推移,可能会添加更多的方法。刚刚复习过的Collections类可能会满足您的大部分需求,特别是当您刚刚进入 Java 编程领域时。因此,我们不会像在Collections类中那样,花时间解释每个方法的用途。此外,CollectionUtils的方法是在Collections的方法之外创建的,因此它们更加复杂和微妙,不适合本书的范围。

为了让您了解CollectionUtils类中可用的方法,我们按照相关功能对它们进行了分组:

  • 检索元素的方法:
    • Object get(Object object, int index)
    • Map.Entry<K,V> get(Map<K,V> map, int index)
    • Map<O,Integer> getCardinalityMap(Iterable<O> collection)
  • 将元素或元素组添加到集合的方法:
    • boolean addAll(Collection<C> collection, C[] elements)
    • boolean addIgnoreNull(Collection<T> collection, T object)
    • boolean addAll(Collection<C> collection, Iterable<C> iterable)
    • boolean addAll(Collection<C> collection, Iterator<C> iterator)
    • boolean addAll(Collection<C> collection, Enumeration<C> enumeration)
  • 合并Iterable元素的方法:
    • List<O> collate(Iterable<O> a, Iterable<O> b)
    • List<O> collate(Iterable<O> a, Iterable<O> b, Comparator<O> c)
    • List<O> collate(Iterable<O> a, Iterable<O> b, boolean includeDuplicates)
    • List<O> collate(Iterable<O> a, Iterable<O> b, Comparator<O> c, boolean includeDuplicates)
  • 删除或保留具有或不具有条件的元素的方法:
    • Collection<O> subtract(Iterable<O> a, Iterable<O> b)
    • Collection<O> subtract(Iterable<O> a, Iterable<O> b, Predicate<O> p)
    • Collection<E> removeAll(Collection<E> collection, Collection<?> remove)
    • Collection<E> removeAll(Iterable<E> collection, Iterable<E> remove, Equator<E> equator)
    • Collection<C> retainAll(Collection<C> collection, Collection<?> retain)
    • Collection<E> retainAll(Iterable<E> collection, Iterable<E> retain, Equator<E> equator)
  • 比较两个集合的方法:
    • boolean containsAll(Collection<?> coll1, Collection<?> coll2)
    • boolean containsAny(Collection<?> coll1, Collection<?> coll2)
    • boolean isEqualCollection(Collection<?> a, Collection<?> b)
    • boolean isEqualCollection(Collection<E> a, Collection<E> b, Equator<E> equator)
    • boolean isProperSubCollection(Collection<?> a, Collection<?> b)
  • 转换集合的方法:
    • Collection<List<E>> permutations(Collection<E> collection)
    • void transform(Collection<C> collection, Transformer<C,C> transformer)
    • Collection<E> transformingCollection(Collection<E> collection, Transformer<E,E> transformer)
    • Collection<O> collect(Iterator<I> inputIterator, Transformer<I,O> transformer)
    • Collection<O> collect(Iterable<I> inputCollection, Transformer<I,O> transformer)
    • Collection<O> R collect(Iterator<I> inputIterator, Transformer<I,O> transformer, R outputCollection)
    • Collection<O> R collect(Iterable<I> inputCollection, Transformer<I,O> transformer, R outputCollection)
  • 选择和筛选集合的方法:
    • Collection<O> select(Iterable<O> inputCollection, Predicate<O> predicate)
    • Collection<O> R select(Iterable<O> inputCollection, Predicate<O> predicate, R outputCollection)
    • Collection<O> R select(Iterable<O> inputCollection, Predicate<O> predicate, R outputCollection, R rejectedCollection)
    • Collection<O> selectRejected(Iterable<O> inputCollection, Predicate<O> predicate)
    • Collection<O> R selectRejected(Iterable<O> inputCollection, Predicate<O> predicate, R outputCollection)
    • E extractSingleton(Collection<E> collection)
    • boolean filter(Iterable<T> collection, Predicate<T> predicate)
    • boolean filterInverse(Iterable<T> collection, Predicate<T> predicate)
    • Collection<C> predicatedCollection(Collection<C> collection, Predicate<C> predicate)
  • 生成两个集合的并集、交集或差集的方法:
    • Collection<O> union(Iterable<O> a, Iterable<O> b)
    • Collection<O> disjunction(Iterable<O> a, Iterable<O> b)
    • Collection<O> intersection(Iterable<O> a, Iterable<O> b)
  • 创建不可变空集合的方法:
    • <T> Collection<T> emptyCollection()
    • Collection<T> emptyIfNull(Collection<T> collection)
  • 检查集合大小和空性的方法:
    • int size(Object object)
    • boolean sizeIsEmpty(Object object)
    • int maxSize(Collection<Object> coll)
    • boolean isEmpty(Collection<?> coll)
    • boolean isNotEmpty(Collection<?> coll)
    • boolean isFull(Collection<Object> coll)
  • 反转数组的方法:
    • void reverseArray(Object[] array)

最后一个方法可能属于处理数组的实用程序类,这就是我们现在要讨论的。

管理阵列

在本节中,我们将回顾如何创建和初始化数组对象,以及在哪里可以找到允许我们对数组执行某些操作的方法,例如复制、排序和比较。

虽然数组在某些算法和遗留代码中占有一席之地,但实际上,ArrayList()可以完成数组所能做的一切,并且不需要预先设置大小。事实上,ArrayList正在使用数组将其元素存储在后面。因此,阵列和ArrayList的性能也是相当的。

因此,除了创建和初始化的基础知识之外,我们不会过多地讨论阵列管理。我们将提供一个简短的概述和参考,说明在哪里可以找到 array 实用程序方法,以防您需要它们。

初始化数组

我们已经看到了一些数组构造的示例。现在,我们将回顾它们,并介绍创建和初始化数组对象的其他方法。

创作表达

数组创建表达式包括:

  • 数组元素类型
  • 嵌套数组的级别数
  • 至少第一个级别的数组长度

以下是一级阵列创建示例:

int[] ints = new int[10];
System.out.println(ints[0]);     //prints: 0

Integer[] intW = new Integer[10];
System.out.println(intW[0]);     //prints: null

boolean[] bs = new boolean[10];
System.out.println(bs[0]);       //prints: false

Boolean[] bW = new Boolean[10];
System.out.println(bW[0]);       //prints: 0

String[] strings = new String[10];
System.out.println(strings[0]);  //prints: null

A[] as = new A[10];
System.out.println(as[0]);       //prints: null 
System.out.println(as.length);   //prints: 10

正如我们在第 5 章Java 语言元素和类型中所示,每个 Java 类型都有一个默认的初始化值,在对象创建过程中没有显式赋值时使用。因为数组是一个类,所以它的元素就像任何类的实例字段一样被初始化——即使程序员没有显式地给它们赋值。数字基元类型的默认值为 0,boolean基元类型的默认值为false,而所有引用类型的默认值为null。上例中使用的类A定义为class A {}。数组的长度在最终的公共属性length中捕获。

多级嵌套初始化如下所示:

    //A[][] as2 = new A[][10];             //compilation error
    A[][] as2 = new A[10][];
    System.out.println(as2.length);        //prints: 10
    System.out.println(as2[0]);            //prints: null
    //System.out.println(as2[0].length);   //NullPointerException
    //System.out.println(as2[0][0]);       //NullPointerException

    as2 = new A[2][3];
    System.out.println(as2[0]); //prints: ManageArrays$A;@282ba1e
    System.out.println(as2[0].length); //prints: 3
    System.out.println(as2[0][0]);     //prints: null

首先要注意的是,如果试图在不定义第一级数组长度的情况下创建数组,则会生成编译错误。第二个观察结果是,多级数组的length属性捕获了第一级(顶层)数组的长度。第三,顶级数组的每个元素都是一个数组。如果下一级数组不是最后一级数组,则下一级数组的元素也是数组。

在前面的示例中,我们没有设置第二级数组长度,因此顶级数组的每个元素都被初始化为null,因为这是任何引用类型(数组也是引用类型)的默认值。这就是为什么尝试获取生成的NullPointerException二级数组的长度或任何值。

一旦我们将二级数组的长度设置为 3,我们就能够得到它的长度和它的第一个元素的值(null,因为这是默认值)。奇怪的打印输出ManageArrays$A;@282ba1e是数组二进制引用,因为对象数组没有实现toString()方法。最接近的方法是实用程序类java.util.Arrays的静态方法toString()(见下一节)。它返回所有数组元素的String表示:

System.out.println(Arrays.toString(as2));   
        //prints: [[ManageArrays$A;@282ba1e, [ManageArrays$A;@13b6d03]
System.out.println(Arrays.toString(as2[0])); //[null, null, null]

对于最后一个(最深的)嵌套数组,它可以正常工作,但对于更高级别的数组,它仍然会打印一个二进制引用。如果要打印出所有嵌套数组的所有元素,请使用Arrays.deepToString(Object[])方法:

System.out.println(Arrays.deepToString(as2)); 
           //the above prints: [[null, null, null], [null, null, null]]

请注意,如果数组元素未实现toString()方法,则将为非null的元素打印二进制引用。

数组初始值设定项

数组初始值设定项由逗号分隔的表达式列表组成,包含在大括号{}中。允许并忽略最后一个表达式后的逗号:

String[] arr = {"s0", "s1", };
System.out.println(Arrays.toString(arr)); //prints: [s0, s1]

在我们的示例中,我们经常使用这种方法初始化数组,因为这是最简洁的方法。

静态初始化块

与集合的情况一样,当必须执行某些代码时,可以使用静态块初始化数组静态属性:

class ManageArrays {
private static A[] AS_STATIC;
  static {
    AS_STATIC = new A[2];
    for(int i = 0; i< AS_STATIC.length; i++){
        AS_STATIC[i] = new A();
    }
    AS_STATIC[0] = new A();
    AS_STATIC[1] = new A();
  }
  //... the rest of class code goes here
}

每次加载类时,甚至在调用构造函数之前,都会执行静态块中的代码。但如果字段不是静态的,则可以在构造函数中放置相同的初始化代码:

class ManageArrays {
  private A[] as;
  public ManageArrays(){
    as = new A[2];
    for(int i = 0; i< as.length; i++){
        as[i] = new A();
    }
    as[0] = new A();
    as[1] = new A();
  }
  //the reat of class code goes here
}

从集合

如果有一个集合可用作数组值的源,则该集合具有方法toArray(),可按如下方式调用:

List<Integer> list = List.of(0, 1, 2, 3);
Integer[] arr1 = list.toArray(new Integer[list.size()]);
System.out.println(Arrays.toString(arr1)); //prints: [0, 1, 2, 3]

其他可能的方法

在不同的上下文中,可能有其他一些方法用于创建和初始化数组。这也是你喜欢什么风格的问题。以下是可以从中选择的各种阵列创建和初始化方法的示例:

String[] arr2 = new String[3];
Arrays.fill(arr2, "s");
System.out.println(Arrays.toString(arr2));      //prints: [s, s, s]

String[] arr3 = new String[5];
Arrays.fill(arr3, 2, 3, "s");
System.out.println(Arrays.toString(arr3)); 
                              //prints: [null, null, s, null, null]
String[] arr4 = {"s0", "s1", };
String[] arr4Copy = Arrays.copyOf(arr4, 5);
System.out.println(Arrays.toString(arr4Copy)); 
                                //prints: [s0, s1, null, null, null]
String[] arr5 = {"s0", "s1", "s2", "s3", "s4" };
String[] arr5Copy = Arrays.copyOfRange(arr5, 1, 3);
System.out.println(Arrays.toString(arr5Copy));    //prints: [s1, s2]

Integer[] arr6 = {0, 1, 2, 3, 4 };
Object[] arr6Copy = Arrays.copyOfRange(arr6,1, 3, Object[].class);
System.out.println(Arrays.toString(arr6Copy));      //prints: [1, 2]

String[] arr7 = Stream.of("s0", "s1", "s2").toArray(String[]::new);
System.out.println(Arrays.toString(arr7));    //prints: [s0, s1, s2] 

在上面的六个示例中,有五个使用类java.util.Arrays(见下一节)填充或复制数组。所有人都使用Arrays.toString()方法打印生成阵列的元素。

第一个示例将arr2s分配给数组的所有元素。

第二个示例仅将值s分配给索引 2 到索引 3 中的元素。请注意,第二个索引是不包含的。这就是为什么数组arr3中只有一个元素被赋值的原因。

第三个示例复制了arr4数组,使新数组的大小变长。这就是为什么其余的新数组元素被初始化为默认值String,即null。请注意,我们在arr4数组初始值设定项中放置了一个尾随逗号,以证明它是允许的,并且被忽略。它看起来不是一个非常重要的功能。我们已经指出了它,以防您在其他人的代码中看到它,并想知道它是如何工作的。

第四个示例使用索引 1 到 3 中的元素创建数组的副本。同样,第二个索引不包括在内,因此只复制两个元素。

第五个示例不仅创建元素范围的副本,还将它们转换为Object类型,这是可能的,因为源数组是引用类型。

最后一个例子是使用Stream类,我们将在第 18 章中讨论流和管道

类 java.util.Arrays

我们已经多次使用该类java.util.Arrays。它是阵列管理的主要工具。但是,它过去也很受使用集合的人的欢迎,因为方法asList(T...a)是创建和初始化集合的最简洁的方式:

List<String> list = Arrays.asList("s0", "s1");
Set<String> set = new HashSet<>(Arrays.asList("s0", "s1");

但在每一个集合中引入工厂方法of()后,Arrays类的受欢迎程度大幅下降。以下是创建集合的更自然的方法:

List<String> list = List.of("s0", "s1");
Set<String> set = Set.of("s0", "s1");

此集合的对象是不可变的。但如果需要可变的,可以按如下方式创建:

List<String> list = new ArrayList<>(List.of("s0", "s1"));
Set<String> set1 = new HashSet<>(list);
Set<String> set2 = new HashSet<>(Set.of("s0", "s1"));

我们在前面的管理集合一节中详细讨论了这一点。

但是如果您的代码管理数组,那么您肯定需要使用类Arrays。它包含 160 多种方法。它们中的大多数都使用不同的参数和数组类型重载。如果我们按方法名对它们进行分组,将有 21 个组。如果我们按功能进一步分组,则只有以下 10 组将涵盖Arrays类的所有功能:

  • asList():基于提供的数组创建ArrayList对象(参见上节示例)
  • binarySearch():允许搜索指定的数组或数组的一部分(按索引范围)
  • compare()mismatch()equals()deepEquals():比较两个阵列或其部分(按指标范围)
  • copyOf()copyOfRange():复制所有数组或只是其中的一部分(按索引范围)
  • hashcode()deepHashCode():根据提供的数组内容生成哈希码值
  • toString()deepToString():创建一个数组的String表示(参见上一节中的示例)
  • fill()setAll()parallelPrefix()parallelSetAll():设置数组中每个元素的值(固定值或由提供的函数生成)或索引范围指定的值
  • sort()parallelSort():对数组的元素进行排序或仅对数组的一部分进行排序(由索引范围指定)
  • splititerator():返回一个Splititerator对象,用于并行处理数组或数组的一部分(由索引范围指定)
  • stream():生成数组元素流或部分数组元素流(由索引范围指定);参见第 18 章流及管道

所有这些方法都很有帮助,但我们想让您注意到equals(a1, a2)deepEquals(a1, a2)方法。它们对于数组比较特别有用,因为数组对象不允许实现自定义方法equals(a),因此总是使用只比较引用的类Object的实现。

相比之下,equals(a1, a2)deepEquals(a1, a2)方法不仅比较参考文献a1a2,而且在阵列的情况下,使用equals(a)方法比较元素。这意味着,当两个数组都是null或长度相等时,非嵌套数组通过其元素的值进行比较,并被视为相等,并且方法a1[i].equals(a2[i])为每个索引返回true

Integer[] as1 = {1,2,3};
Integer[] as2 = {1,2,3};
System.out.println(as1.equals(as2));               //prints: false
System.out.println(Arrays.equals(as1, as2));       //prints: true
System.out.println(Arrays.deepEquals(as1, as2));   //prints: true

对于嵌套数组,equals(a1, a2)方法使用equals(a)方法来比较下一层的元素。但是嵌套数组的元素是数组,因此它们仅通过引用进行比较,而不是通过元素的值进行比较。如果需要比较所有嵌套级别上的元素值,请使用方法deepEquals(a1, a2)

Integer[][] aas1 = {{1,2,3}, {4,5,6}};
Integer[][] aas2 = {{1,2,3}, {4,5,6}};
System.out.println(Arrays.equals(aas1, aas2));       //prints: false
System.out.println(Arrays.deepEquals(aas1, aas2));   //prints: true

Integer[][][] aaas1 = {{{1,2,3}, {4,5,6}}, {{7,8,9}, {10,11,12}}};
Integer[][][] aaas2 = {{{1,2,3}, {4,5,6}}, {{7,8,9}, {10,11,12}}};
System.out.println(Arrays.deepEquals(aaas1, aaas2)); //prints: true

类 lang3.ArrayUtils

班级org.apache.commons.lang3.ArrayUtils是对班级java.util.Arrays的赞美。它为数组管理工具包添加了新方法,并在可能抛出NullPointerException的情况下处理null的能力。

Arrays类类似,ArrayUtils类有许多(大约 300 个)重载方法,可以收集到 12 个组中:

  • add()addAll()insert():向数组中添加元素
  • clone():克隆一个数组,类似java.util.Arrays中的copyOf()方法和java.lang.System中的arraycopy()方法
  • getLength():返回数组长度并处理null(当数组为null时尝试读取属性length抛出NullPointerException
  • hashCode():计算数组的哈希值,包括嵌套数组
  • contains()indexOf()lastIndexOf():搜索数组
  • isSorted()isEmptyisNotEmpty():检查数组并处理null
  • isSameLength()isSameType():比较数组
  • nullToEmpty():将null数组转换为空数组
  • remove()removeAll()removeElement()removeElements()removeAllOccurances():移除元件
  • reverse()shift()shuffle()swap():改变数组元素的顺序
  • subarray():按索引范围提取数组的一部分
  • toMap()toObject()toPrimitive()toString()toStringArray():将数组转换为另一种类型并处理null

练习–对对象列表进行排序

列出两种允许对对象列表进行排序的方法及其使用的先决条件。

答复

java.util.Collections类的两种静态方法:

  • void sort(List<T> list):对实现接口Comparable(方法compareTo(T)的对象列表进行排序,
  • void sort(List<T> list, Comparator<T> comparator):根据提供的Comparator对对象进行排序(即compare(T o1, T o2)方法)

总结

在本章中,我们向读者介绍了 Java 标准库和 Apache Commons 中允许操作集合和数组的类。每个 Java 程序员都必须知道类java.util.Collectionsjava.util.Arraysorg.acpache.commons.collections4.CollectionUtilsorg.acpache.commons.lang3.ArrayUtils的功能。

在下一章中,我们将讨论一些类,这些类与本章中讨论的类一起,属于一组最流行的实用程序,每个程序员都必须掌握这些实用程序才能成为一名有效的程序员。