面向对象
- 类 (设计图) : 对象共同特征的描述
- 对象 : 真实存在的具体东西
public class 类名 {
1. 成员变量
2. 成员方法
3. 构造器
4. 代码块
5. 内部类
}
- 用来描述一类事物的类叫
Javabean 类
, 类中不写main
方法 - 编写
main
方法的类叫测试类
封装
对象代表什么, 就得封装对应的数据, 并提供数据对应的行为
e.g. 人画圆: "人" "调用" "圆的方法<画圆>"
private
- 修饰成员 (成员变量和成员方法)
- 被修饰的成员只能在本类中才能访问
public class Person {
private int age;
public void setAge(int a) {
if (a >= 0 && a <= 200) {
age = a;
} else {
System.out.println("illegal");
}
}
public int getAge() {
return age;
}
}
this
下面方法中, int a
中 a
的命名没有表示意义
public void setAge(int a) {
if (a >= 0 && a <= 200) {
age = a;
} else {
System.out.println("illegal");
}
}
如果改为下面代码, 则会出错
public void setAge(int age) {
if (age >= 0 && age <= 200) {
age = age;
} else {
System.out.println("illegal");
}
}
采用 this
, 则可以将局部变量传给成员变量
public void setAge(int age) {
if (age >= 0 && age <= 200) {
this.age = age;
} else {
System.out.println("illegal");
}
}
构造方法
- 用于初始化类
- 如果没写, 虚拟机会自动加一个空参构造, 赋默认值
- 可以同时有空参构造和有参构造 (一般都要写)
public class Person {
private int age;
private String name;
public Person() {
}
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public void setAge(int age) {
if (age >= 0 && age <= 200) {
this.age = age;
} else {
System.out.println("illegal");
}
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Person p1 = new Person();
Person p1 = new Person(18, "xxx");
[[D:/java_code/phone/Phone.java|e.g.Phone]]
内存
- 栈内存: 运行空间, 各个方法按照运行顺序进栈出栈
- 堆内存:
new
开辟的变量空间 - 方法区: 方法与类之间以一定结构存储
Person p1;
Person p2 = p1;
// 因为 p1, p2 存的是地址, 因此此时两个引用指向同一个对象
// 修改 p2 则 p1 也被修改
需要注意的是, Java 中对象之间的赋值是引用赋值, 不同于经常使用的 Cpp 中结构体之间的赋值是值传递
注意区别下面两种写法的不同:
- 错误写法
Person[] people = new Person[10];
Person temp = new Person();
for (int i = 0; i < 10; i++) {
// 用输入数据更改 temp
// ...
people[i] = temp;
}
- 正确写法
Person[] people = new Person[10];
for (int i = 0; i < 10; i++) {
Person temp = new Person();
// 用输入数据更改 temp
// ...
people[i] = temp;
}
-
栈内存中有
p1
, 而p1
存的是地址值, 其为引用数据类型, 就是要引用在别的空间中的数据值(比如堆内存) -
基本数据类型则是数据值就存储在自己的空间里
-
this
的内存原理: 本质是代表方法调用者的地址值
p1.setName("xxx")
// p1 即为方法调用者
- 成员变量和局部变量
区别 | 成员变量 | 局部变量 |
---|---|---|
类中位置 | 类中, 方法外 | 方法内, 方法申明上 (形参) |
初始化 | 有默认初始化值 | 无默认初始化值 |
内存位置 | 堆内存 | 栈内存 |
生命周期 | 随对象创建而存在, 对象消失而消失 | 随方法调用而存在, 方法运行结束而消失 |
作用域 | 整个类中 | 当前方法中 |
Practice
[[D:/java_code/game/Person.java|文字版格斗游戏-code]]
[[D:/java_code/student/Student.java|学生管理-code]]
static
public class Student {
private String name;
private int age;
private String gender;
public static String teacherName;
// 所有对象共用一个 teacherName
}
static
修饰成员变量, 成员方法 -> 静态变量, 静态方法
-
静态变量, 推荐类名调用
Student.teacherName = "xxx";
-
静态方法, 多用于
测试类
和工具类
中,Javabean类
中很少使用, 推荐类名调用Javabean类
: 描述一类事物的类测试类
: 用来检查其他类是否书写正确的类工具类
: 不是用来描述一类事物, 而是用来做一些事情的类- 私有化构造方法, 不用创建对象
- 方法定义为静态, 方便调用
-
堆内存中有静态存储位置
静态区
, 优先于对象
的出现 -
注意
- 静态方法只能访问静态变量和静态方法
(因此测试类
中其他方法需要是静态的才能被main
方法调用) - 静态方法中没有
this
- 非静态方法可以访问所有
- 静态方法只能访问静态变量和静态方法
继承
两个类有很多相同的部分, 比如老师和学生不同在于一个学习一个教学, 相同在于都要吃饭睡觉..., 其中相同的部分为人的属性, 因此可以从一个描述人的类 继承
过来
extends
关键字
public class Student extends Person {}
// Student 称为子类(派生类), Person 称为父类(基类或超类)
当类和类之间存在相同的内容(属性), 且子类是父类中的一种(考虑实际意义), 考虑使用继承
下面注意区分 继承
和 可以访问
-
继承关系
- Java 支持单继承
- Java 不支持多继承
- Java 支持多层继承
- 每一个类都直接或间接的继承于
Object类
(虚拟机自动)
-
子类只能访问父类中非私有的成员
- 构造方法都不能继承
- 成员变量都能继承
- 不能直接调用私有成员变量
- 可以通过非私有方法间接获取私有成员变量
- 私有成员方法不能继承
-
相同命名的成员访问符合就近原则, 即子类成员覆盖父类成员
-
重写
- 当父类的方法不能满足子类现在的需求, 需要进行
方法重写
- 子类中出现和父类中一模一样的方法申明, 称这个方法是重写的方法
- 重写注释
@Override
加到方法上方, 程序会检查重写是语法是否正确 - 重写规则
- 重写方法的名称和形参必须与父类中的一致
- 子类重写父类方法时, 子类的访问权限必须 >= 父类
- 子类重写父类方法时, 子类的返回值类型必须 <= 父类
- 建议重写的方法尽量和父类一致
- 只有被继承的方法才能被重写
- 当父类的方法不能满足子类现在的需求, 需要进行
-
super
代表父类存储空间super()
调用父类的构造方法super.xxx
访问父类的成员变量super.xxx()
调用父类的成员方法
-
构造方法
- 构造方法不会被继承, 但子类中所有构造方法先默认访问父类中的无参构造, 再执行自己的
- 子类初始化时, 可能需要父类的数据, 因此需要调用父类构造方法完成父类数据空间的初始化
- 子类构造方法第一行默认写
super()
, 不写也存在 - 如果想调用父类的有参构造, 需要手动写
- 构造方法不会被继承, 但子类中所有构造方法先默认访问父类中的无参构造, 再执行自己的
多态
同类型的对象, 表现出的不同形态
比如, 人有老师和学生等不同形态
表现为: 父类类型 对象 = 子类对象;
父类型作为参数, 可以接收所有子类对象
前提
- 继承
- 有父类引用指向子类对象
- 有方法重写
[[D:/java_code/person/Test.java|e.g.TestPerson]]
父类类型 对象 = 子类对象;
当我们编写如下代码时,
Person p = new Student();
-
调用成员变量 编译看左边, 运行也看左边
- 编译看
Person
, 如果Person
中没有这个成员变量, 编译失败 - 运行也看
Person
, 调用Person
中的成员变量, 比如sout(p.name)
会是人
而不是学生
- 编译看
-
调用成员方法 编译看左边, 运行则看右边
- 编译看
Person
, 如果Person
中没有这个成员方法, 编译失败 - 运行则看
Student
, 调用Student
中的成员方法, 比如p.show()
会是我是学生
而不是我是人
- 编译看
-
理解:
- 成员变量
- 用什么类去申明对象, 对象引用指向这个类在堆内存中对应的空间
- 用
Person
申明, 则找name
时先在Person
对应的空间里找, 找不到则编译错误 - 如果用
Student
申明, 则找name
时先在Student
对应的空间里找, 找不到再往父类的空间找
- 成员方法
- 用
Person
申明, 则找show()
时先在Person
对应的空间里找, 找不到则编译错误 - 而在方法区, 子类中重写的方法, 覆盖了父类的方法
- 因此运行时, 找
show()
这个重写的方法时, 是找子类的成员方法
- 用
- 成员变量
优势与弊端
-
优势
-
多态形式下, 右边对象可以实现解耦合, 便于扩展和维护
Person p = new Student(); p.show(); p.work(); ...
改为
Person p = new Teacher(); p.show(); p.work(); ...
使用多态, 无需修改太多的后续代码
-
定义方法时, 使用父类型作为参数, 可以接收所有子类对象, 体现多态的扩展性和便利
-
-
弊端
如果使用多态Person p = new Student();
, 则不能调用子类的特有方法 (非重写的方法)p.study()
, 因为在父类中没有这个方法, 会编译失败- 强转为子类后可以调用子类的特有方法
Student p2 = (Student)p;
- 可以用
instanceof
关键字判断真实对象类型
或可以写为if (p instanceof Student) { Student p2 = (Student)p; p2.study(); } else if (p instanceof Teacher) { Teacher p2 = (Teacher)p; p2.teach(); } else { sout("wrong"); }
if (p instanceof Student p2) { p2.study(); } else if (p instanceof Teacher p2) { p2.teach(); } else { sout("wrong"); }
- 强转为子类后可以调用子类的特有方法
包
包就是文件夹, 用来管理不同功能的 Java 类, 方便后期代码维护
包名规则: 公司域名反写 + 包的作用, 需要全部英文小写, 见名知意
比如 com.heima.domain
全类名
package com.heima.domain;
public class Student {
}
则 全类名
为 com.heima.domain.Student
使用其他类时, 需要使用全类名
com.heima.domain.Student s = new com.heima.domain.Student();
如果 import com.heima.domain.Student;
则可以 Student s = new Student();
导包规则
- 使用同一个包中的类时, 不需要导包
- 使用
java.lang
包中的类时, 不需要导包 - 其他情况都需要导包
- 如果同时使用两个包中的同名类, 需要用全类名
final
修饰作用
- 修饰方法: 表明方法是最终方法, 不能被重写
- 修饰类: 表明类是最终类, 不能被继承
- 修饰变量: 表示为常量, 只能被赋值一次
- 修饰基本类型: 变量存储的
数据值
不能改变 - 修饰引用类型: 变量存储的
地址值
不能改变, 对象内部的可以改变
- 修饰基本类型: 变量存储的
常量
-
命名规范
- 单个单词: 全部大写
- 多个单词: 全部大写, 中间用下划线
-
用处
实际开发中, 常量一般作为系统的配置信息, 方便维护, 提高可读性
public class StudentSystem {
private static final String ADD_STUDENT = "1";
private static final String DEL_STUDENT = "2";
private static final String EXIT = "3";
...
public static void startStudentSystem() {
ArrayList<Student> list = new ArrayList<>();
loop:
while(true) {
sout("xxx");
...
Scanner sc = new Scanner(System.in);
String choose = sc.next();
switch(choose) {
case ADD_STUDENT -> addStudent(list);
case DEL_STUDENT -> delStudent(list);
case EXIT -> {
sout("退出");
// break loop; // 跳出 loop 标记的循环
System.exit(0); // 停止虚拟机运行
}
}
}
}
}
权限修饰符
private < 空着(缺省/默认) < protected < public
private
只能在同一个类中使用
缺省
相较前者, 还可以在同一个包的其他类中使用
protected
相较前者, 还可以在不同包下的子类
中使用
public
相较前者, 还可以在不同包下的无关类中使用
- 实际开发中, 一般只有 private 和 public
- 成员变量私有
- 方法公开
- 如果方法中的代码是抽取其他方法中共性代码, 一般也私有
(理解为类中不同方法有相同的部分, 比如一些 clear 操作)
(将相同部分提取出来作为一个函数, 方便内部方法调用, 但不用于外界调用)
代码块
局部代码块
public static void main(String[] args) {
// 局部代码块
{
int a = 10;
sout(a);
}
sout(a); // 错误
}
构造代码块
public class Student {
private String name;
private int age;
// 构造代码块
{
sout("开始创建对象了");
}
// 创建对象时, 会先执行构造代码块, 再执行构造方法
public Student() {
// sout("开始创建对象了");
}
public Student(String name, int age) {
// sout("开始创建对象了");
this.name = name;
this.age = age;
}
}
静态代码块
随着类的加载而加载, 自动触发, 只执行一次 (第一次使用这个类时), 可以用于一些数据的初始化
public class Student {
private String name;
private int age;
// 静态代码块
static {
sout("xxx");
}
}
抽象类
抽象方法: 将共性的行为(方法)提取到父类之后, 由于每一个子类执行的内容不一, 父类不能确定具体的方法体, 该方法可以定义为抽象方法
public abstract 返回值类型 方法名(参数); // 无方法体, 直接 ';'
抽象类: 存在抽象方法的类
public abstract class 类名 {}
[[D:/java_code/person/Person.java|e.g.Person]]
注意
- 抽象类不能实例化
- 抽象类不一定有抽象方法, 有抽象方法一定是抽象类
- 抽象类可以有构造方法 (自己不能实例化, 但子类可以调用)
- 抽象类的子类
- 要么重写抽象类中所有的抽象方法
- 要么是抽象类
意义
强制子类必须按照同一种格式重写共性的方法, 统一申明格式, 便于维护和使用
接口
一种规则, 对行为的抽象
public interface 接口名 {}
接口内方法: public abstract 返回值类型 方法名(参数);
注意
- 接口不能实例化
- 接口和类之间是实现关系, 通过
implements
关键字表示
public class 类名 implements 接口名 {}
public class 类名 implements 接口名1, 接口名2, ... {}
public class 类名 extends 父类 implements 接口名1, 接口名2, ... {}
- 接口的子类 (实现类)
- 要么重写接口中的所有抽象方法
- 要么是抽象类
意义
统一一种行为的重写格式
Practice
[[D:/java_code/animal/Animal.java|e.g.Animal抽象类与Swim接口]]
接口中成员的特点
- 成员变量: 只能是常量, 默认用
public static final
修饰 - 构造方法: 无
- 成员方法: 只能是抽象方法, 默认用
public abstract
修饰- JDK7以前: 接口中只能定义抽象方法
- JDK8: 接口中可以定义有方法体的方法
- JDK9: 接口中可以定义私有方法
接口与类的关系
实现关系, 可以多实现
当一个类实现的多个接口中有相同申明的函数时, 不会报错, 且只用重写一次
接口与接口的关系
继承关系, 可以单继承, 也可以多继承
如果实现类实现了最下面的子接口, 那么需要重写所有的抽象方法
JDK8以后
默认方法
为解决接口升级的问题, 避免"一旦在接口添加抽象方法, 所有实现类都需要立即修改"的问题, 允许在接口中定义默认方法, 需要用 default
修饰
public default 返回值类型 方法名(参数) {}
- 默认方法不是抽象方法, 不强制被重写; 重写时要去掉
default
public
可以省略,default
不能省略- 如果实现了多个接口, 多个接口中存在同名的默认方法, 实现类必须对该方法进行重写, 否则调用不知道调用哪个
静态方法
允许在接口中定义静态方法, 需要用 static
修饰
public static 返回值类型 方法名(参数) {}
- 静态方法不需要重写
- 静态方法只能通过接口名调用, 不能通过实现类或者对象名调用
public
可以省略,static
不能省略
JDK9以后
私有方法
私有方法只为接口内方法服务, 不为外界调用
普通私有方法, 为默认方法服务:
private 返回值类型 方法名(参数) {}
静态私有方法, 为静态方法服务:
private static 返回值类型 方法名(参数) {}
(因为静态方法只能访问静态变量和静态方法)
接口的应用
- 接口代表规则, 是行为的抽象; 类想要哪种行为, 让这个类实现对应的接口
- 当一个方法的参数是接口时, 可以传递接口所有实现类的对象, 即
接口多态
接口类型 j = new 实现类对象();
适配器设计模式
设计模式: 设计的套路
适配器设计模式: 解决接口与接口实现类之间的矛盾问题
- 接口 A 中有很多抽象方法, 但 A 的某个实现类 B 可能只想要重写其中一个方法
- 在接口 A 和接口实现类 B 中, 加一个
适配器 Adapter
C (是一个接口, 实现了 A), 对接口 A 中所有抽象方法进行空实现
, 让 B 实现 C 而不再是 A, 且只重写想要的那个方法 适配器
一般public abstract class 适配器名 implemants 接口名 {}
, 使用abstract
不能实例化, 因为适配器
实例化显然没有意义
内部类
public class Car {
String carName;
int carAge;
String carColor;
// 内部类
class Engine {
String engineName;
int engineAge;
}
}
内部类表示的事物是外部类的一部分, 单独出现没有意义
内部类可以直接访问外部类的成员, 包括私有; 外部类访问内部类成员必须创建对象
成员内部类
public class Car {
String carName;
int carAge;
String carColor;
// 成员内部类
class Engine {
String engineName;
int engineAge;
}
}
-
写在成员位置, 属于外部类的成员
-
与成员一样, 可以被修饰符修饰, 如
private, 默认, protected, public, static
-
JDK16之前, 成员内部类里面不能定义静态变量; 之后可以
创建对象
- 在外部其他类直接创建对象, 需要内部类相应的权限
Outer.Inner oi = new Outer().new Inner();
- 外部类编写方法, 对外提供内部类对象
public class Outer {
private class Inner {
}
public Inner getInstance() {
return new Inner();
}
}
注意在外部其他类, 不能
Outer o = new Outer();
Outer.Inner i = o.getInstance();
只能
Outer o = new Outer();
Object i = o.getInstance(); // 或者直接使用 o.getInstance()
访问成员
public class Outer {
private int a = 10;
class Inner {
private int a = 20;
public void show() {
int a = 30;
sout(a); // 30
sout(this.a); // 20
sout(Outer.this.a); // 10
}
}
}
内部类对象在堆内存对应的空间中, 有一个隐藏的 Outer.this
记录外部类对象的地址值
静态内部类
- 静态内部类只能访问外部类中的静态变量和静态方法
- 如果想访问非静态的需要创建外部类的对象, 再访问
外部类对象.成员
创建对象: Outer.Inner oi = new Outer.Inner();
调用静态内部类的静态方法: Outer.Inner.静态方法名();
局部内部类
内部类定义在方法里面, 类似于方法里的局部变量
匿名内部类
隐层了名字的内部类
new 抽象类名或者接口名() {
重写方法;
}; // 注意 ';'
理解: 大括号即其内部实现看成一个没有名字的类, new
说明这实际上是一个对象, 抽象类名或者接口说明是继承关系还是实现关系
用法:
public static void main(String[] args) {
method(
new Animal() {
@Override
public void eat(){
sout("狗吃骨头");
}
}
// 等于传递了一个 Animal 的子类 Dog, 无需创建一个 Dog 对象
// 多态
);
}
public static void method(Animal a) {
a.eat();
}
Swim s = new Swim() {
@Override
public void swim() {
sout("重写游泳方法");
}
};
// 接口多态
// 左边是接口, 右边是接口的实现类的对象, 符合编译看左边, 运行看右边
new Swim() {
@Override
public void swim() {
sout("重写游泳方法");
}
}.swim();
// 等同于 "对象.方法();"
使用场景:
- 当方法的参数是接口或者类时
- 以接口为例, 可以传递接口的实现类对象
- 如果实现类只要使用一次, 可以使用匿名内部类简化代码