集合框架部分

  • 一、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集合的底层是链表吗 java集合底层原理_java集合的底层是链表吗

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集合的底层是链表吗 java集合底层原理_java集合的底层是链表吗_02

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集合的底层是链表吗 java集合底层原理_java集合的底层是链表吗_03

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

java集合的底层是链表吗 java集合底层原理_数组_04

- 数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,**当链表长度超过阈值(8)时**,**将链表转换为红黑树**。在性能上进一步得到提升。
- **put方法简单解析**:如果存在key节点,返回旧值,如果不存在则返回Null。
- 关于红黑树学习参考:
  • 哈希冲突
  • 但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的哈希值
  • 解决办法:
  • 链地址法:
    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集合的底层是链表吗 java集合底层原理_java集合的底层是链表吗_05

  • case1:不同的Segment可以同时写入(并发执行)
  • case2:同一Segment的写和读可以并发执行
  • case3:同一Segment的并发写入需要上锁,会被阻塞,ConcurrentHashMap当中的每个Segment各自持有一把锁,保证了线程安全又降低了锁的粒度
  • 1.8之后 采用CAS+Synchronized
    CAS:CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

java集合的底层是链表吗 java集合底层原理_集合_06

这里放弃了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, 2,3);
//打印集合中最大的的元素的角标
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]);

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