跳转至内容

Java之道/有趣的对象

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

有趣的对象

[编辑 | 编辑源代码]

什么是有趣的?

[编辑 | 编辑源代码]

虽然字符串是对象,但它们并不是很有趣的对象,因为

  • 它们是不可变的。
  • 它们没有实例变量。
  • 您不必使用new构造来创建它们。

在本章中,我们将使用 Java 语言中包含的两种新的对象类型:Point 和 Rectangle。请注意,这些不是出现在屏幕上的图形对象,而是包含数据的变量,就像 int 和 double 一样。与其他变量一样,它们可以在内部用于执行计算。

Point 和 Rectangle 类的定义位于 java.awt 包中,因此我们必须导入它们。

内置的 Java 类被分成许多包,包括 java.lang,其中包含我们迄今为止看到的大多数类,以及 java.awt,其中包含与 Java 抽象窗口工具包 (AWT) 相关的类,其中包含窗口、按钮、图形等的类。

为了使用一个包,您必须导入它,这就是为什么图形部分中的程序以 import java.awt.* 开头的原因。* 表示我们希望导入 AWT 包中的所有类。如果需要,您可以显式命名要导入的类,但这没有太大优势。java.lang 中的类会自动导入,这就是为什么我们的大多数程序不需要 import 语句的原因。

所有 import 语句都出现在程序的开头,在类定义之外。

点对象

[编辑 | 编辑源代码]

在最基本的层面上,点是两个数字(坐标),我们将其作为一个整体视为单个对象。在数学符号中,点通常用括号括起来,用逗号分隔坐标。例如,表示原点,表示从原点向右移动单位和向上移动单位的点。

在 Java 中,点由 Point 对象表示。要创建一个新点,您必须使用new命令

    Point blank;
    blank = new Point (3, 4);

第一行是常规的变量声明:blank 的类型为 Point。第二行看起来有点奇怪;它调用了 new 命令,指定了新对象的类型,并提供了参数。您可能不会感到惊讶,这些参数是新点的坐标。

new 命令的结果是对新点的引用。我稍后会更详细地解释引用;目前重要的是变量 blank 包含新创建对象的地址。

总而言之,程序中的所有变量、值和对象都称为状态。像这样显示程序状态的图表称为状态图。随着程序的运行,状态会发生变化,因此您应该将状态图视为执行特定点的快照。

实例变量

[编辑 | 编辑源代码]

构成对象的数据片段有时称为组件、记录或字段。在 Java 中,它们被称为实例变量,因为每个对象(其类型的实例)都有自己的实例变量副本。

这就像汽车的手套箱。每辆汽车都是汽车类型的实例,每辆汽车都有自己的手套箱。如果您让我从您的汽车的手套箱里拿东西,您必须告诉我哪辆车是您的。

类似地,如果您想从实例变量中读取值,则必须指定要从中获取该值的 对象。在 Java 中,这是使用点表示法完成的。

    int x = blank.x;

表达式 blank.x 表示转到 blank 引用的对象,并获取 x 的值。在这种情况下,我们将该值分配给名为 x 的局部变量。请注意,名为 x 的局部变量与名为 x 的实例变量之间没有冲突。点表示法的目的是明确识别您正在引用的哪个变量。

您可以将点表示法用作任何 Java 表达式的一部分,因此以下内容是合法的。

    System.out.println (blank.x + ", " + blank.y);
    int distance = blank.x * blank.x + blank.y * blank.y;

第一行打印 3, 4;第二行计算值 25。

对象作为参数

[编辑 | 编辑源代码]

您可以按照通常的方式将对象作为参数传递。例如

  public static void printPoint (Point p){
    System.out.println ("(" + p.x + ", " + p.y + ")");
  }

是一个以点作为参数并以标准格式打印它的方法。如果您调用 printPoint (blank),它将打印 (3, 4)。实际上,Java 有一个内置的方法来打印点。如果您调用 System.out.println (blank),您将得到:java.awt.Point[x=3,y=4]

这是 Java 用于打印对象的标准格式。它打印类型的名称,然后打印对象的内容,包括实例变量的名称和值。

作为第二个示例,我们可以重写距离部分中的距离方法,使其以两个点作为参数,而不是四个双精度数。

  public static double distance (Point p1, Point p2) {
    double dx = (double)(p2.x - p1.x);
    double dy = (double)(p2.y - p1.y);
    return Math.sqrt (dx*dx + dy*dy);
  }

类型转换实际上并不是必需的;我只是将它们添加为提醒,Point 中的实例变量是整数。

矩形类似于点,只是它们有四个实例变量,名为 x、y、width 和 height。除此之外,其他一切都非常相似。

    Rectangle box = new Rectangle (0, 0, 100, 200);

创建一个新的 Rectangle 对象,并使 box 指向它。

如果您打印 box,您将得到

   java.awt.Rectangle[x=0,y=0,width=100,height=200]

同样,这是内置的 Java 方法的结果,该方法知道如何打印 Rectangle 对象。


对象作为返回值类型

[编辑 | 编辑源代码]

您可以编写返回对象的方法。例如,findCenter 以 Rectangle 作为参数,并返回一个包含 Rectangle 中心坐标的 Point

  public static Point findCenter (Rectangle box)
    int x = box.x + box.width/2;
    int y = box.y + box.height/2;
    return new Point (x, y);

请注意,您可以使用 new 创建一个新对象,然后立即将结果用作返回值。

对象是可变的

[编辑 | 编辑源代码]

您可以通过为其一个实例变量赋值来更改对象的内容。例如,要移动矩形而不更改其大小,您可以修改 x 和 y 值

    box.x = box.x + 50;
    box.y = box.y + 100;

我们可以获取此代码并将其封装在一个方法中,并将其推广以按任意量移动矩形

  public static void moveRect (Rectangle box, int dx, int dy)
    box.x = box.x + dx;
    box.y = box.y + dy;

变量 dx 和 dy 指示在每个方向上移动矩形的距离。调用此方法会修改作为参数传递的 Rectangle。

    Rectangle box = new Rectangle (0, 0, 100, 200);
    moveRect (box, 50, 100);
    System.out.println (box);

打印

   java.awt.Rectangle[x=50,y=100,width=100,height=200].

通过将对象作为参数传递给方法来修改对象可能很有用,但它也可能使调试变得更加困难,因为并不总是清楚哪些方法调用会修改其参数,哪些不会。稍后,我将讨论这种编程风格的一些优缺点。

同时,我们可以享受 Java 内置方法的便利,其中包括 translate,它与 moveRect 做完全相同的事情,尽管调用它的语法略有不同。我们不是将 Rectangle 作为参数传递,而是在 Rectangle 上调用 translate,并将 dx 和 dy 作为参数传递。

    box.translate (50, 100);

效果完全相同。

别名

aliasing
reference

请记住,当您将值赋给对象变量时,您实际上是在赋予一个对对象的引用。多个变量可以引用同一个对象。例如,这段代码

    Rectangle box1 = new Rectangle (0, 0, 100, 200);
    Rectangle box2 = box1;

box1 和 box2 都引用或指向同一个对象。换句话说,这个对象有两个名称:box1 和 box2。当一个人使用两个名字时,我们称之为别名。对象也一样。

当两个变量是别名时,任何影响一个变量的更改也会影响另一个变量。例如

    System.out.println (box2.width);
    box1.grow (50, 50);
    System.out.println (box2.width);

第一行打印 100,这是 box2 所引用的 Rectangle 的宽度。第二行在 box1 上调用 grow 方法,这会将 Rectangle 在各个方向上扩展 50 像素(有关更多详细信息,请参阅文档)。

从该图中可以清楚地看出,对 box1 做出的任何更改也适用于 box2。因此,第三行打印的值为 200,即扩展后的矩形的宽度。(顺便说一句,Rectangle 的坐标为负数是完全合法的。)

正如您从这个简单的示例中可以看出的那样,涉及别名的代码很快就会变得混乱,并且很难调试。一般来说,应避免使用别名或谨慎使用。

当您创建对象变量时,请记住您正在创建对对象的引用。在您让变量指向某个对象之前,变量的值为 null。null 是 Java 中的一个特殊值(也是一个 Java 关键字),用于表示“无对象”。

声明 Point blank; 等效于此初始化

    Point blank = null;

如果您尝试使用 null 对象,无论是通过访问实例变量还是调用方法,您都会得到一个 NullPointerException。系统将打印错误消息并终止程序。

    Point blank = null;
    int x = blank.x;              // NullPointerException
    blank.translate (50, 50);     // NullPointerException

另一方面,将 null 对象作为参数传递或接收作为返回值是合法的。事实上,这样做很常见,例如表示空集或指示错误条件。

垃圾回收

[编辑 | 编辑源代码]

在别名部分,我们讨论了当多个变量引用同一个对象时会发生什么。当没有变量引用对象时会发生什么?例如

    Point blank = new Point (3, 4);
    blank = null;

第一行创建一个新的 Point 对象并使 blank 指向它。第二行更改 blank,使其不再引用该对象,而是引用空值(null 对象)。

如果没有人引用某个对象,那么就没有人可以读取或写入它的任何值,或者在其上调用方法。实际上,它将不复存在。我们可以将对象保留在内存中,但这只会浪费空间,因此,在程序运行期间,Java 系统会定期查找孤立的对象并回收它们,这个过程称为垃圾回收。稍后,对象占用的内存空间将可用于作为新对象的一部分使用。

您无需执行任何操作即可使垃圾回收生效,并且通常您不会注意到它。

对象和基本类型

[编辑 | 编辑源代码]

Java 中有两种类型的类型:基本类型和对象类型。基本类型,如 int 和 boolean,以小写字母开头;对象类型以大写字母开头。这种区别很有用,因为它提醒我们它们之间的一些差异。

当您声明基本类型变量时,您会获得用于存储基本类型值的存储空间。当您声明对象变量时,您会获得用于存储对对象的引用的空间。为了获得对象本身的空间,您必须使用 new 命令。

如果您没有初始化基本类型,则会为其赋予一个默认值,该值取决于类型。例如,int 的默认值为 0,boolean 的默认值为 true。对象类型的默认值为 null,表示没有对象。

基本类型变量很好地隔离,因为您无法在一个方法中执行任何操作来影响另一个方法中的变量。对象变量可能难以使用,因为它们没有很好地隔离。如果您将对对象的引用作为参数传递,则调用的方法可能会修改该对象,在这种情况下,您将看到效果。当您在对象上调用方法时,情况也是如此。当然,这可能是件好事,但您必须意识到这一点。

基本类型和对象类型之间还有另一个区别。您不能向 Java 语言中添加新的基本类型(除非您加入标准委员会),但您可以创建新的对象类型!我们将在下一章中看到如何操作。

术语表

[编辑 | 编辑源代码]
  • 包(package):类的集合。内置 Java 类按包组织。
  • AWT:抽象窗口工具包(Abstract Window Toolkit),Java 中最大且最常用的包之一。
  • 实例:类别中的一个例子。我的猫是“猫科动物”类别的一个实例。每个对象都是某个类的实例。
  • 实例变量:构成对象的一个命名数据项。每个对象(实例)都拥有其类实例变量的副本。
  • 引用:指示对象的数值。在状态图中,引用显示为箭头。
  • 别名:两个或多个变量引用同一个对象的情况。
  • 垃圾回收:查找没有引用的对象并回收其存储空间的过程。
  • 状态:在程序执行的给定时间点,所有变量和对象及其值的完整描述。
  • 状态图:以图形方式显示的程序状态快照。
华夏公益教科书