Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 312 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
312
Dung lượng
1,15 MB
Nội dung
A.6 PACKAGES Programmers are free to write their own libraries of Java classes, and to share those libraries with other programmers For example, we have written a library that contains classes List, OrderedTree, LineDrawing, etc for this book As programmers all around the world write their own Java libraries, there will inevitably be name conflicts—situations in which two people use the same name to identify different entities Java's packages are a mechanism for reducing name conflicts A package is a collection of related classes Programmers place a class in a package by putting a special package statement at the beginning of the file that defines the class All members of a package have names that, technically, include the package name For example, the classes that accompany this book are all in a package named geneseo.cs.sc The full name of the List class that accompanies this book is therefore geneseo.cs.sc.List.[5] A.6.1 Using Classes from a Package In order to use classes from a package, those classes must be installed on your computer Unfortunately, exactly what "installed" means depends on your Java programming environment: in some environments, "installed" means that files containing the compiled classes have to be present in specific directories, in other environments it means that either the compiled or source files must be listed in "project files" that describe your program, etc Consult your instructor, or the documentation for your programming environment, if you want to install packages for yourself Once classes defined in a package are installed on your computer, you may use them by writing out their full names in your own programs For example, you could declare an instance of this book's List class by writing code such as: geneseo.cs.sc.List aList = new geneseo.cs.sc.List(); Such code quickly becomes painful to write and hard to read Java therefore provides a way to import some or all of a package's definitions into a source file After importing a definition, code in that file can refer to the imported entity by its short name For example, after importing geneseo.cs.sc.List via the statement: import geneseo.cs.sc.List; you could write the above declaration as just: List aList = new List(); To import a single class into a file, you write a statement of the form: import ; at the beginning of the file In this template, is a package name, and is the name of the class you want to import For example: import geneseo.cs.sc.List; To import every class in a package, use an import statement of the form: import * ; where is the name of the package For example: import geneseo.cs.sc.*; A file can contain multiple import statements, if you need to import multiple classes, or need to import from multiple packages [5]Packages only solve the problem of name conflicts if package names themselves don't give rise to conflicts Fortunately there are published conventions for naming packages that make such conflicts unlikely A.5 IMPRECISE CLASSES Sometimes a program needs a way of specifying an object's class imprecisely For example, suppose you want to define a data structure in which you can store arbitrary kinds of data—String objects, Counter objects, Point objects, anything that someone might someday represent as an object The message that stores a piece of data into this structure cannot declare that data to be an instance of any one of these classes, because doing so would preclude storing instances of other classes in the structure Nor can you declare different messages to store different kinds of data, because you can't know what classes future clients will define and want to store in your data structure Similar problems arise in declaring messages that retrieve data from the structure, declaring the structure's member variables, etc What you need is a way to declare parameters, results, and member variables that can be objects of any class, not just one class Java provides several features that help programmers write such declarations A.5.1 Class "Object" Java programmers use the class hierarchy (i.e., superclass/subclass relationships between classes) to specify classes imprecisely In particular, something declared to be an instance of a superclass can actually be an instance of any of that superclass's subclasses (or subclasses of the subclasses, etc.) Therefore, if you don't know exactly what class some object will have, but you do know that it will be one of the subclasses of some general superclass, you can simply declare the object to be an instance of the superclass The most general superclass in Java is Object, which is the superclass of all other classes Any class that doesn't extend something else is automatically a subclass of Object, so every class descends, either directly or indirectly, from Object For example, consider a simple data structure of the sort suggested in the introduction to this section This structure holds data in an array, provides a store message to add an item to the array, and provides a retrieve message to fetch an item, given its position in the array You could use Object to declare these features, as outlined in Listing A.6 (for the complete class definition and sample client code, see file SimpleStructure.java at this book's Web site) LISTING A.6: An Outline of a Simple Generic Data Structure in Java private Object[] data; // The array of items public void store(Object newData) { // The store method } public Object retrieve(int index) { // The retrieve method } The actual values represented by something declared as Object will always really be instances of some subclass of Object For example, here is a statement that adds a String to a simple data structure named container: container.store("This is a string"); Note that this statement passes an ordinary String to store This is a legal use of store, because store can receive any instance of Object as its parameter, and String is a subclass of Object—so all String objects are, indirectly, also instances of Object Class Object is never used to create the value stored in the structure A.5.2 Casts Imprecise classes sometimes leave a programmer knowing an object's class more precisely than is evident from a program For example, suppose you are a client using Listing A.6's simple data structure to store strings Your application will thus retrieve only strings from the data structure Unfortunately, however, a Java compiler only knows that the retrieve message is declared as returning Object, it doesn't know how your application uses retrieve Therefore, if you try to send string messages to the objects you retrieve, or assign those objects to string variables, the compiler will report an error and refuse to compile your program Java's solution to this problem is a cast Casts let programmers assert that a value has a particular type The syntax for writing a cast is: ( ) where is an expression whose result is apparently of one type, and is another type that really has For example, a client of the simple data structure who knew that every object stored in the structure was a string, and so wanted to assign objects retrieved from the structure to a string variable, could write: String item = (String) container.retrieve(i); Although you can sometimes use casts to convert one simple type to another (e.g., you can cast a char to an int, an int to a double), you should generally treat a cast as a promise by the programmer that an expression produces a result of a different type than the one the expression's operators or operands imply You should not use casts to convert values of one type into values of a different type Trying to do so can cause a program to abort with an error called a "class cast exception." A.5.3 Interfaces As you use either the standard Java libraries, or the classes provided with this text, you will encounter Java data types known as "interfaces." Like a class, an interface specifies messages that certain sets of objects handle Unlike a class, however, an interface cannot define methods with which to handle these messages Instead, other classes have to implement the interface, meaning that they provide methods for handling its messages You can use the names of interfaces as you would use the names of classes to declare objects whose exact class is unknown Objects so declared can actually be instances of any class that implements the interface You probably won't need to define your own interfaces while using this book, but you may need to declare classes that implement other programmers' interfaces (recall that "implement" in this context is a technical term that means "provides methods for handling the interface's messages") A class that implements an interface has a declaration that looks like: class implements { } or class extends implements { } In these templates, , , and are the class's name, the name of its superclass, and definitions of its member variables and methods, exactly as in other class declarations is a comma-separated list of names of interfaces A class can implement many interfaces, whereas it can only be a subclass of one class For example: class InterfaceExample implements Comparable, Serializable { } (Comparable and Serializable are interfaces from the Java library Comparable represents objects with a "less than" relation Serializable represents objects that can be written to and read from files.) A.7 JAVA'S IMPLEMENTATION OF OBJECTS Java's internal representation of objects has some consequences that can unpleasantly surprise programmers Fortunately, knowing something about Java's implementation of objects reduces these surprises, and leads to more effective use of the language in general A.7.1 Object Variables are Pointers Object variables in Java are more complicated, with more subtle behaviors, than simple variables A simple variable is basically a place in the computer's memory to store the variable's value For example, the statement int i = 1; sets aside enough memory to hold one integer, notes that henceforth the name i means that location in memory, and places the value 1 into that location Figure A.1 diagrams these effects Image from book Figure A.1: A simple variable is a name for a memory location In contrast, Figure A.2 illustrates an object variable A declaration such as SomeClass obj = new SomeClass(); reserves two regions of memory First, the new operation sets aside enough memory to hold all the members of one SomeClass object This memory includes locations that store the values of the object's member variables, and locations that indicate where to find the code for the object's methods Second, the declaration sets aside one memory location, named obj, that holds information about where in memory to find the members Such information is called a pointer Properly speaking, the object is the memory that holds the members; the variable is only a pointer to the object Imagine stapling two grocery lists together, or coupling a train with a whole series of cars already linked together, generically shown in Figure 11.4 Combining two lists to make one large list goes by many names, including join (usually applied to unordered collections or sets), concatenate (usually applied to sequences of characters or words), and append But the basic idea is the same no matter what the specific features of the list: start with two separate lists and end up with one long list containing all the elements of both lists Figure 11.4: Combining two lists Visit the Elements Any number of related actions, collectively called visiting or traversing, require accessing every element in the list individually, perhaps to print them out, perhaps to make a change to each item, or perhaps simply to find one particular value within the list It is not the specific action, such as printing, that is important here Rather, we need a general means of traversal through the list: a tool that enables us to access each element one at a time In fact, this may be the most important of all the capabilities described here As we develop the concept of list further and ultimately create a List class, we will want to make sure that the resulting class facilitates these basic operations 11.2.2 Visiting the Subparts Traversal of a list is especially important because it is the means for accessing the elements even though their number or order may change Perhaps the best way to assure that every element is reachable is to provide a way to visit each of them in a specific sequence This suggests a path through the list: starting with some first element, each element is linked or connected to a next, as in Figure 11.5 Image from book Figure 11.5: Links in a list Every element in the list should be on the path once—and no more than once This requirement assures that we can visit every element by following the path from the first element to the last Performing some operation on every element in the list can be accomplished by processing each element and then following the path to the next So far, this says nothing about the actual construction of a link, which might be accomplished in any of several ways In a grocery list, items are linked simply by their position on the page; train cars are connected by a physical coupling device Such links are probably not a new concept to most readers If you have ever clicked on a link on a Web page you have followed a link If you have ever followed directions that included something like "go to step 3," you have followed a link In the figure and most other graphical representations, a link is represented as an arrow Humans can intuitively follow the arrows with no special instructions, but it may not be immediately clear how an arrow should best be represented in a computer Any representation of a list class will require some computer equivalent to "arrow", some mechanism that says, "When looking at a given element, this is how to find the next in the sequence." Thus, every element in a list has an associated aspect or property: the link to the next element These two items, element and link, taken together are often thought of as a single unit, called a node But we will soon see an alternative representation that is more amenable to an object-oriented environment Find the Next Element We need a specific tool that effectively says "follow the link." It must somehow explain how to get from "Alpha" in Figure 11.5 to "Beta" This single tool is the enabling tool for any algorithm that requires visiting every element of a list Using Lists Given a tool for getting to the next element, most list-processing algorithms are very simple Perhaps more importantly, they have remarkably similar structures, and with a little experience, can be built almost intuitively A simple example might be something like: Algorithm to find the total cost of groceries: Initialize the total cost to zero Start at the first element While there are more groceries on the list: Set total = total plus cost of current element Find (visit) the next element Recursive versions of most list algorithms are even simpler and look something like: Algorithm totalCost (groceryList): If there are no more elements in the list Set total cost to 0 otherwise set total cost = cost of this element + cost of all following elem This algorithm contains the question, "Are there any elements remaining to be visited?" Hidden within that question are at least two assumptions: that a list may not have any elements at all and that we can recognize when it doesn't With those assumptions, the general structure of almost any traversing algorithm is almost identical: Algorithm genericProcess: If the list has more elements to be visited then Process this element, genericProcess the remaining elements Only the details of the specific action performed during the visit will change In fact, the simplicity of recursive algorithms for list processing is one of the features that make both lists and recursion popular tools For example, one algorithm to print out all the items on a grocery list looks like: Algorithm to print all element names: If there are elements remaining to print then Print the current name, Print the names of the remaining elements 11.2.3 A Unifying Observation Before we actually attempt a formal definition of a list, let's consider the question: What follows an element in a list? There are two possible answers: The next element A series of elements starting with the next element and running to the last element in the list This second answer, while perhaps less intuitive, is more interesting, because it can be stated as: each element is followed by another list (possibly empty) Consider the train example yet again: each car within a train has a coupling device capable of hanging onto—or letting go of—the following car By closing this coupler at the right moment, the car can attach itself to another car But if that additional car happens to be the first car of a train, attaching it also adds that entire train Each car in a train is followed not just by the next car but all the remaining cars in the train Those remaining cars can collectively be thought of as a train The first answer to the question seems to correspond directly to the original concept of list The second answer leads to an alternative description of list, that while consistent with the previous use, enables a much more powerful development of a list class Just as each car in a train can be thought of as the first car of another smaller train, a list can be thought of as composed of a series of lists, each of which is an element followed by another list (rather than simply by an element), as illustrated in Figure 11.6 Notice that just as the list ovals in the figure are simply super-imposed on the original list of elements (recall Figure 11.5), this vision of list can be superimposed on the original The sublist provides the mechanism for visiting the remaining elements; each sublist of any given list contains all of the elements that follow the first element Instead of representing a list as a series of individual elements, we will think of it as a nested series of lists Image from book Figure 11.6: A recursive vision of list By tradition, the two parts of a list are called the head and the tail.[1] The head is the first element of the list, while the tail—the rest of the list—is actually another list In this view, every element is the head of some list, as in Figure 11.7 "Alpha" is the head of the list, "Beta" is the head of the tail of that first list "Delta" is the head of the next list In general, we can refer to any element of the original series as the head of some tail within the list On occasion we may continue to visualize the physical list as a sequence of elements In fact, we may even refer to "the next element of the list," but you should remember that by "the next element" we mean the head of the next sublist (or, "the head of the tail") following the current element Image from book Figure 11.7: The list as sequence of lists 11.2.4 The Empty List The new definition is a recursive definition, and any recursive definition must terminate somehow A list, like so many objects we have discussed in this text, may be empty—a list with no elements A train with no cars is one that hasn't been put together yet (but perhaps is scheduled, in which case the switching yard needs to know about it); a character string with no characters is the null string (represented in most compilers as " "); a folder with no files in it yet is an empty list of files; and hopefully at the end of a shopping trip the shopping list is empty (all items have been crossed off) If we require that the last (innermost) list within a list be the empty or null list, we can avoid any infinite regression In general a list may be: an element—followed by a list or the empty (null) list Requiring that every list terminate with the empty list provides an easy way to recognize the end of a path through the list: if the current list is the empty list, we must be at the end A list made up of lists lends itself perfectly to recursive algorithms The printList algorithm nears its final form as: Algorithm printList: If the list is not empty then Print the current name (the head), printList the next list (the tail) Exercises For each of the previous real-world examples of lists (train, shopping list, computer program, table of contents, words in a word processor, and 11.4 letters on a screen), give an example of each of the basic list operations Repeat for the computer-specific lists 11.5 Draw schematic representations of three of the lists from Section 11.1 Be sure to include the list ovals superimposed on the sequence 11.6 Describe, as a recursive algorithm, the process of buying all the items on a grocery list Describe each element of that list in terms of heads and tails [1]The names are suggestive of the old question: Where does a snake's tail begin? Answer: right behind the head 12.4 FURTHER READING Stacks have been a staple of computing systems for almost a half century Bauer and Samelson first described a scheme using a pushdown stack for the translation and interpretation of arithmetic expressions, which first appeared in German, but was translated into English as: F.L Bauer and K Samelson "Sequential Formula Translation," in Communications of The Association for Computing Machinery, vol.3 (1960), pp.76–83 It is interesting to note that that is the same year as the publication of the "The Algol Report," which may have been the first language specification that explicitly called for a stack For more detailed information specific to the two data structures of this chapter, see any good data structures book such as: Richard Gilberg and Behrouz Forouzan Data Structures: A Pseudocode Approach With C Brooks/Cole, 1998 For the relationship of stacks to the complexity of problems, see any theory of computation text, such as: J Hopcroft, R Motwani, J Ullman & Rotwani Introduction to Automata Theory, Languages, and Computation Addison-Wesley, 2000 For the application of stacks to compiler design, see any compiler text such as the classic: Alfred V Aho, Ravi Sethi, Jeffrey D Ullman Compilers, Addison-Wesley, 1986 or the newer and Java-specific: Andrew Appel and Jens Palsberg Modern Compiler Implementation in Java Cambridge University Press, 2003 The mathematical analysis of queues and the systems that employ them is called queuing theory A good introduction may be found in: Frederick Hillier and Gerald Lieberman Introduction to Operations Research McGraw-Hill, 2002.p 13.2 FORMALIZING THE CONCEPT OF TREE We can define the notion of "tree" in much the way we defined "list." While a tree is certainly a more complex structure than a list, the two formalisms share many of the same features 13.2.1 Trees as Data Structures We have seen several important properties of trees that we want to capture: Every tree has exactly one root Every node other than the root has exactly one parent No node can be its own descendant (or ancestor) No two nodes can have the same child Among other things, these properties mean that there are no cycles, that is, no series of branches or edges that lead from one node through other nodes and back to the original node Figure 13.9 illustrates some way these properties may fail to be met In general, any attempt to define tree formally will want to capture these properties Image from book Figure 13.9: Some structures that are not trees Binary Trees In general, a node can have any number of children, and a tree with an upper bound of n on the number of possible children for any one node is called an nary tree The binary tree (a tree with no more than two children for any node, such as the sports tournaments, genealogy, or decision tree) is especially important in computer science While n-ary trees can be cumbersome, binary trees are easy to write and reason about And as we will see in Section 13.7.2, any n-ary tree has an equivalent binary tree For these reasons, the next several sections reason about binary trees exclusively 13.2.2 Visualizing a Formalism The tree structure can be envisioned much like the list structure—especially when restricted to binary trees Just as a list is conceptually a series of lists, each with a nested, or next list, a tree should be envisioned as a collection of trees, each composed of a root value and two children, each of which is itself a tree Any class definition for a treelike data structure would start just as the list class does, with member variables corresponding to these three pieces: root: any object left: a tree right: another tree Just as a list could be empty, so can a tree Notice that just as each node in a list is actually another list, each node in a tree is another (sub)tree Generally we will say child, subtree, or node depending on which aspect we wish to emphasize Figure 13.10 shows this recursive view of a tree, with empty trees represented by "(empty)" Even from this small tree, it may be obvious that the total number of these empty subtrees may be very large (Section 13.7.1 explores just how large) For that reason, we may often simplify the graphical representations as in Figure 13.11, omitting the explicit ovals representing subtrees, and empty trees along with the links to them However, as we reason about trees, it will be important to take care to recognize whether we are talking about all nodes (subtrees) or just the nonempty ones Similarly, when we say leaf, we need to be clear as to whether we mean the empty trees at the end of each path, or the last node with content "Leaf" will usually mean the last nonempty node However, many of the algorithms will actually visit the empty trees, and that fact will need to be considered when evaluating execution times Image from book Figure 13.10: Schematic representation of a tree structure Image from book Figure 13.11: Simplified graphic representation of a tree The uniformity of the tree structure is key to most proofs involving trees For example, the following lemma is used implicitly in most proofs in this chapter Lemma: The height of a tree is strictly greater than that of either of its children Proof: Let T be a tree and let T' be the taller of its subtrees (if they have the same height, select the left subtree) and let h be the height of T' Let P be the longest path in T' By definition, P has pathlength h - 1 The path formed by joining P and the branch from the T to T' must be one longer than h - 1 So the height of T must be greater than that of T', which by definition is at least as high as the other subtree Since every node is actually a tree, proofs involving nodes can usually be stated in terms of the subtree that has the node of interest at its root For example, one particular place this change of reference is useful is where any two nodes in a tree have a common ancestor That common ancestor is the root of the smallest tree that contains both nodes In particular, for any two nodes in a tree, there is a unique smallest (shortest) subtree containing both nodes To see this, start at the two nodes, A and B in Figure 13.12, and trace upwards until a common ancestor is located Within that smallest tree, the two nodes cannot share any common path (otherwise they could share in an even smaller tree) Thus, they must either be in opposite sides of their common subtree, or one must be its root Image from book Figure 13.12: Reasoning about relationships between nodes ... Subclasses in Java ordinarily inherit all of the features of their superclass In other words, instances of the subclass will have all of the member variables and methods of the superclass (although code in the subclass does not have direct... { } In these templates, , , and are the class's name, the name of its superclass, and definitions of its member variables and methods, exactly as in other class declarations... other parts of a program can directly access the variable The syntax for a member variable declaration is: ; where is the access specifier, and and are the type and name of the variable