什么是多态

多态的概念呢比较简单,就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
如果按照这个概念来定义的话,那么多态应该是一种运行期的状态。

多态的必要条件

为了实现运行期的多态,或者说是动态绑定,需要满足三个条件。
即有类继承或者接口实现、子类要重写父类的方法、父类的引用指向子类的对象。
简单来一段代码解释下:

  1. public class Parent{
  2. public void call(){
  3. sout("im Parent");
  4. }
  5. }
  6. public class Son extends Parent{// 1.有类继承或者接口实现
  7. public void call(){// 2.子类要重写父类的方法
  8. sout("im Son");
  9. }
  10. }
  11. public class Daughter extends Parent{// 1.有类继承或者接口实现
  12. public void call(){// 2.子类要重写父类的方法
  13. sout("im Daughter");
  14. }
  15. }
  16. public class Test{
  17. public static void main(String[] args){
  18. Parent p = new Son(); //3.父类的引用指向子类的对象
  19. Parent p1 = new Daughter(); //3.父类的引用指向子类的对象
  20. }
  21. }

这样,就实现了多态,同样是Parent类的实例,p.call 调用的是Son类的实现、p1.call调用的是Daughter的实现。
有人说,你自己定义的时候不就已经知道p是son,p1是Daughter了么。但是,有些时候你用到的对象并不都是自己声明的啊 。
比如Spring 中的IOC出来的对象,你在使用的时候就不知道他是谁,或者说你可以不用关心他是谁。根据具体情况而定。
另外,还有一种说法,包括维基百科也说明,多态还分为动态多态和静态多态。
上面提到的那种动态绑定认为是动态多态,因为只有在运行期才能知道真正调用的是哪个类的方法。
还有一种静态多态,一般认为Java中的函数重载是一种静态多态,因为他需要在编译期决定具体调用哪个方法、
关于这个动态静态的说法,我更偏向于重载和多态其实是无关的。
但是也要看情况,普通场合,我会认为只有方法的重写算是多态,毕竟这是我的观点。但是如果在面试的时候,我“可能”会认为重载也算是多态,毕竟面试官也有他的观点。我会和面试官说:我认为,多态应该是一种运行期特性,Java中的重写是多态的体现。不过也有人提出重载是一种静态多态的想法,这个问题在StackOverflow等网站上有很多人讨论,但是并没有什么定论。我更加倾向于重载不是多态。
这样沟通,既能体现出你了解的多,又能表现出你有自己的思维,不是那种别人说什么就是什么的。

方法重写与重载

封装、继承、多态 - 图1
重载(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中,继承使用extends关键字实现,而实现通过implements关键字。
Java中支持一个类同时实现多个借口,但是不支持同时继承多个类。

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

class Car extends Benz implements GasolineCar, ElectroCar{
}

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

Java的继承与组合

Java是一个面向对象的语言。每一个学习过Java的人都知道,封装、继承、多态是面向对象的三个特征。每个人在刚刚学习继承的时候都会或多或少的有这样一个印象:继承可以帮助我实现类的复用。所以,很多开发人员在需要复用一些代码的时候会很自然的使用类的继承的方式,因为书上就是这么写的(老师就是这么教的)。但是,其实这样做是不对的。长期大量的使用继承会给代码带来很高的维护成本。
本文将介绍组合和继承的概念及区别,并从多方面分析在写代码时如何进行选择。

面向对象的复用技术

前面提到复用,这里就简单介绍一下面向对象的复用技术。
复用性是面向对象技术带来的很棒的潜在好处之一。如果运用的好的话可以帮助我们节省很多开发时间,提升开发效率。但是,如果被滥用那么就可能产生很多难以维护的代码。
作为一门面向对象开发的语言,代码复用是Java引人注意的功能之一。Java代码的复用有继承,组合以及代理三种具体的表现形式。本文将重点介绍继承复用和组合复用。

继承

继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;继承是一种is-a关系。(图片来自网络,侵删。)
封装、继承、多态 - 图2

组合

组合(Composition)体现的是整体与部分、拥有的关系,即has-a的关系。
封装、继承、多态 - 图3

组合与继承的区别和联系

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

优缺点对比

组 合 关 系 继 承 关 系
优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立 缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性
优点:具有较好的可扩展性 缺点:支持扩展,但是往往以增加系统结构的复杂度为代价
优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 缺点:不支持动态继承。在运行时,子类无法选择不同的父类
优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口 缺点:子类不能改变父类的接口
缺点:整体类不能自动获得和局部类同样的接口 优点:子类能自动继承父类的接口
缺点:创建整体类的对象时,需要创建所有局部类的对象 优点:创建子类的对象时,无须创建父类的对象

如何选择

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

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

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

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

构造函数与默认构造函数

构造函数,是一种特殊的方法。 主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。 特别的一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们即构造函数的重载。
构造函数跟一般的实例方法十分相似;但是与其它方法不同,构造器没有返回类型,不会被继承,且可以有范围修饰符。构造器的函数名称必须和它所属的类的名称相同。 它承担着初始化对象数据成员的任务。
如果在编写一个可实例化的类时没有专门编写构造函数,多数编程语言会自动生成缺省构造器(默认构造函数)。默认构造函数一般会把成员变量的值初始化为默认值,如int -> 0,Integer -> null。

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

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

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

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

成员变量和方法作用域

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