《Java编程思想》第7章「复用类」反常识+新手高频易错点
本章核心是组合、继承、构造器初始化、重写、向上转型、清理、代理,下面按模块整理所有违背直觉、极易踩坑的点,全部是书中原文重点坑。
一、继承基础:90%新手第一大坑(反常识最强)
子类看似重写父类private方法,实际完全不是重写(无编译报错,行为完全不符合预期)
错误直觉
父类有private void f(),子类写public void f(),同名方法=重写,多态会调子类。
真实规则(书中明确反常识)
- private方法自动隐式final,对子类完全不可见;
- 子类同名方法只是全新独立方法,叫「遮蔽」,不是重写;
- 父类引用指向子类对象调用该方法,永远执行父类private版本,不会动态绑定。
classParent{privatevoidf(){System.out.println("父私有f");}publicvoidtest(){f();}}classSonextendsParent{publicvoidf(){System.out.println("子公有f");}}// 测试Parentp=newSon();p.test();// 输出:父私有f,不是子类!无任何编译警告踩坑后果
多态失效,逻辑诡异,编译器不提示任何错误,线上隐性bug。
解决方案
子类避开和父类private方法同名;重写只针对public/protected/package方法,务必加@Override。
构造器不会被继承(新手普遍误以为继承包含构造器)
错误直觉
子类继承父类所有方法,构造器也是方法,所以继承。
真实规则
- 构造器不属于类成员,不能被继承;
- 子类必须通过
super()显式/隐式调用父构造器完成父类初始化。
衍生易错:父类无空参构造,子类不写super直接编译报错
编译器默认在子类构造第一行插入super();若父类只有带参构造,无空参,代码直接报错,新手经常忘记加super(参数)。
super()必须是构造器第一行代码,this()和super()不能共存
反常识
就算只想初始化成员,super也必须放最前面,不能先写业务逻辑再调用父构造;同时一个构造器不能同时有super()和this()。
二、构造器初始化顺序:完全违背代码书写直觉(书中经典PolyConstructors案例)
完整执行顺序(递归,从根到子类)
- 递归调用最顶层父类构造器(Object先执行);
- 父类构造器执行中,按成员声明顺序初始化父类成员;
- 执行父类构造方法体;
- 回到子类,按声明顺序初始化子类成员;
- 执行子类构造方法体。
超级致命反常识坑:构造器内部调用动态绑定方法
错误直觉
构造器里调用重写方法,子类变量已经初始化,可以安全使用。
真实灾难
父构造器执行时,子类成员变量还完全没初始化(默认值0/null),此时调用子类重写方法,会访问未初始化字段,产生空指针、数值错乱。
书中示例:父Glyph构造调用draw(),子类RoundGlyph重写draw并读取子类radius,radius此时是0,完全不符合预期。
classGlyph{voiddraw(){}Glyph(){System.out.println("父构造");draw();// 动态绑定到子类draw,但子类变量未初始化}}classRoundGlyphextendsGlyph{privateintr=5;RoundGlyph(intr){}@Overridevoiddraw(){System.out.println("r = "+r);// 输出 r=0,不是5!}}铁律:构造器内部永远不要调用可重写方法。
成员对象初始化:编译器只管父类构造,不管组合成员
新手误区
继承时编译器强制先执行父构造,组合的成员对象也会自动初始化校验。
反常识真相
- 继承:强制递归初始化基类,不写super直接报错;
- 组合(类内持有其他对象引用):编译器不会强制初始化,只给默认null,使用时极易空指针;
- 四种初始化时机:定义处、构造器、实例初始化块、懒加载,编译器不会兜底。
三、组合 vs 继承:新手无脑滥用继承(全书核心反常识设计原则)
大众错误直觉
要复用代码就用继承,继承比组合高级、好用。
书中核心结论(完全反新手直觉)
- 优先组合,慎用继承;继承使用场景极窄;
- 判断标准:是否需要向上转型?
- 需要多态、向上转型 → 用继承;
- 只是复用功能、不需要转成父类 → 必须用组合;
- 继承是「is-a」强耦合;组合是「has-a」弱耦合,扩展性更强。
典型错误场景
// 错误:只是想用工具类方法,不需要向上转型,不该继承 --继承classMyListextendsArrayList{}// 正确:组合持有对象,转发方法 --组合classMyList{privateArrayListlist=newArrayList<>();publicvoidadd(Objecto){list.add(o);}}代理(中介复用):介于组合与继承之间,极易混淆
新手分不清组合、继承、代理:
- 继承:对外暴露父类全部接口;
- 纯组合:外部拿不到内部对象接口;
- 代理:持有对象+选择性转发方法,隐藏内部实现,不暴露完整接口。
四、重写、重载、遮蔽:概念混淆高频坑
1. 参数列表不同=重载,不是重写(编译器不报大错,多态失效)
错误直觉:同名方法就算重写
父void play(),子类void play(int a),新手以为重写,实际是全新重载方法。
Instrumenti=newWind();i.play();// 调用父类play,不会调子类带参方法解决:所有重写强制加@Override注解,编译期拦截签名错误。
2. 域变量遮蔽(成员变量不存在重写,只有遮蔽)
反常识:变量没有多态,只有方法有多态
父类、子类同名成员变量,父类引用访问时永远读取父类变量,不会动态绑定;
只有实例方法才动态绑定,静态方法、成员变量全部静态解析。
classParent{intx=10;}classSonextendsParent{intx=20;}Parentp=newSon();System.out.println(p.x);// 输出10,不是20!3. final方法不能重写、final类不能继承
新手容易忘记:private等价隐式final;final方法锁定实现,杜绝子类重写破坏逻辑。
知识回炉重温
超级致命反常识坑:构造器内部调用动态绑定方法 --不理解
寓言:盖楼房的施工灾难(完美对应构造器调用重写方法坑)
人物设定
- 地基施工队(父类 Glyph)
手里有一份图纸要求:打地基的时候,必须同步画整栋楼的外观草图(对应父构造器里调用draw()可重写方法)。
地基队只管自己的工序,根本不知道楼上会加什么楼层、装饰。 - 一楼加建队(子类 RoundGlyph)
计划在一楼外墙装一圈圆形雕花,雕花尺寸写在自家仓库里(子类成员变量r)。
他们重写了“画外观草图”这个工作:画图时要把自家雕花尺寸标上去。 - 施工总规则(Java 初始化顺序)
盖楼必须从上到下逆序开工,从最底层地基先完工,再往上一层层施工:
① 先挖地基、浇筑地基钢筋(父类成员初始化)
② 地基施工队完成自身施工流程(执行父构造方法体,这里触发画图)
③ 等地基全部完工,才允许一楼施工队打开自己的仓库、摆放雕花材料(子类成员初始化)
④ 最后才做一楼整体装修(子类构造方法体)
事故全过程(对应代码执行流程)
开发商下令:直接建造带雕花的一楼小楼new RoundGlyph(5)
- 施工系统强制先启动地基工程,一楼队伍原地待命,自家仓库锁死,雕花尺寸数据完全没加载;
- 地基浇筑完毕,地基队长按规定立刻画整楼外观草图;
- 画图这个活被一楼队伍重新定义过(方法重写、动态绑定),系统自动喊一楼队伍来画图;
- 一楼绘图员赶来画图,想要调取仓库里预设好的雕花半径
5,可仓库还没解锁,所有材料数据全是空/默认0; - 图纸画出来雕花尺寸是0,图纸直接报废,出现诡异错误;
- 等这张错误图纸画完、地基工序彻底结束,施工系统才解锁一楼仓库,把雕花半径设为5,但为时已晚,草图早就画坏了。
核心矛盾(反常识根源)
普通人直觉:我要建的是带雕花的房子,房子所有配件肯定提前备好,画图一定能拿到雕花尺寸。
现实施工规则:地基工序完全优先于一楼所有物料加载,地基执行阶段,子类所有属性都处于未初始化空白状态。
动态绑定不会管施工进度,只要方法被子类重写,就一定会调度子类的逻辑,子类逻辑强行访问还没生成的成员,必然出错。
寓言对应代码映射表
| 寓言事物 | Java 代码概念 |
|---|---|
| 地基施工队 | 父类 Glyph |
| 一楼雕花小楼 | 子类 RoundGlyph |
| 画外观草图 draw() | 可被子类重写的实例方法 |
| 仓库雕花半径 r | 子类实例成员变量 |
| 地基完工立刻画图 | 父构造方法体内调用重写方法 |
| 仓库锁死、数据空白 | 子类成员未初始化,默认初始值0/null |
| 图纸尺寸错误 | 程序读取到错误默认值,业务逻辑异常 |
总结一句铁律(寓言大白话版)
地基还没干完,楼上的装修材料根本没进场,千万别在打地基的时候,让楼上工人来做需要自家材料的活儿。
翻译成 Java:父类构造器执行阶段,子类字段未初始化,绝对不要在构造器内部调用能被子类重写的实例方法。
成员对象初始化:编译器只管父类构造,不管组合成员 --不理解
寓言:开一家连锁酒馆,看懂「继承父类构造」和「组合成员变量」的编译器区别
人物设定
- 酒馆总店 = 父类:开分店必须先建好总店的地基、消防、执照,总店的硬性规则没人能跳过
- 分店门店 = 子类(继承):你想开一家分店,属于总店体系下的门店
- 酒馆里的调酒师 = 组合成员对象:分店自己聘请的员工,是分店单独持有的一个外部对象
第一部分:继承(分店→总店),编译器=市场监管局,强制先走父类构造
市场监管局(编译器)有死规定:任何一家挂靠总店的分店,开业之前,总店本身必须是合法完工状态。
- 总店建造(父类构造方法):铺设下水、办理消防证件、搭建房屋主体,这套流程是法定必填项。
- 你修建分店的时候,哪怕你完全不管总店,监管系统会自动强制调用总店的施工流程(隐式
super()); - 如果你故意删掉总店施工流程、或者写了错误的构造调用,监管直接不给你下发开业许可(代码编译报错)。
一句话:继承是上下级从属关系,上层的基础建设由法律(Java语法)强制兜底、强制执行,不做就不让编译。
第二部分:组合(分店→调酒师),编译器只管登记姓名,不管真人上岗
分店内部需要一名调酒师干活,你在分店图纸里写下「本店配置一名调酒师」(也就是代码里写Bartender bartender;成员变量)。
监管局(编译器)只会做一件事:给这个岗位默认填上“人员空缺(null)”。
监管完全不会:
- 不会强制要求你开业前必须招聘到位、办理入职;
- 不会主动帮你去找调酒师、完成实例化
new Bartender(); - 不会校验你这家店有没有调酒师。
- 你要是勤快:开店前在图纸上直接写明招聘好人、或者开店装修(构造方法)时招好人,营业调酒正常运行;
- 你要是偷懒:只标注了岗位,全程不去招人,门店开张后客人点酒,你喊调酒师干活,当场没人在岗(运行时空指针NPE崩溃)。
监管局的逻辑很简单:总店是体系根基,影响整个门店合法性,必须强制校验;调酒师只是门店的一个私有工具人,属于门店自己的业务细节,官方不插手你的人事管理,出事自己负责。
对应原文四条知识点一一对照
继承:强制递归初始化基类,不写super直接报错
开分店必须逐层把总店、总店的上级总部全部施工完毕,super省略编译器自动补,手动写错super参数直接施工审批驳回(编译报错)。组合成员:编译器不会强制初始化,默认null,极易空指针
只登记岗位名称,默认状态就是空缺null,什么时候招人完全由店家自己决定,营业调用岗位功能就会出问题。成员四种初始化方式(店家四种招人时机)
- 定义处初始化:图纸上直接写明「本店自带调酒师张三」
Bartender b = new Bartender(); - 构造器初始化:门店装修施工的时候同步完成招聘
- 实例代码块初始化:门店动工阶段自动执行招人流程
- 懒加载:客人第一次点酒的时候,现场临时招人上岗
这四种方式全是店家自主行为,监管全程不会插手兜底校验。
核心本质总结
继承 = 层级血缘根基,语言层面强制安全校验;
组合 = 自己持有的外部资源,语法只分配内存引用,业务正确性完全交给开发者自己管控。
继承、组合、代理的区别
寓言:咖啡店经营
角色
总店(父类)、网红咖啡店(子类)、咖啡机(外部功能类)
1、继承(is-a:咖啡店属于总店分支)
网红咖啡店 extends 总店
总店定死原料、制作流程,分店全盘接收所有规则。
特点:强绑定,总店改一条规矩,所有分店强制同步;仅适合需要统一归口、做多态管理的场景。
2、组合(has-a:店里摆放一台咖啡机)
咖啡店内部持有咖啡机对象,自身不继承咖啡机代码。
店铺需要萃取咖啡时,直接调用咖啡机的功能即可。
特点:持有资源、自由管控,可随时更换全自动/半自动咖啡机,店铺本体代码无需改动,低耦合易扩展。
3、代理(包装组合:咖啡店增设吧台店员)
店铺组合了咖啡机,再加一层店员代理所有操作:顾客不点咖啡机,只找店员下单。
店员内部调用咖啡机干活,还能额外附加:接单、清洁、售后校验等附加逻辑,对外屏蔽咖啡机原始接口。
特点:在组合基础上加一层包装,管控访问、增强原有功能
三者层级关系
继承:身份归属绑定(纵向血缘)
组合:持有原始工具(最基础的持有关系)
代理:基于组合,封装工具并增加管控逻辑(组合的进阶用法)
一句话区分
继承=我就是你家分支;组合=我手里有你的工具;代理=我拿着你的工具,替别人办事还多加管控。
习题巩固
第1题
classParent{privatevoidtest(){System.out.println("父类私有方法");}publicvoidrun(){test();}}classSonextendsParent{publicvoidtest(){System.out.println("子类公有方法");}}publicclassDemo{publicstaticvoidmain(String[]args){Parentp=newSon();p.run();}}请问代码运行打印结果是?
A:父类私有方法
B:子类公有方法
C:编译报错
D:运行空指针异常
正确答案:A
解析:父类的 private 方法自带隐式 final 修饰,对子类完全隐藏,子类同名方法只是全新独立方法,属于方法遮蔽而非重写。run 方法内部调用的是父类自身私有 test 方法,不受多态动态绑定影响,最终只会输出父类私有方法。
第2题
父类代码:
classFather{Father(Stringname){}}子类仅声明类,无自定义构造方法:
classSonextendsFather{}该段代码编译是否会报错?
A. 不报错,子类自动继承父类构造
B. 报错,编译器默认插入无参 super (),父类没有无参构造
C. 不报错,系统自动生成子类空参构造匹配父类
D. 报错,构造方法不允许被继承
正确答案:B
解析:构造器无法被继承,子类默认空参构造的第一行隐含 super () 调用父类无参构造;父类只定义了有参构造,编译器不会自动生成默认无参构造,super () 找不到对应构造方法,直接编译报错。
第 3 题
关于构造器中 super ()、this () 使用规则,说法正确的是:
A. super () 可以写在构造方法任意行位置
B. 同一个构造方法内,可以同时调用 super () 和 this ()
C. this () 用来调用本类其他构造,必须放在代码第一行
D. 普通成员方法里也可以使用 super ()、this () 调用构造器
正确答案:C
解析:super ()、this () 调用构造器必须写在构造方法第一行,二者不能共存于同一个构造函数;super ()、this () 调用构造器的语法仅允许在构造方法内部使用,普通成员方法不支持该写法。
第 4 题
创建子类对象时,初始化执行流程排序正确的是:
① 执行子类构造方法体
② 初始化父类成员变量
③ 递归执行顶层父类构造器
④ 初始化子类成员变量
⑤ 执行父类构造方法体
A. ③→②→⑤→④→①
B. ③→⑤→②→④→①
C. ②→③→⑤→①→④
D. ③→②→④→⑤→①
正确答案:A
解析:标准初始化顺序:递归执行顶层父类构造器 → 按声明顺序初始化父类成员变量 → 执行父类构造方法体 → 初始化子类成员变量 → 执行子类构造方法体。
第 5 题
运行classBase{Base(){print();}voidprint(){System.out.println("base");}}classSubextendsBase{privateintnum=10;@Overridevoidprint(){System.out.println(num);}}publicclassTest{publicstaticvoidmain(String[]args){newSub();}}程序运行输出结果为?
A. base
B. 10
C. 0
D. 编译报错
正确答案:C
解析:代码语法合法,不存在编译错误。父类构造执行时会触发动态绑定,执行子类重写的 print 方法,但此时子类成员变量 num 还未执行赋值操作,int 类型默认初始值为 0,因此打印数值 0;开发规范:构造方法内部禁止调用可被重写的实例方法。
第 6 题
下面说法正确的是:
A. 使用组合方式定义的成员引用,编译器会强制帮我们完成实例化,不会为 null
B. 继承会强制逐层初始化父类,组合的成员对象编译器不会强制初始化,默认值是 null
C. 组合的成员变量只能在构造方法里初始化
D. 成员对象只会在实例初始化块中初始化
正确答案:B
解析:继承机制会强制逐级初始化父类结构;组合只是普通的成员引用变量,JVM 只会赋予默认值 null,编译器不会自动实例化对象。组合成员支持三处初始化位置:变量定义处、实例初始化块、构造方法,并不局限单一位置。
第 7 题
关于组合和继承的使用原则,描述正确的是:
A. 代码复用直接优先使用继承,继承功能更强大
B. 存在「is-a」关系且需要向上转型多态,选用继承;仅复用功能用组合
C. 组合耦合度更高,扩展性不如继承
D. 继承可以隐藏内部所有方法,组合会暴露全部父类接口
正确答案:B
解析:编程设计原则为优先组合、慎用继承;继承对应 is-a 关系,适合需要向上转型实现多态的场景,单纯复用代码使用组合即可。继承属于强耦合关系,组合耦合更低、拓展性更强;继承会完整暴露父类所有接口,组合可按需转发方法,隐藏内部实现细节。
第 8 题
执行下面代码,打印结果是?
classFather{intnum=10;}classSonextendsFather{intnum=20;}publicclassTest{publicstaticvoidmain(String[]args){Fatherf=newSon();System.out.println(f.num);}}A. 10
B. 20
C. 编译报错
D. 随机数值
正确答案:A
解析:Java 中只有实例方法存在多态,成员变量没有动态绑定机制,访问变量只依据引用的静态类型。此处引用类型为 Father,读取父类的 num=10,该现象称为变量遮蔽。
第 9 题
下列说法错误的是
A、final 修饰的方法不能被子类重写
B、private 方法自带隐式 final 属性
C、final 修饰的类无法被继承
D、父类普通方法加 final,子类可以重载该方法签名一致的方法
正确答案:D
解析:final 修饰方法的作用是禁止子类重写,完全相同方法签名的写法会编译报错;重载要求方法参数列表必须不同。A、B、C 三项描述均符合 Java 语法规则。
第 10 题
运行如下代码的输出结果是?
运行classParent{publicstaticvoidshow(){System.out.println("父类静态方法");}}classChildextendsParent{publicstaticvoidshow(){System.out.println("子类静态方法");}}publicclassDemo{publicstaticvoidmain(String[]args){Parentref=newChild();ref.show();}}A. 父类静态方法
B. 子类静态方法
C. 编译报错
D. 运行异常
正确答案:A
解析:静态方法采用静态绑定,执行逻辑由引用类型决定,和堆中实际创建的对象无关。当前引用类型为 Parent,执行父类静态方法,该行为是静态遮蔽,并不是方法重写。
链接:
《Java编程思想》重点章节+踩坑学习规划