Object landscapes and lifetimes

来源:互联网 发布:python有指针吗 编辑:程序博客网 时间:2024/06/05 19:49

Technically, OOP is just about abstract datatyping, inheritance, and polymorphism, but other issues can be at least asimportant. The remainder of this section will cover these issues.

One of the most important factors is the wayobjects are created and destroyed. Where is the data for an object and how isthe lifetime of the object controlled? There are different philosophies at workhere. C++ takes the approach that control of efficiency is the most importantissue, so it gives the programmer a choice. For maximum run-time speed, thestorage and lifetime can be determined while the program is being written, byplacing the objects on the stack (these are sometimes calledautomaticor scoped variables) or in the static storage area. This places apriority on the speed of storage allocation and release, and control of thesecan be very valuable in some situations. However, you sacrifice flexibilitybecause you must know the exact quantity, lifetime, and type of objects whileyou're writing the program. If you are trying to solve a more general problemsuch as computer-aided design, warehouse management, or air-traffic control,this is too restrictive.

The second approach is to create objectsdynamically in a pool of memory called the heap. In this approach, you don'tknow until run-time how many objects you need, what their lifetime is, or whattheir exact type is. Those are determined at the spur of the moment while theprogram is running. If you need a new object, you simply make it on the heap atthe point that you need it. Because the storage is managed dynamically, atrun-time, the amount of time required to allocate storage on the heap issignificantly longer than the time to create storage on the stack. (Creating storageon the stack is often a single assembly instruction to move the stack pointerdown, and another to move it back up.) The dynamic approach makes the generallylogical assumption that objects tend to be complicated, so the extra overheadof finding storage and releasing that storage will not have an important impacton the creation of an object. In addition, the greater flexibility is essentialto 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'sthe lifetime of an object. With languages that allow objects to be created onthe stack, the compiler determines how long the object lasts and canautomatically destroy it. However, if you create it on the heap the compilerhas no knowledge of its lifetime. In a language like C++, you must determineprogrammatically when to destroy the object, which can lead to memory leaks ifyou don’t do it correctly (and this is a common problem in C++ programs). Javaprovides a feature called a garbage collector that automatically discovers whenan object is no longer in use and destroys it. A garbage collector is much moreconvenient because it reduces the number of issues that you must track and thecode you must write. More important, the garbage collector provides a muchhigher level of insurance against the insidious problem of memory leaks (whichhas brought many a C++ project to its knees).

The rest of this section looks at additionalfactors concerning object lifetimes and landscapes.

1 Collections and iterators

If you don’t know howmany objects you’re going to need to solve a particular problem, or how longthey will last, you also don’t know how to store those objects. How can youknow how much space to create for those objects? You can’t, since thatinformation isn’t known until run-time.

The solution to mostproblems in object-oriented design seems flippant: you create another type ofobject. The new type of object that solves this particular problem holdsreferences to other objects. Of course, you can do the same thing with anarray, which is available in most languages. But there’s more. This new object,generally called acontainer (also called a collection, but theJava library uses that term in a different sense so this book will use“container”), will expand itself whenever necessary to accommodate everythingyou place inside it. So you don’t need to know how manyobjects you’re going tohold in a container. Just create a container object and let it take care of thedetails.

Fortunately, a good OOPlanguage comes with a set of containers as part of the package. In C++, it’spart of the Standard C++ Library and is sometimes called the Standard TemplateLibrary (STL). Object Pascal has containers in its Visual Component Library(VCL). Smalltalk has a very complete set of containers. Java also hascontainers in its standard library. In some libraries, a generic container isconsidered good enough for all needs, and in others (Java, for example) thelibrary has different types of containers for different needs: a vector (calledan ArrayListin Java) forconsistent access to all elements, and a linked list for consistent insertionat all elements, for example, so you can choose the particular type that fitsyour needs. Container libraries may also include sets, queues, hash tables,trees, stacks, etc.

All containers have someway to put things in and get things out; there are usually functions to addelements to a container, and others to fetch those elements back out. Butfetching elements can be more problematic, because a single-selection functionis restrictive. What if you want to manipulate or compare a set of elements inthe container instead of just one?

The solution is aniterator, which is an object whose job is to select the elements within acontainer and present them to the user of the iterator. As a class, it alsoprovides a level of abstraction. This abstraction can be used to separate thedetails of the container from the code that’s accessing that container. Thecontainer, via the iterator, is abstracted to be simply a sequence. Theiterator allows you to traverse that sequence without worrying about theunderlying structure—that is, whether it’s an ArrayList, a LinkedList,a Stack, or something else. Thisgives you the flexibility to easily change the underlying data structurewithout disturbing the code in your program. Java began (in version 1.0 and1.1) with a standard iterator, called Enumeration,for all of its container classes. Java 2 has added a much more completecontainer library that contains an iterator called Iterator that does more than the older Enumeration.

From a designstandpoint, all you really want is a sequence that can be manipulated to solveyour problem. If a single type of sequence satisfied all of your needs, there’dbe no reason to have different kinds. There are two reasons that you need achoice of containers. First, containers provide different types of interfacesand external behavior. A stack has a different interface and behavior than thatof a queue, which is different from that of a set or a list. One of these mightprovide a more flexible solution to your problem than the other. Second,different containers have different efficiencies for certain operations. Thebest example is an ArrayList anda LinkedList. Both are simplesequences that can have identical interfaces and external behaviors. Butcertain operations can have radically different costs. Randomly accessingelements in an ArrayList is aconstant-time operation; it takes the same amount of time regardless of theelement you select. However, in a LinkedListit is expensive to move through the list to randomly select an element, and ittakes longer to find an element that is further down the list. On the otherhand, if you want to insert an element in the middle of a sequence, it’s muchcheaper in a LinkedList than inan ArrayList. These and other operationshave different efficiencies depending on the underlying structure of thesequence. In the design phase, you might start with a LinkedList and, when tuning for performance, change to an ArrayList. Because of the abstractionvia iterators, you can change from one to the other with minimal impact on yourcode.

In the end, rememberthat a container is only a storage cabinet to put objects in. If that cabinetsolves all of your needs, it doesn’t really matter how it is implemented (abasic concept with most types of objects). If you’re working in a programmingenvironment that has built-in overhead due to other factors, then the costdifference between an ArrayListand a LinkedList might notmatter. You might need only one type of sequence. You can even imagine the“perfect” container abstraction, which can automatically change its underlyingimplementation according to the way it is used.

2 The singly rooted hierarchy

One of the issues in OOP that has becomeespecially prominent since the introduction of C++ is whether all classesshould ultimately be inherited from a single base class. In Java (as withvirtually all other OOP languages) the answer is “yes” and the name of thisultimate 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 havean interface in common, so they are all ultimately the same type. Thealternative (provided by C++) is that you don’t know that everything is thesame fundamental type. From a backward-compatibility standpoint this fits themodel of C better and can be thought of as less restrictive, but when you wantto do full-on object-oriented programming you must then build your ownhierarchy to provide the same convenience that’s built into other OOPlanguages. And in any new class library you acquire, some other incompatibleinterface 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 quitevaluable. If you’re starting from scratch, other alternatives such as Java canoften be more productive.

All objects in a singly rooted hierarchy(such as Java provides) can be guaranteed to have certain functionality. Youknow you can perform certain basic operations on every object in your system. Asingly rooted hierarchy, along with creating all objects on the heap, greatlysimplifies argument passing (one of the more complex topics in C++).

A singly rooted hierarchy makes it mucheasier to implement a garbage collector (which is conveniently built intoJava). The necessary support can be installed in the base class, and thegarbage collector can thus send the appropriate messages to every object in thesystem. Without a singly rooted hierarchy and a system to manipulate an objectvia a reference, it is difficult to implement a garbage collector.

Since run-time type information is guaranteedto be in all objects, you’ll never end up with an object whose type you cannotdetermine. This is especially important with system level operations, such asexception handling, and to allow greater flexibility in programming.

3 Collection libraries and support for easy collectionuse

Because a container is a tool that you’ll use frequently, it makes sense tohave a library of containers that are built in a reusable fashion, so you cantake one off the shelf Because a container is a tool that you’ll usefrequently, it makes sense to have a library of containers that are built in areusable fashion, so you can take one off the shelf and plug it into yourprogram. 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 Javathat 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 laterask for them back. But, since the container holds only Objects, when you add your object reference into the container itis upcast to Object, thus losingits 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 theobject that you put into the container?

Here, the cast is used again, but this time you’re not casting up theinheritance hierarchy to a more general type, you cast down the hierarchy to amore specific type. This manner of casting is called downcasting. Withupcasting, you know, for example, that a Circle is a type of Shapeso it’s safe to upcast, but you don’t know that an Object is necessarily a Circleor a Shape so it’s hardly safeto downcast unless you know that’s what you’re dealing with.

It’s not completely dangerous, however, because if you downcast to thewrong thing you’ll get a run-time error called an exception, which will be described shortly. When you fetchobject references from a container, though, you must have some way to rememberexactly what they are so you can perform a proper downcast.

Downcasting and the run-time checks require extra time for the runningprogram, and extra effort from the programmer. Wouldn’t it make sense tosomehow create the container so that it knows the types that it holds,eliminating the need for the downcast and a possible mistake? The solution isparameterized types, which are classes that the compiler can automaticallycustomize to work with particular types. For example, with a parameterizedcontainer, the compiler could customize that container so that it would acceptonly Shapes and fetch only Shapes.

Parameterized types are an important part of C++, partlybecause C++ has no singly rooted hierarchy. In C++, the keyword that implementsparameterized types is “template.” Java currently has no parameterized typessince it is possible for it to get by—however awkwardly—using the singly rootedhierarchy. However, a current proposal for parameterized types uses a syntaxthat is strikingly similar to C++ templates.

原创粉丝点击