Combining composition | CodeGuru

Combining composition

Bruce Eckel’s Thinking in Java Contents | Prev | Next and inheritance It is very common to use composition and inheritance together. The following example shows the creation of a more complex class, using both inheritance and composition, along with the necessary constructor initialization: //: PlaceSetting.java // Combining composition & inheritance   class Plate { […]

Written By
CodeGuru Staff
CodeGuru Staff
Mar 1, 2001
4 minute read
CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More

and
inheritance

It


is very common to use composition and inheritance together. The following


example shows the creation of a more complex class, using both inheritance and


composition, along with the necessary

constructor
initialization:
//: PlaceSetting.java
// Combining composition & inheritance
 
class Plate {
  Plate(int i) {
    System.out.println("Plate constructor");
  }
}
 
class DinnerPlate extends Plate {
  DinnerPlate(int i) {
    super(i);
    System.out.println(
      "DinnerPlate constructor");
  }
}
 
class Utensil {
  Utensil(int i) {
    System.out.println("Utensil constructor");
  }
}
 
class Spoon extends Utensil {
  Spoon(int i) {
    super(i);
    System.out.println("Spoon constructor");
  }
}
 
class Fork extends Utensil {
  Fork(int i) {
    super(i);
    System.out.println("Fork constructor");
  }
}
 
class Knife extends Utensil {
  Knife(int i) {
    super(i);
    System.out.println("Knife constructor");
  }
}
 
// A cultural way of doing something:
class Custom {
  Custom(int i) {
    System.out.println("Custom constructor");
  }
}
 
public class PlaceSetting extends Custom {
  Spoon sp;
  Fork frk;
  Knife kn;
  DinnerPlate pl;
  PlaceSetting(int i) {
    super(i + 1);
    sp = new Spoon(i + 2);
    frk = new Fork(i + 3);
    kn = new Knife(i + 4);
    pl = new DinnerPlate(i + 5);
    System.out.println(
      "PlaceSetting constructor");
  }
  public static void main(String[] args) {
    PlaceSetting x = new PlaceSetting(9);
  }
} ///:~ 

While


the compiler forces you to initialize the base classes, and requires that you


do it right at the beginning of the constructor, it doesn’t watch over


you to make sure that you initialize the member objects, so you must remember


to pay attention to that.


Guaranteeing
proper cleanup

Java


doesn’t have the C++ concept of a

destructor,
a method that is automatically called when an object is destroyed. The reason
is probably that in Java the practice is simply to forget about objects rather
than to destroy them, allowing the
garbage
collector to reclaim the memory as necessary.

Often


this is fine, but there are times when your class might perform some activities


during its lifetime that require cleanup. As mentioned in Chapter 4, you


can’t know when the garbage collector will be called, or if it will be


called. So if you want something cleaned up for a class, you must write a


special method to do it explicitly, and make sure that the client programmer


knows that they must call this method. On top of this, as described in Chapter


9 (

exception
handling), you must guard against an exception by putting such cleanup in a
finally
clause.

Consider


an example of a computer-aided design system that draws pictures on the screen:

//: CADSystem.java
// Ensuring proper cleanup
import java.util.*;
 
class Shape {
  Shape(int i) {
    System.out.println("Shape constructor");
  }
  void cleanup() {
    System.out.println("Shape cleanup");
  }
}
 
class Circle extends Shape {
  Circle(int i) {
    super(i);
    System.out.println("Drawing a Circle");
  }
  void cleanup() {
    System.out.println("Erasing a Circle");
    super.cleanup();
  }
}
 
class Triangle extends Shape {
  Triangle(int i) {
    super(i);
    System.out.println("Drawing a Triangle");
  }
  void cleanup() {
    System.out.println("Erasing a Triangle");
    super.cleanup();
  }
}
 
class Line extends Shape {
  private int start, end;
  Line(int start, int end) {
    super(start);
    this.start = start;
    this.end = end;
    System.out.println("Drawing a Line: " +
           start + ", " + end);
  }
  void cleanup() {
    System.out.println("Erasing a Line: " +
           start + ", " + end);
    super.cleanup();
  }
}
 
public class CADSystem extends Shape {
  private Circle c;
  private Triangle t;
  private Line[] lines = new Line[10];
  CADSystem(int i) {
    super(i + 1);
    for(int j = 0; j < 10; j++)
      lines[j] = new Line(j, j*j);
    c = new Circle(1);
    t = new Triangle(1);
    System.out.println("Combined constructor");
  }
  void cleanup() {
    System.out.println("CADSystem.cleanup()");
    t.cleanup();
    c.cleanup();
    for(int i = 0; i < lines.length; i++)
      lines[i].cleanup();
    super.cleanup();
  }
  public static void main(String[] args) {
    CADSystem x = new CADSystem(47);
    try {
      // Code and exception handling...
    } finally {
      x.cleanup();
    }
  }
} ///:~ 

Everything


in this system is some kind of


Shape

(which is itself a kind of


Object

since it’s implicitly inherited from the root class). Each class redefines


Shape

’s


cleanup( )

method in addition to calling the base-class version of that method using


super

.


The specific


Shape

classes


Circle

,


Triangle

and


Line

all have constructors that “draw,” although any method called


during the lifetime of the object could be responsible for doing something that


needs cleanup. Each class has its own


cleanup( )

method to restore non-memory things back to the way they were before the object


existed.

In


main( )

,


you can see two keywords that are new, and won’t officially be introduced


until Chapter 9:

try
and
finally.
The
try
keyword indicates that the block that follows (delimited by curly braces) is a
guarded
region
,
which means that it is given special treatment. One of these special treatments
is that the code in the
finally
clause following this guarded region is
always
executed, no matter how the
try
block exits. (With exception handling, it’s possible to leave a
try
block in a number of non-ordinary ways.) Here, the
finally
clause is saying “always call
cleanup( )
for
x,
no matter what happens.” These keywords will be explained thoroughly in
Chapter 9.

Note


that in your cleanup method you must also pay attention to the calling order


for the base-class and member-object cleanup methods in case one subobject


depends on another. In general, you should follow the same form that is imposed


by a C++ compiler on its destructors: First perform all of the work specific to


your class (which might require that base-class elements still be viable) then


call the base-class cleanup method, as demonstrated here.

There


can be many cases in which the cleanup issue is not a problem; you just let the


garbage collector do the work. But when you must do it explicitly, diligence


and attention is required.



Order
of garbage collection

There’s


not much you can rely on when it comes to

garbage
collection. The garbage collector might never be called. If it is, it can
reclaim objects in any order it wants. In addition, implementations of the
garbage collector in Java 1.0

often don’t call the
finalize( )
methods. It’s best to not rely on garbage collection for anything but
memory reclamation. If you want cleanup to take place, make your own cleanup
methods and don’t rely on
finalize( ).
(As mentioned earlier, Java 1.1

can be forced to call all the finalizers.)

Name
hiding

Only


C++ programmers might be surprised by name hiding, since it works differently


in that language.

If
a Java base class has a method name that’s overloaded several times,
redefining that method name in the derived class will
not
hide
any of the base-class versions. Thus overloading works regardless of whether
the method was defined at this level or in a base class:
//: Hide.java
// Overloading a base-class method name
// in a derived class does not hide the
// base-class versions
 
class Homer {
  char doh(char c) {
    System.out.println("doh(char)");
    return 'd';
  }
  float doh(float f) {
    System.out.println("doh(float)");
    return 1.0f;
  }
}
 
class Milhouse {}
 
class Bart extends Homer {
  void doh(Milhouse m) {}
}
 
class Hide {
  public static void main(String[] args) {
    Bart b = new Bart();
    b.doh(1); // doh(float) used
    b.doh('x');
    b.doh(1.0f);
    b.doh(new Milhouse());
  }
} ///:~ 

As


you’ll see in the next chapter, it’s far more common to override


methods of the same name using exactly the same signature and return type as in


the base class. It can be confusing otherwise (which is why C++ disallows it,


to prevent you from making what is probably a mistake).


Contents

|

Prev

|

Next
CodeGuru Logo

CodeGuru covers topics related to Microsoft-related software development, mobile development, database management, and web application programming. In addition to tutorials and how-tos that teach programmers how to code in Microsoft-related languages and frameworks like C# and .Net, we also publish articles on software development tools, the latest in developer news, and advice for project managers. Cloud services such as Microsoft Azure and database options including SQL Server and MSSQL are also frequently covered.

Property of TechnologyAdvice. © 2026 TechnologyAdvice. All Rights Reserved

Advertiser Disclosure: Some of the products that appear on this site are from companies from which TechnologyAdvice receives compensation. This compensation may impact how and where products appear on this site including, for example, the order in which they appear. TechnologyAdvice does not include all companies or all types of products available in the marketplace.