Java 入门 - 面向对象

1,381 阅读9分钟

本专栏教程适合有其他语言基础的人快速入门,编写时基于 Java 17

class Person {
    public String name; // name 字段;没有初始化,有默认值
    public int age = 12; // age 字段;直接初始化为 12
}

注意:一个 Java 源文件可以包含多个类的定义,但只能定义一个 public 类,且 public 类名必须与文件名一致。如果要定义多个 public 类,必须拆到多个 Java 源文件中

实例

创建实例

Person pany = new Person(); // pany 是实例名

操作实例

pany.name = "pany"; // 对字段 name 赋值
pany.age = 24; // 对字段 age 赋值
System.out.println(pany.name); // 访问字段 name

方法

语法

修饰符 方法返回类型 方法名(方法参数列表) {
    若干方法语句;
    return 方法返回值; // 如果没有返回值,返回类型设置为 void,可以省略 return
}

例子

class Person {
    private String name; // 设置为 private,外部代码就不能直接访问了,可以通过下方的 public 方法访问
    private int age;
    // 方法
    public String getName() {
        return this.name; // 如果没有命名冲突,可以省略 this,变成 return name
    }
    public void setName(String name) {
        this.name = name; // 这里有命名冲突,不可以省略 this
    }
    public int getAge() {
        return this.age;
    }
    public void setAge(int age) { // 调用该方法时,必须传递一个 int 类型参数
        if (age < 0 || age > 100) {
            throw new IllegalArgumentException("invalid age value");
        }
        this.age = age;
    }
}

可变参数

可变参数用 类型... 定义,可变参数相当于数组类型

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

调用方法

g.setNames("name1", "name2"); // 传入 2 个 String
g.setNames("name1"); // 传入 1 个 String
g.setNames(); // 传入 0 个 String

构造方法

定义

  • 构造方法名和类名相同
  • 构造方法没有返回值(也没有 void 关键字)
  • 调用构造方法,必须用 new 操作符
class Person {
    private String name;
    private int age;
    public Person(String name, int age) { // 构造方法,用来初始化实例字段
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return this.name;
    }
    public int getAge() {
        return this.age;
    }
}

默认构造方法

  • 任何 class 都有构造方法
  • 如果没有自定义构造方法,那么就是默认构造方法
  • 如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法

多构造方法

可以定义多个构造方法,在通过 new 操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分;并且一个构造方法可以调用其他构造方法,有利于代码复用

class Person {
    private String name;
    private int age;

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

    public Person(String name) {
        this(name, 18); // 调用上一个构造方法 Person(String, int)
    }

    public Person() {
        this("Unnamed"); // 调用上一个构造方法 Person(String)
    }
}
  • 如果调用 new Person("Xiao Ming", 20); 会自动匹配到构造方法 public Person(String, int)
  • 如果调用 new Person("Xiao Ming"); 会自动匹配到构造方法 public Person(String)
  • 如果调用 new Person(); 会自动匹配到构造方法 public Person()

方法重载

在一个类中,可以定义多个方法。如果有一些方法,它们的功能都是类似的,只有参数有所不同,那么可以把这一组方法名做成同名方法。例如,在 Hello 类中,定义多个 hello() 方法

class Hello {
    public void hello() {
        System.out.println("hello world");
    }
    public void hello(String name) {
        System.out.println("hello " + name);
    }
    public void hello(String name, int age) {
        if (age < 18) {
            System.out.println("hi, " + name);
        } else {
            System.out.println("hello, " + name);
        }
    }
}

这种方法名相同,但各自的参数不同,称为方法重载

注意:方法重载的返回值类型通常都是相同的

继承

  • 继承是面向对象编程中非常强大的一种机制,它可以复用代码
  • Java 使用 extends 关键字来实现继承
  • Java 只允许一个 class 继承自一个类(一个类有且仅有一个父类)
  • 只有 Object 特殊,它没有父类
// 此时 Person 是父类
class Person {
    private String name;
    private int age;
    public String getName() {...}
    public void setName(String name) {...}
    public int getAge() {...}
    public void setAge(int age) {...}
}

// 此时 Student 是子类
class Student extends Person {
    // 不要重复 name 和 age 字段/方法,只需要定义新增 score 字段/方法
    private int score;
    public int getScore() { … }
    public void setScore(int score) { … }
}

注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段

protected 关键字

  • 子类无法访问父类的 private 字段或者 private 方法。例如,Student 类就无法访问 Person 类的 name 和 age 字段
  • protected 关键字可以把字段和方法的访问权限控制在继承树内部,所以将 private 改成 protected 就可以让其在子类中被访问

super 关键字

super 关键字表示父类(超类)。子类引用父类的字段时,可以用 super.xxx

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

注意:实际上这里使用 super.name,或者 this.name,或者 name,效果都是一样的,编译器会自动定位到父类的 name 字段。

必须调用父类构造方法

Java 中,任何 class 的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句 super()

如果父类没有默认的构造方法,子类构造函数就必须在第一行语句显式调用 super() 并给出参数以便让编译器定位到父类的一个合适的构造方法,否则会报错!

所以还得出一个结论:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

阻止继承

正常情况下,只要某个 class没有 final 修饰符,那么任何类都可以从该 class 继承。

可以使用 sealed 修饰 class,并通过 permits 明确写出能够从该 class 继承的子类名称:

public sealed class Shape permits Rect, Circle, Triangle {
    ...
}

向上转型

如果 Student 是从 Person 继承下来的,那么一个引用类型为 Person 的变量,可以指向Student 类型的实例:

Person p = new Student();

这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型

向下转型

Person p1 = new Student(); // 没问题
Person p2 = new Person();
Student s1 = (Student) p1; // 没问题
Student s2 = (Student) p2; // 报错

Person 类型 p1 实际指向 Student 实例,Person 类型变量 p2 实际指向 Person 实例。在向下转型的时候,把 p1 转型为 Student 会成功,因为 p1 确实指向 Student 实例,把 p2 转型为 Student 会失败,因为 p2 的实际类型是 Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。

为了避免向下转型出错,Java 提供了 instanceof 操作符,可以先判断一个实例究竟是不是某种类型:

Person p = new Student();
if (p instanceof Student) { // 只有判断成功才会向下转型
    Student s = (Student) p; // 一定会成功
}

多态

覆写

在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写

// 父类
class Person {
    public void run() {
        System.out.println("Person.run");
    }
}
// 子类
class Student extends Person {
    // 如果方法签名相同,并且返回值也相同,就是 Override;如果方法签名不同,就是 Overload
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

注意:方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在 Java 程序中,出现这种情况,编译器会报错

多态的定义

public class Main {
    public static void main(String[] args) {
        Person p = new Student(); // 引用变量的声明类型与其实际类型不符
        p.run(); // 应该打印 Student.run
    }
}
class Person {
    public void run() {
        System.out.println("Person.run");
    }
}
class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

定义:Java 的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。这个非常重要的特性在面向对象编程中称之为多态

用处:多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码

final

  • 如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为 final
  • final 修饰的类不能被继承
  • final 修饰的字段在初始化后不能被修改

抽象类

抽象类和抽象方法必须同时存在:

// abstract 类
abstract class Person {
    // abstract 方法:本身没有实现任何方法语句
    public abstract void run();
}

我们无法实例化一个抽象类:

Person p = new Person(); // 编译错误

因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现(覆写)其定义的抽象方法,否则编译会报错!

在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力

面向抽象编程

当我们定义了抽象类 Person,以及具体的 StudentTeacher 子类的时候,我们可以通过抽象类 Person 类型去引用具体的子类的实例:

Person s = new Student();
Person t = new Teacher();
// 好处是:不关心 Person 变量的具体子类型
s.run();
t.run();

这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程

接口

如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口 interface

interface Person {
    // 因为接口定义的所有方法默认都是 public abstract 的,所以这两个修饰符不需要写出来
    void run();
    String getName();
}

所谓 interface,就是比抽象类还要抽象的纯抽象接口,因为它连实例字段都不能有!

类实现接口

class Student implements Person {
    private String name;
    public Student(String name) {
        this.name = name;
    }
    @Override
    public void run() {
        System.out.println(this.name + " run");
    }
    @Override
    public String getName() {
        return this.name;
    }
}

在 Java 中,一个类虽然只能继承自另一个类。但一个类可以实现多个 interface

接口继承

一个 interface 可以继承自另一个 interface

static

静态字段

class Person {
    // 实例字段 name
    public String name;
    // static 静态字段 number:
    public static int number;
}

注意:实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该静态字段。对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例。

不推荐用 实例名.静态字段 去访问静态字段,而是采用 类名.静态字段 这种更合适的方式来访问:

Person.number = 100;

静态方法

class Person {
    public static int number;
    public static void setNumber(int value) {
        number = value;
    }
}

特点:

  • 静态方法可以直接通过类名调用
  • 静态方法属于 class 而不属于实例,因此静态方法内部,无法访问 this 变量,也无法访问实例字段,它只能访问静态字段

接口可以有静态字段

public interface Person {
    // 编译器会自动加上 public statc final
    int MALE = 1; // public statc final int MALE = 1;
}

End

  • 上一篇:Java 入门 - 语法基础
  • 下一篇:Java 入门 - 进阶知识

该系列的每篇文章都收录在本专栏《Java 后端从 0 到 1》

掘金

本文正在参加「金石计划」