集合类 类似数学中的集合概念,集合类表示一组对象,每个对象都可以称为元素 ,不同的集合性质不同,比如一些允许元素重复一些不允许,一些有序其他无序。
集合类其实就是为了更好地组织、管理和操作数据而存在的,包括列表、集合、队列、映射等。
集合根接口 Java已经把常用的集合类型都实现好了,只需要用就可以了。所有的集合类最终都是实现自集合跟接口的,比如ArrayList
类就是实现Collection
接口的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 public interface Collection <E> extends Iterable <E> { int size () ; boolean isEmpty () ; boolean contains (Object o) ; Iterator<E> iterator () ; Object[] toArray(); <T> T[] toArray(T[] a); boolean add (E e) ; boolean remove (Object o) ; boolean containsAll (Collection<?> c) ; boolean addAll (Collection<? extends E> c) ; boolean removeAll (Collection<?> c) ; default boolean removeIf (Predicate<? super E> filter) { Objects.requireNonNull(filter); boolean removed = false ; final Iterator<E> each = iterator(); while (each.hasNext()) { if (filter.test(each.next())) { each.remove(); removed = true ; } } return removed; } boolean retainAll (Collection<?> c) ; void clear () ; boolean equals (Object o) ; int hashCode () ; @Override default Spliterator<E> spliterator () { return Spliterators.spliterator(this , 0 ); } default Stream<E> stream () { return StreamSupport.stream(spliterator(), false ); } default Stream<E> parallelStream () { return StreamSupport.stream(spliterator(), true ); } }
在这个接口中,与操作集合类相关的功能都定义的比较完备,那么来看实现类。
List列表 List
列表即线性表。线性表支持随机访问,相比之前的Collection
接口功能更完善。
List
是集合类型的一个分支,主要特性有:
是一个有序的集合,插入元素默认插入尾部,按顺序从前往后存放,每个元素都有自己的特定的下标位置。
列表中允许有重复元素。
在List
接口中,定义了列表类型需要支持的全部操作,List
直接继承自前面介绍的Collection
接口,其中很多方法在List
接口中被重新定义了一次,为了更加明确方法的具体功能。
List
接口的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public interface List <E> extends Collection <E> { ... boolean addAll (int index, Collection<? extends E> c) ; ... default void replaceAll (UnaryOperator<E> operator) { Objects.requireNonNull(operator); final ListIterator<E> li = this .listIterator(); while (li.hasNext()) { li.set(operator.apply(li.next())); } } @SuppressWarnings({"unchecked", "rawtypes"}) default void sort (Comparator<? super E> c) { Object[] a = this .toArray(); Arrays.sort(a, (Comparator) c); ListIterator<E> i = this .listIterator(); for (Object e : a) { i.next(); i.set((E) e); } } ... E get (int index) ; E set (int index, E element) ; void add (int index, E element) ; E remove (int index) ; int indexOf (Object o) ; int lastIndexOf (Object o) ; ListIterator<E> listIterator () ; ListIterator<E> listIterator (int index) ; List<E> subList (int fromIndex, int toIndex) ; ... }
在List
接口中扩展了大量列表支持的操作,比如直接根据下标位置进行的CRUD操作。
ArrayList顺序表 当我们使用数组作为底层来实现这个List
接口,我们就得到了ArrayList
即顺序表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class ArrayList <E> extends AbstractList <E> implements List <E>, RandomAccess, Cloneable, java.io.Serializable { private static final int DEFAULT_CAPACITY = 10 ; ... transient Object[] elementData; private int size; public ArrayList (int initialCapacity) { if (initialCapacity > 0 ) { this .elementData = new Object [initialCapacity]; } else if (initialCapacity == 0 ) { this .elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException ("Illegal Capacity: " + initialCapacity); } } ... public boolean add (E e) { ensureCapacityInternal(size + 1 ); elementData[size++] = e; return true ; } ... private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 ; private void grow (int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1 ); if (newCapacity - minCapacity < 0 ) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0 ) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); } }
一般地,我们要使用一个集合类,会使用接口的引用 ,而不是直接使用具体实现类的引用:
1 2 3 4 5 6 7 8 9 10 11 12 import java.util.ArrayList;import java.util.List;public class Main { public static void main (String[] args) { List<String> list = new ArrayList <>(); list.add("darling" ); list.add("in" ); System.out.println(list); } }
这是Java的面向对象编程中的多态的一种应用,在这里List
是一个接口,而ArrayList
是List
接口的一种实现类。通过使用List
接口的引用,我们可以灵活地切换到其他实现了List
接口的类比如LinkedList
等,只需要简单修改实例化的部分List<String> list = new ArrayList<>();
。此外,使用接口类型的引用还有一个好处,因为我们只关心集合类应该提供哪些 由接口定义功能而不是如何实现的。
在使用Integer
时要注意传参问题:
1 2 3 4 5 6 7 8 9 10 11 12 import java.util.ArrayList;import java.util.List;public class Main { public static void main (String[] args) { List<Integer> list = new ArrayList <>(); list.add(10 ); list.remove((Integer) 10 ); System.out.println(list); } }
在这种情况下使用list.remove
的时候,我们要删除刚刚添加的10这个值,就必须注明要删除的是Integer
类的10,否则list.remove
会默认删除下标为10的元素。在这里如果不加就会出现警告:
如果我们这样写:
1 2 3 4 5 6 7 8 9 10 11 12 import java.util.ArrayList;import java.util.List;public class Main { public static void main (String[] args) { List<Integer> list = new ArrayList <>(); list.add(new Integer (10 )); list.remove(new Integer (10 )); System.out.println(list); } }
输出
发现会删除成功。因为集合类在删除元素时,只会调用equals
方法进行判断是否为指定元素而不是进行等号判断,所以需要注意,如果两个对象使用equals
方法相等,那么集合中就是相等的两个对象。这个功能在ArrayList
源码中是这样定义的:
1 2 3 4 5 6 7 8 9 10 11 12 public boolean remove (Object o) { if (o == null ) { ... } else { for (int index = 0 ; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true ; } } return false ; }
列表中允许存在相同元素,所以我们可以添加两个一样的元素:
1 2 3 4 5 6 7 8 9 10 11 12 import java.util.ArrayList;import java.util.List;public class Main { public static void main (String[] args) { List<String> list = new ArrayList <>(); list.add("darling" ); list.add("darling" ); System.out.println(list); } }
那如果我们要删除呢,是一起删除还是只删一个呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.util.ArrayList;import java.util.List;public class Main { public static void main (String[] args) { List<String> list = new ArrayList <>(); list.add("darling" ); list.add("x" ); list.add("darling" ); String str = "darling" ; list.remove(str); System.out.println(list); } }
输出
可以发现在这种情况下,只会删除排在前面的第一个元素。
集合类支持嵌套使用,一个集合中可以存放多个集合:
1 2 3 4 5 6 7 8 9 10 11 import java.util.LinkedList;import java.util.List;public class Main { public static void main (String[] args) { List<List<String>> list = new LinkedList <>(); list.add(new LinkedList <>()); System.out.println(list.get(0 ).isEmpty()); } }
在Arrays
工具类中,可以快速生成一个只读的List
:
1 2 3 4 5 6 public class Main { public static void main (String[] args) { List<String> list = Arrays.asList("A" , "B" , "C" ); System.out.println(list); } }
使用Arrays.List
生成的List
是只读的,不能进行修改操作,只能使用获取内容相关的方法,否则会抛出UnsupportedOperationException
异常:
1 2 3 4 5 6 7 public class Main { public static void main (String[] args) { List<String> list = Arrays.asList("A" , "B" , "C" ); list.add("D" ); } }
报错:
如果要生成可以正常使用的,可以将这个只读的列表作为参数传入:
1 2 3 4 5 6 7 public class Main { public static void main (String[] args) { List<String> list = new ArrayList <>(Arrays.asList("A" , "B" , "C" )); list.add("D" ); System.out.println(list); } }
输出
也可以使用静态代码块:
1 2 3 4 5 6 7 8 public static void main (String[] args) { List<String> list = new ArrayList <String>() {{ add("A" ); add("B" ); add("C" ); }}; System.out.println(list); }
LinkedList链表 另一个List
列表的实现类是LinkedList
链表,只不过相比于ArrayList
是采用链式实现的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class LinkedList <E> extends AbstractSequentialList <E> implements List <E>, Deque<E>, Cloneable, java.io.Serializable { transient int size = 0 ; transient Node<E> first; transient Node<E> last; public LinkedList () { } ... private static class Node <E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this .item = element; this .next = next; this .prev = prev; } } ... }
LinkedList
的使用和ArrayList
使用几乎相同,只不过LinkedList
不仅可以当作List
来使用,也可以作为双端队列使用,对于二者的选择需要根据具体场景的需求。
迭代器 集合类都支持使用foreach
语法:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { List<String> list = Arrays.asList("A" , "B" , "C" ); for (String str : list) { System.out.println(str); } } }
输出:
编译之后可以发现使用了一个名为Iterator
的迭代器:
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { List<String> list = Arrays.asList("A" , "B" , "C" ); Iterator var2 = list.iterator(); while (var2.hasNext()) { String s = (String)var2.next(); System.out.println(s); } }
那么迭代器是什么呢:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { List<String> list = Arrays.asList("A" , "B" , "C" ); Iterator<String> iterator = list.iterator(); } }
通过使用迭代器,就可以实现对集合中元素的遍历。
运行机制 迭代器的机制大致如下, 一个迭代器首先有一个默认指向集合中第一个元素的指针: 每一次next
操作,都会将指针后移一位,直到完成每个元素的遍历,此时再调用next
就不会再得到下一个元素。因为集合类的实现方案有很多,可能是链式存储,也有可能是数组存储,不同的实现有着不同的遍历方式,而迭代器则可以将多种多样不同的集合类遍历方式进行统一,只需要各个集合类根据自己的情况进行对应实现就行了。
接口定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public interface Iterator <E> { boolean hasNext () ; E next () ; default void remove () { throw new UnsupportedOperationException ("remove" ); } default void forEachRemaining (Consumer<? super E> action) { Objects.requireNonNull(action); while (hasNext()) action.accept(next()); } }
在ArrayList
和LinkedList
中,迭代器的实现也不同。
比如ArrayList
就是直接按下标访问:
1 2 3 4 5 public E next () { ... cursor = i + 1 ; return (E) elementData[lastRet = i]; }
而LinkedList
就是不断向后寻找结点:
1 2 3 4 5 6 public E next () { ... next = next.next; nextIndex++; return lastReturned.item; }
虽然这两种列表的实现不同,遍历方式也不同,但是都是按照迭代器的标准进行了实现。所以想要遍历集合中所有元素就可以直接使用迭代器完成而不需要关心集合类是如何实现的:
1 2 3 4 5 6 7 8 9 public class Main { public static void main (String[] args) { List<String> list = Arrays.asList("A" , "B" , "C" ); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } } }
需要注意的是,迭代器的使用是一次性的,如果需要再次进行遍历操作,就需要重新生成一个迭代器对象。
为了简便,我们可以使用foreach
语法来快速遍历集合类,效果是相同的:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { List<String> list = Arrays.asList("A" , "B" , "C" ); for (String str : list) { System.out.println(str); } } }
Java8提供了一个支持lambda表达式的forEach
方法,这个方法接受一个Consumer
也就是对每个遍历的元素进行的操作:
1 2 3 4 public static void main (String[] args) { List<String> list = Arrays.asList("A" , "B" , "C" ); list.forEach(System.out::println); }
效果也是完全相同的,因为forEach
方法内部本质上也是迭代器在处理,这个方法是在Iterable
接口中定义的:
1 2 3 4 5 6 default void forEach (Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this ) { action.accept(t); } }
Iterable接口 Iterable
接口实际上是集合类Collection
的父接口,也就是最顶层的接口,定义了迭代器生成相关的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface Iterable <T> { Iterator<T> iterator () ; default void forEach (Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this ) { action.accept(t); } } default Spliterator<T> spliterator () { return Spliterators.spliteratorUnknownSize(iterator(), 0 ); } }
得益于Iterable
提供的迭代器生成方法,实际上只要是实现了迭代器接口的类(包括自定义的)都可以使用foreach
语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package org.example;import java.util.*;public class Main { public static void main (String[] args) { Test test = new Test (); for (String str : test) { System.out.println(str); } } } class Test implements Iterable <String> { @Override public Iterator<String> iterator () { return new Iterator <String>() { @Override public boolean hasNext () { return true ; } @Override public String next () { return "TEST" ; } }; } }
我们自己编写了一个类,实现了Iterable
接口,生成了一个Iterator
对象,直接返回true
实际上是一个无限循环,每次访问都返回一个字符串TEST
。虽然这个Test
是我们自己定义的类,并不是集合类。
ListIterator ListIterator
是针对List
的强化版本,增加了更方便的操作,因为List
是有序集合,所以ListInterator
支持两种方向的遍历操作,不仅能从前向后,也能从后向前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public interface ListIterator <E> extends Iterator <E> { boolean hasNext () ; E next () ; boolean hasPrevious () ; E previous () ; int nextIndex () ; int previousIndex () ; void remove () ; void set (E e) ; void add (E e) ; }
测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Main { public static void main (String[] args) { List<String> list = new ArrayList <>(Arrays.asList("A" , "B" , "C" )); ListIterator<String> iterator = list.listIterator(); iterator.next(); iterator.set("X" ); System.out.println(list); while (iterator.hasNext()) { String str = iterator.next(); System.out.println(str); } while (iterator.hasPrevious()) { String str = iterator.previous(); System.out.println(str); } } }
输出:
因为ListIterator
至此双向遍历,所以可以反复使用。
Queue和Deque 在前面LinkedList
的接口定义中:
1 2 3 public class LinkedList <E> extends AbstractSequentialList <E> implements List <E>, Deque<E>, Cloneable, java.io.Serializable
还实现了一个Deque
接口。Deque
接口继承自Queue
接口。
Queue队列 Queue
队列接口扩展了队列的相关操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public interface Queue <E> extends Collection <E> { boolean add (E e) ; boolean offer (E e) ; E remove () ; E poll () ; E element () ; E peek () ; }
可以直接将一个LinkedList
当作一个队列使用:
1 2 3 4 5 6 7 8 9 public class Main { public static void main (String[] args) { Queue<String> queue = new LinkedList <>(); queue.offer("darling" ); queue.add("in" ); System.out.println(queue.poll()); System.out.println(queue.poll()); } }
输出:
Deque双端队列 普通的队列是: 而双端队列允许在队列的两端进行入队和出队操作:
利用这种特性,双端队列既可以当作普通队列使用,也可以当作栈来使用,Java中关于Deque
双端队列接口的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public interface Deque <E> extends Queue <E> { void addFirst (E e) ; void addLast (E e) ; boolean offerFirst (E e) ; boolean offerLast (E e) ; E removeFirst () ; E removeLast () ; E pollFirst () ; E pollLast () ; E getFirst () ; E getLast () ; E peekFirst () ; E peekLast () ; boolean removeFirstOccurrence (Object o) ; boolean removeLastOccurrence (Object o) ; ... void push (E e) ; E pop () ; ... Iterator<E> descendingIterator () ; }
可以直接把双端队列当作栈来使用:
1 2 3 4 5 6 7 8 9 public class Main { public static void main (String[] args) { Deque<String> deque = new LinkedList <>(); deque.push("A" ); deque.push("B" ); System.out.println(deque.pop()); System.out.println(deque.pop()); } }
输出:
可以发现Deque
双端队列实现了先入后出的操作。
使用反向迭代器和正向迭代器:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Main { public static void main (String[] args) { Deque<String> deque = new LinkedList <>(); deque.addLast("A" ); deque.addLast("B" ); Iterator<String> descendingIterator = deque.descendingIterator(); System.out.println(descendingIterator.next()); Iterator<String> iterator = deque.iterator(); System.out.println(iterator.next()); } }
输出:
除了LinkedList
实现队列以外,还有其他的实现类,但是并不常用,比如:
1 2 3 4 public static void main (String[] args) { Deque<String> deque = new ArrayDeque <>(); Queue<String> queue = new PriorityQueue <>(); }
优先级队列 优先级队列可以根据每个元素的优先级,对出队顺序进行调整,默认情况下按照自然顺序:
1 2 3 4 5 6 7 8 9 10 11 public class Main { public static void main (String[] args) { Queue<Integer> queue = new PriorityQueue <>(); queue.offer(10 ); queue.offer(4 ); queue.offer(5 ); System.out.println(queue.poll()); System.out.println(queue.poll()); System.out.println(queue.poll()); } }
输出
虽然我们插入的顺序是10、4、5,但是出队顺序是按照优先级进行的。
也可以自定义比较规则,需要给一个Comparator
的实现:
1 2 3 4 5 6 7 8 9 10 11 12 public class Main { public static void main (String[] args) { Queue<Integer> queue = new PriorityQueue <>((a, b) -> b - a); queue.offer(10 ); queue.offer(4 ); queue.offer(5 ); System.out.println(queue); System.out.println(queue.poll()); System.out.println(queue.poll()); System.out.println(queue.poll()); } }
输出:
可以看到,优先级队列并不是队列中所有元素都按照优先级排列的,优先级队列只是保证出队顺序是按照优先级进行的。
Set Set
集合比较特殊,先看定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public interface Set <E> extends Collection <E> { int size () ; boolean isEmpty () ; boolean contains (Object o) ; Iterator<E> iterator () ; Object[] toArray(); <T> T[] toArray(T[] a); boolean add (E e) ; boolean remove (Object o) ; boolean containsAll (Collection<?> c) ; boolean addAll (Collection<? extends E> c) ; boolean retainAll (Collection<?> c) ; boolean removeAll (Collection<?> c) ; void clear () ; boolean equals (Object o) ; int hashCode () ; @Override default Spliterator<E> spliterator () { return Spliterators.spliterator(this , Spliterator.DISTINCT); } }
可以发现,Set
接口中定义的方法都是从Collection
中直接继承的,因此Set
支持的功能和Collection
中定义的差不多,只不过Set
中:
不允许出现重复元素
不支持随机访问即无法使用下标访问
HashSet HashSet
底层是使用哈希表实现的。可以非常高效地从HashSet
中存取元素:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { Set<String> set = new HashSet <>(); System.out.println(set.add("AAA" )); System.out.println(set.add("AAA" )); System.out.println(set); } }
输出:
在Set
接口中并没有定义支持下标位置访问的添加和删除操作,只能简单地删除Set
中的某个对象:
1 2 3 4 5 6 7 8 9 public class Main { public static void main (String[] args) { Set<String> set = new HashSet <>(); System.out.println(set.add("A" )); System.out.println(set.add("B" )); System.out.println(set.remove("A" )); System.out.println(set); } }
由于底层采用哈希表实现,所以Set
无法维持插入元素的顺序:
1 2 3 4 5 6 7 public class Main { public static void main (String[] args) { Set<String> set = new HashSet <>(); set.addAll(Arrays.asList("darling" , "in" , "the" , "franxx" )); System.out.println(set); } }
1 [the, darling, in, franxx]
LinkedHashSet 如果想要使用维持顺序的Set
集合,可以使用LinkedHashSet
,底层维护的是一个LinkedHashMap
,可以在插入数据时利用链表自动维护顺序,因此这样就能保证插入顺序和最后的迭代顺序一致:
1 2 3 4 5 6 7 public class Main { public static void main (String[] args) { Set<String> set = new LinkedHashSet <>(); set.addAll(Arrays.asList("darling" , "in" , "the" , "franxx" )); System.out.println(set); } }
1 [darling, in, the, franxx]
TreeSet 还有一种Set
叫做TreeSet
,可以在元素插入时进行排序:
1 2 3 4 5 6 7 8 9 public class Main { public static void main (String[] args) { Set<Integer> set = new TreeSet <>(); set.add(3 ); set.add(1 ); set.add(2 ); System.out.println(set); } }
输出:
可以看到,不是按照插入顺序,而是按照数字的大小进行排序。
当然我们也可以自定义排序规则:
1 2 3 4 5 6 7 8 9 public class Main { public static void main (String[] args) { Set<Integer> set = new TreeSet <>((a, b) -> b - a); set.add(3 ); set.add(1 ); set.add(2 ); System.out.println(set); } }
输出:
Set
更重要的是性质与使用方法。
Map映射 Map
通过保存键值对的形式来存储映射关系,可以轻松地通过键找到对应的映射值。Map
并不是Collection
体系下的接口,而是单独的一个体系,因为操作特殊。
Map
接口的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public interface Map <K,V> { int size () ; boolean isEmpty () ; boolean containsKey (Object key) ; boolean containsValue (Object value) ; V get (Object key) ; V put (K key, V value) ; V remove (Object key) ; void putAll (Map<? extends K, ? extends V> m) ; void clear () ; Set<K> keySet () ; Collection<V> values () ; Set<Map.Entry<K, V>> entrySet(); interface Entry <K,V> { K getKey () ; V getValue () ; V setValue (V value) ; boolean equals (Object o) ; int hashCode () ; ... } ... }
尝试使用HashMap
:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { Map<Integer, String> map = new HashMap <>(); map.put(1 , "Keith" ); map.put(2 , "Flick" ); System.out.println(map.get(2 )); } }
输出:
Map
中无法添加相同的键,如果出现相同的键,那么第二次出现的值会覆盖掉之前的:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { Map<Integer, String> map = new HashMap <>(); map.put(1 , "Keith" ); map.put(2 , "Flick" ); System.out.println(map.get(2 )); } }
输出:
为了防止意外将之前的键值对覆盖掉,可以使用putIfAbsent()
方法,:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { Map<Integer, String> map = new HashMap <>(); map.put(1 , "Keith" ); map.putIfAbsent(1 , "Flick" ); System.out.println(map.get(1 )); } }
输出:
如果使用get
获取一个不存在的映射,那么会返回null
作为结果:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { Map<Integer, String> map = new HashMap <>(); map.put(1 , "Keith" ); map.putIfAbsent(1 , "Flick" ); System.out.println(map.get(2 )); } }
输出:
也可以为这种情况添加一个预备方案,当Map
中不存在时可以使用getOrDefault
返回一个备选的返回值:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { Map<Integer, String> map = new HashMap <>(); map.put(1 , "Keith" ); map.putIfAbsent(2 , "Flick" ); System.out.println(map.getOrDefault(3 , "Genie" )); } }
输出:
与Set
类似,因为HashMap
底层通过哈希表实现,所以不维护顺序,我们在获取所有键和值时可能是乱序的(根据Java版本有所不同):
1 2 3 4 5 6 7 8 9 10 11 public class Main { public static void main (String[] args) { Map<Integer, String> map = new HashMap <>(); map.put(1 , "Keith" ); map.put(2 , "Flick" ); map.put(3 , "Genie" ); System.out.println(map); System.out.println(map.keySet()); System.out.println(map.values()); } }
想维护顺序可以使用LinkedHashMap
。
HashMap 从最简单的HashMap
开始,底层采用的是哈希表(哈希表参考之前的笔记)。哈希表可能出现哈希冲突,这样保存到元素数量就会存在限制,而可以通过链地址法 解决这种问题,如下图:
实际上这个表就是一个存放头结点的数组+若干结点,而HashMap
也是一样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class HashMap <K,V> extends AbstractMap <K,V> implements Map <K,V>, Cloneable, Serializable { ... static class Node <K,V> implements Map .Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; ... } ... transient Node<K,V>[] table; final float loadFactor; public HashMap () { this .loadFactor = DEFAULT_LOAD_FACTOR; } ... }
可以看到,HashMap
底层结构和之前的哈希表差不多只不过多了一些东西:
HashMap
支持自动扩容。
HashMap
不只使用链地址法,当链表长度达到一定限制时会自动转变为效率更高的红黑树结构。
HashMap
中的put
方法的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); } final V putVal (int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0 ) n = (tab = resize()).length; if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null ); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this , tab, hash, key, value); else { for (int binCount = 0 ; ; ++binCount) { if ((e = p.next) == null ) { p.next = newNode(hash, key, value, null ); if (binCount >= TREEIFY_THRESHOLD - 1 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
根据这段put
的代码可知,当我们成功插入一个键值对时会得到一个null
返回值,而如果插入冲突则会得到一个被覆盖的值:
1 2 3 4 5 6 7 public class Main { public static void main (String[] args) { Map<Integer, String> map = new HashMap <>(); System.out.println(map.put(0 , "darling" )); System.out.println(map.put(0 , "in" )); } }
输出:
当HashMap
的一个链表长度过大时会自动转换为红黑树:
但是这种方法治标不治本,受限制的一直是底层哈希表的长度,我们还需要进一步对底层的这个哈希表进行扩容才能从根本上解决问题,这时需要用到resize()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null ) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0 ; if (oldCap > 0 ) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1 ) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1 ; } else if (oldThr > 0 ) newCap = oldThr; else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int )(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } ... threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node [newCap]; table = newTab; if (oldTab != null ) { ... } }
LinkedHashMap LinkedHashMap
直接继承自HashMap
,具有HashMap
的全部性质,同时保留了插入顺序。这样在遍历LinkedHashMap
时顺序就和插入循序一致。当然也可以使用访问顺序,也就是刚访问过的元素会被排在最后一位。
TreeMap 一种比较特殊的Map
,就像名字TreeMap
一样,内部直接维护了一个没有哈希表的红黑树。因为它会将我们插入的结点按照规则进行排序,所以说我们可以自定义比较规则:
1 2 3 4 5 6 7 8 9 public class Main { public static void main (String[] args) { Map<Integer, String> map = new TreeMap <>((a, b) -> b - a); map.put(1 , "darling" ); map.put(3 , "in" ); map.put(0 , "the" ); System.out.println(map); } }
输出:
1 {3=in, 1=darling, 0=the}
Set与Map 回头来看上个章节的HashSet
的底层实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class HashSet <E> extends AbstractSet <E> implements Set <E>, Cloneable, java.io.Serializable { private transient HashMap<E,Object> map; private static final Object PRESENT = new Object (); public HashSet () { map = new HashMap <>(); } ... public Iterator<E> iterator () { return map.keySet().iterator(); } public int size () { return map.size(); } public boolean isEmpty () { return map.isEmpty(); } }
可以发现,虽然外壳是HashSet
,但实际上内部维护的时一个共享值的HashMap
。所以HashSet
使用了HashMap
的数据结构实现了Set
定义的功能。
再看TreeSet
,实际上用的就是TreeMap
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class TreeSet <E> extends AbstractSet <E> implements NavigableSet <E>, Cloneable, java.io.Serializable { private transient NavigableMap<E,Object> m; private static final Object PRESENT = new Object (); ... public TreeSet () { this (new TreeMap <E,Object>()); } ... }
Map提供的方法
Stream 流 Java8 添加了一个新的抽象称为流Stream
。Stream
使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
将要处理的元素集合看作一种流,流在管道里传输,并且可以在管道的节点上进行处理,比如筛选、排序、聚合等。元素流在管道中经过中间操作 的处理,最后由最终操作 得到前面处理的结果:
正常来说,我们要创建一个列表,把A
、B
、C
放进去,然后再删除B
,可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Main { public static void main (String[] args) { List<String> list = new ArrayList <>(); list.add("A" ); list.add("B" ); list.add("C" ); Iterator iterator = list.iterator(); while (iterator.hasNext()) { if (iterator.next().equals("B" )) iterator.remove(); } System.out.println(list); } }
输出:
如果使用stream
流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Main { public static void main (String[] args) { List<String> list = new ArrayList <>(); list.add("A" ); list.add("B" ); list.add("C" ); list = list .stream() .filter(e -> !e.equals("B" )) .collect(Collectors.toList()); System.out.println(list); } }
输出:
换一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Main { public static void main (String[] args) { List<Integer> list = new ArrayList <>(); list.add(1 ); list.add(2 ); list.add(3 ); list.add(3 ); list = list .stream() .distinct() .sorted((a, b) -> b -a) .map(e -> e + 1 ) .limit(2 ) .collect(Collectors.toList()); System.out.println(list); } }
输出:
当遇到大量操作时可以使用Stream
流快速实现要求。
需要注意的是,在整个语句中可能不是依次执行的。实际上Stream
会先记录每一步操作而不是直接开始执行内容,当整个链式调用完成后才会依次进行。
可以使用Stream
流来生成符合条件的随机数:
1 2 3 4 5 6 7 8 9 10 11 public class Main { public static void main (String[] args) { Random random = new Random (); random .ints(-100 , 100 ) .limit(10 ) .filter(i -> i < 0 ) .sorted() .forEach(System.out :: println); } }
输出:
我们可以生成一个统计实例来帮助我们快速进行统计:
1 2 3 4 5 6 7 8 9 10 11 12 public class Main { public static void main (String[] args) { Random random = new Random (); IntSummaryStatistics statistics = random .ints(0 , 100 ) .limit(100 ) .summaryStatistics(); System.out.println(statistics.getMax()); System.out.println(statistics.getCount()); System.out.println(statistics.getAverage()); } }
输出:
普通的List
只需要一个方法就可以转换为更加方便的IntStream
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Main { public static void main (String[] args) { List<Integer> list = new ArrayList <>(); list.add(1 ); list.add(2 ); list.add(3 ); list.add(4 ); list.add(1 ); list.stream() .mapToInt(i -> 1 ) .summaryStatistics(); } }
还可以通过flat
对整个流进行进一步的细分:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Main { public static void main (String[] args) { List<String> list = new ArrayList <>(); list.add("A, B" ); list.add("C, D" ); list.add("E, F" ); list = list .stream() .flatMap(e -> Arrays.stream(e.split(", " ))) .collect(Collectors.toList()); System.out.println(list); } }
输出:
也可以只通过Stream
完成所有数字的和,使用reduce
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Main { public static void main (String[] args) { List<Integer> list = new ArrayList <>(); list.add(1 ); list.add(2 ); list.add(3 ); int sum = list .stream() .reduce((a, b) -> a + b) .get(); System.out.println(sum); } }
输出:
Collections工具类 JDK实现的Collection
类是专门用于集合的工具类。
如果想快速求得List
中的最大值和最小值:
1 2 3 4 5 6 7 8 9 10 11 public class Main { public static void main (String[] args) { List<Integer> list = new ArrayList <>(); list.add(1 ); list.add(2 ); list.add(3 ); System.out.println(Collections.max(list)); System.out.println(Collections.min(list)); } }
输出:
可以对一个实现Comparable
接口的集合类进行二分搜索:
1 2 3 4 public static void main (String[] args) { List<Integer> list = Arrays.asList(2 , 3 , 8 , 9 , 10 , 13 ); System.out.println(Collections.binarySearch(list, 8 )); }
输出:
可以对集合的元素进行快速填充,需要注意的是这个填充是对集合中已有的元素进行覆盖:
1 2 3 4 5 6 7 public class Main { public static void main (String[] args) { List<Integer> list = new ArrayList <>(Arrays.asList(1 ,2 ,3 ,4 ,5 )); Collections.fill(list, 6 ); System.out.println(list); } }
输出:
如果集合中本身没有元素则fill
方法不会生效。
有时我们需要生成一个空的集合类返回,可以使用emptyList
等快速生成一个只读的空集合:
1 2 3 4 5 6 7 public class Main { public static void main (String[] args) { List<Integer> list = Collections.emptyList(); list.add(10 ); } }
可以寻找子集合的位置:
1 2 3 4 5 6 public class Main { public static void main (String[] args) { List<Integer> list = new ArrayList <>(Arrays.asList(1 ,2 ,3 ,4 ,5 )); System.out.println(Collections.indexOfSubList(list, Arrays.asList(4 , 5 ))); } }
输出:
得益于泛型的类型擦除机制,实际上最后只要是Object的实现类都可以保存到集合类中,那么就会出现这种情况:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { List list = new ArrayList <>(Arrays.asList(1 ,2 ,3 ,4 ,5 )); list.add("aaa" ); System.out.println(list); } }
输出:
由于泛型机制上的一些漏洞,实际上对应类型的集合类有可能会存放其他类型的值,泛型的类型检查只存在于编译阶段,只要我们绕过这个阶段,在实际运行时,并不会真的进行类型检查,要解决这种问题很简单,就是在运行时进行类型检查:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { List list = new ArrayList <>(Arrays.asList(1 ,2 ,3 ,4 ,5 )); list = Collections.checkedList(list, Integer.class); list.add("aaa" ); System.out.println(list); } }
checkedList
等可以将给定集合类进行包装,在运行时同样会进行类型检查,如果通过上面的漏洞插入一个本不应该是当前类型集合支持的类型,那么会直接抛出类型转换异常: