大家好,我是栗筝i,从 2022 年 10 月份开始,我持续梳理出了全面的 Java 技术栈内容,一方面是对自己学习内容进行整合梳理,另一方面是希望对大家有所帮助,使我们一同进步。得到了很多读者的正面反馈。
而在 2023 年 10 月份开始,我将推出 Java 面试题/知识点系列内容,期望对大家有所助益,让我们一起提升。
本篇是对 Java 基础系列的面试题 / 知识点的总结的下篇
系列相关链接:
全网最全的 Java 面试题内容梳理(持续更新中)
Java基础面试题&知识点总结(上篇)
Java基础面试题问题(下篇)
解答:面向对象编程(Object-Oriented Programming,简称 OOP)是一种编程范式,它使用 “对象” 来设计软件和创建可重用的代码。
在 OOP 中,每个对象都是一个特定类的实例。类定义了对象的属性(也称为数据成员或字段)和方法(也称为成员函数或行为)。对象的属性是用来存储数据的,而方法则是用来执行任务的。
OOP 主要有以下四个基本特性:
封装:封装是将对象的状态(属性)和行为(方法)包装在一起的过程。封装可以隐藏对象的内部实现细节,只暴露出需要的信息。
继承:继承是从已有的类派生出新的类的过程。新的类(子类)可以继承父类的属性和方法,并可以添加新的属性和方法,也可以重写父类的方法。
多态:多态是指允许一个接口使用多种实际类型的能力。多态可以使得代码更加灵活和可扩展。
抽象:抽象是将复杂的系统简化的过程。我们可以通过创建抽象类或接口来定义对象的通用结构。
面向对象编程的主要目标是提高软件的可重用性、灵活性和可维护性。
解答:在面向对象编程中,类和对象是核心概念。
类(Class):类是一种模板或蓝图,定义了一类对象的属性和方法。类定义了对象的基本结构,但它本身并不占用任何内存空间。例如,我们可以定义一个 “汽车” 类,它有颜色、品牌、速度等属性,以及启动、停止、加速等方法。
对象(Object):对象是类的实例。当我们根据类创建对象时,系统会为对象分配内存,并在内存中保存类定义的属性和方法。每个对象都有自己的属性值,这些值定义了对象的状态。对象的方法则定义了对象的行为。例如,我们可以根据 “汽车” 类创建一个具体的汽车对象,它的颜色是红色,品牌是宝马。
简单来说,类是抽象的,它定义了一类事物的通用特性;对象是具体的,它是类的一个实例,具有类定义的结构和行为。
解答:封装、继承和多态是面向对象编程的三大基本特性。
封装:封装是将对象的状态(属性)和行为(方法)包装在一起的过程。封装可以隐藏对象的内部实现细节,只暴露出需要的信息。这样可以保护对象的内部状态,防止外部直接访问对象的内部数据。
继承:继承是从已有的类派生出新的类的过程。新的类(子类)可以继承父类的属性和方法,并可以添加新的属性和方法,也可以重写父类的方法。继承可以提高代码的复用性,使得子类可以拥有父类的所有功能。
多态:多态是指允许一个接口使用多种实际类型的能力。多态可以使得代码更加灵活和可扩展。在 Java 中,多态主要体现在接口的多实现和类的多重继承。
这三个特性是面向对象编程的基础,它们使得我们可以更好地组织和管理代码,提高代码的可读性和可维护性。
解答:接口(Interface)和抽象类(Abstract Class)都是面向对象编程中的高级特性,它们都不能直接实例化,需要通过子类或实现类来实例化。
接口:接口是一种完全抽象的类,它只包含抽象方法(在 Java 8 之后,接口也可以包含默认方法和静态方法)。一个类可以实现多个接口,实现接口的类必须实现接口中的所有方法。
抽象类:抽象类是一种特殊的类,它可以包含抽象方法和非抽象方法。抽象方法是没有实现的方法,需要子类来提供实现。一个类只能继承一个抽象类。
接口和抽象类的主要区别如下:
总的来说,接口更多地被用来定义行为(即方法),而抽象类既可以定义行为,也可以定义状态(即属性)。在设计类的层次结构时,我们通常会使用抽象类,而在定义一组相关的行为时,我们通常会使用接口。
解答:构造函数是一种特殊的方法,用于初始化新创建的对象。在 Java 中,构造函数的名称必须与类名相同,并且没有返回类型。
构造函数与普通方法的主要区别如下:
名称:构造函数的名称必须与类名相同,而普通方法可以有任何有效的标识符作为名称。
返回类型:构造函数没有返回类型,而普通方法必须有返回类型。
调用方式:构造函数在创建对象时自动调用,无需手动调用。而普通方法需要手动调用。
用途:构造函数主要用于初始化对象的状态(即设置属性的初始值)。而普通方法用于描述对象的行为。
例如,以下是一个简单的类,其中包含一个构造函数和一个普通方法:
public class MyClass {
private int value;
// 构造函数
public MyClass(int value) {
this.value = value;
}
// 普通方法
public void displayValue() {
System.out.println("Value: " + value);
}
}
在这个例子中,MyClass
的构造函数接受一个参数 value
,并将其赋值给对象的 value
属性。displayValue
是一个普通方法,用于打印对象的 value
属性。
解答:方法重载和方法重写是 Java 中两种重要的特性。
方法重载(Overloading):在同一个类中,如果有多个方法的名称相同,但参数列表不同(参数的数量、类型或顺序不同),那么这些方法就被称为重载方法。方法重载允许我们使用同一个方法名执行不同的操作。
方法重写(Overriding):在子类中,如果有一个方法与父类的某个方法的名称、参数列表和返回类型都相同,那么这个方法就被称为重写方法。方法重写允许我们在子类中改变父类的行为。
方法重载和方法重写的主要区别如下:
总的来说,方法重载是静态的,它在编译时就已经确定了具体调用哪个方法;而方法重写是动态的,它在运行时才确定具体调用哪个方法。
解答:static
是 Java 中的一个关键字,它可以用来修饰类的成员(成员变量和成员方法),也可以用来创建静态代码块。
static
关键字用来声明独立于对象的静态变量,无论一个类实例化多少对象,它的静态变量只有一份拷贝。 静态变量也被称为类变量。局部变量不能被声明为 static
变量。static
关键字用来声明独立于对象的静态方法。静态方法不能使用类的非静态变量。静态方法从参数列表得到数据,然后计算这些数据。static
关键字还可以形成静态代码块以优化程序性能。static
代码块在类加载的时候就运行了,而且只运行一次,同时运行时机是在构造函数之前。总的来说,static
关键字主要有以下几个作用:
解答:在 Java 中,final
是一个关键字,它可以用来修饰类、方法和变量。
final
类:如果一个类被声明为 final
,那么它不能被继承。这意味着没有其他类可以是这个类的子类。final
类通常表示最终的、不可改变的实体,例如 String
类和 Integer
类都是 final
类。
final
方法:如果一个方法被声明为 final
,那么它不能被子类重写。但是,子类可以继承 final
方法。
final
变量:如果一个变量被声明为 final
,那么它的值就不能被修改,它就成为了一个常量。我们必须在声明 final
变量时或在构造函数中初始化它。
final
关键字提供了一种方式,可以明确表示某个实体不应该被改变。这对于创建不可变的对象和防止意外修改非常有用。
解答:this
和 super
是 Java 中的两个特殊关键字,它们在处理类和对象时非常有用。
this
关键字:this
是一个引用变量,它指向当前对象。在实例方法或构造函数中,this
通常用于引用当前对象的变量或方法。当类的成员变量与局部变量重名时,我们可以使用 this
来区分它们。此外,this
还可以用于在一个构造函数中调用另一个构造函数。
super
关键字:super
是一个引用变量,它指向当前对象的父类。我们可以使用 super
来访问父类的变量和方法。当子类需要调用父类的构造函数或者需要访问父类的方法时,我们可以使用 super
。此外,如果子类重写了父类的方法,我们也可以通过 super
来调用父类的原始方法。
总的来说,this
和 super
都是用于在类的内部引用对象自身或其父类的特殊关键字。
解答:内部类,也称为嵌套类,是定义在另一个类中的类。根据内部类的位置和特性,我们可以将内部类分为四种:成员内部类、静态内部类、方法内部类和匿名内部类。
内部类有以下几个主要用途:
封装:内部类可以访问外部类的所有成员(包括私有成员),因此,我们可以使用内部类来隐藏复杂的实现细节,提供简单的接口。
增强封装性和可读性:内部类可以将相关的类组织在一起,这样可以使代码更易于阅读和维护。
支持多重继承:Java 不支持多重继承,但我们可以使用内部类来模拟多重继承。
实现回调:内部类常常用于实现回调。在 GUI 编程和多线程编程中,我们经常需要在某个特定的时间点执行某个特定的任务,这时我们就可以使用内部类。
总的来说,内部类是一种高级特性,它可以使我们的代码更加整洁、灵活和易于维护。
解答:匿名内部类是一种没有名字的内部类,它通常用于只需要使用一次的场合。
匿名内部类通常用于以下两种类型的场合:
实现接口:匿名内部类可以在定义一个类的同时实现一个接口。例如,我们可以在创建一个线程时使用匿名内部类来实现 Runnable 接口。
继承类:匿名内部类可以在定义一个类的同时继承一个类。例如,我们可以在创建一个图形界面的按钮时使用匿名内部类来继承 ActionListener 类。
匿名内部类的语法格式如下:
new 父类名或接口名() {
// 方法重写
@Override
public void method() {
// 执行语句
}
}
匿名内部类是一种简洁的语法,它可以让我们的代码更加简洁和易于阅读。但是,由于匿名内部类没有名字,所以它只能在定义的地方使用,不能在其他地方引用,这限制了它的使用范围。
解答:访问修饰符是 Java 中的关键字,用于设置类、方法和变量的访问权限。Java 提供了四种访问修饰符:public、private、protected 和默认(没有关键字,也称为 package-private)。
public
:被 public
修饰的类、方法或变量可以在任何地方被访问。
private
:被 private
修饰的类(只有内部类可以声明为 private)、方法或变量只能在其定义的类中被访问。
protected
:被 protected
修饰的类(只有内部类可以声明为 protected)、方法或变量可以在同一个包中的任何类以及其他包中的子类中被访问。
默认(package-private):如果一个类、方法或变量没有显式声明访问修饰符,那么它的访问权限就是默认的。默认访问权限允许同一个包中的类访问,但不允许其他包中的类访问。
访问修饰符是面向对象编程的重要特性,它们可以保护对象的状态,防止外部直接访问对象的内部数据,从而提高代码的安全性和可维护性。
解答:Java 中的异常机制是一种用于处理程序运行时可能出现的错误情况的机制。在 Java 中,异常是通过使用 try
、catch
和 finally
关键字以及 throw
和 throws
语句来处理的。
当在代码中发生异常时,会创建一个异常对象,这个对象包含了关于异常的详细信息(例如异常类型和发生异常的地方)。然后,这个异常对象会被抛出,运行时系统会寻找合适的代码来处理这个异常。
Java 的异常可以分为两大类:Exception
和 Error
。
Exception
:这是程序可以处理的异常。它分为两种类型:检查型异常(Checked Exception)和运行时异常(Runtime Exception)。检查型异常是指编译器要求我们必须处理的异常,例如 IOException
。运行时异常是编译器不要求我们处理的异常,例如 NullPointerException
。
Error
:这是程序通常无法处理的严重问题,如 OutOfMemoryError
。这些问题在通常的情况下,程序无法恢复和处理。
总的来说,Java 的异常处理机制提供了一种结构化和易于管理的方式,用于处理程序运行时的错误情况。
解答:Java 中的异常主要分为两大类:Checked Exception
和 Unchecked Exception
。
Checked Exception
:这种类型的异常在编译时期就会被检查,也就是说,如果在代码中可能抛出的异常没有被捕获或者抛出,那么编译器将会报错。这种类型的异常通常是由外部错误引起的,比如文件不存在(FileNotFoundException
)、网络连接失败(IOException
)等,这些异常都需要程序员显式地进行处理,否则程序无法编译通过。
Unchecked Exception
:这种类型的异常在编译时期不会被检查,也就是说,即使代码中可能抛出的异常没有被捕获或者抛出,编译器也不会报错。Unchecked Exception
又可以分为两种:Runtime Exception
和 Error
。
Runtime Exception
:这种异常通常是由程序逻辑错误引起的,比如空指针访问(NullPointerException
)、下标越界(IndexOutOfBoundsException
)等,这些异常是可以通过改进程序来避免的。
Error
:这种异常通常是由严重的系统错误或者虚拟机错误引起的,比如内存溢出(OutOfMemoryError
)、栈溢出(StackOverflowError
)等,这些异常是程序无法处理的。
总的来说,Java 中的异常种类繁多,不同种类的异常需要采取不同的处理方式,理解这些异常的特性和分类,对于编写健壮的代码非常重要。
解答:Java 的异常层次结构主要由 java.lang.Throwable
类及其子类构成。Throwable
类是所有异常和错误的超类。它有两个主要的子类:Error
和 Exception
。
Error
:Error
类及其子类表示 Java 运行时系统的内部错误和资源耗尽错误。应用程序通常不会抛出这类异常,也不会去尝试捕获它。例如,OutOfMemoryError
、StackOverflowError
等。
Exception
:Exception
类及其子类表示程序可能会遇到的问题,需要程序员进行处理。Exception
又可以分为两类:Checked Exception
和 Unchecked Exception
。
Checked Exception
:这些异常在编译时会被检查,必须被显式捕获或者抛出。例如,IOException
、SQLException
等。
Unchecked Exception
:这些异常在编译时不会被检查,不需要显式捕获或者抛出。Unchecked Exception
主要包括 RuntimeException
及其子类,例如,NullPointerException
、IndexOutOfBoundsException
等。
这种层次结构使得我们可以通过捕获异常的超类来捕获一类异常,也可以通过捕获具体的异常类来精确处理某个异常。
解答:throw
和 throws
是 Java 中用于处理异常的两个关键字,它们的用途和使用方式有所不同。
throw
:throw
关键字用于在代码中显式地抛出一个异常。我们可以使用 throw
关键字抛出一个具体的异常对象,这个异常对象必须是 Throwable
类或其子类的实例。throw
语句后面必须立即跟着一个异常对象。
throw new Exception("This is an exception");
throws
:throws
关键字用于声明一个方法可能会抛出的异常。在方法签名的末尾使用 throws
关键字,后面跟着可能会抛出的异常类型。一个方法可以声明抛出多种类型的异常,多个异常类型之间用逗号分隔。
public void readFile() throws IOException, FileNotFoundException {
// method body
}
总的来说,throw
是在代码中抛出一个具体的异常,而 throws
是在声明一个方法时,指明该方法可能会抛出的异常类型。
解答:在 Java 中,我们可以通过继承 Exception
类或其子类来自定义异常。以下是创建自定义异常的基本步骤:
创建一个新的类,其名称通常以 “Exception” 结尾,以表明这是一个异常类。
让这个类继承 Exception
类或其子类。如果这个异常需要被显式捕获,那么应该继承 Exception
类;如果这个异常是运行时异常,那么应该继承 RuntimeException
类。
提供类的构造函数。至少应该提供两个构造函数,一个无参数的构造函数,和一个带有 String
参数的构造函数,这个 String
参数表示异常的详细信息。
以下是一个自定义异常的例子:
public class MyException extends Exception {
public MyException() {
super();
}
public MyException(String message) {
super(message);
}
}
在这个例子中,我们创建了一个名为 MyException
的自定义异常类,它继承了 Exception
类,并提供了两个构造函数。当我们需要抛出这个异常时,可以使用 throw
关键字,如:throw new MyException("This is my exception");
。
解答:try
、catch
和 finally
是 Java 中用于处理异常的关键字。
try
:try
块用于包含可能会抛出异常的代码。如果 try
块中的代码抛出了异常,那么 try
块后面的代码将不会被执行,程序会立即跳转到对应的 catch
块。
catch
:catch
块用于捕获和处理异常。每个 try
块后面可以跟随一个或多个 catch
块。如果 try
块中的代码抛出了异常,那么程序会查找第一个能处理这种类型异常的 catch
块,然后执行这个 catch
块中的代码。
finally
:finally
块包含的代码总是会被执行,无论 try
块中是否抛出了异常,无论 catch
块是否执行。finally
块通常用于放置清理代码,比如关闭文件、释放资源等。
解答:如果 try
块中有 return
语句,那么 finally
块的代码仍然会被执行。这是因为 finally
块的代码总是在 try
或 catch
块中的 return
语句之前执行。但是,如果 finally
块中也有 return
语句,那么这个 return
语句会覆盖 try
或 catch
块中的 return
语句,方法会返回 finally
块中的 return
语句的值。
解答:反射是 Java 提供的一种强大的工具,它允许运行中的 Java 程序对自身进行自我检查,并且可以操作类、方法、属性等元素。反射机制主要提供了以下功能:
反射的主要用途:
开发通用框架:许多大型框架(如 Spring、MyBatis 等)的底层都会用到反射,它们通过反射去创建对象,调用方法,这样框架使用者就只需要进行简单的配置,而不需要关心底层的复杂实现。
开发工具类:例如,我们可以通过反射编写一个通用的 toString 工具方法,用于生成任意对象的字符串表示。
实现动态代理:Java 的动态代理机制就是通过反射实现的,它可以在运行时动态地创建一个接口的实现类。
虽然反射非常强大,但是反射操作会比非反射操作慢很多,所以我们应该在必要的时候才使用反射。
Java 的反射机制是基于 Java 虚拟机(JVM)中的类信息(Class Information)实现的。
在 Java 中,当一个类被加载到 JVM 中时,JVM 会为这个类生成一个 Class
对象。这个 Class
对象包含了类的所有信息,包括类的名称、包、父类、接口、构造器、方法、字段等。这些信息在类被加载时从类的字节码文件中提取出来,并保存在 Class
对象中。
当我们使用反射去获取一个类的信息或操作一个类时,实际上是通过操作这个类对应的 Class
对象来实现的。例如,我们可以通过 Class
对象的 getMethod
方法获取类的方法,通过 newInstance
方法创建类的实例,通过 getField
方法获取类的字段等。
因此,Java 的反射机制的实现原理就是通过操作 Class
对象来操作类的信息。这也是为什么我们在使用反射时,首先需要获取类的 Class
对象。
解答:Java 反射的实现主要涉及 java.lang
和 java.lang.reflect
这两个包中的类。以下是一些主要的类及其作用:
java.lang.Class
:这是反射的核心类,它代表了正在运行的 Java 应用程序中的类和接口。我们可以通过 Class
对象获取类的名称、父类、接口、构造器、方法、字段等信息,也可以通过 Class
对象创建类的实例。
java.lang.reflect.Constructor
:这个类代表类的构造器。我们可以通过 Constructor
对象获取构造器的参数类型,也可以通过 Constructor
对象创建类的实例。
java.lang.reflect.Method
:这个类代表类的方法。我们可以通过 Method
对象获取方法的名称、返回类型、参数类型等信息,也可以通过 Method
对象调用方法。
java.lang.reflect.Field
:这个类代表类的字段。我们可以通过 Field
对象获取字段的名称、类型、修饰符等信息,也可以通过 Field
对象获取和设置字段的值。
java.lang.reflect.Modifier
:这个类代表类、方法、字段的修饰符。我们可以通过 Modifier
类获取修饰符的字符串表示,也可以判断修饰符是否包含某个关键字(如 public
、static
等)。
以上这些类提供了丰富的方法,使得我们可以通过反射获取和操作类的几乎所有信息。
解答:在 Java 中,我们可以通过 Class
类的 newInstance
方法或 Constructor
类的 newInstance
方法来通过反射创建对象。
以下是两种方法的示例:
Class
类的 newInstance
方法:try {
Class<?> cls = Class.forName("java.lang.String");
String str = (String) cls.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
在这个例子中,我们首先通过 Class.forName
方法获取了 String
类的 Class
对象,然后通过 Class
对象的 newInstance
方法创建了 String
类的实例。
Constructor
类的 newInstance
方法:try {
Class<?> cls = Class.forName("java.lang.String");
Constructor<?> constructor = cls.getConstructor(String.class);
String str = (String) constructor.newInstance("Hello, World!");
} catch (Exception e) {
e.printStackTrace();
}
在这个例子中,我们首先获取了 String
类的 Class
对象,然后通过 Class
对象的 getConstructor
方法获取了 String
类的构造器,最后通过 Constructor
对象的 newInstance
方法创建了 String
类的实例。
需要注意的是,这两种方法都可能抛出异常,所以我们需要捕获或抛出这些异常。
解答:Java 反射创建对象和使用 new
关键字创建对象都可以用来实例化类,但是它们之间存在一些重要的区别:
创建对象的方式不同:
new
关键字创建对象时,我们在编译时就知道要创建的类的类型。性能差异:
new
关键字创建对象的性能要比使用反射创建对象的性能高。这是因为反射操作需要在运行时解析类的信息,这会消耗更多的 CPU 和内存资源。安全性差异:
new
关键字创建对象时,我们可以直接访问类的公有成员,但不能访问类的私有成员。应用场景不同:
new
关键字创建对象,因为这样更简单、更高效。解答:Spring 框架广泛地使用了 Java 的反射机制,主要用于以下几个方面:
依赖注入:Spring 通过读取配置文件或注解,获取到 Bean 的全类名,然后通过反射机制实例化对象,并通过反射设置对象的属性或调用方法,实现依赖注入。
事件监听:Spring 的事件监听机制也是基于反射实现的。当事件发生时,Spring 会通过反射调用监听器的处理方法。
AOP:Spring 的 AOP(面向切面编程)也是基于反射实现的。Spring 通过反射创建代理对象,并通过反射调用目标方法和切面方法。
数据绑定:Spring MVC 在处理请求时,会根据请求参数名和 Bean 属性名进行匹配,然后通过反射设置 Bean 的属性值,实现数据绑定。
Bean 的生命周期管理:Spring 通过反射调用 Bean 的初始化方法和销毁方法,管理 Bean 的生命周期。
Spring 通过反射,使得我们只需要进行简单的配置,就可以实现复杂的功能,大大提高了开发效率。