跳转到内容

Java/面向对象编程之道

来自 Wikibooks,开放的书籍,开放的世界

面向对象编程

[编辑 | 编辑源代码]

编程语言和风格

[编辑 | 编辑源代码]
programming language
language!programming
programming style
object-oriented programming
functional programming
procedural programming
programming!object-oriented
programming!functional
programming!procedural

世界上有许多编程语言,编程风格(有时称为范式)也几乎一样多。本书中出现的三个风格是过程式、函数式和面向对象。虽然 Java 通常被认为是一种面向对象语言,但可以使用任何风格编写 Java 程序。我在本书中展示的风格几乎是过程式的。现有的 Java 程序和内置的 Java 包以三种风格的混合形式编写,但它们比本书中的程序更倾向于面向对象。

很难定义什么是面向对象编程,但以下是一些它的特征

项目符号

对象定义(类)通常对应于相关的现实世界对象。例如,在第 deck 章中,创建 Deck 类是迈向面向对象编程的一步。

大多数方法是对象方法(您在对象上调用的方法),而不是类方法(您只是调用的方法)。到目前为止,我们编写的所有方法都是类方法。在本章中,我们将编写一些对象方法。

与面向对象编程最相关的语言特性是继承。我将在本章后面介绍继承。

项目符号

inheritance

最近,面向对象编程变得非常流行,有些人声称它在各个方面都优于其他风格。我希望通过向您介绍各种风格,我已经为您提供了理解和评估这些主张所需的工具。

对象和类方法

[编辑 | 编辑源代码]
object method
method!object
class method
method!class
static

Java 中有两种类型的方法,称为类方法和对象方法。到目前为止,我们编写的每种方法都是类方法。类方法由第一行中的关键字 static 标识。任何没有关键字 static 的方法都是对象方法。

虽然我们没有编写任何对象方法,但我们已经调用了一些。每当您在 _某个对象_ 上调用方法时,它就是一个对象方法。例如,drawOval 是我们在 g 上调用的对象方法,g 是一个 Graphics 对象。此外,我们在字符串章节中调用的字符串方法也是对象方法。

Graphics
class!Graphics

任何可以用类方法编写的东西也可以用对象方法编写,反之亦然。有时使用其中一种方法比另一种方法更自然。出于很快就会变得清晰的原因,对象方法通常比相应的类方法更短。

当前对象

[编辑 | 编辑源代码]
current object
object!current
this

当您在对象上调用方法时,该对象成为当前对象。在方法内部,您可以通过名称引用当前对象的实例变量,而无需指定对象名称。

constructor

此外,您可以使用关键字 this 引用当前对象。我们已经看到它在构造函数中使用。实际上,您可以将构造函数视为一种特殊类型的对象方法。

complex number
Complex
class!Complex
arithmetic!complex

作为本章剩余部分的运行示例,我们将考虑一个用于复数的类定义。复数在数学和工程的许多分支中都很有用,许多计算是使用复数运算执行的。复数是实部和虚部的总和,通常写成 _的形式,其中_ 是实部,_ 是虚部,表示 -1 的平方根。因此,_。

以下是用于名为 Complex 的新对象类型的新类定义

逐字类 Complex

 // instance variables
 double real, imag;
 // constructor
 public Complex () 
   this.real = 0.0;  this.imag = 0.0;
 
 // constructor
 public Complex (double real, double imag) 
   this.real = real;  this.imag = imag;
 

逐字

这里应该没有什么令人惊讶的。实例变量是两个包含实部和虚部的双精度数。两个构造函数是常见的类型:一个不接受参数并将默认值分配给实例变量,另一个接受与实例变量相同的参数。正如我们之前所见,关键字 this 用于引用正在初始化的对象。

instance variable
variable!instance
constructor

在 main 中,或在任何我们想要创建 Complex 对象的地方,我们都可以选择先创建对象然后设置实例变量,或者同时进行这两项操作

逐字

   Complex x = new Complex ();
   x.real = 1.0;
   x.imag = 2.0;
   Complex y = new Complex (3.0, 4.0);

逐字

复数上的函数

[编辑 | 编辑源代码]
operator!Complex
method!function
pure function

让我们看一下我们可能想要对复数执行的一些操作。复数的绝对值定义为 _。abs 方法是一个纯函数,计算绝对值。写成类方法,它看起来像这样

逐字

 // class method
 public static double abs (Complex c) 
   return Math.sqrt (c.real * c.real + c.imag * c.imag);
  

逐字

此版本的 abs 计算 c 的绝对值,即它作为参数接收的 Complex 对象。下一个版本的 abs 是一个对象方法;它计算当前对象的绝对值(调用该方法的对象)。因此,它不接收任何参数

逐字

 // object method
 public double abs () 
   return Math.sqrt (real*real + imag*imag);
 

逐字

我删除了关键字 static 以表明这是一个对象方法。此外,我还消除了不必要的参数。在方法内部,我可以通过名称引用实例变量 real 和 imag,而无需指定对象。Java 隐式地知道我正在引用当前对象的实例变量。如果我想明确表示,我可以使用关键字 this

逐字

 // object method
 public double abs () 
   return Math.sqrt (this.real * this.real + this.imag * this.imag);
 

逐字

但这会更长,而且实际上并没有更清楚。要调用此方法,我们在对象上调用它,例如

逐字

   Complex y = new Complex (3.0, 4.0);
   double result = y.abs();

逐字

复数上的另一个函数

[编辑 | 编辑源代码]

我们可能想要对复数执行的另一个操作是加法。您可以通过添加实部和添加虚部来添加复数。写成类方法,它看起来像这样

逐字

 public static Complex add (Complex a, Complex b) 
   return new Complex (a.real + b.real, a.imag + b.imag);
 

逐字

要调用此方法,我们将两个操作数作为参数传递

逐字

   Complex sum = add (x, y);

逐字

写成对象方法,它只接受一个参数,它会将其添加到当前对象

逐字

 public Complex add (Complex b) 
   return new Complex (real + b.real, imag + b.imag);
 

逐字

同样,我们可以隐式地引用当前对象的实例变量,但要引用 b 的实例变量,我们必须使用点表示法明确地命名 b。要调用此方法,您在其中一个操作数上调用它,并将另一个操作数作为参数传递。

dot notation

逐字

   Complex sum = x.add (y);

逐字

从这些示例中您可以看到,当前对象 (this) 可以替代其中一个参数。因此,当前对象有时被称为隐式参数。

修饰符

modifier
method!modifier

作为另一个示例,我们将看一下 conjugate,它是一个修改方法,它将 Complex 数转换为它的复共轭。的复共轭是 _。

作为类方法,它看起来像这样

逐字

 public static void conjugate (Complex c) 
   c.imag = -c.imag;
 

逐字

作为对象方法,它看起来像这样

逐字

 public void conjugate () 
   imag = -imag;
 

逐字

现在你应该开始感觉到,将一种方法转换为另一种方法是一个机械的过程。经过一些练习,你就能做到不假思索地进行转换,这很好,因为你不应该被限制在编写一种方法或另一种方法。你应该对两种方法都熟悉,这样你就可以选择最适合你正在编写的操作的方法。

例如,我认为 add 应该被写成类方法,因为它是对两个操作数的对称操作,并且两个操作数都作为参数出现是合理的。在其中一个操作数上调用该方法并将另一个操作数作为参数传递似乎很奇怪。

另一方面,应用于单个对象的简单操作可以最简洁地写成对象方法(即使它们接受一些额外的参数)。

toString 方法

[edit | edit source]
toString
method!toString

有两种对象方法在许多对象类型中很常见:toString 和 equals。toString 将对象转换为一些合理的字符串表示形式,可以打印出来。equals 用于比较对象。

当你使用 print 或 println 打印一个对象时,Java 会检查你是否提供了一个名为 toString 的对象方法,如果有,它会调用它。如果没有,它会调用 toString 的默认版本,该版本会产生 Section printobject 中描述的输出。

以下是 toString 在 Complex 类中的可能样子

逐字

 public String toString () 
   return real + " + " + imag + "i";
 

逐字

toString 的返回值类型是 String,当然,它不接受任何参数。你可以像往常一样调用 toString

逐字

   Complex x = new Complex (1.0, 2.0);
   String s = x.toString ();

逐字

或者你可以通过 print 间接调用它

逐字

   System.out.println (x);

逐字

每当你将一个对象传递给 print 或 println 时,Java 都会调用该对象的 toString 方法并打印结果。在这种情况下,输出是 1.0 + 2.0i。

如果虚部为负,此版本的 toString 看起来不太好。作为练习,请修正它。

equals 方法

[edit | edit source]
equals
method!equals

当你使用 == 运算符比较两个对象时,你实际上是在问,“这两个东西是同一个对象吗?” 也就是说,这两个对象是否指向内存中的同一位置。

对于许多类型来说,这不是相等性的适当定义。例如,如果两个复数的实部相等,且虚部相等,那么这两个复数相等。

type!object

当你创建一个新的对象类型时,你可以通过提供一个名为 equals 的对象方法来提供你自己的相等性定义。对于 Complex 类,它看起来像这样

逐字

 public boolean equals (Complex b) 
   return (real == b.real && imag == b.imag);
 

逐字

按照惯例,equals 始终是一个对象方法。返回值类型必须是 boolean。

Object 类中 equals 的文档提供了一些你在制定自己的相等性定义时应该牢记的指导方针

引用

equals 方法实现了一个等价关系

equality
identity

项目符号

它是自反的:对于任何引用值 x,x.equals(x) 应该返回 true。

它是对称的:对于任何引用值 x 和 y,如果且仅当 y.equals(x) 返回 true 时,x.equals(y) 应该返回 true。

它是传递的:对于任何引用值 x、y 和 z,如果 x.equals(y) 返回 true 且 y.equals(z) 返回 true,那么 x.equals(z) 应该返回 true。

它是一致的:对于任何引用值 x 和 y,x.equals(y) 的多次调用始终返回 true 或始终返回 false。

对于任何引用值 x,x.equals(null) 应该返回 false。

项目符号

引用

我提供的 equals 定义满足除一个之外的所有条件。哪一个?作为练习,请修正它。

从另一个对象方法中调用对象方法

method!invoking

正如你所料,从另一个对象方法中调用对象方法是合法的且常见的。例如,要规范化一个复数,你用绝对值(两个部分都)除以它。这为什么有用可能并不明显,但确实有用。

让我们将 normalize 方法写成对象方法,并将其设为修饰符。

逐字

 public void normalize () 
   double d = this.abs();
   real = real/d;
   imag = imag/d;
 

逐字

第一行通过在当前对象上调用 abs 来查找当前对象的绝对值。在这种情况下,我明确地命名了当前对象,但我可以省略它。如果你在一个对象方法内调用另一个对象方法,Java 会假设你在当前对象上调用它。

作为练习,请将 normalize 重写为纯函数。然后将其重写为类方法。

奇异性和错误

[edit | edit source]
method!object
method!class
overloading

如果你在同一个类定义中既有对象方法又有类方法,很容易混淆。组织类定义的一种常见方法是将所有构造函数放在开头,然后是所有对象方法,最后是所有类方法。

你可以在同一个类定义中拥有同名对象方法和类方法,只要它们的参数数量和类型不同即可。与其他类型的重载一样,Java 会根据你提供 的参数来决定调用哪个版本。

static

现在我们知道了 static 关键字的含义,你可能已经明白 main 是一个类方法,这意味着在调用它时没有“当前对象”。

current object
this
instance variable
variable!instance

由于类方法中没有当前对象,因此使用 this 关键字是错误的。如果你尝试这样做,你可能会收到类似以下的错误消息:“未定义变量:this”。此外,你不能在不使用点表示法并提供对象名称的情况下引用实例变量。如果你尝试这样做,你可能会收到“无法对非静态变量进行静态引用……”的错误消息。这不是最好的错误消息之一,因为它使用了一些非标准语言。例如,它指的是“非静态变量”,实际上是指“实例变量”。但一旦你明白了它的意思,你就知道它的意思了。

继承

[edit | edit source]
inheritance

与面向对象编程最常关联的语言特性是继承。继承是指定义一个新类的能力,该新类是先前定义的类(包括内置类)的修改版本。

此特性的主要优点是,你可以在不修改现有类的情况下向现有类添加新的方法或实例变量。这对于内置类特别有用,因为即使你想要修改它们,你也无法修改它们。

继承被称为“继承”的原因是,新类继承了现有类中的所有实例变量和方法。扩展这个比喻,现有类有时被称为父类。

可绘制矩形

[edit | edit source]
Rectangle
class!Rectangle
drawable

作为继承的一个例子,我们将采用现有的 Rectangle 类并使其“可绘制”。也就是说,我们将创建一个名为 DrawableRectangle 的新类,该类将拥有 Rectangle 的所有实例变量和方法,以及一个名为 draw 的附加方法,该方法将接收一个 Graphics 对象作为参数并绘制矩形。

类定义看起来像这样

逐字 import java.awt.*;

class DrawableRectangle extends Rectangle

 public void draw (Graphics g) 
   g.drawRect (x, y, width, height);
 

逐字

是的,整个类定义实际上就这么多。第一行导入 java.awt 包,这是 Rectangle 和 Graphics 所在的地方。

AWT
import
statement!import

下一行表明 DrawableRectangle 继承自 Rectangle。extends 关键字用于标识父类。

其余部分是 draw 方法的定义,该方法引用了实例变量 x、y、width 和 height。引用那些没有出现在此类定义中的实例变量可能看起来很奇怪,但请记住,它们是从父类继承的。

要创建和绘制一个 DrawableRectangle,你可以使用以下代码

逐字

 public static void draw

(Graphics g, int x, int y, int width, int height)

   DrawableRectangle dr = new DrawableRectangle ();
   dr.x = 10;  dr.y = 10;
   dr.width = 200;  dr.height = 200;
   dr.draw (g);
 

逐字

draw 的参数是一个 Graphics 对象和绘图区域的边界框(而不是矩形的坐标)。

对于一个没有构造函数的类使用 new 命令可能看起来很奇怪。DrawableRectangle 继承了其父类的默认构造函数,所以这里没有问题。

constructor

我们可以设置 dr 的实例变量并在其上调用方法,方法如常。当我们调用 draw 时,Java 会调用我们在 DrawableRectangle 中定义的方法。如果我们在 dr 上调用 grow 或其他一些 Rectangle 方法,Java 会知道使用父类中定义的方法。

类层次结构

[编辑 | 编辑源代码]
class hierarchy
Object
parent class
class!parent

在 Java 中,所有类都扩展自其他类。最基本的类称为 Object。它不包含任何实例变量,但它确实提供了 equals 和 toString 方法等。

许多类扩展了 Object,包括我们编写的大多数类以及许多内置类,如 Rectangle。任何没有明确指定父类的类默认继承自 Object。

然而,一些继承链更长。例如,Slate 扩展了 Frame(见附录 slate),Frame 扩展了 Window,Window 扩展了 Container,Container 扩展了 Component,Component 扩展了 Object。无论链条有多长,Object 都是所有类的最终父类。

Java 中的所有类都可以组织成一个称为类层次结构的“家族树”。Object 通常出现在顶部,所有“子”类都在下面。例如,如果您查看 Frame 的文档,您将看到构成 Frame 家谱的那部分层次结构。

面向对象设计

[编辑 | 编辑源代码]
object-oriented design

继承是一个强大的功能。一些在没有继承的情况下会很复杂的程序可以用它简洁、简单地编写。此外,继承可以促进代码重用,因为您可以自定义内置类的行为,而无需修改它们。

另一方面,继承可能会使程序难以阅读,因为有时不清楚在调用方法时在哪里可以找到定义。例如,您可以对 Slate 调用的方法之一是 getBounds。您可以找到 getBounds 的文档吗?事实证明,getBounds 在 Slate 的父类的父类的父类的父类中定义。

此外,许多可以使用继承完成的事情可以用几乎同样优雅的方式(甚至更优雅)在没有继承的情况下完成。

词汇表

[编辑 | 编辑源代码]

描述

[对象方法:] 在对象上调用并对该对象进行操作的方法,该对象在 Java 中由关键字 this 或英文中的“当前对象”引用。对象方法没有关键字 static。

[类方法:] 带有关键字 static 的方法。类方法不会在对象上调用,并且它们没有当前对象。

[当前对象:] 调用对象方法的对象。在方法内部,当前对象由 this 引用。

[this:] 引用当前对象的关键字。

[隐式:] 任何未说出来或暗示的东西。在对象方法中,您可以隐式地(不命名对象)引用实例变量。

[显式:] 任何完整拼写出来的东西。在类方法中,对实例变量的所有引用都必须是显式的。

object method
class method
current object
this
implicit
explicit
华夏公益教科书