面向对象是一种编程方式,通过对象的方式将现实世界映射到计算机模型的一种表达方法。在解决显示世界问的的过程中对问题进行抽象,通过对象的方式代入到计算机模型中进行解决。其基本思想是使用 抽象、类、对象、封装、继承、多态等基本概念来进行程序设计。
抽象
抽象是面向对象的一个必不可少的概念。抽象是将一类对象的共同特征总结出来,构造类Java中的 class的过程。包括数据抽象和行为抽象两个方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是扫描。
- 行为抽象:是指对可执行代码的抽象。用户在在使用是只需要关系对象行为的接口而不关系具体实现细节,这也符合 迪米特法则/最少知道原则。
- 数据抽象:只想外界提供关键信息,并隐藏其背后实现细节,只表现必要的信息而不呈献细节。
例如:
// Switch 开关
class Switch{
// 开关状态
private boolean status = false;
// 输出电压
private int voltage = 0;
// 改变开关状态
public void toSwitch() {
this.status = !this.status;
// 如果开关打开,输出5v 电压 如果关闭 输出 0v 电压
this.voltage = this.status ? 5 : 0;
}
// 是否打开
public void isOpen() {
return this.status;
}
}
//
public static void main(String[] args){
Switch s1 = new Switch();
Switch s2 = new Switch();
// 切换开关
s1.toSwitch();
System.out.println("第二个开关是否打开了:" + s2.isOpen());
}
我们有一个Switch
类,Switch
是对 输出电压和开关状态的数据抽象,Switch.toSwitch()
是对开关的行为抽象。外部使用的过程中,不需要关心 Switch.toSwitch()
的实现细节,而只注重所需要功能即可。
类
类是具有相同属性和方法的一组对象的集合。他们属于该类的所有对象提供了统一的抽象描述符,其内部主要包含属性和行为两个主要部分。在面向对象的编程语言中,类是一个独立的程序单位,它应该有一个类名并包括属性和方法两个主要部分。
Java中的类实现包括两个部分:类声明和类体。
类声明
[public][abstract|final] class className [extends superclassName] [implements interfaceNameList]{……}
其中,修饰符public,abstract,final 说明了类的属性,className为类名,superclassName为类的父类的名字,interfaceNameList为类所实现的接口列表
类体
class className{
[public | protected | private ] [static] [final] [transient] [volatile] type variableName;//成员变量
[public | protected | private ] [static] [final | abstract] [native] [synchronized] returnType methodName([paramList]) [throws exceptionList]{
statements
}//成员方法
}
成员变量限定词的含义:
static
: 静态变量(类变量)final
: 常量;transient: 暂时性变量,用于对象存档,用于对象的序列化volatile
: 贡献变量,用于并发线程的共享
方法的实现也包括两部分内容:方法声明和方法体。
方法声明
方法声明中的限定词的含义:
static
: 类方法,可通过类名直接调用abstract
: 抽象方法,没有方法体final
: 方法不能被重写native
: 集成其它语言的代码synchronized
: 控制多个并发线程的访问
方法声明包括方法名、返回类型和外部参数。其中参数的类型可以是简单数据类型,也可以是复合数据类型(又称引用数据类型)。
对于简单数据类型来说,java实现的是值传递,方法接收参数的值,但不能改变这些参数的值。如果要改变参数的值,则用引用数据类型,因为引用数据类型传递给方法的是数据在内存中的地址,方法中对数据的操作可以改变数据的值。
方法体
方法体是对方法的实现,它包括局部变量的声明以及所有合法的Java指令。方法体中声明的局部变量的作用域在该方法内部。若局部变量与类的成员变量同名,则类的成员变量被隐藏。
为了区别参数和类的成员变量,我们必须使用this
。this
用在一个方法中引用当前对象,它的值是调用该方法的对象。返回值须与返回类型一致,或者完全相同,或是其子类。当返回类型是接口时,返回值必须实现该接口。
构造方法
- 构造方法是一个特殊的方法。Java 中的每个类都有构造方法,用来初始化该类的一个对象。
- 构造方法具有和类名相同的名称,而且不返回任何数据类型。
- 重载经常用于构造方法。
- 构造方法只能由
new
运算符调用
对象
对象是系统中用来描述客观事物的一个实体,它是构成系统的一个基本单位。对象是通过类来进行实例化的。类是对象的一套模板。类规定了对象所拥有的属性和行为。
一个对象的声明周期分为三个阶段:生成
,使用
,销毁
特性
封装
利用抽象数据类型将数据和基于数据的行为封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型内部,尽可能的隐藏内部细节,只保留对外的接口使之与外部发生联系。用户无需指定内部对象内部细节,但可以通过对象对外提供的接口来访问该对象。
优点:
- 减少耦合: 可以独立的开发、测试、优化、使用、理解和修改
- 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块。使用模拟数据进行调试,调试的对象可以自行 new 不影响业务内部new 的对象
- 可有效的调节性能:可以通过刨析确定哪些模块影响了系统的性能。因为经过封装后,相同逻辑的功能都在一个 class种可以如果出现问题可以方便快速定位和修改
- 提高软件的可重用性
- 降低了构建大型系统的风险:面向对象的方式更好的切合解决大型问题的思路。将不同功能划分为不同class单元进行解决,降低大型系统开发的复杂性,从而间接的避免了出错的风险
在Java种对于封装提供了4种访问级别:
范围 | private | default | protected | public |
---|---|---|---|---|
同一个类中使用 | √ | √ | √ | √ |
同一个包中使用 | √ | √ | √ | |
子类中可使用 | √ | √ | ||
全局可用 | √ |
private
:类中限定为private的成员,只能被这个类本身访问。如果一个类的构造方法声明为private
,则其它类不能生成该类的一个实例。default
:类中不加任何访问权限限定的成员属于缺省的(default
)访问状态,可以被这个类本身和同一个包中的类所访问。protected
:类中限定为protected
的成员,可以被这个类本身、它的子类(包括同一个包中以及不同包中的子类)和同一个包中的所有其他的类访问。public
:类中限定为public
的成员,可以被所有的类访问。
例子
以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用
注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行
public class Person {
private String name;
private int gender;
private int age;
public String getName() {
return name;
}
public String getGender() {
return gender == 0 ? "男" : "女";
}
public void work() {
// 是否符合工作年龄
if (18 <= age && age <= 50) {
System.out.println(name + " 工作很努力!");
} else {
System.out.println(name + " 不能再工作了!");
}
}
}
继承
Java种的关键字 extends 来进行类的继承
继承实现了 IS-A 关系,例如 玛莎拉蒂
和 汽车
就是一种 IS-A 关系,因此 马萨拉蒂
可以继承自 汽车
,从而获得 汽车
非 private
的属性和方法。
继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。玛莎拉蒂
可以当做 汽车
来使用,也就是说可以使用 汽车
引用 玛莎拉蒂
对象。父类引用指向子类对象称为 向上转型
汽车 car = new 玛莎拉蒂();
Java 中的继承:
- Java中父类可以拥有多个子类,但是子类只能继承一个父类,称为单继承。
- 继承实现了代码的复用。
- Java中所有的类都是通过直接或间接地继承
java.lang.Object
类得到的。 - 子类不能继承父类中访问权限为
private
的成员变量和方法。 - 子类可以重写父类的方法,即命名与父类同名的成员变量。
Java中通过super
来实现对父类成员的访问,super
用来引用当前对象的父类。**super**
** 的使用有三种情况:**
- 访问父类被隐藏的成员变量,如:
super.variable
; - 调用父类中被重写的方法,如:
super.Method([paramlist])
super()
调用父类构造方法; - 调用父类的构造函数,如:
super([paramlist])
;
多态
对象的多态性是指在父类中定义的属性或方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为。这使得同一个属性或方法在父类及其各个子类中具有不同的语义。例如:"几何图形"的"绘图"方法,"椭圆"和"多边形"都是"几何图"的子类,其"绘图"方法功能不同
多态分为编译时多态和运行时多态:
- 编译时多态:在编译阶段,具体调用哪个被重载的方法,编译器会根据参数的不同来静态确定调用相应的方法
- 运行时多态:由于子类继承了父类所有的属性(私有的除外),所以子类对象可以作为父类对象使用。程序中凡是使用父类对象的地方,都可以用子类对象来代替。一个对象可以通过引用子类的实例来调用子类的方法
运行时多态有三个条件:
- 继承
- 覆盖(重写)
- 向上转型
重载(Overloading)
- 方法重载是让类以统一的方式处理不同数据类型的手段。
- 一个类中可以创建多个方法,它们具有相同的名字,但具有不同的参数和不同的定义。调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法。
- 返回值类型可以相同也可以不相同,无法以返回型别作为重载函数的区分标准。
重写(Overriding)
- 子类对父类的方法进行重新编写。如果在子类中的方法与其父类有相同的的方法名、返回类型和参数表,我们说该方法被重写 (Overriding)。
- 如需父类中原有的方法,可使用super关键字,该关键字引用了当前类的父类。
- 子类函数的访问修饰权限不能低于父类的。
类图
描述类与类、对象与对象之间的关系
泛化关系 (Generalization)
用来描述继承关系,在 Java 中使用 extends 关键字。
实现关系 (Realization)
用来实现一个接口,在 Java 中使用 implements 关键字。
聚合关系 (Aggregation)
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。
组合关系 (Composition)
和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
关联关系 (Association)
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。
依赖关系 (Dependency)
和关联关系不同的是,依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:
- A 类是 B 类中的(某中方法的)局部变量;
- A 类是 B 类方法当中的一个参数;
- A 类向 B 类发送消息,从而影响 B 类发生变化;
感悟
抽象
通常在惯性思维里,我们解决问题一般需要把问题化解为细小的问题进行解决。面向对象的话是吧每个问题的抽象为单独的单位,而面向更大粒度的进行解决这个问题,每个独立单位只需要提供该单位需要提供的功能即可。把问题划分为单位,这个是抽象的过程。按照需要将具有相同属性 相同行为的部分抽象为一个类 class在解决大问题的过程种,调用这些类的对外暴露的行为即可。无需事事亲为。
例如:
把大象放进冰箱需要几步
面向过程:
void 打开冰箱门();
void 放入大象(大象 obj);
void 关闭冰箱门();
// 调用:
int main() {
打开冰箱门();
大象 obj;
放入大象(obj);
关闭冰箱门();
}
面向对象:
// 抽象所有可以放入冰箱的东西为物品
class 物品{
}
class 大象 extends 物品 {
}
class 冰箱{
public void 打开冰箱门();
public void 放入(物品 obj);
public void 关闭冰箱门();
}
class 苹果 extends 物品{
}
// 调用
public static void main(String[] args){
冰箱 厨房冰箱 = new 冰箱();
冰箱 客厅冰箱 = new 冰箱();
// 放入大象
厨房冰箱.打开冰箱门();
厨房冰箱.放入(new 大象());
厨房冰箱.关闭冰箱门();
// 放入苹果
客厅冰箱.打开冰箱门();
客厅冰箱.放入(new 苹果());
客厅冰箱.关闭冰箱门();
}
通过这里可以看到,如果只是解决一个单一不会变的问题,面向过程是比较简洁的一种实现方式。但是如果需求的改变或者问题很复杂,又有很多重复逻辑的部分:
现在需要10个冰箱,大象放到第一个冰箱,苹果放到第二个冰箱,腊肉放到第四个冰箱 …
这样的话,面向过程就很难受了,需要创建出30个冰箱的操作。面向对象就可以把冰箱抽象出来,每次调用通过对象的方式进行调用即可,因为每个部分的操作逻辑都相同。