ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java面向对象三大特征详解:封装、继承、多态实战

Java面向对象三大特征详解:封装、继承、多态实战 简介一份面向Java初学者及需要巩固面向对象编程基础的学习者的实例解析材料重点围绕封装、继承、多态三大特性展开。文档通过Teacher、Student等具体类的完整代码演示私有属性封装、setter/getter访问接口、构造方法重载及toString()方法重写同时结合Play、TaoistPriest等继承示例说明extends关键字、父类引用与子类实例的调用关系。资源为单个PDF文档压缩包大小49KB内容精炼适合快速查阅或配合课堂学习。目前已有5578人学习下载。整体从基础语法到测试类运行输出层层递进能帮助读者直观理解三大特性的实际写法建立面向对象编程的基本代码范式。1. 为什么从三个真实类里理解封装继承多态接手过老项目的开发者大概都经历过实体类字段全是public业务代码到处直接改值一个字段改名要牵连几十个调用点父类写了一个方法子类各写各的逻辑却没人说得清何时调用父类版本模块之间用具体类交互想换一种实现就得同步搜索所有new出来的位置。Java面向对象编程的封装、继承、多态正是针对这些问题给出的语言级约束。下面的实例覆盖Teacher/Student属性管理、游戏职业继承和登录回调三个场景把三大特征落到可以直接运行的代码里。适合正在准备Java面试、刚刚接手Java业务项目的开发者也适合想重新梳理基础的一线工程师。2. 封装private字段与getter/setter的每个细节2.1 字段设为private入口统一为方法Teacher类把name、majorDirection、teachCourse、teachAge四个字段全部声明为private。这样类外部无法用“对象.字段名”直接读写必须通过public方法进入。数据校验、权限控制和后续重构都可以集中在这几个方法里。如果字段公开业务代码直接写stu.age -1系统没有任何兜底。封装的第一层意义就是把“直接操作”转换为“调用行为”对象内部怎么保存、怎么运算完全由类自己决定。public class Teacher { private String name; private String majorDirection; private String teachCourse; private int teachAge; public Teacher() { super(); } public Teacher(String name, String majorDirection, String teachCourse, int teachAge) { this.name name; this.majorDirection majorDirection; this.teachCourse teachCourse; this.teachAge teachAge; } // getter/setter 方法 }无参构造器保留super()为未来子类继承提供基础构造路径。带参构造器用this.name name区分形参与实例字段。如果写成name nameJava会认为赋值双方都是形参实例字段根本没有被初始化这是封装初写者最容易踩的坑。2.2 setAge中的非法值拦截Student类的setAge给出了一个典型的保护逻辑入参为负数时打印提示但不上写入字段。实际项目里往往改为抛IllegalArgumentException让调用方立刻感知错误而不是带着一个错误值继续运行。public void setAge(int age) { if (age 0) { System.out.println(年龄不能为负值); } else { this.age age; } }这段代码的意图是“不被信任的入参不能直接进入对象内部”。站在调用方角度输入值不合法时对象保持上一次的合法状态而不是处于半损坏状态。如果项目已经依赖校验框架还可以用javax.validation的Min(0)替代手写判断但setter内部保留基本防御仍然是必要的。2.3 getCourses为什么返回字符串而不是数组原文中Student的getCourses()返回Arrays.toString(courses)而不是直接返回String[]。这是有意为之。如果直接返回数组引用外部可以先String[] c stu.getCourses();再c[0] 篡改内部数组被绕过所有校验直接修改。更常见的工程化做法是返回数组副本public String[] getCoursesArray() { return Arrays.copyOf(courses, courses.length); }调用方拿到的是新数组无论怎么修改都影响不到对象内部。如果只用来展示返回Arrays.toString(courses)连数组索引都不暴露。封装的核心不是每个字段都配getter而是以合适的方式暴露能力同时保证内部状态不被外部意外破坏。2.4 访问修饰符与封装调优访问修饰符同类同包子类任何位置private是否否否默认缺省是是否否protected是是是否public是是是是设计原则是成员变量默认private需要给子类重写使用的钩子方法才考虑protected。把字段从private放宽到默认权限同包代码就具备了直接写字段的权限后续加校验时需要同时改所有调用点。字段一旦public外部依赖会迅速扩散这在团队协作里几乎无法收敛。即使只是内部使用的临时变量也建议通过方法访问避免一个人改着改着就把封装拆穿。3. 继承super调用、实例初始化块与重写边界3.1 为什么从Play这个父类开始四个游戏角色类Play、TaoistPriest、Master、Warrior。Play定义了字段main和hitMonster方法三个职业都extends Play。这样一来所有角色共享“打怪”行为各自只保留职业标识代码量减少行为统一。如果不用继承每个职业类都要复制一份hitMonster后续想改成“打怪获得经验值”就得改四个地方漏改一个就会出现职业差异。继承在这里不是炫技而是复用和维护的刚需。3.2 子类构造器必须调用父类构造器public class Play { String main; public Play(String main) { this.main main; } public void hitMonster() { System.out.println(main 打怪); } } public class TaoistPriest extends Play { { System.out.print(我是道士); } public TaoistPriest(String main) { super(main); } }Java规则是如果父类只有带参构造器子类构造器必须在第一行显式调用super(main)。如果父类有无参构造器系统会自动补super()。这里如果删掉TaoistPriest的构造器编译会直接报Implicit super constructor Play() is undefined。这个报错信息已经明确定位了问题但不少新手看到后第一反应是去父类补一个无参构造器而不是在最外层子类传参结果父类main永远为空。3.3 实例初始化块与构造器执行顺序TaoistPriest类里用两个花括号包起来的是实例初始化块。一个对象new出来时执行顺序是父类实例初始化块与父类构造器、子类实例初始化块、子类构造器体。所以输出“我是道士”先出现然后构造器给main赋值最后hitMonster输出“灵魂火符打怪”。如果把初始化块放在构造器之后只是代码阅读顺序靠后JVM执行时仍然先执行初始化块。面试中常问的“父类构造器什么时候调用”本质就是子类构造器第一条语句一定涉及super()。实例初始化块在普通业务代码里不常用因为它让阅读顺序变得不直观。常见替代方案是在子类构造器中首行输出标识。这个示例保留初始化块是为了说明构造器真正执行之前JVM已经完成了一批动作。理解这一点才能解释为什么父类构造器里调用被子类重写的方法时会触发子类版本的逻辑。3.4 重写方法的访问权限不能降低Play.hitMonster是public三个子类没有重写它直接继承。如果子类想重写方法签名必须保持一致并且访问修饰符不能比父类更严格。父类是public子类不能改成protected或默认。父类是protected子类可以改成public或保持protected但不能改成默认。这个限制保证了接口契约的一致性凡是父类能访问的地方替换成子类对象后也不会暴露更少能力。父类方法访问权限子类重写允许的权限publicpublicprotectedprotected或public默认同包内默认、protected或publicprivate不能重写只能新建方法另外重写方法抛出的受检异常范围不能扩大返回值类型可以缩小为父类返回类型的子类型。例如父类返回Number子类重写返回Integer调用方按父类签名拿到的依然是Number不影响既有代码。这些边界在翻阅java.lang包源码时经常出现集合框架里协变返回就是典型。3.5 继承的替代方案组合优先Play这个例子中如果只是为了共享hitMonster方法组合也能实现把Play对象作为字段在子类方法里调用。Java只有单继承一个类只能有一个父类而组合没有这个限制。遇到“一个类既需要角色行为又需要背包行为”时继承做不到同时继承Play和Bag组合可以同时持有两个对象。判断标准仍然是用is-a关系衡量TaoistPriest is a Play成立继承就合理Bag和角色之间是has-a关系应该用组合。面向对象设计里“组合优先于继承”正是为了减少父类变化对子类的连带影响。4. 多态接口回调与运行时才确定的登录结果4.1 为什么用接口而不是具体类登录示例中Server类并不知道外部会传入什么监听器它只接收一个LoginListener接口引用。我们看这个实现Server负责登录逻辑LoginListener负责结果处理。如果不使用接口Server的login方法就需要直接依赖MyLoginListener调用方无法替换处理方式。引入接口后无论是打印到控制台、写入日志还是弹窗提示都只是实现类的差异。多态在这里体现为同一个logon(userName, password, listener)调用因为listener实际类型不同最终执行行为不同。4.2 LoginListener与Server代码解析public interface LoginListener { public void succeed(String msg); public void failed(String msg); } public class MyLoginListener implements LoginListener { Override public void succeed(String msg) { System.out.println(msg); } Override public void failed(String msg) { System.out.println(msg); } } public class Server { public void login(String userName, String password, LoginListener listener) { System.out.print(loading); for (int i 0; i 10; i) { try { Thread.sleep(100 * i); System.out.print(.); } catch (InterruptedException e) { e.printStackTrace(); } } if (zhangsan.equals(userName) 123.equals(password)) { if (listener ! null) { listener.succeed(登录成功); } } else { if (listener ! null) { listener.failed(登录失败); } } } }这段代码里if (zhangsan.equals(userName))比userName.equals(zhangsan)更安全userName可能为null字面量调用equals永远不会抛NullPointerException。Server方法里的第三个参数是接口类型这就是多态的基础。Servlet中的事件监听、Spring中的ApplicationListener都是类似的模式。接口定义协议具体实现类只负责“登录成功后干什么”至于结果槽怎么展示Server完全不关心。对比项重载重写发生时期编译期运行期方法签名参数列表不同必须一致返回类型可以不同可协变不能扩大依赖关系同一个类内父子类或接口实现面试中常拿“重载是编译期多态重写是运行期多态”来区分本质上就是在说重载由编译器根据参数数量、类型决定调用目标重写则要等到运行时JVM根据对象实际类型完成分派。4.3 匿名内部类与Lambda的替代写法MyLoginListener单独形成一个类好处是逻辑复用坏处是一旦监听逻辑只使用一次类文件数量就会膨胀。Java中接口只有一个抽象方法时可以直接用Lambda简化。这段代码演示了如何用匿名内部类替换独立实现类效果与new MyLoginListener()一致server.login(userName, password, new LoginListener() { Override public void succeed(String msg) { System.out.println([成功] msg); } Override public void failed(String msg) { System.out.println([失败] msg); } });登录示例的接口有两个抽象方法所以不能用Lambda必须在接口上补充FunctionalInterface并合并方法或者保留匿名内部类。这里不强行改造是为了提醒读者多态不依赖Lambda匿名内部类同样体现接口回调。实现类该抽出来还是该匿名取决于代码复用频率不影响调用方的多态判断。4.4 接口方法默认修饰符与实现约束接口里public void succeed(String msg);等价于public abstract void succeed(String msg);接口方法默认就是public abstract实现在子类中必须使用public。有的新手在实现类里写成void succeed(String msg)编译会报“试图分配更低的访问权限”这与第3章重写时不能降低访问权限的规则完全一致。无论接口还是父类对外暴露的方法都要维持同等可见度否则JVM没有足够权限在运行时正确分派调用。5. 把三大特征串起来验证封装、继承和多态的三个手段5.1 用javac直接编译整个包不依赖IDE在项目根目录执行编译把class文件输出到out目录mkdir -p out javac -d out src/com/hz/test/*.java java -cp out com.hz.test.Test-d out让编译器按包结构生成class文件。如果类之间有继承关系javac会自动解析依赖不需要手动指定编译顺序。输出“我是道士灵魂火符打怪”时说明继承链路和构造器执行顺序符合预期。多态登录模块单独编译运行javac -d out src/com/hz/listener/*.java java -cp out com.hz.listener.LoginTest5.2 用assert验证setAge的拦截行为在Test类里追加一段断言Student stu new Student(); stu.setAge(-1); assert stu.getAge() 0 : 非法年龄被写入;Java断言默认关闭运行时添加-ea参数才会生效。这个断言能证明封装不是摆设负年龄被拦截后对象内部年龄仍然保持初始值0。如果团队使用JUnit可以改为assertEquals(0, stu.getAge())失败时能看到更清晰的测试报告。这个验证比手动println更自动化适合纳入持续集成的冒烟用例。5.3 用多态数组替代逐职业调用三个职业测试类中每个对象分别调用hitMonster代码重复度很高。可以把对象装进Play[]数组用循环统一触发Play[] players { new TaoistPriest(灵魂火符), new Master(雷电术), new Warrior(烈火术) }; for (Play p : players) { p.hitMonster(); }循环里p声明为Play类型调用hitMonster时JVM根据实际对象类型分派到对应子类。这比if (p instanceof TaoistPriest)判断后再强转的写法简洁也更靠近策略模式的核心思路。以后新增职业只需要在数组里追加一个对象循环本身不需要改动。这里的p虽然编译期类型是父类运行期却分别指向不同子类对象每个对象输出的职业前缀和技能名都不同这就是多态在运行时最直接的一次演示。本文还有配套的精品资源点击获取
返回列表