Java常见问题及知识点总结

总结了Java 面试指南 | JavaGuide的Java ⾯试中最最最常问的⼀些问题的答案及相关知识点,供自己查阅。目前本文章包含Java基础、Java集合和Java并发。后续会继续补充。

Java基础

1. Java 中的几种基本数据类型?

  1. byte:1字节,-2^7 到 2^7-1

  2. short:2字节,-2^15 到 2^15-1

  3. int:4字节,-2^31 到 2^31-1

  4. long:8字节,-2^63 到 2^63-1

  5. float:4字节,最小值2^−126×2^−23=2^−149≈1.4E−45,最大值(2−2^−23)×2^127≈3.4E38

  6. double:8字节,最小值2^-1074≈4.9E-324,最大值(2−2^−52)×2^1023≈1.8E308

  7. char:2字节,最小值\u0000(十进制值为 0),最大值\uFFFF(十进制值为 65,535)

  8. boolean:1位,true/false

2. String 、 StringBuffer 和 StringBuilder ?

特性 String StringBuffer StringBuilder
可变性 不可变 可变 可变
线程安全性 线程安全(不可变对象自动线程安全) 线程安全(内部使用 synchronized 不线程安全
性能 每次修改都会创建新的对象,性能较低 相比 String 性能更高,但因使用同步机制较慢 性能最优,适用于单线程环境
初始容量 不适用(固定不可变) 默认初始容量 16,支持动态扩展 默认初始容量 16,支持动态扩展
扩容策略 不适用(不可变) 当字符数超过容量时,容量增加一倍 当字符数超过容量时,容量增加一倍
适用场景 适合不可变字符串的操作,常用于常量或配置 适合需要线程安全的可变字符串操作 适合单线程环境下需要频繁修改字符串的操作
主要操作方法 concat()replace()substring() append()insert()delete()reverse() append()insert()delete()reverse()
线程安全细节 不适用 使用 synchronized 锁保护方法 不使用锁,性能更好

3. String s1 = new String("abc")?

这段代码可能会创建一个或两个字符串对象,具体取决于是否字符串 "abc" 已经存在于字符串常量池中。

 String s1 = new String("abc");
  • 如果 "abc" 已经存在于字符串常量池中,则 new String("abc") 会创建一个新的 String 对象在堆中。因此,总共会创建 1 个 新的 String 对象。

  • 如果 "abc" 不存在于字符串常量池中,则 "abc" 会被添加到常量池中,然后在堆中创建一个新的 String 对象。因此,总共会创建 2 个 String 对象,一个在字符串常量池中,一个在堆中。

4. == 与 equals? hashCode 与 equals ?

== vs equals

  • ==:比较的是两个对象的内存地址,即它们是否引用同一个对象。用于判断两个对象是否是同一个实例

  • equals:比较的是两个对象的内容是否相等。默认的实现是 Object 类中的 equals 方法,它实际上使用的是 == 来比较对象的内存地址,但可以在自定义类中重写 equals 方法来比较对象的实际内容。

hashCodeequals

  • hashCode:hashCode 方法返回对象的哈希码(一个整数),它用于在哈希表中定位对象。

  • equals: equals 方法用于判断两个对象是否相等(重写后)。

5. 包装类型的缓存机制?

  1. 缓存范围:

    • 对于 Integer 类型,Java 缓存了从 -128127 的整数值。这是因为这些值在大多数程序中非常常用,并且在这个范围内创建对象的开销较大,因此通过缓存来节省内存和提高性能。

    • 对于 Character 类型,缓存了从 \u0000\u007F 的字符(即 ASCII 范围内的字符)。

    • 对于其他类型,如 ByteShort,也有类似的缓存机制,但范围可能有所不同。

  2. 实现机制:

    • Integer.valueOf(int): 这个方法会先检查传入的整数是否在缓存范围内。如果是,它会返回缓存中的对象;如果不是,它会创建一个新的 Integer 对象。

    • Character.valueOf(char): 类似地,对于 Character 类型,valueOf 方法会检查字符是否在缓存范围内,并返回缓存的字符对象。

  3. 为什么缓存:

    • 性能优化: 对于小范围的常用值,缓存可以减少对象创建的次数,从而提高性能。

    • 内存节约: 缓存可以避免重复创建相同值的对象,从而节省内存。

  4. 示例:

    Integer a = 100;
    Integer b = 100;
    System.out.println(a == b); // 输出 true,因为 100 在缓存范围内,a 和 b 指向同一个对象
    ​
    Integer c = 200;
    Integer d = 200;
    System.out.println(c == d); // 输出 false,因为 200 不在缓存范围内,c 和 d 是不同的对象

注意事项

  • 缓存的局限性: 缓存机制只适用于特定范围的值。超出缓存范围的值会创建新的对象,即使它们的值相同。

  • new 关键字: 使用 new 关键字创建包装对象时(如 new Integer(100)),不会使用缓存机制,而是每次都创建新的对象。

6.自动装箱与拆箱

自动装箱:当将基本数据类型赋值给其对应的包装类对象时,Java 编译器会自动调用包装类的 valueOf 方法,将基本数据类型转换为包装类型对象。示例:

int a = 10;
Integer b = a; // 自动装箱,将 int 转换为 Integer
Integer b = Integer.valueOf(a);  //原理

Integer.valueOf(int) 方法会先检查值是否在缓存范围内(-128 到 127),如果在,则返回缓存中的 Integer 对象,否则创建新的 Integer 对象。

自动拆箱:当将包装类型对象赋值给基本数据类型或在表达式中使用时,Java 编译器会自动调用包装类的 xxxValue() 方法,将包装类型对象转换为基本数据类型。示例:

Integer a = 10;
int b = a; // 自动拆箱,将 Integer 转换为 int
int b = a.intValue(); //原理

注意事项

  1. 性能影响:

    • 自动装箱和拆箱会导致隐式的对象创建和方法调用,可能会影响性能,特别是在高频率的装箱和拆箱操作中。

    • 尤其在使用集合类(如 List<Integer>)时,频繁的装箱和拆箱可能会导致不必要的对象分配和垃圾回收。

  2. 空指针异常(NullPointerException):

    • 自动拆箱可能会导致空指针异常。例如,如果 Integer 对象为 null,尝试将其自动拆箱为 int 时会抛出 NullPointerException

    Integer a = null;
    int b = a; // 这里会抛出 NullPointerException

  3. 比较操作:

    自动装箱时需要注意使用 == 比较两个包装对象时,可能会出现意外结果。因为 == 比较的是对象的引用,而非值。要正确比较值,需要使用 equals 方法。

7.深拷贝和浅拷贝区别

浅拷贝:复制对象时,基本类型字段被复制,但引用类型字段只复制引用。两个对象共享引用类型的成员,指向同一个对象。 实现方法:.clone()方法、构造函数复制

深拷贝:复制对象时,基本类型字段被复制,引用类型字段也会复制为新的对象副本。两个对象完全独立,不共享任何引用类型的成员。 实现方法:递归调用clone方法、使用序列化和反序列化实现深拷贝

8.谈谈对 Java 注解的理解

Java 注解(Annotations)是一种用于提供元数据的机制,允许开发者在代码中添加额外的信息而不影响实际的业务逻辑。注解在 Java 5 中被引入,用来替代 Java 中的标记接口和配置文件,增强了代码的可读性和灵活性。

注解是以 @ 符号开头的,通常位于类、方法、字段、参数等的上方,注解本身并不会直接影响代码的执行,但它可以被编译器或运行时环境读取,并根据注解的信息采取相应的措施。示例:

@Override
public String toString() {
    return "Example";
}

上面这个 @Override 注解告诉编译器这个方法是从父类或接口中重写的,如果没有正确重写,编译器会报错。

Java 注解解决了什么问题?
  1. 替代配置文件:

    • 在 Java EE 和 Spring 等框架中,注解通常用来替代繁琐的 XML 配置文件。通过注解,配置可以直接和代码放在一起,减少了配置错误和冗余,提高了配置的可维护性。例如,Spring 中的 @Autowired 用于自动注入依赖,代替 XML 中的 <bean> 配置。

    @Autowired
    private MyService myService;

  2. 元数据提供:

    • 注解允许开发者为代码添加元数据,这些元数据可以在编译时、类加载时或运行时通过反射机制被读取和处理。比如,@Deprecated 注解标记某个方法已经过时,不推荐使用,这样在使用该方法时编译器就会发出警告。

    @Deprecated
    public void oldMethod() {
        // Deprecated method
    }

  3. 简化代码:

    • 注解简化了代码,通过注解的使用,可以减少代码重复,增强代码的可读性和维护性。比如,JPA 的 @Entity 注解将类声明为实体类,@Table 注解指定数据库表名等,大大简化了数据库表映射的工作。

    @Entity
    @Table(name = "users")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(name = "username")
        private String username;
    }

  4. 提高编译期的安全性:

    • 通过注解,可以在编译期进行一些检查,减少运行时错误。例如,@Override 注解用于检测方法是否正确重写父类方法,防止拼写错误或方法签名不匹配等问题。

  5. 框架和库的扩展:

    • 许多框架和库使用注解来简化开发工作。例如,JUnit 中的 @Test 注解标记测试方法,框架会自动识别并执行这些测试。注解让框架和库能够以一种简洁、灵活的方式进行扩展和定制。

  6. 编译时和运行时处理:

    • 注解可以在编译时通过注解处理器进行处理(如 @Retention(RetentionPolicy.SOURCE)),也可以在运行时通过反射机制进行处理(如 @Retention(RetentionPolicy.RUNTIME))。这为开发者提供了灵活的手段来控制代码的行为。

常见的 Java 注解
  • 内置注解:

    • @Override: 用于标记重写父类方法。

    • @Deprecated: 用于标记过时的方法、类或字段。

    • @SuppressWarnings: 用于告诉编译器忽略特定的警告。

  • 元注解:

    • @Retention: 指定注解的保留策略(SOURCE、CLASS、RUNTIME)。

    • @Target: 指定注解可以应用的代码元素(类、方法、字段等)。

    • @Inherited: 指定注解是否可以被子类继承。

    • @Documented: 指定注解是否会包含在 JavaDoc 中。

  • 自定义注解:

    • 开发者可以根据需求定义自己的注解,用来标记和处理特定的代码逻辑。例如,可以定义一个 @MyAnnotation 来标记需要特殊处理的方法。

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotation {
        String value();
    }

9.ExceptionError的区别

ExceptionError 是 Java 中异常处理机制的重要组成部分,它们都是继承自 Throwable 类,但在概念和使用上有显著区别。

Throwable 是 Java 中所有异常和错误的超类,它有两个直接子类:ExceptionError

1.Exception

  • 定义: Exception 表示程序中可以捕获并处理的异常。它通常是由于代码错误或其他可预见的情况引发的,例如试图访问一个不存在的文件、除零操作、数组下标越界等。

  • 子类:

    • Checked Exception(受检异常): 必须在编译时处理(即用 try-catch 块捕获或用 throws 声明)。例如:IOException, SQLException

    • Unchecked Exception(未受检异常): 是 RuntimeException 的子类,不需要在编译时显式捕获。例如:NullPointerException, ArrayIndexOutOfBoundsException

  • 处理方式: 开发者可以通过 try-catch 语句捕获和处理这些异常,也可以通过在方法签名中使用 throws 关键字将异常抛出给调用者处理。

示例

try {
    int result = 10 / 0; // 可能抛出 ArithmeticException
} catch (ArithmeticException e) {
    System.out.println("Caught an ArithmeticException: " + e.getMessage());
}

2.Error

  • 定义: Error 表示严重的错误,这类错误通常是由于运行环境的问题导致的,程序无法合理地从这些错误中恢复。典型的例子包括内存不足 (OutOfMemoryError)、栈溢出 (StackOverflowError) 等。

  • 子类: Error 的子类包括 OutOfMemoryError, StackOverflowError, LinkageError 等。

  • 处理方式: 一般不应该捕获 Error,因为它们通常表示系统级别的错误,捕获和处理它们可能会隐藏更严重的问题,甚至导致系统不稳定。通常来说,这类错误应交由 JVM 处理。

示例

public class StackOverflowDemo {
    public static void recursiveMethod() {
        recursiveMethod(); // 递归调用导致 StackOverflowError
    }

    public static void main(String[] args) {
        try {
            recursiveMethod();
        } catch (StackOverflowError e) {
            System.out.println("Caught a StackOverflowError: " + e.getMessage());
        }
    }
}

区别总结:

  • Exception: 是开发者用于处理异常情况的工具,使得程序能够处理错误并继续执行。

  • Error: 是 JVM 用来表示程序无法继续执行的严重问题。

在编写代码时,开发者通常只需要关注 Exception 及其子类的处理,而不需要显式地处理 Error 类及其子类。

10. Java 反射的概念(不懂

反射(Reflection)是Java的一项强大特性,它允许程序在运行时动态地获取类的结构信息并操作类的成员(如属性、方法、构造器等)。通过反射,程序可以在编译时不知道类的具体信息的情况下,在运行时对类进行操作。

反射的基本操作

获取类的 Class 对象

每个 Java 类都有一个 Class 对象,包含了与该类相关的元数据。可以通过以下方式获取:

Class<?> clazz = Class.forName("com.example.MyClass"); // 使用类的全限定名:包名.子包名.类名
Class<?> clazz2 = MyClass.class; // 直接使用类名.class
Class<?> clazz3 = instance.getClass(); // 使用对象.getClass()

获取类的信息

可以通过反射获取类的构造函数、方法、字段、注解等信息。

Method[] methods = clazz.getDeclaredMethods(); // 获取所有方法
Field[] fields = clazz.getDeclaredFields(); // 获取所有字段
Constructor<?>[] constructors = clazz.getDeclaredConstructors(); // 获取所有构造函数

操作类的成员

通过反射,可以动态调用方法、设置字段值或创建类的实例。

Method method = clazz.getDeclaredMethod("methodName", String.class);
method.invoke(instance, "parameterValue"); // 动态调用方法

Field field = clazz.getDeclaredField("fieldName");
field.setAccessible(true); // 绕过访问控制检查
field.set(instance, "newValue"); // 动态设置字段值
反射的用途
  1. 框架和库

    • 反射在框架中很常用,例如 Spring、Hibernate 等。它们利用反射来实例化对象、调用方法、依赖注入等,而不需要在编译时知道具体的类。

  2. 动态代理

    • 反射用于动态生成代理类,动态代理在 AOP(面向切面编程)中尤为常见。代理类可以在运行时动态创建,处理方法调用并添加额外的逻辑。

  3. 序列化和反序列化

    • 在 JSON 序列化工具(如 Jackson、Gson)中,反射用于动态地将对象转换为 JSON 字符串,或将 JSON 字符串转换为对象。

  4. 开发工具和 IDE

    • 开发工具和 IDE 使用反射来提供功能,如代码自动补全、调试器的变量查看、JVM 内部状态的检查等。

反射的缺点
  1. 性能开销:反射操作比直接调用方法或访问字段慢得多,因为它绕过了常规的编译期优化。频繁使用反射可能会导致性能瓶颈。

  2. 安全问题:反射允许访问和修改私有成员,这可能破坏类的封装性和安全性。如果滥用,可能导致不可预知的错误或安全漏洞。

  3. 编译时类型检查缺失:由于反射是在运行时执行的,编译器无法对反射的操作进行类型检查。很多错误会在运行时才会暴露出来,增加了调试的复杂性。

  4. 可维护性降低:反射代码可能较难理解和维护,特别是当其被广泛使用时,会使得代码变得复杂且难以跟踪。

为什么框架需要反射?

框架使用反射主要是为了提供灵活性和动态性,使得程序员可以编写更少的代码,同时实现复杂的功能。例如,Spring 框架使用反射来自动装配依赖、管理 Bean 的生命周期,和实现 AOP 功能。这使得开发者可以专注于业务逻辑,而不需要处理繁琐的底层实现细节。

11. Java 泛型、类型擦除和通配符

泛型

Java 泛型是一种允许类、接口和方法在定义时使用类型参数的机制。泛型使得代码可以适用于多种数据类型,同时提供编译时的类型检查,增强了代码的安全性和可重用性。示例:

public class Box<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

在这个示例中,Box 类使用了泛型类型参数 T,因此可以在不修改 Box 类代码的情况下,创建适用于不同类型的 Box 对象。

类型擦除(Type Erasure)

类型擦除是 Java 泛型的一个重要特性。Java 编译器在编译过程中会删除或替换泛型信息,这意味着在运行时,所有的泛型类型参数都被擦除,转换为原始类型。

  • 作用: 类型擦除允许 Java 泛型与现有的非泛型代码兼容,保持了 Java 的向后兼容性。

  • 运行时表现: 由于类型擦除,泛型的类型参数在运行时并不存在。例如,List<String>List<Integer> 在运行时都是 List,无法通过反射或其他方式区分它们。

类型擦除的过程:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值