NIO <一>

package testb.pro.testbpro.nio;

import lombok.extern.slf4j.Slf4j;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author wxq[IT010511]
 * @Description:
 * @Date: 2022-09-29
 * @Modified by:
 */
@Slf4j
public class TestNio {
    public static void main(String[] args) throws Exception {
        /**
         * nio:non blocking io - new io
         */

        //Channel类似于Stream,是读写数据的双向通道,可以从channel中读取数据写到Buffer
        //也可以从Buffer中读取数据写入Channel,之前的Stream要么是输入流InputSteam要么
        //是输出流OutputStream
        //和channel进行交互的是一个叫Buffer的缓冲区对象

        //常见的Channel有:
        //FileChannel:炒作文件的通道对象
        //DatagramChannel:操作udp协议的网络通道
        //ServerSocketChannel:操作tcp协议的服务端通道
        //SocketChannel:操作tcp协议的客户端通道
        //疑问:channel既然是双向的,为什么还要分socketChannel和ServerSocketChannel


        //buffer是用来缓冲读写数据的缓冲区,常见的缓冲区对象有
        /**
         * ByteBuffer
         *      MappedByteBuffer
         *      DirectByteBuffer:直接缓冲区
         *      HeapByteBuffer:非直接缓冲区
         * ShortBuffer
         * IntBuffer
         * LongBuffer
         * FloatBuffer
         * DoubleBuffer
         * CharBuffer
         */


        //Selector就是一个线程,只不过该线程会监听客户端通道的事件变化,如果哪个通道中有事件,就
        //去处理哪个通道,实现一个线程处理多个通道,叫多路复用

        //多线程版:最初为了处理多个客户端的请求,每来一个客户端,服务端就创建一个线程处理该客户端请求
        /**
         * 客户端1-----------------------------服务端线程1
         * 客户端2-----------------------------服务端线程2
         * 客户端3-----------------------------服务端线程3
         * 客户端4-----------------------------服务端线程4
         */
        //每个线程都会占用一定的内存,一个线程1M,1000个1G,10000000 = 10000G
        //cpu要不停的再众多线程之间切换,要切换就得保存每个线程的状态,比如上次执行到哪里了,
        //下次要从哪里执行,每个线程的局部变量等等,线程上下文切换成本高


        //线程池版:为了解决多线程版线程过多导致内存撑爆和上下文切换成本太高的问题,只能使用线程
        //池来限制线程的数量在一定范围内,而且线程可以复用,不用频繁的创建销毁,但是也有问题,比如
        //线程池总共十个线程,来了十个客户端,但是这十个客户端都阻塞住了,那么其他的客户端就只能一直等待
        //所以线程池方案只适合短连接的方式


        //上面两种方式都是某个客户端来了之后就会获取一个线程,之后这个线程一直为该客户端服务,即使期间
        //该客户端没有任何读写操作,与它绑定的后天线程也得不到释放,那有没有一种方案,使的 如果客户端没有
        //事件就释放后台线程,有事件再获取后台线程,或者反过来,后台线程可以监听到每个客户端的事件变化,
        //当有事件时就处理,没有事件时可以处理其他客户端-------这就是我们的多路复用方案


        //首先:Selector多路复用模式只能工作在非阻塞模式下
        //什么是阻塞模式:channel要读数据结果没有数据,就一直等待;channel要写数据结果没有数据可写,就一直等待
        //Selector也只是一个后台线程,只不过给线程可以检测到每个客户端的事件变化:比如说写事件、
        //读事件、连接事件等,而Selector只服务于有事件的客户端,没有事件的客户端不去理会它


        //FileChannel的使用:项目路径下新建test.txt,里面存上字符串:1234567890abcdefg
        fileChannelDemo();


        //ByteBuffer的正确使用
        /**
         * 向buffer中写入数据:channel.read(buffer); buffer.put();
         * 调用flip()方法切换到读模式
         * 从buffer中读取数据:channel.write(buffer); buffer.get()
         * 清空缓冲区或指针移动到0:buffer.clear(); buffer.compact()----没有读完的数据前移,写的时候从他们的后面写
         *
         */

        //ByteBuffer就是一个数组,有三个重要的属性指标:
        //capacity:数组容量
        //position:下一个可以读写的位置
        //limit:第一个不能读写的位置
        /**
         * 刚创建完成时 ByteBuffer buffer = ByteBuffer.allocate(16);
         * capacity = 16
         * limit = 16  因为该位置不能读取,所以最大的limit就是最大索引+1=容量
         * position = 0
         */

        ByteBuffer buffer = ByteBuffer.allocate(16);
        log.info(buffer.capacity() + "");//16
        log.info(buffer.limit() + "");//16
        log.info(buffer.position() + "");//0
        System.out.println("-------------------------------------------------------------");
        //写入7个字符后
        buffer.put(new byte[]{1, 2, 3, 4, 5, 6, 7});
        log.info(buffer.capacity() + "");//16
        log.info(buffer.limit() + "");//16
        log.info(buffer.position() + "");//7
        log.info(buffer.get(7) + "");//0 因为索引7的位置上还没有写入数据,所以时byte字节数组的默认值0

        System.out.println("-------------------------------------------------------------");
        //切换到读模式
        buffer.flip();
        log.info(buffer.capacity() + "");//16
        log.info(buffer.limit() + "");//7  所以limit时第一个不能读的位置
        log.info(buffer.position() + "");//0

        System.out.println("-------------------------------------------------------------");
        //读取四个字符后
        buffer.get();
        buffer.get();
        buffer.get();
        buffer.get();
        log.info(buffer.capacity() + "");//16
        log.info(buffer.limit() + "");//7  所以limit时第一个不能读的位置
        log.info(buffer.position() + "");//4 下一个可以读取的位置时索引4的位置

        System.out.println("-------------------------------------------------------------");
        //compact时将未读取的数据前移
        //上面已经读取了四个字节,还剩三个字节,所以最后的三个字节会移动到前三个位置,
        // 下一个可以写入的位置时索引为3的位置,即第四个位置
        buffer.compact();
        log.info(buffer.capacity() + "");//16
        log.info(buffer.limit() + "");//16  所以limit时第一个不能读的位置
        log.info(buffer.position() + "");//3
        System.out.println("-------------------------------------------------------------");
        //clear方法时将写指针直接移动到开始的位置即索引为0的位置
        buffer.clear();
        log.info(buffer.capacity() + "");//16
        log.info(buffer.limit() + "");//16  所以limit时第一个不能读的位置
        log.info(buffer.position() + "");//0


        System.out.println("-------------------------------------------------------------");
        //clear和compact只是移动指针,并没有清空里面的数据,里面的数据下次写入时会覆盖
        buffer.flip();
        log.info(buffer.capacity() + "");//16
        log.info(buffer.limit() + "");//0
        log.info(buffer.position() + "");//0
        //因为前面已经通过clear方法已经将limit指针移动到了索引为0的位置,
        // 所以要想读取里面的旧数据,需要手动设置一下
        buffer.limit(16);
        log.info(buffer.get(0) + "");//5  因为前面compact时将最后的三个值移动到了前面,
        // 所以前三个值是567,问:最后三个值是多少呢?还是567
        log.info(buffer.get(1) + "");//6
        log.info(buffer.get(2) + "");//7
        log.info(buffer.get(3) + "");//4


        //ByteBuffer常见方法:allocate
        //注意,后面多次运行main方法,每次都会秀昂test中写入数据,数据超过了会报错,所以如果保存先
        //把test.txt中的数据清除掉
        ByteBuffer buf = ByteBuffer.allocate(60);


        //向Buffer中写数据
        RandomAccessFile file = new RandomAccessFile("test.txt", "rw");
        FileChannel channel = file.getChannel();
        int readByte = channel.read(buf);
        //或者
        log.info(buf.capacity() + "");//60
        log.info(buf.limit() + "");//60
        log.info(buf.position() + "");//5
//        byte bb = 22;
//        buf.put(bb);


        //从buffer中读取数据
        buf.flip();
        int writeBute = channel.write(buf);
        //或者get()方法会移动指针,get(i)方法不会移动指针
        buf.flip();
        byte b = buf.get();


        //如果想重复读取数据
        /**
         * rewind方法会将position重新设置为0
         * get(i)直接回去指定位置的值而不会移动指针
         */


        //mark与reset,makr负责标记锚点,二reset则会回到锚点:
        //也就是使用mark记录下某个索引,比如索引为3的位置,之后经过一系列的操作position的位置
        //肯定也就变了,但是如果此时调用reset方法,position的值马上会变成3
        //需要注意的是:rewind和flip会清除makr的标记


        //字符串与ByteBuffer的相互转换
        ByteBuffer buff1 = StandardCharsets.UTF_8.encode("我是字符串");
        ByteBuffer buf1 = Charset.forName("utf-8").encode("我也是");

        CharBuffer decode = StandardCharsets.UTF_8.decode(buff1);
        System.out.println(decode.toString());
        String s = decode.toString();
        System.out.println(s);


        String ss = new String(buff1.array(), 0, buff1.limit());
        System.out.println(ss);


        //注:buffer是非线程安全的


        //Scattering Reads分散读取  可以将channel数据读到不同的buffer中
        //将test.txt中的内容改成Thereisadog
        //我们可以将每一个有意的的单词读取到一个缓冲区There is a dog 大小:5 2 1 3
        scatteringReader();


        System.out.println("---------------------------------------------------------------");
        //Gathering Writting 集中写:可以将多个buffer的数据一次性写入channel
        //需要注意的是:如果你写完之后想读取文件内容进行查看,那么文件一定要重新加载,因为文件内容已经改变了
        gatherWritting();


        //重要知识点:粘包、半包现象的解决
        /**
         * 我们发送的每一条数据都是有意义的,但是我们不可能将数据一条一条的发送,这样效率太低
         * 一般情况下,我们会将多条数据一起发送,为了将每条消息区分开,我们可以加入分隔符如:\n
         * hello,zhangsan\n
         * hello,lisi\n
         * how do you do wangmazi\n
         * 但是有于种种原因,客户端接收到的数据有可能是如下格式
         * hello,zhangsan\nhello,lisi\nh   这种属于粘包,缓冲区较大,一条消息装不满
         * ow do you do wangmazi\n      这种属于半包,由于粘包的影响,一般会存在半包的情况
         *
         * 还有一种情况是一条消息很大,把缓冲区装满了还没有装下
         * 如:我只发送一条消息,内容是 :1234567890abc\n
         * 但是接受缓冲区的大小为5,所以第一次接受到的数据是
         * 12345
         * 第二次接受到的是
         * 67890
         * 第三次接收到的是
         * abc\n
         * 但这三次接收到的属于同一条消息,需要将他们组合起来
         *
         */
        System.out.println("------------------------------------------------------");
        ByteBuffer buffer1 = ByteBuffer.allocate(29);
        //假入第一次从客户端接收过来的数据是这样的hello,zhangsan\nhello,lisi\nh
        //就需要通过代码处理沾包半包的现象
        buffer1.put("hello,zhangsan\nhello,lisi\nh".getBytes(StandardCharsets.UTF_8));
        nianBaoBanBaoHandler(buffer1);
        //第二次ow do you do wangmazi\n
        buffer1.put("ow do you do wangmazi\n".getBytes(StandardCharsets.UTF_8));
        nianBaoBanBaoHandler(buffer1);




        //文件Io总结
        /**
         * Filechannel只能工作再阻塞模式下
         * FileChannel要通过FileInputStream FileOutputStream RandomAccessfile的getChannel方法来获取
         * 虽然channel是双向的,但是和获取他们的方式也有关系
         * FileInputStream获取的Channel只能读
         * FileOutputStream获取的Channel只能写
         * RandomAccessFile获取的Channel读写权限由构建RandomAccessFile时指定
         *
         *
         * 从chennel读向buffer写,返回读取的字节数,返回-1表示文件数据已经读完了
         * int len = channel.read(buffer);
         *
         * 从buffer读,向channel写,写入前一定要判读buffer中是否有数据,并且buffer一定要切换到读模式
         *  //channel有可能不能一次性将buffer中的数据全部写入,所以需要循环判断
         *  while(buffer.hasRemaining()){
         *      channel.write(buffer);
         * }
         *
         *
         * 使用Channel后必须要关闭,但是如果关闭了他们的创建者,channel也会关闭。如关闭了RandomAccessFile
         *
         * channel也可以获取和设置position
         * long position = channel.position();
         * channel.position(newOps);
         * position如果设置为文件末尾,读取时会返回-1,写入时会追加写入,但是position如果超过了文件
         * 的末尾,写入新的内容时,新内容会和原内容之间产生空隙
         *
         * 使用size方法可以获取文件的大小
         *
         * 出于性能的考虑,数据不会立刻写入磁盘,如果向把数据立刻写入磁盘,可以调用force(true)方法
         *
         *
         *
         * 两个Channel之间的数据传输 ----底层使用的是直接内存,性能非常高:只能传输2G以内的数据
         *  TransferFrom : target.transferFrom(source);
         *  TransferTo:     source.transferTo(target);
         */

        //transferFrom();
        transferTo();



        //Path和Paths:Paht是路径类,Paths是文件工具类
        /**
         * Path source = Paths.get("1.txt"); // 相对路径 使用 user.dir 环境变量来定位 1.txt
         *
         * Path source = Paths.get("d:\\1.txt"); // 绝对路径 代表了  d:\1.txt
         *
         * Path source = Paths.get("d:/1.txt"); // 绝对路径 同样代表了  d:\1.txt
         *
         * Path projects = Paths.get("d:\\data", "projects"); // 代表了  d:\data\projects
         * * . 代表了当前路径
         * * ..代表了上一级路径
         *
         * 例如目录结构如下
         * d:
         * 	|- data
         * 		|- projects
         * 			|- a
         * 			|- b
         *
         * Path path = Paths.get("d:\\data\\projects\\a\\..\\b");
         * System.out.println(path);
         * System.out.println(path.normalize()); // 正常化路径
         * 输出结果
         * d:\data\projects\a\..\b
         * d:\data\projects\b
         */



        //Files文件工具类
        /**
         * 检查文件是否存在
         * Path path = Paths.get("helloword/data.txt");
         * System.out.println(Files.exists(path));
         *
         * 创建一级目录
         * Path path = Paths.get("helloword/d1");
         * Files.createDirectory(path);
         * 如果目录已存在,会抛异常 FileAlreadyExistsException
         * 不能一次创建多级目录,否则会抛异常 NoSuchFileException
         *
         *
         * 创建多级目录用
         * Path path = Paths.get("helloword/d1/d2");
         * Files.createDirectories(path);
         *
         *
         * 拷贝文件
         * Path source = Paths.get("helloword/data.txt");
         * Path target = Paths.get("helloword/target.txt");
         * Files.copy(source, target);
         * 如果文件已存在,会抛异常 FileAlreadyExistsException
         * 如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制
         * Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
         *
         *
         * 移动文件
         * Path source = Paths.get("helloword/data.txt");
         * Path target = Paths.get("helloword/data.txt");
         * Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
         * StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性
         *
         *
         * 删除文件
         * Path target = Paths.get("helloword/target.txt");
         * Files.delete(target);
         * 如果文件不存在,会抛异常 NoSuchFileExceptio
         *
         *
         * 删除目录
         * Path target = Paths.get("helloword/d1");
         * Files.delete(target);
         * 如果目录还有内容,会抛异常 DirectoryNotEmptyException
         */

        //遍历目录文件
        runDirectory();

        //统计jar包数目
        caculatorJars();

        //删除多级目录
        deleteDirctorys();


        //拷贝多级目录
        copyDirectorys();

    }

    public static void fileChannelDemo() {
        try (RandomAccessFile file = new RandomAccessFile("test.txt", "rw")) {
            FileChannel channel = file.getChannel();
            ByteBuffer buffer = ByteBuffer.allocate(10);
            do {
                int len = channel.read(buffer);
                log.info("读到的字节数:{}", len);
                //channel.read没有数据时会返回-1,有数据时返回读到的字节数
                if (len == -1) {
                    break;
                }
                //此时buffer中已经有数据了,切换到度模式读取出里面的数据
                buffer.flip();
                while (buffer.hasRemaining()) {
                    log.info("读到的字符:{}", (char) buffer.get());
                }
                //读完之后清空缓冲区,其实数据没有清除,只是将position设置为0了
                buffer.clear();
                System.out.println("---------------------------------------------");
            } while (true);
        } catch (Exception e) {
            System.out.println(e.getCause());
        }
    }

    public static void scatteringReader() {
        try (RandomAccessFile file = new RandomAccessFile("test.txt", "rw")) {
            FileChannel channel = file.getChannel();
            ByteBuffer a = ByteBuffer.allocate(5);
            ByteBuffer b = ByteBuffer.allocate(2);
            ByteBuffer c = ByteBuffer.allocate(1);
            ByteBuffer d = ByteBuffer.allocate(3);
            ByteBuffer[] buf = new ByteBuffer[]{a, b, c, d};
            channel.read(buf);
            //将所有的buffer切换到读模式
            for (ByteBuffer buffer : buf) {
                buffer.flip();
                //读取数据
                String s = StandardCharsets.UTF_8.decode(buffer).toString();
                System.out.println(s);
            }

        } catch (Exception e) {
            System.out.println();
        }
    }

    public static void gatherWritting() {
        try (RandomAccessFile file = new RandomAccessFile("test.txt", "rw")) {

            FileChannel channel = file.getChannel();
            ByteBuffer d = ByteBuffer.allocate(4);
            ByteBuffer e = ByteBuffer.allocate(4);

            d.put(new byte[]{'f', 'o', 'u', 'r'});
            e.put(new byte[]{'f', 'i', 'v', 'e'});

            //切换成度模式
            d.flip();
            e.flip();
            ByteBuffer buf[] = new ByteBuffer[]{d, e};
            channel.write(buf);

            //读出来看看 因为文件内容已经变了,所以需要重新加载
            RandomAccessFile file2 = new RandomAccessFile("test.txt", "rw");
            FileChannel channel2 = file2.getChannel();
            ByteBuffer f = ByteBuffer.allocate(8);
            channel2.read(f);
            f.flip();
            log.info(f.capacity() + "");//8
            log.info(f.limit() + "");//8
            log.info(f.position() + "");//0

            System.out.println(StandardCharsets.UTF_8.decode(f).toString());

        } catch (Exception e) {
            System.out.println();
        }
    }

    public static void nianBaoBanBaoHandler(ByteBuffer buffer){
        buffer.flip();
        //此时要对buffer做出一定的操作,所以先将buffer的一些原始属性保存一下,以免后面要用
        int oldposition = buffer.position();
        int oldlimit = buffer.limit();
        //判断此次接收到的数据有没有完整的消息:就是判断消息中是否包含\n
        for (int i=0;i<buffer.limit();i++){
            byte b = buffer.get(i);
            if (b=='\n'){
                //如果position=5  此时i=10  那么消息的长度是(不包含\n)?
                ByteBuffer buff = ByteBuffer.allocate(i-buffer.position());
                //现在我们就要把pisition到i之间的数据写入到我们自己的buffer作为一条完整数据
                //这样我们就只能读取pisition到i之间的数据了
                buffer.limit(i);
                //将源buffer中的数据拷贝到目标buf中
                buff.put(buffer);

                //将limit还原
                buffer.limit(oldlimit);
                //虽然get(i)不会移动指针,但是buff.put(Buffer)操作的时候,position已经移动到limit了
                //但是limit的位置是\n,下次不用再读了,所以可以重置一下position
                buffer.position(i+1);

                //打印接收到的完整消息
                buff.flip();
                System.out.println(StandardCharsets.UTF_8.decode(buff).toString());
            }
        }

        //最后有剩下不包括\n的数据(半包),需要和下次读取的数据一起组合成完整的消息,所以缓冲区清理
        //我们使用compact
        buffer.compact();
    }

    public static void transferFrom(){
        System.out.println("--------------transferFrom--------------------------");
        try (RandomAccessFile to = new RandomAccessFile("test2.txt", "rw");
             RandomAccessFile from = new RandomAccessFile("test.txt","rw");) {
            FileChannel toChannel = to.getChannel();
            FileChannel fromChannel = from.getChannel();
            toChannel.transferFrom(fromChannel,0,fromChannel.size());

        }catch (Exception e){}


    }

    public static void transferTo(){
        System.out.println("--------------transferTo--------------------------");
        try (RandomAccessFile to = new RandomAccessFile("test2.txt", "rw");
             RandomAccessFile from = new RandomAccessFile("test.txt","rw");) {
            FileChannel toChannel = to.getChannel();
            FileChannel fromChannel = from.getChannel();
            fromChannel.transferTo(0,fromChannel.size(),toChannel);

        }catch (Exception e){}
    }

    public static void Transfer2g(){
        try (
                FileChannel from = new FileInputStream("data.txt").getChannel();
                FileChannel to = new FileOutputStream("to.txt").getChannel();
        ) {
            // 效率高,底层会利用操作系统的零拷贝进行优化
            long size = from.size();
            // left 变量代表还剩余多少字节
            for (long left = size; left > 0; ) {
                System.out.println("position:" + (size - left) + " left:" + left);
                left -= from.transferTo((size - left), left, to);
            }
        } catch (IOException e) {
        }
    }

    public static void runDirectory()throws Exception{
        Path path = Paths.get("D:\\workSpace\\TestCommon\\src");
        AtomicInteger dirCount = new AtomicInteger();
        AtomicInteger fileCount = new AtomicInteger();

        Path path1 = Files.walkFileTree(path, new SimpleFileVisitor<Path>() {

            //进入文件夹前
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                return super.preVisitDirectory(dir, attrs);
            }

            //访问文件
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                System.out.println("文件名称:"+file);
                fileCount.incrementAndGet();
                return super.visitFile(file, attrs);
            }

            //文件访问失败后
            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                return super.visitFileFailed(file, exc);
            }

            //文件夹访问后
            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                System.out.println("文件夹名称:"+dir);
                dirCount.incrementAndGet();
                return super.postVisitDirectory(dir, exc);
            }
        });

        System.out.println(dirCount);
        System.out.println(fileCount);
    }

    private static void caculatorJars() throws Exception{
        Path path = Paths.get("D:\\Java\\jdk1.8.0_131");
        AtomicInteger jarCount = new AtomicInteger();
        Files.walkFileTree(path,new SimpleFileVisitor<Path>(){
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                if (file.toFile().getName().endsWith(".jar")){
                    jarCount.incrementAndGet();
                }
                return super.visitFile(file, attrs);
            }
        });
        System.out.println("jar包的数目:"+jarCount);
    }


    private static void deleteDirctorys() throws Exception{
        Path path = Paths.get("d:\\aa\\bb");
        Files.walkFileTree(path,new SimpleFileVisitor<Path>(){
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file);
                return super.visitFile(file, attrs);
            }

            //文件夹访问完成后
            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.delete(dir);
                return super.postVisitDirectory(dir, exc);
            }
        });
    }


    private static void copyDirectorys() throws Exception{
        long start = System.currentTimeMillis();
        Path source = Paths.get("D:\\aa");
        Path target = Paths.get("D:\\bb");

        Files.walk(source).forEach(path->{
            try {
                String targetName = path.toString().replace("D:\\aa","D:\\bb");
                //是否是目录
                if (Files.isDirectory(path)){
                    Files.createDirectories(Paths.get(targetName));
                }

                //判断是否是文件
                if (Files.isRegularFile(path)){
                    Files.copy(path,Paths.get(targetName));
                }
            }catch (Exception e){}
        });
        long end = System.currentTimeMillis();
        System.out.println(end-start);
    }


}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值