Java之道/有趣的对象
虽然字符串是对象,但它们并不是很有趣的对象,因为
- 它们是不可变的。
- 它们没有实例变量。
- 您不必使用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 中最大且最常用的包之一。
- 实例:类别中的一个例子。我的猫是“猫科动物”类别的一个实例。每个对象都是某个类的实例。
- 实例变量:构成对象的一个命名数据项。每个对象(实例)都拥有其类实例变量的副本。
- 引用:指示对象的数值。在状态图中,引用显示为箭头。
- 别名:两个或多个变量引用同一个对象的情况。
- 垃圾回收:查找没有引用的对象并回收其存储空间的过程。
- 状态:在程序执行的给定时间点,所有变量和对象及其值的完整描述。
- 状态图:以图形方式显示的程序状态快照。