结合JDK源码看设计模式——组合模式
前言:
相信大家都打开过层级很多很多的文件夹。如果把第一个文件夹看作是树的根节点的话,下面的子文件夹就可以看作一个子节点。不过最终我们寻找的还是文件夹中的文件,文件可以看做是叶子节点。下面我们介绍一种模式,与这种树级结构息息相关。当然,今天的主角是HashMap。接下来我们一起来看HashMap中到底是怎么跟树级结构进行挂钩的。
一、定义
将对象组合成树形结构以表示“部分-整体”的一个层次结构,使客户端对单个对象和组合对象保持一致的方式处理。
二、适用场景
1、客户端可以忽略组合对象与单个对象的差异
注意组合模式中的概念,当客户端使用组合模式的时候是对单个对象和组合对象保持一致的方式处理,而不是不同的方式处理。所以如果客户端可以忽略组合对象和单个对象的差异时,才用组合模式。
2、处理一个树形结构
这里就很好理解了,组合模式就是处理树形结构的
三、结合HashMap看组合模式
1、抽象构件:总的抽象类或者接口,定义一些通用的方法,比如新增、删除。
2、中间构件:继承或者实现抽象构件,定义存储方式,并针对特殊需要重写抽象构件的方法。
3、叶子节点:继承或者实现抽象构件,并针对特殊需要重写抽象构件的方法。
叶子节点需要实现或者继承抽象构件,如果有需要也可以添加到中间构件上。就比如说我现在进入D盘,D盘就是一个抽象构件,无论在D盘哪里,我都可以执行新建,删除操作。中间构件就相当于文件夹,里面定义了相应的存储方式,加入的文件是叶子节点,并且会按照这种方式来存储,同时这个中间构件也可以选择再加入一个其他中间构件或者自己。叶子节点就可以看作文件。注意,在组合模式下所有类都需要直接或者间接,继承或实现总的抽象构件。下面讲HashMap中的putAll()方法
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { public void putAll(Map<? extends K, ? extends V> m) { putMapEntries(m, true); } final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // pre-size float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } } }
上面是简化的HashMap。我们看见putAll方法传入的是Map对象,Map就是一个抽象构件(同时这个构件中只支持键值对的存储格式),而HashMap是一个中间构件,HashMap中的Node节点就是叶子节点。说到中间构件就会有规定的存储方式。HashMap中的存储方式是一个静态内部类的数组Node<K,V>[] tab。下面是简化的具体代码
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } }
所以我们调用put方法实际上是加入一个叶子节点,而我们调用putAll相当于把其他Map下面的文件夹中的文件拷贝过来。还有Hashtable也可以看作一个中间构件,里面的putAll方法同样是适合放入实现Map接口的对象。这里如果将Hashtable的这个中间构件放入HashMap中,那么我们这些方法还能用吗。答案是能用,组合模式处理的就是一致性的问题。Map接口中的方法,比如put(),remove(),到下面的中间构件中其实都是一致的功能,不过就是不同的中间构件中的处理方式可能会有细微的差别。下面是我的测试代码
public class Test { public static void main(String[] args) {
Map<Integer,String> rootmap=new HashMap<Integer,String>(); rootmap.put(1,"HashMap文件1"); Map<Integer,String> map1=new Hashtable<Integer,String>(); map1.put(2,"Hashtable文件1"); map1.put(3,"Hashtable文件2"); map1.put(4,"Hashtable文件3"); rootmap.putAll(map1); System.out.println(rootmap);
} }
输出结果为:
上面实现将Hashtable中的键值对放入HashMap中。如果放在组合模式下,你就可以看作是将Hashtable这个中间构件下的文件拷贝到HashMap这个中间构件中。在我们电脑里文件任何类型的文件都可以拷到其他任意地方。但是用来理解Map这个抽象构件的就不行,因为实现Map接口的类只支持放入键值对格式的叶子节点,如果不是(比如说ArrayList)就不行,这是底层方法不支持。同时我们回到组合模式上重要的一点:一致性,Map下面添加都是put方法,需要传入两个值,List下都是add方法,只需要传入一个值,如果List中的可以通过调用putAll方法放入Map里,那么我该怎么给List赋key或value呢。(不是说不能有put(1,list)这样的操作,而是说在组合模式下操作时候需要有一致性,并且这里说的组合模式针对的是putAll方法,与其他方法无关)
四、总结
这里只是解析了HashMap中的putAll组合模式。在平常写代码的过程中要使用组合模式,就需要先定义一个抽象类或者接口。在这里就可以看作是抽象构件,主要实现一些基本的通用的功能。接着如果有需要就要定义一个中间构件,并且实现抽象构件,里面的方法可以根据特殊需要重写。而且这个类最重要的是要有存储结构在里面,用Map存储,或者用List存储等等都可以。接着就是我们真正的叶子节点,同样是继承或实现抽象构件,里面的方法可以根据特殊需要重写。当你看完这篇博客,你可以再去看定义和适用场景,说不定收获会更大。