JAVA基础面试题-最常见问题(2)

发布于 2025-09-03 14:41:38 浏览 27 次

JAVA基础面试题

1.Java 中的注解原理是什么?

Java 注解(Annotations)自 Java 5 版本开始引入,是一种用于为代码添加元数据的方式。这些元数据可以被编译器或运行时环境使用,以实现各种功能,如代码生成、编译时检查、运行时处理等。注解本身不直接影响程序的逻辑,它们提供了一种方式来嵌入程序中的附加信息。

注解的基本组成:

Java 注解由三个主要部分组成:

· 注解声明:使用 @interface 关键字声明。

· 成员:类似于接口的成员,但不包括方法的实现。

· 应用:通过在代码元素(如类、方法、字段等)前使用 @ 符号加上注解名称来应用。

注解的基本用法:

// 定义一个简单的注解
public @interface MyAnnotation {
    String value() default "default value";
}
 
// 使用注解
@MyAnnotation(value = "Hello")
public class MyClass {
    // 类体...
}

2.你使用过 Java 的反射机制吗?如何应用反射?

Java 的反射机制是指在运行时动态获取类的信息(如属性、方法、构造器等)并操作其成员的能力。我在实际开发中常用反射解决以下场景:

·动态创建对象
无需硬编码类名,通过类的全限定名加载类并实例化:

Class<?> clazz = Class.forName("com.example.User");
Object obj = clazz.getConstructor().newInstance(); // 调用无参构造器

·访问私有成员
突破访问权限限制,操作私有属性或方法(需设setAccessible(true)):

Field field = clazz.getDeclaredField("name");
field.setAccessible(true); // 允许访问私有字段
field.set(obj, "Alice"); // 为对象设置私有属性值

·动态调用方法
根据方法名和参数类型,在运行时调用任意方法:

Method method = clazz.getMethod("setAge", int.class);
method.invoke(obj, 20); // 调用 obj 的 setAge(20) 方法

·框架底层实现

许多框架(如 Spring、MyBatis)依赖反射实现解耦:
Spring 的依赖注入(通过反射实例化 Bean 并注入属性)
MyBatis 的结果集映射(通过反射将数据库字段映射到对象属性)

 **反射的核心是 java.lang.Class 类,它是反射的入口,提供了获取类信息的各种方法。但反射会绕过编译期检查,可能降低性能,因此需谨慎使用。**

3.什么是 Java 中的不可变类?

Java 中的不可变类是指实例创建后,其内部状态(属性值)无法被修改的类。一旦对象被初始化,所有属性值将保持不变,任何修改操作都会返回新的对象。

4.什么是 Java 的 SPI(Service Provider Interface)机制?

Java SPI (Service Provider Interface)是 JDK 内置的一种服务提供发现机制,主要用于框架扩展和组件替换,允许第三方为同一接口提供多种实现。其核心思想是将装配控制权移至程序外部,通过动态加载实现类实现模块化设计。 ‌

核心原理:
SPI通过在META-INF/services/目录下创建以接口名称命名的文件,文件中列出接口的具体实现类名。当程序需要使用该服务时,可通过java.util.ServiceLoader动态加载实现类。这种机制将接口与实现分离,增强了代码的可扩展性和灵活性。

5.Java 泛型的作用是什么?

Java 泛型的核心作用是在编译期提供类型约束和类型安全

避免类型转换:
泛型可以消除代码中的许多强制类型转换,这使得代码更加清晰可读,减少了出错的可能性。

// 不使用泛型
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 需要强制类型转换

// 使用泛型
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // 自动类型推断

编译期类型检查:
提前发现类型不匹配错误(如向 List<Integer> 添加字符串会编译报错),减少运行时异常。

// 不使用泛型
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 需要强制类型转换

// 使用泛型
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // 无需强制类型转换

代码复用:
用一套逻辑支持多种数据类型(如 ArrayList<T> 可存储任意类型,无需为每种类型写单独实现)。

// 不使用泛型
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 需要强制类型转换

// 使用泛型
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // 自动类型推断

6.Java 泛型擦除是什么?

泛型擦除(Type Erasure)是 Java 泛型的核心实现机制,指的是在 Java 编译期间,编译器对泛型类型参数(如 <T>、<E>)进行类型检查,但在编译为字节码后,所有泛型类型信息都会被擦除,替换为其边界类型(通常是Object,或者指定的上限类型),运行时不存在泛型信息。

7.什么是 Java 泛型的上下界限定符?

Java泛型的上下界限定符(Bounded Type Parameters)允许我们对泛型类型参数的范围进行限制,提供了更灵活且安全的类型约束机制。通过使用extends和super关键字,我们可以定义泛型参数的上界和下界。

上界限定符(Upper Bounded Wildcards)
上界限定符使用<? extends T>语法,表示类型参数必须是T类型或其子类型。这被称为"上限通配符"。

import java.util.List;
import java.util.ArrayList;

public class UpperBoundExample {
    
    // 计算Number列表的总和
    public static double sumOfList(List<? extends Number> list) {
        double sum = 0.0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }
    
    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3, 4, 5);
        System.out.println("Integer list sum: " + sumOfList(intList));
        
        List<Double> doubleList = List.of(1.1, 2.2, 3.3, 4.4, 5.5);
        System.out.println("Double list sum: " + sumOfList(doubleList));
        
        // 以下代码会编译错误,因为String不是Number的子类
        // List<String> stringList = List.of("a", "b", "c");
        // System.out.println(sumOfList(stringList));
    }
}

下界限定符(Lower Bounded Wildcards)
下界限定符使用<? super T>语法,表示类型参数必须是T类型或其父类型。这被称为"下限通配符"。

import java.util.List;
import java.util.ArrayList;

public class LowerBoundExample {
    
    // 向列表中添加整数
    public static void addIntegers(List<? super Integer> list) {
        for (int i = 1; i <= 5; i++) {
            list.add(i);
        }
    }
    
    public static void main(String[] args) {
        List<Number> numberList = new ArrayList<>();
        addIntegers(numberList);
        System.out.println("Number list: " + numberList);
        
        List<Object> objectList = new ArrayList<>();
        addIntegers(objectList);
        System.out.println("Object list: " + objectList);
        
        // 以下代码会编译错误,因为String不是Integer的父类
        // List<String> stringList = new ArrayList<>();
        // addIntegers(stringList);
    }
}

8.Java 中的深拷贝和浅拷贝有什么区别?

Java 中深拷贝和浅拷贝的核心区别在于是否复制对象内部的引用类型成员:

·浅拷贝:
仅复制对象本身及基本类型成员,引用类型成员仅复制引用(指向原对象的同一内存地址)。修改拷贝对象的引用成员会影响原对象。
实现:类实现 Cloneable 接口,重写 clone() 方法(默认浅拷贝)。

·深拷贝:
完全复制对象本身及所有引用类型成员(递归复制,直至基本类型)。拷贝对象与原对象完全独立,修改互不影响。
实现:需手动递归复制所有引用成员,或通过序列化(如ObjectInputStream/ObjectOutputStream)实现。

9.什么是 Java 的 Integer 缓存池?

Java 的 Integer 缓存池是 JDK 内部实现的一种优化机制,用于高效管理一定范围内的整数对象,以减少内存占用并提升性能。 ‌
Integer缓存池通过维护一个静态数组(默认范围为-128至127),当创建该范围内的整数对象时,直接返回缓存中的对象而非生成新实例。这种机制被称为"享元模式",通过共享细粒度对象来优化性能。 ‌

10.Java 的类加载过程是怎样的?

Java 的类加载过程是将 .class 字节码文件加载到 JVM 并转化为可执行类的过程,主要分为加载、链接、初始化三个阶段,每个阶段又包含具体步骤:

加载(Loading)
核心任务:将类的字节码文件(.class)加载到内存,生成 java.lang.Class 对象(类的元数据载体)。

链接(Linking)
链接阶段分为验证、准备、解析三步,确保类的字节码合法且能正确执行。

·验证(Verification):

检查字节码的安全性和合法性(如是否符合 Java 虚拟机规范、是否有恶意代码等),避免危害 JVM 安全。
包括:文件格式验证(魔数、版本号等)、字节码验证(语义合法性)、符号引用验证等。

·准备(Preparation):

为类的静态变量分配内存并设置默认初始值(非显式赋值)。
例如 public static int num = 10;,此阶段会为 num 分配内存,设置默认值 0(显式赋值 10 在初始化阶段执行)。
静态常量(final static)在此阶段直接赋值(如 public static final int MAX = 100; 会直接设置为 100)。

·解析(Resolution):

将常量池中的符号引用(如类名、方法名的字符串标识)转换为直接引用(内存地址或指针)。
例如,将 User.getName() 中的 User 和 getName 转换为实际对应的类和方法在内存中的地址。

初始化(Initialization)

·核心任务:执行类的初始化逻辑,包括静态代码块和静态变量的显式赋值。
·触发时机:当类被主动使用时(如创建实例、调用静态方法 / 变量、反射、初始化子类等)。
·执行顺序:先执行父类的初始化(递归向上,直至 Object 类)。
按代码顺序执行静态变量的显式赋值和静态代码块(两者优先级相同,按书写顺序执行)。
例如:

public class Demo {
    static int a = 1; // 显式赋值
    static { b = 2; } // 静态代码块
    static int b;
}
// 初始化时,先执行 a=1,再执行 b=2,最终 a=1,b=2

11.什么是 Java 的 BigDecimal?

Java 的 BigDecimal 是用于精确处理任意精度十进制数的类,主要解决 float 和 double 等基本浮点类型因二进制存储导致的精度丢失问题(如 0.1 无法被精确表示),它通过整数 unscaledValue(未缩放值)和 int scale(缩放比例,即小数点后位数)来存储数值,支持高精度的算术运算(加、减、乘、除等),并允许通过设置舍入模式(如 RoundingMode.HALF_UP 四舍五入)控制运算结果的精度,广泛应用于金融、计算科学等对数值精度要求极高的场景,使用时需注意避免直接使用 double 作为构造参数(可能引入精度问题),而应优先使用 String 构造,同时进行除法等运算时必须指定精度和舍入模式以防止除不尽的情况抛出异常。

12.BigDecimal 为什么能保证精度不丢失?

BigDecimal 能保证精度不丢失,核心原因在于其底层采用 “整数未缩放值 + 整数缩放比例” 的存储结构,而非像 float、double 那样依赖二进制浮点存储(二进制无法精确表示部分十进制小数,如 0.1,导致计算时出现精度偏差):它通过一个 BigInteger 类型的 unscaledValue 存储数值的整数形式(例如 0.1 会被存储为整数 1),再通过一个 int 类型的 scale 记录小数点后的位数(例如 0.1 的 scale 为 1),这种存储方式本质上是用整数运算模拟十进制运算,完全遵循十进制的计数规则,避免了二进制与十进制转换时的精度损耗;同时,BigDecimal 提供的所有算术运算(加、减、乘、除等)均基于其内部整数结构实现,且支持通过指定舍入模式(如 RoundingMode.HALF_UP)和目标精度来精确控制运算结果,确保每一步计算都能按照预期保留所需精度,因此能在金融计算、科学运算等对精度要求极高的场景中,彻底解决基本浮点类型的精度丢失问题,实现数值的精确存储与计算。

13.使用 new String("yupi") 语句在 Java 中会创建多少个对象?

使用 new String("yupi") 可能创建 1 个或 2 个对象,取决于字符串常量池中是否已存在 "yupi":

·若常量池中不存在 "yupi",则会先在常量池创建 1 个 "yupi" 对象,再在堆内存中创建 1 个新的 String 对象(引用常量池中的值),共2个对象
·若常量池中已存在 "yupi",则仅在堆内存中创建 1 个新的 String 对象(引用常量池中的已有值),共1个对象

核心原因是 new 关键字必然在堆中新建对象,而字符串字面量 "yupi" 会先检查并复用常量池中的已有对象(若存在)。

14.Java 中 final、finally 和 finalize 各有什么区别?

Java 中 final、finally 和 finalize 是三个完全不同的概念,区别主要体现在作用和使用场景上:final 是修饰符,用于修饰类、方法或变量,修饰类时表示该类不可被继承,修饰方法时表示该方法不可被重写,修饰变量时表示该变量为常量,初始化后不可修改;finally 是异常处理结构的一部分,通常与 try-catch 搭配使用,用于声明无论是否发生异常都必须执行的代码(如资源释放),确保关键操作不会被遗漏;finalize 是 Object 类的一个方法,用于在对象被垃圾回收前执行一些清理操作(如释放非 Java 资源),但该方法的执行时机不确定,且已被标记为过时,不推荐使用。

15.为什么在 Java 中编写代码时会遇到乱码问题?

Java 中出现乱码的核心原因是字符编码 / 解码过程中使用的字符集不统一:计算机存储字符时需将其转换为字节(编码),读取时再转回字符(解码),若编码与解码使用的字符集(如 UTF-8、GBK、ISO-8859-1 等)不一致,就会导致字节序列无法正确映射为原始字符,从而出现乱码。常见场景包括:文件读写时未指定统一字符集(如用 GBK 编码的文件以 UTF-8 读取);网络传输中请求 / 响应的字符集不匹配(如前端用 UTF-8 发送数据,后端用 ISO-8859-1 解析);字符串在不同字符集间转换时处理不当(如将 UTF-8 字节用错误字符集解码后再编码);IDE 或 JVM 环境的默认字符集与代码预期不符等。本质上,乱码是字符的 “字节表示” 与 “字符集规则” 不匹配的结果。

16.为什么 JDK 9 中将 String 的 char 数组改为 byte 数组?

JDK 9 中将 String 内部的 char[] 改为 byte[] 主要是为了节省内存空间,优化存储效率:

char 类型在 Java 中占 2 个字节(UTF-16 编码),而实际应用中字符串多由 ASCII 字符(如英文字母、数字)组成,这类字符仅需 1 个字节即可表示,使用 char[] 会造成一半空间浪费;改为 byte[] 后,String 可通过一个额外的 coder 字段标记编码方式(LATIN1 或 UTF16),对于纯 ASCII 字符串,每个字符仅占用 1 字节,非 ASCII 字符仍用 2 字节存储,平均减少约 50% 的内存占用,尤其对字符串密集型应用(如 Web 服务、大数据处理),能显著降低内存消耗,提升系统性能。这一改动在保持 String 功能不变的前提下,通过更灵活的编码方式实现了存储优化。

17.如何在 Java 中调用外部可执行程序或系统命令?

在 Java 中调用外部可执行程序或系统命令,主要通过 java.lang.Runtime 类或 java.lang.ProcessBuilder 类实现,两者都能创建 Process 对象来与外部程序交互:

使用 Runtime.getRuntime().exec() 方法:直接传入命令字符串或字符串数组,返回 Process 对象,可通过该对象获取输入流、输出流和错误流,示例:

Process process = Runtime.getRuntime().exec("ls -l"); // 执行Linux的ls命令
// 处理输出(需注意流读取可能阻塞,建议用线程)
try (BufferedReader reader = new BufferedReader(
     new InputStreamReader(process.getInputStream()))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}
int exitCode = process.waitFor(); // 等待命令执行完成并获取退出码

使用 ProcessBuilder 类:更灵活,支持设置工作目录、环境变量等,通过 start() 方法启动进程,示例:

ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "dir"); // Windows的dir命令
pb.directory(new File("C:/")); // 设置工作目录
Process process = pb.start();
// 处理输出和等待完成(同上述方式)

需注意:外部命令的路径需正确(或配置环境变量);要及时处理输入输出流避免缓冲区满导致阻塞;需捕获 IOException 等异常;对于复杂命令,推荐用 ProcessBuilder 并以字符串数组形式传入命令参数(避免空格等特殊字符解析问题)。

18.如果一个线程在 Java 中被两次调用 start() 方法,会发生什么?

在 Java 中,一个线程对象若被两次调用 start() 方法,会抛出 IllegalThreadStateException 异常。

原因是:线程的生命周期中,start() 方法的作用是将线程从 “新建状态(New)” 转入 “就绪状态(Runnable)”,而一个线程对象一旦启动(即第一次调用 start() 后),就会进入运行或终止状态,其内部状态会被标记为 “已启动”。此时若再次调用 start(),JVM 会检测到线程已非新建状态,从而触发此异常,以阻止线程被重复启动。

这一设计确保了线程的生命周期唯一性 —— 一个线程对象只能被启动一次,即使执行完毕(进入终止状态),也无法通过再次调用 start() 重启,若需重复执行任务,需重新创建线程对象。

19.栈和队列在 Java 中的区别是什么?

在 Java 中,栈(Stack)和队列(Queue)是两种不同的数据结构,核心区别体现在元素存取顺序和典型实现类上:

栈(Stack) 遵循 “后进先出(LIFO,Last In First Out)” 原则,即最后添加的元素最先被取出,如同叠放的盘子,只能从顶端操作;Java 中可通过 java.util.Stack 类(继承自 Vector,不推荐)或 Deque 的 push()/pop() 方法(推荐,如 ArrayDeque)实现,常用于表达式求值、方法调用栈等场景。

队列(Queue) 遵循 “先进先出(FIFO,First In First Out)” 原则,即最先添加的元素最先被取出,如同排队购票,从队尾添加、队头取出;Java 中主要通过 java.util.Queue 接口实现,常见实现类有 LinkedList、ArrayDeque、PriorityQueue(优先级队列,特殊实现)等,多用于任务调度、缓冲处理等场景。

20.Java 的 Optional 类是什么?它有什么用?

Java 的 Optional<T> 是 Java 8 引入的一个容器类,用于封装可能为 null 的对象,其核心作用是通过显式的 API 设计避免 NullPointerException,使代码更健壮、可读性更强。

Optional 不直接存储 null,而是通过 empty() 表示空状态,通过 of() 或 ofNullable() 包装非空或可能为空的对象。它提供了一系列方法(如 isPresent() 判断是否有值、get() 获取值、orElse() 定义默认值、ifPresent() 消费值等),强制开发者在使用对象前处理空值情况,替代了传统的 if (obj != null) 判空逻辑,减少了冗余代码。例如,obj.orElse(new Object()) 可在对象为空时返回默认值,obj.ifPresent(o -> doSomething(o)) 仅在对象非空时执行操作。

通过 Optional,代码能更清晰地表达 “值可能缺失” 的语义,避免了隐藏的空指针风险,是函数式编程风格在 Java 中的典型应用。

0 条评论

发布
问题