文章目录
Java集合框架支持以下两种类型的容器:
- 一种是为了存储一个元素集合,称为集合(collection)
- 另一种是为了存储键 / 值对,称为映射(map)
注:所有 Collection 都实现了 Iterable 接口
Java 库中的具体集合
集合类型 | 描述 |
---|---|
ArrayList | 一种可以动态增长和缩减的索引序列 |
LinkedList | 一种可以在任何位置进行高效地插入和删除操作的有序序列 |
ArrayDeque | 一种用循环数组实现的双端队列 |
HashSet | 一种没有重复元素的无序集合 |
TreeSet | 一种有序集 |
EnumSet | 一种包含枚举类型值的集 |
LinkedHashSet | 一种可以记住元素插入次序的集 |
PriorityQueue | 一种允许高效删除最小元素的集合 |
HashMap | 一种存储键 / 值关联的数据结构 |
TreeMap | 一种键值有序排列的映射表 |
EnumMap | 一种键值属于枚举类型的映射表 |
LinkedHashMap | 一种可以记住键 / 值项添加次序的映射表 |
WeakHashMap | 一种其值无用武之地后可以被垃圾回收器回收的映射表 |
IdentityHashMap | 一种用 == 而不是用 equals 比较键值得映射表 |
集合(Collection)
1 线性表(List)
- ArrayList
基于动态数组实现,支持随机访问。
因为 ArrayList 是基于数组实现的,所以支持快速随机访问。RandomAccess 接口标识着该类支持快速随机访问。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
数组的默认大小为 10
private static final int DEFAULT_CAPACITY = 10;
-
Vector:和 ArrayList 类似,但它是线程安全的。
-
LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
每个链表存储了 first 和 last 指针:
transient Node<E> first;
transient Node<E> last;
---------------------------------------LinkedList存储结构图----------------------------
两种类型比较 | ArrayList | LinkedList |
---|---|---|
获取指定元素 | 速度很快 | 需要从头开始查找元素 |
添加元素到末尾 | 速度很快 | 速度很快 |
在指定位置添加/删除 | 需要移动元素 | 不需要移动元素 |
内存占用 | 少 | 较大 |
通常情况下,我们总是优先使用ArrayList
List
接口允许我们添加重复的元素,即List
内部的元素可以重复
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("aaa");
for (String str:list
) {
System.out.println(str);
}
aaa
bbb
aaa
List
还允许添加null:
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("apple");
list.add(null);
list.add("orange");
System.out.println(list.get(1));
}
}
null
如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的情况。
List<String> list = ...;
ListIterator<String> iter1 = list.listIterator();
ListIterator<String> iter2 = list.listIterator();
iter1.next();
iter2.remove();
iter2.next();//throws ConcurrentModificationException 并发修改异常
2 规则集(Set)
Set:不保证有序,不可重复
List:有顺序,可重复
- 如果我们只需要存储不重复的key,并不需要存储映射的value,那么就可以使用Set
- 因为放入Set的元素和Map的key类似,都要正确实现equals()和hashCode()方法,否则该元素无法正确地放入Set
Set接口并不保证有序,而SortedSet接口则保证元素是有序的
- HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口。
基于哈希表实现(散列表用链表数组实现,标准类库使用的桶数是 2 的幂,默认值 16),支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
如果要对散列表再散列,就需要创建一个桶数更多的表。**装填因子(load factor)**决定何时对散列表进行再散列。例如,如果装填因子为 0.75(默认值)而表中超过 75% 的位置已经填入元素,这个表就会用双倍的桶数自动地进行再散列 - TreeSet是有序的,因为它实现了SortedSet接口。
基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。 - LInkedHashSet 用一个链表实现来扩展 HashSet 类,其中元素按照它们插入规则集的顺序获取。
具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序
用一张图表示:
我们来看下面这个例子
package Collection.Set;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
/**
* Set:不保证有序,不可重复
* List:有顺序,可重复
* HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口;
* TreeSet是有序的,因为它实现了SortedSet接口。
* TreeSet底层实际是用TreeMap实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。
* TreeSet内部需要对存储的元素进行排序,因此,我们对应的类需要实现Comparable接口。
* 这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。
* 如果我们只需要存储不重复的key,并不需要存储映射的value,那么就可以使用Set
*/
public class TestTreeSet {
public static void main(String[] args) {
Set<String> set = new TreeSet<>();//TreeSet有顺序,这个顺序是元素的排序顺序
System.out.println(set.add("cc"));//true
System.out.println(set.add("aa"));//true
System.out.println(set.add("cc"));//false,不可重复
System.out.println(set);
set.remove("cc");
System.out.println(set);
System.out.println("---------------");
Set<String> set1 = new TreeSet<>();
set1.add("cc");
set1.addAll(set);
System.out.println(set1);
System.out.println("---------------");
/**HashSet无序,注意输出的顺序既不是添加的顺序,也不是String排序的顺序,在不同版本的JDK中,这个顺序也可能是不同的。*/
Set<String> set2 = new HashSet<>();
set2.add("apple");
set2.add("orange");
set2.add("banana");
for (String s:set2
) {
System.out.println(s);
}
System.out.println("---------------");
Set<String> set3 = new LinkedHashSet<>(set2);
set3.forEach(e -> System.out.println(e));
}
}
true
true
false
[aa, cc]
[aa]
---------------
[aa, cc]
---------------
orange
banana
apple
---------------
orange
banana
apple
TreeSet底层实际是用TreeMap实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。
TreeSet内部需要对存储的元素进行排序,因此,我们对应的类需要实现Comparable
接口或Comparator
接口
这样,才能根据 compareTo(T o) 或 compare(T o1,T o2 )方法比较对象之间的大小,才能进行内部排序。
3 队列(Queue)
-
LinkedList:可以用它来实现双向队列。
-
PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
public class Test {
public static void main(String[] args) {
Queue<String> queue = new PriorityQueue<>();
queue.offer("apple");
queue.offer("banana");
queue.offer("orange");
queue.offer("blue");
while (queue.size()>0) {
System.out.println(queue.remove() + " ");
}
}
apple
banana
blue
orange
优先队列使用Comparable
以元素的自然顺序进行排序。拥有最小数值的元素被赋予最高优先级,因此最先从队列中删除。如果几个元素具有相同的最高优先级,则任意选择一个。
映射(Map)
Map
中不存在重复的key
,因为放入相同的key
,只会把原有的key-value
对应的value
给替换掉,虽然key
不能重复,但value
是可以重复的
Map<String, Integer> map = new HashMap<>();
map.put("apple", 123);
map.put("pear", 123); // ok
Map不保证顺序,遍历Map时,不可假设输出的key是有序的!
1 HashMap
- 存储结构
内部包含了一个 Entry 类型的数组 table
transient Entry[] table;
Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。
HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry
HashMap 类对于定位一个值、插入一个条目以及删除一个条目而言是高效的。
更新映射项
正常情况下,可以得到一个与一个键关联的原值,完成更新,再放回更新后的值。不过,必须考虑一个特殊情况,即键第一次出现。
使用一个映射统计一个单词在文件中出现的频度,看到一个单词(word)时,我们将计数器增 1, merge 方法可以简化这个操作。如果键原先不存在,下面的调用:
counts.merge(word,1,Integer :: sum);
将把 word 与 1 关联,否则使用 Integer :: sum 函数组合原值和 1*(将原值和 1 求和)
2 TreeMap
TreeMap 在内部会对 Key 进行排序,这种 Map 就是 SortedMap。注意到SortedMap 是接口,它的实现类是 TreeMap
-
使用TreeMap时,放入的Key必须实现Comparable接口。
String、Integer这些类已经实现了Comparable接口,因此可以直接作为Key使用。作为Value的对象则没有任何要求。 -
HashMap效率高于TreeMap;在需要排序的Map时才选用TreeMap。
-
如果作为Key的class没有实现Comparable接口,那么,必须在创建TreeMap时同时指定一个自定义排序算法:
public class TestTreeMap {
public static void main(String[] args) {
Map<Integer, String> map = new TreeMap<>();
map.put(20, "aa");
map.put(10, "bb");
map.put(5, "ccc");
map.put(5, "cc");//不可重复,直接覆盖
/**按照key递增的方式排序*/
for (Integer key : map.keySet()
) {
System.out.println(key + "-->" + map.get(key));
}
Map<Emp, String> map02 = new TreeMap<>();
map02.put(new Emp(10, "小涛", 10000), "小涛好样的");
map02.put(new Emp(20, "小小涛", 100000), "小小涛好样的");
map02.put(new Emp(30, "小小小涛", 1000000), "小小小涛好样的");
map02.put(new Emp(003, "子涛", 1000000), "子涛好样的");
for (Emp key : map02.keySet()
) {
System.out.println(key + "-->" + map02.get(key));
}
}
}
class Emp implements Comparable<Emp> {
private int id;
private String name;
private double salary;
public Emp(int id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
/**
* 这里用到compareTO方法:负整数->小于,0->等于,正整数->大于
*/
@Override
public int compareTo(Emp emp) {
if (this.salary < emp.salary) {
return -1;
} else if (this.salary > emp.salary) {
return 1;
} else {
if (this.id < emp.id) {
return -1;
} else if (this.id > emp.id) {
return 1;
} else {
return 0;
}
}
}
@Override
public String toString() {
return "id: " + id + ";name: " + name + ";salary:" + salary;
}
}
5-->cc
10-->bb
20-->aa
id: 10;name: 小涛;salary:10000.0-->小涛好样的
id: 20;name: 小小涛;salary:100000.0-->小小涛好样的
id: 3;name: 子涛;salary:1000000.0-->子涛好样的
id: 30;name: 小小小涛;salary:1000000.0-->小小小涛好样的
注意到Emp类并未覆写equals()和hashCode(),因为TreeMap不使用equals()和hashCode()
3 LinkedHashMap
-
LinkedHashMap类用链表实现来扩展HashMap类,它支持映射中条目的排序。
-
HashMap类中的条目是无序的,但在LindedHashMap中,元素既可以按它们被最后一次访问时的顺序,从最早到最晚(称为访问顺序,而不是插入顺序)排序。
-
无参构造方法是以插入顺序来创建LinkedHashMap的。
要按访问顺序创建LinkedHashMap对象,可以使用构造方法LinkedHashMap(initialCapacity,loadFactor,true).
import java.util.*;
public class Test {
public static void main(String[] args) {
Map<String,Integer> hashMap = new HashMap<>();
hashMap.put("Smith", 30);
hashMap.put("Anderson", 31);
hashMap.put("Lewis", 29);
hashMap.put("Cook", 29);
System.out.println(hashMap + "\n");//随机排序
Map<String,Integer> treeMap = new TreeMap<>(hashMap);
System.out.println(treeMap);//按键的升序排列
//遍历Map写法一
treeMap.forEach((name,age) -> System.out.println("name: " + name + "; age: " + age));
System.out.println();
//写法二
Set<Map.Entry<String,Integer>> ss = treeMap.entrySet();
for (Iterator ite = ss.iterator();ite.hasNext();){
Map.Entry e = ((Map.Entry) ite.next());
System.out.println("name\t" + e.getKey() + "-----" + "age\t" + e.getValue());
}
System.out.println();
Map<String,Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("Smith",30);
linkedHashMap.put("Anderson",31);
linkedHashMap.put("Lewis",29);
linkedHashMap.put("Cook",29);
System.out.println(linkedHashMap);//按插入顺序排列
}
}
{Lewis=29, Smith=30, Cook=29, Anderson=31}
{Anderson=31, Cook=29, Lewis=29, Smith=30}
name: Anderson; age: 31
name: Cook; age: 29
name: Lewis; age: 29
name: Smith; age: 30
name Anderson-----age 31
name Cook-----age 29
name Lewis-----age 29
name Smith-----age 30
{Smith=30, Anderson=31, Lewis=29, Cook=29}
如输出所示,HashMap 中条目的顺序是随机的,而TreeMap 中的条目是按键的升序排列的,LinkedHashMap 中的条目则是按元素插入顺序排列的。
三者的使用场景
如果更新映射时不需要保持映射中元素的顺序,就使用 HashMap;如果需要保持映射中元素的插入顺序或访问顺序,就使用LinkedHashMap;如果需要使映射按照键排序,就使用 TreeMap。
补充:
1、WeakHashMap
WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。
WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收。
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
2、枚举集与映射
EnumSet 是一个枚举类型元素及的高效实现。由于枚举类型只有有限个实例,所以 EnumSet 内部用位序列实现。如果对应的值在集中,则相应的位被置为1。
EnumSet 类没有公共的构造器。可以使用静态工厂方法构造这个集:
enum Weekday{Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday};
EnumSet<Weekday> always = EnumSet.allof(Weekday.class);//创建一个包含指定元素类型中所有元素的枚举集。
EnumSet<Weekday> never = EnumSet.noneof(Weekday.class);//使用指定的元素类型创建一个空的枚举集。
EnumSet<Weekday> workday = EnumSet.range(Weekday.Monday,Weekday.Tuesday);
EnumSet<Weekday> mwf = EnumSet.of(Weekday.Monday,Weekday.Wednesday,Weekday.Friday);
EnumMap 是一个键类型为枚举类型的映射。它可以直接且高效地用一个值数组实现。在使用时,需要在构造器中指定键类型:
EnumMap<Weekday,Employee> personInCharge = new EnumMap<>(Weekday.class);
3、同步视图
如果由多个线程访问集合,就必须确保集不会被意外地破坏。例如,如果一个线程试图将元素添加到散列表中,同时另一个线程正在对散列表进行再散列,其结果将是灾难性的。
类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类。
Map<String,Employee> map = Collections.synchronizedMap(new HashMap<String,Employee>());
现在就可以由多线程访问 map 对象了。像 get 和 put 这类方法都是同步操作的。