跳至主要內容

Java

LincZero大约 11 分钟

Java

目录

类的重用 - 继承(inheritance)

继承 - 类继承(类、超类和子类)

一些概念

  • 已存在的类:称为超类(superclass)、基类(base class)或父类(parent class)
  • 新类:称为子类(subclass)、派生类(derived class)或孩子类(child class)
  • 术语习惯:Java通用术语 超类和子类,C++通常用术语 父类和子类
  • 继承层次
    • 在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)
    • 通常,一个祖先类可以拥有多个子孙继承链
  • is-a
    • 有一个用来判断是否应该设计为继承关系的简单规则,这就是“is-a”规则
    • “is-a”规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。 例如,可以将一个子类的对象赋给超类变量

定义子类

  • java (在Java中,所有的继承都是公有继承,而没有C++中的私有继承和保护继承)

    public class Manager extends Employee
    {
        // ...
    }
    
  • C++

    class A : public APerent {}
    

多继承(不支持)

Java不支持多继承

多态(替换原则)

在Java程序设计语言中,对象变量是多态的。 一个父类变量既可以引用一个父类对象,也可以引用一个该类的任何一个子类对象

在Java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。

可能引起的bug:注意要规避以下写法:

Manager[] managers = new Manager[10];	// 经理列表
Employee[] staff = managers;			// 普通雇员列表
staff[0] = new Employee("Harry Hacker");// 不报错,但危险
// 在这里,staff[0]与manager[0]引用的是同一个对象,也就是说staff[0]经理列表中居然混杂了一个普通雇员
// 经验:所有数组都要牢记创建它们的元素类型,并负责监督仅将类型兼容的引用存储到数组中

继承 - 方法继承

覆盖方法、选择方法版本

  • 继承(inheritance):子类自动地继承了父类中的方法

  • 覆盖(Override):提供一个新的方法来覆盖(Override)超类中的原方法

    • 如果希望调用超类而不是子类的方法

      • Java:使用特定的关键字super解决这个问题

        super.getSalary()
        
      • C++:使用::

        父类名::getSalary()
        
    • 注意:有些人认为super与this引用是类似的概念,实际上,这样比较并不太恰当。这是因为super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字

    • 注意:在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是,如果超类方法是public,子类方法一定要声明为public

子类构造器(super关键字)

C++

A::A():PerentA(){}
// 一般使用成员初始化列表语法(效率高)来调用基类构造函数
// 缺省时:
//     如果省略基类的构造函数,则程序自动使用默认的基类构造函数

Java

public Manager(String name, double salarym, int year, int month, int day)
{
    super(name, salary, year, month, day);
    // 使用super调用构造器的语句必须是子类构造器的【第一条】语句
    // 这里的关键字super具有不同的含义
    // 是“调用超类Employee中含有n、s、year、month和day参数的构造器”的简写形式
    bonus = 0;
}
// 缺省时:
//     如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器
//     如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,则Java编译器将报告错误。

动态性

动态绑定

  • 一个对象变量(例如,变量e)可以指示多种实际类型的现象被称为多态(polymorphism)
  • 在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)
  • C:默认是静态绑定
  • Java:不需要将方法声明为虚拟方法。动态绑定是默认的处理方式。如果不希望让一个方法具有虚拟特征,可以将它标记为final

动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。假设增加一个新类Executive,并且变量e有可能引用这个类的对象,我们不需要对包含调用e.getSalary()的代码进行重新编译

方法调用的原理

理论解析

调用过程的详细描述:

  • 1)编译器查看对象的声明类型和方法名 编译器获得所有可能被调用的候选方法

    假设调用x.f(param),且隐式参数x声明为C类的对象 需要注意的是:有可能存在多个名字为f,但参数类型不一样的方法。例如,可能存在方法f(int)和方法f(String) 编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法(超类的私有方法不可访问)。

  • 2)编译器查看调用方法时提供的参数类型 重载解析(overloading resolution) 编译器获得需要调用的方法名字和参数类型

    如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析(overloading resolution) 例如,对于调用 x.f(“Hello”) 来说,编译器将会挑选 f(String) ,而不是 f(int) 。由于允许类型转换(int可以转换成double,Manager可以转换成Employee,等等),所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。

  • 3)静态绑定或动态绑定(static binding / dynamic binding)

    如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)

    与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。例如编译器采用**动态绑定(dynamic binding)**的方式生成一条调用 f(String) 的指令。

  • 4)当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。

    假设x的实际类型是D,它是C类的子类。如果D类定义了方法 f(String) ,就直接调用它;否则,将在D类的超类中寻找 f(String) ,以此类推。

    每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。在前面的例子中,虚拟机搜索D类的方法表,以便寻找与调用 f(Sting) 相匹配的方法。这个方法既有可能是 D.f(String) ,也有可能是 X.f(String) ,这里的X是D的超类。这里需要提醒一点,如果调用 super.f(param) ,编译器将对隐式参数超类的方法表进行搜索。


实战举例

class Employee
{
    public String getSalary() {}
}
class Manager extends Employee
{
    // ...
}
Employee e;
e = new Employee( , , );
e = new Manager( , , );

在运行时,调用 e.getSalary() 的解析过程为:

1)首先,虚拟机提取e的实际类型的方法表。既可能是Employee、Manager的方法表,也可能是Employee类的其他子类的方法表。

2)接下来,虚拟机搜索定义getSalary签名的类。此时,虚拟机已经知道应该调用哪个方法。

3)最后,虚拟机调用方法。

关闭动态性(final)

多态性的默认

  • Java:默认所有方法具有多态性,方法默认为动态绑定
  • C++:默认所有方法不具有多态性,方法默认为静态绑定
  • 所以除了禁止类或类方法被继承或重写外,Java中使用final还有一个功能是关闭动态绑定 在早期的Java中,有些程序员为了避免动态绑定带来的系统开销而使用final关键字 内联优化:并且如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程为称为内联(inlining)

继承 - 其他

阻止继承:final类和方法

Java

  • 作用1:不允许类被继承 例如,假设希望阻止人们定义Executive类的子类,就可以在定义这个类的时候,使用final修饰符声明

    public final class Executive extends Manager
    {
        ...
    }
    
  • 作用2:不允许类方法被复写 类中的特定方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法(final类中的所有方法自动地成为final方法)

    public class Emplyee
    {
        ...
        public final String getName()
        {
            return name;
        }
        ...
    }
    

C++11,final关键字

  • 作用1:不允许类被继承

    struct Basel final{};		// 不让别人继承自己
    struct Derived1:Base1 {};	// 此时会报错
    
  • 作用2:不允许类方法被复写

    struct Base2{
        virtual void f() final;	// 不允许被复写
    };
    struct Derived2:Base2{
        void f();
    }
    

用途:将方法或类声明为final主要目的是:确保它们不会在子类中改变语义。例如在Java中:

  • Calendar类中的getTime和setTime方法都声明为final 这表明Calendar类的设计者负责实现Date类与日历状态之间的转换,而不允许子类处理这些问题
  • String类也是final类 这意味着不允许任何人定义String的子类。 换言之,如果有一个String的引用,它引用的一定是一个String对象,而不可能是其他类的对象。

方法/域的访问权限(保护访问)

保护访问:

  • 人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为protected
  • 在实际应用中,要谨慎使用protected属性。假设需要将设计的类提供给其他程序员使用,而在这个类中设置了一些受保护域,由于其他程序员可以由这个类再派生出新类,并访问其中的受保护域。在这种情况下,如果需要对这个类的实现进行修改,就必须通知所有使用这个类的程序员。这违背了OOP提倡的数据封装原则。
  • 示例:Object类中的clone方法

与C++不同

  • Java

    • // 【四个访问修饰符】
      priveate	// 仅对本类可见
      public		// 对所有类可见
      protected	// 对本包和所有子类可见
      默认	   	   // 对本包可见
      
  • C++

    • 基本同上
  • 总结

    • Java中的受保护部分对所有子类及同一个包中的所有其他类都可见。这与C++中的保护机制稍有不同
    • Java中的protected概念要比C++中的安全性差

其他

this和super的小总结

  • 关键字this有两个用途
    • 一是引用隐式参数
    • 二是调用该类其他的构造器
  • super关键字也有两个用途
    • 一是调用超类的方法
    • 二是调用超类的构造器
  • 在调用构造器的时候,这两个关键字的使用方式很相似。 调用构造器的语句只能作为另一个构造器的第一条语句出现。 构造参数既可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器。

【功能扩展】继承

【功能扩展】继承 x 转换,强制类型转换

double x = 3.405;
int nx = (int) x;

进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。

Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;	// 代换原则

Manager boss = (Manager) staff[0];

应该养成这样一个良好的程序设计习惯:在进行类型转换之前,先查看一下是否能够成功地转换

在一般情况下,应该尽量少用类型转换和instanceof运算符

if (staff[1] instanceof Manager)	// 查看是否能成功类型转换
{
    boss = (Manager) staff[1];
    ...
}

与C++不同

  • 类型转换 Java使用的类型转换语法来源于C语言,但处理过程却有些像C++的dynamic_cast操作。以下两种写法等价

    • Java

      Manager boss = (Manager) staff[1];					// Java
      
    • C++

      Manager* boss = dynamic_cast<Manager*>(staff[1]);	// C++
      
  • 判断是否能转换

    • Java:当类型转换失败时,抛出一个异常(有点像C++中的引用(reference)转换)

      if (staff[1] instanceof Manager)					// Java,需要将instanceof运算符和类型转换组合起来使用
      {
          boss = (Manager) staff[1];
          ...
      }
      
    • C++:当类型转换失败时,生成一个null对象

      Manager* boss = dynamic_cast<Manager*>(staff[1]);	// C++,直接转换
      if (boss != NULL) ...