Java基础面试

Java基础常见面试问题

写在前面:本人是一名二流大学应届毕业生,这篇博客是自己在面试时所遇到的Java基础问题。记录了下来写成了博客,框架的问题后面有时间再整理,不具有权威性,若有问题跟疑问,敬请交流。

一、面对对象的三大特征

封装:把客观事物封装成抽象的类,并且类可以把自己的方法和数据只让可信的类或者对象进行操作,对不可信的进行隐藏。

继承:子类继承父类的特征和行为,具有父类的方法。继承概念的实现方式有三类:

  • 实现继承
  • 接口继承
  • 可视继承

实现继承是指使用基类的属性和方法而无需额外编码的能力

接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力

可视继承是指子窗体(类)使用基窗体(类)的外观和实现代码的能力

多态:多态同一个行为具有多个不同表现形式或形态的能力。是指一个类实例(对象)的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。

多态的优点:

  • 消除类型之间的耦合关系
  • 可替换性
  • 可扩充性
  • 接口性
  • 灵活性
  • 简化性

静态多态(编译时多态):重载

动态多态(运行时多态):重写

运行时多态实现机制

调用JVM中的方法区,通过方法区中的方法表调用方法接口,然后通过invokeVirtual(调用实例方法)指令调用方法。

二、面对对象的五个基本原则

单一职责原则

一个类只负责一项工作,你去改变它的时候有且只有一个去改变它的理由

开闭原则

新增开放,修改关闭

里氏代换原则

对父类的调用同样适用于子类

依赖倒置原则

高层次的模块不应该依赖于低层次的模块。

具体实现应该依赖于抽象,而不是抽象依赖于实现。

接口隔离原则

使用多个专门的接口比使用单一的总接口要好。接口之前相互隔离

三、普通方法和构造方法区别

构造方法必须与类名相同,且只有public这个修饰,而且没有返回值。而普通方法,不能与类名一致。

构造方法不是成员,所以父类的构造方法不能被继承所以也不可能重写,但是可以调用,普通方法,可以被继承也就支持重写

四、强引用、软引用、弱引用、虚引用

JVM执行GC的时候,判断对象是否存活有两种方式,一种是可达性分析算法,另一种就是引用计数法。

这里提一嘴可达性分析算法。我们通过"GC Roots"的对象(1、运行时方法区中Java栈的本地变量表中引用的对象。2、方法区中静态属性应用的对象。3、方法区中常量引用的对象。4、本地方法栈中引用的对象。)作为起点,从这些起点往下搜索,走过的路径被称为引用链。当一个对象到"GC Roots"没有任何引用链时,证明该对象不可达,则该对象不可引用。详情可以看这篇博客

进入正题。对象的引用被划分为4种级别由高到低分别是:强引用>软引用>弱引用>虚引用。

强引用:最普遍的引用,如果一个对象有强引用,则GC不会回收它,哪怕内存不够了,情愿抛出异常也不会回收。如果一个方法有强引用,那么强引用的引用是保存在Java栈中的,而引用内容在堆中,当这个方法完成后,引用数会变回0,就会被回收。

软引用:若一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足时,就会回收这些对象的内存。只要垃圾回收器没有回收它。

弱引用:比软引用更软,它的声明周期更短,如果GC扫描到一个对象只有弱应用,就不管内存够不够了,直接回收。

虚引用:见名思其意,形同虚设,虚引用不会影响生命周期,如果一个对象只有虚引用,那它跟没有任何引用一样,啥时候被回收都有可能。

五、集合

在这里插入图片描述
(图片源自网络,如有侵权,联系删除)

List

1、ArrayList

底层是动态数组,默认大小是10,扩容阈值是1.5。它的好处在于查找很快。

2、LinkedList

底层是双向链表,好处是不需要扩容,插入和删除操作很快,坏处是查找慢。

ArrayList和LinkedList的区别

在某些情况下LinkedList的表现要优于ArrayList,有些算法在LinkedList中实现时效率更高。比方说,利用Collections.reverse方法对列表进行反转时,其性能就要好些。

但是这并不意味着LinkedList就一定比ArrayList效率要高,比如在进行插入操作时,如果数据够长的情况下,在前半段插入,LinkedList的效率要高,但是在后半段插入的时候,ArrayList效率要高,这是因为插入的时候,ArrayList是直接定位到下标,然后把后面的数据复制移动,而LinkedList它是先移动指针到指定位置,然后再才开始插入,所以它的开销要比ArrayList要高。

Map

1、HashMap

hashmap有较快的访问速度,但是对于遍历顺序却是不确定的

jdk1.7时底层是数组+链表,使用的是头插法(在多线程的情况下可能会出现死循环)

jdk1.8时底层是数组+链表+红黑树,使用的是尾插法,默认容量是16,负载因子是0.75,也就是当容量达到12的时候,就会触发扩容,扩容是扩大到当前容量的两倍。如果链表长度达到8时且hashmap的数组长度大于64,链表将会以红黑树(Ologn)的形式进行存储。

hashmap在初始化容量的时候是2的次方数,那它是怎么办到的呢?(找到大于等于这个数的2的次方数)

(这里讲的是如果找到小于等于这个数的2的次方数)在这之前我先说明下2的次方数的特点,它的二进制数下,只有一个1那就肯定是2的次方数,它会将传进来的int值进行右移位操作,会分别右移1、2、4、8、16。每次右移位操作过后,与上次的数字进行或运算(有一为一),这样就会慢慢变成全1,最后用这个数字减去它右移一位的数字,就可以得到这个数字最高位为1,其余位为0的二进制数。那么为题又来了,为什么是要右移1、2、4、8、16位呢?因为int类型的长度是4个字节,也就是32位,而1+2+4+8+16=31,所以这么移动就能够保证低位全是1。

举一个例子,我们要找小于等于11的2的次方数,那么根据运算,11的二进制数就是1011,右移一位(0101)与1011做或运算,得到1111,然后再右移两位(1111)与1111做或运算,依次计算下去。最后右移16位做完或运算后的结果就是1111。我们再用这个右移16位做完或运算后的结果去减该二进制数右移一位的结果,就是1111-0111=1000,如此便找到了小于1011(十一)的2的次方数:1000(八)。

回到刚刚讲的地方,我们要找的是大于等于这个数的2的次方数,但是刚刚那个方法找到的是小于等于的2的次方数,所以在调用这个方法的时候我们会将要处理的数字,左移一位(扩大两倍),如此一来就可以解决这个问题。问题解决了,那么新的问题又来了,那如果这个数字是8本来就是2的次方数,我还要去左移一位就变成了的16,这样一来,不就错了吗?所以,在进行左移操作的时候,我们会对传进来的数字减一(注意不是位运算,而是数值上-1),这样8-1=7,7*2=14,我们去找14下面的二的次方数就没有问题了。

讲完是如何实现的,我们再讲讲为什么容量要为2的幂次方数,因为n-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突。

当向hashmap插入数据的时候,首先要确定哈希桶数组中的位置。那么我们如何确定node的存储位置呢?hashmap首先调用hashcode()方法,获取key对应的hash值,然后对其进行高位运算:将hash值右移16位,取得高16位,与原来的低16位进行异或运算。最后将这个值与table.length-1进行与运算获得该对象的保留位以计算下标。

hashmap不是线程安全的 ,当多个线程同时写入hashmap的时候可能会导致写入数据不一致的情况

2、LinkedHashMap

因为hashmap不能保证迭代器的顺序,所以提供了linkedhashmap。默认是插入顺序,也可以通过构造器设置为按照访问顺序

3、TreeMap

不熟,我只知道它是按照Key进行排序的

4、HashTable

hashtable继承自Dictionary类,hashmap继承自AbstractMap类,但是二者都实现了Map的接口。

线程安全,因为每个方法都用了Synchronize进行修饰,这也导致了效率低下。所以我平时都用的ConcurrentHashMap。

5、ConcurrentHashMap

ConcurrentHashMap是对HashTable进行了改进,只锁住当前的这个桶,锁的颗粒度更小了,并发的性能更高。

六、线程

创建方式

1、继承Thead、实现Runnable

2、线程池:

chcaedThreadPool(可缓存)

fixedThreadPool(可指定大小)

scheduledThreadPool(可控制执行周期的)

singleThreadExecutor(单个线程的线程池)

可讲的太多了,等我后面另起一篇。

并发编程的三个概念

原子性:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性:程序执行的顺序按照代码的先后顺序执行。

七、JVM

类加载过程

在这里插入图片描述

运行时数据区

1、程序计数器

它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。

2、堆

存储对象本身以及数组(数组的引用是放在java栈中),这部分也是垃圾产生的关键地区,垃圾主要在堆跟方法区中产生,栈中不会有垃圾,因为栈用完过后,就弹出栈了。

此外,堆是线程共享的,但是这会不会出现线程不安全的问题呢?通过我们的编程经验告诉我们,并不会,但是这是如何办到的呢?其实堆说是线程共享的,但是其实又不是线程共享的。分配内存的时候,JVM其实是分配的一块内存,在这块内存中,堆是私有的。打个比方,我有一栋楼,每进来一个人,我就分配一间房给他,这间房对他来说就是私有的,不是共享的。所以堆线程共享才不会出现线程安全问题。

3、Java栈

如图!
如图

4、本地方法栈

与Java栈类似,只不过执行的是本地方法。通过调用本地方法接口,访问本地方法库。

5、方法区

存储每个类的信息(类名、方法信息等)、静态变量(static)、常量(final)。在物理上属于堆的一部分。方法区中还有一个很重要的部分就是运行时常量池。它是每一个类或接口的常量池的运行时态。在类或接口被加载的时候,它就会被创建出来。这个地方也是产生垃圾的主要场所。

在这儿我展开一个话题,这个地方在jdk1.7时,很对人称为永久代,但是它本身并不是永久代。永久代的对象放在方法区中,这就等同于永久代的内存空间就是方法区。

其实方法区是一种规范,具体情况还请百度,展开来讲又是一篇博客了。

在jdk1.8中HotSpots取消了永久代,取而代之的是元空间,如果说永久代就是方法区,那永久代没有了,是不是方法区也没有了。当然不是,这玩意一直都存在。那元空间与永久代有什么不同吗?上面我们也说了,永久代的对象是放在方法区的,而方法区在物理上是属于堆的,所以永久代的存储位置是堆,而元空间是本地内存。

双亲委派机制(如何打破双亲委派机制)

我们编写的类,会先向上询问(Application ClassLoader->Extension ClassLoader->Bootstrap ClassLoader)是否可以加载,询问完之后,从上往下尝试加载(上面括号反过来)。

用实际情况来讲,就是如果你写了个叫做String的类,然后恰巧你的包还叫java.lang,那你觉得你调用的时候,是调用的自己写的String还是系统的String。
编程经验告诉我们,肯定是系统的,这就是双亲委派机制所达到的效果。用一个最朴素的例子,地方法规与中央法规出了冲突,就以中央为准。

那有什么办法解决这种机制吗?有,改写类加载器就可以,具体实现自行查找,展开来讲又是一篇博客。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值