Section 11f

File Input with Priming and Sentinels


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Getting All The Data From The File

Due to its fairly low-level approach to data management, Java does not do a very good job of managing data input from files. However, you can make your file operations work just fine. There are three strategies for this. Each has its own usage and benefits.

One Way: Using a Sentinel

You can use sentinel values to identify the ends of each line in your text files, or to indicate the end of a text file. For example, if you had a file with some rows of numbers that had to be uploaded, you could output some unusual value (i.e., a value that would not be expected to be used in that particular data set, called a sentinel) at the end of your data, and before the last endline so that when the data is reacquired, the input process will know when the data from each line has been uploaded. Your original output process might look like the following.

// within the class initialization (global to class)
private final int SENTINEL_VALUE = -1;
private final char SPACE = ' ';

// in method where loop is used (local to method)
// method: File_Output_Class constructor
File_Output_Class outFile = new File_Output_Class();
String fileName = "sentineldata.txt";
int number, numToOutput = 5;

// open input file, test for success
// method: openOutputFile
if( outFile.openOutputFile( fileName ) )
{
// loop across five values
for( number = 1; number <= 5; number++ )
{
// output the number with a space between
// method: writeChar, writeInt
outFile.writeChar( SPACE_CHAR );
outFile.writeInt( number );
}
// end loop

// output a sentinal value after the data
// method: writeChar, writeInt
outFile.writeChar( SPACE_CHAR );

outFile.writeInt( SENTINEL_VALUE );

outFile.writeEndline();

// close file
// method: closeOutputFile
outFile.closeOutputFile();
}

// otherwise, assume file open failed
else
{
// display failed output
// method: println
System.out.println( "Output operation failed." );
}

Watch this video to see the above code used as a program for downloading with a sentinel.

1 2 3 4 5 -1

The recovery code would then look like the following.

// within the class initialization (global to class)
private final int SENTINEL_VALUE = -1;
private final char SPACE_CHAR = ' ';

// in method where loop is used (local to method)
File_Input_Class inFile = new File_Input_Class();
String fileName = "sentineldata.txt"
int number;

// open input file, test for success
// method: openInputFile
if( inFile.openInputFile() )
{
// read prime for loop
// method: getInt
number = inFile.getInt();

// loop until sentinel found
while( number != SENTINEL_VALUE )
{
// display value found
// method: println
System.out.println( "Integer value: " + number );

// get the next value
// method: getInt
number = inFile.getInt();
}
// end loop

// close input file
// method: closeInputFile
inFile.closeInputFile();
}

// otherwise, assume file open failure
else
{
// display file open failure
// method: println
System.out.println( "File not found" );
}

It is important to note here that it is not a good practice to input and output data in the same method or operation. This is another way to break modularity. And very soon, you will be using arrays which means you will be able to input the data in one operation or method, and output the data in a separate operation or method. Keep this in mind but for right now until you have studied arrays, you will have to work with reduced modularity conditions.

Watch this video to see the above code used in a program.

A Second Way: Using Read Priming

The sentinel operation can be handy but once you use the read priming process, you will find that there is a simpler way to check for, and input, data. Consider the following data.

1 2 3

Note that there is no sentinel value here, just three data items (i.e., numbers). Now consider the following code.

This first code is flawed. But the flaw is not obvious. Take a look.

// in method where loop is used (local)
File_Input_Class fileIn = new File_Input_Class();
String fileName = "randvals.txt";
int number;

// open input file, check for success
if( fileIn.openInputFile( fileName ) )
{
// loop until file operation fails
// method: checkForEndOfFile
while( !fileIn.checkForEndOfFile() )
{
// get input value
// method: getInt
number = fileIn.getInt();

// display value found
// method: println
System.out.println( "Integer value: " + number );
}
// end loop

// close file
// method: closeInputFile
fileIn.closeInputFile();
}

// otherwise, assume file open failure
else
{
// display file open failure
// method: println
System.out.println( "File not found" );
}

The failure here is not obvious because it looks like the program would do everything correctly. However, what will actually happen is that the method will get the first number and, without testing for file input failure, display it, then it will get the next number and display it, and then get the third number and display it. Now having acquired the third number, the loop should stop. However, it has no reason to. In almost any text file, there should be at least one end line in the file before the end of the file. In fact, the File_Output_Class guarantees that, and most other code should as well.

So, the loop continues into the next access operation. This time when it reaches for the data item, it is not there, and the checkForEndOfFile will now report true. However, this test is not done here at the point between getting the data and outputting it. Thus, the program will output incorrect data. Different systems may do different things. The File_Input_Class input operations will return a zero character, zero value, or empty string (depending on which acquisition method is used), but others may not change the number at all. Either the last number in the series will be incorrectly output as zero or it will be the same as the third value. Then, when the program cycles back to the while loop and checkForEndOfFile() test, it will discover the failure and stop the loop . . . one iteration too late.

This does not look very different from the previous loop, but the difference is significant.

File_Input_Class fileIn = new File_Input_Class();
String fileName = "randvals.txt";
int number;

// open input file, check for success
if( fileIn.openInputFile( fileName ) )
{
// read prime for the loop
// method: getInt
number = fileIn.getInt();

// loop until file operation fails
// method: checkForEndOfFile
while( !fileIn.checkForEndOfFile() )
{
// display value found
// method: println
System.out.println( "Integer value: " + number );

// get the next value (reprime)
// method: getInt
number = fileIn.getInt();
}
// end loop

// close file
// method: closeInputFile
fileIn.closeInputFile();
}

// otherwise, assume file open failure
else
{
// display file open failure
// method: println
System.out.println( "File not found" );
}

The checkForEndOfFile() test cannot look ahead to find out if it is at the end of the file; it can only report if the file stream object’s most recent action was a success or a failure. This backward-looking operation works okay as long as you follow a single rule: When you don't know how much data is in a file, always test all input files for "not at end of file" before using the data you last attempted to input. In this case, it means every time you "reach" for input from a file, you should do nothing else until you test the input file object for checkForEndOfFile(). This is an important rule. If you do not follow it, you will not get the correct data into your program.

To repeat, when inputting unknown quantities of data, if you do not prime your loop before starting it, and/or you do not “re-prime” your loop by inputting the data at the end of the loop, data access failure is very likely. You must prime your loop before starting it, and you must “re-prime” your loop with an input action last thing in the loop block.

Watch this video to see the failure of this process, and then watch this video to see the right way to do it. It is important to understand the failure of not read priming before starting the loop so that you do not make this mistake in the future.

A Third Way: Using File Header Information

This third form of managing unknown input quantities is the best because it does not rely on previously located sentinel values or finding the end of the file. Either way is acceptable. A third way to do this is to place a number at the beginning of the file indicating how many items there are in the line, or possibly how many rows of items there are in the file, or some combination of these. Again, this means your program will rely on previously output data, but if this is done carefully, it can work fine. Here is an example of a text file with the data, and what is called the header part of the file (the first line shown below).

number of items: 17

65, 42, 19, 55, 57,

22, 14, 98, 33, 15,

27, 11, 44, 13, 18,

85, 93

The code to upload or read this data could be written as follows:

// constant initialization (global to class)
private final char COLON_CHAR = ':';

// variable initialization (local to method)
File_Input_Class fileIn = new File_Input_Class();
String fileName = "textfile.txt";
int counter, number, numberOfItems;

// open the file
// method: open
if( fileIn.open( fileName ) )
{
// read in the "number of items" string, ignore
// method: getString
fileIn.getString( COLON_CHAR );

// read in the number of items in the loop
// method: getInt
numberOfItems = fileIn.getInt();

// loop across given number of items
for( counter = 0; counter < numberOfItems; counter++ )
{
// get a value
// method; getInt
number = fileIn.getInt();

// display value found
// method: println
System.out.println( "Integer value: " + number );
}

// close file
// method: fstream .close
fileIn.closeInputFile();
}

// otherwise, assume file open failure
else
{
// display file open failure
// method: println
System.out.println( "File not found" );
}

Once the method gets the number of items, it can use that value to input exactly the correct number of subsequent values and it does that within the loop. Notice that there is no input for the commas. These are naturally removed by the getInt method that, like the others, always acquires and ignores the first character after whatever data type value has been input. For the integer in this case, the getInt method will continue to accept the data until no more digits between 0 and 9 (inclusive) are found. However, the method must input each next character to test for a digit. Once it finds a character that is not a digit, it completes but due to this strategy that last character will be lost. As stated previously, this should not be a problem since valid data should not immediately follow any other valid data.

The above code acquires the header string "number of items" using the getString method. Note that the method call does not return the actual string. It doesn't need to since there is no use for it in the program. This is a strategy that can be used in various ways. When a method provides a result, it is acceptable to ignore the return quantity if there is no use for it in the program, and this is an example.

A Side Note about String Input

Notice that this getString method uses a parameter. There are two getString methods available in the File_Input_Class. One of them gets a a string up to some delimiter. Remembering that a delimiter is just some kind of character indicator that divides data, this could be a period, a comma, a semicolon, or in this case, a colon. When you look at the text data you notice that it ends with a colon so this is easy. You just tell your program to get all the text it can find up to a colon by calling getString with a colon parameter. You could just as easily do this with any of the other punctuation but as it happens here, it was a colon. So all of the text "number of items" was captured by the method.

On the other hand, sometimes you just want to get the text a word at a time. The other getString gets any text up to a space and since it is always the same delimiter, there is no need to place this as a parameter; you just use getString(). It turns out that this method just calls the other one with a "space" parameter but again that keeps you from having to type that in. For the string "number of items", the printSpace() method would return "number". This is another example of overloaded methods that can help with different conditions.

Watch this video to see an example of this kind of input.

File I/O, Uncomplicated

The past few topics have provided you with the tools for storing data to, and acquiring data from, text files, most commonly applied to hard drive access. You should use the first of these topics to become familiar with the concepts of file I/O, and the next couple of topics provide you with some specific code examples, issues, and operations so you can put them to work in your own code.

Since you have been using this input and output structure for most of the time you have been programming, this new information is something to extend and expand on instead of having to learn something completely brand new. The next topic will expand on how you can acquire strings from text files, so it is again just an extension on what you have already learned.