When we think of a class type, we assume that apps will create objects of that type. In some cases, however, it’s useful to declare classes for which you never intend to instantiate objects. Such classes are called abstract classes. Because they’re used only as base classes in inheritance hierarchies, we refer to them as abstract base classes. These classes cannot be used to instantiate objects, because, as you’ll soon see, abstract classes are incomplete—derived classes must define the “missing pieces.” Section 12.5.1 demonstrates abstract classes.
The purpose of an abstract class is primarily to provide an appropriate base class from which other classes can inherit, and thus share a common design. In the Shape
hierarchy of Fig. 11.3, for example, derived classes inherit the notion of what it means to be a Shape
—common attributes such as Location
, Color
and BorderThickness
, and behaviors such as Draw
, Move
, Resize
and ChangeColor
. Classes that can be used to instantiate objects are called concrete classes. Such classes provide implementations of every method they declare (some of the implementations can be inherited). For example, we could derive concrete classes Circle
, Square
and Triangle
from abstract base class TwoDimensionalShape
. Similarly, we could derive concrete classes Sphere
, Cube
and Tetrahedron
from abstract base class ThreeDimensionalShape
. Abstract classes are too general to create real objects—they specify only what is common among derived classes. We need to be more specific before we can create objects. For example, if you send the Draw
message to abstract class TwoDimensionalShape
, the class knows that two-dimensional shapes should be drawable, but it does not know what specific shape to draw, so it cannot implement a real Draw
method. Concrete classes provide the specifics needed to instantiate objects.
Not all inheritance hierarchies contain abstract classes. However, you’ll often write client code that uses only abstract base-class types to reduce client code’s dependencies on a range of specific derived-class types. For example, you can write a method with a parameter of an abstract base-class type. When called, such a method can be passed an object of any concrete class that directly or indirectly extends the abstract base class specified as the parameter’s type.
Abstract classes sometimes constitute several levels of the hierarchy. For example, the Shape
hierarchy of Fig. 11.3 begins with abstract class Shape
. On the next level of the hierarchy are two more abstract classes, TwoDimensionalShape
and ThreeDimensionalShape
. The next level of the hierarchy declares concrete classes for TwoDimensionalShape
s (Circle
, Square
and Triangle
) and for ThreeDimensionalShape
s (Sphere
, Cube
and Tetrahedron
).
You make a class abstract by declaring it with the keyword abstract
. An abstract class normally contains one or more abstract methods. An abstract method is one with keyword abstract
in its declaration, as in
public abstract void Draw(); // abstract method
Abstract methods are implicitly virtual and do not provide implementations. A class that contains abstract methods must be declared as an abstract class even if it contains some concrete (non-abstract
) methods. Each concrete derived class of an abstract base class also must provide concrete implementations of the base class’s abstract methods. We show an example of an abstract class with an abstract method in Fig. 12.4.
Properties also can be declared abstract
or virtual
, then overridden in derived classes with the override
keyword, just like methods. This allows an abstract base class to specify common properties of its derived classes. Abstract property declarations have the form:
public abstract PropertyType MyProperty { get; set; }
The semicolons after the get
and set
keywords indicate that we provide no implementation for these accessors. An abstract property omits implementations for the get
accessor and/or the set
accessor. Concrete derived classes must provide implementations for every accessor declared in the abstract property. When both get
and set
accessors are specified, every concrete derived class must implement both. If one accessor is omitted, the derived class is not allowed to implement that accessor. Doing so causes a compilation error.
Constructors and static
methods cannot be declared abstract
or virtual
. Constructors are not inherited, so such a constructor could never be implemented. Similarly, derived classes cannot override static
methods, so such as static
method could never be implemented.
An abstract class declares common attributes and behaviors of the various classes that inherit from it, either directly or indirectly, in a class hierarchy. An abstract class typically contains one or more abstract methods or properties that concrete derived classes must override. The instance variables, concrete methods and concrete properties of an abstract class are subject to the normal rules of inheritance.
Attempting to instantiate an object of an abstract class is a compilation error.
Failure to implement a base class’s abstract methods and properties in a derived class is a compilation error unless the derived class is also declared abstract
.
Although we cannot instantiate objects of abstract base classes, you’ll soon see that we can use abstract base classes to declare variables that can hold references to objects of any concrete classes derived from those abstract classes. Apps typically use such variables to manipulate derived-class objects polymorphically. Also, you can use abstract base-class names to invoke static
methods declared in those abstract base classes.
Polymorphism is particularly effective for implementing so-called layered software systems. In operating systems, for example, each different type of physical device could operate quite differently from the others. Even so, common commands can read or write data from and to the devices. For each device, the operating system uses a piece of software called a device driver to control all communication between the system and the device. The write message sent to a device-driver object needs to be interpreted specifically in the context of that driver and how it manipulates a specific device. However, the write call itself really is no different from the write to any other device in the system: Place some number of bytes from memory onto that device. An object-oriented operating system might use an abstract base class to provide an “interface” appropriate for all device drivers. Then, through inheritance from that abstract base class, derived classes are formed that all behave similarly. The device-driver methods are declared as abstract methods in the abstract base class. The implementations of these abstract methods are provided in the derived classes that correspond to the specific types of device drivers. New devices are always being developed, often long after the operating system has been released. When you buy a new device, it comes with a device driver provided by the device vendor. The device is immediately operational after you connect it to your computer and install the device driver. This is another elegant example of how polymorphism makes systems extensible.