掌握常见常用Java集合框架
说到集合框架,下面这张图一定经常会看见
初看这副图,你可能会觉得眼花缭乱,问题不大,本文这就带你去了解这副图。
1.整体感知
- 从图中可以看出,集合框架主要分为两个类型,Collection和Map , Collection 是一个存储一系列单个对象的容器,Map 是一个图,可以存储 一系列键值对。Collection 有三个子接口 List, Set, Queue
- 所以集合框架有四种具体的类型:Map, List, Set, Queue
- List代表了有序可重复集合,可直接根据元素的索引来访问;Set代表无序不可重复集合,只能根据元素本身来访问;Queue是队列集合;Map代表的是存储键值对(key-value)的集合,可根据元素的key来访问value。
2.顶层接口
Iterator Iterable ListIterator
- 先来看我们经常用到的 Iterator 接口 和它的子接口 ListIterator
Iterator 有三个主要方法
- hasNext() : 检测集合是否还有下一个元素
- next() : 返回迭代器的下一个元素,并更新迭代器的游标(类似指针)
- remove() : 将迭代器返回的元素删除
下面来通过例子来观察一下
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(89);
list.add(39);
list.add(29);
list.add(19);
list.add(29);
Iterator<Integer> it = list.iterator(); //获取迭代器
while(it.hasNext()){
Integer next = it.next();
System.out.print(next + " ");
}
Iterator<Integer> it2 = list.iterator();
while(it2.hasNext()){
if(it2.next() < 30){
it2.remove();
}
}
System.out.println();
Iterator<Integer> it3 = list.iterator(); //获取迭代器
while(it3.hasNext()){
Integer next = it3.next();
System.out.print(next + " ");
}
}
输出结果为
89 39 29 19 29
89 39
下面解释一下运行的过程,新建一个ArrayList集合,往里面添加了5个元素,第一次获取迭代器,遍历了集合,第二次获取迭代器,把集合里小于30的数删除,第三次再获取迭代器,再次遍历集合,发现小于30的数都被删除掉了。
- 下面来看一下ListIterator 接口,具体可以参考:Java 集合中关于Iterator 和ListIterator的详解
- (1)ListIterator有**add()方法,可以向List中添加对象,而Iterator不能
(2)ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()**方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
(3)ListIterator可以定位当前的索引位置,**nextIndex()和previousIndex()**可以实现。Iterator没有此功能。
(4)都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。 - 最后来看一下Iterable接口
public interface Iterable<T> {
Iterator<T> iterator();
/**
* @since 1.8
*/
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);
}
}
可以看到其实里面也提供了Iterator接口,在JDK1.8之后,iterable提供foreach遍历,也就是我们经常用的增强for循环。但究其本质,增强for循环其实底层还是用迭代器遍历。
- 最后来小结一波
- Iterator 是最常用的迭代器接口,主要提供了三个方法供我们去操作集合,hasNext(), next(), remove()。
- ListIterator则可以称的上是Iterator的增强版,但它只可以对list集合操作,在Iterator的基础上,它还提供了add()方法,hasPrevious() 和 previous() 供我们后续遍历list集合,它还可以定位一个元素的索引和修改元素。
- Iterable则提供了foreach遍历集合的方法。
3.Map集合
Map存储的是键值对<key, value> 形式的值, 每个key对应唯一一个value, 所以Map不可以有重复的key值,但存储重复的key值时,会将后来的 value 值覆盖掉原来的value值。也就是这个原因,作为key值得元素都必须重写hashcode()和equals()方法。
- HashMap 和 HashTable
HashMap是最常用的一个实现类,当存入元素时,会将key的hashcode转化为数组的索引放入对应的数组位置,查找时以同样的方式查找。
HashTable一般都用不到了,操作方法和HashMap差不多,但性能比HashMap差,主要是底层实现导致的。HashTable有一个子类叫properties,是一个key和value都是String类型得Map,主要用于读取配置文件。
两者的区别:
- HashTable是线程安全的,HashMap是线程不安全的, HashTable底层实现的时候加了synchronized关键字。
- 底层实现时,HashTable是数组+链表,HashMap是数组+链表+红黑树, 具体是当链表长度大于8时会转为红黑树,因为这样查询效率会比原来快。
- HashMap可以用null作为key,而HashTable不可以
- LinkedHashMap
LinkedHashMap是HashMap的子类,它内部有一条双向链表来维护键值对的次序,维护了Map的迭代顺序,与插入顺序一致,具体可以用来实现LRU缓存策略。 - TreeMap
TreeMap有排序的功能,底层是数组+红黑树实现的,每一个键值对是一个树节点,默认按key值排序,因此key值必须实现Comparable接口。迭代的时候输出就是按照key值得默认顺序输出。
下面来例子演示一下
public static void main(String[] args) {
Map<String, Integer> hashmap = new HashMap<>();
hashmap.put("Messi",6);
hashmap.put("Ronaldo",5);
hashmap.put("Kaka",1);
hashmap.put("Modric",1);
hashmap.put("Lingard",100);
System.out.println("用HashMap: ");
Set<Map.Entry<String, Integer>> set1 = hashmap.entrySet();
for(Map.Entry<String, Integer> s: set1){
System.out.println(s.getKey() +" "+s.getValue());
}
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("Messi",6);
linkedHashMap.put("Ronaldo",5);
linkedHashMap.put("Kaka",1);
linkedHashMap.put("Modric",1);
linkedHashMap.put("Lingard",100);
System.out.println("用LinkedHashMap: ");
Set<Map.Entry<String, Integer>> set2 = linkedHashMap.entrySet();
for(Map.Entry<String, Integer> s: set2){
System.out.println(s.getKey() +" "+s.getValue());
}
Map<String,Integer> treeMap = new TreeMap<>();
treeMap.put("Messi",6);
treeMap.put("Ronaldo",5);
treeMap.put("Kaka",1);
treeMap.put("Modric",1);
treeMap.put("Lingard",100);
System.out.println("用TreeMap: ");
Set<Map.Entry<String, Integer>> set3 = treeMap.entrySet();
for(Map.Entry<String, Integer> s: set3) {
System.out.println(s.getKey() + " " + s.getValue());
}
}
用HashMap:
Ronaldo 5
Lingard 100
Modric 1
Messi 6
Kaka 1
用LinkedHashMap:
Messi 6
Ronaldo 5
Kaka 1
Modric 1
Lingard 100
用TreeMap:
Kaka 1
Lingard 100
Messi 6
Modric 1
Ronaldo 5
用HashMap会使遍历得时候变得无序,每次遍历得时候可能都会不一样,用LinkedHashMap
则严格按照添加顺序输出, 用TreeMap时则会将key值排序在输出。
4.Set集合
Set集合就是存储一系列不重复得元素,和Map有点像,就是少了value。
Set集合和基本上差不多,有几个具体得实现类,
HashSet,是基于HashMap 实现的,
** LinkedHashSet ,是基于LinkedHashMap实现的, **
TreeSet,是基于TreeMap实现的
操作除了没了value值,和Map差不多,存储的元素都要重写hashcode()和equals()方法。
5.List集合
常见的实现有ArrayList, LinkedList, Vector, Stack,
先说一下Vector和Stack, Stack是vector 的子类,它们都是线程安全的,也正是因为这一点,使得它们现在已经过时了。
- 现在主要来说一下ArrayList和LinkedList
ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。
ArrayList 继承了 AbstractList ,并实现了 List 接口。
ArrayList 新建时默认容量为10,如果快达到了容量,就会有扩容机制将容量扩大到原来的1.5倍,所以如果我们创建ArrayList时知道要存储大小时最好指定一下大小,避免不断扩容而增大开销
LinkedList类似于 ArrayList,底层是用链表实现的,并实现了List, Deque, Cloneable, Serializable
接口,实现了Deque接口,说明Deque可以当作双端队列来使用,也就是说,既可以当作“栈”使用,又可以当作队列使用。
关于两者的区别
以下情况使用 ArrayList :
- 频繁访问列表中的某一个元素。
- 只需要在列表末尾进行添加和删除元素操作。
以下情况使用 LinkedList :
- 你需要通过循环迭代来访问列表中的某些元素。
- 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。
6.Queue集合
主要分为单向队列和双端队列,双端队列就是实现Deque接口,最常见的就是上面所说的LinkedList, 单向队列主要介绍优先队列PriorityQueue。
- 下面先来看JDK为我们提供的双端队列方法
细心的朋友可能会发现,API为每种操作都提供两种方法,那么它们有什么不同呢,调用不同方法操作失败时,结果也会不同。
失败结果 | 添加 | 删除 | 查找 |
抛出异常 | add() | remove() | get() |
False | offer() | poll() | peek() |
实现双端队列的除了LinkedList, 还有ArrayDeque, 两者的区别和LinkedList和ArrayList的区别有点像,都是一个底层是链表,一个是数组。用的不多
- 最后讲一下单向队列的PriorityQueue
PriorityQueue是基于优先堆实现的,优先队列,顾名思义它可以根据优先级来进行排序
要求添加的元素实现Comparable接口,并不可以存NULL值
下面来个例子演示一下
先定义了一个Player类,实现Comparable接口,并重写了CompareTo方法
public static void main(String[] args) {
Queue<Player> players = new PriorityQueue<>();
Player p1 = new Player("Ronaldo", 93);
Player p2 = new Player("Messi",94);
Player p3 = new Player("Neymar",92);
Player p4 = new Player("lewandovsiki",91);
Player p5 = new Player("Lingard",100);
players.add(p1);
players.add(p2);
players.add(p3);
players.add(p4);
players.add(p5);
while (!players.isEmpty()){
Player p = players.poll();
System.out.println(p);
}
}
Player{name='Lingard', ability=100}
Player{name='Messi', ability=94}
Player{name='Ronaldo', ability=93}
Player{name='Neymar', ability=92}
Player{name='lewandovsiki', ability=91}
会发现输出顺序和ability有关,这是因为内部已经根据CompareTo方法排好序了,也就是根据ability为优先级了。
最后献上一个表格来总结一下
实现类 | 增删复杂度 | 查复杂度 | 底层数据结构 | 线程安全 |
Vector | O(N) | O(1) | 数组 | 是(过时) |
ArrayList | O(N) | O(1) | 数组 | 否 |
LinkedList | O(1) | O(N) | 双向链表 | 否 |
HashSet | O(1) | O(1) | 数组+链表+红黑树 | 否 |
TreeSet | O(logN) | O(logN) | 红黑树 | 否 |
LinkedHashSet | O(1) | O(1)~O(N) | 数组 + 链表 + 红黑树 | 否 |
ArrayDeque | O(N) | O(1) | 数组 | 否 |
PriorityQueue | O(logN) | O(logN) | 堆(数组实现) | 否 |
HashTable | O(1) / O(N) | O(1) / O(N) | 数组+链表 | 是(过时) |
HashMap | O(1) ~ O(N) | O(1) ~ O(N) | 数组+链表+红黑树 | 否 |
TreeMap | O(logN) | O(logN) | 数组+红黑树 | 否 |
HashTable | O(1) / O(N) | O(1) / O(N) | 数组+链表 | 是(过时) |
HashMap | O(1) ~ O(N) | O(1) ~ O(N) | 数组+链表+红黑树 | 否 |
TreeMap | O(logN) | O(logN) | 数组+红黑树 | 否 |