面向对象三大原则总结
1、简单概述
java中的三大特性:封装、继承、多态
2、封装
现实生活中存在着大量的被封装的例子。比如说手机、空调、电视机等等。手机通过屏幕上的显示点击触屏即可操作,空调和遥控器等通过遥控器来进行操作,将功能封装到遥控器上的按键上面,屏幕了电视机和空调内部的复杂结构和实现原理。
对于使用者来说,不需要关心内部有多么复杂,只需要关注的对应的功能能够实现即可。
所以总结起来,两个重要作用:
- 1、保证内部结构的安全性;
- 2、屏蔽复杂,暴露简单;
在代码级别上,封装有什么作用?
一个类当中的数据在封装了之后,对于代码的调用人员来说,不需要关注代码中的复杂实现,只是对外提供相应的API操作入口就可以实现对应的功能。
将类体中安全级别较高的数据封装起来,外部人员不能够随意访问,用来保证类中内部数据的安全性。
从这里就可以看到所谓的封装就可以简单的理解成是java中的方法。
所以最常见的就是一个Javabean,成员属性用private关键字来进行修饰,通过public修饰的方法来暴露操作数据的API而已。
3、继承
继承是类和类之间的关系;继承是通过已经存在的类作为基础,从而建立起来新的类;所以继承中的知识就是围绕着继承来进行展开的。
首先需要进行声明的是:
- 1、如果java中的一个类没有显示的有继承关系,也就是没有extends关键字,那么默认的继承的是Object类。
- 2、继承除了构造函数不能够继承之外,私有方法也可以可以继承的,只不过不能够访问父类中的私有成员方法。
3.1、继承的作用:
基本作用:提高代码复用性;
主要作用:为实现方法覆盖和多态机制做铺垫;
3.2、什么时候考虑使用继承
最简单的方式就是是否符合" is a "的关系。如下所示:
- Cat is a Animal
- Dog is a Animal
- CreditAccount is a Account
可以使用这个衡量标准来进行衡量。
如果说有A、B两个类有共性内容,但是不符合A is a B或者是B is a A的这种使用特性,那么就没有必要使用父类来进行共性抽取了。
在之前的java封装中写过一个标准的javabean类,但是还是有些细节没有描述清楚
public class Person {
private Integer id;
private String username;
public Person() {
}
public Person(Integer id, String username) {
this.id = id;
this.username = username;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", username='" + username + '\'' +
'}';
}
}
可以从上面看到类上没有extends关键字,在java中默认给省略了,其实是继承了Object类的。关键没有显示的写,但是最终依然会使用到其中的方法
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
// 这个也是经常使用到的方法
public final native Class<?> getClass();
// 下面两个方法是常用的方法
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
protected native Object clone() throws CloneNotSupportedException;
// 最常用的方法。但是这个方法通常都是要被重写的,因为这个原始方法没有太大的参考意义
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
// 线程中的方法
public final native void notify();
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
// 垃圾回收调用的方法
protected void finalize() throws Throwable { }
}
3.3、方法重写
什么时候考虑方法重写
子类继承父类之后,当父类中的方法不足以满足子类中的业务需求的时候,那么这个时候子类就可以来对父类中的方法来进行重写。
将方法重写和方法重载区分开来
方法重载需要满足三个条件:1、在同一个类中;2、方法功能类似,方法名称相同;2、参数(顺序、数据类型、个数)
方法重写的条件:1、存在继承关系;2、父类业务方法不满足子类需求,需要进行重写;
注意:方法覆盖只是针对成员方法而已,而不是静态方法
定义一个动物类:
public class Animal {
public void fly(){
System.out.println("动物在飞翔");
}
}
Bird符合Bird is a Animal的特性,所以使用继承
public class Bird extends Animal {
}
public class Test {
public static void main(String[] args) {
Bird bird = new Bird();
bird.fly();
}
}
控制台打印:动物在飞翔
但是鸟儿应该有自己的飞翔方式,所以父类中的fly方法满足不了子类的方式,考虑重写
public class Bird extends Animal {
@Override
public void fly() {
System.out.println("鸟儿在飞翔");
}
}
那么再次进行输出打印:鸟儿在飞翔
方法重写的注意事项
1、必须存在继承关系且方法名称必须相同且参数列表必须相同;
2、子类和父类返回值类型相同或者子类有更大的返回值类型;
3、子类访问权限修饰符不能更低,可以是大于或者等于;
4、重写之后的方法不能比重写之前的方法抛出更多的异常,也就是说小于或者等于;
二者时间的关系无非就是:>=,=,=<这三个关系;
=:表示的是相同的方法名称,参数列表相同;
》=:表示的是重写之后的数据类型可以大于等于重写之前的数据类型;
《=:表示的是重写只有的方法不能比重写之前的方法抛出更多的异常;
4、多态
一个对象在不同阶段展现出来的不同形态,多种状态。在编译期间和运行期间有不同的状态。
编译期间叫静态绑定;运行期间叫动态绑定。
Animal cat = new Cat();
cat.move();
编译期间,编译器发现cat的类型是Animal,那么会先去查找Animal中的move方法是否存在,能够进行调用。如果存在,则静态绑定成功;
运行期间,因为cat是一个引用,在运行期间才会真正的去堆中找到对象的move方法来进行执行。所以在运行期间才是真正去调用对象的方法来进行运行的。
4.1、多态在开发中的作用
从一个例子中来看:
V1版本
/**
* 主人给宠物喂食
*/
public class Master {
public void feed(Dog dog){
dog.feed();
}
}
创建动物类:
public class Dog {
public void feed(){
System.out.println("狗吃屎");
}
}
对于的测试类:
/**
* 主人给宠物喂食
*/
public class Master {
public void feed(Dog dog){
dog.feed();
}
}
这样子写是没有毛病的。但是如果过了一段时间,主人不去养狗了,而去喜欢养猫、养嘤嘤怪了。
那么对应在软件行业中,是因为客户产生了新的需求。作为软件开发人员,就应该来满足客户新的需求。那么这样子如何来进行扩展呢?
不使用多态机制的前提下,目前我们只能够在Master类中添加一个新的方法。
比如说是喂猫的案例:
public class Cat {
public void feed(){
System.out.println("猫吃鱼");
}
}
再来一个案例:
/**
* 主人给宠物喂食
*/
public class Master {
/**
* 狗吃屎
* @param dog
*/
public void feed(Dog dog){
dog.feed();
}
/**
* 猫吃鱼
* @param cat
*/
public void feed(Cat cat){
cat.feed();
}
}
其实写到这里,应该想到一些什么了。既然两个都是功能类似的方法,是方法重载。那么是否能够抽象出来一个父类出来?
但是抽象出来的父类,抽象的如果仅仅只是方法而已的话,那么参数如何来进行确定呢?这个先暂时放到一边
从上面可以看到,在软件扩展中,对于Master类的代码要修改的越多越好。因为可能说其他的功能代码已经上线了,已经可以非常稳定的来进行运行了,不可能因为修改大量的代码而导致当前的系统出现问题。因为修改的越多,就可能导致未知的风险越多。
其实这里涉及到一个软件开发原则:开闭原则(OCP原则)。对扩展开放,对修改关闭,在软件开发中,修改的代码应该保证越少越好。
所谓的对扩展开放,指的是:可以额外进行添加;
所谓的对修改关闭,指的是:最好不要修改已有的代码程序;
所以针对上面的案例中,不应该来对应Master来进行修改,而是将Cat和Dog抽象成一个抽象类Pet,让Master不再去面向具体的类编程,而是面向抽象编程。
V2版本
public class Pet {
public void feed(){
System.out.println("宠物要吃东西");
}
}
两个子类:
public class Dog extends Pet{
public void feed(){
System.out.println("狗吃屎");
}
}
public class Cat extends Pet {
public void feed(){
System.out.println("猫吃鱼");
}
}
对应的主人类:
public class Master {
public void feed(Pet pet){
pet.feed();
}
}
对应的测试类:
public class Test {
public static void main(String[] args) {
Master master = new Master();
Dog dog = new Dog();
master.feed(dog);
Cat cat = new Cat();
master.feed(cat);
}
}
那么多态机制是在哪里展现的呢?
是在方法上,因为父类中的方法是Pet pet 类型,而子类传入进来的是子类实际对象,如下所示:
master.feed(Pet pet = new Dog)
master.feed(Pet pet = new Cat)
这里就展现出来了多态的好处和优势。
版本总结
如果说将来又有什么新的扩展,那么主人样的是鳄鱼,嘤嘤怪等,那么现在只需要实现Pet类重写其中的方法即可,不需要来修改Master类,从而就能够达到扩展实现的效果。
这就是对扩展开放,对修改时关闭的。
public class YingWu extends Pet{
@Override
public void feed() {
System.out.println("喂食鹦鹉");
}
}
对应的测试类:
public class Test {
public static void main(String[] args) {
Master master = new Master();
Dog dog = new Dog();
master.feed(dog);
Cat cat = new Cat();
master.feed(cat);
YingWu yingWu = new YingWu();
master.feed(yingWu);
}
}
不建议面向抽象编程,而是面向抽象编程,为了更高的扩展性。
因为面向具体编程会让软件的扩展性很差,而面向抽象编程可以给我们留一线空间,方便将来我们更好的编程扩展。
降低程序的耦合度,提高程序的扩展力。也从例子中来看一下:
public class Master{
public void eat(Dog dog){}
public void eat(Cat cat){}
}
如果这样子来写的话,那么Master将会强依赖Dog类和Cat类,三者之间的关系很紧密,也就是耦合度很高。
但是如果如下缩写:
public class Master{
public void eat(Pet pet){}
}
这个时候Master就将会只依赖Pet,而不会依赖Cat和Dog。事实上对于Master来说,它是感知不到Dog和Cat的。
这就是降低类和类之间的耦合度。
4.2、面向抽象编程
既然是面向抽象编程,那么再来拿上面的例子来:
master.feed(Pet pet = new Dog)
master.feed(Pet pet = new Cat)
那么这里的Pet pet能够写成Object?
答案是不可以。为什么?如果是Object,难道就不够抽象么?
确实是够抽象,如果这里写了eat方法,那么因为Object类中是没有eat方法的。如果这个时候想要来进行使用的话,那么就必须要用instanceof 进行向下转型,那么这个时候有需要考虑到多种情况,利用if判断,如下所示:
public void feed(Object obj){
if (obj instanceof Cat){
Cat cat = (Cat) obj;
cat.feed();
}
if (obj instanceof Dog){
Dog dog = (Dog) obj;
dog.feed();
}
}
如果业务还有扩展,那么会这样继续衍生下去,又会造成耦合。所以没有必要这样子来进行操作。
面向对象编程
封装、继承、多条:环环相扣
因为有了封装,对象有了一个整体的概念;对象和对象之间产生了继承,才有了方法的重写和多态。
5、抽象类
5.1、什么是抽象类
对象是我们现实生活中存在的,真正存在的事物。而类是现实生活中不存在的东西,只不过是人的大脑思维总结出来的产物而已(总结了一系列相同事物共同特征的模板)。而抽象类又是类的抽象,很明显,不是一个类的抽象,而是多个类的抽象。
类是可以创建对象的,因为是相同事物的模板。但是多个类的抽象之后的类,是无法创建出来实际生活中存在的对象的。
所以也就导致了抽象类无法创建对象。
6、接口
6.1、基础语法
在jdk8中,对于接口中的方法来说,并非都是公共的抽象方法了,jdk8接口新增了静态方法和默认方法。
那么也就是说,可以写三种方法:
// 抽象方法
void abstranctMethod();
// static修饰符定义静态方法
static void staticMethod() {
System.out.println("接口中的静态方法");
}
// default修饰符定义默认方法
default void defaultMethod() {
System.out.println("接口中的默认方法");
}
静态方法意味着我们可以直接通过接口名称来进行调用,跟普通的类调用静态方法一样;
默认方法意味着子类可以根据自己的需要进行实现。子类有两种方式:1、可以实现;2、也可以不实现;默认方法留给了子类更高的扩展性,比如说常见的拦截器中的接口,现在就是使用默认方法来进行操作。
默认方法的作用在哪里?我个人觉得为了减少类和类之间的复杂度。
如果接口中有一个方法,现在有两个实现类,如果需求扩展中,需要新添加一个方法,但是这个方法两个类都可能使用到。
那么这个时候是选择两个类中都来进行实现?还是说抽象出来一个抽象类,来进行实现。
这两种方式选择都不好,对于第一种,无疑是代码从新copy;对于第二种,仅仅只为了部分方法而进行抽取。
而新添加的默认方法,对于子类来说,可以选择实现或者是不实现。
6.2、需求分析
分析一个小案例:我中午去餐馆吃饭
接口是抽象的。
哪个是接口?
菜单是一个接口
谁面向接口调用?
我对着菜单点餐
谁负责实现接口?
厨房里面的厨师负责来进行实现。
接口有什么用?
菜单让顾客和后厨解耦合了,顾客不需要找后厨,后厨不需要找顾客确定点什么菜。
顾客和厨师面向菜单进行沟通。
接口的使用离不开多态机制。
public interface FoodMenu {
void xiHongShiChaoJiDan();
void yuXiangRouSi();
}
public class ChineseCooker implements FoodMenu {
@Override
public void xiHongShiChaoJiDan() {
System.out.println("中式西红柿鸡蛋");
}
@Override
public void yuXiangRouSi() {
System.out.println("中式鱼香肉丝");
}
}
public class EnglishCooker implements FoodMenu {
@Override
public void xiHongShiChaoJiDan() {
System.out.println("英式西红柿鸡蛋");
}
@Override
public void yuXiangRouSi() {
System.out.println("英式鱼香肉丝");
}
}
/**
* Customer has a foodMenu
* 具有has a 关系,都要以以属性关系组织
* 具有 is a 关系的,使用继承
*/
public class Customer {
private FoodMenu foodMenu;
public Customer(FoodMenu foodMenu) {
this.foodMenu = foodMenu;
}
public void order(){
foodMenu.xiHongShiChaoJiDan();
foodMenu.yuXiangRouSi();
}
}
public class Test {
public static void main(String[] args) {
FoodMenu foodMenu = new ChineseCooker();
Customer customer = new Customer(foodMenu);
customer.order();
FoodMenu foodMenu1 = new EnglishCooker();
Customer customer1 = new Customer(foodMenu1);
customer1.order();
}
}
6.3、接口在开发中的作用
有没有发现到,对于调用者来说,面向接口编程使用过于简单,屏蔽掉了底层的复杂实现逻辑。
有接口就会有多态的体现,接口就是统一这个方法的功能,而没有具体的实现,具体的实现是在实现类中来进行实现的。
任意一个接口都有自己的调用者和实现者,解耦合是将调用者和实现者分离开。
调用者面向接口调用;实现者面向接口实现;
各自做各自的事情,Customer只需要面向FoodMenu进行编程,而FoodMenu有不同的实现方式,在进行调用的时候,传入不同的实现类来进行实现,调用不同的逻辑实现。
6.4、思考service为什么需要设计接口
使用接口的好处自然而然的可以知道是解耦合。但是解耦合的前提是有多个实现类,将调用者和具体的实现者进行分离。
但是对于controller、service、dao层代码来说,service通过设计成一个service接口和一个service接口实现类,那么对于有一个实现类的,还需要有service接口这个设计的必要么??
所谓的service就是一个功能实现的逻辑,一个主体实现的逻辑都应该要在service中来进行实现。所以这个实现就显得有些复杂,但是对于上层调用者来说,controller层是不需要关注service具体实现逻辑的,仅仅只是调用就能够拿到我想实现的结果而已。
我们假设一下,如果三个人分别来实现controller、service、dao的话,首先dao已经有类似mybatis的框架已经屏蔽复杂性解决了。
但是对于controller层来说,做参数校验、调用具体的实现逻辑等简单方式。
所以这就涉及到了具体的责任划分,如果不同的人来说,那么将接口设计好之后,不同的人分工,各自做各自的事情,面向接口开发。
controller面向service接口进行编程,service的实现类来进行具体的实现,双方都不会去影响对应的逻辑。系统开发效率就会变快。
这个时候使用Service接口?是让表示层不依赖于业务层的具体实现。
但是往往controller、service、dao都是一个人来进行开发的且service都只有一个实现类,那么这个时候还有必要么?
这么一看,似乎servie接口是多余的。但是我从博客中看到一些描述信息,如下:
1、springaop是jdk动态代理,以前是要写接口,现在人把这种恶心保存了下来,根本不明白其中的道理,现在不写接口完全可以
2、为什么要用Service接口?是让表示层不依赖于业务层的具体实现。service接口有多个实现的话是对的,但实际项目中service通常只有一个实现,根本没有必要写接口
似乎一切都变得逐渐清晰起来,但是是否真的不需要service接口呢?
那么看到这里,如何来进行选择呢?我觉的从两个角度出发:
先从工序上说,你在写上一层的时候,会用到下一层提供的逻辑,具体表现形式就是各种各样的service类和里面的方法。上一层开搞的时候,就一定会知道下一层会干什么事,比如“将传入编号对应的人员信息设置为离职”,但下一层的代码不一定已经一行一行写出来了。所以这会儿需要有个接口,让写上层代码的人先能把代码写下去。有各种理由可以支持这种工序的合理性,比如一般来说,上一层的一行代码会对应下一层的好多行代码,那先让写上层代码的人写一遍,解决高端层面的bug,会提高很多效率。
再从抽象角度说,不同业务模块之间的共用,不一定是共用某段代码,也可能是共用某段逻辑框架,这时候就需要抽象一个接口层出来,再通过不同的注入逻辑实现。比如模块1是登记学生信息,模块2是新闻发布,看上去风马牛不相及。但分析下来如果两个模块都有共同点,顺序都是1、验证是否有权限 2、验证输入参数是否合法 3、将输入参数转化为业务数据 4、数据库存取 5、写log,那就可以写一个service接口,里面有上述5个函数,再分别写两个service实现。具体执行的时候,通过各种注入方法,直接new也好,用spring注入也好,实现不同的效果。
最终从设计模式出发,为什么要用Service接口和DAO接口?我们还得回到最基本的面向对象设计原则上去。
面向对象设计原则中有三条与此相关:开闭原则、依赖倒转原则、理氏替换原则。还记得依赖倒转原则吧?高层不依赖于低层,二者都依赖于抽象,也就是面向接口编程。
为什么要用Service接口?是让表示层不依赖于业务层的具体实现。
为什么要用DAO接口?是让业务层不依赖于持久层的具体实现。有了这两个接口,Spring IOC容器才能发挥作用。
举个例子,用DAO接口,那么持久层用Hibernate,还是用iBatis,还是 JDBC,随时可以替换,不用修改业务层Service类的代码。
使用接口的意义就在此。
小结
从产品角度上来说,我们有可能是会面临着将来需要进行扩展业务的需求的;
从设计原则上来说,我们需要面向抽象编程,不要面向具体编程,方便扩展;
从程序扩展上来说,更利于开发和扩展;
7、is a、has a、like a
dog is a animal,凡是能满足is a的关系的,继承关系;
i has a pen,凡是能够满足has a的关系的,关联关系;
cooker like a menu:凡是能够满足like a的关系的,实现关系;