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);
    }

因其是不可变的,所以无法进行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"));
    }

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

[teacher, player, player]

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);
    }

打印结果如下:

[position0, position1]
position0
[position0, position1, position2]

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);
    }

打印结果:

[123]
[1234]
[12345]

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);
    }

打印结果:

[2]
[1]

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);
    }

结果:

{123=20}

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

字符串操作

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);
    }

以下是打印结果

tenantId_appId_funcId
李四是美女
张三是帅哥

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);
    }

两次打印结果如下

tenantId
appId
funcId
宁教我负天下人
休教天下人负我

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);
    }

打印结果如下:

3

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]
[1, 2, 3, 4]

双向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"));
        
    }
{key1=value, key2=value2}
value2
key2

并发集合小总结

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);
        }
    }

当执行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、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;
    }

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;
    }

用文字来描述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;
    }

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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值