继承
继承的基本思想是:基于已有的类创建新的类。
父类并不是优于子类或拥有比子类更多的功能,子类比父类拥有的功能更多。
Employee
类(父类、超类)
public class Employee {
private String name;
private Date hireDay;
private Double salary;
public Employee() {
}
public Employee(String name, Date hireDay, Double salary) {
this.name = name;
this.hireDay = hireDay;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getHireDay() {
return hireDay;
}
public void setHireDay(Date hireDay) {
this.hireDay = hireDay;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
}
Manager
类(子类)
public class Manager extends Employee{
private Double bonus;
public Manager() {
}
public Manager(Double bonus) {
this.bonus = bonus;
}
public Manager(String name, Date hireDay, Double salary, Double bonus) {
super(name, hireDay, salary);
this.bonus = bonus;
}
public Double getBonus() {
return bonus;
}
public void setBonus(Double bonus) {
this.bonus = bonus;
}
}
由于setBonus
不是Employee
类中定义的,所以属于Employee
类的对象不能使用它。但尽管Manager类中没有显示地定义getName
和getHireDay
等方法, 但是可以对Manger
对象使用这些方法,这是因为Manager
类自动地继承了父类Employee
中地这些方法。童颜地name
、salary
和hireDay
都是从父类继承而来。
通过扩展父类定义子类的时候,只需要指出子类与父类的不同之处。因此在设计类的时候,应该将最一般的方法放在父类中,而将更特殊的方法放在子类中,这种将通用功能抽取到父类的做法在面向对象程序设计中十分普遍。
覆盖方法
父类中有些方法子类并不一定适用,以薪水salary
为例,Manager
子类中,薪水应该是工资和奖金的总和。因此需要提供一个新的方法来覆盖(override
)父类中的这个方法。
Manager
类中覆盖getSalary
方法
@Override
public Double getSalary() {
return super.getSalary() + this.getBonus();
}
注意,因为salary
字段是从Employee
类中继承而来,而本身Manager
并没有这个字段(显式的指定),所以我们需要使用特殊关键字super
解决这个问题:super.getSalary()
。
super
和this
的概念并不一样,因为super
不是一个对象的引用,例如,不能将值super
赋给另一个对象变量,它只是一个指示编译器调用父类方法的特殊关键字。
构造器
public Manager(String name, Date hireDay, Double salary, Double bonus) {
super(name, hireDay, salary);
this.bonus = bonus;
}
这里的super
具有不同的含义,由于Manager
类的构造器不能访问Employee
类的私有字段,所以必须通过一个构造器来初始化这些私有字段,可以利用特殊的super
语法调用这个构造器。使用super
调用构造器的语句必须是子类构造器的第一条语句。
关于this
和super
关键字this
有两个含义:
- 隐式参数的引用
- 调用该类的其他构造器
关键字super
有两个含义:
- 调用父类的方法
- 调用父类的构造器
this
和super
这两个关键字紧密相关,调用构造器的语句只能作为另一个构造器的第一条语句出现。构造器参数可以传递给当前(this
)的另一个构造器,也可以传递给父类(super
)的构造器。
接下来我们可以这样使用:
public class Test {
public static void main(String[] args) {
Employee[] employees = new Employee[3];
Manager boss = new Manager("ljq", new Date(), 20000d, 10000d);
employees[0] = boss;
employees[1] = new Employee("zcc", new Date(), 20000d);
employees[2] = new Employee("coco", new Date(), 20000d);
Arrays.stream(employees)
.forEach(employee -> System.out.println(employee.getSalary()));
}
}
注意,这里的employees
虽然是Employee
对象数组,但完全可以将子类的Manger
赋值进去,当元素引用的是Employee
对象时,调用的是Employee
类的getSalary
方法,当元素是Manager
时,调用的时Manager
类中的getSalary
方法。虚拟机知道元素实际引用的对象类型,因此能够正确的调用相应的方法。
上述情况被称为多态:一个对象变量可以指示多种实际类型的现象称为多态(polymorphism
)。这种在运行时能够自动选择适当的方法称为动态绑定(dynamic binding
)。
继承层次
继承并不仅限于一个层次。如下图所示:
由一个公共父类派生出来的所有类的集合称为继承层次。在继承层次中,从某个特定的类到其祖先的路径称为该类的继承链。
java
中不支持多继承,但可以实现多个接口。
多态
有一个简单规则可以用来判断是否应该将数据设计为继承关系,这就是 is-a
规则,它指出子类的每个对象也是父类的对象。
is-a
规则的另外一种表述是替换原则。它指出程序中出现父类对象的任何地方都可以使用子类对象替换。
方法调用
假设要调用x.f(args)
,隐式参数x声明为类C
的一个对象。下面是调用过程的详细描述:
- 编译器查看对象的声明类型和方法名,需要注意的是:有可能存在多个名字为
f
但参数类型不一样的方法。编译器将会一一列举C
类中所有名为f
的方法和其父类中所有名为f
而且可访问的方法。 - 接下来,编译器要确定方法调用中提供的参数类型,如果在所有名为f的方法中存在一个与所提供参数类型完全匹配的方法,就选择这个方法。这个过程为重载解析(
overloading resolution
)。如果没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,编译器就会报告一个错误。注意:返回类型虽然不是方法签名中的一部分,但是在覆盖一个方法的时候,需要保证子类中的方法的返回类型是父类方法返回类型的子类(如父类中返回的是Employee
类型,则子类中可以是Manager
),这种方法被称为有可协变的返回类型。 - 如果是
private
方法、static
方法、final
方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,这被称为静态绑定。 - 程序运行并采用动态绑定调用方法时,虚拟机必须调用与
x
所引用对象地实际类型对应的那个方法。
在覆盖一个方法的时候, 子类方法不能低于父类方法的可见性。
阻止继承:final类和方法
有时候,我们可能希望阻止人们利用某个类定义子类,**不允许扩展的类被称为final
类。**格式如下:
public final class Executive extends Manager{
}
类中某个特定方法也可以被声明为final
,如果这样做,子类就不能覆盖这个方法(final
类中的所有方法自动称为final
方法)
public final String getName() {
return name;
}
字段也可以声明为final
,对于final
字段来说,构造对象之后就不允许改变它们的值了(只能赋值一次),但final
类中只会将方法变成final
,字段却不会。
将方法和类声明为final
的主要原因是:确保它们不会在子类中改变语义。
强制类型转换
**将一个类型强制转换称另一个类型的过程被称为强制类型转换。**进行强制类型转换的唯一原因是:要在暂时忽视对象的实际类型之后使用对象的全部功能。
在将一个值存入变量时,编译器将检查你是否承诺过多,如果将一个子类的引用赋给一个父类变量,编译器是允许的。但将一个父类的引用赋给一个子类变量时,就承诺过多了,必须进行强制类型转换,这样才能通过运行时检查。
个人理解:承诺过多感觉不是特别准确,我认为可以理解为承诺能做到的过多比较合适,子类引用给父类变量,父类变量并没有承诺做到的比子类变量多,而父类引用给到子类变量,这个变量承诺做到的东西会远远大于父类。简单来说,就是变量的承诺应小于等于引用实际的功能,不然我根据变量使用方法时,却发现引用找不到对应的方法,这不就是承诺能做到的功能过多了嘛
承诺过多会产生ClassCastException
异常。
综上所述:
- 只能在继承层次内进行强制类型转换。
- 在将父类降至转换成子类之前,应使用
instanceof
进行检查。
equals
方法设计原则
- 自反性:对于任何非空引用
x
,x.equals(x)
应该返回true
; - 对称性:对于任何引用
x
和y
,当且仅当y.equals(x)
返回true
时,x.equals(y)
返回true
; - 传递性:对于任何引用
x
、y
和z
,如果x.equals(y)
返回true
,y.equals(z)
返回true
,x.equals(z)
也应该返回true
; - 一致性:如果
x
和y
引用的对象没有发生变化,反复调用x.equals(y)
应该返回同样的结果。 - 对于任意非空引用
x
,x.equlas(null)
应该返回false
;
在父类与子类之间,设计equals
方法有两种情形:
- 如果子类可以有自己的相等性概念,则对称性需求将强制使用
getClass
检测。 - 如果由父类决定相等性概念,那么就可以使用
instanceof
检测,这样可以在不同子类的对象之间进行相等性比较。
书中给出的完美的equals
方法的建议:
- 显示参数命名为
otherObject
,稍后需要将它强制转换成另一个名为other
的变量。 - 检测
this
与otherObject
是否相等(使用==
判断内存地址) - 检测
otherObject
是否为null
,如果为null
,返回false
。(很有必要) - 比较
this
与otherObject
的类,如果equals
的语义可以在子类中改变,就是用getClass
检测;如果所有的子类都有相同的相等性语义,可以使用instanceof
检测; - 将
otherObject
强制转换为相应类类型的变量; - 根据相等性概念的要求来比较字段。使用
==
比较基本类型字段,使用Objects.equals
比较对象字段。如果所有的字段都匹配,就返回true
,否则返回false
;
根据上述建议编写Employee
类的equals
方法
@Override
public boolean equals(Object otherObject) {
if (this == otherObject) return true;
if (otherObject == null || getClass() != otherObject.getClass()) {
return false;
}
Employee other = (Employee) otherObject;
return this.name == other.name
&& this.hireDay.equals(other.hireDay)
&& this.salary == other.salary;
}
反射
反射机制可以用来:
- 在运行时分析类的能力;
- 在运行时检查对象,例如,编写一个适用于所有类的
toString
方法; - 实现泛型数组操作代码;
- 利用
Method
对象;
Class类
在程序运行期间,Java
运行时系统始终为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类。虚拟机利用运行时类型信息选择要执行的正确的方法。
可以使用静态方法forName
获得类名对应的Class
对象。
try {
Class<?> aClass = Class.forName("five.Employee");
Method[] method = aClass.getMethods();
Arrays.stream(method).forEach(System.out::println);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}