Bootstrap

java集合(Map)

Map

Map用于保存具有映射关系的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的valuekey和value都可以是任何引用类型的数据。 Map的key不允许重复,即同一个Map对象的任何两个key通过equals方法比较总是返回false。

在这里插入图片描述
从图中可以看出,如果把Map里的所有key放在一起来看,它们就组成了一个Set集合(所有的key没有顺序,key与key之间不能重复),实际上Map确实包含了一个keySet()方法,用于返回Map里所有key组成的Set集合。

提示:
Set与Map之间的关系非常密切。虽然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接口中定义了如下常用的方法

  • void clear():删除该Map对象中的所有key-value对。
  • boolean containsKey(Object key):查询Map中是否包含指定的key,如果包含则返回true。
  • boolean containsValue(Object value):查询Map中是否包含一个或多个value,如果包含则返回true。
  • Set entrySet():返回Map中包含的key-value对所组成的Set集合,每个集合元素都是Map.Entry (Entry是Map的内部类)对象。
  • Object get(Object key):返回指定key所对应的value;如果此Map中不包含该key,则返回null。
  • boolean isEmpty():查询该Map是否为空(即不包含任何key-value对),如果为空则返回true。
  • Set keySet():返回该Map中所有key组成的Set集合。
  • Object put(Object key, Object value):添加一个key-value对,如果当前Map中已有一个与该key相等的key-value对,则新的key-value对会覆盖原来的key-value对。
  • void putAll(Map m):将指定Map中的key-value对复制到本Map中。
  • Object remove(Object key):删除指定key所对应的key-value对,返回被删除key所关联的value,如果该key不存在,则返回null。
  • int size():返回该Map里的key-value对的个数。
  • Collection values():返回该Map里所有value组成的Collection。
    在这里插入图片描述
    Map中包括一个内部类Entry,该类封装了一个key-value对。
    Entry包含如下三个方法
  • Object getKey():返回该Entry里包含的key值。
  • Object getValue():返回该Entry里包含的value值。
  • Object setValue(V value):设置该Entry里包含的value值,并返回新设置的value值。

1.1 HashMap和Hashtable实现类

HashMap和Hashtable都是Map接口的典型实现类,它们之间的关系完全类似于ArrayList和Vector的关系:Hashtable是一个古老的Map实现类,它从JDK 1.0起就已经出现了,当它出现时,Java还没有提供Map接口,所以它包含了两个烦琐的方法,即elements()(类似于Map接口定义的values()方法)和keys()(类似于Map接口定义的keySet()方法),现在很少使用这两个方法。

除此之外,Hashtable和HashMap存在两点典型区别

  • Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以HashMap比Hashtable的性能高一点;但如果有多个线程访问同一个Map对象时,使用Hashtable实现类会更好。
  • Hashtable不允许使用null作为key和value,如果试图把null值放进Hashtable中,将会引发NullPointerException异常;但HashMap可以使用null作为key或value。

由于HashMap里的key不能重复,所以HashMap里最多只有一个key-value对的key为null,但可以有无数多个key-value对的value为null。下面程序示范了用null值作为HashMap的key和value的情形。

        public class NullInHashMap
        {
            public static void main(String[] args)
            {
                  HashMap hm=new HashMap();
                  //试图将两个key为null值的key-value对放入HashMap中
                  hm.put(null , null);
                  hm.put(null , null);   //①
                  //将一个value为null值的key-value对放入HashMap中
                  hm.put("a" , null);   //②
                  //输出Map对象
                  System.out.println(hm);
            }
        }

运行结果:

        {null=null, a=null}

根据上面输出结果可以看出,HashMap重写了toString()方法,实际上所有的Map实现类都重写了toString()方法,调用Map对象的toString()方法总是返回如下格式的字符串:{key1=value1,key2=value2…}。

为了成功地在HashMap、Hashtable中存储、获取对象,用作key的对象必须实现hashCode()方法和equals()方法。

与HashSet集合不能保证元素的顺序一样,HashMap、Hashtable也不能保证其中key-value对的顺序。类似于HashSet,HashMap、Hashtable判断两个key相等的标准也是:两个key通过equals()方法比较返回true,两个key的hashCode值相等

除此之外,HashMap、Hashtable中还包含一个containsValue()方法,用于判断是否包含指定的value。那么HashMap、Hashtable如何判断两个value相等呢?HashMap、Hashtable判断两个value相等的标准更简单:只要两个对象通过equals()方法比较返回true即可。

下面程序示范了Hashtable判断两个key相等的标准和两个value相等的标准。

        class A
        {
            int count;
            public A(int count)
            {
                  this.count=count;
            }
            //根据count的值来判断两个对象是否相等
            public boolean equals(Object obj)
            {
                  if (obj==this)
                        return true;
                  if (obj!=null &&
                        obj.getClass()==A.class)
                  {
                        A a=(A)obj;
                        return this.count==a.count;
                  }
                  return false;
            }
            //根据count来计算hashCode值
            public int hashCode()
            {
                  return this.count;
            }
        }
        class B
        {
            //重写equals()方法,B对象与任何对象通过equals()方法比较都相等
            public boolean equals(Object obj)
            {
                  return true;
            }
        }
        public class HashtableTest
        {
            public static void main(String[] args)
            {
                  Hashtable ht=new Hashtable();
                  ht.put(new A(60000) , "疯狂Java讲义");
                  ht.put(new A(87563) , "轻量级Java EE企业应用实战");
                  ht.put(new A(1232) , new B());
                  System.out.println(ht);
                  //只要两个对象通过equals()方法比较返回true
                  //Hashtable就认为它们是相等的value
                  //由于Hashtable中有一个B对象
                  //它与任何对象通过equals()方法比较都相等,所以下面输出true
                  System.out.println(ht.containsValue("测试字符串"));  //①
                  //只要两个A对象的count相等,它们通过equals()方法比较返回true,且hashCode值相等
                  //Hashtable即认为它们是相同的key,所以下面输出true
                  System.out.println(ht.containsKey(new A(87563)));   //②
                  //下面语句可以删除最后一个key-value对
                  ht.remove(new A(1232));   //③
                  //通过返回Hashtable的所有key组成的Set集合
                  //从而遍历Hashtable的每个key-value对
                  for (Object key : ht.keySet())
                  {
                        System.out.print(key + "---->");
                        System.out.print(ht.get(key) + "\n");
                  }
            }
        }

根据Hashtable判断两个key相等的标准,程序在②处也将输出true,因为两个A对象虽然不是同一个对象,但它们通过equals()方法比较返回true,且hashCode值相等,Hashtable即认为它们是同一个key。类似的是,程序在③处也可以删除对应的key-value对。

程序最后还示范了如何遍历Map中的全部key-value对:调用Map对象的keySet()方法返回全部key组成的Set集合,通过遍历该Set集合的所有元素(就是Map的全部key)就可以遍历Map中的所有key-value对。

与HashSet类似的是,如果使用可变对象作为HashMap、Hashtable的key,并且程序修改了作为key的可变对象,则也可能出现与HashSet类似的情形:程序再也无法准确访问到Map中被修改过的key

        public class HashtableErrorTest
        {
            public static void main(String[] args)
            {
                  Hashtable ht=new Hashtable();
                  //此处的A类与前一个程序的A类是同一个类
                  ht.put(new A(60000) , "疯狂Java讲义");
                  ht.put(new A(87563) , "轻量级Java EE企业应用实战");
                  //获得Hashtable的key Set集合对应的Iterator迭代器
                  Iterator it=ht.keySet().iterator();
                  //取出Map中第一个key
                  A first=(A)it.next();
                  first.count=87563;   //①
                  //输出{A@1560b=疯狂Java讲义, A@1560b=轻量级Java EE企业应用实战}
                  System.out.println(ht);
                  //只能删除没有被修改过的key所对应的key-value对
                  ht.remove(new A(87563));
                  System.out.println(ht);
                  //无法获取剩下的value,下面两行代码都将输出null
                  System.out.println(ht.get(new A(87563)));  //②
                  System.out.println(ht.get(new A(60000)));   //③
            }
        }

**注意:**与HashSet类似的是,尽量不要使用可变对象作为HashMap、Hashtable的key,如果确实需要使用可变对象作为HashMap、Hashtable的key,则尽量不要在程序中修改作为key的可变对象。

1.2 LinkedHashMap实现类

HashSet有一个子类是LinkedHashSet,HashMap也有一个LinkedHashMap子类;LinkedHashMap也使用双向链表来维护key-value对的次序(其实只需要考虑key的次序),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致

LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。

LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能;但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。下面程序示范了LinkedHashMap的功能:迭代输出LinkedHashMap的元素时,将会按添加key-value对的顺序输出。

        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));
                  }
            }
        }

1.3 使用Properties读写属性文件

Properties类是Hashtable类的子类,正如它的名字所暗示的,该对象在处理属性文件时特别方便(Windows操作平台上的ini文件就是一种属性文件)。Properties类可以把Map对象属性文件关联起来,从而可以把Map对象中的key-value对写入属性文件中,也可以把属性文件中的“属性名=属性值”加载到Map对象中。由于属性文件里的属性名、属性值只能是字符串类型,所以Properties里的key、value都是字符串类型。
该类提供了如下三个方法来修改Properties里的key、value值。

  • String getProperty(String key):获取Properties中指定属性名对应的属性值,类似于Map的get(Object key)方法。
  • String getProperty(String key, String defaultValue):该方法与前一个方法基本相似。该方法多一个功能,如果Properties中不存在指定的key时,则该方法指定默认值。
  • Object setProperty(String key, String value):设置属性值,类似于Hashtable的put()方法。
    除此之外,它还提供了两个读写Field文件的方法。
  • void load(InputStream inStream):从属性文件(以输入流表示)中加载key-value对,把加载到的key-value对追加到Properties里(Properties是Hashtable的子类,它不保证key-value对之间的次序)。
  • void store(OutputStream out, String comments):将Properties中的key-value对输出到指定的属性文件(以输出流表示)中。
        public class PropertiesTest
        {
            public static void main(String[] args)
                  throws Exception
            {
                  Properties props=new Properties();
                  //向Properties中添加属性
                  props.setProperty("username" , "yeeku");
                  props.setProperty("password" , "123456");
                  //将Properties中的key-value对保存到a.ini文件中
                  props.store(new FileOutputStream("a.ini")
                        , "comment line");   //①
                  //新建一个Properties对象
                  Properties props2=new Properties();
                  //向Properties中添加属性
                  props2.setProperty("gender" , "male");
                  //将a.ini文件中的key-value对追加到props2中
                  props2.load(new FileInputStream("a.ini") );   //②
                  System.out.println(props2);
            }
        }

上面程序示范了Properties类的用法,其中①代码处将Properties对象中的key-value对写入a.ini文件中;②代码处则从a.ini文件中读取key-value对,并添加到props2对象中。编译、运行上面程序,该程序输出结果如下:

        {password=123456, gender=male, username=yeeku}

上面程序还在当前路径下生成了一个a.ini文件,该文件的内容如下:

        #comment line
        #Thu Aug 25 18:03:49 CST 2011
        password=123456
        username=yeeku

提示:
Properties可以把key-value对以XML文件的形式保存起来,也可以从XML文件中加载key-value对,用法与此类似,此处不再赘述。

1.4 SortedMap接口和TreeMap实现类

TreeMap就是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点。TreeMap存储key-value对(节点)时,需要根据key对节点进行排序。TreeMap可以保证所有的key-value对处于有序状态

  • 自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的key应该是同一个类的对象,否则将会抛出ClassCastException异常。
  • 定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序。采用定制排序时不要求Map的key实现Comparable接口。\

类似于TreeSet中判断两个元素相等的标准,TreeMap中判断两个key相等的标准是:两个key通过compareTo()方法返回0,TreeMap即认为这两个key是相等的

如果使用自定义类作为TreeMap的key,且想让TreeMap良好地工作,则重写该类的equals()方法和compareTo()方法时应保持一致的返回结果:两个key通过equals()方法比较返回true时,它们通过compareTo()方法比较应该返回0。如果equals()方法与compareTo()方法的返回结果不一致, TreeMap与Map接口的规则就会冲突。

注意:
再次强调:Set和Map的关系十分密切,Java源码就是先实现了HashMap、TreeMap等集合,然后通过包装一个所有的value都为null的Map集合实现了Set集合类。

与TreeSet类似的是,TreeMap中也提供了一系列根据key顺序访问key-value对的方法。

  • Map.Entry firstEntry():返回该Map中最小key所对应的key-value对,如果该Map为空,则返回null。
  • Object firstKey():返回该Map中的最小key值,如果该Map为空,则返回null。
  • Map.Entry lastEntry():返回该Map中最大key所对应的key-value对,如果该Map为空或不存在这样的key-value对,则都返回null。
  • Object lastKey():返回该Map中的最大key值,如果该Map为空或不存在这样的key,则都返回null。
  • Map.Entry higherEntry(Object key):返回该Map中位于key后一位的key-value对(即大于指定key的最小key所对应的key-value对)。如果该Map为空,则返回null。
  • Object higherKey(Object key):返回该Map中位于key后一位的key值(即大于指定key的最小key值)。如果该Map为空或不存在这样的key-value对,则都返回null。
  • Map.Entry lowerEntry(Object key):返回该Map中位于key前一位的key-value对(即小于指定key的最大key所对应的key-value对)。如果该Map为空或不存在这样的key-value对,则都返回null。
  • Object lowerKey(Object key):返回该Map中位于key前一位的key值(即小于指定key的最大key值)。如果该Map为空或不存在这样的key,则都返回null。
  • NavigableMap subMap(Object fromKey, boolean fromInclusive,Object toKey, boolean toInclusive):返回该Map的子Map,其key的范围是从fromKey(是否包括取决于第二个参数)到toKey(是否包括取决于第四个参数)。
  • SortedMap subMap(Object fromKey, Object toKey):返回该Map的子Map,其key的范围是从fromKey(包括)到toKey(不包括)。
  • SortedMap tailMap(Object fromKey):返回该Map的子Map,其key的范围是大于fromKey(包括)的所有key。
  • NavigableMap tailMap(Object fromKey, boolean inclusive):返回该Map的子Map,其key的范围是大于fromKey(是否包括取决于第二个参数)的所有key。
  • SortedMap headMap(Object toKey):返回该Map的子Map,其key的范围是小于toKey(不包括)的所有key。
  • NavigableMap headMap(Object toKey, boolean inclusive):返回该Map的子Map,其key的范围是小于toKey(是否包括取决于第二个参数)的所有key。

提示: 表面上看起来这些方法很复杂,其实它们很简单。因为TreeMap中的key-value对是有序的,所以增加了访问第一个、前一个、后一个、最后一个key-value对的方法,并提供了几个从TreeMap中截取子TreeMap的方法

    class R implements Comparable
    {
        int count;
        public R(int count)
        {
              this.count=count;
        }
        public String toString()
        {
              return "R[count:" + count + "]";
        }
        //根据count来判断两个对象是否相等
        public boolean equals(Object obj)
        {
              if (this==obj)
                    return true;
              if (obj!=null
                    && obj.getClass()==R.class)
              {
                    R r=(R)obj;
                    return r.count==this.count;
              }
              return false;
        }
        //根据count属性值来判断两个对象的大小
        public int compareTo(Object obj)
        {
              R r=(R)obj;
              return count > r.count ? 1 :
                    count < r.count -1 ? -1 : 0;
        }
    }
    public class TreeMapTest
    {
        public static void main(String[] args)
        {
              TreeMap tm=new TreeMap();
              tm.put(new R(3) , "轻量级Java EE企业应用实战");
              tm.put(new R(-5) , "疯狂Java讲义");
              tm.put(new R(9) , "疯狂Android讲义");
              System.out.println(tm);
              //返回该TreeMap的第一个Entry对象
              System.out.println(tm.firstEntry());
              //返回该TreeMap的最后一个key值
              System.out.println(tm.lastKey());
              //返回该TreeMap的比new R(2)大的最小key值
              System.out.println(tm.higherKey(new R(2)));
              //返回该TreeMap的比new R(2)小的最大的key-value对
              System.out.println(tm.lowerEntry(new R(2)));
              //返回该TreeMap的子TreeMap
              System.out.println(tm.subMap(new R(-1) , new R(4)));
        }
    }

运行上面程序,看到如下运行结果:

        {R[count:-5]=疯狂Java讲义, R[count:3]=轻量级Java EE企业应用实战, R[count:9]=疯狂
        Android讲义}
        R[count:-5]=疯狂Java讲义
        R[count:9]
        R[count:3]
        R[count:-5]=疯狂Java讲义
        {R[count:3]=轻量级Java EE企业应用实战}

各Map实现类的性能分析

TreeMap通常比HashMap、Hashtable要慢(尤其在插入、删除key-value对时更慢),因为TreeMap底层采用红黑树来管理key-value对(红黑树的每个节点就是一个key-value对)。

使用TreeMap有一个好处:TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作。当TreeMap被填充之后,就可以调用keySet(),取得由key组成的Set,然后使用toArray()方法生成key的数组,接下来使用Arrays的binarySearch()方法在已排序的数组中快速地查询对象。

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

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

文章参考:《java核心讲义》

;