Knowledge of Java I/O flow

Posted by gio2k on Wed, 05 Jan 2022 14:47:49 +0100

IO is input / output. The Java IO class library constructs a set of I/O system based on the abstract basic classes InputStream and OutputStream, which mainly solves the problems of reading data from the data source and writing data to the destination. We put The data source and destination can be understood as both ends of the IO stream. Of course, usually, these two ends may be files or network connections.

Let's use the following diagram to deepen our understanding:

Read data from a data source into program memory through an InputStream stream object

Of course, we reverse the flow of the above figure, which is the schematic diagram of OutputStream.

In fact, in addition to the InputStream/OutputStream system for byte stream, the Java IO class library also provides a Reader/Writer system for character stream. The Reader/Writer inheritance structure is mainly for internationalization because it can better handle 16 bit Unicode characters.

In learning, these two sets of IO stream processing systems can be compared with reference to learning, because there are many similarities.

1. Understand the overall design

At the beginning of writing IO code, I was always confused by various IO stream classes. When can I remember so many IO related classes and methods.

In fact, as long as we master the overall design idea of IO class library and understand its hierarchy, it will be very clear. It's good to know when to use which stream objects to combine the desired functions. For API, you can check the manual.

First, the flow direction of a stream can be divided into input stream or Reader, output stream or Writer. Any class derived from InputStream or Reader has a read() basic method to read a single byte or byte array; Any class derived from OutputStream or Writer contains the basic method write(), which is used to write a single byte or an array of bytes.

From the perspective of operating bytes or characters, there are classes for byte stream, which basically end with XxxStream, and classes for character stream end with XxxReader or XxxWriter. Of course, these two types of streams can be transformed. There are two classes of transformed streams, which will be mentioned later.

Generally, when using IO streams, the following codes are similar:

FileInputStream inputStream = new FileInputStream(new File("a.txt"));
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);

This is actually the use of decorator mode. Decorator mode is used in IO stream system to package various function stream classes.

In the Java IO stream system, FilterInputStream/FilterOutStream and FilterReader/FilterWriter are the interface classes of decorator mode, and some function stream classes are wrapped down from this class. There are DataInputStream, BufferedInputStream, LineNumberInputStream, PushbackInputStream, etc. of course, there are also output function stream classes; Character oriented function flow class, etc.

The following figures describe the inheritance architecture of the entire IO flow

InputStream stream system

OutputStream stream system

Reader system

Writer system

2. File is actually a tool class

The File class actually represents not only a File, but also a group of files in a directory (representing a File path). Let's take a look at some of the most commonly used methods in the File class

 1 File.delete() Delete a file or folder directory.
 2 File.createNewFile() Create a new empty file.
 3 File.mkdir() Create a new empty folder.
 4 File.list() Gets the file and folder names in the specified directory.
 5 File.listFiles() Gets the file and folder objects in the specified directory.
 6 File.exists() Does the file or folder exist
 8 String   getAbsolutePath()   // Get absolute path
 9 long   getFreeSpace()       // Returns the number of unallocated bytes in the partition.
10 String   getName()         // Returns the name of the file or folder.
11 String   getParent()         // Returns the pathname string of the parent directory; null if no parent directory is specified.
12 File   getParentFile()      // Returns the parent directory File object
13 String   getPath()         // Returns a pathname string.
14 long   getTotalSpace()      // Returns the size of this file partition.
15 long   getUsableSpace()    //Returns the number of bytes occupied.
16 int   hashCode()             //File hash code.
17 long   lastModified()       // Returns the time when the file was last modified.
18 long   length()          // Gets the length in bytes.
19 boolean canRead()  //Judge whether it is readable
20 boolean canWrite()  //Determine whether it is writable
21 boolean isHidden()  //Determine whether to hide
24 // Member function
25 static File[]    listRoots()    // Lists the available file system roots.
26 boolean    renameTo(File dest)    // rename
27 boolean    setExecutable(boolean executable)    // Set execution permissions.
28 boolean    setExecutable(boolean executable, boolean ownerOnly)    // Set execution permissions for all other users.
29 boolean    setLastModified(long time)       // Set the last modification time.
30 boolean    setReadable(boolean readable)    // Set read permissions.
31 boolean    setReadable(boolean readable, boolean ownerOnly)    // Set read permissions for all other users.
32 boolean    setWritable(boolean writable)    // Set write permissions.
33 boolean    setWritable(boolean writable, boolean ownerOnly)    // Set write permissions for all users.

It should be noted that the File path separator table is different in different systems, such as "\" in Windows and "/" in Linux. The File class provides us with an abstract representation of File Separator, which shields the differences in the system layer. Therefore, do not use a representative path such as "\" in the code, which may cause code execution errors on the Linux platform.

Here are some examples:

According to the incoming rules, the File object array composed of all the files in the directory is traversed

 1 public class Directory {
 2    public static File[] getLocalFiles(File dir, final String regex){
 3        return dir.listFiles(new FilenameFilter() {
 4            private Pattern pattern = Pattern.compile(regex);
 5            public boolean accept(File dir, String name) {
 6                return pattern.matcher(new File(name).getName()).matches();
 7            }
 8        });
 9    }
11    // Overloading methods 
12    public static File[] getLocalFiles(String path, final String regex){
13        return getLocalFiles(new File(path),regex);
14    }
16    public static void main(String[] args) {
17        String dir = "d:";
18        File[] files = Directory.getLocalFiles(dir,".*\\.txt");
19        for(File file : files){
20            System.out.println(file.getAbsolutePath());
21        }
22    }
23 }

Output results:

2d:\\New text document.txt

In the above code, dir Listfiles (filenamefilter) is an implementation of the policy pattern and uses anonymous inner classes.

The above examples are examples in Java programming ideas. Each code example in this book is very classic. Bruce Eckel's application of object-oriented ideas is perfect, which is very worth tasting.

3. InputStream and OutputStream

InputStream is an input stream. As mentioned earlier, it is a stream object used when reading data from a data source object into program content. By looking at the source code of InputStream, we know that it is an abstract class,

1 public abstract class InputStream  extends Object  implements Closeable

Some basic input flow methods are provided:

1 //Read a byte from the data and return it. When the end of the stream is encountered, return - 1
2 abstract int read() 
4 //Read in a byte array, and return the actual number of bytes read in. Read in b.length bytes at most, and return - 1 when encountering the end of the stream
5 int read(byte[] b)
7 // Read in a byte array and return the actual number of bytes read in or - 1 when the end is encountered
8 //b: Represents the array of data read in, off: represents the offset in B of the position where the first read in byte should be placed, len: the maximum number of read in bytes
9 int read(byte[],int off,int len)
11 // Returns the current number of bytes that can be read. If it is read from a network connection, this method should be used with caution,
12 int available() 
14 //Skip n bytes in the input stream and return the actual number of skipped bytes
15 long skip(long n)
17 //Mark the current position in the input stream
18 void mark(int readlimit) 
20 //Judge whether the flow supports marking and returns true
21 boolean markSupported() 
23 // The last tag is returned, and subsequent calls to read will reread these bytes.
24 void reset() 
26 //It is very important to close the input stream. The stream must be closed after use
27 void close()

The streams inherited directly from InputStream basically correspond to each data source type.

ByteArrayInputStreamUse byte array as InputStream
StringBufferInputStreamConvert String to InputStream
FileInputStreamRead content from file
PipedInputStreamGenerates data for writing to the associated PipedOutputStream. Realize pipelining
SequenceInputStreamConverts two or more InputStream objects to a single InputStream
FilterInputStreamAbstract class is mainly used as the interface class of "decorator" to realize other function flows

OutputStream is an abstraction of the output stream. It writes the data in the program memory to the destination (that is, the end receiving the data). See the signature of the following class:

1 public abstract class OutputStream implements Closeable, Flushable {}

It provides a basic method, which is much simpler than the input stream. The main methods are write (several overloaded methods), flush flush and close.

 1 // Write out one byte of data
 2 abstract void write(int n)
 4 // Write bytes to data b
 5 void write(byte[] b)
 7 // Write out bytes to array b, off: represents the offset of the first write out byte in b, len: the maximum number of write out bytes
 8 void write(byte[] b, int off, int len)
10 //Flush the output stream, that is, send all buffered data to the destination
11 void flush()
13 // Close output stream
14 void close()

Similarly, OutputStream also provides some basic stream implementations, which can also correspond to a specific destination (receiver), such as output to byte array or output to file / pipeline.

ByteArrayOutputStreamCreate a buffer in memory where all data sent to the "stream" is placed
FileOutputStreamWrite data to file
PipedOutputStreamUse with PipedInputStream. Realize pipelining
FilterOutputStreamAbstract class is mainly used as the interface class of "decorator" to realize other function flows

4. Flow packaged with decorator

The Java IO stream architecture uses the decorator pattern to add additional functionality to the underlying I / O streams. This additional function may be: the stream can be buffered to improve performance, the stream can read and write basic data types, etc.

These stream types that add functionality through the decorator pattern are extended from the FilterInputStream and FilterOutputStream abstract classes. You can go back to the diagrams mentioned at the beginning of the article to deepen your impression.

FilterInputStream type

DataInputStreamUsed in conjunction with DataOutputStream, the stream can read basic data types such as int char long
BufferedInputStreamUsing buffers is mainly to improve performance
LineNumberInputStreamTo track the line number in the input stream, you can use getLineNumber and setLineNumber(int)
PushbackInputStreamThis enables the stream to pop up a "one byte buffer", which can fallback the last character read

FilterOutStream type

DataOutputStreamUsed in conjunction with DataInputStream, the stream can write basic data types such as int char long
PrintStreamUsed to produce formatted output
BufferedOutputStreamUsing the buffer, you can call flush() to empty the buffer

In most cases, when we use streams, we use input streams and output streams together. The purpose is to transfer and store data. What's the use of separate read() for us? What can we do with reading a byte? Right. Therefore, we should understand that the use of streams is to combine or use function streams to transfer or store data.

5. Reader and Writer

Reader is the base class for all readers in Java IO. Reader is similar to InputStream, except that reader is based on characters rather than bytes.

Writer is the base class for all writers in Java IO. Similar to the relationship between Reader and InputStream, writer is based on characters rather than bytes. Writer is used to write text and OutputStream is used to write bytes.

The basic function classes of Reader and Writer can be learned by comparing InputStream and OutputStream.

Byte orientedCharacter oriented
StringBufferInputStream (deprecated)StringReader
No corresponding classStringWriter

There are two "adapter" stream types that can convert a byte stream into a byte stream. This means that InputStreamReader can convert InputStream into Reader and OutputStreamWriter can convert OutputStream into Writer.

Adapter class, byte stream, character stream

Of course, there is also a decorator implementation similar to byte stream, adding additional functions or behavior to character stream. These function character stream classes mainly include:

  • BufferedReader
  • BufferedWriter
  • PrintWriter
  • LineNumberReader
  • PushbackReader

6. I/O flow in system class

Think about your first Java program? If I guess correctly, it should be hello world.

1 System.out.println("hello world")

Simple to outrageous, let's talk about standard input / output streams today.

In the standard IO model, Java provides system in,System.out and system error.

Start with system In, look at the source code

1 public final static InputStream in

Is a static field and an unwrapped InputStream. Usually, we will use BufferedReader to wrap and read the input line by line. Here we will use the adapter stream InputStreamReader mentioned earlier.

1 public class SystemInReader {
2    public static void main(String[] args) throws IOException {
3        BufferedReader reader = new BufferedReader(new InputStreamReader(;
4        String s;
5        while ((s = reader.readLine()) != null && s.length() != 0 ){
6            System.out.println(s);
7        }
8    }
9 }

The program will wait for us to input, input what, and then output. Enter an empty string to end.


System.out is a PrintStream stream. System.out usually outputs the data you write to the console. System.out is usually used only on console programs like command line tools. System.out is also often used to print program debugging information (although it may not be the best way to get program debugging information).

System.err is a PrintStream stream. System.err and system Out works similarly, but it is more used to print error text.

You can redirect these system streams

Although system in, System. out,System. Err these three streams are Java The static members of the lang. system class, which have been initialized in advance when the JVM starts, can still be changed.

You can redirect using setIn(InputStream), setOut(PrintStream), setErr(PrintStream). For example, you can redirect the output of the console to a file.

1 OutputStream output = new FileOutputStream("d:/system.out.txt");
2 PrintStream printOut = new PrintStream(output);
3 System.setOut(printOut);

7. Compression (ZIP document)

The Java IO class library supports reading and writing compressed data streams. We can compress one or a batch of files into a zip document. These compression related stream classes are processed by bytes. Let's take a look at the relevant stream classes designed for compression and decompression.

Compression classfunction
CheckedInputStreamgetCheckSum() can generate a checksum (not just decompression) for any InputStream
CheckedOutputStreamgetCheckSum() can generate a checksum (not just compression) for any OutputStream
DeflaterOutputStreamBase class of compressed class
ZipOutputStreamIt inherits from deflatoroutputstream and compresses the data into Zip file format
GZIPOutputStreamIt inherits from deflatoroutputstream and compresses the data into GZIP file format
InflaterInputStreamBase class of decompression class
ZipInputStreamIt inherits from the InflaterInputStream and decompresses the data in Zip file format
GZIPInputStreamIt inherits from the InflaterInputStream and decompresses the data in GZIP file format

CheckedInputStream and CheckedOutputStream in the table are generally used together with Zip compression and decompression process, mainly to ensure the correctness of data packets in our compression and decompression process and obtain data that has not been tampered with in the middle.

Take checkdinputstream as an example. Its constructor needs to pass in a Checksum type:

1    public CheckedInputStream(InputStream in, Checksum cksum) {
2        super(in);
3        this.cksum = cksum;
4    }

Checksum is an interface. You can see that the policy mode is used here, and the specific verification algorithm can be selected. The Java class library provides me with two checksum algorithms: adler32 and CRC32. Adler32 may be better in performance, but CRC32 may be more accurate. Each has its own advantages and disadvantages.

OK, next, let's look at the specific use of compressed / decompressed streams.

7.1 compress multiple files into zip packages

 1 public class ZipFileUtils {
 2    public static void compressFiles(File[] files, String zipPath) throws IOException {
 4        // Define the file output stream, indicating that it is to be compressed into a zip file
 5        FileOutputStream f = new FileOutputStream(zipPath);
 7        // Add verification function to output stream
 8        CheckedOutputStream checkedOs = new CheckedOutputStream(f,new Adler32());
10        // To define the output stream in zip format, you should understand that you have been using decorator mode to add functions to the stream
11        // ZipOutputStream is also inherited from FilterOutputStream
12        ZipOutputStream zipOut = new ZipOutputStream(checkedOs);
14        // Add buffer function to improve performance
15        BufferedOutputStream buffOut = new BufferedOutputStream(zipOut);
17        //For the compressed output stream, we can set a comment
18        zipOut.setComment("zip test");
20        // The following is the process of reading a batch of Files from the Files [] array and writing them to the zip package
21        for (File file : files){
23            // Create a buffer stream for reading files, also in decorator mode, using BufferedReader
24            // Wrapped FileReader
25            BufferedReader bfReadr = new BufferedReader(new FileReader(file));
27            // A file object is represented by a ZipEntry in the zip stream and added to the zip stream using putNextEntry
28            zipOut.putNextEntry(new ZipEntry(file.getName()));
30            int c;
31            while ((c = != -1){
32                buffOut.write(c);
33            }
35            // Note that it should be closed here
36            bfReadr.close();
37            buffOut.flush();
38        }
39        buffOut.close();
40    }
42    public static void main(String[] args) throws IOException {
43        String dir = "d:";
44        String zipPath = "d:/";
45        File[] files = Directory.getLocalFiles(dir,".*\\.txt");
46        ZipFileUtils.compressFiles(files, zipPath);
47    }
48 }

In the main function, we use the Directory tool class in the chapter "File is actually a tool class" in this article.

7.2 unzip the zip package to the target folder

 1    public static void unConpressZip(String zipPath, String destPath) throws IOException {
 2        if(!destPath.endsWith(File.separator)){
 3            destPath = destPath + File.separator;
 4            File file = new File(destPath);
 5            if(!file.exists()){
 6                file.mkdirs();
 7            }
 8        }
 9        // Create a new file input stream class,
10        FileInputStream fis = new FileInputStream(zipPath);
12        // Add verification function to input stream
13        CheckedInputStream checkedIns = new CheckedInputStream(fis,new Adler32());
15        // Create a new zip output stream because the file in ZIP format is read
16        ZipInputStream zipIn = new ZipInputStream(checkedIns);
18        // Add buffer stream function to improve performance
19        BufferedInputStream buffIn = new BufferedInputStream(zipIn);
21        // Read in each ZipEntry object from the zip input stream
22        ZipEntry zipEntry;
23        while ((zipEntry = zipIn.getNextEntry()) != null){
24            System.out.println("Decompressing" + zipEntry);
26            // Write the extracted file to the destination folder
27            int size;
28            byte[] buffer = new byte[1024];
29            FileOutputStream fos = new FileOutputStream(destPath + zipEntry.getName());
30            BufferedOutputStream bos = new BufferedOutputStream(fos, buffer.length);
31            while ((size =, 0, buffer.length)) != -1) {
32                bos.write(buffer, 0, size);
33            }
34            bos.flush();
35            bos.close();
36        }
37        buffIn.close();
39        // Output checksum
40        System.out.println("Checksum:" + checkedIns.getChecksum().getValue());
41    }
43    // Called directly in the main function
44    public static void main(String[] args) throws IOException {
45        String dir = "d:";
46        String zipPath = "d:/";
47//        File[] files = Directory.getLocalFiles(dir,".*\\.txt");
48//        ZipFileUtils.compressFiles(files, zipPath);
50        ZipFileUtils.unConpressZip(zipPath,"F:/ziptest");
51    }

There is another easier way to unzip the zip package, using the ZipFile object. The entries() method of this object directly returns an enumeration of ZipEntry type. Look at the following code snippet:

1        ZipFile zipFile = new ZipFile("");
2        Enumeration e = zipFile.entries();
3        while (e.hasMoreElements()){
4            ZipEntry zipEntry = (ZipEntry) e.nextElement();
5            System.out.println("file:" + zipEntry);
6        }

8. Object serialization

8.1 what is serialization and deserialization?

Serialization is the process of converting an object into a byte sequence, and deserialization is the process of reconstituting a byte sequence into an object.

8.2 why is there an object serialization mechanism

The object in the program actually exists in memory. When we close the JVM, it will not continue to exist anyway. Is there a mechanism to make objects "persistent"? The serialization mechanism provides a way for you to input the byte stream of object serialization into a file and save it on disk.

Another meaning of serialization mechanism is that we can transfer objects through the network. For remote method call (RMI) in Java, the bottom layer needs the guarantee of serialization mechanism.

8.3 how to implement serialization and deserialization in Java

First, the object to be serialized must implement a Serializable interface (this is an identification interface and does not include any methods)

1 public interface Serializable {
2 }

The second is to use two object stream classes: ObjectInputStream and ObjectOutputStream. It mainly uses the readObject method of ObjectInputStream object to read in the object and the writeObject method of ObjectOutputStream to write the object to the stream

Next, we write a simple pojo object to the file through the serialization mechanism and read it into the program memory again.

 1 public class User implements Serializable {
 2    private String name;
 3    private int age;
 5    public User(String name, int age) {
 6 = name;
 7        this.age = age;
 8    }
10    @Override
11    public String toString() {
12        return "User{" +
13                "name='" + name + '\'' +
14                ", age='" + age + '\'' +
15                '}';
16    }
18    public static void main(String[] args) throws IOException, ClassNotFoundException {
19        User user = new User("Second battalion commander",18);
20        ObjectOutputStream objectOps = new ObjectOutputStream(new FileOutputStream("f:/user.out"));
21        objectOps.writeObject(user);
22        objectOps.close();
24        // Then take the object out of the file
25        ObjectInputStream objectIns = new ObjectInputStream(new FileInputStream("f:/user.out"));
27        // There's going to be a strong turn here
28        User user1 = (User) objectIns.readObject();
29        System.out.println(user1);
30        objectIns.close();
31    }
32 }

Program running results:

User{name='Second battalion commander', age='18'}

8.4 data that you do not want to serialize is masked using the transient keyword

If the user object above has a password field that belongs to sensitive information, it cannot be serialized, but the object that implements the Serializable interface will automatically serialize all data fields. What should we do? Just add the keyword transient to the password field.

1 private transient String password;

The serialization mechanism is briefly introduced here. This is Java's native serialization. There are many serialization protocols available on the market, such as Json, fastjason, Thrift, Hessian, protobuf, etc

9. Typical usage of I / O flow

There are many kinds of IO streams. I/O stream classes can be combined in different ways, but usually we use several combinations. The footwall counts the typical usage of several I/O streams by example.

9.1 buffered input file

 1 public class BufferedInutFile {
 2    public static String readFile(String fileName) throws IOException {
 3        BufferedReader bf = new BufferedReader(new FileReader(fileName));
 4        String s;
 6        // The contents read here exist in StringBuilder. Of course, other processing can also be done
 7        StringBuilder sb = new StringBuilder();
 8        while ((s = bf.readLine()) != null){
 9            sb.append(s + "\n");
10        }
11        bf.close();
12        return sb.toString();
13    }
15    public static void main(String[] args) throws IOException {
16        System.out.println(BufferedInutFile.readFile("d:/1.txt"));
17    }
18 }

9.2 formatted memory input

To read formatted data, you can use DataInputStream.

 1 public class FormattedMemoryInput {
 2    public static void main(String[] args) throws IOException {
 3        try {
 4            DataInputStream dataIns = new DataInputStream(
 5                    new ByteArrayInputStream(BufferedInutFile.readFile("f:/").getBytes()));
 6            while (true){
 7                System.out.print((char) dataIns.readByte());
 8            }
 9        } catch (EOFException e) {
10            System.err.println("End of stream");
11        }
12    }
13 }

The above program will output all the code of the current class itself on the console and throw an EOFException exception. The reason why the exception is thrown is that it has reached the end of the reservation and is still reading data. Here you can use available() to determine how many characters are available.

 1 package com.herp.pattern.strategy;
 3 import;
 4 import;
 5 import;
 7 public class FormattedMemoryInput {
 8    public static void main(String[] args) throws IOException {
 9        DataInputStream dataIns = new DataInputStream(
10                new ByteArrayInputStream(BufferedInutFile.readFile("").getBytes()));
11        while (true){
12            System.out.println((char) dataIns.readByte());
13        }
14    }
15 }

9.3 basic file output

The FileWriter object can write data to a file. First, create a FileWriter associated with the specified file, and then wrap it with BufferedWriter to provide buffering function. In order to provide formatting mechanism, it is decorated as PrintWriter.

 1 public class BasicFileOutput {
 2    static String file = "BasicFileOutput.out";
 4    public static void main(String[] args) throws IOException {
 5        BufferedReader in = new BufferedReader(new StringReader(BufferedInutFile.readFile("f:/")));
 6        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
 8        int lineCount = 1;
 9        String s;
10        while ((s = in.readLine()) != null){
11            out.println(lineCount ++ + ": " + s);
12        }
13        out.close();
14        in.close();
15    }
16 }

Here is the basic file output Out file, you can see that we added the line number through the code byte

 1: package com.herp.pattern.strategy;
 3: import*;
 5: public class BasicFileOutput {
 6:     static String file = "BasicFileOutput.out";
 8:     public static void main(String[] args) throws IOException {
 9:         BufferedReader in = new BufferedReader(new StringReader(BufferedInutFile.readFile("f:/BasicFileOutput")));
10:         PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
12:         int lineCount = 1;
13:         String s;
14:         while ((s = in.readLine()) != null){
15:             out.println(lineCount ++ + ": " + s);
16:         }
17:         out.close();
18:         in.close();
19:     }
20: }

9.4 data storage and recovery

In order to output data that can be recovered by another "stream", we need to write data using DataOutputStream, and then recover data using DataInputStream. Of course, these streams can be in any form (the form here is actually the types at both ends of the stream we mentioned earlier), such as files.

 1 public class StoringAndRecoveringData {
 2    public static void main(String[] args) throws IOException {
 3        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("data.txt")));
 4        out.writeDouble(3.1415926);
 5        out.writeUTF("Three company walk");
 6        out.writeInt(125);
 7        out.writeUTF("Point praise and attention");
 8        out.close();
10        DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("data.txt")));
11        System.out.println(in.readDouble());
12        System.out.println(in.readUTF());
13        System.out.println(in.readInt());
14        System.out.println(in.readUTF());
15        in.close();
16    }
17 }

Output results:

 Three company walk
 Point praise and attention

Note that we use writeUTF() and readUTF() to write and read strings.

Topics: Java Back-end