03-Objects-Everywhere
第三章 万物皆对象
如果我们说另外一种不同的语言,我们会发觉一个不同的世界!— Ludwig Wittgenstein (1889-1951)
相比
对象操纵
“名字代表什么?玫瑰即使不叫玫瑰,也依旧芬芳”
所有的编程语言都会操纵内存中的元素。有时程序员必须要有意识地直接或间接地操纵它们。在
下面来创建一个
String s;
这里我们只是创建了一个s
赋值–指向任何对象。通常更安全的做法是:创建一个引用的同时进行初始化。代码示例:
String s = "asdf";
对象创建
“引用”用来关联“对象”。在new
操作符来创建一个新对象。new
关键字代表:创建一个新的对象实例。所以,我们也可以这样来表示前面的代码示例:
String s = new String("asdf");
以上展示了字符串对象的创建过程,以及如何初始化生成字符串。除了
数据存储
那么,程序在运行时是如何存储的呢?尤其是内存是怎么分配的。有
-
寄存器(Registers)最快的存储区域,位于
CPU 内部 2。然而,寄存器的数量十分有限,所以寄存器根据需求进行分配。我们对其没有直接的控制权,也无法在自己的程序里找到寄存器存在的踪迹(另一方面,C/C++ 允许开发者向编译器建议寄存器的分配) 。 -
栈内存(Stack)存在于常规内存
RAM (随机访问存储器,Random Access Memory)区域中,可通过栈指针获得处理器的直接支持。栈指针下移分配内存,上移释放内存。这是一种仅次于寄存器的非常快速有效的分配存储方式。创建程序时,Java 系统必须知道栈内保存的所有项的生命周期。这种约束限制了程序的灵活性。因此,虽然在栈内存上存在一些Java 数据(如对象引用) ,但Java 对象本身的数据却是保存在堆内存的。 -
堆内存(Heap)这是一种通用的内存池(也在
RAM 区域) ,所有Java 对象都存在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用new
命令实例化对象即可,当执行代码时,会自动在堆中进行内存分配。这种灵活性是有代价的:分配和清理堆内存要比栈内存需要更多的时间(如果可以用Java 在栈内存上创建对象,就像在C++ 中那样的话) 。随着时间的推移,Java 的堆内存分配机制现在已经非常快,因此这不是一个值得关心的问题了。 -
常量存储(Constant storage)常量值通常直接放在程序代码中,因为它们永远不会改变。如需严格保护,可考虑将它们置于只读存储器
ROM (只读存储器,Read Only Memory)中 3。 -
非
RAM 存储(Non-RAM storage)数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子: (1)序列化对象:对象被转换为字节流,通常被发送到另一台机器; (2)持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复成常规的、基于RAM 的对象。Java 为轻量级持久化提供了支持。而诸如JDBC 和Hibernate 这些类库为使用数据库存储和检索对象信息提供了更复杂的支持。
基本类型的存储
有一组类型在new
关键字来产生。通常 new
出来的对象都是保存在堆内存中的,以此方式创建小而简单的变量往往是不划算的。所以对于这些基本类型的创建方法,new
创建变量,而是使用一个“自动”变量。 这个变量直接存储
基本类型 | 大小 | 最小值 | 最大值 | 包装类型 |
---|---|---|---|---|
boolean | — | — | — | Boolean |
char | 16 bits | Unicode 0 | Unicode 216 -1 | Character |
byte | 8 bits | -128 | +127 | Byte |
short | 16 bits | - 215 | + 215 -1 | Short |
int | 32 bits | - 231 | + 231 -1 | Integer |
long | 64 bits | - 263 | + 263 -1 | Long |
float | 32 bits | IEEE754 | IEEE754 | Float |
double | 64 bits | IEEE754 | IEEE754 | Double |
void | — | — | — | Void |
所有的数值类型都是有正
char c = 'x';
Character ch = new Character(c);
或者你也可以使用下面的形式:
Character ch = new Character('x');
基本类型自动转换成包装类型(自动装箱)
Character ch = 'x';
相对的,包装类型转化为基本类型(自动拆箱
char c = ch;
个中原因将在以后的章节里解释。
高精度数值
在BigInteger
和 BigDecimal
。尽管它们大致可以划归为“包装类型”,但是它们并没有对应的基本类型。
这两个类包含的方法提供的操作,与对基本类型执行的操作相似。也就是说,能对
关于这两个类的详细信息,请参考
数组的存储
许多编程语言都支持数组类型。在
当我们创建对象数组时,实际上是创建了一个引用数组,并且每个引用的初始值都为
我们还可创建基本类型的数组。编译器通过将该数组的内存全部置零来保证初始化。本书稍后将详细介绍数组,特别是在数组章节中。
代码注释
/*
开头,可以跨越多行,到 */
结束。注意,许多程序员在多行注释的每一行开头添加 *
,所以你经常会看到:
/* 这是
* 跨越多行的
* 注释
*/
但请记住,/*
和 */
之间的内容都是被忽略的。所以你将其改为下面这样也是没有区别的。
/* 这是跨越多
行的注释 */
第二种注释形式来自//
开头并一直持续到行结束。这种注释方便且常用,因为直观简单。所以你经常看到:
// 这是单行注释
对象清理
在一些编程语言中,管理变量的生命周期需要大量的工作。一个变量需要存活多久?如果我们想销毁它,应该什么时候去做呢?变量生命周期的混乱会导致许多
作用域
大多数程序语言都有作用域的概念。作用域决定了在该范围内定义的变量名的可见性和生存周期。在{}
的位置决定的。例如:
{
int x = 12;
// 仅 x 变量可用
{
int q = 96;
// x 和 q 变量皆可用
}
// 仅 x 变量可用
// 变量 q 不在作用域内
}
{
int x = 12;
{
int x = 96; // Illegal
}
}
在上例中,
对象作用域
new
关键字来创建
{
String s = new String("a string");
}
// 作用域终点
上例中,引用
只要你需要,new
出来的对象就会一直存活下去。 相比在
那么问题来了:我们在new
出来的对象并判断哪些不再可达,继而释放那些被占用的内存,供其他新的对象使用。也就是说,我们不必担心内存回收的问题了。你只需简单创建对象即可。当其不再被需要时,能自行被垃圾收集器释放。垃圾回收机制有效防止了因程序员忘记释放内存而造成的“内存泄漏”问题。
类的创建
类型
如果一切都是对象,那么是什么决定了某一类对象的外观和行为呢?换句话说,是什么确定了对象的类型?你可能很自然地想到 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;
你可以用这种方式嵌套许多对象(尽管这样的设计会带来混乱
基本类型默认值
如果类的成员变量(字段)是基本类型,那么在类初始化时,这些类型将会被赋予一个初始值。
基本类型 | 初始值 |
---|---|
boolean | false |
char | \u0000 (null) |
byte | (byte) 0 |
short | (short) 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
这些默认值仅在
这种默认值的赋予并不适用于局部变量 —— 那些不属于类的字段的变量。 因此,若在方法中定义的基本类型数据,如下:
int x;
这里的变量
方法使用
在许多语言(如
在
[返回类型] [方法名](/*参数列表*/){
// 方法体
}
返回类型
方法的返回类型表明了当你调用它时会返回的结果类型。参数列表则显示了可被传递到方法内部的参数类型及名称。方法名和参数列表统称为方法签名(signature of the method
我们可以像下面这样调用一个对象的方法:
[对象引用].[方法名](参数1, 参数2, 参数3);
若方法不带参数,例如一个对象引用 a
的方法 f
不带参数并返回
int x = a.f();
上例中方法 f
的返回值类型必须和变量 x
的类型兼容 。调用方法的行为有时被称为向对象发送消息。面向对象编程可以总结为:向对象发送消息。
参数列表
方法参数列表指定了传递给方法的信息。正如你可能猜到的,这些信息就像
int storage(String s) {
return s.length() * 2;
}
此方法计算并返回某个字符串所占的字节数。参数 s
的类型为storage()
后,我们可以把它看作和任何其他对象一样,可以向它发送消息。在这里,我们调用 length()
方法,它是一个
s.length() * 2
产生的。在方法中,我们可以返回任何类型的数据。如果我们不想方法返回数据,则可以通过给方法标识 void
来表明这是一个无需返回值的方法。 代码示例:
boolean flag() {
return true;
}
double naturalLogBase() {
return 2.718;
}
void nothing() {
return;
}
void nothing2() {
}
当返回类型为void
,则编译器会强制我们返回相应类型的值。
上面的描述可能会让你感觉程序只不过是一堆包含各种方法的对象,在这些方法中,将对象作为参数并发送消息给其他对象。大部分情况下确实如此。但在下一章的运算符中我们将会学习如何在方法中做出决策来完成更底层、详细的工作。对于本章,知道如何发送消息就够了。
程序编写
在看到第一个
命名可见性
命名控制在任何一门编程语言中都是一个问题。如果你在两个模块中使用相同的命名,那么如何区分这两个名称,并防止两个名称发生“冲突”呢?在
.
用来代表子目录的划分。
在
使用反向
使用反向com.mindviewinc.utility.foibles
这样的目录结构中,我们创建了 com
和 mindviewinc
空目录。它们存在的唯一目的就是用来表示这个反向的
这种方式似乎为我们在编写
对于这本书中的例子,我不想让深层次结构给你的学习带来额外的麻烦,这实际上需要你在开始之前学习熟悉一种重量级的
使用其他组件
无论何时在程序中使用预先定义好的类,编译器都必须找到该类。最简单的情况下,该类存在于被调用的源代码文件中。此时我们使用该类 —— 即使该类在文件的后面才会被定义(
要解决此问题,你必须通过使用
import java.util.ArrayList;
上例可以告诉编译器使用位于标准库*
来导入其中部分类,而无需显式得逐一声明这些类。代码示例:
import java.util.*;
本书中的示例很小,为简单起见,我们通常会使用 .*
形式略过导入。然而,许多教程书籍都会要求程序员逐一导入每个类。
static 关键字
类是对象的外观及行为方式的描述。通常只有在使用 new
创建那个类的对象后,数据存储空间才被分配,对象的方法才能供外界调用。这种方式在两种情况下是不足的。
-
有时你只想为特定字段(注:也称为属性、域)分配一个共享存储空间,而不去考虑究竟要创建多少对象,甚至根本就不创建对象。
-
创建一个与此类的任何对象无关的方法。也就是说,即使没有创建对象,也能调用该方法。
一些面向对象的语言使用类数据(class data)和类方法(class method
我们可以在类的字段或方法前添加 static
关键字来表示这是一个静态字段或静态方法。 代码示例:
class StaticTest {
static int i = 47;
}
现在,即使你创建了两个 StaticTest
对象,但是静态变量 i
仍只占一份存储空间。两个对象都会共享相同的变量 i
。 代码示例:
StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();
st1.i
和 st2.i
指向同一块存储空间,因此它们的值都是st2.i
。我们也可以通过类名直接引用它,这种方式对于非静态成员则不可行:
StaticTest.i++;
++
运算符将会使变量结果st1.i
和 st2.i
的值都变成了
使用类名直接引用静态变量是首选方法,因为它强调了变量的静态属性。类似的逻辑也适用于静态方法。我们可以通过对象引用静态方法,就像使用任何方法一样,也可以通过特殊的语法方式 Classname.method()
来直接调用静态字段或方法 7。 代码示例:
class Incrementable {
static void increment() {
StaticTest.i++;
}
}
上例中,Incrementable
的 increment()
方法通过 ++
运算符将静态数据 i
加
Incrementable sf = new Incrementable();
sf.increment();
当然了,首选的方法是直接通过类来调用它。代码示例:
Incrementable.increment();
相比非静态的对象,static
属性改变了数据创建的方式。同样,当 static
关键字修饰方法时,它允许我们无需创建对象就可以直接通过类的引用来调用该方法。正如我们所知,static
关键字的这些特性对于应用程序入口点的 main()
方法尤为重要。
小试牛刀
最后,我们开始编写第一个完整的程序。我们使用
// 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());
}
}
在本书中,所有代码示例的第一行都是注释行,其中包含文件的路径信息(比如本章的目录名是
如果你想在代码中使用一些额外的类库,那么就必须在程序文件的开始处使用java.lang
包。
现在打开你的浏览器在 Oracle 上查看文档。如果你还没有从 Oracle 网站上下载
选择 java.lang
,你会看到该库中所有类的列表。由于 java.lang
隐式包含在每个java.lang
类库中没有
现在,我们可以找到
如果你在文档中选择
System.out.println("A String of things");
每个main()
方法作为程序运行的入口。其方法签名和返回类型如下。代码示例:
public static void main(String[] args) {
}
关键字
下面我们来看一段有趣的代码:
System.out.println(new Date());
上面的示例中,我们创建了一个日期(Date)类型的对象并将其转化为字符串类型,输出到控制台中。 一旦这一行语句执行完毕,我们就不再需要该日期对象了。这时,
查看
// 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"));
}
}
输出结果
java.runtime.name=Java(TM) SE Runtime Environment
sun.boot.library.path=C:\Program
Files\Java\jdk1.8.0_112\jr...
java.vm.version=25.112-b15
java.vm.vendor=Oracle Corporation
java.vendor.url=http://java.oracle.com/
path.separator=;
java.vm.name=Java HotSpot(TM) 64-Bit Server VM
file.encoding.pkg=sun.io
user.script=
user.country=US
sun.java.launcher=SUN_STANDARD
sun.os.patch.level=
java.vm.specification.name=Java Virtual Machine
Specification
user.dir=C:\Users\Bruce\Documents\GitHub\on-ja...
java.runtime.version=1.8.0_112-b15
java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment
java.endorsed.dirs=C:\Program
Files\Java\jdk1.8.0_112\jr...
os.arch=amd64
java.io.tmpdir=C:\Users\Bruce\AppData\Local\Temp\
main()
方法中的第一行会输出所有的系统字段,也就是环境信息。
编译和运行
要编译和运行本书中的代码示例,首先必须具有
移动到子目录
javac HelloDate.java
此命令不应产生任何响应。如果我们收到任何类型的错误消息,则表示未正确安装
若执行不报错的话,此时可以键入:
java HelloDate
我们将会得到正确的日期输出。这是我们编译和运行本书中每个程序(包含 main()
方法)的过程 9。此外,本书的源代码在根目录中也有一个名为gradlew
命令时,
编码风格
class AllTheColorsOfTheRainbow {
// ...
}
有时称这种命名风格叫“驼峰命名法”。对于几乎所有其他方法,字段(成员变量)和对象引用名都采用驼峰命名的方式,但是它们的首字母不需要大写。代码示例:
class AllTheColorsOfTheRainbow {
int anIntegerRepresentingColors;
void changeTheHueOfTheColor(int newHue) {
// ...
}
// ...
}
在
本章小结
本章向你展示了简单的
-
这里可能有争议。有人说这是一个指针,但这假定了一个潜在的实现。此外,
Java 引用的语法更类似于C++ 引用而非指针。在 《Thinking in Java》 的第1 版中,我发明了一个新术语叫“句柄”(handle) ,因为C++ 引用和Java 引用有一些重要的区别。作为一个从C++ 的过来人,我不想混淆Java 可能的最大受众 ——C++ 程序员。在《Thinking in Java》的第2 版中,我认为“引用”(reference)是更常用的术语,从C++ 转过来的人除了引用的术语之外,还有很多东西需要处理,所以他们不妨双脚都跳进去。但是,也有些人甚至不同意“引用”。在某书中我读到一个观点:Java 支持引用传递的说法是完全错误的,因为Java 对象标识符(根据该作者)实际上是“对象引用”(object references) ,并且一切都是值传递。所以你不是通过引用传递,而是“通过值传递对象引用。人们可以质疑我的这种解释的准确性,但我认为我的方法简化了对概念的理解而又没对语言造成伤害(嗯,语言专家可能会说我骗你,但我会说我只是对此进行了适当的抽象。 ) ↩︎ -
大多数微处理器芯片都有额外的高速缓冲存储器,但这是按照传统存储器而不是寄存器。 ↩︎
-
一个例子是字符串常量池。所有文字字符串和字符串值常量表达式都会自动放入特殊的静态存储中。 ↩︎
-
静态方法,我们很快就能接触到,它可以在没有对象的情况下直接被类调用。 ↩︎
-
通常除了前面提到的“特殊”数据类型
boolean 、 char、 byte、 short、 int、 long、float 和double 。通常来说,传递对象就意味者传递对象的引用。 ↩︎ -
静态方法在使用之前不需要创建对象,因此它们不能直接调用非静态的成员或方法(因为非静态成员和方法必须要先实例化为对象才可以被使用
) 。 ↩︎ -
在某些情况下,它还为编译器提供了更好的优化可能。 ↩︎
-
请注意,此文档未包含在
JDK 中; 你必须单独下载才能获得它。 ↩︎ -
对于本书中编译和运行命令行的每个程序,你可能还需要设置
CLASSPATH 。 ↩︎ -
为了保持本书的代码排版紧凑,我并没完全遵守规范,但我尽量会做到符合
Java 标准。 ↩︎