Until now, every type that we have used has been a pre-defined Java type. Now we are going to discuss how programmers can define their own types and then use them. For example, suppose that a group of programmers at a software firm are working on a project that involves a great deal of date processing. Rather than using three variables to represent a single date (a month, a day, and a year), it would be convenient to have a data type called Date which would, with a single variable, represent a date. Once we have this new data type defined, we could declare variables of type Date like this:
Date date1; Date date2;
Each date variable would store all of the information necessary to represent a single date. The group leader might assign one of the group members the task of defining the new type. This new Date type would also need some methods so that the programmers using the class would be able to perform operations on Dates. The group leader might specify that Date variables need methods such as print() to output a Date on the screen, set() to give a Date a value, comesBefore() to determine which comes first between two Dates, increment() to advance a Date to the next day, increasedBy() to advance a Date a certain number of days, and so on.
The way that new types like this are created by programmers in Java is through the use of classes. We have been defining a class all along, of course, but this has been a special sort of class called a main class because it contains a method named main(). In order to define new types we are going to define additional classes that are completely separate from our main class. We will define them in the same .java file, below the main class. Sometimes we will refer to a program that uses our newly defined class as the client program.
When we declare a variable of type Date (or any other class), the variable is called an object. Don't let the term "object" confuse or intimidate you. An object is simply a special kind of variable.
Unlike declarations for primitive type variables (like int and double and char), the declaration of an object does not allocate a storage location for storing the object. It allocates only a storage location for storing a reference to an object.
Does that sound mysterious? Let me try to make it a little more concrete. A reference is a memory address. So when we say that we are storing a reference to an object, what we mean is that we are storing the memory address of the object. This is called "indirection" and it's extremely common in programming. It just means that instead of storing an object directly, we store the memory address of the object. It's probably not clear at this point why we would want to do things this way, but you'll just have to trust us on it for now :)
From now on I'll never use the term "memory address". I'll just use the term "reference".
So, when we say
Date date1;
we don't yet have an object. We just have an uninitialized variable waiting to store a reference to an object. To create an actual object, we use the "new" operator:
date1 = new Date();
Better yet, we can combine the declaration of the reference and the creation of the object into one statement:
Date date1 = new Date();
Now we have two variables. The first variable is a reference variable named date1. We don't use that variable for anything except as the way to access the second variable, which is a Date object. This Date object has no name, so the only way we access it is through the reference variable date1.
When we declare a class, we start by listing all of the components that objects of that class will have. Consider the date example again. In order to store a date we must store three things: an integer for the month, an integer for the day, and an integer for the year. (This is the most intuitive way to store a date. Actually we could store dates using a number of different methods. For example, we could store the month in a string, or we could store the entire date in a single string, or we could represent the date as the number of days that have passed since January 1, 1600. We'll stick to the more intuitive solution of storing three integers.)
As mentioned previously, a Date object should also have methods that it can call. to start with we will just define two methods:
A print() method to print out the date on the screen. This is necessary because we won't be able to use println() to print dates. Once we have this method defined, we would then print out a date by saying
date1.print();
A set() method that will be used to set the date. This would take the place of using the assignment statement to set a date. So if we want to set date1 to April 17, 1947, we would say
date1.set(4, 17, 1947);
We will proceed with just these two, and then add more functionality to our Date objects as we go.
Before we start defining our class, we should illustrate what a client program that uses the class might look like. We don't need a full-blown program here, just a simple program that demonstrates the use of each of the member methods. Here is an example program for the Date class, along with the output that it should produce:
public class Example {
public static void main(String[] args) {
Date date1 = new Date();
date1.set(4, 27, 1947);
System.out.print("Date1 should be 4/27/1947: ");
date1.print();
System.out.println();
}
}
date1 should be 4/27/1947: 4/27/1947
Here is the syntax for declaring a class. To start with, a class declaration has the following form:
class Date {
<list the components that each
object of this class will contain>
}
Here is the class in its entirety, along with the program that will be using the class:
public class Example {
public static void main(String[] args) {
Date date1 = new Date();
date1.set(4, 27, 1947);
System.out.print("Date1 should be 4/27/1947: ");
date1.print();
System.out.println();
}
}
class Date {
private int month;
private int day;
private int year;
public void set(int inMonth, int inDay, int inYear) {
day = inDay;
month = inMonth;
year = inYear;
}
public void print() {
System.out.print(month + "/" + day + "/" + year);
}
}
I have several observations to make about this class.
Observation #1) Notice that the print() method has no parameters. This should cause some doubt in your mind. If there are no parameters to the print() method, what is printed? The answer is that it is the calling object that is being printed. Let me explain. Inside a method you have access to the data members of the calling object, even though they are not explicitly stated as parameters. Remember that when we call print() in our client program, it will look like this:
date1.print();
The objective of this method call is to print out date1. So inside our print() method we will have statements that print out the month, day, and year data members of the calling object (date1, in this example).
Observation #2) Notice the use of public and private in the class. Some of the members we have listed for our Dateclass are intended to be available to the client programmer. Some of the members we have listed are intended to be available only to the methods of the class. This is an extremely important concept to understand if you are to use classes effectively. The most important thing to understand about classes is that not only are we defining a new type, we are making that new type completely independent and self-contained, so that the class will be easily reusable; in fact, we are going to maintain a much higher degree of independence and self-containedness than we were able to with methods. In order to maintain this high degree of independence and thus reusability, we are going to deny the client program access to any of the data members of a class. The result of this is that we could completely change the way that dates are stored, and completely rewrite each of our member methods to reflect this change, but any client programs that use our class will still work.
On the other hand, we do want the client program to be able to call the member methods of our class. We make this distinction by making the methods in the class public and the data fields of the class private.
Observation #3) When I say "month" inside my print() method, it means "the month data member of the calling object". So if the client program included the statement
date1.print();
using month inside the print() member method would mean the month data member of date1. To state this rule more generally, when a data member of the class is used inside the definition of a member method, it refers to that data member of the calling object.
Observation #4) Difference between static and instance variables and methods. You may have noticed that when we learned about defining our own methods, we always used the keyword "static". In the example above, the methods inside the Date class do not have the "static" keyword. By default, if we don't use the "static" modifier, methods are assumed to be "instance" methods. The difference between "static" and "instance" members will be covered in detail in lesson 9. For now, just omit the word "static" when you are defining methods that are members of a class other than the main class.
Let me review a few things about the set() method to make sure you've got it.
The set() method has three parameters. Why did I name these inMonth, inDay, and inYear instead of just month, day, and year? Primarily because we need to distinguish between the parameters and the data members. The data members are already named month, day, and year, so we have to think of different names for the parameters.
The first assignment statement inside the set() method says to set the month data member of the calling object equal to the value of the first parameter, inMonth. The second assignment statement says to set the day data member of the calling object equal to the value of the second parameter, inDay. And similarly for the year data member. So when the client program has a statement date1.set(4, 27, 1947); (as our client program from earlier in this lesson does), what happens is the month data member of date1 gets set to 4, the day data member of date1 gets set to 27, and the year data member of date1 gets set to 1947.
Let's move on now to extend the functionality of our class. We would like to be able to do more than just print dates and set dates. For example, we would like to be able to compare two dates and see if one comes before another, so we will define a method named comesBefore() which takes one parameter and compares the calling object with the parameter. If the calling object comes before the parameter, comesBefore() will return true, otherwise comesBefore() will return false. A code segment in a client program that uses this method might look like this:
Date date2 = new Date();
date2.set(2, 28, 1965);
if (date1.comesBefore(date2)){
System.out.println("date1 comes before date2");
} else {
System.out.println("date1 does not come before date2");
}
Notice that the argument here is a Date object. Now that we have defined this new type named "Date", we can use Date's in all of the ways that we used other types like int and double, including having a Date object as a parameter. When we send a Date object as an argument, we are sending an object that contains three parts: the month, the day, and the year.
To implement this method, we will first compare years. We return true if the year data member of the calling object is less than the year data member of the parameter, and false if the year data member of the calling object is greater than the year data member of the parameter. If neither of these first two conditions is true, it means that the years are equal, and we must go on to compare months. We compare the months in the same manner in which we compared the years, and then go on to compare days if the months are equal. Here is the method definition for comesBefore. It is possible to write this method much more concisely, but I think that this solution is easier to understand than the more concise solution.
public boolean comesBefore(Date otherDate) {
if (year < otherDate.year){
return true;
}
if (year > otherDate.year){
return false;
}
if (month < otherDate.month){
return true;
}
if (month > otherDate.month){
return false;
}
return day < otherDate.day;
}
Let's now add a member method to theDate class that will allow us to increment a date. By "increment" we mean change the value of the Dateobject so that it represents the date that is one day later than the date currently represented. A code segment in a client program that uses this method might look like this:
date2.increment();
System.out.print("one day later, date2 is: ");
date2.print();
System.out.println();
Notice that the increment() method is a void method (you can tell because it is called like a statement, not like an expression) that actually modifies the value of the calling object (date2, in this case). In a moment we will see a similar example where the method returns the resulting value but does not modify the calling object itself.
The implementation of the increment() method is not as straightforward as it may at first seem. We begin by simply adding one to the day data member. The problem is, what if day was originally the last day of the month? In this case, we would have to set day back to 1, and add one to the month data member. But what if the month was originally 12? We don't want the month to be 13! So in this case we would have to set the month back to 1 and add one to the year data member. To further complicate matters, it is not easy to tell whether the day is the last day of the month, because each month has a different number of days. So we'll write an auxiliary method named numDaysInMonth that returns the number of days in the month of its calling object. In order to write this method we will need yet another auxiliary method to determine whether the year of the calling object is a leap year. Whew! Here is the code for the increment() method and its two auxiliary methods:
public void increment() {
day++;
if (day > daysInMonth()) {
day = 1;
month++;
}
if (month > 12) {
month = 1;
year++;
}
}
private int daysInMonth() {
switch (month){
case 2: if (isLeapYear()){
return 29;
} else {
return 28;
}
case 4:
case 6:
case 9:
case 11: return 30;
default: return 31;
}
}
private boolean isLeapYear() {
if (year % 400 == 0) {
return true;
}
if (year % 100 == 0) {
return false;
}
return year % 4 == 100;
}
Consider the call to the numDaysInMonth() method for a moment. This method will be a member method of the Dateclass. However, it is called without an object in front. So far, every time a member method has been called it has been called using the syntax
dateObject.dateMemberMethod();
The reason that we don't use this syntax when calling numDaysInMonth() is this: we want to call numDaysInMonth() using the object that called increment() as the calling object. For example, if date1 in the client program was the calling object that called increment(), we want that same calling object to call the numDaysInMonth() method. This way when we say month in the numDaysInMonth() method, we are still referring to the same calling object that we had in the increment() method. The rule for using member methods is the same as the rule for using data members. Recall that when we want to refer to the month data member of the calling object, we just use the word month by itself. In the same way, when we want to refer to the numDaysInMonth() member method of the calling object, we just use the method call by itself, without putting an object in front. This same thing occurs again when the numDaysInMonth() method calls the isLeapYear() method.
Let's now add a member method called increasedBy() to the Dateclass that will add a certain number of days to a date. We will specify this method somewhat differently than we specified the increment() method. The increment() method was designed as a void method that did not return a value but did modify the calling object. Let's design our increasedBy() method so that it does not modify the calling object, but instead returns a date that is equal to the calling object increased by the number of days indicated in the parameter. A code segment in a client program that uses this method might look like this:
date1 = date2.increaseBy(12);
System.out.print("After setting date1 to equal date2 + 12,"
+ "date 2 is still: ");
date2.print();
System.out.println();
System.out.print("but date1 is now: ");
date1.print();
System.out.println();
The statement in which the call to increasedBy() appears should NOT modify the value of date2. Rather it should modify date1 so that it represents the date that comes 12 days after the date represented by date2. The output of this code segment should be:
After setting date1 to date2 + 12, date2 is still 7/24/1947
but date1 is now 3/13/1947
Compared to the increment() method, increasedBy() is fairly straightforward. We simply call increment() the appropriate number of times. For example, if the parameter is 7, we would call increment() 7 times.
One interesting thing about the increasedBy() method is that its return type is Date. Although we have not yet seen a method that has a class as the return type, there should be no trouble seeing how this works. It simply means that the value that is returned by this method with the return statement must be a Date. In addition, when this method is called in the client program, it must be used in the place where you would normally expect to see a Datevalue. In the code segment example above, for example, it is used on the right side of an assignment statement where the left side of the assignment statement is a Dateobject and so the compiler is expecting a Dateobject on the right side as well.Another interesting thing about the increasedBy() method is that we have to have a temporary Date object. We set the temporary Date object to the value of the calling object by using the set() method, then we increment the temporary Date the appropriate number of times using the increment() method, and finally we return the temporary Date. We need to have a temporary Date to avoid modifying the value of the calling object. Let me explain that statement. Some students might initially try to solve this problem by simply incrementing the calling object the appropriate number of times and then returning the calling object. The problem with this approach is that the calling object gets modified, and according to the specification I gave, as well as the code segment example above, we don't want the calling object to be modified by this method.
What follows is the complete Date class as it now stands. Notice that the methods numDaysInMonth() and isLeapYear() are private. This is because these methods were not part of the specification and are not intended to be called from the client program. They are helper functions intended to be called only from other member methods. So we make them private. All of your data members should always be private. The rule for member methods, however, is not so simple. Most member methods must be public so that they can be called by the client. Member methods that are not intended to be called by the client, however, should be private.
public class Example {
public static void main(String[] args) {
Date date1 = new Date();
date1.set(7, 24, 1947);
System.out.print("After being set to 7/24/1947, date1 is: ");
date1.print();
System.out.println();
Date date2 = new Date();
date2.set(2, 28, 1965);
System.out.print("After being set to 2/28/1965, date2 is: ");
date2.print();
System.out.println();
if (date1.comesBefore(date2)){
System.out.println("date1 comes before date2");
} else {
System.out.println("date1 does not come before date2");
}
date2.increment();
System.out.print("one day later, date2 is: ");
date2.print();
System.out.println();
date1 = date2.increaseBy(12);
System.out.print("After setting date1 to equal date2 + 12,"
+ "date 2 is still: ");
date2.print();
System.out.println();
System.out.print("but date1 is now: ");
date1.print();
System.out.println();
}
}
class Date {
private int month;
private int day;
private int year;
public void print() {
System.out.print(month + "/" + day + "/" + year);
}
public void set(int inMonth, int inDay, int inYear) {
day = inDay;
month = inMonth;
year = inYear;
}
public boolean comesBefore(Date otherDate) {
if (year < otherDate.year){
return true;
}
if (year > otherDate.year){
return false;
}
if (month < otherDate.month){
return true;
}
if (month > otherDate.month){
return false;
}
return day < otherDate.day;
}
public void increment() {
day++;
if (day > daysInMonth()) {
day = 1;
month++;
}
if (month > 12) {
month = 1;
year++;
}
}
private int daysInMonth() {
switch (month){
case 2: if (isLeapYear()){
return 29;
} else {
return 28;
}
case 4:
case 6:
case 9:
case 11: return 30;
default: return 31;
}
}
private boolean isLeapYear() {
if (year % 400 == 0) {
return true;
}
if (year % 100 == 0) {
return false;
}
return year % 4 == 100;
}
public Date increaseBy(int numDays) {
Date tempDate = new Date();
tempDate.set(month, day, year);
for (int count = 0; count < numDays; count++){
tempDate.increment();
}
return tempDate;
}
}
/* SAMPLE RUN:
After being set to 7/24/1947, date1 is: 7/24/1947
After being set to 2/28/1965, date2 is: 2/28/1965
date1 comes before date2
one day later, date2 is: 3/1/1965
After setting date1 to equal date2 + 12,date 2 is still: 3/1/1965
but date1 is now: 3/13/1965
*/
A constructor is a method that is automatically called when the client program declares an object. In the future we will discuss some very important uses of constructors; however, for now, the only use that constructors have is initializing objects. Let's write a constructor that sets Date objects equal to January 1, 1600. Once we do, then the code:
Date date1 = new Date(); date1.print();
will cause the date 1/1/1600 to get printed to the screen.
In order to actually add a constructor to our Date class, you'll need to know two things about the syntax of constructors. First, a constructor must have the same name as the class to which it belongs. Second, a constructor has no return type, not even void. Where you might expect to see the word void or int, there is no word at all. The return type is missing.
Here's the definition we will use for the constructor in our Date class:
Date() {
month = 1;
day = 1;
year = 1600;
}
To add even more flexibility to our class, we would like to also include a constructor that allows the client program to initialize the a Dateobject at the same time that it is being declared. For example, we'd like to be able to declare a Date object named date2 and initialize it to February 28, 1965 using this syntax:
Date date2 = new Date(2,28,1965);
This syntax may seem a bit strange at first. It looks like a cross between a variable declaration and a method call, except the name of the method being called is not date2, but rather Date! As a matter of fact, this is exactly what is happening. In addition to declaring a new object, we are calling a constructor to initialize the new object. Here's what the new constructor will look like.
Date(int inMonth, int inDay, int inYear) {
month = inMonth;
day = inDay;
year = inYear;
}
The body of this constructor looks a lot like the body of the set() method, and rightly so, since the tasks are similar. The only difference is that the set() method is dealing with a Date object that has already been declared elsewhere in the client program, while the constructor defined here is dealing with a brand new Date object.
You might be wondering if this is possible, since we now have two methods both named Date! As it turns out, and despite the fact that we have neglected to tell you this before, it is perfectly acceptable to have two methods in Java with the same name. The compiler identifies which one to execute based on the parameter list, formally called the signature of the method. This process (defining a method with the same name as one that already exists, but giving it a different parameter list) is called method overloading.
Here is the complete listing of the Date class, along with a client program to test the class, and a sample run of the program.
public class Example {
public static void main(String[] args) {
Date date1 = new Date();
System.out.print("When first declared, date1 is: ");
date1.print();
System.out.println();
date1.set(7, 24, 1947);
System.out.print("After being set to 7/24/1947, date1 is: ");
date1.print();
System.out.println();
Date date2 = new Date(2, 28, 1965);
System.out.print("When first declared and initialized to "
+ "2/28/1965, date2 is: ");
date2.print();
System.out.println();
if (date1.comesBefore(date2)){
System.out.println("date1 comes before date2");
} else {
System.out.println("date1 does not come before date2");
}
date2.increment();
System.out.print("one day later, date2 is: ");
date2.print();
System.out.println();
date1 = date2.increaseBy(12);
System.out.print("After setting date1 to equal date2 + 12,"
+ "date 2 is still: ");
date2.print();
System.out.println();
System.out.print("but date1 is now: ");
date1.print();
System.out.println();
}
}
class Date {
private int month;
private int day;
private int year;
Date() {
month = 1;
day = 1;
year = 1600;
}
Date(int inMonth, int inDay, int inYear) {
month = inMonth;
day = inDay;
year = inYear;
}
public void print() {
System.out.print(month + "/" + day + "/" + year);
}
public void set(int inMonth, int inDay, int inYear) {
day = inDay;
month = inMonth;
year = inYear;
}
public boolean comesBefore(Date otherDate) {
if (year < otherDate.year){
return true;
}
if (year > otherDate.year){
return false;
}
if (month < otherDate.month){
return true;
}
if (month > otherDate.month){
return false;
}
return day < otherDate.day;
}
public void increment() {
day++;
if (day > daysInMonth()) {
day = 1;
month++;
}
if (month > 12) {
month = 1;
year++;
}
}
private int daysInMonth() {
switch (month){
case 2: if (isLeapYear()){
return 29;
} else {
return 28;
}
case 4:
case 6:
case 9:
case 11: return 30;
default: return 31;
}
}
private boolean isLeapYear() {
if (year % 400 == 0) {
return true;
}
if (year % 100 == 0) {
return false;
}
return year % 4 == 100;
}
public Date increaseBy(int numDays) {
Date tempDate = new Date();
tempDate.set(month, day, year);
for (int count = 0; count < numDays; count++){
tempDate.increment();
}
return tempDate;
}
}
/* sample run:
When first declared, date1 is: 1/1/1600
After being set to 7/24/1947, date1 is: 7/24/1947
When first declared and initialized to 2/28/1965, date2 is: 2/28/1965
date1 comes before date2
one day later, date2 is: 3/1/1965
After setting date1 to equal date2 + 12,date 2 is still: 3/1/1965
but date1 is now: 3/13/1965
*/