首页 / 资讯中心 / 文章详情

Java面向对象三大特性:封装、继承、多态的协同工作与实战解析

Java面向对象三大特性:封装、继承、多态的协同工作与实战解析 ★ FEATURED ARTICLE
写Java写了这些年面试过不少人也带过不少新人我越来越确定一件事面试时问封装、继承、多态是什么几乎人人都能背出概念但只要往深里追问一句这三个特性是怎么协同工作的大部分人就卡住了。JavaSE的面向对象三大特性表面上是你入门Java的第一道坎实际上是你从会写代码到会设计代码的分水岭。这篇文章我不会跟你念教科书我想用踩坑换来的经验把封装、继承、多态这件事拆开揉碎了讲清楚——它们各自解决什么问题、为什么这么设计、实际开发中怎么用才不出事。无论是正在学JavaSE的初学者还是准备面试的求职者或者是写了两三年代码但总觉得基础不牢的朋友这篇都能帮你把地基重新夯实一遍。1. 面向对象从怎么做到谁来做的思维转变1.1 为什么Java强推面向对象我刚学Java的时候脑子里全是C语言的印记。C语言写程序思路是一条线先定义数据结构然后写函数去操作这些数据程序就是一个函数调用另一个函数数据在整个程序里游走。这就是典型的面向过程思维——它关心的是每一步该怎么做。面向对象则完全不同它关心的核心是谁来做这件事。Java把世界抽象成一个个对象每个对象既拥有数据属性也拥有行为方法对象与对象之间通过调用方法互相协作。这种思维方式更贴近人类的认知习惯你不需要理解汽车内部是怎么运转的你只需要知道踩油门这个动作能让车加速。Java从语言层面强制要求你用类class来组织代码所有代码必须写在类里这不是故意为难你而是想从一开始就逼你建立起万物皆对象的思维框架。三大特性——封装、继承、多态——正是在这个框架下回答面向对象编程中最核心的三个问题封装回答一个对象应该暴露什么、隐藏什么继承回答对象之间如何共享代码、建立层次关系多态回答同一个行为在不同的对象上如何呈现出不同表现这三个问题想明白了你写的代码就会有一种设计过的感觉想不明白你只是在用Java语法写C语言的代码。1.2 类与对象三大特性的载体类与对象的关系我上课时最爱用的类比是月饼模具和月饼。类是模具它规定了月饼的形状、大小、花纹对象是用模具做出来的实体月饼每个月饼都可以有自己不同的馅料。在Java里类是抽象的模板对象是具体的存在。public class Dog { private String name; private int age; public void bark() { System.out.println(name 在叫); } }这段代码声明了一个Dog类它有两个属性name和age有一个行为bark。但在这个阶段它只是模具内存里还没有任何一条狗存在。想要真正使用它必须通过new关键字创建对象Dog dog1 new Dog(); dog1.name 旺财; // 等等这里其实编译不过去因为name是private注意上面这行代码我故意留了一个编译错误。在JavaSE的学习阶段很多初学者就是在这里开始迷糊的为什么明明Dog类里有name这个属性我却不能直接用dog1.name赋值其实答案就在下一个章节里——这就是封装的第一课。1.3 面向对象与面向过程的实战对比很多初学者觉得面向对象是一个哲学概念离代码很远。我换个实际案例你就明白了。假设你要实现一个功能计算一个矩形的面积和周长。面向过程的写法public class RectangleAreaCalculator { public static void main(String[] args) { double width 10; double height 5; double area width * height; double perimeter 2 * (width height); System.out.println(面积 area); System.out.println(周长 perimeter); } }这种写法的问题在于width和height是散落在main方法里的两个裸变量如果程序里还有别的地方也需要用到这个矩形的数据你就得把这些变量传来传去或者干脆复制一份。数据一旦被复制就可能出现两个地方修改了同一个东西但彼此不知道的灾难。面向对象的写法public class Rectangle { private double width; private double height; public Rectangle(double width, double height) { this.width width; this.height height; } public double getArea() { return width * height; } public double getPerimeter() { return 2 * (width height); } }宽和高被封装进了Rectangle对象里外部代码不需要关心面积是怎么算的只需要调用getArea()方法。以后如果计算逻辑变了比如改成用海伦公式算三角形只改Rectangle类内部就行外部代码一行都不用动。这个例子虽然简单但它已经暴露出了面向对象的核心价值数据与操作数据的逻辑被绑定在一起形成了一个内聚的单元。这就是封装的雏形也是理解另外两大特性的基础。2. 封装把细节锁起来只留一扇门2.1 封装的本质访问修饰符与可见性封装这个词听起来高深说白了就是一句话该藏的藏该露的露。Java通过访问修饰符来控制类、方法、属性的可见性常用的有四个级别我从最开放到最封闭给你列一张表修饰符同类同包子类不同包任意类public是是是是protected是是是否默认不写是是否否private是否否否这张表基本是JavaSE面试必考的内容但我见过很多人死记硬背还是记混。我给你一个记忆锚点private是我的就是我的默认是同包的老铁可以碰protected是同包的老铁和我的亲儿子可以碰public是谁都能碰。实际开发中属性的默认选择是private方法的默认选择是public。为什么因为属性描述的是对象内部状态状态如果随便被外部改动整个对象就可能处于不一致的状态。方法则是对象提供的服务能力服务就是拿来给别人用的所以默认public。2.2 private只是开始getter/setter背后的设计哲学很多从培训班出来的同学写类的时候有一套固定动作属性全声明成private然后用IDE自动生成getter/setter。这么做没有错但你真的理解为什么要加这层壳吗我先说结论getter/setter不是让你拿来直通属性的通道而是让你在属性被访问或者属性被修改这个时刻插入自己逻辑的钩子。举个真实的例子。我有一个用户类其中age是年龄属性。如果不加任何控制外部代码可以直接写user.age -30;一个-30岁的人就诞生了。但如果你用setter接收赋值就可以在入口处做校验public class User { private int age; public int getAge() { return age; } public void setAge(int age) { if (age 0 || age 150) { throw new IllegalArgumentException(年龄不合法 age); } this.age age; } }这样一来所有外部代码想修改年龄都必须在你的规则允许范围内否则直接抛异常。这就是封装的核心价值数据不再裸露而是通过受控的接口被访问。但我要泼一盆冷水很多人的getter/setter只是一种形式上的封装属性本身是private但setter里什么都不做相当于只挂了一把没锁上的锁。等你写了几年代码再回头看你会发现真正高水平的封装不是给每个字段都配上getter/setter而是只暴露其他对象真正需要的方法内部细节能藏多少藏多少。2.3 包与访问权限项目级封装的实战布局封装不止作用于类内部还作用于包package级别。Java的包机制用于组织类避免命名冲突同时配合访问修饰符形成不同层级的防护。我在实际带项目时比较推荐的分层封装风格是controller层接收外部请求参数校验调用serviceservice层业务逻辑处理事务控制调用daodao层数据库交互SQL执行domain/model层实体对象、值对象在这种分层架构里你会频繁用public来暴露接口用private来隐藏实现细节。跨层访问必须通过公开方法禁止直接穿透访问内部数据否则封装就被破坏了。这里有一个设计细节在JavaSE阶段你不需要过度设计但至少要养成一个习惯——写类之前先想清楚这个类的哪些东西是外部需要知道的哪些是内部处理细节。外部不需要知道的东西一律private。等以后用Spring写企业级项目的时候你会发现这个习惯能帮你避免无数个改一个字段全项目编译报错的悲惨时刻。2.4 封装实战一个不可破坏的银行账户类讲完理论我给你一个可以完整跑起来的例子。银行账户可以说是封装的最佳教学场景——账上的钱不能随意被改变对外只能通过存款、取款这些受控方法来操作。public class BankAccount { private String accountNo; private double balance; public BankAccount(String accountNo, double initialBalance) { this.accountNo accountNo; // 构造器里也要校验否则可以直接 new 一个负数余额的账户 if (initialBalance 0) { throw new IllegalArgumentException(初始余额不能为负数); } this.balance initialBalance; } public double getBalance() { return balance; } public void deposit(double amount) { if (amount 0) { throw new IllegalArgumentException(存款金额必须为正数); } balance amount; } public void withdraw(double amount) { if (amount 0) { throw new IllegalArgumentException(取款金额必须为正数); } if (amount balance) { throw new IllegalArgumentException(余额不足当前余额 balance); } balance - amount; } }注意几个细节balance字段没有setter因为直接设置余额这个动作本身就不该存在——余额只能通过存款和取款来变化。getBalance是必要的因为用户需要查询余额。构造器里也做了校验否则new BankAccount(001, -100)就能创建一个状态非法的对象。封装到这里才算是真的到位了从对象的出生构造到使用方法调用所有改变内部状态的路径都经过了校验。外部代码无论怎么折腾都不可能把账户余额搞成负数。这就是不可破坏的类的含义。3. 继承站在父类的肩膀上而不是复制粘贴3.1 继承到底解决什么问题继承解决的最核心问题是代码复用和建立类型层次关系。比如你写了三个类Student学生、Teacher教师、Cleaner保洁这三个类有一些共同属性——姓名、年龄、工号/学号、入职/入学时间。如果没有继承你只能复制粘贴三份相同的字段和方法以后想加一个共同属性比如身份证号就得改三个类漏改一个就出BUG。有了继承你可以抽取一个Person基类public class Person { private String name; private int age; private String id; public void eat() { System.out.println(name 在吃饭); } public void sleep() { System.out.println(name 在睡觉); } }然后让Student、Teacher、Cleaner都继承Personpublic class Student extends Person { private String studentNo; public void study() { System.out.println(getName() 在学习); } }这样Student就自动拥有了name、age、id字段和eat、sleep方法自己只需要扩展学生专属的studentNo和study。这就是站在父类的肩膀上。3.2 super关键字与构造器调用链继承中最容易踩坑的地方不是怎么写extends而是子类构造器与父类构造器之间的调用关系。Java有一个硬性规则子类构造器的第一行必须调用父类的某个构造器。如果你不写编译器会自动帮你加上super()调用父类的无参构造器。如果父类没有无参构造器编译就会报错。刚才的Person类没写任何构造器所以编译器默认给它生成了一个无参构造器Student继承它没有任何问题。但如果Person有一个带参构造器情况就变了public class Person { private String name; public Person(String name) { this.name name; } }此时编译器不会再生成无参构造器而Student的构造器里默认调用super()就会找不到无参构造器直接编译报错。解决办法是在子类构造器里显式调用父类的带参构造器public class Student extends Person { private String studentNo; public Student(String name, String studentNo) { super(name); // 必须写在第一行 this.studentNo studentNo; } }这里有一个很多初学者会犯的错误在调用super()之前访问了子类的实例变量。Java强制super()必须是第一行就是为了保证父类先初始化完毕。你想一下子类可能重写了父类的方法如果父类构造器在执行过程中调用了某个被子类重写的方法而子类的实例变量还没初始化那就会拿到默认值甚至null。所以规则很明确先父后子先super后this。3.3 方法重写Override的规则与陷阱方法重写Override是继承的高级玩法子类对父类的方法重新实现。JavaSE阶段最典型的例子是重写toString方法public class Student extends Person { private String studentNo; Override public String toString() { return Student{name getName() , studentNo studentNo }; } }重写有严格的规则面试几乎必问方法名、参数列表必须完全一致返回值类型可以相同也可以是父类返回类型的子类型协变返回类型访问修饰符不能比父类更严格比如父类是public子类不能改成protected不能抛出比父类更宽泛的受检异常我见过太多的开发事故就是以为在重写实际上在重载方法名写错一个字母、参数类型写错、忘加Override注解。所以我的建议非常强硬凡是意图重写父类方法的地方一律加上Override注解。这个注解的作用不是锦上添花而是让编译器帮你检查如果你其实没有重写任何父类方法编译器会直接报错提示你。这一行注解能帮你拦下大量低级错误。3.4 继承体系设计的三问法继承不是用得越多越好。我在评审代码时最怕看到那种七八层深的继承链类A继承类BB继承CC继承D……一旦业务变更你根本不知道该改哪一层。我自己在决定要不要用继承时会问三个问题子类是不是父类的一种特殊形态is-a关系——Student是一个Person所以Student继承Person是合理的。但Student是一个Person和Student有一个BestFriend是两回事后者应该用组合。父类的所有方法对子类是否都有意义如果父类有一个飞行方法fly()但继承它的Penguin企鹅类根本不会飞那这个继承设计就是失败的。子类是否会复用父类的大部分实现如果子类为了用父类的两个方法不得不把其他十个方法都强行继承过来就应该考虑把公共方法抽到更小的接口里。这三个问题只要有一个回答是否定的我就会谨慎使用继承。尤其是第三个问题很多刚入门的朋友特别喜欢为了复用而继承结果父类越来越臃肿子类被一堆用不上的方法绑架这就是典型的继承滥用。3.5 组合优于继承什么时候该放弃继承有一个著名的设计原则叫组合优于继承Composition over Inheritance意思是能用组合has-a实现的复用尽量别用继承is-a实现。我举个例子说明两者的区别。假设你要给一个Dog类加一个会游泳的能力。用继承的思路创建一个SwimmingDog类继承Dog重写相关方法。但如果还要一个会看家的Dog怎么办SwimmingDog继承Dog后还得再写个GuardingDog吗Java不支持多继承你就陷入了类爆炸的困境。用组合的思路把能力抽取为接口或独立的类然后在Dog类里持有它们的实例。public interface Swimmable { void swim(); } public class Dog implements Swimmable { private String name; Override public void swim() { System.out.println(name 在狗刨); } }如果想让Dog既会游泳又会看家就让Dog同时实现Swimmable和Guardable两个接口。接口本质上是一种能力契约比继承更灵活。我个人的准则是当子类和父类的关系说不清是是一个还是有一个的时候优先选组合。继承关系一旦建立就会形成永久性的代码耦合父类改动一个受保护字段所有子类都可能受影响。而组合只依赖公开接口耦合面小得多。4. 多态同样的一句调用不一样的表现4.1 多态的三大条件多态Polymorphism是三大特性中最抽象、也最体现面向对象威力的一个。它的意思是同一个类型的引用指向不同的对象时调用同一个方法会表现出不同的行为。Java中实现多态有三个必要条件缺一不可继承或接口实现方法重写父类引用指向子类对象Person p new Student(张三, S001); p.eat(); // 如果Student重写了eat方法这里会调用Student的版本这里的p是Person类型的引用但它指向的是Student对象。调用p.eat()时Java运行的不是Person.eat()而是Student重写后的eat()。这个运行期才知道到底调用谁的特性叫做动态绑定。4.2 向上转型与动态绑定向上转型Upcasting就是把子类对象赋值给父类引用。为什么叫向上因为类继承关系通常画成一棵从上到下的树父类在上子类在下。Person p new Student(张三, S001); // 向上转型这种写法有一个重要的限制p只能调用Person类中声明过的方法不能调用Student特有的方法比如study()。原因是编译器只看引用类型Person它只知道p是一个Person至于这个Person实际指向的是不是Student编译器不关心也管不着。那动态绑定是怎么发生的关键在于Java对象在内存中实际上是完整的子类对象只是通过父类引用来观察它。当我们调用p.eat()时JVM会沿着这个对象真实的类结构去寻找eat()方法——找到的自然是Student重写后的版本。这就是多态的底层逻辑也是向上转型方法重写能实现多态的根本原因。这里有一个高频面试题向上转型后能调用子类独有的方法吗答案是编译期不能运行期也调不到——因为编译器根本不允许调用一个父类引用上不存在的方法你必须先把引用转回子类型。4.3 重载与重写多态的静与动很多初学者把重载Overload和重写Override搞混。我提供一个快速区分方法重写是父子类之间的关系是运行时多态。重载是同一个类里多个方法之间的关系是编译时多态更准确的叫法是编译时绑定或静态分派。public class Calculator { public int add(int a, int b) { return a b; } public double add(double a, double b) { return a b; } }这两个add方法构成重载关系调用calc.add(1, 2)时编译器根据参数类型两个int在编译期就能确定调用第一个方法所以叫编译时多态。而重写发生在继承体系中。重写要求方法签名完全相同重载要求方法签名一定不同。还有一点很容易忽略重写方法上的Ovveride注解如果用在重载方法上编译器会直接报错因为那个方法并没有覆盖父类的任何方法。4.4 多态实战策略模式雏形与开闭原则多态最经典的价值体现在开闭原则上对扩展开放对修改关闭。我讲一个实际开发中常用的场景。假设你有一套支付系统要支持支付宝、微信、银行卡三种支付方式。不用多态的话会写出一大堆if-elsepublic void pay(String type, double amount) { if (alipay.equals(type)) { // 支付宝逻辑 } else if (wechat.equals(type)) { // 微信逻辑 } else if (bankcard.equals(type)) { // 银行卡逻辑 } }每次新增一种支付方式就要改这个方法的if-else改多了就漏漏了就出线上事故。用多态来重构public interface PaymentStrategy { void pay(double amount); } public class AlipayStrategy implements PaymentStrategy { Override public void pay(double amount) { System.out.println(使用支付宝支付 amount 元); } } public class WechatStrategy implements PaymentStrategy { Override public void pay(double amount) { System.out.println(使用微信支付 amount 元); } }调用方只依赖PaymentStrategy接口public class PaymentContext { private PaymentStrategy strategy; public PaymentContext(PaymentStrategy strategy) { this.strategy strategy; } public void execute(double amount) { strategy.pay(amount); } }以后新增一种京东支付只需要写一个新的实现类不需要改动任何已有代码这就是对扩展开放原来的支付逻辑稳如泰山这就是对修改关闭。多态把变化的逻辑从稳定的骨架里剥离出来这正是面向对象设计最迷人地方。4.5 instanceof与类型转换的隐患有了多态就一定会有想从父类引用转回子类引用的需求。这时要用到向下转型Downcasting而向下转型最怕ClassCastException。Person p new Student(张三, S001); Teacher t (Teacher) p; // 运行时报错ClassCastExceptionp实际上指向Student对象强转成Teacher当然会失败。安全做法是先判断类型if (p instanceof Teacher) { Teacher t (Teacher) p; t.teach(); } else if (p instanceof Student) { Student s (Student) p; s.study(); }从Java 16开始instanceof有了模式匹配的新语法写法更简洁if (p instanceof Student s) { s.study(); }不过我要提醒一句过度使用instanceof是设计坏味道。如果你在一个方法里频繁写instanceof分支往往说明你的父类接口设计得不够抽象或者你把本应该由多态处理的事情强行用类型判断来处理了。正确的思路是优先在父类或接口中定义统一的方法让子类各自实现调用方只面向父类接口编程根本不需要关心具体类型。5. 三大特性如何协同工作5.1 封装为继承保驾护航三大特性从来不是孤立存在的。封装是基础继承和多态都建立在这个基础之上。为什么这样说我们看一个反面案例。假设父类Person里有个字段name被声明成了public子类Student想改姓名属性直接name 李四因为父类的细节完全暴露子类根本不知道这个字段要不要经过校验很容易绕过父类的约束。而如果父类把name声明为private只提供一个受控的setter那么子类和外部代码一样都必须通过setter来修改姓名校验逻辑就永远被执行不会因为继承关系被跳过。封装让继承更安全因为父类的核心状态始终处于受控状态。5.2 继承为多态铺平道路多态需要两个前提继承关系和方法重写。这两个前提本质上都依赖继承机制——要么extends一个类要么implements一个接口。所以说得直接一点没有继承就没有多态至少在原生的Java语法层面是如此。反过来多态又提升了继承的灵活性。如果没有多态继承就只是代码复制粘贴的偷懒工具你从一个父类派生出一堆子类调用时却还要判断具体类型来区分处理那就失去了继承的体系化优势。有了多态你才能写出面向父类编程的优雅代码一个方法接收父类引用却能自动适配所有子类。5.3 一个完整案例员工薪资系统我把三大特性揉进一个完整的案例里大家感受一下它们是怎么协作的。场景公司有三种员工——普通员工、经理、销售。月底发工资时不同类型员工的薪资计算方式不同。第一步用封装设计基类Employeepublic abstract class Employee { private String name; private double baseSalary; public Employee(String name, double baseSalary) { this.name name; this.baseSalary baseSalary; } public String getName() { return name; } public double getBaseSalary() { return baseSalary; } // 抽象方法子类必须实现自己独特的薪资计算方式 public abstract double calculateSalary(); }第二步用继承实现三种员工并重写薪资计算逻辑public class NormalEmployee extends Employee { public NormalEmployee(String name, double baseSalary) { super(name, baseSalary); } Override public double calculateSalary() { return getBaseSalary(); } } public class Manager extends Employee { private double bonus; public Manager(String name, double baseSalary, double bonus) { super(name, baseSalary); this.bonus bonus; } Override public double calculateSalary() { return getBaseSalary() bonus; } } public class Salesman extends Employee { private double salesAmount; private double commissionRate; public Salesman(String name, double baseSalary, double salesAmount, double commissionRate) { super(name, baseSalary); this.salesAmount salesAmount; this.commissionRate commissionRate; } Override public double calculateSalary() { return getBaseSalary() salesAmount * commissionRate; } }第三步用多态统一处理public class PayrollSystem { public static void main(String[] args) { ListEmployee employees new ArrayList(); employees.add(new NormalEmployee(张三, 5000)); employees.add(new Manager(李四, 8000, 5000)); employees.add(new Salesman(王五, 4000, 100000, 0.05)); for (Employee emp : employees) { System.out.println(emp.getName() 的本月工资是 emp.calculateSalary()); } } }你看main方法里的循环根本没有出现任何一个普通员工经理销售的具体类型判断。循环里只知道它们是Employee调用calculateSalary()时却各自算出了不同的工资。这就是多态的威力。如果以后公司新增一种实习生你只需要写一个Intern类继承Employee重写calculateSalary()然后在集合里添加一个实例。PayrollSystem的main方法一行都不用改。封装保证了每个员工的数据独立性继承保证了不同员工之间的代码复用多态保证了薪资计算的扩展性——三者缺一不可这正是面向对象设计的组合拳。6. 常见问题与避坑指南6.1 构造器与继承的经典连环坑问题一子类构造器不写super()行不行编译器会自动补super()前提是父类存在无参构造器。如果父类只有带参构造器编译器直接报错。所以我的建议是如果父类定义了带参构造器要么同时显式定义一个无参构造器要么确保所有子类都显式调用父类的带参构造器。问题二构造器里能不能调用被子类重写的方法这是一个很隐蔽的坑。看这段代码public class Parent { public Parent() { init(); // 实际调用的是子类重写后的方法 } protected void init() { System.out.println(Parent init); } } public class Child extends Parent { private String config child-config; public Child() { // super() 自动调用 } Override protected void init() { System.out.println(Child init, config config); } }当你new Child()时先执行Parent构造器Parent构造器调用init()由于动态绑定实际执行的是Child重写后的init()而此时Child的config字段还没完成初始化因为子类字段初始化在父类构造器返回后才执行打印出来就是null。这是个比空指针更可怕的隐性BUG因为代码不报错但结果错得离谱。解决办法是构造器内部只调用private/final方法绝不调用可被子类重写的方法。6.2 equals/hashCode与继承的恩怨JavaSE阶段学Object类时大家都会重写equals和hashCode但一旦牵扯到继承就有很多隐藏问题。场景父类Person定义了equals基于name比较子类Student继承Person并加了一个studentNo字段。如果Student不重写equals两个studentNo不同但name相同的人会被判定为同一个对象。如果Student重写了equals把studentNo也加入比较那么由于对称性person.equals(student)和student.equals(person)就会出现不对称的结论。Person p new Person(张三); Student s1 new Student(张三, S001); Student s2 new Student(张三, S002); p.equals(s1); // Person的equals只比较name返回true s1.equals(p); // Student的equals还要比较studentNop没有该字段返回false这是equals在继承场景下的经典陷阱业界公认的解决方式有两种一是用getClass()做严格类型判断确保两个对象类型完全一致才比较二是尽量复用组合而不是继承来扩展数据字段。我个人更推荐第二种思路如果两个类只是字段上有差异优先考虑组合而不是继承能从根上避开这类对称性难题。6.3 泛型与多态的相互作用泛型和多态经常一起出现但有一个细节经常被忽略泛型是不变的Invariant。也就是说ListStudent并不是ListPerson的子类型即使Student是Person的子类。所以下面的代码编译不过ListStudent students new ArrayList(); ListPerson persons students; // 编译报错为什么Java要这么设计因为如果允许协变你就能通过persons这个List 引用往里面add一个Person对象而底层的实际集合其实是List 这会在运行时把集合搞坏。如果你确实需要接收泛型类型为Person的集合正确姿势是使用通配符public void printNames(List? extends Person persons) { for (Person p : persons) { System.out.println(p.getName()); } }这里的? extends Person允许传入List 、List 等但代价是你只能读取不能往这个列表里添加任何元素除了null。这是Java通过类型系统保障安全的经典设计面试时经常被问到值得重点掌握。6.4 面试高频追问与深度解析我把关于三大特性面试中最容易翻车的问题整理成表每个都值得多花点时间想清楚面试问题推荐回答要点常见错误什么是多态同一引用类型指向不同对象调用同一方法表现不同行为依托继承/接口、重写、向上转型只背概念不说三大条件重载和重写的区别前者编译期决定、方法签名不同、同一类内后者运行期决定、方法签名一致、父子类间混淆动态绑定与静态绑定为什么属性要private防止外部直接修改内部状态保证数据一致性和可维护性只说规范要求不能解释原因super()和this()能不能共存二者都必须放在构造器第一行所以不能共存以为可以同时调用继承和组合怎么选is-a用继承has-a用组合组合更灵活降低耦合一律选组合忽略继承的场景价值向上转型后能否调用子类特有方法编译期不能需向下转型并配合instanceof判断直接说不能错了还有一个人在面试时的高频追问多态有什么好处我建议你不要只答代码复用、可扩展而是用一个完整的小例子说明新增子类时旧代码一行不用改这就是开闭原则落地。面试官想看的是你能不能把抽象概念落到具体场景里。写在最后的一点经验很多人学JavaSE都会有一种感觉三大特性听了无数遍但一到写项目就不知道怎么把这些概念用进去。我自己的体会是学习阶段可以刻意给自己布置一个用面向对象重构的练习找一个写了大量if-else的小功能试着用接口把分支逻辑抽取成一个个策略类用继承去优化相似的实体类用多态来替换显式类型判断。真正动手改造过一两次之后你会发现封装不再是加不加getter的问题继承不再是extends关键字的问题多态不再是面试题的答案。它们会成为你写代码时自然的思考方式——这大概就是基础的意义。
阅读完成 · 觉得有帮助?
咨询建站