Featured image of post Java|2.类

Java|2.类

AI 辅助信息

  • 使用工具:Gemini 2.5 Pro

对于 C++ 开发者而言,Java 的 class 语法会带来一种强烈的既视感。然而,在这相似的外表之下,隐藏着截然不同的设计哲学和实现机制。理解这些核心差异,是真正掌握 Java 面向对象的关键。

内存管理:自动 vs. 手动

这是两者最根本、最重要的区别。

  1. C++: 采用手动内存管理。开发者通过 new 在堆上创建对象,必须在适当的时候通过 delete 手动释放。

    忘记释放会导致内存泄漏,提前释放则会导致悬挂指针。

    类的析构函数 (~ClassName()) 在对象销毁时被调用,用于资源回收。

    1
    2
    3
    4
    5
    
    void cpp_function() {
        MyClass* obj = new MyClass(); // 手动分配
        // ... 使用 obj ...
        delete obj; // 必须手动释放
    } // 如果不 delete,就会内存泄漏
    
  2. Java: 采用自动垃圾回收 (Garbage Collection, GC)。

    开发者只管用 new 创建对象,完全不用关心何时释放。

    JVM 的垃圾回收器会自动追踪不再被引用的对象,并回收其内存。

    因此,Java 中没有析构函数的概念。

    1
    2
    3
    4
    
    public void javaMethod() {
        MyClass obj = new MyClass(); // 只管创建
        // ... 使用 obj ...
    } // 方法结束后,如果 obj 不再被引用,GC 会在未来某个时间自动回收它
    

对象模型:引用 vs. 值与指针并存

  1. C++: 对象可以作为值存在于栈上,也可以作为指针指向堆上的实例。

    这两种方式在语法和行为上有明显区别。

    1
    2
    
    MyClass stackObj; // 值类型,对象在栈上
    MyClass* heapObj = new MyClass(); // 指针类型,对象在堆上
    
  2. Java: 除了基本数据类型 (int, double 等),一切皆为引用。

    你声明一个对象变量,它存储的永远是指向堆上对象的“引用”(可以理解为安全的指针)。

    你永远无法在栈上直接创建一个对象实例。

    1
    2
    
    MyClass objRef; // 只是一个引用,值为 null
    objRef = new MyClass(); // 在堆上创建对象,让引用指向它
    

这个区别导致了参数传递的不同:Java 的对象参数传递,本质上是引用(地址)的按值传递。

Java 构造器 vs. C++ 构造函数

对于 C++ 开发者来说,Java 的构造器在概念上很熟悉,但在实现机制和语法上存在一些关键差异。

初始化方式

  • Java: 成员变量的初始化通常在构造器的 {} 方法体内通过赋值完成,或者在声明时直接赋初值。Java 没有 C++ 那样的“成员初始化列表”。

    1
    2
    3
    4
    5
    6
    7
    
    class MyClass {
        private int value;
    
        public MyClass(int v) {
            this.value = v; // 在方法体内赋值初始化
        }
    }
    
  • C++ 对比: C++ 推荐使用成员初始化列表 (: member(value)),这种方式是直接初始化,而非赋值,通常效率更高。

继承中的调用

  • Java: 子类构造器必须在第一行调用父类的构造器。

    • 通过 super(参数列表) 显式调用父类构造器。
    • 如果不显式调用,编译器会自动插入一个 super(),即调用父类的无参构造器。
    • 如果父类没有无参构造器,则子类必须显式调用 super() 并传入所需参数,否则编译失败。
    1
    2
    3
    4
    5
    6
    
    class SubClass extends SuperClass {
        public SubClass(int n) {
            super(n); // 必须是构造器的第一条语句
            // ...
        }
    }
    
  • C++ 对比: C++ 在子类构造函数的成员初始化列表中调用父类构造函数,语法为 SubClass() : SuperClass(params) {}

析构与内存管理

  • Java: Java 没有析构函数。对象的内存由垃圾回收器(GC)自动管理和回收,开发者无需关心。构造器只负责对象的创建和初始化。

  • C++ 对比: C++ 拥有析构函数 ~ClassName(),它在对象生命周期结束时自动调用,是手动管理资源(如内存、文件句柄等)和 RAII 模式的核心。

默认构造器

  • Java: 如果你没有定义任何构造器,编译器会为你生成一个公开的(public)、无参数的默认构造器。但只要你定义了任何一个构造器,编译器就不再自动提供。

  • C++ 对比: 规则与 Java 基本相同,这也是两者的一个共同点。

构造器间的调用(委托构造)

  • Java: 使用 this(参数列表) 可以在一个构造器中调用同一个类的另一个重载构造器。与 super() 类似,this() 也必须是构造器中的第一条语句。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    class Box {
        private int width, height;
    
        public Box(int width) {
            this(width, 0); // 调用下面的构造器
        }
    
        public Box(int width, int height) {
            this.width = width;
            this.height = height;
        }
    }
    
  • C++ 对比: C++11 引入了类似的功能,称为委托构造函数,语法与调用父类构造函数类似,也是在成员初始化列表中完成 Box(int width) : Box(width, 0) {}

继承:单继承 vs. 多继承

  1. C++: 支持多重继承,一个类可以同时继承多个父类。

    这虽然灵活,但也带来了菱形继承等复杂问题。

  2. Java: 只支持单继承,一个类最多只能有一个直接父类 (extends)。

    为了弥补多继承的缺失,Java 引入了接口 (interface),一个类可以实现 (implements) 任意多个接口,以此来获得多种行为能力。这种“单继承,多实现”的模式被认为是更安全、更清晰的设计。

继承 (Inheritance) - extends

核心思想:“是一个” (is-a) 的关系

继承体现的是一种“一般到特殊”的关系。子类(Subclass)继承父类(Superclass),意味着子类是一种特殊的父类。

1
2
Dog is an Animal.
Car is a Vehicle.

当一个类 extends 另一个类时,它会自动获得父类所有非 private 的属性和方法。

主要用途:

  1. 代码复用:子类可以直接使用父类的代码,无需重复编写。

  2. 建立类型层次结构:形成一个清晰的、从抽象到具体的分类体系。

语法和示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 父类
class Animal {
    String name;

    public void eat() {
        System.out.println("This animal eats food.");
    }
}

// 子类继承父类
class Dog extends Animal {
    // Dog 自动拥有了 name 属性和 eat() 方法

    // 子类可以有自己的方法
    public void bark() {
        System.out.println("Woof!");
    }

    // 子类可以重写 (Override) 父类的方法
    @Override
    public void eat() {
        System.out.println("This dog eats dog food.");
    }
}

关键限制:单继承

Java 为了避免 C++ 中多重继承带来的复杂性和混乱(如“菱形继承”问题),规定一个类最多只能 extends 一个父类。你不能写 class Dog extends Animal, Mammal

接口 (Interface) - implements

核心思想: “能做什么” (can-do) 的契约

接口定义的是一组行为规范或能力契约。它不关心“你是什么”,只关心“你能做什么”。如果一个类 implements 一个接口,它就承诺自己具备这个接口所定义的所有能力(方法)。

1
2
3
A Bird can Fly.
A Car can Move.
A USB_Flash_Drive can Read and Write.

接口中只包含抽象方法(没有方法体)和常量。实现接口的类必须为所有抽象方法提供具体的实现。

主要用途:

  1. 定义标准/规范:为一组不相关的类提供一个共同的行为标准。例如,Comparable 接口定义了对象之间如何比较大小。

  2. 实现多态:让不同的类可以通过同一个接口类型来引用,实现灵活的程序设计。

  3. 解耦:面向接口编程是实现高内聚、低耦合系统设计的核心原则。

语法和示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 1. 定义一个“可播放”的接口 (行为规范)
// 这个接口规定,任何可播放的东西,都必须有一个 play() 方法。
interface Playable {
    void play(); // 抽象方法,没有方法体
}

// 2. 创建第一个实现类:Music
// Music 是一个 Playable 的东西,所以它实现了 Playable 接口。
class Music implements Playable {
    private String songName;

    public Music(String songName) {
        this.songName = songName;
    }

    // 必须提供 play() 方法的具体实现
    @Override
    public void play() {
        System.out.println("正在播放音乐: " + this.songName);
    }
}

// 3. 创建第二个实现类:Video
// Video 也是一个 Playable 的东西。
class Video implements Playable {
    private String movieTitle;

    public Video(String movieTitle) {
        this.movieTitle = movieTitle;
    }

    // 同样,必须提供 play() 方法的具体实现
    @Override
    public void play() {
        System.out.println("正在播放视频: " + this.movieTitle);
    }
}

// 4. 创建一个媒体播放器类
public class MediaPlayer {

    // 关键点:这个方法接受的参数是接口类型 Playable,
    // 而不是具体的 Music 或 Video 类型。
    // 这意味着它可以接收任何实现了 Playable 接口的对象。
    public void startPlayback(Playable media) {
        System.out.println("准备播放...");
        // 通过接口调用方法。
        // JVM 会在运行时判断 media 变量实际指向的是哪个对象 (Music 还是 Video),
        // 然后调用那个对象自己的 play() 方法。这就是“多态”。
        media.play();
        System.out.println("播放结束。");
        System.out.println("--------------------");
    }

    // 主程序入口
    public static void main(String[] args) {
        // 创建一个播放器实例
        MediaPlayer player = new MediaPlayer();

        // 创建具体的媒体对象
        Playable myMusic = new Music("《晴天》- 周杰伦");
        Playable myVideo = new Video("《星际穿越》");

        // 将不同的对象传入同一个方法
        player.startPlayback(myMusic); // 传入 Music 对象
        player.startPlayback(myVideo); // 传入 Video 对象
    }
}

如果多个接口定义了一样的函数怎么办?

比如,我有一个 Starfield 类,它impelements了接口 Game 和 Artwork,里面都定义了play(),那会怎么样?

  1. 当多个接口有完全相同的方法签名时(方法名、参数、返回类型都一样),实现类只需要提供一个公共的实现。

    这个单一的实现会同时满足所有相关接口的“契约”。

    无论你通过哪个接口的引用来调用这个方法,最终执行的都是子类中那唯一的实现。

  2. 方法名相同,但参数列表不同。

    这种情况完全没有问题,它就是方法重载 (Method Overloading)。

    你的类只需要把这两个方法都实现一遍就行。

  3. 方法名和参数列表都相同,但返回类型不同

    这种情况是绝对不允许的,会导致编译错误。

    一个类不可能同时满足这两个接口的契约,因为它无法定义出两个只有返回类型不同的同名方法。

虚函数:默认 vs. 显式

  1. C++: 只有被 virtual 关键字修饰的成员函数才是虚函数,才能在子类中被重写 (override) 并实现多态。

    普通成员函数是静态绑定的。

  2. Java: 所有非 static、非 private 的方法默认都是虚函数。

    你不需要(也不能)使用 virtual 关键字。这意味着在 Java 中,多态是默认行为,使用起来更简单直接。

头文件:无 vs. 有

  1. C++: 采用声明与实现分离的模式

    类的声明通常放在头文件 (.h 或 .hpp) 中,实现放在源文件 (.cpp) 中

  2. Java: 没有头文件的概念。

    类的声明和实现都必须在同一个 .java 文件中。编译器会自动处理类之间的依赖关系。

泛型 vs. 模板

  1. C++ 模板 (Templates)

    是一个强大的编译时元编程工具。编译器会为每一种用到的具体类型生成一份独立的代码。

    例如 vector 和 vector 在编译后是两个完全不同的类。

  2. Java 泛型 (Generics)

    主要是一个编译时类型安全检查机制。

    它通过类型擦除 (Type Erasure) 实现。

    在编译后,所有泛型信息(如 ArrayList 中的 String)都会被擦除,变回原始类型(如 ArrayList)。

    这意味着在运行时,ArrayList 和 ArrayList 是同一个类。

权限管控

Java 通过四个访问控制修饰符来设定类、接口、方法和变量的访问权限,这是实现封装的关键机制。它决定了哪些代码可以访问到你的代码。

访问权限从最宽松到最严格,依次是:public > protected > default > private。

public (公开的)

可见范围:任何地方。

说明:被 public 修饰的成员,可以被任何其他类从任何包中访问。这是最开放的访问级别。

应用场景:通常用于定义一个类的公共 API(应用程序编程接口),即你希望外部世界与之交互的方法和常量。

1
2
3
4
5
6
7
8
9
package com.mycompany.app;

public class Calculator {
    public static final double PI = 3.14159; // 公开的常量

    public int add(int a, int b) { // 公开的方法
        return a + b;
    }
}

protected (受保护的)

可见范围:

  1. 同一个包 (package) 内的所有类。

  2. 不同包中的子类 (subclass)。

说明:这个修饰符主要用于继承体系。它允许子类访问父类的某些实现细节,但又不希望这些细节对外部世界完全公开。

应用场景:当你想让一个方法或变量只被其子类使用或重写时。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 在 com.mycompany.app 包中
public class Shape {
    protected void calculateArea() { // 受保护的方法
        System.out.println("Calculating area...");
    }
}

// 在 com.anothercompany.tools 包中
import com.mycompany.app.Shape;

public class Circle extends Shape {
    public void draw() {
        // 可以访问父类的 protected 方法,因为 Circle 是 Shape 的子类
        calculateArea();
    }
}

default (默认/包私有)

可见范围:仅限同一个包 (package) 内。

说明:如果你不写任何访问修饰符(既不是 public,也不是 protected 或 private),那么它就是 default 访问级别。这种成员对于包外的任何类(包括子类)都是不可见的。

应用场景:当你希望某些类或成员只作为包内部的辅助工具,不希望被外部包直接使用时。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 在 com.mycompany.app 包中
class Helper { // default 级别的类,包外不可见
    void assist() { // default 级别的方法
        System.out.println("Assisting...");
    }
}

public class Main {
    public void run() {
        Helper helper = new Helper(); // 在同一个包内,可以访问
        helper.assist();
    }
}

private (私有的)

可见范围:仅限同一个类 (class) 内部。

说明:这是最严格的访问级别。被 private 修饰的成员,即使是同一个包中的其他类,甚至是其子类,都无法直接访问。

应用场景:用于隐藏类的内部状态和实现细节。这是封装的最佳实践,通常你会将类的字段(实例变量)设为 private,然后提供 public 的 getter/setter 方法来控制对这些字段的访问。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Person {
    private String name; // 私有字段
    private int age;     // 私有字段

    // 公开的 getter 方法,用于读取 name
    public String getName() {
        return name;
    }

    // 公开的 setter 方法,用于设置 name
    public void setName(String newName) {
        this.name = newName;
    }
}

总结对比

下表清晰地展示了四种访问修饰符的可见性范围:

修饰符同一个类同一个包不同包的子类不同包的非子类
public
protected
default
private

方法重写 (Override) 与重载 (Overload)

这是体现 Java 多态性的两种核心方式。

  1. 重写 (Override)

    定义:子类重新定义了从父类继承而来的、方法签名完全相同(方法名、参数列表、返回类型均相同)的方法。

    关系:发生在父类与子类之间。

    目的:实现子类特有的行为,是运行时多态的核心。

    注解:推荐在重写的方法上使用 @Override 注解,这能让编译器帮你检查是否满足重写的规则。

  2. 重载 (Overload)

    定义:在同一个类中,定义了多个同名但参数列表不同(参数个数、类型或顺序不同)的方法。

    关系:发生在一个类内部。

    目的:用同样的方法名处理不同的输入参数,是一种编译时多态。

C++ 与 Java 总结对比

特性JavaC++
内存管理自动垃圾回收 (GC)手动管理 (new/delete, RAII)
对象模型只有引用类型(堆上)值类型(栈上)和指针类型(堆上)
继承单继承,多实现接口多重继承
多态实现方法默认是虚函数需显式使用 virtual 关键字
文件结构无头文件,声明和实现在同一文件声明 (.h) 与实现 (.cpp) 分离
泛型编程泛型 (通过类型擦除实现)模板 (通过代码生成实现)
操作符重载不支持(String+ 除外)支持

对于 C++ 开发者来说,从“手动控制一切”的思维模式,切换到 Java “将底层复杂性交给虚拟机管理”的思维模式,是学习过程中的核心转变。

本文采用 CC BY 4.0 许可协议,转载请注明出处。
使用 Hugo 构建
主题 StackJimmy 设计