【不失业计划】 Java集合框架+底层原理

集合框架部分

      • 一、Collection
        • 1、Set
        • 2、Queue
        • 3、List
      • 二、Map
        • 1、HashMap
        • 2、LinkedHashMap
        • 3、HashTable
        • 4、ConcurrentHashMap
        • 5、TreeMap
      • 三、工具类
        • 1、Collections
        • 2、Arrays
        • 3、各种转换

集合可以看作是一种容器,用来存储对象信息。所有集合类都位于java.util包下,但支持多线程的集合类位于java.util.concurrent包下。

Java集合类主要由两个根接口Collection和Map派生出来的,Collection派生出了三个子接口:List、Set、Queue(Java5新增的队列),因此Java集合大致也可分成List、Set、Queue、Map四种接口体系,(注意:Map不是Collection的子接口)。

数组与集合的区别:

  • 数组长度不可变化而且无法保存具有映射关系的数据;集合类用于保存数量不确定的数据,以及保存具有映射关系的数据。
  • 数组元素既可以是基本类型的值,也可以是对象;集合只能保存对象。

一、Collection

【不失业计划】 Java集合框架+底层原理_第1张图片

1、Set

Set是一种不包含重复元素的Collection,Set最多只有一个null元素。

  • HashSet:线程不同步,内部使用HashMap进行数据存储,提供的方法基本都是调用HashMap的方法,所以两者本质是一样的,集合元素可以为null。且不能保证元素的顺序。

    存储原理:(同HashMap一样)

    当向HashSet集合存储一个元素时,HashSet会调用该对象的hashCode()方法得到其hashCode值,然后根据hashCode值决定该对象的存储位置。HashSet集合判断两个元素相等的标准是

    (1)两个对象通过equals()方法比较返回true;

    (2)两个对象的hashCode()方法返回值相等。

    因此,如果(1)和(2)有一个不满足条件,则认为这两个对象不相等,可以添加成功。如果两个对象的hashCode()方法返回值相等,但是两个对象通过equals()方法比较返回false,HashSet会以链式结构将两个对象保存在同一位置,这将导致性能下降,因此在编码时应避免出现这种情况。

  • LinkedHashSet

    LinkedHashSet是HashSet的一个子类,具有HashSet的特性,也是根据元素的hashCode值来决定元素的存储位置。但它使用链表维护元素的次序,元素的顺序与添加顺序一致。由于LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet,但在迭代访问Set里的全部元素时有很好的性能。

  • NavigableSet:添加了搜索功能,可以对元素进行搜索:小于、小于等于、大于、大于等于,返回一个符合条件的最接近给定元素的Key。

  • TreeSet:线程不同步,内部使用NavigableMap操作,默认元素“自然顺序”排列,可以通过Comparator改变排序,它采用红黑树的数据结构来存储集合元素

  • EnumSet:线程不同步,内部使用Enum数组实现,不允许添加null值,速度比HashSet快,只能存储在构造函数传入的枚举类的枚举值。

  • 各个Set实现类的性能分析:

    • HashSet的性能比TreeSet的性能好(特别是添加,查询元素时),因为TreeSet需要额外的红黑树算法维护元素的次序,如果需要一个保持排序的Set时才用TreeSet,否则应该使用HashSet。
    • LinkedHashSet是HashSet的子类,由于需要链表维护元素的顺序,所以插入和删除操作比HashSet要慢,但遍历比HashSet快。
    • EnumSet是所有Set实现类中性能最好的,但它只能 保存同一个枚举类的枚举值作为集合元素。
    • 以上几个Set实现类都是线程不安全的,如果多线程访问,必须手动保证集合的同步性。
  • Set常用方法

    • add() 新增:重复新增的值会被覆盖

    • 因为Set没有下标和key,所以没有修改方法

    • 删除:remove(Object) 和removeAll(Set)

    • 循环:

      Set<String> ss=new HashSet<String>();
      ss.add("a");ss.add("b");ss.add("c");ss.add("d");ss.add("e");ss.add("f");ss.add("g");ss.add("h");
      //循环方法1
      for (String s : ss) {
        System.out.print(s+",  ");
      }
      //循环方法2
      Iterator<String> iterator = ss.iterator();
      while(iterator.hasNext()){
      	System.out.print(iterator.next()+",  ");
      }
      
2、Queue

Queue用于模拟队列这种数据结构,队列通常是指“先进先出”(FIFO)的容器。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。

  • Dueue

    Deque接口是Queue接口的子接口,它代表一个双端队列。LinkedList也实现了Deque接口,所以也可以被当作双端队列使用。

  • ArrayDeque

    用数组实现的Deque;既然是底层是数组那肯定也可以指定其capacity,也可以不指定,默认长度是16,然后根据添加的元素的个数,动态扩展。ArrayDeque由于是两端队列,所以其顺序是按照元素插入数组中对应位置产生的(下面会具体说明)。
    由于本身数据结构的限制,ArrayDeque没有像ArrayList中的trimToSize方法可以为自己瘦身。ArrayDeque的使用方法就是上面的Deque的使用方法,基本没有对Deque拓展什么方法。

  • Queue常用方法

    • 成功返回true,在操作失败时抛出异常
      • add(E e):添加一个元素到队尾
      • remove():获取队首的元素,并从队列中移除
      • element():获取队首的元素,但不从队列中移除
    • 成功返回true,失败时返回一个特殊值(取决于操作,为NULL或false)
      • offer(E e):添加一个元素到队尾(offer(E e)操作是专为容量受限的队列实现而设计的;在大多数实现中,插入操作不会失败。)
      • poll():获取队首的元素,并从队列中移除
      • peek():获取队首的元素,但不从队列中移除
3、List
  • ArrayList

    • ArrayList的底层实现是一个动态数组,也是我们最常用的集合,是List类的典型实现。它允许任何符合规则的元素插入甚至包括null。每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小,当数组大小不足时增长率为当前长度的50%

    • 随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。

    • ArrayList是线程不安全的,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。

    • 具有的特点

      • ArrayList擅长于随机访问。
      • 同时ArrayList是非同步的。
      • 线程不安全的。
  • Vector

    • 与ArrayList相似,它的操作与ArrayList几乎一样。默认初始容量为10,当数组大小不足时增长率为当前长度的100%。

    • 但是Vector是同步的。所以说Vector是线程安全的动态数组。他的同步是通过Iterator方法加synchronized实现的。

    • vector是线程安全的吗?

      vector的单个操作时原子性的,也就是线程安全的。但是如果两个原子操作复合而来,这个组合的方法是非线程安全的,需要使用锁来保证线程安全。

  • Stack

    • Stack是Vector的子类,用户模拟“栈”这种数据结构,“栈”通常是指“后进先出”(LIFO)的容器。最后“push”进栈的元素,将被最先“pop”出栈。
    • 栈常用方法:
      • empty():判断栈是否为空
      • peek():查看栈顶对象,但不移除
      • pop():移除栈顶对象,并作为此函数的返回值返回该对象
      • push(E item):把项压入栈顶
      • search(Object o):返回该对象在栈中位置
    • Stack与Vector一样,是线程安全的,但是性能较差,尽量少用Stack类。如果要实现栈”这种数据结构,可以考虑使用LinkedList
  • LinkedList

    • LinkedList类是List接口的实现类——这意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,可以被当作成双端队列来使用,因此既可以被当成“栈"来使用,也可以当成队列来使用。

    • LinkedList的实现机制与ArrayList完全不同ArrayList内部是以数组的形式来保存集合中的元素的,因此随机访问集合元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能比较出色。

    • 常用方法:

      void addFirst(E e):将指定元素插入此列表的开头。
      void addLast(E e): 将指定元素添加到此列表的结尾。
      E getFirst(E e): 返回此列表的第一个元素。
      E getLast(E e): 返回此列表的最后一个元素。
      boolean offerFirst(E e): 在此列表的开头插入指定的元素。
      boolean offerLast(E e): 在此列表末尾插入指定的元素。
      E peekFirst(E e): 获取但不移除此列表的第一个元素;如果此列表为空,则返回 null。
      E peekLast(E e): 获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null。
      E pollFirst(E e): 获取并移除此列表的第一个元素;如果此列表为空,则返回 null。
      E pollLast(E e): 获取并移除此列表的最后一个元素;如果此列表为空,则返回 null。
      E removeFirst(E e): 移除并返回此列表的第一个元素。
      boolean removeFirstOccurrence(Objcet o): 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。
      E removeLast(E e): 移除并返回此列表的最后一个元素。
      boolean removeLastOccurrence(Objcet o): 从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。
      
    • LinkedList也是非线程安全的。

    • LinkedList与ArrayList的性能对比

      • ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。ArrayList应使用随机访问(即,通过索引序号访问)遍历集合元素。
      • LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。LinkedList应使用采用逐个遍历的方式遍历集合元素。
      • 如果涉及到“动态数组”、“栈”、“队列”、“链表”等结构,应该考虑用List,具体的选择哪个List,根据下面的标准来取舍。
        • 对于需要快速插入,删除元素,应该使用LinkedList。
        • 对于需要快速随机访问元素,应该使用ArrayList。
        • 对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类(如ArrayList)。
        • 对于“多线程环境,且List可能同时被多个线程操作”,此时,应该使用同步的类(如Vector)。

二、Map

【不失业计划】 Java集合框架+底层原理_第2张图片

1、HashMap
  • 实现原理

    ​ HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。

    • 添加一个数据时:

      • 通过哈希算法将任意长度的key值通过散列算法换成固定长度的地址(hashcode())
      • 通过这个地址在数组中进行查找,找到hashcode相同的位置,在对该列表中的值进行equals()比较,若相同,更新当前存放的value值,若不相同,在列表结尾新加一个结点存放当前value值
      • 注意:hashmap的key值和value值都可以为null,并且进行添加和检索元素时需要使用equals()方法和hashcode()方法进行检索才能确定元素是否存在。
    • jdk1.7之前(链表+数组)
      【不失业计划】 Java集合框架+底层原理_第3张图片

    • jdk1.8之后(数组+链表+红黑树)

【不失业计划】 Java集合框架+底层原理_第4张图片

- 数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,**当链表长度超过阈值(8)时**,**将链表转换为红黑树**。在性能上进一步得到提升。
- **put方法简单解析**:如果存在key节点,返回旧值,如果不存在则返回Null。
- 关于红黑树学习参考:https://www.cnblogs.com/skywang12345/p/3245399.html
  • 哈希冲突

    • 但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的哈希值

    • 解决办法:

      • 链地址法:

        HashMap中的策略,即在碰撞得到的相同的结果后,用一个链表见这些结果存储起来

      • 开放定址法:

        threadlocalmap中的策略,当发生哈希冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空的位置为止

      • 再哈希法:

        又叫双哈希法,有多个不同的Hash函数,出现冲突后采用其他的哈希函数计算,直到不再冲突为止。

      • 公共溢出区法:

        创建哈希表时,将所有产生冲突的的同义词集中放在一个溢出表中。

  • 特点

    • key 用 Set 存放,所以想做到 key 不允许重复,key 对应的类需要重写 hashCode 和 equals 方法
    • 允许空键和空值(但空键只有一个,且放在第一位)
    • 元素是无序的,而且顺序会不定时改变
    • 线程不安全的,默认初始容量为16,默认加载因子的大小:0.75
2、LinkedHashMap
  • LinkedHashMap使用双向链表来维护key-value对的次序(其实只需要考虑key的次序即可),该链表负责维护Map的迭代顺序,与插入顺序一致,因此性能比HashMap低,但在迭代访问Map里的全部元素时有较好的性能。

  • **保存了记录的插入顺序,**在使用Iterator遍历LinkedHashMap时,会先得到的记录肯定是先插入的,也可以在构造时用带参数,按照应用次数排序,在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap的容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap遍历速度只与实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

3、HashTable
  • 线程安全的,HashMap的迭代器(Iterator)是快速失败(fail-fast)迭代器。HashTable不能储存Null的Key和Value。

  • HashTable和HashMap的区别

    • 继承的父类不同

      Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。

    • 线程安全性不同

      HashTable是线程安全的,HashMap是线程不安全的。Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。

    • 是否有contains方法

      HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey。

      Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。

    • 是否允许空值

      **Hashtable中,key和value都不允许出现null值。**但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。

      HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

    • 内部默认容量和扩容方式不同

      HashTable在不指定容量的情况下的默认容量为11,Hashtable扩容时,将容量变为原来的2倍加1。

      HashMap在不指定容量的情况下的默认容量为16,HashMap扩容时,将容量变为原来的2倍。

4、ConcurrentHashMap

HashMap是线程不安全的,因为在并发插入元素的时候,可能出现代换链表,让下一次的读操作进入死循环。

ConcurrentHashMap是线程安全的。在jdk1.7和1.8也有不一样的变化:

  • 1.7之前 采用Segment分段锁实现

    ConcurrentHashMap将原HashMap分配为2^n个Segment保存在Segments数组中:

【不失业计划】 Java集合框架+底层原理_第5张图片

  • case1:不同的Segment可以同时写入(并发执行)

  • case2:同一Segment的写和读可以并发执行

  • case3:同一Segment的并发写入需要上锁,会被阻塞,ConcurrentHashMap当中的每个Segment各自持有一把锁,保证了线程安全又降低了锁的粒度

  • 1.8之后 采用CAS+Synchronized

    CAS:CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

【不失业计划】 Java集合框架+底层原理_第6张图片

这里放弃了Segment而采用Node,结构基本上和Java8的HashMap一样,不过保证线程安全性。

5、TreeMap
  • 线程不同步的,基于红黑树的NavigableMap实现,能够把它保存的记录更具键进行排序,默认是按照键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时得到的记录时排过序的
  • 常考问题
    • TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。

    • TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。

    • TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。

    • TreeMap 实现了Cloneable接口,意味着它能被克隆。

    • TreeMap基于红黑树(Red-Black tree)实现。**该映射根据其键的自然顺序(字母排序)进行排序,**或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。

    • TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n)

    • TreeMap是非线程安全的。 它的iterator 方法返回的迭代器是fail-fast的。

三、工具类

1、Collections
//排序
Collections.sort(list);
//元素交换,并打印交换后的集合
Collections.swap(list, 23);
//打印集合中最大的的元素的角标
Collections.max(list);
//二分查找,查找前必须排序,如果没有找到就返回负数。Collections.sort(list);
collections.binarySearch( list, "aqwc" );
//填充,也就是把集合中所有的元素替换成新的元素
Collections.fiLL(list, "159");
//反转
Collections.sort(list, new StrLengthCompareator());collections.sort(list, collections.reverseOrder(new StrLengthCompareator()));
//同步
collections.synchronizedList(list);
//随机
Collections.shuffLe(list);
2、Arrays
//排序 : 
sort(arr);
//查找 : 
binarySearch(arr);
//比较: 用于比较两个数组元素是否数量相同,并且相同位置的元素是否相同。 另外,如果两个数组引用都是null,则它们被认为是相等的 。
equals(arr);
//填充 : 
fill(arr);
//转列表:  定长 不可添加删除新元素
arr.asList();
//转字符串 :
arr.toString();
//复制: 
copyOf();
//替换:jdk1.8之后新加方法
arr.replaceAll(UnaryOperator<E> operator)——>arr.replaceAll(a->a.equals("ss")?"张三":a);
//流处理:jdk1.8之后的新特性  新方法
stream();
3、各种转换
  • 数组和集合之间的互转

    /**
    *数组转集合
    */
    String[] array = new String[] {"zhu", "wen", "tao"};
    // 只用于遍历
    List<String> mlist = Arrays.asList(array);
    //可以使用add() remove()方法
    List<String> list = new ArrayList<String>(Arrays.asList(array));
    //或者
    List<String> list1 = new ArrayList<String>(array.length);
    Collections.addAll(list, array);
    
    
    /**
    *集合转数组
    */
    String[] array = mlist.toArray(new String[0]);
    
    

这部分资料整理比较早,当时从多个地方借鉴了资料,现在不好找回,如有相似联系删除修改。

你可能感兴趣的:(个人学习,不失业计划,学习整理,java,集合,map)