面向对象
面向对象
类和对象
客观存在的事物皆为对象,所以我们也常常说万物皆对象.
- 类
- 类的理解
- 类是对现实生活中一类具有共同属性和行为的事物的抽象
- 类是对象的数据类型,类是具有相同属性和行为的一组对象的集合
- 简单理解:类就是对现实事物的一种描述
- 类的组成
- 属性:指事物的特征,例如:手机事物(品牌,价格,尺寸)
- 行为:指事物能执行的操作,例如:手机事物(打电话,发短信)
- 类和对象的关系
- 类:类是对现实生活中一类具有共同属性和行为的事物的抽象
- 对象:是能够看得到摸得着的真实存在的实体
- 简单理解:类是对事物的一种描述,对象则为具体存在的事物
类的定义
类的组成是由属性和行为两部分组成
- 属性:在类中通过成员变量来体现(类中方法外的变量)
- 行为:在类中通过成员方法来体现(和前面的方法相比去掉static关键字即可)
类定义的步骤
- 定义类
- 编写类的成员变量
- 编写类的成员方法
1 |
|
对象的使用
创建对象的格式
类名 对象名 = new 类名();
调用成员的格式
- 对象名.成员变量
- 对象名.成员方法()
示例代码
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/*
创建对象
格式:类名 对象名 = new 类名();
范例:Phone p = new Phone();
使用对象
1:使用成员变量
格式:对象名.变量名
范例:p.brand
2:使用成员方法
格式:对象名.方法名()
范例:p.call()
*/
public class PhoneDemo {
public static void main(String[] args) {
//创建对象
Phone p = new Phone();
//使用成员变量
System.out.println(p.brand);
System.out.println(p.price);
p.brand = "小米";
p.price = 2999;
System.out.println(p.brand);
System.out.println(p.price);
//使用成员方法
p.call();
p.sendMessage();
}
}
对象内存图
单个对象内存图
Student s = new Student();
- 加载class文件
- 申名局部变量
- 在堆内存中开辟一个空间
- 默认初始化
- 显示初始化
- 构造方法初始化
上述图片执行流程为:
方法区存储StudentTest.class,虚拟机去执行StudentTest中的main方法
由于运行main方法,所以在栈内存中开辟一块main方法的内存区域,开始执行main方法中的代码
Student s = new Student(),表示在main方法内存中创建一个变量Student s,因为Student是一个类,所以也会存储到方法区内,又因为使用new关键字创建对象,会在堆内存中开辟一个空间来存储Student对象
在new 一个Student对象时,会进行成员变量的默认初始化(就是成员变量的默认值,比如 int为0,String为null)
如果在Student对象中的成员变量设置了默认值(private String name=”小明”),这个就是显示初始化
如果在new Student时传入了参数(new Student(“小红”,18)),这就是构造方法初始化。
还会存储方法区内Student.class中成员方法的地址(这里指的是study方法)
当在对堆内存的对象初始化完成后,会将堆内存的地址值赋值给存在于栈内存中的s局部变量
System.out.println(s);控制台打印输出的就是Student对象中的堆内存地址001
System.out.println(s.name+”……”+s.age);控制台输出的就是默认初始化的值,因为在创建Student对象时,没有经历过显示初始化和构造方法初始化
s.name=”阿强”、s.age=23;,再去打印输出,就是阿强…23。通过s.name找到堆内存中Student对象并给该对象的name赋值为阿强,通过s.age找到堆内存中Student对象并给该对象的age赋值为23
s.study();这里再次调用一个方法,所以study方法会进入栈内存中。在该方法中只是System.out.println(“好好学习”);在控制台打印好好学习
执行完study方法后出栈,接着main方法出栈
最后student没有方法去使用,也会当做垃圾被回收。
多个对象内存图
具体流程就是重复单个的对象的创建内存图(这个new 的相同的对象,但是对象在堆内存的地址是不同的)
总结:多个对象在堆内存中,都有不同的内存划分,成员变量存储在各自的内存区域,成员方法对个对象公用一份。
两个引用指向同一个对象
注意:如果将通过Student stu2=stu1,这样是将new的stu1的堆内存中的地址值赋值给stu2,所以stu1和stu2是指向同一个堆内存的Student的内存空间,那么修改stu1的值,stu2再去读取,也就会读到stu1修改后的值。
成员变量和局部变量
区别
类中的位置不同
成员变量(类中方法外),局部变量(方法内部或方法声明上)
内存中位置不同
成员变量(堆内存),局部变量(栈内存)
生命周期不同
成员变量(随着对象的存在而存在,随着对象的消失而消失),局部变量(随着方法的调用而存在,随着方法的调用完毕而消失)
初始化值不同
成员变量(有默认初始化值),局部变量(没有默认初始化值,必须先定义,赋值才能使用)
封装
封装思想
封装概述
是面向对象三大特征之一(封装、继承、多态)
对象代表什么,就得封装对应的数据,并提供数据对应的行为
就比如:人关门(门是通过人的力,导致门关闭的)
这里人和门是分别两个对象,那么就对应的有Person类和Door类,在Door类中有一个属性为门的关闭状态,那么就应该提供该状态的行为,也就是关门的方法。
封装代码实现
将类的某些信息隐藏在类的内部,不允许外部程序直接访问,而是通过类提供的方法来实现对隐藏信息的操作和访问
成员变量private,提供对应的getXxx()/setXxx()方法
private关键字
private是一个修饰符,可以用来修饰成员(成员变量、成员方法)
被private修饰的成员,只能在本类进行访问,针对private修饰的成员变量,如果需要被其他类使用,提供相应的操作
- 提供get变量名()方法,用于获取成员变量的值,方法用public修饰
- 提供set变量名(参数)方法,用于设置成员变量的值,方法用public修饰
示例
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/*
学生类
*/
class Student {
//成员变量
String name;
private int age;
//提供get/set方法
public void setAge(int a) {
if(a<0 || a>120) {
System.out.println("你给的年龄有误");
} else {
age = a;
}
}
public int getAge() {
return age;
}
//成员方法
public void show() {
System.out.println(name + "," + age);
}
}
/*
学生测试类
*/
public class StudentDemo {
public static void main(String[] args) {
//创建对象
Student s = new Student();
//给成员变量赋值
s.name = "林青霞";
s.setAge(30);
//调用show方法
s.show();
}
}
this关键字
this修饰的变量用于指代成员变量,其主要作用是区分局部变量名和成员变量名重名问题
- 方法的形参如果与成员变量同名,不带this修饰的变量指的是形参,而不是成员变量
- 方法的形参没有与成员变量同名,不带this修饰的变量指的是成员变量
1 |
|
继承
java中提供一个关键字extends,用这个关键字,我们可以让一个类和另一个类建立起继承关系。
例如 public class Student extends Person{}中,Student称为子类(派生类),Person称为父类(基类或者超类)。
当类与类之间,存在相同(共性)的内容,并满足子类是父类中的一种,就可以考虑使用继承,来优化代码
这里要注意:一定是要满足子类是父类的一种,比如员工有id和name,商品也有id和name,这个存在相同的内容,但是不满足子类是父类的一种,不能把员工和商品抽取出一个共同的父类。
使用继承的好处
- 可以把多个子类中重复的代码抽取到父类中,提高代码的复用性
- 子类可以得到父类的属性和行为,子类可以使用,并且子类可以在父类的基础上,增加其他的功能,使子类更强大
继承的特点
java只支持单继承,不支持多继承,但支持多层继承
为什么支持多继承(下列代码为假设java支持多继承,演示会出现的问题)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class 父类A{
public void method(){
System.out.println("复习数学")
}
}
public class 父类B{
public void method(){
System.out.println("复习语文")
}
}
public class 子类 extends 父类A,父类B{
}
public class Test{
public static void main(String[] args){
子类 z =new 子类();
z.method(); //这里在子类调用方法时,不知道去加载那个父类的方法。
}
}多层继承:子类A继承父类B,父类B可以继承父类C
每一个类都直接或者间接的继承与Object类
子类继承父类方法的内容
内容 | 非私有 | 私有 |
---|---|---|
构造方法 | 不能 | 不能 |
成员变量 | 能 | 能 |
成员方法 | 能 | 不能 |
构造方法是否可以被继承
类的构造方法名应该要和类名一致,所以父类的构造方法不能被子类继承
1 |
|
成员变量是否可以被继承
加载流程
- 将TestStudent.class字节码文件加载到方法区,jvm将自动把main方法加载到栈内存中
- 创建一个Zi对象的变量z
- 会加载Zi.class的字节码文件,这里会存储Zi类的成员变量name
- 在加载Zi.class后,发现继承与Fu,所以这边会加载Fu.class文件,在Fu中存储name和age
- new Zi(),会在堆内存中创建对象,该内存会分为两份,左边存储的是父类的成员变量,右边存储自己的成员变量
- 将堆内存的地址值赋值给z变量
- 打印z的地址值
- 把钢门吹雪赋值给z对象的name(查找顺序,先找右边的,没找到再去找左边的)
- 把23赋值给z对象的age,王者农药赋值给z对象的game
- 打印z对象的name、age、game
父类成员变量被private修饰
看内存图可以发现,父类private修饰的成员变量是被子类继承了,存储在子类对象的内存中,但是由于是private修饰的,z.name在堆内存中的Zi对象找不到对应的name去赋值。
成员方法是否可以被继承
子类主要继承的是父类的虚方法(非private、非static、非final修饰的成员方法就是需虚方法)
加载顺序
- 加载TestStudent.class字节码文件到方法区,jvm调用该类的main方法
- 创建Zi类变量z
- 加载Zi.class
- 加载父类Fu.class
- 由于Fu没有继承父类,则会默认继承Object.class
- 加载Object.class.它有五个虚方法存储在虚方法表中
- Fu继承Object,会继承Object的5个虚方法表,加上自己的fuShow1,就有六个虚方法
- Zi继承Fu,继承六个虚方法,加上自己的ziShow(),总共7个虚方法
- new Zi(),在堆内存中创建内存区域,分为2份,右边的存自己的成员变量,左边的存父类的成员变量,把堆内存中的地址值赋值给变量z
- 打印z的地址值
- 调用z的ziShow方法和fuShow1方法打印出对应的值(去虚方法表中找对应的方法并找到了)
- 最后调用z的fuShow2方法报错,说明私有的成员方法不能被继承。(没在虚方法中找到对应的方法)
继承中,成员变量的访问特点
就近原则:谁离我近,我就用谁。先在局部位置找,本类成员位置找,父类成员位置找,逐级往上
1 |
|
继承中,成员方法的访问特点
直接调用满足就近原则:谁离我近,我就用谁,super调用,直接访问父类
1 |
|
继承中:构造方法的访问特点
父类中的构造方法不会被子类继承
子类中所有的构造方法默认先访问父类中的无参构造,在执行自己
为什么(原因)
子类在初始化的时候,有可能会使用到父类中的数据,如果父类没有完成初始化,子类将无法使用父类的数据
子类初始化之前,一定要调用父类构造方法先完成父类数据空间的初始化
怎么调用父类的构造方法
子类构造方法的第一行语句默认都是super(),不写也存在,且必须在第一行
如果想要方法访问父类有参构造,必须手动书写
this、super使用总结
- this:理解为一个变量,表示当前方法调用者的地址值
- super:代表父类存储空间
关键字 | 访问成员变量 | 访问成员方法 | 访问构造方法 |
---|---|---|---|
this | this.成员变量 访问本类成员变量 | this.成员方法(….) 访问本类成员方法 | this(…) 访问本类构造方法 |
super | super.成员变量 访问父类成员变量 | super.成员方法(…) 访问父类成员方法 | super(…) 访问父类构造方法 |
方法的重写
当父类的方法不能满足子类现在的需求时,需要进行方法重写。
书写格式
在继承体系中,子类出现了和父类中一模一样的方法声明,我们就称子类这个方法时重写的方法。
@Override重写注解
- @Override是放在重写后的方法上,校验子类重写时语法是否正确
- 加上注解后如果有红色波浪线,表示语法错误
- 建议重写方法都加@Override注解,代码安全,优雅
方法重写的本质
方法重写注意事项和要求
- 重写方法的名称,形参列表必须与父类中的一致
- 子类重写父类方法时,访问权限子类必须大于父类(空着不写<protected<public)
- 子类重写方法时,返回值类型子类必须小于等于父类
重写的方法尽量和父类保持一致
- 私有方法不能被重写
- 子类不能重写父类的静态方法,如果重写会报错。
构造方法
构造方法概述
构造方法是一种特殊的方法
作用
创建对象 Student stu = new Student();
格式
public class 类名{
修饰符 类(参数){
}
}
功能
主要是完成对象数据的初始化
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Student {
private String name;
private int age;
//构造方法
public Student() {
System.out.println("无参构造方法");
}
public void show() {
System.out.println(name + "," + age);
}
}
/*
测试类
*/
public class StudentDemo {
public static void main(String[] args) {
//创建对象
Student s = new Student();
s.show();
}
}
构造方法注意事项
构造方法的创建
如果没有定义构造方法,系统将给出一个默认的无参构造方法
如果定义了构造方法,系统将不在提供默认的构造方法
构造方法的重载
如果自定义了带参构造方法,还要使用无参构造方法,就必须在写一个无参构造方法
推荐使用方式
无论是否使用,都手写无参构造方法
重要功能
可以使用带参构造,为成员变量进行初始化
多态
多态的格式
1 |
|
多态的前提:有继承关系,子类对象是可以赋值给父类类型的变量。例如Animal是一个动物类型,而Cat是一个猫类型,Cat继承了Animal,Cat对象也是Animal类型,自然可以赋值给父类类型的变量。
多态的使用场景
如果没有多态,在下图中register方法只能传递学生对象,其他的Teacher和administrator对象是无法传递给register方法的,在这种情况下,只能定义三个不同的register方法分别接收学生、老师和管理员
有了多态之后,方法的形参就可以定义为共同的父类Person。
注意点
- 当一个方法的形参是一个类,我们可以传递这个类所有的子类对象
- 当一个方法的形参是一个接口,我们可以传递这个接口所有的实现类对象
- 而且多态还可以根据传递的不同对象来调用不同类中的方法
代码示例
1 |
|
多态的定义和前提
多态:是指同一行为,具有多个不同表现形式
前提
- 有继承或者实现关系
- 方法的重写(意义体现:不重写,无意义)
- 父类的引用指向子类对象(格式体现)
多态的运行特点
调用成员变量时:编译看左边,运行看左边
调用成员方法时:编译看左边,运行看右边
代码解析
1 |
|
理解:
Fu f= new Zi();现在是用f去调用变量和方法的,而f是Fu类型的,默认都会从Fu这个类去找。
成员变量
在子类的对象中,会把父类的成员变量也继承下来,比如父:name 子:name
成员方法
如果子类对方法进行了重写,那么在虚方法表中时会把父类的方法进行覆盖的。
多态调用成员的内存图解
代码执行流程
- 将Test类加载到方法区,虚拟机将Test.class的main方法加载到栈内存中执行
- 定义一个Animal a变量,将Animal.class加载到方法区(有所有的成员变量和所有的成员方法,并在虚方法表中有一个show方法)
- new Dog(),将Dog.class加载到方法区,由于Dog是继承Animal的,所以会继承Animal的所有成员变量和虚方法表中的方法,发现Dog中重写的show方法,这里会把show方法覆盖,由Animal的show方法变为Dog的show方法。接着在堆内存中开辟一块内存空间,将这块内存分类两部分,左边的存储父类继承过来的成员变量name,值为动物,右边存储自己的成员变量name,值为狗,将堆内存中的Dog内存地址赋值给变量a
- sout(a.name),打印变量a.name,找到堆内存中存储的父类的成员变量name,值为动物并打印
- a.show,通过a的地址值找到堆内存中的信息,在找到方法区中的Dog.class的信息,因为该class中的show方法已经重写了,这里是子类的show方法,所以加载到栈内存执行show方法,打印的是Dog—–show方法
多态的弊端
我们已经知道多态编译阶段是看左边父类类型的,如果子类有些独有的功能,此时多态的写法就无法访问子类的独有功能了
1 |
|
引用类型转换
为什么要转型
多态的写法就无法访问子类独有的功能了
回顾基本数据类型转换
- 自动转换:范围小的赋值给范围大的,自动完成 double d =5;
- 强制转换:范围大的赋值给范围小的,强制转换:int i = (int)3.14;
多态的转型分为向上转型(自动转换)与向下转型(强制转换)两种
向上转型(自动转换)
多态本身是子类类型向父类类型向上转换(自动转换)的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型
1
2父类类型 变量名 = new 子类类型
如 Animal a = new Cat();
向下转型(强制转换)
父类类型向子类类型向下转换的过程,这个过程是去强制的。一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型
使用格式:
1
2
3子类类型 变量名 = (子类类型) 父类变量名;
如:Aniaml a = new Cat();
Cat c =(Cat) a;
转型的异常
转型的过程中,一不小心就会遇到这样的问题
1 |
|
这段代码可以通过编译,但是运行时,却报出了 ClassCastException
,类型转换异常!这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。
instanceof关键字
为了避免ClassCastException的发生,Java提供了 instanceof
关键字,给引用变量做类型的校验,格式如下:
1 |
|
所以,转换前,我们最好先做一个判断,代码如下:
1 |
|
instanceof新特性
JDK14的时候提出了新特性,把判断和强转合并成了一行
1 |
|
综合练习
根据需求完成代码:
1 |
|
1 |
|
标准类制作
类名需要见名知意
成员变量使用private修饰
提供至少两个构造方法
- 无参构造方法
- 带全部参数的构造方法
get和set方法
提供每一个成员变量对应的setXxx()/getXxx()
如果还有其他行为,也需要写上。