Java Classes Wrapping it up

From Juneday education
Jump to: navigation, search

Description

In this chapter, we'll create a class from scratch as an exercise, and we'll write a small program to test our class to see that it behaves as expected.

The example we'll use is to create a class for representing a point in time, Time. Note that there are excellent classes for representing temporal stuff in Java's new time API (see our page on that: Java:API:Time), so please accept that the class we're writing here is plainly for exercise on the concept of writing a class from scratch!

Meta information about this chapter

Expand using link to the right to see the full content.

Purpose

The purpose of this exercise, which we usually use as a voluntary exercise, is to have a slightly larger task at this stage in the course material. The students have just finished the chapters on Classes and are now ready to write a full class from scratch. The class is called Time and represents a 24 hour time (00:00:00 through 23:59:59).

This way, students can check if they have understood all the parts of a class, and how to put it all together.

Another purpose is to explain information hiding and encapsulation. This is done by forcing the student to use a different representation of the time internally than what the student would expect. Rather than keeping three variables seconds, minutes and hours, we force the student to store the time in one single variable with the total amount of seconds. This shows that their is no one-to-one mapping between accessor methods and instance variables. We only need to store the total amount of seconds. The minutes, for instance, can be calculated in the accessor method minutes().

Goals

The goal is to give the students a voluntary "check-your-progress" kind of task, which includes all chapters up to this point in the course material.

Instructions to the teacher

Common problems related to this chapter

The class contains some uses of Exceptions, which is something the students haven't learned yet at this point in the course materials. We think it is manageable anyway, since we both provide instructions and examples, as well as a suggested solution source code bundle.

If questions arise about the exception parts, tell the student that this is something they will learn later on, but is good to have been exposed to already. The keen student may be referred to the chapters on exceptions to read ahead, and the normal student can be referred to the source code - or get help from the supervisors or teacher. Make it clear that exceptions are not the topic at hand, and that they should focus on the whole of the class, rather than on details.

Another thing we predict will be a problem for some students, is running the tests, which contain assertions. It is common to forget to run the tests with the -ea flag, which would make the JVM ignore the assertions.

Lastly, we predict that some students will struggle to understand the remainder operator (e.g. how to go from a large number of seconds to the number of hours, minutes and seconds). Refer the student to the video lecture, which explains how this operator works.

For those who don't like or understand the remainder operator, we've included some object oriented versions that don't use remainder at the end of this chapter.

Requirements for the Time class

  • The class should represent a time with hours, minutes and seconds
  • It should be possible to create a new Time from three int values representing hour, minute and second
  • The class should have an instance method toString() which returns a String representing the Time
    • The String should have the format HH:mm:ss - for instance 23:49:59 (24 hour time)
  • The class should have an instance method tick() which increments the Time with one second
    • That method is here, of course, mostly for you to practice how to increment the Time correctly with wrapping around values:
    • 59 seconds + 1 increments the minute by one and sets the second to 0, 59 minutes plus one minute increments the hour with one and sets the minute to 0, 23 hours plus one wraps around to 00 (midnight)
  • The class should have an instance method for setting the Time to a completely new Time (accepting three ints as arguments for hour, minute and second)
  • The class should have accessor methods returning the int values for:
    • hour
    • minute
    • second
  • You should use, internally, one single int holding the seconds representing the time
    • a number between 0 (00:00:00) and 86399 (23:59:59) as it happens
    • That is the number of hours times 60 times 60 plus the number of minutes times 60 plus the number of seconds

Object oriented aspects on the class

This class will serve as an example of the thing called encapsulation - an object has both state and behavior. It also shows the concept of information hiding in that it has a private internal representation of the time and opens up abstractions for manipulating and accessing the values of the time represented. How we store the actual data for the Time the object represents is of no concern of the client code using the Time. Any code using our Time class will have to do with the public interface to the object of type "reference to Time". The public interface is just a fancy word for the public constructor and the public methods.

Again, this class is meant as an exercise for you, and not necessarily as an example of a well-designed typical Java class - we have chosen it because we think it contains interesting elements of writing classes (private variable and public methods etc).

Getting started

From the requirements list above we have enough information to get started on our class.

Class declaration

The name of the class is Time so you know how to get started declaring the class!

Private instance variable

Declare one single private int as an instance variable and name it seconds. This variable will contain the state of the object created from this class. When you think about it, you only need one single variable in order to represent a 24 hour time - the parts representing hour, minute and second can be calculated from the total amount of seconds in the the time (knowing that one hour contains 60 minutes and one minute contains 60 seconds.

Of course, you could have opted for having three int variables holding the value for hour, minute and second respectively, but then we would miss an opportunity to reflect on the fact that how we store a time can be very different from how we interact with and think about a time.

Constructor

Write a public constructor accepting three int parameters hour, minute and second. In the constructor, simply pass on the values to a call to the method setTime(int, int, int) which we'll write next.

Method for setting the time

We decided to let our Time class be mutable - which means that a "live" object can be changed to represent a new state, using methods. In this case, we have one public void method called setTime(int hour, int minute, int second) and (soon) one method called tick(). Remember from the chapter about java.lang.String being immutable, that it is possible to make a class immutable so that the value (state) of objects of the class can't be changed, only new objects of the class can be created. In this example, we opted for a mutable class (for no particular reason). In fact, we (the authors) are fans of immutable classes, but that's not the topic we want to practice here.

So, write a public void method called setTime(int hour, int minute, int second). The purpose of this method is to let client code (code using objects of our class) change what time an existing object represents.

The logic of this method should be two-fold. First we check that the values passed in makes sense. We don't want to accept e.g. 25 as the value for hour, 63 as the value for minute or second. Then we calculate the value for our seconds variable and store that state. The things we need to check is thus:

  • 0 <= hour <= 23
  • 0 <= minute <= 59
  • 0 <= second <= 59

We propose that you write a private helper method private void checkTime(int hour, int minute, int second) which does this check, and call it the first thing you do in the setTime() method. Moving the check to its own method (a helper-method if you like), keeps the code for setTime() smaller and cleaner.

So how do we check time? We'll learn about exceptions in a later chapter but we'll use it here anyway. Don't worry, we'll explain what to do and how it works! The idea of the checkTime() method, is to check that all parameters have valid values. If they do, we'll do nothing at all (all is fine!). If they don't, then we'll crash the program (rather than letting client code set an invalid time which doesn't make sense). This strategy puts the responsibility to provide correct values for hour, minute and second on the client code programmer (the caller of the constructor or setTime() method). If they provide incorrect (or meaningless) values for a time, it is a programmer error and we'd rather crash than live with that. We don't want it to be possible to represent a time which doesn't exist or make sense.

How do you crash the program then? We'll actually, saying that we "crash the program" was a little harsh. What we'll do instead, is to "throw an Exception". This will effectively crash the program unless the calling code makes an effort to detect that such an exception was thrown. Chances are, though, that the caller won't make such efforts, and the program will crash. The idea is, that if someone calls the constructor or setTime() method with invalid values, this is a bug which should be corrected and we think it is better to crash the program than to silently accept that this bug created an invalid time. Bugs should be fixed!

So, make an IF-statement the first thing you do in the checkTime() method, and check that the values are correct. See above for the requirements of hour, minute and second.

If you find that one of the values isn't correct (you'll need to use && between the predicates (tests) of the values if you test for validity, otherwise use || between the tests if you check for invalidity), then do this:

      throw new IllegalArgumentException
        (new StringBuilder("Bad time: ")
         .append(hour)
         .append(":")
         .append(minute)
         .append(":")
         .append(second)
         .toString());

Otherwise (all values were correct), do nothing (it is a void method, so you don't have to return anything).

Pseudo code:

if (hour is not ok || minute is not ok || second is not ok ) {
  throw exception
}

Now, back to the setTime(int hour, int minute, int second) method. After the call to checkTime(), calculate the total number of seconds and store that in your private instance variable seconds. How many seconds is there in a time like ours, then? It should be 3600 per hour plus 60 per minutes plus the seconds "at the end", no?

This idea will also be used in the next method, the public instance method tick()

Make it tick

The public void tick() method should increment the seconds variable with one. But it should never surpass the max time (according to our randomly chosen strategy). Why? It would be possible to calculate a time from a huge number of seconds too, but keeping the seconds value within some bounds makes sure that the int value never overflows (exceeds the max value possible to store in a 32 bit signed Java int - in which case the value would "wrap around and become negative).

Let's decide that the seconds value never can be greater than (or equal to) 86400. How can we do that? We could use an IF-statement and reset the seconds to 0 when it reaches 86400. Or we could use the remainder operator %. In short, the remainder operator lets us confine a value to set of values, using integer division and calculating the "remainder" of such a division.

A short example:

Let's take the whole number 7 and divide it with 6 using integer division. That's effectively asking, how many times can we take 6 from 7? The answer is one. But if we do so, what's left? Also one.

What about 7 divided by 5? How many times can we take 5 from 7? One. What's left? 2.

In Java, we can calculate the remainder of 7 divided by 6 like this: int remainder = 7 % 6; // 1 and the remainder of 7 divided by 5 as int remainder = 7 % 5; // 2. The only possible outcomes of something % 5 are the numbers between 0 and 4.

What would happen if we took our seconds value, and used remainder to keep it in check, using e.g. 86400 as the divisor? What is for instance 86400 % 86400? It's 0. What is 86401 % 86400? 1. What is 86402 % 86400? 2. Etc.

Note that this is not a course on basic maths, and this is certainly not the only way to assure that the value of seconds never exceeds 86399. We just felt like introducing the % operator, since we are going to use it again, soon. We could of course have had settled with an IF-statement, checking that if seconds is 86400, then we'd set it to 0 instead.

The accessor methods

Some of the final pieces of our class are the methods for querying a Time object for its parts; the seconds(), minutes() and hours instance methods. They should all be public methods with the return type int.

Now comes the question, if we have saved the time as the total amount of seconds from 00:00:00, how do we split that up into hours, minutes and seconds? Let's start with the seconds part.

Let's think about this for a while. If we start with something simple, like 00:00:45, we have 45 seconds in total. We can use our friend % for this. How many times can we take 60 from 45? 0. What do we have left? 45 seconds. Sounds promising?

What about something slightly more complicated. If we have the time 00:01:10, how many seconds do we have? 70. How many times can we take 60 from 70? 1. But what do we have left? 10. Sounds promising? What about 00:02:10? 130 seconds. How many times can we take 60 from 130? 2. What do we have left? 10. Looks promising?

So let's decide that we are convinced and that taking the total number of seconds % 60, in order to get the number of seconds from our time. This makes the public int seconds() method fairly straightforward.

What about the minutes part? If we have 00:02:10 again, we have 130 seconds (as per above). How do we know how many whole minutes exist in 130 seconds? We use integer division (is our first attempt). What do we get if we divide 130 with 60 using integer division? It's the same as answering "how many times can we take 60 from 130?". The answer is 2! But what if we have a greater number of seconds, like 3730 (one hour, 2 minutes and 10 seconds - 01:02:10)? Then our strategy doesn't work any more, since 3730 / 60 is 62 in integer division. We want to limit ourselves to minutes between 0 and 59. Sounds familiar?

What if we then take 3730 / 60 % 60? Aha! Then we get 2 as the answer. First we see how many chunks of 60 seconds we have, which was 62. OK, so how many times can we take 60 from that? 1 time. What do we have left? 2. So the correct answer should be to first divide by 60, then use remainder of 60 to get the actual number of minutes. That's what you can use for the minutes() method.

OK, then. What about hours? Yeah, what about them? They tend to pass very quickly when you are programming. But what about going from our total amount of seconds to the number of hours?

Since we know that the largest number of total seconds we store is 86399 (see above if you don't remember why), we can actually settle with integer division here. In order to go from 86399 to the number of hours, we need to see how many times we can take one hour's worth of seconds from our total amount of seconds. How many seconds are there in one hour? 60 * 60, right? So we can actually divide our total amount of seconds with 60 and then with 60 again, using integer division. That would work for our hours() method. We don't need to calculate the remainder, since the largest number we store in our seconds variable with the total seconds is 86399. Incidentally, 86399/3600 is 23 which, incidentally, is the largest number of hours we can store in a 24 hour Time object.

So this would work fine.

Converting a Time object to a String

The last part of our Time class is the method for getting a String representation of a Time object. We stick to the convention of writing a public String toString() method for this.

We want a nicely formatted string on the format HH:mm:ss and pad with zeroes. So that 1:0:10 is formatted 01:00:10.

For this, we'll make use of java.lang.String's static format() method. Formatting text is done with format strings, and the format string for a two-digit number padded with zeros is %02d. So the whole format pattern for our time will be %02d:%02d:%02d. The format method takes a format string and a number of arguments with the values to put in the format patterns. As values we use calls to our hours(), minutes() and seconds() methods.

The expression to return from the toString() method, thus becomes:

String.format("%02d:%02d:%02d",
              hours(),    // first  %02d
              minutes(),  // second %02d
              seconds()); // third  %02d

The colons in the format string will remain unchanged, as we want.

Next, we'll look at how to test our class using something called assertions (not officially part of the Programming with Java book, but nice to have seen).

Testing the class

OK, so now we have written a class from scratch (using our instructions, but anyway). So how do we test the class, to see if it works the way we expect it to work?

We have tried to reason about the formulas used to calculate the value to be stored and what the value represents in terms of hours, minutes and seconds. That's of course one way to test that a program (or as in this case, a class) is correct. You reason about it.

But we can also write simple tests to see if the expected behavior is met when we use it. Of course, one needs to be very careful when writing tests, so that one is sure to test the right things. If the test is broken (faulty or buggy), then we haven't tested anything. And also, it's good to think about the fact that you cannot test everything and prove that a program is correct simply by testing it. You can discover bugs when testing but you can never prove the absence of bugs by simply testing the program.

So when we design tests, we should spend some time thinking about what cases to test, since we cannot test every thing and every situation. So one approach could be to include fringe cases (edge cases) in your tests, or things that for some reason seems likely to fail.

When testing a 24 hour time with minutes and seconds, we can test that the time wraps around to 00:00:00 after 23:59:59, for instance. That seems to be an important property and something which despite our reasoning and calculations might be buggy. We could also test that minutes wrap around correctly (also that ticking a second after 59 minutes and 59 seconds resets minutes and seconds to 0 and increases the hour by one (unless its 23:59:59).

The strategy we'll use for testing here, is to come up with some challenging or interesting times and see that the toString() method returns the correct string representation of the time (since it internally uses the hours(), minutes() and seconds() methods).

So we'll create some Time objects representing interesting times, and then invoke toString() and assert that we get the String we expect to get. If the assertion failes, the test program should fail with an error message. So, if the test program doesn't fail (or crash), we at least know that our assertions were true.

The TestTime class

We'll write a class TestTime with a main method and create a series of Time objects representing some carefully (well, rather carefully) selected time values. Then we assert that the String we get back from toString() on the objects is equal to our predicted (expected or wanted) time string.

For instance, we could create a Time object representing 23:59:59 and call toString() on it, and assert that the String we get is equal to "23:59:59". In Java, we can use the assert construct which looks like this:

    Time t;
    t = new Time(23, 59, 59);
    assert t.toString().equals("23:59:59")
      : "Expected: 23:59:59. Got: " + t;

The way an assert statement works, is that it takes two arguments (separated by colon) where the first argument should be a predicate (a boolean expression, a claim about something which can be true or false), and the second argument should be an expression of type String which will be the error message if the assert expression is evaluated to false. If the boolean expression is evaluated to false, an AssertException is thrown with the error message String as the message.

So, in a way, the above could have been re-written as something like:

    Time t;
    t = new Time(23, 59, 59);
    if (! t.toString().equals("23:59:59")) {
      throw new AssertionException("Expected: 23:59:59. Got: " + t);
    }

If the execution continues past the assertion statement, then there was no error and the test passed (was OK).

How can we test that the checkTime() method works, then? We want to assert that if we send in what we know is a faulty time, then an IllegalArgumentException should be thrown.

Don't worry if you don't know about exceptions. There's a chapter on Exceptions later on in this book! Just read this and try to understand it and make a mental note about the fact that there are creatures called Exceptions in Java and that you will learn more about them later on.

One way to to that, is to have a try-catch block and call the method with the value which should throw an exception. In the catch-clause, we print a message saying that the test passed (if we want to), because a positive outcome is that the method threw the exception as we hoped.

Just under the call to the method which should throw an exception, we assert false and a String with a message that the test failed (since we should never reach this statement, since we hoped that an exception should have been thrown):

  static void testIllegal(int hour, int minute, int second) {
    try {
      Time t = new Time(hour, minute, second);
      assert false : "Failed: " + hour + ", " + minute + ", " + second +
        " was accepted"; 
    } catch (IllegalArgumentException e) {
      System.err.println(e.getMessage() + " Passed!");
    }
  }

Calling the method above with bad time values like testIllegal(99, 1, 1) (there can't be an hour value of 99), would trigger an IllegalArgumentException and we could print that the test passed, if we want to do that. If the test fails (no IllegalArgumentException was thrown dispite of the hour value of 99), then the assert false will crash our program.

Also, we should check that the tick() works and correctly increases a time one second (including wrapping around seconds, minutes and hours). We'll leave it to you as an exercise to come up with a more extensive list of tests.

Note: You must enable assertions when you run the test program: java -ea org.juneday.main.TestTime

We'll post a link to suggested source code for both the Time class and the TestTime program soon.

We hope you enjoyed this!

Extra exercise:

Re-write the Time class to instead use three instance variables for hour, minute and second. Replace the mathematical stuff with direct use of the variables (you still need the checkTime() method to prevent bad input data)

Object oriented version without mathematics

For those of you who don't like the remainder version above, you may use object orientation instead.

And those of you who have a problem understanding object orientation, you might learn something from this version too.

The idea behind this version is to teach you an object oriented approach to solving the problem of keeping a 24 hour time and correctly wrapping around the max values for seconds, minutes and hour.

The problem is to make sure that when we tick() the Time one second, if the second value is at 59, it should go down to 0 again but instead increase the minute value by one.

Let's look at the rules for wrapping around the values of the time:

  • The smallest Time allowed is 00:00:00 (exactly at midnight)
  • The largest Time allowed is 23:59:59 (one second to midnight)
  • The largest second allowed is 59 after which it becomes 00 but causes minute to increase by one
  • The largest minute allowed is 59 after which it becomes 00 but causes hour to increase by one
  • The largest hour allowed is 23 after which it becomes 00 but it doesn't cause any other field to increase - since this only happens at midnight and there is no unit after hour

We can see a pattern emerge from the rules. Apparently, a Time consists of three TimeUnits; hours, minutes and seconds. They all wrap around to zero at some limit. Two of them, seconds and minutes, have the concept of a nextUnit which they affect when wrapping around:

  • When seconds wraps around, it affects its nextUnit, the minutes
  • When minutes wraps around, it affects its nextUnit, the hours

So, your Time class needs three instance variables of type TimeUnit:

  • private TimeUnit hours
  • private TimeUnit minutes
  • private TimeUnit seconds

What behavior does our Time class need?

  • A constructor, accepting three int values representing hours, minutes and seconds
  • Telling us the number of hours(), minutes() and seconds() as an int value
  • Ticking one second up, using the public void tick() method
  • Representing itself as a String on the format hh:mm:ss, using public String toString()

Looking at our analysis of TimeUnit, we see that the TimeUnit class needs three instance variables:

  • private int value; // the value of the unit
  • private int limit; // the limit of when to wrap to zero
  • private TimeUnit nextUnit; // The next unit to increase when wrapping to zero, if any

What behavior does our TimeUnit class need?

  • A constructor accepting value and limit (so that we can create e.g. an our with limit 24)
  • A constructor accepting value, limit and a TimeUnit for the nextUnit (so that we can create e.g. a second with limit 60 and a TimeUnit representing the minute as its nextUnit)
  • A method to increment() the value and handle wrap-around when we reach the limit
  • A method telling us its intValue() (so that the Time class knows the value when needed)
  • A method representing itself as a String, padding with zeros, so that e.g. 4 becomes the String "04"
Time and TimeUnit class diagram
So, it seems we need to write two classes, Time which is composed with TimeUnit (composed means "consists of" - every Time has three TimeUnit members), and TimeUnit, which is composed with itself (A TimeUnit object can have an instance variable of type TimeUnit, as its nextUnit).

Let's start with the TimeUnit class and try to translate the above to code. TimeUnit seems simple enough to start with.

public class TimeUnit {

  // three instance variables as per description above
  private int value;
  private int limit;
  private TimeUnit nextUnit;

  // A constructor accepting value and limit
  // (so that we can create e.g. an our with limit 24)
  public TimeUnit(int value, int limit) {
    this.value = value;
    this.limit = limit;
  }

  // A constructor accepting value, limit and a TimeUnit
  // for the nextUnit (so that we can create e.g. a second
  // with limit 60 and a TimeUnit representing the minute
  // as its nextUnit)
  public TimeUnit(int value, int limit, TimeUnit nextUnit) {
    // We have a constructor for value and limit, let's us that:
    this(value, limit);
    this.nextUnit = nextUnit;
  }

  // A method to increase the value and handle wrap-around
  // when we reach the limit
  public void increment() {
    // will we reach limit?
    if (value + 1 == limit) {
      // do we have a nextUnit? - an hour doesn't!
      if (nextUnit != null) {
        nextUnit.increment();
      }
      // reset to zero
      value = 0;
    } else {
      // We will not reach limit, so just increment value
      value += 1;
    }
  }

  // A method telling us its int value (so that the
  // Time class knows the value when needed)
  public int intValue() {
    return intValue;
  }

  // A method representing itself as a String, padding with zeros,
  // so that e.g. <code>4</code> becomes the String <code>"04"</code>
  @Override // we will learn about this in a future lecture
  public String toString() {
    return String.format("%02d", value); // pads numbers < 10 with a zero
  }

}

That's pretty much it. But shouldn't we handle illegal values?

Let's change the constructor accepting limit and value to only accept values under the limit and not negative numbers:

    public TimeUnit(int limit, int value) {
      this.limit = limit;
      if (value >= limit) {
        throw new IllegalArgumentException(this.getClass().getName() +
                                           " cannot exceed " + limit);
      }
      if (value < 0) {
        throw new IllegalArgumentException(this.getClass().getName() +
                                           " value cannot be negative: " +
                                           value);
      }
      this.value = value;
    }

We will teach you about exceptions in a future lecture, so don't worry if you don't understand the throw this-or-that-thing just yet. Focus on the if-statement checking for illegal arguments.

That was pretty simple! Now we have a TimeUnit class which the Time class can use for its members seconds, minutes and hours.

So let's create the class for Time, as per our analysis above:

public class Time {

  // your Time class needs three instance variables of type TimeUnit:
  private TimeUnit hours;
  private TimeUnit minutes;
  private TimeUnit seconds;

  // A constructor, accepting three int values
  // representing hour, minute and second:
  public Time(int hour, int minute, int second) {
    this.hours = new TimeUnit(24, hour);
    // minutes will use hours as its nextUnit
    this.minutes = new TimeUnit(60, minute, hours);
    // seconds will use minutes as its nextUnit
    this.seconds = new TimeUnit(60, second, minutes);
  }

  // Behaviour:

  // Telling us the number of hours, minutes and seconds
  public int seconds() {
    return seconds.intValue();
  }

  public int minutes() {
    return minutes.intValue();
  }
  
  public int hours() {
    return hours.intValue();
  }

  // Ticking one second up, it's up to seconds to increase minutes if needed
  public void tick() {
    seconds.increment();
  }

  // Representing itself as a String on the format hh:mm:ss
  public String toString() {
    return new StringBuilder(hours.toString())
      .append(":")
      .append(minutes.toString())
      .append(":")
      .append(seconds.toString())
      .toString();
  }
}

That's it? Yes, pretty much. We still have to test that our Time class works, since we should never trust ourselves to be perfect programmers.

The point of this version, was to show you that using object orientation, a simple analysis of the problem and breaking down the problem in smaller parts, made the problem actually very simple.

Once we realized that a Time consists of three TimeUnits, we could start thinking of a smaller problem. We then realized that a TimeUnit should handle wrap around of its value according to some limit, and that some units like seconds and minutes have the concept of a nextUnit which should be increment()ed when wrapping around, the class practically wrote itself.

The only tricky part was the constructor which should handle illegal arguments. We haven't yet learned about throwing exceptions, but we think you understand the purpose of the if statements: don't accept values greater than or equal to the limit. And don't accept negative values.

Perhaps, the increment() method was a little tricky too. But it's really not that hard to understand (we think).

It needs to check that if the value is increased by one, will it reach the limit?

    if (value + 1 == limit) {

If so, does it have a nextUnit?

      if (value + 1 == limit) {
        if (nextUnit != null) {
          nextUnit.increment();
        }

If so, it increments also the nextUnit before resetting the value to zero.

      if (value + 1 == limit) {
        if (nextUnit != null) {
          nextUnit.increment();
        }

If the limit wouldn't be reached, we simply increase the value with one.

      if (value + 1 == limit) {
        if (nextUnit != null) {
          nextUnit.increment();
        }
        value = 0;
      } else {
        value += 1;
      }

With the TimeUnit class in place, writing the Time class was very simple. We only needed to create the TimeUnits in the constructor (passing along the value, limit and, for seconds and minutes, the nextUnit. To tick() the Time one second up, we only needed to call seconds.increment() because the seconds object was capable of and responsible for also increasing it's nextUnit (the minutes) in case of wrap around. This would in turn trigger the same behavior of the minutes object, which had the hours object as its nextUnit.

We hope that you found this interesting and helpful. At least you will probably have picked some methodology for how to analyse a small problem like representing time and handling wrap around. And how to solve it using object orientation. The OO-part was to realize that the seconds, minutes and hours objects were not simply of type int but rather of a class TimeUnit which we could write with just a few lines of code.

And probably some of you, were pleased to see that object oriented thinking can spare us from having to use mathematics all the time (nu pun intended).

Links

Slides and video

Java - Classes - Wrapping it up (Full playlist) | Java - Classes - Wrapping it up 1/8 | 2/8 | 3/8 | 4/8 | 5/8 | 6/8 | 7/8 | 8/8 | Media:Classes - Wrapping it up.pdf

Source code

Suggested solution for the Time class as of the instructions above

The source code can be downloaded from here.

Expand using link to the right to see the code.

Lines of code: 111.

//Time.java:
package org.juneday.time;

public class Time {
  
  private int secs;

  public Time(int hour, int minute, int second) {
    setTime(hour, minute, second);
  }

  private void checkTime(int hour, int minute, int second) {
    if (hour > 23 || hour < 0 ||
        minute > 59 || minute < 0 ||
        second > 59 || second < 0) {
      throw new IllegalArgumentException
        (new StringBuilder("Bad time: ")
         .append(hour)
         .append(":")
         .append(minute)
         .append(":")
         .append(second)
         .toString());      
    }
  }

  public void setTime(int hour, int minute, int second) {
    checkTime(hour, minute, second);
    secs = hour * 60 * 60 + minute * 60 + second;    
  }

  public void tick() {
    secs = (secs + 1) % (60 * 60 * 24);
  }

  public int hours() {
    return secs / 60 / 60;
  }

  public int minutes() {
    return secs / 60 % 60;
  }

  public int seconds() {
    return secs % 60;
  }

  @Override
  public String toString() {
    return String
      .format("%02d:%02d:%02d",
              hours(),
              minutes(),
              seconds());
  }
  
}

Bonus - An object oriented version where you don't have to use the remainder operator %

The source code can be downloaded from:

Expand using link to the right to see the code.

Lines of code: 88.

//Time.java:
package org.juneday.time;

public class Time {
  
  private TimeUnit hours;
  private TimeUnit minutes;
  private TimeUnit seconds;

  public Time(int hour, int minute, int second) {
    this.hours = new TimeUnit(24, hour);
    this.minutes = new TimeUnit(60, minute, hours);
    this.seconds = new TimeUnit(60, second, minutes);
  }

  public void tick() {
    seconds.increment();
  }

  public int seconds() {
    return seconds.intValue();
  }

  public int minutes() {
    return minutes.intValue();
  }
  
  public int hours() {
    return hours.intValue();
  }
  
  public String toString() {
    return new StringBuilder(hours.toString())
      .append(":")
      .append(minutes.toString())
      .append(":")
      .append(seconds.toString())
      .toString();
  }

}

// TimeUnit.java:
package org.juneday.time;

public class TimeUnit {
  
  private int limit;
  private int value;
  private TimeUnit nextUnit;
  
  public TimeUnit(int limit, int value) {
    this.limit = limit;
    if (value >= limit) {
      throw new IllegalArgumentException(this.getClass().getName() +
                                " cannot exceed " + limit);
    }
    if (value < 0) {
      throw new IllegalArgumentException(this.getClass().getName() +
                                         " value cannot be negative: " +
                                         value);
    }
    this.value = value;
  }

  public TimeUnit(int limit, int value, TimeUnit nextUnit) {
    this(limit, value);
    this.nextUnit = nextUnit;
  }

  public int intValue() {
    return value;
  }
  
  public void increment() {
    if (value + 1 == limit) {
      if (nextUnit != null) {
        nextUnit.increment();
      }
      value = 0;
    } else {
      value += 1;
    }
  }

  @Override
  public String toString() {
    return String.format("%02d", value);
  }  
  
}

A more complicated suggested solution, using inheritance (introduced later in this course material)

Source code can be downloaded from:

Expand using link to the right to see the code.

Lines of code: 108.

//Time.java:
package org.juneday.time;

public class Time {

  
  private TimeUnit hours;
  private TimeUnit minutes;
  private TimeUnit seconds;

  public Time(int hour, int minute, int second) {
    this.hours = new Hour(hour);
    this.minutes = new Minute(minute, hours);
    this.seconds = new Second(second, minutes);
  }

  public void tick() {
    seconds.increment();
  }

  public int seconds() {
    return seconds.intValue();
  }
  public int minutes() {
    return minutes.intValue();
  }
  public int hours() {
    return hours.intValue();
  }
  
  public String toString() {
    return new StringBuilder(hours.toString())
      .append(":")
      .append(minutes.toString())
      .append(":")
      .append(seconds.toString())
      .toString();
  }

}

//TimeUnit.java:
package org.juneday.time;

public abstract class TimeUnit {
  
  private int limit;
  private int value;
  private TimeUnit nextUnit;
  
  public TimeUnit(int limit, int value) {
    this.limit = limit;
    if (value >= limit) {
      throw new IllegalArgumentException(this.getClass().getName() +
                                " cannot exceed " + limit);
    }
    if (value < 0) {
      throw new IllegalArgumentException(this.getClass().getName() +
                                         " value cannot be negative: " +
                                         value);
    }
    this.value = value;
  }

  public TimeUnit(int limit, int value, TimeUnit nextUnit) {
    this(limit, value);
    this.nextUnit = nextUnit;
  }

  public int intValue() {
    return value;
  }
  
  public void increment() {
    if (value + 1 == limit) {
      if (nextUnit != null) {
        nextUnit.increment();
      }
      value = 0;
    } else {
      value += 1;
    }
  }

  @Override
  public String toString() {
    return String.format("%02d", value);
  }  
  
}

// Hour.java:
package org.juneday.time;

public class Hour extends TimeUnit {
  public Hour(int value) {
    super(24, value);
  }
}

// Minute.java:
package org.juneday.time;

public class Minute extends TimeUnit {
  public Minute(int value, TimeUnit nextUnit) {
    super(60, value, nextUnit);
  }
}

// Second.java:
package org.juneday.time;

public class Second extends TimeUnit {
  public Second(int value, TimeUnit nextUnit) {
    super(60, value, nextUnit);
  }
}

Alternative solution using a nested class

Source code can be downloaded from:

Expand using link to the right to see the code.

Lines of code: 87.

package org.juneday.time;

public class Time {

  private static class TimeUnit {
  
    private int limit;
    private int value;
    private TimeUnit nextUnit;
  
    public TimeUnit(int limit, int value) {
      this.limit = limit;
      if (value >= limit) {
        throw new IllegalArgumentException(this.getClass().getName() +
                                           " cannot exceed " + limit);
      }
      if (value < 0) {
        throw new IllegalArgumentException(this.getClass().getName() +
                                           " value cannot be negative: " +
                                           value);
      }
      this.value = value;
    }

    public TimeUnit(int limit, int value, TimeUnit nextUnit) {
      this(limit, value);
      this.nextUnit = nextUnit;
    }

    public int intValue() {
      return value;
    }
  
    public void increment() {
      if (value + 1 == limit) {
        if (nextUnit != null) {
          nextUnit.increment();
        }
        value = 0;
      } else {
        value += 1;
      }
    }

    @Override
    public String toString() {
      return String.format("%02d", value);
    }  
  
  }
  
  private TimeUnit hours;
  private TimeUnit minutes;
  private TimeUnit seconds;

  public Time(int hour, int minute, int second) {
    this.hours = new TimeUnit(24, hour);
    this.minutes = new TimeUnit(60, minute, hours);
    this.seconds = new TimeUnit(60, second, minutes);
  }

  public void tick() {
    seconds.increment();
  }

  public int seconds() {
    return seconds.intValue();
  }

  public int minutes() {
    return minutes.intValue();
  }
  
  public int hours() {
    return hours.intValue();
  }
  
  public String toString() {
    return new StringBuilder(hours.toString())
      .append(":")
      .append(minutes.toString())
      .append(":")
      .append(seconds.toString())
      .toString();
  }

}

Alternative solution using a nested class with Optional<TimeUnit> as nextUnit

Source code can be downloaded from:

Expand using link to the right to see the code.

package org.juneday.time;

import java.util.Optional;

public class Time {

  private static class TimeUnit {
  
    private int limit;
    private int value;
    private Optional<TimeUnit> nextUnit;
  
    public TimeUnit(int limit, int value) {
      this.limit = limit;
      if (value >= limit) {
        throw new IllegalArgumentException(this.getClass().getName() +
                                           " cannot exceed " + limit);
      }
      if (value < 0) {
        throw new IllegalArgumentException(this.getClass().getName() +
                                           " value cannot be negative: " +
                                           value);
      }
      this.value = value;
      this.nextUnit = Optional.empty();
    }

    public TimeUnit(int limit, int value, TimeUnit nextUnit) {
      this(limit, value);
      this.nextUnit = Optional.of(nextUnit);
    }

    public int intValue() {
      return value;
    }
  
    public void increment() {
      if (value + 1 == limit) {
        if (nextUnit.isPresent()) {
          nextUnit.get().increment();
        }
        value = 0;
      } else {
        value += 1;
      }
    }

    @Override
    public String toString() {
      return String.format("%02d", value);
    }  
  
  }
  
  private TimeUnit hours;
  private TimeUnit minutes;
  private TimeUnit seconds;

  public Time(int hour, int minute, int second) {
    this.hours = new TimeUnit(24, hour);
    this.minutes = new TimeUnit(60, minute, hours);
    this.seconds = new TimeUnit(60, second, minutes);
  }

  public void tick() {
    seconds.increment();
  }

  public int seconds() {
    return seconds.intValue();
  }

  public int minutes() {
    return minutes.intValue();
  }
  
  public int hours() {
    return hours.intValue();
  }
  
  public String toString() {
    return new StringBuilder(hours.toString())
      .append(":")
      .append(minutes.toString())
      .append(":")
      .append(seconds.toString())
      .toString();
  }

}

Further reading

Where to go next

Next page is the first of a new topic: Inheritance - What is it

« PreviousBook TOCNext »