1.Junit单元测试框架
1.1 概述
junit是单元测试,你想测哪个方法就写一个对应的测试方法,然后用junit运行。每个方法之间是独立的,非常灵活。而且测试方法一般不会直接写在原类中,而是单独的测试类,这样测试代码就完全与逻辑代码分开了。
Junit是Java语言编写单元测试框架。Junit属于第三方工具,一般情况下需要导入jar包,而多数Java开发环境都集成了Junit。
1.2 使用方式
问题:在之前,我们写了一个方法,我们都需要使用通过main方法来调用。当我们需要测试的方法很多的时候,每次使用main方法来调用时是非常麻烦的。所以我们有了接下来的单元测试,在开发中使用调用方法就显得特别的简单。
创建java项目,并创建“com.itheima.sh.junit”包
-
编写测试类
说明:如果以前想让一个方法运行必须在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模型中的 Socket
和 ServerSocket
相对应的 SocketChannel
和 ServerSocketChannel
两种不同的套接字通道实现。
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在大门见面
阻塞:我提前到大门口等待,对于我什么都做不了就是阻塞。
非阻塞:快递员什么时候到,给我打电话我再去,期间我可以在家做其他事情,不至于一直等待什么都做不了。非阻塞效率更高