本文整理了一些面试常考的Java基础知识点,方便同学们复习。
基础概念
谈谈你对Java的理解
-
平台无关性,一次编译到处运行。
-
GC垃圾回收机制:Java无需像C++一样手动管理内存。
-
Java的语言特性:泛型、反射、lambda表达式等。
-
面向对象:包括封装、继承、多态。
-
Java自身类库如:集合、并发库、网络库以及IO流。
-
异常处理。
Java 和 C++的区别
- Java不提供指针直接访问内存,程序的内存更加安全。
- Java的类是单继承,而C++的类是多继承(Java的接口是可以多继承的)。
- Java有自动垃圾回收机制,无需手动释放内存。
- Java可实现平台无关性,一次编译,不同系统平台都能运行。
Java平台无关性是如何实现的?
Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要重新编译,Java虚拟机在执行字节码的时候会将字节码转换成具体平台上的机器指令。所以Java也是一种编译与解释并存的语言。它既具备编译型语言的特征,也具备解释型语言的特征。
引用维基百科的介绍:
为了改善编译语言的效率而发展出的即时编译技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码。到执行期时,再将字节码直译,之后执行。Java与LLVM是这种技术的代表产物。
为什么JVM不直接将源码解析成机器码?
- 准备工作:每次执行前都需要进行各种检查。
- 兼容性:这样做使得其他语言也能够按照JVM的标准解析成字节码。
equals()和==的区别
== :比较的是变量(栈)在内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作。
要点:
- 可以比较操作符两端的操作数是否是同一个对象。 注:两边的操作数必须是同一类型的(可以是父类和子类之间)才能编译通过。
- 比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为
true,如:int a=10;与long b=10L;与double c=10.0;都是相同的(为true),因为它们都指向地址为10的堆。
equals:用来比较两个对象在内存中存放的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以equals()方法适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object类中的equals()方法返回的却是==的判断。但在某些类中,例如最典型的String类,它重写了equals()方法,调用equals()会逐一比较两个字符串的内容是否相等。
String、String StringBuffer 和 StringBuilder 的区别
三者的主要区别如下:
- 可变性:String是不可变的,每次操作String都会生成一个新的String对象。而StringBuilder和StringBuffer都继承自AbstractStringBuilder类,它们的底层都是可变的字符数组,可通过提供的
append等方法进行操作。 - 线程安全性:String和StringBuffer是线程安全的,而StringBuilder不是线程安全的。
- 性能:每次对String操作都会生成一个新的String对象,故频繁操作会导致性能开销增加,在频繁操作字符串时使用StringBuffer或StringBuilder能够获得较好的性能。StringBuilder性能比StringBuffer高10%~15%,但其线程不安全,故在单线程频繁操作字符串时,适合使用StringBuilder,多线程频繁操作字符串时适合使用StringBuffer。
补充:String类在处理字符串拼接时,重载了“+”运算符,底层是通过StringBuilder类调用append()方法实现的,拼接完成之后调用toString()得到一个String对象 。不过,在循环内使用“+”进行字符串拼接会造成StringBuilder类重复创建,导致性能消耗。
面向对象
面向对象与面向过程的区别
-
面向过程:面向过程是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发。
-
面向对象:面向对象是把构成问题的事物分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出高内聚,低耦合的系统。 但性能方面比面向过程差。
方法重写(覆盖)和方法重载的区别
方法重写(Override):也称为方法覆盖
- 方法重写发生在父类与子类之间
- 方法名,参数列表,返回类型(除非子类中方法的返回类型是父类中返回类型的子类)必须相同。
- 访问修饰符的限制一定要大于被重写方法的访问修饰符(public > protected > default > private)
- 重写方法一定不能抛出新的Checked Exception或者比重写方法声明更加宽泛的Checked Exception(参见下文异常的分类)
方法重载(Overload):
- 重载是同一个类中多态性的一种表现
- 重载要求同名方法的参数列表不同
- 重载时返回值类型可以相同也可以不同
接口和抽象类的异同
共同点:
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以用 default 关键在接口中定义默认方法)。
不同:
- 一个类只能继承一个类,但是可以实现多个接口。
- 接口支持多继承,即一个接口可以继承多个接口,间接解决了 Java 中类不能多继承的问题。
- 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值。抽象类的成员变量默认为default,可在子类中被重新定义,也可被重新赋值。
以下表格可直观对比接口和抽象类。
| 参数 | 抽象类 | 接口 |
|---|---|---|
| 实现 | 子类使用 extends 关键字来继承抽象类,如果子类不是抽象类,则需要提供抽象类中所有声明的方法的实现。 | 子类使用 implements 关键字来实现接口,需要提供接口中所有声明的方法的实现。 |
| 访问修饰符 | 可以用 public、protected 和 default 修饰 | 默认修饰符是 public,不能使用其它修饰符 |
| 方法 | 可以包含普通方法 | 只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现 |
| 变量 | 既可以定义普通成员变量,也可以定义静态常量 | 只能定义静态常量,不能定义普通成员变量 |
| 构造方法 | 抽象类里的构造方法并不是用于创建对象,而是让其子类调用这些构造方法来完成属于抽象类的初始化操作 | 没有构造方法 |
| 初始化块 | 可以包含初始化块 | 不能包含初始化块 |
| main 方法 | 可以有 main 方法,并且能运行 | 没有 main 方法 |
| 与普通Java类的区别 | 抽象类不能实例化,除此之外和普通 Java 类没有任何区别 | 是完全不同的类型 |
| 运行速度 | 比接口运行速度要快 | 需要时间去寻找在类种实现的方法,所以运行速度稍微有点慢 |
深拷贝和浅拷贝的区别
- 浅拷贝:浅拷贝会在堆上创建一个新的对象,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
- 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
异常
Java的异常处理机制主要回答了三个问题:
- What:异常类型回答了什么被抛出
- Where:异常堆栈跟踪回答了在哪抛出
- Why:异常信息回答了为什么被抛出
异常分类
框架图如下:

从图中可以看出,异常分为:Error和Exception,Error是程序无法处理的系统错误,程序中断无法进行恢复,编译器不做检查。例如JVM中的StackOverFlowError、OutOfMemoryError等。Exception是程序可以处理的异常,捕获后可以恢复程序。
Exception分为:
-
Checked Exception:Checked Exception应被
try catch处理或throws关键字抛出,若没有处理则无法通过编译。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的Checked Exception有: IO 相关的异常、ClassNotFoundException、SQLException。 -
Unchecked Exception:在编译过程中,我们即使不处理Unchecked Exception也能正常通过编译。
RuntimeException及其子类都统称为Unchecked Exception。
常见的Runtime Exception如下:
- NullPointerException:空指针异常
- ArrayIndexOutOfBoundsException:数组越界异常
- IllegalArgumentException:传递非法参数异常
- ClassCastException:类型强制转换异常
- NumberFormatException:数字格式异常(它是
IllegalArgumentException的子类) - ......
非RuntimeException:
- ClassNotFoundException :找不到指定Class异常
- IOException:IO操作异常
Error:
- NoClassDefFoundError:找不到class定义错误
- StackOverflowError:深递归导致栈被耗尽而抛出的异常
- OutOfMemoryError:内存溢出异常
补充:导致NoClassDefFoundError的原因:
- 类依赖的class或者jar不存在。
- 类文件存在,但是存在不同的域中。
- 大小写问题,javac编译时是无视大小写的,很有可能编译出来的class文件与预期不同。
异常处理机制
Java中的异常处理机制主要如下:
- 抛出异常:创建异常对象,交由运行时系统进行处理。
- 捕获异常:寻找合适的异常处理器处理异常,否则程序终止运行。
try-catch-finally
- try:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
- catch:用于处理try捕获到的异常。
- finally:无论是否捕获或处理异常,finally块里的语句都会被执行。当在 try块或 catch块中遇到 return 语句时,finally 语句块将在return之前被执行。
注:finally中的代码有可能不会被执行。如遇到下列情况,finally就不会被执行:
- 程序所在的线程死亡
- 关闭CPU
异常处理的原则
- 具体明确:抛出的异常应能够通过异常类名和message准确说明异常的类型和产生异常的原因。
- 提早抛出:应尽可能早地发现并抛出异常,便于精确定位问题。
- 延迟捕获:异常的捕获和处理应尽可能延迟,让掌握更多信息的作用域来处理异常。
在项目中使用异常
我在项目中使用异常,以RESTful风格的Web应用为例,我会先设计一个ApiException类作为父类,这个类继承自RuntimeException,它是所有业务中异常类的父类。在业务中,我会具体自定义5-10个异常类,例如NotFoundException资源不存在异常,InternalServerException服务器内部异常...它们是具体的业务异常类,都继承自ApiException。这样在具体抛出时就可以很方便地指定异常的具体信息以及错误码,便于维护。当然Spring也提供了一系列的异常框架,我们将在Spring篇章中进行介绍。
下面是我设计的ApiException异常类:
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public class ApiException extends RuntimeException{
protected String errCode;
protected Integer httpCode;
protected String errMsg;
protected String detail;
public ApiException(String errCode,Integer httpCode,String errMsg,String detail){
this.errCode = errCode;
this.httpCode = httpCode;
this.errMsg = errMsg;
this.detail = detail;
}
public ApiException(String errCode,Integer httpCode,String errMsg){
}
public String getErrCode() {
return errCode;
}
public void setErrCode(String errCode) {
this.errCode = errCode;
}
public Integer getHttpCode() {
return httpCode;
}
public void setHttpCode(Integer httpCode) {
this.httpCode = httpCode;
}
public String getErrMsg() {
return errMsg;
}
public void setErrMsg(String errMsg) {
this.errMsg = errMsg;
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.JSON_STYLE)
.append("errCode", getErrCode())
.append("httpCode", getHttpCode())
.append("errMsg", getErrMsg())
.append("detail", getDetail())
.toString();
}
}
具体业务异常类举例:
InternalServerException.java
public class InternalServerException extends ApiException{
public InternalServerException(String detail) {
super("INTERNAL_SERVER_ERROR",500,"服务器内部错误",detail);
}
}
以上代码来源于我的Yeliheng-blog博客项目,感兴趣的同学可以前往看看:https://github.com/yeliheng/yeliheng-blog
异常处理的性能
先说结论,在Java中,try-catch的效率是比if-else低的。原因是:
- try-catch块会影响JVM的优化。在某些情况下,JVM无法对其指令进行重排序。
- 异常对象实例需要保存栈快照等信息,资源开销比较大。
泛型
泛型是JDK1.5之后的特性, 《Java 核心技术》中对泛型的定义是:
“泛型” 意味着编写的代码可以被不同类型的对象所重用。
泛型:是一种把明确类型的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,而这种参数类型可以用在类、方法和接口中,分别被称为泛型类、泛型方法、泛型接口。Java 的泛型是伪泛型,这是因为在运行时,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。
早期的Object类型可以接收任意的对象类型,但是在实际的使用中,会有类型转换的问题,所以Java提供了泛型来解决这个安全问题。
泛型的三种使用方式
-
泛型类
Demo
// 泛型标识无固定要求,常见的如T、E、K、V等形式的参数常用于表示泛型 // 在实例化泛型类时,必须指定T的具体类型 // T 表示一种特定的类型 // E 也是一种类型的意思,只不过通常代表集合中的元素 // K V 表示Java键值中的Key-Value // ? 这是一种无限的符号,代表任何类型都可以 public class GenericDemo<T> { private T test; public GenericDemo(T test) { this.test = test; } public T getTest() { return test; } }实例化:
Generic<Integer> genericInteger = new Generic<Integer>(123456); -
泛型接口
public interface GenericDemo<T> { public T method(); } -
泛型方法
public static <E> void genericDemo(E[] arr) { for (E e : arr) { // Do Something... } }
常见的用到泛型的场景
- 算法中:例如排序算法,二分查找算法等
- 项目中:为RESTful应用创建一个统一的全局返回json类
- ······
I/O
Java中的IO流分为哪几种?
- 按照流的流向分,可分为输入流和输出流。
- 按照操作单元划分,可分为字节流和字符流。
- 按照流的角色划分,可分为节点流和处理流。
BIO、NIO、AIO
BIO
BIO(Blocking I/O):即同步阻塞IO,应用程序发起一个操作后,会一直阻塞,直到内核把数据拷贝到用户空间,应用才能继续运行。
NIO
NIO(Non-blocking I/O):NIO在JDK1.4中被引入。它是同步非阻塞IO,应用发起一个IO操作后可返回执行其他操作,但是要进行轮询查看IO操作是否就绪。NIO是多路复用的同步非阻塞IO模型。
NIO的核心有:Channels、Buffers、Selectors。所有的IO在NIO中都由一个Channel开始,Channel可以理解为流,数据从Channel读到Buffer中,也可以从Buffer写到Channel中。
一些常见的Channel如下:
- FileChannel:FileChannel接口提供例如
transferTo()、transferFrom()等方法,能够将FileChannel中的数据拷贝到另外一个Channel,或者从另一个Channel中拷贝到FileChannel中。常用于大文件拷贝。从操作系统的层面上看,transferTo()和transferFrom()避免了两次用户态和内核态之间的上下文切换,即实现“零拷贝”,效率较高。 - DatagramChannel
- SocketChannel
- ServerSocketChannel
- ...
Selector:即多路复用器,它可以让一个线程能够同时管理多个客户端连接,当收到客户端数据后,Selector才会为其服务。
NIO组成如下:

AIO
AIO(Asynchronous I/O):AIO在JDK1.7中被引入。它是异步非阻塞IO,基于事件和回调机制实现的。应用发起IO操作之后会直接返回执行其他操作,不会阻塞在那里。当后台处理IO操作结束后,操作系统会通知该线程执行后续操作。
我们可以通过回调函数,实现CompletionHandler接口,调用时触发回调函数来进一步处理返回后的结果,也可以使用Future,通过isDone()查看是否准备好,通过get()方法等待返回数据。
总结
- BIO适用于连接数目较小且固定的架构;
- NIO适用于连接数目多且连接比较短的架构,例如聊天室;
- AIO适用于连接数目多且比较长的架构。
IO多路复用
IO多路复用模型有三种不同的操作方式,分别是select,poll,和epoll。它会调用系统级别的select/poll/epoll。Java的NIO模型就调用了系统底层的IO多路复用。在IO多路复用模型中,线程首先会发起select调用,询问内核数据是否准备就绪,等内核数据准备就绪后,用户线程会再发起read调用。
流程如下:

select、poll、epoll涉及到操作系统底层知识,不是本篇整理的内容。感兴趣的同学可自行查阅相关资料~
反射
谈谈什么是反射?
Java反射机制是在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。
用过反射吗?
Java反射的三种方法:
- 调用对象的
getClass()方法。 Class.forName("xxx.xxx.xxx")。- 类名.class。
常用方法:
newInstance():创建对象实例。getMethod():可以获取所有public的方法(包含继承和实现的方法)。getDeclaredMethod():获取指定对象的方法(不包含继承和实现方法)。setAccessible():传入true/false,当设为true时可访问私有方法。Class.getField():获取类中字段属性。Invoke():可以用于执行指定的方法。
反射机制的优缺点
优点:
- 能够运行时动态获取类的实例,提高灵活性;
- 与动态编译相结合;
缺点:
-
使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
解决方案:
-
通过
setAccessible(true)关闭JDK的安全检查来提升反射速度 -
创建一个类的实例时,有缓存会快很多
-
ReflectASM工具类,通过字节码生成的方式加快反射速度
-
-
相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)
反射会影响程序性能吗?
反射确实会导致性能问题。反射导致的性能问题和使用次数有关系,少量反射性能基本上没啥差别,反射调用次数很多时(网上有个数据是100次),性能差异会比较明显。直接访问实例的方式效率最高,其次是调用方法的方式,然后是反射访问实例的方式,最慢的是通过反射访问方法的方式。反射中的getMethod()和getDeclaredField()方法会比invoke()和set()方法耗时。反射时需要判断是否安全?是否允许这样操作?入参是否正确?是否能够在虚拟机中找到需要反射的类?一系列判断条件导致了反射会比较耗时。也有可能是因为调用native方法,需要使用JNI接口,导致了性能问题。最好不要频繁地使用反射,通过反射直接访问实例会比通过反射访问方法快很多,所以应该优先采用访问实例的方式。