1. 什么是面向对象?说说封装、继承、多态的本质

面向对象(OOP)以对象为中心组织代码,把数据和行为绑定在一起。三大特性是面试必问,关键在于讲出"本质"而不只是定义:

多态演示 — 面向接口编程
interface Animal { void speak(); }

class Dog implements Animal {
    public void speak() { System.out.println("汪汪"); }
}
class Cat implements Animal {
    public void speak() { System.out.println("喵喵"); }
}

// 编译时类型是 Animal,运行时实际调用 Dog/Cat 的方法 → 运行时多态
Animal a = new Dog();
a.speak();  // 输出:汪汪
a = new Cat();
a.speak();  // 输出:喵喵

🎯 面试要点

  • 多态的三个必要条件:继承/实现、方法重写、父类引用指向子类对象
  • 多态的好处:代码可扩展,新增类型无需修改调用方(开闭原则)
  • Java 中多态依赖"动态绑定"(虚方法表),final/static/private 方法无法被重写,属于静态绑定

2. 抽象类和接口的区别?(必考)

JDK 8 之后接口可以拥有默认方法和静态方法,两者界限变模糊,但设计语义依然不同:

选择准则:有"is-a"关系且需要共享状态/代码 → 抽象类;定义能力契约、需要多实现、面向扩展 → 接口。JDK8 后接口有 default 方法,很多"模板方法"场景也可以用接口 + 默认方法实现。

接口的默认方法 & 静态方法
interface Flyable {
    void fly();                                  // 抽象方法
    default void hover() {                     // 默认方法:实现类可选择性重写
        System.out.println("悬停");
    }
    static Flyable none() {                     // 静态方法:接口名直接调用
        return () -> {};
    }
}

🎯 面试要点

  • 接口字段默认是 public static final;JDK8 起接口方法可加 default 实现
  • 一个类实现两个接口,若默认方法冲突,必须重写解决
  • 抽象类本质还是类:有构造方法(供子类 super 调用)、可以有 static/实例代码块

3. 重载(Overload)和重写(Override)的区别?

重写与重载对比
class Parent {
    protected Number calc(int a) throws IOException { ... }
}
class Child extends Parent {
    // 重写:权限不更严格 ✓ 协变返回 Integer ✓ 异常更具体/不抛 ✓
    @Override
    protected Integer calc(int a) { ... }

    // 重载:同名不同参数(与父类方法无关)
    public double calc(int a, double b) { ... }
}

🎯 面试要点

  • 重载看"参数",重写看"签名+权限+异常",两者判断依据完全不同
  • 重写必须加 @Override 吗?不必需,但强烈建议——编译器可校验签名
  • 常见陷阱:构造方法不能重写;重写后调用用的是"运行时类型"(动态分派)

4. 内部类有哪几种?为什么局部内部类访问局部变量必须是 final 的?

四种内部类:

为什么局部变量必须 final(或 effectively final)?因为局部变量在方法栈上,方法结束后即销毁;而内部类对象可能被引用到方法外继续存活。Java 的做法是把局部变量的副本捕获进内部类对象。若变量可变,副本与原变量就不一致了,产生数据同步问题。JDK8 起放宽为 effectively final(只要不重新赋值即可,编译器自动补 final)。

局部变量捕获机制
public Runnable createTask(final int id) {
    // 编译器将 id 的副本传入内部类,两份值必须一致 → 要求 final
    return new Runnable() {
        public void run() {
            System.out.println("task-" + id);   // 操作的是捕获的副本
        }
    };
}

🎯 面试要点

  • 静态内部类 vs 成员内部类:前者不持有外部引用,可独立创建,更省内存
  • 匿名内部类在 JDK8 后优先用 Lambda 替代(更简洁)
  • 内部类加载不依赖外部类,但内部类编译后是独立的 .class 文件(Outer$Inner.class)

5. 类的初始化顺序(经典陷阱题)

创建一个子类对象时,执行顺序为:

  1. 父类静态成员/静态代码块(按代码顺序,只执行一次)
  2. 子类静态成员/静态代码块
  3. 父类实例成员/实例代码块 → 父类构造方法
  4. 子类实例成员/实例代码块 → 子类构造方法

口诀:先静态后实例、先父后子;静态只加载一次,实例每次 new 都执行。

运行下面的代码,输出什么?
class A {
    static { System.out.print("A-static "); }
    { System.out.print("A-instance "); }
    A() { System.out.print("A-ctor "); }
}
class B extends A {
    static { System.out.print("B-static "); }
    { System.out.print("B-instance "); }
    B() { System.out.print("B-ctor"); }
}
new B();
new B();

输出:A-static B-static A-instance A-ctor B-instance B-ctor A-instance A-ctor B-instance B-ctor

🎯 面试要点

  • 静态块只在类首次加载时执行一次(第二次 new 不再执行)
  • 实例代码块在构造方法之前执行(编译期会被拷贝进每个构造方法开头)
  • 子类构造方法第一行会隐式调用 super(),所以父类构造一定先于子类

6. 为什么重写 equals 必须同时重写 hashCode?

hashCode 与 equals 的约定:

原因:HashMap/HashSet 查找时先算 hashCode 定位桶,再用 equals 比较桶内元素。若只重写 equals 不重写 hashCode,两个相等对象会分到不同桶,导致 contains 找不到、set 中允许重复。反之 hashCode 相同但 equals 不同则无碍(只是冲突变多)。

标准写法(IDEA 可自动生成)
public class User {
    private String name;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User u = (User) o;
        return age == u.age && Objects.equals(name, u.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

🎯 面试要点

  • String 是重写 equals 的经典例子(比较内容而非地址)
  • HashMap 的 key 必须是不可变的(变则丢失元素),String/Integer 天然合适
  • 面试延伸:equals 与 == 的区别 → == 比较引用地址(基本类型比较值)

🎤 常见面试追问

  1. 多态的底层原理是什么?——运行时动态绑定:JVM 通过虚方法表(vtable)在运行时根据对象实际类型找方法入口;final/static/private 方法静态绑定。
  2. 接口和抽象类,为什么 JDK8 后还要区分?——语义不同:抽象类表达"是什么"(可共享状态/代码);接口表达"能做什么"(能力契约、可多实现)。设计时按意图选。
  3. 两个接口默认方法冲突怎么解决?——实现类必须重写该方法(或指定调用 super 接口的默认方法)。
  4. HashMap 里放自定义对象做 key 要注意什么?——必须正确重写 equals 和 hashCode,且对象不可变(字段不变),否则 key 变了找不到。
  5. 初始化顺序里,static 块和实例块各执行几次?——静态块类加载时执行一次;实例块每次 new 都执行(构造器前)。

📖 名词解释(本页术语)

术语 大白话解释
封装把数据(属性)和操作数据的方法包在类里,属性用 private 藏起来,外部只能通过方法操作——像银行的 ATM 只暴露插卡口,不暴露保险库。
继承子类 extends 父类,自动拥有父类的属性和方法,表达"is-a"关系(猫是一种动物)。
多态同一个方法调用在不同对象上有不同表现:父类引用指向子类对象,调用的却是子类的方法(Animal a = new Dog(); a.speak() 叫出汪汪)。
抽象类不能 new、专门被继承的类,可以含抽象方法(子类必须实现)和普通方法。表达"是什么"。
接口能力契约:只声明"能做什么",实现类负责实现。可多实现。JDK8 起可含 default 方法。
重载(Overload)同类中同名方法、参数列表不同,编译时根据参数选哪个版本。
重写(Override)子类用相同签名覆盖父类方法,运行时多态的基础,需 @Override 校验。
内部类定义在类内部的类。静态内部类不依赖外部实例;成员/局部/匿名内部类依赖外部实例,匿名类可用 Lambda 替代。
hashCode对象的"哈希值",HashMap 用它定位存储桶。约定:equals 相等 → hashCode 必须相等(反之不必)。
实例代码块 / 静态代码块{ } 是实例块(每次 new 执行);static { } 是静态块(类加载执行一次)。
⚠️ 本页面由 AI 生成,内容仅供参考,请以官方文档和实际源码为准。