拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Java面向对象进阶:多态、final、权限修饰符与代码块串讲

先聊个现象。很多同学学Java时类、对象、封装、继承都还能对上号一到多态就开始迷糊明明父类引用指向子类对象调用方法时走的却是子类逻辑这让不少人第一次感觉到代码好像有自己的想法。多态、包、final、权限修饰符、代码块这几个知识点表面看是五个独立考点实际上在真实项目里是搅在一起用的。面试时八股文背得再熟写不出能体现面向对象设计意图的代码照样过不了关。这篇文章就用一条主线把它们串起来配合能直接复制运行的代码帮你把这块硬骨头啃下来。先说清楚这篇文章适合谁刚学完继承、准备进入面向对象进阶阶段的初学者以及面试前想系统梳理Java基础的人。文里会解释每个关键设计背后的为什么也会把我自己在项目里踩过的坑、总结出的经验放进去。1. 多态到底多在哪先理解这个最大分水岭1.1 一道例题暴露的本质很多教程讲多态喜欢用动物叫的经典例子。我也用这个因为它确实直观class Animal { public void shout() { System.out.println(动物在叫); } } class Dog extends Animal { Override public void shout() { System.out.println(汪汪汪); } } class Cat extends Animal { Override public void shout() { System.out.println(喵喵喵); } }然后呢注意看这段代码Animal a1 new Dog(); Animal a2 new Cat(); a1.shout(); // 输出汪汪汪 a2.shout(); // 输出喵喵喵同一个Animal类型的引用调用同一个shout方法结果却不一样。这就是多态编译看左边运行看右边。编译看左边指的是a1是Animal类型编译器要确认Animal里有shout方法没有就编译报错。运行看右边指的是程序真正运行时JVM根据new出来的实际对象来决定调用哪个方法。左边变量的类型是表面身份右边new出来的对象才是真实身份。1.2 三个前提条件缺一不可学多态前有件事一定要搞清楚不是把子类对象赋给父类引用就算多态它有三个硬性前提。必须有继承关系或者实现了同一个接口但接口属于另一个进阶话题这里先不展开必须有方法重写如果子类没有重写父类的方法调用时走的还是父类的逻辑多态就不成立必须有父类引用指向子类对象的写法Parent p new Child();我见过不少初学者只写继承、不写重写然后说自己实现了多态。一运行输出的全是父类的东西直接就懵了。多态的核心动作是重写重写才是多态的灵魂。1.3 成员变量不参与多态这是个高频面试坑方法调用会走子类但成员变量的访问不会。看这段代码class Parent { String name parent; } class Child extends Parent { String name child; } Parent p new Child(); System.out.println(p.name); // 输出parent很多人第一次遇到这个结果都觉得自己记错了。其实规则很简单成员变量看左边类型左边是Parent访问的就是Parent里的name。方法看右边实际对象变量看左边引用类型。这个点几乎每个Java面试题集里都会出现需要刻意记一下。因为成员变量不参与多态所以写类的时候建议成员变量一律用private私有化通过getter/setter方法访问。这样既能保护数据也能避免这种引用类型不同导致访问结果不同的混乱。1.4 向下转型把多态的对象还原回来有向上转型自然就有向下转型。向上转型是自动发生的几乎无风险向下转型就得小心了因为它可能抛出ClassCastException。Animal a new Dog(); // 想调用Dog特有的方法watchDoor必须先转型 if (a instanceof Dog) { Dog d (Dog) a; d.watchDoor(); }写代码时应先用instanceof判断类型再做强制转换。这是我实际开发里必须遵守的规矩哪怕看起来逻辑上肯定是这个类型也先判断一下。多态让类型变得灵活灵活往往也意味着运行时的不确定防御性判断不能省。1.5 多态最值钱的地方面向父类编程初学者最容易问的一句话是搞这么复杂图什么直接Dog d new Dog()不好吗答案是当代码里出现大量具体类型时每增加一个新类调用方的代码就要跟着改。多态的价值在于让调用方依赖父类这个稳定的抽象而不依赖具体的子类。常见的三种写法方法参数用父类public void train(Animal a) { a.shout(); }这个方法可以接收任何Animal的子类。以后新增一个Pig类train方法一行都不用改。方法返回值用父类集合元素用父类比如ListAnimal里装各种动物循环时直接用Animal类型接收都是多态的价值体现。你在项目中用的ListString list new ArrayList()就是多态。ArrayList实现了List接口声明用List、实例化用ArrayList后续想换实现类改右边就行了。这就是面向接口/父类编程的日常含义。2. 权限修饰符四个访问层级背后的封装意识2.1 一张表看清四个级别的可见性Java的权限控制是很多新手码农极少主动思考的环节。很多人写类一律public觉得反正自己能访问到就行。但权限修饰符恰恰是封装思想能落地的工具。先明确一件事Java里有四个访问级别但只有三个关键字。默认权限就是什么都不写它不是没有权限修饰符而是使用package-private包私有这个级别。修饰符同类同包不同包子类不同包其他类private是否否否默认不写是是否否protected是是是否public是是是是记忆诀窍private最小public最大默认介于之间但主要面向同包protected在默认的基础上多开放了一条跨包子类继承的通道。2.2 protected最容易踩的坑跨包子类的访问限制protected这个级别很多人理解成子类可以访问父类的protected成员。这句话不够精确。看代码package com.demo.a; public class Father { protected void show() { System.out.println(Father show); } }package com.demo.b; import com.demo.a.Father; public class Son extends Father { public void test() { Father father new Father(); // father.show(); // 编译错误 Son son new Son(); son.show(); // 编译通过 son.show(); // 编译通过 this.show(); // 编译通过 } }跨包继承时子类可以访问的protected成员只能通过子类类型或者子类的子类类型的引用来访问用父类引用直接访问父类的protected成员反而编译不过。这涉及protected成员在被继承后归属权的规则理解成子类继承了protected成员后它属于子类自己的领地了会比较顺。2.3 实际项目里的权限设计习惯权限设计有一个核心原则能小则小。这也是最小暴露原则。成员变量一律用private通过public的getter/setter暴露。变量是内部实现细节外部不应该直接操作。本类内部用的辅助方法private。需要被同包其他类协作调用的方法默认权限就够。需要子类重写或使用的钩子方法protected。比如模板方法模式里父类定义算法骨架把某些步骤交给子类实现这些待扩展点通常就是protected。对外提供的APIpublic。做项目时还要注意一个矛盾权限太小确实更安全但过度private会给测试和扩展造成麻烦。合理的做法是把稳定的、内部不打算变的东西锁死把可能变化的、需要外部协作的东西适度开放。3. 包不只是文件夹更是访问控制的隐形边界3.1 包的两个本质作用很多入门教程把包当成放代码的文件夹这个说法没错但只解释了一半。包的本质有两个作用第一是命名空间防止类名冲突第二是访问边界默认权限和protected的可见范围都与包直接相关。命名空间的作用很好理解。公司里两个团队都写了个User类如果没有包就没法同时用。有了包com.company.order.User和com.company.member.User就是两个不同的类。访问边界则解释了为什么上节表格里默认权限同包可见、跨包彻底不可见。包在这里相当于一个逻辑上的小区同一个小区内部可以共用默认权限的成员小区外部一概不认。protected允许小区外的亲戚不同包的子类访问但范围也仅限继承这条线。3.2 命名规范与import的本质包的命名规范是公司域名倒写比如com.example.demo。如果项目将来要上线域名倒写能保证包名的全局唯一性。理解import也很关键。很多初学者以为import像C语言的#include一样把别的类的代码复制进来。其实完全不是。import的唯一作用是让你在写代码时不用打全限定名。// 不用import的写法 public class Test { public static void main(String[] args) { com.example.demo.Student s new com.example.demo.Student(); } } // 用import的写法 import com.example.demo.Student; public class Test { public static void main(String[] args) { Student s new Student(); } }写代码时import只是省略了前缀。如果两个不同包里有同名类解决方法很简单用import引入其中一个另一个用全限定名。这个我实际开发中经常碰到尤其是同时操作订单系统和用户系统的User时。3.3 包名与目录结构的关系还有一个容易忽略的问题包名看起来像目录层级但Java并不强制要求源码文件的物理路径与包名一致。不过实际开发中强烈建议IDE按包名创建目录因为javac和大多数构建工具都默认按这种结构找类。我的经验是别在这个地方标新立异包名与目录结构保持一致能省掉大量编译排查时间。3.4 实际项目常见的包组织方式实际项目的分包一般按技术分层 业务模块来。常见结构com.company.project ├── controller ├── service │ └── impl ├── dao ├── entity ├── config ├── utilcontroller接收外部请求负责参数解析service业务逻辑处理dao数据库操作entity实体类config配置类util工具类这个结构虽然不是唯一的但它能帮你理解一种思路包不只是防重名的更是代码的职责边界。放在controller里的类不应该出现SQL语句放在dao里的类不应该处理HTTP请求。包结构清楚了权限修饰符的应用边界也就清楚了——同一个包里的协作类和包外的调用类访问范围天然就分层了。4. final用拒绝来守护设计稳定性4.1 final的本质是不可变的声明面向对象提供了继承这张万能牌但不是每个类、每个方法都应该被继承和重写。final关键字就是给Java开发者提供的反向控制工具用来明确表达这里不允许再变了。final修饰类这个类不能被继承。典型例子是String、System等基础类。为什么让String不可继承因为String在Java里被大量使用如果允许继承可能被改写行为特征破坏安全性与性能假设。final修饰方法这个方法不能被子类重写。典型应用场景是模板方法模式里的核心算法骨架父类定义好整体执行顺序用final锁死步骤只允许子类重写某个可变步骤。4.2 final修饰变量的两条关键规则final变量规则要分情况记清楚。局部变量一旦赋值就不能再改。public void test() { final int x; x 10; // x 20; // 编译错误 }成员变量必须在构造完之前被赋值。赋值时机有三个声明时直接初始化、构造代码块里初始化、构造方法里初始化。三选一不能遗漏。下面这段是错误示范class Person { // final成员变量没有在声明、构造代码块或构造方法里初始化 // 编译报错 final int id; }因为final成员变量一旦对象创建就得有确定的值编译器不允许出现可能没赋值的状态。引用类型变量要特别注意final保证的是引用指向不能被改变而引用指向的对象内容可以随便改。final ListString list new ArrayList(); list.add(ok); // 正常 // list new ArrayList(); // 编译错误这个点面试里反复出现。很多人只背结论final修饰后不能改遇到引用类型就栽了。其实想想设计意图就清楚了final的目的是保证这个变量始终指向同一个对象至于对象内部怎么变是对象自己和他的方法说了算。4.3 static final全局常量的正确姿势最常用的组合是static final用来定义全局常量。public class Constants { public static final int MAX_RETRY_COUNT 3; public static final String DEFAULT_CHARSET UTF-8; }命名上用static final定义的常量要求全大写单词之间用下划线分隔。static保证常量属于类而不是某个实例final保证值不可变。编译期间使用这种常量的地方编译器会直接替换成字面值。所以修改常量值后使用方必须重新编译才能生效这是我在项目里踩过的一个隐藏坑。4.4 final与多态的配合什么能重写什么不能多态依赖重写但重写并不是越自由越好。一个设计良好的父类应该明确区分三件事谁都能用的通用实现public方法留给子类改写的扩展点protected 允许重写不希望任何人改的核心骨架final方法这个思路放在大型框架里尤其明显。以模板方法模式为例父类用final修饰run()方法定义执行流程流程内部某个步骤抽象出来交给子类实现。如果没有final子类一冲动把run整个重写了模板方法模式就名存实亡了。所以我现在写类时凡是想好了这个流程顺序不许改的方法一律加final从编译层面拦住想乱重写的同事。5. 代码块对象创建路上那些悄悄执行的代码5.1 三种代码块谁在什么时候执行代码块分三类局部代码块、构造代码块、静态代码块。很多人觉得这个知识点鸡肋但代码块与初始化顺序的考题在面试里是常客而且理解了代码块才能真正看懂一个对象从类加载到构造完成的全过程。class Demo { static { System.out.println(静态代码块); } { System.out.println(构造代码块); } public Demo() { System.out.println(构造方法); } public void test() { { System.out.println(局部代码块); } } }三个块的区别就是它们所处的位置和执行时机。静态代码块类加载时执行且只执行一次。类加载发生在JVM第一次碰到这个类时比如第一次new对象、访问静态成员。它适合做类级别的初始化比如读取配置文件、初始化连接池。构造代码块每次new对象时在构造方法之前执行。如果类里写了多个构造方法构造代码块会被多个构造方法共享相当于提了一个公共初始化逻辑。局部代码块方法里用大括号包起来的代码段作用是限制变量的作用域。大括号结束变量就出作用域了可以尽早释放内存。5.2 静态代码块不访问非静态成员的硬性原因静态代码块里访问非静态成员编译会报错。原因要理解本质静态代码块属于类在类加载阶段执行但非静态成员要等对象创建之后才有实体。类加载的时候对象不一定存在自然不能访问。这其实和静态方法不能直接访问实例成员是同一个道理。5.3 初始化顺序的完整链路有继承关系时初始化顺序是面试高频题。直接上代码建议你自己跑一遍验证class Parent { static { System.out.println(1. Parent静态代码块); } { System.out.println(3. Parent构造代码块); } public Parent() { System.out.println(4. Parent构造方法); } } class Child extends Parent { static { System.out.println(2. Child静态代码块); } { System.out.println(5. Child构造代码块); } public Child() { System.out.println(6. Child构造方法); } } public class InitTest { public static void main(String[] args) { new Child(); new Child(); } }输出顺序1. Parent静态代码块 2. Child静态代码块 3. Parent构造代码块 4. Parent构造方法 5. Child构造代码块 6. Child构造方法 3. Parent构造代码块 4. Parent构造方法 5. Child构造代码块 6. Child构造方法看到规律了吗静态代码块只在第一次创建对象时执行而且父类先于子类。非静态部分每次都执行父类构造代码块、父类构造方法再到子类构造代码块、子类构造方法。整体顺序用一句话概括父类静态先于子类静态静态先于实例父类先于子类。5.4 构造代码块在项目里的实用场景构造代码块不是摆设。假设一个类的所有构造方法都需要打印日志或者对某个字段做统一校验在每个构造方法里重复写会很啰嗦。放进构造代码块每次new对象时自动执行省掉大量重复代码。我在写带多个重载构造方法的类时经常用构造代码块做统一的非空校验和默认值填充。5.5 隐藏陷阱构造方法中调用可重写方法的隐患代码块还引出一个更隐蔽的坑父类构造方法中调用了一个可被子类重写的方法由于初始化顺序是父类构造在前子类字段在后当父类构造方法执行到那个重写方法时子类的字段还没有被初始化。此时访问子类字段拿到的可能是默认值null或0。这种问题非常难排查因为代码看起来很合理运行结果却莫名其妙。我的经验是构造方法里只做初始化必要的工作不调用任何可重写的方法。如果一定要调用就把被调用的方法设为final或private从根上杜绝这种隐患。6. 把五个知识点串起来一个完整的员工薪资案例前面五章分开讲是为了把每个概念说透。但实际项目里这些知识是拧在一起的。用一个完整案例把它们串起来。假设要写一个员工薪资系统有经理Manager和开发Developer两种角色薪资计算方式不同。用面向对象思路设计package com.example.payroll; public class Employee { private String name; private double baseSalary; static { System.out.println(员工系统初始化中...); } { System.out.println(创建员工对象...); } public Employee(String name, double baseSalary) { this.name name; this.baseSalary baseSalary; } public String getName() { return name; } public final double getBaseSalary() { return baseSalary; } public double calcSalary() { return baseSalary; } }注意几个设计点。baseSalary用private外部只能通过getBaseSalary()方法读取这是封装的落地。getBaseSalary()用final修饰是因为基本工资的计算规则对所有员工都一样我不希望某个子类把它改掉。静态代码块和构造代码块则模拟了类加载和对象创建时的初始化环节。package com.example.payroll; 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 calcSalary() { return getBaseSalary() bonus; } }package com.example.payroll; public class Developer extends Employee { private int overtimeHours; private static final double OVERTIME_RATE 200; public Developer(String name, double baseSalary, int overtimeHours) { super(name, baseSalary); this.overtimeHours overtimeHours; } Override public double calcSalary() { return getBaseSalary() overtimeHours * OVERTIME_RATE; } }OVERTIME_RATE用static final定义是全局常量每个Developer对象根据加班小时数计算薪资体现的是重写与多态。最后写入口package com.example.payroll; public class PayrollSystem { public static void main(String[] args) { Employee manager new Manager(张伟, 10000, 5000); Employee developer new Developer(李强, 8000, 20); printSalary(manager); printSalary(developer); } private static void printSalary(Employee e) { System.out.println(e.getName() 月薪 e.calcSalary()); } }运行结果员工系统初始化中... 创建员工对象... 创建员工对象... 张伟 月薪15000.0 李强 月薪12000.0这个案例里多态体现在printSalary方法接收Employee类型参数manager和developer都能传进去各自调用各自的calcSalary()权限修饰符体现在成员变量私有、公开方法public、常量public static finalfinal体现在getBaseSalary方法不可重写、常量不可修改代码块体现在静态代码块和构造代码块的执行包体现在com.example.payroll的命名空间。如果你来设计这个系统试着想想这几个问题给Employee新增一个子类PartTimeEmployee小时工需要改PayrollSystem里的代码吗不需要。如果把calcSalary改成final子类还能重写吗不能。权限修饰符把name锁成private后外部能直接修改吗只能通过方法。想明白这几个问题这五个知识点才算真正变成你自己的东西。最后说一点个人体会。学了这么多语法真正写代码时要先想清楚哪里变、哪里不变。final守住不变的多态隔离变化的权限控制暴露的范围包划清职责边界代码块搞定初始化时机。这套组合拳就是Java面向对象设计的日常形态。多写几个类似的小系统把每个关键字放进真实场景里感受它的价值比背一百道八股文都管用。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门