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.
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.
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.
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.)
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.
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 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.
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().
| Ball | Person who has checked out ball | |
|---|---|---|
| Color | Diameter | |
| red | 1 | Church |
| white | 2 | Turing |
| blue | 3 | Post |
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.
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.
Written by Michael Grobe
Academic Computing Services
The University of Kansas
August 2003