详解Java中集合的体系结构,并发修改异常出现的原理

Part1  java集合体系结构

【一Collection


一.List接口有三个实现类:ArrayList,LinkedList,Vector
(1)ArrayList:底层是数组,存储于内存的一段连续空间,假设int[] arr首地址是1000,int类型占4个字节,所以arr[0]的首地址是1000,arr[1]的首地址就是1004,只要知道索引就能定位,因此查询快;而它的增删是以迭代的方式进行的,某位置增删一个元素,后面的每个元素都需要向后或向前移动一位。ArrayList是非线程安全的。
(2)LinkedList:底层基于链表实现,链表内存是不连续的散的,每一个元素存储本身内存地址及前后元素的地址。LinkedList是非线程安全的。
1.链表为什么增删快?
就像自行车链条一样,你想增加某一元素,在随机位置打破前后的连接并将自己接上去即可。删除某一元素,先找到该元素再打破其前后的连接并将前后元素重新连接。
2.为什么查找慢?
因为地址是散乱的,每个元素只知道前后邻居是谁,如果你找某个元素,就需要从首或者尾开始挨个问,直到问到被找元素。
【总结】ArrayList增删慢,查询快;LinkedList增删快,查询慢。快慢是相对与两者而言的。
(3)Vector非常类似ArrayList        
ArrayList和Vector的区别:ArrayList是非线程安全的,效率高;Vector是基于线程安全的,效率低。但是我们学了同步代码块,可以将线程不安全的集合转成线程安全的。因此个人猜想这也是为什么现在课程中不用学Vector。
如何将线程不安全的集合转成线程安全?
      大家可以在Collections类中查找synchronizedXXX()方法。


二.【以目前所学内容分类】Set接口有三个实现类:
  HashSet(底层由HashMap实现),TreeSet(底层由平衡二叉树实现),以及HashSet的直接子类LinkedHashSet。
(1)HashSet实现 Set接口,由哈希表(实际上是一个HashMap实例,[本文后面还会提到])支持。它不保证Set的迭代顺序;特别是它不保证该顺序恒久不变。此类允许但最多只能有一个null元素。如果add()方法再次添加相同元素,则返回false。

【拓展】HashSet如何保证元素唯一性的原理
* 1.HashSet原理
   * 我们使用Set集合都是需要去掉重复元素的, 如果在存储的时候逐个equals()比较,效率较低;哈希算法提高了去重复的效率,降低了使用equals()方法的次数(哈希算法算出座位,座位相同时才用equals()比较)
   * 当HashSet调用add()方法存储对象的时候, 先调用对象的hashCode()方法得到一个哈希值, 然后在集合中查找是否有哈希值相同的对象
  * 如果没有哈希值相同的对象就直接存入集合
  * 如果有哈希值相同的对象, 就和哈希值相同的对象逐个进行equals()比较,比较结果为false就存入, true则不存

  * 2.将自定义类的对象存入HashSet去重复
  * 类中必须重写hashCode()和equals()方法
equals()默认比较地址值,如果是自定义集合,地址值都不相同,无法保证唯一性
      第一种情况:如果哈希值不同,直接存入
      第二种情况:如果哈希值相同,equals()方法比较结果也相同,不存储
      第三种情况:如果哈希值相同,equals()方法比较结果不同的,以桶结构存储

【补充】
  public int hashCode() {
                final int prime = 31;
  ...
  }
    /*
  重写hashCode()定义的prime为什么是31?
  1, 31是一个质数,质数是能被1和自己本身整除的数。质数的特性能够使得它和其他数相乘后得到的结果比其他方式更容易产成唯一性,也就是哈希值的冲突概率最小。
  2, 31这个数既不大也不小,太小计算后也可能相同,太大可能超过取值范围
  3, 31这个数好算,2的五次方-1,2向左移动5位
  */

(2)LinkedHashSet,与HashSet一样,但是能保证存取顺序。
【扩展】
  1.随机存储n个数,HashSet效率比LinkedHashSet略高
    2.虽然HashSet存取无序,但是元素存储的地址是固定的。Hash是用空间换时间
Hash算法支持的集合,在存储元素时,会开辟一个大数组,根据计算的哈希值将元素放在不同区域,如果哈希值相同但元素不相同,则在下面再次开辟空间将该元素挂在下面(桶结构存储),因此占用空间比较大,但是用Hash算法效率高。
(3)TreeSet使用元素的自然顺序对元素进行排序,或者根据创建TreeSet时提供的Comparator比较器进行排序,具体取决于使用的构造方法。
存储特点:第一个元素存储的时候,直接作为根节点存储;
          从第二个元素开始,每个元素从根节点开始比较
          大   :作为右孩子存储
          小   :作为左孩子存储
          相等 :不存储(元素唯一)
读取按照左中右的顺序读取出来,因此TreeSet可用于排序

【拓展】
1、面试题:Collection和Collections有什么区别?
           Collection是单列集合体系的跟接口,包含了单列集合体系的共性
           Collections是一个工具类,方法都是用于操作Collection
[补充]
    Collections的binarySearch(list, key),根据传入的key返回索引,但是list必须满足升序(可用sort()方法先排好序)。并且返回的是第1个索引,如果需要返回所有索引则需要重写里面的方法。binarySearch多用于数据库检索。

2、迭代器方法是Collection的方法,所有集合只要有此方法均可使用



[二] Map

1、Map接口概述
根据API的描述可以知道:
   * 将键映射到值的对象
   * 一个映射不能包含重复的键
   * 每个键最多只能映射到一个值
  HashMap、LinkedHashMap、Hashtable(基本已经被HashMap代替)、TreeMap。 可以存储null键和null值 key不重复。

2、 Hashtable和 HashMap
   Hashtable:JDK1.0开始。线程安全,效率低。不允许null键和null值
   HashMap:JDK1.2开始。线程不安全,效率高。允许null键和null值
3、HashMap和LinkedHashMap跟Set类似,这里就不赘述。
4、Map集合的两种遍历方式:
   1. Set<String> keys = map.keySet();根据键获取对应的值
   2. Set<Map.Entry<Student, String>> entrys = hm.entrySet();获取每一个键值对对象

【补充】
(1)泛型:
     优点:A.扫黄
           B.将运行期的错误提前到了编译期,提高代码安全性
           C.省去强转的麻烦
     注意:泛型前后需要一致
           泛型必须是引用数据类型
           后面的泛型可以省略(JDK1.7新特性:菱形泛型)
    泛型通配符
          A: 泛型通配符<?>
             任意类型,如果没有明确,那么就是Object以及任意的Java类了
          B: ? extends E
             向下限定,E及其子类
          C: ? super E
             向上限定,E及其父类
(2)数组→集合:
     Arrays.asList(),数组转换成集合虽然不能增加或减少元素,但是可以用集合的思想操作数组,也就是说可以使用集合其他的方法
  集合→数组:
     Collection.toArray()



Part2  并发修改异常出现的原理

1.异常解释
ConcurrentModificationException:当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。
2.产生的原因
迭代器是依赖于集合而存在的,在使用迭代器遍历集合的时候,集合的中添加、删除或修改了元素,导致索引变化,但迭代器并不知道,所以就报错了,这个错叫并发修改异常。
简单描述就是:迭代器遍历元素的时候,是不能通过集合修改元素的。
3.解决方法
A:迭代器迭代元素,迭代器修改元素,如ListIterator。
B:集合遍历元素,集合修改元素(用普通for 为什么不用增强for?因为集合中增强for的底层就是迭代器)
【补充】ListIterator可用于倒序遍历集合(但必须先正序遍历一遍,将指针移到末尾)。
        普通迭代器无法倒序遍历集合,因此增强for也不能倒序遍历集合。



Part3  一些相关知识的补充

补充1:
单列集合Collection与双列集合Map的关系:
看过一个老师的视频说单列集合的底层就是双列集合,但是List等并未找到相关资料佐证,因此为了严谨性,单列集合中Set底层也是双列集合,存的new Object();作为单列集合时会隐藏。
举例:
Set底层依赖的是Map,源码中Set的add()方法使用的map.put(),第一个参数是key,第二个是PRESENT,单列集合PRESENT存储的是静态的Object对象。在使用的时候并不显示value列。


补充2
Collection中带all的方法:

All的功能演示
  boolean addAll(Collection c)
  boolean removeAll(Collection c)
  boolean containsAll(Collection c)
  boolean retainAll(Collection c)


1.boolean addAll(Collection c)
  Collection c1 = new ArrayList();
  c1.add("a");
  c1.add("b");
  c1.add("c");
  c1.add("d");
               
  Collection c2 = new ArrayList();       //alt + shift + r改名
  c2.add("a");
  c2.add("b");
  c2.add("c");
  c2.add("d");
               
  c1.addAll(c2);    //将c2中的每一个元素添加到c1中
                                                 //结果:c1 [a,b,c,d,a,b,c,d]
  c1.add(c2);      //将c2看成一个对象添加到c1中
                                 //结果:c1 [a,b,c,d,[a,b,c,d]]


2.boolean removeAll(Collection c)
   Collection c1 = new ArrayList();
  c1.add("a");
  c1.add("b");
  c1.add("c");
  c1.add("d");
               
  Collection c2 = new ArrayList();
  c2.add("a");
  c2.add("b");
  c2.add("z");
               
  boolean b = c1.removeAll(c2);        //删除的是交集
  System.out.println(b);                  //true
  System.out.println(c1);                 //[c,d]
    //c1和c2如果没有交集,则返回false
    //c1和c2不需要一一对应,比如c1[a,b,c,d],c2[x,f,a],c1.removeAll(c2)后c1[b,c,d]


3.boolean containsAll(Collection c)
  Collection c1 = new ArrayList();
  c1.add("a");
  c1.add("b");
  c1.add("c");
  c1.add("d");
               
  Collection c2 = new ArrayList();
  c2.add("a");
  c2.add("b");
  c2.add("z");
               
  boolean b = c1.containsAll(c2);   //判断调用的集合是否包含传入的集合。
  System.out.println(b);  //false      //c2可以有重复内容,但必须所有元素c1中都有,否则false


4.boolean retainAll(Collection c)
  Collection  c1  =  new  ArrayList();
  c1.add("a");
  c1.add("b");
  c1.add("c");
  c1.add("d");
               
  Collection c2 = new ArrayList();
  c2.add("a");
  c2.add("b");
  c2.add("c");
  booleanb = c1.retainAll(c2);  //取交集
  System.out.println(b);           //true
  System.out.println(c1);          //结果:[a,b,c]
  //如果c1[a,b,c,d] c2[a,d,c,b,e,f]
  booleanb = c1.retainAll(c2);   //取交集
  System.out.println(b);           //false
  System.out.println(c1);          //结果:[a,b,c,d]
  //c1结果是c1和c2的交集,c1包含c2、c1与c2相交,或者c1与c2无交集返回true,其他情况(即c1包含于c2)返回false


补充3
不同类型的Map集合使用keySet()方法遍历集合时的输出特点:
示例代码:
     LinkedHashMap<String, String> lhm = new LinkedHashMap<String , String>();
        lhm.put("1", "OOO");
        lhm.put("3", "OOO");
        lhm.put("2", "OOO");
        lhm.put("5", "OOO");
        lhm.put("4", "OOO");

        Iterator<String> it =  lhm.keySet().iterator();

     while (it.hasNext()) {
         System.out.println(it.next());
     }

通过实验验证,结果:
Hashtable.keySet()         降序
TreeMap.keySet()           升序
HashMap.keySet()           乱序
LinkedHashMap.keySet()     原序
说明:
除了TreeMap的keySet()在原码中有解释,其余Map并没有相关文档对其返回顺序有说明。
没有特殊要求建议使用LinkedHashMap()因为保证存取顺序。


补充4
使用TreeMap强制存入重复元素,重写compareTo()则会在遍历的时候返回value = null。
解释:
遍历过程用到getEntry(key)方法,里面用到了compare()方法,而我们重写的compareTo()方法强制使num==0返回正数,修改了比较器底层。
TreeMap是二叉树结构存储数据。当我们拿着键通过get()方法获取值时,底层拿着我们传入的键去逐个比对此处调用比较器中的compare()方法:小于零则往左边去找,大于零往右去找,只有当等于0时才返回该值,而我们强制compareTo()方法等于0的时候返回1,所以一直往右边去找,永远找不到,直到最后返回null。

以上内容如有错误或者不严谨之处,请大家指正,谢谢。

猜你喜欢

转载自blog.csdn.net/chengsw1993/article/details/80718903