Laboratory Training 5

Working with Exceptions and Files in Java

1 Training Tasks

1.1 Individual Task

Create and implement classes that represent entities given in an individual assignment of the previous Laboratory training. The decision should be based on the hierarchy of classes.

You need to create two derived classes from the class that represents the main entity. The first derived class should be supplemented by the ability to read data from the appropriately prepared text file and write this data to another file after sorting. The second derived class should implement support reading data from a pre-generated XML document, storing data in structures that are automatically created using data binding technology, and writing data to another XML document after sorting. To avoid duplication of data in the program, you must also redefine the class representing the second entity. Derived classes should implement a common interface, which declare the functions of reading from a file and writing to a file.

An additional console output should be provided.

1.2 Sorting Integers

Implement a program that reads positive integer values from the text file (the numbers are separated by spaces, it should be read to the end of the file), places these numbers into an array, sorts them in descending order and increasing the sum of digits and stores both results in two new text files. The above actions should be implemented in separate static functions. To define the sort order, create two classes that implement the Comparator interface.

1.3 Working with ZIP Archive (Additional Task)

Create classes Student and Academic Group (with an array of students as a field). Create objects, store data about the students of the academic group in the archive. Read data from the archive in another program.

2 Instructions

2.1 Exception Handling

2.1.1 Basic Concepts

The exception handling mechanism is a very important part of Java programming practice. Almost every Java program contains some parts of this mechanism. Exception objects allow programmer to separate points where runtime errors occur from points of error handling. It allows programmers to create more reliable multipurpose classes and libraries.

An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions. The mechanism of generation and handling exceptions allows transferring information about the error from place of origin to the place where this error can be handled. Exceptions in Java are divided into synchronous (runtime error, exception generated by throw operation) and asynchronous (system failures of the Java virtual machine). The place of occurrence of the second group of exceptions is difficult to detect.

Exception mechanism is present in all modern languages of object-oriented programming. In comparison to C++, Java implements more strict exception handling mechanism.

2.1.2 Exception Throwing Syntax

To generate an exception, operator throw is used. The throw statement must be followed by an object that is of type Throwable or one of its subclasses. For program-defined exceptions, you typically want an exception object to be an instance of a subclass of the Exception class (derived from Throwable). In most cases, it makes sense to define a new subclass of Exception that is specific to your program. These derived classes usually reflect the specifics of a particular application.

class SpecificException extends Exception {
}

There is also a base class for generation of system errors: the Error class. Exception and Error classes have a common base class - Throwable.

In most cases, exception object is created on the spot using new operator. For example, a typical throw statement might look like this:

void f() . . .
    . . .
    if (/* some error */) {
        throw new SpecificException();
    }

You must specify all exception types that method could throw using throws keyword:

void f() throws SpecificException, AnotherException {
    . . .
    if (/* some error */) {
        throw new SpecificException();
    }
    if (/* another error */) {
        throw new AnotherException();
    }
    . . .
}

In the following example, function reciprocal() raises exception in case of division by zero.

class DivisionByZero extends Exception {
}

class Test {

    double reciprocal(double x) throws DivisionByZero {
        if (x == 0) {
            throw new DivisionByZero();
        }
        return 1 / x;
    }

}

Unlike C++, Java does not allow use of primitive types for exceptions. Only objects of types derived from Throwable or Exception are allowed.

The exception list must be preserved for overridden methods by inheritance.

2.1.3 Exception Handling Syntax

An exception that has been thrown in a particular part of a code should be caught in another part. If we want to call a function that can potentially throw an exception, the call to this function is placed in the try { } block:

double x, y;
. . .
try {
    y = reciprocal(x);
}

A try block is followed by a sequence of one or more catch statements, or handlers, each of which handles a different type of exception. For example:

catch (DivisionByZero d) {
    // exception handling
}
catch (Exception ex)  {
    // exception handling
}

Exceptions form an object hierarchy. The base type handler also accepts exceptions to all types derived from it. What you have to do here is put catch blocks for the more specific exceptions before those for the more general exceptions. Suppose there is such an exception class hierarchy:

class BaseException extends Exception {
}

class FileException extends BaseException {
}

class FileNotFoundException extends FileException {
}

class WrongFormatException extends FileException {
}

class MathException extends BaseException {
}

class DivisionByZero extends MathException {
}

class WrongArgument extends MathException {
}
      

Suppose there is some function that can throw all types of these exceptions:

public class Exceptions {

    public static void badFunc() throws BaseException {
        // there may be various exceptions
    }

}

Depending on the program logic, different types of exceptions can be processed in more detail:

try {
    Exceptions.badFunc();
}
catch (FileNotFoundException ex) {
    // file not found
}
catch (WrongFormatException ex) {
    // wrong format
}
catch (FileException ex) {
    // other file-related errors
}
catch (MathException ex) {
    // all mathematical errors are handled together
}
catch (BaseException ex) {
    // handle all other exceptions of badFunc() function
}
catch (Exception ex) {
    // just in case
}
      

Following the last catch block you can also include a finally block. This code is guaranteed to run whether an exception is thrown or not thrown, even if some block contains an exit from the function.

try {
    openFile();
    // other actions
}
catch (FileError f) {
    // exception handling
}
catch (Exception ex) {
    // exception handling
}
finally {
    closeFile();
}

In the Java 7 version, new constructs have been added to the exception syntax, which makes working with exceptions more convenient. For example, you can create an event handler of various types using the bitwise OR operation:

public void newMultiCatch() {
    try {
        methodThatThrowsThreeExceptions();
    } 
    catch (ExceptionOne | ExceptionTwo | ExceptionThree e) {
        // exceptions handling
    }
}

Other additional features are associated with the so-called "try-with-resources" syntax construct. You can place creation of objects those implement java.lang.AutoCloseable interface directly after try. This guarantee invocation of close() method by leaving of try {} catch {} block (similar to running the code in the finally block):

try (ClassThatImplementsAutoCloseable sc = new ClassThatImplementsAutoCloseable()) {
    // actions that may lead to an exception
}
catch (Exception f) {
    // exception handling
}   // automatic call of sc.close()

In contrast to C++, you cannot use catch (...) for catch of any exception. Instead of this, use objects of base classes of all exceptions:

catch (Exception ex) {
    // exception handling
}

or

catch (Throwable ex) {
    // exception handling
}

The typical implementation of exception handler is call of printStackTrace() method.

catch (Throwable ex)  {
    ex.printStackTrace();
}

This method prints a stack trace for this Throwable object on the error output stream (System.err). The following example may be regarded as typical:

java.lang.NullPointerException
        at SomeClass.g(SomeClass.java:9)
        at SomeClass.f(SomeClass.java:6)
        at SomeClass.main(SomeClass.java:3)

If some exception cannot be completely handled within the catch () { } block, it can be passed on to other handlers:

catch (SomeException ex) {
    // local exception handling
    throw ex;
}

Sometimes some extra information is needed to adequately handle the exception information. For example, we create a function inside which a square root should be found. If the argument is negative, you need to generate an exception. It is useful to know which negative value was obtained for debugging the program. You can create an exception class whose object will store this value. It is set in the constructor and at the exception handling point it can be obtained using the getter. This approach can be demonstrated in the following example. Assume, such exception class is created:

public class WrongArgumentException extends Exception {
    private double arg;

    public WrongArgumentException(double arg) {
        this.arg = arg;
    }

    public double getArg() {
        return arg;
    }

}

An exception can be thrown in some function if the argument cannot be used:

public class SomeLib {

    public static void doSomeUseful(double x) throws WrongArgumentException {
        // check x
        if (x < 0)
            throw new WrongArgumentException(x);
        double y = Math.sqrt(x);
        // further work
    }

}

Now a caught exception object can be applied to get more detailed information.

public class ExceptionTest {
    public static void main(String[] args) {
        double x = new java.util.Scanner(System.in).nextDouble();
        try {
            // . . .
            SomeLib.doSomeUseful(x);
            // . . .
        }
        catch (WrongArgumentException e) {
            System.err.println(e.getClass().getName() + e.getArg());
        }
    }
}

As you can see from the following example, invocation of the getClass().getName() method allows getting the class name. Such invocation can be applied to any object (not only to exception).

The call of functions those might throw an exception outside of try block produces a compiler error. The exception checking should always be performed:

double f(double x) {
    double y;
    try {
        y = reciprocal(x);
    }
    catch (DivisionByZero ex) {
        ex.printStackTrace();
        y = 0;
    }
    return y;
}

You can add throws declaration to calling function. Now this uncaught exception is transmitted to outer handlers:

double g(double x) throws DivisionByZero {
    double y;
    y = reciprocal(x);
    return y;
}

This rule is obligatory for all Java exceptions, apart from objects of RuntimeException class and its descendants. If a function produces such exception, you can either place its call into try block or ignore an exception. Functions that throw such exception do not declare them in their header. A typical exception from RuntimeException class hierarchy is NullPointerException.

The IntelliJ IDEA environment allows you to automate the process of creating blocks of catching and processing exceptions. If you select the block in the text editor and then apply the Code | Surround With... | try / catch function, the selected block will be surrounded with try block, and there will be added catch blocks that will contain the standard processing of all possible exceptions.

Note. The Eclipse also allows you to automate the process of creating blocks of catching and processing exceptions. This can be done using the Source | Surround With | Try/catch Block function of the main menu.

2.2 Input and Output Streams. Character Streams

2.2.1 Overview

Like most modern languages and platforms, Java generalizes the concept of streams, extending common approaches to file, console, network, and other I/O processes.

Classes that perform file input and output, as well as other actions with streams, are located in the java.io package. The classes defined in this package offer a number of methods for creating streams, reading, writing, etc. Objects of FileReader and FileWriter classes directly work with text files. There are two groups of classes: for working with text and binary files respectively.

All stream-aware functions require exception handle. All work with streams other than standard System.in and System.out streams, requires handling of I/O-related exceptions. These are objects of IOException class or objects of derived classes (FileNotFoundException, ObjectStreamException, etc.).

It is very important to close all files that were open. When closing files, the data remaining in the buffer is written to the file, the release of the buffer and other resources associated with the file are made. You can close the file using the close() method. For example, for stream called in:

  in.close();

If an application that requires file input is executed in an Eclipse environment (or in IntelliJ IDEA), files with source data should be placed in the project directory (not in the package folder). In the project directory, you can find the resulting files that appear after finishing execution of a program that involved the file output.

The program can simultaneously open multiple streams for input and output.

2.2.2 Working with Character Streams

Streams that work with textual information are called character streams. Names of corresponding stream classes end with the words "...Reader" and "...Writer" respectively.

The BufferedReader class implements so called buffering input. This class reads text from a character-input stream, buffering characters to provide for the efficient reading. The BufferedWriter class writes text to a character-output stream, buffering characters to provide for the efficient writing. The print() and println() methods of PrintWriter class realize formatted output.

An important concept concerned with file streams is buffering. Buffering involves the creation of a special memory area (buffer), into which data is retrieved from a file for later element-wise reading or for element-wise recording data before copying to the disk. BufferedReader class objects perform such buffered reading.

BufferedWriter objects are used for buffered output. Direct formatted output is performed by print() and println() methods of the PrintWriter class.

The following program reads one integer and one real value from data.txt file and writes their sum into results.txt file.

package ua.inf.iwanoff.oop.fifth;

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

public class FileTest {

    void readWrite() {
        try {
            FileReader fr = new FileReader("data.txt");
            BufferedReader br = new BufferedReader(fr);
            String s = br.readLine();
            int x;
            double y;
            try {
                StringTokenizer st = new StringTokenizer(s);
                x = Integer.parseInt(st.nextToken());
                y = Double.parseDouble(st.nextToken());
            }
            finally {
                br.close();
            }
            double z = x + y;
            FileWriter fw = new FileWriter("results.txt");
            PrintWriter pw = new PrintWriter(fw);
            pw.println(z);
            pw.close();
        }
        catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new FileTest().readWrite();
    }

}
      

To open a file, an object of the FileReader class is created, the constructor gets file name as an argument. The reference to the created object is passed to the BufferedReader class constructor. Reading string data from a file can be done using the readLine() method, which returns a reference to the character string, or null, if the end of the file is reached.

The variable s refers to a string containing two numbers. To receive separate tokens from this string the object of the class StringTokenizer is used, whose constructor gets this string. References to individual parts of a string can be successively obtained using the nextToken() method. These references can be used directly, or used to convert data into numeric values (static parseDouble() and parseInt() classes of Double and Integer, respectively).

You can use the Scanner class to read from a file. The actual parameter of the constructor may be the file stream. The previous example can be implemented using the Scanner class. You can also shorten the code by excluding unnecessary variables. In addition, it is advisable to use the try () { } Java 7 construct for automatic stream closing:

package ua.inf.iwanoff.oop.fifth;

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

public class FileTest {

    void readWrite() {
        try (Scanner scanner = new Scanner(new FileReader("data.txt"))) {
            try (PrintWriter pw = new PrintWriter("results.txt")) {
                pw.println(scanner.nextInt() + scanner.nextDouble());
            }
        }
        catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new FileTest().readWrite();
    }

}    

The advantage of this approach is the arbitrary location of the source data (not necessarily in a single line). As can be seen from the example above, several try () blocks can use one catch () block. An alternative is to place several statements inside the brackets:

try (Scanner scanner = new Scanner(new FileReader("data.txt"));
     PrintWriter pw = new PrintWriter("results.txt")) {
    pw.println(scanner.nextInt() + scanner.nextDouble());
}
catch (IOException ex) {
    ex.printStackTrace();
}    

While working with the Scanner class, you can define additional parameters, for example, set a separator character (or a sequence of characters). For example, you can add the following line before reading the data:

scanner.useDelimiter(",");

Now the scanner object will accept commas as delimiters (instead of spaces).

2.3 Working with Binary Streams (Byte Streams)

To work with non-textual (binary) files, Java offers streams whose names end with "...Stream" instead of "...Reader" and "...Writer", for example, InputStream, FileInputStream, OutputStream, FileOutputStream, etc. Such streams are called byte streams. The following example demonstrates copying a binary FileCopy.class to a project folder with a new name:

package ua.inf.iwanoff.oop.fifth;

import java.io.*;

public class FileCopy {

    public static void copy(String inFile, String outFile) {
        byte[] buffer = new byte[1024]; // Byte buffer
        try (InputStream input = new FileInputStream(inFile);
             OutputStream output = new FileOutputStream(outFile)) {
            int bytesRead;
            while ((bytesRead = input.read(buffer)) >= 0) {
                output.write(buffer, 0, bytesRead);
            }
        } 
        catch (IOException ex) {
            ex.printStackTrace();
        }    
    }

    public static void main(String[] args) {
        copy("bin/ua/inf/iwanoff/oop/fifth/FileCopy.class", "FileCopy.copy");
    }

}

As seen from the above example, Java allows normal slash (/) instead of backslash character. This is more universal approach suitable for different operating systems. In addition, the backslash character should be written twice (\\).

To work with binary files there are additional possibilities: the use of data streams and object streams. The so-called data streams support binary input / output of values of simple data types (boolean, char, byte, short, int, long, float and double), as well as values of type String. All data streams implement the DataInput or DataOutput interfaces. For most tasks, standard implementations of these interfaces are sufficient: DataInputStream and DataOutputStream. The data in the file is stored in the form in which they are presented in memory. To write rows use the writeUTF() method. The following example demonstrates writing data to binary file:

package ua.inf.iwanoff.oop.fifth;

import java.io.*;

public class DataStreamDemo {

    public static void main(String[] args) {
        double x = 4.5;
        String s = "all";
        int[] a = { 1, 2, 3 };
        try (DataOutputStream out = new DataOutputStream(
                new FileOutputStream("data.dat"))) {
            out.writeDouble(x);
            out.writeUTF(s);
            for (int k : a)
                out.writeInt(k);
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Now data can be read in another program:

package ua.inf.iwanoff.oop.fifth;

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

public class DataReadDemo {

    public static void main(String[] args) {
        try (DataInputStream in = new DataInputStream(
                new FileInputStream("data.dat"))) {
            double x = in.readDouble();
            String s = in.readUTF();
            List<Integer> list = new ArrayList<>();
            try {
                while (true) {
                    int k = in.readInt();
                    list.add(k);
                }
            }
            catch (Exception e) {
            }
            System.out.println(x);
            System.out.println(s);
            System.out.println(list);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

}

Note. In the above program, the termination of the cycle is carried out through catch of an exception. This approach is not recommended because the exception mechanism reduces the performance of the program. In our case, it would be advisable to separately store the length of the array before its items, and then use this length to organize the for loop while reading.

You can also use the java.io.RandomAccessFile class to read and write data. The object of this class allows you to shift freely within the file in the forward and reverse direction. The main advantage of the RandomAccessFile class is the ability to read and write data being at an arbitrary location within the file.

In order to create an object of the class RandomAccessFile, it is necessary to call its constructor with two parameters: the name of the file for input / output and the mode of access to the file. You can use special strings such as "r" (for reading), "rw" (for reading and writing), etc. to define the mode. For example, opening a data file can be as follows:

RandomAccessFile file1 = new RandomAccessFile("file1.dat", "r");  // for reading 
RandomAccessFile file2 = new RandomAccessFile("file2.dat", "rw"); // for reading and writing

Once the file is open, you can use methods like readDouble(), readInt(), readUTF(), etc. to read or writeDouble(), writeInt(), writeUTF(), etc. for output.

File management is based on a pointer to the current position where data is read or written. At the time the RandomAccessFile object is created, the pointer is set to the beginning of the file and is set to 0. The calls to the read...() and write...() methods shift the position of the current pointer to the number of bytes read or written. To arbitrarily shift the pointer to a certain number of bytes, you can use the skipBytes() method, or set the pointer to a certain file location by calling the seek() method. To get the current position in which the pointer is located, you need to call the getFilePointer() method. For example, the first program writes data to a new file:

RandomAccessFile fileOut = new RandomAccessFile("new.dat", "rw");
int a = 1, b = 2;
fileOut.writeInt(a);
fileOut.writeInt(b);
fileOut.close();    

Another program reads a second integer:

RandomAccessFile fileIn = new RandomAccessFile("new.dat", "rw");
fileIn.skipBytes(4); // move the file pointer to the second number
int c = fileIn.readInt();
System.out.println(c);
fileIn.close();    

To find the length of a file in bytes, you can use the length() function.

2.4 Binary Serialization of Objects

The serialization mechanism involves recording objects into the stream of bits for storage in a file or for the file transfer over computer networks. Deserialization involves reading the stream of bits, creating saved objects and recreating their state at the time of preservation. In order for the objects of a certain class to be serialized, the class must implement the java.io.Serializable interface. This interface does not define any method, its presence only indicates that objects of this class can be serialized. However, guaranteed serialization and deserialization require the presence of a special static field called serialVersionUID, which ensures the uniqueness of the class. The Eclipse environment allows you to generate the required value automatically (Quick Fix | Adds a generated serial version ID from the context menu).

The ObjectOutputStream and ObjectInputStream classes allow serialization and deserialization. They implement the ObjectOutput and ObjectInput interfaces respectively. The mechanisms of serialization and deserialization will be considered in the following example. Suppose the Point class is described:

package ua.inf.iwanoff.oop.fifth;

import java.io.Serializable;

public class Point implements Serializable {
    private static final long serialVersionUID = -3861862668546826739L;
    private double x, y;
  
    public void setX(double x) {
        this.x = x;
    }

    public void setY(double y) {
        this.y = y;
    }

    public double getX() {
        return x;
    }
  
    public double getY() {
        return y;
    }
  
}
      

Also created Line class:

package ua.inf.iwanoff.oop.fifth;

import java.io.Serializable;

public class Line implements Serializable {
    private static final long serialVersionUID = -4909779210010719389L;
    private Point first = new Point(), second = new Point();
  
    public void setFirst(Point first) {
        this.first = first;
    }
  
    public Point getFirst() {
        return first;
    }

    public Point getSecond() {
        return second;
    }
  
    public void setSecond(Point second) {
        this.second = second;
    }
}

In the following program (in the same package), objects are created with subsequent serialization:

package ua.inf.iwanoff.oop.fifth;

import java.io.*;

public class SerializationTest {

    public static void main(String[] args) {
        Line line = new Line();
        line.getFirst().setX(1);
        line.getFirst().setY(2);
        line.getSecond().setX(3);
        line.getSecond().setY(4);
        try (ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("temp.dat"))) {
            out.writeObject(line);
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

}

In another program, we can deserialize:

package ua.inf.iwanoff.oop.fifth;

import java.io.*;

public class DeserializationTest {

    public static void main(String[] args) throws ClassNotFoundException {
        try (ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("temp.dat"))) {
            Line line = (Line) in.readObject();
            System.out.println(line.getFirst().getX() + " " + line.getFirst().getY()
                    + " " + line.getSecond().getX() + " " + line.getSecond().getY());
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        ;
    }

}

You can also serialize objects that contain arrays of other objects. For example:

package ua.inf.iwanoff.oop.fifth;

import java.io.*;

class Pair implements Serializable {
    private static final long serialVersionUID = 6802552080830378203L;
    double x, y;

    public Pair(double x, double y) {
        this.x = x;
        this.y = y;
    }

}

class ArrayOfPairs implements Serializable {
    private static final long serialVersionUID = 5308689750632711432L;
    Pair[] pairs;

    public ArrayOfPairs(Pair[] pairs) {
        this.pairs = pairs;
    }

}

public class ArraySerialization {

    public static void main(String[] args) {
        Pair[] points = { new Pair(1, 2), new Pair(3, 4), new Pair(5, 6) };
        ArrayOfPairs arrayOfPoints = new ArrayOfPairs(points);
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("temp.dat"))) {
            out.writeObject(arrayOfPoints);
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Now we can accomplish deserialization:

package ua.inf.iwanoff.oop.fifth;

import java.io.*;

public class ArrayDeserialization {

    public static void main(String[] args) throws ClassNotFoundException {
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("temp.dat"))) {
            ArrayOfPairs arrayOfPairs = (ArrayOfPairs) in.readObject();
            for (Pair p : arrayOfPairs.pairs) {
                System.out.println(p.x + " " + p.y);
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

}

Some class fields whose values do not affect the state of an object can be described with the transient modifier. For example:

class SomeClass implements Serializable {
    transient int someUnnecessaryField;
}    

Values in such fields will not be saved in the stream during serialization and will not be restored during deserialization.

Serialization and deserialization can be used instead of file input and output. The main disadvantage of binary serialization is the need to work with binary (non-textual) files.

2.5 Working with Archives

2.5.1 Standard Means of Working with Archives

The java.util.zip package provides the ability to work with standard ZIP and GZIP file formats.

To write to the archive, use the class ZipOutputStream. Using the setMethod() function of this class, you can define the archiving method: ZipOutputStream.DEFLATED (with compression) or ZipOutputStream.STORED (without compression). The setLevel() method defines the compression level (from 0 to 9, by default Deflater.DEFAULT_COMPRESSION, as a rule, the maximum compression). The setComment() method allows you to add a comment to the archive.

For each data block to be placed in a zip file, the ZipEntry object is created. The necessary file name is passed to the ZipEntry constructor. It is possible to set separate parameters similarly. Next, using the putNextEntry() method of the ZipOutputStream class, the corresponding entry point to the archive is "opened". By using file streaming tools, the data is archived, then the ZipEntry object should be closed by calling closeEntry().

The following example demonstrates the creation of the Source.zip archive, to which the contents of the ZipCreator.java source file is written:

package ua.inf.iwanoff.oop.fifth;

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

public class ZipCreator {

    public static void main(String[] args) {
        try (ZipOutputStream zOut = new ZipOutputStream(new FileOutputStream("Source.zip"))) {
            ZipEntry zipEntry = new ZipEntry("src/ua/inf/iwanoff/oop/fifth/ZipCreator.java");
            zOut.putNextEntry(zipEntry);
            try (FileInputStream in = new FileInputStream("src/ua/inf/iwanoff/oop/fifth/ZipCreator.java")) {
                byte[] bytes = new byte[1024];
                int length;
                while ((length = in.read(bytes)) >= 0) {
                    zOut.write(bytes, 0, length);
                }
            } 
            zOut.closeEntry();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

}

The newly created archive contains a relative path to the file. If this is not required, only the name of the path should be specified when creating a ZipEntry object:

ZipEntry zipEntry = new ZipEntry("ZipCreator.java");

In order to read data from an archive, you should use the ZipInputStream class. Each such archive always needs to get individual entries. The getNextEntry() method returns an object of type ZipEntry. The read() method of the ZipInputStream class returns -1 at the end of the current record (and not only at the end of the zip file). Next, the closeEntry() method is invoked to allow the switch to read the next record. In the following example, ZipCreator.java is read from the previously created archive and output of its content in the console window:

package ua.inf.iwanoff.oop.fifth;

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

public class ZipExtractor {

    public static void main(String[] args) {
        try (ZipInputStream zIn = new ZipInputStream(new FileInputStream("Source.zip"))) {
            ZipEntry entry;
            byte[] buffer = new byte[1024];
            while ((entry = zIn.getNextEntry()) != null) {
                int bytesRead;
                System.out.println("------------" + entry.getName() + "------------");
                while ((bytesRead = zIn.read(buffer)) >= 0) {
                    System.out.write(buffer, 0, bytesRead);
                }
                zIn.closeEntry();
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Similarly, work with archives of the GZIP format is carried out. The appropriate streams of reading and writing are GZIPInputStream and GZIPOutputStream.

2.5.2 Creation and Use of JAR Files

To deploy the Java application on the client's computer, it is enough to copy all the necessary .class files in the required package folders. Java ideology does not allow you to create executable files (.exe). However, the files needed to execute the program can be packed into a special type archive, the so-called JAR. This is actually a ZIP archive, but it contains a MANIFEST.MF file in the META-INF directory, in which the program's main class (this class must contain main() method). This class name should be set by the Main-Class parameter. JAR version number is set by Manifest-Version parameter.

The program collected in the JAR file can be launched in such a command:

java -jar file_name.jar

GUI applications should be launched in such a command:

javaw -jar file_name.jar

In this case, the console window will not be created.

If the JDK tools have been installed on your computer and correctly identified all paths, you can run the application double-clicking on the JAR file.

In order to create a JAR file in the IntelliJ IDEA environment, you should adjust certain settings. In particular, in the main menu, select File | Project Structure | Project Settings | Artifacts, then click the "+" button and add Jar | From modules with dependencies.... Next in the dialog window, specify the module (Module, usually fills automatically) and the main class (Main Class, class with the main() function, which is a starting point of application). The best option for choosing the main class is to use the files icon (Browse...). The next step is the direct creation of the archive. In the main menu, select Build | Build Artifacts..., then Action | Build. The artifacts branch that contains a created JAR file appears in the out subtree of the Project tree.

3 Sample Programs

3.1 Copying of Text Files Line by Line

Suppose we need to create an application that performs copying text files line by line. File names are specified by command line arguments. The text of the program can be as follows:

package ua.inf.iwanoff.oop.fifth;

import java.io.*;

public class TextFileCopy {

    public static void main(String[] args) {
        if (args.length < 2) {
            System.out.println("Arguments are needed!");
            return;
        }
        try (BufferedReader in = new BufferedReader(new FileReader(args[0]));
             PrintWriter out = new PrintWriter(new FileWriter(args[1]))) {
            String line;
            while ((line = in.readLine()) != null) {
                out.println(line);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

}
      

3.2 Sorting Real Numbers

Suppose it is necessary to implement a program that reads from a text file real values in the range from -1000 to 1000, sorts them by increment and by decreasing absolute values and stores both results in two new text files. Numbers in the output file are separated by spaces, they should be read to the end of file.

In the DoubleNumbers class that we are designing, we create a nested static class to represent an exception associated with an incorrect real value (less than -1000 or more than 1000). In addition, while the sortDoubles() function that performs the main task, can throw IOException (file not found, file can not be created, etc.) and InputMismatchException (an object of type Scanner tries to get Double from a token that cannot be converted into number). To sort absolute values in descending order, we create a separate static function compareByAbsValues(), in which a local class is created and its object is returned. The source code will look like this:

package ua.inf.iwanoff.oop.fifth;

import java.io.*;
import java.util.*;
import static java.lang.Math.*;

public class DoubleNumbers {

    /**
     * An internal exception class that allows you to keep the invalid 
     * real value read from file (less than -1000 or more than 1000)
     *
     */
    public static class DoubleValueException extends Exception {
        private double wrongValue;

        public DoubleValueException(double wrongValue) {
            this.wrongValue = wrongValue;
        }

        public double getWrongValue() {
            return wrongValue;
        }

    }

    /**
     * A static function that determines the method of comparing
     * real numbers when sorting by decreasing the absolute value 
     *  
     * @return an object that implements the Comparator interface
     * 
     */
    public static Comparator<Double> compareByAbsValues() {
        // Local class:
        class LocalComparator implements Comparator<Double> {
            @Override
            public int compare(Double d1, Double d2) {
                return -Double.compare(abs(d1), abs(d2));
            }
        }
        return new LocalComparator();
    }

    /**
     * The function reads real numbers in the range from -1000 to 1000, sorts numbers
     * in two ways and puts results into two resulting files
     * 
     * @param inFileName  source file name
     * @param firstOutFileName  name of the file containing numbers sorted ascending
     * @param secondOutFileName  name of the file containing numbers 
     * sorted by increasing absolute values
     * @throws DoubleValueException
     * @throws IOException
     * @throws InputMismatchException
     */
    public static void sortDoubles(String inFileName, String firstOutFileName,
            String secondOutFileName) throws DoubleValueException, IOException,
            InputMismatchException {
        Double[] arr = {};
        try (BufferedReader reader = new BufferedReader(new FileReader(inFileName));
             Scanner scanner = new Scanner(reader)) {
            while (scanner.hasNext()) {
                double d = scanner.nextDouble();
                if (abs(d) > 1000) {
                    throw new DoubleValueException(d);
                }
                Double[] arr1 = new Double[arr.length + 1];
                System.arraycopy(arr, 0, arr1, 0, arr.length);
                arr1[arr.length] = d;
                arr = arr1;
            }
        }
        PrintWriter firstWriter = new PrintWriter(new FileWriter(firstOutFileName));
        PrintWriter secondWriter = new PrintWriter(new FileWriter(secondOutFileName));
        try {
            Arrays.sort(arr);
            for (Double x : arr)
                firstWriter.print(x + " ");
            Arrays.sort(arr, compareByAbsValues());
            for (Double x : arr)
                secondWriter.print(x + " ");
        }
        // The resulting files should be closed in the finally block:
        finally {
            firstWriter.close();
            secondWriter.close();
        }
    }

    public static void main(String[] args) {
        try {
            sortDoubles("in.txt", "out1.txt", "out2.txt");
        }
        // Invalid real value:
        catch (DoubleValueException e) {
            e.printStackTrace();
            System.err.println("Wrong value: " + e.getWrongValue());
        }
        // Error associated with files:
        catch (IOException e) {
            e.printStackTrace();
        }
        // The file contains something that is not a real number:
        catch (InputMismatchException e) {
            e.printStackTrace();
        }
    }

}

The hasNext() function returns true if the Scanner object can read the next value.

3.3 Binary Serialization and Deserialization

Suppose we need to create the Country and Continent classes, create an object of type Continent, perform its serialization and deserialization. The Country class can be as follows:

package ua.inf.iwanoff.oop.fifth;

import java.io.Serializable;

public class Country implements Serializable {
    private static final long serialVersionUID = -6755942443306500892L;
    private String name;
    private double area;
    private int population;

    public Country(String name, double area, int population) {
        this.name = name;
        this.area = area;
        this.population = population;
   }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getArea() {
        return area;
    }

    public void setArea(double area) {
        this.area = area;
    }

    public int getPopulation() {
        return population;
    }

    public void setPopulation(int population) {
        this.population = population;
    }

}

The Continent class can be as follows:

package ua.inf.iwanoff.oop.fifth;

import java.io.Serializable;

public class Continent implements Serializable {
    private static final long serialVersionUID = 8433147861334322335L;
    private String name;
    private Country[] countries;

    public Continent(String name, Country... countries) {
        this.name = name;
        this.countries = countries;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Country[] getCountries() {
        return countries;
    }

    public void setCountries(Country[] countries) {
        this.countries = countries;
    }

}

The following program creates and serializes the Continent object:

package ua.inf.iwanoff.oop.fifth;

import java.io.*;

public class DataSerialization {

    public static void main(String[] args) {
        Continent c = new Continent("Europe", 
            new Country("Ukraine", 603700, 46314736),
            new Country("France", 547030, 61875822),
            new Country("Germany", 357022, 82310000)
        );
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Countries.dat"))) {
            out.writeObject(c);
        }
        catch (IOException e) {
            e.printStackTrace();
        };
    }

}

In this way, we can make deserialization:

package ua.inf.iwanoff.oop.fifth;

import java.io.*;

public class DataDeserialization {

    public static void main(String[] args) throws ClassNotFoundException {
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("Countries.dat"))) {
            Continent continent = (Continent) in.readObject();
            for (Country c : continent.getCountries()) {
                System.out.println(c.getName() + " " + c.getArea() + " " + c.getPopulation());
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        };
    }

}

3.4 Working with Archives

The object data from Example 3.3 can be saved in the archive. The following program creates a Continent object and stores the data in the archive. Each country has its own ZipEntry entry point:

package ua.inf.iwanoff.oop.fifth;

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

public class StoreToZip {

    public static void main(String[] args) {

        Continent continent = new Continent("Europe", 
            new Country("Ukraine", 603700, 46314736),
            new Country("France", 547030, 61875822),
            new Country("Germany", 357022, 82310000)
        );
        try (ZipOutputStream zOut = new ZipOutputStream(new FileOutputStream("Continent.zip"));
            DataOutputStream out = new DataOutputStream(zOut)) {
            for (Country country : continent.getCountries()) {
                ZipEntry zipEntry = new ZipEntry(country.getName());
                zOut.putNextEntry(zipEntry);    
                out.writeDouble(country.getArea());
                out.writeInt(country.getPopulation());
                zOut.closeEntry();
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

}

So we can read from the archive:

package ua.inf.iwanoff.oop.fifth;

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

public class ReadFromZip {

    public static void main(String[] args) {
        try (ZipInputStream zIn = new ZipInputStream(new FileInputStream("Continent.zip"));
                                  DataInputStream in = new DataInputStream(zIn)) {
            ZipEntry entry;
            while ((entry = zIn.getNextEntry()) != null) {
                System.out.println("Country: " + entry.getName());
                System.out.println("Area: " + in.readDouble());
                System.out.println("Population: " + in.readInt());
                System.out.println();
                zIn.closeEntry();
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

}

3.5 Classes "Country" and "Census"

Suppose we need to supplement the previously created program with classes that perform the tasks of the example given in the previous Laboratory training, but data on the population census are read from file, and the results of sorting by population are recorded in another file. We can create new derived classes from previously created classes, one of which will read data from a text file and write modified (sorted) data into a text file, and the other provides these functions by working with data streams. The best option is not to create a new project, but to add a new package to a previously created project that will allow you to refer to previously created classes.

To unify the work of the program, it is useful for us to create a common base class that declares the corresponding reading and writing operations and implements the possibilities of demonstrating interaction with files of different formats. The code can be as follows:

package ua.inf.iwanoff.java.fifth;

import ua.inf.iwanoff.java.second.AbstractCountry;
import ua.inf.iwanoff.java.third.CountryWithList;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.InputMismatchException;

/**
 * Class for presenting the country in which the censuses are carried out.
 * The class is extended with the capabilities of file reading and writing
 */
public abstract class CountryWithFile extends CountryWithList {
    /**
     * Reads country and census data from the specified file
     *
     * @param fileName file name
     * @throws Exception
     */
    public abstract void readFromFile(String fileName) throws Exception;

    /**
     * Writes country and census data to the specified file
     *
     * @param fileName file name
     * @throws Exception
     */
    public abstract void writeToFile(String fileName) throws Exception;

    /**
     * Tests class methods
     */
    public void testCountry(String from, String toByPopulation, String toByComments) {
        createCountry();
        try {
            writeToFile(from);
        }
        catch (IOException e) {
            System.err.println("Write failed");
            e.printStackTrace();
        }
        catch (Exception e) {
            e.printStackTrace();
            System.err.println("Wrong format");
        }
        testCountry();
        clearCensuses();
        try {
            readFromFile(from);
            sortByPopulation();
            writeToFile(toByPopulation);
            sortByComments();
            writeToFile(toByComments);
        }
        catch (FileNotFoundException e) {
            System.err.println("Read failed");
            e.printStackTrace();
        }
        catch (IOException e) {
            System.err.println("Write failed");
            e.printStackTrace();
        }
        catch (InputMismatchException e) {
            e.printStackTrace();
            System.err.println("Wrong format");
        }
        catch (Exception e) {
            e.printStackTrace();
            System.err.println("Wrong format");
        }
        testCountry();
    }
}

The CountryWithTextFile class for working with text files will be as follows:

package ua.inf.iwanoff.java.fifth;

import ua.inf.iwanoff.java.second.Census;

import java.io.FileReader;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.Scanner;

/**
 * Class for presenting the country in which the censuses are carried out.
 * The class has been expanded with possibilities for working with text files
 */
public class CountryWithTextFile extends CountryWithFile {
    /**
     * Reads country and census data from the specified file
     *
     * @param fileName file name
     * @throws Exception
     */
    @Override
    public void readFromFile(String fileName) throws Exception {
        clearCensuses();
        try (Scanner scanner = new Scanner(new FileReader(fileName))) {
            setName(scanner.next());
            setArea(scanner.nextDouble());
            while (scanner.hasNext()) {
                int year = scanner.nextInt();
                int population = scanner.nextInt();
                String comments = scanner.nextLine();
                comments = comments.trim();
                addCensus(new Census(year, population, comments));
            }
        }
    }

    /**
     * Writes country and census data to the specified file
     *
     * @param fileName file name
     * @throws Exception
     */
    @Override
    public void writeToFile(String fileName) throws Exception {
        try (PrintWriter out = new PrintWriter(new FileWriter(fileName))) {
            out.println(getName() + " " + getArea());
            for (Census census : getCensuses()) {
                out.print(census.getYear() + " " + census.getPopulation() + " ");
                out.println(census.getComments());
            }
        }
    }

    /**
     * Demonstration of the program
     * @param args command line arguments (not used)
     */
    public static void main(String[] args) {
        new CountryWithTextFile().testCountry("Ukraine.txt", "ByPopulation.txt", "ByComments.txt");
    }
}

After the execution of the main() function of CountryWithTextFile class, text files containing the results of sorting appear in the root directory of the project.

The class that works with the data streams would be:

package ua.inf.iwanoff.java.fifth;

import ua.inf.iwanoff.java.second.Census;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;

/**
 * Class for presenting the country in which the censuses are carried out.
 * The class has been expanded with possibilities for working with data streams
 */
public class CountryWithDataFile extends CountryWithFile {
    /**
     * Reads country and census data from the specified file
     *
     * @param fileName file name
     * @throws Exception
     */
    @Override
    public void readFromFile(String fileName) throws Exception {
        clearCensuses();
        try (DataInputStream in = new DataInputStream(
                new FileInputStream(fileName))) {
            setName(in.readUTF());
            setArea(in.readDouble());
            int count = in.readInt();
            for (int i = 0; i < count; i++) {
                int year = in.readInt();
                int population = in.readInt();
                String comments = in.readUTF();
                addCensus(new Census(year, population, comments));
            }
        }
    }

    /**
     * Writes country and census data to the specified file
     *
     * @param fileName file name
     * @throws Exception
     */
    @Override
    public void writeToFile(String fileName) throws Exception {
        try (DataOutputStream out = new DataOutputStream(
                new FileOutputStream(fileName))) {
            out.writeUTF(getName());
            out.writeDouble(getArea());
            out.writeInt(censusesCount());
            for (int i = 0; i < censusesCount(); i++) {
                out.writeInt(getCensus(i).getYear());
                out.writeInt(getCensus(i).getPopulation());
                out.writeUTF(getCensus(i).getComments());
            }
        }
    }

    /**
     * Demonstration of the program
     * @param args command line arguments (not used)
     */
    public static void main(String[] args) {
        new CountryWithDataFile().testCountry("Ukraine.dat", "ByPopulation.dat", "ByComments.dat");
    }
}

After executing the program, binary data files are automatically created in the root directory of the project.

4 Exercises

  1. Read real values from a text file (to the end of file), find their sum and output this sum to another text file.
  2. Read the real values from the text file (to the end of the file), find the product of the modules of non-zero elements, and output it to another text file.
  3. Read the integer values from the text file (to the end of the file), find the product of even numbers, and output it to another text file.
  4. Read the whole value from the text file (to the end of the file), replace the negative values with absolute values, replace the positive values with zeros, and output the values to another text file.
  5. Read the integer values from the text file (to the end of the file), divide the even values by 2, multiply the odd values by 2 and write the resulting values to another text file.
  6. Create classes Faculty and Institute (with an array of faculties as a field). Create objects, implement their binary serialization and deserialization.

5 Quiz

  1. What is the exception mechanism for?
  2. What are the alternatives to the exception mechanism?
  3. How to create an exception object?
  4. What types can exception objects be?
  5. Can the main result of a function be used if an exception has been thrown?
  6. How to catch and handle an exception?
  7. How to create a block for handling all exceptions?
  8. Some function can throw an exception. Can you invoke this function outside of try block?
  9. What is the purpose of printStackTrace() invocation?
  10. What additional features of the syntax of exceptions appeared in the version of Java 7?
  11. What is the difference between byte streams and character streams in their application?
  12. What classes provide the work with text files and binary files?
  13. Why do you need to close files?
  14. Is it possible to open multiple I/O streams at the same time?
  15. What is the way of automatic closing of I/O streams?
  16. What are the advantages of using RandomAccessFile class?
  17. What are the DataOutputStream and DataInputStream data files used for? What are their advantages and disadvantages?
  18. What is serialization and what is it used for?
  19. What are the advantages and disadvantages of serialization?
  20. What functions should be defined for the implementation of the java.io.Serializable interface?
  21. What is the transient modifier used for?
  22. How does Java work with archives?