第 1 章 编程之道

第 1 章 编程之道

本书旨在教你像计算机科学家那样思考。这种思维方式兼具数学、工程和自然科学的优点:计算机科学家像数学家那样使用规范的语言来描绘概念,具体地说就是计算;像工程师那样设计,将各个部分组装成系统并权衡不同的解决方案;像科学家那样观察复杂系统的行为,进而作出假设并进行验证。

对计算机科学家来说,最重要的技能是解决问题(problem solving)。这包括系统地阐述问题、创造性地提出解决方案,以及清晰而准确地描述解决方案。实践表明,学习编程为获得解决问题的技能提供了极佳的机会,这正是本章名为“编程之道”的原因所在。

一方面,你将学习编程,这本身就是一项很有用的技能;另一方面,你将把编程作为达到目的的手段。随着不断往下阅读,目的将变得更加清晰。

1.1 何为编程

程序(program)由一系列指令组成,指定了如何执行计算。这里的计算可能是数学计算,如求解方程组或找出多项式的根,也可能是符号计算,如在文档中搜索并替换文本或编译程序(真够奇怪的,编译程序竟然也是计算)。虽然细节因语言而异,但几乎所有语言都支持一些基本指令。

  • 输入

    从键盘、文件、传感器或其他设备获取数据。

  • 输出

    在屏幕上显示数据,或者将数据发送给文件或其他设备。

  • 数学运算

    执行基本的数学运算,如加法和除法。

  • 决策

    检查特定的条件,并根据检查结果执行相应的代码。

  • 重复

    反复执行某种操作,但通常每次执行时都略有不同。

信不信由你,这几乎就是程序的全部内容。你使用的每个程序都由类似于上面的小指令组成,不管它有多复杂。因此,你可将编程(programming)视为这样的过程,即将复杂而庞大的任务分解为较小的子任务。不断重复这个过程,直到分解得到的子任务足够简单,用计算机提供的基本指令就能完成。

1.2 何为计算机科学

对编程而言,最有趣的一个方面是决定如何解决特定的问题,尤其是问题存在多种解决方案时。例如,对数字列表进行排序的方法很多,其中每种方法都有其优点。要确定哪种方法是特定情况下的最佳方法,你必须具备规范地描述和分析解决方案的技能。

计算机科学(computer science)就是算法科学,包括找出算法并对其进行分析。算法(algorithm)由一系列指定如何解决问题的步骤组成。有些算法的速度比其他算法快,有些使用的计算机内存更少。面对以前没有解决过的问题,你在学着找出算法的同时,也将学习如何像计算机科学家那样思考。

设计算法并编写代码很难,也很容易出错。由于历史的原因,编程错误被称为 bug,而找出并消除编程错误的过程被称为调试(debugging)。通过学习调试程序,你将获得解决新问题的技能。面临出乎意料的错误时,需要创造性思维。

虽然调试可能令人沮丧,但它是计算机编程中有趣且挑战智商的部分。从某种程度上来说,调试犹如侦破工作:必须根据掌握的线索猜想出引发结果的过程和事件。在有些情况下,在考虑如何修复程序以及改善其性能的过程中,还能发现新的算法。

1.3 编程语言

本书要介绍的编程语言是 Java,这是一种高级语言(high-level language)。你可能还听说过其他高级语言,如 Python、C、C++、Ruby 和 JavaScript。

要想运行用高级语言编写的程序,必须将其转换为低级语言(low-level language),即“机器语言”。这种转换需要一定的时间,这是高级语言的一个小小的缺点,但高级语言有两个优点。

  • 用高级语言编程容易得多:编写程序所需要的时间更短,程序更简洁、更容易理解,同时更容易确保程序正确无误。

  • 高级语言是可移植的(portable),这意味着用高级语言编写的程序只需做少量修改甚至无需修改,就可在不同类型的计算机上运行。用低级语言编写的程序只能在一种计算机上运行,这种程序必须重写才能在其他计算机上运行。

有两种将高级语言转换为低级语言的程序:解释器和编译器。解释器(interpreter)读取并执行用高级语言编写的程序,这意味着程序怎么说它就怎么做。它每次处理程序的一小部分,即交替地读取代码行并执行计算。图 1-1 展示了解释器的结构。

{%}

图 1-1:解释型语言是如何执行的

相反,编译器(compiler)读取并转换整个程序,然后才开始运行程序。在这种情况下,用高级语言编写的程序称为源代码(source code),而转换得到的程序称为目标代码(object code)或可执行程序(executable)。程序编译后可反复执行,无需在每次执行前都进行转换。因此,编译型程序的运行速度通常比解释型程序更快。

Java 既是解释型的又是编译型的。Java 编译器不将程序直接转换为机器语言,而是生成字节码(byte code)。字节码类似于机器语言,解释起来既轻松又快捷,同时也是可移植的,即可在一台机器上编译程序,在另一台机器上运行生成的字节码。运行字节码的解释器被称为 Java 虚拟机(Java Virtual Machine,JVM)。

{%}

图 1-2:Java 程序的编译和运行过程

图 1-2 展示了这个过程包含的步骤。这个过程看似复杂,但在大多数程序开发环境中,这些步骤都是自动完成的:通常只需按一下按钮或输入简单的命令,就能编译并运行程序。然而,知道幕后执行了哪些步骤很重要,这样就可以在出现问题时找出问题所在。

1.4 Hello World程序

传统上,学习一门新的编程语言时,通常先编写一个名为 Hello World 的程序,它所做的只是在屏幕上显示“Hello, World!”。用 Java 编写时,这个程序与下面的类似:

public class Hello {

    public static void main(String[] args) {
        // 生成一些简单的输出
        System.out.println("Hello, World!");
    }
}

这个程序运行时显示如下内容:

Hello, World!

注意,输出中没有引号。

Java 程序由定义和方法定义组成,而其中的方法由语句(statement)组成。语句是一行执行基本操作的代码。在 Hello World 程序中,这是一条打印语句(print statement),在屏幕上显示一条消息:

System.out.println("Hello, World!");

System.out.println 在屏幕上显示结果,其中的 println 表示“打印一行”。令人迷惑的是,打印既可以表示“在屏幕上显示”,也可以表示“发送到打印机”。在本书中,表示输出到屏幕上时,我们尽可能说“显示”。与大多数语句一样,打印语句也以分号(;)结尾。

Java 是区分大小写的,这意味着大写和小写是不同的。在前面的示例中,System 的首字母必须大写,使用 systemSYSTEM 都行不通。

方法(method)是一系列命名的语句。前面的程序定义了一个名为 main 的方法:

public static void main(String[] args)

方法 main 比较特殊:程序运行时,首先执行方法 main 中的第一条语句,并在执行完这个方法的最后一条语句后结束。在本书的后文中,你将看到定义了多个方法的程序。

(class)是方法的集合。前面的程序定义了一个名为 Hello 的类。你可以随便给类命名,但根据约定,类名的首字母应大写。类必须与其所属的文件同名,因此前面的类必须存储在文件 Hello.java 中。

Java 用大括号({})编组。在 Hello.java 中,外面的大括号包含类定义,而里面的大括号包含方法定义。

以双斜线(//)开头的行是注释(comment),它用自然语言编写的文本解释代码。编译器遇到 // 时,将忽略随后到行尾的所有内容。注释对程序的执行没有任何影响,但可以让其他程序员(还有未来的你自己)更容易地明白你要做什么。

1.5 显示字符串

方法 main 中可包含任意条语句。例如,要显示多行输出,你可以像下面这样做:

public class Hello {

    public static void main(String[] args) {
        // 生成一些简单的输出
        System.out.println("Hello, World!"); // 第一行
        System.out.println("How are you?");  // 第二行
    }
}

这个示例表明,除独占一行的注释外,还可在行尾添加注释。

用引号括起的内容称为字符串(string),因为它们包含一系列串在一起的字符。字符包括字母、数字、标点、符号、空格、制表符,等等。

System.out.println 在指定的字符串末尾添加了一个特殊字符——换行符(newline),其作用是移到下一行开头。如果你不想在末尾添加换行符,可用 print 代替 println

public class Goodbye {

    public static void main(String[] args) {
        System.out.print("Goodbye, ");
        System.out.println("cruel world");
    }
}

在这个示例中,第一条语句没有添加换行符,因此输出只有一行:Goodbye, cruel world。请注意,第一个字符串末尾有一个空格,这也包含在输出中。

1.6 转义序列

可用一行代码显示多行输出,只需告诉 Java 在哪里换行就可以了:

public class Hello {

    public static void main(String[] args) {
        System.out.print("Hello!\nHow are you doing?\n");
    }
}

上述代码的输出为两行,每行都以换行符结尾:

Hello!
How are you doing?

\n 是一个转义序列(escape sequence),表示特殊字符的字符序列。反斜线让你能够对字符串的内容进行转义。请注意,\nHow 之间没有空格;如果在这里添加一个空格,第二行输出的开头将会是一个空格。

转义序列的另一个常见用途是在字符串中包含引号。由于双引号标识字符串的开头和结尾,因此,要想在字符串中包含双引号,必须用反斜线对其进行转义。

System.out.println("She said \"Hello!\" to me.");

结果如下:

She said "Hello!" to me.

表1-1:常见的转义序列

\n换行符
\t制表符
\"双引号
\\反斜线

1.7 设置代码格式

在 Java 程序中,有些空格是必不可少的。例如,不同的单词之间至少得有一个空格,因此下面的程序是不合法的:

publicclassGoodbye{

    publicstaticvoidmain(String[] args) {
        System.out.print("Goodbye, ");
        System.out.println("cruel world");
    }
}

但其他空格大都是可有可无的。例如,下面的程序完全合法:

public class Goodbye {
public static void main(String[] args) {
System.out.print("Goodbye, ");
System.out.println("cruel world");
}
}

换行也是可选的,因此可将前面的代码编写如下:

public class Goodbye { public static void main(String[] args)
{ System.out.print("Goodbye, "); System.out.println
("cruel world");}}

这也是可行的,但程序阅读起来更难了。要以直观的方式组织程序,换行和空格都很重要,使用它们可让程序更容易理解,发生错误时也更容易查找。

很多编辑器都自动设置源代码的格式:以一致的方式缩进和换行。例如,在 DrJava(参见附录 A)中,可以按 Ctrl+A 选择所有的代码,再按 Tab 键来缩进代码。

从事大量软件开发工作的组织通常会制定严格的源代码格式设置指南,例如,Google 就发布了针对开源项目的 Java 编码标准,其网址为 http://google.github.io/styleguide/javaguide.html

这些指南提及了本书还未介绍的 Java 功能,因此你现在可能看不懂,但在阅读本书的过程中,你可能时不时地想要回过头来阅读它们。

1.8 调试代码

最好能在计算机前阅读本书,因为这样你就可以一边阅读一边尝试其中的示例。本书中的很多示例可直接在 DrJava 的 Interactions 窗格(见附录 A)中运行,但如果将代码存储到源代码文件中,则更容易对其修改再运行。

每当你使用新功能时,都应该尝试故意犯些错误。例如,在 Hello World 程序中,如果遗漏一个引号,结果将如何呢?如果两个引号都遗漏了,结果将如何呢?如果 println 拼写得不正确,结果又将如何呢?这些尝试不仅有助于牢记学过的知识,还有助于调试程序,因为你将知道各种错误消息意味着什么。现在故意犯错胜过以后无意间犯错。

调试犹如实验科学:一旦对出问题的地方有所感觉,就修改程序并再次运行。如果假设没错,你就能预测修改后的结果,从而离程序正确运行更近一步;如果假设有误,你就必须作出新的假设。

编程和调试必须齐头并进。不能先随便编写大量的代码,再通过反复调试来确保它们能够正确地运行;相反,应先编写少量可正确运行的代码,再逐步修改和调试,最终得到一个提供所需功能的程序。这样的方式可以确保在任何时候都有可运行的程序,从而更容易隔离错误。

Linux 操作系统淋漓尽致地展示了这种原则。这个操作系统现在包含数百万行的代码,但最初只是一个简单的程序,Linus Torvalds 用它来研究 Intel 80386 芯片。正如 Larry Greenfield 在 Linxu User's Guide 中指出的,Linux 是 Linus Torvalds 早期开发的项目之一,最初只是一个决定打印 AAAA 还是 BBBB 的程序,后来才演变为 Linux。

最后,编程可能引发强烈的情绪。面对棘手的 bug 而束手无策时,你可能会感到愤怒、沮丧或窘迫。别忘了,并非只有你这样,大多数乃至所有程序员都有类似的经历;不要犹豫,赶快向朋友求助吧!

1.9 术语表

对于每个术语,本书都尽可能在首次用到时作出定义。同时,我们会在每章末尾按出现顺序列出涉及的新术语及其定义。如果你花点时间研究以下术语表,后面的内容阅读起来将更加轻松。

  • 解决问题

    明确地描述问题、找到并描述解决方案的过程。

  • 程序

    一系列的指令,指定了如何在计算机上执行任务。

  • 编程

    用问题解决技能创建可执行的计算机程序。

  • 计算机科学

    科学而实用的计算方法及其应用。

  • 算法

    解决问题的流程或公式,可以涉及计算机,也可以不涉及。

  • bug

    程序中的错误。

  • 调试

    找出并消除错误的过程。

  • 高级语言

    人类能够轻松读写的编程语言。

  • 低级语言

    计算机能够轻松运行的编程语言,也叫“机器语言”或“汇编语言”。

  • 可移植

    程序能够在多种计算机上运行。

  • 解释

    指运行用高级语言编写的程序,即每次转换其中的一行并立即执行转换得到的指令。

  • 编译

    将用高级语言编写的程序一次性转换为低级语言,供以后执行。

  • 源代码

    用高级语言编写的、未编译的程序。

  • 目标代码

    编译器转换程序后得到的输出。

  • 可执行代码

    可在特定硬件上执行的目标代码的别名。

  • 字节码

    Java 程序使用的一种特殊目标代码,类似于低级语言,但像高级语言一样是可移植的。

  • 语句

    程序的一部分,指定了算法中的一个步骤。

  • 打印语句

    将输出显示到屏幕上的语句。

  • 方法

    命名的语句序列。

  • 就目前而言,指的是一系列相关的方法。(后面你将看到,类并非只包含方法。)

  • 注释

    程序的一部分,包含有关程序的信息,但对程序的运行没有任何影响。

  • 字符串

    一系列字符,是一种基本的文本数据类型。

  • 换行符

    标识文本行尾的特殊字符。

  • 转义序列

    在字符串中用于表示特殊字母的编码序列。

1.10 练习

每章末尾都有练习,只需要利用在该章学到的知识就能完成。强烈建议你尝试完成每个练习,光阅读是学不会编程的,得实践才行。

要想编译并运行 Java 程序,需要下载并安装一些工具。这样的工具很多,但我们推荐 DrJava——一个非常适合初学者使用的“集成开发环境”。有关如何安装 DrJava,请参阅附录 A 的 A.1 节。

本章的示例代码位于仓库 ThinkJavaCode 的目录 ch01 中,有关如何下载这个仓库,请参阅前言中的“使用示例代码”一节。做以下的练习前,建议你先编译并运行本章的示例。

练习1-1

计算机科学家有个毛病,喜欢赋予常见的英语单词以新的义项。例如,在英语中 statement 和 comment 是同义词,但在程序中它们的含义不同。

(1) 在计算机行话中,语句和注释有何不同?

(2) 说程序是可移植的是什么意思?

(3) 在普通英语中,单词 compile 是什么意思?

(4) 何为可执行程序(executable)?为何这个单词被用作名词?

每章末尾的术语表旨在突出计算机科学中有特殊含义的单词和短语。看到熟悉的单词时,千万不要理所应当地认为你知道它们的含义!

练习1-2

接着往下读之前,请先搞清楚如何编译和运行 Java 程序。有些编程环境提供了类似于本章 Hello World 程序的示例程序。

(1) 输入 Hello World 程序的代码,再编辑并运行它。

(2) 添加一条打印语句,在“Hello, World!”后面再显示一条诙谐的消息,如“How are you?”,然后再编译并运行这个程序。

(3) 在这个程序中添加一条注释(什么地方都可以),再编译并运行它。新添的注释应该对结果没有任何影响。

这个练习看似微不足道,却为编写程序打下了坚实的基础。要想得心应手地调试程序,必须熟悉编程环境。

在一些编程环境中,一不小心就不知道当前执行的是哪个程序了。你可能想调试某个程序,却不小心运行了另一个程序。为确保你看到的就是要运行的程序,一种简单的方法是添加并修改打印语句。

练习1-3

将能想到的错误都犯一次是个不错的注意,这样你就知道编译器都会显示哪些错误消息了。在有些情况下,编译器会准确地指出错误,你只需要修复指出的错误即可;但有时候,错误消息会将你引入歧途。调试多了就会培养出感觉,知道什么情况下该信任编译器,什么情况下只能自力更生。

请在本章的 Hello World 程序中尝试下面每一种错误。每次修改后编译程序并阅读出现的错误消息,然后再修复错误。

(1) 删除其中的一个左大括号。

(2) 删除其中的一个右大括号。

(3) 将方法名 main 改为 mian

(4) 删除单词 static

(5) 删除单词 public

(6) 删除单词 System

(7) 将 println 改为 Println

(8) 将 println 替换为 print

(9) 删除其中的一个括号;添加一个括号。

目录