• java中“冷门”工具类的总结


    前言

    最近挖掘了一些在项目中不常用的工具类,有些实用性还是很高的,特此总结一下。
    另外又顺便看了一下常用并发集合的相关知识,也在此自我总结一下。

    一些不常用的工具类

    不可变集合

    不可变集合包括 ImmutableList, ImmutableMap,ImmutableSet,ImmutableSortedSet等,当其创建之后就不会发生变化,可以在一些只读的场景来使用他们,减少空间的浪费。

        public static void main(String[] args) {
            //初始化一个不可变集合
            ImmutableList<String> immutableList = ImmutableList.of("12");
            List<String> list = new ArrayList<String>() {{
                add("321");
            }};
            //copy 一个list
            ImmutableList<String> immutableList1 = ImmutableList.copyOf(list);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    因其是不可变的,所以无法进行add等操作。
    在这里插入图片描述

    多值Map

    Map是一个key对应一个value,当key重复时,再进行put则会进行覆盖。但有些场景需要一个key对应多个value,比如一个人可以有多个职位,key是人的id,value是人的职位id,这种类似的场景可以使用多值Map。

       public static void main(String[] args) {
            ArrayListMultimap<String, String> multiMap = ArrayListMultimap.create();
            multiMap.put("adam","teacher");
            multiMap.put("adam","player");
            multiMap.put("adam","player");
            System.out.println(multiMap.get("adam"));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    以下是输出的结果,可以看到,返回的是一个List,并且value值重复也并不会被过滤掉。

    [teacher, player, player]
    
    • 1

    Table表

    Map是通过一个key来决定value,但某些场景我们想通过两个key来决定value,比如通过经度、纬度才能确定当前的位置。

     public static void main(String[] args) {
            HashBasedTable<String, String, String> table = HashBasedTable.create();
            //通过经纬度来确定当前位置
            table.put("12", "25", "position");
            table.put("12", "26", "position");
            table.put("21", "25", "position1");
            //根据第一个key来获取value
            Collection<String> values0 = table.row("12").values();
            System.out.println(values0);
            //根据两个key来获取value
            String s = table.get("12", "25");
            System.out.println(s);
            //获取table中的所有值
            Collection<String> values1 = table.values();
            System.out.println(values1);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    打印结果如下:

    [position0, position1]
    position0
    [position0, position1, position2]
    
    • 1
    • 2
    • 3

    Lists、Maps、Sets

    Lists的简单使用

      public static void main(String[] args) {
            List<String> list = new ArrayList<String>() {{
                add("123");
                add("1234");
                add("12345");
            }};
            //分页操作
            Lists.partition(list,1).forEach(System.out::println);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    打印结果:

    [123]
    [1234]
    [12345]
    
    • 1
    • 2
    • 3

    Sets简单使用

      public static void main(String[] args) {
            Set<String> set1 = new HashSet<String>() {{
                add("1");
                add("2");
            }};
    
            Set<String> set2 = new HashSet<String>() {{
                add("1");
            }};
    
            //找出不同
            Sets.SetView<String> difference = Sets.difference(set1, set2);
            System.out.println(difference);
    
            //找出相同
            Sets.SetView<String> intersection = Sets.intersection(set1, set2);
            System.out.println(intersection);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    打印结果:

    [2]
    [1]
    
    • 1
    • 2

    Maps的简单使用

      public static void main(String[] args) {
            Map<String,String> map = new HashMap<String,String>(){{
                put("123", "20");
                put("1234", "201");
                put("12345", "202");
            }};
    
            //过滤key
            Map<String, String> map1 = Maps.filterKeys(map, "123"::equals);
            System.out.println(map1);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    结果:

    {123=20}
    
    • 1

    其余的方法大家可以自行研究~

    字符串操作

    Joiner的简单使用

      public static void main(String[] args) {
            /*
             * 比如拼接redisKey的场景
             */
            Joiner joiner = Joiner.on("_");
            String key = joiner.skipNulls().join(Arrays.asList("tenantId", null, "appId", "funcId"));
            System.out.println(key);
    
            /*
             * 打印map中key和value的场景
             */
            Map<String, String> map = new HashMap<String, String>() {{
                put("张三", "帅哥");
                put("李四", "美女");
            }};
    
            String join = Joiner.on("\n").withKeyValueSeparator("是").join(map);
            System.out.println(join);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    以下是打印结果

    tenantId_appId_funcId
    李四是美女
    张三是帅哥
    
    • 1
    • 2
    • 3

    Splitter与Joiner相对,它可实现字符串的分割

     public static void main(String[] args) {
            /*
             * 比如拼接redisKey的场景
             */
            Joiner joiner = Joiner.on("_");
            String key = joiner.skipNulls().join(Arrays.asList("tenantId", null, "appId", "funcId"));
    
            //使用Splitter进行分割
            Iterable<String> split = Splitter.on("_").split(key);
            split.forEach(System.out::println);
    
            String key1 = "宁教我负天下人休教天下人负我";
    
            //按照指定长度进行分割
            Iterable<String> split1 = Splitter.fixedLength(7).split(key1);
            split1.forEach(System.out::println);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    两次打印结果如下

    tenantId
    appId
    funcId
    宁教我负天下人
    休教天下人负我
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Bag

    Bag和List很相似,但它可以统计出重复元素的数量。

       public static void main(String[] args) {
            Bag box = new HashBag(Arrays.asList("1", "1", "2", "3"));
            box.add("1");
            //查看“1”有多少个
            int count = box.getCount("1");
            System.out.println(count);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    打印结果如下:

    3
    
    • 1

    LazyList

    LazyList可以延迟某元素的生成,在集合被访问的时候再生成,是一种懒加载的方式,一定程度上提高了性能。

     public static void main(String[] args) {
    
            List<String> list = new ArrayList<String>() {{
                add("1");
                add("2");
                add("3");
            }};
    
            List<String> lazy = LazyList.lazyList(list, () -> "4");
    
            System.out.println(lazy);
    
            //只有用到的时候才存入;类似于orElseGet
            lazy.get(3);
            System.out.println(lazy);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    打印结果如下:

    [1, 2, 3]
    [1, 2, 3, 4]
    
    • 1
    • 2

    双向Map

    jdk中的Map要求键唯一,双向Map则要求键、值都必须唯一,这样它既可以根据key来获取value,也可以通过value来获取key,所以叫双向Map。

      public static void main(String[] args) {
            BidiMap bidiMap = new TreeBidiMap<String, String>();
    
            bidiMap.put("key", "value");
            //key value存在相同则进行覆盖
            bidiMap.put("key1", "value");
            bidiMap.put("key2", "value2");
            System.out.println(bidiMap);
    
            //根据 key获取value
            System.out.println(bidiMap.get("key2"));
            //根据value获取key
            System.out.println(bidiMap.getKey("value2"));
            
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    {key1=value, key2=value2}
    value2
    key2
    
    • 1
    • 2
    • 3

    并发集合小总结

    CopyOnWriteArrayList

    ArrayList一般都是在方法内部使用,所以相对来说是安全的,但是多线程环境下是非安全的,先来看一下ArrayList的源码。
    在jdk8下,如果new一个ArrayList,不指定其大小,默认为空:

        public ArrayList(int initialCapacity) {
        	//指定大小
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
            //不指定大小默认为空
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    当执行add操作时:

      public boolean add(E e) {
      		//根据加上当前元素之后该集合元素的数量来判断是否扩容
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    
        private void ensureCapacityInternal(int minCapacity) {
            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
        }
        
        //入参为当前数组以及加上当前元素后当前数组的大小
        private static int calculateCapacity(Object[] elementData, int minCapacity) {
        	//如果是第一次add操作,且初始化时没有指定ArrayList大小
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            	//则默认大小为DEFAULT_CAPACITY 10
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            //如果非第一次add或者new的时候指定了容量大小,则返回集合当前的大小+1
            return minCapacity;
        }
        
       private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // 如果集合大小小于add该元素之后集大小则进行扩容操作。
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
        
        private void grow(int minCapacity) {
            int oldCapacity = elementData.length;
            //扩容后的容量大小为扩容前的(1+0.5)倍
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            //如果扩容后的大小小于当前大小的话,则新集合大小为当前大小。
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // 拷贝一个新的数组。
            elementData = Arrays.copyOf(elementData, newCapacity);
        }    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    以上有两个比较重要的名词:
    1、minCapacity:预期集合大小,即要想把当前元素加到该集合中,该集合需要的大小。
    2、elementData:集合中存放元素的数组
    用文字来描述一下add流程:
    1、判断是否需要扩容
      1.1、如果集合预期的大小超过了当前集合大小,则进行扩容
      1.2、将集合扩容1.5倍
      1.3、如果扩容之后的的大小小于预期大小,则集合大小为预期大小,但如果扩容之后大小大于集合最大值,则进行huge扩容。
    2、将集合中的元素加1(i++操作,非原子性)。
    在以上过程中,不安全性体现在两个地方:
    1、数组越界
    当集合元素数量为9,线程A和线程B同时进行add操作,A执行时发现预期集合大小为10,等于当前容量,不需要扩容(也就是上述1.1步骤),线程B此时和线程A运行到同一行代码处,发现也不需要扩容,则当A把元素添加到集合之后,B再添加就会出现数组越界的情况。
    2、覆盖操作
    因为向数组中存入的操作方式为size++,也就是分了三步
    1、从主存中拿到当前size的值放到本地高速缓存中
    2、把size值进行加1
    3、再把size值放到主存中
    当线程A和线程B同时进行size++,A进行到第1步,B也进行到第1步,当A和B都执行到第3步时,则会发生覆盖的情况。
    以上就是ArrayList多线程下不安全的原因,解决方法是使用Vector或CopyOnWriteArrayList,前者主要是用了内部锁,这次主要讨论一下后者。

    看一下CopyOnWriteArrayList的源码:

    
     private transient volatile Object[] array;
     
     public boolean add(E e) {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                Object[] elements = getArray();
                int len = elements.length;
                //将元素拷贝到一个新的数组中
                Object[] newElements = Arrays.copyOf(elements, len + 1);
                newElements[len] = e;
                //然后把新数组给之前的数组
                setArray(newElements);
                return true;
            } finally {
                lock.unlock();
            }
        }
      final Object[] getArray() {
            return array;
        }
      final void setArray(Object[] a) {
            array = a;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    CopyOnWriteArrayList的add操作虽然没有使用内部锁,但使用了可重入锁,每次只能有一个线程执行add操作,这点和Vector的add操作其实是一样的,但CopyOnWriteArrayList在add的时候新建了一个新的数组,在新数组中添加好元素后再将引用给之前的数组,这样CopyOnWriteArrayList在进行读操作时读的是之前的数组,保证了线程的安全性,因此其适用于读多写少的场景。

    ConcurrentHashMap

    我们先来看一下Map的源码,分析一下Map的不安全性体现在哪里。

    	transient Node<K,V>[] table;
        //hashMap有3个构造,无参构造在初始化的时,负载因子的值为0.75
        public HashMap() {
            this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
        }
    	
       public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
        
       final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            //如果在初始化时未设置容量大小,则默认为16
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            //15与当前元素的hash值进行与运算,如果得到的位置为空,则创建一个新的节点。
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                //当前元素的hash值与之前元素的hash值相同且key相同,还equels,就覆盖之前的节点
                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 {
                //元素不重复,也不是红黑树,则是单纯的发生了hash冲突
                    for (int binCount = 0; ; ++binCount) {
                    	//如果发生冲突节点的下一个节点为null,则就将节点放进去
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            //走到这里说明比较了8次,则将链表转红黑树。
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        //发生冲突节点的下一个几点和当前节点元素重复,则直接覆盖。
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        //再进行链表的下一个节点重新判断。
                        p = e;
                    }
                }
                //e不为null说明发生了覆盖,则返回当前元素。
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            //如果当前元素数量大于了阈值,则进行扩容。
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    用文字来描述jdk1.8的扩容机制如下:
    1、初始化容量
    2、获取要put元素的数组下标
      2.1判断该下标是否发生了碰撞
        2.1.1如果没有碰撞则直接put
        2.1.2如果发生了碰撞就判断是否和要put的元素一样
          2.1.2.1如果一样就直接覆盖
          2.1.2.2如果不一样就判断是不是树
            如果是树就放到树节点
            如果不是树就将创建新的节点并和上一个节点连接上
              然后判断连接之后的链表长度是不是大于8,
                如果大于8就转树
                小于8就put
    3、判断容量是否达到阈值
    达到了就扩容
    没达到就不扩容

    jdk1.8的话会出现覆盖现象:
    即线程A和线程B同时扩容,但它俩同时put元素时key的hash值是一样的,当A和B都运行到了2.1.1步骤时,A判断没有hash冲突时直接put,B线程也是同样操作,则就会出现了覆盖的现象。

    jdk1.7扩容的话会有循环链的情况,在此不做过多描述了,扩容图如下:
    在这里插入图片描述

    为了解决以上问题,我们可以使用ConcurrentHashMap,我们看一下它的jdk1.8源码:

     transient volatile Node<K,V>[] table;
     
     final V putVal(K key, V value, boolean onlyIfAbsent) {
            if (key == null || value == null) throw new NullPointerException();
            //获取hash值,方式是将hashCode转成二进制右移16位后与hashCode进行异或运算再与int最大值进行与运算。
            int hash = spread(key.hashCode());
            int binCount = 0;
            for (Node<K,V>[] tab = table;;) {
                Node<K,V> f; int n, i, fh;
                //如果当前数组为空,则进行初始化
                if (tab == null || (n = tab.length) == 0)
                    tab = initTable();
                //hash算法来计算当前位置是否为空
                else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                    //如果当前位置为空则进行cas机制进行创建节点。
                    if (casTabAt(tab, i, null,
                                 new Node<K,V>(hash, key, value, null)))
                        break;                   // no lock when adding to empty bin
                }
               //如果是MOVED,也就是-1,则进行多线程扩容
                else if ((fh = f.hash) == MOVED)
                    tab = helpTransfer(tab, f);
                else {
                //走到这个else,则说明了hash冲突
                    V oldVal = null;
                    //当出现hash冲突,就锁住当前元素,保证只有一个线程操作
                    synchronized (f) {
                        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;
                                    }
                                }
                            }
                            //如果节点转化成了树,则添加节点
                            else if (f instanceof TreeBin) {
                                Node<K,V> p;
                                binCount = 2;
                                if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                               value)) != null) {
                                    oldVal = p.val;
                                    if (!onlyIfAbsent)
                                        p.val = value;
                                }
                            }
                        }
                    }
                    if (binCount != 0) {
                    	//链表长度大于8则转树
                        if (binCount >= TREEIFY_THRESHOLD)
                            treeifyBin(tab, i);
                        if (oldVal != null)
                            return oldVal;
                        break;
                    }
                }
            }
            addCount(1L, binCount);
            return null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    当没有出现hash冲突时,通过cas机制保证线程安全,当出现hash冲突后,则加了一把内部锁,锁住当前要操作的元素。以上只是jdk1.8的情况。

  • 相关阅读:
    ubuntu18.04安装mysql5.7并配置数据存储路径
    QML<10> qml 文件的插件封装 ,打包发布 ,隐藏qml文件
    LightGBM调参与模型权重可视化
    C#上位机调试经验
    MySQL基本知识点梳理和查询优化
    【数据库MongoDB】MongoDB与大数据关系以及MongoDB中重要的进程:mongod进程与mongo进程关系
    【linux系统】如何在服务器上安装Anaconda
    连接云服务器Docker中的Mysql 详细图文操作(全)
    Flink基础
    【阅读笔记】如何阅读一本书
  • 原文地址:https://blog.csdn.net/aaaPostcard/article/details/127707357