Tinking in Java --- NIO of Java and Object serialization
I/O in the previous blog is called classic I/O, because most of them have been created since Java1.0. Today, this blog is about NIO, therefore, NIO is a series of improved input/output processing functions provided by Java starting from JDK 1.4. These New functions are collectively referred to as New IO (NIO ). Another concept of object serialization refers to converting objects that implement the Serializable interface into a byte sequence and converting the byte sequence into the original object later. In this way, the object can be written to the disk or transmitted over the network. The following is a summary of the two content.
1. Java NIO
Java NIO is more efficient because the structure it uses is closer to the operating system's IO method:Use channels and buffers.There is data in the channel, but we cannot deal with it directly. Whether it is retrieving data from the channel or putting data, we must use a buffer, more strictly, the buffer stores the most primitive bytes of data rather than other types. The Channel class corresponds to the Channel we mentioned above, while the Buffer class corresponds to the Buffer, so we need to understand these two classes.
(1). Buffer class
From the underlying data structure, the Buffer is like an array, where we can store multiple data of the same type. The Buffer class is an abstract array. Its most common subclass is ByteBuffer. The minimum unit of access for this class is byte, which is used to deal with channels. Of course, in addition to ByteBuffer, other basic types (except boolean) all have their own corresponding Buffer: CharBuffer, protocol Buffer, IntBuffer, LongBuffer, FloatBuffer, and DoubleBuffer. Using these types, we can easily put the basic types of data into ByteBuffer. the Buffer class also has a subclass MappedByteBuffer, which is used to indicate the results of the Channel to obtain part or all of the disk files.
Buffer has three important concepts: capacity, limit, and positiion ).
Capacity refers to the size of the Buffer, that is, the maximum amount of data that the Buffer can load.
The boundary refers to the value that is added to the last data position of the current load, indicating that the first data location should not be read or written.
Location indicates the index of the next buffer location that can be read or written.
The main function of Buffer is to load data and then output data. Therefore, we need to know the specific process: first, the positiion of Buffer is 0, and limit is equal to capacity. The program can use put () the method puts some data into the Buffer (or extracts some data from the Channel). In this process, the position will move backwards. After the Buffer load ends, call the flip () method of the Buffer to prepare the output data. This method sets limit to position and limit. When the output data is complete, the Buffer calls the clear () method. The clear () method does not affect all the data. It only sets the position to 0 and the limit to capacity, in this way, we are ready to input data to the Buffer.
Note that the sub-classes of the Buffer do not have constructor, so you cannot explicitly declare a Buffer. The following code demonstrates the basic usage of CharBuffer:
Package lkl; import java. nio. *; public class BufferTest {public static void main (String [] args) {// use the static method to create a CharBuffer System. out. println (after the buffer is created:); CharBuffer buffer = CharBuffer. allocate (10); System. out. println (position: + buffer. position (); System. out. println (limit: + buffer. limit (); System. out. println (capacity: + buffer. capacity (); // put three characters in the buffer. put (a); buffer. put (B); buffer. put (c); // prepare the System for using the buffer. out. println (); System. out. println (after loading data to the buffer and calling the flip () method:); buffer. flip (); System. out. println (position: + buffer. position (); System. out. println (limit: + buffer. limit (); System. out. println (capacity: + buffer. capacity (); // read the elements in the buffer, the absolute and relative methods do not change the position pointer // The relative method will move the position pointer back to a System. out. println (buffer. get (); System. out. println (buffer. get (2); System. out. println (); System. out. println (after calling clear ():); // call the clear () method to prepare for re-entering data into the buffer // but this method only moves the positions of each pointer, instead of clearing the buffer. clear (); System. out. println (position: + buffer. position (); System. out. println (limit: + buffer. limit (); System. out. println (capacity: + buffer. capacity (); // The clear () method does not clear the buffer. // You can also access the Content System in the buffer in an absolute way. out. println (buffer. get (2 ));}}
Generally, ByteBuffer is used. Therefore, you need to convert the data into a byte array and put it in. However, you can use asXXXBuffer of the ByteBuffer class to simplify the process. The following code is used:
Package lkl; import java. nio. byteBuffer; // write basic data types to the Channel // the easiest way to insert basic data types to ByteBuffer is to use ascharBuffer () // asw.buffer () obtain the view on the buffer, and then call the put () method of the view // The short type needs to be converted, other basic types do not require public class GetData {public static void main (String [] args) {ByteBuffer buff = ByteBuffer. allocate (1024); // read the char data buff. asCharBuffer (). put (java); // buff. flip (); // at this time, no flip () char c; while (c = buff. getChar ())! = 0) {System. out. print (c +);} System. out. println (); buff. rewind (); // read the short data buff. asw.buffer (). put (short) 423174); System. out. println (buff. getShort (); buff. rewind (); // read the long data buff. asLongBuffer (). put (689342343); System. out. println (buff. getLong (); buff. rewind (); // read the float data buff. asFloatBuffer (). put (2793); System. out. println (buff. getFloat (); buff. rewind (); // read the double data buff. asDoubleBuffer (). put (4.223254); System. out. println (buff. getDouble (); buff. rewind ();}/* Output j a v a 29958 689342343 2793.0 4.223254 */}
Of course, there are many other methods in the Buffer class, which can be understood through its API documentation. Now we know that Buffer must be used to deal with channels.
(2). Channel class
The Channel class corresponds to the Channel we mentioned at the beginning. It is noted that the Channel class is oriented to byte streams, so not all the IO classes we learned earlier can be converted to channels. In fact, Java provides Channel implementation classes such as FileChannel, initramchannel, selectableChannel, ServerSocketChannel, and SocketChannel. Here we only know FileChannel, which can be obtained through the getChannel () Methods of FileInputStream, FileOutputStream, and RandomAccessFile classes; of course, the FileChannel objects corresponding to these classes have different functions. The FileChannel corresponding to FileOutputStream can only write data to the file. The FileChannel corresponding to FileInputStream can only read data to the file, the FileChannel corresponding to RandomAccessFile can read and write files. This also indicates that this class can be called by no constructor. The following code demonstrates how to use a Channel to write data to and read data from a file:
Import java. io. *; import java. nio. *; import java. nio. channels. fileChannel; import java. nio. charset. charset; import java. nio. charset. charsetDecoder; // Channel is a new stream object provided by java. // Channel can map all or part of the file to a Channel. // However, we cannot directly deal with the Channel, both read and write operations require Buffer. // the Channel is obtained through the InputStream and getChannel () of the OutputStream of the traditional node instead of the constructor () obtain the public class FileChannelTest {public static void main (String [] args) {File f = new File (/Test/. java); try (// create FIleInputStream, and use the input stream of this file to create FileChannel inChannel = new FileInputStream (f ). getChannel (); // create a FileChannel with the file output stream to control the output FileChannel outChannel = new FileOutputStream (/Test/test.txt ). getChannel () {// map all data in the FileChannel to ByteBuffer MappedByteBuffer buffer = inChannel. map (FileChannel. mapMode. READ_ONLY, 0, f. length (); // use the GBK character set to create the decoder Charset charset = Charset. forName (GBK); // output all content in the buffer to the outChannel. write (buffer); buffer. clear (); // create the decoder object CharsetDecoder decoder = charset. newDecoder (); // use the decoder to convert ByteBuffer to CharBuffer charbuffer = decoder. decode (buffer); // The toString method in CharBuffer can obtain the corresponding string System. out. println (charbuffer. toString ();} catch (IOException e) {e. printStackTrace ();}}}
Note that the above Code uses decoding. This is because ByteBuffer contains bytes, so if we directly output the code, garbled characters will be generated. If we want to read the correct content from ByteBuffer, then the code is required. There are two forms: the first is encoding when writing data into ByteBuffer; the second is reading data from ByteBuffer and decoding. The Charset class can be used for encoding and decoding. The following code is used:
Package lkl; import java. io. fileInputStream; import java. nio. byteBuffer; import java. nio. charBuffer; import java. nio. channels. fileChannel; import java. nio. charset. charset; import java. io. *; // FileChannel conversion data type // The FileChannel write type can only be ByteBuffer, so the encoding and decoding problem is caused by public class BufferToText {private static final int SIZE = 1024; public static void main (String [] args) throws IOException {FileChannel fc = new fileindium UtStream (/Test/B .txt ). getChannel (); ByteBuffer buff = ByteBuffer. allocate (SIZE); fc. read (buff); buff. flip (); // convert ByteBuffer to CharBuffer, but the type conversion is not actually implemented, and the output garbled System. out. println (buff. asCharBuffer (); buff. rewind (); // the start position of the pointer returned to prepare for decoding // decodes the output so that the bytes are correctly converted to the String encoding = System. getProperty (file. encoding); System. out. println (Decoded using + encoding +: + Charset. forName (encoding ). decode (buff); buff. clea R (); // encode the input data so that the bytes are correctly converted to the fc = new FileOutputStream (/Test/a1.txt) character ). getChannel (); buff. put (some txt. getBytes (UTF-8); // encode buff when converting characters into bytes. flip (); fc. write (buff); fc. close (); fc = new FileInputStream (/Test/a1.txt ). getChannel (); buff. clear (); fc. read (buff); buff. flip (); System. out. println (buff. asCharBuffer (); // No problem after encoding and conversion. // If CharBuffer is directly used for writing, no encoding problem. fc = new FileOutputStream (/Test/a1.t Xt ). getChannel (); buff. clear (); buff. asCharBuffer (). put (this is test txt); fc. write (buff); fc = new FileInputStream (/Test/a1.txt ). getChannel (); buff. clear (); fc. read (buff); buff. flip (); System. out. println (buff. asCharBuffer (); fc. close ();}/* cannot begin? Too many tasks? Too many Decoded using UTF-8: this is test file too many? Upload this is test txt */}
(3). Questions about big and small orders
Large-end Order (high priority) and small-end Order (low priority) Problems
Large-Endian refers to storing important bytes in the storage unit with the lowest address.
The small order refers to placing important bytes in the storage unit with the highest address.
ByteBuffer stores data in large order.
For example: 00000000 01100001
The preceding binary data set represents a short INTEGER (eight digits)
If '97 is expressed in the big-Endian order, if it is a small-Endian order, it indicates (0110000100000000) 24832.
The following code demonstrates the comparison between the big-End sequence and the Small-End sequence:
Import java. nio. byteBuffer; import java. nio. byteOrder; import java. util. arrays; // we can use ByteOrder with parameters. BIG_ENDIAN or ByteOrder. LITTLE_ENDIAN // oder () method to change ByteBuffer byte sorting method public class Endians {public static void main (String [] args) {ByteBuffer bb = ByteBuffer. allocate (12); bb. asCharBuffer (). put (abcdef); System. out. println (Arrays. toString (bb. array (); bb. rewind (); bb. order (ByteOrder. BIG_ENDIAN); bb. asCharBuffer (). put (abcdef); System. out. println (Arrays. toString (bb. array (); bb. rewind (); bb. order (ByteOrder. LITTLE_ENDIAN); bb. asCharBuffer (). put (abcdef); System. out. println (Arrays. toString (bb. array ();}/* Output [0, 97, 0, 98, 0, 99, 0,100, 0,101, 0,102] [0, 97, 0, 98, 0, 99, 0,100, 0,101, 0,102] [97, 0, 98, 0, 99, 0,100, 0,101, 0,102, 0] */
Data is also transmitted over the Internet in large order (high priority ).
Ii. Object serialization
Object serialization refers to converting objects that implement the Serializable interface into a byte sequence, and can completely restore this byte sequence to the original object in the future. The serialization of objects can achieve lightweight persistence. "Persistence" means that the life cycle of an object does not depend on whether the program is being executed or not; it can survive between program calls. By writing a serialized object to a disk and then restoring the object when calling the program again, you can implement the persistence process. It is called "lightweight" because there is no keyword to conveniently implement this process. We need to manually maintain the entire process.
In general, serialization is not very difficult. We only need to inherit the Serializable interface of the corresponding class, and this interface is a mark interface, you do not need to implement specific content, and then call ObjectOutputStream to write the object into the file (serialization). If you want to restore it, use ObjectInputStream to read it from the file (deserialization ); note that both classes are packaged streams and other node streams need to be input. As shown in the following code, the deserialized object is indeed the same as the original object:
Package lkl; import java. io. *; import java. nio. byteBuffer; import java. nio. charBuffer; import java. nio. channels. fileChannel; import java. nio. charset. charset; import java. util. *; class Base implements Serializable {private int I; private int j; public Base (int I, int j) {this. I = I; this. j = j;} public String toString () {return [+ I ++ j +];} public class Test {public static void main (String [] args) throws IOException, classNotFoundException {Base base = new Base (1, 3); System. out. println (base); ObjectOutputStream out = new ObjectOutputStream (new FileOutputStream (/Test/Base. out); out. writeObject (base); // write the object to the disk ObjectInputStream in = new ObjectInputStream (new FileInputStream (/Test/Base. out); Base base1 = (Base) in. readObject (); // read the object from the disk System. out. println (base1);}/* [1 3] [1 3] */}
In addition to implementing the Serializable interface, we can also implement the sequence through implementing the Externalizable interface. This interface runs to control the serialization process, we manually choose to serialize and deserialize those variables. These are implemented based on the two functions in this interface: writeExternal () and readExternal. The following code demonstrates the simple implementation of the Externalizable interface. Note that Blip1 and Blip2 are slightly different:
Constructin objects: Blip1 ConstructorBlip2.ConstructorSaving objects: Blip1.writeExternalBlip2.writeExternalRecovering p1: Blip1 ConstructorBlip1.readExternal
We can see that the default constructor will be called in the deserialization process. If no default constructor can be called (the permission is not public), then in the deserialization process, an error occurs.
In addition, if we implement the Serializable interface but want some variables not to be serialized, we can use the transient keyword to modify them. Note that static variables will not be automatically serialized for the amount of Serializable interfaces implemented. We must manually serialize and deserialize them. The following code demonstrates these two points:
Package lkl; import java. io. *; import java. nio. byteBuffer; import java. nio. charBuffer; import java. nio. channels. fileChannel; import java. nio. charset. charset; import java. util. *; class Base implements Serializable {private int I; private transient int j; private static int k = 9; public Base (int I, int j) {this. I = I; this. j = j;} public String toString () {return [+ I ++ j ++ k +];} public class Test {public static void main (String [] args) throws IOException, ClassNotFoundException {Base base = new Base (1, 3); System. out. println (base); ObjectOutputStream out = new ObjectOutputStream (new FileOutputStream (/Test/Base. out); out. writeObject (base); // write the object to the disk ObjectInputStream in = new ObjectInputStream (new FileInputStream (/Test/Base. out); Base base1 = (Base) in. readObject (); // read the object from the disk System. out. println (base1);}/* [1 3 9] [1 0 9] */}
The following code demonstrates that in the Serializable interface, we can also control the serialization and deserialization processes by writing our own methods (it feels messy ):
Package lkl; import java. io. *; // Add the following methods to the implementation of the Serializable interface: // private void writeObject (ObjectOutputStream) throws IOException // private void readObject (ObjectInputStream) throws IOException, classNotFoundException // you can customize the elements to be serialized and deserialized in the two methods. // you can choose to execute the default writeObject () by calling defaultWriteObject () in writeObject () // call defaultReadObject () in readObject () to execute the default readObject () public class SerialCtl implements Serializable {private String a; private transient String B; public SerialCtl (String aa, string bb) {a = Not Transient: + aa; B = transient: + bb;} public String toString () {return a ++ B;} private void writeObject (ObjectOutputStream stream) throws IOException {stream. defaultWriteObject (); stream. writeObject (B);} private void readObject (ObjectInputStream stream) throws IOException, ClassNotFoundException {stream. defaultReadObject (); B = (String) stream. readObject ();} public static void main (String [] args) throws IOException, ClassNotFoundException {SerialCtl SC = new SerialCtl (Test1, Test2); System. out. println (Before:); System. out. println (SC); // This serialization information is not stored in the file, but stored in the buffer to ByteArrayOutputStream buf = new ByteArrayOutputStream (); ObjectOutputStream o = new ObjectOutputStream (buf); o. writeObject (SC); ObjectInputStream in = new ObjectInputStream (new ByteArrayInputStream (buf. toByteArray (); SerialCtl sc2 = (SerialCtl) in. readObject (); System. out. println (After:); System. out. println (sc2 );}}
It should be emphasized that, if we have many objects that can be serialized that have mutual reference relationships, we only need to package them for serialization, the system automatically maintains a network with a serialization relationship. Then, during deserialization, the system still obtains the corresponding information of this object through the. class file.