java集合类学习

一:Collection接口

       Set接口和List接口都集成于Collection接口

       1.Set

          无序不可重复

        (1)HashSet

                 HashSet其实时一个map,源代码:

public HashSet() {
  map = new HashMap<E,Object>();
}

                 add()方法调用的是个map,源代码:

public boolean add(E e) {
  return map.put(e, PRESENT)==null;
}

                 可以看出HashSet加入的对象做了map的key,因此其中的元素是不可以重复的。

       2.List

         有序可重复

       (1)ArryList

                实现是一个Object类型的数组ArrayList的长度增长算法为

                “新长度=(旧长度*3)/2+1”,源代码算法如下

      int newCapacity = (oldCapacity * 3)/2 1;ArrayList.add()最终调用的

                是System.arraycopy(src, srcPos, dest, destPos, length);方法,并没有使用

                HashCode

                ArrayList为什么线程不安全,请看:

               https://blog.csdn.net/u012859681/article/details/78206494

       (2)Vector

                线程安全的List,所谓线程安全是它只有在使用add()、remove()、set()、get()等

                方法时是线程安全的,但是它在遍历集合的时候并不能做到线程池安全,通过看

                它的源码可以发现它在调用add()、remove()、set()、get()等方法时,使用了

                Sychronized。

       (3)CopyOnWriteArrayList和Collections.synchronizedList()

                CopyOnWriteArrayList和Collections.synchronizedList是实现线程安全的列表的

                两种方式。两种实现方式分别针对不同情况有不同的性能表现,其中

                CopyOnWriteArrayList的多线程写操作性能较差,而多线程的读操作性能较好。

                Collections.synchronizedList的写操作性能比CopyOnWriteArrayList在多线程操

                作的写操作好很多,而读操作因为是采用了synchronized关键字的方式,其读

                操作性能并不如CopyOnWriteArrayList。因此在不同的应用场景下,应该选择不

                同的多线程安全实现类。

                CopyOnWriteArrayList是java.util.concurrent包中的一个List的实现类。

                CopyOnWrite的意思是在写时拷贝,也就是如果需要对CopyOnWriteArrayList

                的内容进行改变,首先会拷贝一份新的List并且在新的List上进行修改,最后将原

                List的引用指向新的List。CopyOnWriteArrayList可以线程安全的遍历。

       (4)Queue保持一个队列(先进先出)的顺序

二:Map接口

       一组成对的"键值对"对象

      1.HashMap

       (1)HashMap概述

                HashMap实现是一个链表的数组,基于链地址法形成的散列,数组是由Entry组

                成,一个Entry包含一个key-value 键值对。

                简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是

                主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry

                的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果

                定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,

                存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象

                的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,

                性能才会越好。

   

   可以参考文章:

   https://www.cnblogs.com/holyshengjie/p/6500463.html 

https://yikun.github.io/2015/04/01/Java-HashMap%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/

       (2)HashMap是怎么处理Hash冲突的

                Hash冲突时在put时产生,做如下操作:

              (a) 对key的hashCode()做hash,然后再计算index;

              (b)如果没碰撞直接放到bucket里;

              (c)如果碰撞了,以链表的形式存在buckets前面(1.8是放在buckets后面)

              (d)如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换

                       成红黑树;

              (e)如果节点已经存在就替换old value(保证key的唯一性)

              (f)如果bucket满了(超过load factor*current capacity),就要resize。

                      put时源码:

public V put(K key, V value) {
     if (key == null)
         return putForNullKey(value);
     int hash = hash(key.hashCode());
     int i = indexFor(hash, table.length);
     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
         Object k;
         //如果发现key已经在链表中存在,则修改并返回旧的值
         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
             V oldValue = e.value;
             e.value = value;
             e.recordAccess(this);
             return oldValue;
         }
     }
     //如果遍历链表没发现这个key,则会调用以下代码
     modCount++;
     addEntry(hash, key, value, i);
     return null;
}

                  addEntry源码:

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

                  Entry的构造方法:

Entry( int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
}

                get时的逻辑:

              (a)bucket里的第一个节点,直接命中;

              (b)如果有冲突,则通过key.equals(k)去查找对应的entry

              (c)若为树,则在树中通过key.equals(k)查找,O(logn);

              (d)若为链表,则在链表中通过key.equals(k)查找,O(n)。

                       Hashmap多线程的环境下会出现死锁,具体原因参考文章:

                       https://coolshell.cn/articles/9606.html

                get时源码:                    

public V get(Object key) {

        if (key == null)

            return getForNullKey();

        int hash = hash(key.hashCode());

        //先定位到数组元素,再遍历该元素处的链表

        for (Entry<K,V> e = table[indexFor(hash, table.length)];

             e != null;

             e = e.next) {

            Object k;

            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

                return e.value;

        }

        return null;

}

       (3)HashMap为什么是线程不安全的

                http://www.cnblogs.com/qiumingcheng/p/5259892.html

       (4)1.8对HashMap的优化

                JDK 1.8对HashMap进行了比较大的优化,底层实现由之前的“数组+链表”改为

                “数组+链表+红黑树”,当链表长度太长(默认超过8)时,链表就转换为红黑

                树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树

                的插入、删除、查找等算法。

                什么是红黑树以及红黑树的优点查看:

                https://www.sohu.com/a/201923614_466939,看后面别看前面,前面都是废话

      2.HashTable

        Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。

        它并不继承自Collection,把它写在这里纯粹是为了方便和HashMap比较。

        HashTable和HashMap采用相同的存储机制,二者的实现基本一致,不同的是:

      (1)HashMap是非线程安全的,HashTable是线程安全的,内部的方法基本都是

               synchronized。

      (2)HashTable不允许有null值的存在。

               在HashTable中调用put方法时,如果key为null,直接抛出

               NullPointerException

      3.TreeMap以及TreeMap和HashMap的比较

       (1)非线程安全的

       (2)基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。

       (3)TreeMap适用于按自然顺序或自定义顺序遍历键(key);HashMap适用于在

                Map中插入、删除和定位元素。

      4.ConcurrentHashMap和HashTable的区别

         ConcurrentHashMap继承自AbstractMap,实现了ConcurrentMap接口,使得它

         具有Map的属性,同时又有多线程相关的属性,它并没有继承Collection,写在

         这纯粹是为了比较方便

        ConcurrentHashMap的性能优于HashTable,ConcurrentHashMap的锁是基于

        Lock的,而HashTable的锁是基于synchronized的

      6.Lock和sychronized:

      (1)sychronized获取锁的线程如果被阻塞了,那么其它等待锁的线程需要一直等

               待下去,而lock就可以使线程不处于一直等待的状态,可以只等待一段时间或

               响应中断。

      (2)当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作

               会发生冲突现象,但是读操作和读操作不会发生冲突现象。如果使用

               sycnronized读操作和读操作会发生冲突,而使用lock读操作和读操作不会发生

               冲突。

      (3)lock可以直到线程获取锁是否成功,而sychronized却不能。

      (4)sychronized可以自动释放锁,lock需要手动释放锁(经常在finally中)。

      (5)锁类型:

               sychronized:可重入 不可中断 非公平

               lock:可重入 可中断 可公平(两者皆可)

      7.了解ConcurrentHashMap:

         ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然

         后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他

         段的数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),

         它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作

         完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现

         死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也

         是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需

         要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。

        ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment

        是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,

        HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment

        数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment

        里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个

        Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改

        时,必须首先获得它对应的Segment锁。其结构如下图:

参考文章:http://www.importnew.com/16142.html

关于ArrayList的5道面试题请查看网址:http://blog.csdn.net/quentain/article/details/51365432

猜你喜欢

转载自blog.csdn.net/jialanshun/article/details/78048962