JAVA面试之基础

▷ 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的区别和联系

一. 结构特点:
  1. List和Set是存储单列数据的集合,Map是存储键值对这样的双列数据的集合;
  2. List中存储的数据是有顺序的,并且值允许重复;Map中存储的数据是无序的,它的键是不允许重复的,但是值是允许重复的;Set中存储的数据是无顺序的,并且不允许重复,但元素在集合中的位置是由元素的hashcode决定,即位置是固定的(Set集合是根据hashcode来进行数据存储的,所以位置是固定的,但是这个位置不是用户可以控制的,所以对于用户来说set中的元素还是无序的)。
二. 实现类:
  1. List接口有三个实现类
    1.1、LinkedList
    基于链表实现,链表内存是散列的,增删快,查找慢;
    1.2、ArrayList
    基于数组实现,非线程安全,效率高,增删慢,查找快;
    1.3、Vector
    基于数组实现,线程安全,效率低,增删慢,查找慢;
  2. Map接口有四个实现类
    2.1、HashMap
    基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键;
    2.2、HashTable
    线程安全,低效,不支持 null 值和 null 键;
    2.3、LinkedHashMap
    是 HashMap 的一个子类,保存了记录的插入顺序;
    2.4、SortMap接口
    TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序
  3. Set接口有三个实现类
    3.1、HashSet
    底层是由 Hash Map 实现,不允许集合中有重复的值,使用该方式时需要重写 equals()和 hash Code()方法;
    3.2、LinkedHashSet
    继承于 HashSet,同时又基于 LinkedHashMap 来进行实现,底层使用的是 LinkedHashMap
    3.3、TreeSet
    基于 TreeMap实现。使用元素的[自然顺序]对元素进行排序,或者根据创建 set 时提供的Comparator进行排序
三. 区别:
  1. List 集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过list.get(i)方法来获取集合中的元素;
  2. Map 中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对象可以重复;
  3. Set 集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定的方式排序,例如 Tree Set 类,可以按照默认顺序,也可以通过实现 Java.util.Comparator< Type >接口来自定义排序方式。
四. 补充:HashMapHashTable

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组成,如下图所示:


JAVA面试之基础_第1张图片
结构图

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成功。
    1. 如果没有初始化就先调用initTable()方法来进行初始化过程。
    2. 如果没有hash冲突就直接CAS插入
    3. 如果还在进行扩容操作就先进行扩容
    4. 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入
    5. 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
    6. 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
    7. 扩容过程有点复杂,这里主要涉及到多线程并发扩容,ForwardingNode的作用就是支持扩容操作,将已处理的节点和空节点置为ForwardingNode,并发处理时多个线程经过ForwardingNode就表示已经遍历了,就往后遍历 ,如下图:


      JAVA面试之基础_第2张图片
      扩容流程图
  • get
    1. 计算hash值,定位到该table索引位置,如果是首节点符合就返回
    2. 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
    3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回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,有以下几点:
    1. 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
    2. JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
    3. 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据

你可能感兴趣的:(JAVA面试之基础)