Java基础之剖析面向对象的三大特性:封装、继承、多态

封装

封装也被称为数据隐藏,在面向对象程序设计中,是将对象特有的实例域(也就是对象的属性)隐藏私有化, 外界不能直接访问到对象的属性,只能通过对象提供的属性方法访问。举个简单的例子:

如下代码:一个Girl类,有三个私有属性,姓名、年龄、体重,别的人(类)是无法获取这三个属性的,但是提供了三个set方法,可以设置Girl对象的姓名年龄、体重和身高。但是对于一个女生来说,体重和年龄是不能随便说出来的,所以并没有提供对应的get方法,所以是无法获取体重和年龄的。

package cn.test;

public class Girl {

    //姓名
    private String nane;
    //年龄
    private String age;
    //体重
    private String weight;

    public void setNane(String nane) {
        this.nane = nane;
    }

    public void setAge(String age) {
        this.age = age;
    }

    public void setWeight(String weight) {
        this.weight = weight;
    }

    public String getNane() {
        return nane;
    }
}

对比:

如果代码这么写的话,那女生的身高和年龄,任何人都可以知道(任何类都可以访问),而且也不符合javaBean的开发规范。

package cn.test;

public class Girl {

    //姓名
    public String nane;
    //年龄
    public String age;
    //体重
    public String weight;
     
}

 封装的真正内涵也就是把该隐藏的隐藏起来,该暴露的暴露出来,实现封装的好处:

  1. 隐藏类的实现细节:只提供暴露方法,避免了代码之间的乱引用,提高了重用性和可靠性;
  2. 设置默认属性:可以设置属性的默认值
  3. 可以数据进行校验,保证信息的完整性和合理性:加入属性校验,不合理的输入和访问直接拒绝掉,比如年龄是500岁;
  4. 利用修改,提高代码的可维护性:修改属性的字段类型时,只需要同步修改get和set方法。

Java的封装是通过访问控制符来控制的,一张图表就可以理解明白: 

 privatedefalutprotectedpublic
同一个类
同一个包 
子类中  
全局范围内   

 

在这里需要注意的是类的属性一般都要定义为私有属性也就是private,当然常量除外;在类中的有些方法起到辅助作用,但是不想被其他类调用的话,也应该使用private修饰;如果在一个父类中,方法需要被子类重写,但是又不想让被其他类调用,应该使用protecqted修饰;还有就是其他类可以任意调用的话,就可以用public修饰。

继承

Java只支持单继承,所有一个类只能同时拥有一个父类。继承描述的是一个"is -a"关系,如果A继承了B,可以描述为“A是B”。继承有三个特点:

  1. 子类拥有父类对象的所有属性和方法,父类的私有属性虽然无法访问,但是拥有;
  2. 子类可以拥有自己的属性和方法,也就是可以扩展父类;
  3. 子类可以用自己的方式实现父类的方法(也就是重载和重写)。

 举个例子:

下面代码中提供了三个类,一个动物类是父类,两个子类人类和黑猩猩类,还有一个测试类。父类中,还有三个属性:年龄、体重、身高,两个动作特性:吃和喝;人类的类中,有自己的属性:语言,自己的动作特性:驾驶,这就是对父类做了扩展,还重写了父类的吃的特性。而黑猩猩类中只是重写了父类吃的特性。在测试类中,发现,声明子类People时,子类可以调用父类的操作属性的方法,同时还可以调用父类的动作特性,说明子类拥有父类的所有属性和方法,及时属性是私有的;还能看到People调用自己重写父类的方法,但是方法的具体实现和父类却不一样。

/**
 * 动物的公共属性
 */
public class Animal {
    private String age;//年龄
    private String weight;//体重
    private String height;//身高
    /**
     * 吃的技能
     * @param str
     */
    public void eat(String str){
        System.out.print("吃:" +str);
    }
    /**
     * 喝的技能
     * @param str
     */
    public void drink(String str){
        System.out.print("喝:" +str);
    }

/**
 * 人类
 * 除了保持动物的基本特征,
 * 还有自己区别动物的地方
 * 比如开车:
 */
public class People extends Animal{
    /**
     * 独有的属性
     * 语言
     */
    private String language;
    /**
     * 独特的特性开车
     * @param car
     */
    public void drive(String car){
        System.out.print("驾驶:" +car);
    }

    /**
     * 重写父类方法eat
     * @param food
     */
    @Override
    public  void eat(String food){
        System.out.print("在座位上吃 :" +food);
    }

/**
 * 黑猩猩
 * 保持动物的特征
 */
public class Chimpanzee extends Animal{
    /**
     * 重写父类方法eat
     * @param food
     */
    @Override
    public  void eat(String food){
        System.out.print("在树上吃 :" +food);
    }
}

//测试类
public class Test {
    public static void main(String[] args) {
        People people = new People();
        people.setAge("23");
        people.setLanguage("汉语");
        people.eat("肯德基");
        people.drive("小轿车");
    }
}

关于构造器:

构造器是不能继承的,但是Java关于构造器有明确的规范:如果子类的构造器没有显示的调用父类的构造器,则将会自动调用默认无参的构造器(类本身会有默认一个无参构造器),如果父类中没有不带参数的构造器,并且子类的构造器没有显示的调用父类构造器的话,那么是无法通过编译的。这也说明了继承强耦合的特点(构造器不支持重写,支持重载)。

通过super显示调用父类构造器,回忆下super和this的作用:super有两个用途:1.调用父类的方法,2。调用父类构造器;this:1.引用隐式参数 2.调用其他构造器。super和this的作用域都是在子类中,但是调用对象不同,super调用对象是父类,this调用对象是子类。

来看下例子,给父类Animal和子类Peopleg各增加一个显示的无参构造方法

public class Animal {

    private String age;//年龄
    private String weight;//体重
    private String height;//身高

    public Animal( ){
        System.out.print("父类初始化。。。"+"\n");
    }
public class People extends Animal{

    public People( ){
        System.out.print("子类初始化。。。"+"\n");
    }

 再执行Test类中的main方法,看打印结果可以看出,父类的加载是在子类加载之前。

父类初始化。。。
子类初始化。。。
在座子上吃 :肯德基
驾驶:小轿车

当然了为了更加明确基础关系,你可以在父类的方法上将方法修饰为 protected,这样一来,非同包下的非子类就不能任意父类的中的方法,可以使继承关系更加稳健。

聊聊向上转型:

向上转型就是子类转型成父类,在继承图上向上移动,因此被称为向上转型,向上转型转型最直观的遍历就是提高了代码的复用性,不用每增加一个子类就新增一套方法,只要坚守住该类是父类的子类就可以执行调用,这也更能体现出两个类之间的关系。

比如给父类增加一个getInfo方法,入参为父类本身;修改一下测试类,People和Chimpanzee分别调用这个方法并传入本身

    /**
     * 信息
     * @param animal
     */
    protected  void getInfo(Animal animal){
        System.out.print("年龄"+animal.age+"身高"+animal.height+"\n");
    }

//测试类
public class Test {
    public static void main(String[] args) {
        People people = new People();
        people.setAge("23");
        people.setHeight("185");
        people.getInfo(people);
        Chimpanzee chimpanzee = new Chimpanzee();
        chimpanzee.setAge("5");
        chimpanzee.setHeight("193");
        chimpanzee.getInfo(chimpanzee);
    }
}

打印结果:在父类只新增了一个方法,但是任意子类都可以调用

年龄:23身高:185

年龄:5身高:193

继承的缺点 

一直在强调继承的优点,虽然继承在编码和设计上带来了很大的便利,但是也存在的响应的缺点:

  1. 继承是“is-a”的一种关系,是一种强耦合的关系,父类发生变化时,子类也需要跟着变化,所有多变或者不稳定的业务场景不建议使用继承
  2. 继承打破了封装的严谨性,封装的意义本身就是为了保证一个类的安全性和隐藏性,不过继承恰恰打破了这种平衡。

 那什么时候需要使用继承,什么时候不能用继承,还需要根据场景而定,这里只是建议慎用。

多态

多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

实现多态有三个必备的条件:

  1. 继承
  2. 方法重写
  3. 向上转型 

多态有两种实现方式,一种是基于继承,一种是基于接口实现

我们修改下上面的例子,已经子类People重写了父类Animal的中的eat方法

public class Test {
    public static void main(String[] args) {
        Animal people = new People();
        people.eat("肯德基");
    }

输出结果: 

在座子上吃 :肯德基

 上面的例子只是简单应用,来看一个经典的例子:

父类为A,A中show(D obj)和show(A obj)是重载关系;B继承了A,B的show(B obj)重载了A的show(D obj)或者show(A obj)方法,同时也show(A obj)重写了A的show(A obj),,B中show(B obj)和show(A obj)也是重载关系;C、D都继承了B,是B的子类。

public class A {
    public String show(D obj) {
        return ("A and D");
    }

    public String show(A obj) {
        return ("A and A");
    }

}

public class B extends A{
    public String show(B obj){
        return ("B and B");
    }

    public String show(A obj){
        return ("B and A");
    }

}

public class C extends B{
    
}

public class D extends B{
    
}

public class Test2 {
    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new B();
        B b = new B();
        C c = new C();
        D d = new D();

        System.out.println("1--" + a1.show(b));
        System.out.println("2--" + a1.show(c));
        System.out.println("3--" + a1.show(d));
        System.out.println("4--" + a2.show(b));
        System.out.println("5--" + a2.show(c));
        System.out.println("6--" + a2.show(d));
        System.out.println("7--" + b.show(b));
        System.out.println("8--" + b.show(c));
        System.out.println("9--" + b.show(d));
        System.out.println("10--" + b.show(a1));

    }
}


输出结果:

1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D
10--B and A

分析代码前先来回顾下重载和重写

重载和重写是在多态特性的最基本的特征,那如何确定正确的目标方法?这里又引出一个新的概念,将一个方法调用同一个方法主体关联起来被称为绑定,绑定又分为前期绑定和后期绑定(也就是动态绑定、运行时绑定),也就是只有程序运行期间才能确定一个引用变量到底会指向哪个类的实现方法,这也就体现了多态性。前期绑定和后期绑定也被称为静态分派和动态分派。静态绑定典型应用就是重载,而动态绑定的典型应用是重写。

使用哪个重载版本:

首先选定对应参数类型的方法,当重载方法中没有对应参数类型的方法,那将在继承的关系中从下往上开始搜索,越接近上层优先级越低

例如:从1、2、3可以看出,当传输入参数有对应类型时,3执行的结果就是对应的方法,但是1,2没有对应的参数时,会向上转型为父类,查找对应的方法。

调用哪个重写方法

声明子类,调用方法时,先查子类没有该方法,如果没有子类没有该方法,在继承的关系中从下往上开始搜索,依次查找;当没有对应参数类型方法时,则参数向上转型为父类再依次查找;如果子类重写父类方法时,优先调用子类重写父类的方法;

例如:7.8.9.10中,重点说下8,9,10。8的调用逻辑中,B没有show(C obj),B的父类A也没有,则C向上转型为B,在B类中找到对应方法,9的调用逻辑中,B中没有show(D obj),而B的父类A中存在;10的调用逻辑中,B类和父类A都存在show(A obj)方法,优先调用B类重写A类的方法。

父类声明子类实现

以优先查找父类方法中,如果子类重写父类方法,则优先调用子类方法

1 A a2 = new B();
2   a2 = new C();

如代码1,根据代码的字面意思,把"A"被称为静态类型或者就外观类型,而后面的“B”则是变量的实际类型,两者在程序中都可以发生变化,静态类型类型可以仅发生在使用时,变量本身不会改变,并且时可知的,而实际变量的变化只有在运行期期才可以确定(这里只的虚拟机)。

例如:4,5,6.在4,5的调用逻辑中,首先在A中查找,如果A中没有找到方法,参数向上转型为A,而子类重写了show(A objf)方法,则调用B类中的方法返回找到方法返回;6中在A中找到方法,直接返回。

参考博客:http://blog.csdn.net/thinkGhoster/archive/2008/04/19/2307001.aspx

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值