几道最基础面试题
一、方法的参数传递
1、值传递与引用传递的理解
个人认为:不存在引用传递,均是值传递
2、方法传参是否会修改值
如下图所示代码:
对应的输出结果是:
值传递: 传的是值的副本
局部变量的作用域: 局部变量随着方法而产生,也随着方法而销毁
引用类型的修改: 修改的是堆中的数据值
String类型的存放: 存放于字符串常量池
,被final修饰
String类型的修改: 先判断字符串常量池中,有没有修改后的值
?有的话,直接指向原有的值;没有的话,再创建一个,指过去(其实就是修改了地址值
)
基础类型的包装类: 方法传参中,具有不可变性
解析代码:
main方法中: age变量的 值是20
personName变量的 值是地址值(“abc”在堆中的地址值)
str变量的 值是地址值(“abc”在堆中的地址值)
changeValue1方法中: age变量只是一个副本,原值是20,修改后是30 (基本类型)
但是在方法销毁后,这个age变量也被销毁,也就是这个副本被销毁
而main方法中的原件变量值并没有被修改
changeValue2方法中: personName变量只是一个副本,修改时是修改了地址值所指向堆中的数据值 (引用类型)
堆中的原值是“abc”,修改后是“xxx”,但是其地址值没变
但是在方法销毁后,这个personName变量也被销毁,也就是这个副本被销毁
而main方法中的原件变量值并没有被修改,还是原来的地址值,此时该地址值指向的堆中数据却被修改了
changeValue3方法中: str变量只是一个副本,修改时是修改了地址值
(String类型)
原值是“abc”在字符串常量池中的地址值,修改后是“xxx”在字符串常量池中的地址值
但是在方法销毁后,这个str变量也被销毁,也就是这个副本被销毁
而main方法中的原件变量值未被修改,还是那个地址值,此时该地址值指向字符串常量池中的数据未被修改
二、== 与 equals
1、== 与 equals 的使用要求
== : 可使用于基本类型,也能使用于引用类型
equals : 只能使用于引用类型
2、== 与 equals 的对比
如图所示代码:
对应的结果为:
解析代码:
== : 比较的就是 栈中变量的值 ;如果是 基本类型 ,就是比较数据值
;如果是 引用类型 ,就是比较地址值
凡是 new出来的对象 ,栈中变量的值都是地址值
;而 不同的new对象 ,地址值是不一样的
,所以 ==
比较百分百是false
equals: 属于Object类的方法;Object类中对其的实现就是用了 ==
所以只要一个类 没有重写equals方法 ,那么就等同于 ==
String类, 重写了equals方法 ,而且是 比较数据值的hashCode值
String类,重写了hashCode方法,只要数据值一样,hashCode值就一样
所以String变量中,只要 数据值一样 ,用equals方法比较就肯定是true
HashSet : 根据hashCode值比较是否是同一个值
;new出来的对象,hashCode值都不一样,除非重写了hashCode方法
String类,重写了hashCode方法,只要数据值一样,hashCode值就一样
所以上述代码中,结果如图所示
三、String 的各类比较
1、String 的存放
在 jdk1.8 之前,字符串常量池在方法区中
;为了减小方法区内存溢出风险,在 jdk1.8 之后 就把常量池移到java 堆中
了
String s = "abc"
:创建的对象,存储在字符串常量池中;在创建字符串对象之前,会先在 常量池中检查是否存在 abc 对象 ;存在,则直接返回常量池中 abc 对象的引用
;不存在,会在常量池中创建该对象,并将常量池中该对象的引用返回给对象 s
简单来说,就是String s = ""
创建的局部变量 s,其值是字符串常量池的地址值
String s = new String("abc")
:实际上 abc
本身就是字符串常量池中的一个对象,在运行 new String()
时,把常量池中的字符串 abc 复制到堆中;因此该方式 会在堆和常量池中都创建 abc 字符串对象; 最后把 java 堆中对象的引用返回给 s
简单来说,就是String s = new String("")
创建的局部变量 s,其值是堆的地址值
2、String 的 intern 方法
调用 String 的 intern 方法 时,先 判断字符串常量池中是否已经有对应的字符串(通过 String 的 equals 方法
比较);有的话,则直接返回常量池中对应的地址值;没有的话,就先在常量池中创建,再返回常量池中对应的地址值
所以:对于任意两个字符串 s 和 t ,s.intern() == t.intern()
比较;仅当s.equals(t)
为true时,才为true
简单来说,就是String s = s.intern()
创建的局部变量 s,其值是字符串常量池的地址值
3、String 的各类比较
如图所示代码:
对应的结果为:
解析: 上述代码中,栈中局部变量:
s1的值是 字符串常量池 的地址值,s2的值是 堆 的地址值;所以两个地址值不一样
s1和s5的 字符串值都不一样 了,字符串常量池中的地址值肯定也不一样
s1和s6的 字符串值都不一样 了,字符串常量池中的地址值肯定也不一样
s1和s6.intern()的 字符串值都不一样 了,字符串常量池中的地址值肯定也不一样
s2的值是 堆 的地址值,s2.intern()的值是 字符串常量池 的地址值;所以两个地址值不一样
四、代码块与构造方法的执行顺序
1、普通代码块的概念与执行顺序
如图所示代码:
对应的结果为:
普通代码块: 在方法或语句中出现的{...}
;就称为普通代码块
;普通代码块可以多次执行
普通代码块的执行顺序: 两部分普通代码块,执行顺序是“先出先执行”
2、构造代码块与构造方法的执行顺序
1)构造代码块的 概念
直接在类中定义且 没有用static修饰、没有名字 的{...}
代码块;称为构造代码块
;构造代码块可以 多次执行
2)构造代码块的 执行顺序
两部分构造代码块,执行顺序是“先出先执行”
3)构造代码块的 执行时机
构造代码块在每次创建对象时都会被调用
;是在构造方法方法中的隐式调用
,在隐式调用父类构造方法之后
4)构造代码块与构造方法的优先级
如图所示代码:
对应的结果为:
解析: 构造代码块优先于构造方法
3、静态代码块、构造代码块、构造方法的优先级
1)静态代码块、构造代码块、构造方法的 优先级
静态代码块 > 构造代码块 > 构造方法
静态代码块只执行一次;构造代码块和构造方法可多次执行
2)无继承情况例子说明
如图所示代码:
对应的结果为:
解析:
(1)在运行CodeBlock03的 main方法前,生成CodeBlock03的模板,此时运行了CodeBlock03的静态代码块
(2)之后运行CodeBlock03的main方法
(3)调用Code的构造方法,需要 先生成Code的模板,才能调用构造方法;此时优先运行了Code的静态代码块
(4)运行完Code的静态代码块之后,需要先运行Code的构造代码块
(5)运行完Code的构造代码块之后,才能运行Code的构造方法
之后的调用,由于 静态代码块只执行一次 ,所以在原本的顺序下,踢出静态代码块即可
3)有继承情况例子说明
如图所示代码:
对应的结果为:
解析:
(1)运行TestStaticSeq的main方法
(2)调用子类的构造方法,需要 先生成父类的模板,才能生成子类的模板;此时优先运行了父类的静态代码块
(3)生成父类的模板后,要先生成子类的模板;才能运行子类的构造方法,此时优先运行了子类的静态代码块
(4)运行子类构造方法,先 调用父类构造方法(隐式);调用父类构造方法,需先运行父类构造代码块
(5)运行了父类构造代码块之后,才会运行父类的构造方法
(6)在子类构造方法中,调用完父类构造方法后(隐式),会调用子类的构造代码块
(7)运行完子类的构造代码块之后,才会运行子类的构造方法
之后的调用,由于 静态代码块只执行一次 ,所以在原本的顺序下,踢出静态代码块即可
4)构造方法的 隐式三步
在 调用一个类的构造方法时(无论是无参还是有参) ,都会有一个隐式三步
:
(1)调用父类的构造方法(无参调无参,有参调有参)
(2)给非静态成员变量赋予显示值
(3)调用自身的构造代码块
具体如下:下面代码中,1、2、3步就是隐式3步
;最后还有一个隐式 return 返回
注意: 隐式三步的第二步和第三步是不固定的,哪个写前面就先哪个
public class Father{
private int id = 007;
private String name = "零零发";
private int age = 98;
private float height = 1.78F;
public Teacher() {
// 1、super(); 调用父类的构造方法(无参调无参,有参调有参)
// 2、id = 007; 给非静态成员变量赋予显示值(在JVM内存程度上,真正的关联变量名和变量值)
.
.
.
// 3、{...} 调用自身的构造代码块
... // 自己手写的代码
// return; 隐式返回
}
五、linux的常用命令
1、top:查看整体机器性能
进程和程序,看看哪个进程占比较高
%CPU:CPU使用占比
%MEM:内存使用占比
%Cpu(s):Cpu使用情况(在多核机器时,可通过连按6次1键;来查看每个CPU的使用情况
)
id:idle,空闲率(越大越好)
load average:负载的平均数;如果有三个数,就是 1分钟、5分钟、15分钟的系统平均负载量
三个数相加,除以3,乘以百分百;如果高于60%,说明系统负载严重
按键 q,退出
2、uptime:简化版的查看整体机器性能
一样有 load average
3、free -m:查看内存情况
free: 按字节展示,自己的换算
free -g: 以 g 为单位展示,没小数;所以误差比较大
所以:建议使用free -m
4、df -h:查看硬盘情况
df: 按字节展示,自己的换算
所以:建议使用df -h
5、vmstat -n 2 3:查看CPU情况,每2秒一次,显示3条记录
看头的进程数,看尾的CPU使用情况
6、iostat - xdk 2 3:查看磁盘io情况
看最后的 %util ,说明磁盘IO高,可能需要进行SQL语句优化
7、chmod:权限命令
8、ifconfig:查看ip
六、Spring 的 bean作用域
Spring 的 bean 有 4种 作用域,分别是:singleton、prototype、request、session
1、singleton: 单例的作用域,默认值;在 初始化 IOC 容器时 创建对应的唯一的bean
,之后再也不创建
2、prototype: 原型的作用域;这里的原型指的是每一个 getBean() 方法
都会创建自己的bean
3、request: 每一次请求都会实例化一个bean
4、session: 每一个会话都会实例化一个bean
通过 scope
属性进行指定
七、Spring支持的常用数据库事务传播行为与隔离级别
1、事务原理
依赖于真正的数据库事务,使用AOP实现;以下提到的代理默认使用动态代理(AOP中)
1)纯JDBC操作数据库,实现事务
(1)获取连接 Connection con = DriverManager.getConnection()
(2)开启事务con.setAutoCommit(true/false)
(3)执行CRUD
(4)提交事务/回滚事务 con.commit() / con.rollback()
(5)关闭连接 conn.close()
2)spring操作数据库,实现事务:无需上述的(2)、(4)两步,由spring自动完成
(1)配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识
(2)spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)
(3)真正的数据库层的事务提交和回滚是通过 binlog 或者 redo log 实现的
2、传播行为
1)传播行为的理解: 当一个方法调用另一个方法时,两个方法均有事务时,对多个事务的处理机制
2)传播行为的类型: 有 7种 传播行为,但是 常用的就两种 ;required、requires_new
3)传播行为的设置: 通过 propagation 属性进行指定
3、数据库事务并发可能出现的问题
1)脏读: 线程A在修改数据库,线程B读取了修改后的值进行后续操作;线程A却发生了 回滚 ,线程B读取的数据就是脏读
2)不可重复读: 线程A读取了一次数据,线程B修改了这个数据,线程A再次读取时,出现两次读取不一致
3)幻读: 线程A读取了数据行,线程B增加或删除了n行,线程A再次读取数据行时,出现两次读取行数不一致
3、隔离级别
1)隔离级别的理解: 针对 第2点的三种问题 的处理级别
2)隔离级别的类型: 有 4种 传播行为
(1)read uncommitted:读未提交
(2)read committed:读已提交
(3)repeatable read:可重复读
(4)serializable:串行化
由于 隔离级别越高 ,数据一致性越好,但并发性越差
所以 常用的就两种 ;read committed、repeatable read
3)隔离级别的设置: 通过 isolation 属性进行指定
4)Oracle 和 Mysql 对隔离级别的支持情况:
八、SpringMVC 大概的工作流程
九、Mybatis 中实体类属性名和数据表字段名不一致的解决办法
1、sql 语句中对字段名进行起别名
2、在 Mybatis 的全局配置文件中开启驼峰形式
3、在 Mapper 映射文件中使用 resultMap 来自定义映射规则
十、Redis持久化的基础
1、RDB 持久化
在 指定的时间间隔 内,将内存中的数据集快照写入磁盘
恢复时,直接将快照文件读到内存中
优点: 节省磁盘空间;恢复速度快
缺点: 数据庞大时比较消耗性能;有可能会丢失最后一次快照之后的数据
2、AOF 持久化
以日志的方式来记录每个写操作,只追加文件不改写文件
Redis启动之初会读取该文件重新构建数据;也就是Redis重启时,根据日志文件内容从前至后执行一次
,已完成恢复工作
优点: 备份机制更稳健;丢失数据概率更低;可以处理误操作
缺点: 比RDB占用更多的磁盘空间;恢复速度慢;每次读写都同步的话,有一定性能压力;存在个别bug,没办法恢复
十一、Mysql 的索引创建时机
1、索引的本质
2、哪些情况需要创建索引
1)主键自动创建唯一索引
2)频繁作为查询条件的字段
3)与其他表关联的字段(外键关系)
4)查询中需要统计或者分组的字段
5)查询中需要排序的字段
6)单键索引和组合索引,组合索引的性价比更高
3、哪些情况不要创建索引
1)表记录太少
2)经常增删改的表或者字段
3)查询条件中用不到的字段
4)过滤性不好的字段
十二、JVM 垃圾回收机制的基础
1、发生在哪个部分:堆
2、gc 有几种:minor gc;full gc
3、gc 的算法:
1)引用计数算法:
2)复制算法: 主要是 minor gc
在使用
3)标记清除算法: 主要是 full gc
在使用
4)标记压缩算法: 主要是 full gc
在使用
5)标记清除压缩算法: 主要是 full gc
在使用; 3)和4)的结合 ;先标记、后清除;等到内存碎片较多时,进行压缩
十三、Redis在项目中的使用场景
十四、几种常用的设计模式
1、单例设计模式应用场景
- 网站的计数器:一般也是单例模式实现,否则难以同步
- 应用程序的日志应用:一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加
- 数据库连接池的设计:一般也是采用单例模式,因为数据库连接是一种数据库资源
- 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,都生成一个对象去读取
- Application:也是单例的典型应用
- Windows的Task Manager (任务管理器):就是很典型的单例模式
- Windows的Recycle Bin (回收站):也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例
2、模板方法设计模式应用场景
abstract class Template {
public final void getTime() {
long start = System.currentTimeMillis();
code();
long end = System.currentTimeMillis();
System.out.println("执行时间是:" + (end - start));
}
public abstract void code();
}
class SubTemplate extends Template {
public void code() {
for (int i = 0; i < 10000; i++) {
System.out.println(i);
}
}
}
- 数据库访问的封装
- Junit单元测试
- Hibernate中模板程序
- JavaWeb的Servlet中关于 doGet / doPost 方法调用
- Spring中JDBCTemlate、HibernateTemplate等
3、代理模式设计模式应用场景
interface Network {
public void browse();
}
// 被代理类
class RealServer implements Network { @Override
public void browse() {
System.out.println("真实服务器上网浏览信息");
}
}
// 代理类
class ProxyServer implements Network {
private Network network;
public ProxyServer(Network network) {
this.network = network;
}
public void check() {
System.out.println("检查网络连接等操作");
}
public void browse() {
check();
network.browse();
}
}
public class ProxyDemo {
public static void main(String[] args) {
Network net = new ProxyServer(newRealServer());
net.browse();
}
}
应用场景:
- 安全代理:屏蔽对真实角色的直接访问
- 远程代理:通过代理类处理远程方法调用(RMI)
- 延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象
比如你要开发一个大文档查看软件,大文档中有大的图片,有可能一个图片有100MB,在打开文件时,不可能将所有的图片都显示出来,这样就可以使用代理模式,当需要查看图片时,用proxy来进行大图片的打开
分类:
静态代理(静态定义代理类)
动态代理(动态生成代理类)
JDK自带的动态代理,需要反射等知识
十五、几道网红题
1、看似考察参数传递,实则不是
解决办法:
1)运行 method 方法的打印,不运行 main 的打印
2)捕获打印流,重写打印流
2、数组除以首元素后遍历
定义一个int型的数组:int[] arr = new int[]{12,3,3,34,56,77,432};让数组的每个位置上的值去除以首位置的元素,得到的结果,作为该位置上的新值;遍历新的数组
3、数组输出值问题
前者是地址值,后者是数据值
4、多态性问题
class Base {
int count = 10;
public void display() {
System.out.println(this.count);
}
}
class Sub extends Base {
int count = 20;
public void display() {
System.out.println(this.count);
}
}
public class FieldMethodTest {
public static void main(String[] args){
Sub s = new Sub(); // 此步,new 对象时,堆中的 new Sub() 对象中,已经包含父类的成员变量和方法(方法被覆盖)
System.out.println(s.count); // 20
s.display(); // 20
Base b = s; // 向上转型,多态性;赋值的是地址值
System.out.println(b == s); // true(==:在引用类型时,比较的是地址值)
System.out.println(b.count); // 10(向上转型的多态性,对于成员变量是无效的;b 调就是 b 的成员变量)
b.display(); // 20(向上转型的多态性,对方法生效;父类方法被子类覆盖)
}
}
5、包装类问题
// 三元运算符:后两者的值应是同类型,所以此处会将 Integer 转换成Double
Object o1 = true ? new Integer(1) : new Double(2.0);
System.out.println(o1); // 输出:1.0
Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
System.out.println(o2); // 输出:1
6、多线程问题
sleep() 和 wait() 有什么区别?
- 定义位置不一样: sleep()方法定义在Thread中;wait()方法定义在Object中
- 使用要求不一样: sleep()方法可以随时随地进行调用,wait()方法只能在同步代码块或同步方法中调用
- 唤醒方式不一样: sleep()方法只要时间到了就自动唤醒,wait()方法需要使用notify()或notifyAll()方法进行唤醒
- 是否存在异常不一样: sleep()方法的使用需要处理异常,wait()方法的使用不用处理异常
- 是否释放对象锁资源不一样: sleep()方法不会释放对象锁资源,wait()方法会释放对象锁资源
启动一个线程是用run()还是start()?
启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。run()方法就是正常的对象调用方法的执行,并不是使用分线程来执行的
synchronized和Lock的异同?
主要相同点: Lock能完成synchronized所实现的所有功能
主要不同点: Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放
7、String、StringBuffer、StringBuilder问题
结论:
- 常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量
- 只要其中有一个是变量,结果就在堆中
- 如果拼接的结果调用intern()方法,返回值就在常量池中
- 使用final修饰String时,其定义也是在常量池中
对比String、StringBuffer、StringBuilder
- String(JDK1.0):不可变字符序列;char[]数组长度默认为0,不可变
- StringBuffer(JDK1.0):可变字符序列、效率低、线程安全;char[]数组长度默认为16,可扩容(原来的2倍 + 2)
- StringBuilder(JDK 5.0):可变字符序列、效率高、线程不安全;char[]数组长度默认为16,可扩容(原来的2倍 + 2)
注意:作为参数传递的话,方法内部String不会改变其值,StringBuffer和StringBuilder
会改变其值
String str = null;
StringBuffer sb = new StringBuffer();
sb.append(str);
System.out.println(sb.length());// 4
System.out.println(sb);// null ; 在此处,这是一个字符串,不是关键词
StringBuffer sb1 = new StringBuffer(str);
System.out.println(sb1);// 运行到此处时,报错
8、集合问题
@Test
public void testListRemove() {
List list = new ArrayList();
list.add(1); // List中不能放基本类型,会进行自动装箱
list.add(2);
list.add(3);
list.add(4);
updateList(list);
}
private static void updateList(List list) {
// 第一种:
list.remove(2); // 2的含义,有可能是索引,也有可能是值;但是值的话是要进行自动装箱,比较麻烦;所以会被识别为索引
System.out.println(list); // 1、2、4
// 第二种:
list.remove(new Integer(2)); // 2的含义,只能是值;索引是基本类型的int
System.out.println(list); // 1、4
}
// 1:表示Person重写了hashCode()和equals()方法
// 2:表示Person没有重写hashCode()和equals()方法
HashSet set = new HashSet();
Person p1 = new Person(1001,"AA");
Person p2 = new Person(1002,"BB");
set.add(p1);
set.add(p2);
p1.name = "CC";
set.remove(p1);
System.out.println(set); // 1:输出2个; 2:输出1个
set.add(new Person(1001,"CC"));
System.out.println(set); // 1:输出3个; 2:输出2个
set.add(new Person(1001,"AA"));
System.out.println(set); // 1:输出4个; 2:输出3个