标签 JAVA基础 下的文章

概念

  • IO:输入/输出(Input/Output),视角为内存视角。
  • 流:是一种抽象概念,是对数据传输的总称。也就是说数据在设备间的传输称为流,流的本质是数据传输
  • IO流就是用来处理设备间数据传输问题的。常见的应用场景: 文件复制; 文件上传; 文件下载、文件查看

IO流的分类

  • 按照数据的流向

    • 输入流:读数据
    • 输出流:写数据
  • 按照数据类型来分

    • 字节流(万能流)

      • 字节输入流(InputStream)
      • 字节输出流(OutputStream)
    • 字符流

      • 字符输入流(Reader)
      • 字符输出流(Writer)
  • 字符流指的是传递字符的流,建议只用于读写纯文本文件;

    • 纯文本文件:如果某个文件用系统自带的记事本打开并且能够轻松看懂,就是纯文本文件。
  • 字节流指的是传递字节的流,可以读写任意文件。又称万能流。

3. 字节流

字节流就是以字节为单位操作数据的流。

3.1 字节输出流:写

3.1.1 基本使用:写一个字节

  • 相关方法说明
方法签名说明
FileOutputStream FileOutputStream(File file)基于一个File封装一个FileOutputStream对象
FileOutputStream FileOutputStream(String pathName)基于一个字符串格式的文件路径封装一个FileOutputStream对象
void write(int b)把指定的内容写入文件
void close()关闭流释放资源
  • 三个步骤

    1. 创建字节输出流(FileOutputStream)对象
    2. 写出数据
    3. 关闭流释放资源
  • 注释

    // FileOutputStream FileOutputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileOutputStream对象
    // FileOutputStream FileOutputStream(File file)        基于一个File封装一个FileOutputStream对象
    // void write(int b)  把指定的内容写入文件
    // void close()  关闭流释放资源
  • 演示代码

    
    /**
     * 字节输出流入门案例:写一个字节
     */
    public class ByteStreamDemo01 {
        public static void main(String[] args) throws IOException {
    
            // FileOutputStream FileOutputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileOutputStream对象
            // FileOutputStream FileOutputStream(File file)        基于一个File封装一个FileOutputStream对象
            // void write(int b)  把指定的内容写入文件
            // void close()  关闭流释放资源
    
    
    
            // 创建自己输入流对象
            // 创建输出流对象时,会判断关联的文件是否存在;
            // 如果不存在,会自动创建一个空白的文件
            // 如果存在,则清空文件内容
            FileOutputStream fos = new FileOutputStream("d:\\file\\a.txt");
    
            // 写出数据
            // 字节输入流,操作的是字节;写97其实写的不是97这两个字符,而是一个码点/码值;
            // 程序会把他当做一个字节写入到文件。我们查看,把这个字节的数字作为码点,查询字符集中对应的字符,后展示。
            fos.write(98);
    
            // 关闭流释放资源
            // 创建输入流对象时,会自动关联对应的文件;这个时候,可以理解为当前这个Java程序独占了该文件
            // 其他的程序想要操作相同文件时,会有问题。
            // 所以使用完之后,要关闭流,释放对文件的独占
            fos.close();
    
        }
    }

3.1.2 写入一个字节数组

  • 相关方法说明
方法签名说明
void write(int b)一次写一个字节数据(编码值)到指定位置(文件)
void write(byte[] b)一次写一个字节数组数据到指定位置(文件)
void write(byte[] b,int off,int len)一次写一个字节数组的部分到指定位置(文件)
  • 注释

    // void write(byte[] b)                一次写一个字节数组数据到指定位置(文件)
    // void write(byte[] b,int off,int len)一次写一个字节数组的部分到指定位置(文件) 
  • 演示代码

    
    public class ByteSteamDemo01 {
        public static void main(String[] args) throws IOException {
    
    
            // 1. 创建字节输出流(`FileOutputStream`)对象
            // FileOutputStream FileOutputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileOutputStream对象
            FileOutputStream fos = new FileOutputStream("d:\\file\\java74\\a.txt",true);
    
            // 2.1 写出数据,每次写一个字节
            // void write(int b)  把指定的内容写入文件
            fos.write(97);
            fos.write(98);
            fos.write(99);
            fos.write(48);
    
            // 2.2 写出数据,每次写一个字节数组
            // void write(byte[] b)                一次写一个字节数组数据到指定位置(文件)
            fos.write(new byte[]{97,98,99});
    
            // void write(byte[] b,int off,int len)一次写一个字节数组的部分到指定位置(文件)
            // fos.write(new byte[]{-28, -72, -83, -27, -101, -67},0,3);
            fos.write(new byte[]{-28, -72, -83, -27, -101, -67},3,3);
    
            // 乱码的可能原因之一:使用字节流读写时,没有读写一个字符应该拥有的所有字节
            // fos.write(new byte[]{-28, -72, -83, -27, -101, -67},0,2);
    
            // 3. 关闭流释放资源
            fos.close();
        }
    }

换行

  • 写数据时写入如下内容即可实现换行

    • windows:\r\n(新版本Windows中 也可以正确识别Linuxmac的换行符)
    • linux:\n
    • mac:\r
  • 字符串提供了把字符串对象转换成字节数组的方法

    • Byte[] getBytes()
  • 演示代码

    // 加一个换行
    // 新版本Windows中  也可以正确识别`Linux`和`mac`的换行符
    fos.write("\r".getBytes());
    fos.write("你好,中国!".getBytes());
    // 加一个换行
    fos.write("\n".getBytes());
    fos.write("你好,中国!".getBytes());
    // 加一个换行
    fos.write("\r\n".getBytes());
    fos.write("你好,中国!".getBytes());

追加写入

创建FileOutputStream对象时,除了关联一个文件之外,还可指定是否追加写入。

  • public FileOutputStream(String name,boolean append)

    • 创建文件输出流以指定的名称写入文件。
    • 如果第二个参数为true ,则字节追加将写入文件的末尾;而不是直接写入,覆盖原有内容。
  • 演示代码

    // 第二个参数指定为true,关联文件后写入内容时,会追加写入而非清空原有内容
    FileOutputStream fos = new FileOutputStream("d:\\file\\a.txt",true);

    完整代码

    
    public class ByteStremDemo03 {
        public static void main(String[] args) throws IOException {
    
            // 1. 创建字节输出流(FileOutputStream)对象
            // 第二个参数指定为true,关联文件后写入内容时,会追加写入而非清空原有内容
            FileOutputStream fos = new FileOutputStream("d:\\file\\a.txt",true);
    
            // 2. 写入数据
    
            // 写入换行 windows
            fos.write("\r\n".getBytes());
    
            // 使用字节流,写  “你好,中国!”到文件
            // 使用API把字符串转为字节(而非自己查码表后拼接),写入文件
            String str = "你好,中国2023!";
            byte[] strBytes = str.getBytes();
            fos.write(strBytes);
    
    
            // 3. 关闭流释放资源
            fos.close();
    
        }
    }

注意事项

  1. 创建字节输出流(FileOutputStream)对象,会去关联一个文件并进行一些基础的操作。

    • 如果文件不存在,就创建空白文件;
    • 如果文件存在且有内容,默认清空文件内容;

      • 如果不想文件内容被清空,就在创建对应输出流对象的时候,指定第二个参数为true,表示追加写入。
  2. 写数据

    • 写出的整数,实际写出的是整数作为编码值在码表上对应的字符;
    • 可以写入多个字节(一个字节数组或者一个字节数组的部分内容);
    • 如果需要换行就写入"\r\n".getBytes()
  3. 释放资源

    • Java程序操作文件时会独占关联的文件,每次使用完流必须要释放资源。否则其他程序将不能正确访问该文件;

字节输入流:读

基本读取:读一个字节

字节输入流:读取一个字节

  • 相关方法说明
方法签名说明
FileInputStream FileInputStream(String pathName)基于一个字符串格式的文件路径封装一个FileInputStream对象
int read()从文件中读取一个字节,值为字节的编码值
void close()关闭流释放资源
  • 步骤

    1. 创建输入流对象(FileInputStream),关联文件
    2. 读数据
    3. 关闭流释放资源
  • 注释

    // FileInputStream FileInputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileInputStream对象
    // int read()从文件中读取一个字节,值为字节的码表值
    // void close()关闭流释放资源 
  • 演示代码

    
    public class ByteStreamDemo02 {
        public static void main(String[] args) throws IOException {
            // 1. 创建流对象,关联文件
            // FileInputStream FileInputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileInputStream对象
            FileInputStream fis = new FileInputStream("d:\\file\\java74\\a.txt");
    
            // 2. 读:读一个字节
            // int read()   从文件中读取一个字节,返回值为字节的码表值
            int charPoint = fis.read();
            System.out.println("charPoint = " + ((char)charPoint));
    
            // 3. 关闭流,释放资源
            // void close() 关闭流释放资源
            fis.close();
        }
    }
  • 文件内容

    abcdefg
  • 控制台输出

    a

读多个字节

字节输入流:循环读取多个字节

  • 相关方法说明
方法签名说明
int read()从文件中读取一个字节,值为字节的编码值
  • 演示代码

    
    public class ByteStreamDemo03 {
        public static void main(String[] args) throws IOException {
            // 1. 创建流对象,关联文件
            // FileInputStream FileInputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileInputStream对象
            FileInputStream fis = new FileInputStream("d:\\file\\java74\\a.txt");
    
            // 2. 读:读一个字节
            // int read()   从文件中读取一个字节,返回值为字节的码表值
            // read(fis);
            // 定义变量,接收每次读取到的码点
            int charPoint;
            // 循环读取码点,并和-1判断
            while ((charPoint = fis.read()) != -1) {
                // 如果不是-1,说明没有读取到文件末尾,就使用读取到的这个码点(eg:转换成字符使用)
                System.out.println(charPoint + " : " + ((char) charPoint));
            }
    
            // 3. 关闭流,释放资源
            // void close() 关闭流释放资源
            fis.close();
        }
    
        private static void read(FileInputStream fis) throws IOException {
            int charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + (charPoint));
        }
    }
  • 文件内容

    abcdefg
  • 控制台输出

    97 : a
    98 : b
    99 : c
    100 : d
    101 : e
    102 : f
    103 : g

读字节数组

字节输入流:读一个字节数组

  • 相关方法说明
方法签名说明
int read(byte[] bytes)从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1
byte[] readAllBytes()读取文件中所有内容并转换为字节数组返回
  • 演示代码

    
    public class ByteStreamDemo04 {
        public static void main(String[] args) throws IOException {
            // 1. 创建流对象,关联文件
            // FileInputStream FileInputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileInputStream对象
            FileInputStream fis = new FileInputStream("d:\\file\\java74\\a.txt");
    
            // 2. 读:读一个字节数组
            // read2Array(fis);
    
    
            //  byte[] readAllBytes()   读取文件中所有内容并转换为字节数组返回
            byte[] allBytes = fis.readAllBytes();
    
            String str = new String(allBytes);
            // allBytes.length = 7
            System.out.println("allBytes.length = " + allBytes.length);
            // str = abcdefg
            System.out.println("str = " + str);
    
            // 3. 关闭流,释放资源
            // void close() 关闭流释放资源
            fis.close();
        }
    
        /**
         * 读:读一个字节数组
         * @param fis
         * @throws IOException
         */
        private static void read2Array(FileInputStream fis) throws IOException {
            //  int read(byte[] bytes)  从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1
            byte[] bytes = new byte[1024];
            int len = fis.read(bytes);
            // 打印字节数组,看到大量的默认值0  bytes = [97, 98, 99, 100, 101, 102, 103, 0, 0, ...]
            // System.out.println("bytes = " + Arrays.toString(bytes));
    
            // 转换成字符串,会出现大量乱码  abcdefg后面有一大堆方块
            // String str = new String(bytes);
    
            // 不想出现乱码,就要控制转换的长度,只转换读取到的有效内容 str = abcdefg
            String str = new String(bytes,0,len);
            System.out.println("str = " + str);
        }
    }
  • 文件内容

    abcdefg
  • 控制台输出内容

注意:
  • read方法调用时,需要传递一个字节数组对象,用于接收读取到的多个字节
  • 使用字节数组中数据时,需要通过读取的长度来控制,否则可能造成数据出错。

循环读取多个字节数组

字节输入流-循环读取多个字节数组

方法和之前一样,配合while循环使用

  • 相关方法说明
方法签名说明
int read(byte[] bytes)从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1
  • 演示代码

    
    /**
     * 字节输入流:循环读取多个字节数组
     * @Description: 字节输入流:读一个字节
     */
    public class ByteStreamDemo05 {
        public static void main(String[] args) throws IOException {
            // 1. 创建流对象,关联文件
            // FileInputStream FileInputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileInputStream对象
            FileInputStream fis = new FileInputStream("d:\\file\\java74\\a.txt");
    
            // 2. 读:读一个字节数组
    
            // read(fis);
            //  int read(byte[] bytes)  从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1
            byte[] bytes = new byte[2];
            int len;
            while ((len = fis.read(bytes)) != -1) {
                // 使用读取到的字节数组
                // 最后一次数据中,多了一个f
                // String str = new String(bytes);
                // 读取多少个字节,就转多少个字节,不会出现错误数据
                String str = new String(bytes,0,len);
                System.out.println("str = " + str);
            }
    
            // 3. 关闭流,释放资源
            // void close() 关闭流释放资源
            fis.close();
        }
    
        private static void read(FileInputStream fis) throws IOException {
            //  int read(byte[] bytes)  从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1
            byte[] bytes = new byte[1024];
            int len = fis.read(bytes);
    
            // 返回的是成功读取的字节数
            System.out.println("len = " + len);
    
    
            len = fis.read(bytes);
            // 返回的是成功读取的字节数
            System.out.println("len = " + len);
        }
    
    }
  • 核心代码

    //  int read(byte[] bytes)  从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1
    byte[] bytes = new byte[2];
    int len;
    while ((len = fis.read(bytes)) != -1) {
        // 使用读取到的字节数组
        // 最后一次数据中,多了一个f
        // String str = new String(bytes);
        // 读取多少个字节,就转多少个字节,不会出现错误数据
        String str = new String(bytes,0,len);
        System.out.println("str = " + str);
    }
  • 文件内容

    abcdefg
  • 控制台输出内容

    str = ab
    str = cd
    str = ef
    str = g
注意:
  • read方法调用时,需要传递一个字节数组对象,用于接收读取到的多个字节
  • 使用字节数组中数据时,需要通过读取的长度来控制,否则可能造成数据出错。

注意事项

  1. 创建字节输入流(FileInputStream)对象,会去关联一个文件并进行一些基础的操作。

    • 如果文件不存在,就报错;
  2. 读数据

    • read():读取的内容默认得到的是该数据的编码值,读取到文件末尾则返回-1
    • read(byte[] bytes):读取到多个字节并存入字节数组中,返回成功读取的字节个数;读取到文件末尾返回-1
      在使用返回的数组时,一定要注意使用读取到的有效字节个数,否则数据有误。
  3. 释放资源

    • Java程序操作文件时会独占关联的文件,每次使用完流必须要释放资源。否则其他程序将不能正确访问该文件。

读写案例:复制文件

  • 需求:
    把一个文件复制到另外一个位置:把D:\\file\\猜猜我是啥.mp4复制到其他位置。
  • 概念
    数据源:被读取的文件,复制数据的来源
    目的地:要写入的文件,复制数据的目的地
  • 演示代码

    
    
    /**
     * 使用字节流复制文件
     *
     * @Description: 使用字节流复制文件
     */
    public class ByteStreamDemo06 {
        public static void main(String[] args) throws IOException {
            // 1. 数据源:读,关联输入流。内存视角
            FileInputStream fis = new FileInputStream("D:\\file\\猜猜我是啥.mp4");
    
            // 2. 目的地:写,关联输出流。内存视角
            FileOutputStream fos = new FileOutputStream("D:\\file\\猜猜你是啥.mp4");
    
    
            // 3. 边读编写
            // 3.1 每次读写一个字节
    
            // operateByByte(fis, fos);
    
            // 3.2 每次读写一个字节数组
            operateByByteArray(fis, fos);
    
    
            // 4. 关闭流,释放资源(先开后关)
            fos.close();
            fis.close();
    
    
        }
    
        /**
         * 每次读写一个字节数组
         * @param fis
         * @param fos
         * @throws IOException
         */
        private static void operateByByteArray(FileInputStream fis, FileOutputStream fos) throws IOException {
            // 统计性能,开始时间
            long start = System.currentTimeMillis();
    
    
            // a. 定义一个字节数组,用于存储每次读取到的多个字节
            byte[] bytes = new byte[1024];
            // b. 定义一个int变量,用于记录每次读取到的字节数
            int len;
            // c. 循环读取并出入字节数组,知道返回的有效字节数为-1
            while ((len = fis.read(bytes)) != -1) {
                // d. 循环内部,不停的将读取到的数组写入文件,注意控制长度
                fos.write(bytes,0,len);
            }
    
            // 统计性能,结束时间
            long end = System.currentTimeMillis();
            // 自定义字节数组复制,共耗时:45ms
            System.out.println("自定义字节数组复制,共耗时:" + (end - start) + "ms");
        }
    
        /**
         * 每次读写一个字节
         *
         * @param fis
         * @param fos
         * @throws IOException
         */
        private static void operateByByte(FileInputStream fis, FileOutputStream fos) throws IOException {
            // 统计性能,开始时间
            long start = System.currentTimeMillis();
    
            // a. 定义一个变量,接收每次读取到的字节(的码点)
            int read;
            // b. 循环不断的读,直到读到文件结尾,返回-1
            while ((read = fis.read()) != -1) {
                // c. 每次读取到之后,就写入文件
                fos.write(read);
            }
            // 统计性能,结束时间
            long end = System.currentTimeMillis();
            // 逐个字节复制,共耗时:37946ms
            System.out.println("逐个字节复制,共耗时:" + (end - start) + "ms");
        }
    }

单字节拷贝慢的原因

  • 每次读取一个字节,需要大量频繁的让内存和硬盘交互,性能低下。

多字节拷贝速度提高的原因

  • 减少了硬盘和内存的交互次数,提高了性能。

释放资源

手动处理

思考:如何确保资源被正确释放

输出流操作文件期间如果产生了异常,异常之后的代码就不会继续执行,也就无法释放资源。

这个程序(生产环境程序不会关闭)就会独占关联的文件,造成其他程序无法访问该文件。

我们就要想办法让任何时候,资源都可以释放掉。

所以这个时候就需要用到finally

  • 演示代码

    
    public class ByteStreamDemo07 {
        public static void main(String[] args) {
            FileInputStream fis = null;
            FileOutputStream fos = null;
    
            try {
                System.out.println(1 / 0);
                // 读,输入流 从硬盘到内存
                fis = new FileInputStream("d:\\file\\猜猜我是啥.mp4");
    
    
                // 写,输出流, 从内存到硬盘
                fos = new FileOutputStream("d:\\file\\猜猜我是啥2.mp4");
    
    
                // 定义变量记录每次读取到的字节个数
                int len;
                // 创建数组缓存读取到的数据
                byte[] bytes = new byte[1024];
                // 边读边写
                while ((len = fis.read(bytes)) != -1) {
                    fos.write(bytes, 0, len);
                }
            } catch (IOException e) {
                e.printStackTrace();
                // 处理异常。常见的操作:记录日志,调用第三方接口发邮件、发短信
            } finally {
                // 关闭流,释放资源
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
    
                }
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("流真的关闭了,舒了一口气!!");
            }
        }
    }
问题:释放资源的代码比核心代码还多还复杂!!!

自动释放

JDK7中提供了语法糖,简化资源的释放。可以理解为对try代码块的增强。

语法糖名称:try-with-resource

格式:

try(开启资源){
    // 可能出现异常的代码
}catch(异常类型 异常变量){
    // 处理异常
}

作用:

  • 在try小括号中开启的资源,会在try代码块结束后自动释放
  • 只要是实现了Closeable接口的类,都可以称之为资源,都可以被Try-With-Resource管理并自动关闭
  • 演示代码

    
    
    /**
     * JDK7新特性演示:自动释放资源
     *
     */
    public class TryWithResource {
    
    
        public static void main(String[] args) {
    
            /*
                JDK7提供的新特性:
                Try-With-Resource:可以理解为对Try{}catch(){}finally{}的简化
    
                格式:
                    try(开启资源){}catch(){处理异常}
    
                效果:
                    在try小括号中开启的资源,会在try代码块结束后自动释放
    
                资源:
                    只要是实现了AutoCloseable接口的类,都可以称之为资源,都可以被Try-With-Resource管理并自动关闭
    
             */
    
            // beforeJdk7();
    
            // jdk7_01();
            // jdk7_02();
    
    
        }
    
        private static void jdk7_02() {
            // 在try小括号中开启的资源,try代码块中可以使用,并且会在try代码块执行完成后自动关闭释放
            // 多个资源
            try (
                    FileInputStream fis = new FileInputStream("d:\\a.mp4");
                    FileOutputStream fos = new FileOutputStream("d:\\a.mp4");
            ) {
    
                int read = fis.read();
                System.out.println("(char)read = " + (char) read);
                fos.write(read);
    
                // Try代码块中,如果出现异常,会被catch字句捕获,并在字句中处理
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private static void jdk7_01() {
            // 在try小括号中开启的资源,try代码块中可以使用,并且会在try代码块执行完成后自动关闭释放
            // 单个资源
            try (FileInputStream fis = new FileInputStream("d:\\a.mp4")) {
    
                int read = fis.read();
                System.out.println("(char)read = " + (char) read);
    
                // Try代码块中,如果出现异常,会被catch字句捕获,并在字句中处理
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private static void beforeJdk7() {
            FileInputStream fis = null;
            try {
                fis = new FileInputStream("d:\\a.mp4");
                int read = fis.read();
                System.out.println("read = " + (char) read);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

Java中的IO流主要分为四类:字节流、字符流、字节缓冲流和字符缓冲流

输入和输出

字符流

字符流是专门为读取文本数据而生的

FileReader类

这是字符输入流,用来将文件中的字符数据读取到程序中来

创建字符输入流通道

FileReader(File file) 创建字符输入流管道与源文件接通
FileReader(String pathname) 创建字符输入流管道与源文件接通

FileReader读取文件的步骤如下:

//第一步:创建FileReader对象与要读取的源文件接通
//第二步:调用read()方法读取文件中的字符
//第三步:调用close()方法关闭流

read()

每次读取一个字符返回,没有发现没有数据可读会返回-1

read(char[] buffer)

每次会一个字符数组去读取数据,返回字符数读取了多少个字符,如果没有发现数据可读会返回-1

read()的应用

public class FileReaderTest1 {
    public static void main(String[] args)  {
        try (
                // 1、创建一个文件字符输入流管道与源文件接通
                Reader fr = new FileReader("text.txt");
         ){
                // 2、一个字符一个字符的读(性能较差)
        // 记住每次读取的字符编号。
               int c;
               while ((c = fr.read()) != -1){
                    System.out.print((char) c);
                }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

read(byte[] buffer)的应用

public class FileReaderTest1 {
    public static void main(String[] args)  {
        try (
              //创建一个文件字符输入流管道与源文件接通
              Reader fr = new FileReader("io-app2\\src\\itheima01.txt");
        ){
            //每次读取多个字符。(性能是比较不错的!)
            char[] buffer = new char[3];
            int len; // 记住每次读取了多少个字符。
            while ((len = fr.read(buffer)) != -1){
                // 读取多少倒出多少
                System.out.print(new String(buffer, 0, len));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

FileWriter类

它可以将程序中的字符数据写入文件

FileWriter往文件中写字符数据的步骤如下:

第一步:创建FileWirter对象与要读取的目标文件接通
第二步:调用write(字符数据/字符数组/字符串)方法读取文件中的字符
第三步:调用close()方法关闭流

创建字节输出流管道与源文件对象接通

//会覆盖原文件内容
FileWriter(File file) 创建字节输出流管道与源文件对象接通
FileWirter(String filePath) 创建字节输出流管道与源文件对象接通

//可以追加内容
FileWriter(File file,boolean append) 创建字节输出流管道与源文件对象接通 可以追加内容
FileWirter(String filePath,boolean append) 创建字节输出流管道与源文件对象接通 可以追加内容

write(int c)

写一个字符

write(String str)

写一个字符串

write(String str,int off,int len)

写一个字符串的一部分

wirte(char[] cbuf)

写入一个字符数组

wirte(char[] cbuf,int off,int len)

写入字符数组的一部分

前提

try (
// 0、创建一个文件字符输出流管道与目标文件接通。
// 覆盖管道
// Writer fw = new FileWriter("io-app2/src/itheima02out.txt");
 // 追加数据的管道
Writer fw = new FileWriter("io-app2/src/itheima02out.txt", true);
){
//下面内容
} catch (Exception e) {
   e.printStackTrace();
}

write(int c)的应用

// 1、public void write(int c):写一个字符出去
fw.write('a');
fw.write(97);
//fw.write('磊'); // 写一个字符出去
fw.write("\r\n"); // 换行

write(String str)的应用

// 2、public void write(String c)写一个字符串出去
fw.write("我爱你中国abc");
fw.write("\r\n");

write(String str,int off,int len)的应用

// 3、public void write(String c ,int pos ,int len):写字符串的一部分出去
fw.write("我爱你中国abc", 0, 5);
fw.write("\r\n");

wirte(char[] cbuf)的应用

// 4、public void write(char[] buffer):写一个字符数组出去
char[] buffer = {'黑', '马', 'a', 'b', 'c'};
fw.write(buffer);
fw.write("\r\n");

wirte(char[] cbuf,int off,int len)

// 5、public void write(char[] buffer ,int pos ,int len):写字符数组的一部分出去
fw.write(buffer, 0, 2);
fw.write("\r\n");

字节流

字节流就是以字节为单位操作数据的流。

FileOutputStream

创建字节流输出通道

FileOutputStream(File file) //基于一个File封装一个FileOutputStream对象
FileOutputStream(String pathName) //基于一个字符串格式的文件路径封装一个FileOutputStream对象

write(int b)

把指定的内容写入文件

write(byte[] b)

一次写一个字节数组数据到指定位置(文件)

write(byte[] b,int off,int len)

一次写一个字节数组的部分到指定位置(文件)

前提

常见一个字节输出流
FileOutputStream fos = new FileOutputStream("a.txt",true);

write(int b)的应用

//写出数据,每次写一个字节
// void write(int b)  把指定的内容写入文件
fos.write(97);
fos.write(98);
fos.write(99);
fos.write(48);

write(byte[] b)的应用

//写出数据,每次写一个字节数组
// void write(byte[] b)                一次写一个字节数组数据到指定位置(文件)
fos.write(new byte[]{97,98,99});

write(byte[] b,int off,int len)的应用

一次写一个字节数组的部分到指定位置(文件)
// fos.write(new byte[]{-28, -72, -83, -27, -101, -67},0,3);
fos.write(new byte[]{-28, -72, -83, -27, -101, -67},3,3);

注意事项

[scode type="red" size=""]

  1. 创建字节输出流(FileOutputStream)对象,会去关联一个文件并进行一些基础的操作。

    • 如果文件不存在,就创建空白文件;
    • 如果文件存在且有内容,默认清空文件内容;

      • 如果不想文件内容被清空,就在创建对应输出流对象的时候,指定第二个参数为true,表示追加写入。
  2. 写数据

    • 写出的整数,实际写出的是整数作为编码值在码表上对应的字符;
    • 可以写入多个字节(一个字节数组或者一个字节数组的部分内容);
    • 如果需要换行就写入"\r\n".getBytes()
  3. 释放资源

    • Java程序操作文件时会独占关联的文件,每次使用完流必须要释放资源。否则其他程序将不能正确访问该文件;

[/scode]

FileInputStream

基于一个字符串格式的文件路径封装一个FileInputStream对象

read()

从文件中读取一个字节,值为字节的编码值

read(byte[] bytes)

从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1

readAllBytes()

读取文件中所有内容并转换为字节数组返回

前题

FileInputStream fis = new FileInputStream("a.txt");

read()的应用

int read()   从文件中读取一个字节,返回值为字节的码表值

read(byte[] bytes)的应用

byte[] bytes = new byte[1024];
int len = fis.read(bytes);
// 打印字节数组,看到大量的默认值0  bytes = [97, 98, 99, 100, 101, 102, 103, 0, 0, ...]
// System.out.println("bytes = " + Arrays.toString(bytes));

// 转换成字符串,会出现大量乱码  abcdefg后面有一大堆方块
// String str = new String(bytes);

// 不想出现乱码,就要控制转换的长度,只转换读取到的有效内容 str = abcdefg
String str = new String(bytes,0,len);
System.out.println("str = " + str);

readAllBytes()的应用

//  byte[] readAllBytes()   读取文件中所有内容并转换为字节数组返回
byte[] allBytes = fis.readAllBytes();

String str = new String(allBytes);

注意事项

[scode type="red" size=""]

  1. 创建字节输入流(FileInputStream)对象,会去关联一个文件并进行一些基础的操作。

    • 如果文件不存在,就报错;
  2. 读数据

    • read():读取的内容默认得到的是该数据的编码值,读取到文件末尾则返回-1
    • read(byte[] bytes):读取到多个字节并存入字节数组中,返回成功读取的字节个数;读取到文件末尾返回-1
      在使用返回的数组时,一定要注意使用读取到的有效字节个数,否则数据有误。
  3. 释放资源

    • Java程序操作文件时会独占关联的文件,每次使用完流必须要释放资源。否则其他程序将不能正确访问该文件。

[/scode]

字节缓冲流

字符缓冲流

序列化

反序列化

字符集

计算机只能处理0和1组成的二进制数据,为了让计算机能够存储处理字符,计算机的设计者美国人就把他们会用到的每一个字符进行了编码(所谓编码,就是人为的为每一个字符关联一个数字(称为码点)),计算机在存储某个字符时,存储的就是字符对应码点的二进制数据。
ASCII字符集:《美国信息交换标准代码》,包含英文字母、数字、标点符号、控制字符
    特点:1个字符占1个字节

GBK字符集:中国人自己的字符集,兼容ASCII字符集,还包含2万多个汉字
    特点:1个字母占用1个字节;1个汉字占用2个字节

Unicode字符集:包含世界上所有国家的文字,有三种编码方案,最常用的是UTF-8
    UTF-8编码方案:英文字母、数字占1个字节兼容(ASCII编码)、汉字字符占3个字节
  • 相关方法(String类中方法)
方法名说明
byte[] getBytes()使用平台的默认字符集将一个字符串编码为字节数组
byte[] getBytes(String charsetName)使用指定的字符集将一个字符串编码为字节数组
String(byte[] bytes)使用平台的默认字符集解码指定的字节数组来创建字符串
String(byte[] bytes, String charsetName)通过指定的字符集解码指定的字节数组来创建字符串
  • 注释

    // byte[] getBytes () 使用平台的默认字符集将一个字符串编码为字节数组
    // byte[] getBytes (String charsetName)使用指定的字符集将一个字符串编码为字节数组
    // String( byte[] bytes)使用平台的默认字符集解码指定的字节数组来创建字符串 |
    // String( byte[] bytes, String charsetName)通过指定的字符集解码指定的字节数组来创建字符串
public static void main(String[] args) throws UnsupportedEncodingException {

        // byte[] getBytes () 使用平台的默认字符集将一个字符串编码为字节数组
        // byte[] getBytes (String charsetName)使用指定的字符集将一个字符串编码为字节数组
        // String( byte[] bytes)使用平台的默认字符集解码指定的字节数组来创建字符串 |
        // String( byte[] bytes, String charsetName)通过指定的字符集解码指定的字节数组来创建字符串

        // 1. 编码
        // 从字符串  ->   码点数字
        String str1 = "中国";
        // byte[] getBytes () 使用平台的默认字符集将一个字符串编码为字节数组
        byte[] str1U8Bytes = str1.getBytes();
        // str1U8Bytes = [-28, -72, -83, -27, -101, -67]
        System.out.println("str1U8Bytes = " + Arrays.toString(str1U8Bytes));


        // byte[] getBytes (String charsetName)使用指定的字符集将一个字符串编码为字节数组
        byte[] strGbkBytes = str1.getBytes("GBK");
        // strGbkBytes = [-42, -48, -71, -6]
        System.out.println("strGbkBytes = " + Arrays.toString(strGbkBytes));


        // 2.解压
        // 从 码点数字(字节数组) -> 字符串 ====> 字符串构造方法


        // 使用默认字符集:U8
        // String( byte[] bytes)使用平台的默认字符集解码指定的字节数组来创建字符串
        String str2 = new String(new byte[]{-28, -72, -83, -27, -101, -67});
        // str2 = 中国
        System.out.println("str2 = " + str2);

        // 使用指定字符集:GBK
        // String( byte[] bytes, String charsetName)通过指定的字符集解码指定的字节数组来创建字符串
        String str3 = new String(new byte[]{-42, -48, -71, -6},"gbk");
        // str3 = 中国
        System.out.println("str3 = " + str3);

        // 演示乱码
        // 编解码使用不同的字符集:UTF-8解码时,使用字节数组是通过GBK编码出来的
        String str4 = new String(new byte[]{-42, -48, -71, -6});
        // �й�
        System.out.println("str4 = " + str4);

    }

集合概述和分类

集合的分类

前面我们已经学习过了ArrayList集合,但是除了ArrayList集合,Java还提供了很多种其他的集合,如下图所示:

1666154871520.png

这些集合一共分两大类,如下图所示:一类是单列集合元素是一个一个的,另一类是双列集合元素是一对一对的。

1666154948620.png

主要学习Collection单列集合。Collection是单列集合的根接口,Collection接口下面又有两个子接口List接口、Set接口,List和Set下面分别有不同的实现类,如下图所示:

1666155169359.png

上图中各种集合的特点如下图所示:

1666155218956.png

可以自己写代码验证一下,各种集合的特点

//简单确认一下Collection集合的特点
ArrayList<String> list = new ArrayList<>(); //存取顺序一致,可以重复,有索引
list.add("java1");
list.add("java2");
list.add("java1");
list.add("java2");
System.out.println(list); //[java1, java2, java1, java2] 

HashSet<String> list = new HashSet<>(); //存取顺序不一致,不重复,无索引
list.add("java1");
list.add("java2");
list.add("java1");
list.add("java2");
list.add("java3");
System.out.println(list); //[java3, java2, java1] 

Collection集合的常用方法

Collection集合的一些常用方法,这些方法所有Collection实现类都可以使用。 这里我们以创建ArrayList为例,来演示

Collection<String> c = new ArrayList<>();
//1.public boolean add(E e): 添加元素到集合
c.add("java1");
c.add("java1");
c.add("java2");
c.add("java2");
c.add("java3");
System.out.println(c); //打印: [java1, java1, java2, java2, java3]

//2.public int size(): 获取集合的大小
System.out.println(c.size()); //5

//3.public boolean contains(Object obj): 判断集合中是否包含某个元素
System.out.println(c.contains("java1")); //true
System.out.println(c.contains("Java1")); //false

//4.pubilc boolean remove(E e): 删除某个元素,如果有多个重复元素只能删除第一个
System.out.println(c.remove("java1")); //true
System.out.println(c); //打印: [java1,java2, java2, java3]

//5.public void clear(): 清空集合的元素
c.clear(); 
System.out.println(c); //打印:[]

//6.public boolean isEmpty(): 判断集合是否为空 是空返回true 反之返回false
System.out.println(c.isEmpty()); //true

//7.public Object[] toArray(): 把集合转换为数组
Object[] array = c.toArray();
System.out.println(Arrays.toString(array)); //[java1,java2, java2, java3]

//8.如果想把集合转换为指定类型的数组,可以使用下面的代码
String[] array1 = c.toArray(new String[c.size()]);
System.out.println(Arrays.toString(array1)); //[java1,java2, java2, java3]

//9.还可以把一个集合中的元素,添加到另一个集合中
Collection<String> c1 = new ArrayList<>();
c1.add("java1");
c1.add("java2");
Collection<String> c2 = new ArrayList<>();
c2.add("java3");
c2.add("java4");
c1.addAll(c2); //把c2集合中的全部元素,添加到c1集合中去
System.out.println(c1); //[java1, java2, java3, java4]

最后,我们总结一下Collection集合的常用功能有哪些,ArrayList、LinkedList、HashSet、LinkedHashSet、TreeSet集合都可以调用下面的方法。

Collection遍历方式

因为Collection集合是单列集合的祖宗类,学会了Collection集合的遍历,就可以用来遍历其他所有单列集合

迭代器遍历集合

接下来学习的迭代器就是一种集合的通用遍历方式。

代码写法如下:

Collection<String> c = new ArrayList<>();
c.add("赵敏");
c.add("小昭");
c.add("素素");
c.add("灭绝");
System.out.println(c); //[赵敏, 小昭, 素素, 灭绝]

//第一步:先获取迭代器对象
//解释:Iterator就是迭代器对象,用于遍历集合的工具)
Iterator<String> it = c.iterator();

//第二步:用于判断当前位置是否有元素可以获取
//解释:hasNext()方法返回true,说明有元素可以获取;反之没有
while(it.hasNext()){
    //第三步:获取当前位置的元素,然后自动指向下一个元素.
    String e = it.next();
    System.out.println(s);
}

迭代器代码的原理如下:

  • 当调用iterator()方法获取迭代器时,当前指向第一个元素
  • hasNext()方法则判断这个位置是否有元素,如果有则返回true,进入循环
  • 调用next()方法获取元素,并将当月元素指向下一个位置,
  • 等下次循环时,则获取下一个元素,依此内推

1666162606524.png

最后,我们再总结一下,使用迭代器遍历集合用到哪些方法

1666162899638.png

增强for遍历集合

接下来介绍一种更加简化的写法,叫做增强for循环。

格式如下:

1666163065998.png

需要注意的是,增强for不光可以遍历集合,还可以遍历数组。接下来我们用代码演示一em.o下:

Collection<String> c = new ArrayList<>();
c.add("赵敏");
c.add("小昭");
c.add("素素");
c.add("灭绝");

//1.使用增强for遍历集合
for(String s: c){
    System.out.println(s); 
}

//2.再尝试使用增强for遍历数组
String[] arr = {"迪丽热巴", "古力娜扎", "稀奇哈哈"};
for(String name: arr){
    System.out.println(name);
}

forEach遍历集合

在JDK8版本以后还提供了一个forEach方法也可以遍历集合,如果下图所示:

1666163351517.png

我们发现forEach方法的参数是一个Consumer接口,而Consumer是一个函数式接口,所以可以传递Lambda表达式

Collection<String> c = new ArrayList<>();
c.add("赵敏");
c.add("小昭");
c.add("素素");
c.add("灭绝");

//调用forEach方法
//由于参数是一个Consumer接口,所以可以传递匿名内部类
c.forEach(new Consumer<String>{
    @Override
    public void accept(String s){
        System.out.println(s);
    }
});


//也可以使用lambda表达式对匿名内部类进行简化
c.forEach(s->System.out.println(s)); //[赵敏, 小昭, 素素, 灭绝]

遍历集合案例

接下来,我们看一个案例,在集合中存储自定义的对象,并遍历。具体要求如下

1666164331639.png

首先,我们得写一个电影类,用来描述每一步电影应该有哪些信息。

public class Movie{
    private String name; //电影名称
    private double score; //评分
    private String actor; //演员
    //无参数构造方法
    public Movie(){}
    //全参数构造方法
    public Movie(String name, double score, String actor){
        this.name=name;
        this.score=score;
        this.actor=actor;
    }
    //...get、set、toString()方法自己补上..
}

接着,再创建一个测试类,完成上面的需求

public class Test{
    public static void main(String[] args){
        Collection<Movie> movies = new ArrayList<>();
        movies.add(new MOvie("《肖申克的救赎》", 9.7, "罗宾斯"));
        movies.add(new MOvie("《霸王别姬》", 9.6, "张国荣、张丰毅"));
        movies.add(new MOvie("《阿甘正传》", 9.5, "汤姆汉克斯"));
        
        for(Movie movie : movies){
            System.out.println("电影名:" + movie.getName());
            System.out.println("评分:" + movie.getScore());
            System.out.println("主演:" + movie.getActor());
        }
    }
}

以上代码的内存原理如下图所示:当往集合中存对象时,实际上存储的是对象的地址值

1666165033103.png

List系列集合

1666165150752.png

List集合的常用方法

List集合是索引的,所以多了一些有索引操作的方法,如下图所示:

1666165187815.png

接下来,我们用代码演示一下这几个方法的效果

//1.创建一个ArrayList集合对象(有序、有索引、可以重复)
List<String> list = new ArrayList<>();
list.add("蜘蛛精");
list.add("至尊宝");
list.add("至尊宝");
list.add("牛夫人"); 
System.out.println(list); //[蜘蛛精, 至尊宝, 至尊宝, 牛夫人]

//2.public void add(int index, E element): 在某个索引位置插入元素
list.add(2, "紫霞仙子");
System.out.println(list); //[蜘蛛精, 至尊宝, 紫霞仙子, 至尊宝, 牛夫人]

//3.public E remove(int index): 根据索引删除元素, 返回被删除的元素
System.out.println(list.remove(2)); //紫霞仙子
System.out.println(list);//[蜘蛛精, 至尊宝, 至尊宝, 牛夫人]

//4.public E get(int index): 返回集合中指定位置的元素
System.out.println(list.get(3));

//5.public E set(int index, E e): 修改索引位置处的元素,修改后,会返回原数据
System.out.println(list.set(3,"牛魔王")); //牛夫人
System.out.println(list); //[蜘蛛精, 至尊宝, 至尊宝, 牛魔王]

List集合的遍历方式

List集合相比于前面的Collection多了一种可以通过索引遍历的方式,所以List集合遍历方式一共有四种:

  • 普通for循环(只因为List有索引)
  • 迭代器
  • 增强for
  • Lambda表达式
List<String> list = new ArrayList<>();
list.add("蜘蛛精");
list.add("至尊宝");
list.add("糖宝宝");

//1.普通for循环
for(int i = 0; i< list.size(); i++){
    //i = 0, 1, 2
    String e = list.get(i);
    System.out.println(e);
}

//2.增强for遍历
for(String s : list){
    System.out.println(s);
}

//3.迭代器遍历
Iterator<String> it = list.iterator();
while(it.hasNext()){
    String s = it.next();
    System.out.println(s);
}

//4.lambda表达式遍历
list.forEach(s->System.out.println(s));

ArrayList底层原理(重要)

ArrayList集合底层是基于数组结构实现的,也就是说当你往集合容器中存储元素时,底层本质上是往数组中存储元素。 特点如下:

1666166151267.png

我们知道数组的长度是固定的,但是集合的长度是可变的,这是怎么做到的呢?原理如下:

1666166661149.png

数组扩容,并不是在原数组上扩容(原数组是不可以扩容的),底层是创建一个新数组,然后把原数组中的元素全部复制到新数组中去。

1666166956907.png

LinkedList底层原理

LinkedList底层是链表结构,链表结构是由一个一个的节点组成,一个节点由数据值、下一个元素的地址组成。如下图所示

1666167170415.png

如,现在要在B节点和D节点中间插入一个元素,只需要把B节点指向D节点的地址断掉,重新指向新的节点地址就可以了。如下图所示:

1666167298885.png

假如,现在想要把D节点删除,只需要让C节点指向E节点的地址,然后把D节点指向E节点的地址断掉。此时D节点就会变成垃圾,会把垃圾回收器清理掉

1666167419164.png

上面的链表是单向链表,它的方向是从头节点指向尾节点的,只能从左往右查找元素,这样查询效率比较慢;还有一种链表叫做双向链表,不光可以从做往右找,还可以从右往左找。如下图所示:

1666167523139.png

LinkedList集合是基于双向链表实现了,所以相对于ArrayList新增了一些可以针对头尾进行操作的方法,如下图示所示:

1666167572387.png

LinkedList集合的应用场景

  • 我们先来认识一下队列结构,队列结构你可以认为是一个上端开口,下端也开口的管子的形状。元素从上端入队列,从下端出队列。

1666167793391.png

入队列可以调用LinkedList集合的addLast方法,出队列可以调用removeFirst()方法.

//1.创建一个队列:先进先出、后进后出
LinkedList<String> queue = new LinkedList<>();
//入对列
queue.addLast("第1号人");
queue.addLast("第2号人");
queue.addLast("第3号人");
queue.addLast("第4号人");
System.out.println(queue);

//出队列
System.out.println(queue.removeFirst());//第4号人
System.out.println(queue.removeFirst());//第3号人
System.out.println(queue.removeFirst());//第2号人
System.out.println(queue.removeFirst());//第1号人
  • 接下来,我们再用LinkedList集合来模拟一下栈结构的效果。还是先来认识一下栈结构长什么样。栈结构可以看做是一个上端开头,下端闭口的水杯的形状。
    元素永远是上端进,也从上端出,先进入的元素会压在最底下,所以栈结构的特点是先进后出,后进先出

1666168222486.png

有没有感觉栈结构很像,手枪的子弹夹呀!!第一个压进入的子弹在最底下,最后一个才能打出来,最后一个压进入的子弹在最顶上,第一个打出来。

1666168656191.png

接着,我们就用LinkedList来模拟下栈结构,代码如下:

//1.创建一个栈对象
LinkedList<String> stack = new ArrayList<>();
//压栈(push) 等价于 addFirst()
stack.push("第1颗子弹");
stack.push("第2颗子弹");
stack.push("第3颗子弹");
stack.push("第4颗子弹");
System.out.println(stack); //[第4颗子弹, 第3颗子弹, 第2颗子弹,第1颗子弹]

//弹栈(pop) 等价于 removeFirst()
System.out.println(statck.pop()); //第4颗子弹
System.out.println(statck.pop()); //第3颗子弹
System.out.println(statck.pop()); //第2颗子弹
System.out.println(statck.pop()); //第1颗子弹

//弹栈完了,集合中就没有元素了
System.out.println(list); //[]

总结

  1. List集合的特点是什么?

    有序、可重复、有索引
  2. List集合底层是基于什么数据结构的?

    基于数组实现的
  3. 为什么List集合查询快、增删慢?

    查询快:因为数组是连续的一段内存空间,当指定下标时,计算机可以算出对于元素的地址,所以查询快

    增删慢:因为数组是连续的一段内存空间,假如在数组中间增加删除元素,后面的所有元素全要后移或前移

  4. 你能说出List集合的扩容过程吗?(四步)

    1. 无参构造创建List集合,默认大小是0
    2. 添加第一个元素时,底层会创建一个长度为10的数组
    3. 存满时,会扩容1.5倍
    4. 如果一次添加多个元素,1.5倍还放不下,则新创建的数组的长度以实际为准

正则表达式

正则表达式其实是由一些特殊的符号组成的,它代表的是某种规则。

正则表达式的作用1:用来校验字符串数据是否合法

正则表达式的作用2:可以从一段文本中查找满足要求的内容

正则表达式初体验

现在,我们就以QQ号码为例,来体验一下正则表达式的用法。注意:现在仅仅只是体验而已,我们还没有讲正则表达式的具体写法。

  • 不使用正则表达式,校验QQ号码代码是这样的
public static boolean checkQQ(String qq){
        // 1、判断qq号码是否为null
        if(qq == null || qq.startsWith("0") || qq.length() < 6 || qq.length() > 20){
            return false;
        }

        // 2、qq至少是不是null,不是以0开头的,满足6-20之间的长度。
        // 判断qq号码中是否都是数字。
        // qq = 2514ghd234
        for (int i = 0; i < qq.length(); i++) {
            // 根据索引提取当前位置处的字符。
            char ch = qq.charAt(i);
            // 判断ch记住的字符,如果不是数字,qq号码不合法。
            if(ch < '0' || ch > '9'){
                return false;
            }
        }
        // 3、说明qq号码肯定是合法
        return true;
    }
  • 用正则表达式代码是这样的
public static boolean checkQQ1(String qq){
    return qq != null && qq.matches("[1-9]\\d{5,19}");
}

使用正则表达式,大大简化的了代码的写法。

正则表达式书写规则

前面我们已经体验到了正则表达式,可以简化校验数据的代码书写。这里需要用到一个方法叫matches(String regex)。这个方法时属于String类的方法。

1667469085970.png

这个方法是用来匹配一个字符串是否匹配正则表达式的规则,参数需要调用者传递一个正则表达式。但是正则表达式不能乱写,是有特定的规则的。

下面我们就学习一下,正则表达式的规则。从哪里学呢?在API中有一个类叫做Pattern,我们可以到API文档中搜索,关于正则表达式的规则,这个类都告诉我们了。我这里把常用的已经给大家整理好了。

1667469259345.png

我们将这些规则,在代码中演示一下

/**
 * 目标:掌握正则表达式的书写规则
 */
public class RegexTest2 {
    public static void main(String[] args) {
        // 1、字符类(只能匹配单个字符)
        System.out.println("a".matches("[abc]"));    // [abc]只能匹配a、b、c

        System.out.println("d".matches("[^abc]"));   // [^abc] 不能是abc


        // 2、预定义字符(只能匹配单个字符)  .  \d  \D   \s  \S  \w  \W

        // \转义
        System.out.println("\"");
        // \n \t
        System.out.println("3".matches("\\d"));  // \d: 0-9
        System.out.println("a".matches("\\d"));  //false


        // 3、数量词: ?   *   +   {n}   {n, }  {n, m}
        System.out.println("1".matches("[0-9]?"));   // ? 代表0次或1次

        System.out.println("123".matches("[0-9]*"));   // * 代表0次或多次

    }
}

正则表达式应用案例

学习完正则表达式的规则之后,我们看几个实际案例。

  • 正则表达式校验手机号码
/**
 * 目标:校验用户输入的电话、邮箱、时间是否合法。
 */
public class RegexTest3 {
    public static void main(String[] args) {
        checkPhone();
    }

    public static void checkPhone(){
        while (true) {
            System.out.println("请您输入您的电话号码(手机|座机): ");
            Scanner sc = new Scanner(System.in);
            String phone = sc.nextLine();
            // 1. 手机号码,以1开头,第二位是3-9中的任意一个数字,后面跟着9个数字。
            // 2. 座机号码,以0开头,后面跟着2-7位数字,然后可能会有一个横杠(“-”),后面是4-19位数字。
            if(phone.matches("(1[3-9]\\d{9})|(0\\d{2,7}-?[1-9]\\d{4,19})")){
                System.out.println("您输入的号码格式正确~~~");
                break;
            }else {
                System.out.println("您输入的号码格式不正确~~~");
            }
        }
    }
}
  • 使用正则表达式校验邮箱是否正确
public class RegexTest3 {
    public static void main(String[] args) {
        checkEmail();
    }

    public static void checkEmail(){
        while (true) {
            System.out.println("请您输入您的邮箱: ");
            Scanner sc = new Scanner(System.in);
            String email = sc.nextLine();

            // 1. \w{2,} 匹配一个包含至少两个字母数字字符的字符串,用来匹配电子邮件地址的用户名部分。
            //2. @ 匹配一个 "@" 符号,用来分隔用户名和域名。
            //3. \w{2,20} 匹配一个包含至少两个字母数字字符、至多20个字母数字字符的字符串,用来匹配电子邮件地址的域名部分。
            //4. (\\.\\w{2,10}){1,2} 匹配一个域名后面跟着的一个或两个点号(用 \\ 来转义点号),每个点号后面跟着一个至少两个、至多十个字母数字字符的字符串,用来匹配电子邮件地址的子域名和顶级域名。
            if(email.matches("\\w{2,}@\\w{2,20}(\\.\\w{2,10}){1,2}")){
                System.out.println("您输入的邮箱格式正确~~~");
                break;
            }else {
                System.out.println("您输入的邮箱格式不正确~~~");
            }
        }
    }
}

总结一下,正则表达式优缺点都很明显。优点是很简练,缺点是可读性和可扩展性很差,以及很难写。

认识异常

我们调用一个方法时,经常一不小心就出异常了,然后在控制台打印一些异常信息。其实打印的这些异常信息,就叫做异常。

异常的继承体系

1667313423356.png

1.1.2演示运行时异常

int[] arr = {11,22,33};
//5是一个不存在的索引,所以此时产生ArrayIndexOutOfBoundsExcpetion
System.out.println(arr[5]); 

下图是API中对ArrayIndexOutOfBoundsExcpetion类的继承体系,以及告诉我们它在什么情况下产生。

1667313567748.png

1.1.3演示编译时异常

编译时异常,它把异常的提醒提前了,你调用方法是否真的有问题,只要可能有问题就给你报出异常提示(红色波浪线)。

编译时异常的目的:意思就是告诉你,你小子注意了!!,这里小心点容易出错,仔细检查一下

1667313705048.png

1.1.4认识异常处理

有人说,我检查过了,我确认我的代码没问题,为了让它不报错,继续将代码写下去。我们这里有两种解决方案。

  • 第一种:使用throws在方法上声明,意思就是告诉下一个调用者,这里面可能有异常啊,你调用时注意一下。
/**
 * 目标:认识异常。
 */
public class ExceptionTest1 {
    public static void main(String[] args) throws ParseException{
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date d = sdf.parse("2028-11-11 10:24");
        System.out.println(d);
    }
}
  • 第二种:使用try...catch语句块异常进行处理。
public class ExceptionTest1 {
    public static void main(String[] args) throws ParseException{
        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date d = sdf.parse("2028-11-11 10:24");
            System.out.println(d);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}

1.2 自定义异常

Java提供了一些异常,但是Java官方无法为这个世界上的全部问题都提供异常类,如果企业自己的某种问题,想通过异常来表示,那就需要自己来定义异常类了。

我们通过一个实际场景,来给大家演示自定义异常。

需求:写一个saveAge(int age)方法,在方法中对参数age进行判断,如果age<0或者>=150就认为年龄不合法,如果年龄不合法,就给调用者抛出一个年龄非法异常。

分析:Java的API中是没有年龄非常这个异常的,所以我们可以自定义一个异常类,用来表示年龄非法异常,然后再方法中抛出自定义异常即可。

  • 先写一个异常类AgeIllegalException(这是自己取的名字,名字取得很奈斯),继承
// 1、必须让这个类继承自Exception,才能成为一个编译时异常类。
public class AgeIllegalException extends Exception{
    public AgeIllegalException() {
    }

    public AgeIllegalException(String message) {
        super(message);
    }
}
  • 再写一个测试类,在测试类中定义一个saveAge(int age)方法,对age判断如果年龄不在0~150之间,就抛出一个AgeIllegalException异常对象给调用者。
public class ExceptionTest2 {
    public static void main(String[] args) {
        // 需求:保存一个合法的年
        try {
            saveAge2(225);
            System.out.println("saveAge2底层执行是成功的!");
        } catch (AgeIllegalException e) {
            e.printStackTrace();
            System.out.println("saveAge2底层执行是出现bug的!");
        }
    }

//2、在方法中对age进行判断,不合法则抛出AgeIllegalException
    public static void saveAge(int age){
        if(age > 0 && age < 150){
            System.out.println("年龄被成功保存: " + age);
        }else {
            // 用一个异常对象封装这个问题
            // throw 抛出去这个异常对象
            throw new AgeIllegalRuntimeException("/age is illegal, your age is " + age);
        }
    }
}
  • 注意咯,自定义异常可能是编译时异常,也可以是运行时异常
1.如果自定义异常类继承Excpetion,则是编译时异常。
特点:方法中抛出的是编译时异常,必须在方法上使用throws声明,强制调用者处理。

2.如果自定义异常类继承RuntimeException,则运行时异常。
特点:方法中抛出的是运行时异常,不需要在方法上用throws声明。

1.3 深入异常处理

比如有如下的场景:A调用用B,B调用C;C中有异常产生抛给B,B中有异常产生又抛给A;异常到了A这里就不建议再抛出了,因为最终抛出被JVM处理程序就会异常终止,并且给用户看异常信息,用户也看不懂,体验很不好。

此时比较好的做法就是:1.将异常捕获,将比较友好的信息显示给用户看;2.尝试重新执行,看是是否能修复这个问题。

1667315686041.png

我们看一个代码,main方法调用test1方法,test1方法调用test2方法,test1和test2方法中多有扔异常。

  • 第一种处理方式是,在main方法中对异常进行try...catch捕获处理了,给出友好提示。
public class ExceptionTest3 {
    public static void main(String[] args)  {
        try {
            test1();
        } catch (FileNotFoundException e) {
            System.out.println("您要找的文件不存在!!");
            e.printStackTrace(); // 打印出这个异常对象的信息。记录下来。
        } catch (ParseException e) {
            System.out.println("您要解析的时间有问题了!");
            e.printStackTrace(); // 打印出这个异常对象的信息。记录下来。
        }
    }

    public static void test1() throws FileNotFoundException, ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date d = sdf.parse("2028-11-11 10:24:11");
        System.out.println(d);
        test2();
    }

    public static void test2() throws FileNotFoundException {
        // 读取文件的。
        InputStream is = new FileInputStream("D:/meinv.png");
    }
}
  • 第二种处理方式是:在main方法中对异常进行捕获,并尝试修复
/**
 * 目标:掌握异常的处理方式:捕获异常,尝试修复。
 */
public class ExceptionTest4 {
    public static void main(String[] args) {
        // 需求:调用一个方法,让用户输入一个合适的价格返回为止。
        // 尝试修复
        while (true) {
            try {
                System.out.println(getMoney());
                break;
            } catch (Exception e) {
                System.out.println("请您输入合法的数字!!");
            }
        }
    }

    public static double getMoney(){
        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请您输入合适的价格:");
            double money = sc.nextDouble();
            if(money >= 0){
                return money;
            }else {
                System.out.println("您输入的价格是不合适的!");
            }
        }
    }
}

1.1 认识算法

算法是解决某个实际问题的过程和方法。比如百度地图给你规划路径,计算最优路径的过程就需要用到算法。再比如你在抖音上刷视频时,它会根据你的喜好给你推荐你喜欢看的视频,这里也需要用到算法。

作为java程序员,我们日常几乎用不到算法,只需要学习几种常见的基础算法就可以了。而且Java语言本身就内置了一些基础算法给我们使用,实际上自己也不会去写这些算法。

我们为什么要学习算法呢?算法可以锻炼我们的编程思维。

1.2 冒泡排序(重要)

接下来,我们学习一种算法叫排序算法,它可以价格无序的整数,排列成从小到大的形式(升序),或者从大到小的形式(降序)

学习算法我们先要搞清楚算法的流程,然后再去“推敲“如何写代码。(注意,我这里用的次是推敲,也就是说算法这样的代码并不是一次成型的,是需要反复修改才能写好的**)。

1667403334232.png

先来学习冒泡排序,先来介绍一下,冒泡排序的流程

冒泡排序核心思路:每次将相邻的两个元素继续比较
如下图所示:
   第一轮比较 3次
   第二轮比较 2次
   第三轮比较 1次

1667403618904.png

public class Test1 {
    public static void main(String[] args) {
        // 1、准备一个数组
        int[] arr = {5, 2, 3, 1};

        // 2、定义一个循环控制排几轮
        for (int i = 0; i < arr.length - 1; i++) {
            // i = 0  1  2           【5, 2, 3, 1】    次数
            // i = 0 第一轮            0   1   2         3
            // i = 1 第二轮            0   1             2
            // i = 2 第三轮            0                 1

            // 3、定义一个循环控制每轮比较几次。
            for (int j = 0; j < arr.length - i - 1; j++) {
                // 判断当前位置的元素值,是否大于后一个位置处的元素值,如果大则交换。
                if(arr[j] > arr[j+1]){
                    int temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
        System.out.println(Arrays.toString(arr));
    }
}

选择排序

我们先分析选择排序算法的流程:选择排序的核心思路是,每一轮选定一个固定的元素,和其他的每一个元素进行比较;经过几轮比较之后,每一个元素都能比较到了。

1667467347586.png

接下来,按照选择排序的流程编写代码

ublic class Test2 {
    public static void main(String[] args) {
        // 1、准备好一个数组
        int[] arr = {5, 1, 3, 2};
        //           0  1  2  3

        // 2、控制选择几轮
        for (int i = 0; i < arr.length - 1; i++) {
            // i = 0 第一轮    j = 1 2 3
            // i = 1 第二轮    j = 2 3
            // i = 2 第三轮    j = 3
            // 3、控制每轮选择几次。
            for (int j = i + 1; j < arr.length; j++) {
                // 判断当前位置是否大于后面位置处的元素值,若大于则交换。
                if(arr[i] > arr[j]){
                    int temp = arr[i];
                    arr[i] = arr[j];
                    arr[j] = temp;
                }
            }
        }
        System.out.println(Arrays.toString(arr));
    }
}

查找算法

1.3 查找算法

接下来,我们学习一个查找算法叫做二分查找。在学习二分查找之前,我们先来说一下基本查找,从基本查找的弊端,我们再引入二分查找,这样我们的学习也会更加丝滑一下。

先聊一聊基本查找:假设我们要查找的元素是81,如果是基本查找的话,只能从0索引开始一个一个往后找,但是如果元素比较多,你要查找的元素比较靠后的话,这样查找的此处就比较多。性能比较差。

1667467611576.png

再讲二分查找:二分查找的主要特点是,每次查找能排除一般元素,这样效率明显提高。但是二分查找要求比较苛刻,它要求元素必须是有序的,否则不能进行二分查找。

  • 二分查找的核心思路
第1步:先定义两个变量,分别记录开始索引(left)和结束索引(right)
第2步:计算中间位置的索引,mid = (left+right)/2;
第3步:每次查找中间mid位置的元素,和目标元素key进行比较
如果中间位置元素比目标元素小,那就说明mid前面的元素都比目标元素小
此时:left = mid+1
    如果中间位置元素比目标元素大,那说明mid后面的元素都比目标元素大
    此时:right = mid-1
如果中间位置元素和目标元素相等,那说明mid就是我们要找的位置
此时:把mid返回
注意:一搬查找一次肯定是不够的,所以需要把第1步和第2步循环来做,只到left>end就结束,如果最后还没有找到目标元素,就返回-1.

1667468412882.png

/**
 * 目标:掌握二分查找算法。
 */
public class Test3 {
    public static void main(String[] args) {
        // 1、准备好一个数组。
        int[] arr = {7, 23, 79, 81, 103, 127, 131, 147};

        System.out.println(binarySearch(arr, 150));

        System.out.println(Arrays.binarySearch(arr, 81));
    }

    public static int binarySearch(int[] arr, int data){
        // 1、定义两个变量,一个站在左边位置,一个站在右边位置
        int left = 0;
        int right = arr.length - 1;

        // 2、定义一个循环控制折半。
        while (left <= right){
            // 3、每次折半,都算出中间位置处的索引
            int middle = (left + right) / 2;
            // 4、判断当前要找的元素值,与中间位置处的元素值的大小情况。
            if(data < arr[middle]){
                // 往左边找,截止位置(右边位置) = 中间位置 - 1
                right = middle - 1;
            }else if(data > arr[middle]){
                // 往右边找,起始位置(左边位置) = 中间位置 + 1
                left = middle + 1;
            }else {
                // 中间位置处的元素值,正好等于我们要找的元素值
                return middle;
            }
        }
        return -1; // -1特殊结果,就代表没有找到数据!数组中不存在该数据!
    }
}

Lambda是用来简化匿名代码的书写格式的,而方法引用是用来进一步简化Lambda表达式的,它简化的更加过分。

静态方法引用

准备好下面的代码

public class Test1 {
    public static void main(String[] args) {
        Student[] students = new Student[4];
        students[0] = new Student("蜘蛛精", 169.5, 23);
        students[1] = new Student("紫霞", 163.8, 26);
        students[2] = new Student("紫霞", 163.8, 26);
        students[3] = new Student("至尊宝", 167.5, 24);

        // 原始写法:对数组中的学生对象,按照年龄升序排序
        Arrays.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.getAge() - o2.getAge(); // 按照年龄升序排序
            }
        });

        // 使用Lambda简化后的形式
        Arrays.sort(students, (o1, o2) -> o1.getAge() - o2.getAge());
    }
}

准备另外一个类CompareByData类,用于封装Lambda表达式的方法体代码;

public class CompareByData {
    public static int compareByAge(Student o1, Student o2){
        return o1.getAge() - o2.getAge(); // 升序排序的规则
    }
}

现在我们就可以把Lambda表达式的方法体代码,改为下面的样子

Arrays.sort(students, (o1, o2) -> CompareByData.compareByAge(o1, o2));

Java为了简化上面Lambda表达式的写法,利用方法引用可以改进为下面的样子。实际上就是用类名调用方法,但是把参数给省略了。这就是静态方法引用

//静态方法引用:类名::方法名
Arrays.sort(students, CompareByData::compareByAge);

总结一下,当在Lambda表达式中使用静态方法时,可以使用 类名::方法名 的方式,而这就是静态方法引用

我们学习一个JDK8新增的一种语法形式,叫做Lambda表达式。作用:用于简化匿名内部类代码的书写。**

Lambda表达式基本使用

怎么去简化呢?Lamdba是有特有的格式的,按照下面的格式来编写Lamdba。

(被重写方法的形参列表) -> {
    被重写方法的方法体代码;
}
需要给说明一下的是,在使用Lambda表达式之前,必须先有一个接口,而且接口中只能有一个抽象方法。(注意:不能是抽象类,只能是接口)

像这样的接口,我们称之为函数式接口,只有基于函数式接口的匿名内部类才能被Lambda表达式简化。

public interface Swimming{
    void swim();
}

有了以上的Swimming接口之后,接下来才能再演示,使用Lambda表达式,简化匿名内部类书写。

public class LambdaTest1 {
    public static void main(String[] args) {
        // 目标:认识Lambda表达式.
        //1.创建一个Swimming接口的匿名内部类对象
Swimming s = new Swimming(){
             @Override
             public void swim() {
                 System.out.println("学生快乐的游泳~~~~");
             }
         };
         s.swim();

        //2.使用Lambda表达式对Swimming接口的匿名内部类进行简化
        Swimming s1 = () -> {
              System.out.println("学生快乐的游泳~~~~");
        };
        s1.swim();
    }
}

好的,我们现在已经知道Lamdba表达式可以简化基于函数式接口的匿名内部类的书写。接下来,我们可以把刚才使用Arrays方法时的代码,使用Lambda表达式简化一下了。

public class LambdaTest2 {
    public static void main(String[] args) {
        // 目标:使用Lambda简化函数式接口。
        double[] prices = {99.8, 128, 100};
//1.把所有元素*0.8: 先用匿名内部类写法
        Arrays.setAll(prices, new IntToDoubleFunction() {
            @Override
            public double applyAsDouble(int value) {
                // value = 0  1  2
                return prices[value] * 0.8;
            }
        });
//2.把所有元素*0.8: 改用Lamdba表达式写法
        Arrays.setAll(prices, (int value) -> {
                return prices[value] * 0.8;
        });

        System.out.println(Arrays.toString(prices));
        System.out.println("-----------------------------------------------");

        Student[] students = new Student[4];
        students[0] = new Student("蜘蛛精", 169.5, 23);
        students[1] = new Student("紫霞", 163.8, 26);
        students[2] = new Student("紫霞", 163.8, 26);
        students[3] = new Student("至尊宝", 167.5, 24);
//3.对数组中的元素按照年龄升序排列: 先用匿名内部类写法
        Arrays.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return Double.compare(o1.getHeight(), o2.getHeight()); // 升序
            }
        });
//4.对数组中的元素按照年龄升序排列: 改用Lambda写法
        Arrays.sort(students, (Student o1, Student o2) -> {
                return Double.compare(o1.getHeight(), o2.getHeight()); // 升序
        });
        System.out.println(Arrays.toString(students));
    }
}

到这里,你已经学会了Lambda表达式的基本使用了。

Lambda表达式省略规则

Lambda表达式还可以简化,具体的简化规则如下:

1.Lambda的标准格式
(参数类型1 参数名1, 参数类型2 参数名2) -> {
...方法体的代码...
return 返回值;
}

2.在标准格式的基础上()中的参数类型可以直接省略
(参数名1, 参数名2) -> {
...方法体的代码...
return 返回值;
}

3.如果{}总的语句只有一条语句,则{}可以省略、return关键字、以及最后的“;”都可以省略
(参数名1, 参数名2) -> 结果

4.如果()里面只有一个参数,则()可以省略
参数名 -> 结果

简化案例:匿名内部类开始 -> Lambda标准格式 -> Lambda简化格式

public class LambdaTest2 {
    public static void main(String[] args) {
        // 目标:使用Lambda简化函数式接口。
        double[] prices = {99.8, 128, 100};
//1.对数组中的每一个元素*0.8: 匿名内部类写法
        Arrays.setAll(prices, new IntToDoubleFunction() {
            @Override
            public double applyAsDouble(int value) {
                // value = 0  1  2
                return prices[value] * 0.8;
            }
        });
//2.需求:对数组中的每一个元素*0.8,使用Lambda表达式标准写法
        Arrays.setAll(prices, (int value) -> {
                return prices[value] * 0.8;
        });
//3.使用Lambda表达式简化格式1——省略参数类型
        Arrays.setAll(prices, (value) -> {
            return prices[value] * 0.8;
        });
//4.使用Lambda表达式简化格式2——省略()
        Arrays.setAll(prices, value -> {
            return prices[value] * 0.8;
        });
        //5.使用Lambda表达式简化格式3——省略{}
        Arrays.setAll(prices, value -> prices[value] * 0.8 );

        System.out.println(Arrays.toString(prices));
        
        System.out.println("------------------------------------")

        Student[] students = new Student[4];
        students[0] = new Student("蜘蛛精", 169.5, 23);
        students[1] = new Student("紫霞", 163.8, 26);
        students[2] = new Student("紫霞", 163.8, 26);
        students[3] = new Student("至尊宝", 167.5, 24);

        //1.使用匿名内部类
        Arrays.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return Double.compare(o1.getHeight(), o2.getHeight()); // 升序
            }
        });
//2.使用Lambda表达式表达式——标准格式
        Arrays.sort(students, (Student o1, Student o2) -> {
                return Double.compare(o1.getHeight(), o2.getHeight()); // 升序
        });
//3.使用Lambda表达式表达式——省略参数类型
        Arrays.sort(students, ( o1,  o2) -> {
            return Double.compare(o1.getHeight(), o2.getHeight()); // 升序
        });
//4.使用Lambda表达式表达式——省略{}
        Arrays.sort(students, ( o1,  o2) -> Double.compare(o1.getHeight(), o2.getHeight()));


        System.out.println(Arrays.toString(students));
    }
}

1.1 Arrays基本使用

Arrays是操作数组的工具类,它可以很方便的对数组中的元素进行遍历、拷贝、排序等操作。

下面我们用代码来演示一下:遍历、拷贝、排序等操作。需要用到的方法如下

/**
 * 目标:掌握Arrays类的常用方法。
 */
public class ArraysTest1 {
    public static void main(String[] args) {
        // 1、public static String toString(类型[] arr): 返回数组的内容
        int[] arr = {10, 20, 30, 40, 50, 60};
        System.out.println(Arrays.toString(arr));

        // 2、public static 类型[] copyOfRange(类型[] arr, 起始索引, 结束索引) :拷贝数组(指定范围,包前不包后)
        int[] arr2 = Arrays.copyOfRange(arr, 1, 4);
        System.out.println(Arrays.toString(arr2));

        // 3、public static copyOf(类型[] arr, int newLength):拷贝数组,可以指定新数组的长度。
        int[] arr3 = Arrays.copyOf(arr, 10);
        System.out.println(Arrays.toString(arr3));

        // 4、public static setAll(double[] array, IntToDoubleFunction generator):把数组中的原数据改为新数据又存进去。
        double[] prices = {99.8, 128, 100};
        //                  0     1    2
        // 把所有的价格都打八折,然后又存进去。
        Arrays.setAll(prices, new IntToDoubleFunction() {
            @Override
            public double applyAsDouble(int value) {
                // value = 0  1  2
                return prices[value] * 0.8;
            }
        });
        System.out.println(Arrays.toString(prices));

        // 5、public static void sort(类型[] arr):对数组进行排序(默认是升序排序)
        Arrays.sort(prices);
        System.out.println(Arrays.toString(prices));
    }
}

1.2 Arrays排序对象数组

刚才我们使用Arrays操作数组时,数组中存储存储的元素是int类型、double类型,是可以直接排序的,而且默认是升序排列。

如果数组中存储的元素类型是自定义的对象,如何排序呢?接下来,我们就学习一下Arrays如何对对象数组进行排序。

首先我们要准备一个Student类,代码如下:

public class Student{
    private String name;
    private double height;
    private int age;

    public Student(String name, double height, int age) {
        this.name = name;
        this.height = height;
        this.age = age;
    }

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

然后再写一个测试类,往数组中存储4个学生对象,代码如下。此时,运行代码你会发现是会报错的。

public class ArraysTest2 {
    public static void main(String[] args) {
        // 目标:掌握如何对数组中的对象进行排序。
        Student[] students = new Student[4];
        students[0] = new Student("蜘蛛精", 169.5, 23);
        students[1] = new Student("紫霞", 163.8, 26);
        students[2] = new Student("紫霞", 163.8, 26);
        students[3] = new Student("至尊宝", 167.5, 24);

        // 1、public static void sort(类型[] arr):对数组进行排序。
Arrays.sort(students);
System.out.println(Arrays.toString(students));
    }
}

上面的代码为什么会报错呢?因为Arrays根本就不知道按照什么规则进行排序。为了让Arrays知道按照什么规则排序,我们有如下的两种办法。

  • 排序方式1:让Student类实现Comparable接口,同时重写compareTo方法。Arrays的sort方法底层会根据compareTo方法的返回值是正数、负数、还是0来确定谁大、谁小、谁相等。代码如下:
public class Student implements Comparable<Student>{
    private String name;
    private double height;
    private int age;
    
    //...get、set、空参数构造方法、有参数构造方法...自己补全

    // 指定比较规则
    // this  o
    @Override
    public int compareTo(Student o) {
        // 约定1:认为左边对象 大于 右边对象 请您返回正整数
        // 约定2:认为左边对象 小于 右边对象 请您返回负整数
        // 约定3:认为左边对象 等于 右边对象 请您一定返回0
/* if(this.age > o.age){
            return 1;
        }else if(this.age < o.age){
            return -1;
        }
        return 0;*/

        //上面的if语句,也可以简化为下面的一行代码
        return this.age - o.age; // 按照年龄升序排列
        // return o.age - this.age; // 按照年龄降序排列
    }
    
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", height=" + height +
                ", age=" + age +
                '}';
    }
}
  • 排序方式2:在调用Arrays.sort(数组,Comparator比较器);时,除了传递数组之外,传递一个Comparator比较器对象。Arrays的sort方法底层会根据Comparator比较器对象的compare方法方法的返回值是正数、负数、还是0来确定谁大、谁小、谁相等。代码如下
public class ArraysTest2 {
    public static void main(String[] args) {
        // 目标:掌握如何对数组中的对象进行排序。
        Student[] students = new Student[4];
        students[0] = new Student("蜘蛛精", 169.5, 23);
        students[1] = new Student("紫霞", 163.8, 26);
        students[2] = new Student("紫霞", 163.8, 26);
        students[3] = new Student("至尊宝", 167.5, 24);

// 2、public static <T> void sort(T[] arr, Comparator<? super T> c)
        // 参数一:需要排序的数组
        // 参数二:Comparator比较器对象(用来制定对象的比较规则)
        Arrays.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                // 制定比较规则了:左边对象 o1   右边对象 o2
                // 约定1:认为左边对象 大于 右边对象 请您返回正整数
                // 约定2:认为左边对象 小于 右边对象 请您返回负整数
                // 约定3:认为左边对象 等于 右边对象 请您一定返回0
//                if(o1.getHeight() > o2.getHeight()){
//                    return 1;
//                }else if(o1.getHeight() < o2.getHeight()){
//                    return -1;
//                }
//                return 0; // 升序
                 return Double.compare(o1.getHeight(), o2.getHeight()); // 升序
                // return Double.compare(o2.getHeight(), o1.getHeight()); // 降序
            }
        });
        System.out.println(Arrays.toString(students));
    }
}