1、Set集合与Map集合
Set集合代表着元素无序、不可重复的集合,Map集合则表示由key-value对组成的集合,Map集合类似于关联数组。从表面上来看,它们之间的相似性很小,但是实际上Map集合和Set集合之间有很大的关系,可以说是,Map集合是Set集合的扩展。
1、1 Set与Map之间的关系
仔细观察Map集合集成体系中蓝色的区域,可以看到,这个Map集合的接口、实现类的类名和Set集合中的接口、实现类的类名完全一致,把Map后缀改为Set后缀即可。
从表面上来看,这两种集合并没有太多的相似之处,但如果只看Map集合中的key部分,我们发现Map集合中的key具有一个特征:所有的key不能重复,并且key之间没有顺序。也就是说,如果将Map集合中所有的key集中起来,那么key就组成了一个Set集合。实际上,Map集合提供了Set<K> KeySet()方法来返回所有key组成的Set集合。其实,从Set集合一方看,对于Map集合而言,相当于每个元素都是key-value对的Set集合。
import java.io.Serializable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
class SimpleEntry<K , V> implements Entry<K , V> , Serializable {
private final K key;
private V value;
public SimpleEntry(K key , V value) {
this.key = key;
this.value = value;
}
public SimpleEntry(Entry<? extends K, ? extends V> entry) {
this.key = entry.getKey();
this.value = entry.getValue();
}
public K getKey() { //获取key
return key;
}
public V getValue() { //获取value
return value;
}
public V setValue(V value) { //改变key-value对中的value值
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) { //根据key比较两个SimpleEntry是否相等
if ( o == this )
return true;
if ( o.getClass() == SimpleEntry.class ) {
SimpleEntry se = (SimpleEntry)o;
return se.getKey().equals(getKey());
}
return false;
}
public int hashCode() {
return key == null ? 0 : key.hashCode();
}
public String toString() {
return key + "=" + value;
}
}
public class SetMap<K , V> extends HashSet<SimpleEntry<K, V>>{
public void clear() { //实现清空所有key-value方法
super.clear();
}
public boolean containsKey(K key) { //判断是否包含某个key
return super.contains(new SimpleEntry<K , V>(key , null));
}
public boolean containsValue(V value) { //判断是否包含某个value
for ( SimpleEntry<K, V> se : this ) {
if ( se.getValue().equals(value) ) {
return true;
}
}
return false;
}
public V get(Object key) { //根据指定的key取出对应的value
for ( SimpleEntry<K, V> se : this) {
if ( se.getKey().equals(key) ) {
return se.getValue();
}
}
return null;
}
public V put(K key , V value) { //将制定的key-value对放入集合中
add(new SimpleEntry<K, V>(key , value));
return value;
}
public void putAll(Map<? extends K , ? extends V> m) {
for ( K key : m.keySet()) {
add(new SimpleEntry<K , V>(key , m.get(key)));
}
}
public V removeEntry(Object key) { //根据指定的key删除指定的key-value对
for ( Iterator<SimpleEntry<K, V>> it = this.iterator() ; it.hasNext() ; ) {
SimpleEntry<K, V> en = (SimpleEntry<K, V>)it;
if ( en.getKey().equals(key) ) {
V v = en.getValue();
it.remove();
return v;
}
}
return null;
}
public int size() { //获取该Map中包含多少了key-value对
return super.size();
}
}
上面程序中定义了一个SimpleEntry<K , V>类。当一个Set的所有集合元素都是SimpleEntry<K , V>对象时,该Set就变成了一个Map<K , V>集合。通过SetMap<K , V> extends HashSet<SimpleEntry<K, V>>派生出的子类完全可以被当成Map集合进行使用,因此上面程序也提供了Map集合中绝大部分的方法。
public class SetMapTest {
public static void main(String[] args) {
SetMap<String , Integer> scores = new SetMap<>();
scores.put("语文", 90);
scores.put("数学", 65);
scores.put("英语", 87);
System.out.println(scores);
System.out.println("size = " + scores.size());
scores.removeEntry("数学");
System.out.println("删除key为\"数学\"的Entry之后:" + scores);
System.out.println("语文成绩:" + scores.get("语文"));
System.out.println("是否包含\"英语\"key:" + scores.containsKey("英语"));
System.out.println("是否包含 90 value:" + scores.containsValue(90));
scores.clear();
System.out.println("执行clear()方法之后的集合:" + scores);
}
}
输出结果为:
[语文=90, 英语=87, 数学=65]
size = 3
删除key为"数学"的Entry之后:[语文=90, 英语=87]
语文成绩:90
是否包含"英语"key:true
是否包含 90 value:true
执行clear()方法之后的集合:[]
1、2 HashMap与HashSet
上一节将Set集合扩展成了Map集合,由于Set采用了HashSet作为具体的实现类,Hash会使用Hash算法来保存集合中的每个元素,因此扩展的Map从本质上来说是是一个HashMap。
实际上,HashSet和HashMap有很多相似的地方。对于HashSet来说,系统采用Hash算法来决定集合中每个元素的存储位置,这样可以相对来提高存、取集合元素的速度;对于HashMap来说,系统将value当成key的“附属物”,系统根据Hash算法来决定key的存储位置,这样也可以相对来提高存、取集合元素的速度,而value总是跟着key进行存储。
虽然集合说是存储的Java对象,但是实际上并不会将正在的Java对象放入Set集合中,而只是在Set集合中保留了这些对象的引用。换句话来说,Java集合实际上是多个引用变量所组成的集合,这些引用变量指向实际的Java对象。
import java.util.ArrayList;
import java.util.List;
class Orange {
double weight;
public Orange(double weight) {
this.weight = weight;
}
}
public class ListTest {
public static void main(String[] args) {
Orange a1 = new Orange(2.5);
Orange a2 = new Orange(6.8);
List<Orange> list = new ArrayList<3>();
list.add(a1);
list.add(a2);
System.out.println(list.get(0) == a1);
System.out.println(list.get(1) == a2);
}
}
输出结果为:
true
true
上面程序中创建了两个Orange对象,此时系统内存的分配示意图如下所示。
ArrayList底层是基于数组实现的,每次创建ArrayList时传入的int参数就是它所封装的数组的长度;如果创建ArrayList时没有传入int参数,那么ArrayList的初始长度就为10,也就是底层封装的数组长度为10。接着将两个Orange对象放入ArrayList中,这与Java对象放入数组中的效果完全相同:系统不会真正把Java对象放入ArrayList中,只是向ArrayList集合中存入这些Java对象的引用。此时系统内存的分配示意图如下所示。
对于HashMap而言,它的存储方式比ArrayList更复杂一些,它采用一种Hash算法来决定每一个元素的存储位置。例如1、1节中执行scores.put("语文" , 90);,系统将调用“语文”的hashCode()方法得到其hashCode值——每个Java对象都有hashCode()方法。得到这个对象的hashCode值之后,系统会根据该hashCode值来决定该元素的存储位置。下面就是Java中HashMap类的put(K key , V value)方法的源代码:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
从源代码中可以看到一个重要的内部接口Map.Entry,每个Map.Entry就是一个key-value对。当系统决定存储HashMap中的key-value对时,完全没有考虑Entry中的value,而仅仅是根据Key来计算并决定每个Entry存储位置。当程序试图将一个key-value对放入HashMap中时,首席那根据该key的hashCode()返回值决定该Entry的存储位置,如果两个Entry的key的hashCode()返回值相同,那么它们的存储位置就相同;如果这两个Entry的key通过equals比较返回true,则新添加Entry的value将覆盖集合中原有的value,但是key不会覆盖;如果两个Entry的key通过equals比较返回false,则先添加的Entry将与集合中原有的Entry形成新的Entry链,而且新添加的Entry位于Entry链的头部。
上面程序中使用的table其实就是一个普通数组,每个数组都有一个固定的长度,这个数组的长度就是HashMap的容量。HashMap包含以下3个构造器:
- HashMap():构建一个初始容量为16,负载因子为0.75的HashMap。
- HashMap(int initialCapacity):构建一个初试容量为initialCapacity,负载因子为0.75的HashMap。
- HashMap(int initialCapacity, float loadFactor)构建一个初试容量为initialCapacity,负载因子为loadFactor的HashMap。
注意:从HashMap构造器的源代码中可以看出,创建HashMap时指定的initialCapacity并不等于HashMap的实际容量。通常来说,HashMap实际容量总比initialCapacity大一些,除非指定的initialCapacity参数值恰好是2的n次方。
对于HashMap及其子类而言,均采用Hash算法来决定集合中元素的存储位置。当系统开始初始化HashMap时,系统会创建一个长度为capacity的Entry数组。这个数组里可以存储元素的位置被称为“桶(bucket)”,每个bucket都有其指定索引,系统可以根据索引快速访问该bucket里存储的元素。无论何时,HashMap的每个“桶”里只存储一个元素(即一个Entry)。由于Entry对象可以包含一个引用变量,也就是Entr构造器的最后一个参数用于指定下一个Entry,因此可能出现:HashMap的bucket中只有一个Entry,但这个Entry指向另一个Entry,这样就形成了一个Entry链,如下图所示。
当HashMap中每个bucket里存储的Entry只有单个Entry,即没有通过指针产生的Entry链,此时的HashMap性能最佳。当程序通过key取出对应的value时,系统只要先计算出该key的hashCode返回值,再根据该hashCode()返回值找出该key在table数组中的索引,然后取出该索引出的Entry,最后返回该key对应的value。
归纳起来,HashMap在底层将key-value对当成一个整体进行处理,这个整体就是一个Entry对象。HashMap底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据Hash算法来决定其存储位置;当需要取出一个Entry时,也会根据Hash算法找到其存储位置,直接取出该Entry。当创建HashMap时,有一个默认的负载因子(默认值为0.75)。这是时间和空间上的一种折中:增大负载因子可以减少Hash表所占用的空间,但会增加查询数据的时间开销;减小负载因子会提高数据查询能力,但会增加Hash表所占用的内存空间。
如果一开始就知道HashMap会保存多个key-value对时,则可以在创建时就使用较大的初始容量,如果HashMap中Entry的数量一直不会超过极限容量(capacity * load factor),HashMap就无须调用resize()方法重新分配table数组,从而保证有较好的性能。如果一开始初始容量设置太高可能会浪费空间,因此创建HashMap时初始容量的设置也需要很小心。
对于HashSet而言,它是基于HashMap实现的。HashSet底层采用HashMap来保存所有元素,因此HashSet的实现比较简单。所有放入HashMap中的几何元素实际上由HashMap的key来保存,而HashMap的value则存储了一个PRESENT,它是一个静态的Object对象。HashSet绝大部分方法都是通过调用HashMap的方法来实现的,因此HashSet和HashMap两个集合在本质上是相同的。
注意:由于HashSet的add()方法添加集合元素时是加上转变为HashMap的put()方法来添加key-value对,当新放入的HashMap的Entry中的key与集合中原有Entry的key相同(hashCode()返回值相同,通过equals比较也返回true)时,新添加的Entry的value将覆盖原来Entry的value,但key不会有任何变化。因此,如果向HashSet中添加一个已经存在的元素,新添加的集合元素(底层由HashMap的key保存)不会覆盖已有的集合元素。
import java.util.HashSet;
import java.util.Set;
class Name {
private String first;
private String last;
public Name(String first , String last) {
this.first = first;
this.last = last;
}
public boolean equals(Object o) {
if ( o == this )
return true;
if ( o.getClass() == Name.class ) {
Name name = (Name)o;
return name.first.equals(first) && name.last.equals(last);
}
return false;
}
}
public class HashSetTest {
public static void main(String[] args) {
Set<Name> s = new HashSet<>();
s.add(new Name("123" , "abc"));
System.out.println(s.contains(new Name("123" , "abc")));
}
}
输出结果:
false
上面程序中查找是否包含new Name("123" , "abc"),结果输出false。这是因为HashSet判断两个对象相等的标准除了要求通过equals()方法比较返回true之外,还要求两个对象的hashCode()返回值相同。而上面的程序中没有重写Name类的hashCode()方法,所以HashSet会把它们当成两个对象进行处理,最后结果自然就是false。
由此可见,当试图把某个类的对象当成HashMap的key,或者试图将这个类的对象放入HashSet中保存时,重写该类的equals()方法和hashCode()方法很重要,而且这两个方法的返回值必须保持一致。通常来说,所有参与计算hashCode()返回值的关键属性,都应该用作equals()比较的标准。
修改之后的程序如下所示:
import java.util.HashSet;
import java.util.Set;
class Name {
private String first;
private String last;
public Name(String first , String last) {
this.first = first;
this.last = last;
}
public boolean equals(Object o) {
if ( o == this )
return true;
if ( o.getClass() == Name.class ) {
Name name = (Name)o;
return name.first.equals(first) && name.last.equals(last);
}
return false;
}
public int hashCode() {
return first.hashCode();
}
public String toString() {
return "Name[first = " + first + ",last = " + last + "]";
}
}
public class HashSetTest2 {
public static void main(String[] args) {
Set<Name> s = new HashSet<>();
s.add(new Name("123" , "abc"));
System.out.println(s.contains(new Name("123" , "abc")));
}
}
输出结果为:
true
1、3 TreeMap与TreeSet
NavigableMap只是一个借口,因此从底层来看依然是使用TreeMap来包含Set集合中所有元素。
//TreeSet构造器部分源码
public TreeSet() {
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
有序状态,TreeSet中的所有元素总是根据指定的排序规则保持有序状态。
提示:红黑树是一种自平衡二叉查找树,树中每个节点的值,都大于或等于它的左子树中的所有节点的值,并且小于或等于它的右子树中的所有节点的值。,这样的设计确保红黑树运行时可以快速地在树种查找和定位所需要的节点。