大家好!我是仰望星空的鑫。今天我们来聊一聊Java中的集合。
为什么我们要学习集合呢?其实主要有两大目的,
-
是方便我们用集合中的API来优化我们的代码。
-
是我们可以通过阅读集合源码来学习借鉴jdk是如何写出优雅代码的。
集合框架(collections framework)
两大基类Collection与Map
Collection
Map
Collection接口
1.List接口
2.Set接口
Map接口
Map是java中的一个接口,常见的实现类有Hashmap,TreeMap,LinkedHashMap,ConcurrenHashMap。
HashMap底层是通过数组+链表+红黑树
TreeMap底层是红黑树实现
LinkedHashMap底层是数组+链表+红黑树+双向链表
集合框架(collections framework)
首先大家要先知道什么是集合,集合和数组有什么区别呢?其实集合存储的就是一些对象,但是数组的长度不能改变,集合的长度是可以改变的。我们也可以把集合想成一种微型的数据库。那么我们 *** 作集合的目的就是实现“增删改查”功能。
两大基类Collection与Map在集合框架的类继承体系中,有两个基类接口,分别是:
-
Collection表示一组纯数据
-
Map表示一组key-value对
由图可以看出collection集合有三个接口
-
set:表示不允许有重复元素的集合。
-
List:允许有重复元素的集合。
-
Queue:主要用于存储数据,不能改变数据。
由图可以看出Map集合有四个接口:
-
把map的内容看做key集合
-
把map的内容看作value的集合
-
把map的内容看做key—value映射的集合
Collection接口
Collection接口有两个主要的子接口,set接口和list接口,(注意,map不是collection的子接口)
1.List接口特点:
1、代表一个有序集合
2、允许存放重复元素
3、集合中的每个元素都有其对应的顺序索引,可以通过索引来查找元素。(这点类似于java中的数组)
4、实现list接口的子就接口有:Arraylist,Linkedlist,Vector,Stack。
(1)ArrayList
ArrayList是一个动态数组(具有数组的属性),也是我们最常用的集合。它允许任何符合规则的元素插入甚至包括null。每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容 *** 作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容 *** 作而浪费时间、效率。
ArrayList是可以随机访问的,同时ArrayList是非同步的(及多线程情况下是线程不安全的)
(2)LinkedList
Linklist是一个双向链表实现的,所以他具有链表的一些优点(查询慢,但是增删快)。同时他也是非同步的,如果多个线程共享一个数据使用,就会出现线程不安全的情况,如果想避免该情况的发生,一种方法就是创建List时同时构造一个同步list。
(3)Vector
与ArrayList相同,但是他是线程安全的。所以他是一种线程安全的动态数组。
2.Set接口Set是一种不包括重复元素的Collection。它维持它自己的内部排序,所以随机访问没有任何意义。与List一样,它同样允许null的存在但是仅有一个。由于Set接口的特殊性,所有传入Set集合中的元素都必须不同,同时要注意任何可变对象,如果在对集合中元素进行 *** 作时,导致e1.equals(e2)==true,则必定会产生某些问题。Set接口有三个具体实现类,分别是散列集HashSet、链式散列集LinkedHashSet和树形集TreeSet。
需要注意的是:虽然Set中元素没有顺序,但是元素在set中的位置是由该元素的HashCode决定的,其具体位置其实是固定的。
(1)HashSet
HashSet 是一个没有重复元素的集合。它是由HashMap实现的,不保证元素的顺序(这里所说的没有顺序是指:元素插入的顺序与输出的顺序不一致),而且HashSet允许使用null 元素。HashSet是非同步的,如果多个线程同时访问一个哈希set,而其中至少一个线程修改了该set,那么它必须保持外部同步。 HashSet按Hash算法来存储集合的元素,因此具有很好的存取和查找性能。
(2)LinkedHashSet
LinkedHashSet继承自HashSet,其底层是基于LinkedHashMap来实现的,有序,非同步。LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。
(3)TreeSet
TreeSet是一个有序集合,其底层是基于TreeMap实现的,非线程安全。TreeSet可以确保集合元素处于排序状态。
Map接口
讲解Map接口,其实就是讲解Map接口的实现类,首先Map与Collection集合是并列关系,Map是保存具有映射关系的数据:Key-value;
Map中的key,value可以是任何引用类型的数据,他们的值会封装到HashMap$Node中去
public static void main(String[] args) {
Map map = new HashMap();
map.put("no1","渣渣鑫");
map.put("no2","渣渣鑫");
System.out.println("map"+map);
}
Map中的key不允许重复,如果重复了,就会覆盖之前key对应的值,但是value可以重复,大家看代码可以发现,value可以重复,同时你会发现,map集合是无序的,输出顺序与输入顺序不同。
这个与源码有关。
Map的key值可以为null,但是只能有一个为null,value可以有多个null值。
Map与List和Set集合不同,Map是通过键值对的映射来存储数据的,每一个Key对应一个Value,所以它不能存储相同的key值,但是value可以相同。
Map是java中的一个接口,常见的实现类有Hashmap,TreeMap,LinkedHashMap,ConcurrenHashMap。 HashMap底层是通过数组+链表+红黑树我们new一个HashMap的时候会发生什么呢?首先Hashmap有多种构造方法,但是最重要的一个是指定初始值的大小和负载因子。如果不指定的话,默认初始容量为16,负载因子为0.75
问题:初始化容量值和负载因子是什么?
这二者的作用与HashMap的扩容机制密切相关,所以我先讲一下关于HashMap的扩容机制。
首先将(key1,val1)直接放入Node类型的数组中去,此时没有给构造方法传入任何参数,默认数组初始化容量是16,默认的负载因子是0.75,当在集合中存放数据的时候,首先先看一下哈希表中的数组的数量是否达到计算出来的阈值【初始化容量*负载因子(16*0.75=12)】,也就是说当存入数据大于12的时候就会开启自动扩容机制(初始化容量为2的幂次),扩容后的容量为原容量的两倍。
问题:HashMap的初始化容量必须是2的幂次方吗?2的奇数或者2的倍数不可以吗?
- 2的幂次方:hashmap在确定元素落在数组的位置的时候,计算方法是(n - 1) & hash,n为数组长度也就是初始容量 。hashmap结构是数组,每个数组里面的结构是node(链表或红黑树),正常情况下,如果你想放数据到不同的位置,肯定会想到取余数确定放在那个数组里,计算公式:hash % n,这个是十进制计算。在计算机中, (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,计算更加高效。
- 奇数不行:在计算hash的时候,确定落在数组的位置的时候,计算方法是(n - 1) & hash,奇数n-1为偶数,偶数2进制的结尾都是0,经过hash值&运算后末尾都是0,那么0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这样就会造成空间的浪费而且会增加hash冲突。
问题:HashMap的负载因子必须是0.75吗?
负载因子太大:
负载因子的大小决定了扩容阈值的大小,当负载因子变大,扩容发生的频率就会变低,发生hash冲突的几率就会比较大。【哈希冲突:简单理解就是参与哈希运算的数据是无限的,但是计算后的结果是有限的,因此总会存在不同数据经过哈希运算得出相同的结果,这就是哈希冲突】,哈希冲突导致的结果就是数组中的某一条链表比较长,会影响性能。
负载因子太小:
当负载因子太小的时候,扩容频率比较高,浪费的空间也比较多,发生Hash冲突的几率小,但是空间浪费严重。比如负载因子为0.5的时候,hashmap的初始容量是128,当数量达到65的时候就会触发扩容机制,扩容后的容量为256,256里面只存储了65个元素,其余的全部浪费了。
问题:哈希算法是如何计算key值得?
他就是先算出正常的哈希值,然后与高16位做异或运算,产生最终的哈希值。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
问题:HashMap是如何存入数据的
首先根据Key值进行哈希算法得到index的值,相当于是索引值,如果该值没有与其他索引值产生碰撞,就直接放入数组中去,如果产生了哈希冲突,就需要判断目前数据结构采用的是链表还是红黑树的存储,根据不同的情况进行插入。
问题:什么时候用链表什么时候用红黑树
当数组的大小大于64且链表的大小大于8的时候才会将链表改为红黑树,当红黑树的值小于6的时候,会退化为链表。
问题:关于HashMap线程安全问题?
HashMap是线程不安全的,在多线程情况下,put *** 作会导致数据不一致,扩容的时候,在jdk1.8之前采用的是头插法,当两个线程同时检测到hashmap需要扩容,在进行同时扩容的时候可能会造成链表的循环。在1.8之后就不存在这个问题了,因为当链表长度大于8的时候就会转变成红黑树。
问题:如何解决线程不安全的问题呢?
使用HashTable代替HashMap,因为HashTable在外层套了synchronize,这种被包装出来的效率都比较低效,所以我们用的最多的线程安全的集合是ConcurrenHashMap;ConcurrenHashMap的底层数据结构是数组+链表/红黑树。他支持高并发的访问和更新,是线程安全的。
TreeMap底层是红黑树实现TreeMap的key不能为null,如果为null会报空指针异常。
LinkedHashMap底层是数组+链表+红黑树+双向链表如果大家想了解更多的编程知识,欢迎大家关注渣渣鑫的公众号!和渣渣鑫一起进步!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)