← APE preparation CS Skill Tree

General program design

Three jobs sit under this one heading: moving data in and out of a file, deciding what your code does when something goes wrong, and putting values in order so you can find them again. Each one is a place where code that compiles and runs can still be wrong, and each one comes down to a small number of rules that decide whether it is.

Open, read, write, close

A file is a name and some bytes on a disk, and your program cannot reach either one directly. It asks the operating system for a connection to that name, and what comes back is an object. A Scanner if you are going to read. A PrintStream if you are going to write. Every read and every write from then on goes through that object, and when you are finished you hand the connection back.

your code
  <-> Scanner or PrintStream
      <-> the operating system
          <-> the bytes on disk

Four verbs, in that order, every time. Open asks for the connection, and it is the step that can fail, because the name you were given may not be a file that exists. Read or write moves data through the connection. Close gives it back.

Closing is not tidying up after yourself, and there are 2 separate reasons for that.

The first is buffering. When you call println, the stream is allowed to keep your text in memory and write it to the disk later, in one larger batch, because a disk is slow and one big write costs far less than a hundred small ones. Until close or flush runs, nothing promises the bytes reached the file. A program that writes 3 lines and exits without closing can leave a file holding none of them, and it will not report an error, because from the program's point of view nothing failed.

The second is the connection itself. The operating system handed it out and is keeping track of it. There is a limit on how many it will hand out at once, and a program that opens files and never closes them runs into that limit. Closing returns the one you borrowed.

That is also why setting the field to null is not a substitute for closing. Assigning null makes the connection unreachable from your code, which is not the same as ending it. The buffer is still unflushed and the operating system still counts the file as open. Close it first, then null the field.

Errors, and the second way out of a method

Every method you have written so far has one way out. It reaches a return, or it runs off the end of its body, and control goes back to whoever called it. An exception is a second way out. When a method hits something it cannot deal with, it stops where it stands, builds an object that describes what happened, and throws it. That object travels back up the chain of callers, one at a time, until a caller catches it. If nobody catches it, the program stops and prints the chain of calls it was in the middle of.

main
  calls openInputFile
    calls new Scanner(file)
      throws FileNotFoundException

openInputFile catches it here,
so main never sees it

Java sorts exceptions into 2 groups and treats them differently before your program ever runs. A checked exception is one the compiler will not let you ignore: you either catch it or you declare that your method throws it, and a method that does neither does not compile. IOException and FileNotFoundException are checked. An unchecked exception is one the compiler says nothing about. IllegalArgumentException and NullPointerException are unchecked.

The split is about whose fault the situation is. A file that is not on the disk is a normal fact of the world, and no amount of careful coding prevents it, so Java forces you to have an answer ready. A null arriving where a name was expected is a bug in the calling code, and the answer is to fix the caller, so the compiler stays out of it.

That same question decides whether you throw or catch. You throw when the caller handed you something you cannot work with and only the caller can fix. You catch when the thing that went wrong is a condition your program is expected to survive, and you know what to do about it.

Order, and finding things again

Sorted means one promise about the whole collection: walk it from the front, and no value ever comes before a value that should have been earlier. Nothing enforces that promise for you. It holds because every method that changes the collection keeps it, and it stops holding the first time one of them does not.

There are 2 ways to end up with a sorted collection. Put everything in and sort it afterward, or never let it get out of order in the first place. A sort walks the collection repeatedly, moving values until the promise holds. addOrdered takes the other route: it finds the one spot a new value belongs in and puts it there, so the promise is never broken and never has to be restored.

Searching is what the order buys you. A search that looks at every value from the front until it finds a match works on anything, sorted or not. A search that halves the remaining range each time, the way you look up a word in a dictionary, is much faster, and it needs 2 things the walk does not: the values in order, and the ability to jump straight to the middle one. An array gives you both. A chain of nodes gives you only the first, because reaching the middle node means walking to it.

What the official site names, and what "grossly inefficient" points at

The department's own words for this area:

Penalty points can be assessed in this category for grossly inefficient code

File input and output

Exception handling

Order-related manipulations, such as addOrdered, sort, and search

Official site: FAQs About the APE, item 15.

Read the first line the way it is written. Grossly inefficient, not merely slow. Nobody is counting instructions. The word points at work a program does over and over for no reason, and it has a small number of recognizable shapes.

  • Walking the whole collection inside a loop that is already walking the whole collection, when one walk would have done.
  • Re-sorting after every single insert instead of inserting into the right place.
  • Building a long string by adding to it inside a loop, which copies everything built so far on every pass.
  • Asking a question you already know the answer to, such as walking the chain to count it when the class keeps a size field that is already correct.
  • Reading the same file twice because the first pass threw away something it had.

After you write a method, count how many times it touches each value, and compare that against what the job actually requires. If it is doing more, find the pass you can delete.

The lessons

Trace these before you go on

Work each one on paper and write the answer down before you open it.

TraceWhat is in the file

A program opens out.txt, writes the 3 words alpha, beta and gamma with println, and then reaches the end of main without closing.

  1. What is guaranteed to be in out.txt?
  2. Which single call would make all 3 lines certain to be there?
  3. The program is changed to set the PrintStream field to null instead of closing. What changes?
Answers

1. Nothing is guaranteed. The stream is allowed to hold all 3 lines in a buffer. It may happen to have written them, and that is not something to rely on.

2. close(), which flushes what is buffered and then ends the connection. flush() alone would also put the bytes on disk, and would leave the connection open.

3. Nothing improves. The buffer is still unwritten and the operating system still counts the file as open. The only difference is that your code can no longer reach the stream to close it.

TraceChecked or unchecked, throw or catch

For each one, say which group the exception is in and whether the method that notices the problem should throw or catch.

  1. A method is handed null where it expected a file name.
  2. A method is handed the name of a file that is not on the disk.
  3. new Scanner(someFile) is called and the file disappears between the check and the call.
Answers

1. Unchecked. The method throws an IllegalArgumentException, because a null name is a bug in the caller and there is nothing sensible the method can do with it.

2. This one does not have to involve an exception at all. The method can ask the File whether it exists, record why it is not opening, and return. A missing file is a condition to survive, not a bug to report.

3. Checked. The Scanner constructor declares FileNotFoundException, so the compiler requires a catch around it or a throws on your method header. Catching it is what lets the method record a message and return instead of stopping the program.

TraceCount the passes

A list holds 100 values. For each version, say how many times a value is looked at, and which one is doing work it does not need to do.

  1. Version A walks the list once and keeps a running largest.
  2. Version B walks the list once, and for every value walks the whole list again to check whether anything is bigger.
  3. Version C sorts the list and reads the last value.
Answers

1. 100 looks. One pass, one comparison per value, and the answer is right at the end.

2. 100 times 100, so 10000 looks, for an answer version A already had. This is the shape the penalty sentence is aimed at: a second walk inside the first one, doing work the first walk could have done.

3. Far more than 100, and the list is left in a different order than the caller gave you. Sorting to find one value pays for an ordering nobody asked for.

In the practice APE

The Practice APE Exam Instructions put 7 problems under this heading. 6 of them are file methods, split across 2 folders: openInputFile and openOutputFile in one, then read, writeToOutputFile, closeInputFile and closeOutputFile in the other. The last one is a sort method on a linked list that keeps a sentinel node at the front. Each one is worked inside the lesson it belongs to.

Exception handling has no problem of its own there, which does not mean it is absent. It runs through all 6 file methods: every one of them decides what to do about a name that is null, a file that is missing, or a stream that is not open, and those decisions are most of what the problems are asking for.

The one thing to keep

Open, read or write, close, and close for real, because the bytes may still be in a buffer and the connection is borrowed. Throw when the caller gave you something you cannot use, catch when the world gave you a condition you can survive, and let the compiler tell you which exceptions you are not allowed to ignore. Keep a collection in order as values arrive rather than repairing it afterward, and before you call a method finished, count how many times it looks at each value.