Skip to content

二、面向对象(J11–J22) ​

J11 · 知识点:重载(Overload) ​

【题目】以下哪种情况属于方法重载(Overload)? A. 子类中重写父类方法,方法签名相同 B. 子类中定义与父类同名同参但返回类型不同的方法 C. 同一个类中方法名相同、参数列表不同 D. 方法名不同、参数列表相同

答案:C

【考点】重载的判定标准:方法名相同 + 参数列表不同(个数/类型/顺序),与返回类型无关。

【结论】选 C。方法重载要求同一类中方法名相同、参数列表不同,返回类型不参与判定。

【逐项辨析】

  • A错:子类重写父类方法是 Override(运行期多态),不是 Overload(编译期多态)。
  • C正确:同一个类中方法名相同、参数个数 / 类型 / 顺序不同即构成重载。
  • B错:同名同参仅返回类型不同,在同一类中既不构成重载也不构成重写,会直接编译错误。
  • D错:方法名不同不构成重载,重载的核心就是“同名不同参”。

【知识点】 重载(Overload)是编译期多态(静态绑定),发生在同一类内(或子类继承父类方法后定义同名不同参方法)。判定标准只看方法名和参数列表(个数、类型、顺序),与返回类型、访问修饰符、异常列表均无关。例如:

java
class Demo {
    void f(int a) {}
    void f(String s) {}      // 参数类型不同,重载
    void f(int a, int b) {}  // 参数个数不同,重载
    int f(int a, String s) { return 0; } // 顺序不同,重载
}

子类继承父类后,若定义与父类方法同名但参数不同,对子类而言也构成重载。重载的意义在于让同一语义操作适配不同参数类型,提升 API 的易用性。

【记忆锚点】 「重载看参数,重写看签名;重载是静态,重写是动态」。

【易混对比】

维度重载(Overload)重写(Override)
发生位置同一类或父子类父子类之间
方法名必须相同必须相同
参数列表必须不同必须相同
返回类型无关相同或协变子类
访问权限无关不能更严格
异常声明无关不能更宽
绑定时机编译期(静态绑定)运行期(动态绑定)

【自测】 以下代码是否构成重载?

java
class A {
    int f(int a) { return 0; }
    long f(int a) { return 0L; }
}

答:不构成重载,编译报错。参数列表完全相同,仅返回类型不同无法区分方法。与第 J11 题连考。

【知识关联】

  • 同库关联:与 J12(重写)、J13(多态)组成 OOP 核心题群;与 J71(泛型擦除影响重载选择)进阶连考。
  • 实现层:重载在编译期决定(静态分派,invokevirtual 常量池指向具体描述符);重写是运行期动态分派。字节码方法描述符含参数类型,故重载靠参数列表区分。
  • 面试追问:① 重载能否只靠返回值区分?② null 作为实参如何选择重载?

【拓展延伸】

  • 变式问法:给出两个仅返回值不同的方法问能否编译;问自动装箱与重载优先级(精确匹配优先于装箱/可变参数)。
  • 版本差异:语义稳定;Kotlin 支持默认参数/命名参数,部分场景可替代重载。
  • 工程注意点:重载参数顺序尽量保持直觉一致;避免「一长串几乎相同的重载」;文档写清每个重载语义差异。

J12 · 知识点:重写(Override)规则 ​

【题目】子类重写父类方法时,下列说法正确的是?

A. 访问权限可以比父类更严格 B. 返回类型可以任意改变 C. 抛出的受检异常范围不能比父类更宽 D. 重写方法可以是 static 的

答案:C

【考点】重写规则的“两同两小一大”。

【结论】选 C。子类重写时,抛出的受检异常范围不能比父类更宽。

【逐项辨析】

  • A错:访问权限只能比父类更宽松(放大),不能更严格。例如父类为 protected,子类可改为 public,反之则编译报错。
  • B错:返回类型必须相同,或是父类返回类型的子类(协变返回,JDK5 起支持),不能任意改变。
  • C正确:受检异常必须是父类方法声明异常的子类或同类,不能抛出更宽泛的父类异常。
  • D错:static 方法不能被重写。子类定义与父类同名同参的 static 方法属于“隐藏(hide)”,调用时按引用类型静态绑定。

【知识点】 重写遵循“两同两小一大”原则:

  • 两同:方法名相同、参数列表相同;
  • 两小:返回类型不能更大(协变允许子类返回类型)、受检异常不能更宽;
  • 一大:访问权限不能更小(只能放大)。

协变返回类型自 JDK5 起支持,允许子类重写方法返回父类返回类型的子类。父类的 private 方法对子类不可见,子类定义同名方法属于新方法,不构成重写。final 方法不可被重写。重写是多态的核心机制,由 JVM 在运行期根据实际对象类型动态绑定方法版本。

【记忆锚点】 「两同两小一大:同名同参,返异小,异常小,权限大」。

【易混对比】

  • 隐藏(Hide)vs 重写(Override):static 方法、private 方法、final 方法均不能被重写;子类对 static 方法同名同参定义叫隐藏,调用时看引用类型(静态绑定),重写看实际对象类型(动态绑定)。
  • 换问法:若父类方法声明 throws IOException,子类重写可以声明 throws FileNotFoundException(子类异常,合法),但不能声明 throws Exception(父类异常,非法)。

【自测】 以下代码能否编译通过?

java
class Parent {
    protected void f() throws IOException {}
}
class Child extends Parent {
    void f() throws FileNotFoundException {}
}

答:不能。子类重写将访问权限从 protected 缩小为默认(包私有),违反“一大”原则;虽然 FileNotFoundException 是 IOException 子类满足“异常小”,但权限缩小已导致编译失败。与第 J12 题连考。

【知识关联】

  • 同库关联:与 J11(重载)、J13(多态)、J18(访问修饰符影响重写)、J19(static 隐藏非重写)强相关。
  • 实现层:invokevirtual/invokeinterface 运行期查 vtable;返回类型协变(JDK 5+)允许子类返回更具体类型;异常不能比父类更宽。
  • 面试追问:① 构造器能否重写?② private/final/static 方法能否重写?

【拓展延伸】

  • 变式问法:问重写时访问权限能否变小(不能);问 @Override 注解作用(编译检查)。
  • 版本差异:JDK 5 支持协变返回值;JDK 8 接口 default 可被重写(J17)。
  • 工程注意点:始终加 @Override;遵循里氏替换;子类重写不要改变语义只扩展行为。

J13 · 知识点:多态 ​

【题目】给定 class Animal {}、class Dog extends Animal {}、Animal a = new Dog();,下列说法正确的是?

A. 编译期 a 的类型是 Dog B. 该代码无法编译 C. a 可以直接调用 Dog 特有的方法 D. 运行期 a 的实际对象类型是 Dog

答案:D

【考点】多态的“编译看左边(静态类型),运行看右边(实际类型)”。

【结论】选 D。Animal a = new Dog() 运行时实际创建的是 Dog 对象,a 的实际类型为 Dog。

【逐项辨析】

  • A错:编译期看引用声明类型,a 的编译期类型是 Animal,不是 Dog。
  • D正确:运行期看实际对象类型,new Dog() 创建的是 Dog 实例,a 的实际对象为 Dog。
  • C错:a 的编译期类型是 Animal,只能调用 Animal 中声明的方法;调用 Dog 特有方法必须向下强转 ((Dog) a).method()。
  • B错:Dog 是 Animal 子类,向上转型自动完成且安全,代码完全可以编译。

【知识点】 多态的核心机制是“编译看左边,运行看右边”。编译器根据引用类型(静态类型)决定可调用哪些方法;JVM 根据实际对象类型(动态类型)决定调用哪个具体实现。向上转型(Upcasting)将子类引用赋给父类引用,自动完成且安全;向下转型(Downcasting)需显式强转,可能抛出 ClassCastException,建议先用 instanceof 判断。

多态的意义在于统一接口、隔离变化——同一引用调用同一方法,实际执行的是对象真实类型的方法。这也是“开闭原则”的基础:对扩展开放(新增子类),对修改关闭(无需改动基于父类引用的调用代码)。

【记忆锚点】 「编译看左边,运行看右边;上调自动,下转要强」。

【易混对比】

  • 静态绑定 vs 动态绑定:private、static、final 方法在编译期即可确定调用版本(静态绑定);实例方法根据运行时类型决定(动态绑定)。
  • 换问法:Animal a = new Dog(); a.eat(),若 Animal 和 Dog 都有 eat(),实际执行 Dog.eat();若 Dog 独有 bark(),直接 a.bark() 编译报错。

【自测】 以下代码输出什么?

java
class Animal { void speak() { System.out.print("A"); } }
class Dog extends Animal { void speak() { System.out.print("D"); } }
Animal a = new Dog();
a.speak();

答:输出 D。运行时看实际类型 Dog,调用 Dog 的 speak()。与第 J13 题连考。

【知识关联】

  • 同库关联:与 J11/J12、J21(instanceof)、J19(static 无多态)构成面向对象主干;与 P57(Python 鸭子类型)对照——Python 无编译期多态约束。
  • 实现层:动态绑定依赖对象头类型指针与 vtable;字段访问没有多态(按引用类型静态绑定),只有实例方法有多态。
  • 面试追问:① 为何字段没有多态?② 向上转型/向下转型的编译与运行期规则?

【拓展延伸】

  • 变式问法:父引用指向子对象,调用被重写方法输出什么;问多态与设计模式(策略、模板方法)的关系。
  • 版本差异:语义稳定;Python/JS 是鸭子类型,运行期才失败。
  • 工程注意点:面向抽象编程减少 if-else 类型分派;向下转型前先 instanceof(J21);避免在父类构造器中调用可被重写的方法。

J14 · 知识点:构造器特性 ​

【题目】关于构造器(Constructor)的说法,正确的是?

A. 构造器可以被继承 B. 构造器可以被重写 C. 构造器可以被重载 D. 构造器有返回类型

答案:C

【考点】构造器的命名、继承、重载特性。

【结论】选 C。构造器可以被重载,但不能被继承或重写,也没有返回类型。

【逐项辨析】

  • A 错:构造器不能被继承。子类不继承父类构造器,但可通过 super(...) 调用父类构造器完成父类部分初始化。
  • B 错:构造器不能被重写。重写要求方法签名相同且存在继承关系,而构造器名必须与类名一致,父子类构造器名称本就不同。
  • C 正确:同一类中可定义多个参数列表不同的构造器,构成重载,通过 this(...) 相互调用以减少代码重复。
  • D 错:构造器没有返回类型,连 void 都不能写;写了 void 就变成普通方法而非构造器。

【知识点】 构造器是特殊的实例方法,名称与类名完全一致,无返回类型。编译器默认提供无参构造器,但一旦显式定义了任何构造器,默认无参构造器即消失。子类构造器首行若无显式 super() 或 this(),编译器会自动插入 super() 调用父类无参构造器——若父类没有无参构造器,则子类必须显式调用父类带参构造器,否则编译报错。

构造器重载通过参数个数、类型区分,常用于提供带默认值的多种构造方式。构造器不能被 static、final、abstract、synchronized 修饰,因为它负责对象创建这一特殊生命周期阶段。

【记忆锚点】 「构造器名同类名,无返回,可重载,不继承」。

【易混对比】

  • 构造器 vs 普通方法:构造器在对象实例化时自动调用,用于初始化;普通方法需显式调用。构造器不返回任何值,普通方法可返回 void 或具体类型。
  • 换问法:父类只有带参构造器时,子类必须怎么做?——必须在子类构造器第一行显式写 super(参数),否则编译报错。

【自测】 以下代码能否编译通过?

java
class A { A(int x) {} }
class B extends A { B() {} }

答:不能。A 显式定义了带参构造器,无参构造器不再默认提供;B 的构造器隐式调用 super() 找不到 A(),编译报错。应改为 B() { super(0); }。与第 J14 题连考。

【知识关联】

  • 同库关联:与 J15(super/this)、J20(初始化顺序)同属构造与初始化题群;与 J01(字段默认值)衔接。
  • 实现层:构造器编译为 <init> 方法;若未显式调用 super(),javac 插入父类无参 <init>;与类初始化方法 <clinit> 区分。
  • 面试追问:① 构造器能否被继承/重写?② 抽象类的构造器有何作用?

【拓展延伸】

  • 变式问法:问构造器返回值(无);问私有构造器用途(单例/工具类)。
  • 版本差异:record(JDK 16+)自动生成规范构造器;enum 构造器必 private(J72)。
  • 工程注意点:避免构造器中泄漏 this;复杂对象用 Builder;不要在构造器中调用可重写方法。

J15 · 知识点:super() 与 this() ​

【题目】关于子类构造器中 super() 和 this() 的调用规则,正确的是? A. 可以同时出现在构造器第一行 B. 可以出现在构造器任意位置 C. 必须出现在构造器第一行,且两者只能出现一个 D. this() 可以出现在子类构造器的任意位置

答案:C

【考点】构造器链:super() 调父类构造器、this() 调本类其他构造器,均须位于第一行且互斥。

【结论】选 C。super() 和 this() 都必须作为构造器的第一条语句,且二者互斥,只能出现一个。

【逐项辨析】

  • A错:两者不能同时出现,因为每个都要求是第一行,语法上无法并存。
  • C正确:super(...) 调父类构造器,this(...) 调本类其他构造器,均须为第一行且只能出现一个。
  • B错:不能出现在任意位置,必须是构造器的第一条语句,以确保对象初始化按正确顺序进行。
  • D错:this() 同样必须位于第一行,不能出现在任意位置。

【知识点】 构造器调用链规则:子类实例化时,必须先完成父类部分的初始化。super(...) 用于显式调用父类指定构造器;this(...) 用于显式调用本类其他构造器以减少代码重复。若子类构造器第一行既无 super 也无 this,编译器自动插入无参 super()。

父类构造器执行顺序沿继承链自顶向下:Object → 顶层父类 → ... → 子类。这一机制确保对象初始化时,父类字段先于子类字段完成构造。this() 指向的本类其他构造器最终仍需通过 super() 完成父类构造,因此整个链条的根部一定是父类构造器。

【记忆锚点】 「super 调父,this 调己,都只能站第一,二者水火不容」。

【易混对比】

  • super 关键字的双重身份:super() 是构造器调用,必须第一行;super.method() 是调用父类被重写的方法,可在任意位置使用。
  • 换问法:子类构造器第一行写 this() 后,还会调用父类构造器吗?——会,this() 指向的本类其他构造器最终仍需通过 super() 完成父类构造。

【自测】 以下代码输出顺序是什么?

java
class A { A() { System.out.print("A"); } }
class B extends A {
    B() { this(1); System.out.print("B"); }
    B(int x) { System.out.print("C"); }
}
new B();

答:ACB。B() 调用 this(1),B(int) 第一行隐式 super() 调用 A() 输出 A,然后输出 C,回到 B() 输出 B。与第 J15 题连考。

【知识关联】

  • 同库关联:与 J14、J20(父类静态→父类实例→子类实例)、J19(static 与 this)强相关。
  • 实现层:this()/super() 必须是构造器第一条语句;字节码中 <init> 开头要么 aload0;invokespecial 父类.<init> 要么 aload0;invokespecial 本类其他.<init>。
  • 面试追问:① 为何 this/super 不能同时出现在同一构造器?② 构造器链会不会成环?

【拓展延伸】

  • 变式问法:给出父子构造器代码问输出顺序;问 this(x) 与 super() 互斥规则。
  • 版本差异:语义稳定;record/enum 有额外约束。
  • 工程注意点:把公共初始化抽到 init() 会破坏 final 语义,优先构造器链;用 this(默认值) 提供便利构造器。

J16 · 知识点:抽象类与接口 ​

【题目】关于抽象类和接口,下列说法正确的是?

A. 抽象类不能有构造器 B. 一个类可以继承多个抽象类 C. JDK8 之前,接口中的方法全部是抽象方法 D. 抽象类不能有非抽象方法

答案:C

【考点】抽象类与接口的结构差异、接口方法的版本演进。

【结论】选 C。JDK8 之前,接口中的方法隐式为 public abstract,全部是抽象方法。

【逐项辨析】

  • A 错:抽象类可以有构造器,供子类 super() 调用,以初始化抽象类中定义的字段。
  • C 正确:JDK8 之前接口方法只能是 public abstract;JDK8 起新增 default 和 static 方法,JDK9 起支持 private 方法。
  • B 错:Java 类是单继承,一个类最多只能 extends 一个父类(无论是否抽象),但可实现多个接口。
  • D 错:抽象类可以包含非抽象方法(已实现的方法),甚至可以没有抽象方法(但如此设计通常无意义)。

【知识点】 抽象类与接口的设计定位不同:抽象类是“is-a”关系,用于提取子类共性,可含字段、构造器、实现方法;接口是“has-a”或“can-do”能力契约。版本演进:

版本接口新增能力
JDK 1.0只能有 public abstract 方法
JDK 5支持嵌套枚举类型
JDK 8新增 default、static 方法(有方法体)
JDK 9新增 private 方法(接口内部复用)

抽象类不能实例化(new 会编译报错),子类必须实现所有抽象方法(除非子类也是抽象类)。接口中的变量隐式为 public static final 常量,方法在 JDK8 前隐式为 public abstract。

【记忆锚点】 「抽象类是半成品,有构造可有实现;接口本是纯契约,JDK8 之后有默认」。

【易混对比】

特性抽象类接口(JDK8+)
构造器有无
继承/实现单继承(extends)多实现(implements)
方法实现可以有default / static / private 可以有
字段实例变量、常量均可只能是 public static final
访问修饰符任意默认 public
设计意图提取共性(is-a)定义能力(can-do)

【自测】 JDK8 接口中以下哪种方法不能有方法体?(版本口径:private 接口方法是 JDK9 才引入的特性——javac --release 8 对私有接口方法直接报「-source 8 中不支持 私有接口方法」,故严格限定 JDK8 时 D 项在语言层面并不存在;按 JDK9+ 口径四个选项才同时成立,答案不变) A. default 方法 B. static 方法 C. 普通抽象方法 D. private 方法

答:C。普通抽象方法仍只有声明无实现;default(JDK8)、static(JDK8)、private(JDK9)都可以有方法体。与第 J16 题连考。

【知识关联】

  • 同库关联:与 J17(default 方法)、J75(默认方法继承链)直接连考;与 J12(重写)、J18(访问修饰符)相关。
  • 实现层:接口方法默认 public abstract(JDK 7-);字段隐式 public static final。JDK 8+ 可有 default/static;JDK 9+ 可有 private 方法。抽象类可有实例字段与构造器。
  • 面试追问:① 何时选抽象类何时选接口?② 接口多实现 vs 类单继承如何影响设计?

【拓展延伸】

  • 变式问法:问接口能否有构造器/实例变量(不能);问抽象类能否全为具体方法。
  • 版本差异:JDK 7 及以前接口不能有实现;JDK 8 default/static;JDK 9 private interface methods,便于抽取公共逻辑。
  • 工程注意点:SPI/策略扩展优先接口;共享状态或模板方法用抽象类;避免「接口爆炸」,可用 default 提供向后兼容扩展(J17)。

J17 · 知识点:接口 default 方法 ​

【题目】关于 JDK8 接口的 default 方法,下列说法错误的是?

A. 可以有方法体 B. 实现类必须重写它 C. 一个类实现的两个接口若有同名 default 方法,该类必须重写以解决冲突 D. 它解决了接口扩展时的兼容性问题

答案:B

【考点】接口默认方法机制与“菱形冲突”解决规则。

【结论】选 B。实现类不是必须重写 default 方法,可以选择直接继承接口的默认实现。

【逐项辨析】

  • A 说法正确(非答案):default 方法的核心特征就是拥有方法体,提供默认实现。
  • B 说法错误(应选):实现类可以选择不重写 default 方法,直接继承默认实现即可。
  • C 说法正确(非答案):两个接口存在同名同参的 default 方法时产生菱形冲突,实现类必须重写以消除歧义。
  • D 说法正确(非答案):default 方法的设计初衷正是让接口平滑演进,旧实现类无需修改即可兼容新增方法。

【知识点】 接口 default 方法(JDK8 引入)打破了“接口不能有实现”的传统限制,使接口可以在不破坏现有实现类的前提下新增方法。菱形冲突解决规则:当类实现的多个接口存在同名同参的 default 方法时,编译器无法决定继承哪个,强制实现类必须重写;重写后可通过 接口名.super.方法名() 显式调用指定接口的默认实现。

static 方法属于接口本身,不继承,不产生冲突。类优先原则——若父类和父接口都有同名方法,类的方法优先于接口 default 方法被继承。

【记忆锚点】 「default 有默认,实现可继承;多接口同名冲突,必须重写摆平」。

【易混对比】

  • default vs abstract:default 有方法体、可选重写;abstract 无方法体、必须重写。
  • default vs static:default 方法可被实现类继承和重写;static 方法只属于接口,通过接口名直接调用,实现类不继承。
  • 换问法:若父类和父接口都有同名方法,子类调用时优先用哪个?——类的方法优先于接口 default 方法。

【自测】 以下代码能否编译通过?

java
interface A { default void f() { System.out.println("A"); } }
interface B { default void f() { System.out.println("B"); } }
class C implements A, B {}

答:不能。A 和 B 的 default 方法 f() 冲突,C 必须重写 f(),或在重写中通过 A.super.f() / B.super.f() 指定调用其中一个。与第 J17 题连考。

【知识关联】

  • 同库关联:与 J16、J75(实现类继承链中 default 冲突/类优先)、J12(可被重写)同一题群。
  • 实现层:default 方法进入接口的 vtable;菱形冲突时编译器强制实现类生成桥接/重写;Interface.super.m() 生成 invokespecial 指定调用。
  • 面试追问:① 类优先原则是什么?② 两个接口同名 default,实现类不重写会怎样?

【拓展延伸】

  • 变式问法:问「实现类必须重写 default」为何是错误说法(可选);问 static 接口方法是否被继承(否)。
  • 版本差异:JDK 8 引入;JDK 9 增加 private interface method;后续版本语义稳定。
  • 工程注意点:用 default 给老接口加方法时避免破坏二进制兼容;冲突时显式重写并文档化;不要用 default 塞业务逻辑,适合简单默认实现。

J18 · 知识点:访问修饰符 ​

【题目】以下访问修饰符按可见范围从大到小排列,正确的是?

A. protected > public > 默认 > private B. public > 默认 > protected > private C. public > protected > 默认(package-private)> private D. public > protected > private > 默认

答案:C

【考点】四个访问修饰符的可见性边界。

【结论】选 C。可见性从大到小为:public > protected > 默认(package-private)> private。

【逐项辨析】

  • C 正确:public 全开放;protected 同包 + 所有子类(含不同包子类);默认仅同包;private 仅本类。
  • B 错:protected 范围大于默认,因为 protected 允许不同包的子类访问,而默认不行。
  • A 错:public 范围最大,protected 不可能大于 public。
  • D 错:默认范围大于 private(同包类可访问),private 不可能排在默认之前。

【知识点】 Java 访问控制是封装(Encapsulation)的核心实现手段。四个修饰符的精确边界:

修饰符本类同包不同包子类不同包非子类
public✓✓✓✓
protected✓✓✓✗
默认✓✓✗✗
private✓✗✗✗

protected 对“不同包的子类”可见,但对“不同包的非子类”不可见——这是与默认修饰符的关键差异。内部类可以访问外部类的私有成员,这是编译器生成合成访问器方法实现的。

【记忆锚点】 「公保默私,范围递减;protected 跨包子类,默认寸步不离」。

【易混对比】

  • 换问法:protected 成员能否被同包非子类访问?——可以,protected 包含同包所有类,不限于子类。
  • 进阶考法:private 方法在子类中完全不可见,子类定义同名方法属于新方法,不构成重写。

【自测】 某类的 protected 方法,在另一个包中的非子类里能否访问?

答:不能。protected 对另一个包中的非子类不可见,此时与 private 效果相同。与第 J18 题连考。

【知识关联】

  • 同库关联:与 J12(重写访问权限不能更小)、J16(接口成员默认 public)相关。注:本库没有「private 与封装」专题题(J09 讲的是 final 关键字),别错引。
  • 实现层:访问控制在编译期检查;跨包 private 访问由编译器生成合成 accessor(NestHost/Nestmates,JDK 11+ 优化内部类私有访问)。
  • 面试追问:① protected 是否等于「子类可见」?② 为什么内部类能访问外部类 private?

【拓展延伸】

  • 变式问法:排序四修饰符;问同包非子类能否访问 protected(能)。
  • 版本差异:模块系统(JDK 9+)增加 module 导出约束,public 也未必跨模块可见。
  • 工程注意点:最小可见性原则;对外 API 谨慎 protected;跨模块库注意 module-info exports。

J19 · 知识点:static 关键字 ​

【题目】关于 static 关键字的说法,错误的是?

A. static 方法中不能直接访问非静态成员 B. static 变量属于类,所有实例共享 C. static 方法可以被重写 D. 静态代码块在类加载时执行

答案:C

【考点】static 成员的类归属、static 方法与重写的关系。

【结论】选 C。static 方法不能被重写,子类定义与父类同名同参的 static 方法属于“隐藏(hide)”。

【逐项辨析】

  • A 说法正确(非答案):static 方法没有 this 引用,只能直接访问静态成员;访问非静态成员需先创建对象。
  • B 说法正确(非答案):static 变量(类变量)在方法区存储,所有实例共享同一份数据。
  • C 说法错误(应选):static 方法不能被重写。重写是多态的基础,需要动态绑定;static 方法在编译期静态绑定。
  • D 说法正确(非答案):静态代码块在类加载的初始化阶段执行,且由于类只加载一次,静态代码块也只执行一次。

【知识点】 static 修饰的成员属于类而非实例。类加载时,静态变量分配内存并初始化(准备阶段赋默认值,初始化阶段赋真实值),静态代码块按声明顺序执行。static 方法通过类名调用,无 this 引用,因此不能访问非静态成员,也不能被重写。

子类对父类 static 方法的同名同参定义称为隐藏(hide),编译器根据引用类型决定调用版本。例如:

java
class A { static void f() { System.out.print("A"); } }
class B extends A { static void f() { System.out.print("B"); } }
A obj = new B();
obj.f(); // 输出 A,按引用类型 A 静态绑定

【记忆锚点】 「static 属类不属对象,无 this 不访问实例,方法不能重写只能藏」。

【易混对比】

  • 重写(Override)vs 隐藏(Hide):实例方法同名同参是重写,运行期动态绑定;static 方法同名同参是隐藏,编译期静态绑定。
  • 静态代码块 vs 实例代码块 vs 构造器:静态代码块类加载时执行一次;实例代码块每次构造前执行;构造器每次构造时执行。
  • 换问法:static 方法中能否使用 this 或 super?——不能,static 上下文不存在当前实例。

【自测】 以下代码输出什么?

java
class A { static void f() { System.out.print("A"); } }
class B extends A { static void f() { System.out.print("B"); } }
A obj = new B();
obj.f();

答:输出 A。static 方法隐藏按引用类型(A)静态绑定,不是重写。与第 J19 题连考。

【知识关联】

  • 同库关联:与 J12(实例方法重写)、J20(静态块执行顺序)、J63(方法区/元空间存 static)、J68(类加载初始化)强相关。
  • 实现层:static 方法无 this,invokestatic 静态绑定;子类同名同参 static 为 hide,按引用类型编译期选择。类变量在准备阶段赋零值、初始化阶段赋真实值。
  • 面试追问:① 重写与隐藏的本质区别?② 静态方法能否被反射动态调用?

【拓展延伸】

  • 变式问法:父引用指向子对象调用 static 方法输出父类版本;问静态块与 main 谁先执行(静态块在类初始化时)。
  • 版本差异:语义稳定;Java 无静态扩展方法(用工具类或 JDK 8 接口 static)。
  • 工程注意点:工具类 private 构造器 + static 方法;避免 static 可变全局状态(难测、线程不安全);测试中用 @StaticMock 要谨慎。

J20 · 知识点:初始化顺序 ​

【题目】执行 new B(); 时,下列代码的输出顺序是?

java
class A {
    static { System.out.print("A"); }
    { System.out.print("B"); }
    A() { System.out.print("C"); }
}
class B extends A {
    static { System.out.print("D"); }
    { System.out.print("E"); }
    B() { System.out.print("F"); }
}

A. ABDCEF B. ADBECF C. ADBCEF D. DABCEF

答案:C

【考点】类初始化顺序:静态块(父→子)→ 实例块+构造器(父→子)。

【结论】选 C。输出顺序为 ADBCEF。

【推导过程】

  1. 类加载阶段:先初始化父类 A,执行 A 的静态代码块,输出 A。
  2. 继续类加载阶段:初始化子类 B,执行 B 的静态代码块,输出 D。
  3. 实例化阶段:创建 B 的对象,先构造父类 A 的部分。执行 A 的实例代码块,输出 B;再执行 A 的构造器,输出 C。
  4. 实例化阶段:继续构造子类 B 的部分。执行 B 的实例代码块,输出 E;再执行 B 的构造器,输出 F。
  5. 综合顺序:A → D → B → C → E → F,即 ADBCEF。

【逐项辨析】

  • C ADBCEF 正确:按“先静态后实例,先父后子”规则推导。
  • B ADBECF 错:混淆了实例代码块与构造器的执行顺序。同一类中实例代码块总是在构造器之前执行。
  • A ABDCEF 错:它把父类的实例块与构造器(B、C)排到了子类静态块 D 之前。new B() 会先完成两个类的静态初始化(父类 A → 子类 D),之后才进入实例初始化(父类实例块 B → 父类构造 C → 子类实例块 E → 子类构造 F),所以 D 必须紧跟 A、排在 B 之前。
  • D DABCEF 错:子类静态初始化在父类静态初始化之后,D 不能在 A 之前。

【知识点】 Java 对象初始化遵循严格的顺序规则:

  1. 静态初始化(父 → 子):静态变量赋值 + 静态代码块,按源码顺序执行,类加载时仅执行一次;
  2. 实例初始化(父 → 子):实例变量赋值 + 实例代码块,按源码顺序执行,每次 new 都执行;
  3. 构造器(父 → 子):实例代码块之后执行。

口诀:“先静态后实例,先父后子”。静态成员随类加载而初始化;实例成员随对象创建而初始化。若构造器首行有 this(...),则先执行本类其他构造器,但最终仍需沿继承链完成父类构造。

【记忆锚点】 「先静后实,先父后子;静只一次,实每 new 一次」。

【易混对比】

  • 静态代码块 vs 实例代码块:静态在类加载时执行且仅一次;实例在每次构造对象前执行。
  • 父类构造器调用:子类构造器首行隐式 super() 确保父类实例代码块和构造器先于子类执行。
  • 换问法:若父类静态代码块中 new 了子类对象,输出顺序会更复杂——此时父类加载未完毕就会触发子类加载,但 JVM 保证每个类的初始化只进行一次。

【自测】 以下代码输出什么?

java
class A {
    static { System.out.print("1"); }
    { System.out.print("2"); }
    A() { System.out.print("3"); }
}
class B extends A {
    static { System.out.print("4"); }
    { System.out.print("5"); }
    B() { System.out.print("6"); }
    public static void main(String[] args) { new B(); new B(); }
}

答:1423562356。第一次 new B() 触发类加载(1、4)和实例化(2、3、5、6);第二次 new B() 类加载已完毕,只走实例化(2、3、5、6)。与第 J20 题连考。

【知识关联】

  • 同库关联:与 J14/J15(构造器链)、J19(静态块)、J01(默认值)、J24(常量池,static final 编译期)构成初始化全景。
  • 实现层:类初始化 <clinit> 合并静态字段与静态块,按文本顺序;实例初始化在 <init> 中:父类实例块/构造 → 子类实例块/字段 → 子类构造器。
  • 面试追问:① 子类静态块与父类静态块谁先?② 构造器中调用重写方法会发生什么?

【拓展延伸】

  • 变式问法:给出多层继承 + 静态/实例块代码问完整输出顺序。
  • 版本差异:语义稳定;record 的紧凑构造器简化字段校验。
  • 工程注意点:避免在构造器中调用可重写方法(子类字段可能尚未赋值);复杂初始化用 @PostConstruct(Spring S16)或工厂方法。

J21 · 知识点:instanceof 与类型转换 ​

【题目】执行 String s = "a"; Object o = s;,下列说法正确的是? A. 代码运行时会报错 B. o instanceof Object 为 false C. 直接通过 o 调用 String 的特有方法,无需强转 D. o instanceof String 为 true

答案:D

【考点】instanceof 基于运行时类型判断;向上转型自动完成。

【结论】选 D。向上转型后,o 的运行时类型仍是 String,因此 o instanceof String 为 true。

【逐项辨析】

  • D正确:o 的运行时实际对象是 String 实例,instanceof 基于运行时类型判断,结果为 true。
  • B错:String 是 Object 的子类,o 的运行时类型是 String,故 o instanceof Object 也为 true。
  • C错:o 的编译期类型是 Object,只能调用 Object 声明的方法;String 特有方法(如 substring() 等)不能直接调用,需向下强转 (String) o。
  • A错:向上转型是 Java 合法语法,自动完成且安全,不会报错。

【知识点】 instanceof 是运行时类型检查运算符,判断对象是否为某类或其子类的实例。向上转型(Upcasting)将子类引用赋给父类引用,自动完成,安全;向下转型(Downcasting)将父类引用赋给子类引用,需显式强转,不安全,可能抛出 ClassCastException。

Java 16 起引入模式匹配 instanceof:if (o instanceof String s) 可直接在条件成立时使用 s,无需额外强转。类型转换的原则:编译看引用类型,运行看实际类型。null instanceof AnyClass 永远为 false,因为 null 不是任何类的实例。

【记忆锚点】 「instanceof 看实际,向上自动向下强,模式匹配省强转」。

【易混对比】

  • instanceof vs isInstance():instanceof 是运算符;Class.isInstance() 是反射方法,效果等价但适用场景不同。
  • 编译期检查:若编译器能确定某引用与某类型无继承关系(如 "abc" instanceof Integer),直接编译报错。
  • 换问法:null instanceof String 结果是什么?——false,null 不是任何类的实例。

【自测】 以下代码能否编译通过?

java
Object o = "hello";
Integer i = (Integer) o;

答:能编译通过(编译期只看引用类型 Object,可以转为 Integer),但运行时报 ClassCastException,因为实际对象是 String,不能转为 Integer。与第 J21 题连考。

【知识关联】

  • 同库关联:与 J13(多态)、J12(重写)、J71(泛型擦除后无法 instanceof 泛型参数)直接相关;与 J22(equals)在对象比较题群相邻。
  • 实现层:instanceof 在字节码为 instanceof 指令,运行期查类继承关系;向下转换 checkcast 失败抛 ClassCastException。
  • 面试追问:① 子类 instanceof 父类为何为 true?② 接口类型 instanceof 的规则?

【拓展延伸】

  • 变式问法:null instanceof X 恒为 false;问强制转换失败时机(运行期)。
  • 版本差异:JDK 16+ 正式引入 pattern matching for instanceof(if (obj instanceof String s)),减少显式强转。
  • 工程注意点:优先模式匹配;避免过度向下转型(可能破坏多态);集合泛型用通配符减少强转。

J22 · 知识点:equals 与 hashCode ​

【题目】重写 equals() 时必须重写 hashCode(),最根本的原因是? A. 只是为了提高性能 B. 否则代码无法编译 C. 保证相等的对象在 HashSet/HashMap 中哈希值一致,否则 equals 相等却可能无法去重、无法取出 D. 两者没有关系

答案:C

【考点】equals/hashCode 约定(约定一:equals 相等的对象 hashCode 必须相等)及其在哈希集合中的作用。

【结论】选 C。重写 equals 必须重写 hashCode,以保证相等对象在 HashSet/HashMap 等哈希集合中行为正确。

【逐项辨析】

  • B错:不重写 hashCode 不会导致编译错误,代码可以编译通过,但运行时在哈希集合中行为异常。
  • C正确:HashSet/HashMap 先按 hashCode 定位桶,再用 equals 比较;若 equals 相等但 hashCode 不同,对象会被放入不同桶,导致去重失败、get 取不到值。
  • A错:hashCode 的主要目的不是提高性能,而是保证对象在哈希表中的正确性;良好的 hashCode 分布才能提升性能。
  • D错:equals 和 hashCode 有严格约定关系,equals 相等则 hashCode 必须相等(反之不成立)。

【知识点】 Java 规范对 hashCode 有三条约定:

  1. 同一对象多次调用 hashCode 应返回相同值(对象未修改时);
  2. 两对象 equals 相等,则 hashCode 必须相等;
  3. equals 不等,hashCode 不要求必须不等(但不相等可提高哈希表性能)。

哈希集合(HashSet、HashMap、LinkedHashSet 等)的工作流程:先计算 hashCode 定位数组桶索引,再在桶内(链表 / 红黑树)用 equals 逐一比较。违反约定会导致:equals 相等的对象被存入不同桶,Set 无法去重,Map 用 key 取不到值。IDE 和 Lombok 的 @EqualsAndHashCode 可自动生成符合规范的实现。

【记忆锚点】 「equals 相等 hashCode 必等,hashCode 是桶号 equals 是门牌」。

【易混对比】

  • == vs equals:== 比较引用地址;equals 默认行为是 ==,但 String、包装类等已重写为内容比较。
  • hashCode 实现:默认基于对象内存地址(Object.native 方法);重写时应使用 equals 中参与比较的全部字段计算,保证一致性。
  • 换问法:只重写 hashCode 不重写 equals 会有问题吗?——也会有,因为 equals 仍按 == 比较,逻辑相等的对象可能不被视为相等。

【自测】 以下代码中 HashSet 的元素个数是多少?

java
class Person {
    String name;
    Person(String name) { this.name = name; }
    public boolean equals(Object o) {
        return o instanceof Person && ((Person)o).name.equals(name);
    }
    // 未重写 hashCode
}
Set<Person> set = new HashSet<>();
set.add(new Person("A"));
set.add(new Person("A"));
System.out.println(set.size());

答:2。两个 Person 的 name 都是 "A",equals 为 true,但继承自 Object 的 hashCode 基于地址计算,两个 new 出来的对象地址不同,hashCode 不同,被放入不同桶,Set 无法去重。与第 J22 题连考。

【知识关联】

  • 同库关联:与 J34(HashMap 依赖 hashCode/equals)、J37(HashSet)、J28(Integer equals)构成相等性题群;与 P05(is 与 ==)对照。
  • 实现层:HashMap 先比 hash 再 equals;重写 equals 必须重写 hashCode 以保证「相等对象相同 hash」契约。
  • 面试追问:① 为何重写 equals 必须重写 hashCode?② == 与 equals 分别比较什么?

【拓展延伸】

  • 变式问法:两个内容相同的 new String("a") 的 == 与 equals;或作为 HashMap key 时的查找失败场景。
  • 版本差异:JDK 7+ Objects.equals/null-safe;record 自动生成基于组件的 equals/hashCode。
  • 工程注意点:用 Objects.hash;可变对象作 key 有风险;调试时断点看 equals 契约。

持续学习,持续积累。