java集合框架回顾分析

符号 阅读:195 2020-02-18 16:34:58 评论:0

一.三种常用的集合List,Set,Map,其中List和Set都是Collection的子接口,Map是独立的接口。

二.List的特点:

  1)list集合中能存放重复的元素,能存放多个null。

  2)list集合是存取有序的,输出时会依据输入的元素顺序输出。

  3)list的常用实现类有ArrayList,LinkedList,Vector。

1.ArrayList的特点:

  1-1.ArrayList有三种构造方法

  1) ArrayList(),构造一个初始容量为 10 的空列表。

       2) ArrayList(int initialCapacity),构造一个具有指定初始容量的空列表。

       3) ArrayList(Collection<? extends E> c),构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。

当采用不带参数的构造方法ArrayList()生成一个集合对象时,其实是在底层调用ArrayList(int initialCapacity)这一构造方法生产一个长度为10的Object类型的数组。当采用带有集合类型参数的构造方法时,在底层生成一个包含相同的元素和长度的Object类型的数组。

  1-2.底层是通过动态数组实现的,ArrayList实例都有一个初始容量(如果不指定,就默认为10),随着向ArrayList中添加元素,其容量也会随之增长。

ArrayList的扩容机制:当输入的元素数目minCapacity超过了原数组容量oldCapacity,那么ArrayList底层会新生成一个数组,长度为原数组的1.5倍+1,并将原数组中的元素copy到新数组中,并且后续添加的元素都会放在新数组中,当新数组的长度无法容纳新添加的元素时,重复该过程。

长度扩大为1.5倍是为了避免内存浪费 ,+1是说每次开辟新内存时 都需要多开辟1为当前赋值用。

  1-3 .ArrayList 底层是基于数组来实现的,可以通过下标准确的找到目标元素,因此查找的效率高;但是添加或删除元素会涉及到大量元素的位置移动,效率低。

  1-4.ArrayList不是同步的,在多线程环境下,可以使用Collections.synchronizedList方法声明一个线程安全的ArrayList,

  例如: List arraylist = Collections.synchronizedList(new ArrayList());

2.LinkedList的特点:

  2-1.LinkedList提供了两个带不同参数的构造方法。

     1) LinkedList(),构造一个空列表。

     2) LinkedList(Collection<? extends E> c),构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。

  当调用不带参数的构造方法,header的前驱节点和后置节点都指向header自己,构成了一个双向循环的链表。当采用带有集合类型参数的构造方法时,会先调用不带参数的构造方法创建一个空的循环链表,然后将集合元素添加进去。

  2-2.底层是通过双向循环链表实现的,添加、删除操作只需要修改节点的指针信息,因此添加、删除元素的效率高,但是查找操作是从header节点开始的,从前往后或者从后往前依次逐个节点查找,因此查询效率低。

  2-3.LinkedList不是线程安全的。在多线程环境下,可以使用Collections.synchronizedList方法声明一个线程安全的ArrayList

  例如:List list = Collections.sychronizedList(new LinkedList());

3.vector与ArrayList的区别

  1)vector是同步的,是线程安全的,因此效率比ArrayList低。

  2)vector扩容的时候是直接扩大为原来的2倍。

4.ArrayList与LinkedList的区别

  ArrayList通过数组实现,因此可以直接通过搜索下标进行查找,查询效率高,而LinkedList通过双向循环链表实现,在添加、删除操作只需要修改节点的指针信息,因此添加、删除元素的效率高。

三.Set的特点:

  1)不允许元素重复,只能存放一个null。

  2)是无序的容器,无法保证存储顺序。

  3)常用的实现类有HashSet,LingkedHashSet,TreeSet。

1.HashSet的特点:

  1-1.HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变,此类允许使用null元素。

  1-2.在将对象存储在HashSet之前,要确保重写hashCode()方法和equals()方法,这样才能比较对象的值是否相等,确保集合中没有储存相同的对象。

  HashSet不能重复存储equals相同的数据 。原因就是equals相同,数据的散列码也就相同(hashCode必须和equals兼容)。大量相同的数据将存放在同一个散列单元所指向的链表中,造成严重的散列冲突,对查找效率是灾难性的。

  1-3.HashSet的存储是无序的 ,没有前后关系,他并不是线性结构的集合。

2.LinkedHashSet

  2-1.LinkedHashSet继承自HashSet,源码更少、更简单,唯一的区别是LinkedHashSet内部使用的是LinkHashMap。这样做的意义或者好处就是LinkedHashSet中的元素顺序是可以保证的,也就是说遍历序和插入序是一致的。

  2-2.同HashSet相比并没有实现新的功能(新的方法),只不过把HashSet中预留的构造方法启用了,因而可以实现有序插入

  LinkedHashMap调用父类构造方法初始化时,还顺便设置了变量accessOrder = false,看上面得源码可以知道,这是给了迭代器一个参数,false代表迭代时使用插入得顺序

  2-3.与HashSet的对比

  LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet

3.TreeSet

  3-1.TreeSet类型是J2SE中唯一可实现自动排序的类型

      3-2.TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。向  TreeSet中加入的应该是同一个类的对象。

  1)自然排序实现了Comparable接口,使用要排序元素的CompareTo(Object obj)方法来比较元素之间大小关系,然后将元素按照升序排列

   obj1.compareTo(obj2)方法如果返回0,则说明被比较的两个对象相等,如果返回一个正数,则表明obj1大于obj2,如果是 负数,则表明obj1小于obj2。

  2)自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator接口,实现 int compare(To1,To2)方法

  它实际上用的是待比较对象的compareTo(Object obj)方法;1.id.compareTo(2.id)

  3-3HashSet、TreeSet的区别

  1)HashSet是使用散列表进行存储,元素无序,元素允许为null。TreeSet是使用树结构来进行存储,元素按字符串顺序排序存储,元素不允许为null。

   2)存储时保证数据唯一性依据不同,HashSet是通过复写hashCode()方法和equals()方法来保证的,而TreeSet通过Compareable接口的compareTo()方法来保证的

四.Map的特点:

  1)Map 的 每个 Entry 都持有两个对象,也就是一个键一个值,键必须唯一,值可以重复。

  2)可以有多个null值,但只能有一个null键。

  3)常用的实现类有HashMap,LinkedHashMap,TreeMap,Hashtable。

1.HashMap的特点:

  1-1.HashMap的构造方法,HashMap是基于哈希表的Map接口实现,初始容量是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。

  1)HashMap(),构造一个默认初始容量(16),默认加载因子(0.75)的空HashMap。

  2)HashMap(int initialCapacity),构造一个指定初始容量,默认加载因子的空HashMap。

  3)HashMap(int initialCapacity,float loadFactor),构造一个指定初始容量,指定加载因子的空HashMap。

  4)HashMap(Map<?extends K, ?extends V>m),构造一个映射关系与指定Map相同的新HashMap。

  1-2.HashMap的底层是通过数组和单链表结合实现的,每个Entry对象存储的位置由key值的hashcode计算hash值,再用hash值和数组长度来决定。

  1)hashcode值的计算,重写 hashCode 方法,一般key值多为String类型

  String类中定义的hashcode()方法 

public int hashCode() { 
    int h = hash; 
    if (h == 0 && value.length > 0) { 
        char val[] = value; 
 
        for (int i = 0; i < value.length; i++) { 
            h = 31 * h + val[i]; 
        } 
        hash = h; 
    } 
    return h; 
}

 value:是一个 private final 修饰的 char 数组,String 类是通过该数组来存在字符串的。

hash:是一个 private 修饰的 int 变量,用来存放 String 对象的 hashCode。

之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5) - i, 现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。

  2)hash值的计算

static int hash(int h) { 
                  h ^= (h >>> 20) ^ (h >>> 12); 
         return h ^ (h >>> 7) ^ (h >>> 4); 
     }

保证hashCode 不同 ,这样设计保证了对象的hashCode的32位值只要有一位发生改变,整个hash()返回值就会改变,高位的变化会反应到低位里。

  3)计算索引

static int indexFor(int h, int length) { 
         return h & (length-1); 
    }

在构造函数中存在:capacity <<= 1;这样做总是能够保证HashMap的底层数组长度为2的n次方当length为2的n次方时,h&(length - 1)就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化

h&(length - 1),这句话除了上面的取模运算外还有一个非常重要的责任:均匀分布数据和充分利用空间。

所以说当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在数组中分布较均匀,查询速度也较快。

  1-3.存储的原理

  当我们向HashMap中put一对key-value键值对时,首先判断key是否为null,如果为null,则遍历table[0]处的链表,若此链表上存在key为null的元素,则用value覆盖此元素的value值,如果不存在这样的元  素,那么将此键值对生成的Entry对象存放到table[0]中,并将其next指向此处原来的Entry对象;如果key不为null,首先根据key的hashCode值计算出hash值,根据hash值和数组长度计算出要存放到数组中的位置i,然后遍历table[i]处的链表,如果链  表上存在元素其hash值与计算得到的hash值相等并且其key值与新增的key相等,那么就以新增的value覆盖此元素原来的value并返回原来的value值;如果链表上不存在满足上面条件的元素,则将key-value生  成的Entry对象存放到table[i]处,并将其next指向此处原来的Entry对象。

  1-4.当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,数组大小变为原来的2倍,因为数组长度变化,存储位置也会发生变化,转移元素就需要一个个又一次哈希到新数组中。

  1-5.要注意的是HashMap不是线程安全的,我们可以使用Collections.synchoronizedMap方法来获得线程安全的HashMap。

  例如:Map map = Collections.sychronizedMap(new HashMap());

2.LinkedHashMap的特点

  2-1在HashMap的基础上多了双向链表结构,变得存取有序。键,值不能为null。

2-2继承于HashMap,LinkedHashMap增加了两个属性用于保证迭代顺序,分别是 双向链表头结点header 标志位accessOrder (值为true时,表示按照访问顺序迭代;值为false时,表示按照插入顺序代)。

  LinkedHashMap 的存取过程基本与HashMap基本类似,只是在细节实现上稍有不同,这是由LinkedHashMap本身的特性所决定的,因为它要额外维护一个双向链表用于保持迭代顺序  

  2-3LinkedHashMap 一共提供了五个构造函数,它们都是在HashMap的构造函数的基础上实现的1)..2)..3)..4)..

  5)LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder),该构造函数意在构造一个指定初始容量和指定负载因子的具有指定迭代顺序的LinkedHashMap

  2-4.与HashMap的区别

  如果需要输出的顺序和输入的相同,那么用LinkedHashMap 可以实现,它还可以按读取顺序来排列.

3.TreeMap的特点

  3-1.TreeMap的构造方法

  (1)TreeMap():构建一个空的映像树
  (2)TreeMap(Map m): 构建一个映像树,并且添加映像m中所有元素
  (3)TreeMap(Comparator c): 构建一个映像树,并且使用特定的比较器对关键字进行排序
  (4)TreeMap(SortedMap s): 构建一个映像树,添加映像树s中所有映射,并且使用与有序映像s相同的比较器排序

  3-2.底层是红黑树,实现了SortedMap接口,会按照相关的值进行排序,因此变得存取有序。

  键,值都不允许为null。

  3-3.与HashMap的区别

  HashMap里面存入的键值对在取出的时候是随机的,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map中插入、删除和定位元素,HashMap是最好的选择。  TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。

4.线程安全的Map

4-1.Hashtable 

方法都被synchoronized修饰,当一个线程访问Hashtable的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。举个例子,当一个线程使用put方法时,另一个线程不但不可以使用put方法,连get方法都不可以

4-2.ConcurrentHashMap 

CHM引入了分割,并提供了HashTable支持的所有的功能。在CHM中,支持多线程对Map做读操作,并且不需要任何的blocking。这得益于CHM将Map分割成了不同的部分,在执行更新操作时只锁住一部分

五. 各集合的使用场景

1)如果你经常会使用索引来对容器中的元素进行访问,那么 List 是你的正确的选择。如果你已经知道索引了的话,那么 List 的实现类比如 ArrayList 可以提供更快速的访问,如果经常添加删除元素的,那么肯定要选择LinkedList。

2)如果你想容器中的元素能够按照它们插入的次序进行有序存储,那么还是 List,因为 List 是一个有序容器,它按照插入顺序进行存储。

3)如果你想保证插入元素的唯一性,也就是你不想有重复值的出现,那么可以选择一个 Set 的实现类,比如 HashSet、LinkedHashSet 或者 TreeSet。所有 Set 的实现类都遵循了统一约束比如唯一性,而且还提供了额外的特性比如 TreeSet 还是一个 SortedSet,所有存储于 TreeSet 中的元素可以使用 Java 里的 Comparator 或者 Comparable 进行排序。LinkedHashSet 也按照元素的插入顺序对它们进行存储。

4)如果你以键和值的形式进行数据存储那么 Map 是你正确的选择。你可以根据你的后续需要从 Hashtable、HashMap、TreeMap 中进行选择。

标签:java集合
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

发表评论
搜索
关注我们

扫一扫关注我们,了解最新精彩内容