首先,这本书的中文版(第四版,2006年)的内容主要基于Java SE 5-6,里面的部分内容与现在相比(JRE 10 soon)已经过时。
作者Bruce Eckel与2017年出版了On Java 8,可以看作是Thinking in Java 5th Edition
Github上有翻译项目 https://github.com/LingCoder/OnJava8
第一章 对象导论 #
“我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们。语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” – Alfred Korzybski (1930)
抽象 #
Smalltalk 作为第一个成功的面向对象并影响了 Java的程序设计语言 ,Alan Kay 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:
- 万物皆对象。你可以将对象想象成一种特殊的变量。它存储数据,但可以在你对其“发出请求”时执行本身的操作。理论上讲,你总是可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表示为一个对象。
- 程序是一组对象,通过消息传递来告知彼此该做什么。要请求调用一个对象的方法,你需要向该对象发送消息。
- 每个对象都有自己的存储空间,可容纳其他对象。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。
- 每个对象都有一种类型。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。
- 同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收发送给"形状”的消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。
接口 #
那么如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其解决一些实际的问题,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的==“接口”(Interface)定义的==,对象的“类型”或“类”则规定了它的接口形式`。“类型”与“接口”的对应关系是面向对象程序设计的基础。
封装 #
- 让应用程序员不要触摸他们不应该触摸的部分。(请注意,这也是一个哲学决策。部分编程语言认为如果程序员有需要,则应该让他们访问细节部分。);
- 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松地完成改造。
Java 有三个显式关键字来设置类中的访问权限,其中
protected (受保护) 类似于 private,区别是子类(下一节就会引入继承的概念)可以访问 protected的成员,但不能访问 private 成员;
default 被称为包访问,因为该权限下的资源可以被同一包(库组件)中其他类的成员访问。
复用 #
最简单的复用是使用一个类的对象,或者用现有类的对象去组装类(这里的对象称为成员变量)。举例来说,汽车有引擎和仪表盘,这里的引擎和仪表盘即是成员变量,如果现有交通工具这个类的话,交通工具类的引擎和仪表盘变量就可以拿来组装汽车类。
这样的"组装"行为严格来讲分为两类,
- 组合(Composition)经常用来表示“拥有”关系(has-a relationship)。例如,“汽车拥有引擎”。
- 聚合(Aggregation) 动态的 组合。
组合和聚合都属于关联关系的一种,只是额外具有整体-部分的意义。至于是聚合还是组合,需要根据实际的业务需求来判断。可能相同超类和子类,在不同的业务场景,关联关系会发生变化。只看代码是无法区分聚合和组合的,具体是哪一种关系,只能从语义级别来区分。聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时共享同一个部件。这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。
继承 #
创建基类,以表现思想的核心,创建子类,以不同方式表现思想。子类与基类的关系反映出类的相似性与差异性,这样一个逻辑结构能够将大部分的问题情景进行抽象。 要为子类创建基类中没有的方法,使用extends关键字 要为子类改变基类中的方法,使用overriding特性 Java中的类均为单根继承,即所有类都是Object的子类
多态 #
发送消息给对象时,如果程序不知道接收的具体类型是什么,但最终执行是正确的,这就是对象的多态性(Polymorphism)。面向对象的程序设计语言是通过动态绑定的方式来实现对象的多态性的。编译器和运行时系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好地设计程序。
集合 #
- 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,它们中的一种提供的解决方案可能比其他灵活得多。
- 不同的集合对某些操作有不同的效率。例如,List 的两种基本类型:ArrayList 和 LinkedList。虽然两者具有相同接口和外部行为,但是在某些操作中它们的效率差别很大。在 ArrayList 中随机查找元素是很高效的,而 LinkedList 随机查找效率低下。反之,在 LinkedList 中插入元素的效率要比在 ArrayList 中高。由于底层数据结构的不同,每种集合类型在执行相同的操作时会表现出效率上的差异。
参数化类型机制(Parameterized Type Mechanism),或者叫泛型(Generic)使得在集合中存取对象时,免于向下转型的开销,
ArrayList<Shape> shapes = new ArrayList<>();第二章 安装Java #
目录 Directory #
Windows和Linux使用不同的路径分隔符(file separator)
Windows使用\,如C:\Users\admin Linux使用/,如~/workspace/github
在代码中,Windows系统下的路径常常需要使用\\来表示,前一个字符用于转义
Shell基本操作 #
说来惭愧,里面很多快捷的指令我都不会
# 目录操作
cd <路径>
cd .. 移动到上级目录
pushd <路径> 记住来源的同时移动到其他目录
popd 返回来源
ls 列举出当前目录下所有的文件和子目录名(不包含隐藏文件)
# 可以选择使用通配符 * 来缩小搜索范围。
# 示例(1): 列举所有以“.java”结尾的文件,输入 ls *.java (Windows: dir *.java)
# 示例(2): 列举所有以“F”开头,“.java”结尾的文件,输入ls F*.java (Windows: dir
# F.java)
# 创建目录 Mac/Linux
mkdir
mkdir books
# 创建目录 Windows
md
md books
# 移除文件 Mac/Linux
rm
rm somefile.java
# 移除文件 Windows
del
del somefile.java
# 移除目录 Mac/Linux
rm -r
rm -r books
# 移除目录 Windows
deltree
deltree books
# 重复命令
!! # 重复上条命令
!n # 重复倒数第n条命令
# 命令历史 Mac/Linux
history
# 命令历史 Windows
按 F7 键
# 文件解压
# Linux/Mac 都有命令行解压程序 unzip,你可以通过互联网为 Windows 安装命令行解压程序 unzip。
# 图形界面下(Windows 资源管理器,Mac Finder,Linux Nautilus 或其他等效软件)右键单击该文件,
# 在 Mac 上选择“open”,在 Linux 上选择“extract here”,或在 Windows 上选择“extract all…”。
# 要了解关于 shell 的更多信息,请在维基百科中搜索 Windows shell,Mac/Linux用户可搜索 bash shell。第三章 一切都是对象 #
创建对象 #
String s = new String("asdf");等号左边创建了一个String类型的引用,名为s,等号右边使用new创建了一个新String对象实例。Java内部会将这个对象存储到内存当中。
数据存储 #
程序在运行时主要有5个位置可以储存数据,即寄存器,堆,栈,ROM,以及外部非RAM储存。
- 栈内存(Stack) 存在于常规RAM 区域中,可通过栈指针获得处理器的直接支持。栈指针下移分配内存,上移释放内存,这是一种快速有效的内存分配方法,速度仅次于寄存器。创建程序时,Java 系统必须准确地知道栈内保存的所有项的生命周期。种约束限制了程序的灵活性。因此,虽然在栈内存上存在一些 Java 数据,特别是对象引用,但 Java 对象却是保存在堆内存的。
- 堆内存(Heap) 这是一种通用的内存池(也在 RAM区域),所有 Java 对象都存在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用 new 命令实例化对象即可,当执行代码时,会自动在堆中进行内存分配。这种灵活性是有代价的:分配和清理堆内存要比栈内存需要更多的时间(如果可以用 Java 在栈内存上创建对象,就像在C++ 中那样的话)。随着时间的推移,Java 的堆内存分配机制现在已经非常快,因此这不是一个值得关心的问题了。
堆栈的进一步解释https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap
基本类型的存储 #
对于这些基本类型的创建方法, Java 使用了和 C/C++ 一样的策略。也就是说,不是使用new创建变量,而是使用一个“自动”变量。 这个变量直接存储"值",并置于Stack内存中,因此更加高效。 如果希望在Heap而不是Stack中储存数据,使用基本类型自己对应的包装类即可。
| 基本类型 | 大小 | 最小值 | 最大值 | 包装类型 |
|---|---|---|---|---|
| boolean | — | — | — | Boolean |
| char | 16 bits | Unicode 0 | Unicode 2^16-1 | Character |
| byte | 8 bits | -128 | +127 | Byte |
| short | 16 bits | -2^15 | +2^15 -1 | Short |
| int | 32 bits | -2^31 | +2^31 -1 | Integer |
| long | 64 bits | -2^63 | +2^63 -1 | Long |
| float | 32 bits | IEEE754 | IEEE754 | Float |
| double | 64 bits | IEEE754 | IEEE754 | Double |
| void | — | — | — | Void |
高精度运算 #
Java中使用BigInteger和BigDecimal类来进行高精度运算。 对这两个类的对象计算时需要调用方法,而不是使用运算符。
作用域 #
Java中使用大括号{}来定义变量的作用域,作用域用于规定变量的可调用性和生存周期。
{
int x = 12;
// 仅 x 变量可用
{
int q = 96;
// x 和 q 变量皆可用
}
// 仅 x 变量可用
// 变量 q 不在作用域内
}{
int x = 12;
{
int x = 96; // 非法操作,x已经被初始化了
}
}基本类型的初始值 #
如果类的成员变量(属性)是基本类型,那么在类初始化时,这些类型将会被赋予一个初始值。这些初始值对于程序来说并不一定是合法或者正确的。 所以,为了安全,我们最好始终显式地初始化变量。
| 基本类型 | 初始值 |
|---|---|
| boolean | false |
| char | \u0000 (null) |
| byte | (byte) 0 |
| short | (short) 0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0d |
程序编写 #
命名可见性 #
Java 采取了一种新的方法避免了以上这些问题:为一个类库生成一个明确的名称,Java 创建者希望我们反向使用自己的网络域名,因为域名通常是唯一的。因此我的域名是MindviewInc.com,所以我将我的foibles类库命名为com.mindviewinc.utility.foibles。反转域名后,.用来代表子目录的划分。 这种方式似乎为我们在编写 Java 程序中的某个问题打开了大门。空目录填充了深层次结构,它们不仅用于表示反向 URL,还用于捕获其他信息。这些长路径基本上用于存储有关目录中的内容的数据。如果你希望以最初设计的方式使用目录,这种方法可以从“令人沮丧”到“令人抓狂”,对于生产级的 Java 代码,你必须使用专门为此设计的 IDE 来管理代码。例如 NetBeans,Eclipse 或 IntelliJ IDEA。实际上,这些 IDE 都为我们管理和创建深层次空目录结构。
使用其他组件 #
无论何时在程序中使用预先定义好的类,编译器都必须找到该类。最简单的情况下,该类存在于被调用的源代码文件中。此时我们使用该类 —— 即使该类在文件的后面才会被定义。因此 Java 消除了所谓的“前向引用”问题。
你必须通过使用 import 关键字来告诉 Java 编译器具体要使用的类。import 指示编译器导入一个包,也就是一个类库。大多数时候,我们都在使用 Java 标准库中的组件。有了这些构件,你就不必写一长串的反转域名。例如
import java.util.ArrayList;
import java.util.*;static关键字 #
类是对象的外观及行为方式的描述。通常只有在使用new创建那个类的对象后,数据存储空间才被分配,对象的方法才能供外界调用。这种方式在两种情况下是不足的。
- 有时你只想为特定字段(注:也称为属性、域)分配一个共享存储空间,而不去考虑究竟要创建多少对象,甚至根本就不创建对象。
- 创建一个与此类的任何对象无关的方法。也就是说,即使没有创建对象,也能调用该方法。
当我们说某个事物是静态时,就意味着该字段或方法不依赖于任何特定的对象实例 。 即使我们从未创建过该类的对象,也可以调用其静态方法或访问其静态字段。相反,对于普通的非静态字段和方法,我们必须要先创建一个对象并使用该对象来访问字段或方法,因为非静态字段和方法必须与特定对象关联。 一些面向对象的语言使用类数据(class data)和类方法(class method),表示静态数据和方法只是作为类,而不是类的某个特定对象而存在的。有时 Java 文献也使用这些术语。
# 我们可以在类的属性或方法前添加 static 关键字
# 来表示这是一个静态属性或静态方法。
class StaticTest {
static int i = 47;
}
# 现在,即使你创建了两个 StaticTest 对象,但是静态变量 i 仍只占一份存储空间。两个对象都会共享相同的变量 i。
StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();
# 我们可以通过对象引用静态方法,就像使用任何方法一样,也可以通过特殊的语法方式 Classname.method() 来直接调用静态属性或方法
class Incrementable {
static void increment() {
StaticTest.i++;
}
}
# 当然了,首选的方法是直接通过类来调用它。
Incrementable.increment();实战 #
java.lang隐式包含在每个 Java 代码文件中,因此这些类是自动可用的。 最后,我们开始编写第一个完整的程序。我们使用 Java 标准库中的Date类来展示一个字符串和日期。
// objects/HelloDate.java
import java.util.*;
public class HelloDate {
# 每个 java 源文件中允许有多个类。同时,源文件的名称必须要和其中一个类名相同,否则编译器将会报错。
# 每个独立的程序应该包含一个 main() 方法作为程序运行的入口。其方法签名和返回类型如下。
# main() 方法的参数是一个 字符串(String) 数组。
# 参数 args 并没有在当前的程序中使用到,但是 Java 编译器强制要求必须要有,
# 这是因为它们被用于接收从命令行输入的参数。
public static void main(String[] args) {
System.out.println("Hello, it's: ");
# 上面的示例中,我们创建了一个日期(Date)类型的对象并将其转化为字符串类型,输出到控制台中。
# 一旦这一行语句执行完毕,我们就不再需要该日期对象了。
# 这时,Java 垃圾回收器就可以将其占用的内存回收,我们无需去主动清除它们。
System.out.println(new Date());
}
}查看 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"));
}
}本章小结 #
本章向你展示了简单的 Java 程序编写以及该语言相关的基本概念。到目前为止,我们的示例都只是些简单的顺序执行。在接下来的两章里,我们将会接触到 Java 的一些基本操作符,以及如何去控制程序执行的流程。
运算符 #
赋值中的别名现象 #
基本类型的赋值都是直接的,而不像对象,赋予的只是其内存的引用。举个例子,a = b ,如果 b 是基本类型,那么赋值操作会将 b 的值复制一份给变量 a, 此后若 a 的值发生改变是不会影响到 b 的。作为一名程序员,这应该成为我们的常识。 如果是为对象赋值,那么结果就不一样了。对一个对象进行操作时,我们实际上操作的是它的引用。所以我们将右边的对象赋予给左边时,赋予的只是该对象的引用。此时,两者指向的堆中的对象还是同一个。代码示例:
// operators/Assignment.java
// Assignment with objects is a bit tricky
class Tank {
int level;
}
public class Assignment {
public static void main(String[] args) {
Tank t1 = new Tank();
Tank t2 = new Tank();
t1.level = 9;
t2.level = 47;
System.out.println("1: t1.level: " + t1.level +
", t2.level: " + t2.level);
# t1: 9, t2: 47
t1 = t2;
System.out.println("2: t1.level: " + t1.level +
", t2.level: " + t2.level);
# t1: 47, t2: 47
t1.level = 27;
System.out.println("3: t1.level: " + t1.level +
", t2.level: " + t2.level);
# t1: 27, t2: 27
}
}这是一个简单的 Tank 类,在 main() 方法创建了两个实例对象。 两个对象的level属性分别被赋予不同的值。 然后,t2 的值被赋予给 t1。在许多编程语言里,预期的结果是 t1 和 t2 的值会一直相对独立。但是,在 Java 中,由于赋予的只是对象的引用,改变 t1 也就改变了 t2。 这是因为 t1 和 t2 此时指向的是堆中同一个对象。(t1 原始对象的引用在 t2 赋值给其时丢失,它引用的对象会在垃圾回收时被清理)。
这种现象通常称为别名(aliasing),这是 Java 处理对象的一种基本方式。
递增和递减 #
每种类型的运算符,都有两个版本可供选用;通常将其称为“前缀”和“后缀”。“前递增”表示++运算符位于变量或表达式的前面;而“后递增”表示++运算符位于变量的后面。类似地,“前递减”意味着--运算符位于变量的前面;而“后递减”意味着--运算符位于变量的后面。对于前递增和前递减(如++a 或--a),会先执行递增/减运算,再返回值。而对于后递增和后递减(如a++或a--),会先返回值,再执行递增/减运算。
测试对象等价 #
关系运算符==和!=可用于比较对象
// operators/Equivalence.java
public class Equivalence {
public static void main(String[] args) {
Integer n1 = 47;
Integer n2 = 47;
System.out.println(n1 == n2);
System.out.println(n1 != n2);
}
}表达式System.out.println(n1 == n2)将会输出比较的结果。因为两个 Integer 对象相同,所以先输出 true,再输出 false。但是,尽管对象的内容一样,对象的引用却不一样。==和!=比较的是对象引用,所以输出实际上应该是先输出 false,再输出 true
译者注:如果你把 47 改成 128,那么打印的结果就是这样,因为 Integer 内部维护着一个 IntegerCache 的缓存,默认缓存范围是 [-128, 127],所以 [-128, 127] 之间的值用 == 和 != 比较也能能到正确的结果,但是不推荐用关系运算符比较,具体见 JDK 中的 Integer 类源码。
那么怎么比较两个对象的内容是否相同呢?你必须使用所有对象(不包括基本类型)中都存在的 equals()方法,下面是如何使用equals()方法的示例:
// operators/EqualsMethod.java
public class EqualsMethod {
public static void main(String[] args) {
Integer n1 = 47;
Integer n2 = 47;
System.out.println(n1.equals(n2));
# 输出结果 true
}
}上例的结果看起来是我们所期望的。但其实事情并非那么简单。下面我们来创建自己的类:
// operators/EqualsMethod2.java
// 默认的 equals() 方法没有比较内容
class Value {
int i;
}
public class EqualsMethod2 {
public static void main(String[] args) {
Value v1 = new Value();
Value v2 = new Value();
v1.i = v2.i = 100;
System.out.println(v1.equals(v2));
# 输出结果 false
}
}上例的结果再次令人困惑:结果是 false。原因:equals()的默认行为是比较对象的引用而非具体内容。因此,除非你在新类中覆写equals()方法,否则我们将获取不到想要的结果。不幸的是,在学习 复用(Reuse) 章节后我们才能接触到“覆写”(Override),并且直到 附录:集合主题,才能知道定义equals()方法的正确方式,但是现在明白equals()行为方式也可能为你节省一些时间。
大多数 Java 库类通过覆写equals()方法比较对象的内容而不是其引用。
下划线 #
Java 7 中有一个深思熟虑的补充:我们可以在数字字面量中包含下划线_,以使结果更清晰。这对于大数值的分组特别有用。
// operators/Underscores.java
public class Underscores {
public static void main(String[] args) {
double d = 341_435_936.445_667;
System.out.println(d);
int bin = 0b0010_1111_1010_1111_1010_1111_1010_1111;
System.out.println(Integer.toBinaryString(bin));
System.out.printf("%x%n", bin); // [1]
long hex = 0x7f_e9_b7_aa;
System.out.printf("%x%n", hex);
}
}下面是合理使用的规则:
- 仅限单
_,不能多条相连。 - 数值开头和结尾不允许出现
_。 F、D和L的前后禁止出现_。- 二进制前导 b 和 十六进制 x 前后禁止出现
_。
注意 %n的使用。熟悉 C 风格的程序员可能习惯于看到\n来表示换行符。问题在于它给你的是一个“Unix风格”的换行符。此外,如果我们使用的是 Windows,则必须指定\r\n。这种差异的包袱应该由编程语言来解决。这就是 Java 用%n实现的可以忽略平台间差异而生成适当的换行符,但只有当你使用System.out.printf()或 System.out.format()时。对于System.out.println(),我们仍然必须使用\n;如果你使用%n,println()只会输出%n而不是换行符。
指数计数法 #
在科学与工程学领域,e 代表自然对数的基数,约等于 2.718 (Java 里用一种更精确的 double 值 Math.E 来表示自然对数)。指数表达式 "1.39 x e-43",意味着 “1.39 × 2.718 的 -43 次方”。然而,自 FORTRAN 语言发明后,人们自然而然地觉得e 代表 “10 的几次幂”。这种做法显得颇为古怪,因为 FORTRAN 最初是为科学与工程领域设计的。
理所当然,它的设计者应对这样的混淆概念持谨慎态度。但不管怎样,这种特别的表达方法在 C,C++ 以及现在的 Java 中顽固地保留下来了。所以倘若习惯 e 作为自然对数的基数使用,那么在 Java 中看到类似“1.39e-43f”这样的表达式时,请转换你的思维,从程序设计的角度思考它;它真正的含义是 “1.39 × 10 的 -43 次方”。
// operators/Exponents.java
// "e" 表示 10 的几次幂
public class Exponents {
public static void main(String[] args) {
// 大写 E 和小写 e 的效果相同:
float expFloat = 1.39e-43f;
expFloat = 1.39E-43f;
System.out.println(expFloat);
double expDouble = 47e47d; // 'd' 是可选的
double expDouble2 = 47e47; // 自动转换为 double
System.out.println(expDouble);
}
}类型转换 #
要执行强制转换,需要将所需的数据类型放在任何值左侧的括号内
// operators/Casting.java
public class Casting {
public static void main(String[] args) {
int i = 200;
long lng = (long)i;
lng = i; // 没有必要的类型提升
long lng2 = (long)200;
lng2 = 200;
// 类型收缩
i = (int)lng2; // Cast required
}
}在 Java 里,类型转换则是一种比较安全的操作。但是,若将数据类型进行“向下转换”(Narrowing Conversion)的操作(将容量较大的数据类型转换成容量较小的类型),可能会发生信息丢失的危险。此时,编译器会强迫我们进行转型,好比在提醒我们:该操作可能危险,若你坚持让我这么做,那么对不起,请明确需要转换的类型。 对于“向上转换”(Widening conversion),则不必进行显式的类型转换,因为较大类型的数据肯定能容纳较小类型的数据,不会造成任何信息的丢失。
从 float 和 double 转换为整数值时,小数位将被截断。若你想对结果进行四舍五入,可以使用java.lang.Math 的round()方法
在char,byte和short类型中,我们可以看到算术运算符的“类型转换”效果。我们必须要显式强制类型转换才能将结果重新赋值为原始类型。对于int类型的运算则不用转换,因为默认就是int型。虽然我们不用再停下来思考这一切是否安全,但是两个大的int型整数相乘时,结果有可能超出int型的范围,这种情况下结果会发生溢出。下面的代码示例:
// operators/Overflow.java
// 厉害了!内存溢出
public class Overflow {
public static void main(String[] args) {
int big = Integer.MAX_VALUE;
System.out.println("big = " + big);
int bigger = big * 4;
System.out.println("bigger = " + bigger);
}
}
// 输出结果
// big = 2147483647
// bigger = -4控制流 #
do-while #
do
statement
while(Boolean-expression);while 和 do-while 之间的唯一区别是:即使条件表达式返回结果为 false, do-while 语句也至少会执行一次。 在 while 循环体中,如布尔表达式首次返回的结果就为 false,那么循环体内的语句不会被执行。实际应用中,while 形式比 do-while 更为常用。
for #
for 循环可能是最常用的迭代形式。 该循环在第一次迭代之前执行初始化。随后,它会执行布尔表达式,并在每次迭代结束时,进行某种形式的步进。
for(initialization; Boolean-expression; step)
statement
// control/ListCharacters.java
public class ListCharacters {
public static void main(String[] args) {
for(char c = 0; c < 128; c++)
if(Character.isLowerCase(c))
System.out.println("value: " + (int)c +
" character: " + c);
}
}逗号操作符 #
在 Java 中逗号运算符仅有一种用法:在 for 循环的初始化和步进控制中定义多个变量。我们可以使用逗号分隔多个语句,并按顺序计算这些语句。注意:要求定义的变量类型相同。
// control/CommaOperator.java
public class CommaOperator {
public static void main(String[] args) {
for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) {
System.out.println("i = " + i + " j = " + j);
}
}
}for-in 语法 #
Java 5 引入了更为简洁的“增强版 for 循环”语法来操纵数组和集合。大部分文档也称其为 for-each 语法,但因为了不与 Java 8 新添的 forEach() 产生混淆,因此我称之为 for-in 循环。 for-in 无需你去创建 int 变量和步进来控制循环计数。 下面我们来遍历获取 float 数组中的元素。
// control/ForInFloat.java
import java.util.*;
public class ForInFloat {
public static void main(String[] args) {
Random rand = new Random(47);
float[] f = new float[10];
for(float x : f)
System.out.println(x);
}
}任何一个返回数组的方法都可以使用 for-in 循环语法来遍历元素。 正因如此,除非先创建一个 int 数组,否则我们无法使用 for-in 循环来操作。为简化测试过程,我已在 onjava 包中封装了 Range 类,利用其 range() 方法可自动生成恰当的数组。
// control/ForInInt.java
import static onjava.Range.*;
public class ForInInt {
public static void main(String[] args) {
for(int i : range(10)) // 0..9
System.out.print(i + " ");
System.out.println();
for(int i : range(5, 10)) // 5..9
System.out.print(i + " ");
System.out.println();
for(int i : range(5, 20, 3)) // 5..20 step 3
System.out.print(i + " ");
System.out.println();
for(int i : range(20, 5, -3)) // Count down
System.out.print(i + " ");
System.out.println();
}
}
// 0 1 2 3 4 5 6 7 8 9
// 5 6 7 8 9
// 5 8 11 14 17
// 20 17 14 11 8break和continue #
在任何迭代语句的主体内,都可以使用 break 和 continue 来控制循环的流程。 其中,break 表示跳出当前循环体。而 continue 表示停止本次循环,开始下一次循环。
如果没有 break outer 语句,就没有办法在一个内部循环里找到出外部循环的路径。这是由于 break 本身只能中断最内层的循环(对于 continue 同样如此)。 当然,若想在中断循环的同时退出方法,简单地用一个 return 即可。 下面这个例子向大家展示了带标签的 break 以及 continue 语句在 while 循环中的用法
// control/LabeledWhile.java
// 带标签的 break 和 conitue 在 while 循环中的使用
public class LabeledWhile {
public static void main(String[] args) {
int i = 0;
outer:
while(true) {
System.out.println("Outer while loop");
while(true) {
i++;
System.out.println("i = " + i);
if(i == 1) {
System.out.println("continue");
continue;
# 移动到第11行开始执行
}
if(i == 3) {
System.out.println("continue outer");
continue outer;
# 移动到第9行开始执行
}
if(i == 5) {
System.out.println("break");
break;
# 离开第11行的while循环,移动到第9行开始执行
}
if(i == 7) {
System.out.println("break outer");
break outer;
# 离开第9行的while循环
}
}
}
}
}
/*Outer while loop
i = 1
continue
i = 2
i = 3
continue outer
Outer while loop
i = 4
i = 5
break
Outer while loop
i = 6
i = 7
break outer*/同样的规则亦适用于 while:
- 简单的一个
continue会退回最内层循环的开头(顶部),并继续执行。
带有标签的
continue会到达标签的位置,并重新进入紧接在那个标签后面的循环。break会中断当前循环,并移离当前标签的末尾。带标签的
break会中断当前循环,并移离由那个标签指示的循环的末尾。
大家要记住的重点是:在 Java 里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中 break 或 continue。
break 和 continue 标签在编码中的使用频率相对较低 (此前的语言中很少使用或没有先例),所以我们很少在代码里看到它们。
在 Dijkstra 的 “Goto 有害” 论文中,他最反对的就是标签,而非 goto。他观察到 BUG 的数量似乎随着程序中标签的数量而增加[^2]。标签和 goto 使得程序难以分析。但是,Java 标签不会造成这方面的问题,因为它们的应用场景受到限制,无法用于以临时方式传输控制。由此也引出了一个有趣的情形:对语言能力的限制,反而使它这项特性更加有价值。
switch #
switch 有时也被划归为一种选择语句。根据整数表达式的值,switch 语句可以从一系列代码中选出一段去执行。Java 7 增加了在字符串上 switch 的用法。
switch(integral-selector) {
case integral-value1 : statement; break;
case integral-value2 : statement; break;
case integral-value3 : statement; break;
case integral-value4 : statement; break;
case integral-value5 : statement; break;
// ...
default: statement;
}在上面的定义中,大家会注意到每个 case 均以一个 break 结尾。这样可使执行流程跳转至 switch 主体的末尾。这是构建 switch 语句的一种传统方式,但 break 是可选的。若省略 break, 会继续执行后面的 case 语句的代码,直到遇到一个 break 为止。通常我们不想出现这种情况,但对有经验的程序员来说,也许能够善加利用。注意最后的 default 语句没有 break,因为执行流程已到了 break 的跳转目的地。 下面是一个可能提供答案的测试程序。 所有命令行参数都作为 String 对象传递,因此我们可以 switch 参数来决定要做什么。 那么问题来了:如果用户不提供参数 ,索引到 args 的数组就会导致程序失败。 解决这个问题,我们需要预先检查数组的长度,若长度为 0,则使用空字符串 "" 替代;否则,选择 args 数组中的第一个元素。 Math.random() 的结果集范围包含 0.0 ,不包含 1.0。 在数学术语中,可用 [0,1)来表示。