Classes and objects in Java

Learn how to make classes, fields, methods, constructors, and objects work together in your Java applications

1 2 3 Page 2
Page 2 of 3

This example presents a method named average for calculating and outputting the average of an array of doubles. In addition to parameter values, it declares local variables sum and i to help in the calculation. sum's lifetime and scope range from its point of declaration to the end of the method. i's lifetime and scope are confined to the for loop.

A local variable's lifetime and scope are limited to the block in which it's been declared, as well as to sub-blocks. This is why i is inaccessible outside of the for loop, whose header is followed by a single-statement block. The following example should clarify this fact:

{
  int i;
  {
     i = 1; // okay: i still exists
  }
}
i = 2; // error: i no longer exists

Method overloading

Java lets you declare methods with the same name but with different parameter lists in the same class. This feature is known as method overloading. When the compiler encounters a method-call expression, it compares the called method's comma-separated list of arguments with each overloaded method's parameter list as it looks for the correct method to call.

Two same-named methods are overloaded when their parameter lists differ in number or order of parameters. Alternatively, two same-named methods are overloaded when at least one parameter differs in type. For example, consider the following four draw() methods, which draw a shape or string at the current or specified draw position:

void draw(Shape shape)
{
   // drawing code
}

void draw(Shape shape, double x, double y)
{
   // drawing code
}

void draw(String string)
{
   // drawing code
}

void draw(String string, double x, double y)
{
   // drawing code
}

When the compiler encounters draw("abc");, it will select the third method because it offers a matching parameter list. However, what will the compiler do when it encounters draw(null, 10, 20);? It will report a "reference to draw is ambiguous" error message because there are two methods from which to choose.

You cannot overload a method by changing only the return type. For example, you couldn't specify int add(int x, int y) and double add(int x, int y) because the compiler doesn't have enough information to distinguish between these methods when it encounters add(4, 5); in source code. The compiler would report a "redefinition" error.

Return statement

Sometimes, a method must terminate its execution before finishing. For example, a method for plotting a pixel might detect negative coordinates. Other methods need to return a value to their callers. For both situations, Java provides the return statement to terminate method execution and return control to the method's caller. This statement has the following syntax:

return [ expression ] ;

You can specify return without an expression to prematurely exit a method or a constructor. For example, consider a copy() method that copies bytes from the standard input stream (obtained via System.in.read() method calls) to the standard output stream (via System.out.print() method calls). This method is shown below:

static void copy() throws java.io.IOException // I'll discuss throws and exceptions
{                                             // in a future article.
   while (true)
   {
      int _byte = System.in.read();
      if (_byte == -1)
         return;
      System.out.print((char) _byte);
   }
}

System.in.read() reads bytes from the standard input stream, which defaults to the keyboard but can be redirected to a file. When the stream is redirected to a file, -1 is returned when there are no more bytes to read. When copy() detects this situation, it executes return to return from the infinite loop to its caller.

You can specify return with an expression to return a value to the method's caller. (Constructors don't support this version of return because they don't have return types.) For example, you might return early from a method that is searching for a specific array value when the value is found. This scenario is demonstrated in the following example:

static int search(int[] values, int srchValue)
{
   for (int i = 0; i < values.length; i++)
      if (values[i] == srchValue)
         return i; // return index of found value
   return -1; // -1 is an invalid index, so it's useful for indicating "value not found"
}

Constructors: Initializing objects

As well as explicitly assigning values to fields, a class can declare one or more blocks of code for more extensive object initialization. Each code block is a constructor. Its declaration consists of a header followed by a brace-delimited body. The header consists of a class name (a constructor doesn't have its own name) followed by an optional parameter list:

className ( [parameterList] )
{
   // constructor body
}

The className must match the name of the class in which the constructor is declared. The parameterList is a comma-separated list of parameters. A brace-delimited body containing code to execute when the constructor is called follows. Unlike a method, a constructor doesn't have a return type because it doesn't return any value.

The following example declares a constructor in the Book class. The constructor initializes a Book object's title and pubYear fields to the arguments that were passed to the constructor's _title and _pubYear parameters when the object was created. The constructor also increments the count class field:

class Book
{
   // ...

   Book(String _title, int _pubYear)
   {
      title = _title;
      pubYear = _pubYear;
      ++count;
   }

   // ...
}

The parameter names have leading underscores to prevent a problem with the assignments. For example, if you renamed _title to title and specified title = title;, you would have merely assigned the parameter's value to the parameter, which accomplishes nothing. However, you can avoid this problem by prefixing the field names with this.:

class Book
{
   // ...

   Book(String title, int pubYear)
   {
      this.title = title;
      this.pubYear = pubYear;
      ++count;
   }

   // ...

   void setTitle(String title)
   {
      this.title = title;
   }

   void setPubYear(int pubYear)
   {
      this.pubYear = pubYear;
   }

   // ...
}

A parameter (or local variable) name that's identical to an instance field name shadows (meaning hides or masks) the field. Keyword this represents the current object (actually, its reference). Prepending this. to the field name removes the shadowing by accessing the field name instead of the same-named parameter.

Although you can initialize fields such as title and pubYear through the assignments shown above, it's preferable to perform the assignments via setter methods such as setTitle() and setPubYear(), as demonstrated below:

class Book
{
   // ...

   Book(String title, int pubYear)
   {
      setTitle(title);
      setPubYear(pubYear);
      ++count;
   }

   // ...
}

Note that in the future these methods might perform additional initialization tasks; why duplicate this code in the constructor?

Constructor calling

Classes can declare multiple constructors. For example, consider a Book constructor that accepts a title argument only and sets the publication year to -1 to indicate that the year of publication is unknown. This extra constructor along with the original constructor are shown below:

class Book
{
   // ...

   Book(String title)
   {
      setTitle(title);
      setPubYear(-1);
      ++count;
   }

   Book(String title, int pubYear)
   {
      setTitle(title);
      setPubYear(pubYear);
      ++count;
   }

   // ...
}

But there is a problem with this new constructor: it duplicates code (setTitle(title);)) located in the existing constructor. Duplicate code adds unnecessary bulk to the class. Java provides a way to avoid this duplication by offering this() syntax for having one constructor call another:

class Book
{
   // ...

   Book(String title)
   {
      this(title, -1);

      // Do not include ++count; here because it already
      // executes in the second constructor and would 
      // execute here after this() returns. You would end
      // up with one extra book in the count.
   }

   Book(String title, int pubYear)
   {
      setTitle(title);
      setPubYear(pubYear);
      ++count;
   }

   // ...
}

The first constructor uses keyword this followed by a bracketed argument list to call the second constructor. The single parameter value is passed unchanged as the first argument, and -1 is passed as the second argument. When using this(), remember that it must be the first piece of code in a constructor; otherwise, the compiler reports an error.

Objects: Working with class instances

Once you have declared a class, you can create objects from it. An object is nothing more than a class instance. For example, now that the Book class has been declared, you can create one or more Book objects. Accomplish this task by specifying the new operator followed by a Book constructor, as follows:

Book book = new Book("A Tale of Two Cities", 1859);

new loads Book into memory and then calls its constructor with arguments "A Tale of Two Cities" and 1859. The object is initialized to these values. When the constructor returns from its execution, new returns a reference (some kind of pointer to an object) to the newly initialized Book object. This reference is then assigned to the book variable.

Accessing fields

After creating a Book object, you can access its instance fields by using the member access operator (.) with the Book reference:

System.out.println(book.title); // Output: A Tale of Two Cities
book.pubYear = 2019;
System.out.println(book.pubYear); // Output: 2019

You don't have to create any Book objects to access class fields. Instead, you prepend the class name and member access operator to the class method's name when accessing these fields:

System.out.println(Book.count); // Output: 1
Book.count = 0;
System.out.println(Book.count); // Output: 0

Calling methods

After creating a Book object, you can call its getTitle() and getPubYear() methods to return the instance field values. Also, you can call setTitle() and setPubYear() to set new values. In either case, you use the member access operator with the Book reference to accomplish this task:

System.out.println(book.getTitle()); // Output: A Tale of Two Cities
System.out.println(book.getPubYear()); // Output: 1859
book.setTitle("Moby Dick");
book.setPubYear(1851);
System.out.println(book.getTitle()); // Output: Moby Dick
System.out.println(book.getPubYear()); // Output: 1851

You don't have to create any Book objects to call class methods. Instead, you prepend the class name and member access operator to the class method's name when calling these methods:

Book.showCount(); // Output: count = 1

I previously mentioned that instance methods affect only the objects on which they are called; they don't affect other objects. The following example reinforces this truth by creating two Book objects and then accessing each object's title, which is subsequently output:

Book book1 = new Book("A Tale of Two Cities", 1859);
Book book2 = new Book("Moby Dick", 1851);
Book book3 = new Book("Unknown");
System.out.println(book1.getTitle()); // Output: A Tale of Two Cities
System.out.println(book2.getTitle()); // Output: Moby Dick
System.out.println(book3.getPubYear()); // Output: -1
Book.showCount(); // Output: count = 3

Calling varargs methods

When calling a method that takes one or more array arguments, you either pass the name of an array or specify extra syntax that tends to clutter source code. For example, I previously presented the void average(double[] values) class method that calculates the average value from an array of double precision floating-point values, and then outputs the average. The following code fragment shows the two ways you could call this method:

double[] values = { 1.0, 2.0, 3.0, 4.0 };
average(values);
average(new double[] { 1.0, 2.0, 3.0, 4.0 });

Java 5 introduced a variable arguments (varargs) feature to reduce the clutter when passing an array to a method or constructor. To use varargs, declare the method or constructor with ... after the rightmost parameter type name in the method's/constructor's parameter list:

void average(double... values) { /* ... */ }

The compiler treats ... as syntactic sugar for declaring an array. In the example, we have simply replaced [] with .... However, this syntactic sugar allows us to specify 1.0, 2.0, 3.0, 4.0 without the surrounding new double[] { } clutter when calling this method:

average(1.0, 2.0, 3.0, 4.0);

In spite of the ... syntax, the values parameter is still an array of type double[]. You don't have to modify the code within average()'s body to interact with values.

Information hiding and access levels

1 2 3 Page 2
Page 2 of 3