I/O流;序列化;编码

一、什么是IO

1、定义      

1.1 定义

Java中I/O操作主要是指使用Java进行输入,输出操作. Java所有的I/O机制都是基于数据流进行输入输出,这些数据流表示了字符或者字节数据的流动序列。Java的I/O流提供了读写数据的标准方法。任何Java中表示数据源的对象都会提供以数据流的方式读写它的数据的方法。  

1.2 类库 

Java.io是大多数面向数据流的输入/输出类的主要软件包。此外,Java也对块传输提供支持,在核心库 java.nio中采用的便是块IO。

流IO的好处是简单易用,缺点是效率较低。块IO效率很高,但编程比较复杂。

 1.3 Java IO模型      

Java的IO模型设计非常优秀,它使用Decorator模式,按功能划分Stream,您可以动态装配这些Stream,以便获得您需要的功能。例如,您需要一个具有缓冲的文件输入流,则应当组合使用FileInputStream和BufferedInputStream。 

二、数据流的基本概念

1、定义        

数据流是一串连续不断的数据的集合,就象水管里的水流,在水管的一端一点一点地供水,而在水管的另一端看到的是一股连续不断的水流。数据写入程序可以是一段、一段地向数据流管道中写入数据,这些数据段会按先后顺序形成一个长的数据流。对数据读取程序来说,看不到数据流在写入时的分段情况,每次可以读取其中的任意长度的数据,但只能先读取前面的数据后,再读取后面的数据。不管写入时是将数据分多次写入,还是作为一个整体一次写入,读取时的效果都是完全一样的。 

 在电脑上的数据有三种存储方式,一种是外存,一种是内存,一种是缓存。比如电脑上的硬盘,磁盘,U盘等都是外存,在电脑上有内存条,缓存是在CPU里面的。外存的存储量最大,其次是内存,最后是缓存,但是外存的数据的读取最慢,其次是内存,缓存最快。这里总结从外存读取数据到内存以及将数据从内存写到外存中。对于内存和外存的理解,我们可以简单的理解为容器,即外存是一个容器,内存又是另外一个容器。那又怎样把放在外存这个容器内的数据读取到内存这个容器以及怎么把内存这个容器里的数据存到外存中呢?

2、使用范围     

在Java类库中,IO部分的内容是很庞大的,因为它涉及的领域很广泛:

         标准输入输出,文件的操作,网络上的数据流,字符串流,对象流,zip文件流等等,java中将输入输出抽象称为流,就好像水管,将两个容器连接起来。将数据从外存中读取到内存中的称为输入流,将数据从内存写入外存中的称为输出流。

    流是一个很形象的概念,当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。

 3、概念分类

3.1 数据流:一组有序,有起点和终点的字节的数据序列。包括输入流和输出流。

 

 

3.2 输入流(Input  Stream):程序从输入流读取数据源。数据源包括外界(键盘、文件、网络…),即是将数据源读入到程序的通信通道。

 

 

 3.3 输出流:程序向输出流写入数据。将程序中的数据输出到外界(显示器、打印机、文件、网络…)的通信通道。

 

4、按照传输单元分类

流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种:

 4.1  字节流:数据流中最小的数据单元是字节。

 4.2  字符流:数据流中最小的数据单元是字符, Java中的字符是Unicode编码,一个字符占用两个字节。

5、 为什么使用流传输数据

采用数据流的目的就是使得输出输入独立于设备。

Input  Stream不关心数据源来自何种设备(键盘,文件,网络)。
Output  Stream不关心数据的目的是何种设备(键盘,文件,网络)。

三、 标准I/O

      Java程序可通过命令行参数与外界进行简短的信息交换,同时,也规定了与标准输入、输出设备,如键盘、显示器进行信息交换的方式。而通过文件可以与外界进行任意数据形式的信息交换。

1. 命令行参数

public class TestArgs {
   public static void main(String[] args) {
      for (int i = 0; i < args.length; i++) {
        System.out.println("args[" + i + "] is <" + args[i] + ">");
      }
   }
}
运行命令:java Java C VB

运行结果:

args[0] is <Java>

args[1] is <C>

args[2] is <VB>

2. 标准输入,输出数据流

java系统自带的标准数据流:java.lang.System:

java.lang.System 
public final class System extends Object{ 
static  PrintStream err;//标准错误流(输出)
static  InputStream in;//标准输入(键盘输入流)
static  PrintStream out;//标准输出流(显示器输出流)
}

注意:

  • System类不能创建对象,只能直接使用它的三个静态成员。
  • 每当main方法被执行时,就自动生成上述三个对象。

2.1 标准输出流 System.out

System.out向标准输出设备输出数据,其数据类型为PrintStream。方法:

Void print(参数)
Void println(参数)  

2.2 标准输入流 System.in

System.in读取标准输入设备数据(从标准输入获取数据,一般是键盘),其数 据类型为InputStream。方法:
int read()  //返回ASCII码。若,返回值=-1,说明没有读取到任何字节读取工作结束。
int read(byte[] b)//读入多个字节到缓冲区b中返回值是读入的字节数

例如:

import java.io.*;
public class StandardInputOutput {
public static void main(String args[]) {
int b;
try {
System.out.println("please Input:");
while ((b = System.in.read()) != -1) {
System.out.print((char) b);
}
} catch (IOException e) {
System.out.println(e.toString());
}
}
}
等待键盘输入,键盘输入什么,就打印出什么:

2. 标准错误流

   System.err输出标准错误,其数据类型为PrintStream。可查阅API获得详细说明。

    标准输出通过System.out调用println方法输出参数并换行,而print方法输出参数但不换行。println或print方法都通 过重载实现了输出基本数据类型的多个方法,包括输出参数类型为boolean、char、int、long、float和double。同时,也重载实现 了输出参数类型为char[]、String和Object的方法。其中,print(Object)和println(Object)方法在运行时将调 用参数Object的toString方法。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class StandardInputOutput {
public static void main(String args[]) {
String s;
// 创建缓冲区阅读器从键盘逐行读入数据
InputStreamReader ir = new InputStreamReader(System.in);
BufferedReader in = new BufferedReader(ir);
System.out.println("Unix系统: ctrl-d 或 ctrl-c 退出"
+ "\nWindows系统: ctrl-z 退出");
try {
// 读一行数据,并标准输出至显示器
s = in.readLine();
// readLine()方法运行时若发生I/O错误,将抛出IOException异常
while (s != null) {
System.out.println("Read: " + s);
s = in.readLine();
}
// 关闭缓冲阅读器
in.close();
} catch (IOException e) { // Catch any IO exceptions.
e.printStackTrace();
}
}
}

四、 java.IO层次体系结构

1、主要类与接口     

在整个Java.io包中最重要的就是5个类和一个接口。5个类指的是File、OutputStream、InputStream、Writer、Reader;一个接口指的是Serializable.掌握了这些IO的核心操作那么对于Java中的IO体系也就有了一个初步的认识了。

 2、Java I/O主要包括如下几个层次,包含三个部分:

2.1 流式部分:

IO的主体部分;

2.2 非流式部分:

主要包含一些辅助流式部分的类,如:File类、RandomAccessFile类和FileDescriptor等类;

  1. File(文件特征与管理):用于文件或者目录的描述信息,例如生成新目录,修改文件名,删除文件,判断文件所在路径等。
  2. InputStream(二进制格式操作):抽象类,基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。
  3. OutputStream(二进制格式操作):抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。
  4. Reader(文件格式操作):抽象类,基于字符的输入操作。
  5. Writer(文件格式操作):抽象类,基于字符的输出操作。
  6. RandomAccessFile(随机文件操作):它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。

说明:Java中字符是采用Unicode标准,一个字符是16位,即一个字符使用两个字节来表示。为此,JAVA中引入了处理字符的流。

2.3 其他类:

文件读取部分的与安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。

2.4 Java中IO流的体系结构如图:

五、 非流式文件类--File类

1、定义:

在Java语言的java.io包中,由File类提供了描述文件和目录的操作与管理方法。但File类不是InputStream、OutputStream或Reader、Writer的子类,因为它不负责数据的输入输出,而专门用来管理磁盘文件与目录。

2、作用:File类主要用于命名文件、查询文件属性和处理文件目录。

public class File extends Object implements Serializable,Comparable{}

3、 构造函数

File类共提供了三个不同的构造函数,以不同的参数形式灵活地接收文件和目录名信息。构造函数:

3.1 File (String   pathname)    

例:

File  f1=new File("FileTest1.txt"); //创建文件对象f1,f1所指的文件是在当前目录下创建的FileTest1.txt

3.2 File (String  parent  ,  String child)

例:

File f2=new  File(“D:\\dir1","FileTest2.txt") ;//  注意:D:\\dir1目录事先必须存在,否则异常

3.3 File (File    parent  , String child)

例:

File  f4=new File("\\dir3");
File  f5=new File(f4,"FileTest5.txt");  //在如果 \\dir3目录不存在使用f4.mkdir()先创建     

3.4 常用方法

一个对应于某磁盘文件或目录的File对象一经创建, 就可以通过调用它的方法来获得文件或目录的属性。    

public boolean exists( ) 判断文件或目录是否存在
public boolean isFile( ) 判断是文件还是目录 
public boolean isDirectory( ) 判断是文件还是目录
public String getName( ) 返回文件名或目录名
public String getPath( ) 返回文件或目录的路径。
public long length( ) 获取文件的长度 
public String[ ] list ( ) 将目录中所有文件名保存在字符串数组中返回。 
File类中还定义了一些对文件或目录进行管理、操作的方法,常用的方法有:
public boolean renameTo( File newFile ); 重命名文件
public void delete( ); 删除文件
public boolean mkdir( ); 创建目录   

例子:

import java.io.File;
import java.io.IOException;
public class TestFile {
public static void main(String args[]) throws IOException {
File dir = new File("\\root");
File f1 = new File(dir, "fileOne.txt");
File f2 = new File(dir, "fileTwo.java");
// 文件对象创建后,指定的文件或目录不一定物理上存在
if (!dir.exists())
dir.mkdir();
if (!f1.exists())
f1.createNewFile();
if (!f2.exists())
f2.createNewFile();
System.out.println("f1's AbsolutePath= " + f1.getAbsolutePath());
System.out.println("f1 Canread=" + f1.canRead());
System.out.println("f1's len= " + f1.length());
String[] FL;
int count = 0;
FL = dir.list();
for (int i = 0; i < FL.length; i++) {
count++;
System.out.println(FL[i] + "is in \\root");
}
System.out.println("there are" + count + "file in //root");
}
}

说明:File类的方法:

  • exists()测试磁盘中指定的文件或目录是否存在
  • mkdir()创建文件对象指定的目录(单层目录)
  • createNewFile()创建文件对象指定的文件
  • list()返回目录中所有文件名字符串

六、 Java.IO流类库

1. io流的四个基本类

1.1 java.io包中包含了流式I/O所需要的所有类。

在java.io包中有四个基本类:InputStream、OutputStream及Reader、Writer类,它们分别处理字节流和字符流。

基本数据流的I/O
输入/输出 字节流 字符流
输入流 Inputstream Reader
输出流 OutputStream Writer

1.2 IO框架:

    

 1.3 Java中其他多种多样变化的流均是由它们派生出来的:

 

 

 

 

 

 

 

 

 

      JDK1.4版本开始引入了新I/O类库,它位于java.nio包中,新I/O类库利用通道和缓冲区等来提高I/O操作的效率。

      在java.io包中, java.io.InputStream 表示字节输入流, java.io.OutputStream表示字节输出流,处于java.io包最顶层。这两个类均为抽象类,也就是说它们不能被实例化,必须生成子类之后才能实现一定的功能。

2、io流的具体分类

2.1 按I/O类型来总体分类:

2.1.1Memory 

  • 从/向内存数组读写数据: CharArrayReader、 CharArrayWriter、ByteArrayInputStream、ByteArrayOutputStream
  • 从/向内存字符串读写数据 StringReader、StringWriter、StringBufferInputStream

2.1.2.Pipe管道

实现管道的输入和输出(进程间通信): PipedReader、PipedWriter、PipedInputStream、PipedOutputStream

2.1.3.File 文件流。

对文件进行读、写操作 :FileReader、FileWriter、FileInputStream、FileOutputStream

2.1.4. ObjectSerialization 对象输入、输出 :ObjectInputStream、ObjectOutputStream

2.1.5.DataConversion数据流

按基本数据类型读、写(处理的数据是Java的基本类型(如布尔型,字节,整数和浮点数)):DataInputStream、DataOutputStream

2.1.6.Printing 包含方便的打印方法 :PrintWriter、PrintStream

2.1.7.Buffering缓冲

在读入或写出时,对数据进行缓存,以减少I/O的次数:BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream

2.1.8.Filtering 滤流,在数据进行读或写时进行过滤:FilterReader、FilterWriter、FilterInputStream、FilterOutputStream

2.1.9.Concatenation合并输入

把多个输入流连接成一个输入流 :SequenceInputStream

2.1.10.Counting计数 在读入数据时对行记数 :LineNumberReader、LineNumberInputStream
2.1.11.Peeking Ahead 通过缓存机制,进行预读 :PushbackReader、PushbackInputStream
2.1.12.Converting between Bytes and Characters

按照一定的编码/解码标准将字节流转换为字符流,或进行反向转换(Stream到Reader,Writer的转换类):InputStreamReader、OutputStreamWriter

2.2 按数据来源(去向)分类: 

2.2.1、File(文件): FileInputStream, FileOutputStream, FileReader, FileWriter
2.2.2、byte[]:ByteArrayInputStream, ByteArrayOutputStream
2.2.3、Char[]: CharArrayReader, CharArrayWriter
2.2.4、String: StringBufferInputStream, StringReader, StringWriter
2.2.5、网络数据流:InputStream, OutputStream, Reader, Writer

七、 字节流InputStream/OutputStream

 1. InputStream抽象类 

      InputStream 为字节输入流,它本身为一个抽象类,必须依靠其子类实现各种功能,此抽象类是表示字节输入流的所有类的超类。 继承自InputStream  的流都是向程序中输入数据的,且数据单位为字节(8bit);

InputStream是输入字节数据用的类,所以InputStream类提供了3种重载的read方法。

1.1、Inputstream类中的常用方法: 

  (1) public abstract int read( ):读取一个byte的数据,返回值是高位补0的int类型值。若返回值=-1说明没有读取到任何字节读取工作结束。
  (2) public int read(byte b[ ]):读取b.length个字节的数据放到b数组中。返回值是读取的字节数。该方法实际上是调用下一个方法实现的 
  (3) public int read(byte b[ ], int off, int len):从输入流中最多读取len个字节的数据,存放到偏移量为off的b数组中。 
  (4) public int available( ):返回输入流中可以读取的字节数。注意:若输入阻塞,当前线程将被挂起,如果InputStream对象调用这个方法的话,它只会返回0,这个方法必须由继承InputStream类的子类对象调用才有用, 
  (5) public long skip(long n):忽略输入流中的n个字节,返回值是实际忽略的字节数, 跳过一些字节来读取 
  (6) public int close( ) :我们在使用完后,必须对我们打开的流进行关闭. 

1.2、 主要的子类:

         1) FileInputStream把一个文件作为InputStream,实现对文件的读取操作     
    2) ByteArrayInputStream:把内存中的一个缓冲区作为InputStream使用 
    3) StringBufferInputStream:把一个String对象作为InputStream 
    4) PipedInputStream:实现了pipe的概念,主要在线程中使用 
    5) SequenceInputStream:把多个InputStream合并为一个InputStream 

 2.OutputStream抽象类   

OutputStream提供了3个write方法来做数据的输出,这个是和InputStream是相对应的。 

2.1常用方法  

       1. public void write(byte b[ ]):将参数b中的字节写到输出流。 
  2. public void write(byte b[ ], int off, int len) :将参数b的从偏移量off开始的len个字节写到输出流。 
  3. public abstract void write(int b) :先将int转换为byte类型,把低字节写入到输出流中。 
  4. public void flush( ) : 将数据缓冲区中数据全部输出,并清空缓冲区。 
  5. public void close( ) : 关闭输出流并释放与流相关的系统资源。 

 2.2、主要的子类:   

      1) ByteArrayOutputStream:把信息存入内存中的一个缓冲区中 
      2) FileOutputStream:把信息存入文件中 
      3) PipedOutputStream:实现了pipe的概念,主要在线程中使用 
      4) SequenceOutputStream:把多个OutStream合并为一个OutStream 

流结束的判断:方法read()的返回值为-1时;readLine()的返回值为null时。

3. 文件输入流: FileInputStream类

3.1 定义

FileInputStream可以使用read()方法一次读入一个字节,并以int类型返回,或者是使用read()方法时读入至一个byte数组,byte数组的元素有多少个,就读入多少个字节。在将整个文件读取完成或写入完毕的过程中,这么一个byte数组通常被当作缓冲区,因为这么一个byte数组通常扮演承接数据的中间角色。

 

3.2 作用:

以文件作为数据输入源的数据流。或者说是打开文件,从文件读数据到内存的类。

3.3 使用方法

使用方法(1)    

 File  fin=new File("d:/abc.txt"); 
  FileInputStream in=new FileInputStream( fin);

使用方法(2)

FileInputStream  in=new  FileInputStream(“d: /abc.txt”);

3.4 程序举例:

将InputFromFile.java的程序的内容显示在显示器上

import java.io.IOException;
import java.io.FileInputStream;
;
public class TestFile {
public static void main(String args[]) throws IOException {
try{    
FileInputStream rf=new FileInputStream("InputFromFile.java");
int n=512; byte buffer[]=new byte[n]; 
while((rf.read(buffer,0,n)!=-1)&&(n>0)){
System.out.println(new String(buffer) );
}
System.out.println();
rf.close();
} catch(IOException IOe){    
System.out.println(IOe.toString());
}

}

}

4.文件输出流:FileOutputStream类

4.1  作用: 

用来处理以文件作为数据输出目的数据流;或者说是从内存区读数据入文件。
FileOutputStream类用来处理以文件作为数据输出目的数据流;一个表示文件名的字符串,也可以是File或FileDescriptor对象。 

4.2 创建一个文件流对象有两种方法: 

方式1: 

File   f=new  File (“d:/myjava/write.txt ");
FileOutputStream out= new FileOutputStream (f);

方式2: 

FileOutputStream out=new FileOutputStream(“d:/myjava/write.txt "); 

方式3:构造函数将 FileDescriptor()对象作为其参数。 

FileDescriptor() fd=new FileDescriptor(); 
FileOutputStream f2=new FileOutputStream(fd); 

方式4:构造函数将文件名作为其第一参数,将布尔值作为第二参数。 

FileOutputStream f=new FileOutputStream("d:/abc.txt",true); 

注意: (1)文件中写数据时,若文件已经存在,则覆盖存在的文件;(2)的读/写操作结束时,应调用close方法关闭流。 

4.3 程序举例:

使用键盘输入一段文章,将文章保存在文件write.txt中

import java.io.IOException;
import java.io.FileOutputStream;
public class TestFile {
public static void main(String args[]) throws IOException {
try {
System.out.println("please Input from Keyboard");
int count, n = 512;
byte buffer[] = new byte[n];
count = System.in.read(buffer);
FileOutputStream wf = new FileOutputStream("d:/myjava/write.txt");
wf.write(buffer, 0, count);
wf.close(); // 当流写操作结束时,调用close方法关闭流。
System.out.println("Save to the write.txt");
} catch (IOException IOe) {
System.out.println("File Write Error!");
}
}

}

5. FileInputStream流和FileOutputStream的应用

利用程序将文件file1.txt 拷贝到file2.txt中。

import java.io.File;
import java.io.IOException;
import java.io.FileOutputStream;
import java.io.FileInputStream;

public class TestFile {
public static void main(String args[]) throws IOException {
try {
File inFile = new File("copy.java");
File outFile = new File("copy2.java");
FileInputStream finS = new FileInputStream(inFile);
FileOutputStream foutS = new FileOutputStream(outFile);
int c;
while ((c = finS.read()) != -1) {
foutS.write(c);
}
finS.close();
foutS.close();
} catch (IOException e) {
System.err.println("FileStreamsTest: " + e);
}
}

}

6. 缓冲输入输出流 BufferedInputStream/ BufferedOutputStream

     

       计算机访问外部设备非常耗时。访问外存的频率越高,造成CPU闲置的概率就越大。为了减少访问外存的次数,应该在一次对外设的访问中,读写更多的数据。为此,除了程序和流节点间交换数据必需的读写机制外,还应该增加缓冲机制。缓冲流就是每一个数据流分配一个缓冲区,一个缓冲区就是一个临时存储数据的内存。这样可以减少访问硬盘的次数,提高传输效率。

BufferedInputStream:当向缓冲流写入数据时候,数据先写到缓冲区,待缓冲区写满后,系统一次性将数据发送给输出设备。

BufferedOutputStream :当从向缓冲流读取数据时候,系统先从缓冲区读出数据,待缓冲区为空时,系统再从输入设备读取数据到缓冲区。

1)将文件读入内存:
将BufferedInputStream与FileInputStream相接

  FileInputStream in=new  FileInputStream( “file1.txt ” );

  BufferedInputStream bin=new  BufferedInputStream( in); 

2)将内存写入文件:

将BufferedOutputStream与 FileOutputStream相接

FileOutputStreamout=new FileOutputStream(“file1.txt”);

BufferedOutputStream  bin=new BufferedInputStream(out);

3)键盘输入流读到内存
将BufferedReader与标准的数据流相接 

 InputStreamReader sin=new InputStreamReader (System.in) ;
BufferedReader bin=new             BufferedReader(sin);
import java.io.*;

public class ReadWriteToFile {
public static void main(String args[]) throws IOException {
InputStreamReader sin = new InputStreamReader(System.in);
BufferedReader bin = new BufferedReader(sin);
FileWriter out = new FileWriter("myfile.txt");
BufferedWriter bout = new BufferedWriter(out);
String s;
while ((s = bin.readLine()).length() > 0) {
bout.write(s, 0, s.length());
}

}
}

程序说明:
从键盘读入字符,并写入到文件中BufferedReader类的方法:String readLine()
作用:读一行字符串,以回车符为结束。
BufferedWriter类的方法:bout.write(String s,offset,len)
作用:从缓冲区将字符串s从offset开始,len长度的字符串写到某处。

八、 字符流Writer/Reader

Java中字符是采用Unicode标准,一个字符是16位,即一个字符使用两个字节来表示。为此,JAVA中引入了处理字符的流。

1. Reader抽象类

    用于读取字符流的抽象类。子类必须实现的方法只有 read(char[], int, int) 和 close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。

1.1 FileReader :与FileInputStream对应

主要用来读取字符文件,使用缺省的字符编码,有三种构造函数: 

1.1.1 将文件名作为字符串 :

FileReader f=new FileReader(“c:/temp.txt”); 

1.1.2构造函数将File对象作为其参数。 

File f=new file(“c:/temp.txt”); 
FileReader f1
=new FileReader(f);

1.1.3 构造函数将FileDescriptor对象作为参数

FileDescriptor() fd=new FileDescriptor() 
FileReader f2=new FileReader(fd); 

1.2 CharArrayReader:与ByteArrayInputStream对应

(1) 用指定字符数组作为参数:CharArrayReader(char[]) 
(2) 将字符数组作为输入流:CharArrayReader(char[], int, int) 

1.3 StringReader : 与StringBufferInputStream对应

读取字符串,构造函数如下: 

public StringReader(String s); 

1.4 InputStreamReader

从输入流读取字节,在将它们转换成字符:

Public inputstreamReader(inputstream is);

1.5 FilterReader: 允许过滤字符流

protected filterReader(Reader r); 

1.6BufferReader :

接受Reader对象作为参数,并对其添加字符缓冲器,使用readline()方法可以读取一行。

Public BufferReader(Reader r);

1.7 主要方法:

(1)  public int read() throws IOException; //读取一个字符,返回值为读取的字符 
(2)  public int read(char cbuf[]) throws IOException; /*读取一系列字符到数组cbuf[]中,返回值为实际读取的字符的数量*/ 
(3)  public abstract int read(char cbuf[],int off,int len) throws IOException; 
/*读取len个字符,从数组cbuf[]的下标off处开始存放,返回值为实际读取的字符数量,该方法必须由子类实现*/ 

2. Writer抽象类

     写入字符流的抽象类。子类必须实现的方法仅有 write(char[], int, int)、flush() 和 close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。 其子类如下:

2.1 FileWrite: 与FileOutputStream对应

将字符类型数据写入文件,使用缺省字符编码和缓冲器大小。

Public FileWrite(file f); 

2.2 chararrayWrite:与ByteArrayOutputStream对应 ,将字符缓冲器用作输出。 

Public CharArrayWrite(); 

2.3 PrintWrite:生成格式化输出

public PrintWriter(outputstream os); 

2.4 filterWriter:用于写入过滤字符流 

protected FilterWriter(Writer w); 

2.5 PipedWriter:与PipedOutputStream对应   

2.6 StringWriter:无与之对应的以字节为导向的stream  

2.7  主要方法

(1)  public void write(int c) throws IOException; //将整型值c的低16位写入输出流 
(2)  public void write(char cbuf[]) throws IOException; //将字符数组cbuf[]写入输出流 
(3)  public abstract void write(char cbuf[],int off,int len) throws IOException; //将字符数组cbuf[]中的从索引为off的位置处开始的len个字符写入输出流 
(4)  public void write(String str) throws IOException; //将字符串str中的字符写入输出流 
(5)  public void write(String str,int off,int len) throws IOException; //将字符串str 中从索引off开始处的len个字符写入输出流 
(6)  flush( ) //刷空输出流,并输出所有被缓存的字节。 
(7)  close()    关闭流 public abstract void close() throws IOException

3 .InputStream与Reader差别 OutputStream与Writer差别

InputStream和OutputStream类处理的是字节流,数据流中的最小单位是字节(8个bit)。
Reader与Writer处理的是字符流,在处理字符流时涉及了字符编码的转换问题。

import java.io.*;
public class EncodeTest {
private static void readBuff(byte [] buff) throws IOException {
ByteArrayInputStream in =new ByteArrayInputStream(buff);
int data;
while((data=in.read())!=-1) System.out.print(data+" ");
System.out.println(); in.close(); }

public static void main(String args[]) throws IOException {
System.out.println("内存中采用unicode字符编码:" );
char c='好';
int lowBit=c&0xFF; int highBit=(c&0xFF00)>>8;
System.out.println(""+lowBit+" "+highBit);
String s="好";
System.out.println("本地操作系统默认字符编码:");
readBuff(s.getBytes());
System.out.println("采用GBK字符编码:");
readBuff(s.getBytes("GBK"));
System.out.println("采用UTF-8字符编码:"); 
readBuff(s.getBytes("UTF-8")); }
}

Reader类能够将输入流中采用其他编码类型的字符转换为Unicode字符,然后在内存中为其分配内存。
Writer类能够将内存中的Unicode字符转换为其他编码类型的字符,再写到输出流中。

九、 FileWriter类(字符输出流类)

1、构造方法:

1.1

FileWriter fw = new FileWriter(String fileName);//创建字符输出流类对象和已存在的文件相关联。文件不存在的话,并创建。

如:

FileWriter fw = new FileWriter("C:\\1.txt");

1.2

FileWriter fw = new FileWriter(String fileName,boolean append);//创建字符输出流类对象和已存在的文件相关联,并设置该该流对文件的操作是否为续写。

如:

FileWriter fw = new FileWriter("C:\\1.txt",ture); //表示在fw对文件再次写入时,会在该文件的结尾续写,并不会覆盖掉。

2、主要方法: 

        void write(String str)   //写入字符串。当执行完此方法后,字符数据还并没有写入到目的文件中去。此时字符数据会保存在缓冲区中。此时在使用刷新方法就可以使数据保存到目的文件中去。

 viod flush()                //刷新该流中的缓冲。将缓冲区中的字符数据保存到目的文件中去。

 viod close()               //关闭此流。在关闭前会先刷新此流的缓冲区。在关闭后,再写入或者刷新的话,会抛IOException异常。

十、 如何选择IO流

1、确定是数据源和数据目的(输入还是输出)

              源:输入流 InputStream Reader
              目的:输出流 OutputStream Writer

2、明确操作的数据对象是否是纯文本

             是:字符流Reader,Writer
             否:字节流InputStream,OutputStream

3、明确具体的设备。

3.1 是硬盘文件:File++:

  • 读取:FileInputStream,, FileReader, 
  • 写入:FileOutputStream,FileWriter

3.2 是内存用数组

  • byte[]:ByteArrayInputStream, ByteArrayOutputStream
  • char[]:CharArrayReader, CharArrayWriter

3.3 是String:StringBufferInputStream(已过时,因为其只能用于String的每个字符都是8位的字符串), StringReader, StringWriter

3.4 是网络用Socket流

3.5是键盘:用System.in(是一个InputStream对象)读取,用System.out(是一个OutoutStream对象)打印          

4、是否需要转换流

            是,就使用转换流,从Stream转化为Reader,Writer:InputStreamReader,OutputStreamWriter 

5、是否需要缓冲提高效率

       是就加上Buffered:BufferedInputStream, BufferedOuputStream, BuffereaReader, BufferedWriter

6、是否需要格式化输出

例:将一个文本文件中数据存储到另一个文件中。
     1)数据源和数据目的:读取流,InputStream Reader  输出:OutputStream Writer
     2)是否纯文本:是!这时就可以选择Reader Writer。
     3)设备:是硬盘文件。Reader体系中可以操作文件的对象是 FileReader FileWriter。

            FileReader fr = new FileReader("a.txt");  

             FileWriter fw = new FileWriter("b.txt");  
     4)是否需要提高效率:是,加Buffer
             BufferedReader bfr = new BufferedReader(new FileReader("a.txt");  );  
             BufferedWriter bfw = new BufferedWriter(new FileWriter("b.txt");  );  

十一、 IOException异常类的子类

1.public class  EOFException :
   非正常到达文件尾或输入流尾时,抛出这种类型的异常。
2.public class FileNotFoundException:
   当文件找不到时,抛出的异常。
3.public class InterruptedIOException:
   当I/O操作被中断时,抛出这种类型的异常。
————————————————
原文链接:https://blog.csdn.net/hguisu/article/details/7418161

其他链接:https://blog.csdn.net/liwangcuihua/article/details/131050868

十二、I/O中常见问题整理 

1、字节与字符

1.1字节

1.1.1 定义

字节(Byte )是计算机信息技术用于计量存储容量的一种计量单位,作为一个单位来处理的一个二进制数字串,是构成信息的一个小单位。最常用的字节是八位的字节,即它包含八位的二进制数。计算机中数据的基本单位。

通常在读取图片、声音、可执行文件需要用字节数组来保存文件,在下载文件也是用byte数组来做临时的缓冲器接收文件内容。

  • 位,bit:一个二进制位;二进制数据0或1。
  • 字节,byte:1byte=8bit ;1个字节等于8位,存储空间的基本计量单位。
  • 一个英文字母用1个字节表示,也就是8位二进制数。

1.2 字符

1.2.1定义

字符指类字形单位或符号,包括字母、数字、运算符号、标点符号和其他符号,以及一些功能性符号。字符是电子计算机或无线电通信中字母、数字、符号的统称,其是数据结构中最小的数据存取单位,通常由8个二进制位(一个字节)来表示一个字符。 [1]  字符是计算机中经常用到的二进制编码形式,也是计算机中最常用到的信息形式。

1.2.2 说明

机器只知道字节,而字符却是语义上的单位,它是有编码的,一个字符可能编码成1个2个甚至3个4个字节。这跟字符集编码有关系,英文字母和数字是单字节,但汉字这些自然语言中的字符是多字节的。一个字节只能表示256个字符,不可能用于全球那么多种自然语言的处理,因此肯定需要多字节的存储方式。

那么在文件的输入输出中,InputStream、OutputStream它们是处理字节流的,就是说假设所有东西都是二进制的字节;而 Reader, Writer 则是字符流,它涉及到字符集的问题;按照ANSI编码标准,标点符号、数字、大小写字母都占一个字节,汉字占2个字节。按照UNICODE标准所有字符都占2个字节。

Java采用unicode来表示字符,java中的一个char是2个字节,一个中文或英文字符的unicode编码都占2个字节,但如果采用其他编码方式,一个字符占用的字节数则各不相同。

在 GB 2312 编码或 GBK 编码中,一个英文字母字符存储需要1个字节,一个汉子字符存储需要2个字节。

在UTF-8编码中,一个英文字母字符存储需要1个字节,一个汉字字符储存需要3到4个字节。

在UTF-16编码中,一个英文字母字符存储需要2个字节,一个汉字字符储存需要3到4个字节(Unicode扩展区的一些汉字存储需要4个字节)。

在UTF-32编码中,世界上任何字符的存储都需要4个字节。

1.2.3扩展的存储单位

在计算机各种存储介质(例如内存、硬盘、光盘等)的存储容量表示中,用户所接触到的存储单位不是位、字节和字,而是KB、MB、GB等,但这不是新的存储单位,而是基于字节换算的。

KB:1KB = 1024B 早期用的软盘有360KB和720KB的,不过软盘已经很少使用。

MB:1MB = 1024KB 早期微型机的内存有128MB、256MB、512MB,目前内存都是1GB、2GB甚至更大。

GB:1GB = 1024MB 早期微型机的硬盘有60GB、80GB,目前都是500GB、1TB甚至更大。

TB: 1TB = 1024GB 目前个人用的微型机存储容量也都能达到这个级别了,而作为服务器或者专门的计算机,不可缺少这么大的存储容量。

 2、编码

下述编码定义均参看自百度百科。

2.1 编码

2.1.1 定义

是用预先规定的方法将文字、数字或其它对象编成数码,或将信息、数据转换成规定的电脉冲信号。为保证编码的正确性,编码要规范化、标准化,即需有标准的编码格式。常见的编码格式有ASCII、ANSI、GBK、GB2312、UTF-8、GB18030和UNICODE等。

在电子计算机中,将指令和数字实行编码后,适合计算机运行和操作。编码作为计算机书写指令的过程,是程序设计活动的一部分。在数字磁记录中,可按照一定的规则,进行输入信息序列向编码序列的过程转换。在遥控系统和通信系统中,采用编码步骤可提高传送的效率和可靠性。

将数据转换为编码字符,必要时又可编码成原来的数据形式。

2.1.2 为什么会有多种编码

各个国家和地区所制定的不同 ANSI 编码标准中,都只规定了各自语言所需的“字符”。比如:汉字标准(GB2312)中没有规定韩国语字符怎样存储。这些 ANSI 编码标准所规定的内容包含两层含义:

使用哪些字符。也就是说哪些汉字,字母和符号会被收入标准中。所包含“字符”的集合就叫做“字符集”。

规定每个“字符”分别用一个字节还是多个字节存储,用哪些字节来存储,这个规定就叫做“编码”。

各个国家和地区在制定编码标准的时候,“字符的集合”和“编码”一般都是同时制定的。因此,平常所说的“字符集”,比如:GB2312、GBK、JIS 等,除了有“字符的集合”这层含义外,同时也包含了“编码”的含义。

“UNICODE 字符集”包含了各种语言中使用到的所有“字符”。用来给 UNICODE 字符集编码的标准有很多种,比如:UTF-8、UTF-7、UTF-16、UnicodeLittle、UnicodeBig 等。

2.1.3 意义

如上文所述,编码是依据预先规定的标准将某一对象信息编程计算机可识别的数码,因此,如果没有规定标准的编码方法,那么有这些独立的、不统一的编码规则实现的程序,将不具兼容性,易出现如乱码等由于编码格式一致或不兼容引起的问题。

2.2 ASCII 

2.2.1 定义

ASCII ((American Standard Code for Information Interchange): 美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准,并等同于国际标准ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符  。

2.2.2 局限

在英语中,用128个符号编码便可以表示所有,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。

但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是一样的,不一样的只是128--255的这一段 [5]  。

至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号。

2.3 ANSI

2.3.1 定义

ANSI是一种字符代码,为使计算机支持更多语言,通常使用 0x00~0x7f 范围的1 个字节来表示 1 个英文字符。超出此范围的使用0x80~0xFFFF来编码,即扩展的ASCII编码。

2.3.2 起源

为使计算机支持更多语言,通常使用 0x80~0xFFFF 范围的 2 个字节来表示 1 个字符。比如:汉字 '中' 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。

不同的国家和地区制定了不同的标准,由此产生了 GB2312、GBK、GB18030、Big5、Shift_JIS 等各自的编码标准。这些使用多个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文Windows操作系统中,ANSI 编码代表 GBK 编码;在繁体中文Windows操作系统中,ANSI编码代表Big5;在日文Windows操作系统中,ANSI 编码代表 Shift_JIS 编码。

简单的说,在简体中文系统下,ANSI编码代表GB2312编码;在日文操作系统下,ANSI编码代表JS编码。

不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。ANSI编码表示英文字符时用一个字节,表示中文用两个或四个字节。

ANSI编码作为中国以及部分亚太地区的多字符编码格式,Windows系统和OS X都是提供原生支持的。但是即便如此,许多国外开发者仍然在开发笔记或者文字录入类应用的时候将ANSI编码完全忽略,只加入全球通用的UTF-8编码。

2.4 java中的字符是Unicode编码,那编码格式的UTF-8与GBK又是什么?

java的编码都是Unicode格式,即java内置的字符串所使用的字符集为Unicode。而我们在开发中设置的编码格式,是我们在新开的程序中所要用到的编码格式。

2.5、 Unicode

2.5.1 含义

含义:

Unicode(统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。1990年开始研发,1994年正式公布。

产生原因:

对于英文来讲,ASCII码就足以编码所有字符,但对于中文,则必须使用两个字节来代表一个汉字,这种表示汉字的方式习惯上称为双字节。虽然双字节可以解决中英文字符混合使用的情况,但对于不同字符系统而言,就要经过字符码转换,非常麻烦,如中英、中日、日韩混合的情况。为解决这一问题,很多公司联合起来制定了一套可以适用于全世界所有国家的字符码,不管是东方文字还是西方文字,一律用两个字节来表示,这就是UNICODE。

2.5.2 起源

因为计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。最早的计算机在设计时采用8个比特(bit)作为一个字节(byte),所以,一个字节能表示的最大的整数就是255(二进制11111111=十进制255),0 - 255被用来表示大小写英文字母、数字和一些符号,这个编码表被称为ASCII编码,比如大写字母A的编码是65,小写字母z的编码是122。

如果要表示中文,显然一个字节是不够的,至少需要两个字节,而且还不能和ASCII编码冲突,所以,中国制定了GB2312编码,用来把中文编进去。

类似的,日文和韩文等其他语言也有这个问题。为了统一所有文字的编码,Unicode应运而生。Unicode把所有语言都统一到一套编码里,这样就不会再有乱码问题了。

Unicode通常用两个字节表示一个字符,原有的英文编码从单字节变成双字节,只需要把高字节全部填为0就可以。

2.5.3 作用

能够使计算机实现跨语言、跨平台的文本转换及处理。

2.5.4 实现

Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。目前的Unicode字符分为17组编排,0x0000 至 0x10FFFF,每组称为平面

(Plane),而每平面拥有65536个码位,共1114112个。

然而目前只用了少数平面。UTF-8、UTF-16、UTF-32都是将数字转换到程序数据的编码方案。Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。目前的Unicode字符分为17组编排,0x0000 至 0x10FFFF,每组称为平面(Plane),而每平面拥有65536个码位,共1114112个。然而目前只用了少数平面。UTF-8、UTF-16、UTF-32都是将数字转换到程序数据的编码方案。 

2.5.5 使用

基本上,计算机只是处理数字。它们指定一个数字,来储存字母或其他字符。在创造Unicode之前,有数百种指定这些数字的编码系统。没有一个编码可以包含足够的字符:例如,单单欧洲共同体就需要好几种不同的编码来包括所有的语言。即使是单一种语言,例如英语,也没有哪一个编码可以适用于所有的字母,标点符号,和常用的技术符号。这些编码系统也会互相冲突。也就是说,两种编码可能使用相同的数字代表两个不同的字符,或使用不同的数字代表相同的字符。任何一台特定的计算机(特别是服务器)都需要支持许多不同的编码,但是,不论什么时候数据通过不同的编码或平台之间,那些数据总会有损坏的危险。

2.5.6 为什么使用Unicode

其实原因很简单,因为Unicode比ANSI好用。 自从Windows2K开始,Win的系统内核开始完全支持并完全应用Unicode编写,所有ANSI字符在进入底层前,都会被相应的API转换成Unicode。所以,如果你一开始就使用Unicode,则可以减少转换的用时和RAM开销。 对于JAVA/.NET等这些“新”的语言来说,内置的字符串所使用的字符集已经完全是Unicode。最重要的是,世界上大多数程序用的字符集都是Unicode,因为Unicode有利于程序国际化和标准化。

2.5.7 总结:

即,Unicode编码方式,就是语言的编码大全,把世界上的每一种语言的每一个字都用一个(且唯一的)数字来表示。

3、读写流的方法

好的我们先来讲它们的作用,然后再用代码来实现给大家看:

3.1 read():

1.从读取流读取的是一个一个字节

2.返回的是字节的(0-255)内的字节值

3.读一个下次就自动到下一个,如果碰到-1说明没有值了.

3.2 read(byte[] bytes)

1.从读取流读取一定数量的字节,如果比如文件总共是102个字节

2.我们定义的数组长度是10,那么默认前面10次都是读取10个长度

3.最后一次不够十个,那么读取的是2个

4.这十一次,每次都是放入10个长度的数组.

3.3 read(byte[] bytes,int off ,int len)

1.从读取流读取一定数量的字节,如果比如文件总共是102个字节

2.我们定义的数组长度是10,但是这里我们写read(bytes,0,9)那么每次往里面添加的(将只会是9个长度),就要读12次,最后一次放入3个.

3.所以一般读取流都不用这个而是用上一个方法:read(byte[]);

下面讲解write

3.4 write(int i);

直接往流写入字节形式的(0-255)int值.

3.5 write(byte[] bytes);

往流里边写入缓冲字节数组中的所有内容,不满整个数组长度的”空余内容”也会加入,这个下面重点讲,

3.6write(byte[] bytes,int off,int len);

1.这个是更严谨的写法,在外部定义len,然后每次len(为的是最后一次的细节长度)都等于流往数组中存放的长度

2.如上述read(bytes),前面每次都放入十个,第十一次放入的是2个,如果用第二种write(bytes),将会写入输出流十一次,每次写入十个长度,造成最后一次输出流的数组后面有8个空的,比原来的内容多了

3.所以用write(byte[] bytes,int off,int len);就不会出现多出来的空的情况,因为最后一次len不同

下面是详细的代码

public class Test{
public static void main(String[] args) throws Exception {

UseTimeTool.getInstance().start();

FileInputStream fis = new FileInputStream("D:\\1.mp3");
FileOutputStream fos = new FileOutputStream("D:\\1copy.mp3");
//(PS:一下3个大家分开来写和测试,为了方便我都列出来了)
/*--------------不使用缓冲--------------*/
//如果不缓冲,花了差不多14"秒"
int len = -1;
while ((len = fis.read()) != -1) {
//这里就不是长度的问题了,而是读取的字节"内容",读到一个写一个,相当慢.
System.out.println("len : "+ len); 
fos.write(len);
}

/*--------------使用缓冲--------------*/
//缓冲方法复制歌曲用了不到20"毫秒"
//创建一个长度为1024的字节数组,每次都读取5kb,目的是缓存,如果不用缓冲区,用fis.read(),就会效率低,一个一个读字节,缓冲区是一次读5000个
byte[] bytes = new byte[1024*5];
//每次都是从读取流中读取(5k)长度的数据,然后再写到文件去(5k的)数据,注意,每次读取read都会不同,是获取到下一个,直到后面最后一个.
while (fis.read(bytes)!=-1) {
//write是最追加到文件后面,所以直接每次添5K.
fos.write(bytes); 
}
/*--------------解释len--------------*/
//告诉你为什么用len
byte[] bytes = new byte[1024*5];
int len = -1;
//解释这个fis.read(bytes)的意思:从读取流"读取数组长度"的数据(打印len可知),并放入数组
while ((len = fis.read(bytes,0,1024)) != -1) {
//虽然数组长度的*5,但是这里我们设置了1024所以每次输出1024
System.out.println("len : "+ len);
//因为每次得到的是新的数组,所以每次都是新数组的"0-len"
fos.write(bytes,0,len);
}

fis.close();
fos.close();
UseTimeTool.getInstance().stop();
}
}

为了方便大家,也给大家一个统计时间的工具类

class UseTimeTool {
private static UseTimeTool utt = new UseTimeTool();
private UseTimeTool() {
}
public static UseTimeTool getInstance() {
return utt;
}
private long start;
public void start() {
start = System.currentTimeMillis();
}
public void stop() {
long end = System.currentTimeMillis();
System.out.println("所用時間 : " + (end - start) + "毫秒");
}
}

好了最后一个:len问题 最后多出数组不满的部分我特再写一个出来给大家分析
首先,文本的内容是

 

public class Test{
public static void main(String[] args) throws Exception {

UseTimeTool.getInstance().start();
FileInputStream fis = new FileInputStream("D:\\a.txt");
FileOutputStream fos = new FileOutputStream("D:\\acopy.txt");

不使用len:

byte[] bytes = new byte[1024*5];
while (fis.read(bytes)!=-1) {
fos.write(bytes); 
}

得到的效果:

发现后续后很多的空部分,所以说不严谨

使用len:

byte[] bytes = new byte[1024*5];
int len = -1;
while ((len = fis.read(bytes,0,1024)) != -1) {
fos.write(bytes,0,len);
}

得到的效果

和原来一模一样,讲了那么多就是希望能帮助大家真正的理解.
————————————————
原文链接:https://blog.csdn.net/nzfxx/article/details/51802017

4、关闭流与刷新流

4.1 关闭流:

见链接https://www.cnblogs.com/vole/p/12175911.html

4.2 为什么要关闭流

当我们new一个java流对象之后,不仅在计算机内存中创建了一个相应类的实例对象。而且,还占用了相应的系统资源,比如:文件句柄、端口、数据库连接等。在内存中的实例对象,当没有引用指向的时候,java垃圾收集器会按照相应的策略自动回收,但是却无法对系统资源进行释放。所以,我们需要主动调用close()方法释放java流对象。在java7之后,可以使用try-with-resources语句来释放java流对象,从而避免了try-catch-finally语句的繁琐,尤其是在finally子句中,close()方法也会抛出异常。

4.3 flush()方法

4.3.1

java中 flush专业术语叫缓冲区。

当你print或者write的时候,会暂时保存在缓冲区,并没有发送出去,这是出于效率考虑的,因为数据不会自己发送过去,必须有其他机制,而且这个很消耗资源,就像马桶你需要很多水,才能冲走,你如果扔一点东西,就冲一次水,那你水费要爆表了,同样如果你写一行文字,或者一个字节,就要马上发送出去,那网络流量,CPU使用率等等都要爆表了,所以一般只有在你真正需要发送否则无法继续的时候,调用flush,将数据发送出去。 

因为操作系统的某些机制,为了防止一直不停地磁盘读写,所以有了延迟写入的概念。在网络web服务器上也是,为了防止写一个字节就发送一个消息,所以有缓冲区的概念,比如64K的内存区域,缓冲区写满了再一次性写入磁盘之中(或者发送给客户端浏览器)。flush方法一般是程序写入完成时执行。随后跟着close方法。

4.3.2

由于该知识点涉及到I/O操作,因此,在这里简单的对I/O操作做简单的说明。

冯诺依曼体系结构中,将计算机分为运算器、控制器、存储器、输入/输出设备。而运算器、控制器是CPU的组成成分(还有一些寄存器)。存储器则可以分为内存储器(内存)和外存储器(硬盘)。输入输出设备主要用来完成系统的I/O操作。I/O操作主要是对硬盘中的数据进行读和取。由于CPU的运算速度远远大于I/O操作,因此,当一个进程需要产生许多I/O操作时,会耗费许多系统资源,同时也不利于进程之间的资源竞争,导致系统资源的利用率下降。

由于CPU不能够直接访问外存(硬盘),而需要借助于内存来完成对硬盘中数据的读/取操作。想要完成此操作又不得不借助于I/0系统。但是,JAVA中的输入/输入流在默认情况下,是不被缓存区缓存的,因此,每发生一次read()方法和write()方法都需要请求操作系统再分发/接收一个字节,这样程序的运行效率必然会降低,相比之下,请求一个数据块并将其置于缓冲区中会显得更加高效。于是我们考虑可以将硬盘中的数据事先添加到预定义范围(大小合适)的缓冲池中来预存数据,待CPU产生I/O操作时,可以从这个缓存池中来读取数据,这样便减少了CPU的I/O的次数,提高了程序的运行效率。JAVA也正是采用这种方式,通过为基本流添加了处理流(缓冲机制)来减少I/O操作的次数。

JAVA中的write()方法和flush()方法是抽象基类OutputStream/Writer中的两个方法。其中,OutputStream类中的主要方法包括以下几个:

read()方法和write()是线程阻塞的,也就是说,当某个线程试图向另一端网络节点读取或写入数据时,有可能会发生网络连接异常或者是服务器短期内没有响应,这将会导致该线程阻塞,同样地,在无数据状态进行读取,数据已满进行写操作时,同样会发生阻塞,这时,其他线程抢占资源后继续执行。如果出现此现状,读取到缓冲池中的数据不能够及时的发送到另一端的网络节点,需要该线程再次竞争到CPU资源才可正常发送。

还有一种情况,当我们将数据预存到缓冲池中时,当数据的长度满足缓冲池中的大小后,才会将缓冲池中的数据成块的发送,若数据的长度不满足缓冲池中的大小,需要继续存入,待数据满足预存大小后再成块的发送。往往在发送文件过程中,文件末尾的数据大小不能满足缓冲池的大小。最终导致这部分的数据停留在缓冲池无法发送。

这时,就需要我们在write()方法后,手动调用flush()方法,强制刷出缓冲池中的数据,(即使数据长度不满足缓冲池的大小)从而保证数据的正常发送。当然,当我们调用流的close()方法后,系统也会自动将输出流缓冲区的数据刷出,同时可以保证流的物理资源被回收。

4.3.3 close()和flush的区别:

  • flush(): 刷新缓冲区,流对象还可以以继续使用
  • close():先刷新缓冲区,然后通知系统释放资源,流对象不可以再被使用了

原文链接:https://blog.csdn.net/qq_35271409/article/details/82057096

5、序列化说明

5.1 什么是序列化

众所周知,类的对象会随着程序的终止而被垃圾收集器销毁。如果要在不重新创建对象的情况下调用该类,该怎么做?这就可以通过序列化将数据转换为字节流。

类的序列化:如果希望将对象写进文件,必须具备一种能力,具备可序列化的能力,让自定义的类实现Serializable接口,以开启序列化的按钮。

反序列化:从文件中读取流,反序列化得到被序列化的对象。

对象序列化是一个用于将对象状态转换为字节流的过程,可以将其保存到磁盘文件中或通过网络发送到任何其他程序;

从字节流创建对象的相反的过程称为反序列化。而创建的字节流是与平台无关的,在一个平台上序列化的对象可以在不同的平台上反序列化。

5.2 如何使Java类可序列化?

通过实现java.io.Serializable接口,可以在Java类中启用可序列化。它是一个标记接口,意味着它不包含任何方法或字段,仅用于标识可序列化的语义。

5.3 如果我们试图序列化不可序列化的对象怎么办?

我们将得到一个 RuntimeException 异常:主线程中出现异常 java.io.NotSerializableException。

5.4 什么是serialVersionUID?

SerialVersionUID是一个标识符,当它通常使用对象的哈希码序列化时会标记在对象上。我们可以通过Java中serialver工具找到该对象的serialVersionUID。

语法:serialver classname,SerialVersionUID用于对象的版本控制。当您添加或修改类中的任何字段时,已经序列化的类将无法恢复,因为serialVersionUID已为新类生成与旧的序列化对象将不同。Java序列化过程依赖于正确的serialVersionUID恢复序列化对象的状态,并在serialVersionUID不匹配时抛出java.io.InvalidClassException 异常。

5.5 Transient 关键字

transient修饰符仅适用于变量,不适用于方法和类。在序列化时,如果我们不想序列化特定变量以满足安全约束,那么我们应该将该变量声明为transient。执行序列化时,JVM会忽略transient变量的原始值并将默认值保存到文件中。因此,transient意味着不要序列化。

5.6 Transient 与 Static

静态变量不是对象状态的一部分,因此它不参与序列化。所以将静态变量声明为transient变量是没有用处的。

5.7 Final 与 Transient

final变量将直接通过值参与序列化,所以将final变量声明为transient变量不会产生任何影响。现在,让我们考虑一个显示Java中的序列化和反序列化的程序。

代码示例:

如果对象已写进文件,现在版本升级,User类增加了密码的属性,那么读数据会读出密码吗?如果读到值是多少?会,null

如不添加序列号,升级后,会报错,认为写入的对象与读取之后还原的User对象,不是同一个。

 

解决以上问题,添加一个默认的序列版本号。

package com.asd.reserve.pojo.bean;

import java.io.Serializable;

/**
 * @author zs
 * @date 2020/1/10 15:07
 */
public class User implements Serializable{

    private static final long serialVersionUID = 1L;
    private String name;
    private String age;

    private String id;

    public String getName() {
        return name;
    }

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

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    public User(String name, String age) {
        this.name = name;
        this.age = age;
    }


    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    /*@Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }*/

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                ", id='" + id + '\'' +
                '}';
    }

    /*@Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", id='" + id + '\'' +
                '}';
    }*/


}


package com.asd.reserve.utils.file;

import com.asd.reserve.pojo.bean.User;

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

/**
 * @author zs
 * @date 2020/1/10 15:05
 */
public class SerializableTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //希望使用LinkedHashSet存储User对象-->有序的哈希表,唯 一,有序(添加顺序)
        LinkedHashSet<User> lhs=new LinkedHashSet<User>();
        //创建N个User类型的对象,同时添加到集合中
        /*lhs.add(new User("mary", "20"));
        lhs.add(new User("lili", "20"));
        lhs.add(new User("jack", "20"));*/

        //将集合写进文件
        //write(lhs);
        /**调用读取数据的方法**/
        LinkedHashSet<User> lhsetr=read();
        //遍历集合,输出对象的信息
        for (User user : lhsetr) {
            System.out.println(user);
        }

    }
    public static void write(LinkedHashSet<User> lhs) throws IOException{
        //【1】写对象,ObjectOutputStream
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("E:\\user1.bat"));
        //[2]写对象,将集合写进文件
        oos.writeObject(lhs);
        //【3】关闭
        oos.close();
    }

    public static LinkedHashSet<User>  read() throws IOException, ClassNotFoundException{

        //【1】创建对象
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("E:\\user.bat"));
        //【2】读对象
        LinkedHashSet<User> lhset=(LinkedHashSet<User>)ois.readObject();
        //【3】关闭
        ois.close();

        return lhset;
    }
}

说明:实现Serializable接口,以使类具有序列化,即写进文件的能力,不实现该接口,在通过I/O流输出数据时,会报异常:

Exception in thread "main" java.io.NotSerializableException: com.asd.reserve.pojo.bean.User

上述代码经测试,当删除属性后,反序列化时,读取不到该属性;新增的属性为null。

序列号的作用就是在Java进行序列化工作时,会将serialVersioinUid与所要序列化的目标一起序列化,这样一来,在反序列化的过程中会使用被序列化的serialVersioinUid与类中的serialVersioinUid对比,如果两者相等,则反序列化成功,否则,反序列化失败。

序列化生成的serialVersionUID要和反序列化的serialVersionUID要一致才可以认为是同一序列版本。

根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段。相当于生成serialVersionUID的算法的元素是类名、接口名、成员方法及属性等,如果有一样修改了,那么JVM虚拟机自动生成的serialVersionUID就会不一样。serialVersionUID可以手动指定,如果不指定则JAVA虚拟机自动根据类名、接口名、成员方法及属性等来生成。

一般都会自定义写死,不然如果变更了序列化的类,以前版本的数据反序列化就会失败。

虽然Java的序列化能够保证对象状态的持久保存,但是遇到一些对象结构复杂的情况还是比较难处理的,下面是对一些复杂情况的总结:

  • 当父类实现了Serializable接口的时候,所有的子类都能序列化
  • 子类实现了Serializable接口,父类没有,父类中的属性不能被序列化(不报错,但是数据会丢失)
  • 如果序列化的属性是对象,对象必须也能序列化,否则会报错
  • 反序列化的时候,如果对象的属性有修改或则删减,修改的部分属性会丢失,但是不会报错
  • 在反序列化的时候serialVersionUID被修改的话,会反序列化失败
  • 在存Java环境下使用Java的序列化机制会支持的很好,但是在多语言环境下需要考虑别的序列化机制,比如xml,json,或则protobuf

5.8 是否设置固定值

  看自己的需要,一般都设置成固定的。

  将所有序列化类的serialVersionUID都设置为1L可能会导致以下问题:

  版本控制问题:如果类的任何字段有改变,就应该改变 serialVersionUID,因为改变可能会影响序列化/反序列化的方式。如果你始终设置为1L,就无法利用这个机制来追踪类的版本。

  数据不一致问题:如果两个本质上不同的类,它们的 serialVersionUID 都是1L,那么在反序列化时,Java会认为它们是相同的类,这可能导致数据不一致或者类型转换错误。

  因此,建议在实际应用中,为每个类显式地定义一个唯一的serialVersionUID,并根据类的结构变化进行更新,以确保序列化和反序列化的兼容性和安全性

参考链接:

https://blog.csdn.net/qq_41638851/article/details/134462254

https://blog.csdn.net/L2735516933/article/details/133201106

十三、常见面试题

十四、使用

posted @ 2020-01-09 16:26  慎终若始  阅读(289)  评论(0编辑  收藏  举报