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);
    }
}
相关推荐
ansondroider3 分钟前
Android adb 安装应用失败(安装次数限制)
android·adb·install
李少兄9 分钟前
解决Spring Boot多模块自动配置失效问题
java·spring boot·后端
bxlj_jcj37 分钟前
JVM性能优化之年轻代参数设置
java·性能优化
八股文领域大手子37 分钟前
深入理解缓存淘汰策略:LRU 与 LFU 算法详解及 Java 实现
java·数据库·算法·缓存·mybatis·哈希算法
不当菜虚困1 小时前
JAVA设计模式——(八)单例模式
java·单例模式·设计模式
m0_740154671 小时前
Maven概述
java·maven
FAREWELL000751 小时前
C#进阶学习(十六)C#中的迭代器
开发语言·学习·c#·迭代器模式·迭代器
吗喽对你问好1 小时前
Java位运算符大全
java·开发语言·位运算
艾小逗1 小时前
uniapp中检查版本,提示升级app,安卓下载apk,ios跳转应用商店
android·ios·uni-app·app升级
Java致死1 小时前
工厂设计模式
java·设计模式·简单工厂模式·工厂方法模式·抽象工厂模式