Learn how to store data in objects

The journey from Java wanna-be to Java developer continues

In the previous Java 101 column, I discussed Java as an interpreted language and explained Java byte code and the Java Virtual Machine. You learned how to set up Java on a system using the Java 2 SDK, downloadable from Sun, and created your first simple Java program. I then covered the basics of object-oriented programming, and how it is centered around types and objects. Finally, I introduced the class construct in Java, and you created a class that looks like this:

public class AlarmClock {
  public void snooze() {{column,
    System.out.println("ZZZZZ");
  }
}

You also created a program (a class with a main method) that tests the AlarmClock class:

public class AlarmClockTest {
  public static void main(String[] args) {
    AlarmClock aClock = new AlarmClock();
    aClock.snooze();
  }
}

If you didn't read the last column, you'll probably want to review it before tackling this one.

Variables and primitive types

Though the snooze button is probably the most commonly used button on an alarm clock, the simple AlarmClock class you've created so far is still missing some important requirements. For instance, you have no way of manipulating how long the alarm clock will stay in snooze mode. However, before you can do that, you must take a more detailed look at how Java controls data.

Developers use variables in Java to hold data, with all variables having a data type and a name. The data type determines the values that a variable can hold. You will see in the examples below how integral types hold whole numbers, floating point types hold real numbers, and string types hold character strings.

Called primitive types, integral and floating point are the simplest data types that Java uses. The following program illustrates the integral type, which can hold both positive and negative whole numbers. This program also illustrates comments, which document your code but don't affect the program in any way.

/*
  * This is also a comment. The compiler ignores everything from
  * the first /* until a "star slash" which ends the comment.
  *
  * Here's the "star slash" that ends the comment.
  */
public class IntegerTest {
  public static void main(String[] args) {
    // Here's the declaration of an int variable called anInteger,
    // which you give an initial value of 100.
    int anInteger = 100;      // Declare and initialize anInteger
    System.out.println(anInteger); // Outputs 100
    // You can also do arithmetic with primitive types, using the
    // standard arithmetic operators.
    anInteger = 100 + 100;
    System.out.println(anInteger); // Outputs 200
  }
}

Java also uses floating point types, which can hold real numbers (numbers that include a decimal place).

Here is an example program:

public class DoubleTest {
  public static void main(String[] args) {
    // Here's the declaration of a double variable called aDouble.
    // You also give aDouble an initial value of 5.76.
    double aDouble = 5.76;     // Declare and initialize aDouble
    System.out.println(aDouble);  // Outputs 5.76
    // You can also do arithmetic with floating point types.
    aDouble = 5.76 + 1.45;
    System.out.println(aDouble);  // Outputs 7.21
  }
}

Try running the programs above. Remember, you have to compile them before you can run them:

javac *.java
java IntegerTest
java DoubleTest

Java uses four integral types and two floating point types, which both hold different ranges of numbers and take up varying amounts of storage space. The following table lists them, along with some of their properties:

Integral types
TypeSize (Bits)Range
byte8-128 to 127
short16-32,768 to 32,767
int32-2,147,483,648 to 2,147,483,647
long64-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
Integral and floating point types
Floating point types
TypeSize (Bits)Range
float32Single precision floating point (IEEE 754 conforming)

A string type holds strings, and handles them differently from the way integral and floating point types handle numbers. The Java language includes a String class to represent strings. You declare a string using the type String, and initialize it with a quoted string, a sequence of characters contained within double quotes, as shown below. You can also combine two strings using the + operator.

// Code fragment
// Declaration of variable s of type String,
// and initialization with quoted string "Hello."
String s = "Hello";
// Concatenation of string in s with quoted string " World"
String t = s + " World";
System.out.println(t);     // Outputs Hello World

Variable scope

In addition to type, scope is also an important characteristic of a variable. Scope establishes when a variable is created and destroyed and where a developer can access the variable within a program. The place in your program where you declare the variable determines its scope.

So far, I've discussed local variables, which hold temporary data that you use within a method. You declare local variables inside methods, and you can access them only from within those methods. This means that you can retrieve only local variables anInteger, which you used in IntegerTest, and aDouble, which you used in DoubleTest, from the main method in which they were declared and nowhere else.

You can declare local variables within any method. The example code below declares a local variable in the AlarmClock snooze() method:

public class AlarmClock {
     public void snooze() {
         // Snooze time in millisecond = 5 secs
         long snoozeInterval = 5000;
         System.out.println("ZZZZZ for: " + snoozeInterval);
     }
}

You can get to snoozeInterval only from the snooze() method, which is where you declared snoozeInterval. Refer to the example below:

public class AlarmClockTest {
  public static void main(String[] args) {
    AlarmClock aClock = new AlarmClock();
    aClock.snooze();        // This is still fine.
    // The next line of code is an ERROR.
    // You can't access snoozeInterval outside the snooze method.
    snoozeInterval = 10000;
  }
}

Method parameters

A method parameter, which has a scope similar to a local variable, is another type of variable. Method parameters pass arguments into methods. When you declare the method, you specify its arguments in a parameter list. You pass the arguments when you call the method. Method parameters function similarly to local variables in that they lie within the scope of the method to which they are linked, and can be used throughout the method. However, unlike local variables, method parameters obtain a value from the caller when it calls a method. Here's a modification of the alarm clock that allows you to pass in the snoozeInterval.

public class AlarmClock {
  public void snooze(long snoozeInterval) {
    System.out.println("ZZZZZ for: " + snoozeInterval);
  }
}
public class AlarmClockTest {
  public static void main(String[] args) {
    AlarmClock aClock = new AlarmClock();
    // Pass in the snooze interval when you call the method.
    aClock.snooze(10000);  // Snooze for 10000 msecs.
  }
}

Member variables -- how objects store data

Local variables are useful, but because they provide only temporary storage, their value is limited. Since their lifetimes span the length of the method in which they are declared, local variables compare to a notepad that appears every time you receive a telephone call, but disappears once you hang up the phone. That setup can be useful for jotting down notes, but you often want something a little more permanent. What's a programmer to do? Enter member variables.

Member variables -- of which there are two, instance and static -- make up part of a class. I will consider instance variables now, and return to static variables in a later article.

Developers implement instance variables to contain data useful to a class. An instance variable differs from a local variable in the nature of its scope and its lifetime. The entire class makes up the scope of an instance variable, not the method in which it was declared. In other words, developers can access instance variables anywhere in the class. In addition, the lifetime of an instance variable does not depend on any particular method of the class; that is, its lifetime is the lifetime of the instance that contains it.

Remember instances from the previous article? Instances are the actual objects that you create from the blueprint you design in the class definition. You declare instance variables in the class definition, affecting each instance you create from the blueprint. Each instance contains those instance variables, and data held within the variables can vary from instance to instance.

Consider the AlarmClock class. Passing the snoozeInterval into the snooze() method isn't a great design. Imagine having to type in a snooze interval on your alarm clock each time you fumbled for the snooze button. Instead, just give the whole alarm clock a snoozeInterval. You complete this with an instance variable in the AlarmClock class, as shown below:

public class AlarmClock {
  // You declare snoozeInterval here. This makes it an instance variable.
  // You also initialize it here.
  long m_snoozeInterval = 5000;   // Snooze time in millisecond = 5 secs.
  public void snooze() {
    // You can still get to m_snoozeInterval in an AlarmClock method
    // because you are within the scope of the class.
    System.out.println("ZZZZZ for: " + m_snoozeInterval);
   }
}

You can access instance variables almost anywhere within the class that declares them. To be technical about it, you declare the instance variable within the class scope, and you can retrieve it from almost anywhere within that scope. Practically speaking, you can access the variable anywhere between the first curly bracket that starts the class and the closing bracket. Since you also declare methods within the class scope, they too can access the instance variables.

You can also access instance variables from outside the class, as long as an instance exists, and you have a variable that references the instance. To retrieve an instance variable through an instance, you use the dot operator together with the instance. That may not be the ideal way to access the variable, but for now, complete it this way for illustrative purposes:

public class AlarmClockTest {
  public static void main(String[] args) {
    // Create two clocks. Each has its own m_snoozeInterval
    AlarmClock aClock1 = new AlarmClock();
    AlarmClock aClock2 = new AlarmClock();
    // Change aClock2
    // You'll soon see that there are much better ways to do this.
    aClock2.m_snoozeInterval = 10000;
    aClock1.snooze();    // Snooze with aClock1's interval
    aClock2.snooze();    // Snooze with aClock2's interval
  }
}

Try this program out, and you'll see that aClock1 still has its interval of 5,000 while aClock2 has an interval of 10,000. Again, each instance has its own instance data.

Don't forget, the class definition is only a blueprint, so the instance variables don't actually exist until you create instances from the blueprint. Each instance of a class has its own copy of the instance variables, and the blueprint defines what those instance variables will be.

Two instances of AlarmClock with their own instance variables

Encapsulation

Encapsulation remains as one of the foundations of object-oriented programming. When using encapsulation, the user interacts with the type through the exposed behavior, not directly with the internal implementation. Through encapsulation, you hide the details of a type's implementation. In Java, encapsulation basically translates to this simple guideline: "Don't access your object's data directly; use its methods."

That is an elementary idea, but it eases our lives as programmers. Imagine, for example, that you wanted to instruct a "person" object to stand up. Without encapsulation, your commands could go something like this: "Well, I guess you'd need to tighten this muscle here at the front of the leg, loosen this muscle here at the back of the leg. Hmmm -- need to bend at the waist too. Which muscles spark that movement? Need to tighten these, loosen those. Whoops! Forgot the other leg. Darn. Watch it -- don't tip over ..." You get the idea. With encapsulation, you would just need to invoke the standUp() method. Pretty easy, yes?

Some advantages to encapsulation:

  • Abstraction of detail: The user interacts with a type at a higher level. If you use the standUp() method, you no longer need to know all the muscles required to initiate that motion.

Isolation from changes

: Changes in internal implementation don't affect the users. If a person sprains an ankle, and depends on a cane for a while, the users still invoke only the

standUp()

method.

Correctness

: Users can't arbitrarily change the insides of an object. They can only complete what you allow them to do in the methods you write.

Here is a short example in which encapsulation clearly helps in a program's accuracy:

Related:
1 2 Page 1
Page 1 of 2