面向对象的三大特性
封装
概念
指将数据及相关操作绑定在一起的一种编程机制,使其构成一个不可分割的独立实体。
在Java中,类就是这样一种结构。
当然,数据被保护在类的内部,是为了尽可能地隐藏内部的细节,只保留一些对外"接口"
使之与外部发生联系。
把抽象出来的数据(属性), 和对数据的操作(方法)封装在一起, 数据被保护在内部, 程序的其他部分只有被授权的操作(方法), 才能对数据进行操作.
封装的好处
- 使用者能够完全得到自己想要的功能,又不需要思考过多细节
- 实现者可以隐藏功能实现的细节,方便灵活进行修改而不影响使用者使用
- 可以对数据进行验证,保证安全合理
封装的步骤
- 需要的属性进行私有化
- 提供一个
public
的get
方法,访问成员变量的值
get
方法的命名规则getXxx()
,小驼峰 - 提供一个
public
的set
方法,修改成员变量的值
set
方法的命名规则setXxx(参数)
,小驼峰
注意事项
使用private私有化成员变量,并提供方法给外界访问时,需要注意:
-
成员变量的访问权限应该尽量收紧,尽量私有化,当然有必要时也可以给外界权限直接访问。
-
对于boolean类型的成员变量来说,Getter方法的名字比较特殊,采用
isXxx(Xxx是变量名)
的形式。
继承
概念
复用代码(复用父类的成员:成员变量、成员方法,不涉及static
)
语法
权限修饰符 class 类名 extends 被继承的类/已有的类 {类体}
说明
- 一个类通过
extends
(扩展)关键字去继承另一个类,这个类被称为子类(subClass
),被继承的类称为父类(基类baseClass
,超类superClass
) - 一个类继承了父类,就拥有了父类的成员(成员变量、成员方法)
- 一个子类还可以拥有自己的成员(在父类的基础上进行扩展)
- 父类中没有的,子类无法继承
父子类之间的关系:子类is-a父类的关系
引用数据类型的转换
分类
- 自动类型转换(向上转型)小范围 --> 大范围
自动发生的,不需要写代码 - 强制类型转换(向下转型)大范围 --> 小范围
需要手动写代码
语法:子类类型 对象名 = (子类类型) 父类的引用
进行强制类型转换的时候可能会转换不成功,由异常出现
引用数据类型的转换前提:存在继承关系
强制类型转换的转换成功条件
强转后的引用类型必须能够真正的指向该对象,即强转后的引用的类型必须是该对象的类型或者其父类型
继承的优点
- 代码可以复用,减少了代码的冗余
- 方便扩展
@Override注解的作用:标记该方法是否是重写(覆盖)父类的方法
继承的缺点
父类的修改能够体现到所有的子类中,子类无条件接收父类中的成员
注意事项
- java中的继承是单继承,一个子类只能有一个直接父亲
extends
关键字后面只能有一个类- 不能循环继承
几个概念
- 祖先类:处于继承层次的顶层(如果一个类没有显示的使用
extends
关键字,祖先类就是Object
类,Object
类是所有类的祖先类) - 继承层次:某个祖先类派生出来的所有子类的集合
- 继承链:子类到祖先类的一个路径
protected访问权限
- 同包下,随便访问
- 不同包下,只有在子类当中,创建子类自身对象,才能够访问父类中的
protected
成员
继承中的限制
- 父类中的
private
成员有没有继承? ---- 继承了,但是没有访问权限 - 父类中的构造方法能否继承? ---- 构造方法不能算成员(成员变量、成员方法),子类不能继承
- 父类中的静态成员能不能继承? ---- 父类中的静态成员子类可以使用,但是不属于继承
类加载的时机(Update)
- main方法
- 首次new对象
- 访问类中的静态的成员
- 子类加载的时候,先触发父类的加载(从祖先类开始加载,一直到该子类)
继承中赋值的顺序
先父类的成员进行默认赋值,显示赋值,构造器
再子类的成员赋值,赋值顺序保持一致
为什么父类的赋值先执行?
因为父类的构造器是先执行的,
原因:在子类的构造器中有一个默认隐藏的语句super()
,
此时如果子类的构造器中没有this(实参)
,也没有super()
, ----> 子类对象的隐式初始化
显示初始化就是在子类的构造器中用代码把super
显示的写出来
继承的内存图展示
继承的流程
- 父子类加载(先父后子)
- 创建子类对象
- 子类对象中会专门开辟一片独立的区域,用来存储父类的成员变量(父类成员区域, 近似看成一个父类对象, 被
super
关键字指向, 近似看做super指向当前子类对象的父类对象) - 子类自身的成员仍会存储在自身对象当中(
this
指向当前子类对象)
- 子类对象中会专门开辟一片独立的区域,用来存储父类的成员变量(父类成员区域, 近似看成一个父类对象, 被
- 父子类成员赋值(先父后子)
- 默认初始化
- 显式赋值
- 构造代码块赋值
- 构造器赋值
隐式对象初始化的必要条件
- 父类中有默认的构造方法
- 子类的构造器中没有显式使用
super
调用父类的构造方法,也没有用this
去调用自己的构造方法。
达成上述两个条件,则JVM在初始化子类对象时进行隐式初始化,永远先执行父类的构造方法,顺序为:
- 最上层的父类(Object)
- 其他父类(继承链中越处于上流越先执行)
- 所有父类的构造方法都执行完毕,开始执行子类构造方法
super关键字
- 表示一个
(近似)
的引用,指向的是子类对象中父类的成员区域,(近似)
看作是指向父类对象 super(实参)
表示调用的是父类某个构造方法- 在父类和子类中出现了同名的成员,使用
super
进行区分(通过super.
去调用父类方法)
this VS super
- 含义:
this
—> 引用,指向的是当前的对象
super
—> 近似引用,指向的是子类对象中的父类成员区域,近似看做指向父类的对象 - 使用:
this(实参)
—> 调用子类自身的构造
super(实参)
—> 调用的是父类的构造器 - 区分作用:
this
—> 区分同名成员变量跟局部变量
super
—> 区分同名的父子类中的成员变量 - 是否受权限控制
this
—> 不受访问权限的控制
super
—> 受访问权限的控制
注意
this
和super
在构造器的第一行
-------> this
和super
在一个构造方法里面不能同时出现
总结
我们将程序的运行分成两部分:
第一部分:类加载
-
首先程序要从
main
方法启动,这意味着首先要触发,装有main
方法的那个类的类加载。类加载过程中,一定要考虑连环触发类加载的情况:
- 类中有静态成员变量创建对象,那么一定会触发其它类的类加载。
- 该类还有父类,于是触发父类类加载。
-
类加载这个过程中,静态代码块的代码一定会执行,不要忘记了。
-
如果有静态成员变量的显式赋值,那么显式赋值和静态代码块,按照代码的书写顺序从上往下执行。
-
类加载整个程序运行期间只有一次,如有通过继承连环触发类加载,那么顺序是
先父后子
,从最顶层父类开始。
第二部分:创建对象
- 切记类加载是懒加载,有些类可能等到main方法执行到一半才触发类加载。
- 这个就要随机应变了,以下步骤,都默认类加载全部结束了。
- new对象时,首先去找到new对象的构造器,然后观察第一行
- 如果它的首行显式地调用了另一个构造器(可能是
this(参数)
,也可能是super(参数)
)- 那么程序会先跳转到那个构造器,再去看代码首行有没有显式调用另一个构造器
- 直到找到一个构造器它隐含的super()指向Object类的无参构造
- 于是开始按照这个类中构造代码块和显式赋值的代码书写顺序,从上到下执行其中的代码
- 最后执行这个类的构造器
- 开始执行被跳转的构造器,同样先执行显式赋值和构造代码块后执行构造器
- 最后执行完new对象构造器,创建对象过程结束。
- 那么程序会先跳转到那个构造器,再去看代码首行有没有显式调用另一个构造器
- 如果它的首行没有显式调用另一个构造器,那么必定隐含
super()
指向父类的无参构造器。- 如果直接指向Object类的无参构造,那十分简单,直接不用管
- 执行类中的显式赋值和构造代码块,最后执行构造器
- 如果指向一个普通父类的无参构造,那就观察首行,根据情况执行步骤a或b
- 最终一定父类构造器执行完毕,回到new对象的类中,执行完毕new对象构造器,创建对象过程结束。
- 如果直接指向Object类的无参构造,那十分简单,直接不用管
- 如果它的首行显式地调用了另一个构造器(可能是
继承中的属性隐藏
三种创建对象的方式
-
父类引用指向父类的对象(
Father father = new Father()
)
访问的范围:父类
访问的结果:父类 -
子类引用指向子类的对象(
Son son = new Son()
)
访问的范围:父类 + 子类
访问的结果:子类 -
父类的引用指向子类的对象(
Father fs = new Son()
)
访问的范围:父类
访问的结果:父类
结论:
对于访问范围来说 ----> 取决于引用的类型(编译看左边)
对于访问结果来说 ----> 取决于引用的类型(运行也看左边)
通过对象名.成员方法
调用方法的机制
-
父类引用指向父类的对象(
father.
)
访问的范围:父类
访问的结果:父类 -
子类引用指向子类的对象(
son.
)
访问的范围:子类
访问的结果:子类 -
父类的引用指向子类的对象(
fs.
)
访问的范围:父类
访问的结果:子类
结论:
对于访问范围来说 ----> 取决于引用的类型(编译看左边)
对于访问结果来说 ----> 取决于具体的对象的类型(运行看右边)
方法的覆盖/重写
含义
@override
:用来标记或简称这个方法是不使用重写父类的方法
快捷方式:直接在子类种写跟父类同名的方法名,直接回车
快捷键:alt + enter
-> 选择重写方法
语法
// 成员方法的语句
[ 访问权限修饰符] 返回值类型 方法名( 形参列表){
// 方法体
}
说明:
- 对于权限修饰符来说,子类重写父类方法的时候,修饰符跟父类保持一致或者更为宽松
- 对于返回值类型来说,子类重写父类方法的时候,返回值类型跟父类保持一致或者兼容
(兼容:对于基本数据类型来说要一致,对于引用数据类型来说是可以兼容,能够自动类型转换(向上转型)) - 方法名重写的时候必须是一样的
- 形参列表必须一样
- 方法体里无所谓
- 静态的方法就不是重写
方法的重载(Overload) VS 方法的重写(Override)
注意事项
- 父类中私有方法不能被重写(因为没有权限访问,更不谈重写)
- 静态方法在使用现象上,很像是被重写了,但实际上静态方法不能被重写,而是直接是一个新的静态成员。(使用
@Override
注解标记会报错) - 构造器不能继承,更不能被重写
final 关键字
分类
- final修饰类(class类)
表示这个类不能被继承 - final修饰方法
表示被final
修饰的方法不能被重写
语法:权限修饰符 final 方法的返回值类型 方法名(形参列表) { 方法体}
注意事项:- private方法,本来就无法重写,不需要多此一举。(可以修饰,但是会报警告)
- static方法,本来就无法重写,不需要多此一举。(可以修饰,但是会报警告)
- 构造方法,不能被继承,更不能重写,加final修饰会编译报错。
- final修饰变量
见下面3个常见问题
3个常见问题
常量分类
- 字面值常量
- 整型、小数、字符、字符串、空
- 自定义常量
- 非
final
修饰的常量
- 非
变量
数据类型可以是基本数据类型
- final
修饰基本数据类型 —> 变量的值不能改变 final int a = 1;
也可以是引用数据类型
- final
修饰引用数据类型 —> 表示这个引用不能发生变化,指向不能改变,但是对象的状态是可以改变的
变量的分类
- 局部变量
- 基本数据类型的局部变量 —> 值不能变
- 引用数据类型的局部变量 —> 引用不能变,对象的状态(指对象内部的值)可以改变
- 局部常量:存储在栈上,生命周期跟方法同生共死,唯一的区别就是不可以改变
- 成员变量
- 成员常量:不能使用默认值,必须进行初始化操作,必须赋值(语法:
final 数据类型 成员变量名
) - 成员常量不是真正意义上的唯一
- 成员变量的赋值手段:(1)默认赋值不让用;(2)显示赋值;(3)构造代码块;(4)构造方法赋值
- 赋值方式必须是上述3种方法的其中一种,但是只能选择一个,只能赋值一次
- 成员常量:不能使用默认值,必须进行初始化操作,必须赋值(语法:
- 静态成员变量
- 表示这是一个全局的常量,是唯一的
- 语法上:
final 数据类型 静态成员变量名;
- 静态成员变量也不能用默认值
- 静态成员变量初始化赋值的手段:
1. 显示赋值
2. 静态代码块
要求:使用这两种方式的任意一种都可以,有且只有一种
注意事项
- final static 还是 static final 实测下来,都是可以的,根据个人习惯使用即可。
- final 修饰静态成员变量是一个全局常量,不会害怕外界访问和修改。所以在很多时候,它的访问权限修饰符都是public的。
- 如果使用静态代码测试类加载,那么访问类的全局常量,有些场景是不完整的类加载的, 没有进行初始化。(感兴趣自己测试一下,作为锻炼动手能力的小demo)
多态
概念
字面意思,同一事物在不同情况下所表现出来的不同的状态
说明:
- 同一事物:同一父类引用
- 不同情况:不同的子类对象
- 不同状态:调用同名方法时,实现不同,表现出不同的行为
总结:
Java中的多态指的是,同一个父类引用指向不同子类对象时,调用同名成员方法,根据指向实际对象的不同,得到的行为也会随之不不同。
能够发生多态的条件
- 继承
- 子类要重写父类的方法
- 存在父类引用指向子类的对象
不能发生多态的场景
final
修饰的类不能继承- 不允许方法重写:
final
修饰的方法static
方法private
方法
- 代码种没有体现父类引用指向子类的对象
多态的访问特征
父子类种同名的成员(成员变量、成员方法)
- 访问范围(
.
出来的范围,编译的角度) - 访问结果(最终运行出来的结果,运行的角度)
结论:
- 访问成员变量时,无论是访问的范围还是结果都取决于引用的数据类型(编译看左边,运行也看左边)
- 访问成员方法时,访问的范围取决于引用的类型,运行的结果取决于所指向的具体对象的类型(编译看左边,运行看右边)
多态的优点
- 代码复用
- 进行扩展
多态的缺点
- 在多态中,父类引用指向了子类的对象,因为父类引用的限制,无法访问子类独有的方法
eg:animal.work();
引用数据类型的强制类型转换
强转后的引用类型必须能够真正的指向该对象,即强转后的引用的类型必须是该对象的类型或者其父类型。
instanceof 关键字
作用
强转失败会导致程序抛出异常:ClassCastException
,导致程序终止执行。正是由于强转的条件苛刻,而且失败后果很严重,所以Java当中提供了检测手段,来保障强转的安全性。需要使用关键字:instanceof
语法
引用 instanceof 类名
结果:
- true:表示该引用指向的对象,是后面类名的一个对象或者子类对象
- false
eg:
GrandFather gf = new Son();
System.out.println(gf instanceof GrandFather); // true
System.out.println(gf instanceof Father); // true
System.out.println(gf instanceof Son); // true
System.out.println(gf instanceof Grandson); // false
方法的实参数据类型,需要和方法的形参数据类型保持一致?
方法名(实参)
- 对于基本数据类型而言,要么一致或者要么兼容
兼容:自动类型转换 - 对于引用数据类型而言,要么保持一致要么兼容
兼容:自动类型转换(向上转型)