一线互联网公司Java高级面试题总结

362 阅读18分钟
原文链接: click.aliyun.com

Java重点知识
多线程(线程状态、线程并发,Synchronized与Lock的区别和底层原理,常用的锁及其使用场景和原理,
volatile和ThreadLocal解决了什么问题,CAS在Java中的实现
线程池原理和实现,阻塞队列和线程安全队列,
线程间通信: synchronized + wait、notify/notifyAll, Lock + Condition 的多路复用,
CountDownLatch、CyclicBarrier和Semaphore的作用和用法,使用场景)
JVM内存管理机制和垃圾回收机制(内存模型、GC策略、算法、分代回收GC类型,Full GC、Minor GC作用范围和触发条件)
JVM内存调优(内存调整的6个参数,了解是怎么回事,一般做项目过程中使用较多)
设计模式(熟悉常见设计模式的应用场景,会画类图,常用:代理,2个工厂,策略,单例,观察者,适配器,组合与装饰)
JAVA集合类框架(理解框架图、HashMap、ArrayList、HashSet等的关系和区别,其中HashMap的存储机制几乎每次都有问)
HashMap的原理,底层数据结构,rehash的过程,指针碰撞问题HashMap的线程安全问题,为什么会产生这样的线程安全问题ConcurrentHashMap的数据结构,底层原理,put和get是否线程安全
JAVA的异常处理机制(异常的分类、常见的异常有哪些、Try catch finally的使用)
JVM运行机制(理解JVM是如何运行的,理解类加载机制和类的初始化顺序)
Java 的NIO 3个主要概念 Channel、Buffer、Selector,为何提高了性能?加分项:熟悉Netty
Linux基础(面试笔试中对linux也有一定的要求,建议最好搭建一个linux虚拟机,并练习常用的命令)
框架
Spring
Spring IOC原理,Bean的生成和生命周期(工厂模式 + 反射生成 + 单例),Spring用到的设计模式
Spring AOP原理和应用(动态代理与cglib代理,使用场景和代理的本质区别)
Spring如何处理高并发?高并发下,如何保证性能?
单例模式 + ThreadLocal
单例模式大大节省了对象的创建和销毁,有利于性能提高,ThreadLocal用来保证线程安全性
Spring单例模式下,用ThreadLocal来切换不同线程直接的参数,用ThreadLocal是为了保证线程安全,实际上,ThreadLocal的key就是当前线程的Thread实例
单例模式下,Spring把每个线程可能存在线程安全问题的参数值放进了ThreadLocal,虽然是一个实例,但在不同线程下的数据是相互隔离的,
因为运行时创建和销毁的bean大大减少了,所以大多数场景下,这种方式对内存资源的消耗较少,并且并发越高,优势越明显
特别注意:
Spring MVC的Controller不是线程安全的!!!
Spring MVC 是基于方法的拦截,粒度更细,而Spring的Controller默认是Singleton的,即:每个request请求,系统都会用同一个Controller去处理,
Spring MVC和Servlet都是方法级别的线程安全,如果单例的Controller或Servlet中存在实例变量,都是线程不安全的,而Struts2确实是线程安全的
优点:不用每次创建Controller,减少了对象创建和销毁
缺点:Controller是单例的,Controller里面的变量线程不安全
解决方案:
1.在Controller中使用ThreadLocal变量,把不安全的变量封装进ThreadLocal,使用ThreadLocal来保存类变量,将类变量保存在线程的变量域中,让不同的请求隔离开来
2.声明Controller为原型 scope="prototype",每个请求都创建新的Controller
3.Controller中不使用实例变量
Spring 事务管理的使用和原理?事务的传播属性
声明式事务管理,在Service之上或Service的方法之上,添加 @Transactional注解
@Transactional如何工作?
Spring在启动时,会去解析生成相关的Bean,这是会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据 @Transactional的相关参数进行相关配置注入,这样就在代理中把相关的事务处理掉了(开启正常提交事务,异常回滚事务)真正的数据库层,事务提交和回滚是通过binlog和redo log实现的
Spring如何解决对象的循环依赖引用?( 只支持Singleton作用域的, setter方式的循环依赖!不支持构造器方式和prototype的循环依赖)
原理:
创建Bean A时,先通过无参构造器创建一个A实例,此时属性都是空的,但对象引用已经创建创建出来,然后把Bean A的引用提前暴露出来,
然后setter B属性时,创建B对象,此时同样通过无参构造器,构造一个B对象的引用,并将B对象引用暴露出来。
接着B执行setter方法,去池中找到A(因为此时,A已经暴露出来,有指向该对象的引用了),这样依赖B就构造完成,也初始化完成,然后A接着初始化完成,循环依赖就这么解决了!
总结:先创建对象引用,再通过setter()方式,给属性赋值,层层创建对象 !!!
Bean A初始化时,先对其依赖B进行初始化,同时,通过默认无参构造器,生成自己的引用,而不调用其setter()方法,
当B对象创建时,如果还依赖C,则也通过无参构造器,生成B的引用,
C对象创建时,如果引用了A,则去对象池中查到A的引用,然后调用setter()方式,注入A,完成C对象的创建
C创建完成后,B使用setter()方式,注入C,完成B对象创建,
B对象场景完成后,A使用setter()方式,注入B,完成A对象创建,
最终,完成setter()方式的循环依赖!
数据库
InnoDB和MyISAM区别和选择
1.InnoDB不支持FULLTEXT类型的索引。
2.InnoDB 中不保存表的具体行数,也就是说,执行select count() from table时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count()语句包含 where条件时,两种表的操作是一样的。
3.对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中,可以和其他字段一起建立联合索引。
4.DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除。
5.LOAD TABLE FROM MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB表,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。
另外,InnoDB表的行锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表,例如update table set num=1 where name like “%aaa%”
任何一种表都不是万能的,只用恰当的针对业务类型来选择合适的表类型,才能最大的发挥MySQL的性能优势。
悲观锁和乐观锁的含义(悲观锁:真正的锁,只允许一个线程操作同一条记录,
乐观锁:一种冲突检测机制,一般通过版本号或时间戳方式实现,对性能影响较小)
索引使用及其索引原理(索引底层实现:B+树)
Query查询优化
1.explain sql查看执行效率,定位优化对象的性能瓶颈
2.永远用小结果驱动大的结果集
3.尽可能在索引中完成排序
4.只取出自己需要的column,而不是*
5.使用最有效的过滤条件
6.用表连接代替子查询
7.当只要一行数据时,使用limit 1
8.为搜索字段建立索引
9.千万不要ORDER BY RAND(),避免select *
10.尽可能使用NOT NULL
11.开启查询缓存,并为查询缓存优化查询语句
eg:
select username from user where add_time >= now()
注意:
1.这样的语句不会使用查询缓存,
2.像NOW()和RAND()或是其它的诸如此类的SQL函数都不会开启查询缓存,因为这些函数的返回是会不定的易变的。所以,你所需要的就是用一个变量来代替MySQL的函数,从而开启缓存
3.修改, 对now()进行处理,只取年月日 yyyy-MM-dd,变为一个不衣变的值
Redis的5种数据结构和使用场景
Redis的持久化机制
Redis中Hash类型的底层2种实现区别(压缩表: 省内存 和 跳跃表:查询更快)
Redis作为分布式消息队列使用,性能和注意点
数据结构和算法
常见的排序算法就不说了,需要理解其原理和会写代码,还有时间空间复杂度也要知道
队列、栈:需要理解其存取结构,并能在某些场景下使用
二叉树:树的遍历、树的深度、按层次输出、平衡二叉树、逆序打印树等
链表:逆序、合并两有序的链表、判断链表是否又环、链表倒数第K个元素等
字符串:KMP算法、动态规划(这个是重点,需要好好理解动态规划,常见的题有:求解最长回文子串、求解最长公共子串等)
海量数据处理:现在好多大公司都会问海量数据的处理,所以需要掌握常见的处理方法,比如Bit-map、分而治之、hash映射等,可以百度看看相关的文章,加深理解
常用算法
冒泡排序
快速排序
插入排序
希尔排序
归并排序
堆排序
桶排序
动态规划
最长公共子串
最长回文子串
数组的最大k个值
数字的最大连续子数组之和
左旋转字符串
字符串匹配算法:KMP算法
二分查找
链表
单链表逆序
两个有序单链表合并
两个单链表是否相交
相交处的节点
单链表倒数第K个数
单链表排序
栈和队列
设计包含min函数的栈
两个队列实现栈
两个栈实现队列
一个数组实现栈和队列

前序、中序、后续遍历
求二叉树的深度
按层次遍历二叉树
判断二叉树是否为完全二叉树
判断二叉树是否镜面对称
判断两颗树是否相等
设计模式6大原则

  1. 单一职责原则(SRP)
    定义:就一个类而言,应该仅有一个引起它变化的原因。

从这句定义我们很难理解它的含义,通俗讲就是我们不要让一个类承担过多的职责。如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到破坏。
比如我经常看到一些Android开发在Activity中写Bean文件,网络数据处理,如果有列表的话Adapter 也写在Activity中,问他们为什么除了好找也没啥理由了,把他们拆分到其他类岂不是更好找,如果Activity过于臃肿行数过多,显然不是好事,如果我们要修改Bean文件,网络处理和Adapter都需要上这个Activity来修改,就会导致引起这个Activity变化的原因太多,我们在版本维护时也会比较头疼。也就严重违背了定义“就一个类而言,应该仅有一个引起它变化的原因”。
当然如果想争论的话,这个模式是可以引起很多争论的,但请记住一点,你写代码不只是为了你也是为了其他人。

  1. 开放封闭原则(ASD)
    定义:类、模块、函数等等等应该是可以拓展的,但是不可修改。

开放封闭有两个含义,一个是对于拓展是开放的,另一个是对于修改是封闭的。对于开发来说需求肯定是要变化的,但是新需求一来,我们就要把类重新改一遍这显然是令人头疼的,所以我们设计程序时面对需求的改变要尽可能的保证相对的稳定,尽量用新代码实现拓展来修改需求,而不是通过修改原有的代码来实现。
假设我们要实现一个列表,一开始只有查询的功能,如果产品又要增加添加功能,过几天又要增加删除功能,大多数人的做法是写个方法然后通过传入不同的值来控制方法来实现不同的功能,但是如果又要新增功能我们还得修改我们的方法。用开发封闭原则解决就是增加一个抽象的功能类,让增加和删除和查询的作为这个抽象功能类的子类,这样如果我们再添加功能,你会发现我们不需要修改原有的类,只需要添加一个功能类的子类实现功能类的方法就可以了。
3.里氏替换原则(LSP)
定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象
里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
在使用里氏代换原则时需要注意如下几个问题:
子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。
Java语言中,在编译阶段,Java编译器会检查一个程序是否符合里氏代换原则,这是一个与实现无关的、纯语法意义上的检查,但Java编译器的检查是有局限的。
4.依赖倒置原则(DIP)
定义:高层模块不应该依赖低层模块,两个都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
在Java中,抽象就是指接口或者抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或者继承抽象类而产生的就是细节,也就是可以加上一个关键字new产生的对象。高层模块就是调用端,低层模块就是具体实现类。
依赖倒置原则在Java中的表现就是:模块间通过抽象发生,实现类之间不发生直接依赖关系,其依赖关系是通过接口或者抽象类产生的。如果类与类直接依赖细节,那么就会直接耦合,那么当修改时,就会同时修改依赖者代码,这样限制了可扩展性。
5.迪米特原则(LOD)
定义:一个软件实体应当尽可能少地与其他实体发生相互作用。
也称为最少知识原则。如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
在将迪米特法则运用到系统设计中时,要注意下面的几点:在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;在类的设计上,只要有可能,一个类型应当设计成不变类;在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
6.接口隔离原则(ISP)
定义:一个类对另一个类的依赖应该建立在最小的接口上。
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
采用接口隔离原则对接口进行约束时,要注意以下几点:
接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
concurrent包的实现基础
由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:
A线程写volatile变量,随后B线程读这个volatile变量。
A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。
Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:
首先,声明共享变量为volatile;
然后,使用CAS的原子条件更新来实现线程之间的同步;
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。

原文发布时间为:2018-08-03
本文作者:老邓头
本文来自云栖社区合作伙伴“全栈开发者社区”,了解相关信息可以关注“全栈开发者社区”。