java 基础 集合 Map

164 阅读11分钟

Map:

对于一般的应用场景,程序应该多考虑使用HashMap,因为HashMap正是为快速查询设计的(HashMap底层其实也是采用数组来存储key-value对)。但如果程序需要一个总是排好序的Map时,则可以考虑使用TreeMap。

LinkedHashMap比HashMap慢一点,因为它需要维护链表来保持Map中key-value时的添加顺序。IdentityHashMap性能没有特别出色之处,因为它采用与HashMap基本相似的实现,只是它使用==而不是equals()方法来判断元素相等。EnumMap的性能最好,但它只能使用同一个枚举类的枚举值作为key。

Collections

public class SynchronizedTest{

public static void main(String[] args){

    //下面程序创建了4个同步的集合对象

    Collection c=Collections.synchronizedCollection(new ArrayList());

    List list=Collections.synchronizedList(new ArrayList());

    Set s=Collections.synchronizedSet(new HashSet());

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

实现一个Map 这个Map是说明性的,缺乏效率,并且具有固定的尺寸不灵活:

//: containers/AssociativeArray.java
// Associates keys with values.
import static net.mindview.util.Print.*;

public class AssociativeArray<K,V> {
  private Object[][] pairs;
  private int index;
  public AssociativeArray(int length) {
    pairs = new Object[length][2];
  }
  public void put(K key, V value) {
    if(index >= pairs.length)
      throw new ArrayIndexOutOfBoundsException();
    pairs[index++] = new Object[]{ key, value };
  }
  @SuppressWarnings("unchecked")
  public V get(K key) {
    for(int i = 0; i < index; i++)
      if(key.equals(pairs[i][0]))
        return (V)pairs[i][1];
    return null; // Did not find key
  }
  public String toString() {
    StringBuilder result = new StringBuilder();
    for(int i = 0; i < index; i++) {
      result.append(pairs[i][0].toString());
      result.append(" : ");
      result.append(pairs[i][1].toString());
      if(i < index - 1)
        result.append("\n");
    }
    return result.toString();
  }
  public static void main(String[] args) {
    AssociativeArray<String,String> map =
      new AssociativeArray<String,String>(6);
    map.put("sky", "blue");
    map.put("grass", "green");
    map.put("ocean", "dancing");
    map.put("tree", "tall");
    map.put("earth", "brown");
    map.put("sun", "warm");
    try {
      map.put("extra", "object"); // Past the end
    } catch(ArrayIndexOutOfBoundsException e) {
      print("Too many objects!");
    }
    print(map);
    print(map.get("ocean"));
  }
} /* Output:
Too many objects!
sky : blue
grass : green
ocean : dancing
tree : tall
earth : brown
sun : warm
dancing
*///:~

Map之间的比较
Map 实现描述
HashMap*基于哈希表的实现。(使用此类来代替 Hashtable 。)为插入和定位键值对提供了常数时间性能。可以通过构造方法调整性能,这些构造方法允许你设置哈希表的容量和装填因子。
LinkedHashMapHashMap 类似,但是当遍历时,可以按插入顺序或最近最少使用(LRU)顺序获取键值对。只比 HashMap 略慢,一个例外是在迭代时,由于其使用链表维护内部顺序,所以会更快些。
TreeMap基于红黑树的实现。当查看键或键值对时,它们按排序顺序(由 ComparableComparator 确定)。 TreeMap 的侧重点是按排序顺序获得结果。 TreeMap 是唯一使用 subMap() 方法的 Map ,它返回红黑树的一部分。
WeakHashMap一种具有 弱键(weak keys) 的 Map ,为了解决某些类型的问题,它允许释放 Map 所引用的对象。如果在 Map 外没有对特定键的引用,则可以对该键进行垃圾回收。
ConcurrentHashMap不使用同步锁定的线程安全 Map。这在并发的一章中讨论。
IdentityHashMap使用 == 而不是 equals() 来比较键。仅用于解决特殊问题,不适用于一般用途。
//: containers/Maps.java
// Things you can do with Maps.
import java.util.concurrent.*;
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;

public class Maps {
  public static void printKeys(Map<Integer,String> map) {
    printnb("Size = " + map.size() + ", ");
    printnb("Keys: ");
    print(map.keySet()); // Produce a Set of the keys
  }
  public static void test(Map<Integer,String> map) {
    print(map.getClass().getSimpleName());
    map.putAll(new CountingMapData(25));
    // Map has 'Set' behavior for keys:
    map.putAll(new CountingMapData(25));
    printKeys(map);
    // Producing a Collection of the values:
    printnb("Values: ");
    print(map.values());
    print(map);
    print("map.containsKey(11): " + map.containsKey(11));
    print("map.get(11): " + map.get(11));
    print("map.containsValue(\"F0\"): "
      + map.containsValue("F0"));
    Integer key = map.keySet().iterator().next();
    print("First key in map: " + key);
    map.remove(key);
    printKeys(map);
    map.clear();
    print("map.isEmpty(): " + map.isEmpty());
    map.putAll(new CountingMapData(25));
    // Operations on the Set change the Map:
    map.keySet().removeAll(map.keySet());
    print("map.isEmpty(): " + map.isEmpty());
  }
  public static void main(String[] args) {
    test(new HashMap<Integer,String>());
    test(new TreeMap<Integer,String>());
    test(new LinkedHashMap<Integer,String>());
    test(new IdentityHashMap<Integer,String>());
    test(new ConcurrentHashMap<Integer,String>());
    test(new WeakHashMap<Integer,String>());
  }
} /* Output:
HashMap
Size = 25, Keys: [15, 8, 23, 16, 7, 22, 9, 21, 6, 1, 14, 24, 4, 19, 11, 18, 3, 12, 17, 2, 13, 20, 10, 5, 0]
Values: [P0, I0, X0, Q0, H0, W0, J0, V0, G0, B0, O0, Y0, E0, T0, L0, S0, D0, M0, R0, C0, N0, U0, K0, F0, A0]
{15=P0, 8=I0, 23=X0, 16=Q0, 7=H0, 22=W0, 9=J0, 21=V0, 6=G0, 1=B0, 14=O0, 24=Y0, 4=E0, 19=T0, 11=L0, 18=S0, 3=D0, 12=M0, 17=R0, 2=C0, 13=N0, 20=U0, 10=K0, 5=F0, 0=A0}
map.containsKey(11): true
map.get(11): L0
map.containsValue("F0"): true
First key in map: 15
Size = 24, Keys: [8, 23, 16, 7, 22, 9, 21, 6, 1, 14, 24, 4, 19, 11, 18, 3, 12, 17, 2, 13, 20, 10, 5, 0]
map.isEmpty(): true
map.isEmpty(): true
...
*///:~

HashMap:

jdk 1.7时是数组+链表组成,

jdk1.8时是 数组+ 链表 + 红黑树组成。 链表元素大于等于8时会把链表转为树结构,若桶中链的元素个数小于等于6时,树结构还原成链表。当链表的个数为8左右徘徊时就会生成树转链表,链表转树,效率低下。hasMap的负载因子默认为0.75,2^n是为了散列更加均匀。

更多参看:mp.csdn.net/console/edi…

SortedMap

使用 SortedMap (由 TreeMapConcurrentSkipListMap 实现),键保证按排序顺序,这允许在 SortedMap 接口中使用这些方法来提供其他功能:

  • Comparator comparator() :生成用于此 Map 的比较器, null 表示自然排序。
  • T firstKey() :返回第一个键。
  • T lastKey() :返回最后一个键。
  • SortedMap subMap(fromKey,toKey) :生成此 Map 的视图,其中键从 fromKey(包括),到 toKey (不包括)。
  • SortedMap headMap(toKey) :使用小于 toKey 的键生成此 Map 的视图。
  • SortedMap tailMap(fromKey) :使用大于或等于 fromKey 的键生成此 Map 的视图。

这是一个类似于 SortedSetDemo.java 的示例,显示了 TreeMap 的这种额外行为:

// collectiontopics/SortedMapDemo.java
// What you can do with a TreeMap
import java.util.*;
import onjava.*;

public class SortedMapDemo {
  public static void main(String[] args) {
    TreeMap<Integer,String> sortedMap =
      new TreeMap<>(new CountMap(10));
    System.out.println(sortedMap);
    Integer low = sortedMap.firstKey();
    Integer high = sortedMap.lastKey();
    System.out.println(low);
    System.out.println(high);
    Iterator<Integer> it =
      sortedMap.keySet().iterator();
    for(int i = 0; i <= 6; i++) {
      if(i == 3) low = it.next();
      if(i == 6) high = it.next();
      else it.next();
    }
    System.out.println(low);
    System.out.println(high);
    System.out.println(sortedMap.subMap(low, high));
    System.out.println(sortedMap.headMap(high));
    System.out.println(sortedMap.tailMap(low));
  }
}
/* Output:
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0,
9=J0}
0
9
3
7
{3=D0, 4=E0, 5=F0, 6=G0}
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0}
{3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0, 9=J0}
*/复制ErrorOK!

这里,键值对按照键的排序顺序进行排序。因为 TreeMap 中存在顺序感,所以“位置”的概念很有意义,因此可以拥有第一个、最后一个元素或子图。

LinkedHashMap

LinkedHashMap 针对速度进行哈希处理,但在遍历期间也会按插入顺序生成键值对( System.out.println() 可以遍历它,因此可以看到遍历的结果)。 此外,可以在构造方法中配置 LinkedHashMap 以使用基于访问的 最近最少使用(LRU) 算法,因此未访问的元素(因此是删除的候选者)会出现在列表的前面。 这样可以轻松创建一个能够定期清理以节省空间的程序。下面是一个显示这两个功能的简单示例:

// collectiontopics/LinkedHashMapDemo.java
// What you can do with a LinkedHashMap
import java.util.*;
import onjava.*;

public class LinkedHashMapDemo {
  public static void main(String[] args) {
    LinkedHashMap<Integer,String> linkedMap =
      new LinkedHashMap<>(new CountMap(9));
    System.out.println(linkedMap);
    // Least-recently-used order:
    linkedMap =
      new LinkedHashMap<>(16, 0.75f, true);
    linkedMap.putAll(new CountMap(9));
    System.out.println(linkedMap);
    for(int i = 0; i < 6; i++)
      linkedMap.get(i);
    System.out.println(linkedMap);
    linkedMap.get(0);
    System.out.println(linkedMap);
  }
}
/* Output:
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0}
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0}
{6=G0, 7=H0, 8=I0, 0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0}
{6=G0, 7=H0, 8=I0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 0=A0}
*/!

这些键值对确实是按照插入顺序进行遍历,即使对于LRU版本也是如此。 但是,在LRU版本中访问前六项(仅限)后,最后三项将移至列表的前面。然后,当再次访问“ 0 ”后,它移动到了列表的后面。

创建不可修改的 Collection 或 Map

通常,创建 CollectionMap 的只读版本会很方便。 Collections 类通过将原始集合传递给一个方法然后返回一个只读版本的集合。 对于 Collection (如果不能将 Collection 视为更具体的类型), ListSetMap ,这类方法有许多变体。这个示例展示了针对每种类型,正确构建只读版本集合的方法:

// collectiontopics/ReadOnly.java
// Using the Collections.unmodifiable methods
import java.util.*;
import onjava.*;

public class ReadOnly {
  static Collection<String> data = new ArrayList<>(Countries.names(6));
  public static void main(String[] args) {
    Collection<String> c = Collections.unmodifiableCollection(new ArrayList<>(data));
    System.out.println(c); // Reading is OK
    //- c.add("one"); // Can't change it

    List<String> a = Collections.unmodifiableList(new ArrayList<>(data));
    ListIterator<String> lit = a.listIterator();
    System.out.println(lit.next()); // Reading is OK
    //- lit.add("one"); // Can't change it

    Set<String> s = Collections.unmodifiableSet(new HashSet<>(data));
    System.out.println(s); // Reading is OK
    //- s.add("one"); // Can't change it

    // For a SortedSet:
    Set<String> ss = Collections.unmodifiableSortedSet(new TreeSet<>(data));

    Map<String,String> m =Collections.unmodifiableMap(new 					                                        HashMap<(Countries.capitals(6)));
    System.out.println(m); // Reading is OK
    //- m.put("Ralph", "Howdy!");

    // For a SortedMap:
    Map<String,String> sm =
      Collections.unmodifiableSortedMap(
        new TreeMap<>(Countries.capitals(6)));
  }
}
/* Output:
[ALGERIA, ANGOLA, BENIN, BOTSWANA, BURKINA FASO,
BURUNDI]
ALGERIA
[BENIN, BOTSWANA, ANGOLA, BURKINA FASO, ALGERIA,
BURUNDI]
{BENIN=Porto-Novo, BOTSWANA=Gaberone, ANGOLA=Luanda,
BURKINA FASO=Ouagadougou, ALGERIA=Algiers,
BURUNDI=Bujumbura}
*/!

为特定类型调用 “unmodifiable” 方法不会导致编译时检查,但是一旦发生转换,对修改特定集合内容的任何方法调用都将产生 UnsupportedOperationException 异常。

在每种情况下,在将集合设置为只读之前,必须使用有意义的数据填充集合。填充完成后,最好的方法是用 “unmodifiable” 方法调用生成的引用替换现有引用。这样,一旦使得内容无法修改,那么就不会冒有意外更改内容的风险。另一方面,此工具还允许将可修改的集合保留为类中的私有集合,并从方法调用处返回对该集合的只读引用。所以,你可以在类内修改它,但其他人只能读它。

同步 Collection 或 Map

synchronized 关键字是多线程主题的重要组成部分,更复杂的内容在并发中介绍。在这里,只需要注意到 Collections 类包含一种自动同步整个集合的方法。 语法类似于 “unmodifiable” 方法:

// collectiontopics/Synchronization.java
// Using the Collections.synchronized methods
import java.util.*;

public class Synchronization {
  public static void main(String[] args) {
    Collection<String> c =
      Collections.synchronizedCollection(
        new ArrayList<>());
    List<String> list = Collections
      .synchronizedList(new ArrayList<>());
    Set<String> s = Collections
      .synchronizedSet(new HashSet<>());
    Set<String> ss = Collections
      .synchronizedSortedSet(new TreeSet<>());
    Map<String,String> m = Collections
      .synchronizedMap(new HashMap<>());
    Map<String,String> sm = Collections
      .synchronizedSortedMap(new TreeMap<>());
  }
}

最好立即通过适当的 “synchronized” 方法传递新集合,如上所示。这样,就不会意外地暴露出非同步版本。

Fail Fast

Java 集合还具有防止多个进程修改集合内容的机制。如果当前正在迭代集合,然后有其他一些进程介入并插入,删除或更改该集合中的对象,则会出现此问题。也许在集合中已经遍历过了那个元素,也许还没有遍历到,也许在调用 size() 之后集合的大小会缩小...有许多灾难情景。 Java 集合库使用一种 fail-fast 的机制,该机制可以检测到除了当前进程引起的更改之外,其它任何对集合的更改操作。如果它检测到其他人正在修改集合,则会立即生成 ConcurrentModificationException 异常。这就是“fail-fast”的含义——它不会在以后使用更复杂的算法尝试检测问题(快速失败)。

通过创建迭代器并向迭代器指向的集合中添加元素,可以很容易地看到操作中的 fail-fast 机制,如下所示:

// collectiontopics/FailFast.java
// Demonstrates the "fail-fast" behavior
import java.util.*;

public class FailFast {
  public static void main(String[] args) {
    Collection<String> c = new ArrayList<>();
    Iterator<String> it = c.iterator();
    c.add("An object");
    try {
      String s = it.next();
    } catch(ConcurrentModificationException e) {
      System.out.println(e);
    }
  }
}
/* Output:
java.util.ConcurrentModificationException
*/

异常来自于在从集合中获得迭代器之后,又尝试在集合中添加元素。程序的两个部分可能会修改同一个集合,这种可能性的存在会产生不确定状态,因此异常会通知你更改代码。在这种情况下,应先将所有元素添加到集合,然后再获取迭代器。

ConcurrentHashMapCopyOnWriteArrayListCopyOnWriteArraySet 使用了特定的技术来避免产生 ConcurrentModificationException 异常。

归纳起来简单地说HashMap:
  • HashMap在底层将key-value对当成一个整体进行处理,这个整体就是一个Entry对象。

  • HashMap底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据Hash算法来决定其存储位置;当需要取出一个Entry时,也会根据Hash算法找到其存储位置,直接取出该Entry。由此可见,HashMap之所以能快速存、取它所包含的Entry,完全类似于现实生活中的:不同的东西要放在不同的位置,需要时才能快速找到它。

  • 当创建HashMap时,有一个默认的负载因子(load factor),其默认值为0.75。这是时间和空间成本上的一种折衷:增大负载因子可以减少Hash表(就是那个Entry数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap的get()与put()方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加Hash表所占用的内存空间

  • HashMap时根据实际需要适当地调整load factor的值。如果程序比较关心空间开销,内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕,则可以适当减少负载因子。通常情况下,程序员无需改变负载因子的值。

  • 如果开始就知道HashMap会保存多个key-value对,可以在创建时就使用较大的初始化容量,如果HashMap中Entry的数量一直不会超过极限容量(capacity * load factor),HashMap就无需调用resize()方法重新分配table数组,从而保证较好的性能。当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为capacity的Entry数组),因此创建HashMap时初始化容量设置也需要小心对待

Map与Set

Set与Map之间的关系非常密切:

Set集合和Map集合的对应关系如下。

■ Set <-> Map

■ EnumSet <-> EnumMap

■ SortedSet <-> SortedMap

■ TreeSet <-> TreeMap

■ NavigableSet <-> NavigableMap

■ HashSet <-> HashMap

■ LinkedHashSet <-> LinkedHashMap

虽然Map中放的元素是key-value对,Set集合中放的元素是单个对象,但如果我们把key-value对中的value当成key的附庸:key在哪里,value就跟在哪里。这样就可以像对待Set一样来对待Map了。事实上,Map提供了一个Entry内部类来封装key-value对,而计算Entry存储时则只考虑Entry封装的key。从Java源码来看, Java是先实现了Map,然后通过包装一个所有value都为null的Map就实现了Set集合。

如果把Map里的所有value放在一起来看,它们又非常类似于一个List:元素与元素之间可以重复,每个元素可以根据索引来查找,只是Map中的索引不再使用整数值,而是以另一个对象作为索引。如果需要从List集合中取出元素,则需要提供该元素的数字索引;如果需要从Map中取出元素,则需要提供该元素的key索引。因此,Map有时也被称为字典,或关联数组

遍历map用keyset()方法:

public class LinkedHashMapTest {

    public static void main(String[] args) {

        LinkedHashMap scores = new LinkedHashMap();

        scores.put("语文", 80);
        scores.put("英文", 82);
        scores.put("数学", 76);
		//遍历scores里的所有key-value对
        for (Object key : scores.keySet()) {
            System.out.println(key + "------>" + scores.get(key));
        }
    }
}
HashSet与HashMap的性能:

对于HashSet及其子类而言,它们采用hash算法来决定集合中元素的存储位置,并通过hash算法来控制集合的大小;对于HashMap、Hashtable及其子类而言,它们采用hash算法来决定Map中key的存储,并通过hash算法来增加key集合的大小。

hash表里可以存储元素的位置被称为“桶(bucket)”,在通常情况下,单个“桶”里存储一个元素,此时有最好的性能:hash算法可以根据hashCode值计算出“桶”的存储位置,接着从“桶”中取出元素。但hash表的状态为open:在发生“hash冲突”的情况下,单个桶会存储多个元素,这些元素以链表形式存储,必须按顺序搜索。如图8.8所示是hash表保存各元素,且发生“hash冲突”的示意图

因为HashSet和HashMap、Hashtable都使用hash算法来决定其元素(HashMap则只考虑key)的存储,因此HashSet、HashMap的hash表包含如下属性:

  • 容量(capacity) :hash中桶的数量
  • 初始化容量(initial capacity) :创建hash表时桶的数量,HashMap和HashSet都允许在构造器中指定初始化容量
  • 尺寸(size) :当前hash表中记录的数量。
  • 负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的散列表,依此类推。轻负载的散列表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢

hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。HashSet和HashMap、Hashtable的构造器允许指定一个负载极限,HashSet和HashMap、Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。

“负载极限”的默认值(0.75)是时间和空间成本上的一种折中:较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMap的get()与put()方法都要用到查询);较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销。程序员可以根据实际情况来调整HashSet和HashMap的“负载极限”值。

如果开始就知道HashSet和HashMap、Hashtable会保存很多记录,则可以在创建时就使用较大的初始化容量,如果初始化容量始终大于HashSet和HashMap、Hashtable所包含的最大记录数除以负载极限,就不会发生rehashing。使用足够大的初始化容量创建HashSet和HashMap、Hashtable时,可以更高效地增加记录,但将初始化容量设置太高可能会浪费空间,因此通常不要将初始化容量设置得太高

Set与Map更多参看对hashset和Map有一个比较好的认知: