IO流

1.IO定义

  • Java中I/O操作主要是指使用Java进行输入,输出操作. Java所有的I/O机制都是基于数据流进行输入输出,这些数据流表示了字符或者字节数据的流动序列
  • Java的I/O流提供了读写数据的标准方法,任何Java中表示数据源的对象都会提供以数据流的方式读写它的数据的方法。
  • IO又分为流IO(java.io)和块IO(java.nio)
  • Java.io是大多数面向数据流的输入/输出类的主要软件包。此外,Java也对块传输提供支持,在核心库 java.nio中采用的便是块IO。
  • 流IO的好处是简单易用,缺点是效率较低。块IO效率很高,但编程比较复杂

2.流定义

  • 在电脑上的数据有三种存储方式,一种是外存,一种是内存,一种是缓存。比如电脑上的硬盘,磁盘,U盘等都是外存,在电脑上有内存条,缓存是在CPU里面的。外存的存储量最大,其次是内存,最后是缓存,但是外存的数据的读取最慢,其次是内存,缓存最快
  • java中将输入输出抽象称为流,就好像水管,将两个容器连接起来。将数据从外存中读取到内存中的称为输入流,将数据从内存写入外存中的称为输出流。

3.流的分类

1.根据流向分为输入流和输出流

  • 注意输入流和输出流是相对于程序而言的。
  • 输出:把程序(内存)中的内容输出到磁盘、光盘等存储设备中
  • 输入:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中

2.根据传输数据单位分为字节流和字符流

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

    • 按照字 的方式读取数据,一次读取1个字节byte,等同于一次读取8个二进制位
    • 这种流是万能的,什么类型的文件都可以读取。包括:文本文件,图片,声音文件,视频文件 等
  • 字符流:数据流中最小的数据单元是字符, Java中的字符是Unicode编码,一个字符占用两个字节(无论中文还是英文都是两个字节)
    • 按照 字符 的方式读取数据的,一次读取一个字符.
    • 这种流是为了方便读取 普通文本文件 而存在的,这种流不能读取:图片、声音、视频等文件。只能读取 纯文本文件,连word文件都无法读取。
  • 注意:纯文本文件,不单单是.txt文件,还包括 .java、.ini、.py 。总之只要 能用记事本打开 的文件都是普通文本文件。
  • Java IO流中的四大基流。这四大基流都是抽象类,其他流都是继承于这四大基流的
  • 所有的流都实现了:java.io.Closeable接口,都是可关闭的,都有 close() 方法
  • 流是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费(占用)很多资源。养成好习惯,用完流一定要关闭
  • 所有的输出流都实现了:java.io.Flushable接口,都是可刷新的,都有 flush() 方法
  • 养成一个好习惯,输出流在最终输出之后,一定要记得flush()刷新一下。这个刷新表示将通道/管道当中剩余未输出的数据强行输出完(清空管道!)刷新的作用就是清空管道。
  • 如果没有flush()可能会导致丢失数据

3.根据功能分为节点流和包装流

  • 节点流:可以从或向一个特定的地方(节点)读写数据,直接连接数据源
  • 包装流(处理流):并不直接连接数据源,是对一个已存在的流的连接和封装,是一种典型的装饰器设计模式,使用处理流主要是为了更方便的执行输入输出工作
  • 一个流对象经过其他流的多次包装,称为流的链接。
  • 一个IO流可以即是输入流又是字节流又或是以其他方式分类的流类型,是不冲突的

4.一些特别的的流类型

  • 转换流:转换流只有字节流转换为字符流,因为字符流使用起来更方便,我们只会向更方便使用的方向转化
  • 缓冲流:有关键字Buffered,也是一种处理流,为其包装的流增加了缓存功能,提高了输入输出的效率,增加缓冲功能后需要使用flush()才能将缓冲区中内容写入到实际的物理节点。但是,在现在版本的Java中,只需记得关闭输出流(调用close()方法),就会自动执行输出流的flush()方法,可以保证将缓冲区中内容写入。
  • 对象流:有关键字Object,主要用于将目标对象保存到磁盘中或允许在网络中直接传输对象时使用(对象序列化)

4.操作 IO 流的模板

  • 1.创建源或目标对象

    • 输入:把文件中的数据流向到程序中,此时文件是 源,程序是目标
    • 输出:把程序中的数据流向到文件中,此时文件是目标,程序是源
  • 2.创建 IO 流对象
    • 输入:创建输入流对象
    • 输出:创建输出流对象
  • 3.具体的 IO 操作
  • 4.关闭资源
    • 输入:输入流的 close() 方法
    • 输出:输出流的 close() 方法
  • 注意:程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源。如果不关闭该资源,那么磁盘的文件将一直被程序引用着,不能删除也不能更改。所以应该手动调用 close() 方法关闭流资源

5.Java IO 流的整体架构图

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

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

    java.lang.System
    public final class System  extends Object{   static  PrintStream  err;//标准错误流(输出)  static  InputStream  in;//标准输入(键盘输入流)  static  PrintStream  out;//标准输出流(显示器输出流)
    }
    
  • 注意
    • System类不能创建对象,只能直接使用它的三个静态成员。(构造函数为private)
    • 每当main方法被执行时,就自动生成上述三个对象。

7.File类

  • File类提供了描述文件和目录的操作与管理方法
  • File 类:文件和目录路径名的抽象表示
  • File 类只能操作文件的属性,文件的内容是不能操作的

1.File的分隔符

  • 各个平台之间的路径分隔符是不一样的
  • 为了屏蔽各个平台之间的分隔符差异,我们在构造 File 类的时候可以使用Java 为我们提供的字段
  • File.pathSeparator指的是分隔连续多个路径字符串的分隔符(例:java -cp test.jar; abc.jar HelloWorld中指“;”)
  • File.separator指的是用来分隔同一个路径字符串中的目录(例:C:\Program Files\Common Files中指“\”)
System.out.println(File.pathSeparator);//输出 ;
System.out.println(File.pathSeparatorChar);//输出 ;
System.out.println(File.separator);//输出 \
System.out.println(File.separatorChar);//输出 \

2.File类的构造方法

  • 定义文件路径时,可以用“/”或者“\”
  • 在创建一个文件时,如果目录下有同名文件将被覆盖。
//不使用 Java 提供的分隔符字段,注意:这样写只能在 Windows 平台有效
File f1 = new File("D:\\IO\\a.txt");//或者是D:/IO/a.txt
//使用 Java 提供的分隔符
File f2 = new File("D:"+File.separator+"IO"+File.separator+"a.txt");
System.out.println(f1);//输出 D:\IO\a.txt
System.out.println(f2);//输出 D:\IO\a.txt//File(File parent, String child)
//从父抽象路径名和子路径名字符串创建新的 File实例。
File f3 = new File("D:");
File f4 = new File(f3,"IO");
System.out.println(f4); //D:\IO//File(String pathname)
//通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。
File f5 = new File("D:"+File.separator+"IO"+File.separator+"a.txt");
System.out.println(f5); //D:\IO\a.txt//File(String parent, String child)
//从父路径名字符串和子路径名字符串创建新的 File实例。
File f6 = new File("D:","IO\\a.txt");
System.out.println(f6); //D:\IO\a.txt

3.File 类的常用方法

  • 创建方法

    • 1.boolean createNewFile() 不存在返回true 存在返回false
    • 2.boolean mkdir() 创建目录,如果上一级目录不存在,则会创建失败
    • 3.boolean mkdirs() 创建多级目录,如果上一级目录不存在也会自动创建
  • 删除方法
    • 1.boolean delete() 删除文件或目录,如果表示目录,则目录下必须为空才能删除
    • 2.boolean deleteOnExit() 文件使用完成后删除
  • 判断方法
    • 1.boolean canExecute()判断文件是否可执行
    • 2.boolean canRead()判断文件是否可读
    • 3.boolean canWrite() 判断文件是否可写
    • 4.boolean exists() 判断文件或目录是否存在
    • 5.boolean isDirectory() 判断此路径是否为一个目录
    • 6.boolean isFile()  判断是否为一个文件
    • 7.boolean isHidden()  判断是否为隐藏文件
    • 8.boolean isAbsolute()判断是否是绝对路径 文件不存在也能判断
  • 获取方法
    • 1.String getName() 获取此路径表示的文件或目录名称
    • 2.String getPath() 将此路径名转换为路径名字符串
    • 3.String getAbsolutePath() 返回此抽象路径名的绝对形式
    • 4.String getParent()//如果没有父目录返回null
    • 5.long lastModified()//获取最后一次修改的时间
    • 6.long length() 返回由此抽象路径名表示的文件的长度。
    • 7.boolean renameTo(File f) 重命名由此抽象路径名表示的文件。
    • 8.File[] liseRoots()//获取机器盘符
    • 9.String[] list() 返回一个字符串数组,命名由此抽象路径名表示的目录中的文件和目录。
    • 10.String[] list(FilenameFilter filter) 返回一个字符串数组,命名由此抽象路径名表示的目录中满足指定过滤器的文件和目录。
//File(File parent, String child)//从父抽象路径名和子路径名字符串创建新的 File实例。File dir = new File("D:"+File.separator+"IO");File file = new File(dir,"a.txt");//判断dir 是否存在且表示一个目录if(!(dir.exists()||dir.isDirectory())){//如果 dir 不存在,则创建这个目录dir.mkdirs();//根据目录和文件名,创建 a.txt文件file.createNewFile();}//返回由此抽象路径名表示的文件或目录的名称。 这只是路径名称序列中的最后一个名字。 如果路径名的名称序列为空,则返回空字符串。System.out.println(file.getName()); //a.txt//返回此抽象路径名的父null的路径名字符串,如果此路径名未命名为父目录,则返回null。System.out.println(file.getParent());//D:\IO//将此抽象路径名转换为路径名字符串。 结果字符串使用default name-separator character以名称顺序分隔名称。System.out.println(file.getPath()); //D:\IO\a.txt

4.File 类的技巧

  • 打印给定目录下的所有文件夹和文件夹里面的内容
public static void getFileList(File file){//第一级子目录File[] files = file.listFiles();for(File f:files){//打印目录和文件System.out.println(f);if(f.isDirectory()){getFileList(f);}}}
public static void main(String[] args) throws Exception {File f = new File("D:"+File.separator+"WebStormFile");getFileList(f);
}

8.字节流InputStream/OutputStream

  • 典型实现(FileInputSteam、FileOutStream)

1.字节输入流:InputStream

  • 这个抽象类是表示输入字节流的所有类的超类
  • 数据单位为字节(8bit)
//1、创建目标对象,输入流表示那个文件的数据保存到程序中。不写盘符,默认该文件是在该项目的根目录下//a.txt 保存的文件内容为:AAaBCDEFFile target = new File("io"+File.separator+"a.txt");//2、创建输入流对象InputStream in = new FileInputStream(target);//3、具体的 IO 操作(读取 a.txt 文件中的数据到程序中)/*** 注意:读取文件中的数据,读到最后没有数据时,返回-1*  int read():读取一个字节,返回读取的字节*  int read(byte[] b):读取多个字节,并保存到数组 b 中,从数组 b 的索引为 0 的位置开始存储,返回读取了几个字节*  int read(byte[] b,int off,int len):读取多个字节,并存储到数组 b 中,从数组b 的索引为 0 的位置开始,长度为len个字节*///int read():读取一个字节,返回读取的字节int data1 = in.read();//获取 a.txt 文件中的数据的第一个字节System.out.println((char)data1); //A//int read(byte[] b):读取多个字节保存到数组b 中byte[] buffer  = new byte[10];in.read(buffer);//获取 a.txt 文件中的前10 个字节,并存储到 buffer 数组中System.out.println(Arrays.toString(buffer)); //[65, 97, 66, 67, 68, 69, 70, 0, 0, 0]System.out.println(new String(buffer)); //AaBCDEF[][][]//int read(byte[] b,int off,int len):读取多个字节,并存储到数组 b 中,从索引 off 开始到 lenin.read(buffer, 0, 3);System.out.println(Arrays.toString(buffer)); //[65, 97, 66, 0, 0, 0, 0, 0, 0, 0]System.out.println(new String(buffer)); //AaB[][][][][][][]//4、关闭流资源in.close();

2.字节输出流:OutputStream

  • 这个抽象类是表示字节输出流的所有类的超类
//1、创建目标对象,输出流表示把数据保存到哪个文件。不写盘符,默认该文件是在该项目的根目录下File target = new File("io"+File.separator+"a.txt");//2、创建文件的字节输出流对象,第二个参数是 Boolean 类型,true 表示后面写入的文件追加到数据后面,false 表示覆盖OutputStream out = new FileOutputStream(target,true);//3、具体的 IO 操作(将数据写入到文件 a.txt 中)/*** void write(int b):把一个字节写入到文件中* void write(byte[] b):把数组b 中的所有字节写入到文件中* void write(byte[] b,int off,int len):把数组b 中的从 off 索引开始的 len 个字节写入到文件中*/out.write(65); //将 A 写入到文件中out.write("Aa".getBytes()); //将 Aa 写入到文件中out.write("ABCDEFG".getBytes(), 1, 5); //将 BCDEF 写入到文件中//经过上面的操作,a.txt 文件中数据为 AAaBCDEF//4、关闭流资源out.close();System.out.println(target.getAbsolutePath());

3.用字节流完成文件的复制

  • 注意缓冲区buffer的大小可能会造成读取一半从而生成乱码
/*** 将 a.txt 文件 复制到 b.txt 中*///1、创建源和目标File srcFile = new File("io"+File.separator+"a.txt");File descFile = new File("io"+File.separator+"b.txt");//2、创建输入输出流对象InputStream in = new FileInputStream(srcFile);OutputStream out = new FileOutputStream(descFile);//3、读取和写入操作byte[] buffer = new byte[10];//创建一个容量为 10 的字节数组,存储已经读取的数据int len = -1;//表示已经读取了多少个字节,如果是 -1,表示已经读取到文件的末尾while((len=in.read(buffer))!=-1){//打印读取的数据System.out.println(new String(buffer,0,len));//将 buffer 数组中从 0 开始,长度为 len 的数据读取到 b.txt 文件中out.write(buffer, 0, len);}//4、关闭流资源out.close();in.close();

9.字符流Reader/Writer

  • 典型实现(FileReader、FileWriter)
  • 为什么要使用字符流?
    • 因为使用字节流操作汉字或特殊符号语言的时候容易乱码,因为汉字不止一个字节,为了解决这个问题,建议使用字符流。
  • 什么情况下使用字符流?
    • 一般可以用记事本打开的文件,我们可以看到内容不乱码的。就是文本文件,可以使用字符流。而操作二进制文件(比如图片、音频、视频)必须使用字节流

1.字符输入流:Reader

  • 用于读取字符流的抽象类
//1、创建源
File srcFile = new File("io"+File.separator+"a.txt");
//2、创建字符输出流对象
Reader in = new FileReader(srcFile);
//3、具体的 IO 操作/**** int read():每次读取一个字符,读到最后返回 -1* int read(char[] buffer):将字符读进字符数组,返回结果为读取的字符数* int read(char[] buffer,int off,int len):将读取的字符存储进字符数组 buffer,返回结果为读取的字符数,从索引 off 开始,长度为 len**/
//int read():每次读取一个字符,读到最后返回 -1
int len = -1;//定义当前读取字符的数量
while((len = in.read())!=-1){//打印 a.txt 文件中所有内容System.out.print((char)len);
}//int read(char[] buffer):将字符读进字符数组
char[] buffer = new char[10]; //每次读取 10 个字符
while((len=in.read(buffer))!=-1){System.out.println(new String(buffer,0,len));
}//int read(char[] buffer,int off,int len)
while((len=in.read(buffer,0,10))!=-1){System.out.println(new String(buffer,0,len));
}
//4、关闭流资源
in.close();

2.字符输出流:Writer

//1、创建源
File srcFile = new File("io"+File.separator+"a.txt");
//2、创建字符输出流对象
Writer out = new FileWriter(srcFile);
//3、具体的 IO 操作/**** void write(int c):向外写出一个字符* void write(char[] buffer):向外写出多个字符 buffer* void write(char[] buffer,int off,int len):把 buffer 数组中从索引 off 开始到 len个长度的数据写出去* void write(String str):向外写出一个字符串*/
//void write(int c):向外写出一个字符
out.write(65);//将 A 写入 a.txt 文件中
//void write(char[] buffer):向外写出多个字符 buffer
out.write("Aa帅锅".toCharArray());//将 Aa帅锅 写入 a.txt 文件中
//void write(char[] buffer,int off,int len)
out.write("Aa帅锅".toCharArray(),0,2);//将 Aa 写入a.txt文件中
//void write(String str):向外写出一个字符串
out.write("Aa帅锅");//将 Aa帅锅 写入 a.txt 文件中//4、关闭流资源
/**** 注意如果这里有一个 缓冲的概念,如果写入文件的数据没有达到缓冲的数组长度,那么数据是不会写入到文件中的* 解决办法:手动刷新缓冲区 flush()* 或者直接调用 close() 方法,这个方法会默认刷新缓冲区*/
out.flush();
out.close();
  • FileWriter写数据之换行和追加写
  • 1.数据的换行
    • \n可以实现换行,但是windows系统自带的记事本打开并没有换行,因为windows识别的换行不是\n,而是\r\n
    • 例:fw.write(“\r\n”);
    • windows:\r\n
    • Linux:\n
    • Mac:\r
  • 2.数据的追加写入
    • 构造方法: FileWriter(String fileName,boolean append)
    • 例:FileWriter fw = new FileWriter(“a.txt”,true); //表示追加写入,默认是false

3.用字符流完成文件的复制

/*** 将 a.txt 文件 复制到 b.txt 中*/
//1、创建源和目标
File srcFile = new File("io"+File.separator+"a.txt");
File descFile = new File("io"+File.separator+"b.txt");
//2、创建字符输入输出流对象
Reader in = new FileReader(srcFile);
Writer out = new FileWriter(descFile);
//3、读取和写入操作
char[] buffer = new char[10];//创建一个容量为 10 的字符数组,存储已经读取的数据
int len = -1;//表示已经读取了多少个字节,如果是 -1,表示已经读取到文件的末尾
while((len=in.read(buffer))!=-1){out.write(buffer, 0, len);
}//4、关闭流资源
out.close();
in.close();

10.包装流(包含缓冲流,转换流对象流等等)

  • 包装流隐藏了底层节点流的差异,并对外提供了更方便的输入\输出功能,让我们只关心这个高级流的操作
  • 使用包装流包装了节点流,程序直接操作包装流,而底层还是节点流和IO设备操作
  • 关闭包装流的时候,只需要关闭包装流即可

1.缓冲流

  • 是一个包装流,目的是缓存作用,加快读取和写入数据的速度。
  • 字节缓冲流:BufferedInputStream、BufferedOutputStream
  • 字符缓冲流:BufferedReader、BufferedWriter
  • 在将字符输入输出流、字节输入输出流的时候,读取操作,通常都会定义一个字节或字符数组,将读取/写入的数据先存放到这个数组里面,然后在取数组里面的数据。这比我们一个一个的读取/写入数据要快很多,而这也就是缓冲流的由来。只不过缓冲流里面定义了一个 数组用来存储我们读取/写入的数据,当内部定义的数组满了(注意:我们操作的时候外部还是会定义一个小的数组,小数组放入到内部数组中),就会进行下一步操作。
  • 缓冲流的 JDK 底层源码,可以看到,程序中定义了这样的 缓存数组,大小为 8192
  • 设置缓冲区的大小不要随便设置,要么就设置成8192的整数倍,要么就用默认值
  • Windows和Linux当前都使用4KB的内存页面大小。因此,BufferedReader上的默认缓冲区将恰好占用2页

//没有用缓冲流的操作
//1、创建目标对象,输入流表示那个文件的数据保存到程序中。不写盘符,默认该文件是在该项目的根目录下//a.txt 保存的文件内容为:AAaBCDEFFile target = new File("io"+File.separator+"a.txt");//2、创建输入流对象InputStream in = new FileInputStream(target);//3、具体的 IO 操作(读取 a.txt 文件中的数据到程序中)/*** 注意:读取文件中的数据,读到最后没有数据时,返回-1*  int read():读取一个字节,返回读取的字节*  int read(byte[] b):读取多个字节,并保存到数组 b 中,从数组 b 的索引为 0 的位置开始存储,返回读取了几个字节*  int read(byte[] b,int off,int len):读取多个字节,并存储到数组 b 中,从数组b 的索引为 0 的位置开始,长度为len个字节*///int read():读取一个字节,返回读取的字节int data1 = in.read();//获取 a.txt 文件中的数据的第一个字节System.out.println((char)data1); //A//int read(byte[] b):读取多个字节保存到数组b 中byte[] buffer  = new byte[10];//这里我们定义了一个 长度为 10 的字节数组,用来存储读取的数据in.read(buffer);//获取 a.txt 文件中的前10 个字节,并存储到 buffer 数组中System.out.println(Arrays.toString(buffer)); //[65, 97, 66, 67, 68, 69, 70, 0, 0, 0]System.out.println(new String(buffer)); //AaBCDEF[][][]//int read(byte[] b,int off,int len):读取多个字节,并存储到数组 b 中,从索引 off 开始到 lenin.read(buffer, 0, 3);System.out.println(Arrays.toString(buffer)); //[65, 97, 66, 0, 0, 0, 0, 0, 0, 0]System.out.println(new String(buffer)); //AaB[][][][][][][]//4、关闭流资源in.close();
//字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("io"+File.separator+"a.txt"));
//定义一个字节数组,用来存储数据
byte[] buffer = new byte[1024];
int len = -1;//定义一个整数,表示读取的字节数
while((len=bis.read(buffer))!=-1){System.out.println(new String(buffer,0,len));
}
//关闭流资源
bis.close();<br><br>//字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("io"+File.separator+"a.txt"));
bos.write("ABCD".getBytes());
bos.close();//字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("io"+File.separator+"a.txt"));
char[] buffer = new char[10];
int len = -1;
while((len=br.read(buffer))!=-1){System.out.println(new String(buffer,0,len));
}
br.close();//字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("io"+File.separator+"a.txt"));
bw.write("ABCD");
bw.close();

2.转换流

  • 把字节流转换为字符流
  • InputStreamReader:把字节输入流转换为字符输入流
  • OutputStreamWriter:把字节输出流转换为字符输出流

1.用转换流进行文件的复制

/*** 将 a.txt 文件 复制到 b.txt 中*///1、创建源和目标File srcFile = new File("io"+File.separator+"a.txt");File descFile = new File("io"+File.separator+"b.txt");//2、创建字节输入输出流对象InputStream in = new FileInputStream(srcFile);OutputStream out = new FileOutputStream(descFile);//3、创建转换输入输出对象Reader rd = new InputStreamReader(in);Writer wt = new OutputStreamWriter(out);//3、读取和写入操作char[] buffer = new char[10];//创建一个容量为 10 的字符数组,存储已经读取的数据int len = -1;//表示已经读取了多少个字符,如果是 -1,表示已经读取到文件的末尾while((len=rd.read(buffer))!=-1){wt.write(buffer, 0, len);}//4、关闭流资源rd.close();wt.close();

2.转换流和子类区别

  • FileReader继承自InputStreamReader
  • FileWriter继承自OutputStreamWriter
  • OutputStreamWriter和InputStreamReader是字符和字节的桥梁:也可以称之为字符转换流。
  • 字符转换流原理:字节流+编码表
  • FileWriter和FileReader:作为子类,仅作为操作字符文件的便捷类存在。当操作的字符文件,使用的是默认编码表时可以不用父类,而直接用子类就完成操作了,简化了代码
    InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt"));//默认字符集。
    InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt"),"GBK");//指定GBK字符集。
    FileReader fr = new FileReader("a.txt");
    
  • 这三句代码的功能是一样的,其中第三句最为便捷
  • 注意:一旦要指定其他编码时,绝对不能用子类,必须使用字符转换流
  • 什么时候用子类:
    • 1.操作的是文件。
    • 2.使用默认编码。

3.内存流(数组流)

  • 把数据先临时存在数组中,也就是内存中。所以关闭 内存流是无效的,关闭后还是可以调用这个类的方法。底层源码的 close()是一个空方法
  • 1.字节内存流:ByteArrayOutputStream 、ByteArrayInputStream
  • 2.字符内存流:CharArrayReader、CharArrayWriter
  • 3.字符串流:StringReader,StringWriter(把数据临时存储到字符串中)

1.字节内存流:ByteArrayOutputStream ,ByteArrayInputStream

//字节数组输出流:程序---》内存
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//将数据写入到内存中
bos.write("ABCD".getBytes());
//创建一个新分配的字节数组。 其大小是此输出流的当前大小,缓冲区的有效内容已被复制到其中。
byte[] temp = bos.toByteArray();
System.out.println(new String(temp,0,temp.length));byte[] buffer = new byte[10];
///字节数组输入流:内存---》程序
ByteArrayInputStream bis = new ByteArrayInputStream(temp);
int len = -1;
while((len=bis.read(buffer))!=-1){System.out.println(new String(buffer,0,len));
}//这里不写也没事,因为源码中的 close()是一个空的方法体
bos.close();
bis.close();

2.字符内存流:CharArrayReader,CharArrayWriter

//字符数组输出流
CharArrayWriter caw = new CharArrayWriter();
caw.write("ABCD");
//返回内存数据的副本
char[] temp = caw.toCharArray();
System.out.println(new String(temp));//字符数组输入流
CharArrayReader car = new CharArrayReader(temp);
char[] buffer = new char[10];
int len = -1;
while((len=car.read(buffer))!=-1){System.out.println(new String(buffer,0,len));
}

3.字符串流:StringReader,StringWriter(把数据临时存储到字符串中)

//字符串输出流,底层采用 StringBuffer 进行拼接
StringWriter sw = new StringWriter();
sw.write("ABCD");
sw.write("帅锅");
System.out.println(sw.toString());//ABCD帅锅//字符串输入流
StringReader sr = new StringReader(sw.toString());
char[] buffer = new char[10];
int len = -1;
while((len=sr.read(buffer))!=-1){System.out.println(new String(buffer,0,len));//ABCD帅锅
}

4.合并流

  • 把多个输入流合并为一个流,也叫顺序流,因为在读取的时候是先读第一个,读完了在读下面一个流
//定义字节输入合并流
SequenceInputStream seinput = new SequenceInputStream(new FileInputStream("io/a.txt"), new FileInputStream("io/b.txt"));
byte[] buffer = new byte[10];
int len = -1;
while((len=seinput.read(buffer))!=-1){System.out.println(new String(buffer,0,len));
}seinput.close();

5.对象流

  • ObjectOutputStream,通过 writeObject()方法做序列化操作
  • ObjectInputStream,通过 readObject() 方法做反序列化操作

1.使用对象流来完成序列化和反序列化

  • 我们打开 a.txt 文件,发现里面的内容乱码,注意这不需要我们来看懂,这是二进制文件,计算机能读懂就行了。
  • 反序列化的对象必须要提供该对象的字节码文件.class
// 第一步:创建一个 JavaBean 对象
public class Person implements Serializable{private String name;private int age;public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "Person [name=" + name + ", age=" + age + "]";}public Person(String name, int age) {super();this.name = name;this.age = age;}
}  
//第二步:使用 ObjectOutputStream 对象实现序列化
OutputStream op = new FileOutputStream("io"+File.separator+"a.txt");
ObjectOutputStream ops = new ObjectOutputStream(op);
ops.writeObject(new Person("vae",1));ops.close();
//第三步:使用ObjectInputStream 对象实现反序列化
InputStream in = new FileInputStream("io"+File.separator+"a.txt");
ObjectInputStream os = new ObjectInputStream(in);
byte[] buffer = new byte[10];
int len = -1;
Person p = (Person) os.readObject();
System.out.println(p);  //Person [name=vae, age=1]
os.close();

6.数据流

  • java.io.DataInputStream,数据字节输入流
  • java.io.DataOutputStream,数据字节输出流(这个流可以将 数据连同数据的类型 一并写入文件。)
  • DataOutputStream写的文件,只能使用DataInputStream去读。并且读的时候你需要提前知道写入的顺序
// 创建数据专属的字节输出流
DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));
// 写数据
byte b = 100;
short s = 200;
int i = 300;
long l = 400L;
float f = 3.0F;
double d = 3.14;
boolean sex = false;
char c = 'a';
// 写
dos.writeByte(b); // 把数据以及数据的类型一并写入到文件当中。
dos.writeShort(s);
dos.writeInt(i);
dos.writeLong(l);
dos.writeFloat(f);
dos.writeDouble(d);
dos.writeBoolean(sex);
dos.writeChar(c);// 刷新
dos.flush();
// 关闭最外层
dos.close();//读
DataInputStream dis = new DataInputStream(new FileInputStream("data"));
// 开始读
byte b = dis.readByte();
short s = dis.readShort();
int i = dis.readInt();
long l = dis.readLong();
float f = dis.readFloat();
double d = dis.readDouble();
boolean sex = dis.readBoolean();
char c = dis.readChar();System.out.println(b);
System.out.println(s);
System.out.println(i + 1000);
System.out.println(l);
System.out.println(f);
System.out.println(d);
System.out.println(sex);
System.out.println(c);dis.close();

7.标准输出流

  • java.io.PrintWriter,
  • java.io.PrintStream,标准的字节输出流。默认输出到控制台
    • 标准输出流不需要手动close()关闭。
    • 可以改变标准输出流的输出方向 System.setOut(PrintStream对象)
// 可以改变标准输出流的输出方向吗? 可以// 标准输出流不再指向控制台,指向“log”文件。
PrintStream printStream = new PrintStream(new FileOutputStream("log"));
// 修改输出方向,将输出方向修改到"log"文件。
System.setOut(printStream);// 修改输出方向
// 再输出
System.out.println("hello world");
System.out.println("hello kitty");
System.out.println("hello zhangsan");

高并发IO底层原理

1.读写原理

  • 用户程序进行IO的读写,依赖于底层的IO读写,基本上会用到底层的read和write两大系统调用
  • 在不同的操作系统中,IO读写的系统调用的名称可能不完全一样,但是基本功能是一样的
  • read系统调用,并不是直接从物理设备把数据读取到内存中;write系统调用,也不是直接把数据写入到物理设备
  • 上层应用无论是调用操作系统的read,还是调用操作系统的write,都会涉及缓冲区
  • 具体来说,调用操作系统的read,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区。
  • 上层程序的IO操作,实际上不是物理设备级别的读写,而是缓存的复制。
  • read和write两大系统调用,都不负责数据在内核缓冲区和物理设备(如磁盘)之间的交换
  • 这项底层的读写交换,是由操作系统内核(Kernel)来完成的
  • 在用户程序中,无论是Socket的IO、还是文件IO操作,都属于上层应用的开发,它们的输入(Input)和输出(Output)的处理,在编程的流程上,都是一致的

2.内核缓冲区与进程缓冲区

  • 缓冲区的目的,是为了减少频繁地与设备之间的物理交换
  • 外部设备的直接读写,涉及操作系统的中断。发生系统中断时,需要保存之前的进程数据和状态等信息,而结束中断之后,还需要恢复之前的进程数据和状态等信息。为了减少这种底层系统的时间损耗、性能损耗,于是出现了内存缓冲区。
  • 有了内存缓冲区,上层应用使用read系统调用时,仅仅把数据从内核缓冲区复制到上层应用的缓冲区(进程缓冲区);上层应用使用write系统调用时,仅仅把数据从进程缓冲区复制到内核缓冲区中。底层操作会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候,再进行IO设备的中断处理,集中执行物理设备的实际IO操作,这种机制提升了系统的性能。至于什么时候中断(读中断、写中断),由操作系统的内核来决定,用户程序则不需要关心
  • 从数量上来说,在Linux系统中,操作系统内核只有一个内核缓冲区。而每个用户程序(进程),有自己独立的缓冲区,叫作进程缓冲区。所以,用户程序的IO读写程序,在大多数情况下,并没有进行实际的IO操作,而是在进程缓冲区和内核缓冲区之间直接进行数据的交换。

3.详解典型的系统调用流程

  • 这里以read系统调用为例,先看下一个完整输入流程的两个阶段

    • 1.等待数据准备好
    • 2.从内核向进程复制数据
  • 如果是read一个socket(套接字),那么以上两个阶段的具体处理流程如下
    • 1.第一个阶段,等待数据从网络中到达网卡。当所等待的分组到达时,它被复制到内核中的某个缓冲区。这个工作由操作系统自动完成,用户程序无感知
    • 2.第二个阶段,就是把数据从内核缓冲区复制到应用进程缓冲区
  • 再具体一点,如果是在Java服务器端,完成一次socket请求和响应,完整的流程如下
    • 1.客户端请求:Linux通过网卡读取客户端的请求数据,将数据读取到内核缓冲区。
    • 2.获取请求数据:Java服务器通过read系统调用,从Linux内核缓冲区读取数据,再送入Java进程缓冲区
    • 3.服务器端业务处理:Java服务器在自己的用户空间中处理客户端的请求
    • 4.服务器端返回数据:Java服务器完成处理后,构建好的响应数据,将这些数据从用户缓冲区写入内核缓冲区。这里用到的是write系统调用
    • 5.发送给客户端:Linux内核通过网络IO,将内核缓冲区中的数据写入网卡,网卡通过底层的通信协议,会将数据发送给目标客户端

4.四种主要的IO模型

1.同步阻塞IO(Blocking IO,BIO)

  • 阻塞IO指的是需要内核IO操作彻底完成后才返回到用户空间执行用户程序的操作指令
  • “阻塞”指的是用户程序(发起IO请求的进程或者线程)的执行状态
  • 传统的IO模型都是阻塞IO模型,并且在Java中默认创建的socket都属于阻塞IO模型
  • 可以将同步与异步看成发起IO请求的两种方式
  • 同步IO是指用户空间(进程或者线程)是主动发起IO请求的一方,系统内核是被动接收方。
  • 异步IO则反过来,系统内核是主动发起IO请求的一方,用户空间是被动接收方。
  • 总结:同步阻塞IO(Blocking IO)指的是用户空间(或者线程)主动发起,需要等待内核IO操作彻底完成后才返回到用户空间的IO操作。在IO操作过程中,发起IO请求的用户进程(或者线程)处于阻塞状态。

详解同步阻塞IO

  • 默认情况下,在Java应用程序进程中所有对socket连接进行的IO操作都是同步阻塞IO
  • 在阻塞式IO模型中,从Java应用程序发起IO系统调用开始,一直到系统调用返回,这段时间内发起IO请求的Java进程(或者线程)是阻塞的。直到返回成功后,应用进程才能开始处理用户空间的缓冲区数据
  • 例:在Java中发起一个socket的read操作的系统调用,流程大致如下
    • 1.从Java进行IO读后发起read系统调用开始,用户线程(或者线程)就进入阻塞状态。
    • 2.当系统内核收到read系统调用后就开始准备数据。一开始,数据可能还没有到达内核缓冲区(例如,还没有收到一个完整的socket数据包),这时内核就要等待。
    • 3.内核一直等到完整的数据到达,就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),然后内核返回结果(例如返回复制到用户缓冲区中的字节数)。
    • 4.直到内核返回后用户线程才会解除阻塞的状态,重新运行起来。阻塞IO的特点是在内核执行IO操作的两个阶段,发起IO请求的用户进程(或者线程)被阻塞了。
  • 阻塞IO的优点是:应用程序开发非常简单;在阻塞等待数据期间,用户线程挂起,基本不会占用CPU资源。
  • 阻塞IO的缺点是:一般情况下会为每个连接配备一个独立的线程,一个线程维护一个连接的IO操作。在并发量小的情况下,这样做没有什么问题。在高并发的应用场景下,阻塞IO模型需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大,性能很低,基本上是不可用的。

2.同步非阻塞IO(Non-Blocking IO,NIO)

  • 非阻塞IO指的是用户空间的程序不需要等待内核IO操作彻底完成,可以立即返回用户空间去执行后续的指令,即发起IO请求的用户进程(或者线程)处于非阻塞状态,与此同时,内核会立即返回给用户一个IO状态值
  • 阻塞和非阻塞的区别:
    • 阻塞是指用户进程(或者线程)一直在等待,而不能做别的事情;非阻塞是指用户进程(或者线程)获得内核返回的状态值就返回自己的空间,可以去做别的事情。
  • 在Java中,非阻塞IO的socket被设置为NONBLOCK模式。
  • 注意:同步非阻塞IO也可以简称为NIO,但是它不是Java编程中的NIO。Java编程中的NIO(New IO)类库组件所归属的不是基础IO模型中的NIO模型,而是IO多路复用模型。
  • 总结:同步非阻塞IO指的是用户进程主动发起,不需要等待内核IO操作彻底完成就能立即返回用户空间的IO操作。在IO操作过程中,发起IO请求的用户进程(或者线程)处于非阻塞状态。

详解同步非阻塞IO

  • 在Linux系统下,socket连接默认是阻塞模式,可以将socket设置成非阻塞模式。
  • 在NIO模型中,应用程序一旦开始IO系统调用,就会出现以下两种情况
    • 1.在内核缓冲区中没有数据的情况下,系统调用会立即返回一个调用失败的信息
    • 2.在内核缓冲区中有数据的情况下,在数据的复制过程中系统调用是阻塞的,直到完成数据从内核缓冲区复制到用户缓冲区。复制完成后,系统调用返回成功,用户进程(或者线程)可以开始处理用户空间的缓冲区数据
  • 例:发起一个非阻塞socket的read操作的系统调用,流程如下
    • 1.在内核数据没有准备好的阶段,用户线程发起IO请求时立即返回。所以,为了读取最终的数据,用户进程(或者线程)需要不断地发起IO系统调用。
    • 2.内核数据到达后,用户进程(或者线程)发起系统调用,用户进程(或者线程)阻塞。内核开始复制数据,它会将数据从内核缓冲区复制到用户缓冲区,然后内核返回结果(例如返回复制到的用户缓冲区的字节数)。
    • 3.用户进程(或者线程)读到数据后,才会解除阻塞状态,重新运行起来。也就是说,用户空间需要经过多次尝试才能保证最终真正读到数据,而后继续执行。
  • 同步非阻塞IO的特点是应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好就继续轮询,直到完成IO系统调用为止
  • 同步非阻塞IO的优点是:每次发起的IO系统调用在内核等待数据过程中可以立即返回,用户线程不会阻塞,实时性较好。
  • 同步非阻塞IO的优点是:每次发起的IO系统调用在内核等待数据过程中可以立即返回,用户线程不会阻塞,实时性较好。
  • 总结:在高并发应用场景中,同步非阻塞IO是性能很低的,也是基本不可用的,一般Web服务器都不使用这种IO模型。在Java的实际开发中,不会涉及这种IO模型,但是此模型还是有价值的,其作用在于其他IO模型中可以使用非阻塞IO模型作为基础,以实现其高性能

3.IO多路复用(IO Multiplexing)

  • 为了提高性能,操作系统引入了一种新的系统调用,专门用于查询IO文件描述符(含socket连接)的就绪状态。在Linux系统中,新的系统调用为select/epoll系统调用。通过该系统调用,一个用户进程(或者线程)可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核就能够将文件描述符的就绪状态返回给用户进程(或者线程),用户空间可以根据文件描述符的就绪状态进行相应的IO系统调用。
  • 为了提高性能,操作系统引入了一种新的系统调用,专门用于查询IO文件描述符(含socket连接)的就绪状态。在Linux系统中,新的系统调用为select/epoll系统调用。通过该系统调用,一个用户进程(或者线程)可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核就能够将文件描述符的就绪状态返回给用户进程(或者线程),用户空间可以根据文件描述符的就绪状态进行相应的IO系统调用。
  • IO多路复用属于一种经典的Reactor模式实现,有时也称为异步阻塞IO,Java中的Selector属于这种模型。

详解IO多路复用

  • 如何避免同步非阻塞IO模型中轮询等待的问题:答案是采用IO多路复用模型
  • 目前支持IO多路复用的系统调用有select、epoll等。几乎所有的操作系统都支持select系统调用,它具有良好的跨平台特性。epoll是在Linux 2.6内核中提出的,是select系统调用的Linux增强版本。
  • 在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程可以不断地轮询成百上千的socket连接的就绪状态,当某个或者某些socket网络连接有IO就绪状态时就返回这些就绪的状态(或者说就绪事件)。
  • 例:发起一个多路复用IO的read操作的系统调用,流程如下
    • 1.选择器注册。首先,将需要read操作的目标文件描述符(socket连接)提前注册到Linux的select/epoll选择器中,在Java中所对应的选择器类是Selector类。然后,开启整个IO多路复用模型的轮询流程
    • 2.就绪状态的轮询。通过选择器的查询方法,查询所有提前注册过的目标文件描述符(socket连接)的IO就绪状态。通过查询的系统调用,内核会返回一个就绪的socket列表。当任何一个注册过的socket中的数据准备好或者就绪了就说明内核缓冲区有数据了,内核将该socket加入就绪的列表中,并且返回就绪事件。
    • 3.用户线程获得了就绪状态的列表后,根据其中的socket连接发起read系统调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区
    • 4.复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行
  • 说明:在用户进程进行IO就绪事件的轮询时,需要调用选择器的select查询方法,发起查询的用户进程或者线程是阻塞的。当然,如果使用了查询方法的非阻塞的重载版本,发起查询的用户进程或者线程也不会阻塞,重载版本会立即返回。
  • IO多路复用模型的特点是:IO多路复用模型的IO涉及两种系统调用,一种是IO操作的系统调用,另一种是select/epoll就绪查询系统调用。IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll。
  • 和NIO模型相似,多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,以找出达到IO操作就绪的socket连接。
  • IO多路复用模型与同步非阻塞IO模型是有密切关系的,具体来说,注册在选择器上的每一个可以查询的socket连接一般都设置成同步非阻塞模型,只是这一点对于用户程序而言是无感知的
  • IO多路复用模型的优点是:一个选择器查询线程可以同时处理成千上万的网络连接,所以用户程序不必创建大量的线程,也不必维护这些线程,从而大大减少了系统的开销。与一个线程维护一个连接的阻塞IO模式相比,这一点是IO多路复用模型的最大优势。
  • 通过JDK的源码可以看出,Java语言的NIO组件在Linux系统上是使用epoll系统调用实现的。所以,Java语言的NIO组件所使用的就是IO多路复用模型。
  • IO多路复用模型的缺点是,本质上select/epoll系统调用是阻塞式的,属于同步IO,需要在读写事件就绪后由系统调用本身负责读写,也就是说这个读写过程是阻塞的。要彻底地解除线程的阻塞,就必须使用异步IO模型

4.异步IO(Asynchronous IO,AIO)

  • 异步IO指的是用户空间的线程变成被动接收者,而内核空间成为主动调用者。在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕并放在了用户缓冲区内,内核在IO完成后通知用户线程直接使用即可。
  • 异步IO类似于Java中典型的回调模式,用户进程(或者线程)向内核空间注册了各种IO事件的回调函数,由内核去主动调用。

详解异步IO

  • 异步IO模型的基本流程是:用户线程通过系统调用向内核注册某个IO操作。内核在整个IO操作(包括数据准备、数据复制)完成后通知用户程序,用户执行后续的业务操作
  • 在异步IO模型中,在整个内核的数据处理过程(包括内核将数据从网络物理设备(网卡)读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区)中,用户程序都不需要阻塞
  • 例:发起一个异步IO的read操作的系统调用,流程如下
    • 1.用户线程发起了read系统调用后,立刻就可以去做其他的事,用户线程不阻塞。
    • 2.内核开始IO的第一个阶段:准备数据。准备好数据,内核就会将数据从内核缓冲区复制到用户缓冲区。
    • 3.内核会给用户线程发送一个信号(Signal),或者回调用户线程注册的回调方法,告诉用户线程read系统调用已经完成,数据已经读入用户缓冲区。
    • 4.用户线程读取用户缓冲区的数据,完成后续的业务操作
  • 异步IO模型的特点是在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。正因为如此,异步IO有的时候也被称为信号驱动IO。
  • 异步IO模型的缺点是应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说需要底层内核提供支持。
  • 理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。就目前而言,Windows系统下通过IOCP实现了真正的异步IO。在Linux系统下,异步IO模型在2.6版本才引入,JDK对它的支持目前并不完善,因此异步IO在性能上没有明显的优势。

5.通过合理配置来支持百万级并发连接

  • Linux操作系统中文件句柄数的限制。在生产环境Linux系统中,基本上都需要解除文件句柄数的限制。原因是Linux系统的默认值为1024,也就是说,一个进程最多可以接受1024个socket连接,这是远远不够的
  • 文件句柄也叫文件描述符。在Linux系统中,文件可分为普通文件、目录文件、链接文件和设备文件。文件描述符(File Descriptor)是内核为了高效管理已被打开的文件所创建的索引,是一个非负整数(通常是小整数),用于指代被打开的文件。所有的IO系统调用(包括socket的读写调用)都是通过文件描述符完成的
  • 在Linux下,通过调用ulimit命令可以看到一个进程能够打开的最大文件句柄数量。这个命令的具体使用方法是:
ulimit -n
  • ulimit命令是用来显示和修改当前用户进程的基础限制命令,-n选项用于引用或设置当前的文件句柄数量的限制值,Linux系统的默认值为1024
  • 理论上,1024个文件描述符对绝大多数应用(例如Apache、桌面应用程序)来说已经足够,对于一些用户基数很大的高并发应用则是远远不够的。一个高并发的应用面临的并发连接数往往是十万级、百万级、千万级,甚至像腾讯QQ一样的上亿级。
  • 文件句柄数不够,会导致什么后果呢?当单个进程打开的文件句柄数量超过了系统配置的上限值时会发出“Socket/File:Can’t open so many files”的错误提示。
  • 所以,对于高并发、高负载的应用,必须调整这个系统参数,以适应并发处理大量连接的应用场景。可以通过ulimit来设置这两个参数,方法如下
 ulimit -n 1000000
  • 在上面的命令中,n的值设置越大,可以打开的文件句柄数量越大。建议以root用户来执行此命令。
  • 使用ulimit命令有一个缺陷,即该命令只能修改当前用户环境的一些基础限制,仅在当前用户环境有效。也就是说,在当前的终端工具连接当前shell期间,修改是有效的,一旦断开用户会话,或者说用户退出Linux,它的数值就又变回系统默认的1024了。并且,系统重启后,句柄数量会恢复为默认值。
  • ulimit命令只能用于临时修改,如果想永久地把最大文件描述符数量值保存下来,可以编辑/etc/rc.local开机启动文件,在文件中添加如下内容
 ulimit -SHn 1000000
  • 以上示例增加了-S和-H两个命令选项。选项-S表示软性极限值,-H表示硬性极限值。硬性极限值是实际的限制,就是最大可以是100万,不能再多了。软性极限值则是系统发出警告(Warning)的极限值,超过这个极限值,内核会发出警告。
  • 普通用户通过ulimit命令可将软性极限值更改到硬性极限值的最大设置值。如果要更改硬性极限值,必须拥有root用户权限。
  • 要彻底解除Linux系统的最大文件打开数量的限制,可以通过编辑Linux的极限配置文件/etc/security/limits.conf来做到。修改此文件,加入如下内容:
soft nofile 1000000
hard nofile 1000000
  • oft nofile表示软性极限,hard nofile表示硬性极限。举个实际例子,在使用和安装目前非常流行的分布式搜索引擎ElasticSearch时,必须修改这个文件,以增加最大的文件描述符的极限值。当然,在生产环境运行Netty时,也需要修改/etc/security/limits.conf文件来增加文件描述符数量的极限值。

序列化和反序列化

1.序列化

1.序列化的定义

  • 指把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。这个过程称为序列化
  • 通俗来说就是将数据结构或对象转换成二进制串的过程
  • 一次序列化多个对象可以将对象放到集合当中,序列化集合。
  • 参与序列化的ArrayList集合以及集合中的元素User都需要实现 java.io.Serializable 接口

2.反序列化

1.反序列化的定义

  • 把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程
  • 通俗来说就是将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程

3.序列化的意义

  • 1.在分布式系统中,此时需要把对象在网络上传输,就得把对象数据转换为二进制形式,需要共享的数据的 JavaBean 对象,都得做序列化。
  • 2.服务器钝化:如果服务器发现某些对象好久没活动了,那么服务器就会把这些内存中的对象持久化在本地磁盘文件中(Java对象转换为二进制文件);如果服务器发现某些对象需要活动时,先去内存中寻找,找不到再去磁盘文件中反序列化我们的对象数据,恢复成 Java 对象。这样能节省服务器内存。

4.序列化的步驟

  • 1.需要做序列化的对象的类,必须实现序列化接口:Java.lang.Serializable 接口(这是一个标志接口,没有任何抽象方法),Java 中大多数类都实现了该接口,比如:String,Integer
  • 2.底层会判断,如果当前对象是 Serializable 的实例,才允许做序列化,Java对象 instanceof Serializable 来判断。
  • 3.在 Java 中使用对象流来完成序列化和反序列化

5.序列化中会出现的问题

  • 1.某些数据不需要做序列化

    • 解决办法:在字段面前加上 transient

      private String name;//需要序列化
      transient private int age;//不需要序列化
      
    • 那么我们在反序列化的时候,打印出来的就是Person [name=vae, age=0],整型数据默认值为
  • 2.序列化版本问题
    • 在完成序列化操作后,由于项目的升级或修改,可能我们会对序列化对象进行修改,比如增加某个字段,那么我们在进行反序列化就会报错:
    • 解决办法:在 JavaBean 对象中增加一个 serialVersionUID 字段,用来固定这个版本,无论我们怎么修改,版本都是一致的,就能进行反序列化了
    • 注意:Java虚拟机看到Serializable接口之后,会自动生成一个序列化版本号,这种自动生成的序列化版本号缺点是:一旦代码确定之后,不能进行后续的修改,因为只要修改,必然会重新编译,此时会生成全新的序列化版本号,这个时候java虚拟机会认为这是一个全新的类,凡是一个类实现了Serializable接口,建议给该类手动提供一个固定不变的序列化版本号。这样,以后这个类即使代码修改了,但是版本号不变,java虚拟机会认为是同一个类
      private static final long serialVersionUID = 8656128222714547171L;
      

NIO

  • 同步与异步,指的是消息处理的方式;
  • 阻塞与非阻塞,指的是等待消息响应时的状态。

1.NIO的简介

  • 在1.4版本之前,Java IO类库是阻塞IO
  • 从1.4版本开始,引进了新的异步IO库,被称为Java New IO类库,简称为Java NIO
  • New IO类库的目标就是要让Java支持非阻塞IO,基于此,更多的人喜欢称Java NIO为非阻塞IO(Non-Blocking IO),称“老的”阻塞式Java IO为OIO(Old IO)。
  • 总体上说,NIO弥补了原来面向流的OIO同步阻塞的不足,为标准Java代码提供了高速、面向缓冲区的IO

2.NIO和OIO的对比

  • 在Java中,NIO和OIO的区别主要体现在三个方面

    • 1.OIO是面向流(Stream Oriented)的,NIO是面向缓冲区(Buffer Oriented)的。

      • 在一般的OIO操作中,面向字节流或字符流的IO操作总是以流式的方式顺序地从一个流(Stream)中读取一个或多个字节,因此,我们不能随意改变读取指针的位置。在NIO操作中则不同,NIO中引入了Channel和Buffer的概念。面向缓冲区的读取和写入只需要从通道读取数据到缓冲区中,或将数据从缓冲区写入通道中。NIO不像OIO那样是顺序操作,它可以随意读取Buffer中任意位置的数据
    • 2.OIO的操作是阻塞的,而NIO的操作是非阻塞的
      • OIO操作都是阻塞的。例:我们调用一个read方法读取一个文件的内容,调用read的线程就会被阻塞,直到read操作完成。在NIO模式中,当我们调用read方法时,如果此时有数据,则read读取数据并返回;如果此时没有数据,则read也会直接返回,而不会阻塞当前线程。
      • NIO的非阻塞是如何做到的呢?即NIO使用了通道和通道的多路复用技术。
    • 3.OIO没有选择器(Selector)的概念,而NIO有选择器的概念。
      • NIO的实现是基于底层选择器的系统调用的,所以NIO需要底层操作系统提供支持;而OIO不需要用到选择器。

3.Java NIO类库包含的三个核心组件

1.Channel(通道)

  • OIO中,同一个网络连接会关联到两个流:一个是输入流(Input Stream),另一个是输出流(Output Stream)。Java应用程序通过这两个流不断地进行输入和输出的操作。
  • NIO中,一个网络连接使用一个通道表示,所有NIO的IO操作都是通过连接通道完成的。一个通道类似于OIO中两个流的结合体,既可以从通道读取数据,也可以向通道写入数据。
  • Java NIO中一个socket连接使用一个Channel来表示。从更广泛的层面来说,一个通道可以表示一个底层的文件描述符,例如硬件设备、文件、网络连接等。然而,远不止如此,Java NIO的通道可以更加细化。例如,不同的网络传输协议类型,在Java中都有不同的NIO Channel实现。
  • Java NIO中一个socket连接使用一个Channel来表示。从更广泛的层面来说,一个通道可以表示一个底层的文件描述符,例如硬件设备、文件、网络连接等。然而,远不止如此,Java NIO的通道可以更加细化。例如,不同的网络传输协议类型,在Java中都有不同的NIO Channel实现。

1.四种通道

  • 1.FileChannel:文件通道,用于文件的数据读写。
  • 2.SocketChannel:套接字通道,用于套接字TCP连接的数据读写。
  • 3.ServerSocketChannel:服务器套接字通道(或服务器监听通道),允许我们监听TCP连接请求,为每个监听到的请求创建一个SocketChannel通道。
  • 4.DatagramChannel:数据报通道,用于UDP的数据读写。
  • 这四种通道涵盖了文件IO、TCP网络、UDP IO三类基础IO读写操作。下面从通道的获取、读取、写入、关闭这四个重要的操作入手,对它们进行简单的介绍。

2.FileChannel

  • FileChannel(文件通道)是专门操作文件的通道
  • 通过FileChannel,既可以从一个文件中读取数据,也可以将数据写入文件中
  • 特别申明一下,FileChannel为阻塞模式,不能设置为非阻塞模式。
  • 下面分别介绍FileChannel的获取、读取、写入、关闭这四个操作。
  • 获取FileChannel
    • 可以通过文件的输入流、输出流获取FileChannel
    • 也可以通过RandomAccessFile(文件随机访问)类来获取FileChannel实例
  • 读取FileChannel
    • 在大部分应用场景中,从通道读取数据都会调用通道的int read(ByteBuffer buf)方法,它把从通道读取的数据写入ByteBuffer缓冲区,并且返回读取的数据量
    • 以上代码中channel.read(buf)读取通道的数据时,对于通道来说是读模式,对于ByteBuffer缓冲区来说是写入数据,这时ByteBuffer缓冲区处于写模式。
  • 写入FileChannel
    • 把数据写入通道,在大部分应用场景中都会调用通道的write(ByteBuffer)方法,此方法的参数是一个ByteBuffer缓冲区实例,是待写数据的来源。write(ByteBuffer)方法的作用是从ByteBuffer缓冲区中读取数据,然后写入通道自身,而返回值是写入成功的字节数。
    • 在以上的outchannel.write(buf)调用中,对于入参buf实例来说,需要从其中读取数据写入outchannel通道中,所以入参buf必须处于读模式,不能处于写模式
  • 关闭通道
    • 当通道使用完成后,必须将其关闭。关闭非常简单,调用close()方法即可。
  • 强制刷新到磁盘
    • 在将缓冲区写入通道时,出于性能的原因,操作系统不可能每次都实时地将写入数据落地(或刷新)到磁盘,完成最终的数据保存。在将缓冲区数据写入通道时,要保证数据能写入磁盘,可以在写入后调用一下FileChannel的force()方法。
package BufferDemo;import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;public class FileChannelDemo1 {private final  static int CAPACITY = 20;public static void main(String[] args) throws IOException {//获取//创建输入源File srcFile = new File("E:" + File.separator + "a.txt");//创建文件输入流FileInputStream fis = new FileInputStream(srcFile);//获取文件流的通道FileChannel fisChannel = fis.getChannel();//创建输出源File destFile = new File("E:" + File.separator + "b.txt");//创建文件输出流FileOutputStream fos = new FileOutputStream(destFile);//获取文件流的通道FileChannel fosChannel = fos.getChannel();//或者通过创建RandomAccessFile(文件随机访问)类来获取FileChannel实例//创建RandomAccessFile随机访问对象String fileName = "E:" + File.separator + "c.txt";RandomAccessFile rw = new RandomAccessFile(fileName, "rw");FileChannel channel = rw.getChannel();System.out.println("-------------------------");//读取//从通道读取数据都会调用通道的int read(ByteBuffer buf)方法,它把从通道读取的数据写入ByteBuffer缓冲区,并且返回读取的数据量RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");//获取通道(可读可写)FileChannel fileChannel = accessFile.getChannel();//获取一个字节缓冲区ByteBuffer buf = ByteBuffer.allocate(CAPACITY);int length = -1;//调用通道的read()方法,读取数据并写入字节类型的缓冲区while ((length = fileChannel.read(buf)) != -1) {}buf.flip();int outLength = 0;while ((outLength = fileChannel.write(buf)) != -1) {System.out.println("写入的字节数" + outLength);}fileChannel.close();fileChannel.force(true);}
}

3.使用FileChannel完成文件复制的实战案例

  • 使用FileChannel复制文件。具体的功能是使用FileChannel将原文件复制一份,把原文件中的数据都复制到目标文件中
package BufferDemo;import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;public class FileNIOCopyDemo {public static void main(String[] args) throws IOException {nioCopyResourceFile();}public static void nioCopyResourceFile() throws IOException {//源String srcPath = "E:" + File.separator + "a.txt";//目标String destPath = "E:" + File.separator + "h.txt";nioCopyFile(srcPath,destPath);}public static void nioCopyFile(String srcPath, String destPath) throws IOException {File srcFile = new File(srcPath);File destFile = new File(destPath);FileInputStream fis = null;FileOutputStream fos = null;FileChannel inChannel = null;FileChannel outChannel = null;long startTime = System.currentTimeMillis();try {if(!destFile.exists()){destFile.createNewFile();}fis = new FileInputStream(srcFile);fos = new FileOutputStream(destFile,true);inChannel = fis.getChannel();outChannel = fos.getChannel();int len = -1;//新建buf,处于写模式ByteBuffer buf = ByteBuffer.allocate(1024);//从输入通道读取到bufwhile ((len = inChannel.read(buf)) != -1) {//buf第一次模式切换:翻转buf,从写模式变成读模式buf.flip();int outLength = 0;//将buf写入输出的通道while ((outLength = outChannel.write(buf)) != 0) {System.out.println("写入的字节数:" + outLength);}//buf第二次模式切换,清除buf,变成写模式buf.clear();}outChannel.force(true);} catch (IOException e) {e.printStackTrace();} finally {outChannel.close();fos.close();inChannel.close();fis.close();}long endTime = System.currentTimeMillis();System.out.println("复制消耗时间:" + (endTime - startTime));}
}
  • 除了FileChannel的通道操作外,还需要注意代码执行过程中隐藏的ByteBuffer的模式切换。新建的ByteBuffer在写模式时才可作为inChannel.read(ByteBuffer)方法的参数,inChannel.read()方法将从通道inChannel读到的数据写入ByteBuffer。然后,调用缓冲区的flip方法,将ByteBuffer从写模式切换成读模式才能作为outchannel.write(ByteBuffer)方法的参数,以便从ByteBuffer读取数据,最终写入outchannel(输出通道)。
  • 完成一次复制之后,在进入下一次复制前还要进行一次缓冲区的模式切换。此时,需要通过clear方法将Buffer切换成写模式才能进入下一次的复制。所以,在示例代码中,每一轮外层的while循环都需要两次ByteBuffer模式切换:第一次模式切换时翻转buf,变成读模式;第二次模式切换时清除buf,变成写模式

2.Buffer(缓冲区)

  • 应用程序与通道的交互主要是进行数据的读取和写入。为了完成NIO的非阻塞读写操作,NIO为大家准备了第三个重要的组件——Buffer。
  • 所谓通道的读取,就是将数据从通道读取到缓冲区中;
  • 所谓通道的写入,就是将数据从缓冲区写入通道中。
  • 缓冲区的使用是面向流进行读写操作的OIO所没有的,也是NIO非阻塞的重要前提和基础之一

1.详解NIO Buffer类及其属性

  • NIO的Buffer本质上是一个内存块,既可以写入数据,也可以从中读取数据
  • Java NIO中代表缓冲区的Buffer类是一个抽象类,位于java.nio包中。
  • NIO的Buffer内部是一个内存块(数组),与普通的内存块(Java数组)不同的是:NIO Buffer对象提供了一组比较有效的方法,用来进行写入和读取的交替访问
  • Buffer类是一个非线程安全类

2.Buffer类

  • Buffer类是一个抽象类,对应于Java的主要数据类型
  • 在NIO中,有8种缓冲区类,分别是ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedByteBuffer
  • 前7种Buffer类型覆盖了能在IO中传输的所有Java基本数据类型,第8种类型是一种专门用于内存映射的ByteBuffer类型
  • 不同的Buffer子类可以操作的数据类型能够通过名称进行判断,比如IntBuffer只能操作Integer类型的对象

3.Buffer类的重要属性

  • Buffer的子类会拥有一块内存,作为数据的读写缓冲区,但是读写缓冲区并没有定义在Buffer基类中,而是定义在具体的子类中
  • 例:ByteBuffer子类就拥有一个byte[]类型的数组成员final byte[] hb,可以作为自己的读写缓冲区,数组的元素类型与Buffer子类的操作类型相对应。
  • 作为读写缓冲区的数组,并没有定义在Buffer类中,而是定义在各具体子类中
  • 为了记录读写的状态和位置,Buffer类额外提供了一些重要的属性,其中有三个重要的成员属性:capacity(容量)、position(读写位置)和limit(读写的限制)

1.capacity属性

  • Buffer类的capacity属性表示内部容量的大小。一旦写入的对象数量超过了capacity,缓冲区就满了,不能再写入了。
  • Buffer类的capacity属性一旦初始化,就不能再改变。Buffer类的对象在初始化时会按照capacity分配内部数组的内存,在数组内存分配好之后,它的大小就不能改变了。
  • 前面讲到,Buffer类是一个抽象类,Java不能直接用来新建对象。在具体使用的时候,必须使用Buffer的某个子类,例如DoubleBuffer子类,该子类能写入的数据类型是double,如果在创建实例时其capacity是100,那么我们最多可以写入100个double类型的数据
  • capacity并不是指内部的内存块byte[]数组的字节数量,而是指能写入的数据对象的最大限制数量。

2.position属性

  • Buffer类的position属性表示当前的位置
  • position属性的值与缓冲区的读写模式有关。在不同的模式下,position属性值的含义是不同的,在缓冲区进行读写的模式改变时,position值会进行相应的调整。
  • 在写模式下,position值的变化规则如下
    • 1.在刚进入写模式时,position值为0(默认值),表示当前的写入位置为从头开始。
    • 2.每当一个数据写到缓冲区之后,position会向后移动到下一个可写的位置。
    • 3.初始的position值为0,最大可写值为limit-1。当position值达到limit时,缓冲区就已经无空间可写了。
  • 在读模式下,position值的变化规则如下:
    • 当缓冲区刚开始进入读模式时,position会被重置为0。
    • 当从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动到下一个可读的位置。
    • 在读模式下,limit表示可读数据的上限。position的最大值为最大可读上限limit,当position达到limit时表明缓冲区已经无数据可读。
  • Buffer的读写模式具体如何切换呢?当新建了一个缓冲区实例时,缓冲区处于写模式,这时是可以写数据的。在数据写入完成后,如果要从缓冲区读取数据,就要进行模式的切换,可以调用flip()方法将缓冲区变成读模式,flip为翻转的意思。
  • 在从写模式到读模式的翻转过程中,position和limit属性值会进行调整,具体的规则是:
    • 1.limit属性被设置成写模式时的position值,表示可以读取的最大数据位置。
    • 2.position由原来的写入位置变成新的可读位置,也就是0,表示可以从头开始读。

3.limit属性

  • Buffer类的limit属性表示可以写入或者读取的数据最大上限,其属性值的具体含义也与缓冲区的读写模式有关。在不同的模式下,limit值的含义是不同的,具体分为以下两种情况:

    • 1.在写模式下,limit属性值的含义为可以写入的数据最大上限。在刚进入写模式时,limit的值会被设置成缓冲区的capacity值,表示可以一直将缓冲区的容量写满。
    • 2.在读模式下,limit值的含义为最多能从缓冲区读取多少数据
  • 一般来说,在进行缓冲区操作时是先写入再读取的。当缓冲区写入完成后,就可以开始从Buffer读取数据,调用flip()方法(翻转),这时limit的值也会进行调整。具体如何调整呢?将写模式下的position值设置成读模式下的limit值,也就是说,将之前写入的最大数量作为可以读取数据的上限值。
  • Buffer在翻转时的属性值调整主要涉及position、limit两个属性,但是这种调整比较微妙,不是太好理解,下面举一个简单的例子:
    • 首先,创建缓冲区。新创建的缓冲区处于写模式,其position值为0,limit值为最大容量capacity。
    • 然后,向缓冲区写数据。每写入一个数据,position向后面移动一个位置,也就是position的值加1。这里假定写入了5个数,当写入完成后,position的值为5。
    • 最后,使用flip方法将缓冲区切换到读模式。limit的值会先被设置成写模式时的position值,所以新的limit值是5,表示可以读取数据的最大上限是5。之后调整position值,新的position会被重置为0,表示可以从0开始读。
    • 缓冲区切换到读模式后就可以从缓冲区读取数据了,一直到缓冲区的数据读取完毕。

4.Mark

  • 除了以上capacity、position、limit三个重要的成员属性之外,Buffer还有一个比较重要的标记属性:mark(标记)属性。该属性的大致作用为:在缓冲区操作过程当中,可以将当前的position值临时存入mark属性中;需要的时候,再从mark中取出暂存的标记值,恢复到position属性中,重新从position位置开始处理
  • 总结

4.Buffer类的重要方法

  • 在使用Buffer实例之前,我们首先需要获取Buffer子类的实例对象,并且分配内存空间
  • 需要获取一个Buffer实例对象时,并不是使用子类的构造器来创建,而是调用子类的allocate()方法。
  • 下面的程序片段演示如何获取一个整型的Buffer实例对象
package BufferDemo;import java.nio.IntBuffer;public class BufferDemo1 {//一个整型的Buffer变量static IntBuffer intBuffer = null;public static void main(String[] args) {intBuffer = IntBuffer.allocate(20);System.out.println("position = " + intBuffer.position());System.out.println("limit = " + intBuffer.limit());System.out.println("capacity = " + intBuffer.capacity());}
}
//分配一个缓冲区
//新缓冲区的位置将为零,其限制将是其容量,其标记将未定义,并且其每个元素将被初始化为零
//它将有一个 {@link array backing array},它的 {@link arrayOffset array offset} 将为零
public static IntBuffer allocate(int capacity) {if (capacity < 0)throw new IllegalArgumentException();return new HeapIntBuffer(capacity, capacity);}
HeapIntBuffer(int cap, int lim) {            // package-privatesuper(-1, 0, lim, cap, new int[cap], 0);/*hb = new int[cap];offset = 0;*/}
// Creates a new buffer with the given mark, position, limit, capacity,
// backing array, and array offset
//使用给定的标记、位置、限制、容量、后备数组和数组偏移量创建一个新缓冲区
IntBuffer(int mark, int pos, int lim, int cap,   // package-privateint[] hb, int offset)
{super(mark, pos, lim, cap);this.hb = hb;this.offset = offset;
}
// Creates a new buffer with the given mark, position, limit, and capacity,
// after checking invariants.
//在检查不变量后,使用给定的标记、位置、限制和容量创建一个新缓冲区
Buffer(int mark, int pos, int lim, int cap) {       // package-privateif (cap < 0)throw new IllegalArgumentException("Negative capacity: " + cap);this.capacity = cap;limit(lim);position(pos);if (mark >= 0) {if (mark > pos)throw new IllegalArgumentException("mark > position: ("+ mark + " > " + pos + ")");this.mark = mark;}
}
//设置此缓冲区的限制。如果容量大于新限制,则将其设置为新限制。如果标记已定义且大于新限制,则将其丢弃
public final Buffer limit(int newLimit) {if ((newLimit > capacity) || (newLimit < 0))throw new IllegalArgumentException();limit = newLimit;if (position > limit) position = limit;if (mark > limit) mark = -1;return this;}
//设置此缓冲区的位置。如果标记已定义且大于新位置,则将其丢弃
public final Buffer position(int newPosition) {if ((newPosition > limit) || (newPosition < 0))throw new IllegalArgumentException();position = newPosition;if (mark > position) mark = -1;return this;}

1.allocate(int capacity)方法

  • 本例中,IntBuffer是具体的Buffer子类,通过调用IntBuffer.allocate(20)创建了一个intBuffer实例对象,并且分配了20×4字节的内存空间
  • 从上面的运行结果可以看出:一个缓冲区在新建后处于写模式,position属性(代表写入位置)的值为0,缓冲区的capacity值是初始化时allocate方法的参数值(这里是20),而limit最大可写上限值也为allocate方法的初始化参数值

2.put()方法

  • 在调用allocate()方法分配内存、返回了实例对象后,缓冲区实例对象处于写模式,可以写入对象,如果要把对象写入缓冲区,就需要调用put()方法
  • 在调用allocate()方法分配内存、返回了实例对象后,缓冲区实例对象处于写模式,可以写入对象,如果要把对象写入缓冲区,就需要调用put()方法
public static void main(String[] args) {intBuffer = IntBuffer.allocate(20);for (int i = 1; i <= 5; i++) {intBuffer.put(i);}System.out.println("position = " + intBuffer.position());System.out.println("limit = " + intBuffer.limit());System.out.println("capacity = " + intBuffer.capacity());}
  • 从结果可以看到,写入了5个元素之后,缓冲区的position属性值变成了5,所以指向了第6个(从0开始的)可以进行写入的元素位置。limit最大可写上限、capacity最大容量两个属性的值都没有发生变化。

3.flip()方法

  • 向缓冲区写入数据之后,是否可以直接从缓冲区读取数据呢?不能!这时缓冲区还处于写模式,如果需要读取数据,要将缓冲区转换成读模式。flip()翻转方法是Buffer类提供的一个模式转变的重要方法,作用是将写模式翻转成读模式。
package BufferDemo;import java.nio.IntBuffer;public class BufferDemo1 {//一个整型的Buffer变量static IntBuffer intBuffer = null;public static void main(String[] args) {intBuffer = IntBuffer.allocate(20);for (int i = 1; i <= 5; i++) {intBuffer.put(i);}System.out.println("position = " + intBuffer.position());System.out.println("limit = " + intBuffer.limit());System.out.println("capacity = " + intBuffer.capacity());intBuffer.flip();System.out.println("position = " + intBuffer.position());System.out.println("limit = " + intBuffer.limit());System.out.println("capacity = " + intBuffer.capacity());for (int i = 0; i < intBuffer.limit(); i++) {System.out.println(intBuffer.get(i));}}
}
  • 调用flip()方法后,新模式下可读上限limit的值变成了之前写模式下的position属性值,也就是5;而新的读模式下的position值简单粗暴地变成了0,表示从头开始读取。
  • 对flip()方法从写入到读取转换的规则,再一次详细介绍如下:
    • 首先,设置可读上限limit的属性值。将写模式下的缓冲区中内容的最后写入位置position值作为读模式下的limit上限值。
    • 其次,把读的起始位置position的值设为0,表示从头开始读。
    • 最后,清除之前的mark标记,因为mark保存的是写模式下的临时位置,发生模式翻转后,如果继续使用旧的mark标记,就会造成位置混乱。
  • 新的问题来了:在读取完成后,如何再一次将缓冲区切换成写模式呢?答案是:可以调用Buffer.clear()清空或者Buffer.compact()压缩方法,它们可以将缓冲区转换为写模式

get()方法

  • 调用flip()方法将缓冲区切换成读模式之后,就可以开始从缓冲区读取数据了。读取数据的方法很简单,可以调用get()方法每次从position的位置读取一个数据,并且进行相应的缓冲区属性的调整。
  • 从程序的输出结果可以看到,读取操作会改变可读位置position的属性值,而可读上限limit值并不会改变。在position值和limit值相等时,表示所有数据读取完成,position指向了一个没有数据的元素位置,已经不能再读了,此时再读就会抛出BufferUnderflowException异常
  • 这里强调一下,在读完之后是否可以立即对缓冲区进行数据写入呢?答案是不能。现在还处于读模式,我们必须调用Buffer.clear()或Buffer.compact()方法,即清空或者压缩缓冲区,将缓冲区切换成写模式,让其重新可写。
  • 此外还有一个问题:缓冲区是不是可以重复读呢?答案是可以的,既可以通过倒带方法rewind()去完成,也可以通过mark()和reset()两个方法组合实现。
  • 注意get()和get(int index)的区别:其中get()后,position的值会发生变化,而get(int index)position的值不会改变
package BufferDemo;import java.nio.IntBuffer;public class BufferDemo1 {//一个整型的Buffer变量static IntBuffer intBuffer = null;public static void main(String[] args) {intBuffer = IntBuffer.allocate(20);for (int i = 1; i <= 5; i++) {intBuffer.put(i);}System.out.println("position = " + intBuffer.position());System.out.println("limit = " + intBuffer.limit());System.out.println("capacity = " + intBuffer.capacity());intBuffer.flip();System.out.println("position = " + intBuffer.position());System.out.println("limit = " + intBuffer.limit());System.out.println("capacity = " + intBuffer.capacity());for (int i = 0; i < intBuffer.limit(); i++) {System.out.println(intBuffer.get());}System.out.println("position = " + intBuffer.position());System.out.println("limit = " + intBuffer.limit());System.out.println("capacity = " + intBuffer.capacity());intBuffer.rewind();System.out.println(intBuffer.get());}
}

rewind()

  • 已经读完的数据,如果需要再读一遍,可以调用rewind()方法。rewind()也叫倒带,就像播放磁带一样倒回去,再重新播放。
  • rewind ()方法主要是调整了缓冲区的position属性与mark属性,具体的调整规则如下:
    • 1.position重置为0,所以可以重读缓冲区中的所有数据。
    • 2.limit保持不变,数据量还是一样的,仍然表示能从缓冲区中读取的元素数量。
    • 3.mark被清理,表示之前的临时位置不能再用了。
  • 通过源代码,我们可以看到rewind()方法与flip()方法很相似,区别在于:倒带方法rewind()不会影响limit属性值;而翻转方法flip()会重设limit属性值。

mark()和reset()

  • mark()和reset()两个方法是配套使用的:Buffer.mark()方法将当前position的值保存起来放在mark属性中,让mark属性记住这个临时位置;然后可以调用Buffer.reset()方法将mark的值恢复到position中。
  • Buffer.mark()和Buffer.reset()两个方法都涉及mark属性的使用。mark()方法与mark属性的名字虽然相同,但是一个是Buffer类的成员方法,一个是Buffer类的成员属性,不能混淆
  • 在前面重复读取的示例代码中,在读到第三个元素(i为2时)时,可以调用mark()方法,把当前位置position的值保存到mark属性中,这时mark属性的值为2。接下来可以调用reset()方法将mark属性的值恢复到position中,这样就可以从位置2(第三个元素)开始重复读取了。

clear()

  • 在读模式下,调用clear()方法将缓冲区切换为写模式。此方法的作用是:

    • 1.将position清零。
    • 2.limit设置为capacity最大容量值,可以一直写入,直到缓冲区写满。
      *在缓冲区处于读模式时,调用clear(),缓冲区会被切换成写模式。调用clear()之后,我们可以看到清空了position(写入的起始位置)的值,其值被设置为0,并且limit值(写入的上限)为最大容量。

5.使用Buffer类的基本步骤

  • 1.使用创建子类实例对象的allocate()方法创建一个Buffer类的实例对象。
  • 2.调用put()方法将数据写入缓冲区中
  • 3.写入完成后,在开始读取数据前调用Buffer.flip()方法,将缓冲区转换为读模式。
  • 4.调用get()方法,可以从缓冲区中读取数据。
  • 5.读取完成后,调用Buffer.clear()方法或Buffer.compact()方法,将缓冲区转换为写模式,可以继续写入

3.Selector(选择器)

  • IO多路复用指的是一个进程/线程可以同时监视多个文件描述符(含socket连接),一旦其中的一个或者多个文件描述符可读或者可写,该监听进程/线程就能够进行IO就绪事件的查询。
  • 在Java应用层面,实现对多个文件描述符的监视需要用到Java NIO组件——选择器。选择器可以理解为一个IO事件的监听与查询器。通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。
  • 从编程实现维度来说,IO多路复用编程的第一步是把通道注册到选择器中,第二步是通过选择器所提供的事件查询(select)方法来查询这些注册的通道是否有已经就绪的IO事件(例如可读、可写、网络连接完成等)。
  • 由于一个选择器只需要一个线程进行监控,因此我们可以很简单地使用一个线程,通过选择器去管理多个连接通道。与OIO相比,NIO使用选择器的最大优势是系统开销小。系统不必为每一个网络连接(文件描述符)创建进程/线程,从而大大减少了系统的开销。总之,一个线程负责多个连接通道的IO处理是非常高效的,这种高效来自Java的选择器组件Selector及其底层的操作系统IO多路复用技术的支持。

1.选择器与注册

  • 简单地说,选择器的使命是完成IO的多路复用,其主要工作是通道的注册、监听、事件查询。一个通道代表一条连接通路,通过选择器可以同时监控多个通道的IO(输入输出)状况。选择器和通道的关系是监控和被监控的关系。
  • 选择器提供了独特的API方法,能够选出(select)所监控的通道已经发生了哪些IO事件,包括读写就绪的IO操作事件。
  • 在NIO编程中,一般是一个单线程处理一个选择器,一个选择器可以监控很多通道。所以,通过选择器,一个单线程可以处理数百、数千、数万甚至更多的通道。在极端情况下(数万个连接),只用一个线程就可以处理所有的通道,这样会大量地减少线程之间上下文切换的开销。
  • 通道和选择器之间的关联通过register(注册)的方式完成。调用通道的Channel.register(Selector sel,int ops)方法,可以将通道实例注册到一个选择器中。register方法有两个参数:第一个参数指定通道注册到的选择器实例;第二个参数指定选择器要监控的IO事件类型。
  • 可供选择器监控的通道IO事件类型包括以下四种:
    • 1.可读:SelectionKey.OP_READ。
    • 2.可写:SelectionKey.OP_WRITE。
    • 3.连接:SelectionKey.OP_CONNECT。
    • 4.接收:SelectionKey.OP_ACCEPT。
  • 以上事件类型常量定义在SelectionKey类中。如果选择器要监控通道的多种事件,可以用“按位或”运算符来实现。例如,同时监控可读和可写IO事件:
    //监控通道的多种事件,用按位或运算符来实现
    int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE
    
  • 什么是IO事件?
  • 这个概念容易混淆,这里特别说明一下。这里的IO事件不是对通道的IO操作,而是通道处于某个IO操作的就绪状态,表示通道具备执行某个IO操作的条件。例如,某个SocketChannel传输通道如果完成了和对端的三次握手过程,就会发生“连接就绪”(OP_CONNECT)事件;某个ServerSocketChannel服务器连接监听通道,在监听到一个新连接到来时,则会发生“接收就绪”(OP_ACCEPT)事件;一个SocketChannel通道有数据可读,就会发生“读就绪”(OP_READ)事件;一个SocketChannel通道等待数据写入,就会发生“写就绪”(OP_WRITE)事件
  • socket连接事件的核心原理和TCP连接的建立过程有关。关于TCP的核心原理和连接建立时的三次握手、四次挥手知识,请参阅本书后面有关TCP原理的内容。

2.SelectableChannel

  • 并不是所有的通道都是可以被选择器监控或选择的。例如,FileChannel就不能被选择器复用。判断一个通道能否被选择器监控或选择有一个前提:判断它是否继承了抽象类SelectableChannel(可选择通道),如果是,就可以被选择,否则不能被选择。
  • 简单地说,一个通道若能被选择,则必须继承SelectableChannel类。
  • SelectableChannel类是何方神圣呢?它提供了实现通道可选择性所需要的公共方法。Java NIO中所有网络连接socket通道都继承了SelectableChannel类,都是可选择的。FileChannel并没有继承SelectableChannel,因此不是可选择通道。

3.SelectionKey

  • 通道和选择器的监控关系注册成功后就可以选择就绪事件,具体的选择工作可调用Selector的select()方法来完成。通过select()方法,选择器可以不断地选择通道中所发生操作的就绪状态,返回注册过的那些感兴趣的IO事件。换句话说,一旦在通道中发生了某些IO事件(就绪状态达成),并且是在选择器中注册过的IO事件,就会被选择器选中,并放入SelectionKey(选择键)的集合中。
  • SelectionKey是什么呢?简单地说,SelectionKey就是那些被选择器选中的IO事件。前面讲到,一个IO事件发生(就绪状态达成)后,如果之前在选择器中注册过,就会被选择器选中,并放入SelectionKey中;如果之前没有注册过,那么即使发生了IO事件,也不会被选择器选中。SelectionKey和IO的关系可以简单地理解为SelectionKey就是被选中了的IO事件。
  • 在实际编程时,SelectionKey的功能是很强大的。通过SelectionKey,不仅可以获得通道的IO事件类型(比如SelectionKey.OP_READ),还可以获得发生IO事件所在的通道。另外,还可以获得选择器实例。

4.选择器使用流程

5.使用NIO实现Discard服务器的实战案例

6.使用SocketChannel在服务端接收文件的实战案例

浅拷贝,深拷贝

1.创建对象的5种方式

  • 1.通过 new 关键字

    • 这是最常用的一种方式,通过 new 关键字调用类的有参或无参构造方法来创建对象。

      Object obj = new Object()
      
  • 2.通过 Class 类的 newInstance() 方法
    • 这种默认是调用类的无参构造方法创建对象。比如 Person p2 = (Person) Class.forName(“com.ys.test.Person”).newInstance();
  • 3.通过 Constructor 类的 newInstance 方法
    • 这和第二种方法类似,都是通过反射来实现。
    • 通过 java.lang.relect.Constructor 类的 newInstance() 方法指定某个构造器来创建对象
    • 实际上第二种方法利用 Class 的 newInstance() 方法创建对象,其内部调用还是Constructor 的 newInstance() 方法。
      Person p3 = (Person) Person.class.getConstructors()[0].newInstance();
      
  • 4.利用 Clone 方法
    • Clone 是 Object 类中的一个方法,通过 对象A.clone() 方法会创建一个内容和对象 A 一模一样的对象 B,clone 克隆,顾名思义就是创建一个一模一样的对象出来。

      Person p4 = (Person) p3.clone();
      
  • 5.反序列化
    • 序列化是把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。而反序列化则是把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。

2.浅拷贝

  • Java 中数据类型可以分为两大类:基本类型和引用类型
  • 基本类型也称为值类型,分别是字符类型 char,布尔类型 boolean以及数值类型 byte、short、int、long、float、double。
  • 引用类型则包括类、接口、数组、枚举等。
  • Java 将内存空间分为堆和栈。基本类型直接在栈中存储数值,而引用类型是将引用放在栈中,实际存储的值是放在堆中,通过栈中的引用指向堆中存放的数据。
  • 原始类 Person。下面我们产生一个 Person 对象,并调用其 clone 方法复制一个新的对象。
  • 注意:调用对象的 clone 方法,必须要让类实现 Cloneable 接口,并且覆写 clone 方法。
  • 对象 Person 的属性 Address,经过 clone 之后,其实只是复制了其引用,他们指向的还是同一块堆内存空间,当修改其中一个对象的属性 Address,另一个也会跟着变化
  • 浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。

3.深拷贝

  • 深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。

如何实现深拷贝

  • 深拷贝的原理我们知道了,就是要让原始对象和克隆之后的对象所具有的引用类型属性不是指向同一块堆内存,这里有三种实现思路。
  • 1.让每个引用类型属性内部都重写clone() 方法
    • 1.既然引用类型不能实现深拷贝,那么我们将每个引用类型都拆分为基本类型,分别进行浅拷贝。比如上面的例子,Person 类有一个引用类型 Address(其实String 也是引用类型,但是String类型有点特殊,后面会详细讲解),我们在 Address 类内部也重写 clone 方法
    • 2.测试还是和上面一样,我们会发现更改了p2对象的Address属性,p1 对象的 Address 属性并没有变化。
    • 3.但是这种做法有个弊端,这里我们Person 类只有一个 Address 引用类型,而 Address 类没有,所以我们只用重写 Address 类的clone 方法,但是如果 Address 类也存在一个引用类型,那么我们也要重写其clone 方法,这样下去,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,那么代码量显然会很大,所以这种方法不太合适
package com.ys.test;public class Address implements Cloneable{private String provices;private String city;public void setAddress(String provices,String city){this.provices = provices;this.city = city;}@Overridepublic String toString() {return "Address [provices=" + provices + ", city=" + city + "]";}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}}
//Person.class 的 clone() 方法
@Overrideprotected Object clone() throws CloneNotSupportedException {Person p = (Person) super.clone();p.address = (Address) address.clone();return p;}
  • 2.利用序列化

    • 序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里写到流中的对象则是原始对象的一个拷贝,因为原始对象还存在 JVM 中,所以我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。
    • 注意每个需要序列化的类都要实现 Serializable 接口,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外
    • 因为序列化产生的是两个完全独立的对象,所有无论嵌套多少个引用类型,序列化都是能实现深拷贝的。
//深度拷贝
public Object deepClone() throws Exception{// 序列化ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos);oos.writeObject(this);// 反序列化ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());ObjectInputStream ois = new ObjectInputStream(bis);return ois.readObject();
}

String,StringBuilder,StringBuffered的区别


String

  • String:字符串常量,字符串长度不可变。Java 中 String 是 immutable(不可变)的。
  • 对于String来说,是把数据存放在了常量池中,因为所有的String,默认都是以常量形式保存,且由final修饰,因此在线程池中它是线程安全的。因为每一个String当被创建好了以后,他就不再发生任何变化,但是它的执行速度是最差的。
    我们要创建String的时候,他在常量池中对这些信息进行处理,如果在程序中出现了大量字符串拼接的工作,效率是非常底下的。
  • 用于存放字符的数组被声明为 final 的,因此只能赋值一次,不可再更改。

StirngBuffer:(效率不如StringBuilder,但远比String要高)

  • 如果要频繁对字符串内容进行修改,出于效率考虑最好使用 StringBuffer,如果想转成 String 类型,可以调用 StringBuffer 的 toString() 方法。
  • 线程安全的可变字符序列。在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。可将字符串缓冲区安全地用于多个线程。
  • 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。
  • append 方法始终将这些字符添加到缓冲区的末端;
  • insert 方法则在指定的点添加字符。
  • 例如,如果 z 引用一个当前内容是 start 的字符串缓冲区对象,则此方法调用 z.append(“le”) 会使字符串缓冲区包含 startle ,而 z.insert(4, “le”) 将更改字符串缓冲区,使之包含 starlet 。
  • StringBuffer相对于StringBuilder效率要相对低一点,但也远比String要高的多。效率低的原因:对于StringBuffer来说更多的考虑到了多线程的情况,在进行字符串操作的时候,它使用了synchronize关键字,对方法进行了同步处理。
    因此StringBuffer适用于多线程环境下的大量操作。

StringBuilder:

  • 在进行多线程处理的时候,如果多个线程对于这一个对象同时产生操作,会产生预期之外的结果。对于StringBuilder来说,执行效率虽然高,但是因为线程不安全,所以不建议在多线程的环境下对同一个StringBuilder对象进行操作。
    因此StringBuilder适用于单线程环境下的大量字符串操作。
  • 字符串变量(非线程安全)。在内部,StringBuilder 对象被当作是一个包含字符序列的变长数组
  • java.lang.StringBuilder 是一个可变的字符序列,是 JDK5.0 新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)
  • String 类型和 StringBuffer 的主要性能区别:String 是不可变的对象, 因此在每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,性能就会降低。
  • 使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。所以多数情况下推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。

IO流/NIO-基础相关推荐

  1. 深入浅出IO流知识——基础流

    茫茫人海千千万万,感谢这一秒你看到这里.希望我的文章对你的有所帮助! 愿你在未来的日子,保持热爱,奔赴山海! I/O基础流 Java对数据的操作是通过流的方式,IO流用来处理设备之间的数据传输,上传文 ...

  2. java byte char io流_Java基础进阶 IO流之字节流

    1.IO流 1.1.概述 之前学习的File类它只能操作文件或文件夹,并不能去操作文件中的数据.真正保存数据的是文件,数据是在文件中.而File类它只是去对文件本身做操作,不能对文件中的数据进行操作. ...

  3. java io nio socket_通过socket编程掌握IO流 —— NIO

    一.本次目标 改造server,采用NIO读取client信息: 改造client,亦采用NIO发送消息,与之前不同的BIO形成对比: 二.编码 1.新建byte数组拼接公共类 主要用作在channe ...

  4. java boolean io流_java基础入门-day22-IO流

    (1)I/O:Input/Output(了解) java中对数据的输入输出问题是通过io技术解决的. java提供的对象都在java.io包下. (2)IO的分类(掌握) A:按照数据的流向 输入流 ...

  5. JAVA系列 IO流 知识结构整理 建立合理的知识框架逻辑 输入输出流 理顺你的思维架构Fileoutput inputStream FileWriter FIleReader结构整理

    最近听见一首歌,个人创作时听见觉得挺好听的 先分享给大家https://music.163.com/#/song?id=41666363 それがあなたの幸せとしても (カバー) 切入正题 今天给大家介 ...

  6. java当中各种流的应用场景_Java中有哪些流,常用流有哪些并有何特点和使用场景 NIO流,特性及使用时注意的事项 IO与NIO区别...

    IO流 流(stream):一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象,对文件中的内容读和写的操作. 及时关闭不使用的流,避免造成资源流失,可能会导致内存溢出 1.数据流向 输入流 ...

  7. java不死神兔总数_Java基础知识强化之IO流笔记13:递归之不死神兔问题(斐波那契数列)...

    1.这个问题是如下的: 有一对兔子,从出生后第3个月起,每个月都生一对兔子,小兔子长到第3个月又生一对兔子,加入兔子都不死,问第20个月兔子的对数? 分析:我们找规律 兔子对数 第1个月:   1 第 ...

  8. Java基础知识——IO流

    简介 IO是指Input/Output,即输入和输出.以内存为中心: Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等 Output指把数据从内存输出到外部,例 ...

  9. 【java基础】IO流是啥?有啥用?(上)

    今天我们说说java代码中对文件的操作,比如新建删除文件,读取文件内容等. File类 File类用于操作文件和目录,可对文件或目录进行新建,删除和重命名等操作.但是如果要访问文件内容本身,就需要用到 ...

  10. Java基础:IO 流中的 flush

    无意中发现了一个巨牛的人工智能教程,忍不住分享一下给大家.教程不仅是零基础,通俗易懂,而且非常风趣幽默,像看小说一样!觉得太牛了,所以分享给大家.点 这里 可以跳转到教程. 内容概要 Java IO ...

最新文章

  1. python论坛哪些好-好的python论坛
  2. leetcode C++ 6. Z 字形变换 将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。
  3. 软件测试的特殊字符包含什么,测试常见的特殊字符集及接口测试中的测试字符...
  4. iOS之深入解析dispatch source的原理与功能
  5. Ecshop:后台添加新功能栏目以及管理权限设置
  6. 减小程序规模!稀疏数组Sparsearray,数据结构二维数组与稀疏数组转换,Java实现
  7. 前端学习(2383):vue编码规范
  8. editor.md 实现拖拽剪切复制粘贴上传图片,文件插件
  9. OSGL 工具库 - 类型转换的艺术
  10. WebStorm中Node.js项目配置教程(1)——创建项目
  11. Webpack 4.X 从入门到精通 - 第三方库(六)
  12. bzoj2705: [SDOI2012]Longge的问题
  13. Linux从入门到精通——Linux中的补充知识点
  14. Spring Framework 常用工具类一
  15. python中define的用法_C语言中#define用法总结
  16. matlab中princ,基于MATLAB_SIMLINK的Turbo交织器的仿真实现
  17. 立波 iphone3gs越狱教程:成功把iphone3gs手机升级成ios6.1.3系统,完美越狱,解决no service和耗电量大的问题
  18. unity xml反序列化为数据类
  19. 哈尔滨理工大学软件与微电子学院第八届程序设计竞赛同步赛(低年级)I 小乐乐切方块
  20. VScode 完整安装、配置及完全卸载

热门文章

  1. 《百年孤独》的读后感范文3483字
  2. video使用canvas截图黑屏
  3. 计算机新手教程装系统,新手小白必看电脑重装系统教程 四种方法至少会一种就够用!...
  4. 网络安全工程师从0单排日记-1
  5. AI金融:利用LSTM预测股票每日最高价
  6. 巴可推出全新4K 3D医疗显示器
  7. 语言可读性可写性_如果您想谈论可访问性,那么我们需要谈论可读性问题。
  8. ADS简单模型参数总结
  9. 赞一下老北京人的指路精准度
  10. 腾讯十年运维专家谈运维的自我修养