Java 后端面试题 day 01《Java 后端面试经》专栏文章索引:
Java 后端面试题 day 01
Java 后端面试题 day 02
Java 后端面试题 day 03
1. 面向对象2. JDK、JRE 和 JVM 三者之间的区别3. == 和 equals4. final
4.1 为什么局部内部类和匿名内部类只能访问局部 final 变量? 5. String、StringBuffer 和 StringBuilder 区别及使用场景6. 重载和重写的区别7. 接口和抽象类的区别8. List 和 Set 的区别9. hashCode 与 equals
9.1 hashCode 介绍9.2 为什么要有 hashCode 10. ArrayList 和 linkedList 区别11. HashMap 和 HashTable 的区别?底层实现是什么?12. ConcurrentHashMap 原理,jdk7 和 jdk8 的区别13. 如何实现一个 IOC 容器14. 什么是字节码?采用字节码的好处是什么?15. Java 类加载器有哪些16. 双亲委托模型
1. 面向对象什么是面向对象?
对比面向过程,是两种不同的处理问题的角度。面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要做什么。
比如:洗衣机洗衣服
面向过程会将任务拆解成一系列的步骤(函数),1、打开洗衣机 -----> 2、放衣服 -----> 3、放洗衣粉 -----> 4、清洗 -----> 5、烘干
面向对象会拆出人和洗衣机两个对象:
人:打开洗衣机 放衣服 放洗衣粉
洗衣机:清洗 烘干
从以上例子能看出,面向过程比较直接高效,而面向对象更易于复用、扩展和维护
封装: 封装的意义在于,明确标识出允许外部使用的所有成员函数和数据项。
内部细节对外部调用透明,外部调用无需修改或者关心内部实现。
- javabean 的属性私有,提供 get、set 对外访问,因为属性的赋值或者获取逻辑只能由 javabean 本身决定。而不能由外部胡乱修改。
public String name;
public void setName(String name) {
// 该 name 有自己的命名规则,明显不能由外部直接赋值
this.name = "java" + name;
}
- ORM 框架
操作数据库,我们不需要关心链接是如何建立的、sql 是如何执行的,只需要引入 mybatis,调方法即可。
继承: 继承基类的方法,并做出自己的改变和/或扩展。
子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的。
多态: 基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。
继承,方法重写,父类引用指向子类对象。
父类类型 变量名 = new 子类对象; 变量名.方法名();
无法调用子类特有的功能
2. JDK、JRE 和 JVM 三者之间的区别JDK: Java Development Kit,java 开发工具,提供给开发人员使用
JRE: Java Runtime Environment,java 运行时环境,提供给运行 java 程序的用户使用
JVM: Java Virtual Machine,java 虚拟机,解释 class 文件
== 对比的是栈中的值,基本数据类型对比的是变量值,引用类型对比的是堆中内存对象的地址
equals:object 中默认也是采用 == 比较,通常会重写
Object
public boolean equals(Object obj) {
return (this == obj);
}
String
public boolean equals(Object object) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i]) return false;
i++;
}
return true;
}
}
return false;
}
上述代码可以看出,String 类中被复写的 equals() 方法其实是比较两个字符串的内容。
public class StringDemo {
public static void main(String args[]) {
String str1 = "Hello";
String str2 = new String("hello");
String str3 = str2;
System.out.println(str1==str2); // false
System.out.println(str1==str3); // faslse
System.out.println(str2==str3); // true
System.out.println(str1.equals(str2)); // true
System.out.println(str1.equals(str3)); // true
System.out.println(str2.equals(str3)); // true
}
}
4. final
修饰类:表示类不可被继承修饰方法:表示方法不可被子类覆盖,但是可以重载修饰变量:表示变量一旦被赋值就不可以更改它的值
(1)修饰成员变量
如果 final 修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值如果 final 修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值
(2)修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用 final 修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对 final 变量赋初始值(仅一次)
public class FinalVar {
final static int a = 0; // 在声明的时候需要赋值或者静态代码块赋值
final int b = 0; // 在声明的时候需要赋值或者代码块中赋值或者构造器赋值
public static void main(String[] args) {
final int localA; // 局部变量只声明没有初始化,不会报错,与 final 无关
localA = 0; // 在使用之前一定要赋值
// localA = 1; 但是不允许第二次赋值
}
}
(3)修饰基本类型数据和引用类型数据
如果是基本数据类型的变量,则其数值一旦在初始化之后便不能修改如果是引用类型的变量,则在其初始化之后便不能再让其指向另一个对象。但是引用的值是可变的。
public class FinalReferenceTest {
public static void main() {
final int[] iArr = {1,2,3,4};
iArr[2] = -3; // 合法
iArr = null;
final Person p = new Person(25);
p.setAge(24); // 合法
p = null; // 非法
}
}
4.1 为什么局部内部类和匿名内部类只能访问局部 final 变量?
编译之后会生成两个 class 文件,Test.class 和 Test1.class
public class Test {
public static void main(String[] args) {
}
// 局部 final 变量 a, b
public void test(final int b) {
final int a = 10;
// 匿名内部类
new Thread() {
public void run() {
System.out.println(a);
System.out.println(b);
}
}.start();
}
class OutClass {
private int age = 12;
public void outPrint(final int x) {
class InClass {
public void InPrint() {
System.out.println(x);
System.out.println(age);
}
}
new InClass().InPrint();
}
}
}
首先需要知道一点的是:内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍然可以访问它,实际访问的是局部变量的 “copy”。这样就好像延长了局部变量的生命周期。
将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题?
就将局部变量设置为 final, 对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协,使得局部变量与内部类建立的拷贝保持一致。
5. String、StringBuffer 和 StringBuilder 区别及使用场景String 是 final 修饰的,不可变,每次操作都会产生新的 String 对象
StringBuffer 和 StringBuilder 都是在原对象上操作
StringBuffer 是线程安全的,StringBuilder 是线程不安全的
StringBuffer 方法都是 synchronized 修饰的
性能:StringBuilder > StringBuffer > String
场景:经常需要改变字符串内容时使用前面两个。
想要效率就优先使用 StringBuilder,多线程使用共享变量时使用 StringBuffer.
6. 重载和重写的区别重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类,如果父类方法访问修饰符为 private 则子类就不能重写该方法,发生在运行时。
public int add(int a, String b) public String add(int a, Stirng b) // 编译报错7. 接口和抽象类的区别
抽象类可以存在普通成员函数,而接口中只能存在 public abstract 函数。抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。抽象类只能继承一个,接口可以实现多个。
接口的设计目的,是对类的行为进行约束(更准确的说是一种”有“约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现进行限制。
而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而 A 减 B 的部分,留给各个子类自己实现。正式因为 A - B 在这里没有实现,所以抽象类不允许实例化出来(否则当调用 A-B 时,无法执行)。
抽象类是对类本质的抽象,表达的是 is a 的关系,比如:BMW is a Car. 抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
而接口是对行为的抽象,表达的是 like a 的关系。比如:Bird like a Aircraft(像飞行器一样可以飞),但其本质上 is a Bird。接口的核心是定义行为,既实现类可以做什么,至于实现类主体是谁、如何实现的,接口并不关心。
使用场景:当你关注一个事物的本质的时候,用抽象类:当你关注一个操作的时候,用接口。
抽象类的功能要远超过接口,但是,定义抽象类的代价高,因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化好多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口,在设计阶段会降低难度。
8. List 和 Set 的区别List: 有序,按对象进入的顺序保存对象,可重复,允许多个 null 元素对象,可以使用 iterator 取出所有元素,再逐一遍历,还可以使用 get(int index) 方法获取指定下标的元素。Set: 无序,不可重复,最多有一个 null 元素对象,取元素时只能用 iterator 接口取得所有元素,再逐一遍历各个元素,并没有提供下标访问的方法。 9. hashCode 与 equals 9.1 hashCode 介绍
hashCode() 的作用是获取哈希码,也成为散列码。它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,java 中的任何类都包含有 hashCode() 函数。散列表存储的是键值对(key-value),它的特点是:能根据 ”键“快速的检索出对应的 ”值“。这其中就利用到了散列码!(可以快速找到所需要的对象)
9.2 为什么要有 hashCode以 ”HashSet“ 如何检查重复” 为例子来说明为什么要有 hashCode
对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,看该位置是否有值,如果没有,hashSet 会假设对象没有重复出现,但是如果发现有值,这时就会调用 equals() 方法来检查两个对象是否真的相同。如果两者相同,hashSet 就不会让其加入,操作成功。如果不同的话,就会重新散列到其他位置。这样就会大大减少了 equals 的次数,相应就大大提高了执行速度。
如果两个对象相等,则 hashCode 一定也是相同的两个对象相等,对两个对象分别调用 equals 方法都返回 true两个对象相等的 hashcode 值,它们不一定是相等的因此,如果 equals 方法被覆盖过,则 hashCode 方法也必须被覆盖hashCode() 的默认行为是对堆上的对象产生独特值,如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) 10. ArrayList 和 linkedList 区别
ArrayList: 基于动态数组,连续内存存储,适合下标访问(随机访问)。扩容机制:因为数组长度固定,超过长度存数据需要新建数组,然后将旧数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提高性能,甚至超过 linkeList (需要创建大量的 node 对象)。
linkedList: 基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询,需要逐一遍历
遍历 linkedList 必须使用 iterator 不能使用 for 循环,因为每次 for 循环体内通过 get(i) 取得某一元素时都需要对 list 重新进行遍历,性能消耗极大。
另外不要试图使用 indexOf 等返回元素索引,并利用其进行遍历,使用 indexOf 对 list 进行了遍历,当结果为空时会遍历整个列表。
11. HashMap 和 HashTable 的区别?底层实现是什么?区别:
(1)HashMap 方法没有 synchronized 修饰,线程非安全,HashTable 线程安全
(2)HashMap 允许 key 和 value 为 null, 而 HashTable 不允许
底层实现: 数组+链表实现
jdk8 开始链表高度为 8、数组长度为 64,链表转变为红黑树,元素以内部类 Node 节点存在
计算 key 的 hash 值,二次 hash 然后对数组长度取模,对应到数组下标如果没有产生 hash 冲突(下标位置没有元素),则直接创建 node 存入数组如果产生 hash 冲突,先进行 equal 比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到 8,并且数组长度到 64 则转变为红黑树,长度低于 6 则将红黑树转回链表key 为 null,存在下标 0 的位置
数组扩容:如果当数组长度超过默认分配的长度,则重新创建一个数组,给定一个更大的新的长度,然后将旧数组的元素复制到新数组中
12. ConcurrentHashMap 原理,jdk7 和 jdk8 的区别jdk7:
数据结构:ReentrantLock+Segment+HashEntry,一个 Segment 中包含一个 HashEntry 数组,每个 HashEntry 又是一个链表结构
元素查询:二次 hash,第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表的头部
锁:Segment 分段锁 Segment 继承了 ReentrantLock,锁定操作的 Segment,其他的 Segment 不受影响,并发度为 Segment 个数,可以通过构造函数指定,数组扩容不会影响其他的 Segment
get() 方法无需加锁,volatile 保证
jdk8:
数据结构:synchronized+CAS+Node+红黑树,Node 的 val 和 next 都用 volatile 修饰,保证可见性
查找、替换和赋值操作都是用 CAS
锁:锁链表的 head 节点,不影响其他元素的读写,锁的粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容
读操作无锁:
Node 的 val 和 next 使用 volatile 修饰、读写线程对该变量互相可见
数组用 volatile 修饰,保证扩容时被读线程感知
- 配置文件配置包扫描路径递归包扫描获取 .class 文件反射,确定需要交给 IOC 管理的类对需要注入的类进行依赖注入
配置文件中指定需要扫描的包路径定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解,获取配置文件注解从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息及文件夹信息,我们将当前路径下所有以 .class 结尾的文件添加到一个 Set 集合中进行存储遍历这个 set 集合,获取在类上有指定注解的类,并将其交给 IOC 容器,定义一个安全的 Map 用来存储这些对象遍历这个 IOC 容器,获取到每一个类的实例,判断里面是否有依赖其他的类的实例,然后进行递归注入 14. 什么是字节码?采用字节码的好处是什么?
java 中的编译器和解释器:
java 中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个共同的接口。
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在 java 中,这种虚拟机理解的代码叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,java 源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就解释了 Java 的编译与解释并存的特点。
java 源代码 ----> 编译器 -----> jvm 可执行的 java 字节码(即虚拟指令)-----> jvm -----> jvm 中的解释器 -----> 机器可执行的二进制机器码 -----> 程序运行
采用字节码的好处:
java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率的问题,同时又保留了解释型语言可移植的特点。所以 java 程序运行时比较高效。而且,由于字节码并不针对一种特定的机器,因此,java 程序无须重新编译便可在多种不同的计算机上运行。
JDK 自带有三个类加载器:boostarp ClassLoader、ExtClassLoader、AppClassLoader
BoostrapClassLoader 是 ExtClassLoader 的父类加载器,默认负责加载 %JAVA_HOME%/lib 下的 jar 包和 class 文件。
ExtClassLoader 是 AppClassLoader 的父类加载器,负责加载 %JAVA_HOME%/lib/ext 文件夹下的 jar 包和 class 类。
AppClassLoader 是自定义类加载器的父类,负载加载 classpath 下的类文件。系统类加载器,线程上下文加载器
继承 ClassLoader 实现自定义类加载器
16. 双亲委托模型
双亲委派模型的好处:
主要是为了安全性,避免用户自己编写的类动态替换 java 的一些核心类,比如 Spring同时也避免了类的重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就是不同的两个类



