Java工程师面试1000题51-60

51、讲一讲ArrayList的内部实现。

回答这样的问题,不要光回答个皮毛,可以详细介绍一下ArrayList内部是如何实现数组的增加和删除的,要知道,数组在创建的时候长度是固定的,那么我们往ArrayList中不断添加对象的时候,它是如何管理的呢?

ArrayList内部是使用Object[]数组实现的,接下来,我们分别分析一下ArrayList的构造、add、remove、clear方法的具体实现原理(以JDK1.8为例)。

①空参构造:

elementData是ArrayList的一个属性,是一个Object[]数组,源码如下:

DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个为空的Object[]数组,源码如下:

也就是说,当我们new一个空参的ArrayList的时候,系统内部实现了一个new Object[0]的数组。

②带参构造1:

该构造函数传入一个int值,该值如果大于0,则new一个int值容量的Object[]数组赋值给elementData,如果等于0,则将EMPTY_ELEMENTDATA(源码如下)赋值给elementData,小于0抛出异常。

EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA是相同的。

③带参构造2:

如果调用构造函数的时候传入了一个Collection的子类,则将该集合转换为数组并赋值给elementData,如果这个数组的长度不等于0,则会验证一下数组的getClass是否等于Object,如果不等于则会重新强转一遍,至于为什么这样做,jdk中有注释c.toArray might (incorrectly) not return Object[] (see 6260652)see 6260652 这个编号代表JDK bug库中的编号。可以去官网查看bug详情:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652  ,如果elementData数组的getClass等于Object不做任何操作,如果不等于,则再调用Arrays的copyOf方法转为Object[]类型。

④add方法:

add方法有两个重载,分别如下:

(判断方法重载的依据:1、 必须是在同一个类中2、 方法名相同3、 方法参数的个数、顺序或类型不同4、 与方法的修饰符或返回值没有关系。只有前三条都符合了,第四条才有效)

先看第一个add方法,首先调用ensureCapacityInternal方法,将当前的size+1传递给ensureCapacityInternal方法,ensureCapacityInternal方法:

确保数组已使用长度(size)加1之后足够存下 下一个数据;修改次数modCount 标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法,增长数组,grow方法会将当前数组的长度变为原来容量的1.5倍;确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。返回添加成功布尔值。

第二个add方法其实和上面的add类似,该方法可以按照元素的位置,指定位置插入元素,具体的执行逻辑如下:

1)确保数插入的位置小于等于当前数组长度,并且不小于0,否则抛出异常
2)确保数组已使用长度(size)加1之后足够存下 下一个数据
3)修改次数(modCount)标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法,增长数组
4)grow方法会将当前数组的长度变为原来容量的1.5倍。
5)确保有足够的容量之后,使用System.arraycopy 将需要插入的位置(index)后面的元素统统往后移动一位。
6)将新的数据内容存放到数组的指定位置(index)上
⑤remove方法:

remove方法有两个重载,分别是根据索引remove和根据对象remove。

根据索引remove:1)判断索引有没有越界;2)自增修改次数;3)将指定位置(index)上的元素保存到oldValue;4)将指定位置(index)上的元素都往前移动一位;5)将最后面的一个元素置空,好让垃圾回收器回收;6)将原来的值oldValue返回(注意:调用这个方法不会缩减数组的长度,只是将最后一个数组元素置空而已。

根据对象remove:循环遍历所有对象,得到对象所在索引位置,然后调用fastRemove方法,执行remove操作

⑥clear方法:

添加操作次数(modCount),将数组内的元素都置空,等待垃圾收集器收集,不减小数组容量。

52、并发集合和普通集合如何区别?

在Java中,有普通集合、同步的集合(即线程安全的集合)、并发集合。并发集合常见的有ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque等。并发集合位于java.util.concurrent包下,是在JDK1.5之后才有的。

普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性;线程安全集合仅仅是给集合添加了synchronized(同步的)同步锁,严重影响了性能,而且对并发的效率就更低了;并发集合通过复杂的策略不仅保证了多线程的安全,又提高了并发时的效率。

53、Java中ArrayList和LinkedList、Vector的区别?

ArrayList是一个可以处理变长数组的类型,可以存放任意类型的对象。ArrayList的所有方法都是默认在单一线程下进行的,因此ArrayList不具有线程安全性。

LinkedList可以看做为一个循环双向链表,LinkedList也是线程不安全的,在LinkedList的内部实现中,并不是用普通的数组来存放数据的,而是使用结点<Node>来存放数据的,有一个指向链表头的结点first和一个指向链表尾的结点last。LinkedList的插入方法的效率要高于ArrayList,但是查询的效率要低一点。LinkedList是由一系列表项连接而成的,一个表项总是包含三部分:元素内容、前去表、后继表。

Vector也是一个类似于ArrayList的可变长度的数组类型,它的内部也是使用数组来存放数据对象的。值得注意的是Vector与ArrayList唯一的区别是,Vector是线程安全的。在扩展容量的时候,Vector是扩展为原来的2倍,而ArrayList是扩展为原来的1.5倍。

54、数组和链表产生的背景?

在计算机中要对给定的数据集进行若干处理,首要任务就是先把数据集的一部分(当数据量非常大时,可能只可以一部分一部分读取数据到内存中来处理)或者全部存储到内存中,然后再对内存中的数据进行各种处理。

当内存空间中有足够大的连续空间时,可以把数据连续的存放在内存中,各种编程语言中的数组一般都是按这种方式存储的(也可能有例外),但是,当内存中只有一些零散的可用空间时,想要连续存储数据就变得比较困难了,这时候能想到的一种解决方式就是移动内存中的数据,把离散的内存空间进行整理,聚集成一块连续的大空间,但是这种情况下就有可能要移动别人的数据了,保不齐在移动的过程中会把重要的数据丢失,另外一种,不影响别人的数据存储方式就是把集中地数据分开离散地存储到这些不连续空间中,这时候,为了能把所有数据可以联系起来,需要在前一块数据的存储空间中记录下一块数据的地址,这样只要知道第一块内存空间的地址就能环环相扣把所有数据都联系起来了。C/C++中用指针实现的链表就是这种存储形式。由上可知,内存中的存储形式可以分为连续性存储和离散型存储,他们就对应了我们常说的数组和链表。

55、数组和链表的优缺点?

数组是将元素在内存中连续存储的,它的优点是因为数据是连续存储的,内存地址连续,所以在查找数据的时候效率更高,缺点是:在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好他的空间大小,在运行的时候空间大小是无法随着你的需要进行增加和减少的,当数据量超出了申请的内存空间时,就会出现越界情况。数据量小于所申请的内存空间时,又会出现内存浪费。并且,数组在增加和插入、删除数据的时候效率比较低。

链表是动态申请的内存空间,不需要向数组那样需要提前申请好内存空间,链表只需要在用的时候申请就可以了,根据需要动态的申请或者删除空间,对于数据的增加和删除比较灵活,还有就是链表中的数据在内存中可以存储在任何位置,通过存在元素的指针来联系数据。

56、数组和链表的使用场景?

数组的使用场景:数据较少,经常做的运算是按照序号访问数据的操作,数组更容易实现,任何高级语言都支持。

链表的使用场景:对线性表的长度或者规模难以估计,频繁的做插入或者删除操作的时候使用。

57、说一下你对ConcurrentHashMap的理解。

ConcurrentHashMap是线程安全的HashMap的实现,有三个重要属性,initialCapacity默认值为16,loadFactor属性默认值0.75,concurrencyLevel属性默认值是16.其内部使用了锁分段技术,维持这锁的是Segment的数组,在Segment数组中又存放着Entity[]数组,内部使用hash算法将数据较为均匀的分布在不同锁内。

ConcurrentHashMap的put方法上没有加上synchronized,执行时,首先使用key.hashcode对key进行hash操作,得到key的hash值,hash操作的算法也和map也不同,根据此hash值计算并获取其对应的数组中的Segment对象(继承自ReentrantLock),接着调用此Segment对象的put方法来完成当前操作。ConcurrentHashMap基于concurrencyLevel划分出了多个Segment来对key-value进行存储,从而避免每次put操作都得锁住整个数组,在默认情况下,最佳情况下可运行16个线程并发无阻塞的操作集合对象,最大程度上减少并发时的阻塞现象。

ConcurrentHashMap进行get操作时,首先对key进行hash运算,基于对应的hash值找到对应的Segment对象,调用其get方法完成当前操作。而Segment的get操作会首先通过hash值和对象数组大小减1的值进行按位与操作来获取数组上对应位置的HashEntry,在这个步骤中可能会因为对象数组大小的改变,以及数组上对应位置的HashEntry产生不一致,那么ConcurrentHashMap是怎么保证的呢?那是因为对象数组大小的改变只有在put操作时可能发生,由于HashEntry对象数组对应的变量是volatile类型的,因此可以保证如果HashEntry对象数组大小发生改变,读操作可以看到最新的对象数组大小。

在获取到了HashEntry对象之后,怎么能保证它及其next属性构成的链表上的对象不会改变呢?这点ConcurrentHashMap采用了一个简单的方式,即HashEntry对象的hash、key、next属性都是final的,这就意味着没办法插入一个HashEntry对象到基于next属性构成的链表中间或者末尾。这样就保证当获取到HashEntry对象后,其基于next属性构建的链表是不会发生变化的。

ConcurrentHashMap默认情况下采用将数据分为16个段进行存储,并且16个段分别持有各自不同的锁Segment,锁仅用于put和remove等改变集合对象的操作,基于volatile及HashEntry链表的不变性实现了读取的不加锁。这些方式使得ConcurrentHashMap能够保持极好的并发支持,尤其是对于读远比插入和删除频繁的Map而言。ConcurrentHashMap采用的这些方法真可谓是对Java内存模型、并发机制有着深刻见解和牢牢掌握的体现!

58、List a = new ArrayList();和ArrayList a = new ArrayList();的区别?

List a = new ArrayList();这一句创建了一个ArrayList后把其上溯到了List了,此时a他是一个List对象了,有些ArrayList有但是List没有的属性和方法,他就不能使用了(例如ArrayList中有trimToSize方法,但是List中就没有);而ArrayList a = new ArrayList();是创建了一个ArrayList对象,保留了ArrayList的所有属性,所以需要使用ArrayList独有的方法时候,不能使用前者。

59、Comparable和Comparator接口是什么?

Comparable和Comparator接口被用来对对象集合或者数组进行排序。Comparable接口被用来提供对象的自然排序,我们可以使用它来提供基于单个逻辑的排序。Comparator接口被用来提供不同的排序算法,我们可以选择需要使用的Comparator来对给定的对象集合进行排序。

如果我们想使用Array或Collection的排序方法时,需要在自定义类里实现Java提供Comparable接口。Comparable接口有compareTo(T OBJ)方法,它被排序方法所使用。我们应该重写这个方法,如果“this”对象比传递的对象参数更小、相等或更大时,它返回一个负整数、0或正整数。但是,在大多数实际情况下,我们想根据不同参数进行排序。比如,作为一个CEO,我想对雇员基于薪资进行排序,一个HR想基于年龄对他们进行排序。这就是我们需要使用Comparator接口的情景,因为Comparable.compareTo(Object o)方法实现只能基于一个字段进行排序,我们不能根据对象排序的需要选择字段。Comparator接口的compare(Object o1, Object o2)方法的实现需要传递两个对象参数,若第一个参数比第二个小,返回负整数;若第一个等于第二个,返回0;若第一个比第二个大,返回正整数。

60、Collections类是什么?

Java.util.Collections是一个工具类仅包含静态方法,它们操作或返回集合。它包含操作集合的多态算法,返回一个由指定集合支持的新集合和其它一些内容。这个类包含集合框架算法的方法,比如折半搜索、排序、混编和逆序等。

  • 36
    点赞
  • 121
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值