Java基础

目录

第一阶段

第一阶段 目标:建立编程思想

Java就业方向

  • Java EE软件工程师:电商、团购、众筹、sns、教育、金融、搜索
  • 大数据软件工程师:大数据引用工程师、大数据算法工程师、大数据分析和挖掘
  • Android软件工程师:android开发工程师

1、Java概述

写第一个Java小程序

  1. 创建txt文本,写入如下内容,并保存为Helloworld.java

    public class Helloworld {
        public static void main(String[] args) {
            System.out.println("Hello World!!!");
        }
    }
    
  2. 进入Helloworld.java的保存路径,打开cmd

  3. 在DOS窗口中输入:

    javac Helloworld.java
    
  4. 继续输入

    java Helloworld
    
  5. 显示结果

    Hello World!!!
    

1.1、Java历史

  • 1990,sun公司 启动绿色计划
  • 1991,创建 oak(橡树)语言,后发现 oak已被注册,看见咖啡上冒热气—>改名为:Java
  • 1994,gosling 参加 硅谷大会 演示 Java 功能,震惊世界
  • 1995,sun 公司正式发布 Java 1.0版本
  • 2009年,甲骨文公司(Oracle 公司)宣布收购 sun 公司
  • 2011年,甲骨文公司(Oracle 公司)发布 Java 7
  • 2014年,甲骨文公司(Oracle 公司)发布 Java 8(长期支持版)
  • JDK 8和 JDK 11是社会上最主流的版本
  • Oracle官网:https://www.oracle.com/
  • 其他JDK版本如下:
  • JavaJDK版本信息

Java技术体系平台

  • Java SE(Java Standard Edition) 标准版

    支持面向桌面级应用(Windows下的应用程序)的Java平台,提供了完整的Java核心API,此版本以前称为 J2SE。

  • Java EE(Java Enterprise Edition) 企业版

    开发企业环境下的应用程序提供的一套解决方案。该技术体系中包含:Servlet、JSP等,主要针对于Web应用开发,此版本之前为J2EE。

  • Java ME(Java Micro Edition) 小型版

    支持Java程序运行在移动端(手机、PDA)上的平台,对Java API有所精简,并加入了针对移动端的支持,此版本之前为 J2ME。

1.2、Java特点

  1. 面向对象(OOP)

  2. 健壮性。Java的强类型机制、异常处理、垃圾的自动收集等是Java程序健壮性的重要保证。

  3. 跨平台性。即:一个编译好的.class文件可以在多个系统下运行(只需要对应系统下安装对应的虚拟机),这种特性称为跨平台性。

  4. 解释型语言(JS、PHP、Java),编译型语言:C/C++

    解释型语言:编译后的代码,不能被机器执行,需要解释器来执行
    编译型语言:编译后的代码,可以直接被机器执行

1.3、Java运行机制

Java 核心机制——Java 虚拟机(JVM,Java Virtual Machine)

1)JVM 是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令、管理数据、内存、寄存器,包含在JDK中。

2)对于不同的操作系统,有不同的虚拟机。

3)Java虚拟机机制屏蔽了底层运行平台的差别,实现了“一次编译,到处运行”。

Test.java文件运行为例

Java运行机制(⭐⭐⭐⭐⭐)

Java运行机制

注:.java文件也叫源文件,.class文件也叫字节码文件。

运行的本质就是把字节码文件加载到JVM中执行。

1.4、JDK 和 JRE

JVM基本介绍:

  • JVM全称:Java Virtural Machine,Java虚拟机
  • 只认识xxx.class文件,jvm只负责将.class文件中的字节码指令进行识别并调用的操作系统向上的API完成动作。所以,JVM是Java能够跨平台的核心。即JVM能够跨平台 -> Java能跨平台。
  • 边解释边执行.class字节码文件,会把字节码文件中的指令翻译成对应操作系统的机器码。

JDK基本介绍:

  • JDK全称:Java Development Kit,Java开发工具包

    JDK = JRE + Java的开发工具(Java,javac,javadoc,javap等)

  • JDK是提供给Java开发人员用的,包含了Java的开发工具,也包括了JRE。

JRE基本介绍:

  • JRE全称:Java Runtime Environment ,Java运行时环境

    JRE = JVM + Java的核心类库(类)

  • 包括Java虚拟机(JVM)和Java程序所需的核心类库等,如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。

JDK、JRE 和 JVM 的包含关系?(⭐⭐⭐⭐)

$JDK = JRE + 开发工具集(javac,java编译工具等)$

$JRE = JVM + Java SE 标准类库(java核心类库)$

$JDK = JVM + Java SE 标准类库 + 开发工具集$

如果只想运行编译好的.class文件,只需要JRE。

JDK的安装

  1. Oracle官网下载JDK:Java Downloads | Oracle

  2. 选择对应版本的JDK安装程序

    JDK 8下载

配置环境变量

  1. 右键打开【我的电脑】—>【属性】—>【高级系统设置】—>【高级】—>【环境变量】
  2. 在【系统变量】下,新建变量JAVA_HOME,路径为JDK安装路径。如:D:\Program Files (x86)\JavaJDK8
  3. 编辑【系统变量】中的Path环境变量,【新建】添加路径:%JAVA_HOME%\bin
  4. 打开DOS命令行(cmd),任意目录下敲入:java 或者 javac,出现参数信息,配置成功

注:JDK安装路径不要有中文字符或特殊符号(如空格)等。

1.5、快速入门

使用DOS命令行编译源文件和运行Java程序,需要使源文件的编码格式和DOS命令行的代码页保持一致。否则会出现乱码问题。

如:源文件编码格式为GBK,则DOS命令行的代码页为936。

如图:编码不一致

什么是编译

即:javac Hello.java

  1. 有了Java源文件,通过编译器将其编译成JVM可以识别的字节码文件。
  2. 在该源文件目录下,通过javac.exe编译工具对Hello.java文件进行编译。本质就是将.class文件加载到 JVM 执行
  3. 如果程序没有错误,没有任何提示,则在当前目录下会出现一个Hello.class文件,该文件称为字节码文件,也是可以执行的Java程序

DOS窗口执行java命令

注:对于修改后的Java源文件,需要使用javac命令重新编译和运行才能使修改部分生效。

一个源文件中最多只能有一个public 类,其他类的个数不限。可以将main方法写在非 public 类中,然后指定运行非 public 类,这样入口方法就是非 public 的 main方法。

如果源文件包含一个 public 类,则文件名必须以该类名命名。

常见错误

  1. 找不到文件

    解决方法:源文件名不存在或者写错,或者当前路径错误

  2. 类XXX是公共的,请在文件中说明

    解决方法:声明为 public 的主类应与文件名一致,否则编译失败

  3. 编译失败

    解决方法:注意错误出现的行数,再到源代码中指定位置改错

  4. 无法映射字符

    解决方法:修改源文件的编码格式为GBK或者将DOS窗口的编码格式改为UTF-8

1.6、转义字符

\t:一个制表位,实现对齐的功能

\n:换行符

\\:一个\

\":一个

\’:一个

\r:一个回车

1.7、Java开发规范

  1. 类、方法的注释,要以javadoc的方式来写。
  2. 非Java Doc的注释(单行注释和多行注释),往往是给代码的维护者看的,着重告诉读者为什么这样写,如何修改,注意什么问题等。
  3. 使用Tab操作,实现缩进,默认整体向右边移动,使用shift + tab整体向左移。
  4. 运算符和 = 两边加一个空格,增加代码可读性。
  5. 源文件使用UTF-8编码。
  6. 行宽度不要超过80个字符。
  7. 代码编写的两种风格:次行风格行尾风格(推荐)。

1.8、Java API 文档

JDK 8在线官方文档:Java Platform Standard Edition 8 Documentation (oracle.com)

1.9、Java注释

用于注释解释程序的文字就是注释,注释提高了代码的可读性;注释是一个程序猿必须要具有的良好编程习惯,将自己的思想通过注释写出来,再用代码实现。Java中的注释类型:

  • 单行注释(快捷键:Ctrl + /)

    // 单行注释
    
  • 多行注释

    /**
     * 多行注释
     * 多行注释
     */
    
  • 文档注释

    /** 文档注释
     * @author Yxz
     * @version 1.0
     * @Description TODO
     * @date 2021-07-30 16:39
     */
    

1.10、DOS命令

Dos介绍:Disk Operating System 磁盘操作系统

相对路径:从当前目录开始定位,形成的一个路径。

绝对路径:从顶级目录C盘或D盘等开始定位,形成的路径。如:D:\Program

常用Dos命令(⭐⭐⭐⭐):

  • 查看当前目录的所有内容:dir,如:dir d:\java
  • 切换到其他盘:cd 盘符号,如:切换到C盘cd /D c:
  • 切换到当前盘的其他目录下:使用cd 相对路径(或绝对路径)。如:cd d:\java\JavaApplication
  • 切换到上一级:cd ..,如:cd ..\
  • 切换到根目录:cd \
  • 查看指定文件夹的所有子级目录:tree 路径。如:tree d:\java
  • 清屏操作:cls
  • 退出Dos:exit
  • 创建目录:md。如:在当前文件夹下创建test100文件夹test200文件夹,md test100 test200
  • 删除目录:rd。如:删除当前文件夹下的test100目录:rd test100
  • 拷贝文件:copy。如:将文件test2.txt复制到D盘Java文件目录下并且命名为temp.txt,copy test2.txt d:java\temp.txt
  • 删除文件:del,如:删除文件test2.txt,del test2.txt
  • 输入内容到文件:echo。将内容输入到文件里,如:echo hello > test.txt,将hello输入到test.txt文件里。
  • 输入空文件:type。可以将空内容输入到文件里,如:type nul > test.txt,test.txt文件里为空。
  • 剪切:move。将文件移动到某个文件目录下,move test.txt ..\test2\ok.txt,将文件test.txt移动到父目录下的test2文件下并命名为ok.txt

1.11、如何学习新技术

  1. 需求:工作需要、跳槽(对方要求)、技术控
  2. 看看能否使用传统技术解决
  3. 引出我们学习的新技术和知识点
  4. 学习新技术或者知识点的基本原理和基本语法(不要考虑细节)
  5. 快速入门:能写出基本程序,实现CRUD(增删改查)
  6. 开始研究技术的注意事项、使用细节、使用规范、如何优化 ——> 学无止境,技术的魅力

2、变量

2.1、数据类型

基本数据类型

  • 数值型

    • 整数类型:byte[1],short[2],int[4],long[8]——用于存放整数值
    • 浮点(小数)类型:float[4],double[8]——用于存放小数
  • 字符型:char[2]——存放单个字符

    java中char的本质是一个整数,输出时是对应unicode码对应的字符。char类型可以进行运算。

    存储时:‘a’ ——> 码值97 ——> 二进制 ——> 存储

    读取时:二进制 ——> 97 ——> ‘a’ ——> 显示

  • 布尔型boolean[1],存放truefalse

引用数据类型

  • 类class
  • 接口interface
  • 数组[]

常见字符编码表

  • ASCII码1个字节表示,一共表示128个字符,实际上一个字节可以表示256个字符,ASCII码只占用了一个字节的后面七位,最前面一位统一规定为0)

  • Unicode码固定大小编码,使用2个字节表示字符,字母和汉字统一两个字节存储,浪费存储空间.使用Unicode没有乱码问题,可以表示65536个字符,编码0-127的字符与ASCII码一样,Unicode码兼容ASCII码)

  • UTF-8码(改进的Unicode码,大小可变的编码,字母使用1个字节,汉字使用3个字节。可以使用1-6个字节表示符号,根据不同符号变化字节长度)

  • GBK(可以表示汉字,范围广,字母使用1个字节,汉字使用2个字节)

  • GB2312(可以表示汉字,GB2312 < GBK)

  • BIG5码(存储繁体中文)

  • 布尔型:boolean[1],存放 truefalse 。一般用于流程控制。

 public class PrimitiveTypeTest {
     public static void main(String[] args) {
         // byte
         System.out.println("基本类型:byte 二进制位数:" + Byte.SIZE);
         System.out.println("包装类:java.lang.Byte");
         System.out.println("最小值:Byte.MIN_VALUE = " + Byte.MIN_VALUE);
         System.out.println("最大值:Byte.MAX_VALUE = " + Byte.MAX_VALUE);
         System.out.println();
 
         // short
         System.out.println("基本类型:short 二进制位数:" + Short.SIZE);
         System.out.println("包装类:java.lang.Short");
         System.out.println("最小值:Short.MIN_VALUE = " + Short.MIN_VALUE);
         System.out.println("最大值:Short.MAX_VALUE = " + Short.MAX_VALUE);
         System.out.println();
 
         // int
         System.out.println("基本类型:int 二进制位数:" + Integer.SIZE);
         System.out.println("包装类:java.lang.Integer");
         System.out.println("最小值:Integer.MIN_VALUE = " + Integer.MIN_VALUE);
         System.out.println("最大值:Integer.MAX_VALUE = " + Integer.MAX_VALUE);
         System.out.println();
 
         // long
         System.out.println("基本类型:long 二进制位数:" + Long.SIZE);
         System.out.println("包装类:java.lang.Long");
         System.out.println("最小值:Long.MIN_VALUE = " + Long.MIN_VALUE);
         System.out.println("最大值:Long.MAX_VALUE = " + Long.MAX_VALUE);
         System.out.println();
 
         // float
         System.out.println("基本类型:float 二进制位数:" + Float.SIZE);
         System.out.println("包装类:java.lang.Float");
         System.out.println("最小值:Float.MIN_VALUE = " + Float.MIN_VALUE);
         System.out.println("最大值:Float.MAX_VALUE = " + Float.MAX_VALUE);
         System.out.println();
 
         // double
         System.out.println("基本类型:double 二进制位数:" + Double.SIZE);
         System.out.println("包装类:java.lang.Double");
         System.out.println("最小值:Double.MIN_VALUE = " + Double.MIN_VALUE);
         System.out.println("最大值:Double.MAX_VALUE = " + Double.MAX_VALUE);
         System.out.println();
 
         // char -> 单一的 16 位 Unicode 字符
         System.out.println("基本类型:char 二进制位数:" + Character.SIZE);
         System.out.println("包装类:java.lang.Character");
         // 以数值形式而不是字符形式将Character.MIN_VALUE输出到控制台
         System.out.println("最小值:Character.MIN_VALUE = "
                 + (int) Character.MIN_VALUE);
         // 以数值形式而不是字符形式将Character.MAX_VALUE输出到控制台
         System.out.println("最大值:Character.MAX_VALUE = "
                 + (int) Character.MAX_VALUE);
     }
 }
 /* 运行结果:
 基本类型:byte 二进制位数:8
 包装类:java.lang.Byte
 最小值:Byte.MIN_VALUE = -128
 最大值:Byte.MAX_VALUE = 127
 
 基本类型:short 二进制位数:16
 包装类:java.lang.Short
 最小值:Short.MIN_VALUE = -32768
 最大值:Short.MAX_VALUE = 32767
 
 基本类型:int 二进制位数:32
 包装类:java.lang.Integer
 最小值:Integer.MIN_VALUE = -2147483648
 最大值:Integer.MAX_VALUE = 2147483647
 
 基本类型:long 二进制位数:64
 包装类:java.lang.Long
 最小值:Long.MIN_VALUE = -9223372036854775808
 最大值:Long.MAX_VALUE = 9223372036854775807
 
 基本类型:float 二进制位数:32
 包装类:java.lang.Float
 最小值:Float.MIN_VALUE = 1.4E-45
 最大值:Float.MAX_VALUE = 3.4028235E38
 
 基本类型:double 二进制位数:64
 包装类:java.lang.Double
 最小值:Double.MIN_VALUE = 4.9E-324
 最大值:Double.MAX_VALUE = 1.7976931348623157E308
 
 基本类型:char 二进制位数:16
 包装类:java.lang.Character
 最小值:Character.MIN_VALUE = 0
 最大值:Character.MAX_VALUE = 65535
 */

注意

  1. Java各整数类型有固定的范围和字段长度,不受具体OS【操作系统】的影响,以保证Java程序的可移植性。
  2. Java整型常量默认为int型,声明long类型常量须后加lL
  3. Java程序中变量常声明为int型,除非int型范围不够才使用long
  4. bit:计算机中的最小存储单位。
  5. byte(Byte):计算机中基本存储单元。
  6. 1 Byte = 8 bit
  7. 浮点数在机器中存放形式:浮点数 = 符号位 + 指数位 + 尾数位。
  8. 尾数部分可能丢失,造成精度损失(小数都是近似值)。
  9. Java浮点型表示的范围和字段长度不受OS的影响(float 4个字节,double 8个字节)。
  10. Java的浮点型常量默认为double型,声明float型常量须后加fF
  11. 浮点数常量有两种表示形式。如:5.1210.1.131(=0.131)
  12. 通常情况下,应该使用double型,因为它比float型精度更高。
  13. 不要对运算后的小数进行相等判断(小数运算后会出现精度丢失)

2.2、变量基本使用

变量是程序的基本组成单位。变量的三个基本要素:类型 + 名称 + 值。

public class Var01 {
    public static void main(String[] args) {
        int a; // 声明一个整型变量a
        a = 100; // 给变量a赋值
        System.out.println(a);
        
        int b = 800; // 声明一个变量b并初始化赋值800
        System.out.println(b);
    }
}

注意:

  1. 变量表示内存中的一个存储区域(不同的变量,类型不同,占用的空间大小不同)。
  2. 该区域有自己的名称(变量名)和类型(数据类型)。
  3. 变量必须先声明,后使用。
  4. 该区域的数据可以在同一类型范围内不断变化。
  5. 变量在同一个作用域内不能重名。
  6. 变量 $=$ 数据类型 $+$ 变量名 $+$ 变量值。

2.3、数据类型转换

自动类型转换

当Java程序在进行赋值或者运算时,精度小(容量小)的类型自动转换为精度大(容量大)的数据类型。

数据类型按精度(容量)大小排序:

char ——> int ——> long ——> float ——> double

byte ——> short ——> int ——> long ——> float ——> double

  1. 有多种类型的数据混合运算时,系统先自动将所有数据转成容量最大的那种数据类型,然后再进行计算。
  2. 将具体数值赋给变量前,须先判断是否超过变量类型的存储范围。
  3. 如果是变量类型赋值,会直接判断类型是否超出
  4. byte、short、char之间不会相互自动转换
  5. byte、short、char三者可以计算,在计算时首先转换成int型(与是否混合运算无关)。
  6. boolean不参与转换。
  7. 表达式结果的类型自动提升为操作数中最大的类型
public class AutoConvert {
    public static void main(String[] args) {
        int i = 1;	// 正确
        
        int b1 = 10;
        float b2 = b1 + 1.1;	// 报错1:1.1默认为double型,num1+1.1为double型,不能兼容float
        // 改进方法1:float b2 = b1 + 1.1F;
        // 改进方法2:double b2 = b1 + 1.1;
        
        byte c1 = 128;	// 错误2:超出byte表示范围[-128, 127]
        
        int d1 = 1;
        byte d2 = d1;	// 报错3:变量类型赋值,会直接判断类型是否超出接受类型的大小。不兼容的类型
        
        byte e1 = 10;
        char e2 = e1;	// 报错4:byte、short、char之间不会相互自动转换,byte不能自动转换成char
        
        byte b = 1;
        short s1 = 1;
        short s2 = b + s1;	// 报错5:b + s1 => 结果为int型,short不能兼容int型
        // 改进方法:short s2 = b + s1;
        byte b3 = 2;
        byte b4 = b + b3;	// 报错5:有byte参与运算,结果自动提升为int型,byte不能兼容
        
        boolean pass = true;
        int i2 = pass;	// 报错6:boolean不参与类型的自动转换
        
        byte b5 = 1;
        short s3 = 100;
        int num200 = 2;
        double num300 = 1.1;
        int res = b5 + s3 + num200 + num300;	// 报错7:表达式中最高精度为double型num300,结果为double型,int不能兼容
        // 改进方法1;double res = b5 + s3 + num200 + num300;
        // 改进方法2;float res = b5 + s3 + num200 + 1.1F
        // 改进方法3;double res = b5 + s3 + num200 + 1.1F;
    }
}

强制类型转换

自动类型转换的逆过程。将容量大的数据类型转换为容量小的数据类型。使用时要加上强制转换符(),但有可能造成精度降低或溢出

注意:

  1. 数据的从大——>小,用强转。
  2. 强转符号只针对于最近的操作数有效,往往会使用小括号提升优先级。
  3. char类型可以保存int型的常量值,但不能保存int型的变量,需要强转。
public class ForceCOnvert {
    public static void main(String[] args) {
        int n1 = (int)1.9;
        SYstem.out.println("n1 = " + n1);	// 精度损失
        
        int n2 = 2000;
        byte b1 = (byte)n2;
        SYstem.out.println("b1 = " + b1);	// 数据溢出:2000超出byte范围[-128, 127]
        
        int x = (int)10 * 3.5 + 6 * 1.5;	// 编译错误2:double ——> int不兼容,精度丢失
        // 强转符号只针对于最近的操作数有效,往往会使用小括号提升优先级
        SYstem.out.println(x);
        // 改进方法:int x = (int)(10 * 3.5 + 6 * 1.5);
        
        char c1 = 100;	// 正确,char能保存int型常量值
        int m = 100;	// 正确
        char c2 = m;	// 错误3:char不能保存int型的变量
        char c3 = (char)m;	// 正确,char保存int型变量需要强转
        System.out.println(c3);	// 输出(100对应的字符)d
    }
}

基本数据类型和String类型的转换

  1. 基本类型转String类型

    语法: 将基本类型的值+“”即可

public class StringToBasic {
    public static void main(String[] args) {
        int n1 = 100;
        float n2 = 1.1f;
        double n3 = 3.14;
        boolean b1 = true;
        String str1 = n1 + "";
        String str2 = n2 + "";
        String str3 = n3 + "";
        String str4 = b1 + "";
        System.out.println(str1 + " " + str2 + " " + str3 + " " + str4);
    }
}
  1. String类型转基本数据类型

    语法:通过基本类型的包装类调用parseXXX方法即可

    注意:

    1. 在将String类型转换成基本数据类型时,要确保String类型能够转成有效的数据。
    2. 如果格式不正确,就会抛出异常,程序就会终止。
    public class BasicToString {
        public static void main(String[] args) {
            String s5 = "123";
            int num1 = Integer.parseInt(s5);
            double num2 = Double.parseDouble(s5);
            float num3 = Float.parseFloat(s5);
            long num4 = Long.parseLong(s5);
            byte num5 = Byte.parseByte(s5);
            boolean b = Boolean.parseBoolean("true");
            short num6 = Short.parseShort(s5);
            
            // 将String类型转换成char类型,在字符串中将指定位置的字符传换成char
            char c = s5.charAt(0);
            
            String str = "hello";
            int n1 = Integer.parseInt(str);	// 抛出异常
            System.out.println(n1);	
        }
    }
    

3、运算符

3.1、运算符介绍

运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等。

3.2、算术运算符

算数运算符(ArithmeticOperator)是对数值类型的变量进行运算。

运算符 运算 范例 结果
$+$ 正号 +7 7
$-$ 负号 b = 11; -b -11
$+$ 9 + 9 18
$-$ 10 - 5 5
$*$ 7 * 8 56
$/$ 9 / 4 2
% 取模(取余) 10 % 3 1
++ 自增(前) a = 2; b = ++a; a = 3; b = 3
++ 自增(后) a = 2; b = a++; a = 3; b = 2
-- 自减(前) a= 2; b = --a; a = 1; b = 1
-- 自减(后) a= 2; b = a--; a = 1; b = 2
+ 字符串连接 “study ” + “java” “study java”

注意

  1. 除法结果类型为操作数的最高精度。

  2. 取模(%)本质:a % b = a - a / b * b

  3. a % b当a是小数时,公式:a % b = a - (int)a / b * b

  4. 有小数的运算,运算的结果是近似值。

  5. Java语言中 + 的使用:

    1. 当左右两边都是数值型时,则作加法运算
    2. 当左右两边有一方为字符串,则作拼接运算
    3. 运算顺序:从左至右
public class ArithmeticOperator {
    public static void main(String[] args){
        System.out.println(10 / 4);	// 2
        System.out.println(10.0 / 4);	// 2.5
        
        // 取模(取余)
        System.out.println(10 % 3);	// 1 = 10 - 10 / 3 * 3
        System.out.println(-10 % 3);	// -1 = (-10) - (-10) / 3 * 3
        System.out.println(10 % -3);	// 1 = 10 - 10 / (-3) * (-3)
        System.out.println(-10 % -3);	// -1 = (-10) - (-10) / (-3) * (-3)
        System.out.println(-10.5 % 3);	// -1.5 = -10.5 - (int)(-10.5) / 3 * 3, 结果为-1.5的近似值
        System.out.println(-10.4 % 3);	// -1.4 = -10.4 - (int)(-10.4) / 3 * 3, 结果为-1.4的近似值
    }
}

3.3、关系运算符

关系运算符(RelationalOperator)的结果都是boolean型,要么是true,要么是false

运算符 运算 范例 结果
$==$ 相等于 $8 == 7$ false
$!=$ 不等于 $8 != 7$ true
$<$ 小于 $8 < 7$ false
$>$ 大于 $8 > 7$ true
$<=$ 小于等于 $8 <= 7$ false
$>=$ 大于等于 $8 >= 7$ true
instance of 检查是否是类的对象 “yxz” instance of String true
public class RelationalOperator {
    public static void main(String[] args) {
        int a = 9;
        int b = 8;
        System.out.println(a > b);
        System.out.println(a < b);
        System.out.println(a >= b);
        System.out.println(a <= b);
        System.out.println(a == b);
        System.out.println(a != b);
    }
}

3.4、逻辑运算符

逻辑运算符(LogicalOperator)用于连接多个条件(多个关系表达式),最终的结果也是一个boolean值。

  1. 短路与&&,短路或||,取反!
  2. 逻辑与&,逻辑或|,逻辑异或^

| a | b | a&b | a && b | a | b | a || b | !a | a ^ b |
| :-----: | :-----: | :-----: | :------: | :-----: | :------: | :-----: | :-----: |
| true | true | true | true | true | true | false | false |
| true | false | false | false | true | true | false | true |
| false | true | false | true | true | true | true | true |
| false | false | false | false | false | false | true | false |

&&&的区别

  1. 短路与&&:如果第一个条件为false,则后面的条件都不会判断,最终结果为false
  2. 逻辑与&:不管第一个条件是否为false,后面的条件都要判断,效率低。
  3. 开发中,使用的基本是短路与&&

|||的区别

  1. 短路与||:如果第一个条件为true,则后面的条件都不会判断,最终结果为true
  2. 逻辑与|:不管第一个条件是否为true,后面的条件都要判断,效率低。
  3. 开发中,使用的基本是短路与||
public class LogicalOperator {
    public static void main(String[] args) {
        // &&短路与
        int age = 50;
        if(age > 20 && age < 90) {
            System.out.println("ok100");
        }
        
        // &逻辑与
        if(age > 20 & age < 40) {
            System.out.println("ok200");
        }
        
        //区别
        int a = 4;
        int b = 9;
        if(a < 1 && ++b < 50) {
            System.out.println("ok300");
        }
        System.out.println("a = " + a + "b = " + b);	// 输出a = 4,b = 9
        
        if(a < 1 & ++b < 50) {
            System.out.println("ok300");
        }
        System.out.println("a = " + a + "b = " + b);	// 输出a = 4,b = 10
    }
}

3.5、赋值运算符

赋值运算符(AssignOperator)就是将某个运算后的值,赋给指定的变量。

分类

  • 基本赋值运算符=
  • 复合赋值运算符+=-=*=/=%=等。先运算后赋值

特点

  1. 运算顺序从右往左
  2. 赋值运算符的左边只能是变量,右边可以是变量、表达式、常量值。
  3. 复合赋值运算符会进行类型转换
public class AssignOperator {
    public static void main(String[] args) {
        int n1 = 10;
        n1 += 4;	// 等价于:n1 = n1 + 4;
        System.out.println(n1);	// 14
        n1 /= 3;	// 等价于:n1 = n1 / 3;
        System.out.println(n1);	// 4
        
        // 复合赋值运算符会进行强制类型转换
        byte b = 3;
        b += 2;	// 等价于:b = (byte)(b + 2);
        b++;	// 等价于:b = (byte)(b + 1)
    }
}

3.6、三元运算符

三元运算符(TernaryOperator)基本语法:条件表达式 ? 表达式1 : 表达式2;

如果条件表达式为true,运算后的结果是表达式1;如果条件表达式为false,运算后的结果是表达式2.

表达式1和表达式2要为可以赋给接受变量的类型(或可以自动转换)。

三元运算符可以转成if-else语句

public class TernaryOperator {
    public static void main(String[] args) {
        int a = 30;
        int b = 50;
        int result = a > b ? a++ : b--;
        System.out.println(result)	// 50
    }
}

3.7、运算符的优先级

表达式运算中的运算顺序。如下表,上一行运算符总优先于下一行。

只有单目运算符、赋值运算符是从右向左运算的。

. () {} ; ,
$R->L$ ++ -- ~ !
$L->R$ * / %
$L->R$ + -
$L->R$ << >> >>> 位移
$L->R$ < > <= >= instanceof
$L->R$ == !=
$L->R$ &
$L->R$ ^
$L->R$ `
$L->R$ &&
$L->R$ `
$L->R$ ? :
$R->L$ = *= /= %=
+= -= <<= >>=
>>>= &= ^= `

标识符的命名规则和规范

  1. Java中对各种变量、方法和类等命名时使用的字符序列成为标识符。
  2. 凡是可以自己起名字的地方都叫标识符。如:int num1 = 90;
  3. 命名规则(必须遵守):
    1. 由``26个英文字母大小写0-9_$`组成。
    2. 数字不可以开头。
    3. 不可以使用关键字和保留字,但能包含关键字和保留字。
    4. Java中严格区分大小写,长度无限制。
    5. 标识符不能含有空格。

标识符的规范

  1. 包名:多单词组成时所有字母都小写。如:com.yxz.dao
  2. 类名、接口名:多单词组成时,所有单词的首字母大写(大驼峰)。如:OperatorTest
  3. 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写(小驼峰,驼峰法)。如:setNameBean
  4. 常量名:所有字母都大写。多单词时每个单词之间使用下划线连接。如:LENGTH_MAXSIZE
  5. [Java开发手册(嵩山版).pdf](file:D:/学习文件/Java开发手册(嵩山版).pdf)

关键字和保留字

  1. 关键字:被Java语言赋予了特殊含义,用作专门用途的字符串

    特点:关键字中所有字母都为小写。

    关键字1

    关键字2

  2. 保留字:现有Java版本尚未使用,但以后版本可能会作为关键字使用。命名标识符时要避免使用。

    如:byValuecastfuturegenericinneroperatorouterrestvargotoconst

键盘输入

在编程中。需要接收用户输入的数据,可以使用键盘输入语句获取。需要一个扫描器(对象),就是Scanner类

步骤:

  1. 导入该类的所在包,java.util.*
  2. 创建该类的对象(声明变量)
  3. 调用该类的成员方法
import java.util.Scanner;

public class Input {
    public static void main(String[] args) {
        // 1.导入Scanner类所在包
        
        // 2.创建Scanner对象,使用关键字 new 创建一个对象
        Scanner scanner = new Scanner(System.in);
        
        // 3.接收用户输入,调用类的相关方法
        System.out.println("请输入名字:");
        String name = scanner.next();	// 等待接收键盘输入
        System.out.println("请输入年龄:");
        int age = scanner.nextInt();	// 等待接收键盘输入
        System.out.println("请输入月薪:");
        double salary = scanner.nextDouble();	// 等待接收键盘输入
        
        System.out.println(name + "的个人信息");
        System.out.println("年龄:" + age + "\t月薪:" + salary);
    }
}

3.8、进制及转换

对于整数,有四种表示方式:

  1. 二进制:0,1。满2进1,以0b或0B开头。
  2. 十进制:0-9,满10进1。
  3. 八进制:0-7,满8进1,以数字0开头表示。
  4. 十六进制:0-9及A(10)-F(15),满16进1,以0x或0X开头表示。A-F不区分大小写。
public class BinaryTest {
    public static void main(String[] args) {
        // 二进制
        int n1 = 0b1010;	// 10
        //十进制
        int n2 = 1010;	// 1010
        // 八进制
        int n3 = 01010;	// 520
        // 十六进制
        int n4 = 0x10101;	// 65793
    }
}

进制转换

  1. 二进制转十进制

    规则:从最低位(右边)开始,将每个位上的数提取出来,乘以2的(位数-1)次方,然后求和。

    如:$0b1011$ ——> $120+1*21+022+1*23 = 11$

  2. 八进制转十进制

    规则:从最低位(右边)开始,将每个位上的数提取出来,乘以8的(位数-1)次方,然后求和。

    如:$0234$ ——> $480+3*81+28^2 = 156$

  3. 十六进制转十进制

    规则:从最低位(右边)开始,将每个位上的数提取出来,乘以16的(位数-1)次方,然后求和。

    如:$0X23A$ ——> $10160+3*161+216^2 = 570$

  4. 十进制转二进制

    规则:将该数不断除以2,直到商为0为止,然后将每步得到的余数倒过来,就是对应的二进制。

    如:34 ——> 0b100010 ——> 不足一个字节(8位), 高位补0 ——> 0b00100010

    十进制转二进制

  5. 十进制转八进制

    规则:将该数不断除以8,直到商为0为止,然后将每步得到的余数倒过来,就是对应的八进制。

    如:131 ——> 0203

  6. 十进制转十六进制

    规则:将该数不断除以16,直到商为0为止,然后将每步得到的余数倒过来,就是对应的八进制。

    如:237 ——> 0xED

  7. 二进制转八进制

    规则:从低位开始,将二进制数每三位一组,转换成对应的八进制数即可。

    如:0b11010101 ——> 0b11(3)010(2)101(5) ——> 0325

  8. 二进制转十六进制

    规则:从低位开始,将二进制数每四位一组,转换成对应的十六进制数即可。

    如:0b11010101 ——> 0b1101(D)0101(5) ——> 0XD5

  9. 八进制转二进制

    规则:将八进制数每1位,转成对应的一个3位的二进制数即可。

    如:0237 ——> 0(0b)2(010)3(011)7(111) ——> 0b010011111

  10. 十六进制转二进制

    规则:将八进制数每1位,转成对应的一个4位的二进制数即可。

    如:0x23B ——> 0x2(0010)3(0011)B(1011) ——> 001000111011

3.9、位运算符

Java中的位运算符

符号名称 位运算符 范例 结果
按位与 & 2 & 3 2
按位或 ` ` `2
按位取反 ~ ~2 -3
按位异或 ^ 2 ^ 3 1
算数左移 << 8 << 2 32
算数右移 >> 4 >> 2 1
无符号右移(逻辑右移) >>> 16 >>> 2 4

运算规则:

  1. 算术右移>>:低位溢出,符号位不变,并用符号位补溢出的高位。
  2. 算数左移<<:符号位不变,低位补0。
  3. 逻辑右移>>>:也叫无符号右移。低位溢出,高位补0。

原码、反码、补码

对于有符号数:

  1. 二进制的最高位是符号位:0表示正数,1表示负数。
  2. 正数的原码、反码、补码都一样(正数三码合一)
  3. 负数的反码 $=$ 它的原码符号位不变 $+$ 其他位按位取反(0->1, 1->0)
  4. 负数的补码 $=$ 它的反码$+$1,负数的反码 $=$ 负数的补码 $-$ 1
  5. 0的反码、补码都是0。
  6. Java中没有无符号数
  7. 在计算机运算的时候都是以补码的方式运算的
  8. 看运算结果时,要看它的原码
public class BitOperator {
    public static void main(String[] args) {
        // 计算 2 & 3 = ?
        // 1. 2的原码:00000000 00000000 00000000 00000010
        //    2的补码:00000000 00000000 00000000 00000010(正数三码合一)
        // 2. 3的原码:00000000 00000000 00000000 00000011
        //    3的补码:00000000 00000000 00000000 00000011(正数三码合一)
        // 3. 2的补码 & 3的补码
        // 00000000 00000000 00000000 00000010(2的补码)
        // 00000000 00000000 00000000 00000011(3的补码)
        // 00000000 00000000 00000000 00000010 是运算后的补码
        // 4. 运算后的原码:00000000 00000000 00000000 00000010(正数三码合一)
        System.out.println(2 & 3);	// 2 & 3 == 2
        
        // 计算 ~-2
        // 1. -2的原码:10000000 00000000 00000000 00000010(+2的原码基s础上,符号位改为1表示负数)
        // 2. -2的反码:11111111 11111111 11111111 11111101(原码基础上, 符号位不变, 其他位按位取反)
        // 3. -2的补码:11111111 11111111 11111111 11111110(反码基础上, +1)
        // 4. ~-2操作:00000000 00000000 00000000 00000001 是运算后的补码
        // 5. 运算后的原码: 00000000 00000000 00000000 00000001(正数三码合一)
        System.out.println(~-2);	// ~-2 == 1
        
        // 计算 ~2
        // 1. 2的补码:00000000 00000000 00000000 00000010(正数三码合一)
        // 2. ~2操作 :11111111 11111111 11111111 11111101 是运算后的补码
        // 3. 运算后的反码:11111111 11111111 11111111 11111100(运算后的补码基础上, -1)
        // 4. 运算后的原码:10000000 00000000 00000000 00000011(运算后的反码基础上, 符号位不变, 其他位按位取反)
        System.out.println(~-2);	// ~2 == -3
    }
}

4、控制结构

4.1、顺序控制

程序从上到下逐行地执行,中间没有任何判断和跳转。

Java中定义成员变量时采用合法的前向引用(先定义后使用)。

4.2、分支控制

  1. 单分支if

    基本语法:

    if (条件表达式) {
    	代码块;(可以多条语句)
    }
    

    说明:当条件表达式为true时,就会执行{}内的代码。如果为false,就不执行。如果{}中只有一条语句,可以不写{},建议写上{}

  2. 双分支if-else

    基本语法:

    if (条件表达式) {
    	代码块1;(可以多条语句)
    } else {
    	代码块2;(可以多条语句)
    }
    

    说明:当条件表达式为true时,就会执行代码块1,否则执行代码块2

  3. 多分支if-else if

    基本语法:

    if (条件表达式1) {
    	代码块1;(可以多条语句)
    } else if (条件表达式2) {
    	代码块2;(可以多条语句)
    } else if (条件表达式3) {
    	代码块3;(可以多条语句)
    }
    ......
      else {
        代码块n;(可以多条语句)
    }
    

    说明:当条件表达式1成立,则执行代码块1,如果不成立,判断条件表达式2,成立则执行代码块2,否则判断条件表达式3...,都不成立则执行else内的代码块。只能有一个执行入口。多分支可以没有else,如果所有条件表达式都不成立,则一个执行入口都没有。

  4. 选择分支switch-case

    基本语法:

    switch (表达式) {
                case 常量1:
                    语句块1;
            		break;
                case 常量2:
                    语句块2;
            		break;
            	......
                case 常量n:
            		语句块n;
            		break;
                default:
                    语句块;
            		break;
            }
    

    说明:表达式对应一个值,当表达式的值等于常量1,就执行语句块1,break表示退出switch,不再继续匹配。如果一个都没匹配上,执行default语句块

    注意:

    1. 表达式数据类型,应和case后的常量类型一致,或者是可以自动转成可以相互比较的类型。
    2. switch(表达式)中表达式的返回值必须是:(byte、short、int、char、enum、String)
    3. case子句中的值必须是常量或者是常量表达式,不能是变量。
    4. default子句是可选的,当没有匹配的case时,执行default。
    5. break语句用来在执行完一个case分支后使程序跳出switch语句块;如果没有break,程序会顺序执行到switch结尾,除非遇到break。

4.3、循环控制

让一段代码可以循环重复的执行。

  1. for循环

    基本语法:

    for (循环变量初始化; 循环条件; 循环变量迭代) {
        循环体;(可以是多条语句)
    }
    

    说明:

    1. for关键字,表示循环控制。
    2. for有四要素:(1)循环变量初始化(2)循环条件(3)循环操作(4)循环变量迭代。
    3. 循环条件是返回一个boolean值的表达式。
    4. for(; 循环判断条件; )中的初始化和变量迭代可以写到其他地方,但是两边的;不能省略。
    5. 循环初始值可以有多条初始化语句,但是要求类型一样,并且中间用,隔开,循环变量迭代也可以有多条变量迭代语句,中间用,隔开。
    public class ForCircle {
        public static void main(String[] args) {
            int count = 3;
            for (int i = 0, j = 0; i < count; i++, j += 2) {
                System.out.println("i = " + i + "j = " + j);
            }
    
            int sum = 0;
            int i = 1;
            for (; i <= 100; ) {
                sum += i++;
            }
            System.out.println("sum = " + sum);
    
            for(; ; ) {
                // 死循环,配合break语句使用
            }
        }
    }
    

    练习:打印1~100之间所有9的倍数的整数,统计个数及总和。(思想:化繁为简、先死后活)

    import java.util.Scanner;
    
    public class ForExercise {
        public static void main(String[] args) {
            // 化繁为简:即将复杂的需求,拆解成简单的需求,逐步完成。
            // 先死后活:先考虑固定的值,然后转成可以灵活变化的值。
            
            // 化繁为简
            // 1. 完成 输出1-100的值
            // 2. 输出过程中,进行过滤,只输出9的倍数 i % 9 == 0
            // 3. 统计个数,定义一个变量 int count = 0; 当满足条件时,count++
            // 4. 求和,定义一个变量 int sum = 0; 当满足条件时累加 sum += i
            
            int count = 0;
            for (int i = 1; i <= 100; i++) {
                if(i % 9 == 0) {
                    System.out.println("i = " + i);
                    count++;
                    sum += i;
                }
            }
            System.out.println("sum = " + sum + ", count = " + count);
            
            // 先死后活
            // 1. 将边界值设定为变量,输出[start, end]范围内的9的倍数
            // 2. 将9的倍数改成变量x的倍数
            // 3. 变量start、end、x使用Scanner来接收键盘输入
            Scanner scanner = new Scanner(System.in);
            int start = scanner.nextInt();
            int end = scanner.nextInt();
            int x = scanner.nextInt();
            int countX = 0;
            int sumX = 0;
            
            for (int i = start; i <= end; i++) {
                if(i % x == 0) {
                    System.out.println("i = " + i);
                    countX++;
                    sumX += i;
                }
            }
            System.out.println("sumX = " + sumX + ", countX = " + countX);
        }
    }
    
  2. while循环

    基本语法:

    循环变量初始化;
    while (循环条件) {
    	循环体;(可以是多条语句)
        循环变量迭代;
    }
    

    说明:while循环也有四要素,只是四要素放的位置和for循环不一样。

    练习:打印1-100之间所有能被3整除的数。

    public class WhileExercise {
        public static void main(String[] args) {
            int i = 1;
            int endNum = 100;
            while (i <= endNum) {
                if (i % 3 == 0) {
                    System.out.println("i = " + i);
                }
                i++;
            }
        }
    }
    
  3. do-while循环

    基本语法:

    循环变量初始化;
    do {
        循环体;
        循环变量迭代;
    } while (循环条件);
    

    说明:

    1. do while是关键字。
    2. do while也有循环四要素,只是位置不一样。
    3. 先执行一次,再判断是否循环。(至少会执行一次循环体)
    4. 最后有一个;
  4. 嵌套循环(多层循环)

    将一个循环放在另一个循环体内,形成嵌套循环。(建议一般使用两层,最多不超过三层,否则代码可读性很差)。

    只有内层循环的循环条件为false时,才会完全跳出内循环,并结束外层循环的当次循环,开始外层循环的下一次循环。

    设外层循环次数m次,内层循环n次,则内层循环体执行$n*m$次。

    练习:打印1-100中的所有素数。

    public class MultiplyFor {
        public static void main(String[] args) {
            // 外循环,遍历2-100(1既不是素数,也不是合数)
            for(int j = 2; j <= 100; j++) {
                int i = 2;
                boolean isPrime = true;
                // 内循环,从2开始,循环条件i * i < j,找到j是否有除1和本身的其他因数
                for(; i * i <= j; i++) {
                    if(j % i == 0) {
                        isPrime = false;
                        break;
                    }
                }
                if(isPrime) {
                    System.out.println(j);
                }
            }
        }
    }
    

4.4、break

break语句用于终止某个语句块的执行,一般用在switch或者循环中。

注意:

  1. break语句出现在多层嵌套的语句块中,可以通过标签指明要终止的是哪一层语句块。

  2. 标签的基本使用:

    label1: 
    {......循环体中:
    	label2:
     	{......循环体中:
            label3:
         	{......循环体中:
                break label2;
             ....
            }
        }
    }
    
    1. break语句可以指定退出哪层。
    2. label1是标签,由程序员指定。
    3. break后指定到哪个label就退出哪层。
    4. 在实际的开发中,尽量不要使用标签。
    5. 如果没有指定break,默认退出最近的循环体。
    public class Label {
        public static void main(String[] args) {
            abc1:
            for (int i = 0; i < 10; i++) {
                label2:
                for (int j = 0; j < 10; j++) {
                    if (j == 2) {
                        break abc1;
                    }
                }
            }
        }
    }
    

4.5、continue

continue语句用于结束本次循环,继续执行下一次循环。

continue语句出现在多层嵌套的循环语句体中时,可以通过标签指明要跳过的是哪一层循环,和break使用方法一样。

基本语法:

{...循环体中:
	continue;
...}

4.6、return

return使用在方法体里面,表示跳出所在的方法。如果return写在main()方法里面,退出程序。

public class Test {
    public Test(){
    }
    
    public static void main(String[] args) {
        Test test = new Test();
        int sum = test.getSum(100);
        System.out.println(sum);
    }
    
    public int getSum(int n) {
        int sum = 0;
        for(int i = 1; i < n; i++) {
            sum += i;
        }
        return sum;
    }
}

5、数组、排序和查找

5.1、数组

数组(Array)可以存放多个同一类型的数据。数组也是一种数据类型,属于引用类型。

基本语法:

// 静态初始化
数据类型[] 数组名 = {元素1, 元素2, 元素3,..., 元素n};	// 数组大小为n
int[] array01 = {1, 2, 3, 4, 5};

// 第一种动态分配方式
数据类型[] 数组名 = new 数据类型[数组大小];// java方式声明数组,建议使用
数据类型 数组名[] = new 数据类型[数组大小];// C语言方式声明数组,可以使用
double[] array02 = new double[10];

// 第二种动态分配方式,先声明,再new分配空间
数据类型[] 数组名;	// 先声明(内存未给数组分配内存空间)
数组名 = new 数据类型[数组大小];	// 后实例化(给数组分配内存空间)
int[] a;
a = new int[10];

说明:

  1. 访问数组元素方法:数组名[下标]。下标(索引,index)从0开始编号,如:array[0]表示数组第一个元素,array[1]表示第二个元素,依此类推。
  2. 可以通过for就可以循环访问数组的元素/值。遍历数组就是指循环访问每一个数组元素。
  3. 数组名.length可以获取数组的大小/长度。数组长度在第一次实例化时就已经固定不能再改变
  4. 数组是多个相同类型数据的组合,实现对同种数据的统一管理。
  5. 数组中的元素可以是任何数据类型,包括基本类型和引用类型,但不能混用。
  6. 数组创建后,如果没有赋值,有默认值。(int 0, short 0, byte 0, long 0, float 0.0, double 0.0, char \u0000, boolean false, String null)
  7. 使用数组的步骤:1.声明数组并开辟空间 2.给数组各个元素赋值 3.使用数组。
  8. 数组下标(索引)必须在指定范围内使用,否则抛出异常:下标越界异常。
  9. 数组型数据是对象(object)。

数组赋值机制

  1. 基本数据类型赋值,这个值就是具体的数据,而且相互不影响。

  2. 数组赋值(ArrayAssign)在默认情况下是引用传递,赋的值是地址。

    public class ArrayAssign {
        public static void main(String[] args) {
            // 基本数据类型赋值 -> 值传递
            int n1 = 10;
            int n2 = n1;
            n1 = 80;	// n1 = 80, n2 = 10 修改n1的值,不影响n2的值
    		
            // 数组类型赋值,默认是引用传递,赋的值是地址 -> 地址传递(引用传递)
            int[] arr1 = {1, 2, 3};
            int[] arr2 = arr1;
            arr2[0] = 10;	// 修改arr2的元素会影响到arr1
            for (int i = 0; i < arr1.length; i++) {
                System.out.println(arr1[i] + " ");
            }
        }
    }
    

    JVM内存图解(⭐⭐⭐⭐⭐)

    值传递和地址传递区别

二维数组

语法:

数据类型[][] 数组名 = new 数据类型[数组大小1][数组大小2];
// 数组大小1:表示有几个一维数组
// 数组大小2:表示每个一维数组的大小

一维数组中每个元素都是单独一个一维数组,就构成了二维数组。

其他数组的属性、操作类比一维数组即可。

注:

  1. 二维数组的声明方式有

    数据类型[][] 数组名 = new 数据类型[数组大小1][数组大小2];
    数据类型 数组名[][] = new 数据类型[数组大小1][数组大小2];
    数据类型[] 数组名[] = new 数据类型[数组大小1][数组大小2];
    
  2. 二维数组的列数可以不等,即数组大小1$≠$数组大小2。如int[][] map = {{1, 2}, {3, 4, 5}};

5.2、排序

排序是将一群数据,依指定的顺序进行排列的过程。

排序分类:

  1. 内部排序

    指将需要处理的所有数据都加载到内部存储器中进行排序。包括:交换式排序法、选择式排序法、插入式排序法。

  2. 外部排序

    数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。包括:合并排序法、直接合并排序法。

冒泡排序(BubbleSort)

基本思想:通过对 待排序的序列从后向前(从下标较大的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前往后移

public class BubbleSort {
    public static void main(String[] args) {
        // 化繁为简
        int[] a = {4, 2, 5, 3, 1};
        
        // 第一轮,目标:将第1大数放在倒数第1的位置上
        System.out.print("第一轮:\n");
        for(int j = 0; j < 4; j++) {
            if(a[j] > a[j + 1]) {
                int temp = a[j + 1];
                a[j + 1] = a[j];
                a[j] = temp;
            }
        }
        // 输出排序后的数组
        for (int i = 0; i < 5; i++) {
            System.out.print(a[i] + "\t");
        }

        // 第二轮,目标:将第2大数放在倒数第2的位置上
        System.out.print("\n第二轮:\n");
        for(int j = 0; j < 3; j++) {
            if(a[j] > a[j + 1]) {
                int temp = a[j + 1];
                a[j + 1] = a[j];
                a[j] = temp;
            }
        }
        // 输出排序后的数组
        for (int i = 0; i < 5; i++) {
            System.out.print(a[i] + "\t");
        }

        // 第三轮,目标:将第3大数放在倒数第3的位置上
        System.out.print("\n第三轮:\n");
        for(int j = 0; j < 2; j++) {
            if(a[j] > a[j + 1]) {
                int temp = a[j + 1];
                a[j + 1] = a[j];
                a[j] = temp;
            }
        }
        // 输出排序后的数组
        for (int i = 0; i < 5; i++) {
            System.out.print(a[i] + "\t");
        }

        // 第四轮,目标:将第4大数放在倒数第4的位置上
        System.out.print("\n第四轮:\n");
        for(int j = 0; j < 1; j++) {
            if(a[j] > a[j + 1]) {
                int temp = a[j + 1];
                a[j + 1] = a[j];
                a[j] = temp;
            }
        }
        // 输出排序后的数组
        for (int i = 0; i < 5; i++) {
            System.out.print(a[i] + "\t");
        }
        System.out.println("\n===============");

        // 外层嵌套一层循环
        for(int i = 0; i < 4; i++) {
            System.out.print("第" + (i + 1) + "轮\n");
            for(int j = 0; j < 4 - i; j++) {
                if(a[j] > a[j + 1]) {
                    int temp = a[j + 1];
                    a[j + 1] = a[j];
                    a[j] = temp;
                }
            }
            // 输出排序后的数组
            for (int j = 0; j < 5; j++) {
                System.out.print(a[j] + "\t");
            }
            System.out.print("\n");
        }

        int[] arr = {10, 4, 3, 6, 8, 2, 7, 1, 0, 5, 9};
        // 先死后活,将数组长度改成arr.length
        for(int i = 0; i < arr.length - 1; i++) {
            for(int j = 0; j < arr.length - 1 - i; j++) {
                if(arr[j] > arr[j + 1]) {
                    int temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                }
            }
            // 输出排序后的数组
            for (int j = 0; j < arr.length; j++) {
                System.out.print(arr[j] + "\t");
            }
        }
    }
}

5.3、查找

Java中常用查找方法:

  1. 顺序查找(SequentialSearch)

    import java.util.Scanner;
    
    public class SeqSearch {
        public static void main(String[] args) {
            String[] names = {"张三", "李四", "王五", "老六"};
            Scanner scanner = new Scanner(System.in);
            
            System.out.print("请输入名字:");
            String findName = scanner.next();
            
            // 遍历数组,逐一比较
            int index = -1;
            for (int i = 0; i < names.length; i++) {
                // 比较字符串 equals(String str)
                if (findName.equals(names[i])) {
                    System.out.println("恭喜你找到 " + findName);
                    System.out.println("下标为:" + i);
                    index = i;	// 保存下标
                    break;
                }
            }
            
            // 未找到则提示信息
            if (index == -1) {
                 System.out.println("sorry, 查无此人!");
            }
        }
    }
    
  2. 二分查找(数据结构与算法中介绍)

6、面向对象编程(基础)

6.1、类与对象

类(class)是抽象的,概念的,代表一类事物,如:人类、猫类。即 类 ——> 数据类型

对象(object)是具体的,实际的,代表一个具体事物。即对象 ——> 实例

类是对象的模板,对象是类的一个个体,对应一个实例。

类包含属性(成员变量、字段、field)和行为(成员方法、类方法)。

属性(property)是类的一个组成部分,一般是基本数据类型,也可以是引用类型(对象、数组)

注意:

  1. 属性的定义语法和变量一致:访问修饰符 属性类型 属性名;,如:public String name;

    Java一共有四种控制属性的访问范围,即以下四种访问修饰符:public、protected、默认、private

  2. 属性的定义类型可以为任意类型,包含基本类型或引用类型。

  3. 属性如果不赋初值,有默认值,规则和数组一致。

JVM中对象的存储形式(⭐⭐⭐⭐⭐)

JVM的内存对象存在形式

如何创建对象

  1. 先声明,再创建

    基本语法:

    类名 对象名;	// 声明对象名,此时对象为空,没有创建对象空间
    对象名 = new 类名();	// 创建对象(实例化)。new 会将创建的对象空间的地址返回赋给对象引用
    
    Cat cat;	// 声明对象名,此时cat为空,没有创建对象空间
    cat = new Cat();	// 创建对象(实例化)。new 会将创建的对象空间的地址返回赋给cat
    
  2. 直接创建

    基本语法:

    类名 对象名 = new 类名();	// 声明对象名,同时创建对象空间并返回地址赋给对象名
    
    Cat cat = new Cat(); //	类名Cat 对象引用(对象名)cat = 对象new Cat()
    

访问属性

基本语法:

对象名.属性名;

cat.age;

Java类和对象的内存分配机制

  1. 栈:一般存放基本数据类型(局部变量)

  2. 堆:存放对象(自定义类、数组等)

  3. 方法区:常量池(常量,比如字符串等),类加载信息(只加载一次)

    public class Person {
        // 类属性:姓名
        String name;
        
        // 类属性:年龄
        int age;
        
        // 类属性:薪水
        double salary;
        
        // 类属性:通过考试
        boolean isPass;
        
        // 默认构造方法
        public Person() {
            
        }
        
        // main()方法
        public static void main(String[] args) {
            Person p = new Person();	// p是对象引用(对象名), new Person()创建的对象空间(包括数据)才是真正的对象
            p.name = "Yancy";
            p.age = 22;
            p.salary = 10000.0;
            p.isPass = false;
            /*
            1、先加载Person类信息(属性和方法信息,只加载一次)
            2、在堆中分配空间,进行默认初始化
            3、把对象在堆中地址返回给p
            4、进行指定初始化,如:p.name = "Yancy"
            */
        }
    }
    
    

6.2、成员方法

成员方法是对象的行为。

基本语法:

访问修饰符 方法返回值类型 方法名(形参列表) {
    方法体;
    return 返回值;
}

说明:

  1. 访问修饰符:控制方法的使用范围。
  2. 方法返回值类型:表示成员方法的输出,void表示没有返回值。返回值类型可以为任意类型(基本类型、引用类型)。
  3. 方法名:遵循驼峰命名法,见名知意。
  4. 形参列表:表示成员方法的输入。用于传入参数,有值传递地址传递。参数类型可以为任意类型(基本类型、引用类型)。
  5. 方法体:实现某一功能的代码块。语句可以为输入、输出、变量、运算、分支、循环、方法调用,但是里面不能再定义方法。即方法不能嵌套定义
  6. return用于返回值,一个方法最多有一个返回值,返回值必须和返回值类型一致或兼容。如果是void,可以只写return;
  7. 一个方法可以有0个参数,也可以有多个参数,中间用,隔开。
  8. 调用带参数的方法时,一定对应着参数列表传入相同类型或兼容类型的参数
  9. 方法定义时的参数称为形式参数,简称形参;方法调用时的参数称为实际参数,简称实参。实参和形参的类型要一致或兼容,个数和顺序必须一致!
  10. 同一个类中的方法调用:直接调用即可,无需借助对象名。
  11. 跨类中的方法调用:需要通过对象名调用跨类方法调用和方法的访问修饰符有关

注:当程序执行到方法时,就会开辟一个独立的空间(栈空间)

成员方法的好处:

  1. 提高代码的复用性。
  2. 可以将实现的细节封装起来,然后供其他用户来调用即可。

示例1:

public class Method01 {
    String name;

    // speak 成员方法
    // public 表示方法公开
    // void : 表示方法没有返回值
    // speak() : speak是方法名,()形参列表
    // {} 方法体 :执行代码块
    public void speak() {
        System.out.println("我是一个好人");
    }
}

示例2:

public class Method01 {
    String name;

    public static void main(String[] args) {
        Method01 method01 = new Method01();
        method01.speak();
        method01.cal01();
        int returnRes = method01.cal02(100);
        System.out.println("0~100的计算结果 = " + returnRes);
        
        int[] res = method01.getSumAndSub(5, 3);
        System.out.println("和 = " + res[0] + "差 = " + res[1]);
    }

    // speak 成员方法
    // public 表示方法公开
    // void : 表示方法没有返回值
    // speak() : speak是方法名,()形参列表
    // {} 方法体 :执行代码块
    public void speak() {
        System.out.println("我是一个好人");
        B b = new B();
        b.hi();	// 调用跨类方法需要借助对象名
    }

    public void cal01() {
        int res = 0;
        for (int i = 0; i <= 1000; i++){
            res += i;
        }
        System.out.println("0~1000的计算结果 = " + res);
    }

    public int cal02(int n) {
        speak();	// 同类方法可以直接调用,无需借助对象名
        int res = 0;
        for (int i = 0; i <= n; i++){
            res += i;
        }
        return res;
    }
    
    public int[] getSumAndSub(int a, int b) {
        int[] res = new int[2];
        res[0] = a + b;
        res[1] = a - b;
        return res;
    }
}

class B {
    public void hi() {
        System.out.println("B类中的hi()被执行");
    }
}

6.3、方法传参机制

参考代码

public class Person {
    public static void main(String[] args) {
        Person p = new Person();
        int res = p.getSum(10, 20);
        System.out.println("getSum()的结果 = " + res);
    }

    public int getSum(int num1, int num2) {
        int res = num1 + num2;
        return res;
    }
}

传参机制图解(⭐⭐⭐⭐⭐)

成员方法的调用机制

  1. 对于基本数据类型,参数传递的是值(值拷贝、值传递),形参在方法体中的的任何改变不影响实参。实参传入到方法的参数列表后,内存空间会给方法开辟新的栈空间,此时会将传递的实参的值拷贝给方法体的形参
  2. 对于引用数据类型,参数传递的是地址(地址传递),可以通过改变方法体中的形参而影响实参
public class MethodParameter {
    public static void main(String[] args) {
        MethodParameter methodParameter = new MethodParameter();
        int a = 10;
        int b = 20;
        methodParameter.swap(a, b);	// 基本数据类型,传参机制 ——> 值拷贝(值传递)
        System.out.println("a = " + a + "b = " + b);	// a = 10, b = 20
        
        B obj = new B();
        int[] arr = {1, 211, 6};
        obj.updateValue(arr);	// 引用数据类型,传参机制 ——> 地址传递(引用传递)
        // 遍历数组
        System.out.println("main()方法的arr数组");
        for(int i : arr) {	// 加强for循环,遍历数组的常用操作。使用i接收数组arr中每个元素
            System.out.print(i + "\t");	// 输出:985 211 6
        }
        System.out.println();
    }
    
    public void swap(int a, int b) {
        int temp = a;
        a = b;
        b = temp;
    }
}

class B {
    public void updateValue(int[] arr) {
        arr[0] = 985;	// 修改元素
        // 遍历数组
        System.out.println("B类 update方法的arr数组");
        for(int i : arr) {	// 加强for循环,遍历数组的常用操作。使用i接收数组arr中每个元素
            System.out.print(i + "\t");	// 输出:985 211 6
        }
        System.out.println();
    }
}

克隆对象

通过地址传递的方法,克隆对象。

示例:

public class Person {
    String name;
    int age;
    
    public static void main(String[] args) {
        Person p = new Person();
        p.name = "milan";
        p.age = 22;
        MyTools tools = new MyTools();
        Person p2 = tools.copyPerson(p);	// p2的属性和p的属性相同,但是两个对象不等
        System.out.println("p的名字:" + p.name + ", p的年龄:" + p.age);
        System.out.println("p2的名字:" + p2.name + ", p2的年龄:" + p2.age);
        
        // 检验两个对象是否相等(属性相等 + 对象空间地址相等)
        // 方法一:==判断
        if(p == p2) {
            System.out.println("p和p2相等");
        } else {
            System.out.println("p和p2不等");
        }
        
        // 方法二:使用object自带的equals()方法判断
        if(p.equals(p2)) {
            System.out.println("p和p2相等");
        } else {
            System.out.println("p和p2不等");
        }
    }
}

class MyTools {
    public Person copyPerson(Person p) {
        Person p2 = new Person();
        p2.name = p.name;
        p2.age = p.age;
        return p2;
    }
}

6.4、方法递归调用

递归(recursion)就是方法自己调用自己,每次调用时传入不同的变量。递归有助于编程解决复杂问题,可以让代码更简洁。

递归能解决什么问题?

  1. 各种数学问题:8皇后问题,汉诺塔,阶乘(factorial)问题,迷宫问题,球和篮子的问题 等等。
  2. 各种算法中用到递归:快速排序,归并排序,二分查找,分治算法 等等。
  3. 用栈解决的问题 ——> 递归代码更简洁。

示例:

public class Recursion {
    T t = new T();
    t.test(4);	// 输出:n = 2 n = 3 n = 4
    int res = t.factorial(5);
    System.out.println("res = " + res);	// 输出:res = 120
}

class T {
    public void test(int n) {
        if (n > 2) {
            test(n - 1);
        }
        System.out.println("n = " + n);
    }
    
    // 计算n的阶乘n!
    public int factorial(int n) {
        if(n == 1) {
            return 1;
        } else {
            return factorial(n - 1) * n;
        }
    }
}

递归执行机制1

递归执行机制2

递归重要规则

  1. 执行一个方法时,就创建一个新的独立空间(栈空间)。
  2. 方法的局部变量是独立的,不会相互影响。
  3. 如果方法中使用的是引用类型变量,就会共享该引用类型的数据。
  4. 递归三要素:递归函数的参数和返回值、递归终止条件、递归的等价关系式。递归必须向退出递归的条件逼近,否则会无限递归,抛出异常(StackOverFlowError, 栈溢出)。
  5. 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁。当方法执行完毕或返回时,方法占用的内存空间会被收回或释放。

迷宫问题(递归解决)

已知老鼠在地图最右上角,地图上有若干障碍物,现在需要找到一条路径使得老鼠能够走到地图右下角。规定老鼠每次只能走一格。

进阶思考:一共有多少条不同的路径数?最短路径?迷宫足够大时,递归爆栈,算法如何优化?

迷宫地图

public class MiGong {
    public static void main(String[] args) {
        // 1. 先创建迷宫,用二维数组表示 int[][] map = new int[8][7];
        int[][] map = new int[8][7];

        // 2. 规定map数组的元素值: 0表示可以走,1表示障碍物
        // 3. 最上面的一行和最下面的一行设置为1
        for (int i = 0; i < 7; i++) {
            map[0][i] = 1;
            map[7][i] = 1;
        }

        // 4.最左边的一列和最右边的一列设置为1
        for (int i = 0; i < 8; i++) {
            map[i][0] = 1;
            map[i][6] = 1;
        }

        // 5. 障碍物的位置设为1
        map[3][1] = 1;
        map[3][2] = 1;
        map[2][2] = 1;

        // 输出当前地图
        System.out.println("==========当前地图情况===========");
        for (int[] value : map) {
            for (int i : value) {
                System.out.print(i + " ");
            }
            System.out.println();
        }

        // 使用findWay找路
        Solution solution = new Solution();
        solution.findWay(map, 1, 1);    // 传入老鼠的初始位置

        System.out.println("\n=======找路的情况如下=========");
        for (int[] ints : map) {
            for (int anInt : ints) {
                System.out.print(anInt + " ");
            }
            System.out.println();
        }
    }
}

class Solution {
    // 1. findWay()方法找到出迷宫的路径
    // 2. 如果找到,返回true,否则返回false
    // 3. 二维数组map表示迷宫
    // 4. i, j 就是老鼠的位置,初始化位置为(1, 1)
    // 5. 递归找路,规定map数组各个值的含义:
    //    0表示可以走,1表示障碍物,2表示已走路线,3表示走过但是走不通
    // 6. 当map[6][5] = 2,说明找到通路就可以退出,否则继续找
    // 7. 先确定老鼠找路的策略:下 -> 右 -> 上 -> 左(策略不唯一 ——> 找到的路径不唯一)
    public boolean findWay(int[][] map, int i, int j) {
        if (map[6][5] == 2) {    // 说明已经找到,返回true
            return true;
        } else {
            if (map[i][j] == 0) {    // 当前位置为0,表示可以走
                // 假定当前位置可以走通
                map[i][j] = 2;
                // 使用找路策略,来确定当前位置可以走通:下 -> 右 -> 上 -> 左
                if (findWay(map, i + 1, j)) {    // 先走下
                    return true;
                } else if (findWay(map, i, j + 1)) { // 右
                    return true;
                } else if (findWay(map, i - 1, j)) { // 上
                    return true;
                } else if (findWay(map, i, j - 1)) { // 左
                    return true;
                } else {
                    map[i][j] = 3;    // 表示当前位置走过但是走不通
                    return false;
                }
            } else {    // map[i][j] = 1, 2, 3
                return false;    // 其他情况,返回false
            }
        }
    }
}

汉诺塔问题

有三根柱子,在一个柱子上从下往上按照从大到小顺序摞着n个圆盘。现在要把这n个圆盘按照大小顺序重新摆放在另一根柱子上,并且规定在小圆盘上不能放大圆盘,且三根柱子之间一次只能移动一格圆盘。

public class HanoiTower {
    public static void main(String[] args) {
        Tower solve = new Tower();
        solve.move(5, 'A', 'B', 'C');
    }
}

class Tower {
    // move()方法表示移动圆盘的操作
    // num: 要移动的圆盘个数
    // a, b, c: 表示三根柱子A塔,B塔,C塔
    public void move(int num, char a, char b, char c) {
        // 如果只有一个盘num = 1
        if (num == 1) {
            System.out.println(a + "->" + c);
        } else {
            // 如果有多个盘,可以看成两块,最下面的一块盘和上面的所有盘(num - 1)
            // 1. 先移动上面所有的盘到b, 借助c
            move(num - 1, a, c, b);
            // 2. 把最下面的盘移动到c
            System.out.println(a + "->" + c);
            // 3. 再把b塔的所有盘移动到c,借助a
            move(num - 1, b, a, c);
        }
        
    }
}

八皇后问题(回溯算法的典型)

在 $8×8$ 的国际象棋上摆放八个皇后,使其不能互相攻击。即:任意两个皇后都不能处于同一行、同一列或同一斜线上,共有多少种摆法?

思路分析:

  1. 第一个皇后先放在第一行第一列。

  2. 第二个皇后放在第二行第一列,然后判断是否可行,如果不可行,继续放在第二列、第三列,依次把所有列都放完直到找到一个合适的方案。

  3. 继续第三个皇后,以此类推,直到第八个皇后也能放在合理的位置,则找到一个正确解。

  4. 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放在第一列的所有正确解,全部得到。

  5. 然后回头继续第一个皇后放第二列,继续循环1-4的步骤

    注:理论上要创建二维数组表示棋盘,实际可以通过算法,用一个一维数组解决。arr[8] = {0, 4, 7, 5, 2, 6, 1, 3}对应arr下标表示第几行,即第几个皇后,arr[i] = val表示第i + 1个皇后,放在第i + 1行的第val + 1

6.5、方法重载

Java中允许同一个类中,多个同名方法的存在,但要求形参列表不一致,这种现象称为方法重载(overload)

如:System.out.println(); outPrintStream类型的对象

方法重载注意:

  1. 方法名必须相同。
  2. 参数列表必须不同(1.形参类型不同 2.形参个数不同 3.形参顺序不同 ——> 至少一样不同),与参数名无关。
  3. 与方法返回类型无关。
  4. 当传入的实参与多个重载方法匹配时(出现自动类型转换),优先调用无自动类型转换的方法
public class OverLoad {
    public static void main(String[] args) {
        MyCalculator mc = new MyCalculator();
        System.out.println(mc.calculate(1, 2));	// 优先调用无需转换类型的方法calculate(int, int)
        System.out.println(mc.calculate(1, 3.14));
        System.out.println(mc.calculate(5.2, 0));
    }
}

class MyCalculator {
    // 下面四种不同calculate()方法构成了方法的重载
    public int calculate(int n1, int n2) {
        return n1 + n2;
    }
    
    public double calculate(int n1, double n2) {
        return n1 + n2;
    }
    
    public double calculate(double n1, int n2) {
        return n1 + n2;
    }
    
    public int calculate(int n1, int n2, int n3) {
        return n1 + n2 + n3;
    }
}

6.6、可变参数

Java允许将同一个类多个同名同功能参数个数不同的方法,封装成一个方法。可以通过可变参数(Variable parameters)实现。

基本语法:

访问修饰符 返回类型 方法名(数据类型...形参名) {
    方法体;
}

案例:类Method,方法sum(可以计算 2个数的和,3个数的和,4.5。。。)。

public class VarParameter {
    public static void main(String[] args) {
        Method method = new Method();
        int res = method.sum("variable parameter", 1, 2, 3, 4, 5, 6);
        System.out.println(res);
        
        int[] arr = {985, 211, 520};
        int result = method.sum("传递数组", arr);
        System.out.println("传递arr数组求和 = " + result);
        
        System.out.println(method.calScore("Mi Lan", 99.0, 92, 87, 95.5, 90));
    }
}

class Method {
    // 可以使用方法重载实现
    /*
    public int sum(int n1, int n2) {
        return n1 + n2;
    }
    
    public int sum(int n1, int n2, int n3) {
        return n1 + n2 + n3;
    }
    
    public int sum(int n1, int n2, int n3, int n4) {
        return n1 + n2 + n3 + n4;
    }
    */
    // ......
    // 以上三个方法名相同,功能相同,形参个数不同 -> 使用可变参数优化
    // int...: 表示接收的是可变参数,类型是int,即可以接收多个int(0——>n),本质上是个数组
    // 遍历nums求和
    public int sum(String str, int... nums) {
        System.out.println("接收的参数个数 = " + nums.length);
        int res = 0;
        for (int i = 0; i < nums.length; i++) {
            res += nums[i];
        }
        return res;
    }
    
    public String calScore(String name, double... scores) {
        double totalScore = 0;
        for (int i = 0; i < scores.length; i++) {
            totalScore += scores[i];
        }
        return name + "有" + scores.length + "门课的成绩总分为" + totalScore;
    }
}

注意:

  1. 可变参数的实参可以为0个或任意多个。
  2. 可变参数的实参可以为数组。
  3. 可变参数的本质就是可变长度的数组。
  4. 可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数在形参列表最后。
  5. 一个形参列表中只能出现一个可变参数。

6.7、作用域

Java中主要变量就是属性(成员变量)和局部变量。

局部变量一般是指在成员方法中定义的变量。

Java中作用域的分类:

  1. 全局变量:也就是属性,作用域为整个整体。可以被本类使用,或被其它类通过对象调用。
  2. 局部变量:也就是除了属性之外的其他变量,作用域为从定义它的语句开始到代码块执行结束。只能在本类中对应的方法中使用。

全局变量可以不赋值,直接使用,因为有默认值。局部基本数据类型变量(没有默认值)必须赋值后才能使用

注意:

  1. 属性和局部变量可以重名,访问时遵循就近原则(在方法中与属性同名的局部变量会暂时覆盖属性,要访问属性需要借助this或者对象名)。

  2. 在同一个作用域中,变量不能重名。如在同一个成员方法中,两个局部变量不能重名。

  3. 属性生命周期较长,伴随着对象的创建而创建,伴随着对象的死亡而死亡。局部变量生命周期较短,伴随着它的定义语句执行而创建,伴随着代码块的结束而死亡。

  4. 全局变量可以加修饰符,局部变量不可以加修饰符

    public class VarScope {
        String name; // 默认值为null
        public int age;	// 属性可以加修饰符
        
        public static void main(String[] args) {
            VarScope varscope = new VarScope();
            varscope.hi();
            
        }	// 对象varscope伴随着程序终止而被销毁
        
        public void hi() {
            int num;	// 此时num没有值,直接输出会报未初始化赋值的错误
           	// int num;	// 同一个作用域中,变量不能重名
            num = 10;
            System.out.println(num);
            
            String name = "Mi Lan";	// 与属性同名,在这个方法体内,局部变量name覆盖属性name
            System.out.println(name);
            System.out.println(this.name);	// 通过this关键字可以访问该类属性name
            
            int res = 0;	// 局部变量不能加修饰符
            for (int i = 0; i < 100; i++) {	// 局部变量 i 的作用域伴随着for循环结束而终止。
                res += i;
            }// 此处退出循环后,局部变量i立即销毁
        }
    }
    

6.8、构造器

构造器(构造方法, constructor)是类的一种特殊方法,它的主要作用是完成对新对象的初始化。在创建对象时,系统会自动的调用该类的构造器完成对该对象的初始化。

基本语法:

[修饰符] 方法名(形参列表) {
    方法体;
}

说明:

  1. 构造器的修饰符可以默认,也可以为publicprivateprotected
  2. 构造器没有返回值类型,也不能写void
  3. 构造方法的方法名和类名必须一样。
  4. 参数列表和其他成员方法一样的规则。
  5. 构造器的调用由系统完成。
  6. 如果不定义构造器,系统会给类创建一个默认无参构造器。一旦定义了有参构造器,就会覆盖默认无参构造器,无法再使用无参构造,除非再显式定义一个无参构造器
  7. 一个类可以定义多个不同的构造器,即构造器重载。
  8. 构造器的任务是完成对象属性的初始化,而不是创建对象
public class Constructor {
    public static void main(String[] args) {
        Person p1 = new Person("Mi Lan", 22);
        System.out.println("p1对象name: " + p1.name + ", 芳龄: " + p1.age);
    }
}

class Person {
    String name;
    int age;
    
    // 构造器
    public Person(String pName, int pAge) {
        name = pName;
        age = pAge;
    }
}

class Dog {
    String name;
    int age;
    /*默认构造器,如果未定义有参构造器,则系统自动创建默认构造器。可以使用javap反编译显示出来
    Dog() {
    
    }
    */
    Dog(String dogName, int dogAge) {	// 有参构造器,会覆盖掉默认构造器(此时无法调用无参构造器)
        
    }
    // 显式定义无参构造器
    Dog() {
        
    }
}

反编译工具

使用javap指令能对给定的.class文件提供的字节码进行反编译成.java文件。

通过javap指令,可以对照源代码和字节码,从而了解很多编译器内部的工作,对更深入的理解如何提高程序执行的效率等问题有很大帮助。

基本语法:

javap <options> <classes>

常用:javap -c -v 类名

javap常用指令 含义
-help --help -? 输出此用法消息
-version 版本消息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类和成员(默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的系统信息(路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置

创建对象的流程分析

案例代码

class Person {
    int age = 18;
    String name;
    
    Person(String pName, int pAge) {	// 构造器
        name = pName;	// 给属性赋值
        age = pAge;
    }
    
    public static void main(String[] args) {
        Person p = new Person("Mi Lan", 22);
    }
}

流程分析(面试题)

  1. 加载Person类信息(Person.class),只会加载一次。
  2. 使用new给对象在堆中分配空间。
  3. 完成对象初始化
    1. 默认初始化。给对象的属性根据数据类型赋默认值。如:name = null,age = 0;
    2. 显式初始化。类属性是否初始化。如上面代码中int age = 18;
    3. 构造器的初始化。将构造器的实参赋给对象属性。如上面代码中new Person("Mi Lan", 22);
  4. 将对象在堆中的地址返回给对象引用(对象名)。如上面代码中Person p = new Person("Mi Lan", 22);

6.9、this

Java虚拟机(JVM)会给每个对象分配this代表当前对象。可以理解成,当new给对象在堆中分配空间时,会自动给该对象分配一个“隐藏的属性”——this,这个this类似C语言中的指针,只是这个“指针”永远指向包含该“指针”的对象。

简单来说:哪个对象调用,this就代表哪个对象。

public class UseThis {
    public static void main(String[] args) {
        Dog dog1 = new Dog("大壮", 3);
        System.out.println("dog1的hashCode = " + dog1.hashCode());// dog1的hashCode和构造器内部打印this的hashCode一致
        dog1.printDogInfo();	// 打印的hashCode和dog1的hashCode一致
        Dog dog2 = new Dog("大黄", 2);
        System.out.println("dog2的hashCode = " + dog2.hashCode());// dog2的hashCode和构造器内部打印this的hashCode一致
        dog1.printDogInfo();	// 打印的hashCode和dog2的hashCode一致
        // 结论:this指代当前对象
    }
}

class Dog {
    public String name;
    public int age;
    
    public Dog(String name, int age) {
        // this.name 就是当前对象的属性name
        this.name = name;
        // this.age 就是当前对象的属性age
        this.age = age;
        // hashCode():返回该对象的哈希码值
        // hashCode会根据在内存中不同对象的地址映射成不同的整数
        System.out.println("this.hashCode = " + this.hashCode());
    }
    
    public void printDogInfo() {// 打印狗的信息
        System.out.println(name + "\t" + age + "\t");
        System.out.println("Info方法中this.hashCode = " + this.hashCode());
    }
}

this关键字的本质(⭐⭐⭐⭐)

this本质

注意:

  1. this关键字可以用来访问本类的属性、方法、构造器。
  2. this用于区分当前类的属性和局部变量。
  3. 访问成员方法的语法:this.方法名(参数列表);
  4. 访问构造器的语法:this(参数列表);注意只能在构造器中使用,即只能在构造器中使用this调用另外一个构造器。
  5. this不能在类定义的外部使用,只能在类定义的方法中使用。
  6. 如果要在构造器中使用this访问另外一个构造器,则 this(参数列表); 这个语句必须是构造器中的第一个语句
class ThisDetail {
    public static void main(String[] args) {
    	T t1 = new T("张三", 30);
        t1.f2();
        
        T t2 = new T();
        t2.f3();
    }
}

class T {
    public String name = "smith";
    public int age = 18;
    
    public T() {// 默认构造器
        // 在默认构造器中访问有参构造器
        this("Mi Cai", 23); // 使用this访问另一个构造器,必须是构造器中的第一个语句
        System.out.println("T()构造器");
    }
    
    public T(String name, int age) {// 有参构造器
        this.name = name;
        this.age = age;
        System.out.println("T(String name, int age)构造器");
    }
    
    
    public void f1() {
        System.out.println("f1()方法...");
    }
    
    public void f2() {
        System.out.println("f2()方法...");
        // 调用本类的f1方法
        // 第一种方式
        f1();
        // 第二种方式,与第一种方式有区别(继承介绍)
        this.f1();
    }
    
    public void f3() {
        String name = "Mi Lan";
        // 就近原则,name指代局部变量name,age指代属性age
        System.out.println("name = " + name + ", age = " + age);	// Mi Lan, 18
        // 使用this访问当前对象的属性
        System.out.println("name = " + this.name + ", age = " + this.age);	// smith, 18
    }
}

6.10、实战运用

人机猜拳游戏

import java.util.Random;
import java.util.Scanner;

/*
人机猜拳游戏
电脑每次随机生成0, 1, 2
0 表示石头 1表示剪刀 2表示布
并统计人的输赢次数
 */
public class MoraGame {
    public static void main(String[] args) {
        System.out.println("猜拳游戏\n0 表示石头 1表示剪刀 2表示布");
        // 创建一个玩家对象
        Player player = new Player();
        // 声明一个变量统计输赢的次数
        int isWinCount = 0;

        // 创建一个二维数组,接收局数,玩家出拳以及电脑出拳的情况
        int[][] arr1 = new int[3][3];
        int j = 0;

        // 创建一个一维数组,用来接收输赢的情况
        String[] arr2 = new String[3];

        Scanner scanner = new Scanner(System.in);
        for (int i = 0; i < 3; i++) {
            // 获取玩家出拳
            System.out.println("请输入你要出的拳(0-石头, 1-剪刀, 2-布):");
            int num = scanner.nextInt();
            player.setPlayerGuessNum(num);
            int playerGuess = player.getPlayerGuessNum();
            arr1[i][j + 1] = playerGuess;

            // 获取电脑出拳
            int computerGuess = player.setComputerGuessNum();
            arr1[i][j + 2] = computerGuess;

            // 将玩家猜的拳和电脑比较
            String isWin = player.vsComputer();
            arr2[i] = isWin;
            arr1[i][j] = player.count;

            // 对每一局的情况输出
            System.out.println("=====================================");
            System.out.println("局数\t玩家的出拳\t电脑的出拳\t输赢情况");
            System.out.println(player.count + "\t\t" + playerGuess + "\t\t\t" + computerGuess + "\t\t\t" + isWin);
            System.out.println("=====================================");
            System.out.println("\n");
            isWinCount = player.winCount(isWin);
        }

        // 对游戏的最终结果进行输出
        System.out.println("局数\t玩家的出拳\t电脑的出拳\t\t输赢情况");
        int a;
        for (a = 0; a < arr1.length; a++) {
            for (int b = 0; b < arr1[a].length; b++) {
                System.out.print(arr1[a][b] + "\t\t\t");
            }
            System.out.print(arr2[a]);
            System.out.println();
        }
        System.out.println("你赢了" + isWinCount + "次");
    }
}

class Player {
    // 玩家出拳的类型
    int playerGuessNum;
    // 电脑出拳的类型
    int computerGuessNum;
    // 玩家赢的次数
    int winCount;
    // 比赛的总场数
    int count = 1;

    /**
     * 设置电脑随机生成猜拳的数字的方法
     *
     * @return :返回电脑猜拳的数字
     */
    public int setComputerGuessNum() {
        Random r = new Random();
        this.computerGuessNum = r.nextInt(3);
        return computerGuessNum;
    }

    /**
     * 获取玩家猜拳的数字的方法
     *
     * @return :返回玩家猜拳的数字
     */
    public int getPlayerGuessNum() {
        return playerGuessNum;
    }

    /**
     * 设置玩家猜拳的方法
     *
     * @param playerGuessNum :玩家猜拳的数字
     */
    public void setPlayerGuessNum(int playerGuessNum) {
        if (playerGuessNum > 2 || playerGuessNum < 0) {
            throw new IllegalArgumentException("数字输入错误");
        }
        this.playerGuessNum = playerGuessNum;
    }

    /**
     * 统计胜场次数
     *
     * @param s :胜负情况
     * @return :返回胜场次数
     */
    public int winCount(String s) {
        count++; // 总场数自增
        if (s.equals("你赢了")) {
            winCount++;
        }
        return winCount;
    }

    /**
     * 胜负判断
     *
     * @return :返回胜负情况
     */
    public String vsComputer() {
        if (playerGuessNum == 0 && computerGuessNum == 1) {// 人:石头,机:剪刀
            return "你赢了";
        } else if (playerGuessNum == 1 && computerGuessNum == 2) {// 人:剪刀,机:布
            return "你赢了";
        } else if (playerGuessNum == 2 && computerGuessNum == 0) {// 人:布,机:石头
            return "你赢了";
        } else if (playerGuessNum == computerGuessNum) {// 一样
            return "打平手";
        } else {
            return "你输了";
        }
    }
}

IDEA

  1. IDEA介绍

    IDEA全称IntelliJ IDEA

    在业界被公认为最好的Java开发工具。

    IDEA是JetBrains公司的产品。

    除了支持Java开发,还支持HTML,CSS,PHP,MySQL,Python等。

  2. 安装:官网

  3. IDEA界面

    上边——菜单区,左边——项目导航区,右边——代码编辑区,下边——输出的控制台区

  4. 设置字体:

    1. 外观和行为Appearance & Behavior -> Appearance
    2. 文件file -> settings -> Editor -> Font
  5. 在IDEA中,使用run一个.java文件时,会把该文件编译成.class字节码文件,然后再运行。

    编译生成的.class文件会自动在项目目录下的out文件夹

  6. IDEA常用快捷键

    修改快捷键:文件file -> settings -> Keymap -> Editor Actions

    1. 删除当前行:Ctrl + D
    2. 快速格式化代码:Ctrl + Shift + F
    3. 快速运行程序:Alt + R。如果需要使用快捷键运行,需要先配置主类第一次运行需要鼠标右键Run
    4. 生成构造器等:Alt + Insert
    5. 查看一个类的层级关系:Ctrl + H
    6. 将光标放在一个方法上,定位到哪个类的方法:Ctrl + B
  7. 模板:文件file -> settings -> Editor -> Live templates

7、面向对象编程(中级)

7.1、包

作用:

  1. 区分很多相同名字的类。
  2. 当类很多时,可以很好的管理类。
  3. 控制访问范围。

基本语法:

package 包名;

package关键字,表示打包。

包的本质分析(原理):实际上就是创建不同的文件夹来保存类文件

包的命名规则:只能包含数字、字母、下划线、小圆点.,但不能用数字开头,不能是关键字或保留字

包的命名规范:一般是小写字母+小圆点.。一般是com.公司名firm.项目名project.业务模块名model

com.sina.crm.user// 用户模块

com.sina.crm.order// 订单模块

com.sina.crm.utils// 工具类

Java中常用的包(.*表示所有包):

  1. java.lang.*:lang包是基本包,默认引入,不需要再引入
  2. java.util.*:util包,系统提供的工具包,工具类
  3. java.net.*:网络包,网络开发
  4. java.awt.*:Java图形化界面开发(GUI)的包

注意:

  1. package 包名;的声明必须放在类的最上面,一个类中最多一句package
  2. import指令位置放在package下面,在类定义前面。

7.2、访问修饰符

Java提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围):

  1. 公开级别:用public修饰,对整个项目所有包中的类公开。
  2. 受保护级别:用protected修饰,对子类同一个包中的类公开。
  3. 默认级别:没有修饰符号,只向同一个包中的类公开。
  4. 私有级别:用private修饰,只有类本身可以访问,不对外公开。

访问修饰符的权限图(⭐⭐⭐⭐⭐)

访问修饰符

注意:

  1. 修饰符可以用来修饰类中的属性,成员方法以及类。
  2. 只有默认的和public才能修饰类,并且遵循以上访问权限的特点。
  3. 成员方法的访问规则和属性完全一样。
  4. 默认级别的子类对象不能访问不同包下父类受保护级别的方法或属性

com.yxz.modifier.A.java

package com.yxz.modifier;

public class A {
    // 四个属性,分别使用不同的访问修饰符来修饰
    public int n1 = 100;
    protected int n2 = 200;
    int n3 = 300;
    private int n4 = 400;

    public void m1() {
        // 同一个类中可以访问public、protected、默认、private修饰的属性或方法
        System.out.println("n1 = " + n1 + ", n2 = " + n2 + ", n3 = " + n3 + ", n4 = " + n4);
    }

    protected void m2() {
    }

    void m3() {
    }

    private void m4() {
    }

    public void say() {
        // 同一个类中可以访问public、protected、默认、private修饰的属性或方法
        m1();
        m2();
        m3();
        m4();
    }
}

com.yxz.modifier.B.java

package com.yxz.modifier;

public class B {
    public void hi() {
        A a = new A();
        // 在同一个包下,可以访问 public,protected和默认修饰属性或方法,不能访问private
        System.out.println("n1 = " + a.n1 + ", n2 = " + a.n2 + ", n3 = " + a.n3);
        a.m1();
        a.m2();
        a.m3();
        // a.m4(); 错误:不同类中无法访问private修饰的方法
    }
}

com.yxz.modifier.Test.java

package com.yxz.modifier;

public class Test {
    public static void main(String[] args) {
        A a = new A();
        a.m1();
        B b = new B();
        b.hi();
    }
}

// 只有默认和public可以修饰类
class Tiger {
}

com.yxz.pkg.Test.java

package com.yxz.pkg;

import com.yxz.modifier.A;

public class Test {
    public static void main(String[] args) {
        A a = new A();
        // 在不同包下,可以访问public修饰的属性或方法
        // 但是不能访问protected、默认、private修饰的属性或方法
        System.out.println(a.n1);
        a.m1();
        // 在不同包下,不能访问protected、默认、private修饰的属性或方法
//        a.m2();
//        a.m3();
//        a.m4();
        B b = new B();
//        b.m2();   // 默认级别的子类对象不能访问不同包下父类受保护级别的方法
//        System.out.println(b.n2); // 默认级别的子类对象不能访问不同包下父类受保护级别的属性
    }
}

class B extends A {
}

面向对象编程(OOP, Object-oriented programming)三大特征:封装继承多态

7.3、封装

封装(encapsulation)就是把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作(方法)才能对数据进行操作。

封装的好处:

  1. 隐藏实现的细节,调用者无需关注内部运行原理。
  2. 可以对数据进行验证,保证数据的安全。

封装的实现步骤:

  1. 将属性进行私有化private(外部不能直接修改属性)。

  2. 提供一个公共的(public)set方法,用于对属性判断并赋值。

    public void setXxx(数据类型 参数名) {// Xxx表示某个属性
        // 数据验证的业务逻辑
        this.属性 = 参数名;
    }
    
  3. 提供一个公共的get方法,用于获取属性的值。

    public 返回值类型 getXxx() {// Xxx表示某个属性
        // 权限判断
        return xxx;
    }
    

综合案例

编写一个类Person,属性(姓名,年龄,工作,薪水)
要求:外部不能随便查看人的年龄、工资等隐私
并对设置的年龄进行合理的验证,年龄合理就设置,否则给默认年龄。必须在1-120之间
工资不能直接查看,name的长度在2-6个字符。

package com.yxz.encap;

public class Encapsulation {
    public static void main(String[] args) {
        Person person = new Person();
        // person.name = "Mi cai"; 名字长度需要判断是否合理
        // person.age = 22; 错误:不同类之间不能访问私有属性
        person.setName("米彩");
        person.setAge(23);
        person.setSalary(1000000.0);
        person.setJob("总裁");
        System.out.println(person.toString());

        Person person1 = new Person("米兰", 2000, 300000.0, "经理");
        System.out.println(person1.toString());
    }
}

class Person {
    public String name; // 名字公开
    private int age;    // 年龄私有化
    private double salary;  // 薪水 私有化
    private String job;

    public Person() {
    }

    public Person(String name, int age, double salary, String job) {
//        this.name = name;
//        this.age = age;
//        this.salary = salary;
//        this.job = job;
        setName(name);
        setAge(age);
        setSalary(salary);
        setJob(job);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        // 加入对数据的校验,增加业务逻辑
        if (name.length() >= 2 && name.length() <= 6) {
            this.name = name;
        } else {
            System.out.println("名字长度不合法,需要(2-6)个字符,给默认名字");
            this.name = "无名小卒";
        }
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        // 判断
        if (age >= 1 && age <= 120) {
            this.age = age;
        } else {
            System.out.println("设置的年龄不合理,年龄需要在1-120");
            this.age = 18;  // 默认年龄
        }
    }

    public double getSalary() {
        // 增加对当前对象的权限判断
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", salary=" + salary +
                ", job='" + job + '\'' +
                '}';
    }
}

7.4、继承

继承(inherit)是面向对象的第二大特征。继承可以解决代码复用的问题。当多个类存在相同的属性和方法时,可以从这些类中抽象出父类。在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends关键字来声明继承父类即可。

继承示意图

继承的基本语法:

class 子类 extends 父类 {    

}

继承父类后,子类就会自动拥有父类定义的属性和方法

父类又叫 超类,基类,子类又叫派生类。

继承的作用:

  1. 提高代码的复用性。
  2. 提高代码的可扩展性和可维护性。

注意:

  1. 子类继承了父类所有的属性和方法,父类非私有的属性和方法可以在子类中直接访问。但是父类的私有属性和方法不能在子类直接访问,要通过父类提供的公共的方法去访问。
  2. 子类必须调用父类的构造器,先完成父类的初始化(先有父亲后有儿子)。
  3. 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器super();。如果父类没有提供无参构造器,则必须在子类的构造器中用super显式地指定使用父类的哪个构造器以完成对父类的初始化工作,否则编译不会通过。
  4. 如果希望指定去调用父类的某个个构造器,则显式的调用一下:super(参数列表)
  5. 使用super时,需要放在子类构造器的第一行(和this在构造器中一样)且只能在构造器中使用。
  6. super()this()都只能放在构造器的第一行,因此这两个方法不能共存于一个构造器。
  7. Java中所有的类都是Object类的子类 -> Object类是所有类的基类(超类)。IDEA快捷键Ctrl + H查看类的继承关系。
  8. 父类构造器的调用不限于直接父类。将一直向上追溯直到Obejct类(顶级父类)
  9. 子类最多只能继承一个父类(指直接继承),即Java是单根继承机制。
  10. 不能滥用继承,子类和父类之间必须满足is-a的逻辑关系。

案例1:学生Student(父类) <- 小学生Pupil(子类),大学生Graduate(子类)。双重继承,说明继承的作用。

Student.java文件

package com.yxz.inherit_improve;

// 父类,是Pupil和Graduate的父类
public class Student {
    // 共有属性
    public String name;
    public int age;
    private double score;

    // 共有方法
    public void setScore(double score) {
        this.score = score;
    }

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

Pupil.java文件

package com.yxz.inherit_improve;

// 子类Pupil继承父类Student
public class Pupil extends Student{
    public void testing() {
        System.out.println("小学生 " + name + " 正在考《小学数学》...");
    }
}

Graduate.java文件

package com.yxz.inherit_improve;

// 子类Graduate继承父类Student
public class Graduate extends Student{
    public void testing() {
        System.out.println("大学生 " + name + " 正在考《高等数学》...");
    }
}

Inherit.java文件——测试类

package com.yxz.inherit_improve;

public class Inherit {
    public static void main(String[] args) {
        // 小学生
        Pupil pupil = new Pupil();
        pupil.name = "小明";
        pupil.age = 11;
        pupil.testing();
        pupil.setScore(60);
        System.out.println(pupil.toString());

        // 大学生
        Graduate graduate = new Graduate();
        graduate.name = "杨明";
        graduate.age = 19;
        graduate.testing();
        graduate.setScore(98);
        System.out.println(graduate.toString());
    }
}

/**控制台输出:
 * 小学生 小明 正在考《小学数学》...
 * Student{name='小明', age=11, score=60.0}
 * 大学生 杨明 正在考《高等数学》...
 * Student{name='杨明', age=19, score=98.0}
 */

案例2:TopBase <- Base <- Sub。三层继承,说明继承的十个注意点。

TopBase.java文件

package com.yxz.inherit_improve;

public class TopBase {
    public TopBase() {
        // super(); Object类的无参构造器
        System.out.println("TopBase()构造器被调用...");
    }
}

Base.java文件

package com.yxz.inherit_improve;

public class Base extends TopBase {// 父类
    // 4个不同修饰符的属性
    public int n1 = 100;
    protected int n2 = 200;
    int n3 = 300;
    private int n4 = 400;

//    public Base() {
//        System.out.println("父类Base()构造器被调用...");
//    }

    public Base(String name, int age) {
        // super(); TopBase类的无参构造器
        System.out.println("父类Base(String name, int age)构造器被调用...");
    }

    // 父类提供一个public方法供子类访问父类私有属性和私有方法
    public int getN4() {
        return n4;
    }

    public void test100() {
        System.out.println("test100");
    }

    protected void test200() {
        System.out.println("test200");
    }

    void test300() {
        System.out.println("test300");
    }

    private void test400() {
        System.out.println("test400");
    }

    public void callTest400() {
        test400();
    }
}

Sub.java文件

package com.yxz.inherit_improve;

public class Sub extends Base {
    public Sub() {
        // 子类必须调用父类的构造器,完成父类的初始化
        // super(); // 默认调用父类的无参构造器 -> 子类中的无参构造器默认包含语句super();
        // 当父类的无参构造器被覆盖时,必须在子类的构造器中显式的指定调用父类的其他有参构造器
        super("小米", 10); // super()必须在子类的构造器的第一行(先有父亲再有儿子)
        System.out.println("子类Sub()构造器被调用...");
    }

    public Sub(String name) {
        super("小米", 10);
        System.out.println("子类Sub(String name)构造器被调用...");
    }

    public void sayOk() {
        // 父类的非private属性和方法都可以访问
        System.out.println("n1 = " + n1 + " n2 = " + n2 + " n3 = " + n3);
        //System.out.println(n4); 错误:不能直接访问父类的私有属性
        test100();
        test200();
        test300();
        //test400(); 错误:不能直接访问父类的私有方法
        // 通过父类提供的公共的方法去访问父类的私有属性和私有方法
        System.out.println("n4 = " + getN4());
        callTest400();
    }
}

InheritDetail.java文件

package com.yxz.inherit_improve;

public class InheritDetail {
    public static void main(String[] args) {
        System.out.println("==========第1个对象===========");
        Sub sub = new Sub();
        sub.sayOk();
        System.out.println("==========第2个对象===========");
        Sub sub1 = new Sub("jack");
    }
}

/**控制台输出:
 * ==========第1个对象===========
 * TopBase()构造器被调用...
 * 父类Base(String name, int age)构造器被调用...
 * 子类Sub()构造器被调用...
 * n1 = 100 n2 = 200 n3 = 300
 * test100
 * test200
 * test300
 * n4 = 400
 * test400
 * ==========第2个对象===========
 * TopBase()构造器被调用...
 * 父类Base(String name, int age)构造器被调用...
 * 子类Sub(String name)构造器被调用...
 *
 * 进程已结束,退出代码为 0
 */

继承的本质分析(⭐⭐⭐)

继承的内存布局

子类查找(访问)属性和方法的规则

(1) 首先看子类是否有该属性(方法)。

(2) 如果子类有这个属性(方法)并且可以访问,则返回信息。

(3) 如果子类没有这个属性(方法),就看直接父类有没有这个属性(方法)。

如果直接父类有该属性(方法)且可以访问就返回父类的信息。

如果直接父类的该属性(方法)是私有的,且不可直接访问,则返回报错,不会继续向上访问父类

(4) 如果直接父类没有就按照 (3) 的规则再向上找父类,直到Object类。

package com.yxz.inherit_improve;

public class InheritTheory {
    public static void main(String[] args) {
        Son son = new Son();
        // 子类查找(访问)信息的规则:
        // (1). 首先看子类是否有该属性
        // (2). 如果子类有这个属性并且可以访问,则返回信息
        // (3). 如果子类没有这个属性,就看直接父类有没有这个属性
        //     (直接父类有该属性且可以访问就返回信息,如果父类的该属性是私有的,则返回报错,不会继续向上访问)
        // (4). 如果直接父类没有就按照(3)的规则再向上找父类,直到Object
        System.out.println(son.name);   // 大头儿子
        System.out.println(son.age);    // 39
        System.out.println(son.hobby);  // 旅游
    }
}

class GrandPa {
    String name = "大头爷爷";
    String hobby = "旅游";
}

class Father extends GrandPa{
    String name = "大头爸爸";
    int age = 39;
}

class Son extends Father{
    String name = "大头儿子";
}

7.5、多态

多态(polymorphic):方法或对象具有多种形态。多态是面向对象的第三大特征,多态是建立在封装和继承基础之上的。

多态的具体体现:

  1. 方法的多态:方法重载和方法重写体现多态。在同一方法名中传入不同参数时,会自动调用不同方法(重载)。当不同类对象调用同一方法名时,会自动调用不同方法(重写)。

    public class Test {
        public static void main(String[] args) {
            A a = new A();
            B b = new B();
    
            // 不同类对象调用同一方法名 -> 自动调用对应的方法(方法重写)
            a.say();
            b.say();
            // 传入不同个数的参数 -> 自动调用匹配的方法(方法重载)
            System.out.println(b.sum(1, 2));
            System.out.println(b.sum(10, 20, 30));
        }
    }
    
    class A {// 父类
    
        public void say() {
            System.out.println("父类 A中的say()方法被调用");
        }
    }
    
    class B extends A {
        public int sum(int n1, int n2) {// 和下面的sum方法构成方法重载
            return n1 + n2;
        }
    
        public int sum(int n1, int n2, int n3) {
            return n1 + n2 + n3;
        }
    
    
        public void say() { // 和父类A中的say方法构成方法重写
            System.out.println("子类 B中的say()方法被调用");
        }
    }
    
  2. 对象的多态(核心)

    编译类型 对象引用 = new 运行类型();

    1. 一个对象的编译类型和运行类型可以不一致

    2. 编译类型在定义对象时就已经确定,不能改变

    3. 对象的运行类型是可以变化的,可以通过getClass()方法获取对象的运行类型。

    4. 编译类型看定义时=号的左边,运行类型看=号的右边

      Animal animal = new Animal(); // animal的编译类型和运行类型一致,都是Animal
      Animal animal = new Dog();	// animal编译类型是Animal,运行类型Dog
      animal = new Cat();	// animal的运行类型变成了Cat,编译类型仍然是Animal
      // 结论:父类的引用(对象名)可以指向父类对象,也可以指向父类的子类对象
      
      package com.yxz.poly;
      
      public class PolyObject {
          public static void main(String[] args) {
              Animal animal = new Animal();
              // animal 编译类型:Animal,运行类型:Animal
              animal.say();
      
              animal = new Cat();
              // animal 编译类型:Animal,运行类型:Cat
              animal.say();
      
              animal = new Dog();
              // animal 编译类型:Animal,运行类型:Dog
              animal.say();
          }
      }
      /**控制台输出:
       * Animal cry() 动物叫...
       * Cat cry() 小猫喵喵叫...
       * Dog cry() 小狗汪汪叫...
       */
      
      class Animal {
          public void say() {
              System.out.println("Animal cry() 动物叫...");
          }
      }
      
      class Cat extends Animal {
          public void say() {
              System.out.println("Cat cry() 小猫喵喵叫...");
          }
      }
      
      class Dog extends Animal {
          public void say() {
              System.out.println("Dog cry() 小狗汪汪叫...");
          }
      }
      

注意:多态的前提是:两个对象(类)存在继承关系

多态的向上转型

  1. 向上转型的本质:父类的引用指向了子类的对象

  2. 语法:父类类型 对象引用 = new 子类类型();

  3. 向上转型调用方法的规则如下:

    1. 可以调用父类中的所有成员(遵守访问权限规则)。

    2. 不能调用子类中特有成员,无法通过编译。

      原因:因为在编译阶段,能调用哪些成员,是由对象引用的编译类型来决定。

    3. 最终运行效果(运行阶段)看子类(对象引用的运行类型)的具体实现。即:在运行阶段调用方法时,根据对象引用的运行类型,从子类(运行类型)中开始查找方法(遵循访问成员的规则)。

多态的向下转型

  1. 语法:子类类型 引用名 = (子类类型)父类引用
  2. 只能强转父类的引用,不能强转父类的对象。
  3. 要求父类的引用必须指向的是当前目标类型的对象。
  4. 当向下转型后,可以调用子类类型中的所有的成员(向下转型的目的)

注意:

  1. 属性没有重写之说。属性的值看对象引用的编译类型
  2. instanceOf比较操作符:用于判断对象的运行类型是否为XX类型或XX类型的子类型。
package com.yxz.poly;

public class PolyDetail {
    public static void main(String[] args) {
        // 父类引用指向子类对象(向上转型)
        // 语法:父类类型 引用名 = new 子类类型();
        Animal animal = new Cat();
        Object obj = new Cat();

        // 可以调用父类中的所有成员(遵守访问权限)
        // 不能调用子类的特有成员
        // animal.catchMouse(); 编译错误,无法通过编译
        // 在编译阶段,父类引用能调用哪些成员是由编译类型决定
        // 最终运行效果(运行阶段)看子类(运行类型)的具体实现
        // 子类重写了eat方法,所以优先调用子类的eat方法。
        animal.eat();   // 猫吃鱼
        animal.sleep(); // 睡觉
        animal.run();   // 奔跑
        animal.show();// 你好
        // 属性的值看编译类型
        System.out.println("animal.age = " + animal.age); // anmal编译类型:Animal,输出10

        // animal的编译类型:Animal,运行类型:Cat
        Cat cat = (Cat) animal; // 向下转型
        cat.catchMouse();
        System.out.println("cat.age = " + cat.age); // cat编译类型:Cat,输出20

        System.out.println(cat instanceof Animal); // cat的运行类型:Cat,是Animal或其子类:true
        System.out.println(animal instanceof Cat); // animal的运行类型:Cat,是Cat或其子类:true
        Object obj1 = new Object();
        System.out.println(obj1 instanceof Animal); // obj1的运行类型:Object,是Animal或其子类:false
    }
}

/**
 * 控制台输出:
 * 猫吃鱼
 * 睡觉
 * 奔跑
 * 你好
 * animal.age = 10
 * 猫抓老鼠
 * cat.age = 20
 * true
 * true
 * false
 */

class Animal {
    String name = "动物";
    int age = 10;

    public void sleep() {
        System.out.println("睡觉");
    }

    public void run() {
        System.out.println("奔跑");
    }

    public void eat() {
        System.out.println("吃饭");
    }

    public void show() {
        System.out.println("你好");
    }
}

class Cat extends Animal {
    int age = 20;

    public void eat() {
        System.out.println("猫吃鱼");
    }

    public void catchMouse() {// Cat特有方法
        System.out.println("猫抓老鼠");
    }
}

Java的动态绑定机制(非常非常非常重要)

  1. 调用对象方法的时候,该方法会和该对象的内存地址(运行类型)进行绑定
  2. 调用对象属性时,没有动态绑定机制,哪里声明,哪里使用。
package com.yxz.poly;

public class DynamicBinding {
    public static void main(String[] args) {
        A a = new B();// 向上转型
        // 1. a调用方法sum(),先从运行类型(B类)开始查找
        // 2. 运行类型中没有sum方法 —> 根据继承机制,到父类中查找sum方法
        // 3. 父类中有sum方法,调用
        // 4. 父类sum方法中调用了getI方法(子类父类都有),根据动态绑定机制 —> 该方法和该对象内存地址(运行类型)绑定
        // 5. 再到运行类型(B类)中查找getI方法,在B类中找到getI方法
        // 6. getI方法访问属性i,根据属性没有动态绑定机制,哪里声明哪里使用,先找子类中的i = 20
        // 7. 返回到父类中的sum方法,返回i + 10 = 30
        System.out.println(a.sum());    // 30
        System.out.println(a.sum1()); // 20
    }
}

class A {
    public int i = 10;

    public int sum() {
        return getI() + 10;
    }

    public int sum1() {
        return i + 10;
    }

    public int getI() {// 父类getI
        return i;
    }
}

class B extends A {
    public int i = 20;

//    public int sum() {
//        return getI() + 20;
//    }

//    public int sum1() {
//        return i + 10;
//    }

    public int getI() {// 子类getI
        return i;
    }
}

多态的应用

  1. 多态数组

    数组的定义类型为父类类型,里面保存的实际元素类型为子类类型

    应用实例:创建一个Person对象、2个Student对象和2个Teacher对象,统一放在数组中,并调用每个对象的say方法。

    Person.java文件

    package com.yxz.poly.polyarr;
    
    public class Person {
        private String name;
        private int age;
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public String say() {
            return name + "\t" + age;
        }
    }
    

    Student.java文件

    package com.yxz.poly.polyarr;
    
    public class Student extends Person {
        private double score;
    
        public Student(String name, int age, double score) {
            super(name, age);
            this.score = score;
        }
    
        public double getScore() {
            return score;
        }
    
        public void setScore(double score) {
            this.score = score;
        }
    
        @Override
        public String say() {
            return "学生 " + super.say() + " \t\tscore = " + score;
        }
    
        // 特有方法
        public void study() {
            System.out.println("学生" + getName() + "正在学java...");
        }
    }
    

    Teacher.java文件

    package com.yxz.poly.polyarr;
    
    public class Teacher extends Person {
        private double salary;
    
        public Teacher(String name, int age, double salary) {
            super(name, age);
            this.salary = salary;
        }
    
        public double getSalary() {
            return salary;
        }
    
        public void setSalary(double salary) {
            this.salary = salary;
        }
    
        @Override
        public String say() {
            return "老师 " + super.say() + " \t\tsalary = " + salary;
        }
    
        // 特有方法
        public void teach() {
            System.out.println("老师" + getName() + "正在授课...");
        }
    }
    

    PloyArray.java文件

    package com.yxz.poly.polyarr;
    
    public class PloyArray {
        public static void main(String[] args) {
            Person[] persons = new Person[5];
            persons[0] = new Person("jack", 20);
            persons[1] = new Student("mary", 18, 100);
            persons[2] = new Student("smith", 19, 30.1);
            persons[3] = new Teacher("milan", 30, 20000);
            persons[4] = new Teacher("king", 29, 25000);
    
            // 循环遍历多态数组,调用say
            for (int i = 0; i < persons.length; i++) {
                // persons[i] 编译类型:Person,运行类型是根据实际情况由JVM判断
                System.out.println(persons[i].say());   // 动态绑定机制
                if (persons[i] instanceof Student) {    // 判断运行类型是否是Student类
                    ((Student) persons[i]).study(); //向下转型
                } else if (persons[i] instanceof Teacher) {  // 判断运行类型是否是Teacher类
                    ((Teacher) persons[i]).teach(); //向下转型
                } else if (persons[i] instanceof Person) {	// 判断运行类型是否是Person类
                    // 不做任何处理
                } else {
                    System.out.println("类型有误...");
                }
            }
        }
    }
    
  2. 多态参数

    方法定义的形参类型为父类类型,实参类型允许为子类类型。

    案例1:主人喂动物食物。

    1. 定义一个动物类Animal,属性name(private)。狗类Dog和猫类Cat继承动物类。
    2. 定义一个食物类Food,属性name(private)。骨头类Bone和鱼类Fish继承食物类。
    3. 定义一个主人类Master,属性name(private)。添加一个喂食feed(Animal animal, Food food)方法,根据传入的不同类型分别打印不同的信息。
    4. 测试类中调用方法feed,分别传入不同对象。

    Animal.java文件

    package com.yxz.poly.method;
    
    public class Animal {// 动物类
        private String name;
    
        public Animal(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
    class Dog extends Animal {
        public Dog(String name) {
            super(name);
        }
    }
    
    class Cat extends Animal {
        public Cat(String name) {
            super(name);
        }
    }
    

    Food.java文件

    package com.yxz.poly.method;
    
    public class Food {// 食物类
        private String name;
    
        public Food(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
    class Fish extends Food {
        public Fish(String name) {
            super(name);
        }
    }
    
    class Bone extends Food {
        public Bone(String name) {
            super(name);
        }
    }
    

    Master.java文件

    package com.yxz.poly.method;
    
    public class Master {// 主人类
        private String name;
    
        public Master(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public void feed(Animal animal, Food food) { // 参数多态
            System.out.println("主人" + getName() + "给宠物 " + animal.getName() + " 喂 " + food.getName() + " 吃");
        }
    }
    

    Polymorphic.java——测试类

    package com.yxz.poly.method;
    
    public class Polymorphic {
        public static void main(String[] args) {
            // 创建主人对象
            Master tom = new Master("tom");
    
            // 创建小狗
            Dog dog = new Dog("大黄");
            Bone bone = new Bone("大棒骨");
            tom.feed(dog, bone);
    
            // 创建小猫
            Cat cat = new Cat("花猫");
            Fish fish = new Fish("黄花鱼");
            tom.feed(cat, fish);
        }
    }
    

    案例2:计算不同员工的年薪

    1. 定义员工类Employee,属性包含姓名和月工资(private),以及计算年工资getAnnual方法。
    2. 普通员工Worker和经理Manager继承了员工。经理类多了奖金bonus属性和管理manage方法,普通员工类多了work方法,普通员工和经理类要求分别重写getAnnual方法。
    3. 测试类中添加一个方法showEmployeeAnnual(Employee e),实现获取任何员工对象的年工资,并在main方法中调用该方法e.getAnnual。
    4. 测试类中添加一个方法testWork(Employee e),如果是普通员工,则调用work方法,如果是经理,则调用manage方法。

    Employee.java文件

    package com.yxz.ployparameter;
    
    public class Employee {
        private String name;
        private double salary;
    
        public Employee(String name, double salary) {
            this.name = name;
            this.salary = salary;
        }
    
        /**
         * 获取年工资的方法
         *
         * @return :年工资
         */
        public double getAnnual() {
            return 12 * salary;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public double getSalary() {
            return salary;
        }
    
        public void setSalary(double salary) {
            this.salary = salary;
        }
    }
    

    Worker.java文件

    package com.yxz.ployparameter;
    
    public class Worker extends Employee {
        public Worker(String name, double salary) {
            super(name, salary);
        }
    
        @Override
        public double getAnnual() {
            return super.getAnnual();
        }
    
        public void work() {
            System.out.println("普通员工 " + getName() + " is working...");
        }
    }
    

    Manager.java文件

    package com.yxz.ployparameter;
    
    public class Manager extends Employee {
        private double bonus;
    
        public Manager(String name, double salary, double bonus) {
            super(name, salary);
            this.bonus = bonus;
        }
    
        public double getBonus() {
            return bonus;
        }
    
        public void setBonus(double bonus) {
            this.bonus = bonus;
        }
    
        @Override
        public double getAnnual() {
            return super.getAnnual() + bonus;
        }
    
        public void manage() {
            System.out.println("经理" + getName() + " is managing...");
        }
    }
    

    PloyParameter.java——测试类

    package com.yxz.ployparameter;
    
    public class PloyParameter {
        public static void main(String[] args) {
            Worker tom = new Worker("Tom", 3000);
            Manager miCai = new Manager("MiCai", 13000, 200000);
            PloyParameter ployParameter = new PloyParameter();
            ployParameter.showEmployeeAnnual(tom);
            ployParameter.testWork(tom);
            ployParameter.showEmployeeAnnual(miCai);
            ployParameter.testWork(miCai);
        }
    
        public void showEmployeeAnnual(Employee e) {
            System.out.println(e.getAnnual());  // 动态绑定机制
        }
    
        // testWork方法:如果是普通员工,调用work方法,如果是经理,调用manage方法
        public void testWork(Employee e) {
            if (e instanceof Worker) {
                ((Worker) e).work();    // 向下转型
            } else if (e instanceof Manager) {
                ((Manager) e).manage(); // 向下转型
            } else {
                // 不做处理
            }
        }
    }
    

7.6、super

super代表父类的引用,用于访问父类的属性、方法、构造器。

基本语法:

  1. super.属性名; 访问父类的属性,但不能访问父类的private属性。
  2. super.方法名(参数列表); 访问父类的方法,但不能访问父类的private方法。
  3. super(参数列表); 访问父类的构造器,只能在构造器的第一个语句。

super的作用:

  1. 调用父类的构造器 -> 分工明确,父类属性由父类初始化,子类的属性由子类初始化。

  2. 当子类中有和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过super。如果没有重名,使用super、this、直接访问是一样的效果。

    使用直接访问this访问属性遵从就近原则,先在本类中找属性或方法,找不到再找父类。super会直接跳过本类直接到父类中找属性或方法。

  3. super的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以用super去访问爷爷类的成员。如果多个基类(超类)都有同名成员,使用super访问遵循就近原则

super和this的比较

区别点 this super
访问属性 优先访问本类中的属性,如果本类中没有此属性则从父类中继续查找 直接从父类开始访问属性
调用方法 优先访问本类中的方法,如果本类中没有此方法则从父类中继续查找 直接从父类开始访问方法
调用构造器 调用本类构造器,必须放在构造器的首行 调用父类构造器,必须放在子类构造器的首行
特殊 表示当前对象 子类中访问父类对象

7.7、overwrite

方法覆盖(方法重写, overwrite, override):子类有一个方法和父类的某个方法的名称、返回类型、参数一样,那么子类的这个方法就覆盖了父类的方法。不局限于直接父类,间接父类也可以触发方法覆盖

构成方法覆盖的条件:

  1. 子类的方法的方法名、参数列表要和父类的方法的方法名、参数列表完全一样才能构成方法覆盖。
  2. 子类方法的返回值类型和父类方法的返回值类型一样,或者是父类返回值类型的子类。如:父类方法返回值类型Object类,子类方法返回值类型是String,其他一样,则构成方法覆盖。
  3. 子类方法不能缩小(可以放大或相等)父类方法的访问权限。public > protected > 默认 > private
public class A {
    public void hi() {
    }
    
    protected Object say() {
        return null;
    }
}

class B extends A {
    public void hi() {// 构成方法覆盖
    }

    public void hi(String name) {// 构成方法重载,不构成方法覆盖(形参列表不一致)
    }

    private void hi(int age) {// 构成方法重载,不构成方法覆盖(缩小了父类方法的访问范围)
    }
    
    public Object say() {// 构成方法覆盖(扩大了父类方法访问范围)
        return null;
    }
    
//    protected String say() {// 不构成方法重载,可以构成方法覆盖(返回类型String是父类方法返回类型Object类的子类)
//        return null;
//    }
}

方法重载(overload)和方法重写(override)的区别

名称 发生范围 方法名 形参列表 返回值类型 访问修饰符
重载(overload) 本类 必须一致 参数类型、参数个数、参数顺序至少一个不同 无要求 无要求
重写(override) 父子类 必须一致 必须一致 子类重写的方法,返回值类型和父类返回值类型一致或者是其子类 子类方法的访问修饰符不能缩小父类方法的访问范围

案例(综合练习)

  1. 编写一个Person类,包括name, age属性(private),构造器、方法say(返回自我介绍的字符串)。
  2. 编写一个Student类,继承Person类,增加id、socre属性(private),以及构造器,定义say方法(返回自我介绍的字符串)。
  3. 在main()方法中,分别创建Person和Student对象,调用say方法输出自我介绍。
public class Test {
    public static void main(String[] args) {
        Person person = new Person("jack", 34);
        System.out.println(person.say());
        Student student = new Student("杨明", 21, 123, 98.5);
        System.out.println(student.say());
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String say() {
        return "name = " + name + " age = " + age;
    }
}

class Student extends Person {
    private int id;
    private double score;

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

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public double getScore() {
        return score;
    }

    public void setScore(double score) {
        this.score = score;
    }

    public String say() {
        return super.say() + " id = " + id + " score = " + score;
    }
}

7.8、Object类详解

  • equals()方法

    ==和equals的对比

    1. ==:既可以判断基本类型,又可以判断引用类型。

      如果判断基本类型,判断的是值是否相等。如:int i = 10; double d = 10.0;

      如果判断引用类型,判断的是对象地址是否相等,即判定是不是同一个对象。

    2. equals():是Object类中的方法,只能判断引用类型。默认判断的是地址是否相等子类中往往重写该方法,用于判断内容是否相等

      如:Integer类和String类的equals()方法已经重写,不再判断地址,只判断内容。

      Object类的equals()方法源码:

      public boolean equals(Object obj) {
          return (this == obj); // 判断对象地址是否相等
      }
      

      查看JDK中Integer的equals()方法源码String类的equals()方法源码

      // Integer类的equals()方法
      public boolean equals(Object obj) {// 重写Object类的equals()方法
          if (obj instanceof Integer) {	// 判断对象是否属于Integer类
              return value == ((Integer)obj).intValue(); // 向下转型并调用Integer的intValue()方法
          }
          return false;
      }
      
      // String类的equals()方法
      public boolean equals(Object anObject) {// 重写Object类的equals()方法
          if (this == anObject) {	// 如果是同一个对象则返回真
              return true;
          }
          if (anObject instanceof String) {	// 判断对象的运行类型是否属于String类
              String aString = (String)anObject;		// 向下转型,子类类型 对象引用 = (子类类型)父类引用
              if (!COMPACT_STRINGS || this.coder == aString.coder) {
                  return StringLatin1.equals(value, aString.value);
              }
          }
          return false; 	// 如果对象不属于String类,返回假
      }
      
    3. 实例:创建一个Person类,属性包括name、age、gender(private),

      判断两个Person对象的内容是否相等,如果两个Person对象的各个属性都一样,则返回true,反之false。

      package com.yxz.object_;
      
      public class EqualsExercise01 {
          public static void main(String[] args) {
              Person person1 = new Person("jack", 10, '男');
              Person person2 = new Person("jack", 10, '男');
              System.out.println(person1.equals(person2)); // true
              System.out.println(person1 == person2); // false ==对引用类型判断是否是同一个对象地址
          }
      }
      
      class Person {
          private String name;// 姓名
          private int age;// 年龄
          private char gender;// 性别
      
          public Person(String name, int age, char gender) {
              this.name = name;
              this.age = age;
              this.gender = gender;
          }
      
          @Override
          public boolean equals(Object obj) {
              // 如果是同一个对象,返回true
              if (this == obj) {
                  return true;
              }
              // 如果obj是Person对象
              if (obj instanceof Person) {
                  // 将obj对象向下转型,目的:需要得到子类的所有属性和方法
                  Person p = (Person) obj;
                  return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender;
              }
              // 如果不是Person对象
              return false;
          }
      }
      
  • finalize()方法
    说明:

    1. 当对象被回收时,系统自动调用该对象的finalize()方法。子类可以重写该方法,做一些释放资源的操作。
    2. 什么时候被回收:当某个对象没有任何引用时,则JVM就认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用finalize()方法
    3. 垃圾回收机制的调用,是由系统决定,也可以通过System.gc()主动触发垃圾回收机制。
    public class Finalize{
        public static void main(String[] args) {
            Car bmw = new Car("宝马");
            bmw = null; // 此时bmw指向的对象就是一个垃圾,垃圾回收器就会根据GC算法回收(销毁)对象,在销毁对象前,会调用该对象finalize()方法
        }
    }
    
    class Car {
        private String name;
        
        public Car(String name) {
            this.name = name;
        }
    }
    
  • hashCode()方法

    返回该对象的哈希码值(一般是通过将该对象的内部地址转换成一个整数来实现的),Object类支持此方法是为了提高哈希表的性能。

    结论:

    1. 提高具有哈希结构的容器的效率。
    2. 两个引用,如果指向的是同一个对象,则哈希值肯定是一样的。
    3. 两个引用,如果指向的是不同对象,则哈希值肯定不一样。
    4. 哈希值主要根据地址号来的,不能完全将哈希值等价于地址。
    5. 在集合等容器中如果需要hashCode,也会重写此方法。
  • toString()方法

    该方法默认返回:全类名+@+哈希值的十六进制。(全类名 = 包名 + 类名) 子类往往重写toString方法,用于返回对象的属性信息。在IDEA中使用快捷键Alt + Insert可以选择根据具体实现类的属性重写Object的toString()方法。

    重写toString()方法,打印对象或者拼接对象时,都会自动调用该对象的toString形式。

    当直接输出一个对象时,toString()方法会被默认调用。

    Object类的toString()方法源码

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    

    说明:

    1. getClass():返回class+包名+类名. 如:class java.lang.String
    2. getClass().getName():获取全类名(包名+类名)。如:java.lang.String
    3. getClass().getSimpleName():获取类名. 如:String
    4. Integer.toHexString(hashCode()):将对象的hashCode值转成十六进制字符串。

    案例:定义一个Monster类,属性包括name、job、salary。将Monster对象的属性输出

    package com.yxz.object_;
    
    public class ToStringExercise {
        public static void main(String[] args) {
            Monster monster = new Monster("小妖怪", "巡山的", 1000);
            System.out.println(monster.toString());
            System.out.println(monster);    // 默认调用toString()方法
        }
    }
    
    class Monster {
        private String name;
        private String job;
        private double salary;
    
        public Monster(String name, String job, double salary) {
            this.name = name;
            this.job = job;
            this.salary = salary;
        }
    
        @Override
        public String toString() {
            return "Monster{" +
                    "name='" + name + '\'' +
                    ", job='" + job + '\'' +
                    ", salary=" + salary +
                    '}';
        }
    }
    

7.9、断点调试

在开发中,程序员查找错误时,可以用断点调试(breakpoint debug),一步一步的看源码执行的过程,从而发现错误所在。

重要提示:在断点调试过程中,是运行状态,是以对象的运行类型来执行的

介绍:

  1. 断点调试是指在程序的某一行设置一个断点,调试时,程序运行到这一行就会停住,然后可以一步一步往下跳是,调试过程中可以看各个变量当前的值。出错的话,调试到出错的代码行即显示错误,停下。进行分析找到bug。
  2. 断点调试是程序员必须掌握的技能。
  3. 断点调试也能帮助我们查看Java底层源码的执行过程,提高Java水平。
  4. 断点调试快捷键:
    1. F7:跳入 -> 跳入方法体内执行。
    2. F8:跳过 -> 逐行执行代码。
    3. Shift + F8:跳出 -> 跳出方法。
    4. F9:resume,执行到下一个断点。

IDEA断点调试说明

8、项目实战运用

8.1、零钱通

项目需求

使用Java开发零钱通项目,可以完成查看明细(功能1),收益入账(功能2),消费(功能3),退出系统(功能4)等功能。

在用户输入4时,给出提示“确定要退出吗?y/n”,必须输入正确的y/n,否则循环输入指令,直到输入y/n。

在收益入账和消费时,判断金额是否合理,并给出相应提示。

项目流程(化繁为简)

  1. 先完成菜单显示功能,并可以选择功能。

  2. 完成零钱通明细功能。方法:1、把收益入账和消费保存到数组 2、使用对象 3、使用String拼接。

  3. 完成收益入账功能。

  4. 完成消费功能。

  5. 退出系统功能。

  6. 在用户输入4时,给出提示“确定要退出吗?y/n”,必须输入正确的y/n,否则循环输入指令,直到输入y/n。

    思路分析:

    (1)定义一个变量choice,接收用户输入
    (2)使用while + break,来处理接收到的输入是 y 还是 n
    (3)退出while后,再判断choice是y还是n,就可以决定是否退出
    (4)建议一段代码,完成一个小功能,降低耦合度

  7. 在收益入账和消费时,判断金额是否合理,并给出相应提示。

    如何校验数据?过关斩将思想:排除所有非法数据即可 -> 先判断是否是非法数据,再执行正确数据的代码(提高代码可读性)

  8. 使用面向过程完成项目的功能后,使用OOP(Object-oriented Programming)思想封装(变量 -> 类属性,功能 -> 类方法)

项目源码

SmallChangeSysOOP.java文件

package com.yxz.smallchange.oop;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;

/**
 * 该类是完成零钱通的各个功能的类
 * 使用OOP(Object-Oriented Programming),面向对象编程
 * 各个功能对应一个方法
 */
public class SmallChangeSysOOP {
    // 类属性
    // 1. 完成菜单显示功能,定义相关变量
    boolean loop = true;
    Scanner scanner = new Scanner(System.in);
    String key = "";
    // 2. 完成零钱通明细
    String details = "------------------零钱通明细--------------";
    // 3. 完成收益入账
    double money = 0;   // 入账金额
    double balance = 0; // 余额
    Date date = null;   // date 是 java.util.Date类型,表示日期
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); // 用于日期格式化的对象
    // 4. 完成消费
    // 定义新变量,记录消费原因
    String note = "";

    public void mainMenu() {
        do {
            System.out.println("\n==================零钱通菜单==============");
            System.out.println("\t\t\t\t1 零钱通明细");
            System.out.println("\t\t\t\t2 收益  入账");
            System.out.println("\t\t\t\t3 消费  支出");
            System.out.println("\t\t\t\t4 退出  系统");
            System.out.print("请选择(1-4): ");
            key = scanner.next();
            switch (key) {
                case "1":
                    this.detail();
                    break;
                case "2":
                    this.income();
                    break;
                case "3":
                    this.pay();
                    break;
                case "4":
                    this.exit();
                    break;
                default:
                    System.out.println("选择有误,请重新选择...");
            }
        } while (loop);
        System.out.println("---------------已经退出零钱通--------------");
    }

    // 完成零钱通明细
    public void detail() {
        System.out.println(details);
    }

    // 收益入账
    public void income() {
        System.out.print("收益入账金额:");
        money = scanner.nextDouble();
        // money的范围需要校验
        if (money <= 0) {
            System.out.println("收益入账金额需要大于0元");
            return; // 退出方法
        }
        balance += money;
        // 拼接收益入账信息到 details
        date = new Date(); // 获取到当前日期
        System.out.println();
        details += "\n收益入账\t+" + money + "\t" + sdf.format(date) + "\t" + "余额:" + balance;
    }

    // 消费
    public void pay() {
        System.out.print("消费金额:");
        money = scanner.nextDouble();
        // money的范围需要校验
        if (money <= 0 || money > balance) {
            System.out.println("消费金额应该在0 - " + balance + "元");
            return;
        }
        System.out.print("消费说明:");
        note = scanner.next();
        balance -= money;   // 余额变化
        // 拼接消费信息到 details
        date = new Date(); // 获取到当前日期
        System.out.println();
        details += "\n" + note + "\t-" + money + "\t" + sdf.format(date) + "\t" + "余额:" + balance;
    }

    public void exit() {
        /*
         思路分析:
        (1)定义一个变量choice,接收用户输入
        (2)使用while + break,来处理接收到的输入是 y 还是 n
        (3)退出while后,再判断choice是y还是n,就可以决定是否退出
        (4)建议一段代码,完成一个小功能,降低耦合度。
         */
        String choice = "";
        while (true) {  // 要求用户必须输入y/n,否则一直循环
            System.out.print("你确定要退出吗?y/n: ");
            choice = scanner.next();
            if ("y".equals(choice) || "n".equals(choice)) {
                break;
            }
        }
        // 当用户退出while后,进行判断
        if (choice.equals("y")) {
            loop = false;
        }
    }
}

SmallChangeSysApp.java文件

package com.yxz.smallchange.oop;

/**
 * 测试类,调用SmallChangeSysOOP对象方法
 */

public class SmallChangeSysApp {
    public static void main(String[] args) {
        new SmallChangeSysOOP().mainMenu(); // 匿名对象,用了一次就销毁
    }
}

8.2、房屋出租系统

房屋租赁程序框架图(分层模式) ——> 系统有哪些类(文件),明确类与类的调用关系

每个层次完成不同功能 -> 各司其职

  1. HouseVies.java类[View层,界面层]

    1. 显示界面
    2. 接收用户的收入
    3. 调用HouseService完成对房屋信息的各种操作

    代码实现分析:

    1. 编写mainMenu()方法,界面显示主菜单。
    2. 编写listHouses()方法,调用业务层的list()方法,界面显示房屋列表信息。
    3. 编写addHouse()方法,界面接收用户输入,创建House对象,调用业务层add()方法。
    4. 编写delHouse()方法,界面接收用户输入的房屋id,调用业务层的del()方法。
    5. 编写updateHouse()方法,界面接收用户输入的房屋id,调用业务层的find()方法查找对象并修改。
    6. 编写findHouse()方法,界面接收用户输入的房屋id,调用业务层的find()方法。
    7. 编写exit()方法,界面接收用户输入的选择。

    源码

    package com.yxz.house.view;
    
    import com.yxz.house.domain.House;
    import com.yxz.house.service.HouseService;
    import com.yxz.house.utils.Utility;
    
    /**
     * 1. View层,显示界面
     * 2. 接收用户的输入
     * 3. 调用HouseService完成对房屋信息的各种操作(CRUD)
     */
    public class HouseView {
        // 属性
        private boolean loop = true;    // 控制显示菜单
        private final HouseService houseService = new HouseService(2); // 声明一个HouseService对象,用于listHouses方法调用其list()方法
    
        /**
         * 显示主菜单的方法
         */
        public void mainMenu() {
            do {
                System.out.println("===================房屋出租系统菜单===================");
                System.out.println("\t\t\t\t 1. 新 增 房 源");
                System.out.println("\t\t\t\t 2. 查 找 房 屋");
                System.out.println("\t\t\t\t 3. 删 除 房 屋 信 息");
                System.out.println("\t\t\t\t 4. 修 改 房 屋 信 息");
                System.out.println("\t\t\t\t 5. 房 屋 列 表");
                System.out.println("\t\t\t\t 6. 退 出 系 统");
                System.out.print("请输入你的选择(1-6):");
                // 接收用户选择
                char key = Utility.readChar();
                switch (key) {
                    case '1' -> addHouse();
                    case '2' -> findHouse();
                    case '3' -> delHouse();
                    case '4' -> updateHouse();
                    case '5' -> listHouses();
                    case '6' -> exit();
                    default -> System.out.println("没有此功能,请重新选择...\n");
                }
            } while (loop);
        }
    
        /**
         * 调用业务层的list()方法,界面显示房屋列表信息
         */
        public void listHouses() {
            System.out.println("-------------------房 屋 列 表 信 息------------------");
            System.out.println("编号\t房主\t电话\t\t\t地址\t\t月租\t状态(未出租/已出租)");
            House[] houses = houseService.list();   // 得到所有房屋信息
    
            for (House house : houses) {
                if (house == null) {
                    break;
                }
                System.out.println(house);
            }
            System.out.println("-------------------房屋列表显示完毕-------------------\n");
        }
    
        /**
         * 界面接收用户输入,创建House对象,调用业务层add()方法
         */
        public void addHouse() {
            System.out.println("-------------------添 加 房 屋 信 息-------------------");
            System.out.print("姓名: ");
            String name = Utility.readString(8);
            System.out.print("电话: ");
            String phone = Utility.readString(11);
            System.out.print("地址: ");
            String address = Utility.readString(16);
            System.out.print("月租: ");
            int rent = Utility.readInt();
            System.out.print("状态: ");
            String state = Utility.readString(3);
    
            // 创建一个新的House对象, 注意id是系统分配的,
            House newHouse = new House(0, name, phone, address, rent, state);
            if (houseService.add(newHouse)) {
                System.out.println("-------------------添 加 房 屋 成 功-------------------\n");
            } else {
                System.out.println("-------------------添 加 房 屋 失 败-------------------\n");
            }
        }
    
        /**
         * 界面接收用户输入的房屋id,调用业务层的del()方法
         */
        public void delHouse() {
            System.out.println("-------------------删 除 房 屋 信 息------------------");
            System.out.print("请输入待删除房屋的编号id(-1退出):");
            int delId = Utility.readInt();
            if (delId == -1) {
                System.out.println("-------------------放弃删除房屋信息-------------------\n");
                return;
            }
            char choice = Utility.readConfirmSelection();   // 该方法有循环判断是否选择y/n的逻辑
            if (choice == 'Y') {
                if (houseService.del(delId)) {
                    System.out.println("------------------删 除 房 屋 成 功-------------------\n");
                } else {
                    System.out.println("--------------------房屋信息不存在--------------------\n");
                }
            } else {
                System.out.println("-------------------放弃删除房屋信息-------------------\n");
            }
        }
    
        /**
         * 根据id查找房屋信息,界面接受用户输入的房屋id,调用业务层find()方法
         */
        public void findHouse() {
            System.out.println("-------------------查 找 房 屋 信 息------------------");
            System.out.print("请输入需要查找的房屋id:");
            int findId = Utility.readInt();
            // 调用find方法
            House house = houseService.find(findId);
            if (house != null) {
                System.out.println(house);
            } else {
                System.out.println("--------------------房屋信息不存在--------------------\n");
            }
        }
    
        /**
         * 修改房屋信息,界面接收要修改的房屋id,调用业务层的find()方法返回要修改的房屋对象并其修改属性
         */
        public void updateHouse() {
            System.out.println("-------------------修 改 房 屋 信 息------------------");
            System.out.print("请选择待修改房屋编号(-1退出):");
            int updateId = Utility.readInt();
            if (updateId == -1) {
                System.out.println("-------------------放弃修改房屋信息-------------------\n");
                return;
            }
            House house = houseService.find(updateId);
            if (house == null) {
                System.out.println("--------------------房屋信息不存在--------------------\n");
                return;
            }
    
            System.out.print("姓名(" + house.getName() + "):");
            String name = Utility.readString(8, "");
            if (!"".equals(name)) {
                house.setName(name);
            }
    
            System.out.print("电话(" + house.getPhone() + "):");
            String phone = Utility.readString(11, "");
            if (!"".equals(phone)) {
                house.setPhone(phone);
            }
    
            System.out.print("地址(" + house.getAddress() + "):");
            String address = Utility.readString(16, "");
            if (!"".equals(address)) {
                house.setAddress(address);
            }
    
            System.out.print("租金(" + house.getRent() + "):");
            int rent = Utility.readInt(-1);
            if (rent != -1) {
                house.setRent(rent);
            }
    
            System.out.print("状态(" + house.getState() + "):");
            String state = Utility.readString(3, "");
            if (!"".equals(state)) {
                house.setState(state);
            }
    
            System.out.println("-------------------修改房屋信息成功-------------------\n");
        }
    
        /**
         * 退出房屋出租系统
         */
        public void exit() {
            // 使用工具类提供的方法
            char c = Utility.readConfirmSelection();
            if (c == 'Y') {
                loop = false;
                System.out.println("=================已退出房屋出租系统===================");
            }
        }
    }
    
  2. HouseService.java类[Service层,业务层]

    定义House[],保存House对象。

    1. 响应HouseView的调用。
    2. 完成对房屋信息的各种操作(增删改查,C-Creat R-Read U-Update D-Delete)

    代码实现分析:

    1. 编写list()方法,返回所有的房屋信息。
    2. 编写add(House newHouse)方法,把newHouse对象加入到houses数组,并返回bool值。
    3. 编写del(int delId)方法,完成真正的删除任务,返回bool值表示删除操作是否成功。
    4. 编写find(int findId)方法,查找指定id的房屋对象,如果有则返回对象,没有则返回null。

    源码

    package com.yxz.house.service;
    
    import com.yxz.house.domain.House;
    
    /**
     * HouseService 业务层
     * 定义House[],保存House对象。
     * 1. 响应HouseView的调用。
     * 2. 完成对房屋信息的各种操作(增删改查,C-Creat R-Read U-Update D-Delete)
     */
    
    public class HouseService {
    
        private House[] houses;
        private int houseNums = 1; // 记录当前有多少房屋信息
        private int idCounter = 1; // 记录当前id增长到哪个值
    
        public HouseService(int size) {
            houses = new House[size]; // 当创建一个HouseService对象,指定数组大小
            houses[0] = new House(1, "jack", "189xxxxxxxx", "上海市", 15000, "未出租");
        }
    
        /**
         * list方法,返回所有房屋信息
         *
         * @return :返回House[]
         */
        public House[] list() {
            return houses;
        }
    
        /**
         * 添加新的房屋对象
         *
         * @param newHouse :新增加的房屋信息
         * @return :表示增加的操作是否成功
         */
        public boolean add(House newHouse) {
            // 判断是否可以继续添加
            if (houseNums == houses.length) { // 数组已满,不能再添加
                // 数组扩容
                House[] tempHouses = new House[houses.length + 1]; // 创建一个临时数组,存放原houses数组的对象
                System.arraycopy(houses, 0, tempHouses, 0, houses.length);
                houses = tempHouses; // 修改原数组引用houses指向的对象地址
            }
            // 把newHouse对象加入到houses[]
            houses[houseNums++] = newHouse;
            // 程序员需要设计一个id自增长的机制,更新newHouse的id
            newHouse.setId(++idCounter);
            return true;
        }
    
        /**
         * 删除指定房屋id的房屋信息
         *
         * @param delId :要删除房屋信息的房屋id
         * @return :表示房屋删除操作是否成功。
         */
        public boolean del(int delId) {
            // 先查询到要删除的房屋信息对应的下标,下标与房屋id不一致
            int index = -1;
            for (int i = 0; i < houseNums; i++) {
                if (delId == houses[i].getId()) {   // 要删除的房屋(id),是数组下标为i的元素
                    index = i;
                }
            }
    
            if (index == -1) {  // 说明delId在数组中不存在
                return false;
            }
    
            for (int i = index; i < houseNums - 1; i++) {// 从被删除的id开始,将后面的房屋信息前移
                houses[i] = houses[i + 1];
            }
            houses[--houseNums] = null; // 将此时的房屋数-1,再把当前最后一个房屋置空
            return true;
        }
    
        /**
         * 根据用户输入的id,查找房屋信息
         *
         * @param findId :房屋id
         * @return :表示查找到的房屋对象
         */
        public House find(int findId) {
            for (int i = 0; i < houseNums; i++) {
                if (findId == houses[i].getId()) {
                    return houses[i];
                }
            }
            return null;
        }
    }
    
  3. House.java类[Model层,domain/数据层]

    1. 一个House对象表示一个房屋信息。

    源码

    package com.yxz.house.domain;
    
    /**
     * House类的对象表示一个房屋信息
     */
    public class House {
        // 属性:编号、房主、电话、地址、月租、状态(未出租/已出租)
        private int id;
        private String name;
        private String phone;
        private String address;
        private int rent;
        private String state;
        
        // 构造器 和 setter/getter方法
        public House(int id, String name, String phone, String address, int rent, String state) {
            this.id = id;
            this.name = name;
            this.phone = phone;
            this.address = address;
            this.rent = rent;
            this.state = state;
        }
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPhone() {
            return phone;
        }
    
        public void setPhone(String phone) {
            this.phone = phone;
        }
    
        public String getAddress() {
            return address;
        }
    
        public void setAddress(String address) {
            this.address = address;
        }
    
        public int getRent() {
            return rent;
        }
    
        public void setRent(int rent) {
            this.rent = rent;
        }
    
        public String getState() {
            return state;
        }
    
        public void setState(String state) {
            this.state = state;
        }
    
        @Override
        public String toString() {
            return id +
                    "\t\t" + name +
                    "\t" + phone +
                    "\t\t" + address +
                    "\t\t" + rent +
                    "\t" + state;
        }
    }
    
  4. Utility.java类[Utility工具类]

    在实际开发中,公司都会提供相应的工具类和开发库,可以提高开发效率。

    1. 了解Utility类的使用

      Utility.java源码

      /**
       * 工具类的作用:
       * 处理各种情况的用户输入,并且能够按照程序员的需求,得到用户的控制台输入。
       */
      
      import java.util.*;
      
      
      public class Utility {
          //静态属性
          private static Scanner scanner = new Scanner(System.in);
      
          /**
           * 功能:读取键盘输入的一个菜单选项,值:1——5的范围
           *
           * @return 1——5
           */
          public static char readMenuSelection() {
              char c;
              for (; ; ) {
                  String str = readKeyBoard(1, false);//包含一个字符的字符串
                  c = str.charAt(0);//将字符串转换成字符char类型
                  if (c != '1' && c != '2' &&
                          c != '3' && c != '4' && c != '5') {
                      System.out.print("选择错误,请重新输入:");
                  } else break;
              }
              return c;
          }
      
          /**
           * 功能:读取键盘输入的一个字符
           *
           * @return 一个字符
           */
          public static char readChar() {
              String str = readKeyBoard(1, false);//就是一个字符
              return str.charAt(0);
          }
      
          /**
           * 功能:读取键盘输入的一个字符,如果直接按回车,则返回指定的默认值;否则返回输入的那个字符
           *
           * @param defaultValue 指定的默认值
           * @return 默认值或输入的字符
           */
          public static char readChar(char defaultValue) {
              String str = readKeyBoard(1, true);//要么是空字符串,要么是一个字符
              return (str.length() == 0) ? defaultValue : str.charAt(0);
          }
      
          /**
           * 功能:读取键盘输入的整型,长度小于2位
           *
           * @return 整数
           */
          public static int readInt() {
              int n;
              for (; ; ) {
                  String str = readKeyBoard(10, false);//一个整数,长度<=10位
                  try {
                      n = Integer.parseInt(str);//将字符串转换成整数
                      break;
                  } catch (NumberFormatException e) {
                      System.out.print("数字输入错误,请重新输入:");
                  }
              }
              return n;
          }
      
          /**
           * 功能:读取键盘输入的 整数或默认值,如果直接回车,则返回默认值,否则返回输入的整数
           *
           * @param defaultValue 指定的默认值
           * @return 整数或默认值
           */
          public static int readInt(int defaultValue) {
              int n;
              for (; ; ) {
                  String str = readKeyBoard(10, true);
                  if (str.equals("")) {
                      return defaultValue;
                  }
      
                  //异常处理...
                  try {
                      n = Integer.parseInt(str);
                      break;
                  } catch (NumberFormatException e) {
                      System.out.print("数字输入错误,请重新输入:");
                  }
              }
              return n;
          }
      
          /**
           * 功能:读取键盘输入的指定长度的字符串
           *
           * @param limit 限制的长度
           * @return 指定长度的字符串
           */
          public static String readString(int limit) {
              return readKeyBoard(limit, false);
          }
      
          /**
           * 功能:读取键盘输入的指定长度的字符串或默认值,如果直接回车,返回默认值,否则返回字符串
           *
           * @param limit        限制的长度
           * @param defaultValue 指定的默认值
           * @return 指定长度的字符串
           */
          public static String readString(int limit, String defaultValue) {
              String str = readKeyBoard(limit, true);
              return str.equals("") ? defaultValue : str;
          }
      
      
          /**
           * 功能:读取键盘输入的确认选项,Y或N
           * 将小的功能,封装到一个方法中.
           *
           * @return Y或N
           */
          public static char readConfirmSelection() {
              System.out.println("请输入你的选择(Y/N): 请小心选择");
              char c;
              for (; ; ) {//无限循环
                  //在这里,将接受到字符,转成了大写字母
                  //y => Y n=>N
                  String str = readKeyBoard(1, false).toUpperCase();
                  c = str.charAt(0);
                  if (c == 'Y' || c == 'N') {
                      break;
                  } else {
                      System.out.print("选择错误,请重新输入:");
                  }
              }
              return c;
          }
      
          /**
           * 功能: 读取一个字符串
           *
           * @param limit       读取的长度
           * @param blankReturn 如果为true ,表示 可以读空字符串。
           *                    如果为false表示 不能读空字符串。
           *                    <p>
           *                    如果输入为空,或者输入大于limit的长度,就会提示重新输入。
           * @return
           */
          private static String readKeyBoard(int limit, boolean blankReturn) {
      
              //定义了字符串
              String line = "";
      
              //scanner.hasNextLine() 判断有没有下一行
              while (scanner.hasNextLine()) {
                  line = scanner.nextLine();//读取这一行
      
                  //如果line.length=0, 即用户没有输入任何内容,直接回车
                  if (line.length() == 0) {
                      if (blankReturn) return line;//如果blankReturn=true,可以返回空串
                      else continue; //如果blankReturn=false,不接受空串,必须输入内容
                  }
      
                  //如果用户输入的内容大于了 limit,就提示重写输入
                  //如果用户如的内容 >0 <= limit ,我就接受
                  if (line.length() < 1 || line.length() > limit) {
                      System.out.print("输入长度(不能大于" + limit + ")错误,请重新输入:");
                      continue;
                  }
                  break;
              }
      
              return line;
          }
      }
      
    2. 测试Utility类

  5. HouseRentApp.java程序入口, 直接创建对象调用方法

    源码

    package com.yxz.house;
    
    import com.yxz.house.view.HouseView;
    
    public class HouseRentApp {
        public static void main(String[] args) {
            // 创建HouseView对象,并显示主菜单 -> 程序(系统)入口
            new HouseView().mainMenu();
        }
    }
    

第二阶段

第二阶段 目标:提升编程能力

1、面向对象编程(高级)

1.1、类变量和类方法

类变量和类方法使用static关键字修饰,也称静态变量(静态属性)静态方法。类变量是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。

定义类变量/类方法的语法:

访问修饰符 static 数据类型 变量名;// 推荐使用
static 访问修饰符 数据类型 变量名;

访问修饰符 static 数据返回类型 方法名() {} // 推荐使用
static 访问修饰符 数据返回类型 方法名() {}

如何访问类变量/类方法:(静态变量/静态方法的访问修饰符的访问权限和范围与普通成员一样,遵守访问规则)

类名.类变量名;	// 推荐使用
对象名.类变量名;

类名.类方法名();	// 推荐使用
对象名.类方法名();

特点:

  1. 类变量和类方法会被该类的所有的对象实例共享
  2. static类变量在类加载时就生成并初始化,即使没有创建对象,只要类加载了,就可以使用类变量
  3. 在JDK8之前的版本中,静态变量是存放在方法区的静态域中;在JDK8之后的版本中,方法区中的类信息加载后会在堆中开辟属于该类的一个Class对象实例,属于该类的静态变量会存放在Class对象实例的尾部。
  4. 类变量的生命周期是随着类的加载开始,随着类消亡而销毁。
  5. 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区。
  6. 类方法中不能使用和对象有关的关键字,如:this,super
  7. 类方法中只能访问静态变量或静态方法
  8. 普通成员方法既可以访问非静态变量(方法),也可以访问静态变量(方法)。
  9. 静态方法可以被子类继承,但是不能被子类重写(覆盖) -> 静态方法属于类成员

何时使用类变量/类方法:

  1. 当一个变量需要该类所有对象实例共享时,使用类变量。
  2. 当方法中不涉及到任何和对象相关的成员,可以将方法设计成静态方法,提高开发效率 -> 不希望创建对象实例,也可以调用方法时(当作工具使用)。

1.2、理解main方法语法

public static void main(String[] args){}

说明:

  1. main()方法是Java虚拟机调用。

  2. Java虚拟机需要调用类的main()方法 -> main()方法的访问权限必须是public

  3. Java虚拟机在执行main()方法时不必创建对象 -> main()方法必须是static

  4. main()方法接收String类型的数组参数,该数组中保存执行Java命令时所传递给运行的类的参数。

  5. 语法:java 执行的程序(带有main()方法的程序) 参数1 参数2 参数3...。如图:

    main方法args传参

  6. 在main()方法中,可以直接调用main()方法所在类的静态方法和静态属性。但是不能直接访问该类的非静态成员,要创建该类的对象实例去访问。

  7. 在IDEA中,传入main()方法的参数:构建 -> 编辑配置 -> 对应程序

    IDEA传入main方法args参数

1.3、代码块

代码化块又称为初始化块,属于类中的成员 -> 类的一部分,类似于方法,将逻辑语句封装在方法体中,通过{}包装起来。

但和方法不同,没有方法名,没有返回值类型,没有参数,只有方法体,而且不能通过对象或类显示调用,而是在加载类或创建对象时隐式调用。

基本语法:

[修饰符] {
    代码;
};

说明:

  1. 修饰符要么static,要么不写。
  2. 代码块分为两类。使用static修饰的叫静态代码块,没有static修饰的叫普通代码块。
  3. 代码可以为任何逻辑语句(输入、输出、方法调用、控制结构等)。
  4. 结尾的;可写可不写。

代码块的好处:

  1. 相当于另外一种形式的构造器(对构造器的补充机制),可以做初始化的操作。
  2. 如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的重用性。
  3. 代码块的调用优先于构造器,不管调用哪个构造器,都会先调用代码块的内容。

注意:

  1. static代码块的作用是对类进行初始化,static代码块随着类的加载而执行且只会执行一次。如果是非静态代码块,每创建一个对象就执行一次

  2. 类什么时候被加载

    1. 创建对象实例时(new)。
    2. 创建子类对象实例时,父类也会被加载。
    3. 使用类的静态成员时(静态变量、静态方法、静态代码块)。
  3. 非静态代码块,在创建对象实例时会被隐式的调用如果只是使用类的静态成员时,普通代码块并不会执行

  4. 创建一个对象实例时,在一个类的调用顺序:静态(代码块、属性) -> 非静态(代码块、属性) -> 构造器

    1. 调用静态代码块和静态属性初始化静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按照从上往下定义的顺序调用。
    2. 调用普通代码块和普通属性的初始化。普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通属性初始化,按照从上往下定义的顺序调用。
    3. 调用构造方法
    package com.yxz.oop_high;
    
    public class CodeBlock {
        public static void main(String[] args) {
            // 假设以下每次运行时只执行一个输出语句
    //        System.out.println(A.age);
            /*
            分析:加载一次类信息,访问静态成员,不创建对象实例
            1. 调用静态代码块,输出:A的静态代码块 被调用
            2. 调用静态变量,输出:100
             */
    
    //        System.out.println(new A().name);
            /*
              分析:加载一次类信息,创建对象实例,访问非静态成员
              1. 加载类信息,调用静态代码块,输出:A的静态代码块 被调用
              2. 创建对象实例,调用A类的普通代码块,输出:A的非静态代码块 被调用
              3. 调用A的构造器,实例化对象,输出:A的构造器 被调用
              4. 访问普通成员,输出:abc
             */
    
    //        System.out.println(B.salary);
            /*
              分析:加载一次类信息,调用静态成员,不创建对象实例
              1. 先调用父类的静态代码块,完成父类初始化,输出:A的静态代码块 被调用
              2. 加载类信息,调用本类的静态代码块,完成子类初始化,输出:B的静态代码块 被调用
              3. 访问静态变量,输出:1314.52
             */
    
            System.out.println(new B().count);
            /*
              分析:加载一次类信息,创建对象实例,访问非静态成员
              1. 先完成父类信息的加载,调用父类的静态代码块,输出:A的静态代码块 被调用
              2. 完成子类信息的加载,调用本类的静态代码块,输出:B的静态代码块 被调用
              3. 需要创建对象实例,先完成父类非静态信息加载,输出:A的非静态代码块 被调用
              4. 要创建子类对象,先调用父类构造器完成父类的初始化,输出:A的构造器 被调用
              5. 需要创建本类对象实例,先完成本类的非静态信息加载,输出:B的非静态代码块 被调用
              6. 调用完成子类构造器,完成子类对象初始化,输出:B的构造器 被调用
              7. 访问本类的非静态信息,输出:10
             */
        }
    }
    
    class A {
        public static int age = 100; // 静态变量
    
        static {
            // this.age = 120; // 错误,静态代码块内不能使用this/super和对象有关的关键字
            System.out.println("A的静态代码块 被调用...");
        }
    
        public String name = "abc";
    
        {
            // 非静态代码块在创建对象实例时会被调用
            System.out.println("A的非静态代码块 被调用...");
        }
    
        public A() {
            // 默认隐藏的语句
            // 1. super();
            // 2. 普通代码块和普通属性的初始化;
            System.out.println("A的构造器 被调用...");
        }
    }
    
    class B extends A {
        public static double salary = 1314.520; // 静态变量
    
        static {
            System.out.println("B的静态代码块 被调用...");
        }
    
        public int count = 10;
    
        {
            System.out.println("B的非静态代码块 被调用...");
        }
    
        public B() {
            // 默认隐藏的语句
            // 1. super();
            // 2. 普通代码块和普通属性的初始化;
            System.out.println("B的构造器 被调用...");
        }
    }
    
  5. 创建一个子类对象时(继承关系),他们的静态代码块、静态属性初始化、普通代码块、普通属性初始化、构造器的调用顺序

    1. 父类的静态代码块和静态属性(优先级一样,按定义顺序执行)。
    2. 子类的静态代码块和静态属性(优先级一样,按定义顺序执行)。
    3. 父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)。
    4. 父类的构造器。
    5. 子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)。
    6. 子类的构造器。
    package com.yxz.oop_high;
    
    public class CodeBlockDetail {
        public static void main(String[] args) {
            new B02();
        }
    }
    
    class A02 { //父类
        private static int n1 = getVal01();
    
        static {
            System.out.println("A02的一个静态代码块..");//(2)
        }
    
        public int n3 = getVal02();//普通属性的初始化
    
        {
            System.out.println("A02的一个普通代码块..");//(5)
        }
    
        public A02() {//构造器
            //隐藏
            //super()
            //普通代码和普通属性的初始化......
            System.out.println("A02的构造器");//(7)
        }
    
        public static int getVal01() {
            System.out.println("A02的静态方法getVal01");//(1)
            return 10;
        }
    
        public int getVal02() {
            System.out.println("A02的普通方法getVal02");//(6)
            return 10;
        }
    }
    
    class B02 extends A02 { // 子类
    
        private static int n3 = getVal03();
    
        static {
            System.out.println("B02的一个静态代码块..");//(4)
        }
    
        public int n5 = getVal04();
    
        {
            System.out.println("B02的一个普通代码块..");//(9)
        }
    
        //一定要慢慢的去品..
        public B02() {//构造器
            //隐藏了
            //super()
            //普通代码块和普通属性的初始化...
            System.out.println("B02的构造器");//(10)
        }
    
        public static int getVal03() {
            System.out.println("B02的静态方法getVal03");//(3)
            return 10;
        }
    
        public int getVal04() {
            System.out.println("B02的普通方法getVal04");//(8)
            return 10;
        }
    }
    
  6. 静态代码块只能调用静态成员,普通代码块可以调用任意属性。

1.4、单例设计模式

设计模式(共23种):在大量的实践中总结和理论化之后优选的代码结构、编程风格以及解决问题的思考方式。

单例(单个的实例)模式(singleton pattern):采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

单例模式(8种)的其中两种方式:饿汉式、懒汉式。

  1. 饿汉式:在类中创建静态对象 -> 当加载类信息时就会自动创建该对象。即有可能还不需要使用该对象,但是类对象已经创建完毕 -> 很着急的饿汉

    实现步骤:

    1. 构造器私有化 -> 防止用户使用new创建对象。
    2. 类的内部创建对象。为了能够在静态方法中访问该对象,需要使用static修饰该对象 -> 该类的内部对象是static的
    3. 向外暴露一个静态的公共方法getInstance,返回一个类的对象实例

    代码实现:

    package com.yxz.oop_high.single_;
    
    /**
     * 实现饿汉式的单例模式
     */
    public class SingleTon01 {
        public static void main(String[] args) {
            // System.out.println(GirlFriend.n1); //当访问类变量时,就已经加载了类信息 -> 静态对象已经创建完毕
            GirlFriend girlFriend1 = GirlFriend.getInstance();
            GirlFriend girlFriend2 = GirlFriend.getInstance();
            System.out.println(girlFriend1 == girlFriend2); // true,是同一个对象地址
        }
    }
    
    /**
     * 只能有一个女朋友(单例模式)
     */
    class GirlFriend {
        // public static int n1 = 100;
        // 为了保证在静态方法getInstance()中能够返回girlFriend对象,要用static修饰
        private static GirlFriend girlFriend = new GirlFriend("宝贝"); // 第二步:在类的内部直接创建
        private String name;
        // 如何保证用户只能创建一个 GirlFriend 对象
        // 步骤[单例模式-饿汉式]
        // 1.构造器私有化
        // 2.在类的内部直接创建
        // 3.提供一个公共的static方法,返回一个GirlFriend对象
    
        private GirlFriend(String name) { // 第一步:构造器私有化
            // System.out.println("构造器被调用...");
            this.name = name;
        }
    
        public static GirlFriend getInstance() { // 第三步:提供一个公共的static方法,返回一个GirlFriend对象
            return girlFriend; // 返回一个对象
        }
    
        @Override
        public String toString() {
            return "GirlFriend{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    

    饿汉式缺点:当访问类的其他静态成员时,不需要创建该类对象,但是该类静态对象已经在第一次加载类信息时已经创建完毕 -> 可能造成已经创建了对象,但是没有使用的问题。

  2. 懒汉式:现在类中声明一个静态对象属性但不实例化对象,在公共的静态方法中创建对象 -> 只有使用getInstance方法时才创建对象 -> 懒汉

    实现步骤:

    1. 构造器私有化 -> 防止用户使用new创建对象。
    2. 类的内部定义一个静态属性对象但不实例化。
    3. 向外暴露一个静态的公共方法getInstance,创建并返回一个类的对象实例只有第一次调用该方法时才创建对象,否则还返回上次创建的对象

    代码实现:

    package com.yxz.oop_high.single_;
    
    /**
     * 实现懒汉式的单例模式
     */
    public class SingleTon02 {
        public static void main(String[] args) {
            // System.out.println(BoyFriend.n1); // 访问其他类成员时,加载类信息但不会创建对象
    
            BoyFriend boyFriend1 = BoyFriend.getInstance();
            System.out.println(boyFriend1);
            BoyFriend boyFriend2 = BoyFriend.getInstance();
            System.out.println(boyFriend1 == boyFriend2); // true;
        }
    }
    
    /**
     * 只能有一个男朋友(单例模式)
     */
    class BoyFriend {
        // public static int n1 = 100;
        private static BoyFriend boyFriend; // 第二步:定义一个static静态属性对象,此时对象置空
        private String name;
    
        // 步骤[单例模式-懒汉式]
        // 1.构造器私有化
        // 2.定义一个static静态属性对象
        // 3.提供一个公共的static方法,可以放回一个BoyFriend对象
        private BoyFriend(String name) { // 第一步:构造器私有化
            // System.out.println("构造器被调用...");
            this.name = name;
        }
    
        public static BoyFriend getInstance() {// 第三步:提供一个公共的static方法,返回一个GirlFriend对象
            if (boyFriend == null) { // 判断是否创建了对象
                boyFriend = new BoyFriend("亲爱的");
            }
            return boyFriend;
        }
    
        @Override
        public String toString() {
            return "BoyFriend{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    

饿汉式和懒汉式的区别:

  1. 创建对象时机不同。饿汉式是在类加载就创建了对象实例,懒汉式是在使用时才创建。
  2. 饿汉式不存在线程安全问题,懒汉式存在线程安全问题。
  3. 饿汉式存在资源浪费的可能,懒汉式不存在资源浪费问题。
  4. 在JavaSE标准类中,java.lang.Runtime是经典的单例模式。

1.5、final 关键字

final关键字可以修饰类、属性、方法和局部变量

适用场景(作用):

  1. final修饰类:该类无法被继承 -> final修饰的类没有子类。
  2. final修饰方法:该方法无法被子类覆盖/重写(override)。
  3. final修饰类的属性:该属性的值无法被修改。
  4. final修饰局部变量:该局部变量的值无法修改。

注意:

  1. final修饰的属性又叫常量属性,一般用XX_XX_XX来命名(全部大写字母)。

  2. final修饰的属性必须赋初值,并且以后不能在修改,赋值可以在如下位置:

    1. 定义时赋值。
    2. 在普通代码块中。
    3. 在构造器中。
  3. 如果final修饰的属性是静态的,则初始化的位置只能是:

    1. 定义时。
    2. 静态代码块中
  4. final类不能被继承,但是可以实例化对象。

  5. 如果类不是final类,但是含有final方法,则该final方法虽然不能重写,但是可以被继承(遵守继承机制)。

  6. 一般地,如果一个类是final类,则没必要将该类的方法修饰成final方法 -> final类无子类,不会重写方法。

  7. final不能修饰构造器。

  8. final和static往往搭配使用,效率更高,调用final static修饰的成员时不会导致类加载,底层编译器做了优化处理

    public class Final01 {
        public static void main(String[] args) {
            System.out.println(A.n1); // 输出内容:10000
        }
    }
    
    class A {
        public final static int n1 = 10000;
        static {
            System.out.println("A 的静态代码块被执行"); // 不会执行该语句
        }
    }
    
  9. 包装类(IntegerDoubleFloatLongBooleanStringByte等)都是final类,不能被继承 -> 包装类没有子类

1.6、抽象类

当父类的某些方法,需要声明并让子类继承,但是又不确定方法如何具体实现时,可以将其声明为抽象方法(abstract method),由子类重写实现该方法,那么这个类就是抽象类(abstract class)

说明:

  1. 使用abstract关键字来修饰一个类时,该类就叫抽象类。

    抽象类的基本语法:访问修饰符 abstract 类名{}

  2. 使用abstract关键字来修饰一个方法时,该方法就叫抽象方法 -> 抽象方法没有方法体

    基本语法:访问修饰符 abstract 返回值类型 方法名(参数列表);

  3. 抽象类的价值更多作用在于设计,设计者设计好后让子类继承并实现抽象类。

  4. 抽象类是面试官常问知识点,在框架和设计模式使用较多。

注意:

  1. 抽象类不能被实例化 -> 不能创建抽象类的对象

  2. 抽象类不一定要包含抽象方法,还可以有实现的方法,-> 抽象类可以没有abstract方法

  3. 抽象方法一定在抽象类里面 -> 一旦类包含了抽象方法,则这个类必须声明为abstract

  4. abstract只能修饰类和方法,不能修饰属性和其他的。

  5. 抽象类可以有任意成员(抽象类本质还是类),比如:非抽象方法、构造器、静态属性等。

  6. 抽象方法不能有方法体。

  7. 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非这个类也声明为abstract类。所谓实现方法就是有方法体。

  8. 抽象方法不能使用private、final和static来修饰,因为这些关键字都是和方法重写机制相违背。

    private修饰的方法不能被子类访问 -> 子类无法重写父类private方法 -> 和abstract方法要求子类继承并重写实现方法的本意违背

    final修饰的方法不能被子类继承 -> 子类无法继承父类final方法 -> 和abstract方法要求子类继承并重写实现方法的本意违背

    static修饰的方法属于类方法且不能被子类重写 -> 不创建类对象也能使用类名调用的方法 -> 和abstract方法没有方法体相悖

抽象类的最佳实践-模板设计模式

任务需求:

  1. 有多个类,完成不同的任务job
  2. 要求能够得到各自完成任务的时间。

方案一:基本思维流程:在不同类的job方法里面计算时间

A.java类文件,完成任务1

package com.yxz.oop_high.abstract_;

// 任务1:计算1+2+...+100000000所花费的时间
public class A {
    public void job() {
        // 得到开始的时间
        long start = System.currentTimeMillis();
        long num = 0;
        for (int i = 1; i < 100000000; i++) {
            num += i;
        }
        // 得到结束的时间
        long end = System.currentTimeMillis();
        System.out.println("执行的时间: " + (end - start));
    }
}

B.java类文件,完成任务2

package com.yxz.oop_high.abstract_;

// 任务2:计算1*2*...*100所花费的时间
public class B {
    public void job() {
        // 得到开始的时间
        long start = System.currentTimeMillis();
        long num = 1;
        for (int i = 1; i < 10000000; i++) {
            num *= i;
        }
        // 得到结束的时间
        long end = System.currentTimeMillis();
        System.out.println("执行的时间: " + (end - start));
    }
}

方案二:将方案一的代码重复部分抽取出来封装成单独的方法calculateTimes(),然后在calculateTimes()方法中调用job()方法 -> 提高代码复用性

A.java类文件,完成任务1

package com.yxz.oop_high.abstract_;

// 任务1:计算1+2+...+100000000所花费的时间
public class A {
    // 代码改进方案一:将重复的代码抽出来封装成一个新方法,在新方法中调用计算任务job,如下
    public void calculateTimes() {
        long start = System.currentTimeMillis();
        // 调用任务
        job();
        long end = System.currentTimeMillis();
        System.out.println("执行的时间: " + (end - start));
    }

    public void job() {
        long num = 0;
        for (int i = 1; i < 100000000; i++) {
            num += i;
        }
    }
}

B.java类文件,完成任务2

package com.yxz.oop_high.abstract_;

// 任务2:计算1*2*...*100所花费的时间
public class B {
    // 代码改进方案一:将重复的代码抽出来组成一个新方法,在新方法中调用计算任务job
    public void calculateTimes() {
        long start = System.currentTimeMillis();
        // 调用任务
        job();
        long end = System.currentTimeMillis();
        System.out.println("执行的时间: " + (end - start));
    }

    public void job() {
        long num = 1;
        for (int i = 1; i < 10000000; i++) {
            num *= i;
        }
    }
}

方案三:在方案二的基础上,抽象一个父类Template,在父类声明一个抽象方法job(),将子类的封装方法calculateTimes()放在父类中,并在该方法中调用抽象方法job()。根据继承机制,当子类重写实现了父类的抽象方法job(),也会继承父类的calculateTimes()方法,可以直接调用该方法实现模板设计模式。

Template.java模板类文件(父类)

package com.yxz.oop_high.abstract_;

// 模板类
abstract public class Template {
    public abstract void job(); // 抽象方法,由子类重写实现

    public void calculateTimes() { // 实现的方法
        long start = System.currentTimeMillis();
        job(); // 调用抽象方法,运行时会动态绑定子类重写的方法!!!!
        long end = System.currentTimeMillis();
        System.out.println("执行的时间: " + (end - start));
    }
}

A.java类文件,子类 完成任务1的job()方法实现

package com.yxz.oop_high.abstract_;

// 任务1:计算1+2+...+100000000所花费的时间
public class A extends Template {
    @Override
    public void job() { // 重写了父类的抽象方法
        long num = 0;
        for (int i = 1; i < 100000000; i++) {
            num += i;
        }
    }
}

B.java类文件,完成任务2

package com.yxz.oop_high.abstract_;

// 任务2:计算1*2*...*100所花费的时间
public class B extends Template {
    @Override
    public void job() { // 重写了父类的抽象方法
        long num = 1;
        for (int i = 1; i < 10000000; i++) {
            num *= i;
        }
    }
}

Abstract01.java测试类

package com.yxz.oop_high.abstract_;

public class Abstract01 {
    public static void main(String[] args) {
        A a = new A();
        a.calculateTimes(); // 动态绑定机制
        B b = new B();
        b.calculateTimes(); // 动态绑定机制
    }
}

1.7、接口

接口(Interface)就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,根据具体情况把这些方法写出来。

接口的基本语法:

interface 接口名 {
	属性
    方法(1.抽象方法 2.默认实现方法 3.静态方法)
}

class 类名 implements 接口名 {
    本类属性
    本类方法
    必须实现接口的所有抽象方法
}

说明:

  1. 在JDK7.0之前 接口里的所有方法都没有方法体 -> 都是抽象方法
  2. 在JDK8.0之后接口可以有静态方法默认方法(需要使用default关键字修饰),即接口中只有静态方法和默认方法有方法体。
  3. 在接口中的抽象方法可以省略abstract关键字。

注意:

  1. 接口不能被实例化

  2. 接口中的所有方法是public方法(可以省略public关键字),接口中抽象方法可以不用abstract修饰。

  3. 一个普通类实现接口,就必须将该接口的所有方法都实现。在IDEA中,将光标停在实现接口的类名上,使用Alt + Enter快捷键可以快速实现接口所有方法。

  4. 抽象类实现接口,可以不用实现接口的方法

  5. 一个类可以同时实现多个接口。

  6. 接口中的属性只能是public static final属性,而且属性必须初始化。如:int n1 = 1; =等价于=> public final static int n1 = 1

  7. 接口中属性和静态方法的访问形式:接口名.属性名, 接口名.方法名()

  8. 通过实现类的对象,调用接口中的默认方法。如果实现类重写了接口中的默认方法,则调用时还是调用重写后的默认方法(和继承的重写一致)。

    interface A {
        int n = 10; // 等价于 public final static int n = 10;
        static void hi() {
            System.out.println("hi");
        }
        public default void method() { // 默认方法必须有default关键字
            System.out.println("默认方法1");
        }
        default void method2() { // 省略了public关键字,权限依然是public
            System.out.println("默认方法2");
        }
    }
    
    public class Test implements A {
        public static void main(String[] args) {
             A.hi();
             System.out.println(A.n);
        }
    }
    
  9. 接口不能继承其他的类,但是可以继承多个别的接口。接口与接口之间使用extends继承,接口与类之间使用implements实现。

    interface A {}
    interface B {}
    interface C extends A, B{} // 接口继承不能使用implements
    
  10. 接口的修饰符只能是public和默认,这点和类的修饰符一样。

接口 VS 继承

接口是对Java中单根继承机制的补充。

  1. 继承

    当子类继承父类之后,子类就自动拥有父类的能力即父类所有(可以访问的)属性和方法。(先天拥有的能力)

    继承的价值主要在于:解决代码的复用性和可维护性。

    继承需要满足is-a的关系。

  2. 接口

    子类需要扩展功能,可以通过实现接口的方式来扩展。(后天学习拥有的能力)

    接口的价值主要在于:设计,设计好各种规范(方法),让其它类实现这些方法。

    接口比继承更加灵活,只需要满足like-a的关系。

    接口在一定程度上实现代码解耦[接口的规范性+动态绑定机制]。

接口的多态特性

  1. 多态参数

    接口引用可以指向实现了接口的类的对象实例 -> 接口名 接口类型的引用名 = new 实现接口的类名();

    案例:手机类和相机类实现UsbInterface,可以将phone对象和camera对象传入接口引用.

    UsbInterface.java接口文件

    package com.yxz.oop_high.interface_;
    
    public interface UsbInterface {
        // 规定接口的相关方法
        public void start();
        public void stop();
    }
    

    Camera.java类文件

    package com.yxz.oop_high.interface_;
    
    public class Camera implements UsbInterface { // 实现接口的所有抽象方法
        @Override
        public void start() {
            System.out.println("相机开始工作...");
        }
    
        @Override
        public void stop() {
            System.out.println("相机停止工作...");
        }
    }
    

    Phone.java类文件

    package com.yxz.oop_high.interface_;
    
    // Phone类 实现UsbInterface -> 需要实现UsbInterface接口 规定/声明的方法
    public class Phone implements UsbInterface { // 实现接口方法
        @Override
        public void start() {
            System.out.println("手机开始工作...");
        }
    
        @Override
        public void stop() {
            System.out.println("手机停止工作...");
        }
    }
    

    Computer.java类文件,创建work(UsbInterface usbInterface)方法 -> 根据传入的不同实现接口的类的对象,调用不同的方法 -> 接口的多态

    package com.yxz.oop_high.interface_;
    
    public class Computer {
        // 编写一个方法,计算机工作
        public void work(UsbInterface usbInterface) { // 将设备接入到计算机
            // 通过接口,来调用方法 -> 接口的多态
            usbInterface.start(); // 动态绑定机制:根据传入的不同实现接口的类的对象,调用不同的start方法
            usbInterface.stop(); // 动态绑定机制:根据传入的不同实现接口的类的对象,调用不同的stop方法
        }
    }
    

    Interface01.java测试类

    package com.yxz.oop_high.interface_;
    
    public class Interface01 {
        public static void main(String[] args) {
            // 创建对象
            // Phone 实现了 UsbInterface 接口
            Phone phone = new Phone();
            // Camera 实现了 UsbInterface 接口
            Camera camera = new Camera();
            // 创建计算机
            Computer computer = new Computer();
            computer.work(phone);   // 将手机接入计算机,然后工作 -> 多态+动态绑定机制
            System.out.println("=================");
            computer.work(camera);  // 将相机接入计算机,然后工作 -> 多态+动态绑定机制
            
            // 接口的多态
            // 接口类型的变量 phone2 可以指向实现了UsbInterface接口类的对象实例
            UsbInterface phone2 = new Phone();
            UsbInterface camera2 = new Camera();
        }
    }
    
  2. 多态数组

    案例:在Usb(接口)数组中,存放Phone和Camera对象,Phone类有一个特有方法call()。遍历Usb数组,如果是Phone对象,除了调用Usb接口定义的方法work()外,还要调用特有方法call()。

    public class InterfacePloyArr {
        public static void main(String[] args) {
            // 接口数组,多态数组
            Usb[] usbs = new Usb[2];
            usbs[0] = new Phone(); // 接口多态
            usbs[1] = new Camera(); // 接口多态
            
            for(int i = 0; i < usbs.length; i++) {
                usbs[i].work(); // 动态绑定
                // 进行类型判断,向下转型
                if (usbs[i] instanceof Phone) { // 判断其运行类型是否为Phone
                    ((Phone)usbs[i]).call(); // 向下转型
                }
            }
        }
    }
    
    interface Usb {
        void work();
    }
    
    class Phone implements Usb {
        // 特有方法call
        public void call() {
            System.out.println("手机可以打电话...");
    	}
        // 实现接口的方法
        @Override
        public void work() {
            System.out.println("手机工作中...");
        }
    }
    
    class Camera implements Usb {
        // 实现接口的方法
        @Override
        public void work() {
            System.out.println("相机工作中...");
        }
    }
    
  3. 接口存在多态传递现象

    如果一个类实现了的接口继承了其他接口,那么该类相当于也实现了其他接口。如下:

    public class InterfacePloyPass {
        public static void main(String[] args) {
            // 接口类型的变量可以指向实现了该接口的类的对象实例
            IG ig = new Teacher();
            // 如果IG 继承了 IH 接口,而Teacher类实现了 IG接口
            // 那么 就相当于Teacher也实现了IH接口 -> 接口的多态传递现象
            IH ih = new Teacher(); // 虽然Teacher类没有实现IH接口,但是IG接口继承了IH接口
        }
    }
    
    interface IH {
        void hi();
    }
    interface IG extends IH {} // 接口继承
    class Teacher implements IG {
        @Override
        public void hi() {}// 要求必须实现接口的方法
    }
    

1.8、内部类

内部类(inner class):一个类的内部又完整的嵌套了另一个类结构,被嵌套的类就是内部类(inner class),嵌套其他类的类称为外部类(outer class)。是类的第五大成员(类的五大成员:1.属性 2.构造器 3.成员方法 4.代码块 5.内部类)。内部类最大的特点就是可以直接访问外部类的私有属性,并且可以体现类与类之间的包含关系。

基本语法:

class Outer { // 外部类
    class Inner { // 内部类
    }
}
class Other { // (外部)其他类
}

内部类的分类

  1. 定义在外部类的局部位置上,如:方法体内

    1. 局部内部类(有类名)

      局部内部类的位置通常在方法体内,也可以在代码块中。局部内部类本质上还是一个类 -> 也可以有类的五大成员

      特点:

      1. 可以直接访问外部类的所有成员,包括私有成员

      2. 不能添加访问修饰符,因为它的地位就是一个局部变量 -> 局部变量不能用访问修饰符。但是可以用final修饰,不让其他内部类继承, 也可以使用abstract修饰成抽象局部内部类.

      3. 作用域:仅仅在定义它的方法体内或代码块中

      4. 局部内部类访问外部类的成员的方式:直接访问。无需借助类名或对象。

      5. 外部类 -- 访问 --> 局部内部类的成员,方式:在含有局部内部类的方法中创建内部类对象,内部类对象调用内部类成员。在其他外部类创建该外部类对象然后调用含有内部类的方法。

      6. 外部其他类 -- 不能访问 --> 局部内部类(地位局部变量)。

      7. 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用外部类名.this.成员访问。

        外部类名.this本质上是一个外部类对象,哪个对象调用了含有内部类的方法,外部类名.this就指向了哪个对象。

      局部内部类的测试代码

      package com.yxz.oop_high.innerclass;
      
      public class LocalInnerClass {
          public static void main(String[] args) {
              Outer02 outer02 = new Outer02();
              outer02.m1(); // 调用含有局部内部类的方法
              System.out.println("outer02的hashCode = " + outer02); // 输出和内部类的方法中输出一样的hashCode -> 同一个对象
          }
      }
      
      class Outer02 { // 外部类
          private int n1 = 100;
          private int n2 = 300;
          public void m1() { // 方法
              // 局部内部类是定义在外部类的局部位置,通常在方法体内
              final class Inner02 { // 局部内部类(本质上还是一个类)
                  private int n1 = 123;
                  // 可以访问外部类的所有成员,包括私有的
                  public void f1() {
                      System.out.println("n2 = " + n2);
                      m2(); // 可以访问外部类的所有成员
                      System.out.println("内部类的n1 = " + n1);
                      // 内部类和外部类的成员重名时,要访问外部类成员,使用外部类名.this.成员名
                      // Outer02.this本质就是外部类的对象,即哪个对象调用了含有内部类方法m1(),就指向哪个对象
                      System.out.println("外部类的n1 = " + Outer02.this.n1);
                      System.out.println("Outer02.this的hashCode = " + Outer02.this);
                  }
              }
              // 外部类在含有局部内部类的方法中可以创建局部内部类对象,然后调用方法即可
              Inner02 inner02 = new Inner02();
              inner02.f1(); // 调用内部类的方法
      //        class Inner03 extends Inner02{} // 无法继承final类
          }
      
          private void m2() {
              System.out.println("m2()被调用...");
          }
      }
      
    2. 匿名内部类(没有类名非常常用)

      匿名内部类本质:1.类 2.局部内部类(方法体中或者代码块中) 3.匿名,无法直接看到类名 4.是一个对象

      匿名内部类的基本语法:

      class Outer { // 外部类
          public void method() {
              new 类名(参数列表) { // 基于类的匿名内部类
                  类体;
              };
              new 接口() { // 基于接口的匿名内部类
                  类体;
              };
              new 抽象类(参数列表) { // 基于抽象类的匿名内部类
                  类体;
                  必须实现抽象类的抽象方法;
              };
          }
      }
      

      匿名内部类的测试代码

      package com.yxz.oop_high.innerclass;
      
      interface IA {
          void cry();
      }
      
      /**
       * 匿名内部类的演示
       */
      public class AnonymousInnerClass {
          public static void main(String[] args) {
              Outer03 outer03 = new Outer03();
              outer03.method();
              outer03.method();
          }
      }
      
      class Outer03 {
          private int n1 = 10;
      
          public void method() { // 方法
              // 基于接口的匿名内部类
              // 1.需求:想使用IA接口,并创建对象且只使用一次,不用类实现接口的方式
              // 使用匿名内部类简化开发
              // tiger的编译类型:IA
              // tiger的运行类型:匿名内部类:XXXX => Outer03$1
              /*
              匿名内部类Outer03$1的底层源码:由系统分配匿名内部类的类名
                  class Outer03$1 implements IA {
                      @Override
                      public void cry() {
                          System.out.println("老虎叫...");
                      }
                  }
               */
              // JDK底层在创建匿名内部类Outer03$1时,立即马上创建了Outer03$1的对象实例,并且把对象地址返回给tiger
              // 匿名内部类使用一次就消失,不能再使用,但是接口引用指向的对象还存在
              IA tiger = new IA() {
                  @Override
                  public void cry() {
                      System.out.println("老虎叫...");
                  }
              };
              System.out.println("tiger的运行类型:" + tiger.getClass());
              tiger.cry();
      
              // 基于类的匿名内部类
              // 1. father的编译类型:Father
              // 2. father的运行类型:XXXX => 底层系统分配类名Outer03$2
              // 3. 底层会创建匿名内部类Outer03$2
              // 4. 同时返回了匿名内部类Outer03$2的对象实例给father
              // 5. 参数列表jack会自动传递给Father类的构造器
              /*
                  class Outer03$2 extends Father{ // 有继承关系
                  }
               */
              Father father = new Father("jack") {
                  @Override
                  public void test() { // 动态绑定机制
                      System.out.println("匿名内部类重写了test方法");
                  }
              };
              System.out.println("father的运行类型 = " + father.getClass());
              father.test(); // 运行类型是匿名内部类Outer$2,调用test方法会动态绑定匿名内部类重写的test方法
      
              // 基于抽象类的匿名内部类
              // 编译类型:Animal
              // 运行类型:匿名内部类XXXX => 由系统底层分配类名Outer03$3
              Animal animal = new Animal() {
                  @Override
                  void eat() {
                      System.out.println("小狗狗吃东西");
                  }
              };
              animal.eat();
              
              /* 底层本质:
              	class 匿名内部类 extends Person {}
               */
              new Person() { // 匿名内部类本身也是返回对象
                  @Override
                  public void hi() {
                      System.out.println("子类重写的hi()方法,n1 = " + n1);
                  }
              }.hi(); // 本质:匿名内部类的对象调用方法,动态绑定子类(匿名内部类Outer03$4)的hi()方法
          }
      }
      
      class Father {
          public Father(String name) {
              System.out.println("Father构造器被调用,接收到name = " + name);
          }
      
          public void test() {
          }
      }
      
      abstract class Animal {
          abstract void eat();
      }
      

      注意:

      1. 匿名内部类既是一个类的定义,同时它本身也是一个对象,因此从语法上看,它既有定义类的特征,也有创建对象的特征。
      2. 创建匿名内部类的最后有一个分号;相等于创建对象的语句。
      3. 如果在匿名内部类重写了接口的方法或者父类的方法,在使用匿名内部类对象调用方法时会有动态绑定机制。
      4. Java底层在创建匿名内部类时,会给匿名内部类分配类名,创建完之后会立即返回一个匿名内部类对象。
      5. 可以访问外部类的所有成员,包含私有的。
      6. 不能添加访问修饰符,地位是局部变量。
      7. 作用域:仅仅在定义它的方法或代码块中。
      8. 外部其他类 -- 不能访问 --> 匿名内部类。
      9. 如果外部类和匿名内部类的成员同名时,匿名内部类要访问外部类的同名成员,方法和局部内部类一样:外部类名.this.成员访问

      匿名内部类的实践:当作实参传递,更加简洁高效

      案例一:

      package com.yxz.oop_high.innerclass;
      
      public class InnerClassExercise01 {
          public static void main(String[] args) {
              test(new A() { // 将匿名内部类当作实参使用 -> 匿名内部类本质上返回一个对象
                  @Override
                  public void show() {
                      System.out.println("匿名内部类实现了接口的方法");
                  }
              });
              // 传统方法(硬编码) -> 死板,不灵活
              test(new Picture());
          }
          
          static void test(A a) {
              a.show();
          }
      }
      
      interface A {
          void show();
      }
      
      // 传统方式 实现接口 => 编程领域的硬编码
      class Picture implements A {
          @Override
          public void show() {
              System.out.println("一幅名画...");
          }
      }
      

      案例二:

      package oop_high_Exercise;
      
      // 接口类
      interface Computer {
          // work方法是完成计算,如何计算交给匿名内部类完成
          double work(double d1, double d2);
      }
      
      public class Homework04 {
          public static void main(String[] args) {
              CellPhone cellPhone = new CellPhone();
              cellPhone.testWork(new Computer() {// 将匿名内部类当成对象传递给方法
                  @Override
                  public double work(double d1, double d2) {
                      return d1 + d2;
                  }
              }, 99, 999);
              cellPhone.testWork(new Computer() {// 将匿名内部类当成对象传递给方法
                  @Override
                  public double work(double d1, double d2) {
                      return d1 * d2;
                  }
              }, 520, 1314);
          }
      }
      
      class CellPhone {
          // 当调用testWork方法时,直接传入一个实现了Computer接口的匿名内部类即可
          // 该匿名内部类可以灵活的实现work方法,完成不同的计算任务
          public void testWork(Computer computer, double d1, double d2) {
              double res = computer.work(d1, d2); // 动态绑定匿名内部类重写的方法
              System.out.println("运算的结果 = " + res);
          }
      }
      
  2. 定义在外部类的成员位置上:

    1. 成员内部类(不用static修饰)

      成员内部类是定义在外部类的成员位置,并且没有static修饰。

      成员内部类的基本语法:

      class Outer { // 外部类
      	class Inner { // 成员内部类
              类体;
          }
      }
      

      注意:

      1. 可以直接访问外部类的所有成员,包含私有属性。
      2. 可以添加任意访问修饰符,因为它的地位是类的成员。
      3. 作用域:和外部类其他成员一样,为整个类体。在外部类的成员方法中创建成员内部类对象,再调用方法。
      4. 成员内部类 -- 访问 --> 外部类,方式:直接访问
      5. 外部类 -- 访问 --> 内部类,访问方式:在外部类的成员方法中创建内部类对象,再调用方法。
      6. 外部其他类 -- 访问 --> 成员内部类,通过内部类对象访问,创建内部类对象的两种方式:
        1. 使用外部类对象new一个内部类对象,外部类名.内部类名 引用 = 外部类对象.new 内部类名();
        2. 在外部类中编写一个方法,方法返回类型是内部类对象,接收该方法返回值得到内部类的对象实例。
      7. 如果外部类和内部类的成员重名时,内部类访问遵循就近原则,如果内部类想要访问外部类的重名成员,使用外部类名.this.成员访问。

      成员内部类的测试代码

      package com.yxz.oop_high.innerclass;
      
      public class MemberInnerClass01 {
          public static void main(String[] args) {
              Outer05 outer05 = new Outer05();
              outer05.t1();
      
              // 外部其他类,使用成员内部类的三种方式
              // 第一种方式:使用外部类对象new一个内部类对象
              Outer05.Inner05 inner05 = outer05.new Inner05();
      
              // 第二种方式:在外部类中编写一个方法,方法返回内部类对象
              Outer05.Inner05 inner05Instance = outer05.getInner05Instance();
          }
      }
      
      class Outer05 {
          private int n1 = 1000;
          private String name = "bbq";
      
          private void hi() {
              System.out.println("hi()方法");
          }
      
          // 成员方法
          public void t1() {
              // 创建成员内部类对象
              Inner05 inner05 = new Inner05();
              inner05.say();
              System.out.println(inner05.sal);
          }
      
          public Inner05 getInner05Instance() {
              return new Inner05();
          }
      
          // 成员内部类
          public class Inner05 {
              public double sal = 99.8;
              private int n1 = 5;
      
              public void say() {
                  // 可以直接访问外部类的所有成员
                  System.out.println("n1 = " + n1 + " name = " + name); // 访问所有外部类成员
                  hi();
                  // 访问外部类重名成员,使用外部类名.this.成员访问
                  System.out.println("外部类成员n1 = " + Outer05.this.n1);
              }
          }
      }
      
    2. 静态内部类(使用static修饰)

      静态内部类是定义在外部类的成员位置,并且有static修饰,地位相当于静态变量。

      说明:

      1. 可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员。
      2. 可以添加任意访问修饰符。
      3. 作用域:同其他类成员,整个类体。
      4. 静态内部类 -- 访问 --> 外部类(静态属性),访问方式:直接访问。
      5. 外部类 -- 访问 --> 静态内部类,访问方式:在外部类的方法中创建内部类对象,再调用外部类方法。
      6. 外部其他类 -- 访问 --> 静态内部类,
      7. 如果外部类和静态内部类的成员重名时,静态内部类要访问外部类同名(静态)属性,使用外部类名.属性访问。静态内部类不能直接访问外部类非静态成员(需要创建外部类对象)。

      静态内部类的测试代码

      package com.yxz.oop_high.innerclass;
      
      public class StaticInnerClass01 {
          public static void main(String[] args) {
              Outer06 outer06 = new Outer06();
              outer06.m1();
      
              // 外部其他类使用静态内部类,
              // 方式1:相当于调用类的静态变量,可以通过类名直接访问(遵守访问权限)
              Outer06.Inner06 inner06 = new Outer06.Inner06();
              inner06.say();
              // 方式2:编写一个(静态)方法,方法返回静态内部类的对象实例
              Outer06.Inner06 inner06Instance = outer06.getInner06Instance(); // 外部类对象调用非静态方法
              Outer06.Inner06 inner06Instance_ = Outer06.getInner06Instance_(); // 外部类调用静态方法
          }
      }
      
      class Outer06 {
          private static String name = "张三";
          private int n1 = 10;
      
          public static void cry() {
          }
      
          public void m1() {
              // 外部类访问静态内部类:先创建对象,在调用方法
              Inner06 inner06 = new Inner06();
              inner06.say();
          }
      
          public Inner06 getInner06Instance() {
              return new Inner06();
          }
      
          public static Inner06 getInner06Instance_() {
              return new Inner06();
          }
      
          // 成员 静态内部类
          static class Inner06 {
              public void say() {
                  // System.out.println(n1); 错误,静态内部类不能直接访问外部类的非静态成员
                  System.out.println(name); // 可以访问外部类的所有静态成员
                  cry();
                  Outer06 outer06 = new Outer06();
              }
          }
      }
      

2、枚举和注解

2.1、自定义类实现枚举

枚举(enumeration, 简写enum)是一组常量的集合。枚举属于一种特殊的类,里面只包含一组有限的特定的对象。

如何实现自定义枚举类:

  1. 构造器私有化 -> 防止直接创建对象。
  2. 去掉类的相关set方法 -> 防止属性值被修改。
  3. 在类的内部,直接创建静态对象。
  4. 优化,使用final关键字修饰静态对象。
package com.yxz.enum_;

public class Enumeration01 {
    public static void main(String[] args) {
        System.out.println(Season.SPRING);
    }
}

class Season {
    private final String name;
    private final String desc; // 描述
    // 定义四个固定的对象
    public static final Season SPRING = new Season("春天", "温暖");
    public static final Season SUMMER = new Season("夏天", "炎热");
    public static final Season AUTUMN = new Season("秋天", "凉爽");
    public static final Season WINTER = new Season("冬天", "寒冷");

    private Season(String name, String desc) {
        this.name = name;
        this.desc = desc;
    }

    public String getName() {
        return name;
    }


    public String getDesc() {
        return desc;
    }

    @Override
    public String toString() {
        return "Season{" +
                "name='" + name + '\'' +
                ", desc='" + desc + '\'' +
                '}';
    }
}

2.2、enum关键字实现枚举

步骤:

  1. 使用enum关键字替代class
  2. 将创建静态常量对象的语句改成常量名(实参列表);,相当于简化了代码,底层还是会调用构造器。
  3. 如果有多个常量(对象),使用,间隔即可。
  4. 如果使用enum实现枚举,需要将常量对象写在最前面。
package com.yxz.enum_;

public class Enumeration02 {
    public static void main(String[] args) {
        System.out.println(Season.SPRING);
        System.out.println(Season.SUMMER);
        Season a = Season.SPRING;
        Season b = Season.SPRING;
        System.out.println(a == b); // true,同一个静态常量对象
    }
}

enum Season { // 枚举类
    // 枚举对象写在最前面
    SPRING("春天", "温暖"), SUMMER("夏天", "炎热"), AUTUMN("秋天", "凉爽"), WINTER("冬天", "寒冷");
    private final String name;
    private final String desc; // 描述

    private Season(String name, String desc) {
        this.name = name;
        this.desc = desc;
    }

    public String getName() {
        return name;
    }


    public String getDesc() {
        return desc;
    }

    @Override
    public String toString() {
        return "Season{" +
                "name='" + name + '\'' +
                ", desc='" + desc + '\'' +
                '}';
    }
}

注意:

  1. 当使用enum关键字实现一个枚举类时,默认会继承java.lang.Enum类,而且枚举类会自动变成一个final类。使用javap指令反编译查看源码。
  2. 传统方式实现枚举类中定义对象的语句public static final Season SPRING = new Season("春天", "温暖");简化成SPRING("春天", "温暖")仍然会调用构造器 -> 枚举类需要提供构造器。
  3. 如果使用无参构造器创建枚举对象,则实参列表和小括号都可以省略
  4. 当有多个枚举对象时,使用,间隔,最后有一个;结尾。
  5. 枚举对象必须放在枚举类的行首
  6. 使用enum实现的枚举类的所有构造器必须是private的,默认是private的 -> 构造器的修饰符可以不写
  7. enum实现的枚举类中的常量对象都是静态对象 -> 同一个常量对象被所有枚举类的对象共享。
  8. 使用enum关键字后就不能再继承其他类,因为enum修饰的类会隐式继承Enum类,Java是单根继承继承,可以使用接口

枚举类和switch的搭配使用

基本语法:

switch (枚举类对象) {
    case 枚举常量值1: 语句; break;
    case 枚举常量值2: 语句; break;
    case 枚举常量值3: 语句; break;
        ......
}

案例:枚举类Color,五种颜色的枚举对象,实现接口的show()方法,打印枚举对象的三个属性值。

package oop_high_Exercise;

enum Color implements ColorInterface {
    RED(255, 0, 0),
    BLUE(0, 0, 255),
    BLACK(0, 0, 0),
    YELLOW(255, 255, 0),
    GREEN(0, 255, 0);

    int redValue;
    int greenValue;
    int blueValue;

    Color(int redValue, int greenValue, int blueValue) {
        this.redValue = redValue;
        this.greenValue = greenValue;
        this.blueValue = blueValue;
    }

    @Override
    public void show() {
        System.out.println("redValue = " + redValue + ", greenValue = " + greenValue + ", blueValue = " + blueValue);
    }
}

interface ColorInterface {
    void show();
}

public class Homework08 {
    public static void main(String[] args) {
        Color green = Color.GREEN;
        green.show();

        switch (green) {
            case RED -> System.out.println("红色");
            case BLUE -> System.out.println("蓝色");
            case BLACK -> System.out.println("黑色");
            case GREEN -> System.out.println("绿色");
            case YELLOW -> System.out.println("黄色");
            default -> System.out.println("颜色错误");
        }
    }
}

enum常用方法

  1. toString():Enum类已经重写,返回的是当前对象名,子类可以重写该方法。
  2. name():返回当前常量对象(枚举对象)名,子类不能重写该方法
  3. ordinal():返回的是该枚举对象的次序/编号,从0开始编号。
  4. values():返回当前枚举类所有枚举对象。
  5. valueOf(String str):根据传入的字符串去枚举类中匹配枚举对象名,如果匹配成功则返回对应的枚举对象,否则抛出异常。
  6. compareTo():比较两个枚举对象,比较的就是枚举对象的次序/编号(从0开始,ordinal()方法返回的编号),返回两个对象的编号差值。

2.3、JDK内置的基本注解类型

注解(Annotation)也被称为元数据(Metadata),用于修饰解释包、类、方法、属性、构造器、局部变量等数据信息。

注解和注释一样,注解不影响程序的逻辑,但注解可以被编译或运行,相当于在代码中的补充信息。

在JavaSE中,注解的使用目的比较简单,如标记过时的功能,忽略警告等。在JavaEE中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗代码和XML配置等。

使用Annotation时要在其前面增加@符号,并把该Annotation当成一个修饰符使用,用于修饰它支持的程序元素。

  1. @Override:限定某个方法,是重写父类方法,该注解只能用于方法(继承时已讲)。

    添加@Override注解,编译器会对该方法进行校验(从编译层面验证) -> 判断该方法是否真的重写父类的方法,如果不构成重写,则编译错误。

    @Override的源码

    package java.lang;
    
    import java.lang.annotation.*;
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }
    

    说明:@interface不是interface,表示一个注解类。是JDK5.0后加入的。

    @Target(ElementType.METHOD)说明该@Override注解只能修饰方法。@Target是修饰注解的注解,称为元注解。

  2. @Deprecated:用于表示某个程序元素(类、方法等)已过时即不推荐使用,但是不代表不能使用。

    @Deprecated
    class A {
        @Deprecated
        public int n1 = 10;
    	@Deprecated
        public void hi(){}
    }
    

    @Deprecated的源码:

    package java.lang;
    
    import java.lang.annotation.*;
    import static java.lang.annotation.ElementType.*;
    
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
    public @interface Deprecated {
        String since() default "";
        boolean forRemoval() default false;
    }
    

    @Deprecated可以修饰构造器、字段、局部变量、方法、包、参数等等。可以做版本升级过度使用。

  3. @Suppresswarnings:抑制编译器警告。

    抑制的信息种类根据传入的参数决定。

    @SuppressWarnings抑制警告的范围和其放置位置有关

    public class Test {
        @SuppressWarnings("all") // 抑制所有警告信息
        public static void main(String[] args) {
            int i = 0;
            int b = 10;
            System.out.println(b);
        }
    }
    

    @SuppressWarnings的源码

    package java.lang;
    
    import java.lang.annotation.*;
    import static java.lang.annotation.ElementType.*;
    
    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SuppressWarnings {
        String[] value(); // 传入数组,对多种类型的警告进行抑制
    }
    

2.4、元注解

元注解是修饰注解的注解。

元注解种类:

  1. @Retention

    只能用于修饰一个注解,指定注解的作用范围,@Rentention包含一个RetentionPolicy类型的成员变量,使用时必须为该value成员变量指定值(SOURCE,CLASS,RUNTIME)

    RetentionPolicy.SOURCE:编译器使用后,直接丢弃的注解。

    RetentionPolicy.CLASS:编译器将注解记录在class文件中,当运行Java程序时,JVM不会保留注解(默认值)。

    RetentionPolicy.RUTIME:编译器将把注解记录在class文件中,当运行Java程序时,JVM会保留注解,程序可以通过反射获取该注解。

  2. @Target:指定注解可以在哪些地方使用。

  3. @Documented:指定该注解是否会在javadoc体现。即在生成文档时,可以看到该注解。定义为Documented的注解必须设置Retention值为RUNTIME。

  4. @Inherited:子类会继承父类注解。

3、异常

3.1、异常的概念

当程序出现了问题,不应该就终止运行,使用针对性的代码进行特殊处理(try-catch)来保证程序即使出现异常也能正常运行 -> 保证程序的健壮性。

在IDEA中,将可能抛出异常的代码选中,使用快捷键:Alt+Shift+T,选择try-catch,可以快速生成异常处理try-catch的代码。

在Java语言中,将程序执行中发生的不正常情况称为“异常”。(开发过程中的语法错误和逻辑错误不是异常)

执行过程中所发生的异常事件可分为两类:

  1. Error(错误):Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。比如:StackOverflowError[栈溢出]和OOM(out of memory)Error是严重错误,程序会崩溃
  2. Exception:其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如:空指针访问,试图读取不存在的文件网络连接中断等等,Exception分为两大类:运行时异常(feishoujian程序运行时发生的异常)和编译时异常(编程时,编译器检查出的异常)

3.2、异常体系图

常见的异常,体现了继承和实现的关系:

Java异常体系图(常见)

总结:

  1. 异常分为两大类:非受检(unchecked)异常(运行时异常) 和 受检(checked)异常(编译时异常)。
  2. 运行时异常,编译器检查不出来。一般指编程时的逻辑错误,是程序员应该避免其出现的异常。java.lang.RuntimeException类及它的子类都是运行时异常。
  3. 对于运行时异常,可以不做处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。
  4. 编译时异常,是编译器要求必须处置的异常,否则代码无法编译通过。

3.3、常见的异常

常见的运行异常

NullPointerException:空指针异常

当应用程序试图在需要对象的地方使用null时,抛出该异常。

ArithmeticException:数学运算异常

当出现异常的运算条件时,抛出此异常。如:除数为0时,抛出该异常。

ArrayIndexOutOfBoundsException:数组下标越界异常

用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引,抛出索引越界异常

ClassCastException:类型转换异常

当试图将对象强制转换为不是实例的子类时,抛出该异常。

NumberFormatException:数字格式不正确异常

当程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。使用该异常可以确保输入是满足条件数字。

常见的编译异常

SQLException:操作数据库时,查询表可能发生异常。

IOException:操作文件时,发生的异常。

FileNotFoundException:当操作一个不存在的文件时,发生异常。

ClassNotFoundException:加载类,而该类不存在时,抛出该异常。

EOFException:操作文件,到文件末尾,发生异常。

IllegalArgumentException:参数异常。

3.4、异常处理

异常处理就是当发生异常时,对异常处理的方式。

方式一:使用try-catch-finally:程序员在代码中捕获发生的异常,自行处理。如果没有finally代码块也可以 -> try-catch处理异常。

处理机制示意:

try {
    可能有异常的代码块;
} catch(Exception e) {
    当捕获到异常后,执行catch代码块的内容
    1.系统将异常封装成Exception对象e,传递给catch代码块
    2.得到异常对象后,程序员自己处理
    3.如果没有发生异常,则catch代码块不会执行
} finally {
    1.不管代码块是否有异常发生,始终要执行finally代码块的内容
    2.通常将释放资源的操作代码放在finally代码块中
}

注意:

  1. 如果异常发生了,则异常发生后面的try块中的代码不再执行,直接进入到catch块。
  2. 如果异常没有发生,则顺序执行try块,不会进入catch块。
  3. finally块用于执行不管是否发生异常都需要执行的操作。
  4. 如果代码块可能有多个异常可以使用多个catch分别捕获不同的异常进行进行处理。(要求:子类异常在前捕获,父类异常后捕获)
  5. 可以进行try-finally配合使用,相当于没有捕获异常,执行完finally块后程序会直接中断。应用场景:执行一段代码,不管是否发生异常,都必须执行某个业务逻辑的代码。
package com.yxz.exception_;

public class Exception01 {
    public static void main(String[] args) {
        try {
            int[] arr = {1, 2, 3};
            for (int i = 0; i <= arr.length; i++) {
                System.out.println(arr[i]); // ArrayIndexOutOfBoundsException
            }

            String str = "Mi cai";
            str = null;
            System.out.println(str.length()); // NullPointerException

            int n1 = 10;
            int n2 = 0;
            int result = n1 / n2; // ArithmeticException
        } catch (NullPointerException e) {
            System.out.println("处理了空指针异常");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("处理了数组索引越界异常");
        } catch (ArithmeticException e) {
            System.out.println("处理了数学算数异常");
        } catch (Exception e) {
            System.out.println("未知异常");
        } finally {
            System.out.println("释放了资源");
        }
    }
}

方式二:使用throws:将发生的异常抛出,交给调用者(方法)来处理,最顶级的处理者是JVM。

throws处理机制图

如果一个方法可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常,表明将该方法将不对这些异常进行处理,而由该方法的调用者负责处理。

在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。throws关键字后也可以是异常列表,即可以抛出多个异常。

public class Throws01 {
    public static void main(String[] args) {
    }
    
    public void f1() throws FileNotFoundException, NullPointerException, ArithmeticException {
        FileInputStream fis = new FileInputStream("d://a.txt");
    }
}

注意:

  1. 对于编译异常,程序中必须处理,比如try-catch或者throws。
  2. 对于运行异常,程序中如果没有处理,默认throws方式处理。
  3. 子类重写父类的方法时,对抛出异常的规定:子类重写的方法,所抛出的异常类型要么和父类抛出的异常一致,要么为父类抛出的异常的类型的子类型。
  4. 在throws过程中,如果有方法try-catch,就相当于处理异常,就可以不必throws。
  5. 在Java中,如果抛出运行异常,并不要求程序员显式处理,有默认处理机制throws。

3.5、自定义异常

当程序中出现了某些“错误”,但该错误信息并没有在Throwable子类中描述处理,就可以自己设计异常类,用于描述错误信息。

自定义异常的步骤:

  1. 定义类:自定义异常类名,继承Exception或RuntimeException
  2. 如果继承Exception,属于编译异常。
  3. 如果继承RuntimeException,属于运行异常(一般情况下,让自定义异常继承RuntimeException,即做成运行异常 -> 可以使用默认处理机制)。
public class CustomException {
    public static void main(String[] args) {
        int age = 180;
        if (!(age >= 18 && age <= 120)) {
            throw new AgeException("年龄需要在18-120之间");
        }
        System.out.println("你的年龄正确");
    }
}

class AgeException extend RuntimeException {
    public AgeException(String message) {
        super(message);
    }
}

3.6、throw和throws 的对比

throws:异常处理的一种方式,用于方法声明处,后面紧跟异常类型。

throw:手动生成异常对象的关键字,用于方法体中,后面跟异常对象。

3.7、面试题:finally块和return的执行顺序

  1. try块中有return 基本数据类型;

    public static int testReturn1() {
            int i = 1;
            try {
                i++;
                System.out.println("try:" + i);
                return i;
            } catch (Exception e) {
                i++;
                System.out.println("catch:" + i);
            } finally {
                i++;
                System.out.println("finally:" + i);
            }
            return i;
            /*
            输出:
                try:2
                finally:3
                2
             */
        }
    

    说明:当try中带有return时,会先执行return前的代码,然后暂时保存需要return的信息再执行finally中的代码,最后再通过return返回之前保存的信息。所以,这里方法返回的值是try中计算后的2,而非finally中计算后的3。

  2. try块中有return 引用类型;

    public static List<Integer> testReturn2() {
            List<Integer> list = new ArrayList<>();
            try {
                list.add(1);
                System.out.println("try:" + list);
                return list;
            } catch (Exception e) {
                list.add(2);
                System.out.println("catch:" + list);
            } finally {
                list.add(3);
                System.out.println("finally:" + list);
            }
            return list;
            /*
            输出:
                try:[1]
                finally:[1, 3]
                [1, 3]
             */
        }
    

    说明:return时会临时保存需要返回的信息,不受finally中的影响,为什么这里会有变化?其实问题出在参数类型上,上一个例子用的是基本类型,这里用的引用类型。list里存的不是变量本身,而是变量的地址,所以当finally通过地址改变了变量,还是会影响方法返回值的

  3. try块中有异常,catch块中有return 引用类型;

    public static int testReturn3() {
            int i = 1;
            try {
                i++;
                System.out.println("try:" + i);
                int x = i / 0;
            } catch (Exception e) {
                i++;
                System.out.println("catch:" + i);
                return i;
            } finally {
                i++;
                System.out.println("finally:" + i);
            }
            return i;
            /*
            输出:
                try:2
                catch:3
                finally:4
                3
             */
        }
    

    说明:catch中return与try中一样,会先执行return前的代码,然后暂时保存需要return的信息,再执行finally中的代码,最后再通过return返回之前保存的信息。所以,这里方法返回的值是try、catch中累积计算后的3,而非finally中计算后的4。

  4. try块中有异常,finally块中有return 引用类型;

    public static int testReturn4() {
            int i = 1;
            try {
                i++;
                System.out.println("try:" + i);
                return i;
            } catch (Exception e) {
                i++;
                System.out.println("catch:" + i);
                return i;
            } finally {
                i++;
                System.out.println("finally:" + i);
                return i;
            }
            /*
            输出:
                try:2
                finally:3
                3
             */
        }
    

    说明:当finally中有return的时候,try中的return会失效,在执行完finally的return之后,就不会再执行try中的return。这种写法,编译是可以编译通过的,但是编译器会给予警告,所以不推荐在finally中写return,这会破坏程序的完整性,而且一旦finally里出现异常,会导致catch中的异常被覆盖。

总结:

  1. finally中的代码总会被执行。
  2. 当try、catch中有return时,也会执行finally。return的时候,要注意返回值的类型,是否受到finally中代码的影响。
  3. finally中有return时,会直接在finally中退出,导致try、catch中的return失效。

4、常用类

4.1、包装类

针对八种基本定义相应的引用类型——包装类(Wrapper),有了类的特点就可以调用类中的方法。

基本数据类型 包装类及继承父类
boolean Boolean extends Object
char Character extends Object
byte Byte extends Number
short Short extends Number
int Integer extends Number
long Long extends Number
float Float extends Number
double Double extends Number

包装类和基本数据的转换

  1. JDK5前的手动装箱和拆箱方式,装箱:基本类型 -> 包装类型,反之拆箱。
  2. JDK5及以后都是自动装箱和拆箱方式。
  3. 自动装箱底层调用的是valueOf方法,如:Integer.valueOf()
public class Integer01 {
    public static void main(String[] args) {
        // jdk5前是手动装箱和拆箱
        // 手动装箱 int -> Integer
        int n1 = 100;
        Integer integer = new Integer(n1); // 手动装箱
        Integer integer1 = Integer.valueOf(n1); // 手动装箱
        
        // 手动拆箱
        // Integer -> int
        int i = integer.intValue();
        
        // jdk5之后,自动装箱和自动拆箱
        int n2 = 200;
        // 自动装箱 int -> Integer
        Integer integer2 = n2; // 底层使用Integer.valueOf(n2)
        // 自动拆箱
        int n3 = integer2; // 底层使用integer.intValue()方法
        
        Object obj1 = true ? new Integer(1) : new Double(2.0); // 三元运算符是一个整体,提升精度
        System.out.println(obj1); // 1.0
    }
}

包装类型和String类型的相互转换

public class Integer01 {
    public static void main(String[] args) {
        // 包装类(Integer) -> String
        Integer i = 100;
        // 方式1
        String str1 = i + "";
        // 方式2
        String str2 = i.toString();
        // 方式3
        String str3 = String.valueOf(i);
        
        // String -> 包装类(Integer)
        String str4 = "12345";
        Integer i2 = Integer.parseInt(str4);// 自动装箱
        Integer i3 = new Integer(str4); // 构造器
        
    }
}

包装类的面试题

  1. Object obj1 = true ? new Integer(1) : new Double(2.0); // 三元运算符是一个整体,提升精度
    System.out.println(obj1); // 1.0
    

    三元运算符是一个整体,表达式的返回值精度由表达式1和表达式2的最高精度决定。

  2. Integer i = new Integer(1);
    Integer j = new Integer(1);
    System.out.println(i == j); // false:使用new创造了对象,则判断对象地址 -> 不同
    
    Integer m = 1; // 自动装箱 底层Integer.valueOf(1) -> 查看valueOf源码 -> 1在[-128, 127]之间,所以不创建对象
    Integer n = 1; // 自动装箱 底层Integer.valueOf(1) -> 查看valueOf源码 -> 1在[-128, 127]之间,所以不创建对象
    System.out.println(m == n); // true
    
    Integer x = 128; // 自动装箱 底层Integer.valueOf(1) -> 查看valueOf源码 -> 128不在[-128, 127]之间,所以创建对象
    Integer y = 128; // 自动装箱 底层Integer.valueOf(1) -> 查看valueOf源码 -> 128不在[-128, 127]之间,所以创建对象
    System.out.println(x == y); // false
    

    valueOf的源码

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high) // -128 ~ 127
            return IntegerCache.cache[i + (-IntegerCache.low)]; // 调用静态方法并返回一个数组值,不创造对象
        return new Integer(i); // 当i的值不在[-128, 127]之间,则返回新创建的对象
    }
    
  3. Integer x = 128;
    int y = 128;
    System.out.println(x == y); // true
    

    注:只要有一个基本数据类型,==都是判断值是否相等。

4.2、String

String的图

说明:

  1. String实现了Serializable接口,说明String对象可以串行化,即String对象可以在网络上传输。
  2. String实现了Comparable接口,说明String对象可以进行比较。

String类的理解和创建对象

  1. String对象用于保存字符串,也就是一组字符序列。

  2. "jack"是字符串常量,双括号括起的字符序列。

  3. 字符串的字符使用Unicode字符编码,一个字符(不区分字母和汉字)都占两个字节。

  4. String是final类,即不能被继承。

  5. String有属性private final char value[]:用于存放字符串内容。

    注:value是一个final类型,不可以修改 -> value不可以指向新的地址,但是单个字符内容可以变化

    final char[] value = {'a', 'b', 'c'};
    value[0] = 'H'; // 可以修改单个字符的值
    char[] v2 = {'t', 'o', 'm'};
    value = v2; // 错误:不可以更改final类型value的地址
    

String两种方式创建对象的区别:(⭐⭐⭐⭐⭐)

方式一:直接赋值String s = "yxz";

方式二:调用构造器String s2 = new String("yxz");

说明:

  1. 方式一:先从常量池查看是否有"yxz"数据空间,如果有,直接让s指向该数据空间;如果没有则重新创建,然后指向。s最终指向的是常量池的空间地址

  2. 方式二:先在堆中创建空间,里面维护了value属性,指向常量池的"yxz"空间。如果常量池没有"yxz",重新创建,如果有,直接通过value指向。s2最终指向的是堆中的空间地址

  3. 两种方式创建对象的内存布局示意图:

    String创建对象内存布局

String字符串是不可变的。一个字符串对象一旦被分配,其内容不可变。

面试题

  1. String a = "hello" + "abc"; 创建了几个对象?一个对象

    String a = "hello" + "abc";编译器底层优化,等价于String a = "helloabc";

  2. String a = "hello";
    String b = "abc";
    String c = a + b;
    

    上述代码创建了几个对象?三个字符串常量对象("hello","abc","helloabc"),一个StringBuilder对象,一个String对象

    说明:String c = a + b; 底层是Stringbuilder sb = new StringBuilder(); sb.append(a); sb.append(b); String c = sb.toString(); sb是在堆中开辟的对象,并且append()是在原来字符串sb的基础上追加的。

    重要规则:String c1 = "ab" + "cd";常量相加,看的是常量池。 String c1 = a + b; 变量相加,底层使用StringBuilder是在堆中new了一个对象。

  3. public class Test {
        final char[] ch = {'j', 'a', 'v', 'a'};
        String str = new String("hsp");
    
        public static void main(String[] args) {
            Test ex = new Test();
            ex.change(ex.str, ex.ch);
            System.out.print(ex.str + " and ");
            System.out.println(ex.ch);
        }
    
        public void change(String str, char ch[]) {
            str = "java";
            ch[0] = 'h';
        }
    }
    

    以上代码输出什么?输出:hsp and hava

    内存分析图:

    String面试题内存分析图

String常用方法:

  • equals():区分大小写,判断内容是否相等
  • equalsIgnoreCase():忽略大小写的判断内容是否相等
  • length():获取字符的个数,字符串长度
  • intern():返回字符串对象内容在常量池的地址。
  • indexOf():获取字符在字符串中第一次出现的索引,索引从0开始,如果找不到,返回-1
  • lastIndexOf():获取字符在字符串中最后一次出现的索引,索引从0开始,如找不到,返回-1
  • substring():截取指定范围的字符串
  • trim():去掉字符串的前后空格
  • charAt():获取某索引处的字符,注:不能使用str[index]的方式获取
  • toUpperCase():将字符串中所有字母都大写并返回。
  • toLowerCase():将字符串中所有字母都小写并返回。
  • concat():拼接字符串并返回
  • replace():替换字符串中的字符
  • split():分割字符串
  • compareTo():比较字符串大小
  • toCharArray():转换成字符数组
  • format():格式字符串

4.3、StringBuffer

java.lang.StringBuffer代表可变的字符序列,可以对字符串内容进行增删。

很多方法和String相同,但StringBuffer是可变长度的

StringBuffer是一个容器

StringBuffer的继承关系图:

StringBuffer的继承图

  1. 直接父类AbstractStringBuilder中有属性char[] value,不是final类型,该value数组存放StringBuffer对象的字符串内容,不是final -> 可以修改字符串内容 -> value数组存放在堆中

  2. StringBuffer实现了Serializable,即StringBuffer的对象可以串行化。

  3. StringBuffer是final类,不能被继承。

  4. StringBuffer保存的是字符串变量,里面的值可以更改,每次StringBuffer的更新实际上可以更新内容,不用每次更新地址(创建新对象),效率较高。

    String每次更新都会创建新的对象(如果常量池中有字符串则不会创建)并更改地址。

  5. StringBuffer创建对象时,默认给value数组设置大小为16,可以调用构造器显式指定其StringBuffer的大小 = str.length() + 16。

String -> StringBuffer 的两种方式

方式一:调用有参构造器StringBuffer(String str)

String str = "hello";
StringBuffer sb1 = new StringBuffer(str);

方式二:调用append(String str)方法

StringBuffer sb2 = new StringBuffer();
sb2 = sb1.append(str);

StringBuffer -> String 的两种方式

方式一:toString()方法

StringBuffer sb3 = new StringBuffer("hello");
String s = sb3.toString();

方式二:使用String的构造器传入StringBuffer对象

String s1 = new String(sb3);

StringBuffer类常用方法

  • append():增加字符或字符串
  • delete(int start, int end):删除索引[start, end)范围内的字符。
  • replace(int start, int end, String str):替换索引[start, end)内的内容为字符(串)str
  • indexOf():查找指定的子串在字符串中第一次出现的索引
  • insert():在指定索引处插入字符(串)
  • length():返回字符串长度。

4.4、StringBuilder

  1. 一个可变的字符序列。此类提供一个与StringBuffer兼容的API(两者方法一样),但不保证同步(StringBuilder不是线程安全的)。该类被设计用作StringBuffer的一个简易替换,用在字符串缓冲区被单个线程使用的时候在单线程中,建议优先使用该类,大部分情况下它比StringBuffer更快
  2. 在StringBuilder上的主要操作是append和insert方法,可重载这些方法,以接收任意类型的数据。
  3. StringBuilder继承AbstractSrtingBuilder类,实现了Serializable接口 -> StringBuilder对象可以串行化(可以网络传输、保存到文件)。
  4. StringBuilder是final类,不能被继承。
  5. StringBuilder的方法,没有做互斥的处理,即没有synchronized关键字 -> 因此推荐在单线程情况下使用StringBuilder

String、StringBuffer、StringBuilder的比较

  1. StringBuilder和StringBuffer非常类似,均代表可变的字符序列,而且方法也一样。
  2. String:不可变字符序列,效率低,但是复用率高 -> 如果要对字符串做大量修改操作,不要使用String
  3. StringBuffer:可变字符序列,效率较高(增删),线程安全
  4. StringBuilder:可变字符序列,效率最高,线程不安全

String、StringBuffer、StringBuilder的选择

  1. 如果字符串存在大量的修改操作,一般使用StringBuffer或StringBuilder。
  2. 如果字符串存在大量的修改操作,并在单线程的情况,使用StringBuilder。
  3. 如果字符串存在大量的修改操作,并在多线程的情况,使用StringBuffer。
  4. 如果字符串很少修改,被多个对象引用,使用String,如配置信息等。

4.5、Math

Math类常用方法:

  • abs():绝对值
  • pow(double a, double b):求a的b次幂
  • ceil(double a):向上取整,返回大于等于a的最小整数
  • floor(double a):向下取整,返回大于等于a的最大整数
  • round(double/float a):四舍五入。先给a+0.5,再取整返回一个整型(long/int)
  • sqrt():求开方
  • random():生成随机数。返回0 <= x < 1之间的一个随机小数。获取[a, b]之间的一个随机整数 公式:(int)(a + Math.random() * (b - a + 1))
  • max():两个数的最大值
  • min():两个数的最小值

4.6、Date、Calendar、LocalDate

第一代日期类Date(在java.util包下)

Date类继承关系图

  1. Date精确到毫秒,代表特定的瞬间

    Date类构造器:

    • Date():获取当前系统的时间
    • Date(long date):通过指定毫秒数date得到对应时间。

    Date类的常用方法:

    • getTime():获取某个时间对应的毫秒数。
  2. SimpleDateFormat:格式和解析日期的类。它允许对日期进行格式化(日期 -> 文本)、解析(文本 -> 日期)和规范化。

    创建SimpleDateFormat对象,可以指定相应的格式。格式中使用的字母是规定好的,不能乱写,格式字符规定:

    日期格式字符规定

    SimpleDateFormat类构造器:

    • SimpleDateFormat(String pattern):pattern是根据规定格式字母组成的字符串

      如:SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss E");

    SimpleDateFormat类的常用方法:

    • format(Date date):将指定日期转换成指定格式的字符串
    • parse(String s):可以把一个格式化的String 转成对应的Date,得到的Date在输出时仍然是国外的方式,如果希望指定格式输出,需要format转换。在把String转成Date时,使用的SimpleDateFormat格式需要和String的格式一样,否则会抛出转换异常ParseException。

    示例代码:

    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    public class UseDate {
        public static void main(String[] args) throws ParseException { // 抛出异常
            // 获取当前系统时间
            Date date = new Date();
            // 默认输出的日期格式是国外的方式,因此通常需要对格式进行转换
            System.out.println("当前时间是:" + date);
            Date date1 = new Date(92733); // 通过指定毫秒数得到时间
            System.out.println(date.getTime()); // 获取某个时间对应的毫秒数
    
            // 创建SimpleDateFormat对象,根据字母规定,传入格式串
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss E");
            String format = simpleDateFormat.format(date); // 将日期转换成指定格式的字符串
            System.out.println(format);
    
            String s = "2022年11月07日 05:20:00 周一";
            Date parseDate = simpleDateFormat.parse(s); // 可能会抛出转换异常,需要抛出异常或者try-catch捕获异常
            System.out.println("转换后的日期:" + simpleDateFormat.format(parseDate));
        }
    }
    

第二代日期类Calendar(在java.util包下)

Calendar类继承关系图

public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {}
  1. Calendar类是一个抽象类,它为特定瞬间与一组诸如YEARMONTHDAY_OF_MONTHHOUR等日历、字段之间的转换提供了一些方法,并为操作日历提供了方法。
  2. Calendar类的构造器是受保护的,获取其对象可以通过getInstance()方法。
  3. Calendar类提供大量的方法和字段给程序员,通过获取字段转化成时间。
  4. Calendar没有提供对应的格式化的类,需要程序员自己组合。
  5. 如果要获取24时进制的小时数,将Calendar.HOUR -> Calendar.HOUR_OF_DAY
import java.util.Calendar;

public class Calendar_ {
    public static void main(String[] args) {
        Calendar c = Calendar.getInstance();
        System.out.println(c);
        System.out.println("年:" + c.get(Calendar.YEAR));
        // Calendar返回月份是从0月开始编号
        System.out.println("月:" + (c.get(Calendar.MONTH) + 1));
        System.out.println("日:" + c.get(Calendar.DAY_OF_MONTH));
        System.out.println("小时:" + c.get(Calendar.HOUR));
        System.out.println("分钟:" + c.get(Calendar.MINUTE));
        System.out.println("秒:" + c.get(Calendar.SECOND));
        // 自己组合显示日期时间
        System.out.println(c.get(Calendar.YEAR) + "-" + (c.get(Calendar.MONTH) + 1) + "-"
                + c.get(Calendar.DAY_OF_MONTH) + " " + c.get(Calendar.HOUR) + ":"
                + c.get(Calendar.MINUTE) + ":" + c.get(Calendar.SECOND));
    }
}

第三代日期类

JDK 1.0中包含了java.util.Date类,但是它的大多数方法在JDK 1.1引入Calendar类后就被弃用。而Calendar类也存在问题:

  1. 可变性:像日期和时间这样的类应该是不可变的
  2. 偏移性:Date中的年份是从1900年开始的,而月份都是从0开始
  3. 格式化:格式化只对Date有用,Calendar没有格式化类
  4. Date类和Calendar类不是线程安全的,不能处理闰秒等(每隔2年,多出1秒)

为了解决以上问题,JDK 8.0引入第三代日期类

LocalDateTime类继承关系图

第三代日期类:

  1. LocalDate类(日期/年月日),在java.time包下
  2. LocalTime类(时间/时分秒),在java.time包下
  3. LocalDateTime类(日期时间/年月日时分秒),在java.time包下

LocalDateTime类常用方法:

  • now():静态方法,返货一个LocalDateTime类的对象

  • getYear():年

  • getMonth():英语月份

  • getMonthValue():数字月份

  • getDayOfMonth():日

  • getHour():小时(24进制)

  • getMinute():分钟

  • getSecond():秒

  • plusDays(long days):当前时间加上一个天数,返回一个将来的时间

  • plusYears(long years):当前时间加上年数,返回将来的时间。

  • minusWeeks(long weeks):当前时间减去周数,返回过去的时间

  • minusHours(long hours):当前时间减去小时数,返回过去的时间

  • 等等,需要什么方法,查阅JDK 8.0版本之后的API文档

在开发中,尽量使用第三代日期类。

DateTimeFormatter格式日期类(在java.time.format包下)

类似于SimpleDateFormat类

DateTimeFormatter dtf = DateTimeFormatter.ofPattern(格式字符串);

String format = dtf.format(日期对象);

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class UseLocalDateTime {
    public static void main(String[] args) {
        LocalDateTime ldt = LocalDateTime.now();
        System.out.println(ldt);
        System.out.println("年:" + ldt.getYear());
        System.out.println("月:" + ldt.getMonth()); // 英语月份
        System.out.println("月:" + ldt.getMonthValue()); // 数字月份
        System.out.println("日:" + ldt.getDayOfMonth());
        System.out.println("时:" + ldt.getHour());
        System.out.println("分:" + ldt.getMinute());
        System.out.println("秒:" + ldt.getSecond());

        LocalDate now = LocalDate.now(); // 可以获取年月日
        LocalTime now1 = LocalTime.now(); // 可以获取时分秒

        // 使用DateTimeFormatter对象 对 LocalDateTime对象进行格式化
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH小时mm分ss秒");
        String format = dateTimeFormatter.format(ldt);
        System.out.println("格式化的日期 = " + format);
        LocalDateTime localDateTime = ldt.plusDays(999);
        System.out.println("999天后是:" + dateTimeFormatter.format(localDateTime));
    }
}

第三代日期类Instant(在java.time包下)

  1. 通过Instant类的静态方法now()获取表示当前时间戳的对象。
  2. 通过Date类的form()方法,可以把Instant转成Date对象。
  3. 通过Date对象的toInstant()方法可以把date转成Instant对象。
import java.time.Instant;
import java.util.Date;

public class UseInstant {
    public static void main(String[] args) {
		Instant now2 = Instant.now();
        System.out.println(now2);
        Date from = Date.from(now2);
        Instant instant = from.toInstant();
    }
}

4.7、System

System类的常见方法:

  • exit():退出当前程序。exit(0)表示程序正常退出,0-正常状态。

  • arraycopy(Type[] src, int srcPos, Type[] des, int destPos, int length):复制数组元素,比较适合底层调用,一般使用Arrays.copyOf完成复制数组。参数说明:src源数组,srcPos从源数组的哪个索引位置开始拷贝,des目标数组,destPos把源数组src的数据拷贝到目标数组dest的哪个索引,length从源数组拷贝多少个数据到目标数组dest

    int[] src = {1, 2, 3};
    int[] dest = new int[3];
    System.arraycopy(src, 0, dest, 0, src.length);
    
  • currentTimeMillis():返回当前时间距离1970年1月1日0时0分的毫秒数(返回值long型)

  • gc():运行垃圾回收机制System.gc();

4.8、Arrays

Arrays里面包含一系列静态方法,用于管理和操作数组。

Arrays类常用方法:

  • toString():返回数组的字符串形式。如:Arrrays.toString(arr)

  • sort():排序(自然排序和定制排序)

    示例代码:

    import java.util.Arrays;
    import java.util.Comparator;
    
    public class SortDetail {
        public static void main(String[] args) {
            Integer[] arr = {7, -1, 89, 53, 66};
            /*
                1.sort方法是重载的,可以通过传入一个接口Comparator实现定制排序
                2.调用定制排序时,传入两个参数 (1)待排序的数组arr
                (2)实现了Comparator接口的匿名内部类,要实现接口的compare()方法
                sort方法会调用TimSort类的private static <T> void binarySort(T[] a, int lo, int hi, int start,
                                           Comparator<? super T> c)方法
                该方法的核心代码:
                while (left < right) {
                    int mid = (left + right) >>> 1;
                    if (c.compare(pivot, a[mid]) < 0) // 动态绑定匿名内部类实现的compare()方法
                        right = mid;
                    else
                        left = mid + 1;
                }
                通过根据compare()方法的返回值是否大于0来确定排序的规则
                上述代码充分体现了 接口编程 + 动态绑定 + 匿名内部类 的综合使用
             */
            Arrays.sort(arr, new Comparator<Integer>() { // 基于接口的匿名内部类,<Integer>表示泛型
                @Override
                public int compare(Integer o1, Integer o2) { // 动态绑定机制,实现了接口的compare()方法
                    return o2 - o1;
                }
            });
            for (Integer integer : arr) {
                System.out.print(integer + " ");
            }
        }
    }
    

    案例:4本书排序,三种方式排序:(1)按照价格从高到低 (1)按照价格从低到高 (1)按照书名长度从大到小

    import java.util.Comparator;
    
    public class BookSort {
        public static void main(String[] args) {
            BookSort test = new BookSort();
            Book[] books = new Book[4];
            books[0] = new Book("红楼梦", 100);
            books[1] = new Book("三国演义", 90);
            books[2] = new Book("Python从精通到跑路", 5);
            books[3] = new Book("Java从入门到放弃", 300);
    
            // 按价格从高到低排序
            System.out.println("按价格从高到低排序");
            test.sortBooks(books, new Comparator() { // 基于接口的匿名内部类
                @Override
                public int compare(Object o1, Object o2) { // 动态绑定机制
                    Book book1 = (Book) o1; // 向下转型
                    Book book2 = (Book) o2;
                    double priceMargin = book1.getPrice() - book2.getPrice();
                    // 因为方法返回int型,所以需要转换一下,正数和负数表示相反的排序顺序
                    if (priceMargin > 0) {
                        return 1;
                    } else if (priceMargin < 0) {
                        return -1;
                    } else {
                        return 0;
                    }
                }
            });
            for (Book book : books) {
                System.out.println(book);
            }
    
            // 按价格从低到高排序
            System.out.println("按价格从低到高排序");
            test.sortBooks(books, new Comparator() {
                @Override
                public int compare(Object o1, Object o2) {
                    Book book1 = (Book) o1;
                    Book book2 = (Book) o2;
                    double priceMargin = book2.getPrice() - book1.getPrice();
                    if (priceMargin > 0) {
                        return 1;
                    } else if (priceMargin < 0) {
                        return -1;
                    } else {
                        return 0;
                    }
                }
            });
            for (Book book : books) {
                System.out.println(book);
            }
    
            // 按书名长度从大到小排序
            System.out.println("按书名长度从大到小排序");
            test.sortBooks(books, new Comparator() {
                @Override
                public int compare(Object o1, Object o2) {
                    Book book1 = (Book) o1;
                    Book book2 = (Book) o2;
                    return book1.getName().length() - book2.getName().length();
                }
            });
            for (Book book : books) {
                System.out.println(book);
            }
        }
    
        /**
         * 书本冒泡排序方法
         *
         * @param books : 数组
         * @param c     : 比较器接口
         */
        public void sortBooks(Book[] books, Comparator c) { // 传入接口
            for (int i = 0; i < books.length - 1; i++) {
                for (int j = 0; j < books.length - 1 - i; j++) {
                    if (c.compare(books[j], books[j + 1]) < 0) { // 动态绑定匿名内部类实现的compare()方法
                        Book book = books[j];
                        books[j] = books[j + 1];
                        books[j + 1] = book;
                    }
                }
            }
        }
    }
    
    class Book {
        private String name;
        private double price;
    
        public Book(String name, double price) {
            this.name = name;
            this.price = price;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public double getPrice() {
            return price;
        }
    
        public void setPrice(double price) {
            this.price = price;
        }
    
        @Override
        public String toString() {
            return "Book{" +
                    "name='" + name + '\'' +
                    ", price=" + price +
                    '}';
        }
    }
    
  • binarySearch(Type[] arr, Type i):通过二分搜索法进行查找元素,要求必须在有序的数组中查找。如果元素i不存在则返回-(low + 1)low指的是该元素在数组中应该存在的位置(索引)

  • copyOf(Type[] arr, int arr.length):数组元素的复制(底层调用System.arraycopy()方法)。从arr数组中,拷贝 arr.length个元素到newArr数组中。

    如果拷贝的数组长度 > arr.length,就在新数组的后面增加null

    如果拷贝的数组长度 < 0,就抛出异常NegativeArraySizeException

  • fill(Type[] arr, Type i):数组元素的填充。使用i替换掉arr数组中所有元素。

  • equals(Type[] arr1, Type[] arr2):比较两个数组元素内容是否完全一致。

  • asList():将一组值转换成list集合。

    List<Integer> asList = Arrays.asList(2, 3, 4, 5, 6, 1);
    System.out.println("asList = " + asList);
    System.out.println("asList的运行类型:" + asList.getClass());
    

    上述代码中,asList的编译类型:List接口,运行类型是List接口的实现子类Arrays一个内部类java.util.Arrays$ArrayList,是Arrays类的静态内部类:

    private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable {}
    

4.9、BigInteger、BigDecimal

BigInteger大整数类,用于大数的运算。底层是将数当作字符串处理。对于BigInteger类型的数进行运算时不能直接使用运算符,需要调用其提供的方法运算。BigInteger类常用方法:

  • add(BigInteger val):加法运算
  • subtract(BigInteger val):减法运算
  • multiply(BigInteger val):乘法运算
  • divide(BigInteger val):除法运算
import java.math.BigInteger;

public class UseBigInteger {
    public static void main(String[] args) {
        BigInteger bigInteger1 = new BigInteger("999999999999999999999999999999999999999");
        BigInteger bigInteger2 = new BigInteger("33333333333333333333333333");
        BigInteger add = bigInteger1.add(bigInteger2); // 加法
        System.out.println(add);
        BigInteger subtract = bigInteger1.subtract(bigInteger2); // 减法
        System.out.println(subtract);
        BigInteger multiply = bigInteger1.multiply(bigInteger2); // 乘法
        System.out.println(multiply);
        BigInteger divide = bigInteger1.divide(bigInteger2); // 除法
        System.out.println(divide);
    }
}

BigDecimal类,用于高精度浮点型的运算。

BigDecimal类常用方法:

  • add(BigDecimal val):加法运算
  • subtract(BigDecimal val):减法运算
  • multiply(BigDecimal val):乘法运算
  • divide(BigDecimal val):除法运算,注意:当除法除不尽时,会出现无限循环小数,程序抛出ArithmeticException异常,需要指定保留精度。在方法中传入参数BigDecimal.ROUND_CEILING —> 除法结果保留到被除数的精度 -> divide(bigDecimal2, BigDecimal.ROUND_CEILING)
import java.math.BigDecimal;

public class UseBigDecimal {
    public static void main(String[] args) {
        BigDecimal bigDecimal1 = new BigDecimal("1234.1415926535201314");
        BigDecimal bigDecimal2 = new BigDecimal("3.987653");
        System.out.println(bigDecimal1.add(bigDecimal2)); // 加法
        System.out.println(bigDecimal1.subtract(bigDecimal2)); // 减法
        System.out.println(bigDecimal1.multiply(bigDecimal2)); // 乘法
        // System.out.println(bigDecimal1.divide(bigDecimal2)); // 除法,可能会抛出异常(当除不尽时,无限循环小数)
        System.out.println(bigDecimal1.divide(bigDecimal2, BigDecimal.ROUND_CEILING));
    }
}

编程技巧

  1. 保证数据的合法性:当正确(错误)情况较少时,列举出所有正确(错误)情况然后整体取反。
  2. 处理非法数据:过关斩将,在开头把所有非法数据处理完再执行正确数据的代码。

案例1

package com.yxz.common_class.exercise;

import java.util.Scanner;

/**
 * 输入用户名、密码、邮箱,如果信息录入正确,则提示注册成功,否则生成异常对象
 * 要求:
 * 1.用户名长度为2或3或4
 * 2.密码的长度为6,要求全是数字
 * 3.邮箱中包含@和. 并且@在.的前面
 */

public class Homework02 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String name = scanner.next();
        String pwd = scanner.next();
        String email = scanner.next();
        try {
            userRegister(name, pwd, email);
            System.out.println("恭喜你,注册成功!!!");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    public static void userRegister(String name, String pwd, String email) {
        // 过关斩将,校验传入的数据是否正确
        // 正确情况只有一种,写出正确情况再整体取反,简化代码
        if (!(name != null && pwd != null && email != null)) {
            throw new RuntimeException("参数不能为空");
        }

        // 第一关:用户名长度为2或3或4
        int usernameLength = name.length();
        // 正确情况只有三种,写出正确情况再整体取反,简化代码
        if (!(usernameLength == 2 || usernameLength == 3 || usernameLength == 4)) {
            throw new RuntimeException("用户名长度为2或3或4");
        }

        // 第二关:密码的长度为6,要求全是数字
        // 正确情况只有一种,写出正确情况再整体取反,简化代码
        if (!(pwd.length() == 6) && isDigit(pwd)) {
            throw new RuntimeException("密码的长度为6,要求全是数字");
        }

        // 第三关:邮箱中包含@和. 并且@在.的前面
        int i = email.indexOf('@');
        int j = email.indexOf('.');
        // 正确情况只有一种,写出正确情况再整体取反,简化代码
        if (!(i > 0 && j > i)) {
            throw new RuntimeException("邮箱中包含@和. 并且@在.的前面");
        }
    }

    public static boolean isDigit(String pwd) {
        char[] chars = pwd.toCharArray();
        for (char aChar : chars) {
            if (aChar < '0' || aChar > '9') {
                return false;
            }
        }
        return true;
    }
}

案例2

package com.yxz.common_class.exercise;

import java.util.Scanner;

/**
 * 编写程序,输入形式为:Zhang san Fen 的人名,以Fen,Zhang .S的形式打印出来
 * 其中S是中间单词的首字母大写
 */

public class Homework03 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String name = scanner.nextLine();
        printName(name);
    }

    public static void printName(String name) {
        // 数据校验
        if (name == null) {
            System.out.println("名字不能为空");
            return;
        }

        String[] names = name.split(" ");
        if (names.length != 3) {
            System.out.println("输入的姓名格式不对");
            return;
        }

        String format = String.format("%s,%s .%c", names[2], names[0], names[1].toUpperCase().charAt(0));
        System.out.println(format);
    }
}

5、集合

本章节全是重点(⭐⭐⭐⭐⭐)

优点

  1. 可以动态保存任意多个对象,使用方便。
  2. 提供了一系列方便的操作对象的方法:add、remove、set、get等。
  3. 使用集合添加/删除新元素的代码更简洁

5.1、集合框架体系

很重要!!!(建议背诵)

集合框架体系图

Collection体系图

字典框架体系图

Map体系图

集合主要分成两组(单列集合,双列集合)

Collection接口有两个重要的子接口List、Set,它们的实现子类都是单列集合。

Map接口的实现子类是双列集合,以键值对(Key-Value)的形式存放元素。

5.2、Collection接口

public interface Collection<E> extends Iterable<E> {}

Collection接口实现类的特点

  1. Collection实现子类可以存放多个元素,每个元素可以是Object
  2. 有些Collection的实现类,可以存放重复的元素,有些不可以
  3. 有些Collection的实现类,有些是有序的(List),有些不是有序的(Set)
  4. Collection接口没有直接的实现子类,是通过它的子接口Set和List来实现

Collection接口的常用方法:

  • add():添加单个元素

  • remove():删除指定元素。有重载方法,可以删除指定索引位置的元素,也可以删除指定元素

  • contains():查找元素是否存在

  • size():获取元素个数

  • isEmpty():判断是否为空

  • clear():清空

  • addAll():添加多个元素

  • containsAll():查找多个元素是否都存在

  • removeAll():删除多个元素

以ArrayList为例,测试Collection接口方法

package com.yxz.collection_;

import java.util.ArrayList;
import java.util.List;

public class UseCollection {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        List list = new ArrayList();
        // add():添加单个元素
        list.add(10); // 自动装箱为Integer
        list.add("hello"); // 自动装箱为String
        list.add(true); // 自动装箱为Boolean
        System.out.println("list = " + list);
        // remove():删除指定元素
        list.remove(new Integer(10)); // 删除指定元素
        list.remove(1); // 根据指定索引位置的元素
        System.out.println("list = " + list);
        // contains():查找元素是否存在
        System.out.println("list是否包含\"hello\" = " + list.contains("hello"));
        // size():获取元素个数
        list.add(3.14);
        list.add('X');
        System.out.println(list.size());
        // isEmpty():判断是否为空
        System.out.println("list是否为空 = " + list.isEmpty());
        // clear():清空
        list.clear();
        // addAll():添加多个元素
        List list1 = new ArrayList();
        list1.add(false);
        list1.add("world");
        list1.add("break");
        list.addAll(list1);
        System.out.println("list = " + list);
        // containsAll():查找多个元素是否都存在
        System.out.println("list是否包含list1所有元素 = " + list.containsAll(list1));
        // removeAll():删除多个元素
        list.removeAll(list1);
        System.out.println(list);
    }
}

Collection接口遍历元素方式

  1. 使用Iterator迭代器

    Iterator对象称为迭代器,主要用于遍历Collection集合中的元素。

    所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器。

    Iterator仅用于遍历集合,Iterator本身并不存放对象。

    执行原理

    Iterator iterator = coll.iterator();得到一个集合迭代器

    iterator调用方法hasNext()用来判断是否还有下一个元素

    iterator调用方法next()指针下移,并将下移后集合位置上的元素返回

    注意:在调用it.next()方法之前必须要调用it.hasNext()进行检测。如果不调用,且没有下一个元素,直接调用next()方法会抛出NoSuchElementException异常。

    在IDEA中,快速生成包含迭代器的while循环 的快捷键:itit

    显示所有快速生成代码的快捷键:Ctrl + J,选择itit生成while循环。

  2. 增强for循环

    增强for循环,可以代替iterator迭代器

    特点:增强for就是简化版的iterator,本质一样,只能用于遍历集合或数组。底层会调用以下源码:

    public Iterator<E> iterator() {
        return new Itr();
    }
    public boolean hasNext() {
        return cursor != size;
    }
    

    基本语法:

    for (元素类型 元素名 : 容器对象名) {
        访问元素的语句
    }
    

    IDEA中,生成增强for循环的快捷键:I 或者 iter

5.2.1、List接口

public interface List<E> extends Collection<E> {}

List接口(在java.util包下)是Collection接口的子接口

  1. List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复。
  2. Lisr集合类中的每个元素都有其对应的顺序索引,即支持索引(从0开始)。
  3. List容器中的每个元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
  4. JDK API中List接口的实现类有很多,常用的有:ArrayListLinkedListVector

List集合添加了一些根据索引来操作集合元素的方法

  • void add(int index, Object element):在index位置插入element元素
  • boolean addAll(int index, Collection elements):在index位置开始将elements中的所有元素添加进来
  • Object get(int index):获取指定index位置的元素
  • int indexOf(Object obj):返回obj在集合中首次出现的位置
  • int lastIndexOf(Object obj):返回obj在当前集合中最后一次出现的位置
  • Object remove(int index):移除指定index位置的元素,并返回此元素
  • Object set(int index, Object element):设置指定index位置的元素为element,相当于替换
  • List subList(int fromIndex, int toIndex):返回区间[fromIndex, toIndex)内的所有元素构成的子集合
ArrayList类
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}

说明:

  1. 可以加入null值,并且可以多个重复
  2. ArrayList底层使用数组来实现数据存储
  3. ArrayList基本等同于Vector,除了ArrayList是线程不安全(执行效率高),在多线程的情况下,不建议使用ArrayList。

注意:ArrayList底层结构和源码分析

测试代码,查看底层源码执行逻辑:

public class UseCollection {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        // ArrayList list = new ArrayList(8);
        // 使用for给list集合添加1-10的数据
        for (int i = 1; i <= 10; i++) {
            list.add(i);
        }
        // 使用for给list添加11-15的数据
        for (int i = 11; i <= 15; i++) {
            list.add(i);
        }
        list.add(16);
        list.add(17);
        list.add(18);
    }
}
  1. ArrayList中维护了一个Object类型的数组elementData。底层源码transient Object[] elementData;

    transient关键字表示瞬间,短暂的,被该关键字修饰的属性不会被序列化。

  2. 当创建ArrayList对象时,如果使用的是无参构造器,则初始化elementData容量为0,第1次添加,则扩容elementData为10,如果需要再次扩容,则扩容elementData为原来容量的1.5倍。(扩容机制:0 -> 10 -> (10的1.5倍)15 -> ...)

    ArrayList类底层源码

    调用无参构造器创建对象时:创建了一个空的数组

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    

    第一次给容器添加对象时:newCapacity = 10

    public boolean add(E e) {
        modCount++; // 记录当前集合被修改的次数,防止多个线程同时修改该集合
        add(e, elementData, size);
        return true;
    }
    
    private void add(E e, Object[] elementData, int s) { // s是传入的当前集合的大小
        if (s == elementData.length) // 如果当前集合大小和容量相等,说明大小不够,要去扩容
            elementData = grow(); // 扩容
        elementData[s] = e; // 再赋值
        size = s + 1;
    }
        
    private Object[] grow() {
        return grow(size + 1); // 调用grow()方法扩容
    }
    
    private Object[] grow(int minCapacity) { // 该方法确定最小容量
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                                                      minCapacity - oldCapacity, /* minimum growth */
                                                      oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else {
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }
    
    public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
        // assert oldLength >= 0
        // assert minGrowth > 0
    
        int newLength = Math.max(minGrowth, prefGrowth) + oldLength; // 1.5倍扩容
        if (newLength - MAX_ARRAY_LENGTH <= 0) {
            return newLength;
        }
        return hugeLength(oldLength, minGrowth);
    }
    

    第二次及其以后,按照1.5倍扩容。扩容时使用的方法Arrays.copyOf(原数组, 新容量newCapacity),保证了原数组数据不丢失的情况下增加容量

  3. 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为原来容量的1.5倍。(扩容机制:(指定大小)x -> 1.5*x -> 1.5*1.5*x -> ...)

    底层源码

    调用有参构造器

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    

    第一次添加元素和扩容上面调用无参构造器添加元素、扩容的逻辑一致。

Vector类
public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}

说明:

  1. Vector底层也是一个对象数组,protected Object[] elementData;
  2. Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized关键字
  3. 在开发中,需要线程同步安全时,考虑使用Vector
底层结构 版本 线程安全(同步)效率 扩容倍数
ArrayList 可变数组Object[] JDK 1.2 不安全,效率高 如果有参构造则1.5倍扩,如果无参构造,第一次扩容10,从第二次开始1.5倍扩容
Vector 可变数组Object[] JDK 1.0 安全,效率低 如果无参,默认10,之后2倍扩。如果指定大小,容量不够时每次直接2倍扩容

Vector类底层源码

  1. 调用无参构造创建Vector对象时,会自动调用Vector的有参构造并传入初始大小10。

    public Vector() {
        this(10); // 调用有参构造器,默认初始化大小为10
    }
    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }
    public Vector(int initialCapacity, int capacityIncrement) {
        super(); // 完成父类初始化
        if (initialCapacity < 0) // false
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity]; // 创建数组,初始化容量为10
        this.capacityIncrement = capacityIncrement; // capacityIncrement = 0
    }
    
  2. 先判断是否需要扩容,再添加元素

    public synchronized boolean add(E e) {
        modCount++;
        add(e, elementData, elementCount);
        return true;
    }
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length) // false -> 当前容量足够 -> 不扩容
            elementData = grow();
        elementData[s] = e;
        elementCount = s + 1;
    }
    private Object[] grow() {
        return grow(elementCount + 1);
    }
    private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                                                  minCapacity - oldCapacity, /* minimum growth */
                                                  capacityIncrement > 0 ? capacityIncrement : oldCapacity
                                                  /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    }
    public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
        // assert oldLength >= 0
        // assert minGrowth > 0
    
        int newLength = Math.max(minGrowth, prefGrowth) + oldLength; // 2倍扩容
        if (newLength - MAX_ARRAY_LENGTH <= 0) {
            return newLength;
        }
        return hugeLength(oldLength, minGrowth);
    }
    private static int hugeLength(int oldLength, int minGrowth) {
        int minLength = oldLength + minGrowth;
        if (minLength < 0) { // overflow
            throw new OutOfMemoryError("Required array length too large");
        }
        if (minLength <= MAX_ARRAY_LENGTH) {
            return MAX_ARRAY_LENGTH;
        }
        return Integer.MAX_VALUE;
    }
    
LinkedList类
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable {}

说明:

  1. LinkedList实现了双向链表和双端队列特点
  2. 可以添加任意元素(元素可以重复),包括null
  3. 线程不安全,没有实现同步

LinkedList的底层操作机制

  1. LinkedList底层维护了一个双向链表
  2. LinkedList中维护了两个属性first和last分别指向首节点和尾节点
  3. 每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表
  4. LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率较高

LinkedList类底层源码

  1. 当创建LinkedList对象时,先完成父类初始化,然后调用了LinkedList类的无参构造器,并不进行预扩容处理。public LinkedList() {}

    并初始化链表的首位指针:

    transient Node<E> first; // 头指针,指向链表头结点
    transient Node<E> last; // 尾指针,指向链表尾结点
    
  2. 每次添加元素都会将元素放在链表最后一个节点,第一次添加元素时,链表开辟第一个节点,该节点的首尾指向都指向null

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null); // 开辟新节点,并让新节点的next指针置空
        last = newNode; // 让尾指针指向新节点
        if (l == null) // 第一次添加元素是为true
            first = newNode; // 头指针指向第一个节点(头结点)
        else
            l.next = newNode; // 让链表最后一个节点的后向指针指向新开辟的节点
        size++;
        modCount++;
    }
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
    
  3. 每次删除时,如果不指定删除哪个节点,则默认删除第一个节点

    public E remove() {
        return removeFirst(); // 默认删除第一个节点
    }
    public E removeFirst() {
        final Node<E> f = first;// 将f指向的双向链表的第一个节点拿掉
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next; // 取出第一个节点的信息(后向指针,数据域)
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }
    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
    
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
    
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
    
        x.item = null;
        size--;
        modCount++;
        return element;
    }
    

LinkedList类的常用方法:

  • add():添加元素到双向链表尾处
  • remove():删除元素,无参默认删除第一个节点,重载方法可以指定删除索引处的节点
  • set(int index, Type data):修改指定节点的数据域
  • get():得到指定节点对象
  • getFirst():获取双向链表的表头节点

ArrayList和LinkedList的比较:

底层结构 增删效率 改查效率
ArrayList 可变数组 较低,数组扩容 较高,随机存取
LinkedList 双向链表 较高,通过链表追加 较低,顺序存取

如何选择ArrayList和LinkedList:

  1. 查找修改的操作多,选择ArrayList,支持随机存取
  2. 增加删除的操作多,选择LinkedList
  3. 一般地,在程序中,80%都是查询操作,大部分情况下选择ArrayList

5.2.2、Set接口

public interface Set<E> extends Collection<E> {}

Set接口说明:

  1. 无序(添加和取出的顺序不一致),没有索引,取出的顺序不会变。
  2. 不允许重复元素,所以最多包含一个null
  3. JDK API中Set接口的常用实现类:HashSetLinkedHashSetTreeSet
  4. Set接口和List接口一样是Collection的子接口,因此常用方法和Collection接口一样(Set接口对象没有get方法)。
  5. Set接口和Collection的遍历方式一样,遍历方式可以用迭代器和增强for循环,但是不能用索引的方式来获取元素
  6. Set接口的实现类的对象 ==> 简称:Set接口对象
HashSet类
public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable{}

说明:

  1. HashSet实现了Set接口

  2. HashSet底层是HashMap,HashMap底层是数组+单向链表+红黑树。HashSet构造器源码:

    public HashSet() {
        map = new HashMap<>();
    }
    
  3. 可以存放null值,但是只能存放一个 -> 元素(对象)不能重复

  4. HashSet不保证元素是有序的,取决于hash后,再确定索引的结果 -> 存放元素的顺序和取出顺序不一定一致

  5. 在执行add()添加方法时,方法会返回一个boolean值,表示操作是否成功,当集合中该元素已经存在时,添加重复元素失败 -> 返回false

  6. 在执行remove(Object obj)时, 会计算obj的hash值根据hash值去table表中找到相应的索引, 然后删除该索引处的元素.

  7. IDEA中,使用快捷键Alt+Insert选择重写hashCode()和equals()方法可以快速生成hashCode()和equals()方法。

HashMap底层机制

模拟实现HashMap(数组+链表)机制:

package com.yxz.collection_;

public class HashSetStructure {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        // 模拟实现HashSet底层(HashMap底层)
        // 1.创建一个数组,数组类型Node[],也称为 表
        Node[] table = new Node[16];
        System.out.println("table = " + table);
        // 2.创建节点
        Node john = new Node("john", null);
        table[2] = john;
        Node jack = new Node("jack", null);
        john.next = jack; // 将jack节点挂到john下
        Node rose = new Node("Rose", null);
        jack.next = rose; // 将rose节点挂到jack下
        Node lucy = new Node("lucy", null);
        table[3] = lucy; // 把lucy放到table表的索引为3的位置
        System.out.println("table = " + table);
    }
}

class Node { // 节点:存储数据,有后向指针,形成链表
    Object item; // 存放数据
    Node next; // 指向下一个节点

    public Node(Object item, Node next) {
        this.item = item;
        this.next = next;
    }

    @Override
    public String toString() {
        return "Node{" +
                "item=" + item +
                ", next=" + next +
                '}';
    }
}

分析HashSet添加元素底层如何实现?(hash() + equals())

  1. HashSet(HashMap)添加一个元素时,先得到hash值(根据重写的hashCode()方法得到并进行特殊处理),然后根据相应算法转成索引值。
  2. 找到存储数据表table,看这个索引位置是否已经存放元素。如果没有则直接加入。如果有元素,则调用equals比较(根据对象所属类的equals()方法),如果相同,就放弃添加,如果不相同,则添加以链表的形式到最后的位置。
  3. 在Java 8.0中,如果一条链表的元素个数 >= static final int TREEIFY_THRESHOLD = 8,并且table的大小 >= static final int MIN_TREEIFY_CAPACITY = 64,就会进行树化(红黑树)。

分析HashSet的扩容和转成红黑树的机制?

  1. HashSet底层是HashMap,第一次添加时table数组扩容到16,$临界值(threshold) = 当前table数组容量 * 加载因子(loadFactor = 0.75) = 12$。
  2. 如果table数组使用到了临界值12,就会扩容到$16 * 2 = 32$,新的临界值就是$32 * 0.75 = 24$,依此类推...
  3. 在Java 8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认值=8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认值=64),就会对满足上述条件的链表进行树化(红黑树),否则仍然采用数组扩容机制(每次2倍)。
  4. 当table表中的元素个数(包括所有链表上的元素)大于临界值,就进行数组扩容。

测试代码并对HashSet底层机制分析:

package com.yxz.collection_;

import java.util.HashSet;

public class HashSetSource {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {

        HashSet hashSet = new HashSet();
        for (int i = 1; i <= 12; i++) {
            hashSet.add(new A(i));
        }
        System.out.println("set = " + hashSet);

        /*
        源码解读:
        1.执行HashSet()
         public HashSet() {
                    map = new HashMap<>();
               }
        2.执行add()
          public boolean add(E e) { // e = "java"
                return map.put(e, PRESENT)==null; // (static) PRESENT = new Object();
            }
         3.执行put(),该方法会执行hash(key),得到key对应的hash值(此处hash值不完全等价于hashCode,有算法进行特殊处理),
           算法是(h = key.hashCode()) ^ (h >>> 16); // 这里的hashCode()方法会根据程序员重写的hashCode()方法动态绑定
             public V put(K key, V value) { // e = "java", value = PRESENT 共享
                return putVal(hash(key), key, value, false, true);
            }
         4.执行putVal()
             final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
                Node<K,V>[] tab; Node<K,V> p; int n, i; // 定义了辅助变量
                // table 就是HashMap的一个数组,类型是Node[]
                // if语句表示如果当前table是null,或者大小 = 0,就是第一次扩容,16个空间
                if ((tab = table) == null || (n = tab.length) == 0)
                    n = (tab = resize()).length;
                // (1)根据key,得到hash 去计算该key应该存放到table表的哪个索引位置
                // 并且把这个位置的对象,赋给辅助变量p
                // (2)判断p是否为null
                //  (2.1)如果p为null,表示还没有存放过元素,将创建一个Node(key = "java", value = PRESENT)
                //  (2.2)就放在该位置tab[i] = newNode(hash, key, value, null);
                if ((p = tab[i = (n - 1) & hash]) == null)
                    tab[i] = newNode(hash, key, value, null);
                else {
                    // 开发技巧:定义辅助变量(局部变量)时,在需要辅助变量时再定义
                    Node<K,V> e; K k;
                    if (p.hash == hash && // 如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样,并且满足以下条件之一的
                    // (1)准备加入的key 和 p指向的Node节点的key是同一个对象 (==判断对象地址是否相同)
                    // (2)p指向的Node节点的key(对象)的equals()方法 和 准备加入的key比较后相同 (重写的equals()方法判断对象是否相同)
                        ((k = p.key) == key || (key != null && key.equals(k))))
                        e = p;
                    // 再判断 p 是不是一棵红黑树
                    // 如果是一棵红黑树,就调用putTreeVal()来添加
                    else if (p instanceof TreeNode)
                        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                    else { // 如果table对应索引位置已经是一个链表,就使用for循环比较
                        // (1)依次和该链表的每一个元素比较后都不相同,则加入到该链表的最后
                            注意:在把元素添加到链表后,立即判断:该链表是否已经有TREEIFY_THRESHOLD(8)个节点,
                                    如果达到,就调用 treeifyBin(tab, hash);对当前满足8个节点的链表进行树化(转成红黑树)
                                    注意:treeifyBin()方法在转成红黑树前,要再判断一次,判断条件:
                                    // MIN_TREEIFY_CAPACITY = 64(默认)
                                    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 
                                        resize(); // 如果一个链表满足8个节点,但是table表不足64时,先进行table表的扩容
                                    // 如果table表有一个链表满足8个节点且table表容量 >= 64,则对该链表进行树化 -> 红黑树
                        // (2)依次和该链表的每一个元素比较过程中,如果有相同元素,就直接break退出循环放弃添加
                        for (int binCount = 0; ; ++binCount) {
                            if ((e = p.next) == null) { // 更新e,使e指针指向下一个节点
                                p.next = newNode(hash, key, value, null);
                                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                    treeifyBin(tab, hash);
                                break;
                            }
                            if (e.hash == hash &&
                                ((k = e.key) == key || (key != null && key.equals(k))))
                                break;
                            p = e; // 更新p,让p指针保持和e指针同一指向
                        }
                    }
                    if (e != null) { // existing mapping for key
                        V oldValue = e.value;
                        if (!onlyIfAbsent || oldValue == null)
                            e.value = value;
                        afterNodeAccess(e);
                        return oldValue;
                    }
                }
                ++modCount;
                if (++size > threshold) // 当前table数组中的元素总个数大于临界值
                    resize(); // 扩容
                afterNodeInsertion(evict);
                return null;
            }
         */
    }
}

class A {
    private int n;

    public A(int n) {
        this.n = n;
    }

    @Override
    public int hashCode() {
        return 100; // 返回同样hash值
    }
}

案例:

  1. 编写一个Employee类,属性(name,sal,birthday),birthday是MyDate类
  2. 编写MyDate类有属性(year,month,day)。
  3. 在main方法中,创建HashSet对象,添加多个Employee对象,当name和birthday相同时认为是同一个对象,不能重复添加到HashSet对象中去。
package com.yxz.collection_;

import java.util.HashSet;
import java.util.Objects;

public class HashSetExercise {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("Mi cai", 500000.5, new MyDate(1997, 11, 12)));
        hashSet.add(new Employee("Mi lan", 350000.7, new MyDate(1999, 10, 10)));
        hashSet.add(new Employee("Mi cai", 500000.3, new MyDate(1997, 11, 12)));
        for (Object o : hashSet) {
            System.out.println(o);
        }
    }
}

class Employee {
    private String name;
    private double sal;
    private MyDate birthday;

    public Employee(String name, double sal, MyDate birthday) {
        this.name = name;
        this.sal = sal;
        this.birthday = birthday;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return name.equals(employee.name) && birthday.equals(employee.birthday);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, birthday);
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", sal=" + sal +
                ", birthday=" + birthday +
                '}';
    }
}

class MyDate {
    private int year;
    private int month;
    private int day;

    public MyDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MyDate myDate = (MyDate) o;
        return year == myDate.year && month == myDate.month && day == myDate.day;
    }

    @Override
    public int hashCode() {
        return Objects.hash(year, month, day);
    }

    @Override
    public String toString() {
        return "MyDate{" +
                "year=" + year +
                ", month=" + month +
                ", day=" + day +
                '}';
    }
}
LinkedHashSet类
public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {}

说明:

  1. LinkedHashSet是HashSet的子类(LinkedHashSet调用核心方法时,会直接调用父类HashSet的方法,所有原理一致,只是单向链表 -> 双向链表)。

  2. LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表(LinkedHashSet有head头指针和tail尾指针,每一个节点都有before和after属性 -> 形成双向链表)

  3. LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。

  4. LinkedHashSet不允许重复元素

  5. 在LinkedHashSet中添加一个元素时,先求hash值,再确定索引,确定该元素在table表的位置,然后将添加的元素加入到双向链表中(如果已经存在,不添加。原理和HashSet一样)。添加时的代码逻辑:

    tail.next = newElement; // 尾节点的后向next指针指向新节点
    newElement.pre = tail; // 新节点的前向pre指针指向原尾节点
    tail = newElement; // 原尾结点指向新节点
    
TreeSet类
public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable {}

说明:

  1. TreeSet底层是TreeMap

  2. TreeSet存储单列key,其value值为静态常量对象private static final Object PRESENT = new Object();,TreeMap以key-value形式存储。

  3. TreeSet在对key排序时,如果没有显式传入比较器,TreeSet会进入TreeMap并将key对象转换成Comparable接口类型(需要该对象的类型实现Comparable接口), 底层源码:Comparable<? super K> k = (Comparable<? super K>) key;(向下转型) 如果不可转换则抛出类型转换异常(ClassCastException)。即在没有传入比较器的情况下, TreeSet的key对象必须实现Comparable接口的compareTo()方法

  4. 如果TreeSet的key是String类型的,创建TreeSet时不传入比较器,则默认按照字典序升序排序(字母a->z,A->Z),因为String实现了Comparable接口并重写了compareTo()方法。

  5. 想要自定义排序,需要调用有参构造器并传入一个比较器(匿名内部类)指定排序规则。底层会将该比较器赋给TreeSet父类TreeMap的属性Comparator<? super K> cpr = comparator;

    匿名内部类需要重写compare()方法,运行时会动态绑定到该方法。

  6. 如果当前要加入的key和一个已经存在的key值一样,则无法加入新key值。

  7. key不能为null,否则会抛出空指针异常。

import java.text.ParseException;
import java.util.Comparator;
import java.util.TreeSet;

public class Test {
    @SuppressWarnings({"all"})
    public static void main(String[] args) throws ParseException {
        // 1.当使用无参构造器,创建TreeSet时,仍然是无序的
        // 2.使用TreeSet提供的一个构造器,可以传入一个比较器(匿名内部类)
        //   制定排序规则

//        TreeSet treeSet = new TreeSet();
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String) o1).compareTo((String) o2);
            }
        });
        treeSet.add("jack");
        treeSet.add("tom");
        treeSet.add("a");
        treeSet.add("smith");
        /*
        1.调用含有比较器的TreeSet构造器
        public TreeSet(Comparator<? super E> comparator) {
            this(new TreeMap<>(comparator)); // TreeSet底层是TreeMap
        }
        2.将比较器传给TreeMap的属性
        public TreeMap(Comparator<? super K> comparator) {
            this.comparator = comparator;
        }
        3.第一次添加时,会把k-v(v是常量)封装到Entry对象放入root
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        4.在调用add("tom")方法时,底层会执行到:
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                // 动态绑定到匿名内部类(匿名对象)重写的compare()方法
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)s
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else // 如果相等,返回0,key加入不了
                    return t.setValue(value);
            } while (t != null);
        }
        5.如果没有显式传入比较器
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
            	// key必须实现Comparable接口,否则抛出类型转换异常ClassCastException
                Comparable<? super K> k = (Comparable<? super K>) key; // 向下转型
            do {
                parent = t;
                // key对象必须实现Comparable接口的compareTo()方法
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
         */
    }
}

5.3、Map接口

Map接口(JDK 8)的特点:

  1. Map与Collection并列存在,用于保存具有映射关系的数据:Key-Value

  2. Map中的Key和Value可以是任意引用类型的数据,会封装到HashMap@Node对象中

  3. Map中的key不允许重复,key相同时会进行value替换。原因和HashSet一样,HashSet只是让Value值为常量对象PRESENT。

  4. Map中的value可以重复

  5. Map的key可以为null,value也可以为null。key为null只能有一个,value为null可以多个

  6. 常用String类作为Map的key,key的类型可以为任意Object类及其子类。

  7. key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value

  8. Map存放数据的key-value,一对k-v是放在一个HashMap$Node中的,因为Node实现了Entry接口,也说是一对k-v就是一个Entry

    k-v 是存放在HashMap$Node node = newNode(hash, key, value, null)

    k-v为了方便程序员的遍历,还会创建EntrySet集合,该集合存放的元素的类型是Entry。而一个Entry对象就有k-v属性,即EntrySet<Entry<K, V>>

    源码声明:Set<Map.Entry<K, V>> entrySet();

    在entrySet的定义类型是Map.Entry,但是实际上存放的还是HashMap$Node,因为HashMap$Node实现了Map.Entry接口(接口的多态 -> 接口的引用可以指向实现接口的类的对象实例),把HashMap$Node存放在entrySet就方便程序员遍历(entrySet中存放的是HashMap$Node的table表的地址),因为Map.Entry提供了抽象方法K getKey()K getValue(),这样在实现子类Node中可以重写方法,运行时就可以动态绑定到子类Node重写的方法 -> HashMap$Node拥有getKey()和getValue()方法。

    Map.Entry和HashMap$Node的源码:

    interface Entry<K, V> {} // Map的一个子接口
    static class Node<K,V> implements Map.Entry<K,V> {} // HashMap的静态内部类
    

Map常用方法:

  • put(Object key, Object value):添加键值对
  • remove():根据键删除映射关系
  • get(Object obj):根据键获取值,根据传入的key值,返回key对应的value值。
  • size():获取元素个数
  • isEmpty():判断元素个数是否为0
  • clear():清空容器
  • containsKey():查找键是否存在
  • keySet():获取所有的键
  • entrySet():获取所有的key-value
  • values():获取所有的值
package com.yxz.map_;

import java.util.HashMap;
import java.util.Map;

public class UseMap {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("邓超", new Book("", 100));
        map.put("邓超", "孙俪");
        map.put("王宝强", "马蓉");
        map.put("鹿晗", "关晓彤");
        map.put("黄子韬", "徐艺洋");
        map.put(null, "刘亦菲");

        System.out.println("map = " + map);
        // remove:根据键删除映射关系
        map.remove(null);
        System.out.println("map = " + map);
        // get:根据键获取值
        Object val = map.get("鹿晗");
        System.out.println("val = " + val);
        // size:获取元素个数
        System.out.println("k-v = " + map.size());
        // isEmpty:判断个数是否为0
        System.out.println(map.isEmpty());
        // clear:清空
//        map.clear();
        // containsKey:查找键是否存在
        System.out.println(map.containsKey("yxz")); // false
    }
}
class Book {
    private String name;
    private int num;

    public Book(String name, int num) {
        this.name = name;
        this.num = num;
    }
}

Map的六大遍历方式:

  1. 通过ketSet()方法,先取出所有的key,再通过key获取到对应value(增强for、迭代器)
  2. 通过values()方法,取出所有的value(增强for、迭代器)
  3. 通过entrySet()方法,来获取key-value(增强for、迭代器)
package com.yxz.map_;

import java.util.*;

public class UseMap {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("邓超", "123");
        map.put("邓超", "孙俪");
        map.put("王宝强", "马蓉");
        map.put("鹿晗", "关晓彤");
        map.put("黄子韬", "徐艺洋");
        map.put(null, "刘亦菲");

        // 第一组:先取出所有的Key,通过Key取出对应的Value
        Set keySet = map.keySet();
        // 1.增强for
        System.out.println("-------第一种方式---------");
        for (Object key : keySet) {
            System.out.println(key + "-" + map.get(key));
        }
        // 2.迭代器
        System.out.println("-------第二种方式---------");
        Iterator iterator = keySet.iterator();
        while (iterator.hasNext()) {
            Object key = iterator.next();
            System.out.println(key + "-" + map.get(key));
        }

        // 第二组:把所有的values取出来
        Collection values = map.values();
        // 可以使用所有的Collection使用的遍历方法
        System.out.println("-------取出所有的value值 增强for--------");
        // 1.增强for
        for (Object value : values) {
            System.out.println(value);
        }
        // 2.迭代器
        System.out.println("-------取出所有的value值 迭代器--------");
        Iterator iterator2 = values.iterator();
        while (iterator2.hasNext()) {
            Object value = iterator2.next();
            System.out.println(value);
        }

        // 第三组:通过EntrySet来获取k-v
        Set entrySet = map.entrySet(); // EntrySet<Map.Entry<K, V>>
        // 1.增强for
        System.out.println("-----使用EntrySet的增强for(第3种)----------");
        for (Object entry : entrySet) {
            // 将entry转成Map.Entry -> 实现Map.Entry的子类HashMap$Node就能调用重写Map.Entry的方法getKey,getValue
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "-" + m.getValue());
        }
        // 2.迭代器
        System.out.println("-----使用EntrySet的迭代器(第4种)-----------");
        Iterator iterator3 = entrySet.iterator();
        while (iterator3.hasNext()) {
            Object entry = iterator3.next(); // 向上转型
            // System.out.println(entry.getClass()); // 动态绑定到实现Map.Entry的接口子类HashMap$Node
            // 向下转型 -> Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "-" + m.getValue());
        }
    }
}

总结:

  1. Map接口的常用实现类:HashMap、Hashtable、Properties
  2. HashMap是Map接口使用频率最高的实现类
  3. HashMap是以key-value键值对的方式来存储数据
  4. key不能重复,但是value可以重复,允许使用null键和null值
  5. 如果添加相同的key,则会覆盖原来的key-value,等同于修改(key不会替换,value值会被替换)
  6. 与HashSet一样,HashMap不保证映射的顺序,因为底层是以hash表的方式存储的(JDK 8的HashMap底层 = 数组+链表+红黑树)
  7. HashMap没有实现同步,线程不安全,方法没有做同步互斥的操作 -> 没有synchronized关键字

5.3.1、HashMap类

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {}

HashMap底层存储原理(⭐⭐⭐⭐⭐⭐)

HashMap存储示意图

HashMap底层机制及源码分析

扩容机制(和HashSet相同)

  1. HashMap底层维护了Node类型的数组table,默认为null
  2. 当创建对象时,将加载因子(loadfactor)初始化为0.75
  3. 当添加key-value时,通过key的hash值得到在table表的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key是否和准备加入的元素的key相等,如果相等,则直接替换value;如果不相等需要判断是树结构还是链表结构并作出相应处理。如果添加元素时发现容量不够则需要扩容。
  4. 第1次添加,需要扩容table容量为16,临界值(threshold)为12
  5. 以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的2倍即24,依此类推...
  6. 再Java 8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认=8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认=64),就会对该链表进行树化(红黑树)
package com.yxz.map_;

import java.util.HashMap;

public class HashMapSource {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        HashMap map = new HashMap();
        map.put("java", 10);
        map.put("php", 10);
        map.put("java", 20);
        System.out.println("map = " + map);

        /* 分析源码:
          1.执行构造器 new HashMap()
            初始化加载因子 loadfactor = 0.75
            HashMap$Node[] table = null
          2.执行put(),会调用hash()方法,计算key的hash值:return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
            public V put(K key, V value) { // key = "java", value = 10
                return putVal(hash(key), key, value, false, true);
            }
          3.执行putVal()
            final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
                Node<K,V>[] tab; Node<K,V> p; int n, i; // 辅助变量
                // 如果底层的table 数组为null,或者length = 0,就扩容到16
                if ((tab = table) == null || (n = tab.length) == 0)
                    n = (tab = resize()).length;
                // 取出hash值对应的table的索引位置的Node,如果为null,就直接把加入的k-v
                // 创建成一个Node,加入该位置即可
                if ((p = tab[i = (n - 1) & hash]) == null)
                    tab[i] = newNode(hash, key, value, null);
                else {
                    Node<K,V> e; K k;
                    if (p.hash == hash && // 如果table的索引位置的key的hash和新的key的hash值相同,并且满足两个条件之一的:
                    // 条件(1)table现有的节点的key和准备添加的key是同一个对象(==判断同一个对象)
                    // 条件(2)equals()返回真
                        ((k = p.key) == key || (key != null && key.equals(k))))
                        e = p;
                    else if (p instanceof TreeNode) // 如果当前table已有的Node是红黑树,就按照红黑树的方式处理
                        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                    else {
                        // 如果找到的节点后面是链表,就循环比较该链表上的每一个节点
                        for (int binCount = 0; ; ++binCount) { // 死循环,只有通过两种条件的break才能跳出
                            if ((e = p.next) == null) { // 第1个条件:整个链表遍历完都没有相同的节点
                                p.next = newNode(hash, key, value, null); // 链表尾加上该节点
                                // 加入新节点后,判断当前链表的节点个数,满8个就树化
                                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                    treeifyBin(tab, hash); // 树化
                                break; // 退出循环
                            }
                            if (e.hash == hash && // 第2个条件:循环比较中,有一个节点和准备加入的节点相同,就只是替换相同key的value值
                                ((k = e.key) == key || (key != null && key.equals(k))))
                                break; // 什么都不做,退出循环
                            p = e; // 更新p指针,保持和e指向统一
                        }
                    }
                    if (e != null) { // existing mapping for key
                        V oldValue = e.value;
                        if (!onlyIfAbsent || oldValue == null)
                            e.value = value; // 替换key对应的value值
                        afterNodeAccess(e);
                        return oldValue;
                    }
                }
                ++modCount; // 每增加一个Node,就size++
                if (++size > threshold) // 如果size > 临界值,就扩容
                    resize();
                afterNodeInsertion(evict);
                return null;
            }

           5.关于树化(转成红黑树)
            final void treeifyBin(Node<K,V>[] tab, int hash) {
                int n, index; Node<K,V> e;
                // 如果table为null,或者大小还没有到64,暂时不树化,只扩容
                if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                    resize();
                // table容量到64,且有一个链表节点数满8个 -> 链表树化
                else if ((e = tab[index = (n - 1) & hash]) != null)
         */
    }
}

5.3.2、Hashtable类

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {}

说明:

  1. 存放的元素是键值对key-value

  2. Hashtable的键和值都不能为null,否则会抛出异常NullPointerException

  3. Hashtable使用方法基本上和HashMap一样

  4. Hashtable是线程安全的(synchronized),HashMap线程不安全

  5. Hashtable底层有数组Entry[],初始化大小为11,Entry[]数组是Hashtabel的静态内部类Hashtable$Entry类型

    临界值threshold = 11 * 0.75 = 8

    扩容机制:当满足(count >= threshold)时,就按照int newCapacity = (oldCapacity << 1) + 1;的大小进行扩容。(新容量 = 原容量 * 2 + 1)

    执行方法addEntry(hash, key, value, index); 将K-V封装成Entry并添加

HashMap和Hashtable对比

支持版本 线程安全(同步) 效率 允许null键null值
HashMap 1.2 不安全 可以
Hashtable 1.0 安全 较低 不可以

5.3.3、Properties类

public class Properties extends Hashtable<Object,Object> {}

说明:

  1. Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据(key和value也不能为null,否则抛出空指针异常)。
  2. 它的使用特点和Hashtable类似
  3. Properties还可以用于从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改
  4. 开发中,xxx.properties文件通常作为配置文件,在IO流章节详解
  5. Properties的getProperty(String key)方法要求key值必须为String类型,get()方法的key值可以为任意类型

5.3.4、TreeMap类

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

说明:

  1. 类似于TreeSet类,这里TreeMap是双列存储,以key-value形式存储。
  2. 如果当前要加入的key和一个已经在TreeMap中的key值一样,则添加失败,但是会替换原有value值。
  3. TreeSet底层就是TreeMap,执行时源码一致,只是value值是变量。
  4. key不能为null,value可以为null。

开发中如何选择集合实现类

选择什么集合实现类,主要取决于业务操作特点,根据集合实现类特点进行选择

  1. 先判断存储的类型(一组对象[单列]或一组键值对[双列])

  2. 一组对象[单列]:Collection接口

    1. 允许重复:List

      增删操作多:LinkedList(底层 = 双向链表)

      改查操作多:ArrayList(底层 = Object类型的可变数组)

    2. 不允许重复:Set

      无序:HashSet(底层 = HashMap = 哈希表 = 数组 + 链表 + 红黑树)

      排序:TreeSet

      插入和取出顺序一致:LinkedHashSet(底层是LinkedHashMap, 底层 = 数组 + 双向链表)

  3. 一组键值对[双列]:Map

    1. 键无序:HashMap(底层 = 哈希表, JDK 7: 数组 + 链表, JDK 8: 数组 + 双链表 + 红黑树)
    2. 键排序:TreeMap
    3. 键插入和取出顺序一致:LinkedHashMap(底层是HashMap)
    4. 读取文件:Properties

面试题:HashSet和TreeSet分别如何实现去重的?

  1. HashSet去重机制:hashCode()+equals(). 底层先通过存入key对象, 进行运算得到一个hash值, 通过hash值得到对应的索引,如果发现table索引所在的位置没有数据则直接存放; 如果有数据, 则进行==或equals()比较(如果索引处是单链表则循环遍历比较), 比较后不相同就在表尾处加入, 否则不加入.
  2. TreeSet去重机制:如果传入了一个Comparator比较器(匿名内部类, 匿名对象), 就使用实现的compare()方法去重, 如果方法返回0就认为是相同元素(对象), 就不添加. 如果没有传入比较器, 则以添加的对象实现的Comparable接口的compareTo去重.(如果对象所属类型没有实现Comparable接口则抛出类型转换异常)

5.4、Collections

Collections工具类

  1. Collections是一个操作Set、List和Map等集合的工具类
  2. Colllections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作。

排序操作(均为static方法)

  1. reverse(List):反转List中元素的顺序
  2. shuffle(List):对List集合元素进行随机排序
  3. sort(List):根据元素的自然顺序对指定List集合元素按升序排序
  4. sort(List, Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
  5. swap(List, int, int):将指定List集合中的i处元素和j处元素进行交换

查找、替换操作

  1. Object max(Collection):根据元素的自然顺序, 返回给定集合中的最大元素
  2. Object max(Collection, Comparator):根据Comparator指定的顺序, 返回给定集合中的最大元素
  3. Object min(Collection):
  4. Object min(Collection, Comparator):
  5. int frequency(Collection, Object):返回指定集合中指定元素的出现次数
  6. void copy(List dest, List src):将src的内容复制到dest中
  7. boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对象的所有旧值
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Test {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("tom");
        list.add("smith");
        list.add("king");
        list.add("micai");
        list.add("queen");
        list.add("milan");
        list.add("micai");
        list.add("tom");
        // 1. reverse(List):反转List中元素的顺序
        Collections.reverse(list);
        System.out.println("反转后的\nlist = " + list);
        // 2. shuffle(List):对List集合元素进行随机排序
        Collections.shuffle(list);
        System.out.println("随机排序后的\nlist = " + list);
        // 3. sort(List):根据元素的自然顺序对指定List集合元素按升序排序
        Collections.sort(list);
        System.out.println("自然顺序升序排序后的\nlist = " + list);
        // 4. sort(List, Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
        Collections.sort(list, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String) o2).compareTo((String) o1); // 字典序降序排序
            }
        });
        System.out.println("字典序降序后的\nlist = " + list);
        // 5. swap(List, int, int):将指定List集合中的i处元素和j处元素进行交换
        Collections.swap(list, 0, 1);
        System.out.println("交换后的\nlist = " + list);

        // 1. Object max(Collection):根据元素的自然顺序, 返回给定集合中的最大元素
        System.out.println("自然顺序最大的元素 = " + Collections.max(list));
        // 2. Object max(Collection, Comparator):根据Comparator指定的顺序, 返回给定集合中的最大元素
        Object maxObj = Collections.max(list, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String) o1).length() - ((String) o2).length(); // 按字符串长度从小到大排序
            }
        });
        System.out.println("长度最大的元素 = " + maxObj);
        // 3. Object min(Collection):
        // 4. Object min(Collection, Comparator):
        // 5. int frequency(Collection, Object):返回指定集合中指定元素的出现次数
        System.out.println("micai出现的次数 = " + Collections.frequency(list, "micai"));
        // 6. void copy(List dest, List src):将src的内容复制到dest中
        // 为了完成一个完整的拷贝, 需要先给dest赋值, 大小和list.size()一样
        ArrayList dest = new ArrayList();
        for (int i = 0; i < list.size(); i++) {
            dest.add("");
        }
        Collections.copy(dest, list);
        System.out.println("拷贝后的\ndest = " + dest);
        // 7. boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对象的所有旧值
        Collections.replaceAll(list, "tom", "汤姆");
        System.out.println("替换tom后的\nlist = " + list);
    }
}

6、泛型

泛型(generic)的好处:

  1. 编译时, 编译器会检查添加元素的类型, 提高了安全性.
  2. 减少了类型转换的次数, 提高了效率.
  3. 不再提示编译警告.

泛型介绍:泛(广泛)型(数据类型)

  1. 泛型又称参数化类型, 即接收数据类型的类型, 是JDK 5.0出现的新特性, 解决数据类型的安全性问题.
  2. 在类声明或实例化时, 只要指定好需要的具体的类型即可.(类似于模板)
  3. Java泛型可以保证如果程序在编译时没有发出警告, 运行时就不会产生ClassCastException异常. 同时, 代码更加简洁, 健壮.
  4. 泛型的作用:可以在类声明时通过一个标识表示类中某个属性的类型, 或者是某个方法的返回值的类型, 或者是参数类型.
  5. 给泛型指定具体的泛型类型后, 只能存放指定类型的对象而不会向上转型, 从容器中取出对象时也是指定类型, 不再需要向下转型.
package com.yxz.generic;

import java.util.ArrayList;

@SuppressWarnings({"all"})
public class Generic02 {
    public static void main(String[] args) {
        // 传统方法 ==> 使用泛型
        // 1.当使用ArrayList<Dog> 表示存放到ArrayList集合中的元素是Dog类型
        // 2.如果编译器发现添加的类型不满足要求,就会报错(数据类型约束)
        // 3.遍历的时候,可以直接取出 Dog 类型而不是 Object
//        ArrayList arrayList = new ArrayList();
        ArrayList<Dog> arrayList = new ArrayList<Dog>();
        arrayList.add(new Dog("旺财", 10));
        arrayList.add(new Dog("发财", 5));
        arrayList.add(new Dog("大黄", 3)); // 放入时就是Dog类型, 而不会向上转型成Object
        // 不小心,添加了一只猫
//        arrayList.add(new Cat("招财猫", 4));

//        for (Object o : arrayList) {
//            Dog o1 = (Dog) o;
//            System.out.println(o1.getName() + ":" + o1.getAge() + "岁");
//        }
        for (Dog dog : arrayList) { // 取出时就是Dog, 无需再向下转型
            System.out.println(dog.getName() + ":" + dog.getAge() + "岁");
        }
    }
}

class Dog {
    private String name;
    private int age;

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

class Cat {
    private String name;
    private int age;

    public Cat(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

6.1、泛型语法

泛型的声明:

interface 接口<T>{}class 类名<K, V, S>{}

说明:

  1. 其中T,K,V,S不代表值, 而是表示类型
  2. 任意字母表示都可以(也可以使用其他标识符), 常用T表示, 是Type的缩写. 一般用单个大小字母表示泛型类型

泛型的实例化:要在类名后面指定类型参数的值(类型).

如:List strList = new ArrayList(); Iterator iterator = customers.iterator();

注意:

  1. 泛型的类型只能是引用类型, 不能是基本数据类型.
  2. 在给泛型指定泛型具体类型后, 可以传入该类型或者其子类类型.
  3. 泛型使用形式:
    1. List list1 = new ArrayList();
    2. List<Integer> list2 = new ArrayList<>(); -> 编译器会进行类型推断(推荐使用)
  4. 如果写成List list1 = new ArrayList(); -> 表示默认给它的泛型是Object类型 --等价于-> List list2 = new ArrayList();
    package com.yxz.generic;
    
    public class Generic03 {
        public static void main(String[] args) {
            // 注意: E具体的数据类型在定义Person对象的时候指定 -> 在编译期间就确定E的类型
            Person<String> person1 = new Person<String>("jack");
            person1.show(); // String
            /* 等价于:
            class Person {
                String s;
    
                public Person(String s) {
                    this.s = s;
                }
    
                public String hi() {
                    return s;
                }
            }
             */
    
            Person<Integer> person2 = new Person<Integer>(60);
            person2.show(); // Integer
            /* 等价于:
            class Person {
                Integer s;
    
                public Person(Integer s) {
                    this.s = s;
                }
    
                public Integer hi() {
                    return s;
                }
            }
             */
        }
    }
    
    // 在类声明时通过一个标识标识类中某个属性的类型
    class Person<E> { // 自定义泛型类
        private E s; // 属性使用泛型, 该数据类型在定义Person对象的时候指定 -> 在编译期间就确定E的类型
    
        public Person(E s) { // 参数类型使用泛型
            this.s = s;
        }
    
        public E hi() { // 返回值类型使用泛型
            return s;
        }
    
        public void show() {
            System.out.println(s.getClass());
        }
    }
    

    案例:编写一个Employee类, 属性(name,sal,birthday), 并且给每个属性创建getter,setter方法, 重写toString方法输出name,sal,birthday. 其中birthday属于MyDate类型(属性:year,month,day), 给每个属性创建getter,setter方法. 创建三个Employee对象, 并把这三个对象放入ArrayList集合中(ArrayList需要使用泛型定义), 对集合中的元素进行排序, 并遍历输出.

    排序方式:调用ArrayList的sort方法, 传入Comparator对象(使用泛型), 先按照name排序, 如果name相同, 则按照生日日期的先后排序.

    package com.yxz.generic;
    
    import java.util.ArrayList;
    import java.util.Comparator;
    
    @SuppressWarnings({"all"})
    public class GenericExercise {
        public static void main(String[] args) {
            Employee employee1 = new Employee("Micai", 120000, new MyDate(1997, 1, 21));
            Employee employee2 = new Employee("Micai", 600000, new MyDate(1997, 3, 20));
            Employee employee3 = new Employee("mary", 140000, new MyDate(1998, 5, 16));
            ArrayList<Employee> employees = new ArrayList<>();
            employees.add(employee1);
            employees.add(employee2);
            employees.add(employee3);
            System.out.println("----------------排序前-----------------");
            for (Employee employee : employees) {
                System.out.println(employee);
            }
            employees.sort(new Comparator<Employee>() {
                @Override
                public int compare(Employee emp1, Employee emp2) {
                    if (!(emp1 instanceof Employee && emp2 instanceof Employee)) {
                        System.out.println("类型不正确");
                        return 0;
                    }
                    // 比较name
                    int i = emp1.getName().compareTo(emp2.getName());
                    if (i != 0) {
                        return i;
                    }
                    // name相同,比较birthday
                    // 封装
                    return emp1.getBirthday().compareTo(emp2.getBirthday());
                }
            });
            System.out.println("-----------排序后-----------");
            for (Employee employee : employees) {
                System.out.println(employee);
            }
        }
    }
    @SuppressWarnings({"all"})
    class Employee {
        private String name;
        private double sal;
        private MyDate birthday;
    
        public Employee(String name, double sal, MyDate birthday) {
            this.name = name;
            this.sal = sal;
            this.birthday = birthday;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public double getSal() {
            return sal;
        }
        public void setSal(double sal) {
            this.sal = sal;
        }
        public MyDate getBirthday() {
            return birthday;
        }
        public void setBirthday(MyDate birthday) {
            this.birthday = birthday;
        }
        @Override
        public String toString() {
            return "Employee{" +
                    "name='" + name + '\'' +
                    ", sal=" + sal +
                    ", birthday=" + birthday +
                    '}';
        }
    }
    @SuppressWarnings({"all"})
    class MyDate implements Comparable<MyDate> { // 接口泛型指定为MyDate
        private int year;
        private int month;
        private int day;
    
        public MyDate(int year, int month, int day) {
            this.year = year;
            this.month = month;
            this.day = day;
        }
        public int getYear() {
            return year;
        }
        public void setYear(int year) {
            this.year = year;
        }
        public int getMonth() {
            return month;
        }
        public void setMonth(int month) {
            this.month = month;
        }
        public int getDay() {
            return day;
        }
        public void setDay(int day) {
            this.day = day;
        }
        @Override
        public String toString() {
            return "MyDate{" +
                    "year=" + year +
                    ", month=" + month +
                    ", day=" + day +
                    '}';
        }
        @Override
        public int compareTo(MyDate o) { // 泛型,指定比较MyDate
            // name相同, 比较birthday - year
            int yearMinus = year - o.getYear();
            if (yearMinus != 0) {
                return yearMinus;
            }
            // year相同, 比较month
            int monthMinus = month - o.getMonth();
            if (monthMinus != 0) {
                return monthMinus;
            }
            // month相同, 比较day
            return day - o.getDay();
        }
    }
    

    6.2、自定义泛型

    6.2.1、泛型类

    基本语法:

    class 类名<T, R...> {} // ...表示可以指定多个泛型
    

    注意:

    1. 普通成员可以使用泛型(属性,方法)
    2. 使用泛型的数组不能初始化. -> T[] arr = new T[8];(错误写法) -> 数组在new时不能确定T的类型 -> 无法确定在内存中为数组分配的空间大小
    3. 静态方法/静态属性中不能使用类的泛型.(静态是和类相关的, 在类加载时, 对象还没有创建. 泛型是在创建对象时才指定, 与静态的机制矛盾. 如果静态方法和静态属性使用了泛型, JVM就不能完成类的初始化加载)
    4. 泛型类的类型是在创建对象时确定的(因为创建s对象时,需要指定泛型的类型)
    5. 如果在创建对象时, 没有指定类型, 默认为Object

    6.2.2、泛型接口

    基本语法:

    interface 接口名<T, R...> {}
    

    注意:

    1. 接口中, 静态成员也不能使用泛型(这个和泛型类规定一样) <- 接口中的属性都是public static final

    2. 泛型接口的类型, 在继承接口或者实现接口时确定.

      interface A<T, R> {
      //    T name; 错误:name是public static final修饰的
          T get(R r);
          void hi(R r);
      }
      interface B extends A<String, Double> {} // 继承泛型接口时必须确定泛型的具体类型
      class AA implements B { // 实现B接口必须实现B接口以及B接口继承的父类A接口
          @Override
          public String get(Double aDouble) { // 自动填充泛型类型
              return null;
          }
          @Override
          public void hi(Double aDouble) {} // 自动填充泛型类型
      }
      class BB implements A { // 等价于class BB implements A<Object, Object>{}, 建议显式指定Objcct
      }
      
    3. 没有指定类型, 默认为Object. 建议显式指定泛型的具体类型

    6.2.3、泛型方法

    基本语法:

    修饰符 <T, R...> 返回类型 方法名(参数列表){}
    

    注意:

    1. 泛型方法, 可以定义在普通类中, 也可以定义在泛型类中.
    2. 当泛型方法被调用时, 类型会确定.
    3. 泛型方法可以是普通泛型方法, 也可以是静态泛型方法.
    4. public void f(E e){} 修饰符后没有<T, R...>, 则f方法不是泛型方法, 而是使用了泛型参数
    public class Test {
        @SuppressWarnings({"all"})
        public static void main(String[] args) {
            Car car = new Car();
    //        car.fly(10, 100); // 编译出错, 多个方法都匹配
            car.fly("micai", 26); // 调用泛型方法时, 传入参数, 编译器就会自动确定类型
            A<String> stringA = new A<>();
            stringA.hi("aaaa");
            A.ok("hello");
        }
    }
    
    class Car { // 普通类
        public <T, R> void fly(T t, R r) {// 泛型方法
            System.out.println(t.getClass() + ", " + r.getClass());
        }
    
        public <T, R> void fly(int a, R r) {// 泛型方法重载, 语法没问题, 但是无法调用该方法, 编译器无法确定匹配哪个方法
            System.out.println("a = " + a + ", " + r.getClass());
        }
    }
    
    class A<T> { // 泛型类
        // 泛型方法
        public <U> void f(U u, T t) {// 可以使用类声明的泛型, 也可以使用自己声明的泛型
            System.out.println(u.getClass() + ", t = " + t.getClass());
        }
    
        public void hi(T t) { // 不是泛型方法, 方法使用了类声明的泛型 T = String
            System.out.println(t.getClass()); // class java.lang.String
            System.out.println(t.getClass().getName()); // java.lang.String
            System.out.println(t.getClass().getSimpleName()); // String
        }
        
        public static <P> void ok(P p){ // 静态泛型方法
            System.out.println("ok方法的p的类型 = " + p.getClass());
        }
    }
    

    6.3、泛型继承和通配符

    1. 泛型不具备继承性. 如:List list = new ArrayList(); --> 错误写法, 泛型不具备继承性

    2. 通配符(注意: ?的位置不能换):

      1. <?>:支持任意泛型类型
      2. <? extends A>:支持A类以及A类的子类, 规定了泛型的上限.
      3. <? super A>:支持A类以及A类的父类, 不限于直接父类, 规定了泛型的下限
      import java.util.ArrayList;
      import java.util.List;
      
      public class Test {
          public static void main(String[] args) {
              List<Object> list1 = new ArrayList<>();
              List<String> list2 = new ArrayList<>();
              List<A> list3 = new ArrayList<>();
              List<B> list4 = new ArrayList<>();
              List<C> list5 = new ArrayList<>();
      
              // List<?> c:表示接受任意的泛型类型
              printCollection1(list1); // Object
              printCollection1(list2); // String
              printCollection1(list3); // A
              printCollection1(list4); // B
              printCollection1(list5); // C
      
              // List<? extends A> c:表示接受A类及其子类类型, 规定泛型的上限为A类
      //        printCollection2(list1); // ×: Object不是A的子类
      //        printCollection2(list2); // ×: String不是A的子类
              printCollection2(list3); // ✔: 可以接受上限A类
              printCollection2(list4); // ✔: B类是A的子类
              printCollection2(list5); // ✔: C类是A的子类
      
              // List<? super A> c:表示接受A类以及A的父类, 不局限于直接父类, 规定泛型的下限
              printCollection3(list1); // ✔: Object是A的父类
      //        printCollection3(list2); // ×: String不是A的父类
              printCollection3(list3); // ✔: 可以接受下限A类
      //        printCollection3(list4); // ×: B类不是A的父类
      //        printCollection3(list5); // ×: C类不是A的父类
          }
      
          public static void printCollection1(List<?> c) {}
          public static void printCollection2(List<? extends A> c) {}
          public static void printCollection3(List<? super A> c) {}
      
      }
      class A {}
      class B extends A {}
      class C extends B {}
      
    3. 扩展1:JUnit单元测试

      1. JUnit是一个Java语言的单元测试框架.
      2. 多数Java的开发环境都已经集成了JUnit作为单元测试工具.
      3. 在需要单元测试的方法上加注解@Test, 再使用快捷键Alt+Enter选择导入. 一般使用JUnit 5.0的版本.
      4. 第一次使用@Test注解时需要将JUnit加载到类路径, 在同一个包下不能有Test类.

      泛型和JUnit的案例:

      User.java

      package com.yxz.junit_;
      
      public class User {
          private int id;
          private int age;
          private String name;
      
          public User(int id, int age, String name) {
              this.id = id;
              this.age = age;
              this.name = name;
          }
      
          public int getId() {
              return id;
          }
      
          public void setId(int id) {
              this.id = id;
          }
      
          public int getAge() {
              return age;
          }
      
          public void setAge(int age) {
              this.age = age;
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          @Override
          public String toString() {
              return "User{" +
                      "id=" + id +
                      ", age=" + age +
                      ", name='" + name + '\'' +
                      '}';
          }
      }
      

      DAO.java

      package com.yxz.junit_;
      
      import java.util.*;
      
      /**
       * 定义一个泛型类DAO<T>, 在其中定义一个Map成员变量, Map的键为String类型, 值为T类型
       * 分别创建以下方法:
       * 1.public void save(String id, T entity):保存T类型的对象到Map成员变量中
       * 2.public T get(String id):从map中获取id对应的对象
       * 3.public void update(String id, T entity):替换map中key为id的内容, 改为entity对象
       * 4.public List<T> list():返回map中存放的所有T对象
       * 5.public void delete(String id):删除指定id对象
       */
      
      @SuppressWarnings({"all"})
      public class DAO<T> {
          private Map<String, T> m = new HashMap<>();
      
          public void save(String id, T entity) {
              m.put(id, entity);
          }
      
          public T get(String id) {
              return m.get(id);
          }
      
          public void update(String id, T entity) {
              m.put(id, entity);
          }
      
          public List<T> list() {
              List<T> list = new ArrayList<>();
              // 遍历map, 将map的所有value封装到ArrayList即可
              Set<String> keySet = m.keySet();
              for (String key : keySet) {
                  list.add(get(key)); // 调用本类的get方法
              }
              return list;
          }
      
          public void delete(String id) {
              m.remove(id);
          }
      }
      

      GenericTest.java

      package com.yxz.junit_;
      
      import org.junit.jupiter.api.Test;
      
      import java.util.List;
      
      /**
       * @Description: TODO
       * @Author: Yao Xuan zhi
       * @Create: 2022-11-10, 22:15:23
       * @IDE: IntelliJ IDEA
       */
      
      public class GenericTest {
          @Test
          public void testList() {
              DAO<User> dao = new DAO<>();
              dao.save("001", new User(1, 26, "Micai"));
              dao.save("002", new User(2, 24, "Milan"));
              dao.save("003", new User(3, 29, "mary"));
              List<User> list = dao.list();
              System.out.println("list = " + list);
      
              dao.update("003", new User(3, 31, "An qi"));
              dao.delete("001");// 删除
              System.out.println("=================修改后==============");
              list = dao.list();
              System.out.println("list = " + list);
              System.out.println("id = 003 " + dao.get("003"));
          }
      }
      

      Java绘图技术

      绘图原理:

      Component类提供了两个和绘图相关最重要的方法:

      1. paint(Graphics g): 绘制组件的外观, 提供了大量方法绘制图形
      2. repaint(): 刷新组件的外观

      paint()方法何时被调用:

      1. 当组件第一次在屏幕显式的时候, 程序会自动的调用paint()方法来绘制组件
      2. 窗口最小化, 再最大化
      3. 窗口的大小发生变化
      4. repaint()方法被调用

      GUI界面程序设计

      Graphics类

      Graphics类可以理解成就是画笔, 提供了各种绘制图形的方法.(参考JDK API文档)

      常用方法:

      1. 画直线:drawLine(int x1, int y1, int x2, int y2)
      2. 画矩形边框:drawRect(int x, int y, int width, int height)
      3. 画椭圆边框:drawOval(int x, int y, int width, int height)
      4. 填充矩形:fillRect(int x, int y, int width, int height)
      5. 填充椭圆:fillOval(int x, int y, int width, int height)
      6. 画图片:drawImage(Image img, int x, int y, ImageObserver observer)
      7. 画字符串:drawString(String str, int x, int y)
      8. 设置画笔的字体:setFont(Font font)
      9. 设置画笔的颜色:setColor(Color c)
      package com.yxz.draw;
      
      import javax.swing.*;
      import java.awt.*;
      
      public class DrawCircle extends JFrame { // JFrame对应窗口, 可以理解成一个画框
          // 定义一个画板
          private MyPanel mp = null;
      
          public DrawCircle() {
              // 初始化面板
              mp = new MyPanel();
              // 把面板放入到窗口(画框)
              this.add(mp);
              // 设置窗口大小
              this.setSize(1500, 1500);
              // 设置可见
              this.setVisible(true);
              // 当点击窗口的×时, 程序完全退出
              this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
          }
      
          public static void main(String[] args) {
              new DrawCircle();
          }
      }
      
      class MyPanel extends JPanel {
          /**
           * MyPanel 对象就是一个画板
           * Graphics提供了很多绘图方法
           *
           * @param g :可以理解成是一支画笔
           */
          @Override
          public void paint(Graphics g) { // 绘图方法
              super.paint(g); // 调用父类方法完成初始化
              //g.drawOval(10, 10, 100, 100);
              //1. 画直线:drawLine(int x1, int y1, int x2, int y2)
              // g.drawLine(10, 10, 100, 100);
              //2. 画矩形边框:drawRect(int x, int y, int width, int height)
              // g.drawRect(10, 10, 100, 100);
              //3. 画椭圆边框:drawOval(int x, int y, int width, int height)
              // g.drawOval(10, 10, 100, 100);
              //4. 填充矩形:fillRect(int x, int y, int width, int height)
              // g.setColor(Color.CYAN);
              // g.fillRect(10, 10, 100, 100);
              //5. 填充椭圆:fillOval(int x, int y, int width, int height)
              // g.setColor(Color.RED);
              // g.fillOval(15, 15, 100, 100);
              //6. 画图片:drawImage(Image img, int x, int y, ImageObserver observer)
              // (1)获取图片资源, /葡萄_11.png 表示在该项目的根目录src文件夹下去获取 葡萄_11.png 图片资源
              // Image image = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/葡萄_11.png"));
              // g.drawImage(image, 10, 10, 886, 1041, this);
              //7. 画字符串:drawString(String str, int x, int y)
              g.setColor(Color.MAGENTA);
              g.setFont(new Font("Console", Font.BOLD, 50));
              g.drawString("美女嘎嘎", 100, 100);
              //8. 设置画笔的字体:setFont(Font font)
              //9. 设置画笔的颜色:setColor(Color c)
          }
      }
      

      扩展2:Java事件处理机制

      Java事件处理是采取“委派事件模型”.当事件发生时, 产生事件的对象, 会把此“消息”传递给“事件的监听者”处理. 这里所说的“信息”实际上就是java.awt.event事件类库里某个类所创建的对象. 称为“事件的对象”.

      事件处理机制示意图

      事件源: 事件源是一个产生事件的对象, 比如按钮, 窗口等.

      事件: 事件就是承载事件源状态改变时的对象. 比如:当键盘事件,鼠标事件,窗口事件等等, 会生成一个事件对象, 该对象保存着当前事件很多信息, 比如KeyEvent对象有含义被按下键的Code值. java.awt.event包javax.swing.event包中定义了各种事件类型.

      事件监听器接口

      1. 当事件源产生一个事件, 可以传送给事件监听者处理
      2. 事件监听者实际上就是一个类, 该类实现了某个事件监听器接口.
      3. 事件监听器接口有多种, 不同的事件监听器接口可以监听不同的事件, 一个类可以实现多个接口.
      4. 这些接口在java.awt.event包javax.swing.event包中定义, 列出常用的事件监听器接口.

      案例:在键盘上按下 上下左右键 可以移动小球

      package com.yxz.event_;
      
      import javax.swing.*;
      import java.awt.*;
      import java.awt.event.KeyEvent;
      import java.awt.event.KeyListener;
      
      public class BallMove extends JFrame {
          // 定义面板
          private MyPanel mp = null;
      
          public BallMove() {
              mp = new MyPanel();
              this.add(mp);
              this.setSize(400, 400);
              // 窗口JFrame对象可以监听键盘事件, 即可以监听到在mp面板上发生的键盘事件
              this.addKeyListener(mp);
              this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
              this.setVisible(true);
          }
      
          public static void main(String[] args) {
              new BallMove();
          }
      }
      
      // KeyListener 是键盘监听器, 可以监听键盘事件(按了什么键)
      class MyPanel extends JPanel implements KeyListener {
          // 为了让小球可以移动, 把小球的左上角的坐标设置成变量
          int x = 10;
          int y = 10;
      
          /**
           * 面板画出小球
           *
           * @param g
           */
          @Override
          public void paint(Graphics g) {
              super.paint(g);
              g.fillOval(x, y, 20, 20); // 默认黑色
          }
      
          /**
           * 有字符输入时, 该方法就会触发
           *
           * @param e
           */
          @Override
          public void keyTyped(KeyEvent e) {}
      
          /**
           * 当某个键按下, 该方法就会触发
           *
           * @param e
           */
          @Override
          public void keyPressed(KeyEvent e) {
              // System.out.println((char)e.getKeyCode() + "被按下...");
              // 根据用户按下的不同键, 来处理小球的移动(上下左右的键)
              // 在java中,会给每个键分配一个值
              if (e.getKeyCode() == KeyEvent.VK_DOWN) {
                  y++;
              } else if (e.getKeyCode() == KeyEvent.VK_UP) {
                  y--;
              } else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
                  x--;
              } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
                  x++;
              }
              // 让面板重绘
              this.repaint();
          }
      
          /**
           * 当某个键被松开, 该方法就会触发
           *
           * @param e
           */
          @Override
          public void keyReleased(KeyEvent e) {}
      }
      

      7、线程(基础)

      程序(Program):是为了完成特定任务, 用某种语言编写的一组指令的集合.

      进程(Process):

      1. 进程是指运行中的程序, 比如使用QQ软件, 就启动了一个进程, 操作系统(OS)就会为该进程分配内存空间.
      2. 进程是程序的一次执行过程, 或是正在运行的一个程序. 进程是动态过程, 有它自身的产生, 存在和消亡的过程.
      public class Test {
          @SuppressWarnings({"all"})
          public static void main(String[] args) {
              Runtime runtime = Runtime.getRuntime();
              // 获取当前电脑的cpu核心数量
              int cpuNums = runtime.availableProcessors();
              System.out.println("当前电脑的cpu核心数 = " + cpuNums);
          }
      }
      

      7.1、线程介绍

      线程(Thread):

      1. 线程是由进程创建的, 是进程的一个实体.
      2. 一个进程可以有多个线程.
      3. 单线程:同一个时刻, 只允许执行一个线程.
      4. 多线程:同一个时刻, 可以执行多个线程. 如:一个qq进程, 可以同时打开多个聊天窗口, 一个迅雷进程可以同时下载多个文件
      5. 并发:同一个时刻, 多个任务交替执行, 造成一种“貌似同时”的错觉, 简单的说, 单核CPU实现的多任务就是并发. 在同一个时刻点只执行一个任务.
      6. 并行:同一个时刻, 多个任务同时执行. 多核CPU可以实现并行.

      Thread类继承关系图

      7.2、线程使用

      7.2.1、继承 Thread

      当一个类继承了Thread后, 该类就可以当作线程使用. 继承Thread后, 一般会重写run()方法, 实现需要的业务逻辑代码

      Thread类的run()方法实现了Runnable接口的run()方法.

      在多线程中, 如果主线程结束了, 但是还有子线程运行, 并不会导致整个应用程序结束.

      如果在主线程中直接调用继承Thread类的run()方法, 并不是真正的启动一个子线程, 而只是调用了一个普通方法, 此时主线程会阻塞, 直到run()方法调用结束才会继续执行. 如:cat.run();

      package com.yxz.thread_;
      
      public class Thread01 {
          public static void main(String[] args) throws InterruptedException {
              // 创建Cat对象, 当作线程使用
              Cat cat = new Cat();
              cat.start(); // 启动线程 -> 最终会执行run()方法
              // cat.run(); 这里run()方法只是一个普通方法, 没有真正启动一个子线程
              // 当main线程启动一个子线程Thread-0, 主线程并不会阻塞, 而是继续执行
              // 这时主线程和子线程交替执行 -> 多线程
              /*
                  1.调用start()方法:
                  public synchronized void start() {
                      if (threadStatus != 0)
                          throw new IllegalThreadStateException();
                          group.add(this);
      
                      boolean started = false;
                      try {
                          start0();
                          started = true;
                      } finally {
                          try {
                              if (!started) {
                                  group.threadStartFailed(this);
                              }
                          } catch (Throwable ignore) {
      
                          }
                      }
                  }
                  2.private native void start0();
                  // start0()是本地方法, 由JVM调用, 底层是C/C++实现的.
                  // 真正实现多线程的效果是start0(), 而不是run()方法
                  start()方法小于start0()方法后, 该线程并不一定会立马执行, 只是将线程变成了可运行状态
                  具体什么时候执行, 取决于CPU, 由CPU统一调度
               */
              System.out.println("主线程继续执行" + Thread.currentThread().getName()); // main线程
              for (int i = 0; i < 60; i++) {
                  System.out.println("主线程 i = " + i);
                  Thread.sleep(1000);
              }
          }
      }
      
      // 1.当一个类继承了Thread类, 该类就可以当作线程使用
      // 2.一般情况会重写run方法, 实现业务逻辑
      // 3.run() Thread类实现了Runnable接口的run方法
      /* Thread实现的run方法
          @Override
          public void run() {
              if (target != null) {
                  target.run();
              }
          }
       */
      class Cat extends Thread {
          private int times = 0;
          @Override
          public void run() { // 重写run方法, 写业务逻辑
              while (true) {
                  // 线程每隔1秒, 在控制台输出语句
                  System.out.println("喵喵, 我是小猫咪" + (++times) + ", 线程名 = " + Thread.currentThread().getName());
                  // 线程休眠1秒
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  if (times == 80) {
                      break;
                  }
              }
      
          }
      }
      

      在程序运行时(进程运行时), 在终端输入命令:jconsole 调用Java监视和管理控制台, 查看线程运行情况.

      线程监控

      7.2.2、实现 Runnable

      继承Thread使用线程的问题: Java是单继承机制, 在某些情况下一个类可能以及继承了某个父类, 这时再用继承Thread类方法来创建线程就不可能.

      实现Runnable接口来创建线程(建议使用) ------静态代理设计模式

      package com.yxz.thread_;
      
      public class Thread02 {
          public static void main(String[] args) {
              Dog dog = new Dog();
              // dog.start(); // 这里不能调用start()方法, Runnable接口没有start()方法
              Thread thread = new Thread(dog);
              thread.start();
      
              Tiger tiger = new Tiger();
              ThreadProxy threadProxy = new ThreadProxy(tiger); // 静态代理模式, tiger实现了Runnable接口
              threadProxy.start(); // 调用线程代理类(相当于Thread类)的start方法
          }
      }
      
      class Animal {
      }
      
      class Tiger extends Animal implements Runnable {
      
          @Override
          public void run() {
              System.out.println("老虎嗷嗷嗷叫...");
          }
      }
      
      // 线程代理类, 模拟了一个极简的Thread类
      class ThreadProxy implements Runnable {
          private Runnable target = null;
      
          public ThreadProxy(Runnable target) { // 静态代理模式
              this.target = target;
          }
      
          @Override
          public void run() {
              if (target != null) {
                  target.run(); // 动态绑定(此时target=tiger, 运行类型Tiger)
              }
          }
      
          public void start() { // 模拟Thread类的start方法
              start0();
          }
      
          public void start0() { // 模拟Thread类的start0()方法 -> 真正实现多线程的方法
              run();
          }
      }
      
      class Dog implements Runnable {
          private int count = 0;
      
          @Override
          public void run() {
              while (true) {
                  System.out.println("小狗汪汪叫...hi" + (++count) + ", 线程名 = " + Thread.currentThread().getName());
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  if (count == 10) {
                      break;
                  }
              }
          }
      }
      

      继承Thread VS 实现Runnable

      1. 从Java设计来看, 通过继承Thread或者实现Runnable接口来创建线程本质上没有区别, Thread类本身就实现了Runnable接口.
      2. 实现Runnable接口方式更加适合多个线程共享一个资源的情况, 并且避免了单继承机制的弊端.(建议使用Runnable接口)

      7.3、线程方法

      线程常用的方法:

      1. setName()/getName():设置/获取线程名称, 使之与参数name相同.
      2. start():底层会创建新的线程, 调用run()方法, 不会启动新的线程.
      3. run():调用线程对象的run()方法.
      4. setPriority()/getPriority():修改/获取线程的优先级
      5. interrupt():中断线程, 但并没有真正的结束线程. 所以一般用于中断正在休眠的线程.
      6. sleep():线程的静态方法, 使当前线程休眠.
      7. yield():线程的礼让, 让出CPU, 让其他线程执行, 但礼让的时间不确定, 所以也不一定礼让成功.
      8. join():线程的插队. 插队的线程一旦插队成功, 则肯定先执行完插入的线程所有的任务.
      9. setDaemon(Boolean b):设置当前线程是否为守护线程.(必须在start()启动该线程前调用该方法, 否则抛出异常IllegalThreadStateException)

      用户线程:也叫工作线程, 当线程的任务执行完或通知方式结束.

      守护线程:一般是为工作线程服务的, 当所有的用户线程结束, 守护线程自动结束. 常见的守护线程:垃圾回收机制

      7.4、线程生命周期

      JDK中用Thread.State枚举了线程的几种状态:

      • NEW:尚未启动的线程处于此状态
      • RUNNABLE:在Java虚拟机中执行的线程处于此状态, 此时线程并不一定在运行, 也有可能是就绪状态等待调度器分配CPU(可以细化成ReadyRunning两种状态)
      • BLOCKED:被阻塞等待监视器锁定的线程处于此状态
      • WAITING:正在等待另一个线程执行待定动作的线程处于此状态
      • TIMED_WATING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
      • TERMINATED:已退出的线程处于此状态

      线程的6种状态示意图(⭐⭐⭐⭐⭐)

      线程状态转换图

      7.5、Synchronized

      线程同步机制:

      1. 在多线程编程, 一些敏感数据不允许被多个线程同时访问, 此时就使用同步访问技术, 保证数据在任何同一时刻, 最多有一个线程访问, 以保证数据的完整性.
      2. 简单来说, 线程同步, 即当有一个线程在对内存进行操作时, 其他线程都不可以对这个内存地址进行操作, 直到该线程完成操作, 其他线程才能对该内存地址进行操作.

      同步具体方法:使用synchronized关键字

      1. 同步代码块

        synchronized(对象) { // 得到对象的锁, 才能操作同步代码
            // 需要被同步的代码; -> 临界区
        }
        
      2. synchronized还可以放在方法声明中, 表示整个方法为同步方法

        public synchronized void m(String name) {
            // 需要被同步的代码;
        }
        
      package com.yxz.thread_;
      
      /**
       * 多个窗口卖同一种票, 保证无票时不会再卖出一张票
       */
      
      public class SellTicket {
          public static void main(String[] args) {
              SellTicket03 sellTicket03 = new SellTicket03();
              new Thread(sellTicket03).start(); // 第一个线程访问对象sellTicket03
              new Thread(sellTicket03).start(); // 第二个线程访问对象sellTicket03
              new Thread(sellTicket03).start(); // 第三个线程访问对象sellTicket03
          }
      }
      
      class SellTicket03 implements Runnable {
          private int ticketNum = 100;
          private boolean loop = true;
          Object obj = new Object();
          
          // 同步静态方法的锁为当前类本身(SellTicket03.class)
          // public synchronized static void m1(){} 锁加在SellTicket03.class上
          public synchronized static void m1() {
          }
          
          public static void m2() { // 静态方法
              synchronized(SellTicket03.class) {
                  System.out.println("同步代码块");
              }
          }
      
          // 方法1:同步方法, public synchronized void sell(){} 这时互斥锁在this对象
          public synchronized void sell() { // 同步方法, 保证同一时刻只有一个线程能访问该方法
              // 也可以写成同步代码块
              // 方法2:同步代码块
              synchronized(this) { // 也可以换另一个对象synchronized(obj), 但是必须保证三个线程访问同一个对象
                  if (ticketNum <= 0) {
                      System.out.println("售票结束...");
                      loop = false; // 当票卖完时, 设置循环条件为false, 三个线程进入该条件就会退出
                      return;
                  }
      
                  try {
                      Thread.sleep(50);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
      
                  System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" +
                          " 剩余票数 = " + (--ticketNum));
              }
          }
          @Override
          public void run() {
              while (loop) {
                  sell(); // 循环调用sell方法
              }
          }
      }
      

      7.6、互斥锁

      说明:

      1. 在Java语言中, 引入了对象互斥锁的概念, 来保证共享数据操作的完整性.
      2. 每个对象都对应于一个可称为“互斥锁”的标记. 这个标记用来保证在任一时刻, 只能有一个线程访问该对象.
      3. 关键字synchronized来与对象的互斥锁联系. 当某个对象用synchronized修饰时, 表明该对象在任一时刻只能由一个线程访问.
      4. 同步的局限性:导致程序的执行效率要降低.
      5. 同步方法(非静态的)的锁可以是this, 也可以是其他对象(要求是同一个对象).
      6. 同步方法(静态的)的锁为当前类本身(类.class).

      注意:

      1. 同步方法如果没有使用static修饰, 默认锁对象为this
      2. 如果是静态方法, 默认锁对象:当前类.class
      3. 实现互斥锁的步骤:
        1. 需要先分析上锁的代码
        2. 选择同步代码块或同步方法
        3. 要求多个线程的锁对象为同一个(共享一个对象).

      7.7、死锁

      每个线程都占用了对方的锁资源, 但是都不肯相让, 导致了死锁.

      以下操作会释放锁:

      1. 当前线程的同步方法(同步代码块)执行结束
      2. 当前线程在同步代码块(同步方法)中遇到break, return.
      3. 当前线程在同步代码块(同步方法)中出现了未处理的Error或Exception, 导致异常结束
      4. 当前线程在同步代码块(同步方法)中执行了线程对象的wait()方法, 当前线程暂停, 并释放锁.

      注意:以下操作不会释放锁

      1. 线程执行同步代码块(同步方法)时, 程序调用Thread.sleep()|Thread.yield()方法暂停当前线程的执行, 但不会释放锁.

      2. 线程执行同步代码块(同步方法)时, 其他线程调用了该线程的suspend()方法将该线程挂起, 该线程不会释放锁.

        应尽量避免使用suspend()和resume()方法来控制线程, 方法已经过时, 不再推荐使用.

      package com.yxz.thread_;
      
      import java.util.Scanner;
      
      /**
       * 案例: 线程之间的协调控制
       * 1.在main方法中启动两个子线程
       * 2.第1个线程循环随机打印100以内的整数
       * 3.直到第2个线程从键盘读取了"Q"命令
       */
      
      public class Homework01 {
          public static void main(String[] args) {
              A a = new A(); // a线程
              B b = new B(a); // b线程
              a.start(); // 启动a线程
              b.start(); // 启动b线程
              System.out.println("主线程main退出");
          }
      }
      
      class A extends Thread {
          private boolean loop = true;
      
          public void setLoop(boolean loop) {
              this.loop = loop;
          }
      
          @Override
          public void run() {
              while (loop) { // 循环执行a线程, 直到b线程中修改了loop
                  System.out.println((int) (Math.random() * 100 + 1));
                  // 休眠
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
              System.out.println("a线程退出..");
          }
      }
      
      class B extends Thread {
          private A a;
          private Scanner scanner = new Scanner(System.in);
      
          public B(A a) {
              this.a = a;
          }
      
          @Override
          public void run() {
              // 接收用户的输入
              System.out.println("b线程提示:请输入你的指令(Q)表示退出:");
              char key = scanner.next().toUpperCase().charAt(0);
              if (key == 'Q') { // 控制a线程的结束
                  // 以通知的方式结束a线程
                  a.setLoop(false);
                  System.out.println("b线程退出.");
              }
          }
      }
      

      8、IO 流

      8.1、文件

      文件(File)就是保存数据的地方.

      文件流: 文件在程序中是以流的形式来操作的.

      1. 流:数据在数据源(文件)和程序(内存)之间经历的路径.
      2. 输入流:数据从数据源(文件)到程序(内存)的路径.
      3. 输出流:数据从程序(内存)到数据源(文件)的路径.

      File类继承关系图

      常用的文件操作

      创建文件的方法:

      1. newFile(String pathname):根据路径构建一个File对象
      2. newFile(File parent, String child):根据父目录文件 + 子路径构建
      3. newFile(String parent, String child):根据父目录 + 子路径构建
      4. createNewFile():创建新文件
      package com.yxz.file_;
      
      import org.junit.jupiter.api.Test;
      
      import java.io.File;
      import java.io.IOException;
      
      public class FileCreate {
          public static void main(String[] args) {
          }
      
          //方式1. newFile(String pathname):根据路径构建一个File对象
          @Test
          public void create01() {
              // 双反斜杠(\\)表示一个反斜杠, 第一个反斜杠(\)是转义符号, 冒号(:)必须是英文半角的冒号
              // 路径可以使用(\\)或者(/), 建议使用双反斜杠(\\)表示路径
              String filePath = "d:\\Java\\Demo\\news1.txt";
              File file = new File(filePath);
              try {
                  file.createNewFile();
                  System.out.println("文件1创建成功");
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      
          //方式2. newFile(File parent, String child):根据父目录文件 + 子路径构建
          @Test
          public void create02() {
              // 如果文件要保存在路径 d:\\Java\\Demo\\news2.txt
              // 则父目录文件(d:\\Java\\Demo\\), 子路径(news2.txt)
              File parentFile = new File("d:\\Java\\Demo\\");
              String fileName = "news2.txt";
              // 这里的file只是在内存中创建一个对象, 与磁盘没有联系
              File file = new File(parentFile, fileName);
              try {
                  // 只有执行了createNewFile方法, 才会真正的在磁盘中创建文件
                  file.createNewFile(); // 将文件信息写入到磁盘
                  System.out.println("文件2创建成功");
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      
          //方式3. newFile(String parent, String child):根据父目录 + 子路径构建
          @Test
          public void create03() {
              String parentPath = "d:/Java/Demo\\";
              String fileName = "news3.txt";
              File file = new File(parentPath, fileName);
              try {
                  file.createNewFile();
                  System.out.println("文件3创建成功");
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      }
      

      获取文件的相关信息的方法:

      1. getName():获取文件名
      2. getAbsolutePath():获取文件的绝对路径
      3. getParent():获取文件的父目录
      4. length():获取文件的大小, 按字节来统计
      5. exists():文件是否存在
      6. isFile():是不是一个文件
      7. isDirectory():是不是一个目录
      package com.yxz.file_;
      
      import org.junit.jupiter.api.Test;
      
      import java.io.File;
      
      public class FileInformation {
          public static void main(String[] args) {}
          // 获取文件的信息
          @Test
          public void info() {
              // 1.创建文件对象
              File file = new File("d:\\Java\\Demo\\news1.txt");
              // 2.调用相应的方法, 得到对应的信息
              System.out.println("文件名: " + file.getName());
              System.out.println("文件" + file.getName() + "的绝对路径=" + file.getAbsolutePath());
              System.out.println("文件" + file.getName() + "的父目录=" + file.getParent());
              System.out.println("文件" + file.getName() + "是否存在=" + file.exists());
              System.out.println("文件" + file.getName() + "是不是一个文件=" + file.isFile());
              System.out.println("文件" + file.getName() + "是不是一个目录=" + file.isDirectory());
          }
      }
      

      目录的操作和文件删除

      1. mkdir():创建一级目录
      2. mkdirs():创建多级目录
      3. delete():删除空目录或文件
      package com.yxz.file_;
      
      import org.junit.jupiter.api.Test;
      
      import java.io.File;
      
      public class Directory_ {
          public static void main(String[] args) {
          }
      
          // 判断 d:\\Java\\Demo\\news3.txt 是否存在, 如果存在就删除
          @Test
          public void m1() {
              String filePath = "d:\\Java\\Demo\\news3.txt";
              File file = new File(filePath);
              if (file.exists()) {
                  if (file.delete()) {
                      System.out.println(filePath + "删除成功");
                  } else {
                      System.out.println(filePath + "删除失败");
                  }
              } else {
                  System.out.println("该文件不存在");
              }
          }
      
          // 判断 d:\\Java\\Demo\\temp 目录是否存在, 如果存在就删除
          // Java中, 目录也被当作一个文件(特殊的文件)
          @Test
          public void m2() {
              String filePath = "d:\\Java\\Demo\\temp";
              File file = new File(filePath);
              if (file.exists()) {
                  if (file.delete()) {
                      System.out.println(filePath + "目录删除成功");
                  } else {
                      System.out.println(filePath + "目录删除失败");
                  }
              } else {
                  System.out.println("该目录不存在");
              }
          }
      
          // 判断 d:\\Java\\Demo\\temp 目录是否存在, 如果存在就提示存在, 不存在就创建
          @Test
          public void m3() {
              String directoryPath = "d:\\Java\\Demo\\temp";
              File file = new File(directoryPath);
              if (file.exists()) {
                  System.out.println(directoryPath + "已经存在");
              } else {
                  if (file.mkdirs()) { // 创建多级目录就使用mkdirs方法, 一级目录(d:\\demo)使用mkdir方法
                      System.out.println(directoryPath + "创建成功");
                  } else {
                      System.out.println(directoryPath + "创建失败");
                  }
              }
          }
      }
      

      8.2、IO流原理及流的分类

      Java I/O流原理:

      1. I/O是Input/Output的缩写, I/O技术是非常实用的技术, 用于处理数据传输. 如:读/写文件, 网络通讯等.
      2. Java程序中, 对于数据的输入/输出操作以“流(stream)”的方式进行.
      3. java.io包下提供了各种“流”类和接口, 用以获取不同种类的数据, 并通过方法输入或输出数据.
      4. 输入input:读取外部数据(磁盘/光盘等存储设备的数据)到程序(内存)中.
      5. 输出output:将程序(内存)数据输出到磁盘/光盘等存储设备中.

      流的分类:

      1. 按操作数据单位:

        1. 字节流(8bit): 常用于操作二进制文件, 可以无损操作
        2. 字符流(按字符, 对应的字节由文件编码格式决定): 常用于操作文本文件
      2. 按数据流的流向:

        1. 输入流
        2. 输出流
      3. 按流的角色:

        1. 节点流
        2. 处理流(包装流)
        抽象基类 字节流 字符流
        输入流 InputStream Reader
        输出流 OutputStream Writer

        Java的IO流一共40多个类, 都是从如上4个抽象基类(IO流的顶级父类)派生的. 由这四个类派生出来的子类名称都是以其父类名作为子类名后缀.

      8.3、节点流和处理流

      1. 节点流可以从一个特定的数据源读写数据. 如:FileReader, FileWriter
      2. 处理流(也称包装流)是“连接”已经存在的流(节点流或处理流)之上, 为程序提供更为强大的读写功能. 如:BufferedReader, BufferedWriter

      节点流和处理流

      节点流和处理流的区别和联系:

      1. 节点流是底层流/低级流, 直接跟数据源相关.
      2. 处理流包装节点流(即处理流中包含节点流的对象), 既可以消除不同节点流的实现差异, 也可以提供更方便的方法来完成输入输出.
      3. 处理流(包装流)对节点流进行包装, 使用了修饰器设计模式, 不会直接与数据源相连.

      处理流的主要功能:

      1. 性能的提高:主要以增加缓冲的方式来提高输入输出的效率.
      2. 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入输出大批量的数据, 使用更加灵活方便.

      缓冲处理流:BufferedInputStream, BufferedOutputStream

      BufferedInputStream类

      BufferedInputStream类继承关系图

      字节输入流可以处理二进制文件, 也可以处理文本文件(字符) ==> 字节是基本单位

      BufferedOutputStream类

      BufferedOutputStream类继承关系图

      字节输入流可以处理二进制文件, 也可以处理文本文件(字符) ==> 字节是基本单位

      package com.yxz.file_.inputstream_;
      
      import org.junit.jupiter.api.Test;
      
      import java.io.FileInputStream;
      import java.io.IOException;
      
      public class FileInputStream_ {
          public static void main(String[] args) {
          }
      
          // 读取文件 d:\Java\Demo\news1.txt
          // read方法: 读取单个字节, 返回该字节的码值, 如果读取到文件末尾, 返回-1
          @Test
          public void readFile01() {
              String filePath = "d:\\Java\\Demo\\news1.txt";
              int readData = 0;
              // 1.创建FileInputStream对象, 用于读取文件
              FileInputStream fileInputStream = null;
              try {
                  fileInputStream = new FileInputStream(filePath);
                  // 2.read()每次从该输入流读取一个字节的数据. 如果没有输入可用, 此方法将阻塞
                  // 如果返回-1, 表示读取完毕, 注:如果读取汉字(UTF-8中一个汉字三个字节), 会出现乱码
                  while ((readData = fileInputStream.read()) != -1) {
                      System.out.print((char) readData);
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  // 3.关闭文件流, 释放资源
                  try {
                      fileInputStream.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
          // 读取文件 d:\Java\Demo\news1.txt
          // 使用read(byte[] b)读取文件, 提高效率
          @Test
          public void readFile02() {
              String filePath = "d:\\Java\\Demo\\news1.txt";
              byte[] buf = new byte[35];
              int readLen = 0;
              // 1.创建FileInputStream对象, 用于读取文件
              FileInputStream fileInputStream = null;
              try {
                  fileInputStream = new FileInputStream(filePath); // 会抛出FileNotFoundException异常
                  // 2.read(byte[] b)表示每一次从该输入流读取最多b.length个字节(35个字节)的数据到字节数组.
                  // 如果没有输入可用, 此方法将阻塞
                  // 如果返回-1, 表示读取完毕
                  // 如果读取正常, 返回实际读取的总字节数
                  while ((readLen = fileInputStream.read(buf)) != -1) { // read方法会抛出IOException异常
                      System.out.print(new String(buf, 0, readLen)); // 将读取到数组的内容构建成字符串输出
                  }
              } catch (IOException e) { // 捕获FileNotFoundException异常和IOException异常
                  e.printStackTrace();
              } finally {
                  // 3.关闭文件流, 释放资源
                  try {
                      fileInputStream.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      

      对象处理流:ObjectInputStream, ObjectOutputStream

      ObjectInputStream类

      为了能够将基本数据类型或者对象进行序列化和反序列化操作 ==> 对象处理流(ObjectInputStream/ObjectOutputStream)

      序列化和反序列化:

      1. 序列化就是在保存数据时, 保存数据的值和数据类型.
      2. 反序列化就是在恢复数据时, 恢复数据的值和数据类型.
      3. 需要让某个对象支持序列化机制, 则必须让其类型是可序列化的, 为了让某个类是可序列化的, 该类必须实现如下两个接口之一:
        1. Serializable // 标记接口(接口里面没有方法, 推荐使用)
        2. Externalizable // 该接口有方法需要实现

      ObjectInputStream类继承关系图

      package com.yxz.file_.object;
      
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.io.ObjectInputStream;
      
      public class ObjectInputStream_ {
          public static void main(String[] args) throws IOException, ClassNotFoundException {
              // 指定反序列化文件
              String filePath = "d:\\Java\\Demo\\object1.dat";
      
              ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
              // 读取反序列化的顺序要和序列化顺序保持一致, 否则会抛出异常
              System.out.println(ois.readInt());
              System.out.println(ois.readBoolean());
              System.out.println(ois.readByte());
              System.out.println(ois.readChar());
              System.out.println(ois.readDouble());
              System.out.println(ois.readUTF());
              // readObject方法会返回一个Object类型(Dog类向上转型)
              Object dog = ois.readObject();
              System.out.println("dog运行类型 = " + dog.getClass());
              System.out.println("dog的信息:" + dog);
          }
      }
      
      ObjectOutputStream类

      ObjectOutputStream类继承关系图

      package com.yxz.file_.object;
      
      import java.io.FileOutputStream;
      import java.io.IOException;
      import java.io.ObjectOutputStream;
      import java.io.Serializable;
      
      public class ObjectOutputStream_ {
          public static void main(String[] args) throws IOException {
              String filePath = "d:\\Java\\Demo\\object1.dat";
      
              ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
              // 写入时必须调用指定类型的write方法, 否则无法反序列化成功
              oos.writeInt(100); // int -> Integer(Integer继承的Number类实现了Serializable接口)
              oos.writeBoolean(true); // boolean -> Boolean(实现了Serializable接口)
              oos.writeByte(127); // byte -> Byte(Byte继承的Number类实现了Serializable接口)
              oos.writeChar('y'); // char -> Character(Character继承的Number类实现了Serializable接口)
              oos.writeDouble(3.14); // double -> Double(Double继承的Number类实现了Serializable接口)
              oos.writeUTF("yxz 学java"); // String实现了Serializable接口
              oos.writeObject(new Dog("旺财", 5)); // Dog类实现了Serializable接口
              oos.close();
              System.out.println("写入完毕...");
      
          }
      }
      
      // 如果需要序列化某个自定义类的对象, 该类必须实现序列化接口
      class Dog implements Serializable {
          private String name;
          private int age;
      
          public Dog(String name, int age) {
              this.name = name;
              this.age = age;
          }
      
          @Override
          public String toString() {
              return "Dog{" +
                      "name='" + name + '\'' +
                      ", age=" + age +
                      '}';
          }
      }
      

      注意:

      1. 序列化和反序列化, 读写顺序要一致
      2. 要求序列化或反序列化对象, 需要实现Serializable接口
      3. 序列化的类中建议添加SerialVersionUID, 为了提高版本的兼容性
      4. 序列化对象时, 默认将里面所有属性都进行序列化, 但除了statictransient修饰的成员
      5. 序列化对象时, 要求对象的属性的类型也需要实现序列化接口
      6. 序列化具备可继承性, 也就是如果某类已经实现了序列化, 则它的所有子类也已经默认实现了序列化.
      标准输入输出流

      标准输入流(InputStream):键盘

      标准输出流(OutputStream):显示器

      package com.yxz.file_.system_;
      
      import java.util.Scanner;
      
      public class InputAndOutput {
          public static void main(String[] args) {
              // System类的public final static InputStream in = null
              // System.in 的编译类型:InputStream
              // System.in 的运行类型:BufferedInputStream
              System.out.println(System.in.getClass());
              // System.in表示标准输入:键盘
      
              // System类的public final static PrintStream out = null;
              // System.out 的编译类型:PrintStream
              // System.out 的运行类型:PrintStream
              System.out.println(System.out.getClass());
              // System.out表示标准输出:显示器
              Scanner scanner = new Scanner(System.in);
              // 表示给Scanner从标准输入:键盘获取数据
              System.out.println("输入内容:");
              String next = scanner.nextLine(); // 读取数据
              System.out.println("next = " + next);
          }
      }
      

      转换流:

      InputStreamReader类

      InputStreamReader类继承关系图

      InputStream是字节输入流的顶级抽象父类, Reader是字符输入流的顶级抽象父类, InputStreamReader可以理解成将字节输入流转换成字符输入流

      说明:

      1. InputStreamReader:Reader的子类, 可以将InputStream(字节流)包装成Reader(字符流)
      2. 当处理纯文本数据时, 如果使用字符流效率更高, 并且可以有效解决中文问题, 建议将字节流转换成字符流.
      3. 可以在使用时指定编码格式, 如:utf-8, gbk, gb2312, ISO8859-1等.

      构造方法:

      public InputStreamReader(InputStream in, Charset cs):cs表示指定编码格式为cs, 读取的文件的编码格式为cs

      package com.yxz.file_.stream;
      
      import java.io.*;
      
      // 将字节流FileInputStream 转化成 字符流InputStreamReader, 指定编码utf-8/gbk
      public class InputStreamReader_ {
          public static void main(String[] args) throws IOException {
              String filePath = "d:\\Java\\Demo\\news3.txt";
              // 1.把字节流FileInputStream 转成 字符流InputStreamReader
              // 2.指定读取的文件的编码方式为gbk
              InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath), "gbk");
              // 3.把 InputStreamReader 传入 BufferedReader
              BufferedReader br = new BufferedReader(isr);
      
              // 将 第1步 第2步 第3步 合并
              BufferedReader br1 = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "gbk"));
      
              // 4.读取
              String s = br.readLine();
              System.out.println("s = " + s);
              br.close();
          }
      }
      
      OutputStreamWriter类

      OutputStreamWriter类继承关系图

      构造方法:

      public OutputStreamWriter(OutputStream out, Charset cs):cs表示指定编码格式, 写入数据后, 文件按照cs的编码方式保存

      package com.yxz.file_.stream;
      
      import java.io.*;
      
      // 把字节流FileOutputStream 转成 字符流OutputStreamWriter
      public class OutputStreamWriter_ {
          public static void main(String[] args) throws IOException {
              String filePath = "d:\\Java\\Demo\\OutputStreamWriter_gbk.txt";
              String charSet = "gbk";
              OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filePath), charSet);
              osw.write("yxz 学习java");
              osw.close();
              System.out.println("按照" + charSet + "的编码方式保存文件完毕");
          }
      }
      

      打印流:只有输出流, 没有输入流

      PrintStream类

      字节打印流(PrintStream类)

      PrintStream类继承关系图

      package com.yxz.file_.print;
      
      import java.io.IOException;
      import java.io.PrintStream;
      import java.nio.charset.StandardCharsets;
      
      // 字节打印流PrintStream
      public class PrintStream_ {
          public static void main(String[] args) throws IOException {
              PrintStream out = System.out;
              // 在默认情况下, PrintStream 输出数据的位置是 标准输出, 即显示器
              out.print("hello, java");
              /* 底层源码:
                  public void print(String s) {
                      if (s == null) {
                          s = "null";
                      }
                      write(s);
                  }
               */
              // 因为print底层是write, 所以可以直接调用write方法进行输出
              out.write("学无止境, 只要学不死, 就往死里学".getBytes(StandardCharsets.UTF_8));
              // 可以修改打印流的输出位置(设备)
              System.setOut(new PrintStream("d:\\Java\\Demo\\printStream1.txt"));
              /* 底层源码:
                  public static void setOut(PrintStream out) {
                      checkIO();
                      setOut0(out);
                  }
                  private static native void setOut0(PrintStream out);
               */
              // 输出修改到"d:\\Java\\Demo\\printStream1.txt"
              System.out.println("我爱java");
              out.close();
          }
      }
      
      PrintWriter类

      字符打印流(PrintWriter类)

      PrintWriter类继承关系图

      package com.yxz.file_.print;
      
      import java.io.FileWriter;
      import java.io.IOException;
      import java.io.PrintWriter;
      
      // 字符输出流PrintWriter
      public class PrintWriter_ {
          public static void main(String[] args) throws IOException {
              // PrintWriter printWriter = new PrintWriter(System.out);
              PrintWriter printWriter = new PrintWriter(new FileWriter("d:\\Java\\Demo\\printWriter.txt"));
              printWriter.print("为了年入百万, 继续冲!!!");
              printWriter.close(); // = flush + 关闭流, 这时才会写入(输出)文件
              // 必须要关闭流, 否则无法输出到指定位置
          }
      }
      

      8.4、输入流

      InputStream

      InputStream字节输入流的常用子类:ObjectInputStream, FileInputStream, FilterInputStream, BufferedInputStream

      InputStream类继承关系图

      FileInputStream类

      FileInputStream字节输入流常用方法:

      1. FileInputStream():构造方法, 传入文件路径.
      2. read():读取单个字节, 返回该字节的码值, 如果读取到文件末尾, 返回-1
      3. read(byte[] b):从输入流读取一些字节数,并将它们存储到缓冲区b。实际读取的字节数作为整数返回。该方法阻塞直到输入数据可用,检测到文件结束或抛出异常。如果b的长度为零,则不会读取字节并返回0; 否则,尝试读取至少一个字节。 如果没有字节可用,因为流在文件末尾,则返回值-1; 否则,读取至少一个字节并存储到b。第一个字节读取存储在元素b[0],下一个字节存入b[1]等等。读取的字节数最多等于b的长度。令k为实际读取的字节数; 这些字节将存储在元素b[0]至b[k-1],使元素b[k]至b[b.length-1]不受影响。

      注意:字符流/字节流默认是按照utf-8的编码格式读取文件

      Reader

      Reader字符输入流的常用子类FileReader, BufferedReader, InputStreamReader

      Reader继承关系图

      FileReader类

      FileReader常用方法:

      1. new FileReader(File/String)
      2. read():每次读取单个字符, 返回该字符, 如果到文件末尾返回-1
      3. read(char[]):批量读取多个字符到char[]数组, 返回读取到的字符数, 如果到文件末尾返回-1
      4. new String(char[]):将char[]转换成String
      5. new String(char[], int off, int len):将char[]的指定部分转换成String
      package com.yxz.file_.inputstream_;
      
      import org.junit.jupiter.api.Test;
      
      import java.io.FileReader;
      import java.io.IOException;
      
      public class FileReader_ {
          public static void main(String[] args) {
          }
      
          @Test
          public void readFile01() {
              String filePath = "d:\\Java\\Demo\\news1.txt";
              FileReader fileReader = null;
              int data = 0;
              try {
                  // 创建FileReader对象
                  fileReader = new FileReader(filePath);
                  // 循环读取单个字符
                  while ((data = fileReader.read()) != -1) {
                      System.out.print((char) data);
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  try {
                      if (fileReader != null) {
                          fileReader.close();
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      
          @Test
          public void readFile02() {
              String filePath = "d:\\Java\\Demo\\news1.txt";
              FileReader fileReader = null;
              char[] buf = new char[1024];
              int readLen = 0;
              try {
                  // 创建FileReader对象
                  fileReader = new FileReader(filePath);
                  // 循环读取, 使用read(buf), 返回的是实际读取到的字符数
                  // 如果返回-1, 说明读取到文件末尾
                  while ((readLen = fileReader.read(buf)) != -1) {
                      System.out.print(new String(buf, 0, readLen));
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  try {
                      if (fileReader != null) {
                          fileReader.close();
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      
      BufferedReader类
      package com.yxz.file_.buffer;
      
      import java.io.BufferedReader;
      import java.io.FileReader;
      import java.io.IOException;
      
      public class BufferedReader_ {
          public static void main(String[] args) throws IOException {
              String filePath = "d:\\Java\\Demo\\news2.txt";
              BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
              // 读取
              String line; // 按行读取, 效率高
              // bufferedReader.readLine()表示按行读取, 当返回null时, 表示读取完毕
              // readLine读取时不会读取换行符
              while ((line = bufferedReader.readLine()) != null) {
                  System.out.println(line);
              }
      
              // 关闭流 只需要关闭外层流BufferedReader, 底层会自动关闭节点流
              bufferedReader.close();
              /*
                  底层源码:
                  public void close() throws IOException {
                      synchronized (lock) {
                          if (in == null)
                              return;
                          try {
                              in.close(); // 这里的in对象指的是传入的节点流new FileReader(filePath)
                          } finally {
                              in = null;
                              cb = null;
                          }
                      }
                  }
               */
          }
      }
      

      8.5、输出流

      OutputStream

      OutputStream字节输出流的常用子类:FileOutputStream, ObjectOutputStream, BufferedOutputStream

      OutputStream继承关系图

      FileOutputStream类
      1. FileOutputStream(String name):创建文件输出流以指定的名称写入文件, 默认以覆盖的方式写入文件
      2. FileOutputStream(String name, boolean append):创建文件输出流以指定的名称写入文件. 如果append指定true, 则在文件末尾以追加方式写入
      3. write(int b):将指定的字节写入此文件输出流
      4. write(byte[] b, int off, int len):从位于偏移量off的指定字节数组写入len字节到该文件输出流
      5. write(byte[] b):将b.length字节从指定的字节数组写入此文件输出流
      package com.yxz.file_.inputstream_;
      
      import org.junit.jupiter.api.Test;
      
      import java.io.FileOutputStream;
      import java.io.IOException;
      import java.nio.charset.StandardCharsets;
      
      public class FileOutputStream_ {
          public static void main(String[] args) {}
      
          @Test
          public void writeFile() {
              // 创建FileOutputStream对象
              String filePath = "d:\\Java\\Demo\\news3.txt";
              FileOutputStream fileOutputStream = null;
      
              try {
                  // 得到FileOutputStream对象
                  // new FileOutputStream(filePath) 创建方式, 当写入内容时, 会覆盖掉文件原来内容
                  // new FileOutputStream(filePath, true) 创建方式, 当写入内容时, 会在文件末尾追加内容
                  fileOutputStream = new FileOutputStream(filePath); // 创建文件
                  // 1.写入一个字节
      //            fileOutputStream.write('H'); // 写入一个字节
                  String str = "鞠婧祎--我女神";
                  // 2.写入一个字符串
                  // str.getBytes()可以把字符串转换成byte数组
                  fileOutputStream.write(str.getBytes());
                  // write(byte[] b, int off, int len):将len个长度的字节从位于偏移量off的指定字节数组写入此文件输出流
                  fileOutputStream.write(str.getBytes(StandardCharsets.UTF_8), 0, str.length());
                  System.out.println("成功写入文件");
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  try {
                      // 关闭流, 释放资源
                      fileOutputStream.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      
      package com.yxz.file_.bufferedstream_;
      
      import java.io.*;
      
      public class BufferedCopy2 {
          public static void main(String[] args) {
              // 字节流可以操作二进制文件, 也可以操作文本文件
              String srcFilePath = "D:\\Java\\Demo\\鱼皮 - Java 学习路线一条龙版本 V2.xmind";
              String destFilePath = "D:\\Java\\Demo\\鱼皮 - Java 学习路线一条龙版本 V2_copy.xmind";
              BufferedInputStream bis = null;
              BufferedOutputStream bos = null;
      
              try {
                  // 创建对象
                  bis = new BufferedInputStream(new FileInputStream(srcFilePath));
                  bos = new BufferedOutputStream(new FileOutputStream(destFilePath));
      
                  // 循环读取文件并写入
                  byte[] buff = new byte[1024];
                  int readLen = 0;
                  while ((readLen = bis.read(buff)) != -1) {
                      bos.write(buff, 0, readLen);
                  }
                  System.out.println("文件拷贝完成~~~");
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  // 关闭外层处理流
                  if (bis != null) {
                      try {
                          bis.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
                  if (bos != null) {
                      try {
                          bos.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      }
      

      Writer

      Writer:字符输出流的常用子类:OutputStreamWriter, FileWriter. BufferedWriter

      Writer继承关系图

      FileWriter类

      FileWriter常用方法:

      1. new FileWriter(File/String):覆盖模式, 相当于流的指针在首端
      2. new FileWriter(File/String, true):追加模式, 相当于流的指针在尾端
      3. write(int):写入单个字符
      4. write(char[]):写入指定数组
      5. 1write(char[], off, len):写入指定数组的指定部分
      6. write(String):写入整个字符串
      7. write(String, off, len):写入字符串的指定部分
      8. 相关String方法:toCharArray(), 将String转换成char[]

      注意:FileWriter使用后, 必须要关闭(close)或刷新(flush), 否则无法写入到指定文件

      package com.yxz.file_.inputstream_;
      
      import org.junit.jupiter.api.Test;
      
      import java.io.FileWriter;
      import java.io.IOException;
      
      public class FileWriter_ {
          public static void main(String[] args) {}
      
          @Test
          public void writer01() {
              String filePath = "d:\\Java\\Demo\\news2.txt";
              FileWriter fileWriter = null;
              try {
                  fileWriter = new FileWriter(filePath, true); // 追加模式
                  //1. write(int):写入单个字符
                  fileWriter.write('W');
                  //2. write(char[]):写入指定数组
                  char[] c = {'Y', 'x', 'z'};
                  fileWriter.write(c);
                  //3. write(char[], off, len):写入指定数组的指定部分
                  fileWriter.write("Yxz学习Java".toCharArray(), 0, 9);
                  //4. write(String):写入整个字符串
                  fileWriter.write("\n学好Java月入十万不是梦");
                  //5. write(String, off, len):写入字符串的指定部分
                  fileWriter.write("\nCTO月入百万不是梦", 0, 11);
                  System.out.println("成功写入");
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  if (fileWriter != null) {
                      try {
                          // fileWriter.flush();
                          fileWriter.close(); // 对于FileWriter, 一定要关闭(刷新,flush)流, 否则无法将数据真正的写入到文件中
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      }
      
      BufferedWriter类
      package com.yxz.file_.buffer;
      
      import java.io.BufferedWriter;
      import java.io.FileWriter;
      import java.io.IOException;
      
      public class BufferedWriter_ {
          public static void main(String[] args) throws IOException {
              String filePath = "d:\\Java\\Demo\\news4.txt";
              // 创建BufferedWriter
              BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filePath, true));
              bufferedWriter.write("hello1, java");
              // 需要插入换行
              bufferedWriter.newLine(); // 根据系统插入换行符
              bufferedWriter.write("hello2, java");
              bufferedWriter.newLine(); // 根据系统插入换行符
              bufferedWriter.write("hello3, java");
              bufferedWriter.newLine(); // 根据系统插入换行符
              // 关闭外层流
              bufferedWriter.close();
          }
      }
      

      案例:复制文本文件

      package com.yxz.file_.buffer;
      
      import java.io.*;
      
      public class BufferedCopy {
          public static void main(String[] args) {
              // BufferedReader 和 BufferedWriter 是对字符(文本文件txt)操作
              // 不能使用BufferedReader 和 BufferedWriter去操作二进制文件(视频,音频,doc,pdf,xls等等), 可能会造成文件损坏
              String srcFilePath = "d:\\Java\\Demo\\news2.txt";
              String destFilePath = "d:\\Java\\Demo\\news2_copy.txt";
              BufferedReader br = null;
              BufferedWriter bw = null;
              String line;
              try {
                  br = new BufferedReader(new FileReader(srcFilePath));
                  bw = new BufferedWriter(new FileWriter(destFilePath));
                  // readLine读取一行内容, 但是不会换行
                  while ((line = br.readLine()) != null) {
                      // 读取每一行内容, 写入
                      bw.write(line);
                      // 每一行写入后, 插入一个换行
                      bw.newLine();
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  if (br != null) {
                      try {
                          br.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
      
                  if (bw != null) {
                      try {
                          bw.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      }
      

      8.6、Properties类

      public class Properties extends Hashtable<Object,Object> {}
      

      说明:

      1. Properties文件是专门用于读写配置文件的集合类(数据库常用)

        配置文件格式:键=值

        注意:=号两边没有空格, 键和值也不需要双引号(“”), 默认类型为String类型

        package com.yxz.file_.properties_;
        
        import java.io.FileReader;
        import java.io.IOException;
        import java.util.Properties;
        
        // 使用Properties类读取mysql.properties文件
        public class Properties02 {
            public static void main(String[] args) throws IOException {
                // 1.创建Properties对象
                Properties properties = new Properties();
                // 2.加载指定配置文件
                properties.load(new FileReader("D:\\Program Files (x86)\\IDEA\\Projects\\Module02_Study\\src\\com\\yxz\\file_\\properties_\\mysql.properties"));
                // 3.把k-v输出到控制台
                properties.list(System.out);
                // 4.根据k, 获取对应v值
                String user = properties.getProperty("user");
                String pwd = properties.getProperty("pwd");
                System.out.println("用户名:" + user + ", 密码:" + pwd);
            }
        }
        

        mysql.properties文件

        ip=192.168.100.100
        user=root
        pwd=12345
        
      2. Properties类常用方法:

        1. load():加载配置文件的键值对到Properties对象

        2. list():将数据显示到指定设备/流对象

        3. getProperty(key):根据键获取值

        4. setProperty(key, value):设置键值对到Properties对象

        5. store(OutputStream out, String comments):将Properties中的键值对储存到配置文件out中. 在IDEA中, 保存信息到配置文件, 如果含有中文, 为将中文存储为对应的Unicode码. comments表示注释, 如果comments不为null, 则生成文件时会在文件第一行加上该comments内容.

          注:如果store方法的输出流对象传入的是字符流对象, 则中文不变, 如果是字节流对象, 则中文保存为对应的Unicode码

      unicode码查询工具

      package com.yxz.file_.properties_;
      
      import java.io.FileOutputStream;
      import java.io.IOException;
      import java.util.Properties;
      
      // 使用Properties类创建properties文件, 或者修改配置文件内容
      public class Properties03 {
          public static void main(String[] args) throws IOException {
              // 1.创建对象
              Properties properties = new Properties();
              // 2.设置properties的k-v, 如果该文件没有key, 就是创建; 如果有, 则修改key的对应value值
              /*
                  Properties父类是Hashtable, 底层就是Hashtable, 核心方法:
                  public synchronized V put(K key, V value) {
                      // Make sure the value is not null
                      if (value == null) {
                          throw new NullPointerException();
                      }
      
                      // Makes sure the key is not already in the hashtable.
                      Entry<?,?> tab[] = table;
                      int hash = key.hashCode();
                      int index = (hash & 0x7FFFFFFF) % tab.length;
                      @SuppressWarnings("unchecked")
                      Entry<K,V> entry = (Entry<K,V>)tab[index];
                      for(; entry != null ; entry = entry.next) {
                          if ((entry.hash == hash) && entry.key.equals(key)) {
                              V old = entry.value;
                              entry.value = value;
                              return old;
                          }
                      }
      
                      addEntry(hash, key, value, index);
                      return null;
                  }
               */
              properties.setProperty("charSet", "utf-8");
              properties.setProperty("user", "鞠婧祎");
              properties.setProperty("pwd", "12345");
              // 3.将k-v存储文件中即可
              properties.store(new FileOutputStream("D:\\Program Files (x86)\\IDEA\\Projects\\Module02_Study\\src\\com\\yxz\\file_\\properties_\\mysql2.properties"), null);
              // 注:如果传入字节流对象, 则中文保存为对应unicode码, 如果传入字符流对象, 中文不变
              // null表示properties文件的第一行的注释内容(String类型), null表示没有注释
              System.out.println("创建成功");
          }
      }
      

      mysql2.properties文件

      #Thu Nov 17 01:41:40 CST 2022
      user=\u97A0\u5A67\u794E
      charSet=utf-8
      pwd=12345
      

      第三阶段

      第三阶段 目标:分析需求,代码实现能力

      1、网络编程

      1.1、网络基础

      网络通信

      1. 概念:两台设备之间通过网络实现数据传输
      2. 网络通信:将数据通过网络从一台设备传输到另一台设备
      3. java.net包下提供了一系列的类和接口, 供程序员使用, 完成网络通信

      网络

      1. 概念:两台或多台设备通过一定物理设备连接起来构成了网络
      2. 根据网络的覆盖范围不同, 对网络进行分类:
        1. 局域网:覆盖范围最小, 仅仅一个教室或机房
        2. 域域网:覆盖范围较大, 可以覆盖一个城市
        3. 广域网:覆盖范围最大, 可以覆盖全国甚至全球, 广域网的代表--万维网

      IP地址

      1. 概念:用于唯一标识网络中的每台计算机/主机
      2. 查看IP地址的命令:ipconfig
      3. IPv4地址的表示形式(4个字节 -> 32位):点分十进制. 如:xx.xx.xx.xx
      4. 每一个十进制数的范围:0~255(1个字节能表示的范围)
      5. IP地址的组成 = 网络地址 + 主机地址. 如:192.168.16.69
      6. IPv6(16个字节 -> 128位, 冒分十六进制表示)是互联网工程任务组设计的用于替代IPv4的下一代IP协议, 其地址数量可以为全世界的每一粒沙子边上一个地址
      7. 由于IPv4最大的问题在于网络地址资源有限, 严重制约了互联网的应用和发展. IPv6的使用, 不仅能解决网络地址资源数量的问题, 而且也解决了多种接入设备连入互联网的障碍.

      IPv4地址的分类

      域名

      1. 如:www.baidu.com
      2. 域名为了方便记忆, 解决ip地址的记忆困难
      3. 将IP地址映射成域名

      端口号

      1. 概念:用于标识计算机上某个特定的网络程序
      2. 表示形式:以整数形式, 端口范围0~65535($2^{16}-1$, 两个字节)
      3. 0~1024已经被占用, 比如:ssh 22, ftp 21
      4. 常见的网络程序端口号:
        • tomcat:8080
        • mysql:3306
        • oracle:1521
        • sqlserver:1433

      网络通信协议

      TCP/IP协议

      TCP/IP(全称:Transmission Control Protocol/Internet Protocol)协议, 中文名:传输控制协议/因特网互联协议, 又叫网络通讯协议, TCP/IP协议是Internet最基本的协议, Internet国际互连网络的基础, 简单来说, 就是由网络层的IP协议和传输层的TCP协议组成的.

      数据进入协议的封装过程

      网络7层模型(⭐⭐⭐⭐⭐⭐)

      网络通信协议7层模型

      TCP和UDP

      TCP协议:传输控制协议

      1. 使用TCP协议前, 须建立TCP连接, 形成传输数据通道
      2. 传输前, 采用“三次握手”方式, 是可靠的.
      3. TCP协议进行通信的两个应用进程:客户端(Client), 服务端(Server)
      4. 在连接中可进行大数据量的传输
      5. 传输完毕, 需释放已建立的连接, 效率低

      UDP协议:用户数据协议

      1. 数据, 源, 目的封装成数据包, 不需要建立连接
      2. 每个数据包的大小限制在64K内, 不适合传输大量数据
      3. 因为无需连接, 所以不可靠
      4. 发送数据结束时无需释放资源(因为不是面向连接的), 速度快

      1.2、InetAddress类

      常用方法:

      1. getLocalHost():获取本机InetAddress对象
      2. getByName():根据指定主机名/域名获取IP地址对象
      3. getHostName():获取InetAddress对象的主机名
      4. getHostAddress():获取InetAddress对象的地址
      package com.yxz.internet;
      
      import java.net.InetAddress;
      import java.net.UnknownHostException;
      
      public class API_ {
          public static void main(String[] args) throws UnknownHostException {
              // 1.获取本机的InetAddress对象
              InetAddress localHost = InetAddress.getLocalHost();
              System.out.println(localHost);
      
              // 2.根据指定的主机名获取对象
              InetAddress host1 = InetAddress.getByName("Coder-1024");
              System.out.println(host1);
      
              // 3.根据域名返回InetAddress对象
              InetAddress host2 = InetAddress.getByName("www.baidu.com");
              System.out.println(host2);
      
              // 4.通过InetAddress对象, 获取对应地址
              String hostAddress = host2.getHostAddress();
              System.out.println("host2对应的IP地址:" + hostAddress);
      
              // 5.通过InetAddress对象, 获取对应的主机名/域名
              String hostName = host2.getHostName();
              System.out.println("host2Name = " + hostName);
          }
      }
      

      1.3、Socket

      介绍:

      1. 套接字(Socket)开发网络应用程序被广泛采用, 以至于成为事实上的标准
      2. 通信的两端都要有Socket, 是两台机器间通信的端点
      3. 网络通信其实就是Socket间的通信
      4. Socket允许程序把网络连接当成一个流, 数据在两个Socket间通过IO传输
      5. 一般主动发起通信的应用程序属客户端, 等待通信请求的为服务端(一般情况下, 客户端和服务端不在同一个主机)
      6. 基于Socket的两种编程方式:TCP编程(可靠), UDP编程(不可靠)

      常用方法:

      1. getOutputStream():输出流, 将数据写入到数据通道
      2. getInputStream():输入流, 读取客户端写入到数据通道的数据
      3. shutdownOutput():结束标记. 以示消息(数据)发送完毕
      4. close():关闭socket, 释放资源

      1.4、TCP编程

      基本介绍:

      1. 基于客户端-服务端的网络通信
      2. 底层使用的是TCP/IP协议
      3. 应用场景:客户端发送数据, 服务券接收并显示在控制台
      4. 基于Socket的TCP编程

      netstat指令

      1. netstat -an可以查看当前主机网络情况, 包括端口监听情况和网络连接情况
      2. netstat -an | more可以分页显示(输入空格进入下一页)
      3. netstat -anb表示查看网络情况时, 显示哪一个程序在监听对应的端口
      4. 要求在DOS控制台下执行该指令

      netstat指令

      当客户端连接到服务器端后, 实际上客户端也是通过一个端口和服务器端进行通讯的, 这个客户端的端口号是由TCP/IP协议来分配的, 是不确定的, 随机的.

      1.4.1、字节流

      应用案例1:

      1. 编写一个服务器端, 一个客户端
      2. 服务器端在9999端口监听, 当没有客户端连接9999端口时, 程序会阻塞, 等待连接
      3. 客户端连接到服务器端(ip, 端口), 发送“hello server”, 然后退出
      4. 服务器端接收到客户端的信息并发送“hello client”给客户端, 然后退出

      TCP编程原理图(⭐⭐⭐⭐)

      TCP编程(字节流读写)

      SocketTCP01Server.java服务器端

      package com.yxz.internet.socket;
      
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.OutputStream;
      import java.net.ServerSocket;
      import java.net.Socket;
      
      // 服务器端
      public class SocketTCP01Server {
          public static void main(String[] args) throws IOException {
              // 1.在本机的9999端口监听, 等待连接(要求在本机没有其他服务在9999端口监听)
              // ServerSocket可以通过accept()返回多个Socket(多个客户端连接服务器的并发)
              ServerSocket serverSocket = new ServerSocket(9999);
              System.out.println("服务器端 在9999端口监听...");
              // 2.当没有客户端连接9999端口时, 程序会阻塞, 等待连接
              // 如果有客户端连接, 则返回一个Socket对象
              Socket socket = serverSocket.accept(); // 如果没有客户端, 程序在此阻塞
              System.out.println("服务器端 socket = " + socket.getClass());
              // 3.通过socket.getInputStream()读取客户端写入到数据通道的数据
              InputStream inputStream = socket.getInputStream(); // 如果没有信息读取, 程序在此阻塞, 等待
              // 4.读取数据
              byte[] buf = new byte[1024];
              int readLen = 0;
              while ((readLen = inputStream.read(buf)) != -1) {
                  System.out.println(new String(buf, 0, readLen));// 根据读取到的实际长度显示内容
              }
              // 5.获取socket相关联的输出流
              OutputStream outputStream = socket.getOutputStream();
              outputStream.write("hello client".getBytes());
              // 设置结束标记
              socket.shutdownOutput();
              // 6.关闭流和socket
              inputStream.close();
              outputStream.close();
              socket.close();
              serverSocket.close();
              System.out.println("服务器端 退出...");
          }
      }
      

      SocketTCP01Client.java客户端

      package com.yxz.internet.socket;
      
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.OutputStream;
      import java.net.InetAddress;
      import java.net.Socket;
      
      // 客户端
      public class SocketTCP01Client {
          public static void main(String[] args) throws IOException {
              // 1.连接服务器
              // 连接本机的9999端口, 一般情况下, 第一个参数为域名
              Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
              System.out.println("客户端 socket返回 = " + socket.getClass());
              // 2.连接上以后, 生成Socket, 通过socket.getOutputStream()
              // 得到和socket对象关联的输出流对象
              OutputStream outputStream = socket.getOutputStream();
              // 3.通过输出流, 将数据写入到数据通道
              outputStream.write("hello server".getBytes());
              // 设置结束标记
              socket.shutdownOutput();
              // 4.获取和socket关联的输入流, 读取数据(字节)并显示
              InputStream inputStream = socket.getInputStream();
              byte[] buf = new byte[1024];
              int readLen = 0;
              while ((readLen = inputStream.read(buf)) != -1) {
                  System.out.println(new String(buf, 0, readLen));
              }
              // 5.关闭流对象和socket, 必须关闭
              inputStream.close();
              outputStream.close();
              socket.close();
              System.out.println("客户端 退出...");
          }
      }
      

      1.4.2、字符流

      应用案例2:

      1. 编写一个服务器端, 一个客户端
      2. 服务器端在9999端口监听, 当没有客户端连接9999端口时, 程序会阻塞, 等待连接
      3. 客户端连接到服务器端(ip, 端口), 发送“hello server 字符流”, 然后退出
      4. 服务器端接收到客户端的信息并发送“hello client 字符流”给客户端, 然后退出

      注意:

      1. 使用字符输出流的newLine()方法设置结束标记, 表示回复内容结束, 相当于字节流案例中的shutdownOutput()方法, 且对方必须使用readLine()方法读取换行符
      2. 字符流写入数据后必须调用flush()方法手动刷新, 否则无法将数据写入数据通道
      3. 关闭流的使用只需关闭外层处理流.
      4. 关闭流的顺序:后打开的流先关闭

      SocketTCP02Server.java服务器端

      package com.yxz.internet.socket;
      
      import java.io.*;
      import java.net.ServerSocket;
      import java.net.Socket;
      
      // 服务器端
      public class SocketTCP02Server {
          public static void main(String[] args) throws IOException {
              // 1.在本机的9999端口监听, 等待连接(要求在本机没有其他服务在9999端口监听)
              // ServerSocket可以通过accept()返回多个Socket(多个客户端连接服务器的并发)
              ServerSocket serverSocket = new ServerSocket(9999);
              System.out.println("服务器端 在9999端口监听...");
      
              // 2.当没有客户端连接9999端口时, 程序会阻塞, 等待连接
              // 如果有客户端连接, 则返回一个Socket对象
              Socket socket = serverSocket.accept(); // 如果没有客户端, 程序在此阻塞
              System.out.println("服务器端 socket = " + socket.getClass());
      
              // 3.通过socket.getInputStream()读取客户端写入到数据通道的数据
              InputStream inputStream = socket.getInputStream(); // 如果没有信息读取, 程序在此阻塞, 等待
      
              // 4.IO读取数据, 使用字符流. 转换流InputStreamReader 将 InputStream -> Reader
              BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
              String s = bufferedReader.readLine();
              System.out.println(s); // 输出
      
              // 5.获取socket相关联的输出流
              OutputStream outputStream = socket.getOutputStream();
              // 使用字符输出流的方式回复信息
              BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
              bufferedWriter.write("hello client 字符流");
              bufferedWriter.newLine(); // 插入换行符, 表示回复内容结束
              bufferedWriter.flush(); // 手动刷新
      
              // 6.关闭外层流和socket
              bufferedReader.close();
              bufferedWriter.close();
              socket.close();
              serverSocket.close();
              System.out.println("服务器端 退出...");
          }
      }
      

      SocketTCP02Client.java客户端

      package com.yxz.internet.socket;
      
      import java.io.*;
      import java.net.InetAddress;
      import java.net.Socket;
      
      // 客户端
      public class SocketTCP02Client {
          public static void main(String[] args) throws IOException {
              // 1.连接服务器
              // 连接本机的9999端口, 一般情况下, 第一个参数为域名
              Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
              System.out.println("客户端 socket返回 = " + socket.getClass());
      
              // 2.连接上以后, 生成Socket, 通过socket.getOutputStream()
              // 得到和socket对象关联的输出流对象
              OutputStream outputStream = socket.getOutputStream();
      
              // 3.通过输出流, 将数据写入到数据通道, 使用字符流
              BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
              bufferedWriter.write("hello server 字符流");
              bufferedWriter.newLine(); // 插入一个换行符, 表示写入的内容结束, 注意:对方必须使用readLine()读取换行符!!!
              bufferedWriter.flush(); // 如果使用字符流, 需要手动刷新, 否则数据不会写入数据通道
      
              // 4.获取和socket关联的输入流, 读取数据(字符)并显示
              InputStream inputStream = socket.getInputStream();
              BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
              String s = bufferedReader.readLine();
              System.out.println(s);
      
              // 5.关闭外层流对象和socket, 必须关闭
              bufferedReader.close();
              bufferedWriter.close();
              socket.close();
              System.out.println("客户端 退出...");
          }
      }
      

      综合案例:网络上传文件

      1. 编写一个服务器端和一个客户端
      2. 服务器端在8888端口监听
      3. 客户端连接到服务器端, 发送一张图片D:\Program Files (x86)\Visual Studio\Images\鞠婧祎08.jpg. 二进制文件使用字节流BufferedInputStream和BuffedOutputStream
      4. 服务器端接收到客户端发送的图片, 保存到src目录下, 发送“收到图片”再退出
      5. 客户端接收到服务器端发送的“收到图片”再退出
      6. 程序使用工具类StreamUtils.java
      import java.io.BufferedReader;
      import java.io.ByteArrayOutputStream;
      import java.io.InputStream;
      import java.io.InputStreamReader;
       
      public class StreamUtils {
          public static byte[] streamToByteArray(InputStream is) throws Exception {
              // 创建输出流对象
              ByteArrayOutputStream bos = new ByteArrayOutputStream();
              // 字节数组
              byte[] b = new byte[1024];
              int len;
              while ((len = is.read(b)) != -1) {
                  // 循环读取
                  // 把读取到的数据,写入 bos
                  bos.write(b, 0, len);
              }
              byte[] array = bos.toByteArray();
              bos.close();
              return array;
          }
       
          public static String streamToString(InputStream is) throws Exception {
              BufferedReader reader = new BufferedReader(new InputStreamReader(is));
              StringBuilder builder = new StringBuilder();
              String line;
              while ((line = reader.readLine()) != null) {
                  builder.append(line + "\r\n");
              }
              return builder.toString();
          }
      }
      

      分析步骤:

      网络上传文件示意图

      TCPFileUploadServer.java服务器端

      package com.yxz.internet.upload;
      
      import java.io.*;
      import java.net.ServerSocket;
      import java.net.Socket;
      
      public class TCPFileUploadServer {
          public static void main(String[] args) throws Exception {
              // 1.服务器端在8888端口监听
              ServerSocket serverSocket = new ServerSocket(8888);
              System.out.println("服务器端 8888端口监听...");
              // 2.等待连接
              Socket socket = serverSocket.accept();
      
              // 3.读取客户端发送的数据
              BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
              byte[] bytes = StreamUtils.streamToByteArray(bis);
              // 4.将得到的bytes数组, 写入到指定路径
              String destFilePath = "D:\\Program Files (x86)\\IDEA\\Projects\\Module02_Study\\鞠婧祎.jpg";
              BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFilePath));
              bos.write(bytes);
              bos.close();
      
              // 5.向客户端回复"收到图片"
              // 通过socket获取到输出流
              BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
              writer.write("收到图片");
              writer.flush(); // 将内容刷新到数据通道
              socket.shutdownOutput(); // 设置写入结束标记
              
              // 6.关闭其他资源
              writer.close();
              bis.close();
              socket.close();
              serverSocket.close();
          }
      }
      

      TCPFileUploadClient.java客户端

      package com.yxz.internet.upload;
      
      import java.io.BufferedInputStream;
      import java.io.BufferedOutputStream;
      import java.io.FileInputStream;
      import java.io.InputStream;
      import java.net.InetAddress;
      import java.net.Socket;
      
      public class TCPFileUploadClient {
          public static void main(String[] args) throws Exception {
              Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
              // 创建读取磁盘文件的输入流
              String filePath = "D:\\Program Files (x86)\\Visual Studio\\Images\\鞠婧祎08.jpg";
              BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath));
              // bytes对应文件的字节数组
              byte[] bytes = StreamUtils.streamToByteArray(bis);
      
              // 通过socket获取输出流, 将bytes数据发送给服务端
              BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
              bos.write(bytes); // 将文件对应的字节数组的内容写入到数据通道
              bis.close();
              socket.shutdownOutput(); // 设置写入的结束标记
      
              // 接收从服务器端回复的消息
              InputStream inputStream = socket.getInputStream();
              // 使用StreamUtils的方法, 直接将inputStream读取到的内容转成字符串
              String s = StreamUtils.streamToString(inputStream);
              System.out.println(s);
      
              // 关闭流
              bos.close();
              socket.close();
          }
      }
      

      1.5、UDP编程

      基本介绍:

      1. 类DatagramSocketDatagramPacket(数据报/数据包) 实现了基于UDP协议网络程序
      2. UDP数据报通过数据报套接字DatagramSocket发送和接收, 系统不保证UDP数据包一定能够安全送到目的地, 也不能确定什么时候可以抵达
      3. DatagramPacket对象封装了UDP数据报, 在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号.
      4. UDP协议中每个数据报都给出了完整的地址信息, 因此无需建立发送方和接收方的连接.

      基本流程:

      1. 核心的两个类DatagramSocket和DatagramPacket
      2. 建立发送端和接收端
      3. 建立数据包
      4. 调用DatagramSocket的发送send(), 接收receive()方法.
      5. 关闭DatagramSocket

      注意:

      1. 没有客户端和服务器端, 演变成发送端和接收端
      2. 接收数据和发送数据是通过DatagramSocket对象完成
      3. 将数据封装到DatagramPacket对象中(装包)
      4. 当接收到DatagramPacket对象, 需要进行拆包, 取出数据
      5. DatagramSocket可以指定在哪个端口接收数据

      UDP编程原理图(⭐⭐⭐⭐)

      UDP网络编程原理

      UDPReceiverA.java接收端A

      package com.yxz.internet.udp;
      
      import java.io.IOException;
      import java.net.DatagramPacket;
      import java.net.DatagramSocket;
      import java.net.InetAddress;
      
      public class UDPReceiverA {
          public static void main(String[] args) throws IOException {
              // 1.创建一个DatagramSocket 对象, 准备在9999端口接收数据
              DatagramSocket datagramSocket = new DatagramSocket(9999);
              // 2.创建一个DatagramPacket对象, 准备接收数据, UDP协议中每个数据包最大64k
              byte[] buf = new byte[64 * 1024];
              DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);
              // 3.调用方法receive, 将通过网络传输的DatagramPacket对象填充到packet对象
              System.out.println("接收端A等待接收数据");
              datagramSocket.receive(datagramPacket); // 在9999端口等待, 如果有数据包发送到该端口就接收, 如果没有数据包, 就会阻塞等待
              System.out.println("数据已接收...");
              // 4.将packet进行拆包, 取出数据
              int length = datagramPacket.getLength(); // 实际接收到的数据长度
              byte[] data = datagramPacket.getData(); // 接收到的数据
              String s = new String(data, 0, length);
              System.out.println(s);
              // 5.回复消息"好的, 明天见"
              byte[] message = "好的, 明天见".getBytes();
              DatagramPacket packet = new DatagramPacket(message, message.length, InetAddress.getByName("10.130.135.48"), 9898);
              datagramSocket.send(packet);
              // 6.关闭资源
              datagramSocket.close();
              System.out.println("接收端A 退出");
      
          }
      }
      

      UDPSenderB.java发送端B

      package com.yxz.internet.udp;
      
      import java.io.IOException;
      import java.net.*;
      
      public class UDPSenderB {
          public static void main(String[] args) throws IOException {
              // 1. 创建DatagramSocket对象, 准备在9898端口接收数据
              DatagramSocket datagramSocket = new DatagramSocket(9898); // 因为接收端和发送端都在同一台电脑, 所以需要指定不同的端口
              // 2.将需要发送的数据, 封装到DatagramPacket对象
              byte[] data = "hello, 明天吃火锅~".getBytes();
              // 给远程主机发送packet, 封装对象(data内容的字节数组, 数组长度, 主机IP地址, 接收端的端口)
              DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("10.130.135.48"), 9999);
              datagramSocket.send(packet);
              // 3.接收回复的消息
              byte[] message = new byte[64 * 1024];
              DatagramPacket packet1 = new DatagramPacket(message, message.length);
              System.out.println("发送端B 等待回复消息...");
              datagramSocket.receive(packet1);
              System.out.println("数据已接收");
              // 4.拆包, 取出数据
              int length = packet1.getLength();
              byte[] data1 = packet1.getData();
              String s = new String(data1, 0, length);
              System.out.println(s);
              // 5.关闭资源
              datagramSocket.close();
              System.out.println("发送端B 退出");
          }
      }
      

      2、反射

      反射(Reflection)的快速入门案例:

      1. 根据配置文件re.properties指定信息, 创建Cat对象并调用方法hi

        classfullpath=com.lww.reflection.Cat
        method=hi
        
      2. 通过外部文件配置, 在不修改源码的情况下, 来控制程序, 也符合设计模式的OCP原则(Open/Close Principle, 开放封闭原则: 开放配置文件, 封闭源码, 扩展功能)

      3. 测试代码:

        Cat.java源码文件

        package com.lww.reflection;
        
        public class Cat {
            private String name = "招财猫";
            private int age = 3;
        
            public Cat() {
            }
            public Cat(String name, int age) {
                this.name = name;
                this.age = age;
            }
            public void hi() {
                System.out.println("hi " + name + " 的年龄:" + age);
            }
            public void cry() {
                System.out.println("小猫" + name + "喵喵叫...");
            }
            public void jump() {
                System.out.println(name + "会跳跃");
            }
        }
        

        Reflection01.java源码文件

        package com.lww.reflection;
        
        import java.io.FileInputStream;
        import java.io.IOException;
        import java.lang.reflect.InvocationTargetException;
        import java.lang.reflect.Method;
        import java.util.Properties;
        
        @SuppressWarnings({"all"})
        public class Reflection01 {
            public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
                // 传统方式: new一个对象, 调用方法:对象.方法名()
                Cat cat = new Cat("豆豆", 3);
                cat.hi();
        
                // 1.使用Properties类, 读取配置文件
                Properties properties = new Properties();
                properties.load(new FileInputStream("Module02_Study\\src\\com\\lww\\reflection\\re.properties"));
                String classfullpath = properties.getProperty("classfullpath").toString();
                String methodName = properties.getProperty("method").toString();
                System.out.println("classfullpath = " + classfullpath);
                System.out.println("methodName = " + methodName);
                // 2.创建对象, 传统方法行不通 ==> 反射机制
                System.out.println("============================分隔符=======================");
                // 3.使用反射机制解决
                // (1)加载类, 返回一个Class对象, 对象的编译类型为Class
                Class cls = Class.forName(classfullpath);
                // (2)通过cls 得到加载的类 com.lww.reflection.Cat 的对象实例
                Object o = cls.newInstance(); // 调用类的无参构造器
                System.out.println("o的运行类型 = " + o.getClass()); // com.lww.reflection.Cat
                // (3)通过 cls 得到加载的类 com.lww.reflection.Cat 的 methodName="hi"的方法对象
                // 即: 在反射机制中, 方法也是对象 -> 万物皆对象
                Method method = cls.getMethod(methodName);
                // (4)通过method 调用方法, 通过方法对象method来实现方法
                method.invoke(o); // 传统方法: 对象.方法名(), 反射机制: 方法对象.invoke(类对象)
            }
        }
        

        re.properties开放的配置文件

        classfullpath=com.lww.reflection.Cat
        method=hi
        

        可以在不修改源码文件中的内容, 通过修改配置文件中的method的值, 从而达到控制程序达到调用不同方法的效果

      2.1、反射机制

      基本介绍:

      1. 反射机制允许程序在执行期借助于ReflectionAPI取得任何类的内部信息(如:成员变量, 构造器, 成员方法等), 并能操作对象的属性及方法. 反射机制在设计模式和框架底层都会大量涉及到.
      2. 加载完类之后, 在堆中就产生了一个Class类型的对象(一个类只有一个Class类型的对象), 这个对象包含了类的完整结构信息. 通过这个Class对象得到类的结构. Class对象就像一面镜子, 通过镜子看到类的结构 -> 反射

      反射原理图(⭐⭐⭐⭐⭐)

      Java反射原理图

      反射机制可以完成:

      1. 在运行时判断任意一个对象所属的类
      2. 在运行时构造任意一个类的对象
      3. 在运行时得到任意一个类所具有的成员变量和方法
      4. 在运行时调用任意一个对象的成员变量和方法
      5. 生成动态代理

      反射相关的类:

      1. java.lang.Class:代表一个类, Class对象表示某个类加载后在堆中的对象
      2. java.lang,reflect.Method:代表类的方法, Method对象表示某个类的方法
      3. java.lang.reflect.Field:代表类的成员变量, Field对象表示某个类的成员变量
      4. java.lang.reflect.Constructor:代表类的构造方法, Constructor对象表示某个类的构造器
      package com.lww.reflection;
      
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.lang.reflect.Constructor;
      import java.lang.reflect.Field;
      import java.lang.reflect.InvocationTargetException;
      import java.lang.reflect.Method;
      import java.util.Properties;
      
      @SuppressWarnings({"all"})
      public class Reflection02 {
          public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
              Properties properties = new Properties();
              properties.load(new FileInputStream("Module02_Study\\src\\com\\lww\\reflection\\re.properties"));
              String classfullpath = properties.getProperty("classfullpath").toString();
              String methodName = properties.getProperty("method").toString();
              Class cls = Class.forName(classfullpath);
              Object o = cls.newInstance();
              System.out.println("o的运行类型 = " + o.getClass());
              Method method = cls.getMethod(methodName);
              method.invoke(o);
      
              // java.lang.reflect.Field:代表类的成员变量, Field对象表示某个类的成员变量
              // 得到name字段
              // getField不能得到私有的属性
      //        Field name = cls.getField("name"); // name为私有属性, getField无法获取
              Field ageField = cls.getField("age");
              System.out.println(ageField.get(o)); // 传统方法 对象.成员变量, 反射机制 成员变量对象.get(对象名)
      
              // java.lang.reflect.Constructor:代表类的构造方法, Constructor对象表示某个类的构造器
              Constructor constructor1 = cls.getConstructor(); // ()中可以指定构造器的参数类型, 返回一个无参构造器对象
              System.out.println(constructor1); // Cat()
              Constructor constructor2 = cls.getConstructor(String.class, int.class);// 传入String.class是String类的class对象, int.class是Integer类的Class对象
              System.out.println(constructor2); // Cat(String name)
      
          }
      }
      

      反射的优缺点:

      1. 优点: 可以动态的创建和使用对象(也是框架底层的核心), 使用灵活, 没有反射机制, 框架技术就失去底层支持
      2. 缺点: 使用反射机制基本是解释执行, 对执行速度有影响

      反射调用优化----关闭访问检查

      AccessibleObject类继承关系图

      1. Method, Field, Constructor对象都有setAccessible()方法
      2. setAccessible()方法的作用是启动和禁用访问安全检查的开关.
      3. setAccessible(boolean flag): 参数flag的作用是设置反射对象在使用时是否访问检查. true表示取消反射检查, 提高反射效率. false表示反射对象执行访问检查
      package com.lww.reflection;
      
      import java.lang.reflect.InvocationTargetException;
      import java.lang.reflect.Method;
      
      @SuppressWarnings({"all"})
      public class Reflection03 {
          public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
              m1();
              m2();
              m3();
          }
      
          /**
           * 传统方法 对象.方法() 测试耗时
           */
          public static void m1() {
              Cat cat = new Cat();
              long start = System.currentTimeMillis();
              for (int i = 0; i < 9_0000_0000; i++) {
                  cat.call();
              }
              long end = System.currentTimeMillis();
              System.out.println("m1() 耗时 = " + (end - start));
          }
      
          /**
           * 反射创建对象 并 调用方法 -> 方法对象.invoke(对象) 测试反射机制的耗时
           *
           * @throws ClassNotFoundException
           * @throws InstantiationException
           * @throws IllegalAccessException
           * @throws InvocationTargetException
           * @throws NoSuchMethodException
           */
          public static void m2() throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
              Class cls = Class.forName("com.lww.reflection.Cat");
              Object o = cls.newInstance();
              Method call = cls.getMethod("call");
              long start = System.currentTimeMillis();
              for (int i = 0; i < 9_0000_0000; i++) {
                  call.invoke(o);
              }
              long end = System.currentTimeMillis();
              System.out.println("m2() 耗时 = " + (end - start));
          }
      
          /**
           * 反射机制优化: 在反射调用方法时, 关闭访问检查 测试反射优化的耗时
           *
           * @throws ClassNotFoundException
           * @throws InstantiationException
           * @throws IllegalAccessException
           * @throws InvocationTargetException
           * @throws NoSuchMethodException
           */
          public static void m3() throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
              Class cls = Class.forName("com.lww.reflection.Cat");
              Object o = cls.newInstance();
              Method call = cls.getMethod("call");
              call.setAccessible(true); // 在反射调用方法时, 关闭访问检测
              long start = System.currentTimeMillis();
              for (int i = 0; i < 9_0000_0000; i++) {
                  call.invoke(o);
              }
              long end = System.currentTimeMillis();
              System.out.println("m3() 耗时 = " + (end - start));
          }
      }
      

      2.2、Class类

      Class类继承关系图

      基本介绍:

      1. Class也是类, 因此它继承Object类.
      2. Class类对象不是new出来的, 而是系统创建的
      3. 对于某个类的Class类对象, 在内存中只有一个, 因为同一个类只加载一次
      4. 每个类的实例都会记得自己是由哪个Class类实例所生成
      5. 通过Class类对象可以完整的得到一个类的完整结构, 通过一系列API
      6. Class对象是存放在堆中
      7. 类的字节码二进制数据, 是放在方法区的, 有的地方称为类的元数据(包括 方法代码, 变量名, 方法名, 访问权限等)
      package com.lww.reflection;
      
      public class Class01 {
          public static void main(String[] args) throws ClassNotFoundException {
              // 1.Class类继承Object类
              // 2.Class类对象不是new出来的, 而是系统创建的
              // (1)传统方法new对象
              //Cat cat = new Cat();
              /*
              public Class<?> loadClass(String name) throws ClassNotFoundException {
                  return loadClass(name, false);
              }
               */
              // (2)反射方式
              Class cls1 = Class.forName("com.lww.reflection.Cat");
              /*
              public static Class<?> forName(String className)
                      throws ClassNotFoundException {
                  Class<?> caller = Reflection.getCallerClass();
                  return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
              }
      
              static ClassLoader getClassLoader(Class<?> caller) {
                  // This can be null if the VM is requesting it
                  if (caller == null) {
                      return null;
                  }
                  // Circumvent security check since this is package-private
                  return caller.getClassLoader0();
              }
      
              ClassLoader getClassLoader0() { return classLoader; }
              // 系统调用类加载器创建Class类对象
              public Class<?> loadClass(String name) throws ClassNotFoundException {
                  return loadClass(name, false);
              }
               */
              // 3. 对于某个类的Class类对象, 在内存中只有一个, 因为类只加载一次
              Class cls2 = Class.forName("com.lww.reflection.Cat");
              System.out.println(cls1.hashCode());
              System.out.println(cls2.hashCode()); // cls1 == cls2 同一个类对象
              // 4.每个类的实例都会记得自己是由哪个Class类实例所生成
          }
      }
      

      Class类常用方法:

      方法名 功能说明
      static Class forName(String name) 返回指定类名name的Class对象
      Object newInstance() 调用缺省构造函数, 返回该Class对象的一个实例
      getName() 返回此Class对象所表示的实体(类, 接口, 数组类, 基本类型等)名称
      Class[] getInterfaces() 获取当前Class类对象的接口
      ClassLoader getClassLoader() 返回该类的类加载器
      Class getSuperclass() 返回表示此Class所表示的实体的超类Class
      Constructor[] getConstructors() 返回一个包含某些Constructor对象的数组
      Field[] getDeclaredFields() 返回Field对象的一个数组
      Method getMethod(String name, Class<?>... parameterTypes) 返回一个Method对象, 此对象的形参类型为parameterTypes
      package com.lww.reflection;
      
      public class Car {
          public String brand = "宝马";
          public int price = 500000;
          public String color = "渐变蓝色";
      
          @Override
          public String toString() {
              return "Car{" +
                      "brand='" + brand + '\'' +
                      ", price=" + price +
                      ", color='" + color + '\'' +
                      '}';
          }
      }
      
      package com.lww.reflection;
      
      import java.lang.reflect.Field;
      
      // 演示Class类的常用方法
      @SuppressWarnings({"all"})
      public class Class02 {
          public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException {
              String classAllPath = "com.lww.reflection.Car";
              // 获取Car类对应的Class对象
              // <?>表示不确定的Java类型
              Class<?> cls = Class.forName(classAllPath);
              System.out.println(cls); // 显示cls对象, 是哪个类的Class对象 com.lww.reflection.Car
              System.out.println(cls.getClass()); // 输出cls的运行类型 java.lang.Class
              // 得到包名
              System.out.println(cls.getPackage().getName()); // com.lww.reflection
              // 得到全类名
              System.out.println(cls.getName());
              // 通过cls创建对象实例
              Car car = (Car) cls.newInstance();
              System.out.println(car); // 调用car的toString()方法
              // 通过反射获取属性 brand
              Field brand = cls.getField("brand"); // 只能获取公共属性
              System.out.println(brand.get(car)); // 宝马
      
              // 通过反射给属性赋值
              brand.set(car, "奥迪");
              System.out.println(brand.get(car)); // 奥迪
              // 通过Field[]对象获取所有字段属性
              Field[] fields = cls.getFields();
              for (Field field : fields) {
                  System.out.print(field.get(car) + " ");
              }
          }
      }
      

      获取Class类对象

      1. 前提: 已知一个类的全类名, 且该类在类路径下, 可以通过Class类的静态方法forName()获取, 可能抛出ClassNotFoundException.

        如: Class cls1 = Class.forName("com.lww.reflection.Car");

        应用场景: 多用于配置文件, 读取类的全路径, 加载类

      2. 前提: 若已知具体的类, 通过类的class获取, 该方式最安全可靠, 程序性能最高.

        如: Class cls2 = Cat.class;

        应用场景: 多用于参数传递, 比如通过反射得到对应构造器对象

      3. 前提: 已知某个类的实例obj, 调用该实例的getClass()方法获取Class对象

        如:Class cls3 = obj.getClass(); 获取它的运行类型

        应用场景: 有对象实例, 通过创建好的对象实例obj, 获取Class对象

      4. ClassLoader cl = obj.getClass().getClassLoader();

        Class cls4 = cl.loadClass("类的全类名");

      5. 基本数据类型(int, char, boolean, float, double, byte, long, short)都有Class对象. 通过基本数据类型.class获取Class对象

        Class<Integer> integerClass = int.class;
        Class<Character> characterClass = char.class;
        Class<Boolean> booleanClass = boolean.class;
        Class<Float> floatClass = float.class;
        Class<Double> doubleClass = double.class;
        Class<Byte> byteClass = byte.class;
        Class<Short> shortClass = short.class;
        Class<Long> longClass = long.class;
        
      6. 基本数据类型对应的包装类, 可以通过包装类.TYPE得到Class对象

        Class<Integer> type1 = Integer.TYPE;
        Class<Character> type2 = Character.TYPE;
        Class<Boolean> type3 = Boolean.TYPE;
        Class<Float> type4 = Float.TYPE;
        Class<Double> type5 = Double.TYPE;
        Class<Byte> type6 = Byte.TYPE;
        Class<Short> type7 = Short.TYPE;
        Class<Long> type8 = Long.TYPE;
        
      package com.lww.reflection;
      
      @SuppressWarnings({"all"})
      public class GetClass_ {
          public static void main(String[] args) throws ClassNotFoundException {
              // 1.Class.forName()
              String classAllPath = "com.lww.reflection.Car"; // classAllPath一般通过配置文件读取
              Class<?> cls1 = Class.forName(classAllPath);
              System.out.println(cls1);
      
              // 2.类名.class
              Class cls2 = Car.class; // 需要在同一个包下
              System.out.println(cls2);
      
              // 3.对象.getClass()
              Car car = new Car();
              Class cls3 = car.getClass();
              System.out.println(cls3);
      
              // 4.通过类加载器[4种]来获取类的Class对象
              // (1)先得到类加载器car
              ClassLoader classLoader = car.getClass().getClassLoader();
              // (2)通过类加载器得到Class对象
              Class<?> cls4 = classLoader.loadClass(classAllPath);
              System.out.println(cls4);
      
              // 对于同一个类而言, 上述四种方式获取的Class对象是同一个对象
              System.out.println(cls1.hashCode() + ", " + cls2.hashCode() + ", " + cls3.hashCode() + ", " + cls4.hashCode());
      
              // 5.基本数据类型(int, char, boolean, float, double, byte, long, short)都有Class对象
              Class<Integer> integerClass = int.class;
              Class<Character> characterClass = char.class;
              Class<Boolean> booleanClass = boolean.class;
              Class<Float> floatClass = float.class;
              Class<Double> doubleClass = double.class;
              Class<Byte> byteClass = byte.class;
              Class<Short> shortClass = short.class;
              Class<Long> longClass = long.class;
              System.out.println(integerClass); // int, 自动装箱和自动拆箱
      
              // 6.基本数据类型对应的包装类, 可以通过包装类.TYPE 得到Class对象
              Class<Integer> type1 = Integer.TYPE;
              Class<Character> type2 = Character.TYPE;
              Class<Boolean> type3 = Boolean.TYPE;
              Class<Float> type4 = Float.TYPE;
              Class<Double> type5 = Double.TYPE;
              Class<Byte> type6 = Byte.TYPE;
              Class<Short> type7 = Short.TYPE;
              Class<Long> type8 = Long.TYPE;
              System.out.println(type3); // boolean
              System.out.println(type1 == integerClass); // true: 同一个对象, Java底层对于基本数据类型会进行自动装箱/拆箱
          }
      }
      

      以下类型都有Class对象:

      1. 外部类, 成员内部类, 静态内部类, 局部内部类, 匿名内部类
      2. interface:接口
      3. 数组
      4. enum: 枚举
      5. annotation: 注解
      6. 基本数据类型, 包装类
      7. void
      8. Class类
      package com.lww.reflection;
      
      import java.io.Serializable;
      
      public class AllTypeClass {
          public static void main(String[] args) {
              Class<String> cls1 = String.class; // 外部类
              Class<Serializable> cls2 = Serializable.class; // 接口
              Class<Integer[]> cls3 = Integer[].class; // 数组
              Class<double[][]> cls4 = double[][].class; // 二维数组
              Class<Deprecated> cls5 = Deprecated.class; // 注解
              Class<Thread.State> cls6 = Thread.State.class; // 枚举
              Class<Long> cls7 = long.class; // 基本数据类型
              Class<Float> cls8 = Float.class; // 包装类
              Class<Void> cls9 = void.class; // void数据类型
              Class<Class> cls10 = Class.class; // Class -> 万物皆对象!
          }
      }
      

      2.3、类的加载

      基本说明:
      反射机制是Java实现动态语言的关键, 也就是通过反射实现类动态加载

      1. 静态加载: 编译时加载相关的类, 如果没有对应类则报错, 依赖性强. new创建对象时在编译阶段会静态加载类信息
      2. 动态加载: 运行时加载需要的类, 如果运行时不用该类, 即使不存在该类也不会报错, 降低了依赖性. 反射是动态加载, 只有运行到相关类的代码时才会加载信息

      类加载时机:

      1. 当使用new创建对象时(静态加载)
      2. 当子类被加载时, 父类也会被加载(静态加载)
      3. 调用类中的静态成员时(静态加载)
      4. 通过反射(动态加载)

      类加载过程图(⭐⭐⭐⭐⭐)

      类加载过程图

      各阶段完成的任务(⭐⭐⭐⭐⭐)

      类加载各阶段完成任务

      类加载的五个阶段:(⭐⭐⭐⭐⭐)

      1. 加载阶段:JVM在该阶段的主要目的是将字节码从不同的数据源(可能是class文件, 也可能是jar包, 甚至网络)转化为二进制字节流加载到内存中, 并生成一个代表该类的java.lang.Class对象.

      2. 连接阶段-验证:

        1. 目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求, 并且不会危害虚拟机自身的安全
        2. 包括: 文件格式验证(是否以魔数oxcafebabe开头, 打开class文件, 内容以oxcafebabe开头), 元数据验证, 字节码验证和符号引用验证
        3. 可以考虑使用 -Xverify:none参数来关闭大部分的类验证措施, 缩短虚拟机类加载的时间
      3. 连接阶段-准备:

        JVM会在该阶段对静态变量分配内存并默认初始化(对应数据类型的默认初始值, 如0, 0L, null, false等). 这些变量所使用的内存都将在方法区中(也有说法在堆中)进行分配. 非静态变量在该阶段不会分配内存. 静态常量会根据指定赋值初始化

        class A {
            // 连接阶段-准备, 属性如何处理:
            // n1是实例属性, 不是静态变量, 因此在该阶段不会为n1分配内存
            public int n1 = 10;
            // n2是static, 是静态变量, 分配内存, 默认初始化n2 = 0, 不是20
            public static int n2 = 20;
            // n3是static final, 是常量, 和静态变量不一样, 因为类常量一旦赋值就不变, 因此直接初始化赋值n3 = 30
            public static final int n3 = 30;
        }
        
      4. 连接阶段-解析:

        虚拟机将常量池内的符号引用替换为直接引用(内存地址)的过程.

      5. Initialization初始化

        1. 到初始化阶段, 才真正开始执行类中定义的Java程序代码, 此阶段是执行<clinit>()方法的过程.

        2. <clinit>()方法是由编译器按语句在源文件中出现的顺序, 依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句, 并进行合并.

        3. 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁-同步, 如果多个线程同时去初始化一个类, 那么只会有一个线程去执行这个类的<clinit>()方法, 其他线程都需要阻塞等待, 直到活动线程执行<clinit>()方法完毕. 正因为有这个线程同步机制, 才能保证某个类在内存中只有一个Class对象

          底层源码

          protected Class<?> loadClass(String name, boolean resolve)
              throws ClassNotFoundException
          {	// 正因为有这个线程同步机制, 才能保证某个类在内存中只有一个Class对象
              synchronized (getClassLoadingLock(name)) {
                  // First, check if the class has already been loaded
                  Class<?> c = findLoadedClass(name);
                  if (c == null) {
                      long t0 = System.nanoTime();
                      try {
                          if (parent != null) {
                              c = parent.loadClass(name, false);
                          } else {
                              c = findBootstrapClassOrNull(name);
                          }
                      } catch (ClassNotFoundException e) {
                          // ClassNotFoundException thrown if class not found
                          // from the non-null parent class loader
                      }
          
                      if (c == null) {
                          // If still not found, then invoke findClass in order
                          // to find the class.
                          long t1 = System.nanoTime();
                          c = findClass(name);
          
                          // this is the defining class loader; record the stats
                          sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                          sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                          sun.misc.PerfCounter.getFindClasses().increment();
                      }
                  }
                  if (resolve) {
                      resolveClass(c);
                  }
                  return c;
              }
          }
          

      2.4、反射获取类的结构信息

      反射获取类的结构信息的常用方法

      java.lang.Class类提供获取类的结构信息的方法:

      方法名 作用
      getName 获取全类名(包名.类名)
      getSimpleName 获取类名
      getFields 获取所有public修饰的属性, 包含本类以及父类(直接父类和间接父类)的
      getDeclaredFields 获取本类中所有的属性
      getMethods 获取所有public修饰的方法, 包括本类以及父类(直接父类和间接父类)的
      getDeclaredMethods 获取本类中所有的方法
      getConstructors 获取本类所有public修饰的构造器
      getDeclaredConstructors 获取本类所有的构造器
      getPackage 以Package形式返回包信息
      getSuperClass 以Class形式返回父类信息
      getInterfaces 以Class[]形式返回所有接口信息
      getAnnotations 以Annotation[]形式返回所有注解信息

      java.lang.reflect.Field类提供获取类的字段信息的方法:

      方法名 作用
      getModifiers 以int形式返回修饰符.[0-默认修饰符,1-public,2-private,4-protected,8-static,16-final] 如:public static = 1 + 8 = 9
      getType 以Class形式返回字段的类型
      getName 返回字段名

      java.lang.reflect.Method类提供获取类的方法信息的方法:

      方法名 作用
      getModifiers 以int形式返回修饰符.[0-默认修饰符,1-public,2-private,4-protected,8-static,16-final] 如:protected static = 4 + 8 = 12
      getReturnType 以Class形式获取返回类型
      getName 返回方法名
      getParameterTypes 以Class[]返回参数类型数组

      java.lang.reflect.Constructor类提供获取类的构造器信息的方法:

      方法名 作用
      getModifiers 以int形式返回修饰符.[0-默认修饰符,1-public,2-private,4-protected]
      getParameterTypes 以Class[]返回参数类型数组
      getName 返回构造器名(全类名)

      反射爆破

      通过反射创建对象的两种方式:

      1. 调用类中的public修饰的默认无参构造器
      2. 调用类中的指定构造器

      Class类相关方法:

      • newInstance():调用类中的无参构造器, 获取对应类的对象
      • getConstructor(Class...clazz): 根据参数列表, 获取对应的public构造器对象
      • getDecalaredConstructor(Class...clazz):根据参数列表, 获取对应的构造器对象

      Constructor类相关方法:

      • setAccessible(boolean flag):暴破. 使用反射解决访问非public构造器的问题
      • newInstance(Object...obj):调用构造器
      package com.lww.reflection;
      
      import java.lang.reflect.Constructor;
      import java.lang.reflect.InvocationTargetException;
      
      public class ReflectCreateInstance {
          public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
              // 1.先获取到User类的Class对象
              Class<?> userClass = Class.forName("com.lww.reflection.User");
              // 2.通过public的无参构造器创建实例
              Object o = userClass.newInstance();
              System.out.println(o);
      
              // 3.通过public的有参构造器创建实例
              // 3.1.得到对应的有参构造器
              Constructor<?> constructor = userClass.getConstructor(String.class);
              // 3.2.创建对象实例并传入实参
              Object o1 = constructor.newInstance("yxz");
              System.out.println(o1);
      
              // 4.通过非public的有参构造器创建实例
              // 4.1.得到private的构造器对象
              Constructor<?> constructor1 = userClass.getDeclaredConstructor(String.class, int.class);
              // 4.2 创建对象实例
              constructor1.setAccessible(true); // 暴破(暴力破解): 使用反射可以访问非public构造器
              Object yxz = constructor1.newInstance("yxz", 21);
              System.out.println(yxz);
          }
      
      }
      
      class User {
          private String name;
          private int age;
      
          private User(String name, int age) {
              this.name = name;
              this.age = age;
          }
      
          public User(String name) {
              this.name = name;
          }
      
          public User() {
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          public int getAge() {
              return age;
          }
      
          public void setAge(int age) {
              this.age = age;
          }
      
          @Override
          public String toString() {
              return "User{" +
                      "name='" + name + '\'' +
                      ", age=" + age +
                      '}';
          }
      }
      

      通过反射访问类中的成员

      访问属性

      1. 根据属性名获取Field对象
      2. 暴破: fieldObject.setAccessible(true): fieldObject是字段对象. 参数设置为true可以访问类中的非public字段
      3. 访问:
        1. fieldObject.set(object, 值): object表示对象名, 给object对象设置字段属性值
        2. syso(fieldObject.get(object)): object是对象名
      4. 如果是静态属性, 则set和get中的参数object可以写成null
      package com.lww.reflection;
      
      import java.lang.reflect.Field;
      
      public class ReflectAccessProperty {
          public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchFieldException {
              // 1.得到Student类对应的Class对象
              Class<?> studentClass = Student.class;
              // 2.创建对象
              Object o = studentClass.newInstance();
              // 3.使用反射得到age属性对象
              Field age = studentClass.getField("age");
              age.set(o, 23);
              System.out.println(o);
              System.out.println(age.get(o));
      
              // 4.使用反射操作静态私有属性name
              Field name = studentClass.getDeclaredField("name");
              // 对name进行暴破, 访问私有属性
              name.setAccessible(true);
              name.set(o, "yxz");
              name.set(null, "lww"); // 只有静态属性的对象可以为null, 静态属性与对象无关
              System.out.println(o);
              System.out.println(name.get(o));
              System.out.println(name.get(null)); // 静态属性的对象可以为null
          }
      }
      
      class Student {
          private static String name;
          public int age;
      
          public Student() {
          }
      
          @Override
          public String toString() {
              return "Student{" +
                      "name='" + name + '\'' +
                      ", age=" + age +
                      '}';
          }
      }
      

      访问方法

      1. 根据方法名和参数列表获取Method方法对象:Method m = clazz.getDeclaredMethod(方法名, XX.class);
      2. 获取对象: Object o = clazz.newInstance();
      3. 暴破: m.setAccessible(true);
      4. 访问: Object returnValue = m.invoke(o, 方法的实参列表);
      5. 如果是静态方法, 则invoke的参数o可以写成null
      6. 在反射中, 如果方法有返回值, 统一返回(编译类型)Object对象, 该对象运行类型与方法返回值类型一致
      package com.lww.reflection;
      
      import java.lang.reflect.InvocationTargetException;
      import java.lang.reflect.Method;
      
      public class ReflectAccessMethod {
          public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
              // 1.加载类
              Class<?> bossCls = Class.forName("com.lww.reflection.Boss");
              // 2.创建对象实例
              Object o = bossCls.newInstance();
              // 3.获取方法对象
              // 3.1 得到hi方法对象
              Method hi = bossCls.getMethod("hi", String.class); // 传入方法的形参class对象
              Method hi1 = bossCls.getDeclaredMethod("hi", String.class);
              // 3.2 调用
              hi.invoke(o, "java");
      
              // 4.调用private static方法
              // 4.1 得到方法对象
              Method say = bossCls.getDeclaredMethod("say", int.class, String.class, char.class);
              // 4.2 因为say方法是private, 所以需要暴破
              say.setAccessible(true);
              // 4.3 调用方法
              System.out.println(say.invoke(o, 21, "lww", '女'));
              // 因为say方法为静态方法, 所以对象可以为null
              System.out.println(say.invoke(null, 22, "yxz", '男'));
      
              // 在反射中, 如果方法有返回值, 统一返回Object对象
              Object invoke = say.invoke(null, 21, "lww", '女');
              System.out.println(invoke.getClass()); // class java.lang.String
      
          }
      }
      
      class Boss {
          private static String name;
          public int age;
      
          public Boss() {
          }
      
          private static String say(int n, String s, char c) {
              return n + " " + s + " " + c;
          }
      
          public void hi(String s) {
              System.out.println("hi " + s);
          }
      
          @Override
          public String toString() {
              return "Boss{" +
                      "name='" + name + '\'' +
                      ", age=" + age +
                      '}';
          }
      }
      

      反射综合练习:使用反射机制创建文件

      package com.lww.reflection;
      
      import java.lang.reflect.Constructor;
      import java.lang.reflect.InvocationTargetException;
      import java.lang.reflect.Method;
      
      public class ReflectExercise {
          public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
              // 1. 加载File类
              Class<?> fileCls = Class.forName("java.io.File");
              // 2. 获取所有构造器
              Constructor<?>[] declaredConstructors = fileCls.getDeclaredConstructors();
              for (Constructor<?> declaredConstructor : declaredConstructors) {
                  System.out.println("构造器: " + declaredConstructor);
              }
              // 3. 指定得到File(java.lang.String)构造器
              Constructor<?> declaredConstructor = fileCls.getDeclaredConstructor(String.class);
              // 4. 指定文件路径
              String fileAllPath = "d:\\Java\\Demo\\reflect.txt";
              // 5. 创建File对象
              Object o = declaredConstructor.newInstance(fileAllPath);// 创建File对象, 此时内存中只有对象, 还未创建文件
              // 6. 获取createNewFile()方法的对象
              Method createNewFile = fileCls.getDeclaredMethod("createNewFile");
              // 7. 调用方法获取createNewFile()创建文件
              createNewFile.invoke(o); // ==> o.createNewFile()
              System.out.println("文件创建成功");
          }
      }
      

      3、Mysql基础

      此章节笔记见: MySQL基础

      4、JDBC和连接池

      4.1、JDBC概述

      基本介绍:

      1. JDBC为访问不同的数据库提供了一组接口,为使用者屏蔽了细节问题。

      2. Java程序员使用JDBC,可以连接任何提供了JDBC驱动程序的数据库系统,从而完成对数据库的各种操作。

      3. JDBC基本原理:

        JDBC原理示意图

      4. 模拟JDBC:

      JdbcInterface.java文件 -> 模拟Java厂商指定的规范

      package com.jdbc.imitate;
      
      public interface JdbcInterface {
          // 获取数据库连接
          public Object getConnection();
      
          // 模拟CRUD
          public void crud();
      
          // 关闭数据库连接
          public void close();
      }
      

      MysqlJdbcImpl.java文件 -> 模拟MySQL厂商的实现接口类(jar包,驱动程序)

      package com.jdbc.imitate;
      
      public class MysqlJdbcImpl implements JdbcInterface {
      
          @Override
          public Object getConnection() {
              System.out.println("模拟 获取到MySQL的连接...");
              return null;
          }
      
          @Override
          public void crud() {
              System.out.println("模拟 MySQL中增删改查...");
          }
      
          @Override
          public void close() {
              System.out.println("模拟 MySQL的关闭连接...");
          }
      }
      

      OracleJdbcImpl.java文件 -> 模拟Oracle厂商的实现接口类(jar包,驱动程序)

      package com.jdbc.imitate;
      
      public class OracleJdbcImpl implements JdbcInterface {
      
          @Override
          public Object getConnection() {
              System.out.println("模拟 获取到Oracle的连接...");
              return null;
          }
      
          @Override
          public void crud() {
              System.out.println("模拟 Oracle中增删改查...");
          }
      
          @Override
          public void close() {
              System.out.println("模拟 Oracle的关闭连接...");
          }
      }
      

      JdbcTest.java测试类 -> 模拟Java程序操作各种数据库

      package com.jdbc.imitate;
      
      public class JdbcTest {
          public static void main(String[] args) {
              JdbcInterface jdbcInterface1 = new MysqlJdbcImpl();
              jdbcInterface1.getConnection();
              jdbcInterface1.crud();
              jdbcInterface1.close();
              System.out.println("==============================");
              JdbcInterface jdbcInterface2 = new OracleJdbcImpl();
              jdbcInterface2.getConnection();
              jdbcInterface2.crud();
              jdbcInterface2.close();
          }
      }
      

      4.2、JDBC快速入门

      JDBC API介绍

      JDBC API是一系列的接口,它统一和规范了应用程序和数据库的连接、执行SQL语句,并得到返回结果等各类操作,相关类和各类接口在java.sql包javax.sql包中。

      JDBC程序的编写步骤

      1. 准备工作:下载相应驱动包(jar包)并添加到项目目录下的libs(自己创建的)目录,右键点击“add as library”加入项目库。
      2. 注册驱动 -> 加载Driver类
      3. 获取连接 -> 得到Connection(数据库的连接)
      4. 执行增删改查 -> 发送SQL命令给相应数据库系统执行
      5. 释放资源 -> 关闭相关数据库连接

      数据库连接方式1:通过new com.mysql.cj.jdbc.Driver()获取Driver对象,这种方式属于静态加载,灵活性差,依赖性强

      package com.jdbc.myjdbc;
      
      import com.mysql.cj.jdbc.Driver;
      
      import java.sql.Connection;
      import java.sql.SQLException;
      import java.sql.Statement;
      import java.util.Properties;
      
      public class Jdbc01 {
          public static void main(String[] args) throws SQLException {
              // 0.前置工作:在项目下创建一个libs目录,用于放各种jar包,
              // 如mysql的驱动包,并右键jar包 点击"add as library" 加入到项目库
              // 1.注册驱动
              Driver driver = new Driver();
              /*
                  说明:
                  jdbc:mysql:// -> 固定格式,表示协议,通过jdbc的方式连接mysql
                  localhost -> 表示主机,可以是ip地址(localhost表示的ip地址127.0.0.1)
                  3380 -> 表示mysql服务监听的端口号
                  jdbc_test -> 表示mysql DBMS连接的哪一个数据库
                  mysql连接的本质就是socket连接
              */
              String url = "jdbc:mysql://localhost:3380/jdbc_test";
              // 将用户名和密码放入到Properties对象中
              // 说明:user 和 password是规定好的key,后面的值对应用户名和密码
              Properties properties = new Properties();
              properties.setProperty("user", "root");
              properties.setProperty("password", "123456");
      
              // 2.得到连接
              Connection connect = driver.connect(url, properties);
      
              // 3.执行sql语句
              String sql = "INSERT INTO goddess(NAME,borndate) VALUES('刘亦菲','1987-08-25')";
              // Statement对象 用于执行静态 SQL 语句并返回它所生成结果的对象。
              Statement statement = connect.createStatement();
              // 返回受DML操作影响的行数
              int resultRows = statement.executeUpdate(sql);
              System.out.println(resultRows > 0 ? "插入成功" : "操作失败");
      
              // 4.关闭资源
              statement.close();
              connect.close();
          }
      }
      

      数据库连接方式2:通过反射Class.forName()方式,动态加载Driver类,更加灵活,减少依赖性

      数据库连接方式3:使用DriverManager替代Driver

      数据库连接方式4:使用Class.forName()自动完成注册驱动,简化代码

      注:MySQL驱动5.1.6及以后的版本可以无需Class.forName(“com.mysql.cj.jdbc.Driver”)

      从JDK1.5以后使用了JDBC4,不再需要显式调用Class.forName()注册驱动,而是自动调用驱动包jar下的路径:META-INF/services/java.sql.Driver文本中的类名称去注册。

      建议:写上Class.forName("com.mysql.cj.jdbc.Driver"),可读性更好

      数据库连接方式5:在方式4的基础上,使用配置文件,连接数据库更加灵活

      参考代码:

      package com.jdbc.myjdbc;
      
      import com.mysql.cj.jdbc.Driver;
      import org.junit.jupiter.api.Test;
      
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.sql.Connection;
      import java.sql.DriverManager;
      import java.sql.SQLException;
      import java.util.Properties;
      
      public class Jdbc02 {
          // 方式1:new com.mysql.cj.jdbc.Driver()静态获取Driver对象
          @Test
          public void connect01() throws SQLException {
              Driver driver = new Driver();
              String url = "jdbc:mysql://localhost:3380/jdbc_test";
              Properties properties = new Properties();
              properties.setProperty("user", "root");
              properties.setProperty("password", "123456");
              Connection connect = driver.connect(url, properties);
              System.out.println("方式2:" + connect);
          }
      
          // 方式2:通过反射,动态加载Driver
          @Test
          public void connect02() throws ClassNotFoundException, InstantiationException, IllegalAccessException, SQLException {
              Class<?> aClass = Class.forName("com.mysql.cj.jdbc.Driver");
              Driver driver = (Driver) aClass.newInstance();
              String url = "jdbc:mysql://localhost:3380/jdbc_test";
              Properties properties = new Properties();
              properties.setProperty("user", "root");
              properties.setProperty("password", "123456");
              Connection connect = driver.connect(url, properties);
              System.out.println("方式2:" + connect);
          }
      
          // 方式3:使用DriverManager替代Driver
          @Test
          public void connect03() throws ClassNotFoundException, InstantiationException, IllegalAccessException, SQLException {
              Class<?> aClass = Class.forName("com.mysql.cj.jdbc.Driver");
              Driver driver = (Driver) aClass.newInstance();
              String url = "jdbc:mysql://localhost:3380/jdbc_test";
              String user = "root";
              String password = "123456";
              Properties properties = new Properties();
              properties.setProperty("user", user);
              properties.setProperty("password", password);
              // 向 DriverManager 注册给定驱动程序。
              DriverManager.registerDriver(driver);
              Connection connection = DriverManager.getConnection(url, properties);
              System.out.println("方式3:" + connection);
          }
      
          // 方式4:使用Class.forName()自动完成注册驱动,简化代码(实际开发中使用最多,推荐)
          @Test
          public void connect04() throws ClassNotFoundException, SQLException {
              // 在加载Driver类时,自动完成了注册驱动
              /*
                 源码分析:
                 public class Driver extends NonRegisteringDriver implements java.sql.Driver {
                      public Driver() throws SQLException {
                      }
                      // 1.静态代码块:会在类加载时执行一次
                      // 2.DriverManager.registerDriver(new Driver()); 注册驱动
                      // 3.因此注册驱动已经完成
                      static {
                          try {
                              DriverManager.registerDriver(new Driver());
                          } catch (SQLException var1) {
                              throw new RuntimeException("Can't register driver!");
                          }
                      }
                  }
               */
              Class.forName("com.mysql.cj.jdbc.Driver");
              String url = "jdbc:mysql://localhost:3380/jdbc_test";
              String user = "root";
              String password = "123456";
              Connection connection = DriverManager.getConnection(url, user, password);
              System.out.println("方式4:" + connection);
          }
      
          // 方式5:在方式4的基础上,使用配置文件,连接数据库更加灵活
          @Test
          public void connect05() throws IOException, ClassNotFoundException, SQLException {
              // 通过Properties对象获取配置文件的信息
              Properties properties = new Properties();
              properties.load(new FileInputStream("src\\mysql.properties"));
              // 获取相关的值
              String user = properties.getProperty("user");
              String password = properties.getProperty("password");
              String driver = properties.getProperty("driver");
              String url = properties.getProperty("url");
              Class.forName(driver); // 建议写上
      
              Connection connection = DriverManager.getConnection(url, user, password);
              System.out.println("方式5:" + connection);
          }
      }
      

      mysql.properties配置文件

      user=root
      password=123456
      url=jdbc:mysql://localhost:3380/jdbc_test
      driver=com.mysql.cj.jdbc.Driver
      

      ResultSet结果集

      基本介绍:

      1. 表示数据库结果集的数据表,通常通过执行查询数据库的语句生成。
      2. ResultSet对象保持一个光标指向其当前的数据行。最初,光标位于第一行之前
      3. next()方法将光标移动到下一行,并且由于ResultSet对象中没有更多行时返回false,因此可以在while循环中使用循环来遍历结果集
      4. getXxx(int index|String columnName)方法:参数可以为列的索引(即第几列,从1开始),也可以是列名(不受select语句中列名顺序影响)。获取某一行某一列的值。
      package com.jdbc.resultset;
      
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.sql.*;
      import java.util.Properties;
      
      public class ResultSetTest {
          public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
              Properties properties = new Properties();
              // Properties.load()中加载Properties文件的路径是绝对路径
              properties.load(new FileInputStream("D:\\Program Files (x86)\\IDEA\\Projects\\Module02_Study\\src\\mysql.properties"));
              // getResourceAsStream(String path):
              // 如果path带"/",那么就是从类路径.classpath中去找文件,
              // 如果path不带"/",那么就是从当前class文件的路径下找文件
              // properties.load(ResultSetTest.class.getResourceAsStream("/mysql.properties"));
              // 获取相关值
              String user = properties.getProperty("user");
              String password = properties.getProperty("password");
              String driver = properties.getProperty("driver");
              String url = properties.getProperty("url");
              // 1. 注册驱动
              Class.forName(driver);
              // 2. 得到连接
              Connection connection = DriverManager.getConnection(url, user, password);
              // 3. 得到Statement
              Statement statement = connection.createStatement();
              // 4. 编写sql语句
              String sql = "select id,name,sex,borndate from goddess";
              // executeQuery(String sql): 执行给定的 SQL 语句,该语句返回单个 ResultSet 对象。
              ResultSet resultSet = statement.executeQuery(sql);
              // next初始指向表头
              // 5. 使用while循环取出数据
              while (resultSet.next()) { // next()使光标向后移动,如果没有更多行,返回fasle
                  int id = resultSet.getInt(1);// 获取该行的第1列的值
                  int id2 = resultSet.getInt("id"); // 通过列名来获取值
                  String name = resultSet.getString(2);// 获取该行的第2列的值
                  String gender = resultSet.getString(3);// 获取该行的第3列的值
                  Date date = resultSet.getDate(4);// 获取该行的第4列的值
                  System.out.println("id: " + id + "\t" + "name: " + name + "\t" + "gender: " + gender + "\t" + "date: " + date);
              }
              // 6. 关闭连接
              resultSet.close();
              statement.close();
              connection.close();
          }
      }
      

      ResultSet对象存储的内容分析:

      ResultSet内容分析1

      ResultSet内容分析2

      Statement

      基本介绍:

      1. Statement对象 用于执行静态SQL语句并返回其生成的结果的对象。
      2. 在建立连接后,需要对数据库进行访问,执行命令或是SQL语句,可以通过
        • Statement【存在SQL注入问题,实际开发不会使用】
        • PreparedStatement【预处理】
        • CallableStatement【存储过程】
      3. Statement对象执行SQL语句,存在SQL注入风险。
      4. SQL注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的SQL语句段或命令,恶意攻击数据库。
      5. 要防范SQL注入,只要用PreparedStatement(从Statement扩展而来)取代Statement就可以。

      模拟SQL注入

      输入的用户名:1' or

      输入的用户密码:or '1' = '1

      package com.jdbc.statement;
      
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.sql.*;
      import java.util.Properties;
      import java.util.Scanner;
      
      public class Statement_ {
          public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
              Scanner scanner = new Scanner(System.in);
              // 如果想要看到SQL注入,需要使用nextLine()方法,next()方法读取到空格或者'就停止
              System.out.print("请输入管理员名字:");
              String admin_name = scanner.nextLine(); // 输入用户名:1' or
              System.out.print("请输入管理员密码:");
              String admin_pwd = scanner.nextLine(); // 输入万能密码: or '1' = '1
      
              Properties properties = new Properties();
              properties.load(new FileInputStream("D:\\Program Files (x86)\\IDEA\\Projects\\Module02_Study\\src\\mysql.properties"));
              String user = properties.getProperty("user");
              String password = properties.getProperty("password");
              String driver = properties.getProperty("driver");
              String url = properties.getProperty("url");
              // 1.注册驱动
              Class.forName(driver);
              // 2.得到连接
              Connection connection = DriverManager.getConnection(url, user, password);
              // 3.得到Statement
              Statement statement = connection.createStatement();
              // 4.编写sql
              String sql = "select user, password from users where user = '"
                      + admin_name + "' and password = '" + admin_pwd + "'";
              ResultSet resultSet = statement.executeQuery(sql);
              if (resultSet.next()) {
                  System.out.println("登录成功");
              } else {
                  System.out.println("登陆失败");
              }
              // 5.关闭连接
              resultSet.close();
              statement.close();
              connection.close();
          }
      }
      

      PreparedStatement

      基本介绍:

      1. PreparedStatement执行的SQL语句中的参数用问号(?)来表示,调用PreparedStatement对象的setXxx()方法来设置这些参数。setXxx()方法有两个参数,第一个参数是要设置的SQL语句中的参数的索引(从1开始,即?在SQL语句中是第几个?),第二个是设置的SQL语句中的参数的值。
      2. 调用executeQuery():返回Result对象
      3. 调用executeUpdate():执行更新,包括增、删、改。
      package com.jdbc.preparedstatement_;
      
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.sql.*;
      import java.util.Properties;
      import java.util.Scanner;
      
      public class PreparedStatement_ {
          public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
              Scanner scanner = new Scanner(System.in);
              // 如果想要看到SQL注入,需要使用nextLine()方法,next()方法读取到空格或者'就停止
              System.out.print("请输入管理员名字:");
              String admin_name = scanner.nextLine(); // 输入用户名:1' or
              System.out.print("请输入管理员密码:");
              String admin_pwd = scanner.nextLine(); // 输入万能密码: or '1' = '1
      
              Properties properties = new Properties();
              properties.load(new FileInputStream("D:\\Program Files (x86)\\IDEA\\Projects\\Module02_Study\\src\\mysql.properties"));
              String user = properties.getProperty("user");
              String password = properties.getProperty("password");
              String driver = properties.getProperty("driver");
              String url = properties.getProperty("url");
              // 1.注册驱动
              Class.forName(driver);
              // 2.得到连接
              Connection connection = DriverManager.getConnection(url, user, password);
              // 3.得到PreparedStatement
              // 3.1.编写sql,sql语句中的?相当于占位符
              String sql = "select user, password from users where user = ? and password = ?";
              // 3.2.preparedStatement对象是PreparedStatement接口的实现类的对象
              PreparedStatement preparedStatement = connection.prepareStatement(sql);
              // 此时sql语句已经和preparedStatement对象关联,执行sql语句时不需要再传入sql语句
              // 3.3.给?赋值
              preparedStatement.setString(1, admin_name);
              preparedStatement.setString(2, admin_pwd);
              // 4.执行select语句,要使用executeQuery()方法
              ResultSet resultSet = preparedStatement.executeQuery(); // 不能传入sql语句
              if (resultSet.next()) {
                  System.out.println("登录成功");
              } else {
                  System.out.println("登陆失败");
              }
      
              // 5.关闭连接
              resultSet.close();
              preparedStatement.close();
              connection.close();
          }
      }
      
      package com.jdbc.preparedstatement_;
      
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.sql.*;
      import java.util.Properties;
      import java.util.Scanner;
      
      public class PreparedStatementDML_ {
          public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
              Scanner scanner = new Scanner(System.in);
              System.out.print("请输入要修改的管理员名字:");
              String admin_name = scanner.nextLine();
      //        System.out.print("请输入要修改的管理员密码:");
      //        String admin_pwd = scanner.nextLine();
      
              Properties properties = new Properties();
              properties.load(new FileInputStream("D:\\Program Files (x86)\\IDEA\\Projects\\Module02_Study\\src\\mysql.properties"));
              String user = properties.getProperty("user");
              String password = properties.getProperty("password");
              String driver = properties.getProperty("driver");
              String url = properties.getProperty("url");
              // 1.注册驱动
              Class.forName(driver);
              // 2.得到连接
              Connection connection = DriverManager.getConnection(url, user, password);
              // 3.得到PreparedStatement
              // 3.1.编写sql,sql语句中的?相当于占位符
              // Update
      //        String sql = "update users set password = ? where user = ?";
      //        String sql = "insert into users(user, password) values (?,?)";
              String sql = "delete from users where user = ?";
              // 3.2.preparedStatement对象是PreparedStatement接口的实现类的对象
              PreparedStatement preparedStatement = connection.prepareStatement(sql);
              // 3.3.给?赋值
              preparedStatement.setString(1, admin_name);
      
              // 4.调用executeUpdate()方法,返回受影响的行数
              int rows = preparedStatement.executeUpdate();
              System.out.println(rows > 0 ? "修改成功" : "修改失败");
      
              // 5.关闭连接
              preparedStatement.close();
              connection.close();
          }
      }
      

      4.3、JDBC API

      JDBC API

      4.4、JDBC Utils

      在JDBC操作中,获取数据库连接和释放资源经常使用到,所以可以把获取数据库连接和释放资源操作抽出来封装成JDBC工具类JDBCUtils

      JDBCUtils.java工具类

      package com.jdbc.utils;
      
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.sql.*;
      import java.util.Properties;
      
      @SuppressWarnings({"all"})
      public class JDBCUtils {
          // 用户名
          private static String user;
          // 密码
          private static String password;
          // 驱动名
          private static String driver;
          // url
          private static String url;
      
          // 在static代码块完成类变量的初始化
          static {
              Properties properties = new Properties();
              try {
                  properties.load(new FileInputStream("src\\mysql.properties"));
                  // 从配置文件中读取相关属性值
                  user = properties.getProperty("user");
                  password = properties.getProperty("password");
                  driver = properties.getProperty("driver");
                  url = properties.getProperty("url");
              } catch (IOException e) {
                  // 在实际开发中,会将编译异常转成运行异常,抛出
                  // 调用者可以选择捕获该异常,也可以选择默认处理该异常,比较方便
                  throw new RuntimeException(e);
              }
          }
      
          /**
           * 获取MySQL数据库的连接的方法
           *
           * @return :返回Connection连接对象
           */
          public static Connection getConnection() {
      
              try {
                  return DriverManager.getConnection(url, user, password);
              } catch (SQLException e) {
                  // 在实际开发中,会将编译异常转成运行异常,抛出
                  // 调用者可以选择捕获该异常,也可以选择默认处理该异常,比较方便
                  throw new RuntimeException(e);
              }
          }
      
          /**
           * 释放资源的方法:如果需要释放资源则传入对象,否则传入null
           *
           * @param resultSet  :结果集对象
           * @param statement  :Statement或PreparedStatement对象
           * @param connection :数据库连接
           */
          public static void close(ResultSet resultSet, Statement statement, Connection connection) {
              try {
                  if (resultSet != null) {
                      resultSet.close();
                  }
                  if (statement != null) {
                      statement.close();
                  }
                  if (connection != null) {
                      connection.close();
                  }
              } catch (SQLException e) {
                  // 在实际开发中,会将编译异常转成运行异常,抛出
                  // 调用者可以选择捕获该异常,也可以选择默认处理该异常,比较方便
                  throw new RuntimeException(e);
              }
          }
      }
      

      JDBCUtils_Use.java测试JDBCUtils类

      package com.jdbc.utils;
      
      import org.junit.jupiter.api.Test;
      
      import java.sql.Connection;
      import java.sql.PreparedStatement;
      import java.sql.SQLException;
      import java.util.Scanner;
      
      public class JDBCUtils_Use {
          @Test
          public void testDML() {
              // 1.得到连接
              Connection connection = null;
              // 2.编写sql
              // String sql = "insert into goddess(name) values(?)";
              // String sql = "delete from goddess where name = ?";
              String sql = "update goddess set id = ? where name = ?";
              PreparedStatement preparedStatement = null;
              Scanner scanner = new Scanner(System.in);
              int id = scanner.nextInt();
              String name = scanner.nextLine();
              try {
                  connection = JDBCUtils.getConnection();
                  System.out.println(connection.getClass());
                  // 运行类型:com.mysql.cj.jdbc.ConnectionImpl
                  preparedStatement = connection.prepareStatement(sql);
                  // 给占位符赋值
                  preparedStatement.setInt(1, id);
                  preparedStatement.setString(2, name);
                  // 执行
                  int rows = preparedStatement.executeUpdate();
                  System.out.println(rows > 0 ? "操作成功" : "操作失败");
              } catch (SQLException e) {
                  e.printStackTrace();
              } finally {
                  JDBCUtils.close(null, preparedStatement, connection);
              }
          }
      }
      

      注意:

      Junit单元测试无法使用scanner输入和I/O流的解决方案:

      1. 通过IDEA工具Help-Edit Custom VM Options打开配置文件位置
      2. 修改配置文件idea64.exe.vmoptions,在最后一行添加:-Deditable.java.test.console=true
      3. 重启IDEA

      4.5、事务

      基本介绍:

      1. JDBC程序中当一个Connection对象创建时,默认情况下是自动提交事务:每次执行一个SQL语句时,如果执行成功,就会向数据库自动提交,提交事务后就不能回滚。
      2. JDBC程序中为了让多个SQL语句作为一个整体执行(要么全部成功,要么全部失败),需要使用事务。
      3. 调用Connection的setAutoCommit(false)可以取消自动提交事务。
      4. 在所有的SQL语句都执行成功后,调用Connection的commit()方法提交事务。
      5. 在其中某个操作失败或出现异常时,调用Connection的rollback()方法回滚事务。

      案例演示:银行转账

      package com.jdbc.transaction_;
      
      import com.jdbc.utils.JDBCUtils;
      import org.junit.jupiter.api.Test;
      
      import java.sql.Connection;
      import java.sql.PreparedStatement;
      import java.sql.SQLException;
      
      public class Transaction_ {
          @Test
          public void noTransaction() {
              // 1.得到连接
              Connection connection = null;
              // 2.编写sql
              String sql1 = "update account set balance = balance - 1000 where id = 1";
              String sql2 = "update account set balance = balance + 1000 where id = 2";
              PreparedStatement preparedStatement = null;
              try {
                  connection = JDBCUtils.getConnection();
                  preparedStatement = connection.prepareStatement(sql1);
                  preparedStatement.executeUpdate(); // 执行第1条sql语句
                  int a = 1 / 0; // 抛出异常,try块中下面的代码不再执行
                  preparedStatement = connection.prepareStatement(sql2);
                  preparedStatement.executeUpdate(); // 执行第2条sql语句
              } catch (SQLException e) {
                  e.printStackTrace();
              } finally {
                  JDBCUtils.close(null, preparedStatement, connection);
              }
          }
      
          @Test
          public void useTransaction() {
              // 1.得到连接
              Connection connection = null;
              // 2.编写sql
              String sql1 = "update account set balance = balance - 1000 where id = 1";
              String sql2 = "update account set balance = balance + 1000 where id = 2";
              PreparedStatement preparedStatement = null;
              try {
                  connection = JDBCUtils.getConnection();
                  connection.setAutoCommit(false); // 设置不自动提交事务,相当于开启了事务
                  preparedStatement = connection.prepareStatement(sql1);
                  preparedStatement.executeUpdate(); // 执行第1条sql语句
                  int a = 1 / 0; // 抛出异常,try块中下面的代码不再执行
                  preparedStatement = connection.prepareStatement(sql2);
                  preparedStatement.executeUpdate(); // 执行第2条sql语句
                  // 如果没有抛出异常,则提交事务
                  connection.commit();
              } catch (SQLException e) {
                  // 当捕获了异常,这里可以回滚事务,即撤销执行的sql语句
                  try {
                      // 如果rollback()不传入SavePoint保存点,则默认回滚到开启事务的状态
                      System.out.println("执行发生了异常,开始回滚事务");
                      connection.rollback();
                      System.out.println("已撤销执行的sql语句,事务回滚完毕");
                  } catch (SQLException ex) {
                      ex.printStackTrace();
                  }
                  e.printStackTrace();
              } finally {
                  JDBCUtils.close(null, preparedStatement, connection);
              }
          }
      }
      

      4.6、批处理

      基本介绍:

      1. 当需要成批插入或更新记录时,可以采用Java的批量更新机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下单独提交处理更有效率。
      2. JDBC的批量处理语句常用方法:
        • addBatch(): 添加需要批量处理的SQL语句或参数
        • executeBatch(): 执行批量处理的语句
        • clearBatch(): 清空批处理包的语句
      3. JDBC连接MySQL时,如果要使用批处理功能,必须要在url中加入参数?rewriteBatchedStatements=true
      4. 批处理往往和PreparedStatement一起搭配使用,既可以减少编译次数,有减少运行次数,效率提高。
      package com.jdbc.batch_;
      
      import com.jdbc.utils.JDBCUtils;
      import org.junit.jupiter.api.Test;
      
      import java.sql.Connection;
      import java.sql.PreparedStatement;
      import java.sql.SQLException;
      
      public class TestBatch {
          @Test
          public void prepared() throws SQLException {
              Connection connection = JDBCUtils.getConnection();
              String sql = "create table batch(id int, name varchar(25), password varchar(25))";
              PreparedStatement preparedStatement = connection.prepareStatement(sql);
              boolean isSuccess = preparedStatement.execute();
              System.out.println(isSuccess);
              JDBCUtils.close(null,preparedStatement,connection);
          }
      
          @Test
          public void noBatch() throws SQLException {
              Connection connection = JDBCUtils.getConnection();
              String sql = "insert into batch(id, name, password) values (?,?,?)";
              PreparedStatement preparedStatement = connection.prepareStatement(sql);
              System.out.println("开始执行...");
              long start = System.currentTimeMillis();
              for (int i = 1; i <= 5000; i++) {
                  preparedStatement.setInt(1,i);
                  preparedStatement.setString(2,"test" + i);
                  preparedStatement.setString(3,"pwd" + i);
                  preparedStatement.executeUpdate();
              }
              long end = System.currentTimeMillis();
              System.out.println("传统方式耗时:" + (end - start)); // 传统方式耗时:6626
              JDBCUtils.close(null,preparedStatement,connection);
          }
      
          @Test
          public void useBatch() throws SQLException {
              Connection connection = JDBCUtils.getConnection();
              String sql = "insert into batch(id, name, password) values (?,?,?)";
              PreparedStatement preparedStatement = connection.prepareStatement(sql);
              System.out.println("开始执行...");
              long start = System.currentTimeMillis();
              for (int i = 1; i <= 5000; i++) {
                  preparedStatement.setInt(1,i);
                  preparedStatement.setString(2,"test" + i);
                  preparedStatement.setString(3,"pwd" + i);
                  // 将sql语句加入到批处理包中
                  preparedStatement.addBatch();
                  if (i % 1000 == 0) {    // 当批处理包中有1000条sql语句时就执行
                      preparedStatement.executeBatch();
                      // 批量执行后清空批处理包
                      preparedStatement.clearBatch();
                  }
              }
              long end = System.currentTimeMillis();
              System.out.println("批处理方式耗时:" + (end - start)); // 批处理方式耗时:116
              JDBCUtils.close(null,preparedStatement,connection);
          }
      }
      

      mysql.properties配置文件

      user=root
      password=123456
      url=jdbc:mysql://localhost:3380/jdbc_test?rewriteBatchedStatements=true
      driver=com.mysql.cj.jdbc.Driver
      

      addBatch()源码分析

      public void addBatch(Object batch) {
              if (this.batchedArgs == null) { // 当第1次添加sql语句时,创建ArrayList用于存放sql语句
                  this.batchedArgs = new ArrayList();
              }
              this.batchedArgs.add(batch);
          }
      
      public boolean add(E e) { // 每增加1条sql语句就size+1
              ensureCapacityInternal(size + 1);  // Increments modCount!!
              elementData[size++] = e;
              return true;
          }
      
      public void addBatch() throws SQLException {
              try {
                  synchronized(this.checkClosed().getConnectionMutex()) {
                      QueryBindings queryBindings = ((PreparedQuery)this.query).getQueryBindings();
                      queryBindings.checkAllParametersSet();
                      this.query.addBatch(queryBindings.clone());
                  }
              } catch (CJException var6) {
                  throw SQLExceptionsMapping.translateException(var6, this.getExceptionInterceptor());
              }
      	}
      

      第一次添加sql语句时,ArrayList初始容量为10,当容量满时,则按原大小进行1.5倍扩容

      addBatch源码分析1

      elementData => Object[] 就会存放预处理的sql语句,初始大小为10

      addBatch源码分析2

      当原数组大小为10已经存满时,ArrayList按1.5倍扩容到15

      addBatch源码分析3

      4.7、连接池

      传统获取Connection问题分析:

      1. 传统的JDBC数据库连接使用DriverManager来获取,每次向数据库建立连接的时候都要将Connection加载到内存中,再验证IP地址,用户名和密码(0.05s~1s的时间)。需要数据库连接的时候,就向数据库要求一个,频繁的进行数据库连接操作将占用很多的系统资源,容易造成服务器崩溃。
      2. 每一次数据库连接,使用完后都得断开(释放资源),如果程序出现异常而未能关闭,将导致数据库内存泄漏,最终将导致重启数据库。
      3. 传统获取连接的方式,不能控制创建的的连接数量,如连接过多,也可能导致内存泄漏,MySQL崩溃。
      4. 要想解决传统开发中的数据库连接问题 -> 采用数据库连接池技术

      数据库连接池

      基本介绍:

      1. 预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需要从“缓冲池”中取出一个,使用完毕之后再放回去。
      2. 数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。
      3. 当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。

      数据库连接池原理

      数据库连接池原理2

      数据库连接池种类:

      1. JDBC的数据库连接池使用javax.sql.DataSource(本质上是一个接口)来表示,DataSource只是一个接口,该接口通常由第三方提供实现(提供相应的.jar包)。
      2. C3P0数据库连接池,速度相对较慢,稳定性好(Hibernate, Spring使用)
      3. DBCP数据库连接池,速度相对C3P0较快,但不稳定。
      4. Proxool数据库连接池,有监控连接池状态的功能,稳定性较C3P0差一点。
      5. BoneCP数据库连接池,速度快。
      6. Druid(德鲁伊)是阿里提供的数据库连接池,集DBCP、C3P0、Proxool优点于一身的数据库连接池(最常用,推荐)。

      注意:在数据库连接池技术中,close()方法不是真正的断掉数据库连接,而是把使用的Connection对象放回连接池 -> 断掉对象的引用

      C3P0数据库连接池

      ComboPooledDataSource类

      ComboPooledDataSource类继承关系图

      常用方法:

      • 构造器ComboPooledDataSource(String configName): 传入数据源名称
      • setDriverClass(String driver): 设置驱动类
      • setJdbcUrl(): 设置url
      • setUser(): 设置登录数据库的用户名
      • setPassword(): 设置登录数据库的用户密码
      • setInitialPoolSize(int size): 设置初始化连接数
      • setMaxPoolSize(int size): 设置最大连接数
      • getConnection(): 获取数据库连接,返回Connection对象
      package com.jdbc.datasource_;
      
      import com.jdbc.utils.JDBCUtils;
      import com.mchange.v2.c3p0.ComboPooledDataSource;
      import org.junit.jupiter.api.Test;
      
      import java.io.FileInputStream;
      import java.sql.Connection;
      import java.sql.SQLException;
      import java.util.Properties;
      
      public class C3P0_ {
          /**
           * 测试传统方式连接MySQL5000次
           */
          @Test
          public void testCon() throws SQLException {
              System.out.println("开始连接");
              long start = System.currentTimeMillis();
              for (int i = 0; i < 5000; i++) {
                  Connection connection = JDBCUtils.getConnection();
                  // 对数据库的操作...
                  connection.close(); // 关闭数据库
              }
              long end = System.currentTimeMillis();
              System.out.println("传统JDBC方式 连接MySQL5000次 耗时: " + (end - start));
              // 传统JDBC方式 连接MySQL5000次 耗时: 31287
          }
      
          /**
           * 方式1:相关参数,在程序中指定user,url,password等
           */
          @Test
          public void testC3P0_01() throws Exception {
              // 1. 创建一个数据源对象
              ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
              // 2. 通过配置文件获取相关的连接信息
              Properties properties = new Properties();
              properties.load(new FileInputStream("src\\mysql.properties"));
              // 从配置文件中读取相关属性值
              String user = properties.getProperty("user");
              String password = properties.getProperty("password");
              String driver = properties.getProperty("driver");
              String url = properties.getProperty("url");
      
              // 3. 给数据源 comboPooledDataSource 设置相关信息
              // 注意:连接池的连接的管理是由comboPooledDataSource 来管理
              comboPooledDataSource.setDriverClass(driver);
              comboPooledDataSource.setJdbcUrl(url);
              comboPooledDataSource.setUser(user);
              comboPooledDataSource.setPassword(password);
              // 4. 设置初始化连接数
              comboPooledDataSource.setInitialPoolSize(10);
              // 5. 最大连接数
              comboPooledDataSource.setMaxPoolSize(50);
              long start = System.currentTimeMillis();
              for (int i = 0; i < 5000; i++) {
                  Connection connection = comboPooledDataSource.getConnection(); // 这个方法就是从DataSource接口实现的
                  //System.out.println("连接成功");
                  connection.close();
              }
              long end = System.currentTimeMillis();
              System.out.println("C3P0方式1 连接MySQL5000次 耗时: " + (end - start));
              // C3P0方式1 连接MySQL5000次 耗时: 775
          }
      
          /**
           * 方式2:使用配置文件模板来完成
           */
          @Test
          public void testC3P0_02() throws Exception {
              //  ComboPooledDataSource(String configName) 传入数据源名称
              ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource("yxz_mysql");
              // 测试5000次连接mysql
              long start = System.currentTimeMillis();
              for (int i = 0; i < 5000; i++) {
                  Connection connection = comboPooledDataSource.getConnection();
                  //System.out.println("连接成功");
                  connection.close();
              }
              long end = System.currentTimeMillis();
              System.out.println("C3P0方式2 连接MySQL5000次 耗时: " + (end - start));
              // C3P0方式2 连接MySQL5000次 耗时: 856
          }
      }
      

      c3p0-config.xml:C3P0的配置文件

      <c3p0-config>
          <!--默认配置,如果不指定数据源名称,则按默认配置来-->
          <default-config>
              <!-- driverClass:驱动类 -->
              <property name="driverClass">com.mysql.cj.jdbc.Driver</property>
      
              <!-- jdbcUrl:url -->
              <property name="jdbcUrl">jdbc:mysql://localhost:3306/database?characterEncoding=UTF-8</property>
      
              <!-- user:用户名 -->
              <property name="user">root</property>
      
              <!-- password:密码 -->
              <property name="password">123456</property>
      
              <!-- initialPoolSize:初始化时获取三个连接,取值应在minPoolSize与maxPoolSize之间。 -->
              <property name="initialPoolSize">10</property>
      
              <!-- maxIdleTime:最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。-->
              <property name="maxIdleTime">60</property>
      
              <!-- acquireIncrement:每次增长的连接数 -->
              <property name="acquireIncrement">5</property>
      
              <!-- maxPoolSize:连接池中保留的最大连接数 -->
              <property name="maxPoolSize">100</property>
      
              <!-- minPoolSize: 连接池中保留的最小连接数 -->
              <property name="minPoolSize">5</property>
          </default-config>
      
          <!--配置连接池mysql-->
          <!-- named-config 表示数据源名称 代表连接池-->
          <named-config name="yxz_mysql">
              <property name="driverClass">com.mysql.cj.jdbc.Driver</property>
              <property name="jdbcUrl">jdbc:mysql://localhost:3380/jdbc_test?characterEncoding=UTF-8</property>
              <property name="user">root</property>
              <property name="password">123456</property>
              <property name="initialPoolSize">10</property>
              <property name="maxIdleTime">30</property>
              <property name="maxPoolSize">100</property>
              <property name="minPoolSize">10</property>
          </named-config>
      
          <!--配置连接池2,可以配置多个-->
      </c3p0-config>
      

      使用C3P0注意:

      1. 需要导入C3P0的jar包
      2. C3P0的配置文件必须名为c3p0-config.xml,会自动查找该文件,
      3. C3P0配置文件应放在项目的src目录下。

      Druid数据库连接池

      package com.jdbc.datasource_;
      
      import com.alibaba.druid.pool.DruidDataSourceFactory;
      import org.junit.jupiter.api.Test;
      
      import javax.sql.DataSource;
      import java.io.FileInputStream;
      import java.sql.Connection;
      import java.util.Properties;
      
      public class Druid_ {
          @Test
          public void testDruid_() throws Exception {
              // 1.加入druid jar包
              // 2.加入配置文件druid.properties,放在项目的src目录下
              // 3.创建Properties对象,读取配置文件
              Properties properties = new Properties();
              properties.load(new FileInputStream("src\\druid.properties"));
              // 4.创建一个指定参数的Druid数据库连接池
              DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
              System.out.println("开始连接");
              long start = System.currentTimeMillis();
              for (int i = 0; i < 500000; i++) {
                  // 5.得到连接
                  Connection connection = dataSource.getConnection();
                  // 6.关闭连接
                  connection.close();
              }
              long end = System.currentTimeMillis();
              System.out.println("Druid 连接MySQL500000次 耗时: " + (end - start));
              // // Druid 连接MySQL500000次 耗时: 1265
          }
      }
      

      druid.properties:Druid的配置文件

      driverClassName=com.mysql.cj.jdbc.Driver
      url=jdbc:mysql://localhost:3380/jdbc_test?rewriteBatchedStatements=true
      characterEncoding=utf-8
      username=root
      password=123456
      initialSize=5
      minIdle=5
      maxActive=50
      maxWait=3000
      validationQuery=SELECT 1
      testWhileIdle=true
      

      JDBCUtilsByDruid.java:Druid的工具类

      package com.jdbc.utils;
      
      import com.alibaba.druid.pool.DruidDataSourceFactory;
      
      import javax.sql.DataSource;
      import java.io.FileInputStream;
      import java.sql.Connection;
      import java.sql.ResultSet;
      import java.sql.SQLException;
      import java.sql.Statement;
      import java.util.Properties;
      
      public class JDBCUtilsByDruid {
          private static DataSource dataSource;
      
          /*
            初始化数据源
           */
          static {
              Properties properties = new Properties();
              try {
                  properties.load(new FileInputStream("src\\druid.properties"));
                  dataSource = DruidDataSourceFactory.createDataSource(properties);
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
      
          /**
           * 获取数据库连接对象的方法
           *
           * @return 返回数据库连接对象
           * @throws SQLException sql异常
           */
          public static Connection getConnection() throws SQLException {
              return dataSource.getConnection();
          }
      
          /**
           * 释放资源的方法
           *
           * @param resultSet  sql查询的结果集
           * @param statement  预处理和Statement对象
           * @param connection 数据库连接
           */
          public static void close(ResultSet resultSet, Statement statement, Connection connection) {
              try {
                  if (resultSet != null) {
                      resultSet.close();
                  }
                  if (statement != null) {
                      statement.close();
                  }
                  if (connection != null) {
                      connection.close();
                  }
              } catch (SQLException e) {
                  throw new RuntimeException(e);
              }
          }
      }
      

      测试Druid工具类

      package com.jdbc.utils;
      
      import org.junit.jupiter.api.Test;
      
      import java.sql.*;
      
      @SuppressWarnings({"all"})
      public class JDBCUtilsByDruid_Use {
          @Test
          public void testDruidUtils() {
              // 1.得到连接
              Connection connection = null;
              // 2.编写sql
              String sql = "select * from goddess";
              PreparedStatement preparedStatement = null;
              ResultSet resultSet = null;
              try {
                  connection = JDBCUtilsByDruid.getConnection();
                  System.out.println(connection.getClass());
                  // 运行类型:com.alibaba.druid.pool.DruidPooledConnection
                  preparedStatement = connection.prepareStatement(sql);
                  // 执行
                  resultSet = preparedStatement.executeQuery();
                  while (resultSet.next()) {
                      int id = resultSet.getInt("id");
                      String name = resultSet.getString("name");
                      String sex = resultSet.getString("sex");
                      Date borndate = resultSet.getDate("borndate");
                      String phone = resultSet.getString("phone");
                      System.out.println("id:\t" + id + "\tname:" + name + "\tsex:" + sex
                              + "\tborndate:" + borndate + "\tphone:" + phone);
                  }
              } catch (SQLException e) {
                  e.printStackTrace();
              } finally {
                  JDBCUtilsByDruid.close(resultSet, preparedStatement, connection);
              }
          }
      }
      

      使用Druid数据库连接池的注意事项:

      1. Druid配置文件druid.properties,该文件需要放在项目的src目录下
      2. 得到Druid数据库连接池对象使用

      面试问题:为什么Druid和C3P0和传统的JDBC都有close()方法,它们都是关闭数据库连接吗?

      答:不是。Druid和C3P0都实现了Connection接口,所以它们都有close方法,但是close()方法具体的实现不同。Druid和C3P0只是断开了数据库连接池的连接对象的引用,把使用过的Connection对象放回连接池以供其他程序使用,而数据库连接并没有断开。

      Druid中有关close()方法的源码如下:

      public class DruidPooledConnection extends PoolableWrapper implements PooledConnection, Connection {
      	public void close() throws SQLException {
              if (!this.disable) {
                  DruidConnectionHolder holder = this.holder;
                  if (holder == null) {
                      if (this.dupCloseLogEnable) {
                          LOG.error("dup close");
                      }
      
                  } else {
                      DruidAbstractDataSource dataSource = holder.getDataSource();
                      boolean isSameThread = this.getOwnerThread() == Thread.currentThread();
                      if (!isSameThread) {
                          dataSource.setAsyncCloseConnectionEnable(true);
                      }
      
                      if (dataSource.isAsyncCloseConnectionEnable()) {
                          this.syncClose();
                      } else if (CLOSING_UPDATER.compareAndSet(this, 0, 1)) {
                          try {
                              Iterator var4 = holder.getConnectionEventListeners().iterator();
      
                              while(true) {
                                  if (!var4.hasNext()) {
                                      List<Filter> filters = dataSource.getProxyFilters();
                                      if (filters.size() > 0) {
                                          FilterChainImpl filterChain = new FilterChainImpl(dataSource);
                                          filterChain.dataSource_recycle(this);
                                      } else {
                                          this.recycle();
                                      }
                                      break;
                                  }
      
                                  ConnectionEventListener listener = (ConnectionEventListener)var4.next();
                                  listener.connectionClosed(new ConnectionEvent(this));
                              }
                          } finally {
                              CLOSING_UPDATER.set(this, 0);
                          }
      
                          this.disable = true;
                      }
                  }
              }
          }
      }
      

      4.8、Apache—DBUtils

      问题引入:

      1. select语句查询返回的ResultSet结果集是和Connection关联的 -> 一旦close()后,无法再对ResultSet操作。
      2. 结果集不利于数据的管理 -> 只能使用一次,close后无法再使用。
      3. 使用返回的信息也不方便 -> 只能用getXxx()的方法并传入列名才能获取到值,且方法名和类型有关,做不到见名知意。

      解决方案(核心思想):

      ApDBUtils的核心思想

      案例:查询goddess表中所有记录

      Goddess.java

      package com.jdbc.datasource_;
      
      import java.util.Date;
      
      public class Goddess { // JavaBean,POJO,Domain对象
          private Integer id;
          private String name;
          private String sex;
          private Date borndate;
          private String phone;
      
          public Goddess() { // 必须声明一个无参构造器,反射需要
          }
      
          public Goddess(Integer id, String name, String sex, Date borndate, String phone) {
              this.id = id;
              this.name = name;
              this.sex = sex;
              this.borndate = borndate;
              this.phone = phone;
          }
      
          public Integer getId() {
              return id;
          }
      
          public void setId(Integer id) {
              this.id = id;
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          public String getSex() {
              return sex;
          }
      
          public void setSex(String sex) {
              this.sex = sex;
          }
      
          public Date getBorndate() {
              return borndate;
          }
      
          public void setBorndate(Date borndate) {
              this.borndate = borndate;
          }
      
          public String getPhone() {
              return phone;
          }
      
          public void setPhone(String phone) {
              this.phone = phone;
          }
      
          @Override
          public String toString() {
              return "\nGoddess{" +
                      "id=" + id +
                      ", name='" + name + '\'' +
                      ", sex='" + sex + '\'' +
                      ", borndate=" + borndate +
                      ", phone='" + phone + '\'' +
                      '}';
          }
      }
      

      JDBCUtilsByDruid_Use.java测试类

      package com.jdbc.datasource_;
      
      import com.jdbc.utils.JDBCUtilsByDruid;
      import org.junit.jupiter.api.Test;
      
      import java.sql.*;
      import java.util.ArrayList;
      
      @SuppressWarnings({"all"})
      public class JDBCUtilsByDruid_Use {
          /**
           * 使用传统方法将查询到的resultSet记录封装到Goddess对象,放入在ArrayList集合
           */
          @Test
          public void testSelectToArrayList() {
              // 1.得到连接
              Connection connection = null;
              // 2.编写sql
              String sql = "select * from goddess";
              PreparedStatement preparedStatement = null;
              ResultSet resultSet = null;
              ArrayList<Goddess> goddesses = new ArrayList<>(); // 创建ArrayList,存放Goddess对象
              try {
                  connection = JDBCUtilsByDruid.getConnection();
                  preparedStatement = connection.prepareStatement(sql);
                  // 执行
                  resultSet = preparedStatement.executeQuery();
                  while (resultSet.next()) {
                      int id = resultSet.getInt("id");
                      String name = resultSet.getString("name");
                      String sex = resultSet.getString("sex");
                      Date borndate = resultSet.getDate("borndate");
                      String phone = resultSet.getString("phone");
                      goddesses.add(new Goddess(id, name, sex, borndate, phone));
                  }
      
                  for (Goddess goddess : goddesses) {
                      System.out.println("name:" + goddess.getName() + "\tborndate:" + goddess.getBorndate());
                  }
              } catch (SQLException e) {
                  e.printStackTrace();
              } finally {
                  JDBCUtilsByDruid.close(resultSet, preparedStatement, connection);
              }
              // 因为ArrayList集合和connection没有任何关联,所以可以在connection关闭后访问ArrayList
              System.out.println("list集合数据:" + goddesses);
          }
      }
      

      Apache-DBUtils类

      基本介绍:

      1. commons-dbutils 是Apache组织提供的一个开源JDBC工具类库,它是对JDBC的封装,使用dbutils能极大简化jdbc编码的工作量。
      2. QueryRunner类:该类封装了SQL的执行,是线程安全的。可以实现增、删、改、查、批处理。使用QueryRunner类实现查询操作。
      3. ResultSetHandler接口:该接口用于处理java.sql.ResultSet,将数据按要求转换为另一种形式。
      • ArrayHandler:把结果集中的第一行数据转成对象数组。
      • ArrayListHandler:把结果集中的每一行数据都转成一个数组,再存放到List中。
      • BeanHandler:将结果集中的第一行数据封装到一个对应的JavaBean实例中
      • BeanListHandler:将结果集中的每一行数据都封装到一个对应的JavaBean实例中,存放到List里。
      • ColumnListHandler:将结果集中某一列的数据存放到List中。
      • KeyedHandler(name):将结果集中的每行数据都封装到Map里,再把这些map再存到一个map里,其key为指定的key。
      • MapHandler:将结果集中的每一行数据封装到一个Map里,key是列名,value是对应的值。
      • MapListHandler:将结果集中的每一行数据都封装到一个Map里,然后再存放到List。
      package com.jdbc.datasource_;
      
      import com.jdbc.utils.JDBCUtilsByDruid;
      import org.apache.commons.dbutils.QueryRunner;
      import org.apache.commons.dbutils.handlers.BeanHandler;
      import org.apache.commons.dbutils.handlers.BeanListHandler;
      import org.apache.commons.dbutils.handlers.ScalarHandler;
      import org.junit.jupiter.api.Test;
      
      import java.sql.Connection;
      import java.sql.SQLException;
      import java.util.List;
      
      @SuppressWarnings({"all"})
      public class DBUtils_Use {
      
          /**
           * 使用Apache-DBUtils工具类 + Druid数据库连接池完成对goddess表的crud操作
           */
          @Test
          public void testQueryMany() throws SQLException {
              // 1.得到连接
              Connection connection = JDBCUtilsByDruid.getConnection();
              // 2.使用DBUtils类和接口,先导入DBUtils和相关jar包:commons-dbutils-1.7.jar,加入到本Project
              // 3.创建QueryRunner
              QueryRunner queryRunner = new QueryRunner();
              // 4.执行相关方法,返回ArrayList结果集
              String sql = "select * from goddess where id >= ?";
              /*
                  query方法解读:
                  1.query方法就是执行sql语句,得到resultSet --封装到--> ArrayList集合中
                  2.返回集合List
                  3.connection:连接
                  4.sql:要执行的sql语句
                  5.new BeanListHandler<>(Goddess.class):将resultSet --封装成--> Goddess对象 --> 添加到ArrayList中
                    底层使用反射机制,获取Goddess类属性进行封装
                  6. 1:给sql中的占位符?赋值,可以有多个值,因为最后的参数是可变参数Object... params
                  7.底层得到的resultSet会由query()方法关闭,底层创建的preparedStatement也会关闭
               */
              /**
               * 对queryRunner.query()方法源码分析:
               * private <T> T query(Connection conn, boolean closeConn, String sql, ResultSetHandler<T> rsh, Object... params) throws SQLException {
               *         if (conn == null) {
               *             throw new SQLException("Null connection");
               *         } else if (sql == null) {
               *             if (closeConn) {
               *                 this.close(conn);
               *             }
               *
               *             throw new SQLException("Null SQL statement");
               *         } else if (rsh == null) {
               *             if (closeConn) {
               *                 this.close(conn);
               *             }
               *
               *             throw new SQLException("Null ResultSetHandler");
               *         } else {
               *             PreparedStatement stmt = null;
               *             ResultSet rs = null;
               *             Object result = null;
               *
               *             try {
               *                 stmt = this.prepareStatement(conn, sql); // 创建preparedStatement
               *                 this.fillStatement(stmt, params); // 对sql语句中的?赋值
               *                 rs = this.wrap(stmt.executeQuery()); // 执行sql语句返回resuletSet
               *                 result = rsh.handle(rs); // 返回的resuletSet -> ArrayList[使用反射机制,对传入的class对象处理]
               *             } catch (SQLException var33) {
               *                 this.rethrow(var33, sql, params);
               *             } finally {
               *                 try {
               *                     this.close(rs); // 关闭resuletSet
               *                 } finally {
               *                     this.close(stmt); // 关闭preparedStatement
               *                     if (closeConn) {
               *                         this.close(conn); // 关闭数据库连接
               *                     }
               *
               *                 }
               *             }
               *
               *             return result; // 返回ArrayList
               *         }
               *     }
               */
              List<Goddess> list =
                      queryRunner.query(connection, sql, new BeanListHandler<>(Goddess.class), 1);
              for (Goddess goddess : list) {
                  System.out.print(goddess);
              }
      
              // 5.释放资源
              JDBCUtilsByDruid.close(null, null, connection);
          }
      
          /**
           * 查询单行记录多列的情况,传入BeanHandler对象,query方法返回Bean对象,如果没有记录则返回null
           *
           * @throws SQLException
           */
          @Test
          public void testQuerySingle() throws SQLException {
              Connection connection = JDBCUtilsByDruid.getConnection();
              QueryRunner queryRunner = new QueryRunner();
              String sql = "select * from goddess where id = ?";
              Goddess goddess = queryRunner.query(connection, sql, new BeanHandler<>(Goddess.class), 1);
              System.out.println(goddess);
              JDBCUtilsByDruid.close(null, null, connection);
          }
      
          /**
           * 查询单行单列的情况,使用ScalarHandler,query方法返回Object对象,如果没有记录则返回null
           *
           * @throws SQLException
           */
          @Test
          public void testScalar() throws SQLException {
              Connection connection = JDBCUtilsByDruid.getConnection();
              QueryRunner queryRunner = new QueryRunner();
              String sql = "select name from goddess where id = ?";
              // 查询单行记录的某一列值,使用ScalarHandler
              Object name = queryRunner.query(connection, sql, new ScalarHandler(), 1);
              System.out.println(name);
              JDBCUtilsByDruid.close(null, null, connection);
          }
      
          /**
           * queryRunner.update方法测试DML操作
           *
           * @throws SQLException
           */
          @Test
          public void testDML() throws SQLException {
              Connection connection = JDBCUtilsByDruid.getConnection();
              QueryRunner queryRunner = new QueryRunner();
              String sql = "insert into users values (?,?,?)";
              // update()方法 可以执行增删改的操作,返回值表示受影响的行数
              int affectedRows = queryRunner.update(connection, sql, 2, "yxz", "66666");
              System.out.println(affectedRows > 0 ? "执行成功" : "操作对数据库无影响");
              JDBCUtilsByDruid.close(null, null, connection);
          }
      }
      

      在自定义Bean类时的注意事项:

      Bean类中的属性名称必须和数据库表中的字段名保持一致!!!(因为反射根据Bean加载类信息需要)

      Bean类中必须为所有属性设置get、set方法,反射需要

      Bean类中属性的类型全部使用包装类,不要使用基本数据类型,因为MySQL中所有数据类型都可能为null,而Java中只有引用类型才会有null值

      数据库表中char类型的字段,在Bean类中对应属性使用String类型

      数据库表中date类型的字段,在Bean类中对应属性使用java.util.Date类型

      Bean类的属性名大小写不用管,因为源码中使用了equalsIgnoreCase()方法进行比较

      表字段和JavaBean的类型映射关系

      4.9、DAO增删改查—BasicDao

      使用Apache-DBUtils + Druid虽然简化了JDBC开发,但还有不足:

      1. SQL语句是固定的,不能通过参数传入,通用性不好,需要改进 -> 更方便执行增删改查。
      2. 对于select操作,如果有返回值,返回类型不确定,需要使用泛型。
      3. 数据表一旦变多,业务复杂,不可能只靠一个Java程序完成。
      4. 关于BasicDAO的思想示意图:

      DAO的思想

      简单设计:com.jdbc.dao_

      1. com.jdbc.dao_.utils工具类

        • JDBCUtilsByDruid
        package com.jdbc.dao_.utils;
        
        import com.alibaba.druid.pool.DruidDataSourceFactory;
        
        import javax.sql.DataSource;
        import java.io.FileInputStream;
        import java.sql.Connection;
        import java.sql.ResultSet;
        import java.sql.SQLException;
        import java.sql.Statement;
        import java.util.Properties;
        
        public class JDBCUtilsByDruid {
            private static DataSource dataSource;
        
            /*
              初始化数据源
             */
            static {
                Properties properties = new Properties();
                try {
                    properties.load(new FileInputStream("src\\druid.properties"));
                    dataSource = DruidDataSourceFactory.createDataSource(properties);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        
            /**
             * 获取数据库连接对象的方法
             *
             * @return 返回数据库连接对象
             * @throws SQLException sql异常
             */
            public static Connection getConnection() throws SQLException {
                return dataSource.getConnection();
            }
        
            /**
             * 释放资源的方法
             *
             * @param resultSet  sql查询的结果集
             * @param statement  预处理和Statement对象
             * @param connection 数据库连接
             */
            public static void close(ResultSet resultSet, Statement statement, Connection connection) {
                try {
                    if (resultSet != null) {
                        resultSet.close();
                    }
                    if (statement != null) {
                        statement.close();
                    }
                    if (connection != null) {
                        connection.close();
                    }
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        
      2. com.jdbc.dao_.domainJavaBean类

        • Goddess类
        package com.jdbc.dao_.domain;
        
        import java.util.Date;
        
        public class Goddess { // JavaBean,POJO,Domain对象
            private Integer id;
            private String name;
            private String sex;
            private Date borndate;
            private String phone;
        
            public Goddess() { // 必须声明一个无参构造器,反射需要
            }
        
            public Goddess(Integer id, String name, String sex, Date borndate, String phone) {
                this.id = id;
                this.name = name;
                this.sex = sex;
                this.borndate = borndate;
                this.phone = phone;
            }
        
            public Integer getId() {
                return id;
            }
        
            public void setId(Integer id) {
                this.id = id;
            }
        
            public String getName() {
                return name;
            }
        
            public void setName(String name) {
                this.name = name;
            }
        
            public String getSex() {
                return sex;
            }
        
            public void setSex(String sex) {
                this.sex = sex;
            }
        
            public Date getBorndate() {
                return borndate;
            }
        
            public void setBorndate(Date borndate) {
                this.borndate = borndate;
            }
        
            public String getPhone() {
                return phone;
            }
        
            public void setPhone(String phone) {
                this.phone = phone;
            }
        
            @Override
            public String toString() {
                return "Goddess{" +
                        "id=" + id +
                        ", name='" + name + '\'' +
                        ", sex='" + sex + '\'' +
                        ", borndate=" + borndate +
                        ", phone='" + phone + '\'' +
                        '}';
            }
        }
        
      3. com.jdbc.dao_.daoDAO类

        • BasicDAO类
        package com.jdbc.dao_.dao;
        
        import com.jdbc.dao_.utils.JDBCUtilsByDruid;
        import org.apache.commons.dbutils.QueryRunner;
        import org.apache.commons.dbutils.handlers.BeanHandler;
        import org.apache.commons.dbutils.handlers.BeanListHandler;
        import org.apache.commons.dbutils.handlers.ScalarHandler;
        
        import java.sql.Connection;
        import java.sql.SQLException;
        import java.util.List;
        
        @SuppressWarnings({"all"})
        public class BasicDAO<T> { // 泛型指定其他类型
            private QueryRunner queryRunner = new QueryRunner();
        
            /**
             * 通用的update方法,针对任意的表,执行增删改的操作
             *
             * @param sql        要执行的sql语句
             * @param parameters sql语句中的?的值
             * @return 返回执行sql语句后,数据库中受影响的行数
             */
            public int update(String sql, Object... parameters) {
                Connection connection = null;
                try {
                    connection = JDBCUtilsByDruid.getConnection();
                    return queryRunner.update(connection, sql, parameters);
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                } finally {
                    JDBCUtilsByDruid.close(null, null, connection);
                }
            }
        
            /**
             * 查询多行结果的通用方法
             *
             * @param sql        要执行的select语句
             * @param cls        传入一个类的Class对象,如Goddess.class
             * @param parameters 可变参数,传入?的具体值,可以是多个
             * @return 根据cls.class对象,返回对应的ArrayList集合
             */
            public List<T> queryMulti(String sql, Class<T> cls, Object... parameters) {
                Connection connection = null;
                try {
                    connection = JDBCUtilsByDruid.getConnection();
                    return queryRunner.query(connection, sql, new BeanListHandler<T>(cls), parameters);
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                } finally {
                    JDBCUtilsByDruid.close(null, null, connection);
                }
            }
        
            /**
             * 查询单行结果的通用方法
             *
             * @param sql        要执行的select语句
             * @param cls        传入一个类的Class对象,如Goddess.class
             * @param parameters 可变参数,传入?的具体值,可以是多个
             * @return 根据cls.class对象,返回对应的T对象
             */
            public T querySingle(String sql, Class<T> cls, Object... parameters) {
                Connection connection = null;
                try {
                    connection = JDBCUtilsByDruid.getConnection();
                    return queryRunner.query(connection, sql, new BeanHandler<T>(cls), parameters);
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                } finally {
                    JDBCUtilsByDruid.close(null, null, connection);
                }
            }
        
            /**
             * 查询单行单列结果的通用方法
             *
             * @param sql        要执行的select语句
             * @param parameters 可变参数,传入?的具体值,可以是多个
             * @return 根据cls.class对象,返回Obejct对象
             */
            public Object queryScalar(String sql, Object... parameters) {
                Connection connection = null;
                try {
                    connection = JDBCUtilsByDruid.getConnection();
                    return queryRunner.query(connection, sql, new ScalarHandler(), parameters);
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                } finally {
                    JDBCUtilsByDruid.close(null, null, connection);
                }
            }
        }
        
        • GoddessDAO类
        package com.jdbc.dao_.dao;
        
        import com.jdbc.dao_.domain.Goddess;
        
        public class GoddessDAO extends BasicDAO<Goddess>{
            // 1.继承父类BasicDAO<Goddess>的方法
            // 2.根据业务需求,可以编写GoddessDAO的特有方法
        }
        
      4. com.jdbc.dao_.test测试类

        • TestDAO类
        package com.jdbc.dao_.test;
        
        import com.jdbc.dao_.dao.GoddessDAO;
        import com.jdbc.dao_.domain.Goddess;
        import org.junit.jupiter.api.Test;
        
        import java.util.List;
        
        public class TestDAO {
            /**
             * 测试GoddessDAO 对 goddess表的操作
             */
            @Test
            public void testGoddessDAO() {
                GoddessDAO goddessDAO = new GoddessDAO();
                // 1.查询多行记录
                List<Goddess> list = goddessDAO.queryMulti("select * from goddess where id < ?", Goddess.class, 20);
                for (Goddess goddess : list) {
                    System.out.println(goddess);
                }
        
                // 2.查询单行记录
                Goddess goddess = goddessDAO.querySingle("select * from goddess where id = ?", Goddess.class, 1);
                System.out.println("============================单行记录查询结果===============================");
                System.out.println(goddess);
        
                // 3.查询单行单列
                Object name = goddessDAO.queryScalar("select name from goddess where id = ?", 1);
                System.out.println("==========================单行单列记录查询结果============================");
                System.out.println(name);
            }
        }
        

      5、正则表达式

      5.1、入门

      正则表达式(Regular Expression, regexp):对字符串(文本)执行模式匹配的技术。

      基本介绍:

      1. 一个正则表达式,就是用某种模式去匹配字符串的一个公式。
      2. 不是只有Java有正则表达式,很多语言都支持正则表达式进行字符串操作:JavaScript、PHP、Java、python等。

      源码剖析

      package regexp;
      
      import java.util.regex.Matcher;
      import java.util.regex.Pattern;
      
      @SuppressWarnings({"all"})
      public class RegTheory {
          public static void main(String[] args) {
              String content = "1991年4月,Java之父James Gosling带领绿色计划(Green Project)项目启动,定位于消费电子产品(机顶盒、冰箱、收音机)运行架构的Oak语言诞生,这也是Java的前身,但是市场反响一般。\n" +
                      "1995年5月23日,随着互联网浪潮在1995年兴起,Oak迅速蜕,Java语言诞生,在SunWorld大会上发布Java1.0,第一次提出Write Once,Run Anywhere的口号。\n" +
                      "1996年1月23日,JDK1.0发布,纯解释型的Java虚拟机(Sun Classic VM)、Applet、AWT等诞生。\n" +
                      "1996年4月,十个最主要的操作系统和计算机供应商声明将在其产品中嵌入Java技术,\n" +
                      "1996年5月底,Sun于美国旧金山举行了首届JavaOne大会,从此JavaOne成为全世界数百万Java开发者每年一度的技术盛会。\n" +
                      "1996年9月,已有大约8.3万个网页应用了Java技术来制作。\n" +
                      "1997年2月19日,Sun公司发布了JDK1.1,代表技术:JAR文件格式、JDBC、JavaBeans、RMI等,Java语法也进行了增强,内部类(Inner Class)和反射(Reflection)出现。\n" +
                      "1998年12月4日,JDK1.2发布,这是一个里程碑式的重要版本,工程代号为Playground(竞技场),这个版本代表性技术非常多,如EJB、JavaPlug-in、Swing、JavaIDL等,还有使用极为频繁的Collections体系工具类等,并且这个版本中Java虚拟机第一次内置了JIT(Just In Time)即时编译器,后续还发布了JDK1.2.1和JDK1.2.2两个小版本升级。在JDK1.2中曾经共存过ClassicVM、HotSpotVM、ExactVM三个虚拟机,其中HotSpot是作为附加程序提供。也是在这个版本中Sun开始拆分对应的Java产品线,Sun在这个版本中把Java技术体系拆分为三个方向:\n" +
                      "分别是面向桌面应用开发的J2SE(Java 2 Platform,Standard Edition)\n" +
                      "面向企业级开发的J2EE(Java 2 Platform,Enterprise Edition)\n" +
                      "面向手机等移动终端开发的J2ME(Java 2 Platform,Micro Edition)\n" +
                      "1999年4月27日,HotSpot虚拟机诞生,HotSpot最初由Longview Techno-logies公司研发,后来在1997年被Sun公司收购,后来它成为JDK 1.3及之后所有JDK版本的默认Java虚拟机。\n" +
                      "2000年5月8日,工程代号为Kestrel(美洲红隼)的JDK 1.3发布。JDK 1.3的改进主要体现在Java类库上(如数学运算和新的Timer API等),此外一直作为扩展服务的JNDI服务也开始作为一项平台级服务被提供,还有基于CORBA IIOP协议来实现通信段RMI也出现了。\n" +
                      "2001年5月17日,JDK1.3的修订版JDK1.3.1发布,工程代号Ladybird(瓢虫)。从JDK1.3开始,Sun公司维持着稳定的开发节奏,大概每个两年发布一个主要版本,期间发布的各个修订版本以昆虫作为工程代号。\n" +
                      "2002年2月13日,JDK 1.4发布,工程代号为Merlin(灰背隼)。JDK 1.4是标志着Java真正走向成熟的一个版本。Compaq、SAS、Fujitsu、Symbian、IBM等一众大公司参与功能规划,甚至实现自己的独立发行版本。哪怕在二十年后的今天一些主流功能也可以在JDK1.4上运行,这个版本的主要代表技术包含正则表达式、异常链、NIO、日志类、XML解析器和XSLT转换器等等。\n" +
                      "2002年9月16日,工程代号为Grasshopper(蚱蜢)的JDK1.4.1修订版发布。在这一年前后微软平台的.NET Framework发布,至此Java平台和.NET平台的竞争开始拉开了序幕。但似乎Java的开源策略更胜一筹,终于在2014 年 11 月 12 日,微软正式宣布了.NET Core 的开源。\n" +
                      "2003年6月26日,工程代号为Mantis(螳螂)的JDK1.4.2修订版发布。\n" +
                      "2004年9月30日,JDK 5发布,工程代号为Tiger(老虎),Sun公司从这个版本开始放弃JDK 1.x的命名方式,将产品版本号修改成了JDK x,JDK1.2以来Java语言在语法上的改动都不大,该版本在语法易用性上做出了非常大的改进,如自动装箱、泛型、动态注解、枚举、可变长参数、foreach等。此外改进了Java的内存模型(Java Memory Model,JMM)、提供了java.util.concurrent并发包(由Doug Lea大师带Java进入了并发时代)等,JDK 5是官方声明可以支持Windows 9x操作系统的最后一个版本。\n" +
                      "2006年11月13日,JavaOne大会上,Sun公司宣布计划要把Java开源,随后在一年多的时间内,陆续的将JDK各部分在GPL V2协议下公开源码,随后并建立了OpenJDK组织对这些源码进行独立管理,除了极少部分的产权代码,OpenJDK几乎拥有了SunJDK 7中的全部代码。\n" +
                      "2006年12月11日,JDK6发布,工程代号为Mustang(野马)。在这个版本中,Sun公司终结了从JDK 1.2开始已经有八年历史的J2EE、J2SE、J2ME的产品线命名方式,启用Java EE 6、Java SE 6、Java ME 6的新命名来代替。在JDK 6中提供了众多改进,如通过Mozilla JavaScript Rhino引擎提供初步动态语言支持,提供编译器注解处理器(Annotation Processor这也是Lombok的原理,通过注解生成模板代码)和微型HTTP服务器API,以及对虚拟机内部锁、同垃圾收集、类加载机制等方面进行了大量优化改动。在JDK 6发布以后由于代码的复杂化,Java开源、开发JavaFx、世界经济危机以及Oracle对Sun的收购提案等原因,Sun公司内忧外患自顾不暇,原本稳定的开发进度也受到了很大的影响,使得JDK 6的生命周期也持续了很久,一共发布了211个更新补丁,最终版本为Java SE 6 Update 211,于2018年10月18日发布。\n" +
                      "2009年2月19日,工程代号为Dolphin(海豚)的JDK 7发布,这是其第一个里程碑版本,按照规划,共有十个里程碑版本发布,最后一个里程碑版本发布与2010年9月9日,由于各种原因JDK 7没有按照原计划完成。JDK 7开发阶段Sun公司在技术竞争和商业竞争中都深陷泥潭,已经无力推动开发进展,为了尽快完成JDK 7的发布,因此裁掉了大部分原定的功能,对于原定的Lambdax项目、Jigsaw项目、动态语言支持、Gabage-First垃圾收集器、Coin项目只匆匆交付了处于Experimental状态的G1垃圾收集器(直到2012年4月的Update 4中才开始正式商用),其余项目延期到JDK 8中。Oracle从JDK 7开始进行接手,迅速展现出了其极具商业化的处世风格,面对Java中使用最广泛的Java SE免费产品线,定义了一套新的商业版产品Java SE Support ,此外JDK 7计划维护到2022年,已经面向付费用户发布了211个补丁,最新版本为JDK 7 Update 211。\n" +
                      "2009年4月20日,Oracle宣布正式以74亿美元的价格收SUN公司,一代巨头由此没落,Java商标正式划归Oracle所有,Java语言本身并不属于哪间公司所有,它由JCP组织进行管理。此外Oracle还收购了BEA公司,JavaEE服务器Weblogic就是该公司的产品。\n" +
                      "2011年7月28日,JDK7发布,做出的改进:提供新的G1收集器、加强对非Java语言的调用支持、可并行的类加载架构等。\n" +
                      "2014年3月18日,Oracle公司发布JDK 8,从此使用JEP(JDK Enhancement Proposals)来定义和管理纳入新版JDK发布范围的功能特性,JDK 8中实现了JDK7中定义并且未完成的功能,其中也有被延期到JDK 9的Jigsaw模块化功能。\n" +
                      "JEP 126 Lambda函数表达式支持\n" +
                      "JEP 104 内置Nashorn JavaScript引擎\n" +
                      "JEP 150 新的时间日期API\n" +
                      "JEP 122 移除HotSpt永久代";
              // 1.\\d表示一个数字
              String regStr = "(\\d\\d)(\\d\\d)";
              // 2.创建一个模式对象(即正则表达式对象)
              Pattern pattern = Pattern.compile(regStr);
              // 3.创建一个匹配器(根据正则表达式的规则去匹配content字符串)
              Matcher matcher = pattern.matcher(content);
              // 4.开始匹配
              /**
               * 源码剖析:
               * matcher.find() 完成的任务:考虑分组()
               * 什么是分组:(\\d\\d)表示第1组,第2个()表示第2组
               * 1.根据指定的规则,定位满足规则的子字符串(比如1991),1991对应的开始索引:0,结束索引:3
               * 2.找到后,将子字符串的开始的索引记录到matcher对象的属性int[] groups
               *   groups[0]中,即groups[0] = 0,把该(子字符串的结束的索引+1)的值记录到groups[1] = 4;
               *   有分组的情况:
               *   2.1 groups[0] = 0,把该(子字符串的结束索引+1)的值记录到groups[1] = 4
               *   2.2 记录第1组()匹配到的字符串groups[2] = 0,groups[3] = 2
               *   2.3 记录第2组()匹配到的字符串groups[4] = 2,groups[5] = 4
               *   如果有更多的分组,依此类推。。。
               * 3.同时记录 oldLast 的值为该(子字符串的结束的索引+1的值),即4
               *
               * matcher.group(0) 分析:
               * public String group(int group) {
               *         if (first < 0)
               *             throw new IllegalStateException("No match found");
               *         if (group < 0 || group > groupCount())
               *             throw new IndexOutOfBoundsException("No group " + group);
               *         if ((groups[group*2] == -1) || (groups[group*2+1] == -1))
               *             return null;
               *         return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString();
               *     }
               * 1.把根据groups[0] 和 groups[1]记录的位置从content开始截取子字符串并返回 -> [0,4)
               * 2.如果再次指向find()方法,仍然按照上面的步骤继续执行,
               *   即如果再次找到符合的子字符串,则新子字符串的开始索引和(结束索引+1)重新覆盖groups[0]和groups[1]
               */
              while (matcher.find()) {
                  /* 小结:
                  1.如果正则表达式有()即分组
                  2.取出匹配的字符串规则如下:
                  3.group(0)表示匹配到的整体的子字符串
                  4.group(1)表示匹配到的字符串的第1个子字符串的第1组
                  5.group(2)表示匹配到的字符串的第1个子字符串的第2组
                  但是group(n)中n的值不能取 超出分组数即模式中 ()的对数
                   */
                  System.out.println("找到:" + matcher.group(0));
                  System.out.println("第1组()匹配到的值="+matcher.group(1));
                  System.out.println("第2组()匹配到的值="+matcher.group(2));
                  // 第一次循环的输出:
                  //找到:1991
                  //第1组()匹配到的值=19
                  //第2组()匹配到的值=91
              }
          }
      }
      

      5.2、元字符

      基本介绍:

      元字符(Metacharacter)-转义号\\

      \\说明:在Java语言中,\\代表其他语言中一个.

      需要用到转义符号的字符:. * + ( ) $ / \ ? [ ] ^ { }

      符号 含义 示例 解释 匹配输入
      [] 可接收的字符列表 [abcd] 匹配a、b、c、d中的任意一个字符
      [^] 不接收的字符列表 [^abc] 匹配除了a、b、c之外的任意一个字符,包括数字和特殊符号
      - 连字符 A-Z 匹配任意单个大写字母
      . 匹配除\n以外的任何字符 a..b 以a开头,b结尾,中间包括2个任意字符的长度为4的字符串 aaab、a35b、a#*b
      \\d 匹配单个数字字符,相当于[0-9] \\d{3}(\\d)? 包含3个或4个数字的字符串 123、9876
      \\D 匹配单个非数字字符,相当于[^0-9] \\D(\\d)* 以单个非数字字符开头,后接任意个数字字符串 a、A342
      \\w 匹配单个数字、大小写字母字符,相当于[0-9a-zA-Z] \\d{3}\\w 以3个数字字符开头的长度为7的数字字符串 234abcd、12345Pe
      \\W 匹配单个非数字、大小写字母字符,相当于[^0-9a-zA-Z] \\W+\\d 以至少1个非数字字母字符开头,2个数字字符结尾的字符串 #29、#?@10
      \\s 匹配任何空白字符
      \\S 匹配任何非空白字符

      5.2.1、限定符

      限定符:用于指定其前面的字符和组合项连续出现多少次。

      符号 含义 示例 说明 匹配项
      * 指定字符重复0次或n次(相当于无要求) (abc)* 仅包含任意个abc的字符串 abc、abcabc
      + 指定字符重复1次或n次(至少一次) m+(abc)* 以至少1个m开头,后接任意个abc的字符串 m、mabc、mabcabc
      ? 指定字符重复0次或1次(最多一次) m+abc? 以至少1个m开头,后接ab或abc的字符串,?作用于最近的字符 mab、mabc、mmmabc
      只能输入n个字符 [abcd] 由abcd中字母组成的任意长度为3的字符串 abc、dbc、adc
      指定至少n个匹配 [abcd] 由abcd中字母组成的任意长度不小于3的字符串 aab、dbc、aaabdc
      指定至少n个但不多于m个匹配 [abcd] 由abcd中字母组成的任意长度不小于3且不大于5的字符串 abc、abcd、aaaaa

      注意:Java中匹配默认贪婪匹配,即尽可能匹配多的。

      Java中汉字的编码范围:[\u0391-\uffe5]

      5.2.2、选择匹配符

      选择匹配符(|):在匹配某个字符串的时候是选择性的,即:既可以匹配这个,也可以匹配另一个。如:ab|cd:表示匹配ab或cd

      5.2.3、字符匹配符

      • [a-z]:可以匹配a-z中任意一个字符
      • Java正则表达式默认是区分字母大小写的,如何实现不区分大小写?
        • (?i)abc:表示abc都不区分大小写
        • a(?i)bc:表示bc不区分大小写
        • a((?i)b)c:表示只有b不区分大小写
        • Pattern pat = Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);表示匹配时不区分字母大小写。

      5.2.4、定位符

      定位符:规定要匹配的字符串出现的位置,比如在字符串的开始还是在结束的位置。

      符号 含义 示例 说明 匹配项
      ^ 指定起始字符 [1]+[a-z]* 以至少1个数字开头,后接任意个小写字母的字符串 123、6aa、555edf
      $ 指定结束字符 [2]\-[a-z]+$ 以1个数字开头后接连字符-,并以至少1个小写字母结尾的字符串 1-a
      \\b 匹配目标字符串的边界 yxz\\b 字符串的边界指的是子串间有空格,或者是目标字符串的结束位置 wjdsfdsayxz fasdyxz
      \B 匹配目标字符串的非边界 yxz\\B 和\\b相反 yxzashfgkue

      5.3、三个常用类

      5.3.1、Pattern

      Pattern对象是一个正则表达式对象,Pattern类没有公共构造方法。要创建一个Pattern对象,调用其公共静态方法compile(),它返回一个Pattern对象。该方法接受一个正则表达式作为它的第一个参数。如:Pattern pattern = Pattern.compile(regStr);

      matches()方法: 整体匹配,如果正则表达式对象和字符串整体一致,则返回true,如果正则只是匹配到字符串的一部分,则返回false。

      5.3.2、Matcher

      Matcher对象是对输入字符串进行解释和匹配的引擎。与Pattern类一样,Matcher也没有公共构造方法。需要调用Pattern对象的matcher()方法来获得一个Matcher对象

      replaceAll(String str)方法:将匹配到的内容替换为str

      5.3.3、PatternSyntaxException

      PatternSyntaxException是一个非强制异常类,它表示一个正则表达式模式中的语法错误。

      5.4、分组、捕获、反向引用

      捕获分组:

      常用分组构造形式 说明
      (pattern) 非命名捕获。捕获匹配的子字符串。编号为0的第1个捕获是由整个正则表达式模式匹配的文本,其他捕获结果则根据左括号的顺序从1开始自动编号
      (?pattern) 命名捕获。将匹配的子字符串捕获到一个组名称或编号名称中。用于name的字符串不能包含任何标点符号,并且不能以数字开头。可以使用单引号替代尖括号,例如:?'name'

      非捕获分组:

      常用分组构造形式 说明
      (?:pattern) 匹配pattern但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储供以后使用的匹配。这对于用“or”字符(|)组合模式部件的情况很有用。如:`'industr(?:y
      (?=pattern) 它是一个非捕获匹配。如:`'Windows(?=95
      (?!pattern) 该表达式匹配不处于匹配pattern的字符串的起始点的搜索字符串。它是一个非捕获匹配。如:`'Windows(?!95

      分组

      可以用圆括号组成一个比较复杂的匹配模式,那么一个圆括号的部分可以看作是一个子表达式(一个分组)

      捕获

      把正则表达式中子表达式/分组匹配的内容,保存到内存中以数字编号或显式命名的组里,方便后面引用,从左向右,以分组的左括号为标志,第一个出现的组号为1,第二个为2,以此类推。组0代表的是整个正则式。

      反向引用

      前提:圆括号的内容被捕获后,可以在这个括号后被使用,从而写出一个比较实用的匹配模式,称为反向引用。这个引用既可以是正则表达式内部,也可以是在正则表达式外部。内部反向引用:\\分组号,外部反向引用:$分组号

      案例:

      1. 要匹配两个连续的相同数字:(\\d)\\1
      2. 要匹配五个连续的相同数字:(\\d)\\1{4}
      3. 要匹配个位与千位相同,十位与百位相同的数(5225,1551):(\\d)(\\d)\\2\\1
      4. 要匹配商品编号,形式如:12321-333999111的号码,要求满足前面是一个五位数,然后一个-号,然后是一个九位数,连续的每三位数要相同:\\d{5}-(\\d)\\1{2}(\\d)\\2{2}(\\d)\\3{2}

      经典案例:结巴去重

      package regexp;
      
      import java.util.regex.Matcher;
      import java.util.regex.Pattern;
      
      public class RegExpDistinct {
          public static void main(String[] args) {
              String content = "我...我要....学学学学编程java!";
              // 1.去掉所有‘.’
              Pattern pattern = Pattern.compile("\\.");
              Matcher matcher = pattern.matcher(content);
              content = matcher.replaceAll("");
      //        System.out.println("content:" + content);
      //
      //        // 2.去掉重复字,使用(.)\\1+
      //        // content:我我要学学学学编程java!
      //        pattern = Pattern.compile("(.)\\1+"); // 分组捕获到的内容记录到$1
      //        matcher = pattern.matcher(content); // 注意:因为正则表达式变化,需要重新获取matcher
      //        while (matcher.find()) {
      //            System.out.println("找到:" + matcher.group(0));
      //        }
      //        // 使用反向外部引用$1 来替换匹配到的内容
      //        content = matcher.replaceAll("$1");
      //        System.out.println("content=" + content);
      
              // 3.一条语句可以实现去重
              content = Pattern.compile("(.)\\1+").matcher(content).replaceAll("$1");
              System.out.println("content = " + content);
          }
      }
      

      String类中关于正则表达式的方法

      String replaceAll(String regex, String replacement):根据正则表达式regex将str中匹配到的内容替换成replacement,返回String类型

      Boolean matches(String regex):根据正则表达式regex进行整体匹配,返回true或false。

      String[] split(String regex):根据正则表达式regex匹配上的内容进行分割字符串,返回一个String[]。

      package regexp;
      
      public class StringReg {
          public static void main(String[] args) {
              String content = "JDK1.2, JDK1.3, JDK1.4都是小版本";
              content = content.replaceAll("JDK1\\.2|JDK1\\.3|JDK1\\.4", "JDK");
              System.out.println(content);
      
              // 验证手机号:13688889999,必须以138、139 开头
              content = "13888889999";
              if (content.matches("1(38|39)\\d{8}")) {
                  System.out.println("验证成功");
              } else {
                  System.out.println("验证失败");
              }
      
              // 要求按照# 或者 - 或者 ~ 或者 数字 来分割
              content = "hello#abc-myName~jack98北京";
              String[] split = content.split("#|-|~|\\d+");
              for (String s : split) {
                  System.out.println(s);
              }
          }
      }
      

      5.5、贪婪匹配和非贪婪匹配

      Java中默认匹配都是贪婪匹配,即匹配尽可能多的。

      可以在限定符后加上?表示非贪婪匹配。当?紧随其他限定符(*,+,?,{n},{n,},{n,m})之后时,匹配模式是“非贪婪匹配”。非贪婪匹配模式匹配搜索到的尽可能短的字符串。而默认的贪心匹配模式匹配搜索到的尽可能长的字符串。例如:在字符串“oooo”中,o+?只匹配单个o,而o+匹配结果为oooo。

      5.6、应用实例

      正则表达式匹配URL地址

      package regexp;
      
      import java.util.regex.Matcher;
      import java.util.regex.Pattern;
      
      @SuppressWarnings({"all"})
      public class RegExpURL {
          public static void main(String[] args) {
              /**
               * 正则表达式匹配url
               * 1.先确定url开头 http:// 或 https://,确定正则匹配模式^((http|https)://)?
               * 2.然后通过([\w-]+\.)+([\w-])+ 匹配域名 www.bilibili.com
               * 3.匹配url剩余部分(\\/[\\w-?=&/%.#_]*)?$
               */
              String content = "https://www.bilibili.com/video/BV1fh411y7R8?p=894&vd_source=61c88ed5566b8e501164b2b249e61557";
              String regStr = "^((http|https)://)?([\\w-]+\\.)+[\\w-]+(\\/[\\w-?=&/%.#_]*)?$";
              Pattern pattern = Pattern.compile(regStr);
              Matcher matcher = pattern.matcher(content);
              if (matcher.find()) {
                  System.out.println("满足格式");
              } else {
                  System.out.println("不满足格式");
              }
          }
      }
      

      6、JDK新特性

      Java 8新特性

      基本介绍:

      Java8(又称jdk1.8)是Java语言开发的一个重要版本。Java8是oracle公司于2014年3月发布,可以看成是自Java5以来最具革命性的版本。Java 8为Java语言、编译器、类库、开发工具和JVM带来大量新特性。

      优点:

      • 速度更快
      • 代码更少(新的语法:Lambda表达式)
      • 强大的Stream API
      • 便于并行
      • 最大化减少空指针异常:Optional类
      • Nashorn引擎,允许在JVM上允许JS应用

      Lambda表达式

      Lambda是一个匿名函数,可以把Lambda表达式理解为一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。

      快速入门:

      package new_characteristic.jdk8;
      
      import org.junit.jupiter.api.Test;
      
      import java.util.Comparator;
      
      public class LambdaTest {
      
          @Test
          public void test1() {
              Runnable r1 = new Runnable() {
                  @Override
                  public void run() {
                      System.out.println("我爱Java");
                  }
              };
              r1.run();
      
              // 改写成Lambda表达式:
              Runnable r2 = () -> System.out.println("我爱python");
              r2.run();
          }
      
          @Test
          public void test2() {
              Comparator<Integer> com1 = new Comparator<Integer>() {
                  @Override
                  public int compare(Integer o1, Integer o2) {
                      return Integer.compare(o1, o2);
                  }
              };
              System.out.println(com1.compare(31, 29));
      
              // 改写成Lambda表达式:
              Comparator<Integer> com2 = (o1, o2) -> Integer.compare(o1, o2);
              System.out.println(com2.compare(520, 1314));
      
              // 方法引用的方式:
              Comparator<Integer> com3 = Integer :: compare;
              System.out.println(com3.compare(321, 123));
          }
      }
      

      Lambda表达式的基本语法:

      举例:(o1, o2) -> Integer.compare(o1, o2);

      格式:

      1. ->:Lambda表达式的操作符,也称箭头操作符
      2. ->左边:Lambda表达式的形参列表,其实就是接口中的抽象方法的形参列表。
      3. ->右边:Lambda体。其实就是重写的抽象方法的方法体。

      Lambda表达式的本质:作为函数式接口的实例 -> 万事万物皆对象

      Lambda表达式的6种语法格式:

      1. lambda无参,无返回值
      2. lambda有参,无返回值
      3. 数据类型可以省略,因为可以由编译器推断得出,称为“类型推断”
      4. lambda若只需要一个参数时,参数的小括号可以省略。
      5. lambda需要两个或以上的参数,多条执行语句,并且可以有返回值
      6. 当lambda体只有一条语句时,return与大括号若有,都可以省略
      package new_characteristic.jdk8;
      
      import org.junit.jupiter.api.Test;
      
      import java.util.ArrayList;
      import java.util.Comparator;
      import java.util.function.Consumer;
      
      @SuppressWarnings({"all"})
      public class LambdaTest1 {
      
          // 语法格式1:无参,无返回值
          @Test
          public void test1() {
              Runnable r1 = new Runnable() {
                  @Override
                  public void run() { // 抽象方法无参,无返回值
                      System.out.println("我爱Java");
                  }
              };
              r1.run();
      
              // 改写成Lambda表达式:
              Runnable r2 = () -> {
                  System.out.println("我爱python");
              };
              r2.run();
          }
      
          // 语法格式2:lambda需要一个参数,无返回值
          @Test
          public void test2() {
              Consumer<String> con1 = new Consumer<String>() {
                  @Override
                  public void accept(String s) {
                      System.out.println(s);
                  }
              };
              con1.accept("谎言和誓言的区别是什么?");
      
              System.out.println("===========================");
              Consumer<String> con2 = (String s) -> {
                  System.out.println(s);
              };
              con2.accept("一个是听的人当真了,一个是说的人当真了");
          }
      
          // 语法格式3:数据类型可以省略,因为可以由编译器推断得出,称为“类型推断”
          @Test
          public void test3() {
              Consumer<String> con1 = (String s) -> {
                  System.out.println(s);
              };
              con1.accept("一个是听的人当真了,一个是说的人当真了");
      
              System.out.println("===========================");
              Consumer<String> con2 = (s) -> { // 类型推断
                  System.out.println(s);
              };
              con2.accept("一个是听的人当真了,一个是说的人当真了");
      
              // 类型推断场景
              ArrayList<String> list = new ArrayList<>(); // 类型推断
              int[] nums = {1, 2, 3}; // 类型推断
          }
      
          // 语法格式4:lambda若只需要一个参数时,参数的小括号可以省略。
          @Test
          public void test4() {
              Consumer<String> con1 = (s) -> { // 类型推断
                  System.out.println(s);
              };
              con1.accept("一个是听的人当真了,一个是说的人当真了");
      
              System.out.println("===========================");
      
              Consumer<String> con2 = s -> { // 类型推断
                  System.out.println(s);
              };
              con2.accept("一个是听的人当真了,一个是说的人当真了");
          }
      
          // 语法格式5:lambda需要两个或以上的参数,多条执行语句,并且可以有返回值
          @Test
          public void test5() {
              Comparator<Integer> com1 = new Comparator<Integer>() {
                  @Override
                  public int compare(Integer o1, Integer o2) {
                      return o1.compareTo(o2);
                  }
              };
              System.out.println(com1.compare(13, 31));
      
              System.out.println("===========================");
      
              Comparator<Integer> com2 = (o1, o2) -> {
                  System.out.println(o1);
                  System.out.println(o2);
                  return o1.compareTo(o2);
              };
              System.out.println(com2.compare(14, 41));
          }
      
          // 语法格式6:当lambda体只有一条语句时,return与大括号若有,都可以省略
          @Test
          public void test6() {
              Comparator<Integer> com1 = (o1, o2) -> {
                  return o1.compareTo(o2);
              };
              System.out.println(com1.compare(14, 41));
      
              System.out.println("============================");
      
              Comparator<Integer> com2 = (o1, o2) -> o1.compareTo(o2);
              System.out.println(com2.compare(25, 52));
          }
      }
      

      Lambda表达式总结:

      使用Lambda表达式的前提:作为函数式接口的实现(只有一个方法)。

      1. ->左边:Lambda形参列表的参数类型可以省略(类型推断),如果Lambda形参列表只有一个参数,其一对()也可以省略。
      2. ->右边:Lambda体应该使用一对{}包裹,如果Lambda体只有一条执行语句(可能是return语句),可以省略一对{}return关键字({}和return关键字要么一起省略,要么都不省略)
      3. Lambda表达式本质:接口的对象

      函数式接口

      基本介绍:

      只包含一个抽象方法的接口,称为函数式接口(FunctionalInterface)

      可以通过Lambda表达式来创建函数式接口的对象。(若Lambda表达式抛出一个受检异常(即:非运行时异常)),那么该异常需要在目标接口的抽象方法上进行声明。

      可以在一个接口上使用@FunctionalInterface注解,这样能够检查该接口是否是一个函数式接口。同时javadoc也会包含一条声明来说明该接口是一个函数式接口。

      java.util.function包下定义了Java 8的大量函数式接口。

      Java内置四大核心函数式接口:

      函数式接口 参数类型 返回类型 用途
      Consumer消费型接口 T void 对类型为T的对象应用操作,包含方法void accept(T t)
      Supplier供给型接口 T 返回类型为T的对象,包含方法T get()
      Function<T, R>函数型接口 T R 对类型为T的对象应用操作,并返回结果,结果是R类型的对象,包含方法R apply(T t)
      Predicate指定型接口 T boolean 确定类型为T的对象是否满足某约束,并返回boolean值,包含方法:boolean test(T t)
      package new_characteristic.jdk8;
      
      import org.junit.jupiter.api.Test;
      
      import java.util.ArrayList;
      import java.util.Arrays;
      import java.util.List;
      import java.util.function.Consumer;
      import java.util.function.Predicate;
      
      public class LambdaTest2 {
          @Test
          public void test1() {
              happy(500, new Consumer<Double>() {
                  @Override
                  public void accept(Double aDouble) {
                      System.out.println("大学生每月开销:" + aDouble + "元");
                  }
              });
      
              System.out.println("***********************************");
              happy(600, aDouble -> System.out.println("大学生每月开销:" + aDouble + "元"));
          }
      
          /**
           * 传入参数给消费式接口中的方法
           *
           * @param money 钱
           * @param con   消费型接口
           */
          public void happy(double money, Consumer<Double> con) {
              con.accept(money);
          }
      
          @Test
          public void test2() {
              List<String> list = Arrays.asList("北京", "南京", "天津", "普京", "东京");
              List<String> filtered1 = filterString(list, new Predicate<String>() {
                  @Override
                  public boolean test(String s) {
                      return s.contains("京");
                  }
              });
              System.out.println(filtered1);
              System.out.println("***********************************");
              List<String> filtered2 = filterString(list, s -> s.contains("北"));
              System.out.println(filtered2);
      
          }
      
          /**
           * 根据给定的规则,过滤集合中的字符串,规则由Predicate中的test方法决定
           *
           * @param list 需要过滤的字符串集合
           * @param pre  函数型接口
           * @return 返回过滤后的集合
           */
          public List<String> filterString(List<String> list, Predicate<String> pre) {
              ArrayList<String> filter = new ArrayList<>();
              for (String s : list) {
                  if (pre.test(s)) {
                      filter.add(s);
                  }
              }
              return filter;
          }
      }
      

      函数式接口

      方法引用与构造器引用

      方法引用

      基本介绍:

      1. 当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用。
      2. 方法引用可以看作是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法。
      3. 要求:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致。针对情况1和情况2
      4. 格式:使用操作符::将类(或对象)与方法名分隔开来,常用情况:
        • 对象::实例方法名(情况1)
        • 类::静态方法名(情况2)
        • 类:实例方法名(情况3)
      5. 方法引用,本质上就是Lambda表达式,而Lambda表达式作为函数式接口的实例,所以方法引用也是函数式接口的实例。
      package new_characteristic.jdk8;
      
      import oop_medium_Exercise.homework05.Employee;
      import org.junit.jupiter.api.Test;
      
      import java.util.Comparator;
      import java.util.function.BiPredicate;
      import java.util.function.Consumer;
      import java.util.function.Function;
      import java.util.function.Supplier;
      
      /**
       * @Description: 测试方法引用的三种方式 对象::非静态方法名,类名::静态方法名,类名::非静态方法名
       * @Author: Yao Xuan zhi
       * @Create: 2023-03-21, 14:51:16
       * @IDE: IntelliJ IDEA
       */
      
      public class MethodRefTest {
          // 情况一:对象::非静态方法名
      
          /**
           * Consumer<T>接口中的void accept(T t);
           * PrintStream中void println(T t)方法
           */
          @Test
          public void test1() {
              // lambda表达式
              Consumer<String> con1 = s -> System.out.println(s);
              con1.accept("Java8新特性 Lambda表达式");
      
              System.out.println("*****************************");
              Consumer<String> con2 = System.out::println;
              con2.accept("Java8新特性 方法引用");
          }
      
          /**
           * Employee中T getName()
           * Supplier<T> 中T get();
           */
          @Test
          public void test2() {
              Employee emp1 = new Employee("Lww", 10000);
              Supplier<String> sup1 = () -> emp1.getName();
              System.out.println(sup1.get());
      
              System.out.println("*****************************");
              Employee emp2 = new Employee("Lww", 10000);
              Supplier<String> sup2 = emp1::getName;
              System.out.println(sup2.get());
          }
      
          // 情况二:类名::静态方法名
      
          /**
           * Comparator<T>中的int compare(T o1, T o2);
           * Integer中的int compare(T t1, T t2)
           */
          @Test
          public void test3() {
              Comparator<Integer> com1 = (t1, t2) -> Integer.compare(t1, t2);
              System.out.println(com1.compare(4, 3));
      
              System.out.println("*****************************");
              Comparator<Integer> com2 = Integer::compare;
              System.out.println(com2.compare(4, 3));
          }
      
          /**
           * Function<T, R>接口中的 R apply(T t);
           * Math中的static long round(double a)
           */
          @Test
          public void test4() {
              Function<Double, Long> func = new Function<Double, Long>() {
                  @Override
                  public Long apply(Double aDouble) {
                      return Math.round(aDouble);
                  }
              };
              System.out.println(func.apply(1314.520));
      
              System.out.println("*****************************");
              Function<Double, Long> func1 = d -> Math.round(d);
              System.out.println(func1.apply(3.14));
      
              System.out.println("*****************************");
              Function<Double, Long> func2 = Math::round;
              System.out.println(func2.apply(520.1314));
          }
      
          // 情况三:类名::实例方法名
      
          /**
           * Comparator<T>接口中的int compare(T o1, T o2);
           * String中的int T.compareTo(T t)
           */
          @Test
          public void test5() {
              Comparator<String> com1 = (s1, s2) -> s1.compareTo(s2);
              System.out.println(com1.compare("abc", "abd"));
      
              System.out.println("*****************************");
              Comparator<String> com2 = String::compareTo;
              System.out.println(com2.compare("abz", "abn"));
          }
      
          /**
           * BiPredicate<T, U>中的boolean test(T t, U u);
           * String中的boolean t.equals(u)
           */
          @Test
          public void test6() {
              BiPredicate<String, String> pre1 = (s1, s2) -> s1.equals(s2);
              System.out.println(pre1.test("java", "java"));
      
              System.out.println("*****************************");
              BiPredicate<String, String> pre2 = String::equals;
              System.out.println(pre2.test("python", "python"));
          }
      
          /**
           * Function<T, R>中的R apply(T t);
           * Employee中的String getName()
           */
          @Test
          public void test7() {
              Function<Employee, String> func1 = e -> e.getName();
              System.out.println(func1.apply(new Employee("Lww", 12345)));
      
              System.out.println("*****************************");
              Function<Employee, String> func2 = Employee::getName;
              System.out.println(func2.apply(new Employee("Lww", 12345)));
          }
      }
      
      构造器引用

      和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致,抽象方法的返回类型即为构造器所属类的类型。

      数组引用和构造器引用类似,把数组看成一个类。

      package new_characteristic.jdk8;
      
      import org.junit.jupiter.api.Test;
      
      import java.util.Arrays;
      import java.util.function.BiFunction;
      import java.util.function.Function;
      import java.util.function.Supplier;
      
      public class ConstructorRef {
          //构造器引用和数组引用
      
          /**
           * Supplier<T>中的T get();
           * Employee的空参构造器
           */
          @Test
          public void test1() {
              Supplier<Employee> sup = new Supplier<Employee>() {
                  @Override
                  public Employee get() {
                      return new Employee();
                  }
              };
              System.out.println(sup.get());
      
              System.out.println("*************************");
              Supplier<Employee> sup1 = () -> new Employee();
              System.out.println(sup1.get());
      
              System.out.println("*************************");
              Supplier<Employee> sup2 = Employee::new;
              System.out.println(sup2.get());
          }
      
          /**
           * Function<T, R>中的 R apply(T t);
           */
          @Test
          public void test2() {
              Function<Integer, Employee> func1 = id -> new Employee(id);
              System.out.println(func1.apply(1));
      
              System.out.println("*************************");
              Function<Integer, Employee> func2 = Employee::new;
              Employee employee = func2.apply(2);
              System.out.println(employee);
          }
      
          /**
           * BiFunction<T, U, R>接口中的构造器 R apply(T t, U u)
           */
          @Test
          public void test3() {
              BiFunction<Integer, String, Employee> func1 = (id, name) -> new Employee(id, name);
              Employee yancy = func1.apply(3, "yancy");
              System.out.println(yancy);
      
              System.out.println("*************************");
              BiFunction<Integer, String, Employee> func2 = Employee::new;
              Employee yxz = func2.apply(5, "yxz");
              System.out.println(yxz);
          }
      
          /**
           * 数组引用
           */
          public void test4() {
              Function<Integer, String[]> func1 = length -> new String[length];
              String[] apply = func1.apply(5);
              System.out.println(Arrays.toString(apply));
      
              System.out.println("*************************");
              Function<Integer, String[]> func2 = String[]::new;
              String[] apply2 = func2.apply(10);
              System.out.println(Arrays.toString(apply2));
          }
      }
      

      Stream API

      1、说明
      • Java8中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream API。
      • Stream API ( java.util.stream) 把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
      • Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
      2、什么是Stream?

      Stream 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

      Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构,讲的是数据,而 Stream 是有关计算的,讲的是计算。前者是主要面向内存,存储在内存中,后者主要是面向 CPU,通过 CPU 实现计算。

      注意:

      1. Stream 自己不会存储元素
      2. Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
      3. Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。即一旦执行终止操作,就执行中间操作链,并产生结果。
      4. Stream一旦执行了终止操作,就不能再调用其它中间操作或终止操作了。
      3、Stream的操作三个步骤

      1- 创建 Stream
      一个数据源(如:集合、数组),获取一个流

      2- 中间操作
      每次处理都会返回一个持有结果的新Stream,即中间操作的方法返回值仍然是Stream类型的对象。因此中间操作可以是个操作链,可对数据源的数据进行n次处理,但是在终结操作前,并不会真正执行。

      3- 终止操作(终端操作)
      终止操作的方法返回值类型就不再是Stream了,因此一旦执行终止操作,就结束整个Stream操作了。一旦执行终止操作,就执行中间操作链,最终产生结果并结束Stream。

      Stream的操作

      3.1、创建Stream实例

      方式一:通过集合

      方式二:通过数组

      Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:

      • static Stream stream(T[] array): 返回一个流
      • public static IntStream stream(int[] array)
      • public static LongStream stream(long[] array)
      • public static DoubleStream stream(double[] array)
      @Test
      public void test02(){
          String[] arr = {"hello","world"};
          Stream<String> stream = Arrays.stream(arr); 
      }
      
      @Test
      public void test03(){
          int[] arr = {1,2,3,4,5};
          IntStream stream = Arrays.stream(arr);
      }
      

      方式三:通过Stream的of()

      可以调用Stream类静态方法 of(), 通过显示值创建一个流。它可以接收任意数量的参数。

      public static Stream of(T… values) : 返回一个流

      @Test
      public void test04(){
          Stream<Integer> stream = Stream.of(1,2,3,4,5);
          stream.forEach(System.out::println);
      }
      

      方式四:创建无限流(了解)

      可以使用静态方法 Stream.iterate() 和 Stream.generate(), 创建无限流。
      迭代:public static Stream iterate(final T seed, final UnaryOperator f)
      生成:public static Stream generate(Supplier s)

      // 方式四:创建无限流
      @Test
      public void test05() {
      	// 迭代
      	// public static<T> Stream<T> iterate(final T seed, final
      	// UnaryOperator<T> f)
      	Stream<Integer> stream = Stream.iterate(0, x -> x + 2);
      	stream.limit(10).forEach(System.out::println);
      
      	// 生成
      	// public static<T> Stream<T> generate(Supplier<T> s)
      	Stream<Double> stream1 = Stream.generate(Math::random);
      	stream1.limit(10).forEach(System.out::println);
      }
      
      3.2、一系列中间操作

      多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。

      1-筛选与切片

      方 法 描 述
      filter(Predicatep) 接收 Lambda , 从流中排除某些元素
      distinct() 筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
      limit(long maxSize) 截断流,使其元素不超过给定数量
      skip(long n) 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补

      2-映 射

      方法 描述
      map(Function f) 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
      mapToDouble(ToDoubleFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。
      mapToInt(ToIntFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。
      mapToLong(ToLongFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。
      flatMap(Function f) 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流

      3-排序

      方法 描述
      sorted() 产生一个新流,其中按自然顺序排序
      sorted(Comparator com) 产生一个新流,其中按比较器顺序排序
      package com.atguigu.stream;
      
      import org.junit.Test;
      
      import java.util.Arrays;
      import java.util.stream.Stream;
      
      public class StreamMiddleOperate {
      	@Test
          public void test01(){
              //1、创建Stream
              Stream<Integer> stream = Stream.of(1,2,3,4,5,6);
      
              //2、加工处理
              //过滤:filter(Predicate p)
              //把里面的偶数拿出来
              /*
               * filter(Predicate p)
               * Predicate是函数式接口,抽象方法:boolean test(T t)
               */
              stream = stream.filter(t -> t%2==0);
      
              //3、终结操作:例如:遍历
              stream.forEach(System.out::println);
          }
          @Test
          public void test02(){
              Stream.of(1,2,3,4,5,6)
                      .filter(t -> t%2==0)
                      .forEach(System.out::println);
          }
          @Test
          public void test03(){
              Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
                      .distinct()
                      .forEach(System.out::println);
          }
          @Test
          public void test04(){
              Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
                      .limit(3)
                      .forEach(System.out::println);
          }
          @Test
          public void test05(){
              Stream.of(1,2,2,3,3,4,4,5,2,3,4,5,6,7)
                      .distinct()  //(1,2,3,4,5,6,7)
                      .filter(t -> t%2!=0) //(1,3,5,7)
                      .limit(3)
                      .forEach(System.out::println);
          }
          @Test
          public void test06(){
              Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
                      .skip(5)
                      .forEach(System.out::println);
          }
          @Test
          public void test07(){
              Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
                      .skip(5)
                      .distinct()
                      .filter(t -> t%3==0)
                      .forEach(System.out::println);
          }
          @Test
          public void test08(){
              long count = Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
                      .distinct()
                      .peek(System.out::println)  //Consumer接口的抽象方法  void accept(T t)
                      .count();
              System.out.println("count="+count);
          }
          @Test
          public void test09(){
              //希望能够找出前三个最大值,前三名最大的,不重复
              Stream.of(11,2,39,4,54,6,2,22,3,3,4,54,54)
                      .distinct()
                      .sorted((t1,t2) -> -Integer.compare(t1, t2))//Comparator接口  int compare(T t1, T t2)
                      .limit(3)
                      .forEach(System.out::println);
          }
          @Test
          public void test10(){
              Stream.of(1,2,3,4,5)
                      .map(t -> t+=1)//Function<T,R>接口抽象方法 R apply(T t)
                      .forEach(System.out::println);
          }
          @Test
          public void test11(){
              String[] arr = {"hello","world","java"};
      
              Arrays.stream(arr)
                      .map(t->t.toUpperCase())
                      .forEach(System.out::println);
          }
          @Test
          public void test12(){
              String[] arr = {"hello","world","java"};
              Arrays.stream(arr)
                      .flatMap(t -> Stream.of(t.split("|")))//Function<T,R>接口抽象方法 R apply(T t)  现在的R是一个Stream
                      .forEach(System.out::println);
          } 
      }
      
      3.3、终止操作
      • 终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。

      • 流进行了终止操作后,不能再次使用。

      1-匹配与查找

      方法 描述
      allMatch(Predicate p) 检查是否匹配所有元素
      anyMatch(Predicate p) 检查是否至少匹配一个元素
      noneMatch(Predicate p) 检查是否没有匹配所有元素
      findFirst() 返回第一个元素
      findAny() 返回当前流中的任意元素
      count() 返回流中元素总数
      max(Comparator c) 返回流中最大值
      min(Comparator c) 返回流中最小值
      forEach(Consumer c) 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。
      相反,Stream API 使用内部迭代——它帮你把迭代做了)

      2-归约

      方法 描述
      reduce(T identity, BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 T
      reduce(BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 Optional

      备注:map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名。

      3-收集

      方 法 描 述
      collect(Collector c) 将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法

      Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。

      另外,Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表:

      方法 返回类型 作用
      toList Collector<T, ?, List> 把流中元素收集到List
      toSet Collector<T, ?, Set> 把流中元素收集到Set
      toCollection Collector<T, ?, C> 把流中元素收集到创建的集合
      counting Collector<T, ?, Long> 计算流中元素的个数
      summingInt Collector<T, ?, Integer> 对流中元素的整数属性求和
      averagingInt Collector<T, ?, Double> 计算流中元素Integer属性的平均值
      summarizingInt Collector<T, ?, IntSummaryStatistics> 收集流中Integer属性的统计值。如:平均值
      joining Collector<CharSequence, ?, String> 连接流中每个字符串
      maxBy Collector<T, ?, Optional> 根据比较器选择最大值
      minBy Collector<T, ?, Optional> 根据比较器选择最小值
      reducing Collector<T, ?, Optional> 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值
      collectingAndThen Collector<T,A,RR> 包裹另一个收集器,对其结果转换函数
      groupingBy Collector<T, ?, Map<K, List>> 根据某属性值对流分组,属性为K,结果为V
      partitioningBy Collector<T, ?, Map<Boolean, List>> 根据true或false进行分区
      package com.atguigu.stream;
      
      import java.util.List;
      import java.util.Optional;
      import java.util.stream.Collectors;
      import java.util.stream.Stream;
      
      import org.junit.Test;
      
      public class StreamEndding {
          @Test
          public void test01(){
              Stream.of(1,2,3,4,5)
                      .forEach(System.out::println);
          }
          @Test
          public void test02(){
              long count = Stream.of(1,2,3,4,5)
                      .count();
              System.out.println("count = " + count);
          }
          @Test
          public void test03(){
              boolean result = Stream.of(1,3,5,7,9)
                      .allMatch(t -> t%2!=0);
              System.out.println(result);
          }
      	@Test
          public void test04(){
              boolean result = Stream.of(1,3,5,7,9)
                      .anyMatch(t -> t%2==0);
              System.out.println(result);
          }
      	@Test
          public void test05(){
              Optional<Integer> opt = Stream.of(1,3,5,7,9).findFirst();
              System.out.println(opt);
          }
      	@Test
          public void test06(){
              Optional<Integer> opt = Stream.of(1,2,3,4,5,7,9)
                      .filter(t -> t%3==0)
                      .findFirst();
              System.out.println(opt);
          }
      	@Test
          public void test07(){
              Optional<Integer> opt = Stream.of(1,2,4,5,7,8)
                      .filter(t -> t%3==0)
                      .findFirst();
              System.out.println(opt);
          }
          @Test
          public void test08(){
              Optional<Integer> max = Stream.of(1,2,4,5,7,8)
                      .max((t1,t2) -> Integer.compare(t1, t2));
              System.out.println(max);
          }
          @Test
          public void test09(){
              Integer reduce = Stream.of(1,2,4,5,7,8)
                      .reduce(0, (t1,t2) -> t1+t2);//BinaryOperator接口   T apply(T t1, T t2)
              System.out.println(reduce);
          }
          @Test
          public void test10(){
              Optional<Integer> max = Stream.of(1,2,4,5,7,8)
                      .reduce((t1,t2) -> t1>t2?t1:t2);//BinaryOperator接口   T apply(T t1, T t2)
              System.out.println(max);
          }
          @Test
          public void test11(){
              List<Integer> list = Stream.of(1,2,4,5,7,8)
                      .filter(t -> t%2==0)
                      .collect(Collectors.toList());
      
              System.out.println(list);
          }   
      }
      
      4、Java9新增API

      新增1:Stream实例化方法

      ofNullable()的使用:

      Java 8 中 Stream 不能完全为null,否则会报空指针异常。而 Java 9 中的 ofNullable 方法允许我们创建一个单元素 Stream,可以包含一个非空元素,也可以创建一个空 Stream。

      //报NullPointerException
      //Stream<Object> stream1 = Stream.of(null);
      //System.out.println(stream1.count());
      
      //不报异常,允许通过
      Stream<String> stringStream = Stream.of("AA", "BB", null);
      System.out.println(stringStream.count());//3
      
      //不报异常,允许通过
      List<String> list = new ArrayList<>();
      list.add("AA");
      list.add(null);
      System.out.println(list.stream().count());//2
      //ofNullable():允许值为null
      Stream<Object> stream1 = Stream.ofNullable(null);
      System.out.println(stream1.count());//0
      
      Stream<String> stream = Stream.ofNullable("hello world");
      System.out.println(stream.count());//1
      

      iterator()重载的使用:

      //原来的控制终止方式:
      Stream.iterate(1,i -> i + 1).limit(10).forEach(System.out::println);
      
      //现在的终止方式:
      Stream.iterate(1,i -> i < 100,i -> i + 1).forEach(System.out::println);
      

      接口增强

      待补充...

      Optional类

      待补充...

      并行流

      并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。相比较串行的流,并行流可以很大程度上提高程序的执行效率。

      Java8中将并行进行了优化,StreamAPI可以声明性的通过parallel()sequential()在并行流与顺序流之间进行切换。

      串行流

      待补充...

      Java 9新特性

      待补充...

      Java 10新特性

      待补充...

      Java 11新特性

      待补充...


      1. 0-9 ↩︎

      2. 0-9 ↩︎

posted @ 2023-04-03 21:29  Yxz-smile  阅读(275)  评论(0编辑  收藏  举报