基础部分面经

目录

一、Java集合

1.数组与List的区别?

2.常见的集合有哪些?

3.1HashMap为什么不安全?

3.2HashMap的底层数据结构是什么?

3.3为什么解决hash冲突时候,不直接用红黑树?而选择先用链表,再转红黑树?

3.4为什么链表转红黑树的阈值是8?

3.5 那为什么转回链表节点是用的6而不是复用8?

3.6重要属性:

3.7 延申——为什么负载因子初始值是0.75而不是其他的?

3.8 HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?

3.8 2次方出处:

3.9 HashMap 的容量必须是 2 的 N 次方,这是为什么?

3.10 HashMap 的默认初始容量是 16,为什么是16而不是其他的?

3.11 HashMap 的插入流程是怎么样的?

3.12刚开始有个计算 key 的 hash 值,是怎么设计的?

3.13 为什么要将hashCode的高16位参与运算?

3.14 扩容机制流程:

3.15 扩容后定位,红黑树和链表都是通过 e.hash & oldCap == 0 来定位在新表的索引位置,这是为什么?

3.16HashMap是线程安全的吗?

3.17 介绍下死循环

3.18 ★★★介绍下JDK1.8主要做了哪些优化?

4. 除了HashMap,还用过哪些Map,在使用时这么选择?

5.TreeMap

5.1TreeMap概述:

6.CorurrentHashMap

6.1基础概念:

二、字符串相关

1.String、StringBuffer、StringBuilder的区别

三、其它基础知识点

1.public、private、protected的区别?

2.Arraylist与 LinkedList 异同点?

3.深拷贝和浅拷贝?

4.零拷贝

5.“==”和equals的区别?

6.CopyOnWriteArrayList的底层原理是怎样的?

7.Lamda表达式介绍下?在哪儿用过,为什么要用Lambda表达式? 

8.Windows下的安装包为什么在Linux下不能用?核心原因是什么?

四、Java特性

1.怎么理解封装、继承、多态?

2.Integer和Int的区别?为什么要对基础类型设计封装类型?


一、Java集合

1.数组与List的区别?

区别数组(Array)集合(List )
存储结构连续;array其实记录的是数组的首地址,array(1)就是地址基础上加1;不连续;每个list节点都有着一个Next属性,指向下一个节点的地址;
空间扩展固定大小,扩展会报错可扩展
存储内容

基本类型、对象类型

Array数组在存放的时候一定是同种类型的元素

对象类型

ArrayList就不一定了,因为ArrayList可以存储Object

2.常见的集合有哪些?

 java集合分三种,List、Set、Map,这三种集合适用于不同的场景

适用场景实现类型
List适用于有序,可重复的集合

(1)ArrayList:数组实现的,常用于查询,因为他不需要移动指针;

(2)LinedList: 链表实现的,常用与增删改查,因为他不需要移动数据;

Set适用于不可重复集合

(1)HashSet:哈希表实现的, 数据无序, 可以放一个Null值,存储单列数据

(2)TreeSet:二叉树实现的,数据自动排序,不允许放null值,存储单列数据

Map适用于键值对的存储

(1)TreeMap: 二叉树实现的,数据有序,HashTable 与 HashMap无序;

(2)HashMap线程不安全,效率快,适用于单线程操作

(3)HashTable线程安全因为底层都加了synchronized关键字来确保线程同步,适用于多线程操作

3.1HashMap为什么不安全?

        主要体现在两个方面:第一是在jdk1.7中,当并发执行扩容操作时候会造成环形链表和数据丢失的情况;其次是在jdk1.8在并发执行put操作的时候会发生数据覆盖的情况。

总结

  • List、Set实现Collection接口, Map是个顶级接口
  • List、Set存储单列数据,Map存储键值对
  • List有序可重复; Set不可重复并且最多只能存一个Null;Map键值对存储并且键不可重复,值可以重复

 

 

3.2HashMap的底层数据结构是什么?

        HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。(其实所谓Map其实就是保存了两个对象之间的映射关系的一种集合

        在JDK1.7 中,由数组+链表组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。

        在JDK1.8 中,由数组+链表+红黑树组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:(1)当链表超过 8 且数据总量超过 64 才会转红黑树。(2)将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。

对于移除,当同一个索引位置的节点在移除后达到 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点(untreeify)。

3.3为什么解决hash冲突时候,不直接用红黑树?而选择先用链表,再转红黑树?

        因为红黑树需要左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于8个的时候,此时做查询操作,链表结构已经能够保障查询性能啦。当元素大于8个的时候,红黑树搜索时间复杂度是O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的

3.4为什么链表转红黑树的阈值是8?

        我们平时在进行方案设计时,必须考虑的两个很重要的因素是:时间和空间。对于 HashMap 也是同样的道理,简单来说,阈值为8是在时间和空间上权衡的结果。

        红黑树节点大小约为链表节点的2倍(节点本身大小),在节点太少时,红黑树的查找性能优势并不明显,付出2倍空间的代价作者觉得不值得。理想情况下,使用随机的哈希码,节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的公式计算,链表中节点个数为8时的概率为 0.00000006(跟大乐透一等奖差不多,中大乐透?不存在的),这个概率足够低了,并且到8个节点时,红黑树的性能优势也会开始展现出来,因此8是一个较合理的数字。


3.5 那为什么转回链表节点是用的6而不是复用8?

        如果我们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8徘徊时,就会频繁进行红黑树和链表的转换,造成性能的损耗。

3.6重要属性:

属性值功能
size表示已经存储的节点(entry)个数

threshold

扩容阈值

HashMap的个数到达这个值,触发扩容。

在我们新建 HashMap 对象时, threshold 还会被用来存初始化时的容量。HashMap 直到我们第一次插入节点时,才会对 table 进行初始化,避免不必要的空间浪费。

loadFactor

负载因子

代表了table的填充度有多少,默认是0.75 扩容阈值=容量*负载因子

3.7 延申——为什么负载因子初始值是0.75而不是其他的?

        这个也是在时间和空间上权衡的结果。如果值较高,例如1,此时会减少空间开销,但是 hash 冲突的概率会增大,增加查找成本;而如果值较低,例如 0.5 ,此时 hash 冲突会降低,但是有一半的空间会被浪费,所以折衷考虑 0.75 似乎是一个合理的值。如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值 。 相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值

/**已经存储的节点(entry)个数*/
transient int size;

/**扩容阈值,HashMap的个数到达这个值,触发扩容。 当table=={}时,该值为初始容量(初始容量默认16);当table被填充了,也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到
*/
int threshold;

/**负载因子,代表了table的填充度有多少,默认是0.75,  扩容阈值=容量*负载因子
负载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。
所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。
*/
final float loadFactor;

/**HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时,
如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),
需要抛出异常ConcurrentModificationException*/
transient int modCount;

3.8 HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?

        默认初始容量是16。HashMap 的容量必须是2的N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传 9,容量为16。

3.8 2次方出处:

 解释:|=(或等于):这个符号比较少见,但是“+=”应该都见过,看到这你应该明白了。例如:a |= b ,可以转成:a = a | b。

        >>>(无符号右移):例如 a >>> b 指的是将 a 向右移动 b 指定的位数,右移后左边空出的位用零来填充,移出右边的位被丢弃。

                

假设 n 的值为 0010 0001,则该计算如下图:

        可以看出这5个公式会通过最高位的1,拿到2个1、4个1、8个1、16个1、32个1。当然,有多少个1,取决于我们的入参有多大,但我们肯定的是经过这5个计算,得到的值是一个低位全是1的值,最后返回的时候 +1,则会得到1个比n 大的 2 的N次方。

        这时再看开头的 cap - 1 就很简单了,这是为了处理 cap 本身就是 2 的N次方的情况。

        计算机底层是二进制的,移位和或运算是非常快的,所以这个方法的效率很高。

3.9 HashMap 的容量必须是 2 的 N 次方,这是为什么?

        计算索引位置的公式为:(n - 1) & hash,当 n 为 2 的 N 次方时,n - 1 为低位全是 1 的值,此时任何值跟 n - 1 进行 & 运算的结果为该值的低 N 位,达到了和取模同样的效果,实现了均匀分布。实际上,这个设计就是基于公式:x mod 2^n = x & (2^n - 1),因为 & 运算比 mod 具有更高的效率。

3.10 HashMap 的默认初始容量是 16,为什么是16而不是其他的?

        16是2的N次方,并且是一个较合理的大小。如果用8或32,我觉得也是OK的。实际上,我们在新建 HashMap 时,最好是根据自己使用情况设置初始容量这才是最合理的方案。

3.11 HashMap 的插入流程是怎么样的?

3.12刚开始有个计算 key 的 hash 值,是怎么设计的?

        拿到 key 的 hashCode,并将 hashCode 的高16位和 hashCode 进行异或(XOR)运算,得到最终的 hash 值。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

3.13 为什么要将hashCode的高16位参与运算?

        主要是为了在 table 的长度较小的时候,让高位也参与运算,并且不会有太大的开销。

3.14 扩容机制流程:

3.15 扩容后定位,红黑树和链表都是通过 e.hash & oldCap == 0 来定位在新表的索引位置,这是为什么?

扩容前 table 的容量为16,a 节点和 b 节点在扩容前处于同一索引位置。

扩容后,table 长度为32,新表的 n - 1 只比老表的 n - 1 在高位多了一个1(图中标红)。

扩容前
扩容前
扩容后

       两个节点在老表是同一个索引位置,因此计算新表的索引位置时,只取决于新表在高位多出来的这一位(图中标红),而这一位的值刚好等于 oldCap。

        因为只取决于这一位,所以只会存在两种情况:1)  (e.hash & oldCap) == 0 ,则新表索引位置为“原索引位置” ;2)(e.hash & oldCap) != 0,则新表索引位置为“原索引 + oldCap 位置”。

3.16HashMap是线程安全的吗?

        不是。HashMap 在并发下存在数据覆盖、遍历的同时进行修改会抛出 ConcurrentModificationException 异常等问题,JDK 1.8 之前还存在死循环问题。

3.17 介绍下死循环

        导致死循环的根本原因是 JDK 1.7 扩容采用的是“头插法”,会导致同一索引位置的节点在扩容后顺序反掉。而 JDK 1.8 之后采用的是“尾插法”,扩容后节点顺序不会反掉,不存在死循环问题。

JDK 1.7.0 的扩容代码如下:
void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

3.18 ★★★介绍下JDK1.8主要做了哪些优化?

(1)底层数据结构从“数组+链表”改成“数组+链表+红黑树”,主要是优化了 hash 冲突较严重时,链表过长的查找性能:O(n) -> O(logn)。

(2)计算 table 初始容量的方式发生了改变,老的方式是从1开始不断向左进行移位运算,直到找到大于等于入参容量的值;新的方式则是通过“5个移位+或等于运算”来计算。

(3)优化了 hash 值的计算方式,老的很复杂,新的只是简单的让高16位参与了运算。

(4) 扩容时插入方式从“头插法”改成“尾插法”,避免了并发下的死循环。

(5)扩容时计算节点在新表的索引位置方式从“h & (length-1)”改成“hash & oldCap”,性能可能提升不大,但设计更巧妙、更优雅。

4. 除了HashMap,还用过哪些Map,在使用时这么选择?

介绍使用场景
Hashtable早期的线程安全Map,直接通过在方法发加synchronized实现线程安全现在理论上不会使用
CorurrentHashMap线程安全的Map,通过synchronized+CAS实现线程安全需要保证线程安全
LinkedHashMap能记录访问顺序或插入顺序的Map,通过head、tail属性维护有序双向链表,通过Entry的after、before属性维护节点顺序需要记录访问顺序或插入顺序
TreeMap通过实现Comparator实现自定义顺序的Map,如果没有指定Comparator则会按照key的升序排序,key如果没有实现Comparable接口,则会抛异常需要自定义排序
HashMap最通用的Map,非线程安全、无序无特殊需求都可使用

5.TreeMap

5.1TreeMap概述:

        TreeMap存储K-V键值对,通过红黑树(R-B Tree)实现;与HashMap的区别是TreeMap的结果是排序的。遍历比较简单,TreeMap的遍历可以使用map.values(), map.keySet(),map.entrySet(),

6.CorurrentHashMap

6.1基础概念:

        CorurrentHashMap是J.U.C(java.util.concurrent包)的重要成员,它是HashMap的一个线程安全的、支持高效并发的版本。在默认理想状态下,ConcurrentHashMap可以支持16个线程执行并发写操作及任意数量线程的读操作。通过synchronized+CAS实现线程安全。

二、字符串相关

1.String、StringBuffer、StringBuilder的区别

        StringBuffer是线程安全的,StringBuilder是线程不安全的,所以单线程环境下StringBuilder效率更高。

三、其它基础知识点

1.public、private、protected的区别?

  • public: Java语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。
  • private 私有型;Java语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的类、属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。
  • protected 保护型;介于public 和 private 之间的一种访问修饰符,一般称之为“保护形”。被其修饰的类、属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。
  • default:即不加任何访问修饰符,通常称为“默认访问模式“。该模式下,只允许在同一个包中进行访问。

2.Arraylist LinkedList 异同点?

          首先Arraylist是底层数据结构是基于动态数组的,LinkedList底层是基于双向链表实现的。所以在插入删除元素上,LinkedList性能更好一些。因为数组的存储地址是连续的,所以ArrayList查询的时候性能比LinkedList要好一些。所以LinkedList更适用于频繁插入删除的场景,ArrayList适用于频繁查询的场景。内存空间占用上ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而 LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全

        ArrayList扩容:ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。 默认情况下,新的容量会是原容量的1.5

3.深拷贝和浅拷贝?

深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用(user类、userservice类)

(1)浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,内部的类属性指向的是同一个对象。 A和拷贝的A1引用对象指向的同一个。

(2)深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不一定是同一个对象。 A和拷贝的A1引用对象指向的不是同一个。

4.零拷贝

        零拷贝指的是,应用程序在需要把内核中的一块区域数据转移到另外 一块儿内核区域时,不需要经过复制到用户空间,再转移到目标区域去,而是实现直接转移。

5.“==”和equals的区别?

首先这两个都是用来判断两个数据是否相等。两种方式各有各的应用场景。

“==”对于基本数据类型来说,比较的是值,对于引用类型来说,比较的是对象的内存地址,也就是判断两个引用是否都指向了同一个对象。

“equals” java.lang.Object类中的方法,主要用于两个对象之间,检测一个对象是否等于另一个对象。每个类都能够重写equals()方法,如果类没有重写equals()方法的话,与“==”是等价的。类如果覆盖了equals()方法。一般,我们用覆盖equals()方法来两个对象的内容相等;若它们的 内容相等,则返回true(即,认为这两个对象相等)

6.CopyOnWriteArrayList的底层原理是怎样的?

      ArrayList是线程不安全的, CopyOnWriteArrayList线程安全的。

7.Lamda表达式介绍下?在哪儿用过,为什么要用Lambda表达式? 

      Lambda是一个匿名函数,我们可以把Lambda表达式理解为一段可以传递的代码(将代码像数据一样进行传递)。它可以写出更加简介、灵活的代码,作为一种更紧凑的代码风格,使Java语言表达能力得到了提高。     

场景的话:在for循环输出用过  

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
for (int element : numbers) {
    System.out.prinln(element);
}

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(x -> System.out.println(x));

8.Windows下的安装包为什么在Linux下不能用?核心原因是什么?

        首先是因为Linux系统和Windows系统的格式不同,格式就是协议,就是在固定位置有意义的数据。Linux下可执行程序文件格式是elf,可以使用readelf命令查看elf文件头。而Windows下的可执行程序是PE格式,它是一种可移植的可执行文件。

        还有一点是因为Linux系统和Windows系统的API不同,这个API指的就是操作系统的API,Linux中的API被称为系统i盗用,是通过 int 0x80这个软中对俺实现的。而Windows中的API放在动态链接库文件中,也就是Windos开发人员所说的DDL,这是一个库,里面包含代码和数据。Linux中的可执行程序获得系统资源的方法和Windows不一样,所以显然是不能在Windows中运行的。

四、Java特性

1.怎么理解封装、继承、多态?

        (1)封装是面向对象的特征之一,就是把客观事物封装成抽象的类。目的是减少代码的荣誉,避免重复性使用代码,所以我们定义方法,就可以看作对一段同样作用代码的封装,用来降低代码的重复性。定义一个类、接口等,也可以视为对具有相同特性的代码块的封装。封装,主要在于装,即保证成一个整体,但封也同样重要,着代表着安全性,所以权限修饰符的封装意义更多体现在对装的安全要求,即封。总而言之,写代码时,当遇到大量重复性代码时,就要考虑是否可以封装来优化。

        (2)继承可以视为避免重复性封装。物理继承类还是实现接口,都是因为父类中有一段重复性代码(属性或者功能)同样适用于子类,为避免重复封装这段代码,所以才有了继承的体现。

        (3)多态的话可以视为对继承功能的优化,即一种对拥有继承特性代码的新使用方式。多态的前提就是继承,因为所有子类都适用父类中一段重复性代码,所以将所有调用该功能的子类对象都指向父类引用来避免子类重复性声明或者提高代码扩展性。因此多态的使用要在继承(接口实现)的过程中含有方法重写才更有意义。

2.Integer和Int的区别?为什么要对基础类型设计封装类型?

        Integer和int的区别有很多,我简单罗列三个方面,第一个,作为成员变量来说,Integer的初始值是null,int的初始值是0;第二个,Integer存储在堆内存中,因为它是个对象,而int类型呢它是直接存储在栈空间里面; 第三个,Integer是一个对象类型,它封装了很多的方法和属性,我们在使用的时候更加灵活,至于为什么要设计成封装类型,我认为主要的原因是,Java本身就是一个面向对象的语言,一切操作都是以对象作为基础的,比如说像集合里面存的元素,也只支持Object类型,普通类型是无法通过集合来存储的,以上就是我的理解。

3.32位和64位的区别?

        32位和64位意味着处理器一次能处理的最大位数。主要区别是针对CPU而言,指CPU一次处理的二进制位数,64位计算机一般指拥有64位的处理器和64位的操作系统,32位计算机一般指拥有32位的处理器和32位的操作系统。而现在的32位处理器只支持安装32位操作系统,64位处理器却能同时支持安装32位和64位操作系统。

从寻址能力来看:

        32位系统的最大寻址空间是2的32次方=4294967296(bit)= 4(GB)左右;

        64位系统的最大寻址空间为2的64次方方,数值大于1亿GB;

        也就是说32位系统的处理器最大只支持到4G内存,而64位系统最大支持的内存高达亿位数,实际运用过程中大多数的电脑32位系统最多识别3.5GB内存,64位系统最多识别128GB内存。

从兼容性来看:

         一般情况下,我们很大部分的软件都是在32位架构环境下开发的,这就是为啥64位系统的兼容性不如32位。

        我们需要了解的是,在32位系统下是无法运行64位软件的,而64位系统却支持安装大部分的32位软件。在64位系统下32位软件安装文件夹带x86区分的哦。

系统体积大小来看:

        64位操作系统包含32位系统兼容库,既兼容64位又兼容32位的使用程序;

        64位系统都比32位系统大的多,比如win7 64位比win7 32位系统大700M左右。

从设计初衷来看:

        64位操作系统的设计初衷是为了满足机械设计和分析、三维动画、视频编辑和创作,以及科学计算和高性能计算应用程序等领域中需要大量内存和浮点性能的客户需求。

从运算速度来看:

        运算速度不同,64位CPU GPRs(General-Purpose Registers,通用寄存器)的数据宽度为64位,64位指令集可以运行64位数据指令,也就是说处理器一次可提取64位数据(只要两个指令,一次提取8个字节的数据),比32位(需要四个指令,一次提取4个字节的数据)提高了一倍,理论上性能会相应提升1倍。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值