集合可以看作是一种容器,用来存储对象信息。所有集合类都位于java.util包下,但支持多线程的集合类位于java.util.concurrent包下。
Java集合类主要由两个根接口Collection和Map派生出来的,Collection派生出了三个子接口:List、Set、Queue(Java5新增的队列),因此Java集合大致也可分成List、Set、Queue、Map四种接口体系,(注意:Map不是Collection的子接口)。
数组与集合的区别:
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实现类的性能分析:
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()+", ");
}
Queue用于模拟队列这种数据结构,队列通常是指“先进先出”(FIFO)的容器。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。
Dueue
Deque接口是Queue接口的子接口,它代表一个双端队列。LinkedList也实现了Deque接口,所以也可以被当作双端队列使用。
ArrayDeque
用数组实现的Deque;既然是底层是数组那肯定也可以指定其capacity,也可以不指定,默认长度是16,然后根据添加的元素的个数,动态扩展。ArrayDeque由于是两端队列,所以其顺序是按照元素插入数组中对应位置产生的(下面会具体说明)。
由于本身数据结构的限制,ArrayDeque没有像ArrayList中的trimToSize方法可以为自己瘦身。ArrayDeque的使用方法就是上面的Deque的使用方法,基本没有对Deque拓展什么方法。
Queue常用方法
ArrayList
ArrayList的底层实现是一个动态数组,也是我们最常用的集合,是List类的典型实现。它允许任何符合规则的元素插入甚至包括null。每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小,当数组大小不足时增长率为当前长度的50%。
随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。
ArrayList是线程不安全的,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。
具有的特点
Vector
与ArrayList相似,它的操作与ArrayList几乎一样。默认初始容量为10,当数组大小不足时增长率为当前长度的100%。
但是Vector是同步的。所以说Vector是线程安全的动态数组。他的同步是通过Iterator方法加synchronized实现的。
vector是线程安全的吗?
vector的单个操作时原子性的,也就是线程安全的。但是如果两个原子操作复合而来,这个组合的方法是非线程安全的,需要使用锁来保证线程安全。
Stack
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的性能对比
实现原理
HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。
- 数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,**当链表长度超过阈值(8)时**,**将链表转换为红黑树**。在性能上进一步得到提升。
- **put方法简单解析**:如果存在key节点,返回旧值,如果不存在则返回Null。
- 关于红黑树学习参考:https://www.cnblogs.com/skywang12345/p/3245399.html
哈希冲突
但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的哈希值
解决办法:
链地址法:
HashMap中的策略,即在碰撞得到的相同的结果后,用一个链表见这些结果存储起来
开放定址法:
threadlocalmap中的策略,当发生哈希冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空的位置为止
再哈希法:
又叫双哈希法,有多个不同的Hash函数,出现冲突后采用其他的哈希函数计算,直到不再冲突为止。
公共溢出区法:
创建哈希表时,将所有产生冲突的的同义词集中放在一个溢出表中。
特点
LinkedHashMap使用双向链表来维护key-value对的次序(其实只需要考虑key的次序即可),该链表负责维护Map的迭代顺序,与插入顺序一致,因此性能比HashMap低,但在迭代访问Map里的全部元素时有较好的性能。
**保存了记录的插入顺序,**在使用Iterator遍历LinkedHashMap时,会先得到的记录肯定是先插入的,也可以在构造时用带参数,按照应用次数排序,在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap的容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap遍历速度只与实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。
线程安全的,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倍。
HashMap是线程不安全的,因为在并发插入元素的时候,可能出现代换链表,让下一次的读操作进入死循环。
ConcurrentHashMap是线程安全的。在jdk1.7和1.8也有不一样的变化:
1.7之前 采用Segment分段锁实现
ConcurrentHashMap将原HashMap分配为2^n个Segment保存在Segments数组中:
case1:不同的Segment可以同时写入(并发执行)
case2:同一Segment的写和读可以并发执行
case3:同一Segment的并发写入需要上锁,会被阻塞,ConcurrentHashMap当中的每个Segment各自持有一把锁,保证了线程安全又降低了锁的粒度
1.8之后 采用CAS+Synchronized
CAS:CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
这里放弃了Segment而采用Node,结构基本上和Java8的HashMap一样,不过保证线程安全性。
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的。
//排序
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);
//排序 :
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();
数组和集合之间的互转
/**
*数组转集合
*/
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]);
这部分资料整理比较早,当时从多个地方借鉴了资料,现在不好找回,如有相似联系删除修改。