▷ java基本类型、所占字节和范围:
1.整型
类型 | 字节 | 范围 | 描述 |
---|---|---|---|
byte | 1字节 | -2^7 ~ 2^7-1 | 8位、有符号的,以二进制补码表示的整数 |
short | 2个字节 | -2^15 ~ 2^15 - 1 | 16 位、有符号的以二进制补码表示的整数 |
int | 4个字节 | -2^31 ~ 2^31 - 1 | 32位、有符号的以二进制补码表示的整数 |
long | 8个字节 | -2^63 ~ 2^63 -1 | 64 位、有符号的以二进制补码表示的整数 |
2.浮点型
类型 | 字节 | 范围 | 描述 |
---|---|---|---|
float | 4个字节 | -2^128 ~ +2^128(-3.40E+38 ~ +3.40E+38) | 单精度、32位、符合IEEE 754标准的浮点数 |
double | 8个字节 | -2^1024 ~ +2^1024(-1.79E+308 ~ +1.79E+308) | 双精度、64 位、符合IEEE 754标准的浮点数 |
3.逻辑性
类型 | 字节 | 范围 | 描述 |
---|---|---|---|
boolean | 1/8 个字节 | true/false | 一位的信息,用掩码取字节最后一位来表示 |
4.字符型
类型 | 字节 | 范围 | 描述 |
---|---|---|---|
char | 2个字节 | 0 ~ 65,535(\u0000 ~ \uffff) | 单一的 16 位 Unicode 字符,一个字符能存储下一个中文汉字 |
▷ Set、List、Map的区别和联系
一. 结构特点:
- List和Set是存储单列数据的集合,Map是存储键值对这样的双列数据的集合;
- List中存储的数据是有顺序的,并且值允许重复;Map中存储的数据是无序的,它的键是不允许重复的,但是值是允许重复的;Set中存储的数据是无顺序的,并且不允许重复,但元素在集合中的位置是由元素的hashcode决定,即位置是固定的(Set集合是根据hashcode来进行数据存储的,所以位置是固定的,但是这个位置不是用户可以控制的,所以对于用户来说set中的元素还是无序的)。
二. 实现类:
- List接口有三个实现类
1.1、LinkedList
基于链表实现,链表内存是散列的,增删快,查找慢;
1.2、ArrayList
基于数组实现,非线程安全,效率高,增删慢,查找快;
1.3、Vector
基于数组实现,线程安全,效率低,增删慢,查找慢; - Map接口有四个实现类
2.1、HashMap
基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键;
2.2、HashTable
线程安全,低效,不支持 null 值和 null 键;
2.3、LinkedHashMap
是 HashMap 的一个子类,保存了记录的插入顺序;
2.4、SortMap接口
TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序 - Set接口有三个实现类
3.1、HashSet
底层是由 Hash Map 实现,不允许集合中有重复的值,使用该方式时需要重写 equals()和 hash Code()方法;
3.2、LinkedHashSet
继承于 HashSet,同时又基于 LinkedHashMap 来进行实现,底层使用的是 LinkedHashMap
3.3、TreeSet
基于 TreeMap实现。使用元素的[自然顺序]对元素进行排序,或者根据创建 set 时提供的Comparator
进行排序
三. 区别:
- List 集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过list.get(i)方法来获取集合中的元素;
- Map 中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对象可以重复;
- Set 集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定的方式排序,例如 Tree Set 类,可以按照默认顺序,也可以通过实现 Java.util.Comparator< Type >接口来自定义排序方式。
四. 补充:HashMap
和HashTable
HashMap
是线程不安全的,HashMap
是一个接口,是 Map
的一个子接口,是将键映射到值得对象,不允许键值重复,允许空键和空值;由于非线程安全, HashMap的效率要较 HashTable
的效率高一些.
HashTable
是线程安全的一个集合,不允许 null 值作为一个 key 值或者 Value 值;
HashTable
是 sychronize(同步化),多个线程访问时不需要自己为它的方法实现同步,而 HashMap 在被多个线程访问的时候需要自己为它的方法实现同步;
▷ HashMap原理
一. Hash表:
不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1),查找、新增、删除效率十分高。
数据结构的物理存储结构只有两种:顺序存储结构
和链式存储结构
;在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组
,所以查询时,通过一个hash函数计算出存储下标,可以直接定位到要找的值,所以速度快;
如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突(哈希碰撞)
。
哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表
的方式.
二. HashMap实现原理
- HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,数组中的元素是一段链表的引用,且数组元素允许为空,即,HashMap维护的数组元素是一段段长短不一的单向链表。
- 链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
- 因此,HashMap查询效率小于等于数组,大于等于链表;添加操作,小于链表,大于数组,原因是需要遍历链表,看是否存在。
- 源代码中,inflateTable这个方法用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,所以数组长度都是2的幂次方。
- 当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容,扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。
- hashMap的数组长度一定保持2的次幂,可以保证低位全为1,而扩容后只有一位差异,也就是多出了最左位的1,这样在通过 h&(length-1)的时候,只要h对应的最左边的那一个差异位为0,就能保证得到的新的数组索引和老数组索引一致(大大减少了之前已经散列良好的老数组的数据位置重新调换),数组长度保持2的次幂,length-1的低位都为1,会使得获得的数组索引index更加均匀,降低哈希碰撞的概率。
三. 重写equals方法需要重写hashCode算法
如果我们已经对HashMap的原理有了一定了解,这个结果就不难理解了。尽管我们在进行get和put操作的时候,使用的key从逻辑上讲是等值的(通过equals比较是相等的),但由于没有重写hashCode方法,所以put操作时,key(hashcode1)-->hash-->indexFor-->最终索引位置 ,而通过key取出value的时候 key(hashcode1)-->hash-->indexFor-->最终索引位置,由于hashcode1不等于hashcode2,导致没有定位到一个数组位置而返回逻辑上错误的值null(也有可能碰巧定位到一个数组位置,但是也会判断其entry的hash值是否相等,上面get方法中有提到。)
所以,在重写equals的方法的时候,必须注意重写hashCode方法,同时还要保证通过equals判断相等的两个对象,调用hashCode方法要返回同样的整数值。而如果equals判断不相等的两个对象,其hashCode可以相同(只不过会发生哈希冲突,应尽量避免)。
四. java8改进hashMap
- HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。Hashtable是遗留类,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。
- java8,当一个链表太长的时候,HashMap会动态的将它替换成一个红黑树,这话的话会将时间复杂度从O(n)降为O(logn),hash算法越差,散列分布越不均匀,则红黑树的效果越明显。
▷ 为什么集合类不实现Cloneable和Serializable接口
克隆(cloning)或者序列化(serialization)的语义和含义是跟具体的实现相关的。因此应该由集合类的具体实现类来决定如何被克隆或者序列化
▷ Concurrenthashmap的实现(1.7和1.8)
前言
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在HashMap扩容时,会改变链表中的元素的顺序。在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,将元素从链表头部插入造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
1.7实现
ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,如下图所示:
Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。
- 初始化
ConcurrentHashMap的初始化是会通过位与运算来初始化Segment的大小,因为ssize用位于运算来计算(ssize <<=1),所以Segment的大小取值都是以2的N次方,无关concurrencyLevel的取值,当然concurrencyLevel最大只能用16位的二进制来表示,即65536,换句话说,Segment的大小最多65536个,没有指定concurrencyLevel元素初始化,Segment的大小ssize默认为16。 - put
数据插入需要通过两次Hash算法定位,Segment实现了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。 - get
ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null - size操作
第一种方案他会使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的。
第二种方案是如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回。
1.8实现
摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap
- Node
Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据,就是一个链表,但是只允许对数据进行查找,不允许进行修改 - TreeNode
TreeNode继承于Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构,他就是通过TreeNode作为存储结构代替Node来转换成黑红树。 - TreeBin
TreeBin从字面含义中可以理解为存储树形结构的容器,而树形结构就是指TreeNode,所以TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制。 - put操作
put的过程很清晰,对当前的table进行无条件自循环直到put成功。- 如果没有初始化就先调用initTable()方法来进行初始化过程。
- 如果没有hash冲突就直接CAS插入
- 如果还在进行扩容操作就先进行扩容
- 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入
- 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
- 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
-
扩容过程有点复杂,这里主要涉及到多线程并发扩容,ForwardingNode的作用就是支持扩容操作,将已处理的节点和空节点置为ForwardingNode,并发处理时多个线程经过ForwardingNode就表示已经遍历了,就往后遍历 ,如下图:
- get
- 计算hash值,定位到该table索引位置,如果是首节点符合就返回
- 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
- 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
- size
在JDK1.8版本中,对于size的计算,在扩容和addCount()方法就已经有处理了,JDK1.7是在调用size()方法才去计算,其实在并发集合中去计算size是没有多大的意义的,因为size是实时在变的,只能计算某一刻的大小,但是某一刻太快了,人的感知是一个时间段,所以并不是很精确。
1.7和1.8的区别
- JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
- JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用CAS和synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了,cas是一种无锁机制可以用预期值等来保证同步问题。
- JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
- JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,有以下几点:
- 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
- JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
- 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据