前面介绍了两种集合的用法,它们的共性为每一个元素都是惟一的,区别在于一个无序一个有序。虽然说往集合里面保存数据还算容易,但要从集合中取出数据就没那么方便了,由于集合竟然不提供get方法,没有get方法怎么从一堆数据之中挑出你想要的东西呢?难道只能从头遍历集合的全部元素,再逐个加以辨别吗?显然这个缺陷是集合的硬伤,比如去银行开帐户,存钱的时候你们都开开心心,但是等到取钱的时候,却发现柜员拿出一叠存单一张一张找过去,等找到你的存单之时,黄花菜儿都凉了。所以,实际开发中通常不多直接使用集合,而是使用集合的升级版本——映射。
映射指的是两个实体之间存在一对一的关系,例如一个身份证号码对应某个公民,一个书名对应某本书籍等等。有了映射关系以后,从一堆数据中寻找目标对象就好办了,只要给定目标对应的号码或者名称,根据映射关系可以马上找到号码或名称表明的对象。这样下次去银行取钱,就没必要等柜员兀自地翻存单,只要在电脑上输入身份证号,便可自动找到当初的存款记录。
Java编程经过“键值对”的概念来表达映射关系,它包含“键名”和“键值”两个实体,且键名与键值是一一对应的,相同的键名指向的键值也必然是相同的。如此一来,映射里面的每一个元素都是一组键值对,即“Key→Value”,在代码中采起形如“Map<Key, Value>”的格式来表达,其中Key表示键名的数据类型,Value表示键值的数据类型。往映射里面保存数据的时候,须要填写完整的键值对信息;而从映射中取出数据,只需提供键名便可得到相应的键值信息。
因为Map属于接口,所以开发过程一般调用它的两个实现类,包括哈希图HashMap和红黑树TreeMap。映射与集合密切相关,它们的存储原理也相似,好比HashMap和HashSet同样采起哈希表结构,而TreeMap和TreeSet同样采起二叉树结构;不一样的是,映射元素的惟一性和有序性是由各元素的键名决定的。由于HashMap和TreeMap仅仅是内部存储结构存在差别,外部的代码调用仍然保持一致,因此接下来就以HashMap为例阐述映射的具体用法。
与HashSet相比,HashMap在编码上主要有三处改动,分别说明以下。
1、建立映射实例必须同时指定键名和键值的数据类型,即HashMap后面的那对尖括号内部要有两个类型名称。下面是建立一个手机映射的代码例子:html
// 建立一个哈希映射,该映射的键名为String类型,键值为MobilePhone类型 HashMap<String, MobilePhone> map = new HashMap<String, MobilePhone>();
2、往映射中添加新的键值对,调用的是put方法而非add方法,而且put方法的第一个参数为新元素的键名,第二个参数为新元素的键值。若是映射内部不存在该键名,则映射会直接增长一组键值对;若是映射已经存在该键名,则映射会自动将新的键值覆盖旧的键值。给手机映射添加若干组手机信息的代码示例以下:java
map.put("米8", new MobilePhone("小米", 3000)); map.put("Mate20", new MobilePhone("华为", 6000)); map.put("荣耀10", new MobilePhone("荣耀", 2000)); map.put("红米6", new MobilePhone("红米", 1000)); map.put("OPPO R17", new MobilePhone("OPPO", 2800));
3、遍历映射内部的全部元素,也有好几种方式,依次说明以下。
一、经过迭代器遍历。首先调用映射实例的entrySet得到该映射的集合入口,再调用入口对象的iterator方法得到映射的迭代器,而后使用迭代器遍历整个映射。在遍历过程当中,每次调用next方法获得的是下个位置的键值对记录,此时还需调用该记录的getKey方法才能获取键值对中的键名,调用getValue方法获取键值对中的键值。详细的迭代器遍历代码以下所示:面试
// 第一种遍历方式:显式指针,即便用迭代器 Set<Map.Entry<String, MobilePhone>> entry_set = map.entrySet(); Iterator<Map.Entry<String, MobilePhone>> iterator = entry_set.iterator(); while (iterator.hasNext()) { // 注意这里要先把入口取出来,这样才能分别getKey和getValue Map.Entry<String, MobilePhone> iterator_item = iterator.next(); // 获取该键值对的键名 String key = iterator_item.getKey(); // 获取该键值对的键值 MobilePhone value = iterator_item.getValue(); System.out.println(String.format("iterator_item key=%s, value=%s %d", key, value.getBrand(), value.getPrice())); }
二、经过for循环遍历。第一种遍历方式能够看到明确的迭代器对象,故而又被称做显式指针。其实迭代器仅仅起到了指示的做用,它彻底能够被简化的for循环所取代。尽管在for循环中看不到迭代器对象,但编译器知道这里有个隐含着的迭代器,所以for循环遍历也被称做隐式指针。下面是采起for循环遍历手机映射的代码例子:编程
// 第二种遍历方式:隐式指针,即便用for循环 for (Map.Entry<String, MobilePhone> for_item : map.entrySet()) { // 获取该键值对的键名 String key = for_item.getKey(); // 获取该键值对的键值 MobilePhone value = for_item.getValue(); System.out.println(String.format("for_item key=%s, value=%s %d", key, value.getBrand(), value.getPrice())); }
三、经过键名集合遍历。有别于上述两种依次遍历键值对的方式,第三种方式先调用映射的keySet方法得到只包含键名的集合,再经过遍历键名集合来获取每一个键名对应的键值。该方式的映射遍历代码示例以下:性能
// 第三种遍历方式:先得到键名的集合,再经过键名集合遍历整个映射 // 注意:HashMap的keySet方法返回的是无序集合 Set<String> key_set = map.keySet(); for (String key : key_set) { // 经过键名获取该键值对的键值 MobilePhone value = map.get(key); System.out.println(String.format("set_item key=%s, value=%s %d", key, value.getBrand(), value.getPrice())); }
四、经过forEach方法遍历。显然前面的几种方式都很啰嗦,自从Java8引入了Lambda表达式,遍历映射的全部元素也变得异常简洁了,单单下面一行代码就所有搞定:编码
// 第四种遍历方式:使用forEach方法夹带Lambda表达式进行遍历 map.forEach((key, value) -> System.out.println(String.format("each_item key=%s, value=%s %d", key, value.getBrand(), value.getPrice())) );
最后简要描述一下TreeMap背后的红黑树概念,这是各家公司面试Java开发人员的常见知识点。红黑树是一种自平衡二叉查找树,所谓平衡指的是像天平那样左右两边的重量相等,从而使天平保持不偏不倚的水平状态。固然对于二叉树来讲,绝对的平衡是难以达到的,只能作到相对平衡,即左右两棵子树的高度差不大于1,同时左右两棵子树自己也是平衡二叉树。鉴于平衡二叉树的平衡特性,从根节点出发到达每一个叶子节点的距离比较平均,使得整棵树的查找性能相对优越。高度平衡的二叉树在进行查找操做时表现近乎完美,可是给它添加新节点或者删除原节点的时候,二叉树为了在节点增删以后仍然保持高度平衡,可能不得很少次左旋右旋,一旦这棵树遇到频繁的节点添加和删除操做,它的总体性能将会急剧降低,真可谓鱼与熊掌不可兼得。
为了兼顾平衡二叉树的查找性能和节点增删后的旋转性能,另外一种折中的自平衡二叉树(即红黑树)应运而生,红黑树并不是高度平衡的,而是一种相对平衡,它的每次插入操做最多只要两次旋转,删除操做最多只要三次旋转。平衡二叉树要求左右子树的高度差不大于1,而红黑树只要求左右子树的高度差不大于根节点到叶子节点的最短距离,也就是说,根节点到叶子节点的最长距离不大于最短距离的两倍。红黑树不处于最理想的平衡状态,而是处于大体平衡的状态,那么对它的叶子节点进行增删之时,这样无论左旋仍是右旋,只需少数几回旋转就能让左右字书的高度差保持在合理的范围内了。指针
更多Java技术文章参见《Java开发笔记(序)章节目录》orm