面向对象介绍

面向对象:并不是一个技术,而是一种编程的指导思想

以什么形式组织代码,以什么思路解决问题

为什么要学习面向对象?

因为生活中,我们要解决问题时,就是采用这种指导思想去解决的

所以,我们写程序去解决问题时,如果也能采用这种指导思想

就会使编程变得非常简单,程序也便于理解

面向对象,重点学什么?

  1. 学习自己如何设计对象
  2. 学习已有的对象如何使用

总结:面向对象是一种思想,这种思想可以让我们写代码的思路更贴切于生活

类和对象

  • Java中想要创建对象,必须先要有类的存在
  • 类指的是一组相关属性和行为的集合,可以将其理解为是一张对象的设计图
  • Java中需要根据类,创建对象
  • 一个类,可以创建出多个对象

类的组成

类的组成有两部分:属性和行为

属性又称成员变量:跟之前定义变量的格式一样,只不过位置需要放在方法外面。它用于描述事物的名词

行为又称成员方法:跟之前定义方法的格式一样,只不过需要去掉static关键字。它用于描述事物的动词

创建和使用对象

  1. 创建对象格式:类名 对象名 = new 类名();
  2. 成员变量的使用格式:对象名.变量名;
  3. 成员方法的使用格式:对象名.方法名(实际参数);

完整代码如下图所示:

Snipaste_2025-07-19_20-47-33

类和对象的内存图

单个对象

单个对象内存图

多个对象

两个对象内存图

  • 在一次程序的执行中,一个类创建了多个对象,也只会在方法存放一个对应的字节码
  • 每创建一个新的对象,就会在堆内存中开辟一个新的内存空间
  • 虽然成员变量是独立的,但是成员方法都是引用同一份

引用指向内存图

引用指向内存图

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

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

this关键字

当局部变量和成员变量出现了重名的情况,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
/* Student类 */
public class Student {
String name;
int age;

public void sayHello(String name) {
System.out.println(name);
}

public void showAge(int age) {
System.out.println(this.age);
}
}

/* 测试类 */
public class Test {
public static void main(String[] args) {
Student s = new Student();
s.name = "张三";
s.age = 23;
s.sayHello("李四"); // 输出结果为:李四
s.showAge(25); // 输出结果为:23
}
}

this的本质

  • this代表当前类对象的引用(地址)
  • 哪一个对象调用的方法,方法中的this就代表哪一个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Student类 */
public class Student {
String name;
int age;

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

/* 测试类 */
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
System.out.println(s1); // ,,,@b4c966a
s1.print(); // ,,,@b4c966a

Student s2 = new Student();
System.out.println(s2); // ,,,@2f4d3709
s2.print(); // ,,,@2f4d3709
}
}

this内存图

this内存图

构造方法

  • 构造器:初始化一个新建的对象,构建、创造对象的时候,所调用的方法
  • 格式:
    1. 方法名与类名相同,大小写也要一致
    2. 没有返回值类型,连void都没有
    3. 没有具体的返回值(不能由return带回结果数据)
  • 作用:
    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
public class Student{
String name;
int age;

// 空参构造
public Student() {
System.out.println("空参构造方法被调用了")
}

// 有参构造
public Student(String name,int age) {
this.name = name;
this.age = age;
System.out.println("有参构造方法被调用了")
}
}

// 测试类
public class Test{
public static void main(String[] args) {
Student s1 = new Student(); // 空参构造方法被调用了
Student s2 = new Student("张三", 23); // 有参构造方法被调用了
}
}

构造方法不能手动调用,如:s1.Student("李四", 25);是错误的

如果类中没有给出构造方法,系统会生成一个默认的无参构造方法

如果定义了构造方法,那么系统就不再会提供默认的构造方法

构造方法也是方法,运行重载关系出现

推荐使用方法:无参构造、有参构造都由用户手动定义

构造方法内存图如下:

构造方法内存图

封装

封装就是将数据(属性)操作数据的方法捆绑在一起

组成一个整体,也就是类

同时,通过访问修饰符来控制对类中属性和方法的访问权限,从而实现信息隐藏

封装的好处:

  • 更好的维护数据
  • 使用者无需关心内部实现,只要知道如何使用即可

权限修饰符

权限修饰符 同一个类中 同一个包中 不同的子类 不同包的无关类
private
(default)
protected
public

标准JavaBean

标准JavaBean的要求如下:

  1. 这类中的成员变量都要私有,并且对外提供Getter和Setter方法
  2. 类中提供无参、有参构造方法

示例:

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

public Teacher() {
}

public Teacher(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 void show() {
System.out.println(name + "老师的年龄是" + age);
}
}

Getter方法用于获取成员属性,Setter方法用于修改成员属性

Setter方法可以对输入的数据进行处理,保证数据的安全性

JavaBean又叫做实体类,实体类只负责数据的存取,而对数据的处理交给其他类来完成,以实现数据数据业务处理相分离

static关键字

  • static是静态的意思,可以修饰成员变量,也可以修饰成员方法
  • 被其修饰的成员,被该类的所有对象所共享
  • 多了一种调用方式,可以通过类名调用。如:Student.xxxStudent.xxx()
  • 随着类的加载而加载,优先于对象存在

static修饰成员属性

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

public Student() {
}

public Student(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 class Test {
public static void main(String[] args) {
Student.school = "苏州大学";
Student s1 = new Student("张三",23);
Student s2 = new Student("李四", 22);

System.out.println(s1.getName() + "---" + s1.getAge() + "---" + Student.school);
System.out.println(s2.getName() + "---" + s2.getAge() + "---" + Student.school);
// 张三---23---苏州大学
// 李四---22---苏州大学
}
}

static内存图

注意:随着类的加载而加载,优先于对象存在

static修饰符内存图

static修饰成员方法

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
// 数组工具类,查找数组中的最大最小值,输出数组的所有元素
public class ArrayTools {
public static int getMaxNum(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (max < arr[i]) {
max = arr[i];
}
}
return max;
}

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

public static void printArray(int[] arr) {
for (int n : arr) {
System.out.print(n + ",");
}
System.out.println();
}
}

// 测试类
import java.util.Random;

public class Test {
public static void main(String[] args) {
// 创建数组
Random r = new Random();
int[] arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = r.nextInt(-100,100);
}

ArrayTools.printArray(arr);
System.out.println("数组最大元素为:" + ArrayTools.getMaxNum(arr));
System.out.println("数组最小元素为:" + ArrayTools.getMinNum(arr));
// 66,-9,25,-75,-76,-44,-23,94,-35,40,
// 数组最大元素为:94
// 数组最小元素为:-76
}
}

上面的工具类实现了基础的功能,但还有优化的地方,优化后的代码如下:

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
package com.norlcyan._static;
/**
* 这是一个数组的工具类,内部提供了数组的便捷操作
* 求最大值,最小值,遍历打印数组
* @version 1.0
* @author Zhao
* */
public class ArrayTools {
// 防止创建本类对象
private ArrayTools() {};

/**
* 此方法可以从传入的数组中找到最大值并返回
* @param arr 需要求最大值的数组
* @return 返回找到的最大值
*/
public static int getMaxNum(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (max < arr[i]) {
max = arr[i];
}
}
return max;
}

/**
* 此方法可以从传入的数组中找到最小值并返回
* @param arr 需要求最小值的数组
* @return 返回找到的最小值
*/
public static int getMinNum(int[] arr) {
int min = arr[0];
for (int i = 1; i < arr.length; i++) {
if (min > arr[i]) {
min = arr[i];
}
}
return min;
}

/**
* 此方法遍历打印数组中的每一个元素
* @param arr 需要遍历打印的数组
*/
public static void printArray(int[] arr) {
for (int n : arr) {
System.out.print(n + ",");
}
System.out.println();
}
}

优化点1:私有化了构造方法,防止用户创建对象(因为该类中都为static方法,不需要对象就可以调用)

优化点2:使用文档注释,让工具类更加规范、易读

static注意事项

  1. static中只能访问静态成员(直接访问)
1
2
3
4
5
6
7
8
9
10
public class Test {

int a = 10;
static int b = 20;

public static void main(String[] args) {
System.out.println(a); // 报错,因为main方法用的static修饰
System.out.println(b); // 正确,使用了static修饰的变量可以访问
}
}
  1. static中没有this关键字
1
2
3
4
5
public class Test {
public static void main(String[] args) {
System.out.println(this); // 报错:... cannot be referenced from a static context
}
}

重新认识main方法

1
2
3
4
5
6
7
8
9
package com.norlcyan._static;

import java.util.Random;

public class Test {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
  • public:被JVM调用,访问权限足够大
  • static:被JVM调用,不用创建对象。因为main方法是静态的,所以测试类中其他方法也需要是静态的
  • void:被JVM调用,不需要给JVM返回值
  • main:一个通用的名称,虽然不是关键字,但是被JVM识别
  • String[] args:以前用于接收键盘录入数据的,现在不再使用,但作为传统保留下来了

文档注释使用

通过IDEA中的 Tools → Generate JavaDoc 生成

Snipaste_2025-07-21_15-35-42

Snipaste_2025-07-21_15-36-59

继承

  • 继承:让类与类之间产生关系(子父类关系),子类可以直接使用父类中非私有成员
  • 使用场景:当类与类之间,存在相同(共性)的内容,并且产生了is a的关系,就可以考虑使用继承,来优化代码
  • 继承的优势:提高代码复用性,维护性

继承的格式:

  • 格式:public class 子类名 extends 父类名 { }
  • 范例:public class Zi extends Fu { }
  • Fu:父类,也称为基类、超类
  • Zi:子类,也称为派生类

示例:

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
package com.norlcyan._extends;

public class ExtendsDemo01 {
public static void main(String[] args) {
Coder coder = new Coder();
coder.setName("张三");
coder.setAge(23);
coder.setSalary(6666.6);
System.out.println(coder.getName() + "---" + coder.getAge() + "---" + coder.getSalary());
}
}

class Employee {
private String name;
private int age;
private double salary;

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 double getSalary() {
return salary;
}

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

class Coder extends Employee{

}

一个文件中是可以存在多个类的,但是只能有一个公共类

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
package com.norlcyan._extends;

public class ExtendsDemo02 {
public static void main(String[] args) {
Zi z = new Zi();
z.show();
// 不使用Super:30
// 使用Super:10
}
}

class Fu {
int num = 10;
}

class Zi extends Fu {
int num = 30;

public void show() {
System.out.println("不使用Super:" + num);
System.out.println("使用Super:" + super.num);
}
}

重写成员方法

  • 在继承体系中,子类可以继承到父类的方法

    但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改

    这就需要采用方法的重写,方法重写又称方法覆盖

  • 子类重写父类方法,需要保证方法声明完全一致(方法名、参数、返回值类型需要保持一致)

  • 父类中私有方法不能被重写

  • 子类重写父类方法时,访问权限必须大于等于父类

示例:

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
package com.norlcyan._extends;

public class ExtendsDemo03 {
public static void main(String[] args) {
Son s = new Son();
s.saySomething();
// Hello World
// I am Son
}
}

class Father {

public void saySomething() {
System.out.println("I am Father");
}

public void sayHello() {
System.out.println("Hello World");
}
}

class Son extends Father{
@Override // Override注解:校验当前方法,是否是重写的方法
public void saySomething() {
super.sayHello();
System.out.println("I am Son");
}
}

Java中继承的特点

  • 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
28
29
30
public class ExtendsDemo04 {
public static void main(String[] args) {
C c = new C();
c.methodA();
c.methodB();
c.methodC();
}
}

class A {
public void methodA() {
System.out.println("I am A's Method");
}
}

class B extends A{
public void methodB() {
System.out.println("I am B's Method");
}
}

class C extends B{
public void methodC() {
System.out.println("I am C's Method");
}
}

class D extends C,B,A { // 错误,不支持多继承
...
}

构造方法

  • 子类在初始化之前,需要先完成父类的初始化
  • 系统会在子类的构造方法自动调用父类的构造方法,在子类所有的构造方法中,第一句话默认隐藏了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
package com.norlcyan._extends;

public class ExtendsDemo05 {
public static void main(String[] args) {
_Zi _zi1 = new _Zi();
// Fu 空参构造方法被调用了
// Zi 空参构造方法被调用了
System.out.println("--------------------");
_Zi _zi2 = new _Zi(10);
// Fu 空参构造方法被调用了
// Zi 带参构造方法被调用了
}
}

class _Fu {
public _Fu() {
System.out.println("Fu 空参构造方法被调用了");
}

public _Fu(int num) {
System.out.println("Fu 带参构造方法被调用了");
}
}

class _Zi extends _Fu{
public _Zi() {
System.out.println("Zi 空参构造方法被调用了");
}

public _Zi(int num) {
System.out.println("Zi 带参构造方法被调用了");
}
}

构造方法内存图

继承构造方法内存图

完整案例:

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
package com.norlcyan._extends;

public class ExtendsDemo06 {
public static void main(String[] args) {
Teacher teacher = new Teacher("张三", 30,10001);
System.out.println(teacher.getTeacherID() + "---" + teacher.getName() + "---" + teacher.getAge());

Student student = new Student();
student.setName("李四");
student.setAge(18);
student.setScore(99.5);
System.out.println(student.getName() + "---" + student.getAge() + "---" + student.getScore());
}
}

// 人类
class Person {
private String name;
private int age;

public Person() {
}

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

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

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

// 老师类
class Teacher extends Person{
private int teacherID;

public Teacher() {
}

public Teacher(int teacherID) {
this.teacherID = teacherID;
}

public Teacher(String name, int age, int teacherID) {
super(name, age);
this.teacherID = teacherID;
}

public int getTeacherID() {
return teacherID;
}

public void setTeacherID(int teacherID) {
this.teacherID = teacherID;
}
}

// 学生类
class Student extends Person {
private double score;

public Student() {
}

public Student(double score) {
this.score = score;
}

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

public double getScore() {
return score;
}

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

继承重要案例

1
2
3
4
程序员类 Coder:
成员变量:姓名,年龄,工资
成员方法:work方法,实现内容如下所示↓
姓名为张三,年龄为23,工资为15000的程序员正在编写代码
1
2
3
4
项目经理类 Manage:
成员变量:姓名,年龄,工资,奖金
成员方法:work方法,实现内容如下所示↓
姓名为李四,年龄为24,工资为18000,奖金为5000的项目经理正在分配任务...
1
2
3
员工类 Employee:
成员变量:姓名,年龄,工资
成员方法:work方法
  • 私有成员变量
  • 提供空参、有参构造方法
  • 提供Getter和Setter
  • 编写work方法

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
package com.norlcyan.extends_practice;

public class Employee {
private String name;
private int age;
private double salary;

public Employee() {
}

public Employee(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}

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 double getSalary() {
return salary;
}

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

public void work() {}
}

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
package com.norlcyan.extends_practice;

public class Manager extends Employee{
private double bonus;

public Manager() {
}

public Manager(String name, int age, double salary, double bonus) {
super(name, age, salary);
this.bonus = bonus;
}

public double getBonus() {
return bonus;
}

public void setBonus(double bonus) {
this.bonus = bonus;
}

@Override
public void work() {
System.out.println("姓名为" + super.getName() + ",年龄为" + super.getAge() + ",工资为" + super.getSalary() + ",奖金为" + this.bonus + "的项目经理正在分配任务...");
}
}

Coder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.norlcyan.extends_practice;

public class Coder extends Employee{
public Coder() {
}

public Coder(String name, int age, double salary) {
super(name, age, salary);
}

@Override
public void work() {
System.out.println("姓名为" + super.getName() + ",年龄为" + super.getAge() + ",工资为" + super.getSalary() + "的程序员正在编写代码");
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
package com.norlcyan.extends_practice;

public class Test {
public static void main(String[] args) {
Coder coder = new Coder("张三",23,15000);
coder.work();

Manager manager = new Manager("李四", 24, 18000, 5000);
manager.work();
}
}

this和super的区别

  • this:代表本类对象的引用(内存地址)
  • super:代表父类存储空间的标识
关键字 访问成员变量 访问成员方法 访问构造方法
this this.本类成员变量; this.本类成员方法(); 必须在构造方法第一行
super super.父类成员变量; super.父类成员方法(); 必须在构造方法第一行

如果调用的成员在子类中不存在,可以省略super不写

this()、super()二者都在争夺构造方法的第一行,所以二者不能共存

this调用构造方法示例:

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.norlcyan.thisAndSuper;

public class Demo01 {
public static void main(String[] args) {
A a1 = new A(1,2);
A a2 = new A(3,4,5);
}
}

class A {
int a;
int b;
int c; // 假设后期增加了一个成员属性

public A() {
}

public A(int a, int b) {
this.a = a;
this.b = b;
}

// 重新设计完整的带参构造函数,就可以省略部分代码
public A(int a, int b, int c) {
this(a,b);
this.c = c;
}
}

final关键字

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

final修饰的特点:

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

示例:

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.norlcyan._final;

public class FinalTest {
public static void main(String[] args) {
// 基本数据类型:数据值不可以被修改
final int NUM = 10;
// NUM = 100; 报错信息:Cannot assign a value to final variable 'NUM'
System.out.println(NUM); // 10

// 引用数据类型:地址值不可以被修改
final int[] arr1 = {1,2,3,4,5};
int[] arr2 = {10,20,30,40,50};
// arr1 = arr2; 报错信息:Cannot assign a value to final variable 'arr1'
arr1[0] = 11;
System.out.println(arr1[0]); // 11
}
}

class Fu {
public final void privacyMethod() {
System.out.println("隐私方法,不能被随意修改");
}
}

final class Zi extends Fu {
// public final void privacyMethod() {} 错误信息:overridden method is final
}

// class Sun extends Zi {} 错误信息:Cannot inherit from final Zi

final修饰基本数据类型时,数据值不能改变

final修饰引用数据类型时,地址值不能改变,但地址里面的内容是可以改变的

final修饰成员变量时,要么在定义的时候完成赋值,要么在构造方法之前完成赋值(也就是不存在默认值)。如下所示:

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
package com.norlcyan._final;

public class FinalTest {

}

class A {
final int num = 10;
}

class B {
final int num;

public B(int num) {
this.num = num;
}
}

class C {
final int num; // Variable 'num' might not have been initialized

public C() {};

public C(int num) {
this.num = num;
}
}

常量命名规范:如果是一个单词,所有字母大写。如果是多个单词,所有字母大写,单词之间用”_”分隔

抽象类

  • 抽象类是一种特殊的父类,内部可以编写抽象方法
  • 抽象方法:将共性的行为(方法)抽取到父类之后,发现该方法的实现逻辑无法在父类中给出具体明确,该方法就可以定义为抽象方法
  • 特点:当父类存在抽象方法,那么子类必须要将抽象方法重写,给出具体实现内容

示例:

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
package com.norlcyan.AbstractClass;

public class AbstractDemo01 {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat();
}
}

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

class Dog extends Animal{
// 子类必须要重写抽象方法,否则会报错
@Override
public void eat() {
System.out.println("狗吃肉");
}
}

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

注意事项:

  • 抽象类不能实例化
  • 抽象类存在构造方法
  • 抽象类中可以存在普通方法
  • 抽象类的子类
    • 要么重写抽象类中的所有抽象方法
    • 要么是抽象类

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

abstract关键字冲突问题

  • final:被abstract修饰的方法,强制要求子类重写,但被final修饰的方法子类不能重写
  • private:被abstract修饰的方法,强制要求子类重写,但被private修饰的方法不能重写
  • static:被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
package com.norlcyan.template;

public class Test {
public static void main(String[] args) {
Book b = new Book();
b.write();
}
}

abstract class TextTemplate {
// 将方法用final修饰,防止继承该类后重写此方法
public final void write() {
System.out.println("文章标题...");
body(); // 调用抽象方法
System.out.println("文章结尾...");
}

public abstract void body();
}

class Book extends TextTemplate{
@Override
public void body() {
System.out.println("这是文章的主题内容...");
}
}

接口

  • 接口:体现的思想是对规则的声明。Java中的接口更多体现的是对行为的抽象
  • 格式:public interface 接口名 {}
  • 接口不能实例化
  • 接口和类之间是实现关系,通过implements关键字表示:public class 类名 implements 接口名 {}
  • 接口的子类(实现类)要么重写接口中的所有方法,要么是抽象类

示例:

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
package _interface;

public class InterfaceDemo01 {
public static void main(String[] args) {
InterImpl ii = new InterImpl();
ii.method();
ii.show();
}
}

interface inter {
public abstract void show();
public abstract void method();
}

class InterImpl implements inter {

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

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

// 可以写成以下这种形式,虽然不会报错但是不推荐
abstract class InterImpl2 implements inter {

}

接口中的成员特点

成员变量特点:只能是常量,因为默认加入三个关键字:public static final,即使不手动添加也会存在

成员方法特点:只能是抽象方法,因为默认加入两个关键字:public abstract,即使不手动添加也会存在

构造方法:没有

示例:

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
package _interface;

public class InterfaceDemo02 {
public static void main(String[] args) {
System.out.println(MyInter.num);
// MyInter.num = 20; 报错信息:Cannot assign a value to final variable 'num'
MyInterImpl mii = new MyInterImpl();

mii.show();
}
}

interface MyInter {
int num = 10;
// private int num = 20; 报错信息:Modifier 'private' not allowed here

void show();
// public void test() {}; 报错信息:Interface abstract methods cannot have body
}

class MyInterImpl implements MyInter {
public MyInterImpl() {
super(); // 指向的是Object类
}

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

但是,在JDK8和JDK9中针对成员方法,引入了部分新特性,详细内容在后续章节会提到

类和接口之间的各种关系

  • 类和类的关系:继承关系,只能单继承,但是可以多层继承
  • 类和接口的关系:实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口。接口可以看做是一种特殊的抽象父类
  • 接口和接口之间的关系:继承关系,可以单继承,也可以多继承

类和接口:

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 _interface;

public class InterfaceDemo03 {
}

interface InterA {
void showA();
void test();
}

interface InterB {
void showB();
void test();
}

class Fu {
public void test() {
System.out.println("Extends Test");
}
}

class InterABImpl extends Fu implements InterA,InterB {

@Override
public void showA() {
System.out.println("Show A Method");
}

@Override
public void showB() {
System.out.println("Show B Method");
}
}

如果接口A和接口B有重名的方法,实现类中只需要重写一次即可(因为接口只是定义规则,不实现具体逻辑,所以不存在冲突

如果接口A、接口B、父类Fu有重名的方法,当实现类继承了Fu,那在实现类中不重写重名方法也没问题,就相当于父类完成了接口中方法的重写

接口和接口:

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
package _interface;

public class InterfaceDemo03 {
}

interface A {
void methodA();
void test();
}

interface B {
void methodB();
void test();
}

interface C extends A,B {
void methodC();
void test();
}

class CImpl implements C {

@Override
public void methodC() {

}

@Override
public void methodA() {

}

@Override
public void methodB() {

}

@Override
public void test() {

}
}

接口之间可以多继承,但是实现类要将继承的所有接口中的方法都实现

接口与接口继承时,即使出现重名方法也不会报错,只需要实现类中实现一次即可

抽象类和接口的对比

  • 成员变量:
    • 抽象类:可以定义变量,也可以定义常量
    • 接口:只能定义常量
  • 成员方法:
    • 抽象类:可以是定义具体方法,也可以定义抽象方法
    • 接口:只能定义抽象方法
  • 构造方法:
    • 抽象类:有
    • 接口:没有

抽象类:抽象类的本质还是在描述一个事物,只是有部分的方法无法直接说明,所以需要抽象方法

接口:接口可以为程序制定规则,代码更加规范。实现类实现接口中的方法,方法名不能修改,方法数量不能缺少。这样当后期维护、交接代码的时候,可以更加快速的上手。

接口新特性(接口升级问题)

JDK8的新特性:接口中可以定义有方法体的方法(修饰符必须为默认修饰符default静态修饰符static

JDK9的新特性:接口中可以定义私有方法(修饰符为私有修饰符private

1
2
3
interface Inter{}
class AInterImpl implements Inter {}
class BInterImpl implements Inter {}

假设最开始设计Inter接口时,接口中只有5个方法。但后期对接口进行丰富后,方法变为了10个,那么实现类就会全部报错。

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

作用:解决接口升级问题

default

接口中默认方法的定义格式:

  • 格式:public default 返回值类型 方法名(参数列表) {}
  • 示例:public default void show() {}
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
package _interface;

public class InterfaceDemo04 {
public static void main(String[] args) {
OrderServiceImpl o = new OrderServiceImpl();
o.create();
o.cancel();

o.paid();
o.delete();
}
}

interface OrderService {
// 假设以下两个方法是最初版本
void create();
void cancel();

// 后续添加了支付、删除功能,为了不影响实现类,可以用default修饰符
default void paid() {
System.out.println("支付订单");
}

default void delete() {
System.out.println("删除订单");
}

}

class OrderServiceImpl implements OrderService {

@Override
public void create() {
System.out.println("创建订单");
}

@Override
public void cancel() {
System.out.println("取消订单");
}
}

但是,由于在接口中定义了方法的具体实现逻辑,那就会导致多实现出问题(因为多实现中,方法不会给出具体实现逻辑,所以不会引起重名方法冲突问题)

解决方法就是在实现类中,重写重名的方法,可以选择根据哪个接口的方法进行继承,或者自己重写全部实现逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package _interface;

public class InterfaceDemo05 {
}

interface TestA {
default void show() {
System.out.println("default Show A Method");
}

default void test() {
System.out.println("default Test A Method");
}
}

interface TestB {
default void show() {
System.out.println("default Show B Method");
}

default void test() {
System.out.println("default Test BMethod");
}
}

class TestABImpl implements TestA, TestB {
@Override
public void show() {
TestA.super.show(); // 指定继承哪个接口的方法
}

@Override
public void test() {
System.out.println("My Test Method");
}
}

默认方法不是抽象方法,所以不强制被重写(但是可以被重写,重写的时候去掉default关键字)

public可以省略,default不能省略

如果实现了多个接口,多个接口中存在相同的方法声明,子类就必须对该方法进行重写

static

接口中允许定义static静态方法

接口中静态方法的定义格式:

  • 格式:public static 返回值类型 方法名(参数列表) {}
  • 示例:public static void show() {}
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 _interface;

public class InterfaceDemo06 {
public static void main(String[] args) {
P p = new P();
p.eat();
// p.show(); 报错信息:Static method may be invoked on containing interface class only

StaticInter.show();
}
}

interface StaticInter {
static void show() {
System.out.println("Interface Static Show Method");
}

void eat();
}

class P implements StaticInter{
@Override
public void eat() {
System.out.println("吃饭");
}
}

静态方法只能通过接口名调用,不能通过实现类名或者对象名调用

public可以省略,static不能省略

private

接口中允许定义private私有方法

接口中私有方法的定义格式:

  • 格式1:private 返回值类型 方法名(参数列表) {}
  • 示例:private void show() {}
  • 格式2:private static 返回值类型 方法名(参数列表) {}
  • 示例:private static void method() {}
  • 使用场景:当接口中的某个方法只希望被自己内部的默认方法或静态方法调用时,而外部不能允许调用时,就可以用private修饰符修饰
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
package _interface;

public class InterfaceDemo07 {
public static void main(String[] args) {
Logger l = new Logger("日志记录器A");
l.start();
l.end();
// start方法执行...
// 日志记录
// end方法执行...
// 日志记录
}
}

interface MyLogger {
default void start() {
System.out.println("start方法执行...");
log();
}

default void end() {
System.out.println("end方法执行...");
log();
}

private void log() {
System.out.println("日志记录");
}
}

class Logger implements MyLogger {
private String name;

public Logger(String name) {
this.name = name;
}

public String getName() {
return name;
}

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

多态

同一个行为具有多个不同表现形式或形态的能力

  • 对象多态:将方法的形参定义为父类类型,这个方法可以接收父类的任意子类对象
    • public static void useAnimal(Animal a);
    • 假设有个Cat类和Dog类继承了Animal类,那么调用方法的时候就可以useAnimal(new Dog())useAnimal(new Cat())
    • 更详细的案例代码可以看多态的好处和弊端这一章
  • 行为多态:同一个行为,具有多个不同表现形式或形态的能
    • 还是上面的案例,当方法体中调用了a.eat(),但是根据传入的不同对象能够呈现出不同的形式,这个就是行为多态

多态前提

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

示例:

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
package polymorphism;

public class PolymorphismDemo01 {
public static void main(String[] args) {
// 子类引用,指向子类对象
Zi z = new Zi();
System.out.println(z.num); // 20
z.show(); // Zi...Show...

// 父类引用,指向子类对象(以多态的形式创建对象)
Fu f = new Zi();
System.out.println(f.num); // 10
f.show(); // Zi...Show...
}
}

class Fu {
int num = 10;

public void show() {
System.out.println("Fu...Show...");
}
}

class Zi extends Fu {
int num = 20;

@Override
public void show() {
System.out.println("Zi...Show...");
}
}
  • 成员变量:**编译看左边(父类),运行看左边(父类)**
    • 因为是父类的引用,所以访问存在局限性,只能访问super空间中的数据
  • 成员方法:**编译看左边(父类),运行看右边(子类)**
    • 编译时检查方法在父类中是否存在
    • 不存在:编译出错
    • 存在:编译通过,但运行的时候一定会执行子类的方法逻辑。这是因为如果父类是抽象方法,那么调用父类中的方法没有意义,所以为了确保不出现这种情况,就将其设计为了执行子类方法
  • 静态成员:**编译看左边(父类),运行看左边(父类)**
    • static修饰的成员,推荐使用类名调用
    • 假设show是静态方法,那么f.show(); ===> 字节码文件中 ===> Fu.show();

成员变量内存图:

img_001

多态的好处和弊端

  • 多态的好处:提高了程序的扩展性

如果对多态不了解,一定要看懂以下示例代码

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
package polymorphism;

public class PolymorphismDemo02 {
public static void main(String[] args) {
useDog(new Dog()); // 参数:Dog d = new Dog();
useCat(new Cat()); // 参数:Cat c = new Cat();

useAnimal(new Dog()); // 参数:Animal a = new Dog();
useAnimal(new Cat()); // 参数:Animal a = new Cat();


}

public static void useDog(Dog d) {
d.eat();
}

public static void useCat(Cat c) {
c.eat();
}

public static void useAnimal(Animal a) {
a.eat();
}
}

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

class Dog extends Animal {

@Override
public void eat() {
System.out.println("狗吃肉");
}

public void watchHome() {
System.out.println("狗看家");
}
}

class Cat extends Animal {

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

public void catchMouse() {
System.out.println("猫抓老鼠");
}
}

方法的形参定义为父类类型,就可以传入该类的任意子类对象了,但方法中只能调用父类与子类的共性方法

多态转型

  • 多态的弊端:不能使用子类的特有成员
  • 向上转型:
    • 从子到父(父类引用指向子类对象)
    • Fu f = new Zi();
  • 向下转型:
    • 从父到子(父类引用所指向的对象,交给子类类型)
    • 当父类对象需要访问子类特有的方法子类的属性时,需要进行向下转型
    • Zi z = (Zi)f;

示例:

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
package polymorphism;

public class PolymorphismDemo02 {
public static void main(String[] args) {
useAnimal(new Dog());
useAnimal(new Cat());

}

public static void useAnimal(Animal a) {
a.eat();

if (a instanceof Dog) { // instanceof:判断左边的变量是否是右边的类型
Dog d = (Dog) a;
d.watchHome();
} else if (a instanceof Cat) {
Cat c = (Cat) a;
c.catchMouse();
}
}
}

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

class Dog extends Animal {

@Override
public void eat() {
System.out.println("狗吃肉");
}

public void watchHome() {
System.out.println("狗看家");
}
}

class Cat extends Animal {

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

public void catchMouse() {
System.out.println("猫抓老鼠");
}
}

如果被转的引用类型变量,对应的实际类型和目标类型不是同一种类型,那么在转换的时候就会出现ClassCastException

1
2
Animal a = new Dog();
Cat c = (Cat) a; // ClassCastException

可以使用instanceof关键字判断,格式:对象名 instanceof 类型

判断一个对象是否是一个类的实例,通俗理解就是判断关键字左边的对象是否是右边的类型,返回布尔类型结果

instanceof不能判断基本数据类型

综合案例

订单业务案例

订单业务接口代码:

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
package order;

/**
* 订单业务接口
*/
public interface OrderService {
/**
* 创建单个订单
*/
void create();

/**
* 查询单个订单
*/
void findOne();

/**
* 查询订单列表
*/
void findList();

/**
* 取消订单
*/
void cancel();

/**
* 完结订单
*/
void finish();

/**
* 支付订单
*/
void paid();
}

订单业务实现类:

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 order;

public class OrderServiceImpl implements OrderService{
@Override
public void create() {
System.out.println("创建单个订单");
}

@Override
public void findOne() {
System.out.println("查询单个订单");
}

@Override
public void findList() {
System.out.println("查询订单列表");
}

@Override
public void cancel() {
System.out.println("取消订单");
}

@Override
public void finish() {
System.out.println("完结订单");
}

@Override
public void paid() {
System.out.println("支付订单");
}
}

海外订单业务实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package order;

public class OverseasOrderServiceImpl implements OrderService{

public void check() {
System.out.println("IP地址检测");
}

@Override
public void create() {
System.out.println("国外业务---创建单个订单");
}

@Override
public void findOne() {
System.out.println("国外业务---查询单个订单");
}

@Override
public void findList() {
System.out.println("国外业务---查询订单列表");
}

@Override
public void cancel() {
System.out.println("国外业务---取消订单");
}

@Override
public void finish() {
System.out.println("国外业务---完结订单");
}

@Override
public void paid() {
System.out.println("国外业务---支付订单");
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package order;

import java.util.Scanner;

public class Test {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请选择业务范围:1. 国内业务 2. 国外业务");
int n = sc.nextInt();

OrderService orderService = null; // 将对象初始值置空

switch (n) {
case 1:
orderService = new OrderServiceImpl(); // 多态,将接口看做父类
break;
case 2:
orderService = new OverseasOrderServiceImpl();

// 调用特有方法
OverseasOrderServiceImpl overseasOrderService = (OverseasOrderServiceImpl) orderService;
overseasOrderService.check();
break;
}

if (orderService != null) {
orderService.create();
orderService.findOne();
orderService.findList();
orderService.cancel();
orderService.finish();
orderService.paid();
}
}
}

结构图:

img_002

模拟支付接口

需求:某网站需要开发一个支付功能,需要支持多种支付方法(支付平台支付、银行卡网银支付、信用卡快捷支付)

支付过程如下:

1
2
3
4
请选择支付方式:1、支付平台支付 2、银行卡网银支付 3、信用卡快捷支付
请输入您的支付方式:2
请输入您的支付金额:56.78
通过银行卡网银支付了:56.78元!

支付接口:

1
2
3
4
5
package payment;

public interface PaymentService {
void paid(double money);
}

支付平台支付实现类:

1
2
3
4
5
6
7
8
package payment;

public class PlatformPaymentServiceImpl implements PaymentService{
@Override
public void paid(double money) {
System.out.println("通过支付平台支付了:" + money + "元");
}
}

银行卡网银支付:

1
2
3
4
5
6
7
8
9
package payment;

public class BankcardPaymentServiceImpl implements PaymentService{

@Override
public void paid(double money) {
System.out.println("通过银行卡网银支付了:" + money + "元");
}
}

信用卡快捷支付:

1
2
3
4
5
6
7
8
9
package payment;

public class CreditcardPaymentServiceImpl implements PaymentService{

@Override
public void paid(double money) {
System.out.println("通过信用卡快捷支付了:" + money + "元");
}
}

测试类:

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 payment;

import java.util.Scanner;

public class Test {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请选择支付方式:1、支付平台支付 2、银行卡网银支付 3、信用卡快捷支付");
System.out.print("请输入您的支付方式:");
int choice = sc.nextInt();

PaymentService paymentService = switch (choice) {
case 1 -> new PlatformPaymentServiceImpl();
case 2 -> new BankcardPaymentServiceImpl();
case 3 -> new CreditcardPaymentServiceImpl();
default -> null;
};

if (paymentService != null) {
System.out.print("请输入您的支付金额:");
double money = sc.nextDouble();
paymentService.paid(money);
}

}
}

结构图:

img_003

Object类

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

Object类的常用方法:

方法名 说明
public String toString() 默认是返回当前对象在堆内存中的地址信息:类的全限名@内存地址
public boolean equals(Object o) 比较两个对象是否相同

toString()

  • 细节:System.out.println();打印对象的时候,源码中会自动调用该对象的toString()方法

  • 底层实现:

    1
    2
    3
    public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
    }
  • Object类的toString方法,源码:

    1
    2
    3
    4
    5
    // 对象的全类名@十六进制哈希值
    // 哈希值:对象的整数表示形式,常被人称作地址值
    public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
  • 推荐在类中重写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
42
43
44
45
46
package com.norlcyan.MyObject;

public class ObjectDemo {
public static void main(String[] args) {
Student student = new Student("张三",18);
System.out.println(student.toString());
System.out.println(student);
}
}

class Student {
private String name;
private int age;

public Student() {
}

public Student(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;
}

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

开发中直接输出对象,默认输出对象的地址其实是毫无意义的

开发中输出对象变量,更多的时候是希望看到对象的内容数据而不是对象的地址信息

equals()

  • 细节:

    • 当引用数据类型直接进行比较(==)时,比较的是地址值。也就说,即使两个对象中的所有属性都相同,比较的结果也是false
    • 调用equals方法对两个对象进行比较,如果类中没有重写的话,底层依旧是比较两个对象的地址值
  • Object类的equals方法实现:

    1
    2
    3
    public boolean equals(Object obj) {
    return (this == obj);
    }
  • 重写equals方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    package Pojo;

    import java.util.Objects;

    public class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(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;
    }

    @Override
    public boolean equals(Object obj) {
    // this:s1
    // obj:s2
    // return this.age == obj.age; 多态的弊端,父类不能访问子类的成员属性。这里Object obj = new Student();

    if (!(obj instanceof Student)) { // 防止传入非Student类
    return false;
    }

    Student sObj = (Student) obj; // 多态向下转型,使得sObj可以访问特有的属性
    return this.age == sObj.age && this.name.equals(sObj.name); // String是引用数据类型,但是底层已经完成了equals的重写
    }
    }

    注意:return this.age == obj.age;属于多态的弊端,父类不能访问子类的成员属性。这里Object obj = new Student();

IDEA重写equals()

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public boolean equals(Object o) {
if (this == o) return true; // 自己比较自己,肯定相等

// 传入对象为空 || 类与不同类比较,肯定不相等
if (o == null || getClass() != o.getClass()) return false;

Student student = (Student) o; // 向下转型
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}

Objects.equals(name, student.name),Objects类下的一个静态方法,注意不是Object而是Objects

当快速生成重写equals方法的代码时,会有一个选项提示是否生成hashCode()方法重写,推荐生成。这是为了与哈希集合(如 HashMapHashSet)的兼容性

示例:

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

@Override
public boolean equals(Object obj) {
// 自定义逻辑:根据 name 和 age 判断相等
}
}

Person p1 = new Person("Alice", 20);
Person p2 = new Person("Alice", 20);

System.out.println(p1.equals(p2)); // true(逻辑相等)
System.out.println(p1.hashCode() == p2.hashCode()); // false(默认使用 Object.hashCode())

此时,如果将 p1p2 放入 HashSet,它们会被视为两个不同的对象,导致集合中出现重复元素

Objects

Objects类与Object还是继承关系,Objects类是从JDK1.7开始之后才有的

Objects常见方法:

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

Objects.euqals底层实现:

1
2
3
4
5
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
// a == b,说明就是自己比较自己,结果一定是true
// a != null && a.equals(b),先进行非空判断,然后再调用传入对象的equals方法
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
package Pojo;

import java.util.Objects;

public class Test {
public static void main(String[] args) {
Student s1 = null;
Student s2 = new Student("张三", 18);
System.out.println(s1.equals(s2)); // 空指针异常
System.out.println(Objects.equals(s1,s2)); // false
}
}

当需要进行两个对象的相同值比较时,推荐使用Objects.equals(Obj o1,Obj o2),可以避免空指针异常

代码块

在Java类下,使用{ }括起来的代码被称为代码块

分类:

  • 局部代码块

    • 位置:方法中的一对{}

    • 作用:限定变量的生命周期,提早释放内存,提高内存利用率

    • 使用频率:很少

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class CodeBlock01 {
      public static void main(String[] args) {
      {
      int num = 10;
      System.out.println(num); // 10
      }

      System.out.println(num); // Cannot resolve symbol 'num'
      }
      }
  • 构造代码块

    • 位置:类中,方法外的一对{}

    • 特点:创建对象的时候,无论使用哪一个构造方法创建对象,都要执行代码块,并且优于构造方法执行

    • 作用:如果发现所有构造方法中,存在相同代码,就可以考虑将这段代码抽取到构造代码块

    • 使用频率:较少

    • 示例:

      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 CodeBlock01 {
      public static void main(String[] args) {
      Person p1 = new Person();
      // 构造代码块执行...
      // 空参构造方法执行...

      Person p2 = new Person(10);
      // 构造代码块执行...
      // 带参构造方法执行...
      }
      }

      class Person {
      {
      System.out.println("构造代码块执行...");
      }

      public Person() {
      System.out.println("空参构造方法执行...");
      }

      public Person(int n) {
      System.out.println("带参构造方法执行...");
      }
      }

      // 实际上的Person类:
      class Person {
      public Person() {
      System.out.println("构造代码块执行...");
      System.out.println("空参构造方法执行...");
      }

      public Person(int n) {
      System.out.println("构造代码块执行...");
      System.out.println("带参构造方法执行...");
      }
      }
  • 静态代码块

    • 位置:类中方法外的一对{}需要加入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
      public class CodeBlock01 {
      public static void main(String[] args) {
      Person p1 = new Person();
      // 静态代码块执行...
      // 构造代码块执行...
      // 空参构造方法执行...

      Person p2 = new Person(10);
      // 构造代码块执行...
      // 带参构造方法执行...
      }
      }

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

      {
      System.out.println("构造代码块执行...");
      }

      public Person() {
      System.out.println("空参构造方法执行...");
      }

      public Person(int n) {
      System.out.println("带参构造方法执行...");
      }
      }
  • 同步代码块:在多线程一章讲解

内部类

  • 内部类就是定义在一个类里面的类
1
2
3
4
5
class Outer {
class Inner {

}
}
  • 内部类分类:
    • 成员内部类
    • 静态内部类
    • 局部内部类
    • 匿名内部类
  • 内部类的优势:封装性更好,但是使用起来较为繁琐,建议少用

成员内部类

  • 创建内部类对象格式:
    • 格式:外部类名.内部类名 对象名 = new 外部类对象().new 内部类对象();
    • 示例:Outer.Inner in = new Outer().new Inner();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InnerDemo01 {
public static void main(String[] args) {
Outer.Inner oi = new Outer().new Inner();
System.out.println(oi.a);
oi.show();
}
}

class Outer {
class Inner {
int a = 10;

public void show() {
System.out.println("Inner Class Show");
}
}
}
  • 成员访问特点
    • 内部类访问外部类:所有成员都可以直接访问,包括私有成员
    • 外部了访问内部类:需要创建内部类对象才能访问
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
package innerClass;

public class InnerDemo01 {
public static void main(String[] args) {
Outer.Inner oi = new Outer().new Inner();
oi.innerPrint();

Outer o = new Outer();
o.outerPrint();
}
}

class Outer {
public int outerNum = 20;

public void outerShow() {
System.out.println("Outer Class Show");
}

public void outerPrint() {
// System.out.println(innerNum);
Inner i = new Inner();
System.out.println(i.innerNum); // 10 访问内部类成员属性
i.innerShow(); // Inner Class Show 访问内部类成员方法
}

class Inner {
public int innerNum = 10;

public void innerShow() {
System.out.println("Inner Class Show");
}

public void innerPrint() {
System.out.println(outerNum); // 20 访问外部类成员属性
outerShow(); // Outer Class Show 访问外部类成员方法
}
}
}

当外部类和内部类出现了重名属性或方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package innerClass;

public class InnerDemo01 {
public static void main(String[] args) {
Outer.Inner oi = new Outer().new Inner();
oi.show();
}
}

class Outer {
int num = 150;

class Inner {
int num = 110;

public void show() {
int num = 70;
System.out.println(num); // 70
System.out.println(this.num); // 110
System.out.println(Outer.this.num); // 150
}
}
}

静态内部类

  • 有static修饰的成员内部类

  • 定义格式:

    1
    2
    3
    4
    5
    class Outer {
    static class Inner {

    }
    }
  • 创建静态内部类对象格式:

    • 格式:外部类名.内部类名 对象名 = new 外部类名.内部类对象();
    • 示例:Outer.Inner in = new Outer.Inner();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class InnerDemo01 {
public static void main(String[] args) {
Outer.Inner oi = new Outer.Inner();
oi.show();

Outer.Inner.method();
}
}

class Outer {
static class Inner {
public void show() {
System.out.println("Inner Show");
}

public static void method() {
System.out.println("Inner Method");
}
}
}

局部内部类

  • 定义在方法、代码块、构造器等执行体中的类,没啥用,了解即可

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class InnerDemo01 {
public static void main(String[] args) {
Outer o = new Outer();
o.show();
}
}

class Outer {
public void show() {
class B {
public void print() {
System.out.println("Print...");
}
}

// 只能在此处创建对象
B b = new B();
b.print();
}
}

匿名内部类(重点)

首先,先观察以下代码:

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
package innerClass;

public class InnerDemo02 {
/*
问题:调用方法时,方法的形参是接口,那传入的应该是什么?
回答:需要传入这个接口的实现类

复盘:调用useInter方法
1. 编写实现类,实现接口
2. 重写方法
3. 创建实现类对象,将对象作为参数传入
*/
public static void main(String[] args) {
useInter(new InterImpl());
}

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

interface Inter {
void show();
}

class InterImpl implements Inter {

@Override
public void show() {
System.out.println("实现类重写后的show方法...");
}
}

可以发现,如果方法的参数为接口,那么调用方法时还需要专门创建一个实现类,然后再将该类对象作为参数才能调用

概述:匿名内部类本质上是一个特殊的局部内部类(定义在方法内部)

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

格式:

1
2
3
new 类名/接口 () {	
...
}

new 类名() {} ==> 继承这个类,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package innerClass;

public class Test {
public static void main(String[] args) {
new Fu() {
@Override
public void show() {
System.out.println("继承后的Show方法");
}
}.show(); // 继承后的Show方法
}
}

class Fu {
public void show() {
System.out.println("Fu Show方法");
}
}

new 接口() {} ==> 实现这个接口,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package innerClass;

public class InnerDemo02 {
public static void main(String[] args) {
useInter(new Inter() {
@Override
public void show() {
System.out.println("实现类重写后的show方法...");
}
});
}

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

interface Inter {
void show();
}

当接口中只有一个方法时,推荐使用匿名内部类,但是当存在多个方法,还是推荐创建实现类的方式

注意:

  1. 在匿名内部类中修改外部变量时,Java 要求该变量必须是 final 或等效不可变的在匿名内部类中修改外部变量时,Java 要求该变量必须是 final 或等效不可变的

  • 包本质来说就是文件夹,用来管理类文件的
  • 建包的语法格式:package 公司域名倒写.技术名称。包名建议全部英文小写,且具备意义
  • 建包语句必须在第一行,一般IDEA工具会帮助创建

导包

  • 相同包下的类可以直接方法,但是不同包下的类必须导包,才可以使用!
  • 如果导入的类在java.lang包(核心包),就不需要编写import导包代码
  • 导入具体包下的类,格式:import 包名.类名;
  • 导入包下的所有类,格式:import 包名.*;

一个类中,需要使用不同的类,但是这两个类名是相同的

默认只能导入一个,另一个需要带包访问:

1
2
3
4
5
6
7
8
9
// 假设当前包下有一个自定义的Scanner类
import java.util.*;
public class PackageDemo {
public static void main(String[] args) {
Scanner scanner = new Scanner(); // 创建的是当前包目录下Scanner类的对象

java.util.Scanner sc = new java.util.Scanner(System.in); // 创建的是java.util包下Scanner类的对象
}
}

Lambda表达式

  • Lambda表达式是JDK8开始后的一种新语法形式
  • 作用:简化匿名内部类的代码写法
  • 格式:
1
() -> {}
  • 详细说明:
1
2
3
4
(匿名内部类被重写方法的形参列表) -> {
被重写方法的方法体代码
}
注:-> 是语法形式,无实际意义

注意:Lambda表达式只能简化函数式接口的匿名内部类的写法形式

函数式接口

  • 首先必须是接口,其次接口中有且仅有一个抽象方法的形式
  • 通常我们会在接口上加一个@FunctionalInterface注解,标记该接口必须是满足函数式接口的

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FunctionalInterface
interface A {
void showA();
}

interface B {
void showB();
}


interface C {
void showC();
void test();
}

A、B是函数式接口,C因为有一个以上的方法,所以只是普通接口

完整代码1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package lambda;

public class LambdaTest01 {
public static void main(String[] args) {
useShowHandler(() -> {
System.out.println("Lambda表达式");
});
}

public static void useShowHandler(ShowHandler showHandler) {
showHandler.show();
}
}

@FunctionalInterface
interface ShowHandler {
void show();
}

完整代码2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package lambda;

public class LambdaTest02 {
public static void main(String[] args) {
useStringHandler((String msg) -> {
System.out.println(msg + " Lambda"); // stringHandler Lambda
});
}

public static void useStringHandler(StringHandler stringHandler) {
stringHandler.printMessage("stringHandler");
}
}

@FunctionalInterface
interface StringHandler {
void printMessage(String msg);
}

完整代码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
package lambda;

import java.util.Random;

public class LambdaTest03 {
public static void main(String[] args) {
int num1 = getRandomNumber(() -> {
return new Random().nextInt(100) + 1;
});

// Lambda可以简写
int num2 = getRandomNumber(() -> new Random().nextInt(100) + 1);

System.out.println(num1);
System.out.println(num2);
}

public static int getRandomNumber(RandomNumberHandler randomNumberHandler) {
return randomNumberHandler.getNum();
}
}

@FunctionalInterface
interface RandomNumberHandler {
int getNum();
}

Lambda表达式简写

  • 参数类型可以省略不写

  • 如果只有一个参数,参数类型可以省略,同时()也可以省略

  • 如果Lambda表达式的方法体代码只有一行代码

    可以省略大括号不写,同时要省略分号

    此时,如果这行代码是return语句,必须省略return不写,同时也必须省略;不写

格式1:

1
MyFunction(() -> System.out.println("Lambda表达式简写形式一"));

格式2:

1
MyFunction(msg -> System.out.println("Lambda表达式简写形式二" + msg));

格式3:

1
int n = MyFunction(() -> new Random().nextInt(100) + 1);

格式4:

1
int n = MyFunction((a , b) -> a + b);

Lambda和匿名内部类的区别

  • 使用限制不同
    • 匿名内部类:可以操作类、接口
    • Lambda表达式:只能操作函数式接口
  • 实现原理不同
    • 匿名内部类:编译之后,产生一个单独的.class字节码文件
    • Lambda表达式:编译之后,没有一个单独的.class字节码文件