Collection
和
Map
可谓构成
Java
容器的两大体系,你熟知的数据结构。
ArrayList
、
LinkedList
、
HashSet
、
HashMap
、
TreeSet
、
TreeMap
、
PriorityQueue
、
Stack
都从
Collection
和
Map
实现而
来。
容器(Collection)是什么?
容器(
Collection
)是容纳数据用的。
Java
的容器
(Collection)
可以装一组对象。既然是一组对
象,那么他们就应该可以被遍历
(traverse)
。
可以被遍历的数据是可以被迭代的
(Iterable)
。可以被迭代的数据,就可以使用
for
循环进行迭代。
实现Iterable<T>接口
可以被迭代的数据需要实现
Iterable
接口,而
Iterable
内部需要实现一个迭代器。下面这段程序,
教你实现一个产生随机字符串的迭代器。
public class RandomStringGenerator<T> implements Iterable<T> {
private final List<T> list;
public RandomStringGenerator(List<T> list) {
this.list = list;
}
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
@Override
public boolean hasNext() {
return true;
}
@Override
public T next() {
return list.get((int) (list.size() * Math.random()));
}
};
}
public static void main(String[] argv) {
var list = Arrays.asList("List", "Tree", "Array");
var gen = new RandomStringGenerator<String>(list);
for(var s: gen) {
System.out.println(s);
}
}
}
容器(Collection)接口
容器都是可以被迭代的。
ArrayList
、
LinkedList
、
TreeSet
、
HashSet
、
PriorityQueue
、
Stack
都是
容器, 都可以被迭代,都可以使用
for
循环,直接遍历。
判断一个容器是不是空的
isEmpty()
方法
想知道容器的大小
size()
方法
想知道容器中有没有某个元素
contains(object)
。
将容器转化成数组
toArray()
方法
添加元素到容器
add(E e)
方法
从容器中移除一个元素
remove(object)
方法
判断一个容器的元素是否在这个容器当中
containsAll(Collection<? extends T> c)
从容器中移除一个容器的元素
removeAll(Collection<?> c)
移除不在某个容器中的元素
retainAll(Collection<?> c)
清空这个容器
clear()
以上这些函数是实现容器必须实现的。
很多实现容器的数据结构。并不是直接实现
Collection<E>
,而是再抽象出中间的接口。比如
ArrayList
的继承如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializ
public interface List<E> extends Collection<E>
上面代码中,
ArrayList<E>
先继承于
List<E>
再继承于
Collection<E>
。
集合(Set)
集合
(Set<E>)
是一种容器
(Collection<E>)
。这种容器。不仅仅可以用来存一组数据。而且他可以
保证这一组数据没有重复。
ConcurrentSkipListSet, CopyOnWriteArraySet, EnumSet, HashSet, LinkedHashSet, TreeSet
,
ImmutableCollections.SetN
这些类。都是集合。只不过他们用来容纳数据。并且保证数据不重复
的手段不一样。
实现集合最重要的是给定一个元素,集合能够快速的判断这个元素是否在集合当中。所以你看到
上面的所有的类并没有通过数组或者链表。来实现集合的方法。因为在数组和链表当中,查找一
个元素。需要遍历整个数组和链表。这样太慢了。
所以集合可能的
3
类实现是树、跳表、散列表和位运算。
这里我先简单的说一下这三种结构。树和跳表通常是二分查找结构(实现集合通常是平衡的二叉
搜索树,比如红黑树);哈希表是一个映射结构。
ConcurrentSkipListSet
跳表来实现集合。
HashSet
、
LinkedHashSet
和
ImmutableCollections.SetN
是用散列实现集合。
TreeSet
用树实现集合。
EnumSet
是一个抽象类和工厂,实际是
RegularEnumSet
,里面在用位运算实现集合。
集合
(Set)
复用容器(
Collection
)的接口即可,只不过所有的函数都要控制一下元素的唯一性。
映射(Map)
映射
(Map)
是两个集合间的对应关系。在
Java
中的
Map
将键
(Key)
映射到值
(Value)
。
Map
在
Java
中
是一个接口,具体的实现有很多,比如
HashMap
,
TreeMap ,Hashtable
,
SortedMap
等。
为了实现映射,我们需要容器存储
Key
,需要容器存储
value
,也需要容器存储
key-value
。一组
key-value
在
Java
中我们称之为一个条目
(Entry)
。
Map是不是Entry的容器?
Map
最核心的功能,是根据
Key
查找值。因此一个
Map
中。不能拥有重复的
Key
。
当然从某种角度来看,我们可以把
Map
看做存储条目的容器。接口
Map.Entry<K,V>
表示
Java
中
Map
的条目。每一个
Map
内部需要实现自己的
Entry
。
但是这样看是片面的。因为
Map
的核心能力,不是提供容器而是提供快速的数据查找能力。那本
质是映射,根据不同的
Map
实现,可能会把
Entry
存储到某个容器当中。也可能将
Key
和
Value
存
储到两个不同的容器当中。
每个
Map
要求实现
Set<Map.Entry<K, V>> entrySet()
接口,可见
Entry
在
Map
中是以集合的形式
存在(不能重复)。但是我们同样不能说
Map
是存储
Entry
的集合
(Set)
,这是因为,
Map
并没有要
求一定要将
Entry
存储下来,可以在
entrySet
中动态计算
Entry
的集合。
所以
Map
不是集合,也不是容器,它是映射。
Map
中需要容器,需要数据结构,但是具体如何去
存储、如何去查询
Map
并没有约束。
Map.Entry<K,V>接口
每一种
Map
都必须实现自己的
Map.Entry<K, V>
类型。
下面是,
Hashtable
中的一段程序,
Hashtable
通过内部类实现了自己的
Map.Entry<K, V>
。
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
Map<K,V>的接口
Map
的接口中
K
代表
Key
的类型,
V
代表值的类型。
Map
和容器非常的像,需要实现一些集合中也存在的接口函数。比如说:
int size()
boolean isEmpty()
void clear()
另外,
Map
中提供了三种提取容器的方法:
Collection<V> values()
将值提取为容器。
Set<K> keySet()
将
Key
提取为集合。
Set<Map.Entry<K, V>> entrySet()
将
Entry
提取为集合。
上面三种方法体现的是映射的三要素。
KeySet
是原集合,
values
是目标集合
,Entry
是关联关系。
最后我们来看一下
Map
几个最重要的用途。
boolean containsKey(Object key)
查找
map
中有没有某个键。
boolean containsValue(Object value)
查找
map
中有没有某个
value
。
V get(Object key)
根据某一个键拿到
value
。
V put(K key, V value)
写入一组
Key
到
value
的映射关系。
V remove(Object key)
相当于
get
和删除的结合体,拿到值删除关系。
还有批量操作
:
void putAll(Map<? extends K, ? extends V> m)
这里是批量添加。
Map的实现
Map
的诸多实现中有:
ConcurrentHashMap, ConcurrentSkipListMap, EnumMap, HashMap,
Hashtable, LinkedHashMap, Properties, TreeMap,
和集合类似
Map
最常的种实现如下:
ConcurrentHashMap
,
HashMap
、
Hashtable
、
LinkedHashMap
是基于哈希表的实现
- TreeMap是基于树的实现
- ConcurrentSkipListMap是基于跳表的实现
- EnumMap是基于位运算的实现
和集合类似,哈希表的实现是一种稀疏的结构。树的实现是一种紧密的结构。树的实现中间继承
路径中会实现
NavigableMap<K,V>
,从而实现浏览的能力。
HashMap vs Hashtable
上面诸多实现当中
HashMap
和
Hashtable
实现非常相近。这里有
2
个显著的区别:
Hashtable
中的所有对用户的方法都用
synchronized
关键字上了锁(因此线程安全)。如果
你学到了本课程后续的并发编程环节。你会知道这并不是一种好的实现方案。
HashMap
没
有做任何控制。
- HashMap允许null key/null value;
- Hashtable不允许null key/null value
总结
Java
的数据结构。但是归根结底,其实只有两类。一类就是存储数据的容器 (Collection<T>)
。
一类就是将数据映射到另一种数据的
Map<K, V>
。
Map
中依然要用到容器,用 到Iterable
。但
Map
的本质不是容器,而是映射。
这些数据结构。我觉得大家可以从两方面去掌握。一方面就是什么东西是什么。比如说
HashMap
是
Hash
实现的
Map
,
TreeSet
是
Tree
实现的
Set。
另一个需要掌握的维度就是数据结构和算法本身。
TreeSet
是二叉搜索树中的红黑树,
HashMap
是哈希表,
ConcurrentSkipListMap
是跳表,
EnumSet是位运算。