多态
Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态(Polymorphism)。其实就是子类的对象赋值给父类的引用变量。
举个代表性的例子:
父类Parent有A方法,B方法,成员变量X,子类Sub继承父类,重写B方法,自己有C方法,成员变量X,然后
Parent parent=new Sub();
Parent。A() 当调用该引用变量的A方法则正常会走父类的A方法;
Parent。B() 当调用该引用变量的B方法(Parent类中定义了该方法,子类Sub中覆盖了父类的该方法)时,实际执行的是子类Sub类中覆盖后的B方法,这就出现多态了。
Parent。C() 当调用该引用变量的C方法时,父类中没有,则报错;虽然parent引用变量实际上确实包含C方法(例如,可以通过反射来执行该方法),但因为它的编译时类型为
Parent,因此编译时无法调用C方法。通过引用变量来访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量。引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。因此编写Java代码时,引用变量只能调用声明该变量时所用类里包含的方法。例如, 通过Object p=new Person()代码定义一个变量p,则这个p只能调用Object类的方法,而不能调用Person类里定义的方法。综合起来说,在编写Java程序时,引用变量只能调用它编译时类型的方法,而不能调用它运行时类型的方法,即使它实际所引用的对象确实包含该方法。如果需要让这个引用变量调用它运行时类型的方法,则必须把它强制类型转换成运行时类型,强制类型转换需要借助于类型转换运算符。
重点:
与方法不同的是,对象的实例变量则不具备多态性。比如上面的parent引用变量,程序中输出它的X实例变量时,并不是输出Sub类里定义的实例变量,而是输出Parent类的实例变量。
又引申出来了新的问题:强制类型转换
语法是 :(type)variable,这种用法可以将variable变量转换成一个type类型的变量。前面在介绍基本类型的强制类型转换时,已经看到了使用这种类型转换运算符的用法,类型转换运算符可以将一个基本类型变量转换成另一个类型。
除此之外,这个类型转换运算符还可以将一个引用类型变量转换成其子类类型。这种强制类型转换不是万能的,当进行强制类型转换时需要注意:基本类型之间的转换只能在数值类型之间进行,这里所说的数值类型包括整数型、字符型和浮点型。但数值类型和布尔类型之间不能进行类型转换。
引用类型之间的转换只能在具有继承关系的两个类型之间进行,如果是两个没有任何继承关系的类型,则无法进行类型转换,否则编译时就会出现错误。如果试图把一个父类实例转换 成子类类型,则这个对象必须实际上是子类实例才行(即编译时类型为父类类型,而运行时类型是子类类型),否则将在运行时引发ClassCastException异常。
考虑到进行强制类型转换时可能出现异常,因此进行类型转换之前应先通过instanceof运算符来判断是否可以成功转换。先判断:
注意:
当把子类对象赋值给父类引用变量时,被称为向上转型upcasting),这种转型总是可以成功的,这也从另一个侧面证实了子类是一种特殊的父类。这种转型只是表明这个引用变量的编译时类型是父类,但实际执行它的方法时,依然表现出子类对象的行为方式。但把一个父类对象赋给子类引用变量时,就需要进行强制类型转换,而且还可能在运行时产生ClassCastException异常,使用instanceof运算符可以让强制类型转换更安全。instanceof和类型转换运算符一样,都是Java提供的运算符,与+、-等算术运算符的用法大致相似。
instanceof运算符的前一个操作数通常是一个引用类型变量,后 一个操作数通常是一个类(也可以是接口,可以把接口理解成一种特殊的类),它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是,则返回true,否则返回false。
在使用instanceof运算符时需要注意:instanceof运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。
instanceof运算符的作用是:
在进行强制类型转换之前,首先判断前一个对象是否是后一个类的实例,是否可以成功转换,
instanceof和(type)是Java提供的两个相关的运算符,通常先用instanceof判断一个对象是否可以强制类型转换,然后再使用(type)运算符进行强制类型转换,从而保证程序不会出现错误。
再想想:利用继承和多态,其实会破坏封装,比如
Animal a = new Dog();
a.eat() ;
如果eat方法在Dog类中被重写,下面a.eat()就是调用的Dog中的;也就是意味着父类的eat()被恶意篡改了!!!
为了保证父类有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则。
1.尽量隐藏父类的内部数据。尽量把父类的所有成员变量都设置成private访问类型,不要让子类直接访问父类的成员变量。
2.不要让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private访问控制符修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,则必须以public修饰,但又不希望子类重写该方法,可以使用final修饰符来修饰该方 法;如果希望父类的某个方法只是被子类重写,但不希望被其他类自由访问,则可以使用protected来修饰该方法。
3.尽量不要在父类构造器中调用将要被子类重写的方法。会导致调用混乱,空指针等问题。
那么怎么将类变成最终类呢?
1.如果想把某些类设置成最终类,即不能被当成父类不能被继承不能被调用构造器,则可以使用final修饰这个类 , 例如 JDK 提 供 的 java.lang.String 类和java.lang.System类。
2.如果想当父类,但是不想让构造器被随便调用,则使用private修饰这个类的所有构造器,从而保证子类无法调用该类的构造器,也就无法继承该类(其实也当不成父类)。但是如果还想产生实例对象呢?对于把所有的构造器都使用private修饰的父类而言,可另外提供一个静态方法,用于创建该类的实例。
类的继承不是随随便便的
到底何时需要从父类派生新的子类呢?不仅需要保证子类是一种特殊的父类,而且需要具备以下两个条件之一。
1.子类需要额外增加成员变量,而不仅仅是变量值的改变。例如从Person类派生出 Student子类 , Person 类里没有提供grade(年级)成员变量,而Student类需要grade成员变量来保存Student对象就读的年级,这种父类到子类的派生,就符合Java继承的前提。
2.子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)。例如从 Person类派生出Teacher 类 , 其中Teacher类需要增加一个teaching()方法,该方法用于描述Teacher对象独有的行为方式:教学。
终于明白啥叫 类的组合
其实就是之前一直觉得有点那个的点,就是一个类实际上也是类型,就可以当成int这种使用,这就叫做组合,组合是把旧类对象作为新类的成员变量组合进来,用以实现新类的功能, 用户看到的是新类的方法,而不能看到被组合对象的方法。
到底该用继承?还是该用组合呢?
继承是对已有的类做一番改造,以此获得一个特殊的版本。简而言之,就是将一个较为抽象的类改造成能适用于某些特定需求的类。因此,对于上面的Wolf和Animal的关系,使用继承更能表达其现实意义。用一个动物来合成一匹狼毫无意义:狼并不是由动物组成的。
反之,如果两个类之间有明确的整体、部分的关系,例如Person类需要复用Arm类的方法(Person对象由Arm对象组合而成),此时就应该采用组合关系来实现复用,把Arm作为Person类的组合成员变量,借助于Arm的方法来实现Person的方法,这是一个不错的选择。
总之,继承要表达的是一种“是(is-a)”的关系,而组合表达的是“有(has-a)”的关系。