【备战面试】每日10道面试题打卡——Java基础篇(二)


⭐️写在前面



在这里插入图片描述


1.HashMap和HashTable的区别是什么,底层实现是什么?

HashTable现在不怎么用了,ConcurrentHashMap用的比较多,但是关于它的面试题还是要准备一下的

区别

  1. HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;
  2. HashMap允许key和value为null,而HashTable不允许
  3. 初始化大小不相同HashMap初始化大小为16,HashTable初始化大小为11,两者的默认加载因子为0.75

底层实现

HashMap的底层实现可以参考文章
HashMap底层源码解析上(超详细图解+面试题)
HashMap底层源码解析下(超详细图解)

两者均是位桶+链表结构

jdk8开始链表高度到8,、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在

  • 计算key的hash值,二次hash然后对数组长度取模,对应到数组下标
  • 如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组
  • 如果产生hash冲突,先进性equals比较,相同则取代该元素,不同,则判断链表高度并插入链表,如果链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表
  • key为null,存在下标0的位置

2.如何实现一个IOC容器

  • 配置文件中指定要扫描的包路径
  • 定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注解
  • 从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息及文件夹信息,我们将当前路径下所有以.class结尾的文件添加到一个Set集合中进行存储
  • 遍历这个集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个安全的Map用来存储这些对象
  • 遍历这个IOC容器,获取到每一个类的实例,判断里面是否有依赖其他的类的实例,然后进行递归注入

3.什么是字节码?采用字节码的好处是什么?

在Java中,因为我们的编译程序和操作系统并不是能直接进行交流的,他们之间隔着一层虚拟机,虚拟机负责在人社平台上都提供给编译程序一个接口,编译程序只需要面向虚拟机,生成虚拟机能够理解的代码就可以,然后由解释器将虚拟机中的代码转换为特定系统的机器码执行。在java中,这种供虚拟机理解的代码就是字节码

这里也清楚了为什么java语言会比c、c++等执行效率慢的原因,因为c、c++为编译型语言,将代码解析为机器码供机器阅读,而java代码却需要先由编译器编译为字节码交给jvm,再由jvm中的解释器解释为机器码供机器阅读,中间多了一层

我们梳理一下Java程序正常运行的步骤

java源代码->编译器->jvm可执行的Java字节码(即虚拟指令)->jvm->jvm中解释器->机器可执行的二进制机器码->程序执行

这样设计的好处也是巨大的

java通过字节码,在一定程度上解决了解释型语言执行效率低的问题,同时又保留解释型语言可移植的特点,使java在运行时更高效,而且字节码不针对于某种特定的机器,因此,java程序不用编译就能在不同计算机上运行

因为我们写好的程序只需要面向虚拟机就好,不用在意是什么系统,jvm会帮我们解决好剩下的一切,所以java才有了跨平台机制,一次编译多处运行,而其他编译型语言要收到不同系统的影响,比如8位,16位系统的int为2字节,long为4字节,而32位系统的int、long为4字节,而64位系统的long为8字节

4.面向对象和面向过程的区别

面向对象和面向过程是两种不同的处理问题的角度

面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要做什么

我们以洗衣机洗衣服为例

面向过程会把任务拆解为一系列的步骤(函数)

  • 打开洗衣机
  • 放衣服
  • 放洗衣粉
  • 清洗衣服
  • 烘干

而面向对象会根据洗衣服这件事拆分出两个对象

  • 人:打开洗衣机,放衣服,放洗衣粉
  • 洗衣机:清洗衣服,烘干

从上面例子可以看出,面向过程比较直接高效,而面向对象更易于复用、扩展和维护

5.说一下面向对象的三大特性

  • 封装:将同一事物的特征功能封装在一起,只对外暴露可以调用的接口,外部调用只需要知道怎么调用,至于内部怎么实现的不需要关心
  • 继承:子类可以继承父类的方法,属性,行为等,并可以做出新的改变和扩展,java中不支持多继承,但是接口可以支持多实现
  • 多态:封装和继承都是为多态服务的,多态指同一种行为具有多个不同的表现形式,实现多态有两种方式,分别是重载和重写

这里我们复习一下重载和重写的区别

重载:同一个类中的不同方法方法名相同,参数顺序不同,参数类型不同,参数数量不同,方法返回值和访问修饰符可以不同,方法的重载发生在编译

重写:发生在父子类中,方法名,参数列表必须相同,返回值类型子类必须小于等于父类,子类的异常返回范围必须小于等于父类,访问修饰符范围大于等于父类,如果父类方法访问修饰符为private,则子类就不能重写该方法

6.说一下双亲委派模型

关于双亲委派机制的详细描述,可以看这篇博客
通俗易懂的双亲委派机制

双亲委派模型图
在这里插入图片描述
我们参考着源码来分析

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

向上委派实际上就是查找缓存,因为我们加载过的类会被放入缓存中,在不考虑自定义类加载器的情况下,首先会去AppClassLoader中检查是否加载过,如果加载过就不需要在加载,没有的话调用父加载器的loadClass方法,同理父类也会检查自己是否加载过,没有的话继续向上查找,这是一个递归的过程

直到到达BootStrapClassLoader之前,都是再检查自己是否记载过,BootStrapClassLoader已经没有父加载器了,这时考虑自身能否加载,不能的话便会下沉到子加载器去加载,一直到最底层,如果没有加载器可以加载,就会抛出ClassNotFoundException异常

为什么要设置双亲委派机制,双亲委派机制的好处

  • 如果有人想替换系统级别的类,比如String.java。而在双亲委派机制下这些类都被BootstrapClassLoader加载过了(因为当一个类需要加载的时候,最先去尝试加载的就是BootStrapClassLoader),这样其他类加载器就没有机会再去加载,这样从一定程度上防止了危险代码的植入

  • 双亲委派机制也避免了类的重复加载,因为JVM中区分不同类不仅仅是根据类名,也会通过是否被不同的ClassLoader加载来区分类的不同

7.说一下Java中的异常体系

Java中的所有异常都来自顶级父类Throwable

Throwable下有两个子类ExceptionError

Error是程序无法处理的错误,比如说OOM内存溢出,一旦出现这个错误,则程序将被迫停止运行

Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckedException检查异常

RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过

8.GC如何判断对象可以被回收

GC有两种方法判断对象是否被回收

  • 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数+1,释放一个引用时计数-1,计数为0时表示对象可被回收
  • 可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断该对象是可回收对象

引用计数法的弊端是当A对象和B对象相互引用时,他们的引用计数都是1,但是他们都不再被其他对象所使用了,因为计数不为0导致无法被垃圾回收

可以作为GC Root的对象

(1)虚拟机栈(栈帧中的本地变量表)中引用的对象
(2)方法区中类静态变量引用的对象
(3)方法区中常量引用的对象
(4)本地方法栈中JNI(即一般说的Native)

可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会,对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalize队列中判断是否是要执行finalize()方法

当对象变成GC Root不可达时,GC会判断该对象是否覆盖finalize()方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由低优先级线程执行该队列中的对象的finalize方法,执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”
每个对象只能触发一次finalize()方法

9.线程的生命周期,线程有哪些状态

线程通常有5种状态:创建、就绪、运行、阻塞和死亡状态

阻塞的情况又分为3种

  1. 等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入等待池中,等待阻塞状态只能由其他线程调用notifyAll或者notifyAll方法才能被唤醒,wait是Object类的方法
  2. 同步阻塞:运行的线程在获取前对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中
  3. 其他阻塞:运行的线程执行sleepjoin方法,或者发出I/O请求时,JVM会把该线程置为阻塞状态,当sleep状态超时,join等待线程终止或者超时,或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法

新建状态(New):新建了一个线程对象

就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法,该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权

运行状态(Running):就绪状态的线程获取了CPU,执行程序代码

阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态

死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期

10.sleep()、wait()、join()、yield()的区别

在回答这个问题之前我们要明白两个概念
锁池

所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程都需要在这个锁池中进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,得到锁后进入就绪队列等待cpu资源分配

等待池

当我们调用wait()方法后,线程会放到等待池当中,等待池的线程不会去竞争同步锁,只有调用了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中
在这里插入图片描述
(1)sleep是Thread类的静态本地方法,wait则是Object类的本地方法

(2)sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中

sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu的调度,获取到cpu资源后就可以继续执行了,而如果sleep时该线程有锁,那么sleep不会释放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁,如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出interrupteexception异常返回,这点和wait是一样的

(3).sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字

这个是因为wait是Object类的本地方法,它的调用是基于对象的,这就代表可能有多个线程调用wait()方法,我们在向这个对象所对应的数据结构进行写入删除时就会出现并发问题,理论上我们需要一个锁进行控制

(4)sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要(不指定时间需要被别人中断)

(5)sleep一般用于当前线程休眠,或者轮询暂停操作,wait则多用于多线程之间的通信
(6)sleep会让出CPU执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁

yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行

join()执行后线程进入阻塞状态例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程

public class Test02 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println("22222222");
            }
        });
        t1.start();
        t1.join();//t1调用join,则main线程必须等待t1执行完毕
        //这行代码必须要等t1全部执行完毕才会执行
        System.out.println("1111");
    }
}
22222222
1111

main线程中调用了t1线程的join,main线程进入了阻塞队列,直到t1线程结束

我们可以看看join的源码

    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

可以看到join内部是通过wait实现的,当main线程调用t.join时候,main线程会获得t对象的锁,并调用该对象的wait,直到该对象唤醒main线程

在这里插入图片描述

上面的面试题是博主通过牛客面经,博文,资料加上我自己的理解总结而成的,小温自己也在积极准备面试,所以文章中出现的关于面试题的错误请在评论区指出,我再进行改正优化,如果文章对你有所帮助,请给博主一个免费的三连吧,感谢大家

  • 30
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 19
    评论
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

温文艾尔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值