本文github地址
你可能没意识到Java对函数式~程的重视程度,看看Java 8加入函数式编E扩充多功能就清楚了。Java 8之所以费q么大功夫引入函数式~程Q原因有二:
q一节我们学?em>streamQ也是Java函数式编E的主角。对于Java 7来说stream完全是个陌生东西Q?em>streamq不是某U数据结构,它只是数据源的一U视图。这里的数据源可以是一个数l,Java容器或I/O channel{。正因如此要得到一?em>stream通常不会手动创徏Q而是调用对应的工h法,比如Q?/p>
Collection.stream()
或?code>Collection.parallelStream()ҎArrays.stream(T[] array)
Ҏ常见?em>stream接口l承关系如图Q?/p>
图中4U?em>stream接口l承?code>BaseStreamQ其?code>IntStream, LongStream, DoubleStream对应三种基本cdQ?code>int, long, doubleQ注意不是包装类型)Q?code>Stream对应所有剩余类型的stream视图。ؓ不同数据cd讄不同stream接口Q可?.提高性能Q?.增加特定接口函数?/p>
你可能会奇怪ؓ什么不?code>IntStream{设计成Stream
的子接口Q毕竟这接口中的Ҏ名大部分是一L。答案是q些Ҏ的名字虽然相同,但是q回cd不同Q如果设计成父子接口关系Q这些方法将不能共存Q因为Java不允许只有返回类型不同的Ҏ重蝲?/p>
虽然大部分情况下stream是容器调?code>Collection.stream()Ҏ得到的,?em>stream?em>collections有以下不同:
?em>stream的操作分Zؓ两类Q?strong>中间操作(intermediate operations)和结束操?terminal operations)Q二者特ҎQ?/p>
如果你熟悉Apache Spark RDDQ对stream的这个特点应该不陌生?/p>
下表汇MStream
接口的部分常见方法:
操作cd | 接口Ҏ |
---|---|
中间操作 | concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered() |
l束操作 | allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray() |
区分中间操作和结束操作最单的ҎQ就是看Ҏ的返回|q回gؓstream的大都是中间操作Q否则是l束操作?/p>
stream跟函数接口关p非常紧密,没有函数接口stream无法工作。回一下:函数接口是指内部只有一个抽象方法的接口。通常函数接口出现的地斚w可以使用Lambda表达式,所以不必记忆函数接口的名字?/p>
我们?code>forEach()Ҏq不陌生Q在Collection
中我们已l见q。方法签名ؓvoid forEach(Consumer<? super E> action)
Q作用是对容器中的每个元素执?code>action指定的动作,也就是对元素q行遍历?/p>
׃forEach()
是结束方法,上述代码会立x行,输出所有字W串?/p>
函数原型?code>Stream<T> filter(Predicate<? super T> predicate)Q作用是q回一个只包含满predicate
条g元素?code>Stream?br />
上述代码输Zؓ长度{于3的字W串you
?/span>too
。注意,׃filter()
是个中间操作Q如果只调用filter()
不会有实际计,因此也不会输ZQ何信息?/span>
函数原型?code>Stream<T> distinct()Q作用是q回一个去除重复元素之后的Stream
?/p>
Stream<String> stream= Stream.of("I", "love", "you", "too", "too"); stream.distinct() .forEach(str -> System.out.println(str));
上述代码会输出去掉一?code>too之后的其余字W串?/p>
排序函数有两个,一个是用自焉序排序,一个是使用自定义比较器排序Q函数原型分别ؓStream<T> sorted()
?code>Stream<T> sorted(Comparator<? super T> comparator)?/p>
Stream<String> stream= Stream.of("I", "love", "you", "too"); stream.sorted((str1, str2) -> str1.length()-str2.length()) .forEach(str -> System.out.println(str));
上述代码输出按照长度升序排序后的字W串Q结果完全在预料之中?/p>
函数原型?code><R> Stream<R> map(Function<? super T,? extends R> mapper)Q作用是q回一个对当前所有元素执行执?code>mapper之后的结果组成的Stream
。直观的_是Ҏ个元素按照某U操作进行{换,转换前后Stream
中元素的个数不会改变Q但元素的类型取决于转换之后的类型?/p>
Stream<String> stream = Stream.of("I", "love", "you", "too"); stream.map(str -> str.toUpperCase()) .forEach(str -> System.out.println(str));
上述代码输出原字符串的大写形式?/p>
函数原型?code><R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)Q作用是Ҏ个元素执?code>mapper指定的操作,q用所?code>mapperq回?code>Stream中的元素l成一个新?code>Stream作ؓ最l返回结果。说h太拗口,通俗的讲flatMap()
的作用就相当于把?em>stream中的所有元素都"摊^"之后l成?code>StreamQ{换前后元素的个数和类型都可能会改变?/p>
Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
stream.flatMap(list -> list.stream())
.forEach(i -> System.out.println(i));
上述代码中,原来?code>stream中有两个元素Q分别是两个List<Integer>
Q执?code>flatMap()之后Q将每个List
?#8220;摊^”成了一个个的数字,所以会C生一个由5个数字组成的Stream
。所以最l将输出1~5q?个数字?/p>
截止到目前我们感觉良好,已介l?code>StreamAPI理解hq不费劲ѝ如果你此以ؓ函数式编E不q如此,恐怕是高兴地太早了。下一节对Stream
规约操作的介l将h你现在的认识?/p>
本文github地址Q欢q关注?/p>
关于C++标准模板?Standard Template Library, STL)的书c和资料有很多,关于Java集合框架(Java Collections Framework, JCF)的资料却很少Q甚臛_难找C本专门介l它的书c,q给Java学习者们带来不小的麻烦。我深深的不解其中的原因?strong>虽然JCF设计参考了STLQ但其定位不是Java版的STLQ而是要实C个精紧凑的容器框?/strong>Q对STL的介l自然不能替代对JCF的介l?/p>
本系列文章主要从数据l构和算?/strong>层面分析JCF中List, Set, Map, Stack, Queue{典型容器,l合生动图解和源代码Q帮助读者对Java集合框架建立清晰而深入的理解。本文ƈ不特意介lJava的语aҎ,但会在需要的时候做出简z的解释?/p> 具体内容安排如下Q?/p>Contents
Authors
Name Weibo Id GitHub Mail 李豪 @计算所的小鼠标 CarpenterLee hooleeucas@163.com
以上所有博文均?a >博主GitHub上有副本Qƈ且能保证最新版本。欢q各位博友关?/strong>?br />
Java WeakHashMap 到底Weak在哪里,它真的很弱吗Q?em>WeakHashMap 的适用场景是什么,使用旉要注意些什么?弱引用和强引用对Java GC有什么不同媄响?本文给出清晰而简z的介绍?/p>
在Java集合框架pd文章的最后,W者打介l一个特D的成员Q?em>WeakHashMapQ从名字可以看出它是某种 Map。它的特D之处在?WeakHashMap 里的entry
可能会被GC自动删除Q即使程序员没有调用remove()
或?code>clear()Ҏ?/p>
更直观的_当?WeakHashMap Ӟ即没有昄的添加或删除M元素Q也可能发生如下情况Q?/p>
- 调用两次
size()
Ҏq回不同的|- 两次调用
isEmpty()
ҎQ第一ơ返?code>falseQ第二次q回true
Q?/li>- 两次调用
containsKey()
ҎQ第一ơ返?code>trueQ第二次q回false
Q尽两ơ用的是同一?code>keyQ?/li>- 两次调用
get()
ҎQ第一ơ返回一?code>valueQ第二次q回null
Q尽两ơ用的是同一个对象?/li>
遇到q么奇葩的现象,你是不是觉得使用者一定会疯掉Q其实不ӞWeekHashMap 的这个特点特别适用于需要缓存的场景。在~存场景下,׃内存是有限的Q不能缓存所有对象;对象~存命中可以提高pȝ效率Q但~存MISS也不会造成错误Q因为可以通过计算重新得到?/p>
要明?WeekHashMap 的工作原理,q需要引入一个概念:弱引用(WeakReferenceQ?/strong>。我们都知道Java中内存是通过GC自动理的,GC会在E序q行q程中自动判断哪些对象是可以被回收的Qƈ在合适的时机q行内存释放。GC判断某个对象是否可被回收的依据是Q?strong>是否有有效的引用指向该对?/strong>。如果没有有效引用指向该对象Q基本意味着不存在访问该对象的方式)Q那么该对象是可回收的。这里的“有效引用”q不包括弱引?/strong>。也是_虽然弱引用可以用来访问对象,但进行垃圑֛收时弱引用ƈ不会被考虑在内Q仅有弱引用指向的对象仍然会被GC回收?/p> WeakHashMap 内部是通过弱引用来理 关于强引用,弱引用等概念以后再具体讲解,q里只需要知道Java中引用也是分U类的,q且不同U类的引用对GC的媄响不同就够了?/p> WeakHashMap的存储结构类gHashMapQ读者可自行参考前?/a>Q这里不再赘q?/p> 关于强弱引用的管理方式,博主会另开专题单独讲解?/p> 如果你看q前几篇关于 Map ?Set 的讲解,一定会问:既然?WeekHashMapQ是否有 WeekHashSet 呢?{案是没?( 。不qJava Collections工具cȝZ解决ҎQ?code>Collections.newSetFromMap(Map<E,Boolean> map)Ҏ可以Q?Map包装成一?em>Set。通过如下方式可以快速得C?Weak HashSetQ?br /> 不出你所料, x深入Java集合框架(Java Collections Framework Internals)pd已经全部讲解完毕Q希望这几篇短的博文能够帮助各位读者对Java容器框架建立基本的理解。通过q里可以q回本系列文章目?/a>?/p> 如果对各位有哪怕些微的帮助Q博d感到非常高兴Q如果博文中有Q何的U漏和谬误,Ƣ迎各位博友指正?/p>entry
的,弱引用的Ҏ对应到 WeakHashMap 上意味着什么呢Q?strong>一?code>key, value攑օ?WeakHashMap 里ƈ不能避免?code>keyDGC回收Q除非在 WeakHashMap 之外q有对该key
的强引用具体实现
Weak HashSet?
Set<Object> weakHashSet = Collections.newSetFromMap(
new WeakHashMap<Object, Boolean>());newSetFromMap()
Ҏ只是对传入的 Map做了单包装:
public static <E> Set<E> newSetFromMap(Map<E, Boolean> map) {
return new SetFromMap<>(map);
}
private static class SetFromMap<E> extends AbstractSet<E>
implements Set<E>, Serializable
{
private final Map<E, Boolean> m; // The backing map
private transient Set<E> s; // Its keySet
SetFromMap(Map<E, Boolean> map) {
if (!map.isEmpty())
throw new IllegalArgumentException("Map is non-empty");
m = map;
s = map.keySet();
}
public void clear() { m.clear(); }
public int size() { return m.size(); }
public boolean isEmpty() { return m.isEmpty(); }
public boolean contains(Object o) { return m.containsKey(o); }
public boolean remove(Object o) { return m.remove(o) != null; }
public boolean add(E e) { return m.put(e, Boolean.TRUE) == null; }
public Iterator<E> iterator() { return s.iterator(); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
public String toString() { return s.toString(); }
public int hashCode() { return s.hashCode(); }
public boolean equals(Object o) { return o == this || s.equals(o); }
public boolean containsAll(Collection<?> c) {return s.containsAll(c);}
public boolean removeAll(Collection<?> c) {return s.removeAll(c);}
public boolean retainAll(Collection<?> c) {return s.retainAll(c);}
// addAll is the only inherited implementation
}l语
Java LinkedHashMap?em>HashMap有什么区别和联系Qؓ什?em>LinkedHashMap会有着更快的P代速度Q?em>LinkedHashSet?em>LinkedHashMap有着怎样的内在联p?本文从数据结构和法层面Q结合生动图解ؓ读者一一解答?/p>
如果你已看过前面关于HashSet?em>HashMapQ以?em>TreeSet?em>TreeMap的讲解,一定能够想到本文将要讲解的LinkedHashSet?em>LinkedHashMap其实也是一回事?em>LinkedHashSet?em>LinkedHashMap在Java里也有着相同的实玎ͼ前者仅仅是对后者做了一层包装,也就是说LinkedHashSet里面有一?em>LinkedHashMapQ适配器模式)。因此本文将重点分析LinkedHashMap?/p>
LinkedHashMap实现?em>Map接口Q即允许攑օkey
?code>null的元素,也允许插?code>value?code>null的元素。从名字上可以看容器?em>linked list?em>HashMap的合体Q也是说它同时满HashMap?em>linked list的某些特性?strong>可将LinkedHashMap看作采用linked list增强?em>HashMap?/strong>
事实?em>LinkedHashMap?em>HashMap的直接子c,二者唯一的区别是LinkedHashMap?em>HashMap的基上,采用双向链表Qdoubly-linked listQ的形式所?code>entryq接hQ这hZ证元素的q代序跟插入顺序相?/strong>。上囄ZLinkedHashMap的结构图Q主体部分跟HashMap完全一P多了 除了可以保P代历序Q这U结构还有一个好处:q代LinkedHashMap时不需要像HashMap那样遍历整个 有两个参数可以媄?em>LinkedHashMap的性能Q初始容量(inital capacityQ和负蝲pLQload factorQ。初始容量指定了初始 对象放入到LinkedHashMap?em>LinkedHashSet中时Q有两个Ҏ需要特别关心: 通过如下方式可以得到一个跟?em>Mapq代序一LLinkedHashMapQ?br /> Z性能原因Q?em>LinkedHashMap是非同步的(not synchronizedQ,如果需要在多线E环境用,需要程序员手动同步Q或者通过如下方式?em>LinkedHashMap包装成(wrappedQ同步的Q?/p> 注意Q这里的插入有两重含?/strong>Q?/p> 上述代码中用C 上述代码只是单修改相?code>entry的引用而已?/p> 注意Q这里的删除也有两重含义Q?/p> 前面已经说过LinkedHashSet是对LinkedHashMap的简单包装,?em>LinkedHashSet的函数调用都会{换成合适的LinkedHashMapҎQ因?em>LinkedHashSet的实现非常简单,q里不再赘述?br />header
指向双向链表的头部(是一个哑元)Q?strong>该双向链表的q代序是entry
的插入顺?/strong>?/p>table
Q而只需要直接遍?code>header指向的双向链表即?/strong>Q也是?em>LinkedHashMap的P代时间就只跟entry
的个数相养I而跟table
的大无兟?/p>table
的大,负蝲pL用来指定自动扩容的界倹{当entry
的数量超q?code>capacity*load_factorӞ容器自动扩容ƈ重新哈希。对于插入元素较多的场景Q将初始定w讑֤可以减少重新哈希的次数?/p>hashCode()
?code>equals()?strong>hashCode()
Ҏ军_了对象会被放到哪?code>bucket里,当多个对象的哈希值冲H时Q?code>equals()Ҏ军_了这些对象是否是“同一个对?#8221;hashCode()
?code>equals()Ҏ?/p>
Map copy = new LinkedHashMap(m);
}Map m = Collections.synchronizedMap(new LinkedHashMap(...));
Ҏ剖析
get()
get(Object key)
ҎҎ指定?code>keyD回对应的value
。该Ҏ?code>HashMap.get()Ҏ的流E几乎完全一P读者可自行参考前?/a>Q这里不再赘q?/p>put()
put(K key, V value)
Ҏ是将指定?code>key, valueҎ加到map
里。该Ҏ首先会对map
做一ơ查找,看是否包含该元组Q如果已l包含则直接q回Q查找过E类gget()
ҎQ如果没有找刎ͼ则会通过addEntry(int hash, K key, V value, int bucketIndex)
Ҏ插入新的entry
?/p>entry
插入到冲H链表的头部?/li>addEntry()
代码如下Q?br />
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);// 自动扩容Qƈ重新哈希
hash = (null != key) ? hash(key) : 0;
bucketIndex = hash & (table.length-1);// hash%table.length
}
// 1.在冲H链表头部插入新的entry
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
// 2.在双向链表的N插入新的entry
e.addBefore(header);
size++;
}addBefore()
Ҏ新entry e
插入到双向链表头引用header
的前面,q样e
成为双向链表中的最后一个元素?code>addBefore()的代码如下:
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}remove()
remove(Object key)
的作用是删除key
值对应的entry
Q该Ҏ的具体逻辑是在removeEntryForKey(Object key)
里实现的?code>removeEntryForKey()Ҏ会首先找?code>key值对应的entry
Q然后删除该entry
Q修攚w表的相应引用Q。查找过E跟get()
ҎcM?/p>bucket
里删除,如果对应的冲H链表不I,需要修改冲H链表的相应引用?/li>removeEntryForKey()
对应的代码如下:
final Entry<K,V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);// hash&(table.length-1)
Entry<K,V> prev = table[i];// 得到冲突链表
Entry<K,V> e = prev;
while (e != null) {// 遍历冲突链表
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {// 扑ֈ要删除的entry
modCount++; size--;
// 1. e从对应bucket的冲H链表中删除
if (prev == e) table[i] = next;
else prev.next = next;
// 2. e从双向链表中删除
e.before.after = e.after;
e.after.before = e.before;
return e;
}
prev = e; e = next;
}
return e;
}LinkedHashSet
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
// LinkedHashSet里面有一个LinkedHashMap
public LinkedHashSet(int initialCapacity, float loadFactor) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
public boolean add(E e) {//单的Ҏ转换
return map.put(e, PRESENT)==null;
}
}
上一文?a >史上最清晰的红黑树讲解Q上Q?/a>对Java TreeMap的插入以及插入之后的调整q程l出了详q?strong>本文接着以Java TreeMapZQ从源码层面讲解U黑树的删除Q以及删除之后的调整q程。如果还没有看过上一文章,请在阅读本文之前大致览一下前文,以方便理解?/p>
对于一二叉查找树Q给定节点tQ其后Q树U比大于t的最的那个元素Q可以通过如下方式扑ֈQ?/p>
- t的右子树不空Q则t的后l是其右子树中最的那个元素?/li>
- t的右孩子为空Q则t的后l是其第一个向左走的祖先?/li>
后节点在红黑树的删除操作中会用到?/p>
TreeMap中寻找节点后l的代码如下Q?br />
remove(Object key)
的作用是删除key
值对应的entry
Q该Ҏ首先通过上文中提到的getEntry(Object key)
Ҏ扑ֈkey
值对应的entry
Q然后调?code>deleteEntry(Entry<K,V> entry)删除对应?code>entry。由于删除操作会改变U黑树的l构Q有可能破坏U黑树的U束条gQ因此有可能要进行调整?/p>
getEntry()
函数前面已经讲解q,q里重点?code>deleteEntry()上,该函数删除指定的entry
q在U黑树的U束被破坏时q行调用fixAfterDeletion(Entry<K,V> x)
q行调整?/p>
׃U黑树是一增强版的二叉查找树Q红黑树的删除操作跟普通二叉查找树的删除操作也非常相|唯一的区别是U黑树在节点删除之后可能需要进行调?/strong>。现在考虑一|通二叉查找树的删除过E,可以单分ZU情况:
- 删除点p的左叛_树都为空Q或者只有一子树非I?/li>
- 删除点p的左叛_树都非空?/li>
对于上述情况1Q处理v来比较简单,直接p删除Q左叛_树都为空ӞQ或者用非空子树替代pQ只有一子树非I时Q;对于情况2Q可以用p的后lsQ树中大于x的最的那个元素Q代替pQ然后用情?删除sQ此时s一定满x?Q可以画ȝQ?/p>
Z以上逻辑Q红黑树的节点删除函?code>deleteEntry()代码如下Q?br />
上述代码中占据大量代码行的,是用来修改父子节炚w引用关系的代码,光辑q不隄解。下面着重讲解删除后调整函数fixAfterDeletion()
。首先请思考一下,删除了哪些点才会D调整Q?strong>只有删除ҎBLACK的时候,才会触发调整函数Q因为删除RED节点不会破坏U黑树的MU束Q而删除BLACK节点会破坏规??/p>
跟上文中讲过?code>fixAfterInsertion()函数一Pq里也要分成若干U情c记住,无论有多情况,具体的调整操作只有两U:1.改变某些节点的颜Ԍ2.Ҏ些节点进行旋转?/p>
上述图解的M思想是:情?首先转换成情?Q或者{换成情况3和情?。当Ӟ该图解ƈ不意味着调整q程一定是从情?开始。通过后箋代码我们q会发现几个有趣的规则:a).如果是由情况1之后紧接着q入的情?Q那么情?之后一定会退出@环(因ؓx为红ԌQb).一旦进入情?和情?Q一定会退出@环(因ؓx为rootQ?/p>
删除后调整函?code>fixAfterDeletion()的具体代码如下,其中用到了上文中提到?code>rotateLeft()?code>rotateRight()函数。通过代码我们能够看到Q情?其实是落在情?内的。情?~情?跟前四种情况是对U的Q因此图解中q没有画出后四种情况Q读者可以参考代码自行理解?br />
前面已经说过TreeSet
是对TeeMap
的简单包装,?code>TreeSet的函数调用都会{换成合适的TeeMap
ҎQ因?code>TreeSet的实现非常简单。这里不再赘q?br />
本文以Java TreeMapZQ从源代码层面,l合详细的图解,剥茧抽丝地讲解红黑树QRed-Black treeQ的插入Q删除以及由此生的调整q程?/p>
Java TreeMap实现?em>SortedMap接口Q也是说会按照key
的大顺序对Map中的元素q行排序Q?code>key大小的评判可以通过其本w的自然序Qnatural orderingQ,也可以通过构造时传入的比较器QComparatorQ?/p>
TreeMap底层通过U黑树(Red-Black treeQ实?/strong>Q也意味着 Z性能原因Q?em>TreeMap是非同步的(not synchronizedQ,如果需要在多线E环境用,需要程序员手动同步Q或者通过如下方式?em>TreeMap包装成(wrappedQ同步的Q?/p> U黑树是一U近似^衡的二叉查找树,它能够确保Q何一个节点的左右子树的高度差不会过二者中较低那个的一?/strong>。具体来_U黑树是满如下条g的二叉查找树Qbinary search treeQ: 在树的结构发生改变时Q插入或者删除操作)Q往往会破坏上q条?或条?Q需要通过调整使得查找树重新满红黑树的条件?/p> 前文说到当查找树的结构发生改变时Q红黑树的条件可能被破坏Q需要通过调整使得查找树重新满红黑树的条件。调整可以分Zc:一cL颜色调整Q即改变某个节点的颜Ԍ另一cLl构调整Q集改变索树的结构关pR结构调整过E包含两个基本操作:左旋QRotate LeftQ,xQRotateRightQ?/strong>?/p> 左旋的过E是?code>x的右子树l?code>x逆时针旋转,使得 TreeMap中左旋代码如下: x的过E是?code>x的左子树l?code>x时针旋转,使得 TreeMap中右旋代码如下: 具体代码如下Q?br /> 上述代码的插入部分ƈ不难理解Q首先在U黑树上扑ֈ合适的位置Q然后创建新?code>entryq插入(当然Q新插入的节点一定是树的叶子Q。难Ҏ调整函数 调整函数 有关containsKey()
, get()
, put()
, remove()
都有着log(n)
的时间复杂度。其具体法实现参照了《算法导论》?/p>SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));
预备知识
左旋
x
的右子树成ؓx
的父Ԍ同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满?/p>
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K,V> r = p.right;
p.right = r.left;
if (r.left != null)
r.left.parent = p;
r.parent = p.parent;
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
}
}x
x
的左子树成ؓx
的父Ԍ同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满?/p>
private void rotateRight(Entry<K,V> p) {
if (p != null) {
Entry<K,V> l = p.left;
p.left = l.right;
if (l.right != null) l.right.parent = p;
l.parent = p.parent;
if (p.parent == null)
root = l;
else if (p.parent.right == p)
p.parent.right = l;
else p.parent.left = l;
l.right = p;
p.parent = l;
}
}Ҏ剖析
get()
get(Object key)
ҎҎ指定?code>keyD回对应的value
Q该Ҏ调用?code>getEntry(Object key)得到相应?code>entryQ然后返?code>entry.value。因?code>getEntry()是算法的核心。算法思想是根?code>key的自焉序(或者比较器序Q对二叉查找树进行查找,直到扑ֈ满k.compareTo(p.key) == 0
?code>entry?/p>
final Entry<K,V> getEntry(Object key) {
if (key == null)//不允许keygؓnull
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;//使用元素的自焉?/span>
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)//向左?/span>
p = p.left;
else if (cmp > 0)//向右?/span>
p = p.right;
else
return p;
}
return null;
}put()
put(K key, V value)
Ҏ是将指定?code>key, value
Ҏ加到map
里。该Ҏ首先会对map
做一ơ查找,看是否包含该元组Q如果已l包含则直接q回Q查找过E类ggetEntry()
ҎQ如果没有找到则会在U黑树中插入新的entry
Q如果插入之后破坏了U黑树的U束Q还需要进行调_旋{Q改变某些节点的颜色Q?br />
int cmp;
Entry<K,V> parent;
if (key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;//使用元素的自焉?/span>
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0) t = t.left;//向左?/span>
else if (cmp > 0) t = t.right;//向右?/span>
else return t.setValue(value);
} while (t != null);
Entry<K,V> e = new Entry<>(key, value, parent);//创徏q插入新的entry
if (cmp < 0) parent.left = e;
else parent.right = e;
fixAfterInsertion(e);//调整
size++;
return null;
}fixAfterInsertion()
Q前面已l说q,调整往往需?.改变某些节点的颜Ԍ2.Ҏ些节点进行旋转?/p>fixAfterInsertion()
的具体代码如下,其中用到了上文中提到?code>rotateLeft()?code>rotateRight()函数。通过代码我们能够看到Q情?其实是落在情?内的。情?~情?跟前三种情况是对U的Q因此图解中q没有画出后三种情况Q读者可以参考代码自行理解?br />
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {//如果y为nullQ则视ؓBLACK
setColor(parentOf(x), BLACK); // 情况1
setColor(y, BLACK); // 情况1
setColor(parentOf(parentOf(x)), RED); // 情况1
x = parentOf(parentOf(x)); // 情况1
} else {
if (x == rightOf(parentOf(x))) {
x = parentOf(x); // 情况2
rotateLeft(x); // 情况2
}
setColor(parentOf(x), BLACK); // 情况3
setColor(parentOf(parentOf(x)), RED); // 情况3
rotateRight(parentOf(parentOf(x))); // 情况3
}
} else {
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK); // 情况4
setColor(y, BLACK); // 情况4
setColor(parentOf(parentOf(x)), RED); // 情况4
x = parentOf(parentOf(x)); // 情况4
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x); // 情况5
rotateRight(x); // 情况5
}
setColor(parentOf(x), BLACK); // 情况6
setColor(parentOf(parentOf(x)), RED); // 情况6
rotateLeft(parentOf(parentOf(x))); // 情况6
}
}
}
root.color = BLACK;
}remove()
remove(Object key)
的作用是删除key
值对应的entry
Q该Ҏ首先通过上文中提到的getEntry(Object key)
Ҏ扑ֈkey
值对应的entry
Q然后调?code>deleteEntry(Entry<K,V> entry)删除对应?code>entry。由于删除操作会改变U黑树的l构Q有可能破坏U黑树的U束Q因此有可能要进行调整?/p>remove()
的具体讲解将攑ֈ下一博文当中,敬请期待Q?/strong>
每个博客园的园友或许都会有这U经历:自己辛辛苦苦Q认认真真的写了博客,然后满心Ƣ喜的发C博客园首,当你以ؓ大功告成坐等点击量暴表的时候,却发现自q博文Ҏ无h问|。那是何等的痛?(
不要再自我怀疑,不要再自怨自艾,博客不火Q不一定是博文内容不够严}深入Q也不一定是你能力不I而可能仅仅是因ؓ你选择了错误的发表时机?/p>
本文Z博客园近3个月4000首博文,q用大数据分析,机器学习Q文本挖掘等先进技术,深入而细致的剖析军_博客热度的若q因素,让您从此也能写出_湛的技术博客,成ؓ博客园的技术达人(做梦到此l束...Q?/strong>
我们对一天中不同旉D发表的博客q行l计Q然后计每个小时内的博客发表量Q以及当前这个小时每博客的q_热度。这里的热度是用来衡量一博文受Ƣ迎E度的综合指标,计算公式为:
hot=(recommend*10+comment*5+view)
为避免离点Q当hotDq?600时则?600处理。上图中标注的热度(U色U)为对文章热度求^均之后,然后做归一?code>(avg_hot/800*100%)之后的结果?/p>
从上囑֏以明昄出,一天之内有三个旉D大家比较爱发博?/strong>Q分别是10:00左右Q?6:00左右?2:00左右Q这分别对应的是上午上班旉Q下午上班时_和晚上加班时间?strong>一天内也有三个旉D大家不怎么爱发博客Q分别是1:00~7:00Q?2:00左右Q?9:00左右Q分别对应大家的睡觉旉Q午饭时间和下班旉?/p>
什么时候发的博客更Ҏ火呢Q抛开凌晨那段旉不提Q因为博客量太少Q,上图可以看出Q早?:00左右发的博客热度最高,中午12:00左右和晚?2:00左右也是个热度小高峰?/p>
Ҏ上图中的蓝线和红U,我们发现博客发表高峰和访问高峎ͼ热度评估主要Z讉K量,所以热度表CZ讉K量的势Qƈ不L成比例。具体表现如下:
- 早上8:00是一天中讉K量最高的时候,但博客发表ƈ不是很多Q上班\上大家刷刷博客?Q?/li>
- 上午10:00左右是博客发表的高峰Q但讉K量却呈下降趋势(忙着写自q博客而忘记看别h的博客)
- 中午12:00左右讉K量很高,但博客发表量却出奇的低(吃饭的时候不写博客,倒是可以手机刷刷博客Q?/li>
我们对一周中不同天发表的博客q行l计Q然后计每天的博客发表量,以及当天每篇博客的^均热度?/p>
通过上图可以看出Q?/p>
- 星期一和星期二是博客发表的热潮Q上班前两天不但工作U极Q写博客也很U极Q?/li>
- 之后一直下降,到周六达到最低谷Q终于盼来周末,谁还写博客!Q?/li>
- 博客热度跟发表量基本dQ可见工作日大家不但工作热情高,写博客和d客的热情都不低?/li>
- C周末Q写博客的h,看博客的人更!
- 周四博客阅读量出C回升Q你可以帮忙x是ؓ什么?/li>
上图意味着Q?strong>周末q是老老实实的出去玩吧Q即使写了博客也不会有h看的。特别是周六Q千万不要在周六发表技术博客,切记切记Q?/p>
l过以上分析Q我们得出结论:Z避免吃力不讨好的情况Q发表博客一定要认准时机?/p>
以上四项基本原则Q一定要牢记于心Q切C要轻易违背。否则没有点击量Q你的博客还不如写道日记本里?/p>
Java里有一个叫?em>Stack的类Q却没有叫做Queue的类Q它是个接口名字Q。当需要用栈ӞJava已不推荐使用StackQ而是推荐使用更高效的ArrayDequeQ既?em>Queue只是一个接口,当需要用队列时也就首?em>ArrayDeque了(ơ选是LinkedListQ?/p>
要讲栈和队列Q首先要?em>Deque接口?em>Deque的含义是“double ended queue”Q即双端队列Q它既可以当作栈使用Q也可以当作队列使用。下表列ZDeque?em>Queue相对应的接口Q?/p>
Queue Method | Equivalent Deque Method | 说明 |
---|---|---|
add(e) |
addLast(e) |
向队插入元素,p|则抛出异?/td> |
offer(e) |
offerLast(e) |
向队插入元素,p|则返?code>false |
remove() |
removeFirst() |
获取q删除队首元素,p|则抛出异?/td> |
poll() |
pollFirst() |
获取q删除队首元素,p|则返?code>null |
element() |
getFirst() |
获取但不删除队首元素Q失败则抛出异常 |
peek() |
peekFirst() |
获取但不删除队首元素Q失败则q回null |
下表列出?em>Deque?em>Stack对应的接口:
Stack Method | Equivalent Deque Method | 说明 |
---|---|---|
push(e) |
addFirst(e) |
向栈插入元素,p|则抛出异?/td> |
?/td> | offerFirst(e) |
向栈插入元素,p|则返?code>false |
pop() |
removeFirst() |
获取q删除栈元素,p|则抛出异?/td> |
?/td> | pollFirst() |
获取q删除栈元素,p|则返?code>null |
peek() |
peekFirst() |
获取但不删除栈顶元素Q失败则抛出异常 |
?/td> | peekFirst() |
获取但不删除栈顶元素Q失败则q回null |
上面两个表共定义?em>Deque?2个接口。添加,删除Q取值都有两套接口,它们功能相同Q区别是对失败情늚处理不同?strong>一套接口遇到失败就会抛出异常,另一套遇到失败会q回Ҏ|false
?code>nullQ?/strong>。除非某U实现对定w有限Ӟ大多数情况下Q添加操作是不会p|的?strong>虽然Deque的接口有12个之多,但无非就是对容器的两端进行操作,或添加,或删除,或查?/strong>。明白了q一点讲解v来就会非常简单?/p>
ArrayDeque?em>LinkedList?em>Deque的两个通用实现Q由于官Ҏ推荐使用AarryDeque用作栈和队列Q加之上一已l讲解过LinkedListQ本文将着重讲?em>ArrayDeque的具体实现?/p>
从名字可以看?em>ArrayDeque底层通过数组实现Qؓ了满_以同时在数组两端插入或删除元素的需求,该数l还必须是@环的Q即循环数组Qcircular arrayQ?/strong>Q也是说数l的M一炚w可能被看作vҎ者终炏V?em>ArrayDeque是非U程安全的(not thread-safeQ,当多个线E同时用的时候,需要程序员手动同步Q另外,该容器不允许攑օ 上图中我们看刎ͼ 实际需要考虑Q?.I间是否够用Q以?.下标是否界的问题。上图中Q如?code>head?code>0之后接着调用 下标界的处理解册v来非常简单,null
元素?/p>
head
指向首端W一个有效元素,tail
指向W一个可以插入元素的IZ。因为是循环数组Q所?code>head不一定ȝ?Q?code>tail也不一定L?code>head大?/p>
Ҏ剖析
addFirst()
addFirst(E e)
的作用是?em>Deque的首端插入元素,也就是在head
的前面插入元素,在空间够且下标没有界的情况下Q只需要将elements[--head] = e
卛_?/p>
addFirst()
Q虽然空余空间还够用Q但head
?code>-1Q下标越界了。下列代码很好的解决了这两个问题?br />
public void addFirst(E e) {
if (e == null)//不允许放入null
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;//2.下标是否界
if (head == tail)//1.I间是否够用
doubleCapacity();//扩容
}上述代码我们看到Q?/span>I间问题是在插入之后解决?/strong>Q因?/span>
tail
L指向下一个可插入的空位,也就意味着elements
数组臛_有一个空位,所以插入元素的时候不用考虑I间问题?/span>head = (head - 1) & (elements.length - 1)
可以了Q?strong>q段代码相当于取余,同时解决?code>head值的情况elements - 1
是二进制低位全1
Q跟head - 1
怸之后pvC取模的作用,如果head - 1
敎ͼ其实只可能是-1Q,则相当于对其取相对于elements.length
的补码?/p>
下面再说说扩容函?code>doubleCapacity()Q其逻辑是申请一个更大的数组Q原数组的两倍)Q然后将原数l复制过厅R过E如下图所C:
图中我们看到Q复制分两次q行Q第一ơ复?code>head双的元素,W二ơ复?code>head左边的元素?br />
addLast(E e)
的作用是?em>Deque的尾端插入元素,也就是在tail
的位|插入元素,׃tail
L指向下一个可以插入的IZQ因此只需?code>elements[tail] = e;卛_。插入完成后再检查空_如果I间已经用光Q则调用doubleCapacity()
q行扩容?/p>
下标界处理方式addFirt()
中已l讲q,不再赘述?/span>
pollFirst()
的作用是删除q返?em>Deque首端元素Q也xhead
位置处的元素。如果容器不I,只需要直接返?code>elements[head]卛_Q当然还需要处理下标的问题。由?code>ArrayDeque中不允许攑օnull
Q当elements[head] == null
Ӟ意味着容器为空?br />
pollLast()
的作用是删除q返?em>Deque元素Q也xtail
位置前面的那个元素?br />
peekFirst()
的作用是q回但不删除Deque首端元素Q也xhead
位置处的元素Q直接返?code>elements[head]卛_?br />
peekLast()
的作用是q回但不删除Deque元素Q也xtail
位置前面的那个元素?br />
LinkedList同时实现?em>List接口?em>Deque接口Q也是说它既可以看作一个顺序容器,又可以看作一个队列(QueueQ,同时又可以看作一个栈Q?em>StackQ。这L来,LinkedList直就是个全能冠军。当你需要用栈或者队列时Q可以考虑使用LinkedListQ一斚w是因为Java官方已经声明不徏议?em>Stackc,更遗憄是,Java里根本没有一个叫?em>Queue的类Q它是个接口名字Q。关于栈或队列,现在的首选是ArrayDequeQ它有着?em>LinkedListQ当作栈或队列用时Q有着更好的性能?/p>
LinkedList底层通过双向链表实现Q本节将着重讲解插入和删除元素时双向链表的l护q程Q也x之间解跟List接口相关的函敎ͼ而将Queue?em>Stack以及Deque相关的知识放在下一节讲。双向链表的每个节点用内部类Node表示?em>LinkedList通过first
?code>last引用分别指向链表的第一个和最后一个元素。注意这里没有所谓的哑元Q当链表为空的时?code>first?code>last都指?code>null?br />
LinkedList的实现方式决定了所有跟下标相关的操作都是线性时_而在首段或者末ֈ除元素只需要常数时间。ؓq求效率LinkedList没有实现同步QsynchronizedQ,如果需要多个线Eƈ发访问,可以先采?code>Collections.synchronizedList()Ҏ对其q行包装?/p>
add()Ҏ有两个版本,一个是add(E e)
Q该Ҏ?em>LinkedList的末插入元素,因ؓ?code>last指向链表末尾Q在末尾插入元素的花Ҏ常数旉。只需要简单修改几个相兛_用即可;另一个是add(int index, E element)
Q该Ҏ是在指定下表处插入元素,需要先通过U性查找找到具体位|,然后修改相关引用完成插入操作?/p>
l合上图Q可以看?code>add(E e)的逻辑非常单?br />
的逻辑E显复杂Q可以分成两部,1.先根据index扑ֈ要插入的位置Q?.修改引用Q完成插入操作?br />
add(int index, E element)
上面代码中的node(int index)
函数有一点小的trickQ因为链表双向的Q可以从开始往后找Q也可以从结־前找Q具体朝那个方向扑֏决于条gindex < (size >> 1)
Q也xindex是靠q前端还是后端?/p>
remove()
Ҏ也有两个版本Q一个是删除跟指定元素相{的W一个元?code>remove(Object o)Q另一个是删除指定下标处的元素remove(int index)
?/p>
两个删除操作都要1.先找到要删除元素的引用,2.修改相关引用Q完成删除操作。在L被删元素引用的时?code>remove(Object o)调用的是元素?code>equalsҎQ?code>remove(int index)使用的是下标计数Q两U方式都是线性时间复杂度。在步骤2中,两个revome()
Ҏ都是通过unlink(Node<E> x)
Ҏ完成的。这里需要考虑删除元素是第一个或者最后一个时的边界情c?br />
get(int index)
得到指定下标处元素的引用Q通过调用上文中提到的node(int index)
Ҏ实现?br />
set(int index, E element)
Ҏ指定下标处的元素修Ҏ指定|也是先通过node(int index)
扑ֈ对应下表元素的引用,然后修改Node
?code>item的倹{?br />