欢迎光临
我们一直在努力

【Java03】:面向对象基础:封装、继承与多态

1. 前言

这篇博客主要整理 Java 面向对象基础内容,包括 类、方法、构造方法、方法重载、继承和多态。

相比前两篇的基础语法和流程控制,这一篇开始进入 Java 比较核心的部分。因为之前学过一些 C++,所以“类”“对象”“构造方法”“继承”这些概念理解起来会有一定帮助。不过 Java 也有自己的规则,比如 Java 只支持单继承、所有类最终都继承自 Object,以及子类构造方法必须先调用父类构造方法等。

2. 类文件结构

一个 Java 源文件中可以定义多个类,但最多只能有一个 public 类。

如果有 public 类,那么:

public 类名必须和文件名一致。

例如文件名是 Main.java,那么其中的 public 类应该写成:

public class Main {
}

如果要定义多个 public 类,就必须拆分到多个 .java 文件中。

3. 方法与封装

3.1 为什么字段一般要 private?

在面向对象中,类的字段一般不直接暴露给外部,而是用 private 修饰。

例如:

class Person {
private String name;
private int age;
}

这样外部代码不能直接修改 name 和 age,可以避免随便赋值导致对象状态不合理。

字段变成 private 后,外部通过方法间接操作字段读写它们。

例如:

public void setAge(int age) {
if (age < 0 || age > 100) {
throw new IllegalArgumentException("invalid age value");
}
this.age = age;
}

这样外部代码不能直接修改 age,只能调用 setAge()。在方法内部,我们就可以检查参数是否合法。

再比如:

public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("invalid name");
}
this.name = name.strip();
}

这里可以避免传入 null 或空字符串,并且用 strip() 去掉首尾空格。

简单理解:private 字段负责隐藏数据,public 方法负责提供安全的操作入口。

3.2 方法是对外接口

一个类可以通过方法给外部代码暴露一些操作,同时自己保证内部逻辑正确。

例如:

Person ming = new Person();
ming.setName("Xiao Ming");
ming.setAge(18);

调用方法的格式是:

实例变量.方法名(参数);

4. this 关键字

在方法内部,this 表示当前实例。

例如:

class Person {
private String name;

public String getName() {
return this.name;
}
}

如果没有命名冲突,也可以省略 this:

public String getName() {
return name;
}

但是如果局部变量和字段同名,就必须使用 this。

例如:

class Person {
private String name;

public void setName(String name) {
this.name = name;
}
}

这里:

  • 前面的 this.name 表示当前对象的字段;
  • 后面的 name 表示方法参数。

如果不写 this,就分不清了。

5. private 方法

方法也可以用 private 修饰:只能在类内部使用,外部无法调用。

例如:

class Person {
private int birth;

public int getAge() {
return calcAge(2026);
}

private int calcAge(int currentYear) {
return currentYear this.birth;
}
}

这里 calcAge() 是一个私有方法,外部不能直接调用,但 getAge() 可以在类内部调用它。

这个例子也说明:类对外暴露的是 getAge(),至于内部到底有没有 age 字段,调用者并不需要知道。

6. 可变参数

可变参数用 类型…变量名 表示。

例如:

class Group {
private String[] names;

public void setNames(String... names) {
this.names = names;
}
}

调用时可以传入任意个参数:

Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun");
g.setNames("Xiao Ming");
g.setNames();

可变参数本质上类似数组。

也可以写成数组参数:

public void setNames(String[] names) {
this.names = names;
}

但这样调用时需要手动创建数组:

g.setNames(new String[] {"Xiao Ming", "Xiao Hong"});

所以可变参数写起来更方便。

可变参数可以传入 0 个参数,此时接收到的是空数组,而不是 null。

7. 参数传递

7.1 基本类型参数

如果参数是基本类型,传进去的是值的副本。

例如:

public static void change(int n) {
n = 100;
}

外部变量不会被修改。

7.2 引用类型参数

如果参数是引用类型,传进去的是引用的副本。这个副本和外部变量指向同一个对象。

所以,如果通过这个引用修改对象内部状态,外部也能看到变化。

但是,如果只是让参数变量重新指向新对象,不会改变外部变量本身。

简单记:Java 只有值传递。引用类型传递的是“引用的值”,所以可以修改对象内部状态,但不能让外部变量改指向。

8. 构造方法

构造方法用于创建对象时初始化实例。

  • 方法名必须和类名相同;
  • 没有返回值,也不能写 void;
  • 调用构造方法必须使用 new。
  • 例如:

    class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
    this.name = name;
    this.age = age;
    }
    }

    创建对象:

    Person p = new Person("Xiao Ming", 18);

    如果一个类没有定义构造方法,编译器会自动生成一个无参构造方法。

    但是,如果我们自己定义了构造方法,编译器就不会再自动生成默认构造方法。

    例如:

    class Person {
    public Person(String name) {
    }
    }

    这时再写:

    Person p = new Person();

    就会报错,因为无参构造方法不存在。

    字段也可以直接初始化:

    class Person {
    private String name = "Unnamed";
    private int age = 10;
    }

    构造方法也可以重载,也就是定义多个参数不同的构造方法。

    9. 方法重载

    方法重载指的是:方法名相同,但参数不同。

    例如:

    class Hello {
    public void sayHello() {
    System.out.println("Hello");
    }

    public void sayHello(String name) {
    System.out.println("Hello, " + name);
    }
    }

    参数不同可以是:个数、类型、顺序

    但一般不推荐只靠参数顺序区分,容易让人看混。

    需要注意:

    返回值不同不能作为方法重载的判断依据。

    例如下面这样不算合法重载:

    public int test() {
    return 1;
    }

    public String test() {
    return "hello";
    }

    因为调用 test() 时,编译器无法只根据返回值判断要调用哪个方法。

    10. 继承

    10.1 extends 关键字

    Java 使用 extends 实现继承。

    例如:

    class Person {
    protected String name;
    protected int age;
    }

    class Student extends Person {
    private int score;
    }

    这里 Student 继承自 Person。

    可以理解为:

    Student is a Person

    通过继承,子类可以复用父类已有的字段和方法,只需要编写自己新增的功能。

    在术语上:

    • Person 是父类、超类、基类;
    • Student 是子类、扩展类。

    注意:子类不要定义和父类重名的字段,否则容易造成混乱。

    10.2 Object 类

    在 Java 中,如果一个类没有显式写 extends,编译器会自动让它继承 Object。

    也就是说:

    class Person {
    }

    实际上可以理解为:

    class Person extends Object {
    }

    所以,所有 Java 类最终都继承自 Object。

    继承关系可以简单理解为:

    Object

    Person

    Student

    Java 只允许一个类继承一个父类,也就是单继承。

    11. protected 关键字

    protected 可以让字段或方法在继承树内部访问。

    例如:

    class Person {
    protected String name;
    }

    子类可以访问:

    class Student extends Person {
    public String hello() {
    return "Hello, " + name;
    }
    }

    访问权限大致可以简单记成:

    private 只能本类访问
    protected 本类和子类可以访问
    public 外部都可以访问

    12. super 关键字

    super 表示父类。

    在子类中,可以用 super 调用父类构造方法或父类方法。

    12.1 子类构造方法必须先调用父类构造方法

    例如:

    class Person {
    protected String name;
    protected int age;

    public Person(String name, int age) {
    this.name = name;
    this.age = age;
    }
    }

    如果子类这样写:

    class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
    this.score = score;
    }
    }

    会报错。

    原因是:Java 要求子类构造方法的第一行必须先调用父类构造方法。

    如果没有写,编译器会自动加上:

    super();

    但父类 Person 没有无参构造方法,所以会报错。

    正确写法是:

    class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
    super(name, age);
    this.score = score;
    }
    }

    如果父类没有无参构造方法,子类必须显式调用 super(…)。

    12.2 子类不会继承父类构造方法

    这一点也容易忘:

    子类不会继承父类的构造方法。

    子类要创建对象,必须有自己的构造方法。如果没写,编译器可以生成默认构造方法,但它不是从父类继承来的。

    13. 向上转型和向下转型

    13.1 向上转型

    把子类类型赋值给父类类型,叫向上转型。

    例如:

    Student s = new Student();
    Person p = s;
    Object o = s;

    这是安全的,因为 Student is a Person,同时也是 Object。

    13.2 向下转型

    把父类类型强制转成子类类型,叫向下转型。

    例如:

    Person p1 = new Student();
    Student s1 = (Student) p1;

    这是可以的,因为 p1 实际指向的是 Student 对象。

    但是:

    Person p2 = new Person();
    Student s2 = (Student) p2;

    会在运行时报错,因为 p2 实际上不是 Student。

    向下转型可能失败,失败时会抛出 ClassCastException。

    13.3 instanceof

    为了避免向下转型出错,可以先用 instanceof 判断。

    Person p = new Student();

    if (p instanceof Student) {
    Student s = (Student) p;
    }

    instanceof 用来判断一个引用变量指向的对象是不是某个类型,或者这个类型的子类。

    如果变量是 null,那么 instanceof 的结果一定是 false。

    较新的 Java 版本中,还可以写成:

    Object obj = "hello";

    if (obj instanceof String s) {
    System.out.println(s.toUpperCase());
    }

    这样判断成功后,可以直接使用变量 s。

    14. 继承和组合

    继承表示 is-a 关系。

    例如:

    Student is a Person

    组合表示 has-a 关系。

    例如:

    Car has an Engine

    如果只是“拥有某个对象”,不应该强行使用继承。

    简单记:is-a 用继承,has-a 用组合。

    实际开发中,很多时候会优先考虑组合,因为继承会让子类和父类绑定得更紧,而且 Java 只支持单继承。

    15. 多态

    15.1 Override 和 Overload 的区别

    Override 是覆写,发生在父类和子类之间。

    如果子类定义了一个和父类方法签名相同的方法,就是覆写。

    例如:

    class Person {
    public void run() {
    System.out.println("Person run");
    }
    }

    class Student extends Person {
    @Override
    public void run() {
    System.out.println("Student run");
    }
    }

    Overload 是重载,发生在同一个类中,方法名相同,但参数不同。

    例如:

    public void hello() {
    }

    public void hello(String name) {
    }

    两者区别:

    概念含义
    Overload 方法名相同,参数不同,是新方法
    Override 方法签名相同,子类覆盖父类方法

    建议覆写方法时加上 @Override。

    这样如果方法签名写错,编译器会提醒。

    例如:

    class Person {
    public void run() {
    }
    }

    class Student extends Person {
    @Override
    public void run(String s) {
    }
    }

    这里会编译报错,因为 run(String s) 并没有覆写父类的 run()。

    15.2 动态方法调用

    多态最重要的一点是:

    Java 的实例方法调用,取决于对象的实际类型,而不是变量的声明类型。

    例如:

    Person p = new Student();
    p.run();

    虽然变量 p 的声明类型是 Person,但它实际指向的是 Student 对象。

    如果 Student 覆写了 run(),那么最终调用的是 Student 的 run()。

    这就是多态。

    多态的好处是:代码可以面向父类编写,运行时根据实际子类对象调用不同实现。

    15.3 覆写 Object 的方法

    所有类最终都继承自 Object,所以可以根据需要覆写 Object 中的一些方法。

    常见方法有:

    方法作用
    toString() 把对象转换成字符串
    equals() 判断对象是否逻辑相等
    hashCode() 返回对象的哈希值

    例如:

    class Person {
    private String name;

    @Override
    public String toString() {
    return "Person: name=" + name;
    }

    @Override
    public boolean equals(Object o) {
    if (o instanceof Person p) {
    return this.name.equals(p.name);
    }
    return false;
    }

    @Override
    public int hashCode() {
    return this.name.hashCode();
    }
    }

    初学阶段先知道:如果希望对象打印得更清楚,可以覆写 toString();如果希望按内容比较对象,可以覆写 equals()。

    16. final 关键字

    final 可以修饰类、方法和字段。

    16.1 final 类

    如果一个类用 final 修饰,那么它不能被继承。

    例如:

    public final class String {
    }

    String 类就是 final 的,所以不能被继承。

    16.2 final 方法

    如果一个方法用 final 修饰,那么它不能被子类覆写。

    class Person {
    public final void run() {
    }
    }

    16.3 final 字段

    如果字段用 final 修饰,那么初始化后不能再重新赋值。

    class Person {
    public final String name;

    public Person(String name) {
    this.name = name;
    }
    }

    这有点像 C++ 里的 const,可以保证字段初始化后不再改变。

    需要注意:如果 final 修饰的是引用类型,那么这个引用不能再指向别的对象,但对象内部状态是否能变,要看这个对象本身是否可变。

    17. 今日易错点总结

    17.1 private 字段不是不能用,而是要通过方法间接使用

    字段设成 private 后,外部不能直接访问,但可以通过 get、set 方法间接访问。

    这样做的好处是可以在方法里检查参数,保证对象状态合理。

    17.2 this 主要用来区分字段和局部变量

    如果字段和参数同名,必须写:

    this.name = name;

    否则容易把局部变量和字段混在一起。

    17.3 返回值不同不能构成重载

    重载看的是参数列表,不看返回值。

    17.4 子类构造方法必须先调用父类构造方法

    如果父类没有无参构造方法,子类必须显式写:

    super(...);

    并且它必须是构造方法中的第一条语句。

    17.5 向下转型前最好先用 instanceof 判断

    向上转型一定安全,向下转型可能失败。

    为了避免 ClassCastException,可以先判断:

    if (p instanceof Student) {
    Student s = (Student) p;
    }

    17.6 Override 和 Overload 不要混

    • Overload:方法名相同,参数不同;
    • Override:子类覆写父类方法,方法签名相同。

    覆写时建议加上 @Override,让编译器帮忙检查。

    18. 学习小结

    今天感觉最不好理解的是 多态。尤其是 Person p = new Student(); 这种写法,变量的声明类型和对象的实际类型不一样,最后调用方法时又是根据实际类型来决定,这个地方刚开始看会有点绕。现在可以先简单理解为:父类变量可以指向子类对象,而真正执行哪个方法,要看运行时对象到底是谁。

    不过 getName()、setName() 这一类方法就比较好理解,也能明显感觉到它们的作用。字段设成 private 后,外部不能随便修改数据,只能通过方法间接访问。这样就可以在 setAge()、setName() 里做参数检查,避免把年龄设置成负数,或者把名字设置成空字符串。相比直接暴露字段,这种写法对数据有更好的保护性,是很值得学习的写法。

    目前感觉,面向对象的重点不是只记住某个语法形式,而是理解“类如何对外提供接口、如何隐藏内部细节、子类和父类之间到底是什么关系”。这些关系理清楚之后,后面再学接口、抽象类和更复杂的多态应该会更容易一些。

    第三篇 Java 学习博客就先记录到这里~

    19. 参考资料

    • 廖雪峰 Java 教程
    赞(0)
    未经允许不得转载:171主机测评 » 【Java03】:面向对象基础:封装、继承与多态
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址