七.类与对象
1.面向过程与面向对象思想
面向过程
在解决一个问题时,会按照预先设定的想法和步骤,一步一步去实现,这里每一步具体的实现中都需我们自己去亲自实现和操作。
例子:自己做->买菜->洗菜->切菜->炒菜->吃
角色: 执行者
特点:费时、费精力、结果也不一定完美
面向对象
在解决一个问题时,可去找具有相对应的功能的事物帮着我们去解决问题,至于这个事物如何工作,与我们无关,我们只看结果,不关心解决流程。
例子:叫外卖->下单->餐馆做饭->骑手送餐->吃
角色: 指挥者
特点:省时,省精力,结果相对完美
面向过程是面向对象的基础,问题可自己不解决托给别人解决,别人也可以在再托给别人,但最终,事情必须要处理掉,一旦处理就是面向过程。
面向对象和面向过程差异
1)面向对象是一种符合人们思考习惯的思想。
2)面向过程中更多的体现是执行者,面向对象中更多的体现是指挥者。
3)面向对象可以将复杂的问题进行简单化,更加贴近真实的社会场景 。
2.类与对象的关系
对象
面向对象编程语言主要是使用对象们来进行相关的编程。
对象,万事万物中存在的每一个实例,一 个电脑、一个手机、一个人、抖音里的一个短视频、支付宝里的一个交易记录、淘宝里的订单。
描述一个对象的内容
1) 对象的属性:就是对象的相关参数,可以直接用数据来衡量的一些特征——常量/变量来表示 (成员变量)
2)对象的行为:就是过将对象的属性进行联动,产生出一系列的动作或行为——函数(成员函数)
类
那些具有相同属性特征和行为的对象们的统称。对象就是该类描述下具体存在的一个事物。
3.封装与private关键字
封装~包装
常见的封装体现:1)函数 2)类
封装好处:1)提高了安全性 2) 向外界隐藏了一些不需要被外界获知的内容 3) 提高了代码的复用性 4) 是面向对象的三大特点之一
面向对象的三大特点:封装 继承 多态
封装不是封死,还要向外提供一些访问内部内容的方法
private关键字属于权限关键字 public protected 默认不写 private
private可以作用在对象属性和行为上,外界在创建对象后,则不能访问被private修饰的内容
总结: 1)类中不需要对外界提供的内容,最好都私有化 2)如果后续真的需要对私有的内容进行更改,最好加上setXXX修改器,getXXX访问器
不可变对象或类
指其内部的数据都是私有的,没有向外界提供任何修改内部的方法(String)
- 所有的数据都是私有的
- 没有修改器的方法
4. 局部变量与成员变量
代码执行流程:
1) javac 编译Sample.java源代码 生成Sample.class和Person.class两个字节码文件。
2) 如果java Person ,运行Person字节码文件,则报错,没有主函数不是主类 。
3)只能java Sample 运行Sample程序 。
4)将相关的字节码(Sample.class Person.class)文件加载进JVM中内存下的方法区。
5)在方法区中Sample字节码所在的区域里,找主函数,将主函数的栈帧加载进栈内存开始运行。
6)开始执行主函数的第一句代码,创建Person对象。
7)在堆内存中开辟一个空间并分配地址,在该空间中创建成员变量并默认初始化。
8)在主函数空间中创建局部变量p1,并将该对象的地址传给p1
9)接着执行主函数第二句代码,调用p1对象的setName方法 。
10)从方法区中的Person里,将setName函数栈帧加载进栈,主函数暂停运行。
11)setName进栈后,创建局部变量name(形参),并将实参“小强”这个字符串在字符串常量池 中的地址赋予name
12)因为setName成员函数只有一份在方法区中Person所属区间里,之后可以被多个同类对象调 用,为了区分到底是哪个对象调用的该方法,所以在每一个成员函数中,都会有一个隐藏的 关键字数据 this ,this相当于一个变量来存储当前对象的地址。(当前对象的引用)
13)执行setName中的内容,如果数据没有问题的话,就将局部变量的值赋值个当前对象的成员变量 。
14)setName函数执行最后一行隐藏的return,表示函数结束并弹栈 。
15)主函数成为当前栈顶,继续执行。
16)执行p1调用setAge函数,从方法区中Person所属空间里找setAge这一段代码,将该函数栈帧加 载进栈内存成为新的栈顶,则主函数暂停,该函数运行。先创建形参age的局部变量,接收实参传来的值10,为了区分对象的调用关系,自带this关键字数据,this存的还是p1的地址,如 果age没有问题,则将10传给this所指向的对象中age这个成员变量。setAge执行最后一行隐藏的return,表示函数结束并弹栈。
17)主函数称为新的栈顶继续执行,调用p1的speak函数进栈 。
18)在方法区中Person字节码所属空间里读取speak代码,将该栈帧加载进占内存中,主函数暂 停,该函数执行,无形参只能表示没有形参的局部变量,但是在函数内部也可以创建其他的 局部变量,并且有this关键数据存的是p1的地址,然后去打印name和age,由于speak空间中 } public int getAge() { return age; } } 已经没有其他名为name或age的局部变量,所以找不到,接着找this对象中的数据,找到了则打印,直至函数结束并弹栈。
19)主函数又称为栈顶,也没有代码了,执行隐藏的return,主函数弹栈,表示程序结束。
局部变量和成员变量的区别
1)生命周期
成员变量随着对象的创建而创建,随着对象的消亡而消失。
局部变量随着函数的进栈而创建,随着函数的出栈而消失。
2) 存储位置
成员变量在堆内存中对象所属空间里。
局部变量在栈内存中函数所属空间里。
3) 定义位置
成员函数在类中,函数外定义。
局部变量在函数中定义。
4)初始化
成员变量有默认初始化 。
局部变量必须初始化之后再调用。
5.构造函数
回顾问题:
之前在创建对象的时候,对象的成员变量是在对象创建之后通过setXXX修改器赋值
对对象成员变量的赋值可以进行可选操作
构造函数:在创建对象的时候执行的函数,在该函数中也可对成员变量进行 一些操作
构造函数的格式
权限修饰符 类名(参数列表) {
构造函数的代码块
}
1)构造函数没有返回值
2)构造函数的名称必须是类名
3)参数列表可选的,构造函数是可以重载的
4)虽然构造函数没有返回值,还是存在return关键字的
4)当我们的类中没有定义任何构造函数时,会有一个默认隐藏的无参构造函数存在
5)构造函数和成员函数一样,为了区分对象的调用,构造函数自带this关键字数据
构造函数需注意的问题
1)如果一旦定义其他参数列表的构造函数的话,这个隐藏的无参构造函数就会消失,建议手写出来
2)构造函数只有在创建对象的时候执行,当对象创建完毕之后,该对象的构造函数则不能执行
3)成员函数只有在对象创建之后才能执行
4)成员函数不能够直接调用构造函数:报找不到符号错误 会误认为是同名的成员函数
5)构造函数能直接调用成员函数:但是这些成员函数一般是构造函数的部分代码片段被切割出来了而已,从语意上而言,不属于对象的特有行为(也有特例),所以这些函数长得样子就是 成员函数的样子,但没有必要向外界提供访问,所以加上private
6)构造函数可以直接调用构造函数:但是必须通过 this(参数列表) ,需要注意的是, 构造函数可以单向调用其他构造函数,但坚决不能出现回调。
7)构造函数是在创建对象的时候执行的,可以在期间对成员变量进行初始化,问:setXXX还需要不?看需求,如果后期成员变量需要修改,则提供setXXX修改器。
成员变量初始化问题
成员变量的初始化经历了三个步骤:1)默认初始化(大家默认都是0值) 2)显式初始化(大家的值都一样 ) 3)针对性初始化(大家的值可选)
6 .对象的创建流程及内存图解
public class Sample {
public static void main(String[] args) {
Stack stack = new Stack();
System.out.println(stack);
for (int i = 1; i <= 10; i++) {
stack.push(i);
}
System.out.println(stack.toString());
System.out.println(stack.pop());
System.out.println(stack);
System.out.println(stack.peek());
}
}
class Stack {
private int[] data; //栈的容器
private int top = -1; //栈顶元素的角标 开始为-1
private static int capacity = 10; //栈容器的最大容量 top + 1 <= capacity
public Stack() {
this(capacity);
}
public Stack(int capacity) {
data = new int[capacity];
}
//向栈中进栈一个元素e
public void push(int e) {
if (size() == data.length) {
//需要扩容
resize(data.length * 2);
}
top++;
data[top] = e;
}
public int pop() {
if (isEmpty()) {
System.out.println(">>>栈已空!无法弹出元素!");
return -1; //表示出错
}
int e = data[top];
top--;
if (size() == data.length / 4 && data.length > capacity) {
resize(data.length / 2);
}
return e;
}
public int peek() {
if (isEmpty()) {
System.out.println(">>>栈已空!无法获取栈顶元素!");
return -1; //表示出错
}
return data[top];
}
private void resize(int len) {
int[] arr = new int[len];
for (int i = 0; i <= top; i++) {
arr[i] = data[i];
}
data = arr;
}
//获取有效元素的个数
public int size() {
return top + 1;
}
//判断栈是否为空
public boolean isEmpty() {
return top == -1;
}
//打印一个对象 其实就是在打印这个对象toString方法的结果
public String toString() {
if (isEmpty()) {
return "[]";
} else {
String s = "[";
for (int i = 0; i <= top; i++) {
if (i == top) {
s = s + data[i] + "]";
} else {
s = s + data[i] + ", ";
}
}
return s;
}
}
}
public class Sample {
public static void main(String[] args) {
Player p1 = new Player("老王",100);
Player p2 = new Player("老李",100);
p1.shootEnemy(p2);
Gun gun = new Gun();
p1.holdGun(gun);
p1.shootEnemy(p2);
Clip clip = new Clip();
for (int i = 1; i <= 30; i++) {
clip.pushBullet(new Bullet());
}
p1.loadClip(clip);
for (int i = 1; i <= 30; i++) {
p1.shootEnemy(p2);
}
p1.shootEnemy(p2);
}
}
class Player {
private String name;
private int blood;
private Gun gun;
public Player() {}
public Player(String name,int blood) {
this.name = name;
this.blood = blood;
}
public Player(String name,int blood,Gun gun) {
this.name = name;
this.blood = blood;
this.gun = gun;
}
public void holdGun(Gun gun) {
this.gun = gun;
}
public void shootEnemy(Player enemy) {
if (gun == null) {
System.out.println(">>>玩家信息:没有枪,开P");
} else {
System.out.printf(">>>玩家信息:%s向%s开了一枪\n",name,enemy.name);
gun.shootEnemy(enemy);
}
}
public void loadClip(Clip clip) {
if (gun == null) {
System.out.println(">>>玩家信息:没抢,装不了弹夹");
} else {
gun.loadClip(clip);
}
}
public void damage(int hurt) {
if (blood == 0) {
System.out.println(">>>玩家信息:" + name + "已经成盒,请勿鞭尸");
} else {
blood -= hurt;
if (blood > 0) {
System.out.println(">>>玩家信息:" + name + "掉血" + hurt + ",剩余" + blood);
} else {
blood = 0;
System.out.println(">>>玩家信息:" + name + "已经成盒");
}
}
}
}
class Gun {
private Clip clip;
public Gun() {
this(null);
}
public Gun(Clip clip) {
this.clip = clip;
}
public void loadClip(Clip clip) {
this.clip = clip;
}
public void shootEnemy(Player enemy) {
if (clip == null) {
System.out.println(">>>枪信息:没有弹夹,开了个空枪");
return;
}
Bullet bullet = clip.popBullet();
if (bullet == null) {
System.out.println(">>>枪信息:弹夹没子弹 开了个空枪");
} else {
bullet.hitEnemy(enemy);
}
}
}
class Clip {
private int capacity = 30;
private int surplus = 0;
private Bullet[] magazine;
public Clip() {
this(30);
}
public Clip(int capacity) {
this.capacity = capacity;
magazine = new Bullet[capacity];
}
public void pushBullet(Bullet bullet) {
if (surplus == capacity) {
System.out.println(">>>弹夹信息:弹夹已满,无法装入子弹");
return;
}
magazine[surplus] = bullet;
surplus++;
showClip();
}
public Bullet popBullet() {
if (surplus == 0) {
System.out.println(">>>弹夹信息:弹夹已空,无法弹出子弹");
return null;
}
Bullet bullet = magazine[surplus - 1];
surplus--;
showClip();
return bullet;
}
public void showClip() {
System.out.printf(">>>弹夹信息:%d/%d\n",surplus,capacity);
}
}
class Bullet {
private int hurt = 10;
public Bullet(){}
public Bullet(int hurt) {
this.hurt = hurt;
}
public void hitEnemy(Player enemy) {
enemy.damage(hurt);
}
}
7. static关键字
静态关键字
1)主函数有static修饰-静态函数,全局变量static修饰
2)主要用于修饰成员变量(对象的特有属性)和成员函数,变为静态变量和静态函数
静态变量最大的特点是同类下多个对象之间的共有属性
何时定义静态变量
在同一类下,多个对象之间有相同的属性和值,那么就可以将该属性和值从 成员变量变为静态变量,目的就是为了节省空间。
何时定义静态函数
当一个成员函数不访问成员时,即可定义为静态函数! 静态函数一旦定义出来,可以直接用类名去调用(Math.sqrt()),当然也可通过创建对象来去调用静态函数! 静态优先于对象存在,且在同一类中,静态无法访问非静态(成员),非静态是可访问静态 。
静态函数中不存在this: 当通过对象去调用一个属性时,先找成员,再找静态,最后找父类 如果从成员函数中去调用一个属性时,先找局部,再找成员,再找静态,最后找父类。
优点:节省堆内存中的空间,可不用费力气去创建对象来调用功能,可对类进行一些初始操作(结合代码块来做)
8. 静态变量与成员变量
存储位置
成员变量存储在堆内存中对象所属空间里 ;静态变量存储在静态方法区中对应的字节码空间里 。
生命周期
成员变量随着对象的创建而创建,随着对象的消亡而消亡 ; 静态变量随着类的加载而存在,随着程序的结束而消失 。
所属不同
成员变量属于对象的,称之为是对象的特有属性 ;静态变量属于类的,称之为是类的属性,或者叫对象的共有属性。
调用方式不用
成员变量在外界必须通过创建对象来调用,内部的话成员函数可以直接调用成员变量,但是静态函数不能直接调用成员变量,如果非要在静态函数中调用成员的话,只能创建对象,通过对象来调用 ; 静态变量在外界可以通过对象调用,也可以通过类来调用,内部的话静态函数/成员函数可以调用静态变量。
八.继承
1.继承概述
多个事物之前有共同的属性或行为,这种代码的复用性比较差,可以将多个事务之间重复性的属性或行为进行抽取,抽取出来之后放在另外一个单独的类里面。
虽将Person类抽取出来,但是目前Worker和Teacher与Person没有关系;要让类之间产生父子关系,用关键字extends。
一句话:把多个事物中重复性的内容进行抽取,并生成另外一个类,该类就是其他类的父类,其他的为子类,父类与子类之间用关键字extends标明。
继承的好处
1)继承的出现提高了代码的复用性
2) 继承的出现让类与类之间产生关系,也为我们后面多态提供了前提
单继承与多继承
在现实生活中,父与子一对多关系,子与父一对一
使用继承时,除了要考虑类与类之间重复的内容之外,更重要的是去考虑关系,必须是一种 is a 关系
Java中 类与类之间只能支持单继承, 接口与接口之间可以支持多继承
继承体系
既有了单继承,也有父子关系,爷孙关系,曾祖父-重孙,继承出现了层级,继承体系
需注意的问题
子类并不是或是父类的一个子集!实际上,一个子类通常比它的父类包含更多的信息和方法。(子类更多的是父类的一种升级和延伸)
父类中的一些私有内容,子类是获取不到的。若父类对其自身的私有内容设置了公共的访问器和修改器的话,子类可以通过该访问器和修改器来获取父类的私有内容。
不要为了获取某一个特殊的属性或行为,而去乱认爸爸
在设计类和其继承关系时,一定要符合社会常识认知伦理问题
子父类中,成员变量的特点
1)子类没有,父类有且非私有,子类对象能获取num
2)子类没有,父类有且私有,子类对象不能获取num
3)子类有,父类有且非私有,子类对象获取的是子类的num 不存在重写的概念
4)子类有,父类没有, 子类对象获取的是子类的num
如果子类中,成员变量和父类变量和局部变量重名时,这里面有一个特殊的 super
this表示的是当前对象,存的是当前对象在堆内存中的地址
super不表示父类的对象,因为在此我们并没有去创建父类的对象!super仅仅表示父类空间, 并没有创建父类的对象!
子父类中,构造函数的特点
当创建子类对象时,在子类的构造函数执行之前,父类的构造函数先执行,虽然父 类的构造函数执行但不代表父类对象的创建。
每一个类中的构造函数第一句如果不是 this() 调用本类中其他构造函数的话,默认第一句 是隐藏的 super()
因为在创建子类对象的时候,需要父类为继承给子类的一些数据进行初始化。
this()与super()是否冲突
若子类的构造函数们之间没有调用关系,则每一个子类的构造函数第一句都是 super()
在构造函数中,第一句要么是this(),要么是super()
没有可能每一个构造函数第一句都是this()——否则递归调用
有可能每一个构造函数第一句都是super()——构造函数之间不调用
this()与super()本身不冲突的,如果构造函数之间有调用关系,那么最后一个被调用的构造函数就不能再回调,那么其第一句就不能是this(),只能是super()
如果父类中,没有无参构造函数的存在,只有有参数的构造函数的话,那么子类中默认的 super() 就调用不到父类无参构造函数!引发错误!
建议每一个类都把它的无参构造函数写出来!
子父类中,成员函数的特点
如果父类有,子类没有,调用的是父类的
如果父类没有,子类有,调用的是子类的
如果父类有,子类也有,调用的是子类的
如果父类有,但是为私有,则子类继承不到,除非子类自己写一个;称之为是函数的重写/覆盖/Override
重写作用
严格意义上,子类并非是父类的一个子集。子类的内容很大程度上,很多情况下,都是父类的一种扩展或增量,重写仅仅去保留了父类的功能声明,但是具体的功能内容由子类来决定。
如果子父类中有同名函数且参数列表相同(私有例外),编译器就认为是重写关系! 重写的时候,子类的权限必须大于等于父类的权限 重写的时候,返回值不能更改
子父类中,静态成员的特点
静态变量的特点与成员变量是一致的 ;静态函数的特点与成员函数是一致的
2. final关键字
final翻译叫做最终,final可以修饰变量、函数、类
final修饰变量
表示该变量的值不可被改变; 变量主要分为两种,基本数据类型、引用数据类型的变量
final修饰的是基本数据类型变量 表示变量所存储的常量值不能改变
final修饰的是引用数据类型变量 表示变量所存储的对象地址值不能改变 ,但是可以改变该对象 中的数据(如果对象中的数据也是final 则也不能修改)
一般,当我们在定义常量(字面量 用变量+final来表示),定义成静态变量
public static final 数据类型 变量名 = 常量数据;
对于常量的变量名起名规则为 全部单词大写 单词与单词之间用下划线分隔
public static final double PI = 3.14;
public static final int MAX_VALUE = 100;
final修饰函数
如果某一个类中的函数,不想让其子类去重写的话,该函数就可以声明为final类型
final修饰类
表示该类不能被继承
3.抽象类
抽象:指的就是不具体,模糊不清这种含义 ,看不懂 ,不明白
不能直接将抽象和类挂钩, 之所以有抽象类的存在,是因为有抽象函数! 具有抽象函数的类,称之为叫抽象类!
抽象函数
当我们将多个事物的共同行为(函数)进行抽取并封装到另外一个类中时,发现在该类 中,这些方法的具体执行内容无法确定,只能由这些子类来决定该函数的具体执行,那么在该类中,将 这些抽取来的函数仅保留函数声明,但不保留函数体即可,那么该函数就是抽象函数,用abstract关键 字来修饰,既然有了抽象函数的存在,那么具有抽象函数的类也被称之为抽象类,也必须用abstract修 饰。抽象类不能创建对象,只有其实现子类能够创建对象。
抽象类的特点
抽象类和抽象函数都需要被abstract修饰,抽象方法一定在抽象类中
抽象类不能创建对象,因为如果一旦创建对象,在调用其函数时,函数没有具体执行内容
只有覆盖了抽象类中所有的抽象函数后,子类才可以实例化。否则,该子类还是一个抽象类
抽象类细节问题
抽象类一定是一个父类,因为抽象类本身就是有多个事物进行抽取而来的
抽象类有成员变量、成员函数、构造函数,抽象类与一般类唯一的区别就是抽象类中抽象函数,其他一律相同(抽象类不能创建对象)!
抽象类和一般类的异同点:
相同点:
都是用来描述事物的
都可以定义属性和行为
不同点:
一般类可以具体的描述是,抽象类描述事物时会有一些不具体的信息
抽象类比一般类可以多定义一个成员:抽象函数
一般类可以创建对象,而抽象类不能创建对象
抽象类中是可以不定义抽象函数,有抽象函数的类一定是抽象类,抽象类不一定有抽象函数!
抽象关键字abstract不能与那些其他的关键字共存
final:final修饰类,表示该类不能被继承;final修饰函数时,表示函数不能被重写; 不能,抽象类本就是父类,并且其中的抽象函数就等着被子类重写。
private:private修饰函数,表示函数被私有,不能被子类继承;不能,抽象函数就 等着被子类重写。
static:static修饰的函数,属于类的,随着类的加载从而被加载方法区中,和对象没 有关系了,可以直接用类来调用静态成员,如果抽象函数被静态修饰,被类调用时 没意义。
4 .接口
从代码角度,接口其实就是抽象类的一种特殊表现形式 ;当一个抽象类中,所有的方法都是抽象函数时,那么,该类就可以用接口来表示
接口还是类吗?不是类了,一些类的功能和操作不再适用于接口。
接口中没有成员函数,没有成员变量,没有构造函数,没静态函数,没有静态变量 ;接口也不能直接去创建对象
接口中的变量和函数会有一些特殊的含义
接口中的变量 默认是公共静态常量 public static final类型 就算不写这些关键字 也是默认的
接口中的函数 默认是公共抽象的函数 public abstract 类型 就算不写这些关键字 也是默认的
类与类之间是单继承关系,类与接口之间是多实现关系,接口与接口之间是多继承关系
类与接口之间的实现关系用implements关键字表示
要么这个类实现接口中所有的方法; 要么这个类声明为abstract 这一点和继承抽象类一致的
类与接口也有着多实现的存在,要么把这几个接口全部实现,要么声明为abstract
接口与接口有着多继承的关系,对于InterfaceC接口的实现子类而言,要么把这几个接口全部实现, 要么声明为abstract
接口与接口之间会不会存在实现关系?不存在
接口中的特点
接口中的变量都是全局静态常量
接口中的方法都是全局抽象函数
接口不可以创建对象
子类必须覆盖接口中所有的抽象方法,或声明为abstract
类与接口之间可以存在多实现关系
接口与接口之间可以存在多继承关系
类与类之间只能是单继承关系
接口的存在, 主要解决的就是类与类之间只有单继承的关系
如果一个类,可以存在两种状态的描述时,类在继承的同时也可以进行接口的实现
当一个类在继承另外一个类的时候,就已经拥有了该继承体系的所有功能,接口的作用就是在这些 体系功能之上,增加的一些额外的功能!大大的扩展了类的功能!
接口除了作为类的功能扩展之外,接口还可以作为一种对外暴露的规则,来进行解耦操作
接口之间可以多继承,类之间不能多继承
看函数的函数体
接口与抽象类的区别
相同点: 都位于继承或实现的顶端 ;都不能实例化 ;都包含抽象函数,其子类都必须覆盖这些方法 。
不同点: 一个类只能继承一个父类,但是可以实现多个接口 抽象类中其实可以存在一些已经实现好的方法,有部分未实现的方法由子类来决定;接口中只 能包含抽象函数,子类必须完全实现。