ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

美团·初级 Android 面试——面向对象三大特性人人会背

2026/9/1 5:38:06 拓冰建站 浏览量
美团·初级 Android 面试——面向对象三大特性人人会背 坐在美团的面试间里面试官把简历往桌上一推开口第一句就不是介绍一下项目而是面向对象三大特性说说看。你心里一松。这题小学就背过。封装、继承、多态嘴里顺溜地往外蹦。然后面试官笑了笑封装在 JVM 层面到底封了什么多态的时候字段会不会跟着一起变空气突然安静。说白了背概念谁都会大厂面试官要的从来不是定义。他要的是——你知不知道这些特性在虚拟机里是怎么落地的。今天这篇我就把这场对话原样复盘一遍帮你把会背变成真懂。一、封装不是加个 private 就完事面试官先说封装吧你觉得它解决什么问题候选人标准答法隐藏内部实现细节只暴露必要的接口降低耦合。这话没错但太薄。深度解析封装的本质是边界控制。Java 里靠访问修饰符划边界private只在类内可见protected跨包子类可见public全局。举个 Android 里的真例子。我们写ViewModel持有 UI 状态如果把MutableLiveData直接public暴露出去外部谁都能setValue状态就失控了。正确做法是只暴露不可变的LiveDataclass HomeViewModel : ViewModel() { // 内部可变外部拿不到这个引用 private val _count MutableLiveData(0) // 对外只给只读视图调用方无法篡改 val count: LiveDataInt get() _count fun inc() { _count.value (_count.value ?: 0) 1 } }普通答法 vs 高分答法普通答法封装就是 private 字段 getter/setter保护数据安全。高分答法封装保护的是业务不变式invariant。getter/setter 不是必选项关键是不让外部破坏对象的一致性。我会补一句Kotlin 的val/var配合private set比 Java 的 getter 更克制比如var token: String private set外部能读不能改这才是现代封装。二、继承单继承的代价与方法重写面试官继承呢Java 为什么只支持单继承候选人避免多继承的歧义比如两个父类有同名方法子类不知道调哪个。深度解析这就引到了核心问答——重载overload和重写override分别在编译期还是运行期确定先说结论重载是编译期静态绑定重写是运行期动态分派。重载看的是编译时的方法签名方法名 参数类型/个数/顺序跟对象实际类型无关。重写看的是运行时的对象实际类型靠 JVM 的方法表vtable查。open class Animal { open fun sound() ... // 可被重写 fun eat(food: String) 吃$food // 重载基准 fun eat(food: Int) 吃$food份 // 重载参数类型不同编译期定 } class Dog : Animal() { override fun sound() 汪汪 // 重写运行期动态分派 }JVM 层面Dog会有一张方法表里面sound的槽位指向Dog.sound的实现。父类引用val a: Animal Dog()调a.sound()字节码是invokevirtual运行时查a的实际类型Dog的方法表——所以输出汪汪。普通答法 vs 高分答法普通重写是子类改父类方法运行时生效。高分我会点出invokevirtual的动态分派机制以及静态类型决定能调哪些方法实际类型决定调到哪。顺带提一句私有方法、final 方法、构造器是静态绑定invokespecial/invokestatic不走 vtable所以构造器里调可重写方法是个经典坑——此时子类字段还没初始化。三、多态JVM 层到底怎么实现的面试官那你说的多态虚拟机层面到底干了啥这是本篇第一道核心问答。深度解析多态 父类引用指向子类对象 运行期动态分派。JVM 的实现靠两样东西方法表vtable和invokevirtual 指令。每个类在加载时方法区会存一张方法表记录该类所有虚方法的入口地址。子类的方法表继承父类布局被重写的槽位地址覆盖成子类实现新增方法追加在后面。调用invokevirtual时1. 从操作数栈弹出对象引用obj2. 查obj实际类型的方法表3. 按编译期解析出的方法符号引用定位槽位4. 跳到该槽位的实际地址执行。普通答法 vs 高分答法普通多态就是父类引用调子类方法。高分我会强调只有方法有多态字段没有。这正是面试官最爱追问的下一句——四、实战追问多态场景下字段会动态绑定吗面试官那如果父类有个字段name子类也定义了namea.name取的是哪个答案很反直觉取父类的。字段不参与多态它是静态绑定由编译期的静态类型决定。open class Base { val tag base } class Sub : Base() { val tag sub } // 隐藏而非重写父类字段 fun main() { val s: Base Sub() println(s.tag) // 输出 base字段看静态类型 Base }字节码里字段访问是getfield直接按声明的字段偏移取根本不查 vtable。所以子类那个tag只是字段隐藏shadowing不是重写多态对它无效。工程实战这坑在项目里真实出现过。我们有一个基类BaseRequest有val timeout 30子类想改成 60结果网络层用父类引用调用永远走 30请求频繁超时。改成open valoverride才解决。高分答法补充Kotlin 的open val重写在字节码层面其实是生成了getter方法getTag()走的是方法分派所以能多态而直接访问val字段在 Kotlin 里其实也是调 getter但上面那个子类重新声明 val没加override时父类的 getter 没被覆盖所以仍取父类值。这点讲清楚面试官基本就点头了。五、开放追问接口默认方法与菱形问题面试官Java 8 加了 default method如果俩接口都有同名默认方法类同时实现俩调哪个候选人编译报错深度解析不会编译报错但必须显式重写来消除歧义否则编译不通过——这正是 Java 用单继承 接口规避菱形继承后default method 重新引入的小尾巴。interface A { fun foo() println(A) } interface B { fun foo() println(B) } class C : A, B { // 不重写 foo 直接报错 override fun foo() { // 必须显式指定 A.super.foo() // 用 super 限定选 A 的实现 } }规则优先级类自己的实现 接口中显式重写的 父接口的 default。如果没类实现也没重写编译器无法决定选 A 还是 B强制你重写。高分答法我会把话题拉回设计层面——default method 的初衷是给已有接口如List无痛加forEach、stream而不破坏旧实现但它破坏了接口无实现的纯粹性。我的看法是业务代码里尽量别用 default method 塞逻辑它让接口的契约变模糊。需要默认行为就用抽象类需要多继承能力才上接口。收尾我的几点实战建议写到这里回头看那道人人会背的题。封装、继承、多态从来不是三个孤立名词它们在 JVM 里是一整套连贯机制访问修饰符划边界、vtable 支撑动态分派、字段静态绑定是例外。几个面试 Tips具体到怎么答被问三大特性时别急着背定义先抛一句它们在 JVM 层是这么落地的直接把对话拉到源码层区分度立刻出来。重写 vs 重载务必点明编译期 vs 运行期这是硬分界。主动提字段不多态、只有方法多态这反常识点最容易让面试官眼前一亮。说到继承顺带带一句构造器里别调可重写方法展示你踩过坑。评论区聊聊你在面试里被面向对象追问到最尴尬的一句是啥我评论区翻牌。---点赞、在看、转发三连是对我最大的支持「Android 大厂面经·从入门到精通」连载系列上一篇暂无系列开篇下一篇预告第002篇 字节跳动·应届 Android 面试——String 为什么设计成不可变评论区聊聊你面试遇到过最难的 Android 问题是哪道关于本系列「Android 大厂面经·从入门到精通」是360篇连载系列覆盖美团、字节跳动、阿里巴巴、快手、百度、京东、华为、小米等30家公司从 Java 基础到架构师终面的真实面试内容。本篇属于阶段1·入门篇——Java 语言基础。