2
\$\begingroup\$

Looking for input on my code for a Stopwatch utility written in Java.

/**
 * File: Stopwatch.java
 */
package org.me.mystuff;

import java.util.concurrent.TimeUnit;

public class Stopwatch {
    /**
     * The start timestamp, as determined by a call to {@code System.nanoTime}.
     */
    private long start;

    /**
     * The total elapsed time recorded so far in nanoseconds (raw value).
     */
    private long elapsed;

    /**
     * Whether or not the stopwatch is running.
     */
    private boolean running;

    /**
     * Constructs a new {@code Stopwatch} instance.
     */
    public Stopwatch() {
        reset();
    }

    /**
     * Stops time interval measurement, resets the elapsed time to zero, and
     * starts measuring time again.
     * 
     * <p>
     * This is a convenience method equivalent to calling {@code reset} followed
     * by {@code start}.
     */
    public void restart() {
        reset();
        start();
    }

    /**
     * Stops time interval measurement and resets the elapsed time value to
     * zero.
     */
    public void reset() {
        running = false;
        start = 0;
        elapsed = 0;
    }

    /**
     * Initializes a new {@code Stopwatch} instance, sets the elapsed time value
     * to zero, and starts measuring elapsed time.
     * 
     * @return a running {@code Stopwatch} instance
     */
    public static Stopwatch startNew() {
        final var s = new Stopwatch();
        s.start();
        return s;
    }

    /**
     * Starts, or resumes, measuring elapsed time for an interval.
     */
    public void start() {
        // Calling start() on a running stopwatch is a no-op.
        if (!isRunning()) {
            running = true;
            start = start <= 0 ? System.nanoTime() : System.nanoTime() - elapsed;
        }
    }

    /**
     * Stops measuring elapsed time for an interval.
     */
    public void stop() {
        // Calling stop() on a stopped stopwatch is a no-op.
        if (isRunning()) {
            elapsed = System.nanoTime() - start;
            running = false;
        }
    }

    /**
     * Returns the elapsed time measured so far by this stopwatch in the
     * specified {@code TimeUnit}.
     * 
     * @param timeUnit
     *            the desired time unit
     * @return the duration in the specified time unit
     */
    public double elapsed(TimeUnit timeUnit) {
        // If the stopwatch is running, compute and return the current timestamp.
        // Otherwise, just return the timestamp as of the last call to stop().
        final long duration = isRunning() ? System.nanoTime() - start : elapsed;
        return UnitConverter.convert(duration, timeUnit);
    }

    /**
     * Returns {@code true} if the stopwatch is currently running.
     * 
     * @return {@code true} if this stopwatch is running
     */
    public boolean isRunning() {
        return running;
    }

    /**
     * Returns a string representation of the elapsed time measured so far by
     * this stopwatch, expressed in milliseconds using two decimal places.
     * 
     * <p>
     * Example: 1.2345 milliseconds would return a string equal to
     * {@code "1.23 ms"}.
     * 
     * <p>
     * This is a convenience method that simplifies querying the stopwatch for
     * reporting purposes.
     */
    @Override
    public String toString() {
    final double duration = elapsed(TimeUnit.MILLISECONDS); 
    final double rounded = Math.round(duration * 100d) / 100d;
    return String.valueOf(rounded).concat(unit);
    }

    /**
     * This class is an internal utility to supplement the stopwatch's unit
     * conversion operations.
     */
    private static class UnitConverter {
        /**
         * The number of nanoseconds in a millisecond.
         */
        static final double NANOS_PER_MILLI = 1e6;

        /**
         * The number of nanoseconds in a second.
         */
        static final double NANOS_PER_SECOND = 1e9;

        /**
         * The number of nanoseconds in a minute.
         */
        static final double NANOS_PER_MINUTE = 6e10;

        /**
         * The number of nanoseconds in an hour.
         */
        static final double NANOS_PER_HOUR = 3.6e12;

        /**
         * Converts the specified nanosecond value to the specified time unit
         * and returns the result.
         * 
         * @param nanoTime
         *            the value to convert (in nanoseconds)
         * @param toUnit
         *            the desired time unit
         * @return the converted value
         */
        static double convert(long nanoTime, TimeUnit toUnit) {
            switch (toUnit) {
            case MILLISECONDS:
                return toMillis(nanoTime);
            case SECONDS:
                return toSeconds(nanoTime);
            case MINUTES:
                return toMinutes(nanoTime);
            case HOURS:
                return toHours(nanoTime);
            default:
                throw new IllegalArgumentException("no valid time unit specified");
            }
        }

        static double toMillis(long nanos) {
            return nanos / NANOS_PER_MILLI;
        }

        static double toSeconds(long nanos) {
            return nanos / NANOS_PER_SECOND;
        }

        static double toMinutes(long nanos) {
            return nanos / NANOS_PER_MINUTE;
        }

        static double toHours(long nanos) {
            return nanos / NANOS_PER_HOUR;
        }
    }
}

The stopwatch uses System.nanoTime as its time source. In general it works similarly to the standard .NET version and is, as far as my unit tests go, correct. You can query it whether or not it is running, and it will give the most recent recorded value each time.

I am just curious to hear some thoughts on my code and suggestions for improvement, if any. It's extremely lightweight so feel free to test it on your machine if you wish.

\$\endgroup\$
0
\$\begingroup\$
  • You are recreating TimeUnit.convert(long, TimeUnit) in your UnitConverter. Or, keeping with single responsiblity principle, I don't really see why a stopwatch should be able to do unit conversion.
  • Elapsed time is always forced to double primitive type even if the user wants nothing but nanoseconds. See above.
  • Rounding and formatting in toString should be done with DecimalFormat and RoundingMode (https://stackoverflow.com/questions/153724/how-to-round-a-number-to-n-decimal-places-in-java). But in the end toString is a debugging method, which should output the exact state of the object and it shouldn't convert types or round values at all. See above.
  • Decide between static initializer methods and public constructors. Having both types of initializers is confusing.
  • Because primitive types can't convey the information, you should add the unit specifier to the names of your private members. E.g. elapsedNanos.
\$\endgroup\$

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.