ConcurrentHashMap源码解析-Java7
目录
二.源码分析-类定义
2.2 Segment内部类
2.3 HashEntry内部类
三.常用接口源码分析
3.2 map.put操作
3.3 创建新segment
3.4 segment.put操作
3.5 segment.rehash扩容
3.6 map.get操作
3.7 map.remove操作
原文地址:https://www.cnblogs.com/-beyond/p/13157083.html
一.ConcurrentHashMap的模型图
之前看了Java8中的HashMap,然后想接着看Java8的ConcurrentHashMap,但是打开Java8的ConcurrentHashMap,瞬间就怂了,将近7k行代码,而反观一下Java7的Concurrent,也就在1500多行,那就先选择少的看吧。毕竟Java7的ConcurrentHashMap更加简单。下面所有的介绍都是针对Java7而言!!!!!
下面是ConcurrentHashMap的结构图,ConcurrentHashMap有一个segments数组,每个segment中又有一个table数组,该数组的每个元素时HashEntry类型。
可以简单的理解为ConcurrentHashMap是多个HashMap组成,每一个HashMap是一个segment,就如传说中一样,ConcurrentHashMap使用分段锁的方式,这个“段”就是segment。
ConcurrentHashMap之所以能够保证并发安全,是因为支持对不同segment的并发修改操作,比如两个线程同时修改ConcurrentHashMap,一个线程修改第一个segment的数据,另一个线程修改第二个segment的数据,两个线程可以并发修改,不会出现并发问题;但是多个线程同一个segment进行并发修改,则需要先获取该segment的锁后再修改,修改完后释放锁,然后其他要修改的线程再进行修改。
那么,ConcurrentHashMap可以支持多少并发量呢?这个也就是问,ConcurrentHashMap最多能支持多少线程并发修改,其实也就是segment的数量,只要修改segment的这些线程不是修改同一个segment,那么这些线程就可以并行执行,这也就是ConcurrentHashMap的并发量(segment的数量)。
注意,ConcurrentHashMap创建完成后,也就是segment的数量、并发级别已经确定,则segment的数量和并发级别都不能再改变了,即使发生扩容,也是segment中的table进行扩容,segment的数量保持不变。
二.源码分析-类定义
2.1 极简ConcurrentHashMap定义
下面是省略了大部分代码的ConcurrentHashMap定义:
package java.util.concurrent; import java.util.AbstractMap;
import java.util.concurrent.locks.ReentrantLock; public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable { final Segment<K, V>[] segments; /**
* segment分段的定义
*/
static final class Segment<K, V> extends ReentrantLock implements Serializable { transient volatile HashEntry<K, V>[] table;
} /**
* 存放的元素节点
*/
static final class HashEntry<K, V> { }
}
2.2 Segment内部类
ConcurrentHashMap内部有一个segments属性,是Segment类型的数组,Segment类和HashMap很相似(Java7的HashMap),维持一个数组,每个数组是一个HashEntry,当发生hash冲突后,则使用拉链法(头插法)来解决冲突。
而Segment数组的定义如下,省略了方法:
static final class Segment<K, V> extends ReentrantLock implements Serializable {
static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
private static final long serialVersionUID = 2249069246763182397L; // segment的负载因子(segments数组中的所有segment负载因子都相同)
final float loadFactor; // segment中保存元素的数组
transient volatile HashEntry<K, V>[] table; // 该segment中的元素个数
transient int count; // modify count,该segment被修改的次数
transient int modCount; // segment的扩容阈值
transient int threshold;
}
注意每个Segment都有负载因子、以及扩容阈值,但是后面可以看到,其实segments数组中的每一个segment,负载因子和扩容阈值都相同,因为创建的时候,都是使用0号segment的负载因子和扩容阈值。
2.3 HashEntry内部类
Segment类中有一个table数组,table数组的每个元素都是HashEntry类型,和HashMap的Entry类似,源码如下:(省略了方法)
/**
* map中每个元素的类型
*/
static final class HashEntry<K, V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K, V> next;
}
2.4 ConcurrentHashMap的一些常量
ConcurrentHashMap中有很多常量,
// 默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16; // 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认的并发级别(同时支持多少线程并发修改)
// 因为JDK7中ConcurrentHashMap中是用分段锁实现并发,不同分段的数据可以进行并发操作,同一个段的数据不能同时修改
static final int DEFAULT_CONCURRENCY_LEVEL = 16; // 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 每一个分段的数组容量初始值
static final int MIN_SEGMENT_TABLE_CAPACITY = 2; // 最多能有多少个segment
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative // 尝试对整个map进行操作(比如说统计map的元素数量),可能需要锁定全部segment;
// 这个常量表示锁定所有segment前,尝试的次数
static final int RETRIES_BEFORE_LOCK = 2;
三.常用接口源码分析
3.1 ConcurrentHashMap构造方法
ConcurrentHashMap有多个构造方法,但是内部其实都是调用同一个进行创建:
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
} public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
} public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
} /**
* 统一调用的构造方法
*
* @param initialCapacity 初始容量
* @param loadFactor 负载因子
* @param concurrencyLevel 并发级别
*/
@SuppressWarnings("unchecked")
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
// 校验参数合法性
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) {
throw new IllegalArgumentException();
} // 对并发级别进行调整,不允许超过segment的数量(超过segment其实是没有意义的)
if (concurrencyLevel > MAX_SEGMENTS) {
concurrencyLevel = MAX_SEGMENTS;
} // 根据concurrencyLevel确定sshift和ssize的值
int ssize = 1; // ssize是表示segment的数量,ssize是不小于concurrencyLevel的数,并且是2的n次方
int sshift = 0;// sshift是ssize转换为2进制后的位数,比如ssize为16(1000),则sshift为4
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// 比如concurrencyLevel默认为16,走完循环,sshift为4,ssize为16
// 如果concurrentLevel为8,则sshift为3,ssize为8 // segment的shift偏移量
this.segmentShift = 32 - sshift;
// segment掩码
this.segmentMask = ssize - 1; // 对传入的初始容量进行调整(不允许大于最大容量)
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
} // 假设传入的容量为128,并发级别为16,则initialCapacity为128,ssize为16
int c = initialCapacity / ssize;
// c可以理解为根据传入的初始容量,计算出每个segment中的数组容量
if (c * ssize < initialCapacity) {
++c;
} // cap表示的是每个segment中的数组容量
int cap = MIN_SEGMENT_TABLE_CAPACITY;
// 如果默认的每个segment中的数组长度小于上面计算出来的每个segment的数组长度,则将容量翻倍
while (cap < c) {
cap <<= 1;
} // 创建一个segment,作为segments数组的第一个segment
Segment<K, V> s0 = new Segment<K, V>(loadFactor, (int) (cap * loadFactor), new HashEntry[cap]); // 创建segments数组
Segment<K, V>[] ss = (Segment<K, V>[]) new Segment[ssize]; // 将s0赋值给segments数组的第一个元素(偏移量为0)
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] // 复制segment数组
this.segments = ss;
} /**
* 传入map,将map中的元素加入到ConcurrentHashMap中
* 注意使用默认的负载因子(0.75)和默认的并发级别(16)
* 初始容量取map容量和默认容量的较大值
*/
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY),
DEFAULT_LOAD_FACTOR,
DEFAULT_CONCURRENCY_LEVEL);
putAll(m);
}
3.2 map.put操作
map.put,map就是指ConcurrentHashMap,其实就是确定HashEntry应该放入哪个segment中的哪个位置,所以可分为3步:
1.首先需要确定放入哪个segment;
2.确定segment后,再确定HashEntry应该放入segment的哪个位置;
3.进行覆盖覆盖或者插入。
/**
* put操作,注意key或者value为null时,会抛出NPE
*/
@SuppressWarnings("unchecked")
public V put(K key, V value) {
Segment<K, V> s;
if (value == null) {
throw new NullPointerException();
} // 计算key的hash
int hash = hash(key); // hash值右移shift位后 与 mask掩码进行取与,确定数据应该放到哪个segments数组的哪一个segment中
int j = (hash >>> segmentShift) & segmentMask; // 判断计算出的segment数组位置上的segment是否为null,如果为null,则进行创建segment
if ((s = (Segment<K, V>) UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null) {
s = ensureSegment(j);
} // 创建segment后,将数据put到segment中,调用的segment.put()
return s.put(key, hash, value, false);
}
3.3 创建新segment
上面put的时候,如果发现segment为null,则会进行调用ensureSegment进行创建segment,代码如下:
/**
* 扩容(创建)segment
*
* @param k 需要扩容或者需要创建的segment位置
* @return 返回扩容后的segment
*/
@SuppressWarnings("unchecked")
private Segment<K, V> ensureSegment(int k) {
final Segment<K, V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // 传入的index,对应的偏移量
Segment<K, V> seg; // 判断是否需要扩容或者创建segment,如果获取到segment不为null,则返回segment
if ((seg = (Segment<K, V>) UNSAFE.getObjectVolatile(ss, u)) == null) {
Segment<K, V> proto = ss[0]; // “原型设计模式”,使用segments数组的0号位置segment
int cap = proto.table.length;// 0号segment的table长度
float lf = proto.loadFactor; // 0号segment的负载因子
int threshold = (int) (cap * lf); // 0号segment的扩容阈值 // 创建一个HashEntry的数组,数组容量和0号segment相同
HashEntry<K, V>[] tab = (HashEntry<K, V>[]) new HashEntry[cap]; // 再次检测,指定的segment是不是为null,如果为null才进行下一步操作
if ((seg = (Segment<K, V>) UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck
// 创建segment
Segment<K, V> s = new Segment<K, V>(lf, threshold, tab); // 使用CAS将新创建的segment填入指定的位置
while ((seg = (Segment<K, V>) UNSAFE.getObjectVolatile(ss, u)) == null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) {
break;
}
}
}
} // 返回新增的segment
return seg;
}
上面需要注意的是:
1.创建segment中的table数组时,是使用0号segment的table属性(长度、负载因子、阈值);
2.创建segment前会进行再check,check发现segment的确为null时,再进行创建segment;
3.利用CAS来将创建的segment填入segments数组中;
3.4 segment.put操作
当确定HashEntry应该放到哪个segment后,就开始调用segment的put方法,如下:
/**
* 在确定应该存放到那个segment后,就segment.put()将k-v存入segment中
*
* @param key put的key
* @param hash hash(key)的值
* @param value put的value
* @param onlyIfAbsent true:key对应的Entry不进行覆盖,false:key对应的entry存在与否,都进行put覆盖
* @return
*/
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 先获取锁(ReentrantLock,内部使用非公平锁)
HashEntry<K, V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K, V>[] tab = table; // 根据hash值计算出在segment的table中的位置
int index = (tab.length - 1) & hash; // 定位到segment的table的index位置(第一个节点)
HashEntry<K, V> first = entryAt(tab, index); // 从第一个节点开始遍历
for (HashEntry<K, V> e = first; ; ) {
// 节点不为空,则判断是否key是否相同(相同HashEntry)
if (e != null) {
K k;
// 比较是否key是否相等(判断put的key是否已经存在)
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
// 如果key已经存在,则进行覆盖,如果onlyIsAbsent为false(不关心key对应的Entry是否存在)
oldValue = e.value;
if (!onlyIfAbsent) {
// 覆盖旧值,修改计数加1
e.value = value;
++modCount;
}
break;
}
e = e.next;
} else {
// 头插法,将put的k-v创建新HashEntry,放到first的前面
if (node != null) {
node.setNext(first);
} else {
node = new HashEntry<K, V>(hash, key, value, first);
} // segment中table元素数量加1
int c = count + 1; // 加1后的size大于扩容阈值,且数组的长度小于最大容量,则进行rehash
if (c > threshold && tab.length < MAXIMUM_CAPACITY) {
// 扩容,并进行rehash
rehash(node);
} else {
// 将节点放入数组中的指定位置
setEntryAt(tab, index, node);
} // 修改次数加一,修改segment的table元素个数
++modCount;
count = c; // 旧值为null
oldValue = null;
break;
}
}
} finally {
// 释放锁
unlock();
}
return oldValue;
}
梳理一下,大致在做下面几件事:
1.先获取锁(ReetrantLock,内部使用非公平锁NonFairSync);
2.获取到锁后根据hash计算出在table的位置;
3.遍历table的HashEntry的链表,如果有相同key的,则进行覆盖,如果没有,在进行头插法;
4.插入链表后,确定是否需要扩容,需要则执行rehash;
5.修改计数(count、modCount),并且释放锁。
3.5 segment.rehash扩容
segment扩容时,会将该segment的容量扩容为之前的2倍,并且将各位置的链表节点元素进行rehash。
/**
* 将segment的table容量扩容一倍(newCap=oldCap*2),注意只会扩容该node所在的segment
*
* @param node segment[i]->链表的头结点
*/
@SuppressWarnings("unchecked")
private void rehash(HashEntry<K, V> node) {
HashEntry<K, V>[] oldTable = table;
int oldCapacity = oldTable.length; // 新容量为旧容量的2倍
int newCapacity = oldCapacity << 1; // 设置新的扩容阈值
threshold = (int) (newCapacity * loadFactor); // 申请新数组,数组长度为新容量值
HashEntry<K, V>[] newTable = (HashEntry<K, V>[]) new HashEntry[newCapacity]; // 循环遍历segment的数组(旧数组)
int sizeMask = newCapacity - 1; // 新的掩码
for (int i = 0; i < oldCapacity; i++) {
// 获取第i个位置的HashEntry节点,如果该节点为null,则该位置为空,不作处理
HashEntry<K, V> e = oldTable[i];
if (e != null) {
HashEntry<K, V> next = e.next; // 计算新位置
int idx = e.hash & sizeMask; // next为null,表示该位置只有一个节点,直接将节点复制到新位置
if (next == null) { // Single node on list
newTable[idx] = e;
} else { // Reuse consecutive sequence at same slot
HashEntry<K, V> lastRun = e;
int lastIdx = idx;
for (HashEntry<K, V> last = next; last != null; last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
newTable[lastIdx] = lastRun;
// 从头结点开始,开始将节点拷贝到新数组中
for (HashEntry<K, V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K, V> n = newTable[k];
newTable[k] = new HashEntry<K, V>(h, p.key, v, n);
}
}
}
} // 将头结点添加到segment的table中
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
为啥扩容的时候没有加锁呀?
其实已经加锁了,只不过不是在rehash中加锁!!!因为rehash是在map.put中调用,put的时候已经加锁了,所以rehash中不用加锁。
3.6 map.get操作
get操作,先定位到segment,然后定位到segment的具体位置,进行获取
/**
* 从ConcurrentHashMap中获取key对应的value,不需要加锁
*/
public V get(Object key) {
Segment<K, V> s;
HashEntry<K, V>[] tab; // 计算key的hash
int h = hash(key); // 计算key处于哪一个segment中(index)
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; // 获取数组中该位置的segment,如果该segment的table不为空,那么就继续在segment中查找,否则返回null,因为未找到
if ((s = (Segment<K, V>) UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { // tab指向segment的table数组,通过hash计算定位到table数组的位置(然后开始遍历链表)
HashEntry<K, V> e;
for (e = (HashEntry<K, V>) UNSAFE.getObjectVolatile(tab, ((long) (((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
// 判断key和hash是否匹配,匹配则证明找到要查找的数据,将数据返回
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
} return null;
}
3.7 map.remove操作
删除节点,和get的流程差不多,先定位到segment,然后确定segment的哪个位置(哪条链表),遍历链表,找到后进行删除。
/**
* 删除map中key对应的元素
*/
public V remove(Object key) {
// 计算key的hash
int hash = hash(key); // 根据hash确定segment
Segment<K, V> s = segmentForHash(hash); // 调用segment.remove进行删除
return s == null ? null : s.remove(key, hash, null);
} /**
* 删除segment中key对应的hashEntry
* 如果传入的value不为空,则会在value匹配的时候进行删除,否则不操作
*/
final V segmeng.remove(Object key, int hash, Object value) {
// 获取锁失败,则不断自旋尝试获取锁
if (!tryLock()) {
scanAndLock(key, hash);
} V oldValue = null;
try {
HashEntry<K, V>[] tab = table;
// 定位到segment中table的哪个位置
int index = (tab.length - 1) & hash;
HashEntry<K, V> e = entryAt(tab, index);
HashEntry<K, V> pred = null; // 遍历链表
while (e != null) {
K k;
HashEntry<K, V> next = e.next;
// 如果key和hash都匹配
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
V v = e.value;
// 如果没有传入value,则直接删除该节点
// 如果传入了value,比如调用的map.remove(key,value),则要value匹配才会删除,否则不操作
if (value == null || value == v || value.equals(v)) {
if (pred == null) { // 头结点就是要找删除的元素,next为null,则将null赋值数组的该位置
setEntryAt(tab, index, next);
} else { //
pred.setNext(next);
} // 修改次数加一,map数量减一
++modCount;
--count;
oldValue = v;
}
break;
} // 不匹配时,pred保存当前一次检测的节点,e指向下一个节点
pred = e;
e = next;
}
} finally {
unlock();// 释放锁
}
return oldValue;
}
ConcurrentHashMap源码解析-Java7的更多相关文章
- Java之ConcurrentHashMap源码解析
ConcurrentHashMap源码解析 目录 ConcurrentHashMap源码解析 jdk8之前的实现原理 jdk8的实现原理 变量解释 初始化 初始化table put操作 hash算法 ...
- Java并发包源码学习系列:JDK1.8的ConcurrentHashMap源码解析
目录 为什么要使用ConcurrentHashMap? ConcurrentHashMap的结构特点 Java8之前 Java8之后 基本常量 重要成员变量 构造方法 tableSizeFor put ...
- ConcurrentHashMap源码解析(1)
此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 注:在看这篇文章之前,如果对HashMap的层不清楚的话,建议先去看看HashMap源码解析. http:/ ...
- 第二章 ConcurrentHashMap源码解析
注:在看这篇文章之前,如果对HashMap的层不清楚的话,建议先去看看HashMap源码解析. http://www.cnblogs.com/java-zhao/p/5106189.html 1.对于 ...
- ConcurrentHashMap源码解析,多线程扩容
前面一篇已经介绍过了 HashMap 的源码: HashMap源码解析.jdk7和8之后的区别.相关问题分析 HashMap并不是线程安全的,他就一个普通的容器,没有做相关的同步处理,因此线程不安全主 ...
- 数据结构算法 - ConcurrentHashMap 源码解析
五个线程同时往 HashMap 中 put 数据会发生什么? ConcurrentHashMap 是怎么保证线程安全的? 在分析 HashMap 源码时还遗留这两个问题,这次我们站在 Java 多线程 ...
- 并发编程(十六)——java7 深入并发包 ConcurrentHashMap 源码解析
以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容 ...
- 深入并发包 ConcurrentHashMap 源码解析
以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容 ...
- ConcurrentHashMap源码解析 JDK8
一.简介 上篇文章详细介绍了HashMap的源码及原理,本文趁热打铁继续分析ConcurrentHashMap的原理. 首先在看本文之前,希望对HashMap有一个详细的了解.不然看直接看Concur ...
随机推荐
- cp: 无法创建普通文件 : 文件已存在
背景 碰到一个偶现的编译出错问题,如图 报错的信息是 cp: 无法创建普通文件"xxx": 文件已存在 排查原因 看了下 Makefile,这句非常简单,就是 cp ./xxx . ...
- 简说Spring中的资源加载
声明: 本文若有 任何纰漏.错误,请不吝指正!谢谢! 问题描述 遇到一个关于资源加载的问题,因此简单的记录一下,对Spring资源加载也做一个记录. 问题起因是使用了@PropertySource来进 ...
- DQN(Deep Q-learning)入门教程(四)之Q-learning Play Flappy Bird
在上一篇博客中,我们详细的对Q-learning的算法流程进行了介绍.同时我们使用了\(\epsilon-贪婪法\)防止陷入局部最优. 那么我们可以想一下,最后我们得到的结果是什么样的呢?因为我们考虑 ...
- GitHub 热点速览 Vol.22:如何打造超级技术栈
作者:HelloGitHub-小鱼干 摘要:build-your-own-x,无论是新手还是老手,这都是一个指向标.方向有了,剩下就是时间和实践的事情,收集了大量可用于软件和 Web 开发的 Publ ...
- 附021.Traefik-ingress部署及使用
一 Helm部署 1.1 获取资源 [root@master01 ~]# mkdir ingress [root@master01 ~]# cd ingress/ [root@master01 ing ...
- ASP.NET Core 3.x API版本控制
前言 一般来说需要更改我们API的时候才考虑版本控制,但是我觉得我们不应该等到那时候来实现它,我们应该有一个版本策略从我们应用程序开发时就开始制定好我们的策略,我们一直遵循着这个策略进行开发. 我们其 ...
- Rocket - diplomacy - enumerateMask
https://mp.weixin.qq.com/s/s3hr5JJX2_pwNgdu8WqV0Q 介绍enumerateMask的实现.(仅供理解,非严谨证明) 1. 基本定义 ...
- JSON.parse() 的实现
目录 1. JSON.parse() 2. 前置知识 2.1 JSON格式中的数据类型 2.2 转义字符的处理 2.2 判断对象是否相等 2.3 寻找匹配的字符串 2.4 基础的递归思想 3. 实现流 ...
- spring Resource
在日常程序开发中,处理外部资源是很繁琐的事情,我们可能需要处理URL资源.File资源资源.ClassPath相关资源.服务器相关资源(JBoss AS 5.x上的VFS资源)等等很多资源.因此处理这 ...
- Java实现 LeetCode 284 顶端迭代器
284. 顶端迭代器 给定一个迭代器类的接口,接口包含两个方法: next() 和 hasNext().设计并实现一个支持 peek() 操作的顶端迭代器 – 其本质就是把原本应由 next() 方法 ...