JavaRanch Home    
 
This page:         last edited 16 February 2014         What's Changed?         Edit

User Input Part Deux   

(level: Beginner/Intermediate)

This page is "part 2" of the UserInput tutorial, so if you haven't read the first part. I suggest you do before you go any further.

.

UserInput focused on rationalizing the process of user input, which is notoriously fiddly and verbose, into methods that you can reuse. In this chapter, we plan to show you how to build on that by creating your own utility class (or classes).

There are two sections to this part - a "simple" one, and a more "object-oriented" one:

  • The first helps beginners to create their own utility class so that they don't have to constantly copy code when they're writing exercises that need user input. It's simple (especially if you read UserInput), but it has some drawbacks.

  • The second suggests a way to build a more flexible input object.

After which you may be interested in going on to UserInputPartIII, which shows a way to build a proper input framework that you can extend and configure any way you like. Needless to say, this part is MUCH longer, and will take more time to write; so if you see a notice at the start of it saying "[under construction]", please be patient.



The simple way - A basic utility class  

You will probably have run across utility classes in your lessons already - for example: Math - and their structure is almost always the same:

  1. They are not instantiated.
  2. Their methods are static, so they are called via the class name, eg: Math.log(2.0).

and the method for setting one up always follows the same pattern:

public final class Input {
  private Input() {}
  ...

There are three things to note about the above declaration:

  1. The class is public, as you will probably want anyone to be able to use it.
  2. It has a private no-args constructor. This prevents anyone from accidentally instantiating it.
  3. It is also final. This is important because you don't want anyone to be able to extend it. The fact is that, since it can't be instantiated, it can't actually be extended either; but adding the final makes that point clear to anyone using it - ie, it's basically a form of documentation.

.

So...now what do we do?

Well, the UserInput page showed you how to set up generic input methods, so now you simply move (note: move, not copy) them to your new class, viz:


public final class Input {

  private Input() {}

  // The input() method is defined near the end.

  private static final Integer integerOrNull(
    Scanner s, String prompt)
  {
    try {
      return Integer.valueOf( input(s, prompt) );
    } catch (NumberFormatException e) {
      return null;
    }
  }

  private static final Double doubleOrNull(
    Scanner s, String prompt)
  {
    try {
      return Double.valueOf( input(s, prompt) );
    } catch (NumberFormatException e) {
      return null;
    }
  }

  // Returns the "range" prompt for a set of bounds.
  private static final String rangeString(
    Number lowest, Number highest)
  {
    if (lowest == null || highest == null)
      throw new NullPointerException("bounds cannot be null");

    return "between " + lowest + " and " + highest;
  }

  /**
   * Prompts for an integer and does not return until
   * the user enters a valid one.
   * @param s The Scanner to be used for input.
   * @param suffix The suffix to be used in the prompt.
   * @return The user's input, converted to an integer.
   */
  public static final Integer getInteger(
    Scanner s, String suffix)
  {
    String prompt = "Please enter " + suffix;
    Integer i;

    while((i = integerOrNull(s, prompt)) == null) {
      System.out.println("Invalid number. Please try again.");
    }

    return i;
  }

  /**
   * Prompts for an integer and does not return until
   * the user enters a valid one IN THE SPECIFIED RANGE.
   * @param s The Scanner to be used for input.
   * @param name The name of the value being input
   *             (should include the indefinite article).
   * @param lowest The lowest value allowed.
   * @param highest The highest value allowed.
   * @return The user's input, converted to an integer.
   */
  public static final int inRange(
    Scanner s, String name, int lowest, int highest)
  {
    String range = rangeString(lowest, highest);
    String suffix = name + " " + range;

    int inRange = getInteger(s, suffix);

    while (inRange < lowest || inRange > highest) {
      System.out.println("Out of bounds: must be " + range);
      inRange = getInteger(s, suffix);
    }

    return inRange;
  }

  /**
   * Prompts for a double and does not return until
   * the user enters a valid one.
   * @param s The Scanner to be used for input.
   * @param suffix The suffix to be used in the prompt.
   * @return The user's input, converted to a double.
   */
  public static final Double getDouble(
    Scanner s, String suffix)
  {
    String prompt = "Please enter " + suffix;
    Double d;

    while((d = doubleOrNull(s, prompt)) == null) {
      System.out.println(
        "Invalid decimal number. Please try again.");
    }

    return i;
  }

  /**
   * Prompts for a double and does not return until
   * the user enters a valid one IN THE SPECIFIED RANGE.
   * @param s The Scanner to be used for input.
   * @param name The name of the value being input
   *             (should include the indefinite article).
   * @param lowest The lowest value allowed.
   * @param highest The highest value allowed.
   * @return The user's input, converted to a double.
   */
  public static final double inRange(
    Scanner s, String name, double lowest, double highest)
  {
    String range = rangeString(lowest, highest);
    String suffix = name + " " + range;

    double inRange = getDouble(s, suffix);

    while (inRange < lowest || inRange > highest) {
      System.out.println("Out of bounds: must be " + range);
      inRange = getDouble(s, suffix);
    }

    return inRange;
  }

  /**
   * Prompts for a date as three separate values, validating each
   * based on previous input, and returns the result as a Calendar.
   * @param s The Scanner to be used for input.
   * @return The user's input, converted to a Calendar.
   */
  public static final Calendar getDate(Scanner s) {
    int yy = inRange(s, "a year"19002050);

    // Determine if the year they entered is a leap-year
    boolean leapYear = yy % 4 != 0 ? false
      : yy % 100 != 0 ? true : yy % 400 == 0;

    int mm = inRange(s, "a month"112);

    // Work out the length of the month they entered
    int monthLength;
    switch (mm) {
      case 2:
        monthLength == leapYear ? 29 : 28;
        break;
      case 4:
      case 6:
      case 9:
      case 11:
        monthLength == 30;
        break;
      default:
        monthLength == 31;
    }

    int dd = inRange(s, "a day"1, monthLength);

    return new Gregorian''''''Calendar(yy, mm - 1, dd);
  }

  /**
   * Prompts for the answer to a question, and does not exit until
   * the user enters a valid reply, returning it as a boolean.
   * @param s The Scanner to be used for input.
   * @param question The question to be used as the prompt.
   * @param yes The value that indicates yes/true.
   * @param no The value that indicates no/false.
   * @return The user's input, converted to a boolean.
   */
  public static final boolean getBoolean(Scanner s,
    String question, String yes, String no)
  {
    String prompt = question + " Enter '"
      + yes + "' for yes, '" + no "' for no";

    while(true) {
      String input = input(s, prompt);

      if (input.equals(yes))
        return true;
      else if (input.equals(no))
        return false;
      else System.out.println("Input is not '"
         + yes + "' or '" + no "'");
    } 
  }

  /**
   * Prompts for input and returns the user's input.
   * @param s The Scanner to be used for input.
   * @param prompt The String to be used as a prompt.
   * @return The user's input, trimmed of all whitespace.
   */
  public static final String input(Scanner s, String prompt)
  {
    System.out.println(prompt);
    return s.nextLine().trim();
  }
}

Hopefully, most of the methods should be familiar from the UserInput page. We've simply transferred them to a class that combines them all, so that they can be used by anyone. I've also added a rangeString() helper method for creating the "range" part of a message.

Note also the special comments that I've included for all the public methods. These are javadoc comments, and I advise you to get to know them, because they provide the wonderful documentation you get in the API.

And now we change our old class to use the utility methods - ie, something like this:

import myutilities.Input;

public class myClass {
  ...

  public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);

    int number = Input.getInteger(scanner,
      "a number");

    int month  = Input.inRange(scanner,
      "a Month", 1, 12);

    double temp = Input.inRange(scanner,
      "today's temperature", -50.0, 130.0);

    Calendar date = Input.getDate(scanner);

    ...
  }

And you can now use those Input methods in any class that needs input.



Improving the Input class (slightly more advanced)  

You may have noticed that there's still quite a lot of duplicated code in our Input class. The getInteger()/getDouble() and inRange() methods, for example, are virtually identical except for the type-specific calls they make. Wouldn't it be nice if we could write ones that simply take the type of thing we want to convert to?

At this point, a lot of people (especially beginners) immediately start looking at reflection, because it seems like an answer to all your prayers for dynamic type-checking.

My advice: DON'T.

Reflection can be a very powerful tool in the right situations, but it should be used very sparingly because it's verbose, error-prone, difficult to test, and SLOW.

Furthermore, it should only be used in cases where clients really don't know what a type is going to be until runtime, and that's not the case here. When we call an Input method, we DO know what type we want; we simply want a way to generalise our methods.

So what can we do instead? Answer: Create a class or interface that encapsulates our "type". That's how Object-Orientation works.

In our case, the only thing that's specific in our input process is the conversion; everything else is generic. And for types that don't need converting? Simple: don't convert; just return them.

.

So, putting that into practise, we might come up with something like this:

public interface Type<T> {
  // Returns the supplied String converted to this
  // type, or null if there was a problem.
  public T convert(String s);
}

and then we can write member types for our Input class as follows:

public static final Type<Integer> INT =
  new Type<Integer>() {
    @Override
    public Integer convert(String s) {
     try {
      return Integer.valueOf(s);
     } catch (NumberFormatException e) {
      return null;
     }
    }
  };

and for types that don't need conversion:

public static final Type<String> STRING =
  new Type<String>() {
    @Override
    public String convert(String s) {
      return s;
    }
  };

or indeed, just leave it out altogether. After all, a String can be obtained by simply calling Input.input().

And just in case you're not familiar with the constructs above, they are anonymous classes - a very useful thing to get to know.

.  

If the above isn't very clear yet, let's see what happens when we put it in our Input class. Pay particular attention to the get() and inRange() methods that replace the type-specific ones we had before:


public final class Input {

  private Input() {}

  /**
   * Defines a type that needs to be created from an
   * input String.
   */
  public static interface Type<T> {
    /**
     * Returns the supplied String converted to this
     * type; or null if there was a problem.
     * @param s The String to be converted.
     * @return As described above.
     */
    public T convert(String s);
  }

  // Member types - These replace the '...OrNull()'
  // methods we had before.

  /**
   * A type for Integers.
   */
  public static final Type<Integer> INT = new Type<Integer>() {
    @Override
    public Integer convert(String s) {
      try {
        return Integer.valueOf(s);
      } catch (NumberFormatException e) {
        return null;
      }
    }
  }; 

  /**
   * A type for Doubles.
   */
  public static final Type<Double> DOUBLE = new Type<Double>() {
    @Override
    public Double convert(String s) {
      try {
        return Double.valueOf(s);
      } catch (NumberFormatException e) {
        return null;
      }
    }
  };

  // Returns the "range" prompt for a set of bounds.
  private static final <T> String rangeString(T lowest, T highest)
  {
    if (lowest == null || highest == null)
      throw new NullPointerException("bounds cannot be null");

    // put quotes around anything that isn't a Number
    String quote = lowest instanceof Number ? "" : "'";

    return "between " + quote + lowest + quote
      + " and " + quote + highest + quote;
  }

  /**
   * Prompts for a specific type and does not return until
   * the user enters a valid one.
   * @param type The type to be input.
   * @param s The Scanner to be used for input.
   * @param suffix The suffix to be used in the prompt.
   * @return The user's input, converted to the specified type.
   */
  public static final <T> T get(Type<T> type,
    Scanner s, String suffix)
  {
    String prompt = "Please enter " + suffix;
    T value;

    while((value = type.convert( input(s, prompt) )) == null) {
      System.out.println("Invalid input. Please try again.");
    }

    return value;
  }

  /**
   * Prompts for a Comparable type and does not return until
   * the user enters a valid one IN THE SPECIFIED RANGE.
   * @param type The type to be input.
   * @param s The Scanner to be used for input.
   * @param name The name of the value being input
   *             (should include the indefinite article).
   * @param lowest The lowest value allowed.
   * @param highest The highest value allowed.
   * @return The user's input, converted to a double.
   */
  public static final <T extends Comparable<? super T>>
    T inRange(Type<T> type, Scanner s, String name,
      T lowest, T highest)
  {
    String range = rangeString(lowest, highest);
    String suffix = name + " " + range;

    T inRange = get(type, s, suffix);

    while (inRange.compareTo(lowest) < 0
        || inRange.compareTo(highest) > 0) {
      System.out.println("Out of bounds: must be " + range);
      inRange = get(type, s, suffix);
    }

    return inRange;
  }

  /**
   * Prompts for a date as three separate values, validating each
   * based on previous input, and returns the result as a Calendar.
   * @param s The Scanner to be used for input.
   * @return The user's input, converted to a Calendar.
   */
  public static final Calendar getDate(Scanner s) {
    // note the changed call
    int yy = inRange(INT, s, "a year"19002050);

    // Determine if the year they entered is a leap-year
    boolean leapYear = yy % 4 != 0 ? false
      : yy % 100 != 0 ? true : yy % 400 == 0;

    int mm = inRange(INT, s, "a month"112);

    // Work out the length of the month they entered
    int monthLength;
    switch (mm) {
      case 2:
        monthLength == leapYear ? 29 : 28;
        break;
      case 4:
      case 6:
      case 9:
      case 11:
        monthLength == 30;
        break;
      default:
        monthLength == 31;
    }

    int dd = inRange(INT, s, "a day"1, monthLength);

    return new Gregorian''''''Calendar(yy, mm - 1, dd);
  }

  // Other methods (input() and getBoolean()) as before.
  ...
}

Note the static qualifier on the Type interface. To be honest, I'm not sure it's absolutely necessary, but I always put it in to remind myself that the definition is nested; and it IS important when you're defining classes.

.

Do you see what's happened? No more overloaded methods, and no (or very little) duplicated code. Furthermore, we can add new member types (for example, a LONG type) as we find a need, and we shouldn't need to change anything else. Indeed, clients can supply their own types if they want to, by simply writing conversion classes that implement Input.Type. And that is what Object-orientation is all about.

And now our calling class will look like this:

import myutilities.Input;

public class myClass {
  ...

  public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);

    int number = Input.get(Input.INT,
      scanner, "a number");

    int month  = Input.inRange(Input.INT
      scanner, "a Month", 1, 12);

    double temp = Input.inRange(Input.DOUBLE,
      scanner, "today's temperature", -50.0, 130.0);

    Calendar date = Input.getDate(scanner);

    ...
  }

.

If you find the definitions of the generic methods confusing, don't worry about it too much for the moment. The first Input style will probably do you just fine until you learn a bit more about generics.

There is also a slight problem with what we've written so far: The error message. It simply says "Invalid input", when we probably want something a bit more descriptive. This could easily be remedied by adding a getType() method to our Type interface, but I'll leave that up to your ingenuity.

It should also be added that this is only ONE way of doing it; there are many others.

.

However, there are still a few drawbacks to the "utility class" approach. Try and see if you can work out what some of them are before you read on.



Drawbacks

Basically, the class is rather brittle, and a lot of that is due to the fact that it can't be instantiated, and its methods are all static:

  1. It only allows one prompt prefix: "Please enter".
  2. It assumes that messages are always directed to System.out.
  3. You have to pass the Scanner to every method. Wouldn't it be nice to be able to set that up once and just use it for every input?
  4. It doesn't allow the user to break out of the input process, even if they just "don't get it". It might be nice, for example, to give them a certain number of tries before the class simply gives up and throws an Exception.

All the above can be easily remedied, but you need to use an object rather than a utility.



A more 'Objective' way - An Input object  

Right, so we want to to include the things mentioned above in the "Drawbacks" section. The simplest way to do that is to make Input a class that we can instantiate, rather than a utility, and add them as fields that can be set up at construction time. Let's have a look at what that might look like:

public final class Input {
  // value used when any number of tries is allowed
  private static final int UNLIMITED =
    Integer.MAX_VALUE;

  private final Scanner scanner;
  private final PrintStream out;
  private final String prefix;
  private final int maxTries;

  // A constructor that allows the client to supply all
  // the "environment" information we need.
  public Input(InputStream in, OutputStream out,
      String prefix, int tries) {
    this.scanner = new Scanner(in);
    // Make our output stream an auto-flushable PrintStream
    // so we can use it just like System.out.
    this.out = new PrintStream(out, true);
    this.prefix = prefix;
    this.maxTries = tries > 0 ? tries : UNLIMITED;
  }

  // A constructor that uses reasonable defaults for the
  // input stream, output stream and prompt prefix.
  public Input(int tries) {
    this(System.in, System.out, "Please enter", tries);
  }

  ...
}

Pretty simple, no? The main difference so far is that now our constructors are public. We still keep the class final because we really don't want anyone extending it (yet).

And the rest of the Input class doesn't need to change too much, except that its methods will no longer be static. We also need to add logic to keep track of the number of tries on each input.

Let's have a look at what it might look like. I've used the second version as my basis:


public final class Input {
  private static final int UNLIMITED = Integer.MAX_VALUE;

  private final Scanner IN;
  private final PrintStream OUT;
  private final String prefix;
  private final int maxTries;

  /**
   * Returns an Input object that uses "environment"
   * information supplied by the client.
   * @param in The input stream to be used for input.
   * @param out The output stream to be used for messages.
   * @param prefix The prefix to be used in all prompts.
   * @param tries The number of tries allowed. A value less
   *        than 1 means "unlimited".
   */
  public Input(InputStream in, OutputStream out,
      String prefix, int tries) {
    this.IN = new Scanner(in);
    this.OUT = new PrintStream(out, true);
    this.prefix = prefix;
    this.maxTries = tries > 0 ? tries : UNLIMITED;
  }

  /**
   * Returns an Input object that uses System streams for
   * input and output, and prefixes each prompt with
   * "Please enter ".
   * @param tries The number of tries allowed. A value less
   *        than 1 means "unlimited".
   */
  public Input(int tries) {
    this(System.in, System.out, "Please enter", tries);
  }

  /**
   * Defines a type that needs to be created from an
   * input String.
   */
  public static interface Type<T> {
    /**
     * Returns the supplied String converted to this
     * type; or null if there was a problem.
     * @param s The String to be converted.
     * @return As described above.
     */
    public T convert(String s);
    /**
     * Returns the description of this Type, primarily for use
     * in error messages. It should include the INdefinite
     * article ('a' or 'an').
     * @return As described above.
     */
    public String description();
  }

  /**
   * A type for Integers.
   */
  public static final Type<Integer> INT = new Type<Integer>() {
    @Override
    public Integer convert(String s) {
      try {
        return Integer.valueOf(s);
      } catch (NumberFormatException e) {
        return null;
      }
    }
    @Override
    public String description() {
      return "an integer";
    }
  }; 

  /**
   * A type for Doubles.
   */
  public static final Type<Double> DOUBLE = new Type<Double>() {
    @Override
    public Double convert(String s) {
      try {
        return Double.valueOf(s);
      } catch (NumberFormatException e) {
        return null;
      }
    }
    @Override
    public String description() {
      return "a decimal number";
    }
  };

  /**
   * An object that keeps track of the number of input tries.
   * Note that this class is NOT 'static'.
   */
  private class TryCount {
    private int attempt = 0;

    /**
     * Increments this count, returning UNLESS it exceeds
     * the maximum number of tries allowed.
     * @throws IllegalStateException if the number of allowed
     *         tries is exceeded.
     */
    public final void increment() {
      int allowed = Input.this.maxTries;
      if (++attempt > allowed)
        throw new IllegalStateException("Max. attempts (" 
          + allowed + ") exceeded.");
    }

    /**
     * Returns the prompt for the attempt associated with this
     * count.
     * Specifically, if the number of attempts is limited,
     * it adds the prefix "(try C of M) ", where C is the
     * current count and M is the maximum allowed; otherwise,
     * it returns the prompt unchanged.
     * @param prompt The base prompt for this attempt.
     * @return The full prompt, as described above.
     */
    public final String prompt(String prompt) {
      int allowed = Input.this.maxTries;
      if (allowed == Input.UNLIMITED)
        return prompt;
      else
        return String.format("(%d of %d) %s",
          attempt, allowed, prompt);
    }
  }

  /**
   * Prompts for a type, keeping track of the number of tries.
   * The method loops until they either get it right, or they
   * exceed the number of tries allowed.
   * @param type The type to be input.
   * @param suffix The suffix to be used in the prompt.
   * @param count The count of the number of tries attempted
   *        so far.
   * @return The user's input, converted to the specified type.
   * @throws IllegalStateException if the number of tries allowed
   *         is exceeded.
   */
  private final <T> T get(Type<T> type, String suffix,
      TryCount count)
  {
    String basePrompt = "Please enter " + suffix;
    while ( true ) {
      count.increment();
      String input = input(count.prompt(basePrompt));
      T value = type.convert(input);
      if (value != null)
        return value;
      OUT.println("Invalid input for " + type.description()
        + ". Please try again.");
    }
  }

  /**
   * Returns the description of a "range".
   * @param lowest The lowest value allowed.
   * @param highest The highest value allowed.
   * @return As described above.
   */
  private final <T> String rangeString(T lowest, T highest)
  {
    if (lowest == null || highest == null)
      throw new NullPointerException("bounds cannot be null");

    // put quotes around anything that isn't a Number
    String quote = lowest instanceof Number ? "" : "'";

    return "between " + quote + lowest + quote
      + " and " + quote + highest + quote;
  }

  /**
   * Prompts for a specific type and does not return until
   * the user enters a valid one or they exceed the number
   * of tries allowed.
   * @param type The type to be input.
   * @param suffix The suffix to be used in the prompt.
   * @return The user's input, converted to the specified type.
   * @throws IllegalStateException if the number of tries allowed
   *         is exceeded.
   */
  public final <T> T get(Type<T> type, String suffix) {
    return get(type, suffix, new TryCount());
  }

  /**
   * Prompts for a Comparable type and does not return until
   * the user enters a valid one IN THE SPECIFIED RANGE.
   * @param type The type to be input.
   * @param name The name of the value being input
   *             (should include the indefinite article).
   * @param lowest The lowest value allowed.
   * @param highest The highest value allowed.
   * @return The user's input, converted to a double.
   * @throws IllegalStateException if the number of tries allowed
   *         is exceeded.
   */
  public final <T extends Comparable<? super T>>
    T inRange(Type<T> type, String name, T lowest, T highest)
  {
    String range = rangeString(lowest, highest);
    String suffix = name + " " + range;
    TryCount count = new TryCount();
    while (true) {
      T val = get(type, suffix, count);
      if (val.compareTo(lowest) >= 0 &&
          val.compareTo(highest) <= 0)
        return val;
      OUT.println("Out of bounds: must be " + range);
    }
  }

  /**
   * Prompts for a date as three separate values, validating each
   * based on previous input, and returns the result as a Calendar.
   * @return The user's input, converted to a Calendar.
   * @throws IllegalStateException if the number of tries allowed
   *         is exceeded on any single value.
   */
  public final Calendar getDate() {
    // note the changed call
    int yy = inRange(INT, "a year"19002050);

    // Determine if the year they entered is a leap-year
    boolean leapYear = yy % 4 != 0 ? false
      : yy % 100 != 0 ? true : yy % 400 == 0;

    int mm = inRange(INT, "a month"112);

    // Work out the length of the month they entered
    int monthLength;
    switch (mm) {
      case 2:
        monthLength == leapYear ? 29 : 28;
        break;
      case 4:
      case 6:
      case 9:
      case 11:
        monthLength == 30;
        break;
      default:
        monthLength == 31;
    }

    int dd = inRange(INT, "a day"1, monthLength);

    return new Gregorian''''''Calendar(yy, mm - 1, dd);
  }

  /**
   * Prompts for the answer to a question, and does not exit until
   * the user enters a valid reply, returning it as a boolean.
   * @param question The question to be used as the prompt.
   * @param yes The value that indicates yes/true.
   * @param no The value that indicates no/false.
   * @return The user's input, converted to a boolean.
   * @throws IllegalStateException if the number of tries allowed
   *         is exceeded.
   */
  public final boolean getBoolean(String question,
    String yes, String no)
  {
    String prompt = question + " " + prefix + "'"
      + yes + "' for yes, '" + no "' for no";
    TryCount count = new TryCount();

    while ( true ) {
      String input = attempt(count.increment(), prompt);
      if (input.equals(yes))
        return true;
      else if (input.equals(no))
        return false;
      else OUT.println("Input is not '"
         + yes + "' or '" + no "'");
    }
  }

  /**
   * Prompts for input and returns the user's input.
   * @param prompt The String to be used as a prompt.
   * @return The user's input, trimmed of all whitespace.
   */
  public final String input(String prompt)
  {
    OUT.println(prompt);
    return IN.nextLine().trim();
  }
}

Note that we've had to add quite a bit of extra logic to keep track of the number of attempts. This is because the value is independent of all the other things we're doing, which makes things a bit tricky. However, the "business" methods are still pretty much the same, apart from the fact that they are no longer static, and we don't have to supply a Scanner to them.

Note also:

  • TryCount is a non-static nested class, because it needs to be associated with a specific Input instance, so it can access its maxTries value. There are probably many ways we could have done this, but a mutable "count" object seemed to me the clearest. As you'll see later, there is another approach that is arguably even better.
  • The increment() method throws IllegalStateException when the number of tries is exceeded. An alternative (and probably better) approach is to define your own custom Exception; again, you'll see that in action later.
  • I've changed the internal loops back to the "do forever" (while (true)) style, because we have to increment the count each time we iterate.
  • I've added a description() method to our Type interface, so that error messages are more specific. We could have done this at any point in the development, but now seemed like an opportune time.

.

And now our calling class will look something like this:

import myutilities.Input;

public class myClass {
  ...

  public static void main(String[] args) {
    // Set up an Input object that uses System
    // streams and the default prefix, and allows
    // our user 5 tries to "get it right".
    Input input = new Input(5);

    int number = input.get(Input.INT, "a number");

    int month  = input.inRange(Input.INT, "a Month",
      1, 12);

    double temp = input.inRange(Input.DOUBLE,
      "today's temperature", -50.0, 130.0);

    Calendar date = input.getDate();

    ...
  }

As you see: very little different from before apart from the initial setup.

.

Phew. A lot of code. I bet you never thought you would be writing this much back when you started, but I go back to what I said right at the beginning of this tutorial:

User input is tough.

However:

  • It's all in one place, so it can be used any time you need an input value.
  • You never need to write it again.
  • (Very important) It's NOT in main() any more.
  • A lot of the bulk is documentation, which I strongly urge you to do as you write.
  • We've developed the class incrementally.

.

However, we're still not finished yet. There's no doubt that our input object is a lot more flexible than before, but that "try count" logic is pretty cumbersome - and kind of ugly.

UserInputPartIII shows you how to adapt what we've written into a proper input framework, but it does assume some knowledge of Java Generics. If you don't feel you're quite ready yet, you should still be able to use what you've read so far to write a decent utility class that will last you until you're ready for the "next stage of evolution".



CategoryWinston

JavaRanchContact us — Copyright © 1998-2014 Paul Wheaton