Java基础面试题总结

  1. 1. Java基本概念
    1. 1.1 Java语言有哪些特点?
    2. 1.2 Java和C++有什么联系和区别?
    3. 1.3 JVM、JRE和JDK的关系是什么?
    4. 1.4 什么是字节码?采用字节码的好处是什么?
    5. 1.5 Java有哪些数据类型?
  2. 2. 面向对象
    1. 2.1 面向对象的三大特性是什么?
    2. 2.2 访问修饰符的区别?
    3. 2.3 Java语言是如何实现多态的?
    4. 2.4 重载和重写的区别?
    5. 2.5 抽象类和接口的区别?
    6. 2.6 什么是不可变对象?有什么好处?
    7. 2.7 equals方法和==的区别?
    8. 2.8 String、StringBuffer、StringBuilder的区别是什么?
    9. 2.9 为什么Java中的String要设计成不可变的?
    10. 2.10 基本类型和包装类型有什么区别?什么是自动装箱/拆箱?
    11. 2.11 B/S和C/S架构分别是什么?
  3. 3. 反射
    1. 3.1 什么是反射?
    2. 3.2 举一下反射使用的例子?
    3. 3.3 介绍一下反射在JDBC和Spring中的应用
    4. 3.4 反射机制的原理是什么?

Java 基础常见面试题总结,涉及 Java 基本概念、OOP、反射等内容,文章将不断更新。

1. Java基本概念

1.1 Java语言有哪些特点?

  • 面向对象(封装、继承、多态):Java 是一种面向对象的语言,它对类、对象、继承、封装、多态、接口、包等内容均有很好的支持。为了简单起见,Java 只支持类之间的单继承,但是可以使用接口来实现多继承。
  • 平台无关性:Java是“一次编写,到处运行”(Write Once, Run Anywhere)的语言,因此采用 Java 语言编写的程序具有很好的可移植性,而保证这一点的正是 Java 的虚拟机机制,在引入虚拟机之后,Java 语言在不同的平台上运行不需要重新编译。
  • 可靠性、安全性:Java 是被设计成编写高可靠和稳健软件的。Java 消除了某些编程错误,使得用它写可靠软件相当容易。
  • 支持多线程:C++ 没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 具备内置的多线程功能,可以将一个程序的不同程序段设置为不同的线程,使各线程并发、独立执行,提高系统的运行效率。
  • 支持网络编程:Java 诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便。
  • 编译与解释并存:Java 是一种先编译后解释的语言,Java 程序在 Java 平台运行时会被编译成字节码文件,然后可以在有 Java 环境的操作系统上运行。
  • 动态性:Java 语言设计成适应于变化的环境,它是一个动态的语言。例如,Java 中的类是根据需要载入的,甚至有些是通过网络获取的。

1.2 Java和C++有什么联系和区别?

联系:

  • 面向对象:Java 和 C++ 都支持面向对象编程,包括类、对象、继承、封装和多态。
  • 语法:Java 的语法在很大程度上受到了 C++ 的影响,因此这两种语言在语法上有很多相似之处。

区别:

  • 内存管理:Java 有垃圾回收机制,可以自动回收不再使用的内存,而 C++ 需要程序员手动管理内存。
  • 指针和引用:C++ 支持指针,但 Java 没有指针的概念。相反,Java 使用引用来实现某些相似的功能。
  • 继承:C++ 支持多重继承,而 Java 不支持多重继承,但允许一个类实现多个接口。
  • 运行环境:Java 程序在 Java 平台上运行,可以在任何安装了 Java 虚拟机的系统上运行,而 C++ 程序是直接编译成特定操作系统的机器码。
  • 异常处理:Java 有一套完整的异常处理机制,而 C++ 的异常处理机制相对较弱。

1.3 JVM、JRE和JDK的关系是什么?

JVM、JRE 和 JDK 是 Java 开发和运行的三个核心组件,它们之间的关系可以概括为:JDK 包含 JRE,而 JRE 包含 JVM。下面是对这三者的详细介绍:

  • JVM(Java Virtual Machine):Java 虚拟机,是 Java 能够实现跨平台的核心机制。JVM 只认识 .class 后缀的文件,它能将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。
  • JRE(Java Runtime Environment):Java 运行环境,包括 Java 虚拟机(JVM)和 Java 程序所需的核心类库等。如果想要运行一个开发好的 Java 程序,计算机中只需要安装 JRE 即可。
  • JDK(Java Development Kit):Java 的开发工具包,是提供给 Java 开发人员使用的,其中包含了 Java 的开发工具和 JRE。其中的开发工具包括:运行工具(java.exe)、编译工具(javac.exe)、打包工具(jar.exe)等。

所以,简单来说,JDK 是用于开发 Java 应用的,JRE 是用于运行 Java 应用的,而 JVM 则是使 Java 能够跨平台的核心。

1.4 什么是字节码?采用字节码的好处是什么?

字节码是一种中间状态的二进制文件,是由源码编译过来的,可读性没有源码的高。CPU 并不能直接读取字节码,在 Java 中,字节码需要经过 JVM 转译成机器码之后,CPU 才能读取并运行。采用字节码的好处主要有以下几点:

  • 跨平台性:字节码可以在不同的平台上运行,只需要有一个能够识别并解释字节码的解释器即可。
  • 高效率:字节码可以在运行时动态编译为机器代码,这样就可以在保证程序执行效率的同时避免了额外的编译步骤。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点,所以 Java 程序运行时比较高效。
  • 可调试性:字节码是可读的,因此可以方便地进行调试和分析。
  • 可扩展性:字节码可以被扩展以支持新的特性,而不需要更改现有的机器代码。

1.5 Java有哪些数据类型?

Java 的数据类型可以分为两大类:基本数据类型和引用数据类型。

基本数据类型包括以下八种:

  • byte:8位有符号二进制整数,取值范围为 -128~127
  • short:16位有符号二进制整数,取值范围为 -32768~32767
  • int:32位有符号二进制整数,取值范围为 -2147483648~2147483647
  • long:64位有符号二进制整数,取值范围为 -9223372036854775808~9223372036854775807
  • float:32位单精度浮点数。
  • double:64位双精度浮点数。
  • boolean:布尔值,只有两个取值:truefalse
  • char:单个16位 Unicode 字符,取值范围为 \u0000~\uffff

引用数据类型包括:

  • 类(Class):由程序员定义的一种数据类型,它将数据和对数据的操作封装在一起。
  • 接口(Interface):一种引用类型,类似于类,由完全抽象的方法和常量组成。
  • 数组(Array):可以保存多个同类型变量的容器。

2. 面向对象

2.1 面向对象的三大特性是什么?

  • 封装:封装是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。
  • 继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
  • 多态:多态是指允许不同类的对象对同一消息做出响应。也就是说,同一个接口可以具有多种实现方式。

2.2 访问修饰符的区别?

Java 有四种访问修饰符,它们分别是 publicprotecteddefault(默认,无修饰符)和 private。以下是它们的详细介绍:

  • public:公共访问修饰符,被声明为 public 的类、方法、构造方法和接口能够被任何其他类访问。如果几个相互访问的 public 类分布在不同的包中,则需要导入相应 public 类所在的包。由于类的继承性,类所有的公有方法和变量都能被其子类继承。
  • protected:受保护的访问修饰符,被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;同时,被 protected 所修饰的成员也能被该类的所有子类继承下来,无论子类和基类是否在同一包中。
  • default(默认,无修饰符):包私有访问修饰符,表示只能在当前包中的类访问该成员。被 default 所修饰的成员只能被该类所在同一个包中的子类所继承下来。
  • private:私有访问修饰符,表示只能在当前类中访问该成员,除了当前类都不能访问。私有访问修饰符是最严格的访问级别,所有被声明为 private 的方法、变量和构造方法只能被所属类访问,并且类和接口不能声明为 private

2.3 Java语言是如何实现多态的?

我们通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直延迟到运行时才能确定,这也是为什么有时候多态方法又被称为延迟方法的原因。Java 语言实现多态主要依赖于以下三个条件:

  • 继承:必须存在子类继承父类的继承关系,只有在存在继承关系的前提下,子类才能继承父类的属性和方法,从而实现多态。
  • 重写:子类需要对父类中的一些方法进行重写,当调用这些方法时,会调用子类重写的方法,而不是原本父类的方法。
  • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样,该引用才能够具备调用父类的方法和子类的方法的能力。

2.4 重载和重写的区别?

Java 中的重载和重写都是实现多态的方式,但它们的实现方式和使用场景有所不同:

重载(Overload):

  • 重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载。
  • 重载的方法可以改变返回类型;可以相同也可以不同,但不能通过返回类型是否相同来判断重载。
  • 重载是编译时的多态性。

重写(Override):

  • 重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型。
  • 重写的方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。
  • 重写是运行时的多态性。

注意:构造器不能被继承,因此不能被重写,但可以被重载。每一个类必须有自己的构造函数,负责构造自己这部分的构造内容。子类不会覆盖父类的构造函数,相反必须在构造函数的一开始就调用父类的构造函数。

2.5 抽象类和接口的区别?

语法层面上的区别:

  • 接口只能定义抽象方法不能实现方法,抽象类既可以定义抽象方法,也可以实现方法。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  • 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法。
  • 抽象类是单继承,而接口是多继承。一个类只能继承一个抽象类,但可以实现多个接口。
  • 抽象类的方法访问控制符无限制,只是抽象类中的 abstract 方法不能被 private 修饰;而接口有限制,接口默认为 public 控制符。
  • 抽象类可以有构造方法,接口中不能有构造方法。

设计层面上的区别:

  • 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(只有行为)进行抽象。
  • 二者的设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。

2.6 什么是不可变对象?有什么好处?

在 Java 中,不可变对象(Immutable Object)是指一旦被创建后,对象所有的状态及属性在其生命周期内不会发生任何变化。也就是说,一个对象在创建后,不能对该对象进行任何更改。如 StringInteger 以及其它包装类。不可变对象有很多优点:

  • 构造、测试和使用都很简单:由于对象状态不会改变,所以可以避免了很多复杂的状态检查和同步。
  • 线程安全且没有同步问题:线程安全是最大的好处,在并发环境下,不可变对象无需进行额外的同步操作,因此可以极大地简化并发编程。
  • 不需要担心数据会被其它线程修改:这是因为对象的状态不会改变,所以不会出现一个线程正在读取对象状态,而另一个线程同时修改该状态的情况。
  • 当用作类的属性时不需要保护性拷贝:如果类的属性是可变的,那么在返回属性值或者接收新的属性值时,需要进行保护性拷贝以防止属性被外部代码修改。而对于不可变对象,由于其状态不能被修改,所以无需进行保护性拷贝。
  • 可以很好的用作 Map 键值和 Set 元素:由于不可变对象一旦创建就不能改变,所以它们是值得信赖的键值,可以确保在对象被用作键值的过程中始终保持一致性。

然而,不可变对象也有一些缺点,最大的缺点就是创建对象的开销,因为每一步修改操作都会产生一个新的对象。

2.7 equals方法和==的区别?

在 Java 中,==equals() 方法都可以用来比较两个对象,但它们的比较方式和使用场景有所不同:

首先 == 是一个运算符,而 equals() 是一个方法,二者比较的内容有以下不同:

  • ==:如果比较的是基本数据类型,则比较的是数值是否相等;如果比较的是引用数据类型,则比较的是两个对象的内存地址是否相等。
  • equals():默认情况下,比较的是两个对象的内存地址。但是,许多类(如 StringInteger 等)已经重写equals() 方法,使其能够比较两个对象的内容是否相等。

二者的一般使用场景如下:

  • ==:通常用于比较基本数据类型,或者比较两个对象是否指向同一内存地址。
  • equals():通常用于比较两个对象的内容是否相等。

2.8 String、StringBuffer、StringBuilder的区别是什么?

  • 可变性:String 是不可变的,也就是说,一旦 String 对象被创建,其值就不能被改变。如果需要修改 String,Java 会创建一个新的 String 对象。StringBufferStringBuilder 是可变的,也就是说,它们可以在原地修改字符串,而不需要创建新的对象。
  • 线程安全性:StringBuffer 是线程安全的,因为它的所有公共方法都是同步的。这意味着在多线程环境下,StringBuffer 可以安全地使用。StringBuilder 不是线程安全的。因此,如果你的代码只在单线程环境下运行,使用 StringBuilder 通常会比 StringBuffer 更快。
  • 性能:对于需要频繁修改字符串的情况,使用 StringBufferStringBuilder 通常比使用 String 更高效。这是因为每次修改 String 时,都会创建一个新的对象,这会对性能产生影响。在大部分情况下,StringBuilder 的性能优于 StringBuffer,这主要是因为 StringBuilder 不需要考虑线程安全。

2.9 为什么Java中的String要设计成不可变的?

  • 安全性:不可变对象本身是线程安全的,可以在多线程环境下安全使用,无需额外的同步。此外,String 经常被用作许多 Java 类的参数,例如网络连接和文件路径,如果是可变的,那么它的值可能在你不知情的情况下被改变,这可能会导致安全问题。
  • 哈希码缓存:由于 String 是不可变的,所以它的哈希码是固定的,可以被缓存,这对于哈希映射(如 HashMap)来说非常有用,可以提高查找效率。
  • 字符串池:在 Java 中,相同的字符串字面量只会在内存中存在一份,这被称为字符串池(String Pool)。这种设计可以节省内存,提高效率。如果 String 是可变的那么字符串池就无法实现了。
  • 类加载器安全:String 是 Java 类加载器使用的关键类,如果是可变的那么可能会影响到类加载器的安全性。

2.10 基本类型和包装类型有什么区别?什么是自动装箱/拆箱?

基本类型和包装类型的主要区别如下:

  • 初始值:基本类型有初始值,而包装类型的默认值是 null
  • null 值:包装类型可以为 null,而基本类型不可以。
  • 存储位置:如果一个基本类型是成员变量就存储在堆内存里,如果是局部变量就存储在栈内存里;而包装类型则存储的是堆中的引用。
  • 泛型:包装类型可用于泛型,而基本类型不可以。
  • 比较:在使用 == 进行判断的时候,基本类型使用 == 直接判断其值是否相等,而包装类型判断的是其指向的地址是否相等。

自动装箱和自动拆箱是 Java 语言的特性,使得基本类型和包装类型之间的转换更加方便:

  • 自动装箱:就是将基本数据类型自动转换为对应的包装类。例如,Integer i = 10;,这里的10是一个 int 类型,但 Java 会自动将其转换(装箱)为 Integer 类型。
  • 自动拆箱:就是将包装类自动转换为基本数据类型。例如,int a = i;,这里的 iInteger 类型,但 Java 会自动将其转换(拆箱)为 int 类型。

这些特性使得我们在编写 Java 代码时可以更自然地混合使用基本类型和包装类型,而不需要关心它们之间的转换细节。

2.11 B/S和C/S架构分别是什么?

B/S 架构和 C/S 架构是两种常见的软件系统体系结构。

  • B/S 架构,全称为 Browser/Server,即浏览器/服务器结构。它是 Web 兴起后的一种网络结构模式,Web 浏览器是客户端最主要的应用软件。这种模式统一了客户端,将系统功能实现的核心部分集中到服务器上,简化了系统的开发、维护和使用。客户端只需要安装一个浏览器,通过 Web 服务器与数据库服务器进行数据交互。B/S 架构利用了 Web 浏览器技术和 Internet 协议,实现了异构系统的连接和信息的共享。
  • C/S 架构,全称是 Client/Server,即客户端/服务器体系结构,主要应用于局域网内。它是一种网络体系结构,通常采取两层结构,服务器负责数据的管理,客户端负责完成与用户的交互任务。即客户端是用户运行应用程序的 PC 端或者工作站,客户端要依靠服务器来获取资源。

3. 反射

3.1 什么是反射?

在 Java 中,反射是一种强大的工具,它允许程序在运行时访问类或对象的信息,并动态地操作它们。以下是反射的一些主要特性和用途:

  • 动态创建对象:反射可以在运行时动态地创建任意一个类的对象。
  • 获取类的信息:反射可以获取任意一个类的所有属性和方法,包括私有的。
  • 动态调用方法和属性:反射可以在运行时动态地调用任意一个对象的任意方法和属性。
  • 动态修改属性:反射可以改变对象的属性,甚至可以打破封装性,导致 Java 对象的属性不安全。

反射在许多 Java 框架中都有应用,例如 Spring 和 Hibernate,它们使用反射来实现依赖注入和对象关系映射。然而,反射也有其缺点,例如可能会消耗更多的系统资源,如果不需要动态地创建一个对象,那么就不需要用反射。此外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

3.2 举一下反射使用的例子?

(1)获取 Class 对象:我们可以通过三种方式获取 Class 对象:

1
2
3
4
5
6
7
8
9
// 1. 通过类名.class的方式
Class<String> c1 = String.class;

// 2. 通过对象的getClass()方法
String s = "Hello World";
Class<? extends String> c2 = s.getClass();

// 3. 通过Class类的静态方法forName(String className)
Class<?> c3 = Class.forName("java.lang.String");

(2)创建对象:我们可以通过 Class 对象的 newInstance() 方法来创建对应类的对象:

1
2
Class<?> c = Class.forName("java.lang.String");
String s = (String) c.newInstance();

(3)获取方法并调用:我们可以通过 Class 对象的 getMethod() 方法来获取一个类的方法,然后通过 Method 对象的 invoke() 方法来调用这个方法:

1
2
3
Class<?> c = Class.forName("java.lang.String");
Method method = c.getMethod("substring", int.class, int.class);
String s = (String) method.invoke("Hello World", 0, 5); // Hello

(4)获取和设置字段:我们可以通过 Class 对象的 getField() 方法来获取一个类的公有字段,然后通过 Field 对象的 get()set() 方法来获取和设置这个字段的值:

1
2
3
4
5
Class<?> c = Class.forName("java.awt.Dimension");
Object o = c.newInstance();
Field f = c.getField("width");
f.set(o, 100);
System.out.println(f.getInt(o)); // 100

3.3 介绍一下反射在JDBC和Spring中的应用

(1)在 JDBC 中的应用:我们在使用 JDBC 连接数据库时,会使用 Class.forName() 通过反射加载数据库的驱动程序。例如,假设我们有 com.mysql.cj.jdbc.Driver 这个类,如果我们使用 MySQL 数据库,那么就传入 MySQL 的驱动类:

1
2
3
4
5
// 加载并注册JDBC驱动
Class.forName("com.mysql.cj.jdbc.Driver");

// 创建数据库连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/<DatabaseName>", "<username>", "<password>");

(2)在 Spring 中的应用:Spring 通过配置文件配置各种各样的 bean,你需要用到哪些 bean 就配哪些,Spring 容器就会根据你的需求去动态加载。Spring 的 IoC 容器可以动态地加载和管理 bean,创建对象。这是通过反射实现的,Spring 会读取配置文件中的类全名,然后通过反射来创建对象:

1
2
3
4
5
6
<!-- Spring配置文件 -->
<beans>
<bean id="myBean" class="com.example.MyClass">
<property name="myProperty" value="Hello World" />
</bean>
</beans>
1
2
3
4
5
6
// 从Spring IoC容器中获取bean
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
MyClass myBean = (MyClass) context.getBean("myBean");

// 输出:Hello World
System.out.println(myBean.getMyProperty());

3.4 反射机制的原理是什么?

Java 反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质上,当 JVM 得到 Class 对象之后,再通过 Class 对象进行反编译,从而获取对象的各种信息。Java 属于先编译再运行的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到 JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。

反射的原理可以通过以下步骤来理解:

  • 加载:首先,将 .class 文件读入内存,并为之创建一个 Class 对象。
  • 反编译:然后,通过 Class 对象进行反编译,从而获取对象的各种信息。

反射机制的优点是在运行时获得类的各种内容,进行反编译,对于 Java 这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。