Java基础面试总结(三)

  • JAVA Collections框架
    • 包含大量集合接口、接口实现方法和操作(例如排序、查找、反转、替换、复制、最大元素、最小元素等),主要提供了 List、Queue(队列)、Set(集合)、Stack(栈)、Map(映射表存放键值对)
      • Set(集合):存入Set的每个元素都必须重新定义equals()方法来确保对象的唯一性。该接口有两个实现类:HashSet和TreeSet(实现了SortedSet接口,因此其中的元素是有序的);LinkedHashSet
      • List(有序的表):按对象进入的顺序保存对象,队每个元素插入的位置、删除的位置进行精确的控制;他可以保存重复元素,LinkedList、ArrayList、Vector,stack都实现了这个接口;
      • Map(映射表):保存键值对,其中的值可以重复,但键是唯一的,不能重复。HashMap、TreeMap、LinkedMap、WeekHashMap、IdentityHashMap都实现该接口,但执行效率不同。具体而言,就是HashMap基于散列表实现,采用对象的HashCode可以快速查询。LinkedHashMap采用界面维护内部顺序,TreeMap基于红黑树的数据节后实现按序排列。
  • List
    • ArrayList 
      • ArrayList 实现了 List 中所有可选操作,并允许包括 NULL 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作其支撑数组的大小。ArrayList 是基于数组实现的,是一个动态数组,其容量能自动增长,并且用 size 属性来标识该容器里的元素个数,而非这个被包装数组的大小。每个 ArrayList 实例都有一个容量,该容量是指用来存储列表元素的数组的大小,并且它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝。因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用 ensureCapacity 操作来增加 ArrayList 实例的容量,这可以减少递增式再分配的数量。注意,此实现不是同步的。如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改.)了列表,那么它必须保持外部同步。
      • ArrayList 实现了 Serializable 接口,因此它支持序列化,能够通过序列化传输。阅读源码可以发现,ArrayList内置的数组用 transient 关键字修饰,以示其不会被序列化。当然,ArrayList的元素最终还是会被序列化的,在序列化/反序列化时,会调用 ArrayList 的 writeObject()/readObject() 方法,将该 ArrayList中的元素(即0…size-1下标对应的元素) 和 容量大小 写入流/从流读出。这样做的好处是,只保存/传输有实际意义的元素,最大限度的节约了存储、传输和处理的开销。
      • ArrayList 实现了 RandomAccess 接口, 支持快速随机访问,实际上就是通过下标序号进行快速访问。
      • ArrayList 实现了Cloneable接口,能被克隆。Cloneable 接口里面没有任何方法,只是起一个标记作用,表明当一个类实现了该接口时,该类可以通过调用clone()方法来克隆该类的实例。
      • ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用 Collections.synchronizedList(List l) 函数返回一个线程安全的ArrayList类,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。
      • 向 ArrayList 中增加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度。如果超出,ArrayList 将会进行扩容,以满足添加数据的需求。数组扩容通过一个 public 方法 ensureCapacity(int minCapacity) 来实现 : 在实际添加大量元素前,我也可以使用 ensureCapacity 来手动增加 ArrayList 实例的容量,以减少递增式再分配的数量。
      • ArrayList 进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长为其原容量的 1.5 倍 + 1。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用 ensureCapacity 方法来手动增加ArrayList实例的容量。
      • 小结关于ArrayList的源码,总结如下:
        • 三个不同的构造方法。无参构造方法构造的ArrayList的容量默认为10; 带有Collection参数的构造方法的实现是将Collection转化为数组赋给ArrayList的实现数组elementData。
        • 扩充容量的方法ensureCapacity。ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量)。之后,用 Arrays.copyof() 方法将元素拷贝到新的数组。从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList,否则建议使用LinkedList。
        • ArrayList 的实现中大量地调用了Arrays.copyof() 和 System.arraycopy()方法 。Arrays.copyof()有重新创建一个长度为newlength的数组,然后调用System.arraycopy()。底层时c语言的memmove函数,保证数组元素的正确复制和移动,效率高,推荐使用。
    • LinkedList
      • LinkedList 是 List接口的双向链表实现。LinkedList 实现了 List 中所有可选操作,并且允许所有元素(包括 null)。除了实现 List 接口外,LinkedList 为在列表的开头及结尾进行获取(get)、删除(remove)和插入(insert)元素提供了统一的访问操作,而这些操作允许LinkedList 作为Stack(栈)、Queue(队列)或Deque(双端队列:double-ended queue)进行使用。
      • LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输;与 ArrayList 不同,LinkedList 没有实现 RandomAccess 接口,不支持快速随机访问。
    • ArrayList与LinkedList比较
    • ArrayList是基于数组的实现,LinkedList是基于带头结点的双向循环链表的实现;
    • ArrayList支持随机访问,LinkedList不支持;
    • LinkedList可作队列和栈使用,实现了Dequeue接口,而ArrayList没有;
    • ArrayList 寻址效率较高,插入/删除效率较低;LinkedList插入/删除效率较高,寻址效率较低
  • map
    • HashMap
      • (1)、对NULL键的特别处理:HashMap 中可以保存键为NULL的键值对,且该键值对是唯一的。若再次向其中添加键为NULL的键值对,将覆盖其原值。此外,如果HashMap中存在键为NULL的键值对,那么一定在第一个桶中。
      • (2)、HashMap 中的哈希策略
        • 使用 hash() 方法用于对Key的hashCode进行重新计算,以防止质量低下的hashCode()函数实现。由于hashMap的支撑数组长度总是 2 的倍数,通过右移可以使低位的数据尽量的不同,从而使Key的hash值的分布尽量均匀;
        • 使用 indexFor() 方法进行取余运算,以使Entry对象的插入位置尽量分布均匀
      • (3)、HashMap 的底层数组长度为何总是2的n次方?
        • 当底层数组的length为2的n次方时,h&(length - 1) 就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化
      • (4)、初始容量16,两倍扩容,先添加再检查
      • (5)、HashMap在jdk1.8之后引入了红黑树的概念,表示若桶中链表元素超过8时,会自动转化成红黑树;若桶中元素小于等于6时,树结构还原成链表形式。
        • 原因:红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
        • 还有选择6和8的原因是:中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
      • 初始容量 和 负载因子,这两个参数是影响HashMap性能的重要参数。其中,容量表示哈希表中桶的数量 (table 数组的大小),初始容量是创建哈希表时桶的数量;负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。若负载因子越大,那么对空间的利用更充分,但查找效率的也就越低;若负载因子越小,那么哈希表的数据将越稀疏,对空间造成的浪费也就越严重。系统默认负载因子为 0.75,这是时间和空间成本上一种折衷,一般情况下我们是无需修改的。
      • HashMap 很少会用到equals方法,因为其内通过一个哈希表管理所有元素,利用哈希算法可以快速的存取元素。当我们调用put方法存值时,HashMap首先会调用Key的hashCode方法,然后基于此获取Key哈希码,通过哈希码快速找到某个桶,这个位置可以被称之为 bucketIndex。如果两个对象的hashCode不同,那么equals一定为 false;否则,如果其hashCode相同,equals也不一定为 true。所以,理论上,hashCode 可能存在碰撞的情况,当碰撞发生时,这时会取出bucketIndex桶内已存储的元素,并通过hashCode() 和 equals() 来逐个比较以判断Key是否已存在。如果已存在,则使用新Value值替换旧Value值,并返回旧Value值;如果不存在,则存放新的键值对<Key, Value>到桶中。因此,在 HashMap中,equals() 方法只有在哈希码碰撞时才会被用到。
      • 随着HashMap中元素的数量越来越多,发生碰撞的概率将越来越大,所产生的子链长度就会越来越长,这样势必会影响HashMap的存取速度。为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理,该临界点就是HashMap中元素的数量在数值上等于threshold(table数组长度*加载因子)。
      • HashMap 的底层数组长度为何总是2的n次方?扩容:resize 包含初始化和扩容https://www.bilibili.com/video/av50449402?from=search&seid=1170539893142288868
        • 不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,空间利用率较高,查询速度也较快;
        • h&(length - 1) 就相当于对length取模,而且在速度、效率上比直接取模要快得多,即二者是等价不等效的,这是HashMap在速度和效率上的一个优化。
      • 1.7之前时数组+链表,1.8之后数组+红黑树,在插入元素时会判断时链表结构还是树结构(链表超过8,就会改成红黑树结构.1.8中新元素会插入到链表的尾部,还有就是避免死锁;在扩容是,不会再进行移位异或操作,直接看hash之后的某一位,如果为1,直接index=index+扩容前大小;若为0,直接解释index=原index),然后进行相应的操作。因为当插入元素过多时,链表长度会非常长,导致查询效率降低,红黑树提高查询效率
      • 1.7在插入一个节点的时候,需要将其的next指针指向数组位置(hash之后的桶),然后整条链表中新插入节点作为头节点(数组桶的位置)
      • 一个节点包含4个变量 key value next hashcode
      • 在初始化时可以指定容量,15的话初始化16,30的话初始化32,初始化值永远是2的平方倍,在底层hash计算桶的位置时,是hash出来的h&(length-1)。在算出hashcode之后会右移或异或运算,让一个hashcode的高4位和第四位都参与运算,让结果更散列,提高元素分布的均匀性,插入效率高,查询效率低
    • LinkedHashMap
      • 将hashmap和双向链表向结合。每个节点除了key value hash next(前者就是hashmap的节点结构) before after(后者用于维护双向链表结构)在一个entey插入时,先拆入一个哈希表(数组+单链表),再将其插入到一个双向链表中。二者相互独立。
      • hashmap写入慢,读取快;linkedhashmap是个有序链表读取慢,写入快;如果需要输出的顺序和输入的相同,那么用LinkedHashMap 可以实现,它还可以按读取顺序来排列.
    • ConcurrentHashMap
      • ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作。
      • HashMap进行扩容重哈希时导致Entry链形成环。一旦Entry链中有环,势必会导致在同一个桶中进行插入、查询、删除等操作时陷入死循环。原因就是当线程1对hash进行扩容时,链表的相对位置进行的调换,从而导致线程2再进行操作的时候出现了循环链表
      • 对整个hashmap加锁导致同步--->hashtable(Hashtable是线程安全的,它的每个方法中都加入了Synchronize方法)
      • 1.7里面有2个数组,sgement对象包含的数组就是真正存元素的数组(hashEntry)。可以理解为ConcurrentHashMap包含很多小的hashmap,所有小的hashmap共同使用一把锁;当插入一个新的kv时,先判断进入那个segment数组里,然后再判断再进入那个hashmap里。初始化数组(和hashmap一样的存元素的数组)大小和负载因子还有并发等级,其中包含先初始化segment数组,然后初始化里面的hashentry。当ssize(初始为1)<并发等级(默认为16)时,ssize左移移位,1-2-4-8-16。总是2的平方倍。在创建了segment数组对象后,当每一个hashentry进入时先加锁(segment对象继承了ReentrantLock接口),其余和hashmap类型类似;本质上,ConcurrentHashMap就是一个Segment数组,而一个Segment实例则是一个小的哈希表。由于Segment类继承于ReentrantLock类,从而使得Segment对象能充当锁的角色,这样,每个 Segment对象就可以守护整个ConcurrentHashMap的若干个桶,其中每个桶是由若干个HashEntry 对象链接起来的链表。通过使用段(Segment)将ConcurrentHashMap划分为不同的部分,ConcurrentHashMap就可以使用不同的锁来控制对哈希表的不同部分的修改,从而允许多个修改操作并发进行, 这正是ConcurrentHashMap锁分段技术的核心内涵。进一步地,如果把整个ConcurrentHashMap看作是一个父哈希表的话,那么每个Segment就可以看作是一个子哈希表,如下图所示;注意,假设ConcurrentHashMap一共分为2^n个段,每个段中有2^m个桶,那么段的定位方式是将key的hash值的高n位与(2^n-1)相与。在定位到某个段后,再将key的hash值的低m位与(2^m-1)相与,定位到具体的桶位。

      • 1.8中取消segment对象,直接在数组中的每个链表的头节点加锁(synchronzied)
      • 写操作:put在链头加元素,remove/clear不会对原有链进行修改
        • put:定位段 —> 对段加锁 ——> 扩容检查 ——> Key检查 ——> 链头插入
          • 先检查key是否不为空,然后对key.hashcode进行再哈希,并根据该值与并发级别(2^n)的高n位定位至某一段,然后将put操作委托给该段。该段在put一条记录时,会首先定位至段中某一特定的桶并对该段上锁,然后检查该段是否需要扩容,并查找该单链表中是否已经存在指定Key,若存在,则更新Value值;否则,将其插入都桶中第一个节点,并更新该段的节点的个数Count,解锁。
        • remove:定位段 —> 对段加锁 ——> Key检查 ——>Entry链复制(原始链表并没有被修改)——> 删除成功

        • clear:对segments数组中的每个段进行清空 -> 每个Segments中的桶置空(原始链表任然存在)
    • ConcurrentHashMap,HashMap 与 HashTable
      • 本质:三者都实现了Map接口,ConcurrentHashMap和HashMap是AbstractMap的子类,HashTable是Dictionary的子类;
      • 线程安全性:HashMap 是线程不安全的,但 ConcurrentHashMap 和 HashTable 是线程安全的,但二者保证线程安全的策略不同;前者采用的是分段锁机制,默认理想情况下,可支持16个线程的并发写和任意线程的并发读,效率较高;HashTable 采用的是同步操作,效率较低
      • 键值约束:HashMap 允许键、值为 null,但 ConcurrentHashMap 和 HashTable 既不允许键为null,也不允许值为 null;
      • 哈希策略:三者哈希策略不同,HashTable是key.hashCode取余;ConcurrentHashMap与HashMap都是先对hashCode进行再哈希,然后再与(桶数 - 1)进行取余运算,但是二者的再哈希算法不同;
      • 扩容机制:扩容检查机制不同,ConcurrentHashMap 和 HashTable 在插入元素前检查,HashMap 在元素插入后检查;
      • 初始容量:HashTable 初始容量 11,扩容 2倍 + 1;HashMap初始容量16,扩容2倍
  • set
    • Set不包含重复的元素,这是Set最大的特点,也是使用Set最主要的原因。常用到的Set实现有 HashSet,LinkedHashSet 和 TreeSet。一般地,如果需要一个访问快速的Set,你应该使用HashSet;当你需要一个排序的Set,你应该使用TreeSet;当你需要记录下插入时的顺序时,你应该使用LinedHashSet。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值