Android面试指南(八)
目录
1、Java语言相关
1.1、String的intern方法
1.2、HashMap的扩容
1.3、Java数组不支持泛型
1.4、泛型类型保留到运行时
1.5、匿名内部类使用的外部变量需要加final
2、Kotlin语言相关
3、设计模式
1、Java语言相关
1.1、String的intern方法
1)、String的存储机制
String内存模型的核心特点包括:
- 不可变性:String实例一旦创建不可修改
- 双重存储机制:实例可能存储在堆或字符串常量池
- intern方法作用:将字符串显式存入字符串池中
String类的特殊性体现在:
- 不可变性:与基本数据类型封装类相同
- 声明语法特殊:唯一可通过字面量创建的引用类型
- 存储位置特殊:实例可能存在于字符串常量池而非堆
①、String不可变性的实现机制:
- final类声明:禁止继承
- private final char[]存储数据:外部无法修改
- 方法设计原则:比如substring等操作均返回新实例
②、String实例不可变的表现:
- 任何修改操作(如拼接、截取)均生成新实例
- 原实例内容始终保持不变
- 必须通过改变引用指向新实例实现"修改"效果
2)、字符串常量池
- 字符串池与常量池的区别:常量池存在于每个class文件的逻辑结构中,存储以字面量方式声明的字符串;运行时常量池是class文件常量池加载到内存后的形态。
- 字符串池功能:全局存储字符串(非每个类独立),HotSpot虚拟机通过哈希表实现。
- 关联性:常量池中的string类型常量通过查询字符串池解析,二者通过引用保持一致性,导致概念易混淆。
字符串常量池的优化原理:
- 相同字面量的多个String实例可共享
- intern方法实现实例复用
- 哈希值相同且equals为true的实例可合并
内存优化示例:字面量均为"ABC"的name1和name2引用可通过intern指向同一实例。
3)、总结
string的不可变性使字符串池复用成为可能,JDK1.7+的字符串池存储堆引用,相关面试题多围绕内存模型展开,需理解存储机制。
- JDK 1.7以下(不包括1.7),字符串池在永久代,intern方法的含义是:
- 如果字符串池中存在与当前字符串内容相同的缓存,则返回缓存实例;
- 否则,在字符串池中创建相应实例,并返回。
- JDK1.7及以上,字符串池在堆中,字符串池中,存储的是字符串堆引用,intern方法的含义是:
- 如果字符串池中存在与当前字符串内容相同的缓存,则返回缓存的引用;
- 否则,在字符串池中存储当前字符串的引用。
1.2、HashMap的扩容
HashMap容量选择二的N次方的原因如下:
- 计算数组下标时可通过位运算替代取余操作,从而提升运算性能
- 扩容时无需重新计算所有节点位置,仅需检查新参与运算的最高位即可
1)哈希算法
散列算法将任意长度输入转换为固定长度输出,要求相同输入必产生相同输出。哈希碰撞指不同输入产生相同输出的情况。理想哈希算法应最大限度避免碰撞,简单取余算法因碰撞概率高不适用于高性能场景。
哈希碰撞:哈希表中元素位置通过哈希码对容量取余确定。碰撞发生时元素会被放入相同桶中,该机制是哈希表定位元素的基础算法。
2)HashMap
HashMap主体结构是初始长度16的数组,数组存储Node类型元素。传统桶结构采用链表实现,JDK1.8新增红黑树实现(TreeNode为Node子类)。数组与红黑树会根据元素数量动态转换,红黑树在数据量大时具有性能优势。
①、put方法
put方法实际调用putVal实现存储,过程中会通过hash()方法重新计算键的哈希码。
②、putVal方法
putVal方法执行流程如下:
- 检测数组是否为空,空则触发resize扩容
- 计算元素数组下标(非直接取余)
- 下标为空时新建节点插入
- 下标非空时判断键是否相等,相等则覆盖值
- 不相等时根据节点类型(链表/红黑树)遍历查找
- 链表插入后检查是否需转为红黑树
- 最终检查是否触发扩容
计算元素在数组中的下标:下标计算采用(n-1)&hash替代取余操作,二者数学等价但位运算效率更高。以容量16为例:
- 16-1=15(二进制1111)
- 与哈希码按位与操作可保留后四位
- 该优化的前提是容量必须为2的n次幂,此为设计原因之一。
③、hash方法
hash()方法通过扰动函数解决低位碰撞问题:
- 将哈希码无符号右移16位使高位参与运算
- 与原值异或融合高位特征到低位
- JDK 1.8的扰动逻辑较1.7简化但原理一致,核心目标是提升低位随机性。
④、resize方法
扩容机制特性分析:
维度 | JDK 1.7前 | JDK 1.8优化 |
容量变化 | 翻倍为2^n | 保持2^n特性 |
重定位方式 | 全量重新计算下标 | 仅检查新增最高位 |
元素移动 | 全部迁移 | 原位置或原位置+旧容量 |
优化依据:扩容后元素新位置仅取决于新增运算位(0保持原位,1偏移旧容量值)。此特性同样依赖2^n容量设计。 |
1.3、Java数组不支持泛型
Java数组不支持泛型的说法需明确两点:泛型仅在编译期提供有限类型安全保证,且实际限制在于无法创建泛型类型数组。核心原因包括:
- 类型擦除机制使运行时泛型信息丢失
- 数组协变性与泛型设计理念冲突
- Java泛型实现受历史兼容性制约
Java数组不支持泛型的原因在于数组与泛型在设计理念上存在根本性冲突。数组是JVM底层实现的特殊实例类型,自Java早期版本就已存在;而泛型在Java 1.5版本才引入。两者核心矛盾在于:数组具有协变性(父类型数组引用可指向子类型数组实例),而泛型默认具有不变性(泛型类之间无继承关系)。这种设计差异导致无法通过编译期类型安全检查创建泛型数组实例。
1)、Java泛型的实现方式
类型擦除机制特点:
- 泛型类型参数仅在编译期有效,字节码中替换为原始类型
- 生成桥接方法保证多态性
- 历史原因:保持与Java1.5前版本的二进制兼容性
2)、泛型的类型擦除机制
泛型类型擦除机制实现方式可通过反编译字节码观察。new指令及构造方法调用均不包含泛型信息,创建的ArrayList实际为Object类型。绕过编译器报错可将任意类型元素放入ArrayList,此现象解释了字符串元素存入非泛型集合的可能性。
类型擦除:类定义的泛型经类型擦除后固化为Object。子类继承父类时给定的泛型参数需具体化,例如实现change方法时参数与返回值被具体化为Apple类型。由于父类方法签名中为Object,编译器自动生成桥接方法以维持重写关系。类型擦除导致运行时无法创建泛型数组,且泛型无法在运行时保证内存安全。总结:
- 字节码创建数组需明确类型
- 类型擦除使泛型无法延续至运行时
- Java泛型无法实现运行时类型安全检查
3)、Kotlin中的数组
Kotlin数组支持泛型通过两种实现方式:
数组类型 | 实现特性 | 对应字节码指令 | Java等效类型 |
类集合语法数组 | 泛型实现类似List,不具备协变性 | anewarray | 包装类类型数组 |
基本类型数组 | 兼容Java基本数据类型 | newarray | 基本数据类型数组 |
Kotlin通过IntArray等类封装基本类型数组,反编译显示其字节码同样不包含泛型信息,证实Kotlin泛型基于类型擦除机制实现。
4)、Kotlin为何支持创建泛型数组
Kotlin虽采用类型擦除机制,但通过强制泛型类型声明消除安全隐患。由于无需兼容历史字节码或源码,集合与数组API设计均要求显式指定泛型类型,此设计规避了Java中存在的类型安全问题。
1.4、泛型类型保留到运行时
1)、使用Class<T>
<T> T create(Class<T> kind) {
return new kind.newInstance();
} // TODO catch
通过Class<T>参数传递类型信息。该方案特点:
- 支持反射创建实例
- 局限性:
- 反射效率低
- 无法处理嵌套泛型(如List<String>.class非法)
2)、通过子类把泛型类型具体化(reified)
public class AppleBasket extends Basket<Apple>{
... // 这里Apple具体化
}
public class GenericsTypeToken<T> {
public Type type = null;
public GenericsTypeToken() {
type =
((ParameterizedType)this.getClass().getGenericSuperclass())
.getActualTypeArguments()[0];
}
}
new GenericsTypeToken<Basket<Apple>>() {}.type;
字节码签名保留泛型信息。实现步骤:
- 继承具体化泛型类
- 通过getGenericSuperclass()获取签名属性 - 解析Type接口获取实际类型
- Gson等库采用类似TypeToken机制实现泛型反序列化。
Type type = new TypeToken<List<User>>() {}.getType();
List<User> list = new Gson().fromJson(userJson, userListType);
1.5、匿名内部类使用的外部变量需要加final
- 匿名内部类, 持有外部类的引用;
- 以编译器自动生成的成员变量的形式持有;
- 通过编译器自动生成的构造方法传入。
- 匿名内部类, 通过这个引用访问外部类的成员变量和方法。
- 匿名内部类, 访问外部局部变量时, 其实是访问自身的一个成员变量;
- 这个成员变量, 是编译器自动生成的(val$变量);
- 这个成员变量, 由编译器自动生成的构造方法初始化;
- 为了保证这个成员变量和外部局部变量时刻保持一致性, 二者必须都是final的。
2、Kotlin语言相关
推荐阅读:一文带你快速掌握Kotlin核心技能
3、设计模式
设计模式:是前人通过试错总结的最佳实践,用于解决特定问题(如提高代码复用性、可读性、健壮性)。那我们遇到过哪些设计模式的使用案例呢?
①、模板方法模式
- Android Activity生命周期:开发者通过重写onCreate、onResume等方法响应状态变更,无法干预状态流转逻辑。
- 核心要点:
- 父类定义算法框架,子类实现细节。
- 决策权在高层模块(如父类控制调用时机)。
- 与策略模式区别:模板方法通过继承封装算法,策略模式通过组合封装。
②、责任链模式
责任链模式通过链式节点处理请求,经典案例有:
- Android View事件分发:事件沿视图层级传递直至被消费。
- OkHttp拦截器:请求通过拦截器链逐层处理。
责任链模式适用于多个处理者具备处理相同事件能力且需筛选最符合要求或优先级最高的场景。核心特征包括:
- 职责差异化:责任链各节点具备不同职责
- 顺序依赖性:节点间存在明确先后顺序
- 扩展优势:通过接口实现新责任并加入责任链即可完成功能扩展
- 动态调整:支持运行时修改责任链节点构成
③、装饰者模式
装饰者模式的核心在于动态扩展类的能力,通过组合而非继承实现功能扩展。
安卓平台的ContextWrapper类采用装饰者模式。通过包装ContextImpl实现动态功能扩展,避免直接继承带来的局限性。
④、代理模式
代理模式与装饰者模式结构相似,均实现接口并持有接口引用。代理模式通过替身控制对象访问,代理模式的要点包括远程对象代理和虚拟代理。与装饰者模式目的不同,前者控制访问,后者扩展行为。
典型场景:安卓框架层通过Binder跨进程实现代理模式。ActivityManager通过IActivityManager接口代理跨进程访问。
⑤、Android中的其它设计模式
安卓平台常见设计模式包括:
- 观察者模式:广播、Adapter的notifyDataSetChanged
- 适配器模式:ListView的Adapter
- 构建者模式:AlertDialog.Builder
- 备忘模式:Activity的onSaveInstanceState
- 迭代器模式:Cursor遍历
- 原型模式:Intent的clone方法
- 享元模式:线程池、连接池
- 命令模式:Handler消息循环
OK,今天的内容就这么多了,下期再会!