面向对象

类与对象

类与对象引出

  1. 单独的定义变量解决
  2. 使用数组解决
  • 现有技术解决的缺点分析
  1. 不利于数据的管理
  2. 效率低

java设计者引入类与对象(OQP),根本原因就是现有的技术,不能完美的解决新的新的需求.

类与对象概述

类和对象的区别和联系

通过上面的案例和讲解我们可以看出;

  1. 类是抽象的,概念的,代表一类事物,比如人类,猫类…即它是数据类型.
  2. 对象是具体的,实际的,代表一个具体事物,即是实例.
  3. 类是对象的模板,对象是类的一个个体,对应一个实例

属性/成员变量

  • 基本介绍
  1. 从概念或叫法上看:成员变量=属性= field(即成员变量是用来表示属性的,授课中,统一叫属性)

  2. 属性是类的一个组成部分,一般是基本数据类型,也可是引用类型(对象,数组)。
    比如我们前面定义猫类的int age就是属性

  • 注意事项和细节说明
  1. 属性的定义语法同变量,示例:访问修饰符属性类型属性名;这里老师简单的介绍访问修饰符:控制属性的访问范围
    有四种访问修饰符public, proctected,默认, private
  2. 属性的定义类型可以为任意类型,包含基本类型或引用类型
  3. 属性如果不赋值,有默认值,规则和数组一致。具体说:
    int 0,short 0.byte , long 0, float 0.0,double 0.0,char \u0000,boolean false,String null
  • 类和对象的内存分配机制

Java内存的结构分析

  1. 栈:一般存放基本数据类型(局部变量)
  2. 堆:存放对象(Cat cat,数组等)
  3. 方法区:常量池(常量,比如字符串),类加载信息

Java创建对象的流程简单分析
Person p = new Person();
p.name = “jack” ;
p.age = 10

  1. 先加载Person类信息(属性和方法信息,只会加载一次)
  2. 在堆中分配空间,进行默认初始化(看规则)
  3. 把地址赋给p,p就指向对象
  4. 进行指定初始化,比如p.name =” jack “p.age = 10

成员方法

基本介绍
在某些情况下,我们要需要定义成员方法(简称方法)。比如人类:除了有一些属性外(年龄,姓名..),我们人类还有一些行为比如:可以说话、跑步..,通过学习,还可以做算术题。这时就要用成员方法才能完成。现在要求对Person类完善。

成员方法快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Method01 {
//编写一个main方法
public static void main(string[] args) {
//方法使用
//1.方法写好后,如果不去调用(使用),不会输出
//2.先创建对象,然后调用方法即可
Person p1 = new Person();
p1.speak();//调用方法
}
}

class Person {
String name;
int age;
//方法(成员方法)
//添加speak成员方法,输出“我是一个好人”//老韩解读
//1. public表示方法是公开
//2. void :表示方法没有返回值
//3. speak() : speak是方法名,)形参列表
//4.{ }方法体,可以写我们要执行的代码
//5. System.out.println("我是一个好人");表示我们的方法就是输出一句话
public void speak() {
System.out.println("我是一个好人");
}
}

方法调用机制

成员方法的定义

public 返回数据类型 方法名 (形参列表..){
//方法体语句;
return返回值;
}

  1. 参数列表:表示成员方法输入cal(int n)
  2. 数据类型(返回类型):表示成员方法输出, void表示没有返回值
  3. 方法主体:表示为了实现某一功能代码块
  4. return语句不是必须的。

总结一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.什么是方法?
答:方法是一种语法结构,它可以把一段代码封装成一个功能,以便重复调用
2.方法的完整格式是什么样的?
//格式如下:
修饰符 返回值类型 方法名( 形参列表 ){
方法体代码(需要执行的功能代码)
return 返回值;
}
3.方法要执行必须怎么办?
必须调用才执行;
//调用格式:
方法名(...);

4.使用方法有什么好处?
答:提高代码的复用性,提高开发效率,使程序逻辑更清晰。

注意事项和使用细节

  • 修饰符(作用是控制方法使用的范围)

  • 返回类型

  1. 一个方法最多有一个返回值
  2. 返回类型可以为任意类型,包含基本类型或引用类型(数组,对象)
  3. 如果方法要求有返回数据类型,则方法体中最后的执行语句必须为return值;而且要求返回值类型必须和return的值类型一致或兼容
  4. 如果方法是void,则方法体中可以没有return语句,或者只写 return ;
  • 方法名
    遵循驼峰命名法,最好见名知义,表达出该功能的意思即可,比如得到两个数的和getSum,开发中按照规范

  • 形参列表

  1. 一个方法可以有0个参数,也可以有多个参数,中间用逗号隔开,比如getSum(int n1,int n2)
  2. 参数类型可以为任意类型,包含基本类型或引用类型,比如 printArr(int[][] map)
  3. 调用带参数的方法时,一定对应着参数列表传入相同类型或兼容类型的参数!
  4. 方法定义时的参数称为形式参数,简称形参;方法调用时的传入参数称为实际参数,简称实参,实参和形参的类型要一致或兼容、个数、顺序必须一致!
  • 方法体
    里面写完成功能的具体的语句,可以为输入、输出、变量、运算、分支、循环、方法调用,但里面不能再定义方法!即:方法不能嵌套定义。

成员方法与传参机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) i{
int a = 10;
int b = 20;
//创建AA对象名字obj
AA obj = new AA();
obj.swap(a, b);//调用swap
System.out.println( "a=" + a + " b=" + b);
}
class AA {
public void swap(int a,int b){
System.out.println("\na和b交换前的值\na=" + a + "\tb=" + b);//a=10 b=20//完成了a和b的交换
int tmp = a;
a = b;
b = tmp;
System.out.println("\na和b交换后的值\na=" + a + "\tb=" + b); //a=20 b=10
}
}

先记住一个结论:Java的参数传递机制都是:值传递,传递的是实参存储的值的副本。

总结一下:

1
2
3
4
1.基本类型和引用类型的参数在传递的时候有什么不同?
= 都是值传递
- 基本类型的参数传递存储的数据值。
- 引用类型的参数传递存储的地址值。

方法的执行原理

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

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

方法进栈和弹栈的过程,就类似于手枪子弹夹,上子弹和击发子弹的过程。最后上的一颗子弹是,第一个打出来的;第一颗上的子弹,是最后一个打出来的。

假设在main方法中依次调用A方法、B方法、C方法,在内存中的执行流程如下:

  • 每次调用方法,方法都会从栈顶压栈执行没执行
  • 每个方法执行完后,会从栈顶弹栈出去

方法使用常见的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 1. 方法在内种没有先后顺序,但是不能把一个方法定义在另一个方法中。

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

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

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

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

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

方法参数传递案例

方法参数传递案例1

1
2
3
4
5
6
7
8
9
10
11
12
需求:输出一个int类型的数组内容,要求输出格式为:[11, 22, 33, 44, 55]。

分析:
1.方法是否需要接收数据进行处理?
方法要打印int类型数组中的元素,打印哪一个数组需求并不明确;
所以可以把int数组写成参数,让调用者指定

2.方法是否需要返回数据?
方法最终的目的知识打印数组中的元素。
不需要给调用者返回什么,所以不需要返回值,返回值类型写void

3.方法内部的业务:遍历数组,并输出相应的内容

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MethodTest3 {
public static void main(String[] args) {
// 目标:完成打印int类型的数组内容。
int[] arr = {10, 30, 50, 70};
printArray(arr);

int[] arr2 = null;
printArray(arr2);

int[] arr3 = {};
printArray(arr3);
}

/*
参数:int[] arr表示要被打印元素的数组,需要调用者传递
*/
public static void printArray(int[] arr){
if(arr == null){
System.out.println(arr); // null
return; // 跳出当前方法
}

System.out.print("[");
// 直接遍历接到的数组元素
for (int i = 0; i < arr.length; i++) {
if(i == arr.length - 1){
System.out.print(arr[i]);
}else {
System.out.print(arr[i] + ", ");
}
}
System.out.println("]");
}
}

方法参数传递案例2

1
2
3
4
5
6
7
8
9
10
11
12
需求:比较两个int类型的数组是否一样,返回true或者false

分析:
1.方法是否需要接收数据进行处理?
因为,方法中需要两个int数组比较,但是需求并不明确是哪两个数组;
所以,需要接收两个int类型的数组,形参声明为:int[] arr1,int[] arr2

2.方法是否需要返回数据?
因为,方法最终的结果需要true或者false;
所以,返回值类型是boolean

3. 方法内部的业务:判断两个数组内容是否一样。

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class MethodTest4 {
public static void main(String[] args) {
// 目标:完成判断两个int类型的数组是否一样。
int[] arr1 = {10, 20, 30};
int[] arr2 = {10, 20, 30};
System.out.println(equals(arr1, arr2));
}

/*
参数:
int[] arr1, 参与比较的第一个int数组
int[] arr2 参与比较的第二个int数组
返回值:
返回比较的结果true或者false
*/
public static boolean equals(int[] arr1, int[] arr2){
// 1、判断arr1和arr2是否都是null.
if(arr1 == null && arr2 == null){
return true; // 相等的
}

// 2、判断arr1是null,或者arr2是null.
if(arr1 == null || arr2 == null) {
return false; // 不相等
}

// 3、判断2个数组的长度是否一样,如果长度不一样,直接返回false.
if(arr1.length != arr2.length){
return false; // 不相等
}

// 4、两个数组的长度是一样的,接着比较它们的内容是否一样。
// arr1 = [10, 20, 30]
// arr2 = [10, 20, 30]
for (int i = 0; i < arr1.length; i++) {
// 判断当前位置2个数组的元素是否不一样,不一样直接返回false
if(arr1[i] != arr2[i]){
return false; // 不相等的
}
}
return true; // 两个数组是一样的。
}
}

方法递归

递归能解决什么问题?

  1. 各种数学问题如:8皇后问题,汉诺塔,阶乘问题,迷宫问题,球和篮子的问题
  2. 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等
  3. 将用栈解决的问题–>递归代码比较简洁

递归执行机制

递归举例
列举两个小案例,来帮助大家理解递归调用机制

  1. 打印问题
  2. 阶乘问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//输出什么? 
public void test(int n){
if (n > 2){
test(n - 1);
}
System.out.println("n=" + n);
}

//阶乘
public int factorial(int n){
if (n == 1) {
return 1;
}else {
return factorial(n - 1) * n;
}
}

递归重要规则

  1. 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
  2. 方法的局部变量是独立的,不会相互影响,比如n变量
  3. 如果方法中使用的是引用类型变量(比如数组,对象),就会共享该引用类型的数据.
  4. 递归必须向退出递归的条件逼近,否则就是无限递归,
  5. 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就
    将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。

重载

  • 方法重载
  • 一个类中,出现多个方法的名称相同,但是它们的形参列表是不同的,那么这些方法就称为方法重载了。
  • 方法重载的注意事项
  • 一个类中,只要一些方法的名称相同、形参列表不同,那么它们就是方法重载了,其它的都不管(如∶修饰符返回值类型是否一样都无所谓)。

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

  • 重载的好处
  1. 减轻了起名的麻烦
  2. 减轻了记名的麻烦

注意事项和使用细节

  1. 方法名∶必须相同
  2. 形参列表:必须不同(形参类型或个数或顺序,至少有一样不同,参数名无要求)
  3. 返回类型:无要求

课堂练习题

  1. 判断题:
    与void show(int a,char b,double c)构成重载的有:[b c d e ]
    a) void show(int x,char y,double z) //不是
    b) int show(int a,double c,char b) //是
    c) void show(int a,double c.char b){} //是
    d) boolean show(int c.char b) //是
    e) void show(double c) //是
    f) double show(int x,char y,double z) //不是
    g) void shows(){} //不是

可变参数

基本概念
java允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法。就可以通过可变参数实现

基本语法
访问修饰符返回类型方法名(数据类型…形参名){}

注意事项和使用细节

  1. 可变参数的实参可以为0个或任意多个。
  2. 可变参数的实参可以为数组。
  3. 可变参数的本质就是数组.
  4. 可变参数可以和普通类型的参数一起放在形参列表,但必须保业可艾乡多以取口
  5. 一个形参列表中只能出现一个可变参数
    public int sum(String str,int… a, String… s)l/

Java编程案例

  • 编程思维、编程能力不是一朝一夕形成的,需要大量思考,练习和时间的沉淀。
  • 具体措施:前期,建议先模仿;后期,自然就能创新了;

案例一:买飞机票

1
2
3
4
5
6
7
8
9
10
1.首先,考虑方法是否需要接收数据处理?
阅读需求我们会发现,不同月份、不同原价、不同舱位类型优惠方案都不一样;
所以,可以将原价、月份、舱位类型写成参数

2.接着,考虑方法是否有返回值?
阅读需求我们发现,最终结果是求当前用户的优惠票价
所以,可以将优惠票价作为方法的返回值。

3.最后,再考虑方法内部的业务逻辑
先使用if判断月份是旺季还是淡季,然后使用switch分支判断是头等舱还是经济舱,计算 票价

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Test1 {
public static void main(String[] args) {
// 目标:完成买飞机票的案例。
double price = calculate(1000, 11, "头等舱");
System.out.println("优惠价是:" + price);
}

public static double calculate(double price,int month,String type){
// 1、判断当前月份是淡季还是旺季
if(month >= 5 && month <= 10) {
// 旺季
// 2、判断仓位类型。
switch (type){
case "头等舱":
price *= 0.9; // price = price * 0.9;
break;
case "经济舱":
price *= 0.85;
break;
}
}else {
// 淡季
switch (type){
case "头等舱":
price *= 0.7; // price = price * 0.7;
break;
case "经济舱":
price *= 0.65;
break;
}
}
return price;
}
}

案例二:开发验证码

分析一下,需求是要我们开发一个程序,生成指定位数的验证码。考虑到实际工作中生成验证码的功能很多地方都会用到,为了提高代码的复用性,我们还是把生成验证码的功能写成方法比较好。

1
2
3
4
5
6
7
8
9
10
11
12
1.首先,考虑方法是否需要接收数据处理?
要求生成指定位数的验证码,到底多少位呢?让调用者传递即可
所以,需要一个参数,用来表示验证码的位数

2.接着,考虑方法是否需要有返回值?
该方法的结果,就是为了得到验证码
所以,返回值就是验证码;

3.最后,再考虑方法内部的业务逻辑
1)先按照方法接收的验证码位数n,循环n次
2)每次循环,产生一个字符,可以是数字字符、或者大小写字母字符
3)定义一个String类型的变量用于记住产生的每位随机字符

按照思路,编写代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Test2 {
public static void main(String[] args) {
// 目标:完成生成随机验证码。
System.out.println(createCode(8));
}

public static String createCode(int n){
//1)先按照方法接收的验证码位数n,循环n次
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;
}
}

案例三:评委打分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.首先,考虑方法是否需要接收数据来处理?
需求中说,有多个评委的打分,但是到底多少个评委呢? 可以由调用者传递
所以,我们可以把评委的个数写成参数;

2.接着,考虑方法是否需要有返回值?
需求中,想要的最终结果是平均分
所以,返回值就是平均分;

3.最后,再考虑方法内部的业务逻辑
1)假设评委的个位为n个,那么就需要n个评委的分数,首先可以新建一个长度为n的数组, 用来存储每一个评委的分数

2)循环n次,使用Scanner键盘录入n个1~100范围内的整数,并把整数存储到数组中

3)求数组中元素的总和、最大值、最小值

4)最后再计算平均值; 平均值 = (和-最大值-最小值)/(数组.length-2);

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Test3 {
public static void main(String[] args) {
// 目标:完成评委打分案例。
System.out.println("当前选手得分是:" + getAverageScore(6));
}

public static double getAverageScore(int n){
// 1、定义一个动态初始化的数组,负责后期存入评委的打分
int[] scores = new int[n]; // 6
// scores = [0, 0, 0, 0, 0, 0]

// 2、遍历数组的每个位置,依次录入评委的分数
Scanner sc = new Scanner(System.in);
for (int i = 0; i < scores.length; i++) {
// i = 0 1 2 3 4 5
System.out.println("请您录入第"+ (i + 1) +"个评委的分数:");
int score = sc.nextInt();
scores[i] = score;
}

// 3、从数组中计算出总分,找出最高分,最低分。
int sum = 0; // 求总分用的变量
int max = scores[0]; // 求最大值的
int min = scores[0]; // 求最小值的。

// 遍历数组找出这些数据的。
for (int i = 0; i < scores.length; i++) {
// i = 0 1 2 3 4 5
int score = scores[i];
// 求和
sum += score;
// 求最大值
if(score > max){
max = score;
}
// 求最小值
if(score < min){
min = score;
}
}
// 4、计算出平均分并返回
return 1.0 * (sum - min - max) / (number - 2);
}
}

案例四:数字加密

仔细阅读需求后发现,简答来说该需求要做的事情,就是把一个4位数的整数,经过一系列的加密运算(至于怎么运算,待会再详细分析),得到一个新的整数。

我们还是把这个需求用方法来实现,按照下面的思维模式进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
1.首先,考虑方法是否需要接收数据处理?
需要一个4位数,至于是哪一个数,让方法的调用者传递。
所以,方法的参数,就是这个需要加密的四位数

2.接着,考虑方法是否需要有返回值?
方法最终的结果是一个加密后的数据
所以,返回值就表示为加密后的数据。

3.最后,再考虑方法内部的业务逻辑,这里的业务逻辑就是那一系列的加密运算
1)先要把4位数整数拆分为,4个数字,用一个数组保存起来
2)再将数组中的每一个元素加5,再对10取余
3)最后将数组中的元素反转,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class Test4 {
public static void main(String[] args) {
// 目标:完成数字加密程序的开发。
System.out.println("加密后的结果是:" + encrypt(8346));
}

public static String encrypt(int number){
// number = 1983
// 1、把这个密码拆分成一个一个的数字,才可以对其进行加密啊。
int[] numbers = split(number);
// numbers = [1, 9, 8, 3]

// 2、遍历这个数组中的每个数字,对其进行加密处理。
for (int i = 0; i < numbers.length; i++) {
// i = 0 1 2 3
numbers[i] = (numbers[i] + 5) % 10;
}
// numbers = [6, 4, 3, 8]

// 3、对数组反转,把对数组进行反转的操作交给一个独立的方法来完成
reverse(numbers);
// numbers = [8, 3, 4, 6]

// 4、把这些加密的数字拼接起来做为加密后的结果返回即可。
String data = "";
for (int i = 0; i < numbers.length; i++) {
data += numbers[i];
}
return data;
}

public static void reverse(int[] numbers) {
// 反转数组的
// numbers = [6, 4, 3, 8]
// i j
for (int i = 0, j = numbers.length - 1; i < j; i++,j--) {
// 交换i和j位置处的值。
// 1、把后一个位置处的值交给一个临时变量先存起来
int temp = numbers[j];
// 2、把前一个位置处的值赋值给后一个位置处
numbers[j] = numbers[i];
// 3、把后一个位置处原来的值(由临时变量记住着)赋值给前一个位置
numbers[i] = temp;
}
}

public static int[] split(int number) {
// number = 1983
int[] numbers = new int[4];
numbers[0] = number / 1000;
numbers[1] = (number / 100) % 10;
numbers[2] = (number / 10) % 10;
numbers[3] = number % 10;
return numbers;
}
}

案例五:数组拷贝

仔细阅读需求发现,想要实现的效果就是:给定一个数组,然后经过我们编写的程序,得到一个和原数组一模一样的数组。

1
2
3
4
5
6
7
8
9
10
11
12
1.首先,考虑方法是否需要接收数据处理?
该方法的目的是拷贝数组,拷贝哪一个数组呢? 需要调用者传递
所以,参数应该是一个数组

2.接着,考虑方法是否需要有返回值?
该方法最终想要得到一个新数组
所以,返回值是拷贝得到的新数组

3.最后,考虑方法内部的业务逻辑?
1)创建一个新的数组,新数组的长度和元素数组一样
2)遍历原数组,将原数组中的元素赋值给新数组
3)最终将新数组返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Test5 {
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("]");
}
}

案例六:抢红包

我们还是把这个案例用一个方法来编写,同样按照下面的模式来分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1.首先,考虑方法是否需要接收数据处理?
需要接收5个红包,至于是哪5个红包,可以有调用者传递;把5个红包的数值,用数组来存 储。
所以,参数就是一个数组

2.接着,考虑方法是否需要有返回值?
按照需求的效果,抢完红包就直接打印了,不需要返回值

3.最后,考虑方法内部的业务逻辑是怎么的?
思考:红包实际上是数组中的元素,抢红包实际上随机获取数组中的元素;而且一个红包只能抢一次,怎么做呢?我们可以把数组中获取到元素的位置,置为0,下次再或者这个位置的元素一判断为0,再重新获取新的元素,依次内推,直到把数组中所有的元素都获取完。

我们我们把抽红包的思路再整理一下:
1)首先,写一个循环,循环次数为数组的长度
2)每次循环,键盘录入,提示"用户录入任意键抽奖:"
3)随机从数组中产生一个索引,获取索引位置的元素,这个元素就表示抽的红包
如果值不为0,则打印如:"恭喜您,您抽中了520元",把这个位置元素置为0
如果值为0,则说明这个红包被抽过,重新循环到第2步,重新抽奖
【注意:如果当前这一次没有抽中,这一次抽奖机会被浪费掉了,我们可以把控 制循环的次数自减一下】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Test6 {
public static void main(String[] args) {
int[] moneys = {100,999,50,520,1314};
start(moneys);
}
//开始抽奖
public static void start(int[] moneys){
//1)首先,写一个循环,循环次数为数组的长度
for (int i = 0; i < moneys.length; i++) {
//2)每次循环,键盘录入,提示"用户录入任意键抽奖:"
while (true){
Scanner sc = new Scanner(System.in);
System.out.print("用户录入任意键抽奖:");
String msg = sc.next();
//3)随机从数组中产生一个索引,获取索引位置的元素,这个元素就表示抽的红包
Random r = new Random();
int index = r.nextInt(moneys.length);
int money = moneys[index];
if(money!=0){
//如果值不为0,则打印如:"恭喜您,您抽中了520元"
System.out.println("恭喜您,您抽中了"+money+"元");
moneys[index] = 0;
break;
}else {
//如果值为0,则说明这个红包被抽过,重新循环到第2步,重新抽奖
//此时这一次抽奖机会被浪费掉了,可以把控制循环的次数自减一下
i--;
}
}
}
}
}

案例七:找素数

首先我们得统一认识一下什么是素数:只能被1和本身整除的数是素数,比如:3、7是素数,9,21不是素数(因为9可以被3整除,21可以被3和7整除)

再思考题目需求该怎么做?打印输出101~200之间的素数,并求有多少个?,我们也是把这个需求写成一个方法,还是按照三个步骤分析方法如何编写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1.首先,考虑方法是否需要接收数据处理?
该方法是求一个范围内的素数,一个范围需要两个数据来确定,比如:101~200
所以,方法需要两个参数来接收范围的开始值start,和范围的结束值end

2.接着,考虑方法是否需要返回值?
该方法需要求一个范围内的素数的个数
所以,返回值就是素数的个数

3.最后,考虑方法内部的业务逻辑
思考:怎么判断一个数是素数呢?要仅仅抓住,素数的要求:“只能被1和本身整除的数是素数”。我们可以从反向思考,如果这个数只要能被除了1和本身以外的数整除,那么这个数就不是素数。
//比如1:判断9是否为素数
int num = 9;
boolean flag = true; //规定flag等于true表示num是素数;否则表示num不是素数
//如果这个数num只要能被除了1和本身以外的数整除,那么这个数就不是素数。
for(int j=2; j<9-1; j++){
//当j=3时,num%j == 9%3 == 0;
if(num%j==0){
//说明num=9; 表示一个素数。把flag改为false;
flag = false;
}
}

把上面的代码循环执行,每次循环然后把num换成start~end之间的整数即可。

编写代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Test7 {
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;
}
}

面向对象基础

面向对象

总结一些:所谓编写对象编程,就是把要处理的数据交给对象,让对象来处理。

面向对象编程有什么好处?

Java的祖师爷,詹姆斯高斯林认为,在这个世界中 万物皆对象!任何一个对象都可以包含一些数据,数据属于哪个对象,就由哪个对象来处理。

所以面向对象编程的好处,用一句话总结就是:面向对象的开发更符合人类的思维习惯,让编程变得更加简单、更加直观。

程序中对象到底是个啥?

对象实质上是一种特殊的数据结构。这种结构怎么理解呢?

你可以把对象理解成一张表格,表当中记录的数据,就是对象拥有的数据。

一句话总结,对象其实就是一张数据表,表当中记录什么数据,对象就处理什么数据。

对象是怎么出来的?

用什么来设计这张表呢?就是类(class),类可以理解成对象的设计图,或者对象的模板。

我们需要按照对象的设计图创造一个对象。设计图中规定有哪些数据,对象中就只能有哪些数据。

一句话总结:对象可以理解成一张数据表,而数据表中可以有哪些数据,是有类来设计的。

执行原理和注意事项

类与对象的一些注意事项。

  1. 类名建议用英文单词,首字母大写,满足驼峰模式,且要有意义,比如: Student.Car…
  2. 类中定义的变量也称为成员变量(对象的属性),类中定义的方法也称为成员方法(对象的行为).
  3. 成员变量本身存在默认值,同学们在定义成员变量时一般来说不需要赋初始值(没有意义)。
  4. 一个代码文件中,可以写多个class类,但只能一个用public修饰,
    且public修饰的类名必须成为代码文件名。
  5. 对象与对象之间的数据不会相互影响,但多个变量指向同一个对象时就会相互影响了。
  6. 如果某个对象没有一个变量引用它,则该对象无法被操作了,该对象会成为所谓的垃圾对象。

作用域

  • 基本使用
    面向对象中,变量作用域是非常重要知识点,相对来说不是特别好理解,请大家注意听,认真思考,要求深刻掌握变量作用域。
  1. 在java编程中,主要的变量就是属性(成员变量)和局部变量。
  2. 我们说的局部变量一般是指在成员方法中定义的变量。
    举例Cat类: cry
  3. java中作用域的分类
    全局变量:也就是属性,作用域为整个类体
    Cat类: cry eat等方法使用属性【举例】
    局部变量:也就是除了属性之外的其他变量,作用域为定义它的代码块中!
  4. 全局变量可以不赋值,直接使用,因为有默认值,局部变量必须赋值后,才能使用,因为没有默认值。

this

关于this关键字我们就学习到这里,重点记住这句话:哪一个对象调用方法方法中的this就是哪一个对象

  1. this关键字是什么?
  • this就是一个变量,可以用在方法中,用来拿到当前对象;哪个对象调用方法,this就指向哪个对象,也就是拿到哪个对象。
  1. this关键字在实际开发中常用来干啥?
  • 用来解决对象的成员变量与方法内部变量的名称一样时,导致访问冲突问题的。

构造器

有参构造、无参构造

  1. 构造器长什么样子?
    1
    2
    3
    4
    5
    6
    public class Student {
    //构造器
    public Student() {
    ...
    }
    }
1
2
3
4
5
6
7
8
9
10
11
12
1.什么是构造器?
答:构造器其实是一种特殊的方法,但是这个方法没有返回值类型,方法名必须和类名相同。

2.构造器什么时候执行?
答:new 对象就是在执行构造方法;

3.构造方法的应用场景是什么?
答:在创建对象时,可以用构造方法给成员变量赋值

4.构造方法有哪些注意事项?
1)在设计一个类时,如果不写构造器,Java会自动生成一个无参数构造器。
2)一定定义了有参数构造器,Java就不再提供空参数构造器,此时建议自己加一个无参数构造器。

封装

  1. 什么是封装?
  • 就是用类设计对象处理某一个事物的数据时,应该把要处理的数据,以及处理这些数据的方法,
  • 设计到一个对象中去。
    面向对象的三大特征:封装、继承、多态。
  1. 封装的设计规范是什么样的?

封装的设计规范用8个字总结,就是:合理隐藏、合理暴露

  1. 代码层面如何控对象的成员公开或隐藏?
  • 公开成员,可以使用public(公开)进行修饰。
  • 隐藏成员,使用private(私有,隐藏)进行修饰。

实体类

1
2
3
4
5
6
7
1.JavaBean实体类是什么?有啥特点
JavaBean实体类,是一种特殊的;它需要私有化成员变量,有空参数构造方法、同时提供getXxx和setXxx方法;

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

2.JavaBean的应用场景?
JavaBean实体类,只负责封装数据,而把数据处理的操作放在其他类中,以实现数据和数据处理相分离。

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


到这里,我们关于面向对象的基础知识就学习完了。面向对象的核心点就是封装,将数据和数据的处理方式,都封装到对象中; 至于对象要封装哪些数据?对数据进行怎样的处理? 需要通过类来设计。

API

常用API(全称是Application Program Interface 应用程序接口),说人话就是:别人写好的一些程序,给咱们程序员直接拿去调用。

  • 比如String类,表示字符串,提供的方法全都是对字符串操作的。
  • 比如ArrayList类,表示一个容器,提供的方法都是对容器中的数据进行操作的。

  • 什么是包?
  • 包是用来分门别类的管理各种不同程序的,类似于文件夹,建包有利于程序的管理和维护。
  • 建包的语法格式:
    1
    2
    3
    package com.itheima.javabean;
    public class Student {
    }

在自己程序中调用其他包下的程序的注意事项

  1. 如果当前程序中,要调用自己所在包下的其他程序,可以直接调用。(同一个包下的类,互相可以直接调用)

  2. 如果当前程序中,要调用其他包下的程序,则必须在当前程序中导包,才可以访问!导包格式: import包名.类名;

  3. 如果当前程序中,要调用Java提供的程序,也需要先导包才可以使用;但是Java.lang包下的程序是不需要我们导包的,可以直接使用。

  4. 如果当前程序中,要调用多个不同包下的程序,而这些程序名正好一样,此时默认只能导入一个程序,另一个程序必须带包名访问。

String

  • String是什么,可以做什么?
    代表字符串,可以用来创建对象封装字符串数据,并对其进行处理。

  • java.lang.String 代表’字符串封装字符串数据分理字符串的方法
    1、创建对象
    2、封装字符串数据
    3、调string的方法

String创建对象封装字符串数据的方式

  • 方式一: Java程序中的所有字符串文字(例如“abc”)都为此类的对象。

    1
    2
    String name ="小黑";
    String schoolName="黑马程序员";
  • 方式二:调用String类的构造器初始化字符串对象。
    构造器 说明
    public string() 创建一个空白字符串对象,不含有任何内容
    public string(string original) 根据传入的字符串内容,来创建字符串对象
    public string(char[] chars) 根据字符数组的内容,来创建字符串对象
    public string(byte[] bytes) 根据字节数组的内容,来创建字符串对象

  • String提供的操作字符串数据的常用方法

String的注意事项

  • 第一点
  1. String对象的内容不可改变,被称为不可
    变字符串对象。
  • 第二点
  1. 只要是以“..”方式写出的字符串对象,会存储到字符串常量池,且相同内容的字符串只存储一份;
  2. 但通过new方式创建字符串对象,每new一次都会产生一个新的对象放在堆内存中。

String的应用案例

  1. 字符串的比较使用==比较好吗?为什么?什么时候使用==?

不好,对于字符串对象的比较,==比较的是地址,容易出业务bug
基本数据类型的变量或者值应该使用==比较。

  1. 开发中比较字符串推荐使用什么方式比较?
    使用String提供的equals方法,它只关心字符串内容一样就返回true.

方法名
public boolean equals (object anobject)
public boolean equalsIgnoreCase (string anotherString)

ArrayList

  1. 集合是什么,有什么特点?
  • 一种容器,用来存储数据的
  • 集合的大小可变。
  1. ArrayList是什么?怎么使用?
  • 是集合中最常用的一种,ArrayList是泛型类,可以约束存储的数据类型。

  • 创建对象,调用无参数构造器初始化对象:public ArrayList();

  • 调用相应的增删改查数据的方法

  1. 从集合中遍历元素,并筛选出元素删除它,应该如何操作才能不出bug?
  • 方式一:每次删除一个数据后,索引-1。
  • 方式二:从集合后面遍历然后删除,可以避免漏掉元素。

ArrayList应用案例

我们往集合存储的元素是String类型的元素,实际上在工作中我们经常往集合中自定义存储对象。

接下来我们做个案例,用来往集合中存储自定义的对象,先阅读下面的案例需求:

分析需求发现:

  1. 在外卖系统中,每一份菜都包含,菜品的名称、菜品的原价、菜品的优惠价、菜品的其他信息。那我们就可以定义一个菜品类(Food类),用来描述每一个菜品对象要封装那些数据。
  2. 接着再写一个菜品管理类(FoodManager类),提供展示操作界面、上架菜品、浏览菜品的功能。
  • 首先我们先定义一个菜品类(Food类),用来描述每一个菜品对象要封装那些数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Food {
private String name; //菜品名称
private double originalPrice; //菜品原价
private double specialPrice; //菜品优惠价
private String info; //菜品其他信息

public Food() {
}

public Food(String name, double originalPrice, double specialPrice, String info) {
this.name = name;
this.originalPrice = originalPrice;
this.specialPrice = specialPrice;
this.info = info;
}

public String getName() {
return name;
}

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

public double getOriginalPrice() {
return originalPrice;
}

public void setOriginalPrice(double originalPrice) {
this.originalPrice = originalPrice;
}

public double getSpecialPrice() {
return specialPrice;
}

public void setSpecialPrice(double specialPrice) {
this.specialPrice = specialPrice;
}

public String getInfo() {
return info;
}

public void setInfo(String info) {
this.info = info;
}
}
  • 接下来写一个菜品管理类,提供上架菜品的功能、浏览菜品的功能、展示操作界面的功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class FoodManager{
//为了存储多个菜品,预先创建一个ArrayList集合;
//上架菜品时,其实就是往集合中添加菜品对象
//浏览菜品时,其实就是遍历集合中的菜品对象,并打印菜品对象的属性信息。
private ArrayList<Food> foods = new ArrayList<>();
//为了在下面的多个方法中,能够使用键盘录入,提前把Scanner对象创建好;
private Scanner sc = new Scanner(System.in);

/**
1、商家上架菜品
*/
public void add(){
System.out.println("===菜品上架==");
// 2、提前创建一个菜品对象,用于封装用户上架的菜品信息
Food food = new Food();
System.out.println("请您输入上架菜品的名称:");
String name = sc.next();
food.setName(name);

System.out.println("请您输入上架菜品的原价:");
double originalPrice = sc.nextDouble();
food.setOriginalPrice(originalPrice);

System.out.println("请您输入上架菜品的优惠价:");
double specialPrice = sc.nextDouble();
food.setSpecialPrice(specialPrice);

System.out.println("请您输入上架菜品的其他信息:");
String info = sc.next();
food.setInfo(info);

// 3、把菜品对象添加到集合容器中去
foods.add(food);
System.out.println("恭喜您,上架成功~~~");
}

/**
2、菜品;浏览功能
*/
public void printAllFoods(){
System.out.println("==当前菜品信息如下:==");
for (int i = 0; i < foods.size(); i++) {
Food food = foods.get(i);
System.out.println("菜品名称:" + food.getName());
System.out.println("菜品原价:" + food.getOriginalPrice());
System.out.println("菜品优惠价:" + food.getSpecialPrice());
System.out.println("其他描述信息:" + food.getInfo());
System.out.println("------------------------");
}
}
/**
3、专门负责展示系统界面的
*/
public void start(){
while (true) {
System.out.println("====欢迎进入商家后台管理系统=====");
System.out.println("1、上架菜品(add)");
System.out.println("2、浏览菜品(query)");
System.out.println("3、退出系统(exit)");
System.out.println("请您选择操作命令:");
String command = sc.next();
switch (command) {
case "add":
add();
break;
case "query":
printAllFoods();
break;
case "exit":
return; // 结束当前方法!
default:
System.out.println("您输入的操作命令有误~~");
}
}
}
}
  • 最后在写一个测试类Test,在测试类中进行测试。其实测试类,只起到一个启动程序的作用。
1
2
3
4
5
6
public class Test {
public static void main(String[] args) {
FoodManager manager = new FoodManager();
manager.start();
}
}

运行结果如下:需要用户输入add、query或者exit,选择进入不同的功能。

能够把这个案例写出来,说明对面向对象的思维封装数据,以及使用ArrayList容器存储数据,并对数据进行处理这方面的知识已经运用的很熟悉了。

ATM案例

一、ATM项目介绍

ATM系统功能介绍

大家都应该去过银行的ATM机上取过钱,每次取钱的时候,首先需要用户把卡插入机器,然后机器会自动读取你的卡号,由用户输入密码,如果密码校验通过,就会进入ATM机的主操作界面:有查询、取款、存款、转账等业务功能,用户选择哪个功能就执行对应预先设定好的程序。

二、项目架构搭建、欢迎界面设计

接下来,我们带着大家开始开发这个ATM系统。首先我们来完成项目的架构搭建、和欢迎界面的设计。

首先我们来分析一下,开发这个ATM系统的流程:

  • 由于每一个账户都包含一些个人信息,比如:卡号、姓名、性别、密码、余额、每次取现额度等等。所以,首先可以设计一个Account类,用来描述账户对象需要封装那些数据。

  • 紧接着,定义一个ATM类,用来表示ATM系统,负责提供所有的业务需求。

    比如:展示ATM系统的欢迎页面、开户、登录、转账等功能。

  • 最后,定义一个测试类Test,负责启动我们开发好的ATM系统,进行测试。

第一步:先来完成Account类的编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//首先可以设计一个Account类,来描述账户对象需要封装哪些数据。
public class Account {
private String cardId; //卡号
private String userName; //用户名
private char sex; //性别
private String passWord;//密码
private double money; //余额
private double limit; // 限额

public String getCardId() {
return cardId;
}

public void setCardId(String cardId) {
this.cardId = cardId;
}

public String getUserName() {
return userName + ( sex == '男' ? "先生" : "女士");
}

public void setUserName(String userName) {
this.userName = userName;
}

public char getSex() {
return sex;
}

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

public String getPassWord() {
return passWord;
}

public void setPassWord(String passWord) {
this.passWord = passWord;
}

public double getMoney() {
return money;
}

public void setMoney(double money) {
this.money = money;
}

public double getLimit() {
return limit;
}

public void setLimit(double limit) {
this.limit = limit;
}
}

第二步:编写一个ATM类,负责对每一个账户对象进行管理

1
2
3
4
public class ATM {
//创建一个存储账户对象的集合;后面每开一个账户,就往集合中添加一个账户对象
private ArrayList<Account> accounts = new ArrayList<>();
}

第三步:在ATM类中,编写欢迎界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ATM {
//创建一个存储账户对象的集合;后面每开一个账户,就往集合中添加一个账户对象
private ArrayList<Account> accounts = new ArrayList<>();
//为了后面键盘录入方便一点,先创建好一个Scanner对象
private Scanner sc = new Scanner(System.in);

/**启动ATM系统 展示欢迎界面 */
public void start(){
while (true) {
System.out.println("===欢迎您进入到了ATM系统===");
System.out.println("1、用户登录");
System.out.println("2、用户开户");
System.out.println("请选择:");
int command = sc.nextInt();
switch (command){
case 1:
// 用户登录
System.out.println("进入登录功能");
break;
case 2:
// 用户开户
System.out.println("进入开户功能");
break;
default:
System.out.println("没有该操作~~");
}
}
}
}

三、开户功能实现

接下来,我们完成开户功能的实现。需求如下:

为了系统的代码结构更加清晰,在ATM类中,写一个开户的方法。

步骤如下:

  • 1、创建一个账户对象,用于封装用户的开户信息
  • 2、需要用户输入自己的开户信息,赋值给账户对象
  • 输入账户名,设置给账户对象
  • 输入性别,如果性别是'男'或者'女',将性别设置给账户对象;否则重新录入性别知道录入正确为止。
  • 输入账户、并且输入两次密码,只有两次密码相同,才将账户和密码设置给账户对象。
  • 输入提现限额,并且设置给账户对象
  • 3、输出开户成功,的提示语句。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/** 完成用户开户操作  */
private void createAccount(){
System.out.println("==系统开户操作==");
// 1、创建一个账户对象,用于封装用户的开户信息
Account acc = new Account();

// 2、需要用户输入自己的开户信息,赋值给账户对象
System.out.println("请您输入您的账户名称:");
String name = sc.next();
acc.setUserName(name);

while (true) {
System.out.println("请您输入您的性别:");
char sex = sc.next().charAt(0); // "男"
if(sex == '男' || sex == '女'){
acc.setSex(sex);
break;
}else {
System.out.println("您输入的性别有误~只能是男或者女~");
}
}

while (true) {
System.out.println("请您输入您的账户密码:");
String passWord = sc.next();
System.out.println("请您输入您的确认密码:");
String okPassWord = sc.next();
// 判断2次密码是否一样。
if(okPassWord.equals(passWord)){
acc.setPassWord(okPassWord);
break;
}else {
System.out.println("您输入的2次密码不一致,请您确认~~");
}
}

System.out.println("请您输入您的取现额度:");
double limit = sc.nextDouble();
acc.setLimit(limit);

// 重点:我们需要为这个账户生成一个卡号(由系统自动生成。8位数字表示,不能与其他账户的卡号重复:会在下节课详细讲解)
//TODO 这里先留着,待会把生成卡号的功能写好,再到这里调用

// 3、把这个账户对象,存入到账户集合中去
accounts.add(acc);
System.out.println("恭喜您," + acc.getUserName() + "开户成功,您的卡号是:" + acc.getCardId());
}

因为生成卡号比较麻烦,所以在下一节,我们单独来写一个方法用于生成卡号。

四、生成卡号

第一步:先在ATM类中,写一个判断卡号是否存在的功能。

  • 遍历存储Account对象的集合,得到每一个Account对象,获取对象的卡号

  • 如果卡号存在,返回该卡号对应的Account对象

  • 如果卡号不存在,返回null

1
2
3
4
5
6
7
8
9
10
11
12
/** 根据卡号查询账户对象返回 accounts = [c1, c2, c3 ...]*/
private Account getAccountByCardId(String cardId){
// 遍历全部的账户对象
for (int i = 0; i < accounts.size(); i++) {
Account acc = accounts.get(i);
// 判断这个账户对象acc中的卡号是否是我们要找的卡号
if(acc.getCardId().equals(cardId)){
return acc;
}
}
return null; // 查无此账户,这个卡号不存在的
}

第二步:再在ATM类中,写一个生成卡号的功能

  • 1、先随机产生8个[0,9]范围内的随机数,拼接成一个字符串
  • 2、然后再调用getAccountByCardId方法,判断这个卡号字符串是否存在
  • 3、判断生成的卡号是否存在
  • 如果生成的卡号不存在,说明生成的卡号是有效的,把卡号返回,
  • 如果生成的卡号存在,说明生成的卡号无效,循环继续生产卡号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** 返回一个8位 数字的卡号,而且这个卡号不能与其他账户的卡号重复 */
private String createCardId(){
while (true) {
// 1、定义一个String类型的变量记住8位数字作为一个卡号
String cardId = "";
// 2、使用循环,循环8次,每次产生一个随机数给cardId连接起来
Random r = new Random();
for (int i = 0; i < 8; i++) {
int data = r.nextInt(10); // 0 - 9
cardId += data;
}
// 3、判断cardId中记住的卡号,是否与其他账户的卡号重复了,没有重复,才可以做为一个新卡号返回。
Account acc = getAccountByCardId(cardId);
if(acc == null){
// 说明cardId没有找到账户对象,因此cardId没有与其他账户的卡号重复,可以返回它做为一个新卡号
return cardId;
}
}
}

写完生成卡号的功能后,在开户功能的TODO位置,调用生成卡号的功能,并且将生成的卡号设置到账户对象中。

五、登录功能

在上面我们已经完成了开户功能。接下来我们来编写登录功能,编写登录功能的时候我们要满足一下需求:

① 如果系统没有任何账户对象,则不允许登录。

② 让用户输入登录的卡号,先判断卡号是否正确,如果不正确要给出提示。

③ 如果卡号正确,再让用户输入账户密码,如果密码不正确要给出提示,如果密码也正确,则给出登录成功的提示。

登录功能具体实现步骤如下:

  • 1、判断系统中是否存在账户对象,存在才能登录,如果不存在,我们直接结束登录操作
  • 2、输入登录的卡号,并判断卡号是否存在
  • 3、如果卡号不存在,直接给出提示
  • 4、如果卡号存在,接着输入用户密码,并判断密码是否正确
  • 5、如果密码也正确,则登录成功,并且记录当前的登录账户
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/** 完成用户的登录操作 */
private void login(){
System.out.println("==系统登录==");
// 1、判断系统中是否存在账户对象,存在才能登录,如果不存在,我们直接结束登录操作
if(accounts.size() == 0){
System.out.println("当前系统中无任何账户,请先开户再来登录~~");
return; // 跳出登录操作。
}

// 2、系统中存在账户对象,可以开始进行登录操作了
while (true) {
System.out.println("请您输入您的登录卡号:");
String cardId = sc.next();
// 3、判断这个卡号是否存在啊?
Account acc = getAccountByCardId(cardId);
if(acc == null){
// 说明这个卡号不存在。
System.out.println("您输入的登录卡号不存在,请确认~~");
}else {
while (true) {
// 卡号存在了,接着让用户输入密码
System.out.println("请您输入登录密码:");
String passWord = sc.next();
// 4、判断密码是否正确
if(acc.getPassWord().equals(passWord)){
loginAcc = acc;
// 密码正确了,登录成功了
System.out.println("恭喜您," + acc.getUserName() + "成功登录了系统,您的卡号是:" + acc.getCardId());
//TODO 把展示登录界面的功能写成一个方法,写好了再回来调用。
return; // 跳出并结束当前登录方法
}else {
System.out.println("您输入的密码不正确,请确认~~");
}
}
}
}
}

六、展示用户操作界面

登录成功之后,需要显示登录后的用户操作界面。效果如下

写成一个方法,用来展示登录成功的操作界面,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/** 展示登录后的操作界面的 */
private void showUserCommand(){
while (true) {
System.out.println(loginAcc.getUserName() + "您可以选择如下功能进行账户的处理====");
System.out.println("1、查询账户");
System.out.println("2、存款");
System.out.println("3、取款");
System.out.println("4、转账");
System.out.println("5、密码修改");
System.out.println("6、退出");
System.out.println("7、注销当前账户");
System.out.println("请选择:");
int command = sc.nextInt();
switch (command){
case 1:
//TODO 查询当前账户
break;
case 2:
//TODO 存款
break;
case 3:
//TODO取款
break;
case 4:
//TOD 转账
break;
case 5:
//TODO 密码修改
return;// 跳出并结束当前方法
case 6:
//TODO 退出
System.out.println(loginAcc.getUserName() + "您退出系统成功!");
return; // 跳出并结束当前方法
case 7:
// 注销当前登录的账户
if(deleteAccount()){
// 销户成功了,回到欢迎界面
return;
}
break;
default:
System.out.println("您当前选择的操作是不存在的,请确认~~");
}
}
}

写好用户操作界面的方法之后,再到登录成功的位置调用,登录成功后,马上显示用户操作界面。刚才在哪里打了一个TODO标记的,回去找找。

到这里,登录功能就写好了。

六、查询账户、退出

  • 查询账户:在用户操作界面,选择1查询当前账户信息。效果如下:

登录成功的时候,已经把当前账户对象用一个成员变量存储了 ,所以直接按照如下格式打印账户对象的属性信息即可。

这里也写成一个方法

1
2
3
4
5
6
7
8
9
10
11
/**
展示当前登录的账户信息
*/
private void showLoginAccount(){
System.out.println("==当前您的账户信息如下:==");
System.out.println("卡号:" + loginAcc.getCardId());
System.out.println("户主:" + loginAcc.getUserName());
System.out.println("性别:" + loginAcc.getSex());
System.out.println("余额:" + loginAcc.getMoney());
System.out.println("每次取现额度:" + loginAcc.getLimit());
}
  • 退出功能:其实就是将ATM系统中,在用户界面选择6时,直接结束程序。

七、存款

接下来来完成存款操作。

我们把存款功能也写成一个方法,具体步骤如下:

    1. 键盘录入要存入的金额
    1. 在原有余额的基础上,加上存入金额,得到新的余额
    1. 再将新的余额设置给当前账户对象
1
2
3
4
5
6
7
8
9
10
/** 存钱 */
private void depositMoney() {
System.out.println("==存钱操作==");
System.out.println("请您输入存款金额:");
double money = sc.nextDouble();

// 更新当前登录的账户的余额。
loginAcc.setMoney(loginAcc.getMoney() + money);
System.out.println("恭喜您,您存钱:" + money + "成功,存钱后余额是:" + loginAcc.getMoney());
}

写好存款的方法之后,在case 2:的下面调用depositMoney()方法

到这里,存款功能就写好了。

八、取款

接下来我们写一下取款的功能。

把取款的功能也写成一个方法,具体步骤如下

  • 1、判断账户余额是否达到了100元,如果不够100元,就不让用户取钱了

  • 2、让用户输入取款金额

  • 3、判断账户余额是否足够

    • 如果余额足够, 继续判断当前取款金额是否超过了每次限额

      • 如果超过限额,提示“每次只能取xxx限额的钱”

      • 如果不超过限额,则在当前余额上减去取钱的金额,得到新的余额

        并将新的余额设置给账户对象。

    • 如果余额不足,提示“你的余额不足,你的账户余额是xxx元”

按照上面分析的步骤,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/** 取钱 */
private void drawMoney() {
System.out.println("==取钱操作==");
// 1、判断账户余额是否达到了100元,如果不够100元,就不让用户取钱了
if(loginAcc.getMoney() < 100){
System.out.println("您的账户余额不足100元,不允许取钱~~");
return;
}

// 2、让用户输入取款金额
while (true) {
System.out.println("请您输入取款金额:");
double money = sc.nextDouble();

// 3、判断账户余额是否足够
if(loginAcc.getMoney() >= money){
// 账户中的余额是足够的
// 4、判断当前取款金额是否超过了每次限额
if(money > loginAcc.getLimit()){
System.out.println("您当前取款金额超过了每次限额,您每次最多可取:" + loginAcc.getLimit());
}else {
// 代表可以开始取钱了。更新当前账户的余额即可
loginAcc.setMoney(loginAcc.getMoney() - money);
System.out.println("您取款:" + money + "成功,取款后您剩余:" + loginAcc.getMoney());
break;
}
}else {
System.out.println("余额不足,您的账户中的余额是:" + loginAcc.getMoney());
}
}
}

写好取钱方法之后,在case 3:的位置调用drawMoney()方法

九、转账

接下来我们来编写转账的功能。转账的意思就是,将一个账户的钱转入另一个账,具体的转账逻辑如下:

我们把转账功能也写成一个方法

  • 1、判断系统中是否存在其他账户

  • 2、判断自己的账户中是否有钱

  • 3、真正开始转账了,输入对方卡号

  • 4、判断对方卡号是否正确啊?

  • 5、如果卡号正确,就继续让用户输入姓氏, 并判断这个姓氏是否正确?

    • 如果姓氏不正确,给出提示“对不起,您姓氏有问题,转账失败!”
  • 6、如果姓氏正确,继续判断这个转账金额是否超过自己的余额。

    • 如果转账金额超过余额,给出提示“对不起,余额不足,转账失败!”
  • 7、如果对方卡号存在、姓氏匹配、余额足够,就完成真正的转账操作

    • 获取当前自己账户的余额,减去转账金额,就可以得到自己账户新的余额,

      并将新的余额,设置给当前账户

    • 并且获取对方的账户余额,加上转账金额,就可以得到对方账户新的余额,

      并将新的余额,设置给对方账户

    • 给出提示:“您转账成功了~~~”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/** 转账 */
private void transferMoney() {
System.out.println("==用户转账==");
// 1、判断系统中是否存在其他账户。
if(accounts.size() < 2){
System.out.println("当前系统中只有你一个账户,无法为其他账户转账~~");
return;
}

// 2、判断自己的账户中是否有钱
if(loginAcc.getMoney() == 0){
System.out.println("您自己都没钱,就别转了~~");
return;
}

while (true) {
// 3、真正开始转账了
System.out.println("请您输入对方的卡号:");
String cardId = sc.next();

// 4、判断这个卡号是否正确啊??
Account acc = getAccountByCardId(cardId);
if(acc == null){
System.out.println("您输入的对方的卡号不存在~~");
}else {
// 对方的账户存在,继续让用户认证姓氏。
String name = "*" + acc.getUserName().substring(1); // * + 马刘德华
System.out.println("请您输入【" + name + "】的姓氏:");
String preName = sc.next();
// 5、判断这个姓氏是否正确啊
if(acc.getUserName().startsWith(preName)) {
while (true) {
// 认证通过了:真正转账了
System.out.println("请您输入转账给对方的金额:");
double money = sc.nextDouble();
// 6、判断这个金额是否没有超过自己的余额。
if(loginAcc.getMoney() >= money){
// 7、转给对方了
// 更新自己的账户余额
loginAcc.setMoney(loginAcc.getMoney() - money);
// 更新对方的账户余额
acc.setMoney(acc.getMoney() + money);
System.out.println("您转账成功了~~~");
return; // 跳出转账方法。。
}else {
System.out.println("您余额不足,无法给对方转这么多钱,最多可转:" + loginAcc.getMoney());
}
}
}else {
System.out.println("对不起,您认证的姓氏有问题~~");
}
}
}
}

写好修改转账功能之后,在case 4:这里调用。如下:

到这里,转账功能就写好了。

十、修改密码

接下来我们完成修改密码的功能。

把修改密码的功能也是写成一个方法,具体步骤如下

  • 1、提醒用户输入当前密码

  • 2、认证当前密码是否正确

    • 如果认证密码错误,提示“您当前输入的密码不正确~~”;重新输入密码,再次认证密码是否正确。
  • 3、如果认证密码正确,开始修改密码,修改密码时需要用户输入2次新密码

  • 4、判断2次 密码是否一致

    • 如果两次密码一致,就将新密码设置给当前账户对象,密码修改成功
    • 如果两次密码不一直,则给出提示“您输入的2次密码不一致~~”;重新输入新密码,并确认密码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/** 账户密码修改 */
private void updatePassWord() {
System.out.println("==账户密码修改操作==");
while (true) {
// 1、提醒用户认证当前密码
System.out.println("请您输入当前账户的密码:");
String passWord = sc.next();

// 2、认证当前密码是否正确啊
if(loginAcc.getPassWord().equals(passWord)){
// 认证通过
while (true) {
// 3、真正开始修改密码了
System.out.println("请您输入新密码:");
String newPassWord = sc.next();

System.out.println("请您再次输入密码:");
String okPassWord = sc.next();

// 4、判断2次 密码是否一致
if(okPassWord.equals(newPassWord)){
// 可以真正开始修改密码了
loginAcc.setPassWord(okPassWord);
System.out.println("恭喜您,您的密码修改成功~~~");
return;
}else {
System.out.println("您输入的2次密码不一致~~");
}
}
}else {
System.out.println("您当前输入的密码不正确~~");
}
}
}

写好修改密码的功能之后。在case 5:的位置调用updatePassWord()方法。如下图所示

好了,到这里修改密码的功能就写好了。

十一、注销

各位同学,接下来我们完成最后一个功能,注销功能。

这里把注销功能也写成一个方法,具体步骤如下

  • 1、先确认是否需要注销账户,让用户输入y或者n
    • 如果输入y,表示确认
    • 如果输入n,表示取消注销操作
  • 2、输入y后,继续判断当前用户的账户是否有钱
    • 如果账户有钱,提示:“对不起,您的账户中存钱金额,不允许销”
    • 如果账户没有钱,则把当前账户对象,从系统的集合中删除,完成注销。

按照上面的步骤代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 销户操作 */
private boolean deleteAccount() {
System.out.println("==进行销户操作==");
// 1、问问用户是否确定要销户啊
System.out.println("请问您确认销户吗?y/n");
String command = sc.next();
switch (command) {
case "y":
// 确实要销户
// 2、判断用户的账户中是否有钱:loginAcc
if(loginAcc.getMoney() == 0) {
// 真的销户了
accounts.remove(loginAcc);
System.out.println("您好,您的账户已经成功销户~~");
return true;
}else {
System.out.println("对不起,您的账户中存钱金额,不允许销户~~");
return false;
}
default:
System.out.println("好的,您的账户保留!!");
return false;
}
}

注销功能写好之后,在用户操作界面的case 7:位置调用deleteAccount()的方法。


到这里注销账户的功能就写好了。

面向对象高级

前面我们说过面向对象最核心的套路是:设计对象来处理数据,解决问题。

静态static

  1. static是什么?
  • 叫静态,可以修饰成员变量、成员方法。
  1. static修饰的成员变量叫什么?怎么使用?有啥特点?
  • 类变量(静态成员变量)
    类名.类变量(推荐)
    对象名.类变量(不推荐)
  • 属于类,与类一起加载一次,在内存中只有一份,会被类的所有对象共享。
  1. 无static修饰的成员变量叫什么?怎么使用?有啥特点?
  • 实例变量(对象变量)
    对象.实例变量
  • 属于对象,每个对象中都有一份。

  1. 成员变量有几种?各自在什么情况下定义?

类变量:数据只需要一份,且需要被共享时(访问,修改)
实例变量:每个对象都要有一份,数据各不同(如:name、score、age)

  1. 访问自己类中的类变量,是否可以省略类名不写?
    可以的
    注意:在某个类中访问其他类里的类变量,必须带类名访问

成员方法的分类

  • 类方法∶有static修饰的成员方法,属于类。
1
2
3
4
static void printHellowor1d(){
system.out.println("He1lo wor1d!");
system.out.printin("Hello wor1d!");
}

类名.类方法(推荐)
student.printHelloWorld;
对象名.类方法(不推荐)
s1.printHelloWorld;

  • 实例方法︰无static修饰的成员方法,属于对象。
    pubiic void printPass()
    对象.实例方法

工具类

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

  • 我们写一个生成验证码的工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyUtils{
public static String createCode(int n){
//1.定义一个字符串,用来记录产生的验证码
String code = "";

//2.验证码是由所有的大写字母、小写字母或者数字字符组成
//这里先把所有的字符写成一个字符串,一会从字符串中随机找字符
String data = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKMNOPQRSTUVWXYZ";

//3.循环n次,产生n个索引,再通过索引获取字符
Random r = new Random();
for(int i=0; i<n; i++){
int index = r.nextInt(data.length());
char ch = data.charAt(index);
//4.把获取到的字符,拼接到code验证码字符串上。
code+=ch;
}

//最后返回code,code的值就是验证码
return code;
}
}
  • 接着可以在任何位置调用MyUtilscreateCOde()方法产生任意个数的验证码
1
2
3
4
5
6
//比如这是一个登录界面
public class LoginDemo{
public static void main(String[] args){
System.out.println(MyUtils.createCode());
}
}
1
2
3
4
5
6
//比如这是一个注册界面
public class registerDemo{
public static void main(String[] args){
System.out.println(MyUtils.createCode());
}
}

在补充一点,工具类里的方法全都是静态的,推荐用类名调用为了防止使用者用对象调用。我们可以把工具类的构造方法私有化。

1
2
3
4
5
6
7
8
9
10
11
public class MyUtils{
//私有化构造方法:这样别人就不能使用构造方法new对象了
private MyUtils(){

}

//类方法
public static String createCode(int n){
...
}
}
  1. 类方法有啥应用场景?
    可以用来设计工具类。

  2. 工具类是什么,有什么好处?
    工具类中的方法都是类方法,每个类方法都是用来完成一个功能的。
    提高了代码的复用性;调用方便,提高了开发效率。

  3. 为什么工具类要用类方法,而不是用实例方法?
    实例方法需要创建对象来调用,会浪费内存。

  4. 工具类定义时有什么要求?
    工具类不需要创建对象,建议将工具类的构造器私有化。

static的注意事项

  • 使用类方法、实方法时的门点注意事项
  1. 类方法中可以直接访问类的成员,不可以直接访问实例成员

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

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

代码块

  • 代码块概述
  • 代码块是类的5大成分之一(成员变量、构造器、方法、代码块、内部类)。
  • 代码块分为两种;
  • 静态代码块:
    • 格式: static { }
    • 特点:类加载时自动执行,由于类只会加载一次,所以静态代码块也只会执行一次。
    • 作用:完成类的初始化,例如:对类变量的初始化赋值。
  • 实例代码块:
    • 格式:{}
    • 特点:每次创建对象时,执行实例代码块,并在构造器前执行。
    • 作用:和构造器一样,都是用来完成对象的初始化的,例如:对实例变量进行初始化赋值。

我们先类学习静态代码块:

1
2
3
4
5
6
7
8
9
public class Student {
static int number = 80;
static String schoolName = "黑马";
// 静态代码块
static {
System.out.println("静态代码块执行了~~");
schoolName = "黑马";
}
}

静态代码块不需要创建对象就能够执行

1
2
3
4
5
6
7
8
9
10
public class Test {
public static void main(String[] args) {
// 目标:认识两种代码块,了解他们的特点和基本作用。
System.out.println(Student.number);
System.out.println(Student.number);
System.out.println(Student.number);

System.out.println(Student.schoolName); // 黑马
}
}

执行上面代码时,发现没有创建对象,静态代码块就已经执行了。

关于静态代码块重点注意:静态代码块,随着类的加载而执行,而且只执行一次。

再来学习一下实例代码块

实例代码块的作用和构造器的作用是一样的,用来给对象初始化值;而且每次创建对象之前都会先执行实例代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Student{
//实例变量
int age;
//实例代码块:实例代码块会执行在每一个构造方法之前
{
System.out.println("实例代码块执行了~~");
age = 18;
System.out.println("有人创建了对象:" + this);
}

public Student(){
System.out.println("无参数构造器执行了~~");
}

public Student(String name){
System.out.println("有参数构造器执行了~~");
}
}

接下来在测试类中进行测试,观察创建对象时,实例代码块是否先执行了。

1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
Student s2 = new Student("张三");
System.out.println(s1.age);
System.out.println(s2.age);
}
}

对于实例代码块重点注意:实例代码块每次创建对象之前都会执行一次

设计模式

  1. 什么是设计模式,设计模式主要学什么?单例模式解决了什么问题?
  • 设计模式就是具体问题的最优解决方案。
  • 解决了什么问题?怎么写?
  • 确保一个类只有一个对象。
  1. 单例怎么写?饿汉式单例的特点是什么?
  • 把类的构造器私有;定义一个类变量存储类的一个对象;提供一个类方法返回对象。
  • 在获取类的对象时,对象已经创建好了。
  1. 单例有啥应用场景,有啥好处?
  • 任务管理器对象、获取运行时对象。
  • 在这些业务场景下,使用单例模式,可以避免浪费内存。
  • 懒汉式单例设计模式
  • 拿对象时,才开始创建对象。
  • 写法
  • 把类的构造器私有。
  • 定义一个类变量用于存储对象。
  • 提供一个类方法,保证返回的是同一个对象。

继承

接下来,我们演示一下使用继承来编写代码,注意观察继承的特点。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class A{
//公开的成员
public int i;
public void print1(){
System.out.println("===print1===");
}

//私有的成员
private int j;
private void print2(){
System.out.println("===print2===");
}
}

然后,写一个B类,让B类继承A类。在继承A类的同时,B类中新增一个方法print3

1
2
3
4
5
6
7
8
9
10
11
public class B extends A{
public void print3(){
//由于i和print1是属于父类A的公有成员,在子类中可以直接被使用
System.out.println(i); //正确
print1(); //正确

//由于j和print2是属于父类A的私有成员,在子类中不可以被使用
System.out.println(j); //错误
print2();
}
}

接下来,我们再演示一下,创建B类对象,能否调用父类A的成员。再写一个测试类

1
2
3
4
5
6
7
8
9
10
11
12
public class Test{
public static void main(String[] args){
B b = new B();
//父类公有成员,子类对象是可以调用的
System.out.println(i); //正确
b.print1();

//父类私有成员,子类对象时不可以调用的
System.out.println(j); //错误
b.print2(); //错误
}
}

到这里,关于继承的基本使用我们就算学会了。为了让大家对继承有更深入的认识,我们来看看继承的内存原理。

这里我们只需要关注一点:子类对象实际上是由子、父类两张设计图共同创建出来的。

所以,在子类对象的空间中,既有本类的成员,也有父类的成员。但是子类只能调用父类公有的成员

  1. 什么是继承?继承后有啥特点?
  • 继承就是用extends关键字,让一个类和另一个类建立起一种父子关系。
  • 子类可以继承父类非私有的成员。
  1. 带继承关系的类,Java会怎么创建它的对象?对象创建出来后,可以直接访问哪些成员?
  • 带继承关系的类,java会用类和其父类,这多张设计图来一起创建类的对象。
  • 对象能直接访问什么成员,是由子父类这多张设计图共同决定的,这多张设计图对外暴露了什么成员,对象就可以访问什么成员。

权限修饰符

使用继承编写的代码中我们有用到两个权限修饰符,一个是public(公有的)、一个是private(私有的),实际上还有两个权限修饰符,一个是protected(受保护的)、一个是缺省的(不写任何修饰符)。

什么是权限修饰符呢?

权限修饰符是用来限制类的成员(成员变量、成员方法、构造器…)能够被访问的范围。

每一种权限修饰符能够被访问的范围如下

单继承、Object

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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{}
  1. 继承相关的两个注意事项?
  • Java是单继承的:一个类只能继承一个直接父类;Java中的类不支持多继承,但是支持多层继承。
  • object类是Java中所有类的祖宗。

方法重写

什么是方法重写

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

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

  1. 什么是方法重写?
  • 当子类觉得父类中的某个方法不好用,或者无法满足自己的需求时,子类可以重写一个方法名称、参数列表一样的方法,去覆盖父类的这个方法,这就是方法重写。
  • 注意:重写后,方法的访问,Java会遵循就近原则。
  1. 方法重写的其它注意事项
1
2
3
4
5
- 1.重写的方法上面,可以加一个注解@Override,用于标注这个方法是复写的父类方法
- 2.子类复写父类方法时,访问权限必须大于或者等于父类方法的权限
public > protected > 缺省
- 3. 重写的方法返回值类型,必须与被重写的方法返回值类型一样,或者范围更小
- 4. 私有方法、静态方法不能被重写,如果重写会报错。

子类访问其他成员特点

  • 原则:在子类中访问其他成员(成员变量、成员方法),是依据就近原则的
  1. 在子类方法中访问其他成员(成员变量、成员方法),是依照就近原则的。
  • 先子类局部范围找。
  • 然后子类成员范围找。
  • 然后父类成员范围找,如果父类范围还没有找到则报错。
  1. 如果子父类中,出现了重名的成员,会优先使用子类的,如果此时一定要在子类中使用父类的怎么办?
  • 可以通过super关键字,指定访问父类的成员: super.父类成员变量/父类成员方法

子类构造器的特点

  1. 子类构造器的特点:
  • 子类的全部构造器,都会先调用父类的构造器,再执行自己。
  1. 子类构造器是如何实现调用父类构造器的:
  • 默认情况下,子类全部构造器的第一行代码都是super()(写不写都有),它会调用父类的无参数构造器。
  • 如果父类没有无参数构造器,则我们必须在子类构造器的第一行手写super(…),指定去调用父类的有参数构造器。
  1. 补充知识:this(..)调用兄弟构造器
  • 任意类的构造器中,是可以通过this(…)去调用该类的其他构造器的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class student {
    private string schoolName;
    private string name;

    public student(string name){
    this(name ,“黑马程序员”);
    }
    public student(string name , string schoolName ){
    this.name = name;
    this.schoolName = schoolName;
    }
    }
  1. this(…)和super(…)使用时的注意事项:

this(…) 、 super(..)都只能放在构造器的第一行,因此,有了this(…)就不能写super(…)了,反之亦然。

最后我们被this和super的用法在总结一下

1
2
3
4
5
6
7
8
9
10
11
12
13
访问本类成员:
this.成员变量 //访问本类成员变量
this.成员方法 //调用本类成员方法
this() //调用本类空参数构造器
this(参数) //调用本类有参数构造器

访问父类成员:
super.成员变量 //访问父类成员变量
super.成员方法 //调用父类成员方法
super() //调用父类空参数构造器
super(参数) //调用父类有参数构造器

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

多态

什么是多态?

多态是在继承、实现情况下的一种现象,表现为:对象多态、行为多态。

  • 多态的具体代码体现
    People p1 =new student();
    p1.run();
    People p2 =new Teacher();
    p2.run();

  • 多态的前提

  • 有继承/实现关系;存在父类引用子类对象;存在方法重写。多态的一个注意事项
  • 多态是对象、行为的多态,Java中的属性(成员变量)不谈多态。

定义方法时,使用父类类型作为形参,可以接收一切子类对象,扩展行更强,更便利。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test2 {
public static void main(String[] args) {
// 目标:掌握使用多态的好处
Teacher t = new Teacher();
go(t);

Student s = new Student();
go(s);
}

//参数People p既可以接收Student对象,也能接收Teacher对象。
public static void go(People p){
System.out.println("开始------------------------");
p.run();
System.out.println("结束------------------------");
}
}
  1. 使用多态有什么好处?存在什么问题?
    可以解耦合,扩展性更强;使用父类类型的变量作为方法的形参时,可以接收一切子类对象。
  • 多态下不能直接调用子类的独有方法。
  1. 类型转换
  • 自动类型转换:父类变量名=new子类();
    例如:People p = new Teacher();

  • 强制类型转换:子类变量名=(子类)父类变量
    例如Teacher t = (Teacher)p;

多态形式下不能直接调用子类特有方法,但是转型后是可以调用的。这里所说的转型就是把父类变量转换为子类类型。格式如下:

1
2
3
4
5
//如果p接收的是子类对象
if(父类变量 instance 子类){
//则可以将p转换为子类类型
子类 变量名 = (子类)父类变量;
}
  • 强制类型转换的一个注意事项
  • 存在继承/实现关系就可以在编译阶段进行强制类型转换,编译阶段不会报错。
  • 运行时,如果发现对象的真实类型与强转后的类型不同,就会报类型转换异常

强转前,Java建议:

  • 使用instanceof关键字,判断当前对象的真实类型,再进行强转。
    p instanceof Student

final

  • final关键字是最终的意思,可以修饰(类、方法、变量)
1
2
3
- final修饰类:该类称为最终类,特点是不能被继承
- final修饰方法:该方法称之为最终方法,特点是不能被重写。
- final修饰变量:该变量只能被赋值一次。
  • final修饰变量的注意
  • final修饰基本类型的变量,变量存储的数据不能被改变。
  • final修饰引用类型的变量,变量存储的地址不能被改变
    但地址所指向对象的内容是可以被改变的。

常量

  • 被 static final 修饰的成员变量,称之为常量。
  • 通常用于记录系统的配置信息
1
2
3
4
5
6
public class Constant {
//常量: 定义一个常量表示学校名称
//为了方便在其他类中被访问所以一般还会加上public修饰符
//常量命名规范:建议都采用大写字母命名,多个单词之前有_隔开
public static final String SCHOOL_NAME = "教育";
}

注意!常量名的命名规范:建议使用大写英文单词,多个单词使用下划线连接起来。

  • 使用常量记录系统配置信息的优势、执行原理
  • 代码可读性更好,可维护性也更好。
  • 程序编译后,常量会被“宏替换”︰出现常量的地方全部会被替换成其记住的字面量,
    这样可以保证使用常量和直接用字面量的性能是一样的。

抽象类

  • 在Java中有一个关键字叫abstract,它就是抽象的意思,它可以修饰类也可以修饰方法。
1
2
- 被abstract修饰的类,就是抽象类
- 被abstract修饰的方法,就是抽象方法(不允许有方法体)

  • 类的成员(成员变量、成员方法、构造器),类的成员都可以有。如下面代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 抽象类
public abstract class A {
//成员变量
private String name;
static String schoolName;

//构造方法
public A(){

}

//抽象方法
public abstract void test();

//实例方法
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
  • 抽象类是不能创建对象的,如果抽象类的对象就会报错

  • 抽象类虽然不能创建对象,但是它可以作为父类让子类继承。而且子类继承父类必须重写父类的所有抽象方法。

1
2
3
4
5
6
7
//B类继承A类,必须复写test方法
public class B extends A {
@Override
public void test() {

}
}
  • 子类继承父类如果不复写父类的抽象方法,要想不出错,这个子类也必须是抽象类
1
2
3
4
//B类基础A类,此时B类也是抽象类,这个时候就可以不重写A类的抽象方法
public abstract class B extends A {

}
  • 抽象类的注意事项、特点

注意:抽象类不能创建对象

  1. 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类。
  2. 类该有的成员(成员变量、方法、构造器)抽象类都可以有。
  3. 抽象类最主要的特点:抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现。
  4. 一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。
  • 抽象类的应用场景和好处是什么?
    父类知道每个子类都要做某个行为但每个子类要做的情况不一样,父类就定义成抽象方法,交给子类去重身实现,我们抽出这样的抽象类,就是为了更好的支持多态。

模拟方法设计模式

学习完抽象类的语法之后,接下来,我们学习一种利用抽象类实现的一种设计模式。先解释下一什么是设计模式?设计模式是解决某一类问题的最优方案

设计模式在一些源码中经常会出现,还有以后面试的时候偶尔也会被问到,所以在合适的机会,就会给同学们介绍一下设计模式的知识。

那模板方法设计模式解决什么问题呢?模板方法模式主要解决方法中存在重复代码的问题

比如A类和B类都有sing()方法,sing()方法的开头和结尾都是一样的,只是中间一段内容不一样。此时A类和B类的sing()方法中就存在一些相同的代码。

怎么解决上面的重复代码问题呢? 我们可以写一个抽象类C类,在C类中写一个doSing()的抽象方法。再写一个sing()方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 模板方法设计模式
public abstract class C {
// 模板方法
public final void sing(){
System.out.println("唱一首你喜欢的歌:");

doSing();

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

public abstract void doSing();
}

然后,写一个A类继承C类,复写doSing()方法,代码如下

1
2
3
4
5
6
public class A extends C{
@Override
public void doSing() {
System.out.println("我是一只小小小小鸟,想要飞就能飞的高~~~");
}
}

接着,再写一个B类继承C类,也复写doSing()方法,代码如下

1
2
3
4
5
6
public class B extends C{
@Override
public void doSing() {
System.out.println("我们一起学猫叫,喵喵喵喵喵喵喵~~");
}
}

最后,再写一个测试类Test

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

综上所述:模板方法模式解决了多个子类中有相同代码的问题。具体实现步骤如下

1
2
3
1步:定义一个抽象类,把子类中相同的代码写成一个模板方法。
2步:把模板方法中不能确定的代码写成抽象方法,并在模板方法中调用。
3步:子类继承抽象类,只需要父类抽象方法就可以了。
  1. 模板方法设计模式解决了什么问题?
  • 解决方法中存在重复代码的问题。
  1. 模板方法设计模式应该怎么写?
  • 定义一个抽象类。
  • 在里面定义2个方法,
    一个是模板方法:放相同的代码里,
    一个是抽象方法:具体实现交给子类完成。
  1. 模板方法建议使用什么关键字修饰?为什么
  • 建议使用final关键字修饰模板方法。

接口

认识接口

  • Java提供了一个关键字interface,用这个关键字我们可以定义出一个特殊的结构∶接口。
1
2
3
4
public interface 接口名{
//成员变量(常量)
//成员方法(抽象方法)
}

那接口到底什么使用呢?需要我注意下面两点

  • 接口是用来被类实现(implements)的,我们称之为实现类。

  • 一个类是可以实现多个接口的(接口可以理解成干爹),类实现接口必须重写所有接口的全部抽象方法,否则这个类也必须是抽象类

修饰符 class 实现类 implements接口1,接口2,接口3,… {
}

  1. 使用接口有啥好处,第一个好处是什么?
  • 可以解决类单继承的问题,通过接口,我们可以让一个类有一个亲爹的同时,还可以找多个干爹去扩展自己的功能。
  1. 为什么我们要通过接口,也就是去找干爹,来扩展自己的功能呢?
  • 因为通过接口去找干爹,别人通过你implements的接口,就可以显性的知道你是谁,从而也就可以放心的把你当作谁来用了。
  1. 使用接口的第二个好处是什么?
  • 一个类我们说可以实现多个接口,同样,一个接口也可以被多个类实现的。这样做的好处是我们的程序就可以面向接口编程了,这样我们程序员就可以很方便的灵活切换各种业务实现了。

JDK8新增方法

我们看一下这三种方法分别有什么特点?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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(){

}
}

接下来我们写一个B类,实现A接口。B类作为A接口的实现类,只需要重写抽象方法就尅了,对于默认方法不需要子类重写。代码如下:

1
2
3
4
5
6
7
8
9
10
11
public class B implements A{
@Override
public void test4() {

}

@Override
public void test5() {

}
}

最后,写一个测试类,观察接口中的三种方法,是如何调用的

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
// 目标:掌握接口新增的三种方法形式
B b = new B();
b.test1(); //默认方法使用对象调用
// b.test2(); //A接口中的私有方法,B类调用不了
A.test3(); //静态方法,使用接口名调用
}
}

综上所述:JDK8对接口新增的特性,有利于对程序进行扩展。

  1. JDK8开始,接口中新增了哪些方法?
  • 默认方法:使用default修饰,使用实现类的对象调用。
  • 静态方法:static修饰,必须用当前接口名调用
  • 私有方法:private修饰,jdk9开始才有的,只能在接口内部被调用。
  • 他们都会默认被public修饰。
  1. JDK8开始,接口中为啥要新增这些方法?
  • 增强了接口的能力,更便于项目的扩展和维护。

接口的多继承

接口其他注意事项

  • 一个接口可以继承多个接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Test {
public static void main(String[] args) {
// 目标:理解接口的多继承。
}
}

interface A{
void test1();
}
interface B{
void test2();
}
interface C{}

//比如:D接口继承C、B、A
interface D extends C, B, A{

}

//E类在实现D接口时,必须重写D接口、以及其父类中的所有抽象方法。
class E implements D{
@Override
public void test1() {

}

@Override
public void test2() {

}
}

接口除了上面的多继承特点之外,在多实现、继承和实现并存时,有可能出现方法名冲突的问题,需要了解怎么解决(仅仅只是了解一下,实际上工作中几乎不会出现这种情况)

1
2
3
4
1.一个接口继承多个接口,如果多个接口中存在相同的方法声明,则此时不支持多继承
2.一个类实现多个接口,如果多个接口中存在相同的方法声明,则此时不支持多实现
3.一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会有限使用父类的方法
4.一个类实现类多个接口,多个接口中有同名的默认方法,则这个类必须重写该方法。

内部类

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

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

内部类有四种形式,分别是成员内部类、静态内部类、局部内部类、匿名内部类。

  • 成员内部类
  • 就是类中的一个普通成员,类似前面我们学过的普通的成员变量、成员方法。
1
2
3
4
5
public class Outer {
//成员内部类
public class Inner {
}
}

注意:JDK16之前,成员内部类中不能定义静态成员,JDK 16开始也可以定义静态成员了

  • 创建对象的格式:
    外部类名.内部类名 对象名= new 外部类(…).new 内部类(…);
    outer.Inner in = new Outer( ).new Inner( );
  1. 成员内部类是什么?如何创建其对象?
  • 就是类中的一个普通成员,类似前面我们学过的普通成员变量、成员方法
  • 外部类名.内部类名对象名=new外部类(…).new内部类(.…);
  1. 成员内部类的实例方法中,访问其他成员有啥特点?
  • 可以直接访问外部类的实例成员、静态成员
  • 可以拿到当前外部类对象,格式是:外部类名.this。

静态内部类

  • 什么是静态内部类?
  • 有static修饰的内部类,属于外部类自己持有。
1
2
3
4
5
public class Outer{
//静态内部类
public static class Inner{
}
}

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

1
2
3
//格式:外部类.内部类 变量名 = new 外部类.内部类();
Outer.Inner in = new Outer.Inner();
in.test();
  • 静态内部类中访问外部类成员的特点
  • 可以直接访问外部类的静态成员,不可以直接访问外部类的实例成员。

局部内部类

局部内部类是定义在方法中的类,和局部变量一样,只能在方法中有效。所以局部内部类的局限性很强,一般在开发中是不会使用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Outer{
public void test(){
//局部内部类
class Inner{
public void show(){
System.out.println("Inner...show");
}
}

//局部内部类只能在方法中创建对象,并使用
Inner in = new Inner();
in.show();
}
}

匿名内部类

我们还是先认识一下什么是匿名内部类?

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

匿名内部类

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

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

1
2
3
4
new 父类/接口(参数值){
@Override
重写父类/接口的方法;
}
  • 特点:匿名内部类本质就是一个子类,并会立即创建出一个子类对象。
  • 作用:用于更方便的创建一个子类对象。

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

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

1
2
3
public abstract class Animal{
public abstract void cry();
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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.eat(); //直线上面重写的cry()方法
}
}

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

匿名内部类的作用:简化了创建子类对象、实现类对象的书写格式。

匿名内部类的应用场景

学习完匿名内部类的基本使用之后,我们再来看一下匿名内部类在实际中的应用场景。其实一般我们会主动的使用匿名内部类。

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

1
2
3
public interface Swimming{
public void swim();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test{
public static void main(String[] args){
Swimming s1 = new Swimming(){
public void swim(){
System.out.println("狗刨飞快");
}
};
go(s1);

Swimming s1 = new Swimming(){
public void swim(){
System.out.println("猴子游泳也还行");
}
};
go(s1);
}
//形参是Swimming接口,实参可以接收任意Swimming接口的实现类对象
public static void go(Swimming s){
System.out.println("开始~~~~~~~~");
s.swim();
System.out.println("结束~~~~~~~~");
}
}

  1. 匿名内部类的书写格式是什么样的?
    new类或接口(参数值…){
    类体(一般是方法重写);
    };

  2. 匿名内部类有啥特点?

  • 匿名内部类本质就是一个子类,并会立即创建出一个子类对象
  1. 匿名内部类有啥作用、应用场景?
  • 可以更方便的创建出一个子类对象。
  • 匿名内部类通常作为一个参数传输给方法。

枚举

枚举是一种特殊的类,它的格式是:

1
2
3
public enum 枚举类名{
枚举项1,枚举项2,枚举项3;
}
  • 枚举类的特点:
    1
    2
    3
    public enum A{
    X, Y ,Z;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    Compiled from "A.java"
    public final class A extends java.lang.Enum<A> {
    public static final A X = new A();
    public static final A Y= new A();
    public static final A Z = new A();
    public static A[] values();
    public static A valueof(java.lang.string);
    }
    枚举类A是用class定义的,说明枚举确实是一个类,而且X,Y,Z都是A类的对象;而且每一个枚举项都是被public static final 修饰,所以被可以类名调用,而且不能更改。
  • 枚举类的第一行只能罗列十些名称,这些名称都是常量,并且每个常量记住的都是枚举类的一个对象。
  • 枚举类的构造器都是私有的(写不写都只能是私有的),因此,枚举类对外不能创建对象。
  • 枚举都是最终类,不可以被继承。
  • 枚举类中,从第二行开始,可以定义类的其他各种成员。
  • 编译器为枚举类新增了几个方法,并且枚举类都是继承: java.lang.Enum类的,从enum类也会继承到一些方法。
  • 枚举的常见应用场景:
  • 用来表示一组信息,然后作为参数进行传输。
    选择定义一个一个的常量来表示一组信息,并作为参数传输
  • 参数值不受约束。
    选择定义枚举表示一组信息,并作为参数传输
  • 代码可读性好,参数值得到了约束,对使用老更友好,建议使用!

泛型

泛型
定义类、接口、方法时,同时声明了一个或者多个类型变量(如: ),称为泛型类、泛型接口,泛型方法、它们统称为泛型。

1
2
public class ArrayList<E>{
}

当别人使用ArrayList集合创建对象时,new ArrayList<String> 就表示元素为String类型,new ArrayList<Integer>表示元素为Integer类型。

  • 作用:泛型提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力!这样可以避免强制类型转换及其可能出现的异常。
  • 泛型的好处:在编译阶段可以避免出现一些非法的数据。

  • 泛型的本质:把具体的数据类型传递给类型变量。

泛型类

修饰符class类名<类型变量,类型变量,.…>{
}

public class ArrayList{
}

注意:类型变量建议用大写的英文字母,常用的有:E、T、K、V等

泛型接口

1
2
3
4
//这里的类型变量,一般是一个字母,比如<E>
public interface 接口名<类型变量>{

}

注意:类型变量建议用大写的英文字母,常用的有:E、T、K、V等

首先我们得有一个学生类和老师类

1
2
3
public class Teacher{

}
1
2
3
public class Student{

}

我们定义一个Data<T>泛型接口,T表示接口中要处理数据的类型。

1
2
3
4
5
public interface Data<T>{
public void add(T t);

public ArrayList<T> getByName(String name);
}

接下来,我们写一个处理Teacher对象的接口实现类

1
2
3
4
5
6
7
8
9
10
11
//此时确定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对象的接口实现类

1
2
3
4
5
6
7
8
9
10
11
//此时确定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){

}
}

在实际工作中,一般也都是框架底层源代码把泛型接口写好,我们实现泛型接口就可以了。

泛型方法

1
2
3
public <泛型变量,泛型变量> 返回值类型 方法名(形参列表){

}

通配符
泛型限定的意思是对泛型的数据类型进行范围的限制。有如下的三种格式

  • 表示任意类型
  • 表示指定类型或者指定类型的子类
  • 表示指定类型或者指定类型的父类

泛型擦除

最后,关于泛型还有一个特点需要给同学们介绍一下,就是泛型擦除。什么意思呢?也就是说泛型只能编译阶段有效,一旦编译成字节码,字节码中是不包含泛型的。而且泛型只支持引用数据类型,不支持基本数据类型。

泛型的注意事项。

  1. 泛型是工作在编译阶段的,一旦程序编译成class文件,class文件中就不存在泛型了,这就是泛型擦除。

  2. 泛型不支持基本数据类型,只能支持对象类型(引用数据类型)。

API(2)

API(Application Programming interface)意思是应用程序编程接口,说人话就是Java帮我们写好的一些程序,如:类、方法等,我们直接拿过来用就可以解决一些问题。

Object类、toString、equals方法、对象克隆clone

object类的作用:
object类是Java中所有类的祖宗类,因此,Java中所有类的对象都可以直接使用Object类中提供的一些方法。

Object类的常见方法
方法名 说明
public String toString() 返回对象的字符串表示形式。
public boolean equals(Object o) 判断两个对象是否相等。
protected object clone() 对象克隆

  • toString()方法
1
2
3
public String toString()
调用toString()方法可以返回对象的字符串表示形式。
默认的格式是:“包名.类名@哈希值16进制”

假设有一个学生类如下

1
2
3
4
5
6
7
8
9
public class Student{
private String name;
private int age;

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

再定义一个测试类

1
2
3
4
5
6
public class Test{
public static void main(String[] args){
Student s1 = new Student("赵敏",23);
System.out.println(s1.toString());
}
}

如果,在Student类重写toString()方法,那么我们可以返回对象的属性值,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Student{
private String name;
private int age;

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

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

equals(Object o)方法

接下来,我们学习一下Object类的equals方法

1
2
public boolean equals(Object o)
判断此对象与参数对象是否"相等"

我们写一个测试类,测试一下

1
2
3
4
5
6
7
8
9
10
11
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
}
}

但是如果我们在Student类中,把equals方法重写了,就按照对象的属性值进行比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Student{
private String name;
private int age;

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

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

//重写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;
return name != null ? name.equals(student.name) : student.name == null;
}
}

总结一下Object的toString方法和equals方法

1
2
3
4
5
6
7
public String toString()
返回对象的字符串表示形式。默认的格式是:“包名.类名@哈希值16进制”
【子类重写后,返回对象的属性值】

public boolean equals(Object o)
判断此对象与参数对象是否"相等"。默认比较对象的地址值,和"=="没有区别
【子类重写后,比较对象的属性值】

clone() 方法

接下来,我们学习Object类的clone()方法,克隆。意思就是某一个对象调用这个方法,这个方法会复制一个一模一样的新对象,并返回。

1
2
public Object clone()
克隆当前对象,返回一个新对象

想要调用clone()方法,必须让被克隆的类实现Cloneable接口。如我们准备克隆User类的对象,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class User implements Cloneable{
private String id; //编号
private String username; //用户名
private String password; //密码
private double[] scores; //分数

public User() {
}

public User(String id, String username, String password, double[] scores) {
this.id = id;
this.username = username;
this.password = password;
this.scores = scores;
}

//...get和set...方法自己加上

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

接着,我们写一个测试类,克隆User类的对象。并观察打印的结果

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
User u1 = new User(1,"zhangsan","wo666",new double[]{99.0,99.5});
//调用方法克隆得到一个新对象
User u2 = (User) u1.clone();
System.out.println(u2.getId());
System.out.println(u2.getUsername());
System.out.println(u2.getPassword());
System.out.println(u2.getScores());
}
}

我们发现,克隆得到的对象u2它的属性值和原来u1对象的属性值是一样的。

上面演示的克隆方式,是一种浅克隆的方法,浅克隆的意思:拷贝出来的对象封装的数据与原对象封装的数据一模一样(引用类型拷贝的是地址值)

下面演示一下深拷贝User对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class User implements Cloneable{
private String id; //编号
private String username; //用户名
private String password; //密码
private double[] scores; //分数

public User() {
}

public User(String id, String username, String password, double[] scores) {
this.id = id;
this.username = username;
this.password = password;
this.scores = scores;
}

//...get和set...方法自己加上

@Override
protected Object clone() throws CloneNotSupportedException {
//先克隆得到一个新对象
User u = (User) super.clone();
//再将新对象中的引用类型数据,再次克隆
u.scores = u.scores.clone();
return u;
}
}
  1. Object中toString方法的作用是什么?存在的意义是什么?
  • 基本作用:返回对象的字符串形式。
  • *存在的意义:让子类重写,以便返回子类对象的内容。
  1. Object中equals方法的作用是什么?存在的意义是什么?
  • 基本作用:默认是比较两个对象的地址是否相等。
  • 存在的意义:让子类重写,以便用于比较对象的内容是否相同。
  • Object类提供的对象克隆方法
    protected object clone() 对象克隆
    当某个对象调用这个方法时,这个方法会复制一个一模一样的新对象返回。

  • 浅克隆
    拷贝出的新对象,与原对象中的数据一模一样(引用类型拷贝的只是地址)

  • 深克隆
    对象中基本类型的数据直接拷贝。
    对象中的字符串数据拷贝的还是地址。
    对象中还包含的其他对象,不会拷贝地址,会创建新对象。

Objects类

Objects是一个工具类,提供了很多操作对象的静态方法给我们使用。objects类的常见方法
方法名 说明
public static boolean equals(Object a,Object b) 先做非空判断,再比较两个对象
public static boolean isNull(Object obj) 判断对象是否为nul1,为null返回true ,反之
public static boolean nonNull(Object obj) 判断对象是否不为null,不为null则返回true,反之

下面写代码演示一下这几个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
}
}

包装类

创建包装类对象

我们先来学习,创建包装类对象的方法,以及包装类的一个特性叫自动装箱和自动拆箱。我们以Integer为例,其他的可以自己学,都是类似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//1.创建Integer对象,封装基本类型数据10
Integer a = new Integer(10);

//2.使用Integer类的静态方法valueOf(数据)
Integer b = Integer.valueOf(10);

//3.还有一种自动装箱的写法(意思就是自动将基本类型转换为引用类型)
Integer c = 10;

//4.有装箱肯定还有拆箱(意思就是自动将引用类型转换为基本类型)
int d = c;

//5.装箱和拆箱在使用集合时就有体现
ArrayList<Integer> list = new ArrayList<>();
//添加的元素是基本类型,实际上会自动装箱为Integer类型
list.add(100);
//获取元素时,会将Integer类型自动拆箱为int类型
int e = list.get(0);

包装类数据类型转换

在开发中,经常使用包装类对字符串和基本类型数据进行相互转换。

  • 把字符串转换为数值型数据:包装类.parseXxx(字符串)
1
2
public static int parseInt(String s)
把字符串转换为基本数据类型
  • 将数值型数据转换为字符串:包装类.valueOf(数据);
1
2
public static String valueOf(int a)
把基本类型数据转换为
  • 写一个测试类演示一下
1
2
3
4
5
6
7
8
9
10
11
12
13
//1.字符串转换为数值型数据
String ageStr = "29";
int age1 = Integer.parseInt(ageStr);

String scoreStr = 3.14;
double score = Double.prarseDouble(scoreStr);

//2.整数转换为字符串,以下几种方式都可以(挑中你喜欢的记一下)
Integer a = 23;
String s1 = Integer.toString(a);
String s2 = a.toString();
String s3 = a+"";
String s4 = String.valueOf(a);
    1. 创建包装类的对象方式、自动装箱和拆箱的特性;
    1. 利用包装类提供的方法对字符串和基本类型数据进行相互转换

StringBuilder、StringBuffer、StringJoiner

StringBuilder方法演示

接下来我们用代码演示一下StringBuilder的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test{
public static void main(String[] args){
StringBuilder sb = new StringBuilder("itehima");

//1.拼接内容
sb.append(12);
sb.append("黑马");
sb.append(true);

//2.append方法,支持临时编程
sb.append(666).append("黑马2").append(666);
System.out.println(sb); //打印:12黑马666黑马2666

//3.反转操作
sb.reverse();
System.out.println(sb); //打印:6662马黑666马黑21

//4.返回字符串的长度
System.out.println(sb.length());

//5.StringBuilder还可以转换为字符串
String s = sb.toString();
System.out.println(s); //打印:6662马黑666马黑21
}
}
  • StringBuffer与StringBuilder
    注意:
    StringBuffer的用法与StringBuilder是一模一样的
    但StringBuilder是线程不安全的StringBuffer是线程安全的

  • StringJoiner

下面演示一下StringJoiner的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test{
public static void main(String[] args){
StringJoiner s = new StringJoiner(",");
s.add("java1");
s.add("java2");
s.add("java3");
System.out.println(s); //结果为: java1,java2,java3

//参数1:间隔符
//参数2:开头
//参数3:结尾
StringJoiner s1 = new StringJoiner(",","[","]");
s1.add("java1");
s1.add("java2");
s1.add("java3");
System.out.println(s1); //结果为: [java1,java2,java3]
}
}

Math、System、Runtime

  • Math是数学的意思,该类提供了很多个进行数学运算的方法,如求绝对值,求最大值,四舍五入等,话不多说,直接上代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MathTest {
public 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类,这是系统类,提供了一些获取获取系统数据的方法。比如获取系统时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 目标:了解下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");
}
}

  • 接下来,我们再学习一个Java的运行时类,叫Runtime类。这个类可以用来获取JVM的一些信息,也可以用这个类去执行其他的程序。话不多少,上代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 目标:了解下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");
Thread.sleep(5000); // 让程序在这里暂停5s后继续往下走!!
p.destroy(); // 销毁!关闭程序!
}
}

BigDecimal

为了解决计算精度损失的问题,Java给我们提供了BigDecimal类,它提供了一些方法可以对数据进行四则运算,而且不丢失精度,同时还可以保留指定的小数位。下面看代码,演示一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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 augend): 加法
BigDecimal c1 = a1.add(b1);
System.out.println(c1);

// 3、public BigDecimal subtract(BigDecimal augend): 减法
BigDecimal c2 = a1.subtract(b1);
System.out.println(c2);

// 4、public BigDecimal multiply(BigDecimal augend): 乘法
BigDecimal c3 = a1.multiply(b1);
System.out.println(c3);

// 5、public BigDecimal divide(BigDecimal b): 除法
BigDecimal c4 = a1.divide(b1);
System.out.println(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(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);
}
}

  1. BigDecimal的作用是什么?
    解决浮点型运算时,出现结果失真的问题。

  2. 应该如何把浮点型转换成BigDecimal的对象?
    BigDecimal b1 = BigDecimal.valueOf(0.1)

传统时间、Date日期类、SimpleDataFormat

Date对象记录的时间是用毫秒值来表示的。Java语言规定,1970年1月1日0时0分0秒认为是时间的起点,此时记作0,那么1000(1秒=1000毫秒)就表示1970年1月1日0时0分1秒,依次内推。

  • 创建Date对象:

使用new Date(),这将创建一个代表当前时间的Date对象。
使用new Date(long millis),通过传递一个毫秒值来创建一个特定的Date对象。

  • 获取时间毫秒值:

调用Date对象的getTime()方法,它将返回Date对象表示的毫秒值。
使用System.currentTimeMillis(),直接获取当前时间的毫秒值,而无需创建Date对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}
}
  • 我们把Date对象转换为指定格式的日期字符串这个操作,叫做日期格式化,

  • 反过来把指定格式的日期符串转换为Date对象的操作,叫做日期解析。

注意:创建SimpleDateFormat对象时,在构造方法的参数位置传递日期格式,而日期格式是由一些特定的字母拼接而来的。我们需要记住常用的几种日期/时间格式

1
2
3
4
5
6
7
8
9
10
11
12
字母	   表示含义
yyyy 年
MM 月
dd 日
HH 时
mm 分
ss 秒
SSS 毫秒

"2022年12月12日" 的格式是 "yyyy年MM月dd日"
"2022-12-12 12:12:12" 的格式是 "yyyy-MM-dd HH:mm:ss"
按照上面的格式可以任意拼接,但是字母不能写错

最后,上代码演示一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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、格式化日期对象,和时间 毫秒值。
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);
}
}

日期格式化&解析案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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("小皮您秒杀失败了~~");
}
}
}

Calendar

Calendar类表示日历,它提供了一些比Date类更好用的方法。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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);

// 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, 11, 22);
System.out.println(now);
}
}

JDK8开始新增的日期、时间

  • 为什么要学JDK8新增的时间?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 目标:搞清楚为什么要用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);

Calendar c = Calendar.getInstance();
int year = c.get(Calendar.YEAR);
System.out.println(year);

// 2、都是可变对象,修改后会丢失最开始的时间信息。

// 3、线程不安全。

// 4、不能精确到纳秒,只能精确到毫秒。
// 1秒 = 1000毫秒
// 1毫秒 = 1000微妙
// 1微妙 = 1000纳秒
}
}

LocalDate、LocalTime. LocalDateTime

LocalDate:代表本地日期(年、月、日、星期)
LocalTime:代表本地时间(时、分、秒、纳秒)
LocalDateTime:代表本地日期、时间(年、月、日、星期、时、分、秒、纳秒)

  • 它们获取对象的方案
    方法名 示例
    public static xxxx now(): 获取系统当前时间对应的该对象

LocalDate ld = LocalDate.now();
LocalTime lt = LocalTime.now();
LocalDateTime ldt = LocalDateTime.now();

  • LocalDate类的基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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类的基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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类的基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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);

}
}

zoneld、ZoneDateTime

  • 我们学习代表时区的两个类。由于世界各个国家与地区的经度不同,各地区的时间也有所不同,因此会划分为不同的时区。每一个时区的时间也不太一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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

作用:可以用来记录代码的执行时间,或用于记录用户操作某个事件的时间点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 目标:掌握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

  • 接下来,我们学习一个新增的日期格式化类,叫DateTimeFormater。它可以从来对日期进行格式化和解析。它代替了原来的SimpleDateFormat类。

接下来,将上面的方法用代码来演示一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 目标:掌握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);
}
}

Duration、Period

  • 其中Period用来计算日期间隔(年、月、日),Duration用来计算时间间隔(时、分、秒、纳秒)

  • 先来演示Period类的用法,它的方法如下图所示。可以用来计算两个日期之间相隔的年、相隔的月、相隔的日。只能两个计算LocalDate对象之间的间隔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 目标:掌握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());
}
}
  • 接下来,我们学习Duration类。它是用来表示两个时间对象的时间间隔。可以用于计算两个时间对象相差的天数、小时数、分数、秒数、纳秒数;支持LocalTime、LocalDateTime、Instant等时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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. Period有啥作用?
  • 可以用于计算两个LocalDate对象相差的年数、月数、天数。
  1. Duration有啥作用?
  • 可以用于计算两个时间对象相差的天数、小时数、分数、秒数、纳秒数,支持
    LocalTime.LocalDateTime.lnstant等时间。

Arrays

我们先认识一下Arrays是干什么用的,Arrays是操作数组的工具类,它可以很方便的对数组中的元素进行遍历、拷贝、排序等操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 目标:掌握Arrays类的常用方法。
*/
public class ArraysTest1 {
public static void main(String[] args) {
// 1、public static String toString(类型[] arr): 返回数组的内容
int[] arr = {10, 20, 30, 40, 50, 60};
System.out.println(Arrays.toString(arr));

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

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

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

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

自定义排序规则Comparable

Arrays根本就不知道按照什么规则进行排序。为了让Arrays知道按照什么规则排序,我们有如下的两种办法。

  • 排序方式1:让Student类实现Comparable接口,同时重写compareTo方法。Arrays的sort方法底层会根据compareTo方法的返回值是正数、负数、还是0来确定谁大、谁小、谁相等。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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来确定谁大、谁小、谁相等。代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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));
}
}
  • 方式一:让该对象的类实现Comparable(比较规则)接口,然后重写compareTo方法,自己来制定比较规则。

  • 方式二∶使用下面这个sort方法,创建Comparator比较器接口的匿名内部类对象,然后自己制定比较规则。

    public static void sort(T[]arr, Comparator<? super T>c)对数组进行排序(支持自定义排序规则)

  • 自定义排序规则时,需要遵循的官方约定如下:
  • 如果认为左边对象大于右边对象应该返回正整数
  • 如果认为左边对象小于右边对象应该返回负整数
  • 如果认为左边对象等于右边对象应该返回0整数

Lambda表达式

Lambda表达式

  • Lambda表达式是JDK8开始新增的一种语法形式;作用:用于简化匿名内部类代码的书写。
  • 格式
    1
    2
    3
    (被重写方法的形参列表) -> {
    被重写方法的方法体代码;
    }
    需要给说明一下的是,在使用Lambda表达式之前,必须先有一个接口,而且接口中只能有一个抽象方法。(注意:不能是抽象类,只能是接口)
  • 注意:Lambda表达式只能简化函数式接口的匿名内部类!!!
  • 什么是函数式接口?
  • 有且仅有一个抽象方法的接口。
  • 注意:将来我们见到的大部分函数式接口,上面都可能会有一个@Functionalnterface的注解,有该注解的接就必定是函数式接口

Lambda表达式的省略写法(进一步简化Lambda表达式的写法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1.Lambda的标准格式
(参数类型1 参数名1, 参数类型2 参数名2)->{
...方法体的代码...
return 返回值;
}

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

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

4.如果()里面只有一个参数,则()可以省略
(参数名)->结果
  • 参数类型可以省略不写。
  • 如果只有一个参数,参数类型可以省略,同时()也可以省略。
  • 如果Lambda表达式中的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号!此时,如果这行代码是return语句,也必须去掉return不写。

方法引用

静态方法的引用

  • 类名∵静态方法。
  • 使用场景
    如果某个Lambda表达式里只是调用一个静态方法,并且前后参数的形式一致,就可以使用静态方法引用。

我们先学习静态方法的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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表达式的方法体代码;

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

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

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

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

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

实例方法的引用

  • 对象名∵:实例方法。
  • 使用场景
    如果某个Lambda表达式里只是调用一个实例方法,并且前后参数的形式一致,就可以使用实例方法引用。

还是基于上面的案例,我们现在来学习一下实例方法的引用。现在,我想要把下图中Lambda表达式的方法体,用一个实例方法代替。

1
2
CompareByData compare = new CompareByData();
Arrays.sort(students, (o1, o2) -> compare.compareByAgeDesc(o1, o2)); // 降序

最后,再将Lambda表达式的方法体,直接改成方法引用写法。实际上就是用类名调用方法,但是省略的参数。这就是实例方法引用

1
2
CompareByData compare = new CompareByData();
Arrays.sort(students, compare::compareByAgeDesc); // 降序

特定类型方法的引用

1
2
3
4
Java约定:
如果某个Lambda表达式里只是调用一个实例方法,并且前面参数列表中的第一个参数作为方法的主调, 后面的所有参数都是作为该实例方法的入参时,则就可以使用特定类型的方法引用。
格式:
类型::方法名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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));
}
}

构造器引用

  • 类名::new。
  • 使用场景
    如果某个Lambda表达式里只是在创建对象,并且前后参数情况一致,就可以使用构造器引用。

我们学习最后一种方法引用的形式,叫做构造器引用。还是先说明一下,构造器引用在实际开发中应用的并不多,目前还没有找到构造器的应用场景。所以大家在学习的时候,也只是关注语法就可以了。

现在,我们准备一个JavaBean类,Car类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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类型

1
2
3
interface CreateCar{
Car create(String name, double price);
}

最后,再准备一个测试类,在测试类中创建CreateCar接口的实现类对象,先用匿名内部类创建、再用Lambda表达式创建,最后改用方法引用创建。同学们只关注格式就可以,不要去想为什么(语法就是这么设计的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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、使用匿名内部类改进
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);
}
}