Java基础笔记-上

主要参考2023视频

一、Java三大平台

1、概述

  • Java SE(Standard Edition)
    Java语言的标准版,用于桌面应用的开发,是其他两个版本的基础
    学习目的:为今后要从事的Java EE开发,打基础

  • Java ME(Micro Edition)
    Java语言的小型版,用于嵌入式消费类电子设备

  • Java EE(Enterprise Edition)
    Java语言的企业版,用于Web方向的网站开发

2、Java的跨平台性

平台
指的是操作系统

跨平台
Java程序可以在任意操作系统上运行

跨平台工作原理
我们写的Java程序在运行时不看你在哪个操作系统,就看装没装虚拟机,只要装了虚拟机,就能运行在虚拟机中。即在需要运行的Java应用程序的操作系统上安装与操作系统对应的虚拟机即可
Java跨平台工作原理

二、JDK(Java Develop Kits)的组成

  • JVM(Java Virtual Machine)
    Java虚拟机,真正运行Java程序的地方

  • 核心类库
    Java自己写好的程序,给程序员自己的程序调用

  • JRE(Java Runtime Environment)
    我们将以上两部分统称为JRE,即Java的运行环境

  • 开发工具
    Java,Javac……

以上共同组成JDK
JDK的组成

三、标识符命名规范

1、硬性要求

必须要这么做,否则代码会报错

  • 必须由数字、字母、下划线(_)、美元符号($)组成
  • 数字不能开头
  • 不能是关键字
  • 区分大小写

2、小驼峰命名法

  1. 标识符是一个单词的时候,所有的字母小写
    示例:name
  2. 标识符由多个单词组成时,从第二个单词开始,首字母大写
    示例:firstName

3、大驼峰命名法

  1. 标识符是一个单词的时候,首字母大写
    示例:Student
  2. 标识符由多个单词组成时,每个单词的首字母大写
    示例:GoodStudent

4、使用方法

  • 小驼峰命名法主要给变量起名
  • 大驼峰命名法主要给类起名

四、数据类型

1、基本数据类型的四类八种

数据类型关键字内存占用取值范围
整数byte1字节负的2的7次方 ~ 2的7次方-1(-128~127)
short2字节负的2的15次方 ~ 2的15次方-1(-32768~32767)
int4字节负的2的31次方 ~ 2的31次方-1
long8字节负的2的63次方 ~ 2的63次方-1
浮点数float4字节1.401298e-45 ~ 3.402823e+38
double8字节4.9000000e-324 ~ 1.797693e+308
字符char2字节0-65535
布尔boolean1字节true,false

在java中整数默认是int类型,浮点数默认是double类型

整数类型和小数类型的取值范围大小关系
double > float > long > int > short > byte

注意

  • 如果要定义long类型的变量,在数据值后面需要加一个L作为后缀,L可以是大写的,也可以是小写的,建议使用大写,以免与数字1混淆
  • 定义float变量时,数据值也需要加一个F作为后缀,大小写都可以

2、引用数据类型

  • 数组
  • 接口

关于引用数据类型的一个问题
空指针异常(NullPointException)

原因
当引用数据类型的变量,被赋值为null后,就代表跟堆内存的连接被切断了,这时候还想去访问堆内存的数据,就会出现空指针异常

3、类型转换细节

public class TypeDemo {
    public static void main(String[] args) {
        byte b1 = 1;
        byte b2 = 2;
        // byte b3 = b1 + b2; 报错

        /*
        错误原因:
        b1和b2是两个byte类型,在运算的时候,会提升为int类型
        提升之后就是两个int类型在运算了,运算结果还是int
        将int类型的结果,赋给byte类型的变量,不能直接给
        byte b3 = b1 + 2; 这样写也会报错
         */

        // 解决方式1:用int类型接收
        int b3 = b1 + b2;
        // 解决方式2:运算完后,做一个强转
        byte b4 = (byte) (b1 + 2);

        System.out.println(b3);
        System.out.println(b4);

        // 下面的代码是正确的
        byte b = 3 + 4;
        System.out.println(b);

        /*
        原因:
        Java存在常量优化机制
        在编译的时候(javac)就会将3和4这两个字面量进行运算
        产生的字节码文件:byte b = 7;
         */
    }
}

运行结果:

3
3
7

五、逻辑运算符

1、类型

符号介绍说明
&逻辑与并且,遇false则false
l逻辑或或者,遇true则true
!逻辑非取反
^逻辑异或1.相同为true,不同为false。2.一个数字,被另外一个数字异或两次,该数本身不变。

在Java中,以上运算符可以直接操作数值(和C++有所不同)。使用 & 和 &&( | 和 || )得到的结果是相同的,但更多的时候使用双与、双或,因为当左边的结果能决定最终结果时,就不计算后面的表达式,效率更高

逻辑异或还是按位异或,即先将十进制数值转换为二进制形式,然后按位进行异或运算,运算完成后再将结果转为十进制数值

符号介绍说明
&&短路与具有短路效果,左边为false,右边就不执行了
ll短路或具有短路效果,左边为true,右边就不执行了

示例

boolean a = 4 > 3 & 3 < 30;
System.out.println(4 > 3 & 1 > 2);   // 逻辑与,结果为false
System.out.println(4 > 3 | 1 > 2);   // 逻辑与,结果为true

// 一个数字,被另外一个数字异或两次,该数本身不变
System.out.println(5^5^10);          // 结果为10
System.out.println(10^5^10);         // 结果为5

2、数据交换

数据交换的一个问题,使用异或运算符的特点解决

//实现两个变量的数据交换,不允许定义第三方变量
int x = 10;     
int y = 20;     

x = x ^ y;      // x = 10 ^ 20
y = x ^ y;      // y = 10 ^ 20 ^ 20     y = 10
x = x ^ y;      // x = 10 ^ 20 ^ 10     x = 20

System.out.println("x=" + x);   // x = 20
System.out.println("y=" + y);   // y = 10

六、数值拆分

需求
将一个三位数拆分为个位、十位、百位后打印输出

分析

  • 个位的计算:数值 % 10
  • 十位的计算:数值 / 10 % 10
  • 百位的计算:数值 / 10 / 10 % 10

数值位数更多时以此类推

取出数值的最高位的简化处理

  • 123:百位123 / 100 = 1
  • 1234:千位1234 / 1000 = 1
  • 12345:万位12345 / 10000 = 1

七、switch语句

格式

switch (表达式) {
	case1:
		语句体1;
		break;
	case2:
		语句体2;
		break;
	……
	default:
		语句体n+1;
		break;
}

注:可以不写default语句,对语法没有影响

执行流程:
拿着switch()中的表达式的值与case后的值匹配,
匹配成功就执行后面的代码,再由break结束掉整个switch语句。
如果给出的case都匹配失败就会执行最后的default

注意

  • case后面的值不允许重复
  • case后面的值只能是字面量,不能是变量
  • switch的()中能接收的类型:byte、short、char、int;JDK5开始可以是枚举,JDK7开始可以是String
  • case穿透现象:如果不写break跳出switch语句,代码就会一直指行下去
    public class SwitchDemo {
        public static void main(String[] args) {
            switch (1) {
                case 1:
                    System.out.println(1);
                case 2:
                    System.out.println(2);
                    break;
                case 3:
                    System.out.println(3);
                    break;
                default:
                    System.out.println("default");
                    break;
            }
        }
    }
    
    运行结果:
    1
    2
    

JDK14开始,case后面允许编写多个数据,多个数据间用,分隔

八、原码、反码、补码

概述
我们在代码中书写的数字默认为十进制,如果要书写其它进制的数字,需要在数字前面添加标识
不同进制的书写格式
示例

public class ScaleDemo {
    public static void main(String[] args) {
        System.out.println(110); // 110
        System.out.println(0b110); // 6
        System.out.println(0110); // 72
        System.out.println(0x110); // 272
    }
}

书写二进制数字格式是从JDK7以后才有的

二进制数据共有三种状态
原码、反码、补码

1、原码

原码是数据的二进制体现形式,一个字节由8个二进制位组成

高位:二进制数据中,最左侧的数据,通过高位代表符号位。0代表正数,1代表负数

11001011

其余位:表示数值大小

弊端:原码与原码遇到负数运算,会出现错误。因为计算机在运算的时候,都是以二进制补码的形式在运算,而补码需要用反码推出

2、反码

正数的反码与其原码相同,而负数的反码是对其原码逐位取反,但符号位除外

若原码:

11001011

则反码:

10110100

3、补码

正数的补码与其原码相同,而负数的补码是在其反码的末尾加1

若反码:

10110100

则补码:

10110101

4、总结

计算机数据的存储使用二进制补码形式存储,并且最高位是符号位。
正数:最高位是0
负数:最高位是1

  • 正数的原码、反码、补码都一样
  • 负数的的原码、反码、补码之间的转换:
    • 原码转补码:除最高位,对其余位取反再加1
    • 补码转原码:除最高位,对其余位取反再加1

5、强转中的精度损失

问题

public class ScaleDemo {
    public static void main(String[] args) {
        int a = 130;
        int b = (byte)a;
        System.out.println(a); // 130
        System.out.println(b); // -126
    }
}

解释

  • 整数130,默认为int,int占用4个字节,也就是四组8个二进制位:
    00000000 00000000 00000000 10000010
  • 强转到byte,4个字节强转为1个字节,就是砍掉前3组8位:
    10000010
  • 将上面的补码10000010推算成原码为11111110,即十进制的-126

九、跳转控制语句break、continue

break:跳出循环
范围:只能在switch语句循环语句中使用

continue:跳过本次循环
范围:只能在循环语句中使用

补充

  • 我们可以给循环起名字,即标号,在循环语句的前面写一个冒号,冒号前面写名字,一般放在循环语句的上一行
  • 可以通过break和continue来使用标号,如:break 标号名;就可以直接结束标号名代表的循环

标号示例

public static void main(String[] args) {
        tagName:
        while(true){
            switch(number) {
                case 1:
                    System.out.println("1");
                    break;
                case 2:
                    System.out.println("2");
                    break tagName; // 执行到这里即可跳出while死循环
                case 3:
                    System.out.println("3");
                    break;
            }
        }
}

十、Random随机数

Random randomNum = new Random();
int randNum = randomNum.nextInt(100) + 1; // 随机生成1~100的数

以上代码产生一个1~100的随机数
randomNum.nextInt(100)产生的随机数范围为0~99,参数100表示个数,从0开始
后面加1,即表示产生一个1~100的随机数
然后保存在randNum变量中

十一、数组介绍

数组指的是一种容器,用来存储同种数据类型的多个值

定义数组

数组的定义格式

1.数据类型[] 数组名;

2.数据类型 数组名[];

这种定义格式,定义出来的,只是数组类型的变量而已,内存中还没有创建出数组容器,需要初始化
在java中一般使用第一种定义格式

初始化
在内存中,为数组容器开辟空间,并将数据存入容器的过程

打印数组名的介绍
我们创建两个数组并分别打印数组名如下

//创建两个数组并静态初始化
int[] arr1 = {2,3,3};
double[] arr2 = {2.1,3.1,3.3};
//打印数组名
System.out.println(arr1);   //打印结果:[I@50cbc42f
System.out.println(arr2);   //打印结果:[D@75412c2f

得到的两个结果如下:
[I@50cbc42f
[D@75412c2f

说明:
@:就是一个分隔符,分隔数据,没别的意思
[:说明这是一个数组
I和D:和数组的数据类型有关
@后面的为十六进制数,表示数组的内存地址

注意
打印字符类型数组的数组名,不会看到地址值,而是数组中存储的数据内容

十二、数组的静态初始化

初始化就是赋值符号右边的内容

数组的静态初始化格式

1.完整格式:
	数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3...};
	
2.简化格式:
	数据类型[] 数组名 = {元素1,元素2,元素3...};

简化格式只是简化了代码书写,真正运行期间还是按照完整格式运行的,即new出来,会在堆内存开辟空间并且产生地址,数组名就代表了这个地址

十三、数组的动态初始化

数组的动态初始化,即在初始化的时候,只需要指定数组的长度,系统就会分配默认值(初始值)

数组的动态初始化格式

数据类型[] 数组名 = new 数据类型[长度];       

默认值的分类

  • 整数:0
  • 小数:0.0
  • 布尔:false
  • 字符:\u0000
    此为Unicode字符,一种编码格式:以 \u开头,后面加上一组十六进制数据
    \u0000常见的体现是空白字符
  • 字符串:null

十四、两种初始化的对比

1、两种初始化的区别

  • 静态初始化
    手动指定元素,系统会根据元素的个数,计算出数组的长度
  • 动态初始化
    手动指定长度,系统分配默认初始值

2、使用选择

  • 静态初始化
    如果要操作的数据,需求中已经明确给出了,使用静态初始化
  • 动态初始化
    只明确元素个数,不明确具体数值,使用动态初始化

十五、Java内存分配介绍

对象内存图(bilibili)

1、堆

new出来的东西会在堆内存中开辟空间并产生地址

2、栈

3、方法区

方法区是字节码文件加载时进入的内存。当我们运行一个Java程序时,会把它的字节码文件加载进方法区,此时文件中所有的方法在方法区中处于候命状态,等里面的方法被调用执行时,会进入栈内存中运行,运行完成后,方法就从栈内存中弹出去

4、本地方法栈

简单说是辅助虚拟机干活的,咱们不用管

5、寄存器

寄存器由CPU调用,咱们不用管

方法的参数传递问题

  1. 基本数据类型:传递的是数据值
  2. 引用数据类型:传递的是地址值

十六、二维数组介绍

概述
二维数组其实也是一种容器,该容器用于存储一维数组,可以理解为容器的嵌套

使用思路
若要操作的多组数据,又属于同一组数据,就可以考虑使用二维数组维护

十七、二维数组的静态初始化

二维数组的静态初始化格式

格式:
    数据类型[][] 数组名 = new 数据类型[][] {{元素1, 元素2}, {元素1, 元素2}};
示例:
    int[][] arr = new int[][] {{11, 22}, {33, 44}};
    
简化格式:
    数据类型[][] 数组名 = {{元素1, 元素2}, {元素1, 元素2}};
示例:
    int[][] arr = {{11, 22}, {33, 44}};
    
这样写格式更清晰:
    int[][] arr = {
        {11, 22, 33},
        {44, 55, 66}
    };

打印数组名得到的是二维数组的地址

int[][] arr = {
		{11,22,33},
		{44,55,66}
};
System.out.println(arr);    // 打印结果:[[I@50cbc42f

用一维数组的方式打印输出

public static void main(String[] args) {
            int[][] arr = {
                    {11, 22, 33},
                    {44, 55, 66}
            };
            System.out.println(arr[0]);    // 打印结果:[I@50cbc42f
            System.out.println(arr[1]);    // 打印结果:[I@75412c2f
    }

这样得到的是一维数组的地址,也就是里面的元素二维数组在存储一维数组时,它具体存储的是一维数组的地址值,所以arr[0]和arr[1]都分别是一个一维数组
二维数组存储
可以将创建好的一维数组直接存入二维数组中

public class Test {
    public static void main(String[] args) {
        int[] a = {11, 22, 33};
        int[] b = {44, 55, 66};
        int[][] ab = new int[2][3];
        ab[0] = a;
        ab[1] = b;
        
        // 遍历输出二维数组ab
        for (int i = 0; i < ab.length; i++) {
            for (int j = 0; j < ab[i].length; j++) {
                System.out.println(ab[i][j]);
            };
        }
    }
}

运行结果:

11
22
33
44
55
66

十八、二维数组的动态初始化

二维数组的动态初始化格式

格式:
    数据类型[][] 数组名 = new 数据类型[m][n];
    m表示这个二维数组,可以存放多少个一维数组
    n表示每一个一维数组,可以存放多少个元素
示例:
    int[][] arr = new int[3][4];

十九、面向对象三大特征

有了封装才有面向对象,有了面向对象才有继承和多态

1、封装

2、继承

3、多态

二十、类和对象

1、构造方法

概述

  • 构造方法又称构造器,是创建对象的时候,自动执行的方法
  • 类中若没有编写构造方法,系统会提供一个默认的、无参数的构造方法
  • 构造方法也是方法,可以重载

构造方法的作用

  • 本质的作用:构造所属类的实例(实例就是对象),也就是创建对象
  • 结合执行时机:我们可以利用构造方法在创建对象的时候,给对象中的数据初始化。

对于本质作用的理解
当存在一个类,才有创建对象的说法,即创建该类的对象。而所有的类都可以通过new出来的形式创建对象。而每个类中都至少有一个构造方法,如果我们没提供的话,就由系统默认提供一个

public class Book { // 这是一个Book类
    String id;
    String bookName;
    int price;

    Book(){}; // Book类的无参构造方法
}

Book book = new Book(); // Book是一个类,该行就是new一个Book对象出来

2、成员变量和局部变量的区别

区别成员变量局部变量
类中位置不同方法外方法内
初始化值不同有默认初始化值没有,使用之前需完成赋值
内存位置不同堆内存栈内存
声明周期不同随着对象的创建而存在,随着对象的消失而消失随着方法的调用而存在,随着方法的运行结束而消失
作用域自己所归属的大括号中自己所归属的大括号中

3、注意

  • 打印对象名,得到的是对象的内存地址。但是,目前所接触到的String、StringBuilder、ArrayList类的对象,打印对象名,得到的是元素内容,不是地址值(顺带一提,打印字符数组的数组名也得到的也是数组内容,不是地址)
  • 普通变量不初始化不能使用,但成员变量就算没有赋值初始化,也可以直接使用,使用的是对象的默认值。所以创建类时,里面的成员变量一般不赋值,创建具体对象后再赋值

二十一、封装

1、封装介绍

所谓封装就是:使用类设计对象时,将需要处理的数据,以及处理这些数据的方法,设计到对象中。换句话说,就是将变量和方法写进类中,使用类创建对象后,就可以通过对象给变量赋值,此时类中的数据就封装到了对象中,对象中的数据存储在堆内存中(因为是new出来的),且对象间互不干扰,便于管理、维护

设计规范
合理隐藏,合理暴露

2、权限修饰符

权限修饰符可以修饰成员变量成员方法

2.1、private(常用)

  • private所修饰的成员(包括成员变量和成员方法)只能在同一个类中进行访问。设置为私有成员变量是为了保证数据安全性
  • 被其所修饰的成员的使用范围:同一个类中

2.2、default

  • 称作默认权限,在成员前什么都不写,使用的就是default
  • 被其所修饰的成员的使用范围:同一个类中,同一个包中

2.3、protected

  • 被其所修饰的成员的使用范围:同一个类中,同一个包中,不同包的子类
  • protected修饰符在开发中用得很少

2.4、public(常用)

  • 被其所修饰的成员的使用范围:在任意位置访问,不能超出模块(module)

二十二、标准JavaBean

1、基本概念

JavaBean
就是实体类,即封装数据的类。实体类只负责数据存取,而对数据的处理交给其他类来完成,即再建一个类来处理这些数据,以实现数据和数据业务处理相分离

标准JavaBean需满足的标准

  1. 这个类中的成员变量都要设置为私有,并且要对外提供相应的setXxx(给变量赋值的方法)和getXxx(获取变量的值的方法)方法
  2. 类中提供无参,带参构造方法

2、IDEA中的ptg插件

可在IDEA中一键生成标准JavaBean

步骤
在IDEA中找到File、Settings、Plugins、搜索ptg,下载即可
使用时在代码中右键,选择Ptg To JavaBean即可生成

二十三、常用API

API(Application Programming Interface)
应用程序编程接口。就是别人写好的一些类,咱们只需要创建这些类的对象,然后调用里面的方法来解决问题

API帮助文档
可以理解为是Java的一份字典,已写好的API在这个文档里都有详细介绍,只要会使用该文档,能找到自己要使用的类怎么用即可

API帮助文档使用流程

  1. 进入文档在左上角点击“显示”,然后在索引位置搜索自己要查看的类
  2. 看包
    目的:是不是java.lang包(核心包),核心包不需要编写导包代码(import)。不是java.lang包,都需要编写导包代码
  3. 看这个类的介绍
    目的:搞清楚这个给类的作用
  4. 看这个类的构造方法
    目的:为了将该类的对象创建出来
  5. 看这个类的成员方法(方法摘要)
    方法名
    参数
    返回值
    介绍

二十四、String类

1、String类的特点

  • Java中String类很特殊,只有它多了一种创建对象的方法。因为Jave中所有双引号字符串,都是String这个类的对象
    String s = "abc";
    
    System.out.println("abc".length());     // 结果为3
    System.out.println(s.length());         // 结果为3
    
  • 字符串在创建之后,其内容不可更改。若想改变,只能用新的字符串(即对象)来替换
    String s = "abc";   // "abc"这是一个对象
          
    s = "def"; 
    /* 上面这行代码表示用"def"这个新的字符串对象
    替换了原有的s所记录的内容,原来的"abc"其实还在,里面的内容一点没变
    */
    
  • 字符串虽然不可改变,但是其内容可以被共享。与字符串常量池有关

字符串常量池:当我们使用双引号创建字符串对象时,会检查常量池中是否存在该数据

  • 不存在:创建
  • 存在:复用

2、String类的一些成员方法

方法说明
public boolean equals(要比较的字符串)字符串比较)将此字符串与指定的对象比较,完全一样结果才true,否则为false
public boolean equalsIgnoreCase(要比较的字符串)字符串比较)与上面的一样,但是不考虑大小写
public char[] toCharArray()字符串的遍历:调用后遍历字符数组即可)将字符串转换为字符数组
public char charAt(int index)字符串的遍历:多调用几次)返会指定索引处的char值,即根据索引找字符串中的字符
public int length()返回此字符串的长度(这是String类中的一个方法,与数组中的“length”不同,数组中的length是数组的一个属性)
public String subString(int beginIndex)截取)返回一个新的字符串,它是此字符串的一个子字符串,该方法会一直截取到字符串的末尾
public String subString(int beginIndex, int endIndex)截取)subString方法的重载,根据传入的开始和结束索引,对字符串做截取。截取范围:包含头,不包含尾
public String replace(有两个参数)替换)参数1:旧值,参数2:新值。用新值替换旧值
public String[] split(String regex)切割)根据传入的字符串作为规则,切割当前字符串

二十五、StringBuilder类

1、StringBuilder引入

  • 为什么要有StringBuilder?一句话,它可以提高字符串的操作效率

2、StringBuilder介绍

  • 和String一样,同属于java.lang包下,不需要编写导包代码

  • 它是一个可变的字符序列,也是与String最大的区别,因为String的内容不可更改

  • StringBuilder是字符串的缓冲区,我们可以将其理解为是容器,这个容器可以存储任意数据类型,但是只要进入到这个容器,全部变成字符串

3、StringBuilder类的常用成员方法

方法名说明
public StringBuilder append(任意类型)添加数据,并返回对象本身,所以可以实现链式编程
public StringBuilder reverse()反转容器中的内容
public int length()返回长度(字符出现的个数)
public String toString()可以把StringBuilder类型转换为String类型

二十六、集合

1、集合介绍

集合是一种容器,用来装数据的,类似于数组。数组定义完成并启动后,长度就固定了,而集合的大小可变,开发中用的更多。Java中的集合有很多,一大堆,先介绍ArrayList

2、ArrayList集合

长度可变的原理初认识

  • 它的底层也是数组。当创建ArrayList集合容器的时候,底层会存在一个长度为10的空数组
  • 当装不下时,他会自动扩容一个大小为原数组1.5倍的一个新数组,并将所有原数组数据拷贝到新数组中。随后,原来的数组会变成内存中的垃圾,会被Java的垃圾回收器不定时的情理掉,也就是释放这块内存

3、ArrayList集合的使用

ArrayList list = new ArrayList();   // 创建一个ArrayList对象
list.add("Lisa");                   // 调用add方法添加数据

ArrayList这个类把数据封装的很好,我们调用者不需要关心底层数组,只用关心真实数据有几个就可以了

用以上方法创建集合可以添加任意数据类型,但数据不够严谨

可以按以下方式创建,只需要在类型后加一对尖括号,指明数据类型即可。从JDK7版本开始,= 右边的类型尖括号的里面可以不用写类型了,尖括号会自动匹配到左边的类型

ArrayList<String> list = new ArrayList<String>(); // 创建一个ArrayList对象,并指定只能存储String类型的元素
list.add("Lisa");                                 // 调用add方法添加数据

/*
<>  这个尖括号叫做泛型
泛型中不允许编写基本数据类型,但是,
可以使用基本数据类型所对应的包装类,包装类就是将基本数据类型包装成类,变成引用数据类型
         
基本数据类型    包装类
byte           Byte
short          Short
int            Integer
long           Long
float          Float
double         Double
boolean        Boolean
char           Character
*/

4、ArrayList集合的常用成员方法

方法名说明
public boolean add(E e))将指定的元素添加到此集合的末尾
public void add(int index, E element))在此集合中的指定位置插入指定的元素
public E remove(int index))删除指定索引处的元素,返回被删除的元素
public boolean remove(Object o))删除时指定的元素,返回删除是否成功
public E set(int index, E element))修改指定索引处的元素,返回被修改的元素
public E get(int index))返回指定索引处的元素
public int size())返回集合中的元素个数

注意一个问题
删除ArrayList中的元素时注意,集合不会让元素中间有空缺,删除一个元素后,集合会把后面的元素整体向前进行移动,所以当要删除的元素连在一起时,会漏删

解决方法

  1. 使用正序遍历删除,在删除操作后面增加一个减减(--)操作
  2. 使用倒序遍历操作

二十七、泛型

1、泛型概述

介绍

  • 泛型是JDK5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查
  • 没有泛型的时候或者如果我们没有给集合指定数据类型,默认所有的数据类型都是Object类型,此时可以往集合中添加任意数据类,弊端就是我们在获取数据的时候,无法使用它的特有行为,推出了泛型后,可以在添加数据的时候把类型统一

格式

  • <数据类型>:指定一种类型的格式,尖括号里面可以任意书写,一般只写一个字母,例如:<E>
  • <类型1, 类型2…>:指定多种类型的格式,多种类型之间用逗号隔开,例如:<E, T>

注意

  • 泛型只能支持引用数据类型,想写基本数据类型,需要写对应的包装类
  • Java中的泛型其实是伪泛型(泛型的擦除)

泛型的好处

  • 把运行时期的问题提前到了编译期间
  • 避免了强制类型转换可能出现的异常

泛型可以在很多地方进行定义

  • 写在类后面,就叫泛型类【写在类名后面】
  • 写在方法上面,就叫泛型方法【写在返回值类型前面】
  • 写在接口后面,就叫泛型接口【写在接口名后面】

2、泛型类

写一个MyArrayList

import java.util.Arrays;

public class MyArrayListTest {
    public static void main(String[] args) {
        MyArrayList<String> ml = new MyArrayList<>();
        ml.add("a");
        ml.add("b");
        ml.add("c");
        System.out.println(ml.get(0));
        System.out.println(ml);
    }
}

// 当编写一个类时,如果某个变量的数据类型不确定时,就可以定义带有泛型的类
class MyArrayList<E> {
    Object[] obj = new Object[10]; // 初始化大小为10的数组
    int size; // 类中定义的,默认为0

    // 添加元素
    public boolean add(E e) {
        obj[size] = e;
        size++;
        return true;
    }

    // 根据索引获取元素
    public E get(int index) {
        return (E)obj[index];
    }

    @Override
    public String toString() {
        return "MyArrayList{" +
                "obj=" + Arrays.toString(obj) +
                '}';
    }
}

运行结果:

a
MyArrayList{obj=[a, b, c, null, null, null, null, null, null, null]}

3、泛型方法

类中方法的形参类型不确定时,可以使用类名后面定义的泛型<E>,也可以在方法声明上定义自己的泛型

格式

修饰符 <类型> 返回值类型 方法名(类型 变量名) {

}

示例

public static <E> void fun(E e) {
        
}

4、泛型接口

泛型接口的两种使用方式

  • 实现类给出具体类型
  • 实现类延续泛型,创建实现类对象时再确定类型

实现类给出具体类型

class MyArrayList2 implements List<String> {
    // 重写方法的代码
}

实现类延续泛型,创建实现类对象时再确定类型

class MyArrayList3<E> implements List<E> {
    // 重写方法的代码
}

5、泛型的继承和通配符

泛型的继承
泛型不具备继承性,但是数据具备继承性

泛型的通配符:?(就是一个问号)

  • 表示不确定的类型
  • 可以限定类型的范围
    • ? extends E:表示可以传递E或E的所有子类类型
    • ? super E:表示可以传递E或E的所有父类类型

应用场景

  • 如果我们在定义类、方法、接口的时候,类型不确定,就可以定义泛型类,泛型方法,泛型接口
  • 如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可使用泛型的通配符

二十八、集合进阶

集合体系结构

  1. Collection(单列集合)
    在添加数据时每次只添加一个数据

  2. Map(双列集合)
    在添加数据时每次添加一对数据

集合体系结构

二十八-1、Collection单列集合(接口)

1、Collection集合概述

Collection集合体系

  • Collection为接口,List和Set是它的子接口,其余分别为它们的实现类
  • 其中Vector已淘汰,直接无视它

Collection集合体系
Collection集合概述

  • 是单例集合的顶层(祖宗)接口,它表示一组对象,这些对象也称为Collection的元素,它的功能可以被全部单列集合继承使用
  • JDK 不提供此接口的任何直接实现,它提供更具体的子接口(如Set和List)实现

List系列集合添加元素的特点

  • 有序:存和取的顺序是一样的
  • 可重复:集合中存储的元素可以重复
  • 有索引:可以通过索引获取集合中的每一个元素

Set系列集合添加元素的特点

  • 无序:存和取的顺序有可能不一样
  • 不可重复:集合中不能存储重复的元素(可用于数据的去重)
  • 无索引:不能通过索引获取元素

创建Collection集合的对象

  • 多态的方式
  • 具体的实现类,如ArrayList

Collection集合常用方法

方法名说明
boolean add(E e)添加元素
boolean remove(Object o)从集合中移除指定的元素,删除成功返回ture,删除失败返回false
boolean removeIf(Object o)根据条件进行移除
void clear()清空集合中的元素
boolean contains(Object o)判断集合中是否存在指定的元素,底层是依赖equals方法进行判断是否存在,所有,如果集合中存储的是自定义对象,要在JavaBean类中重写equals方法
boolean isEmpty()判断集合是否为空
public int size()集合的长度,也就是集合中元素的个数

对于Collection单列集合只需关注以下五种
方框中是对名字的解释,方便记忆理解
单列集合

2、Collection集合的遍历

Collection集合的遍历方式有三种

  • 迭代器遍历
  • 增强for遍历
  • Lambda表达式遍历

2.1、迭代器遍历

迭代器介绍

  • 迭代器不依赖索引
  • 迭代器是集合的专用遍历方式
  • 迭代器在Java中的类是Iterator,是一个接口
  • 通过Iterator<E> iterator()方法获取迭代器,方法返回迭代器对象,默认指向当前集合的0索引
  • Iterator中的常用方法
方法名说明
boolean hasNext()判断当前位置是否有元素,有返回ture,没有返回false
E next()获取当前位置的元素,并将迭代器对象移向下一个索引位置

示例

public class IteratorDemo1 {
    public static void main(String[] args) {
        // 创建集合对象
        Collection<String> c = new ArrayList<>();
  
        // 添加元素
        c.add("hello");
        c.add("world");
        c.add("java");
        c.add("javaee");
  
        // Iterator<E> iterator():返回此集合中元素的迭代器,通过集合的iterator()方法得到
        Iterator<String> it = c.iterator();
  
        // 用while循环改进元素的判断和获取
        while (it.hasNext()) {
            String s = it.next();
            System.out.println(s);
        }
    }
}

运行结果:

hello
world
java
javaee

注意

  • 使用迭代器进行遍历时,hashNext()方法和next()方法要配套使用,而且是一一对应使用
  • 迭代器遍历完毕,指向的位置不会复位,再使用只能获取一个新的迭代器对象
  • 迭代器遍历时,不能用集合的方法进行增加或者删除,可以用迭代器提供的remove()方法进行删除

迭代器中删除的方法remove()可以删除迭代器对象当前指向的元素

public class IteratorDemo2 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("b");
        list.add("c");
        list.add("d");
  
        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            String s = it.next();
            if("b".equals(s)){
                //指向谁,那么此时就删除谁.
                it.remove();
            }
        }
        System.out.println(list);
    }
}

运行结果:

[a, c, d]

2.2、增强for遍历

  • 介绍

    • 增强for的底层就是迭代器,为了简化迭代器的代码书写
    • 它是JDK5之后出现的,其内部原理是一个Iterator迭代器
    • 实现Iterable接口的类才可以使用迭代器和增强for
    • 所有的数组和单列集合Collection才能用增强for遍历
  • 格式

    for(元素的数据类型 变量名 : 数组或集合) {// 已经将当前遍历到的元素封装到变量中了,直接使用变量即可}
    
  • 代码

    public class MyCollectionDemo1 {
        public static void main(String[] args) {
            ArrayList<String> list =  new ArrayList<>();
            list.add("a");
            list.add("b");
            list.add("c");
            list.add("d");
            list.add("e");
            list.add("f");
    
            // 数据类型一定是集合或者数组中元素的类型
            // str仅仅是一个变量名而已,在循环的过程中,依次表示集合或者数组中的每一个元素
            // list就是要遍历的集合或者数组
            for(String str : list){
                System.out.println(str);
            }
        }
    }
    

    运行结果:

    a
    b
    c
    d
    e
    f
    

2.3、Lambda表达式遍历

得益于JDK8开始的新技术Lambda表达式,提供了一种更简单、更直接的遍历集合的方式。Lambda表达式遍历就是使用了一个forEach方法进行遍历

foreach方法
default void forEach(Consumer<? super T> action) 结合lambda表达式进行遍历

import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Consumer;

public class CollectionDemo1 {
    public static void main(String[] args) {
        // 创建集合并添加元素
        Collection<String> coll = new ArrayList<>();
        coll.add("Pig");
        coll.add("Dog");
        coll.add("Cat");

        // 先使用匿名内部类的形式
        coll.forEach(new Consumer<String>() {
            @Override
            // s依次表示集合中的每一个数据
            public void accept(String s) {
                System.out.println(s);
            }
        });
        
        // 简化的lambda表达式形式
        coll.forEach( s -> System.out.println(s)
        );
    }
}

/*
forEach底层原理:
方法的底层其实也会自己遍历集合,依次得到每一个元素
把得到的每一个元素,传递给下面的accept方法
s依次表示集合中的每一个数据
 */

运行结果:

Pig
Dog
Cat
Pig
Dog
Cat

3、List集合(接口)

3.1、List的特有方法

方法介绍

方法名描述
void add(int index, E element)在此集合中的指定位置插入指定的元素
E remove(int index)删除指定索引处的元素,返回被删除的元素
E set(int index, E element)修改指定索引处的元素,返回被修改的元素,原索引处的元素依次后移
E get(int index)返回指定索引处的元素

示例

import java.util.ArrayList;
import java.util.List;

public class ListDemo1 {
    public static void main(String[] args) {
        // 创建一个集合
        List<String> list = new ArrayList<>();

        // 添加元素
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        // 在索引1处添加元素,原来的元素依次后移
        list.add(1,"X");
        // 删除索引为3的元素
        list.remove(3);
        System.out.println(list);
    }
}

运行结果:

[aaa, X, bbb]

3.2、List的遍历

List集合的遍历方式

  • 迭代器遍历
  • 列表迭代器遍历(ListIterator接口)
  • 增强for遍历
  • Lambda表达式遍历
  • 普通for循环(因为List集合存在索引)

其中列表迭代器遍历和普通for循环遍历是List集合特有的遍历方式

五种遍历方式对比
List集合的五种遍历方式对比

列表迭代器遍历(ListIterator接口)和增强for遍历示例

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class ListDemo2 {
    public static void main(String[] args) {
        // 创建集合对象
        List<String> list = new ArrayList<>();

        // 添加元素
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");

        // 普通for循环遍历
        for (int i = 0; i < list.size(); i++) {
            String s = list.get(i);
            System.out.println(s);
        }

        System.out.println("--------");

        // 列表迭代器(ListIterator接口)
        // 它额外添加了一个方法,在遍历的过程中,可以添加元素
        ListIterator<String> it = list.listIterator();
        while (it.hasNext()) {
            String str = it.next();
            if ("bbb".equals(str)) {
                // 只能用迭代器的方法添加元素
                it.add("qqq");
            }
        }
        System.out.println(list);
    }
}

运行结果:

aaa
bbb
ccc
--------
[aaa, bbb, qqq, ccc]

3.3、List子类的特点

ArrayList集合
底层是数组结构实现,查询快、增删慢

LinkedList集合
底层是链表结构实现,查询慢、增删快

3.4、ArrayList集合(实现类)

底层原理

  • 利用空参构造创建的集合,在底层创建一个默认长度为0的数组
  • 添加第一个元素时,底层会创建一个新的长度为10的数组
  • 存满时,会扩容1.5倍
  • 如果一次添加多个元素,1.5倍还放不下,则新创建数组的长度以实际为准

3.5、LinkedList集合(实现类)

LinkedList集合的底层数据结构是双链表,查询慢,增删快,但是如果操作的是首位元素,速度也很快

LinkedList集合的特有方法

方法名说明
public void addFirst(E e)在该列表开头插入指定的元素
public void addLast(E e)将指定的元素追加到此列表的末尾
public E getFirst()返回此列表中的第一个元素
public E getLast()返回此列表中的最后一个元素
public E removeFirst()从此列表中删除并返回第一个元素
public E removeLast()从此列表中删除并返回最后一个元素

4、Set集合(接口)

4.1、概述

在Collection体系中的位置
Collection单列集合体系
Set集合的实现类

  • HashSet:无序、不重复、无索引
  • LinkedHashSet:有序、不重复、无索引
  • TreeSet:可排序、不重复、无索引

4.2、HashSet集合(实现类)

HashSet的特点

  • HashSet集合底层采取哈希表存储数据
  • 哈希表是一种对于增删改查数据性能都较好的结构

哈希表组成

  • JDK8之前:数值 + 链表
  • JDK8开始:数据 + 链表 + 红黑树

哈希值

  • 哈希值可以理解为对象的整数表现形式
  • 哈希值是根据hashCode方法算出来的int类型的整数
  • 该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算
  • 一般情况下,会重写hashCode方法,利用对象内部的属性值计算哈希值

对象的哈希值的特点

  • 如果没有重写hashCode方法,不同对象计算出的哈希值是不同的
  • 如果已经重写hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
  • 在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样(哈希碰撞)

如果没有重写hashCode方法,不同对象计算出的哈希值是不同的

public class HashSetDemo1 {
    public static void main(String[] args) {
        // 创建对象
        Student s1 = new Student(18, "M");
        Student s2 = new Student(18, "M");

        // 如果没有重写hashCode方法,不同对象计算出的哈希值是不同的
        System.out.println(s1.hashCode()); // 990368553
        System.out.println(s2.hashCode()); // 1096979270
    }
}

// 标准JavaBean
class Student {
    private int age;
    private String name;

    public Student() {
    }

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

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    public String toString() {
        return "Student{age = " + age + ", name = " + name + "}";
    }
}

如果已经重写hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的

import java.util.Objects;

public class HashSetDemo1 {
    public static void main(String[] args) {
        // 创建对象
        Student s1 = new Student(18, "M");
        Student s2 = new Student(18, "M");

        // 如果已经重写hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
        System.out.println(s1.hashCode()); // 1596
        System.out.println(s2.hashCode()); // 1596
    }
}

// 标准JavaBean
class Student {
    private int age;
    private String name;

    public Student() {
    }

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

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }

    public String toString() {
        return "Student{age = " + age + ", name = " + name + "}";
    }
}

HashSet集合存储自定义类型元素,要想实现元素的唯一,要求必须重写hashCode方法和equals方法

4.3、LinkedHashSet集合(实现类)

LinkedHashSet集合是HashSet集合的子类

LinkedHashSet的特点

  • 有序、不重复、无索引
  • 这里的有序指的是保证存储和取出的元素顺序一致
  • 原理:底层数据结构依然是哈希表,只是每个元素又额外多了一个双链表,用来记录存储的顺序

注意
如果要数据去重,一般使用HashSet,如果要求去重且存取有序,才使用LinkedHashSet

4.4、TreeSet集合(实现类)

TreeSet的特点

  • 可排序、不重复、无索引
  • 可排序:对于数值类型,按照元素的默认规则(由小到大)排序;对于字符、字符串类型,按照字符在ASCII码表中的数字升序进行排序
  • TreeSet集合底层是基于红黑树实现排序的,增删改查的性能都比较好

TreeSet的排序规则

  • 方式一:默认排序(自然排序),空参构造TreeSet()
    JavaBean类实现Comparable接口指定比较规则
  • 方式二:比较器排序,带参构造TreeSet(Comparator comparator)
    创建TreeSet对象的时候传递Comparator的实现类对象,重写compare方法,根据返回值进行排序

使用原则

  • 一般使用第一种,如果第一种不能满足当前需求,就使用第二种
  • 如果两种方式都存在,Java会使用第二种

空参构造示例1

import java.util.TreeSet;

public class TreeSetDemo {
    public static void main(String[] args) {
        // 使用空参构造创建TreeSet集合对象
        TreeSet<Integer> ts = new TreeSet<>();

        // 添加元素
        ts.add(4);
        ts.add(5);
        ts.add(1);
        ts.add(3);
        ts.add(2);

        // 打印集合
        System.out.println(ts);
    }
}

运行结果:

[1, 2, 3, 4, 5]

空参构造示例2

import java.util.TreeSet;

public class TreeSetDemo2 {
    public static void main(String[] args) {
        // 创建集合对象
        TreeSet<Person> ts = new TreeSet<>();

        // 创建自定义类对象,实现了Comparable<>泛型类,按年龄从小到大排序
        Person p1 = new Person("zhangsan", 13);
        Person p2 = new Person("lisi", 14);
        Person p3 = new Person("wangwu", 15);

        // 添加元素
        ts.add(p3);
        ts.add(p2);
        ts.add(p3); // 重复元素,不会添加
        ts.add(p1);
        
        // 打印集合
        System.out.println(ts);
    }
}

// 自定义类实现Comparable泛型接口,重写compareTo方法,根据返回值进行排序
class Person implements Comparable<Person>{
    private String name;
    private int age;

    public Person() {
    }

    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 toString() {
        return "Person{name = " + name + ", age = " + age + "}";
    }

    @Override
    // 重写compareTo方法,按年龄升序排序
    public int compareTo(Person o) {
        return this.getAge() - o.getAge();
    }
    /*
    this:表示当前要添加的元素
    o:表示已经在红黑树中存在的元素
    如果返回值为负数,表示当前存入的元素是较小值,存红黑树的左边
    如果返回值为0,表示当前存入的元素跟集合中元素重复了,不存
    如果返回值为正数,表示当前存入的元素是较大值,存红黑树的右边
     */
}

运行结果:

[Person{name = zhangsan, age = 13}, Person{name = lisi, age = 14}, Person{name = wangwu, age = 15}]

带参构造示例

import java.util.Comparator;
import java.util.TreeSet;

public class Test5 {
    public static void main(String[] args) {
        Person p1 = new Person("zs", 18);
        Person p2 = new Person("ls", 28);
        Person p3 = new Person("ww", 38);
        // 创建集合对象,使用比较器排序
        TreeSet<Person> ts = new TreeSet<>(new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge(); // 按年龄升序
            }
            // o1:表示当前要添加的元素
            // o2:表示已经在红黑树中存在的元素
        });
        
        // 添加元素
        ts.add(p2);
        ts.add(p1);
        ts.add(p3);
        
        // 打印集合
        System.out.println(ts);
    }
}

// 标准JavaBean
class Person {
    private String name;
    private int age;

    public Person() {
    }

    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 toString() {
        return "Person{name = " + name + ", age = " + age + "}";
    }
}

运行结果:

[Person{name = zs, age = 18}, Person{name = ls, age = 28}, Person{name = ww, age = 38}]

5、总结

  1. 如果想要集合中的元素可重复
    用ArrayList集合,基于数组(用的最多)

  2. 如果想要集合中的元素可重复,而且当前的增删操作明显多于查询
    用LinkedList集合,基于链表

  3. 如果想对集合中的元素去重
    用HashSet集合,基于哈希表(用的最多)

  4. 如果想对集合中的元素去重,而且保证存取有序
    用LinkedHashSet集合,基于哈希表和双链表,效率低于HashSet

  5. 如果想对集合中的元素进行排序
    用TreeSet集合,基于红黑树

二十八-2、Map双列集合(接口)

1、Map集合概述

Map集合的特点

  • 双列集合一次需存一对数据,分别为键和值,并且一个键对应一个值
  • 键不可以重复,值可以重复
  • 一个键和一个值组成的整体称为键值对,也叫键值对对象,在Java中也叫Entry对象

Map体系
其中Hashtable和Properties与IO相关,不在此介绍
Map集合体系
Map的常见方法

方法名说明
V put(K key, V value)添加元素,如果键不存在,直接把键值对添加到Map集合中,返回null;如果键存在,会把原有的键值对对象覆盖,并把覆盖的值返回
V remove(Object key)根据键删除键值对元素,并把值返回
void clear()移除所有的键值对元素
boolean containsKey(Object key)判断集合是否包含指定的键
boolean containsValue(Object value)判断集合是否包含指定的值
boolean isEmpty()判断集合是否为空
int size()集合的长度,也就是集合中键值对的个数

示例

import java.util.HashMap;
import java.util.Map;

public class MapDemo1 {
    public static void main(String[] args) {
        // 创建Map集合的对象
        Map<String, String> m = new HashMap<>();

        // 添加元素
        m.put("1", "One");
        m.put("2", "Two");
        System.out.println(m.put("3", "Four"));
        System.out.println(m.put("3", "Three"));

        // 打印集合
        System.out.println(m);

        // 删除元素
        System.out.println(m.remove("1"));
        System.out.println(m);

        // 清空元素
        m.clear();
        System.out.println(m);
    }
}

运行结果:

null
Four
{1=One, 2=Two, 3=Three}
One
{2=Two, 3=Three}
{}

2、Map集合的遍历

Map集合的遍历方式有三种

  • 键找值
  • 键值对
  • Lambda表达式

2.1、键找值

所谓键找值就是通过键找值,步骤如下

  1. 获取所有的键,把这些键放到一个单列集合中
  2. 遍历单列集合,得到每一个键(可以用迭代器,增强for,Lambda表达式)
  3. 利用键获取对应的值

相关方法
V get(Object key) 根据键获取值

示例

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MapDemo2 {
    public static void main(String[] args) {
        // 创建Map集合对象
        Map<String, String> m1 = new HashMap<>();

        // 添加元素
        m1.put("1", "One");
        m1.put("2", "Two");
        m1.put("3", "Three");

        // 通过键找值遍历
        // 1、获取所有的键,把这些键放到一个单列集合中
        Set<String> keys = m1.keySet();
        // 2、遍历单列集合,得到每一个键,这里用增强for
        for(String key : keys) {
            System.out.print("键:" + key);
            // 3、利用键获取对应的值,用get方法
            System.out.println(" 值:" + m1.get(key));
        }
    }
}

运行结果:

键:1 值:One
键:2 值:Two
键:3 值:Three

2.2、键值对

所谓键值对就是通过键值对对象进行遍历,先依次获取所有的键值对对象,再通过getKey()方法获取里面的键,通过getValue()方法获取里面的值

相关方法

  • Set<Map.Entry<K, V>> entrySet() 获取所有键值对对象的集合
  • V getValue() 返回与此项对应的值
  • K getKey() 返回与此项对应的键

示例

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MapDemo3 {
    public static void main(String[] args) {
        // 创建Map集合对象
        Map<String, String> m1 = new HashMap<>();

        // 添加元素
        m1.put("1", "One");
        m1.put("2", "Two");
        m1.put("3", "Three");

        // 通过键值对遍历
        // 1、通过一个entrySet()方法获取所有的键值对对象
        Set<Map.Entry<String, String>> entries = m1.entrySet();
        // 2、遍历entries这个集合,得到里面的每一个键值对对象
        for (Map.Entry<String, String> entry : entries) {
            // 用entry调用get方法分别获取键和值
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println("键:" + key + " 值:" + value);
        }
    }
}

运行结果:

键:1 值:One
键:2 值:Two
键:3 值:Three

2.3、Lambda表达式

相关方法
default void forEach(BiConsumer<? super K, ? super V> action) 结合lambda遍历Map集合
forEach底层其实就是利用第二种方式进行遍历,依次得到每一个键和值,再调用accept方法

示例

import java.util.HashMap;
import java.util.Map;
//import java.util.function.BiConsumer;

public class MapDemo4 {
    public static void main(String[] args) {
        // 创建Map集合对象
        Map<String, String> map = new HashMap<>();

        // 添加元素
        map.put("1", "One");
        map.put("2", "Two");
        map.put("3", "Three");

        /* 先使用匿名内部类遍历
        map.forEach(new BiConsumer<String, String>() {
            @Override
            public void accept(String key, String value) {
                System.out.println("键:" + key + " 值:" + value);
            }
        });
        */
        
        // 利用Lambda表达式遍历
        map.forEach(
                (key, value) -> System.out.println("键:" + key + " 值:" + value)
        );
    }
}

运行结果:

键:1 值:One
键:2 值:Two
键:3 值:Three

3、HashMap集合(实现类)

HashMap的特点

  • 由键决定:无序、不重复、无索引
  • HashMap和HashSet一样,底层都是哈希表结构
  • 当创建一个HashMap对象时,在顶层,会创建一个长度为16,
  • 依赖hashCode方法和equals方法保证键的唯一
  • 如果存储的是自定义对象,需要重写hashCode和equals方法,但是如果存储的是自定义对象,就不需要写hashCode和equals方法

4、LinkedHashMap集合(实现类)

LinkedHashMap的特点

  • LinkedHashMap是HashMap的子类
  • 由键决定:有序、不重复、无索引
  • 有序指的是存储和取出元素的顺序一致
  • 原理:底层数据结构依然是哈希表,只是每个键值对元素又额外多了一个双链表,用来记录存储的顺序

5、TreeMap集合(实现类)

TreeMap的特点

  • TreeMap底层和TreeSet一样,也是红黑树结构
  • 由键决定特性:不重复,无索引,可排序
  • 默认按照键的大小进行排序,也可以自己规定键的排序规则

两种排序规则

  1. 实现Comparable接口,指定比较规则
  2. 创建集合时传递Comparator比较器对象,指定比较规则

二十八-3、其它

1、可变参数

JDK5之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化

格式
数据类型...args

底层
可变参数底层其实就是一个数组,只不过不需要我们自己创建,Java会帮我们创建好,使用时就当成数组用

示例

public class ArgsDemo {
    public static void main(String[] args) {
        System.out.println(getSum(1, 3, 56, 13, 2)); // 75
    }

    // 参数为可变参数,完成求和功能
    public static int getSum(int... args) {
        int sum = 0;
        for (int i = 0; i < args.length; i++) {
            sum += args[i];
        }
        return sum;
    }
}

注意

  1. 在方法的形参中只能有一个可变参数
  2. 如果方法中有多个参数,可变参数要放到最后

2、Collections工具类

java.utils.Collections是集合的工具类,注意和Colection单列集合区分

常用方法

  • public static <T> boolean addAll(Collection<T> c, T... elements):批量添加元素
  • public static void shuffle(List<?> list):打乱List集合元素的顺序
  • public static <T> void sort(List<T> list):将集合中元素按照默认规则排序
  • public static <T> void sort(List<T> list, Comparator<T> ):根据指定的规则进行排序

3、不可变集合

3.1、什么是不可变集合

不可变集合就是不可以被修改的集合,创建完成后,长度不能改,内容也不能改

应用场景

  • 如果某个数据不能被修改,把它防御性第拷贝到不可变集合中是个很好的选择
  • 当集合对象被不可信的库调用时,不可变形式是安全的
  • 简单理解就是,如果不想让别人修改集合中的内容,就可以提供一个不可变集合,别人拿到这个不可变集合就只能进行查询操作,不能修改

不可变集合分类
在List、Set、Map接口中,都存在静态的of方法,可以获取一个不可变的集合

  • static <E> List<E> of(E... elements):创建一个指定元素的List集合对象
  • static <E> Set<E> of(E... elements):创建一个指定元素的Set集合对象
  • static <K, V> Map<K, V> of(E... elements):创建一个指定元素的Map集合对象

3.2、不可变的list集合

static <E> List<E> of(E... elements):创建一个指定元素的List集合对象

示例

import java.util.Iterator;
import java.util.List;

public class ImmutableDemo {
    public static void main(String[] args) {
        // 创建不可变的List集合,不能修改
        List<String> list = List.of("Li", "Mi", "Xi");

        // 可以查询
        System.out.println(list.get(0));
        System.out.println(list.get(1));
        System.out.println(list.get(2));

        System.out.println("-----分隔线-----");

        // 可以遍历
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

运行结果:

Li
Mi
Xi
-----分隔线-----
Li
Mi
Xi

3.3、不可变的Set集合

static <E> Set<E> of(E... elements):创建一个指定元素的Set集合对象

示例

import java.util.Set;

public class ImmutableDemo2 {
    public static void main(String[] args) {
        // 创建不可变的Set集合,不能修改
        Set<String> set = Set.of("Li", "Mi", "Xi"); // 参数要唯一

        // 可以遍历
        for (String s : set) {
            System.out.println(s);
        }
    }
}

运行结果:

Xi
Li
Mi

3.4、不可变的Map集合

static <K, V> Map<K, V> of(E... elements):创建一个指定元素的Map集合对象

示例

import java.util.Map;
import java.util.Set;

public class ImmutableDemo3 {
    public static void main(String[] args) {
        // 创建不可变的Map集合,不能修改
        Map<String, String> map = Map.of(
                "浙江", "浙",
                "江苏", "苏",
                "上海", "沪",
                "北京", "京"
        ); // 参数有上限,最多20个,即10对键值对

        // 遍历
        Set<String> keys = map.keySet();
        for (String key : keys) {
            System.out.println(key + "=" + map.get(key));
        }
    }
}

注意

  • Map.of方法的参数最多20个,也就是10对键值对
  • 如果要将参数也设计为可变参数,那么当我们传参的时候,键和值都是可变参数,但是,一个方法的可变参数只能有一个,因为可变参数要放在参数列表的最后,两个可变参数不可能同时在最后的位置

如果我们要传的参数大于20个怎么办?思路是将键值对看作一个整体,有两种方式

  • JDK10以前:public static <K, V> Map<K, V> ofEntries(Map.Entry<? extends K, ? extends V>... entries)
  • JDK10及以后:public static <K, V> Map<K, V> copyOf(Map<? extends K, ? extends V> map)

示例

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class ImmutableDemo4 {
    public static void main(String[] args) {
        // 创建普通的Map集合
        Map<String, String> hm = new HashMap();

        // 添加元素
        hm.put("浙江", "浙");
        hm.put("江苏", "苏");
        hm.put("上海", "沪");
        hm.put("北京", "京");
        hm.put("四川", "川");
        // 利用上面的数据来获取一个不可变集合

        // 1、获取所有键值对对象(Entry对象)
        Set<Map.Entry<String, String>> entries = hm.entrySet();

        // 2、转换成数组
        Map.Entry[] arr = entries.toArray(new Map.Entry[0]);
        // toArray方法在底层会比较集合的长度跟数组的长度,
        // 如果集合的长度 > 数组的长度:数据在数组中放不下,此时会根据实际数据的个数重新创建数据
        // 如果集合的长度 < 数组的长度:数据在数组中放得下,不会创建新数据,而是直接用

        // 3、使用ofEntries方法,map就是一个不可变集合
        Map map = Map.ofEntries(arr);

        // 上面3步的简化写法,map2就是一个不可变集合
        Map<Object, Object> map2 = Map.ofEntries(hm.entrySet().toArray(new Map.Entry[0]));

        // JDK10开始的copyOf方法,map3就是一个不可变集合
        Map<String, String> map3 = Map.copyOf(hm);
    }
}

4、Stream流(接口)

Stream流的作用
结合了Lambda表达式,简化集合、数组的操作

4.1、Stream流初体验

案例示例

/*
创建集合添加元素,完成以下需求:
1、把所有以“张”头的元素存储到新集合中
2、把以“张”开头的,长度为3的元素存储到新集合中
3、遍历打印最终结果
*/

import java.util.ArrayList;

public class StreamDemo1 {
    public static void main(String[] args) {
        // 创建集合
        ArrayList<String> list1 = new ArrayList<>();

        // 添加元素
        list1.add("张无忌");
        list1.add("周芷若");
        list1.add("赵敏");
        list1.add("张强");
        list1.add("张三丰");

        // 1、把所有以“张”头的元素存储到新集合中
        ArrayList<String> list2 = new ArrayList<>();
        for (String name : list1) {
            if (name.startsWith("张")) {
                list2.add(name);
            }
        }

        // 2、把以“张”开头的,长度为3的元素存储到新集合中
        ArrayList<String> list3 = new ArrayList<>();
        for (String name : list2) {
            if (name.length() == 3) {
                list3.add(name);
            }
        }

        // 3、遍历打印最终结果
        for (int i = 0; i < list3.size(); i++) {
            System.out.println(list3.get(i));
        }

        System.out.println("-----分隔线-----");

        // 使用Stream流
        list1.stream().filter(name -> name.startsWith("张")).filter(name -> name.length() == 3).forEach(name -> System.out.println(name));
    }
}

运行结果:

张无忌
张三丰
-----分隔线-----
张无忌
张三丰

4.2、获取Stream流

Stream流的使用步骤

  1. 先得到一条Stream流(流水线),并把数据放上去
  2. 使用中间方法对流水线上的数据进行操作
  3. 使用终结方法对流水线上的数据进行操作

中间方法和终结方法

  • 中间方法:方法调用完毕后,还可以调用其它方法,如:过滤,转换
  • 终结方法 :Stream流中的最后一步,调用完毕后,不能调用其它方法,如:输出,打印

生成Stream流的方式

  • Collection单列集合
    使用Collection中的默认方法:default Stream<E> stream()
  • Map双列集合
    无法直接使用Stream流,需要通过keySet()或entrySet()先转成Collection单列集合,再获取Stream流
  • 数组
    通过Arrays工具类中的静态方法Arrays.stream()
  • 同种数据类型的多个数据(一堆零散数据)
    通过Stream接口中的静态方法public static<T> Stream<T> of(T... values)

Collection单列集合获取Stream流示例

import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Consumer;
import java.util.stream.Stream;

public class StreamDemo2 {
    public static void main(String[] args) {
        // Collection单列集合获取Stream流
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "a", "b", "c", "b", "e", "f");
        // 获取到一条流水线,并把集合中的数据放到流水线上
        Stream<String> stream1 = list.stream();
        stream1.forEach(new Consumer<String>() {
            @Override
            public void accept(String s) {
                // s依次表示流水线上的每一个数据
                System.out.print(s + " ");
            }
        });
        System.out.println();

        // 一般按下面这种写法
        list.stream().forEach(s -> System.out.print(s + " "));
    }
}

运行结果:

a b c b e f 
a b c b e f 

Map双列集合获取Stream流示例

import java.util.HashMap;

public class StreamDemo3 {
    public static void main(String[] args) {
        HashMap<String, Integer> hm = new HashMap<>();
        hm.put("aaa", 111);
        hm.put("bbb", 222);
        hm.put("ccc", 333);
        hm.put("ddd", 444);
        hm.put("eee", 555);

        // 获取Stream流,第一种方式
        hm.keySet().stream().forEach(s -> System.out.println(s));

        // 获取Stream流,第二种方式
        hm.entrySet().stream().forEach(s -> System.out.println(s));
    }
}

运行结果:

aaa
ccc
bbb
eee
ddd
aaa=111
ccc=333
bbb=222
eee=555
ddd=444

数组获取Stream流示例

import java.util.Arrays;

public class StreamDemo4 {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        
        // 获取Stream流
        Arrays.stream(arr). forEach(s -> System.out.println(s));
    }
}

运行结果:

1
2
3
4
5

同种数据类型的多个数据(一堆零散数据)获取Stream流示例

import java.util.stream.Stream;

public class StreamDemo5 {
    public static void main(String[] args) {
        // 基本数据类型
        Stream.of(1, 2, 3, 4, 5).forEach(s -> System.out.print(s + " "));
        System.out.println();
        // 引用数据类型
        Stream.of("a", "b", "c", "d").forEach(s -> System.out.print(s + " "));
        /*
        Stream接口中静态方法of的细节:
        方法的形参是一个可变参数,可以传递一堆零散的数据,也可以传递数组
        但是数组必须是引用数据类型的,如果是基本数据类型,会把整个数组当成一个元素,放到Stream中
        所以一般不要用of方法传递数组
        */
    }
}

运行结果:

1 2 3 4 5 
a b c d

4.3、Stream流的中间方法

常见方法

方法名说明
Stream<T> filter(Predicate predicate)用于对流中的数据进行过滤
Stream<T> limit(long maxSize)获取前几个元素
Stream<T> skip(long n)跳过前几个元素
static <T> Stream<T> concat(Stream a, Stream b)合并a和b两个流为一个流
Stream<T> distinct()元素去重(底层依赖hashCode方法和equals方法)
Stream<R> map(Function<T, R> mapper)转换流中的数据类型

注意

  • 中间方法,会返回新的Stream流,原来的Stream流只能用一次,建议使用链式编程
  • 修改Stream流中的数据,不会影响原来集合或者数组中的数据

4.4、Stream流的终结方法

常见方法

方法名说明
void forEach(Consumer action)对此流的每个元素执行操作,也就是遍历
long count()返回此流中的元素数
toArray()收集流中的元素,放到数组中
R collect(Collector collector)收集流中的元素,放到集合中

forEach和count方法示例

import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Consumer;

public class StreamDemo6 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "唱", "跳", "rap", "篮球", "Music");

        // void forEach(Consumer action)
        // 1、直接调用
        list.stream().forEach(new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.print(s + " ");
            }
        });
        System.out.println(); // 换行

        // 2、使用Lambda表达式的方式
        list.stream().forEach(s -> System.out.print(s + " "));
        System.out.println();

        // long count()
        System.out.println(list.stream().count());
    }
}

运行结果:

唱 跳 rap 篮球 Music 
唱 跳 rap 篮球 Music 
5

4.5、Stream流的收集操作

对数据使用Stream流的方式操作完毕后,可以把流中的数据收集到集合中,属于Stream流的终结方法

常用方法

方法名说明
toArray()收集流中的元素,放到数组中
R collect(Collector collector)收集流中的元素,放到集合中

工具类Collectors提供了具体的收集方法

方法名说明
public static <T> Collector toList()把元素收集到List集合中
public static <T> Collector toSet()把元素收集到Set集合中
public static Collector toMap(Function keyMapper, Function valueMapper)把元素收集到Map集合中

toArray方法示例

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.function.IntFunction;

public class StreamDemo7 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "唱", "跳", "rap", "篮球", "Music");

        // toArray方法
        // 1、空参,返回Object类型的数组
        Object[] arr = list.stream().toArray();
        System.out.println(Arrays.toString(arr));

        // 2、带参,返回指定类型的数组
        String[] strings = list.stream().toArray(new IntFunction<String[]>() {
            @Override
            public String[] apply(int value) {
                return new String[value];
            }
        });
        System.out.println(Arrays.toString(strings));
        /*
        InterFunction泛型:具体类型的数组
        apply的形参:流中数据的个数,要跟数组的长度保持一致
        apply的返回值:具体类型的数组
        方法体:创建数组并返回
        toArray方法的参数:租用就是创建一个指定类型的数组
        toArray方法的底层:会依次得到流里面的数据,并把数据放到数组中
        toArray方法的返回值:是一个装着流里面所以数据的数组
        也可以换成Lambda表达式的写法,更简洁
         */
    }
}

运行结果:

[,, rap, 篮球, Music]
[,, rap, 篮球, Music]

collect方法示例

5、方法引用

5.1、概述

方法引用

  • 把已经有的方法拿过来,当做函数式接口中抽象方法的方法体
  • 使用方法引用比Lambda表达式的简化方式还要简洁

方法引用的使用规则

  • 引用处必须是函数式接口
  • 被引用的方法必须已存在
  • 被引用方法的形参和返回值需要跟抽象方法保持一致
  • 被引用方法的功能要能满足当前的需求

示例

import java.util.Arrays;
import java.util.Comparator;

public class FunctionDemo1 {
    public static void main(String[] args) {
        // 需求:创建一个数组,进行倒序排序
        Integer[] arr = {3, 4, 5, 1, 2, 6};
        
        // 匿名内部类
        Arrays.sort(arr, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        System.out.println(Arrays.toString(arr));

        // Lambda表达式
        Arrays.sort(arr, (Integer o1, Integer o2) -> {
            return o2 - o1;
        });
        System.out.println(Arrays.toString(arr));

        // Lambda表达式的简化格式
        Arrays.sort(arr, (o1, o2) -> o2 - o1);
        System.out.println(Arrays.toString(arr));

        // 方法引用
        Arrays.sort(arr, FunctionDemo1::subtraction);
        System.out.println(Arrays.toString(arr));
    }

    // 将要被引用的方法
    public static int subtraction(int num1, int num2) {
        return num2 - num1;
    }
}

运行结果:

[6, 5, 4, 3, 2, 1]
[6, 5, 4, 3, 2, 1]
[6, 5, 4, 3, 2, 1]
[6, 5, 4, 3, 2, 1]

5.2、方法引用符

::该符号(两个冒号)为引用运算符,而它所在的表达式被称为方法引用

5.3、方法引用的分类

方法引用的分类

  1. 引用静态方法
  2. 引用成员方法
    • 引用其它类的成员方法
    • 引用本类的成员方法
    • 引用父类的成员方法
  3. 引用构造方法
  4. 其它调用方式
    • 使用类名引用成员方法
    • 引用数组的构造方法

5.4、引用静态方法

格式
类名::静态方法名

范例
Integer::parseInt

示例

// 集合中有以下数字,要求把它们都变成int型
// "1" "2" "3" "4" "5"

import java.util.ArrayList;
import java.util.Collections;

public class FunctionDemo2 {
    public static void main(String[] args) {
        // 创建集合,将list1中的元素转为int型装进newList中
        ArrayList<String> list1 = new ArrayList<>();
        ArrayList<Integer> list2 = new ArrayList<>();
        ArrayList<Integer> list3 = new ArrayList<>();
        Collections.addAll(list1, "1", "2", "3", "4", "5");

        // 常规方式
        for (int i = 0; i < list1.size(); i++) {
            int temp = Integer.parseInt(list1.get(i));
            list2.add(temp);
        }
        System.out.println(list2);
        list2.clear();

        // 方法引用
        list1.stream().map(Integer::parseInt).forEach(s -> list2.add(s));
        System.out.println(list2);
    }
}

运行结果:

[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]

5.5、引用成员方法

引用成员方法又分为

  • 引用其它类的成员方法
  • 引用本类的成员方法
  • 引用父类的成员方法

格式:对象::成员方法名

  • 其它类:其它类对象::方法名
  • 本类:this::方法名
  • 父类:super::方法名

引用处不能是静态方法,因为静态方法中没有this

引用其它类的成员方法示例

/*
集合中有一些名字,按要求过滤数据
数据:"张无忌", "周芷若", "赵敏", "张强", "张三丰"
要求:只要以“张”开头的,而且名字是三个字
 */

import java.util.ArrayList;
import java.util.Collections;

public class FunctionDemo3 {
    public static void main(String[] args) {
        // 创建集合,添加数据
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰");

        // java中没有直接能满足需求的方法,所以我们可以自己写一个
        list.stream().filter(Tool::stringJudge).forEach(s -> System.out.println(s));
    }
}

// 在类中自己写一个成员方法
class Tool {
    // 将被引用的成员方法
    public static boolean stringJudge(String s) {
        return s.startsWith("张") && s.length() == 3;
    }
}

运行结果:

张无忌
张三丰

5.6、引用构造方法

为什么要引用构造方法
就是为了创建对象

格式
类名::new

范例
Student::new 就是创建一个Student类的对象

5.6、其它调用方式-使用类名引用成员方法

该方式的规则

  • 需要有函数式接口
  • 被引用的方法必须已经存在
  • 被引用的方法的形参,需要跟抽象方法的第二个形参到最后一个形参保持一致,如果没有第二个参数,说明被引用的方法需要是无参的成员方法
  • 返回值需要保持一致
  • 被引用方法的功能需要满足当前的需求

格式
类名::成员方法名

范例
String::substring

5.7、其它调用方式-引用数组的构造方法

概述
Java的底层有一个类,专门用来描述数组,它里面也有构造方法,只不过这个类是Java底层已经提供好的,当我们创建一个数据,其实就是调用了这个构造方法,我们只要知道引用数组的构造方法就是创建一个数组

格式
数据类型[]::new

范例
int[]::new

示例

/*
集合中存储一些整数,收集到数组中
 */

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;

public class FunctionDemo4 {
    public static void main(String[] args) {
        // 创建集合,添加元素
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list, 1, 2, 3, 4, 5);

        // 收集到数组中,数组的类型需要跟流中数据的类型保持一致
        Integer[] arr = list.stream().toArray(Integer[]::new);
        System.out.println(Arrays.toString(arr));
    }
}

运行结果:

[1, 2, 3, 4, 5]

二十九、static关键字

static 是静态的意思,它是一个修饰符,可以修饰成员变量,也可以修饰成员方法

被其修饰的成员的特点

  1. 该成员被类的所有对象所共享
  2. 多了一种调用方式,可以通过类名进行调用(推荐使用类名调用)
  3. 随着类的加载而加载,优先于对象存在(在创建对象之前就可通过类名调用到静态成员使用)

成员变量什么时候加static

  • 需要共享数据
  • 该变量需要被所有对象所共享

成员方法什么时候加static
常用于制作工具类,工具类中的成员方法都要加 static

工具类
不是描述事物,而是帮我们完成事情的类(打工),这种类我们称之为工具类。对工具类我们一般直接用类名调用方法,不需要创建对象。所以,对于工具类,一般要私有化其构造方法(加private关键字),就不能创建该类的对象了

总结
就是所写的方法需要让调用者更方便地调用,希望可以通过类名直接调用,就可以加static

注意

  • 在静态方法中,只能直接访问静态成员变量。若要访问非静态的成员变量,需要把对象创建出来,用对象名进行访问,相当于间接访问
  • static中不允许使用this关键字(this代表当前对象的引用,肯定要创建对象之后,才能使用this关键字)

重新认识 main 方法public static void main(String[] args) {}

  • public:被JVM调用,访问权限要足够大
  • static:被JVM调用,为了JVM调的方便,不用去创建对象
  • void:被JVM调用,不需要给JVM返回值
  • main:一个通用的名称,虽然不是关键字,但是能被JVM识别
  • String[]:一个字符串的数组,以前用来做键盘录入,现在有Scanner了,就没什么用了,只是作为一个传统被保留下来
  • args:数组名,可以改

如果编写的方法和主方法平级,该方法要加static,因为主方法是静态的,所以,为了能在主方法中调用其它方法,这些方法也得是静态的

三十、继承

1、继承的格式

关键字:extends

格式:
public class 子类名 extends 父类名 {
}

示例:
public class Son extends Father {
}

2、创建类的细节

  • 一个 .java 文件中可以编写多个类(建议还是一个 .java 文件对应一个类)
  • 需要保证类与类之间是平级关系
  • 一个 .java 文件中只能有一个类能被 public 修饰,因为加了 public ,就限制该类名要与文件名必须保持一致

3、继承中的成员访问特点

成员变量
子父类中,如果出现了同名的成员变量,根据就近原则,优先使用子类的。但如果要用父类的,加一个 super 作为标识。如:super.成员变量名

成员方法
子父类中,如果出现了方法声明一模一样的方法(即方法名、参数、返回值都相同),在创建子类对象,调用方法时,会优先使用子类的方法逻辑(大括号里的内容),这虽然是就近原则的现象,但其实是子类的方法,对父类的方法,进行了重写操作

构造方法

  • 构造方法不能被继承(所以没有重写的说法,但是可以重载)
  • 子类初始化之前,需要先完成父类的初始化,所以子类要先访问父类的空参构造方法
  • 在所有类的构造方法里的第一行代码,包括父类,但除了 Object 类,都隐藏了一句代码:super(); 通过这行代码,子类会访问父类的空参数构造方法,从而完成父类的初始化
  • 其实Java中所有的类,都直接或者间接的继承到了 Object 类,Object 类是最大的祖宗
public class Test05 {
    public static void main(String[] args) {
        Son s1 = new Son();
        Son s2 = new Son(10);
    }
}

class Father {

    public Father() {
 	   	// super();     Java中所有类的构造函数中(除了Object类)都隐藏了这句代码
        System.out.println("Father类的空参构造方法");
    }

    public Father(int x) {
    	// super();     Java中所有类的构造函数中(除了Object类)都隐藏了这句代码
        System.out.println("Father类的带参构造方法");
    }
}

class Son extends Father {

    public Son() {
        // super();     隐藏的代码
        System.out.println("Son类的空参构造方法");
    }

    public Son(int x) {
        // super();     隐藏的代码
        System.out.println("Son类的带参构造方法");
    }
}

运行结果:

Father类的空参构造方法
Son类的空参构造方法
Father类的空参构造方法
Son类的带参构造方法

4、方法重写

先做一个区分,以免混淆

  • 方法重载(Overload):即函数重载。已经很熟悉了,不多说明
  • 方法重写(Override):是继承中的概念。子父类中出现方法声明一模一样的方法(返回值、方法名,参数列表都相同,只是方法的大括号内的内容不同),子类会重写该方法

方法重写的使用场景
当子类需要使用父类的方法,但是觉得父类的方法逻辑不好,需要修改或增强,就可以对父类的方法进行重写

注意

  • 父类中私有方法不能被重写
  • 子类重写父类方法时,访问权限必须大于等于父类(private < default < protected < public)

5、Java中继承的特点

Java只支持单继承,不支持多继承(C类继承了A类,就不能再继承B类),但是支持多层继承(B类继承A类,C类又继承B类)

继承内存图

三十一、this 和 super 关键字

  • this:代表当前类对象的引用(地址)
  • super:代表父类存储空间的标识

简单记就是:

  • this调本类的成员
  • super调父类的成员
关键字访问成员变量访问成员方法访问构造方法
thisthis.本类成员变量;this.本类成员方法;this(); this(带参);
supersuper.父类成员变量;super.父类成员方法;super(); super(带参);

this调用本类构造方法的使用场景:开发中的开闭原则:对功能扩展做开放,对修改代码做关闭

用this() 和 super() 访问构造方法时,都必须放在构造方法的第一行,所以二者不能共存

三十二、final 关键字

final 关键字在Java中是一个修饰符,可以修饰 方法变量

1、final 修饰的特点

  • 修饰方法:表明该方法是最终方法,不能被重写
  • 修饰类:表明该类是最终类,不能被继承
  • 修饰变量:表明该变量是常量,不能再被赋值

2、final 修饰变量的一些细节

  • 修饰基本数据类型的变量:数据值不可改变
  • 修饰引用数据类型的变量:地址值不可改变,但是内容可以修改
  • 修饰成员变量:该变量要么在定义的时候初始化(推荐),要么在构造方法中赋值

3、final 修饰变量的命名规范

  • 如果变量是一个单词,所有字母大写。如:MAX
  • 如果变量是多个单词,所有字母大写,中间使用下划线分隔。如:MAX_VALUE

三十三、抽象类

1、抽象类介绍

关键字:abstract

抽象类是一种特殊的父类,内部可以编写抽象方法

如果一个类中存在抽象方法,那么该类就必须声明为抽象类

抽象类的子类必须重写父类的抽象方法

2、什么是抽象方法

当我们将共性的方法,抽取到父类后,发现这个方法无法在父类中给出具体明确的代码(描述不清了),而且这个方法还是子类必须要有的方法,就可将该父类设计为抽象类

【与其说是抽取,其实是父类有多个子类,并且这些子类都重写了父类中的同一个方法,并且各有特点,而父类中的此方法有不好写,就干脆不写了,大括号一删,分号一加,定义为抽象方法,(这种形式很像C++中的函数声明)所以此类也必须定义为抽象类,而它的子类就必须重写该抽象方法】

abstract class Animal {         // 抽象类
    public abstract void eat(); // 抽象方法
}

class Cat extends Animal{
    public void eat() {
        System.out.println("Cat eat");  // 重写父类的抽象方法
    }
}

class Dog extends Animal{
    public void eat() {
        System.out.println("Dog eat");  // 重写父类的抽象方法
    }
}

3、抽象类的注意事项

1. 抽象类不能实例化,也就是不能创建对象
如果抽象类可以创建对象,就可以调用内部没有方法体的方法(抽象方法),但这样做是没有意义的。既然不能创建对象,那还有没有构造方法呢?(毕竟构造方法的本质作用就是创建对象,上篇笔记的第二十一点有所提及)

2. 抽象类存在构造方法
存在的意义:让子类能通过 super 进行访问

// 父类
abstract class Fu {
    public Fu() {} // 父类的构造方法
    public abstract void show(); // 抽象方法
}

// 子类继承父类
class Zi extends Fu{

    public Zi() { // 子类的构造方法
        super(); // 访问父类的构造方法
    }

    @Override
    public void show() {

    }
}

3. 抽象类可以存在普通方法
存在意义:让子类继承到,进行使用

4. 作为抽象类的子类,要么重写抽象类中的所有抽象方法,要么也是一个抽象类
抽象方法要么存在抽象类种,要么就被重写。如果子类不重写父类中的抽象方法,那就要把它声明为一个抽象类。因为子类其实也继承了父类中的抽象方法,抽象方法要存活在抽象类中,那可以把子类也变成抽象类,就可以不用重写重写方法了
【就是说一个类,它继承了一个抽象类,那它必须重写这个抽象类中的抽象方法,否则编译器会报错。还有一种办法就是上面说的,将这个类也变成一个抽象类,就可以不重写抽象方法(这种情况很少)】

4、abstract 关键字的冲突

和以下这些关键字冲突,不能写在一起

  • final
    被 final 修饰的类不能被继承,也就没有子类来重写父类中的抽象方法
  • private
    被 private 修饰的方法子类无法访问,也就没法重写
  • static
    被 static 修饰的方法可以用类名调用,类名调用抽象方法没有意义

三十四、接口

1、接口介绍

  • Java中非常重要的技术:接口,会用到 interface 和 implements 关键字
  • 编写类使用 class 关键字,编写接口只需将 class 关键字换成 interface
  • 接口,体现的思想是对规则的声明,Java中的接口更多体现的是对行为的抽象,也就是抽象方法
  • 如果一个类中都是抽象方法,且没有成员变量(可以有成员常量),没有普通方法(包括构造方法)。这种类,我们通常会设计为Java中的接口。因为这个类存在的唯一价值,就只是声明规则了
  • 接口不允许实例化,也就是不能创建对象。那该怎么使用接口呢?就是找类实现接口,实现这套规则
  • 接口和类之间是实现关系
  • 实现了接口的类,叫做“实现类”,也就多了一个名字而已。实现类也可以看作接口的子类,应该说接口是“实现类”的干爹
interface Inter {           // 创建一个接口
    public abstract void show();        // 抽象方法
    public abstract void method();      // 抽象方法
}

class test2 implements Inter{    // 创建一个类实现接口,该类也叫“实现类”

    @Override
    public void show() {        // 重写接口中的抽象方法

    }

    @Override
    public void method() {      // 重写接口中的抽象方法

    }
}

【和抽象类相似,一个接口的实现类,(我们可以称这个接口是此实现类的“干爹”)要么重写抽象类中的所有抽象方法,要么也是一个抽象类,理由和上面的“抽象类的注意事项”中描述的差不多。理一下接口和抽象类的概念。一切都是从继承开始,单说继承很简单。接着是抽象类,抽象类就是一个父类,(因为它必须有子类去重写抽象方法)这就用到了继承的概念,但它是一个特殊的父类,里面存在抽象方法,所以它的子类必须重写抽象方法,或是,也变成一个抽象类(这种情况很少,因为还要在创建一个,应该叫孙类,来重写抽象方法)。而接口是什么,就是一个抽象类,只不过这个抽象类中全是抽象方法,没有变量,没有构造方法(但可以有常量,下面有写到),因为很特殊,我们给了它一个“接口”的名字,所以 接口也可以理解为一个类。再看,继承一个抽象类会用 extends 关键字,也就是普通的继承所用的关键字,而接口,只是将 extends 换成了 implements ,而我们称这个使用了implements 的类叫做实现类,毕竟不是严格的继承关系,算是干爹和侄子的关系】

2、接口中的成员特点

  1. 构造方法
    接口中没有构造方法
  2. 成员变量
    只能定义常量,我们用定义变量的方式定义所谓“变量”其实是常量,因为系统会默认在前面加入三个关键字:public、static、final
  3. 成员方法
    只能是抽象方法,因为系统会默认在前面加入两个关键字:public、abstract

3、接口和类之间的关系

  1. 类和类之间可以存在的关系:继承关系,只支持单继承,不支持多继承,可以多层继承
  2. 类和接口之间可以存在的关系:实现关系,可以单实现,可以多实现,甚至可以在继承一个类的同时,实现多个接口
  3. 接口和接口之间可以存在的关系:继承关系,可以单继承,可以多继承

示例

// 接口A
interface A {
    void showA();
}

// 接口B
interface B {
    void showB();
}

// 类C,多实现,同时实现接口A和B。可理解为同时认了A,B两个干爹
class C implements A, B {

    @Override
    public void showA() {

    }

    @Override
    public void showB() {

    }
}

// 接口D,并且同时继承了接口A和B
interface D extends A, B {
    void showD();
}	// 若要创建类实现这个接口,那么要重写三个抽象方法:showA,showB,showD

三十五、多态

多态就是同类型的对象,表现出不同形态

1、多态现象分为两种

1.1、对象多态

父类引用指向子类对象

好处
将一个方法的形参定义为父类类型,这个方法就可以就接收到该父类(或干爹)的任意子类对象了。提高程序的扩展性

1.2、行为多态

定义方法的时候,使用父类作为类型参数,可以接收所有子类对象

好处
同一个方法,具有多种不同表现形式或形态的能力,提高了程序的扩展性

2、多态前提

以下三个条件要同时满足

  • 有继承或实现关系
  • 有方法重写
  • 有父类引用指向子类对象,或接口类引用指向实现类对象

3、多态的成员访问特点

  1. 使用多态创建对象调用成员变量的特点
    多态创建完对象,调用成员变量时,在编译时会检查父类中有没有这份数据,如果有,编译成功,没有的话,编译就会报错,运行时走的也是父类中的数据

  2. 使用多态创建对象调用成员方法的特点
    编译时会检查父类中有没有此方法,如果没有,编译会出错,如果有此方法,编译通过,但运行时一定执行走子类的方法逻辑。因为担心你调用的方法在父类中是一个抽象方法,所以为了使调用有意义,一定会走子类的方法逻辑

  3. 使用多态创建对象调用静态成员的特点
    先说一个细节:静态成员推荐使用类名调用,虽然可以使用对象名调用,但这是一种假象,代码在生成字节码文件后,会自动将对象名调用改成类名调用
    所以说:使用类名调用静态成员,类名是谁,调用的就是谁的逻辑,类名是父类的,就调用的父类的逻辑,类名是子类的,就调用子类的逻辑

1、2点示例

public class PolymorphismDemo {
    public static void main(String[] args) {
        
        Fu f = new Zi();            // 父类引用指向子类对象
        
        System.out.println(f.num);  // 10
                                    // 调用对象内部的成员变量,使用的是父类中的成员

        f.show();                   // Zi...show,
                                    // 运行时走的子类的代码逻辑
    }
}

class Fu {

    int num = 10;                   // 父类中的num

     public void show() {           // 父类中的show方法
         System.out.println("Fu...show");
     }
}

class Zi extends Fu {

    int num = 20;                   // 子类中的num

    public void show() {            // 重写父类的show方法
        System.out.println("Zi...show");
    }
}

总结
Fu f = new Zi();

  1. 访问成员变量:编译和运行都看左边(父类)
  2. 访问成员方法:编译看左边(父类),运行看右边(子类)。走的是重写后的方法逻辑
  3. 多态创建对象,调用静态成员:调的是父类中的成员

4、多态的转型

先看一个多态的弊端:不能使用子类的特有成员

结合多态的成员访问特点,调用成员变量或成员方法时,编译期间,会检查父类中有没有这个成员,如果要调子类特有的成员,父类中肯定没有,编译就会报错

要解决这个问题,需要使用到多态的转型,解决该问题使用第二种

多态的转型分为两种
1、向上转型:从子到父(父类引用指向子类对象)

Fu f = new Zi();

2、向下转型:从父到子(将父类引用所指向的对象,转交给子类类型)

Zi z = (Zi)f; // 需要强制转换

多态中转型时可能存在的安全隐患:ClassCaseException:类型转换异常
发生在在引用数据类型的强制转换中,实际类型和目标类型不匹配就会出现此异常

解决方法:使用 instanceof 关键字作一个判断
关键字 instanceof:判断左边的引用,是否是右边的数据类型,返回 true 或 false

示例

public class Test {
    public static void main(String[] args) {
        Animal animal = new Cat();
        animal.eat();

        Cat cat = (Cat)animal; // 向下转型,才能使用特有的方法
        cat.hunt();

        if (animal instanceof Dog) {
            Dog dog = (Dog)animal;
            dog.lookHome();
        } else {
            System.out.println("转换失败");
        }

        // JDK14新特性:可以将判断和强转合在一起写
        if (animal instanceof Dog d) {
            d.lookHome();
        }
    }
}

class Animal {
    public void eat() {
        System.out.println("吃东西");
    }
}

class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("Cat吃东西");
    }

    public void hunt() {
        System.out.println("Cat抓老鼠");
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("Dog吃东西");
    }

    public void lookHome() {
        System.out.println("Dog看家");
    }
}

运行结果:

Cat吃东西
Cat抓老鼠
转换失败

三十六、接口新特性

1、JDK8的新特性

接口中可以定义有方法体的方法(默认,静态),在实现类中可以不重写这两类方法

  • 允许在接口中定义非抽象方法,但需要用 defaulet 关键字修饰,这些方法就是默认方法

    • 默认方法的定义格式
      public default 返回值类型 方法名(参数列表) {}
    • 示例
      public class InterfaceDemo {
          public static void main(String[] args) {
              InterImpl ii = new InterImpl();
              ii.show();
          }
      }
      
      interface Inter {
          default void show() {
              System.out.println("接口的默认方法");
          }
      }
      
      class InterImpl implements Inter {
         // 可以不重写接口中的show()方法,当然也可重写,需要去掉default关键字
      }
      
    • 注意
      如果实现了多个接口,且多个接口中存在相同的默认方法,此时实现类必须重写默认方法
  • 允许定义静态方法,既然已经允许接口带有方法体了,干脆也放开静态方法,但只允许用接口名调用,不允许实现类通过对象调用

2、JDK9的新特性

  • 接口中可以定义私有方法
    接口中的多个默认方法中有相同的代码,此时可以将相同的代码抽取出来写成一个方法,在每个默认方法中调用该方法,所以,这个抽取出来的方法只是接口内部使用,不希望暴露给其它类使用,可以加上 private 关键字设为私有
    interface Inter {
        // 私有方法,用private就不能写default,但效果和不写相同,可以有代码体
        private void fun() {
            System.out.println("这是默认方法");
        }
      
        default void show1() {
            fun(); // 调用私有方法
        }
    
        default void show2() {
            fun(); // 调用私有方法
        }
    }
    

三十七、代码块

使用 {} 括起来的代码称为代码块

分类

  • 局部代码块
  • 构造代码块
  • 静态代码块
  • 同步代码块(和多线程相关)

1、局部代码块(了解)

位置
方法中的一对大括号

作用
限定变量的声明周期,提早的释放内存

示例

public static void main(String[] args) {
        {
            int num = 10;
            System.out.println(num);
        }
        // System.out.println(num); num变量的内存已释放,不能使用
    }

2、构造代码块(了解)

位置
类中,方法外的一对大括号

特点
在创建对象,执行构造方法的时候,就会执行构造代码块(优先于构造方法执行)

作用
将多个构造方法中,重复的代码,抽取到构造代码块中,从而提升代码的复用性

示例

public class Test2 {
    public static void main(String[] args) {
        Student stu1 = new Student();
        Student stu2 = new Student(10);
    }
}

class Student {
    {
        // 类中,方法外的一对大括号
        System.out.println("构造代码块");
    }

    public Student() {
        System.out.println("空参构造");
    }

    public Student(int num) {
        System.out.println("带参构造");
    }
    
}

运行结果:

构造代码块
空参构造
构造代码块
带参构造

3、静态代码块

位置
类中,方法外的一对大括号,再括号前多加一个 static 关键字

特点
随着类的加载而执行,因为类只加载一次,所有也只执行一次

作用
对数据进行初始化(当需要初始化的数据是一个对象,而且这个对象的初始化很复杂,就可以借助静态代码块来初始化)

示例

public class Test2 {
    public static void main(String[] args) {
        Student stu1 = new Student();
        Student stu2 = new Student(10);
    }
}

class Student {
    static {
        // 类中,方法外的一对大括号,多加一个static关键字
        System.out.println("静态static代码块");
    }

    public Student() {
        System.out.println("空参构造");
    }

    public Student(int num) {
        System.out.println("带参构造");
    }

}

运行结果:

静态static代码块
空参构造
带参构造

三十八、内部类

内部类就是定义在一个类里面的类

1、成员内部类(了解)

class Outer {
    // 局部内部类
    class Inner {
        
    }
}
  • 创建对象的格式
    外部类名.内部类名 对象名 = new 外部类对象().new 内部类对象();
    例:Outer.Inner in = new Outer().new Inner();

  • 成员访问细节
    内部类中,访问外部类成员:直接访问,包括私有
    外部类中,访问内部类成员:需要创建对象访问

2、静态内部类(了解)

有 static 修饰的成员内部类

class Outer {
    // 静态内部类
    static class Inner {

    }
}
  • 创建对象的格式
    外部类名.内部类名 对象名 = new 外部类名.内部类对象();
    例:Outer.Inner in = new Outer.Inner();
  • 注意
    静态只能访问静态
    public class Test2 {
        public static void main(String[] args) {
            Outer.Inner.show();
        }
    }
    
    class Outer {
        int num1 = 10;
        static int num2 = 20; // 静态变量
    
        // 静态内部类
        static class Inner {
            public static void show() {
                System.out.println(num2); // 可以直接访问
                // System.out.println(num1); 不能直接访问,需要创建对象访问
                Outer outer = new Outer();
                System.out.println(outer.num1);
            }
        }
    }
    
    运行结果:
    20
    10
    

3、局部内部类(了解且鸡肋)

放在方法、代码块、构造器等执行体中

public class Test2 {
    public static void main(String[] args) {
        A a = new A();
        a.show();
    }
}

class A {
    public void show() {
        // 局部内部类
        class B {
            public void fun() {
                System.out.println("fun()...");
            }
        }
        B b = new B();
        b.fun();
    }
}

运行结果:

fun()...

4、匿名内部类

概述

  • 匿名内部类本质上是一个特殊的局部内部类(定义在方法内部)
  • 匿名就是没有名字的意思,所以匿名内部类是一个没有名字的类

前提
需要存在一个接口或类

格式
new 类名/接口() {}

  • new 类名() {} 代表继承这个类,这个整体是继承了这个类的对象,{}才是真正的匿名内部类
  • new 接口() {} 代表实现这个接口,这个整体是实现了这个接口的对象,{}才是真正的匿名内部类

先看一个问题,再来详细看匿名内部类
当一个方法的参数是接口,应当传入什么,没错,应该传入该接口的实现类对象(多态)

public class Test2 {
    public static void main(String[] args) {
       uerInter(new InterImpl());
    }

    public static void uerInter(Inter i) { // Inter i = new InterImpl();
        i.show();
    }
}

interface Inter {
    void show();
}

class InterImpl implements Inter {

    @Override
    public void show() {
        System.out.println("Override");
    }
}

运行结果:

Override

为了调用uerInter()方法,先编写了一个类实现接口,然后重写了show()方法,再创建实现类对象传到方法中,如果用匿名内部类,一步就可以解决

public class Test2 {
    public static void main(String[] args) {
        // 使用匿名内部类
       uerInter(new Inter() {
           public void show() {
               System.out.println("重写方法");
           }
       });
    }

    public static void uerInter(Inter i) {
        i.show();
    }
}

interface Inter {
    public void show();
}

运行结果:

重写方法

作用
可以让代码更加简洁,在定义类的时候就对其进行实例化。但是,如果接口中的抽象方法很多,再用匿名内部类,代码就会显得臃肿,这时就最好写一个实现类来传入实现类对象,所以在抽象方法不是很多时使用匿名内部类可以让代码更简洁

匿名内部类用法

public class Test00 {
    public static void main(String[] args) {
        // 使用匿名内部类后可以直接赋给接口对象
        Person p = new Person() {
            @Override
            public void fun() {
                System.out.println("调用了fun()方法1号");
            }
        };
        p.fun(); // 用对象调用方法

        // 也可以直接调用重载后的方法
        new Person() {
            @Override
            public void fun() {
                System.out.println("调用了fun()方法2号");
            }
        }.fun();
    }
}

// 接口/函数式接口
interface Person {
    void fun();
}

运行结果:

调用了fun()方法1号
调用了fun()方法2

三十九、Lambda表达式

1、基本用法

概述

  • Lambda表达式是JDK8开始后的一种新语法形式
  • 并不是所有的匿名内部类都用Lambda表达式作简化,Lambda表达式只能操作函数式编程接口(有且仅有一个抽象方法的接口)

作用
简化匿名内部类的代码写法

格式

简化格式:
() -> {}

具体格式:
(匿名内部类被重写方法的形参列表) -> {
	被重写方法的方法体代码
}

注:-> 是语法格式,无实际含义

示例

public class Test2 {
    public static void main(String[] args) {
        // 使用Lambda表达式
       uerInter(() -> {
               System.out.println("重写方法");
       });
    }

    public static void uerInter(Inter i) {
        i.show();
    }
}

interface Inter {
    public void show();
}

2、Lambda表达式的省略写法

规则

  • 参数类型可以不写
  • 如果只有一个参数,参数类型可以省略,同时 () 也可以省略
  • 如果Lambda表达式的方法体只有一行代码,可以省略大括号不写,同时要省略分号(;),此时,如果这行代码是return语句,必须省略return不写

3、Lambda表达式和匿名内部类的区别

  • 使用限制不同
    匿名内部类:可以操作类、接口
    Lambda:只能操作函数式接口(有且仅有一个抽象方法的接口)

  • 实现原理不同
    匿名内部类:编译之后,产生一个单独的 .class 文件
    Lambda:编译之后,没有一个单独的 .class 文件

四十、Object类

1、介绍

  • 所有的类,都直接或间接的继承了Object类(祖宗类)
  • Object类的方法是一切子类都可以直接使用的

2、toString成员方法

返回该对象的字符串表示

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

/*
getClass().getName:全类名(包名+类名)
"@":就是一个分隔符
Integer.toHexString:转十六进制
hashCode():返回的是对象内存地址+哈希算法,算出来的整数值(哈希值)
*/

细节
使用打印语句,打印对象名的时候,println()方法,源码层面,会自动调用该对象的toString方法

package test;

public class Test1 {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(a);            // 直接打印对象名
        System.out.println(a.toString()); // 调用toString方法
    }
}

class A {

}

运行结果:

test.A@282ba1e
test.A@282ba1e

重写toString方法
父类 toString() 方法存在的意义就是被子类重写,以便返回对象的内容信息,而不是地址信息,这样直接打印对象名或者调用toString方法就能返回属性信息

package test;

public class Test1 {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(a);
        System.out.println(a.toString());
    }
}

class A {
    @Override
    public String toString() {
        return "重写toString方法,返回对象的属性信息";
    }
}

运行结果:

重写toString方法,返回对象的属性信息
重写toString方法,返回对象的属性信息

总结

  • 直接输出对象,默认输出的是地址其实没有意义,所以,我们一般会重写toString方法,把对象的属性拼接为字符串返回
  • 将来如果打印一个对象名,得到的不是一串地址,说明这个类肯定重写了toString方法

3、equals成员方法

细节
使用 == 比较两个对象,比较的是地址

Object类中的equals方法
public boolean equals(Object obj)
用于对象之间的比较,返回true或false,默认比较的是对象内存地址,通常会在子类中重写equals方法,让对象之间比较内容

package test;

public class Test1 {
    public static void main(String[] args) {
        Person p1 = new Person(18, "LI");
        Person p2 = new Person(18, "LI");
        // 没有重写equals方法,直接比较对象,两个对象的地址肯定不同
        System.out.println(p1.equals(p2)); // false
    }
}

class Person {
    private int age;
    private String name;

    public Person() {
    }

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

重写equals方法

package test;

public class Test1 {
    public static void main(String[] args) {
        Person p1 = new Person(18, "LI");
        Person p2 = new Person(18, "LI");
        // 简单重写了equals方法,比较对象的内容
        System.out.println(p1.equals(p2)); // true
    }
}

class Person {
    private int age;
    private String name;

    public Person() {
    }

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

    @Override
    public boolean equals(Object obj) { // Object obj = p2 多态
        Person p = (Person)obj; // 向下转型,为了调用子类特有的成员
        return this.age == p.age && this.name == p.name;
    }
}

重写equals方法的改进版

package test;

public class Test1 {
    public static void main(String[] args) {
        Person p1 = new Person(18, "LI");
        Person p2 = new Person(18, "LI");
        System.out.println(p1.equals(p2)); // true
        System.out.println(p1.equals("和String对象比较")); // false
    }
}

class Person {
    private int age;
    private String name;

    public Person() {
    }

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

    @Override
    public boolean equals(Object obj) {
        /*
         判断obj的类型是否正确
         如果对象类型不同,内容肯定不同,直接返回false
         类型相同再作比较
         从而避免出现异常
         */
        if (obj instanceof Person) {
            Person p = (Person)obj; // 向下转型
            return this.age == p.age && this.name == p.name;
        } else {
            return false;
        }
    }
}

四十一、Objects类

概述
Objects 类与 Object 类也是继承关系,Objects 类是从JDK1.7开始之后才有的
需要导包import java.util.Objects;

Objects 类的常见方法

方法名说明
public static boolean equals(Object a, Object b)比较两个对象,底层会先进行非空判断,从而可以避免空指针异常,再进行equals比较
public static boolean isNull(Object obj)判断变量是否为null(鸡肋)

细节
Objects 类的 equals 方法内部还是依赖我们重写的 equals 方法,所以,如果我们没有重写 equals 方法,它还是比较的对象地址,好处就是它内部带有一个非空判断,避免空指针异常

package test;

import java.util.Objects; // 导包

public class Test1 {
    public static void main(String[] args) {
        Person p1 = new Person(18, "LI");
        Person p2 = new Person(18, "LI");
        /*
         使用Objects类的equals方法比较对象
         内部还是依赖我们自己重写的equals方法
         只是它内部会有一个非空判断
         避免空指针异常
         所以我们还是要自己重写equals方法
         */
        System.out.println(Objects.equals(p1, p2)); // true
        System.out.println(Objects.equals(p1, "String对象")); // false
    }
}

class Person {
    private int age;
    private String name;

    public Person() {
    }

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

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Person) {
            Person p = (Person)obj;
            return this.age == p.age && this.name == p.name;
        } else {
            return false;
        }
    }
}

四十二、Math类

包含执行基本数字运算的方法
常见成员方法

方法名说明
public static int abs(int a)返回 int 值的绝对值
public static double ceil(double a)向上取整
public static double floor(double a)向下取整
public static int round(float a)四舍五入
public static double pow(double a, double b)返回a的b次幂
public static double random()返回double类型的随机值,范围 [ 0.0,1.0 )

四十三、System类

常用成员方法

方法名说明
pubic static void exit(int status)终止当前运行的Java虚拟机,非零表示异常终止
public static long currentTimeMillis()返回当前系统的时间毫秒值形式(返回1970年1月1日0时0分0秒到现在所经历的时间毫秒值)
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束

arraycopy 方法说明
Object src:数据源数组
int srcPos:起始索引
Object dest:目的数组
int destPos:起始索引
int length:拷贝的个数

四十四、BigDecimal类

导包import java.math.BigDecimal;
BigDecimal 类用于解决小数运算中的不精确问题

public class MathDemo {
    public static void main(String[] args) {
        double num1 = 0.1;
        double num2 = 0.2;
        System.out.println(num1 + num2); // 0.30000000000000004
    }
}

创建对象

1public BigDecimal(double val) // 不推荐,计算不精确
2public BigDecimal(String val)
3public static BigDecimal valueOf(double val)

示例

package demo;

import java.math.BigDecimal;

public class MathDemo {
    public static void main(String[] args) {
        // 第2种方式
        BigDecimal bd1 = new BigDecimal("0.1");
        BigDecimal bd2 = new BigDecimal("0.2");
        System.out.println(bd1.add(bd2)); // 0.3

        // 第3种方式
        BigDecimal.valueOf(0.1);
        BigDecimal.valueOf(0.2);
        System.out.println(bd1.add(bd2)); // 0.3
    }
}

常用成员方法
加法:BigDecimal add(BigDecimal augend)
减法:BigDecimal subtract(BigDecimal subtrahend)
乘法:BigDecimal multiply(BigDecimal multiplicand)
除法:BigDecimal divide(BigDecimal divisor) 除不尽会报错
除法:BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)

  • 除法参数说明
    BigDecimal divisor:另一个BigDecimal对象
    int scale:小数点后精确多少位
    int roundingMode:舍入模式

  • 常见舍入模式
    RoundingMode.HALF_UP:四舍五入
    RoundingMode.UP:进一法,后面的不要,向前加一
    RoundingMode.DOWN:后面的不要

四十五、包装类

将基本数据类型,包装成类(变成引用数据类型),就可以调用方法解决问题

八种基本数据类型都有对应的包装类

基本数据类型引用数据类型
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

下面以 Integer 类为例介绍包装类

1、Integer 类

手动装箱:调用方法,手动将基本数据类型,包装成类
public Integer(int value) 通过构造方法(不推荐)
public static Integer valueOf(int i) 通过静态方法

手动拆箱:调用方法,手动将包装成类,拆成基本数据类型
public int intValue() 以 int 类型返回该 Integer 的值

public class IntegerDemo {
    public static void main(String[] args) {
        // 手动装箱
        int num = 10;
        Integer i1 = Integer.valueOf(num); // 将基本数据类型包装成类
        System.out.println(i1); // 10
        
        // 手动拆箱
        int res = i1.intValue(); // 将包装类拆成基本数据类型
        System.out.println(res); // 10
    }
}

从JDK5开始,出现了自动拆装箱
自动装箱:可以将基本数据类型,直接赋值给包装类的变量
自动拆箱:可以将包装类的数据,直接赋值给基本数据类型变量

public class IntegerDemo3 {
    public static void main(String[] args) {
        // 自动装箱
        int num = 28;
        Integer integer1 = 18;
        Integer integer2 = num;
        System.out.println(integer1); // 18
        System.out.println(integer2); // 28

        // 自动拆箱
        int i1 = integer1; // 18
        int i2 = integer2; // 28
        System.out.println(i1);
        System.out.println(i2);
    }
}

Integer 类常用方法

方法名说明
public static String toBinaryString(int i)转二进制
public static String toOctalString(int i)转八进制
public static String toHexString(int i)转十六进制
public static int parseInt(String s)将数字字符串,转换为数字

2、细节题

细节
使用==比较两个对象,比较的是地址

看下面的代码

public class IntegerDemo2 {
    public static void main(String[] args) {
        Integer i1 = 127;
        Integer i2 = 127;
        Integer i3 = 128;
        Integer i4 = 128;

        System.out.println(i1 == i2); // true
        System.out.println(i3 == i4); // false
    }
}

结论
自动装箱的时候,如果装箱的数据范围,是 -128~127,==比较的结果是true,反之都是false

解释

  • 自动装箱的原理:系统自动帮我们调用了 Integer.valueOf(int i) 方法
  • valueOf方法源码:
    public static Integer valueOf(int i) {
    	  if (i >= -128 && i <=127) {
      	  return IntegerCache.cache[i + (-IntegerCache.low)]
      	  }
        return new Integer(i);
    }
    
  • Integer 类中,底层存在一个长度为256个大小的数组,Integer[] cache,在数组中,存储了256个Integer对象,范围是 -128~127
  • 如果自动装箱的数据的范围在 -128~127 之间,不会创建新的对象,而是从底层的数组中,取出一个提前创建好的对象返回,如果不在,就会重新 new 出新的 Integer 对象并且返回
  • 因为自动装箱的代码用的很频繁,为了在-128~127的数值范围中不频繁创建对象,这样设计也是为了节省内存的消耗

四十六、Arrays 工具类

工具类,专门用于操作数组元素

常用成员方法

方法名说明
public static String toString(类型[ ] a)将数组元素拼接为带有格式的字符串(就是打印出每个数组元素)
public static boolean equals(类型[ ] a, 类型[ ] b)比较两个数组内容是否相同
public static int binarySearch(int[ ] a, int key)查找元素在数组中的索引(用的是二分查找,所以需要保证数组中元素是有序的)
public static void sort(类型[ ] a)对数组进行默认升序排序

四十七、正则表达式

本质就是一个字符串,可以指定一些规则,来校验其它字符串

1、字符类(默认匹配一个字符)

[abc] 只能是a,b,c 其中之一([ ] 中括号代表对单个字符作限制

[^abc] 除了a,b,c之外的任何字符

[a-zA-Z] a到z,A到Z

[a-zA-Z0-9] a到z,A到Z,数字0-9都可以

[a-d[m-p]] a到d之间,或m到p之间都可以(也可以写成[a-dm-p]

[a-z&&[def]] a-z的范围中,只能是d,e,f(鸡肋)

[a-z&&[^bc]] a-z,除了b和c

[a-z&&[^m-p]] a到z,除了m-p

2、预定义字符类

正则表达式中:

.(英文标点的句号) 代表任意字符

\ 转义字符

\d 一个数字字符:[0-9](使用时需要转义,即\\d ,以下相同)

\D 一个非数字字符:[^0-9]

\s 一个空白字符

\S 非空白字符:[^\s]

\w 英文,数字,下划线:[a-zA-Z_0-9]

\W 除了英文,数字,下划线:[^\w]

3、数量

在内容规则限定后面加上:

+ 一次或多次

? 一次或零次

* 任意次数

{n} 正好n次

{n,} 至少n次

{n,m} 至少n次但不超过m次

示例

public class RegexTest {
    public static void main(String[] args) {
        /*
        QQ号正则:
        不能以0开头
        全部都是数字
        5~12位
         */
        String qqRegex = "[1-9]\\d{4,11}";
        System.out.println("1145114".matches(qqRegex));         // true

        /*
        手机号正则:
        全部是数字,必须是11位
        第一位:必须1开头
        第二位:3 4 5 6 7 8 9
         */
        String teleRegex = "[1][3-9]\\d{9}";
        System.out.println("13114511409".matches(teleRegex));   // true
    }
}

4、String 类中与正则有关的常见方法

方法名说明
public String replace(String regex, String newStr)按照正则表达式匹配的内容进行替换
public String[] split(String regex)按照正则表达式匹配的内容进行分割字符串,返回一个字符串数组

5、使用正则做爬取

Pattern 类:对正则表达式做封装
Matcher 类:文本匹配器,按照正则表达式的规则从头开始读取字符串,在大串中去找符合匹配规则的子串


以下介绍一些时间API
JDK8版本之前,介绍Date类、SimpleDateFormat类、Calendar类
JDK8版本之后,种类很多,更好用

JDK8之前传统的APIJDK8开始之后新增的时间API
1、设计欠妥,使用不方便,很多都被淘汰1、设计更合理,功能丰富使用更方便
2、都是可变对象,修改后会丢失最开始的时间信息2、都是不可变对象,修改后会返回新的时间对象,不会丢失最开始的时间
3、线程不安全3、线程安全
4、只能精确到毫秒4、能精确到毫秒、纳秒

四十八、Date 类

代表时间和日期

导包import java.util.Date;

常用构造方法

构造器(构造方法)说明
public Date()创建一个 Date 对象,封装的是当前的时间
pubic Date(long time)把时间毫秒值转换成 Date 日期对象,就是从时间原点结合给的毫秒值做计算,时间原点:1970年1月1日0时0分0秒
public class DateTest {
    public static void main(String[] args) {
        
        Date d1 = new Date();
        System.out.println(d1);   // Wed Mar 29 17:43:14 CST 2023

        Date d2 = new Date(0L);
        System.out.println(d2);   // Thu Jan 01 08:00:00 CST 1970
       							  // 我们在东八区,有时差
       	Date d3 = new Date(1000L);
        System.out.println(d3);   // Thu Jan 01 08:00:01 CST 1970			    
    }
}

常见成员方法

方法名说明
public long getTime()返回从时间原点到对象所封装的时间的总毫秒数
public void setTime(long time)设置日期对象的时间为时间原点结合给的毫秒值所得的时间
public class DateTest {
    public static void main(String[] args) {

        Date d1 = new Date();
        System.out.println(d1);     // Wed Mar 29 17:43:14 CST 2023
        System.out.println(d1.getTime());   // 1680237275765

        Date d2 = new Date(0L);
        System.out.println(d2);     // Thu Jan 01 08:00:00 CST 1970
        System.out.println(d2.getTime());   // 0

        Date d3 = new Date(1000L);
        System.out.println(d3);     // Thu Jan 01 08:00:01 CST 1970
        System.out.println(d3.getTime());   // 1000

		d3.setTime(0L);             // 将日期对象d3封装的时间设置为时间原点
        System.out.println(d3);     // Thu Jan 01 08:00:00 CST 1970
    }
}

四十九、SimpleDateFormat 类

用于日期格式化

导包 import java.text.SimpleDateFormat;

常见构造方法

构造器说明
public SimpleDateFormat()构造一个SimpleDateFormat,使用默认格式
public SimpleDateFormat(String pattern)构造一个SimpleDateFormat,使用指定的格式

常用成员方法

方法名说明
public final String format(Date date)将日期对象,转化为字符串
public final Date parse(String source)将日期字符串,解析为日期对象
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class SimpleDateFormatDemo {
    public static void main(String[] args) throws ParseException {

        // 创建日期格式化对象,使用默认格式
        SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat();
        // 使用指定格式
        SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyy年MM月dd日");

        // 创建Date对象,封装此刻的时间
        Date d1 = new Date();
        Date d2 = new Date();

        // 将日期对象,转换为字符串
        String res1 = simpleDateFormat1.format(d1);
        System.out.println(res1);        // 2023/4/1 下午1:51

        String res2 = simpleDateFormat2.format(d1);
        System.out.println(res2);        // 2023年04月01日

        // parse()方法实例
        String today = "2023年04月01日";
        SimpleDateFormat simpleDateFormat3 = new SimpleDateFormat("yyyy年MM月dd日");
        Date date = simpleDateFormat3.parse(today);
        System.out.println(date);       // Sat Apr 01 00:00:00 CST 2023
    }
}

JDK6帮助文档中的格式说明:
JDK帮助文档6版

五十、Calendar 类

1、介绍

代表系统此刻时间对应的日历,通过它可以单独获取或修改时间中的年、月、日、时、分、秒等

导包 import java.util.Calendar;

Calendar 类是一个抽象类,不能创建对象

类中有一个静态方法 public static Calendar getInstance() 可以用它创建对象

创建对象:Calendar c = Calendar.getInstance();
左边是父类引用,右边获取子类对象,所以构成多态

2、常用成员方法

方法名说明
public int get(int field)获取日历中的某个字段信息(推荐参数写Calendar类中的静态常量)
public void set(int feild, int value)修改日历中的某个字段信息
public void add(int field, int amount)为某个字段增加或减少指定的值
public final Date getTime()获取日期对象
public final void setTime(Date date)给日历设置日期对象

public int get(int field)方法示例

import java.util.Calendar;

public class CalendarDemo {
    public static void main(String[] args) {

        Calendar c = Calendar.getInstance(); // 创建Calendar对象c

        // 为了更直观,所以推荐参数写Calendar类中的静态常量
        // Calendar.YEAR 获取年份信息,在Calendar内部对应1
        int res1 = c.get(Calendar.YEAR);
        int res2 = c.get(1);
        System.out.println(res1);  // 2023
        System.out.println(res2);  // 2023

        int month = c.get(Calendar.MONTH);
        // Calendar的月份是从0~11月,要得到正确的月份需要加一
        System.out.println(month + 1);  // 4

        // 获取星期,需要提前设计一个数组才能打印出正确的星期
        char[] weeks = {' ', '日', '一', '二', '三', '四', '五', '六'}; // java中的char是两字节
                      // 0,   1,    2,    3,    4,    5,    6,   7,
        int weekIndex = c.get(Calendar.DAY_OF_WEEK);
        System.out.println(weeks[weekIndex]); // 一
    }
}

获取正确的星期需要提前设计一个数组,因为老外的星期是从星期七开始到星期一结束

星期
返回结果2345671

public void set(int feild, int value)方法示例

import java.util.Calendar;

public class CalendarDemo {
    public static void main(String[] args) {

        Calendar c = Calendar.getInstance();

        c.set(Calendar.YEAR, 2022);               // 修改年份信息
        System.out.println(c.get(Calendar.YEAR)); // 2022
        
        c.set(2008, 8, 8);    					  // 直接修改日期相关信息
        System.out.println(c.get(Calendar.YEAR)); // 2008

    }
}

public void add(int field, int amount)方法示例

import java.util.Calendar;

public class CalendarDemo {
    public static void main(String[] args) {

        Calendar c = Calendar.getInstance();

        c.add(Calendar.YEAR, -1);                  // 当前年份2023减一
        System.out.println(c.get(Calendar.YEAR));  // 2022
    }
}

以下是JDK8开始之后新增的时间API

五十一、LocalDate、LocalTime、LocalDateTime 类

LocalDate 类,代表本地日期(年、月、日、星期)

LocalTime 类,代表本地时间(时、分、秒、纳秒)

LocalDateTime 类,代表本地日期和时间(年、月、日、星期、时、分、秒、纳秒)

这三个类都是不可改变的,所以调用方法修改当前对象,都会返回一个新的对象

1、创建对象

1、方法名:public static xxxx now() 获取系统当前时间对应的该对象
xxxx代表以上三个类

示例

LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime = LocalDateTime.now();

2、方法名:public static xxxx of() 获取指定时间的对象

示例

LocalDate localDate = LocalDate.of(2023, 1, 1);
LocalTime localTime = LocalTime.of(8, 5, 6);
LocalDateTime localDateTime = LocalDateTime.of(2023, 1, 1, 8, 5, 6);

2、修改年、月、日、时、分、秒的相关方法

方法名说明
withHours、withMinute、withSecond、withNano修改时间,返回时间对象,都是 with 开头
plusHours、plusMinutes、plusSecond、plusNano把某个信息加多少,返回新时间对象
minusHours、minusMinutes、minusSecond、minusNanos把某个信息减多少,返回新时间对象
equals、isBefore、isAfter判断两个时间对象、是否相等、在前、还是在后

示例

package com.test;

import java.time.LocalDateTime;

public class Test {
    public static void main(String[] args) {
        LocalDateTime nowTime = LocalDateTime.now();// 创建LocalDateTime对象
        
        System.out.println(nowTime);                // 打印当前时间
        
        System.out.println("减一小时" + nowTime.minusHours(1));
        System.out.println("减一分钟" + nowTime.minusMinutes(1));
        System.out.println("减一纳秒" + nowTime.minusNanos(1));

        System.out.println("加一年" + nowTime.plusYears(1));
        System.out.println("加一小时" + nowTime.plusHours(1));
        System.out.println("加一分钟" + nowTime.plusMinutes(1));

        System.out.println("修改年份" + nowTime.withYear(2000));
        System.out.println("修改月份" + nowTime.withMonth(12));
        // 打印结果
        //2023-04-11T20:26:00.798754100
        //减一小时2023-04-11T19:26:00.798754100
        //减一分钟2023-04-11T20:25:00.798754100
        //减一纳秒2023-04-11T20:26:00.798754099
        //加一年2024-04-11T20:26:00.798754100
        //加一小时2023-04-11T21:26:00.798754100
        //加一分钟2023-04-11T20:27:00.798754100
        //修改年份2000-04-11T20:26:00.798754100
        //修改月份2023-12-11T20:26:00.798754100
    }
}

3、LocalDateTime 转 LocalDate 和 LocalTime 的方法

方法名说明
public LocalDate toLocalDate()转成一个LocalDate对象
public LocalTime toLocalTime()转成一个LocalTime对象

五十二、DateTimeFormatter 类

用于时间的格式化和解析

方法名说明
static DateTimeFormatter ofPattern(格式)获取格式化对象
String format(时间对象)按照指定方式格式化

获取对象
static DateTimeFormatter ofPattern(格式)

格式化
String format(时间对象)

解析:(解析用的是日期对象里的parse方法)
LocalDateTime.parse("需要解析的字符串", 格式化对象);
LocalDate.parse("需要解析的字符串", 格式化对象);
LocalTime.parse("需要解析的字符串", 格式化对象);

示例

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class DateTimeFormatterDemo {
    public static void main(String[] args) {
        // 创建日期对象
        LocalDateTime now = LocalDateTime.now();
        System.out.println("格式化前:" + now);     // 格式化前:2023-04-14T10:05:45.457768900

        // 创建格式化对象
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");

        // 格式化
        String result = formatter.format(now);
        System.out.println("格式化后:" + result);  // 格式化后:2023年04月14日

        // 解析
        String time = "2008年08月08日";
        // 解析用的是日期对象里的方法parse
        LocalDate parse = LocalDate.parse(time, formatter);
        System.out.println(parse);                // 2008-08-08
    }
}

五十三、Instant、ZoneId、ZonedDateTime 类

Instant,时间戳(时间节点)/时间线,类似Date

ZoneId,时区

ZonedDateTime,带时区的时间

1、Instant 时间戳

用于表示时间的对象,类似Date

常见方法

方法名说明
static Instant now()获取当前时间的 Instant 对象(世界标准时间,本初子午线)
static Instant ofXxxx(long epochMilli)根据(秒/毫秒/纳秒)获取 Instant 对象
ZoneDateTime atZone(ZoneId zone)指定时区
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class InstantDemo {
    public static void main(String[] args) {
        // 获取当前时间的 Instant 对象
        Instant instant = Instant.now();
        // 打印结果有时差
        System.out.println("当前时间是:" + instant);

        // 需要使用ZoneId和ZonedDateTime获得正确的时间
        ZonedDateTime zonedDateTime = Instant.now().atZone(ZoneId.of("Asia/Shanghai"));
        System.out.println(zonedDateTime);
    }
}

2、ZoneId 时区

常见方法

方法名说明
static Set< String > getAvailableZoneIds()获取Java中支持的所有时区(在国内就用Aisa/Shanghai)
static ZoneId systemDefault()获取系统默认时区
static ZoneId of(zoneId)获取一个指定时区
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Set;

public class ZoneIdDemo {
    public static void main(String[] args) {
        // 1、获取Java中支持的所有时区
        Set<String> set = ZoneId.getAvailableZoneIds();
        System.out.println(set);

        // 2、获取系统默认时区
        ZoneId zoneId = ZoneId.systemDefault();
        System.out.println(zoneId); // Asia/Shanghai

        // 3、获取一个指定时区
        ZoneId of = ZoneId.of("Africa/Nairobi");
        // ZoneId只代表时区对象
        System.out.println(of); // Africa/Nairobi
        // 将时区和时间对象结合
        ZonedDateTime zonedDateTime = Instant.now().atZone(of);
        // 带时区的时间对象
        System.out.println(zonedDateTime);// 2023-04-14T06:39:21.838402500+03:00[Africa/Nairobi]
    }
}

3、ZonedDateTime 带时区的时间

常见方法

方法名说明
static ZoneDateTime now()获取当前时间的 ZoneDateTime 对象
static ZoneDateTime of(…)获取指定时间和时区的 ZoneDateTime 对象

五十四、Period、Duration、ChronoUnit 类

Period 用于计算两个“日期”间隔(年,月,日)

Duration 用于计算两个“时间”间隔(秒,纳秒)

ChronoUnit 用于计算两个“日期”间隔(功能最强大,最全,重点掌握)

1、Period 示例

import java.time.LocalDate;
import java.time.Period;

public class PeriodDemo {
    public static void main(String[] args) {
        // 此刻年月日
        LocalDate today = LocalDate.now();
        System.out.println(today);      // 2023-04-14

        // 昨天年月日
        LocalDate otherDate = LocalDate.of(2023, 4, 13);
        System.out.println(otherDate);  // 2023-04-13

        // Period对象表示的时间间隔对象
        Period period = Period.between(today, otherDate);// 第二个参数减第一个参数

        // 间隔多少年
        System.out.println(period.getYears());      // 0
        // 间隔的月份
        System.out.println(period.getMonths());     // 0
        // 间隔的天数
        System.out.println(period.getDays());       // -1
    }
}

2、Duration 示例

import java.time.Duration;
import java.time.LocalDateTime;

public class DurationDemo {
    public static void main(String[] args) {
        // 此刻的日期时间对象
        LocalDateTime today = LocalDateTime.now();
        System.out.println(today);

        // 昨天的日期时间对象
        LocalDateTime otherDate = LocalDateTime.of(2023, 4, 13, 0, 0, 0);
        System.out.println(otherDate);

        // Duration对象
        Duration duration = Duration.between(otherDate, today);// 第二个参数减第一个参数

        // 相差的天数
        System.out.println(duration.toDays());      // 1
        // 相差的小时数
        System.out.println(duration.toHours());     // 37
        // 相差的分钟数
        System.out.println(duration.toMinutes());   // 2220
        // 相差的纳秒数
        System.out.println(duration.toNanos());     // 133211364525300
    }
}

3、ChronoUnit 示例

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

public class ChronoUnitDemo {
    public static void main(String[] args) {
        // 此刻的日期时间对象
        LocalDateTime today = LocalDateTime.now();
        System.out.println(today);
        
        // 另一个时间
        LocalDateTime otherDate = LocalDateTime.of(2023, 3, 3, 0, 0, 0);
        System.out.println(otherDate);
        
        // 计算时间间隔
        System.out.println("相差的年数:" + ChronoUnit.YEARS.between(otherDate, today));
        System.out.println("相差的月数:" + ChronoUnit.MONTHS.between(otherDate, today));
        System.out.println("相差的周数:" + ChronoUnit.WEEKS.between(otherDate, today));
        System.out.println("相差的天数:" + ChronoUnit.DAYS.between(otherDate, today));
        System.out.println("相差的时数:" + ChronoUnit.HOURS.between(otherDate, today));
        System.out.println("相差的毫秒数:" + ChronoUnit.MILLIS.between(otherDate, today));
        System.out.println("相差的纳秒数:" + ChronoUnit.NANOS.between(otherDate, today));
    }
}

The End

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值