四、08【Java常用类】之Java集合类Map接口

今天的博客主题

       Java常用类 ——》Java集合类Map接口


Map

public interface Map<K,V> { ... }

Map接口描述

将键映射到值的对象。映射不能包含重复的键;每个键最多只能映射到一个值。

这个接口取代了Dictionary类,Dictionary类是一个完全抽象的类,而不是接口。

映射接口提供三个集合视图,允许将映射的内容视为一组键、一组值或一组键-值映射。映射的顺序定义为映射集合视图上的迭代器返回其元素的顺序。一些映射实现,比如TreeMap类,对它们的顺序做出了特定的保证;其他的,比如HashMap类,则没有。

注意:如果可变对象用作映射键,则必须非常小心。当对象是映射中的键时,如果以影响等于比较的方式更改对象的值,则不指定映射的行为。这一禁令的一个特殊情况是,map不允许将其本身作为密钥。虽然允许映射将自身包含为一个值,但要特别小心:equals和hashCode方法不再在这样的映射上定义良好。

所有通用映射实现类都应提供两个“标准”构造函数:一个创建空映射的void(无参数)构造函数,一个具有map类型的单个参数的构造函数,该构造函数创建具有与其参数相同的键值映射的新映射。实际上,后一个构造函数允许用户复制任何映射,生成所需类的等效映射。无法强制执行此建议(因为接口不能包含构造函数),但JDK中的所有通用映射实现都符合。

此接口中包含的“破坏性”方法(即修改其操作的映射的方法)被指定为在此映射不支持该操作时引发UnsupportedOperationException。如果是这种情况,则如果调用对映射没有影响,则这些方法可能(但不是必需)抛出unsupportedperationexception。例如,如果要“叠加”其映射的映射为空,则在不可修改的映射上调用putAll(Map)方法可能会引发异常,但不需要引发异常。

一些映射实现对它们可能包含的键和值有限制。例如,有些实现禁止空键和值,有些实现对键的类型有限制。尝试插入不合格的键或值会引发未经检查的异常,通常是NullPointerException或ClassCastException。尝试查询是否存在不合格的键或值可能会引发异常,或者它可能只是返回false;有些实现将显示前一种行为,有些则显示后一种行为。更一般地,如果尝试对不合格的键或值执行操作,而该键或值的完成不会导致将不合格的元素插入到映射中,则根据实现的选择,可能会引发异常,也可能会成功。此类异常在该接口的规范中标记为“可选”。

集合框架接口中的许多方法都是根据equals方法定义的。

例如,contains key(Object key)方法的规范说:“如果且仅当此映射包含键k的映射(key==null),则返回true?k==null:key.equals(k)。“不应将此规范解释为暗示使用非空参数键调用Map.containsKey将导致对任何键k调用key.equals(k)。实现可以自由实现优化,从而避免equals调用,例如,首先比较两个键的哈希代码。(Object.hashCode()规范保证散列码不相等的两个对象不能相等。)更一般地说,各种集合框架接口的实现可以在实现者认为适当的地方自由地利用底层对象方法的指定行为。

对于映射直接或间接包含自身的自引用实例,执行映射递归遍历的某些映射操作可能会失败,并出现异常。这包括clone()、equals()、hashCode()和toString()方法。实现可以选择性地处理自引用场景,但是大多数当前实现不这样做。

简单说

Map是一个已经约定KV结构的存储的接口。

Map也是一个集合框架和Collection是一个级别的。

Map体系结构

 

Map特点

Map属于是一个映射,是无序的。

以键值对的形式添加元素。

键不能重复,值可以重复。

它只是个接口,具体怎么实现还得看实现类是怎么做的。

 

AbstractMap

public abstract class AbstractMap<K,V> implements Map<K,V> { ... }

这个类提供了 Map 接口的框架实现,以最小化实现这个接口所需的工作。

为了实现一个不可修改的映射,程序员只需要扩展这个类并为entrySet方法提供一个实现,该方法返回映射的集合视图。通常,返回的集合将依次在抽象集上实现。此集合不应支持add或remove方法,其迭代器不应支持remove方法。

若要实现可修改的映射,程序员必须另外重写此类的put方法(否则将抛出不支持的DoperationException),并且 entrySet() 返回的迭代器必须另外实现其remove方法。

根据映射接口规范中的建议,程序员通常应该提供一个void(无参数)和映射构造函数。

此类中每个非抽象方法的文档详细描述了其实现。如果正在实现的映射允许更有效的实现,则这些方法中的每一个都可能被重写。

Map 接口的实现类 都会先继承 AbstractMap 这个抽象类(抽象类的概念都是清楚的哈)

 

HashMap

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable { ... }

HashMap类描述

基于哈希表的映射接口实现。此实现提供所有可选的映射操作,并允许空值和空键。(HashMap类大致等同于Hashtable,只是它不同步并且允许空值。)这个类不保证映射的顺序;特别是,它不保证顺序随时间保持不变。

此实现为基本操作(get和put)提供恒定的时间性能,假设哈希函数在存储桶之间正确地分散元素。集合视图上的迭代需要与HashMap实例的“容量”(bucket的数量)加上其大小(键值映射的数量)成比例的时间。因此,如果迭代性能很重要的话,不要设置太高的初始容量(或者太低的负载系数)。

HashMap实例有两个影响其性能的参数:初始容量和负载因子。容量是哈希表中存储桶的数量,初始容量只是创建哈希表时的容量。负载因子是在哈希表的容量自动增加之前,允许哈希表获得的满容量的度量。当哈希表中的条目数超过加载因子和当前容量的乘积时,哈希表将重新灰化(即重建内部数据结构),以便哈希表具有大约两倍的存储桶数。

一般来说,默认加载因子(0.75)在时间和空间成本之间提供了一个很好的折衷。较高的值会减少空间开销,但会增加查找成本(反映在HashMap类的大多数操作中,包括get和put)。在设置初始容量时,应考虑map中的预期条目数及其负载系数,以尽量减少再冲操作次数。如果初始容量大于最大条目数除以负载系数,则不会发生再刷新操作。

如果要在HashMap实例中存储许多映射,那么创建具有足够大容量的映射将比让它根据需要执行自动重新灰化以扩展表更有效地存储映射。请注意,使用具有相同hashCode()的多个键肯定会降低任何哈希表的性能。为了改善影响,当键是可比较的时,这个类可以使用键之间的比较顺序来帮助打破联系。

请注意,此实现不同步。如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了该映射,则必须在外部对其进行同步。(结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键相关联的值不是结构修改。)这通常通过在自然封装映射的某个对象上进行同步来完成。如果不存在此类对象,则应使用Collections.synchronizedMap方法“包装”映射。最好在创建时执行此操作,以防止意外地对映射进行非同步访问:

Map m = Collections.synchronizedMap(new HashMap(...));

这个类的所有“集合视图方法”返回的迭代器都是快速失败的:如果在迭代器创建之后的任何时候对映射进行了结构上的修改,除了通过迭代器自己的remove方法之外,迭代器将抛出一个ConcurrentModificationException。因此,在面对并发修改时,迭代器会快速而干净地失败,而不是在将来某个不确定的时间冒着任意的、不确定的行为的风险。

注意,不能保证迭代器的fail-fast行为,因为通常情况下,在存在不同步的并发修改的情况下,不可能做出任何硬保证。Fail-fast迭代器在尽最大努力的基础上抛出ConcurrentModificationException。因此,编写依赖于此异常的正确性的程序是错误的:迭代器的快速失败行为应该只用于检测错误。

HashMap构造函数

// 构造具有默认初始容量(16)和默认加载因子(0.75)的空哈希映射。
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

// 构造具有指定初始容量和默认加载因子(0.75)的空哈希映射。
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

// 构造具有指定初始容量和负载因子的空哈希映射。
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

// 使用与指定映射相同的映射构造新的哈希映射。
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

加载因子为什么是0.75?

简单说

HashMap是基于哈希表的Map接口实现的(哈希表的作用是来保证键的唯一性)

不是同步的,故线程不安全

如果要指定HashMap的键一定要确保重写了equals()和hashCode()两个方法。

常用API

详细文档:https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html

(文档里的 Method Summary,上手试一试,多用就记住了,场景最重要)

 

LinkedHashMap

public class LinkedHashMap<K,V> extends HashMap<K,V>
    implements Map<K,V> { ... }

LinkedHashMap类描述

实现了哈希表和链表的映射接口,具有可预测的迭代顺序。

这个实现与HashMap的不同之处在于它维护了一个贯穿其所有条目的双链接列表。这个链表定义了迭代顺序,通常是键插入到映射中的顺序(插入顺序)。

注意,如果将键重新插入到映射中,则插入顺序不受影响。如果在m.containsKey(k)将在调用之前立即返回true时调用m.put(k,v),则键k将重新插入到映射m中。

此实现使其用户在使用的时候免于HashMap(和Hashtable)提供的未指定的、通常是混乱的排序,而不会导致与TreeMap相关联的成本增加。它可用于生成与原始地图顺序相同的地图副本,而不考虑原始地图的实现方式:

void foo(Map m) {
    Map copy = new LinkedHashMap(m);
    ...
}

如果一个模块在输入时获取一个映射,复制它,然后返回其顺序由复制顺序决定的结果,则此技术特别有用。(一般都很喜欢按同样的顺序来操作)

提供了一个特殊的构造函数来创建一个链接散列映射,其迭代顺序是从最近访问到最近访问(访问顺序)的最后一次访问其条目的顺序。这种 map 非常适合建立LRU缓存。调用put、putIfAbsent、get、getOrDefault、compute、computeIfAbsent、computeIfPresent或merge方法会导致对相应项的访问(假设在调用完成后该项存在)。replace方法仅在值被替换时才导致对条目的访问。putAll方法为指定映射中的每个映射生成一个条目访问,顺序是由指定映射的条目集迭代器提供键值映射。没有其他方法生成入口访问。特别是,对集合视图的操作不会影响备份映射的迭代顺序。

removeEldestEntry(Map.Entry)方法可能被重写,以便在将新映射添加到映射时强制执行自动删除过时映射的策略。

这个类提供所有可选的映射操作,并允许空元素。与HashMap类似,它为基本操作(add、contains和remove)提供恒定的时间性能,假设hash函数在bucket之间正确地分散元素。性能可能略低于HashMap,这是因为维护链接列表增加了开销,但有一个例外:在LinkedHashMap的集合视图上迭代需要与映射大小成比例的时间,而不管其容量如何。HashMap上的迭代可能更昂贵,所需时间与其容量成正比。

链表哈希映射有两个影响其性能的参数:初始容量和加载因子。它们的定义与HashMap完全相同。但是,请注意,对于这个类来说,为初始容量选择一个过高的值的惩罚不如为HashMap严重,因为这个类的迭代时间不受容量的影响。

请注意,此实现不同步。如果多个线程同时访问链表散列映射,并且至少有一个线程在结构上修改了该映射,则必须在外部对其进行同步。这通常是通过在自然封装映射的某个对象上进行同步来实现的。如果不存在此类对象,则应使用Collections.synchronizedMap方法“包装”映射。最好在创建时执行此操作,以防止意外地对映射进行非同步访问:

Map m = Collections.synchronizedMap(new LinkedHashMap(...));

结构修改是添加或删除一个或多个映射的任何操作,或者在访问顺序链表哈希映射的情况下,影响迭代顺序。在插入顺序链表哈希映射中,仅更改与已包含在映射中的键关联的值不是结构修改。在访问顺序链表散列映射中,仅使用get查询映射是一种结构修改。

这个类的所有集合视图方法返回的集合的迭代器方法返回的迭代器都是快速失败的:如果在迭代器创建后的任何时候对映射进行了结构修改,则迭代器将以任何方式抛出ConcurrentModificationException,迭代器自己的remove方法除外。因此,在面对并发修改时,迭代器会快速而干净地失败,而不是在将来某个不确定的时间冒着任意的、不确定的行为的风险。

注意,不能保证迭代器的fail-fast行为,因为通常情况下,在存在不同步的并发修改的情况下,不可能做出任何硬保证。Fail-fast迭代器在尽最大努力的基础上抛出ConcurrentModificationException。因此,编写依赖于此异常的正确性的程序是错误的:迭代器的快速失败行为应该只用于检测错误。

此类的所有集合视图方法返回的集合的spliterator方法返回的拆分器是延迟绑定、快速失败和额外的report spliterator.ORDERED。

LinkedHashMap构造函数

// 构造具有默认初始容量(16)和加载因子(0.75)的空插入顺序LinkedHashMap实例。
public LinkedHashMap() {
    super();
    accessOrder = false;
}
// 构造具有指定初始容量和默认加载因子(0.75)的空插入顺序LinkedHashMap实例。
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}
// 构造具有指定初始容量和加载因子的空插入顺序LinkedHashMap实例。
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}
// 构造具有指定初始容量、加载因子和排序模式的空LinkedHashMap实例。
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}
// 构造具有与指定映射相同映射的插入顺序LinkedHashMap实例。
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

简单说

LinkedHashMap 扩展 HashMap,底层数据结构是链表哈希表结构

有序的,可指定排序方式,不是同步的,线程不是安全的。

效率比 HashMap 低,是因为要维护链表的指针

常用API

详细文档:https://docs.oracle.com/javase/8/docs/api/java/util/LinkedHashMap.html

 

Dictionary

public abstract class Dictionary<K,V> extends Object

Dictionary类是任何类的抽象父类,例如Hashtable,它将键映射到值。每个键和值都是一个对象。

在任何一个Dictionary对象中,每个键最多关联一个值。给定一个字典和一个键,就可以查找相关的元素。任何非空对象都可以用作键和值。

通常,该类的实现应该使用equals方法来决定两个键是否相同。

注意:这个类已经过时了。新的实现应该实现映射接口,而不是扩展这个类。

 

Hashtable

public class Hashtable<K,V> extends Dictionary<K,V>
 implements Map<K,V>, Cloneable, Serializable

Hashtable类描述

这个类实现了一个哈希表,它将键映射到值。任何非空对象都可以用作键或值。

要成功地从哈希表中存储和检索对象,用作键的对象必须实现hashCode方法和equals方法。

Hashtable实例有两个影响其性能的参数:初始容量和负载因子。容量是哈希表中的存储bucket数,初始容量只是创建哈希表时的容量。注意,散列表是打开的:在“散列冲突”的情况下,一个bucket存储多个条目,这些条目必须按顺序搜索。负载因子是在哈希表的容量自动增加之前,允许哈希表获得的满容量的度量。初始容量和负载因子参数只是实现的提示。关于何时以及是否调用rehash方法的确切细节取决于实现。

通常,默认的加载因子(.75)在时间和空间成本之间提供了一个很好的折衷。较高的值会减少空间开销,但会增加查找条目的时间开销(这反映在大多数哈希表操作中,包括get和put)。

初始容量控制着浪费的空间和重新整理操作的需要之间的折衷,这是非常耗时的。如果初始容量大于哈希表将包含的最大条目数除以其负载因子,则不会发生重新散列操作。但是,设置初始容量过高会浪费空间。

如果要在哈希表中创建许多条目,那么创建具有足够大容量的哈希表可能比根据需要执行自动重新灰化以扩展表更有效地插入条目。

这个例子创建了一个数字哈希表。它使用数字的名称作为键:

Hashtable<String, Integer> numbers = new Hashtable<String, Integer>();
numbers.put("one", 1);
numbers.put("two", 2);
numbers.put("three", 3);

若要检索数字,则需要:

Integer n = numbers.get("two");
if (n != null) {
    System.out.println("two = " + n);
}

所有此类的“集合视图方法”返回的集合的迭代器方法返回的迭代器都会快速失败:如果在迭代器创建后的任何时间以任何方式(除了通过迭代器自己的remove方法)修改哈希表的结构,迭代器将抛出一个ConcurrentModificationException。因此,在面对并发修改时,迭代器会快速而干净地失败,而不是在将来某个不确定的时间冒着任意的、不确定的行为的风险。Hashtable的keys和elements方法返回的枚举不会很快失败。

注意,不能保证迭代器的fail-fast行为,因为通常情况下,在存在不同步的并发修改时,它不可能做出任何硬保证。Fail-fast迭代器在尽最大努力的基础上抛出ConcurrentModificationException。因此,编写依赖于此异常的正确性的程序是错误的:迭代器的快速失败行为应该只用于检测错误。

从Java 2平台v1.2开始,这个类被改进以实现Map接口,使其成为Java Collections框架的成员。与新的集合实现不同,哈希表是同步的。如果不需要线程安全实现,建议使用HashMap代替Hashtable。如果需要线程安全的高并发实现,建议使用ConcurrentHashMap代替Hashtable。

Hashtable构造函数

// 使用默认的初始容量(11)和加载因子(0.75)构造一个新的空哈希表。
public Hashtable() {
    this(11, 0.75f);
}
// 使用指定的初始容量和默认加载因子(0.75)构造一个新的空哈希表。
public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}
// 使用指定的初始容量和指定的加载因子构造一个新的空哈希表。
public Hashtable(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);

    if (initialCapacity==0)
        initialCapacity = 1;
    this.loadFactor = loadFactor;
    table = new Entry<?,?>[initialCapacity];
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
// 使用与给定映射相同的映射构造新哈希表。
public Hashtable(Map<? extends K, ? extends V> t) {
    this(Math.max(2*t.size(), 11), 0.75f);
    putAll(t);
}

简单说

Hashtable 底层数据结构 哈希表。

同步的,线程安全,每个方法都使用了 synchronized 关键字进行修饰

键值不允许为空,键的对象必须实现hashCode方法和equals方法。

键存在,新值覆盖旧值,并返回旧值。

哈希表中的键数超过此哈希表的容量和加载因子时,将自动调用 rehash() 方法,进行扩容。

新的容量:int newCapacity = (oldCapacity << 1) + 1;

常用API

详细地址:https://docs.oracle.com/javase/8/docs/api/java/util/Hashtable.html

Hash冲突?碰撞? 散列冲突?

 

SortedMap

public interface SortedMap<K,V> extends Map<K,V>

SortedMap接口描述

一种映射,进一步提供对其键的总排序。

映射根据其键的自然顺序进行排序,或者由通常在排序的映射创建时提供的比较器进行排序。当迭代排序映射的集合视图(由entrySet、keySet和values方法返回)时,会反映此顺序。还提供了一些额外的操作来利用订单。(这个接口是SortedSet的map模拟)

插入到排序映射中的所有键都必须实现可比较接口(或被指定的比较器接受)。此外,所有这些键必须是相互可比的:k1.compareTo(k2)(或comparator.compare(k1,k2))不能对排序后的映射中的任何键k1和k2抛出ClassCastException。尝试违反此限制将导致有问题的方法或构造函数调用抛出ClassCastException。

注意,如果排序后的映射要正确实现映射接口,则由排序后的映射维护的顺序(无论是否提供显式比较器)必须与equals一致。(请参阅Comparable接口或Comparator接口以获得与equals一致的精确定义)这是因为Map接口是根据equals操作定义的,但是排序的Map使用compareTo(或compare)方法执行所有键比较,因此从这一点上看,被该方法视为相等的两个键是排序后的 map 相等。

tree map 的行为是定义良好的,即使它的顺序与等于不一致;它只是没有遵守映射接口的一般约定。

所有通用的排序映射实现类都应该提供四个“标准”构造函数。虽然接口不能指定as-required构造函数,但无法强制执行此建议。所有排序映射实现的预期“标准”构造函数为:

1)一个void(无参数)构造函数,它创建一个空的排序映射,该映射根据其键的自然顺序排序。

2)具有Comparator类型的单个参数的构造函数,它创建根据指定的Comparator排序的空排序映射。

3)一种具有Map类型的单参数的构造函数,它创建一个具有与其参数相同的键值映射的新映射,并根据键的自然顺序进行排序。

4)具有sorted map类型的单个参数的构造函数,它创建一个具有与输入排序映射相同的键值映射和顺序的新排序映射。

注意:有几个方法返回键范围受限的子映射。这类范围是半开放的,也就是说,它们包括其低端点,但不包括其高端点(如适用)。如果需要一个封闭范围(包括两个端点),并且键类型允许计算给定键的后继项,那么只需请求从lowEndpoint到后继项(highEndpoint)的子范围。例如,假设m是一个键为字符串的映射。以下习惯用法获取一个视图,其中包含m中的所有键值映射,这些映射的键值介于low和high之间(包括low和high):

SortedMap<String, V> sub = m.subMap(low, high+"\0");

类似的技术可用于生成开放范围(既不包含端点也不包含端点)。以下习惯用法获取一个视图,其中包含m中的所有键值映射,这些映射的键值介于low和high之间,互斥:

SortedMap<String, V> sub = m.subMap(low+"\0", high);

 

NavigableMap

public interface NavigableMap<K,V> extends SortedMap<K,V>

NavigableMap接口描述

用 navigation 方法扩展的 SortedMap,返回给定搜索目标的最接近匹配项。

方法lowerEntry、floorEntry、ceilingEntry和higherEntry返回映射。分别与小于、小于或等于、大于或等于给定键关联的项对象,如果没有该键,则返回null。类似地,lowerKey、floorKey、ceilingKey和higherKey方法只返回关联的键。所有这些方法都是为定位而不是遍历条目而设计的。

NavigableMap可以按升序键或降序键访问和遍历。descendingMap方法返回映射的视图,所有关系方法和方向方法的意义都颠倒过来。升序操作和视图的性能可能比降序操作和视图的性能更快。subMap、headMap和tailMap方法不同于类似的SortedMap方法,它们接受额外的参数来描述上下界是包含的还是排除的。任何NavigableMap的子映射都必须实现NavigableMap接口。

此接口还定义了方法firstEntry、pollFirstEntry、lastEntry和pollLastEntry,这些方法返回和/或移除最小和最大的映射(如果存在),否则返回null。

条目返回方法的实现预计将返回Map.entry对,它们表示生成映射时映射的快照,因此通常不支持可选的entry.setValue方法。但是请注意,可以使用方法put更改关联映射中的映射。

方法subMap(K,K)、headMap(K)和tailMap(K)被指定为返回SortedMap,以允许对现有的SortedMap实现进行兼容改造以实现NavigableMap,但鼓励此接口的扩展和实现重写这些方法以返回NavigableMap。类似地,可以重写SortedMap.keySet()以返回NavigableSet。

 

TreeMap

public class TreeMap<K,V> extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, Serializable

TreeMap类描述

基于红黑树的NavigableMap实现。映射根据其键的自然顺序进行排序,或者由在映射创建时提供的比较器进行排序,具体取决于使用的构造函数。

此实现为containsKey、get、put和remove操作提供了有保证的日志(n)时间开销。算法是对Cormen,Leiserson和Rivest的算法介绍中的算法的改编。

注意,tree map 维护的顺序与任何排序的映射一样,无论是否提供显式比较器,都必须与equals一致,这样排序的映射才能正确实现映射接口。(参阅Comparable or Comparator以获得与equals一致的精确定义。)这是因为映射接口是根据equals操作定义的,但排序映射使用compareTo(或compare)方法执行所有键比较,因此从排序映射的角度来看,被此方法视为相等的两个键是,平等。排序映射的行为是定义良好的,即使其顺序与等于不一致;它只是没有遵守映射接口的一般约定。

注意,此实现不同步。如果多个线程同时访问一个映射,并且至少有一个线程在结构上修改了该映射,则必须在外部对其进行同步。(结构修改是添加或删除一个或多个映射的任何操作;仅更改与现有键关联的值不是结构修改。)这通常通过在自然封装映射的某个对象上进行同步来完成。如果不存在此类对象,则应使用Collections.synchronizedSortedMap方法“包装”映射。最好在创建时执行此操作,以防止意外地对映射进行非同步访问:

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

这个类的所有“集合视图方法”返回的集合的迭代器方法返回的迭代器是快速失败的:如果在迭代器创建后的任何时候以任何方式(除了通过迭代器自己的remove方法)对映射进行结构修改,迭代器将抛出一个ConcurrentModificationException。因此,在面对并发修改时,迭代器会快速而干净地失败,而不是在将来某个不确定的时间冒着任意的、不确定的行为的风险。

注意,不能保证迭代器的fail-fast行为,因为通常情况下,在存在不同步的并发修改时,它不可能做出任何硬保证。Fail-fast迭代器在尽最大努力的基础上抛出ConcurrentModificationException。因此,编写依赖于此异常的正确性的程序是错误的:迭代器的快速失败行为应该只用于检测错误。

此类及其视图中的方法返回的所有Map.Entry对表示生成映射时映射的快照。它们不支持Entry.setValue方法。(但可以使用put更改关联映射中的映射)

 

TreeMap构造函数

// 使用键的自然顺序构造一个新的空 treemap
public TreeMap() {
    comparator = null;
}
// 根据给定的比较器排序,构造一个新的空 treemap
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}
// 构造一个新的树映射,该树映射包含与给定映射相同的映射,并根据其键的自然顺序进行排序。
public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}
// 构造一个新的树映射,该树映射包含相同的映射,并使用与指定的排序映射相同的顺序。
public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

简单说

TreeMap是一个有序的KV结构集合,底层数据结构是红黑树

不是同步的,线程不安全。

支持排序(自然排序,比较器排序)

常用API

详细地址:https://docs.oracle.com/javase/8/docs/api/java/util/TreeMap.html

注意:TreeSet里的那个坑在TreeMap同样也有,因为TreeSet底层就是TreeMap。

 

总结

Map

Map主要用于存储健值对,根据键得到值,因此不允许键重复,但允许值重复。

Map接口概述:Java.util.Map<k,v>接口:是一个双列集合,一个key只能对应一个value。

Map集合的特点: 是一个双列集合,有两个泛型key和value,使用的时候key和value的数据类型可以相同。也可以不同。

底层是一个哈希表(数组+单向链表):查询快,增删快,是一个无序集合

Collection中的集合元素是孤立的,可理解为单身,是一个一个存进去的,称为单列集合。

Map中的集合元素是成对存在的,可理解为夫妻,是一对一对存进去的,称为双列集合。

Map没有继承Collection接口,提供是key到value的映射。

Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。

Map具有将对象映射到其他对象的功能,是一个K-V形式存储容器,你可以通过containsKey()和containsValue()来判断集合是否包含某个减或某个值。

Map可以很容以拓展到多维(值可以是其他容器甚至是其他Map):Map<Object,List<Object>>

Map集合的数据结构仅仅针对键有效,与值无关。

HashMap

HashMap 无序,不是同步的,线程不安全,效率高,支持null元素(允许一条元素的键为Null);

根据键的HashCode 来存储数据,根据键可以直接获取它的值,访问速度快。

遍历时,取得数据的顺序是完全随机的。

在 Map 中插入、删除和定位元素,HashMap 是最好的选择。

HashMap 不支持线程的同步。如果需要同步,可以用 Collections的synchronizedMap() 方法使HashMap具有同步的能力,或者使用 ConcurrentHashMap 集合。

HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

key为null的键值对永远都放在以table[0]为头结点的链表中。

HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。学过数据结构的同学都知道,解决hash冲突的方法有很多,HashMap底层是通过链表来解决hash冲突的。

HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。=

HashMap通过key的hashCode来计算hash值,当hashCode相同时,通过“拉链法”解决冲突。

相比于之前的版本,JDK1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。原本Map.Entry接口的实现类Entry改名为了Node。转化为红黑树时改用另一种实现TreeNode。

JDK1.8中最大的变化就是在一个Bucket中,如果存储节点的数量超过了8个,就会将该Bucket中原来以链表形式存储节点转换为以树的形式存储节点;而如果少于6个,就会还原成链表形式存储。

为什么这样做?主要是因为 LinkedList 的遍历操作不太友好,如果在节点个数比较多的情况下性能会比较差,而树的遍历效率是比较好的,主要是优化遍历,提升性能。

HashMap基于哈希原理,通过put和get方法存储和获取对象。当我们将键值对传递给put方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到对应的bucket位置存储键对象和值对象作为Map.Entry;如果两个对象的hashcode相同,所以对应的bucket位置是相同的,HashMap采用链表解决冲突碰撞,这个Entry(包含有键值对的Map.Entry对象)会存储到链表的下一个节点中;如果对应的hashcode和key值都相同,则修改对应的value的值。HashMap在每个链表节点中存储键值对对象。当使用get()方法获取对象时,HashMap会根据键对象的hashcode去找到对应的bucket位置,找到对应的bucket位置后会调用keys.equals()方法去找到连表中对应的正确的节点找到对象。

HashMap存数据的过程是:HashMap内部维护了一个存储数据的Entry数组,采用链表解决冲突,每一个Entry本质上是一个单向链表。当准备添加一个key-value对时,首先通过hash(key)方法计算hash值,然后通过indexFor(hash,length)求该key-value对的存储位置,计算方法是先用hash&0x7FFFFFFF后,再对length取模,这就保证每一个key-value对都能存入HashMap中,当计算出的位置相同时,由于存入位置是一个链表,则把这个key-value对插入链表头。

HashMap内存储数据的Entry数组默认是16,如果没有对Entry扩容机制的话,当存储的数据一多,Entry内部的链表会很长,这就失去了HashMap的存储意义了。

所以HasnMap内部有自己的扩容机制:

变量size,记录HashMap的底层数组中已用槽的数量;
变量threshold,阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)    
变量DEFAULT_LOAD_FACTOR = 0.75f,默认加载因子为0.75
HashMap扩容的条件是:当size大于threshold时,对HashMap进行扩容

扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。 扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为O(1),因为最新的Entry会插入链表头部,急需要简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

实现原理:实现一个哈希表,存储元素(key/value)时,用key计算hash值,如果hash值没有碰撞,则只用数组存储元素;如果hash值碰撞了,则相同的hash值的元素用链表存储;如果相同hash值超过8个,则相同的hash值的元素用红黑树存储。获取元素时,用key计算hash值,用hash值计算元素在数组中的下标,取得元素如果命中,则返回;如果不是就在红黑树或链表中找。

Hashtable

Hashtable 同步的,线程安全,效率低效,不支持null,父类是Dictionary。

Hashtable 实现了哈希表从key映射到value的数据结构形式。

任何非null的对象都可以作为key或者value。

要在hashtable中存储和检索对象,作为key的对象必须实现hashCode、equals方法。

如果不需要线程安全的实现,建议使用HashMap代替Hashtable

如果想要一个线程安全的高并发实现,那么建议使用 ConcurrentHashMap 代替 Hashtable。

Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。

LinkeHashMap

LinkedHashMap继承自HashMap,实现了Map<K,V>接口。

有序,不是同步的,线程不安全。

内部维护了一个双向链表,在每次插入数据,或者访问、修改数据时,会增加节点、或调整链表的节点顺序。以决定迭代时输出的顺序。

默认情况,遍历时的顺序是按照插入节点的顺序。这也是其与HashMap最大的区别。

LinkedHashMap由于它的插入有序特性,也是一种比较常用的Map集合。它继承了HashMap,很多方法都直接复用了父类HashMap的方法。

按照插入顺序,并且以Linked-开头,就很有可能是链表实现。如果纯粹以链表实现,也不是不可以,LinkedHashMap内部维护一个链表,插入一个元素则把它封装成Entry节点,并把它插入到链表尾部。功能可以实现,但这带来的查找效率达到了O(n),显然远远大于HashMap在没有冲突的情况下O(1)的时间复杂度。这就丝毫不能体现出Map这种数据结构随机存取快的优点。

所以显然,LinkedHashMap不可能只有一个链表来维护Entry节点,它极有可能维护了两种数据结构:散列表+链表。

LinkedHashMap的LRU特性

LRU的定义:LRU(Least Recently Used),即最近最少使用算法,最初是用于内存管理中将无效的内存块腾出而用于加载数据以提高内存使用效率而发明的算法。

目前已经普遍用于提高缓存的命中率,如Redis、Memcached中都有使用。

为啥说LinkedHashMap本身就实现了LRU算法?原因就在于它额外维护的双向链表中。

在做get/put操作时,LinkedHashMap会将当前访问/插入的节点移动到链表尾部,所以此时链表头部的那个节点就是 "最近最少未被访问"的节点。

TreeMap

TreeMap实现SortMap接口,能够把它保存的记录根据键排序。

默认是按键的升序排序,也可以指定排序的比较器,当用 Iterator 遍历TreeMap时,得到的记录是排过序的。TreeMap取出来的是排序后的键值对。但如果需要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。

TreeMap是基于红黑树结构实现的一种Map,要分析TreeMap的实现首先就要对红黑树有所了解。

(要了解什么是红黑树,就要了解它的存在主要是为了解决什么问题,对比其他数据结构比如数组,链表,Hash表等树这种结构又有什么优点)这里不做分析。请移步:《数据结构》

在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n)。

使用entrySet遍历方式要比keySet遍历方式快。

entrySet遍历方式获取Value对象是直接从Entry对象中直接获得,时间复杂度T(n)=o(1)。

keySet遍历获取Value对象则要从Map中重新获取,时间复杂度T(n)=o(n);keySet遍历Map方式比entrySet遍历Map方式多了一次循环,多遍历了一次table,当Map的size越大时,遍历的效率差别就越大。

HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)

在Map 中插入、删除和定位元素,HashMap是最好的选择。如果要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。使用HashMap要求添加的键类明确定义了hashCode()和 equals()的实现。

 

一句话概括

Map是键值对映射的接口

HashMap底层是通过散列表实现的,比较常用,Key是通过hashCode值来存储数据的。可直接根据K获取V,访问速度很快。只允许一个K为null,V可以多个null。线程不安全,效率高。

Hashtable与HashMap类似,Hashtable的K和V不能为null。写入速度慢。线程安全的,效率低。

TreeMap底层是红黑树实现的,能够把进行存储的数据按照K进行排序,默认是按升序排序,也可以指定排序的比较器。TreeMap不允许K为null。线程不安全,效率高。遍历时得到记录是排过序的。

LinkedHashMap底层是通过链表实现的,有序的map集合,在插入值的时候可以自定义排序方式。key和value都允许为空。线程不安全。

HashMap扩容时是当前容量翻倍即:capacity*2,Hashtable扩容时是容量翻倍+1:capacity*2+1。

 

Comparable 和 Comparator 的区别

Comparable 是一个比较的标准,里面有比较的方法,对象要具有比较的标准,就必须实现 Comparable 接口;类实现这个接口,就有比较的方法;把元素放到 TreeSet 里面去,就会自动的调用 CompareTo 方法;但是这个 Comparable 并不是专为 TreeSet 设计的;只是说,TreeSet 顺便利用而已;就像 HashCode 和 equals 也一样,不是专门为 HashSet 设计一样;只是你顺便利用而已。

Compartor 是个比较器,也不是专门为TreeSet设计. 就是一个第三方的比较器接口;如果对象没有比较性,自己就可以按照比较器的标准,设计一个比较器,创建一个类,实现这个接口,覆写方法。


业精于勤荒于嬉

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值