资源描述
皮晨晖 软件081班
Object landscapes and lifetimes
Technically, OOP is just about abstract data typing, inheritance, and polymorphism, but other issues can be at least as important. The remainder of this section will cover these issues.
One of the most important factors is the way objects are created and destroyed. Where is the data for an object and how is the lifetime of the object controlled? There are different philosophies at work here. C++ takes the approach that control of efficiency is the most important issue, so it gives the programmer a choice. For maximum run-time speed, the storage and lifetime can be determined while the program is being written, by placing the objects on the stack (these are sometimes called automatic or scoped variables) or in the static storage area. This places a priority on the speed of storage allocation and release, and control of these can be very valuable in some situations. However, you sacrifice flexibility because you must know the exact quantity, lifetime, and type of objects while you're writing the program. If you are trying to solve a more general problem such as computer-aided design, warehouse management, or air-traffic control, this is too restrictive.
The second approach is to create objects dynamically in a pool of memory called the heap. In this approach, you don't know until run-time how many objects you need, what their lifetime is, or what their exact type is. Those are determined at the spur of the moment while the program is running. If you need a new object, you simply make it on the heap at the point that you need it. Because the storage is managed dynamically, at run-time, the amount of time required to allocate storage on the heap is significantly longer than the time to create storage on the stack. (Creating storage on the stack is often a single assembly instruction to move the stack pointer down, and another to move it back up.) The dynamic approach makes the generally logical assumption that objects tend to be complicated, so the extra overhead of finding storage and releasing that storage will not have an important impact on the creation of an object. In addition, the greater flexibility is essential to solve the general programming problem.
Java uses the second approach, exclusively]. Every time you want to create an object, you use the new keyword to build a dynamic instance of that object.
There's another issue, however, and that's the lifetime of an object. With languages that allow objects to be created on the stack, the compiler determines how long the object lasts and can automatically destroy it. However, if you create it on the heap the compiler has no knowledge of its lifetime. In a language like C++, you must determine programmatically when to destroy the object, which can lead to memory leaks if you don’t do it correctly (and this is a common problem in C++ programs). Java provides a feature called a garbage collector that automatically discovers when an object is no longer in use and destroys it. A garbage collector is much more convenient because it reduces the number of issues that you must track and the code you must write. More important, the garbage collector provides a much higher level of insurance against the insidious problem of memory leaks (which has brought many a C++ project to its knees).
The rest of this section looks at additional factors concerning object lifetimes and landscapes.
1. The singly rooted hierarchy
One of the issues in OOP that has become especially prominent since the introduction of C++ is whether all classes should ultimately be inherited from a single base class. In Java (as with virtually all other OOP languages) the answer is “yes” and the name of this ultimate base class is simply Object. It turns out that the benefits of the singly rooted hierarchy are many.
All objects in a singly rooted hierarchy have an interface in common, so they are all ultimately the same type. The alternative (provided by C++) is that you don’t know that everything is the same fundamental type. From a backward-compatibility standpoint this fits the model of C better and can be thought of as less restrictive, but when you want to do full-on object-oriented programming you must then build your own hierarchy to provide the same convenience that’s built into other OOP languages. And in any new class library you acquire, some other incompatible interface will be used. It requires effort (and possibly multiple inheritance) to work the new interface into your design. Is the extra “flexibility” of C++ worth it? If you need it—if you have a large investment in C—it’s quite valuable. If you’re starting from scratch, other alternatives such as Java can often be more productive.
All objects in a singly rooted hierarchy (such as Java provides) can be guaranteed to have certain functionality. You know you can perform certain basic operations on every object in your system. A singly rooted hierarchy, along with creating all objects on the heap, greatly simplifies argument passing (one of the more complex topics in C++).
A singly rooted hierarchy makes it much easier to implement a garbage collector (which is conveniently built into Java). The necessary support can be installed in the base class, and the garbage collector can thus send the appropriate messages to every object in the system. Without a singly rooted hierarchy and a system to manipulate an object via a reference, it is difficult to implement a garbage collector.
Since run-time type information is guaranteed to be in all objects, you’ll never end up with an object whose type you cannot determine. This is especially important with system level operations, such as exception handling, and to allow greater flexibility in programming.
2 .Collection libraries and support for easy collection use
Because a container is a tool that you’ll use frequently, it makes sense to have a library of containers that are built in a reusable fashion, so you can take one off the shelf Because a container is a tool that you’ll use frequently, it makes sense to have a library of containers that are built in a reusable fashion, so you can take one off the shelf and plug it into your program. Java provides such a library, which should satisfy most needs.
Downcasting vs. templates/generics
To make these containers reusable, they hold the one universal type in Java that was previously mentioned: Object. The singly rooted hierarchy means that everything is an Object, so a container that holds Objects can hold anything. This makes containers easy to reuse.
To use such a container, you simply add object references to it, and later ask for them back. But, since the container holds only Objects, when you add your object reference into the container it is upcast to Object, thus losing its identity. When you fetch it back, you get an Object reference, and not a reference to the type that you put in. So how do you turn it back into something that has the useful interface of the object that you put into the container?
Here, the cast is used again, but this time you’re not casting up the inheritance hierarchy to a more general type, you cast down the hierarchy to a more specific type. This manner of casting is called downcasting. With upcasting, you know, for example, that a Circle is a type of Shape so it’s safe to upcast, but you don’t know that an Object is necessarily a Circle or a Shape so it’s hardly safe to downcast unless you know that’s what you’re dealing with.
It’s not completely dangerous, however, because if you downcast to the wrong thing you’ll get a run-time error called an exception, which will be described shortly. When you fetch object references from a container, though, you must have some way to remember exactly what they are so you can perform a proper downcast.
Downcasting and the run-time checks require extra time for the running program, and extra effort from the programmer. Wouldn’t it make sense to somehow create the container so that it knows the types that it holds, eliminating the need for the downcast and a possible mistake? The solution is parameterized types, which are classes that the compiler can automatically customize to work with particular types. For example, with a parameterized container, the compiler could customize that container so that it would accept only Shapes and fetch only Shapes.
Parameterized types are an important part of C++, partly because C++ has no singly rooted hierarchy. In C++, the keyword that implements parameterized types is “template.” Java currently has no parameterized types since it is possible for it to get by—however awkwardly—using the singly rooted hierarchy. However, a current proposal for parameterized types uses a syntax that is strikingly similar to C++ templates.
对象的创建和存在时间
从技术角度说,OOP(面对对象程序设计)只是包括抽象的数据类型、继承以及多形性,但另某些问题也也许显得非常重要。本节将就这些问题进行探讨。
最重要的问题之一是对象的创建及破坏方式。对象需要的数据位于哪儿,怎样控制对象的“存在时间”呢?针对这个问题,处理的方案是各异其趣的。C++以为程序的执行效率是最重要的一个问题,因此它允许程序员作出选择。为取得最快的运行速度,存储以及存在时间可在编写程序时决定,只需将对象放置在堆栈(有时也叫作自动或定域变量)或者静态存储区域即可。这么便为存储空间的分派和释放提供了一个优先级。某些情况下,这种优先级的控制是非常有价值的。然而,我们同时也牺牲了灵活性,因为在编写程序时,必须懂得对象的准确的数量、存在时间、以及类型。假如要处理的是一个较常规的问题,如计算机辅助设计、仓储管理或者空中交通控制,这一措施就显得太局限了。
第二个措施是在一个内存池中动态创建对象,该内存池亦叫“堆”或者“内存堆”。若采取这种方式,除非进入运行期,否则根本不懂得到底需要多少个对象,也不懂得它们的存在时间有多长,以及准确的类型是什么。这些参数都在程序正式运行时才决定的。若需一个新对象,只需在需要它的时候在内存堆里简单地创建它即可。因为存储空间的管理是运行期间动态进行的,因此在内存堆里分派存储空间的时间比在堆栈里创建的时间长得多(在堆栈里创建存储空间一般只需要一个简单的指令,将堆栈指针向下或向下移动即可)。因为动态创建措施使对象本来就倾向于复杂,因此查找存储空间以及释放它所需的额外开销不会为对象的创建导致明显的影响。除此以外,更大的灵活性对于常规编程问题的处理是至关重要的。
C++允许我们决定是在写程序时创建对象,还是在运行期间创建,这种控制措施愈加灵活。大家或许以为既然它如此灵活,那么无论怎样都应在内存堆里创建对象,而不是在堆栈中创建。
但还要考虑另外一个问题,亦即对象的“存在时间”或者“生存时间”(Lifetime)。若在堆栈或者静态存储空间里创建一个对象,编译器会判断对象的连续时间有多长,届时会自动“破坏”或者“清除”它。程序员可用两种措施来破坏一个对象:用程序化的方式决定何时破坏对象,或者利用由运行环境提供的一个“垃圾搜集器”特性,自动寻找那些不再使用的对象,并将其清除。当然,垃圾搜集器显得以便得多,但要求所有应用程序都必须容忍垃圾搜集器的存在,并能默许随垃圾搜集带来的额外开销。但这并不符合C++语言的设计宗旨,因此未能包括到C++里。但Java确实提供了一个垃圾搜集器(Smalltalk也有这么的设计;尽管Delphi默以为没有垃圾搜集器,但可选择安装;而C++亦可使用某些由其他企业开发的垃圾搜集产品)。
本节剩余的部分将讨论操纵对象时要考虑的另某些原因。
1 单根结构
在面对对象的程序设计中,因为C++的引入而显得尤为突出的一个问题是:所有类最后是否都应从单独一个基础类继承。在Java中(与其他几乎所有OOP语言同样),对这个问题的答案都是肯定的,并且这个终级基础类的名字很简单,就是一个“Object”。这种“单根结构”具备许多方面的优点。
单根结构中的所有对象都有一个通用接口,因此它们最后都属于相同的类型。另一个方案(就象C++那样)是我们不能确保所有东西都属于相同的基本类型。从向后兼容的角度看,这一方案可与C模型愈加好地配合,并且能够以为它的限制更少某些。但假期我们想进行纯粹的面对对象编程,那么必须构建自己的结构,以期取得与内建到其他OOP语言里的同样的便利。需添加我们要用到的各种新类库,还要使用另某些不兼容的接口。理所当然地,这也需要付出额外的精力使新接口与自己的设计方案配合(也许还需要多重继承)。为得到C++额外的“灵活性”,付出这么的代价值得吗?当然,假如真的需要——假如早已是C教授,假如对C有难舍的情结——那么就真的很值得。但假如你是一名新手,初次接触此类设计,象Java那样的替代方案也许会更省事某些。
单根结构中的所有对象(例如所有Java对象)都能够确保拥有某些特定的功效。在自己的系统中,我们懂得对每个对象都能进行某些基本操作。一个单根结构,加上所有对象都在内存堆中创建,能够极大简化参数的传递(这在C++里是一个复杂的概念)。
利用单根结构,我们能够更以便地实现一个垃圾搜集器。与此有关的必要支持可安装于基础类中,而垃圾搜集器可将适当的消息发给系统内的任何对象。假如没有这种单根结构,并且系统通过一个句柄来操纵对象,那么实现垃圾搜集器的途径会有很大的不一样,并且会晤临许多障碍。
因为运行期的类型信息肯定存在于所有对象中,因此永远不会遇到判断不出一个对象的类型的情况。这对系统级的操作来说显得尤其重要,例如违例控制;并且也能在程序设计时取得更大的灵活性。
2 集合库与以便使用集合
因为集合是我们常常都要用到的一个工具,因此一个集合库是十分必要的,它应当能够以便地重复使用。这么一来,我们就能够以便地取用各种集合,将其插入自己的程序。Java提供了这么的一个库,尽管它在Java 1.0和1.1中都显得非常有限(Java 1.2的集合库则无疑是一个杰作)。
下溯造型与模板/通用性
为了使这些集合能够重复使用,或者“再生”,Java提供了一个通用类型,此前曾把它叫作“Object”。单根结构意味着、所有东西归根结底都是一个对象”!因此容纳了Object的一个集合实际能够容纳任何东西。这使我们对它的重复使用变得非常简便。
为使用这么的一个集合,只需添加指向它的对象句柄即可,以后能够通过句柄重新使用对象。但因为集合只能容纳Object,因此在我们向集合里添加对象句柄时,它会上溯造型成Object,这么便丢失了它的身份或者标识信息。再次使用它的时候,会得到一个Object句柄,而非指向我们早先置入的那个类型的句柄。因此怎样才能偿还它的本来面貌,调用早先置入集合的那个对象的有用接口呢?
在这里,我们再次用到了造型(Cast)。但这一次不是在分级结构中上溯造型成一个更“通用”的类型。而是下溯造型成一个更“特殊”的类型。这种造型措施叫作“下溯造型”(Downcasting)。举个例子来说,我们懂得在上溯造型的时候,Circle(圆)属于Shape(几何形状)的一个类型,因此上溯造型是安全的。但我们不懂得一个Object到底是Circle还是Shape,因此极难确保下溯造型的安全进行,除非确切地懂得自己要操作的是什么。
但这也不是绝对危险的,因为假如下溯造型成错误的东西,会得到我们称为“违例”(Exception)的一个运行期错误。我们稍后即会对此进行解释。但在从一个集合提取对象句柄时,必须用某种方式准确地记住它们是什么,以确保下溯造型的正确进行。
下溯造型和运行期检查都要求花额外的时间来运行程序,并且程序员必须付出额外的精力。既然如此,我们能不能创建一个“智能”集合,令其懂得自己容纳的类型呢?这么做可消除下溯造型的必要以及潜在的错误。答案是肯定的,我们能够采取“参数化类型”,它们是编译器能自动定制的类,可与特定的类型配合。例如,通过使用一个参数化集合,编译器可对那个集合进行定制,使其只接收Shape,并且只提取Shape。
参数化类型是C++一个重要的组成部分,这部分是C++没有单根结构的缘故。在C++中,用于实现参数化类型的核心字是template(模板)。Java目前尚未提供参数化类型,因为因为使用的是单根结构,因此使用它显得有些笨拙。但这并不能确保以后的版本不会实现,因为“generic”这个词已被Java“保存到将来实现”(在Ada语言中,“generic”被用来实现它的模板)。Java采取的这种核心字保存机制其实常常让人摸不着头脑,极难断定以后会发生什么事情。
展开阅读全文