Academic programs

From Juneday education
Jump to: navigation, search

Introduction

With "academic" programs, we mean programs often used in academia lectures and books teaching Java, which show some simple examples without any obvious point, resulting in weird programs which are very hard to understand for beginning students.

A typical academic program is described like this:

A program which creates three Book objects and prints them to standard out'

In such an academic programs, the students are encouraged to write a class Book representing books in a system, and create an application where the main method is used to create three Book objects, and passing the references to the objects to standard out using System.out.println().

After this honorable task, the program simply dies.

Another typical feature of academic programs, is programs which always do the very same thing (like the example above). The program operates on known, often hard coded, data, and always performs the same.

The problems with such programs are that students, in our experience, tend to become very confused with them. Some students have a very hard time to grasp the concept of a program which always does the same thing, and a seemingly pointless thing and then just dies.

Some examples include programs which creates a data structure, like an array, checks the size of the data structure (created on the line above) and prints the size to standard out. Some times, such programs also include the luxury of manipulating the recently created data structure and then printing it again to standard out.

Another problem with these kinds of programs is that it teaches a poor style of programming. Since the limitations and internal workings of the data structure is known, there is rarely any good reason to validate the limitations of the data before using it.

Some examples:

// Create an array of three ints, and print out the third one:
int[] ints = {1,2,3};
System.out.println(ints[2]);

While this clearly shows the syntax for accessing the third element of the array, it avoids sanity checks before using the array. Of course, we should make sure that an array reference isn't null before trying to dereference it. And of course, we should make sure that there is an index 2 (that is, that the array is at least of length 3).

Another confusing fact about the above short program snippet, is that the students might get the idea that all data structures in programs are created literally by hand, and often shortly before they are used.

In real programs, of course, we pass references to objects around as arguments to methods. That means, we have no idea what data comes with a reference of, say an array. We don't even know if the reference references an actual object or null. And we have no idea how long the array is, if it has any elements at all.

One source of confusion to add to the list, could also be that in the beginning, the students write all of their code themselves. Since they write both the client code (the code calling some methods) and the API code (the class with methods), they are pretty sure what data they are passing to methods they write themselves. In real world, however, it is common that a different programmer (or even team or even organization) writes the API code, and the programmer simply uses it. In such a scenario, it is clear that the authors of the API code cannot anticipate what data will be sent to their methods, so they have better make sure the input is sane (check for null, check for valid values, check length of arrays etc).

To make the list of potential problems with academic programs even longer, we'd like to add some words of warning regarding the dangers of "known input".

If the program always is fed the same data, there is less motivation for the student to write generic code which would be usable in more than one project or application.

We've seen many weird examples (see for instance How many 'i' are there in 'Liverpool') where the students are encouraged to solve a very specific problem in an all-but-generic fashion. The problem often consists of these parts in an academic program:

  • Write a program (a standalone application where all work is done in the main method)
  • The program should operate on a known, hardcoded dataset
  • The problem to solve involves some other known, hardcoded data combined with the hardcoded dataset

It can be something as simple as: Write a program which compares the String "ABBA" to the String "BABA" lexicographically. The program should output which String comes first lexicographically.'

The problems with such a programs are manyfold. First of all, the students already know that "ABBA" comes before "BABA" lexicographically. They might wonder why we'd need a computer and a computer program to confirm this. Second, the students will focus their attention to the logic of knowing which string to output, knowing all along which string it is. Third, they might get the idea that comparing strings is best done in pairs of strings - what about finding the "smallest" string according to some comparison criteria, from any number of strings? Third, they will hard code both the string "ABBA" and the String "BABA", so they will feel less compelled to check the strings for null references etc.

When introducing classes, constructors and objects, students are often encourage to write a small program which creates an object from one of their own classes, manipulates the state of the object and prints a string representation of the object to standard out before and after the manipulation. Here's what such a weird and rather pointless could look like:

public class Car{
  private String color;
  private String make;
  public Car(String color, String make){
    this.color = color;
    this.make  = make;
  }
  public void setColor(String newColor){
    this.color = newColor;
  }
  public void setMake(String newMake){
    this.make  = newMake;
  }
  public String  make(){ return make;  }
  public String color(){ return color; }
  @Override
  public String toString(){
    return "A " + color + " " + make;
  }
}
class TestCar{
  public static void main(String[] args){
    Car aCar = new Car("Blue", "Volvo Amazon");
    System.out.println(aCar);
    aCar.setColor("Red");
    System.out.println(aCar);
  }
}
/* Test-run:
$ javac Car.java && java TestCar
A Blue Volvo Amazon
A Red Volvo Amazon
*/

Now, this small and seemingly innocent program contains many of the above problems. We won't go into the unmotivated existence of mutator methods for make and color here, we've written about that elsewhere.

The program, which consists of the main method with its four statements, creates a Car object and saves a reference to it in a reference variable aCar. On the next line, the Car is printed. While this works excellently for showing the students the implicit call to toString() and introduce the concept of inheriting methods from Object, it might seem odd to some students. What's the point of the program? What is a Car object, and what is it used for? Looking at the code, some students will conclude that an object of type Car is mainly a holder for two strings, one with a text representing a color and one with a text representing a car make. Couldn't we then just as well have named the class "String pair" or "color-and-make-texts"?

On the third line, we immediately change our mind about the Volvo and decide that it is in fact not blue, but red. Is this a common task for a programmer? some students might ask themselves - I might as well then take a job at the car paint shop... Anyway, just after we've changed our mind about the color of the Volvo, we print the car out again, so that we can see the result of this quick change of mind when we run the program.

We believe that it isn't exclusively harmful to write small tests like this for our classes. Of course we should encourage our students to test their code and classes. But we think it is important also to give the students realistic expectations on what the business of programming is about. The above example program should be carefully introduced, packaged and presented to the students with some fake background story motivating the existance of the Car class as well as the test.

We recommend that you name the test class such that it conveys its purpose - to test classes and methods. TestCar isn't all that bad. How do we motivate the existence of the Car class then?

It could be a simple background story such as "You are writing a small application for a car paint shop. The application should keep track of paint jobs to be executed by the paint shop company, so you need a class to represent cars in the TODO-list of the company. Since cars will be painted, the objects representing cars must have the ability to mutate their color state..." etc...

Next, you should re-use the Car class for later exercises where you introduce for instance methods. Challenge the students to write a method which processes a list of cars to be painted in some color provided as an argument etc.

Challenge the students to instead of hard coding the test case car, using arguments to the test program with the value for make and color. That will force them to think about how to use String references of unknown contents, how to parse and use arguments etc. It also allows the students to write a small script running the test program repeatedly with different arguments, investigating the result etc. As an extra challenge, have the students write a small script which generates a number (given as argument to the script) of cars of a randomly selected make and color, and call the program using the script. This small and simplistic program can be expanded into quite a test suite using a small and simple bash script, for instance.

We believe it is very important to get the students used to seeing code which creates objects using parameters (or arguments) rather than hard coded values. Because this is what they will write and see in the real life outside academia. Dealing with unknown values also fosters the students to validate data and input before making assumptions about it.

Another typical example of academic programs is a program which should search an array and print out the smallest int value from the array.

While it is an interesting exercise in thinking about problem solving (how to find the smallest int in an int array), unfortunately this is often what the program looks something like this:

public class SmallestInt{
  public static void main(String[] args){
    int[] ints = {234,23,23,565,3,-12,-9, 0, 234, 3243};
    int smallest=ints[0];
    for(int i = 1; i<ints.length; i++){
      int thisNumber = ints[i];
      if(thisNumber < smallest){
        smallest=thisNumber;
      }
    }
    System.out.printf("The smallest value was %d\n", smallest);
  }
}

When the students are faced with a program like the above, some of them are bound to think: "What if I want to search a different list of numbers? Do I have to write a new program? Change the program and recompile?"

As you probably have guessed by now, our complaints about the above program lies mainly in the fact that it isn't a generic program for solving a general problem. We have managed to write a program which solves a problem on a known set of input data.

If you think about it, most students will find even this simple program confusing, because they know that -12 is the smallest integer in the list of numbers to search.

Suggested improvements over the above assignment would include:

Write a program which reads numbers from the standard in stream continuously and when EOT is reached, prints out the smallest number. This allows for extensive testing using pipes and redirection. It will force the students to think about what could go wrong with the input such as what if there are no numbers input to the program, what to print then? What if some lines of input can't be interpreted as integers? How do you transform the strategy from the above program to an unknown number of numbers read?

Another challenge would be to have the students write a method accepting an array of ints of unknown size and return the smallest number from the method. What number should be returned when the method is given a null reference? An empty array? How should the method communicate that it couldn't find any smallest number?

Appendix

A more complete program for find the smallest number

Here's some example snippets showing alternative versions of the "find the smallest number" problem.

Footnote: This doesn't work in an IDE, because of the System.console() trick to discover whether a pipe or redirect is present. In an IDE (actually also in cygwin under windows) there is no System.console(). In the case of an IDE, there is probably no pipe or redirect ever present either, so...

It works under CMD in windows and on GNU/Linux in a terminal running bash.

import java.util.*;

public class Smallest{
  public static void main(String[] args){
    if(System.console()==null){ // No console, probably a pipe
      System.exit(readNumbers());
    }else if(System.console()!=null && args.length==0){
      // a console found, and no arguments
      System.exit(readInteractively());
    }else if(args.length > 0){
      // Arguments, use them!
      System.exit(readArgs(args));
    }else{
      System.out.println("No data");
      System.exit(1);
    }
  }
  static int printResult(List<Integer> numbersRead){
    if(numbersRead.size()!=0){
      Collections.sort(numbersRead);
      System.out.println("The smallest number was: " + numbersRead.get(0));
      return 0;
    }else{
      System.err.println("No valid input found");
      return 2;
    }
  }
  static int readInteractively(){
    ArrayList<Integer>numbersRead=new ArrayList<>();
    Scanner sc = new Scanner(System.in);
    String line=null;
    int num;
    try{
      while( (line=sc.nextLine())!=null){
        try{
          num=Integer.parseInt(line);
          numbersRead.add(num);
        }catch(NumberFormatException e){
          System.err.println("Ignoring bad input: " + line);          
        }
      }
    }catch(NoSuchElementException|
           NullPointerException ignore){
    }
    return printResult(numbersRead);
  }
  static int readArgs(String[] args){
    System.err.println("args...");
    ArrayList<Integer>numbersRead=new ArrayList<>();
    int num;
    for(String s : args){
      try{
        num=Integer.parseInt(s);
        numbersRead.add(num);
      }catch(NumberFormatException|NullPointerException e){
        System.err.println("Ignoring bad input" + s);        
      }
    }
    return printResult(numbersRead);
  }
  static int readNumbers(){
    String line=null;
    int num;
    ArrayList<Integer>numbersRead=new ArrayList<>();
    Scanner sc = new Scanner(System.in); 
    try{
      while( (line=sc.nextLine())!=null){
        try{
          num=Integer.parseInt(line);
          numbersRead.add(num);
        }catch(NumberFormatException nfe){
          System.err.println("Ingoring bad input: " + line);
        }
      }
    }catch(NoSuchElementException|
           NullPointerException ignore){
    }
    return printResult(numbersRead);
  }
}

Example runs:

# Interactive:
$ javac Smallest.java && java Smallest
asdf
Ignoring bad input: asdf
234
s
Ignoring bad input: s
3423
3
3423
34
56567
65
The smallest number was: 3
# Using a pipe:
$ javac Smallest.java && echo 123 2131 12312 3 0432 -324 sdf 1123- 12312|tr ' ' '\n'|java Smallest
Ingoring bad input: sdf
Ingoring bad input: 1123-
The smallest number was: -324
# Reading from file:
$ echo 123 2131 12312 3 0432 -324 sdf 1123 asdf 12312 -9999|tr ' ' '\n' > input.txt
$ javac Smallest.java && java Smallest < input.txt 
Ingoring bad input: sdf
Ingoring bad input: asdf
The smallest number was: -9999