1. 深拷贝和浅拷贝
编辑
问:什么是深拷贝和浅拷贝?它们之间有什么区别?
浅拷贝(Shallow Copy):
- 浅拷贝指的是复制对象的引用,而不是复制对象本身。也就是说,对于对象中的引用类型字段,浅拷贝只是复制了这些字段的引用,而没有复制字段引用的对象本身。
- 如果原始对象和拷贝对象中有相同的引用类型字段,修改其中一个字段会影响到另一个对象。
编辑
深拷贝(Deep Copy):
- 深拷贝指的是复制对象本身,并且递归地复制对象中的所有引用类型字段,确保两个对象中的引用类型字段互不影响。
- 修改原始对象的引用类型字段,不会影响到深拷贝对象的相应字段。
区别:
- 浅拷贝只复制基本数据类型和对象的引用地址,引用类型字段指向同一内存地址。
- 深拷贝会为所有字段分配新的内存空间,确保两个对象互不影响。
编辑
1// 浅拷贝示例 2class Person implements Cloneable { 3 String name; 4 int age; 5 6 public Person(String name, int age) { 7 this.name = name; 8 this.age = age; 9 } 10 11 @Override 12 protected Object clone() throws CloneNotSupportedException { 13 return super.clone(); 14 } 15} 16 17// 深拷贝示例 18class Address { 19 String city; 20 21 public Address(String city) { 22 this.city = city; 23 } 24} 25 26class PersonWithAddress implements Cloneable { 27 String name; 28 int age; 29 Address address; 30 31 public PersonWithAddress(String name, int age, Address address) { 32 this.name = name; 33 this.age = age; 34 this.address = address; 35 } 36 37 @Override 38 protected Object clone() throws CloneNotSupportedException { 39 PersonWithAddress person = (PersonWithAddress) super.clone(); 40 person.address = new Address(this.address.city); // 创建新的Address对象,实现深拷贝 41 return person; 42 } 43} 44
2. HashMap
问:HashMap 的底层是如何实现的?
编辑
答:HashMap 底层是基于数组和链表(或红黑树,在 JDK 1.8 以后)实现的。它的实现分为以下几部分:
- 数组:HashMap 使用一个数组来存储键值对。数组的每个位置是一个桶(bucket),每个桶存储链表或红黑树(取决于元素的数量)。
- 哈希冲突:如果两个键的哈希值相同,会发生哈希冲突。此时,HashMap 会在相应桶中使用链表或者红黑树来解决冲突。
编辑
- 扩容机制:当 HashMap 中的元素个数达到负载因子与容量的乘积时,会触发扩容。扩容时,HashMap 会将容量扩大为原来的两倍,并重新计算所有元素的哈希值,确保哈希桶的均匀分布。
问:HashMap 的线程不安全性是如何产生的?如何解决?
答:HashMap 在多线程环境下线程不安全。主要原因有以下几点:
- 在扩容时,数组和链表的操作是非原子的:在扩容时,可能会发生线程之间的竞争条件,导致数据丢失或覆盖。
- 数据读写不加锁:在多线程环境下,一个线程可能在另一个线程更新值的同时读取到过期或不一致的值。
解决方法:
- 使用
Collections.synchronizedMap方法来包装 HashMap,使其线程安全。 - 使用
ConcurrentHashMap替代HashMap,它提供了更高效的并发支持。
1// 使用 ConcurrentHashMap 替代 HashMap 2ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>(); 3concurrentMap.put("key", 1); 4
问:HashMap 和 Hashtable 有什么区别?
答:
- 线程安全:
Hashtable是线程安全的,而HashMap不是。Hashtable的所有方法都使用synchronized关键字进行同步。 - 性能:由于
Hashtable存在同步机制,它在单线程环境下性能较差,而HashMap更高效。 - null 键和值:
HashMap允许一个null键和多个null值,而Hashtable不允许null键和值。 - 扩容:
Hashtable默认的初始容量为 11,增长因子为 2;而HashMap默认的初始容量为 16,负载因子为 0.75。
问:HashMap 如何处理哈希冲突?
答:当多个键具有相同的哈希值时,HashMap 会使用链表或者红黑树来处理哈希冲突。
- 链表法:哈希冲突时,HashMap 会将这些冲突的键值对以链表的形式挂在同一个桶中。
- 红黑树:当某个桶中的元素超过 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 通过以下几种方式保证线程安全:
- 分段锁:将哈希表分成多个段(Segment),每个段可以独立加锁,这样多个线程可以同时操作不同的段,提高并发性能。
- CAS 操作:
ConcurrentHashMap使用 CAS(Compare-And-Swap)操作来保证对单个元素的更新是原子的,避免了使用传统的锁机制。
问:ConcurrentHashMap 中的 putIfAbsent 方法是如何工作的?
答:putIfAbsent 方法会将指定的键值对放入 ConcurrentHashMap 中,只有在该键不存在的情况下才会插入。如果键已经存在,它会返回该键的当前值,而不会进行任何插入操作。这是原子操作,可以确保并发环境下不会插入重复的键值对。
1ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); 2map.putIfAbsent("key", 1); 3
4. HashSet
问:HashSet 和 HashMap 有什么区别?
答:HashSet 是基于 HashMap 实现的,HashSet 中的元素就是 HashMap 中的键,且没有值。HashSet 保证元素的唯一性,而 HashMap 保证键的唯一性。
5. Java 命名规则
问:Java 的命名规则是什么?
答:
- 类名:类名应使用驼峰命名法(PascalCase),首字母大写,且每个单词的首字母大写,如
MyClass。 - 方法名:方法名应使用驼峰命名法(camelCase),首字母小写,如
calculateTotal(). - 变量名:变量名应使用驼峰命名法(camelCase),首字母小写,如
totalAmount。 - 常量名:常量名应使用全大写字母,并且单词之间用下划线分隔,如
MAX_VALUE。
6. Java 异常
Java 中的异常用于处理程序运行时出现的错误情况。通过异常机制,可以捕捉错误,避免程序因异常崩溃,并且能够提供更好的错误处理和调试机制。Java 异常分为两大类:检查异常(Checked Exception)和非检查异常(Unchecked Exception)。
1. 异常的分类
- 检查异常(Checked Exception):
- 编译时异常,程序在编译时会检查到这些异常,必须显式地处理。
- 例如:IOException、SQLException。
- 必须在方法中声明抛出异常,或者在代码中捕获异常。 - 非检查异常(Unchecked Exception):
- 运行时异常,不要求程序员在编译时进行处理,通常表示程序逻辑错误。
- 例如:NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException。
- 这些异常可以在运行时发生,但不要求显式地捕获或声明抛出。
2. 异常的层次结构
异常类 Throwable 是所有异常的父类,分为两类:
- Error:表示 JVM 自身的错误或资源耗尽问题,如
OutOfMemoryError。 - Exception:用于表示程序中的异常,进一步分为:
- Checked Exception:编译时异常。
- Unchecked Exception:运行时异常。
3. 常见异常类型
NullPointerException:尝试访问一个为null的对象或数组。
java String str = null; System.out.println(str.length()); // 会抛出 NullPointerExceptionArrayIndexOutOfBoundsException:访问数组时使用了非法的索引。
java int[] arr = new int[5]; System.out.println(arr[10]); // 会抛出 ArrayIndexOutOfBoundsExceptionArithmeticException:算术运算错误,比如除以零。
java int result = 10 / 0; // 会抛出 ArithmeticExceptionClassNotFoundException:当应用程序试图通过字符串名称加载类时,指定的类无法找到。
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块:捕获并处理异常。
1try { 2 int[] arr = new int[5]; 3 System.out.println(arr[10]); // 可能会抛出 ArrayIndexOutOfBoundsException 4} catch (ArrayIndexOutOfBoundsException e) { 5 System.out.println("Array index out of bounds!"); 6} catch (Exception e) { 7 System.out.println("Some other exception occurred."); 8} 9
finally 块:finally 块中的代码总是会执行,通常用于清理工作,如关闭文件或数据库连接等,不论是否有异常发生。
1try { 2 // 执行可能抛出异常的代码 3} catch (Exception e) { 4 // 异常处理 5} finally { 6 // 清理工作,例如关闭文件流 7} 8
5. 抛出异常
有时候你需要在方法中主动抛出异常,使用 throw 关键字:
1if (age < 0) { 2 throw new IllegalArgumentException("Age cannot be negative"); 3} 4
当你需要声明方法可能抛出异常时,使用 throws 关键字:
1public void readFile(String filename) throws IOException { 2 FileReader file = new FileReader(filename); 3 // 处理文件操作 4} 5
6. 自定义异常
可以通过继承 Exception 或 RuntimeException 类来创建自己的异常类。例如,创建一个自定义的 InvalidAgeException:
1public class InvalidAgeException extends Exception { 2 public InvalidAgeException(String message) { 3 super(message); 4 } 5} 6
使用自定义异常:
1public class AgeValidator { 2 public void validate(int age) throws InvalidAgeException { 3 if (age < 0) { 4 throw new InvalidAgeException("Age cannot be negative"); 5 } 6 } 7} 8 9public class Test { 10 public static void main(String[] args) { 11 AgeValidator validator = new AgeValidator(); 12 try { 13 validator.validate(-1); 14 } catch (InvalidAgeException e) { 15 System.out.println(e.getMessage()); 16 } 17 } 18} 19
7. 异常链
异常链是指一个异常可以在被捕获时抛出另一个异常,从而将原始异常信息传递下去。可以通过 Throwable 类的 initCause 方法来实现:
1try { 2 // 模拟一个抛出异常的操作 3 throw new NullPointerException("Null pointer exception"); 4} catch (NullPointerException e) { 5 throw new RuntimeException("Runtime exception caused by NullPointerException", e); 6} 7
总结
- 检查异常:必须处理或声明,不处理会导致编译错误。
- 非检查异常:通常表示程序中的逻辑错误,编译时不会强制要求处理。
- 使用
try-catch处理异常,确保程序能够优雅地处理错误。 finally块用于执行清理工作。- 可以使用
throw和throws来手动抛出和声明异常。
《面试真实经历某商银行大厂Java问题和答案总结(三)》 是转载文章,点击查看原文。
