面试真实经历某商银行大厂Java问题和答案总结(三)

1. 深拷贝和浅拷贝

​编辑

问:什么是深拷贝和浅拷贝?它们之间有什么区别?

浅拷贝(Shallow Copy)

  • 浅拷贝指的是复制对象的引用,而不是复制对象本身。也就是说,对于对象中的引用类型字段,浅拷贝只是复制了这些字段的引用,而没有复制字段引用的对象本身。
  • 如果原始对象和拷贝对象中有相同的引用类型字段,修改其中一个字段会影响到另一个对象。编辑

深拷贝(Deep Copy)

  • 深拷贝指的是复制对象本身,并且递归地复制对象中的所有引用类型字段,确保两个对象中的引用类型字段互不影响。
  • 修改原始对象的引用类型字段,不会影响到深拷贝对象的相应字段。

区别

  • 浅拷贝只复制基本数据类型和对象的引用地址,引用类型字段指向同一内存地址。
  • 深拷贝会为所有字段分配新的内存空间,确保两个对象互不影响。编辑
java 复制代码
// 浅拷贝示例  
class Person implements Cloneable {  
    String name;  
    int age;

    public Person(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }

    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        return super.clone();  
    }  
}

// 深拷贝示例  
class Address {  
    String city;

    public Address(String city) {  
        this.city = city;  
    }  
}

class PersonWithAddress implements Cloneable {  
    String name;  
    int age;  
    Address address;

    public PersonWithAddress(String name, int age, Address address) {  
        this.name = name;  
        this.age = age;  
        this.address = address;  
    }

    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        PersonWithAddress person = (PersonWithAddress) super.clone();  
        person.address = new Address(this.address.city); // 创建新的Address对象,实现深拷贝  
        return person;  
    }  
}  

2. HashMap

问:HashMap 的底层是如何实现的?​编辑

:HashMap 底层是基于数组和链表(或红黑树,在 JDK 1.8 以后)实现的。它的实现分为以下几部分:

  1. 数组:HashMap 使用一个数组来存储键值对。数组的每个位置是一个桶(bucket),每个桶存储链表或红黑树(取决于元素的数量)。
  2. 哈希冲突 :如果两个键的哈希值相同,会发生哈希冲突。此时,HashMap 会在相应桶中使用链表或者红黑树来解决冲突。编辑
  3. 扩容机制:当 HashMap 中的元素个数达到负载因子与容量的乘积时,会触发扩容。扩容时,HashMap 会将容量扩大为原来的两倍,并重新计算所有元素的哈希值,确保哈希桶的均匀分布。

问:HashMap 的线程不安全性是如何产生的?如何解决?

:HashMap 在多线程环境下线程不安全。主要原因有以下几点:

  1. 在扩容时,数组和链表的操作是非原子的:在扩容时,可能会发生线程之间的竞争条件,导致数据丢失或覆盖。
  2. 数据读写不加锁:在多线程环境下,一个线程可能在另一个线程更新值的同时读取到过期或不一致的值。

解决方法

  • 使用 Collections.synchronizedMap 方法来包装 HashMap,使其线程安全。
  • 使用 ConcurrentHashMap 替代 HashMap,它提供了更高效的并发支持。
java 复制代码
// 使用 ConcurrentHashMap 替代 HashMap  
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();  
concurrentMap.put("key", 1);  

问:HashMap 和 Hashtable 有什么区别?

  1. 线程安全Hashtable 是线程安全的,而 HashMap 不是。Hashtable 的所有方法都使用 synchronized 关键字进行同步。
  2. 性能 :由于 Hashtable 存在同步机制,它在单线程环境下性能较差,而 HashMap 更高效。
  3. null 键和值HashMap 允许一个 null 键和多个 null 值,而 Hashtable 不允许 null 键和值。
  4. 扩容Hashtable 默认的初始容量为 11,增长因子为 2;而 HashMap 默认的初始容量为 16,负载因子为 0.75。

问:HashMap 如何处理哈希冲突?

:当多个键具有相同的哈希值时,HashMap 会使用链表或者红黑树来处理哈希冲突。

  1. 链表法:哈希冲突时,HashMap 会将这些冲突的键值对以链表的形式挂在同一个桶中。
  2. 红黑树:当某个桶中的元素超过 8 个时,HashMap 会将链表转换为红黑树,以提高查询效率。

问:HashMap 中 get 方法的时间复杂度是多少?

:在最优情况下,get 方法的时间复杂度是 O(1),即常数时间。哈希冲突的情况下,如果链表很长或者转为红黑树,时间复杂度分别是 O(n) 和 O(log n)。

问:HashMap 中的 hashCode 方法和 equals 方法是如何影响哈希表的性能的?

HashMap 使用 hashCode 方法来计算键的哈希值,并根据哈希值将键值对放入不同的桶中。如果两个键具有相同的 hashCode 值,它们就会被放入同一个桶中,这可能会导致哈希冲突。equals 方法用于比较两个键是否相等,当哈希值相同时,equals 方法决定它们是否属于同一个键。

为了保证 HashMap 的性能,hashCode 方法应尽可能均匀地分布哈希值,减少哈希冲突。equals 方法应准确地判断对象是否相等。

3. ConcurrentHashMap

问:ConcurrentHashMap 是如何保证线程安全的?

ConcurrentHashMap 通过以下几种方式保证线程安全:

  1. 分段锁:将哈希表分成多个段(Segment),每个段可以独立加锁,这样多个线程可以同时操作不同的段,提高并发性能。
  2. CAS 操作ConcurrentHashMap 使用 CAS(Compare-And-Swap)操作来保证对单个元素的更新是原子的,避免了使用传统的锁机制。

问:ConcurrentHashMap 中的 putIfAbsent 方法是如何工作的?

putIfAbsent 方法会将指定的键值对放入 ConcurrentHashMap 中,只有在该键不存在的情况下才会插入。如果键已经存在,它会返回该键的当前值,而不会进行任何插入操作。这是原子操作,可以确保并发环境下不会插入重复的键值对。

java 复制代码
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();  
map.putIfAbsent("key", 1);  

4. HashSet

问:HashSetHashMap 有什么区别?

HashSet 是基于 HashMap 实现的,HashSet 中的元素就是 HashMap 中的键,且没有值。HashSet 保证元素的唯一性,而 HashMap 保证键的唯一性。

5. Java 命名规则

问:Java 的命名规则是什么?

  1. 类名 :类名应使用驼峰命名法(PascalCase),首字母大写,且每个单词的首字母大写,如 MyClass
  2. 方法名 :方法名应使用驼峰命名法(camelCase),首字母小写,如 calculateTotal().
  3. 变量名 :变量名应使用驼峰命名法(camelCase),首字母小写,如 totalAmount
  4. 常量名 :常量名应使用全大写字母,并且单词之间用下划线分隔,如 MAX_VALUE

6. Java 异常

Java 中的异常用于处理程序运行时出现的错误情况。通过异常机制,可以捕捉错误,避免程序因异常崩溃,并且能够提供更好的错误处理和调试机制。Java 异常分为两大类:检查异常(Checked Exception)非检查异常(Unchecked Exception)

1. 异常的分类

  • 检查异常(Checked Exception)

    • 编译时异常,程序在编译时会检查到这些异常,必须显式地处理。

    • 例如:IOExceptionSQLException

    • 必须在方法中声明抛出异常,或者在代码中捕获异常。

  • 非检查异常(Unchecked Exception)

    • 运行时异常,不要求程序员在编译时进行处理,通常表示程序逻辑错误。

    • 例如:NullPointerExceptionArrayIndexOutOfBoundsExceptionArithmeticException

    • 这些异常可以在运行时发生,但不要求显式地捕获或声明抛出。

2. 异常的层次结构

异常类 Throwable 是所有异常的父类,分为两类:

  • Error :表示 JVM 自身的错误或资源耗尽问题,如 OutOfMemoryError
  • Exception :用于表示程序中的异常,进一步分为:
    • Checked Exception :编译时异常。
    • Unchecked Exception:运行时异常。

3. 常见异常类型

  • NullPointerException :尝试访问一个为 null 的对象或数组。
    java String str = null; System.out.println(str.length()); // 会抛出 NullPointerException

  • ArrayIndexOutOfBoundsException :访问数组时使用了非法的索引。
    java int[] arr = new int[5]; System.out.println(arr[10]); // 会抛出 ArrayIndexOutOfBoundsException

  • ArithmeticException :算术运算错误,比如除以零。
    java int result = 10 / 0; // 会抛出 ArithmeticException

  • ClassNotFoundException :当应用程序试图通过字符串名称加载类时,指定的类无法找到。
    java try { Class.forName("com.example.MyClass"); } catch (ClassNotFoundException e) { e.printStackTrace(); }

  • IOException :处理输入输出时的异常。
    java try { FileReader file = new FileReader("nonexistentfile.txt"); } catch (IOException e) { e.printStackTrace(); }

  • SQLException :数据库操作异常。
    java try { Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/db", "user", "pass"); } catch (SQLException e) { e.printStackTrace(); }

4. 如何处理异常

Java 提供了 try-catch 语句来捕获并处理异常。处理异常时,可以捕获多个异常类型,并在代码中提供适当的处理逻辑。

try-catch

  • try 块:用来包围可能抛出异常的代码。
  • catch 块:捕获并处理异常。
java 复制代码
try {  
    int[] arr = new int[5];  
    System.out.println(arr[10]);  // 可能会抛出 ArrayIndexOutOfBoundsException  
} catch (ArrayIndexOutOfBoundsException e) {  
    System.out.println("Array index out of bounds!");  
} catch (Exception e) {  
    System.out.println("Some other exception occurred.");  
}  

finallyfinally 块中的代码总是会执行,通常用于清理工作,如关闭文件或数据库连接等,不论是否有异常发生。

java 复制代码
try {  
    // 执行可能抛出异常的代码  
} catch (Exception e) {  
    // 异常处理  
} finally {  
    // 清理工作,例如关闭文件流  
}  

5. 抛出异常

有时候你需要在方法中主动抛出异常,使用 throw 关键字:

java 复制代码
if (age < 0) {  
    throw new IllegalArgumentException("Age cannot be negative");  
}  

当你需要声明方法可能抛出异常时,使用 throws 关键字:

java 复制代码
public void readFile(String filename) throws IOException {  
    FileReader file = new FileReader(filename);  
    // 处理文件操作  
}  

6. 自定义异常

可以通过继承 ExceptionRuntimeException 类来创建自己的异常类。例如,创建一个自定义的 InvalidAgeException

java 复制代码
public class InvalidAgeException extends Exception {  
    public InvalidAgeException(String message) {  
        super(message);  
    }  
}  

使用自定义异常:

java 复制代码
public class AgeValidator {  
    public void validate(int age) throws InvalidAgeException {  
        if (age < 0) {  
            throw new InvalidAgeException("Age cannot be negative");  
        }  
    }  
}

public class Test {  
    public static void main(String[] args) {  
        AgeValidator validator = new AgeValidator();  
        try {  
            validator.validate(-1);  
        } catch (InvalidAgeException e) {  
            System.out.println(e.getMessage());  
        }  
    }  
}  

7. 异常链

异常链是指一个异常可以在被捕获时抛出另一个异常,从而将原始异常信息传递下去。可以通过 Throwable 类的 initCause 方法来实现:

java 复制代码
try {  
    // 模拟一个抛出异常的操作  
    throw new NullPointerException("Null pointer exception");  
} catch (NullPointerException e) {  
    throw new RuntimeException("Runtime exception caused by NullPointerException", e);  
}  

总结

  • 检查异常:必须处理或声明,不处理会导致编译错误。
  • 非检查异常:通常表示程序中的逻辑错误,编译时不会强制要求处理。
  • 使用 try-catch 处理异常,确保程序能够优雅地处理错误。
  • finally 块用于执行清理工作。
  • 可以使用 throwthrows 来手动抛出和声明异常。

相关推荐
绝无仅有5 小时前
面试真实经历某商银行大厂Java问题和答案总结(五)
后端·面试·github
Victor3565 小时前
Redis(63)Redis的Lua脚本如何使用?
后端
风象南5 小时前
SpringBoot实现JWT动态密钥轮换
后端
摇滚侠9 小时前
Spring Boot 3零基础教程,IOC容器中组件的注册,笔记08
spring boot·笔记·后端
程序员小凯11 小时前
Spring Boot测试框架详解
java·spring boot·后端
你的人类朋友12 小时前
什么是断言?
前端·后端·安全
程序员小凯13 小时前
Spring Boot缓存机制详解
spring boot·后端·缓存
wxweven13 小时前
校招面试官揭秘:我们到底在寻找什么样的技术人才?
java·面试·校招
i学长的猫13 小时前
Ruby on Rails 从0 开始入门到进阶到高级 - 10分钟速通版
后端·ruby on rails·ruby