跳至主要內容

面向对象

Akkiri...大约 17 分钟JavaSEJava

⏱️时间安排:21天
⏳开始时间:2022-11-02
⌛结束时间:2022-12-08

面向对象-基础

面向对象概述

面向对象程序设计(object-oriented programming,OPP)是当今主流的程序设计泛型。由于 Java 是彻底的、纯粹的面向对象语言,所以必须熟悉 OOP 才能够很好地使用 Java。

面向对象编程思想指的是:按照真实世界客观事物的自然规律进行分析、客观世界中存在什么样的实体,构建的软件系统就存在什么样的实体。

(class)是构造对象的模板或蓝图。由类构造(construct)对象的过程被称为创建类的实例(instance)。

封装(encapsulation,有时称数据隐藏)是处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的所有者隐藏具体的实现方式。对象中的数据称为实例字段(instance field),操作数据的过程被称为方法(method)。

实现封装的关键在于,绝不能让类中的方法直接访问其他类的实例字段。这使得一个类可以完全改变数据的存储方式,但只要仍使用同样的方法操作数据,其他对象就不用关心这个类的变化。

OOP 的另一个原则让用户自定义 Java类变得更加容易,这就是:可以通过拓展其他类来构建新类(继承 inheritance)。事实上,在 Java 类中,所有的类都拓展自 Object 类。Object 类将在下一节介绍。

对象

使用 OOP 一定要清楚对象的三个主要特征:

  • 对象的行为(behavior)—— 可以对对象完成哪些操作,或者可以对对象应用哪些方法?

  • 对象的状态(state)—— 当调用那些方法时,对象会如何响应?

  • 对象的标识(identity)—— 如何区分具有相同行为与状态的不同对象?

对象的行为是用可调用的方法来定义的。每个对象都保存着描述当前状况的信息,这就是对象的状态。对象状态的改变必须通过调用方法实现。对象的状态并不能完全描述一个对象,每个对象都有一个唯一的标识(identity,或身份),作为同一个类的实例,每个对象的标识总是不同的,状态也往往存在差异。

类之间的关系

在类之间,最常见的关系有:

  • 依赖(“use-a”):一个类的方法使用或操作另一个类的对象。
  • 聚合(“has-a”):一个类的对象包含另一个类的对象。
  • 继承(“is-a”):一个更特殊的类继承自一个更一般的类。

一般采用 UML(Unified Modeling Language,统一建模语言)绘制类图,来描述类之间的关系。

使用预定义的类

Java 提供了许多预定义的类,如 Math 类、Date 类等。下面以 Date 类为例来说明如何构造对象,以及如何使用类的方法。

对象与对象变量

要想使用对象,首先必须构造对象,并指定其初始状态。

在 Java 程序设计语言中,要使用构造函数(constructor)构造新实例。构造函数是一种特殊的方法,用来构造并初始化对象。下面以标准 Java 库中的 Date 类为例。

构造器的名字与类名相同。因此 Date 类的构造器名为 Date。要想构造一个 Date 对象,需要在构造器前面加上 new 操作符,如下所示。

new Date()

这个表达式构造了一个新对象。这个对象被初始化为当前的日期和时间。
如果有所需要的话,也可以将这个对象传递给一个方法。

System.out.println(new Date());

或者,也可以对刚刚创建的对象应用一个方法。Date 类中有一个 toString 方法。这个方法将返回日期的字符串描述。

String s = new Date().toString();

在上面的两个例子里,构造的对象仅使用了一次。通常,我们希望构造的对象可以多次使用,因此。需要将对象存放在一个变量中:

Date birthday = new Date();

对象和对象变量之间存在着一个重要的区别。例如,以下语句:

Date deadline; // deadline doesn't refer to any object

定义了一个对象变量 deadline,它可以引用 Date 类型的对象。在它引用对象之前,不能在这个变量上使用对象的方法。语句 s = deadline.toString(); 将发生编译错误。

在使用变量之前,必须先初始化变量 deadline,这里有两个选择——可以初始化这个变量,让它引用一个新构造的对象;也可以设置这个变量,让他引用一个已有的对象。

注意

对象变量并没有实际包含一个对象,它只是引用一个对象。new 操作符的返回值是对象的引用。

提示

可以显示地将对象变量设置为 null ,指示这个对象变量目前没有引用任何对象。

deadline = null;
...
if (deadline != null)
    System.out.println(deadline);

自定义类

在 Java 中,最简单的类定义形式为:

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

Employee 类

下面是一个简单的 Employee

class Employee{
    private String name;
    private double salary;
    private LocalDate hireDay;

    public Employee(String n, double s, int year, int month, int day){
        name = n;
        salary = s;
        hireDay = LocalDate.of(year, month, day);
    }

    public String getName(){
        return name;
    }

    public double getSalary(){
        return salary;
    }

    public LocalDate getHireDay(){
        return hireDay;
    }

    public void raiseSalary(double byPercent){
        double raise = salary * byPercent / 100;
        salary += raise;
    }
}

剖析 Employee 类

Employee 类包含一个构造器和 4 个方法:

public Employee(String n, double s, int year, int month, int day)
public String getName()
public double getSalary()
public LocalDate getHireDay()
public void raiseSalary(double byPercent)

这个类所有方法都被标记为 public。关键字 public 意味着任何类的任何方法都可以调用这些方法。

接下来,在 Employee 类的实例中有三个实例字段用来存放将要操作的数据:

private String name;
private double salary;
private LocalDate hireDay;

关键字 private 确保只有 Employee 类自身的方法能够访问这些实例字段,而其他类的方法不能够读写这些字段。

注意

有两个实例字段本身就是对象:name 字段是 String 对象,hireDay 字段是 LocalDate 类对象。

构造函数

下面是 Employee 类的构造函数:

public Employee(String n, double s, int year, int month, int day){
    name = n;
    salary = s;
    hireDay = LocalDate.of(year, month, day);
}

可以看到,构造函数与类同名。在构造 Employee 类的对象时,构造函数便会运行,将实例字段初始化为所希望的初始状态。

构造函数与其他方法有一个重要的不同。构造函数总是需要结合 new 运算符来调用,不能对一个已经存在的对象调用构造函数来达到重新设置实例字段的目的。

构造函数要点:

  • 构造函数与类同名。
  • 每个类可以有一个以上的构造函数(重载)。
  • 构造函数可以有 0 个、1 个或多个参数。
  • 构造函数没有返回值。
  • 构造函数总是伴随着 new 操作符一起调用。

提示

若在构造函数中定义与实例字段同名的局部变量。可以使用 this 关键字来指代同名的实例字段。如:

public Employee(String name, double salary,...){
    this.name = name;
    this.salary = salary;
    ...
}

使用 var 声明局部变量

在 Java 10 中,如果可以从变量的初始值推导出它的类型,那么可以使用 var 关键字声明局部变量,而无需指定变量类型。例如

Employee Bob = new Employee("Bob", 50000, 1989, 10, 1);

可以写成

var Bob = new Employee("Bob", 50000, 1989, 10, 1);

使用 null 引用

如果对 null 值应用一个方法,会产生一个 NullPointerException 异常

LocalDate day = null;
String s = day.toString();  //NullPointerException

如果程序没有 “捕获” 异常,程序就会终止。正常情况下程序并不会捕获这些异常。

提示

定义一个类时,最好清楚地知道哪些字段可能为 null

在上面的例子中,我们不希望 name 或 hireDay 字段为 null。这里 hireDay 字段肯定是非 null 的,因为它初始化为一个新的 LocalDate 对象。但是 name 可能为 null,如果调用构造函数是提供的参数是 null,name 就会是 null

私有方法

在 Java 中,要实现私有方法,只需将关键字 public 改为 private 即可

通过将方法设计为私有,如果你改变了方法的实现方式,将没有义务保证这个方法依然可用。只要方法是私有的,类的设计者就可以确信它不会在别处使用,所以可以将其删去。如果一个方法是公共的,就不能简单地将其删除,因为可能会有其他代码依赖这个方法。

final 字段

可以将实例字段定义为 final,这样定义的字段必须在构造对象时初始化。也就是说,必须确保在构造函数执行之后,这个字段的值已经设置,并且以后不能再修改这个字段。例如将 Employee 类中的 name 字段声明为 final,因为再对象构造之后,这个值不会再改变,即没有 setName (更改器)方法。

class Employee{
    private final String name;
    ...
}

final 修饰符对于类型为基本类型或者不可变类的字段尤其有用。

注意

对于可变类,使用 final 修饰符可能会造成混乱。例如

private final StringBuilder evaluations;

它在 Employee 构造函数中初始化为

evaluations = new StringBuilder();

final 关键字只是表示储存在 evaluations 变量中的对象引用不会再指向另一个 StringBuilder 对象。不过这个对象可以更改。

public void giveGoldStar(){
    evaluations.append(LocalDate.now() + ":Gold star!\n");
}

静态字段与静态方法

静态字段

如果将一个字段定义为 static,每个类就只有一个这样的字段。而对于非静态的实例字段,每个对象都有自己的一个副本。例如,需要给每一位员工赋予唯一的标识码,这里给 Employee 类添加一个实例字段 id 和一个 静态字段 nextId:

class Employee{
    private static int nextId = 1;

    private int id;
    ...
}

现在,每一个 Employee 对象都有一个自己的 id 字段,但这个类所有的实例将共享一个 nextId 字段。换句话说,如果有 1000 个 Employee 类对象,则有 1000 个实例字段 id,分别对应每一个对象。但是,只有一个静态字段 nextId。即使没有 Employee 对象,静态字段 nextId 也存在。它属于类,而不属于任何单个的对象。

静态常量

相较于静态变量,静态常量更常用。例如,在 Math 类中定义了一个静态常量:

public class Math{
    ...
    public static final double PI = 3.14159265358979323846;
    ...
}

在程序中,可以用 Math.PI 来访问这个常量。

如果省略了关键字 static,PI 就变成了 Math 类的一个实例字段。也就是说,需要通过 Math 类的一个对象来访问 PI,并且每一个 Math 对象都有它自己的一个 PI 副本。

静态方法

静态方法就是不在对象上执行的方法。例如 Math 类的 pow 方法就是一个静态方法。表达式 Math.pow(x, a) 会计算幂 xa,运算时,它并不使用任何 Math 对象。换句话说就是它没有隐式参数。

可以认为静态方法是没有使用 this 参数的方法(在一个非静态方法中,this 参数指示这个方法的隐式参数)。

注意

静态方法不能访问非静态字段,因为它不能在对象上执行操作。但是静态字段可以访问静态字段

下面是这样一个静态方法的示例:

public static int getNextId(){
    return nextId;  // returns static field
}

可以直接使用类名来调用这个方法:

int n = Employee.getNextId();

这里也可以省略 static 关键字,但是这样一来,你就需要通过 Employee 类对象的引用来调用这个方法。

提示

在下面两种情况下可以使用静态方法:

  • 方法不需要访问对象状态,因为它需要的所有参数都通过显式参数提供。

  • 方法只需要访问类的静态字段。

方法参数

程序设计语言中通常有两种将参数传递给方法(函数)的方式。

  • 按值调用(call by value)方法接受的是调用者提供的值

  • 按引用调用(call by reference)方法接受的是调用者提供的变量地址。

Java 程序设计语言总是采用按值调用。也就是说方法调用的是所有参数值的一个副本。方法不能修改传递给它的任何参数变量内容。

方法参数分为两种类型:

  • 基本数据类型(数字,布尔型)

  • 对象类型

对于这两种数据类型,下面是方法调用的具体执行过程:

  1. 方法内对应参数变量初始化为传入变量的一个副本。
  2. 执行方法体内的操作。
  3. 方法结束后,参数变量不再使用。

这两种数据类型的区别是一个传入的是变量值,而另一个传入的则是对象的引用。

提示

很多程序设计语言提供了两种参数传递的方式:按值调用和按引用调用。Java 程序设计语言总是采用按值调用,即使是对象。

下面是交换两个 Employee 对象的方法:

public static void swap(Employee x, Employee y){
    Employee temp = x;
    x = y;
    y = temp;
}

如果 Java 对对象采用的是按引用调用,那么这个方法就能实现交换:

var a = new Employee("Alice",...);
var b = new Employee("Bob",...);
swap(a, b);

但是,这个方法并没有改变储存在变量 a 和 b 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的副本,这个方法交换的是这两个副本。

最终,在方法结束时参数变量 x 和 y 被丢弃了。原来的变量 a 和 b 仍然引用在这个方法被调用之前所引用的对象。

下面总结一下在 Java 中对方法参数能做什么和不能做什么:

  • 方法不能修改基本数据类型的参数(即数值型和布尔型)。

  • 方法可以改变对象参数的 状态

  • 方法不能让一个对象参数引用一个新的对象。

对象构造

由于对象构造非常重要,Java 为编写构造器提供了多种机制,下面将详细介绍。

重载

有些类有多个构造器。例如,可以构造一个空的 StringBuilder 对象:

var messages = new StringBuilder();

或者,也可以指定一个初始字符串:

var todoList = new StringBuilder("To do:\n");

这种功能叫做 重载 (overloading)。如果多个方法(比如,StringBuilder 构造器方法)有相同的名字、不同的参数,便出现了重载。编译器会将各个方法首部中的参数类型与特定方法调用中所使用值的类型进行匹配,选出正确的方法。如果编译器找不到匹配的参数,就会产生编译错误,因为根本不存在匹配,或者没有一个比其他的更好(这个查找匹配的过程被称为重载解析(overloading resolution))。

注意

Java 允许重载任何方法,不只是构造器方法。要完整地描述一个方法,需要指定方法名以及参数类型。这叫做方法地签名(signature)。

返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却有不同返回类型的方法。

默认字段初始化

如果在构造器中没有显示地为字段设置初值,那么就会被自动地赋为默认值:数值为 0、布尔值为 false、对象引用为 null。

如果不明确地对字段进行初始化,会影响程序代码的可读性。

提示

这是字段与局部变量的一个重要区别。方法中的局部变量必须明确地初始化。但是如果没有初始化类中的字段,将会自动初始化为默认值。

无参构造器

如果写一个类时没有编写构造器,就会为你提供一个无参构造器。

很多类都包含一个无参构造器,由无参构造器创建对象时,对象的状态会设置为适当的默认值。

提示

如果类中提供了至少一个构造器,但是没有提供无参构造器,那么构造对象时,如果不提供参数是不合法的。

注意

仅当类没有其他构造器,才会得到一个默认的无参构造器。编写类时,如果写了一个自己的构造器,要想让这个类的用户能通过以下调用构造一个实例:

new ClassName()

就必须提供一个无参构造器。当然,如果希望所有字段都被赋予默认值,只需要提供以下代码:

public ClassName(){

}

显式字段初始化

不管怎样调用构造器,每个实例字段都要设置为一个有意义的初值。

可以在类定义中直接为任何字段赋值。例如:

class Employee{
    private String name = "";
    ...
}

在执行构造器之前先完成这个赋值操作。适用于一个类的所有构造器把某个特定的实例字段设置为同一个值。

初始值不一定是常量值。可以利用方法调用初始化一个字段。

调用另一个构造器

this 关键字可以指示一个方法的隐式参数。不过,这个关键字还有另外一个含义。

如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器。下面是一个例子;

public Employee(double s){
    //调用 Employee(String, double)
    this("Employee #" + nextId, s);
    nextId++;
}

当调用 new Employee(60000) 时,Employee(double) 构造器将调用 Employee(String, double) 构造器。

采用这种方式使用 this 关键字非常有用,这样对公共的构造器代码只需编写一次。

初始化块

前面已经介绍了两种初始化字段的方法

  • 在构造器中设置值
  • 在声明中赋值

实际上,Java 还有第三种机制,称为初始化块(initialization block)。在一个类的声明中,可以包含任意多个代码块。只要构造这个类的对象,这些块就会被执行。例如:

class Employee{
    private static int nextId;

    private int id;
    private String name;
    private double salary;

    //初始化块
    {
        id = nextId
        nextId++;
    }

    public Employee(String aName, double aSalary){
        name = aName;
        salary = aSalary;
    }

    public Employee(){
        name = "";
        salary = 0;
    }
    ...
}

在这个示例中无论使用哪个构造器构造对象,id 字段都会在对象初始化块中初始化。首先运行初始化块,然后才运行构造器的主体部分。

这种机制不是必须的,也不常见。通常会直接将初始化代码放在构造器中。

另外,如果类的静态字段需要很复杂的初始化代码,可以使用静态的初始化块。

将代码放在一个块中,并标记关键字 static。下面是一个示例。其功能是将员工 ID 的起始值赋予一个小于 10000 的随机整数:

static{
    var generator = new Random();
    nextId = generator.nextInt(10000);
}

注意

当且仅当类第一次加载的时候,会进行静态字段的初始化。

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.5