Data Abstraction
So far, we’ve covered basic program structure in Java, abstract data types, and data structures with invariants. These have raised several Java issues that we should address. The next few lectures discuss some of these issues.
Credits: Most of the material in this lecture is taken from Felleisen et al’s How to Design Classes.
1 Abstracting Over Classes with Similar Structure But Different Types
Assume that you have the following two classes. The first associates names with phone numbers (as in a phone book); the second associates prices with the names of menu items.
class PhoneBookEntry { |
String name; |
PhoneNumber phnum; |
|
PhoneBookEntry(String name, PhoneNumber phnum) { |
this.name = name; |
this.phnum = phnum; |
} |
} |
|
class MenuItem { |
String name; |
int price; |
|
MenuItem(String name, int price) { |
this.name = name; |
this.price = price; |
} |
} |
These two classes have similar structure, and we could imagine writing similar methods over them (i.e., looking up the number or price associated with a name). We would therefore like a way to share the commonalities here. This means that we want to be able to abstract over similar class definitions. This differs from the abstract class ideas we discussed previously because the types of the phone number and price differ across the two classes.
To be concrete, we want to create a general class for associating names with data, along the lines of the following:
the following code, figuring out what to fill in for the ???:
class NameAssoc { |
String name; |
??? data; |
|
NameAssoc(String name, ??? data) { |
this.name = name; |
this.data = data; |
} |
} |
Until now, we would have abstracted over the different types using an interface. However, that doesn’t work in this case because we cannot control what interfaces integers implement. In general, when you want to abstract over types that you may not control, an interface is not an option.
1.1 Abstracting Using a Shared Superclass
Java helps handle this problem by providing a special class called Object that is the superclass of all other classes. Every time you write a class definition, Java includes an implicit extends Object. This means that Object is a type that represents objects of any class. We could therefore rewrite the code as follows:
class NameAssoc { |
String name; |
Object data; |
|
NameAssoc(String name, Object data) { |
this.name = name; |
this.data = data; |
} |
} |
|
class PhoneBookEntry extends NameAssoc { |
PhoneBookEntry(String name, PhoneNumber phnum) { |
super(name, phnum); |
} |
} |
|
class MenuItem extends NameAssoc { |
MenuItem(String name, int price) { |
super(name, price); |
} |
} |
One could imagine creating lists of phone-book entries or menu items; one could also imagine sorting those lists. To do so, each class would need a lessThan method. Let’s look at a reasonable such method for MenuItems, one that compares based on the price:
class MenuItem extends NameAssoc { |
MenuItem(String name, int price) { |
super(name, price); |
} |
|
boolean lessThan(MenuItem thanItem) { |
return this.data < thanItem.data; |
} |
} |
boolean lessThan(MenuItem thanItem) { |
return (Integer)this.data < (Integer)thanItem.data; |
} |
The Object approach to abstracting over classes puts more responsibility on the programmer to use the classes correctly. For example, nothing in the current code prevents a MenuItem from holding a non-numeric piece of data. For example, the following code compiles:
class MenuItem extends NameAssoc { |
MenuItem(String name, int price) { |
super(name, "gotcha"); |
} |
} |
This illustrates that using Object is too general. The Object type is useful when you want to allow any datum and you don’t intend to use it in computation. Here, the above code would raise an error if we tried to use the lessThan method on a MenuItem object. In this case, we would prefer a way to parameterize NameAssoc over specific types in a way that preserves type checking.
1.2 Abstracting Using Generics
The following version of NameAssoc takes the type of data as a parameter:
class NameAssoc<DATA> { |
String name; |
DATA data; |
|
NameAssoc(String name, DATA data) { |
this.name = name; |
this.data = data; |
} |
} |
The following code shows how to create the PhoneBookEntry and MenuItem classes via type parameters:
class PhoneBookEntry extends NameAssoc<PhoneNumber> { |
PhoneBookEntry(String name, PhoneNumber phnum) { |
super(name, phnum); |
} |
} |
|
class MenuItem extends NameAssoc<Integer> { |
MenuItem(String name, int price) { |
super(name, price); |
} |
} |
Generics offer much better type checking than abstractions created through Object. The casts in lessThan in the object version are not needed here, because the version of NameAssoc that MenuItem extends has fixed the data to be Integer.
Finally, let’s look at the lessThan method in the context of generics. The MenuItem class implements the lessThan method as follows:
class MenuItem extends NameAssoc<Integer> { |
... |
|
boolean lessThan(MenuItem thanItem) { |
return this.data < thanItem.data; |
} |
} |
2 Summary
In this lecture, you should have learned:
That Java has a special Object class that every other class extends.
That Java allows you to create classes that are parameterized over types; these are called generics.
That generic methods give more refined type checking than using Object for abstraction.