第一部分 Java 精 髓1991年,Sun Microsystems公司开始研究一种新的计算机语言,这种语言最后撼动了传统编程的基础。起初,这种语言被命名为Oak,到1995年正式命名为Java。Java在两个方面改变了编程的过程。第一,Java集成了有利于编制Internet程序的特性。第二,Java发展了计算机语言的精髓。因此,Java的重要性体现在两点:对Internet的内嵌支持和对计算机语言发展的推动。这两点中的任何一点都足以使Java成为一种出色的语言;但是只有将这两点成功地结合起来,Java才能成为一种伟大的语言,才能确定它在计算机历史中的地位。
简单数据类型和对象:完美的平衡
设计一种面向对象语言所面临的最大挑战,就是如何平衡对象和简单数据类型之间的抉择。从纯理论的观点来看,每种数据类型都应该是一个对象,并且都应该从一个共同的父对象派生而来。这就使得所有数据类型以相同的基本模式运作,共享一个公共的基类属性集合。现在的问题在于,如果将简单数据类型(如int和double)作为对象处理,那么对象机制所引起的额外开销会导致性能(performace)的下降。由于简单数据类型通常用于循环控制和条件语句,所以这些额外开销将带来广泛的负面影响。诀窍就是如何在“一切都是对象”的理想和“性能衡量”的现实之间找到正确的平衡点。
Java非常巧妙地解决了对象与简单数据类型之间的问题。首先,Java定义了8种简单类型:byte、short、int、long、char、float、double和boolean。这些类型能够直接转换为二进制代码。因此,CPU可以直接处理int类型的变量,而无需任何额外开销。在Java中,处理简单数据类型和其他语言一样快速高效。因此,由int型变量所控制的for循环可以高速运行,而不受任何对象化所带来的负面影响。
除了这些简单数据类型,Java中的其他数据类型都从一个共同超类(Object类)派生而来。因此,所有这些数据类型都共享从父类继承而来的方法和属性集。例如,所有对象都有toString()方法,因为toString()是父类Object中定义的方法。
由于简单数据类型不是对象,因此Java可自由地以略有不同的方式处理对象和非对象。这就是Java的真正精髓所在。在Java中,所有对象都通过引用访问,而非直接访问;只有简单数据类型才可以直接访问。因此,Java程序绝对不会直接操作一个对象。这种策略可以带来很多好处,最直接的好处就是能够高效地实现垃圾回收。因为所有对象都通过引用访问:当一个对象没有被引用时,它将被回收。另一个好处是,每个Object类型的指针可以引用系统中的任何对象。
当然,通过引用访问对象将产生额外开销。因为一个引用实际上是一个地址(即指针)。于是对每个对象的访问不是直接进行的,而是通过地址间接完成的。尽管现代CPU可以高效地处理间接访问,但是间接访问总是不如直接处理数据本身快,简单数据类型即是通过直接方式进行的。
尽管简单数据类型的处理非常高效,但是有些时候仍然需要使用跟某个简单类型等价的对象。例如在运行时创建一个整型链表,并在不再使用时将其回收(垃圾回收)。为了处理此类情况,Java为简单类型(如Integer和Double)定义了包装器(wrapper)。包装器使得简单类型在必要时可参与到对象层次的操作中来。Java关于对象和简单数据类型的平衡问题。它支持编写高效程序,同时又完美地解决了允许实现对象模型,而不用担心对简单数据类型的性能会产生负面影响。 通过垃圾回收实现内存管理 垃圾回收作为一种内存管理技术已经存在了很长时间,但是Java使它焕发出崭新的活力。在C++等语言中,内存必须人工管理,程序员必须显式地释放不再使用的对象。这是问题产生的根源,因为忘记释放不再使用的资源,或者释放了正在使用的资源都是很常见的事情。Java代替程序员完成了这些工作,从而防止了此类问题的发生。在Java中,所有的对象都是通过引用访问的,这样,当垃圾回收器发现一个没有引用的对象时,就知道该对象已经不被使用,并且可以回收了。如果Java允许对象的直接访问(与简单数据类型的访问方式类似),那么这种有效的垃圾回收方法将无法实现。
Java的垃圾回收策略在普遍意义上反映了Java的理念。Java设计人员花费了大量的精力,来防止其他编程语言经常出现的典型问题,例如程序员经常忘记释放资源,或者错误地释放正在使用的资源。因此,使用垃圾回收策略有效地避免了此类问题的发生。 完美的简单多线程模型
Java的设计者所提供的编程特性,包括对多线程多任务的语言级支持。多任务具有两种类型:基于进程的多任务和基于线程的多任务。在基于进程的多任务中,最小的可调度单元是进程。进程实际上就是正在执行的一个程序。因此,基于进程的多任务就是允许计算机同时运行两个或多个程序的特性。在基于线程的多任务中,最小的可调度单元是线程。线程定义了一个程序内的某条执行路径。因此,一个进程可以包含有两个或更多的执行线程,而多线程程序可以有两个或更多个可以并发执行的部分。
尽管基于进程的多任务通常是操作系统提供的功能,基于线程的多任务却可以极大地受益于程序设计语言级别的支持。例如,C++没有对多线程编程提供内置支持,于是就必须依赖于操作系统来处理多线程任务。这就意味着创建、启动、同步和结束线程都必须通过对操作系统的多次调用来实现。因此C++中的多线程代码是不可移植的。这也使得C++编程中的多线程没有得以广泛应用。
由于Java内置了对多线程的支持,哪些在其他语言中必须由手工完成的工作,现在都可以由Java自动处理。Java多线程模型中最有特色的部分之一,就是其实现同步的方式。同步建立在两种创新的特性之上。首先,在Java中,所有的对象都有充当互斥锁的内置侦听器。在给定的时刻,任何侦听器只能由一个线程拥有。通过使用synchronized关键字对方法进行修饰,可以启用锁定特性。在调用同步方法时,该对象就被锁定,而试图访问该对象的其他线程就只能等待。其次,Java对同步的支持也可以从所有类的共同超类Object中体现。Object声明了下面三个同步方法:wait()、notify()和notifyAll()。这些方法支持线程间通信。因此,所有的对象都对线程间通信有内置的支持。通过和同步方法结合使用,这些方法可以为线程的互操作提供高级别的控制。
通过将多线程作为语言的一个易于使用的内置特性,Java改变了人们关于程序基本体系结构的观念。在Java出现之前,大部分程序员将程序看成是,只有一条执行路径的单块结构。在Java出现之后,程序演变为多个可互操作的并发任务的集合。这种并发机制的改变对于计算产生了深远的影响,但是其最重大的影响可能是使得软件组件的使用更为便捷。
完全集成的异常机制 异常(exception)的概念性框架出现在Java产生之前。因此,在Java出现之前,其他编程语言就已经提出了异常的概念。例如C++提出异常概念的时间,就比Java诞生的时间早了许多年。异常在Java的最初设计中就已经引入了,而不是在Java产生之后才加入的,因此Java对于异常机制的实现就显得非常重要。异常机制是在Java语言中完全集成的,是Java的基本特征之一。
Java异常机制的关键在于异常是必须使用的,而不是可选的。通过异常处理错误是Java语言的规则,这与C++是有区别的。例如,C++同样支持异常机制,但是这种机制并未完全集成到整个编程环境中。考虑打开或者读取一个文件的操作。在Java中,如果某个操作发生错误,将抛出一个异常。而在C++中,打开或者读取文件的方法会返回一个专用错误代码,报告操作中发生的错误。因为C++库仍然依赖于错误返回代码而不是异常,所以从本质上来说,C++并不支持异常,程序必须不断地进行人工检查,以避免可能出现的错误。但是在Java中,程序员只需要简单地使用一个try/catch代码块,就可以自动捕捉到任何错误。
对多态性支持的改进
多态性(polymorphism)是面向对象编程的属性,它允许多个方法使用同一个接口。Java从多个方面支持多态性,其中两个方面最为突出。第一个是每个方法(标记为 final的方法除外)都可以被子类重写;第二个是设立interface关键字。下面将给出这两方面的详细介绍。
由于超类中的方法可以在派生类中重写,因此创建类的层次结构非常简单。在类的层次结构中,每个子类都将它的超类特化(specialization)。大家知道,超类的一个引用可以引用它的任何一个子类,而且通过超类的引用调用某子类对象的一个方法时,会自动执行由该子类重写后的版本。因此,可以用超类来定义对象的形式并提供对象的默认实现,而子类根据这种默认实现进行修改,以更好地适应具体情况的要求。因此,在超类中定义的一个接口可以作为多个不同实现的基础。
当然,Java进一步采取了“一个接口,多个方法”的概念。它定义了interface关键字,这样就可以将类的方法和类的实现完全分离。尽管接口是抽象的,但是仍然可以声明接口类型的引用。这个概念非常重要,因为它可以改进多态性的应用。只要某个类实现一个接口,并且该接口提供了某种功能,那么任何需要这种功能的代码都可以使用这个类的对象。例如,假设某个接口的名称为MyIF,考虑下面的方法:
void myMeth(MyIF ob) {
// ...
}
任何实现了MyIF接口的对象都可以传递给myMeth()方法。该对象的其他功能无需考虑。myMeth()方法可以对任何实现了MyIF接口的对象进行操作。 通过字节码保证可移植性和安全性
通过字节码保证可移植性和安全性 尽管Java有许多功能强大的特性,但是如果没有字节码(byte code)这一特征,那么Java只不过是编程技术发展进程中的一个足印。字节码是Java语言的一个重要组成部分,它对程序员而言,几乎是透明的。所有的Java程序员都知道,Java编译器的输出不是能够直接由CPU执行的机器指令,而是一种经过高度优化的可移植的指令集合,这种指令集合称为字节码,它只能由Java虚拟机(Java Virtual Machine,JVM)执行。最初,JVM只是一个简单的字节码解释器,现在,JVM也将字节码的on-the-fly编译技术应用到可执行代码中。无论字节码的执行采用何种方式,它的优势对于Java的成功都是至关重要的。
字节码的第一个优势是可移植性。无论计算机使用何种类型的CPU(或操作系统),只要具有JVM,那么由Java程序编译而成的字节码就可以在其中执行。换而言之,只要为某个特定环境实现了JVM,那么每个Java程序都可以在该环境运行。没有必要为每个不同的环境创建都单独执行代码,因为同一种字节码可以在所有环境中运行。因此通过使用字节码,Java为程序员提供了“一次编写,随处运行”的能力。
字节码的第二个优势是安全性。由于字节码在JVM的控制下执行,因此JVM可以防止执行恶意操作的Java程序。保证主机安全的能力对于Java的成功是至关重要的,因为它允许创建Applet。由于Applet是可以通过Internet动态下载的小程序,因此避免Applet破坏主机的机制是非常必要的。字节码和JVM的结合,还保证了Applet的安全下载。可以说,如果没有字节码,那么Web可能根本无法达到今天的地位和影响。
丰富的Java API 从概念上来讲,计算机语言由两部分组成。一是语言本身,由关键字和语法定义;二是标准库,包含一组面向程序员的类、接口和方法。尽管现在所有的主流编程语言都提供了大量的库,但是Java定义的库由于更为丰富和多样显得非常突出。人们在最初创建Java时,它的库包括一组核心程序包,例如java.lang、java.io和java.net。随着Java不断发布新的版本,新的类和程序包也被不断加入。如今的Java已经为程序员提供了功能极其强大的库函数。
从Java创建之初,Java函数库就与其他语言的函数库有所不同,其中一个关键不同之处,在于Java库对网络的支持。在开发Java的时候,其他语言(例如C++)并没有提供(现在仍然没有提供)处理网络的标准函数。Java通过提供相应的类,使得连接和使用Internet的处理非常方便,从而有力地推动了Internet革命的进程。Java向所有的程序员开放了Internet,而不仅仅局限于精通网络编程的那部分人。java.net的功能改变了计算的形式。
Java核心库中的另外一个关键的程序包是java.awt,它支持抽象窗口工具集(Abstract Window Toolkit,AWT)。程序员可以用AWT创建可移植的、基于GUI的代码。也就是说,程序员用AWT类可以创建一个基于视窗的应用程序,并在程序中使用各种标准的GUI元素,例如滚动条、复选框和单选框等。由于AWT的存在,程序员创建的GUI应用程序可以运行在任何支持Java虚拟机的环境中。而这种层次上的GUI移植性,在Java之前是从来没有过的。
在Java中加入AWT彻底改变了程序员思考应用程序环境的方式。在Java前时代,基于GUI的程序必须明确指出其执行环境。这意味着任何Windows程序需要重新编译才能够在一台Apple机上运行。Java通过一个可移植的GUI提供统一的编程环境。
后来Java:Swing也被加入Java中,这是AWT的一个轻型实现。Swing组件包含在javax.swing及其子程序包中。Swing为程序员提供了一组丰富的GUI组件。与AWT相比,它们的可移植性更高。本书将通过许多例子来演示AWT和Swing如何为程序员提供函数,使他们具有编写高效、可移植的GUI应用程序的能力。
如今,Java库已经基于最初的核心库得到了极大发展。Java的每个新版本都会提供一些新的库。新的程序包不断增加,新的功能也不断加入已有的程序包中。Java库的发展过程处于一个连续的状态,因为Java必须能够适应快速演变的计算环境。这种在短期内适应和变化的能力也是Java的精髓之一。Applet Applet是Java最具有革命意义的特征之一,因为它能够创建可移植的、可动态下载的、能够在浏览器的限定下安全执行的程序。人们如今已经普遍接受这一观点,但是在Java前时代,这种可执行内容(executable content)受到广泛置疑:一个疑问是,恶意程序是否会对客户机造成伤害;另外一个疑问是:为某种类型的CPU和操作系统编译的代码在另一种系统上可能无法正常运行。由于连接到Internet的CPU和操作系统种类繁多,那么对于某个程序,为每一类型的环境都创建一个独立的运行版本是不切实际的。Java Applet为以上两个问题提供了一套很好的解决方案。通过使用Applet,Web程序员可以方便地在静态的HTML页面中添加动态的内容。Java Applet使网页变得生动起来,从此告别了静态网页的年代。
除了改变人们对于Web内容的思维方式之外,Applet还有一个重要的影响——它推动了组件软件开发的发展(也可能是一个副作用)。由于Applet是小程序,因此它们通常代表很小的功能单元;而软件组件正是基于这种思想。只要按照Applet的思维考虑问题,那么就向Beans的思想迈出了一小步,甚至更多。在面向组件的体系结构中,一个应用程序由一些互相作用的组件构成。如今这种体系结构已经大量取代了以往编程模式中常见的统一模型。继续变革Java的精髓还体现在另外一个方面,尽管它实际上并不属于Java语言的一部分。Java引入了一种乐于接受新思想的创新文化,以及一个能够迅速吸收这些新思想的过程。虽然很多计算机语言变化缓慢,但是Java却处于不停地发展和适应的过程之中。同时,这个过程通过Java社团(JCP)向整个Java团体公开。JCP提供了一种机制,Java用户可以通过该机制协助决定Java语言、工具和相关技术的未来发展方向。因此,实际使用Java语言的人们可以参与到它的发展中来。
从诞生之初开始,Java就为编程领域带来了变革—— 并且这个变革仍然没有停止。Java仍然处于计算机语言发展的前沿,它在计算发展史上已占有了永恒的地位。第二部分 递归下降的表达式解析器
如何编写一个程序,使之接收包含数字表达式的字符串(如(10-5)*3 )作为输入,并通过计算得到正确的输出结果呢?也许只有少数的“大师级”程序员才能够做到这一点。表达式解析过程将算术表达式转化为计算机可以识别的形式。它也是所有需要进行表达式转换的软件的核心,这些软件包括语言编译器和解释器、电子制表软件等等。
表达式
由于解析器处理的对象是表达式,因此有必要介绍表达式的组成。虽然表达式的类型多种多样,但本章只处理一种类型:数值表达式。数值表达式由下列元素组成:
● 数字
● 运算符+、–、/、*、^、%、=
● 圆括号
● 变量
其中,^运算符表示求幂运算(不是Java中规定的XOR运算),=是赋值运算符。这些元素按照代数学的规则组合成表达式。例如:
10 – 8
(100 – 5) * 14/6
a + b – c
10^5
a = 10 – b
每个运算符的优先级如表2-1所列:
表2-1 运算符优先级
最高优先级最低优先级 + - (正负号)
^
* / %
+ -
=
优先级相等的运算符按照从左到右的顺序计算。
本章介绍的解析器必须满足以下一些约束条件:第一,所有变量都是单个字母(从A到Z的26个变量),字母不区分大小写(把a和A视为同一个变量)。第二,假定所有的数字都是double类型,可以方便地修改解析器从而处理其他类型的值。最后,为保证逻辑清晰和理解方便,解析器只进行基本的错误检查。
解析表达式
没有对表达式解析进行全面思考的人,会认为这个问题非常简单,但实际上并非如此。为了更好理解这一点,计算下面的简单表达式:
10–2*3
众所周知,这个表达式的结果等于4。编写一个程序计算某个特定的表达式非常容易,但问题在于如何创建一个程序,来为任意的表达式给出正确的答案。首先考虑下面的运算法则:
a = get first operand
while(operands present) {
op = get operator
b = get second operand
a = a op b
}
该方法依次获得第一个操作数、运算符和第二个操作数以执行第一个操作;然后获得下一个运算符和操作数以执行下一个操作,依此类推。然而,如果采用这种基本方式计算上面的表达式,那么10 – 2 * 3的结果将是24(也就是8*3)而不是4。这是因为上述过程忽略了运算符的优先级。像这样从左到右依次获取操作数和运算符的方法是行不通的,因为代数学规定乘法运算必须先于减法运算完成。也许有些初学者认为解决这个问题很容易,而且实际上在某些有限的条件下确实如此。但是当表达式中增加了圆括号、求幂运算、变量、一元运算符等元素之后,问题只会变得更为复杂。
尽管编写处理表达式的代码有很多种方式,但是本章介绍的一种最容易由个人开发完成,称之为递归向下的解析器。在阅读本章的过程中,读者会明白这样命名的原因(其他一些解析器的编写方法使用了一些复杂的表格,这些表格通常由另一些计算机程序生成。这些解析器有时也称为表格驱动(table-driven)的解析器)。
表达式的解析
解析和计算表达式的方式有很多种。在使用递归下降的解析器时,表达式被视为递归的数据结构—— 表达式由其本身来定义。假定表达式只能使用+、-、*、/和圆括号,那么所有的表达式可以用下面的规则来定义:
表达式 à项 [+项] [–项]
项à因数[*因数] [/因数]
因数à变量、数字或者(表达式)
方括号里面表示可选元素,而箭头à表示箭头前面的元素由箭头后面的元素定义产生。实际上,该规则通常被称为表达式的生成规则。因此,对于项的定义可以这样表述:“项由因数乘以因数或者因数除以因数产生。”需要注意的是,运算符的优先级已经隐含在表达式的定义中。
下面举例说明表达式的解析过程。表达式
10 + 5 * B
有两个项:10和5*B,第二项包括两个因数:5和B,分别是一个数字和一个变量。
下面看另外一个例子。表达式
14 * (7 – C)
有两个因数:14和(7-C),分别是一个数字和一个圆括号表达式。圆括号表达式包括两个项:一个数字和一个变量。
上述过程形成了递归下降解析器的基础。递归下降解析器是一组互相递归的方法,这些方法以一种链式方式实现生成规则。在每个适当的步骤上,解析器以代数学规定的正确顺序执行指定的操作。为了解释如何使用生成规则来解析表达式,采用下面的表达式来跟踪解析过程:
9/3 – (100 + 56)
整个解析过程如下:
(1) 获得第一项9/3;
(2) 获得第一项的两个因数并完成除法运算,得到结果3;
(3) 获得第二项(100+56)。在这一步启动递归分析过程处理括号内的子表达式;
(4) 获得其中的两项并完成加法运算,得到结果156;
(5) 从第二项的递归计算过程中返回;
(6) 3减去156,答案是-153。
如果读者对于递归的理解有些困难,不要沮丧。递归是一个相当复杂的概念,需要慢慢来习惯。对于表达式的这种递归过程,请记住两个基本概念:第一,运算符的优先级隐含在生成规则的定义方式中;第二,这种递归解析和计算表达式的方式跟人们计算数学表达式的方式非常相似。
本章接下来将介绍两个解析器:第一个能够解析和计算仅由常量数据组成的double类型浮点表达式,该解析器说明递归下降解析方法的基本原理;第二个则增加了对变量的处理。
表达式的分解为了计算表达式的值,解析器需要分解出表达式的独立元素。例如表达式:
A * B – (W + 10)
包括下面这些独立元素:A、*、B、–、(、W、+、10和)。在解析术语中,这样的表达式元素被称为标识符(token),表示表达式中一个不可再分的独立单元。在详细介绍解析器之前,先看看表达式的标识方法,因为它是解析的基础。
为了将表达式分离为单个标识符,需要设计一个过程,从头到尾地扫描表达式,并顺序地返回表达式的每个标识符。该方法必须确定每个标识符的类型,而且必须识别表达式的结尾。在本节介绍的解析器中,实现这些功能的方法名为getToken()。
本章介绍的两个解析器都封装在Parser类中。尽管下文对这个类进行了详细描述,但是现在必须提前说明它的第一部分,以便读者理解getToken()方法的工作过程。Parser类首先定义了一些final变量和域,如下:
class Parser {
// These are the token types.
final int NONE = 0;
final int DELIMITER = 1;
final int VARIABLE = 2;
final int NUMBER = 3;
// These are the types of syntax errors.
final int SYNTAX = 0;
final int UNBALPARENS = 1;
final int NOEXP = 2;
final int DIVBYZERO = 3;
// This token indicates end-of-expression.
final String EOE = "\0";
private String exp; // refers to expression string
private int expIdx; // current index into the expression
private String token; // holds current token
private int tokType; // holds token's type
在表达式解析的过程中,每个标识符必须有一个与之相关的类型。因此Parser类首先定义了几个常数,表明标识符的各个不同类型。本章中所介绍的解析器只用到3种类型:变量、数值和分隔符,它们分别由常量VARIABLE、NUMBER和DELIMITER表示。DELIMITER既可以是运算符,也可以是括号。此外,NONE类型仅仅作为未定义标识符的一个占位符。
接下来,Parser定义了另外几个错误常量,它们代表解析和计算表达式的过程中可能发生的不同类型的错误。SYNTAX代表所有导致非正则表达式的错误;UNBALPARENS表示括号不对称的错误;如果解析器执行时没有表达式被提交,就会报告一个NOEXP错误;DIVBYZERO则表示除数为零的错误。
final变量EOE标志解析器已达到表达式的结尾。
被解析的表达式保存在一个字符串中,exp变量则存储对该字符串的一个引用。这样,exp就可以指向一个形如“10+4”的字符串。而此字符串中的下一个标识符的索引保存在expIdx变量中,初始索引值为0。当前获得的标识符存储在token变量中,其类型则存储在tokType变量中。这些域的属性都是private类型,因为它们只允许由解析器使用并且不能被外部代码修改。下面给出getToken()方法的完整代码。这个方法每次调用获得表达式中的下一个标识符。exp指向包含这个表达式的字符串,其索引由expIdx表示。也就是说,每次调用getToken()都将获得exp[expIdx]表示的下一个标识符。然后getToken()将这个标识符存入token域,将标识符类型存入tokType域中。getToken()方法调用isDelim()方法,其代码如下:
// Obtain the next token.
private void getToken()
{
tokType = NONE;
token = "";
// Check for end of expression.
if(expIdx == exp.length()) {
token = EOE;
return;
}
// Skip over white space.
while(expIdx < exp.length() &&
Character.isWhitespace(exp.charAt(expIdx))) ++expIdx;
// Trailing whitespace ends expression.
if(expIdx == exp.length()) {
token = EOE;
return;
}
if(isDelim(exp.charAt(expIdx))) { // is operator
token += exp.charAt(expIdx);
expIdx++;
tokType = DELIMITER;
}
else if(Character.isLetter(exp.charAt(expIdx))) { // is variable
while(!isDelim(exp.charAt(expIdx))) {
token += exp.charAt(expIdx);
expIdx++;
if(expIdx >= exp.length()) break;
}
tokType = VARIABLE;
}
else if(Character.isDigit(exp.charAt(expIdx))) { // is number
while(!isDelim(exp.charAt(expIdx))) {
token += exp.charAt(expIdx);
expIdx++;
if(expIdx >= exp.length()) break;
}
tokType = NUMBER;
}
else { // unknown character terminates expression
token = EOE;
return;
}
}
// Return true if c is a delimiter.
private boolean isDelim(char c)
{
if((" +-/*%^=()".indexOf(c) != -1))
return true;
return false;
}
下面详细分析getToken()方法。getToken()首先执行一些必要的初始化工作,然后查看expIdx是否等于exp.length(),以此判断是否已到达表达式的结尾。由于expIdx是表达式中的一个索引,因此,如果该索引等于该表达式字符串的长度,则表明整个表达式已经解析完毕。
如果表达式中还能找到未被处理过的标识符,那么getToken()方法将继续处理标识符的过程。首先跳过下一个标识符之前的全部空格。如果表达式以空格结尾,那么跳过这些空格之后必须返回一个EOE标志。否则,在跳过空格之后,exp[expIdx]可能是以下三种字符中的一种:数字、变量或者运算符。根据exp[expIdx]之后字符类型的不同,getToken()方法对当前标识符的处理有所不同。如果该字符之后的一个字符是运算符,那么就将当前标识符保存在token变量中作为字符串返回,同时将tokType置为DELIMITER。如果接下来的字符是一个字母,那么就将它作为一个变量处理,把它保存在token中作为一个字符串返回,同时将tokType置为VARIABLE。如果接下来的字符是一个阿拉伯数字,那么要读出整个数字,并将它以字符串的形式保存在token中,同时将tokType设置为NUMBER。最后,如果下一个字符不是以上三种中的任何一种,那么token将被置为EOE。
为了更清楚地分析getToken()中的代码,在这里省略了大部分的错误检查代码,并作出一些假设。例如,对于任何尚未确认的字符,只要在它之前有一个空格,即可作为表达式的结束字符。同时,在这个版本中,虽然变量的长度可以是任意的,但是只有第一个字母有意义。当然,对于特定的需求,应用程序可以增加更多的错误检查或者其他一些细节。
为了更好地理解标识符的分析过程,首先看看下列表达式返回的每个标识符及其类型(见表2-2):
A + 100 – (B * C) /2
表2-2 标识符的类型
请记住,标识符总是以字符串的形式保存,即使当它仅包含单个字母时也是如此。
最后提醒一点:Java包含了一些非常有用的内嵌的标识符处理功能,例如由StringTokenizer类所支持的多种功能。但是对于解析器而言,最好使用getToken()等专门的标识符处理方法来完成标识符处理工作。
一个简单的表达式解析器
下面是解析器的第一个版本。这个解析器可以计算仅由数字、运算符和括号组成的表达式。尽管getToken()方法可以处理变量,但是这个版本并不对变量做任何处理。在读者明白这个简单解析器的工作原理之后,再增加处理变量的功能。
/* This module contains the recursive descent parser that does not use variables. */ // Exception class for parser errors. class ParserException extends Exception { String errStr; // describes the error public ParserException(String str) { errStr = str; } public String toString() { return errStr; } } class Parser { // These are the token types. final int NONE = 0; final int DELIMITER = 1; final int VARIABLE = 2; final int NUMBER = 3; // These are the types of syntax errors. final int SYNTAX = 0; final int UNBALPARENS = 1; final int NOEXP = 2; final int DIVBYZERO = 3; // This token indicates end-of-expression. final String EOE = "\0"; private String exp; // refers to expression string private int expIdx; // current index into the expression private String token; // holds current token private int tokType; // holds token's type // Parser entry point. public double evaluate(String expstr) throws ParserException { double result; exp = expstr; expIdx = 0; getToken(); if(token.equals(EOE)) handleErr(NOEXP); // no expression present // Parse and evaluate the expression. result = evalExp2(); if(!token.equals(EOE)) // last token must be EOE handleErr(SYNTAX); return result; } // Add or subtract two terms. private double evalExp2() throws ParserException { char op; double result; double partialResult; result = evalExp3(); while((op = token.charAt(0)) == '+' || op == '-') { getToken(); partialResult = evalExp3(); switch(op) { case '-': result = result - partialResult; break; case '+': result = result + partialResult; break; } } return result; }
// Multiply or divide two factors. private double evalExp3() throws ParserException { char op; double result; double partialResult; result = evalExp4(); while((op = token.charAt(0)) == '*' || op == '/' || op == '%') { getToken(); partialResult = evalExp4(); switch(op) { case '*': result = result * partialResult; break; case '/': if(partialResult == 0.0) handleErr(DIVBYZERO); result = result / partialResult; break; case '%': if(partialResult == 0.0) handleErr(DIVBYZERO); result = result % partialResult; break; } } return result; } // Process an exponent. private double evalExp4() throws ParserException { double result; double partialResult; double ex; int t; result = evalExp5(); if(token.equals("^")) { getToken(); partialResult = evalExp4(); ex = result; if(partialResult == 0.0) { result = 1.0; } else for(t=(int)partialResult-1; t > 0; t--) result = result * ex; } return result; } // Evaluate a unary + or -. private double evalExp5() throws ParserException { double result; String op; op = ""; if((tokType == DELIMITER) && token.equals("+") || token.equals("-")) { op = token; getToken(); } result = evalExp6(); if(op.equals("-")) result = -result; return result; } // Process a parenthesized expression. private double evalExp6() throws ParserException { double result; if(token.equals("(")) { getToken(); result = evalExp2(); if(!token.equals(")")) handleErr(UNBALPARENS); getToken(); } else result = atom(); return result; } // Get the value of a number. private double atom() throws ParserException { double result = 0.0; switch(tokType) { case NUMBER: try { result = Double.parseDouble(token); } catch (NumberFormatException exc) { handleErr(SYNTAX); } getToken(); break; default: handleErr(SYNTAX); break; } return result; } // Handle an error. private void handleErr(int error) throws ParserException { String[] err = { "Syntax Error", "Unbalanced Parentheses", "No Expression Present", "Division by Zero" }; throw new ParserException(err[error]); } // Obtain the next token. private void getToken() { tokType = NONE; token = ""; // Check for end of expression. if(expIdx == exp.length()) { token = EOE; return; } // Skip over white space. while(expIdx < exp.length() && Character.isWhitespace(exp.charAt(expIdx))) ++expIdx; // Trailing whitespace ends expression. if(expIdx == exp.length()) { token = EOE; return; } if(isDelim(exp.charAt(expIdx))) { // is operator token += exp.charAt(expIdx); expIdx++; tokType = DELIMITER; } else if(Character.isLetter(exp.charAt(expIdx))) { // is variable while(!isDelim(exp.charAt(expIdx))) { token += exp.charAt(expIdx); expIdx++; if(expIdx >= exp.length()) break; } tokType = VARIABLE; } else if(Character.isDigit(exp.charAt(expIdx))) { // is number while(!isDelim(exp.charAt(expIdx))) { token += exp.charAt(expIdx); expIdx++; if(expIdx >= exp.length()) break; } tokType = NUMBER; } else { // unknown character terminates expression token = EOE; return; } } // Return true if c is a delimiter. private boolean isDelim(char c) { if((" +-/*%^=()".indexOf(c) != -1)) return true; return false; } }
在代码的开始部分,解析器声明了一个ParserException类。这是一个异常类,当解析器在解析表达式的过程中遇到错误时就会抛出异常。此异常需要由使用该解析器的代码进行处理。
以上代码中的解析器能够处理下列运算符:+、–、*、/和%。另外,它还能够处理整数求幂运算^和一元取反运算,并能够正确处理圆括号。
使用解析器的方法是这样的:首先必须创建一个Parser类型的对象,然后调用该对象的evaluate()方法,同时将需要计算的表达式字符串作为参数传递给该方法,最后返回结果。由于Parser类在出错时抛出ParserException异常,因此主应用程序必须处理该异常。下面的例子说明了解析器的使用方法:
// Demonstrate the parser. import java.io.*; class PDemo { public static void main(String args[]) throws IOException { String expr; BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); Parser p = new Parser(); System.out.println("Enter an empty expression to stop."); for(;;) { System.out.print("Enter expression: "); expr = br.readLine(); if(expr.equals("")) break; try { System.out.println("Result: " + p.evaluate(expr)); System.out.println(); } catch (ParserException exc) { System.out.println(exc); } } } } 下面是几个例子的运行情况: Enter an empty expression to stop. Enter expression: 10-2*3 Result: 4.0 Enter expression: (10-2)*3 Result: 24.0 Enter expression: 10/3.5 Result: 2.857142857142857
深入理解Parser类
下面详细介绍Parser类。如前所述,exp指向一个字符串,这个字符串包含了需要计算的表达式。每次调用evaluate()都将重新设置exp域。请牢记:解析器所计算的表达式包含在标准的Java字符串之中。例如,下列字符串包含了解析器可计算的表达式:
"10 – 5" "2 * 3.3 / (3.1416 * 3.3)"
exp的当前索引保存在expIdx中。当解析器开始执行解析操作时,expIdx首先被置为0,然后该值随着表达式解析过程的进行而逐渐增长。token域保存当前被处理的标识符,而tokType保存该标识符的类型。
解析器的入口点是evaluate()方法,调用该方法必须传递一个字符串作为参数,其中包含将要处理的表达式。从evalExp2()到evalExp6()的5个方法与atom()方法共同构成递归下降的解析器。它们实现了一个增强的表达式产生规则集,这个规则集在前面已经讨论过。每个方法顶部的注释部分描述了它们所完成的功能。在解析器的下一个版本中还将增加evalExp1()方法。
handleErr()方法处理表达式中的语法错误。前面已经提到,getToken()和isDelim()方法将表达式分解为基本的元素。解析器调用getToken()方法从表达式的首字符开始,逐个获取表达式中的标识符,直到表达式结尾。根据获得标识符的不同类型,解析器将采取不同的动作。
为了更准确地理解析器计算表达式的过程,分析下面表达式的解析过程:
10 – 3 * 2
由于evaluate()是入口点,因此解析器首先调用evaluate()方法获得第一个标识符。如果这个标识符是EOE,那么表明evaluate()处理的是个空字符串,于是产生一个NOEXP错误。在这个例子中,获得的第一个标识符是数字“10”。接下来解析器调用evalExp2()方法,然后是一系列链式调用:evalExp2()调用evalExp3(),evalExp3()调用evalExp4(),evalExp4()再调用evalExp5()。调用evalExp5()方法后,下一步是检查这个标识符是否为一元取正或者一元取负运算符。在本例中标识符不是这两个一元操作,因此调用evalExp6()方法。此时,evalExp6()或者递归调用evalExp2()(当读取的标识符为括号时),或者调用atom()获得数字的值。由于这个标识符不是左括号,因此解析器调用atom()方法,执行后返回数值10。下一步骤是用getToken()方法继续检索剩余的标识符,与此同时,沿着上述调用链逐步回溯。在本例中,getToken()方法获得下一个运算符“-”,同时回溯调用evalExp2()。
接下来的一个步骤非常重要。由于当前标识符是运算符“-”,因此它被保存到变量op中。然后解析器继续获取下一个标识符“3”,于是开始新的下降调用过程。和刚才一样,调用过程最后又进入atom()方法,返回结果为数值“3”。然后解析器接着读入“*”,这将导致调用链向上回溯到evalExp3(),evalExp3()读入表达式的最后一个标识符“2”。这时解析器进行第一次算术操作—— 2与3相乘。乘积返回给evalExp2(),然后执行减法,得到结果4。尽管刚开始理解这个过程有些困难,但是对其他一些例子的分析可以验证,解析器每次都能对输入的表达式进行正确的处理。
如果解析过程发生错误,那么解析器将调用handleErr()方法,该方法抛出一个ParserException异常对错误进行描述。由evaluate()抛出的ParserException异常必须由使用解析器的代码进行处理。
前面的例子程序表明,这个解析器比较适合于简单的桌面计算器使用。它还不能够用于计算机语言、数据库或者复杂的计算器之中,因为它还需要处理变量的能力,而这是2.6节将要讨论的主题。
向解析器中添加变量
很多应用使用变量存储数据,包括所有的编程语言、大部分的计算器和电子制表软件。上面介绍的第一版本解析器必须添加变量处理的功能才能用于上述应用领域。为此,解析器需要增加以下一些功能。首先,当然需要增加变量。前文已经提到,我们使用从A到Z的26个字母为变量命名。我们存储在Parser类中的一个包含26个元素的双精度数组之中。因此,必须向Parser类中添加下列域:
// Array for variables.
private double vars[] = new double[26];
当Parser对象初始化时,数组中的每个元素都被自动初始化为0。
解析器还需要一个方法来查找指定变量的值。由于变量名由从A到Z的字母组成,因此可以用变量名第一个字母的ASCII值减去A的ASCII值,得到的就是该变量在数组vars中的索引。下面给出的findVar()方法完成了此功能:
// Return the value of a variable.
private double findVar(String vname) throws ParserException
{
if(!Character.isLetter(vname.charAt(0))){
handleErr(SYNTAX);
return 0.0;
}
return vars[Character.toUpperCase(vname.charAt(0))-'A'];
}
分析上述代码可知,虽然findVar()方法接收的参数可以是一个较长的变量名,例如A12或者test,但是只有变量名的第一个字母是有意义的。读者可以试着改变这一特征以适应自己的需求。
另外还必须修改atom()方法以同时具备处理数字和变量的能力。新的版本的代码如下:
// Get the value of a number or variable.
private double atom() throws ParserException
{
double result = 0.0;
switch(tokType) {
case NUMBER:
try {
result = Double.parseDouble(token);
} catch (NumberFormatException exc) {
handleErr(SYNTAX);
}
getToken();
break;
case VARIABLE:
result = findVar(token);
getToken();
break;
default:
handleErr(SYNTAX);
break;
}
return result;
}
从技术上讲,这些代码都是解析器为正确处理变量而增加的;然而,还没有办法为这些变量指定一个值。为了给变量赋值,解析器需要处理赋值运算符(即“=”号)的能力。为了实现赋值功能,需要在Parser类中增加另一个方法evalExp1()。增加evalExp1()之后,递归下降的调用链将由这个方法开始。也就是说,现在evaluate()必须调用evalExp1()方法(而不是evalExp2())开始表达式的解析过程方法。evalExp1()方法的代码如下:
// Process an assignment.
private double evalExp1() throws ParserException
{
double result;
int varIdx;
int ttokType;
String temptoken;
if(tokType == VARIABLE) {
// save old token
temptoken = new String(token);
ttokType = tokType;
// Compute the index of the variable.
varIdx = Character.toUpperCase(token.charAt(0)) - 'A';
getToken();
if(!token.equals("=")) {
putBack(); // return current token
// restore old token -- not an assignment
token = new String(temptoken);
tokType = ttokType;
}
else {
getToken(); // get next part of exp
result = evalExp2();
vars[varIdx] = result;
return result;
}
}
return evalExp2();
}evalExp1()方法需要向前扫描,决定是否真正执行一次赋值操作。这是因为变量名总是在赋值符号之前出现,但是单独的变量名并不能保证其后一定是一个赋值表达式。也就是说,解析器能够知道A=100是赋值操作,但是它实际更聪明,能判定A/10不是赋值操作。为此,evalExp1()从输入流中读取下一个标识符。如果该标识符不是一个等号,那么读到的标识符将返回给输入流,之后将调用putBack()方法进行处理。其代码如下:
// Return a token to the input stream. private void putBack() { if(token == EOE) return; for(int i=0; i < token.length(); i++) expIdx--; } 完成必要的改进之后,解析器如下: /* This module contains the recursive descent parser that uses variables. */ // Exception class for parser errors. class ParserException extends Exception { String errStr; // describes the error public ParserException(String str) { errStr = str; } public String toString() { return errStr; } } class Parser { // These are the token types. final int NONE = 0; final int DELIMITER = 1; final int VARIABLE = 2; final int NUMBER = 3; // These are the types of syntax errors. final int SYNTAX = 0; final int UNBALPARENS = 1; final int NOEXP = 2; final int DIVBYZERO = 3; // This token indicates end-of-expression. final String EOE = "\0"; private String exp; // refers to expression string private int expIdx; // current index into the expression private String token; // holds current token private int tokType; // holds token's type // Array for variables. private double vars[] = new double[26]; // Parser entry point. public double evaluate(String expstr) throws ParserException { double result; exp = expstr; expIdx = 0; getToken(); if(token.equals(EOE)) handleErr(NOEXP); // no expression present // Parse and evaluate the expression. result = evalExp1(); if(!token.equals(EOE)) // last token must be EOE handleErr(SYNTAX); return result; } // Process an assignment. private double evalExp1() throws ParserException { double result; int varIdx; int ttokType; String temptoken; if(tokType == VARIABLE) { // save old token temptoken = new String(token); ttokType = tokType; // Compute the index of the variable. varIdx = Character.toUpperCase(token.charAt(0)) - 'A'; getToken(); if(!token.equals("=")) { putBack(); // return current token
// restore old token -- not an assignment token = new String(temptoken); tokType = ttokType; } else { getToken(); // get next part of exp result = evalExp2(); vars[varIdx] = result; return result; } } return evalExp2(); } // Add or subtract two terms. private double evalExp2() throws ParserException { char op; double result; double partialResult; result = evalExp3(); while((op = token.charAt(0)) == '+' || op == '-') { getToken(); partialResult = evalExp3(); switch(op) { case '-': result = result - partialResult; break; case '+': result = result + partialResult; break; } } return result; } // Multiply or divide two factors. private double evalExp3() throws ParserException { char op; double result; double partialResult; result = evalExp4(); while((op = token.charAt(0)) == '*' || op == '/' || op == '%') { getToken(); partialResult = evalExp4(); switch(op) { case '*': result = result * partialResult; break; case '/': if(partialResult == 0.0) handleErr(DIVBYZERO); result = result / partialResult; break; case '%': if(partialResult == 0.0) handleErr(DIVBYZERO); result = result % partialResult; break; } } return result; } // Process an exponent. private double evalExp4() throws ParserException { double result; double partialResult; double ex; int t; result = evalExp5(); if(token.equals("^")) { getToken(); partialResult = evalExp4(); ex = result; if(partialResult == 0.0) { result = 1.0; } else for(t=(int)partialResult-1; t > 0; t--) result = result * ex; } return result; } // Evaluate a unary + or -. private double evalExp5() throws ParserException { double result; String op; op = ""; if((tokType == DELIMITER) && token.equals("+") || token.equals("-")) { op = token; getToken(); } result = evalExp6(); if(op.equals("-")) result = -result; return result; } // Process a parenthesized expression. private double evalExp6() throws ParserException { double result; if(token.equals("(")) { getToken(); result = evalExp2(); if(!token.equals(")")) handleErr(UNBALPARENS); getToken(); } else result = atom(); return result; } // Get the value of a number or variable. private double atom() throws ParserException { double result = 0.0; switch(tokType) { case NUMBER: try { result = Double.parseDouble(token); } catch (NumberFormatException exc) { handleErr(SYNTAX); } getToken(); break; case VARIABLE: result = findVar(token); getToken(); break; default: handleErr(SYNTAX); break; } return result; } // Return the value of a variable. private double findVar(String vname) throws ParserException { if(!Character.isLetter(vname.charAt(0))){ handleErr(SYNTAX); return 0.0; } return vars[Character.toUpperCase(vname.charAt(0))-'A']; } // Return a token to the input stream. private void putBack() { if(token == EOE) return; for(int i=0; i < token.length(); i++) expIdx--; } // Handle an error. private void handleErr(int error) throws ParserException { String[] err = { "Syntax Error", "Unbalanced Parentheses", "No Expression Present", "Division by Zero" }; throw new ParserException(err[error]); } // Obtain the next token. private void getToken() { tokType = NONE; token = ""; // Check for end of expression. if(expIdx == exp.length()) { token = EOE; return; } // Skip over white space. while(expIdx < exp.length() && Character.isWhitespace(exp.charAt(expIdx))) ++expIdx; // Trailing whitespace ends expression. if(expIdx == exp.length()) { token = EOE; return; } if(isDelim(exp.charAt(expIdx))) { // is operator token += exp.charAt(expIdx); expIdx++; tokType = DELIMITER; } else if(Character.isLetter(exp.charAt(expIdx))) { // is variable while(!isDelim(exp.charAt(expIdx))) { token += exp.charAt(expIdx); expIdx++; if(expIdx >= exp.length()) break; } tokType = VARIABLE; } else if(Character.isDigit(exp.charAt(expIdx))) { // is number while(!isDelim(exp.charAt(expIdx))) { token += exp.charAt(expIdx); expIdx++; if(expIdx >= exp.length()) break; } tokType = NUMBER; } else { // unknown character terminates expression token = EOE; return; } } // Return true if c is a delimiter. private boolean isDelim(char c) { if((" +-/*%^=()".indexOf(c) != -1)) return true; return false; } }
为了测试改进后的解析器的功能,读者可以使用用来测试第一版本解析器的同一个程序。有了这个改进后的解析器,用户可以输入如下示例表达式:
A = 10/4 A – B C = A * (F – 21)
递归下降解析器中的语法检查
在解析表达式时,表达式必须严格符合解析器的语法规则。当输入表达式不符合这种规则时就会产生语法错误。通常情况下,语法错误都是由人为因素引起的,最常见的是打字错误。举例来说,对于本章介绍的解析器而言,下列表达式都不是有效的表达式:
10 ** 8
((10 – 5) * 9
/8
第一个表达式包含了两个连续的运算符,第二个表达式缺少一个右括号,第三个表达式的开头是一个除号。解析器中不允许以上任何一种情况出现。语法错误会导致解析器给出错误的解析结果,因此程序员必须尽量避免出现这种错误。
在这个解析器中,错误发生时会调用handleErr()方法。与其他类型的解析器不同,递归下降的方法使得语法检查更加简单,因为在大部分情况下,错误会发生在atom()、findVar()或者evalExp6()等方法中,这些方法的共同点是都对括号进行检查。
调用handleErr()将抛出ParserException异常,其中包含对错误的描述。Parser类本身并不捕捉这个异常,而是将其抛给调用代码。这样一来,解析器会因为发生错误而马上终止解析过程。当然,读者可以对这种方式进行修改以适应自己的需求。
例如,读者可以进一步扩充ParserException对象中所包含的信息。在当前的代码中,这个类仅仅存储一个描述错误的字符串。读者也可以增加错误代码本身,或者添加指向表达式字符串中错误发生点的索引,或者增加其他一些信息
计算器Applet
本章介绍了两个解析器,它们的使用非常简单,几乎可以用于任何应用程序。下面的例子可用来说明解析器的使用方法。这个例子只用几行代码就创建了一个功能齐全的计算器Applet。该计算器使用两个文本框:第一个用于接收需要计算的表达式;第二个用于显示计算结果。其中后者是一个只读文本框。而错误消息显示在状态栏上。示例运行的输出结果(使用Applet Viewer)如图2-1所示。
// A simple calculator applet. import java.awt.*; import java.awt.event.*; import java.applet.*; /* <applet code="Calc" width=200 height=150> </applet> */ public class Calc extends Applet implements ActionListener { TextField expText, resText; Parser p; public void init() { Label heading = new Label("Expression Calculator ", Label.CENTER); Label explab = new Label("Expression ", Label.CENTER); Label reslab = new Label("Result ", Label.CENTER); expText = new TextField(24); resText = new TextField(24); resText.setEditable(false); // result field for display only add(heading); add(explab); add(expText); add(reslab); add(resText); /* Register expression text field to receive action events. */ expText.addActionListener(this); // create parser p = new Parser(); } // User pressed Enter. public void actionPerformed(ActionEvent ae) { repaint(); } public void paint(Graphics g) { double result = 0.0; String expstr = expText.getText(); try { if(expstr.length() != 0) result = p.evaluate(expstr); // To clear expression after ENTER is pressed // use the folloing line: // expText.setText(""); resText.setText(Double.toString(result)); showStatus(""); // erase any previous error message } catch (ParserException exc) { showStatus(exc.toString()); resText.setText(""); } } }
Calc类首先声明3个实例变量,分别保存3个引用。第一个是expText,指向输入表达式的文本框组件;第二个是resText,指向显示计算结果的文本框组件;而指向解析器的引用保存在变量p中。
Calc类的init()方法创建两个文本框并将它们添加到Applet中,然后为expText文本框注册一个侦听器。用户每次输入回车时,文本框都将产生一个事件。由于显示结果的文本框resText只用于显示计算结果,因此在init()中调用setEditable(false)方法,将其设置为只读属性。经过这样的设置后,显示结果的文本框从外观上看呈现灰色,并且不再响应用户的输入。init()方法最后初始化了一个Parser实例并赋给变量p。使用计算器时,用户只需输入表达式,然后回车。程序会产生一个ActionEvent事件,并调用actionPerformed()方法来处理该事件。接着actionPerformed()方法先调用repaint()方法,最后调用paint()方法。在paint()方法中,解析器执行计算过程,得出表达式的结果,并显示在结果显示文本框中。注意:所有的错误都显示在状态栏中。
一些尝试本章所介绍的两个表达式解析器可用于许多用途,因为它们使程序员无需做很多工作即可扩展应用程序的功能。考虑一个要求用户输入数字的程序。例如有一个应用程序要求用户输入需要打印的文件份数。在正常情况下,程序只需要显示一个文本框并等待输入,然后将输入的文本转化为应用的内部数字格式。这种简单的方法允许用户输入一个值,例如100。然而,如果总共有9个部门,用户希望为每个部门打印72份文件,该如何处理呢?这时用户必须手工计算得出乘积的结果,并在文本框中输入648。然而,如果用户可以使用解析器计算从文本框中得到的数值,那么就可以直接输入9*72,而不再需要其他任何的手工计算。解析和计算数字表达式的能力可以给应用程序(甚至最简单的应用程序)增加成熟、专业的感觉。因此,不妨在应用程序中尝试使用解析器来处理数字的输入问题。
在本章中曾经谈到,解析器只执行最小限度的错误检查。因此您可能需要增加更为详细的错误报告。例如,可以突出显示在表达式中检测到的错误位置。这样就使得用户能够很快发现语法错误并及时改正。
现在这种解析器只能够计算数字表达式。然而,如果向其中增加一些代码,它就有可能计算其他类型的表达式,例如字符串、空间坐标或者是复数等。举例来说,为了使解析器能够计算字符串,必须做如下改动:
(1) 定义一个新标识符类型,称为STRING。
(2) 增强getToken()方法的功能,使其能够正确识别字符串。
(3) 在atom()方法中添加一个新的case处理语句,用来处理STRING类型的标识符。
这些步骤完成之后,解析器就能够正确处理如下字符串表达式:
a = "one"
b = "two"
c = a + b
保存在c中的结果应该是a和b的合并,也就是“onetwo”。第三部分 使用Java实现语言解释器
大多数程序员都曾经梦想着创造自己的计算机语言。坦率地说,能够创造、控制、增强和修改属于自己的计算机语言,这种想法确实非常具有吸引力。然而,只有极少数程序员认为,实现这个想法是一件非常容易和令人愉悦的事情。开发一个功能齐备的编译器(例如Java编译器)的确是一项艰巨的任务。但是相比之下,创建一个语言解释器却简单得多。
使用Java实现语言解释器的原因
使用Java实现语言解释器的原因 大多数程序员都曾经梦想着创造自己的计算机语言。坦率地说,能够创造、控制、增强和修改属于自己的计算机语言,这种想法确实非常具有吸引力。然而,只有极少数程序员认为,实现这个想法是一件非常容易和令人愉悦的事情。开发一个功能齐备的编译器(例如Java编译器)的确是一项艰巨的任务。但是相比之下,创建一个语言解释器却简单得多。
尽管解释器和编译器都以应用程序源代码作为输入内容,但是它们对这些源代码的处理过程却截然不同。编译器将程序的源代码转化为可执行代码的形式。通常情况下,这种可执行代码由计算机的CPU指令组成,因此可以直接在计算机上执行。例如,C++即采用这种编译方式。还有一种情况,编译器输出一种可移植的中间代码,它们由运行时系统执行。Java采用的就是这种方式。在Java中,称这种中间代码为“字节码 ”。
解释器的工作原理则完全不同。它顺序读入程序的源代码,然后依次执行每一条语句。因此,解释器并不真正将源代码转化为目标代码,而是直接执行程序。尽管使用解释器执行程序的速度比将相同的程序编译成目标代码后再执行的速度慢,但是解释器仍然在编程中被广泛使用。原因有以下几个方面:
第一,解释器能够提供真正的交互式环境,由解释器执行的程序能够根据用户的指令暂停或者恢复运行。这种交互式环境在机器人技术等方面用途很广。第二,语言解释器的先天特性决定了它们特别适合于交互式的程序调试。第三,解释器最适合于作为“脚本语言”,比如数据库查询语言等。第四,语言解释器使得同一个程序运行于不同类型的平台成为可能。此时惟一的工作只是为每个新环境实现解释器的运行包。
在有些情况下,术语“解释器”的含义与刚才所描述的情况有所不同。例如,最初的Java运行时系统被称为“字节码解释器”。但是这种解释器与本章中介绍的解释器的类型并不完全相同。字节码是一组高度优化的可移植的机器指令,而Java运行时系统则为字节码提供一个执行环境。然而Java运行时系统并不直接执行源代码,而是执行可移植的机器代码。这也是Java运行时系统被称为Java虚拟机的原因。
将要介绍的解释器代码不仅有趣而且实用。同时,它还充分显示了Java语言简单高效的特性。与第2章中介绍的解析器相同的是,这个语言解释器也使用“纯代码”编写。同时,解释器也是一个相当复杂的程序,但是使用Java语言实现起来却非常简单,这也是Java语言功能多样化的一个例证。此外,解析器代码的简洁还显示了Java语法和库的强大表达能力
解释何种计算机语言 在构造解释器之前,首先必须确定将要解释的语言类型。尽管Java似乎是个不错的选择,但是它过于庞大和复杂。即使选取Java语言的一个小的子集也显得太大了,因为我们只打算利用一章的篇幅进行讲解。而且,通常情况下也没有必要为一个像Java那么强大的语言编写解释器;相反地,编写一个解释器处理某种相对简单的计算机语言倒是可行的。因此,更好的做法是选择一种易于解释、较为简单的语言。BASIC语言的早期版本就非常符合这些标准,因此在此选择了BASIC语言的一个子集,将其作为本章中所介绍的解释器的解释语言。在下文中将称这个子集为Small BASIC。
本章选择了这个类BASIC语言有3方面原因。第一,BASIC语言最初正是为解释执行而设计的。因此,实现一个BASIC解释器相对比较容易。例如,BASIC语言的早期版本不支持局部变量、递归方法、语句块、类、重载等特征—— 但是以上所有这些特性都将增加BASIC语言的复杂性。虽然缺少了许多功能,但是解释BASIC子集的基本原理同样适用于其他语言。理解本章中介绍的解释器代码,能够为开发其他语言解释器打下基础。选择BASIC的第二个原因是,可以用相对较小的代码量实现一个较为合理的子集。第三,早期的BASIC语言语法简单、容易掌握,几乎不需要用额外的时间来学习。因此,即使一点都不了解传统的BASIC语言,也能够毫无困难地使用Small BASIC。
下面给出一个用Small BASIC编写的程序,可以看到,使用这种语言是多么的简单。即使从来没有见过传统风格的BASIC程序,也能够轻松理解其操作过程。
PRINT "A Simple Small BASIC Program"
FOR X = 1 TO 10
GOSUB 100
NEXT
END
100 PRINT X
RETURN
该程序运行后得到如下的输出结果:
A Simple Small BASIC Program
1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0
10.0
尽管在Small BASIC语言中关键字的含义几乎一目了然,但是本章仍将详细解释每个关键字。
最后,Small BASIC仿造的是早期的BASIC版本,它不同于后来出现的Visual Basic。事实上,Visual Basic与原始的BASIC几乎没有多少共同点。当然,只要掌握了这个解释器的工作原理,就可以改造它,使之能够解释所需要的任何语言或者变量
解释器概述
在开始之前有必要再次强调:本章介绍的解释器是一个源代码解释器。也就是说,解释器在执行时,每次读入一条语句,并且根据这条语句执行特定的操作;然后再读入下一条语句,依次类推。这与伪代码解释器是有所区别的,例如早期的Java运行时系统。两者的区别在于:源代码解释器直接对程序的源代码解释执行;而伪代码解释器先将程序的源代码转化为某种与机器无关的中间代码,然后再执行中间代码。相比之下,源代码解释器更易于创建,并且不需要一个独立的编译过程。
Small BASIC解释器包括两个主要的子系统:一个是表达式解析器,负责处理数字表达式;另一个是解释器,负责程序的实际执行。对于前者,可采用本书第2章所介绍的表达式解析器。但是在这里做了某些改进,使得解析器能够解析包含在程序语句中的数字表达式,而不是只能解析孤立的表达式。
解释器子系统和解析器子系统包含在同一个解释器类中,该类名为SBasic。尽管从理论上讲可以使用两个独立的类:一个包含解释器,另一个包含表达式解析器;但是将两者用同一个类来实现的代效率会更高,因为表达式解析器和解释器的代码是密不可分的。例如,两个子系统都操作保存着程序代码的同一个字符数组。如果将它们分别安排在两个类中,将会增加可观的额外开销,并导致性能上的损失和功能上的重复。此外,由于程序解释的任务繁重,而解析表达式只是其中的一部分,因此将整个解释机制包含在单个类中是很有意义的。
解释器执行时,每次从程序的源代码中读入一个标识符。如果读入的是关键字,解释器就按照该关键字的要求执行规定的操作。举例来说,当解释器读入一个PRINT后,它将打印PRINT之后的字符;当读入一个GOSUB时,它就执行指定的子程序。在到达程序的结尾之前,这个过程将反复进行。可以看到,解释器只是简单地执行程序指定的动作。
Small BASIC解释器
Small BASIC解释器的代码相当长—— 一般情况下不会将这么长的代码放在书的一章之中。但是不要被它的长度所吓倒。抛开其长度不谈,这个解释器的概念其实比较简单,只要掌握了解释器的一般模式,就能轻松理解它的每个部分。
接下来给出Small BASIC解释器的完整代码。本章剩下的篇幅将详细解释它的工作原理和使用方法。
// A Small BASIC Interpreter.import java.io.*;import java.util.*;// Exception class for interpreter errors.class InterpreterException extends Exception {String errStr; // describes the error public InterpreterException(String str) { errStr = str; } public String toString() { return errStr; } } // The Small BASIC interpreter.class SBasic { final int PROG_SIZE = 10000; // maximum program size// These are the token types. final int NONE = 0; final int DELIMITER = 1; final int VARIABLE = 2; final int NUMBER = 3; final int COMMAND = 4; final int QUOTEDSTR = 5;// These are the types of errors. final int SYNTAX = 0; final int UNBALPARENS = 1; final int NOEXP = 2; final int DIVBYZERO = 3; final int EQUALEXPECTED = 4; final int NOTVAR = 5; final int LABELTABLEFULL = 6; final int DUPLABEL = 7; final int UNDEFLABEL = 8; final int THENEXPECTED = 9; final int TOEXPECTED = 10; final int NEXTWITHOUTFOR = 11; final int RETURNWITHOUTGOSUB = 12; final int MISSINGQUOTE = 13;final int FILENOTFOUND = 14; final int FILEIOERROR = 15; final int INPUTIOERROR = 16;// Internal representation of the Small BASIC keywords.final int UNKNCOM = 0;final int PRINT = 1;final int INPUT = 2;final int IF = 3;final int THEN = 4;final int FOR = 5;final int NEXT = 6;final int TO = 7;final int GOTO = 8;final int GOSUB = 9;final int RETURN = 10;final int END = 11;final int EOL = 12;// This token indicates end-of-program. final String EOP = "\0"; // Codes for double-operators, such as <=.final char LE = 1;final char GE = 2;final char NE = 3;// Array for variables. private double vars[];// This class links keywords with their keyword tokens.class Keyword { String keyword; // string form int keywordTok; // internal representation Keyword(String str, int t) {keyword = str;keywordTok = t;}}/* Table of keywords with their internal representation.All keywords must be entered lowercase. */Keyword kwTable[] = { new Keyword("print", PRINT), // in this table. new Keyword("input", INPUT),new Keyword("if", IF),new Keyword("then", THEN),new Keyword("goto", GOTO),new Keyword("for", FOR),new Keyword("next", NEXT),new Keyword("to", TO),new Keyword("gosub", GOSUB),new Keyword("return", RETURN),new Keyword("end", END)};private char[] prog; // refers to program arrayprivate int progIdx; // current index into programprivate String token; // holds current token private int tokType; // holds token's type private int kwToken; // internal representation of a keyword// Support for FOR loops.class ForInfo {int var; // counter variable double target; // target value int loc; // index in source code to loop to }// Stack for FOR loops.private Stack fStack;// Defines label table entries.class Label {String name; // label int loc; // index of label's location in source file public Label(String n, int i) {name = n;loc = i;}}// A map for labels.private TreeMap labelTable;// Stack for gosubs.private Stack gStack;// Relational operators.char rops[] = {GE, NE, LE, '<', '>', '=', 0};/* Create a string containing the relationaloperators in order to make checking forthem more convenient. */String relops = new String(rops);// Constructor for SBasic.public SBasic(String progName) throws InterpreterException {char tempbuf[] = new char[PROG_SIZE];int size;// Load the program to execute.size = loadProgram(tempbuf, progName);if(size != -1) {// Create a properly sized array to hold the program.prog = new char[size];// Copy the program into program array.System.arraycopy(tempbuf, 0, prog, 0, size);}}// Load a program.private int loadProgram(char[] p, String fname)throws InterpreterException{int size = 0;try {FileReader fr = new FileReader(fname);BufferedReader br = new BufferedReader(fr);size = br.read(p, 0, PROG_SIZE);fr.close();} catch(FileNotFoundException exc) {handleErr(FILENOTFOUND);} catch(IOException exc) {handleErr(FILEIOERROR);}
// If file ends with an EOF mark, back up.if(p[size-1] == (char) 26) size--;return size; // return size of program}// Execute the program.public void run() throws InterpreterException {// Initialize for new program run.vars = new double[26]; fStack = new Stack();labelTable = new TreeMap();gStack = new Stack();progIdx = 0;scanLabels(); // find the labels in the program sbInterp(); // execute}// Entry point for the Small BASIC interpreter.private void sbInterp() throws InterpreterException{// This is the interpreter's main loop.do {getToken();// Check for assignment statement.if(tokType==VARIABLE) {putBack(); // return the var to the input stream assignment(); // handle assignment statement }else // is keywordswitch(kwToken) {case PRINT:print();break;case GOTexecGoto();break;case IF:execIf();break;case FOR:execFor();break;case NEXT:next();break;case INPUT:input();break;case GOSUB:gosub();break;case RETURN:greturn();break;case END:return;}} while (!token.equals(EOP));}// Find all labels. private void scanLabels() throws InterpreterException{int i;Object result;// See if the first token in the file is a label.getToken();if(tokType==NUMBER) labelTable.put(token, new Integer(progIdx));findEOL();do { getToken();if(tokType==NUMBER) {// must be a line numberresult = labelTable.put(token,new Integer(progIdx));if(result != null)handleErr(DUPLABEL);}// If not on a blank line, find next line. if(kwToken != EOL) findEOL();} while(!token.equals(EOP));progIdx = 0; // reset index to start of program}// Find the start of the next line.private void findEOL(){while(progIdx < prog.length &&prog[progIdx] != '\n') ++progIdx;if(progIdx < prog.length) progIdx++;}// Assign a variable a value.private void assignment() throws InterpreterException{int var;double value;char vname;// Get the variable name.getToken();vname = token.charAt(0);if(!Character.isLetter(vname)) {handleErr(NOTVAR);return;}// Convert to index into variable table.var = (int) Character.toUpperCase(vname) - 'A';// Get the equal sign.getToken();if(!token.equals("=")) {handleErr(EQUALEXPECTED);return;}// Get the value to assign.value = evaluate();// Assign the value.vars[var] = value;}// Execute a simple version of the PRINT statement.private void print() throws InterpreterException{double result;int len=0, spaces;String lastDelim = "";do {getToken(); // get next list item if(kwToken==EOL || token.equals(EOP)) break;if(tokType==QUOTEDSTR) { // is stringSystem.out.print(token);len += token.length();getToken();}else { // is expressionputBack();result = evaluate();getToken();System.out.print(result);// Add length of output to running total.Double t = new Double(result);len += t.toString().length(); // save length}lastDelim = token;// If comma, move to next tab stop.if(lastDelim.equals(",")) {// compute number of spaces to move to next tabspaces = 8 - (len % 8); len += spaces; // add in the tabbing positionwhile(spaces != 0) { System.out.print(" ");spaces--;}}else if(token.equals(";")) {System.out.print(" ");len++;}else if(kwToken != EOL && !token.equals(EOP))handleErr(SYNTAX); } while (lastDelim.equals(";") || lastDelim.equals(","));if(kwToken==EOL || token.equals(EOP)) {if(!lastDelim.equals(";") && !lastDelim.equals(","))System.out.println();}else handleErr(SYNTAX); }
// Execute a GOTO statement. private void execGoto() throws InterpreterException{Integer loc;getToken(); // get label to go to // Find the location of the label.loc = (Integer) labelTable.get(token);if(loc == null)handleErr(UNDEFLABEL); // label not defined else // start program running at that locprogIdx = loc.intValue();}// Execute an IF statement.private void execIf() throws InterpreterException{double result;result = evaluate(); // get value of expression/* If the result is true (non-zero),process target of IF. Otherwise move onto next line in the program. */if(result != 0.0) { getToken();if(kwToken != THEN) {handleErr(THENEXPECTED);return;} // else, target statement will be executed }else findEOL(); // find start of next line }// Execute a FOR loop. private void execFor() throws InterpreterException{ForInfo stckvar = new ForInfo();double value;char vname;getToken(); // read the control variable vname = token.charAt(0);if(!Character.isLetter(vname)) {handleErr(NOTVAR);return;}// Save index of control var.stckvar.var = Character.toUpperCase(vname) - 'A';getToken(); // read the equal signif(token.charAt(0) != '=') {handleErr(EQUALEXPECTED);return;}value = evaluate(); // get initial value vars[stckvar.var] = value;getToken(); // read and discard the TOif(kwToken != TO) handleErr(TOEXPECTED);stckvar.target = evaluate(); // get target value/* If loop can execute at least once,push info on stack. */if(value >= vars[stckvar.var]) { stckvar.loc = progIdx;fStack.push(stckvar);}else // otherwise, skip loop code altogetherwhile(kwToken != NEXT) getToken();}// Execute a NEXT statement. private void next() throws InterpreterException{ForInfo stckvar;try {// Retrieve info for this For loop.stckvar = (ForInfo) fStack.pop();vars[stckvar.var]++; // increment control var// If done, return.if(vars[stckvar.var] > stckvar.target) return; // Otherwise, restore the info.fStack.push(stckvar);progIdx = stckvar.loc; // loop } catch(EmptyStackException exc) {handleErr(NEXTWITHOUTFOR);}}// Execute a simple form of INPUT. private void input() throws InterpreterException{int var;double val = 0.0;String str;BufferedReader br = newBufferedReader(new InputStreamReader(System.in));getToken(); // see if prompt string is present if(tokType == QUOTEDSTR) {// if so, print it and check for comma System.out.print(token);getToken();if(!token.equals(",")) handleErr(SYNTAX);getToken();}else System.out.print("? "); // otherwise, prompt with ?// get the input var var = Character.toUpperCase(token.charAt(0)) - 'A';try {str = br.readLine();val = Double.parseDouble(str); // read the value} catch (IOException exc) {handleErr(INPUTIOERROR);} catch (NumberFormatException exc) {/* You might want to handle this errordifferently than the other interpretererrors. */System.out.println("Invalid input.");}vars[var] = val; // store it }// Execute a GOSUB. private void gosub() throws InterpreterException{Integer loc;getToken();// Find the label to call.loc = (Integer) labelTable.get(token); if(loc == null)handleErr(UNDEFLABEL); // label not defined else {// Save place to return to.gStack.push(new Integer(progIdx));// Start program running at that loc.progIdx = loc.intValue();}}// Return from GOSUB.private void greturn() throws InterpreterException{Integer t;try {// Restore program index.t = (Integer) gStack.pop();progIdx = t.intValue();} catch(EmptyStackException exc) {handleErr(RETURNWITHOUTGOSUB);}}// **************** Expression Parser ****************// Parser entry point. private double evaluate() throws InterpreterException{ double result = 0.0; getToken(); if(token.equals(EOP)) handleErr(NOEXP); // no expression present // Parse and evaluate the expression. result = evalExp1(); putBack(); return result; } // Process relational operators. private double evalExp1() throws InterpreterException{double l_temp, r_temp, result;char op;result = evalExp2();// If at end of program, return.if(token.equals(EOP)) return result;op = token.charAt(0); if(isRelop(op)) {l_temp = result;getToken();r_temp = evalExp1();switch(op) { // perform the relational operation case '<':if(l_temp < r_temp) result = 1.0;else result = 0.0;break;case LE:if(l_temp <= r_temp) result = 1.0;else result = 0.0;break;case '>':if(l_temp > r_temp) result = 1.0;else result = 0.0;break;case GE:if(l_temp >= r_temp) result = 1.0;else result = 0.0;break;case '=':if(l_temp == r_temp) result = 1.0;else result = 0.0;break;case NE:if(l_temp != r_temp) result = 1.0;else result = 0.0;break;}}return result;}
// Add or subtract two terms. private double evalExp2() throws InterpreterException { char op; double result; double partialResult; result = evalExp3(); while((op = token.charAt(0)) == '+' || op == '-') { getToken(); partialResult = evalExp3(); switch(op) { case '-': result = result - partialResult; break; case '+': result = result + partialResult; break; } } return result; } // Multiply or divide two factors. private double evalExp3() throws InterpreterException { char op; double result; double partialResult; result = evalExp4(); while((op = token.charAt(0)) == '*' || op == '/' || op == '%') { getToken(); partialResult = evalExp4(); switch(op) { case '*': result = result * partialResult; break; case '/': if(partialResult == 0.0) handleErr(DIVBYZERO); result = result / partialResult; break; case '%': if(partialResult == 0.0) handleErr(DIVBYZERO); result = result % partialResult; break; } } return result; } // Process an exponent. private double evalExp4() throws InterpreterException { double result; double partialResult; double ex; int t; result = evalExp5(); if(token.equals("^")) { getToken(); partialResult = evalExp4(); ex = result; if(partialResult == 0.0) { result = 1.0; } else for(t=(int)partialResult-1; t > 0; t--) result = result * ex; } return result; } // Evaluate a unary + or -. private double evalExp5() throws InterpreterException { double result; String op; op = ""; if((tokType == DELIMITER) && token.equals("+") || token.equals("-")) { op = token; getToken(); } result = evalExp6(); if(op.equals("-")) result = -result; return result; } // Process a parenthesized expression. private double evalExp6() throws InterpreterException { double result; if(token.equals("(")) { getToken(); result = evalExp2(); if(!token.equals(")")) handleErr(UNBALPARENS); getToken(); } else result = atom(); return result; } // Get the value of a number or variable. private double atom() throws InterpreterException { double result = 0.0; switch(tokType) { case NUMBER: try { result = Double.parseDouble(token); } catch (NumberFormatException exc) { handleErr(SYNTAX); } getToken(); break; case VARIABLE: result = findVar(token); getToken(); break; default: handleErr(SYNTAX); break; } return result; }
// Return the value of a variable. private double findVar(String vname)throws InterpreterException { if(!Character.isLetter(vname.charAt(0))){ handleErr(SYNTAX); return 0.0; } return vars[Character.toUpperCase(vname.charAt(0))-'A']; } // Return a token to the input stream. private void putBack() { if(token == EOP) return; for(int i=0; i < token.length(); i++) progIdx--; } // Handle an error. private void handleErr(int error)throws InterpreterException { String[] err = { "Syntax Error", "Unbalanced Parentheses", "No Expression Present", "Division by Zero","Equal sign expected","Not a variable","Label table full","Duplicate label","Undefined label","THEN expected","TO expected","NEXT without FOR","RETURN without GOSUB","Closing quotes needed","File not found","I/O error while loading file","I/O error on INPUT statement"}; throw new InterpreterException(err[error]); } // Obtain the next token. private void getToken() throws InterpreterException{ char ch;tokType = NONE; token = ""; kwToken = UNKNCOM;// Check for end of program. if(progIdx == prog.length) {token = EOP; return; } // Skip over white space. while(progIdx < prog.length && isSpaceOrTab(prog[progIdx])) progIdx++;// Trailing whitespace ends program. if(progIdx == prog.length) { token = EOP; tokType = DELIMITER;return; } if(prog[progIdx] == '\r') { // handle crlfprogIdx += 2;kwToken = EOL;token = "\r\n";return;}// Check for relational operator.ch = prog[progIdx];if(ch == '<' || ch == '>') {if(progIdx+1 == prog.length) handleErr(SYNTAX);switch(ch) {case '<':if(prog[progIdx+1] == '>') {progIdx += 2;;token = String.valueOf(NE);}else if(prog[progIdx+1] == '=') {progIdx += 2;token = String.valueOf(LE);}else {progIdx++;token = "<";}break;case '>':if(prog[progIdx+1] == '=') {progIdx += 2;;token = String.valueOf(GE);}else {progIdx++;token = ">";}break;}tokType = DELIMITER;return;}if(isDelim(prog[progIdx])) {// Is an operator. token += prog[progIdx]; progIdx++; tokType = DELIMITER; } else if(Character.isLetter(prog[progIdx])) { // Is a variable or keyword.while(!isDelim(prog[progIdx])) { token += prog[progIdx]; progIdx++; if(progIdx >= prog.length) break; } kwToken = lookUp(token);if(kwToken==UNKNCOM) tokType = VARIABLE; else tokType = COMMAND;}else if(Character.isDigit(prog[progIdx])) {// Is a number.while(!isDelim(prog[progIdx])) { token += prog[progIdx]; progIdx++; if(progIdx >= prog.length) break; } tokType = NUMBER; } else if(prog[progIdx] == '"') {// Is a quoted string.progIdx++;ch = prog[progIdx];while(ch !='"' && ch != '\r') {token += ch;progIdx++;ch = prog[progIdx];}if(ch == '\r') handleErr(MISSINGQUOTE);progIdx++; tokType = QUOTEDSTR;}else { // unknown character terminates programtoken = EOP; return; } } // Return true if c is a delimiter. private boolean isDelim(char c) { if((" \r,;<>+-/*%^=()".indexOf(c) != -1)) return true; return false; } // Return true if c is a space or a tab.boolean isSpaceOrTab(char c) {if(c == ' ' || c =='\t') return true;return false;}// Return true if c is a relational operator.boolean isRelop(char c) {if(relops.indexOf(c) != -1) return true;return false;}/* Look up a token's internal representation in thetoken table. */private int lookUp(String s){int i;// Convert to lowercase.s = s.toLowerCase();// See if token is in table.for(i=0; i < kwTable.length; i++)if(kwTable[i].keyword.equals(s))return kwTable[i].keywordTok;return UNKNCOM; // unknown keyword}}
Small BASIC表达式解析器
Small BASIC解释器的核心是表达式解析器。如前所述,Small BASIC采用第2章介绍的解析器。没有阅读过第2章的读者请先读完第2章,因为该章提供了解析器的详细描述。本章保留其基本操作,但需要进一步的改进。
对解析器所做的许多改动主要是为了能够处理Small BASIC语言的语法。例如,它必须能够识别Small BASIC语言的关键字,比如不能将等号(=)作为一个运算符处理,还必须能够处理关系运算符。本章中的解释器充分增强了getToken()的功能,使其能够满足扩展之后的功能要求。本章对解析器的其他改动都是出于效率方面的考虑。例如,在第2章中将表达式的一个引用传递给解析器。但是在Small BASIC解析器中,指向源程序的引用保存在一个实例变量中,并且由解释器和解析器共享。这样可以避免传递引用时所引起的额外开销。由于解释器的执行速度慢是其特征之一,因此在效率方面的提高非常重要。
另一处变化是由于操作对象的不同引起的。在Small BASIC中,解析器处理的不再是字符串,而是字符数组。在第2章介绍的解析器中,传递的是一个包含表达式的字符串。引起这种变化的原因仍然是效率问题。众所周知,程序一般保存在一个普通文本文件中。这个文件是一段字符序列,而不是一个字符串。因此,在程序执行之前,当解释器载入源程序时,它将从文件中读入一个字符数组。尽管这个字符数组可以转换为一个字符串,但是这种转换会带来不必要的效率损失。
由于Small BASIC表达式解析器使用的技术与第2章中介绍的完全相同,因此理解它的操作不会很难,所以本章不打算详细解释它的代码。然而,在讲解释器之前,还必须给出几点综合的说明。下面首先解释Small BASIC中表达式的准确含义。
3.4.1 Small BASIC的表达式
在本章介绍的Small BASIC解释器中,表达式由下列元素组成:
● 整数
● 运算符+ – / * ^ = () < > >= <= <>
● 变量
在Small BASIC中,“^”表示求幂运算。“=”可用作赋值运算符和等号,然而相对于BASIC表达式而言,它只能用于关系表达式。(在标准BASIC语言中,赋值是一条语句而不是一项操作)。“<>”代表不等号。这些元素可以根据代数学的规则组合成表达式。这里是几个表达式的例子:
7 – 8
(100 – 5) * 14/6
a + b – c
10 ^ 5
A < B
这些运算符的优先级关系如表3-1所示。
3-1 运算符的优先级
最高优先级最低优先级
优先级相同的运算符从左向右进行运算。
Small BASIC做了如下的假设:
● 所有的变量都以单个字母命名,也就是说只有从A到Z的26个字母可用作变量名。
● 变量名不区分大小写。即“a”与“A”是同一变量。
● 所有的数字都是double类型.
● 尽管在向屏幕输出消息的时候可能会引用一些字符串常量,但是不支持字符串变量。
这些假设被应用到解析器的实现过程中。
3.4.2 Small BASIC的标识符
Small BASIC解析器的核心是getToken()方法。这里的getToken()方法是第2章中所介绍的getToken()方法的增强版本。其中新增的功能使解析器不仅能够识别数值表达式,还能够识别Small BASIC中的关键字和字符串等其他语言元素。
在Small BASIC中,每个关键字都有两种形式:外部格式和内部格式。外部格式是程序员用于编写程序的文本形式。例如“PRINT”就是PRINT这个关键字的外部格式。尽管可以以外部字符串的形式在解释器中表示标识符,但是通常没人会这么做,因为它的效率实在太低。与之相反,Small BASIC对标识符的内部格式进行操作。内部格式是一种简单的整数值,例如,整数1代表PRINT命令;整数2代表INPUT命令。内部表示法的优势在于:由整数编写的代码比使用字符串编写的代码执行速度快很多。实际上,getToken()的工作就是将标识符从外部格式转换到相应的内部格式。
下面是Small BASIC解释器的getToken()方法。它的执行贯穿解释器执行的整个过程,每次处理一个字符。
// Obtain the next token.private void getToken() throws InterpreterException{ char ch;
tokType = NONE; token = ""; kwToken = UNKNCOM;
// Check for end of program. if(progIdx == prog.length) {token = EOP; return; }
// Skip over white space. while(progIdx < prog.length && isSpaceOrTab(prog[progIdx])) progIdx++;
// Trailing whitespace ends program. if(progIdx == prog.length) { token = EOP; tokType = DELIMITER;return; }
if(prog[progIdx] == '\r') { // handle crlfprogIdx += 2;kwToken = EOL;token = "\r\n";return;}
// Check for relational operator.ch = prog[progIdx];if(ch == '<' || ch == '>') {if(progIdx+1 == prog.length) handleErr(SYNTAX);
switch(ch) {case '<':if(prog[progIdx+1] == '>') {progIdx += 2;;token = String.valueOf(NE);}else if(prog[progIdx+1] == '=') {progIdx += 2;token = String.valueOf(LE);}else {progIdx++;token = "<";}break;case '>':if(prog[progIdx+1] == '=') {progIdx += 2;;token = String.valueOf(GE);}else {progIdx++;token = ">";}break;}tokType = DELIMITER;return;}
解释器
SBasic的解释器部分是实际执行程序的代码。一般而言,解释执行一个Small BASIC程序的工作相当容易,因为每个语句(赋值语句除外)都会以一个关键字开头。因此,解释器的工作就是从每行程序代码的开头获得关键字,然后执行关键字所指定的操作。解释器不断重复这个过程,直到解释完整个程序。在本节剩下的篇幅中,将详细分析解释器的每个组成部分。
3.5.1 InterpreterException类
解释器代码文件首先定义了InterpreterException类。当解释过程发生错误时就会抛出该类型的异常。使用SBasic的代码必须处理这个异常。语法错误、I/O错误和数值表达式中的错误都能引起该异常。
3.5.2 SBasic构造函数
下面列出SBasic的构造函数:
// Constructor for SBasic. public SBasic(String progName) throws InterpreterException { char tempbuf[] = new char[PROG_SIZE]; int size; // Load the program to execute. size = loadProgram(tempbuf, progName); if(size != -1) { // Create a properly sized array to hold the program. prog = new char[size]; // Copy the program into program array. System.arraycopy(tempbuf, 0, prog, 0, size); } }
SBasic的构造函数接受一个文件名作为参数,指定需要解释的Small BASIC程序源文件。然后创建一个临时缓冲区,以保存读入的源文件。这个缓冲区的大小由PROG_SIZE指定,这个值已经被强制设置为10,000。这也是一个SBasic所能解释的程序的最大长度。在必要的时候,可以方便地改变这个数值。
接下来,构造函数调用loadProgram()方法,该方法读入源程序并返回其字符个数,如果读取失败则返回–1。然后构造函数创建一个长度与该程序长度相同的数组,并将它的一个引用赋给变量prog。最后整个程序被复制到这个新的队列。因此,prog指向的数组的长度与源程序的长度是完全相同的。
loadProgram()方法的代码如下:
// Load a program. private int loadProgram(char[] p, String fname) throws InterpreterException { int size = 0; try { FileReader fr = new FileReader(fname); BufferedReader br = new BufferedReader(fr); size = br.read(p, 0, PROG_SIZE); fr.close(); } catch(FileNotFoundException exc) { handleErr(FILENOTFOUND); } catch(IOException exc) { handleErr(FILEIOERROR); } // If file ends with an EOF mark, back up. if(p[size-1] == (char) 26) size--; return size; // return size of program }
这个方法的大部分代码都简单易懂,但请特别注意这几行:
// If file ends with an EOF mark, back up. if(p[size-1] == (char) 26) size--;
正如注释中说明的那样,这行代码丢弃了代表文件结束的EOF标志。众所周知,有些文本编辑器在文件结尾附加一个文件结束标记(通常是值为26的一个ASCII码),然而有些编辑器不附加任何标记。loadProgram()的做法是:判断文件是否有结束标志,如果有就将它删除,这样能同时处理两种情况。3.5.3 关键字
Small BASIC解释器只能解释BASIC语言的一个子集,包括以下关键字:
在SBasic中,这些关键字和行结束符EOL的内部表示分别以final类型进行声明,如下所示:
// Internal representation of the Small BASIC keywords. final int UNKNCOM = 0; final int PRINT = 1; final int INPUT = 2; final int IF = 3; final int THEN = 4; final int FOR = 5; final int NEXT = 6; final int TO = 7; final int GOTO = 8; final int GOSUB = 9; final int RETURN = 10; final int END = 11; final int EOL = 12;
注意UNKNCOM这个变量。lookUp()方法使用这个值来表示未知的关键字。
为了便于将关键字从外部表示转换为相应的内部表示,所有的外部表示和内部表示都保存在一个名为kwTable的表中。这个表由Keyword对象所组成。Keyword类和kwTable的定义如下所示:
// This class links keywords with their keyword tokens. class Keyword { String keyword; // string form int keywordTok; // internal representation Keyword(String str, int t) { keyword = str; keywordTok = t; } } /* Table of keywords with their internal representation. All keywords must be entered lowercase. */ Keyword kwTable[] = { new Keyword("print", PRINT), // in this table. new Keyword("input", INPUT), new Keyword("if", IF), new Keyword("then", THEN), new Keyword("goto", GOTO), new Keyword("for", FOR), new Keyword("next", NEXT), new Keyword("to", TO), new Keyword("gosub", GOSUB), new Keyword("return", RETURN), new Keyword("end", END) };
lookup()方法就是使用kwTable表,将关键字标识符转换为相应的内部表示。如果kwTable表中没有匹配标识符,lookup()将返回UNKNCOM。lookup()方法的代码如下:
/* Look up a token's internal representation in the token table. */ private int lookUp(String s) { int i; // Convert to lowercase. s = s.toLowerCase(); // See if token is in table. for(i=0; i < kwTable.length; i++) if(kwTable.keyword.equals(s)) return kwTable.keywordTok; return UNKNCOM; // unknown keyword }
3.5.4 run()方法
SBasic对象创建之后,解析器调用run()方法执行它先前读入的程序。run()的代码如下所示:
// Execute the program. public void run() throws InterpreterException { // Initialize for new program run. vars = new double[26]; fStack = new Stack(); labelTable = new TreeMap(); gStack = new Stack(); progIdx = 0; scanLabels(); // find the labels in the program sbInterp(); // execute }
run()方法首先分配4个数据结构:一个保存变量值的数组,一个保存FOR循环的堆栈,一个保存标签的树图(TreeMap)和一个保存GOSUB子函数的堆栈。接下来progIdx变量被置为0,它保存着程序当前被解释的位置。每次调用run()方法时,这些域都会被重新设置,这样解释器就能够重复解释执行相同的程序。
然后调用的是scanLabels()方法,它扫描整个程序,找到所有的标签。每找到一个标签,解释器都把这个标签连同它的位置一起保存到labelTable图中。这些操作可以在程序执行之前加快程序的执行速度。
最后,解析器调用sbInterp()方法开始程序的执行过程
3.5.5 sbInterp()方法
解释器通过sbInterp()开始并控制一个Small BASIC程序的解释执行过程。
下面是这个方法的代码:
// Entry point for the Small BASIC interpreter. private void sbInterp() throws InterpreterException { // This is the interpreter's main loop. do { getToken(); // Check for assignment statement. if(tokType==VARIABLE) { putBack(); // return the var to the input stream assignment(); // handle assignment statement } else // is keyword switch(kwToken) { case PRINT: print(); break; case GOT execGoto(); break; case IF: execIf(); break; case FOR: execFor(); break; case NEXT: next(); break; case INPUT: input(); break; case GOSUB: gosub(); break; case RETURN: greturn(); break; case END: return; } } while (!token.equals(EOP)); }
所有的解释器都由一个顶层循环结构所控制,该循环从源程序中读入下一个标识符,然后选择适当的动作进行处理。Small BASIC解释器也不例外,这个主循环包含在sbInterp()方法中。它的工作过程是这样的:首先,从程序中读入一个标识符。假设程序中没有语法错误,如果读入的标识符是一个变量,那么将进行一次赋值操作。如果不是变量,那么这个标识符不是一个行号(将被忽略),就是一个关键字。如果是关键字,则解释器将根据kwToken的值选择一条合适的case语句,kwToken中记录着所有关键字的内部表示。每个关键字都由它自己的方法处理,接下来将依次介绍这些方法。
3.5.6 赋值
在传统的BASIC语言中,赋值是一条语句而不是一个操作。在Small BASIC当中也同样用这样的方法处理赋值。BASIC赋值语句的一般形式如下:
变量名=表达式
解释器调用assignment()方法处理赋值语句,这里给出该方法的实现代码:
// Assign a variable a value. private void assignment() throws InterpreterException { int var; double value; char vname; // Get the variable name. getToken(); vname = token.charAt(0); if(!Character.isLetter(vname)) { handleErr(NOTVAR); return; } // Convert to index into variable table. var = (int) Character.toUpperCase(vname) - 'A'; // Get the equal sign. getToken(); if(!token.equals("=")) { handleErr(EQUALEXPECTED); return; } // Get the value to assign. value = evaluate(); // Assign the value. vars[var] = value; }
assignment()首先从程序中读入一个需要赋值的标识符。如果该标识符不是一个有效的变量,那么解释器将报告NOTVAR错误。接下来将读入一个等号。然后调用evaluate()方法获得将赋给变量的值。最后再把这个值赋给变量。这个方法要比想像中的更为简单清晰,因为表达式解析器和getToken()方法已经解决了大部分比较麻烦的工作。 3.5.9 GOTO语句
在传统的BASIC语言中,最重要的程序控制形式是GOTO语句。GOTO语句的目标必须是一个行号。在Small BASIC语言中也保留了这一传统处理方法。有些人可能知道,早期的BASIC版本要求程序中的每行代码都必须以一个行号开始。但是Small BASIC不再要求每行都有行号;只有当某一行是GOTO语句的目标语句时,它才需要一个行号。因此,在Small BASIC中,行号仅仅只是一个标签。GOTO语句的一般形式如下:
GOTO 行号
当解释器遇到一个GOTO语句时,执行过程必须跳转到与行号相对应的代码行。处理GOTO语句的主要问题在于:必须同时提供向前跳转和向后跳转。为了有效地解决这个问题,要求在执行之前就必须扫描整个程序,并将扫描到的每个标签的位置存储在一张表中。那么,以后每次执行GOTO语句时,就可以从这张表中获得目标代码行的位置,并将程序执行点转移到该位置。
TreeMap集合是一种存储标签及其位置的理想的数据结构,因为每个树图都有一个与特定值关联的键。在解释器中,可以将标签指定为键,将键值指定为标签索引。每个树图保存一个标签/索引对。程序声明了一个名为labelTable的实例变量,由它完成对树图的引用。labelTable的声明如下:
// A map for labels. private TreeMap labelTable;
scanLabels()方法扫描整个程序,并将每个标签的位置插入TreeMap表中。在程序执行之前,由run()方法调用scanLabels()进行最后预处理。而findEOL()方法用来查找一行程序代码的结尾。下面是scanLabels()和findEOL()的代码:
// Find all labels. private void scanLabels() throws InterpreterException { int i; Object result; // See if the first token in the file is a label. getToken(); if(tokType==NUMBER) labelTable.put(token, new Integer(progIdx)); findEOL(); do { getToken(); if(tokType==NUMBER) { // must be a line number result = labelTable.put(token, new Integer(progIdx)); if(result != null) handleErr(DUPLABEL); } // If not on a blank line, find next line. if(kwToken != EOL) findEOL(); } while(!token.equals(EOP)); progIdx = 0; // reset index to start of program } // Find the start of the next line. private void findEOL() { while(progIdx < prog.length && prog[progIdx] != '\n') ++progIdx; if(progIdx < prog.length) progIdx++; }
scanLabels()方法检查每行代码的第一个标识符。如果该标识符是一个数字,那么就认为这是一个行号(或者说,是一个标签)。发现标签后,scanLabels()就调用put()方法将其存储到labelTable中。接着调用TreeMap的put()方法返回一个引用,指向该标签以前的映射。如果先前没有任何映射,那么就返回null。因此,如果返回值不为null,那么说明该标签已经存储在树图中。这将导致一个标签重复的错误,因为在一个程序中不允许出现两个相同的标签。
解释器调用execGoto()方法处理程序中出现的GOTO语句。代码如下所示:
// Execute a GOTO statement. private void execGoto() throws InterpreterException { Integer loc; getToken(); // get label to go to // Find the location of the label. loc = (Integer) labelTable.get(token); if(loc == null) handleErr(UNDEFLABEL); // label not defined else // start program running at that loc progIdx = loc.intValue(); }
首先,通过下面这行代码获得与目标标签相关联的位置:
loc = (Integer) labelTable.get(token);
TreeMap的get()方法返回与键相关联的值。如前所述,这个键就是标签,而键值就是该标签在程序中的索引。如果找不到标签,get()方法将返回null。如果找到标签,那么标签的值将被赋给progIdx。之后,程序将从progIdx所指示的新位置继续执行。(请记住:progIdx是程序当前执行代码的索引。)
3.5.10 IF语句
Small BASIC解释器可执行较为简单的IF语句。在Small BASIC语言中,不允许使用ELSE语句(但是,在理解IF语句执行的操作之后就会发现,增加ELSE语句其实非常简单)。IF语句的一般形式如下:
IF 表达式 rel-op 表达式 THEN 语句
在这里,rel-op必须是某种关系操作符。例如,X < 10就是一个可用于IF语句的有效的关系表达式。只有当关系表达式为真,才会执行THEN之后的语句。
下面的execIf()方法负责IF语句的执行,其代码如下所示:
// Execute an IF statement. private void execIf() throws InterpreterException { double result; result = evaluate(); // get value of expression /* If the result is true (non-zero), process target of IF. Otherwise move on to next line in the program. */ if(result != 0.0) { getToken(); if(kwToken != THEN) { handleErr(THENEXPECTED); return; } // else, target statement will be executed } else findEOL(); // find start of next line }
execIf()方法的执行过程是这样的:首先计算关系表达式的值。如果该表达式的值为真,则执行THEN语句块中的目标语句;否则调用findEOL()方法找到下一行代码的开始位置。需要注意的是,如果表达式值为真,那么execIf()方法不会执行任何操作,而只是返回主循环。于是主程序重复执行,读入下一个标识符,开始新的解释过程。由于IF语句的目标是一条语句,因此返回主循环后,解释器会执行这条目标语句,如同不经过IF语句判断一样。如果表达式值为假,那么execIf()方法先要找到下一行代码的开始位置,然后才返回主循环。
3.5.11 FOR循环
FOR循环的实现提出了一个具有挑战性的问题,程序员可以用它编写出相当优美的程序。FOR循环的一般形式如下:
FOR 循环变量=初值 TO 终值
需要重复执行的代码
NEXT
在Small BASIC语言中,FOR语句只允许循环正向运行,也就是说每经过一次循环就把循环变量的值加1。同时,FOR语句也不支持STEP命令。
在BASIC、Java等语言中,循环可以嵌套,构成多层循环。嵌套循环的主要问题在于需要保存与每层循环直接相关的信息(也就是说,每个NEXT必须与正确的FOR相对应)。为了解决这个问题,在此使用一种基于堆栈的机制实现FOR循环。在最外层循环中,有关循环变量、终值和外层循环在程序中的位置等状态信息被压入堆栈。以后每次遇到NEXT时,就将它们弹出并更新控制变量,重新检查终值。如果循环变量的值超过终值,就终止循环,执行NEXT语句之后的其他代码。相反地,如果循环变量没有超过终值,就将更新过的信息重新压回堆栈中,程序仍然在外层循环中进行。采用这种方法实现的FOR循环不仅可以处理单层循环,而且可以处理嵌套循环,因为最内层的NEXT总是对应着最内层的FOR语句(也就是说,最后被压入堆栈中的信息将被最先弹出)。一旦内层循环结束,那么该循环对应的信息就从堆栈中弹出。如果还存在外层循环,那么外层循环的信息就上升到堆栈的栈顶位置。这样,每个NEXT就能够自动关联与之对应的FOR语句。
在这里使用堆栈保存与循环相关的信息。为此,Small BASIC采用Java的一个集合类:Stack。循环信息包含在ForInfo类的一个对象中。另外,解释器还声明了名为fStack的实例变量指向这个堆栈。ForInfo类是这样描述的:
// Support for FOR loops. class ForInfo { int var; // counter variable double target; // target value int loc; // index in source code to loop to } // Stack for FOR loops. private Stack fStack;
Stack类支持push()和pop()方法,它们的功能分别是将一个对象压入栈顶和从栈顶弹出一个对象。
execFor()方法处理FOR语句,而next()方法处理NEXT语句。它们是这样实现的:
// Execute a FOR loop. private void execFor() throws InterpreterException { ForInfo stckvar = new ForInfo(); double value; char vname; getToken(); // read the control variable vname = token.charAt(0); if(!Character.isLetter(vname)) { handleErr(NOTVAR); return; } // Save index of control var. stckvar.var = Character.toUpperCase(vname) - 'A'; getToken(); // read the equal sign if(token.charAt(0) != '=') { handleErr(EQUALEXPECTED); return; } value = evaluate(); // get initial value vars[stckvar.var] = value; getToken(); // read and discard the TO if(kwToken != TO) handleErr(TOEXPECTED); stckvar.target = evaluate(); // get target value /* If loop can execute at least once, push info on stack. */ if(value >= vars[stckvar.var]) { stckvar.loc = progIdx; fStack.push(stckvar); } else // otherwise, skip loop code altogether while(kwToken != NEXT) getToken(); } // Execute a NEXT statement. private void next() throws InterpreterException { ForInfo stckvar; try { // Retrieve info for this For loop. stckvar = (ForInfo) fStack.pop(); vars[stckvar.var]++; // increment control var // If done, return. if(vars[stckvar.var] > stckvar.target) return; // Otherwise, restore the info. fStack.push(stckvar); progIdx = stckvar.loc; // loop } catch(EmptyStackException exc) { handleErr(NEXTWITHOUTFOR); } }
通过阅读代码中相关的注释,可以更好地理解这些方法的操作过程。下面简单介绍一下。execFor()方法构造了一个ForInfo对象,用来保存循环控制变量的索引、循环终值和progIdx的当前值等信息。接着它将ForInfo对象压入fStack中。然后程序继续执行,直到遇到NEXT语句。这时,fStack出栈,将弹出的循环终值与循环控制变量的当前值做比较。如果控制变量的值小于终值,那么就将保存在loc变量中的索引赋给progIdx,然后进入下一次循环。当然,这个索引就是循环在程序中的位置。于是执行过程返回FOR语句,将刚才的过程再重复执行一遍。如果控制变量的值大于等于终值,那么执行过程在遇到NEXT语句之后,只需继续执行NEXT之后的语句。
检查上面的代码,可以发现它并没有在FOR循环中禁止使用GOTO语句以跳出循环。然而,使用GOTO语句跳出循环会破坏保存循环信息的堆栈,因此应该避免这种情况的发生。
这种基于堆栈的FOR循环解决方案具有普遍的应用价值。尽管Small BASIC语言没有实现其他的循环语句,但是可以将相同的方法应用到任何其他类型的循环语句中,比如WHILE或者DO/WHILE循环。同时,基于堆栈的解决方案可以用于解决任何可嵌套的语言元素问题,比如子程序调用。您将在下一节中看到这一点
3.5.12 GOSUB
尽管Small BASIC不支持真正的独立子程序,但是程序员可以使用GOSUB关键字调用程序的某一部分。此外,从子程序返回时要用到另一个关键字RETURN。GOSUB和RETURN的一般形式如下所示:
GOSUB 行号
……
行号
子程序代码
RETURN
调用子程序,即使调用Small BASIC中有限的子程序,也需要使用堆栈。其原因与FOR语句要求使用堆栈的原因大体相同。Small BASIC允许子程序进行嵌套调用,也就是说一个子程序可能会调用另一个子程序。因此必须使用堆栈,保证将一个RETURN语句与其对应的GOSUB关联起来。GOSUB堆栈保存在另一个Stack对象中,这里定义了一个gStack变量保存该Stack对象的引用:
// Stack for gosubs. private Stack gStack;
gStack中保存着记录程序位置的索引。在解释过程中,每次遇到GOSUB关键字时,就将程序的当前索引压入gStack的顶部;相应地,每次解释执行到RETURN语句时,就从堆栈中弹出返回位置的索引。
下面给出gosub()和return()方法的代码:
// Execute a GOSUB. private void gosub() throws InterpreterException { Integer loc; getToken(); // Find the label to call. loc = (Integer) labelTable.get(token); if(loc == null) handleErr(UNDEFLABEL); // label not defined else { // Save place to return to. gStack.push(new Integer(progIdx)); // Start program running at that loc. progIdx = loc.intValue(); } } // Return from GOSUB. private void greturn() throws InterpreterException { Integer t; try { // Restore program index. t = (Integer) gStack.pop(); progIdx = t.intValue(); } catch(EmptyStackException exc) { handleErr(RETURNWITHOUTGOSUB); } }
下面介绍GOSUB语句的工作流程。当遇到GOSUB关键字时,调用gosub()方法处理—— 首先查找目标代码行的行号,并将其保存在变量loc中。接着,progIdx的当前值被压入GOSUB堆栈的栈项(当子程序执行完毕时,程序将要返回到这个点上来执行)。最后,将loc中保存的索引赋给progIdx。于是程序就转移到子程序的开始位置,从这里继续执行。当遇到RETURN关键字时,调用return()方法处理——首先将GOSUB堆栈的栈项元素弹出,然后将弹出的值赋给progIdx,于是程序就从GOSUB语句的下一条语句开始继续执行。
由于返回地址在GOSUB堆栈中保存,因此子程序允许嵌套调用。当执行到RETURN语句时,返回的将是最后被调用的子程序(也就是说,最近被调用的子程序的返回地址处于gstack堆栈的顶部)。从理论上来说,这个过程允许GOSUB嵌套任意多层。
3.5.13 END语句
END关键字表示程序执行的结束。但并不是所有程序都需要END语句,因为程序的结尾也能终止程序的执行。程序使用END可以在文件结束之前终止程序的执行。它只是简单地将sbInterp()方法返回,从而终止程序的执行Small BASIC的使用
使用SBasic类时,首先必须创建一个SBasic对象,并指定需要解释的文件名,然后调用该对象的run()方法。请一定记住捕获可能抛出的任何InterpreterExceptions异常。
下面这个程序允许用户从命令行指定并运行任何的Small BASIC程序:
// Demonstrate the Small BASIC Interpreter. class SBDemo { public static void main(String args[]) { if(args.length != 1) { System.out.println("Usage: sbasic <filename>"); return; } try { SBasic ob = new SBasic(args[0]); ob.run(); } catch(InterpreterException exc) { System.out.println(exc); } } }
编译SBDemo的命令是:
javac SBasic.java SBDemo.java
运行SBDemo时,必须以某个Small BASIC程序名作为第一个命令行参数。假设需要解释执行的程序名为TEST.BAS,那么在命令行中输入:
java SBDemo TEST.BAS
可以以下面这个简单的Small BASIC程序为例进行测试:
PRINT "This program converts gallons to liters." 100 GOSUB 200 INPUT "Again? (1 or 0): ", x IF x = 1 THEN GOTO 100 END 200 INPUT "Enter gallons: ", g l = g * 3.7854 PRINT g; "gallons is"; l; "liters." RETURN
这个程序执行后将产生如下输出:
This program converts gallons to liters. Enter gallons: 10 10.0 gallons is 37.854 liters. Again? (1 or 0): 1 Enter gallons: 4 4.0 gallons is 15.1416 liters. Again? (1 or 0): 0
更多的Small BASIC程序
本节将给出一些Small BASIC示例代码段。注意,Small BASIC可同时支持大小写。换而言之,Small BASIC是不区分大小写的。因此,输入的关键字和变量既可以是大写,也可以是小写。除了本章给出的示例程序之外,读者也可以自己编写几个小程序进行测试。此外,还可以编写一些存在语法错误的程序,然后观察Small BASIC的报错机制。
下面这个程序具备了Smalll BASIC支持的所有特性:
PRINT "This program demonstrates all features." FOR X = 1 TO 100 PRINT X; X/2, X; X*X NEXT GOSUB 300 PRINT "hello" INPUT H IF H<11 THEN GOTO 200 PRINT 12-4/2 PRINT 100 200 A = 100/2 IF A>10 THEN PRINT "this is ok" PRINT A PRINT A+34 INPUT H PRINT H INPUT "this is a test ", y PRINT H+Y END 300 PRINT "this is a subroutine" RETURN
下面这个程序演示了子程序的嵌套调用过程:
PRINT "This program demonstrates nested GOSUBs." INPUT "enter a number: ", I GOSUB 100 END 100 FOR T = 1 TO I X = X + I GOSUB 150 NEXT RETURN 150 PRINT X; RETURN
接下来这个程序详细演示了INPUT语句的用法:
PRINT "This program computes the volume of a cube." INPUT "Enter length of first side ", l INPUT "Enter length of second side ", w INPUT "Enter length of third side ", d t = l * w * d PRINT "Volume is ", t
下面这个程序演示了嵌套的FOR循环:
PRINT "This program demonstrates nested FOR loops." FOR X = 1 TO 100 FOR Y = 1 TO 10 PRINT X; Y; X*Y NEXT NEXT
最后这个程序则使用了所有的关系运算符:
PRINT "This demonstrates all of the relational operators." A = 10 B = 20 IF A = B THEN PRINT "A = B" IF A <> B THEN PRINT "A <> B" IF A < B THEN PRINT "A < B" IF A > B THEN PRINT "A > B" IF A >= B THEN PRINT "A >= B" IF A <= B THEN PRINT "A <= B"
对解释器进行增强和扩展
向Small BASIC解释器中增加语句并不复杂,可以仿照本章中已列出代码的一般格式。如果要增加不同的变量类型,那么需要创建一个新的类以保存该类型和变量的值,然后再使用该类型的对象数组来保存变量。
创建自己的计算机语言
对Small BASIC解释器进行增强或扩展,是熟悉Small BASIC操作和语言解释器工作原理的一种好方法。但是您不必局限于BASIC语言。采用本章中描述的解释器技术,可以编写出任何计算机语言的解释器,包括Java语言的一个简化子集,甚至可以发明能反映自己编程风格和个性的语言。事实上,Small BASIC所采用的解释器框架,就是程序员可能遇到的对于任何类型的语言特征的理想的“测试集”。举例来说,向解释器中增加REPEAT/UNTIL循环语句需要遵循如下步骤:
(1) 添加REPEAT和UNTIL关键字,并为它们定义相应的整型值;
(2) 在主循环中的switch语句中添加REPEAT和UNTIL语句;
(3) 定义repeat()和until()方法分别处理REPEAT和UNTIL语句(首先调用execForc()和next())。
对于那些喜欢挑战的程序员来说,可以创建一种脚本语言,以自动执行不同的计算任务,例如复制或者删除文件、编译程序等。然后为该语言创建一个解释器。这样可以提供一种选择,来替代标准的批处理文件,实质上,您可以对解释器进行修改,从而完成定制的批处理任务。
转载于:https://blog.51cto.com/youxue/47612