1. 一切都是对象


1.1 面向对象设计语言

Java是基于C++的,但相比之下,Java是更纯粹的面向对象程序设计语言。C++和Java都是混合/杂合型语言,允许多种编辑风格,C++之所以成国一种杂合型语言主要是因为它支持与C语言的向后兼容。Java语言假设我们只进行面向对象的程序设计,在Java中几乎一切都是对象。

每种编程语言都有自己的操纵内存中元素的方式,因为在Java中一切都被视为对象,所以可采用单一固定的语法来操纵元素,即是对象的一个引用。拥有一个引用并不一定需要有一个对角与之关联,例如创建一个String引用:

String s;

但这里只是创建一个引用,并不是对象,如此此时向s发送一个消息,就会返回一个运行时错误。一种安全的做法是创建一个引用的同时便进行初始化,例如:

String s = "Hello World";

通常用new操作符来使创建的引用和新的对象相关联,例如:

String s = new String("Hello World");


1.2 对象存储

有五个不同的地方可以存储数据。

1) 寄存器

这是最快的存储区,因为它位于处理器内部。但是寄存器的数量极其有限,所有寄存器根据需求进行分配,不能直接控制。

2) 堆栈

位于通用RAM中,但通过堆栈指针可以从处理器那里获得直接支持。这是一种快速有效的分配存储的方法,仅次于寄存器。创建程序时,Java系统必须知道存储在堆栈内所有项的确切生命周期,以便移动堆栈指针。

3) 堆

一种通用的内存池(也位于RAM区),用于存放所有的Java对象。堆不同于堆栈的好处是:编译器不需要知道存储的数据在堆里存活多长时间,因此在堆里分配存储有很大的灵活性。只需要用new写一行简单的代码,执行这行代码时,会自动在堆里进行存储分配。

4) 常量存储

常量值通常直接存放在程序代码内部,因为它们永远不会被改变。有时在嵌入式系统中,常量本身会和其他部分隔离开,这种情况下可以选择将其存放在ROM中。

5) 非RAM存储

如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在,比如流对象和持久化对象。


1.3 基本类型

在程序设计中经常用到一系列类型,需要特殊对待。之所以特殊对待,是因为new将对象存储在堆里,所以用new创建一个对象(特别是小的、简单的变量)往往不是很有效。因此对于这些类型,Java采取与C++相同的办法,即不用new来创建变量,而是创建一个并非是引用的自动变量,这个变量直接存储值,并置于堆栈中,因此更加高效。

Java要确定每种基本类型所占存储空间的大小,它们的大小并不随机器硬件架构的变化而变化,这也是Java程序比其他大多数语言编写的程序更具可移植性的原因之一。

基本类型
大小最小值最大值包装器类型
booleanN/AN/AN/ABoolean
char16-bitUnicode 0Unicode 2^16-1Character
byte8 bits-128+127Byte
short16 bits-2^15+2^15-1Short
int32 bits-2^31+2^31-1Integer
long64 bits-2^63+2^63-1Long
float32 bitsIEEE754IEEE754Float
double64 bitsIEEE754IEEE754Double
voidN/AN/AN/AVoid

基本类型具有的包装器类,使得可以在堆中创建一个非基本对象,用来表示对应的基本类型,例如:

char c = 'x';
Character ch = new Character('x');

Java提供了两个用于高精度计算的类:BigInteger和BigDecimal,虽然它们大体上属于包装器类的范畴,但是都没有对应的基本类型。不过,这两个类包含的方法,提供的操作与对基本类型所能执行的操作相似。只不过必须以方法调用方式取代运算符方式来实现,所以运算速度会比较慢。BigInteger支持任意精度的整数,BigDecimal支持任何精度的定点数。

Java的主要目标之一是安全性,所以在C和C++中使用数组(也就是内存块)的危险在Java里不会再出现,Java确保数组会被初始化,而且不能在它的范围之外被访问。这种范围检查是以每个数组上少量的内存开销及运行时的下标检查为代价的。

当创建一个数组对象时,实际上就是创建一个引用数组,并且每个引用都会自动被初始化为一个特定值,该值拥有自己的关键字null。还可以创建用来存放基本数据类型的数组,同样编译器也能确保这种数组的初始化,因为它会将这种数组所占的内存全部置零。


1.4 变量生命周期

大多数语言都有作用域的概念,作用域决定了在其内定义的变量名的可见性和生命周期。在C、C++和Java中,作用域由花括号的位置决定,例如:

{
  int x = 10;
  {
    int y = 20;
  }
}

在作用域里定义的变量只可用于作用域结束之前。Java是自由格式的语言,所以空格、制表符、换行都不会影响程序的执行结果,缩排格式会使Java代码更易阅读。Java中不能这样写:

{
  int x = 10;
  {
    int y = 20;
  }
}

编译器将会报告变量x已经定义过,所以在C和C++里将一个较大作用域的变量隐藏的做法在Java里不不允许的。

Java对象不具备和基本类型一样的生命周期,当用new创建一个Java对象时,它可以存活于作用域之外,例如:

{
  String s = new String("Hello World");
}

引用s在作用域终点就消失了,然而s指向的String对象仍继续占据内存空间,我们无法在这个作用域之后访问这个对象,因为对它唯一的引用已超出了作用域的范围。

Java有一个垃圾回收器,用来监视new创建的所有对象,并辨别不会再被引用的对象,随后,释放这些对象的内存空间,以便供其他新的对象使用,这样就消除了例如内存泄漏的问题。


1.5 类

class关键字用来表示创建一个新的类,例如:

class MyClass {
}

这就引入了一种新的类型,尽管类主体没有实质的内容,还不能用它做太多事情,然而,已经可以用new来创建这种类型的对象,例如:

MyClass c = new MyClass();

一旦定义了一个类,就可以在类中设置两种类型的元素:字段和方法。字段可以是任何类型的对象,可以通过其引用与其进行通信,也可以是基本类型中的一种。如果字段是对某个对象的引用,那么必须初始化该引用,以便使其与一个实际的对象相关联。

每个对象都有用来存储其字段的空间,普通字段不能在对象间共享。可以给字段赋值,但首先必须引用一个对象的成员,具体的实现为,在对象引用的名称之后紧接着一个句点,然后接对象内部的成员名称,例如:

class MyClass {
  int i;
}
MyClass c = new MyClass();
c.i = 100;

若类的某个成员是基本数据类型,即使没有初始化,Java也会确保它获得一个默认值。然而这种初始化方法并不适用于局部变量,例如在某个方法中定义一个变量,那么变量值可能是任意值(与C和C++一样)。

许多程序设计语言用函数来描术命名子程序,而在Java里常用方法来表示。Java的方法决定了一个对象能够接收什么样的消息,方法的基本组成部分包括:名称、参数、返回值和方法体,例如:

int mymethod(int argu) {
}

返回类型描述的是在调用方法之后从方法返回的值。参数列表给出了要传给方法的信息的类型和名称。方法名和参数列表唯一标识一个方法。Java中的方法只能作为类的一部分来创建,方法只有通过对象才能被调用,且这个对象必须能执行这个方法调用,例如:

c.mymethod(100);

这种调用方法的行为通常被称为发送消息给对象。在上例中,消息是mymethod,对象是c。方法的参数列表指定要传给方法什么样的信息,这些信息采用的都是对象形式,因此在参数列表中必须指定每个所传递对象的类型及名字。像Java中传递对象的场合一样,这里传递的实际上也是引用,并且引用类型必须正确。

可以定义方法返回任意的类型,如果不想返回任何值,可以指示方法返回void。若返回类型是void,return关键字的作用只是用来退出方法。因此,没有必要到方法结束时才离开,可在任何地方返回。但如果返回类型不是void,那么无论在何处返回,编译器将强制返回一个正确类型的返回值。


2. 构建Java程序


2.1 基本问题

Java采用了一种全新的方法来避免相同名字产生的冲突问题。为了给一个类库生成不会与其他名字混淆的名字,Java设计者希望程序员反过来使用自己的Internet域名,这样可以保证它们是独一元二的。例如net.test.utility.foibles。

如果想在程序中使用预先定义好的类,可以使用关键字import来准确告诉编译器想要的类是什么。import指示编译器导入一个包,也就是一个类库。大多时候,我们使用与编译器附在一起的Java标准类库里的构件,就不必写一长串的反转域名,例如:

import java.util.ArrayList;

这些代码告诉编译器,需要使用Java的ArrayList类。但是util包含了许多类,有时想使用其中几个,又不想逐一声明,可以使用通配符"*"来达到目的,例如:

import java.util.*

通常来说,创建类时只是在描述类的对象的外观与行为,除非用new创建类的对象,否则实际上并未获得任何对象。执行new来创建对象时,数据存储空间才被分配,其方法才供外界调用。有时只想为某特定域分配单一存储空间,而不考虑创建多少对象,或者希望某个方法不与包含它的类的任何对象关联在一起,可以通过static关键字来实现。

当声明一个事物是static时,意味着这个域或方法不会与包含它的类的任何对象实例关联在一起。所以,即使从未创建某个类的任何对象,也可以调用其static方法或访问其static域。只须将static关键字放在定义之前,就可以将字段或方法设定为static,例如:

class MyClass {
  static int i = 10;
}

现在,即使创建了两个MyClass对象,MyClass.i也只有一份存储空间,这两个对象共享同一个i。引用static变量有两种方法,可以通过一个对象去定位它,如c.i,也可以通过其类名直接引用,例如:

MyClass.i++;

类似逻辑也应用于静态方法,即可以像其他方法一样,通过一个对象来引用某个静态方法,也可以通过类名加以引用,定义静态方法的方式也与定义静态变量的方式相似,例如:

class MyClass {
  static void method() {
  }
}
MyClass.method();


2.2 第一个程序

我们将编写一个完整的程序,此程序先是打印一个字符串,然后是打印当前日期,这里用到了Java标准库里的Date类:

import java.util.*;
public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello World.");
    System.out.println(new Date());
  }
}

在每个程序的开头,必须声明import语句,以便引入在文件代码中需要用到的额外类。之所以说是额外,是因为有一个特定类会自动被导入到每一个Java文件中:java.lang。查找Java的JDK文档,java.lang里没有Date类,所有必须导入另外一个类库才能使用它,而java.lang的system类则包含了静态的out对象,直接使用即可。

要编译、运行这个程序,首先必须要有一个Java开发环境,可以使用Sun免费提供的JDK开发环境。安装好JDK后,还需要设定好路径信息,以确保计算机能找到javac和java这两个文件。


2.3 注释和文档

Java里有两种注释风格。一种是C语言风络的注释,以"/*"开始,随后是注释内容,最后以"*/"结束,例如:

/* This is a comment */

第二种风格以一个"//"开头,直到句末,例如:

// This is a comment

代码文档撰写的最大问题,就是如果文档与代码是分离的,那么在每次修改代码时,都需要修改相应的文档。解决的方法是将代码同文档链接起来,达到这个目的最简单的方法是将所有东西都放在同一个文件内,此外还必须使用一种特殊的注释语法来标记文档,还需要一个工具用于提取那些注释。

javadoc便是用于提取注释的工具,它是JDK安装的一部分。它采用了Java编译器的某些技术,查找程序内的特殊注释标签。它不仅解析由这些标签标记的信息,也将毗邻注释的类名或方法名抽取出来。如此就可以用较少的工作量生成较好的程序文档。全面的javadoc描述可从java.sun.com下载。


3. 操作符


3.1 操作符和优先级

Java是建立在C++基础上的,所以C和C++程序员应该非常熟悉Java的大多数操作符。当然,Java也做了一些改进与简化。

操作符接受一个或多个参数,并生成一个新值。参数的形式与普通的方法调用不同,但结果是相同的。操作符用于操作数,生成一个新值,有些操作符可能会改变操作数自身的值。几乎所有的操作符都只能操作基本类型,例外的操作符是"="、"=="和"!=",这些操作符能操作所有的对象,除此以外,String类支持"+"和"+="。

当一个表达式中存在多个操作符时,操作符的优先级决定了各部分的计算顺序。Java对计算顺序做了特别的规定。其中最简单的规则就是先乘除后加减。其他优先级规则因为容易被忘记,所以应该用括号明确规定计算顺序。


3.2 赋值

赋值使用操作符"="。它的意思是最右边的值复制给左边的值。右值可以是任何常数、变量或者表达式,但左值必须是一个明确的已命名的变量,例如:

a = 1;

基本类型存储了实际的数值,而并非指向一个对象的引用,所以在为其赋值的时候,是直接将一个地方的内容复制到了另一个地方。但是在为对象赋值时,真正操作的是对对象的引用,所以如果将一个对象赋值给另一个对象,实际是将引用从一个地方赋值到另一个地方,例如:

MyClass c1 = new MyClass();
MyClass c2 = new MyClass();
c2 = c1;

由于赋值操作的是一个对象的引用,所以修改c2的同时也会改变c1,这种特殊的现象称作别名现象,是Java操作对象的一种基本方式。


3.3 算术操作符

Java的基本算术操作符包括加号"+"、减号"-"、除号"/"、乘号"*以及取模操作符"%"。整数除法会直接去掉结果的小数位。Java也使用简化符号同时进行运算与赋值操作,这用操作符后紧跟一个等号来表示,它对于Java中的所有操作符都适用,例如:

x += 1;

一元减号"-"和一元加号"+"与二元减号和加号都使用相同的符号。根据表达式的书写形式,编译器会自动判断出使用的是哪一种,例如:

x = -y;

和C类似,Java提供了大量的快捷运算,递增和递减是其中两种。递减操作符是"--",意为减少一个单位,递增操作符是"++",意为增加一个单位。这两个操作符各有两种使用方式,通常称为“前缀式”和“后缀式”。前缀递增表示"++"操作符位于变量或表达式的前面,而后缀递增表示"++"操作符位于变量或表达式的后面,前缀递减和后缀递减的情况类似。对于前缀操作符,会先执行运算,再生成值,而对于后缀操作符,会先生成值,再执行运算,例如:

int i = 1;
i++;
--i;


3.4 关系运算符

关系操作符生成的是一个boolean结果,它们计算的是操作数的值之间的关系。如果关系是真实的,关系表达式会生成true,如果关系不真实,则生成false。关系操作符包括小于"<"、大于">"、小于或等于"<="、大于或等于">="、等于"=="以及不等于"!="。等于和不等于适用于所有基本类型,而其他比较符不适用于boolean类型,例如:

Integer n1 = new Integer(10);
Integer n2 = new Integer(10);
System.out.print(n1 == n2);

这个语句的输出的结果是false,尽管对象的内容相同,然而对象的引用却是不同的,而"=="和"!="比较的就是对象的引用。如果想比较两个对象的实际内容是否相同,必须使用所有对象都适用的特殊方法equals(),但这个方法不适用于基本类型,基本类型直接使用"=="和"!="。


3.5 逻辑操作符

逻辑操作符与"&&"、或"||"、非"!"能根据参数的逻辑关系,生成一个布尔值,例如:

int i = 1;
System.out.println(i < 10 && i > 0);
System.out.println(i < 8 || i > 4);

逻辑操作只可就用于布尔值,不可将一个非布尔值当作布尔值在逻辑表达式中使用。如果在应该使用Strin值的地方使用了布尔值,布尔值会自动转换成适当的文本形式。

当使用逻辑操作符时,会遇到一种短路现象,即一旦能够明确无误地确定整个表达式的值,就不再计算表达式余下部部,因此,整个逻辑表达式靠后的部分有可能不会被运算,例如:

boolean b1 = false && true;
boolean b2 = true || false;


3.6 直接常量

一般而言,如果在程序里使用了直接常量,编译器可以准确知道要生成什么样的类型,但有时候却是模棱两可的,需要用与直接常量相关的某些字符来额外增加一些信息。直接常量后面的后缀字符标志了它的类型。若为大写或小写的字母L代表long,大写或小写的字母F代表float,大写或小写的字母D,代表double,例如:

long i = 100L;
float j = 1F;
double k = 100D;

十六进制数适用于所有整数数据类型,以前缀0x以及后续的0-9或字母的a-f来表示。八进数数由前缀0以及后续的0-7的数字来表示,例如:

int i1 = 0x2f;
int i2 = 0177;

在Java的指数计数中,使用e代表10的幂次,所以在Java中看到像1.23e-45f这样的表达式时,含义为1.23乘以10的负45次方,如果编译器能够正确识别类型,就不必在数值后附加字符。


3.7 位操作符

按位操作符用来操作整数基本数据类型中的二进制位,按位操作符会对两个参数中对应的位执行布尔代数运算,并最终生成一个结果。如果两个输入位都是1,则按位与"&"生成一个输出位1,否则生成一个输出位0。如果两个输入位里只要有一个是1,则按位或"|"生成一个输出位1,只有在两个输入位都是0的情况下,它才会生成一个输出位0。如果输入位的某一个是1,但不全都是1,那么按位异或"^"生成一个输出位1。按位非"~"属于一元操作符,生成与输入位相反的值。按位操作符可与等号"="联合使用,比如"&="、"|="、"^="。

移位操作符操作的运算对象也是二进制位。左移位操作符"<<"按照操作符右侧指定的位数将操作符左边的操作数向左移动,在低位补0。有符号右移位操作符">>"按照操作符右侧指定的位数将操作符左边的操作数向右移动。有符号操作符使用符号扩展,若符号为正,则在高位插入0,若符号为负,则在高位插入1。Java中增加了一种无符号右移位操作符">>>",无论正负,都在高位插入0。移位可与等号"="组合使用,例如"<<="、">>="或">>>="。

如果对char、byte或short类型的数值进行移位处理,那么在移位之前,它们会被转换为int类型,并且得到的结果也是一个int类型,只有数值右端的低5位才有用。若对一个long类型的数值进行处理,最后得到的结果也是long,此时只会用到数值右端的低6位。


3.8 三元操作符

三元操作符也称为条件操作符,它有三个操作数,其表达式采取下述形式:

boolean-exp ? value0 : value1;

如果boolean-exp表达式的结果为true,就计算value0,如果结果为false,就计算value1,它的结果就成为了操作符最终产生的值。


3.9 字符串操作符

"+"和"+="操作符在Java中用以连接不同的字符串,如果表达式以一个字符串起头,那么后续所有操作数都必须是字符串型,例如:

int x = 0, y = 1, z = 2;
String s = "Hello ";
s += "World : ";
s = s + x + y + z

Java编译器会将x、y、z转换成它们的字符串形式,然后连接这些字符串。


4. 控制流程


4.1 if-else

if-else语句是控制程序流程的最基本的形式,可以按以下两种形式使用:

if (Boolean-expression)
  statement
if (Boolean-expression)
  statement
else
  statement

布尔表达式必须产生一个布尔结果,statement代表用分号结尾的简单语句或复合语句。


4.2 while

while、do-while用来控制循环,语句会重复执行,直到起控制作用的布尔表达式得到的结果为止,while循环的格式如下:

while (Boolean-expression)
  statement

在循环刚开始时,会计算一次布尔表达式的值,而在语句的下一次迭×××始前会再计算一次。

do-while的格式如下:

do
  statement
while (Boolean-expression)

while和do-while唯一的区别是do-while中的语句至少会执行一次,即便表达式第一次就被计算为false。


4.3 for

for循环是最常命名用的迭代形式,第一次迭代之前要进行初始化,随后,它会进行条件测试,而且在每一次迭代结束时,进行某种形式的步进,for循环的格式如下:

for (initialization; Boolean-expression; step)
  statement

初始化表达式,布尔表达式,或者步进运算,都可以为空。每次迭代前会测试布尔表达式,若获得的结果是false,就会执行for语句后面的代码行,每次循环结束,会执行一次步进。


4.4 foreach

Java SE5引入了一种新的更加简洁的for语法用于数组和容器,即foreach语法,表示不必创建int变量去对由访问项构成的序列进行计算,foreach将自动产生每一项,例如:

Random rand = new Random(100);
for (int i = 0; i < 10; i++)
  f[i] = rand.nextFloat();
for (float x : f)
  system.out.print(x);

语句定义了一个float类型的变量x,继而将每一个f元素赋值给x。任何返回一个数组的方法都可以使用foreach,例如:

for (char c : "Hello Word!".toCharArray())
  System.out.print(c + " ");


4.5 return

在Java中有多个关键词表示无条件分支,它们只是表示这个分支无需任何测试即可发生,包括return、break、continue和类似goto跳转到标号语句的方式。

return关键词有两方面用途:一方面指定一个方法返回什么值,另一方面它会导致当前方法退出,并返回那个值,例如:

static int test(int value1, int value2) {
  if (value1 > value2)
    return +1;
  else if (value1 < value2)
    return -1;
  else
    return 0;
}

在任何迭代语句的主体部分,都可用break和continue控制循环的流程,其中break用于强行退出循环,而continue则停止执行当前的迭代,退回循环起始处,开始下一次迭代,例如:

for (int i = 0; i < 100; i++) {
  if (i % 10 == 0)
    continue;
  if (i % 50 == 0)
    break;
  System.out.print(i + " ");
}

在Java中,goto仍是保留字,但在语言中并未使用它。然而Java也能完成一些类似于跳转的操作:标签。break和continue通常只中断当前循环,但若随同标签一起使用,它们就会中断循环,直到标签所在的地方。需要留意的是,标签唯一起作用的地方刚好是在迭代语句之前,例如:

label1:
for (int i = 0; i < 100; i++)
  for (int j = 0; j < 100; j++) {
    if (i == 50)
      break label1;
    if (j == 50)
      continue label1;
  }


4.6 switch

switch语句根据整数表达式的值,可以从一系列代码中选出一段去执行,它的格式如下:

switch (integral-selector) {
  case integral-value1: statement; break;
  case integral-value2: statement; break;
  case integral-value3: statement; break;
  defalut: statement
}

Integral-selector是一个能够产生整数值的表达式,switch能将这个表达式的结果与每个ingegral-value比较,若发现相符的,就执行对应的语句,若没有发现相符的,就执行default语句。break是可选的,若省略break,会继续执行后面的case语句,直到遇到break为止。