Java工程师成神之路基础篇之封装、继承、多态

什么是多态

什么是多态

多态的概念呢比较简单,就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。

如果按照这个概念来定义的话,那么多态应该是一种运行期的状态。

多态的必要条件

为了实现运行期的多态,或者说是动态绑定,需要满足三个条件:

  • 有类继承或者接口实现
  • 子类要重写父类的方法
  • 父类的引用指向子类的对象

简单来一段代码解释下:

public class Parent{
    
    public void call(){
        sout("im Parent");
    }
}

public class Son extends Parent{// 1.有类继承或者接口实现
    public void call(){// 2.子类要重写父类的方法
        sout("im Son");
    }
}

public class Daughter extends Parent{// 1.有类继承或者接口实现
    public void call(){// 2.子类要重写父类的方法
        sout("im Daughter");
    }
}

public class Test{
    
    public static void main(String[] args){
        Parent p = new Son(); //3.父类的引用指向子类的对象
        Parent p1 = new Daughter(); //3.父类的引用指向子类的对象
    }
}

这样,就实现了多态,同样是Parent类的实例,p.call 调用的是Son类的实现、p1.call调用的是Daughter的实现。

有人说,你自己定义的时候不就已经知道p是son,p1是Daughter了么。但是,有些时候你用到的对象并不都是自己声明的。

比如Spring 中的IOC出来的对象,你在使用的时候就不知道他是谁,或者说你可以不用关心他是谁。根据具体情况而定。

IOC,是Ioc—Inversion of Control 的缩写,中文翻译成“控制反转”,它是一种设计思想,意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。
换句话说当我们使用Spring框架的时候,对象是Spring容器创建出来并由容器进行管理,我们只需要使用就行了。

静态多态

上面我们说的多态,是一种运行期的概念。另外,还有一种说法,包括维基百科也说明,多态还分为动态多态和静态多态。

上面提到的那种动态绑定认为是动态多态,因为只有在运行期才能知道真正调用的是哪个类的方法。

很多人认为,还有一种静态多态,一般认为Java中的函数重载是一种静态多态,因为他需要在编译期决定具体调用哪个方法。

但是,其实作者认为,多态应该是一种运行期特性,Java中的方法重写是多态的体现。虽然也有人提出重载是一种静态多态的想法,这个问题在StackOverflow等网站上有很多人讨论,但是并没有什么定论。我更加倾向于重载不是多态。

这个是我们几个小伙伴建了的一个纯java交流qun735057581 , 大家有兴趣的可以加下,有什么问题都可以随手来交流分享,群文件我上传了我做Java这几年整理的一些学习手册,开发工具,PDF文档书籍教程,需要的话你们都可以自行下载

方法重写与重载

在这里插入图片描述
重载(Overloading)和重写(Overriding)是Java中两个比较重要的概念。但是对于新手来说也比较容易混淆。本文通过两个简单的例子说明了他们之间的区别。

定义

重载

简单说,就是函数或者方法有同样的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。

重写

重写指的是在Java的子类与父类中有两个名称、参数列表都相同的方法的情况。由于他们具有相同的方法签名,所以子类中的新方法将覆盖父类中原有的方法。

重载 VS 重写

关于重载和重写,你应该知道以下几点:

1、重载是一个编译期概念、重写是一个运行期间概念。
2、重载遵循所谓“编译期绑定”,即在编译时根据参数变量的类型判断应该调用哪个方法。
3、重写遵循所谓“运行期绑定”,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法
4、因为在编译期已经确定调用哪个方法,所以重载并不是多态。而重写是多态。重载只是一种语言特性,是一种语法规则,与多态无关,与面向对象也无关。(注:严格来说,重载是编译时多态,即静态多态。但是,Java中提到的多态,在不特别说明的情况下都指动态多态)

重写的例子

下面是一个重写的例子,看完代码之后不妨猜测一下输出结果:

class Dog{
    public void bark(){
        System.out.println("woof ");
    }
}
class Hound extends Dog{
    public void sniff(){
        System.out.println("sniff ");
    }

    public void bark(){
        System.out.println("bowl");
    }
}

public class OverridingTest{
    public static void main(String [] args){
        Dog dog = new Hound();
        dog.bark();
    }
}

输出结果:

bowl

上面的例子中,dog对象被定义为Dog类型。在编译期,编译器会检查Dog类中是否有可访问的bark()方法,只要其中包含bark()方法,那么就可以编译通过。在运行期,Hound对象被new出来,并赋值给dog变量,这时,JVM是明确的知道dog变量指向的其实是Hound对象的引用。所以,当dog调用bark()方法的时候,就会调用Hound类中定义的bark()方法。这就是所谓的动态多态性。

重写的条件

参数列表必须完全与被重写方法的相同;
返回类型必须完全与被重写方法的返回类型相同;
访问级别的限制性一定不能比被重写方法的强;
访问级别的限制性可以比被重写方法的弱;
重写方法一定不能抛出新的检查异常或比被重写的方法声明的检查异常更广泛的检查异常
重写的方法能够抛出更少或更有限的异常(也就是说,被重写的方法声明了异常,但重写的方法可以什么也不声明)
不能重写被标示为final的方法;
如果不能继承一个方法,则不能重写这个方法。

重载的例子

class Dog{
    public void bark(){
        System.out.println("woof ");
    }

    //overloading method
    public void bark(int num){
        for(int i=0; i<num; i++)
            System.out.println("woof ");
    }
}

上面的代码中,定义了两个bark方法,一个是没有参数的bark方法,另外一个是包含一个int类型参数的bark方法。在编译期,编译期可以根据方法签名(方法名和参数情况)情况确定哪个方法被调用。

重载的条件

被重载的方法必须改变参数列表;
被重载的方法可以改变返回类型;
被重载的方法可以改变访问修饰符;
被重载的方法可以声明新的或更广的检查异常;
方法能够在同一个类中或者在一个子类中被重载。

Java的继承与实现

前面的章节我们提到过面向对象有三个特征:封装、继承、多态。

继承可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。这种派生方式提现了传递性,在Java中,除了继承,还有一种提现传递性的方式叫做实现。

继承和实现两者的明确定义和区别如下:

继承(Inheritance):如果多个类的某个部分的功能相同,那么可以抽象出一个类出来,把他们的相同部分都放到父类里,让他们都继承这个类。
实现(Implement):如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准,让他们的实现这个接口,各自实现自己具体的处理方法来处理那个目标

继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。所以,继承的根本原因是因为要复用,而实现的根本原因是需要定义一个标准。

在Java中,继承使用extends关键字实现,而实现通过implements关键字。

特别需要注意的是,Java中支持一个类同时实现多个接口,但是不支持同时继承多个类。但是这个问题在Java 8之后也不绝对了。

简单点说,就是同样是一台汽车,既可以是电动车,也可以是汽油车,也可以是油电混合的,只要实现不同的标准就行了,但是一台车只能属于一个品牌,一个厂商。

class Car extends Benz implements GasolineCar, ElectroCar{

}

以上,我们定义了一辆汽车,他实现了电动车和汽油车两个标准,但是他属于奔驰这个品牌。像上面这样定义,我们可以最大程度的遵守标准,并且复用奔驰车所有已有的一些功能组件。

另外,在接口中只能定义全局常量(static final)和无实现的方法(Java 8以后可以有defult方法);而在继承中可以定义属性方法,变量,常量等。

Java为什么不支持多继承

前面我们提到过:“Java中支持一个类同时实现多个接口,但是不支持同时继承多个类。但是这个问题在Java 8之后也不绝对了。”

那么,是不是又很很想知道,为什么Java中不支持同时继承多个类呢?

多继承

一个类,只有一个父类的情况,我们叫做单继承。而一个类,同时有多个父类的情况,叫做多继承。

在Java中,一个类,只能通过extends关键字继承一个类,不允许多继承。但是,多继承在其他的面向对象语言中是有可能支持的。

像C++就是支持多继承的,主要是因为编程的过程是对现实世界的一种抽象,而现实世界中,确实存在着需要多继承的情况。比如维基百科中关于多继承举了一个例子:

例如,可以创造一个“哺乳类动物”类别,拥有进食、繁殖等的功能;然后定义一个子类型“猫”,它可以从父类继承上述功能。

但是,"猫"还可以作为"宠物"的子类,拥有一些宠物独有的能力。

所以,有些面向对象语言是支持多重继承的。

但是,多年以来,多重继承一直都是一个敏感的话题,反对者指它增加了程序的复杂性与含糊性。

菱形继承问题

假设我们有类B和类C,它们都继承了相同的类A。另外我们还有类D,类D通过多重继承机制继承了类B和类C。
在这里插入图片描述
这时候,因为D同时继承了B和C,并且B和C又同时继承了A,那么,D中就会因为多重继承,继承到两份来自A中的属性和方法。

这时候,在使用D的时候,如果想要调用一个定义在A中的方法时,就会出现歧义。

因为这样的继承关系的形状类似于菱形,因此这个问题被形象地称为菱形继承问题。

而C++为了解决菱形继承问题,又引入了虚继承。

因为支持多继承,引入了菱形继承问题,又因为要解决菱形继承问题,引入了虚继承。而经过分析,人们发现我们其实真正想要使用多继承的情况并不多。

所以,在 Java 中,不允许“实现多继承”,即一个类不允许继承多个父类。但是 Java 允许“声明多继承”,即一个类可以实现多个接口,一个接口也可以继承多个父接口。由于接口只允许有方法声明而不允许有方法实现(Java 8以前),这就避免了 C++ 中多继承的歧义问题。

但是,Java不支持多继承,在Java 8中支持了默认函数(default method )之后就不那么绝对了。

虽然我们还是没办法使用extends同时继承多个类,但是因为有了默认函数,我们有可能通过implements从多个接口中继承到多个默认函数,那么,又如何解决这种情况带来的菱形继承问题呢?

这个问题,我们在后面的Java 8部分单独介绍。

Java的继承与组合

在前面几篇文章中,我们了解了封装、继承、多态是面向对象的三个特征。并且通过对继承和实现的学习,了解到继承可以帮助我实现类的复用。

所以,很多开发人员在需要复用一些代码的时候会很自然的使用类的继承的方式。

但是,遇到想要复用的场景就直接使用继承,这样做是不对的。长期大量的使用继承会给代码带来很高的维护成本。

本文将介绍一种可以帮助我们复用的新的概念——组合,通过学习组合和继承的概念及区别,并从多方面帮大家分析在写代码时如何进行选择。

面向对象的复用技术

前面提到复用,这里就简单介绍一下面向对象的复用技术。

复用性是面向对象技术带来的很棒的潜在好处之一。如果运用的好的话可以帮助我们节省很多开发时间,提升开发效率。但是,如果被滥用那么就可能产生很多难以维护的代码。

作为一门面向对象开发的语言,代码复用是Java引人注意的功能之一。Java代码的复用有继承,组合以及代理三种具体的表现形式。本文将重点介绍继承复用和组合复用。

继承

前面的章节中重点介绍过继承,我们说继承是类与类或者接口与接口之间最常见的一种关系;继承是一种is-a关系。

is-a:表示"是一个"的关系,如狗是一个动物

在这里插入图片描述
组合

组合(Composition)体现的是整体与部分、拥有的关系,即has-a的关系。

is-a:表示"有一个"的关系,如狗有一个尾巴

在这里插入图片描述
组合与继承的区别和联系

在继承结构中,父类的内部细节对于子类是可见的。所以我们通常也可以说通过继承的代码复用是一种白盒式代码复用。(如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性;)
组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是黑盒式代码复用。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法)
继承,在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。)
组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。

优缺点对比
在这里插入图片描述
如何选择

相信很多人都知道面向对象中有一个比较重要的原则『多用组合、少用继承』或者说『组合优于继承』。从前面的介绍已经优缺点对比中也可以看出,组合确实比继承更加灵活,也更有助于代码维护。

所以,

建议在同样可行的情况下,优先使用组合而不是继承。
因为组合更安全,更简单,更灵活,更高效。

注意,并不是说继承就一点用都没有了,前面说的是【在同样可行的情况下】。有一些场景还是需要使用继承的,或者是更适合使用继承。

继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该好好考虑是否需要继承。《Java编程思想》
只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在is-a关系的时候,类B才应该继承类A。《Effective Java》

构造函数与默认构造函数

构造函数,是一种特殊的方法。 主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。

/**
* 矩形
*/
class Rectangle {

     /**
      * 构造函数
      */
     public Rectangle(int length, int width) {
         this.length = length;
         this.width = width;
     }
     
     public static void main (String []args){
        //使用构造函数创建对象
        Rectangle rectangle = new Rectangle(10,5);
        
     }
}

特别的一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们即构造函数的重载。

构造函数跟一般的实例方法十分相似;但是与其它方法不同,构造器没有返回类型,不会被继承,且可以有范围修饰符。

构造器的函数名称必须和它所属的类的名称相同。它承担着初始化对象数据成员的任务。

如果在编写一个可实例化的类时没有专门编写构造函数,多数编程语言会自动生成缺省构造器(默认构造函数)。默认构造函数一般会把成员变量的值初始化为默认值,如int -> 0,Integer -> null。

如果在编写一个可实例化的类时没有专门编写构造函数,默认情况下,一个Java类中会自动生成一个默认无参构造函数。默认构造函数一般会把成员变量的值初始化为默认值,如int -> 0,Integer -> null。

但是,如果我们手动在某个类中定义了一个有参数的构造函数,那么这个默认的无参构造函数就不会自动添加了。需要手动创建!

/**
* 矩形
*/
class Rectangle {

     /**
      * 构造函数
      */
     public Rectangle(int length, int width) {
         this.length = length;
         this.width = width;
     }
     
     /**
      * 无参构造函数
      */
     public Rectangle() {
         
     }
}

类变量、成员变量和局部变量

Java中共有三种变量,分别是类变量、成员变量和局部变量。他们分别存放在JVM的方法区、堆内存和栈内存中。

    /**
     * @author Hollis
     */
    public class Variables {
    
        /**
         * 类变量
         */
        private static int a;
    
        /**
         * 成员变量
         */
        private int b;
    
        /**
         * 局部变量
         * @param c
         */
        public void test(int c){
            int d;
        }
    }

上面定义的三个变量中,变量a就是类变量,变量b就是成员变量,而变量c和d是局部变量。

成员变量和方法作用域

对于成员变量和方法的作用域,public,protected,private以及不写之间的区别:

public : 表明该成员变量或者方法是对所有类或者对象都是可见的,所有类或者对象都可以直接访问
private : 表明该成员变量或者方法是私有的,只有当前类对其具有访问权限,除此之外其他类或者对象都没有访问权限.子类也没有访问权限.
protected : 表明成员变量或者方法对类自身,与同在一个包中的其他类可见,其他包下的类不可访问,除非是他的子类
default : 表明该成员变量或者方法只有自己和其位于同一个包的内可见,其他包内的类不能访问,即便是它的子类

如果你正在入门学习Java或者即将学习,可以申请加入我的纯Java学习交流裙735057581 ,有什么问题都可以随手来交流分享,群文件我上传了我做Java这几年整理的一些学习手册,开发工具,PDF文档书籍教程,需要的话你们都可以自己下载,欢迎大家来一起学习哦!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值