Android学习总结之Java篇(一)

泛型擦除

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

问题 1:什么是泛型擦除?它在 Java 中的实现原理是什么?
回答核心

泛型擦除是 Java 泛型的底层机制,指编译器在编译时会擦除泛型的具体类型信息,将泛型参数替换为其上限类型(通常是Object),仅在编译期保留类型检查,运行时类型信息丢失。

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

示例

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

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

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

    • 解决方案 :使用TypeToken(如 Gson)或子类化保留类型信息。

      java 复制代码
      // 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. 方法重载冲突:子类无法通过泛型参数重载父类方法。

    • 错误示例

      java 复制代码
      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利用匿名内部类保留泛型类型信息,通过反射获取实际类型。

  • 示例

    java 复制代码
    // 解析嵌套泛型类型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函数)实化泛型,在运行时保留类型信息。

  • 示例

    java 复制代码
    inline fun <reified T> fetchData(): T {
        val type = T::class.java
        // 使用反射或网络请求获取数据
        return data as T
    }
    // 调用时直接获取具体类型
    val result = fetchData<Result>()
  • 原理inline函数在编译时将函数体替换到调用处,reified确保泛型类型被保留。

四、反射与泛型擦除的深度交互(难点)

问题 5:在 Android 中,如何通过反射获取泛型字段的实际类型?
回答核心

通过ParameterizedType接口解析泛型信息。

  • 示例

    java 复制代码
    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 不支持泛型数组?
回答

泛型数组在运行时无法保留类型信息,可能导致内存安全问题。例如:

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),通过计数器记录重入次数,避免死锁。

    java 复制代码
    public synchronized void method1() {
        method2(); // 可重入,无需再次竞争锁
    }
    public synchronized void method2() {}
  • 不可重入锁 :未实现重入逻辑(如早期 Java 版本的 synchronized 非显式实现,现几乎不用)。

CAS 的缺点及解决办法

1. ABA 问题
  • 问题描述:CAS 操作在比较和交换时,会检查变量的值是否与预期值相同。如果一个变量的值从 A 变为 B,再从 B 变回 A,CAS 操作会认为变量的值没有发生变化,从而继续执行更新操作,但实际上变量的值已经发生了变化,这可能会导致一些意外的结果。
  • 解决办法 :使用带有版本号的原子引用类 AtomicStampedReferenceAtomicMarkableReferenceAtomicStampedReference 会在更新值的同时更新版本号,每次更新时会检查值和版本号是否都与预期值相同;AtomicMarkableReference 则是使用一个布尔值来标记变量是否被修改过。
java 复制代码
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 操作。另外,也可以使用锁机制来保证多个共享变量的原子性。
java 复制代码
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);
    }
}
相关推荐
Allan_20256 小时前
数据库学习
数据库·学习
报错小能手6 小时前
linux学习笔记(43)网络编程——HTTPS (补充)
linux·网络·学习
报错小能手6 小时前
linux学习笔记(45)git详解
linux·笔记·学习
pengzhuofan6 小时前
第10章 Maven
java·maven
百锦再7 小时前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
刘一说7 小时前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
Larry_Yanan7 小时前
QML学习笔记(四十四)QML与C++交互:对QML对象设置objectName
开发语言·c++·笔记·qt·学习·ui·交互
壹佰大多7 小时前
【spring如何扫描一个路径下被注解修饰的类】
java·后端·spring
百锦再7 小时前
对前后端分离与前后端不分离(通常指服务端渲染)的架构进行全方位的对比分析
java·开发语言·python·架构·eclipse·php·maven
2501_915918417 小时前
iOS 混淆实战 多工具组合完成 IPA 混淆、加固与工程化落地(iOS混淆|IPA加固|无源码混淆|Ipa Guard|Swift Shield)
android·ios·小程序·https·uni-app·iphone·webview