/* --- CSU213 Spring 2005 Lecture Notes --------- Copyright 2005 Viera K. Proulx Lecture 15: How do we compare ... */ /* Goals: - learn to design a universal test for equality of lists of objects Introduction: We return to the problem first considered in Lecture 11: Let us consider the following examples of classes that represent images saved in a computer, items in a salad (tomaotes, peppers, mushrooms), or segments of a car trip between refueling, with information about the origin and destination, the miles traveled, and the fuel information. +--------------+ +-------------+ +-------------+ | Picture | | Item | | Segment | +--------------+ +-------------+ +-------------+ | String title | | String name | | String from | | int width | | int pieces | | String to | | int height | | int price | | int miles | +--------------+ +-------------+ | int gallons | | int price | +-------------+ Here are some examples of the instances of these classes: Picture snow = new Picture("Snow.gif", 400, 200); Picture beach = new Picture("Beach.gif", 200, 300); Picture bluff = new Picture("Bluff.gif", 200, 600); Item tom = new Item("tomato", 3, 50); Item pep = new Item("pepper", 2, 60); Item rad = new Item("raddish", 6, 10); Segment b2n = new Segment("Boston", "NYC", 200, 10, 210); Segment n2t = new Segment("NYC", "Trenton", 120, 5, 200); Segment t2p = new Segment("Trenton", "Philly", 80, 2, 180); We need a list of pictures to represent all pictures in a folder or a directory, we need a list of salad items to represent a salas serving in a restaurant, and we need a list of trip segments, to compute the total distance traveled, or the cost of the trip. We defined a list of Object-s to represent any one of these lists: +--------+ | ALoObj |<------------+ +--------+ | / \ | --- | | | -------------- | | | | +---------+ +--------------+ | | MTLoObj | | ConsLoObj | | +---------+ +--------------+ | | Object first | | | ALoObj rest |-+ +--------------+ We also made examples of these lists: ALoObj objPictures = new ConsLoObj(snow, new ConsLoObj(beach, new ConsLoObj(bluff, new MTLoObj()))); ALoObj objSalad = new ConsLoObj(tom, new ConsLoObj(pep, new ConsLoObj(rad, new MTLoObj()))); ALoObj objTrip = new ConsLoObj(b2n, new ConsLoObj(n2t, new ConsLoObj(t2p, new MTLoObj()))); We have then designed several methods that process the list of Object-s. -------------------------------------------------------------------------- *************************************************************************** We will consider the method 'contains'. We start with recalling the examples: pictures.contains(snow) --> should be true pictures.contains(new Picture("Lake", 200, 200)) --> should be false salad.contains(pep) --> should be true salad.contains(new Item("Olive", 10, 10)) --> should be false trip.contains(t2p) --> should be true trip.contains(new Segment("DC", "Norfolk", 150, 8, 180)) --> should be false In the class ALoObj we had the purpose and the header: // does this list contain the given object abstract boolean contains(Object obj); In the class MTLoObj the method body was easy: boolean contains(Object obj){ return false; } In the class ConsLoObj we defined the following: boolean contains(Object o){ return this.first.equals(o) || this.rest.contains(o); } However, we know, that the 'equals' method is not sufficient for our tests. We are concerned not only with the intensional equality of objects, but especially whether the contents and structure of the data is the same (extensional equality). For this we used our method 'same'. If we try to rewrite the 'contains' method for the class ConsLoObj as follows: boolean contains(Object o){ return this.first.same(o) || this.rest.contains(o); } we run into a problem. Class Object does not define the method 'same'. In the past, we have defined the method 'same' for different classes. So, we know how to define the method 'same' for the classes Picture, Item, and Segment: // in the class Picture // determine whether this picture is the same as the given one boolean same(Picture p){ return this.title.equals(p.title) && this.width == p.width && this.height == p.height; } // in the class Item // determine whether this item is the same as the given one boolean same(Item i){ return this.name.equals(i.name) && this.pieces == i.pieces && this.price == i.price; } // in the class Segment // determine whether this segment is the same as the given one boolean same(Segment s){ return this.from.equals(s.from) && this.to.equals(s.to) && this.miles == s.miles && this.gallons == s.gallons && this.price == s.price; } Unfortunately, each of these methods consumes a different argument. We can abstract over the argument - replacing it with 'Object obj'. Of course, this will then require that within the body of each method we 'cast' the given obj to the appropriate type as follows: // in the class Picture // determine whether this picture is the same as the given object boolean same(Object obj){ return this.title.equals(((Picture)obj).title) && this.width == ((Picture)obj).width && this.height == ((Picture)obj).height; } // in the class Item // determine whether this item is the same as the given object boolean same(Object obj){ return this.name.equals(((Item)obj).name) && this.pieces == ((Item)obj).pieces && this.price == ((Item)obj).price; } // in the class Segment // determine whether this segment is the same as the given object boolean same(Object obj){ return this.from.equals(((Segment)obj).from) && this.to.equals(((Segment)obj).to) && this.miles == ((Segment)obj).miles && this.gallons == ((Segment)obj).gallons && this.price == ((Segment)obj).price; } Before we proceed, we should run some tests, to make sure the method works the same was as it did before: // test the method same in the class Picture: boolean testSameP1 = snow.same(new Picture("Snow.gif", 400, 200)) == true; boolean testSameP2 = beach.same(new Picture("Snow.gif", 400, 200)) == false; // test the method same in the class Item: boolean testSameI1 = tom.same(new Item("tomato", 3, 50)) == true; boolean testSameI2 = tom.same(rad) == false; // test the method same in the class Segment: boolean testSameS1 = b2n.same(new Segment("Boston", "NYC", 200, 10, 210)) == true; boolean testSameS2 = b2n.same(t2p) == false; We have solved one part of our problem. Now every class whose objects we want to combine into a list has the method 'same' with the same header. But the putside world does not know about it. We could have an abstract class that contains an abstract method with the signature 'boolean same(Object obj)', and have every one of our classes extend it. But mixing salad items with pictures and trip segments is certainly not very apetizing. The three classes have nothing in common, only the fact that we want to compare two of their instances with each other. The solution is to define an 'interface'. java interface is like an abstract class with no fields, only methods. A class may extend only one super class, but may 'implement' one or more interfaces. Interfaces define some common behavior that typically transcends the specific properties of the classes that implement them. The first such property we identified, is extensional equality. We define the interface and the three classes that implement the interface as follows: */ interface ISame{ // determine whether this object is the same as the given one boolean same(Object obj); } // to represent a picture file class Picture implements ISame { String title; int width; int height; Picture(String title, int width, int height) { this.title = title; this.width = width; this.height = height; } // determine whether this picture is the same as the given object boolean same(Object obj){ return this.title.equals(((Picture)obj).title) && this.width == ((Picture)obj).width && this.height == ((Picture)obj).height; } } // to represent a salad item class Item implements ISame { String name; int pieces; int price; Item(String name, int pieces, int price) { this.name = name; this.pieces = pieces; this.price = price; } // determine whether this item is the same as the given object boolean same(Object obj){ return this.name.equals(((Item)obj).name) && this.pieces == ((Item)obj).pieces && this.price == ((Item)obj).price; } } // to represent a segment of a road trip class Segment implements ISame { String from; String to; int miles; int gallons; int price; Segment(String from, String to, int miles, int gallons, int price) { this.from = from; this.to = to; this.miles = miles; this.gallons = gallons; this.price = price; } // determine whether this segment is the same as the given object boolean same(Object obj){ return this.from.equals(((Segment)obj).from) && this.to.equals(((Segment)obj).to) && this.miles == ((Segment)obj).miles && this.gallons == ((Segment)obj).gallons && this.price == ((Segment)obj).price; } } /* We can now run the earlier examples and make sure they work: */ class Examples_1{ Examples_1() {} // picture files Picture snow = new Picture("Snow.gif", 400, 200); Picture beach = new Picture("Beach.gif", 200, 300); Picture bluff = new Picture("Bluff.gif", 200, 600); // salad items Item tom = new Item("tomato", 3, 50); Item pep = new Item("pepper", 2, 60); Item rad = new Item("raddish", 6, 10); // trip segments Segment b2n = new Segment("Boston", "NYC", 200, 10, 210); Segment n2t = new Segment("NYC", "Trenton", 120, 5, 200); Segment t2p = new Segment("Trenton", "Philly", 80, 2, 180); // test the method same in the class Picture: boolean testSameP1 = snow.same(new Picture("Snow.gif", 400, 200)) == true; boolean testSameP2 = beach.same(new Picture("Snow.gif", 400, 200)) == false; // test the method same in the class Item: boolean testSameI1 = tom.same(new Item("tomato", 3, 50)) == true; boolean testSameI2 = tom.same(rad) == false; // test the method same in the class Segment: boolean testSameS1 = b2n.same(new Segment("Boston", "NYC", 200, 10, 210)) == true; boolean testSameS2 = b2n.same(t2p) == false; } /* We still have one problem to resolve. At this point the three class made a contract to implement the 'same' method. However, the 'contains' method in the class ConsLoObj need to be informed that the objects in the list, specifically the object known as 'this.first' is an instance of a class that implements ISame interface, and so it will be able to invoke the method 'same' at the run time. We cast 'this.first' to type ISame: // determine whether this list contains the given object boolean contains(Object o){ return ((ISame)this.first).same(o) || this.rest.contains(o); } We can now finish the definition of these classes and run the examples: */ // to represent a list of objects abstract class ALoObj { // determine whether this list contains the given object abstract boolean contains(Object o); } // to represent an empty list of objects class MTLoObj extends ALoObj { MTLoObj() {} // determine whether this list contains the given object boolean contains(Object o){ return false; } } // to represent a nonempty list of objects class ConsLoObj extends ALoObj { Object first; ALoObj rest; ConsLoObj(Object first, ALoObj rest) { this.first = first; this.rest = rest; } // determine whether this list contains the given object boolean contains(Object o){ return ((ISame)this.first).same(o) || this.rest.contains(o); } } class Examples{ Examples() {} // picture files Picture snow = new Picture("Snow.gif", 400, 200); Picture beach = new Picture("Beach.gif", 200, 300); Picture bluff = new Picture("Bluff.gif", 200, 600); // salad items Item tom = new Item("tomato", 3, 50); Item pep = new Item("pepper", 2, 60); Item rad = new Item("raddish", 6, 10); // trip segments Segment b2n = new Segment("Boston", "NYC", 200, 10, 210); Segment n2t = new Segment("NYC", "Trenton", 120, 5, 200); Segment t2p = new Segment("Trenton", "Philly", 80, 2, 180); // test the method same in the class Picture: boolean testSameP1 = snow.same(new Picture("Snow.gif", 400, 200)) == true; boolean testSameP2 = beach.same(new Picture("Snow.gif", 400, 200)) == false; // test the method same in the class Item: boolean testSameI1 = tom.same(new Item("tomato", 3, 50)) == true; boolean testSameI2 = tom.same(rad) == false; // test the method same in the class Segment: boolean testSameS1 = b2n.same(new Segment("Boston", "NYC", 200, 10, 210)) == true; boolean testSameS2 = b2n.same(t2p) == false; // an empty list of anything... ALoObj mtlist = new MTLoObj(); // list of pictures ALoObj objPictures = new ConsLoObj(snow, new ConsLoObj(beach, new ConsLoObj(bluff, new MTLoObj()))); // list of salad items ALoObj objSalad = new ConsLoObj(tom, new ConsLoObj(pep, new ConsLoObj(rad, new MTLoObj()))); // a list of trip segments ALoObj objTrip = new ConsLoObj(b2n, new ConsLoObj(n2t, new ConsLoObj(t2p, new MTLoObj()))); // test the method contains for the list of pictures boolean testContainsP1 = objPictures.contains(snow) == true; boolean testContainsP2 = objPictures.contains(new Picture("Lake", 200, 200)) == false; boolean testContainsI1 = objSalad.contains(pep) == true; boolean testContainsI2 = objSalad.contains(new Item("Olive", 10, 10)) == false; boolean testContainsS1 = objTrip.contains(t2p) == true; boolean testContainsS2 = objTrip.contains(new Segment("DC", "Norfolk", 150, 8, 180)) == false; } /* ***************************************************************************************** Take a deep breath! We are not done. We still have a problem. What if our list is a list of lists? That means, that each Object in the list is of the type ALoObj. So, we have to make sure that ALoObj also implements the ISame interface. At least, we know what we need to do: to design the method 'same' for the hierarchy that represents a list of Object-s. We can easily make examples from the data we already defined: boolean testSameLP1 = objPictures.same(new ConsLoObj(snow, new ConsLoObj(beach, new ConsLoObj(bluff, new MTLoObj())))) == true; boolean testSameLP2 = objPictures.same(new ConsLoObj(snow, new MTLoObj())) == false; boolean testSameLI1 = objSalad.same(new ConsLoObj(tom, new ConsLoObj(pep, new ConsLoObj(rad, new MTLoObj())))) == true; boolean testSameLI2 = objSalad.same(new ConsLoObj(tom, new MTLoObj())) == false; boolean testSameLS1 = objTrip.same(new ConsLoObj(b2n, new ConsLoObj(n2t, new ConsLoObj(t2p, new MTLoObj())))) == true; boolean testSameLS2 = objTrip.same(new ConsLoObj(b2n, new MTLoObj())) == false; Life would be easier, if we at least knew that the Object we are comparing with is indeed a list of Objects. Maybe a helper method could do the work. We define the following in the class ALoObj: abstract class ALoObj implements ISame{ // determine whether this list of objects is the same as the given Object boolean same(Object obj){ return this.sameLoObj((ALoObj) obj); } // determine whether this list of objects is the same as the given list of objects abstract boolean sameLoObj(ALoObj alist); } The rest is the same as what we did in Lecture 9: Add the the abstract class two new methods: // determine whether this list of objects is the same as the given empty list of objects boolean sameMTLoObj(MTLoObj mtlist){ return false; } // determine whether this list of objects is the same as the given nonempty list of objects boolean sameConsLoObj(ConsLoObj conslist){ return false; } In the class MTLoObj we add: // determine whether this empty list of objects is the same as the given list of objects boolean sameLoObj(ALoObj alist){ return alist.sameMTLoObj(this); } // determine whether this empty list of objects is the same as the given empty list of objects boolean sameMTLoObj(MTLoObj mtlist){ return true; } and in the class ConsLoObj we add: // determine whether this nonempty list of objects is the same as the given list of objects boolean sameLoObj(ALoObj alist){ return alist.sameConsLoObj(this); } // determine whether this nonempty list of objects is the same // as the given nonempty list of objects boolean sameConsLoObj(ConsLoObj conslist){ return ((ISame)this.first).same(conslist.first) && this.rest.same(conslist.rest); } Complete example of all te code from this lecture is in a separate file. */