day24【JUnit单元测试、NIO】

1.Junit单元测试框架

1.1 概述

​ junit是单元测试,你想测哪个方法就写一个对应的测试方法,然后用junit运行。每个方法之间是独立的,非常灵活。而且测试方法一般不会直接写在原类中,而是单独的测试类,这样测试代码就完全与逻辑代码分开了。

​ Junit是Java语言编写单元测试框架。Junit属于第三方工具,一般情况下需要导入jar包,而多数Java开发环境都集成了Junit。

1.2 使用方式

问题:在之前,我们写了一个方法,我们都需要使用通过main方法来调用。当我们需要测试的方法很多的时候,每次使用main方法来调用时是非常麻烦的。所以我们有了接下来的单元测试,在开发中使用调用方法就显得特别的简单。

创建java项目,并创建“com.itheima.sh.junit”包

在这里插入图片描述

  1. 编写测试类

    说明:如果以前想让一个方法运行必须在main方法中调用该方法。

在这里插入图片描述

2.在测试类JunitDemo01方法上添加注解 @Test(说明:关于什么是注解,后面会详细讲解)

在这里插入图片描述
3.@Test修饰的方法要求:public void 方法名() {…} ,没有参数。

说明:单元测试的方法必须是public修饰,void表示没有返回值,没有参数列表,否则就会不满足单元测试要求,报异常。

在这里插入图片描述

4.添加Idea中集成的Junit库,鼠标放在“@Test”处,使用快捷键“alt+Enter”,点击“Add Junit …”

在这里插入图片描述

5.使用:选中方法右键,执行当前方法;选中类名右键,执行类中所有方法(方法必须标记@Test)

在这里插入图片描述

注意:如果按照上述方式无法导入junit包,说明你的idea中没有集成junit,可以参考课后我给大家提供的帮助文档。如果最后还是无法解决那只能在当前项目根目录下创建lib文件夹,将课后资料的jar包放到lib下就可以使用了

1.3 常用注解

  • Junit4.x版本注解

    @Test  :  测试方法,必须是public修饰的,没有返回值,没有参数
    @Before:用来修饰方法,该方法会在每一个测试方法执行之前执行一次。 
    @After:用来修饰方法,该方法会在每一个测试方法执行之后执行一次。
    @BeforeClass:用来修饰静态方法,该方法会在所有测试方法之前执行一次,而且只执行一次。 
    @AfterClass:用来修饰静态方法,该方法会在所有测试方法之后执行一次,而且只执行一次。
    
  • 代码演示

    public class JunitDemo02 {
    	@Test
    	public void myTest(){
    		System.out.println("测试 test");
    	}
    	@Test
    	public void myTest1(){
    		System.out.println("测试 test1");
    	}
    	@Before
    	public void myBefore(){
    		System.out.println("方法前");
    	}
    	
    	@After
    	public void myAfter(){
    		System.out.println("方法后");
    	}
         @BeforeClass // 用来修饰静态方法,该方法会在所有测试方法之前执行一次。
        public static void myBeforeClass(){
            System.out.println("初始化操作BeforeClass.....");
        }
    
        @AfterClass // 用来修饰静态方法,该方法会在所有测试方法之后执行一次。
        public static void myAfterClass(){
            System.out.println("释放资源myAfterClass.....");
        }
    }
    

注意:被测试的类名不能单独是Test,因为junit单元测试框架底层有叫Test,重名了使用会有问题。被测试的类命名规范:以Test开头,以业务类类名结尾,使用驼峰命名法 。比如业务类类名:ProductDao,那么测试类类名就应该叫:TestProductDao

  • Junit常用注解(Junit5.x版本)【自学,了解,开发中平时都使用junit4,所以有兴趣可以看下,这里不讲解】

在这里插入图片描述

1.4 断言

  • 作用

    断言:预先判断某个条件一定成立,如果条件不成立,则直接报错。

    断言代码:

  //第一个参数args1表示期望值
  //第二个参数args2表示实际值
  //如果args1和args2相同,说明结果正确就测试通过,如果不相同,说明结果是错误的,就会报错
  Assert.assertEquals(args1, args2);
  • 代码示例

    public class Test01 {
        @Test
        public void addTest() {
            //测试
            int sum = add(3, 6);
    
            //断言判断结果
            //第一个参数表示期望值
            //第二个参数表示实际值
            //如果结果正确的就测试通过,如果结果错误的,就会报错
            Assert.assertEquals(9, sum);
            System.out.println("sum = " + sum);
        }
    
        //加法
        //这个代码的语法没问题,也没有异常。他是逻辑错误,系统不知道你要算的是加法
        public int add(int a, int b) {
    //        int sum = a + b;
            //这里是*,不是+,而实际上我们希望是+,所以使用断言技术可以进行测试判断,满足就通过,不满足预期值就报异常
            int sum = a * b;
            return sum;
        }
    }
    

2. NIO

概述

​ Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API(就是之前学习的IO,也称为BIO(Blocking IO))。NIO与原来的IO有同样的作用和目的,都是实现数据的传输。但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。**NIO将以更加高效的方式进行文件的读写操作。同时NIO是一种同步非阻塞的I/O模型。**对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。NIO提供了与传统BIO模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现。

Java NIO 传统IO(BIO)的主要区别

在这里插入图片描述

BIO:面向流

在这里插入图片描述

NIO:面向缓冲区

在这里插入图片描述

小结:

​ 1.对于传统IO属于阻塞式IO.NIO属于非阻塞IO,并且NIO具有Selector选择器,BIO没有选择器都是和我们明天讲解的NIO实现网络编程内容有关,我们到时候在介绍。

​ 2. NIO 包含下面几个核心的组件:

  • Channel(通道):通道表示打开到 IO 设备(例如:文件、套接字)的连接。

  • Buffer(缓冲区):若需要使用 NIO ,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。

  • Selector(选择器):Selector 是非阻塞 IO 的核心(明天讲解)

    3.NIO一般是在高并发情况下使用的,效率特别高。比如流行的软件或流行的游戏中会有高并发和大量连接。

3.Buffer缓冲区

Buffer缓冲区概述

1.一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类。

2.Java NIO 中的 Buffer 主要用于与 NIO 通道(Channel)进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。

3.在NIO中,所有的数据都是用Buffer处理的,它是NIO读写数据的中转池

4.Buffer是一个抽象类,它对某种基本类型的数组进行了封装.可以保存多个相同类型的数据。根据数据类型不同(boolean 除外) ,有以下 Buffer 常用子类:

ByteBuffer  (最重要的一个,我们只学习它)
CharBuffer 
DoubleBuffer 
FloatBuffer
IntBuffer 
LongBuffer 
ShortBuffer

ByteBuffer的创建方式

​ 上述 Buffer 类 他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。我们只需要掌握ByteBuffer缓冲区即可。

​ ByteBuffer内部封装了一个byte[]数组,ByteBuffer里面有一些方法可以对数组进行操作。以下是ByteBuffer的创建方式,一共有三种方式。

1.ByteBuffer的创建方式

  • 在堆中创建缓冲区**(间接缓冲区)**:allocate(int capacity)

    • capacity表示的是数组的容量,也就是数组的大小
    ByteBuffer byteBuffer = ByteBuffer.allocate(10);
    System.out.println(byteBuffer.capacity());
    
  • 在系统内存创建缓冲区**(直接缓冲区)**:allocatDirect(int capacity)

    ByteBuffer byteBuffer1 = ByteBuffer.allocateDirect(10);
    System.out.println(byteBuffer1.capacity());
    
  • 通过数组创建缓冲区**(间接缓冲区)**:static ByteBuffer wrap(byte[] array) 将 byte 数组包装到缓冲区中。

    byte[] buf = new byte[10];
    ByteBuffer byteBuffer2 = ByteBuffer.wrap(buf);
    System.out.println(byteBuffer2.capacity());
    

2.间接缓冲区和直接缓冲区区别

  - 在堆中创建缓冲区称为:间接缓冲区(非直接缓冲区)
  - 在系统内存创建缓冲区称为:直接缓冲区

间接缓冲区(非直接缓冲区):

在这里插入图片描述

说明:

​ 传统的IO流和allocate()方式都是间接缓冲区。应用程序到物理磁盘的过程中都要经历一个os操作系统和jvm内存之间的copy复制过程,性能比较低。

直接缓冲区:

在这里插入图片描述

说明:直接缓冲区是占用物理内存的,在物理内存中创建映射文件,从而取消了内核地址空间和用户地址空间之间的数据copy过程,提高了效率。直接缓冲区读写效率虽然高了,但是不受我们的应用程序控制了,由系统控制。我们将数据交给物理内存以后具体什么时候给物理磁盘,程序无法控制。同时会消耗系统的内存,具体何时释放资源得需要垃圾回收机制释放垃圾,然后系统才会释放资源。一般适合大量的数据长时间存储到内存中的数据.

小结:

  • 间接缓冲区的创建和销毁效率要高于直接缓冲区,但是间接缓冲区的工作效率要低于直接缓冲区

常用方法

  • 方法介绍

    • put(byte b) :给数组添加元素
    • byte[] array() : 返回实现此缓冲区的 byte 数组
    public class Test01 {
        @Test
        public void test1(){
            //创建对象 :在堆中创建缓冲区(间接缓冲区):allocate(int capacity)
            ByteBuffer buffer = ByteBuffer.allocate(10);
            //put() 添加方法
            buffer.put((byte)11);
            buffer.put((byte)22);
            //把ByteBuffer类型转成普通数组类型
            //byte[] array() : 返回实现此缓冲区的 byte 数组
            byte[] bytes = buffer.array();
            //打印
            System.out.println(Arrays.toString(bytes));
            //打印结果:  [11, 22, 0, 0, 0, 0, 0, 0, 0, 0]
        }
    }
    
    • int capacity() :获取容量,容量是一个定值
      • Buffer的容量(capacity)是指:Buffer所能够包含的元素的最大数量。定义了Buffer后,容量是不可变的。因为底层封装的是一个数组。
    public class Demo03 {
           @Test
        public void test1(){
            //创建对象
    //        ByteBuffer buffer = ByteBuffer.allocate(3);//报错
            ByteBuffer buffer = ByteBuffer.allocate(10);
    
            //添加元素
            buffer.put((byte)11);
    
            byte[] bytes = {22,33,44};
            buffer.put(bytes);
    
            //打印
            System.out.println(Arrays.toString(buffer.array()));
            //[11, 22, 33, 44, 0, 0, 0, 0, 0, 0]
    
            //buffer的容量
            int capacity = buffer.capacity();
            System.out.println("容量" + capacity);  //10
        }
    }
    
    • int limit() :返回此缓冲区的限制。

    • Buffer limit(int newLimit) :设置此缓冲区的限制.参数:newLimit表示指定的限制的索引,从limit开始后面的位置不能操作(包括newLimit索引对应的位置)

      • 缓冲区的限制不能为负,并且不能大于其容量
      public class Demo04{ 
          public static void main(String[] args) {
              //创建对象
              ByteBuffer buffer = ByteBuffer.allocate(10);
              //添加元素
              buffer.put((byte)11);//索引0
      
              //获取limit
              //默认情况下所有的位置都可以被使用
              int limit = buffer.limit();
              System.out.println("现在限制位置" + limit);//10
      
              //3索引开始后面的位置不能被使用(包括索引3的位置)
              buffer.limit(3);
              int limit2 = buffer.limit();
              System.out.println("现在限制位置" + limit2);//3
      
              //添加元素
            buffer.put((byte)22);//索引1
            buffer.put((byte)33);//索引2
            buffer.put((byte)44);//索引3   因为限制了3索引,所以在存放44的时候出现了异常:BufferOverflowException
          }
      }
      

在这里插入图片描述

  • position() :位置代表将要存放的元素的索引。位置不能小于0,也不能大于limit.

    • 不加参数表示获取当前position,加参数表示设置position
    public class Demo05 {
        public static void main(String[] args) {
            //创建对象
            ByteBuffer buffer = ByteBuffer.allocate(10);
            //添加元素
            buffer.put((byte)11);
            buffer.put((byte)22);
            buffer.put((byte)33);
    
            //获取位置
            //int position = buffer.position();
            //System.out.println("当前的位置" + position);  //3
    
            //设置位置
          buffer.position(6);
            //添加元素
          buffer.put((byte)44);
    
            //打印
            System.out.println(Arrays.toString(buffer.array()));
            //[11, 22, 33, 0, 0, 0, 44, 0, 0, 0]
        }
    }
    

在这里插入图片描述

  • 标记 (mark)与重置(reset) : 标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position标记。

  • public class Demo06{
        public static void main(String[] args) {
             //创建对象
            ByteBuffer buffer = ByteBuffer.allocate(10);
            //添加元素
            buffer.put((byte)11);
            buffer.put((byte)22);
            buffer.put((byte)33);
    
            //标记当前索引
            buffer.mark();
    
            buffer.put((byte)44);
            buffer.put((byte)55);
    
            //打印数组
            System.out.println(Arrays.toString(buffer.array()));
            //[11, 22, 33, 44, 55, 0, 0, 0, 0, 0]
    
          //重置
            buffer.reset();
    
            //添加元素
            buffer.put((byte)66);
    
          //打印数组
            System.out.println(Arrays.toString(buffer.array()));
            //[11, 22, 33, 66, 55, 0, 0, 0, 0, 0]
    
            //重置 还是会重置到原来mark位置
            buffer.reset();
    
            //添加元素
            buffer.put((byte)77);
    
            //打印数组
            System.out.println(Arrays.toString(buffer.array()));
            //[11, 22, 33, 77, 55, 0, 0, 0, 0, 0]
    	}
    }
    

在这里插入图片描述

说明:

​	1.如果不在调用mark()方法,那么mark一直标记的是最之前的position位置,如果在调用reset()方法还是会回到最开始的position位置。

​	2.0 <= mark <= position <= limit <= capacity
  • Buffer clear():还原缓冲区的状态。 清空缓冲区并返回对缓冲区的引用

    • 将position设置为:0
    • 将限制limit设置为容量capacity
    • 丢弃标记mark
    public class Demo07{
      public static void main(String[] args) {
            //创建对象
            ByteBuffer buffer = ByteBuffer.allocate(10);
    
            //添加元素
            buffer.put((byte)11);
            buffer.put((byte)22);
            buffer.put((byte)33);
    
            //限制
            buffer.limit(6);
    
            //容量是10 位置是3 限制是6
            System.out.println("容量是" + buffer.capacity() + " 位置是" + buffer.position() + " 限制是" + buffer.limit());
            
    
            //还原
            buffer.clear();
    
            //容量是10 位置是0 限制是10
            System.out.println("容量是" + buffer.capacity() + " 位置是" + buffer.position() + " 限制是" + buffer.limit());
        }
    }
    
  • flip():缩小limit的范围。 切换。在读写数据之间要调用这个方法。

    • 将limit设置为当前position位置
    • 将当前position位置设置为0
    • 丢弃标记
    public class Demo08{
        public static void main(String[] args) {
             //创建对象
            ByteBuffer buffer = ByteBuffer.allocate(10);
            //添加元素
            buffer.put((byte)11);
            buffer.put((byte)22);
            buffer.put((byte)33);
            //限制
            buffer.limit(6);
    
            //容量是10 位置是3 限制是6
            System.out.println("容量是" + buffer.capacity() + " 位置是" + buffer.position() + " 限制是" + buffer.limit());
    
            //切换读模式
            buffer.flip();
    
            //容量是10 位置是0 限制是3
            System.out.println("容量是" + buffer.capacity() + " 位置是" + buffer.position() + " 限制是" + buffer.limit());
        }
    }
    

说明:

1.如果要操作数组中的数据,必须先使用flip方法切换成读模式。

2.循环再次操作数据的时候,必须先使用clear清空数组。

在这里插入图片描述

**获取Buffer中的数据: **

abstract  byte get() 读取单个字节。读取此缓冲区当前位置的字节,然后该位置position递增。
ByteBuffer get(byte[] dst)批量读取多个字节到 dst 中,position移动到最后
abstract  byte get(int index)读取指定索引位置的字节(不会移动 position)

代码演示:

public void test7(){
        ByteBuffer buf = ByteBuffer.allocate(10);
        buf.put("abcde".getBytes());
        //切换读取数据模式
        buf.flip();
        //容量是10 位置是0 限制是5
        System.out.println("容量是" + buf.capacity() + " 位置是" + buf.position() + " 限制是" + buf.limit());
        //4. 利用 get() 读取缓冲区中的数据
        //4.1 abstract  byte get() 读取单个字节。读取此缓冲区当前位置的字节,然后该位置递增。
//        byte b = buf.get();
//        System.out.println((char)b);//'a'
//        System.out.println((char)buf.get());//'b'
//        //容量是10 位置是2 限制是5
//        System.out.println("容量是" + buf.capacity() + " 位置是" + buf.position() + " 限制是" + buf.limit());
        //4.2 ByteBuffer get(byte[] dst)批量读取多个字节到 dst 中
//        byte[] dst = new byte[buf.limit()];
//        System.out.println(dst.length);//5
//        buf.get(dst);
//        System.out.println(new String(dst, 0, dst.length));//abcde
//        //容量是10 位置是5 限制是5
//        System.out.println("容量是" + buf.capacity() + " 位置是" + buf.position() + " 限制是" + buf.limit());
        //4.3abstract  byte get(int index)读取指定索引位置的字节(不会移动 position)
//        System.out.println((char)buf.get(0));//a
//        System.out.println((char)buf.get(1));//b
//        //容量是10 位置是0 限制是5
//        System.out.println("容量是" + buf.capacity() + " 位置是" + buf.position() + " 限制是" + buf.limit());
        for (int i = 0; i < buf.limit(); i++) {
            System.out.println((char)buf.get(i));
        }
    }

4.Channel通道

4.1概述

​ 1.由 java.nio.channels 包定义的。Channel(通道)表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与Buffer 进行交互。

​ 2.Channel通道,可以去做读取和写入的操作。相当于我们之前学过的IO流。Channel是双向的,一个对象既可以调用读取的方法也可以调用写出的方法。

​ 3.Channel在读取和写出的时候,要使用ByteBuffer作为缓冲数组。

4.2分类

  • FileChannel:从本地文件读写数据的
  • DatagramChannel:读写UDP网络协议数据
  • SocketChannel:读写TCP网络协议数据,客户端
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel,服务端

4.3获取通道

获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:

FileInputStream : 获取的是FileChannel通道

FileOutputStream:获取的是FileChannel通道

 RandomAccessFile:获取的是FileChannel通道

说明:

RandomAccessFile表示随机访问文件类,用来操作文件的。
    构造方法:
    	RandomAccessFile(String name, String mode) 
			参数:
    			name - 操作文件的路径
    			mode -表示操作文件的模式。
    				取值:
    					“r”:以只读的方式打开
    					“rw”:以读、写方式打开,支持文件的读取或写入。若文件不存在,则创建

 DatagramSocket:获取的是DatagramChannel,关于UDP的通道

Socket:获取的是SocketChannel客户端的通道

ServerSocket:获取的是ServerSocketChannel服务器端的通道

4.4 FileChannel基本使用

  • java.nio.channels.FileChannel (抽象类):用于读、写文件的通道。

  • FileChannel是抽象类,我们可以通过FileInputStream、FileOutputStream和RandomAccessFile的getChannel()方法方便的获取一个它的子类对象。

需求:使用FileChannel将D:\test\故事.txt复制到项目根目录下为故事.txt
说明:源文件故事.txt文件大小是134字节

/*
        演示FileChannel完成文件复制
        需求:使用FileChannel将D:\test\故事.txt复制到项目根目录下为故事.txt
            说明:源文件故事.txt文件大小是134字节
     */
    @Test
    public void test8() throws Exception{
        //创建字节输入流
        FileInputStream fis = new FileInputStream("D:\\test\\故事.txt");
        //创建字节输出流
        FileOutputStream fos = new FileOutputStream("故事.txt");

        //获取Channel
        FileChannel c1 = fis.getChannel();
        FileChannel c2 = fos.getChannel();
        //创建缓冲数组
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //从通道c1中读取数据到buffer中
        while ((c1.read(buffer)) != -1) {
            c2.write(buffer);
        }

        //关流
        c1.close();
        c2.close();
        fos.close();
        fis.close();
    }

说明:按照上述代码操作我们发现在项目根目录下生成的故事.txt文件中都是空,并且文件大小是890字节.显然代码是有问题的。

解释如下图所示:

在这里插入图片描述

说明:当前position的位置是在索引为134的位置,那么如果直接使用write()方法写数据,直接会将134索引后面的数据写到指定文件中,所以就会出现上面的问题。

解决办法:需要在写之前调用flip()方法切换为读模式,position拿到0索引位置,limit拿到之前position位置。然后在写,每次写完之后调用clear()方法进行还原以达到所有指针都恢复到之前的位置方便下次读写。

在这里插入图片描述
修改后的代码:

public void test8() throws Exception{
        //创建字节输入流
        FileInputStream fis = new FileInputStream("D:\\test\\故事.txt");
        //创建字节输出流
        FileOutputStream fos = new FileOutputStream("故事.txt");

        //获取Channel
        FileChannel c1 = fis.getChannel();
        FileChannel c2 = fos.getChannel();
        //创建缓冲数组
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //从通道c1中读取数据到buffer中
        while ((c1.read(buffer)) != -1) {
            //切换读模式
            buffer.flip();
            c2.write(buffer);
            //还原
            buffer.clear();
        }

        //关流
        c1.close();
        c2.close();
        fos.close();
        fis.close();
    }

小结:我们将通过上述示例让大家体会NIO的操作过程。执行三个基本的操作:

​ 1)创建一个Buffer;2)然后从源文件读取数据到缓冲区 3)然后再将缓冲区写入目标文件。

4.5 FileChannel结合MappedByteBuffer实现高效读写

​ 上例直接使用FileChannel结合ByteBuffer实现的管道读写,但并不能提高文件的读写效率。ByteBuffer有个子类:MappedByteBuffer,它可以创建一个“直接缓冲区”,并可以将文件直接映射至内存,可以提高大文件的读写效率。

原理:

在这里插入图片描述

钢铁侠和黑寡妇两个人在不同的地方,使用全息投影技术将黑寡妇投影到钢铁侠家,比在不同的地方交流效率高。

在这里插入图片描述

说明:如果将数据从C盘复制到D盘,那么在硬盘中复制效率不如将数据放到系统内存(直接缓冲区)中效率高。

  • ByteBuffer(抽象类)

    ​ |–MappedByteBuffer(抽象类)

  • 可以调用FileChannel的map()方法获取一个MappedByteBuffer,map()方法的原型:

    MappedByteBuffer map(FileChannel.MapMode mode, long position, long size);

    	参数:
    		mode:FileChannel.MapMode.READ_ONLY 只读映射模式。			                 			      FileChannel.MapMode.READ_WRITE 读取/写入映射模式 
    		position:文件中的位置,映射区域从此位置开始
    		size:要映射的区域大小
    

    ​ 说明:将节点中从position开始的size个字节映射到返回的MappedByteBuffer中。把硬盘中的读写变成内存中的读写。

  • FileChannel类的方法abstract long size() 返回此通道的文件的当前大小。

  • 2G以下文件复制

  • 需求:将D:\test\故事.txt赋值到项目根目录

      	@Test
        public void test9() throws Exception{
            //RandomAccessFile
            //r是read的意思,也就是说能对这个文件做读的操作
            RandomAccessFile f1 = new RandomAccessFile("D:\\test\\故事.txt","r");
    
            //rw是read/write的意思,也就是说能做读写的操作
            RandomAccessFile f2 = new RandomAccessFile("故事.txt","rw");
    
            //获取Channel通道
            FileChannel c1 = f1.getChannel();
            FileChannel c2 = f2.getChannel();
    
            //获取文件的大小
            long size = c1.size();
            //System.out.println("size = " + size);//size = 134
            //映射直接把硬盘中的数据映射到内存中
            //MapMode mode   操作方式
            //long position  起始位置
            // long size     大小
            MappedByteBuffer buffer1 = c1.map(FileChannel.MapMode.READ_ONLY, 0, size);
            MappedByteBuffer buffer2 = c2.map(FileChannel.MapMode.READ_WRITE, 0, size);
    
            //循环完成字节的复制
            for (long i = 0; i < size; i++) {
                //获取一个字节
                byte b = buffer1.get();
                //保存到第二个数组中 由于是在直接缓冲区中,至于数组中的数据如何被写到物理磁盘上,那么不是我们能够决定的了
                //而是系统决定的
                buffer2.put(b);
            }
    
            c2.close();
            c1.close();
            f2.close();
            f1.close();
        }
        }
    }
    
  • 2G以上文件复制(分块操作)

  需求:复制D:\\上课视频.zip文件到F:\\上课视频.zip

如果继续使用上述代码进行复制超过2G以上的文件就会报错。

代码如下:

  @Test
      public void test10() throws Exception{
          //RandomAccessFile
          //r是read的意思,也就是说能对这个文件做读的操作
          RandomAccessFile f1 = new RandomAccessFile("D:\\上课视频.zip","r");
  
          //rw是read/write的意思,也就是说能做读写的操作
          RandomAccessFile f2 = new RandomAccessFile("F:\\上课视频.zip","rw");
  
          //获取Channel通道
          FileChannel c1 = f1.getChannel();
          FileChannel c2 = f2.getChannel();
  
          //获取文件的大小
          long size = c1.size();  
          //System.out.println("size = " + size);//size = 134
          //映射直接把硬盘中的数据映射到内存中
          //MapMode mode   操作方式
          //long position  起始位置
          // long size     大小
          MappedByteBuffer buffer1 = c1.map(FileChannel.MapMode.READ_ONLY, 0, size);
          MappedByteBuffer buffer2 = c2.map(FileChannel.MapMode.READ_WRITE, 0, size);
  
          //循环完成字节的复制
          for (long i = 0; i < size; i++) {
              //获取一个字节
              byte b = buffer1.get();
              //保存到第二个数组中 由于是在直接缓冲区中,至于数组中的数据如何被写到物理磁盘上,那么不是我们能够决定的了
              //而是系统决定的
              buffer2.put(b);
          }
  
          c2.close();
          c1.close();
          f2.close();
          f1.close();
      }

报异常:

在这里插入图片描述

因为我们这里是将硬盘文件放到内存中,使用MappedByteBuffer类缓冲区,对于FileChannel类的映射方法map会对文件大小进行判断,如果文件大小size超过int的最大值(大概是2G),那么就会报异常。不能复制大于2G的文件,如果要想复制,我们可以采用将文件分块的方式进行复制,如下图所示:

说明:完成分块复制需要使用到如下几个变量:

  size              文件大小2600M
  everySize         期望每次复制的大小  500M
  count             块数   size%everySize == 0 ? size/everySize:size/everySize+1
  start             每次复制的起始位置  everySize*i
      				for(int i=0;i<5;i++){}
  trueSize          实际每次复制的大小   size-start > everySize ? everySize : size - start
  • 图解

在这里插入图片描述

  • 代码

    public class Demo {
       @Test
        public void test11() throws Exception {
            //RandomAccessFile
            //r是read的意思,也就是说能对这个文件做读的操作
            RandomAccessFile f1 = new RandomAccessFile("D:\\上课视频.zip", "r");
    
            //rw是readwrite的意思,也就是说能做读写的操作
            RandomAccessFile f2 = new RandomAccessFile("F:\\上课视频.zip", "rw");
    
            //获取Channel通道
            FileChannel c1 = f1.getChannel();
            FileChannel c2 = f2.getChannel();
    
            //获取文件的大小
            long size = c1.size();
            System.out.println("文件的大小 " + size);
    
            //每次期望复制500M
            //1)1024字节表示1KB  2) 1024*1024表示1MB   3)1024*1024*500 表示500MB
            int everySize = 1024 * 1024 * 500;
    
            //一共需要复制几次
            long count = (size % everySize == 0 ? size / everySize : size / everySize + 1);
    
            //分块复制 :如果size是2600M,每次复制大小是500M  那么count=2600/500 + 1;
            //使用循环控制复制的次数
            for (long i = 0; i < count; i++) {
                //每次的开始位置
                long start = everySize * i;
                //每次实际复制大小
                long trueSize = (size - start > everySize ? everySize : size - start);
                //MapMode mode   操作方式
                //long position  起始位置  start 0 500 1000 1500 ....
                //long size     大小   trueSize 前面几乎都一样,最后一个有可能值会有变化
                MappedByteBuffer b1 = c1.map(FileChannel.MapMode.READ_ONLY, start, trueSize);
                MappedByteBuffer b2 = c2.map(FileChannel.MapMode.READ_WRITE, start, trueSize);
    
                //一个字节一个字节获取和添加 把b1数值中的内容都复制到b2数组中
                for (long l = 0; l < trueSize; l++) {
                    byte b = b1.get();
                    b2.put(b);
                }
            }
    
            c2.close();
            c1.close();
            f2.close();
            f1.close();
        }
    }
    

4.6 网络编程收发信息

  • 客户端

    ​ 1.Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道,代替之前的Socket

    ​ 2.创建客户端对象:

    ​ 创建客户端对象不再使用构造方法,而是使用SocketChannel中的静态方法,有两种方式可以连接服务器端:

  1)使用静态方法static SocketChannel open(SocketAddress add) 打开套接字通道并将其连接到远程地址。 
     			 参数: add - 与新通道连接的远程地址,属于SocketAddress类型
                       --| SocketAddress 属于抽象类
                           --| 子类构造方法:InetSocketAddress(String hostname, int port)                              根据主机名和端口号创建套接字地址。
      							参数:
                                      hostname:服务器ip地址
                                      port:服务器端口号
    代码举例:  SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
  2)使用静态方法:
      	static SocketChannel open() 打开套接字通道。 但是没有指定服务器端的ip地址和端口号
      	调用SocketChannel的实例方法connect(SocketAddress add)连接服务器:
   代码举例:
          //创建对象
          SocketChannel sc = SocketChannel.open();
          //连接服务器
          sc.connect(new InetSocketAddress("127.0.0.1",9898));

​ 3.向服务器发送数据必须借助于Buffer缓冲区存储数据,然后使用如下方法向通道写数据

   int write(ByteBuffer src) 将字节序列从给定的缓冲区中写入此通道。 

客户端代码如下:

  package com.itheima.sh.demo_17;
  
  import java.net.InetSocketAddress;
  import java.nio.ByteBuffer;
  import java.nio.channels.SocketChannel;
  
  public class ClientDemo {
      public static void main(String[] args) throws Exception{
          //创建对象
          //Socket s = new Socket("127.0.0.1",8888);
  
          //创建对象
  //        SocketChannel sc = SocketChannel.open();
  //        //连接服务器
  //        sc.connect(new InetSocketAddress("127.0.0.1",8888));
          SocketChannel sc = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
          //客户端发数据
          //创建数组
          ByteBuffer buffer = ByteBuffer.allocate(1024);
          //数组中添加数据
          buffer.put("你好啊~".getBytes());
          //切换读模式
          buffer.flip();
          //发出数据
          sc.write(buffer);
          //关流
          sc.close();
      }
  }
  

如果不调用flip方法,服务器端会接收到一大堆空的数据:

在这里插入图片描述

  • 服务器端

    ​ 1.Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道,就像标准IO中

    的ServerSocket一样.即ServerSocketChannel代替之前的ServerSocket,来完成网络编程的收发数据。

    ​ 2.创建服务器端对象

    创建服务器端对象不再使用构造方法,而是使用ServerSocketChannel中的静态方法:

  1)使用静态方法static ServerSocketChannel open() 打开服务器套接字通道,通道的套接字最初未绑定;
               必须通过其套接字的bind方法将其绑定到特定地址,才能接受连接。
  2)ServerSocketChannel bind(SocketAddress local) 将通道的套接字绑定到本地地址,并配置套接字以	              监听连接。  
               注:java.net.SocketAddress(抽象类):代表一个Socket地址。
               我们可以使用它的子类:java.net.InetSocketAddress()
    		     构造方法:InetSocketAddress(int port):指定本机监听端口。
    代码举例:
          //创建
          ServerSocketChannel ssc = ServerSocketChannel.open();
          //服务器绑定端口
          ssc.bind(new InetSocketAddress(8888));     

​ 3.获取客户端连接的通道

  abstract SocketChannel accept() 接受与此通道套接字的连接。返回值是一个客户端的 SocketChannel   

服务器端代码演示:

  package com.itheima.sh.demo_17;
  
  import java.net.InetSocketAddress;
  import java.nio.ByteBuffer;
  import java.nio.channels.ServerSocketChannel;
  import java.nio.channels.SocketChannel;
  
  public class ServerDemo {
      public static void main(String[] args) throws Exception{
          //创建对象
          //ServerSocket ss = new ServerSocket(8888);
  
          //创建服务器对象
          ServerSocketChannel ssc = ServerSocketChannel.open();
          //服务器绑定端口
          ssc.bind(new InetSocketAddress(8888));
  
          //连接上客户端
          System.out.println(1);
          //accept()此时阻塞了,一直在侦听客户端
          SocketChannel sc = ssc.accept();
          System.out.println(2);
          //服务器端接受数据
          //创建数组
          ByteBuffer buffer = ByteBuffer.allocate(1024);
          //接收数据
          int len = sc.read(buffer);
          //打印结构
          System.out.println(new String(buffer.array(),0,len));
  
          //关闭资源
          sc.close();
      }
  }
  

4.7 accept阻塞问题

​ 之前的accept是阻塞的方法,如果连接不到客户端就一直等着。在NIO中可以设置为非阻塞,设置非阻塞之后,就不会在accept()方法上一直停留。

​ 设置方式:

使用ServerSocketChannel调用:
    //参数block的值是 true表示阻塞,false表示非阻塞.非阻塞的意思是不管有没有客户端他都会往下执行不会停留
    //如果没有客户端,那么ssc.accept()获取的是null
	SelectableChannel configureBlocking(boolean block);
  • 服务器端
public class ServerDemo {
    public static void main(String[] args) throws Exception{
        //创建对象
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //绑定端口
        ssc.bind(new InetSocketAddress(8888));

        //设置非阻塞连接
        /*
            写成false叫非阻塞,非阻塞的意思是不管有没有客户端他都会往下执行不会停留
            如果没有客户端,那么ssc.accept()获取的是null
         */
        ssc.configureBlocking(false);
//        System.out.println(1);
//        SocketChannel sc = ssc.accept();
//        System.out.println(2);
//        System.out.println("sc = " + sc);//sc = null

        while(true) {
            //获取客户端连接
            SocketChannel sc = ssc.accept();
            System.out.println("sc = " + sc);

            if(sc != null){
                //不等于null说明连接上了客户端
                System.out.println("连接上了。。");
                //读取数据操作
                break;
            }else{
                //没连接上客户端
                System.out.println("打会儿游戏~");
                Thread.sleep(2000);
            }
        }
    }
}
  • 客户端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class ClientDemo{
    public static void main(String[] args) throws IOException {
      //创建客户端套接字对象连接服务器端
      SocketChannel sc = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
    }
}

上述非阻塞应用场景举例:

举例:取快递。快递员相约12:00在大门见面
阻塞:我提前到大门口等待,对于我什么都做不了就是阻塞。
非阻塞:快递员什么时候到,给我打电话我再去,期间我可以在家做其他事情,不至于一直等待什么都做不了。非阻塞效率更高
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

娃娃 哈哈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值