Skip to content

Files

Latest commit

 

History

History
1328 lines (994 loc) · 61.7 KB

File metadata and controls

1328 lines (994 loc) · 61.7 KB

六、数据结构、泛型和流行工具

本章介绍了 Java 集合框架及其三个主要接口:ListSetMap,包括泛型的讨论和演示。equals()hashCode()方法也在 Java 集合的上下文中讨论。用于管理数组、对象和时间/日期值的工具类也有相应的专用部分。

本章将讨论以下主题:

  • ListSetMap接口
  • 集合工具
  • 数组工具
  • 对象工具
  • java.time

列表、集合和映射接口

Java 集合框架由实现集合数据结构的类和接口组成。集合在这方面类似于数组,因为它们可以保存对对象的引用,并且可以作为一个组进行管理。不同之处在于,数组需要先定义它们的容量,然后才能使用,而集合可以根据需要自动增减大小。一种是添加或删除对集合的对象引用,集合相应地改变其大小,另一个区别是集合的元素不能是原始类型,如shortintdouble。如果需要存储这样的类型值,那么元素必须是相应的包装器类型,例如ShortIntegerDouble

Java 集合支持存储和访问集合元素的各种算法:有序列表、唯一集、Java 中称为映射的字典、栈、队列,Java 集合框架的所有类和接口都属于 Java 类库的java.util包。java.util包包含以下内容:

  • Collection接口进行扩展的接口有:ListSetQueue
  • 实现前面列出的接口的类:ArrayListHashSetStackLinkedList和其他一些类
  • Map接口及其子接口:ConcurrentMapSortedMap,以一对夫妇的名字命名
  • 实现与Map相关的接口的类:HashMapHashTableTreeMap,这三个类是最常用的

要查看java.util包的所有类和接口,需要一本专门的书。因此,在本节中,我们将简要介绍三个主要接口:ListSetMap——以及它们各自的一个实现类:ArrayListHashSetHashMap。我们从ListSet接口共享的方法开始。ListSet的主要区别在于Set不允许元素重复。另一个区别是List保留了元素的顺序,也允许对它们进行排序。

要标识集合中的元素,请使用equals()方法。为了提高性能,实现Set接口的类也经常使用hashCode()方法。它允许快速计算一个整数(称为散列值哈希码),该整数在大多数时间(但并非总是)对每个元素都是唯一的。具有相同哈希值的元素被放置在相同的中。在确定集合中是否已经存在某个值时,检查内部哈希表并查看是否已经使用了这样的值就足够了。否则,新元素是唯一的。如果是,则可以将新元素与具有相同哈希值的每个元素进行比较(使用equals()方法)。这样的过程比逐个比较新元素和集合中的每个元素要快

这就是为什么我们经常看到类的名称有Hash前缀,表示类使用了哈希值,所以元素必须实现hashCode()方法,在实现时一定要确保equals()方法每次为两个对象返回true时,hashCode()方法返回的这两个对象的散列值也是相等的。否则,所有刚才描述的使用哈希值的算法都将不起作用。

最后,在讨论java.util接口之前,先谈一下泛型。

泛型

您最常在以下声明中看到它们:

List<String> list = new ArrayList<String>();
Set<Integer> set = new HashSet<Integer>();

在前面的例子中,泛型是被尖括号包围的元素类型声明。如您所见,它们是多余的,因为它们在赋值语句的左侧和右侧重复。这就是为什么 Java 允许用空括号(<>)替换右侧的泛型,称为菱形

List<String> list = new ArrayList<>();
Set<Integer> set = new HashSet<>();

泛型通知编译器集合元素的预期类型。这样编译器就可以检查程序员试图添加到声明集合中的元素是否是兼容类型。例如:

List<String> list = new ArrayList<>();
list.add("abc");
list.add(42);   //compilation error

它有助于避免运行时错误。它还向程序员提示可能对集合元素进行的操作(因为程序员编写代码时 IDE 会编译代码)。

我们还将看到其他类型的泛型:

  • <? extends T>表示TT的子类型,其中T是用作集合泛型的类型
  • <? super T>表示T或其任何基(父)类,其中T是用作集合泛型的类型

那么,让我们从实现ListSet接口的类的对象的创建方式开始,或者换句话说,可以初始化ListSet类型的变量。为了演示这两个接口的方法,我们将使用两个类:ArrayList(实现List)和HashSet(实现Set)。

如何初始化列表和集合

由于 Java9,ListSet接口具有静态工厂方法of(),可用于初始化集合:

  • of():返回空集合。
  • of(E... e):返回一个集合,其中包含调用期间传入的元素数。它们可以以逗号分隔的列表或数组形式传递。

以下是几个例子:

//Collection<String> coll = List.of("s1", null); //does not allow null
Collection<String> coll = List.of("s1", "s1", "s2");
//coll.add("s3");                        //does not allow add element
//coll.remove("s1");                     //does not allow remove element
((List<String>) coll).set(1, "s3");      //does not allow modify element
System.out.println(coll);                //prints: [s1, s1, s2]

//coll = Set.of("s3", "s3", "s4");       //does not allow duplicate
//coll = Set.of("s2", "s3", null);       //does not allow null
coll = Set.of("s3", "s4");
System.out.println(coll);                //prints: [s3, s4]

//coll.add("s5");                        //does not allow add element
//coll.remove("s2");                     //does not allow remove

正如人们所料,Set的工厂方法不允许重复,因此我们已经注释掉了该行(否则,前面的示例将停止在该行运行)。不太令人期待的是,不能有一个null元素,也不能在使用of()方法之一初始化集合之后添加/删除/修改集合的元素。这就是为什么我们注释掉了前面示例中的一些行。如果需要在集合初始化后添加元素,则必须使用构造器或其他创建可修改集合的工具对其进行初始化(稍后我们将看到一个Arrays.asList()的示例)。

接口Collection提供了两种向实现了CollectionListSet的父接口)的对象添加元素的方法,如下所示:

  • boolean add(E e):尝试将提供的元素e添加到集合中,成功返回true,无法完成返回false(例如Set中已经存在该元素)

  • boolean addAll(Collection<? extends E> c):尝试将所提供集合中的所有元素添加到集合中;如果至少添加了一个元素,则返回true;如果无法将元素添加到集合中,则返回false(例如,当所提供集合c中的所有元素都已存在于Set中时)

以下是使用add()方法的示例:

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

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

下面是一个使用addAll()方法的例子:

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

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

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

以下是add()addAll()方法的功能示例:

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

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

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

Set<String> set1 = new HashSet<>();
set1.add("s1");

Set<String> set2 = new HashSet<>();
set2.add("s1");
set2.add("s2");

System.out.println(set1.addAll(set2)); //prints: true
System.out.println(set1);              //prints: [s1, s2]

注意,在前面代码片段的最后一个示例中,set1.addAll(set2)方法返回true,尽管没有添加所有元素。要查看add()addAll()方法返回false的情况,请看以下示例:

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

Set<String> set1 = new HashSet<>();
set1.add("s1");
set1.add("s2");

Set<String> set2 = new HashSet<>();
set2.add("s1");
set2.add("s2");

System.out.println(set1.addAll(set2)); //prints: false
System.out.println(set1);              //prints: [s1, s2]

ArrayListHashSet类还有接受集合的构造器:

Collection<String> list1 = List.of("s1", "s1", "s2");
System.out.println(list1);      //prints: [s1, s1, s2]

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

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

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

现在,在我们了解了如何初始化集合之后,我们可以转向接口ListSet中的其他方法。

java.lang.Iterable接口

Collection接口扩展了java.lang.Iterable接口,这意味着那些直接或不直接实现Collection接口的类也实现了java.lang.Iterable接口。Iterable接口只有三种方式:

  • Iterator<T> iterator():返回实现接口java.util.Iterator的类的对象,允许集合在FOR语句中使用,例如:
Iterable<String> list = List.of("s1", "s2", "s3");
System.out.println(list);       //prints: [s1, s2, s3]

for(String e: list){
    System.out.print(e + " ");  //prints: s1 s2 s3
}
  • default void forEach (Consumer<? super T> function):将提供的Consumer类型的函数应用于集合的每个元素,直到所有元素都处理完毕或函数抛出异常为止。什么是函数,我们将在第 13 章、“函数编程”中讨论;现在我们只提供一个例子:
Iterable<String> list = List.of("s1", "s2", "s3");
System.out.println(list);                     //prints: [s1, s2, s3]
list.forEach(e -> System.out.print(e + " ")); //prints: s1 s2 s3
  • default Spliterator<T> splititerator():返回实现java.util.Spliterator接口的类的对象,主要用于实现允许并行处理的方法,不在本书范围内

集合接口

如前所述,ListSet接口扩展了Collection接口,这意味着Collection接口的所有方法都被ListSet继承。这些方法如下:

  • boolean add(E e):尝试向集合添加元素
  • boolean addAll(Collection<? extends E> c):尝试添加所提供集合中的所有元素
  • boolean equals(Object o):将集合与提供的对象o进行比较;如果提供的对象不是集合,则返回false;否则将集合的组成与提供的集合的组成进行比较(作为对象o);如果是List,它还比较了元素的顺序;让我们用几个例子来说明:
Collection<String> list1 = List.of("s1", "s2", "s3");
System.out.println(list1);       //prints: [s1, s2, s3]

Collection<String> list2 = List.of("s1", "s2", "s3");
System.out.println(list2);       //prints: [s1, s2, s3]

System.out.println(list1.equals(list2));  //prints: true

Collection<String> list3 = List.of("s2", "s1", "s3");
System.out.println(list3);       //prints: [s2, s1, s3]

System.out.println(list1.equals(list3));  //prints: false

Collection<String> set1 = Set.of("s1", "s2", "s3");
System.out.println(set1);   //prints: [s2, s3, s1] or different order

Collection<String> set2 = Set.of("s2", "s1", "s3");
System.out.println(set2);   //prints: [s2, s1, s3] or different order

System.out.println(set1.equals(set2));  //prints: true

Collection<String> set3 = Set.of("s4", "s1", "s3");
System.out.println(set3);   //prints: [s4, s1, s3] or different order

System.out.println(set1.equals(set3));  //prints: false
  • int hashCode():返回集合的哈希值,用于集合是需要hashCode()方法实现的集合元素的情况

  • boolean isEmpty():如果集合中没有任何元素,则返回true

  • int size():返回集合中元素的计数;当isEmpty()方法返回true时,此方法返回0

  •  void clear():删除集合中的所有元素;调用此方法后,isEmpty()方法返回truesize()方法返回0

  • boolean contains(Object o):如果集合包含提供的对象o,则返回true;要使此方法正常工作,集合中的每个元素和提供的对象必须实现equals()方法,如果是Set,则需要实现hashCode()方法

  • boolean containsAll(Collection<?> c):如果集合包含所提供集合中的所有元素,则返回true,要使此方法正常工作,集合中的每个元素和所提供集合中的每个元素必须实现equals()方法,如果是Set,则应实现hashCode()方法

  • boolean remove(Object o):尝试从此集合中移除指定元素,如果存在则返回true;要使此方法正常工作,集合的每个元素和提供的对象必须实现方法equals(),如果是Set,则应实现hashCode()方法

  • boolean removeAll(Collection<?> c):尝试从集合中移除所提供集合的所有元素;与addAll()方法类似,如果至少移除了一个元素,则返回true,否则返回false,以便该方法正常工作,集合的每个元素和所提供集合的每个元素必须实现equals()方法,在Set的情况下,应该实现hashCode()方法

  • default boolean removeIf(Predicate<? super E> filter):尝试从集合中移除满足给定谓词的所有元素;我们将在第 13 章、“函数式编程”中描述的函数;如果至少移除了一个元素,则返回true

  • boolean retainAll(Collection<?> c):试图在集合中只保留所提供集合中包含的元素;与addAll()方法类似,如果至少保留了一个元素,则返回true,否则返回false,以便该方法正常工作,集合的每个元素和所提供集合的每个元素必须实现equals()方法,在Set的情况下,应该实现hashCode()方法

  • Object[] toArray()T[] toArray(T[] a):将集合转换成数组

  • default T[] toArray(IntFunction<T[]> generator):使用提供的函数将集合转换为数组;我们将在第 13 章、“函数式编程”中解释函数

  • default Stream<E> stream():返回Stream对象(我们在第 14 章、“Java 标准流”中谈到流)

  • default Stream<E> parallelStream():返回一个可能并行的Stream对象(我们在第 14 章“Java 标准流”中讨论流)。

列表接口

List接口有几个不属于其父接口的其他方法:

  • 静态工厂of()方法“如何初始化列表和集合”小节中描述的方法
  • void add(int index, E element):在列表中提供的位置插入提供的元素
  • static List<E> copyOf(Collection<E> coll):返回一个不可修改的List,其中包含给定Collection的元素并保留它们的顺序;下面是演示此方法功能的代码:
Collection<String> list = List.of("s1", "s2", "s3");
System.out.println(list);         //prints: [s1, s2, s3]

List<String> list1 = List.copyOf(list);
//list1.add("s4");                //run-time error
//list1.set(1, "s5");             //run-time error
//list1.remove("s1");             //run-time error

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

Set<String> set1 = Set.copyOf(set);
//set1.add("s2");                 //run-time error
//set1.remove("s1");              //run-time error

Set<String> set2 = Set.copyOf(list);
System.out.println(set2);         //prints: [s1, s2, s3] 
  • E get(int index):返回列表中指定位置的元素
  • List<E> subList(int fromIndex, int toIndex):在fromIndex(包含)和toIndex(排除)之间提取子列表
  • int indexOf(Object o):返回列表中指定元素的第一个索引(位置);列表中的第一个元素有一个索引(位置)0
  • int lastIndexOf(Object o):返回列表中指定元素的最后一个索引(位置);列表中最后一个元素的索引(位置)等于list.size() - 1
  • E remove(int index):删除列表中指定位置的元素;返回删除的元素
  • E set(int index, E element):替换列表中指定位置的元素,返回被替换的元素
  • default void replaceAll(UnaryOperator<E> operator):通过将提供的函数应用于每个元素来转换列表,UnaryOperator函数将在第 13 章、“函数式编程”中描述
  • ListIterator<E> listIterator():返回允许向后遍历列表的ListIterator对象
  • ListIterator<E> listIterator(int index):返回一个ListIterator对象,该对象允许向后遍历子列表(从提供的位置开始);例如:
List<String> list = List.of("s1", "s2", "s3");
ListIterator<String> li = list.listIterator();
while(li.hasNext()){
    System.out.print(li.next() + " ");         //prints: s1 s2 s3
}
while(li.hasPrevious()){
    System.out.print(li.previous() + " ");     //prints: s3 s2 s1
}
ListIterator<String> li1 = list.listIterator(1);
while(li1.hasNext()){
    System.out.print(li1.next() + " ");        //prints: s2 s3
}
ListIterator<String> li2 = list.listIterator(1);
while(li2.hasPrevious()){
    System.out.print(li2.previous() + " ");    //prints: s1
}
  • default void sort(Comparator<? super E> c):根据提供的Comparator生成的顺序对列表进行排序,例如:
List<String> list = new ArrayList<>();
list.add("S2");
list.add("s3");
list.add("s1");
System.out.println(list);                //prints: [S2, s3, s1]

list.sort(String.CASE_INSENSITIVE_ORDER);
System.out.println(list);                //prints: [s1, S2, s3]

//list.add(null);                 //causes NullPointerException
list.sort(Comparator.naturalOrder());
System.out.println(list);               //prints: [S2, s1, s3]

list.sort(Comparator.reverseOrder());
System.out.println(list);               //prints: [s3, s1, S2]

list.add(null);
list.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(list);              //prints: [null, S2, s1, s3]

list.sort(Comparator.nullsLast(Comparator.naturalOrder()));
System.out.println(list);              //prints: [S2, s1, s3, null]

Comparator<String> comparator = (s1, s2) -> 
 s1 == null ? -1 : s1.compareTo(s2);
list.sort(comparator);
System.out.println(list);              //prints: [null, S2, s1, s3]

对列表排序主要有两种方法:

  • 使用Comparable接口实现(称为自然顺序
  • 使用Comparator接口实现

Comparable接口只有compareTo()方法。在前面的例子中,我们已经在String类中的Comparable接口实现的基础上实现了Comparator接口。如您所见,此实现提供了与Comparator.nullsFirst(Comparator.naturalOrder())相同的排序顺序,这种实现方式称为函数式编程,我们将在第 13 章“函数式编程”中详细讨论。

*# 接口集

Set接口有以下不属于其父接口的方法:

  • 静态of()工厂方法,在“如何初始化列表和集合”小节中描述
  • static Set<E> copyOf(Collection<E> coll)方法:返回一个包含给定Collection元素的不可修改的Set,其工作方式与“接口列表”部分描述的static <E> List<E> copyOf(Collection<E> coll)方法相同

映射接口

Map接口有很多类似ListSet的方法:

  • int size()
  • void clear()
  • int hashCode()
  • boolean isEmpty()
  • boolean equals(Object o)
  • default void forEach(BiConsumer<K,V> action)
  • 静态工厂方法:of()of(K k, V v)of(K k1, V v1, K k2, V v2)等多种方法

然而,Map接口并不扩展IterableCollection或任何其他接口。通过可以存储*。*每个键都是唯一的,而同一个映射上不同的键可以存储几个相等的值。键和值的组合构成了一个Entry,是Map的内部接口。值和关键对象都必须实现equals()方法,关键对象也必须实现hashCode()方法

Map接口的很多方法与ListSet接口的签名和功能完全相同,这里不再赘述,我们只介绍Map的具体方法:

  • V get(Object key):按提供的键取值,如果没有该键返回null

  • Set<K> keySet():从映射中检索所有键

  • Collection<V> values():从映射中检索所有值

  • boolean containsKey(Object key):如果映射中存在提供的键,则返回true

  • boolean containsValue(Object value):如果提供的值存在于映射中,则返回true

  • V put(K key, V value):将值及其键添加到映射中;返回使用相同键存储的上一个值

  • void putAll(Map<K,V> m):从提供的映射中复制所有键值对

  • default V putIfAbsent(K key, V value):存储所提供的值,如果映射尚未使用该键,则映射到所提供的键;将映射到所提供键的值返回到现有或新的值

  • V remove(Object key):从映射中删除键和值;如果没有键或值为null,则返回值或null

  • default boolean remove(Object key, Object value):如果映射中存在键值对,则从映射中移除键值对

  • default V replace(K key, V value):如果提供的键当前映射到提供的值,则替换该值;如果被替换,则返回旧值;否则返回null

  • default boolean replace(K key, V oldValue, V newValue):如果提供的键当前映射到oldValue,则用提供的newValue替换值oldValue;如果替换了oldValue,则返回true,否则返回false

  • default void replaceAll(BiFunction<K,V,V> function):将提供的函数应用于映射中的每个键值对,并用结果替换,如果不可能,则抛出异常

  • Set<Map.Entry<K,V>> entrySet():返回一组所有键值对作为Map.Entry的对象

  • default V getOrDefault(Object key, V defaultValue):返回映射到提供键的值,如果映射没有提供键,则返回defaultValue

  • static Map.Entry<K,V> entry(K key, V value):返回一个不可修改的Map.Entry对象,其中包含提供的keyvalue

  • static Map<K,V> copy(Map<K,V> map):将提供的Map转换为不可修改的Map

以下Map方法对于本书的范围来说太复杂了,所以我们只是为了完整起见才提到它们。它们允许组合或计算多个值,并将它们聚集在Map中的单个现有值中,或创建一个新值:

  • default V merge(K key, V value, BiFunction<V,V,V> remappingFunction):如果提供的键值对存在且值不是null,则提供的函数用于计算新值;如果新计算的值是null,则删除键值对;如果提供的键值对不存在或值是null,则提供的非空值替换当前值;此方法可用于聚合多个值;例如,可用于连接字符串值:map.merge(key, value, String::concat);我们将在第 13 章、“函数式编程”中解释String::concat的含义
  • default V compute(K key, BiFunction<K,V,V> remappingFunction):使用提供的函数计算新值
  • default V computeIfAbsent(K key, Function<K,V> mappingFunction):仅当提供的键尚未与值关联或值为null时,才使用提供的函数计算新值
  • default V computeIfPresent(K key, BiFunction<K,V,V> remappingFunction):仅当提供的键已经与值关联并且该值不是null时,才使用提供的函数计算新值

最后一组计算合并方法很少使用。到目前为止最流行的是V put(K key, V value)V get(Object key)方法,它们允许使用主要的Map功能来存储键值对并使用键检索值。Set<K> keySet()方法通常用于迭代映射的键值对,尽管entrySet()方法似乎是一种更自然的方法。举个例子:

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

for(Integer key: map.keySet()){
    System.out.print(key + ", " + map.get(key) + ", ");  
                                   //prints: 3, s3, 2, s2, 1, s1,
}
for(Map.Entry e: map.entrySet()){
    System.out.print(e.getKey() + ", " + e.getValue() + ", "); 
                                   //prints: 2, s2, 3, s3, 1, s1,
}

前面代码示例中的第一个for循环使用更广泛的方法通过迭代键来访问映射的键对值。第二个for循环遍历条目集,我们认为这是一种更自然的方法。请注意,打印出来的值的顺序与我们在映射中的顺序不同。这是因为,自 Java9 以来,不可修改的集合(即of()工厂方法产生的集合)增加了Set元素顺序的随机化。它改变了不同代码执行之间元素的顺序。这样的设计是为了确保程序员不依赖于Set元素的特定顺序,而这对于一个集合是不保证的

不可修改的集合

请注意,of()工厂方法生成的集合在 Java9 中被称为不可变,在 Java10 中被称为不可修改。这是因为不可变意味着不能更改其中的任何内容,而实际上,如果集合元素是可修改的对象,则可以更改它们。例如,让我们构建一个Person1类的对象集合,如下所示:

class Person1 {
    private int age;
    private String name;
    public Person1(int age, String name) {
        this.age = age;
        this.name = name == null ? "" : name;
    }
    public void setName(String name){ this.name = name; }
    @Override
    public String toString() {
        return "Person{age=" + age +
                ", name=" + name + "}";
    }
}

为简单起见,我们将创建一个只包含一个元素的列表,然后尝试修改该元素:

Person1 p1 = new Person1(45, "Bill");
List<Person1> list = List.of(p1);
//list.add(new Person1(22, "Bob")); //UnsupportedOperationException
System.out.println(list);        //prints: [Person{age=45, name=Bill}]
p1.setName("Kelly");       
System.out.println(list);        //prints: [Person{age=45, name=Kelly}]

如您所见,尽管无法将元素添加到由of()工厂方法创建的列表中,但是如果对元素的引用存在于列表之外,则仍然可以修改其元素。

集合工具

有两个类具有处理集合的静态方法,它们非常流行并且非常有用:

  • java.util.Collections
  • org.apache.commons.collections4.CollectionUtils

这些方法是静态的,这意味着它们不依赖于对象状态,因此它们也被称为无状态方法工具方法

java.util.Collections

Collections类中有许多方法可以管理集合、分析、排序和比较它们。其中有 70 多个,所以我们没有机会谈论所有这些问题。相反,我们将研究主流应用开发人员最常使用的:

  • static copy(List<T> dest, List<T> src):将src列表中的元素复制到dest列表中,并保留元素的顺序及其在列表中的位置;目的地dest列表大小必须等于或大于src列表大小,否则会引发运行时异常;此方法用法示例如下:
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]
  • static void sort(List<T> list):根据每个元素实现的compareTo(T)方法对列表进行排序(称为自然排序);只接受具有实现Comparable接口的元素的列表(需要实现compareTo(T)方法);在下面的示例中,我们使用List<String>因为类String机具Comparable
//List<String> list = List.of("a", "X", "10", "20", "1", "2");
List<String> list = Arrays.asList("a", "X", "10", "20", "1", "2");
Collections.sort(list);
System.out.println(list);         //prints: [1, 10, 2, 20, X, a]

请注意,我们不能使用List.of()方法创建列表,因为该列表是不可修改的,并且其顺序不能更改。另外,看看结果的顺序:数字排在第一位,然后是大写字母,然后是小写字母。这是因为String类中的compareTo()方法使用字符的代码点来建立顺序。下面是演示它的代码:

List<String> list = Arrays.asList("a", "X", "10", "20", "1", "2");
Collections.sort(list);
System.out.println(list);     //prints: [1, 10, 2, 20, X, a]
list.forEach(s -> {
    for(int i = 0; i < s.length(); i++){
        System.out.print(" " + Character.codePointAt(s, i));
    }
    if(!s.equals("a")) {
        System.out.print(",");   //prints: 49, 49 48, 50, 50 48, 88, 97
    }
});

如您所见,顺序是由组成字符串的字符的代码点的值定义的。

  • static void sort(List<T> list, Comparator<T> comparator):根据提供的Comparator对象对列表进行排序,不管列表元素是否实现了Comparable接口;例如,让我们对一个由Person类的对象组成的列表进行排序:
class Person  {
    private int age;
    private String name;
    public Person(int age, String name) {
        this.age = age;
        this.name = name == null ? "" : name;
    }
    public int getAge() { return this.age; }
    public String getName() { return this.name; }
    @Override
    public String toString() {
        return "Person{name=" + name + ", age=" + age + "}";
    }
}

这里有一个Comparator类对Person对象列表进行排序:

class ComparePersons implements Comparator<Person> {
    public int compare(Person p1, Person p2){
        int result = p1.getName().compareTo(p2.getName());
        if (result != 0) { return result; }
        return p1.age - p2.getAge();
    }
}

现在我们可以使用PersonComparePersons类,如下所示:

List<Person> persons = Arrays.asList(new Person(23, "Jack"),
        new Person(30, "Bob"), new Person(15, "Bob"));
Collections.sort(persons, new ComparePersons());
System.out.println(persons);    //prints: [Person{name=Bob, age=15}, 
                                           Person{name=Bob, age=30}, 
                                           Person{name=Jack, age=23}]

正如我们已经提到的,Collections类中还有更多的工具,因此我们建议您至少查看一次它的文档并查看所有的功能。

ApacheCommons CollectionUtils

ApacheCommons 项目中的org.apache.commons.collections4.CollectionUtils类包含静态无状态方法,这些方法是对java.util.Collections类方法的补充,它们有助于搜索、处理和比较 Java 集合。

要使用此类,您需要向 Mavenpom.xml配置文件添加以下依赖项:

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

这个类中有很多方法,随着时间的推移,可能会添加更多的方法。这些工具是在Collections方法之外创建的,因此它们更复杂、更细致,不适合本书的范围。为了让您了解CollectionUtils类中可用的方法,以下是根据功能分组的方法的简短说明:

  • 从集合中检索元素的方法
  • 向集合中添加元素或元素组的方法
  • Iterable元素合并到集合中的方法
  • 带或不带条件移除或保留元素的方法
  • 比较两个集合的方法
  • 转换集合的方法
  • 从集合中选择并过滤集合的方法
  • 生成两个集合的并集、交集或差集的方法
  • 创建不可变的空集合的方法
  • 检查集合大小和空性的方法
  • 反转数组的方法

最后一个方法可能属于处理数组的工具类。这就是我们现在要讨论的。

数组工具

有两个类具有处理集合的静态方法,它们非常流行并且非常有用:

  • java.util.Arrays
  • org.apache.commons.lang3.ArrayUtils

我们将简要回顾其中的每一项。

java.util.Arrays

我们已经用过几次了。它是数组管理的主要工具类。这个工具类过去非常流行,因为有asList(T...a)方法。它是创建和初始化集合的最简洁的方法:

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

它仍然是一种流行的创建可修改列表的方法。我们也使用它。但是,在引入了一个List.of()工厂方法之后,Arrays类的流行性大大下降。

不过,如果您需要管理数组,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():生成数组元素流或其中的一部分(由索引的范围指定);参见第 14 章、“Java 标准流”

所有这些方法都是有用的,但我们想提请您注意equals(a1, a2)方法和deepEquals(a1, a2)。它们对于数组比较特别有用,因为数组对象不能实现equals()自定义方法,而是使用Object类的实现(只比较引用)。equals(a1, a2)deepEquals(a1, a2)方法不仅允许比较a1a2引用,还可以使用equals()方法比较元素。以下是演示这些方法如何工作的代码示例:

String[] arr1 = {"s1", "s2"};
String[] arr2 = {"s1", "s2"};
System.out.println(arr1.equals(arr2));             //prints: false
System.out.println(Arrays.equals(arr1, arr2));     //prints: true
System.out.println(Arrays.deepEquals(arr1, arr2)); //prints: true

String[][] arr3 = {{"s1", "s2"}};
String[][] arr4 = {{"s1", "s2"}};
System.out.println(arr3.equals(arr4));             //prints: false
System.out.println(Arrays.equals(arr3, arr4));     //prints: false
System.out.println(Arrays.deepEquals(arr3, arr4)); //prints: true

如您所见,Arrays.deepEquals()每次比较两个相等的数组时,当一个数组的每个元素等于另一个数组在同一位置的元素时,返回true,而Arrays.equals()方法返回相同的结果,但只对一维数组。

ApacheCommons ArrayUtils

org.apache.commons.lang3.ArrayUtils类是对java.util.Arrays类的补充,它向数组管理工具箱添加了新方法,并且在可能抛出NullPointerException的情况下能够处理null。要使用这个类,您需要向 Mavenpom.xml配置文件添加以下依赖项:

<dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-lang3</artifactId>
   <version>3.8.1</version>
</dependency>

ArrayUtils类有大约 300 个重载方法,可以收集在以下 12 个组中:

  • add()addAll()insert():向数组添加元素
  • clone():克隆数组,类似Arrays类的copyOf()方法和java.lang.Systemarraycopy()方法
  • getLength():当数组本身为null时,返回数组长度或0
  • 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.Objects
  • org.apache.commons.lang3.ObjectUtils

它们在类创建期间特别有用,因此我们将主要关注与此任务相关的方法。

java.util.Objects

Objects类只有 17 个方法都是静态的。在将它们应用于Person类时,我们来看看其中的一些方法,假设这个类是集合的一个元素,这意味着它必须实现equals()hashCode()方法:

class Person {
    private int age;
    private String name;
    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }
    public int getAge(){ return this.age; }
    public String getName(){ return this.name; }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if(!(o instanceof Person)) return false;
        Person person = (Person)o;
        return age == person.getAge() &&
                Objects.equals(name, person.getName()); 
    }
    @Override
    public int hashCode(){
        return Objects.hash(age, name);
    }
}

注意,我们没有检查null的属性name,因为当任何参数为nullObject.equals()不会中断。它只是做比较对象的工作。如果其中只有一个是null,则返回false。如果两者都为空,则返回true

使用Object.equals()是实现equals()方法的一种安全方法,但是如果需要比较可能是数组的对象,最好使用Objects.deepEquals()方法,因为它不仅像Object.equals()方法那样处理null,而且还比较所有数组元素的值,即使数组是多维的:

String[][] x1 = {{"a","b"},{"x","y"}};
String[][] x2 = {{"a","b"},{"x","y"}};
String[][] y =  {{"a","b"},{"y","y"}};

System.out.println(Objects.equals(x1, x2));      //prints: false
System.out.println(Objects.equals(x1, y));       //prints: false
System.out.println(Objects.deepEquals(x1, x2));  //prints: true
System.out.println(Objects.deepEquals(x1, y));   //prints: false

Objects.hash()方法也处理空值。需要记住的一点是,equals()方法中比较的属性列表必须与作为参数传入Objects.hash()的属性列表相匹配。否则,两个相等的Person对象将具有不同的哈希值,这使得基于哈希的集合无法正常工作。

另一件值得注意的事情是,还有另一个与哈希相关的Objects.hashCode()方法,它只接受一个参数。但是它产生的值并不等于只有一个参数的Objects.hash()产生的值。例如:

System.out.println(Objects.hash(42) == Objects.hashCode(42));  
                                                        //prints: false
System.out.println(Objects.hash("abc") == Objects.hashCode("abc"));  
                                                        //prints: false

为避免此警告,请始终使用Objects.hash()

另一个潜在的混淆表现在以下代码中:

System.out.println(Objects.hash(null));      //prints: 0
System.out.println(Objects.hashCode(null));  //prints: 0
System.out.println(Objects.hash(0));         //prints: 31
System.out.println(Objects.hashCode(0));     //prints: 0

如您所见,Objects.hashCode()方法为null0生成相同的散列值,这对于一些基于散列值的算法来说是有问题的。

static <T> int compare (T a, T b, Comparator<T> c)是另一种流行的方法,它返回0(如果参数相等)或c.compare(a, b)的结果。它对于实现Comparable接口(为自定义对象排序建立自然顺序)非常有用。例如:

class Person implements Comparable<Person> {
    private int age;
    private String name;
    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }
    public int getAge(){ return this.age; }
    public String getName(){ return this.name; }
    @Override
    public int compareTo(Person p){
        int result = Objects.compare(name, p.getName(),
                                         Comparator.naturalOrder());
        if (result != 0) { 
           return result;
        }
        return Objects.compare(age, p.getAge(),
                                          Comparator.naturalOrder());
    }
}

这样,您可以通过设置Comparator.reverseOrder()值或添加Comparator.nullFirst()Comparator.nullLast()来轻松更改排序算法。

此外,我们在上一节中使用的Comparator实现可以通过使用Objects.compare()变得更加灵活:

class ComparePersons implements Comparator<Person> {
    public int compare(Person p1, Person p2){
        int result = Objects.compare(p1.getName(), p2.getName(),
                                         Comparator.naturalOrder());
        if (result != 0) { 
           return result;
        }
        return Objects.compare(p1.getAge(), p2.getAge(),
                                          Comparator.naturalOrder());
    }
}

最后,我们要讨论的Objects类的最后两个方法是生成对象的字符串表示的方法。当您需要对对象调用toString()方法,但不确定对象引用是否为null时,它们会很方便。例如:

List<String> list = Arrays.asList("s1", null);
for(String e: list){
    //String s = e.toString();  //NullPointerException
}

在前面的例子中,我们知道每个元素的确切值。但是想象一下,列表作为参数传递到方法中。然后我们被迫写下如下内容:

void someMethod(List<String> list){
    for(String e: list){
        String s = e == null ? "null" : e.toString();
    }

看来这没什么大不了的。但是在编写了十几次这样的代码之后,程序员自然会想到一种实用方法来完成所有这些,也就是说,当Objects类的以下两种方法有帮助时:

  • static String toString(Object o):当参数不是null时返回调用toString()的结果,当参数值为null时返回null

  • static String toString(Object o, String nullDefault):当第一个参数不是null时,返回调用第一个参数toString()的结果;当第一个参数值是null时,返回第二个参数值nullDefault

下面的代码演示了这两种方法:

List<String> list = Arrays.asList("s1", null);
for(String e: list){
    String s = Objects.toString(e);
    System.out.print(s + " ");          //prints: s1 null
}
for(String e: list){
    String s = Objects.toString(e, "element was null");
    System.out.print(s + " ");          //prints: s1 element was null
}

在撰写本文时,Objects类有 17 种方法。我们建议您熟悉它们,以避免在已经存在相同工具的情况下编写自己的工具

ApacheCommons ObjectUtils

上一节的最后一条语句适用于 ApacheCommons 库的org.apache.commons.lang3.ObjectUtils类,它补充了上一节中描述的java.util.Objects类的方法。本书的范围和分配的大小不允许对ObjectUtils类的所有方法进行详细的回顾,因此我们将按相关功能分组对它们进行简要的描述。要使用这个类,您需要在 Mavenpom.xml配置文件中添加以下依赖项:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8.1</version>
</dependency>

ObjectUtils类的所有方法可分为七组:

  • 对象克隆方法

  • 比较两个对象的方法

  • 比较两个对象是否相等的notEqual()方法,其中一个或两个对象可以是null

  • 几个identityToString()方法生成所提供对象的String表示,就像由toString()生成一样,这是Object基类的默认方法,并且可选地将其附加到另一个对象

  • 分析null的对象数组的allNotNull()anyNotNull()方法

  • firstNonNull()defaultIfNull()方法,它们分析一个对象数组并返回第一个非null对象或默认值

  • max()min()median()mode()方法,它们分析一个对象数组并返回其中一个对应于方法名称的对象

java.time

java.time包及其子包中有许多类。它们是作为处理日期和时间的其他(旧的包)的替代品引入的。新类是线程安全的(因此,更适合多线程处理),同样重要的是,它们的设计更加一致,更易于理解。此外,新的实现在日期和时间格式上遵循了国际标准组织ISO),但也允许使用任何其他自定义格式。

我们将描述主要的五个类,并演示如何使用它们:

  • java.time.LocalDate
  • java.time.LocalTime
  • java.time.LocalDateTime
  • java.time.Period
  • java.time.Duration

所有这些,以及java.time包的其他类,以及它的子包都有丰富的功能,涵盖了所有的实际案例。但我们不打算讨论所有这些问题;我们将只介绍基本知识和最流行的用例。

LocalDate

LocalDate类不带时间。它表示 ISO 8601 格式的日期(YYYY-MM-DD):

System.out.println(LocalDate.now()); //prints: 2019-03-04

这是在这个地方写这篇文章时的当前日期。这个值是从计算机时钟中提取的。同样,您可以使用静态now(ZoneId zone)方法获取任何其他时区的当前日期。ZoneId对象可以使用静态ZoneId.of(String zoneId)方法构造,其中String zoneIdZonId.getAvailableZoneIds()方法返回的任何字符串值:

Set<String> zoneIds = ZoneId.getAvailableZoneIds();
for(String zoneId: zoneIds){
    System.out.println(zoneId);
}

前面的代码打印了近 600 个时区 ID。以下是其中一些:

Asia/Aden
Etc/GMT+9
Africa/Nairobi
America/Marigot
Pacific/Honolulu
Australia/Hobart
Europe/London
America/Indiana/Petersburg
Asia/Yerevan
Europe/Brussels
GMT
Chile/Continental
Pacific/Yap
CET
Etc/GMT-1
Canada/Yukon
Atlantic/St_Helena
Libya
US/Pacific-New
Cuba
Israel
GB-Eire
GB
Mexico/General
Universal
Zulu
Iran
Navajo
Egypt
Etc/UTC
SystemV/AST4ADT
Asia/Tokyo

让我们尝试使用"Asia/Tokyo",例如:

ZoneId zoneId = ZoneId.of("Asia/Tokyo");
System.out.println(LocalDate.now(zoneId)); //prints: 2019-03-05

LocalDate的对象可以表示过去的任何日期,也可以表示将来的任何日期,方法如下:

  • LocalDate parse(CharSequence text):从 ISO 8601 格式的字符串构造对象(YYYY-MM-DD)

  • LocalDate parse(CharSequence text, DateTimeFormatter formatter):从字符串构造一个对象,格式由DateTimeFormatter对象指定,该对象具有丰富的模式系统和许多预定义的格式;下面是其中的一些:

  • LocalDate of(int year, int month, int dayOfMonth):从年、月、日构造对象

  • LocalDate of(int year, Month month, int dayOfMonth):从年、月(枚举常量)和日构造对象

  • LocalDate ofYearDay(int year, int dayOfYear):从一年和一年中的某一天构造一个对象窗体

下面的代码演示了前面列出的方法:

LocalDate lc1 = LocalDate.parse("2020-02-23");
System.out.println(lc1);                     //prints: 2020-02-23

LocalDate lc2 =  
          LocalDate.parse("20200223", DateTimeFormatter.BASIC_ISO_DATE);
System.out.println(lc2);                     //prints: 2020-02-23

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate lc3 =  LocalDate.parse("23/02/2020", formatter);
System.out.println(lc3);                     //prints: 2020-02-23

LocalDate lc4 =  LocalDate.of(2020, 2, 23);
System.out.println(lc4);                     //prints: 2020-02-23

LocalDate lc5 =  LocalDate.of(2020, Month.FEBRUARY, 23);
System.out.println(lc5);                     //prints: 2020-02-23

LocalDate lc6 = LocalDate.ofYearDay(2020, 54);
System.out.println(lc6);                     //prints: 2020-02-23

LocalDate对象可以提供各种值:

LocalDate lc = LocalDate.parse("2020-02-23");
System.out.println(lc);                  //prints: 2020-02-23
System.out.println(lc.getYear());        //prints: 2020
System.out.println(lc.getMonth());       //prints: FEBRUARY
System.out.println(lc.getMonthValue());  //prints: 2
System.out.println(lc.getDayOfMonth());  //prints: 23
System.out.println(lc.getDayOfWeek());   //prints: SUNDAY
System.out.println(lc.isLeapYear());     //prints: true
System.out.println(lc.lengthOfMonth());  //prints: 29
System.out.println(lc.lengthOfYear());   //prints: 366

LocalDate对象可以修改如下:

LocalDate lc = LocalDate.parse("2020-02-23");
System.out.println(lc.withYear(2021)); //prints: 2021-02-23
System.out.println(lc.withMonth(5));       //prints: 2020-05-23
System.out.println(lc.withDayOfMonth(5));  //prints: 2020-02-05
System.out.println(lc.withDayOfYear(53));  //prints: 2020-02-22
System.out.println(lc.plusDays(10));       //prints: 2020-03-04
System.out.println(lc.plusMonths(2));      //prints: 2020-04-23
System.out.println(lc.plusYears(2));       //prints: 2022-02-23
System.out.println(lc.minusDays(10));      //prints: 2020-02-13
System.out.println(lc.minusMonths(2));     //prints: 2019-12-23
System.out.println(lc.minusYears(2));      //prints: 2018-02-23

LocalDate对象可以比较如下:

LocalDate lc1 = LocalDate.parse("2020-02-23");
LocalDate lc2 = LocalDate.parse("2020-02-22");
System.out.println(lc1.isAfter(lc2));       //prints: true
System.out.println(lc1.isBefore(lc2));      //prints: false

LocalDate类中还有许多其他有用的方法。如果您要处理日期,我们建议您阅读这个类的 API 和其他类的java.time包及其子包。

LocalTime

LocalTime类包含没有日期的时间。它的方法与LocalDate类的方法类似,下面介绍如何创建LocalTime类的对象:

System.out.println(LocalTime.now());         //prints: 21:15:46.360904

ZoneId zoneId = ZoneId.of("Asia/Tokyo");
System.out.println(LocalTime.now(zoneId));   //prints: 12:15:46.364378

LocalTime lt1 =  LocalTime.parse("20:23:12");
System.out.println(lt1);                     //prints: 20:23:12

LocalTime lt2 = LocalTime.of(20, 23, 12);
System.out.println(lt2);                     //prints: 20:23:12

时间值的每个分量可以从一个LocalTime对象中提取,如下所示:

LocalTime lt2 =  LocalTime.of(20, 23, 12);
System.out.println(lt2);                     //prints: 20:23:12

System.out.println(lt2.getHour());           //prints: 20
System.out.println(lt2.getMinute());         //prints: 23
System.out.println(lt2.getSecond());         //prints: 12
System.out.println(lt2.getNano());           //prints: 0

LocalTime类的对象可以修改:

LocalTime lt2 = LocalTime.of(20, 23, 12);
System.out.println(lt2.withHour(3)); //prints: 03:23:12
System.out.println(lt2.withMinute(10)); //prints: 20:10:12
System.out.println(lt2.withSecond(15)); //prints: 20:23:15
System.out.println(lt2.withNano(300)); //prints: 20:23:12.000000300
System.out.println(lt2.plusHours(10));       //prints: 06:23:12
System.out.println(lt2.plusMinutes(2));      //prints: 20:25:12
System.out.println(lt2.plusSeconds(2));      //prints: 20:23:14
System.out.println(lt2.plusNanos(200));      //prints: 20:23:12.000000200
System.out.println(lt2.minusHours(10));      //prints: 10:23:12
System.out.println(lt2.minusMinutes(2));     //prints: 20:21:12
System.out.println(lt2.minusSeconds(2));     //prints: 20:23:10
System.out.println(lt2.minusNanos(200));     //prints: 20:23:11.999999800

LocalTime类的两个对象也可以比较:

LocalTime lt2 =  LocalTime.of(20, 23, 12);
LocalTime lt4 =  LocalTime.parse("20:25:12");
System.out.println(lt2.isAfter(lt4));       //prints: false
System.out.println(lt2.isBefore(lt4));      //prints: true

LocalTime类中还有很多其他有用的方法,如果您需要处理日期,我们建议您阅读这个类的 API 以及java.time包及其子包的其他类。

LocalDateTime

LocalDateTime类包含日期和时间,并且具有LocalDateLocalTime类所具有的所有方法,因此我们不在这里重复它们。我们只展示如何创建LocalDateTime类的对象:

System.out.println(LocalDateTime.now());       
                                   //prints: 2019-03-04T21:59:00.142804
ZoneId zoneId = ZoneId.of("Asia/Tokyo");
System.out.println(LocalDateTime.now(zoneId)); 
                                   //prints: 2019-03-05T12:59:00.146038
LocalDateTime ldt1 = LocalDateTime.parse("2020-02-23T20:23:12");
System.out.println(ldt1);                 //prints: 2020-02-23T20:23:12
DateTimeFormatter formatter =
        DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
LocalDateTime ldt2 =
        LocalDateTime.parse("23/02/2020 20:23:12", formatter);
System.out.println(ldt2);                 //prints: 2020-02-23T20:23:12
LocalDateTime ldt3 = LocalDateTime.of(2020, 2, 23, 20, 23, 12);
System.out.println(ldt3);                 //prints: 2020-02-23T20:23:12
LocalDateTime ldt4 =
        LocalDateTime.of(2020, Month.FEBRUARY, 23, 20, 23, 12);
System.out.println(ldt4);                 //prints: 2020-02-23T20:23:12

LocalDate ld = LocalDate.of(2020, 2, 23);
LocalTime lt = LocalTime.of(20, 23, 12);
LocalDateTime ldt5 = LocalDateTime.of(ld, lt);
System.out.println(ldt5);                 //prints: 2020-02-23T20:23:12

LocalDateTime类中还有很多其他有用的方法,如果您需要处理日期,我们建议您阅读这个类的 API 以及java.time包及其子包的其他类。

PeriodDuration

java.time.Periodjava.time.Duration类被设计为包含一定的时间量:

  • Period对象包含以年、月、日为单位的时间量
  • Duration对象包含以小时、分钟、秒和纳秒为单位的时间量

下面的代码演示了它们在LocalDateTime类中的创建和使用,但是LocalDate类(对于Period)和LocalTime(对于Duration中存在相同的方法:

LocalDateTime ldt1 = LocalDateTime.parse("2020-02-23T20:23:12");
LocalDateTime ldt2 = ldt1.plus(Period.ofYears(2));
System.out.println(ldt2);      //prints: 2022-02-23T20:23:12

以下方法的工作方式相同:

LocalDateTime ldt = LocalDateTime.parse("2020-02-23T20:23:12");
ldt.minus(Period.ofYears(2));
ldt.plus(Period.ofMonths(2));
ldt.minus(Period.ofMonths(2));
ldt.plus(Period.ofWeeks(2));
ldt.minus(Period.ofWeeks(2));
ldt.plus(Period.ofDays(2));
ldt.minus(Period.ofDays(2));
ldt.plus(Duration.ofHours(2));
ldt.minus(Duration.ofHours(2));
ldt.plus(Duration.ofMinutes(2));
ldt.minus(Duration.ofMinutes(2));
ldt.plus(Duration.ofMillis(2));
ldt.minus(Duration.ofMillis(2));

下面的代码演示了创建和使用Period对象的一些其他方法:

LocalDate ld1 =  LocalDate.parse("2020-02-23");
LocalDate ld2 =  LocalDate.parse("2020-03-25");
Period period = Period.between(ld1, ld2);
System.out.println(period.getDays());       //prints: 2
System.out.println(period.getMonths());     //prints: 1
System.out.println(period.getYears());      //prints: 0
System.out.println(period.toTotalMonths()); //prints: 1
period = Period.between(ld2, ld1);
System.out.println(period.getDays());       //prints: -2

Duration的对象可以类似地创建和使用:

LocalTime lt1 =  LocalTime.parse("10:23:12");
LocalTime lt2 =  LocalTime.parse("20:23:14");
Duration duration = Duration.between(lt1, lt2);
System.out.println(duration.toDays());     //prints: 0
System.out.println(duration.toHours());    //prints: 10
System.out.println(duration.toMinutes());  //prints: 600
System.out.println(duration.toSeconds());  //prints: 36002
System.out.println(duration.getSeconds()); //prints: 36002
System.out.println(duration.toNanos());    //prints: 36002000000000
System.out.println(duration.getNano());    //prints: 0

PeriodDuration类中还有很多其他有用的方法,如果您需要处理日期,我们建议您阅读这个类和java.time包及其子包的其他类的 API。

总结

本章向读者介绍了 Java 集合框架及其三个主要接口:ListSetMap。讨论了每个接口,并用其中一个实现类演示了其方法。对泛型也进行了解释和演示。必须实现equals()hashCode()方法,以便 Java 集合能够正确处理对象。

工具类CollectionsCollectionUtils有许多有用的集合处理方法,并在示例中介绍了它们,以及ArraysArrayUtilsObjectsObjectUtils

java.time包的类的方法允许管理时间/日期值,这在特定的实际代码片段中得到了演示。

在下一章中,我们将概述 Java 类库和一些外部库,包括那些支持测试的库。具体来说,我们将探讨org.junitorg.mockitoorg.apache.log4jorg.slf4jorg.apache.commons包及其子包。

测验

  1. 什么是 Java 集合框架?选择所有适用的选项:

    1. 框架集合
    2. java.util包的类和接口
    3. 接口ListSetMap
    4. 实现集合数据结构的类和接口
  2. 集合中的泛型是什么?选择所有适用的选项:

  3. 收集of()工厂方法的局限性是什么?选择所有适用的选项:

    1. 不允许null元素
    2. 不允许向初始化的集合添加元素
    3. 不允许删除初始化集合中的元素
    4. 不允许修改初始化集合的元素
  4. java.lang.Iterable接口的实现允许什么?选择所有适用的选项:

    1. 允许逐个访问集合的元素
    2. 允许在FOR语句中使用集合
    3. 允许在WHILE语句中使用集合
    4. 允许在DO...WHILE语句中使用集合
  5. 接口java.util.Collection的实现允许什么?选择所有适用的选项:

    1. 将另一个集合的元素添加到集合中
    2. 从集合中删除另一个集合的元素
    3. 只修改属于另一个集合的元素
    4. 从集合中删除不属于其他集合的对象
  6. 选择List接口方法的所有正确语句:

  7. 选择Set接口方法的所有正确语句:

  8. 选择Map接口方法的所有正确语句:

    1. int size():返回映射中存储的键值对的计数;当isEmpty()方法返回true时,该方法返回0
    2. V remove(Object key):从映射中删除键和值;返回值,如果没有键或值为null,则返回null
    3. default boolean remove(Object key, Object value):如果映射中存在键值对,则删除键值对;如果删除键值对,则返回true
    4. default boolean replace(K key, V oldValue, V newValue):如果提供的键当前映射到oldValue,则用提供的newValue替换值oldValue;如果替换了oldValue,则返回true,否则返回false
  9. 选择关于Collections类的static void sort(List<T> list, Comparator<T> comparator)方法的所有正确语句:

    1. 如果列表元素实现了Comparable接口,则对列表的自然顺序进行排序
    2. 它根据提供的Comparator对象对列表的顺序进行排序
    3. 如果列表元素实现了Comparable接口,则它会根据提供的Comparator对象对列表的顺序进行排序
    4. 它根据提供的Comparator对象对列表的顺序进行排序,无论列表元素是否实现Comparable接口
  10. 以下代码执行的结果是什么?

List<String> list1 = Arrays.asList("s1","s2", "s3");
List<String> list2 = Arrays.asList("s3", "s4");
Collections.copy(list1, list2);
System.out.println(list1);    
  1. CollectionUtils类方法的功能是什么?选择所有适用的选项: 1. 匹配Collections类方法的功能,但处理null 2. 补充了Collections类方法的功能 3. 以Collections类方法所不具备的方式搜索、处理和比较 Java 集合 4. 复制Collections类方法的功能

  2. 以下代码执行的结果是什么?

Integer[][] ar1 = {{42}};
Integer[][] ar2 = {{42}};
System.out.print(Arrays.equals(ar1, ar2) + " "); 
System.out.println(Arrays.deepEquals(arr3, arr4)); 
  1. 以下代码执行的结果是什么?
String[] arr1 = { "s1", "s2" };
String[] arr2 = { null };
String[] arr3 = null;
System.out.print(ArrayUtils.getLength(arr1) + " "); 
System.out.print(ArrayUtils.getLength(arr2) + " "); 
System.out.print(ArrayUtils.getLength(arr3) + " "); 
System.out.print(ArrayUtils.isEmpty(arr2) + " "); 
System.out.print(ArrayUtils.isEmpty(arr3));
  1. 以下代码执行的结果是什么?
 String str1 = "";
 String str2 = null;
 System.out.print((Objects.hash(str1) == 
                   Objects.hashCode(str2)) + " ");
 System.out.print(Objects.hash(str1) + " ");
 System.out.println(Objects.hashCode(str2) + " "); 
  1. 以下代码执行的结果是什么?
String[] arr = {"c", "x", "a"};
System.out.print(ObjectUtils.min(arr) + " ");
System.out.print(ObjectUtils.median(arr) + " ");
System.out.println(ObjectUtils.max(arr));
  1. 以下代码执行的结果是什么?
LocalDate lc = LocalDate.parse("1900-02-23");
System.out.println(lc.withYear(21)); 
  1. 以下代码执行的结果是什么?
LocalTime lt2 = LocalTime.of(20, 23, 12);
System.out.println(lt2.withNano(300));      
  1. 以下代码执行的结果是什么?
LocalDate ld = LocalDate.of(2020, 2, 23);
LocalTime lt = LocalTime.of(20, 23, 12);
LocalDateTime ldt = LocalDateTime.of(ld, lt);
System.out.println(ldt);                
  1. 以下代码执行的结果是什么?
LocalDateTime ldt = LocalDateTime.parse("2020-02-23T20:23:12");
System.out.print(ldt.minus(Period.ofYears(2)) + " ");
System.out.print(ldt.plus(Duration.ofMinutes(12)) + " ");
System.out.println(ldt);
```*