02_oo_01_class

第十四章 面向对象程序设计

面向对象程序设计(Object-Oriented Programming)是当今较为热门的一种编程范型(Programming Paradigm)。与过程化程序设计(Procedural Programming)相比,面向对象程序设计将数据与相关的逻辑操作"封装"在一起,对内提供了更好的数据保护,对外提供了更加独立、完整的服务。因此,面向对象程序设计逐渐被各大公司采用,作为主流的开发语言和方法。

面向对象的程序由对象组成。每个对象向用户提供了功能和服务,并隐藏了其实现细节。因此,当使用这些对象时,开发人员无需了解其内部实现。

1 类(Class)

Java语言是一门面向对象的编程语言。在Java语言中,类(Class)是构造对象的模板。类描述了对象中包含的数据和操作,以及与其他对象的关系。通过类信息来创建对象的过程被称为对象实例化(Instantiation)过程

面向对象程序设计的一个重要特性是封装(Encapsulation)。从形式上看,封装将数据和相关的行为操作"绑定"在一起,隐藏在对象中。对象中的数据称为成员变量(Member Field);对象中的操作被称为成员方法(Member Method)。从使用方式上看,"封装"将对象包装成了一个"黑盒";用户只能看到对象对外提供的功能,而无法看到其内部实现。这种"黑盒"特性为软件重用性和稳定性提供了极大的帮助和支持。

另一个面向对象程序设计的特性是继承(Inheritance)。继承是一种类之间的关系。在同一个继承等级结构中,子类能够使用父类提供的部分数据和方法,从而提高了复用代码的能力。

开发人员可以使用如下代码结构来定义一个新的类。class是Java语言的关键字,用于标识类的声明语句。ClassName为用户自定义的类的名称。在一对花括号中,开发人员可以为类声明成员变量、构造函数和成员方法。

class ClassName {
    field1
    field2
    ...
    constructor1
    constructor2
    ...
    method1
    method2
    ...
}

例如,我们在下面的Student类中,我们声明了一个成员变量id。Student类还有一个构造函数,用于初始化成员变量。Student类还声明了两个成员方法getId()和setId(),分别用于获取和设置成员变量id的值。

public class Student {
    private int id; // 成员变量

    public Student (int id) { // 构造函数
        this.id = id;
    }

    public int getId() { // 成员方法
        return this.id;
    }

    public void setId(int id) { // 成员方法
        this.id = id;
    }
}

2 构造函数(Constructor)

构造函数(Constructor)是那些名字与类名字相同的函数或者方法。构造函数会在对象创建的过程中被调用。构造函数的主要目的是控制对象的创建和初始化对象的成员变量。

在构造函数的声明中,不需要声明返回值。构造函数可以接受多种不同的参数组合。当构造函数不接受任何参数时,我们称之为无参数构造函数。一个类可以同时有多个构造函数,但是这些构造函数必须有着不同的参数个数或者参数类型。当一个类有多个构造函数时,我们称这些构造函数为重载(Overloading)的构造函数。Java编译器会根据创建对象时使用的参数个数和类型来判断使用哪个构造函数。

当一个类不包含构造函数时,Java编译器会自动为其生成一个默认构造函数。在对象构建的过程中,默认构造函数会将所有的成员变量设置成默认值。于是,整形成员变量会被设置为0;布尔类型的成员变量会被设置为false;对象引用类型的成员变量会被设置为null。

public class Student {
    Student() { // 这是一个无参数构造函数
        ...
    }

    Student(int) { // 这是一个接受int类型参数的构造函数
        ...
    }
}

构造函数之间是可以相互调用的。例如,下面的无参数构造函数通过this()调用了另一个构造函数,并将-1作为参数值传入。当使用this()语句时,它必须出现在构造函数中的第一行。

public class Student {
    private int id;

    public Student() {
        this(-1); // 调用另一个构造函数
    }

    public Student (int id) {
        this.id = id;
    }
}

在上述的例子中,我们定义了两个构造函数,第一个构造函数不接受任何参数,第二个构造函数接收一个int类型的参数id,用于初始化成员变量id。这两个构造函数是public(公开的),即在Student类的外部允许创建Student对象。当构造函数声明为private(私有的)时,Student对象只能在Student类的方法中创建。单例模式(Singleton Pattern)建造者模式(Builder Pattern)是一种常见的通过使用私有构造函数来控制对象创建的例子。

3 对象创建与销毁

在通常情况下,开发人员使用new操作符(new operator)创建新对象。在下面的代码示例中,我们创建了一个新的Student对象,将其赋值给局部变量s。在等号的右侧,new Student()被称为对象创建表达式

Student s = new Student();

Java语言还支持以其他方式创建新对象。例如:调用Object类的clone()方法会复制出一个一模一样的新对象。另外,java.lang.reflect.Constructor.newInstance()方法和Class.newInstance()方法也能创建出新对象。

在对象销毁方面,Java语言并没有提供对象销毁的指令或者方法。当对象使用完毕后,Java语言会自动检测到这些对象,并在合适的时候释放这些对象。Java语言的对象销毁是一个较为复杂的过程,它的实现依赖于Java虚拟机的实现。其详细内容可参考这里

4 对象初始化

除了在构造函数中初始化成员变量,Java语言还支持显示初始化和在初始化块中初始化成员变量。

显示初始化是在声明成员变量时,同时给出初始值。

public class Student {
    private int id = -1; // 显示初始化成员变量i
    ...
}

或者,开发人员也可以在初始化块中初始化成员变量。

public class Student {
    private int id;
    
    {   // 这是初始化块
        id = -1;
    }
    ...
}

5 成员方法(Member Method)

成员方法,有时又称为成员函数,是实现业务逻辑和完成计算的地方。成员方法能访问本类定义的成员变量以及调用本类的其他成员方法,或者使用其他公开类的方法。

成员方法可以接受0个、1个或者多个参数,并且返回一个结果。当成员方法不返回任何数据时,可以使用void作为返回的数据类型。在下面的示例中,getId()和setId()是两个成员方法。getId()没有输入参数,但是它生成并返回一个int类型的运算结果。setId()接受一个int类型的输入参数id,但是它不生成返回任何结果。所以,它的结果类型声明为void。

在成员方法中,可以直接使用本类声明的成员变量。当成员变量的名称与本地变量的名称发生冲突时,可使用关键字this来引用当前对象。因此,在下面的例子中,this.id表示的是当前对象的成员变量id。

public class Student {
    private int id;

    public Student (int id) {
        this.id = id;
    }

    public int getId() {
        return this.id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

有时,开发人员容易混淆voidVoid(第二个Void的第一个字母V是大写字母)。前一个void是关键字,表示方法不返回任何数据或者结果。后一个Void是一种类类型,它的类型是java.lang.Void。因为Void类放置在java.lang包中,会被Java虚拟机自动加载。所以,开发人员能够直接使用Void或者java.lang.Void,而无需将其导入。设计Void的初衷是为了更好地融合方法和泛型编程,使得程序能够检测和推演方法中参数的类型。因为Void是一种类型,常常用于标识方法的返回类型,因此,在使用时我们需要返回null。

Void getSomething() {
    return null;
}

6 访问控制(Access Control)

从上面的例子中,我们可以看到,在关键字class之前,我们还使用了public。public也是一个关键字,用于表示定义的类是公开的(public),可以被任何地方的代码使用。在通常情况下,我们需要将最顶层的类声明为public。当我们在后续章节中介绍包(Package)和模块(Module)时,我们还会从包和模块的层面介绍访问控制。

除了类声明以外,上述代码还将成员变量id声明为private。private也是一个关键字,意思是成员变量id只能在Student类中访问,因为它是私有的(private)。我们会在介绍继承时,再介绍关键字protected的用法。protected意思是可以在本类或者其子类中访问。类似的,成员方法也可以声明为public、protected或者private。

7 静态方法(Static Method)

除了成员变量和成员方法以外,我们还可以在类中声明静态变量(Static Field)静态方法(Static Method)。成员变量和静态变量的区别是成员变量是对象中的数据,而静态变量则是类的数据。例如,在下面的示例中,我们声明了类Student,它有一个成员变量id和一个静态变量MAX_ID。当我们创建了两个Student的对象时,我们就会有着两份id,因为每一个对象会包含一份id。而这两个对象则共享一份MAX_ID,因为这两个对象都是Student对象。我们只有一个Student类。所以,这两个Student对象会共享这一份MAX_ID。

public class Student {
    private int id;
    private static int MAX_ID = 1000;
}

静态变量常常被用于声明常量。例如,在下面的代码示例中,我们声明了常量PI。

public class Math {
    public static final double PI = 3.1415926;
}

实际上,我们在前面的章节已经使用过静态方法。静态方法常常用于实现不能作用于对象上的逻辑。例如,Math类中的pow()方法就是一个静态方法。表达式Math.pow(x, a)用于计算幂xa的值。

每个Java程序都是从main()方法开始运行的。main()方法也是一种静态方法。

静态方法的另一个常用场景是实现工厂方法,用于封装对象创建的逻辑。

8 方法调用

在方法声明完成之后,我们就可以使用这些方法帮助我们完成更复杂的任务。当调用成员方法时,我们需要先创建一个对象,然后在对象上调用该方法。如果需要使用静态方法,我们可以使用类名来指定调用的静态方法,或者使用该类的对象来指定。但是,在实际应用中,我们建议使用类名来调用静态方法,因为这样能够很清晰的区分静态方法调用和成员方法调用。

例如,我们在下面示例的main()函数中分别调用了静态方法newInstance()和成员方法getId()。在调用newInstance()方法时,我们传入了一个整形参数1010。根据方法newInstance()的原型声明,这个参数是必须的,因为newInstance()方法需要根据传入的参数创建一个Student对象。当我们调用方法getId()时,我们使用了s.getId()。s是一个Student的对象,我们使用一个点符号(.)表示在这个对象上调用了getId()成员方法。

public class Student {
    private int id;

    public Student (int id) {
        this.id = id;
    }

    public int getId() {
        return this.id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public static Student newInstance(int id) {
        return new Student(id);
    }

    public static void main(String[] args) {
        Student s = Student.newInstance(1010);
        int studentId = s.getId();
    }
}

从这个例子我们还可以看出,当调用方法时,我们需要为方法传递相匹配的参数列表。如果参数的类型不匹配,Java程序会尝试做隐式类型转换(例如,数值类型的类型提升或者装箱/拆箱操作)。如果还不能找到匹配的方法,则Java编译器会报告错误。

向方法传递参数有两种方式,它们被称为传值(Call by Value)或者传址(Call by Reference)。传值的意思是程序会将参数的值传递到方法中。换句话说,程序会生成参数的一份拷贝,将新生成的拷贝传入方法中。所以,如果该拷贝的值在方法中发生变化,不会影响原参数的值。传址的意思是将参数的"地址"传入方法中。当该参数的值在方法中发生变化时,会影响原参数的值。在Java语言中,传址指的是传引用。

当方法参数是基本数据类型(Primitive Type)时,Java语言使用的是传值的方式。当方法参数是对象引用时,Java语言使用的是传址的方式,即传递对象的引用。所以在下面的示例中,void setId(int id)方法接收一个int类型的参数。这个参数使用传值的方式传递参数。在该方法中,我们无法改变传入参数的值。但是,void setId(Student s)方法接收一个Student对象参数,这个参数使用传址的方法传递参数。在该方法中,我们可以修改对象s中的成员变量的值。

public class Student {
    ...
    public static void setId(int id) {
        id = 1;
    }

    public static void setId(Student s) {
        s.id = 1;
    }
    public static void main(String[] args) {
        Student s = Student.newInstance(1010);
        Student.setId(s.id); // 传值方式,在方法中无法修改s.id的值
        Student.setId(s); // 传址方式,在方法中可以修改s.id的值
    }
    ...
}

我们会在后续章节中介绍可变参数的成员方法和静态方法

9 抽象类(Abstract Class)

抽象类(Abstract Class)是一种包含了抽象成员方法(Abstract Member Method)的类。在类中,只声明了成员方法原型,而没有给出具体实现细节的方法被称为抽象成员方法。也正是因为抽象成员方法没有具体实现,所以,抽象类不能实例化。我们无法生成抽象类的对象。

抽象类的声明使用了关键字abstract,并且在方法原型的声明中,也使用了abstract关键字,声明该成员方法是抽象的。在下面的示例中,我们声明了一个抽象类Classroom。它有一个抽象的成员方法capacity(),用于计算该教室能容纳学生的人数。

public abstract class Classroom {
    public abstract int capacity();
}

10 小结

本章介绍了Java语言中类的概念。类是一个逻辑实体;它将相关的数据与逻辑封装在一起。当我们根据类信息生成多个对象后,每个对象都包含着一份成员变量的数据。而静态变量的数据是"绑定"在类信息中的,所以,这些对象共享静态变量中的数据。另外,只有具体类才能实例化;抽象类是不能实例化的,因为它包含了尚未实现的成员方法。我们会在后续章节中继续介绍接口继承多态等面向对象程序设计的重要概念。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.