Java 面向对象基础初步

面向对象的核心概念概览

面向对象的核心目标是 把数据和操作封装在一起(对象),并通过抽象、继承与多态组织程序。简而言之,我们总是没法回避程序设计的四个话题:

  • 封装(Encapsulation):隐藏内部状态,仅通过方法暴露行为。
  • 继承(Inheritance):子类复用父类代码并扩展行为。
  • 多态(Polymorphism):通过父类型引用调用子类实现,实现运行时多种行为。
  • 抽象(Abstraction):只暴露必要接口,忽略实现细节。

类与对象:声明、字段、方法、构造器

最基本的类

public class Person {
    // 字段(成员变量)
    private String name;
    private int age;

    // 构造器
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 方法(行为)
    public String getName() { return name; }
    public int getAge() { return age; }
    public void setAge(int age) {
        if (age < 0) throw new IllegalArgumentException("age must be >= 0");
        this.age = age;
    }

    public void sayHello() {
        System.out.println("Hi, I'm " + name + ", " + age + " years old.");
    }
}

创建对象(实例化)

Person p = new Person("Alice", 30);
p.sayHello(); // Hi, I'm Alice, 30 years old.

字段类型

  • 实例字段(每个对象一份)
  • 类字段(static,类共享一份)
  • 局部变量(方法内部)

​ 上述讨论内容,我们基本上就确定了一个类。

封装与访问控制

封装的实践主要靠访问修饰符与方法(getter/setter)来实现。

访问修饰符

  • private:仅类内可见(最严格)
  • (package-private)(默认): 同包可见
  • protected:同包或子类可见
  • public:全局可见

最佳实践:默认 private,只对外暴露必要的 public/protected API。保留最小可见性以降低耦合与错误使用风险。

不可变对象

不可变类(immutable)能降低并发复杂度和调试难度。常见做法:

  • 所有字段 private final
  • 不提供 setter
  • 若包含可变对象,返回防御性拷贝或不可变视图

示例不可变类:

public final class Point {
    private final int x;
    private final int y;
    public Point(int x, int y) { this.x = x; this.y = y; }
    public int getX() { return x; }
    public int getY() { return y; }
}

继承与 extends

继承让子类获得父类(超类)的字段和方法。Java 支持单继承(一个类只能有一个直接父类),但允许实现多个接口。

public class Employee extends Person {
    private String id;
    public Employee(String name, int age, String id) {
        super(name, age); // 调用父类构造器
        this.id = id;
    }
    public String getId() { return id; }
}

方法重写(Override)

@Override 注解明确你在重写:

@Override
public void sayHello() {
    System.out.println("Employee " + getName() + " (id=" + id + ")");
}

super 关键字

  • super(...) 调用父类构造器(必须放在构造器首行)
  • super.method() 调用父类方法实现

小心滥用继承

继承表示“is-a”关系(例如 Employee is-a Person)。如果只是为了代码复用而继承往往是错误的,优先考虑组合(has-a)。


多态与动态绑定

多态允许你用父类型引用指向子类型对象,并在运行时调用子类实现。

Person p = new Employee("Bob", 28, "E001");
p.sayHello(); // 调用 Employee 中的覆盖版本(动态绑定)

关键点:

  • 决定调用哪个方法的,是运行时对象的实际类型,而不是引用类型(动态绑定)。
  • 编译时只能访问引用类型可见的方法/字段;不能访问子类特有的方法除非进行类型转换(cast)。

类型转换示例:

if (p instanceof Employee) {
    Employee e = (Employee) p;
    System.out.println(e.getId());
}

抽象类与接口

抽象类 (abstract)
  • 可以包含抽象方法(没有实现)和具体方法(有实现)。
  • 适合需要共享实现与状态的场景。
public abstract class Animal {
    public abstract void sound();
    public void breathe() { System.out.println("breathing..."); }
}
接口 (interface)
  • 定义行为契约。Java 8+ 接口可以包含 default 方法与 static 方法,Java 9+ 有私有方法。
  • 接口更倾向于描述能力(capability),支持多重实现(多态性更强)。
public interface Flyable {
    void fly();
    default void land() { System.out.println("landing"); }
}
何时用抽象类 vs 接口
  • 需要字段或构造器时用抽象类。
  • 希望多个不相关类实现同一行为时用接口。
  • 优先接口(更灵活),必要时用抽象类提供共享实现。

组合(Composition)与委托(Delegation)

组合优先于继承(composition over inheritance)。将功能拆成小组件并组合能降低耦合。

public class Engine { void start() { System.out.println("engine on"); } }

public class Car {
    private final Engine engine;
    public Car(Engine engine) { this.engine = engine; }
    public void start() { engine.start(); } // 委托给 Engine
}

组合的优点:

  • 更灵活(运行时替换组件)
  • 避免子类层次膨胀
  • 更易单元测试(mock 组件)

static, final, 常量与工具类

static

  • 静态字段/方法属于类,不属于实例。
  • 常用于工具方法(Math)、单例模式中的单例引用或缓存。
public class Utils {
    public static int max(int a, int b) { return a > b ? a : b; }
}

final

  • 修饰变量:值不可变(对于引用类型,引用不可变但对象可变)。
  • 修饰方法:禁止子类重写。
  • 修饰类:禁止继承(如 String)。

equals, hashCode, toString 的正确实现

规范摘要
  • equals 应满足自反、对称、传递、一致性和对 null 返回 false
  • 若重写 equals,必须同时重写 hashCode(相等对象必须有相同的 hashCode)。
  • toString 用于调试输出,应简洁明了。
使用示例:IDE / Objects 帮助生成
public class Point {
    private final int x, y;
    public Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point)) return false;
        Point p = (Point) o;
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return 31 * x + y; // 简单组合
    }

    @Override
    public String toString() {
        return "Point(" + x + "," + y + ")";
    }
}
常见错误
  • equals 中用 getClass() 校验时会阻止子类与父类比较(这可能是期望行为,也可能不是)。使用 instanceof 更通用,但需注意 LSP(里氏替换)。
  • 把可变字段用于 hashCode:若对象作为 HashSet key,修改这些字段会破坏 HashMap 的语义。

对象创建、初始化顺序与垃圾回收简介

创建与初始化顺序(类/实例)
  • 类加载时静态字段和静态初始化块按出现顺序执行。
  • 每次创建实例时:父类构造器先被调用,字段初始化与实例初始化块然后按顺序执行,最后子类构造器体执行。

示例顺序(伪):

  1. 父类静态初始化(类第一次加载)
  2. 子类静态初始化
  3. 创建实例:父类实例字段初始化 -> 父类构造器 -> 子类实例字段初始化 -> 子类构造器
垃圾回收(GC)

Java 使用自动垃圾回收(JVM 实现多样)。要点:

  • 不要依赖 finalizer (finalize() 已被废弃,不推荐使用)。
  • 优先控制对象生命周期、减少临时分配以降低 GC 压力。
  • 使用内存分析工具(VisualVM、jmap、jstat、YourKit)诊断内存泄漏/对象分配热点。

更多推荐