Java objects without adornment

Every Java tutorial I have examined introduces objects in ways that end up obscuring fundamental ideas with implementation details and/or language features that do not quite fit the object model, but work anyway. This description tries to present the model without using such adornments.

This material assumes some prior knowledge of the language. Unfortunately, the issues that have bothered me may not be those that concern other readers, so I can make no promises about the usefulness of this document to others. However, please feel free to send comments and suggestions to grobe@ku.edu.

The primal class and inheritance

Java "classes" define "methods" and "instance variables". In the simplest sense, "methods are the things a class can do, and instance variables are things the class can be,"[1] or represent.

Every object is an instance of a class, and as a result a class may be thought of as a "template" for objects created or "instantiated" on that class.

Being "used as a template" means that an object instantiated from a class is given every instance variable and method defined in that class.

A Java class may be defined as an "extension" of another class, which means simply that it "inherits" any instance variables and methods defined in that class, commonly know as the "superclass."

And since all classes explicitly or implicitly extend class Object, possibly through several intervening superclasses, all classes and objects inherit Object's methods and instance variables.

Another way to say this is that all classes are ultimately "differentiated" versions of Object.

As will be discussed later, a class is actually something more than a simple "template." To wit: classes CAN contain methods that can be executed without instantiating the class that contains them, and variables that can be accessed likewise.

The primal object

Here is a simple class that creates an instance of the Object Class.

public class makeAnObject extends Object
{
   public static void main( String[] args )
   {
      Object quack = new Object();

      System.out.println( "The new object has hash value:" + quack.hashCode() );
      System.out.println( "The new object is of class:" + quack.getClass() );
      System.out.println( "The new object is of class:" +
           quack.getClass().getName() );    /* why does this work?  */
                             /* because getClass returns an object of type */
      System.exit(1);       /* Class, which has a getName() method */ 
   } 
}
This class declares itself an extension of the Object Class, which is unnecessary since all Objects are, but it may be a good reminder. This class also declares no constructor since it will not be itself instantiated. (Under the covers, a default constructor will be available, of course, but this can be ignored for the time being.) When this class is compiled and executed, you will see something like:
   C:\directory> javac makeAnObject.java
   C:\directory> java makeAnObject
   The new object has hash value: 28980466
   The new object is of class: class Object
   The new object is of class: Object

The method "main" was executed when Java started the class. Method main

The other methods invoked within method main, getClass() and hashCode(), were inherited from the Object class. They are among 10 or so defined within class Object. Another important method is equals() which can determine whether two Objects are "equal," in senses to be discussed later.

Class variables and class methods

Instance variables are accessible to any method in an object in which they are defined. Variables defined WITHIN methods, however, are "local" variables and usable only within the method in which they are defined.

Only one instance will be created for any instance variable declared "static", no matter how many objects are instantiated from the class that defines them. That one instance can be shared by any of the class's objects. Static variables are also known as "class variables", and are created "when the class is created."[2]

Here is a simple class that defines and uses one class variable, the private String variable called numberOfInstances, to count the number of times the class is instantiated, and one static method, to return the count to any ouside inquirer:

public class derivedObject extends Object
{
   private static String numberOfInstances = new String( "" );

   public derivedObject()    /* the constructor method. */
   {
      super();     /* First construct the ancestor Object, and then */
                   /* concatenate a single character to the string. */
      numberOfInstances = numberOfInstances.concat( "x" );
   }
   public static String getNumberOfInstances()
   {
      return numberOfInstances;
   }
}
Note that method derivedObject() has no type. It is the "constructor" method that is invoked when the "new" operator is called on the class name. The call to "super" must occur before any other activity within the constructor method, and is implicit if not specified. super() will construct an object of type Object (or some other superclass), and the constructor may then go on to perform other initializations, if necessary.

Also note that his class uses a "funky" method for counting instantiations. Its constructor concatenates a single "x" to the numberOfInstances string with each instantiation.

A minimally modified version of makeAnObject can be used to create several instances of the derivedObject and report the final count kept by derivedObject:

public class makeADerivedObject extends Object
{
   public static void main( String[] args )
   {
      derivedObject one = new derivedObject();
      derivedObject two = new derivedObject();
      derivedObject three = new derivedObject();

      System.out.println( "There were " + 
               derivedObject.getNumberOfInstances().length() +
               " instances of derivedObject created.\n" );

      System.exit(1);
   }
}
When these classes are compiled and run the results will look like:
   C:\directory> javac aDerivedObject.java
   C:\directory> javac makeADerivedObject.java
   C:\directory> java makeADerivedObject

   There were 3 instances of derivedObject created.
makeADerivativeObject prints the length of the string, returned by length(), as the number of instantiations of the derivedObject. (FYI: This approach was taken to avoid doing arithmetic with Integer class objects.)

Just as with static variables, only one instance will be created for any METHOD declared "static", and such methods are known as "class methods". The "main" method is declared static because there is no reason to have more than one such method communicating with the command line and instantiating objects of the class.

(Class methods referring to NO non-static instance variables can be executed without instantiating the class in which they are defined. The methods in class math, such as math.random(), math.max(), etc. are examples. They are executed by referring to their class and method names, without any reference to an object.

Along those same lines, the static instance variable used in derivedObject could be referenced by using a statement like:

   derivedObject.numberOfInstances = new String( "6" );
if numberOfInstances were NOT declared private. Note that this reference is made through the class name, rather than the name of any object instantiated on the class.)

Non-static instance variables

Here is a class that defines a slightly more detailed object:
public class Ball extends Object
{
   private String myColor = new String();    
               /* The String constructor provides a default value. */
   private Integer myDiameter = new Integer( "0" );  
               /* The Integer constructor requires a default value. */

   public Ball()    /* Here is the (optional) constructor for class Ball. */
   {
      super();
   }

   public void setColor( String newColor )
   {
      myColor = newColor;
   }
   public String getColor( )
   {
      return myColor;
   }

   public void setDiameter( Integer newDiameter )
   {
      myDiameter = newDiameter;
   }
   public Integer getDiameter( )
   {
      return myDiameter;
   }
}
This class has two non-static "instance variables," and each Ball object will have its own copy of these variables.

These variables are declared "private" so they cannot be accessed from any other object, and the class provides "get" and "set" methods to access those instance variables from other classes.

Here is a class that will create a single instance of class "Ball":

public class makeBall extends Object
{
   /* No constructor method is required since this class is not 
      meant to be instantiated. 
   */

   public static void main( String[] args )
   {
      Ball myBall = new Ball();

      myBall.setColor( new String( "red" ) );
      myBall.setDiameter( new Integer( 6 ) );

      System.out.println( "The new object has hash value: " +
                myBall.hashCode() );
      System.out.println( "The new object is of class: " +
                myBall.getClass().getName() );

      System.out.println( "myBall has color: " + myBall.getColor() );
      System.out.println( "myBall has diameter: " + myBall.getDiameter() );

      System.exit(1);
   }
}
when you compile and run this class you see something like:
   C:\directory> javac Ball.java
   C:\directory> javac makeBall.java
   java makeBall
   The new object has hash value: 32124414
   The new object is of class: Ball
   myBall has color: red
   myBall has diameter: 6
Note that makeBall creates an instance of class Ball, but does not instantiate itself, so makeBall needs no constructor. The default constructor will be available, but is never called.

Overloading the constructor method

Creating a new instance of a Ball object takes three statements in the above example. For example, to define myBall, we used:
   Ball myBall = new Ball();
   myBall.setColor( new String( "red" ) );
   myBall.setDiameter( new Integer( 6 ) );
A new version of the makeBall constructor can be created, however, that allows initial variable values to be sent as arguments through the constructor. This requires a new constructor method like:
   public Ball( String newColor, Integer newDiameter )
   {
      super();                        /* create the ancestor Object, */
      setColor( newColor );      /* and  then add instance variables. */
      setDiameter( newDiameter );
   }
This version of the constructor will be invoked whenever the constructor is called with two arguments of type String and Integer, in that order, as in:
   Ball myBall = new Ball( new String( "red" ), new Integer( 6 ) )
This new method is said to "overload" the constructor method. Additional methods that accept just one of the arguments, or both arguments in reverse order, could be provided to further overload the constructor. Overriding may be prevented by declaring a method "final".

Polymorphism and casting objects

Polymorphism is the name for the way in which every object is a derivative of at least one other class, Object, and possibly other classes descending in a chain from class Object.

Polymorphism allows objects to take on different identities in different contexts. For example, collections, such as ArrayLists, HashSets, HashMaps, TreeMaps, etc., can hold objects of any class, and even objects of different classes at the same time, by treating them all as simple Objects.

Collections simply manipulate objects of type Object, no matter what the most differentiated type an object might have. Java will automatically adjust to treating an object in its less-differentiated aspect. For example, if "zoo" is a HashSet, then one may insert objects of class "dog", "lion", or any other object, into zoo without special syntax.

Here is a class that uses the HashSet class from the java.util package to manage a "set" of strings holding the names of mathematicians of historical importance to computer science:

import java.util.*;

public class testHashSet          /* using automatic extension of Object */
{                                 /* no constructor required. */
   public static void main( String[] args )
   {
      HashSet mathematicians = new HashSet( 431 ); 
                /* assume about 431 entries; the default is about 101. */

      mathematicians.add( new String( "Church" ) );
      mathematicians.add( new String( "Turing" ) );
      mathematicians.add( new String( "Post" ) );
      mathematicians.add( new String( "Turing" ) ); 
                        /* yes, this is a duplicate.  */

      Iterator hashSetIterator = mathematicians.iterator();

      while( hashSetIterator.hasNext() )
      {
         Object aMathematician = hashSetIterator.next();

         System.out.println( "The new object has hash value: " +
                   aMathematician.hashCode() );
         System.out.println( "The new object is of class: " +
                   aMathematician.getClass().getName() );

         System.out.println(  "Mathematician: " + aMathematician + "\n" );
      }

      System.exit(1);
   }
}
When testHashSet is executed, the result looks like:
   The new object has hash value: -1778566063
   The new object is of class: java.lang.String
   Mathematician: Turing
   
   The new object has hash value: 2017797575
   The new object is of class: java.lang.String
   Mathematician: Church
   
   The new object has hash value: 2493632
   The new object is of class: java.lang.String
   Mathematician: Post
Note that only one instance of Turing remains in the set, since sets contain only unique members. The second insertion was ignored.

The equals() method from class Object determines whether two Objects are considered "equal". Later examples will investigate this in more detail.

The next class creates a similar set but it's members are objects of class Ball:

import java.util.*;

public class testHashBallSet      /* using automatic extension of Object */
{                                 /* no constructor needed */
   public static void main( String[] args )
   {
      HashSet balls = new HashSet( 431 );    /* assume about 431 entries. */

      Ball ballOne = new Ball( new String( "red" ), new Integer( 1 ) );

      Ball ballTwo = new Ball( new String( "white" ), new Integer( 2 ) );

      Ball ballThree = new Ball( new String( "blue" ), new Integer( 3 ) );

      balls.add( ballOne );            /* add all three balls to the set */
      balls.add( ballTwo );
      balls.add( ballThree );

      while( ballsIterator.hasNext() )
      {
         Object aBall = ballsIterator.next();   /*  this doesn't work.  */
         /* Ball aBall = ( Ball ) ballsIterator.next();  this DOES  */

         System.out.println( "The new object has hash value: " +
                aBall.hashCode() );
         System.out.println( "The new object is of class: " +
                aBall.getClass().getName() );

         System.out.println( "The new ball is " + aBall.getColor() +
              " and has diameter " + aBall.getDiameter() + "\n" );
      }

      System.exit(1);
   }
}
When this class is compiled the result looks like:
   C: \directory\testHashBallSet.java:32: cannot resolve symbol
   symbol: variable color
   location: class java.lang.Object
            System.out.println( "The new ball is " + aBall.color +
                                                          ^
   C: \directory\testHashBallSet.java:33: cannot resolve symbol
   symbol: variable diameter
   location: class java.lang.Object
           " and has diameter " + aBall.diameter + "\n" );
                                                            ^
   2 errors
These problems can be eliminated by "casting" the aBall Object from Object to Ball, as in:
    Ball aBall = ( Ball ) ballsIterator.next(); 
at which point execution compilation will be successful and execution will look like:
   C:\directory> java testHashBallSet
   
   The new object has hash value: 39351493
   The new object is of class: Ball
   The new ball is blue and has diameter 3
   
   The new object has hash value: 1466222
   The new object is of class: Ball
   The new ball is red and has diameter 1
   
   The new object has hash value: 1470324279
   The new object is of class: Ball
   The new ball is white and has diameter 2
If this were a more complicated example where objects of MULTIPLE types were used as keys in the HashSet, the class would have to call getClass().getName() on each removed object to decide how to cast each object removed from the set.

Polymorphism and overriding Object methods equals() and hashCode()

A slight modification to the example in the previous section produces some strange behavior. The following code adds a fourth ball to the set, and is inserted immediately following the three statements that added the first three balls to the set.

Here is the code that is added:

               /* Now define ball 4 like ball 2 and */
      Ball ballFour = new Ball( new String( "white" ) ), new Integer( 2 ) );
      balls.add( ballFour );       /* ...add it to the set */
When the class is executed the results appear like:
   C:\directory> java testHashBallSet
   
   The new object has hash value: 39351493
   The new object is of class: Ball
   The new ball is blue and has diameter 3
   
   The new object has hash value: 1466222
   The new object is of class: Ball
   The new ball is red and has diameter 1
   
   The new object has hash value: 1470324279
   The new object is of class: Ball
   The new ball is white and has diameter 2
   
   The new object has hash value: 1470324279
   The new object is of class: Ball
   The new ball is white and has diameter 2
Note that the third and fourth balls in the set have exactly the same attributes, so, in some contexts the user would actually want them to be considered to be the same object. However, the equals() method of the Object class only compares memory locations for each Object and will therefore, see them as UNequal.

In contexts where this presents a problem, the equals() and hashCode() methods of the Object class must be "overridden" by defining them within the Ball class, as is done with the following code:

   public boolean equals( Object anObject )
   {
      if( ( anObject != null ) && ( anObject.getClass().getName() != "Ball" ) )
      {
         return false;      /* Can't be equal if it isn't a Ball  */
      }                     /* at least in this context. */
      else
      {      /* Cast anObject to Ball to be able to compare its
                instance variable values with those of this object.  */
         Ball aBall = ( Ball ) anObject;  
         if( myColor.equals( aBall.getColor() ) &&
           ( myDiameter.intValue() == aBall.getDiameter().intValue() ) )
         {
            return true;    /* The compared instance values are identical. */
         }
         else
         {
            return false;   /* The compared instance values are different. */
         }
      }
   }

   public int hashCode()    /* invent a new hash formula that guarantees 
   {                           equal objects will have the same hash. */
          return 13 * myColor.hashCode() + 17 * myDiameter.intValue();
   }
This version of equals() has the same signature as the predefined version, so that this is a method "override" rather than an "overload" as seen earlier with the constructor method.

When Java looks for the equals() method, it will look first among the methods of the Ball class, and find the new version, rather than the default version supplied with the Object class.

These modifications produce the following output when testHashBallSet is executed:

   C: \directory> java testHashBallSet
   
   The new object has hash value: 39351493
   The new object is of class: Ball
   The new ball is blue and has diameter 3
   
   The new object has hash value: 1466222
   The new object is of class: Ball
   The new ball is red and has diameter 1
   
   The new object has hash value: 1470324279
   The new object is of class: Ball
   The new ball is white and has diameter 2
With these modifications Ball objects are now treated as equal if they have the same contents.

Note that the Java specifications require that any two objects considered "equals" must return the same hashCode value. Thus, it is almost always necessary to override hashCode() when overriding equals().

Polymorphism and interfaces: overriding compareTo()

Suppose some organization checks out balls to its members and keeps track of who has which ball on a whiteboard with a table like:

Ball Person who has
checked out ball
ColorDiameter
red1Church
white2Turing
blue3Post

Now suppose, you want to have a program keep track of who's got which balls. (OK, so this is lame, but you get the idea and it avoids introducing new objects.)

Note first that each ball is unique; no two balls have both the same color and the same diameter. As a result, we can use a TreeMap to hold the information in this table. Here is a class that does so:

import java.util.*;

public class testTreeBallMap     /* using automatic extension of Object */
{                                /* no constructor needed */
   public static void main( String[] args )
   {        /* Define four Balls, ... */

      Ball ballOne = new Ball( new String( "red" ), new Integer( 1 ) );

      Ball ballTwo = new Ball( new String "white" ), new Integer( 2 ) );

      Ball ballThree = new Ball( new String( "blue" ), new Integer( 3 ) );

            /* ... the fourth of which is like the third. */
      Ball ballFour = new Ball( new String( "blue" ), new Integer( 3 ) );

            /* Define the checkout table as a TreeMap and fill it. */
      TreeMap ballTable = new TreeMap();     /* Build the table. */

      ballTable.put( ballOne, new String( "Church" ) );
      ballTable.put( ballTwo, new String( "Turing" ) );
      ballTable.put( ballThree, new String( "Post" ) );
      ballTable.put( ballFour, new String( "Kleene" ) );

            /* Define a set and fill it with all keys from the TreeMap. */
      Set hashKeys = ballTable.keySet();

            /* Define an Iterator to manage the hashKeys set. */
      Iterator hashKeyIterator = hashKeys.iterator();

      while( hashKeyIterator.hasNext() )
      {
         Ball aKey = ( Ball )hashKeyIterator.next();
         System.out.println( "The new object has hash value: " +
         ballTable.get( aKey ).hashCode() );
         System.out.println( "The new object is of class: " +
                ballTable.get( aKey ).getClass().getName() );

         System.out.println( "The "+ aKey.getDiameter() + " inch, " + 
                aKey.getColor() + " ball is checked out to " + 
                ballTable.get( aKey ) + "\n" );
      }
      System.exit(1);
   }
}

Note that Ball objects removed from the key set are cast back into their proper class, using:

       Ball aKey = ( Ball )hashKeyIterator.next();
However, this is not enough to get testHashBallMap to run properly. In particular, TreeMaps assume that their any class defining an object used as a key will "implement" the Comparable interface, which simply means that the key class will provide all the methods declared in the Comparable interface.

In this case, the only declared method is,

public int compareTo(Object anotherObject)
which compares the current object with another Object "anotherObject" to determine their relationship (smaller than, equal to, or larger than). For example compareTo() will return -1, 0, or 1, as the current object is smaller than, equal to, or larger than anotherObject.

TreeMap provides a compareTo() method, but it only compares Objects, and must be overridden to work with a user-defined object, in this case with Balls.

To use Balls as keys, then, the Ball class was declared as follows:

   public class Ball extends Object implements Comparable
and modified to do so by overriding the compareTo() method of the TreeMap class with the following version:
   public int compareTo( Object anObject )
   {
      if( ( anObject.getClass().getName() != "Ball" ) )
      {  /* Might want to throw an exception here...though in some contexts */
         /* objects of disparate types could be "comparable". */
         return -1;
      }
      else if ( anObject == null )
      {   
         return 1;    /* The null object seems like the "smallest". */
      }
      else
      {
         Ball aBall = ( Ball ) anObject;

         if( myDiameter.intValue() > aBall.getDiameter().intValue() )
         { return 1; }
         else if( myDiameter.intValue() < aBall.getDiameter().intValue() )
         { return -1; }
         else if( myColor.compareTo( aBall.getColor() ) > 0 )
         { return 1; }
         else if( myColor.compareTo( aBall.getColor() ) < 0 )
         { return -1; }
         else
         { return 0; }
      }
   }
Balls are compared first by size and then, if of equal size, by color.

When this change is made, the program will execute as:

   C:\directory> java testTreeBallMap

   The new object has hash value: 2017797575
   The new object is of class: java.lang.String
   The 1 inch, red ball is checked out to Church
   
   The new object has hash value: -1778566063
   The new object is of class: java.lang.String
   The 2 inch, white ball is checked out to Turing
   
   The new object has hash value: -2044931240
   The new object is of class: java.lang.String
   The 3 inch, blue ball is checked out to Kleene
Note that the keys appear to have emerged in order as defined by the compareTo() method, and that only three balls remain in the list, since ballThree and ballFour compare as equal, so ballFour replaced ballThree in the TreeMap.

Interfaces are Java's answer to the multiple inheritance features provided by other object oriented languages. In some other languages, a class may "extend" multiple superclasses. Java does not allow this, but allows a class to implement multiple interfaces.

An Interface is an "abastract class." An Interface acts much as an API, in that it defines a set of "abstract" methods that must be implemented by any class claiming to implement that Interface. Abstract methods have no bodies, but are composed only of method declarations. These abstract methods must be defined (overridden) within the first child class that implements them.

A brief list of adornments to the basic model

Java is a powerful, even elegant language, but it seems to have a "steep learning curve." This description of the unadorned functioning of Java object creation and use is intended to help readers separate the basics from the decoration, and thereby speed up and/or improve their understanding of the language. What follows is a brief list of features that seem to this writer to "muddy the waters", even though they also appear to be very useful, and well thought-out.

The major adornment to the basic object structure is the availability of "primitive variables," which hold values that are not objects: boolean, char, int, byte, short, long, double, etc. There are contexts in which primitive types cannot be used. For example, you cannot place a primitive value into a Collection object, such as an ArrayList or HashMap, since Collection objects rely upon Object polymorphism to work properly.

To circumvent such limitations, there exist "wrapper" classes for primitive variable types that can be used to embed primitive values within Objects. For example these statements:

   Integer intObject = new Integer( 6 );
   Double doubleObject = new Double( 2.12345678901234 );
create an Integer and a Double object from appropriate primitives.

To add complexity, these wrapper classes can perform ancillary functions such as converting strings to primitive types (rather than to embedded objects). For example:

   int aNumber = Integer.parseInt( "5" );
   double doubleObject = Double.parseDouble( "2.12345678901234" );
convert strings to primitive int and double values, respectively.

Optional language syntax provides another form of adornment. For example, constructors need not call super(); the call will be inserted automatically. In fact, constructors may be omitted altogether if they do nothing. In addition, String objects can be defined using the same syntax used to define primitives. That is, an explicit use of "new" is not required even though Strings are objects.

Also, interface variables may be referenced via a "partially qualified" dot notation. For example, if they were not declared "private", Ball instance variables could be set or read using statements like:

   ballOne.myColor = new String( "red" );
   System.out.println( "Ball One's color is: " + ballOne.myColor );
   ballTwo.myDiameter = new Integer( 2 );
   System.out.println( "Ball Two's diameter is: " + ballTwo.myDiameter );
instead of via the getColor(), setColor(), getDiameter(), and setDiameter() methods. Not only does this demodularize the Ball object, but this notation may be ambiguous if multiple classes in a class hierarchy define class variables with the same name. Using the fully qualified name, specifying every super class will prevent this problem, though still it leaves the object somewhat vulnerable. Unfortunately, requiring get and set methods for each instance variable is tedious.

Local variable scoping is actually more restrictive than mentioned above. Local variables are usable only within a "block". For example, iteration variables defined within a for statement are available only within the scope of the for.

References

In addition to online Sun documentation, two books were used as references for this document:
  1. Bates, Bert and Kathy Sierra, Head First Java, O'Reilley & Associates, May 2003.
  2. Lemay, Laura and Roger Cadenhead, Teach Yourself Java 2 in 21 Days, Second edition, SAMS Publishing, 2000.
Both of these were extremely helpful. [2] was useful in learning the language, and [1] helped understand it much better.

 

Written by Michael Grobe
Academic Computing Services
The University of Kansas
August 2003

Appendix A: Self instantiation

All of the examples used in this document thusfar have separated an object class of interest from the class used to instantiate that class.

Such separation is not required by Java, but was done in the interests of clarity. This appendix presents another framework of equivalent power.

Here is a class that combines the derivedObject class and the makeADerivedObject class into a single class. It has a main method that allows it to execute on its own, instantiate itself, and return the same information returned by makeADerivedObject:.

public class anotherDerivedObject extends Object
{
   private static String numberOfInstances = new String( "" );

   public anotherDerivedObject()    /* the constructor method */
   {
      super();     /* First construct the ancestor Object, and then */
                   /* concatenate a single character to the string. */
      numberOfInstances = numberOfInstances.concat( "x" );
   }

   public static void main( String[] args )
   {
      anotherDerivedObject one = new anotherDerivedObject();
      anotherDerivedObject two = new anotherDerivedObject();
      anotherDerivedObject three = new anotherDerivedObject();

      System.out.println( "There were " + numberOfInstances.length() +
                " instances of anotherDerivedObject created.\n" );

      System.exit(1);
   }
}
When this class is compiled and executed, you will see something like:
   C:\directory> javac anotherDerivedObject.java
   C:\directory> java anotherDerivedObject

   There were 3 instances of anotherDerivedObject created.  
Although this is completely reasonable Java, the notion of a class instantiating itself remains discomforting to some students, even though method main is not invoked.

In any event, a class that normally has no main method might be given one for testing. In such a case, the class would need to be instantiated from within that method, just as in the previous example.