HashMap详解

HashMap源码

前言:

本文是基于JKD1.8.0_291 介绍的,其他版本略有不同,大同小异

1、成员属性介绍

// 默认的初始容量,必须是2的幂次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量,在两个带参数的构造函数隐式指定更高值时使用。 必须是 2 的幂 <= 1<<30。
static final int MAXIMUM_CAPACITY = 1 << 30;
// 在构造函数中未指定时使用的负载因子(触发扩容阈值)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 树化阈值
static final int TREEIFY_THRESHOLD = 8;
// 树降级为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 树化的另一个参数,当哈希表中的所有元素个数超过64时,才允许树化
static final int MIN_TREEIFY_CAPACITY = 64;
// 哈希表 时候初始化?在第一次put元素的时候调用resize()方法进行初始化
transient Node<K, V>[] table;
// 当前哈希表中元素个数
transient int size;
// 当前哈希表结构修改次数  key替换不算修改
transient int modCount;
// 扩容阈值,当你的哈希表中的元素超过阈值时,触发阈值 threshold = capacity(表示当前表的长度) * loadFactor(负载因子的值)
int threshold;
// 负载因子
final float loadFactor;

2、静态方法

// 扰动函数 
// 作用:让key的hash值的高16也参与路由算法,是为了防止一些实现比较差的 hashCode() 方法,减少碰撞,尽可能使元素散列地存储。
// 路由算法 (length -1 ) & hashcode = index
// 异或算法 相同返回0 不同返回1
// 假设
// h = 0b 0010 0101 1010 1100 0011 1111 0010 1100
// 0b 0010 0101 1010 1100 0011 1111 0010 1100
// ^
// 0b 0000 0000 0000 0000 0010 0101 1010 1100
// => 0010 0101 1010 1100 0001 1001 1000 0000
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

二进制运算不想点进去的最下方有介绍

// 作用:返回一个大于等于当前值cap的一个数字,并且这个数字一定是2的次方数
//  >> 右移,高位补符号位
//  >>> 无符号右移,高位补0
//  ^ 异或,相同为0,不同为1
//  | 或 两个位只要有一个为1,那么结果就是1,否则就为0
// 假设我们传递的 cap 为13 那么13的二进制为 0000 0000 0000 1101
static final int tableSizeFor(int cap) {
    int n = cap - 1; 	// n = 12 0000 0000 0000 1100
    n |= n >>> 1;	 	// n = 14 0000 0000 0000 1110 n >>> 1 = 6 0000 0000 0000 0110 
    n |= n >>> 2;	 	// n=  15 0000 0000 0000 1111 n >>> 2 = 3 0000 0000 0000 0011		
    n |= n >>> 4;		// n = 15 0000 0000 0000 1111 n >>> 4 = 0 0000 0000 0000 0000
    n |= n >>> 8;		// n = 15 0000 0000 0000 1111 n >>> 8 = 0 0000 0000 0000 0000
    n |= n >>> 16;		// n = 15 0000 0000 0000 1111 n >>> 16 = 0 0000 0000 0000 0000
    // 所以最后的返回数字是 15 + 1 = 16 也就验证了 这个方法返回的是2的幂次方
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

3、构造方法

    // 只是构造一个具有默认初始容量 (16) 和默认负载因子 (0.75) 的空HashMap 。
	public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }
    // 构造一个具有指定初始容量和默认负载因子 (0.75) 的空HashMap 。
	public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
     // 使用与指定Map相同的映射构造一个新的HashMap 。 HashMap是使用默认负载因子 (0.75) 创建的,初始容量足以在指定的Map 中保存映射
     public HashMap(Map<? extends K, ? extends V> m) {
         this.loadFactor = DEFAULT_LOAD_FACTOR;
         putMapEntries(m, false);
     }
    // 构造一个具有指定初始容量和负载因子的空HashMap 。
	public HashMap(int initialCapacity, float loadFactor) {
        //其实就是做了一些校验
        //capacity必须大于零,最大值也就是MAXIMUM_CAPACITY = 1073741824
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //loadFactor必须大于零
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
        this.loadFactor = loadFactor;
        // 这里也就是调用我们上面讲到的 返回一个大于等于当前值 initialCapacity 的一个数字,并且这个数字一定是2的次方数
        this.threshold = tableSizeFor(initialCapacity);
    }

4、put()方法

	// 将指定值与此映射中的指定键相关联。 如果映射先前包含键的映射,则旧值将被替换
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

可以发现,我们调用put方法的时候,实际上调用的是其内部的putVal方法,所以接下来一起来看一看

	// 在了解这个方法之前我们先介绍一下方法里面的四个参数
	// hash 这个也就是我们在上边通过key获取到的hash值
	// key  要放置的键
	// value 要放置的值
	// onlyIfAbsent 如果为真,则不更改现有值
	// evict 如果为 false,则表处于创建模式
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // tab:引用当前hashmap的散列表
        // p:表示当前散列表的元素
        // n:表示散列表数组的长度
        // i:表示路由寻址,结果
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 延迟初始化逻辑,第一次调用putVal时会初始化HashMap对象中的最耗费内存的散列表
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // (n - 1) & hash 路由算法  算出 当前元素位于数组的哪个位置
        // 最简单的一种情况,寻址找到的桶位,桶位刚刚好是null 这个时候 直接将当前k-v => node 扔进去就可以了
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // e:不为null的话,找到了一个与当前要插入的key-value一致的元素
            // k:表示临时的一个元素key
            Node<K,V> e; K k;
            // 表示桶位中的该元素,与你当前插入的元素的key完全一致,表示后续需要进行替换操作
            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 {
                // 链表的情况 而且链表的头元素与我们要插入的key不一致
                for (int binCount = 0; ; ++binCount) {
                    // 条件成立的话 说明迭代到最后一个元素了,也没有找到一个与你要插入的key一致的node
                    // 说明需要加入到当前链表的末尾
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 条件成立的话,说明当前链表的长度,达到了树华的标准,需要进行树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 树化操作
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 条件成立的话,说明找到了相同key的node元素,需要进行替换操作
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // e不等于null,条件成立说明找到了一个与你插入元素key完全一致的数据,需要进行操作
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // 只有传入的putVal中的参数 onlyIfAbsent 为false,或者 之前key完全一至的value是一个null才允许替换
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // modCount:表示散列表结构被修改的次数,替换node元素的value 不计数
        ++modCount;
        // 插入新元素,size自增,如果自增后的值大于扩容阈值,则触发扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

5、resize()方法

为什么需要扩容?

为了解决哈希冲突导致的链化影响查询效率的问题,扩容会缓解该问题

    // 初始化或加倍表大小。 如果为空,则根据字段阈值中持有的初始容量目标进行分配。 
	// 否则,因为我们使用的是 2 的幂扩展,所以每个 bin 中的元素必须保持相同的索引,或者在新表中以 2 的幂的偏移量移动
	final Node<K,V>[] resize() {
        // oldTab:引用扩容前的哈希表
        Node<K,V>[] oldTab = table;
        // /olCap:表示扩容前table数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // oldThr:表示扩容之前的阈值,触发本次扩容的阈值
        int oldThr = threshold;
        // newCap:扩容之后的table数组的大小
        // newThr:扩容之后,下次触发扩容的条件
        int newCap, newThr = 0;
        // 条件如果成立说明 hashmap中的散列表已经初始化过了,是一次正常的扩容
        if (oldCap > 0) {
            // 扩容之前的 table 数组大小已经达到 最大阈值后,则不扩容,且设置扩容条件为 Integer 最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // oldCap左移一位实现数值翻倍,并且赋值给newCap, newCap 小于数组最大值限制 且 扩容之前的阈值 >= 16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        // oldCap == 0,说明hashMap中的散列表是null 没有初始化
        // 1.new HashMap(initCap,loadFactor);
        // 2.new HashMap(initCap);
        // 3.new HashMap(map);并且这个map有数据的
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // 零初始阈值表示使用默认值
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;  // 16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  // 12
        }
        // newThr为零时,通过newCap和loadFactor计算出一个newThr
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        // newThr赋值给threshold 相当于 下次扩容就是按照newThr的值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        // 创建出一个更长更大的数组 也有可能是第一次创建数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        // 说明HashMap本次扩容之前 table不为null 也就不是初始化操作
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                // 当前node的节点
                Node<K,V> e;
                // 说明当前桶位中有数据,但是数据具体是 单个数据 还是链表 还是红黑树 并不知道
                if ((e = oldTab[j]) != null) {
                    // 方便JVM GC时回收内存
                    oldTab[j] = null;
                    // 第一种情况 当前桶位只有一个元素 从未发生碰撞 这情况 直接计算出当前元素应存放在 新数组中的位置,然后 扔进去就可以了
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    // 第二种情况:当前节点已经树化 红黑树
                    else if (e instanceof TreeNode)
                        // 将原本的二叉树结构拆分组成新的红黑树
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //第三种情况:桶位已经形成链表
                        // jdk1.8中 旧链表迁移新链表 链表元素相对位置没有变化; 实际是对对象的内存地址进行操作 
                        // jdk1.7中 旧链表迁移新链表 如果在新表的数组索引位置相同,则链表元素会倒置
                        
                        //地位链表:存放在扩容之后的数组的下标位置,与当前数组的下标位置一致
                        Node<K,V> loHead = null, loTail = null;
                        //高位链表:存放在扩容之后的数组的下标位置为 当前数组下表位置 + 扩容之前数组的长度
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // hash值与旧的长度做与运算用于判断元素的在数组中的位置是否需要移动
                             /**
                  * 举例:
                  *  (e.hash & oldCap) == 1
                  *  e.hash & (oldCap - 1)  e.hash & (newCap - 1)  e.hash & oldCap
                  *  ...0101 0010            ...0101 0010           ...0101 0010  
                  * &      0 1111		           1 1111                 1 0000
                  *        0 0010                  1 0010				  1	0000
                  *
                  *	 (e.hash & oldCap) == 0
                  *  e.hash & (oldCap - 1)  e.hash & (newCap - 1)  e.hash & oldCap
                  *  ...0100 0010            ...0100 0010           ...0100 0010  
                  * &      0 1111		           1 1111                 1 0000
                  *        0 0010                  0 0010				  0	0000
                  */
                  // 总结:
                  // 数组的长度为2^N,即高位为1,其余为0,计算e.hash & oldCap只需看oldCap最高位1所对应的hash位
                  // 因为newCap进行了双倍扩容,即将oldCap左移一位,那么oldCap-1相当于newCap-1右移一位,右移后高位补0,与运算只能得到0。
                  // 如果(e.hash & oldCap) == 0,hash值需要与运算的那一位为0,那么oldCap - 1与newCap - 1的高位都是0,其余位又是相同的,表明旧元素与新元素计算出的位置相同。
                  // 同理,当其 == 1 时,oldCap-1高位为0,newCap-1高位为1,其余位相同,计算出的新元素的位置比旧元素位置多了2^N,即得出新元素的下标=旧下标+oldCap   
                            // 如果为0,元素位置在扩容后数组中的位置没有发生改变
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 不为0,元素位置在扩容后数组中的位置发生了改变,新的下标位置是原下标位置+原数组长
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

6、get()方法

    // 返回指定键映射到的值,如果此映射不包含键的映射,则返回null 。
	// 更正式地说,如果此映射包含从键k到值v的映射,使得(key==null ? k==null : key.equals(k)) ,则此方法返回v ; 否则返回null 。 (最多可以有一个这样的映射。)
	// 返回值null不一定表示映射不包含键的映射; Map也有可能将键显式映射到null 。 containsKey操作可用于区分这两种情况。
	public V get(Object key) {
        Node<K, V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final Node<K,V> getNode(int hash, Object key) {
        // tab:引用当前HashMap的散列表
        // first:桶位中的头元素
        // e:临时node元素
        // n:table数组长度
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 第一种情况:定位出来的桶位元素 即为我们要get的元素
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 说明当前桶位不止一个元素,可能是链表也可能是红黑数
            if ((e = first.next) != null) {
                // 第二种情况:桶位升级成了红黑树
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 第三种情况:桶位形成了链表
                do {
                    // 遍历链表,获取我们需要的get元素
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

7、remove()方法

    // 如果存在,则从此映射中删除指定键的映射。
	public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
    	// tab:引用当前HashMap中的散列表
        // p:当前node元素
        // n:表示散列表数组长度
        // index:表示寻址结果
        Node<K,V>[] tab; Node<K,V> p; int n, index;
    	// 说明路由的桶位是有数据的,需要进行查找操作,进行删除
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            // node:查找到的结果
            // e:当前Node的下一个元素
            Node<K,V> node = null, e; K k; V v;
            // 第一种情况:当前桶位中的元素 即为你要删除的元素
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            // 说明当前桶位要么是链表要么是红黑树
            else if ((e = p.next) != null) {
                // 判断当前桶位是否升级为红黑树
                if (p instanceof TreeNode)
                    // 红黑树查找操作
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    // 链表情况
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            // 判断node不为空的话,说明按照key查找到需要删除的数据
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                // 第一种情况:node是树节点,说明需要进行树节点移除操作
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                // 第二种情况:桶位元素即位查找结果,则将该元素的下一个元素放置桶位中
                else if (node == p)
                    tab[index] = node.next;
                // //第三种情况:将当前元素p的下一个元素 设置成 要删除元素的 下一个元素
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

HashMap jdk1.8比1.7的优化

jdk1.8的hash方法比1.7性能有所提升;
jdk1.7链表采用头插法,头插法比较快,但容易造成死循环;jdk1.8是尾插法;
jdk1.8中 旧链表迁移新链表,链表元素相对位置没有变化; 实际是对对象的内存地址进行操作,jdk1.7中 旧链表迁移新链表 如果在新表的数组索引位置相同,则链表元素会倒置

HashMap中的红黑树

二叉树

二叉树(英语:Binary tree)是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构。通常分支被称作“左子树”或“右子树”。二叉树的分支具有左右次序,不能随意颠倒。

二叉查找树

二叉查找树(英语:Binary Search Tree),也称为二叉搜索树、有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

  1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于或等于它的根节点的值;
  3. 任意节点的左、右子树也分别为二叉查找树;

一颗二叉查找树如下图所示:

在这里插入图片描述
但是,当我们顺序插入一系列节点后,二叉查找树就退化为线性表,如下图所示:
在这里插入图片描述
虽然二叉查找树退化为线性表之后,最坏效率降为 O(n)。但依旧有很多改进版的二叉查找树可以使树高为O(log n),从而将最坏效率降至O(log n),如AVL树、红黑树等。

AVL树

AVL 树是最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为 1 ,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是 O(logn)。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。

在 AVL 树中,节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子 1、0 或 -1 的节点被认为是平衡的。带有平衡因子 -2 或 2 的节点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来。

AVL 树自平衡的方式是做一次或多次所谓的"AVL旋转"。

红黑树

红黑树(英语:Red–black tree)也是一种自平衡二叉查找树。红黑树在二叉查找树的基础之上,对每个节点都增加了颜色属性,分为红色或黑色。在二叉查找树强制的一般要求以外,对于任何有效的红黑树增加了如下 5 条额外要求:

  1. 节点是红色或黑色。
  2. 根节点是黑色。
  3. 所有叶子都是黑色(叶子是NIL节点)。
  4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

下面是一个具体的红黑树的图例:

img

红黑树的这些特性,保证了红黑树从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,造成的结果是红黑树大致上是平衡的。如上图所示,"nil叶子"或"空(null)叶子"不包含数据而只充当树在此结束的指示。

对于红黑树的读与写,因为每一个红黑树都是一个(特殊的)二叉查找树,因此红黑树上的只读操作与普通二叉查找树上的只读操作相同。然而,在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质。恢复红黑树的性质需要少量 O(log n) 的颜色变更(实际是非常快速的)和不超过三次的树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操作时间仍可以保持为 O(log n) 次。

链表转为红黑树的过程

HashMap 的桶类型除了 Node (链表)外,还有 TreeNode (树)。

TreeNode 类包含成员方法 treeify() ,该方法的作用是形成以当前 TreeNode 对象为根节点的红黑树。

树化过程不外乎是循环遍历链表,构造一颗二叉查找树,最后使用红黑树的平衡方法进行自平衡。

该方法源码如下:

// 形成从此节点链接的节点的树
final void treeify(Node<K, V>[] tab) {
    // 树化的根节点
    TreeNode<K, V> root = null;
    // 循环遍历当前TreeNode链表
    for (TreeNode<K, V> x = this, next; x != null; x = next) {
        next = (TreeNode<K, V>) x.next;
        x.left = x.right = null;
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
            // 如果已设置根节点
        } else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            // 从根节点开始遍历,插入新节点
            for (TreeNode<K, V> p = root; ; ) {
                int dir, ph;
                K pk = p.key;
                // 比较当前节点的hash值与新节点的hash值
                // 若是新节点hash值较小
                if ((ph = p.hash) > h)
                    dir = -1;
                // 若是新节点的hash值较大
                else if (ph < h)
                    dir = 1;
                // 若是新节点与当前节点的hash值相等
                else if ((kc == null &&
                          // 如果新节点的key没有实现Comparable接口..
                          (kc = comparableClassFor(k)) == null) ||
                         // 或者实现了Comparable接口但是k.compareTo(pk)结果为0
                         (dir = compareComparables(kc, k, pk)) == 0)
                    // // 则调用tieBreakOrder继续比较大小
                    dir = tieBreakOrder(k, pk);

                TreeNode<K, V> xp = p;
                // 如果新节点经比较后小于等于当前节点且当前节点的左子节点为null,则插入新节点,反之亦然
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    // 平衡红黑树
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}

红黑树的左旋和右旋

红黑树的左旋和右旋其实很简单,所谓的左旋是把要平衡的节点向左下旋转成一个叶子节点。

如下图所示:
在这里插入图片描述
所谓的右旋是把要平衡的节点向右下旋转成一个叶子节点。

如下图所示:

在这里插入图片描述

左旋源码
static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root,
                                        TreeNode<K, V> p) {
    // p: 当前节点
    // p: 当前节点的父节点的父节点
	// r: 当前节点的右儿子
	// rl: 当前节点的右儿子的左儿子
    TreeNode<K, V> r, pp, rl;
    // 如果当前节点和当前节点的右儿子不为空,就左旋
    if (p != null && (r = p.right) != null) {
        // 当前节点的右儿子的左儿子成为当前节点的右儿子
        if ((rl = p.right = r.left) != null)
            rl.parent = p;
        // 当前节点的右儿子成为当前节点的父节点
        if ((pp = r.parent = p.parent) == null)
            // 如果当前节点是根节点,那么r的颜色必须是黑色
            (root = r).red = false;
        else if (pp.left == p)
            pp.left = r;
        else
            pp.right = r;
        r.left = p;
        p.parent = r;
    }
    return root;
}
右旋源码
static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root,
                                         TreeNode<K, V> p) {
    // p: 当前节点
    // p: 当前节点的父节点的父节点
	// l: 当前节点的左儿子
	// lr: 当前节点的左儿子的右儿子
    TreeNode<K, V> l, pp, lr;
    // 如果当前节点和当前节点的左儿子不为空,就右旋
    if (p != null && (l = p.left) != null) {
        // 当前节点的左儿子的右儿子成为当前节点的左儿子
        if ((lr = p.left = l.right) != null)
            lr.parent = p;
        // 当前节点的左儿子成为当前节点的父节点
        if ((pp = l.parent = p.parent) == null)
            // 如果当前节点是根节点,那么r的颜色必须是黑色
            (root = l).red = false;
        else if (pp.right == p)
            pp.right = l;
        else
            pp.left = l;
        l.right = p;
        p.parent = l;
    }
    return root;
}

红黑树的插入

红黑树是一棵特殊的二叉查找树,如同二叉查找树的插入一样,红黑树的插入,也需要判断插入节点与当前节点的大小。如果插入节点大于或等于当前节点,则插入当前节点的右子树;否则,插入到左子树。循环此过程,直到找到为空的叶子节点放入即可。

final TreeNode<K, V> putTreeVal(HashMap<K, V> map, Node<K, V>[] tab,
                                int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    // root: 树根节点
    TreeNode<K, V> root = (parent != null) ? root() : this;
    for (TreeNode<K, V> p = root; ; ) {
        // p: 当前节点
        // dir: 标识新节点应该插入到当前节点的左子树还是右子树
        // ph: 当前节点的hash值
        // pk: 当前节点的key
        int dir, ph;
        K pk;
        // 判断新节点应该插入到当前节点的左子树还是右子树
        if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            if (!searched) {
                TreeNode<K, V> q, ch;
                searched = true;
                if (((ch = p.left) != null &&
                     (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                     (q = ch.find(h, k, kc)) != null))
                    return q;
            }
            dir = tieBreakOrder(k, pk);
        }

        TreeNode<K, V> xp = p;
        // 终于从上向下遍历到了空的叶子节点,插入新节点
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K, V> xpn = xp.next;
            TreeNode<K, V> x = map.newTreeNode(h, k, v, xpn);
            // 新节点位置在左子树还是右子树
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            // 新节点指向父节点
            x.parent = x.prev = xp;
            if (xpn != null)
                // 如果有兄弟节点,兄弟节点的上一个节点指向新节点
                ((TreeNode<K, V>) xpn).prev = x;
            // 最后平衡红黑树,并保证根节点在哈希桶的头部
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }

红黑树的平衡插入

红黑树的插入与二叉查找树的不同之处,在于最后的平衡部分。

插入节点之后的红黑树平衡方法如下:

/**
 * 平衡红黑树-当新增后
* x: 影响平衡的点(俗称:当前节点)
*/
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root,
                                              TreeNode<K, V> x) {
    // 新增节点x默认是红色
    x.red = true;
    // xp父节点 xpp祖父节点 xppl祖父左节点 xppr 祖父右节点(p: parent, l: left, r: right)
    for (TreeNode<K, V> xp, xpp, xppl, xppr; ; ) {
        if ((xp = x.parent) == null) {
            // 如果插入的是根节点,根据红黑树性质之根节点是黑色,直接涂黑即可
            x.red = false;
            return x;
        } 
        // 插入节点的父节点是黑色或者父节点是根节点,红黑树没有被破坏,不需要调整
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        // 父节点是祖父节点的左子节点
        if (xp == (xppl = xpp.left)) {
            // 当前节点的父节点是红色,且叔叔节点也是红色
            if ((xppr = xpp.right) != null && xppr.red) {
                // 叔叔节点设置为黑色
                xppr.red = false;
                // 父节点设置为黑色
                xp.red = false;
                // 祖父节点设置为红色
                xpp.red = true;
                // 将当前节点指向祖父节点,从新的当前节点重新开始算法
                x = xpp;
            } else {
                // 当前节点的父节点是红色,且叔叔节点是黑色,当前节点是其父右子节点
                if (x == xp.right) {
                    // 当前节点的父节点作为新的当前节点,以新当节点为支点左旋
                    root = rotateLeft(root, x = xp);
                    // 重新赋值
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    // 父节点设置为黑色
                    xp.red = false;
                    if (xpp != null) {
                        // 祖父节点设置为红色
                        xpp.red = true;
                        // 以祖父节点为支点右旋
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        // 父节点是祖父节点的右子节点
        else {
            // 当前节点的父节点是红色,且叔叔节点也是红色
            if (xppl != null && xppl.red) {
                // 叔叔节点设置为黑色
                xppl.red = false;
                // 父节点设置为黑色
                xp.red = false;
                // 祖父节点设置为红色
                xpp.red = true;
                // 将当前节点指向祖父节点,从新的当前节点重新开始算法
                x = xpp;
            } else {
                // 当前节点的父节点是红色,且叔叔节点是黑色,当前节点是其父左子节点
                if (x == xp.left) {
                    // 当前节点的父节点作为新的当前节点,以新当节点为支点右旋
                    root = rotateRight(root, x = xp);
                    // 重新赋值
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    // 父节点设置为黑色
                    xp.red = false;
                    if (xpp != null) {
                        // 祖父节点设置为红色
                        xpp.red = true;
                        // 以祖父节点为支点左旋
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

红黑树的删除

红黑树的删除相比插入更加复杂,需要先从链表结构上进行删除,也就是处理当前节点的 next 指针与 prev 指针。如果当前树的节点太少,那么就将树退化为链表。然后,查找被删除节点的后继节点。所谓后继节点,就是当删除节点p后,如果p有子节点,p的子节点上移继承其位置。当找到后继节点,立即删除节点,如果被删除节点是红色,那么不需要平衡,否则平衡红黑树。

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
    int n;
    if (tab == null || (n = tab.length) == 0)
        // 不处理空的哈希表
        return;
    // // 第 1 部分: 处理链表结构
    // 通过被删除的key的哈希值查找桶(红黑树)的位置
    int index = (n - 1) & hash;
    TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
    // succ: 当前节点(被删除节点)的下一个节点
    // pred: 当前节点(被删除节点)的上一个节点
    TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
    // 从链表结构上删除当前节点(处理上一个节点的 next 指针与下一个节点的 prev 指针)
    if (pred == null)
        // 上一个节点为空,说明是要删除的是根节点,将红黑树的根节点索引改为当前节点的下一个节点
        tab[index] = first = succ;
    else
        // 删除的不是根节点,就把当前节点的上一个节点的next指向当前节点的下一个节点
        pred.next = succ;
    if (succ != null)
        // 当前节点的下一个节点不为空,就把下一个节点的prev指向当前节点的上一个节点
        succ.prev = pred;
    // 删除节点完毕后,红黑树为空,直接返回
    if (first == null)
        return;
    // 更新root指针,并判断是否需要将树转为链表结构
    if (root.parent != null)
        root = root.root();
    if (root == null
        || (movable
            && (root.right == null
                || (rl = root.left) == null
                || rl.left == null))) {
        tab[index] = first.untreeify(map);  // too small
        return;
    }
    // 第 2 部分: 处理树结构
    // p: 被删除的节点; replacement: 后继节点(删除节点p后,如果p有子节点,p的子节点上移继承其位置)
	// pl: 被删除节点的左儿子; pr: 被删除节点的右儿子
    TreeNode<K,V> p = this, pl = left, pr = right, replacement;
    // 查找被删除节点的后继节点,分以下几种情况
    // ⑴ 被删除节点有左子树和右子树
    if (pl != null && pr != null) {
        TreeNode<K,V> s = pr, sl;
        // 1.查找右子树最左叶子节点s与待删除节点p进行位置互换
        while ((sl = s.left) != null) // find successor
            s = sl;
        // 2.交换最左叶子节点和待删除节点的颜色
        boolean c = s.red; s.red = p.red; p.red = c; // swap colors
        // sr:最左叶子节点的右儿子
        TreeNode<K,V> sr = s.right;
        // pp:被删除节点的父节点
        TreeNode<K,V> pp = p.parent;
        // 3.交换被删除节点p和最左叶子节点s的位置
		// 判断最左叶子节点是否是被删除节点的右儿子(即右子树是否只有一个节点)分别处理节点s.right和p.parent的引用
        if (s == pr) { // p was s's direct parent
            p.parent = s;
            s.right = p;
        }
        else {
            TreeNode<K,V> sp = s.parent;
            if ((p.parent = sp) != null) {
                if (s == sp.left)
                    sp.left = p;
                else
                    sp.right = p;
            }
            if ((s.right = pr) != null)
                pr.parent = s;
        }
        p.left = null;
        // 处理p.right和sr.parent的引用
        if ((p.right = sr) != null)
            sr.parent = p;
        // 处理s.left和pl.parent的引用
        if ((s.left = pl) != null)
            pl.parent = s;
        // 处理s.parent的引用和pp.left或pp.right
        if ((s.parent = pp) == null)
            root = s;
        else if (p == pp.left)
            pp.left = s;
        else
            pp.right = s;
        // 4.交换最左叶子节点和被删除节点的位置完成
		// 此时被删除节点p在原最左叶子节点的位置,现在被删除节点p没有左子树,如果有右子树,那么右儿子sr就是后继节点,否则后继节点指向自身
        if (sr != null)
            replacement = sr;
        else
            replacement = p;
    }
    // ⑵ 被删除节点只有左子树,后继节点就是左儿子
    else if (pl != null)
        replacement = pl;
    // ⑶ 被删除节点只有右子树,后继节点就是右儿子
    else if (pr != null)
        replacement = pr;
    // ⑷ 被删除节点没有子树,那么后继节点就指向自身
    else
        replacement = p;
    // 已经找到删除节点后的后继节点,这一步将从树中彻底删除节点p。
    if (replacement != p) {
        // 1.修改替代节点的父节点引用
        TreeNode<K,V> pp = replacement.parent = p.parent;
        // 2.将被删除节点的父节点对其的引用进行修改
        if (pp == null)
            root = replacement;
        else if (p == pp.left)
            pp.left = replacement;
        else
            pp.right = replacement;
        // 3.彻底删除节点p
        p.left = p.right = p.parent = null;
    }
	// 删除节点完成,删除的是红色节点,不需要平衡;否则,平衡
    TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
	// 若没有后继节点,直接删除节点p
    if (replacement == p) {  // detach
        TreeNode<K,V> pp = p.parent;
        p.parent = null;
        if (pp != null) {
            if (p == pp.left)
                pp.left = null;
            else if (p == pp.right)
                pp.right = null;
        }
    }
    if (movable)
        // 确保节点r是树根
        moveRootToFront(tab, r);
}

红黑树的平衡删除

删除红黑树节点之后的平衡代码如下,笔者认为,应当重点关注红黑树的变色、旋转逻辑。

/**
 * 平衡红黑树-当删除后
 * x: 影响平衡的点(俗称:当前节点)
*/
static <K, V> TreeNode<K, V> balanceDeletion(TreeNode<K, V> root,
                                             TreeNode<K, V> x) {
    // xp: 当前节点的父节点
	// xpl: 当前节点的父节点的左儿子
	// xpr: 当前节点的父节点的右儿子
    for (TreeNode<K, V> xp, xpl, xpr; ; ) {
        // 当前节点是null或者是根节点,不改变红黑树的结构,不需要改变
        if (x == null || x == root)
            return root;
        // 当前节点成为根节点,置为黑色
        else if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        } 
        // 当前节点为红色,改为黑色后,不影响路径上黑色的数量,不需要改变
        else if (x.red) {
            x.red = false;
            return root;
        }
        // 当前节点为父节点的左儿子
        else if ((xpl = xp.left) == x) {
            // 如果兄弟节点是红色,那么父节点一定是黑色
            if ((xpr = xp.right) != null && xpr.red) {
                // 1.兄弟节点置为黑色
                xpr.red = false;
                // 2.父节点置为红色
                xp.red = true;
                // 3.以父节点为支点左旋
                root = rotateLeft(root, xp);
                // 4.刷新兄弟节点
                xpr = (xp = x.parent) == null ? null : xp.right;
            }
            // 如果兄弟节点为空,将当前节点向上调整为父节点,继续循环
            if (xpr == null)
                x = xp;
            else {
                // sl: 兄弟节点的左儿子
				// sr: 兄弟节点的右儿子
                TreeNode<K, V> sl = xpr.left, sr = xpr.right;
                if ((sr == null || !sr.red) &&
                    (sl == null || !sl.red)) {
                    // 如果兄弟节点没有红色孩子,则兄弟节点置为红色
                    xpr.red = true;
                    // 本轮结束,将当前节点向上调整为父节点,继续循环
                    x = xp;
                } else {
                    if (sr == null || !sr.red) {
                        // 如果兄弟节点的左儿子是红色就改为黑色
                        if (sl != null)
                            sl.red = false;
                        // 并将兄弟节点置为红色
                        xpr.red = true;
                        // 以兄弟节点为支点右旋
                        root = rotateRight(root, xpr);
                        // 刷新兄弟节点
                        xpr = (xp = x.parent) == null ?
                            null : xp.right;
                    }
                    if (xpr != null) {
                        // 将兄弟节点的颜色染成和父节点一样
                        xpr.red = (xp == null) ? false : xp.red;
                        // 将兄弟节点的右儿子染成黑色,防止出现两个红色节点相连
                        if ((sr = xpr.right) != null)
                            sr.red = false;
                    }
                    if (xp != null) {
                        // 父节点置为黑色,并对其左旋,这样就能保证被删除的节点所在的路径又多了一个黑色节点,从而达到恢复平衡的目的
                        xp.red = false;
                        root = rotateLeft(root, xp);
                    }
                    // 调整完毕,下一次循环直接退出
                    x = root;
                }
            }
        } 
        // 当前节点为父节点的右儿子,同上
        else { // symmetric
            if (xpl != null && xpl.red) {
                xpl.red = false;
                xp.red = true;
                root = rotateRight(root, xp);
                xpl = (xp = x.parent) == null ? null : xp.left;
            }
            if (xpl == null)
                x = xp;
            else {
                TreeNode<K, V> sl = xpl.left, sr = xpl.right;
                if ((sl == null || !sl.red) &&
                    (sr == null || !sr.red)) {
                    xpl.red = true;
                    x = xp;
                } else {
                    if (sl == null || !sl.red) {
                        if (sr != null)
                            sr.red = false;
                        xpl.red = true;
                        root = rotateLeft(root, xpl);
                        xpl = (xp = x.parent) == null ?
                            null : xp.left;
                    }
                    if (xpl != null) {
                        xpl.red = (xp == null) ? false : xp.red;
                        if ((sl = xpl.left) != null)
                            sl.red = false;
                    }
                    if (xp != null) {
                        xp.red = false;
                        root = rotateRight(root, xp);
                    }
                    x = root;
                }
            }
        }
    }
}

红黑树在线演示网站

HashMap常见面试题

HashMap的底层数据结构?

jdk1.7中为数组+链表,jdk1.8中为数组+链表+红黑树

HashMap的存取原理?

通过获取key对象的hashcode计算出该对象的哈希值,通过改哈希值与数组长度减去1进行位与运算(n-1 & hash),得到buckets 的位置,当发生hash冲突时,如果value值一样,则会替换旧的key的value,value不一样则新建链表结点,当链表的长度超过8,则转换为红黑树存储。

img

Java7和Java8的区别?

在jdk1.8之前创建该对象,会创建一个长度为16的Entry[] table用来存储键值对数据。jdk1.8之后不是在构造方法创建了,而是在第一次调用put方法时才进行创建,创建Node[] table.

Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。

为啥会线程不安全?

Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。

Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系。

但是即使不会出现死循环,但是通过源码看到put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。

HashMap会进行resize(扩容)操作,重新计算hash值,在resize操作的时候会造成线程不安全。下面将举两个可能出现线程不安全的地方。

  1. put的时候导致的多线程数据不一致。
    这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
  2. 另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%),即产生链表循环引用的现象

有什么线程安全的类代替么?

currentHashMap 以及 hashTable

默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?

默认的初始化大小是16 原因是这样的,如果桶初始化桶数组设置太大,就会浪费内存空间,16是一个折中的大小,既不会像1,2,3那样放几个元素就扩容,也不会像几千几万那样可以只会利用一点点空间从而造成大量的浪费。

大小为2的幂是,在计算buckets桶位置的时候,公式为((n-1) & hash),2的幂减去1的数的二进制数的结尾都是1,与hash值进行与运算,会得到其余数。进行按位与操作,使得结果剩下的值为对象的hash值的末尾几位,这样就我们只要保证对象的hash值生成足够散列即可

使存储高效,尽量减少碰撞,在((n-1)&hash) 求索引的时候更均匀

数组长度是2的n次幂时

img

数组长度 不是2的n次幂时

img

HashMap的扩容方式?负载因子是多少?为什是这么多?

加载因子设置为0.75而不是1,是因为设置过大,桶中键值对碰撞的几率就会越大,同一个桶位置可能会存放好几个value值,这样就会增加搜索的时间,性能下降,设置过小也不合适,如果是0.1,那么10个桶,threshold为1,你放两个键值对就要扩容,太浪费空间了。

HashMap的主要参数都有哪些?

	//默认的map大小,为2的n次幂
    static final int DEFAULT_INITIAL_CAPACITY(默认初始容量) = 1 << 4; // aka 16

   // 最大容量,指定的值超过 2的30次幂,默认使用这个值
    static final int MAXIMUM_CAPACITY (最大容量)= 1 << 30;

   //在构造函数中没有指定时使用的负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

   //当链表长度为8时,使用以红黑树代替链表,红黑树的结点是链表长度的两倍,当比较短的时候,使用红黑树的效率其实并不高,根据泊松分布公式的统计结果,在结点数达到8时,适合使用红黑树
    static final int TREEIFY_THRESHOLD(树化的阈值) = 8;
  
	// 红黑树转为链表的阈值
    static final int UNTREEIFY_THRESHOLD (树降级为链表的阈值)= 6;

	//链表转红黑树时数组的大小的阈值,即数组大小大于这个数字时且链表长度大于8才会转为红黑树,
	//在数组长度小于64,不会转,而是进行扩容
    static final int MIN_TREEIFY_CAPACITY = 64;

HashMap是怎么处理hash碰撞的?

如果key相同,则会替换旧值,key不相同,则接到后面的链表,如果链表长度到达8且数组的长度大于64时,则将链表转为红黑树,如果数组长度小于64,则是进行扩容

hash的计算规则?

将对象的hashcode()方法返回的hash值,进行无符号的右移16位,并与原来的hash值进行按位异或操作,目的是将hash的低16bit和高16bit做了一个异或,使返回的值足够散列

在get和put的过程中,计算下标时,先对hashCode进行hash操作,然后再通过hash值进一步计算下标,如下图所示:

img

如何解决初始化,输入的值不是2的n次幂

	static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

目的:将值的二进制数,从左到右的第一个出现的1开始,右边的所有值都变成1,使得可以找出比当前值大一点点的2的n次幂的数

当执行 n >>> 16 时,即意味着将32位数都进行了一次按位或运算,将

img

二进制运算符

一、二进制

二进制(binary)在数学和数字电路中指以2为基数的记数系统
计算机的二进制中用 0表示正数 1表示负数

十六进制:有一个前缀0X或0x
八进制:前缀是0,最好不用,容易混淆
二进制:前缀是0b或0B

二进制最高位是符号位:0表示正数 1表示负数

正数的原码,反码,补码都是一样

负数的反码 = 原码符号位(也就是最高位)不变,其他位取反(0—变—1 )( 1—变—0)

负数的补码 = **反码 **+ 1(加的是1的原码00000001)

二进制的计算是满二进一

0的反码,补码都是0

java没有无符号的数,换而言之,Java中的数都是有符号的

二进制使用补码进行运算

位            单位           换算案例
字节          byte         1 byte = 8 bit
千字节         KB          1 KB = 1024 byte
兆            MB           1 MB = 1024 KB
G             GB          1 GB = 1024 MB
太字节         TB          1 TB = 1024 GB

二、移位运算符

1、左移运算符(<<)

语法: 符号位不变,低位补0

1<<2     相当与22次方
00000001 变成   00000100    

特殊情况:若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。
例:a = a << 2 将a的二进制位左移2位,右补0,
左移1位后a = a * 2;

2、右移运算符(>>)

语法: 将低位溢出,符号位不变,并用符号位补溢出高位

-1>>2  //-1

/**
 * -1原码:1000 0001
 * -1反码:1111 1110
 * -1补码:1111 1111
 * -1>>2的补码:1111 1111
 * 反码:1111 1110
 * 原码:1000 0001    
 */
3、无符号右移运算符(>>>)

语法: 低位溢出,符号位不变,高位补0

3>>>2
//3的补码:	 00000 0011
//3>>>2补码:0000 0000

//结果;0

三、按位运算符

1、&(”与“)

语法: 全1才为1
运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;

“与运算”的特殊用途:

1、清零。如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。

2、取一个数中指定位 ,方法:找一个数,对应X要取的位,该数的对应位为1,其余位为零,此数与X进行“与运算”可以得到X中的指位。

设X=10101110,
取X的低4位,用 X & 0000 1111 = 0000 1110 即可得到;
还可用来取X的246位。
2、|(”或“)

语法: 两个位只要有一个为1,那么结果就是1,否则就为0
运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1;

“或运算”特殊作用:

常用来对一个数据的某些位置1。方法:找到一个数,对应X要置1的位,该数的对应位为1,其余位为零。此数与X相或可使X中的某些置位的1。

例:将X=10100000的低4位置1 ,用 X | 0000 1111 = 1010 1111即可得到。
3、~(”非“)

语法: 如果位为0,结果是1,如果位为1,结果是0,会改变符号数
运算规则:~1=0; ~0=1;

4、^(”异或“)

语法: 相同则结果为0,不同则结果为1。
运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0;

异或运算”的特殊作用:
1、使特定位翻转 找一个数,对应X要翻转的各位,该数的对应位为1,其余位为零,此数与X对应位异或即可。

X=1010 1110,使X低4位翻转,用X ^ 0000 1111 = 1010 0001即可得到。

2、与0相异或,保留原值 。

X ^ 0000 0000 = 1010 1110

四、运算案例

// -8 + 5
// -8 的 源码 1000 1000 怎么来的? 8的源码是这样子的 0000 1000 但是这个是-8 所以最高位为 1为负数,所以-8的源码为 1000 1000
// -8 的 反码 1111 0111 源码的最高位不变,其他位则是源码的逐位取反
// -8 的 补码 1111 1000 反码 + 1 也就是 1111 0111 + 0000 0001 = 1111 1000 (满二进一)
// 5 因为是一个正数所以他的 源码、反码、补码都是一样的 0000 0101
// 最终的补码就是 1111 1000 + 0000 0101 = 1111 1101
// 最终的反码就是 1111 1101 - 0000 0001 = 1111 1100
// 最终的源码就是 1111 1100 ====> 1000 0011 = -3
// -10 >> 5
// --------- 源码 ------------------------------------------------
// 1000 0000 0000 1010    -10
// --------------- 反码(除掉高位其他位取相反) ------------------------
// 1111 1111 1111 0101
// --------- 补码(反码 + 1) ---------------------------------------
// 1111 1111 1111 0111
// ---------- 补码右移五位(1 0111舍弃,使用高位补溢出位,也就是1) ---------
// 1111 1111 1111 1111
// ----------- 反码(补码 - 1) -------------------------------------
// 1111 1111 1111 1110
// -------- 源码(除掉高位其他位取相反) -------------------------------
// 1000 0000 0000 0001
// ---------结果(-1)-----------------------------------------------
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值