三、万物皆对象(2)

本章概要

  • 类的创建
    • 类型
    • 字段
    • 基本类型默认值
    • 方法使用
      • 返回类型
      • 参数列表
  • 程序编写
    • 命名可见性
    • 使用其他组件
    • static 关键字
  • 小试牛刀
    • 编译和运行
  • 编码风格

类的创建

类型

如果一切都是对象,那么是什么决定了某一类对象的外观和行为呢?换句话说,是什么确定了对象的类型?你可能很自然地想到 type 关键字。但是,事实上大多数面向对象的语言都使用 class 关键字类来描述一种新的对象。 通常在 class 关键字的后面的紧跟类的的名称。如下代码示例:

class ATypeName {
 // 这里是类的内部
}

在上例中,我们引入了一个新的类型,尽管这个类里只有一行注释。但是我们一样可以通过 new 关键字来创建一个这种类型的对象。如下:

ATypeName a = new ATypeName();

到现在为止,我们还不能用这个对象来做什么事(即不能向它发送任何有意义的消息),除非我们在这个类里定义一些方法。

字段

当我们创建好一个类之后,我们可以往类里存放两种类型的元素:方法(method)和字段(field)。类的字段可以是基本类型,也可以是引用类型。如果类的字段是对某个对象的引用,那么必须要初始化该引用将其关联到一个实际的对象上(通过之前介绍的创建对象的方法)。每个对象都有用来存储其字段的空间。通常,字段不在对象间共享。下面是一个具有某些字段的类的代码示例:

class DataOnly {
    int i;
    double d;
    boolean b;
}

这个类除了存储数据之外什么也不能做。但是,我们仍然可以通过下面的代码来创建它的一个对象:

    DataOnly data = new DataOnly();

我们必须通过这个对象的引用来指定字段值。格式:对象名称.方法名称或字段名称。代码示例:

data.i = 47;
data.d = 1.1;
data.b = false;

如果你想修改对象内部包含的另一个对象的数据,可以通过这样的格式修改。代码示例:

myPlane.leftTank.capacity = 100;

你可以用这种方式嵌套许多对象(尽管这样的设计会带来混乱)。

基本类型默认值

如果类的成员变量(字段)是基本类型,那么在类初始化时,这些类型将会被赋予一个初始值。

基本类型初始值
booleanfalse
char\u0000 (null)
byte(byte) 0
short(short) 0
int0
long0L
float0.0f
double0.0d

这些默认值仅在 Java 初始化类的时候才会被赋予。这种方式确保了基本类型的字段始终能被初始化(在 C++ 中不会),从而减少了 bug 的来源。但是,这些初始值对于程序来说并不一定是合法或者正确的。 所以,为了安全,我们最好始终显式地初始化变量。

这种默认值的赋予并不适用于局部变量 —— 那些不属于类的字段的变量。 因此,若在方法中定义的基本类型数据,如下:

int x;

这里的变量 x 不会自动初始化为0,因而在使用变量 x 之前,程序员有责任主动地为其赋值(和 C 、C++ 一致)。如果我们忘记了这一步, Java 将会提示我们“编译时错误,该变量可能尚未被初始化”。 这一点做的比 C++ 更好,在后者中,编译器只是提示警告,而在 Java 中则直接报错。

方法使用

在许多语言(如 C 和 C++)中,使用术语 函数 (function) 用来命名子程序。在 Java 中,我们使用术语 方法(method)来表示“做某事的方式”。

在 Java 中,方法决定对象能接收哪些消息。方法的基本组成部分包括名称、参数、返回类型、方法体。格式如:

 [返回类型] [方法名](/*参数列表*/){
     // 方法体
 }
返回类型

方法的返回类型表明了当你调用它时会返回的结果类型。参数列表则显示了可被传递到方法内部的参数类型及名称。方法名和参数列表统称为方法签名(signature of the method)。签名作为方法的唯一标识。

Java 中的方法只能作为类的一部分创建。它只能被对象所调用,并且该对象必须有权限来执行调用。若对象调用错误的方法,则程序将在编译时报错。

我们可以像下面这样调用一个对象的方法:

[对象引用].[方法名](参数1, 参数2, 参数3);

若方法不带参数,例如一个对象引用 a 的方法 f 不带参数并返回 int 型结果,我们可以如下表示:

int x = a.f();

上例中方法 f 的返回值类型必须和变量 x 的类型兼容 。调用方法的行为有时被称为向对象发送消息。面向对象编程可以总结为:向对象发送消息。

参数列表

方法参数列表指定了传递给方法的信息。正如你可能猜到的,这些信息就像 Java 中的其他所有信息 ,以对象的形式传递。参数列表必须指定每个对象的类型和名称。同样,我们并没有直接处理对象,而是在传递对象引用。但是引用的类型必须是正确的。如果方法需要 String 参数,则必须传入 String,否则编译器将报错。

int storage(String s) {
    return s.length() * 2;
}

此方法计算并返回某个字符串所占的字节数。参数 s 的类型为 String 。将 s 传递给 storage() 后,我们可以把它看作和任何其他对象一样,可以向它发送消息。在这里,我们调用 length() 方法,它是一个 String 方法,返回字符串中的字符数。字符串中每个字符的大小为 16 位或 2 个字节。你还看到了 return 关键字,它执行两项操作。首先,它意味着“方法执行结束”。其次,如果方法有返回值,那么该值就紧跟 return 语句之后。这里,返回值是通过计算

s.length() * 2

产生的。在方法中,我们可以返回任何类型的数据。如果我们不想方法返回数据,则可以通过给方法标识 void 来表明这是一个无需返回值的方法。 代码示例:

boolean flag() { 
    return true; 
}

double naturalLogBase() { 
    return 2.718; 
}

void nothing() {
     return;
}

void nothing2() {

}

当返回类型为 void 时, return 关键字仅用于退出方法,因此在方法结束处的 return 可被省略。我们可以随时从方法中返回,但若方法返回类型为非 void,则编译器会强制我们返回相应类型的值。

上面的描述可能会让你感觉程序只不过是一堆包含各种方法的对象,在这些方法中,将对象作为参数并发送消息给其他对象。大部分情况下确实如此。但在下一章的运算符中我们将会学习如何在方法中做出决策来完成更底层、详细的工作。对于本章,知道如何发送消息就够了。

在看到第一个 Java 程序之前,我们还必须理解其他几个问题。

程序编写

命名可见性

命名控制在任何一门编程语言中都是一个问题。如果你在两个模块中使用相同的命名,那么如何区分这两个名称,并防止两个名称发生“冲突”呢?在 C 语言编程中这是很具有挑战性的,因为程序通常是一个无法管理的名称海洋。C++ 将函数嵌套在类中,所以它们不会和嵌套在其他类中的函数名冲突。然而,C++ 还是允许全局数据和全局函数,因此仍有可能发生冲突。为了解决这个问题,C++ 使用附加的关键字引入了_命名空间_。

Java 采取了一种新的方法避免了以上这些问题:为一个类库生成一个明确的名称,Java 创建者希望我们反向使用自己的网络域名,因为域名通常是唯一的。因此我的域名是 MindviewInc.com,所以我将我的 foibles 类库命名为 com.mindviewinc.utility.foibles。反转域名后,. 用来代表子目录的划分。

在 Java 1.0 和 Java 1.1 中,域扩展名 com、 edu、 org 和 net 等按惯例大写,因此类库中会出现这样类似的名称:Com.mindviewinc.utility.foibles。然而,在 Java 2 的开发过程中,他们发现这会导致问题,所以现在整个包名都是小写的。此机制意味着所有文件都自动存在于自己的命名空间中,文件中的每个类都具有唯一标识符。这样,Java 语言可以防止名称冲突。

使用反向 URL 是一种新的命名空间方法,在此之前尚未有其他语言这么做过。Java 中有许多这些“创造性”地解决问题的方法。正如你想象,如果我们未经测试就添加一个功能并用于生产,那么在将来发现该功能的问题再想纠正,通常为时已晚(有些错误太严重了就得从语言中删除新功能。)

使用反向 URL 将命名空间与文件路径相关联不会导致BUG,但它却给源代码管理带来麻烦。例如在 com.mindviewinc.utility.foibles 这样的目录结构中,我们创建了 commindviewinc 空目录。它们存在的唯一目的就是用来表示这个反向的 URL。

这种方式似乎为我们在编写 Java 程序中的某个问题打开了大门。空目录填充了深层次结构,它们不仅用于表示反向 URL,还用于捕获其他信息。这些长路径基本上用于存储有关目录中的内容的数据。如果你希望以最初设计的方式使用目录,这种方法可以从“令人沮丧”到“令人抓狂”,对于生产级的 Java 代码,你必须使用专门为此设计的 IDE 来管理代码。例如 NetBeans,Eclipse 或 IntelliJ IDEA。实际上,这些 IDE 都为我们管理和创建深层次空目录结构。

对于这本书中的例子,我不想让深层次结构给你的学习带来额外的麻烦,这实际上需要你在开始之前学习熟悉一种重量级的 IDE。所以,我们的每个章节的示例都位于一个浅的子目录中,以章节标题为名。这导致我偶尔会与遵循深层次方法的工具发生冲突。

使用其他组件

无论何时在程序中使用预先定义好的类,编译器都必须找到该类。最简单的情况下,该类存在于被调用的源代码文件中。此时我们使用该类 —— 即使该类在文件的后面才会被定义(Java 消除了所谓的“前向引用”问题)。而如果一个类位于其他文件中,又会怎样呢?你可能认为编译器应该足够智能去找到它,但这样是有问题的。想象一下,假如你要使用某个类,但目录中存在多个同名的类(可能用途不同)。或者更糟糕的是,假设你正在编写程序,在构建过程中,你想将某个新类添加到类库中,但却与已有的类名称冲突。

要解决此问题,你必须通过使用 import 关键字来告诉 Java 编译器具体要使用的类。import 指示编译器导入一个包,也就是一个类库(在其他语言中,一个库不仅包含类,还可能包括函数和数据,但请记住 Java 中的所有代码都必须写在类里)。大多数时候,我们都在使用 Java 标准库中的组件。有了这些构件,你就不必写一长串的反转域名。例如:

import java.util.ArrayList;

上例可以告诉编译器使用位于标准库 util 下的 ArrayList 类。但是,util 中包含许多类,我们可以使用通配符 * 来导入其中部分类,而无需显式得逐一声明这些类。代码示例:

import java.util.*;

本书中的示例很小,为简单起见,我们通常会使用 .* 形式略过导入。然而,许多教程书籍都会要求程序员逐一导入每个类。

static关键字

类是对象的外观及行为方式的描述。通常只有在使用 new 创建那个类的对象后,数据存储空间才被分配,对象的方法才能供外界调用。这种方式在两种情况下是不足的。

  1. 有时你只想为特定字段(注:也称为属性、域)分配一个共享存储空间,而不去考虑究竟要创建多少对象,甚至根本就不创建对象。
  2. 创建一个与此类的任何对象无关的方法。也就是说,即使没有创建对象,也能调用该方法。

static 关键字(从 C++ 采用)就符合上述两点要求。当我们说某个事物是静态时,就意味着该字段或方法不依赖于任何特定的对象实例 。 即使我们从未创建过该类的对象,也可以调用其静态方法或访问其静态字段。相反,对于普通的非静态字段和方法,我们必须要先创建一个对象并使用该对象来访问字段或方法,因为非静态字段和方法必须与特定对象关联。

一些面向对象的语言使用类数据(class data)和类方法(class method),表示静态数据和方法只是作为类,而不是类的某个特定对象而存在的。有时 Java 文献也使用这些术语。

我们可以在类的字段或方法前添加 static 关键字来表示这是一个静态字段或静态方法。 代码示例:

class StaticTest {
    static int i = 47;
}

现在,即使你创建了两个 StaticTest 对象,但是静态变量 i 仍只占一份存储空间。两个对象都会共享相同的变量 i。 代码示例:

StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();

st1.ist2.i 指向同一块存储空间,因此它们的值都是 47。引用静态变量有两种方法。在前面的示例中,我们通过一个对象来定位它,例如 st2.i。我们也可以通过类名直接引用它,这种方式对于非静态成员则不可行:

StaticTest.i++;

++ 运算符将会使变量结果 + 1。此时 st1.ist2.i 的值都变成了 48。

使用类名直接引用静态变量是首选方法,因为它强调了变量的静态属性。类似的逻辑也适用于静态方法。我们可以通过对象引用静态方法,就像使用任何方法一样,也可以通过特殊的语法方式 Classname.method() 来直接调用静态字段或方法。 代码示例:

class Incrementable {
    static void increment() { 
      StaticTest.i++; 
    }
}

上例中,Incrementableincrement() 方法通过 ++ 运算符将静态数据 i 加 1。我们依然可以先实例化对象再调用该方法。 代码示例:

Incrementable sf = new Incrementable();
sf.increment();

当然了,首选的方法是直接通过类来调用它。代码示例:

Incrementable.increment()

相比非静态的对象,static 属性改变了数据创建的方式。同样,当 static 关键字修饰方法时,它允许我们无需创建对象就可以直接通过类的引用来调用该方法。正如我们所知,static 关键字的这些特性对于应用程序入口点的 main() 方法尤为重要。

小试牛刀

最后,我们开始编写第一个完整的程序。我们使用 Java 标准库中的 Date 类来展示一个字符串和日期。

// objects/HelloDate.java
import java.util.*;

public class HelloDate {
    public static void main(String[] args) {
        System.out.println("Hello, it's: ");
        System.out.println(new Date());
    }
}

在本书中,所有代码示例的第一行都是注释行,其中包含文件的路径信息(比如本章的目录名是 objects),后跟文件名。我的工具可以根据这些信息自动提取和测试书籍的代码,你也可以通过参考第一行注释轻松地在 Github 库中找到对应的代码示例。

如果你想在代码中使用一些额外的类库,那么就必须在程序文件的开始处使用 import 关键字来导入它们。之所以说是额外的,因为有一些类库已经默认自动导入到每个文件里了。例如:java.lang 包。

现在打开你的浏览器在 Oracle 上查看文档。如果你还没有从 Oracle 网站上下载 JDK 文档,那现在就去。查看包列表,你会看到 Java 附带的所有不同的类库。

选择 java.lang,你会看到该库中所有类的列表。由于 java.lang 隐式包含在每个 Java 代码文件中,因此这些类是自动可用的。java.lang 类库中没有 Date 类,所以我们必须导入其他的类库(即 Date 所在的类库)。如果你不清楚某个类所在的类库或者想查看类库中所有的类,那么可以在 Java 文档中选择 “Tree” 查看。

现在,我们可以找到 Java 附带的每个类。使用浏览器的“查找”功能查找 Date,搜索结果中将会列出 java.util.Date,我们就知道了 Dateutil 库中,所以必须导入 java.util.* 才能使用 Date

如果你在文档中选择 java.lang,然后选择 System,你会看到 System 类中有几个字段,如果你选择了 out,你会发现它是一个静态的 PrintStream 对象。 所以,即使我们不使用 new 创建, out 对象就已经存在并可以使用。 out 对象可以执行的操作取决于它的类型: PrintStream ,其在文档中是一个超链接,如果单击该链接,我们将可以看到 PrintStream 对应的方法列表(更多详情,将在本书后面介绍)。 现在我们重点说的是 println() 这个方法。 它的作用是 “将信息输出到控制台,并以换行符结束”。既然如此,我们可以这样编码来输出信息到控制台。 代码示例:

System.out.println("A String of things");

每个 java 源文件中允许有多个类。同时,源文件的名称必须要和其中一个类名相同,否则编译器将会报错。每个独立的程序应该包含一个 main() 方法作为程序运行的入口。其方法签名和返回类型如下。代码示例:

public static void main(String[] args) {
}

关键字 public 表示方法可以被外界访问到。( 更多详情将在 隐藏实现 章节讲到)
main() 方法的参数是一个 字符串(String) 数组。 参数 args 并没有在当前的程序中使用到,但是 Java 编译器强制要求必须要有, 这是因为它们被用于接收从命令行输入的参数。

下面我们来看一段有趣的代码:

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

上面的示例中,我们创建了一个日期(Date)类型的对象并将其转化为字符串类型,输出到控制台中。 一旦这一行语句执行完毕,我们就不再需要该日期对象了。这时,Java 垃圾回收器就可以将其占用的内存回收,我们无需去主动清除它们。

查看 JDK 文档时,我们可以看到在 System 类下还有很多其他有用的方法( Java 的牛逼之处还在于,它拥有一个庞大的标准库资源)。代码示例:

// objects/ShowProperties.java
public class ShowProperties {
    public static void main(String[] args) {
        System.getProperties().list(System.out);
        System.out.println(System.getProperty("user.name"));
        System.out.println(System.getProperty("java.library.path"));
    }
}

输出结果(部分):

在这里插入图片描述

main() 方法中的第一行会输出所有的系统字段,也就是环境信息。 list() 方法将结果发送给它的参数 System.out。在本书的后面,我们还会接触到将结果输出到其他地方,例如文件中。另外,我们还可以请求特定的字段。该例中我们使用到了 user.namejava.library.path

编译和运行

要编译和运行本书中的代码示例,首先必须具有 Java 编程环境。 第二章的示例中描述了安装过程。如果你遵循这些说明,那么你将会在不受 Oracle 的限制的条件下用到 Java 开发工具包(JDK)。如果你使用其他开发系统,请查看该系统的文档以确定如何编译和运行程序。 第二章还介绍了如何安装本书的示例。

移动到子目录 objects 下并键入:

javac HelloDate.java

此命令不应产生任何响应。如果我们收到任何类型的错误消息,则表示未正确安装 JDK,那就得检查这些问题。

若执行不报错的话,此时可以键入:

java HelloDate

我们将会得到正确的日期输出。这是我们编译和运行本书中每个程序(包含 main() 方法)的过程。此外,本书的源代码在根目录中也有一个名为 build.gradle 的文件,其中包含用于自动构建,测试和运行本书文件的 Gradle 配置。当你第一次运行 gradlew 命令时,Gradle 将自动安装(前提是已安装Java)。

编码风格

Java 编程语言编码规范(Code Conventions for the Java Programming Language)要求类名的首字母大写。 如果类名是由多个单词构成的,则每个单词的首字母都应大写(不采用下划线来分隔)例如:

class AllTheColorsOfTheRainbow {
    // ...
}

有时称这种命名风格叫“驼峰命名法”。对于几乎所有其他方法,字段(成员变量)和对象引用名都采用驼峰命名的方式,但是它们的首字母不需要大写。代码示例:

class AllTheColorsOfTheRainbow {
    int anIntegerRepresentingColors;
    void changeTheHueOfTheColor(int newHue) {
        // ...
    }
    // ...
}

在 Oracle 的官方类库中,花括号的位置同样遵循和本书中上述示例相同的规范。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小熊猫呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值