当前位置: 首页 > news >正文

Android学习总结之Java篇(一)

泛型擦除

一、基础概念与原理(必问)

问题 1:什么是泛型擦除?它在 Java 中的实现原理是什么?
回答核心
泛型擦除是 Java 泛型的底层机制,指编译器在编译时会擦除泛型的具体类型信息,将泛型参数替换为其上限类型(通常是Object),仅在编译期保留类型检查,运行时类型信息丢失。

  • 实现原理
    1. 类型替换:如List<String>编译后变为List(原始类型),所有T被替换为Object
    2. 桥接方法:当子类泛型类型与父类不同时,编译器生成桥接方法(如setSrc(Object))以维持多态性。
    3. 兼容性:确保泛型代码能在旧版本 JVM 上运行。

示例

List<String> list = new ArrayList<>();
list.add("Android");
// 编译后,list的类型被擦除为List,运行时无法区分String与Integer类型
二、Android 开发中的典型场景(高频考点)

问题 2:泛型擦除在 Android 开发中会引发哪些问题?如何解决?
回答核心

  1. 运行时类型丢失:无法通过反射直接获取泛型参数类型。

    • 解决方案:使用TypeToken(如 Gson)或子类化保留类型信息。
      // Gson中解析List<String>
      Type type = new TypeToken<List<String>>() {}.getType();
      List<String> list = gson.fromJson(json, type);
      
  2. 泛型数组初始化限制

    • 错误示例T[] array = new T[10];(编译错误)。
    • 解决方案:手动强制转换Object[]数组。
  3. 方法重载冲突:子类无法通过泛型参数重载父类方法。

    • 错误示例
      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)),减少 “饥饿” 但增加上下文切换开销。
  • 非公平锁:允许刚释放的锁被任意线程抢占(如 synchronizedReentrantLock(false)),效率更高但可能导致部分线程长时间等待。
3. 可重入锁 vs 不可重入锁
  • 可重入锁:同一线程可多次获取同一把锁(如 synchronizedReentrantLock),通过计数器记录重入次数,避免死锁。
    public synchronized void method1() {method2(); // 可重入,无需再次竞争锁
    }
    public synchronized void method2() {}
    
  • 不可重入锁:未实现重入逻辑(如早期 Java 版本的 synchronized 非显式实现,现几乎不用)。

CAS 的缺点及解决办法

1. ABA 问题
  • 问题描述:CAS 操作在比较和交换时,会检查变量的值是否与预期值相同。如果一个变量的值从 A 变为 B,再从 B 变回 A,CAS 操作会认为变量的值没有发生变化,从而继续执行更新操作,但实际上变量的值已经发生了变化,这可能会导致一些意外的结果。
  • 解决办法:使用带有版本号的原子引用类 AtomicStampedReference 或 AtomicMarkableReferenceAtomicStampedReference 会在更新值的同时更新版本号,每次更新时会检查值和版本号是否都与预期值相同;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);}
}

相关文章:

  • 关于https请求丢字符串导致收到报文解密失败问题
  • java.lang.AssertionError: Binder ProxyMap has too many entries: 问题处理
  • 深入理解链表:从基础操作到高频面试题解析
  • Linux[开发工具]
  • 主流AI推理模型的详细说明、对比及总结表格
  • android录音生成wav
  • 铭记之日(3)——4.28
  • 【软件工程】需求分析详解
  • maven私服配置
  • 利用Python打印有符号十进制数的二进制原码、反码、补码
  • std::print 和 std::println
  • 万亿参数大模型网络瓶颈突破:突破90%网络利用率的技术实践
  • 【力扣刷题实战】丢失的数字
  • Java大师成长计划之第6天:Java流式API(Stream API)
  • Redis 小记
  • Cursor + Figma-Context-MCP ,让 Cursor 获取 Figma 设计图信息,实现 AI 生成页面的高度还原
  • 【3分钟准备前端面试】Hybrid开发 谷歌浏览器调试安卓app
  • ViTa-Zero:零样本视觉触觉目标 6D 姿态估计
  • 深入解析 Babylon.js 中的 TransformNode.lookAt 方法
  • 【Unity】 Dropdown默认选择不选择任何选项
  • “杭州六小龙”的招聘迷局
  • 光明网评论员:手机“二次放号”,需要重新确认“你是你”
  • 外交部:印度香客赴中国西藏神山圣湖朝圣将于今年夏季恢复
  • 中消协发布“五一”消费提示:践行“光盘行动”,抵制餐饮浪费
  • 跨海论汉|专访白馥兰:对中国农业史的兴趣,从翻译《齐民要术》开始
  • 央行副行长:增强外汇市场韧性,坚决对市场顺周期行为进行纠偏