JavaSE学习笔记(19.Java之IO)

1. 文件处理

Java提供一个File类,用来处理文件和文件夹!每一个File对象,都是文件在内存中的一个映射,可以通过这个对象操作对应的文件!

1.1 FIle类的使用:

Ps:

  1. 默认情况下,相对路径中的默认值是由于系统属性"user.dir"参数指定的;系统属性的相关问题,后面会专门介绍!
  2. File对象的构造可以使用绝对路径和相对路径,如果是使用相对路径是获取不到上级目录的,调用getParent()方法会返回null!
  3. 输入路径字符串的时候,需要将"\"字符进行转义为"\\";或者直接使用"/"字符,Java支持将斜线当做平台无关的路径分隔符!
  4. 获取路径的时候,使用getCanonicalPath()方法,不建议使用getAbsolutePath(),因为getCanonicalPath()方法会规范化处理相对路径中的'.'和'..'字符!

1.2 文件过滤:

File类中list()方法,可以获取目标路径及其子目录中的所有文件;可以使用一个参数为FilenameFilter类的list()方法,用来过滤这些所有文件,FilenameFilter是一个函数式接口,通过重写accpet(File dir,String name)方法,实现文件的过滤,也可以使用lambda表达式!

public String[] list(FilenameFilter filter) {
        String names[] = list();
        if ((names == null) || (filter == null)) {
            return names;
        }
        List<String> v = new ArrayList<>();
        for (int i = 0 ; i < names.length ; i++) {
            if (filter.accept(this, names[i])) {
                v.add(names[i]);
            }
        }
        return v.toArray(new String[v.size()]);
    }

 

2. IO流介绍

2.1 流的概念:

Java将数据导入内存和导出内存,封装为一个IO流的概念,提供一个可以持续内存输入输出的能力!常用的IO流有下面几种分类:

输入流输出流:

  • 内存从流中获取数据,为输入流
  • 内存往流中写入数据,为输出流

字节流字符流:

  • 输入输出以字节为单位,为字节流
  • 输入输出以字符为单位,为字符流

节点流处理流:

  • 直接访问数据节点的,为节点流;如文件流、数组流、String流等
  • 为了使用方便,对节点流使用装饰器模式进行二次包装,封装各节点之间差异的流,为处理流!

2.2 IO流的使用:

2.2.1 输入流:

常用输入流都是InputStream(字节流)和Reader(字符流)的子类

方法说明:

/*InputStream类方法说明*/
/*从输入流中,读取一个int类型的字节*/
public abstract int read() throws IOException;

/*从输入流中,读取一个字节数组*/
public int read(byte b[]) throws IOException

/*从输入流中,读取一个字节数组,并放在数组的特定位置*/
public int read(byte b[], int off, int len) throws IOException

/*关闭输入流,释放输入流中占用的资源,包括物理资源*/
public void close() throws IOException

/*InputStream类提供了几个关于mark扩展能力的方法,需要通过重写方法实现*/
public boolean markSupported()
public void mark(int readAheadLimit) throws IOException
public void reset() throws IOException


/*Reader类方法说明*/
/*从输入流中,读取一个int类型的字符*/
public int read() throws IOException

/*从输入流中,读取一个字符数组*/
public int read(char cbuf[]) throws IOException

/*从输入流中,读取一个字符数组,并放在数组的特定位置*/
abstract public int read(char cbuf[], int off, int len) throws IOException;

/*Nio接口,使用CharBuffer读取输入流中数据*/
public int read(java.nio.CharBuffer target) throws IOException

/*关闭输入流,释放输入流中占用的资源,包括物理资源*/
abstract public void close() throws IOException;

/*Reader类提供了几个关于mark扩展能力的方法,需要通过重写方法实现*/
public boolean markSupported()
public void mark(int readAheadLimit) throws IOException
public void reset() throws IOException

Ps:针对read()方法,这里需要补充一点,在官网API说明中,如果已经到达流末尾而没有可用的字节,则返回-1;在输入数据可用、检测到流末尾或者抛出异常前,此方法一直阻塞!read()方法返回的-1,必须使用int类型接收并判断,不可强制类型转为char类型或者byte类型再进行判断,因为read()方法返回的有效范围为16bit,只有int类型的-1不在这个范围内,才能有效的表示为是流结束而不是数值128或者65536!否则如果强转后发生截断很可能导致判断不到流结束!!!

2.2.2 输出流:

常用的输出流都是OutputStream(字节流)和Writer(字符流)的子类

方法说明:

/*OutputStream类提供的方法*/
/*将int类型的字节,写入输出流*/
public abstract void write(int b) throws IOException

/*将字节数组b,写入输出流*/
public void write(byte b[]) throws IOException

/*将字节数组b特定位置,写入输出流*/
public void write(byte b[], int off, int len) throws IOException

/*将输出流中的缓存数据推送到物理节点*/
public void flush() throws IOException

/*关闭输出流,并将输出流中的缓存数据推送到物理节点*/
public void close() throws IOException


/*Writer类提供的方法*/
/*将int类型的字符,写入输出流*/
public void write(int c) throws IOException

/*将字符数组b,写入输出流*/
public void write(char cbuf[]) throws IOException

/*将字符数组b特定位置,写入输出流*/
abstract public void write(char cbuf[], int off, int len) throws IOException;

/*关闭输出流,并将输出流中的缓存数据推送到物理节点*/
abstract public void close() throws IOException;

/*将输出流中的缓存数据推送到物理节点*/
abstract public void flush() throws IOException;

/*将String字符串,写入输出流*/
public void write(String str) throws IOException
public void write(String str, int off, int len) throws IOException

2.2.3 输入输流代码示例:

public class IoTest {
    public static void main(String args[]) throws InterruptedException, IOException {
        /*通过输入流读入rtest.txt文件中的数据*/
        File fread = new File("rtest.txt");
        Reader reader = new FileReader(fread);
        char c[] = new char[1024];
        reader.read(c);
        System.out.println(c);
        reader.close();

        /*通过输出流将char c[]中的数据写入wtest.txt文件中*/
        File fwrite = new File("wtest.txt");
        Writer writer = new FileWriter(fwrite);
        writer.write(c);
        writer.close();
        
    }
}

2.3 常用的IO流:

2.3.1 InputStream、OutputStream、Reader、Writer抽象基类:

InputStream、OutputStream、Reader、Writer是所有输入输出流的抽象基类

2.3.2 FileXXX文件输入输出流:

用于实现文件与内存之间的输入输出

2.3.3 ByteArrayXXX、CharArrayXXX数组输入输出流:

用于实现数组与内存之间的输入输出

2.3.4 StringReade、StringWriter字符串输入输出流:

用于实现字符串与内存之间的输入输出

2.3.5 PipedXXX管道输入输出流:

管道流主要用于线程之间的管道通信;一个线程中通过输出管道流输出,另一个线程中通过这个输出管道流创建一个输入管道流用于获取数据,进而实现两个线程之间的通信!

2.3.6 ObjectXXX对象输入输出流:

用于对象序列化,反序列化,具体使用再后面序列化中,详细阐述!

2.3.7 BufferedXXX缓存输入输出流:

缓存输入输出流,是典型的处理流;可以将节点流或者其他处理流包装成为缓存流,使用缓存功能,提交输入输出效率,方便使用!

2.3.8 InputStreamReader、OutputStreamWriter转换流:

字节转字符流,是典型的处理流;将字节流转换为字符流完成输入输出

/*处理流包装节点流的使用方式*/
public class IoTest {
    public static void main(String args[]) throws InterruptedException, IOException {
        File fread = new File("rtest.txt");        
        /*通过节点流FileInputStream将文件已字节的形势读入*/
        FileInputStream inStream = new FileInputStream(fread);
        /*通过字节转字符流和缓存流(处理流)对上面的节点流进行包装*/
        BufferedReader bufferReader = new BufferedReader(new InputStreamReader(inStream));

        /*通过缓存流的方式读取数据*/
        System.out.println(bufferReader.readLine());

        /*关闭缓存流*/
        bufferReader.close();
    }
}

2.3.9 PushbackXXX推回输入流:

推回输入流是一种通过缓存实现重复读取的输入流;常规的输入流数据是不可以重复读取的,推回输入流提供了一组unread()方法,用来从数据流中读取数据并推入缓存buffer中;通过read()方法,读取的数据都是优先从缓存buffer中读取,不足的时候,再从数据流中读取,这样就可以实现输入数据的重复读取!注意:默认推回输入流的缓存buffer为1,超出缓存大小会抛出异常,需要手动改写buffer大小!

2.4 重定向标准输入输出:

System类在初始化的时候,设置了默认的标准输入流(in)、标准输出流(out)、标准错输出流(err);默认的输入是键盘,输出是显示器;但是System类中提供了三个静态方法,可以重定向这个三个流!

public static void setIn(InputStream in)

public static void setOut(PrintStream out)

public static void setErr(PrintStream err)

2.5 读取进程数据流:

Java可以通过Runtime类的exce()静态方法启动一个Java子进程,每个子进程中都有三个标准流;父进程可以通过子进程Process类中的三个方法获取这三个流;由于是从父进程调用子进程角度来看的,所有原本子进程的输出流就变成了输入流、输入流就变成了输出流!

/*获取子进程的输出流*/
public abstract InputStream getInputStream();

/*获取子进程的错误流*/
public abstract InputStream getErrorStream();

/*向子进程的输入流中输出数据*/
public abstract OutputStream getOutputStream();

Ps:当通过Runtime.exce()方法返回一个Java子进程Process对象的时候,如果在父进程中调用Process.waitFor()方法,会让父进程等待子进程完成;但是如果子进程持续有对父进程的输入流和错误流,并且父进程并没有消费这两个输入流,就可能导致数据流缓存被塞满,导致子进程写入数据行为挂起,子进程等待父进程消费缓存中数据;这样就发生了父子进程的死锁!

解决办法:1. 使用带有超时时间的waitFor()方法;2. 在父进程中,启动线程处理子进程给父进程的输入流和错误流!

2.6 Properties文件读取:

Properties文件是Java的一种属性文件,是Hashtable的子类,里面可以存放类型为String的键值对!

public class PropertiesTest {
    public static void main(String args[]) throws InterruptedException, IOException {
        /*将数据写入properties文件中*/
        File f = new File("test.properties");
        FileOutputStream outStream = new FileOutputStream(f);

        Properties p = new Properties();
        p.setProperty("key","value");
        p.setProperty("key1","value1");
        p.setProperty("key2","value2");
        p.store(outStream,"");
        outStream.close();

        /*从properties文件中,读取数据*/
        FileInputStream inStream = new FileInputStream(f);
        Properties p1 = new Properties();
        p1.load(inStream);

        System.out.println(p1.getProperty("key"));
        System.out.println(p1.getProperty("key1"));
        System.out.println(p1.getProperty("key2"));
        inStream.close();
    }
}

2.7 RandomAccessFile类:

输入输出流对于文件的读写只能从文件头开始,而且读写操作也不能同时进行!

  • RandomAccessFile类提供一种,可以通过游标位置进行文件读写的方式;RandomAccessFile类中提供了与输入输出流类似的write()/read()方法,获取游标和设置游标的方法getFilePointer()/seek(long pos)
  • RandomAccessFile类的构造器中,还需要一个mode参数,用于指定文件的操作模式,分别有4个值如下:

 

3. NIO介绍:

很多资料中都介绍过NIO和NIO2.0,其实NIO就是JDK1.4对老版本的IO的一些补充,NIO2.0就是JDK1.7对老版本的IO的一些补充!

3.1 NIO:

由于传统的输入输出流,大多数场景的时候都是阻塞式IO,即每次读写操作完成之前,当前线程都是阻塞的,这种阻塞式IO会导致数据流的吞吐量很低;并且每次输入输出,都是以字节为单位进行操作,这样的效率并不高!所以JDK1.4引入了一种新的方式对原有的输入输出流进行整块的无阻塞的输入输出操作!就是通过java.nio包中的Buffer类、Channel类、Selector类组合完成的!

3.1.1 Buffer类和Channel类:

java.nio包中提供了一个Buffer类,用来当做数据的缓存,再结合Channel类与输入输出流建立连接,通过缓存的方式对输入输出流进行整块的无阻塞的输入输出操作!

Channel类:

Channel接口针对于每一种输入输出流都有特定的XXXChannel实现类;每一种输入输出流中都提供了一个getChannel()方法,用来获取对应的Channel连接!

/*以FileInputStream类为例*/
public FileChannel getChannel() {
        synchronized (this) {
            if (channel == null) {
                channel = FileChannelImpl.open(fd, path, true, false, this);
            }
            return channel;
        }
    }

针对Channel的输入输出操作,必须依赖于Buffer缓存类:

  • 通过Channel的map()方法,可以将输入流中的数据映射到Buffer缓存中
  • 通过Channel的read()方法,可以将输入流中的数据读入Buffer缓存中
  • 通过Channel的write()方法,可以将Buffer缓存中的数据写入输出流中

Buffer类:

Buffer是一个抽象类,对应每一个基本类型都有一个抽象子类,如:CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、ByteBuffer,内部实现基本类似,只是类型不同而已;在IO处理中最常用的就是CharBuffer和ByteBuffer,分别对应字符流和字节流!

1. NIO通过Buffer类实现数据的整读整取:

Buffer类中,有三个关键参数,postition(游标下一个位置)、capacity(Buffer容量)、limit(游标限制位);Channel就是通过Buffer的这三个属性完成的数据整块读取的;

  • Channel调用read/map方法一次最多可以从输入流读取limit-postition长的数据放入Buffer;
  • Channel调用write方法一次最多可以写入Buffer中limit-postition长度的数据到输出流

2. Buffer的构造:

Buffer抽象子类中,都提供了一个allocate()静态方法,通过这个静态方法构造Buffer实例;ByteBuffer还额外提供了一个allocateDirect()静态方法,由于allocateDirect的构造成本较高,但是数据的读取效率较高,适用于那些生命周期较长的Buffer使用!

3. Buffer的主要方法:

Buffer基础参数获取方法:

Buffer游标刷新方法:

/*清除游标位置,为下次读入做准备*/
public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

/*回滚游标位置,为下次写出做准备*/
public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

Buffer单数据操作方法:

/*Buffer尾部插入数据*/
public abstract ByteBuffer put(byte b);

/*指定Buffer位置插入数据*/
public abstract ByteBuffer put(int index, byte b);

/*Buffer尾部获取数据*/
public abstract byte get();

/*指定Buffer位置获取数据*/
public abstract byte get(int index);

使用示例:

public class NioTest {
    public static void main(String args[]) throws InterruptedException, IOException {
        /*通过channel的read方法,读取文件数据*/
        File f = new File("rtest.txt");
        FileChannel inchannel = new FileInputStream(f).getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(100);
        inchannel.read(buffer);
        inchannel.close();

        /*通过channel的write方法,将数据写入文件*/
        File fw = new File("wtest.txt");
        FileChannel outchannle = new FileOutputStream(fw).getChannel();
        /*回滚游标*/
        buffer.flip();
        outchannle.write(buffer);
        inchannel.close();

        /*通过channel的map方法,读取文件数据*/
        File fmap = new File("rtest.txt");
        FileChannel mapchannel = new FileInputStream(f).getChannel();
        MappedByteBuffer mapbuffer = mapchannel.map(FileChannel.MapMode.READ_ONLY,0,f.length());
        System.out.println(mapbuffer.position());
        mapchannel.close();
    }
}

Ps:可见通过channel的map方法读取数据到Buffer缓存的时候,与read不一样,position位置并没有向后偏移,Buffer的postition位置依然为0!

3.1.2 Selector类:

通过非阻塞模式的Channel类与Selector类的组合使用,可以提供一种非阻塞IO的能力,详细使用规则见<Java NIO 教程(六) Selector>,这里不做展开说明!

3.1.3 Charset类:

NIO提供了一个Charset类,主要用来实现字符集相关的操作!其实字符集就是用来表征字符串和字节串之间的对应关系的!Java中字符类型默认使用的是Unicode字符集的UTF-16编码!也就是说UTF-16是一个中间态编码,所有编解码都是围绕这个UTF-16编码进行的!

  • 字符串到某种特定格式的字节串的转换被叫做编码,本质上,就是UTF-16编码到某种编码格式的转换!
  • 某种特定格式的字节串到字符串的转换被叫做解码,本质上,就是某种编码格式到UTF-16编码的转换!

Charset类提供的能力:

/*静态方法,获取JDK支持的所有字符集*/
public static SortedMap<String,Charset> availableCharsets() 

/*静态方法,通过字符集名字,获取Charset对象*/
public static Charset forName(String charsetName) 

/*将字符串以当前Charset类型的字符集编码为字节串*/
public final ByteBuffer encode(CharBuffer cb)
public final ByteBuffer encode(String str) 

/*将字节串以当前Charset类型的字符集解码为字符串*/
public final CharBuffer decode(ByteBuffer bb)

/*获取当前Charset类型的字符集的解码器*/
public abstract CharsetDecoder newDecoder()

/*获取当前Charset类型的字符集的编码器*/
public abstract CharsetEncoder newEncoder()

3.1.4 文件锁:

JDK1.4 在FileChannel类中增加了一个文件锁的能力FileLock类,可以通过tryLock()/lock()方法对文件部分位置进行锁操作!

/*FileChannel类中获取锁*/
/*position为锁的位置,size为锁的长度,shared参数用来表示是否为共享锁*/
/*shared是true为共享锁,可以让其他线程读取被锁内容,但不可以获得该锁*/
/*share是false为非共享锁,即不可以读写被锁内容,也不可以获得该锁*/
public abstract FileLock lock(long position, long size, boolean shared)
public abstract FileLock tryLock(long position, long size, boolean shared)

/*FileLock类中通过release()方法,释放锁*/
public abstract void release()

Ps:文件锁跟JVM平台紧密相关,不同平台的实现机制可能不一致,导致有着较差的跨平性,此处只做了解,并不建议使用!

3.2 NIO2.0:

JDK1.7 对IO又进行了一次补充,主要是针对文件系统操作,异步Channel的补充

3.2.1 NIO2.0 文件操作:

由于FIle类对文件操作能力有限,JDK1.7 在java.nio.file包中增加了Path类、Paths工具类、Files工具类,来进行能力补充!

基础操作:

Path、Paths、Files这几个类的基本使用方法,后面再详细描述,这里不再扩展!

文件属性获取:

Files工具类中新增了很多文件属性获取的方法,比起File类的文件属性获取功能要丰富很多;使用都是先通过Files类获取XXXFileAttributeView,再通过View获取Attributes;Files类中提供如下6种View数据:

代码示例:

/*获取文件路径*/
Path path = Paths.get("text.txt");
/*获取文件基础属性view*/
BasicFileAttributeView view = Files.getFileAttributeView(path,BasicFileAttributeView.class);
/*获取文件基础属性*/
BasicFileAttributes attributes = view.readAttributes();

目录遍历:

Files工具类中,提供一种目录遍历的方法,之前Java版本只能通过递归的方式遍历;Files工具类中walkFileTree()方法,可以很方便的遍历目录!

public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor)

public static Path walkFileTree(Path start,Set<FileVisitOption> options,int maxDepth,FileVisitor<? super Path> visitor)

walkFileTree()方法有三个参数,第一个参数是需要遍历的目录对象、第二个参数为遍历深度、第三个参数FileVisitor类中有4个抽象方法分别代表访问子目录和文件时候的4种场景,可以通过重写这4个方法,来定制自己的目录遍历流程!方法如下:

路径中文件变化监控:

Path类中提供了一个register()的方法用来对目标路径中的文件变化进行监控

public class WatchServiceTest {
    public static void main(String args[]) throws InterruptedException, IOException {
        /*通过文件系统默认的监控器进行监控*/
        WatchService service = FileSystems.getDefault().newWatchService();

        /*对当前目录,使用文件系统默认的监控器,监控文件创建,修改,删除*/
        Paths.get(".").register(service,
            StandardWatchEventKinds.ENTRY_CREATE,
            StandardWatchEventKinds.ENTRY_DELETE,
            StandardWatchEventKinds.ENTRY_MODIFY);

        /*通过监控器,捕获一个监控事件*/
        WatchKey key = service.poll();
        /*通过监控器,等待1s,捕获一个监控事件*/
        WatchKey key1 = service.poll(1000, TimeUnit.MILLISECONDS);
        /*通过监控器,阻塞捕获一个监控事件*/
        WatchKey key2 = service.take();

        /*可以通过WatchKey获取所有事件*/
        List<WatchEvent<?>> events = key.pollEvents();
        /*获取事件类型*/
        events.get(0).kind();
        /*获取事件描述*/
        events.get(0).context();
    }
}

3.2.2 异步Channel:

JDK 1.7在java.nio.channels包下,增加了多个以Asynchronous开头的Channel类和接口,提供了一种异步Channel的能力!这里不做扩展说明,详细内容可见<Java NIO 教程(十六) Java NIO AsynchronousFileChannel>

 

4. 序列化与反序列化

序列化:将Java对象中数据转换为字节序列,切记类成员是不进行序列化的!!!

反序列化:将序列化的字符序列转换为Java对象数据

4.1 通过对象输入输出流进行序列化:

Java提供一个Serializable空接口,用来指示那些可以被序列化的对象,然后通过ObjectOutputStream流和ObjectInputStream流进行序列化和反序列化!

public class SerializableTest {
    public static void main(String args[]) throws InterruptedException, IOException {
        File f = new File("text.txt");
        ObjectOutputStream oStream = new ObjectOutputStream(new FileOutputStream(f));

        TestObject t = new TestObject();
        t.a = 1;
        t.b = 'c';

        /*通过ObjectOutputStream流的writeObject()方法,实现对对象t进行序列化*/
        oStream.writeObject(t);
        oStream.close();

        ObjectInputStream iStream = new ObjectInputStream(new FileInputStream(f));
        /*通过ObjcetInputStream流的readObject()方法,实现对t对象字节序列的反序列化*/
        TestObject t1 = null;
        try {
            t1 = (TestObject)iStream.readObject();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        System.out.println(t1.a);
        System.out.println(t1.b);
        iStream.close();

    }
}

class TestObject implements Serializable {
    public int a;
    public char b;
}

Ps:

  • 字节序列在反序列化的时候,必须保证可以引用到序列化对象的类class文件,否则在反序列化的时候,会抛出ClassNotFoundException异常!
  • 在一个文件中有多个字节序列的时候,反序列化顺序要与序列化顺序一致,这样才能保证数据是对齐的!

父类序列化:

当序列化的类存在父类的时候,父类存在如下3种情况:

  1. 父类未实现Serializable接口,没有显示或隐式的无参构造器,反序列化的时候,抛出InvalidClassException异常!
  2. 父类未实现Serializable接口,但是存在一个显示或隐式的无参构造器,子类继承的父类成员不参与序列化和反序列化!
  3. 父类实现Serializable接口,子类继承的父类成员正常进行序列化和反序列化!

引用对象序列化:

当序列化的类中存在引用类型成员的时候,Java为了避免一个引用对象被多次序列化,存储多份相同的序列字节串,实现了一个优化算法;给每个序列化对象一个序列号,在序列化之前判断这个序列号是否被序列化过,如果序列化过,就不再生成序列字节串,只是记录下序列号;未被使用才会生成序列字节串!

  • 这种优化算法有两点好处,第一可以节约空间第二可以记录下引用关系
  • 但是也存在一个问题,就是在序列化过程中,如果引用数据在第一次序列化后发生变化,后面的这个引用序列化是感知不到变化的,因为后面都是记录序列号,不会改变序列字节串!
  • 序列化的类中的引用对象,也必须是实现Serializable接口的,标记为可序列化对象,否则在序列化时抛出NotSerializableException异常!

4.2 transient关键字:

可以使用transient关键字修饰成员,指定成员不进行序列化和反序列化操作!

使用场景:

  • 针对对象中的敏感数据,如果不需要序列化可以使用transient关键字修饰!
  • 针对对象中未被Serializable接口修饰的引用对象,防止抛出NotSerializableException异常,可以使用transient关键字修饰!
  • 由于类成员是不进行序列化的,所以transient关键字修饰类成员是没有意义的!

4.3 自定义序列化:

Java原生的序列化和反序列化在执行的过程中,预留了几个钩子方法,用于在原生序列化和反序列化的过程中,自定义用户行为!代码见ObjectStreamClass.java中ObjectStreamClass构造器!

通过在序列化对象类中实现如下几个方法,来实现钩子方法:

  • writeObject(ObjectOutputStream out)方法:如果序列化对象类实现writeObject()方法,序列化过程不再调用输出流的默认write方法,而是调用用户自定义writeObject()方法!
  • readObject(ObjectInputStream in)方法:如果序列化对象类实现readObject()方法,反序列化过程不再调用输入流的默认read方法,而是调用用户自定义readObject()方法!
  • readObjectNoData()方法:如果在反序列化中两侧版本号不一致,或者序列化流被修改不完整等,反序列化过程会调用readObjectNoData()方法,来实现对反序列化对象的初始化!
  • writeReplace()方法:如果序列化对象类实现writeReplace()方法,序列化过程会用writeReplace()方法返回一个对象,再调用这个返回对象的writeObject()方法,进行序列化!
  • readResolve()方法:如果序列化对象类实现readResolve()方法,反序列化过程会在调用readObject()方法后,再调用readResolve()方法,通过readResolve()方法的返回值,作为反序列化的结果!通常使用在,对单例类的序列化反序列化中,保证多次反序列化的过程中,只产生一个实例!

Ps:

  • 由于writeReplace()方法和readResolve()方法并没有要求访问属性,可以为任意访问权限,但是如果是public/protected属性,会导致子类中默认继承父类的两个方法,但是这两个方法还是根据父类定制的,这样容易造成子类序列化混乱;所以这两个方法建议使用private和final修饰符修饰!
  • writeObject()方法和readObject()方法中,将成员写入out输出流的顺序,要与从in输入流中读取成员顺序一致,不然序列化和反序列化操作不对齐,会导致数据不对齐!

4.4 版本:

Java序列化中提供了一个版本的概念,在序列化对象类中提供一个private static final属性的类常量serialVersionUID作为序列化版本,通过这个值来确定,序列化和反序列化的版本是否一致! 

  • serialVersionUID的值,可以是任意值,只要保证序列化和反序列化类中值对齐即可;不对齐则说明版本不对齐,没有同步升级!
  • 也可以通过JDK中的serialver.exe工具生成serialVersionUID的值

4.5 Externalizable序列化接口:

Java还提供了另一种序列化接口,Externalizable接口;使用方式基本上与Serializable接口类似(都使用ObjectOutputStream流和ObjectInputStream流),只是Externalizable接口没有提供默认的序列化的能力,强制用户自定义序列化和反序列化逻辑!Externalizable接口方式虽然比Serializable接口方式实现复杂,但是在性能方面略好!

/*在序列化类中,实现对象反序列化逻辑*/
void readExternal(ObjectInput in)

/*在序列化类中,实现对象序列化逻辑*/
void writeExternal(ObjectOutput out)

4.6 序列化与Json:

除了Java原生的序列化方式,Json格式也是常用的一种序列化方式!JSON相关描述可见<介绍JSON>、更多JSON的资料可见<JSON最佳实践>

原生序列化与Json格式序列化的比较:

  • Json格式序列化,是将对象数据转换成为一个Json格式的键值对字符串(本质也是字节串,符合Java序列化含义)
  • Json字符串描述更清晰,可以很容易的看懂对象数据的键值关系!
  • Json格式序列化,同样不会将类对象进行序列化;对于同一个对象的多次序列化也会记录对象序列号ID!
  • Json格式的序列化有着很好的跨语言性!
  • 由于Java原生序列化方式,在序列化数据中增加了一些对象间关系的记录,在大数据量的情况下,序列化和反序列化的效率会比Json高一些,但是同样空间的使用也会高一些,少量数据的情况,并不明显!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值