Classes, Objects and Types
Navigate Language Fundamentals topic: ) |
An object is composed of fields and methods. The fields, also called data members, characteristics, attributes, or properties, describe the state of the object. The methods generally describe the actions associated with a particular object. Think of an object as a noun, its fields as adjectives describing that noun, and its methods as the verbs that can be performed by or on that noun.
For example, a sports car is an object. Some of its fields might be its height, weight, acceleration, and speed. An object's fields just hold data about that object. Some of the methods of the sports car could be "drive", "park", "race", etc. The methods really don't mean much unless associated with the sports car, and the same goes for the fields.
The blueprint that lets us build our sports car object is called a class. A class doesn't tell us how fast our sports car goes, or what color it is, but it does tell us that our sports car will have a field representing speed and color, and that they will be say, a number and a word (or hex color code), respectively. The class also lays out the methods for us, telling the car how to park and drive, but these methods can't take any action with just the blueprint — they need an object to have an effect.
In Java, a class is located in a file similar to its own name. If you want to have a class called SportsCar
, its source file needs to be SportsCar.java
. The class is created by placing the following in the source file:
Code listing 3.13: SportsCar.java
public class SportsCar {
/* Insert your code here */
}
|
The class doesn't do anything yet, as you will need to add methods and field variables first.
The objects are different from the primitive types because:
- The primitive types are not instantiated.
- In the memory, for a primitive type only its value is stored. For an object, also a reference to an instance can be stored.
- In the memory, the allocated space of a primitive type is fixed, whatever their value. The allocated space of an object can vary, for instance either the object is instantiated or not.
- The primitive types don't have methods callable on them.
- A primitive type can't be inherited.
Instantiation and constructors
editIn order to get from class to object, we "build" our object by instantiation. Instantiation simply means to create an instance of a class. Instance and object are very similar terms and are sometimes interchangeable, but remember that an instance refers to a specific object, which was created from a class.
This instantiation is brought about by one of the class's methods, called a constructor. As its name implies, a constructor builds the object based on the blueprint. Behind the scenes, this means that computer memory is being allocated for the instance, and values are being assigned to the data members.
In general there are four constructor types: default, non-default, copy, and cloning.
A default constructor will build the most basic instance. Generally, this means assigning all the fields values like null, zero, or an empty string. Nothing would stop you, however, from setting the color of your default sports car color to red, but this is generally bad programming style. Another programmer would be confused if your basic car came out red instead of say, colorless.
Code section 3.79: A default constructor.
SportsCar car = new SportsCar();
|
A non-default constructor is designed to create an object instance with prescribed values for most, if not all, of the object's fields. The car is red, goes from 0-60 in 12 seconds, tops out at 190mph, etc.
Code section 3.80: A non-default constructor.
SportsCar car = new SportsCar("red", 12, 190);
|
A copy constructor is not included in the Java language, however one can easily create a constructor that does the same as a copy constructor. It's important to understand what it is. As the name implies, a copy constructor creates a new instance to be a duplicate of an already existing one. In Java, this can be also accomplished by creating the instance with the default constructor, and then using the assignment operator to equivocate them. This is not possible in all languages though, so just keep the terminology under your belt.
Java has the concept of cloning an object, and the end results are similar to the copy constructor. Cloning an object is faster than creation with the new
keyword, because all the object memory is copied at once to the destination cloned object. This is possible by implementing the Cloneable
interface, which allows the method Object.clone()
to perform a field-by-field copy.
Code section 3.81: Cloning object.
SportsCar car = oldCar.clone();
|
Type
editWhen an object is created, a reference to the object is also created. The object can not be accessed directly in Java, only through this object reference. This object reference has a type assigned to it. We need this type when passing the object reference to a method as a parameter. Java does strong type checking.
Type is basically a list of features/operations, that can be performed through that object reference. The object reference type is basically a contract that guarantees that those operations will be there at run time.
When a car is created, it comes with a list of features/operations listed in the user manual that guarantees that those will be there when the car is used.
When you create an object from a class by default its type is the same as its class. It means that all the features/operations the class defined are there and available, and can be used. See below:
Code section 3.82: Default type.
(new ClassName()).operations();
|
You can assign this to a variable having the same type as the class:
Code section 3.83: A variable having the same type as the class.
ClassName objRefVariable = new ClassName();
objRefVariable.operations();
|
You can assign the created object reference to the class, super class, or to an interface the class implements:
Code section 3.84: Using the super class.
SuperClass objectRef = new ClassName(); // features/operations list are defined by the SuperClass class
...
Interface inter = new ClassName(); // features/operations list are defined by the interface
|
In the car analogy, the created car may have different Types of drivers. We create separate user manuals for them, an Average user manual, a Power user manual, a Child user manual, or a Handicapped user manual. Each type of user manual describes only those features/operations appropriate for the type of driver. For instance, the Power driver may have additional gears to switch to higher speeds, that are not available to other type of users...
When the car key is passed from an adult to a child we are replacing the user manuals, that is called Type Casting.
In Java, casts can occur in three ways:
- up casting going up in the inheritance tree, until we reach the
Object
- up casting to an interface the class implements
- down casting until we reach the class the object was created from
Autoboxing/unboxing
editAutoboxing and unboxing, language features since Java 1.5, make the programmer's life much easier when it comes to working with the primitive wrapper types. Consider this code fragment:
Code section 3.85: Traditional object creation.
int age = 23;
Integer ageObject = new Integer(age);
|
Primitive wrapper objects were Java's way of allowing one to treat primitive data types as though they were objects. Consequently, one was expected to wrap one's primitive data type with the corresponding primitive wrapper object, as shown above.
Since Java 1.5, one may write as below and the compiler will automatically create the wrap object. The extra step of wrapping the primitive is no longer required. It has been automatically boxed up on your behalf:
Code section 3.86: Autoboxing.
int age = 23;
Integer ageObject = age;
|
Keep in mind that the compiler still creates the missing wrapper code, so one doesn't really gain anything performance-wise. Consider this feature a programmer convenience, not a performance booster. |
Each primitive type has a class wrapper:
Primitive type | Class wrapper |
byte |
java.lang.Byte
|
char |
java.lang.Character
|
short |
java.lang.Short
|
int |
java.lang.Integer
|
long |
java.lang.Long
|
float |
java.lang.Float
|
double |
java.lang.Double
|
boolean |
java.lang.Boolean
|
void |
java.lang.Void
|
Unboxing uses the same process in reverse. Study the following code for a moment. The if
statement requires a boolean
primitive value, yet it was given a Boolean wrapper object. No problem! Java 1.5 will automatically unbox this.
Code section 3.87: Unboxing.
Boolean canMove = new Boolean(true);
if (canMove) {
System.out.println("This code is legal in Java 1.5");
}
|
Question 3.11: Consider the following code:
Question 3.11: Autoboxing/unboxing.
Integer a = 10;
Integer b = a + 2;
System.out.println(b);
|
How many autoboxings and unboxings are there in this code?
Answer 3.11: Autoboxing/unboxing.
Integer a = 10;
Integer b = a + 2;
System.out.println(b);
|
3
- 1 autoboxing at line 1 to assign.
- 1 unboxing at line 2 to do the addition.
- 1 autoboxing at line 2 to assign.
- No autoboxing nor unboxing at line 3 as
println()
supports theInteger
class as parameter.
Methods in the Object
class
edit
Methods in the java.lang.Object
class are inherited, and thus shared in common by all classes.
The clone
method
edit
The java.lang.Object.clone()
method returns a new object that is a copy of the current object. Classes must implement the marker interface java.lang.Cloneable
to indicate that they can be cloned.
The equals
method
edit
The java.lang.Object.equals(java.lang.Object)
method compares the object to another object and returns a boolean
result indicating if the two objects are equal. Semantically, this method compares the contents of the objects whereas the equality comparison operator "==
" compares the object references. The equals
method is used by many of the data structure classes in the java.util
package. Some of these data structure classes also rely on the Object.hashCode
method—see the hashCode
method for details on the contract between equals
and hashCode
. Implementing equals() isn't always as easy as it seems, see 'Secrets of equals()' for more information.
The finalize
method
edit
The java.lang.Object.finalize()
method is called exactly once before the garbage collector frees the memory for object. A class overrides finalize
to perform any clean up that must be performed before an object is reclaimed. Most objects do not need to override finalize
.
There is no guarantee when the finalize
method will be called, or the order in which the finalize
method will be called for multiple objects. If the JVM exits without performing garbage collection, the OS may free the objects, in which case the finalize
method doesn't get called.
The finalize
method should always be declared protected
to prevent other classes from calling the finalize
method.
protected void finalize() throws Throwable { ... }
The getClass
method
edit
The java.lang.Object.getClass()
method returns the java.lang.Class
object for the class that was used to instantiate the object. The class object is the base class of reflection in Java. Additional reflection support is provided in the java.lang.reflect
package.
The hashCode
method
edit
The java.lang.Object.hashCode()
method returns an integer (int
). This integer can be used to distinguish objects although not completely. It quickly separates most of the objects and those with the same hash code are separated later in another way. It is used by the classes that provide associative arrays, for instance, those that implement the java.util.Map
interface
. They use the hash code to store the object in the associative array. A good hashCode
implementation will return a hash code:
- Stable: does not change
- Evenly distributed: the hash codes of unequal objects tend to be unequal and the hash codes are evenly distributed across integer values.
The second point means that two different objects can have the same hash code so two objects with the same hash code are not necessarily the same!
Since associative arrays depend on both the equals
and hashCode
methods, there is an important contract between these two methods that must be maintained if the objects are to be inserted into a Map
:
- For two objects a and b
a.equals(b) == b.equals(a)
- if
a.equals(b)
thena.hashCode() == b.hashCode()
- but
ifa.hashCode() == b.hashCode()
thena.equals(b)
In order to maintain this contract, a class that overrides the equals
method must also override the hashCode
method, and vice versa, so that hashCode
is based on the same properties (or a subset of the properties) as equals
.
A further contract that the map has with the object is that the results of the hashCode
and equals
methods will not change once the object has been inserted into the map. For this reason, it is generally a good practice to base the hash function on immutable properties of the object.
The toString
method
edit
The java.lang.Object.toString()
method returns a java.lang.String
that contains a text representation of the object. The toString
method is implicitly called by the compiler when an object operand is used with the string concatenation operators (+
and +=
).
The wait and notify thread signaling methods
editEvery object has two wait lists for threads associated with it. One wait list is used by the synchronized
keyword to acquire the mutex lock associated with the object. If the mutex lock is currently held by another thread, the current thread is added to the list of blocked threads waiting on the mutex lock. The other wait list is used for signaling between threads accomplished through the wait
and notify
and notifyAll
methods.
Use of wait/notify allows efficient coordination of tasks between threads. When one thread needs to wait for another thread to complete an operation, or needs to wait until an event occurs, the thread can suspend its execution and wait to be notified when the event occurs. This is in contrast to polling, where the thread repeatedly sleeps for a short period of time and then checks a flag or other condition indicator. Polling is both more computationally expensive, as the thread has to continue checking, and less responsive since the thread won't notice the condition has changed until the next time to check.
The wait
methods
edit
There are three overloaded versions of the wait
method to support different ways to specify the timeout value: java.lang.Object.wait()
, java.lang.Object.wait(long)
and java.lang.Object.wait(long, int)
. The first method uses a timeout value of zero (0), which means that the wait does not timeout; the second method takes the number of milliseconds as a timeout; the third method takes the number of nanoseconds as a timeout, calculated as 1000000 * timeout + nanos
.
The thread calling wait
is blocked (removed from the set of executable threads) and added to the object's wait list. The thread remains in the object's wait list until one of three events occurs:
- another thread calls the object's
notify
ornotifyAll
method; - another thread calls the thread's
java.lang.Thread.interrupt
method; or - a non-zero timeout that was specified in the call to
wait
expires.
The wait
method must be called inside of a block or method synchronized on the object. This insures that there are no race conditions between wait
and notify
. When the thread is placed in the wait list, the thread releases the object's mutex lock. After the thread is removed from the wait list and added to the set of executable threads, it must acquire the object's mutex lock before continuing execution.
The notify
and notifyAll
methods
edit
The java.lang.Object.notify()
and java.lang.Object.notifyAll()
methods remove one or more threads from an object's wait list and add them to the set of executable threads. notify
removes a single thread from the wait list, while notifyAll
removes all threads from the wait list. Which thread is removed by notify
is unspecified and dependent on the JVM implementation.
The notify methods must be called inside of a block or method synchronized on the object. This insures that there are no race conditions between wait
and notify
.