1.Set集合与Map

仔细对比观察上面Set下和Map下的接口名,不难发现它们如此的相似,必有原因

如果只考察Map的Key会发现,它们不可以重复,没有顺序,也就是说把Map的所有的Key集中起来就是一个Set集合,所以map有了方法 Set<K> keySet();

对于Map而言,实际上他就相当于一个所有元素都是Key-Value的Set集合

问题:如何用Set实现一个Map??

思路:定义一个SimpleEntry类,该类代表一个Key-Value对,当Set集合中的元素都是SimpleEntry时,该Set就可以当作Map来使得

HashMap和HashSet

对于HashSet而言,系统采用Hash算法决定集合元素的存储位置,这样可以保证快速存取集合元素

对于HashMap而言,系统将Value当成Key的附属,系统根据Hash算法来决定Key的存储位置,这样保证快速存取集合的Key,而Value总是跟紧随Key存储

虽然集合号称存储的Java对象,但实际上并不会真的把Java对象放入Set集合,而只是在Set集合中保留这些对象的引用

  存储方式:

对于HashMap,程序执行map.put("语文",80.0);时,系统调用  "语文" 的HashCode的方法,得出其HashCode值,HashMap根据其HashCode值来决定元素的存储位置

HashMap类的put(K key,V value)方法源代码如下(来自JDK 1.6     , JDK 1.8有所改进)

public V put(K key, V value) {
    //如果key为null,调用putForNullKey方法进行处理
if (key == null)
return putForNullKey(value);
      //根据Key的hashCode 值计算Hash值
int hash = hash(key.hashCode());
      //根据指定hash值查找对应table中的索引
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
        //找到指定key与需要放入的key相等(hash值相同,并且通过equals方法比较返回true)
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
    //如果i索引处的entry为null,表明此处没有entry
modCount++;
    //将key value 添加到索引i处
addEntry(hash, key, value, i);
return null;
}

从上面的程序可以看出HashMap在存储Key-Value的时候没有考虑Value,仅仅根据Key来计算Entry的存储位置,Map.Entry是一个重要的接口,它代表了一个Key-Value对

从源代码中可以看出

  当向HashMap中放入Key-Value对时,首先根据Key的HashCode值决定该Entry的存储位置

  如果两个Entry的Key的HashCode值相等,那它们的存储位置相同

  如果两个Entry的Key通过equals比较返回true,那么新添加的Entry的Value会覆盖原来的,但Key不会覆盖

  如果两个Entry的Key通过equals比较返回false,那么新添加的Entry与集合中原来的Entry形成Entry链,新添加的Entry位于链的头部

static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//指向下一个entry 形成entry链
final int hash; ........
void addEntry(int hash, K key, V value, int bucketIndex) {
  //获取指定bucketIndex索引处的entry
Entry<K,V> e = table[bucketIndex];
      //将新创建的entry放入指定bucketIndex索引处,并让新的entry指向原来的entry 如果原来的bucketIndex处没有entry,则不会形成entry链
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
      //如果map中的Key-Value对的数量超过了上限
if (size++ >= threshold)
        //把table对象的长度扩充到原来的2倍
resize(2 * table.length);
}

其中有两个参数:

  size:该变量保存了hashmap中所包含的Key-Value对我数量

  threshold:该变量表示hashmap所能容纳的Key-Value对的极限 ,它的值等于hashmap的容量乘以负载因子(loader factor)

  table就是一个普通的数组,数组的长度就是hashmap的容量,table里面存储的就是hashmap的entry,table中存储元素的位置叫桶bucket

  默认的HashMap()构造器会构造一个初始容量为16,负载因子为0.75的hashmap  (0.75是时间与空间上的一种折衷)

public HashMap(int initialCapacity, float loadFactor) {
    //初始容量不为为负数
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
   //如果初始容量大于最大容量,让初始容量等于最大容量  
     if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
      //负载因子必须大于0的数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor); // Find a power of 2 >= initialCapacity
      
    //计算出大于initialCapacity的最小的2的n次方
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1; this.loadFactor = loadFactor;
      //设置容量等于极限等于容量乘以负载因子
threshold = (int)(capacity * loadFactor);
      //初始化table数组
table = new Entry[capacity];
init();
}
 public V get(Object key) {
if (key == null)
return getForNullKey();
//根据key的hashcode值计算他的hash码
int hash = hash(key.hashCode());
      //直接取出table数组中指定索引处的值
for (Entry<K,V> e = table[indexFor(hash, table.length)];
        
e != null;
          //搜索entry链的下一个entry
e = e.next) {
Object k;
        //如果该entry的key与被搜索的key相同,就把value返回
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}

总结:HashMap底层将Key-Value当成一个整体进行处理,这个整体就是一个entry对象,底层采用一个Entry[]数组table来保存所有Key-Value对

需要存储一个Entry对象时会根据Hash算法来决定其存储位置,取出 一个Entry时也会根据Hash算法来找他的位置,直接取出Entry

如果一开始就知道要在HashMap中存储多个Key-Value对时,可以在初始化指定一个较大的容量,省去resize的性能损耗

HashSet

HashSet底层是采用HashMap实现 的,底层封装了一个HashMap,所有的HashSet集合中的元素实际是由HashMap中的Key来保存的,而Value是一个PRESENT,它是一个静态的Object对象

public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
  //使用hashmap的key保存hashset的值
private transient HashMap<E,Object> map;
  //定义一个虚拟的Object对象当作value的值
private static final Object PRESENT = new Object();
  //初始hashset底层会初始化一个hashmap
public HashSet() {
map = new HashMap<E,Object>();
}
  
  //以指定的参数创建一个hashset,底层是以指定的参数创建一个hashmap
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<E,Object>(initialCapacity, loadFactor);
} public HashSet(int initialCapacity) {
map = new HashMap<E,Object>(initialCapacity);
} HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}
  //调用map的keyset返回所有的hashset值
public Iterator<E> iterator() {
return map.keySet().iterator();
} public int size() {
return map.size();
} public boolean isEmpty() {
return map.isEmpty();
} public boolean contains(Object o) {
return map.containsKey(o);
}
  //将指定元素放入hashset中,实际就是将元素的作为key放入hashmap中
public boolean add(E e) {
return map.put(e, PRESENT)==null;
} public boolean remove(Object o) {
return map.remove(o)==PRESENT;
} public void clear() {
map.clear();
}
}

由于hashset底层是由hashmap实现的,那么根据hashmap的实现原理,当hashset中放入重复的元素时,不会覆盖原来的,只是value会覆盖

注意 重写放入hashset和hashmap中的对象的hashcode方法 和 equals方法很重要,并且 要一至,当hashcode方法返回true时equals方法也要返回true才行

如何正确的重写hashCode方法 和equals方法 ?

所有参与计算hashcode返回值的参数都应用于作为equals()比较的标准

程序员的基本功之Java集合的实现细节的更多相关文章

  1. 从程序员到CTO的Java技术路线图 作者:zz563143188

    在技术方面无论我们怎么学习,总感觉需要提升自已不知道自己处于什么水平了.但如果有清晰的指示图供参考还是非常不错的,这样我们清楚的知道我们大概处于那个阶段和水平. Java程序员 高级特性 反射.泛型. ...

  2. 程序员必须掌握的Java 框架,小白学会之后15k不是问题

    Spring 的核心特性是什么?Spring 优点? Spring 的核心是控制反转(IoC)和面向切面(AOP) Spring 优点: 程序员必须掌握的Java 框架,学会之后50k不是问题 (1) ...

  3. 寻找下一个结点 牛客网 程序员面试金典 C++ java Python

    寻找下一个结点 牛客网 程序员面试金典 C++ java Python 题目描述 请设计一个算法,寻找二叉树中指定结点的下一个结点(即中序遍历的后继). 给定树的根结点指针TreeNode* root ...

  4. 碰撞的蚂蚁 牛客网 程序员面试金典 C++ Java Python

    碰撞的蚂蚁 牛客网 程序员面试金典 C++ Java Python 题目描述 在n个顶点的多边形上有n只蚂蚁,这些蚂蚁同时开始沿着多边形的边爬行,请求出这些蚂蚁相撞的概率.(这里的相撞是指存在任意两只 ...

  5. 检查是否是BST 牛客网 程序员面试金典 C++ java Python

    检查是否是BST 牛客网 程序员面试金典  C++ java Python 题目描述 请实现一个函数,检查一棵二叉树是否为二叉查找树. 给定树的根结点指针TreeNode* root,请返回一个boo ...

  6. 黑马程序员:轻松精通Java学习路线连载1-基础篇!

    编程语言Java,已经21岁了.从1995年诞生以来,就一直活跃于企业中,名企应用天猫,百度,知乎......都是Java语言编写,就连现在使用广泛的XMind也是Java编写的.Java应用的广泛已 ...

  7. 一个老牌程序员说:做Java开发,怎么可以不会这 20 种类库和 API

  8. 从程序员到CTO的Java技术路线图(我爱分享)

    在技术方面无论我们怎么学习,总感觉需要提升自已不知道自己处于什么水平了.但如果有清晰的指示图供参考还是非常不错的,这样我们清楚的知道我们大概处于那个阶段和水平. Java程序员 高级特性 反射.泛型. ...

  9. 从程序员到CTO的Java技术路线图 (转自安卓巴士)

    在技术方面无论我们怎么学习,总感觉需要提升自已不知道自己处于什么水平了.但如果有清晰的指示图供参考还是非常不错的,这样我们清楚的知道我们大概处于那个阶段和水平. Java程序员 高级特性 反射.泛型. ...

随机推荐

  1. web移动端Fixed在Input获取焦点时ios下产生的BUG及处理

    1.现象 可以看到下面两张图,图1搜索框为fixed固定在顶部,滚动没有任何问题. 图2当光标进入搜索框时,ios自作聪明的把光标定位到中间,并且fixed属性被自动修改成了absolute.此时注意 ...

  2. 这是一款可以查阅Github上的热门趋势的APP

    随时查阅当前Github上的热门趋势.使用Material Design设计风格,和流行的MVP+Retrofit+RxJava框架.数据抓取自https://github.com/trending ...

  3. C语言位运算符:与、或、异或、取反、左移和右移

    语言位运算符:与.或.异或.取反.左移和右移 位运算是指按二进制进行的运算.在系统软件中,常常需要处理二进制位的问题.C语言提供了6个位操作运算符.这些运算符只能用于整型操作数,即只能用于带符号或无符 ...

  4. BZOJ 1009 :[HNOI2008]GT考试(KPM算法+dp+矩阵快速幂)

    这道到是不用看题解,不过太经典了,早就被剧透一脸了 这道题很像ac自动机上的dp(其实就是) 然后注意到n很大,节点很小,于是就可以用矩阵快速幂优化了 时间复杂度为o(m^3 *log n); 蒟蒻k ...

  5. bootstrap select2 参数详解

    Select2使用示例地址: https://select2.github.io/examples.html Select2参数文档说明: https://select2.github.io/opti ...

  6. java程序员--小心你代码中的内存泄漏

    当你从c&c++转到一门具有垃圾回收功能的语言时,程序员的工作就会变得更加容易,因为你用完对象,他们会被自动回收,但是,java程序员真的不需要考虑内存泄露吗? 其实不然 1.举个例子-看你能 ...

  7. Ant 警告:sun.misc.BASE64Decoder 是 Sun 的专用 API,可能会在未来版本中删除

    如果你用Ant编译项目,而且在项目中用了SUN的专用API,你会得到警告信息,然后Ant会报告编译失败: 这当然是不合理的,javac只是警告而已,ant凭什么就直接报失败呢? 其实最好的解决办法是避 ...

  8. ajax跨域问题及解决

    overview ajax是一种创建交互式网页应用的网页开发技术,是一种用于创建快速动态网页的技术,通过在后台与服务器进行少量数据交换.而ajax的跨域问题则是请求了其他项目的接口地址,当协议.子域名 ...

  9. EFDB 基本规范&知识

    public abstract class AccountRole //这是继承类型 { [Key] public int ID { get; set; } public bool isDisable ...

  10. 规范 : disable account

    前台的cookies在后台会去拿account出来,之后在filter status = disable的 用户在登入使用界面请求一个ajax,这时发现是401没有权限,这通常是admin把用户的ac ...