本节书摘来异步社区《Java学习指南》一书中的第1章,第1.4节,作者:【美】Patrick Niemeyer , Daniel Leuck,更多章节内容可以访问云栖社区“异步社区”公众号查看。
1.4 设计安全
Java被设计为一种安全语言,对于这一事实你肯定早已耳熟能详了。但是在此“安全”指的是什么呢?对什么而言安全,或者对谁安全呢?对于Java,得到颇多关注的安全性是那些使新型动态可移植软件成为可能的有关特性。Java提供了多层保护以避免恶意代码,并防止诸如病毒和特洛伊木马等更具危险性的东西。在下一节中,我们将查看Java虚拟机体系结构如何在代码运行前评估其安全性,还将介绍Java类加载器(Java解释器的字节码加载机制)如何在不可信类周围加筑围墙。这些特性为高级安全性策略提供了基础,从而可以在每个应用的基础上允许或禁止各种操作。
不过,在本节中,我们将了解Java编程语言的一些通用特性。较之于特定的安全特性,Java通过解决通用设计和编程问题所提供的安全性可能更为重要,但在安全性讨论中这一点往往被忽视了。Java力图做到尽可能安全,即不仅要“抵制”我们自己所犯的简单错误,而且还要避免由原有软件所遗传的错误。Java的目标是保持语言的简单性,并提供展示其有用性的工具,同时令用户可以在需要时基于该语言构建更为复杂的功能。
1.4.1 语法简单性
Java有着简单性的原则。因为Java出身清白,它可以避免那些在其他语言中已经证实为糟糕或有争议的那些特性。例如,Java不允许程序员自定义操作符重载(overloading),而在某些语言中,允许程序员重新定义+和-这样的基本操符号的含义。Java没有源代码预处理器,因此没有宏、#define语句或条件源编译。这些在其他语言中存在的构造主要是为了支持平台依赖性,因此从这个意义上讲,它们在Java中是不需要的。条件编译通常还用于调试,但是Java的高级运行时优化以及断言这样的功能,较为优雅地解决了该问题。(我们将在第4章中讨论有关内容)。
Java为组织类文件提供了一个定义良好的包结构。此包系统允许编译器处理传统make实用工具的某些功能(make是用于将源代码构建为可执行代码的一个工具)。编译器还可以直接处理已编译Java类,因为所有类型信息都得到了保留;在此无需“头文件”,这一点与C或C++ 有所不同。所有这些都意味着Java代码需要读取的上下文环境信息更少。实际上,你有时可能会发现查看Java源代码比参考类文档更为快捷。
对于在其他语言中遭遇麻烦的一些特性,Java则将其取而代之。例如,Java只支持单一的类继承层次体系(每个类只能有一个“父”类),但是允许对接口多重继承。接口类似于C++ 中的一个抽象类,可以指定一个对象的多个操作,但是不会定义其实现,这是一个功能强大的机制,它允许开发者为对象定义一个“契约”,任何具体的对象实现都可以使用并引用该契约。Java中的接口消除了类的多重继承需求,同时不会导致与多重继承相关的问题。在第4章中你将会看到,Java是一种简单而又优雅的编程语言,而这仍然是它最大的吸引力。
1.4.2 类型安全和方法绑定
语言的一大属性是其采用何种类型检查。一般地,在将一种语言划归为“静态”或“动态”时,我们所指的是:有关变量类型的信息究竟是在编译时更多地得到明确,还是直至应用运行时方能更多地加以确定。
在诸如C或C++ 这样的严格静态类型语言中,数据类型在编译源代码时即已固化。这有利于编译器得到足够的信息,从而在代码执行前就能捕获多种错误,例如,编译器不会允许你在一个整数变量中保存一个浮点值。这样,代码将不再需要运行时类型检查,因此可以编译为小而快速的可执行代码。但是静态类型语言不够灵活。它们不能支持诸如集合的高级构造,而这些构造对于带有动态类型检查的语言则相当自然,另外对于静态类型语言而言,应用在运行时也不可能安全地导入新的数据类型。
与此相反,诸如Smalltalk或Lisp等动态语言则有一个运行时系统,可以管理对象的类型,并在应用执行时完成必要的类型检查。这些语言允许更为复杂的操作,另外在许多方面,其功能也更为强大。不过,它们往往速度较慢,不太安全,同时也较难调试。
语言之间的差别可以比作不同汽车之间的差别1。静态类型语言(如C++)可以比作跑车,相当安全,速度也很快,但是只有在柏油大道上才能很好地奔驰。动态性很好的语言(如Smalltalk)则更像是越野车:它们可以提供更大的自由度,但是稍难操控。也许在丛林里驾驶着它驰骋相当有趣(有时也更快),但是有时则未免会陷入壕沟或者遭到熊的袭击。
语言的另一个属性是采用何种方式将方法调用绑定至其定义。在诸如C或C++这样的语言中,方法的定义通常在编译时绑定,除非程序员特别指出。Smalltalk则有所不同,它被称为是一种“延迟绑定”(late-binding)语言,因为它在运行时才会动态地确定方法的定义。出于性能方面的原因,早期绑定(early-binding)相当重要;如此可以运行应用,而不会有运行时搜索方法所带来的开销。但是延迟绑定更为灵活。另外在面向对象语言中,这也是必要的,在此子类可以覆盖其超类中的方法,而且只有运行时系统才能确定应当运行哪个方法。
Java博采了C++ 和Smalltalk的优点,它是一种静态类型、延迟绑定的语言。Java中的每个对象都有一个编译时即已确定的定义良好的类型。这说明,Java编译器可以像是在C++中一样,完成同样的静态类型检查和使用分析。因此,你无法给对象赋予错误的变量类型,也不能在一个对象上调用不存在的方法。更有甚者,Java编译器还可以防止使用未初始化的变量以及创建不会执行的语句(请见第4章)。
不过,Java同时也完全可以做到在运行时确定类型。Java运行时系统会跟踪所有对象,并使得在执行时确定其类型和关系成为可能。这说明,可以在运行时检查一个对象以确定它究竟是什么。与C或C++不同的是,将一种对象类型强制转换为另一种类型时,要由运行时系统加以检查,而且有可能使用新型的动态加载对象(具有一定类型安全性的)。另外,由于Java是一种延迟绑定语言,一个子类总是有可能覆盖其超类中的方法,即使这是一个运行时加载的子类。
1.4.3 递增开发
Java从其源代码中将所有数据类型和方法签名信息带入到其编译后的字节码形式中。这就意味着,Java类可以递增地进行开发。你自己的Java类也可以安全地与来自于其他来源(编译器从未见过此来源)的类一同使用。换句话说,可以编写新的代码来引用二进制类文件,而不会丢失从源代码所得到的类型安全性。
困扰C++ 的一个常见问题是“脆弱基类”问题(fragile base class)。在C++ 中,由于一个基类有多个派生类,因此其实现可能被有效地“冻结”了;修改基类可能需要重新编译所有的派生类。对于类库的开发人员来说,这个问题尤其困难。Java通过在类中动态地定位字段,从而避免了这一问题。只要类维护了其原始结构的一个合法形式,那么就可以对其加以改进,而不会对由该类派生或使用了该类的其他类造成破坏。
1.4.4 动态内存管理
Java和C(C++)这样的低级语言之间的一些最为重要的差别涉及到Java如何管理内存。Java取消了可以引用内存的任意部分的临时的指针,并且为语言增加了垃圾回收和高级数组。这些特性消除了有关安全性、可移植性和优化的许多问题,否则这些问题将很难解决。
垃圾回收本身就可以使无数的程序员免于进行显式的内存分配和释放,而这在C或C++ 中也最容易导致错误。除了在内存中维护对象外,Java运行时系统还记录了对这些对象的所有引用。只要某个对象不再使用,Java即会自动地将其从内存中删除。你只需在不再使用对象时将其忽略,并确信解释器在适当的时候会予以清除。
Java使用了一个复杂的垃圾回收器,它在后台间歇性地运行,这意味着大多数垃圾回收工作均发生在空闲时间里,即介于I/O暂停、鼠标点击或按键之间。高级的运行时系统(如HotSpot)则可完成更高级的垃圾回收工作,甚至可以区分对象的使用模式(如对短期对象和长期对象加以区别),并且可以优化其收集过程。Java运行时现在可以自动调整自身,以便针对不同的应用程序,根据其行为来优化内存的分配。通过这种运行时探查,自动化的内存管理比最勤奋的程序员所管理的资源也要快很多,而某些老派的程序员仍然对此难以置信。
你可能听说过Java没有指针。严格地说,这种说法是正确的,但是它也会带来误导。Java所提供的是引用(reference),这是一种“安全型”指针,而且在Java中,引用是相当普遍的。引用是对象的一个强类型句柄。除了基本数字类型之外,Java中的所有对象都可以通过引用来访问。如果必要的话,可以使用引用来构建所有一般的数据结构,如链表、树等等,对于这些数据结构,以往C程序员惯用的做法是采用指针来构建。唯一的区别在于利用引用必须以一种类型安全的方式来操作。
在引用和指针间还有一个重要的区别,即无法通过引用更改其值(执行指针的算术运算)。引用只能指向特定的对象或某个数组中的元素。引用是一个原子性事物;除非将引用赋给一个对象,否则无法操作引用的值。引用采用传值方式传递,而且引用一个对象时,间接层不能多于一层。对引用的保护是Java安全性中最基本的一个方面。这说明,Java代码必须“按规章办事”,即不得“越权”行事。
不同于C或C++ 指针,Java引用只能指向类的类型。在此不存在指向方法的指针。人们有时会对此有所抱怨,但是你会发现,若任务需要方法指针,那么大多数时候,采用接口和适配器类会更为漂亮地将其完成。另外还需提到一点,Java有一个复杂的“反射(reflection)”API,这确实允许你引用和调用单个的方法。不过,这并不是常规做法。我们将在第7章讨论反射。
最后,Java中的数组是真正的头等(first-class)对象。它们可以像其他对象一样动态地分配和赋值。数组知道其自己的大小和类型,而且尽管你无法直接定义或派生数组类的子类,但是基于其基类型的关系,它们确实有一个定义良好的继承关系。语言中若拥有真正的数组,则可以消除C或C++等语言中对指针算术运算的需求。
1.4.5 错误处理
Java的出发点在于网络化设备和嵌入式系统。对于这些应用,拥有健壮而且智能的错误管理机制是至关重要的。Java有一个强大的异常处理机制,这一点有些类似于C++ 的最新实现。异常提供了一个更为自然和优雅的方式来处理错误。异常可以将错误处理代码从一般的代码中分离出来,从而得到更为简洁、更具可读性的应用。
出现一个异常时,将导致程序执行流程转移到一个提前指定的“捕获”代码块。异常附带有一个对象,其中包含有导致出现异常的情形的相关信息。Java编译器要求方法所声明的异常要么是其能够生成的,要么是可以自行捕获和处理的。这将错误信息的重要性,提高到与参数(argument)和返回类型相同的层次。作为一个Java程序员,应当清楚地知道哪些异常情况需要处理,而且编译器还有助于你编写正确的软件,从而不会让这些异常“放任自流”而未加处理。
1.4.6 线程
如今的应用都需要高度的并行性。即使一个非常简单的应用也可能有一个复杂的用户界面,而这就需要并发的活动。随着机器速度越来越快,用户对于为完成无关任务而占用时间的现象也越来越敏感。线程为客户和服务器应用提供了高效的多处理和任务分配机制。Java使得线程很易于使用,因为其对线程的支持是内置于语言中的。
并发性固然很好,但是采用线程编程所做的不仅仅是同时完成多项任务。在大多数情况下,线程需要得到同步(协调),如果没有显式的语言支持则会相当棘手。Java基于监视器和条件模型可以支持同步,这是一种用于访问资源的加锁和钥匙系统。关键字synchronized指定方法和代码块要在对象内得到安全、串行化的访问。也存在一些简单的基本方法,从而可以在对同一对象加以处理的线程之间显式地等待和标记。
Java还有一个高级的并发包,它提供了强大的工具来解决多线程编程中的常见模式,例如,线程池、任务的协调以及复杂的锁定。通过这个并发包和相关的工具,Java提供了一些比任何其他语言都更为高级的线程相关工具。
尽管一些开发者可能永远不必编写多线程代码,但学习使用线程编程,是掌握Java编程的一个重要部分,这是所有程序员都应该掌握的内容。参见第9章关于这个主题的更多讨论。
1.4.7 可伸缩性
在最低的层次上,Java程序由类组成。类被设计为小型的模块化组件。在类之上,Java提供了包,这是一个结构层,它将类分组为功能单元。包为类的组织提供了一个命名约定,另外还对Java应用中变量和方法的可见性提供了另一级组织控制。
在一个包中,类可以是公开可见的,也可能有所保护以避免外部访问。包构成了另一种类型的作用域,它与应用级更为接近。这有助于构建能够在系统中协同工作的可复用组件。包还有助于设计一个可伸缩的应用,从而在扩展应用时,代码不至于过于相互依赖。