我们知道Java是典型的面向对象程序设计语言。所以可以说面向对象思想贯穿了我们程序设计的始终。本来就是一个很抽象的概念,希望通过我的浅显的见解能帮助大家理解。本文先介绍面向对象程序设计思想,后面着重介绍面向对象的三大特性,结合代码体现面向对象的三大特性。
面向对象程序设计思想
什么是面向对象
面向对象是相对于面向过程思想来说的,我们在大学学的第一门语言相信大家可能很多都是C语言,C语言就是面向过程编程语言,面向过程是以 “过程” 为中心的程序设计思想,将问题的解决问题分解为步骤或者说是模块,模块化思想自顶而下设计程序,尽量的结构化组织架构,把各个模块的各部分元素实现高内聚,低耦合(相对的),然后用函数把这些步骤一步一步地实现,使用的时候一个一个地依次调用,听起来挺具有合理性的是吧,但是在现代越来越大型的系统中,面向过程将数据和方法分离这就造成了后期维护很大的问题,数据管理混乱,理解困难,重用性差,耦合程度也是比较大的。
这个时候 面向对象 编程思想 就体现出了它的优越性,面向对象是一种更符合我们人类思考方式的设计思想,它可以将复杂的事情简单化,并将我们从执行者变成了指挥者。
面向对象 是一种以 “对象” 为中心的 程序设计思想,这里的对象泛指现实世界中一切事物,每种事物都具备自己的属性和行为(成员变量和方法)。面向对象思想就是在计算机程序设计过程中,参照现实事物,将事物的属性特征、行为特征封装起来,让对象去实现具体的细节,当我们需要用到的时候只需要调用对象的行为来实现功能,而不是自己一步一步的将实现细节操作实现。这种思想是将数据作为第一位,而方法或者说是算法作为其次,这是对数据一种优化,操作起来更加的方便,简化了过程。
举例
洗衣服:
面向过程:把衣服脱下来–>找一个盆–>放点洗衣粉–>加点水–>浸泡10分钟–>揉一揉–>清洗衣服–>拧干–>晾 起来
面向对象:把衣服脱下来–>打开全自动洗衣机–>扔衣服–>按钮–>晾起来
造汽车:
面向过程:不是很懂,,,本来就是一项很大的一项工程。
面向对象:设计类,比如发动机类,外形类,轮胎类,车门类 …不需要第一件事就去关心各个组织模块的功能如何实现,而是先去关心各个类的设计,把数据和方法封装不分离!独立为一个整体,让类的实体——对象调用方法实现功能。
面向对象特性
通过上面 面向过程 和 面向对象 程序设计思想的比较,我们应该有了大概的认识。那么什么是类?什么是对象呢?
我们可以发现我们身边有很多对象,比如自己在看这篇文章时你的手机、电脑,你坐的椅子、桌子,包括你和我等等都是一个一个对象,没错,现实世界就是一个一个对象组成的。我们来看一下类和对象的定义以及它们之间的关系。
类: 面向对象编程中一种引用数据类型,是一组相关属性和行为的集合。可以看成是一类事物的模板,使用事物的属性特征和行为特征来描述该类事物。比如:人类,狗类,猫类。
对象:是一类事物的具体体现。就像定义一样,对象是类的实例,必然具备这类事物的属性和行为。
所以,类是对一类事物的描述,是抽象的。
对象是一类事物的实例,是具体的。
你要是非要问我是先有对象还是先有类呢?
… … …
我自己的理解是先有对象后有类,可能是出于现实世界的角度看待这个问题吧,我们每一个人都是一个个对象,类的概念毕竟是抽象的,它表示对现实生活中一类具有共同特征的事物的抽象,是具有相同属性和行为的一组对象的集合,程序中也是对象会被分配内存,而类是一个数据类型…
具体可参考文章 点击链接。
面对对象的三大特性是:封装、继承、多态。封装是面向对象思想核心的体现。继承是多态的前提。
下面我们将重点放在面向对象这三大特性来具体学习一下面向对象思想。
一、封装
我们上面提到了 面向对象思想 是将数据和方法封装起来,具体做法就是将属性隐藏起来,若外界需要访问某个属性,提供公共方法对其访问。类中的大多数数据只能通过本类的方法实现,类包含说明和实现,说明部分被外界所看到,通过简单的外部接口与外界进行联系;实现部分不被外界所看到,在内部通过不同方法的构建,实现不同的功能。
在Java中具体实现就是使用 private 私有修饰符将类的成员变量隐藏起来,并定义成对的 setter 和 getter 方法让外界得以访问,封装可以被认为是一个保护屏障,防止该类的代码和数据被其他类随意访问。要访问该类的数据,必须通过指定的 方式。适当的封装可以让代码更容易理解与维护,也加强了代码的安全性、复用性。
public class Student {
private String name;
private int id;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
如果再加上无参和有参构造方法,这样的类我们称之为一个标准类。此外,方法本身也是一种封装,这个也不能忽视。
二、继承 (多态的前提)
类作为面向对象最基本的概念,我们在设计类的时候需要考虑到共性提取的问题,以提高代码的复用性,同时也为后面的多态创造了前提。。多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要 继承那一个类即可。比如猫类和狗类都可以继承自哺乳动物甚至动物类。
2.1 继承的定义
继承:就是子类继承父类的属性和行为,使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。
继承主要解决的问题是:共性提取
之前写过一篇具体关于继承的一些收获,由于不想让这篇文章显得毕竟冗余(本来一说话就想啰嗦…),所以本文不做过多赘述 。继承总结收获
补充: 父类的私有变量子类也会继承,但是因为 private 原因,子类对象不能直接访问,但是可以定义构造方法再利用 super 关键字调用父类的构造方法,这样在其它类也可以进行初始化。
2.2 抽象类
引入
现在我们思考一个问题,有一个父类我们定义成 图形,里面有相关的属性以及有一个计算面积的方法。 有两个类: 正方形 和 三角形 继承图形类,他们也要有计算面积这个方法,但是正方形和三角形计算面积的方法不相同,在父类中怎么定义计算面积的方法呢?
此时抽象类抽象方法出现了,如果父类当中的方法不确定如何进行 { } 方法体实现,那么就应该是一个抽象方法。 (你能说图形不能计算面积吗?)但具体实现方式不确定。
定义
抽象方法 : 没有方法体的方法。
抽象类:包含抽象方法的类。
注意事项
- 不能直接创建(new)抽象类对象。
- 用一个子类继承抽象父类。子类必须重写抽象父类中所有的抽象方法。(除非该子类也是抽象类)
- 抽象类中可以有构造方法,是供子类创建对象时,初始化父类成员使用的。
- 抽象类中不一定包含抽象方法。但有抽象方法的类必定是抽象类。
(不包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类的设计。设计模式中的适配器设计就是应用这个设计)
2.3 接口(interface)
在介绍多态之前先介绍接口,因为多态是建立在子类继承父类和实现类实现接口前提下的。
接口的定义
接口就是多个类的公共规范。
接口,是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量、构造方法和成员方法,那么 接口的内部主要就是封装了方法,包含抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法 (JDK 9)。
注意:在Java 8 之后接口支持默认方法(default 且不可省略,用于接口升级)和静态方法的定义。
public interface MyInterfaceDefault {
// 抽象方法
public abstract void methodAbs();
// 新添加的默认方法,
public default void methodDefault() {
System.out.println("这是新添加的默认方法");
}
}
public class MyInterFaceDefaultA implements MyInterfaceDefault {
@Override
public void methodAbs() {
System.out.println("这是一个抽象方法AAA");
}
// 注意并没有重写默认方法,所以调用的时候会向上找接口
}
public class MyInterfaceDefaultB implements MyInterfaceDefault{
@Override
public void methodAbs() {
System.out.println("这是一个抽象方法BBB");
}
@Override
public void methodDefault() {
System.out.println("覆盖重写了默认方法BBB");
}
}
public class Demo2UInterface {
public static void main(String[] args) {
MyInterFaceDefaultA a = new MyInterFaceDefaultA();
a.methodAbs(); // 调用抽象方法,实际运行的是右侧实现类
a.methodDefault(); //调用默认方法,如果实现类中没有,会向上找接口
System.out.println("================");
MyInterfaceDefaultB b = new MyInterfaceDefaultB();
b.methodAbs();
b.methodDefault(); // 实现类B重写了接口的默认方法。
}
}
注意
- 接口中没有静态代码块或者构造方法。
- 一个类的直接父类是唯一的,但是一个类可同时实现多个接口。(要覆盖多个方法)
- 如果实现类所实现的多个接口当中,存在重复的抽象方法,那么只需要重写一次即可。
- 如果实现类没有重写所有抽象方法,那么这个实现类就必须是一个抽象类
- 如果实现类实现的多个接口时,存在重复的默认方法,那么实现类一定要对冲突的默认方法进行覆盖重写。
接口之间的多继承
- Java 中类与类之间是单继承的。(直接父类只有一个)
- 类与接口之间是多实现的关系,即一个类可以实现多个接口。
- 接口与接口之间是多继承的。
- 多个父接口当中的抽象方法如果重复,没关系。本来就没方法体…
- 多个父接口当中的默认方法如果重复,那么子接口必须重写默认方法。(必须带default关键字)
public interface MyInterfaceA {
public abstract void methodA();
void methodCommon();
default void method() {
System.out.println("这是父类A的默认方法");
}
}
public interface MyInterfaceB {
public abstract void methodB();
void methodCommon();
default void method() {
System.out.println("这是父类B的默认方法");
}
}
public interface MyInterface extends MyInterfaceA, MyInterfaceB{
// 接口当中的多继承,会继承前两个接口中的抽象方法。
public abstract void method1();
default void method() {
}
}
ps: 由于两个父接口默认方法冲突,所以必须重写默认方法,否则报错。
优先级的问题
注意: 一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类就近 选择执行父类的成员方法。
三、多态
3.1 什么是多态及其实现机制
多态是继封装、继承之后,面向对象的第三大特性。多态,多态,就是多个状态。
官方的定义是:接口的多种不同的实现方式
… … 好抽象…
官方说:多态性是允许你将父对象设置成为一个或更多的他的子对象相等的技术。
其实多态体现在父类或者接口的引用变量指向子类或者实现类的实例对象。
查了下相关资料和博客知道了一些实现多态的机制:
我们在程序中定义的引用变量所指向的具体类型和通过该引用变量的方法调用在编译的时候并不确定,当处于运行期间才确定。就是这个引用变量究竟指向哪一个实例对象,在编译期间是不确定的,只有运行期才能确定,这样不用修改源码就可以把变量绑定到不同的类实例上,让程序拥有了多个运行状态,这就是多态。
多态又分为 编译时多态和运行时多态。
编译时多态:比如重载
运行时多态:比如重写
也就是说: 程序调用方法是在运行期才动态绑定的,那么引用变量所指向的具体实例对象在运行期才确定。所以这个对象的方法是运行期正在内存运行的这个对象的方法而不是引用变量的类型中定义的方法。
对此有一句口诀:编译看左边,运行看右边。比如一个父类引用变量调用子类特有的方法那编译时候就会报错的。如果编译通过了,那么真正运行时会是右边真正在内存创建的对象的方法。(可以类比C# 、C++ 中的虚函数来理解,它们中没有用virtual修饰的普通方法是按照左边引用判断的,虚函数则是看右边指向对象来判断的。)
public class Fu {
public void method() {
System.out.println("父类");
}
public void methodFu() {
System.out.println("子类没有重写的方法");
}
}
public class Zi extends Fu{
@Override
public void method() {
System.out.println("子类方法!");
}
}
public class MultiDemo1 {
public static void main(String[] args) {
//使用多态的写法,左侧父类的引用,指向了右侧子类的对象。
Fu obj = new Zi();
// 还是右侧new 出来的子类
obj.method();
// 编译看左边,运行看右边
obj.methodFu();
}
}
我们来看下这个很简单的例子,父类引用指向子类创建的对象,这个的执行结果编译通过后,执行子类重写父类后的方法,如果子类找不到,则向上找父类。没错这就是多态!这个父类引用指向不同子类对象可能会有多种状态。
3.2 多态的好处
- 无论右边 new 哪一个子类对象,等号左边引用调用方法都不会变化。
- 在实际开发中,父类类型作为方法形参,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。(不用每一个子类都要定义方法)后面举个例子,先把上下转型说一下。
3.3 向上转型
其实就是多态写法。(类似基本数据类型的自动类型转换,小范围 --> 大范围)
Animal animal = new Cat(); // 父类 对象名= new 子类名();
右边创建一个子类对象,把它当作父类来看待使用。 没毛病!!!(猫是一个动物)
ps:向上转型一定是安全的。
3.4 向下转型(还原)
格式: 子类名称 对象名 = (子类名称) 父类引用变量
将父类对象,还原成为本来的子类对象 (有点强转那味了)
注意:向下转型时务必要加 instanceof 判定是否是原类对象!
综合之前接口和多态的例子:
我们的笔记本电脑通过 USB 接口将鼠标、键盘外界USB设备接入。
简易类和接口设计:设计USB接口,鼠标键盘为USB接口实现类,笔记本电脑定义一个方法参数为USB接口。
public interface USB {
public abstract void open();
public abstract void close();
}
public class Mouse implements USB {
@Override
public void open() {
System.out.println("打开鼠标");
}
@Override
public void close() {
System.out.println("关闭鼠标");
}
public void click() {
System.out.println("点击鼠标");
}
}
public class Keyboard implements USB {
@Override
public void open() {
System.out.println("打开键盘");
}
@Override
public void close() {
System.out.println("关闭键盘");
}
// 这是接口没有的方法,是实现类独有的方法
public void click() {
System.out.println("点击鼠标");
}
}
public class Computer {
public void powerOn() {
System.out.println("笔记本电脑开机!");
}
public void powerOff() {
System.out.println("笔记本电脑关机");
}
// 使用 USB 设备的方法,使用接口作为方法的参数(是完全可以的!)(也是数据类型,完全可以,实现接口的类都可以当作参数传递进来)
public void useDevice(USB usb) {
usb.open(); // 打开设备
usb.close(); // 关闭设备
// usb.click(); 这是错误的,接口里面没有,实现类独有的
if (usb instanceof Mouse) { // 一定要先判断
Mouse mouse = (Mouse) usb; // 向下转型转化为原来的实现类对象
mouse.click();
} else if (usb instanceof Keyboard) {
((Keyboard) usb).click(); // 匿名的向下转型
}
}
}
public class DemoMain {
public static void main(String[] args) {
Computer computer = new Computer();
computer.powerOn();
// 准备一个鼠标
Mouse mouse = new Mouse();
computer.useDevice(mouse); // 相当于自动实现了类型转换,实现类向接口可以安全地转换
// 多态写法,首先进行上转型
USB keyboard = new Keyboard(); // 这个直接显式地将实现接口的类转换成接口
computer.useDevice(keyboard); //正好传递一个USB对象
// computer.useDevice(new Keyboard()); 直接使用子类匿名对象,也可以达到。上面两种都是这样
computer.powerOff();
}
}
这个例子用到了接口的多态,也就是说接口的引用变量指向实现类创建的对象。同时也体现了多态的便利,将接口数据类型作为方法参数,无论是哪个实现类对象都可以传进来。说到这就不得不说一下匿名内部类的概念。
接口名称 对象名 = new 接口名称() {
//大括号里面的是一个类
//接口中所有抽象方法的实现
}
适用于:
匿名内部类是省略了[实现类/子类], 匿名对象是省略了对象名,匿名——唯一使用(当接口实现类只需要用一次,匿名对象只用一次也可匿名)
匿名内部类和匿名对象不是一回事儿!
突然想起个知识点作个记录: final修饰的引用类型变量地址不能再更改,但是可通过set方法进行更改里面的内容。
至此,面向对象程序设计思想以及面向对象三大特性已经大概地过了一下,最近过了遍 OOP ,特以此篇文章记录整理下心得。方便以后查看以及分享大家阅读。
参考文章及链接:
多态及其实现机制:https://www.jianshu.com/p/68ddb5484ca2