《On Java 中文版 基础卷》阅读笔记 对象无处不在 03
"学习一门新语言将为你打开一扇通往新世界的大门。" ---路德维希·维特根斯坦
虽然源于 C++, Java 却是一门更为 "纯正" 的面向对象语言。
C++ 和 Java 都是混合型语言,混合型编程语言允许采用多种编程风格,
然而 Java 的设计者认为这种 "混合" 的重要性并不如在 C++ 中那么高。
而 C++ 之所以是混合型的,是因为要向后兼容 C 语言。
由于 C++ 是 C 语言的超集,因此它也包含了 C 语言的一些不良特性,结果就是有时候 C++ 会显得过于复杂。
Java 语言默认你只编写面向对象程序。
所以在你准备大干一场之前,需要先把自己的思维沉浸在面向对象的世界中。
在本章中,你将学习构建 Java 程序的基础组件,并认识 Java 中的一切都是对象这个事实。
3.1 通过引用操作对象
"名字有什么意义呢?就算用别的名字称呼玫瑰,也丝毫不减她的芬芳。" ---威廉·莎士比亚,《罗密欧与朱丽叶》
所有编程语言都在内存中处理各种元素。有时候程序员必须小心处理这种内存操作。
比如,你是直接操作这些元素,还是通过某些特殊方式间接进行操作(比如 C 和 C++ 中的指针)。
Java 将一切都视为对象。
虽然说的是 "将一切都视为对象",但你实际操作的其实是该对象的引用 (reference)。
举个例子,假设有一台电视机(对象)和一个遥控器(引用),只要你掌握了遥控器这个引用,就能控制电视机这个对象。当你想要 "换频道" 或者 "降低音量" 时,你实际操作的是引用,再由引用来修改对象信息。所以,当你在房间里走来走去时,只要带着遥控器(即引用)就依然可以遥控电视机。
同样,遥控器也可以不依赖于电视机而独立存在。也就是说,引用未必会关联某个对象。比如若想保存一个词或一个句子,你就需要创建一个String 类型的引用,如下所示:
java
String s;
不过,这样你只是创建了一个引用,并没有创建一个对应的对象。
如果现在你向变量 s 发送信息,则会得到一条报错消息,因为 s 还没有连接到任何对象(也就是还没有电视机呢)。所以一种安全的做法是,始终在创建引用时就进行初始化,比如:
java
String s = "asdf";
上面的代码使用了 Java 的一个特性,即字符串 (String) 可以用带引号的文本进行初始化。而对于其他类型的对象而言,你需要使用一种更为通用的初始化方式。
3.2 必须创建所有对象
引用的作用是关联对象。通常使用 new 关键字来创建对象。new 关键字表达的意思是 "创建一个这种类型的对象"。所以在之前的例子中,你可以这样编写代码:
java
String s = new String("asdf");
这一行不仅代码代表了 "创建一个新的字符串",同时演示了如何通过一组字符来创建 String 对象。
除了 String 以外,Java 还提供了大量的预置类型可供选择。此外,你还可以创建新类型。实际上,创建新类型是 Java 编程的一项基础操作,你将在本书的后续章节中学习相关内容。
3.2.1 数据保存在哪里
当程序运行时,能够可视化其数据的排布方式是十分有帮助的,对于内存管理来说尤其如此。下面列举了 5 种数据存储方式。
1.寄存器 (register)。这是速度最快的数据存储方式,因为它保存数据的位置不同于其他方式:数据会直接保存在中央处理器 (central processing unit, CPU) 里。然而寄存器的数量是有限的,所以只能按需分配。
此外,你不能直接控制寄存器的分配,甚至你在程序中都找不到寄存器存在过的证据(C 和 C++ 是例外,它们允许你向编译器申请分配寄存器)。
2.栈 (stack)。数据存储在随机存取存储器 (random-access memory, RAM) 里,处理器可以通过栈指针 (stack pointer) 直接操作该数据。
具体来说,栈指针向下移动将申请一块新的内存,向上移动则会释放这块内存。
这是一种极其迅速和高效的内存分配方式,其效率仅次于寄存器。
只不过 Java 在创建应用程序时就必须明确栈上所有对象的生命周期。这种限制约束了程序的灵活性,因此虽然有一些数据会保存在栈上(尤其是对象引用),对象本身却并非如此。
3.堆 (heap)。这是一个通用的内存池(使用的也是 RAM 空间),用于存放所有 Java 对象。
与栈不同的是,编译器并不关心位于堆上的对象需要存在多久。因此,堆的使用是非常灵活的。
比如,当你需要一个对象时,可以随时使用 new 来创建这个对象,那么当这段代码被执行时,Java 会在堆上为该对象分配内存空间。
然而这种灵活性是有代价的:分配和清理堆存储要比栈存储花费更多的时间。
好消息是,随着时间的推移,Java 的堆内存分配机制已经变得非常高效了,所以你并不需要太过关注此类问题。
4.常量存储 (constant storage)。常量通常会直接保存在程序代码中,因为它们的值不会改变,所以这样做是安全的。
有时候常量会与其他代码隔离开来,于是在某些嵌入系统里,这些常量就可以保存在只读存储器 (read-only memory, ROM) 中。
5.非 RAM 存储 (non-RAM storage)。如果一段数据没有保存在应用程序里,那么该数据的生命周期既不依赖于应用程序是否运行,也不受应用程序的管制。
其中最典型的例子之一是 "序列化对象"(serialized object),它指的是"转换为字节流"("序列化")并可以发送至其他机器的对象。
另一个例子则是 "持久化对象"(persistent object),它指的是保存在磁盘上的对象,这些对象即便在程序结束运行之后也依然能够保持其状态。
这些数据存储类型的特点在于,它们会将对象转换成其他形式以保存于其他媒介中,然后在需要的时候重新转换回常规的 RAM 对象。
Java 支持轻量级的持久化对象存储,而 JDBC 以及 Hibernate 等库则提供了更为成熟的解决方案,即支持使用数据库存取对象信息。
3.2.2 特殊情况:基本类型
有一些你经常使用的类型享受特殊待遇,你可以将它们称为 "基本类型"(primitive type)。
它们之所以享受特别待遇,是因为对于基本类型,Java 使用了与 C 以及 C++ 相同的实现机制,这意味着无须使用 new 来创建基本类型的变量,而是直接创建一个自动变量,注意不是引用。
也就是说,该变量会直接在栈上保存它的值,因此运行效率较高。
new 关键字是在堆上创建对象,这就意味着哪怕是创建一些简单的变量也不会很高效。
Java 定义了每一种基本类型所占用的空间大小。即便是在不同的机器上,这些类型所占用的空间也是保持一致的。而这种一致性也是 Java 程序比其他语言程序移植性更好的原因之一。
| 数据类型 | 字节数 | 位数 | 取值范围 / 说明 |
|---|---|---|---|
byte |
1 字节 | 8 位 | -128 ~ 127 |
short |
2 字节 | 16 位 | -32,768 ~ 32,767 |
int |
4 字节 | 32 位 | -2³¹ ~ 2³¹-1 |
long |
8 字节 | 64 位 | -2⁶³ ~ 2⁶³-1 |
float |
4 字节 | 32 位 | IEEE 754 单精度浮点数 |
double |
8 字节 | 64 位 | IEEE 754 双精度浮点数 |
char |
2 字节 | 16 位 | 0 ~ 65,535(Unicode 字符) |
boolean |
未明确定义 | --- | 仅 true 或 false |
表中的所有数值类型都是有符号的,所以不用在表格中查找无符号类型了。
boolean 类型的空间大小没有明确标出,其对象只能被赋值为 true 或 false。
此外,Java 还为基本类型提供了对应的 "包装类" (wrapper class),通过包装类可以创建与基本类型对应的堆空间对象。例如:
java
char c = 'x';
Character ch = new Character(c);
也可以这样表示:
java
Character ch = new Character('x');
而 "自动装箱" (autoboxing) 机制能够将基本类型自动转换为包装类对象,例如:
java
Character ch = 'x';
也可以再转换回来,例如:
java
Character ch = 'x';
char c = ch;
至于使用包装类的原因,后续章节会进行更为细致的讲解。
高精度数字
Java 提供了两个支持高精度计算的类,分别是 BigInteger 和 BigDecimal。虽然这两个类也大致可以被归为包装类,但是它们其实并没有对应的基本类型。
这两个类都提供了一些方法来模拟基本类型的各种操作。也就是说,你能对 int 和 float 做什么,就能对 BigInteger 和 BigDecimal 做什么,区别只是你用方法代替了运算符而已。此外,由于涉及更多的计算量,导致的结果就是相关操作的效率有所降低。所谓 "速度换精度" 即是如此。
BigInteger 可以支持任意精度的整数。也就是说,在运算时你可以表示任意精度的整数值,而不用担心丢失精度。
BigDecimal 可用于任意精度的定点数 (fixed-point number)。例如,你可以将其用于货币计算。
**定点数(fixed-point number)是小数点位置固定、用固定数量的整数位和小数位表示的数值,属于计算机中的一种数值表示方式。**它可以表示整数,也可以表示带小数的数;当小数位数为 0 时,本质上就是整数表示。
小数点是怎么固定的
计算机通常把定点数存成一个整数,再约定其中有多少位用于表示小数。实际数值由这个约定还原出来:
十进制定点数保留 2 位小数时,存储值 12345 可表示 123.45
二进制定点数保留 4 个小数位时,实际值通常等于存储的整数除以 2⁴
因此,同样的存储整数,在小数位约定不同的情况下可能代表不同的数。定点数的关键不在于小数点真的被单独存储,而在于程序和数据格式共同约定它的位置。
和浮点数有什么区别
浮点数(floating-point number)的小数点位置可以随着数值大小变化,能表示很大或很小的数,但运算结果可能出现舍入误差,处理速度和硬件支持也取决于具体环境。定点数的小数位固定,数值范围通常较小,却有计算简单、结果更容易预测、资源占用较低等特点,所以常用于嵌入式设备、信号处理、控制系统,以及需要按固定小数位处理的金额或计量数据。
如果你想了解这两个类的更多细节,请参考 JDK 文档的相关描述。
3.2.3 Java 中的数组
许多编程语言都支持数组。然而在 C 和 C++ 里,因为数组的本质是内存块,所以使用数组是十分危险的。
也就是说,如果 C++ 程序访问了数组边界之外的内存,或者在内存被初始化之前就对其进行操作(这个问题非常普遍),那么结果如何就难以预料了。
Java 的一个核心设计目的是安全,于是许多折磨 C 和 C++ 程序员的问题在 Java 里已经不复存在了。
例如,Java 的数组一定会被初始化,并且无法访问数组边界之外的元素。这种边界检查的代价是需要消耗少许内存,以及运行时需要少量时间来验证索引的正确性。其背后的假设是,安全性以及生产力的改善完全可以抵消这些代价。
当你创建一个用于放置对象的数组时,实际上数组里包含的是引用,而这些引用会被自动初始化为一个特殊的值:null。
Java 会认为一个值为null 的引用没有指向任何对象,所以当你操作引用之前,需要确保将其指向了某个对象。
如果你试图操作一个值为 null 的引用,系统会返回一个运行时报错。
因此,Java 通过以上手段规避了那些常见的数组问题。
此外,你也可以创建一个放置基本类型的数组。编译器会确保对该数组进行初始化,并将数组元素的值设置为0。
本书后续(尤其在第 21 章中)会对数组进行更为详细的介绍。
3.3 注释
Java 支持两种类型的注释。
第一种注释是传统的 C 语言风格,即以 /* 开始,中间可以有多行的注释内容,然后以 */ 结束。
需要注意的是,许多程序员在书写多行注释时,习惯于在每一行的开头使用 *,所以你可能会经常见到如下代码:
java
/**
* ClassName: Person
* Package: com.yangjunbo.spring.definition.examplea
* Description:
*
* @Author 杨钧博
* @Create 2026/9/7 8:42
* @Version 1.0
*/
不过请切记,所有位于 /* 和 */ 之间的内容都会被当作注释,从而被编译器忽略,所以即使这样写效果也是一样的:
java
/* This is a comment that
continues across lines */
第二种注释源于 C++ 语言风格。这种注释以 // 开始,到同一行的末尾结束,因此是一种单行注释。因为这种注释简单易用,所以使用频率很高。以后你会经常看到类似这样的注释:
java
// This is a one-line comment
3.4 无须销毁对象
对于有些编程语言而言,管理数据的生命周期需要花费不少精力。
例如,某个变量需要存在多长时间?如果你需要销毁它,应该何时动手呢?数据的生命周期所造成的困扰会导致大量的 bug。本节将展示 Java如何帮你释放内存并大幅简化这些问题。
3.4.1 作用域
大多数过程式编程语言具有作用域 (scope) 的概念。作用域会决定其范围内定义的变量的可见性和生命周期。C、C++ 以及 Java 的作用域范围都是通过大括号 ({}) 来定义的,下面是一个 Java 作用域的例子:
java
{
int x = 12;
// 在这里只有变量 x 可见
{
int q = 96;
// 在这里变量 x 和 q 都可见
}
//在这里超出了变量 q 的作用域
//只有变量 x 可见
}
在作用域里定义的变量只在该作用域的范围内可见。
顺便一提,代码缩进可以提高 Java 代码的可读性。由于 Java 是自由形态的编程语言,所以多余的空格、tab 缩进、回车符等都不会影响程序的运行效果。
此外,虽然下面的代码对于 C 和 C++ 而言是合法的,但在 Java 中不能这样使用:
java
{
int x = 12;
{
int x = 96;//语法错误
}
}
Java 编译器会提示说,变量 x 已经定义过了。因此,类似 C 和 C++ 那样在外围的作用域中 "隐藏" 变量的方式在 Java 中是不被允许的,因为Java 的设计者认为这种编程方式会引发歧义。
3.4.2 对象的作用域
Java 对象的生命周期和基本类型有所不同。
当你使用 new 创建一个对象时,该对象在其作用域结束后会依然存在。因此,如果你编写如下代码:
java
{
String s = new String("a string");
}
虽然引用 s 会在作用域结束后消失,但是它指向的 String 对象还会继续占用内存。
对于上面的代码而言,该 String 对象在作用域结束后就无法再获取,这是因为已经超出了其唯一引用的作用域范围。
在后续章节中,你将看到如何在程序中传递和复制对象的引用。
如果需要的话,通过 new 创建的对象会存在足够长的时间,因此 C++ 编程中出现的一系列问题在 Java 中就消失了。在编写 C++ 代码时,你不仅要确保对象在需要的时候随时可用,而且事后还要负责销毁这些对象。
等等,还有个问题。如果 Java 对这些对象不闻不问,为什么应用程序并没有因为内存溢出而强行结束呢?而这一点也正是 C++ 存在的问题。
原因就在于,Java 使用了一点魔法,即垃圾收集器会监视所有通过 new 创建的对象,并及时发现哪些对象不再被引用,然后它会释放这些对象所占用的内存,使得这些内存可以用于其他新对象。
这意味着你无须关注何时需要释放内存,你要做的只是创建对象而已,如果你不再需要这些对象,就任由它们去吧(垃圾收集器会处理它们)。这种机制解决了一类非常严重的编程问题:由于程序员忘记释放内存而导致的 "内存泄漏" (memory leak)。
3.5 使用 class 关键字创建新类型
如果一切皆为对象,那么具体到某一类对象的特征和行为是如何确定的呢?换句话说,如何创建对象的类型?
说到这里,你可能会想当然地认为会有一个名为 "type" 的关键字,然而由于历史原因,大多数面向对象编程语言会使用 "class" 关键字来描述新的对象类型。
其用法是 class 关键字后面跟着新的类名:
java
class ATypeName{
//类的具体特性和行为
}
这段代码创建了一个新的对象类型即类,只不过其内容只包含了一行注释,因此目前也没有什么实际的作用。
话虽如此,你还是可以通过 new 关键字创建一个该类的对象:
java
ATypeName a = new ATypeName();
除非你为这个类定义了一些方法,否则到目前为止,这个类能做的事情并不多。
字段
当定义一个类时,你可以为其定义两种元素:字段(有时叫作 "成员数据")和方法(有时叫作 "成员函数")。通过对象的引用与字段进行交互。字段可以是任何类型的对象,也可以是基本类型。如果一个字段是某个对象的引用,你必须通过 new 关键字初始化该对象并将其关联到具体的引用上。
每一个对象都会单独保存其字段。通常来说,不同对象的字段之间不会共享。下面的代码是一个定义了字段的类:
java
class DataOnly{
int i;
double d;
boolean b;
}
这个类只是定义了几个数据字段而已。和之前一样,你可以这样创建一个该类的对象:
java
DataOnly data = new DataOnly();
你可以通过对象成员为字段赋值。具体做法是,先写出对象的引用名,跟着再写一个 ".",然后是对象的成员名:
objectReference.member
例如,可以这样为字段赋值:
java
data.i = 47;
data.d = 1.1;
data.b = false;
如果一个对象包含了其他对象,而你想要修改其他对象的数据,该怎么办?同样可以使用 "." 来实现,例如:
java
myPlane.leftTank.capacity = 100;
理论上你可以用这种方法嵌套无穷多的对象(但是需要提醒的是,这种设计方式并不优雅)。
基本类型的默认值
当一个类的字段是基本类型时,即便你没有初始化这些字段,它们也会拥有默认值,如表所示。
| 数据类型 | 默认值 |
|---|---|
byte |
0 |
short |
0 |
int |
0 |
long |
0L |
float |
0.0f |
double |
0.0d |
char |
'\u0000'(空字符) |
boolean |
false |
这一特点确保了基本类型的字段一定会被初始化(C++ 就不会这么做),并且从源头减少了许多不应该出现的 bug。
只不过对于你编写的程序而言,这些默认值可能并不是正确或合理的值,所以最佳实践应该是你显式地初始化这些变量。
此外,这种机制并不会应用于局部变量 (local variable),因为局部变量并不是类的字段。
因此,如果你在一个方法内部编写如下代码:
java
int x;
那么变量 x 不会自动被初始化为 0。
因此,在使用变量 x 之前,你必须为其赋值以确保正确性。
如果你忘记了赋值,Java 的处理方式明显要比 C++ 更好一些--- Java 会抛出一个编译错误以告知你变量没有被初始化(相比之下,C++ 编译器通常只是警告你有些变量没有被初始化,而 Java 则是直接抛出错误)。
3.6 方法、参数以及返回值
在许多编程语言(比如 C 和 C++)中,"函数" (function) 用于表示子程序。而在 Java 中,称之为 "方法"(method),意思是 "做某件事的方式"。
Java 中的方法决定了对象具有哪些行为可以干什么。方法最基础的几个部分包括:方法名、参数、返回值,以及方法体 (method body)。例如:
java
ReturnType methodName(/* 参数列表 */){
// 方法体
}
ReturnType 表示当调用一个方法时,该方法返回的值是什么类型。
参数列表提供了一系列需要传递给方法的数据,包括数据的类型和名称。
方法名和参数列表共同构成了方法的 "签名"(signature),方法签名即该方法的唯一标识符。
Java 中的方法只能作为类的一部分而存在,方法只能通过对象调用。如果你通过对象调用了一个不属于该对象的方法,会得到一个编译时错误。
调用对象方法的具体方式为,在对象引用之后添加一个 ".",然后紧跟着方法名及其参数列表:
java
objectReference.methodName(arg1,arg2,arg3);
现在思考一下,假设有对象 a 定义了 f() 方法,如何调用一个无参数且返回 int 值的 f() 方法。可以这样编写代码:
java
int x = a.f();
需要注意的是,返回值的类型必须与变量 x 的类型保持一致。
有时候,这种调用方法的行为也被描述为 "向一个对象发送一条消息"。在上面的例子中,f() 代表消息,a 代表对象。此外,也可以将面向对象编程描述为 "向对象发送消息"。
参数列表
参数列表描述了需要传递给方法的数据。
也许你已经猜到了,这些信息和 Java 中的其他内容一样都是对象。
所以,参数列表也需要描述这些对象的类型以及对象名。
和之前一样,当你操作对象时,实际操作的是它的引用。
此外,引用的类型必须正确,如果方法定义的参数是 String 类型,那就必须传递一个 String 对象给该方法,否则编译器会报错。
下面的例子是一个接受 String 类型参数的方法:
java
int storage(String s){
return s.length() * 2;
}
上面的方法计算并返回了保存指定字符串所需的字节数。参数 s 的类型是 String。
一旦 s 被传递至 storage() 方法内部,就可以像操作其他对象一样操作它,也就是可以向它发送消息。
这里调用了它的 length() 方法,这个属于 String 类的方法将返回指定字符串的字符长度。
字符串的每一个字符长度是 16 位,也就是两个字节。
而 return 关键字做了两件事。第一件事是告诉程序 "从方法中离开吧,一切都结束了"。第二件事是当该方法生成了一个值的时候,将这个值放置于 return 之后。在上面的例子里,最后生成的返回值是计算 s.length()*2 的结果。
你可以返回任意类型的值,但是如果不返回任何值,就表示该方法生成的值为 void(意思是什么都没有):
java
boolean flag(){
return true;
}
double naturalLoaBase(){
return 2.718;
}
void nothing(){
return;
}
void nothing2(){}
当返回值的类型是 void 时,使用 return 关键字的作用是退出该方法,因此在方法的末尾处就没有必要使用 return 了。
此外,你可以在方法的任意位置使用 return。
然而,一旦返回值是非 void 类型,无论你在何处返回,编译器都会强制你必须提供正确类型的返回值。
如此看来,Java 程序似乎就是一群对象将其他对象设置为其方法的参数,然后向这些对象发送消息。
到目前为止情况确实如此,不过在第 4 章中将介绍如何通过方法执行一些更为底层的操作。对于本章而言,发送消息就足够了。
发送消息的意思是调用对象的方法。
3.7 编写 Java 程序
3.7.1 名称可见性
所有的编程语言都有一个问题,那就是对名称的控制。
比如,你在程序的一个模块中使用了一个名称,有人在其他模块中也使用了这个名称,
那么应该如何区分两者?又该如何避免两者之间的"冲突"?
对于 C 语言来说,这个问题尤其令人感到头疼,因为程序中经常充斥着各种名称,维护成本极高。
而 C++ 的类(Java 中类的设计参考了 C++)则是将函数放置于类的内部,因此一个类的函数名不会与其他类的同名函数发生冲突。
但是,C++ 依然允许使用全局数据和全局函数,所以潜在的冲突也依然存在。
为了解决这个问题,C++ 使用额外的关键字引入了命名空间的概念。
而 Java 通过一种新颖的方法解决了此问题。
为了能够清晰地描述库的名称,Java 语言的设计者所使用的方法是将你的互联网域名反转使用。
因为域名是唯一的,所以一定不会冲突。
举例来说,如果域名是 ituring.com,那么 foibles 库的名称就是 com.ituring.utility.foibles。
而反转的域名之后的几个 "." 实际上描述了子目录的结构。
在 Java 1.0 和 Java 1.1 里,按照约定俗成的惯例,类似 com、edu、org、net 等扩展名的首字母都是大写,所以你会看到类似Com.ituring.utility.foibles 这样的名称。
然而在 Java 2 的开发过程中,Java 的设计者发现这种方法会引发一些问题。于是从那之后一直到现在,包名都变成了小写字母。
这种机制确保了所有文件都有对应的命名空间,同时文件里定义的类都具有唯一对应的标识符。因此,Java语言就避免了命名冲突的问题。
使用反转的 URL 来定义命名空间是一种全新的尝试,在此之前没有其他语言使用过此方案。
此外,Java 语言还有其他一些与之类似的解决特定问题的方案。
不难想象,如果一种新特性没有经过充分验证的话,将来出现问题的概率会很大。
如果在实际编程阶段应用了该特性,一旦出现问题就回天乏术了(有些问题甚至严重到需要从语言中去掉某些特性)。
将命名空间和利用反转的URL所生成的文件路径相关联会带来一个问题,不过这个问题并不是程序的 bug,而是会对管理源代码带来一些挑战。比如想要使用 com.ituring.utility.foibles 这个命名空间,那么就需要创建名为 com 和 ituring 的空文件夹,其目的只是为了与反转的 URL 相对应。
而这种做法还将引入一个你之后在 Java 工程中会遇到的情况:通过空文件夹层层嵌套而形成的这种文件夹结构,其作用不只是为了对应反转的URL,同时这些冗长的文件夹路径通常用于表明文件夹内容。
因此在编写工程级别的 Java 代码时,基本上你不得不使用那些精心设计的 IDE 以管理层层嵌套的代码,比如 IntelliJ IDEA 等。
由于这种深不见底的文件夹结构会让你在开始学习语言时就想要使用大型 IDE,而我并不想让你承受这种额外的负担和困扰,因此本书所有章节的代码示例都使用了浅显易懂的结构,即子文件夹名与其(英文版)章节名保持一致。
3.7.2 使用其他组件
当你在程序中使用其它类时,编译器必须找到这个类。
最简单的情况是,这个类就存在于当前被调用的源代码文件中。如此一来,你只要使用这个类就可以了。
那么,如果一个类定义在其他文件里呢?可能你会认为万能的编译器会自动找到这个类,然而问题并非如此简单。
比如你想使用一个类,却发现同一个类名存在多个不同的定义(假设这些定义的用途各不相同)。
更有甚者,假设你正在编写一个程序,当你向库中添加了一个新的类时,可能会发现已经和库中一个已有的类名冲突了。
为了解决这个问题,需要消除所有类似这样的潜在歧义,解决方案则是利用 import 关键字告知 Java 编译器你想要使用哪个类。
import 语句的作用是通知编译器导入一个指定位置的包,即放置各种类的库(其他编程语言的库可以由函数、数据和类组成,但是 Java 的一切活动都发生在类中)。
你经常会用到编译器自带的各种 Java 标准库组件。当你使用这些组件时,无须担心那些冗长和反转的域名,使用下面的方式即可将其导入:
java
import java.util.ArrayList;
这一行代码会告诉编译器,你要使用位于 util 库的 ArrayList 类。
此外,util 库也包括了一些其他的类,可能你需要使用其中的几个类,却不想一个个地导入它们。这种情况下,可以使用通配符 "*" 来轻松实现这一点:
java
import java.util.*;
由于本书的代码示例规模都很小,为了方便起见,经常会使用 "*" 导入类。不过需要提醒的是,许多编程风格指南会明确指出,每一个用到的类都应该被单独导入。
3.7.3 static 关键字
创建一个类即描述了其对象的特征和行为。
直到使用 new 关键字时,你才会真正创建一个对象,以及为该对象分配内存,并使得其方法可以被调用。
然而在两种情况下,这种方式会显得不合时宜。
第一种情况是,有时候需要一小块共享空间来保存某个特定的字段,而并不关心创建多少个对象,甚至有没有创建对象。
第二种情况是,你需要使用一个类的某个方法,而该方法和具体的对象无关;换句话说,你希望即便没有生成任何该类的对象,依然可以调用其方法。
static 关键字(源自 C++)为以上两个问题提供了解决方案。
如果你使用了 static 关键字,则意味着使用 static 的字段或方法不依赖于任何对象。
也就是说,即便你没有为一个类创建任何对象,依然可以调用该类的 static 方法或 static 字段。
另外,由于非 static 的字段和方法必须基于指定的对象,因此对于非 static 的字段和方法来说,你必须创建一个对象才可以使用非 static 的字段或方法。
有些面向对象编程语言也会使用 "类数据" (class data) 和 "类方法" (class method) 来表示该数据或方法只服务于类,而非特定的对象。有时候,Java 也会使用这两种称谓。
创建一个 static 字段或方法,只需要把 static 关键字放置于字段或方法定义的前面就可以了。以下是创建并初始化一个 static 字段的例子:
java
class StaticTest{
static int i = 47;
}
即便你创建了两个 StaticTest 类的对象,StaticTest.i 依然只会占用同一块内存空间。也就是说,字段 i 会被两个对象所共享。例如:
java
StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();
st1.i 和 st2.i 的值都是 47,这是因为两者使用的内存空间是相同的。
有两种方法可以调用 static 变量。其一是通过对象来调用,比如之前例子中的 st2.i。此外,你也可以直接通过类名来调用(非 static 成员则不能这样使用):
java
StaticTest.i++;
这里出现的 ++ 运算符的作用是将变量的值加 1。结果就是,st1.i 和 st2.i 的值都变成了 48。
推荐通过类名调用 static 变量,因为这种方法突出了变量的 static 特质。
类似的逻辑也适用于 static 方法。你既可以通过对象来调用 static 方法(和非 static 方法一样),也可以使用专用语法 ClassName.method(),即类名加方法名。例如,你可以这样定义一个 static 方法:
java
class Incrementable(){
static void increment(){
StaticTest.i++;
}
}
increment() 方法通过 ++ 运算符将 static 整数 i 的值加 1。你可以使用传统方式,即通过对象调用 increment() 方法:
java
Incrementable sf = new Incrementable();
sf.increment();
不过,推荐的做法是直接通过类名来调用:
java
Incrementable.increment();
将 static 关键字应用于字段,毫无疑问会改变其数据生成的方式,即 static 字段是基于类创建的,非 static 字段则是基于对象创建的。
而将 static 应用于方法时,即便你没有创建对象,也可以调用该方法。
你之后会看到,作为运行应用程序的入口,在定义 main() 方法时,static 关键字是必不可少的一部分。
3.8 你的第一个 Java 程序
现在,你终于迎来了编写第一个完整程序的时刻。该程序运行时将显示一个字符串,以及一个通过 Java 标准库的 Date 类所生成的日期。
java
// 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());
}
}
在本书的代码示例中,第一行注释都具有特别的作用:显示文件夹路径以及文件名。
此外,通过第一行注释,你也可以很方便地在示例代码中找到对应的源代码。
在每一个代码文件的起始处,你都必须使用 import 语句将所有额外的类导入到文件中。
这里说 "额外" 是因为,所有 Java 文件都会自动导入一个特定的库,即 java.lang。
你可以打开浏览器在 Oracle 网站上查看这个库的文档,如果你还没有下载 JDK 文档,现在就可以去 Oracle 公司的 Java 网站下载,或者通过互联网搜索和查看。
然后,如果你查看包的列表,会看到各种类的库。比如,你可以选择 java.lang,你会看到一个呈现了该库所有类的列表。
由于所有的 Java 代码文件都默认导入了 java.lang,所以列表中的类都可以直接调用。
另外,由于 java.lang 并不包含 Date 类,这就意味着你必须从其他库将其导入后才能使用。
如果你不知道一个特定的类属于哪个库,或者说你想查看所有的类,可以点击 Java 文档页面的 "Tree" 按钮,页面会显示 Java 中所有的类供你查询。
然后,你就可以通过浏览器的查找功能来找到 Date 类。比如,你会在页面列表中找到 java.util.Date,这意味着 Date 类包含在 util 库中。
因此,你必须在 Java 文件中添加一行代码 import java.util.*,然后才可以使用 Date 类。




如果你在文档页面选择了 java.lang 然后查看其中的 System 类,会发现 System 类拥有几个字段。
如果你选择了其中一个字段 out,会发现这是一个 static PrintStream 对象。正因为它是 static 字段,所以你无须借助 new 关键字。
也就是说,out 对象会一直存在,你直接使用就行了。out 对象的用途取决于它的类型,即 PrintStream。
此外,你会发现文档描述中的 PrintStream 是一个超链接,可以点击该超链接来查看 PrintStream 都提供了哪些方法为我们所用。
其中包含的方法数量还不少,会在后续章节中陆续提及。
现在需要的是 println() 方法,其作用是 "在控制台打印我发送给你的内容,然后另起一行"。



因此,如果你需要在控制台显示一段信息,可以在Java 程序中编写如下代码:
java
System.out.println("A String of things");
需要注意的是,文件中的类名必须与文件同名(如果你没有这么做,则编译器会报错)。此外,如果你需要创建一个能够独立运行的程序,那么与文件同名的类中还必须包含一个程序启动的入口方法。这个特殊的方法叫作 main(),具有以下的格式和返回值:
java
public static void main(String[] args){}
public 关键字代表这个方法可以被外部程序所调用(相关详细内容会在第 7 章中进行介绍)。main() 的参数是一个 String 类型的数组,虽然目前并不会使用 args 参数,但 Java 编译器会强制你定义该参数,因为它用于获取控制台的输入。
下面的代码将打印当前的日期:
java
System.out.println(new Date());
在这段代码中,创建了一个作为参数的 Date 对象,并将它的值传递给 println() 方法。
而当这一段语句执行完毕时,Date 对象就没用了,因此垃圾收集器可以随时清理它,而程序员则无须关心这种清理工作。
当你查看 JDK 文档时,会发现 System 类包含了许多实用的方法(Java 的重要资产之一就是内容极为丰富的标准库),比如:
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.name 以及 java.library.path。

编译和运行
为了能够编译和运行本书中的程序,你必须搭建好 Java 编程环境,其安装过程在第 2 章有详细的介绍。
如果你遵从了指引的步骤,你使用的应该是从 Oracle 网站上免费获取的 Java 开发者工具集(Java Developer's Kit, JDK)。
此外,第 2 章也介绍了如何安装本书的代码示例。安装完成后,请打开名为 objects 的子目录并通过命令行输入:
shell
javac HelloDate.java
这一行代码应该不会输出任何内容。如果你看到了报错消息,就意味着你没有正确安装 JDK,那么你需要调查问题原因并解决它。
如果命令行没有报错,那么你可以输入:
java
java HelloDate
结果就是,你会看到当前日期输出到了控制台。
这就是编译和运行本书所有程序的过程。
另外,本书源代码的根目录里还包含了一个叫作 build.gradle 的文件,此文件包含了自动化构建、测试以及运行本书代码文件的 Gradle 的配置。当你初次运行 gradlew 命令时,Gradle 会被自动安装(前提是你已经安装了 Java)。


3.9 编程风格
"Code Conventions for the Java Programming Language (Java 编程语言代码规范)" 中定义的代码风格是类名首字母要大写。
如果类名包含多个词,也要把这些词汇总到一起(也就是说,不再使用下划线作为词语之间的间隔),每一个词的首字母也要大写,例如:
java
class AllTheColorsOfTheRainbow{ //...
这种方式叫作 "驼峰式命名法" (Camel-Case)。而其他内容,比如方法、字段(成员变量),以及对象名等,除了首字母要小写以外,使用的命名方式和类名一样。例如:
java
class AllTheColorsOfTheRainbow{
int anIntegerRepresentingColors;
void changeTheHueOfTheColor(int newHue){
// ...
}
// ...
}
需要注意的是,你定义的冗长的名称也会影响使用,所以请手下留情。
3.10 总结
本章讲述的知识点足以让你理解如何编写一个简单的 Java 程序。
此外,本章还介绍了 Java 语言的概况,以及一些语言基础。
然而到目前为止,你所接触的代码示例都是 "这么做,再这么做,然后再那么做"。
在接下来的两章里,会介绍 Java 编程的基础运算符,并展示如何控制程序流程。