Module 3: Elementary Java, Part II


Functions: a first look

Consider a simple example: (source file )

public class uniform_random {
  // Basic Lehmer generator - constants
  static final long m = 2147483647L;
  static final long a = 48271L;
  static final long q = 44488L;
  static final long r = 3399L;

  // "Global" variable - the seed, set to some 
  // arbitrary non-zero value.
  static long r_seed = 12345678L; 

  // Basic Lehmer generator - uniform[0,1]
  // For more information see Knuth, Vol. II.
  public static double uniform ()
  {
    long hi = r_seed / q;
    long lo = r_seed - q * hi;
    long t = a * lo - r * hi;
    if (t > 0)
      r_seed = t;
    else
      r_seed = t + m;
    return ( (double) r_seed / (double) m );
  }

  public static void main (String[] argv)
  {
    // Let's test the generator. The mean should be 0.5.
    double sum = 0;
    for (int i=1; i<=10000; i++)
      sum += uniform ();

    System.out.println ("Average of 10000 samples: " + sum/10000);
  }

}

Note:



Let's modify the code so that the seed can be set via a method: ( source file )

public class uniform_random2 {
  // Basic Lehmer generator - constants
  static final long m = 2147483647L;
  static final long a = 48271L;
  static final long q = 44488L;
  static final long r = 3399L;

  static long r_seed = 12345678L; 

  // Set the seed to any given value.
  public static void set_seed (long value)
  {
    r_seed = value;
  }

  // Basic Lehmer generator - uniform[0,1]
  // For more information see Knuth, Vol. II.
  public static double uniform ()
  {
    long hi = r_seed / q;
    long lo = r_seed - q * hi;
    long t = a * lo - r * hi;
    if (t > 0)
      r_seed = t;
    else
      r_seed = t + m;
    return ( (double) r_seed / (double) m );
  }

  public static void main (String[] argv)
  {
    // Let's test the generator again. The mean should be 0.5.
    set_seed (13579);
    double sum = 0;
    for (int i=1; i<=10000; i++)
      sum += uniform ();

    System.out.println ("Average of 10000 samples: " + sum/10000);
  }

}

Note:

  • set_seed returns void.
  • A parameter is declared the same way as a variable.
  • The above parameter is a call-by-value parameter.

In-class exercise 3.1: Modify the above code to add a method that computes the area of a circle of given (parameter) radius. Then use the method to compute the mean area of a circle whose radius is uniformly distributed in the range (0,1). Thus, generate a number of circles randomly (according to the above distribution), compute the area of each and take the average. What is the theoretical answer?


Methods: passing parameters

We have already seen an example of passing parameters by value. What about call-by-reference?

Rule: all parameters in Java methods are passed by-value.
Thus, no "swap" method is possible in Java.

What happens if we try to implement "swap"? Let's see: ( source file )

public class swap {

  public static void swap (int a, int b)
  {
    int temp;
    temp = a;
    a = b;
    b = temp;
  }

  public static void main (String[] argv)
  {
    int i=5, j=6;
    swap (i,j);
    System.out.println ("i=" + i + "  j=" + j);
    // What gets printed out?
  }
}

Why didn't they provide call-by-reference in Java?

  • Since Strings, arrays and objects are actually implemented via pointers, a method can modify these types of parameters.
  • Allowing the pointers themselves to be modified would allow for bad code to destroy object references.
  • Thus, one option would have been to allow call-by-reference only with basic types such as integers and reals. The designers chose not to do this.



What do you do when you want a method to modify a variable?

  • If it's an object or an array, simply pass the object or array as a parameter.
  • If it's a basic type (int, double, etc):
    • If there's only one variable, have the method return value do the job:
    •        x = func (a,b,c);   // x gets modified
            
    • If you want a method to modify several variables, consider packaging the variables into an object.
      (We will learn how to do this later).


Methods: overloading

Suppose we want to add the following methods to our uniform_random generator:

  • A method that generates a uniform value in a given range.
  • A method that generates a uniform integer value in a given range.

One option is to define methods as follows:

public class uniform_random3 {
  // Basic Lehmer generator - constants
  static final long m = 2147483647L;
  static final long a = 48271L;
  static final long q = 44488L;
  static final long r = 3399L;

  static long r_seed = 12345678L; 

  // Basic Lehmer generator - uniform[0,1]
  // For more information see Knuth, Vol. II.
  public static double uniform ()
  {
    long hi = r_seed / q;
    long lo = r_seed - q * hi;
    long t = a * lo - r * hi;
    if (t > 0)
      r_seed = t;
    else
      r_seed = t + m;
    return ( (double) r_seed / (double) m );
  }

  // U[a,b] generator 
  public static double uniform_range (double a, double b)
  {
    // Actual code in here
  }

  // Discrete Uniform random generator - returns an
  // integer between a and b
  public static long discrete_uniform (long a, long b)
  {
    // Actual code in here
  }

  public static void main (String[] argv)
  {
    // Use the methods ...
  }

}

Java allows you to overload method names as long as the methods' signatures are unique.

Thus, instead of defining uniform_range and discrete_uniform above, we could simply use uniform: ( source file )

// We need to use the math library.

public class uniform_random4 {
  // Basic Lehmer generator - constants
  static final long m = 2147483647L;
  static final long a = 48271L;
  static final long q = 44488L;
  static final long r = 3399L;

  static long r_seed = 12345678L; 

  // Basic Lehmer generator - uniform[0,1]
  // For more information see Knuth, Vol. II.
  public static double uniform ()
  {
    long hi = r_seed / q;
    long lo = r_seed - q * hi;
    long t = a * lo - r * hi;
    if (t > 0)
      r_seed = t;
    else
      r_seed = t + m;
    return ( (double) r_seed / (double) m );
  }

  // U[a,b] generator 
  public static double uniform (double a, double b)
  {
    if (b > a)
      return ( a + (b-a) * uniform() );
    else { 
      System.out.println ("ERR in uniform(double,double):a="+a+"b="+b); 
      return 0;
    }
  }

  // Discrete Uniform random generator - returns an
  // integer between a and b
  public static long uniform (long a, long b)
  {
    if (b > a) {
      double x = uniform ();
      long c = ( a + (long) Math.floor((b-a+1)*x) );
      return c;
    }
    else if (a == b) 
      return a;
    else { 
      System.out.println ("ERR: in uniform(long,long):a="+a+"b="+b); 
      return 0;
    }
  }

  public static void main (String[] argv)
  {
    // Use the functions ...
    double x = uniform ();
    double y = uniform (2.718, 3.141);
    long i = uniform (7, 16);

    System.out.println ("x=" + x + " y=" + y + " i=" + i);
  }

}

Note:

  • Three functions are called uniform.
  • All three functions have a different signature:
    • One has signature uniform().
    • Another has signature uniform (double, double).
    • The third has signature uniform (long, long).

  • A function's signature is determined by:
    • the function name,
    • the number of arguments
    • the types of the arguments,
    • and the order of its arguments.

  • Return values do not matter in determining signature. Thus the following will not compile:
  • class uniform_random {
      public static double uniform (double a, double b)
      {
      }
    
      // This has the same signature.
      public static long uniform (double a, double b)
      {
      }
    }
           

  • Note: each parameter is declared separately in Java methods.
    The following does not compile:
  •          public static double uniform (double a, b)
           
  • For the first time (apart from System.out.println), we are using a library function:
    • The function is the floor() function in the java.lang.Math object of the java.lang library.
    • The library objects in in java.lang are the only library objects that do not need to be import'ed.
    • The statement
    •        import java.lang.*;
           

      even though not needed here, imports all classes in java.lang.
      (We will see what this means later).


    • Other libraries (packages) need to be import'ed.


Screen I/O

We have seen how to print stuff to the screen using System.out.print() or System.out.println().

For some undiscernible reason, Java does not provide any convenient methods for screen input.

  • The only thing you can do is read a whole line of input from the screen as a String.
    ( Even doing just that is complicated ).
  • To read int's or double's, one needs to parse the input String.
  • To read multiple int's or double's on a single line of input is really complicated.

We will now, step by step, create a bunch of methods that read input from the screen, which we can use later.

First, here's how we read a line of input from the screen: ( source file )

import java.io.*;

public class screen_io1 {

  public static void main (String[] argv)
  {
    // Put out a prompt.
    System.out.print ("Enter string: ");

    // We have to have a try clause because
    // the method readLine throws an exception.
    try {
      // These are the key steps in setting up the read operation.
      InputStreamReader isr = new InputStreamReader (System.in);
      LineNumberReader lr = new LineNumberReader (isr);

      // Now read the input line.
      String input_line = lr.readLine ();

      // Echo it.
      System.out.println ("Echo: " + input_line);
    }
    catch (IOException e) {
      // If there was a problem...
      System.out.println (e);
    }
  }
}

Next, to read an integer (all by itself on a line), we need toparse the input line: ( source file )

import java.io.*;

public class screen_io2 {

  public static void main (String[] argv)
  {
    System.out.print ("Enter an integer: ");

    try {
      InputStreamReader isr = new InputStreamReader (System.in);
      LineNumberReader lr = new LineNumberReader (isr);

      String input_line = lr.readLine ();

      // Parse for an integer.
      int i = 0;
      try {
        i = Integer.parseInt (input_line);
      }
      catch (NumberFormatException e) {
        System.out.println ("Error in input");
        System.exit (0);
      }

      // Echo it.
      System.out.println ("The integer you entered: " + i);
    }
    catch (IOException e) {
      System.out.println (e);
    }
  }
}

Note:

  • Integer is a Java class with some methods useful in handling int's.
  • Integer is actually in the library java.lang.* which is the only library that does not need an import.
  • parseInt is very unforgiving. Blanks on either side of the integer are unacceptable.
  • Similar to Integer, there are Byte, Short, Long, Float and Double objects with parsing methods.
  • The syntax new BufferedReader (System.in) is strange. It is our first look at a dynamic object and the new operator.
    ( We will understand this better when we cover objects. )
  • We have used a
  •     try {
          //... some code
        }
        catch () {
          //... some other code
        }
      

    statement. In these cases, we are forced to by the compiler since the methods we call throw exceptions.

  • Finally, we have used the import statement to access library objects in the package java.io
  •  import java.io.*;
     

Let us put this code into useful methods: ( source file )

import java.io.*;

public class screen_io3 {

  // Read in a string after putting out a prompt.

  public static String read_string (String prompt)
  {
    OutputStreamWriter osw = new OutputStreamWriter (System.out);
    BufferedWriter bw = new BufferedWriter (osw);
    try {
      bw.write (prompt);
      bw.flush ();        // Need to flush buffer to ensure output
    }
    catch (IOException e) {
      System.out.println ("new_io::read_string_prompt1: cannot write\n");
      System.exit (1);
      return "";
    }
    InputStreamReader isr = new InputStreamReader (System.in);
    LineNumberReader lr = new LineNumberReader (isr);
    try {
      String s = lr.readLine ();
      return s;
    }
    catch (IOException e) {
      System.out.println ("new_io::read_string_prompt1: cannot read\n");
      System.exit (1);
      return "";
    }
  }


  // Read in an int.

  public static int read_int (String prompt)
  {
    String s = read_string (prompt);
    while (true) {
      try {
        int i = Integer.parseInt (s);
        return i;
      }
      catch (NumberFormatException e) {
        s = read_string (prompt);
      }
    }
  }

  public static void main (String[] argv)
  {
    // Example:
    int i = read_int ("Enter an int: ");
    System.out.println ("What you entered: " + i);
  }
}


Screen I/O: formatting

  • It would be logical to cover output formatting at this time (for example, how to fix the number of decimal places).
  • However, formatting is easier understood after covering objects, so we will defer discussing formatting until after we've covered objects.


File I/O: reading from a file

The Java approach to I/O in general is similar to that of C++:

  • Each source of information is treated as in input stream.
  • An input stream can be a file, the screen or something else, such as a program that produces data.
  • Input streams can consist of ascii text data, raw bytes or some other encoding.
  • Java provides an enormous variety of library objects to deal with different input streams.
    • Some of these depend on the type of data ( byte vs. text).
    • Others depend on the way in which reading is done ( with or without buffering, for example).

Since a file is just an input stream, reading from a file is similar to reading from the screen, provided the file is identified as an input stream.

The following example shows how to read a text file line-by-line: ( source file )

import java.io.*;

public class file_io1 {

  public static void main (String[] argv)
  {
    try {
      // These are the key steps in setting up the read operation.
      FileReader fr = new FileReader ("testdata");
      LineNumberReader lr = new LineNumberReader (fr);

      // Now read the input lines
      boolean over = false;         
      int i = 1;
      do {
        // Get a line from the file.
        String input_line = lr.readLine ();
        if (input_line != null) {
          System.out.println ("Line " + i + ": " + input_line);
          i++;
        }
        else
          over = true;
      } while (! over);

      // Done.
      lr.close();
    }
    catch (IOException e) {
      // If there was a problem...
      System.out.println (e);
    }
    
  }
}

Note:

  • An exception will occur if the file is not found.
  • readLine() returns a null if EOF is reached.
  • The stream is closed after reading is complete.
  • The above code works for text files. A different set of library objects is used for binary data.

Here's the same example with a (simpler) while-loop: (source file )


  public static void main (String[] argv)
  {
    try {
      FileReader fr = new FileReader ("testdata");
      LineNumberReader lr = new LineNumberReader (fr);

      // Using a while loop ...
      String input_line = lr.readLine ();
      int i = 0;
      while (input_line != null) {
        i ++;
        System.out.println ("Line " + i + ": " + input_line);
	input_line = lr.readLine ();
      }

      // Done.
      lr.close();
    }
    catch (IOException e) {
      // If there was a problem...
      System.out.println (e);
    }
    
  }


File I/O: writing to a file

Similar to reading, writing is an output stream activity. By defining a file as an output stream, you can write to a file: ( source file )

import java.io.*;

public class file_io2 {

  public static void main (String[] argv)
  {
    try {
      // Need to associate a file with a PrintWriter.
      // The last parameter is set to "true" to indicate
      // auto-flush should be activated.
      FileWriter fr = new FileWriter ("testdata2");
      PrintWriter pw = new PrintWriter (fr, true);

      // Now we're ready for writing.
      pw.println ("Hello");
      pw.println ("Hello again");

      // Done.
      pw.close();
    }
    catch (IOException e) {
      System.out.println (e);
    }
    
  }
}

Note:

  • The stream is closed after writing is complete.
  • PrintWriter is used for text files.


File I/O: reading and writing to the same file

Reading and writing to the same file can be done in many ways. We will consider the following simple task:

  • Read the file into memory.
  • Change its contents.
  • Write the new contents back to the same file.

Example: ( source file )

import java.io.*;

public class file_io3 {

  public static void main (String[] argv)
  {
    String filename = "testdata3";
    int n_lines = 0;
    String[] line_buffer = null;

    // First, we read in the file to get the size.
    try {
      FileReader fr = new FileReader (filename);
      LineNumberReader lr = new LineNumberReader (fr);

      boolean over = false;         
      n_lines = 0;
      do {
        String input_line = lr.readLine ();
        if (input_line != null)
          n_lines ++;
        else
          over = true;
      } while (! over);

      lr.close();
    }
    catch (IOException e) {
      System.out.println (e);
    }

    // We have now counted the number of lines and
    // we will read the file into a buffer.
    try {
      FileReader fr = new FileReader (filename);
      LineNumberReader lr = new LineNumberReader (fr);

      // Allocate necessary space.
      line_buffer = new String[n_lines];

      int i = -1;
      boolean over = false;
      do {
        String s = lr.readLine ();
        if (s == null) 
          over = true;
        else 
           line_buffer [++i] = s;
      } while (! over);

      lr.close();
    }
    catch (IOException e) {
      System.out.println (e);
    }
    
    // Next, write the buffer out to the same file
    // with line numbers added.
    try {
      // Open the file for writing
      FileWriter fr = new FileWriter (filename);
      PrintWriter pw = new PrintWriter (fr, true);

      // Write from buffer with line numbers:
      for (int i=0; i < n_lines; i++) 
        pw.println ("Line " + (i+1) + ": " + line_buffer[i]);

      pw.close();
    }
    catch (IOException e) {
      System.out.println (e);
    }

  }
}


File I/O: Appending to a file

To append to a file, simply use the appropriate arguments to FileWriter: include "true" to indicate appending.

Example: ( source file )

import java.io.*;
import java.io.*;

public class file_io_append {

  public static void main (String[] argv)
  {
    try {
      // Using "true" as a second argument indicates "append"
      // in FileWriter.
      FileWriter fr = new FileWriter ("testdata_append", true);
      PrintWriter pw = new PrintWriter (fr, true);

      // Now we're ready for writing.
      pw.println ("Hello");
      pw.println ("Hello again");

      // Done.
      pw.close();
    }
    catch (IOException e) {
      System.out.println (e);
    }
    
  }
}


Simple input parsing

Consider an input file that looks like this:

Circle:
  center.x=50
  center.y=60
  radius=20
Circle:
  center.x=320
  center.y=180
  radius=40
Rectangle: 
  topleft.x=50
  topleft.y=400
  bottomright.x=500
  bottomright.y=40

What we would like to do is to parse the input to extract the numbers.

  • Reading multiple numbers (or even one number following a text string) is difficult in Java.
  • Accordingly, let us develop a useful method to read a file in the above format -- a type of `properties' file.
  • The library provides a StringTokenizer object that performs rudimentary lexical analysis.
  • We will write a method to parse a single `property' string like "radius=9.5".

Here is how it can be done ( source file ):

// We need to import the StringTokenizer object in
// the java.util package:

import java.util.*;

public class file_io4 {

  // Parameters:
  //   in_string: the property string (e.g. "radius=0.9")
  //   property:  the property name (e.g., "radius")
  // The value is assumed to be a real number.

  public static double read_property (String in_string, String property)
  {
    // Obtain a copy of the StringTokenizer object.
    StringTokenizer st = new StringTokenizer (in_string);

    // Get the first token, specifying the delimiter.
    String first_part = st.nextToken ("=");

    // Trim whitespace on either side - a String method.
    first_part = first_part.trim ();

    System.out.println ("First part: " + first_part);

    if (!first_part.equals (property)){
      System.out.println ("Improper string: " + in_string);
      System.exit (0);
    }

    // Get the next token, now allowing for end-of-line.
    String second_part = st.nextToken (" =\t\n\r");

    // Trim whitespace.
    second_part = second_part.trim();

    System.out.println ("Second part: " + second_part);

    // Read the number in the second part using the library's
    // Double object.
    try {
      double d = Double.parseDouble (second_part);
      return d;
    }
    catch (NumberFormatException e) {
      System.out.println ("Second part not a number: " + second_part);
      System.exit (0);
    }
    return 0;
  }

  public static void main (String[] argv)
  {
    // Test
    double d = read_property ("x=5", "x");
    System.out.println (d);

    d = read_property ("center.x=5", "center.x");
    System.out.println (d);

    d = read_property ("center.x=5   ", "center.x");
    System.out.println (d);

    d = read_property (" center.x = 5   ", "center.x");
    System.out.println (d);
  
  }
}

Note:

  • We have used that strange new operator again to create a StringTokenizer.
  • The string to be parsed is fed into this object in the new statement.
  • The object so created has a nextToken() method that returns tokens, one at a time.
  • A String object has a trim() method that trims off whitespace, returning the result as a String.
  • Once we have a real number in a String, use the parseDouble in Double to extract the value.
  • Finally, we have import'ed a library object (StringTokenizer by importing the package (java.util) containing it:
  •  import java.util.*;
     

In-class exercise 3.2: Use the above method, and the methods for reading to a file for the following task. Suppose we want to extract information from a file containing a description of a circle. For example (source file):

Circle:
  center.x=50
  center.y=60
  radius=20
You are to prompt for the file name, read the file, extract the information and print out the parameters of the circle, and its area. Hints:
  • Start by putting together the following methods:
    • read_string (String prompt) from screen_io3.java seen earlier.
    • read_property (String in_string, String property) from the above code.
  • In your main method, write code to input the filename using the example in screen_io1.java above. Write it back to the screen to test that it's working.
  • Use the file_io1.java example earlier to read the file one line at a time. Write each line back to the screen to test that this part is working.
  • Use the code in file_io4.java example earlier to extract the property information.


Encapsulation: single file

Since the above methods of reading an integer etc are useful, let's a create a program unit or module to contain these methods.

We will rewrite the file as follows in the file read_geodata2.java

import java.io.*;
import java.util.*;

class useful_io {

  // Read a string from the screen.

  static String read_string (String prompt)
  {
     // Code not shown
  }


  // Read an int from the screen.

  public static int read_int (String prompt)
  {
     // Code not shown    
  }


  // Read a double from the screen.

  public static double read_double (String prompt)
  {
     // Code not shown
  }


  // Parse a string for a property.

  public static double read_property (String in_string, String property)
  {
     // Code not shown
  }

} // End of class useful_io

public class read_geodata2 {

  public static void main (String[] argv)
  {
    // Get file name from user
    file name = useful_io.read_string ("Enter filename: ");

    // ... rest of code not shown.
  }

}

Note:

  • The useful methods are placed in a class called useful_io.
  • The class useful_io is not a public class.
  • To access methods in useful_io from another class, the class name is used (useful_io) along with the method name, with a period, as in:
  •     file name = useful_io.read_string ("Enter filename: ");
           

  • Both class'es are in the same text file (read_geodata2.java).
  • Only one class in a file can be public:
    • This class must have a main method.
    • Execution starts in this main method.
    • This class (read_geodata2) must have the same name as the file (read_geodata2.java) without the .java extension.

  • Each class is compiled to a separate object file:
    • On compiling read_geodata2.java, you get two files: read_geodata2.class and useful_io.class.

Why is this form of encapsulation a good thing?

  • It logically organizes the code into readable/manegeable units.
  • Data common to an encapsulated unit can be shielded from the outside.
    (Here we didn't have any data).
  • Units can be tested in isolation.
  • Units can be re-used.


Encapsulation: multiple files

To make a collection of methods truly useful, the encapsulation should be placed in a separate file.

We will place our I/O methods in the file useful_io.java :

import java.io.*;
import java.util.*;

public class useful_io {

  // Read a string from the screen.

  public static String read_string (String prompt)
  {
     // Code not shown
  }


  // Read an int from the screen.

  public static int read_int (String prompt)
  {
     // Code not shown    
  }


  // Read a double from the screen.

  public static double read_double (String prompt)
  {
     // Code not shown
  }


  // Parse a string for a property.

  public static double read_property (String in_string, String property)
  {
     // Code not shown
  }

} // End of class useful_io

Next, these methods will be used in the file read_geodata3.java:

import java.io.*;

public class read_geodata3 {

  public static void main (String[] argv)
  {
    // Get file name
    String filename = useful_io.read_string ("Enter filename: ");

    // Rest of code not shown

  }
} // End of read_geodata3.java

Note:

  • The file useful_io.java is currently in the same directory as read_geodata3.java -- this need not be the case.
  • Try the following:
    • Create a sub-directory called temp.
    • Place the file useful_io.java in temp.
    • Compile geo_data3.java.

    It compiles!

  • The compiler will search sub-directories of the current directory to look for useful_io.java.


Packages

To help programmers and programming teams manage files, Java provides the ability to package related code.

Consider the following example:

  • Create the file read_geodata4.java as follows:
  • import java.io.*;
    
    import useful_io_pack.*;  // Imports the entire package useful_io_pack
    
    public class read_geodata4 {
    
      public static void main (String[] argv)
      {
        // Get file name
        String filename = useful_io_subunit.read_string ("Enter filename: ");
    
        // Rest of code not shown.
      }
    }
      
  • Next, create the subdirectory useful_io_pack:
  •     % mkdir useful_io_pack
      
  • In this subdirectory, place the file useful_io_subunit.java:
  • package useful_io_pack;  // Must be the first statement in the file.
    
    import java.io.*;
    import java.util.*;
    
    public class useful_io_subunit {
    
      // Read a string from the screen.
      static String read_string (String prompt)
      {
         // Code not shown
      }
    
      // Parse a string for a property.
      public static double read_property (String in_string, String property)
      {
         // Code not shown
      }
    
    } // End of class useful_io_subunit
      
  • Now compile read_geodata4.java.

What is going on?

  • The compiler sees the import useful_io_pack in read_geodata4.java and looks for the package.
  • A package is assumed to be available in some subdirectory off of the CLASSPATH.
  • Finding the subdirectory useful_io_pack, the compiler looks for class'es inside and finds useful_io_subunit.
  • Currently, our CLASSPATH variable is defined as (e.g,, Unix tcsh):
  •               setenv CLASSPATH /usr/java/lib:.
                

    Thus, the library is searched first, then the current directory.

  • You set your CLASSPATH to any list of directories that you want.
  • Packages can have sub-packages, as long as the sub-packages are found in similarly-named subdirectories of the main package.