备战2020,Java基础面试题上(String、各种集合、hashMap)

点击上方Java后端技术之路”,选择“置顶或者星标

与你一起成长

一、String 原理,String 、StringBuffer、StringBuilder区别。

String是final类,属于不可变字符串,采用char数组。

StringBuffer是线程安全的,内部采用synchronized。

StringBuilder是非线程安全的。

二、String与StringBuilder拼接字符串哪个性能好,为什么?

StringBuilder性能比较好,String在拼接的时候会new出多个对象,消耗资源。尤其是在for循环下进行拼接性能差距很明显。

三、接口与抽象类的区别

1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以有非抽象的方法

2. 接口中的实例变量默认是 final 类型的,而抽象类中则不一定

3. 一个类可以实现多个接口,但最多只能实现一个抽象类

4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定

5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

四、重载与重写的区别

重载(Overload)是让类以统一的方式处理不同类型数据的一种手段,实质表现就是多个具有不同的参数个数或者类型的同名函数(返回值类型可随意,不能以返回类型作为重载函数的区分标准)同时存在于同一个类中,是一个类中多态性的一种表现(调用方法时通过传递不同参数个数和参数类型来决定具体使用哪个方法的多态性)。

重写(Override)是父类与子类之间的多态性,实质是对父类的函数进行重新定义,如果在子类中定义某方法与其父类有相同的名称和参数则该方法被重写,不过子类函数的访问修饰权限不能小于父类的;若子类中的方法与父类中的某一方法具有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法,如需父类中原有的方法则可使用 super 关键字。

五、数组与链表的区别

1.数组是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。最大的特点就是支持随机访问,但插入、删除操作也因此变得比较低效,平均情况时间复杂度为O(n)。

2.链表它并不需要一块连续的内存空间,它通过“指针”(前后节点指向)将一组零散的内存,空间可扩容,比较常用的是单链表,双链表和循环链表。和数组相比,链表更适合插入、删除操作频繁的场景,查询的时间复杂度较高。

六、介绍一下ArrayList

1、ArrayList底层数据结构是一个数组,数组元素类型为Object,对ArrayList的所有操作都是基于数组。

2、ArrayList不是线程安全的。对ArrayList进行添加元素的操作的时候是分两个步骤进行的,即第一步先在object[size]的位置上存放需要添加的元素;第二步将size的值增加1。由于这个过程在多线程的环境下是不能保证具有原子性的,因此ArrayList在多线程的环境下是线程不安全的。

如果非要在多线程的环境下使用ArrayList,就需要保证它的线程安全性,通常有两种解决办法:第一,使用synchronized关键字;第二,可以用Collections类中的静态方法synchronizedList();

3、ArrayList默认大小为10

4、ArrayList扩容机制:

第一,在add()方法中调用ensureCapacityInternal(size + 1)方法来确定集合确保添加元素成功的最小集合容量minCapacity的值。参数为size+1,代表的含义是如果集合添加元素成功后,集合中的实际元素个数。换句话说,集合为了确保添加元素成功,那么集合的最小容量minCapacity应该是size+1。在ensureCapacityInternal方法中,首先判断elementData是否为默认的空数组,如果是,minCapacity为minCapacity与集合默认容量大小中的较大值。

第二,调用ensureExplicitCapacity(minCapacity)方法来确定集合为了确保添加元素成功是否需要对现有的元素数组进行扩容。首先将结构性修改计数器加一;然后判断minCapacity与当前元素数组的长度的大小,如果minCapacity比当前元素数组的长度的大小大的时候需要扩容,进入第三阶段。

第三,如果需要对现有的元素数组进行扩容,则调用grow(minCapacity)方法,参数minCapacity表示集合为了确保添加元素成功的最小容量。在扩容的时候,首先将原元素数组的长度增大1.5倍(oldCapacity + (oldCapacity >> 1)),然后对扩容后的容量与minCapacity进行比较:① 新容量小于minCapacity,则将新容量设为minCapacity;②新容量大于minCapacity,则指定新容量。最后将旧数组拷贝到扩容后的新数组中。

七、ArrayList在遍历的时候可以删除数据吗?

for循环遍历不可以,会报ConcurrentModificationException异常,迭代器Iterator下可以。

内部有一个modCount 进行修改次数检查。

如果没checkForComodification去检查expectedModCount与modCount相等,这个程序肯定会报ArrayIndexOutOfBoundsException

八、LinkedList底层数据结构是什么?说明ArrayList,LinkedList二者区别?

1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构每个元素都包含了 上一个和下一个元素的引用,所以add/remove 只会影响到上一个和下一个元素,。

2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。

3.对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。

九、简单介绍一下HashMap?

1、负载因子0.75 , 提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小。

2、扰动函数就是解决碰撞问题,减少碰撞。

3、hasMap的容量尽量是2的倍数,这样使散列均匀,不易出现hash冲突

HashMap默认初始化大小16,数据结构是一个数组+链表+红黑树(平衡二叉树红黑节点)。

Put的时候首先拿到hash值看是否存在值如果没有值直接放入,存在则进行比较是否相等,不等生成链表,当链表大于8的时候生成红黑树。

HashMap是线程不安全的。

十、介绍一下ConcurrentHashMap

concurrentHashMap是一个线程安全的高可用HashMap。

1.8抛弃了之前的segement分段上锁,采用了CAS+synchronized来保证并发安全,并且把put的流程更加细化,而且resize的时候不会阻塞任何一个线程。

重要参数:

MIN_TRANSFER_STRIDE:意思是resize时候每个线程每次最小的搬运量(搬运多少个entry),范围会继续细分以便可以允许多个resize线程。

RESIZE_STAMP_BITS :位的个数来用来生成戳,用在sizeCtrl里面;

MAX_RESIZERS:最大参与resize的线程数量;

RESIZE_STAMP_SHIFT:用来记录在sizeCtrl中size戳的位偏移数。

Put主逻辑:

1、首先判断key\value都不能为null

2、进行自旋操作,每次把当前的table赋给tab

3、计算key的hash

4、如果当前table没有初始化,现代用initTable方法将tab初始化。

5、Tab索引i的位置元素为null,则直接使用CAS插入

6、判断是否在resize(扩容),在扩容走helpTransfer - transfer

7、没有resize走正常put

8、用synchronized针对单个节点加锁

9、判断节点hash(正常node大于0,树为-2)

10、正常节点插入,插入到队尾

11、树节点插入

initTable方法

1、自旋操作每次针对tab赋值而且判断长度

2、如果抢锁失败(sizeCtl作为自旋锁使用),则告诉cpu让出时间片(Thread.yield())

3、抢锁成功,再次检测tab的赋值以及数组长度

4、SizeCtl置为n的0.75

5、finally里面逻辑,sizeCtl赋值并解锁

addCount方法

增加表容量的计数,如果这个表需要resize但还没开始resize,则初始化transfer(这个东西用来进行resize)。如果已经开始resize了,则看看需不需要加入帮助一起resize。然后重新检查表的计数看看需不需要再次resize

transfer方法(resize用此方法):

原理就是让每一个put完的线程,或者想要put的线程到了整个表需要resize的时候都过来进入resize流水线工作,直到有一个线程说自己已经全部弄完了,方法才能返回。

以前的正常设计是resize的时候整个表就阻塞住了,但是现在resize的时候,想要操作的线程都会来参与一起resize,这么一来其他的线程就不用干等着了。

参考文档:https://www.cnblogs.com/kobebyrant/p/11296309.html

自旋锁:

自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

十一、比较各种集合

HashMap与TreeMap:

1、都是非线程安全

2、HashMap通过hashCode进行快速查找,无序;treeMap有序。

3、hashMap基于哈希实现,treeMap基于红黑树实现

4、hashMap适用于Map的插入删除和定位元素,treeMap适用于顺序遍历key

5、HashMap比TreeMap快一点,建议使用hashMap,需要排序的时候再用treeMap。

HashSet与ArrayList:

1.HashSet

1) HashSet不能够存储相同的元素,元素是否相同的判断:重写元素的equals方法。equals方法和hashCode方法必须兼容,如:equals方法判断的是用户的名字name,那么hashCode的返回的hashcode必须是name。hashcode();

2) HashSet存储是无序的,保存的顺序与添加的顺序是不一致的,它不是线性结构,而是散列结构,(通过散列表:散列单元指向链表)。因此,HashSet的查询效率相对比较高。

3) HashSet不是线程安全的,不是线程同步的。这需要自己实现线程同步:Collections.synchronizedCollection(),方法实现。

4) Haset底层用了HashMap用key做的HashSet的value,迭代器循环代码:map.keySet().iterator()

2.ArrayList

1) ArrayList中存放顺序和添加顺序是一致的。并且可重复元素。

2) 不是线程安全的,不是线程同步的。

3) ArrayList是通过可变大小的数组实现的,允许null在内的所有元素。

4) ArrayList适合通过位子来读取元素。

你可能感兴趣的:(备战2020,Java基础面试题上(String、各种集合、hashMap))