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. Each has a method for updating a value if the name matches a given string:
class PhoneBookEntry { |
String name; |
PhoneNumber phnum; |
|
PhoneBookEntry(String name, PhoneNumber phnum) { |
this.name = name; |
this.phnum = phnum; |
} |
|
updateNumForName (String forname, PhoneNumber newNum) { |
if (this.String.equals(forname)) { |
return this.phnum = newNum; |
} |
} |
} |
|
class MenuItem { |
String name; |
int price; |
|
MenuItem(String name, int price) { |
this.name = name; |
this.price = price; |
} |
|
updatePriceForName (String forname, int newPrice) { |
if (this.String.equals(forname)) { |
return this.price = newPrice; |
} |
} |
} |
These two classes look very similar. We would like a way to share the common code here. Creating an abstract class as we have done before is challenging, however, 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:
class NameAssoc { |
String name; |
??? data; |
|
NameAssoc(String name, ??? data) { |
this.name = name; |
this.data = data; |
} |
|
updatePriceForName (String forname, ??? newData) { |
if (this.String.equals(forname)) { |
return this.data = newData; |
} |
} |
} |
Until now, we would have abstracted over the different types using an interface (something like IDataForName, putting that in place of ???). 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.
Java provides two ways to solve this problem. Let’s look at each.
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; |
} |
|
updateDataForName (String forname, Object newData) { |
if (this.String.equals(forname)) { |
return this.data = newData; |
} |
} |
} |
|
class PhoneBookEntry extends NameAssoc { |
PhoneBookEntry(String name, PhoneNumber phnum) { |
super(name, phnum); |
} |
} |
|
class MenuItem extends NameAssoc { |
MenuItem(String name, int price) { |
super(name, price); |
} |
} |
Now, let’s imagine that we wanted to create lists of phone-book entries or menu items, and we wanted a way to sort those lists. To write a sorting method, each class would need a method to determine whether one entry or menu item was smaller than another. 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; |
} |
|
updateDataForName (String forname, DATA newData) { |
if (this.String.equals(forname)) { |
return this.data = newData; |
} |
} |
} |
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. We can require that every NameAssoc provide a lessThan method as follows:
abstract class NameAssoc<DATA> { |
String name; |
DATA data; |
|
NameAssoc(String name, DATA data) { |
this.name = name; |
this.data = data; |
} |
|
abstract boolean lessThan(NameAssoc<DATA> other); |
} |
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.
That abstract classes can have abstract methods, whose actual definitions are provided by subclasses of the abstract class.