第2章 一切都是对象
使用Java进行设计,必须具有面向对象的思想,这样才能具备这种面向对象程序设计语言的编程能力。 本章中,我们将看到Java程序的基本组成部分,并体会到在Java中一切都是对象。
2.1 用引用操纵对象
每种编程语言都有自己的操纵内存中元素的方式,不管是直接操纵元素还是用某种基于特殊语法的间接表示(如C和C++中的指针)来操纵对象。
在Java中,一切都被视为对象,因此采取单一固定的语法:使用对象的引用作为操纵的标识符。 可以将这一情形想象为用遥控器(引用)来操纵电视机(对象)。并且,即使没有电视机,遥控器亦可独立存在。即你拥有一个引用,并不一定需要有一个对象与它关联。
例如:如果想要操纵一个词或句子,则可以创建一个String应用:
String s;
这里所创建的只是引用,并不是对象。如果此时向s发送消息,则会出现运行时错误。这是因为此时s实际上没有与任何事物关联(即,没有电视机)。因此,一种安全的做法是:创建一个引用的同时进行初始化。
String s = "HelloWorld";
这里使用了Java语言的一个特性:字符串可以用带引号的文本初始化。
2.2 必须由你创建所有对象
一旦创建了一个引用,就希望它能与一个新的对象相关联。通常用new操作符实现。所以前面的例子可以写为:
String s = new String("HelloWorld");
当然,除了String类型,Java提供了大量的现成类型。重要的是,我们可以自行创建新类型,这是Java程序设计中一项基本行为。
2.2.1 存储到什么地方
内存中有五个不同的地方可以存储数据:
-
寄存器:最快的存储区,位于处理器内部。数量有限,会根据需求自动分配,不能直接控制。(C和C++允许向编译器建议寄存器的分配方式)
-
堆栈:位于通用RAM(随机访问存储器)中,存储速度仅次于寄存器。处理器通过堆栈指针操纵堆栈,指针向下移动,分配新内存,向上移动,释放内存。 创建程序时,Java系统必须知道存储在堆栈内所有项的确切生命周期,以便上下移动堆栈指针。 如对象引用则存储于堆栈中。
-
堆:一种通用的内存池(也位于RAM区),用于存放所有的Java对象。 编译器无须知道存储的数据在堆里的存活时间。当通过new创建对象时,会自动在堆里进行储存分配。 在堆中进行存储分配和清理比堆栈中花费的时间更长。
-
常量存储:通常直接存放在程序代码内部,有时会选择将其存放在ROM(只读存储器)中。
-
非RAM存储:如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序不在运行时依然可以存在。 如:流对象和持久化对象。 在流对象中,对象转化成字节流,通常被发送给另一台机器。 在持久化对象中,对象被存放于磁盘上。这种存储方式的技巧在于:把对象转化成可以存放在其他媒介上的事物,在需要时,可恢复成常规的、基于RAM的对象。 Java提供了对轻量级持久化的支持,诸如JDBC和Hibernate这样的机制,提供了对在数据库中存储和读取对象信息的支持。
2.2.2 特例:基本类型
在程序设计中经常用到一系列类型,我们将它们称为基本类型。
Java中基本类型的变量直接存储值,并置于堆栈中,因此更加高效。并且,每种基本类型所占存储空间的大小具有不变性,不会随机器硬件架构的变化而变化,这也是Java程序高可移植性的原因之一。
所有数值类型都有正负号。boolean类型所占存储空间大小没有明确指定,仅能定义为true或false。
基本类型具有的包装器类,使得可以在堆中创建一个非基本对象,用来表示对应的基本类型。 例如:
char c ='x';
Character ch1 = new Character(c);
Character ch2 = new Character('x');
Java SE5的自动装箱、拆箱功能,可以自动地在基本类型和包装器类型之间相互转换:
Character ch = 'x';
char c = ch;
Java提供了两个用于高精度计算的类:BigInteger和BigDecimal,二者都没有对应的基本类型。 这两个类中方法所提供的操作与基本类型通过运算符执行的操作相似,即能作用于int和float的操作,也同样能作用于BigInteger和BigDecimal。不过后者以方法调用的方式取代了运算符方式,这样做精度虽然更高了,但复杂度也变高了,从而降低了速度。
-
BigInteger:支持任意精度的整数,可以准确地表示任何大小的整数值。
-
BigDecimal:支持任何精度的浮点数,例如,可以用它进行精确的货币计算。
2.2.3 Java中的数组
Java的主要目标之一是安全性,所以,Java以每个数组上少量的内存开销及运行时的下标检查为代价,对数组进行范围检查,使得程序员无法在数组范围之外访问它。
当创建一个对象数组时,实际上是创建了一个引用数组,并且每个引用都会被自动初始化为null,当试图不进行初始化就使用这个引用,那么在运行时将会报错。
还可以创建用来存放基本数据类型的数组,编译器也确保了这种数组的初始化,它会将数组所占的内存全部置零。
2.3 永远不需要销毁对象
在大多数程序设计语言中,变量的生命周期的概念,占据了程序设计工作中非常重要的部分。本节将介绍Java是如何进行清理工作。
2.3.1 作用域
大多数过程型语言都作用域(scope)的概念。 作用域决定了在其内定义的变量名的可见性和生命周期。 在C、C++和Java中,作用域由花括号的位置决定。如:
{
int x = 12;
// Only x available
{
int q = 96;
// Both x & q available
}
// Only x available
// q is "out of scope"
}
在作用域里定义的变量只可用于作用域结束之前。
Java是一种自由格式(free-form)的语言,所以空格、制表符、换行都不会影响程序的执行结果。
以下代码在C和C++中是合法的,但在Java中编译错误:
{
int x = 12;
{
int x = 96; //Duplicate local variable x
}
}
编译器会报告变量x已经定义过,不能重复定义。
2.3.2 对象的作用域
Java对象不具备和基本类型一样的声明周期。当用new创建一个Java对象时,它可以存活于作用域之外。
{
String s = new String("HelloWorld");
} // End of Scope
引用s在作用域终点就消失了,但s指向的String对象仍然继续占据内存空间。在上面的代码中,由于该对象的唯一引用在作用域结束后失效,我们也无法访问该对象了。不过,在之后的章节中,我们可以学会:在程序执行过程中,如何传递和复制对象引用。
在Java中,由new创建的对象,只要需要,就会一直保留下去,那些不会再被引用的对象,会被垃圾回收器回收,从而释放这些对象的内存空间,以便供其他新的对象使用。
2.4 创建新的数据类型:类
在Java中,通过关键字class可以创建一个新的对象类型。class后紧跟新类型的名称:
class ATypeName { /* Class body goes here */}
然后,你就可以使用new来创建这种类型的对象:
ATypeName a = new ATypeName();
但是,在给它定义方法之前,暂时它还不具备行为。(即不能向它发送任何有意义的消息)
一旦定义了一个类,就可以在类中设置两种类型的元素:字段和方法。
2.4.1 字段
字段:可以是任何类型的对象引用或基本类型的变量。如果是对象引用,则必须初始化该引用,以便使其与一个实际的对象相关联。
每个对象都有用来存储其字段的空间,普通字段不能在对象间共享。下面是一个具有某些字段的类:
public class DataOnly {
int i;
double d;
boolean b;
}
这个类只能存储数据,我们首先创建该类的一个对象,然后可以给字段赋值,在赋值时,我们需要知道如何引用一个对象的成员。具体实现为:在对象引用的名称后紧接着一个句点,然后再接着对象内部的成员名称:objectReference.member。例如:
DataOnly data = new DataOnly();
data.i = 47;
data.d = 1.1;
data.b = false;
当想修改的数据位于对象所包含的其他对象中时,只需再使用连接句点即可:
myPlane.leftTank.capacity = 100;
数据成员的默认值:
-
基本数据类型:编译器默认将其初始化为零。
-
对象引用:默认为null。
对于局部变量,编译器不会初始化,如果在使用时没有进行初始化,编译器会报错。
2.5 方法、参数和返回值
Java中的方法决定了一个对象能够接收什么样的消息。 方法的基本组成部分包括:名称、参数、返回值和方法体。 下面是它最基本的形式:
ReturnType methodName(/* Argument list */){
/* Method body */
}
-
返回类型:在调用方法之后从该方法返回的值的类型。
-
参数列表:要传给该方法的信息的类型和名称。
方法名和参数列表合起来被称为方法签名,可以唯一地标识出某个方法。
Java中的方法只能作为类的一部分来创建。只有类具有某个方法,该类对象才能调用该方法。通过对象调用方法时,需先列出对象名,紧接着句点,然后是方法名和参数列表,如:
objectName.methodName(arg1,arg2,arg3);
假设A类中有一个方法f(),不带任何参数,返回类型为int,如果A类的一个对象a调用该方法,则可以写成:
int x = a.f();
这种调用方法的行为通常被称为发送消息给对象。 上例中,消息是f(),对象是a。
2.5.1 参数列表
方法的参数列表指定要传递给方法的信息详情,可以是基本类型的变量或对象引用。传递的参数类型必须和方法定义时的参数类型一致,否则编译器会报错。 例如:参数被设定为String类型,则必须传递一个String对象。
下面的例子是一个方法的具体定义:
int storage(String s){
return s.length() * 2;
}
该方法计算所传递的String对象所包含信息的字节数(字符串中每个字符的尺寸都是16位或2个字节)。 此方法参数类型为String,参数名为s。在方法体内,可以对s进行任何String类具有的操作。如s.length(),会返回字符串包含的字符数。
上例中,我们看到了return关键字,它的用法包括两方面:
(1) 该方法已结束,离开此方法。
(2) 返回return关键字后的值。
若返回类型是void,return关键字的作用只是用于退出方法。若返回类型不是void,那么无论在何处返回,编译器都会强制要求返回一个正确类型的值。
到此为止,我们发现:程序只是一系列带有方法的对象组合,这些方法以其他对象为参数,并发送消息给其他对象。
2.6 构建一个Java程序
2.6.1 名字可见性
名字管理对任何程序设计语言来说,都是一个重要问题。
为了避免程序中的某个模块中名字冲突,Java设计者提出:开发者使用自己的Internet域名的倒序去命名包名。
2.6.2 运用其他构件
如果想在自己的程序中使用预先定义好的类:
-
这个类与发出的调用的源文件位于同一个包内,则可以直接使用。
-
这个类位于其他包内,则必须使用关键字import来准确地告诉编译器所需要的类。
例如,我们使用Java标准类库的构件,如ArrayList类:
import java.util.ArrayList;
在util包下有数量众多的类,当我们想要使用其中多个类,并不想逐一声明时,通配符*可以达到这目的:
import java.util.*;
2.6.3 static 关键字
通常情况下,我们需要对某个类进行操作时,需要创建该类对象,然后该对象的数据成员和成员函数会被初始化,从而可以访问其成员,调用其方法。
不过,当我们需要的是所有对象都共享的数据和方法时,上述方法则无法满足我们需求。
在面向对象语言中,它们的术语为:
-
类数据:隶属于类的数据成员。
-
类方法:隶属于类的成员函数。
static关键字可以帮助我们实现上述情况。当声明一个事物为static后,就意味着这个域或方法不再与该类的任何对象实例关联。
通过下面的代码,我们可以得知如何使用static定义字段,并且该类的多个对象均共享该数据成员:
public class StaticTest {
static int i = 47;
public static void main(String[] args) {
StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();
System.out.println(st1.i == st2.i);
System.out.println(StaticTest.i);
}
}
从上例中,我们可以看出引用静态变量有两种方式:
- 通过该类的对象去访问。
- 通过类名直接引用。 建议方式,它强调该变量的静态结构,并且在某些情况下,它为编译器优化提供方便。
下面,我们进行静态方法的定义与使用:
public class Incrementable {
static void increment() {
StaticTest.i++;
}
public static void main(String[] args) {
Incrementable incrementable = new Incrementable();
incrementable.increment();
Incrementable.increment();
}
}
如同引用静态变量一样,静态方法的调用也有同上两种方式。
当static关键字作用于字段时,数据的存储空间分配方式会有所改变(static字段对于每个类都只有一份存储空间,而非static字段则是对每个对象都有一个存储空间)。
static方法与非static方法差别不大,最重要的差别就是,在不创建任何对象的前提下就可以调用它。
2.7 第一个Java程序
最后,我们编写一个完整的程序,分别打印一个字符串和日期,这里用到了Java标准库里的Date类:
import java.util.Date;
public class HelloDate {
public static void main(String[] args) {
System.out.println("Hello,it's:");
System.out.println(new Date());
}
}
在程序开头,我们通过声明import语句,在代码引入额外的类库。java.lang包下的所有类都会被自动地导入每一个Java文件中,即它的所有类都可以被直接使用。
Sun公司为开发人员提供了JDK文档,在该文档中,可以看到Java配套提供的各种类库。
我们可以在文档中看到,System类位于java.lang包下,并且System中有很多字段,out就是其中之一,并且发现它是一个静态的PrintStream对象。println()就是PrintStream类的一个方法,它的实际作用是:将参数信息打印在控制台并换行。
如果创建一个独立运行的程序,文件中必须存在与该文件同名的类,且类中必须包含名为main的方法,形式如下所示:
public static void main(String[] arg){
}
其中public关键字为访问控制符,参数为String对象的数组。
通过阅读JDK文档,将会发现System有许多其他方法,使我们发现更多有趣的效果(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方法中,显示了从运行程序的系统中获取的所有属性,即环境信息、用户名和JDK路径。
2.7.1 编译和运行
要编译、运行上述程序,首先必须有一个Java开发环境。目前,有很多第三方厂商提供开发环境,我们可以选择Sun免费提供的JDK(Java Developer's Kit)开发环境。
安装好JDK后,我们需要设定好路径信息,以确保计算机能够找到javac和java这两个文件。
在程序所在目录下,输入:
javac HelloDate.java
java HelloDate
便可看到程序中的消息和当天日期被输出。
2.8 注释和嵌入式文档
Java中有两种注释风格:
-
多行注释:以 /* 开始,随后是注释内容,可跨越多行,最后以 */ 结束。在编译时,/ *和 */ 之间的所有东西都会被忽略。
-
单行注释:以//开头,直到句末。
例如:
/* This is a comment
* that continues
* across lines
*/
// This is a one-line comment
2.8.1 注释文档
代码文档撰写的最大问题就是文档的维护,最佳解决方案就是将代码和文档都放在同一个文件中,为达到这样的目的,我们需要:
- 使用一种特殊的注释语法来标记文档。
- 需要提取注释,并将其转换成有用格式的工具。
JDK提供了用于提取注释的工具javadoc,它采取了Java编译器的某些技术,查找程序内的特殊注释标签。它不仅解析由这些标签标记的信息,也将毗邻注释的类名或方法名抽取出来。
如果向对javadoc处理过的信息执行特殊的操作(例如,产生不同格式的输出),那么可以通过自定义的Doclet(javadoc处理器)来实现。
2.8.2 语法
注释文档以 /** 开始, 以 */结束。 共有三种类型的注释文档:
- 类注释:位于类定义之前。
- 域注释:位于域定义之前。
- 方法注释:位于方法定义之前。
如下面示例所示:
/** A class comment */
public class Documentation1 {
/** A field comment */
public int i;
/** A method comment */
public void f() { }
}
所有的javadoc命令都只能在注释文档中出现,使用javadoc的方式主要有两种:嵌入HTML和文档标签。
2.8.3 嵌入式HTML
javadoc通过生成HTML文档传送HTML命令,我们可以像在其他Web文档中那样运用HTML,对普通文本按照描述进行格式化:
/**
* You can <em>even</em> insert a list:
* <ol>
* <li> Item one
* <li> Item two
* <li> Item three
* </ol>
*/
在注释文档中,位于每行开头的星号和前导空格都会被javadoc丢弃。javadoc会对所有内容重新格式化,使其与标准文本外观一致。在嵌入式HTML中使用标题标签(< h1 >、< h2 >等)会和javadoc插入的标题冲突。
所有类型的注释文档(类、域和方法)都支持嵌入式HTML。
2.8.4 一些标签示例
下面将介绍一些用于代码文档的javadoc标签:
-
@see:引用其他类,该标签允许用户引用其他类的文档。javadoc会在生成的文档中,加入一个"See Also"的超链接条目。
-
{@link package.class#member label}:与@see相似,它只用于行内。生成的文档用"label"作为超链接文本。
-
{@docRoot}:该标签产生到文档根目录的相对路径,用于文档树页面的显式超链接。
-
{@inheritDoc}:该标签从当前这个类的父类中继承相关文档到当前的文档注释中。
-
@version:生成的文档从该标签后提取版本信息。
-
@author:生成的文档从该标签后提取作者信息,可以连续放置多个该标签,以便列出所有作者。
-
@since:程序使用的最早的版本信息,如JDK版本使用情况。
-
@param:格式如下:@param parameter-name description,描述参数信息。
-
@return:格式如下:@return description,描述返回值信息。
-
@throw:格式如下:@throw fully-qualified-class-name description,描述异常说明。
-
@deprecated:该标签用于指出哪些旧特性已被新特性取代。
2.8.5 文档示例
下面,我们将第一个Java程序加上文档注释:
/** The first Thinking in Java example program.
* Displays a string and today's date.
* @author weiqing.jiao
* @version 4.0
*/
public class HelloDate {
/** Entry point to class & application
* @param args array of string arguments
* @throws exceptions No exceptions thrown
*/
public static void main(String[] args) {
System.out.println("Hello,it's:");
System.out.println(new Date());
}
}
2.9 编码风格
在Java编程语言编码规定中规定代码风格为:类名的首字母要大写;如果类名由多个单词构成,则把它们合并在一起,其中每个内部单词的首字母都采用大写形式。 例如:
public class AllTheColorsOfTheRainbow { // ... }
这中风格称为:驼峰风格。方法名、字段以及对象引用名称等,都是该风格,只是第一个字母采用小写。 例如:
public class AllTheColorsOfTheRainbow {
int anIntegerRepresentingColors;
void changeTheHueOfTheColor(int newHue) {
// ...
}
// ...
}
2.10 总结
本章主要介绍,基础的Java编程知识、Java语言的特性以及它的一些基本思想。