面向对象三大原则总结

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的关系的,实现关系;

posted @ 2022-04-01 14:08  写的代码很烂  阅读(377)  评论(0编辑  收藏  举报