0. 前言:
程序员面试本是一件再平常不过的事情,记得刚毕业的时候面试题背的滚瓜烂熟。但是在职程序员面试却是另一回事了,我们往往没有太多时间复习,特别是大龄程序员,工作日忙于工作,周末还要照顾家庭,一旦面临被优化的风险就很被动,难以在短时间内复习并找到工作。不要问我是怎么知道的,都是切身体会,在复习的过程中我也走了不少弯路,所幸最终结果令自己满意。 为了不让和我一样的程序员遇到同样的问题,我打算写这一系列的文章,这些文章不会像其他面经一般大而全,这些文章仅记录我在复习过程中认为重要的知识点,如果能帮助到你就太好了。
1. JRE和JDK有什么区别
一句话回答: JRE是Java程序的运行环境,而JDK是Java开发的工具包,包含了JRE和一系列开发工具。选择使用JRE还是JDK,取决于你是需要运行Java应用程序还是开发Java应用程序。
细节解释: JRE(Java Runtime Environment)
- 定义:Java运行时环境,是运行Java程序所必需的软件。
- 组成:包含Java虚拟机(JVM)和Java核心类库。
- 用途:使Java程序能够在计算机上运行,无需编译Java源代码。 JDK(Java Development Kit)
- 定义:Java开发工具包,是编写Java程序所需的完整开发环境。
- 组成:包含JRE的所有组件,加上Java编译器(javac)、Java文档生成器(javadoc)等开发工具。
- 用途:为Java开发者提供编写、编译、测试和调试Java程序所需的工具。 二者区别
- 功能:JRE仅用于运行Java程序,而JDK提供编写和编译Java程序所需的工具。
- 受众:JRE面向最终用户,JDK面向开发者。
- 组件:JDK包含JRE的所有组件,并额外提供开发工具。
2. Java语言是编译型语言还是解释型语言
一句话回答: Java是一种编译型与解释型相结合的语言,通过编译成字节码再由JVM解释执行,实现跨平台运行。
细节解释: Java是一种编译型和解释型语言的结合体。具体来说:
- 编译型:Java源代码(.java文件)首先被编译成字节码(.class文件)。这个过程是编译型的,因为它将源代码转换成了一种中间形式,但不是机器码。
- 解释型:Java字节码随后由Java虚拟机(JVM)在运行时解释执行。这个过程是解释型的,因为JVM逐行读取和执行字节码。 这种设计使得Java能够实现跨平台的特性,即"一次编写,到处运行"。字节码可以在任何安装了JVM的平台上运行,而不需要针对特定操作系统重新编译。以下是Java编译和运行的简要流程:
- 编写:开发者使用文本编辑器或集成开发环境(IDE)编写Java源代码。
- 编译:使用Java编译器(javac)将源代码编译成字节码。
- 运行:使用Java运行时环境(JRE)中的JVM来执行字节码。 Java的这种混合特性,既利用了编译型语言的效率,又保持了解释型语言的灵活性和跨平台能力。
3. 移位操作
一句话回答: 移位操作是Java中用于快速进行位运算的运算符,包括逻辑移位、算术移位和无符号移位。
细节解释: 在Java中,移位操作允许开发者对整数类型的数据进行位级别的操作,这在某些算法优化和底层编程中非常有用。以下是Java支持的三种移位操作:
- 逻辑移位(无符号移位) :使用
<<
运算符,将数字的所有位向左移动指定的位数,右边空出的位用0填充。这种移位操作不会改变数字的符号位。int result = 1 << 3; // 结果为8,相当于1乘以2的3次方
- 算术移位 :使用
>>
运算符,将数字的所有位向右移动指定的位数,左边空出的位用符号位填充。这种移位操作保留了数字的符号。int result = -1 >> 2; // 结果为-1,因为符号位被复制填充
- 无符号移位 :使用
>>>
运算符,将数字的所有位向右移动指定的位数,左边空出的位用0填充。这种移位操作不考虑数字的符号,适用于无符号整数类型的操作。int result = -1 >>> 2; // 结果为1073741823,符号位被0填充
移位操作在进行位运算时比乘除操作更快,因此在需要高效处理大数或位模式时非常有用,在面试中也常被问起。
4. 装箱类型
一句话回答: 装箱类型是Java中自动将基本数据类型转换为对应的包装类对象的过程。
细节解释: 在Java中,基本数据类型(如int
、double
等)与对象是不同的概念。基本类型直接存储原始数值,而对象则存储在堆上。装箱类型(或称为自动装箱)允许开发者在需要对象的地方使用基本类型,Java编译器会自动将基本类型转换为对应的包装类对象。 以下是Java的基本数据类型及其对应的装箱类型:
int
->Integer
double
->Double
float
->Float
char
->Character
long
->Long
short
->Short
byte
->Byte
boolean
->Boolean
例如,当您需要将int
类型赋值给Integer
类型的变量时,可以使用自动装箱:Integer num = 5; // 自动装箱,int转换为Integer对象
相对的,拆箱(或称为自动拆箱)是将包装类对象转换回基本数据类型的过程:int num = num.intValue(); // 自动拆箱,Integer对象转换为int
装箱和拆箱在集合操作、方法参数、泛型等场景中非常常见。然而,过度使用装箱类型可能导致性能问题,因为每次装箱都会创建一个新的对象。在面试中,了解装箱类型及其潜在的性能影响可以展示您对Java语言特性的深入理解。
5. 重载和重写的区别
一句话回答: 重载(Overloading)是在同一类中定义多个同名方法,但参数列表不同;重写(Overriding)是子类中定义一个与父类同名且参数列表相同的方法,以提供特定的实现。
细节解释:
重载(Overloading)
- 定义:方法重载是指在同一个类中可以创建多个方法,它们具有相同的方法名,但参数的数量或类型不同。
- 目的:允许开发者定义多个行为相似但参数不同的方法,增强代码的可读性和灵活性。
- 实现:编译器通过参数的数量和类型来区分不同的重载方法。
java
public class Example {
void display(int a) {
System.out.println("Display with int: " + a);
}
void display(double a) {
System.out.println("Display with double: " + a);
}
void display(int a, int b) {
System.out.println("Display with two ints: " + a + " and " + b);
}
}
重写(Overriding)
- 定义:方法重写是指子类提供一个已在父类中定义的方法的具体实现,或提供一个不同的实现。
- 目的:允许子类根据需要改变父类方法的行为,实现多态。
- 实现:子类方法必须具有与父类方法相同的方法名、参数列表和返回类型(对于非泛型类型)。
java
public class Example {
void display(int a) {
System.out.println("Display with int: " + a);
}
void display(double a) {
System.out.println("Display with double: " + a);
}
void display(int a, int b) {
System.out.println("Display with two ints: " + a + " and " + b);
}
}
区别:
- 作用域:重载在同一类中,重写在父子类中。
- 参数列表:重载要求参数列表不同,重写要求参数列表相同。
- 返回类型:重载可以改变返回类型,重写必须保持返回类型一致或在其子类型中(协变返回)。
- 访问级别:重写的方法不能有更严格的访问级别限制,但可以放宽。 在面试中,理解重载和重写的区别以及它们的使用场景,可以展示您对面向对象编程和Java语言特性的掌握。
6. 构造方法
一句话回答: 构造方法是Java中用于初始化新对象的特殊方法,它的名字必须与类名相同,且没有返回类型。
细节解释:
构造方法(Constructor)
- 定义:构造方法是一种特殊的方法,用于创建对象时初始化对象的状态。
- 特点 :与类名相同,无返回类型,不能被声明为
static
、final
、abstract
或synchronized
。 - 作用:为对象的属性设置初始值,确保对象在使用前处于有效状态。
java
public class Example {
int number;
String name;
// 构造方法
public Example(int number, String name) {
this.number = number;
this.name = name;
}
}
构造方法的坑(常见问题):
- 参数名与成员变量名冲突 :如果构造方法的参数名与成员变量名相同,需要使用
this
关键字来区分。 - 无参构造方法:如果类中没有定义任何构造方法,编译器会自动提供一个无参的默认构造方法。如果类显式定义了构造方法,这个默认构造方法将不再提供。
- 继承中的构造方法 :子类无法继承父类的构造方法,但可以使用
super
关键字调用父类的特定构造方法。 - 构造方法的重载:可以为类定义多个重载的构造方法,以提供不同的初始化方式。
- 构造方法中的异常:构造方法可以声明抛出异常,但通常不推荐这样做,因为它可能会使对象的创建变得复杂。
- 构造方法的调用限制 :构造方法不能直接调用其他构造方法,但可以使用
this()
来调用当前类的其他构造方法。
7. 面向对象的三大特征
一句话回答:面向对象编程的三大特征是封装、继承和多态。
细节解释:
封装(Encapsulation)
- 定义:封装是将对象的实现细节隐藏起来,只暴露出一个可以被外界访问和操作的接口。
- 目的:减少系统的复杂性,提高安全性和易于维护性。
- 实现:通常通过访问修饰符(如private、protected、public)来控制对类成员的访问权限。
继承(Inheritance)
- 定义:继承是一种机制,允许一个类(子类)获得另一个类(父类或超类)的属性和方法。
- 目的:实现代码复用,建立类之间的层次结构。
- 实现 :通过关键字
extends
来实现类的继承关系。
多态(Polymorphism)
- 定义:多态是指允许不同类的对象对同一消息做出响应,但具体的行为会根据对象的实际类型而有所不同。
- 目的:提供接口的统一性,允许编写更通用的代码。
- 实现:通过方法重写(Override)和方法重载(Overload)以及接口和抽象类来实现。
8. private属性 +setter/getter和public属性有什么区别
一句话回答: 使用private访问修饰符并配备setter和getter与直接定义为public的主要区别在于封装性,在setter/getter方法内可以按照既定逻辑控制用户代码对属性的设置和访问。 细节解释:
使用private并配备setter和getter:
- 封装性:通过将属性设置为private,可以隐藏其实现细节,防止外部直接访问和修改属性,从而保护对象的状态。
- 控制访问:通过提供公共的setter和getter方法,可以控制对属性的访问和赋值,可以在setter中加入逻辑来验证赋值是否有效。
- 灵活性:如果需要改变属性的访问逻辑或添加额外的处理,只需修改setter和getter方法,而不需要修改使用这些属性的代码。
java
public class Example {
private int number; // 私有属性
public int getNumber() { // 提供getter方法
return number;
}
public void setNumber(int number) { // 提供setter方法
if (number > 0) { // 可以在这里加入验证逻辑
this.number = number;
}
}
}
直接定义为public:
- 直接访问:如果属性被定义为public,那么它可以被任何其他类直接访问和修改,没有访问控制。
- 缺乏封装:直接暴露属性可能会使类的使用者意外地改变对象的状态,违反封装原则。
- 难以维护:如果属性的访问方式需要改变,可能需要修改所有直接访问这些属性的代码。
java
public class Example {
public int number; // 公共属性,直接访问
// 没有setter和getter方法
}
9. hashCode和equals方法的区别
一句话回答: hashCode
和equals
是Java中用于比较对象和散列存储的关键方法,其中equals
用于比较对象的逻辑等价性,而hashCode
用于确定对象在哈希表中的存储位置。
细节解释:
equals方法:
- 用途:用于比较两个对象的等价性,即检查两个对象是否相等。
- 默认实现 :在
Object
类中,equals
方法比较对象的内存地址,即比较它们是否是同一个实例。 - 重写 :通常需要根据对象的属性来重写
equals
方法,以实现逻辑上的等同性比较。
java
public class Example {
private int id;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Example example = (Example) obj;
return id == example.id; // 基于id比较对象是否相等
}
}
hashCode方法:
- 用途:返回对象的哈希码,即一个整数值,用于在哈希表(如HashMap)中确定对象的存储位置。
- 一致性 :在对象的生命周期内,多次调用
hashCode
应返回相同的值。 - 散列冲突:不同的对象可能返回相同的哈希码,但相等的对象必须有相同的哈希码。
- 重写 :当重写了
equals
方法时,通常也需要重写hashCode
方法,以保持equals
和hashCode
的一致性。
java
@Override
public int hashCode() {
return Objects.hash(id); // 基于id生成哈希码
}
区别和联系:
- 逻辑等同性 vs. 哈希表存储 :
equals
用于逻辑上的比较,而hashCode
用于哈希表的存储和检索。 - 内存地址 vs. 属性值 :
equals
默认比较对象的内存地址,而hashCode
基于对象的属性值。 - 重写规则 :如果重写了
equals
,为了保持一致性,也应该重写hashCode
,且保证相等的对象具有相同的哈希码。
10. 检查异常和非受检异常的区别
一句话回答: 检查异常(Checked Exceptions)是编译时异常,必须被显式捕获或声明抛出;非受检异常(Unchecked Exceptions)是运行时异常,包括错误(Errors)和非受检异常(RuntimeExceptions),它们不需要被显式处理。
细节解释:
检查异常(Checked Exceptions):
- 定义 :继承自
Exception
类但不是RuntimeException
的子类。 - 特点:在编译时被检查,方法可以声明抛出这些异常,调用者必须捕获这些异常或进一步声明抛出。
- 目的:强制调用者处理这些异常,从而保证程序的健壮性。
- 示例 :
IOException
、SQLException
等。
java
public void readFile() throws IOException {
// 可能抛出IOException的代码
}
非受检异常(Unchecked Exceptions):
- 定义 :包括
RuntimeException
及其子类,以及Error
类及其子类。 - 特点:在编译时不会被检查,调用者可以选择捕获或忽略。
- 目的:通常表示程序错误或运行时问题,如逻辑错误或资源问题。
- 示例 :
NullPointerException
、IndexOutOfBoundsException
、OutOfMemoryError
等。
java
public void process() {
try {
// 可能抛出RuntimeException的代码
} catch (RuntimeException e) {
// 可选的处理
}
}
区别:
- 编译时检查:检查异常需要编译时检查,非受检异常不需要。
- 处理要求:检查异常必须被显式捕获或声明抛出,非受检异常可以被忽略。
- 使用场景:检查异常通常用于可预见的、可恢复的错误情况,非受检异常通常用于不可预见的程序错误或运行时问题。
- 继承关系 :检查异常是
Exception
的子类但不是RuntimeException
的子类,非受检异常是RuntimeException
的子类或Error
的子类。
在面试中,能够区分检查异常和非受检异常,并解释它们的使用场景和处理方式,可以展示您对Java异常处理机制的理解。