Java面试知识归纳(持续更新)

Java基础

  • 阿里巴巴的Java手册中明确禁止了在循环中使用“+”进行字符串拼接,为什么?

解: “+”是一个语法糖,底层编译之后其实质就是使用 StringBuilder 的 append 函数。在循环中,等于每进行一次循环,都会生成一个 StringBuilder,如果在循环外面声明一个 StringBuilder, 在循环里面使用 append 函数,则可以节约很多系统开销。

  • 阿里巴巴的Java手册中明确禁止了在 foreach 循环里进行元素的 remove/add 操作,为什么?

解: foreach 循环也是一个语法糖,编译后的底层是使用链表的迭代器进行迭代,在迭代器生成的时候,会记录链表的初始更改次数作为迭代器的期望更改次数,每一次迭代的时候,都会比较两个值是否一致,而 remove/add 均会使链表的更改次数 +1, 则在迭代的时候会直接抛出异常。

  • synchronized 有哪几种用法,各有什么不同 // 个人觉得这是一道看似很简单,实则基础不太好的人真的答不上来

解:一共有四种用法


    private synchronized void f1 () {
        // 第一种用法 f1
    }
    
    private static synchronized void f2 () {
        // 第二种用法 f2
    }
    
    private void f () {
        Object lock = new Object();
        synchronized (lock) {
            // 第三种用法 f3
        }
        synchronized (Integer.class) {
            // 第四种用法 f4
        }
    }

第一种和第二种都是修饰函数,第三种第四种是直接锁一个对象。因为synchronized 的本质是获取对象的监视器,实现并发控制,所以这四种用法的本质不同就是锁的到底是哪个对象。其中f1 和 f3 本质是相同的,这个被争抢的对象都是一个实体对象,f1 争抢的对象可以理解为就是this,即f1的写法等价于 synchronized(this){},f2 和 f4的写法本质是相同的,被争抢的对象是一个类对象,比如如果f2这个函数是Integer 这个class 里面的,则等价于 synchronized (Integer.class){}

  • 一个对象要想作为HashMap的key,得具备什么条件?

解: HashMap的底层是散列表,也就是数组+链表(Java8之后还有红黑树),根据key值从 HashMap里面找数据的过程是先根据对象的hashcode()定位其在数组的位置,再去根据equals方法遍历链表查找元素,如果一个对象equals = true 的时候,其hashcode()却不相等,则无法在HashMap中找到,所以最起码的条件,当equals = true的时候,hashCode值一致。// 当然还有一些别的条件,只说最重要的。

  • ConcurrentHashMap 的实现原理

解: 其大部分结构和 HashMap 是一致的,只是有些地方的调整实现了线程安全。
在插入元素的时候,先定位其在数组上的位置,若该位置为 null,则直接使用 CAS 将值插入。
若当前正在扩容,则线程协助扩容。
若是数组位置不为空,则使用 synchronized 锁住头结点,在链表中加入新的键值对,如果超过长度就转成红黑树。

  • volatile 的用法和局限性 // volatile 真的是平常工作中不常用的关键字,我要不是准备面试也不知道这是干嘛的

解: 说到volatile关键字得先说一下 Java 的线程模型,JVM中的对象都是在主内存中的,但是为了线程的执行速度,每个子线程中也有内存空间,子线程在使用对象的时候先拷贝一份该对象到自己的内存空间中,如果在线程工作期间修改了该对象,则会将对这个对象的修改在某个时刻刷新到主内存中,可是这个刷新的时间是没办法控制的,每个线程在获取到主内存中的对象时,都没办法保证这个对象是最新的。volatile就是解决这个问题的,被它修饰过的变量,在子线程使用它的时候会去强制到主内存中获取它的最新数据,一旦子线程修改了该对象,也会强制去刷新主内存的数据。另外volatile还可以禁止上下文指令重排序(重排序的意思是编译器在编译过程中,会对代码的执行进行一些优化,把某些代码执行的先后顺序改一改,但是绝对不会影响代码在单线程下的执行的正确性,但是在多线程下就难说了,所以禁止指令重排序,可以避免不必要的麻烦)。即volatile能保证并发过程中的有序性和可见性。
局限在于无法保证原子性,比如 i ++ 这个操作,其本质是 先读取 i 的值,再将 i + 1 赋值给i,也就是本该是一个操作的变成了两个操作,如果此时 a线程和b线程同时执行 i ++ 的操作,a读取i的值为0,b也读取i的值为0,则a将其赋值为1,b也将其赋值为1,到最后i的值本该为2的,但却还是1,所以volatile无法保证并发过程的原子性。 // 原子性就得靠 CAS 了

  • StackOverflowError 和 OutOfMemoryError 的区别

解: 一般问这个可能是问完JVM内存模型之后了,这两者一个是栈溢出一个是内存溢出,所以说到两者之间的区别,先搞清楚这两者分别是在哪发生的。栈溢出是在栈中发生的,是指栈下探深度太深超过虚拟机所允许的最大深度导致抛出的异常,举个简单例子

    private static void aa () {
        aa();
    }

这是一个没有出口的递归,会无限下探,直到抛出 StackOverflowError。
OutOfMemoryError 即内存溢出,很好理解,就是堆里面没有多余的内存可供分配了。

  • JVM 如何判断一个对象是否该被回收,该对象在被回收前如何再挣扎一下?

解: 判断一个对象是否该被回收主流的是两种算法,一种是引用计数算法,一种是可达性分析算法。
引用计数算法就是每个对象持有一个引用计数器,每多个地方引用它时,就 +1 ,少个地方引用它时就 -1,如果 = 0 即认为该对象可被回收,主流 JVM 都没有用引用计数器算法,因为这个算法有个弊端,它解决不了循环依赖问题。
可达性分析算法就是通过一系列被称为 “GC Roots” 的对象作为起点,从这些节点向下搜索,如果该对象和这些 “GC Roots” 能联通的话,则说明该对象存活。虚拟机栈中引用的对象、方法区中类静态属性引用的对象和方法区中常量引用的对象都能算作 “GC Roots”。
每个对象被杀死要经过两次被标记的过程,如果在第一次被标记后,能够触发该对象的 finalize() 方法,并且将其连上 “GC Roots”,则可以自救。finalize() 一个对象只会被触发一次,所以这种方法只能救一次命,第二次就不行了。

Spring

  • 解释一下 IOC,Spring是如何解决循环依赖问题的

解: IoC即控制反转,IoC是为了解决项目中复杂的依赖关系,使用非硬编码的方式来扩展这些关系,简单来说就是为了解耦,在你需要的地方 IoC 容器会自动帮你注入对应的服务实例。
对于 Spring 而言,就是由 Spring 去负责控制对象的生命周期和对象间的关系,所有的 Bean 在 Spring 中登记,创建对象的控制转移给了 Spring,对象不必要关心初始化的细节问题,都交给容器去处理。

在初始化的时候,容器中有个 Map 去存放原始的、属性尚未填充的 Bean 对象,当获取一个 Bean A 的时候先完成创建 A ,放入这个 Map 中之后再去填充属性,填充 A 时,需要 B,同理将创建之后的 B 放入 Map 中再去填充 B,此时 B中需要 A 对象,则直接从 Map 中获取 A 对象,避免了进入死循环创建。

  • 解释一下 AOP,Spring 实现 AOP 的底层原理

解: AOP 即面向切面编程,可以看做是对面向对象编程的一种补充。
有时候我们需要对某一类对象进行某一种通用的功能增强,如果把这些代码分散在这些对象中,则不好维护,也不利于解耦。使用AOP可以把这些通用的增强集中在一起进行管理,实现一些通用代码和业务代码解耦。

Spring AOP 的底层实现有 JDK 动态代理 和 CGLIB 代理。JDK 动态代理 只能给实现了接口的类进行代理,CGLIB 代理则是隐式创建一个子类,在子类中实现增强进行代理,故CGLIB 代理不可代理被final修饰过的类。Spring默认优先使用JDK动态代理,也可以指定使用 CGLIB 动态代理。

MySQL

  • 说几种 MySQL 的索引失效情况

解: 索引失效情况还是比较多的。就简单罗列几个:1 使用 select * from table where name like '%123'; 如果name字段上加了索引,like '123%' 是可以使用索引的,但是 like '%123' 会使得索引失效。 2 如果设置了一个联合索引 比如 mid pid ,则使用 mid 是可以使用该索引的,但是使用 pid 则会索引失效。

  • 事务的四个特性和隔离级别 // 很硬的题目
    解:
    四大特性:
    原子性:要么全部执行,要么全部回滚
    一致性:数据库从一个状态到另一个状态,没有中间状态.
    永久性:事务对数据库的改变是永久生效的
    隔离性:多个事务之间互不干扰,没有联系

隔离级别:
未提交读:事务还未提交,别的事务已经能够读取到该事务的改动,会发生脏读、不可重复读
提交读:事务提交后,别的事务才能够读取到该事务的改动,会发生不可重复读、幻读
可重复读:在一个事务发生期间,读取某一行的数据都是一致的,会发生幻读
串行化:不管多少事务,挨个运行,一个事务执行完后再执行另一个事务,啥都不会发生

MySQL 的 InnoDB 引擎默认隔离级别是可重复读,因为其使用 MVCC(版本控制) 方式实现事务,可以在一些场景下避免幻读,在有些场景下避免不了幻读。

redis

  • 说一下redis的持久化策略

解:redis并非只是单纯的缓存,虽然所有操作都是在内存中进行的,但是它依然会把数据持久化到硬盘中,一旦把redis关了,再启动之后数据也不会丢失。redis的持久化分为两种,一种是RDB持久化,一种是AOF持久化。
RDB持久化是指redis会定期fork出一条子线程,将某个时间点上数据库的状态保存在一个RDB的压缩的二进制文件,主线程依然可以处理来自服务器的请求。这些生成的RDB文件即使redis关机也不会被删掉,等到了redis重启时,会将其载入恢复数据库状态。可以通过配置文件配置定期保存的时间频率,比如 save 900 1 指的是 900s 如果有一个数据被改动,就会进行一次保存。
RDB持久化是有一些局限的,比如,在两次保存之间,服务器宕机了,那这两次保存之间的改动就会被丢失,所以AOF持久化是针对一些对于redis正确性要求比较高的系统,默认是不开启的,开启的话需要更改redis的配置文件。当开启了AOF后,redis会记录每一条写命令,比如 set sadd 等,记录到 AOF文件。如果redis开启了AOF,则在redis恢复状态时,会优先使用 AOF 文件恢复。

  • 说一下redis的key的过期策略

解:redis的key的过期策略分为定期删除和惰性删除。redis过定期拉出一条线程来检测库中的key是不是已经过期了,如果过期了就给删掉,而这种检测也不是一次性就将库里面所有的key都检测一遍,它会记录每一次检测执行到的位置,下一次检测从该位置继续检;惰性删除就是每一次使用redis的key时,去看一下这条数据是否已经过期,如果过期了就删掉该key。这两种删除策略共同使用一是为了保证redis的效率(如果只使用惰性删除,则每一次查找到数据后还得去检测其是否已过期,则效率必然有损耗),二是为了其过期时间的严格正确性(如果只使用定期删除,则如果在两次检测间隔期间key值过期就会被误取到)。

  • 如何使用redis设计一个分布式锁

解: 这个答案当然不唯一,这里说一下 redisson 框架的实现分布式锁的原理。
获取锁的过程,分为三种情况:
第一种情况,通过key查找在redis中有没有对应的数据,如果没有,则认为该锁没有被获取,此时生成一条哈希记录该哈希的key为线程id,value为锁的重入次数,初始值为1。
第二种情况,通过key查找在redis查找到了相应的数据,该哈希的key与本线程的线程id相同,则value + 1,表明重入次数 + 1
第三种情况,通过key查找在redis查找到了相应的数据,该哈希的key与本线程的线程id不同,则表明该锁已经被别的线程获取到了,此时线程挂起,并且在redis中订阅一个频道去监听锁的最新动态。

解锁的过程,分为两种情况
第一种情况,该锁的重入次数 > 1 ,则重入次数 -1
第二种情况,该锁的重入次数 = 1 ,则直接清除该数据,并且发布一个消息告诉被挂起的线程这个锁已经被解掉了,此时监听这个消息的线程会被激活,重新去争抢锁。

算法

  • 一个int型的数组,如何找出其中的三个数,使之乘积最大

解: 这道题其实很有意思,是我当初找实习的时候被问过的题目,当时是发邮件给我,所以我在家想了半天才想出解。
最朴素的解就是找其中最大的三个数,乘积妥妥的最大,这个想法没错,但得有个前提条件就是整个数组的数都是正数,一旦出现负数,这个答案就错了。比如如果数组是[-100, -10, 1, 2]很明显 -10、1、2 这三个数不是正确答案,于是我当时罗列了很多种情况,所有的数都是正数的情况、所有的数都是负数的情况、有正有负的情况,后来我发现,答案就两种,一种是 最大的三个数,还有一种是 最小的两个数和最大的那个数,没有第三种情况了,于是这道题的答案就变得简单了,只要找出最大值、次大值、第三大值和最小值、次小值这五个数,再将两个答案进行比较,比较大的答案就是正确答案。这里还有个点就是,如何找出这五个数,当时面试官告诉我说,所有能达到这一步的人,回答的都是进行一次排序,只有我直接使用查找算法找出了这五个值,毕竟排序算法的时间复杂度大于查找算法。

  • 一共有十层台阶,一次只能往上走一层台阶或者两层台阶,则一共有多少种走法可以走到最顶层?比如,一个台阶一个台阶走十步算是一种,第一次走两个,后面走八次一个算是一种,先走八次一个,最后走两个算是一种。

解:这个问题先想简单的情况,

一共只有一层台阶,有几种走法,肯定只有一种,则 f(1) = 1;

一共只有两层台阶,有几种走法,就两种,则 f(2) = 2;

一共只有三层台阶,有几种走法,此时就该想到,可以先上到第一层台阶,然后直接跨两层到第三层,也可以先上到第二层台阶,跨一层到第三层,故有 f(3) = f(1) + f(2) = 3;

接下来就简单了 f(4) = f(2) + f(3) = 5 ... 以此类推 直到算出 f(10)

  • 有一个早点铺,一个饼卖 5 块钱,一共有 10 个用户在排队,其中 5 个用户手上有 5 块钱,另外 5 个用户手上有 10 块钱,一共有多少种排队方法。比如 前面 5 个都是 5 块钱用户,后面 5 个都是 10 块钱用户。

解: 这道题和上道题挺相似的,不过上面那道题简单一点是一维的,这道题是二维的。
假设 手握 5 块钱的用户有 x 人,手握 10 块钱的用户有 y 人,则答案为 f(x, y)
当 y > x 时,这道题是没有答案的,原因不用我说了,自己想一想就知道了
当 x = y 时,排在队尾的人肯定是 手握 10 块钱的用户,则此人可以排除 故当 x = y 时,f(x, y) = f(x, y - 1)
当 y < x 时,有两种情况,一种是队尾那个人手握 10 块钱,我们将其排除 为 f (x, y - 1)
第二种情况是队尾那个人手握 5块钱,我们将其排除 为 f(x - 1, y)
则当 y < x 时,f(x, y) = f(x, y - 1) + f(x - 1, y)
再考虑一种情况就是 y = 0 的时候,即 f(x, 0),即只有手握 5 块钱的用户,则 排队不用说了 只有一种 故 f(x, 0) = 1
最终可以得到这个递归函数


 private int f(int x, int y) {
        if (x == y) {
            return f(x, y - 1);
        }
        if (y == 0) {
            return 1;
        }
        return f(x - 1, y) + f(x, y - 1);
    }

答案 即 f(5, 5) = 42

杂七杂八

  • 一个WEB请求从发出到回来经历了哪些东西 (摘自 屈定 对一个WEB请求的理解)

解:

  1. URL解析,比如 非ASCII码的转换,参数、协议、请求头、请求体的建立
  2. DNS域名解析,解析的顺序是先从浏览器缓存中找该域名、找不着的话会在本地hosts中查找,要是还找不到的话就会像本机DNS服务器发出查找请求
  3. 域名解析成功后,浏览器创建与服务器的socket连接,构造请求信息,进行TCP三次握手,开始向服务器传输消息,并等服务器回复信息
  4. 服务器以nginx+tomcat为例,经过以上步骤后请求到达了nginx,nginx对URL进行分析,验证其所在机器上有所需要的服务,并且用户是有权限调用的,决定该URL由哪一个tomcat服务处理,捕获处理结果,返回给请求者,最后四次挥手结束请求。到此完成浏览器服务端的通信
  5. 浏览器拿到了服务器的返回信息后会对内容进行解析,展现成用户所需要的内容

你可能感兴趣的:(Java面试知识归纳(持续更新))