1.2 编程:从符号到程序
我们说话的方式有很多种,能够表达出的意思也远超词汇本身。人类能够用名词、动词、形容词等词语准确表达某一时刻的感受或者某个动作。但与之相比,机器却没有办法像人一样理解一些比较复杂的指令与说法。
机器语言的词汇有限,它所支持的表达方式是有严格定义的,而且定义得很具体,这些方式与人类的语言相比,显得较为单纯。机器语言的目标是准确地表达意图,换句话说,机器语言就是为表达意图而设计的。这与人类的语言不同,我们讲话可能只是为了交流,而且不用像机器语言那样把每个细节都说得很具体。
机器的意图(或者说,你想让机器去做的事情)可以用一条或一组具备明确定义的指令来表达。这意味着,机器是能够理解指令的。然而,机器在执行指令时,必须能以某种形式拿到这样的指令。每种机器通常都有它自己的一套指令。你可以从这套指令(或者说指令集)里面选择一些传给机器,让它去执行,机器执行指令的流程如图1.1所示。
图1.1 CPU内部的指令循环简图(CPU是从内存获取指令的,指令的执行结果也存储到内存中)
现在我们来探讨单个指令。指令,可以理解成发布给处理器的一条命令。处理器是计算机的核心,或者说,是计算机排列并执行指令时不可缺少的中心组件。一台计算机可能只有一个处理器,也可能有多个处理器。究竟是哪种情况,要看计算机的设计。但无论如何,对于某一条具体的指令来说,总是会有一个处理器来处理。为了讲得简单一些,我们假设系统中只有一个用来执行程序的中央处理器(Central Processing Unit,CPU)。
CPU是一种用来执行指令的设备,而计算机程序实际上就是由一些指令组成的。每一种CPU都必须有它的指令集,图1.1里的指令必须是这套指令集中的指令,只有这样,CPU才知道怎么处理。
各种CPU所使用的指令在形式上可能区别很大,这没有统一的标准。于是,我们会看到许多CPU平台,这不一定是坏事,因为这样可以促进CPU的发展。然而问题在于,无论哪一种形式的指令,人类解读起来都不是特别容易。
我们刚才说了,计算机能够执行指令集中的指令,而且理想情况下是连续不断地执行。可以简单地把这样的一条指令流(flow of instruction)想象成内存中的一个队列(queue),每次都有一条指令进来,同时也有一条指令离开。离开的这条指令就是排在队列最前面的指令,它会被CPU获取并得到处理。CPU相当于解释器(interpreter),它反复从内存中获取指令,如图1.1所示。CPU负责解释指令,可是内存中等着由它来解释的这些指令来自何处?它们又是怎么汇聚成一条指令流的呢?
仔细想想,其实计算机指令在大多数情况下都是由编译器(compiler)产生的。
什么是编译器?编译器可以看成一种针对特定CPU或特定平台的程序,它把文本转换成能够在这种CPU或平台上执行的操作。这里的文本就是我们自己编写的源代码,这些源代码会由编译器转换为机器码(machine code),以便在特定的平台执行。这个流程如图1.2所示。
图1.2 源代码经由编译器处理变为能够在特定的目标平台上执行的机器码
机器码是一种能够为计算机所理解的底层语言(low-level language),这种语言中的指令可以由CPU一条一条地处理(参见图1.1)。编译器就是要把你所写的源代码编译为由机器码所组成的程序,使得这个程序能够在特定的平台上运行。
我们在用Java语言编程的时候,没有机器码这个概念。
Java源代码由Java编译器编译为字节码(bytecode)。字节码是运行在Java虚拟机(Java Virtual Machine,JVM)上的(参见图1.3)。Java虚拟机是字节码与目标平台(或者说,目标CPU)之间的一个接口,它会把字节码转换成能够在这种CPU上执行的指令。这样的转换由JIT编译器(Just-In-Time compiler)来完成,它是JVM的一部分。JIT编译器把字节码转换成能够在具体的处理器上执行的指令。JVM本身能够针对特定平台来解释字节码,它的地位相当于图1.2中的机器码。此外,JVM还有其他一些特性,例如,内存管理与垃圾收集等,这些特性使得Java成为一个强大的开发平台。前面说的种种特性,让开发者只需要把自己写的Java代码编译一次就好,编译而成的字节码能够在Java虚拟机所支持的任何一种平台上运行,这就是一次编写,到处运行(Write Once,Run Anywhere,WORA)。
图1.3 用Java语言写成的源代码,经由Java编译器处理,变为不针对特定目标平台的字节码,Java虚拟机负责在特定的目标平台上执行这些字节码
根据刚才讲的内容,Java应该是一种高级语言(high-level language),它经由Java虚拟机转化为底层的(也就是低级的)机器码。Java对计算机的细节做了大幅度的抽象,使得开发者能够用简单的代码实现出复杂的功能,并确保这些功能可以在多种计算机上运作。
目前,我们讨论了通用的解决方案。接下来,我们将会从内存的角度谈谈如何在保证代码易于维护、易于扩展的前提下,降低它的内存占用量。最后我们将讨论各种设计模式,让这些模式帮助我们在日常工作中写出清晰易懂而且更有意思的代码。