目录
一、集合
1.1、集合概念
1.2、集合特点
1.3、常用的集合类
1.4、集合和数组的区别
1.5、List,Set,Map三者的区别?
1.6、集合底层的数据结构
二、Collection接口
2.1、List
2.1.1、list集合元素删除
2.1.2、集合元素判断
2.1.3、List是线程不安全的
2.1.4、ArrayList的优缺点
2.1.5、ArrayList 和 LinkedList 的区别是什么?
2.2、set集合
2.2.1、HashSet如何检查重复?HashSet是如何保证数据不可重复的?
2.2.2、hashCode()与equals()的相关规定:
2.2.3、HashSet与HashMap的区别
2.3、map集合
2.3.1、HashMap的实现原理
2.3.2、HashMap在JDK1.7和JDK1.8中有哪些不同?
2.3.3、HashMap的扩容操作是怎么实现的?
2.3.4、ConcurrentHashMap 和 Hashtable 的区别?
集合就是一个放数据的容器,准确的说是放数据对象引用的容器
集合类存放的都是对象的引用,而不是对象的本身
集合类型主要有3种:set(集)、list(列表)和map(映射)。
①集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。
②和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小
- Collection接口的子接口包括:Set接口和List接口
- Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
- Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
- List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
①数组是固定长度的;集合可变长度的。
②数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
③数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
1、Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。
Collection集合主要有List和Set两大接口
List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
1、Collection
(1) List
①Arraylist: Object数组
②Vector: Object数组
③LinkedList:双向循环链表
(2)Set
①HashSet(无序,唯一)︰基于 HashMap 实现的,底层采用HashMap来保存元素
②LinkedHashSet: LinkedHashSet继承与HashSet,并且其内部是通过LinkedHashMap来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于Hashmap 实现一样,不过还是有一点点区别的。
③TreeSet(有序,唯一):红黑树(自平衡的排序二叉树。)
2、Map
①HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的("拉链法"解决冲突) .JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
②LinkedHashMap: LinkedHashMap继承自HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
③HashTable: 数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的
④TreeMap:红黑树(自平衡的排序二叉树)
1、接口可以被继承。
2、接口可以被多次实现
1、创建list
//只能用list接口里的方法
List list1 = new ArrayList()
//可以用ArrayList里的所有方法,因为AnrayList是实现list接口,所以可以调用的方法,比第一种要多
ArrayList list2 = new ArrayList();
Student student = new Student();
2、list集合元素添加
List list = new ArrayList<>();
//下面这样写会有数组扩容的操作,会消耗性能
list.add("asa" );
list.add ( "qqq");
//如果确定了list里面要放的元紊,建议这么写,性能比较高,没有扩容操作,但是不能再使用add添加新元素
List list1 = Arrays.asList( "asa" , "qqq"");
list1.add( "222"");
System.out.println(list1.toString());
3、数组与List如何转换
①数组转 List:使用Arrays. asList(array)进行转换。
②List转数组:使用List自带的 toArray)方法。
代码实例:
// list to array
List list = new ArrayList();
list.add("123");
list.add("456");
list.toArray();
// array to list
String[] array = new String[]{"123","456"};
Arrays.asList(array);
从下面结果可知集合中的元素并没有全部删除。
public class ListDemo {
public static void main(String[] args) {
List list=new ArrayList<>();
list.add("hello");
list.add("hello");
list.add("hello");
list.add("hello");
for (int i = 0; i < list.size(); i++) {
if("hello".equals(list.get(i))){
list.remove(i);
}
}
System.out.println(list);
}
}
结果:[hello, hello]
原因如下图:
1、解决方法
①倒序删除
public class ListDemo {
public static void main(String[] args) {
List list=new ArrayList<>();
list.add("hello");
list.add("hello");
list.add("hello");
list.add("hello");
for (int i = list.size(); i >=0; i--) {
if("hello".equals(list.get(i))){
list.remove(i);
}
}
System.out.println(list);
}
}
结果是【】
②迭代器删除
利用iterator自带的remove方法进行删除,由于iterator.remove()方法删除时会自动进行数组下进行移位操作。
public static void iterator(ArrayList list) {
Iterator it = list.iterator();
while (it.hasNext()) {
Long a = it.next();
if (a == 3) {
it.remove();
}
}
System.out.println(list);
③lambada表达式删除
public static void lambda(ArrayList list) {
ArrayList delList = new ArrayList<>();
list.stream().forEach(vo -> {
if (vo == 3) {
delList.add(vo);
}
});
list.removeAll(delList);
System.out.println(list);
}
1、判断是否为空
List list=new ArrayList<>();
//使用工具类进行判断不容易出现空指针,下面是字符串的工具类
System.out.println(Strutil.isBlank("hello"));
//集合判断是否是空集合,下面是集合的工具类
System.out.println(CollectionUtil.isEmpty(list));
①线程类
public class MyThread implements Runnable{
ArrayList aaa;
public MyThread(ArrayList aaa){
this.aaa=aaa;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
aaa.add("aaa"+i);
System.out.println(Thread.currentThread().getName()+"在第"+i+ aaa.get(i));
}
}
}
②测试类
public class Test {
public static void main(String[] args) {
ArrayList list=new ArrayList<>();
MyThread m1=new MyThread(list);
MyThread m2=new MyThread(list);
new Thread(m1).start();
new Thread(m2).start();
}
}
③结果:对应的元素添加不一致
①ArrayList的优点如下:
ArrayList底层以数组实现,是一种随机访问模式。ArrayList 实现了RandomAccess接口,因此查找的时候非常快。
ArrayList在顺序添加一个元素的时候非常方便。
②ArrayList 的缺点如下:
删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能,插入元素的时候,也需要做一次元素复制操作,缺点同上。
③ArrayList 比较适合顺序添加、随机访问的场景。
①数据结构实现: ArrayList是动态数组的数据结构实现,而LinkedList 是双向链表的数据结构实现。
②随机访问效率: ArrayList 比 LinkedList在随机访问的时候效率要高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
③增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList效率要高,因为ArrayList增删操作要影响数组内的其他数据的下标。
④内存空间占用: LinkedList 比 ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
⑤线程安全: ArrayList和LinkedList都是不同步的,也就是不保证线程安全;
⑥综合来说,在需要频繁读取集合中的元素时,更推荐使用ArrayList,而在插入和删除操作较多时,更推荐使用LinkedList。
⑦LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
List是有序可重复的,set是无序不重复的。
代码演示:
public class ListDemo {
public static void main(String[] args) {
List list=new ArrayList<>();
list.add("qqq");
list.add("qqq");
list.add("qqq");
Set set=new HashSet<>();
set.add("www");
set.add("www");
set.add("www");
System.out.println(list);
System.out.println(set);
}
}
结果如下图
1、HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。
2、hashcode就是一个int的数。
两个对象的hashcode如果相等,这两个不一定相等,因为可能存在hash碰撞。但是两个对象的hashcode如果不相等,那么这两个对象绝对不相等。
- hashmap判断两个key是否相等
- 先判断这两个对象的hashcode是否相等,如果相等,再去调用equals。如果不相等,就直接认为这两个对象不相等。好处就是:equals需要比对太多元素性能较差,但Hashcode直接比较,性能较高一些。
3,两个对象的equals相等,那么这两个对象一定相等。
①向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles方法比较。
②HashSet中的add )方法会使用HashMap 的put()方法。
③HashMap的 key是唯一的,由源码可以看出 HashSet添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复(HashMap比较key是否相等是先比较hashcode 再比较equals ) 。
部分源码展示:
private static final Object PRESENT = new Object();
private transient HashMap map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
return map.put(e, PRESENT)==null;
}
①如果两个对象相等,则hashcode一定也是相同的
hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值
②两个对象相等,对两个equals方法返回true
③两个对象有相同的hashcode值,它们也不一定是相等的。
④综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
⑤hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
1、HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和nul键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2、HashMap的数据结构:在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个"链表散列"的数据结构,即数组和链表的结合体。
3、HashMap基于Hash 算法实现的
①当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
②存储时,如果出现hash值相同的key,此时有两种情况。
4、获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
5、理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
6、需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)
1、红黑树
红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红(Red)或黑(Black)。
① 红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色[注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点! ]。
②如果一个结点是红色的,则它的子结点必须是黑色的。
③每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!]
④红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。
final Node[] resize() {
Node[] oldTab = table;//oldTab指向hash桶数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空
if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值
threshold = Integer.MAX_VALUE;
return oldTab;//返回
}//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold
}
// 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂
// 直接将该值赋给新的容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 无参构造创建的map,给出默认容量和threshold 16, 16*0.75
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 新的threshold = 新的cap * 0.75
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 计算出新的数组长度后赋给当前成员变量table
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];//新建hash桶数组
table = newTab;//将新数组的值复制给旧的hash桶数组
// 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散
if (oldTab != null) {
// 遍历新数组的所有桶下标
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
// 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收
oldTab[j] = null;
// 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树
if (e.next == null)
// 用同样的hash映射算法把该元素加入新的数组
newTab[e.hash & (newCap - 1)] = e;
// 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
// e是链表的头并且e.next!=null,那么处理链表中元素重排
else { // preserve order
// loHead,loTail 代表扩容后不用变换下标,见注1
Node loHead = null, loTail = null;
// hiHead,hiTail 代表扩容后变换下标,见注1
Node hiHead = null, hiTail = null;
Node next;
// 遍历链表
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
// 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead
// 代表下标保持不变的链表的头元素
loHead = e;
else
// loTail.next指向当前e
loTail.next = e;
// loTail指向当前的元素e
// 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时,
// 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
// 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。
loTail = e;
}
else {
if (hiTail == null)
// 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
1、ConcurrentHashMap和Hashtable 的区别主要体现在实现线程安全的方式上不同。