JAVA-面向对象

第06章_面向对象编程(基础)

学习面向对象内容的三条主线

• Java类及类的成员:(重点)属性、方法、构造器;(熟悉)代码块、内部类

• 面向对象的特征:封装、继承、多态、(抽象)

• 其他关键字的使用:this、super、package、import、static、final、interface、abstract等

1. 面向对象编程概述(了解)

1.1 程序设计的思路

面向对象,是软件开发中的一类编程风格、开发范式。除了面向对象,还有面向过程、指令式编程和函数式编程。在所有的编程范式中,我们接触最多的还是面向过程和面向对象两种。

类比:史书类型

• 纪传体:以人物传记为中心,“本纪”叙述帝王,“世家”记叙王侯封国和特殊人物,“列传”记叙民间人物。

• 编年体:按年、月、日顺序编写。

• 国别体:是一部分国记事的历史散文,分载多国历史。

早期先有面向过程思想,随着软件规模的扩大,问题复杂性的提高,面向过程的弊端越来越明显,出现了面向对象思想并成为目前主流的方式。

1. 面向过程的程序设计思想(Process-Oriented Programming),简称POP

关注的焦点是过程:过程就是操作数据的步骤。如果某个过程的实现代码重复出现,那么就可以把这个过程抽取为一个函数。这样就可以大大简化冗余代码,便于维护。

典型的语言:C语言

代码结构:以函数为组织单位。

是一种“执行者思维”,适合解决简单问题。扩展能力差、后期维护难度较大。

2. 面向对象的程序设计思想( Object Oriented Programming),简称OOP

关注的焦点是类:在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽象出来,用类来表示。

典型的语言:Java、C#、C++、Python、Ruby和PHP等

代码结构:以类为组织单位。每种事物都具备自己的属性和行为/功能。

是一种“设计者思维”,适合解决复杂问题。代码扩展性强、可维护性高。

1.2 由实际问题考虑如何设计程序

可随着需求的更改,功能的增多,发现需要面对每一个步骤很麻烦了,这时就开始思索,能不能把这些步骤和功能进行封装,封装时根据不同的功能,进行不同的封装,功能类似的封装在一起。这样结构就清晰了很多。用的时候,找到对应的类就可以了。这就是面向对象的思想。

类比举例2:人把大象装进冰箱

面向过程:

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
//分为三个类
人{
打开(冰箱){
冰箱.开门();
}
操作(大象){
大象.进入(冰箱);
}
关闭(冰箱){
冰箱.关门();
}
}

冰箱{
开门(){ }
关门(){ }
}

大象{
进入(冰箱){
}
}

2. Java语言的基本元素:类和对象

2.1 引入

人认识世界,其实就是面向对象的。比如,我们认识一下美人鱼(都没见过)

image-20220520162643547

经过“仔细学习”,发现美人鱼通常具备一些特征:

• 女孩

• 有鱼尾

• 美丽

这个总结的过程,其实是抽象化的过程。抽象出来的美人鱼的特征,可以归纳为一个美人鱼类。而图片中的都是这个类呈现出来的具体的对象。

2.2 类和对象概述

类(Class)和对象(Object)是面向对象的核心概念。

1**、什么是类**

:具有相同特征的事物的抽象描述,是抽象的、概念上的定义。

2**、什么是对象**

对象:实际存在的该类事物的每个个体,是具体的,因而也称为实例(instance)。

img

可以理解为:类 => 抽象概念的人;对象 => 实实在在的某个人

img

img

3**、类与对象的关系错误理解**

曰:“白马非马,可乎?”
曰:“可。”
曰:“何哉?”
曰:“马者,所以命形也。白者,所以命色也。命色者,非命形也,故曰白马非马。”

img

2.3 类的成员概述

面向对象程序设计的重点是类的设计

Java中用类class来描述事物也是如此。类,是一组相关属性和行为的集合,这也是类最基本的两个成员。

属性:该类事物的状态信息。对应类中的成员变量

成员变量 <=> 属性 <=> Field

行为:该类事物要做什么操作,或者基于事物的状态能做什么。对应类中的成员方法

(成员)方法 <=> 函数 <=> Method

20220319_211611

举例:

img

2.4 面向对象完成功能的三步骤(重要)

步骤1:类的定义

1
2
3
4
5
6
类的定义使用关键字:class。格式如下:

[修饰符] class 类名{
属性声明;
方法声明;
}
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
举例1:
public class Person{
*//**声明属性age*
int age ;

*//**声明方法showAge()*
public void eat() {
System.out.println("人吃饭");
}
}

举例2:

public class Dog{
*//**声明属性*
String type; *//**种类*
String nickName; *//**昵称*
String hostName; *//**主人名称*

*//**声明方法*
public void eat(){ *//**吃东西*
System.out.println("狗狗进食");
}
}

public class Person{
String name;
char gender;
Dog dog;

*//**喂宠物*
public void feed(){
dog.eat();
}
}

步骤2:对象的创建

image-20220319213201568

创建对象,使用关键字:new

创建对象语法:

1
2
3
4
5
6
方式1:给创建的对象命名*
把创建的对象用一个引用数据类型的变量保存起来,这样就可以反复使用这个对象了*
类名 对象名 = new 类名();

方式2:
new 类名()*//**也称为匿名对象

举例:

1
2
3
4
5
6
7
8
class PersonTest{
public static void main(String[] args){
*//**创建Person**类的对象*
Person per = new Person();
*//**创建Dog**类的对象*
Dog dog = new Dog();
}
}

步骤3:对象调用属性或方法

对象是类的一个实例,必然具备该类事物的属性和行为(即方法)。

使用”对象名.属性” 或 “对象名.方法”的方式访问对象成员(包括属性和方法)

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
//**声明Animal**类*
public class Animal { *//**动物类*
public int legs;

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

public void move() {
System.out.println("Move.");
}
}
1
2
3
4
5
6
7
8
9
10
11
//声明测试类
public class AnimalTest {
public static void main(String args[]) {
*//**创建对象*
Animal xb = new Animal();
xb.legs = 4;*//**访问属性*
System.out.println(xb.legs);
xb.eat();*//**访问方法*
xb.move();*//**访问方法*
}
}

图示理解:标题: fig:

举例2:针对前面步骤1的举例2:类的实例化(创建类的对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Game{
public static void main(String[] args){
Person p = new Person();
*//**通过Person**对象调用属性*
p.name = "康师傅";
p.gender = '男';
p.dog = new Dog(); *//**给Person**对象的dog**属性赋值*
*//**给Person**对象的dog**属性的type**、nickname**属性赋值*
p.dog.type = "柯基犬";
p.dog.nickName = "小白";

*//**通过Person**对象调用方法*
p.feed();
}
}

2.5 匿名对象 (anonymous object)

我们也可以不定义对象的句柄,而直接调用这个对象的方法。这样的对象叫做匿名对象。

– 如:new Person().shout();

使用情况

– 如果一个对象只需要进行一次方法调用,那么就可以使用匿名对象。

– 我们经常将匿名对象作为实参传递给一个方法调用。

3. 对象的内存解析

3.1 JVM内存结构划分

HotSpot Java虚拟机的架构图如下。其中我们主要关心的是运行时数据区部分(Runtime Data Area)。

img

其中:

堆(Heap):此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。

栈(Stack):是指虚拟机栈。虚拟机栈用于存储局部变量等。局部变量表存放了编译期可知长度的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,是对象在堆内存的首地址)。 方法执行完,自动释放。

方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

3.2 对象内存解析

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person { *//**类:人*
String name;
int age;
boolean isMale;
}

public class PersonTest { *//**测试类*
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "赵同学";
p1.age = 20;
p1.isMale = true;

Person p2 = new Person();
p2.age = 10;

Person p3 = p1;
p3.name = "郭同学";

}
}

内存解析图:

image-20220319215723183

说明:

堆:凡是new出来的结构(对象、数组)都放在堆空间中。

对象的属性存放在堆空间中。

创建一个类的多个对象(比如p1、p2),则每个对象都拥有当前类的一套”副本”(即属性)。当通过一个对象修改其属性时,不会影响其它对象此属性的值。

当声明一个新的变量使用现有的对象进行赋值时(比如p3 = p1),此时并没有在堆空间中创建新的对象。而是两个变量共同指向了堆空间中同一个对象。当通过一个对象修改属性时,会影响另外一个对象对此属性的调用。

面试题:对象名中存储的是什么呢?

答:对象地址

1
2
3
4
5
6
7
8
9
10
11
12
public class StudentTest{
public static void main(String[] args){
System.out.println(new Student());*//Student@7852e922*

Student stu = new Student();
System.out.println(stu);*//Student@4e25154f*

int[] arr = new int[5];
System.out.println(arr);*//[I@70dea4e*

}
}

直接打印对象名和数组名都是显示“类型@对象的hashCode值”,所以说类、数组都是引用数据类型,引用数据类型的变量中存储的是对象的地址,或者说指向堆中对象的首地址。

img

3.3 练习

根据代码,画出内存图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Car {
String color = "red";
int num = 4;

void show() {
System.out.println("color=" + color + ",num=" + num);
}
}

class CarTest {
public static void main(String[] args) {
Car c1 = new Car(); *//**建立对象c1*
Car c2 = new Car(); *//**建立对象c2*
c1.color = "blue"; *//**对对象的属性进行修改*
c1.show(); *//**使用对象的方法*
c2.show();
}
}

4. 类的成员之一:成员变量(field)

4.1 如何声明成员变量

语法格式:

1
2
3
[修饰符1] class 类名{
[修饰符2] 数据类型 成员变量名 [= 初始化值];
}

说明:

– 位置要求:必须在类中,方法外

– 修饰符2(暂不考虑)

• 常用的权限修饰符有:private、缺省、protected、public

• 其他修饰符:static、final

– 数据类型

• 任何基本数据类型(如int、Boolean) 或 任何引用数据类型。

– 成员变量名

• 属于标识符,符合命名规则和规范即可。

– 初始化值

• 根据情况,可以显式赋值;也可以不赋值,使用默认值

示例:

1
2
3
4
public class Person{
private int age; *//**声明private**变量 age*
public String name = “Lila”; *//**声明public**变量 name*
}

4.2 成员变量 vs 局部变量

1**、变量的分类:成员变量与局部变量**

在方法体外,类体内声明的变量称为成员变量。

在方法体内部等位置声明的变量称为局部变量。

image-20220511101608038

image-20220319230744617

其中,static可以将成员变量分为两大类,静态变量和非静态变量。其中静态变量又称为类变量,非静态变量又称为实例变量或者属性。接下来先学习实例变量。

2**、成员变量 与 局部变量 的对比**

相同点:

– 变量声明的格式相同: 数据类型 变量名 = 初始化值

– 变量必须先声明、后初始化、再使用。

– 变量都有其对应的作用域。只在其作用域内是有效的

不同点:

1、声明位置和方式 (1)实例变量:在类中方法外 (2)局部变量:在方法体{}中或方法的形参列表、代码块中

2、在内存中存储的位置不同 (1)实例变量:堆 (2)局部变量:栈

3、生命周期 (1)实例变量:和对象的生命周期一样,随着对象的创建而存在,随着对象被GC回收而消亡, 而且每一个对象的实例变量是独立的。 (2)局部变量:和方法调用的生命周期一样,每一次方法被调用而在存在,随着方法执行的结束而消亡, 而且每一次方法调用都是独立。

4、作用域 (1)实例变量:通过对象就可以使用,本类中直接调用,其他类中“对象.实例变量” (2)局部变量:出了作用域就不能使用

5、修饰符(后面来讲) (1)实例变量:public,protected,private,final,volatile,transient等 (2)局部变量:final

6、默认值 (1)实例变量:有默认值 (2)局部变量:没有,必须手动初始化。其中的形参比较特殊,靠实参给它初始化。

3**、对象属性的默认初始化赋值**

当一个对象被创建时,会对其中各种类型的成员变量自动进行初始化赋值。

image-20220319231821747

4、举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {*//**人类*
*//1.**属性*
String name;*//**姓名*
int age = 1;*//**年龄*
boolean isMale;*//**是否是男性*

public void show(String nation) {
*//nation:**局部变量*
String color;*//color:**局部变量*
color = "yellow";
}
}

*//**测试类*
class PersonTest {
public static void main(String[] args) {
Person p = new Person();
p.show("CHN");
}
}

image-20220319231703240

1. 类的成员之二:方法(method)

5.1 方法的引入

img

《街霸》游戏中,每次人物出拳、出脚或跳跃等动作都需要编写50-80行的代码,在每次出拳、出脚或跳跃的地方都需要重复地编写这50-80行代码,这样程序会变得很臃肿,可读性也非常差。为了解决代码重复编写的问题,可以将出拳、出脚或跳跃的代码提取出来放在一个{}中,并为这段代码起个名字,这样在每次的出拳、出脚或跳跃的地方通过这个名字来调用这个{}的代码就可以了。

上述过程中,所提取出来的代码可以被看作是程序中定义的一个方法,程序在需要出拳、出脚或跳跃时调用该方法即可。

5.2 方法(method、函数)的理解

方法是类或对象行为特征的抽象,用来完成某个功能操作。在某些语言中也称为函数或过程。

将功能封装为方法的目的是,可以实现代码重用,减少冗余,简化代码

Java里的方法不能独立存在,所有的方法必须定义在类里。

举例1:

1
2
3
4
5
6
7
8
9
–     Math.random()的random()方法

– Math.sqrt(x)的sqrt(x)方法

– System.out.println(x)的println(x)方法

– new Scanner(System.in).nextInt()的nextInt()方法

– Arrays类中的binarySearch()方法、sort()方法、equals()方法

举例2:

1
2
3
4
5
6
7
8
9
  public class Person{
private int age;
public int getAge() { *//**声明方法getAge()*
return age;
}
public void setAge(int i) { *//**声明方法setAge*
age = i; *//**将参数i**的值赋给类的成员变量age*
}
}

5.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
1****、声明方法的语法格式**

[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表]{
方法体的功能代码
}

(1)一个完整的方法 = 方法头 + 方法体。

方法头就是[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表],也称为方法签名。通常调用方法时只需要关注方法头就可以,从方法头可以看出这个方法的功能和调用格式。

方法体就是方法被调用后要执行的代码。对于调用者来说,不了解方法体如何实现的,并不影响方法的使用。

**(2)方法头可能包含5个部分**

**修饰符**:可选的。方法的修饰符也有很多,例如:public、protected、private、static、abstract、native、final、synchronized等,后面会一一学习。

– 其中,权限修饰符有public、protected、private。在讲封装性之前,我们先默认使用pulbic修饰方法。

– 其中,根据是否有static,可以将方法分为静态方法和非静态方法。其中静态方法又称为类方法,非静态方法又称为实例方法。咱们在讲static前先学习实例方法。

**返回值类型**: 表示方法运行的结果的数据类型,方法执行后将结果返回到调用者。

– 无返回值,则声明:void

– 有返回值,则声明出返回值类型(可以是任意类型)。与方法体中“return 返回值”搭配使用

**方法名**:属于标识符,命名时遵循标识符命名规则和规范,“见名知意”

**形参列表**:表示完成方法体功能时需要外部提供的数据列表。可以包含零个,一个或多个参数。

– 无论是否有参数,()不能省略

– 如果有参数,每一个参数都要指定数据类型和参数名,多个参数之间使用逗号分隔,例如:

• 一个参数: (数据类型 参数名)

• 二个参数: (数据类型1 参数1, 数据类型2 参数2)

– 参数的类型可以是基本数据类型、引用数据类型

**throws 异常列表**:可选,在【第09章-异常处理】章节再讲

**(3)方法体**:方法体必须有{}括起来,在{}中编写完成方法功能的代码

**(4)关于方法体中return语句的说明:**

return语句的作用是结束方法的执行,并将方法的结果返回去

如果返回值类型不是void,方法体中必须保证一定有 return 返回值; 语句,并且要求该返回值结果的类型与声明的返回值类型一致或兼容。

如果返回值类型为void时,方法体中可以没有return语句,如果要用return语句提前结束方法的执行,那么return后面不能跟返回值,直接写return ; 就可以。

return语句后面就不能再写其他代码了,否则会报错:Unreachable code

补充:方法的分类:按照是否有形参及返回值

image-20220320000047155

2、类比举例

image-20220503102323689

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
package com.atguigu.test04.method;

*/***
*** *方法定义案例演示*
**/*
public class MethodDefineDemo {
*/***
*** *无参无返回值方法的演示*
**/*
public void sayHello(){
System.out.println("hello");
}

*/***
*** *有参无返回值方法的演示*
** @*param length int *第一个参数,表示矩形的长*
*** *@*param width int *第二个参数,表示矩形的宽*
*** *@*param sign char *第三个参数,表示填充矩形图形的符号*
**/*
public void printRectangle(int length, int width, char sign){
for (int i = 1; i <= length ; i++) {
for(int j=1; j <= width; j++){
System.out.print(sign);
}
System.out.println();
}
}

*/***
*** *无参有返回值方法的演示*
** @*return
**/*
public int getIntBetweenOneToHundred(){
return (int)(Math.random()*100+1);
}

*/***
*** *有参有返回值方法的演示*
** @*param a int *第一个参数,要比较大小的整数之一*
*** *@*param b int *第二个参数,要比较大小的整数之二*
*** *@*return int *比较大小的两个整数中较大者的值*
**/*
public int max(int a, int b){
return a > b ? a : b;
}
}

5.4 如何调用实例方法

方法通过方法名被调用,且只有被调用才会执行。

1、方法调用语法格式

1
对象.方法名([实参列表])

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
45
举例1:

package com.atguigu.test04.method;

*/***
*** *方法调用案例演示*
**/*
public class MethodInvokeDemo {
public static void main(String[] args) {
*//**创建对象*
MethodDefineDemo md = new MethodDefineDemo();

System.out.println("-----------------------方法调用演示-------------------------");

*//**调用MethodDefineDemo**类中无参无返回值的方法sayHello*
md.sayHello();
md.sayHello();
md.sayHello();
*//**调用一次,执行一次,不调用不执行*

System.out.println("------------------------------------------------");
*//**调用MethodDefineDemo**类中有参无返回值的方法printRectangle*
md.printRectangle(5,10,'@');

System.out.println("------------------------------------------------");
*//**调用MethodDefineDemo**类中无参有返回值的方法getIntBetweenOneToHundred*
md.getIntBetweenOneToHundred();*//**语法没问题,就是结果丢失*

int num = md.getIntBetweenOneToHundred();
System.out.println("num = " + num);

System.out.println(md.getIntBetweenOneToHundred());
*//**上面的代码调用了getIntBetweenOneToHundred**三次,这个方法执行了三次*

System.out.println("------------------------------------------------");
*//**调用MethodDefineDemo**类中有参有返回值的方法max*
md.max(3,6);*//**语法没问题,就是结果丢失*

int bigger = md.max(5,6);
System.out.println("bigger = " + bigger);

System.out.println("8,3中较大者是:" + md.max(8,9));

}
}
1
2
3
4
5
6
7
8
9
10
举例2:

*//1**、创建Scanner**的对象*
Scanner input = new Scanner(System.in);*//System.in**默认代表键盘输入*

*//2**、提示输入xx*
System.out.print("请输入一个整数:"); *//**对象.**非静态方法(**实参列表)*

*//3**、接收输入内容*
int num = input.nextInt(); *//**对象.**非静态方法()

5.5 使用的注意点

(1)必须先声明后使用,且方法必须定义在类的内部

(2)调用一次就执行一次,不调用不执行。

(3)方法中可以调用类中的方法或属性,不可以在方法内部定义方法。

正确示例:

1
2
3
4
5
6
7
8
类{
方法1(){

}
方法2(){

}
}

错误示例:

1
2
3
4
5
6
7
8
9
类{
方法1(){
方法2(){ *//**位置错误*


}

}
}

5.6 关键字return的使用

return在方法中的作用:

– 作用1:结束一个方法

– 作用2:结束一个方法的同时,可以返回数据给方法的调用者

注意点:在return关键字的直接后面不能声明执行语句

5.7 方法调用内存分析

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

方法被调用的时候,需要进入到栈内存中运行。方法每调用一次就会在栈中有一个入栈动作,即给当前方法开辟一块独立的内存区域,用于存储当前方法的局部变量的值。

当方法执行结束后,会释放该内存,称为出栈,如果方法有返回值,就会把结果返回调用处,如果没有返回值,就直接结束,回到调用处继续执行下一条指令。

栈结构:先进后出,后进先出。

举例分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Person {
public static void main(String[] args) {
Person p1 = new Person();
p1.eat();

}
public static void eat() {
sleep();
System.out.println("人:吃饭");
}
public static void sleep(){
System.out.println("人:睡觉");
doSport();
}
public static void doSport(){
System.out.println("人:运动");
}
}

内存分析:

img

5.8 练习

练习1:创建一个Person类,其定义如下:

image-20220320001337145

要求:

(1)创建Person类的对象,设置该对象的name、age和sex属性,调用study方法,输出字符串“studying”,调用showAge()方法显示age值,调用addAge()方法给对象的age属性值增加2岁。 (2)创建第二个对象,执行上述操作,体会同一个类的不同对象之间的关系。

练习2:利用面向对象的编程方法,设计圆类Circle,包含属性(半径)和计算圆面积的方法。定义测试类,创建该Circle类的对象,并进行测试。

练习3:

3.1 编写程序,声明一个method方法,在方法中打印一个108的型矩形,在main方法中调用该方法。

3.2 修改上一个程序,在method方法中,除打印一个108的型矩形外,再计算该矩形的面积,并将其作为方法返回值。在main方法中调用该方法,接收返回的面积值并打印。

3.3 修改上一个程序,在method方法提供m和n两个参数,方法中打印一个mn的型矩形,并计算该矩形的面积, 将其作为方法返回值。在main方法中调用该方法,接收返回的面积值并打印。

练习4:声明一个日期类型MyDate:有属性:年year,月month,日day。创建2个日期对象,分别赋值为:你的出生日期,你对象的出生日期,并显示信息。

练习5(课下练习):用面向对象的方式编写用户登录程序。

用户类:

属性:用户名,密码

方法:登录

界面类:

在界面类中添加main方法,接受用户输入,并调用用户类的登录方法进行验证。

– 输出:

• 登录失败:用户名或密码错误!

• 登录成功:欢迎你,用户名!

参考代码:

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
77
public class User {
String name;
String password;*//**密码*

*/***
*** *实现用户登录的判断*

***

** @*param inputName *输入的用户名*
*** *@*param inputPwd *输入的密码*
**/*
public void login(String inputName,String inputPwd){
if(name.equals(inputName) && password.equals(inputPwd)){
System.out.println("登录成功:欢迎你," + name);
}else{
System.out.println("登录失败:用户名或密码错误!");
}
}

*/***
*** *实现用户登录的判断*
** @*param inputName *输入的用户名*
*** *@*param inputPwd *输入的密码*
*** *@*return true*:**登录成功* false*:**登录失败*
**/*
public boolean login1(String inputName,String inputPwd){
*// if(name.equals(inputName) && password.equals(inputPwd)){*
*// return true;*
*// }else{*
*// return false;*
*// }*

*//**简化为:*
return name.equals(inputName) && password.equals(inputPwd);

}

}

*** *用户界面类*UserInterface*:*

***

*** *-* *在用户界面类中添加*main*方法,接受用户输入,并调用用户类的登录方法进行验证。*
*** *-* *输出:*
*** *-* *登录失败:用户名或密码错误!*
*** *-* *登录成功:欢迎你,用户名!*
**/*
public class UserInterface {
public static void main(String[] args) {

User u1 = new User();
u1.name = "Tom";
u1.password = "abc123";
Scanner scanner = new Scanner(System.in);
System.out.print("请输入用户名:");
String name = scanner.next();
System.out.print("请输入密码:");
String pwd = scanner.next();

*//**演示1**:*

*// u1.login(name,pwd);*

*//**演示2**:*
boolean isLogin = u1.login1(name, pwd);
if(isLogin){
System.out.println("登录成功:欢迎你," + u1.name);
}else{
System.out.println("登录失败:用户名或密码错误!");
}

scanner.close();

}
}

6. 对象数组

数组的元素可以是基本数据类型,也可以是引用数据类型。当元素是引用类型中的类时,我们称为对象数组。

1、案例

定义类Student,包含三个属性:学号number(int),年级state(int),成绩score(int)。 创建20个学生对象,学号为1到20,年级和成绩都由随机数确定。

问题一:打印出3年级(state值为3)的学生信息。

问题二:使用冒泡排序按学生成绩排序,并遍历所有学生信息

提示:

\1) 生成随机数:Math.random(),返回值类型double;

\2) 四舍五入取整:Math.round(double d),返回值类型long。

定义类Student,包含三个属性:学号number(int),年级state(int),成绩score(int)

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class Student {

int number;*//**学号*
int state;*//**年级*
int score;*//**成绩*


public void info(){
System.out.println("number : " + number
\+ ",state : " + state + ",score : " + score);
}

}

public class StudentTest {

public static void main(String[] args) {

*// Student s1 = new Student();*
*// s1.number = 1;*
*// s1.state = (int)(Math.random() \* 6 + 1);//[1,6]*
*// s1.score = (int)(Math.random() \* 101);//[0,100]*
*//*
*// Student s2 = new Student();*
*// s2.number = 2;*
*// s2.state = (int)(Math.random() \* 6 + 1);//[1,6]*
*// s2.score = (int)(Math.random() \* 101);//[0,100]*
*//*
*// //....*
*//* *对象数组*
*// String[] arr = new String[10];*
*//* *数组的创建*
Student[] students = new Student[20];
*//* *通过循环结构给数组的属性赋值*
for (int i = 0; i < students.length; i++) {
*//* *数组元素的赋值*
students[i] = new Student();
*//* *数组元素是一个对象,给对象的各个属性赋值*
students[i].number = (i + 1);
students[i].state = (int) (Math.random() * 6 + 1);*// [1,6]*
students[i].score = (int) (Math.random() * 101);*// [0,100]*
}

*//* *问题一:打印出3**年级(state**值为3**)的学生信息。*
for (int i = 0; i < students.length; i++) {

if (students[i].state == 3) {

*// System.out.println(*
*// "number:" + students[i].number + ",state:" + students[i].state + ",score:" + students[i].score);*
students[i].info();


}

}
System.out.println("******************************");
*//* *问题二:使用冒泡排序按学生成绩排序,并遍历所有学生信息*
*//* *排序前*
for (int i = 0; i < students.length; i++) {

*// System.out.println(*
*// "number:" + students[i].number + ",state:" +*
*// students[i].state + ",score:" + students[i].score);*


students[i].info();
}

System.out.println();
*//* *排序:*
for (int i = 0; i < students.length - 1; i++) {
for (int j = 0; j < students.length - 1 - i; j++) {
if (students[j].score > students[j + 1].score) {
Student temp = students[j];
students[j] = students[j + 1];
students[j + 1] = temp;
}
}
}

*//* *排序后:*
for (int i = 0; i < students.length; i++) {

*// System.out.println(*
*// "number:" + students[i].number + ",state:" +*
*// students[i].state + ",score:" + students[i].score);*
students[i].info();
}
}
}

内存解析:

img

2、注意点

对象数组,首先要创建数组对象本身,即确定数组的长度,然后再创建每一个元素对象,如果不创建,数组的元素的默认值就是null,所以很容易出现空指针异常NullPointerException。

3、练习

(1)定义矩形类Rectangle,包含长、宽属性,area()返回矩形面积的方法,perimeter()返回矩形周长的方法,String getInfo()返回圆对象的详细信息(如:长、宽、面积、周长等数据)的方法

(2)在测试类中创建长度为3的Rectangle[]数组,用来装3个矩形对象,并给3个矩形对象的长分别赋值为10,20,30,宽分别赋值为5,15,25,遍历输出

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
package com.atguigu.test08.array;

public class Rectangle {
double length;
double width;

public double area(){*//**面积*
return length * width;
}

public double perimeter(){*//**周长*
return 2 * (length + width);
}

public String getInfo(){
return "长:" + length +
",宽:" + width +
",面积:" + area() +
",周长:" + perimeter();
}
}

package com.atguigu.test08.array;

public class ObjectArrayTest {
public static void main(String[] args) {
*//**声明并创建一个长度为3**的矩形对象数组*
Rectangle[] array = new Rectangle[3];

*//**创建3**个矩形对象,并为对象的实例变量赋值,*
*//3**个矩形对象的长分别是10,20,30*
*//3**个矩形对象的宽分别是5,15,25*
*//**调用矩形对象的getInfo()**返回对象信息后输出*
for (int i = 0; i < array.length; i++) {
*//**创建矩形对象*
array[i] = new Rectangle();

*//**为矩形对象的成员变量赋值*
array[i].length = (i+1) * 10;
array[i].width = (2*i+1) * 5;

*//**获取并输出对象对象的信息*
System.out.println(array[i].getInfo());
}

}
}

内存解析:

img

7. 再谈方法

7.1 方法的重载(overload)

7.1.1 概念及特点

方法重载:在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可。

– 参数列表不同,意味着参数个数或参数类型的不同

重载的特点:与修饰符、返回值类型无关,只看参数列表,且参数列表必须不同。(参数个数或参数类型)。调用时,根据方法参数列表的不同来区别。

重载方法调用:JVM通过方法的参数列表,调用匹配的方法。

– 先找个数、类型最匹配的

– 再找个数和类型可以兼容的,如果同时多个方法可以兼容将会报错

7.1.2 示例

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//System.out.println()**方法就是典型的重载方法,其内部的声明形式如下:*
public class PrintStream {
public void println(byte x)
public void println(short x)
public void println(int x)
public void println(long x)
public void println(float x)
public void println(double x)
public void println(char x)
public void println(double x)
public void println()

}

public class HelloWorld{
public static void main(String[] args) {
System.out.println(3);
System.out.println(1.2f);
System.out.println("hello!");
}
}

举例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
//**返回两个整数的和*
public int add(int x,int y){
return x+y;
}

*//**返回三个整数的和*
public int add(int x,int y,int z){
return x+y+z;
}
*//**返回两个小数的和*
public double add(double x,double y){
return x+y;
}

举例3:方法的重载和返回值类型无关

public class MathTools {
*//**以下方法不是重载,会报错*
public int getOneToHundred(){
return (int)(Math.random()*100);
}

public double getOneToHundred(){
return Math.random()*100;
}
}

7.1.3 练习

练习1:判 断与void show(int a,char b,double c){}构成重载的有:

a)void show(int x,char y,double z){} // no

b)int show(int a,double c,char b){} // yes

c) void show(int a,double c,char b){} // yes

d) boolean show(int c,char b){} // yes

e) void show(double c){} // yes

f) double show(int x,char y,double z){} // no

g) void shows(){double c} // no

练习2:编写程序,定义三个重载方法并调用。

方法名为mOL。

三个方法分别接收一个int参数、两个int参数、一个字符串参数。分别执行平方运算并输出结果,相乘并输出结果,输出字符串信息。

在主类的main ()方法中分别用参数区别调用三个方法。

练习3:定义三个重载方法max(),第一个方法求两个int值中的最大值,第二个方法求两个double值中的最大值,第三个方法求三个double值中的最大值,并分别调用三个方法。

7.2 可变个数的形参

JDK 5.0 中提供了Varargs(variable number of arguments)机制。即当定义一个方法时,形参的类型可以确定,但是形参的个数不确定,那么可以考虑使用可变个数的形参。

格式:

1
方法名(参数的类型名 ...参数名)

举例:

1
2
3
4
5
//JDK 5.0**以前:采用数组形参来定义方法,传入多个同一类型变量*
public static void test(int a ,String[] books);

*//JDK5.0**:采用可变个数形参来定义方法,传入多个同一类型变量*
public static void test(int a ,String...books);

特点:

可变参数:方法参数部分指定类型的参数个数是可变多个:0个,1个或多个

可变个数形参的方法与同名的方法之间,彼此构成重载

可变参数方法的使用与方法参数部分使用数组是一致的,二者不能同时声明,否则报错。

方法的参数部分有可变形参,需要放在形参声明的最后

在一个方法的形参中,最多只能声明一个可变个数的形参

案例分析:

案例1:n个字符串进行拼接,每一个字符串之间使用某字符进行分割,如果没有传入字符串,那么返回空字符串””

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
public class StringTools {
String concat(char seperator, String... args){
String str = "";
for (int i = 0; i < args.length; i++) {
if(i==0){
str += args[i];
}else{
str += seperator + args[i];
}
}
return str;
}
}

package com.atguigu.test05.param;

public class StringToolsTest {
public static void main(String[] args) {
StringTools tools = new StringTools();

System.out.println(tools.concat('-'));
System.out.println(tools.concat('-',"hello"));
System.out.println(tools.concat('-',"hello","world"));
System.out.println(tools.concat('-',"hello","world","java"));

}
}

案例2:求n个整数的和

public class NumberTools {
public int total(int[] nums){
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}

public int sum(int... nums){
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
}

public class TestVarParam {
public static void main(String[] args) {
NumberTools tools = new NumberTools();

System.out.println(tools.sum());*//0**个实参*
System.out.println(tools.sum(5));*//1**个实参*
System.out.println(tools.sum(5,6,2,4));*//4**个实参*
System.out.println(tools.sum(new int[]{5,6,2,4}));*//**传入数组实参*

System.out.println("------------------------------------");
System.out.println(tools.total(new int[]{}));*//0**个元素的数组*
System.out.println(tools.total(new int[]{5}));*//1**个元素的数组*
System.out.println(tools.total(new int[]{5,6,2,4}));*//**传入数组实参*

}
}

案例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
public class MathTools {
*//**求两个整数的最大值*
public int max(int a,int b){
return a>b?a:b;
}

*//**求两个小数的最大值*
public double max(double a, double b){
return a>b?a:b;
}

*//**求三个整数的最大值*
public int max(int a, int b, int c){
return max(max(a,b),c);
}

*//**求n**个整数的最大值*
public int max(int... nums){
int max = nums[0];*//**如果没有传入整数,或者传入null**,这句代码会报异常*
for (int i = 1; i < nums.length; i++) {
if(nums[i] > max){
max = nums[i];
}
}
return max;
}
*/\* //**求n**整数的最大值*
*public int max(int[] nums){ //**编译就报错,与(int... nums)**无法区分*
*int max = nums[0];//**如果没有传入整数,或者传入null**,这句代码会报异常*
*for (int i = 1; i < nums.length; i++) {*
*if(nums[i] > max){*
*max = nums[i];*
*}*
*}*
*return max;*
*}\*/*

*/\* //**求n**整数的最大值*
*public int max(int first, int... nums){ //**当前类不报错,但是调用时会引起多个方法同时匹配*
*int max = first;*
*for (int i = 0; i < nums.length; i++) {*
*if(nums[i] > max){*
*max = nums[i];*
*}*
*}*
*return max;*
*}\*/*
}

7.3 方法的参数传递机制

7.3.1 形参和实参

形参(formal parameter):在定义方法时,方法名后面括号()中声明的变量称为形式参数,简称形参。

实参(actual parameter):在调用方法时,方法名后面括号()中的使用的值/变量/表达式称为实际参数,简称实参。

7.3.2 参数传递机制:值传递

Java里方法的参数传递方式只有一种:值传递。 即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响。

形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参

形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参

7.3.3 举例

1、形参是基本数据类型

案例:编写方法,交换两个整型变量的值

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
public class Test {
public static void main(String[] args) {
int m = 10;
int n = 20;


System.out.println("m = " + m + ", n = " + n);
*//**交换m**和n**的值*

*// int temp = m;*
*// m = n;*
*// n = temp;*


ValueTransferTest1 test = new ValueTransferTest1();
test.swap(m, n);

System.out.println("m = " + m + ", n = " + n);

}

public void swap(int m,int n){
int temp = m;
m = n;
n = temp;
}

}

内存解析:

img

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
public class Test {
public static void main(String[] args) {


Data d1 = new Data();
d1.m = 10;
d1.n = 20;

System.out.println("m = " + d1.m + ", n = " + d1.n);

*//**实现* *换序*
ValueTransferTest2 test = new ValueTransferTest2();
test.swap(d1);
System.out.println("m = " + d1.m + ", n = " + d1.n);

}
public void swap(Data data){
int temp = data.m;
data.m = data.n;
data.n = temp;
}
}

class Data{
int m;
int n;
}

内存解析:

img

7.3.4 练习

练习1:判断如下程序输出的结果

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 AssignNewObject {
public void swap(MyData my){
my = new MyData(); *//**考虑堆空间此新创建的对象,和main**中的data**对象是否有关*
int temp = my.x;
my.x = my.y;
my.y = temp;

}

public static void main(String[] args) {
AssignNewObject tools = new AssignNewObject();


MyData data = new MyData();
data.x = 1;
data.y = 2;
System.out.println("交换之前:x = " + data.x +",y = " + data.y);*//*
tools.swap(data);*//**调用完之后,x**与y**的值交换?*
System.out.println("交换之后:x = " + data.x +",y = " + data.y);*//*

}
}

class MyData{
int x ;
int 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
练习2:如下操作是否可以实现数组排序

public class ArrayTypeParam {

*//**冒泡排序,实现数组从小到大排序*
public void sort(int[] arr){
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if(arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
*//**打印数组的元素*
public void print(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
System.out.println();
}

public static void main(String[] args) {
ArrayTypeParam tools = new ArrayTypeParam();

int[] nums = {4,3,1,6,7};
System.out.println("排序之前:");
tools.print(nums);

tools.sort(nums);*//**对nums**数组进行排序*

System.out.println("排序之后:");
tools.print(nums);*//**输出nums**数组的元素*

}
}
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
练习3:通过内存结构图,写出如下程序的输出结果

*//**栈:每个方法在调用时,都会有以栈帧的方法压入栈中。栈帧中保存了当前方法中声明的变量:方法内声明的,形参*
*//**堆:存放**new**出来的**"**东西**"**:对象(成员变量在对象中)、数组实体(数组元素)。*
*//**注意:变量前如果声明有类型,那么这就是一个新的刚要定义的变量。如果变量前没有声明类型,那就说明此变量在之前已经声明过。*
public class TransferTest3 {
public static void main(String args[]) {
TransferTest3 test = new TransferTest3();
test.first();
}
public void first() {
int i = 5;
Value v = new Value();
v.i = 25;
second(v, i);
System.out.println(v.i);
}
public void second(Value v, int i) {
i = 0;
v.i = 20;
Value val = new Value();
v = val;
System.out.println(v.i + " " + i);
}
}

class Value {
int i = 15;
}

内存解析:

20220320_233145

练习4:貌似是考查方法的参数传递

img

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 static void method(int a, int b) {
*//* *在不改变原本题目的前提下,如何写这个函数才能在main**函数中输出a=100**,b=200**?*
a = a * 10;
b = b * 20;
System.out.println(a);
System.out.println(b);
System.exit(0);
}

*//**法二:*
public static void method(int a, int b) {

PrintStream ps = new PrintStream(System.out) {
@Override
public void println(String x) {

if ("a=10".equals(x)) {
x = "a=100";
} else if ("b=10".equals(x)) {
x = "b=200";
}
super.println(x);
}
};

System.setOut(ps);

}

练习5:将对象作为参数传递给方法

(1)定义一个Circle类,包含一个double型的radius属性代表圆的半径,一个findArea()方法返回圆的面积。 (2)定义一个类PassObject,在类中定义一个方法printAreas(),该方法的定义如下:public void printAreas(Circle c, int time),在printAreas方法中打印输出1到time之间的每个整数半径值,以及对应的面积。例如,times为5,则输出半径1,2,3,4,5,以及对应的圆面积。 (3)在main方法中调用printAreas()方法,调用完毕后输出当前半径值。程序运行结果如图所示。

img

7.4 递归(recursion)方法

举例1:

image-20220521160246138

举例2:

从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,讲的啥?
从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,讲的啥?
从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,讲的啥?
从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,讲的啥?…

老和尚没了,庙塌了,小和尚还俗结婚了。

递归方法调用:方法自己调用自己的现象就称为递归。

递归的分类: 直接递归、间接递归。

直接递归:方法自身调用自己。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 public void methodA(){
methodA();
}

间接递归:可以理解为A()方法调用B()方法,B()方法调用C()方法,C()方法调用A()方法。

public static void A(){
B();
}

public static void B(){
C();
}

public static void C(){
A();
}

说明

递归方法包含了一种隐式的循环。

递归方法会重复执行某段代码,但这种重复执行无须循环控制。

递归一定要向已知方向递归,否则这种递归就变成了无穷递归,停不下来,类似于死循环。最终发生栈内存溢出。

举例:

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
举例1:计算1 ~ n的和

public class RecursionDemo {
public static void main(String[] args) {
RecursionDemo demo = new RecursionDemo();
*//**计算1~num**的和,使用递归完成*
int num = 5;
*//* *调用求和的方法*
int sum = demo.getSum(num);
*//* *输出结果*
System.out.println(sum);

}
*/**
*通过递归算法实现.*
*参数列表:int*
*返回值类型: int*
**/*
public int getSum(int num) {
*/**
*num**为1**时,**方法返回1,*
*相当于是方法的出口,num**总有是1**的情况*
**/*
if(num == 1){
return 1;
}
*/**
*num**不为1**时,**方法返回 num +(num-1)**的累和*
*递归调用getSum**方法*
**/*
return num + getSum(num-1);
}
}

代码执行图解:

img

举例2:递归方法计算n!

1
2
3
4
5
6
7
public int multiply(int num){
if(num == 1){
return 1;
}else{
return num * multiply(num - 1);
}
}

img

举例3:已知有一个数列:f(0) = 1,f(1) = 4,f(n+2)=2*f(n+1) + f(n),其中n是大于0的整数,求f(10)的值。

1
2
3
4
5
6
7
8
9
public int f(int num){
if(num == 0){
return 1;
}else if(num == 1){
return 4;
}else{
return 2 * f(num - 1) + f(num - 2);
}
}

举例4:已知一个数列:f(20) = 1,f(21) = 4,f(n+2) = 2*f(n+1)+f(n),其中n是大于0的整数,求f(10)的值。

1
2
3
4
5
6
7
8
9
public int func(int num){
if(num == 20){
return 1;
}else if(num == 21){
return 4;
}else{
return func(num + 2) - 2 * func(num + 1);
}
}

举例5:计算斐波那契数列(Fibonacci)的第n个值,斐波那契数列满足如下规律,

1,1,2,3,5,8,13,21,34,55,….

即从第三个数开始,一个数等于前两个数之和。假设f(n)代表斐波那契数列的第n个值,那么f(n)满足: f(n) = f(n-2) + f(n-1);

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
//**使用递归的写法*
int f(int n) {*//**计算斐波那契数列第**n**个值是多少*
if (n < 1) {*//**负数是返回特殊值**1**,表示不计算负数情况*
return 1;
}
if (n == 1 || n == 2) {
return 1;
}
return f(n - 2) + f(n - 1);
}

*//**不用递归*
int fValue(int n) {*//**计算斐波那契数列第**n**个值是多少*
if (n < 1) {*//**负数是返回特殊值**1**,表示不计算负数情况*
return 1;
}
if (n == 1 || n == 2) {
return 1;
}
*//**从第三个数开始,* *等于* *前两个整数相加*
int beforeBefore = 1; *//**相当于**n=1**时的值*
int before = 1;*//**相当于**n=2**时的值*
int current = beforeBefore + before; *//**相当于**n=3**的值*
*//**再完后*
for (int i = 4; i <= n; i++) {
beforeBefore = before;
before = current;
current = beforeBefore + before;
*/**
*假设**i=4*
*beforeBefore = before; //**相当于**n=2**时的值*
*before = current; //**相当于**n=3**的值*
*current = beforeBefore + before; //**相当于**n = 4**的值*
*假设**i=5*
*beforeBefore = before; //**相当于**n=3**的值*
*before = current; //**相当于**n = 4**的值*
*current = beforeBefore + before; //**相当于**n = 5**的值*
*....*
**/*
}
return current;
}

举例6:面试题

宋老师,我今天去百度面试,遇到一个一个双重递归调用的问题,我琢磨了一下,完全不知道为什么。打断点了,也还是没看懂为什么程序会那样走。您有空可以看一下,求指教。

img

1
2
3
4
5
6
7
8
9
10
11
12
private int count = 0;

public int recursion(int k) {
count++;
System.out.println("count1:" + count + " k:" + k);
if (k <= 0) {
return 0;
}
return recursion(k - 1) + recursion(k - 2);*//287*
*//return recursion(k - 1);//11*
*//return recursion(k - 1) + recursion(k - 1);//2047*
}

剖析:

image-20220320235229941

最后说两句:

\1. 递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。

\2. 在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存。考虑使用循环迭代

8. 关键字:package、import

8.1 package(包)

package,称为包,用于指明该文件中定义的类、接口等结构所在的包。

8.1.1 语法格式

package 顶层包名.子包名 ;

举例:

1
2
3
4
5
6
7
8
9
pack1\pack2\PackageTest.java

package pack1.pack2; *//**指定类PackageTest**属于包pack1.pack2*

public class PackageTest{
public void display(){
System.out.println("in method display()");
}
}

说明:

• 一个源文件只能有一个声明包的package语句

• package语句作为Java源文件的第一条语句出现。若缺省该语句,则指定为无名包。

• 包名,属于标识符,满足标识符命名的规则和规范(全部小写)、见名知意

– 包通常使用所在公司域名的倒置:com.atguigu.xxx。

– 大家取包名时不要使用”java.xx”包

• 包对应于文件系统的目录,package语句中用 “.” 来指明包(目录)的层次,每.一次就表示一层文件目录。

• 同一个包下可以声明多个结构(类、接口),但是不能定义同名的结构(类、接口)。不同的包下可以定义同名的结构(类、接口)

8.1.2 包的作用

• 包可以包含类和子包,划分项目层次,便于管理

• 帮助管理大型软件系统:将功能相近的类划分到同一个包中。比如:MVC的设计模式

• 解决类命名冲突的问题

• 控制访问权限

8.1.3 应用举例

举例1:某航运软件系统包括:一组域对象、GUI和reports子系统

img

举例2:MVC设计模式

MVC是一种软件构件模式,目的是为了降低程序开发中代码业务的耦合度。

MVC设计模式将整个程序分为三个层次:视图模型(Viewer)层,控制器(Controller)层,与数据模型(Model)层。这种将程序输入输出、数据处理,以及数据的展示分离开来的设计模式使程序结构变的灵活而且清晰,同时也描述了程序各个对象间的通信方式,降低了程序的耦合性。

视图层viewer:显示数据,为用户提供使用界面,与用户直接进行交互。
>相关工具类 view.utils
>自定义view view.ui

控制层controller:解析用户请求,处理业务逻辑,给予用户响应
>应用界面相关 controller.activity
>存放fragment controller.fragment
>显示列表的适配器 controller.adapter
>服务相关的 controller.service
>抽取的基类 controller.base

模型层model:主要承载数据、处理数据
>数据对象封装 model.bean/domain
>数据库操作类 model.dao
>数据库 model.db

img

8.1.4 JDK中主要的包介绍

java.lang—-包含一些Java语言的核心类,如String、Math、Integer、 System和Thread,提供常用功能 java.net—-包含执行与网络相关的操作的类和接口。 java.io —-包含能提供多种输入/输出功能的类。 java.util—-包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数。 java.text—-包含了一些java格式化相关的类 java.sql—-包含了java进行JDBC数据库编程的相关类/接口 java.awt—-包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。

8.2 import(导入)

为了使用定义在其它包中的Java类,需用import语句来显式引入指定包下所需要的类。相当于import语句告诉编译器到哪里去寻找这个类。

8.2.1 语法格式

1
import 包名.类名;

8.2.2 应用举例

1
2
3
4
5
6
7
8
import pack1.pack2.Test;  *//import pack1.pack2.\*;**表示引入pack1.pack2**包中的所有结构*

public class PackTest{
public static void main(String args[]){
Test t = new Test(); *//Test**类在pack1.pack2**包中定义*
t.display();
}
}

8.2.3 注意事项

• import语句,声明在包的声明和类的声明之间。

• 如果需要导入多个类或接口,那么就并列显式多个import语句即可

• 如果使用a.导入结构,表示可以导入a包下的所有的结构。举例:可以使用java.util.的方式,一次性导入util包下所有的类或接口。

• 如果导入的类或接口是java.lang包下的,或者是当前包下的,则可以省略此import语句。

• 如果已经导入java.a包下的类,那么如果需要使用a包的子包下的类的话,仍然需要导入。

• 如果在代码中使用不同包下的同名的类,那么就需要使用类的全类名的方式指明调用的是哪个类。

• (了解)import static组合的使用:调用指定类或接口下的静态的属性或方法

9. 面向对象特征一:封装性(encapsulation)

9.1 为什么需要封装?

• 我要用洗衣机,只需要按一下开关和洗涤模式就可以了。有必要了解洗衣机内部的结构吗?有必要碰电动机吗?

• 我要开车,我不需要懂离合、油门、制动等原理和维修也可以驾驶。

• 客观世界里每一个事物的内部信息都隐藏在其内部,外界无法直接操作和修改,只能通过指定的方式进行访问和修改。

随着我们系统越来越复杂,类会越来越多,那么类之间的访问边界必须把握好,面向对象的开发原则要遵循“高内聚、低耦合”。

高内聚、低耦合是软件工程中的概念,也是UNIX 操作系统设计的经典原则。

内聚,指一个模块内各个元素彼此结合的紧密程度;耦合指一个软件结构内不同模块之间互连程度的度量。内聚意味着重用和独立,耦合意味着多米诺效应牵一发动全身。

而“高内聚,低耦合”的体现之一:

• 高内聚:类的内部数据操作细节自己完成,不允许外部干涉;

• 低耦合:仅暴露少量的方法给外部使用,尽量方便外部调用。

9.2 何为封装性?

所谓封装,就是把客观事物封装成抽象概念的类,并且类可以把自己的数据和方法只向可信的类或者对象开放,向没必要开放的类或者对象隐藏信息。

通俗的讲,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。

9.3 Java如何实现数据封装

• 实现封装就是控制类或成员的可见性范围。这就需要依赖访问控制修饰符,也称为权限修饰符来控制。

• 权限修饰符:public、protected、缺省、private。具体访问范围如下:

修饰符 本类内部 本包内 其他包的子类 其他包非子类
private × × ×
缺省 × ×
protected ×
public

• 具体修饰的结构:

– 外部类:public、缺省

– 成员变量、成员方法、构造器、成员内部类:public、protected、缺省、private

image-20220321222327616

image-20220617164042390

9.4 封装性的体现

9.4.1 成员变量/属性私有化

概述:私有化类的成员变量,提供公共的get和set方法,对外暴露获取和修改属性的功能。

实现步骤:

使用 private 修饰成员变量

private 数据类型 变量名 ;

代码如下:

1
2
3
4
5
public class Person {
private String name;
private int age;
private boolean marry;
}

提供 getXxx方法 / setXxx 方法,可以访问成员变量,代码如下:

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 Person {
private String name;
private int age;
private boolean marry;

public void setName(String n) {
name = n;
}

public String getName() {
return name;
}

public void setAge(int a) {
age = a;
}

public int getAge() {
return age;
}

public void setMarry(boolean m){
marry = m;
}

public boolean isMarry(){
return marry;
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class PersonTest {
public static void main(String[] args) {
Person p = new Person();

*//**实例变量私有化,跨类是无法直接使用的*
*/\* p.name = "**张三";*
*p.age = 23;*
*p.marry = true;\*/*

p.setName("张三");
System.out.println("p.name = " + p.getName());

p.setAge(23);
System.out.println("p.age = " + p.getAge());

p.setMarry(true);
System.out.println("p.marry = " + p.isMarry());

}
}

成员变量封装的好处:

• 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,限制对成员变量的不合理访问。还可以进行数据检查,从而有利于保证对象信息的完整性。

• 便于修改,提高代码的可维护性。主要说的是隐藏的部分,在内部修改了,如果其对外可以的访问方式不变的话,外部根本感觉不到它的修改。例如:Java8->Java9,String从char[]转为byte[]内部实现,而对外的方法不变,我们使用者根本感觉不到它内部的修改。

开心一笑:

A man and woman are in a computer programming lecture. The man touches the woman’s breasts.

“Hey!” she says. “Those are private!”

The man says, “But we’re in the same class!”

9.4.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
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
public class ArrayUtil {

public int max(int[] arr) {
int maxValue = arr[0];
for(int i = 1;i < arr.length;i++){
if(maxValue < arr[i]){
maxValue = arr[i];
}
}
return maxValue;

}


public int min(int[] arr){
int minValue = arr[0];
for(int i = 1;i < arr.length;i++){
if(minValue > arr[i]){
minValue = arr[i];
}
}
return minValue;
}


public int sum(int[] arr) {
int sum = 0;
for(int i = 0;i < arr.length;i++){
sum += arr[i];
}
return sum;
}


public int avg(int[] arr) {
int sumValue = sum(arr);
return sumValue / arr.length;
}

*//* *创建一系列重载的上述方法*
*// public double max(double[] arr){}*
*// public float max(float[] arr){}*
*// public byte max(byte[] arr){}*

public void print(int[] arr) {
for(int i = 0;i < arr.length;i++){
System.out.print(arr[i] + " ");
}
System.out.println();

}

public int[] copy(int[] arr) {
int[] arr1 = new int[arr.length];
for(int i = 0;i < arr.length;i++){
arr1[i] = arr[i];
}
return arr1;

}

public void reverse(int[] arr) {
for(int i = 0,j = arr.length - 1;i < j;i++,j--){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

public void sort(int[] arr,String desc) {


if("ascend".equals(desc)){*//if(desc.equals("ascend")){*
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {

*// int temp = arr[j];*
*// arr[j] = arr[j + 1];*
*// arr[j + 1] = temp;*
swap(arr,j,j+1);
}
}
}
}else if ("descend".equals(desc)){
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] < arr[j + 1]) {
*// int temp = arr[j];*
*// arr[j] = arr[j + 1];*
*// arr[j + 1] = temp;*
swap(arr,j,j+1);
}
}
}
}else{
System.out.println("您输入的排序方式有误!");
}
}

private void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

*/***
***
*** *@*param arr
*** *@*param value
*** *@*return *返回*value*值出现的位置* *或* *-1**:未找到*
**/*
public int getValue(int[] arr, int value) {
*//**方法:线性查找*
for(int i = 0;i < arr.length;i++){
if(value == arr[i]){
return i;
}
}


return - 1;

}
}

注意:

开发中,一般成员实例变量都习惯使用private修饰,再提供相应的public权限的get/set方法访问。

对于final的实例变量,不提供set()方法。(后面final关键字的时候讲)

对于static final的成员变量,习惯上使用public修饰。

9.5 练习

练习1:

创建程序:在其中定义两个类:Person和PersonTest类。定义如下:

用setAge()设置人的合法年龄(0~130),用getAge()返回人的年龄。在PersonTest类中实例化Person类的对象b,调用setAge()和getAge()方法,体会Java的封装性。

image-20220321223633916

练习2:

自定义图书类。设定属性包括:书名bookName,作者author,出版社名publisher,价格price;方法包括:相应属性的get/set方法,图书信息介绍等。

10. 类的成员之三:构造器(Constructor)

我们new完对象时,所有成员变量都是默认值,如果我们需要赋别的值,需要挨个为它们再赋值,太麻烦了。我们能不能在new对象时,直接为当前对象的某个或所有成员变量直接赋值呢?

可以,Java给我们提供了构造器(Constructor),也称为构造方法。

10.1 构造器的作用

new对象,并在new对象的时候为实例变量赋值。

举例:Person p = new Person(“Peter”,15);

解释:如同我们规定每个“人”一出生就必须先洗澡,我们就可以在“人”的构造器中加入完成“洗澡”的程序代码,于是每个“人”一出生就会自动完成“洗澡”,程序就不必再在每个人刚出生时一个一个地告诉他们要“洗澡”了。

10.2 构造器的语法格式

1
2
3
4
5
6
7
8
[修饰符] class 类名{
[修饰符] 构造器名(){
*//* *实例初始化代码*
}
[修饰符] 构造器名(参数列表){
*//* *实例初始化代码*
}
}

说明:

\3. 构造器名必须与它所在的类名必须相同。

\4. 它没有返回值,所以不需要返回值类型,也不需要void。

\5. 构造器的修饰符只能是权限修饰符,不能被其他任何修饰。比如,不能被static、final、synchronized、abstract、native修饰,不能有return语句返回值。

代码如下:

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 Student {
private String name;
private int age;

*//* *无参构造*
public Student() {}

*//* *有参构造*
public Student(String n,int a) {
name = n;
age = a;
}

public String getName() {
return name;
}
public void setName(String n) {
name = n;
}
public int getAge() {
return age;
}
public void setAge(int a) {
age = a;
}

public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
}

public class TestStudent {
public static void main(String[] args) {
*//**调用无参构造创建学生对象*
Student s1 = new Student();

*//**调用有参构造创建学生对象*
Student s2 = new Student("张三",23);

System.out.println(s1.getInfo());
System.out.println(s2.getInfo());

}
}

10.3 使用说明

当我们没有显式的声明类中的构造器时,系统会默认提供一个无参的构造器并且该构造器的修饰符默认与类的修饰符相同

img

当我们显式的定义类的构造器以后,系统就不再提供默认的无参的构造器了。

在类中,至少会存在一个构造器。

构造器是可以重载的。

10.4 练习

练习1:编写两个类,TriAngle和TriAngleTest,其中TriAngle类中声明私有的底边长base和高height,同时声明公共方法访问私有变量。此外,提供类必要的构造器。另一个类中使用这些公共方法,计算三角形的面积。

练习2:

(1)定义Student类,有4个属性: String name; int age; String school; String major;

(2)定义Student类的3个构造器:

• 第一个构造器Student(String n, int a)设置类的name和age属性;

• 第二个构造器Student(String n, int a, String s)设置类的name, age 和school属性;

• 第三个构造器Student(String n, int a, String s, String m)设置类的name, age ,school和major属性;

(3)在main方法中分别调用不同的构造器创建的对象,并输出其属性值。

练习3:

1、写一个名为Account的类模拟账户。该类的属性和方法如下图所示。

该类包括的属性:账号id,余额balance,年利率annualInterestRate;

包含的方法:访问器方法(getter和setter方法),取款方法withdraw(),存款方法deposit()。

标题: fig:

提示:在提款方法withdraw中,需要判断用户余额是否能够满足提款数额的要求,如果不能,应给出提示。

\1. 创建Customer类。

标题: fig:

\2. 声明三个私有对象属性:firstName、lastName和account。 b. 声明一个公有构造器,这个构造器带有两个代表对象属性的参数(f和l) c. 声明两个公有存取器来访问该对象属性,方法getFirstName和getLastName返回相应的属性。 d. 声明setAccount 方法来对account属性赋值。 e. 声明getAccount 方法以获取account属性。

3.写一个测试程序。

(1)创建一个Customer ,名字叫 Jane Smith, 他有一个账号为1000,余额为2000元,年利率为 1.23% 的账户。 (2)对Jane Smith操作。 存入 100 元,再取出960元。再取出2000元。 打印出Jane Smith 的基本信息

成功存入 :100.0
成功取出:960.0
余额不足,取款失败
Customer [Smith, Jane] has a account: id is 1000, annualInterestRate is 1.23%, balance is 1140.0

11. 阶段性知识补充

11.1 类中属性赋值过程

1、在类的属性中,可以有哪些位置给属性赋值?

① 默认初始化

② 显式初始化

③ 构造器中初始化

④ 通过”对象.属性”或”对象.方法”的方式,给属性赋值

2、这些位置执行的先后顺序是怎样?

顺序:① - ② - ③ - ④

3、说明:

上述中的①、②、③在对象创建过程中,只执行一次。

④ 是在对象创建后执行的,可以根据需求多次执行。

11.2 JavaBean

JavaBean是一种Java语言写成的可重用组件。

– 好比你做了一个扳手,这个扳手会在很多地方被拿去用。这个扳手也提供多种功能(你可以拿这个扳手扳、锤、撬等等),而这个扳手就是一个组件。

所谓JavaBean,是指符合如下标准的Java类:

  • – 类是公共的

  • – 有一个无参的公共的构造器

  • – 有属性,且有对应的get、set方法

• 用户可以使用JavaBean将功能、处理、值、数据库访问和其他任何可以用Java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP页面、Servlet、其他JavaBean、applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。

• 《Think in Java》中提到,JavaBean最初是为Java GUI的可视化编程实现的。你拖动IDE构建工具创建一个GUI 组件(如多选框),其实是工具给你创建Java类,并提供将类的属性暴露出来给你修改调整,将事件监听器暴露出来。

• 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  public class JavaBean {
private String name; *//* *属性一般定义为private*
private int age;
public JavaBean() {
}
public int getAge() {
return age;
}
public void setAge(int a) {
age = a;
}
public String getName() {
return name;
}
public void setName(String n) {
name = n;
}
}

11.3 UML类图

UML(Unified Modeling Language,统一建模语言),用来描述软件模型和架构的图形化语言。

• 常用的UML工具软件有PowerDesinger、Rose和Enterprise Architect。

• UML工具软件不仅可以绘制软件开发中所需的各种图表,还可以生成对应的源代码。

• 在软件开发中,使用UML类图可以更加直观地描述类内部结构(类的属性和操作)以及类之间的关系(如关联、依赖、聚合等)。

– +表示 public 类型, - 表示 private 类型,#表示protected类型

– 方法的写法: 方法的类型(+、-) 方法名(参数名: 参数类型):返回值类型

– 斜体表示抽象方法或类。

image-20220321232230831

img

第07章_面向对象编程(进阶)


本章专题与脉络

第2阶段:Java面向对象编程-第07章

1. 关键字:this

1.1 this是什么?

  • 在Java中,this关键字不算难理解,它的作用和其词义很接近。

    • 它在方法(实例方法/非static的方法)内部使用,表示调用该方法的对象 —this.name=name;这时候
    • 它在构造器内部使用,表示该构造器正在初始化的对象。 – -Person person=new Person();这时候
  • this可以调用的结构:成员变量[不可以是局部变量!!]、方法和构造器

1.2 什么时候使用this

1.2.1 实例方法/构造器中—使用当前对象的成员(区分成员变量和传入的局部变量)

在实例方法或构造器中,如果使用当前类的成员变量或成员方法可以在其前面添加this,增强程序的可读性。不过,通常我们都习惯省略this。

但是,当形参与成员变量同名时,如果在方法内或构造器内需要使用成员变量,必须添加this来表明该变量是类的成员变量。即:我们可以用this来区分成员变量局部变量。比如:

image-20220503102947013

另外,使用this访问属性和方法时,如果在本类中未找到,会从父类中查找。这个在继承中会讲到。

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person{		// 定义Person类
private String name ;
private int age ;
public Person(String name,int age){
this.name = name ;
this.age = age ;
}
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public void getInfo(){
System.out.println("姓名:" + name) ;
this.speak();
}
public void speak(){
System.out.println(“年龄:” + this.age);
}
}

举例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
public class Rectangle {
int length;
int width;

public int area() {
return this.length * this.width;
}

public int perimeter(){
return 2 * (this.length + this.width);
}

public void print(char sign) {
for (int i = 1; i <= this.width; i++) {
for (int j = 1; j <= this.length; j++) {
System.out.print(sign);
}
System.out.println();
}
}

public String getInfo(){
return "长:" + this.length + ",宽:" + this.width +",面积:" + this.area() +",周长:" + this.perimeter();
}
}

测试类:

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 TestRectangle {
public static void main(String[] args) {
Rectangle r1 = new Rectangle();
Rectangle r2 = new Rectangle();

System.out.println("r1对象:" + r1.getInfo());
System.out.println("r2对象:" + r2.getInfo());

r1.length = 10;
r1.width = 2;
System.out.println("r1对象:" + r1.getInfo());
System.out.println("r2对象:" + r2.getInfo());

r1.print('#');
System.out.println("---------------------");
r1.print('&');

System.out.println("---------------------");
r2.print('#');
System.out.println("---------------------");
r2.print('%');
}
}

1.2.2 同一个类中—-构造器互相调用(减少重复代码)

this可以作为一个类中构造器相互调用的特殊格式。 –我们可以通过this调用类中的其他构造器!!!!

  • this():调用本类的无参构造器

  • this(实参列表):调用本类的有参构造器

    image-20230927165025075

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Student {
private String name;
private int age;
// 无参构造
public Student() {
// this("",18);//调用本类有参构造器
}

// 有参构造
public Student(String name) {
this();//调用本类无参构造器 必须是首行 只能最多声明一个this()
this.name = name;
}
// 有参构造
public Student(String name,int age){
//直接this.name=name都可以省略了
this(name);//调用本类中有一个String参数的构造器 必须是首行 只能最多声明一个this()
this.age = age;
}
}

注意:

  • 不能出现递归调用。比如,调用自身构造器。
    • 推论:如果一个类中声明了n个构造器,则最多有 n - 1个构造器中使用了”this(形参列表)
  • this()和this(实参列表)只能声明在构造器首行
    • 推论:在类的一个构造器中,最多只能声明一个”this(参数列表)

1.3 练习

练习1:添加必要的构造器,综合应用构造器的重载,this关键字。

image-20220808191154534

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
public class Boy {
private String name; //name和age前面写的负号 说明是私有属性
private String age;

public void setAge(String age){ //set方法传入的就可以是age 使用this.age=age可以区分传入的局部变量
this.age=age;
}

public void setName(String name){ //set方法传入的就可以是name 使用this.name=name可以区分传入的局部变量
this.name=name;
}

public String getAge(){
return this.age; //this可以省略
}

public String getName(){
return this.name; //this可以省略
}

public void marry(Girl girl){

}

public void shout(){

}

public Boy(){

}

public Boy(String name,String age){
this.name=name; //this()可以调用无参构造器
this.age=age;
}

}
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
public class Girl {
private String name; //name和age前面写的负号 说明是私有属性
private String age;

public void setAge(String age){ //set方法传入的就可以是age 使用this.age=age可以区分传入的局部变量
this.age=age;
}

public void setName(String name){ //set方法传入的就可以是name 使用this.name=name可以区分传入的局部变量
this.name=name;
}

public String getAge(){
return this.age; //this可以省略
}

public String getName(){
return this.name; //this可以省略
}

public void marry(Boy boy){

}

public void compare(Girl girl){

}
}

练习2:

(1)按照如下的UML类图,创建相应的类,提供必要的结构:

image-20220323211412409

在提款方法withdraw()中,需要判断用户余额是否能够满足提款数额的要求,如果不能,应给出提示。deposit()方法表示存款。

(2)按照如下的UML类图,创建相应的类,提供必要的结构

image-20220323211454372

(3)按照如下的UML类图,创建相应的类,提供必要的结构

image-20220323211521808
  • addCustomer 方法必须依照参数(姓,名)构造一个新的 Customer对象,然后把它放到 customer 数组中。还必须把 numberOfCustomer 属性的值加 1。

  • getNumOfCustomers 方法返回 numberofCustomers 属性值。

  • getCustomer方法返回与给出的index参数相关的客户。

(4)创建BankTest类,进行测试。

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 Account {
private double balance; //因为balance前面是负号

//前面一个加号是public 加横线了就是构造器
public Account(double balance){
this.balance=balance; //使用this区分成员变量和局部变量
}

//前面一个加号是public 冒号后面是double是返回值
public double getBalance(){
return this.balance; //this可以省略
}

public void deposit(double amt){ //存钱
if(amt>0){
this.balance+=amt;
System.out.println("存入成功!!!");
}
}

public void withdraw(double amt){ //取钱
if(amt>this.balance){
System.out.println("余额不足!!!");
return; //跳出结果
}
this.balance-=amt;
}

}
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 Customer {
private String firstName; //三个属性都是负号 所以设定为私有private
private String lastName; //三个属性都是负号 所以设定为私有private
private Account account; //三个属性都是负号 所以设定为私有private

//正号所以是public 下划线所以是构造器
public Customer(String f,String l){
this.firstName=f;
this.lastName=l;
}

//有冒号 说明有返回值
public String getFirstName(){
return this.firstName;
}

//有冒号 说明有返回值
public String getLastName(){
return this.lastName;
}

//有冒号 说明有返回值
public Account getAccount(){
return this.account;
}

//没有冒号 说明没有返回值
public void setAccount(Account acct){
this.account=acct;
}

}
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 Bank {
private Customer[] customers; //私有化不能get和set创建
private int numberOfCustomer;

public Bank(){
customers = new Customer[10]; //要初始化 不然BankTest会出现customers是null!!!
}

public void addCustomer(String f,String l){
Customer customer=new Customer(f,l); //新创建的顾客
customers[this.numberOfCustomer++]=customer; //将顾客添加到顾客数组
}

public int getNumberOfCustomer(){
return this.numberOfCustomer;
}

public Customer getCustomer(int index){
if(index < 0 || index >= numberOfCustomer){
return null;
}
return customers[index]; //返回对应位置的顾客
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BankTest {
public static void main(String[] args) {
Bank bank = new Bank();

bank.addCustomer("操","曹");
bank.addCustomer("备","刘");

bank.getCustomer(0).setAccount(new Account(1000)); //银行类bank获取第一个顾客,顾客设定一个账户,账户里面有1000余额
bank.getCustomer(0).getAccount().withdraw(50); //余额取出50

System.out.println("账户余额为:" + bank.getCustomer(0).getAccount().getBalance()); //获取余额950

}
}

内存解析图:

image-20220323211653907

2. 面向对象特征二:继承(Inheritance)

2.1 继承的概述

角度一:从上而下

为描述和处理个人信息,定义类Person:

image-20220323220923386

为描述和处理学生信息,定义类Student:

image-20220323221001495

通过继承,简化Student类的定义:

image-20220323221050791

说明:Student类继承了父类Person的所有属性和方法,并增加了一个属性school。Person中的属性和方法,Student都可以使用。

角度二:从下而上

多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成继承关系。如图所示:

再举例:

image-20220323221436571

2.1.1 继承的好处

  • 继承的出现减少了代码冗余,提高了代码的复用性。

  • 继承的出现,更有利于功能的扩展。

  • 继承的出现让类与类之间产生了is-a的关系,为多态的使用提供了前提。

    • 继承描述事物之间的所属关系,这种关系是:is-a 的关系。可见,父类更通用、更一般,子类更具体。

注意:不要仅为了获取其他类中某个功能而去继承!

2.2 继承的语法

2.2.1 继承中的语法格式

通过 extends 关键字,可以声明一个类B继承另外一个类A,定义格式如下:

1
2
3
4
5
6
7
[修饰符] classA {  //类A,称为父类、超类、基类(base class)、SuperClass
//属性和方法
}

[修饰符] classB extendsA { //类B,称为子类、派生类(derived class)、SubClass
...
}

2.2.2 继承中的基本概念

类B,称为子类、派生类(derived class)、SubClass

类A,称为父类、超类、基类(base class)、SuperClass

2.3 代码举例

1、父类

1
2
3
4
5
6
7
8
9
10
public class Animal {
// 定义name属性
String name;
// 定义age属性
int age;
// 定义动物的吃东西方法
public void eat() {
System.out.println(age + "岁的"+ name + "在吃东西");
}
}

2、子类

1
2
3
4
5
6
7
8
public class Cat extends Animal {  //使用extends继承   
int count;//记录每只猫抓的老鼠数量
// 定义一个猫抓老鼠的方法catchMouse
public void catchMouse() {
count++;
System.out.println("抓老鼠,已经抓了"+ count + "只老鼠");
}
}

3、测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestCat {
public static void main(String[] args) {
// 创建一个猫类对象
Cat cat = new Cat();
// 为该猫类对象的name属性进行赋值 这是父类Animal的属性和方法(继承过来的)
cat.name = "Tom";
// 为该猫类对象的age属性进行赋值
cat.age = 2;
// 调用该猫继承来的eat()方法
cat.eat();
// 调用该猫的catchMouse()方法 这是自己类的方法
cat.catchMouse();
}
}

image-20211230090255997

2.4 继承性的细节说明

1、子类会继承父类所有的实例变量和实例方法

从类的定义来看,类是一类具有相同特性的事物的抽象描述。父类是所有子类共同特征的抽象描述。而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征。

  • 当子类对象被创建时,在堆中给new对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存
  • 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,父类—->父类—->父类是否声明了这个方法,遵循从下往上找的顺序,找到了就停止,一直到根父类都没有找到,就会报编译错误。

所以继承意味着子类的对象除了看子类的类模板还要看父类的类模板。

image-20211230090255997

2、子类不能直接访问父类中私有的(private)的成员变量和方法

子类虽会继承父类私有(private)的成员变量,但子类不能对继承的私有成员变量直接进行访问,——>通过get/set方法进行访问。 (继承性要保证封装性!!!)

如图所示:

image-20220323224757212

3、在Java 中,继承的关键字用的是“extends”,即子类不是父类的子集,而是对父类的“扩展”

子类在继承父类以后,还可以定义自己特有的方法,这就可以看做是对父类功能上的扩展。

4、Java支持多层继承(继承体系)

image-20220323225441417
1
2
3
class A{}
class B extends A{}
class C extends B{}

说明:

  • 子类和父类是一种相对的

  • 顶层父类是Object类。所有的类默认继承Object,作为父类。 (如果没有显示说明的话就是继承Object类)

5、一个父类可以同时拥有多个子类

1
2
3
4
class A{}
class B extends A{}
class D extends A{}
class E extends A{}

6、Java只支持单继承,不支持多重继承

image-20220514162507692
1
2
3
4
5
6
public class A{}
class B extends A{}

//一个类只能有一个父类,不可以有多个直接父类。
class C extends B{} // √
class C extends A,B... // ×

2.5 练习

练习1:定义一个学生类Student,它继承自Person类

image-20220323231804928
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person {
public String name;
public char sex;
public int age;

public Person(String name,char sex,int age){ //构造器
this.name=name;
this.sex=sex;
this.age=age;
}

public String toString(){
return "name是: "+this.name+"sex是: "+this.sex+" age是: "+this.age;
}

}
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 Student extends Person{
public long number;
public int math;
public int english;
public int computer;

public Student(String n,char s,int a,long k,int m,int e,int c){
super(n,s,a); //要使用super调用父类的三个属性 跟this(有参/无参)功能样式一样!!!
this.number=k;
this.math=m;
this.english=e;
this.computer=c;
}

public double aver(){
return 1.0;
}

public int max(){
return 1;
}

public int min(){
return 1;
}

}

练习2:

(1)定义一个ManKind类,包括

  • 成员变量int sex和int salary;

  • 方法void manOrWoman():根据sex的值显示“man”(sex==1)或者“woman”(sex==0);

  • 方法void employeed():根据salary的值显示“no job”(salary==0)或者“ job”(salary!=0)。

(2)定义类Kids继承ManKind,并包括

  • 成员变量int yearsOld;
  • 方法printAge()打印yearsOld的值。

(3)定义类KidsTest,在类的main方法中实例化Kids的对象someKid,用该对象访问其父类的成员变量及方法。

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
public class ManKind {
private int sex; //属性设置为私有 子类只能通过get/set方法获取 [不能直接获取]
private int salary; //属性设置为私有 子类只能通过get/set方法获取 [不能直接获取]

public void manOrWoman(){ //我的误区就是经常往里面传值,其实成员变量没必要,传入的是局部变量!!!
if(sex==1){
System.out.println("man");
return;
}
if(sex==0){
System.out.println("woman");
return;
}
System.out.println("输入sex异常");
}

public int getSex() {
return sex;
}

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

public int getSalary() {
return salary;
}

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

//无参构造器
public ManKind() {
}

//有参构造器
public ManKind(int sex, int salary) {
this(); //调用无参构造器public ManKind(){}
this.sex = sex;
this.salary = salary;
}

public void employeed(){ //我的误区就是经常往里面传值,其实成员变量没必要,传入的是局部变量!!!
if(salary==0){
System.out.println("no job");
return;
}
System.out.println("job");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Kids extends ManKind{  //使用extends关键字继承父类
private int yearsOld; //属性设置成私有 其他人获取只能通过get和set方法 而且private只能在类中和同一包内

public int getYearsOld() {
return yearsOld;
}

public void setYearsOld(int yearsOld) {
this.yearsOld = yearsOld;
}

public void printAge(){
System.out.println(this.yearsOld);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class KidsTest {
public static void main(String[] args) {
Kids someKid=new Kids();
//调用父类属性
someKid.setSex(1);
someKid.setSalary(1); //相当于子类拥有了父类和自己的属性 三个属性一共
//调用父类方法
someKid.manOrWoman();
someKid.employeed();
}
}


输出:
man
job

练习3:根据下图实现类。在CylinderTest类中创建Cylinder类的对象,设置圆柱的底面半径和高,并输出圆柱的体积。

image-20220323231942361
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Circle {
private double radius;
public Circle(){
this.radius=1; //初始化为1
}

//声明set和get方便其他类访问private成员变量
public void setRadius(double radius){
this.radius=radius;
}

public double getRadius(){
return this.radius;
}

public double findArea(){
return this.radius*this.radius*Math.PI; //计算圆的面积
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Cylinder extends Circle{  //使用extends继承父类圆Circle
private double length;

public Cylinder(){
this.length=1; //初始化为1
}

//声明set和get方便其他类访问private成员变量
public void setLength(double length) {
this.length = length;
}

public double getLength(){
return this.length;
}

public double findVolume(){
return this.findArea()*length; //或者getLength()
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class CylinderTest {
public static void main(String[] args) {
Cylinder cy = new Cylinder(); //创建一个圆柱对象 子类
cy.setRadius(2.3); //子类使用父类的属性
cy.setLength(1.4); //子类使用自己的方法
System.out.println("圆柱的体积为:" + cy.findVolume());
System.out.println("圆柱的底面积:" + cy.findArea()); //子类使用父类的方法
}
}

输出:
圆柱的体积为:23.266635192486003
圆柱的底面积:16.619025137490002

3. 方法的重写(override/overwrite)

父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于自己当前的类,该怎么办呢?子类可以对从父类中继承来的方法进行改造,我们称为方法的重写 (override、overwrite)。也称为方法的重置覆盖

在程序执行时,子类的方法将覆盖父类的方法。

3.1 方法重写举例

比如新的手机增加来电显示头像的功能,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.atguigu.inherited.method;

public class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.atguigu.inherited.method;

//SmartPhone:智能手机
public class SmartPhone extends Phone{
//重写父类的来电显示功能的方法
@Override
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
}
//重写父类的通话功能的方法
@Override
public void call() {
System.out.println("语音通话 或 视频通话");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.inherited.method;

public class TestOverride {
public static void main(String[] args) {
// 创建子类对象
SmartPhone sp = new SmartPhone();

// 调用父类继承而来的方法
sp.call();

// 调用子类重写的方法
sp.showNum();
}
}

@Override使用说明:

写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰的知道这是一个重写的方法。

3.2 方法重写的要求

  1. 子类重写的方法必须和父类被重写的方法具有相同的方法名称参数列表

  2. 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型。(例如:Student < Person)。

注意:如果返回值类型是基本数据类型和void,那么必须是相同

  1. 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限。(public > protected > 缺省 > private)

注意:① 父类私有方法不能重写 ② 跨包的父类缺省的方法也不能重写

  1. 子类方法抛出的异常不能大于父类被重写方法的异常

此外,子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。

3.3 小结:方法的重载与重写

方法的重载:方法名相同,形参列表不同。不看返回值类型。

方法的重写:见上面。

(1)同一个类中

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.atguigu.inherited.method;

public class TestOverload {
public int max(int a, int b){
return a > b ? a : b;
}
public double max(double a, double b){
return a > b ? a : b;
}
public int max(int a, int b,int c){
return max(max(a,b),c);
}
}

(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
package com.atguigu.inherited.method;

public class TestOverloadOverride {
public static void main(String[] args) {
Son s = new Son();
s.method(1);//只有一个形式的method方法

Daughter d = new Daughter();
d.method(1);
d.method(1,2);//有两个形式的method方法
}
}

class Father{
public void method(int i){
System.out.println("Father.method");
}
}
class Son extends Father{
public void method(int i){//重写
System.out.println("Son.method");
}
}
class Daughter extends Father{
public void method(int i,int j){//重载
System.out.println("Daughter.method");
}
}

3.4 练习

练习1:如果现在父类的一个方法定义成private访问权限,在子类中将此方法声明为default访问权限,那么这样还叫重写吗? (NO)

练习2:修改继承内容的练习2中定义的类Kids,在Kids中重新定义employeed()方法,覆盖父类ManKind中定义的employeed()方法,输出“Kids should study and no job.”

4. 再谈封装性中的4种权限修饰

权限修饰符:public,protected,缺省,private

修饰符 本类 本包 其他包子类 其他包非子类
private × × ×
缺省 √(本包子类和非子类都可见) × ×
protected √(本包子类和非子类都可见) √(其他包仅限于子类中可见) ×
public

外部类:public和缺省

成员变量、成员方法等:public,protected,缺省,private

1、外部类要跨包使用必须是public,否则仅限于本包使用

(1)外部类的权限修饰符如果缺省,本包使用没问题

image-20211230093627763

(2)外部类的权限修饰符如果缺省,跨包使用有问题

image-20211230094236974

2、成员的权限修饰符问题

(1)本包下使用:成员的权限修饰符可以是public、protected、缺省

image-20211230095320646

(2)跨包下使用:要求严格

image-20211230095817784

(3)跨包使用时,如果类的权限修饰符缺省,成员权限修饰符>类的权限修饰符也没有意义

image-20211230100219840

5. 关键字:super

5.1 super的理解

在Java类中使用super来调用父类中的指定操作:

  • super可用于访问父类中定义的属性
  • super可用于调用父类中定义的成员方法
  • super可用于在子类构造器中调用父类的构造器

注意:

  • 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
  • super的追溯不仅限于直接父类[从下往上一直找]
  • super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识

5.2 super的使用场景

5.2.1 子类中调用父类被重写的方法

  • 如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法;
  • 如果子类重写了父类的方法,在子类中需要通过super.才能调用父类被重写的方法,否则默认调用的子类重写的方法

举例:

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
package com.atguigu.inherited.method;

public class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}

//smartphone:智能手机
public class SmartPhone extends Phone{
//重写父类的来电显示功能的方法
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");

//保留父类来电显示号码的功能
super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
}
}

总结:

  • 方法前面没有super.和this.

    • 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
  • 方法前面有this.

    • 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
  • 方法前面有super.

    • 从当前子类的直接父类找,如果没有,继续往上追溯

5.2.2 子类中调用父类中同名的成员变量

  • 如果实例变量与局部变量重名,可以在实例变量前面加this.进行区别
  • 如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量
  • 如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问

举例:

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
class Father{
int a = 10;
int b = 11;
}
class Son extends Father{
int a = 20;

public void test(){
//子类与父类的属性同名,子类对象中就有两个a
System.out.println("子类的a:" + a);//20 先找局部变量找,没有再从本类成员变量找
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找

//子类与父类的属性不同名,是同一个b
System.out.println("b = " + b);//11 先找局部变量找,没有再从本类成员变量找,没有再从父类找
System.out.println("b = " + this.b);//11 先从本类成员变量找,没有再从父类找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}

public void method(int a, int b){
//子类与父类的属性同名,子类对象中就有两个成员变量a,此时方法中还有一个局部变量a
System.out.println("局部变量的a:" + a);//30 先找局部变量
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找

System.out.println("b = " + b);//13 先找局部变量
System.out.println("b = " + this.b);//11 先从本类成员变量找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
}
class Test{
public static void main(String[] args){
Son son = new Son();
son.test();
son.method(30,13);
}
}

总结:起点不同(就近原则)

  • 变量前面没有super.和this.

    • 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的局部变量
    • 如果不是局部变量,先从当前执行代码的本类去找成员变量
    • 如果从当前执行代码的本类中没有找到,会往上找父类声明的成员变量(权限修饰符允许在子类中访问的)
  • 变量前面有this.

    • 通过this找成员变量时,先从当前执行代码的==本类去找成员变量==
    • 如果从当前执行代码的本类中没有找到,会往上找==父类声明的成员变量(==权限修饰符允许在子类中访问的)
  • 变量前面super.

    • 通过super找成员变量,直接从当前执行代码的直接父类去找成员变量(权限修饰符允许在子类中访问的)
    • 如果直接父类没有,就去父类的父类中找(权限修饰符允许在子类中访问的)

特别说明:应该避免子类声明和父类重名的成员变量

在阿里的开发规范等文档中都做出明确说明:

image-20211230110411580

5.2.3 子类构造器中调用父类构造器

① 子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方式调用父类指定的构造器

② 规定:“super(形参列表)”,必须声明在构造器的首行。(类似于this也在首行)

③ 我们前面讲过,在构造器的首行可以使用”this(形参列表)”,调用本类中重载的构造器,
结合②,结论:在构造器的首行,“this(形参列表)” 和 “super(形参列表)”只能二选一

④ 如果在子类构造器的首行既没有显示调用”this(形参列表)”,也没有显式调用”super(形参列表)”, [先父类后子类]
​ 则子类此构造器默认调用”super()”,即调用父类中空参的构造器

⑤ 由③和④得到结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。
只能是这两种情况之一。 —–写this就是掉自己 写super就是掉父类 不写就是父类的无参构造器!!!!!

⑥ 由⑤得到:一个类中声明有n个构造器,最多有n-1个构造器中使用了”this(形参列表)”,则剩下的那个一定使用”super(形参列表)”。
这里的最后一个可能是写了super,也可能没写用的默认的父类无参的

开发中常见错误:

如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参的构造器,则编译出错

情景举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A{

}
class B extends A{

}

class Test{
public static void main(String[] args){
B b = new B();
//A类和B类都是默认有一个无参构造,B类的默认无参构造中还会默认调用A类的默认无参构造
//但是因为都是默认的,没有打印语句,看不出来
}
}

情景举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{

}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类默认有一个无参构造,
//B类的默认无参构造中会默认调用A类的无参构造
//可以看到会输出“A类无参构造器"
}
}

情景举例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
B(){
System.out.println("B类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类显示声明一个无参构造,
//B类的无参构造中虽然没有写super(),但是仍然会默认调用A类的无参构造
//可以看到会输出“A类无参构造器"和"B类无参构造器")
}
}

情景举例4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
B(){
super();
System.out.println("B类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类显示声明一个无参构造,
//B类的无参构造中明确写了super(),表示调用A类的无参构造
//可以看到会输出“A类无参构造器"和"B类无参构造器")
}
}

情景举例5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
System.out.println("B类无参构造器");
}
}
class Test05{
public static void main(String[] args){
B b = new B();
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个无参构造,
//B类的无参构造没有写super(...),表示默认调用A类的无参构造
//编译报错,因为A类没有无参构造
}
}

image-20200227141228450

情景举例6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
super();
System.out.println("B类无参构造器");
}
}
class Test06{
public static void main(String[] args){
B b = new B();
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个无参构造,
//B类的无参构造明确写super(),表示调用A类的无参构造
//编译报错,因为A类没有无参构造
}
}

image-20200303183542807

情景举例7:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(int a){
super(a);
System.out.println("B类有参构造器");
}
}
class Test07{
public static void main(String[] args){
B b = new B(10);
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个有参构造,
//B类的有参构造明确写super(a),表示调用A类的有参构造
//会打印“A类有参构造器"和"B类有参构造器"
}
}

情景举例8:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class A{
A(){
System.out.println("A类无参构造器");
}
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
super();//可以省略,调用父类的无参构造
System.out.println("B类无参构造器");
}
B(int a){
super(a);//调用父类有参构造
System.out.println("B类有参构造器");
}
}
class Test8{
public static void main(String[] args){
B b1 = new B();
B b2 = new B(10);
}
}

5.3 小结:this与super

1、this和super的意义

this:当前对象

  • 在构造器和非静态代码块中,表示正在new的对象
  • 在实例方法中,表示调用当前方法的对象

super:引用父类声明的成员

2、this和super的使用格式

  • this
    • this.成员变量:表示当前对象的某个成员变量,而不是局部变量
    • this.成员方法:表示当前对象的某个成员方法,完全可以省略this.
    • this()或this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
  • super
    • super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
    • super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
    • super()或super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错

5.4 练习

练习1:修改方法重写的练习2中定义的类Kids中employeed()方法,在该方法中调用父类ManKind的employeed()方法,然后再输出“but Kids should study and no job.”

练习2:修改继承中的练习3中定义的Cylinder类,在Cylinder类中覆盖findArea()方法,计算圆柱的表面积。考虑:findVolume方法怎样做相应的修改?

在CylinderTest类中创建Cylinder类的对象,设置圆柱的底面半径和高,并输出圆柱的表面积和体积。

1
2
3
4
5
6
7
8
9
10
public double findVolume(){
return super.findArea()*length; //直接super调用父类的方法
}

@Override
public double findArea(){
//S表=2πr²+2πrh
//直接super调用父类的属性 因为是private就改成调用get方法
return 2*Math.PI*super.getRadius()*super.getRadius()+2*Math.PI*super.getRadius()*this.length;
}

附加题:在CylinderTest类中创建一个Circle类的对象,设置圆的半径,计算输出圆的面积。体会父类和子类成员的分别调用。

1
2
3
Circle circle=new Circle();
circle.setRadius(2);
System.out.println(circle.findArea()); //直接自己调用自己方法就行

练习3:

1、写一个名为Account的类模拟账户。该类的属性和方法如下图所示。该类包括的属性:账号id,余额balance,年利率annualInterestRate;包含的方法:访问器方法(getter和setter方法),返回月利率的方法getMonthlyInterest(),取款方法withdraw(),存款方法deposit()。

image-20220324003430464

写一个用户程序测试Account类。在用户程序中,创建一个账号为1122、余额为20000、年利率4.5%的Account对象。使用withdraw方法提款30000元,并打印余额。
再使用withdraw方法提款2500元,使用deposit方法存款3000元,然后打印余额和月利率。

提示:在提款方法withdraw中,需要判断用户余额是否能够满足提款数额的要求,如果不能,应给出提示。

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
public class Account {
private int id; //账号
private double balance; //余额
private double annuallnterestRate; //年利率

//三个private属性的set和get方法
public void setId(int id){
this.id=id;
}

public int getId(){
return this.id;
}

public void setBalance(double balance) {
this.balance = balance;
}

public double getBalance() {
return this.balance;
}

public void setAnnuallnterestRate(double annuallnterestRate) {
this.annuallnterestRate = annuallnterestRate;
}

public double getAnnuallnterestRate() {
return this.annuallnterestRate;
}

//构造器
public Account(int id,double balance,double annuallnterestRate){
this.id=id;
this.balance=balance;
this.annuallnterestRate=annuallnterestRate;
}

//返回月利率
public double getMonthlyInterest(){
return this.annuallnterestRate/12;
}

//取款
public void withdraw(double amount){
if(balance<amount){
System.out.println("余额不够!!!");
return; //一定要return 不然余额还是会扣除
}
//取款
balance-=amount;
}

//存款
public void deposit(double amount){
//存款
balance+=amount;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AccountTest {
public static void main(String[] args) {
Account account=new Account(1122,20000,0.045);
//取款30000
account.withdraw(30000);
System.out.println("现在余额是:"+account.getBalance());
//取款2500
account.withdraw(2500);
//存款3000
account.deposit(3000);
System.out.println("现在余额是:"+account.getBalance());
System.out.println("现在是月利率是:"+account.getMonthlyInterest());
}
}

运行结果如图所示:

image-20231003170202016

2、创建Account类的一个子类CheckAccount代表可透支的账户,该账户中定义一个属性overdraft代表可透支限额。在CheckAccount类中重写withdraw方法,其算法如下:

1
2
3
4
5
6
7
8
如果(取款金额<账户余额),
可直接取款
如果(取款金额>账户余额),
计算需要透支的额度
判断可透支额overdraft是否足够支付本次透支需要,如果可以
将账户余额修改为0,冲减可透支金额
如果不可以
提示用户超过可透支额的限额

要求:写一个用户程序测试CheckAccount类。在用户程序中,创建一个账号为1122、余额为20000、年利率4.5%,可透支限额为5000元的CheckAccount对象。

使用withdraw方法提款5000元,并打印账户余额和可透支额。

再使用withdraw方法提款18000元,并打印账户余额和可透支额。

再使用withdraw方法提款3000元,并打印账户余额和可透支额。

提示:

(1)子类CheckAccount的构造方法需要将从父类继承的3个属性和子类自己的属性全部初始化。【不然被报错】

(2)父类Account的属性balance被设置为private,但在子类CheckAccount的withdraw方法中需要修改它的值,因此应修改父类的balance属性,定义其为protected。【也可以不修改,子类继承父类所有方法 直接用super.setBalance(super.getBalance()-amount);】

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
//代表 可透支的账户
public class CheckAccount extends Account { //子类
private double overdraft; // 可透支额度

//子类构造器
public CheckAccount(int id, double balance, double annuallnterestRate,double overdraft) {
super(id, balance, annuallnterestRate); //显式用super调用父类构造器 [如果不写的话就默认是super()无参构造器]
this.overdraft=overdraft; //自己属性
}

//get和set方法
public double getOverdraft() {
return overdraft;
}

public void setOverdraft(double overdraft) {
this.overdraft = overdraft;
}

//重写withdraw方法取款
@Override
public void withdraw(double amount){
//因为子类没有balance属性 所以不写super也会从子类找到父类
if(amount<super.getBalance()){
super.setBalance(super.getBalance()-amount); //符合条件可以取款
}
if(amount>super.getBalance()){
double cha=amount-super.getBalance(); //需要透支的额度
if(this.overdraft>cha){
super.setBalance(0); //账户余额修改为0 减少可透支余额
this.overdraft-=cha;
}else{
System.out.println("超过了可透支余额的限额");
return;
}
}

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CheckAccountTest {
public static void main(String[] args) {
CheckAccount checkAccount=new CheckAccount(1122,20000,0.045,5000);
System.out.println("一开始余额是:"+checkAccount.getOverdraft());
System.out.println("一开始可透支额度是:"+checkAccount.getBalance());
System.out.println("--------------------------------------------");
checkAccount.withdraw(5000);
System.out.println("取了5000之后余额是:"+checkAccount.getBalance());
System.out.println("现在可透支额度是:"+checkAccount.getOverdraft());
System.out.println("--------------------------------------------");
checkAccount.withdraw(18000);
System.out.println("取了18000之后余额是:"+checkAccount.getBalance());
System.out.println("现在可透支额度是:"+checkAccount.getOverdraft());
System.out.println("--------------------------------------------");
checkAccount.withdraw(3000);
System.out.println("取了3000之后余额是:"+checkAccount.getBalance());
System.out.println("现在可透支额度是:"+checkAccount.getOverdraft());

}
}

运行结果如下图所示:

image-20231003172602789

6. 子类对象实例化全过程

image-20220324003713230

1
Dog dog = new Dog("小花","小红");
image-20220324003735416 image-20220324003813163

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Creature {
public Creature() {
System.out.println("Creature无参数的构造器");
}
}
class Animal extends Creature {
public Animal(String name) {
System.out.println("Animal带一个参数的构造器,该动物的name为" + name);
}
public Animal(String name, int age) {
this(name); //调用上面的构造器
System.out.println("Animal带两个参数的构造器,其age为" + age);
}
}
public class Dog extends Animal {
public Dog() {
super("汪汪队阿奇", 3); //调用父类的构造器
System.out.println("Dog无参数的构造器");
}
public static void main(String[] args) {
new Dog();
}
}

运行结果如下图所示:

image-20231003201658599

7. 面向对象特征三:多态性

一千个读者眼中有一千个哈姆雷特。

7.1 多态的形式和体现

7.1.1 对象的多态性

多态性,是面向对象中最重要的概念,在Java中的体现:对象的多态性:父类的引用指向子类的对象

格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)

1
父类类型 变量名 = 子类对象;

举例:

1
2
3
4
5
Person p = new Student();

Object o = new Person();//Object类型的变量o,指向Person类型的对象

o = new Student(); //Object类型的变量o,指向Student类型的对象

对象的多态:在Java中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象

7.1.2 多态的理解

Java引用变量有两个类型:编译时类型运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。
简称:编译时,看左边;运行时,看右边。

  • 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)

  • 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法)

    “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
  • 多态的使用前提:① 类的继承关系 ② 方法的重写

  • 多态的适用性:适用于方法,不适用于属性

7.1.3 举例

创建人、男人和女人三个class。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Person {
String name;
int age;
int id=1001;

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

public void walk(){
System.out.println("人走路");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Man extends Person{

boolean isSmoking;
int id=1002;
@Override
public void eat(){
System.out.println("男人多吃肉,长肌肉");
}
@Override
public void walk(){
System.out.println("男人笔挺的走路");
}

public void earnMoney(){
System.out.println("男人挣钱养家");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Woman extends Person{

boolean isBeauty;

@Override
public void eat(){
System.out.println("女人应该少吃,减肥");
}

@Override
public void walk(){
System.out.println("女人窈窕的走路");
}

public void goShopping(){
System.out.println("女人喜欢逛街...");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class PersonTest {
public static void main(String[] args) {
//1.多态性之前的场景
Person p1=new Person();

//2.多态性 {左右不一致}
Person p2=new Man();
//走的是man的方法
System.out.println(p2.id);

}
}

image-20231003220858899

7.2 为什么需要多态性(polymorphism)?

开发中,有时我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。
[在设计时候无法确定具体是什么?]

案例:

(1)声明一个Animal类,包含eat()和jump()方法

(2)声明一个Cat类,重写eat()和jump()方法,并且多一个catchMouse()方法

(3)声明一个Dog类,重写eat()和jump()方法,并且多一个watchDoor()方法

(4)声明一个AnimalTest类,包含adopt(Animal animal)方法,主要是收养动物【这时候不知道收养的是什么】 –> 考虑多态性

1
2
3
4
5
6
7
8
class Animal{
public void eat(){
System.out.println("动物进食");
}
public void jump(){
System.out.println("动物跳");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class Cat extends Animal{
@Override
public void eat(){
System.out.println("猫吃鱼");
}
@Override
public void jump(){
System.out.println("猫跳~~");
}
public void catchMouse(){
System.out.println("猫抓老鼠");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class Dog extends Animal{
@Override
public void eat(){
System.out.println("狗吃骨头");
}
@Override
public void jump(){
System.out.println("狗急跳墙");
}
public void watchDoor(){
System.out.println("狗能看家");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AnimalTest {
public static void main(String[] args) {
AnimalTest test=new AnimalTest();
//多态性就可以直接更换new的对象就行 就不用写很多adopt方法了
test.adopt(new Dog()); //就可以执行时候考虑具体的动物对象!!!
test.adopt(new Cat()); //就可以执行时候考虑具体的动物对象!!!
}
//领养动物
public void adopt(Animal animal){ //声明一个动物 但是实际上传进来new可以是Dog 或者Cat
animal.eat();
animal.jump();
//在这个情况下就不能写animal.子类方法()!!!! 因为设计的时候只是知道是个动物
}
}

7.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
//多态的好处!!!
public class AnimalTest {
public static void main(String[] args) {
AnimalTest test=new AnimalTest();
//多态性就可以直接更换new的对象就行 就不用写很多adopt方法了
test.adopt(new Dog()); //就可以执行时候考虑具体的动物对象!!!
test.adopt(new Cat()); //就可以执行时候考虑具体的动物对象!!!
}
//领养动物
public void adopt(Animal animal){ //声明一个动物 但是实际上传进来new可以是Dog 或者Cat ------多态的好处!!!!!!!
animal.eat();
animal.jump();
//在这个情况下就不能写animal.子类方法()!!!! 因为设计的时候只是知道是个动物
}
}

//多态的弊端!!!
//[基于7.1.3举例的Person Woman Man[]
public class PersonTest1 {
public static void main(String[] args) {
//向下转型
Person p1=new Man();
//不能调用子类特有的属性和方法 ------多态的弊端!!!!!!!!
//p1.earnMoney();
//System.out.println(p1.isSmoking);
}
}
}

开发中:

方法形参:父类,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。【你具体的类可以更新,毕竟我设计的时候用的是父类,这个不改就行】

【开闭原则OCP】

  • 对扩展开放,对修改关闭
  • 通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能

7.4 虚方法调用(Virtual Method Invocation)

在Java中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。

1
2
Person e = new Student();
e.getInfo(); //调用Student类的getInfo()方法

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。

举例:

image-20220324234208997

前提:Person类中定义了welcome()方法,各个子类重写了welcome()。

image-20220324234214932

执行:多态的情况下,调用对象的welcome()方法,实际执行的是子类重写的方法。

拓展:

静态链接(或早起绑定):当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。那么调用这样的方法,就称为非虚方法调用。比如调用静态方法、私有方法、final方法、父类构造器、本类重载构造器等。

动态链接(或晚期绑定):如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。调用这样的方法,就称为虚方法调用。比如调用重写的方法(针对父类)、实现的方法(针对接口)。

7.5 成员变量没有多态性

  • 若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。

  • 对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.atguigu.polymorphism.grammar;

public class TestVariable {
public static void main(String[] args) {
Base b = new Sub();
System.out.println(b.a);
System.out.println(((Sub)b).a);

Sub s = new Sub();
System.out.println(s.a);
System.out.println(((Base)s).a);
}
}
class Base{
int a = 1;
}
class Sub extends Base{
int a = 2;
}

7.6 向上转型与向下转型

首先,一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。

7.6.1 为什么要类型转换

​ 多态的弊端,因为多态我们就不能调用子类特有的方法和属性。所以,想要调用子类特有的方法必须做类型转换,使得编译通过

image-20220324235337563
  • 向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型

    • 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
    • 此时,一定是安全的,而且也是自动完成的
  • 向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型

    • 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型
    • 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断

7.6.2 如何向上或向下转型

向上转型:自动完成

向下转型:(子类类型)父类变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//基于7.1.3举例的Person Woman Man
public class PersonTest1 {
public static void main(String[] args) {
//向下转型
Person p1=new Man();
//不能调用子类特有的属性和方法
//p1.earnMoney();
//System.out.println(p1.isSmoking);

//向下转型【强转】
Man m1=(Man)p1; //p1和m1指向堆的同一个地址
m1.earnMoney();
System.out.println(m1.isSmoking);

//可能会出现类型转换异常【ClassCastException】
//Person p2=new Woman();
//Man m2=(Man)p2;
//m2.earnMoney();
// |
// |

}
}

image-20231003220521144

7.6.3 instanceof关键字

但是向下转型类似于数据类型转换一样,可能会存在并不是这个子类,但是你要转移成这个子类,就会出现ClassCastException!!!

image-20231003221329249

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验。如下代码格式:

1
2
//检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A
  • 说明:
    • 只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
    • 如果对象a属于类A的子类B,a instanceof A值也为true。
    • 要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误。
    • 如果a instanceOf A 返回true,则:a instanceOf superA 返回也是true。[A是superA的子类]

代码:

1
2
3
4
5
Person p2=new Woman();
if(p2 instanceof Man){ //如果属于女人类就进强制转换
Man m2=(Man)p2;
m2.earnMoney();
}

7.7 练习

练习1:笔试&面试

题目1:继承成员变量和继承方法的区别

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
class Base {
int count = 10;
public void display() {
System.out.println(this.count); //展示base类的count属性10
}
}

class Sub extends Base { //子类
int count = 20;
@Override
public void display() {
System.out.println(this.count); //展示Sub类的count属性20
}
}

public class FieldMethodTest {
public static void main(String[] args){
//创建一个子类对象
Sub s = new Sub();
System.out.println(s.count); // 输出20
s.display(); // 输出20

//多态
Base b = s;
System.out.println(b == s); // 因为两个指向同一个子类对象 所以输出true
System.out.println(b.count); //这时候还是父类的属性 输出10
b.display(); //输出20
}
}

题目2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//考查多态的笔试题目:
public class InterviewTest1 {
public static void main(String[] args) {
//多态
Base base = new Sub();
base.add(1, 2, 3); //调用被重写的子类方法 输出sub_1
}
}

class Base {
public void add(int a, int... arr) {
System.out.println("base");
}
}

class Sub extends Base {
@Override
public void add(int a, int[] arr) {
System.out.println("sub_1");
}
}

题目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
//getXxx()和setXxx()声明在哪个类中,内部操作的属性就是哪个类里的。
public class InterviewTest2 {
public static void main(String[] args) {
//普通创建父类对象f
Father f = new Father();
//普通创建子类对象s
Son s = new Son();
System.out.println(f.getInfo()); //输出父类属性 atguigu
System.out.println(s.getInfo()); //输出子类属性 尚硅谷
//输出子类的方法
s.test(); //输出 尚硅谷 atguigu
System.out.println("-----------------");
//更改子类的属性为大硅谷
s.setInfo("大硅谷");
System.out.println(f.getInfo()); //输出 atguigu
System.out.println(s.getInfo()); //输出 大硅谷
s.test(); //输出 大硅谷 atguigu
}
}

class Father {
private String info = "atguigu";

//私有属性的get和set方法
public void setInfo(String info) {
this.info = info;
}

public String getInfo() {
return info;
}
}

class Son extends Father{ //继承父类
private String info = "尚硅谷";
@Override
public void setInfo(String info) {
this.info = info;
}
@Override
public String getInfo() {
return info;
}

public void test() {
System.out.println(this.getInfo()); //调用子类的属性 尚硅谷
System.out.println(super.getInfo()); //调用父类的属性 atguigu
}
}

题目4:多态是编译时行为还是运行时行为?

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
import java.util.Random;
class Animal{ //父类
protected void eat() {
System.out.println("animal eat food");
}
}

class Cat extends Animal{ //子类继承父类
@Override
protected void eat() {
System.out.println("cat eat fish");
}
}

class Dog extends Animal{ //子类继承父类
@Override
public void eat() {
System.out.println("Dog eat bone");
}
}

class Sheep extends Animal{ //子类继承父类
@Override
public void eat() {
System.out.println("Sheep eat grass");
}
}

public class InterviewTest{
public static Animal getInstance(int key) { //返回值是一个动物父类
switch (key) {
case 0:
return new Cat ();
case 1:
return new Dog ();
default:
return new Sheep ();
}
}

public static void main(String[] args) {
int key = new Random().nextInt(3); //随机生成0-3中的随机数
System.out.println(key); //输出随机数
//多态
Animal animal = getInstance(key); //获取子类
animal.eat(); //是哪个子类就输出哪个子类的方法
}
}

练习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
class Person {
//protected修饰符 --可以跨包但是仅限子类可以获取到
protected String name="宋亚翔";
protected int age=24;
public String getInfo() {
return "Name: "+ name + "\n" +"age: "+ age;
}
}
class Student extends Person {
//protected修饰符 --可以跨包但是仅限子类可以获取到
protected String school="东北林业大学"; //子类特有的属性
@Override
public String getInfo() {
return "Name: "+ name + "\nage: "+ age + "\nschool: "+ school;
}
}
class Graduate extends Student{
public String major="计算机"; //子类特有的属性
@Override
public String getInfo()
{
return "Name: "+ name + "\nage: "+ age + "\nschool: "+ school+"\nmajor:"+major;
}
}

建立InstanceTest 类,在类中定义方法method(Person e);
在method中:
(1)根据e的类型调用相应类的getInfo()方法。
(2)根据e的类型执行:
如果e为Person类的对象,输出:
“a person”;
如果e为Student类的对象,输出:
“a student”
“a person ”
如果e为Graduate类的对象,输出:
“a graduated student”
“a student”
“a 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class InstanceTest {
public static void main(String[] args) {
Person person=new Person();
Person person1=new Student();
Person person2=new Graduate();
InstanceTest test=new InstanceTest();
test.method(person);
System.out.println("--------------------------");
test.method(person1);
System.out.println("--------------------------");
test.method(person2);
System.out.println("--------------------------");
}

public void method(Person e){
//设置key来接到底是哪个类
int key = 0;
if(e instanceof Person){
key=0;
}
if(e instanceof Student){
key=1;
}
if(e instanceof Graduate){
key=2;
}
//根据key判断是哪个类型按照要求输出
switch (key){
case 0:System.out.println(e.getInfo());System.out.println("a person");
break;
case 1:System.out.println(e.getInfo());
System.out.println("a person");
System.out.println("a student");
break;
case 2:System.out.println(e.getInfo());
System.out.println("a person");
System.out.println("a student");
System.out.println("a graduate");
break;
}
}
}

image-20231005215343643

练习3:定义三个类,父类GeometricObject代表几何形状,子类Circle代表圆形,MyRectangle代表矩形。定义一个测试类GeometricTest,编写equalsArea方法测试两个对象的面积是否相等(注意方法的参数类型,利用动态绑定技术),编写displayGeometricObject方法显示对象的面积(注意方法的参数类型,利用动态绑定技术)。

image-20220325000034619
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 GeometricObject {
// 类图中#表示protected修饰符 --可以跨包但是只能是子类使用
protected String color;
protected double weight;

//有参构造器
protected GeometricObject(String color,double weight){
this.color=color;
this.weight=weight;
}

//set和get方法
public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}

public double getWeight() {
return weight;
}

public void setWeight(double weight) {
this.weight = weight;
}

//要被重写的方法
public double findArea(){
return 0.0; //默认输出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
//圆类
public class Circle extends GeometricObject{

//子类私有属性 -半径
private double radius;

//构造器
protected Circle(double radius,String color, double weight) {
super(color, weight); //【首行】调用父类构造器
this.radius=radius;
}

//set和get方法
public double getRadius() {
return radius;
}

public void setRadius(double radius) {
this.radius = radius;
}

@Override
public double findArea(){
return Math.PI*radius*radius; //重写方法 返回Πr平方²
}

}
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 MyRectangle extends GeometricObject{

private double width;
private double height;

//构造器
public MyRectangle(double width,double height,String color,double weight){
super(color, weight); //【首行】调用父类构造器
this.width=width;
this.height=height;
}

//set和get方法
public double getWidth() {
return width;
}

public void setWidth(double width) {
this.width = width;
}

public double getHeight() {
return height;
}

public void setHeight(double height) {
this.height = height;
}

@Override
public double findArea(){
return width*height; //重写方法 返回长*宽
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class GeometricTest {
public static void main(String[] args) {
//两个多态
GeometricObject g1=new Circle(3,"黄色",10);
GeometricObject g2=new MyRectangle(3,4,"绿色",10);
//设定test对象测试方法
GeometricTest test=new GeometricTest();
System.out.println(test.equalsArea(g1,g2));
System.out.println(test.displayGeometricObject(g1));
System.out.println(test.displayGeometricObject(g2));
}

public boolean equalsArea(GeometricObject g1,GeometricObject g2){
return g1.findArea()==g2.findArea(); //判断两个对象的面积是否相等
}

public double displayGeometricObject(GeometricObject g){
//根据对象输出对应的方法
return g.findArea();
}

}

结果截图如下:

image-20231005222429951

8. Object 类

8.1 如何理解根父类

java.lang.Object是类层次结构的根类,即所有其它类的父类。每个类都使用 Object 作为超类。

image-20220503104750655
  • Object类型的变量与除Object以外的任意引用数据类型的对象都存在多态引用

    1
    2
    3
    4
    method(Object obj){…} //可以接收任何类作为其参数

    Person o = new Person();
    method(o);
  • 所有对象(包括数组)都实现这个类的方法。

  • 如果一个类没有特别指定父类,那么默认则继承自Object类。例如:

    1
    2
    3
    4
    5
    6
    7
    public class Person {
    ...
    }
    //等价于:
    public class Person extends Object {
    ...
    }

8.2 Object类的方法

根据JDK源代码及Object类的API文档,Object类当中包含的方法有11个。这里我们主要关注其中的6个:

1、(重点)equals()

= =:

  • 1.基本类型:比较值:只要两个变量的值相等,即为true。

    1
    2
    int a=5; 
    if(a==6){…}
  • 2.引用类型:比较引用(是否指向同一个对象):只有指向同一个对象时,==才返回true。

    1
    2
    3
    Person p1=new Person();  	    
    Person p2=new Person();
    if (p1==p2){…}
    • 用“==”进行比较时,符号两边的数据类型必须兼容(可自动转换的基本数据类型除外),否则编译出错

equals():所有类都继承了Object,也就获得了equals()方法。还可以重写。

  • 只能比较引用类型,Object类源码中equals()的作用与“==”相同:比较是否指向同一个对象。

    image-20220503104750655
  • 格式:obj1.equals(obj2)

  • 没有重写Object中equals()方法[自定义的类] –> 比较两个对象的引用地址是否相同(或比较两个对象是否指向了堆空间中的同一个对象实体)

    重写了Object类中的equals()方法[String、File、Date和包装类等] –> 用于比较两个对象的实体内容是否相等

  • 重写equals()方法的原则

    • 对称性:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。

    • 自反性:x.equals(x)必须返回是“true”。

    • 传递性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。

    • 一致性:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。

    • 任何情况下,x.equals(null),永远返回是“false”;

      ​ x.equals(和x不同类型的对象)永远返回是“false”。

  • 重写equals()方法的方法

    1
    2
    // 1.自己写 
    // 2.idea自动创建[Alt+Ins] 选中equals和hashCode()
  • 重写举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class User{
String name;
int age;

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

//重写equals()
// 1.自己写
// 2.idea自动创建[Alt+Ins] 选中equals和hashCode()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User user)) return false;
return age == user.age && Objects.equals(name, user.name);
}

}

面试题:==和equals的区别

从我面试的反馈,85%的求职者“理直气壮”的回答错误…

  • == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址

  • equals的话,它是属于java.lang.Object类里面的方法,如果该方法没有被重写过默认也是==;我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals是比较值的错误观点。

  • 具体要看自定义类里有没有重写Object的equals方法来判断。

  • 通常情况下,重写equals方法,会比较类中的相应属性是否都相等。

练习1:

1
2
3
4
5
6
7
8
9
10
11
12
int it = 65;
float fl = 65.0f;
System.out.println("65和65.0f是否相等?" + (it == fl)); // 两个是基本数据类型比较值 输出true 【int会自动类型转换为float】

char ch1 = 'A'; char ch2 = 12;
System.out.println("65和'A'是否相等?" + (it == ch1)); // 两个是基本数据类型比较值 输出true 【char会自动类型转换为int比较】
System.out.println("12和ch2是否相等?" + (12 == ch2)); // 两个是基本数据类型比较值 输出true 【char会自动类型转换为int比较】

String str1 = new String("hello");
String str2 = new String("hello");
System.out.println("str1和str2是否相等?"+ (str1 == str2)); // 两个是引用数据类型比较是不是同一对象 输出false
System.out.println("str1是否equals str2?"+(str1.equals(str2))); // 两个是引用数据类型比较是不是同一对象 又因为是String类型重写了方法比较值 输出true

练习2:

编写Order类,有int型的orderId,String型的orderName,相应的getter()和setter()方法,两个参数的构造器,重写父类的equals()方法:public boolean equals(Object obj),并判断测试类中创建的两个对象是否相等。

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
import java.util.Objects;
public class Order {
//因为要设置get和set方法 所以需要设置为private
private int orderld;
private String orderName;

//get和set方法
public int getOrderld() {
return orderld;
}

public void setOrderld(int orderld) {
this.orderld = orderld;
}

public String getOrderName() {
return orderName;
}

public void setOrderName(String orderName) {
this.orderName = orderName;
}

//两个参数的构造器
public Order(int orderld,String ordername){
this.orderld=orderld;
this.orderName=ordername;
}

//根据idea重写 Alt+ins
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order order)) return false;
return getOrderld() == order.getOrderld() && Objects.equals(getOrderName(), order.getOrderName());
}

}
1
2
3
4
5
6
7
8
9
10
11
public class OrderTest {
public static void main(String[] args) {
Order order1=new Order(1,"宋亚翔");
Order order2=new Order(1,"宋亚翔");
Order order3=new Order(2,"宋亚翔");
Order order4=new Order(1,"李武");
System.out.println(order1.equals(order2)); //必须是两个属性都相同才行
System.out.println(order1.equals(order3));
System.out.println(order1.equals(order4));
}
}

代码结果如下:

image-20231006111943546

练习3:

请根据以下代码自行定义能满足需要的MyDate类,在MyDate类中覆盖equals方法,使其判断当两个MyDate类型对象的年月日都相同时,结果为true,否则为false。 public boolean equals(Object o)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class EqualsTest {
public static void main(String[] args) {
MyDate m1 = new MyDate(14, 3, 1976);
MyDate m2 = new MyDate(14, 3, 1976);
if (m1 == m2) {
System.out.println("m1==m2");
} else {
System.out.println("m1!=m2"); // m1 != m2
}

if (m1.equals(m2)) {
System.out.println("m1 is equal to m2");// m1 is equal to m2
} else {
System.out.println("m1 is not equal to m2");
}
}
}
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
import java.util.Objects;
public class MyDate {
//三个参数设置为private属性 加上三个get和set方法
private int year;
private int month;
private int day;

//get和set方法
public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}

public int getMonth() {
return month;
}

public void setMonth(int month) {
this.month = month;
}

public int getDay() {
return day;
}

public void setDay(int day) {
this.day = day;
}

//构造器 因为Test里面是有参的
public MyDate(int day, int month, int year) {
this.year = year;
this.month = month;
this.day = day;
}

//重写equals方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof MyDate myDate)) return false;
return getYear() == myDate.getYear() && getMonth() == myDate.getMonth() && getDay() == myDate.getDay();
}

}

2、(重点)toString()

方法签名:public String toString()

① 默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式”

② 在进行String与其它类型数据的连接操作时,自动调用toString()方法

1
2
3
Date now=new Date();
System.out.println(“now=”+now); //相当于
System.out.println(“now=”+now.toString());

③ 如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()

因为Java的引用数据类型的变量中存储的实际上时对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM帮你调用了对象的toString()。

④ 可以根据需要在用户自定义类型中重写toString()方法
如String 类重写了toString()方法,返回字符串的值。

1
2
s1="hello";
System.out.println(s1);//相当于System.out.println(s1.toString());

例如自定义的Person类:

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

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

练习:定义两个类,父类GeometricObject代表几何形状,子类Circle代表圆形。

image-20220325002959156 image-20220325002932102
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 GeometricObject {
protected String color;
protected double weight;

//get和set方法
public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}

public double getWeight() {
return weight;
}

public void setWeight(double weight) {
this.weight = weight;
}

//空参构造器
protected GeometricObject(){
this.weight=1.0;
this.color="white"; //初始化对象
}

//有参构造器
protected GeometricObject(String color,double weight){
this.color=color;
this.weight=weight;
}

}
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
import java.util.Objects;
public class Circle extends GeometricObject {
private double radius;

public Circle(){
this.radius=1.0;
super.color="white"; //初始化三个属性
super.weight=1.0;
}

public Circle(double radius){
this.radius=radius; //radius根据参数构造器确定
super.color="white";
super.weight=1.0;
}

public Circle(double radius,String color,double weight){
super(color,weight); //显式调用父类有参构造器
this.radius=radius;
}

//set和get方法
public double getRaidus() {
return radius;
}

public void setRaidus(double raidus) {
this.radius = raidus;
}

//计算圆面积
public double findArea(){
return Math.PI*radius*radius; // π×r²
}

@Override
public String toString() { //重写toString()方法
return "Circle{" +
"radius=" + radius +
", color='" + color + '\'' +
", weight=" + weight +
'}';
}

@Override
public boolean equals(Object o) { //重写equals()方法
if (this == o) return true;
if (!(o instanceof Circle circle)) return false;
return Double.compare(radius, circle.radius) == 0;
}

}

3、clone()

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
//Object类的clone()的使用
public class CloneTest {
public static void main(String[] args) {
Animal a1 = new Animal("花花");
try {
Animal a2 = (Animal) a1.clone();
System.out.println("原始对象:" + a1);
a2.setName("毛毛");
System.out.println("clone之后的对象:" + a2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}

class Animal implements Cloneable{
private String name;

public Animal() {
super();
}

public Animal(String name) {
super();
this.name = name;
}

public String getName() {
return name;
}

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

@Override
public String toString() {
return "Animal [name=" + name + "]";
}

@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}

}

4、finalize()

  • 当对象被回收时,系统自动调用该对象的 finalize() 方法。(不是垃圾回收器调用的,是本类对象调用的)
    • 永远不要主动调用某个对象的finalize方法,应该交给垃圾回收机制调用。
  • 什么时候被回收:当某个对象没有任何引用时,JVM就认为这个对象是垃圾对象,就会在之后不确定的时间使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize()方法。
  • 子类可以重写该方法,目的是在对象被清理之前执行必要的清理操作。比如,在方法内断开相关连接资源。
    • 如果重写该方法,让一个新的引用变量重新引用该对象,则会重新激活对象。
  • 在JDK 9中此方法已经被标记为过时的。
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 FinalizeTest {
public static void main(String[] args) {
Person p = new Person("Peter", 12);
System.out.println(p);
p = null;//此时对象实体就是垃圾对象,等待被回收。但时间不确定。
System.gc();//强制性释放空间
}
}

class Person{
private String name;
private int age;

public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
//子类重写此方法,可在释放对象前进行某些操作
@Override
protected void finalize() throws Throwable {
System.out.println("对象被释放--->" + this);
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}

}

5、getClass()

public final Class<?> getClass():获取对象的运行时类型

因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法

1
2
3
4
public static void main(String[] args) {
Object obj = new Person();
System.out.println(obj.getClass());//运行时类型
}

结果:

1
class com.atguigu.java.Person

6、hashCode()

public int hashCode():返回每个对象的hash值。(后续在集合框架章节重点讲解)

1
2
3
4
public static void main(String[] args) {
System.out.println("AA".hashCode());//2080
System.out.println("BB".hashCode());//2112
}

8.3 native关键字的理解

使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++等实现的,并且被编译成了DLL,由Java去调用。

  • 本地方法是有方法体的,用c语言编写。由于本地方法的方法体源码没有对我们开源,所以我们看不到方法体

  • 在Java中定义一个native方法时,并不提供实现体。

1. 为什么要用native方法

Java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,例如:Java需要与一些底层操作系统或某些硬件交换信息时的情况。native方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。

2. native声明的方法,对于调用者,可以当做和其他Java方法一样使用

native method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。JVM将控制调用本地方法的所有细节。

第08章_面向对象编程(高级)


本章专题与脉络

第2阶段:Java面向对象编程-第08章

1. 关键字:static

回顾类中的实例变量(即非static的成员变量)

1
2
3
4
5
6
7
8
9
10
11
12
class Circle{
//私有属性radius
private double radius;
//有参构造器
public Circle(double radius){
this.radius=radius;
}
//findArea()方法
public double findArea(){
return Math.PI*radius*radius;
}
}

创建两个Circle对象:

1
2
Circle c1=new Circle(2.0);	//c1.radius=2.0
Circle c2=new Circle(3.0); //c2.radius=3.0

Circle类中的变量radius是一个实例变量(instance variable),它属于类的每一个对象,c1中的radius变化不会影响c2的radius。

–> 如果想让一个成员变量被类的所有实例所共享,就用static修饰即可,称为类变量/类属性

1.1 类属性、类方法的设计思想

当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过new关键字才会产出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。我们有时候希望无论是否产生了对象或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份。例如,所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

image-20220325213629311

此外,在类中声明的实例方法,在类的外面必须要先创建对象,才能调用。但是有些方法的调用者和当前类的对象无关,这样的方法通常被声明为类方法,由于不需要创建对象就可以调用类方法,从而简化了方法的调用。

这里的类变量、类方法,只需要使用static修饰即可。所以也称为静态变量、静态方法。

1.2 static关键字

  • 使用范围:

    • 在Java类中,可用static修饰属性、方法、代码块、内部类
  • 被修饰后的成员具备以下特点:

    • 随着类的加载而加载
    • 优先于对象存在
    • 修饰的成员,被所有对象所共享
    • 访问权限允许时,可不创建对象,直接被类调用

1.3 静态变量

1.3.1 语法格式

使用static修饰的成员变量就是静态变量(或类变量、类属性)

1
2
3
[修饰符] class{
[其他修饰符] static 数据类型 变量名;
}

1.3.2 静态变量的特点

  • 静态变量的默认值规则和实例变量一样。

  • 静态变量值是所有对象共享。

  • 静态变量在本类中,可以在任意方法、代码块、构造器中直接使用。

  • 如果权限修饰符允许,在其他类中可以通过“类名.静态变量”直接访问,也可以通过“对象.静态变量”的方式访问(但是更推荐使用类名.静态变量的方式)。

  • 静态变量的get/set方法也静态的,当局部变量与静态变量重名时,使用“类名.静态变量”进行区分。

1.3.3 举例

举例1:

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
class Chinese{
//实例变量
String name;
int age;
//类变量
static String nation;//国籍

public Chinese() {
}

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

@Override
public String toString() {
return "Chinese{" +
"name='" + name + '\'' +
", age=" + age +
", nation='" + nation + '\'' +
'}';
}
}
public class StaticTest {
public static void main(String[] args) {
Chinese c1 = new Chinese("康师傅",36);
c1.nation = "中华人民共和国";

Chinese c2 = new Chinese("老干妈",66);

System.out.println(c1);
System.out.println(c2);

System.out.println(Chinese.nation);
}
}

对应的内存结构:(以经典的JDK6内存解析为例,此时静态变量存储在方法区)

image-20220514183814514

举例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
45
46
47
package com.atguigu.keyword;

public class Employee {
private static int total;//这里私有化,在类的外面必须使用get/set方法的方式来访问静态变量
static String company; //这里缺省权限修饰符,是为了方便类外以“类名.静态变量”的方式访问
private int id;
private String name;

public Employee() {
total++;
id = total;//这里使用total静态变量的值为id属性赋值
}

public Employee(String name) {
this();
this.name = name;
}

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

public int getId() {
return id;
}

public String getName() {
return name;
}

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

public static int getTotal() {
return total;
}

public static void setTotal(int total) {
Employee.total = total;
}

@Override
public String toString() {
return "Employee{company = " + company + ",id = " + id + " ,name=" + name +"}";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.atguigu.keyword;

public class TestStaticVariable {
public static void main(String[] args) {
//静态变量total的默认值是0
System.out.println("Employee.total = " + Employee.getTotal());

Employee e1 = new Employee("张三");
Employee e2 = new Employee("李四");
System.out.println(e1);//静态变量company的默认值是null
System.out.println(e2);//静态变量company的默认值是null
System.out.println("Employee.total = " + Employee.getTotal());//静态变量total值是2

Employee.company = "尚硅谷";
System.out.println(e1);//静态变量company的值是尚硅谷
System.out.println(e2);//静态变量company的值是尚硅谷

//只要权限修饰符允许,虽然不推荐,但是也可以通过“对象.静态变量”的形式来访问
e1.company = "超级尚硅谷";

System.out.println(e1);//静态变量company的值是超级尚硅谷
System.out.println(e2);//静态变量company的值是超级尚硅谷
}
}

1.3.4 内存解析

image-20220104100145059

1.4 静态方法

1.4.1 语法格式

用static修饰的成员方法就是静态方法。

1
2
3
4
5
[修饰符] class{
[其他修饰符] static 返回值类型 方法名(形参列表){
方法体
}
}

1.4.2 静态方法的特点

  • 静态方法在本类的任意方法、代码块、构造器中都可以直接被调用。
  • 只要权限修饰符允许,静态方法在其他类中可以通过“类名.静态方法“的方式调用。也可以通过”对象.静态方法“的方式调用(但是更推荐使用类名.静态方法的方式)。
  • 在static方法内部只能访问类的static修饰的属性或方法,不能访问类的非static的结构。
  • 静态方法可以被子类继承,但不能被子类重写。
  • 静态方法的调用都只看编译时类型。
  • 因为不需要实例就可以访问static方法,因此static方法内部不能有this,也不能有super。如果有重名问题,使用“类名.”进行区别。

1.4.3 举例

1
2
3
4
5
6
7
8
9
10
11
package com.atguigu.keyword;

public class Father {
public static void method(){
System.out.println("Father.method");
}

public static void fun(){
System.out.println("Father.fun");
}
}
1
2
3
4
5
6
7
8
package com.atguigu.keyword;

public class Son extends Father{
// @Override //尝试重写静态方法,加上@Override编译报错,去掉Override不报错,但是也不是重写
public static void fun(){
System.out.println("Son.fun");
}
}
1
2
3
4
5
6
7
8
9
10
11
package com.atguigu.keyword;

public class TestStaticMethod {
public static void main(String[] args) {
Father.method();
Son.method();//继承静态方法

Father f = new Son();
f.method();//执行Father类中的method
}
}

1.5 练习

笔试题:如下程序执行会不会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StaticTest {
//代码入口
public static void main(String[] args) { //静态的方法只能调用静态的方法和属性
Demo test = null; //创建对象
test.hello(); //对象.静态方法
Demo.hello(); //类.静态方法
}
}

class Demo{
//静态方法
public static void hello(){
System.out.println("hello!");
}
}
最终输出:
hello!
hello!

练习:

编写一个类实现银行账户的概念,包含的属性有“帐号”、“密码”、“存款余额”、“利率”、“最小余额”,定义封装这些属性的方法。账号要自动生成。

编写主类,使用银行账户类,输入、输出3个储户的上述信息。

考虑:哪些属性可以设计成static属性。

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
package staticTest;

public class Account {

private int id; //账号

private String password;//密码

private double balance; //余额

private static double interestRate;//利率

private static double minBalance = 1.0;//最小余额

private static int init = 1001;//用于自动生成id的基数

//无参构造器
public Account() {
this.id = init;
init++;
password = "000000"; //初始化密码
}

//有参构造器
public Account(String password, double balance) {
this.password = password;
this.balance = balance;
this.id = init;
init++;
}

//set和get方法
public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public double getBalance() {
return balance;
}

public void setBalance(double balance) {
this.balance = balance;
}

public static double getInterestRate() {
return interestRate;
}

public static void setInterestRate(double interestRate) {
Account.interestRate = interestRate;
}

public static double getMinBalance() {
return minBalance;
}

public static void setMinBalance(double minBalance) {
Account.minBalance = minBalance;
}

//重写toString方法
@Override
public String toString() {
return "Account{" +
"id=" + id +
", password='" + password + '\'' +
", balance=" + balance +
'}';
}
}

2. 单例(Singleton)设计模式

2.1 设计模式概述

设计模式是在大量的实践中总结理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式免去我们自己再思考和摸索。就像是经典的棋谱,不同的棋局,我们用不同的棋谱。”套路”

经典的设计模式共有23种。每个设计模式均是特定环境下特定问题的处理方法。

image-20220520174508815

简单工厂模式并不是23中经典模式的一种,是其中工厂方法模式的简化版

对软件设计模式的研究造就了一本可能是面向对象设计方面最有影响的书籍:《设计模式》:《Design Patterns: Elements of Reusable Object-Oriented Software》(即后述《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为”四人组(Gang of Four)”,而这本书也就被称为”四人组(或 GoF)”书。

2.2 何为单例模式

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

2.3 实现思路

如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为private,[就不能用new操作符在类的外部产生类的对象],但在类内部仍可以产生该类的对象。因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的

2.4 单例模式的两种实现方式

2.4.1 饿汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton {
// 1.私有化构造器
private Singleton() {
}

// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single = new Singleton();

// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
return single;
}
}

2.4.2 懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
// 1.私有化构造器
private Singleton() {
}
// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single;
// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
if(single == null) {
single = new Singleton();
}
return single;
}
}

2.4.3 饿汉式 vs 懒汉式

饿汉式:

  • 特点:立即加载,即在使用类的时候已经将对象创建完毕。
  • 优点:实现起来简单;没有多线程安全问题。
  • 缺点:当类被加载的时候,会初始化static的实例,静态变量被创建并分配内存空间,从这以后,这个static的实例便一直占着这块内存,直到类被卸载时,静态变量被摧毁,并释放所占有的内存。因此在某些特定条件下会耗费内存

懒汉式:

  • 特点:延迟加载,即在调用静态方法时实例才被创建。
  • 优点:实现起来比较简单;当类被加载的时候,static的实例未被创建并分配内存空间,当静态方法第一次被调用时,初始化实例变量,并分配内存,因此在某些特定条件下会节约内存
  • 缺点:在多线程环境中,这种实现方法是完全错误的,线程不安全,根本不能保证单例的唯一性。
    • 说明:在多线程章节,会将懒汉式改造成线程安全的模式。

2.5 单例模式的优点及应用场景

由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

举例:

image-20220325222541203

应用场景

  • Windows的Task Manager (任务管理器)就是很典型的单例模式

  • Windows的Recycle Bin (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

  • Application 也是单例的典型应用

  • 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只

    能有一个实例去操作,否则内容不好追加。

  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。

3. 理解main方法的语法

由于JVM需要调用类的main()方法,所以该方法的访问权限必须是public,又因为JVM在执行main()方法时不必创建对象,所以该方法必须是static的,该方法接收一个String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数。

又因为main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,这种情况,我们在之前的例子中多次碰到。

命令行参数用法举例

1
2
3
4
5
6
7
public class CommandPara {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i + "] = " + args[i]);
}
}
}
1
2
//运行程序CommandPara.java
java CommandPara "Tom" "Jerry" "Shkstart"
1
2
3
4
//输出结果
args[0] = Tom
args[1] = Jerry
args[2] = Shkstart

image-20220325223215924

IDEA工具:

(1)配置运行参数

image-20211228101828718 image-20211228102022216

(2)运行程序

image-20211228102059327

笔试题:

1
2
3
4
5
6
7
8
9
//此处,Something类的文件名叫OtherThing.java
class Something {
public static void main(String[] something_to_do) {
System.out.println("Do something ...");
}
}


//上述程序是否可以正常编译、运行? 如果改一下类名的话,其他事可以啊,没什么影响。

4. 类的成员之四:代码块

如果成员变量想要初始化的值不是一个硬编码的常量值,而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值,该怎么办呢?此时,可以考虑代码块/初始化块

  • 代码块(或初始化块)的作用

  • 对Java的类/对象进行初始化

  • 代码块(或初始化块)的分类

    • 静态代码块(static block): 用static修饰[只能被static修饰]

    • 非静态代码块:没有用static修饰

4.1 静态代码块

如果想要为静态变量初始化,可以直接在静态变量的声明后面直接赋值,也可以使用静态代码块。

4.1.1 语法格式

在代码块的前面加static,就是静态代码块。

1
2
3
4
5
【修饰符】 class{
static{
静态代码块
}
}

4.1.2 静态代码块的特点

  1. 可以有输出语句。

    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
class Chinese {

private static String country; //私有静态属性country
private String name; //私有非静态属性name

//非静态代码块
{
System.out.println("非静态代码块,country = " + country);
}
//静态代码块
static {
country = "中国";
System.out.println("静态代码块");
}
//构造器
public Chinese(String name) {
this.name = name;
}
}

public class TestStaticBlock {
public static void main(String[] args) {
//创建TestStaticBlock类然后创建出country=null 执行静态代码块 country=中国 输出静态代码块
Chinese c1 = new Chinese("张三"); //创建对象 执行非静态代码块,country =中国
Chinese c2 = new Chinese("李四"); //创建对象 执行非静态代码块,country =中国
}
}
最终输出:
静态代码块
非静态代码块,country = 中国
非静态代码块,country = 中国

4.2 非静态代码块

4.2.1 语法格式

1
2
3
4
5
6
7
8
9
10
11
【修饰符】 class{
{
非静态代码块
}
【修饰符】 构造器名(){
// 实例初始化代码
}
【修饰符】 构造器名(参数列表){
// 实例初始化代码
}
}

4.2.2 非静态代码块的作用

和构造器一样,也是用于实例变量的初始化等操作。

4.2.3 非静态代码块的意义

如果多个重载的构造器公共代码[先于构造器其他代码执行的],那么可以将这部分代码 —-> 非静态代码块,减少冗余代码。

4.2.4 非静态代码块的执行特点

  1. 可以有输出语句。
    1. 可以对类的属性、类的声明进行初始化操作。
    2. 可以调用 非静态的结构 + 静态的变量或方法。
    3. 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
    4. 每次创建对象的时候,都会执行一次。
    5. 执行顺序:静态代码块 > 非静态代码块 > 构造器

4.3 举例

举例1:

(1)声明User类,

  • 包含属性:username(String类型),password(String类型),registrationTime(long类型),私有化

  • 包含get/set方法,其中registrationTime没有set方法

  • 包含无参构造,

    • 输出“新用户注册”,
    • registrationTime赋值为当前系统时间,
    • username就默认为当前系统时间值,
    • password默认为“123456”
  • 包含有参构造(String username, String password),

    • 输出“新用户注册”,
    • registrationTime赋值为当前系统时间,
    • username和password由参数赋值
  • 包含public String getInfo()方法,返回:“用户名:xx,密码:xx,注册时间:xx”

(2)编写测试类,测试类main方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserTest {
public static void main(String[] args) {
User u1 = new User();
System.out.println(u1.getInfo());
User u2 = new User("song","8888");
System.out.println(u2.getInfo());
}
}
最终输出:
新用户注册
用户名:larkkkkkkk,密码:123456,注册时间:1696858612390
新用户注册
用户名:song,密码:8888,注册时间:1696858612400

如果不用非静态代码块,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
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
public class User {
private String username;
private String password;
private long registrationTime;
//属性的set和get方法
public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

//没有set方法
public long getRegistrationTime() {
return registrationTime;
}

//使用非静态代码块
{
System.out.println("新用户注册");
this.registrationTime = System.currentTimeMillis();
}

//无参构造器
public User(){
//System.out.println("新用户注册");
//this.registrationTime=System.currentTimeMillis();
username="larkkkkkkk";
password="123456";
}

//有参构造器
public User(String username,String password){
//System.out.println("新用户注册");
//this.registrationTime=System.currentTimeMillis();
this.username=username;
this.password=password;
}

public String getInfo(){
return "用户名:"+getUsername()+",密码:"+getPassword()+",注册时间:"+getRegistrationTime();
}

}

如果提取构造器公共代码到非静态代码块,User类是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//非静态代码块
{
System.out.println("新用户注册");
registrationTime = System.currentTimeMillis();
}

public User() {
username = registrationTime+"";
password = "123456";
}

public User(String username, String password) {
this.username = username;
this.password = password;
}

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static DataSource dataSource = null;

static{
InputStream is = null;
try {
is = DBCPTest.class.getClassLoader().getResourceAsStream("dbcp.properties");
Properties pros = new Properties();
pros.load(is);
//调用BasicDataSourceFactory的静态方法,获取数据源。
dataSource = BasicDataSourceFactory.createDataSource(pros);
} catch (Exception e) {
e.printStackTrace();
}finally{
if(is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

4.4 小结:实例变量赋值顺序

image-20220325230208941

4.5 练习

练习1:分析加载顺序 — 由父后子,静态先行

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
class Root{
//静态代码块
static{
System.out.println("Root的静态初始化块");
}
//非静态代码块
{
System.out.println("Root的普通初始化块");
}
//无参构造器
public Root(){
System.out.println("Root的无参数的构造器");
}
}
class Mid extends Root{
//静态代码块
static{
System.out.println("Mid的静态初始化块");
}
//非静态代码块
{
System.out.println("Mid的普通初始化块");
}
//无参构造器
public Mid(){
//没写this(参数列表)和super(参数列表) --默认读取父类的无参构造器
System.out.println("Mid的无参数的构造器");
}
//有参构造器
public Mid(String msg){
this();//调用无参构造器
System.out.println("Mid的带参数构造器,其参数值:"+ msg);
}
}
class Leaf extends Mid{
//静态代码块
static{
System.out.println("Leaf的静态初始化块");
}
//非静态代码块
{
System.out.println("Leaf的普通初始化块");
}
//无参构造器
public Leaf(){
super("尚硅谷"); //通过super调用父类中有一个字符串参数的构造器
System.out.println("Leaf的构造器");
}
}

public class LeafTest{
public static void main(String[] args){
//创建一个子类对象
new Leaf();
}
}

最终输出:
//顺序: 静态代码块 > 非静态代码块 > 构造器
//先加载父类
Root的静态初始化块
Mid的静态初始化块
Leaf的静态初始化块
//后面的就根据类的调用顺序执行
//加载root类
Root的普通初始化块
Root的无参数的构造器
//加载Mid类
Mid的普通初始化块
Mid的无参数的构造器
Mid的带参数构造器,其参数值:尚硅谷
//最终加载最小子类Leaf
Leaf的普通初始化块
Leaf的构造器

练习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
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
class Father {
//静态代码块
static {
System.out.println("11111111111");
}
//非静态代码块
{
System.out.println("22222222222");
}
//无参构造器
public Father() {
System.out.println("33333333333");
}

}

public class Son extends Father {
//静态代码块
static {
System.out.println("44444444444");
}
//非静态代码块
{
System.out.println("55555555555");
}
//无参构造器
public Son() {
System.out.println("66666666666");
}

//执行顺序: 静态代码块 > 非静态代码块 > 构造器
public static void main(String[] args) {
System.out.println("77777777777");
System.out.println("************************");

//创建子类的匿名对象[只创建一次]
new Son();
System.out.println("************************");

//创建子类的匿名对象[只创建一次]
new Son();
System.out.println("************************");

//创建父类的匿名对象[只创建一次]
new Father();
}

}

最终输出:
//先初始化父类和子类的静态代码块(只执行一次)
11111111111
44444444444
77777777777
************************
//先执行父类的非静态代码块和构造器
22222222222
33333333333
//后执行子类的非静态代码块和构造器
55555555555
66666666666
************************
//先执行父类的非静态代码块和构造器
22222222222
33333333333
//后执行子类的非静态代码块和构造器
55555555555
66666666666
************************
22222222222
33333333333

练习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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class Fu{
//私有静态属性
private static int i = getNum("(1)i");
//私有非静态属性
private int j = getNum("(2)j");
//静态代码块
static{
print("(3)父类静态代码块");
}
//非静态代码块
{
print("(4)父类非静态代码块,又称为构造代码块");
}
//父类无参构造器
Fu(){
print("(5)父类构造器");
}
//print方法
public static void print(String str){
System.out.println(str + "->" + i);
}
//获得数字
public static int getNum(String str){
print(str);
return ++i;
}
}

class Zi extends Fu{
//私有静态属性
private static int k = getNum("(6)k");
//私有非静态属性
private int h = getNum("(7)h");
//静态代码块
static{
print("(8)子类静态代码块");
}
//非静态代码块
{
print("(9)子类非静态代码块,又称为构造代码块");
}
//子类无参构造器
Zi(){
//默认调用父类无参构造器 super()
print("(10)子类构造器");
}
//print方法
public static void print(String str){
System.out.println(str + "->" + k);
}
//获得数字
public static int getNum(String str){
print(str);
return ++k;
}
}

public class Test04 {
public static void main(String[] args) {
//创建子类对象
Zi zi = new Zi();
}
}

最终输出:
//先执行父类和子类的创建 静态的东西
1)i->0
3)父类静态代码块->1
6)k->0
8)子类静态代码块->1
//再执行创建对象时从父类->子类实例化
//父类的实例化属性
//父类的非静态代码块
//父类的构造器
2)j->1
4)父类非静态代码块,又称为构造代码块->2
5)父类构造器->2
7)h->1
9)子类非静态代码块,又称为构造代码块->2
10)子类构造器->2

5. final关键字

5.1 final的意义

final:最终的,不可更改的

5.2 final的使用

5.2.1 final修饰类

表示这个类不能被继承,没有子类。提高安全性,提高程序的可读性。

例如:String类、System类、StringBuffer类

1
2
3
4
5
6
final class Eunuch{//太监类

}
class Son extends Eunuch{//错误

}

5.2.2 final修饰方法

表示这个方法不能被子类重写。

例如:Object类中的getClass()

1
2
3
4
5
6
7
8
9
10
class Father{
public final void method(){
System.out.println("father");
}
}
class Son extends Father{
public void method(){//错误
System.out.println("son");
}
}

5.2.3 final修饰变量

final修饰某个变量(成员变量或局部变量),一旦赋值,它的值就不能被修改,即常量,常量名建议使用大写字母。

例如:final double MY_PI = 3.14;

如果某个成员变量用final修饰后,没有set方法 + 必须初始化(可以显式赋值、或在初始化块赋值、实例变量还可以在构造器中赋值)

  • 修饰成员变量
1
2
3
4
5
6
7
8
9
10
11
12
public final class Test {
public static int totalNumber = 5;
public final int ID;

public Test() {
ID = ++totalNumber; // 可在构造器中给final修饰的“变量”赋值
}
public static void main(String[] args) {
Test t = new Test();
System.out.println(t.ID);
}
}
  • 修饰局部变量:
1
2
3
4
5
6
7
8
public class TestFinal {
public static void main(String[] args){
final int MIN_SCORE ;
MIN_SCORE = 0;
final int MAX_SCORE = 100;
MAX_SCORE = 200; //非法
}
}
  • 错误演示:
1
2
3
4
5
6
7
8
class A {
private final String INFO = "atguigu"; //声明常量

public void print() {
//The final field A.INFO cannot be assigned
//INFO = "尚硅谷"; //不能被赋值 修改
}
}

5.3 笔试题

题1:排错

1
2
3
4
5
6
public class Something {
public int addOne(final int x) {
return ++x; //不能修改
// return x + 1; //可以 因为x没被修改
}
}

题2:排错

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Something {
public static void main(String[] args) {
Other o = new Other();
new Something().addOne(o);
}
public void addOne(final Other o) {
// o = new Other(); //不能重新new一个 只能用o调用方法/调用属性
o.i++;
}
}
class Other {
public int i;
}

6. 抽象类与抽象方法(或abstract关键字)

6.1 由来

举例1:

随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。

image-20220325231608838

举例2:

我们声明一些几何图形类:圆、矩形、三角形类等,发现这些类都有共同特征:求面积、求周长。那么这些共同特征应该抽取到一个共同父类:几何图形类中。但是这些方法在父类中又无法给出具体的实现,而是应该交给子类各自具体实现。那么父类在声明这些方法时,就只有方法签名,没有方法体,我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类必须是抽象类

6.2 语法格式

  • 抽象类:被abstract修饰的类。
  • 抽象方法:被abstract修饰没有方法体的方法。

抽象类的语法格式

1
2
3
4
5
6
[权限修饰符] abstract class 类名{

}
[权限修饰符] abstract class 类名 extends 父类{

}

抽象方法的语法格式

1
[其他修饰符] abstract 返回值类型 方法名([形参列表]);

注意:抽象方法没有方法体

image-20220517204707255

代码举例:

1
2
3
public abstract class Animal {
public abstract void eat();
}
1
2
3
4
5
public class Cat extends Animal {
public void eat (){
System.out.println("小猫吃鱼和猫粮");
}
}
1
2
3
4
5
6
7
8
9
public class CatTest {
public static void main(String[] args) {
// 创建子类对象
Cat c = new Cat();

// 调用eat方法
c.eat();
}
}

此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法

6.3 使用说明

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

    理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

    抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。

  2. 抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。

    理解:子类的构造方法中,有默认的super()或手动的super(实参列表),需要访问父类构造方法。

  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。

    理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

6.4 注意事项

  • 不能用abstract修饰变量、代码块、构造器;

  • 不能用abstract修饰私有方法、静态方法、final的方法、final的类。

  • 私有方法不能重写 –私有方法不能被重写但是abstract方法必须让子类重写
    避免静态方法使用类进行调用 –方法只能被①类[一定是static方法]和②对象调用[抽象类不能实例化对象] 我现在abstract不能调用类 所以方法也不能是static方法
    final的方法不能被重写 —-抽象类必须有子类重写所有抽象方法
    final修饰的类不能有子类 –抽象类必须有子类重写所有抽象方法

6.5 应用举例1

image-20220325232847872

在航运公司系统中,Vehicle类需要定义两个方法分别计算运输工具的燃料效率行驶距离

问题:卡车(Truck)和驳船(RiverBarge)的燃料效率和行驶距离的计算方法完全不同。Vehicle类不能提供计算方法,但子类可以。

解决方案:Java允许类设计者指定:超类声明一个方法但不提供实现,该方法的实现由子类提供。这样的方法称为抽象方法。有一个或更多抽象方法的类称为抽象类。

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
//Vehicle是一个抽象类,有两个抽象方法。
public abstract class Vehicle{
public abstract double calcFuelEfficiency(); //计算燃料效率的抽象方法
public abstract double calcTripDistance(); //计算行驶距离的抽象方法
}
//卡车
public class Truck extends Vehicle{
@Override
public double calcFuelEfficiency( ){
//写出计算卡车的燃料效率的具体方法
}
@Override
public double calcTripDistance( ){
//写出计算卡车行驶距离的具体方法
}
}
//驳船
public class RiverBarge extends Vehicle{
@Override
public double calcFuelEfficiency( ){
//写出计算驳船的燃料效率的具体方法
}
@Override
public double calcTripDistance( ){
//写出计算驳船行驶距离的具体方法
}
}

6.6 应用举例2:模板方法设计模式(TemplateMethod)

抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。

解决的问题

  • 当功能内部一部分实现是确定的,另一部分实现是不确定的【这部分暴露出去给子类实现】。

  • 换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式。

类比举例:英语六级模板

image-20220503145003315

制作月饼的模板:

image-20220517205013997

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//父类要是一个抽象类
abstract class Template {
public final void getTime() {
long start = System.currentTimeMillis();
code();
long end = System.currentTimeMillis();
System.out.println("执行时间是:" + (end - start));
}
//抽象方法
public abstract void code();
}
//子类
class SubTemplate extends Template {
//子类必须重写父类所有抽象方法
@Override
public void code() {
for (int i = 0; i < 10000; i++) {
System.out.println(i);
}
}
}

举例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
45
46
47
48
49
50
51
52
53
54
55
56
//抽象类的应用:模板方法的设计模式
public class TemplateMethodTest {
public static void main(String[] args) {
BankTemplateMethod btm = new DrawMoney(); //子类-取款类
btm.process();

BankTemplateMethod btm2 = new ManageMoney(); //子类-理财类
btm2.process();
}
}
abstract class BankTemplateMethod {
// 具体方法
public void takeNumber() {
System.out.println("取号排队");
}

public void evaluate() {
System.out.println("反馈评分");
}

// 抽象方法
public abstract void transact(); // 办理具体的业务 //钩子方法

// 模板方法,把基本操作组合到一起,子类一般不能重写
public final void process() {
//取号
this.takeNumber();
//办理业务
this.transact();// 具体执行时,挂哪个子类,就执行哪个子类的实现代码
//评分
this.evaluate();
}

}

class DrawMoney extends BankTemplateMethod {
@Override
public void transact() {
System.out.println("我要取款!!!");
}
}

class ManageMoney extends BankTemplateMethod {
@Override
public void transact() {
System.out.println("我要理财!我这里有2000万美元!!");
}
}

最终输出:
取号排队
我要取款!!!
反馈评分
取号排队
我要理财!我这里有2000万美元!!
反馈评分

模板方法设计模式是编程中经常用得到的模式。各个框架、类库中都有他的影子,比如常见的有:

  • 数据库访问的封装

  • Junit单元测试

  • JavaWeb的Servlet中关于doGet/doPost方法调用

  • Hibernate中模板程序

  • Spring中JDBCTemlate、HibernateTemplate等

6.7 思考与练习

思考:

问题1:为什么抽象类不可以使用final关键字声明? — 因为final类是不可以被继承的,abstract类必须让子类重写抽象方法

问题2:一个抽象类中可以定义构造器吗? — 可以,要留给子类到时候继承父类成员

问题3:抽象类就是比普通类多定义了抽象方法,除了不能直接进行类的实例化操作之外,并没有任何的不同? — 确实

练习1:

编写一个Employee类,声明为抽象类,包含如下三个属性:name,id,salary。提供必要的构造器和抽象方法:work()。

对于Manager类来说,他既是员工,还具有奖金(bonus)的属性。

请使用继承的思想,设计CommonEmployee类和Manager类,要求类中提供必要的方法进行属性访问。

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 abstract class Employee {
private String name;
private int id;
private double salary;

public Employee(){

}

public Employee(String name, int id, double salary) {
this.name = name;
this.id = id;
this.salary = salary;
}
//抽象方法
public abstract void work();

}

//子类
public class Manager extends Employee{ //继承Employee类
private double bonus;
//必须重写
@Override
public void work() {
System.out.println("我是管理人员,我需要努力工作");
}
}

练习2:软件外包公司外派管理

有一家软件外包公司,可以外派开发人员,该公司有两个角色:普通开发人员Developer和项目经理Manager。他们的关系如下图:

image-20220504164925878

普通开发人员的工作内容是“开发项目”,项目经理的工作内容是“项目管理”。对外的报价是普通开发人员每天500,元,超过60天每天400元。项目经理每天800元,超过60天每天700元。

有一家银行需要1名项目经理、2名开发人员,现场开发90天,计算银行需要付给软件公司的总金额。

提示:创建数组 Employee[] emps = new Employee[3]。其中存储驻场的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
//抽象类
public abstract class Employee {
private String name;
private int age;

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

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
//抽象方法
public abstract void work();
public abstract double calMoney(int days);
}
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 Developer extends Employee{
private int workExperiences=500;

public Developer(String name,int age){
super(name,age); //调用父类的有参构造器
}

@Override
public void work() {
System.out.println("开发项目");
}

@Override
public double calMoney(int days) {
int sum=0;
//看看超没超过60天
int quyu=days%60;
if(quyu>0){ //如果超过60天
sum+=(this.workExperiences*60+400*(quyu));
return sum;
}
return this.workExperiences*days; //默认是没超过60天
}
}
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 Manager extends Employee{
private int manageExperiences=800;

public Manager(String name,int age) {
super(name, age); //调用父类的有参构造器
}

@Override
public void work() {
System.out.println("项目管理");
}

@Override
public double calMoney(int days) {
int sum=0;
//看看超没超过60天
int quyu=days%60;
if(quyu>0){
sum+=(this.manageExperiences*60+700*(quyu));
return sum;
}
return this.manageExperiences*days;
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Test{
public static void main(String[] args) {
Employee[] emps=new Employee[3];
//一名项目经理
emps[0]=new Manager("项目经理",26);
//两名开发人员
emps[1]=new Developer("开发人员1",30);
emps[2]=new Developer("开发人员2",30);
System.out.println(emps[0].calMoney(90)+emps[1].calMoney(90)+emps[2].calMoney(90));
}
}

练习3:

创建父类Shape,包含绘制形状的抽象方法draw()。

创建Shape的子类Circle和Rectangle,重写draw()方法,绘制圆形和矩形。

绘制多个圆形和矩形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//抽象类
public abstract class Shape {
//抽象方法
public abstract void draw();
}
//子类
public class Circle extends Shape{
@Override
public void draw() {
System.out.println("绘制圆形");
}
}
//子类
public class Rectangle extends Shape{
@Override
public void draw() {
System.out.println("绘制矩形");
}
}

练习4:

1、声明抽象父类Person,包含抽象方法public abstract void eat();

1
2
3
4
public abstract class Person{
//抽象方法
public abstract void eat();
}

2、声明子类中国人Chinese,重写抽象方法,打印用筷子吃饭

1
2
3
4
5
6
public class Chinese extends Person{
@Override
public void eat() {
System.out.println("用筷子吃饭");
}
}

3、声明子类美国人American,重写抽象方法,打印用刀叉吃饭

1
2
3
4
5
6
public class American extends Person{
@Override
public void eat() {
System.out.println("用刀叉吃饭");
}
}

4、声明子类印度人Indian,重写抽象方法,打印用手抓饭

1
2
3
4
5
6
public class Indian extends Person{
@Override
public void eat() {
System.out.println("用手抓饭");
}
}

5、声明测试类PersonTest,创建Person数组,存储各国人对象,并遍历数组,调用eat()方法

1
2
3
4
5
6
7
8
9
10
11
public class PersonTest {
public static void main(String[] args) {
Person[] p1=new Person[3];
p1[0]=new Indian();
p1[0].eat();
p1[1]=new Chinese();
p1[1].eat();
p1[2]=new American();
p1[2].eat();
}
}

练习5:工资系统设计

编写工资系统,实现不同类型员工(多态)的按月发放工资。如果当月出现某个Employee对象的生日,则将该雇员的工资增加100元。

实验说明:

(1)定义一个Employee类,该类包含:

private成员变量name,number,birthday,其中birthday 为MyDate类的对象;

abstract方法earnings();

toString()方法输出对象的name,number和birthday。

(2)MyDate类包含:

private成员变量year,month,day ;

toDateString()方法返回日期对应的字符串:xxxx年xx月xx日

(3)定义SalariedEmployee类继承Employee类,实现按月计算工资的员工处理。该类包括:private成员变量monthlySalary;

实现父类的抽象方法earnings(),该方法返回monthlySalary值;toString()方法输出员工类型信息及员工的name,number,birthday。

(4)参照SalariedEmployee类定义HourlyEmployee类,实现按小时计算工资的员工处理。该类包括:

private成员变量wage和hour;

实现父类的抽象方法earnings(),该方法返回wage*hour值;

toString()方法输出员工类型信息及员工的name,number,birthday。

(5)定义PayrollSystem类,创建Employee变量数组并初始化,该数组存放各类雇员对象的引用。利用循环结构遍历数组元素,输出各个对象的类型,name,number,birthday,以及该对象生日。当键盘输入本月月份值时,如果本月是某个Employee对象的生日,还要输出增加工资信息。

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
//提示:
//定义People类型的数组People c1[]=new People[10];
//数组元素赋值
c1[0]=new People("John","0001",20);
c1[1]=new People("Bob","0002",19);
//若People有两个子类Student和Officer,则数组元素赋值时,可以使父类类型的数组元素指向子类。
c1[0]=new Student("John","0001",20,85.0);
c1[1]=new Officer("Bob","0002",19,90.5);

public abstract class Employee {
private String name;
private int number;
private MyDate birthday;

public Employee() {
}

public Employee(String name, int number, MyDate birthday) {
this.name = name;
this.number = number;
this.birthday = birthday;
}

public String getName() {
return name;
}

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

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

public MyDate getBirthday() {
return birthday;
}

public void setBirthday(MyDate birthday) {
this.birthday = birthday;
}

public abstract double earnings();

public String toString(){
return "name = " + name + ",number = " + number +", birthday = " + birthday.toDateString();
}
}
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 SalariedEmployee extends Employee{
private double monthlySalary;//月工资

public SalariedEmployee() {
}

@Override
public double earnings() {
return monthlySalary;
}

public SalariedEmployee(String name, int number, MyDate birthday, double monthlySalary) {
super(name, number, birthday);
this.monthlySalary = monthlySalary;
}

public void setMonthlySalary(double monthlySalary) {
this.monthlySalary = monthlySalary;
}

public String toString(){
return "SalariedEmployee[" + super.toString() + "]";
}
}
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
public class HourlyEmployee extends Employee{
private double wage;//单位小时的工资
private int hour;//月工作的小时数

public HourlyEmployee() {
}

public HourlyEmployee(String name, int number, MyDate birthday, double wage, int hour) {
super(name, number, birthday);
this.wage = wage;
this.hour = hour;
}

public double getWage() {
return wage;
}

public void setWage(double wage) {
this.wage = wage;
}

public int getHour() {
return hour;
}

public void setHour(int hour) {
this.hour = hour;
}

@Override
public double earnings() {
return wage * hour;
}

public String toString(){
return "HourlyEmployee[" + super.toString() + "]";
}
}
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 MyDate {
private int year;
private int month;
private int day;
public MyDate() {
}

public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}

public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}

public int getMonth() {
return month;
}

public void setMonth(int month) {
this.month = month;
}

public int getDay() {
return day;
}

public void setDay(int day) {
this.day = day;
}

public String toDateString(){
return year + "年" + month + "月" + day + "日";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PayrollSystem {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
Employee[] emps = new Employee[2];

emps[0] = new SalariedEmployee("张小亮",1001,new MyDate(1992,12,21),18000);

emps[1] = new HourlyEmployee("侯少鹏",1002,new MyDate(1997,11,12),240,100);

System.out.println("请输入当前的月份:");
int month = scan.nextInt();

for (int i = 0; i < emps.length; i++) {
System.out.println(emps[i].toString());
System.out.println("工资为:" + emps[i].earnings());
if(month == emps[i].getBirthday().getMonth()){
System.out.println("生日快乐!加薪100");
}
}
scan.close();
}
}

7. 接口(interface)

7.1 类比

生活中大家每天都在用USB接口,那么USB接口与我们今天要学习的接口有什么相同点呢?

USB,(Universal Serial Bus,通用串行总线)是Intel公司开发的总线架构,使得在计算机上添加串行设备(鼠标、键盘、打印机、扫描仪、摄像头、充电器、MP3机、手机、数码相机、移动硬盘等)非常容易。

其实,不管是电脑上的USB插口,还是其他设备上的USB插口都只是遵循了USB规范的一种具体设备而已。

bbcc80f541000c71b81650cfaa770c86

只要设备遵循USB规范的,那么就可以与电脑互联,并正常通信。至于这个设备、电脑是哪个厂家制造的,内部是如何实现的,我们都无需关心。

Java的软件系统会有很多模块组成,那么各个模块之间也应该采用这种面向接口低耦合,为系统提供更好的可扩展性和可维护性。

7.2 概述

接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要…则必须能…”的思想。继承是一个”是不是”的is-a关系,而接口实现则是 “能不能”的has-a关系。

  • 例如:电脑都预留了可以插入USB设备的USB接口,USB接口具备基本的数据传输的开启功能和关闭功能。你能不能用USB进行连接,或是否具备USB通信功能,就看你能否遵循USB接口规范
image-20220517211517846
  • 例如:Java程序是否能够连接使用某种数据库产品,那么要看该数据库产品能否实现Java设计的JDBC规范
image-20220325235434103

接口的本质是契约、标准、规范,就像我们的法律一样。制定好后大家都要遵守。

7.3 定义格式

接口的定义,它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。

引用数据类型:数组,类,枚举,接口,注解。

7.3.1 接口的声明格式

1
2
3
4
5
6
7
8
9
[修饰符] interface 接口名{
//接口的成员列表:
// 公共的静态常量
// 公共的抽象方法

// 公共的默认方法(JDK1.8以上)
// 公共的静态方法(JDK1.8以上)
// 私有方法(JDK1.9以上)
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.atguigu.interfacetype;

public interface USB3{
//静态常量
long MAX_SPEED = 500*1024*1024;//500MB/s

//抽象方法
void in();
void out();

//默认方法
default void start(){
System.out.println("开始");
}
default void stop(){
System.out.println("结束");
}

//静态方法
static void show(){
System.out.println("USB 3.0可以同步全速地进行读写操作");
}
}

7.3.2 接口的成员说明

在JDK8.0 之前,接口中只允许出现:

(1)公共的静态的常量:其中public static final可以省略

(2)公共的抽象的方法:其中public abstract可以省略

理解:接口是从多个相似类中抽象出来的规范,不需要提供具体实现

在JDK8.0 时,接口中允许声明默认方法静态方法

(3)公共的默认的方法:其中public 可以省略,建议保留,但是default不能省略

(4)公共的静态的方法:其中public 可以省略,建议保留,但是static不能省略

在JDK9.0 时,接口又增加了:

(5)私有方法

除此之外,接口中没有构造器,没有初始化块,因为接口中没有成员变量需要动态初始化。

7.4 接口的使用规则

1、类实现接口(implements)

接口不能创建对象,但是可以被类实现(implements ,类似于被继承)。

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。

1
2
3
4
5
6
7
8
9
【修饰符】 class 实现类  implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}
image-20220514163212312

注意:

  1. 如果接口的实现类是非抽象类,那么必须重写接口中所有抽象方法

  2. 默认方法可以选择保留,也可以重写。

    重写时,default单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了

  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
interface USB{		// 
public void start() ;
public void stop() ;
}
class Computer{
public static void show(USB usb){
usb.start() ;
System.out.println("=========== USB 设备工作 ========") ;
usb.stop() ;
}
};
class Flash implements USB{
public void start(){ // 重写方法
System.out.println("U盘开始工作。") ;
}
public void stop(){ // 重写方法
System.out.println("U盘停止工作。") ;
}
};
class Print implements USB{
public void start(){ // 重写方法
System.out.println("打印机开始工作。") ;
}
public void stop(){ // 重写方法
System.out.println("打印机停止工作。") ;
}
};
public class InterfaceDemo{
public static void main(String args[]){
Computer.show(new Flash()) ;
Computer.show(new Print()) ;

c.show(new USB(){
public void start(){
System.out.println("移动硬盘开始运行");
}
public void stop(){
System.out.println("移动硬盘停止运行");
}
});
}
};

2、接口的多实现(implements)

之前学过,在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。

实现格式:

1
2
3
4
5
6
7
8
9
【修饰符】 class 实现类  implements 接口1,接口2,接口3。。。{
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口1,接口2,接口3。。。{
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次

举例:

image-20220514163311418 image-20220325235321778

1562216188519

定义多个接口:

1
2
3
public interface A {
void showA();
}
1
2
3
public interface B {
void showB();
}

定义实现类:

1
2
3
4
5
6
7
8
9
10
11
public class C implements A,B {
@Override
public void showA() {
System.out.println("showA");
}

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

测试类

1
2
3
4
5
6
7
public class TestC {
public static void main(String[] args) {
C c = new C();
c.showA();
c.showB();
}
}

3、接口的多继承(extends)

一个接口能继承另一个或者多个接口,接口的继承也使用 extends 关键字,子接口继承父接口的方法。

定义父接口:

1
2
3
4
5
public interface Chargeable {
void charge();
void in();
void out();
}

定义子接口:

1
2
3
public interface UsbC extends Chargeable,USB3 {
void reverse();
}

定义子接口的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TypeCConverter implements UsbC {
@Override
public void reverse() {
System.out.println("正反面都支持");
}

@Override
public void charge() {
System.out.println("可充电");
}

@Override
public void in() {
System.out.println("接收数据");
}

@Override
public void out() {
System.out.println("输出数据");
}
}

所有父接口的抽象方法都有重写。

方法签名相同的抽象方法只需要实现一次。

4、接口与实现类对象构成多态引用

实现类实现接口,类似于子类继承父类,因此,接口类型的变量与实现类的对象之间,也可以构成多态引用。通过接口类型的变量调用方法,最终执行的是你new的实现类对象实现的方法体。

接口的不同实现类:

1
2
3
4
5
6
7
8
9
10
11
public class Mouse implements USB3 {
@Override
public void out() {
System.out.println("发送脉冲信号");
}

@Override
public void in() {
System.out.println("不接收信号");
}
}
1
2
3
4
5
6
7
8
9
10
11
public class KeyBoard implements USB3{
@Override
public void in() {
System.out.println("不接收信号");
}

@Override
public void out() {
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
package com.atguigu.interfacetype;

public class TestComputer {
public static void main(String[] args) {
Computer computer = new Computer();
USB3 usb = new Mouse();
computer.setUsb(usb);
usb.start();
usb.out();
usb.in();
usb.stop();
System.out.println("--------------------------");

usb = new KeyBoard();
computer.setUsb(usb);
usb.start();
usb.out();
usb.in();
usb.stop();
System.out.println("--------------------------");

usb = new MobileHDD();
computer.setUsb(usb);
usb.start();
usb.out();
usb.in();
usb.stop();
}
}

5、使用接口的静态成员

接口不能直接创建对象,但是可以通过接口名直接调用接口的静态方法和静态常量。

1
2
3
4
5
6
7
8
public class TestUSB3 {
public static void main(String[] args) {
//通过“接口名.”调用接口的静态方法 (JDK8.0才能开始使用)
USB3.show();
//通过“接口名.”直接使用接口的静态常量
System.out.println(USB3.MAX_SPEED);
}
}

6、使用接口的非静态方法

  • 对于接口的静态方法,直接使用“接口名.”进行调用即可
    • 也只能使用“接口名.”进行调用,不能通过实现类的对象进行调用
  • 对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用
    • 接口不能直接创建对象,只能创建实现类的对象
1
2
3
4
5
6
7
8
9
10
11
public class TestMobileHDD {
public static void main(String[] args) {
//创建实现类对象
MobileHDD b = new MobileHDD();
//通过实现类对象调用重写的抽象方法,以及接口的默认方法,如果实现类重写了就执行重写的默认方法,如果没有重写,就执行接口中的默认方法
b.start();
b.in();
b.stop();
Usb3.show();
}
}

7.5 JDK8中相关冲突问题

7.5.1 默认方法冲突问题

(1)类优先原则

当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的抽象方法重名,子类就近选择执行父类的成员方法。代码如下:

定义接口:

1
2
3
4
5
6
7
package com.atguigu.interfacetype;

public interface Friend {
default void date(){//约会
System.out.println("吃喝玩乐");
}
}

定义父类:

1
2
3
4
5
6
7
package com.atguigu.interfacetype;

public class Father {
public void date(){//约会
System.out.println("爸爸约吃饭");
}
}

定义子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.interfacetype;

public class Son extends Father implements Friend {
@Override
public void date() {
//(1)不重写默认保留父类的
//(2)调用父类被重写的
// super.date();
//(3)保留父接口的
// Friend.super.date();
//(4)完全重写
System.out.println("跟康师傅学Java");
}
}

定义测试类:

1
2
3
4
5
6
7
8
package com.atguigu.interfacetype;

public class TestSon {
public static void main(String[] args) {
Son s = new Son();
s.date();
}
}

(2)接口冲突(左右为难)

  • 当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?

无论你多难抉择,最终都是要做出选择的。

声明接口:

1
2
3
4
5
6
7
package com.atguigu.interfacetype;

public interface BoyFriend {
default void date(){//约会
System.out.println("神秘约会");
}
}

选择保留其中一个,通过“接口名.super.方法名“的方法选择保留哪个接口的默认方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.interfacetype;

public class Girl implements Friend,BoyFriend{

@Override
public void date() {
//(1)保留其中一个父接口的
// Friend.super.date();
// BoyFriend.super.date();
//(2)完全重写
System.out.println("跟康师傅学Java");
}

}

测试类

1
2
3
4
5
6
7
8
package com.atguigu.interfacetype;

public class TestGirl {
public static void main(String[] args) {
Girl girl = new Girl();
girl.date();
}
}
  • 当一个子接口同时继承了多个接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?

另一个父接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.atguigu.interfacetype;

public interface USB2 {
//静态常量
long MAX_SPEED = 60*1024*1024;//60MB/s

//抽象方法
void in();
void out();

//默认方法
public default void start(){
System.out.println("开始");
}
public default void stop(){
System.out.println("结束");
}

//静态方法
public static void show(){
System.out.println("USB 2.0可以高速地进行读写操作");
}
}

子接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.atguigu.interfacetype;

public interface USB extends USB2,USB3 {
@Override
default void start() {
System.out.println("Usb.start");
}

@Override
default void stop() {
System.out.println("Usb.stop");
}
}

小贴士:

子接口重写默认方法时,default关键字可以保留。

子类重写默认方法时,default关键字不可以保留。

7.5.2 常量冲突问题

  • 当子类继承父类又实现父接口,而父类中存在与父接口常量同名的成员变量,并且该成员变量名在子类中仍然可见。
  • 当子类同时实现多个接口,而多个接口存在相同同名常量。

此时在子类中想要引用父类或父接口的同名的常量或成员变量时,就会有冲突问题。

父类和父接口:

1
2
3
4
5
package com.atguigu.interfacetype;

public class SuperClass {
int x = 1;
}
1
2
3
4
5
6
package com.atguigu.interfacetype;

public interface SuperInterface {
int x = 2;
int y = 2;
}
1
2
3
4
5
package com.atguigu.interfacetype;

public interface MotherInterface {
int x = 3;
}

子类:

1
2
3
4
5
6
7
8
9
10
11
package com.atguigu.interfacetype;

public class SubClass extends SuperClass implements SuperInterface,MotherInterface {
public void method(){
// System.out.println("x = " + x);//模糊不清
System.out.println("super.x = " + super.x);
System.out.println("SuperInterface.x = " + SuperInterface.x);
System.out.println("MotherInterface.x = " + MotherInterface.x);
System.out.println("y = " + y);//没有重名问题,可以直接访问
}
}

7.6 接口的总结与面试题

  • 接口本身不能创建对象【没有构造器】,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用。
  • 声明接口用interface,接口的成员声明有限制:
    • (1)公共的静态常量 public static final可以省略
    • (2)公共的抽象方法 public abstract可以省略
    • (3)公共的默认方法(JDK8.0 及以上)
    • (4)公共的静态方法(JDK8.0 及以上)
    • (5)私有方法(JDK9.0 及以上)
  • 类可以实现(implements)多个接口。如果实现类不是抽象类,就必须实现接口中所有的抽象方法。如果实现类既要继承父类又要实现父接口,那么继承(extends)在前,实现(implements)在后
  • 接口可以继承(extends)多个接口
  • 接口的默认方法可以选择重写/不重写。如果有冲突问题,另行处理。子类重写父接口的默认方法,要去掉default,子接口重写父接口的默认方法,不要去掉default。
  • 接口的静态方法不能被继承,也不能被重写。接口的静态方法只能通过“接口名.静态方法名”进行调用。

面试题

1、为什么接口中只能声明公共的静态的常量?

因为接口是标准规范,那么在规范中需要声明一些底线边界值,当实现者在实现这些规范时,不能去随意修改和触碰这些底线,否则就有“危险”。

例如:USB1.0规范中规定最大传输速率是1.5Mbps,最大输出电流是5V/500mA

​ USB3.0规范中规定最大传输速率是5Gbps(500MB/s),最大输出电流是5V/900mA

例如:尚硅谷学生行为规范中规定学员,早上8:25之前进班,晚上21:30之后离开等等。

2、为什么JDK8.0 之后允许接口定义静态方法和默认方法呢?因为它违反了接口作为一个抽象标准定义的概念。

静态方法:因为之前的标准类库设计中,有很多Collection/Colletions或者Path/Paths这样成对的接口和类,后面的类中都是静态方法,而这些静态方法都是为前面的接口服务的,那么这样设计一对API,不如把静态方法直接定义到接口中使用和维护更方便。

默认方法:(1)我们要在已有的老版接口中提供新方法时,如果添加抽象方法,就会涉及到原来使用这些接口的类就会有问题,那么为了保持与旧版本代码的兼容性,只能允许在接口中定义默认方法实现。比如:Java8中对Collection、List、Comparator等接口提供了丰富的默认方法。(2)当我们接口的某个抽象方法,在很多实现类中的实现代码是一样的,此时将这个抽象方法设计为默认方法更为合适,那么实现类就可以选择重写,也可以选择不重写。

3、为什么JDK1.9要允许接口定义私有方法呢?因为我们说接口是规范,规范是需要公开让大家遵守的。

私有方法:因为有了默认方法和静态方法这样具有具体实现的方法,那么就可能出现多个方法由共同的代码可以抽取,而这些共同的代码抽取出来的方法又只希望在接口内部使用,所以就增加了私有方法。

7.7 接口与抽象类之间的对比

image-20220328002053452

在开发中,常看到一个类要么继承抽象类,要么实现接口。

7.8 练习

笔试题:排错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface A {
int x = 0;
}
class B {
int x = 1;
}
class C extends B implements A {
public void pX() {
System.out.println(x); //没标清楚是谁的x属性
System.out.println(super.x); //输出B的属性
}
public static void main(String[] args) {
new C().pX();
}
}

笔试题:排错

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
interface Playable {
void play(); //public abstract可以省略
}

interface Bounceable {
void play(); //public abstract可以省略
}

interface Rollable extends Playable, Bounceable { //子接口继承多个父接口
Ball ball = new Ball("PingPang");
}

class Ball implements Rollable { //子类实现接口
private String name;
//get方法
public String getName() {
return name;
}
//有参构造器
public Ball(String name) {
this.name = name;
}
//重写方法
@Override
public void play() {
ball = new Ball("Football"); //必须Ball ball = new Ball("Football");
System.out.println(ball.getName());
}
}

练习1:

定义一个接口用来实现两个对象的比较。

1
2
3
4
interface CompareObject{
//若返回值是 0 , 代表相等; 若为正数,代表当前对象大;负数代表当前对象小
public int compareTo(Object o);
}

定义一个Circle类,声明redius属性,提供getter和setter方法

定义一个ComparableCircle类,继承Circle类并且实现CompareObject接口。在ComparableCircle类中给出接口中方法compareTo的实现体,用来比较两个圆的半径大小。

定义一个测试类InterfaceTest,创建两个ComparableCircle对象,调用compareTo方法比较两个类的半径大小。

思考:参照上述做法定义矩形类Rectangle和ComparableRectangle类,在ComparableRectangle类中给出compareTo方法的实现,比较两个矩形的面积大小。

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
public class Circle {
private double radius;
//无参构造器
public Circle() {
}
//有参构造器
public Circle(double radius) {
this.radius = radius;
}
public double getRadius(){
return radius;
}
public void setRadius(double radius){
this.radius=radius;
}

@Override
public String toString() {
return "Circle{" +"radius=" + radius +'}';
}
}

//接口
public interface CompareObject {
//若返回值是0,代表相等
//若为正数,代表当前对象大
//若为负数,代表当前对象小
public int conpareTo(Object o); //抽象方法
}

//继承Circle类 实现CompareObject接口
public class ComparableCircle extends Circle implements CompareObject{ //继承Circle类 实现CompareObject接口
public ComparableCircle() {
}

public ComparableCircle(double radius) {
super(radius);
}

@Override
public int conpareTo(Object o) {
if(this == o){ //如果当前比较的两个对象一致 返回0
return 0;
}
if(o instanceof ComparableCircle){ //o属于圆类
ComparableCircle c = (ComparableCircle)o; //类型强制向下转型
return Double.compare(this.getRadius(),c.getRadius()); //调用Double.compare方法对比
}else{
return 2; //如果输入的类型不匹配,则返回2
}
}

}

//test测试
public class InterfaceTest {
public static void main(String[] args) {
ComparableCircle c1=new ComparableCircle();
ComparableCircle c2=new ComparableCircle();
System.out.println(c1.conpareTo(c2));
}
}

练习2:交通工具案例

阿里的一个工程师,声明的属性和方法如下:

image-20220504172547709

其中,有一个乘坐交通工具的方法takingVehicle(),在此方法中调用交通工具的run()。为了出行方便,他买了一辆捷安特自行车、一辆雅迪电动车和一辆奔驰轿车。这里涉及到的相关类及接口关系如下:

image-20220504172918861

其中,电动车增加动力的方式是充电,轿车增加动力的方式是加油。在具体交通工具的run()中调用其所在类的相关属性信息。

请编写相关代码,并测试。

提示:创建Vehicle[]数组,保存阿里工程师的三辆交通工具,并分别在工程师的takingVehicle()中调用。

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 Developer {
private String name;
private int age;
//set和get方法
public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
//乘坐交通工具
public void takingVehicle(Vehicle vehicle){
vehicle.run();
}
}
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 abstract class Vehicle {  //抽象类
private String brand;
private String color;

public Vehicle(){

}

public Vehicle(String brand,String color){ //子类调用
this.brand=brand;
this.color=color;
}
//set和get方法
public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}

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

}
1
2
3
4
public interface IPower {
//抽象方法
public abstract void power(); //电车和骑车的充电功能
}
1
2
3
4
5
6
7
8
9
10
11
public class Bycicle extends Vehicle{
public Bycicle(String brand,String color){
super(brand,color); //调用父类有参构造器
}
//实现抽象类父类的方法
@Override
public void run() {
System.out.println("开的是:"+getBrand()+",颜色是:"+getColor());
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ElectricVehicle extends Vehicle implements IPower{
public ElectricVehicle(String brand,String color){
super(brand,color); //调用父类构造器
}
//实现接口方法
@Override
public void power() {
System.out.println("充电");
}
//实现抽象类父类方法
@Override
public void run() {
System.out.println("开的是:"+super.getBrand()+",颜色是:"+super.getColor());
}
}
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 Car extends Vehicle implements IPower{
private String carNumber;
public Car(String brand,String color,String carNumber){
super(brand,color); //调用父类构造器
this.carNumber=carNumber;
}

public String getCarNumber() {
return carNumber;
}

public void setCarNumber(String carNumber) {
this.carNumber = carNumber;
}

//实现接口方法
@Override
public void power() {
System.out.println("加油");
}
//实现抽象类父类方法
@Override
public void run() {
System.out.println("开的是:"+super.getBrand()+",颜色是:"+super.getColor()+",开的车号码牌是:"+this.getCarNumber());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class DeveloperTest {
public static void main(String[] args) {
Developer test=new Developer();
Vehicle[] vehicles=new Vehicle[3];
//创建三个交通工具
vehicles[0]=new Bycicle("捷安特","黄色");
vehicles[1]=new Car("奥迪","黄色","1231123");
vehicles[2]=new ElectricVehicle("电动车","绿色");
test.takingVehicle(vehicles[0]);
test.takingVehicle(vehicles[1]);
test.takingVehicle(vehicles[2]);
}
}

最终代码结果如下:

image-20231011102622680

8. 内部类(InnerClass)

8.1 概述

8.1.1 什么是内部类

将一个类B定义在另一个类A里面,里面的那个类B就称为内部类(InnerClass),类A则称为外部类(OuterClass)

8.1.2 为什么要声明内部类呢

具体来说,当一个事物A的内部,还有一个部分需要一个完整的结构B进行描述,而这个内部的完整的结构B又只为外部事物A提供服务,不在其他地方单独使用,那么整个内部的完整结构B最好使用内部类。

总的来说,遵循高内聚、低耦合的面向对象开发原则。

8.1.3 内部类的分类

根据内部类声明的位置(如同变量的分类),我们可以分为:

image-20221124223912529

8.2 成员内部类

8.2.1 概述

如果成员内部类中不使用外部类的非静态成员,那么通常将内部类声明为静态内部类,否则声明为非静态内部类。

语法格式:

1
2
3
4
[修饰符] class 外部类{
[其他修饰符] [static] class 内部类{
}
}

成员内部类的使用特征,概括来讲有如下两种角色:

  • 成员内部类作为类的成员
    • 和外部类不同,Inner class还可以声明为private或protected;
    • 可以调用外部类的结构。(注意:在静态内部类中不能使用外部类的非静态成员)
    • Inner class 可以声明为static的,但此时就不能再使用外层类的非static的成员变量;
  • 成员内部类作为
    • 可以在内部定义属性、方法、构造器等结构
    • 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
    • 可以声明为abstract类 ,因此可以被其它的内部类继承
    • 可以声明为final的,表示不能被继承
    • 编译以后生成OuterClass$InnerClass.class字节码文件(也适用于局部内部类)

注意点:

  1. 外部类访问成员内部类的成员,需要“内部类.成员”或“内部类对象.成员”的方式

  2. 成员内部类可以直接使用外部类的所有成员,包括私有的数据

  3. 当想要在外部类的静态成员部分使用内部类时,可以考虑内部类声明为静态的

8.2.2 创建成员内部类对象

  • 实例化静态内部类
1
2
外部类名.静态内部类名 变量 = 外部类名.静态内部类名();
变量.非静态方法();
  • 实例化非静态内部类
1
2
3
外部类名 变量1 = new 外部类();
外部类名.非静态内部类名 变量2 = 变量1.new 非静态内部类名();
变量2.非静态方法();

8.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
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
77
78
79
80
81
82
83
84
85
86
87
88
89
public class TestMemberInnerClass {
public static void main(String[] args) {
//创建静态内部类实例,并调用方法
Outer.StaticInner inner = new Outer.StaticInner();
inner.inFun();
//调用静态内部类静态方法
Outer.StaticInner.inMethod();

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

//创建非静态内部类实例(方式1),并调用方法
Outer outer = new Outer();
Outer.NoStaticInner inner1 = outer.new NoStaticInner();
inner1.inFun();

//创建非静态内部类实例(方式2)
Outer.NoStaticInner inner2 = outer.getNoStaticInner();
inner1.inFun();
}
}
//外部类
class Outer{
private static String a = "外部类的静态a";
private static String b = "外部类的静态b";
private String c = "外部类对象的非静态c";
private String d = "外部类对象的非静态d";
//内部静态类
static class StaticInner{
private static String a ="静态内部类的静态a";
private String c = "静态内部类对象的非静态c";
public static void inMethod(){
System.out.println("Inner.a = " + a);
System.out.println("Outer.a = " + Outer.a);
System.out.println("b = " + b);
}
public void inFun(){
System.out.println("Inner.inFun");
System.out.println("Outer.a = " + Outer.a);
System.out.println("Inner.a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
// System.out.println("d = " + d);//不能访问外部类的非静态成员
}
}

class NoStaticInner{
private String a = "非静态内部类对象的非静态a";
private String c = "非静态内部类对象的非静态c";

public void inFun(){
System.out.println("NoStaticInner.inFun");
System.out.println("Outer.a = " + Outer.a);
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("Outer.c = " + Outer.this.c);
System.out.println("c = " + c);
System.out.println("d = " + d);
}
}


public NoStaticInner getNoStaticInner(){
return new NoStaticInner();
}
}
最终输出:
Inner.inFun
Outer.a = 外部类的静态a
Inner.a = 静态内部类的静态a
b = 外部类的静态b
c = 静态内部类对象的非静态c
Inner.a = 静态内部类的静态a
Outer.a = 外部类的静态a
b = 外部类的静态b
*****************************
NoStaticInner.inFun
Outer.a = 外部类的静态a
a = 非静态内部类对象的非静态a
b = 外部类的静态b
Outer.c = 外部类对象的非静态c
c = 非静态内部类对象的非静态c
d = 外部类对象的非静态d
NoStaticInner.inFun
Outer.a = 外部类的静态a
a = 非静态内部类对象的非静态a
b = 外部类的静态b
Outer.c = 外部类对象的非静态c
c = 非静态内部类对象的非静态c
d = 外部类对象的非静态d

8.3 局部内部类

8.3.1 非匿名局部内部类

语法格式:

1
2
3
4
5
6
[修饰符] class 外部类{
[修饰符] 返回值类型 方法名(形参列表){
[final/abstract] class 内部类{
}
}
}
  • 编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名、$符号、编号。
    • 这里有编号是因为同一个外部类中,不同的方法中存在相同名称的局部内部类
  • 和成员内部类不同的是,它前面不能有权限修饰符等
  • 局部内部类如同局部变量一样,有作用域
  • 局部内部类中是否能访问外部类的非静态的成员,取决于所在的方法

举例:

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
public class TestLocalInner {
public static void main(String[] args) {
Outer.outMethod(); //调用外部类的静态方法
//输出Outer.outMethod
//创建局部内部类的对象
//调用局部内部类的方法 输出Inner.inMehtod
//输出 外部类方法属性 局部变量c
System.out.println("-------------------");

Outer out = new Outer(); //创建外部类对象
out.outTest(); //调用外部类非静态对象
//创建局部内部类的对象
//调用局部内部类的方法 输出Inner.inMethod1
System.out.println("-------------------");

Runner runner = Outer.getRunner(); //创建外部类对象
runner.run(); //调用实现接口的方法
//输出LocalRunner.run

}
}

class Outer{ //外部类

public static void outMethod(){
System.out.println("Outer.outMethod");
final String c = "局部变量c";
//局部内部类
class Inner{ //内部类
public void inMethod(){
System.out.println("Inner.inMethod");
System.out.println(c);
}
}

Inner in = new Inner();
in.inMethod();
}

public void outTest(){
//局部内部类
class Inner{
public void inMethod1(){
System.out.println("Inner.inMethod1");
}
}

Inner in = new Inner();
in.inMethod1();
}

public static Runner getRunner(){
class LocalRunner implements Runner{ //实现接口
@Override
public void run() { //重写run方法
System.out.println("LocalRunner.run");
}
}
return new LocalRunner();
}

}
//接口
interface Runner{
public abstract void run(); //抽象方法
}
最终输出:
Outer.outMethod
Inner.inMethod
局部变量c
-------------------
Inner.inMethod1
-------------------
LocalRunner.run

8.3.2 匿名内部类

因为考虑到这个子类或实现类是一次性的,那么我们“费尽心机”的给它取名字,就显得多余。那么我们完全可以使用匿名内部类的方式来实现,避免给类命名的问题。

1
2
3
new 父类([实参列表]){
重写方法...
}
1
2
3
new 父接口(){
重写方法...
}

举例1:使用匿名内部类的对象直接调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface A{
public abstract void a(); //抽象方法
}
public class Test{
public static void main(String[] args){
new A(){
@Override
public void a() { //重写接口方法
System.out.println("aaaa");
}
}.a(); //相当于 new 实现类(); 然后调用.a()方法
}
}
最终输出:
aaaa

举例2:通过父类或父接口的变量多态引用匿名内部类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface A{
void a();
}
public class Test{
public static void main(String[] args){
A obj = new A(){
@Override
public void a() {
System.out.println("aaaa");
}
};
obj.a(); // 然后调用.a()方法
}
}

举例3:匿名内部类的对象作为实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface A{
void method();
}
public class Test{
public static void test(A a){ //静态方法
a.method();
}

public static void main(String[] args){
//直接Test.test(A);这样 其中的A属于匿名内部类的对象
test(new A(){
@Override
public void method() {
System.out.println("aaaa");
}
});
}
}

8.4 练习

练习:判断输出结果为何?

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
//外部类
public class Test { //外部类
//无参构造器
public Test() {
Inner s1 = new Inner(); //创建内部类对象
s1.a = 10;
Inner s2 = new Inner(); //创建内部类对象
s2.a = 20;
//创建内部类对象
Test.Inner s3 = new Test.Inner();
System.out.println(s3.a); //输出s3的a属性
}
//非静态内部类
class Inner {
public int a = 5;
}

//main执行
public static void main(String[] args) {
//创建外部类对象
//直接走无参构造器
//创建内部类对象s1 获得s1.a=5 更改为10
//创建内部类对象s2 获得s2.a=5 更改为20
//创建内部类对象s3 获得s3.a=5 输出5
Test t = new Test();
System.out.println("-------");
//创建内部类对象
//常见内部类对象r 获得r.a=5 输出5
Inner r = t.new Inner();
//输出内部类对象的a属性
//直接输出5
System.out.println(r.a);
}
}
最终输出:
5
---------------------
5

练习2:

编写一个匿名内部类,它继承Object,并在匿名内部类中,声明一个方法public void test()打印尚硅谷。

请编写代码调用这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
//第一种方式
public class Test02 {
public static void main(String[] args) {
//匿名内部类 直接创建Object的对象
new Object(){
public void test(){
System.out.println("尚硅谷");
}
}.test();

}
}
//二和三方式没办法解决 --因为没被重写

9. 枚举类

9.1 概述

  • 枚举类型本质上也是一种类,只不过是这个类的对象有限的固定几个不让用户随意创建

  • 枚举类的例子:

    • 星期:Monday(星期一)……Sunday(星期天)
    • 性别:Man(男)、Woman(女)
    • 月份:January(1月)……December(12月)
    • 季节:Spring(春节)……Winter(冬天)
    • 三原色:red(红色)、green(绿色)、blue(蓝色)
    • 支付方式:Cash(现金)、WeChatPay(微信)、Alipay(支付宝)、BankCard(银行卡)、CreditCard(信用卡)
    • 就职状态:Busy(忙碌)、Free(空闲)、Vocation(休假)、Dimission(离职)
    • 订单状态:Nonpayment(未付款)、Paid(已付款)、Fulfilled(已配货)、Delivered(已发货)、Checked(已确认收货)、Return(退货)、Exchange(换货)、Cancel(取消)
    • 线程状态:创建、就绪、运行、阻塞、死亡
  • 若枚举只有一个对象, 则作为一种单例模式的实现。

  • 枚举类的实现:

    • 在JDK5.0 之前,需要程序员自定义枚举类型。
    • 在JDK5.0 之后,Java支持enum关键字来快速定义枚举类型。

9.2 定义枚举类(JDK5.0 之前)

在JDK5.0 之前如何声明枚举类呢?

  • 私有化类的构造器,保证不能在类的外部创建其对象
  • 在类的内部创建枚举类的实例。声明为:public static final ,对外暴露这些常量对象
  • 对象如果有实例变量,应该声明为private final(建议,不是必须),并在构造器中初始化

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Season{
private final String SEASONNAME;//季节的名称
private final String SEASONDESC;//季节的描述
private Season(String seasonName,String seasonDesc){
this.SEASONNAME = seasonName;
this.SEASONDESC = seasonDesc;
}
public static final Season SPRING = new Season("春天", "春暖花开");
public static final Season SUMMER = new Season("夏天", "夏日炎炎");
public static final Season AUTUMN = new Season("秋天", "秋高气爽");
public static final Season WINTER = new Season("冬天", "白雪皑皑");

@Override
public String toString() {
return "Season{" +"SEASONNAME='" + SEASONNAME + '\'' +", SEASONDESC='" + SEASONDESC + '\'' +'}';
}
}
class SeasonTest{
public static void main(String[] args) {
System.out.println(Season.AUTUMN);
}
}

9.3 定义枚举类(JDK5.0 之后)

9.3.1 enum关键字声明枚举

1
2
3
4
5
6
7
8
【修饰符】 enum 枚举类名{
常量对象列表
}

【修饰符】 enum 枚举类名{
常量对象列表;
对象的实例变量列表;
}

举例1:

1
2
3
public enum Week {
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
}
1
2
3
4
5
6
public class TestEnum {
public static void main(String[] args) {
Season spring = Season.SPRING;
System.out.println(spring);
}
}

9.3.2 enum方式定义的要求和特点

  • 枚举类的常量对象列表必须在枚举类的首行,因为是常量,所以建议大写。
  • 列出的实例系统会自动添加 public static final 修饰。
  • 如果常量对象列表后面没有其他代码,那么“;”可以省略,否则不可以省略“;”。
  • 编译器给枚举类默认提供的是private的无参构造,如果枚举类需要的是无参构造,就不需要声明,写常量对象列表时也不用加参数
  • 如果枚举类需要的是有参构造,需要手动定义,有参构造的private可以省略,调用有参构造的方法就是在常量对象名后面加(实参列表)就可以。
  • 枚举类默认继承的是java.lang.Enum类,因此不能再继承其他的类型。
  • JDK5.0 之后switch,提供支持枚举类型,case后面可以写枚举常量名,无需添加枚举类作为限定。

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum SeasonEnum {
SPRING("春天","春风又绿江南岸"),
SUMMER("夏天","映日荷花别样红"),
AUTUMN("秋天","秋水共长天一色"),
WINTER("冬天","窗含西岭千秋雪");

private final String seasonName;
private final String seasonDesc;

private SeasonEnum(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
}

举例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum Week {
MONDAY("星期一"),
TUESDAY("星期二"),
WEDNESDAY("星期三"),
THURSDAY("星期四"),
FRIDAY("星期五"),
SATURDAY("星期六"),
SUNDAY("星期日");

private final String description;

private Week(String description){
this.description = description;
}

@Override
public String toString() {
return super.toString() +":"+ description;
}
}
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 TestWeek {
public static void main(String[] args) {
Week week = Week.MONDAY;
System.out.println(week);

switch (week){
case MONDAY:
System.out.println("怀念周末,困意很浓");break;
case TUESDAY:
System.out.println("进入学习状态");break;
case WEDNESDAY:
System.out.println("死撑");break;
case THURSDAY:
System.out.println("小放松");break;
case FRIDAY:
System.out.println("又信心满满");break;
case SATURDAY:
System.out.println("开始盼周末,无心学习");break;
case SUNDAY:
System.out.println("一觉到下午");break;
}
}
}

经验之谈:

开发中,当需要定义一组常量时,强烈建议使用枚举类。

9.4 enum中常用方法

1
2
3
4
5
6
7
8
9
1. String toString(): 默认返回的是常量名(对象名),可以继续手动重写该方法!

2. static 枚举类型[] values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值,是一个静态方法

3. static 枚举类型 valueOf(String name):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。

4. String name():得到当前枚举常量的名称。建议优先使用toString()。

5. int ordinal():返回当前枚举常量的次序号,默认从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
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
public class SeasonTest1 {
public static void main(String[] args) {
//1.toString() ---返回常量名(对象名)
System.out.println(Season1.SPRING);
System.out.println(Season1.AUTUMN);
System.out.println("---------");

//2.name() ---得到当前枚举常量的名称 [建议先用toString()[
System.out.println(Season1.AUTUMN.name());
System.out.println("---------");

//静态方法
//3.values() ---返回枚举类型的对象数组,可以方便遍历所有的枚举值
Season1[] values=Season1.values();
for (int i = 0; i < values.length; i++) {
System.out.println(values[i]);
}
System.out.println("---------");

//4.valueOf(String name) ---一个字符串转为对应的枚举类对象 [要求字符串必须是枚举类对象的"名字"]
String objName="WINTER";
Season1 s1=Season1.valueOf(objName);
System.out.println(s1);
System.out.println("---------");

//5.ordinal() ---返回当前枚举常量的次序号[从0开始]
System.out.println(Season1.AUTUMN.ordinal());

}
}

enum Season1{
//1.开头必须声明多个对象 之间用逗号隔开
SPRING("春天", "春暖花开"),
SUMMER("夏天", "夏日炎炎"),
AUTUMN("秋天", "秋高气爽"),
WINTER("冬天", "白雪皑皑");
//1.私有化类的构造器
private Season1(String seasonName, String seasoonDesc){
this.seasonName = seasonName;
this.seasoonDesc = seasoonDesc;
}
//2.声明当前类的对象的实例变量 --只让他获取(private)但是不能修改(没有set方法) 【get方法/构造器内赋值】
private final String seasonName; //季节的名称
private final String seasoonDesc; //季节的描述
//3.提供get方法
public String getSeasonName() {
return seasonName;
}
public String getSeasoonDesc() {
return seasoonDesc;
}
}
最终输出:
SPRING
AUTUMN
---------
AUTUMN
---------
SPRING
SUMMER
AUTUMN
WINTER
---------
WINTER
---------
2

9.5 实现接口的枚举类

  • 和普通 Java 类一样,枚举类可以实现一个/多个接口
  • 若每个枚举值呈现相同的行为方式,统一实现该方法。
  • 若每个枚举值呈现出不同的行为方式,让每个枚举值分别实现该方法

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1、枚举类可以像普通的类一样,实现接口,并且可以多个,但要求必须实现里面所有的抽象方法!
enum A implements 接口1,接口2{
//抽象方法的实现
}

//2、如果枚举类的常量可以继续重写抽象方法!
enum A implements 接口1,接口2{
常量名1(参数){
//抽象方法的实现或重写
},
常量名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
45
46
47
48
49
50
51
52
53
54
55
interface Info{
public abstract void show(); //抽象方法
}

enum Season1 implements Info{
//1.开头必须声明多个对象 之间用逗号隔开
SPRING("春天", "春暖花开"){
@Override
public void show(){
System.out.println("春天在哪里?");
}
},
SUMMER("夏天", "夏日炎炎"){
@Override
public void show(){
System.out.println("春天在哪里?");
}
},
AUTUMN("秋天", "秋高气爽"){
@Override
public void show(){
System.out.println("春天在哪里?");
}
},
WINTER("冬天", "白雪皑皑"){
@Override
public void show(){
System.out.println("春天在哪里?");
}
};

//1.私有化类的构造器
private Season1(String seasonName, String seasoonDesc){
this.seasonName = seasonName;
this.seasoonDesc = seasoonDesc;
}

//2.声明当前类的对象的实例变量 --只让他获取(private)但是不能修改(没有set方法) 【get方法/构造器内赋值】
private final String seasonName; //季节的名称
private final String seasoonDesc; //季节的描述

//3.提供get方法
public String getSeasonName() {
return seasonName;
}
public String getSeasoonDesc() {
return seasoonDesc;
}

@Override
public void show(){
System.out.println("统一的重写");
}

}

10. 注解(Annotation)

10.1 注解概述

10.1.1 什么是注解

注解(Annotation)是从JDK5.0开始引入,以“@注解名”在代码中存在。例如:

1
@Override
1
@Deprecated
1
@SuppressWarnings(value=”unchecked”)

Annotation 可以像修饰符一样被使用,
①修饰包、类、构造器、方法、成员变量、参数、局部变量的声明、
②参数值。这些信息被保存在注解的 “name=value” 中。

注解可以在类编译、运行时进行加载,体现不同的功能。

10.1.2 注解与注释

注解也可以看做是一种注释,通过使用 Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。但是,注解,不同于单行注释和多行注释。

  • 对于单行注释和多行注释是给程序员看的。
  • 而注解是可以被编译器或其他程序读取的。程序还可以根据注解的不同,做出相应的处理。

10.1.3 注解的重要性

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

未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,Hibernate3.x以后也是基于注解的,Struts2有一部分也是基于注解的了。注解是一种趋势,一定程度上可以说:框架 = 注解 + 反射 + 设计模式

10.2 常见的Annotation作用

示例1:生成文档相关的注解

1
2
3
4
5
6
7
@author 标明开发该类模块的作者,多个作者之间使用,分割
@version 标明该类模块的版本
@see 参考转向,也就是相关主题
@since 从哪个版本开始增加的
@param 对方法中某参数的说明,如果没有参数就不能写
@return 对方法返回值的说明,如果方法的返回值类型是void就不能写
@exception 对方法可能抛出的异常进行说明 ,如果方法没有用throws显式抛出的异常就不能写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.annotation.javadoc;
/**
* @author 尚硅谷-宋红康
* @version 1.0
* @see Math.java
*/
public class JavadocTest {
/**
* 程序的主方法,程序的入口
* @param args String[] 命令行参数
*/
public static void main(String[] args) {
}

/**
* 求圆面积的方法
* @param radius double 半径值
* @return double 圆的面积
*/
public static double getArea(double radius){
return Math.PI * radius * radius;
}
}

示例2:在编译时进行格式检查(JDK内置的三个基本注解)

@Override: 限定重写父类方法,该注解只能用于方法

@Deprecated: 用于表示所修饰的元素(类,方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择

@SuppressWarnings: 抑制编译器警告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.annotation.javadoc;
 
public class AnnotationTest{
 
public static void main(String[] args) {
@SuppressWarnings("unused")
int a = 10;
}
@Deprecated
public void print(){
System.out.println("过时的方法");
}
 
@Override
public String toString() {
return "重写的toString方法()";
}
}

示例3:跟踪代码依赖性,实现替代配置文件功能

  • Servlet3.0提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署。
1
2
3
4
5
6
7
8
9
10
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

protected void doGet(HttpServletRequest request, HttpServletResponse response) { }
 
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
doGet(request, response);
}
}
1
2
3
4
5
6
7
8
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
  • Spring框架中关于“事务”的管理
1
2
3
4
5
6
7
8
9
@Transactional(propagation=Propagation.REQUIRES_NEW,isolation=Isolation.READ_COMMITTED,readOnly=false,timeout=3)
public void buyBook(String username, String isbn) {
//1.查询书的单价
int price = bookShopDao.findBookPriceByIsbn(isbn);
//2. 更新库存
bookShopDao.updateBookStock(isbn);
//3. 更新用户的余额
bookShopDao.updateUserAccount(username, price);
}
1
2
3
4
5
6
7
8
<!-- 配置事务属性 -->
<tx:advice transaction-manager="dataSourceTransactionManager" id="txAdvice">
<tx:attributes>
<!-- 配置每个方法使用的事务属性 -->
<tx:method name="buyBook" propagation="REQUIRES_NEW"
isolation="READ_COMMITTED" read-only="false" timeout="3" />
</tx:attributes>
</tx:advice>

10.3 三个最基本的注解

10.3.1 @Override

  • 用于检测被标记的方法为有效的重写方法,如果不是,则报编译错误!
  • 只能标记在方法上。
  • 它会被编译器程序读取。

10.3.2 @Deprecated

  • 用于表示被标记的数据已经过时,不推荐使用。
  • 可以用于修饰 属性、方法、构造、类、包、局部变量、参数。
  • 它会被编译器程序读取。

10.3.3 @SuppressWarnings

  • 抑制编译警告。当我们不希望看到警告信息的时候,可以使用 SuppressWarnings 注解来抑制警告信息
  • 可以用于修饰类、属性、方法、构造、局部变量、参数
  • 它会被编译器程序读取。

  • 可以指定的警告类型有(了解)

    • all,抑制所有警告
    • unchecked,抑制与未检查的作业相关的警告
    • unused,抑制与未用的程式码及停用的程式码相关的警告
    • deprecation,抑制与淘汰的相关警告
    • nls,抑制与非 nls 字串文字相关的警告
    • null,抑制与空值分析相关的警告
    • rawtypes,抑制与使用 raw 类型相关的警告
    • static-access,抑制与静态存取不正确相关的警告
    • static-method,抑制与可能宣告为 static 的方法相关的警告
    • super,抑制与置换方法相关但不含 super 呼叫的警告

示例代码:

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
import java.util.ArrayList;
public class TestAnnotation {
@SuppressWarnings("all")
public static void main(String[] args) {
int i;

ArrayList list = new ArrayList();
list.add("hello");
list.add(123);
list.add("world");

Father f = new Son();
f.show();
f.methodOl();
}
}

class Father{
@Deprecated
void show() {
System.out.println("Father.show");
}
void methodOl() {
System.out.println("Father Method");
}
}

class Son extends Father{
/* @Override
void method01() {
System.out.println("Son Method");
}*/
}

10.4 元注解

元注解:对现有的注解进行解释说明的注解。

JDK1.5在java.lang.annotation包定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。

(1)@Target:用于描述注解的使用范围

  • 可以通过枚举类型ElementType的10个常量对象来指定
  • TYPE,METHOD,CONSTRUCTOR,PACKAGE…..

(2)@Retention:用于描述注解的生命周期

  • 可以通过枚举类型RetentionPolicy的3个常量对象来指定
  • SOURCE(源代码)、CLASS(字节码)、RUNTIME(运行时)
  • 唯有RUNTIME阶段才能被反射读取到

(3)@Documented:表明这个注解应该被 javadoc工具记录。

(4)@Inherited:允许子类继承父类中的注解

示例代码:

1
2
3
4
5
6
7
8
package java.lang;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
1
2
3
4
5
6
7
8
9
10
package java.lang;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
1
2
3
4
5
6
7
8
9
10
package java.lang;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

拓展:元数据

String name = “Tom”;

10.5 自定义注解的使用

一个完整的注解应该包含三个部分:
(1)声明
(2)使用
(3)读取

10.5.1 声明自定义注解

1
2
3
4
【元注解】
【修饰符】 @interface 注解名{
【成员列表】
}
  • 自定义注解可以通过四个元注解@Retention,@Target,@Inherited,@Documented,分别说明它的声明周期,使用位置,是否被继承,是否被生成到API文档中。
  • Annotation 的成员在 Annotation 定义中以无参数有返回值的抽象方法的形式来声明,我们又称为配置参数。返回值类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组。
  • 可以使用 default 关键字为抽象方法指定默认返回值
  • 如果定义的注解含有抽象方法,那么使用时必须指定返回值,除非它有默认值。格式是“方法名 = 返回值”,如果只有一个抽象方法需要赋值,且方法名为value,可以省略“value=”,所以如果注解只有一个抽象方法成员,建议使用方法名value。
1
2
3
4
5
6
7
import java.lang.annotation.*;
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value();
}
1
2
3
4
5
6
7
8
import java.lang.annotation.*;
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String columnName();
String columnType();
}

10.5.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
package com.atguigu.annotation;

@Table("t_stu")
public class Student {
@Column(columnName = "sid",columnType = "int")
private int id;
@Column(columnName = "sname",columnType = "varchar(20)")
private String name;

public int getId() {
return id;
}

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

public String getName() {
return name;
}

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

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

10.5.3 读取和处理自定义注解

自定义注解必须配上注解的信息处理流程才有意义。

我们自己定义的注解,只能使用反射的代码读取。所以自定义注解的声明周期必须是RetentionPolicy.RUNTIME。

具体的使用见《尚硅谷_宋红康_第17章_反射机制.md》

10.6 JUnit单元测试

10.6.1 测试分类

黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望的值。

白盒测试:需要写代码的。关注程序具体的执行流程。

image-20220511181800694

image-20220524102038600

10.6.2 JUnit单元测试介绍

JUnit 是由 Erich Gamma 和 Kent Beck 编写的一个测试框架(regression testing framework),供Java开发人员编写单元测试之用。

JUnit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。

要使用JUnit,必须在项目的编译路径中引入JUnit的库,即相关的.class文件组成的jar包。jar就是一个压缩包,压缩包都是开发好的第三方(Oracle公司第一方,我们自己第二方,其他都是第三方)工具类,都是以class文件形式存在的。

10.6.3 引入本地JUnit.jar

第1步:在项目中File-Project Structure中操作:添加Libraries库

image-20211228180938922

image-20221002195547325

其中,junit-libs包内容如下:

image-20220813005206452

第2步:选择要在哪些module中应用JUnit库

image-20220813005511062

第3步:检查是否应用成功

image-20220813005729233

注意Scope:选择Compile,否则编译时,无法使用JUnit。

第4步:下次如果有新的模块要使用该libs库,这样操作即可

image-20220813005944022

image-20220813010018152

image-20220813010055217

image-20220813010124381

10.6.4 编写和运行@Test单元测试方法

JUnit4版本,要求@Test标记的方法必须满足如下要求:

  • 所在的类必须是public的,非抽象的,包含唯一的无参构造器。
  • @Test标记的方法本身必须是public,非抽象的,非静态的,void无返回值,()无参数的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.junit.Test;
public class TestJUnit {
@Test
public void test01(){
System.out.println("TestJUnit.test01");
}

@Test
public void test02(){
System.out.println("TestJUnit.test02");
}

@Test
public void test03(){
System.out.println("TestJUnit.test03");
}
}

image-20220106152412245

10.6.5 设置执行JUnit用例时支持控制台输入

1. 设置数据:

默认情况下,在单元测试方法中使用Scanner时,并不能实现控制台数据的输入。需要做如下设置:

idea64.exe.vmoptions配置文件中加入下面一行设置,重启idea后生效。

1
-Deditable.java.test.console=true

2. 配置文件位置:

image-20220813011625546

image-20220813011642180

添加完成之后,重启IDEA即可。

3. 如果上述位置设置不成功,需要继续修改如下位置

修改位置1:IDEA安装目录的bin目录(例如:D:\develop_tools\IDEA\IntelliJ IDEA 2022.1.2\bin)下的idea64.exe.vmoptions文件。

修改位置2:C盘的用户目录C:\Users\用户名\AppData\Roaming\JetBrains\IntelliJIdea2022.1 下的idea64.exe.vmoptions`件。

10.6.6 定义test测试方法模板

选中自定义的模板组,点击”+”(1.Live Template)来定义模板。

image-20211229100040505

11. 包装类

11.1 为什么需要包装类

Java提供了两个类型系统,基本数据类型引用数据类型。使用基本数据类型在于效率,然而当要使用只针对对象设计的API或新特性(例如泛型),怎么办呢?例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//情况1:方法形参
Object类的equals(Object obj)

//情况2:方法形参
ArrayList类的add(Object obj)
//没有如下的方法:
add(int number)
add(double d)
add(boolean b)

//情况3:泛型
Set<T>
List<T>
Cllection<T>
Map<K,V>

11.2 有哪些包装类

Java针对八种基本数据类型定义了相应的引用类型:包装类(封装类)。有了类的特点,就可以调用类中的方法,Java才是真正的面向对象。

image-20220329001912486

封装以后的,内存结构对比:

1
2
3
4
public static void main(String[] args){
int num = 520;
Integer obj = new Integer(520);
}
image-20220514163725830

11.3 自定义包装类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyInteger {
//属性
int value;
//无参构造器
public MyInteger() {
}
//有参构造器
public MyInteger(int value) {
this.value = value;
}
//重写toString方法
@Override
public String toString() {
return String.valueOf(value); //返回valueOf方法的值
}
}

11.4 包装类与基本数据类型间的转换

11.4.1 装箱

装箱:把基本数据类型转为包装类对象

转为包装类的对象,是为了使用专门为对象设计的API和特性

基本数值—->包装对象

1
2
3
4
5
Integer obj1 = new Integer(4);//使用构造函数函数
Float f = new Float(“4.56”);
Long l = new Long(“asdf”); //NumberFormatException

Integer obj2 = Integer.valueOf(4);//使用包装类中的valueOf方法

11.4.2 拆箱

拆箱:把包装类对象拆为基本数据类型

转为基本数据类型,一般是因为需要运算,Java中的大多数运算符是为基本数据类型设计的。比较、算术等

包装对象—->基本数值

1
2
Integer obj = new Integer(4);
int num1 = obj.intValue();

自动装箱与拆箱:

由于我们经常要做基本类型与包装类之间的转换,从JDK5.0开始,基本类型与包装类的装箱、拆箱动作可以自动完成。例如:

1
2
3
Integer i = 4;//自动装箱。相当于Integer i = Integer.valueOf(4);
i = i + 5;//等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5;
//加法运算完成后,再次装箱,把基本数值转成对象。

注意:只能与自己对应的类型之间才能实现自动装箱与拆箱。

1
2
Integer i = 1;
Double d = 1;//错误的,1是int类型

11.5 基本数据类型、包装类与字符串间的转换

(1)基本数据类型转为字符串

方式1:调用字符串重载的valueOf()方法

1
2
3
4
int a = 10;
//String str = a;//错误的

String str = String.valueOf(a);

方式2:更直接的方式

1
2
3
int a = 10;

String str = a + "";

(2)字符串转为基本数据类型

方式1:除了Character类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型,例如:

  • public static int parseInt(String s):将字符串参数转换为对应的int基本类型。
  • public static long parseLong(String s):将字符串参数转换为对应的long基本类型。
  • public static double parseDouble(String s):将字符串参数转换为对应的double基本类型。

方式2:字符串转为包装类,然后可以自动拆箱为基本数据类型

  • public static Integer valueOf(String s):将字符串参数转换为对应的Integer包装类,然后可以自动拆箱为int基本类型
  • public static Long valueOf(String s):将字符串参数转换为对应的Long包装类,然后可以自动拆箱为long基本类型
  • public static Double valueOf(String s):将字符串参数转换为对应的Double包装类,然后可以自动拆箱为double基本类型

注意:如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出java.lang.NumberFormatException异常。

方式3:通过包装类的构造器实现

1
2
3
4
5
6
7
8
9
int a = Integer.parseInt("整数的字符串");
double d = Double.parseDouble("小数的字符串");
boolean b = Boolean.parseBoolean("true或false");

int a = Integer.valueOf("整数的字符串");
double d = Double.valueOf("小数的字符串");
boolean b = Boolean.valueOf("true或false");

int i = new Integer(“12”);

其他方式小结:

image-20220813012801907

11.6 包装类的其它API

11.6.1 数据类型的最大最小值

1
2
3
4
5
Integer.MAX_VALUE和Integer.MIN_VALUE

Long.MAX_VALUE和Long.MIN_VALUE

Double.MAX_VALUE和Double.MIN_VALUE

11.6.2 字符转大小写

1
2
3
Character.toUpperCase('x');

Character.toLowerCase('X');

11.6.3 整数转进制

1
2
3
4
5
Integer.toBinaryString(int i) 

Integer.toHexString(int i)

Integer.toOctalString(int i)

11.6.4 比较的方法

1
2
3
Double.compare(double d1, double d2)

Integer.compare(int x, int y)

11.7 包装类对象的特点

11.7.1 包装类缓存对象

包装类 缓存对象
Byte -128~127
Short -128~127
Integer -128~127
Long -128~127
Float 没有
Double 没有
Character 0~127
Boolean true和false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Integer a = 1;
Integer b = 1;
System.out.println(a == b);//true

Integer i = 128;
Integer j = 128;
System.out.println(i == j);//false

Integer m = new Integer(1);//新new的在堆中
Integer n = 1;//这个用的是缓冲的常量对象,在方法区
System.out.println(m == n);//false

Integer x = new Integer(1);//新new的在堆中
Integer y = new Integer(1);//另一个新new的在堆中
System.out.println(x == y);//false
1
2
3
Double d1 = 1.0;
Double d2 = 1.0;
System.out.println(d1==d2);//false 比较地址,没有缓存对象,每一个都是新new的

11.7.2 类型转换问题

1
2
3
Integer i = 1000;  //先自动拆箱为int  然后自动类型转换为double
double j = 1000;
System.out.println(i==j);//true 会先将i自动拆箱为int,然后根据基本数据类型“自动类型转换”规则,转为double比较
1
2
3
Integer i = 1000;  //先自动拆箱为int
int j = 1000;
System.out.println(i==j);//true 会自动拆箱,按照基本数据类型进行比较
1
2
3
Integer i = 1;  //先自动拆箱为int
Double d = 1.0 //没写;
System.out.println(i==d);//编译报错

11.7.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
public class TestExam {
public static void main(String[] args) {
int i = 1;
Integer j = new Integer(2);
Circle c = new Circle();
change(i,j,c);
System.out.println("i = " + i);//1
System.out.println("j = " + j);//2
System.out.println("c.radius = " + c.radius);//10.0
}

/*
* 方法的参数传递机制:
* (1)基本数据类型:形参的修改完全不影响实参
* (2)引用数据类型:通过形参修改对象的属性值,会影响实参的属性值
* 这类Integer等包装类对象是“不可变”对象,即一旦修改,就是新对象,和实参就无关了
*/
public static void change(int a ,Integer b,Circle c ){
a += 10;
// b += 10;//等价于 b = new Integer(b+10);
c.radius += 10;
/*c = new Circle();
c.radius+=10;*/
}
}
class Circle{
double radius;
}

11.8 练习

笔试题:如下两个题目输出结果相同吗?各是什么。

1
2
Object o1 = true ? new Integer(1) : new Double(2.0);  //在创建的时候要进行统一编译 同时自动类型转换到1.0和2.0
System.out.println(o1);//1.0 相当于创建两个包装类对象
1
2
3
4
5
6
Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
System.out.println(o2); //输出1

面试题:

1
2
3
4
5
6
7
8
9
10
11
12
13
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i == j); //输出false

Integer m = 1;
Integer n = 1;
System.out.println(m == n); //输出true

// Integer内部定义了一个IntegerCache结构,IntegerCache中定义Integer[],保存-128-127范围内的整数。
// 如果我们使用自动装箱的方式【Integer i=xxx; xxx属于(-128,127]】,就可以直接使用数组中的元素。
Integer x = 128; //128相当于重新new了一个
Integer y = 128; //128相当于重新new了一个
System.out.println(x == y);//输出false

练习:

利用Vector代替数组处理:从键盘读入学生成绩(以负数代表输入结束),找出最高分,并输出学生成绩等级。

  • 提示:数组一旦创建,长度就固定不变,所以在创建数组前就需要知道它的长度。而向量类java.util.Vector可以根据需要动态伸缩。

  • 创建Vector对象:Vector v=new Vector();

  • 给向量添加元素:v.addElement(Object obj); //obj必须是对象

  • 取出向量中的元素:Object obj=v.elementAt(0);

    • 注意第一个元素的下标是0,返回值是Object类型的。
  • 计算向量的长度:v.size();

  • 若与最高分相差10分内:A等;20分内:B等;30分内:C等;其它:D等

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
import java.util.Scanner;
import java.util.Vector;
public class WrapperTest3 {
public static void main(String[] args) {
Vector vector=new Vector();
Scanner input=new Scanner(System.in);
for(;;){
double x=input.nextDouble();
if(x<0){
break;
}else{
vector.addElement(x); //添加向量
}
}
System.out.println("向量长度:"+vector.size());
Vector vector1=new Vector();
double max=Integer.MIN_VALUE; //寻找最大值
for(int i=0;i<vector.size();i++){
Object obj=vector.elementAt(i); //取出当前值
vector1.add(obj); //到时候判断用
double temp= (double) obj; //转换为double值
max=(max>temp?max:temp); //替换max
}
System.out.println("最大值:"+max);
for(int i=0;i<vector1.size();i++){
double cha=max-(double)vector1.elementAt(i); //要统一为double
if(cha<10){
System.out.println("A");
}else if(cha<20){
System.out.println("B");
}else if(cha<30){
System.out.println("C");
}else{
System.out.println("D");
}
}

}
}

最终结果如下:
image-20231021185053363

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 第06章_面向对象编程(基础)
    1. 1.1. 1. 面向对象编程概述(了解)
      1. 1.1.1. 1.1 程序设计的思路
      2. 1.1.2. 1.2 由实际问题考虑如何设计程序
    2. 1.2. 2. Java语言的基本元素:类和对象
      1. 1.2.1. 2.1 引入
      2. 1.2.2. 2.2 类和对象概述
      3. 1.2.3. 2.3 类的成员概述
      4. 1.2.4. 2.4 面向对象完成功能的三步骤(重要)
        1. 1.2.4.1. 步骤1:类的定义
        2. 1.2.4.2. 步骤2:对象的创建
        3. 1.2.4.3. 步骤3:对象调用属性或方法
      5. 1.2.5. 2.5 匿名对象 (anonymous object)
    3. 1.3. 3. 对象的内存解析
      1. 1.3.1. 3.1 JVM内存结构划分
      2. 1.3.2. 3.2 对象内存解析
      3. 1.3.3. 3.3 练习
    4. 1.4. 4. 类的成员之一:成员变量(field)
      1. 1.4.1. 4.1 如何声明成员变量
      2. 1.4.2. 4.2 成员变量 vs 局部变量
    5. 1.5. 1. 类的成员之二:方法(method)
      1. 1.5.1. 5.1 方法的引入
      2. 1.5.2. 5.2 方法(method、函数)的理解
      3. 1.5.3. 5.3 如何声明方法
      4. 1.5.4. 5.4 如何调用实例方法
      5. 1.5.5. 5.5 使用的注意点
      6. 1.5.6. 5.6 关键字return的使用
      7. 1.5.7. 5.7 方法调用内存分析
      8. 1.5.8. 5.8 练习
    6. 1.6. 6. 对象数组
    7. 1.7. 7. 再谈方法
      1. 1.7.1. 7.1 方法的重载(overload)
        1. 1.7.1.1. 7.1.1 概念及特点
        2. 1.7.1.2. 7.1.2 示例
        3. 1.7.1.3. 7.1.3 练习
      2. 1.7.2. 7.2 可变个数的形参
      3. 1.7.3. 7.3 方法的参数传递机制
        1. 1.7.3.1. 7.3.1 形参和实参
        2. 1.7.3.2. 7.3.2 参数传递机制:值传递
        3. 1.7.3.3. 7.3.3 举例
        4. 1.7.3.4. 7.3.4 练习
      4. 1.7.4. 7.4 递归(recursion)方法
    8. 1.8. 8. 关键字:package、import
      1. 1.8.1. 8.1 package(包)
        1. 1.8.1.1. 8.1.1 语法格式
        2. 1.8.1.2. 8.1.2 包的作用
        3. 1.8.1.3. 8.1.3 应用举例
        4. 1.8.1.4. 8.1.4 JDK中主要的包介绍
      2. 1.8.2. 8.2 import(导入)
        1. 1.8.2.1. 8.2.1 语法格式
        2. 1.8.2.2. 8.2.2 应用举例
        3. 1.8.2.3. 8.2.3 注意事项
    9. 1.9. 9. 面向对象特征一:封装性(encapsulation)
      1. 1.9.1. 9.1 为什么需要封装?
      2. 1.9.2. 9.2 何为封装性?
      3. 1.9.3. 9.3 Java如何实现数据封装
      4. 1.9.4. 9.4 封装性的体现
        1. 1.9.4.1. 9.4.1 成员变量/属性私有化
        2. 1.9.4.2. 9.4.2 私有化方法
      5. 1.9.5. 9.5 练习
    10. 1.10. 10. 类的成员之三:构造器(Constructor)
      1. 1.10.1. 10.1 构造器的作用
      2. 1.10.2. 10.2 构造器的语法格式
      3. 1.10.3. 10.3 使用说明
      4. 1.10.4. 10.4 练习
    11. 1.11. 11. 阶段性知识补充
      1. 1.11.1. 11.1 类中属性赋值过程
      2. 1.11.2. 11.2 JavaBean
      3. 1.11.3. 11.3 UML类图
  2. 2. 第07章_面向对象编程(进阶)
    1. 2.1. 本章专题与脉络
    2. 2.2. 1. 关键字:this
      1. 2.2.1. 1.1 this是什么?
      2. 2.2.2. 1.2 什么时候使用this
        1. 2.2.2.1. 1.2.1 实例方法/构造器中—使用当前对象的成员(区分成员变量和传入的局部变量)
        2. 2.2.2.2. 1.2.2 同一个类中—-构造器互相调用(减少重复代码)
      3. 2.2.3. 1.3 练习
    3. 2.3. 2. 面向对象特征二:继承(Inheritance)
      1. 2.3.1. 2.1 继承的概述
        1. 2.3.1.1. 2.1.1 继承的好处
      2. 2.3.2. 2.2 继承的语法
        1. 2.3.2.1. 2.2.1 继承中的语法格式
        2. 2.3.2.2. 2.2.2 继承中的基本概念
      3. 2.3.3. 2.3 代码举例
      4. 2.3.4. 2.4 继承性的细节说明
      5. 2.3.5. 2.5 练习
    4. 2.4. 3. 方法的重写(override/overwrite)
      1. 2.4.1. 3.1 方法重写举例
      2. 2.4.2. 3.2 方法重写的要求
      3. 2.4.3. 3.3 小结:方法的重载与重写
      4. 2.4.4. 3.4 练习
    5. 2.5. 4. 再谈封装性中的4种权限修饰
    6. 2.6. 5. 关键字:super
      1. 2.6.1. 5.1 super的理解
      2. 2.6.2. 5.2 super的使用场景
        1. 2.6.2.1. 5.2.1 子类中调用父类被重写的方法
        2. 2.6.2.2. 5.2.2 子类中调用父类中同名的成员变量
        3. 2.6.2.3. 5.2.3 子类构造器中调用父类构造器
      3. 2.6.3. 5.3 小结:this与super
      4. 2.6.4. 5.4 练习
    7. 2.7. 6. 子类对象实例化全过程
    8. 2.8. 7. 面向对象特征三:多态性
      1. 2.8.1. 7.1 多态的形式和体现
        1. 2.8.1.1. 7.1.1 对象的多态性
        2. 2.8.1.2. 7.1.2 多态的理解
        3. 2.8.1.3. 7.1.3 举例
      2. 2.8.2. 7.2 为什么需要多态性(polymorphism)?
      3. 2.8.3. 7.3 多态的好处和弊端
      4. 2.8.4. 7.4 虚方法调用(Virtual Method Invocation)
      5. 2.8.5. 7.5 成员变量没有多态性
      6. 2.8.6. 7.6 向上转型与向下转型
        1. 2.8.6.1. 7.6.1 为什么要类型转换
        2. 2.8.6.2. 7.6.2 如何向上或向下转型
        3. 2.8.6.3. 7.6.3 instanceof关键字
      7. 2.8.7. 7.7 练习
    9. 2.9. 8. Object 类
      1. 2.9.1. 8.1 如何理解根父类
      2. 2.9.2. 8.2 Object类的方法
        1. 2.9.2.1. 1、(重点)equals()
        2. 2.9.2.2. 2、(重点)toString()
        3. 2.9.2.3. 3、clone()
        4. 2.9.2.4. 4、finalize()
        5. 2.9.2.5. 5、getClass()
        6. 2.9.2.6. 6、hashCode()
      3. 2.9.3. 8.3 native关键字的理解
  3. 3. 第08章_面向对象编程(高级)
    1. 3.1. 本章专题与脉络
    2. 3.2. 1. 关键字:static
      1. 3.2.1. 1.1 类属性、类方法的设计思想
      2. 3.2.2. 1.2 static关键字
      3. 3.2.3. 1.3 静态变量
        1. 3.2.3.1. 1.3.1 语法格式
        2. 3.2.3.2. 1.3.2 静态变量的特点
        3. 3.2.3.3. 1.3.3 举例
        4. 3.2.3.4. 1.3.4 内存解析
      4. 3.2.4. 1.4 静态方法
        1. 3.2.4.1. 1.4.1 语法格式
        2. 3.2.4.2. 1.4.2 静态方法的特点
        3. 3.2.4.3. 1.4.3 举例
      5. 3.2.5. 1.5 练习
    3. 3.3. 2. 单例(Singleton)设计模式
      1. 3.3.1. 2.1 设计模式概述
      2. 3.3.2. 2.2 何为单例模式
      3. 3.3.3. 2.3 实现思路
      4. 3.3.4. 2.4 单例模式的两种实现方式
        1. 3.3.4.1. 2.4.1 饿汉式
        2. 3.3.4.2. 2.4.2 懒汉式
        3. 3.3.4.3. 2.4.3 饿汉式 vs 懒汉式
      5. 3.3.5. 2.5 单例模式的优点及应用场景
    4. 3.4. 3. 理解main方法的语法
    5. 3.5. 4. 类的成员之四:代码块
      1. 3.5.1. 4.1 静态代码块
        1. 3.5.1.1. 4.1.1 语法格式
        2. 3.5.1.2. 4.1.2 静态代码块的特点
      2. 3.5.2. 4.2 非静态代码块
        1. 3.5.2.1. 4.2.1 语法格式
        2. 3.5.2.2. 4.2.2 非静态代码块的作用
        3. 3.5.2.3. 4.2.3 非静态代码块的意义
        4. 3.5.2.4. 4.2.4 非静态代码块的执行特点
      3. 3.5.3. 4.3 举例
      4. 3.5.4. 4.4 小结:实例变量赋值顺序
      5. 3.5.5. 4.5 练习
    6. 3.6. 5. final关键字
      1. 3.6.1. 5.1 final的意义
      2. 3.6.2. 5.2 final的使用
        1. 3.6.2.1. 5.2.1 final修饰类
        2. 3.6.2.2. 5.2.2 final修饰方法
        3. 3.6.2.3. 5.2.3 final修饰变量
      3. 3.6.3. 5.3 笔试题
    7. 3.7. 6. 抽象类与抽象方法(或abstract关键字)
      1. 3.7.1. 6.1 由来
      2. 3.7.2. 6.2 语法格式
      3. 3.7.3. 6.3 使用说明
      4. 3.7.4. 6.4 注意事项
      5. 3.7.5. 6.5 应用举例1
      6. 3.7.6. 6.6 应用举例2:模板方法设计模式(TemplateMethod)
      7. 3.7.7. 6.7 思考与练习
    8. 3.8. 7. 接口(interface)
      1. 3.8.1. 7.1 类比
      2. 3.8.2. 7.2 概述
      3. 3.8.3. 7.3 定义格式
        1. 3.8.3.1. 7.3.1 接口的声明格式
        2. 3.8.3.2. 7.3.2 接口的成员说明
      4. 3.8.4. 7.4 接口的使用规则
      5. 3.8.5. 7.5 JDK8中相关冲突问题
        1. 3.8.5.1. 7.5.1 默认方法冲突问题
        2. 3.8.5.2. 7.5.2 常量冲突问题
      6. 3.8.6. 7.6 接口的总结与面试题
      7. 3.8.7. 7.7 接口与抽象类之间的对比
      8. 3.8.8. 7.8 练习
    9. 3.9. 8. 内部类(InnerClass)
      1. 3.9.1. 8.1 概述
        1. 3.9.1.1. 8.1.1 什么是内部类
        2. 3.9.1.2. 8.1.2 为什么要声明内部类呢
        3. 3.9.1.3. 8.1.3 内部类的分类
      2. 3.9.2. 8.2 成员内部类
        1. 3.9.2.1. 8.2.1 概述
        2. 3.9.2.2. 8.2.2 创建成员内部类对象
        3. 3.9.2.3. 8.2.3 举例
      3. 3.9.3. 8.3 局部内部类
        1. 3.9.3.1. 8.3.1 非匿名局部内部类
        2. 3.9.3.2. 8.3.2 匿名内部类
      4. 3.9.4. 8.4 练习
    10. 3.10. 9. 枚举类
      1. 3.10.1. 9.1 概述
      2. 3.10.2. 9.2 定义枚举类(JDK5.0 之前)
      3. 3.10.3. 9.3 定义枚举类(JDK5.0 之后)
        1. 3.10.3.1. 9.3.1 enum关键字声明枚举
        2. 3.10.3.2. 9.3.2 enum方式定义的要求和特点
      4. 3.10.4. 9.4 enum中常用方法
      5. 3.10.5. 9.5 实现接口的枚举类
    11. 3.11. 10. 注解(Annotation)
      1. 3.11.1. 10.1 注解概述
        1. 3.11.1.1. 10.1.1 什么是注解
        2. 3.11.1.2. 10.1.2 注解与注释
        3. 3.11.1.3. 10.1.3 注解的重要性
      2. 3.11.2. 10.2 常见的Annotation作用
      3. 3.11.3. 10.3 三个最基本的注解
        1. 3.11.3.1. 10.3.1 @Override
        2. 3.11.3.2. 10.3.2 @Deprecated
        3. 3.11.3.3. 10.3.3 @SuppressWarnings
      4. 3.11.4. 10.4 元注解
      5. 3.11.5. 10.5 自定义注解的使用
        1. 3.11.5.1. 10.5.1 声明自定义注解
        2. 3.11.5.2. 10.5.2 使用自定义注解
        3. 3.11.5.3. 10.5.3 读取和处理自定义注解
      6. 3.11.6. 10.6 JUnit单元测试
        1. 3.11.6.1. 10.6.1 测试分类
        2. 3.11.6.2. 10.6.2 JUnit单元测试介绍
        3. 3.11.6.3. 10.6.3 引入本地JUnit.jar
        4. 3.11.6.4. 10.6.4 编写和运行@Test单元测试方法
        5. 3.11.6.5. 10.6.5 设置执行JUnit用例时支持控制台输入
        6. 3.11.6.6. 10.6.6 定义test测试方法模板
    12. 3.12. 11. 包装类
      1. 3.12.1. 11.1 为什么需要包装类
      2. 3.12.2. 11.2 有哪些包装类
      3. 3.12.3. 11.3 自定义包装类
      4. 3.12.4. 11.4 包装类与基本数据类型间的转换
        1. 3.12.4.1. 11.4.1 装箱
        2. 3.12.4.2. 11.4.2 拆箱
      5. 3.12.5. 11.5 基本数据类型、包装类与字符串间的转换
      6. 3.12.6. 11.6 包装类的其它API
        1. 3.12.6.1. 11.6.1 数据类型的最大最小值
        2. 3.12.6.2. 11.6.2 字符转大小写
        3. 3.12.6.3. 11.6.3 整数转进制
        4. 3.12.6.4. 11.6.4 比较的方法
      7. 3.12.7. 11.7 包装类对象的特点
        1. 3.12.7.1. 11.7.1 包装类缓存对象
        2. 3.12.7.2. 11.7.2 类型转换问题
        3. 3.12.7.3. 11.7.3 包装类对象不可变
      8. 3.12.8. 11.8 练习
,