0%

JAVA基础_3_类的继承

第三部分 类的继承

对于面向对象的程序设计而言,“继承”这个特点可以很直观的反应某个程序的框架与逻辑,另一方面,继承这个特点同样是程序设计语言中逐渐精简的特点的展现。

I 类&超类&子类

假如我们要设计很相似的两个类,这两个类的大部分属性和方法都一样,只是其中一个类相比于另一个多了几个属性和方法。此时如果将两个类同时写出来很明显存在代码重复的问题,这在程序设计中是很不建议的,因此可以用到继承这一概念。
以经理Manager与普通员工Employee举例:

1
2
3
4
public class Manager extends Employee
{
//新增的域和方法
}

关键字extends类似于C++中的冒号,表明正在基于已经存在的一个类(超类,基类。父类)来创造一个新的类(子类,派生类,孩子类)。
其中,子类不能直接访问父类的私有域,只有通过父类的公共接口才能访问。如果在方法中覆盖父类的方法并调用这个方法(我们希望调用父类的功能),并且由于子类也有这样的方法,就会无限调用下去直到程序崩溃,为此,我们可以使用关键字super来覆盖已有的方法

1
2
3
4
5
6
7
8
public class Manager extends Employee
{
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
}

为了解决覆盖的问题,C++中需要写出整个父类的名称,而Java中super关键字直观的告诉我们,这个地方使用的是父类的方法,因此不会造成死循环。在子类中我们可以增加域,方法或者覆盖超类的方法,然而绝对不能删除继承的任何域和方法。
this作为隐式参数这个关键字有两个用途,一是可以被引用,二是调用该类的其他构造器,同样的super也作为一个隐式参数不光可以被引用,也可以调用父类的构造器:

1
2
3
4
5
6
7
8
9
public class Manager extends Employee
{
public Manager(String name, double salary, int year, int month, int day)
{
//super必须放在构造器的最前面
super(name, salary, year, month, day);
bonus = 0;
}
}

如果调用for-each循环的时候,在调用父类和子类同名不同功能的方法时,虚拟机会自己判断对象是父类还是子类再进行相应的调用。
一个对象变量可以指示多种实际类型的现象成为多态,在与运行时能够自动选择调用的方法的现象称为动态绑定。如果是private,static,final的方法被称为静态绑定
在Java中,对象变量是多态的,一个Employee对象即可以引用一个Employee对象,也可以引用Employee子类的对象,反之则不可以,并且调用方法的时候遵循哪个类的对象才可以调用哪个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test
{
//关于类的强制类型转换,只能自顶向下进行转换
public static void main(String args)
{
//使用Employee类的对象变量引用boss,可以运行
Manager boss = new Manager();
Employee staff = boss;

//使用Manager类的对象变量引用worker,不能运行
Employee worker = new Employee();
Manager errorBoss = worker;

//可以用boss调用专属于Manager的方法,而staff不可以
//即使他们引用的是同一个对象
boss.setBonus(5000); // OK
staff.setBonus(5000); // error
}
}

不允许被扩展的类被称为final类,在定义类的时候添加final修饰符即可,同理,不允许被覆盖的方法被称为final方法。
如果两个相似的子类需要输出一串简介信息,但是内容相差比较大,对于父类来说不太容易定义这个看似关联实则区别比较大的方法,这里就可以用到抽象类这个概念:

1
2
3
4
5
6
public abstract class Person
{
// 在父类中创建抽象方法后,在子类中重载即可。
// 类比于C++中的:virtual string getDescription() = 0;
public abstract String getDescription();
}

在自下而上的继承中,位于上层的类更通用的同时也更抽象,因此抽象abstract这个关键字就很好的解决了这个问题.
对于java中的访问,有以下四种情况:

修饰符 同一个类 同一个包 子类 所有类
private
default
protected
public

II 所有类的父类 - Object

在Java中,除了基本类型(数值,字符,布尔值)以外,所有的变量都是对象变量,包括我们常见的数组(包括对象数组和普通数组)都是扩展了Object类的对象。
Object类是所有类的父类,我们定义类的时候虽然没有写出”extends Object“,但实际上却有这个含义。
以下将介绍部分Object类提供的方法,关于处理线程的方法将在后续介绍:

  • equals:用于比较两个类是否相等,默认的比较方式是检查是否具有相同的引用,确认两个对象是否相等需要很多逻辑上的判断且子类需要先调用父类的equals。
  • hashCode:返回对象的散列码,可以理解成对象的ID,默认的散列码为对象的存储地址。hashcode自定义散列码时要保证相同对象有相同散列码,也可以用静态方法Object.hash(需要比较的参数)来生成。
  • toString:返回对象的字符串,默认是类的名字+地址,自定义这个方法是一种非常有用的调试工具,标准类库中有很多这样的方法重载

    III 泛型数组列表(ArrayList)与包装器

    对于类似C++中的vector可变数组的操作,java中提供了ArrayList类,它是一个采用类型参数的泛型类,使用时需要用尖括号来定义对象的类型:
    1
    2
    //类比于C++的模板
    ArrayList<Employee> staff = new ArrayList<Employee>();
    ArrayList类和C++的vector极为相似,但是需要注意的是,java中没有运算符的重载,意味着不能通过[]来访问元素,并且访问和修改元素的操作需要get和set方法来实现。
    除此之外,虽说在Java这个极度面向对象的设计语言中,基础数据类型不作为对象存在,不过我们可以用他们来制造属于他们的对象,也就是包装器,具体类型的包装器对应其英文拼写:
    1
    2
    3
    4
    5
    ArrayList<Integer> list = new ArrayList<>();
    //包装器可以实现自动打包(装箱),等价于list.add(Integer.valueOf(3));
    list.add(3);
    //也可以实现自动拆箱,等价于list.get(i).intValue();
    int n = list.get(i);
    除此之外,包装器还提供了许多实用的静态方法,如Integer类中的ParseInt(String)可以将字符串转换为整数并返回。

    IV 可变参数的方法

    顾名思义,即参数不固定的例子,printf就是一个很好的例子:
    1
    2
    3
    4
    5
    6
    7
    8
    public class PrintStream
    {
    //这里的...是实际代码的一部分,表明可以接受任意数量的对象
    public PrintStream printf(String fmt, Object...args)
    {
    return format(fmt, args);
    }
    }
    实际上,printf接收到的两个参数分别为fmt和Object[]数组,也就是说,对于使用者而言,Object…参数类型和Object[]完全一样。

    V 继承的设计技巧

  • 将公共操作和域放在父类中
  • 非特殊情况下,不要使用protected
  • 使用继承实现”is-a”的关系,构建成一棵树
  • 除非所有的继承方法都有意义,否则不要使用继承
  • 在重载方法的时候,不要改变预期的行为
  • 使用多态,而非一连串的分支
  • 不要过多使用反射库