多线程基础相关概念多线程多线程就是同时执行多个应用程序 需要硬件支持效果上来说就是CPU在多个程序之间快速切换 给人一种多个软件同时执行的感觉并发: 某一个时刻 多个任务同时在执行 在同一时刻 有多个指令在多个CPU上同时执行并发: 某个时间段 多个任务同时执行 但是某个时刻只有一个任务在执行 在某一时间段 有多个指令在单个CPU上交替执行当下电脑执行过程中两种情况会同时存在并发: 是因为目前的cpu都是多核心的 每个核心执行一个指令多个核心就同时在执行多个指令并发:每个核心在某一刻 只能执行一个程序指令 之后某一刻 可能在执行其他软件的命令进程正在运行的软件独立性 进程是一个能独立运行的基本单位 同时也是系统分配资源和调度的独立单位动态性 进程的是指是程序的一次执行过程 进程是动态产生的 动态消亡的并发性 任何进程都可以同其他进程一起并发执行线程是进程中的单个循序控制流 是一条执行路径 应用程序中能做的某个事情随机性: 一个宪曾在某个时间段内在执行 但是这段时间的某个时刻不一定在执行 此时cpu可以能在执行另一个软件 cpu可以在多个线程 随机快速切换执行多线程的实现方式jdk原生支持的1.继承thread类2.实现runnable接口3.实现callable接口继承Thread类方法void run()想要多线程中执行的代码写在这个方法中 在线程启动后此方法将自动被调用执行void start()启动线程的方法 之后java虚拟机会调用run()方法实现步骤定义一个类MyThread继承Thread类在MyThread类中重写run()方法创建MyThread类的对象使用线程对象的start()启动线程public class ThreadDemo01 { public static void main(String[] args) { // 3. 创建MyThread类的对象 MyThread mth1 = new MyThread(); MyThread mth2 = new MyThread(); // 4. 使用线程对象的start()启动线程 mth1.start(); mth2.start(); } } // 1. 定义一个类MyThread继承Thread类 class MyThread extends Thread{ @Override // 2. 在MyThread类中重写run()方法 public void run() { // 这里的代码会在对应的线程启动后自动被执行 for (int i = 0; i < 100; i++) { System.out.println("新的线程开始执行代码了"+i); } } } 重点解析问:为什么要冲洗run()方法因为run()是用封装被线程执行代码的 我们可以想要在多线程环境下运行的代码写在run()方法中 这些代码可以称之为任务问: run()方法和start()方法的区别run() 封装多线程执行的代码 封装任务代码的如果直接调用该方法 就是main方法线程中执行普通方法 也就是普通方法的调用start() 启动线程开启一个新线程 在新县城中执行代码 由jvm调用此线程的run方法执行run中的任务代码实现Runnalbe接口Thread构造方法Thread(Runnable target) 分配一个新的Thread对象Thread(Runnable target,String name) 分配一个新的Thread对象Runnable接口中方法run() 任务方法内部编写要在多线程环境下执行的代码实现步骤定义一个类MyRunnable实现Runnale接口在Myrunnable类中重写run()方法 编写需要在多线程环境下运行的代码
概念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() | 关闭流释放资源 | | | |三个步骤创建字节输出流(FileOutputStream)对象写出数据关闭流释放资源注释// 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中 也可以正确识别Linux和mac的换行符）linux：\nmac：\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(); } }注意事项创建字节输出流(FileOutputStream)对象，会去关联一个文件并进行一些基础的操作。如果文件不存在，就创建空白文件；如果文件存在且有内容，默认清空文件内容；如果不想文件内容被清空，就在创建对应输出流对象的时候，指定第二个参数为true，表示追加写入。写数据写出的整数，实际写出的是整数作为编码值在码表上对应的字符；可以写入多个字节（一个字节数组或者一个字节数组的部分内容）；如果需要换行就写入"\r\n".getBytes()释放资源Java程序操作文件时会独占关联的文件，每次使用完流必须要释放资源。否则其他程序将不能正确访问该文件；字节输入流：读基本读取：读一个字节字节输入流：读取一个字节相关方法说明| 方法签名 | 说明 | | ------------------------------------------------------ | --------------------------------------------------------------- | | FileInputStream FileInputStream(String pathName) | 基于一个字符串格式的文件路径封装一个FileInputStream对象 | | int read() | 从文件中读取一个字节，值为字节的编码值 | | void close() | 关闭流释放资源 | | | |步骤创建输入流对象(FileInputStream)，关联文件读数据关闭流释放资源注释// 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方法调用时，需要传递一个字节数组对象，用于接收读取到的多个字节使用字节数组中数据时，需要通过读取的长度来控制，否则可能造成数据出错。注意事项创建字节输入流(FileInputStream)对象，会去关联一个文件并进行一些基础的操作。如果文件不存在，就报错；读数据read():读取的内容默认得到的是该数据的编码值，读取到文件末尾则返回-1read(byte[] bytes)：读取到多个字节并存入字节数组中，返回成功读取的字节个数；读取到文件末尾返回-1在使用返回的数组时，一定要注意使用读取到的有效字节个数，否则数据有误。释放资源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()每次读取一个字符返回,没有发现没有数据可读会返回-1read(char[] buffer)每次会一个字符数组去读取数据,返回字符数读取了多少个字符,如果没有发现数据可读会返回-1read()的应用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=""]创建字节输出流(FileOutputStream)对象，会去关联一个文件并进行一些基础的操作。如果文件不存在，就创建空白文件；如果文件存在且有内容，默认清空文件内容；如果不想文件内容被清空，就在创建对应输出流对象的时候，指定第二个参数为true，表示追加写入。写数据写出的整数，实际写出的是整数作为编码值在码表上对应的字符；可以写入多个字节（一个字节数组或者一个字节数组的部分内容）；如果需要换行就写入"\r\n".getBytes()释放资源Java程序操作文件时会独占关联的文件，每次使用完流必须要释放资源。否则其他程序将不能正确访问该文件；[/scode]FileInputStream基于一个字符串格式的文件路径封装一个FileInputStream对象read()从文件中读取一个字节，值为字节的编码值read(byte[] bytes)从文件中读取一个字节数组的内容，并传入形参，返回读取到的字节数，否则为-1readAllBytes()读取文件中所有内容并转换为字节数组返回前题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=""]创建字节输入流(FileInputStream)对象，会去关联一个文件并进行一些基础的操作。如果文件不存在，就报错；读数据read():读取的内容默认得到的是该数据的编码值，读取到文件末尾则返回-1read(byte[] bytes)：读取到多个字节并存入字节数组中，返回成功读取的字节个数；读取到文件末尾返回-1在使用返回的数组时，一定要注意使用读取到的有效字节个数，否则数据有误。释放资源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还提供了很多种其他的集合，如下图所示：这些集合一共分两大类，如下图所示：一类是单列集合元素是一个一个的，另一类是双列集合元素是一对一对的。主要学习Collection单列集合。Collection是单列集合的根接口，Collection接口下面又有两个子接口List接口、Set接口，List和Set下面分别有不同的实现类，如下图所示：上图中各种集合的特点如下图所示：可以自己写代码验证一下，各种集合的特点//简单确认一下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()方法获取元素，并将当月元素指向下一个位置，等下次循环时，则获取下一个元素，依此内推最后，我们再总结一下，使用迭代器遍历集合用到哪些方法增强for遍历集合接下来介绍一种更加简化的写法，叫做增强for循环。格式如下：需要注意的是，增强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方法也可以遍历集合，如果下图所示：我们发现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)); //[赵敏, 小昭, 素素, 灭绝]遍历集合案例接下来，我们看一个案例，在集合中存储自定义的对象，并遍历。具体要求如下首先，我们得写一个电影类，用来描述每一步电影应该有哪些信息。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()); } } }以上代码的内存原理如下图所示：当往集合中存对象时，实际上存储的是对象的地址值List系列集合List集合的常用方法List集合是索引的，所以多了一些有索引操作的方法，如下图所示：接下来，我们用代码演示一下这几个方法的效果//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有索引）迭代器增强forLambda表达式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集合底层是基于数组结构实现的，也就是说当你往集合容器中存储元素时，底层本质上是往数组中存储元素。 特点如下：我们知道数组的长度是固定的，但是集合的长度是可变的，这是怎么做到的呢？原理如下：数组扩容，并不是在原数组上扩容（原数组是不可以扩容的），底层是创建一个新数组，然后把原数组中的元素全部复制到新数组中去。LinkedList底层原理LinkedList底层是链表结构，链表结构是由一个一个的节点组成，一个节点由数据值、下一个元素的地址组成。如下图所示如，现在要在B节点和D节点中间插入一个元素，只需要把B节点指向D节点的地址断掉，重新指向新的节点地址就可以了。如下图所示：假如，现在想要把D节点删除，只需要让C节点指向E节点的地址，然后把D节点指向E节点的地址断掉。此时D节点就会变成垃圾，会把垃圾回收器清理掉上面的链表是单向链表，它的方向是从头节点指向尾节点的，只能从左往右查找元素，这样查询效率比较慢；还有一种链表叫做双向链表，不光可以从做往右找，还可以从右往左找。如下图所示：LinkedList集合是基于双向链表实现了，所以相对于ArrayList新增了一些可以针对头尾进行操作的方法，如下图示所示：LinkedList集合的应用场景我们先来认识一下队列结构，队列结构你可以认为是一个上端开口，下端也开口的管子的形状。元素从上端入队列，从下端出队列。入队列可以调用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集合来模拟一下栈结构的效果。还是先来认识一下栈结构长什么样。栈结构可以看做是一个上端开头，下端闭口的水杯的形状。元素永远是上端进，也从上端出，先进入的元素会压在最底下，所以栈结构的特点是先进后出，后进先出有没有感觉栈结构很像，手枪的子弹夹呀！！第一个压进入的子弹在最底下，最后一个才能打出来，最后一个压进入的子弹在最顶上，第一个打出来。接着，我们就用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); //[]总结List集合的特点是什么？有序、可重复、有索引List集合底层是基于什么数据结构的？基于数组实现的为什么List集合查询快、增删慢？查询快：因为数组是连续的一段内存空间，当指定下标时，计算机可以算出对于元素的地址，所以查询快增删慢：因为数组是连续的一段内存空间，假如在数组中间增加删除元素，后面的所有元素全要后移或前移你能说出List集合的扩容过程吗？(四步)无参构造创建List集合，默认大小是0添加第一个元素时，底层会创建一个长度为10的数组存满时，会扩容1.5倍如果一次添加多个元素，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类的方法。这个方法是用来匹配一个字符串是否匹配正则表达式的规则，参数需要调用者传递一个正则表达式。但是正则表达式不能乱写，是有特定的规则的。下面我们就学习一下，正则表达式的规则。从哪里学呢？在API中有一个类叫做Pattern，我们可以到API文档中搜索，关于正则表达式的规则，这个类都告诉我们了。我这里把常用的已经给大家整理好了。我们将这些规则，在代码中演示一下/** * 目标：掌握正则表达式的书写规则 */ 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("您输入的邮箱格式不正确~~~"); } } } } 总结一下，正则表达式优缺点都很明显。优点是很简练，缺点是可读性和可扩展性很差，以及很难写。
认识异常我们调用一个方法时，经常一不小心就出异常了，然后在控制台打印一些异常信息。其实打印的这些异常信息，就叫做异常。异常的继承体系1.1.2演示运行时异常int[] arr = {11,22,33}; //5是一个不存在的索引，所以此时产生ArrayIndexOutOfBoundsExcpetion System.out.println(arr[5]); 下图是API中对ArrayIndexOutOfBoundsExcpetion类的继承体系，以及告诉我们它在什么情况下产生。1.1.3演示编译时异常编译时异常，它把异常的提醒提前了，你调用方法是否真的有问题，只要可能有问题就给你报出异常提示（红色波浪线）。编译时异常的目的：意思就是告诉你，你小子注意了！！，这里小心点容易出错，仔细检查一下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.尝试重新执行，看是是否能修复这个问题。我们看一个代码，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 冒泡排序(重要)接下来，我们学习一种算法叫排序算法，它可以价格无序的整数，排列成从小到大的形式（升序），或者从大到小的形式（降序）学习算法我们先要搞清楚算法的流程，然后再去“推敲“如何写代码。（注意，我这里用的次是推敲，也就是说算法这样的代码并不是一次成型的，是需要反复修改才能写好的**）。先来学习冒泡排序，先来介绍一下，冒泡排序的流程冒泡排序核心思路：每次将相邻的两个元素继续比较 如下图所示： 第一轮比较 3次 第二轮比较 2次 第三轮比较 1次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)); } } 选择排序我们先分析选择排序算法的流程：选择排序的核心思路是，每一轮选定一个固定的元素，和其他的每一个元素进行比较；经过几轮比较之后，每一个元素都能比较到了。接下来，按照选择排序的流程编写代码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索引开始一个一个往后找，但是如果元素比较多，你要查找的元素比较靠后的话，这样查找的此处就比较多。性能比较差。再讲二分查找：二分查找的主要特点是，每次查找能排除一般元素，这样效率明显提高。但是二分查找要求比较苛刻，它要求元素必须是有序的，否则不能进行二分查找。二分查找的核心思路第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./** * 目标：掌握二分查找算法。 */ 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)); } }