Java面试
(总结自B栈架构风清扬 https://www.zhihu.com/column/c_1050762683808403456)
JavaSE
0. 谈谈对面向对象思想的理解
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
所以,面向对象的思维更多的是考虑如何去选择合适的工具,然后组织到一起干一件事。

1.JDK,JRE,JVM有什么区别?
- JDK:Java Development Kit,Java开发工具包,提供了Java的开发环境和运行环境。包含了编译Java源文件的编译器Javac,还有调试和分析的工具。
- JRE:Java Runtime Environment,Java运行环境,包含Java虚拟机及一些基础类库
- JVM:Java Virtual Machine,Java虚拟机,提供执行字节码文件的能力所以,如果只是运行Java程序,只需要安装JRE即可。
- 另外注意,JVM是实现Java跨平台的核心,但JVM本身并不是跨平台的,不同的平台需要安装不同的JVM

2.Java的基本数据类型有哪些?
boolean,char,byte,short,int,long,float,double
注意:String是引用类型

3.==和equals的区别
- ==
比较的是值
比较基本的数据类型,比较的是数值
比较引用类型:比较引用指向的值(地址) - equals
默认比较也是地址,因为这个方法的最初定义在Object上,默认的实现就是比较地址
String s1 = new String("zs");
String s2 = new String("zs"); // 堆的地址
System.out.println(s1 == s2);//false
String s3 = "zs"; // 常量池地址
String s4 = "zs";
System.out.println(s3 == s4);//true
System.out.println(s3 == s1);//false
String s5 = "zszs";
String s6 = s3+s4; //字符串不可变 相加会创建新的对象
System.out.println(s5 == s6);//false
final String s7 = "zs";
final String s8 = "zs";
String s9 = s7+s8; // final修饰 不是变量 是常量 仍是转为常量
System.out.println(s5 == s9);//true
final String s10 = s3+s4; //影响s10 下次不能被赋值了
System.out.println(s5 == s10);//false
4. final的作用
final修饰类,表示类不可变,不可继承
比如,String,不可变性
final修饰方法,表示该方法不可重写
比如模板方法,可以固定我们的算法
final修饰变量,这个变量就是常量
- 注意:
修饰的是基本数据类型,这个值本身不能修改
修饰的是引用类型,引用的指向不能修改
例:
`final Student student = new Student(1,"Andy");
student.setAge(18);//注意,这个是可以的!
但是student = new student()不行 改变了引用指向。
5.String s = "java"与String s = new String(“java”)
String s = “java”;
String s = new String(“java”);
这两者的内存分配方式是不一样的。
第一种方式,JVM会将其分配到常量池,而第二种方式是分配到堆内存
注意:String是引用类型
6.String,StringBuffer,StringBuilder区别
- String 跟其他两个类的区别是
String是final类型,每次声明的都是不可变的对象,所以每次操作都会产生新的String对象,然后将指针指向新的String对象。 - StringBuffer,StringBuilder都是在原有对象上进行操作所以,如果需要经常改变字符串内容,则建议采用这两者。
- StringBuffer vs StringBuilder前者是线程安全的,后者是线程不安全的。线程不安全性能更高,安全的性能低。
- 所以在开发中,优先采用StringBuilder.
StringBuilder > StringBuffer > String
线程安全:多线程环境下,对这个对象的访问不需要加额外的同步控制,锁,操作的数据结果依然是正确的。
StringBuffer的每个方法都加了synchronized修饰。
什么时候需要考虑线程安全?
多线程访问同一个资源的时候才需要考虑线程安全。
开发中,StringBuilder解决字符拼接的问题。
7. 接口和抽象类的区别
这个问题,要分JDK版本来区分回答:
- JDK1.8之前:
语法:- 抽象类:方法可以有抽象的,也可以有非抽象, 有构造器
(具体实现) - 接口:方法都是抽象,属性都是常量,默认有public static final修饰
(空实现)
- 抽象类:方法可以有抽象的,也可以有非抽象, 有构造器
- 设计:
- 抽象类:同一类事物的抽取,比如针对Dao层操作的封装,如,BaseDao,BaseServiceImpl
- 接口:通常更像是一种标准的制定,定制系统之间对接的标准
- 例子:
- 单体项目,分层开发,interface作为各层之间的纽带,在controller中注入IUserService,在Service注入IUserDao
- 分布式项目,面向服务的开发,抽取服务service,这个时候,就会产生服务的提供者和服务的消费者两个角色
- 这两个角色之间的纽带,依然是接口
JDK1.8之后:
接口里面可以有实现的方法,注意要在方法的声明上加上default或者static

最后区分几个概念:
多继承,多重继承,多实现
- 多重继承:A->B->C(爷孙三代的关系)
- 多实现:Person implements IRunable,IEatable(符合多项国际化标准)
- 多继承:接口可以多继承,类只支持单继承
8. 算法题-求N的阶乘(手写)
- 什么是递归?
递归,就是方法内部调用方法自身
递归的注意事项:
找到规律,编写递归公式
找到出口(边界值),让递归有结束边界
注意:如果递归太多层,或者没有正确结束递归,则会出现“栈内存溢出Error”!
问题:为什么会出现栈内存溢出,而不是堆内存溢出? - 这道题该怎么写?
规律:N!=(n-1)!*n;
出口:n=1 或 n=0 return 1;
public static int getResult(int n){
if(n<0){
throw new ValidateException("非法参数");
}
if(n==1 || n==0){
return 1;
}
return getResult(n-1)*n;
}
9.算法题-求解斐波那切数列的第N个数是几?
* 规律:每个数等于前两个数之和
* 出口:第1项和第2项都等于1
public static int getFeiBo(int n) {
if (n < 0){
return -1;
}
if (n == 1 || n == 2) {
return 1;
} else {
return getFeiBo(n - 1) + getFeiBo(n - 2);
}
}
10. 什么是向上转型?什么是向下转型?
这道题目一般出现在(笔试-选择题)
举例说明即可:
向上转型:Person person = new Student(); 安全的
向下转型:Teacher teacher = (Teacher)person; 不安全的
11.Int和Integer的区别(重点)
Integer i1 = new Integer(12);
Integer i2 = new Integer(12);//开辟新内存 堆
System.out.println(i1 == i2);//false
Integer i3 = 126;
Integer i4 = 126; //自动装箱机制
int i5 = 126;
System.out.println(i3 == i4);//true
System.out.println(i3 == i5);//true 自动拆箱 比较数值
-128~127用的缓存内的数据
Integer i6 = 128;//越界 开辟新内存
Integer i7 = 128;
int i8 = 128;
System.out.println(i6 == i7);//false 都是应用 看边界
System.out.println(i6 == i8);//true 自动拆箱 比较数值
12.方法的重写和重载的区别
- 重载:发生在一个类里面,方法名相同,参数列表不同(混淆点:跟返回类型没关系)
- 重写:发生在父类子类之间的,方法名相同,参数列表相同
15.List和Set的区别
- List(有序,可重复)
ArrayList LinkedList - Set(无序,不可重复)
HashSet TreeSet
注意:无序:加进去的顺序不一定等于输出的顺序 - Collection和Collections的区别
Java工具类命名+s结尾
16. 谈谈ArrayList和LinkedList的区别
- 底层数据结构的差异
ArrayList,数组,连续一块内存空间
LinkedList,双向链表,不是连续的内存空间
- 一个常规的结论(一般来说)
ArrayList,查找快,因为是连续的内存空间,方便寻址,但删除,插入慢,因为需要发生数据迁移
LinkedList,查找慢,因为需要通过指针一个个寻找,但删除,插入块,因为只要改变前后节点的指针指向即可。
确定要存储1000个对象的信息
ArrayList更省内存(确定 数组直接分配)
LinkedList 有指针的信息 占内存
-
ArrayList细节分析
-
增加
- 添加到末尾,正常不需要做特别的处理,除非现有的数组空间不够了,需要扩容
- 数组初始化容量多大?
10,当你知道需要存储多少数据时,建议在创建的时候,直接设置初始化大小
- 怎么扩容?
当发现容量不够之后,就进行扩容按原先数组容量的1.5倍进行扩容,位运算,下面是关键的源码
java int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1);再将原先数组的元素复制到新数组,ArrayselementData = Arrays.copyOf(elementData, newCapacity)
添加到其他位置,这个时候需要做整体的搬迁
-
删除删除末尾,并不需要迁移删除其他的位置,这个时候也需要搬迁
-
修改修改之前,必须先定位定位-查找-ArrayList(数组是一段连续的内存空间,定位会特别快)
-
查找如上所述
-
-
LinkedList细节分析
1.提供了的两个引用(first,last)
2.增加
添加到末尾,创建一个新的节点,将之前的last节点设置为新节点的pre,新节点设置为lastvoid linkLast(E e) { //获取到最后一个节点 final Node<E> l = last; //构建一个新节点,将当前的last作为这个新节点的pre final Node<E> newNode = new Node<>(l, e, null); //把last指向新节点 last = newNode; //如果原先没有最后一个节点 if (l == null) //将first指向新节点 first = newNode; else //否则,将原先的last的next指向新节点 l.next = newNode; size++; modCount++; } Node节点的定义:内部类 private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }- 修改最后一个节点或者第一个节点,那么就很快(first,last)
修改其他位置,如果是按坐标来定位节点,则会按照二分查找法,源码如下:
if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } - 修改最后一个节点或者第一个节点,那么就很快(first,last)
18.谈谈HashSet的存储原理
首先,我们要明确一点,HashSet底层采用的是HashMap来实现存储,其值作为HashMap的key.
第一,为什么要采用Hash算法?有什么优势,解决了什么问题?
第三,HashSet如何保证保存对象的唯一性?会经历一个什么样的运算过程?
解决的问题是唯一性
存储数据,底层采用的是数组
当我们往数组放数据的时候,你如何判断是否唯一?
可以采用遍历的方式,逐个比较,但是这种效率低,尤其是数据很多的情况下。
所以,为了解决这个效率低的问题,我们采用新的方式。
采用hash算法,通过计算存储对象的hashcode,然后再跟数组长度-1做位运算,得到我们要存储在数组的哪个下标下,如果此时计算的位置没有其他元素,直接存储,不用比较。
此处,我们只会用到hashCode但是随着元素的不断添加,就可能出现“哈希冲突”,不同的对象计算出来的hash值是相同的,这个时候,我们就需要比较,才需要用到equals方法。
如果equals相同,则不插入,不相等,则形成链表(拉链法?)
随着元素不断添加,链表可能会越来越长,会优化红黑树
第二,所谓哈希表是一张什么表?
本质是一个数组,而且数组的元素是链表
20.谈谈LinkedHashMap和HashMap的区别(重点)
有关HashMap:
- 初始化大小是16,如果事先知道数据量的大小,建议修改默认初始化大小。 减少扩容次数,提高性能 ,这是我一直会强调的点。
- 最大的装载因子默认是0.75,当HashMap中元素个数达到容量的0.75时,就会扩容。 容量是原先的两倍。
- HashMap底层采用链表法来解决冲突。 但是存在一个问题,就是链表也可能会过长,影响性能 于是JDK1.8,对HashMap做了进一步的优化,引入了红黑树。 当链表长度超过8,且数组容量大于64时,链表就会转换为红黑树当红黑树的节点数量小于6时,会将红黑树转换为链表。 因为在数据量较小的情况下,红黑树要维护自身平衡,比链表性能没有优势。这3点非常重要!
LinkedHashMap:
链表+散列表的结构,其底层采用了Linked双向链表来保存节点的访问顺序,所以保证了有序性。
21.谈ConcurrentHashMap,HashMap,Hashtable的区别
- Hashtable 上锁,是线程安全的,但效率低
- HashMap是线程不安全的,但效率高
- Collections.synchronizedMap(),工具类提供了同步包装器的方法,来返回具有线程安全的集合对象,性能依然有问题。
为解决这样的矛盾问题,所以JDK提供了并发包,来平衡这样的问题(java.util.concurrent)
ConcurrentHashMap(重点)
- ConcurrentHashMap 兼顾了线程安全和效率的问题
分析:HashTable锁了整段数据(用户操作是不同的数据段,依然需要等待)
解决方案:把数据分段,执行分段锁(分离锁),核心把锁的范围变小,这样出现并发冲突的概率就变小
在保存的时候,计算所存储的数据是属于哪一段,只锁当前这一段
[相当于把哈希表拆成多段 每一个段内部也是哈希表
根据第一次哈希函数得到所在段 在通过哈希函数得到段中位置
只要是put进去不是在一个段,就不需要上锁]
- 开发中的选择
不是多个资源访问同一个线程优先选择HashMap
在方法内使用,定义为一个局部变量,使用HashMap
全局变量,多个线程同时访问,使用ConcurrentHashMap
22.ArrayList vs Vector
ArrayList:线程不安全,效率高,常用
Vector:线程安全的,效率低
24.谈谈IO流的分类及选择
- 分类:
-
按方向分:输入流,输出流
注意,是站在程序的角度来看方向),输入流用于读文件,输出流用于写文件
-
按读取的单位分:字节流,字符流
-
按处理的方式分:节点流,处理流
比如,FileInputStream和BufferedInputStream(后者带有缓存区功能-byte[])

-
IO流的4大基类:InputStream,OutputStream,Reader,Writer
- 选择
字节流可以读取任何文件
读取文本文件的时候:选择字符流(假如有解析文件的内容的需求,比如逐行处理,则采用字符流,比如txt文件)
读取二进制文件的时候,选择字节流(视频,音频,doc,ppt)
25.serialVersionUID的作用是什么
当执行序列化时,我们写对象到磁盘中,会根据当前这个类的结构生成一个版本号ID
当反序列化时,程序会比较磁盘中的序列化版本号ID跟当前的类结构生成的版本号ID是否一致,如果一致则反序列化成功,否则,反序列化失败
加上版本号,有助于当我们的类结构发生了变化,依然可以之前已经序列化的对象反序列化成功
26.请描述下Java的异常体系
Java异常体系让程序更加具有健壮性。

- Error是虚拟机内部错误
栈内存溢出错误:StackOverflowError(递归,递归层次太多或递归没有结束)
堆内存溢出错误:OutOfMemoryError(堆创建了很多对象) - Exception是我们编写的程序错误(程序员的锅)
RuntimeException:也称为LogicException
为什么编译器不会要求你去try catch处理?没有提醒
本质是逻辑错误,比如空指针异常,这种问题是编程逻辑不严谨造成的
应该通过完善我们的代码编程逻辑,来解决问题
比如:算数异常,空指针,类型转换异常,数组越界,
NumberFormateException(数字格式异常,转换失败,比如“a12”就会转换失败) - 非RuntimeException:【可能会出现问题 第三方(参与方)造成】
编译器会要求我们try catch或者throws处理本质是客观因素造成的问题,比如FileNotFoundException写了一个程序,自动阅卷,需要读取答案的路径(用户录入),用户可能录入是一个错误的路径,所以我们要提前预案,写好发生异常之后的处理方式,这也是java程序健壮性的一种体现。
29.throw跟throws的区别
- throw: 作用于方法内,用于主动抛出异常(一个)
- throws:作用于方法声明上,声明该方法有可能会抛些某些异常
针对项目中,异常的处理方式,我们一般采用层层往上抛,最终通过异常处理机制统一处理(展示异常页面,或返回统一的json信息),自定义异常一般继承RunntimeException,我们去看看Hibernate等框架,他们的异常体系都是最终继承自RunntimeException。
30.一道关于try catch finally返回值的问题
在Java虚拟机没有关闭的情况下 finally优先级最高
所以finally是无论如何都会执行,除非JVM关闭了。
JavaWeb
44.谈谈Servlet的生命周期Servlet总结
首先,要明确一点,Servlet是单实例的,这个很重要!
- 生命周期的流程:
创建对象–>初始化–>service()–>doXXX()–>销毁
- 创建对象的时机:
- 默认是第一次访问该Servlet的时候创建
- 也可以通过配置web.xml,来改变创建时机,比如在容器启动的时候去创建,DispatcherServlet(SpringMVC前端控制器)就是一个例子< load-on-startup>1< /load-on-startup >
- 执行的次数
对象的创建只有一次,单例
初始化一次
销毁一次 - 关于线程安全
构成线程不安全三个因素:
1.多线程的环境(有多个客户端,同时访问Servlet)
2.多个线程共享资源,比如一个单例对象(Servlet是单例的)
3.这个单例对象是有状态的(比如在Servlet方法中采用全局变量,并且以该变量的运算结果作为下一步操作的判断依据)
45.描述JSP和Servlet的区别
MVC: 模型 视图 控制器
- 技术的角度:
JSP本质就是一个Servlet
JSP的工作原理:JSP->翻译->Servlet(java)->编译->Class(最终跑的文件) - 应用的角度:
JSP=HTML+Java
Servlet=Java+HTML
各取所长,
JSP的特点在于实现视图,Servlet的特点在于实现控制逻辑
48.描述Session跟Cookie的区别(重要)
- 存储的位置不同
Session:服务端
Cookie:客户端 - 存储的数据格式不同
Session:value为对象,Object类型
Cookie:value为字符串,如果我们存储一个对象,这个时候,就需要将对象转换为JSON - 存储的数据大小
Session:受服务器内存控制
Cookie:一般来说,最大为4k - 生命周期不同
Session:服务器端控制,默认是30分钟,注意,当用户关闭了浏览器,session并不会消失。
Cookie:客户端控制,其实是客户端的一个文件,分两种情况- 默认的是会话级的cookie,这种随着浏览器的关闭而消失,比如保存sessionId的cookie
- 非会话级cookie,通过设置有效期来控制,比如这种“7天免登录”这种功能,就需要设置有效期,setMaxAge(单位是秒)
- cookie的其他配置
httpOnly=true:防止客户端的XSS攻击
path="/" :访问路径
domain="":设置cookie的域名
XSS攻击:跨站脚本攻击
1. 过滤器
2. httpOnly=true
- cookie跟session之间的联系
http协议是一种无状态协议,服务器为了记住用户的状态,我们采用的是Session的机制。而Session机制背后的原理是,服务器会自动生成会话级的cookie来保存session的标识。
49.转发和重定向的区别
- 转发
发生在服务器内部的跳转,所以,对于客户端来说,至始至终就是一次请求,所以这期间,保存在request对象中的数据可以传递。
- 重定向
发生在客户端的跳转,所以,是多次请求,这个时候,如果需要在多次请求之间传递数据,就需要用session对象。
- 面试官的问题:在后台程序,想跳转到百度,应该用转发还是重定向?
答案:重定向,因为转发的范围限制在服务器内部。

数据库
55.谈谈数据库设计的三大范式及反范式
- 数据库的三大范式
第一范式:列不可分(原子性)
第二范式:要有主键
第三范式:不可存在传递依赖
比如商品表里面关联商品类别表,那么只需要一个关联字段product_type_id即可,其他字段信息可以通过表关联查询即可得到
——————————————————————————————
如果商品表还存在一个商品类别名称字段,如product_type_name,那就属于存在传递依赖的情况,第三范式主要是从空间的角度来考虑,避免产生冗余信息,浪费磁盘空间
- 反范式设计:(第三范式)
为什么会有反范式设计?原因一:提高查询效率(读多写少)
比如上述的描述中,显示商品信息时,经常需要伴随商品类别信息的展示,所以这个时候,为了提高查询效率,可以通过冗余一个商品名称字段,这个可以将原先的表关联查询转换为单表查询。
原因二:保存历史快照信息
比如订单表,里面需要包含收货人的各项信息,如姓名,电话,地址等等,这些都属于历史快照,需要冗余保存起来,
不能通过保存用户地址ID去关联查询,因为用户的收货人信息可能会在后期发生变更
58.如何解决SQL注入?
-
SQL注入,是指通过字符串拼接的方式构成了一种特殊的查询语句
比如:select * from t_user where usename=’’ and password=’’’ or 1=1 #select * from t_user where usename=’’ or 1=1 # ’ and password=’’
-
解决方案
采用预处理对象,采用PreparedStatement对象,而不是Statement对象
可以解决SQL注入的问题
另外也可以提高执行效率,因为是预先编译执行
SQL执行过程(语法校验->编译->执行) -
延伸
MyBatis如何解决了SQL注入的问题?采用#
MyBatis的#和$的差异,#可以解决SQL注入,而?号不能解决
59.JDBC如何实现对事务的控制及事务边界
JDBC对事务的操作是基于Connection来进行控制的,具体代码如下:
try {
//开启事务
connection.setAutoCommit(false);//不自动提交
//做业务操作
//doSomething();
//提交事务
connection.commit();
}catch(Exception e){
//回滚事务
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
- 但,注意,事务的边界我们是放在业务层进行控制,因为业务层通常包含多个dao层的操作。
60.谈谈事务的特点
原子性是基础,隔离性是手段,
一致性 是约束条件,而持久性是我们的目的。
-
ACID
原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持久性( Durability )
-
原子性:
事务是数据库的逻辑工作单位,事务中包含的各操作要么都完成,要么都不完成 (要么一起成功,要么一起失败)
-
一致性:
事务一致性是指数据库中的数据在事务操作前后都必须满足业务规则约束。
比如A转账给B,那么转账前后,AB的账户总金额应该是一致的。 -
隔离性:【隔离级别】
一个事务的执行不能被其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
(设置不同的隔离级别,互相干扰的程度会不同) -
持久性:
事务一旦提交,结果便是永久性的。即使发生宕机,仍然可以依靠事务日志完成数据的持久化。
日志包括回滚日志(undo)和重做日志(redo),当我们通过事务修改数据时,首先会将数据库变化的信息记录到重做日志中,然后再对数据库中的数据进行修改。这样即使数据库系统发生奔溃,我们还可以通过重做日志进行数据恢复。
61.谈谈事务的隔离级别
4个隔离级别:解决并发情况下事件的安全性问题
READ UNCOMMITTED 读未提交,脏读、不可重复读、幻读有可能发生。(×)
READ COMMITTED 读已提交,可避免脏读的发生,但不可重复读、幻读有可能发生。(Oracle)
REPEATABLE READ 可重复读,可避免脏读、不可重复读的发生,但幻读有可能发生。(MySql)
SERIALIZABLE 串行化,可避免脏读、不可重复读、幻读的发生,但性能会影响比较大。
幻读,是指在本地事务查询数据时只能看到3条,但是当执行更新时,却会更新4条,所以称为幻读

357

被折叠的 条评论
为什么被折叠?



