Java编程思想笔记——容器深入研究2

理解Map

标准Java类库中包含了Map的几种基本实现,包括:HashMap、TreeMap、LinkedHashMap、WeakHashMap、ConcurrentHashMap、IdentityHashMap。它们都有同样的基本接口Map,但行为特征各不相同,表现在效率、键值对的保存及呈现次序、对象的保存周期、映射表如何让在多线程程序中工作和判定键等价的策略等方面。

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;
  }
  @Override
  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");
    } catch(ArrayIndexOutOfBoundsException e) {
      print("Too many objects!");
    }
    print(map);
    print(map.get("ocean"));
  }
} /*
Too many objects!
sky : blue
grass : green
ocean : dancing
tree : tall
earth : brown
sun : warm
dancing
*/

get方法使用的可能是能想象到的效率最差的方式来定位值的:从数组的头部开始,使用equals方法依次比较键,但这里的关键是简单性而不是效率。上面例子同时还存在固定尺寸问题,但java.util中的各种Map都没有这些问题。

性能

get进行线性搜索时,执行速度会相当慢,这正是HashMap提高速度的地方。HashMap使用特殊值,称作散列码,来取代对键的缓慢搜索。散列码是相对唯一的,用以代表对象的int值,它是通过将该对象的某些信息进行转换而生成的。hashCode()是跟类Object中的方法,因此所有Java对象都能产生散列码。HashMap就是使用对象的hashCode进行快速查询的,此方法能够显著提高性能。
这里写图片描述
HashMap上打星号表示如果没有其他的限制,应该成为默认选择,因为他对速度进行了优化。其他实现强调了其他的特性,因此都不如HashMap快。
散列是映射中存储元素时最常用的方式。
对Map中使用键的要求与对Set中的元素要求一样。任何键都必须具有一个equals方法;如果键被用于散列Map,那它还必须具有恰当的hashCode方法;如果键被用于TreeMap,必须实现Comparable。

public class Maps {
  public static void printKeys(Map<Integer,String> map) {
    printnb("Size = " + map.size() + ", ");
    printnb("Keys: ");
    print(map.keySet());
  }
  public static void test(Map<Integer,String> map) {
    print(map.getClass().getSimpleName());
    map.putAll(new CountingMapData(25));
    map.putAll(new CountingMapData(25));
    printKeys(map);
    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));
    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>());
  }
} /*
HashMap
Size = 25, Keys: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
Values: [A0, B0, C0, D0, E0, F0, G0, H0, I0, J0, K0, L0, M0, N0, O0, P0, Q0, R0, S0, T0, U0, V0, W0, X0, Y0]
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0, 9=J0, 10=K0, 11=L0, 12=M0, 13=N0, 14=O0, 15=P0, 16=Q0, 17=R0, 18=S0, 19=T0, 20=U0, 21=V0, 22=W0, 23=X0, 24=Y0}
map.containsKey(11): true
map.get(11): L0
map.containsValue("F0"): true
First key in map: 0
Size = 24, Keys: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
map.isEmpty(): true
map.isEmpty(): true
TreeMap
Size = 25, Keys: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
Values: [A0, B0, C0, D0, E0, F0, G0, H0, I0, J0, K0, L0, M0, N0, O0, P0, Q0, R0, S0, T0, U0, V0, W0, X0, Y0]
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0, 9=J0, 10=K0, 11=L0, 12=M0, 13=N0, 14=O0, 15=P0, 16=Q0, 17=R0, 18=S0, 19=T0, 20=U0, 21=V0, 22=W0, 23=X0, 24=Y0}
map.containsKey(11): true
map.get(11): L0
map.containsValue("F0"): true
First key in map: 0
Size = 24, Keys: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
map.isEmpty(): true
map.isEmpty(): true
LinkedHashMap
Size = 25, Keys: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
Values: [A0, B0, C0, D0, E0, F0, G0, H0, I0, J0, K0, L0, M0, N0, O0, P0, Q0, R0, S0, T0, U0, V0, W0, X0, Y0]
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0, 9=J0, 10=K0, 11=L0, 12=M0, 13=N0, 14=O0, 15=P0, 16=Q0, 17=R0, 18=S0, 19=T0, 20=U0, 21=V0, 22=W0, 23=X0, 24=Y0}
map.containsKey(11): true
map.get(11): L0
map.containsValue("F0"): true
First key in map: 0
Size = 24, Keys: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
map.isEmpty(): true
map.isEmpty(): true
IdentityHashMap
Size = 25, Keys: [3, 19, 15, 5, 17, 1, 0, 9, 10, 6, 23, 13, 11, 18, 22, 24, 7, 8, 14, 21, 16, 20, 12, 2, 4]
Values: [D0, T0, P0, F0, R0, B0, A0, J0, K0, G0, X0, N0, L0, S0, W0, Y0, H0, I0, O0, V0, Q0, U0, M0, C0, E0]
{3=D0, 19=T0, 15=P0, 5=F0, 17=R0, 1=B0, 0=A0, 9=J0, 10=K0, 6=G0, 23=X0, 13=N0, 11=L0, 18=S0, 22=W0, 24=Y0, 7=H0, 8=I0, 14=O0, 21=V0, 16=Q0, 20=U0, 12=M0, 2=C0, 4=E0}
map.containsKey(11): true
map.get(11): L0
map.containsValue("F0"): false
First key in map: 3
Size = 24, Keys: [19, 15, 5, 17, 1, 0, 9, 10, 6, 23, 13, 11, 18, 22, 24, 7, 8, 14, 21, 16, 20, 12, 2, 4]
map.isEmpty(): true
map.isEmpty(): true
ConcurrentHashMap
Size = 25, Keys: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
Values: [A0, B0, C0, D0, E0, F0, G0, H0, I0, J0, K0, L0, M0, N0, O0, P0, Q0, R0, S0, T0, U0, V0, W0, X0, Y0]
{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0, 9=J0, 10=K0, 11=L0, 12=M0, 13=N0, 14=O0, 15=P0, 16=Q0, 17=R0, 18=S0, 19=T0, 20=U0, 21=V0, 22=W0, 23=X0, 24=Y0}
map.containsKey(11): true
map.get(11): L0
map.containsValue("F0"): true
First key in map: 0
Size = 24, Keys: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
map.isEmpty(): true
map.isEmpty(): true
WeakHashMap
Size = 25, Keys: [24, 22, 23, 20, 21, 18, 19, 16, 17, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Values: [Y0, W0, X0, U0, V0, S0, T0, Q0, R0, P0, O0, N0, M0, L0, K0, J0, I0, H0, G0, F0, E0, D0, C0, B0, A0]
{24=Y0, 22=W0, 23=X0, 20=U0, 21=V0, 18=S0, 19=T0, 16=Q0, 17=R0, 15=P0, 14=O0, 13=N0, 12=M0, 11=L0, 10=K0, 9=J0, 8=I0, 7=H0, 6=G0, 5=F0, 4=E0, 3=D0, 2=C0, 1=B0, 0=A0}
map.containsKey(11): true
map.get(11): L0
map.containsValue("F0"): true
First key in map: 24
Size = 24, Keys: [22, 23, 20, 21, 18, 19, 16, 17, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
map.isEmpty(): true
map.isEmpty(): true
*/

keySet()方法返回由Map的键组成的Set。Java SE5提供了改进的打印支持,可以直接打印values方法,该方法会产生一个包含Map所有值的Collection。(键必须是唯一的,而值是可以重复的)由于这些Collection背后是有Map支持的,所以对Collection的任何改动都会反映到一直相关联的Map。

SortedMap

使用SortedMap(TreeMap的唯一实现),可确保键处于排序状态。使得它具有额外的功能,这些功能由SortedMap接口中的下列方法提供:
Comparator comparator():返回当前Map使用的Comparator;或者返回null,表示以自然方式排序
T firstKey返回Map中的第一个键
T lastKey返回Map中的最末一个键
SortedMap subMap(fromKey,toKey)生成此Map的子集,范围由fromKey(包括)到toKey(不包含)的键确定
SortedMap headMap(toKey)生成此Map的子集,由键小于toKey的所有键值对组成
SortedMap tailMap(fromkey)生成此Map的子集,由键大于或等于fromKey的所有键值对组成

public class SortedMapDemo {
  public static void main(String[] args) {
    TreeMap<Integer,String> sortedMap =
      new TreeMap<Integer,String>(new CountingMapData(10));
    print(sortedMap);
    Integer low = sortedMap.firstKey();
    Integer high = sortedMap.lastKey();
    print(low);
    print(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(); }
    }
    print(low);
    print(high);
    print(sortedMap.subMap(low, high));
    print(sortedMap.headMap(high));
    print(sortedMap.tailMap(low));
  }
} /*
{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}
*/
LinkedHashMap

为了提高速度,LinkedHashMap散列化所有的元素,但在遍历键值对时,却又以元素的插入顺序返回键值对。此外,可以在构造器中设定LinkedHashMap,使之使用基于访问的最近最少使用(LRU)算法,于是没有被访问过的元素就会出现在队列的前面。对于需要定期清理元素以节省空间的程序来说,此功能是的程序很容易实现:

public class LinkedHashMapDemo {
  public static void main(String[] args) {
    LinkedHashMap<Integer,String> linkedMap = new LinkedHashMap<Integer,String>(new CountingMapData(9));
    print(linkedMap);
    linkedMap = new LinkedHashMap<Integer,String>(16, 0.75f, true);
    linkedMap.putAll(new CountingMapData(9));
    print(linkedMap);
    for(int i = 0; i < 6; i++) {
      linkedMap.get(i);
    }
    print(linkedMap);
    print(linkedMap.get(0));
    print(linkedMap);
  }
} /*
{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}
A0
{6=G0, 7=H0, 8=I0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 0=A0}
*/

键值对是以插入顺序进行遍历的,但是,在LRU版本中,在只访问过前六个元素后,最后三个元素移到了队列前面。然后再次访问元素0时,它被移到了队列后端。

散列与散列码

一个天气预报系统,将Groundhog对象(键)与Prediction对象(预报):

public class Groundhog {
  protected int number;
  public Groundhog(int n) { number = n; }
  @Override
  public String toString() {
    return "Groundhog #" + number;
  }
}
public class Prediction {
  private static Random rand = new Random(47);
  private boolean shadow = rand.nextDouble() > 0.5;
  @Override
  public String toString() {
    if(shadow) {
      return "Six more weeks of Winter!";
    } else {
      return "Early Spring!";
    }
  }
}
public class SpringDetector {
  public static <T extends Groundhog>
  void detectSpring(Class<T> type) throws Exception {
    Constructor<T> ghog = type.getConstructor(int.class);
    Map<Groundhog,Prediction> map =
      new HashMap<Groundhog,Prediction>();
    for(int i = 0; i < 10; i++) {
      map.put(ghog.newInstance(i), new Prediction());
    }
    print("map = " + map);
    Groundhog gh = ghog.newInstance(3);
    print("Looking up prediction for " + gh);
    if(map.containsKey(gh)) {
      print(map.get(gh));
    } else {
      print("Key not found: " + gh);
    }
  }
  public static void main(String[] args) throws Exception {
    detectSpring(Groundhog.class);
  }
} /*
map = {Groundhog #3=Early Spring!, Groundhog #7=Early Spring!, Groundhog #5=Early Spring!, Groundhog #9=Six more weeks of Winter!, Groundhog #8=Six more weeks of Winter!, Groundhog #0=Six more weeks of Winter!, Groundhog #6=Early Spring!, Groundhog #4=Six more weeks of Winter!, Groundhog #1=Six more weeks of Winter!, Groundhog #2=Early Spring!}
Looking up prediction for Groundhog #3
Key not found: Groundhog #3
*/

detectSpring方法使用反射机制来实例化及使用Groundhog类或任何从Groundhog派生出来的类。
无法找到表示数字为3的这个键,问题出在Groundhog自动地继承自基类Object,所以这里使用Object的hashCode方法生成散列码,而它默认使用对象的地址计算散列码。
可能会认为只需编写恰当的hashCode方法的覆盖版本即可,但他仍然无法运行,除非同时覆盖equals方法。HashMap使用equals判断当前的键是否与表中存在的键相同。
正确的equals必须满足5个条件:
1.自反省,对任意x,x.equals(x)一定返回true
2.对称性,对任意x和y,如果x.equals(y)为true,则y.equals(x)也为true
3.传递性,对任意x,y,z,如果有x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true
4.一致性,对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回结果应该保持一致
5.对任何不是null的x,x.equals(null)一定返回false

默认的Object.equals只是比较对象的地址,所以一个Groundhog(3)并不等于另一个Groundhog(3)。因此,如果要使用自己的类作HashMap的键,必须同时重载hashCode和equals:

public class Groundhog2 extends Groundhog {
  public Groundhog2(int n) { super(n); }
  @Override
  public int hashCode() { 
    return number; 
  }
  @Override
  public boolean equals(Object o) {
    return o instanceof Groundhog2 &&
      (number == ((Groundhog2)o).number);
  }
}
public class SpringDetector2 {
  public static void main(String[] args) throws Exception {
    SpringDetector.detectSpring(Groundhog2.class);
  }
} /*
map = {Groundhog #2=Early Spring!, Groundhog #4=Six more weeks of Winter!, Groundhog #9=Six more weeks of Winter!, Groundhog #8=Six more weeks of Winter!, Groundhog #6=Early Spring!, Groundhog #1=Six more weeks of Winter!, Groundhog #3=Early Spring!, Groundhog #7=Early Spring!, Groundhog #5=Early Spring!, Groundhog #0=Six more weeks of Winter!}
Looking up prediction for Groundhog #3
Early Spring!
*/

Groundhog2.hashCode返回标识数字做为散列码。程序员确保不同的Groundhog具有不同的编号,hashCode并不需要总是能够返回唯一的标识码。但是equals方法必须严格地判断两个对象是否相等。
equals方法只检查其参数是否是Groundhog2实例(instanceof),但是instanceof悄悄地检查了此对象是否为null,因为如果instanceof左边的参数为null,它会返回false。然后比较number数值。

理解hashCode()

前面只是说明散列的数据结构(HashSet、HashMap、LinkedHashMap和LinkedHashSet)要正确处理键必须覆盖hashCode和equals方法。要很好的解决问题,必须了解数据结构的内部构造。
首先,散列的目的在于:想要使用一个对象来查找另一个对象。不过可以使用TreeMap或者自己实现的Map达到目的。与散列实现相反,下面是一对ArrayLists实现了一个Map,包含了Map接口的完整实现,因此提供了entrySet方法:

public class SlowMap<K,V> extends AbstractMap<K,V> {
  private List<K> keys = new ArrayList<K>();
  private List<V> values = new ArrayList<V>();
  @Override
  public V put(K key, V value) {
    V oldValue = get(key);
    if(!keys.contains(key)) {
      keys.add(key);
      values.add(value);
    } else {
      values.set(keys.indexOf(key), value);
    }
    return oldValue;
  }
  @Override
  public V get(Object key) {
    if(!keys.contains(key)) {
      return null;
    }
    return values.get(keys.indexOf(key));
  }
  @Override
  public Set<Entry<K,V>> entrySet() {
    Set<Entry<K,V>> set= new HashSet<Entry<K,V>>();
    Iterator<K> ki = keys.iterator();
    Iterator<V> vi = values.iterator();
    while(ki.hasNext()) {
      set.add(new MapEntry<K,V>(ki.next(), vi.next()));
    }
    return set;
  }
  public static void main(String[] args) {
    SlowMap<String,String> m= new SlowMap<String,String>();
    m.putAll(Countries.capitals(15));
    System.out.println(m);
    System.out.println(m.get("BULGARIA"));
    System.out.println(m.entrySet());
  }
} /*
{CAMEROON=Yaounde, CHAD=N'djamena, CONGO=Brazzaville, CAPE VERDE=Praia, ALGERIA=Algiers, COMOROS=Moroni, CENTRAL AFRICAN REPUBLIC=Bangui, BOTSWANA=Gaberone, BURUNDI=Bujumbura, BENIN=Porto-Novo, BULGARIA=Sofia, EGYPT=Cairo, ANGOLA=Luanda, BURKINA FASO=Ouagadougou, DJIBOUTI=Dijibouti}
Sofia
[CAMEROON=Yaounde, CHAD=N'djamena, CONGO=Brazzaville, CAPE VERDE=Praia, ALGERIA=Algiers, COMOROS=Moroni, CENTRAL AFRICAN REPUBLIC=Bangui, BOTSWANA=Gaberone, BURUNDI=Bujumbura, BENIN=Porto-Novo, BULGARIA=Sofia, EGYPT=Cairo, ANGOLA=Luanda, BURKINA FASO=Ouagadougou, DJIBOUTI=Dijibouti]
*/

put()方法只是将键与值放入相应的ArrayList。为了与Map接口一致,他必须返回旧的键,或者在没有任何旧键的情况下返回null。同样的,get会在键不在的时候产生null。get中的key是Object类型,而不是参数化类型K。这是将泛型注入到Java语言中的时刻如此 之晚所导致的结果——如果泛型是Java语言最初就具备的属性,那么get就可以执行其参数的类型。
Map.entrySet()方法必须产生一个Map.Entry对象集。但是,Map.Entry是一个接口,用来描述依赖于实现的结构,因此必须同时定义Map.Entry的实现:

public class MapEntry<K,V> implements Map.Entry<K,V> {
  private K key;
  private V value;
  public MapEntry(K key, V value) {
    this.key = key;
    this.value = value;
  }
  @Override
  public K getKey() { return key; }
  @Override
  public V getValue() { return value; }
  @Override
  public V setValue(V v) {
    V result = value;
    value = v;
    return result;
  }
  @Override
  public int hashCode() {
    return (key==null ? 0 : key.hashCode()) ^
      (value==null ? 0 : value.hashCode());
  }
  @Override
  public boolean equals(Object o) {
    if(!(o instanceof MapEntry)) { return false; }
    MapEntry me = (MapEntry)o;
    return
      (key == null ?
       me.getKey() == null : key.equals(me.getKey())) &&
      (value == null ?
       me.getValue()== null : value.equals(me.getValue()));
  }
  @Override
  public String toString() { return key + "=" + value; }
}

entrySet使用了HashSet来保存键值对,并且MapEntry采用了一种简单的方式,即只使用key的hashCode方法。entrySet的恰当实现应该在Map中提供视图而不是副本,并且这个视图允许对原始映射表进行修改。
MapEntry中的equals必须同时检查键和值。

为速度而散列

SlowMap的键没有按照任何特定顺序保存,所以只能使用简单的线性查询。
散列的价值在于速度:散列使得查询得以快速进行。由于瓶颈位于键的查询速度,因此解决方案之一就是保持键的排序状态,然后使用Collections.binarySearch进行查询。
散列则更进一步,使用数组(存储一组元素最快的数据结构)来表示键的信息(不是键本身)。但数组不能调整容量。所以数组并不保存键本身。而是通过键对象生成一个数字,将其作为数组的下标。这个数字就是散列码。不同的键可以产生相同的下标,也就是说,可能会有冲突,因此,数组多大就不重要了,任何键总能在数组中找到它的位置。
于是查询一个值的过程首先是计算散列码,然后使用散列码查询数组。如果没有冲突,那就有了一个完美的散列函数。通常,冲突由外部链接处理:数组并不直接保存值,而是保存值的list。然后对list的值使用equals方法进行线性查询(这部分较慢)。但是如果散列函数好的话,数组的每个位置就只有较少的值。因此,不是查询整个list,而是快速地跳到数组的某个位置,只对很少的元素进行比较。这就是HashMap如此快的原因。

public class SimpleHashMap<K,V> extends AbstractMap<K,V> {
  // 选择一个质数做为哈希表的大小,以实现均匀分布
  static final int SIZE = 997;
  // 泛型数组
  @SuppressWarnings("unchecked")
  LinkedList<MapEntry<K,V>>[] buckets = new LinkedList[SIZE];
  @Override
  public V put(K key, V value) {
    V oldValue = null;
    int index = Math.abs(key.hashCode()) % SIZE;
    if(buckets[index] == null) {
      buckets[index] = new LinkedList<MapEntry<K,V>>();
    }
    LinkedList<MapEntry<K,V>> bucket = buckets[index];
    MapEntry<K,V> pair = new MapEntry<K,V>(key, value);
    boolean found = false;
    ListIterator<MapEntry<K,V>> it = bucket.listIterator();
    while(it.hasNext()) {
      MapEntry<K,V> iPair = it.next();
      if(iPair.getKey().equals(key)) {
        oldValue = iPair.getValue();
        it.set(pair);
        found = true;
        break;
      }
    }
    if(!found) {
      buckets[index].add(pair);
    }
    return oldValue;
  }
  @Override
  public V get(Object key) {
    int index = Math.abs(key.hashCode()) % SIZE;
    if(buckets[index] == null) { return null; }
    for(MapEntry<K,V> iPair : buckets[index]) {
      if(iPair.getKey().equals(key)) {
        return iPair.getValue();
      }
    }
    return null;
  }
  @Override
  public Set<Entry<K,V>> entrySet() {
    Set<Entry<K,V>> set= new HashSet<Entry<K,V>>();
    for(LinkedList<MapEntry<K,V>> bucket : buckets) {
      if(bucket == null) { continue; }
      for(MapEntry<K,V> mpair : bucket) {
        set.add(mpair);
      }
    }
    return set;
  }
  public static void main(String[] args) {
    SimpleHashMap<String,String> m =
      new SimpleHashMap<String,String>();
    m.putAll(Countries.capitals(25));
    System.out.println(m);
    System.out.println(m.get("ERITREA"));
    System.out.println(m.entrySet());
  }
} /*
{CAMEROON=Yaounde, CONGO=Brazzaville, CHAD=N'djamena, COTE D'IVOIR (IVORY COAST)=Yamoussoukro, CENTRAL AFRICAN REPUBLIC=Bangui, GUINEA=Conakry, BOTSWANA=Gaberone, BISSAU=Bissau, EGYPT=Cairo, ANGOLA=Luanda, BURKINA FASO=Ouagadougou, ERITREA=Asmara, THE GAMBIA=Banjul, KENYA=Nairobi, GABON=Libreville, CAPE VERDE=Praia, ALGERIA=Algiers, COMOROS=Moroni, EQUATORIAL GUINEA=Malabo, BURUNDI=Bujumbura, BENIN=Porto-Novo, BULGARIA=Sofia, GHANA=Accra, DJIBOUTI=Dijibouti, ETHIOPIA=Addis Ababa}
Asmara
[CAMEROON=Yaounde, CONGO=Brazzaville, CHAD=N'djamena, COTE D'IVOIR (IVORY COAST)=Yamoussoukro, CENTRAL AFRICAN REPUBLIC=Bangui, GUINEA=Conakry, BOTSWANA=Gaberone, BISSAU=Bissau, EGYPT=Cairo, ANGOLA=Luanda, BURKINA FASO=Ouagadougou, ERITREA=Asmara, THE GAMBIA=Banjul, KENYA=Nairobi, GABON=Libreville, CAPE VERDE=Praia, ALGERIA=Algiers, COMOROS=Moroni, EQUATORIAL GUINEA=Malabo, BURUNDI=Bujumbura, BENIN=Porto-Novo, BULGARIA=Sofia, GHANA=Accra, DJIBOUTI=Dijibouti, ETHIOPIA=Addis Ababa]
*/

散列表中槽位(slot)通常称为桶位(bucket)。为使散列分布均匀,桶的数量通常使用质数。为了能够自动处理冲突,使用了一个LinkedList的数组。
对于put方法方法,按照数组的尺寸取模。如果数组的某个位置是null,这表示还没有元素被散列至此。查看当前位置的list中是否有相同的元素,如果有则将旧的值赋值给oldValue,然后用新的值取代旧值。标记found用来追踪是否找到旧的键值对,如果没有,则将新的对添加到list的末尾。
这个实现并不意味着对性能进行了优化,只是展示了散列映射表执行的各种操作。

覆盖hashCode()

设计hashCode()时重要因素就是:无论何时,对同一个对象调用hashCode()都应该生成同样的值。此外,也不应该是hashCode()依赖于具有唯一性的对象信息,尤其是使用this的值。因为这样做无法生成一个新的键,使之与put中元素的键值对中的键相同。
String为例:程序中有多个String对象,都包含相同的字符串序列,那么这些String对象都映射到同一块内存区域。所以new String(“hello”)生成的两个实例,虽然相互独立的,但是对它们使用hashCode()应该产生相同的结果。

public class StringHashCode {
  public static void main(String[] args) {
    String[] hellos = "Hello Hello".split(" ");
    System.out.println(hellos[0].hashCode());
    System.out.println(hellos[1].hashCode());
  }
} /*
69609650
69609650
*/

要想使hashCode()实用,必须速度快,必须基于对象的内容生成散列码,散列码不必是独一无二的,但是通过hashCode和equals必须能够完全确定对象的身份。
因为在生成桶的下标之前,hashCode还需要做进一步的处理,所以散列码的生成范围并不重要,只要是int即可。
还有一个因素:好的hashCode()应该产生分布均匀的散列码。
1.给int变量result赋予一个非零值常量
2.为对象内每一个有意义的域f(即每个可以做equals()操作的域)计算出一个int散列码c:
这里写图片描述

3.合并计算得到的散列码:result = 37 * resut + c
4.返回result
5.检查hashCode()最后生成的结果,确保相同的对象有相同的散列码

public class CountedString {
  private static List<String> created = new ArrayList<String>();
  private String s;
  private int id = 0;
  public CountedString(String str) {
    s = str;
    created.add(s);
    for(String s2 : created) {
      if(s2.equals(s)) {
        id++;
      }
    }
  }
  @Override
  public String toString() {
    return "String: " + s + " id: " + id + " hashCode(): " + hashCode();
  }
  @Override
  public int hashCode() {
    // 非常简单的方法: 返回s.hashCode() * id
    int result = 17;
    result = 37 * result + s.hashCode();
    result = 37 * result + id;
    return result;
  }
  @Override
  public boolean equals(Object o) {
    return o instanceof CountedString &&
      s.equals(((CountedString)o).s) &&
      id == ((CountedString)o).id;
  }
  public static void main(String[] args) {
    Map<CountedString,Integer> map =
      new HashMap<CountedString,Integer>();
    CountedString[] cs = new CountedString[5];
    for(int i = 0; i < cs.length; i++) {
      cs[i] = new CountedString("hi");
      map.put(cs[i], i);
    }
    print(map);
    for(CountedString cstring : cs) {
      print("Looking up " + cstring);
      print(map.get(cstring));
    }
  }
} /*
{String: hi id: 4 hashCode(): 146450=3, String: hi id: 1 hashCode(): 146447=0, String: hi id: 3 hashCode(): 146449=2, String: hi id: 5 hashCode(): 146451=4, String: hi id: 2 hashCode(): 146448=1}
Looking up String: hi id: 1 hashCode(): 146447
0
Looking up String: hi id: 2 hashCode(): 146448
1
Looking up String: hi id: 3 hashCode(): 146449
2
Looking up String: hi id: 4 hashCode(): 146450
3
Looking up String: hi id: 5 hashCode(): 146451
4
*/

CountedString由一个String和一个id组成,此id代表包括相同String的CountedString对象的编号。所有String都被存储在static ArrayList中,在构造器中通过迭代遍历此ArrayList完成对id的计算。
hashCode()和equals()都基于CountedString 的两个域产生结果;如果他们只基于String或者只基于id,不同的对象就可能产生相同的值。

public class Individual implements Comparable<Individual> {
  private static long counter = 0;
  private final long id = counter++;
  private String name;
  public Individual(String name) { this.name = name; }
  // 'name' is optional:
  public Individual() {}
  public String toString() {
    return getClass().getSimpleName() +
      (name == null ? "" : " " + name);
  }
  public long id() { return id; }
  public boolean equals(Object o) {
    return o instanceof Individual &&
      id == ((Individual)o).id;
  }
  public int hashCode() {
    int result = 17;
    if(name != null)
      result = 37 * result + name.hashCode();
    result = 37 * result + (int)id;
    return result;
  }
  public int compareTo(Individual arg) {
    // Compare by class name first:
    String first = getClass().getSimpleName();
    String argFirst = arg.getClass().getSimpleName();
    int firstCompare = first.compareTo(argFirst);
    if(firstCompare != 0)
    return firstCompare;
    if(name != null && arg.name != null) {
      int secondCompare = name.compareTo(arg.name);
      if(secondCompare != 0)
        return secondCompare;
    }
    return (arg.id < id ? -1 : (arg.id == id ? 0 : 1));
  }
}

comparaTO()方法有一个比较结构,因此它会产生一个排序序列,排序的规则首先要按照实际类型排序,然后如果有名字的话,按照name排序,最后按照创建的顺序排序:

public class IndividualTest {
  public static void main(String[] args) {
    Set<Individual> pets = new TreeSet<Individual>();
    for(List<? extends Pet> lp : MapOfList.petPeople.values()) {
      for(Pet p : lp) {
        pets.add(p);
      }
    }
    System.out.println(pets);
  }
} /*
[Cat Elsie May, Cat Pinkola, Cat Shackleton, Cat Stanford aka Stinky el Negro, Cymric Molly, Dog Margrett, Mutt Spot, Pug Louie aka Louis Snorkelstein Dupree, Rat Fizzy, Rat Freckly, Rat Fuzzy]
*/

由于所有宠物都有名字,因此他们首先按类型排序,然后在同类型中按照名字排序。

选择接口的不同实现

容器之间的区别通常归结为由什么在背后支持它们。也就是说,所使用的接口是有什么样的数据结构实现的。ArrayList底层由数组支持;而LinkedList是由双向链表实现,其中每个对象包含数据的同时还包含执行链表的前一个与后一个元素引用。因此,如果经常在表中插入或删除元素,LinkedList就比较合适;否则,应该使用速度更快的ArrayList。
Set中,HashSet是最常用的,查询速度最快;LinkedHashSet保持元素插入的次序;TreeSet基于Treemap
,生成一个总是处于排序状态的Set。

性能测试框架

待测容器类型是泛型参数C:

public abstract class Test<C> {
  String name;
  public Test(String name) { this.name = name; }
  // 重写方法用于不同的测试
  // 返回测试的实际重复次数
  abstract int test(C container, TestParam tp);
}

每个Test对象都存储了该测试的名字。当调用test方法时,必须给待测容器,以及信使或数据传输对象,它们保存有用于该特定测试的各种参数。这些参数包括size,表示在容器中的元素数量;以及loops,用来控制该测试迭代的次数。
每个容器都要经历一系列对test的调用,每个都带有不同的TestParam,因此TestParam还包含静态的array()方法,使得创建TestParam对象数组变得更容易。array()的第一个版本接受的是可变参数列表,其中包括可互换的size和loops的值;而第二个版本接受相同类型的列表,但是它的值都在String中——通过这种方式,它可以用来解析命令行参数:

public class TestParam {
  public final int size;
  public final int loops;
  public TestParam(int size, int loops) {
    this.size = size;
    this.loops = loops;
  }
  public static TestParam[] array(int... values) {
    int size = values.length/2;
    TestParam[] result = new TestParam[size];
    int n = 0;
    for(int i = 0; i < size; i++) {
      result[i] = new TestParam(values[n++], values[n++]);
    }
    return result;
  }
  public static TestParam[] array(String[] values) {
    int[] vals = new int[values.length];
    for(int i = 0; i < vals.length; i++) {
      vals[i] = Integer.decode(values[i]);
    }
    return array(vals);
  }
}

为了使用这个框架,需要将待测容器以及test对象列表传递给Tester.run()。Tester.run()方法调用适当的重载构造器,然后调用timedTest(),它会执行针对该容器的列表中的每个测试。timedTest()会使用paramList中的每个TestParam对象进行重复测试。因为paramList是从静态的defaultParams数组中初始化出来的,因此可以重新赋值defaultParam,来修改用于所有测试的paramList,或者可以通过传递针对某个测试的定制paramList,来修改用于该测试的paramList:

public class Tester<C> {
  public static int fieldWidth = 8;
  public static TestParam[] defaultParams= TestParam.array(
    10, 5000, 100, 5000, 1000, 5000, 10000, 500);
  protected C initialize(int size) { return container; }
  protected C container;
  private String headline = "";
  private List<Test<C>> tests;
  private static String stringField() {
    return "%" + fieldWidth + "s";
  }
  private static String numberField() {
    return "%" + fieldWidth + "d";
  }
  private static int sizeWidth = 5;
  private static String sizeField = "%" + sizeWidth + "s";
  private TestParam[] paramList = defaultParams;
  public Tester(C container, List<Test<C>> tests) {
    this.container = container;
    this.tests = tests;
    if(container != null)
      headline = container.getClass().getSimpleName();
  }
  public Tester(C container, List<Test<C>> tests,
      TestParam[] paramList) {
    this(container, tests);
    this.paramList = paramList;
  }
  public void setHeadline(String newHeadline) {
    headline = newHeadline;
  }

  public static <C> void run(C cntnr, List<Test<C>> tests){
    new Tester<C>(cntnr, tests).timedTest();
  }
  public static <C> void run(C cntnr,
      List<Test<C>> tests, TestParam[] paramList) {
    new Tester<C>(cntnr, tests, paramList).timedTest();
  }
  private void displayHeader() {
    // Calculate width and pad with '-':
    int width = fieldWidth * tests.size() + sizeWidth;
    int dashLength = width - headline.length() - 1;
    StringBuilder head = new StringBuilder(width);
    for(int i = 0; i < dashLength/2; i++) {
      head.append('-');
    }
    head.append(' ');
    head.append(headline);
    head.append(' ');
    for(int i = 0; i < dashLength/2; i++) {
      head.append('-');
    }
    System.out.println(head);
    // Print column headers:
    System.out.format(sizeField, "size");
    for(Test test : tests) {
      System.out.format(stringField(), test.name);
    }
    System.out.println();
  }
  public void timedTest() {
    displayHeader();
    for(TestParam param : paramList) {
      System.out.format(sizeField, param.size);
      for(Test<C> test : tests) {
        C kontainer = initialize(param.size);
        long start = System.nanoTime();
        int reps = test.test(kontainer, param);
        long duration = System.nanoTime() - start;
        long timePerRep = duration / reps;
        System.out.format(numberField(), timePerRep);
      }
      System.out.println();
    }
  }
}

stringField()和numberField()方法会产生用于输出结果的格式化字符串,格式化的标准宽度可以通过修改静态的fieldWidth的值进行调整。displayHeader()方法为每个测试格式化和打印头信息。
如果需要执行特殊的初始化,可以覆盖initialize()方法,这将产生具有恰当尺寸的容器对象。test()方法中可以看到,其结果被捕获在一个被称为kontainer的局部引用中,这使得你可以将所存储的成员contaner替换为完全不同的被初始化的容器。
每个Test.test()方法的返回值都必须是该测试执行的操作的数量,这些测试都会计算其所有操作所需的纳秒数。通常System.nanoTime()所产生的值的粒度都会大于1,因此,在结果中可能会存在某些时间点上的重合。

对List的选择
public class ListPerformance {
  static Random rand = new Random();
  static int reps = 1000;
  static List<Test<List<Integer>>> tests = new ArrayList<Test<List<Integer>>>();
  static List<Test<LinkedList<Integer>>> qTests = new ArrayList<Test<LinkedList<Integer>>>();
  static {
    tests.add(new Test<List<Integer>>("add") {
      int test(List<Integer> list, TestParam tp) {
        int loops = tp.loops;
        int listSize = tp.size;
        for(int i = 0; i < loops; i++) {
          list.clear();
          for(int j = 0; j < listSize; j++)
            list.add(j);
        }
        return loops * listSize;
      }
    });
    tests.add(new Test<List<Integer>>("get") {
      int test(List<Integer> list, TestParam tp) {
        int loops = tp.loops * reps;
        int listSize = list.size();
        for(int i = 0; i < loops; i++)
          list.get(rand.nextInt(listSize));
        return loops;
      }
    });
    tests.add(new Test<List<Integer>>("set") {
      int test(List<Integer> list, TestParam tp) {
        int loops = tp.loops * reps;
        int listSize = list.size();
        for(int i = 0; i < loops; i++)
          list.set(rand.nextInt(listSize), 47);
        return loops;
      }
    });
    tests.add(new Test<List<Integer>>("iteradd") {
      int test(List<Integer> list, TestParam tp) {
        final int LOOPS = 1000000;
        int half = list.size() / 2;
        ListIterator<Integer> it = list.listIterator(half);
        for(int i = 0; i < LOOPS; i++)
          it.add(47);
        return LOOPS;
      }
    });
    tests.add(new Test<List<Integer>>("insert") {
      int test(List<Integer> list, TestParam tp) {
        int loops = tp.loops;
        for(int i = 0; i < loops; i++)
          list.add(5, 47);
        return loops;
      }
    });
    tests.add(new Test<List<Integer>>("remove") {
      int test(List<Integer> list, TestParam tp) {
        int loops = tp.loops;
        int size = tp.size;
        for(int i = 0; i < loops; i++) {
          list.clear();
          list.addAll(new CountingIntegerList(size));
          while(list.size() > 5)
            list.remove(5);
        }
        return loops * size;
      }
    });
    // Tests for queue behavior:
    qTests.add(new Test<LinkedList<Integer>>("addFirst") {
      int test(LinkedList<Integer> list, TestParam tp) {
        int loops = tp.loops;
        int size = tp.size;
        for(int i = 0; i < loops; i++) {
          list.clear();
          for(int j = 0; j < size; j++)
            list.addFirst(47);
        }
        return loops * size;
      }
    });
    qTests.add(new Test<LinkedList<Integer>>("addLast") {
      int test(LinkedList<Integer> list, TestParam tp) {
        int loops = tp.loops;
        int size = tp.size;
        for(int i = 0; i < loops; i++) {
          list.clear();
          for(int j = 0; j < size; j++)
            list.addLast(47);
        }
        return loops * size;
      }
    });
    qTests.add(
      new Test<LinkedList<Integer>>("rmFirst") {
        int test(LinkedList<Integer> list, TestParam tp) {
          int loops = tp.loops;
          int size = tp.size;
          for(int i = 0; i < loops; i++) {
            list.clear();
            list.addAll(new CountingIntegerList(size));
            while(list.size() > 0)
              list.removeFirst();
          }
          return loops * size;
        }
      });
    qTests.add(new Test<LinkedList<Integer>>("rmLast") {
      int test(LinkedList<Integer> list, TestParam tp) {
        int loops = tp.loops;
        int size = tp.size;
        for(int i = 0; i < loops; i++) {
          list.clear();
          list.addAll(new CountingIntegerList(size));
          while(list.size() > 0)
            list.removeLast();
        }
        return loops * size;
      }
    });
  }
  static class ListTester extends Tester<List<Integer>> {
    public ListTester(List<Integer> container,
        List<Test<List<Integer>>> tests) {
      super(container, tests);
    }
    // 每次测试前填入合适的尺寸
    @Override protected List<Integer> initialize(int size){
      container.clear();
      container.addAll(new CountingIntegerList(size));
      return container;
    }
    // 便利方法
    public static void run(List<Integer> list,
        List<Test<List<Integer>>> tests) {
      new ListTester(list, tests).timedTest();
    }
  }
  public static void main(String[] args) {
    if(args.length > 0) {
      Tester.defaultParams = TestParam.array(args);
    }
    // 只能对数组进行这两个测试
    Tester<List<Integer>> arrayTest =
      new Tester<List<Integer>>(null, tests.subList(1, 3)){
        // 这将在每次测试之前调用,产生一个不可调整列表
        @Override
        protected List<Integer> initialize(int size) {
          Integer[] ia = Generated.array(Integer.class,
            new CountingGenerator.Integer(), size);
          return Arrays.asList(ia);
        }
      };
    arrayTest.setHeadline("Array as List");
    arrayTest.timedTest();
    Tester.defaultParams= TestParam.array(
      10, 5000, 100, 5000, 1000, 1000, 10000, 200);
    if(args.length > 0) {
      Tester.defaultParams = TestParam.array(args);
    }
    ListTester.run(new ArrayList<Integer>(), tests);
    ListTester.run(new LinkedList<Integer>(), tests);
    ListTester.run(new Vector<Integer>(), tests);
    Tester.fieldWidth = 12;
    Tester<LinkedList<Integer>> qTest =
      new Tester<LinkedList<Integer>>(
        new LinkedList<Integer>(), qTests);
    qTest.setHeadline("Queue tests");
    qTest.timedTest();
  }
} /*
--- Array as List ---
 size     get     set
   10     130     183
  100     130     164
 1000     129     165
10000     129     165
--------------------- ArrayList ---------------------
 size     add     get     set iteradd  insert  remove
   10     121     139     191     435    3952     446
  100      72     141     191     247    3934     296
 1000      98     141     194     839    2202     923
10000     122     144     190    6880   14042    7333
--------------------- LinkedList ---------------------
 size     add     get     set iteradd  insert  remove
   10     182     164     198     658     366     262
  100     106     202     230     457     108     201
 1000     133    1289    1353     430     136     239
10000     172   13648   13187     435     255     239
----------------------- Vector -----------------------
 size     add     get     set iteradd  insert  remove
   10     129     145     187     290    3635     253
  100      72     144     190     263    3691     292
 1000      99     145     193     846    2162     927
10000     108     145     186    6871   14730    7135
-------------------- Queue tests --------------------
 size    addFirst     addLast     rmFirst      rmLast
   10         199         163         251         253
  100          98          92         180         179
 1000          99          93         216         212
10000         111         109         262         384
*/

对于背后由数组支持的List和ArrayList,无论列表的大小如何,这些访问都很快速和一致;而对LinkedList,访问时间对于较大的列表将明显增加。很显然,如果需要执行大量的随机访问,链接链表不会是一种好的选择。
iteradd测试使用迭代器在列表中间插入新的元素。对于ArrayList,当列表变大时,其开销将变得很高昂,但是对于LinkedList,相对来说比较低廉,并且不随列表尺寸而发生变化。这是因为ArrayList在插入时,必须创建空间并将它的所有引用向前移动,这会随ArrayList的尺寸增加而产生高昂的代价。LinkedList只需链接新的元素,而不必修改列表中剩余的元素,因此可以认为无论列表尺寸如何变化,其代价大致相同。
在LinkedList中的插入和移除代价相当低廉,并且不随列表尺寸发生变化,但是对于ArrayList,插入操作代价特别高昂,并且其代价将随列表尺寸的增加而增加。
对于随机访问的get()和set()操作,背后由数组支撑的List只比ArrayList稍快一点。
应该避免使用Vertor,它只存在于支持遗留代码的类库中(在此程序中它能正常工作的唯一原因,只是因为为了向前兼容,他被适配成了List)。
最佳的做法可能是将ArrayList做为默认首选,只要需要使用额外的功能,或者当程序的性能因为经常从表中间进行插入和删除而变差的时候,才会选择LinkedList。如果使用的是固定数量的元素,那么既可以选择使用背后有数组支撑的List,也可以选择真正的数组。
CopyOnWriteArrayList是List的一个特殊实现,专门用于并发编程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值