Laboratory Training 1
Working with I/O Streams
1 Training Assignment
1.1 Individual Task
Create and implement classes that represent entities given in an individual assignment of Laboratory training #5 of the course "Fundamentals of Programming (Second Part)". The decision should be based on the hierarchy of classes created during the implementation of the individual task of Laboratory training # 1 of the course "Object-oriented programming (first part)".
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 a binary serialization of previously created objects, deserialization, as well as sorting and serialization into another file. Derived classes must 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 Implementation of Serialization and Deserialization
Create classes Student
and AcademicGroup
(with an array of students as a field). Create objects, implement their binary serialization and deserialization.
1.4 Working with ZIP Archive
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.
1.5 List of Files in all Subdirectories
Enter the name of a particular folder from the keyboard. Display the names of all files in this folder, as well as all subdirectory files, their subdirectories, and so on. Implement the output through a recursive function. If the folder does not exist, display an error message.
2 Instructions
2.1 Overview of Java SE Platform
Java Platform, Standard Edition, (Java SE) is a standard version of the Java platform designed to create and run applets and applications designed for individual use or for small-scale enterprise use. Java SE is determined by the specification of packages and classes that provide solutions to problems in these areas:
- working with mathematical functions
- working with container classes
- working with timer and calendar
- working with text
- internationalization and localization
- working with regular expressions
- working with I / O streams and file system
- working with XML
- serialization and deserialization
- creating graphical user interface programs
- use of graphic tools
- printing support
- support for working with sound
- use RTTI, reflection, and class loaders
- working with threads
- working with databases
- Java Native Interface
- support for scripting
- support for network interaction
- interaction with the software environment
- application security
- support for logging
- deploying Java applications
Some of Java SE technologies will be considered in this course.
2.2 Input and Output Streams
2.2.1 Overview
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.
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 so as 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.
All stream-aware functions require exception handle. They can throw IOException
object or objects of derived classes (FileNotFoundException
, ObjectStreamException
, etc.).
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, 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
The following program reads one integer and one real value from data.txt
file and writes their sum into results.txt
file.
package ua.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.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.in.iwanoff.java.first; 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/in/iwanoff/java/first/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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.3 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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. Example:
package ua.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.4 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. In it 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.in.iwanoff.java.first; 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/in/iwanoff/java/first/ZipCreator.java"); zOut.putNextEntry(zipEntry); try (FileInputStream in = new FileInputStream("src/ua/in/iwanoff/java/first/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.in.iwanoff.java.first; 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 Working with the File System
The java.io
package provides the ability to work both with file contents and with the file system as a whole. This feature implements the File
class. To create an object of this class, the (full or relative) path to the file should be passed as a parameter of the constructor. For example:
File dir = new File("C:\\Users"); File currentDir = new File("."); // Project folder (current)
The File
class contains methods for obtaining a list of files of the specified folder (list()
, listFiles()
), obtaining and modifying the file attributes (setLastModified()
, setReadOnly()
, isHidden()
, isDirectory()
, etc.), creating a new file (createNewFile()
, createTempFile()
), create directories (mkdir()
), delete files and folders (delete()
) and many more. The work of some of these methods can be demonstrated by the following example:
package ua.in.iwanoff.java.first; import java.io.*; import java.util.*; public class FileTest { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); System.out.print("Enter the name of the folder you want to create:"); String dirName = scanner.next(); File dir = new File(dirName); // Create a new folder: if (!dir.mkdir()) { System.out.println("Unable to create a folder!"); return; } // Create a new file inside a new folder: File file = new File(dir + "\\temp.txt"); file.createNewFile(); // Display a list of files in the folder: System.out.println(Arrays.asList(dir.list())); file.delete(); // delete the file dir.delete(); // delete the folder } }
The list()
function without parameters allows you to obtain an array of strings that contains all files and subdirectories of a folder defined when creating an object of type File
. You can see relative filenames (without paths). In the following example, we get a list of files and subdirectories of the folder whose name is entered from the keyboard:
package ua.in.iwanoff.java.first; import java.io.File; import java.io.FilenameFilter; import java.util.Scanner; public class ListOfFiles { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print("Enter folder name:"); String dirName = scanner.next(); File dir = new File(dirName); if (!dir.isDirectory()) { System.out.println("Invalid folder name!"); return; } String[] list = dir.list(); for(String name : list) { System.out.println(name); } } }
Unlike list()
, the listFiles()
function returns an array of objects of type File
. This provides additional features: getting full file names, checking file attributes, working with folders, etc. These additional features will be shown in the following example:
File[] list = dir.listFiles(); // Outputs file data in the default form: for(File file : list) { System.out.println(file); } // The full path is displayed: for(File file : list) { System.out.println(file.getCanonicalPath()); } // Only subdirectories are displayed: for(File file : list) { if (file.isDirectory()) System.out.println(file.getCanonicalPath()); }
To determine the filter mask, you should create an object of a class that implements the FilenameFilter
interface. In the following example, we get a list of files and subdirectories whose names begin with the letter 's'
:
String[] list = dir.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.toLowerCase().charAt(0) == 's'; } }); for(String name : list) { System.out.println(name); }
A similar parameter of type FilenameFilter
can be applied to the listFiles()
function.
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.in.iwanoff.java.first; 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 comareByAbsValues()
, in which a local class is created and its object is returned. The source code will look like this:
package ua.in.iwanoff.java.first; 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> comareByAbsValues() { // 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, comareByAbsValues()); 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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.in.iwanoff.java.first; 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 Laboratory training # 5 of the course "Fundamentals of programming" of the previous semester, but data on the population census are read from file, and the results of sorting by population are recorded in another file. The first of the classes will work with plain text files, and the second one will use binary serialization and deserialization.
To work with censuses, we need the previously created AbstractCensus
and AbstractCountry
classes. They are located in the package ua.in.iwanoff.oop.first
(see Example 3.4 given in Laboratory training # 1 of the course "Object-Oriented Programming"). The best solution is not to create a new project, but to add a new package to the previously created project, which will allow you to refer to previously created classes.
To unify the work of the program, it is useful for us to create an interface that declares the corresponding read and write operations. This interface, for example, can be as follows:
package ua.in.iwanoff.java.first; public interface FileIO { void readFromFile(String fileName) throws Exception; void writeToFile(String fileName) throws Exception; }
The TextFileCountry
class for working with text files will be derived from the CountryWithArray
class (implemented as an example of Laboratory training # 1 of the course "Object-Oriented Programming") and implement the FileIO
interface:
package ua.in.iwanoff.java.first; import ua.in.iwanoff.oop.first.AbstractCensus; import ua.in.iwanoff.oop.first.CensusWithData; import ua.in.iwanoff.oop.first.CountryWithArray; import java.io.*; import java.util.InputMismatchException; import java.util.Scanner; public class TextFileCountry extends CountryWithArray implements FileIO { @Override public void readFromFile(String fileName) throws FileNotFoundException, InputMismatchException { 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(); addCensus(new CensusWithData(year, population, comments)); } } } @Override public void writeToFile(String fileName) throws IOException { try (PrintWriter out = new PrintWriter(new FileWriter(fileName))) { out.println(getName() + " " + getArea()); for (AbstractCensus census : getCensuses()) { out.print(census.getYear() + " " + census.getPopulation()); out.println(census.getComments()); } } } public static void main(String[] args) { TextFileCountry country = new TextFileCountry(); try { country.readFromFile("Ukraine.txt"); country.testCountry(); country.writeToFile("ByComments.txt"); } 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"); } } }
Before program launching we'll create a file with the source data The source data format assumes that the first line contains data about country name and area, then the separate lines contain year of census, population, and comments, separated by spaces. For instance, the following source file can be prepared (Ukraine.txt
):
Ukraine 603628 1959 41869000 The first postwar census 1970 47126500 Population increases 1979 49754600 No comments 1989 51706700 The last soviet census 2001 48475100 The first census in the independent Ukraine
This file can be created by various text editors and placed into the root folder of the project. It is important to specify a UTF-8 code page.
After executing the main()
function of the TextFileCountry
class, a ByComments.txt
file will appear in the project's root folder. The census data in this file will be sorted by the alphabet of comments.
Now we can implement a version with binary serialization and deserialization. We create a SerializedCensus
class that implements the Serializable
interface:
package ua.in.iwanoff.java.first; import ua.in.iwanoff.oop.first.AbstractCensus; import java.io.Serializable; public class SerializedCensus extends AbstractCensus implements Serializable { private static final long serialVersionUID = -4998473980228618354L; private int year; private int population; private String comments; public SerializedCensus() { } public SerializedCensus(int year, int population, String comments) { this.year = year; this.population = population; this.comments = comments; } @Override public int getYear() { return year; } @Override public void setYear(int year) { this.year = year; } @Override public int getPopulation() { return population; } @Override public void setPopulation(int population) { this.population = population; } @Override public String getComments() { return comments; } @Override public void setComments(String comments) { this.comments = comments; } }
The SerializedCountry
class also implements the Serializable
interface, and, moreover, the FileIO
interface. Since the object cannot serialize itself, the function readFromFile()
of the FileIO
interface cannot be implemented. The body of this function will contain the throwing of corresponding standard exception. Deserialization will be performed in a separate static function deserialize()
. The code will be as follows:
package ua.in.iwanoff.java.first; import ua.in.iwanoff.oop.first.AbstractCensus; import ua.in.iwanoff.oop.first.AbstractCountry; import ua.in.iwanoff.oop.first.CompareByComments; import ua.in.iwanoff.oop.first.CountryWithArray; import java.io.*; import java.util.Arrays; public class SerializedCountry extends AbstractCountry implements Serializable, FileIO { private static final long serialVersionUID = 5884026597888745689L; private String name; private double area; private SerializedCensus[] censuses; @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @Override public double getArea() { return area; } @Override public void setArea(double area) { this.area = area; } @Override public AbstractCensus getCensus(int i) { return censuses[i]; } @Override public void setCensus(int i, AbstractCensus census) { censuses[i] = (SerializedCensus) census; } @Override public boolean addCensus(AbstractCensus census) { if (getCensuses() != null) { for (AbstractCensus c : getCensuses()) { if (c.equals(census)) { return false; } } } setCensuses(addToArray(getCensuses(), census)); return true; } @Override public boolean addCensus(int year, int population, String comments) { AbstractCensus census = new SerializedCensus(year, population, comments); return addCensus(census); } @Override public int censusesCount() { return censuses.length; } @Override public void clearCensuses() { censuses = null; } @Override public AbstractCensus[] getCensuses() { return censuses; } @Override public void setCensuses(AbstractCensus[] censuses) { this.censuses = new SerializedCensus[censuses.length]; for (int i = 0; i < censuses.length; i++) { this.censuses[i] = (SerializedCensus) censuses[i]; } } @Override public void sortByPopulation() { Arrays.sort(censuses); } @Override public void sortByComments() { Arrays.sort(censuses, new CompareByComments()); } @Override public void readFromFile(String fileName) throws Exception { throw new UnsupportedOperationException(); } public static SerializedCountry deserialize(String fileName) throws ClassNotFoundException, IOException { ObjectInputStream in = new ObjectInputStream(new FileInputStream(fileName)); SerializedCountry country = (SerializedCountry) in.readObject(); in.close(); return country; } @Override public void writeToFile(String fileName) throws IOException { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(fileName)); out.writeObject(this); out.close(); } }
Separately, we create the CountrySerializer
class, in the main()
function we create the country and perform its serialization. The code will be as follows:
package ua.in.iwanoff.java.first; import java.io.IOException; public class CountrySerializer { public static void main(String[] args) { SerializedCountry country = new SerializedCountry(); country.createCountry(); try { country.writeToFile("Ukraine.dat"); } catch (IOException e) { System.err.println("Write failed"); e.printStackTrace(); } } }
After executing the program, a file called Ukraine.dat
is created in the root directory of the project.
For desertification and testing, we create a separate CountryDeserializer
class:
package ua.in.iwanoff.java.first; import java.io.FileNotFoundException; import java.io.IOException; public class CountryDeserializer { public static void main(String[] args) { SerializedCountry country = null; try { country = SerializedCountry.deserialize("Ukraine.dat"); country.testCountry(); country.writeToFile("ByComments.dat"); } catch (FileNotFoundException e) { System.err.println("Read failed"); e.printStackTrace(); } catch (IOException e) { System.err.println("Write failed"); e.printStackTrace(); } catch (ClassNotFoundException e) { System.err.println("Deserialization error"); e.printStackTrace(); } } }
After executing this program, we can find the ByComments.dat
file in the project's root directory.
4 Exercises
- Read real values from a text file (to the end of file), find their sum and output this sum to another text file.
- 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.
- 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.
- 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.
- 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.
- Create classes Faculty and Institute (with an array of faculties as a field). Create objects, implement their binary serialization and deserialization.
5 Quiz
- What are the main components of the Java SE Platform?
- What is the difference between byte streams and character streams in their application?
- What classes provide the work with text files and binary files?
- Why do you need to close files
?
- Is it possible to open multiple I/O streams at the same time?
- What is the way of automatic closing of I/O streams?
- What are the advantages of using
RandomAccessFile
class? - What are the
DataOutputStream
andDataInputStream
data files used for? What are their advantages and disadvantages? - What is serialization and what is it used for?
- What are the advantages and disadvantages of serialization?
- What functions should be defined for the implementation of the
java.io.Serializable
interface? - What is the
transient
modifier used for? - How does Java work with archives?
- Is it possible to create an archive with several files inside?
- How to define the term "file system"?
- What are the typical functions for working with the file system?
- What tools provides Java to work with the file system?
- How to get the file attributes by means of
java.io.File
class? - What is the difference between
list()
andlistFiles()
methods? - How to copy files by means of
java.io
?