首页 > 编程语言 >Java

Java

时间:2024-07-28 16:42:05浏览次数:7  
标签:Java String System println new public out

Java入门

IDEA优化

image-20240406194731905

image-20240406194736502

image-20240406194739971

idea插件

翻译、阿里巴巴代码规范指导

image-20240406194744539

IDEA debug使用

Step into:单步执行(一行一行代码执行),如果遇到子函数,就会进入子函数,并且继续单步执行。就是每一行需要执行的代码都不跳过,一行一行进行。

Step over:在单步执行的时候,如果遇到子函数,并不会进入子函数,而是把子函数当做一整步执行完成,从而继续执行函数调用位置下的代码。

Step out:当单步执行到子函数内时,用Step out就可以执行完子函数余下部分,并返回到上一层函数。

需要注意的是,若在接下来的代码中还存在断点时,Step over 和 Step out 不会跳过断点,也就是断点位置一定会“断”。

Java进制

  • 二进制。以0b或0B开头,由数字0和1组成。
  • 八进制。以0开头,由数字0到7组成。
  • 十六进制。以0x或0X开头,由数字0到9以及字母A到F组成。(十六进制经常用来表示内存地址)
        int i1 = 0b01100001;
        System.out.printf("i1 = %d\n",i1);

        int i2 = 0141;
        System.out.println(i2);

        int i3 = 0x61;
        System.out.println(i3);

image-20240406194749552

二进制计算

image-20240406194757936

字符存储

其实字符并不是直接存储的,而是把每一个字符编为一个整数,存储的是字符对应整数的二进制形式。美国人搞了一套字符和整数的对应关系表,叫做ASCII编码表。

ASCII编码表中字符编码的规律:

​ 1.字符0对应48,后面的1,2,3,4...9 对应的十进制整数依次往后顺延

​ 2.字符a对应97,后面的b,c,d,e...z 对应的十进制整数依次往后顺延

​ 3.字符A对应65,后面的B,C,D,E...Z 对应的十进制整数依次往后顺延

image-20240406194827954

标识符

由数字、字母、下划线(_)和美元符($)等组成

不能以数字开头、不能用关键字做为名字、且是区分大小写的

命名规范:

1、不能由$、下划线、和中文,尽量不要包含数字,不能以数字开头

2、有实际含义的情况下,尽量英文的见名知意

3、变量名和方法名使用小驼峰命名法,类名使用大驼峰命名法

小驼峰:第一个单词首字母小写,其余单词首字母大写

大驼峰:所有单词首字母都大写

变量

当执行int age = 18; 这句代码时,JVM会在内存中申请一块区域,在这个区域中存储了一个整数18,给这个区域取的名字叫age; 相当于在盒子中存了一个数据18,这个盒子的名字是age,当我们打印age时,就是从盒子中把盒子中的数据取出来再打印。

image-20240406194841069

变量要先声明再使用

变量的有效范围是从定义开始到“}”截止,且在同一个范围内部不能定义2个同名的变量。

变量定义的时候可以不赋初始值;但在使用时,变量里必须有值,否则报错。

image-20240406194844162

字面量

image-20240406194848907

/*
目标:需要同学们掌握常见数据在程序中的书写格式
*/
public class LiteralDemo{
    public static void main(String[] args){
        //1.整数
        System.out.println(666);
        
        //2.小数
        System.out.println(3.66);
        
        //3.字符: 字符必须用单引号引起来,不可为空
        
        System.out.println('a');
        System.out.println('0');
        System.out.println('中');
        System.out.println(' '); //空格也算字符
        //特殊字符:\t表示制表符 \n表示换行
        System.out.println('\t'); //这相当于一个tab键,专业叫做制表符
        System.out.println('\n'); //这是换行的意思
        
        //4.字符串:字符串是双引号引起来的,字符串可为空
        System.out.println("我爱你中国abc");
        
        //5.布尔值:只有两个值true和false
        System.out.println(true);
        System.out.println(false);
    }
}

注释

1.单行注释:

​ //后面根解释文字

2.多行注释

​ /*

​ 这里写注释文字

​ 可以写多行

​ */

3.文档注释

​ /**

​ 这里写文档注释

​ 也可以写多行,文档注释可以利用JDK的工具生成帮助文档

​ */

Java执行原理

1.Java程序的执行原理是什么样的?

不管是什么样的高级编程语言,最终都是翻译成计算机底层可以识别的机器语言。

2.机器语言是由什么组成的啊?

0和1

JDK由JVM、核心类库、开发工具组成,如下图所示

下面分别介绍一下JDK中每一个部分是用来干什么的

image-20240406194852690

- 什么是JVM?

答:JDK最核心的组成部分是JVM(Java Virtual Machine),它是Java虚拟机,真正运行Java程序的地方。

- 什么是核心类库?

答:它是Java本身写好的一些程序,给程序员调用的。 Java程序员并不是凭空开始写代码,是要基于核心类库提供的一些基础代码,进行编程。

- 什么是JRE?

答:JRE(Java Runtime Enviroment),意思是Java的运行环境;它是由JVM和核心类库组成的;如果你不是开发人员,只需要在电脑上安装JRE就可以运行Java程序。

- 什么是开发工具呢?

答:Java程序员写好源代码之后,需要编译成字节码,这里会提供一个编译工具叫做javac.exe,编写好源代码之后,想要把class文件加载到内存中运行,这里需要用到运行工具java.exe。

除了编译工具和运行工具,还有一些其他的反编译工具、文档工具等等...

JDK、JRE的关系用一句话总结就是:用JDK开发程序,交给JRE运行

java跨平台原理

java代码编译成字节码文件后在JVM中运行,不同的操作系统安装JVM即可,一次编译处处运行

JVM不跨平台,不同操作系统安装对应版本的JVM

helloworld

编写Java程序的步骤

编写一个Java程序需要经过3个步骤:编写代码,编译代码,运行代码

image-20240406194856309

  • 编写代码:任何一个文本编辑器都可以些代码,如Windows系统自带的记事本
  • 编译代码:将人能看懂的源代码(.java文件)转换为Java虚拟机能够执行的字节码文件(.class文件)
  • 运行代码:将字节码文件交给Java虚拟机执行

数据类型、运算符

键盘输入

// 1、导包:一般不需要我们自己做,idea工具会自动帮助我们 导包的。
        // 2、抄写代码:得到一个键盘扫描器对象(东西)
        Scanner sc = new Scanner(System.in);

        // 3、开始 调用sc的功能,来接收用户键盘输入的数据。
        System.out.println("请您输入您的年龄:");
        int age = sc.nextInt(); // 执行到这儿,会开始等待用户输入一个整数,直到用户按了回车键,才会拿到数据。
        System.out.println("您的年龄是:"  + age);

        System.out.println("请您输入您的名字:");
        String name = sc.next(); // 执行到这儿,会开始等待用户输入一个字符串,直到用户按了回车键,才会拿到数据。
        System.out.println(name + "欢迎您进入系统~~");

next()和nextLine()

next(),nextInt(),nextDouble(), nextShort(),nextByte()等等方法遇见第一个有效字符(非空格,非换行符、非tab)时,开始扫描,当遇见第一个空格、换行符或tab时,结束扫描,获取扫描到的内容,也就是说获得第一个扫描到的不含空格、换行符、tab的单个字符串。

nextLine():
以Enter回车键为结束符,也就是说 nextLine()方法可以接收是回车之前的所有字符。

nextLine()输入注意事项

在Java中,Scanner类用于从输入源(如键盘)读取数据。当我们使用Scanner的nextInt()、nextDouble()等方法读取数值类型的输入时,这些方法只会读取数值本身,而不会读取数值后面的换行符(即用户按下Enter键时产生的换行符)。
因此,在使用nextInt()或nextDouble()等方法读取数值输入后,换行符仍然留在输入缓冲区中。当我们接下来使用nextLine()方法尝试读取一个字符串时,nextLine()方法会立即读取缓冲区中的换行符,而不会等待用户输入新的数据。这通常会导致程序错误地认为用户已经输入了一个空字符串。
为了解决这个问题,我们在读取数值输入后,通常会添加一个额外的input.nextLine()来清除缓冲区中的换行符。这个方法会读取并丢弃换行符,从而确保下一次调用nextLine()方法时,它会等待用户输入新的数据,而不是立即读取缓冲区中的换行符。
以下是一个简单的示例:

import java.util.Scanner;

public class InputExample {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);

    System.out.println("请输入一个整数:");
    int num = input.nextInt();
 
    System.out.println("请输入一个字符串:");
    input.nextLine(); // 清除缓冲区中的换行符
    String text = input.nextLine();
 
    System.out.println("你输入的整数是:" + num);
    System.out.println("你输入的字符串是:" + text);
 
    input.close();
}

}
在这个例子中,在读取整数后,我们使用input.nextLine()清除了缓冲区中的换行符。这样,当我们再次调用nextLine()方法以读取字符串时,它会等待用户输入新的数据,而不是立即读取换行符。

while循环输入scanner如何退出

以下三种退出方式:

while (!scanner.hasNext("0") ){
	// 标志位退出
}
while (scanner.hasNextLine() ){
	// 代码段
	if(scanner.hasNext("0")) // 内部标志退出
		break;
}
while (scanner.hasNextLine() ){
	// 代码段
	System.exit(0); // 系统退出
}

运算符

算数运算符

注意:在Java中两个整数相除结果还是整数。

与字符串做+运算时会被当成连接符,其结果还是字符串。

字符串的“+”操作

当“+”操作中出现字符串时,这个”+”是字符串连接符,而不是算术运算。

“itheima”+ 666 --> itheima666

在”+”操作中,如果出现了字符串,就是连接运算符,否则就是算术运算。当连续进行“+”操作时,从左到右逐个执行。

1 + 99 + "年黑马" --> 100年黑马

需要我们注意的是,自增自减只能对变量进行操作,不能操作字面量。

2.混合使用:++或者--放在变量或者前面运算规则稍有不同
	//++在后:先做其他事情,再做自增和自减,等到下一次在使用这个变量时,再执行自增和自减
	int a = 10;
	int b = a++; //等价于 int b = a; a++; 

	//++在前:先自增或者自减,再做其他运算
	int x = 10;
	int y = --x; //等价于x--; int y = x;  
int num = 10;
num++;
System.out.println(num);
int num1 = 10;
int num2 = 20;
//          10          10         20          210
int sum = (num1++) + (--num1) + (num2++) + (num2*10);
System.out.println(sum);	// 250
System.out.println(num1);	// 10
System.out.println(num2);	// 21

// 输出:
11
250
10
21

赋值运算符

符号 用法 作用 底层代码形式
+= a+=b 加后赋值 a = (a的类型)(a + b);
-= a-=b 减后赋值 a = (a的类型)(a - b);
*= a*=b 乘后赋值 a = (a的类型)(a * b);
/= a/=b 除后赋值 a = (a的类型)(a / b);
%= a%=b 取余后赋值 a = (a的类型)(a % b);

注意:扩展的赋值运算符隐含了强制类型转换。

面试题:

表达式的最终结果类型由表达式中的最高类型决定。

在表达式中,byte、short、char 是直接转换成int类型参与运算的。

        byte x = 10;
        byte y = 30;
        //  x = x + y; // 编译报错
        // x = (byte)(x + y);
        x += y; // 等价形式:x = (byte)(x + y);
        System.out.println(x);

if中赋值的判断

对于if(b=true)和if(b=false)两种情况程序的执行过程是:

首先把true/false赋值给b,把true/false留在括号中。

可以理解为这时候为if(true) 和if(false)。意思就是说赋值表达式执行后会把值作为一个结果留在这里。

package com.itheima.test;

public class Test {
    public static void main(String[] args) {
        boolean b = true;
        if (b=false){  // 赋值,false
            System.out.println(1);
        }else if (b) {  // false
            System.out.println(2);
        } else if (!b){  // true
            System.out.println(3);
        } else {
            System.out.println(4);
        }
    }
}

// 3


package com.itheima.test;

public class Test {
    public static void main(String[] args) {
        boolean b = true;
        if (b=true){  // 赋值,true
            System.out.println(1);
        }else if (b) {
            System.out.println(2);
        } else if (!b){
            System.out.println(3);
        } else {
            System.out.println(4);
        }
    }
}


// 1

关系运算符

符号 例子 作用 结果
> a>b 判断a是否大于b 成立返回true、不成立返回false
>= a>=b 判断a是否大于或者等于b 成立返回true、不成立返回false
< a<b 判断a是否小于b 成立返回true、不成立返回false
<= a<=b 判断a是否小于或者等于b 成立返回true、不成立返回false
== a==b 判断a是否等于b 成立返回true、不成立返回false
!= a != b 判断a是否不等于b 成立返回true、不成立返回false

逻辑运算符

image-20240406194907568

三元运算符

image-20240406194912208

// 需求3:找3个整数中的较大值。
        int i = 10;
        int j = 45;
        int k = 34;

        // 找出2个整数中的较大值。
        int temp = i > j ? i : j;
        // 找出temp与k中的较大值。
        int max2 = temp > k ? temp : k;
        System.out.println(max2);
        System.out.println("-----------");
        System.out.println((i > j && i >k) ? i : (j > k ? j : k))

运算符优先级

image-20240406194916155

从图中我们发现,&&运算比||运算的优先级高,所以&&和||同时存在时,是先算&&再算||;

比如下面的代码

//这里&&先算 相当于 true || false 结果为true
System.out.println(10 > 3 || 10 > 3 && 10 < 3); // true

最后给大家说一下,在实际开发中,其实我们很少考虑运算优先级, 因为如果你想让某些数据先运算,其实加()就可以了,这样阅读性更高。

//有括号先算 相当于 true && false 结果为false
System.out.println((10 > 3 || 10 > 3) && 10 < 3); //false

位运算

位运算指的是二进制的运算

image-20240406194919825

位运算符指的是二进制位的运算,先将十进制数转成二进制后再进行运算

在二进制位运算中,1表示true,0表示false。

8>>2 = 8/4 = 2

8<<2 = 8*4 = 32

计算机默认采用大端排序,高位在左,低位在右

异或运算的特点

  • 一个数,被另外一个数,异或两次,该数本身不变。

数据类型

原码、反码、补码

原码就是符号位+真值的绝对值,第一位表示符号,其余位表示值

反码:正数的反码是其本身,负数的反码是原码的符号位不变,其余位取反

补码:正数的补码是其本身,负数的补码是其反码+1

浮点数不像整数变量只用符号位和数值位就能表示,float单精度浮点数在计算机占32位(四个字节),它储存在计算机时讲32位划分为三个部分,符号位,指数和尾数。(不细讲)

基本数据类型

最高位表示正负号,0是正,1是负

举例:byte类型的范围为什么是-128~127

image-20240325211945872

image-20240406194926052

image-20240406194929486

        // 如果希望随便写一个整型字面量是long类型的,需要在其后面加上L/l
		// 如果定义的数超出默认int的范围,必须在末尾加上L/l
        long number4 = 73642422442424L;

        // 2、浮点型
        // 注意:随便写一个小数字面量,默认当成double类型对待的,如果希望这个小数是float类型的,需要在后面加上:F/f
        float score1 = 99.5F;

引用数据类型

        // 引用数据类型:String.
        // String代表的是字符串类型,定义的变量可以用来记住字符串。
        String name = "黑马";
        System.out.println(name);

数据类型转换

自动类型转换

小范围-->大范围

在以上情况中,其实都会涉及到类型转换。类型转换的形式总体分为2种,一种是自动类型转换,一种是强制类型转换。 这里先学习自动类型转换

  • 什么是自动类型转换呢?
答:自动类型转换指的是,数据范围小的变量可以直接赋值给数据范围大的变量
    byte a = 12; 
    int b = a; //这里就发生了自动类型转换(把byte类型转换int类型)
  • 自动类型转换的原理是怎样的?

答:自动类型转换其本质就是在较小数据类型数据前面,补了若干个字节

image-20240406194934623

表达式的自动类型转换

image-20240406194938739

// 面试笔试题:
        byte b1 = 110;
        byte b2 = 80;
        int b3 = b1 + b2;
        System.out.println(b3);

// 类型转换案例
    byte a = 3 + 4;
因为3和4,是两个常量,Java中存在【常量优化机制】
常量优化机制:在编译时(javac),就会将3和4计算出一个7的结果,并且会自动判断该结果是否在byte取值范围内
在:编译通过
不在:编译失败
    
char c = 2 + '2';   // 4

强制类型转换

大范围-->小范围

image-20240406194942827

目标数据类型 变量名 = (目标数据类型)被转换的数据;

注意事项

强制类型转换可能造成数据(丢失)溢出;

浮点型强转成整型,直接丢掉小数部分,保留整数部分返回

// 目标:掌握强制类型转换。
        int a = 20;
        byte b = (byte) a;  // ALT + ENTER 强制类型转换。
        System.out.println(a);
        System.out.println(b);

        int i = 1500;
        byte j = (byte) i;
        System.out.println(j);

        double d = 99.5;
        int m = (int) d; // 强制类型转换
        System.out.println(m); // 丢掉小数部分,保留整数部分

流程控制

分支

if

image-20240406194946259

switch

image-20240406194954532

switch****分支的执行流程

①先执行表达式的值,再拿着这个值去与case后的值进行匹配。

②与哪个case后的值匹配为true就执行哪个case块的代码,遇到break就跳出switch分支。

③如果全部case后的值与之匹配都是false,则执行default块的代码。

if、switch的比较,各自适合什么业务场景?

if其实在功能上远远强大于switch。

if适合做条件是区间判断的情况。

switch适合做:条件是比较值的情况、代码优雅、性能较好。

switch注意事项

- 1、表达式类型只能是byte、short、int、char

JDK5开始支持枚举,JDK7开始支持String

不支持double、float、long

- 2、case给出的值不允许重复,且只能是字面量,不能是变量。

- 3、正常使用switch的时候,不要忘记写break,否则会出现穿透现象。

- 4、switch可以没有default,但是一般都会加上

- 5、case语句后面可以不加break.但是如果不加break就会出现case穿透问题:匹配哪一个case就从哪一个位置向下执行,直到遇到了break或者整体结束为止;

- 6、switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。

switch的case穿透:

image-20240406194959741

image-20240406195002974

增强型switch

在Java JDK 14中引入了增强的switch语句,它提供了更灵活和可读性更高的switch语法。增强的switch语句允许在switch语句中使用表达式,而不仅限于常量。它使用箭头(->)操作符将表达式与相应的代码块关联起来,包含了break。

语法
增强的switch语句的语法如下所示:

switch (expression) {
    case value1 -> {
        // 代码块1
    }
    case value2 -> {
        // 代码块2
    }
    // 更多case语句
    default -> {
        // 默认代码块
    }
}

特性解释
增强的switch语句具有以下特性和优势:

  • 表达式匹配:增强的switch语句允许在switch语句中使用表达式,而不仅限于常量。这使得我们可以根据更复杂的条件进行匹配。

  • 代码简化:增强的switch语句简化了代码,减少了冗余的代码和重复的书写。它提供了一种更紧凑的语法形式,使得代码更直观、易读。

  • 可读性提高:增强的switch语句提高了代码的可读性和可维护性。它使得逻辑更清晰,更易于理解和调试。

示例
下面是一些使用增强的switch语句的示例:

使用常量匹配:

int day = 2;
String dayName = switch (day) {
 case 1 -> "Monday";
 case 2 -> "Tuesday";
 case 3 -> "Wednesday";
 case 4 -> "Thursday";
 case 5 -> "Friday";
 default -> "Unknown";
};
System.out.println(dayName); // 输出: Tuesday

使用表达式匹配:

int number = 15;
String numberType = switch (number % 2) {
 case 0 -> "Even";
 case 1 -> "Odd";
 default -> "Unknown";
};
System.out.println(numberType); // 输出: Odd

使用枚举类型匹配:

enum Season {
    SPRING, SUMMER, AUTUMN, WINTER
   }

Season season = Season.SUMMER;
String seasonName = switch (season) {
    case SPRING -> "Spring";
    case SUMMER -> "Summer";
    case AUTUMN -> "Autumn";
    case WINTER -> "Winter";
};

System.out.println(seasonName); // 输出: Summer

以上示例展示了增强的switch语句的用法,它使得在switch语句中可以更灵活地使用表达式进行匹配,从而简化了代码并提高了可读性。

switch多值匹配

public static void main(String[] args) {
    //键盘录入一个数字  赋值给变量week
    Scanner sc = new Scanner(System.in);
    System.out.println("请输入一个星期数");
    String week = sc.next();
    //用switch判断week的值  输出不同的结果
    switch (week) {
        case "周一", "周二", "周三" -> System.out.println("跑步");
        case "周四", "周五", "周六" -> System.out.println("动感单车");
        case "周日" -> System.out.println("好好吃一顿");
        default -> System.out.println("请输入1~7");
    }
}

switch和if的区别

布尔类型,比较区间使用if

如果判断条件是byte、short、long、int、String、枚举的常量数据,并且分支达到三个以上,适合使用switch

循环

for

image-20240406195009407

如何跳出当前的多重嵌套循环

在最外层循环前加一个标记如outfor,然后用break outfor;可以跳出多重循环。例如以下代码:

public class TestBreak {
    public static void main(String[] args) {
        outfor: for (int i = 0; i < 10; i++){
            for (int j = 0; j < 10; j++){
                if (j == 5){
                    break outfor;
                }
                System.out.println("j = " + j);
            }
        }
    }
}

面试题(重点!!!)

int a = 1;
for (int i = 0; i < 10000; i++) {
    a = a++;
}
System.out.println(a);

接下来我们分析一下为什么?

count++是一个表达式,是有返回值的,它的返回值就是count自加前的值,Java对自加是这样处理的:首先把count的值(注意是值,不是引用)拷贝到一个临时变量区,然后对count变量加1,最后返回临时变量区的值。

用代码解释为这样的:

int temp = count; //先把i变量的值10保存到临时变量中
count = count+1; // i变量的值加1操作
count = temp; //再把临时变量中的值赋值给i

程序第一次循环时的详细处理步骤如下:

JVM把count值(其值是0)拷贝到临时变量区。

count值加1,这时候count的值是1。

返回临时变量区的值,注意这个值是0,没修改过。

返回值赋值给count,此时count值被重置成0。

foreach循环

结构:

for(data_type item : collections) {

​ ...

}

        int[] arr = {1, 2, 3, 4, 5, 6};
        for (int i : arr) {
            System.out.println(i);
        }        int[] arr = {1, 2, 3, 4, 5, 6};
        for (int i : arr) {
            System.out.println(i);
        }

        int sum = 0;
        for (int i : arr) {
            sum += i;
        }
        System.out.println(sum);
    }

while

image-20240406195014223

do while

image-20240406195016776

for、while、do while区别

image-20240406195025895

break和continue

  • break作用:跳出并结束当前所在循环的执行
  • continue作用:结束本次循环,进入下一次循环

break : 只能用于结束所在循环, 或者结束所在switch分支的执行。

continue : 只能在循环中进行使用。image-20240406195029328

随机数random

image-20240406195032514

image-20240406195035482

        for (int i = 1; i <= 20; i++) {
            // 生成:1-10之间的随机数
            // 1-10 => -1 => (0 - 9) + 1
            int data2 = r.nextInt(10) + 1;
            System.out.println(data2);
        }

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

        for (int i = 1;i <= 20; i++) {
            // 3 - 17 => -3 => (0 - 14) + 3
            int data3 = r.nextInt(15) + 3;
            System.out.println(data3);
        }

jdk17后随机数

可以指定区间,左闭右开

Random random = new Random();
int num = random.nextInt(6,8);

数组

Java数组

img

数组的静态初始化

// 目标:掌握数组的定义方式一:静态初始化数组。

        // 1、数据类型[] 数组名 = new 数据类型[]{元素1, 元素2, 元素3,.....}
        int[] ages = new int[]{12, 24, 36};
        double[] scores = new double[]{89.9, 99.5, 59.5, 88};

        // 2、简化写法:
        // 数据类型[] 数组名 = {元素1, 元素2, 元素3,.....}
        int[] ages2 = {12, 24, 36};
        double[] scores2 = {89.9, 99.5, 59.5, 88};

        // 3、数据类型[] 数组名 也可以写成 数据类型 数组名[]
        int ages3[] = {12, 24, 36};
        double scores3[] = {89.9, 99.5, 59.5, 88};

数组的动态初始化

image-20240406195040839

数组的元素默认值

创建了数组后, 系统会给数组进行默认的初始化

整数数组,把所有元素默认初始化为0
小数数组,把所有元素默认初始化为0.0
字符数组,把所有元素默认初始化为码值为0的字符, '\u0000'
布尔数组,把所有元素默认初始化为false
引用数组,把所有元素默认初始化为null

image-20240406195046038

静态和动态初始化应用场景

动态初始化:适合开始不确定具体元素值,只知道元素个数的业务场景。

静态初始化:适合一开始就知道要存入哪些元素值的业务场景。

数组在内存中如何存储

java数组的内存分配

image-20240406195049593

image-20240406195053144

引用类型的变量里存放的都是地址值

我们以int[] ages = {12,24,36};这句话为例,看一下这句话到底在计算机中做了那些事情。

  • 首先,左边int[] ages 表示定义了一个数组类型的变量,变量名叫ages
  • 其次,右边{12,24,36}表示创建一个数组对象,你完全可以把它理解成一个能装数据的东西。这个对象在内存中会有一个地址值[I@4c873330,每次创建一个数组对象都会有不用的地址值。
  • 然后,把右边的地址值[I@4c873330赋值给左边的ages变量
  • 所以,ages变量就可以通过地址值,找到数组这个东西。

image-20240406195056086

访问数组元素

image-20240406195059385

数组在计算机中的执行原理

image-20240406195103477

image-20240406195106930

image-20240406195113075

重要!!!

1、jvm内存区域划分了几块,分别存什么?

方法区、堆内存、栈内存

栈内存存放基本数据类型和引用数据类型的引用地址

堆内存存放引用数据类型

2、引用数据类型为什么叫引用数据类型?

3、字符串存在什么地方?--> 方法区的字符串常量池

image-20240406195120274

总结一下int a = 10与 int[] arr = new int[]{11,22,33}的区别

  • a是一个变量,在栈内存中,a变量中存储的数据就是10这个值。

  • arr也是一个变量,在栈中,存储的是数组对象在堆内存中的地址值

多个变量指向同一个数组!!!

总结一下:

  • 两个变量指向同一个数组时,两个变量记录的是同一个地址值。
  • 当一个变量修改数组中的元素时,另一个变量去访问数组中的元素,元素已经被修改过了。

image-20240406195124343

image-20240406195129449

使用数组常见的问题

image-20240406195135352

        int[] a = {1, 2, 3};
        int[] b = a;
        System.out.println("赋值前");
        System.out.println("a = " + a + ", b = " + b);
        b = null;
        System.out.println("赋值后");
        System.out.println("a = " + a + ", b = " + b);

        // 赋值前
        // a = [I@2f92e0f4, b = [I@2f92e0f4
        // 赋值后
        // a = [I@2f92e0f4, b = null

面试题!!!

package com.itheima.test;

import java.util.Arrays;

public class Test {
    public static void main(String[] args) {
        int[] arr = {1,2,3};
        int[] arr1 = {4,5,6};
        swap(arr,arr1);
        for (int i : arr) {
            System.out.println(i + " ");
        }
    }
    public static void swap(int[] arr,int[] arr1) {
        int[] temp = arr;
        arr = arr1;
        arr1 = temp;
    }
}

// 输出结果
1 2 3 

image-20240406195140058

package com.itheima.test;

import java.util.Arrays;

public class Test {
    public static void main(String[] args) {
        int[] arr = {1,2,3};
        int[] arr1 = {4,5,6};
        swap(arr,arr1);
        for (int i : arr) {
            System.out.println(i + " ");
        }
    }
    public static void swap(int[] arr,int[] arr1) {
        int[] temp = arr;
        arr = arr1;
        arr1 = temp;
        arr1[0] = 100;
    }
}

// 输出结果
100 2 3 

image-20240406195143531

内存图

1.内存:可以理解"内存条",任何程序,软件运行起来都会在内存中运行,占用内存,在java的世界中,将内存分为了5大块
    
2.分为哪5大块
  栈(重点)(Stack)
    主要运行方法,方法的运行都会去栈内存中运行,运行完毕之后,需要"弹栈",腾空间
    
  堆(重点):(Heap)
    每new一次,都会在堆内存中开辟空间,并为此空间自动分配一个地址值
    堆中的数据都是有默认值的
    整数:0
    小数:0.0
    字符: '\u0000'
    布尔:false
    引用:null    
        
  方法区(重点)(Method Area)
    代码的"预备区",记录了类的信息以及方法的信息

  本地方法栈(了解):专门运行native方法(本地方法)
    本地方法可以理解为对java功能的扩充 
    有很多功能java语言实现不了,所以就需要依靠本地方法完成
        
  寄存器(了解) -> 跟CPU有关
1693125937484

1.一个数组内存图

1699000833517

2.两个数组内存图

我们创建了两个数组,在堆内存中开辟了两个不同的空间,此时修改一个数组中的元素不会影响到另外一个数组中的数据
1699000841735

3.两个数组指向同一片内存空间

arr2不是new出来的,是arr1直接赋值的,arr1在内存中保存的是地址值,给了arr2,那么arr2的地址值和arr1就是一样的
所以此时arr1和arr2指向了堆内存中的同一片空间(同一个地址值,同一个数组),此时改变一个数组中的元素会影响到另外一个数组

image-20240510120624388

多维数组

多维数组是数组的数组。多维数组的每个元素都是数组本身。例如,

int[][] a = new int[3][4];

在这里,我们创建了一个名为a的多维数组。它是一个二维数组,最多可以容纳12个元素,

image-20240406195146451

二维数组

记住,Java使用基于零的索引,也就是说,Java中数组的索引从0开始,而不是从1开始。

初始化二维数组

int[][] a = {
    {1, 2, 3}, 
    {4, 5, 6, 9}, 
    {7}, 
};

如我们所见,多维数组的每个元素都是数组本身。而且,与C / C ++不同,Java中多维数组的每一行可以具有不同的长度。

image-20240406195149752

二维数组的初始化

访问二维数组

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

        int[][] a = {
            {1, -2, 3}, 
            {-4, -5, 6, 9}, 
            {7}, 
        };
      
        for (int i = 0; i < a.length; ++i) {
            for(int j = 0; j < a[i].length; ++j) {
                System.out.println(a[i][j]);
            }
        }
    }
}
class MultidimensionalArray {
    public static void main(String[] args) {

        //创建二维数组
        int[][] a = {
            {1, -2, 3}, 
            {-4, -5, 6, 9}, 
            {7}, 
        };

        //首先for ... each循环访问单个数组
        //在二维数组中
        for (int[] innerArray: a) {
            //第二次for ... each循环访问行内的每个元素
            for(int data: innerArray) {
                System.out.println(data);
            }
        }
    }
}

二维数组内存图

public class Demo06Array {
    public static void main(String[] args) {
        int[][] arr1 = new int[3][];

        arr1[1] = new int[]{1,2,3};

        arr1[2] = new int[3];

        arr1[2][1] = 100;
    }
}

1693280082069

如何在Java中初始化三维数组?

让我们看看如何在Java中使用3d数组。我们可以初始化一个类似于2d数组的3d数组。例如,

// test is a 3d array
int[][][] test = {
    {
        {1, -2, 3}, 
        {2, 3, 4}
    }, 
    { 
        {-4, -5, 6, 9}, 
        {1}, 
        {2, 3}
    } 
};

基本上,3d数组是2d数组的数组。三维阵列的行也可以像二维阵列一样在长度上有所变化。

示例:3维数组

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

        // 创建三维数组
        int[][][] test = {
            {
                {1, -2, 3}, 
                {2, 3, 4}
            }, 
            { 
                {-4, -5, 6, 9}, 
                {1}, 
                {2, 3}
            } 
        };

        //for..each循环迭代3d数组的元素
        for (int[][] array2D: test) {
            for (int[] array1D: array2D) {
                for(int item: array1D) {
                    System.out.println(item);
                }
            }
        }
    }
}

数组复制

在Java中,我们可以将一个数组复制到另一个数组中。有几种技术可以用来在Java中复制数组。

1.使用赋值运算符复制数组(错误!)

让我们举个实例

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

        int [] numbers = {1, 2, 3, 4, 5, 6};
        int [] positiveNumbers = numbers;    //复制数组

        for (int number: positiveNumbers) {
            System.out.print(number + ", ");
        }
    }
}

输出

1, 2, 3, 4, 5, 6

在上面的示例中,我们使用赋值运算符(=)将一个名为numbers的数组复制到另一个名为positiveEnumbers的数组中。

这种技术是最简单的一种,它同样有效。然而,这种技术有一个问题。如果我们改变一个数组的元素,其他数组的相应元素也会改变。例如:

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

        int [] numbers = {1, 2, 3, 4, 5, 6};
        int [] positiveNumbers = numbers;    //复制数组

        //更改第一个数组的值
        numbers[0] = -1;

        //打印第二个数组
        for (int number: positiveNumbers) {
            System.out.print(number + ", ");
        }
    }
}

输出

-1, 2, 3, 4, 5, 6

在这里,我们可以看到我们已经改变了numbers数组的一个值。当我们打印positiveEnumbers数组时,可以看到相同的值也发生了更改。

这是因为两个数组都引用相同的数组对象。这是因为浅拷贝。要了解有关浅拷贝的更多信息,请访问浅拷贝。

现在,要在复制数组的同时生成新的数组对象,我们需要深度复制而不是浅拷贝。

2.使用循环构造复制数组

让我们举个实例:

import java.util.Arrays;

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

        int [] source = {1, 2, 3, 4, 5, 6};
        int [] destination = new int[6];

        //迭代并将元素从源复制到目标
        for (int i = 0; i < source.length; ++i) {
            destination[i] = source[i];
        }

        //将数组转换为字符串
        System.out.println(Arrays.toString(destination));
    }
}

输出

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

在上面的示例中,我们使用了for循环来遍历源数组的每个元素。在每次迭代中,我们都将元素从source数组复制到destination数组。

在这里,源和目标数组引用不同的对象(深度复制)。因此,如果一个数组的元素被更改,另一个数组的相应元素也将保持不变。

注意以下语句,

System.out.println(Arrays.toString(destination));

在这里,toString()方法用于将数组转换为字符串。

3.使用arraycopy()方法复制数组

在Java中,System类包含一个名为arraycopy()的方法来复制数组。与上述两种方法相比,这种方法是一种更好的复制数组的方法。

方法允许您将源数组的指定部分复制到目标数组。例如,

arraycopy(Object src, int srcPos,Object dest, int destPos, int length)

这里,

  • src -您要复制的源数组
  • srcPos -源数组中的起始位置(索引)
  • dest -目标数组,将从源中复制元素
  • destPos -目标数组中的起始位置(索引)
  • length -要复制的元素数

让我们举个实例:

//使用Arrays.toString()方法
import java.util.Arrays;

class Main {
    public static void main(String[] args) {
        int[] n1 = {2, 3, 12, 4, 12, -2};

        int[] n3 = new int[5];

        //创建长度为n1的n2数组
        int[] n2 = new int[n1.length];

        //将整个n1数组复制到n2
        System.arraycopy(n1, 0, n2, 0, n1.length);
        System.out.println("n2 = " + Arrays.toString(n2));  

        //从n1数组的索引2复制元素
        //将元素复制到n3数组的索引1
        //将复制2个元素
        System.arraycopy(n1, 2, n3, 1, 2);
        System.out.println("n3 = " + Arrays.toString(n3));  
    }
}

输出

n2 = [2, 3, 12, 4, 12, -2]
n3 = [0, 12, 4, 0, 0]

在上面的示例中,我们使用了arraycopy()方法,

  • System.arraycopy(n1, 0, n2, 0, n1.length) - 将n1数组中的整个元素复制到n2数组中
  • System.arraycopy(n1, 2, n3, 1, 2 )- 从索引2开始的n1数组的两个元素被复制到从n3数组1开始的索引中

正如您看到的,int类型数组元素的默认初始值为0。

4.使用copyOfRange()方法复制数组

我们还可以使用Java Arrays类中定义的copyOfRange()方法来复制数组。例如,

//使用toString()和copyOfRange()方法
import java.util.Arrays;

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

        int[] source = {2, 3, 12, 4, 12, -2};

        //将整个源数组复制到目标
        int[] destination1 = Arrays.copyOfRange(source, 0, source.length);      
        System.out.println("destination1 = " + Arrays.toString(destination1)); 

        //从索引2复制到5(不包括5) 
        int[] destination2 = Arrays.copyOfRange(source, 2, 5); 
        System.out.println("destination2 = " + Arrays.toString(destination2));   
    }
}

输出结果

destination1 = [2, 3, 12, 4, 12, -2]
destination2 = [12, 4, 12]

在上面的示例中,请注意以下行:

int[] destination1 = Arrays.copyOfRange(source, 0, source.length);

在这里,我们可以看到我们正在创建destination1数组并同时将源数组复制到它。在调用copyOfRange()方法之前,我们不会创建destination1数组。要了解有关该方法的更多信息,请访问Java copyOfRange。

5.使用循环复制二维数组

类似于一维数组,我们还可以使用for循环来复制二维数组。例如,

import java.util.Arrays;

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

        int[][] source = {
            {1, 2, 3, 4}, 
            {5, 6},
            {0, 2, 42, -4, 5}
        };

        int[][] destination = new int[source.length][];

        for (int i = 0; i<destination.length; ++i) {

            //为目标数组的每一行分配空间
            destination[i] = new int[source[i].length];

            for (int j = 0; j<destination[i].length; ++j) {
                destination[i][j] = source[i][j];
            }
        }

        //显示目标数组
        System.out.println(Arrays.deepToString(destination));  

    }
}

输出

[[1, 2, 3, 4], [5, 6], [0, 2, 42, -4, 5]]

在上面的程序中,请注意以下行:

System.out.println(Arrays.deepToString(destination);

在这里,使用deepToString()方法提供二维数组的更好表示。要了解更多信息,请访问Java deepToString()。

使用arraycopy()复制二维数组

为了使上面的代码更简单,我们可以使用System.arraycopy()替换内部循环,就像处理一维数组一样。例如,例如,

import java.util.Arrays;

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

        int[][] source = {
            {1, 2, 3, 4}, 
            {5, 6},
            {0, 2, 42, -4, 5}
        };

        int[][] destination = new int[source.length][];

        for (int i = 0; i<source.length; ++i) {

            //为目标数组的每一行分配空间
            destination[i] = new int[source[i].length];
            System.arraycopy(source[i], 0, destination[i], 0, destination[i].length);
        }

        //显示目标数组
        System.out.println(Arrays.deepToString(destination));      
    }
}

输出

[[1, 2, 3, 4], [5, 6], [0, 2, 42, -4, 5]]

在这里,我们可以看到,通过用arraycopy()方法替换内部for循环,可以得到相同的输出。

方法

方法注释

方法上使用文档注释/** */

方法定义

image-20240406195159881

方法的使用意义

1、不能把所有的代码都放在main中 --> 不方便维护代码

2、自由控制调用

3、实现代码的复用

方法使用的注意事项

  • 方法在内种没有先后顺序,但是不能把一个方法定义在另一个方法中。

  • 方法的返回值类型写void(无返回申明)时,方法内不能使用return返回数据,
    如果方法的返回值类型写了具体类型,方法内部则必须使用return返回对应类型的数据。

  • return语句的下面,不能编写代码,属于无效的代码,执行不到这儿。

  • 方法不调用就不会执行, 调用方法时,传给方法的数据,必须严格匹配方法的参数情况。

  • 调用有返回值的方法,有3种方式:
    ① 可以定义变量接收结果
    ② 或者直接输出调用,
    ③ 甚至直接调用;

  • 调用无返回值的方法,只有1种方式: 只能直接调用。

方法在计算机中的执行原理

方法调用过程

方法没有被调用的时候,都在方法区中的字节码文件(.class)中存储

方法被调用的时候,需要进入到栈内存中运行

我们知道Java程序的运行,都是在内存中执行的,而内存区域又分为栈、堆和方法区。那Java的方法是在哪个内存区域中执行呢?

答案是栈内存。 每次调用方法,方法都会进栈执行;执行完后,又会弹栈出去。

image-20240406195209844

image-20240406195214167

java方法参数传递的内存分配

重要!!!

1.方法的运行区域在哪里?

​ 答:栈内存。

2.栈有什么特点?方法为什么要在栈中运行程序?

​ 答:先进后出。保证一个方法调用完另一个方法后,可以回来继续执行。

参数传递

形参和实参

形参:全称形式参数,是指在定义方法时,所声明的参数
实参:全称实际参数,调用方法时,实际传入的参数

值类型的参数传递

Java的参数传递机制都是:值传递,传递的是实参存储的值的副本。

image-20240406195223455

引用类型的参数传递

我们发现调用change方法时参数是引用类型,实际上也是值传递,只不过参数传递存储的地址值。此时change方法和main方法中两个方法中各自有一个变量arrs,这两个变量记录的是同一个地址值[I@4c873330,change方法把数组中的元素改了,main方法在访问时,元素已经被修改了。

image-20240406195226175

基本类型和引用类型的参数在传递的时候有什么不同

- 都是值传递

- 基本类型的参数传递存储的数据值。

- 引用类型的参数传递存储的地址值。

java的参数传递(只有值传递)

值传递和引用传递最大的区别是传递的过程中有没有复制出一个副本来,如果是传递副本,那就是值传递,否则就是引用传递。

Java对象的传递,是通过复制的方式把引用关系传递了,因为有复制的过程,所以是值传递,只不过对于Java对象的传递,传递的内容是对象的引用。

方法重载

1.什么是方法重载?

​ 答:一个类中,多个方法的名称相同,但它们形参列表不同。

2.方法重载需要注意什么?

​ - 一个类中,只要一些方法的名称相同、形参列表不同,那么它们就是方法重载了,

​ 其它的都不管(如:修饰符,返回值类型是否一样都无所谓)。

​ - 形参列表不同指的是:形参的个数、类型、顺序不同,不关心形参的名称。

3、方法重载有啥应用场景?

​ 答:开发中我们经常需要为处理一类业务,提供多种解决方案,此时用方法重载来设计是很专业的。

可变参数

image-20240406195234956

在方法声明中,在指定参数类型后加一个省略号(...) 。

一个方法中只能指定一个可变参数,它必须是方法的最后一个参数。任何普通的参数必须在它之前声明。

public static void main(String[] args) {
        sum(1, 2, 3);
        sum(1, 2);
    }
    public static void sum(int... a){

        int sum = 0;
        for (int i = 0; i < a.length; i++) {
            sum += a[i];
        }
        System.out.println(sum);
    }

void方法中的return使用

return关键字的作用

  1. 只能返回一个值,并且终止方法运行

  2. 如果一个方法是void(没有返回值),也可以写return --> 只能终止方法运行

  3. 一个方法内可以有多个return,但是必须处在不同的作用域中

    一个作用域只能有一个,而且一定写在该作用域的最后一行

package com.itheima.test;

public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 9; i++) {
            if (i > 3) {
                return;
            }
            System.out.println(i);
        }
        System.out.println(123);
    }
}

// 输出结果
0
1
2
3

public static void main(String[] args) {
        // 目标:掌握return单独使用:return; 在无返回值方法中的作用:跳出并立即结束当前方法的执行。
        System.out.println("程序开始。。。");
        chu(10, 0);
        System.out.println("程序结束。。。");
    }

    public static void chu(int a, int b){
        if(b == 0){
            System.out.println("您的数据有问题,不能除0~~");
            return; // 跳出并结束当前方法的执行。
        }
        int c = a / b;
        System.out.println("除法结果是:" + c);
    }

// 输出结果
程序开始。。。
您的数据有问题,不能除0~~
程序结束。。。

练习

验证码

    public static void main(String[] args) {
        // 目标:完成生成随机验证码。
        System.out.println(createCode(8));
    }

    public static String createCode(int n){
        // 1、定义一个for循环用于控制产生多少位随机字符
        Random r = new Random();
        // 3、定义一个String类型的变量用于记住产生的每位随机字符
        String code = "";
        for (int i = 1; i <= n; i++) {
            // i = 1 2 3 4 5
            // 2、为每个位置生成一个随机字符:可能是数字、大小写字母。
            // 思路:随机一个0 1 2之间的数字出来,0代表随机一个数字字符,1、2代表随机大写字母,小写字母。
            int type = r.nextInt(3); // 0 1 2
            switch (type) {
                case 0:
                    // 随机一个数字字符
                    code += r.nextInt(10); // 0 - 9  code = code + 8
                    break;
                case 1:
                    // 随机一个大写字符 A 65   Z 65+25    (0 - 25) + 65
                    char ch1 = (char) (r.nextInt(26) + 65);
                    code += ch1;
                    break;
                case 2:
                    // 随机一个小写字符 a 97   z 97+25    (0 - 25) + 97
                    char ch2 = (char) (r.nextInt(26) + 97);
                    code += ch2;
                    break;
            }
        }
        return code;
    }

找素数

    public static void main(String[] args) {
        // 目标:完成找素数。
        System.out.println("当前素数的个数是:" + search(101, 200));
    }

    public static int search(int start, int end){
        int count = 0;
        // start = 101   end = 200
        // 1、定义一个for循环找到101到200之间的每个数据
        for (int i = start; i <= end ; i++) {
            // i = 101 102 103 ... 199 200

            // 信号位思想
            boolean flag = true; // 假设的意思:默认认为当前i记住的数据是素数。

            // 2、判断当前i记住的这个数据是否是素数。
            for (int j = 2; j <= i / 2; j++) {
                if(i % j == 0){
                    // i当前记住的这个数据不是素数了
                    flag = false;
                    break;
                }
            }

            // 3、根据判定的结果决定是否输出i当前记住的数据:是素数才输出展示。
            if(flag){
                System.out.println(i);
                count++;
            }
        }
        return count;
    }

数组拷贝

public static void main(String[] args) {
        // 目标:掌握数组拷贝。
        int[] arr = {11, 22, 33};
        int[] arr2 = copy(arr);
        printArray(arr2);

        // 注意:这个不是拷贝数组,叫把数组变量赋值给另一个数组变量。
//        int[] arr3 = arr;
//        arr3[1] = 666;
//        System.out.println(arr[1]);

        arr2[1] = 666;
        System.out.println(arr[1]);
    }

    public static int[] copy(int[] arr){
        // arr = [11, 22, 33]
        //        0    1   2

        // 1、创建一个长度一样的整型数组出来。
        int[] arr2 = new int[arr.length];
        // arr2 = [0, 0, 0]
        //         0  1  2

        // 2、把原数组的元素值对应位置赋值给新数组。
        for (int i = 0; i < arr.length; i++) {
            // i = 0 1 2
            arr2[i] = arr[i];
        }

        return arr2;
    }

    public static void printArray(int[] arr){
        System.out.print("[");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(i == arr.length - 1 ? arr[i] : arr[i] + ", ");
        }
        System.out.println("]");
    }

方法递归

  • 什么是递归?

    递归是一种算法,从形式上来说,方法调用自己的形式称之为递归。

  • 递归的形式:有直接递归、间接递归,如下面的代码。

/**
 * 目标:认识一下递归的形式。
 */
public class RecursionTest1 {
    public static void main(String[] args) {
        test1();
    }

    // 直接方法递归
    public static void test1(){
        System.out.println("----test1---");
        test1(); // 直接方法递归
    }

    // 间接方法递归
    public static void test2(){
        System.out.println("---test2---");
        test3();
    }

    public static void test3(){
        test2(); // 间接递归
    }
}

如果直接执行上面的代码,会进入死循环,最终导致栈内存溢出

1667660323234

image-20240406195244046

方法递归求阶乘

为了弄清楚递归的执行流程,接下来我们通过一个案例来学习一下。

案例需求:计算n的阶乘,比如5的阶乘 = 1 * 2 * 3 * 4 * 5 ; 6 的阶乘 = 1 * 2 * 3 * 4 * 5 * 6

分析需求用递归该怎么做

假设f(n)表示n的阶乘,那么我们可以推导出下面的式子
	 f(5) = 1*2*3*4*5
    f(5) = f(4)*5
    f(4) = f(3)*4
    f(3) = f(2)*3
    f(2) = f(1)*2
    f(1) = 1
总结规律:
	除了f(1) = 1; 出口
	其他的f(n) = f(n-1)*n

我们可以把f(n)当做一个方法,那么方法的写法如下

package com.itheima.d2_recursion;

/**
 * 目标:掌握递归的应用,执行流程和算法思想。
 */
public class RecursionTest2 {
    public static void main(String[] args) {
        System.out.println("5的阶乘是:" + f(5));
    }

    public static int f(int n){
        // 终结点
        if(n == 1){
            return 1;
        }else {
            return f(n - 1) * n;
        }
    }
}

递归的优缺点

进行递归调用时,将在堆栈上分配新的变量存储位置。随着每个递归调用的返回,旧的变量和参数将从堆栈中删除。因此,递归通常使用更多的内存,并且通常很慢。

另一方面,递归解决方案要简单得多,并且花费更少的时间来编写,调试和维护。

方法递归在计算机的执行原理

image-20240406195249386

面向对象

面向对象基础

面向对象既可以复用属性(变量),也可以复用行为

面相过程只能复用行为

对象相关注释

属性使用文档注释

创建并使用对象的流程

  1. 常见一个类 --> 类是对象的模板
  2. 给类思考对象应该具备的属性(private成员变量)和行为(没有static的成员方法)
  3. 生成构造器以及get/set
  4. 使用new 调用构造器创建对象

对象执行原理

其实Student s1 = new Student();这句话中的原理如下

  • Student s1表示的是在栈内存中,创建了一个Student类型的变量,变量名为s1
  • new Student()会在堆内存中创建一个对象,而对象中包含学生的属性名和属性值
    同时系统会为这个Student对象分配一个地址值0x4f3f5b24
  • 接着把对象的地址赋值给栈内存中的变量s1,通过s1记录的地址就可以找到这个对象
  • 当执行s1.name=“播妞”时,其实就是通过s1找到对象的地址,再通过对象找到对象的name属性,再给对象的name属性赋值为播妞;

搞明白Student s1 = new Student();的原理之后,Student s2 = new Student();原理完全一样,只是在堆内存中重新创建了一个对象,又有一个新的地址。s2.name是访问另对象的属性。

image-20240406195253998

image-20240406195258553

注意事项

image-20240406195302077

多个变量指向同一个对象

image-20240406195305499

this

原理

this就是一个变量,用在方法中,可以拿到当前类的对象。

我们看下图所示代码,通过代码来体会这句话到底是什么意思。哪一个对象调用方法方法中的this就是哪一个对象

image-20240406195310859

image-20240406195318451

this应用场景

通过this在方法中可以访问本类对象的成员变量。

this主要用来解决:变量名称冲突问题的。

image-20240406195322146

构造方法(构造器)

1.什么是构造器?

image-20240406195326079

答:构造器其实是一种特殊的方法,但是这个方法没有返回值类型,方法名必须和类名相同。

2.构造器什么时候执行?

​ 答:new 对象就是在执行构造方法;

3.构造方法的应用场景是什么?

​ 答:在创建对象时,可以用构造方法给成员变量赋值

一次4.构造方法有哪些注意事项?

1)在设计一个类时,如果不写构造器,Java会自动生成一个无参数构造器。

2)一定定义了有参数构造器,Java就不再提供空参数构造器,此时建议自己加一个无参数构造器。

重点

  • 构造函数在实例化对象时被调用一次,但是setter方法可以一直修改,构造器只适合使用在参数比较少的情况(7个以下)。

  • 创建构造函数的两个规则是:

    • 构造函数的名称应与类的名称相同。

    • Java构造函数不得具有返回类型。

  • 如果类没有构造函数,则Java编译器会在运行时自动创建默认构造函数。默认构造函数使用默认值初始化实例变量。例如,int变量将被初始化为0

image-20240406195329582

  • 构造函数类型:

    • 无参数构造函数 - 不接受任何参数的构造函数

    • 默认构造函数 - 如果没有显式定义,Java编译器会自动创建一个构造函数。

    • 参数化构造函数 - 接受参数的构造函数

  • 构造函数不能是抽象的abstract 、static或final。

  • 构造函数可以重载,但不能被重写。

封装

所谓封装,就是用类设计对象处理某一个事物的数据时,应该把要处理的数据,以及处理数据的方法,都设计到一个对象中去。

加private保证变量的安全性,选择性的提供get和set方法

封装的设计原则:合理隐藏,合理暴露

class Person {
    private int age;

    public int getAge() {
        return age;
    }

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

class Main {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.setAge(24);
        System.out.println("我的年龄是 " + p1.getAge());
    }
}

修饰符

数据隐藏是一种通过隐藏实现细节来限制数据成员访问的方法。

封装还提供了一种隐藏数据的方法。

数据隐藏可以通过访问修饰符来实现。在Java中,有四个访问修饰符:

Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。

  • default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
  • private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
  • public : 对所有类可见。使用对象:类、接口、变量、方法
  • protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)

我们可以通过以下表来说明访问权限:

image-20240409153132911

修饰符 当前类 同一包内 子孙类(同一包) 子孙类(不同包) 其他包
public Y Y Y Y Y
protected Y Y Y Y/N(说明) N
default Y Y Y N N
private Y N N N N

image-20240406195335652

protected说明

protected只能在当前类、同包或子类中访问,在其他类中创建子类的对象也无法访问

子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;
子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能通过创建基类实例访问基类的protected方法。

package pack1;

public class Father {
	protected void name() {
		System.out.println("Fat.name()");
	}
}

package pack2;
import pack1.Father;
public class Son extends Father {
	public static void main(String[] args) {
		Father f=new Father();
		f.name();//The method name() from the type Father is not visible 不能通过基类实例访问protected方法
		Son s=new Son();
		s.name();//成功访问
	}
}

总结:也就是说如果类中的某个成员被声明为protected时,那么在其他包中无法通过创建该类的实例来访问它,只能通过子类来访问它。以此实现保护的作用。

JavaBean实体类

1.JavaBean实体类是什么?有啥特点

​ JavaBean实体类,是一种特殊的类;它需要私有化成员变量,有无参构造方法、同时提供get和set方法;

JavaBean实体类仅仅只用来封装数据,只提供对数据进行存和取的方法

2.JavaBean的应用场景?

JavaBean实体类,只负责封装数据,而把数据处理的操作放在其他类中,以实现数据和数据处理的业务类相分离。

public class Student {
    // 1、必须私有成员变量,并为每个成员变量都提供get set方法
    private String name;
    private double score;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getScore() {
        return score;
    }
    public void setScore(double score) {
        this.score = score;
    }
    // 2、必须为类提供一个公开的无参数构造器
    public Student() {
    }
    public Student(String name, double score) {
        this.name = name;
        this.score = score;
    }  
}

成员变量和局部变量

image-20240406195340993

  • 把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
  • 如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。
  • 包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。

注意事项

  • 如果当前程序中,要调用自己所在包下的其他程序,可以直接调用。(同一个包下的类,互相可以直接调用)
  • 如果当前程序中,要调用其他包下的程序,则必须在当前程序中导包, 才可以访问!导包格式:import 包名.类名
  • 如果当前程序中,要调用Java.lang包下的程序,不需要我们导包的,可以直接使用。
  • 如果当前程序中,要调用多个不同包下的程序,而这些程序名正好一样,此时默认只能导入一个程序,另一个程序必须带包名访问。

String和ArrayList

String

image-20240406195358045

        // 目标:掌握创建String对象,并封装要处理的字符串的两种方式。
        // 1、直接双引号得到字符串对象,封装字符串数据
        String name = "黑马666";
        System.out.println(name);

        // 2、new String创建字符串对象,并调用构造器初始化字符串
        String rs1 = new String();
        System.out.println(rs1); // ""

        String rs2 = new String("itheima");
        System.out.println(rs2);

        char[] chars = {'a', '黑', '马'};
        String rs3 = new String(chars);
        System.out.println(rs3);

        byte[] bytes = {97, 98, 99};
        String rs4 = new String(bytes);
        System.out.println(rs4);

String类常用方法

- 构造方法
  String();								创建一个内容为空的字符串对象
         		String(char[] arr);						根据传入的字符数组创建一个字符串对象
         		String(char[] arr,int index,int count); 根据传入的字符数组的一部分来创建字符串对象
         		String(byte[] arr);						根据传入的字节数组创建一个字符串对象
         		String(byte[] arr,int index,int count);	根据传入的字节数组的一部分来创建字符串对象
         		String(String s);						根据传入的字符串来创建一个字符串对象

- 判断功能
  boolean equals(String s);				判断两个字符串内容是否相同。区分大小写
  boolean equalsIgnoreCase(String s);		判断两个字符串内容是否相同。不区分大小写
  boolean startsWith(String s);			判断当前字符串是否以传入的字符串为开头
  boolean endsWith(String s);				判断当前字符串是否以传入的字符串为结尾
  boolean contains(String s);				判断当前字符串中是否包含传入的字符串
  boolean isEmpty();						判断字符串是否为空

- 获取功能
  	
	        int length();							获取字符串的长度
    		char charAt(int index);					获取传入的索引上的对应的字符
    		**int indexOf(String s);**					**获取传入的字符串在当前字符串中第一次出现的索引位置**
    		**int lastIndexOf(String s);**				**获取传入的字符串在当前字符串中最后一次出现的索引位置**
    		String concat(String s);				拼接字符串
    		String substring(int index);			从指定位置开始截取。默认到末尾
    		String substring(int start,int end);	截取指定范围的字符串。包含开始、不包含结束

- 转换功能
  char[] toCharArray();					将字符串转成字符数组
  byte[] getBytes();						将字符串转成字节数组
  String replace(String oldStr,String newStr);	用 新字符串 替换所有的 老字符串
  String replace(char oldStr,char newStr);	用 新字符 替换所有的 老字符
  String toUpperCase();					将字符串转成大写
  String toLowerCase();					将字符串转成小写

- 其他功能
  String[] split(String regex);			按照指定规则切割字符串
  String trim();							去除字符串两端的空白
          
 	   boolean isEmpty()			字符串长度为0返回true,反之返回false

	   boolean isBlank()				字符串长度为0或者仅包含空格返回true,反之返回false
        // 目标:快速熟悉String提供的处理字符串的常用方法。
        String s = "黑马Java";
        // 1、获取字符串的长度
        System.out.println(s.length());

        // 2、提取字符串中某个索引位置处的字符
        char c = s.charAt(1);
        System.out.println(c);

        // 字符串的遍历
        for (int i = 0; i < s.length(); i++) {
            // i = 0 1 2 3 4 5
            char ch = s.charAt(i);
            System.out.println(ch);
        }

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

        // 3、把字符串转换成字符数组,再进行遍历
        char[] chars = s.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            System.out.println(chars[i]);
        }

        // 4、判断字符串内容,内容一样就返回true
        String s1 = new String("黑马");
        String s2 = new String("黑马");
        System.out.println(s1 == s2); // false
        System.out.println(s1.equals(s2)); // true

        // 5、忽略大小写比较字符串内容
        String c1 = "34AeFG";
        String c2 = "34aEfg";
        System.out.println(c1.equals(c2)); // false
        System.out.println(c1.equalsIgnoreCase(c2)); // true

        // 6、截取字符串内容 (包前不包后的)
        String s3 = "Java是最好的编程语言之一";
        String rs = s3.substring(0, 8);
        System.out.println(rs);

        // 7、从当前索引位置一直截取到字符串的末尾
        String rs2 = s3.substring(5);
        System.out.println(rs2);

        // 8、把字符串中的某个内容替换成新内容,并返回新的字符串对象给我们
        String info = "这个电影简直是个垃圾,垃圾电影!!";
        String rs3 = info.replace("垃圾", "**");
        System.out.println(rs3);

        // 9、判断字符串中是否包含某个关键字
        String info2 = "Java是最好的编程语言之一,我爱Java,Java不爱我!";
        System.out.println(info2.contains("Java"));
        System.out.println(info2.contains("java"));
        System.out.println(info2.contains("Java2"));

        // 10、判断字符串是否以某个字符串开头。
        String rs4 = "张三丰";
        System.out.println(rs4.startsWith("张"));
        System.out.println(rs4.startsWith("张三"));
        System.out.println(rs4.startsWith("张三2"));

        // 11、把字符串按照某个指定内容分割成多个字符串,放到一个字符串数组中返回给我们
        String rs5 = "张无忌,周芷若,殷素素,赵敏";
        String[] names = rs5.split(",");
        for (int i = 0; i < names.length; i++) {
            System.out.println(names[i]);
        }

    // indexOf() 返回值
    // 返回指定字符/字符串的第一个匹配项的索引
    // 如果找不到指定的字符/字符串,则返回 -1。
    String str1 = "Learn Java";
    int result;

    //获取字符“ J”的索引
    result = str1.indexOf('J');
    System.out.println(result); // 6

    //获取“ ava”的索引
    result = str1.indexOf("ava");
    System.out.println(result); // 7

    //子字符串不在字符串中
    result = str1.indexOf("java");
    System.out.println(result); // -1

    //字符串中空字符串的索引
    result = str1.indexOf("");
    System.out.println(result); // 0

    }

注意事项

注意事项1:String类的对象是不可变的对象

String是一种不可变对象. 字符串中的内容是不可改变。字符串不可被修改

image-20240406195407186

“”和new字符串区别

  • 在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;
  • 在JDK7.0版本,字符串常量池被移到了堆中了。
  1. 只要是以“...”方式写出的字符串对象,会存储到字符串常量池,且相同内容的字符串只存储一份

image-20240406195415890

  1. 但通过new方式创建字符串对象,每new一次都会产生一个新的对象放在堆内存中。

image-20240406195419446

面试

image-20240406195424590

image-20240406195431900

String[] arr = {"你","你","你"};
System.out.println(arr[0]==arr[1]);
System.out.println(arr[0]==arr[2]);
// true true

image-20240406195436361

""和new字符串的内存图

image-20240406195445589

面试

StringBuilder sb = new StringBuilder("你好");
StringBuilder sb1 = new StringBuilder("你好");
System.out.println(sb==sb1); // false

String s = sb.toString();
String s1 = sb1.toString();
System.out.println(s==s1); // false
String s = new String("hello");
char[] c = {'h','e','l','l','o'};
System.out.println(s.equals(c));    // false

字符串常量池复用机制

如果一个常量已经存在常量池汇总,那么不会重复创建,而是使用之前已经存在的常量

随机产生验证码

    public static void main(String[] args) {
        System.out.println(createCode(4));
        System.out.println(createCode(6));
    }

    /**
       1、设计一个方法,返回指定位数的验证码
     */
    public static String createCode(int n){
        // 2、定义2个变量 一个是记住最终产生的随机验证码 一个是记住可能用到的全部字符
        String code = "";
        String data = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

        Random r = new Random();
        // 3、开始定义一个循环产生每位随机字符
        for (int i = 0; i < n; i++) {
            // 4、随机一个字符范围内的索引。
            int index = r.nextInt(data.length());
            // 5、根据索引去全部字符中提取该字符
            code += data.charAt(index); // code = code + 字符
        }
        // 6、返回code即可
        return code;
    }

为什么StringBuilder类的append方法可以提高字符串拼接的效率

如果用+拼接,每次都会创建一个新的StringBuilder对象,调用append方法,并且最后调用toString方法

如果使用StringBuilder来拼接的话,只会创建一个StringBuilder对象,调用一次toString方法

image-20240406195450479

image-20240405113833309

ArrayList

ArrayList类是List接口的实现,允许我们创建可调整大小的数组。

image-20240406195454026

Java 数组 与 ArrayList

在Java中,我们需要先声明数组的大小,然后才能使用它。一旦声明了数组的大小,就很难更改它。

要解决此问题,我们可以使用ArrayList类。 java.util包中存在的ArrayList类允许我们创建可调整大小的数组。

与数组不同,当我们向数组列表添加或删除元素时,数组列表(ArrayList类的对象)可以自动调整其容量。 因此,数组列表也称为动态数组。

创建ArrayList

这是我们可以在Java中创建数组列表的方法:

ArrayList arrayList= new ArrayList<>();

此处,Type指示集合元素的类型。例如,

//创建整数类型arraylist
ArrayList<Integer> arrayList = new ArrayList<>();

//创建字符串类型arraylist
ArrayList<String> arrayList = new ArrayList<>();

在上面的程序中,我们使用了Integer和String。 在这里,Integer是int类型的相应包装类。

包装类是包装原始数据类型的类。例如,Integer类包装了int类型,Float类包装了Float类型,等等。

注意:我们不能创建原始数据类型(如int,float,char等)的集合。相反,我们必须使用它们对应的包装器类。

对于字符串,String是一个类,没有包装类。因此,我们按原样使用String。

我们还可以使用List接口创建ArrayList。这是因为ArrayList类实现了List接口。

List list = new ArrayList<>();

方法

image-20240406195458634

    public static void main(String[] args) {
        // 1、创建一个ArrayList的集合对象
        // ArrayList<String> list = new ArrayList<String>();
        // 从jdk 1.7开始才支持的
        ArrayList<String> list = new ArrayList<>();

        list.add("黑马");
        list.add("黑马");
        list.add("Java");
        System.out.println(list);

        // 2、往集合中的某个索引位置处添加一个数据
        list.add(1, "MySQL");
        System.out.println(list);

        // 3、根据索引获取集合中某个索引位置处的值
        String rs = list.get(1);
        System.out.println(rs);

        // 4、获取集合的大小(返回集合中存储的元素个数)
        System.out.println(list.size());

        // 5、根据索引删除集合中的某个元素值,会返回被删除的元素值给我们
        System.out.println(list.remove(1));
        System.out.println(list);

        // 6、直接删除某个元素值,删除成功会返回true,反之
        System.out.println(list.remove("Java"));
        System.out.println(list);

        list.add(1, "html");
        System.out.println(list);

        // 默认删除的是第一次出现的这个黑马的数据的
        System.out.println(list.remove("黑马"));
        System.out.println(list);

        // 7、修改某个索引位置处的数据,修改后会返回原来的值给我们
        System.out.println(list.set(1, "黑马程序员"));
        System.out.println(list);
    }

案例-从集合中删除指定元素

接下来,我们学习一个ArrayList的应用案例,需求如下:

image-20240406195502847

我们分析一下这个案例的步骤该如何实现:

1.用户可以选购多个商品,可以创建一个ArrayList集合,存储这些商品
2.按照需求,如果用户选择了"枸杞"批量删除,应该删除包含"枸杞"的所有元素
    1)这时应该遍历集合中每一个String类型的元素
    2)使用String类的方法contains判断字符串中是否包含"枸杞"
    3)包含就把元素删除
3.输出集合中的元素,看是否包含"枸杞"的元素全部删除

按照分析的步骤,完成代码

public class ArrayListTest2 {
    public static void main(String[] args) {
        // 1、创建一个ArrayList集合对象
        ArrayList<String> list = new ArrayList<>();
        list.add("枸杞");
        list.add("Java入门");
        list.add("宁夏枸杞");
        list.add("黑枸杞");
        list.add("人字拖");
        list.add("特级枸杞");
        list.add("枸杞子");
        System.out.println(list);
        //运行结果如下: [Java入门, 宁夏枸杞, 黑枸杞, 人字拖, 特级枸杞, 枸杞子]
       
        // 2、开始完成需求:从集合中找出包含枸杞的数据并删除它
        for (int i = 0; i < list.size(); i++) {
            // i = 0 1 2 3 4 5
            // 取出当前遍历到的数据
            String ele = list.get(i);
            // 判断这个数据中包含枸杞
            if(ele.contains("枸杞")){
                // 直接从集合中删除该数据
                list.remove(ele);
            }
        }
        System.out.println(list);
        //删除后结果如下:[Java入门, 黑枸杞, 人字拖, 枸杞子]
    }
}

运行完上面代码,我们会发现,删除后的集合中,竟然还有黑枸杞,枸杞子在集合中。这是为什么呢?

枸杞子被保留下来,原理是一样的。可以自行分析。

image-20240406195506710

那如何解决这个问题呢?这里打算给大家提供两种解决方案:

  • 集合删除元素方式一:每次删除完元素后,让控制循环的变量i--就可以了;如下图所示
  • image-20240406195510045

具体代码如下:

// 方式一:每次删除一个数据后,就让i往左边退一步
for (int i = 0; i < list.size(); i++) {
    // i = 0 1 2 3 4 5
    // 取出当前遍历到的数据
    String ele = list.get(i);
    // 判断这个数据中包含枸杞
    if(ele.contains("枸杞")){
        // 直接从集合中删除该数据
        list.remove(ele);
        i--;
    }
}
System.out.println(list);
  • 集合删除元素方式二:我们只需要倒着遍历集合,在遍历过程中删除元素就可以了

image-20240406195513789

具体代码如下:

// 方式二:从集合的后面倒着遍历并删除
// [Java入门, 人字拖]
//   i
for (int i = list.size() - 1; i >= 0; i--) {
    // 取出当前遍历到的数据
    String ele = list.get(i);
    // 判断这个数据中包含枸杞
    if(ele.contains("枸杞")){
        // 直接从集合中删除该数据
        list.remove(ele);
    }
}
System.out.println(list);

==和equals的区别

"=="和equals区别

  • "=="是运算符,如果是基本数据类型,则比较存储的值;如果是引用数据类型,则比较所指向对象的地址值。
  • equals是Object的方法,比较的是所指向的对象的地址值,一般情况下,重写之后比较的是对象的值。基本数据类型不能使用equals方法进行比较

String类的==和equals方法

一般情况下,类会重写equals方法用来比较两个对象的内容是否相等。比如String类中的equals()是被重写了,比较的是对象的值

如果类没有重写equals方法,其比较的还是引用类型的地址。

public static void main(String[] args) {
        //基本数据类型的比较
        int num1 = 10;
        int num2 = 10;
        System.out.println(num1 == num2);   //true

        //引用数据类型的比较
        //String类(重写了equals方法)中==与equals的比较
        
        String s1 = "hello";
        String s2 = "hello";
        System.out.println(s1 == s2);    //true,比较地址值:内容相同,因为常量池中只有一个“hello”,所以它们的地址值相同
        System.out.println(s1.equals(s2));//true,比较内容:内容相同,因为常量池中只有一个“hello”,所以它们的地址值相同
        System.out.println(s1.equals("hello")); //true

       
        String s3 = new String("hello");
        String s4 = new String("hello");
        System.out.println(s3 == s4);        //false,比较地址值:s3和s4在堆内存中的地址值不同
        System.out.println(s3.equals(s4));    //true,比较内容:内容相同

    
        //没有重写equals方法的类中==与equals的比较 
        People p1 = new People();
        People p2 = new People();
        People p = p2;
        System.out.println(p1);//People@135fbaa4
        System.out.println(p2);//People@45ee12a7
        System.out.println(p); //People@45ee12a7
        System.out.println(p1.equals(p2));       //false,p1和p2的地址值不同
        System.out.println(p.equals(p2));        //true,p和p2的地址值相同
}

创建字符串对象一般有如下两种写法

​ String s1 = "hello";//在字符串常量池中创建"hello",并将地址值赋值给s1。

​ String s2 = new String("world");//通过new关键字在堆中创建对象,并将对象地址值赋值给s2。

​ • 对于String s2 = new String(“world”);

​ 首先在堆内存中申请内存存储String类型的对象,将地址值赋给s2;

​ 在方法区的常量池中找,有无hello:

​ 若没有,则在常量池中开辟空间存储hello,并将该空间的地址值赋给堆中存储对象的空间;

​ 若有,则直接将hello所在空间的地址值给堆中存储对象的空间。

​ • 对于String s1 = “hello”;

​ 在方法区的常量池中找,有无hello,如果没有,就在常量池中开辟空间存储hello。

​ 然后只需要将hello所在空间的地址值赋给 s1。

字符串作为最基础的数据类型,使用非常频繁,如果每次都通过 new 关键字进行创建,会耗费高昂的时间和空间代价。Java 虚拟机为了提高性能和减少内存开销,就设计了字符串常量池. 在JDK1.7之前字符串常量池是存储在方法区的。JDK1.7之后存储在堆中了。

为什么重写equals方法就一定要重写hashCode方法

hashCode 和 equals 两个方法是用来协同判断两个对象是否相等的,采用这种方式的原因是可以提高程序插入和查询的速度。

如果只重写equals方法,不重写hashCode方法,就有可能导致a.equals(b)这个表达式成立,但是hashCode却不同。会造成一个完全相同的对象会存储在hash表的不同位置。

为什么要重写 equals 方法

Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。大多数情况下不重写equals方法,直接使用Object中的equals方法是没有任何意义的,不如直接使用==运算符,二者是等价的。

两个不同的引用,不重写equals,直接调用Object类中的原始equals方法,比较结果一定是false,这种比较没有意义,因为内存地址不同,即使两个对象的内容完全相同,因此通常情况下,我们要判断两个对象是否相等,一定要重写 equals 方法,这就是为什么要重写 equals 方法的原因。

实例练习

1、下面的代码将创建几个字符串对象?3个

String s1 = new String(“Hello”);

String s2 = new String(“Hello”);

分别创建了s1堆内存上的实例,s2内存上的实例,以及字符串常量池

2.在java中,String s=new String(“xyz”)创建了几个对象?C

A 1个 B 1个或2个 C 2个 D 以上都不对

3.下面的代码输出什么?

String s1 = new String(“abc”);

String s2 = new String(“abc”);

System.out.println(s1 == s2);//false

System.out.println(s1.equals(s2)); // true

4.下面的代码输入什么?

String s1 = "abc"; // s1指向常量池的abc

String s2 = new String("abc"); // s2指向堆内存的abc

//可以拿到s2的常量

s2.intern(); //返回常量池中abc的引用

System.out.println(s1 ==s2); //false

5.下面的代码输出什么?

String s1= "abc";

String s2= "abc";

String s3 = new String("abc");

String s4 = new String("abc");

System.out.println("s3 == s4 : "+(s3==s4)); //false

System.out.println("s3.equals(s4) : "+(s3.equals(s4))); //true

System.out.println("s1 == s3 : "+(s1==s3)); //false

System.out.println("s1.equals(s3) : "+(s1.equals(s3))); //true

System.out.println(s1==s2); //true

6.下面的代码输出什么?

String str1 = “ab” + “cd”;

String str11 = “abcd”;

System.out.println("str1 = str11 : "+ (str1 == str11)); //true

7.下面的代码输出什么?

String str2 = “ab”;

String str3 = “cd”;

String str4 = str2+str3;

String str5 = “abcd”;

System.out.println("str4 = str5 : " + (str4==str5));//false

8.下面的代码输出什么?(出现final的地方都会被换成其记住的字面量)

final String str2 = “ab”;

final String str3 = “cd”;

String str4 = str2+str3;

String str5 = “abcd”;

System.out.println("str4 = str5 : " + (str4==str5));//true

9.下面的代码输入什么?(常量字符串和变量拼接时(如:String str3=baseStr + “01”;)会调用StringBuilder.append()在堆上创建新的对象。)

String str6 = “b”;

String str7 = “a” + str6;

String str67 = “ab”;

System.out.println("str7 = str67 : "+ (str7 == str67)); //false

10.下面的代码输入什么?

final String str8 = “b”;

String str9 = “a” + str8;

String str89 = “ab”;

System.out.println("str9 = str89 : "+ (str9 == str89)); //true

11.下面选项结果为true的是:C

String s1="Hello";

String s2="hello";

String s3=s1.toLowerCase();

String s4=s2.toLowerCase();

A.s1==s3

B.s2==s3

C.s2==s4

D.s3==s4

因为String类是不可变类,调用toLowerCase()函数不会修改原始字符串,而是返回一个新的字符串。因此,如果需要使用转换后的字符串,一定要将它赋值给一个新的变量。

当传入的字符串与转换为小写字母后的字符串相同时,返回的就是原字符串,所以输出判断时,字符串常量池已有指定的内容”hello“,直接进行引用即可,并没有开辟新的空间,所以地址相同

传入的字符串与转换为小写字母后的字符串不同时,源码中指出此时返回的是new出来的String对象,而只要是构造方法new出来的对象都是要在堆中另开辟空间的,所以两者的地址不同,返回false;

String类中intern的用法:

//         jdk 1.7之后intern的用法:
//              intern方法还是会先去查询常量池中是否有已经存在,
//              如果存在,则返回常量池中的引用,这一点与之前jdk 1.6没有区别,区别在于,
//              如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而是在常量池中生成一个对原字符串的引用。
//              简单的说,就是往常量池放的东西变了:原来jdk 1.6在常量池中找不到时,复制一个副本放到常量池,
//                  1.7后则是将在堆上的地址引用复制到常量池。

//        String s1 = new String("a"); // 堆内存和常量池中,s1指向堆内存的a
//        s1.intern();  // 常量池中已经存在,返回常量池a的引用
//        String s2 = "a"; // 常量池中存在,s2指向常量池的a
//        System.out.println(s1==s2); // false s1指向堆内存,s2指向常量池
//        System.out.println(s1.intern() == s2); //true 都指向常量池里的a

//        String s1 = new String("a"); // 堆内存和常量池中,s1指向堆内存的a
//        String s2 = "a"; // 常量池中存在,s2指向常量池的a
//        s1.intern();  // 常量池中已经存在,返回常量池a的引用
//        System.out.println(s1==s2); // false s1指向堆内存,s2指向常量池
//        System.out.println(s2 == s1.intern()); // true 都指向常量池的a


//        String s1 = new String("a")+new String("b"); // s1指向堆内存中的ab
//        s1.intern();  // 常量池中没有ab,jdk 1.7后常量池存放的是堆内存中ab的引用
//        String s2 = "ab"; // 返回常量池中的那个引用,s2实际指向堆内存中的ab
//        System.out.println(s1==s2); // true
//        System.out.println(s1.intern() == s2); // true 实际都指向堆内存的ab


//        String s1 = new String("a")+new String("b"); // s1指向堆内存中的ab
//        String s2 = "ab"; // 常量池中不存在,创建ab,s2指向常量池中的ab
//        s1.intern(); // 常量池中已经存在,返回常量池ab的引用
//        System.out.println(s1==s2); // false
//        System.out.println(s1.intern() == s2); // true 都指向常量池的ab

String类中replace的用法

  • replace方法是区分大小写的,意味着它会将字符串中完全匹配的字符或子字符串替换为新的字符或子字符串。
  • 如果原始字符串中不存在要替换的字符或子字符串,则replace方法不会进行任何替换,直接返回原始字符串。
  • replace方法返回的是一个新的字符串,不会修改原始字符串对象。

String类中大小写转换的使用注意事项

在Java中,可以使用String类的toLowerCase()方法将字符串转换为小写,使用toUpperCase()方法将字符串转换为大写。这两个方法都不会改变原始字符串,而是返回一个新的字符串。

注意事项:

  1. 如果原始字符串中包含特殊字符或者非字母字符,这些字符不会被转换,会直接保留在结果字符串中。
  2. 如果原始字符串中的字符已经是小写或大写,它们会保持不变。
  3. 这两个方法都不会对字符串进行原地转换,而是返回一个新的字符串。

不同JDK版本的新特性

垃圾回收机制

概述

java相较于c、c++语言的优势之一是自带垃圾回收器,垃圾回收是指不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,程序员唯一能做的就是通过调用System.gc 方法来建议执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。

不同的虚拟机实现有着不同的 GC 实现机制,但是一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收。

  1. Allocation Failure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。
  2. System.gc():在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。

1、寻找垃圾

一般有两种方法来判断:

引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。但是他有一个缺点是不能解决循环引用的问题。

可达性分析算法:从 GC Root 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Root 没有任何引用链相连时,则证明此对象是可以被回收的。

引用计数器算法

1.引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
2、对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
3、优点: 实现简单,垃圾对象便于辨识; 判定效率高,回收没有延迟性。
4、缺点:
     1、它需要单独的字段存储计数器,这样的做法增加了存储 空间的开销。
     2、每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了 时间开销。
     3、引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。

1689927844950

可达性分析

JVM 把内存中所有的对象之间的引用关系看作一张图,通过一组名为”GC Root"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。如下图所示:

1689924410244

注意:上图中圆形图标虽然标记的是对象,但实际上代表的是此对象在内存中的引用。包括 GC Root 也是一组引用而并非对象。

GC Root 对象

在 Java 中,有以下几种对象可以作为 GC Root

  1. Java 虚拟机栈(局部变量表)中的引用的对象。
  2. 方法区中静态引用指向的对象。
  3. 仍处于存活状态中的线程对象。
  4. Native 方法中 JNI 引用的对象。

注意:全局变量同静态变量不同,它不会被当作 GC Root。

2、回收垃圾

标记清除算法(Mark and Sweep GC)

从”GC Roots”集合开始,将内存整个遍历一次,保留所有可以被 GC Roots 直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,过程分两步。

  1. Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
  2. Sweep 清除阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清除。

1686362834341

标记清除算法

  • 优点:实现简单,不需要将对象进行移动。
  • 缺点:这个算法需要中断进程内其他组件的执行(stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。

标记-压缩算法 (Mark-Compact)

需要先从根节点开始对所有可达对象做一次标记,之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。因此标记压缩也分两步完成:

  1. Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
  2. Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。

1686362746739

  • 优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
  • 缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。

复制算法(Copying)

将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

  1. 复制算法之前,内存分为 A/B 两块,并且当前只使用内存 A,内存的状况如下图所示:

image-20240409115929562

2.标记完之后,所有可达对象都被按次序复制到内存 B 中,并设置 B 为当前使用中的内存。内存状况如下图所示:

image-20240409115944634

  • 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
  • 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

总结

1689928767455

3、JVM分代回收

Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代,这就是 JVM 的内存分代策略。注意: 在 HotSpot 中除了新生代和老年代,还有永久代。

新生代

新生成的对象优先存放在新生代中,新生代对象存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,回收效率很高。新生代中因为要进行一些复制操作,所以一般采用的 GC 回收算法是复制算法。 新生代又可以继续细分为 3 部分:EdenSurvivor0(简称 S0)Survivor1(简称S1)。这 3 部分按照 8:1:1 的比例来划分新生代。

1686362776046

  • 绝大多数刚刚被创建的对象会存放在 Eden 区。
  • Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden 区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1 是空的。
  • 下一次 Eden 区满时,再执行一次垃圾回收。此次会将 EdenS0 区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0 变为空。
  • 如此反复在 S0S1 之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。

老年代

一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

我们可以使用 -XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法

注意:对于老年代可能存在这么一种情况,老年代中的对象有时候会引用到新生代对象。这时如果要执行新生代 GC,则可能需要查询整个老年代上可能存在引用新生代的情况,这显然是低效的。所以,老年代中维护了一个 512 byte 的 card table,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只需要检查这个 card table 即可,大大提高了性能。



static修饰符

static 修饰符,用来修饰类方法和类变量。

类变量

  • 静态变量:static 关键字用来声明独立于对象的静态变量,无论一个类示例化多少对象,它的静态变量只有一份拷贝。 静态变量也被称为类变量。局部变量不能被声明为 static 变量。

由于静态变量是属于类的,所有对象共有一份,只需要通过类名就可以调用:类名.静态变量

实例变量是属于对象的,每个对象都有一份,需要通过对象才能调用:对象.实例变量

image-20240409091203352

类变量的应用场景

在实际开发中,如果某个数据只需要一份,且希望能够被共享(访问、修改),则该数据可以定义成类变量来记住。

image-20240406195538277

类方法

  • 类方法:static修饰的方法,可以被类名调用,是因为它是随着类的加载而加载的;所以类名直接就可以找到static修饰的方法
  • 实例方法:非static修饰的方法,需要创建对象后才能调用,是因为实例方法中可能会访问实例变量,而实例变量需要创建对象后才存在。所以实例方法,必须创建对象后才能调用。

image-20240406195542231

类方法的应用场景

学习完static修饰方法之后,我们讲一个有关类方法的应用知识,叫做工具类。

如果一个类中的方法全都是静态的,那么这个类中的方法就全都可以被类名直接调用,由于调用起来非常方便,就像一个工具一下,所以把这样的类就叫做工具类。

工具类没有创建对象的需求,建议将工具类的构造器私有化

类方法的注意事项

类方法中可以直接访问类的成员,不可以直接访问实例成员。

实例方法中既可以直接访问类成员,也可以直接访问实例成员。

实例方法中可以出现this关键字,类方法中不可以出现this关键字的。

package com.itheima.d4_static_attention;

public class Student {
    static String schoolName; // 类变量
    double score; // 实例变量

    // 1、类方法中可以直接访问类的成员,不可以直接访问实例成员。
    public static void printHelloWorld(){
        // 注意:同一个类中,访问类成员,可以省略类名不写。
        schoolName = "黑马";
        printHelloWorld2();

        // System.out.println(score); // 报错的
        // printPass(); // 报错的

        // System.out.println(this); // 报错的
    }

    // 类方法
    public static void printHelloWorld2(){

    }

    // 2、实例方法中既可以直接访问类成员,也可以直接访问实例成员。
    // 实例方法
    // 3、实例方法中可以出现this关键字,类方法中不可以出现this关键字的
    public void printPass(){
        schoolName = "黑马2";
        printHelloWorld2();

        System.out.println(score);
        printPass2();

        System.out.println(this);
    }

    // 实例方法
    public void printPass2(){

    }
}

static代码块和实例代码块

image-20240406195558981

父子类的静态变量、静态代码块、实例变量、实例代码块和构造函数的执行顺序

静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。

public static String staticField = "静态变量";

static {
    System.out.println("静态代码块");
}

public String field = "实例变量";

{
    System.out.println("实例代码块");
}

最后才是构造函数的初始化。

public InitialOrderTest() {
    System.out.println("构造函数");
}

存在继承的情况下,初始化顺序为:

  • 父类(静态变量、静态代码块)
  • 子类(静态变量、静态语代码块)
  • 父类(实例变量、实例代码块)
  • 父类(构造方法)
  • 子类(实例变量、实例代码块)
  • 子类(构造方法)

单例设计模式

设计模式

设计模式是解决问题的最优解

饿汉式单例设计模式

image-20240406195614746

拿对象时对象就已经创建好了

package com.itheima.sta.singleton;

public class A {

    // 定义一个类变量用于接受类的一个对象
    private static A a = new A();

    // 外界就无法创建对象
    private A(){}

    // 提供一个公共的方法来获取当前类的对象
    public static A getInstance() {
        return a;
    }
}


package com.itheima.sta.singleton;

public class DemoTest {
    public static void main(String[] args) {
        A a = A.getInstance();
        A a1 = A.getInstance();
        System.out.println(a);
        System.out.println(a1);

        System.out.println(a == a1); // true
    }
}

懒汉式单例设计模式

image-20240409112838232

单例类的应用场景

image-20240406195630719

继承

image-20240409143345483

继承在计算机中的原理

image-20240406195648344

使用继承的优势

减少代码的重复编写,提高代码的复用性

单继承和多层继承

Java语言只支持单继承,不支持多继承,但是可以多层继承。

Java是单继承的:一个类只能继承一个直接父类;

Object类是Java中所有类的父类。

public class Test {
    public static void main(String[] args) {
        // 目标:掌握继承的两个注意事项事项。
        // 1、Java是单继承的:一个类只能继承一个直接父类;
        // 2、Object类是Java中所有类的祖宗。
        A a = new A();
        B b = new B();

        ArrayList list = new ArrayList();
        list.add("java");
        System.out.println(list.toString());
    }
}

class A {} //extends Object{}
class B extends A{}
// class C extends B , A{} // 报错
class D extends B{}

方法重写

当子类觉得父类方法不好用,或者无法满足父类需求时,子类可以重写一个方法名称、参数列表一样的方法,去覆盖父类的这个方法,这就是方法重写。

注意:重写后,方法的访问遵循就近原则

注意事项

  • 重写小技巧:使用Override注解,他可以指定java编译器,检查我们方法重写的格式是否正确,代码可读性也会更好。
  • 子类重写父类方法时,访问权限必须大于或者等于父类该方法的权限( public> protected > 缺省)。
  • 重写的方法返回值类型,必须与被重写方法的返回值类型一样,或者范围更小。
  • 私有方法、静态方法不能被重写如果重写会报错的。(静态方法不能被重写是因为静态方法是属于类的,不属于实例化对象,所以没有多态的说法,自然就不能被重写

子类中访问成员的原则

在子类方法中访问其他成员(成员变量、成员方法),是依照就近原则的

  • 先子类局部范围找。

  • 然后子类成员范围找。

  • 然后父类成员范围找,如果父类范围还没有找到则报错。

如果子类和父类出现同名变量或者方法,优先使用子类的;此时如果一定要在子类中使用父类的成员,可以加this或者super进行区分。

public class Z extends F {
    String name = "子类名称";

    public void showName(){
        String name = "局部名称";
        System.out.println(name); // 局部名称
        System.out.println(this.name); // 子类成员变量
        System.out.println(super.name); // 父类的成员变量
    }

    @Override
    public void print1(){
        System.out.println("==子类的print1方法执行了=");
    }

    public void showMethod(){
        print1(); // 子类的
        super.print1(); // 父类的
    }
}

子类构造器

默认情况下,子类全部构造器的第一行代码都是super(),它会默认调用父类的无参构造器

如果父类存在有参构造器,没有无参构造器,则必须在子类构造器的第一行手写super(...),指定去调用父类的有参构造器(原因是子类构造器默认第一行调用父类的无参构造器,但是父类只提供了有参构造器,Java则不再提供默认的无参构造器,所以子类的第一行必须显示地调用父类的有参构造器)

image-20240409151609282

package com.itheima.d14_extends_constructor;

class F{
//    public F(){
//        System.out.println("===父类F的 无参数构造器 执行了===");
//    }

    public F(String name, int age){

    }
}

class Z extends F{
    public Z(){
        // super(); // 默认存在的
        super("播妞", 17);
        System.out.println("===子类Z的 无参数构造器 执行了===");
    }

    public Z(String name){
        // super(); // 默认存在的
        super("播妞", 17);
        System.out.println("===子类Z的 有参数构造器 执行了===");
    }
}

public class Test {
    public static void main(String[] args) {
        // 目标:先认识子类构造器的特点,再掌握这个特点的常见应用场景。
        Z z = new Z();
        Z z2 = new Z("播妞");
    }
}

image-20240406195655157

子类构造器可以通过调用父类构造器,把对象中包含父类这部分的数据先初始化赋值,再回来把对象里包含子类这部分的数据也进行初始化赋值。

image-20240409175729774

this()调用兄弟构造器

image-20240406195658874

this和super总结

访问本类成员:

​ this.成员变量 //访问本类成员变量(如果当前类没有相关的成员变量,则会去父类找)

​ this.成员方法 //调用本类成员方法

​ this() //调用本类空参数构造器

​ this(参数) //调用本类有参数构造器

访问父类成员:

​ super.成员变量 //访问父类成员变量

​ super.成员方法 //调用父类成员方法

​ super() //调用父类空参数构造器

​ super(参数) //调用父类有参数构造器

注意:this和super访问构造方法,只能用到构造方法的第一句,否则会报错。

多态

什么是多态

多态是在继承、实现情况下的一种现象,表现为:对象多态(父类对象接收子类对象、接口接收实现类实例)、行为多态(子类方法重写、调用父类方法)

多态的前提:继承、父类引用子类成员、方法重写

image-20240406195703590

多态好处

  • 在多态形式下,右边对象是解耦合的,更便于扩展和维护。
  • 定义方法时,使用父类类型的形参,可以接收一切子类对象,扩展性更强、更便利。
public static void demo(Person p){
        if (p instanceof Teacher){
            Teacher t = (Teacher) p;
            t.teach();
        } else if (p instanceof Student) {
            Student s = (Student) p;
            s.study();
        }
    }

多态的问题

因为多态声明的是父类或者接口类型,所以无法访问子类或实现类的特有的方法

image-20240410094504440

类型转换

image-20240406195715407

多态访问成员变量和成员方法

多态是对象和行为的多态,不支持属性的多态

成员变量的访问规则

  • 直接通过对象名称访问成员变量:看等号左边是谁,优先用谁,没有则向上找。
  • 间接通过成员方法访问成员变量:看该方法属于谁,优先用谁,没有则向上找。
public class Fu /*extends Object*/{
    int num = 10;
 
    public void showNum(){
        System.out.println(num);
    }
}
 

public class Zi extends Fu {
    int num = 20;
    int age = 16;
 
    @Override
    public void showNum() {
        System.out.println(num);
    }
}
 

public class demoMult {
    public static void main(String[] args) {
//时用多态写法,父类引用指向子类对象
        Fu obj = new Zi();
        System.out.println( obj.num);	// 10
     //    System.out.println(obj.age);  错误写法
 
        System.out.println("==================");
        obj.showNum();  //没有覆盖重写 方法是父类的 就是10
                        //子类覆盖重写了就是子类的,就是20
}

成员方法的访问规则

看new的是谁,就优先用谁,没有则向上找。


public class Fu /*extends Object*/{
 
    public void method(){
        System.out.println("父类方法");
    }
    public void methodFu(){
        System.out.println("父类特有方法");
    }
 
}
 

public class Zi extends Fu {
    @Override
    public void method() {
        System.out.println("子类方法");
    }
 
    public void methodZi(){
        System.out.println("子类特有方法");
    }
}
 

 
public class demoMult {
    public static void main(String[] args) {
        Fu obj = new Zi();  //多态
 
        obj.method();  //父子都有,优先用子
 
        obj.methodFu(); //子类没有,父类有,向上找到父类。
 
       // 编译看左边,左边是Fu,Fu当中没有methodZi方法,所以编译报错。
      //   obj.methodZi();   错误
        ((Zi) obj).methodZi();
 
    } 
}

对比

成员变量:编译看左边,运行还看左边

成员方法:编译看左边,运行看右边。

final关键字

final 关键字是最终的意思,可以修饰(类、方法、变量)

修饰类:该类被称为最终类,特点是不能被继承了。

修饰方法:该方法被称为最终方法,特点是不能被重写了。

修饰变量:该变量只能被赋值一次。

final修饰的变量必须赋值,要么在定义时赋值,要么在构造器中赋值

final修饰变量的注意点

final修饰基本类型的变量,变量存储的数据不能被改变。

final修饰引用类型的变量,变量存储的地址不能被改变,但地址所指向对象的内容是可以被改变的。

// 注意final修饰引用类型变量,是指这个变量不能再指向其他对象,但是可以修改它指向的字段值

// final修饰数组不能重新赋值,但数组中的元素可以被更改
final int[] arr1 = {10, 20, 30};
// arr1 = null; // 第二次赋值
arr1[1] = 222;
System.out.println(arr1[1]);

常量

image-20240406195724267

抽象类

被abstract修饰的类,就是抽象类

被abstract修饰的方法,就是抽象方法(不允许有方法体)

image-20240406195728323

注意事项

抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类。

类该有的成员(成员变量、方法、构造器),抽象类都可以有。

抽象类最主要的特点:

抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现。

一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。

抽象类中不能有静态方法因为抽象类中的方法是需要被子类重写的,而静态方法是无法被重写的

抽象类肯定含有构造方法, 子类创建对象时调用子类构造方法,子类构造方法体执行前会先执行父类的构造方法

abstract与final不能同时使用

抽象类不能被实例化

抽象类不能被实例化的主要原因是抽象类本身存在抽象方法,而抽象方法没有具体的实现代码。因此,实例化一个抽象类没有意义,因为你无法为抽象方法提供具体的实现。

抽象类的主要作用是为其子类提供一个通用的模板和一组共享的方法签名,以便子类必须提供这些方法的具体实现。这些子类在提供具体实现之前不能被实例化。

当你创建一个抽象类时,你可以在其中定义一些抽象方法,然后让具体的子类来继承这个抽象类并实现这些抽象方法。子类必须提供这些方法的具体实现才能创建对象。

抽象类的好处

  • 用抽象类可以把父类中相同的代码,包括方法声明都抽取到父类,这样能更好的支持多态,提高代码的灵活性。

  • 反过来用,我们不知道系统未来具体的业务实现时,我们可以先定义抽象类,将来让子类去实现,以方便系统的扩展。

模板方法设计模式

image-20240406195731877

模板方法设计模式的写法

1、定义一个抽象类

2、抽象类中定义2个方法

  • 一个是模板方法:把相同的代码放到模板方法里面
  • 一个是抽象方法:具体的方法实现交给子类完成

建议使用final关键字修饰模板方法,因为模板方法是对象直接使用的,不能被子类重写,一旦子类重写了模板方法,模板方法就失效了

package com.itheima.d4_abstract_template_method;
// 模板方法设计模式
public abstract class C {
    // 模板方法
    public final void sing(){
        System.out.println("唱一首你喜欢的歌:");

        doSing();

        System.out.println("唱完了!");
    }

    public abstract void doSing();
}
------------------------------------------------------
package com.itheima.d4_abstract_template_method;

public class B extends C{
    @Override
    public void doSing() {
        System.out.println("我们一起学猫叫,喵喵喵喵喵喵喵~~");
    }
}
--------------------------------------------------------
package com.itheima.d4_abstract_template_method;

import java.text.DateFormat;

public class Test {
    public static void main(String[] args) {
        // 目标:搞清楚模板方法设计模式能解决什么问题,以及怎么写。
        B b = new B();
        b.sing();
    }
}

接口(实现类)

什么是接口(实现类)

Java提供了一个关键字interface,用这个关键字来定义接口这种特殊结构。格式如下

public interface 接口名{
    //成员变量(常量)
    //成员方法(抽象方法)
}

按照接口的格式,我们定义一个接口看看

public interface A{
    //这里public static final可以加,可以不加。
    // public static final String SCHOOL_NAME = "黑马程序员";

    //这里的public abstract可以加,可以不加。
    // public abstract void test();
}

public interface A {
    // 成员变量(常量)
    String SCHOOL_NAME = "黑马程序员";

    // 成员方法(抽象方法)
    void test();

}

写好A接口之后,在写一个测试类,用一下

public class Test{
    public static void main(String[] args){
        //打印A接口中的常量
        System.out.println(A.SCHOOL_NAME);

        //接口是不能创建对象的
        A a = new A();
    }
}

接口注意事项

我们发现定义好接口之后,是不能创建对象的。那接口到底什么使用呢?需要我注意下面两点

  • 接口是用来被类实现(implements)的,我们称之为实现类。
  • 一个类是可以实现多个接口的(接口可以理解成干爹),类实现接口必须重写所有接口的全部抽象方法,否则这个类也必须是抽象类

接口的好处

  • 弥补了类单继承的不足,一个类同时可以实现多个接口。
  • 让程序可以面向接口编程,这样程序员可以灵活方便的切换各种业务实现。

jdk8后接口新增的方法

default方法(只能通过接口的实现类调用)、静态方法(接口名调用)、私有方法(jdk9之后,只能在接口内部使用)

image-20240410182404474

image-20240406195749090

接口的多继承

image-20240406195752392

package com.itheima.d8_interface_jdk8;

public interface A {
    /**
     * 1、默认方法:必须使用default修饰,默认会被public修饰
     * 实例方法:对象的方法,必须使用实现类的对象来访问。
     */
    default void test1(){
        System.out.println("===默认方法==");
        test2();
    }

    /**
     * 2、私有方法:必须使用private修饰。(JDK 9开始才支持的)
     *   实例方法:对象的方法。
     */
    private void test2(){
        System.out.println("===私有方法==");
    }

    /**
     * 3、静态方法:必须使用static修饰,默认会被public修饰
     */
     static void test3(){
        System.out.println("==静态方法==");
     }

     void test4();
     void test5();
     default void test6(){

     }
}

接口的注意事项

1.一个接口继承多个接口,如果多个接口中存在相同的方法声明,则此时不支持多继承

2.一个类实现多个接口,如果多个接口中存在相同的方法声明,则此时不支持多实现

3.一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会优先使用父类的方法

4.一个类实现多个接口,多个接口中有同名的默认方法,则这个类必须重写该方法。

package com.itheima.d9_interface_attention;

import java.util.Calendar;
import java.util.Date;

public class Test2 {
    public static void main(String[] args) {
        // 目标:了解使用接口的几点注意事项。

//        Zi zi = new Zi();
//        zi.run();

    }
}

//// 1、一个接口继承多个接口,如果多个接口中存在方法签名冲突,则此时不支持多继承。
//interface I{
//    void test1();
//}
//interface J{
//    String test1();
//}
////interface K extends I, J{
////
////}

//// 2、一个类实现多个接口,如果多个接口中存在方法签名冲突,则此时不支持多实现。
////class E implements I, J{
////
////}

//// 3、一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会优先用父类的。
//class Fu{
//    public void run(){
//        System.out.println("===父类的run方法执行了===");
//    }
//}
//interface IT{
//    default void run(){
//        System.out.println("===接口IT中的run方法执行了===");
//    }
//}
//class Zi extends Fu implements IT {
//
//}

//// 4、一个类实现了多个接口,多个接口中存在同名的默认方法,可以不冲突,这个类重写该方法即可。
//interface It1{
//    default void test(){
//        System.out.println("IT1");
//    }
//}
//
//interface IT2{
//    default void test(){
//        System.out.println("IT2");
//    }
//}
//
//class N implements It1, IT2{
//    @Override
//    public void test() {
//        System.out.println("自己的");
//    }
//}

接口和抽象类的异同

不同点

● 抽象类中可以定义构造器,接口不能;

● 抽象类可以有抽象方法和具体方法,接口不能有具体方法;

● 接口中的成员全都是 public 的,抽象类中的成员可以使用private、public、protected、默认等修饰;

接口方法在JDK8之前只有public abstract,在JDK8可以有default方法和static方法,在JDK9中允许有private方法(只能在接口内部调用)

● 抽象类中可以定义成员变量,接口中只能是常量;

● 有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法;

● 抽象类中可以包含静态方法,接口中不能有静态方法(jdk8之前);

● 一个类只能继承一个抽象类,一个类可以实现多个接口;

相同点

● 不能够实例化;

● 可以将抽象类和接口类型作为引用类型;

● 一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类;

接口的综合案例

各位同学,关于接口的特点以及接口的好处我们都已经学习完了。接下来我们做一个案例,先来看一下案例需求

image-20240410162446909

首先我们写一个学生类,用来描述学生的相关信息

public class Student {
    private String name;
    private char sex;
    private double score;

    public Student() {
    }

    public Student(String name, char sex, double score) {
        this.name = name;
        this.sex = sex;
        this.score = score;
    }

    public String getName() {
        return name;
    }

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

    public char getSex() {
        return sex;
    }

    public void setSex(char sex) {
        this.sex = sex;
    }

    public double getScore() {
        return score;
    }

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

接着,写一个StudentOperator接口,表示学生信息管理系统的两个功能。

public interface StudentOperator {
    void printAllInfo(ArrayList<Student> students);
    void printAverageScore(ArrayList<Student> students);
}

然后,写一个StudentOperator接口的实现类StudentOperatorImpl1,采用第1套方案对业务进行实现。

public class StudentOperatorImpl1 implements StudentOperator{
    
    // 打印全部的学生信息
    @Override
    public void printAllInfo(ArrayList<Student> students) {
        System.out.println("----------全班全部学生信息如下--------------");
        for (int i = 0; i < students.size(); i++) {
            Student s = students.get(i);
            System.out.println("姓名:" + s.getName() + ", 性别:" + s.getSex() + ", 成绩:" + s.getScore());
        }
        System.out.println("-----------------------------------------");
    }
	
    // 打印所有学生的平均分
    @Override
    public void printAverageScore(ArrayList<Student> students) {
        double allScore = 0.0;
        for (int i = 0; i < students.size(); i++) {
            Student s = students.get(i);
            allScore += s.getScore();
        }
        System.out.println("平均分:" + (allScore) / students.size());
    }
}

接着,再写一个StudentOperator接口的实现类StudentOperatorImpl2,采用第2套方案对业务进行实现。

public class StudentOperatorImpl2 implements StudentOperator{
    
    // 打印所有的学生信息,并打印男女生人数
    @Override
    public void printAllInfo(ArrayList<Student> students) {
        System.out.println("----------全班全部学生信息如下--------------");
        int count1 = 0;
        int count2 = 0;
        for (int i = 0; i < students.size(); i++) {
            Student s = students.get(i);
            System.out.println("姓名:" + s.getName() + ", 性别:" + s.getSex() + ", 成绩:" + s.getScore());
            if(s.getSex() == '男'){
                count1++;
            }else {
                count2 ++;
            }
        }
        System.out.println("男生人数是:" + count1  + ", 女士人数是:" + count2);
        System.out.println("班级总人数是:" + students.size());
        System.out.println("-----------------------------------------");
    }
	
    // 打印去掉最高份和最低分的学生信息
    @Override
    public void printAverageScore(ArrayList<Student> students) {
        double allScore = 0.0;
        double max = students.get(0).getScore();
        double min = students.get(0).getScore();
        for (int i = 0; i < students.size(); i++) {
            Student s = students.get(i);
            if(s.getScore() > max) max = s.getScore();
            if(s.getScore() < min) min = s.getScore();
            allScore += s.getScore();
        }
        System.out.println("学生的最高分是:" + max);
        System.out.println("学生的最低分是:" + min);
        System.out.println("平均分:" + (allScore - max - min) / (students.size() - 2));
    }
}

再写一个班级管理类ClassManager,在班级管理类中使用StudentOperator的实现类StudentOperatorImpl1对学生进行操作

public class ClassManager {
    private ArrayList<Student> students = new ArrayList<>();
    private StudentOperator studentOperator = new StudentOperatorImpl1();

    public ClassManager(){
        students.add(new Student("迪丽热巴", '女', 99));
        students.add(new Student("古力娜扎", '女', 100));
        students.add(new Student("马尔扎哈", '男', 80));
        students.add(new Student("卡尔扎巴", '男', 60));
    }

    // 打印全班全部学生的信息
    public void printInfo(){
        studentOperator.printAllInfo(students);
    }

    // 打印全班全部学生的平均分
    public void printScore(){
        studentOperator.printAverageScore(students);
    }
}

最后,再写一个测试类Test,在测试类中使用ClassMananger完成班级学生信息的管理。

public class Test {
    public static void main(String[] args) {
        // 目标:完成班级学生信息管理的案例。
        ClassManager clazz = new ClassManager();
        clazz.printInfo();
        clazz.printScore();
    }
}

注意:如果想切换班级管理系统的业务功能,随时可以将StudentOperatorImpl1切换为StudentOperatorImpl2。自己试试

内部类

什么是内部类

内部类是类中的五大成分之一(成员变量、方法、构造器、内部类、代码块),如果一个类定义在另一个类的内部,这个类就是内部类。

当一个类的内部,包含一个完整的事物,且这个事物没有必要单独设计时,就可以把这个事物设计成内部类。

成员内部类

image-20240406195804386

内部类访问成员的特点

  • 既可以访问内部类成员、也可以访问外部类成员
  • 如果内部类成员和外部类成员同名,可以使用类名.this.成员区分
package com.itheima.d1_inner_class1;

public class Outer {
    private int age = 99;
    public static String a;
    // 成员内部类
    public class Inner{
        private String name;
        public static String schoolName; // JDK 16开始才支持定义静态成员的
        private int age = 88;

        public void test(){
            System.out.println(age);
            System.out.println(a);

            int age = 66;
            System.out.println(age);// 66
            System.out.println(this.age);// 88
            System.out.println(Outer.this.age);// 99
        }

        public String getName() {
            return name;
        }

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

    public void test2(){
        System.out.println(age);
        System.out.println(a);
    }
}

静态内部类

静态内部类,其实就是在成员内部类的前面加了一个static关键字。静态内部类属于外部类自己持有。

静态内部类访问成员的特点

可以直接访问外部类的静态成员(属于类),不可以直接访问外部类的实例成员(属于对象实例,必须创建对象实例才能访问)。

public class Outer {
    private int age = 99;
    public static String schoolName="黑马";

    // 静态内部类
    public static class Inner{
        //静态内部类访问外部类的静态变量,是可以的;
        //静态内部类访问外部类的实例变量,是不行的
        public void test(){
            System.out.println(schoolName); //99
            //System.out.println(age);   //报错
        }
    }
}

静态内部类创建对象时,需要使用外部类的类名调用。

//格式:外部类.内部类 变量名 = new 外部类.内部类();
Outer.Inner in = new Outer.Inner();
in.test();

局部内部类

image-20240412095357722

匿名内部类!!!

为什么要使用匿名内部类

在实际开发中,我们常常遇到这样的情况:一个接口/类的方法的某个实现方式在程序中只会执行一次,但为了使用它,我们需要创建它的实现类/子类去实现/重写。此时可以使用匿名内部类的方式,可以无需创建新的类,减少代码。

匿名内部类是一种特殊的局部内部类;所谓匿名,指的是程序员不需要为这个类声明名字。

下面就是匿名内部类的格式:

new 父类/接口(参数值){
@Override
重写父类/接口的方法;
}

匿名内部类本质上是一个没有名字的子类对象、或者接口的实现类对象。

比如,先定义一个Animal抽象类,里面定义一个cry()方法,表示所有的动物有叫的行为,但是因为动物还不具体,cry()这个行为并不能具体化,所以写成抽象方法。

public abstract class Animal{
    public abstract void cry();
}

接下来,我想要在不定义子类的情况下创建Animal的子类对象,就可以使用匿名内部类

public class Test{
    public static void main(String[] args){
        //这里后面new 的部分,其实就是一个Animal的子类对象
        //这里隐含的有多态的特性: Animal a = Animal子类对象;
        Animal a = new Animal(){
            @Override
            public void cry(){
                System.out.println("猫喵喵喵的叫~~~");
            }
        }
        a.cry(); //直线上面重写的cry()方法
    }
}

需要注意的是,匿名内部类在编写代码时没有名字,编译后系统会为自动为匿名内部类生产字节码,字节码的名称会以 外部类$1.class的方法命名

image-20240406195814828

匿名内部类的使用场景

在调用方法时,当方法的形参是一个接口或者抽象类,为了简化代码书写,而直接传递匿名内部类对象给方法。这样就可以少写一个类。

package com.itheima.d3_inner_class3;

public class Test2 {
    public static void main(String[] args) {
        // 目标:掌握匿名的常见使用场景。
//        Swimming s1 = new Swimming(){
//            @Override
//            public void swim() {
//                System.out.println("狗swim飞快~~~~");
//            }
//        };
//        go(s1);

        go(new Swimming(){
            @Override
            public void swim() {
                System.out.println("狗swim飞快~~~~");
            }
        });

    }

    // 设计一个方法,可以接收swimming接口的一切实现类对象进来参加游泳比赛。
    public static void go(Swimming s){
        System.out.println("开始-----------------------");
        s.swim();
    }
}



// 猫和狗都要参加游泳比赛
interface Swimming{
    void swim();
}

内部类的注意事项

局部内部类和匿名内部类访问局部变量时,为什么变量必须加上final?

为什么这里的局部变量不能修改

追究其根本原因就是作用域中变量的生命周期导致的;

首先需要知道的一点是:

内部类和外部类是处于同一个级别的(都是类),内部类不会因为定义在方法中,就会随着方法的执行完毕被销毁。

这样就会产生问题:

当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡)。

这样就会出现了一个矛盾:

内部类对象访问了一个不存在的变量。

为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量

这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期。

枚举类

什么是枚举类

image-20240412104845682

枚举类中的第一行,只能写一些合法的标识符(名称),多个名称用逗号隔开。

这些名称,本质是常量,每个常量都会记住枚举类的一个对象。

刚才说,枚举项实际上是枚举类的对象,这一点其实可以通过反编译的形式来验证(需要用到反编译的命令,这里不能直接将字节码拖进idea反编译)

image-20240406195824217

我们会看到,枚举类A是用class定义的,说明枚举确实是一个类,而且X,Y,Z都是A类的对象;而且每一个枚举项都是被public static final 修饰,所以被可以类名调用,而且不能更改。

枚举类的特点

  • 枚举类的第一行只能罗列一些名称,这些名称都是常量,并且每个常量记住的都是枚举类的一个对象。
  • 枚举类的构造器都是私有的(写不写都只能是私有的),因此,枚举类对外不能创建对象。
  • 枚举都是最终类,不可以被继承。
  • 枚举类中,从第二行开始,可以定义类的其他各种成员。
  • 编译器为枚举类新增了几个方法,并且枚举类都是继承:java.lang.Enum类的,从enum类也会继承到一些方法。
package com.itheima.d4_enum;
// 枚举类
public enum A {
    // 常量,每个常量都是记住枚举类的一个对象的。
    X, Y, Z;

    // 私有的private
    A(){
    }

    // private
    A(String name){
    }
    private String name;

    public String getName() {
        return name;
    }

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

枚举类有参构造:

public enum A{
    //定义枚举项
    X,Y,Z("张三"); //枚举项后面加括号,就是在执行枚举类的带参数构造方法。
    
    //定义空构造器
    A(){
        
    }
    
    //成员变量
    private String name;
    //定义带参数构造器
    A(String name){
        this.name=name;
    }
    
    //成员方法
    public String getName(){
        return name;
    }
    ...
}

枚举的使用场景

枚举的应用场景是这样的:枚举一般表示一组信息,然后作为参数进行传输。

image-20240406195842397

这里我们就可以先定义一个枚举类,用来表示男生、或者女生

public enum Constant{
    BOY,GRIL
}

再定义一个测试类,完成用户进入系统后的选择

public class Test{
    public static void main(String[] args){
        //调用方法,传递男生
        provideInfo(Constant.BOY);
    }
    
    public static void provideInfo(Constant c){
        switch(c){
            case BOY:
                System.out.println("展示一些信息给男生看");
                break;
            case GRIL:
                System.out.println("展示一些信息给女生看");
                break;
        }
    }
}

最终再总结一下枚举的应用场景:枚举一般表示几个固定的值,然后作为参数进行传输

泛型

泛型的作用、本质

在Java中,泛型有助于创建可与不同类型的对象(数据)一起使用的类,接口和方法。因此,允许我们重用我们的代码。

注意:泛型不适用于基本类型(int,float,char等)。必须使用包装类

  • 泛型的好处:在编译阶段约束数据类型,并自动检查,可以避免强制类型转换以其可能出现的异常。
  • 泛型的本质:把具体的数据类型传递给类型变量。

泛型类

image-20240406195848322

package com.itheima.d6_generics_class;	
// 写法一,简单泛型类
public class MyArrayList<E> {
    private Object[] arr = new Object[10];
    private int size; // 记录当前位置的

    public boolean add(E e){
        arr[size++] = e;
        return true;
    }

    public E get(int index){
        return (E) arr[index];
    }
}


// 写法二,多元泛型类
package com.itheima.d6_generics_class;

public class MyClass2<E, T> {
    public void put(E e, T t){

    }
}


// 写法三,泛型类继承
package com.itheima.d6_generics_class;

public class MyClass3<E extends Animal> {

}

泛型类示例

简单泛型类:
class Point<T>{         // 此处可以随便写标识符号,T是type的简称  
    private T var ;     // var的类型由T指定,即:由外部指定  
    public T getVar(){  // 返回值的类型由外部决定  
        return var ;  
    }  
    public void setVar(T var){  // 设置的类型也由外部决定  
        this.var = var ;  
    }  
}  
public class GenericsDemo06{  
    public static void main(String args[]){  
        Point<String> p = new Point<String>() ;     // 里面的var类型为String类型  
        p.setVar("it") ;                            // 设置字符串  
        System.out.println(p.getVar().length()) ;   // 取得字符串的长度  
    }  
}


多元泛型类:
class Notepad<K,V>{       // 此处指定了两个泛型类型  
    private K key ;     // 此变量的类型由外部决定  
    private V value ;   // 此变量的类型由外部决定  
    public K getKey(){  
        return this.key ;  
    }  
    public V getValue(){  
        return this.value ;  
    }  
    public void setKey(K key){  
        this.key = key ;  
    }  
    public void setValue(V value){  
        this.value = value ;  
    }  
} 
public class GenericsDemo09{  
    public static void main(String args[]){  
        Notepad<String,Integer> t = new Notepad<String,Integer>(); // 里面的key为String,value为Integer  
        t.setKey("汤姆") ;        // 设置第一个内容  
        t.setValue(20) ;            // 设置第二个内容  
        System.out.print("姓名;" + t.getKey()) ;      // 取得信息  
        System.out.print(",年龄;" + t.getValue()) ;       // 取得信息  
  
    }  
}

泛型接口

在上一节中,我们已经学习了自定义泛型类,接下来我们学习一下泛型接口。泛型接口其实指的是在接口中把不确定的数据类型用<类型变量>表示。定义格式如下:

//这里的类型变量,一般是一个字母,比如<E>
public interface 接口名<类型变量>{

}

比如,我们现在要做一个系统要处理学生和老师的数据,需要提供2个功能,保存对象数据、根据名称查询数据,要求:这两个功能处理的数据既能是老师对象,也能是学生对象。

首先我们得有一个学生类和老师类

public class Teacher{

}
public class Student{

}

我们定义一个Data泛型接口,T表示接口中要处理数据的类型。

public interface Data<T>{
    public void add(T t);

    public ArrayList<T> getByName(String name);
}

接下来,我们写一个处理Teacher对象的接口实现类

//此时确定Data<E>中的E为Teacher类型,
//接口中add和getByName方法上的T也都会变成Teacher类型
public class TeacherData implements Data<Teacher>{
    public void add(Teacher t){

    }

    public ArrayList<Teacher> getByName(String name){

    }
}

接下来,我们写一个处理Student对象的接口实现类

//此时确定Data<E>中的E为Student类型,
//接口中add和getByName方法上的T也都会变成Student类型
public class StudentData implements Data<Student>{
    public void add(Student t){

    }

    public ArrayList<Student> getByName(String name){

    }
}

再啰嗦几句,在实际工作中,一般也都是框架底层源代码把泛型接口写好,我们实现泛型接口就可以了。

泛型接口示例

interface Info<T>{        // 在接口上定义泛型  
    public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型  
}  
class InfoImpl<T> implements Info<T>{   // 定义泛型接口的子类  
    private T var ;             // 定义属性  
    public InfoImpl(T var){     // 通过构造方法设置属性内容  
        this.setVar(var) ;    
    }  
    public void setVar(T var){  
        this.var = var ;  
    }  
    public T getVar(){  
        return this.var ;  
    }  
} 
public class GenericsDemo24{  
    public static void main(String arsg[]){  
        Info<String> i = new InfoImpl<String>("汤姆") ;  // 通过子类实例化对象  多态写法
        System.out.println("内容:" + i.getVar()) ;  
    }  
}

泛型方法

泛型方法示例

package com.itheima.test;

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

        //使用Integer数据初始化类
        DemoClass demo = new DemoClass();
        demo.genericsMethod("Java Programming");
    }
}

class DemoClass {

    //泛型方法
    public <T> void genericsMethod(T data) {
        System.out.println("这是一个泛型方法。");
        System.out.println("传递给方法的数据是 " + data);
    }
}
package com.itheima.test;

import java.util.Arrays;
import java.util.Date;

public class Test {
    public static void main(String[] args) {
        System.out.println(test(1));
        System.out.println(test1("hello"));
        System.out.println(test1(true));
        test2(1);
        test2("hello");
        test2(true);
    }

    public static <T extends Number> T test(T t) {
        return t;
    }
    public static <T> T test1(T t) {
        return t;
    }

    public static <T> void test2(T t) {
        System.out.println(t);
    }


}

通配符(泛型限定)

? 可以在”使用泛型“的时候代表一切类型; E T K V 是在”定义泛型“的时候使用。

接着,我们来学习一个泛型的特殊用法,叫做泛型限定。泛型限定的意思是对泛型的数据类型进行范围的限制。有如下的三种格式

  • 表示任意类型
  • 表示指定类型或者指定类型的子类(上限)
  • 表示指定类型或者指定类型的父类(下限)
package com.itheima.d8_generics_method;

import java.util.ArrayList;

public class Test {
    public static void main(String[] args) {
        // 目标:掌握泛型方法的定义和使用。
        String rs = test("java");
        System.out.println(rs);

        Dog d = test(new Dog());
        System.out.println(d);

        // 需求:所有的汽车可以一起参加比赛。
        ArrayList<Car> cars = new ArrayList<>();
        cars.add(new BMW());
        cars.add(new BENZ());
        go(cars);

        ArrayList<BMW> bmws = new ArrayList<>();
        bmws.add(new BMW());
        bmws.add(new BMW());
        go(bmws);

        ArrayList<BENZ> benzs = new ArrayList<>();
        benzs.add(new BENZ());
        benzs.add(new BENZ());
        go(benzs);

//        ArrayList<Dog> dogs = new ArrayList<>();
//        dogs.add(new Dog());
//        dogs.add(new Dog());
//        go(dogs);
    }

    // ? 通配符,在使用泛型的时候可以代表一切类型   ? extends Car(上限)   ? super Car(下限)
    public static void go(ArrayList<? extends Car> cars){

    }

//    public static <T extends Car> void go(ArrayList<T> cars){
//
//    }

    // 泛型方法
    public static <T> T test(T t){
        return t;
    }
}

泛型擦除

也就是说泛型只能编译阶段有效,一旦编译成字节码,字节码中是不包含泛型的。而且泛型只支持引用数据类型,不支持基本数据类型。

image-20240406195857750

image-20240406195901516

package com.itheima.d9_generics_attention;

import java.math.BigDecimal;
import java.util.ArrayList;
public class Test {
    public static void main(String[] args) {
        // 目标:理解泛型的注意事项。
        // 1、泛型是工作在编译阶段的,一旦程序编译成class文件,class文件中就不存在泛型了,这就是泛型擦除。
        ArrayList<String> list = new ArrayList<>();
        list.add("java1");
        list.add("java2");
        list.add("java3");
        String rs = list.get(2);
        System.out.println(rs);

        // 2、泛型不支持基本数据类型,只能支持对象类型(引用数据类型)。
//        ArrayList<int> list1 = new ArrayList<>();
//        ArrayList<double> list2 = new ArrayList<>();
        ArrayList<Integer> list1 = new ArrayList<>();
        list1.add(12);

        ArrayList<Double> list2 = new ArrayList<>();
        list2.add(23.3);

        double a = 0.1;
        double b = 0.2;
        double c = a + b;
        System.out.println(a);
        BigDecimal a1 = new BigDecimal(0.1);
    }
}

为什么没有<T super XXX>

因为泛型在编译称class字节码文件时被擦除了,使用Object父类替代,Object是所有类的父类,所以使用super没有意义

泛型方法练习题

【多选题】对于泛型类class A { … },T在A类里可以用作不同的地方,在A类类体内,下面语句正确的有(ABDG)

A. T x;
B. T m1() {return null;}
C. static T y;
D. void m2(T i) {}
E. static T s1() {return null;}
F. static void s2(T i) {}
G. static <T1> void s3(T1 i, T1 j){} 
package com.itheima.practice.test;

public class Test2<T> {
    T x;

    T m1() {
        return null;
    }
//    static T y;

    void m2(T i) {
    }

    // 错误 静态的泛型方法必须前面加上<T>,因为静态方法不依赖于类对象,类上的泛型是在new对象时确定的
    // 静态方法不需要依靠对象调用,所以必须提前给出泛型声明
//    static T s1() {return null;}
//    static void s2(T i){}
    static <T> T s1() {
        return null;
    }

    static <T> void s2(T i) {
    }

    static <T1> void s3(T1 i, T1 j) {
    }
    

}


API

Object类

方法名 说明
public String toString() 返回对象的字符串表示形式。
public boolean equals(Object o) 判断两个对象是否相等。
protected Object clone() 对象克隆
public class Test{
	public static void main(String[] args){
        Student s1 = new Student("赵薇",23);
        Student s2 = new Student("赵薇",23);
        
        //equals本身也是比较对象的地址,和"=="没有区别
        System.out.println(s1.equals(s2)); //false
         //"=="比较对象的地址
        System.out.println(s1==s2); //false
    }
}

toString()和equals()

image-20240406195908153

package com.itheima.d10_api_object;

import java.util.Objects;

public class Student { // extends Object{
    private String name;
    private int age;

    public Student() {
    }

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

    // 重写equals方法,比较两个对象的内容一样就返回true.
    // 比较者:s2 == this
    // 被比较者:s1 == o
    @Override
    public boolean equals(Object o) {
        // 1、判断两个对象是否地址一样,一样直接返回true.
        if (this == o) return true;
        // 2、判断o是null直接返回false, 或者比较者的类型与被比较者的类型不一样,返回false
        //               Student.class !=  Pig.class
        if (o == null || this.getClass() != o.getClass()) return false;
        // 3、o不是null,且o一定是学生类型的对象。开始比较内容了!
        Student student = (Student) o;
        return Objects.equals(this.age, student.age) && Objects.equals(this.name, student.name);
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", 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;
    }
}

clone()

浅克隆

image-20240406195913502

@Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

深克隆

image-20240406195917526

    private double[] scores; // 分数

    @Override
    protected Object clone() throws CloneNotSupportedException {
        // super去调用父类Object中的clone方法。
        User clone = (User) super.clone();
        clone.scores = this.scores.clone();
        return clone;
    }

想要调用clone()方法,必须让被克隆的类实现Cloneable接口。

public class User implements Cloneable{
    
}

两者区别就在于,浅拷贝只是简单的复制,对对象里面的对象属性和数组属性只是复制了地址,并没有创建新的相同对象或者数组。而深拷贝是完完全全的复制一份,空间大小占用一样但是位置不同!浅拷贝是拷贝了对象的引用,当原对象发生变化的时候,拷贝对象也跟着变化;深拷贝是另外申请了一块内存,内容和原对象一样,更改原对象,拷贝对象不会发生变化。

浅拷贝和深拷贝的实例

由于字符串是不可变的(immutable),即使进行浅拷贝,也不会导致两个引用指向同一个可变对象的问题。每次对字符串的修改都会产生一个新的字符串对象。

对于对象和数组引用类型的属性,浅拷贝只是赋值了地址,所以当拷贝对象或者原对象的对应属性发生改变时,另外一个也会改变

浅拷贝

package com.itheima.myapi;

public class Origin implements  Cloneable{
    public int[] arr;

    public Origin(int[] arr) {
        this.arr = arr;
    }

    public Origin() {
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 浅拷贝
        return super.clone();
    }
}


package com.itheima.myapi;

import java.util.Arrays;

public class Test {
    public static void main(String[] args) throws Exception {
        Origin origin = new Origin();
        origin.arr = new int[]{1, 2, 3};
        System.out.println(Arrays.toString(origin.arr)); // [1,2,3]


        Origin clone = (Origin) origin.clone();
        clone.arr[1] = 3;
        System.out.println(Arrays.toString(origin.arr)); // 输出[1,3,3]
        System.out.println(origin.arr == clone.arr); // true


        origin.arr[1] = 4;
        System.out.println(Arrays.toString(clone.arr));

    }
}


浅拷贝中关于字符串的处理

package com.itheima.myapi;

public class Origin implements Cloneable {
    public String str;
    public int[] arr;

    public Origin(int[] arr) {
        this.arr = arr;
    }

    public Origin() {
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}


package com.itheima.myapi;

import java.util.Arrays;

public class Test {
    public static void main(String[] args) throws Exception {

        Origin origin = new Origin();
        origin.str = "123";
        origin.arr = new int[]{1, 2, 3};
        Origin clone = (Origin) origin.clone();
		// 指向一样的地址
        System.out.println(origin.str.hashCode()); // 48690
        System.out.println(clone.str.hashCode()); // 48690


        // 修改字符串会创建新的字符串对象,字符串不可变
        clone.str = "345";
        System.out.println(origin.str.hashCode()); // 48690
        System.out.println(clone.str.hashCode());  // 50676

    }
}

深拷贝

深拷贝关于字符串拷贝的仍是地址值,但是字符串不可变,所以修改原始或拷贝对象的字符串属性会创建新的字符串对象

package com.itheima.myapi;

public class Origin implements Cloneable {
    public int[] arr;

    public Origin(int[] arr) {
        this.arr = arr;
    }

    public Origin() {
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 浅拷贝基础信息
        Origin clone = (Origin) super.clone();
        // 深拷贝地址信息
        clone.arr = this.arr.clone();
        return clone;
    }
}



package com.itheima.myapi;

import java.util.Arrays;

public class Test {
    public static void main(String[] args) throws Exception {
        Origin origin = new Origin();
        origin.arr = new int[]{1, 2, 3};
        System.out.println(Arrays.toString(origin.arr)); // [1,2,3]


        Origin clone = (Origin) origin.clone();
        clone.arr[1] = 3;
        System.out.println(Arrays.toString(origin.arr)); // 输出[1,2,3]
        System.out.println(origin.arr == clone.arr); // false


        origin.arr[1] = 4;
        System.out.println(Arrays.toString(clone.arr)); // [1,3,3]
        System.out.println(Arrays.toString(origin.arr)); // [1,4,3]

    }
}

阿里面试题(!!!)

一个实现Cloneable的类中,clone()后的String类型数据,是深拷贝还是浅拷贝?

String类型有点特殊,它本身没有实现Cloneable接口,故根本无法克隆,只能传递引用(注意:Java只有值传递,只是这里传递是原来引用地址值)。在clone()后,克隆后的对象开始也是指向的原来引用地址值,但是一旦String的值发生改变(String作为不可更改的类immutable class,在重新赋值的时候,会创建一个新的对象)就改变了克隆后对象指向的地址,让它指向了一个新的String地址,,不会影响原对象的指向和值,原来的String对象还是指向的它自己的的地址。这样String在拷贝的时候就表现出了深拷贝的特点;

Objects类

image-20240406195921973

Objects类的equal方法,回判断是否为null,不会报空指针异常

public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }
public class Test{
    public static void main(String[] args){
        String s1 = null;
        String s2 = "itheima";
        
        //这里会出现NullPointerException异常,调用者不能为null
        // System.out.println(s1.equals(s2));
        //此时不会有NullPointerException异常,底层会自动先判断空
        System.out.println(Objects.equals(s1,s2));
        
        //判断对象是否为null,等价于==
        System.out.println(Objects.isNull(s1)); //true
        System.out.println(s1==null); //true
        
        //判断对象是否不为null,等价于!=
        System.out.println(Objects.nonNull(s2)); //true
        System.out.println(s2!=null); //true
    }
}

包装类

什么是包装类

image-20240406195926240

装箱和拆箱

自动装箱:可以自动把基本类型的数据转换成对象。

自动拆箱:可以自动把包装类型的对象转换成对应的基本数据类型。

        Integer a2 = Integer.valueOf(12);
        System.out.println(a2);

        // 自动装箱:可以自动把基本类型的数据转换成对象。
        Integer a3 = 12;

        // 自动拆箱:可以自动把包装类型的对象转换成对应的基本数据类型。
        int a4 = a3;

        // 泛型和集合不支持基本数据类型,只能支持引用数据类型。
        // ArrayList<int> list = new ArrayList<>();
        ArrayList<Integer> list = new ArrayList<>();
        list.add(12); // 自动装箱
        list.add(13); // 自动装箱

        int rs = list.get(1); // 自动拆箱

包装类型的缓冲池

new Integer(123) 与 Integer.valueOf(123) 的区别在于:

  • new Integer(123) 每次都会新建一个对象
  • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y);    // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k);   // true

在 Java 8 中,Integer 缓存池的大小默认为 -128~127。

编译器会在缓冲池范围内的基本类型自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。

Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true

基本类型对应的缓冲池如下:

  • Byte:-128~127,也就是所有的 byte 值
  • Short:-128~127
  • Long:-128~127
  • Character:\u0000 - \u007F
  • Boolean:true 和 false

在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。

如果在缓冲池之外:

Integer m = 323;
Integer n = 323;
System.out.println(m == n); // false

image-20240406195944549

// 1、把基本类型的数据转换成字符串
        Integer a = 23;
        String rs1 = Integer.toString(a); // "23"
        System.out.println(rs1 + 1); // 231

        String rs2 =  a.toString(); // "23"
        System.out.println(rs2 + 1);

        String rs3 = a + "";
        System.out.println(rs3 + 1);

        // 2、把字符串类型的数值转换成对应的基本类型。
        String ageStr = "29";
        int ageI = Integer.parseInt(ageStr); // 29

        // int ageI = Integer.valueOf(ageStr); // 29
        System.out.println(ageI + 1); // 30

        String scoreStr = "99.5";
        double score = Double.parseDouble(scoreStr); // 99.5

		// 多了拆箱的操作
        // double score = Double.valueOf(scoreStr); // 99.5
        System.out.println(score + 0.5); // 100.0

字符串

StringBuilder类

image-20240406195949721

package com.itheima.d13_stringBuilder;

public class Test1 {
    public static void main(String[] args) {
        // 目标:搞清楚StringBuilder的用法和作用。
        // StringBuilder s = new StringBuilder(); // s ""
        StringBuilder s = new StringBuilder("itheima"); // s "itheima"

        // 1、拼接内容
        s.append(12);
        s.append("黑马");
        s.append(true);

        // 支持链式编程
        s.append(666).append("黑马2").append(666);
        System.out.println(s);

        // 2、反转操作
        s.reverse();
        System.out.println(s);

        // 3、返回字符串的长度
        System.out.println(s.length());

        // 4、把StringBuilder对象又转换成String类型。
        String rs = s.toString();
        System.out.println(rs);
    }
}

StringBuilder的好处

package com.itheima.d13_stringBuilder;

public class Test2 {
    public static void main(String[] args) {
        // 目标:掌握StringBuilder的好处。
        // 需求:要拼接100万次abc
        // 先用String测试看看性能
        // 效率很低,执行超慢
//        String rs = "";
//        for (int i = 1; i <= 1000000 ; i++) {
//            rs = rs + "abc";
//        }
//        System.out.println(rs);

        // 使用StringBuilder演示
        StringBuilder sb = new StringBuilder();
        for (int i = 1; i <= 1000000; i++) {
            sb.append("abc");
        }
        System.out.println(sb);
    }
}

img

StringBuffer类

image-20240406200000620

String、StringBuffer和StringBuilder区别

1. 可变性

  • String 不可变
  • StringBuffer 和 StringBuilder 可变

2. 线程安全

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

StringJoiner类

img

package com.itheima.d14_stringjoiner;

import java.util.StringJoiner;

public class Test {
    public static void main(String[] args) {
        // 目标:掌握StringJoiner的使用。
        // StringJoiner s = new StringJoiner(", "); // 间隔符!
        StringJoiner s = new StringJoiner(", ", "[", "]"); // 间隔符!
        s.add("java1");
        s.add("java2");
        s.add("java3");
        System.out.println(s); // [java1, java2, java3]

        System.out.println(getArrayData(new int[]{11, 22, 33}));
    }

    public static String getArrayData(int[] arr){
        // 1、判断arr是否为null
        if(arr == null){
            return null;
        }
        // 2、arr数组对象存在。 arr = [11, 22, 33]
        StringJoiner s = new StringJoiner(", ", "[", "]");
        for (int i = 0; i < arr.length; i++) {
            s.add(arr[i] + "");
        }
        return s.toString();
    }
}

image-20240406200013609

Math类

image-20240406200019632

package com.itheima.d1_math;

public class MathTest {
    publiac static void main(String[] args) {
        // 目标:了解下Math类提供的常见方法。
        // 1、public static int abs(int a):取绝对值(拿到的结果一定是正数)
        //    public static double abs(double a)
        System.out.println(Math.abs(-12)); // 12
        System.out.println(Math.abs(123)); // 123
        System.out.println(Math.abs(-3.14)); // 3.14

        // 2、public static double ceil(double a): 向上取整
        System.out.println(Math.ceil(4.0000001)); // 5.0
        System.out.println(Math.ceil(4.0)); // 4.0

        // 3、public static double floor(double a): 向下取整
        System.out.println(Math.floor(4.999999)); // 4.0
        System.out.println(Math.floor(4.0)); // 4.0

        // 4、public static long round(double a):四舍五入
        System.out.println(Math.round(3.4999)); // 3
        System.out.println(Math.round(3.50001)); // 4

        // 5、public static int max(int a, int b):取较大值
        //   public static int min(int a, int b):取较小值
        System.out.println(Math.max(10, 20)); // 20
        System.out.println(Math.min(10, 20)); // 10

        // 6、 public static double pow(double a, double b):取次方
        System.out.println(Math.pow(2, 3)); // 2的3次方   8.0
        System.out.println(Math.pow(3, 2)); // 3的2次方   9.0

        // 7、public static double random(): 取随机数 [0.0 , 1.0) (包前不包后)
        System.out.println(Math.random());
    }
}

System类

image-20240406200023717

package com.itheima.d1_math;

/**
 * 目标:了解下System类的常见方法。
 */
public class SystemTest {
    public static void main(String[] args) {

        // 1、public static void exit(int status):
        //   终止当前运行的Java虚拟机。
        //   该参数用作状态代码; 按照惯例,非零状态代码表示异常终止。
         System.exit(0); // 人为的终止虚拟机。(不要使用)

        // 2、public static long currentTimeMillis():
        //    获取当前系统的时间
        //    返回的是long类型的时间毫秒值:指的是从1970-1-1 0:0:0开始走到此刻的总的毫秒值,1s = 1000ms
        long time = System.currentTimeMillis();
        System.out.println(time);

        for (int i = 0; i < 1000000; i++) {
            System.out.println("输出了:" + i);
        }

        long time2 = System.currentTimeMillis();
        System.out.println((time2 - time) / 1000.0 + "s");
    }
}

Runtime类

代表程序所在的运行环境。

Runtime是一个单例类。

image-20240413110912149

package com.itheima.d1_math;

import java.io.IOException;

/**
 *  目标:了解下Runtime的几个常见方法。
 */
public class RuntimeTest {
    public static void main(String[] args) throws IOException, InterruptedException{

        // 1、public static Runtime getRuntime() 返回与当前Java应用程序关联的运行时对象。
        Runtime r = Runtime.getRuntime();

        // 2、public void exit(int status) 终止当前运行的虚拟机,该参数用作状态代码; 按照惯例,非零状态代码表示异常终止。
        // r.exit(0);

        // 3、public int availableProcessors(): 获取虚拟机能够使用的处理器数。
        System.out.println(r.availableProcessors());

        // 4、public long totalMemory() 返回Java虚拟机中的内存总量。
        System.out.println(r.totalMemory()/1024.0/1024.0 + "MB"); // 1024 = 1K     1024 * 1024 = 1M

        // 5、public long freeMemory() 返回Java虚拟机中的可用内存量
        System.out.println(r.freeMemory()/1024.0/1024.0 + "MB");

        // 6、public Process exec(String command) 启动某个程序,并返回代表该程序的对象。
        // r.exec("D:\\soft\\XMind\\XMind.exe");
        // Process p = r.exec("QQ");
        Process p = r.exec("D:/xitong/XMind2021 (64bit)/XMind 2021/XMind.exe");
        Thread.sleep(5000); // 让程序在这里暂停5s后继续往下走!!
        p.destroy(); // 销毁!关闭程序!
    }
}

BigDecimal类

image-20240406200031374

package com.itheima.d2_bigdecimal;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Test2 {
    public static void main(String[] args) {
        // 目标:掌握BigDecimal进行精确运算的方案。
        double a = 0.1;
        double b = 0.2;

        // 1、把浮点型数据封装成BigDecimal对象,再来参与运算。
        // a、public BigDecimal(double val) 得到的BigDecimal对象是无法精确计算浮点型数据的。 注意:不推荐使用这个,
        // b、public BigDecimal(String val)  得到的BigDecimal对象是可以精确计算浮点型数据的。 可以使用。
        // c、public static BigDecimal valueOf(double val): 通过这个静态方法得到的BigDecimal对象是可以精确运算的。是最好的方案。
        BigDecimal a1 = BigDecimal.valueOf(a);
        BigDecimal b1 = BigDecimal.valueOf(b);

        // 2、public BigDecimal add(BigDecimal b): 加法
        BigDecimal c1 = a1.add(b1);
        System.out.println("0.1+0.2=" + c1);

        // 3、public BigDecimal subtract(BigDecimal b): 减法
        BigDecimal c2 = a1.subtract(b1);
        System.out.println("0.1-0.2=" + c2);

        // 4、public BigDecimal multiply(BigDecimal b): 乘法
        BigDecimal c3 = a1.multiply(b1);
        System.out.println("0.1*0.2=" + c3);

        // 5、public BigDecimal divide(BigDecimal b): 除法
        BigDecimal c4 = a1.divide(b1,RoundingMode.HALF_UP);
        System.out.println("0.1/0.2=" + c4);

//        BigDecimal d1 = BigDecimal.valueOf(0.1);
//        BigDecimal d2 = BigDecimal.valueOf(0.3);
//        BigDecimal d3 = d1.divide(d2);
//        System.out.println(d3);

        // 6、public BigDecimal divide(另一个BigDecimal对象,精确几位,舍入模式) : 除法,可以设置精确几位。
        BigDecimal d1 = BigDecimal.valueOf(0.1);
        BigDecimal d2 = BigDecimal.valueOf(0.3);
        BigDecimal d3 = d1.divide(d2, 2, RoundingMode.HALF_UP); // 0.33
        System.out.println("0.1/0.3=" + d3);

        // 7、public double doubleValue() : 把BigDecimal对象又转换成double类型的数据。
        //print(d3);
        //print(c1);
        double db1 = d3.doubleValue();
        double db2 = c1.doubleValue();
        print(db1);
        print(db2);
    }


    public static void print(double a) {
        System.out.println(a);
    }
}

jdk8之前的时间(不推荐)

Date类

image-20240406200035361

package com.itheima.d3_time;

import java.util.Date;

public class Test1Date {
    public static void main(String[] args) {
        // 目标:掌握Date日期类的使用。
        // 1、创建一个Date的对象:代表系统当前时间信息的。
        Date d = new Date();
        System.out.println(d);

        // 2、拿到时间毫秒值。
        long time = d.getTime();
        System.out.println(time);

        // 3、把时间毫秒值转换成日期对象: 2s之后的时间是多少。
        time += 2 * 1000;
        Date d2 = new Date(time);
        System.out.println(d2);

        // 4、直接把日期对象的时间通过setTime方法进行修改
        Date d3 = new Date();
        d3.setTime(time);
        System.out.println(d3);
    }
}

SimpleDateFormat

image-20240406200039495

image-20240413145731538

image-20240406200044447

package com.itheima.d3_time;

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

public class Test2SimpleDateFormat {
    public static void main(String[] args) throws ParseException {
        // 目标:掌握SimpleDateFormat的使用。
        // 1、准备一些时间
        Date d = new Date();
        System.out.println(d);

        long time = d.getTime();
        System.out.println(time);

        // 2、格式化日期对象,和时间 毫秒值。大写的HH是二十四小时制
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EEE a");
        // SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss EEE a");

        String rs = sdf.format(d);
        String rs2 = sdf.format(time);
        System.out.println(rs);
        System.out.println(rs2);
        System.out.println("-------------------------------------------------------------------");

        // 目标:掌握SimpleDateFormat解析字符串时间 成为日期对象。
        String dateStr = "2022-12-12 12:12:11";
        // 1、创建简单日期格式化对象 , 指定的时间格式必须与被解析的时间格式一模一样,否则程序会出bug.
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date d2 = sdf2.parse(dateStr);
        System.out.println(d2);
    }
}

Calendar类

image-20240406200048442

java.util.Calendar 是日历类,在Date后出现,替换掉了许多Date的方法。该类将所有可能用到的时间信息封装为静态成员变量,方便获取。日历类就是方便获取各个时间属性的。Calendar类(抽象类)无法直接创建对象使用,里边有一个静态方法getInstance(),该方法返回了Calendar类的子类对象。Calendar类中提供很多成员常量,代表给定的日历字段

package com.itheima.d3_time;

import java.util.Calendar;
import java.util.Date;

public class Test4Calendar {
    public static void main(String[] args) {
        // 目标:掌握Calendar的使用和特点。
        // 1、得到系统此刻时间对应的日历对象。
        Calendar now = Calendar.getInstance();
        System.out.println(now);

        // 2、获取日历中的某个信息
        int year = now.get(Calendar.YEAR);
        System.out.println(year);

        int days = now.get(Calendar.DAY_OF_YEAR);
        System.out.println(days);

        // 3、拿到日历中记录的日期对象。
        Date d = now.getTime();
        System.out.println(d);

        // 4、拿到时间毫秒值
        long time = now.getTimeInMillis();
        System.out.println(time);

        // 5、修改日历中的某个信息
        now.set(Calendar.MONTH, 9); // 修改月份成为10月份。
//        now.set(Calendar.DAY_OF_YEAR, 125); // 修改成一年中的第125天。
        System.out.println(now.getTime());

        // 6、为某个信息增加或者减少多少
        now.add(Calendar.DAY_OF_YEAR, 100);
        now.add(Calendar.DAY_OF_YEAR, -10);
        now.add(Calendar.DAY_OF_MONTH, 6);
        now.add(Calendar.HOUR, 12);
        now.set(2026, Calendar.DECEMBER, 22);
        System.out.println(now.getTime());
    }
}

秒杀案例

image-20240413164934696

package com.itheima.d3_time;

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

public class Test3 {
    public static void main(String[] args) throws ParseException {
        // 目标:完成秒杀案例。
        // 1、把开始时间、结束时间、小贾下单时间、小皮下单时间拿到程序中来。
        String start = "2023年11月11日 0:0:0";
        String end = "2023年11月11日 0:10:0";
        String xj = "2023年11月11日 0:01:18";
        String xp = "2023年11月11日 0:10:57";

        // 2、把字符串的时间解析成日期对象。
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
        Date startDt = sdf.parse(start);
        Date endDt = sdf.parse(end);
        Date xjDt = sdf.parse(xj);
        Date xpDt = sdf.parse(xp);

        // 3、开始判断小皮和小贾是否秒杀成功了。
        // 把日期对象转换成时间毫秒值来判断
        long startTime = startDt.getTime();
        long endTime = endDt.getTime();
        long xjTime = xjDt.getTime();
        long xpTime = xpDt.getTime();

        if(xjTime >= startTime && xjTime <= endTime){
            System.out.println("小贾您秒杀成功了~~");
        }else {
            System.out.println("小贾您秒杀失败了~~");
        }

        if(xpTime >= startTime && xpTime <= endTime){
            System.out.println("小皮您秒杀成功了~~");
        }else {
            System.out.println("小皮您秒杀失败了~~");
        }
    }
}

jdk8之后的时间(推荐)

为什么使用jdk8之后的时间

image-20240406200057153

package com.itheima.d4_jdk8_time;

import java.util.Calendar;
import java.util.Date;

/**
 *  目标:搞清楚为什么要用JDK 8开始新增的时间类。
 */
public class Test {
    public static void main(String[] args) {
        // 传统的时间类(Date、SimpleDateFormat、Calendar)存在如下问题:
        // 1、设计不合理,使用不方便,很多都被淘汰了。
        Date d = new Date();
        // 必须自己手动加上
        // System.out.println(d.getYear() + 1900);

        // 要记住字段,最好有个getYear的方法
        Calendar c = Calendar.getInstance();
        int year = c.get(Calendar.YEAR);
        System.out.println(year);

        // 2、都是可变对象,修改后会丢失最开始的时间信息。

        // 3、线程不安全。

        // 4、不能精确到纳秒,只能精确到毫秒。
        // 1秒 = 1000毫秒
        // 1毫秒 = 1000微妙
        // 1微妙 = 1000纳秒
    }
}

image-20240406200101571

LocalDate类

image-20240406200105424

package com.itheima.d4_jdk8_time;
import java.time.LocalDate;
import java.util.Calendar;

public class Test1_LocalDate {
    public static void main(String[] args) {
        // 0、获取本地日期对象(不可变对象)
        LocalDate ld = LocalDate.now(); // 年 月 日
        System.out.println(ld);

        // 1、获取日期对象中的信息
        int year = ld.getYear(); // 年
        int month = ld.getMonthValue(); // 月(1-12)
        int day = ld.getDayOfMonth(); // 日
        int dayOfYear = ld.getDayOfYear();  // 一年中的第几天
        int dayOfWeek = ld.getDayOfWeek().getValue(); // 星期几
        System.out.println(year);
        System.out.println(day);
        System.out.println(dayOfWeek);

        // 2、直接修改某个信息: withYear、withMonth、withDayOfMonth、withDayOfYear
        LocalDate ld2 = ld.withYear(2099);
        LocalDate ld3 = ld.withMonth(12);
        System.out.println(ld2);
        System.out.println(ld3);
        System.out.println(ld);

        // 3、把某个信息加多少: plusYears、plusMonths、plusDays、plusWeeks
        LocalDate ld4 = ld.plusYears(2);
        LocalDate ld5 = ld.plusMonths(2);

        // 4、把某个信息减多少:minusYears、minusMonths、minusDays、minusWeeks
        LocalDate ld6 = ld.minusYears(2);
        LocalDate ld7 = ld.minusMonths(2);

        // 5、获取指定日期的LocalDate对象: public static LocalDate of(int year, int month, int dayOfMonth)
        LocalDate ld8 = LocalDate.of(2099, 12, 12);
        LocalDate ld9 = LocalDate.of(2099, 12, 12);

        // 6、判断2个日期对象,是否相等,在前还是在后: equals isBefore isAfter
        System.out.println(ld8.equals(ld9));// true
        System.out.println(ld8.isAfter(ld)); // true
        System.out.println(ld8.isBefore(ld)); // false
    }
}

LocalTime类

image-20240406200110938

package com.itheima.d4_jdk8_time;
import java.time.LocalTime;

public class Test2_LocalTime {
    public static void main(String[] args) {
        // 0、获取本地时间对象
        LocalTime lt = LocalTime.now(); // 时 分 秒 纳秒 不可变的
        System.out.println(lt);

        // 1、获取时间中的信息
        int hour = lt.getHour(); //时
        int minute = lt.getMinute(); //分
        int second = lt.getSecond(); //秒
        int nano = lt.getNano(); //纳秒

        // 2、修改时间:withHour、withMinute、withSecond、withNano
        LocalTime lt3 = lt.withHour(10);
        LocalTime lt4 = lt.withMinute(10);
        LocalTime lt5 = lt.withSecond(10);
        LocalTime lt6 = lt.withNano(10);

        // 3、加多少:plusHours、plusMinutes、plusSeconds、plusNanos
        LocalTime lt7 = lt.plusHours(10);
        LocalTime lt8 = lt.plusMinutes(10);
        LocalTime lt9 = lt.plusSeconds(10);
        LocalTime lt10 = lt.plusNanos(10);

        // 4、减多少:minusHours、minusMinutes、minusSeconds、minusNanos
        LocalTime lt11 = lt.minusHours(10);
        LocalTime lt12 = lt.minusMinutes(10);
        LocalTime lt13 = lt.minusSeconds(10);
        LocalTime lt14 = lt.minusNanos(10);

        // 5、获取指定时间的LocalTime对象:
        // public static LocalTime of(int hour, int minute, int second)
        LocalTime lt15 = LocalTime.of(12, 12, 12);
        LocalTime lt16 = LocalTime.of(12, 12, 12);

        // 6、判断2个时间对象,是否相等,在前还是在后: equals isBefore isAfter
        System.out.println(lt15.equals(lt16)); // true
        System.out.println(lt15.isAfter(lt)); // false
        System.out.println(lt15.isBefore(lt)); // true

    }
}

LocalDateTime类

image-20240406200116014

image-20240406200120236

package com.itheima.d4_jdk8_time;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

public class Test3_LocalDateTime {
    public static void main(String[] args) {
        // 0、获取本地日期和时间对象。
        LocalDateTime ldt = LocalDateTime.now(); // 年 月 日 时 分 秒 纳秒
        System.out.println(ldt);

        // 1、可以获取日期和时间的全部信息
        int year = ldt.getYear(); // 年
        int month = ldt.getMonthValue(); // 月
        int day = ldt.getDayOfMonth(); // 日
        int dayOfYear = ldt.getDayOfYear();  // 一年中的第几天
        int dayOfWeek = ldt.getDayOfWeek().getValue();  // 获取是周几
        int hour = ldt.getHour(); //时
        int minute = ldt.getMinute(); //分
        int second = ldt.getSecond(); //秒
        int nano = ldt.getNano(); //纳秒

        // 2、修改时间信息:
        // withYear withMonth withDayOfMonth withDayOfYear withHour
        // withMinute withSecond withNano
        LocalDateTime ldt2 = ldt.withYear(2029);
        LocalDateTime ldt3 = ldt.withMinute(59);

        // 3、加多少:
        // plusYears  plusMonths plusDays plusWeeks plusHours plusMinutes plusSeconds plusNanos
        LocalDateTime ldt4 = ldt.plusYears(2);
        LocalDateTime ldt5 = ldt.plusMinutes(3);

        // 4、减多少:
        // minusDays minusYears minusMonths minusWeeks minusHours minusMinutes minusSeconds minusNanos
        LocalDateTime ldt6 = ldt.minusYears(2);
        LocalDateTime ldt7 = ldt.minusMinutes(3);


        // 5、获取指定日期和时间的LocalDateTime对象:
        // public static LocalDateTime of(int year, Month month, int dayOfMonth, int hour,
        //                                  int minute, int second, int nanoOfSecond)
        LocalDateTime ldt8 = LocalDateTime.of(2029, 12, 12, 12, 12, 12, 1222);
        LocalDateTime ldt9 = LocalDateTime.of(2029, 12, 12, 12, 12, 12, 1222);

        // 6、 判断2个日期、时间对象,是否相等,在前还是在后: equals、isBefore、isAfter
        System.out.println(ldt9.equals(ldt8));
        System.out.println(ldt9.isAfter(ldt));
        System.out.println(ldt9.isBefore(ldt));

        // 7、可以把LocalDateTime转换成LocalDate和LocalTime
        // public LocalDate toLocalDate()
        // public LocalTime toLocalTime()
        // public static LocalDateTime of(LocalDate date, LocalTime time)
        LocalDate ld = ldt.toLocalDate();
        LocalTime lt = ldt.toLocalTime();
        LocalDateTime ldt10 = LocalDateTime.of(ld, lt);

    }
}

时区类

什么是时区

image-20240406200145177

ZoneId类和ZonedDateTime类

image-20240406200224417

package com.itheima.d4_jdk8_time;

import java.time.Clock;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.TimeZone;

public class Test4_ZoneId_ZonedDateTime {
    public static void main(String[] args) {
        // 目标:了解时区和带时区的时间。
        // 1、ZoneId的常见方法:
        // public static ZoneId systemDefault(): 获取系统默认的时区
        ZoneId zoneId = ZoneId.systemDefault();
        System.out.println(zoneId.getId());
        System.out.println(zoneId);

        // public static Set<String> getAvailableZoneIds(): 获取Java支持的全部时区Id
        System.out.println(ZoneId.getAvailableZoneIds());

        // public static ZoneId of(String zoneId) : 把某个时区id封装成ZoneId对象。
        ZoneId zoneId1 = ZoneId.of("America/New_York");

        // 2、ZonedDateTime:带时区的时间。
        // public static ZonedDateTime now(ZoneId zone): 获取某个时区的ZonedDateTime对象。
        ZonedDateTime now = ZonedDateTime.now(zoneId1);
        System.out.println(now);

        // 世界标准时间了
        ZonedDateTime now1 = ZonedDateTime.now(Clock.systemUTC());
        System.out.println(now1);

        // public static ZonedDateTime now():获取系统默认时区的ZonedDateTime对象
        ZonedDateTime now2 = ZonedDateTime.now();
        System.out.println(now2);

        // Calendar instance = Calendar.getInstance(TimeZone.getTimeZone(zoneId1));
    }
}

Instant类

image-20240406200242309

package com.itheima.d4_jdk8_time;

import java.time.Instant;
import java.time.LocalDateTime;

/**
 * 目标:掌握Instant的使用。
 */
public class Test5_Instant {
    public static void main(String[] args) {
        // 1、创建Instant的对象,获取此刻时间信息
        Instant now = Instant.now(); // 不可变对象

        // 2、获取总秒数
        long second = now.getEpochSecond();
        System.out.println(second);

        // 3、不够1秒的纳秒数
        int nano = now.getNano();
        System.out.println(nano);

        System.out.println(now);

        Instant instant = now.plusNanos(111);

        // Instant对象的作用:做代码的性能分析,或者记录用户的操作时间点
        Instant now1 = Instant.now();
        // 代码执行。。。。
        Instant now2 = Instant.now();

        LocalDateTime l = LocalDateTime.now();
    }
}

DateTimeFormatter

image-20240406200251793

package com.itheima.d4_jdk8_time;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

/**
 *  目标:掌握JDK 8新增的DateTimeFormatter格式化器的用法。
 */
public class Test6_DateTimeFormatter {
    public static void main(String[] args) {
        // 1、创建一个日期时间格式化器对象出来。
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");

        // 2、对时间进行格式化
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now);

        String rs = formatter.format(now); // 正向格式化
        System.out.println(rs);

        // 3、格式化时间,其实还有一种方案。
        String rs2 = now.format(formatter); // 反向格式化
        System.out.println(rs2);

        // 4、解析时间:解析时间一般使用LocalDateTime提供的解析方法来解析。
        String dateStr = "2029年12月12日 12:12:11";
        LocalDateTime ldt = LocalDateTime.parse(dateStr, formatter);
        System.out.println(ldt);
    }
}

时间间隔类

image-20240406200255957

image-20240406200259785

package com.itheima.d4_jdk8_time;

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

/**
 * 目标:掌握Period的作用:计算机两个日期相差的年数,月数、天数。
 */
public class Test7_Period {
    public static void main(String[] args) {
        LocalDate start = LocalDate.of(2029, 8, 10);
        LocalDate end = LocalDate.of(2029, 12, 15);

        // 1、创建Period对象,封装两个日期对象。
        Period period = Period.between(start, end);

        // 2、通过period对象获取两个日期对象相差的信息。
        System.out.println(period.getYears());
        System.out.println(period.getMonths());
        System.out.println(period.getDays());
    }
}

image-20240413181119801

package com.itheima.d4_jdk8_time;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;

public class Test8_Duration {
    public static void main(String[] args) {
        LocalDateTime start = LocalDateTime.of(2025, 11, 11, 11, 10, 10);
        LocalDateTime end = LocalDateTime.of(2025, 11, 11, 11, 11, 11);
        // 1、得到Duration对象
        Duration duration = Duration.between(start, end);

        // 2、获取两个时间对象间隔的信息
        System.out.println(duration.toDays());// 间隔多少天
        System.out.println(duration.toHours());// 间隔多少小时
        System.out.println(duration.toMinutes());// 间隔多少分
        System.out.println(duration.toSeconds());// 间隔多少秒
        System.out.println(duration.toMillis());// 间隔多少毫秒
        System.out.println(duration.toNanos());// 间隔多少纳秒

    }
}

1.8之前和1.8之后的时间日期对比

使用方式:

  • 1.0:new 对象
  • 1.8:类名.方法

使用用途:

  • 获取当前时间

    • 1.0:new Date() -> getTime()
    • 1.8:类名.now()
  • 时间日期格式化

    • 把时间转成字符串(format)
      • 1.0:new SDF("pattern")
      • 1.8:DTF.方法
    • 把字符串转成时间对象(parse)
  • 如何获取时间戳

    • 1.0:new Date() -> getTime() / System.currentTimeMillis()
    • 1.8:类名.now() -> toInstant(ZoneOffSet.UTC) -> toEpochMilli();

Arrays类

image-20240406200305169

package com.itheima.arrays;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Comparator;
import java.util.function.IntToDoubleFunction;

public class ArraysTest {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6};

        System.out.println(Arrays.toString(arr));

        int[] arr1 = Arrays.copyOfRange(arr, 0, 5);
        System.out.println(Arrays.toString(arr1));

        int[] arr3 = Arrays.copyOf(arr, 3);
        System.out.println(Arrays.toString(arr3));


        Integer[] arr4 = {1, 2, 3, 4, 5, 6};
        Arrays.sort(arr4, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        System.out.println(Arrays.toString(arr4));


        Student[] students = initStu();
        Arrays.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                // 降序
                return o2.getAge() - o1.getAge();
            }
        });
        System.out.println(Arrays.toString(students));


        double[] arr5 = {1, 2, 3, 4, 5, 6};
        Arrays.setAll(arr5, new IntToDoubleFunction() {
            @Override
            public double applyAsDouble(int value) {
                // value: 0 1 2 3...
                BigDecimal res = BigDecimal.valueOf(arr5[value])
                        .divide(BigDecimal.valueOf(0.8), 2, RoundingMode.HALF_UP);
                return res.doubleValue();
            }
        });
        System.out.println(Arrays.toString(arr5));


    }

    public static Student[] initStu() {
        Student[] stuArr = new Student[3];
        stuArr[0] = new Student(19, 188);
        stuArr[1] = new Student(35, 175);
        stuArr[2] = new Student(17, 183);
        return stuArr;
    }
}

Arrays类自定义排序规则---对象排序

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

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

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

public class Student implements Comparable<Student>{
    private String name;
    private double height;
    private int age;

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

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

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

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

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

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

排序方式1

  • 让Student类实现Comparable接口,同时重写compareTo方法。Arrays的sort方法底层会根据compareTo方法的返回值是正数、负数、还是0来确定谁大、谁小、谁相等。代码如下:
public class Student implements Comparable<Student>{
    private String name;
    private double height;
    private int age;

    //...get、set、空参数构造方法、有参数构造方法...自己补全

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

        //上面的if语句,也可以简化为下面的一行代码
        return this.age - o.age; // 按照年龄升序排列
        // return o.age - this.age; // 按照年龄降序排列
    }

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

排序方式2

  • 在调用Arrays.sort(数组,Comparator比较器);时,除了传递数组之外,传递一个Comparator比较器对象。Arrays的sort方法底层会根据Comparator比较器对象的compare方法方法的返回值是正数、负数、还是0来确定谁大、谁小、谁相等。代码如下
public class ArraysTest2 {
    public static void main(String[] args) {
        // 目标:掌握如何对数组中的对象进行排序。
        Student[] students = new Student[4];
        students[0] = new Student("蜘蛛精", 169.5, 23);
        students[1] = new Student("紫霞", 163.8, 26);
        students[2] = new Student("紫霞", 163.8, 26);
        students[3] = new Student("至尊宝", 167.5, 24);

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

jdk8新特性

lambda表达式

Lambda 表达式描述了一个代码块(或者叫匿名方法),可以将其作为参数传递给构造方法或者普通方法以便后续执行。

image-20240406200312303

package com.itheima.d6_lambda;

public class LambdaTest1 {
    public static void main(String[] args) {
        // 目标:认识Lambda表达式.
        //        Animal a = new Animal(){
        //            @Override
        //            public void run() {
        //                System.out.println("狗跑的贼快~~");
        //            }
        //        };
        //        a.run();

        // 注意:Lambda表达式并不是说能简化全部匿名内部类的写法,只能简化函数式接口的匿名内部类。
        // 函数式接口:接口 + 接口内部只有一个抽象方法
        //  错误的代码!


        //        Animal a = () -> {
        //            System.out.println("狗跑的贼快~~");
        //        };
        //        a.run();

        //         Swimming s = new Swimming(){
        //             @Override
        //             public void swim() {
        //                 System.out.println("学生快乐的游泳~~~~");
        //             }
        //         };
        //         s.swim();

        Swimming s = () -> {
            System.out.println("学生快乐的游泳~~~~");
        };
        s.swim();
    }
}

interface Swimming{
    void swim();
}

abstract class Animal{
    public abstract void run();
}

lambda表达式写法简化

image-20240406200319080

package com.itheima.d6_lambda;

import com.itheima.d5_arrays.Student;
import java.util.Arrays;

public class LambdaTest2 {
    public static void main(String[] args) {
        // 目标:使用Lambda简化函数式接口。
        double[] prices = {99.8, 128, 100};

//        Arrays.setAll(prices, new IntToDoubleFunction() {
//            @Override
//            public double applyAsDouble(int value) {
//                // value = 0  1  2
//                return prices[value] * 0.8;
//            }
//        });

//        Arrays.setAll(prices, (int value) -> {
//                return prices[value] * 0.8;
//        });

//        Arrays.setAll(prices, (value) -> {
//            return prices[value] * 0.8;
//        });

//        Arrays.setAll(prices, value -> {
//            return prices[value] * 0.8;
//        });

        Arrays.setAll(prices, value -> prices[value] * 0.8 );

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

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

//        Arrays.sort(students, new Comparator<Student>() {
//            @Override
//            public int compare(Student o1, Student o2) {
//                return Double.compare(o1.getHeight(), o2.getHeight()); // 升序
//            }
//        });

//        Arrays.sort(students, (Student o1, Student o2) -> {
//                return Double.compare(o1.getHeight(), o2.getHeight()); // 升序
//        });

//        Arrays.sort(students, ( o1,  o2) -> {
//            return Double.compare(o1.getHeight(), o2.getHeight()); // 升序
//        });

        Arrays.sort(students, ( o1,  o2) -> Double.compare(o1.getHeight(), o2.getHeight()));


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

方法引用

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

静态方法引用

img

我们先学习静态方法的引用,还是用之前Arrays代码来做演示。现在准备好下面的代码

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

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

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

现在,我想要把下图中Lambda表达式的方法体,用一个静态方法代替

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

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

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

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

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

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

实例方法引用

image-20240406200326636

还是基于上面的案例,我们现在来学习一下实例方法的引用。现在,我想要把下图中Lambda表达式的方法体,用一个实例方法代替。

在CompareByData类中,再添加一个实例方法,用于封装Lambda表达式的方法体

接下来,我们把Lambda表达式的方法体,改用对象调用方法

CompareByData compare = new CompareByData();
Arrays.sort(students, (o1, o2) -> compare.compareByAgeDesc(o1, o2)); // 降序

最后,再将Lambda表达式的方法体,直接改成方法引用写法。实际上就是用类实例调用方法,但是省略参数。这就是实例方法引用

CompareByData compare = new CompareByData();
Arrays.sort(students, compare::compareByAgeDesc); // 降序

特定类型方法的引用

各位小伙伴,我们继续学习特定类型的方法引用。在学习之前还是需要给大家说明一下,这种特定类型的方法引用是没有什么道理的,只是语法的一种约定,遇到这种场景,就可以这样用。

image-20240406200331554

如果某个Lambda表达式里只是调用一个实例方法,并且前面参数列表中的第一个参数作为方法的主调,后面的所有参数都是作为该实例方法的入参时,则就可以使用特定类型的方法引用。

格式:

​ 类型::方法名

public class Test2 {
    public static void main(String[] args) {
        String[] names = {"boby", "angela", "Andy" ,"dlei", "caocao", "Babo", "jack", "Cici"};

        // 要求忽略首字符大小写进行排序。
        Arrays.sort(names, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                // 制定比较规则。o1 = "Andy"  o2 = "angela"
                return o1.compareToIgnoreCase(o2);
            }
        });

        //lambda表达式写法
        Arrays.sort(names, ( o1,  o2) -> o1.compareToIgnoreCase(o2) );
        //特定类型的方法引用!
        Arrays.sort(names, String::compareToIgnoreCase);

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

构造器引用(几乎不使用)

image-20240406200337274

各位小伙伴,我们学习最后一种方法引用的形式,叫做构造器引用。还是先说明一下,构造器引用在实际开发中应用的并不多,目前还没有找到构造器的应用场景。所以大家在学习的时候,也只是关注语法就可以了。

现在,我们准备一个JavaBean类,Car类

public class Car {
    private String name;
    private double price;

    public Car() {

    }

    public Car(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 "Car{" +
        "name='" + name + '\'' +
        ", price=" + price +
        '}';
    }
}

因为方法引用是基于Lamdba表达式简化的,所以也要按照Lamdba表达式的使用前提来用,需要一个函数式接口,接口中代码的返回值类型是Car类型

interface CreateCar{
    Car create(String name, double price);
}

最后,再准备一个测试类,在测试类中创建CreateCar接口的实现类对象,先用匿名内部类创建、再用Lambda表达式创建,最后改用方法引用创建。同学们只关注格式就可以,不要去想为什么(语法就是这么设计的)。

public class Test3 {
    public static void main(String[] args) {
        // 1、创建这个接口的匿名内部类对象。
        CreateCar cc1 = new CreateCar(){
            @Override
            public Car create(String name, double price) {
                return new Car(name, price);
            }
        };
        //2、使用lambda表达式改进
        CreateCar cc2 = (name,  price) -> new Car(name, price);

        //3、使用方法引用改进:构造器引用
        CreateCar cc3 = Car::new;

        //注意:以上是创建CreateCar接口实现类对象的几种形式而已,语法一步一步简化。

        //4、对象调用方法
        Car car = cc3.create("奔驰", 49.9);
        System.out.println(car);
    }
}

Stream流

Stream流(也叫Stream API)。它是从JDK8以后才有的一个新特性,是专业用于对集合或者数组进行便捷操作的。

案例需求:有一个List集合,元素有"张三丰","张无忌","周芷若","赵敏","张强",找出姓张,且是3个字的名字,存入到一个新集合中去。

List<String> names = new ArrayList<>();
Collections.addAll(names, "张三丰","张无忌","周芷若","赵敏","张强");
System.out.println(names);
  • 用传统方式来做,代码是这样的
// 找出姓张,且是3个字的名字,存入到一个新集合中去。
List<String> list = new ArrayList<>();
for (String name : names) {
    if(name.startsWith("张") && name.length() == 3){
        list.add(name);
    }
}
System.out.println(list);
  • 用Stream流来做,代码是这样的(ps: 是不是想流水线一样,一句话就写完了)
List<String> list2 = names.stream().filter(s -> s.startsWith("张")).filter(a -> a.length()==3).collect(Collectors.toList());
System.out.println(list2);

先不用知道这里面每一句话是什么意思,具体每一句话的含义,待会再一步步学习。现在只是体验一下。

学习Stream流我们接下来,会按照下面的步骤来学习。

stream流的使用步骤image-20240406200400789

获取集合和数组的Stream流

image-20240406200414595

package com.itheima.d8_stream;

import java.util.*;
import java.util.stream.Stream;

/**
 * 目标:掌握Stream流的创建。
 */
public class StreamTest2 {
    public static void main(String[] args) {
        // 1、如何获取List集合的Stream流?
        List<String> names = new ArrayList<>();
        Collections.addAll(names, "张三丰","张无忌","周芷若","赵敏","张强");
        Stream<String> stream = names.stream();

        // 2、如何获取Set集合的Stream流?
        Set<String> set = new HashSet<>();
        Collections.addAll(set, "刘德华","张曼玉","蜘蛛精","马德","德玛西亚");
        Stream<String> stream1 = set.stream();
        stream1.filter(s -> s.contains("德")).forEach(s -> System.out.println(s));

        // 3、如何获取Map集合的Stream流?
        Map<String, Double> map = new HashMap<>();
        map.put("古力娜扎", 172.3);
        map.put("迪丽热巴", 168.3);
        map.put("马尔扎哈", 166.3);
        map.put("卡尔扎巴", 168.3);

        Set<String> keys = map.keySet();
        Stream<String> ks = keys.stream();

        Collection<Double> values = map.values();
        Stream<Double> vs = values.stream();

        Set<Map.Entry<String, Double>> entries = map.entrySet();
        Stream<Map.Entry<String, Double>> kvs = entries.stream();
        kvs.filter(e -> e.getKey().contains("巴"))
        .forEach(e -> System.out.println(e.getKey()+ "-->" + e.getValue()));

        // 简化写法
        map.entrySet().stream().filter(e -> e.getKey().contains("巴"))
        .forEach(s -> System.out.println(s.getKey() + "-->" + s.getValue()));
        // 4、如何获取数组的Stream流?
        String[] names2 = {"张翠山", "东方不败", "唐大山", "独孤求败"};
        Stream<String> s1 = Arrays.stream(names2);
        Stream<String> s2 = Stream.of(names2);
    }
}

Stream流的中间方法

处理数据还会返回新的Stream流

image-20240406200617863

image-20240406200536876

// distinct去重复,自定义类型的对象(希望内容一样就认为重复,重写hashCode,equals)

map:

image-20240503120348009

map映射
统计map键出现的次数
Map<Integer, Integer> map = list.stream()
                .collect(Collectors.toMap(k -> k, v -> 1, (oldValue, newValue) -> oldValue + newValue));
        System.out.println(map);

reduce:

image-20240503120312365

reduce归约
public class ReduceStreamDemo {
    public static void main(String[] args) {
        Integer[] ints = {0, 1, 2, 3};
        List<Integer> list = Arrays.asList(ints);

        Optional<Integer> optional = list.stream().reduce((a, b) -> a + b);
        Optional<Integer> optional1 = list.stream().reduce(Integer::sum);
        System.out.println(optional.orElse(0));
        System.out.println(optional1.orElse(0));

        int reduce = list.stream().reduce(6, (a, b) -> a + b);
        System.out.println(reduce);
        int reduce1 = list.stream().reduce(6, Integer::sum);
        System.out.println(reduce1);
    }
}

package com.itheima.d8_stream;
import java.util.*;
import java.util.stream.Stream;

/**
 * 目标:掌握Stream流提供的常见中间方法。
 */
public class StreamTest3 {
    public static void main(String[] args) {
        List<Double> scores = new ArrayList<>();
        Collections.addAll(scores, 88.5, 100.0, 60.0, 99.0, 9.5, 99.6, 25.0);
        // 需求1:找出成绩大于等于60分的数据,并升序后,再输出。
        scores.stream().filter(s -> s >= 60).sorted().forEach(s -> System.out.println(s));
        // 降序
        scores.stream().filter(s -> s >= 60)
        .sorted((o1, o2) -> Double.compare(o2, o1))
        // .forEach(s -> System.out.println(s));
        .forEach(System.out::println);

        List<Student> students = new ArrayList<>();
        Student s1 = new Student("蜘蛛精", 26, 172.5);
        Student s2 = new Student("蜘蛛精", 26, 172.5);
        Student s3 = new Student("紫霞", 23, 167.6);
        Student s4 = new Student("白晶晶", 25, 169.0);
        Student s5 = new Student("牛魔王", 35, 183.3);
        Student s6 = new Student("牛夫人", 34, 168.5);
        Collections.addAll(students, s1, s2, s3, s4, s5, s6);
        // 需求2:找出年龄大于等于23,且年龄小于等于30岁的学生,并按照年龄降序输出.
        students.stream().filter(s -> s.getAge() >= 23 && s.getAge() <= 30)
        .sorted((o1, o2) -> o2.getAge() - o1.getAge())
        .forEach(s -> System.out.println(s));

        // 需求3:取出身高最高的前3名学生,并输出。
        students.stream().sorted((o1, o2) -> Double.compare(o2.getHeight(), o1.getHeight()))
        .limit(3).forEach(System.out::println);
        System.out.println("----------------------------------------------------------------");

        // 需求4:取出身高倒数的2名学生,并输出。   s1 s2 s3 s4 s5 s6
        students.stream().sorted((o1, o2) -> Double.compare(o2.getHeight(), o1.getHeight()))
        .skip(students.size() - 2).forEach(System.out::println);

        // 需求5:找出身高超过168的学生叫什么名字,要求去除重复的名字,再输出。
        students.stream().filter(s -> s.getHeight() > 168)
        // .map(s -> s.getName()) // 可以使用特定类型的方法引用简化
        .map(Student::getName)
        .distinct().forEach(System.out::println);

        // distinct去重复,自定义类型的对象(希望内容一样就认为重复,重写hashCode,equals)
        students.stream().filter(s -> s.getHeight() > 168)
        .distinct().forEach(System.out::println);

        Stream<String> st1 = Stream.of("张三", "李四");
        Stream<String> st2 = Stream.of("张三2", "李四2", "王五");
        Stream<String> allSt = Stream.concat(st1, st2);
        allSt.forEach(System.out::println);
    }
}

Stream流的终结方法

调用完不会返回新的Stream流了

image-20240406200701163

img

package com.itheima.d8_stream;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 目标:Stream流的终结方法
 */
public class StreamTest4 {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        Student s1 = new Student("蜘蛛精", 26, 172.5);
        Student s2 = new Student("蜘蛛精", 26, 172.5);
        Student s3 = new Student("紫霞", 23, 167.6);
        Student s4 = new Student("白晶晶", 25, 169.0);
        Student s5 = new Student("牛魔王", 35, 183.3);
        Student s6 = new Student("牛夫人", 34, 168.5);
        Collections.addAll(students, s1, s2, s3, s4, s5, s6);
        // 需求1:请计算出身高超过168的学生有几人。
        long size = students.stream().filter(s -> s.getHeight() > 168).count();
        System.out.println(size);

        // 需求2:请找出身高最高的学生对象,并输出。
        Student s = students.stream().max((o1, o2) -> Double.compare(o1.getHeight(), o2.getHeight())).get();
        System.out.println(s);

        // 需求3:请找出身高最矮的学生对象,并输出。
        Student ss = students.stream().min((o1, o2) -> Double.compare(o1.getHeight(), o2.getHeight())).get();
        System.out.println(ss);

        // 需求4:请找出身高超过170的学生对象,并放到一个新集合中去返回。
        // 流只能收集一次。
        List<Student> students1 = students.stream().filter(a -> a.getHeight() > 170).collect(Collectors.toList());
        System.out.println(students1);

        Set<Student> students2 = students.stream().filter(a -> a.getHeight() > 170).collect(Collectors.toSet());
        System.out.println(students2);

        // 需求5:请找出身高超过170的学生对象,并把学生对象的名字和身高,存入到一个Map集合返回。
        Map<String, Double> map =
        students.stream().filter(a -> a.getHeight() > 170)
        .distinct().collect(Collectors.toMap(a -> a.getName(), a -> a.getHeight()));
        System.out.println(map);

        // Object[] arr = students.stream().filter(a -> a.getHeight() > 170).toArray();
        Student[] arr = students.stream().filter(a -> a.getHeight() > 170).toArray(len -> new Student[len]);
        System.out.println(Arrays.toString(arr));
    }
}

Stream流只能收集一次,下面这种代码是错误的,因为前面studentStream的流收集完成后已经被关闭了,下面再次执行students4会报错

// 流只能收集一次。
Stream<Student> studentStream = students.stream().filter(a -> a.getHeight() > 170);
List<Student> students3 = studentStream.collect(Collectors.toList());
System.out.println(students3);

Set<Student> students4 = studentStream.collect(Collectors.toSet());
System.out.println(students4);

img

排序算法

冒泡排序

image-20240406200725991

package com.itheima.d1_algorithm;

import java.util.Arrays;

/**
 * 目标:掌握冒泡排序的编写。
 */
public class Test1 {
    public static void main(String[] args) {
        // 1、准备一个数组
        int[] arr = {5, 2, 3, 1};

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

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

选择排序

选择排序的核心思路是,每一轮选择当前比较数据的最小值,将最小值传递到最前面

image-20240406200730142

package com.itheima.d1_algorithm;

import java.util.Arrays;

/**
 *  目标:掌握选择排序。
 */
public class Test2 {
    public static void main(String[] args) {
        // 1、准备好一个数组
        int[] arr = {5, 1, 3, 2};
        //           0  1  2  3

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

查找算法

二分查找

第1步:先定义两个变量,分别记录开始索引(left)和结束索引(right)

第2步:计算中间位置的索引,mid = (left+right)/2;

第3步:每次查找中间mid位置的元素,和目标元素key进行比较

​ 如果中间位置元素比目标元素小,那就说明mid前面的元素都比目标元素小

​ 此时:left = mid+1

​ 如果中间位置元素比目标元素大,那说明mid后面的元素都比目标元素大

​ 此时:right = mid-1

​ 如果中间位置元素和目标元素相等,那说明mid就是我们要找的位置

​ 此时:把mid返回

注意:一般查找一次肯定是不够的,所以需要把第1步和第2步循环来做,只到left>end就结束,如果最后还没有找到目标元素,就返回-1.

package com.itheima.d1_algorithm;

import java.util.Arrays;

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

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

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

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

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

正则表达式

重要!!!image-20240406200736212

image-20240406200748298

image-20240406200752235

package com.itheima.d2_regex;

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

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

        System.out.println("b".matches("[a-zA-Z]")); // [a-zA-Z] 只能是a-z A-Z的字符
        System.out.println("2".matches("[a-zA-Z]")); // false

        System.out.println("k".matches("[a-z&&[^bc]]")); // : a到z,除了b和c
        System.out.println("b".matches("[a-z&&[^bc]]")); // false

        System.out.println("ab".matches("[a-zA-Z0-9]")); // false 注意:以上带 [内容] 的规则都只能用于匹配单个字符

        // 2、预定义字符(只能匹配单个字符)  .  \d  \D   \s  \S  \w  \W
        System.out.println("徐".matches(".")); // .可以匹配任意字符
        System.out.println("徐徐".matches(".")); // false

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

        System.out.println(" ".matches("\\s"));   // \s: 代表一个空白字符
        System.out.println("a".matches("\\s")); // false

        System.out.println("a".matches("\\S"));  // \S: 代表一个非空白字符
        System.out.println(" ".matches("\\S")); // false
		// \w 小写 匹配一个字符(字母数组下划线,不包含汉字)
        System.out.println("a".matches("\\w"));  // \w: [a-zA-Z_0-9]
        System.out.println("_".matches("\\w")); // true
        System.out.println("徐".matches("\\w")); // false
		// \W 大写 匹配非字符
        System.out.println("徐".matches("\\W"));  // [^\w]不能是a-zA-Z_0-9
        System.out.println("a".matches("\\W"));  // false

        System.out.println("23232".matches("\\d")); // false 注意:以上预定义字符都只能匹配单个字符。

        // 3、数量词: ?   *   +   {n}   {n, }  {n, m}
        System.out.println("a".matches("\\w?"));   // ? 代表0次或1次
        System.out.println("".matches("\\w?"));    // true
        System.out.println("abc".matches("\\w?")); // false

        System.out.println("abc12".matches("\\w*"));   // * 代表0次或多次
        System.out.println("".matches("\\w*"));        // true
        System.out.println("abc12张".matches("\\w*")); // false

        System.out.println("abc12".matches("\\w+"));   // + 代表1次或多次
        System.out.println("".matches("\\w+"));       // false
        System.out.println("abc12张".matches("\\w+")); // false

        System.out.println("a3c".matches("\\w{3}"));   // {3} 代表要正好是n次
        System.out.println("abcd".matches("\\w{3}"));  // false
        System.out.println("abcd".matches("\\w{3,}"));     // {3,} 代表是>=3次
        System.out.println("ab".matches("\\w{3,}"));     // false
        System.out.println("abcde徐".matches("\\w{3,}"));     // false
        System.out.println("abc232d".matches("\\w{3,9}"));     // {3, 9} 代表是  大于等于3次,小于等于9次

        // 4、其他几个常用的符号:(?i)忽略大小写 、 或:| 、  分组:()
        System.out.println("abc".matches("(?i)abc")); // true
        System.out.println("ABC".matches("(?i)abc")); // true
        System.out.println("aBc".matches("a((?i)b)c")); // true
        System.out.println("ABc".matches("a((?i)b)c")); // false

        // 需求1:要求要么是3个小写字母,要么是3个数字。
        System.out.println("abc".matches("[a-z]{3}|\\d{3}")); // true
        System.out.println("ABC".matches("[a-z]{3}|\\d{3}")); // false
        System.out.println("123".matches("[a-z]{3}|\\d{3}")); // true
        System.out.println("A12".matches("[a-z]{3}|\\d{3}")); // false

        // 需求2:必须是”我爱“开头,中间可以是至少一个”编程“,最后至少是1个”666“
        System.out.println("我爱编程编程666666".matches("我爱(编程)+(666)+"));	// true
        System.out.println("我爱编程编程66666".matches("我爱(编程)+(666)+"));		// false
    }
}

Pattern类

package com.itheima.d2_regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 目标:掌握使用正则表达式查找内容。
 */
public class RegexTest4 {
    public static void main(String[] args) {
        method1();
    }

    // 需求1:从以下内容中爬取出,手机,邮箱,座机、400电话等信息。
    public static void method1(){
        String data = " 来黑马程序员学习Java,\n" +
        "        电话:1866668888,18699997777\n" +
        "        或者联系邮箱:[email protected],\n" +
        "        座机电话:01036517895,010-98951256\n" +
        "        邮箱:[email protected],\n" +
        "        邮箱:[email protected],\n" +
        "        热线电话:400-618-9090 ,400-618-4000,4006184000,4006189090";
        // 1、定义爬取规则
        String regex = "(1[3-9]\\d{9})|(0\\d{2,7}-?[1-9]\\d{4,19})|(\\w{2,}@\\w{2,20}(\\.\\w{2,10}){1,2})"
        + "|(400-?\\d{3,7}-?\\d{3,7})";
        // 2、把正则表达式封装成一个Pattern对象
        Pattern pattern = Pattern.compile(regex);
        // 3、通过pattern对象去获取查找内容的匹配器对象。
        Matcher matcher = pattern.matcher(data);
        // 4、定义一个循环开始爬取信息
        while (matcher.find()){
            String rs = matcher.group(); // 获取到了找到的内容了。
            System.out.println(rs);
        }
    }
}

贪婪模式和非贪婪模式

在贪婪模式下,匹配器尽可能多地匹配符合要求的字符,直到不能再匹配为止。例如,正则表达式 a.*b 在匹配字符串 "abbcab" 时,会匹配整个字符串 "abbcab",而不是期望的 "ab"。

在非贪婪模式下,匹配器尽可能少地匹配符合要求的字符,直到满足要求为止。例如,正则表达式 a.*?b 在匹配字符串 "abbcab" 时,只会匹配到第一个 "ab",而不是整个字符串。

package com.itheima.d2_regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest5 {
    public static void main(String[] args) {
        method1();
        System.out.println("--------------------");
        method2();
    }

    // 需求2:把全部邮箱的账户名找出来。
    public static void method1(){
        String data = " 来黑马程序员学习Java,\n" +
        "        电话:1866668888,18699997777\n" +
        "        或者联系邮箱:[email protected],\n" +
        "        座机电话:01036517895,010-98951256\n" +
        "        邮箱:[email protected],\n" +
        "        邮箱:[email protected],\n" +
        "        热线电话:400-618-9090 ,heima, 400-618-4000,4006184000,4006189090";
        // 1、定义爬取规则
        String regex = "(\\w{2,})@(\\w{2,20})(\\.\\w{2,10}){1,2}";
        // 2、把正则表达式封装成一个Pattern对象
        Pattern pattern = Pattern.compile(regex);
        // 3、通过pattern对象去获取查找内容的匹配器对象。
        Matcher matcher = pattern.matcher(data);
        // 4、定义一个循环开始爬取信息
        while (matcher.find()){
            System.out.println("group = " + matcher.group());
            System.out.println("group1 = " + matcher.group(1)); // 指定获取正则表达式匹配后的第一组内容
            System.out.println("group2 = " + matcher.group(2)); // 指定获取正则表达式匹配后的第二组内容
        }
    }


    // 需求3:某系统的日志文件记录了当天进入系统的全部用户信息,需要把这些用户的名字爬取出来另作他用。
    public static void method2(){
        String data = "欢迎张全蛋光临本系统!他删库并跑路,欢迎李二狗子光临本系统!" +
        "欢迎马六子光临本系统!它浏览了很多好看的照片!欢迎夏洛光临本系统!他在六点钟购买了一台拖拉机!";
        // 1、定义爬取规则
        // String regex = "欢迎(.+)光临"; // 贪婪匹配
        String regex = "欢迎(.+?)光临"; // +? 非贪婪匹配
        // 2、把正则表达式封装成一个Pattern对象
        Pattern pattern = Pattern.compile(regex);
        // 3、通过pattern对象去获取查找内容的匹配器对象。
        Matcher matcher = pattern.matcher(data);
        // 4、定义一个循环开始爬取信息
        while (matcher.find()){
            System.out.println("group = " + matcher.group());
            System.out.println("group1 = " + matcher.group(1));
        }
    }
}

练习:搜索替换,内容分割

package com.itheima.d2_regex;

import java.util.Arrays;

/**
 * 目标:掌握使用正则表达式做搜索替换,内容分割。
 */
public class RegexTest6 {
    public static void main(String[] args) {
        // 1、public String replaceAll(String regex , String newStr):按照正则表达式匹配的内容进行替换
        // 需求1:请把 古力娜扎ai8888迪丽热巴999aa5566马尔扎哈fbbfsfs42425卡尔扎巴,中间的非中文字符替换成 “-”
        String s1 = "古力娜扎ai8888迪丽热巴999aa5566马尔扎哈fbbfsfs42425卡尔扎巴";
        System.out.println(s1.replaceAll("\\w+", "-"));

        // 需求2(拓展):某语音系统,收到一个口吃的人说的“我我我喜欢编编编编编编编编编编编编程程程!”,需要优化成“我喜欢编程!”。
        String s2 = "我我我喜欢编编编编编编编编编编编编程程程";
        /**
         * (.)一组:.匹配任意字符的。
         * \\1 :为这个组声明一个组号:1号
         * +:声明必须是重复的字
         * $1可以去取到第1组代表的那个重复的字
         */
        System.out.println(s2.replaceAll("(.)\\1+", "$1"));

        // 2、public String[] split(String regex):按照正则表达式匹配的内容进行分割字符串,反回一个字符串数组。
        // 需求1:请把 古力娜扎ai8888迪丽热巴999aa5566马尔扎哈fbbfsfs42425卡尔扎巴,中的人名获取出来。
        String s3 = "古力娜扎ai8888迪丽热巴999aa5566马尔扎哈fbbfsfs42425卡尔扎巴";
        String[] names = s3.split("\\w+");
        System.out.println(Arrays.toString(names));
    }
}

(.)\1+ 在java中的正则含义

表示匹配连续的多个相同的任意字符,只要找到字符串里面存在连续的两个或者以上的相同字符即匹配。

括号中的点表示任意字符,后面的\1表示取第一个括号匹配的内容,后面的加号表示匹配1次或1次以上,二者加在一起就是某个字符重复两次或两次以上。

/**
     * 1.将下面的国家重叠的字符替换成 竖线 |
     * ChinaqqqAmericahhhhhEnglandaaaaaaMexica
     */
@Test
public void test1()
{
    String str="ChinaqqqAmericahhhhhEnglandaaaaaaMexica";

    //(.)\\1+:表示匹配连续的多个相同的任意字符,只要找到字符串里面存在连续的两个或者以上的相同字符即匹配。
    System.out.println(str.replaceAll("(.)\\1+", "|"));//China|America|England|Mexica
}

/**
     * 2.将下面的国家重叠的字符替换成 一个, 也就是去掉重复的分隔符
     * China|||||America::::::England&&&&&&&Mexica
     */
@Test
public void test2()
{
    String str="China|||||America::::::England&&&&&&&Mexica";

    //(.)\\1+:表示匹配连续的多个相同的任意字符,只要找到字符串里面存在连续的两个或者以上的相同字符即匹配。 
    System.out.println(str.replaceAll("(.)\\1+","$1"));//China|America:England&Mexica
}

异常

异常体系

运行时异常、编译时异常

image-20240406200801988

常见的运行时异常和编译时异常

运行时异常:

  • NullPointerException(空指针异常)

  • ArrayIndexOutOfBoundsException(数组越界异常)

  • ClassCastException(类转换异常)

  • ArithmeticException(算术异常)

  • IllegalArgumentException(非法参数异常)

编译时异常:

  • IOException(输入输出异常)

  • SQLException(SQL异常)

  • FileNotFoundException(文件未找到异常)

  • ClassNotFoundException(类未找到异常)

  • InterruptedException(线程中断异常)

  • 日期解析异常

抛出异常和捕获异常

image-20240416105857435

自定义异常

自定义运行时异常和自定义编译时异常

image-20240406200814932

package com.itheima.d3_exception;
// 1、必须让这个类继承自RuntimeException,才能成为一个运行时异常类。
public class AgeIllegalRuntimeException extends RuntimeException{
    public AgeIllegalRuntimeException() {
    }

    public AgeIllegalRuntimeException(String message) {
        super(message);
    }
}




package com.itheima.d3_exception;
// 1、必须让这个类继承自Exception,才能成为一个编译时异常类。
public class AgeIllegalException extends Exception{
    public AgeIllegalException() {
    }

    public AgeIllegalException(String message) {
        super(message);
    }
}





package com.itheima.d3_exception;

/**
 * 目标:掌握自定义异常,以及异常的作用。
 */
public class ExceptionTest2 {
    public static void main(String[] args) {
        // 需求:保存一个合法的年龄
        //        try {
        //            saveAge(223);
        //            System.out.println("底层执行成功的!");
        //        } catch (Exception e) {
        //            e.printStackTrace();
        //            System.out.println("底层出现了bug!");
        //        }

        try {
            saveAge2(225);
            System.out.println("saveAge2底层执行是成功的!");
        } catch (AgeIllegalException e) {
            e.printStackTrace();
            System.out.println("saveAge2底层执行是出现bug的!");
        }
    }

    // 编译时异常
    public static void saveAge2(int age) throws AgeIllegalException{
        if(age > 0 && age < 150){
            System.out.println("年龄被成功保存: " + age);
        }else {
            // 用一个异常对象封装这个问题
            // throw 抛出去这个异常对象
            // throws 用在方法上,抛出方法内部的异常
            throw new AgeIllegalException("/age is illegal, your age is " + age);
        }
    }


    // 运行时异常
    public static void saveAge(int age){
        if(age > 0 && age < 150){
            System.out.println("年龄被成功保存: " + age);
        }else {
            // 用一个异常对象封装这个问题
            // throw 抛出去这个异常对象
            throw new AgeIllegalRuntimeException("/age is illegal, your age is " + age);
        }
    }
}

异常的处理

在开发中异常的常见处理方式是:底层的异常抛出去给最外层,最外层集中捕获处理。

image-20240406200820692

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

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

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

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

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

异常关键字

  • try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。

  • catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。

  • finally – finally语句块总是会被执行。它主要用于回收在try块里打开的资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。

  • throw – 用于方法内部,用于主动抛出异常。

  • throws – 用在方法签名中,用于声明该方法可能抛出的异常。

  • throws 关键字在声明异常的时候可以跟多个,用逗号隔开;而 throw 关键字每次只能抛出一个异常。

package com.itheima.test;

public class Test {
    public static void main(String[] args) {
        System.out.println(test());
    }
    public static int test() {
        try {
            System.out.println(123);
            return 1;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(456);
        }
        return 0;
    }
}


// 运行结果
123
456
1

finally注意事项

try {
            System.out.println(111);
            System.exit(0);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
        // 此处不执行了
            System.out.println(123);
        }
  • 在前面的代码中用了System.exit()退出程序。
  • finally语句块中发生了异常。
  • 程序所在的线程死亡。
  • 关闭CPU。

不要在finally中使用return语句

try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。

如下是一个反例:

private int x = 0;
public int checkReturn() {
    try {
        // x等于1,此处不返回
        return ++x;
    } finally {
        // 返回的结果是2
        return ++x;
    }
}

throw和throws的区别

  • throws 关键字用于声明异常,它的作用和 try-catch 相似;而 throw 关键字用于显式的抛出异常。
  • throws 关键字后面跟的是异常的名字;而 throw 关键字后面跟的是异常的对象。

示例。

throws ArithmeticException;
throw new ArithmeticException("算术异常");
  • throws 关键字出现在方法签名上,而 throw 关键字出现在方法体里。
  • throws 关键字在声明异常的时候可以跟多个,用逗号隔开;而 throw 关键字每次只能抛出一个异常。

集合进阶

集合容器中只能存放对象,基本数据类型需要使用对应的包装类

Collection单列集合

collection集合体系 image-20240406200830075

collection常用方法

image-20240406200840494

package com.itheima.d1_collection;

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

/**
    目标:掌握Collection集合的常用API.
    Collection是集合的祖宗类,它的功能是全部集合都可以继承使用的,所以要学习它。
    Collection API如下:
         - public boolean add(E e):  把给定的对象添加到当前集合中 。
         - public void clear() :清空集合中所有的元素。
         - public boolean remove(E e): 把给定的对象在当前集合中删除。
         - public boolean contains(Object obj): 判断当前集合中是否包含给定的对象。
         - public boolean isEmpty(): 判断当前集合是否为空。
         - public int size(): 返回集合中元素的个数。
         - public Object[] toArray(): 把集合中的元素,存储到数组中。
 */
public class CollectionTest2API {
    public static void main(String[] args) {
        Collection<String> c = new ArrayList<>(); // 多态写法
        // 1.public boolean add(E e):添加元素, 添加成功返回true。
        c.add("java1");
        c.add("java1");
        c.add("java2");
        c.add("java2");
        c.add("java3");
        System.out.println(c);

        // 2.public void clear():清空集合的元素。
        //c.clear();
        //System.out.println(c);

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

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

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

        // 6.public boolean remove(E e):删除某个元素:如果有多个重复元素默认删除前面的第一个!
        System.out.println(c.remove("java1"));
        System.out.println(c);

        // 7.public Object[] toArray():把集合转换成数组
        Object[] arr = c.toArray();
        System.out.println(Arrays.toString(arr));

        String[] arr2 = c.toArray(new String[c.size()]);
        System.out.println(Arrays.toString(arr2));

        System.out.println("--------------------------------------------");
        // 把一个集合的全部数据倒入到另一个集合中去。
        Collection<String> c1 = new ArrayList<>();
        c1.add("java1");
        c1.add("java2");
        Collection<String> c2 = new ArrayList<>();
        c2.add("java3");
        c2.add("java4");
        c1.addAll(c2); // 就是把c2集合的全部数据倒入到c1集合中去。
        System.out.println(c1);
        System.out.println(c2);
    }
}

迭代器遍历集合

image-20240406200844773

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

代码写法如下:

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

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

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

迭代器代码的原理如下:

  • 当调用iterator()方法获取迭代器时,当前指向第一个元素
  • hasNext()方法则判断这个位置是否有元素,如果有则返回true,进入循环
  • 调用next()方法获取元素,然后自动指向下一个位置
  • 等下次循环时,则获取下一个元素,依此内推
增强for循环遍历集合

增强for不光可以遍历集合,还可以遍历数组。接下来我们用代码演示一下:

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

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

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

集合的foreach方法结合lambda表达式遍历结合

c.forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
由于Consumer是接口,无法实例化对象,所以可以使用匿名内部类,
由于consumer是函数式接口,所以可以使用lambda表达式来简化
package com.itheima.d2_collection_traverse;
import java.util.ArrayList;
import java.util.Collection;

/**
     目标:Collection集合的遍历方式三:JDK8开始新增的Lambda表达式。
 */
public class CollectionDemo03 {
    public static void main(String[] args) {
        Collection<String> c = new ArrayList<>();
        c.add("赵敏");
        c.add("小昭");
        c.add("殷素素");
        c.add("周芷若");
        System.out.println(c);
        // [赵敏, 小昭, 殷素素, 周芷若]
        //                     s

        // default void forEach(Consumer<? super T> action):  结合Lambda表达式遍历集合:
        //        c.forEach(new Consumer<String>() {
        //            @Override
        //            public void accept(String s) {
        //                System.out.println(s);
        //            }
        //        });
        //
        //        c.forEach((String s) -> {
        //                System.out.println(s);
        //        });
        //
        //        c.forEach(s  -> {
        //            System.out.println(s);
        //        });
        //
        //        c.forEach(s  -> System.out.println(s) );

        c.forEach(System.out::println );
    }
}

集合在计算机中的存储原理

当往集合中存对象时,实际上存储的是对象的地址值

image-20240406200851809

List集合

image-20240406200855481

List集合的特有方法

image-20240406200859651

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

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

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

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

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

list集合的遍历方式

  • 普通for循环(只因为List有索引)
  • 迭代器
  • 增强for
  • Lambda表达式
package com.itheima.d3_collection_list;

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

/**
    拓展:List系列集合的遍历方式.

    List遍历方式:
        (1)for循环。(独有的,因为List有索引)。
        (2)迭代器。
        (3)foreach。
        (4)JDK 1.8新技术。

 */
public class ListTest2 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("糖宝宝");
        list.add("蜘蛛精");
        list.add("至尊宝");

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

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

        //(3)增强for循环(foreach遍历)
        for (String s : list) {
            System.out.println(s);
        }

        //(4)JDK 1.8开始之后的Lambda表达式
        list.forEach(s -> {
            System.out.println(s);
        });
    }
}

ArrayList集合

ArrayList集合的底层原理

image-20240406200904881

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

image-20240406200909789

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

image-20240406200914191

ArrayList的使用场景 image-20240406200918407

LinkedList集合

LinkedList集合介绍

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

image-20240406200930286

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

image-20240406200934576

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

image-20240406200940702

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

image-20240406200944509

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

image-20240406200948039

LinkedList集合的使用场景

设计栈和队列

  • 队列

image-20240406200952064

image-20240406200955974

package com.itheima.d3_collection_list;

import java.util.LinkedList;

/**
 * 目标:掌握LinkedList集合的使用。
 */
public class ListTest3 {
    public static void main(String[] args) {
        // 1、创建一个队列。
        LinkedList<String> queue = new LinkedList<>();
        // 入队
        queue.addLast("第1号人");
        queue.addLast("第2号人");
        queue.addLast("第3号人");
        queue.addLast("第4号人");
        System.out.println(queue);
        // 出队
        System.out.println(queue.removeFirst());
        System.out.println(queue.removeFirst());
        System.out.println(queue.removeFirst());
        System.out.println(queue);
        System.out.println("--------------------------------------------------");
        // 2、创建一个栈对象。
        LinkedList<String> stack = new LinkedList<>();
        // 压栈(push)   实际调用的就是addFirst方法
        stack.push("第1颗子弹");
        stack.push("第2颗子弹");
        stack.push("第3颗子弹");
        stack.push("第4颗子弹");
        System.out.println(stack);
        // 出栈(pop)   实际调用的就是removeFirst方法
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack);
    }
}

Set集合

image-20240406200959580

//Set<Integer> set = new HashSet<>();	//无序、无索引、不重复    [按照哈希算法计算位置输出的]
//Set<Integer> set = new LinkedHashSet<>(); //有序、无索引、不重复    [666, 555, 777, 888]
Set<Integer> set = new TreeSet<>(); //可排序(升序)、无索引、不重复   [555, 666, 777, 888]
set.add(666);
set.add(555);
set.add(555);
set.add(888);
set.add(888);
set.add(777);
set.add(777);
System.out.println(set); //[555, 666, 777, 888]

HashSet集合

HashSet可以存储null值

image-20240418140748110

哈希值

image-20240406201004024

HashSet集合的底层原理(基于HashMap集合)

image-20240406201012563

HashSet集合底层是基于哈希表实现的,哈希表根据JDK版本的不同,也是有点区别的

  • JDK8以前:哈希表 = 数组+链表
  • JDK8以后:哈希表 = 数组+链表+红黑树

image-20240406201016593

我们发现往HashSet集合中存储元素时,底层调用了元素的两个方法:一个是hashCode方法获取元素的hashCode值(哈希值);另一个是调用了元素的equals方法,用来比较新添加的元素和集合中已有的元素是否相同。

  • 只有新添加元素的hashCode值和集合中以后元素的hashCode值相同、新添加的元素调用equals方法和集合中已有元素比较结果为true, 才认为元素重复。
  • 如果hashCode值相同,equals比较不同,则以链表的形式连接在数组的同一个索引为位置(如上图所示)

在JDK8开始后,为了提高性能,当链表的长度超过8时,就会把链表转换为红黑树,如下图所示:

image-20240406201021172

image-20240418103113199

二叉树

image-20240406201024354

二叉排序树

image-20240406201027335

平衡二叉树

image-20240406201030628

红黑树image-20240406201034245

HashSet去重原理

前面我们学习了HashSet存储元素的原理,依赖于两个方法:一个是hashCode方法用来确定在底层数组中存储的位置,另一个是用equals方法判断新添加的元素是否和集合中已有的元素相同。

要想保证在HashSet集合中没有重复元素,我们需要重写元素类的hashCode和equals方法。比如以下面的Student类为例,假设把Student类的对象作为HashSet集合的元素,想要让学生的姓名和年龄相同,就认为元素重复。

public class Student{
    private String name; //姓名
    private int age; //年龄
    private double height; //身高

    //无参数构造方法
    public Student(){}
    //全参数构造方法
    public Student(String name, int age, double height){
        this.name=name;
        this.age=age;
        this.height=height;
    }
    //...get、set、toString()方法自己补上..

    //按快捷键生成hashCode和equals方法
    //alt+insert 选择 hashCode and equals
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (age != student.age) return false;
        if (Double.compare(student.height, height) != 0) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }

    @Override
    public int hashCode() {
        int result;
        long temp;
        result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        temp = Double.doubleToLongBits(height);
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        return result;
    }
}

接着,写一个测试类,往HashSet集合中存储Student对象。

public class Test{
    public static void main(String[] args){
        Set<Student> students = new HashSet<>();
        Student s1 = new Student("至尊宝",20, 169.6);
        Student s2 = new Student("蜘蛛精",23, 169.6);
        Student s3 = new Student("蜘蛛精",23, 169.6);
        Student s4 = new Student("牛魔王",48, 169.6);
        
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
        
        for(Student s : students){
            System.out.println(s);
        }
    }
}

打印结果如下,我们发现存了两个蜘蛛精,当时实际打印出来只有一个,而且是无序的。

Student{name='牛魔王', age=48, height=169.6}
Student{name='至尊宝', age=20, height=169.6}
Student{name='蜘蛛精', age=23, height=169.6}

LinkedHashSet集合

img

image-20240406201117621

每次添加元素,就和上一个元素用双向链表连接一下。第一个添加的元素是双向链表的头节点,最后一个添加的元素是双向链表的尾节点。

把上个案例中的集合改成LinkedList集合,我们观察效果怎样

public class Test{
    public static void main(String[] args){
        Set<Student> students = new LinkedHashSet<>();
        Student s1 = new Student("至尊宝",20, 169.6);
        Student s2 = new Student("蜘蛛精",23, 169.6);
        Student s3 = new Student("蜘蛛精",23, 169.6);
        Student s4 = new Student("牛魔王",48, 169.6);

        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);

        for(Student s : students){
            System.out.println(s);
        }
    }
}

打印结果如下

Student{name='至尊宝', age=20, height=169.6}
Student{name='蜘蛛精', age=23, height=169.6}
Student{name='牛魔王', age=48, height=169.6}

TreeSet集合

TreeSet不可以存储null值,会报空指针异常

TreeSet集合底层原理基于HashMap集合实现image-20240406201153282

image-20240406201211637

TreeSet自定义排序规则

我们想要告诉TreeSet集合按照指定的规则排序,有两种办法:

第一种:让元素的类实现Comparable接口,重写compareTo方法

第二种:在创建TreeSet集合时,通过构造方法传递Compartor比较器对象

  • 排序方式1:我们先来演示第一种排序方式
//第一步:先让Student类,实现Comparable接口
//注意:Student类的对象是作为TreeSet集合的元素的
public class Student implements Comparable<Student>{
    private String name;
    private int age;
    private double height;
    //无参数构造方法
    public Student(){}
    //全参数构造方法
    public Student(String name, int age, double height){
        this.name=name;
        this.age=age;
        this.height=height;
    }
    //...get、set、toString()方法自己补上..

    //第二步:重写compareTo方法
    //按照年龄进行比较,只需要在方法中让this.age和o.age相减就可以。
    /*
    原理:
    在往TreeSet集合中添加元素时,add方法底层会调用compareTo方法,根据该方法的
    结果是正数、负数、还是零,决定元素放在后面、前面还是不存。
    */
    @Override
    public int compareTo(Student o) {
        //this:表示将要添加进去的Student对象
        //o: 表示集合中已有的Student对象
        return this.age-o.age;
    }
}

此时,再运行测试类,结果如下

Student{name='至尊宝', age=20, height=169.6}
Student{name='紫霞', age=20, height=169.8}
Student{name='蜘蛛精', age=23, height=169.6}
Student{name='牛魔王', age=48, height=169.6}
  • 排序方式2:接下来演示第二种排序方式
//创建TreeSet集合时,传递比较器对象排序
/*
原理:当调用add方法时,底层会先用比较器,根据Comparator的compare方是正数、负数、还是零,决定谁在后,谁在前,谁不存。
*/
//下面代码中是按照学生的年龄升序排序
Set<Student> students = new TreeSet<>(new Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2){
        //需求:按照学生的身高排序
        return Double.compare(o1,o2); 
    }
});

//创建4个Student对象
Student s1 = new Student("至尊宝",20, 169.6);
Student s2 = new Student("紫霞",23, 169.8);
Student s3 = new Student("蜘蛛精",23, 169.6);
Student s4 = new Student("牛魔王",48, 169.6);

//添加Studnet对象到集合
students.add(s1);
students.add(s2);
students.add(s3);
students.add(s4);
System.out.println(students);

image-20240406201247369

不同集合的使用场景

image-20240406201242723

image-20240406201254950

集合的并发修改异常

image-20240406201259707

package com.itheima.d5_collection_exception;
import java.util.*;

/**
 * 目标:理解集合的并发修改异常问题,并解决。
 */
public class CollectionTest1 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("王麻子");
        list.add("小李子");
        list.add("李爱花");
        list.add("张全蛋");
        list.add("晓李");
        list.add("李玉刚");
        System.out.println(list);
        //  [王麻子, 小李子, 李爱花, 张全蛋, 晓李, 李玉刚]

        // 需求:找出集合中全部带“李”的名字,并从集合中删除。
//        Iterator<String> it = list.iterator();
//        while (it.hasNext()){
//            String name = it.next();
//            if(name.contains("李")){
//                list.remove(name);
//            }
//        }
//        System.out.println(list);

        // 使用for循环遍历集合并删除集合中带李字的名字
        //  [王麻子, 小李子, 李爱花, 张全蛋, 晓李, 李玉刚]
        //  [王麻子, 李爱花, 张全蛋, 李玉刚]
        //                                i
//        for (int i = 0; i < list.size(); i++) {
//            String name = list.get(i);
//            if(name.contains("李")){
//                list.remove(name);
//            }
//        }
//        System.out.println(list);

        System.out.println("---------------------------------------------------------");
        // 怎么解决呢?
        // 使用for循环遍历集合并删除集合中带李字的名字
        //  [王麻子, 小李子, 李爱花, 张全蛋, 晓李, 李玉刚]
        //  [王麻子, 张全蛋]
        //                  i
//        for (int i = 0; i < list.size(); i++) {
//            String name = list.get(i);
//            if(name.contains("李")){
//                list.remove(name);
//                i--;
//            }
//        }
//        System.out.println(list);
        // 倒着去删除也是可以的。

        // 需求:找出集合中全部带“李”的名字,并从集合中删除。
//        Iterator<String> it = list.iterator();
//        while (it.hasNext()){
//            String name = it.next();
//            if(name.contains("李")){
//                // list.remove(name); // 并发修改异常的错误。
//                it.remove(); // 删除迭代器当前遍历到的数据,每删除一个数据后,相当于也在底层做了i--
//            }
//        }
//        System.out.println(list);

        // 使用增强for循环遍历集合并删除数据,没有办法解决bug.
//        for (String name : list) {
//            if(name.contains("李")){
//                list.remove(name);
//            }
//        }
//        System.out.println(list);

//        list.forEach(name -> {
//            if(name.contains("李")){
//                list.remove(name);
//            }
//        });
//        System.out.println(list);
    }
}

Collections工具类

常用方法

注意Collections并不是集合,它比Collection多了一个s,一般后缀为s的类很多都是工具类。这里的Collections是用来操作Collection的工具类。它提供了一些好用的静态方法,如下
image-20240406201305891

我们把这些方法用代码来演示一下:

public class CollectionsTest{
    public static void main(String[] args){
        //1.public static <T> boolean addAll(Collection<? super T> c, T...e)
        List<String> names = new ArrayList<>();
        Collections.addAll(names, "张三","王五","李四", "张麻子");
        System.out.println(names);

        //2.public static void shuffle(List<?> list):对集合打乱顺序
        Collections.shuffle(names);
        System.out.println(names);

        //3.public static <T> void short(List<T list): 对List集合排序
        List<Integer> list = new ArrayList<>();
        list.add(3);
        list.add(5);
        list.add(2);
        Collections.sort(list);
        System.out.println(list);
    }
}

Collections工具类自定义排序方法

上面我们往集合中存储的元素要么是Stirng类型,要么是Integer类型,他们本来就有一种自然顺序所以可以直接排序。但是如果我们往List集合中存储Student对象,这个时候想要对List集合进行排序自定义比较规则的。指定排序规则有两种方式,如下:

排序方式1:让元素实现Comparable接口,重写compareTo方法

比如现在想要往集合中存储Studdent对象,首先需要准备一个Student类,实现Comparable接口。

public class Student implements Comparable<Student>{
    private String name;
    private int age;
    private double height;

    //排序时:底层会自动调用此方法,this和o表示需要比较的两个对象
    @Override
    public int compareTo(Student o){
        //需求:按照年龄升序排序
        //如果返回正数:说明左边对象的年龄>右边对象的年龄
        //如果返回负数:说明左边对象的年龄<右边对象的年龄,
        //如果返回0:说明左边对象的年龄和右边对象的年龄相同
        return this.age - o.age;
    }

    //...getter、setter、constructor..
}

然后再使用Collections.sort(list集合)对List集合排序,如下:

//3.public static <T> void short(List<T list): 对List集合排序
List<Student> students = new ArrayList<>();
students.add(new Student("蜘蛛精",23,169.7));
students.add(new Student("紫霞",22,169.8));
students.add(new Student("紫霞",22,169.8));
students.add(new Student("至尊宝",26,169.5));

/*
原理:sort方法底层会遍历students集合中的每一个元素,采用排序算法,将任意两个元素两两比较;
    每次比较时,会用一个Student对象调用compareTo方法和另一个Student对象进行比较;
    根据compareTo方法返回的结果是正数、负数,零来决定谁大,谁小,谁相等,重新排序元素的位置
    
    注意:这些都是sort方法底层自动完成的,想要完全理解,必须要懂排序算法才行;
*/
Collections.sort(students); 
System.out.println(students);

排序方式2:使用调用sort方法是,传递比较器

/*
原理:sort方法底层会遍历students集合中的每一个元素,采用排序算法,将任意两个元素两两比较;
    每次比较,会将比较的两个元素传递给Comparator比较器对象的compare方法的两个参数o1和o2,
    根据compare方法的返回结果是正数,负数,或者0来决定谁大,谁小,谁相等,重新排序元素的位置
    
    注意:这些都是sort方法底层自动完成的,不需要我们完全理解,想要理解它必须要懂排序算法才行.
*/
Collections.sort(students, new Comparator<Student>(){
    @Override
    public int compare(Student o1, Student o2){
        return o1.getAge()-o2.getAge();
    }
}); 
System.out.println(students);

斗地主案例

我们先分析一下业务需求:

  • 总共有54张牌,每一张牌有花色和点数两个属性、为了排序还可以再加一个序号
  • 点数可以是:“3”,"4","5","6","7","8","9","10","J","Q","K","A","2"
  • 花色可以是:“♣”,"♠","♥","♦"
  • 斗地主时:三个玩家每人手里17张牌,剩余3张牌作为底牌
第一步:为了表示每一张牌有哪些属性,首先应该新建一个扑克牌的类
第二步:启动游戏时,就应该提前准备好54张牌
第三步:接着再完全洗牌、发牌、捋牌、看牌的业务逻辑

先来完成第一步,定义一个扑克类Card

public class Card {
    private String number;
    private String color;
    // 每张牌是存在大小的。
    private int size; // 0 1 2 ....

    public Card() {
    }

    public Card(String number, String color, int size) {
        this.number = number;
        this.color = color;
        this.size = size;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public int getSize() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    @Override
    public String toString() {
        return color + number ;
    }
}

再完成第二步,定义一个房间类,初始化房间时准备好54张牌

public class Room {
    // 必须有一副牌。
    private List<Card> allCards = new ArrayList<>();

    public Room(){
        // 1、做出54张牌,存入到集合allCards
        // a、点数:个数确定了,类型确定。
        String[] numbers = {"3","4","5","6","7","8","9","10","J","Q","K","A","2"};
        // b、花色:个数确定了,类型确定。
        String[] colors = {"♠", "♥", "♣", "♦"};
        int size = 0; // 表示每张牌的大小
        // c、遍历点数,再遍历花色,组织牌
        for (String number : numbers) {
            // number = "3"
            size++; // 1 2 ....
            for (String color : colors) {
                // 得到一张牌
                Card c = new Card(number, color, size);
                allCards.add(c); // 存入了牌
            }
        }
        // 单独存入小大王的。
        Card c1 = new Card("",  "

标签:Java,String,System,println,new,public,out
From: https://www.cnblogs.com/kk-koala/p/18328383

相关文章

  • java的几种算法结构
    顺序结构1.java的最基本的结构就是顺序结构除非特别指明,否则就按照顺序一句一句执行2.顺序结构是最简单的算法结构3.语句与语句之间.框与框之间是按从上到下的顺序进行的,他是由若干个依次执行的处理步骤组成的,它是任何算法的离不开的一种基本算法结构选择结构if单选择结构......
  • 在Windows使用Java编译工具
    文章目录创建java文件编译Java文件运行Java文件创建java文件桌面右键->新建->文本文档双击打开输入publicclassHello{publicstaticvoidmain(String[]args){ System.out.println("HelloWorld!"); }}将文件名改为hello后缀.java编译Java文......
  • JAVA 实现 - 哈希表
    哈希算法String.hashCodepublicstaticvoidmain(String[]args){Stringstr1="abc";Stringstr2="bca";inthash=0;for(inti=0;i<str2.length();i++){charc=str1.charAt(i);System.out.pr......
  • Java 多线程技术详解
    文章目录Java多线程技术详解目录引言多线程的概念为什么使用多线程?多线程的特征多线程的挑战多线程的实现方式3.1继承`Thread`类示例代码:3.2实现`Runnable`接口示例代码:3.3使用`Executor`框架示例代码:3.4使用`Callable`和`Future`示例代码:线程的生命......
  • Java----CAS算法与AtomicInteger源码解读
    CAS介绍:为了确保对数据操作的原子性,在java.util.concurrent.atomic下定义许多关于各种基本类型数据的提供原子操作的类。这里我们以AtomicInteger为例子。AtomicInteger的本质:自旋锁+CAS算法CAS的全称是:CompareAndSwap(比较再交换);是现代CPU广泛支持的一种对内存中的......
  • 简单聊聊JavaScript 中的原型链、null 和 undefined 的区别
    1.原型链个人观点:原型链和逻辑判断里三段论有些类似,一个大前提、一个小前提、一个结论。比如,动物会吃肉,狗是动物,所以狗会吃肉。这也是继承的思想原型和构造函数JavaScript是基于原型的面向对象编程语言,每个对象都有一个内部链接到另一个对象(即原型)。这个机制被称为原型链。原......
  • 这一文,关于 Java 泛型的点点滴滴 二 (extends、super、<?> 通配符、泛型与反射)
    本文是《这一文,关于Java泛型的点点滴滴》的第二篇,也是最后一篇。在上一篇文章中我们介绍了关于Java泛型的基础知识,而在本文中,我们将深入Java泛型,介绍了extends、super、<?>通配符,并在最后介绍了使用反射获取泛型信息。在阅读本文之前,请先阅读上一篇文章:这一文,关于Jav......
  • [java]小程序,用接口的方式计算两个数的加减乘除
           ......
  • Java 中的集合
    Author:ACatSmilingSince:2024-07-28概述在Java语言中,数组(Array)和集合都是对多个数据进行存储操作的结构,简称Java容器。此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储。数组在内存存储方面的特点:数组一旦初始化以后,其长度就确定了。数组一旦定义好,其元素......
  • Java常见面试题
    1.JDK和JRE有什么区别?JDK:JavaDevelopmentKit的简称,Java开发工具包,提供了Java的开发环境和运行环境。JRE:JavaRuntimeEnvironment的简称,Java运行环境,为Java的运行提供了所需环境。具体来说JDK其实包含了JRE,同时还包含了编译Java源码的编译器Javac,还包含......