zoukankan      html  css  js  c++  java
  • java源码解析

    String深入解析

    String具有不变性的原因:

    1. String被final修饰,它不可能被继承,也就是任何对String的操作方法,都不会被继承覆写
    2. String中保存数据的是一个char数组的value,它被final修饰,它的内存地址一旦赋值无法修改
    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
    }    
    

    String相等判断源码

    public boolean equals(Object anObject) {
        // 判断内存地址是否相同
        if (this == anObject) {
            return true;
        }
        // 待比较的对象是否是 String,如果不是 String,直接返回不相等
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            // 两个字符串的长度是否相等,不等则直接返回不相等
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                // 依次比较每个字符是否相等,若有一个不等,直接返回不相等
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
    
    1. 判断类型是否相同
    2. 判断长度是否相同
    3. 比较每一个字符是否相等
      当上述3个条件均成立时,两个String相等

    String的replace和replaceAll区别在于前者入参可以是char也可以是String,此外replaceAll是支持正则表达式的,效率上replace高效

    Long

    Long自己实现了一种缓存机制,缓存了-128到127内的所有Long,如果值在Long,则从缓存中取,不会被初始化
    Long中的静态内部类实现了LongCache

    private static class LongCache {
        private LongCache(){}
        // 缓存,范围从 -128 到 127,+1 是因为有个 0
        static final Long cache[] = new Long[-(-128) + 127 + 1];
    
        // 容器初始化时,进行加载
        static {
            // 缓存 Long 值,注意这里是 i - 128 ,所以再拿的时候就需要 + 128
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Long(i - 128);
        }
    }
    

    使用Long时,推荐valueOf,少用parseLong方法,因为前者如果缓存命中从缓存取值,而后者不会

    static

    static修饰的方法时,要注意线程安全,static修饰的工具类方法要避免使用到方法外部的公共变量。工具类方法建议使用public final static

    Arrays

    Arrays二分查询方法能快速定位元素

    其二分查找底层如下:

    // a:我们要搜索的数组,fromIndex:从那里开始搜索,默认是0; toIndex:搜索到何时停止,默认是数组大小
    // key:我们需要搜索的值 c:外部比较器
    private static <T> int binarySearch0(T[] a, int fromIndex, int toIndex,
                                         T key, Comparator<? super T> c) {
        // 如果比较器 c 是空的,直接使用 key 的 Comparable.compareTo 方法进行排序
        // 假设 key 类型是 String 类型,String 默认实现了 Comparable 接口,就可以直接使用 compareTo 方法进行排序
        if (c == null) {
            // 这是另外一个方法,使用内部排序器进行比较的方法
            return binarySearch0(a, fromIndex, toIndex, key);
        }
        int low = fromIndex;
        int high = toIndex - 1;
        // 开始位置小于结束位置,就会一直循环搜索
        while (low <= high) {
            // 假设 low =0,high =10,那么 mid 就是 5,所以说二分的意思主要在这里,每次都是计算索引的中间值
            int mid = (low + high) >>> 1;
            T midVal = a[mid];
            // 比较数组中间值和给定的值的大小关系
            int cmp = c.compare(midVal, key);
            // 如果数组中间值小于给定的值,说明我们要找的值在中间值的右边
            if (cmp < 0)
                low = mid + 1;
            // 我们要找的值在中间值的左边
            else if (cmp > 0)
                high = mid - 1;
            else
            // 找到了
                return mid; // key found
        }
        // 返回的值是负数,表示没有找到
        return -(low + 1);  // key not found.
    }
    

    Arrays提供了很多parallel开头的方法,比如parallelSort方法,方法底层有判断,只有数据量大于8192时,才会真正走并行的实现。

    Collections

    Colletcions.max

    ArrayList

    ArrayList无参构造器初始化时,默认大小是空数组,当第1次add的时候数组扩容到大小为10。如果第一次初始化如果大小<10,那么初始化大小按10计算
    扩容后的大小是原来的1.5倍

    扩容源码如下

    private void ensureCapacityInternal(int minCapacity) {
      //如果初始化数组大小时,有给定初始值,以给定的大小为准,不走 if 逻辑
      if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
      }
      //确保容积足够
      ensureExplicitCapacity(minCapacity);
    }
    private void ensureExplicitCapacity(int minCapacity) {
      //记录数组被修改
      modCount++;
      // 如果我们期望的最小容量大于目前数组的长度,那么就扩容
      if (minCapacity - elementData.length > 0)
        grow(minCapacity);
    }
    //扩容,并把现有数据拷贝到新的数组里面去
    private void grow(int minCapacity) {
      int oldCapacity = elementData.length;
      // oldCapacity >> 1 是把 oldCapacity 除以 2 的意思
      int newCapacity = oldCapacity + (oldCapacity >> 1);
    
      // 如果扩容后的值 < 我们的期望值,扩容后的值就等于我们的期望值
      if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
      // 如果扩容后的值 > jvm 所能分配的数组的最大值,那么就用 Integer 的最大值
      if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
     
      // 通过复制进行扩容
      elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

    ArrayList无参构造器构造,现在add一个值进去,此处的数组大小是1,下一次扩容前最大可用大小是10。

    数组初始化,初加入一个值后,如果使用addAll方法,一下子加入15个值,那么最终数组的大小是16。因为

    // newCapacity 本次扩容的大小,minCapacity 我们期望的数组最小大小
    // 如果扩容后的值 < 我们的期望值,我们的期望值就等于本次扩容的大小
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    

    toArray方法

    public static void main(String[] args) {
            List<Integer> list = new ArrayList<>();
            list.add(1);
            list.add(2);
            list.add(3);
            Integer[] a1 = new Integer[3];
            Integer[] a2 = list.toArray(a1);
            System.out.println(a1.hashCode());
            System.out.println(a2.hashCode());
    
            System.out.println("================================");
    
            Integer[] a3 = new Integer[2];
            Integer[] a4 = list.toArray(a3);
            System.out.println(a3.hashCode());
            System.out.println(a4.hashCode());
        }
    


    可以发现toArray方法,如果入参数组大小小于集合大小,那么它就会返回一个新的数组,否则返回则就是入参数组

    LinkedList

    从头节点增加

    // 从头部追加
    private void linkFirst(E e) {
        // 头节点赋值给临时变量
        final Node<E> f = first;
        // 新建节点,前一个节点指向null,e 是新建节点,f 是新建节点的下一个节点,目前值是头节点的值
        final Node<E> newNode = new Node<>(null, e, f);
        // 新建节点成为头节点
        first = newNode;
        // 头节点为空,就是链表为空,头尾节点是一个节点
        if (f == null)
            last = newNode;
        //上一个头节点的前一个节点指向当前节点
        else
            f.prev = newNode;
        size++;
        modCount++;
    }
    

    从头节点删除

    //从头删除节点 f 是链表头节点
    private E unlinkFirst(Node<E> f) {
        // 拿出头节点的值,作为方法的返回值
        final E element = f.item;
        // 拿出头节点的下一个节点
        final Node<E> next = f.next;
        //帮助 GC 回收头节点
        f.item = null;
        f.next = null;
        // 头节点的下一个节点成为头节点
        first = next;
        //如果 next 为空,表明链表为空
        if (next == null)
            last = null;
        //链表不为空,头节点的前一个节点指向 null
        else
            next.prev = null;
        //修改链表大小和版本
        size--;
        modCount++;
        return element;
    }
    

    节点查询,如果 index 处于队列的前半部分,从头开始找,size >> 1 是 size 除以 2 的意思。否则后半部分查询

    // 根据链表索引位置查询节点
    Node<E> node(int index) {
        // 如果 index 处于队列的前半部分,从头开始找,size >> 1 是 size 除以 2 的意思。
        if (index < (size >> 1)) {
            Node<E> x = first;
            // 直到 for 循环到 index 的前一个 node 停止
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {// 如果 index 处于队列的后半部分,从尾开始找
            Node<E> x = last;
            // 直到 for 循环到 index 的后一个 node 停止
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
    

    LinkedList实现了Queue接口,在链表为空时返回值也不太一样

    因为LinkedList实现双向的迭代方向,新增了一个迭代接口:ListIterator

    ArrayList有最大容量,为Integer的最大值,LinkedList底层是双向链表,理论可无限大,但源码中LinkedList实际大小用int类型,这也说明了LinkedList不能超过Integer的最大值。

    HashMap

    底层数据结构是:数组+链表+红黑树。其中当链表的长度大于等于8时,链表会转化成红黑树,当红黑树大小小于等于6时,红黑树转化成链表。
    HashMap允许null值。

    //初始容量为 16
     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    
     //最大容量
     static final int MAXIMUM_CAPACITY = 1 << 30;
    
     //负载因子默认值
     static final float DEFAULT_LOAD_FACTOR = 0.75f;
     
     //桶上的链表长度大于等于8时,链表转化成红黑树
     static final int TREEIFY_THRESHOLD = 8;
    
     //桶上的红黑树大小小于等于6时,红黑树转化成链表
     static final int UNTREEIFY_THRESHOLD = 6;
    
     //当数组容量大于 64 时,链表才会转化成红黑树
     static final int MIN_TREEIFY_CAPACITY = 64;
    
     //记录迭代过程中 HashMap 结构是否发生变化,如果有变化,迭代时会 fail-fast
     transient int modCount;
    
     //HashMap 的实际大小,可能不准(因为当你拿到这个值的时候,可能又发生了变化)
     transient int size;
    
     //存放数据的数组
     transient Node<K,V>[] table;
    
     // 扩容的门槛,有两种情况
     // 如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方,比如你给定初始化大小 19,实际上初始化大小为 32,为 2 的 5 次方。
     // 如果是通过 resize 方法进行扩容,大小 = 数组容量 * 0.75
     int threshold;
    
     //链表的节点
     static class Node<K,V> implements Map.Entry<K,V> {
     
     //红黑树的节点
     static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    key 在数组中的位置公式:tab[(n - 1) & hash]
    

    计算hash值时,为什么需要右移16位
    hash算法是h^(h>>>16),这是为了h的高低16位都能参与计算,计算出的hash更分散

    为什么把数组长度取模操作换成了&操作
    取模操作处理器计算慢,处理器对&操作比较擅长

    为什么提倡数组大小是2的幂次方
    因为只有大小是2的幂次方,才能使hash值%n(数组大小)==(n-1)&hash

    hashmap的添加元素

    // 入参 hash:通过 hash 算法计算出来的值。
    // 入参 onlyIfAbsent:false 表示即使 key 已经存在了,仍然会用新值覆盖原来的值,默认为 false
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // n 表示数组的长度,i 为数组索引下标,p 为 i 下标位置的 Node 值
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果数组为空,使用 resize 方法初始化
        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);
        // 如果当前索引位置有值的处理方法,即我们常说的如何解决 hash 冲突
        else {
            // e 当前节点的临时变量
            Node<K,V> e; K k;
            // 如果 key 的 hash 和值都相等,直接把当前下标位置的 Node 值赋值给临时变量
            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) {
                    // e = p.next 表示从头开始,遍历链表
                    // p.next == null 表明 p 是链表的尾节点
                    if ((e = p.next) == null) {
                        // 把新节点放到链表的尾部 
                        p.next = newNode(hash, key, value, null);
                        // 当链表的长度大于等于 8 时,链表转红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 链表遍历过程中,发现有元素和新增的元素相等,结束循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //更改循环的当前元素,使 p 在遍历过程中,一直往后移动。
                    p = e;
                }
            }
            // 说明新节点的新增位置已经找到了
            if (e != null) {
                V oldValue = e.value;
                // 当 onlyIfAbsent 为 false 时,才会覆盖值 
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                // 返回老值
                return oldValue;
            }
        }
        // 记录 HashMap 的数据结构发生了变化
        ++modCount;
        //如果 HashMap 的实际大小大于扩容的门槛,开始扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

    HashMap、TreeMap、LinkedHashMap三者异同
    相同点:

    1. 在特定情况下,都使用红黑树
    2. 底层的hash算法相同
    3. 在迭代过程中,如果Map的数据结果被改动,都会报ConcurrentModificationException
      不同点:
    4. HashMap以数组为主,查询快,TreeMap以红黑树为主,利用红黑树特点进行排序,LinkedHashMap在HashMap上增加链表结构,实现顺序插入和最少访问删除
    5. 三者使用场景不同
    6. api上层略有不同

    HashSet

    它底层基于HashMap实现,key为HashMap的key,value为
    private static final Object PRESENT = new Object();

    // 对 HashMap 的容量进行了计算
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    

    HashSet的初始大小为取最大值(期望的值 / 0.75 + 1,默认值 16),expectSize * 0.75 + 1刚好不用扩容,如果对HashMap的初始化大小值选取,可借鉴此公式。

    CopyOnWriteArrayList

    CopyOnWriteArrayList通过锁+数组拷贝+volatile保证线程安全,它线程安全的原因是每次操作都是在新拷贝数组上操作,当操作完成后,再把新数组引用赋值给原数组。当用迭代器操作时,如果有另一线程对CopyOnWriteArrayList进行增删操作,迭代器未结束时持有的仍是旧数组,所以它是线程安全的。

    注意:volatile直接修饰数组时,当数组某一元素改变仍无法保证其可见性,只有当数组引用发生改变时才保证其可见性,这就是为什么源码setArray方法是修改数组的引用

    CopyOnWriteArrayList的add方法

    // 添加元素到数组尾部
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        // 加锁
        lock.lock();
        try {
            // 得到所有的原数组
            Object[] elements = getArray();
            int len = elements.length;
            // 拷贝到新数组里面,新数组的长度是 + 1 的,因为新增会多一个元素
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            // 在新数组中进行赋值,新元素直接放在数组的尾部
            newElements[len] = e;
            // 替换掉原来的数组
            setArray(newElements);
            return true;
        // finally 里面释放锁,保证即使 try 发生了异常,仍然能够释放锁   
        } finally {
            lock.unlock();
        }
    }
    

    批量删除

    // 批量删除包含在 c 中的元素
    public boolean removeAll(Collection<?> c) {
        if (c == null) throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            // 说明数组有值,数组无值直接返回 false
            if (len != 0) {
                // newlen 表示新数组的索引位置,新数组中存在不包含在 c 中的元素
                int newlen = 0;
                Object[] temp = new Object[len];
                // 循环,把不包含在 c 里面的元素,放到新数组中
                for (int i = 0; i < len; ++i) {
                    Object element = elements[i];
                    // 不包含在 c 中的元素,从 0 开始放到新数组中
                    if (!c.contains(element))
                        temp[newlen++] = element;
                }
                // 拷贝新数组,变相的删除了不包含在 c 中的元素
                if (newlen != len) {
                    setArray(Arrays.copyOf(temp, newlen));
                    return true;
                }
            }
            return false;
        } finally {
            lock.unlock();
        }
    }
    

    源码思想借鉴:批量删除是把先找出已存在的元素组成数组,然后再把原数组引用指向新数组。避免直接循环单个元素删除造成频繁数组拷贝

    ConcurrentHashMap

    添加元素

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //计算hash
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //table是空的,进行初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果当前索引位置没有值,直接创建
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //cas 在 i 位置创建新的元素,当 i 位置是空时,即能创建成功,结束for自循,否则继续自旋
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //如果当前槽点是转移节点,表示该槽点正在扩容,就会一直等待扩容完成
            //转移节点的 hash 值是固定的,都是 MOVED
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            //槽点上有值的
            else {
                V oldVal = null;
                //锁定当前槽点,其余线程不能操作,保证了安全
                synchronized (f) {
                    //这里再次判断 i 索引位置的数据没有被修改
                    //binCount 被赋值的话,说明走到了修改表的过程里面
                    if (tabAt(tab, i) == f) {
                        //链表
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //值有的话,直接返回
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                //把新增的元素赋值到链表的最后,退出自旋
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //红黑树,这里没有使用 TreeNode,使用的是 TreeBin,TreeNode 只是红黑树的一个节点
                        //TreeBin 持有红黑树的引用,并且会对其加锁,保证其操作的线程安全
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            //满足if的话,把老的值给oldVal
                            //在putTreeVal方法里面,在给红黑树重新着色旋转的时候
                            //会锁住红黑树的根节点
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                //binCount不为空,并且 oldVal 有值的情况,说明已经新增成功了
                if (binCount != 0) {
                    // 链表是否需要转化成红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    //这一步几乎走不到。槽点已经上锁,只有在红黑树或者链表新增失败的时候
                    //才会走到这里,这两者新增都是自旋的,几乎不会失败
                    break;
                }
            }
        }
        //check 容器是否需要扩容,如果需要去扩容,调用 transfer 方法去扩容
        //如果已经在扩容中了,check有无完成
        addCount(1L, binCount);
        return null;
    }
    

    LinkedBlockingQueue

    put方法

    // 把e新增到队列的尾部。
    // 如果有可以新增的空间的话,直接新增成功,否则当前线程陷入等待
    public void put(E e) throws InterruptedException {
        // e 为空,抛出异常
        if (e == null) throw new NullPointerException();
        // 预先设置 c 为 -1,约定负数为新增失败
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        // 设置可中断锁
        putLock.lockInterruptibly();
        try {
            // 队列满了
            // 当前线程阻塞,等待其他线程的唤醒(其他线程 take 成功后就会唤醒此处被阻塞的线程)
            while (count.get() == capacity) {
                // await 无限等待
                notFull.await();
            }
    
            // 队列没有满,直接新增到队列的尾部
            enqueue(node);
    
            // 新增计数赋值,注意这里 getAndIncrement 返回的是旧值
            // 这里的 c 是比真实的 count 小 1 的
            c = count.getAndIncrement();
    
            // 如果链表现在的大小 小于链表的容量,说明队列未满
            // 可以尝试唤醒一个 put 的等待线程
            if (c + 1 < capacity)
                notFull.signal();
    
        } finally {
            // 释放锁
            putLock.unlock();
        }
        // c==0,代表队列里面有一个元素
        // 会尝试唤醒一个take的等待线程
        if (c == 0)
            signalNotEmpty();
    }
    // 入队,把新元素放到队尾
    private void enqueue(Node<E> node) {
        last = last.next = node;
    }
    
  • 相关阅读:
    Java8系列之重新认识HashMap(转)
    kafka 消费者和生产者测试类
    zookeeper配置
    JNLP应用程序无法打开的解决办法
    Functional Programming 资料收集
    Python的问题解决: IOError: [Errno 32] Broken pipe
    python的subprocess的简单使用和注意事项
    Python tricks(7) -- new-style class的__slots__属性
    Python tricks(6) -- python代码执行的效率
    Python tricks(5) -- string和integer的comparison操作
  • 原文地址:https://www.cnblogs.com/shuangyueliao/p/11456677.html
Copyright © 2011-2022 走看看