Android学习总结之Java篇(一)
泛型擦除
一、基础概念与原理(必问)
问题 1:什么是泛型擦除?它在 Java 中的实现原理是什么?
回答核心:
泛型擦除是 Java 泛型的底层机制,指编译器在编译时会擦除泛型的具体类型信息,将泛型参数替换为其上限类型(通常是Object
),仅在编译期保留类型检查,运行时类型信息丢失。
- 实现原理:
- 类型替换:如
List<String>
编译后变为List
(原始类型),所有T
被替换为Object
。 - 桥接方法:当子类泛型类型与父类不同时,编译器生成桥接方法(如
setSrc(Object)
)以维持多态性。 - 兼容性:确保泛型代码能在旧版本 JVM 上运行。
- 类型替换:如
示例:
List<String> list = new ArrayList<>();
list.add("Android");
// 编译后,list的类型被擦除为List,运行时无法区分String与Integer类型
二、Android 开发中的典型场景(高频考点)
问题 2:泛型擦除在 Android 开发中会引发哪些问题?如何解决?
回答核心:
-
运行时类型丢失:无法通过反射直接获取泛型参数类型。
- 解决方案:使用
TypeToken
(如 Gson)或子类化保留类型信息。// Gson中解析List<String> Type type = new TypeToken<List<String>>() {}.getType(); List<String> list = gson.fromJson(json, type);
- 解决方案:使用
-
泛型数组初始化限制:
- 错误示例:
T[] array = new T[10];
(编译错误)。 - 解决方案:手动强制转换
Object[]
数组。
- 错误示例:
-
方法重载冲突:子类无法通过泛型参数重载父类方法。
- 错误示例:
class Parent<T> {public void method(T param) {} // 擦除后为method(Object) } class Child extends Parent<String> {public void method(String param) {} // 编译错误:与父类方法签名冲突 }
- 解决方案:通过接口或通配符(
?
)定义方法。
- 错误示例:
三、框架与工具的实战应用(重点)
问题 3:Gson 如何处理泛型擦除?请举例说明。
回答核心:
Gson 通过TypeToken
解决泛型擦除问题。TypeToken
利用匿名内部类保留泛型类型信息,通过反射获取实际类型。
- 示例:
// 解析嵌套泛型类型List<Map<String, Integer>> Type type = new TypeToken<List<Map<String, Integer>>>() {}.getType(); List<Map<String, Integer>> result = gson.fromJson(json, type);
- 原理:匿名内部类的父类泛型信息被记录在 Class 文件的
Signature
属性中,通过反射可获取。
问题 4:Kotlin 如何解决泛型擦除?
回答核心:
Kotlin 通过reified
关键字(配合inline
函数)实化泛型,在运行时保留类型信息。
- 示例:
inline fun <reified T> fetchData(): T {val type = T::class.java// 使用反射或网络请求获取数据return data as T } // 调用时直接获取具体类型 val result = fetchData<Result>()
- 原理:
inline
函数在编译时将函数体替换到调用处,reified
确保泛型类型被保留。
四、反射与泛型擦除的深度交互(难点)
问题 5:在 Android 中,如何通过反射获取泛型字段的实际类型?
回答核心:
通过ParameterizedType
接口解析泛型信息。
- 示例:
class MyClass<T> {private List<T> data; } // 获取data字段的泛型类型 Field field = MyClass.class.getDeclaredField("data"); Type genericType = field.getGenericType(); if (genericType instanceof ParameterizedType) {Type actualType = ((ParameterizedType) genericType).getActualTypeArguments()[0];System.out.println("实际类型:" + actualType.getTypeName()); // 输出T的具体类型 }
- 注意:需处理
Type
的多层嵌套(如List<Map<String, ?>>
)。
五、面试官高频追问(陷阱题)
追问 1:泛型擦除如何影响类型安全?
回答:
编译期保证类型安全,但运行时类型信息丢失可能导致ClassCastException
。例如,通过反射向List<String>
中插入Integer
会绕过编译检查,运行时崩溃。
追问 2:为什么 Java 不支持泛型数组?
回答:
泛型数组在运行时无法保留类型信息,可能导致内存安全问题。例如:
List<String>[] array = new List<String>[10]; // 编译错误
array[0] = new ArrayList<Integer>(); // 运行时将引发ClassCastException
追问 3:Retrofit 如何处理泛型擦除?
回答:
Retrofit 通过ParameterizedType
解析方法返回值的泛型类型。例如,Call<Result<T>>
的泛型信息被记录在方法的Signature
属性中,通过反射获取并传递给 Gson 进行序列化。
synchronized 底层原理
底层实现原理
- 对象头:在 Java 中,每个对象都有一个对象头(Object Header),对象头中包含了一些与锁相关的信息,如锁状态、哈希码、分代年龄等。锁状态有四种:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。
- 偏向锁:偏向锁是为了在无竞争的情况下减少锁的开销。当一个线程第一次访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需要简单地测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要查看 Mark Word 中偏向锁的标识是否设置成 1(表示当前是偏向锁):如果没有设置,则使用 CAS 竞争锁;如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。
- 轻量级锁:当多个线程交替执行同步块时,偏向锁会升级为轻量级锁。线程在执行同步块之前,JVM 会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,官方称为 Displaced Mark Word。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程已经竞争到锁,当前线程会尝试自旋等待锁的释放。
- 重量级锁:如果自旋次数达到一定阈值或者有多个线程同时竞争锁,轻量级锁会升级为重量级锁。重量级锁依赖于操作系统的互斥量(Mutex),线程会被阻塞,进入等待队列,当锁被释放时,操作系统会唤醒等待队列中的线程继续竞争锁。
锁的分类
1. 乐观锁 vs 悲观锁
- 悲观锁(如
synchronized
、显式锁ReentrantLock
):- 假设竞争激烈,每次访问共享资源前先加锁,确保独占访问。
- 包含上述偏向锁、轻量级锁、重量级锁(均为悲观锁的不同优化形态)。
- 乐观锁(如 CAS):
- 假设竞争较少,不加锁而是直接尝试操作,失败时重试(无锁编程)。
- 缺点:存在 ABA 问题(需通过
AtomicStampedReference
解决)。
2. 公平锁 vs 非公平锁
- 公平锁:线程按申请顺序获取锁(如
ReentrantLock(true)
),减少 “饥饿” 但增加上下文切换开销。 - 非公平锁:允许刚释放的锁被任意线程抢占(如
synchronized
、ReentrantLock(false)
),效率更高但可能导致部分线程长时间等待。
3. 可重入锁 vs 不可重入锁
- 可重入锁:同一线程可多次获取同一把锁(如
synchronized
、ReentrantLock
),通过计数器记录重入次数,避免死锁。public synchronized void method1() {method2(); // 可重入,无需再次竞争锁 } public synchronized void method2() {}
- 不可重入锁:未实现重入逻辑(如早期 Java 版本的
synchronized
非显式实现,现几乎不用)。
CAS 的缺点及解决办法
1. ABA 问题
- 问题描述:CAS 操作在比较和交换时,会检查变量的值是否与预期值相同。如果一个变量的值从 A 变为 B,再从 B 变回 A,CAS 操作会认为变量的值没有发生变化,从而继续执行更新操作,但实际上变量的值已经发生了变化,这可能会导致一些意外的结果。
- 解决办法:使用带有版本号的原子引用类
AtomicStampedReference
或AtomicMarkableReference
。AtomicStampedReference
会在更新值的同时更新版本号,每次更新时会检查值和版本号是否都与预期值相同;AtomicMarkableReference
则是使用一个布尔值来标记变量是否被修改过。
import java.util.concurrent.atomic.AtomicStampedReference;public class ABAExample {private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 0);public static void main(String[] args) {Thread t1 = new Thread(() -> {int stamp = atomicStampedRef.getStamp();System.out.println("Thread 1 stamp: " + stamp);atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);});Thread t2 = new Thread(() -> {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}int stamp = atomicStampedRef.getStamp();System.out.println("Thread 2 stamp: " + stamp);boolean result = atomicStampedRef.compareAndSet(100, 102, stamp, stamp + 1);System.out.println("Thread 2 update result: " + result);});t1.start();t2.start();}
}
2. 循环时间长开销大
- 问题描述:如果 CAS 操作长时间不成功,线程会一直自旋,会消耗大量的 CPU 资源。
- 解决办法:可以设置自旋的最大次数,当达到最大次数后,线程放弃自旋,进入阻塞状态。另外,也可以使用锁机制,当 CAS 操作失败时,使用传统的锁来保证线程同步。
3. 只能保证一个共享变量的原子操作
- 问题描述:CAS 操作只能对一个共享变量进行原子操作,如果需要对多个共享变量进行原子操作,CAS 就无法满足需求。
- 解决办法:可以使用
AtomicReference
类将多个共享变量封装成一个对象,然后对这个对象进行 CAS 操作。另外,也可以使用锁机制来保证多个共享变量的原子性。
import java.util.concurrent.atomic.AtomicReference;class Pair {int x;int y;public Pair(int x, int y) {this.x = x;this.y = y;}
}public class MultiVariableCASExample {private static AtomicReference<Pair> atomicPair = new AtomicReference<>(new Pair(0, 0));public static void main(String[] args) {Pair expected = atomicPair.get();Pair newPair = new Pair(1, 1);boolean result = atomicPair.compareAndSet(expected, newPair);System.out.println("Update result: " + result);}
}