JAVA--继承
概述
利用继承,人们可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和域。 在此基础上,还可以添加一些新的方法和域,以满足新的需求。这是 Java 程序设计中的一项核心技术。"is-a" 关系是继承的一个明显特征。
初始化
public class Manager extends Employee {
private double bonus;
public Manager(String name, double salary, int year, int month, int day) {
super(name, salary, year, month, day);
bonus = 0;
}
public Manager(String name, double salary, int year, int month, int day, int bonus) {
this(name, salary, year, month, day);
this.bonus = bonus;
}
public void setBonos(double bonus) {
this.bonus = bonus;
}
}
- 使用super 调用构造器的语句必须是子类构造器的第一条语句。super 关键字有两个用途: 调用超类的方法,、调用超类的构造器。
- 关键字 this 有两个用途:引用隐式参数 、调用该类其他的构造器 。
- 在调用构造器的时候,this和super关键字的使用方式很相似。调用构造器的语句只能作为另一个构造器的第一条语句出现。构造参数既可以传递给本类(this) 的其他构造器,也可以传递给超类(super) 的构造器。
方法调用
一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。一个变量既可以引用一个A类对象,也可以引用一个A类的任何一个子类的对象。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。
方法调用过程详解
- 编译器査看对象的声明类型和方法名。假设调用 x.f(param) 且隐式参数 x 声明为 C 类的对象。需要注意的是: 有可能存在多个名字为f, 但参数类型不一样的方法。例如,可能存在方法 f(int) 和方法f(String), 编译器将会一一列举所有 C 类中名为 f 的方法和其超类中访问属性为 public 且名为 f 的方法 (超类的私有方法不可访问)。至此,编译器已获得所有可能被调用的候选方法。
- 接下来, 编译器将査看调用方法时提供的参数类型。 如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配, 就选择这个方法。这个过程被称为重载解析(overloading resolution),PS:方法的名字和参数列表称为方法的签名。 例如, f(int) 和 f(String) 是两个具有相同名字,不同签名的方法。 如果在子类中定义了一个与超类签名相同的方法, 那么子类中的这个方法就覆盖了超类中的这个相同签名的方法。不过, 返回类型不是签名的一部分
- 如果是private方法、static方法、final方法或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称为静态绑定(static binding)。 与此对应的是, 调用的方法依赖于隐式参数的实际类型, 并且在运行时实现动态绑定。
- 当程序运行,并且采用动态绑定调用方法时, 虚拟机一定调用与 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), 编译器将对隐式参数超类的方法表进行搜索。
阻止继承: final 类和方法
public final class A extends B {
...
}
public class C extends B {
...
public final getName() {
return name;
}
...
}
有时候,可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为 final 类。如果在定义类的时候使用了 final 修饰符就表明这个类是 final 类。 类中的特定方法也可以被声明为 final。 如果这样做,子类就不能覆盖这个方法,类将方法或类声明为 final 主要目的是: 确保它们不会在子类中改变语义。
强制类型转换
将一个类型强制转换成另外一个类型的过程被称为类型转换。进行类型转换的唯一原因是: 在暂时忽视对象的实际类型之后, 使用对象的全部功能。
//success x 的值转换成整数类型, 舍弃了小数部分
double x = 3.405;
int nx = (int) x;
//前置条件
Manager boss = new Manager(...);
boss.setBonus(5000) ;
Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee(...);
staff[2] = new Employee(...);
---------------------------------
//success
Employee staff = new Manager(...);
//success
Manager boss = (Manager) staff[0];
//error 抛出 ClassCastException 异常
Manager boss = (Manager) staff[1];
//推荐用法
if (staff[1] instanceof Manager){
boss = (Manager) staff[1]:
...
}
将一个值存入变量时, 编译器将检查是否允许该操作。将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量, 必须进行类型转换,这样才能够通过运行时的检査。由上可知:
- 只能在继承层次内进行类型转换。
- 在将超类转换成子类之前, 应该使用 instanceof 进行检查。
在一般情况下,应该尽量少用类型转换和 instanceof 运算符。
抽象类(abstract)
// 抽象类
public abstract class Person {
private String name;
public Person(String name) {
this.name = name;
}
//抽象方法
public abstract String getDescriptionO;
public String getName() {
return name;
}
}
//实现子类
public class Student extends Person {
private String major;
public Student(String name, String major) {
super(name) ;
this.major=major;
}
public String getDescription() {
return "a student majoring in " + major;
}
}
--------------------------
//error
Person p = new Person(...);
//success
Person p = new Student(...);
- 为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。除了抽象方法之外,抽象类还可以包含具体数据和具体方法。
- 抽象方法充当着占位的角色,它们的具体实现在子类中。
- 扩展抽象类可以有两种选择。 一种是在抽象类中定义部分抽象类方法或不定义抽象类方法, 这样就必须将子类也标记为抽 象类; 另一种是定义全部的抽象方法, 这子类就不是抽象的了。
- 类即使不含抽象方法, 也可以将类声明为抽象类。
- 抽象类不能被实例化。如果将一个类声明为 abstract, 就不能创建这个类的对象。
- 可以定义一个抽象类的对象变量, 但是它只能引用非抽象子类的对象。
4个访问修饰符
- 仅对本类可见 private。
- 对所有类可见 public。
- 对本包和所有子类可见 protected。
- 对本包可见(默认),不需要修饰符。
所有类的超类(Object)
可以使用 Object 类型的变量引用任何类型的对象。当然,Object 类型的变量只能用于作为各种值的通用持有者。 要想对其中的内容进行具体的操作, 还需要清楚对象的原始类型,并进行相应的类型转换。
equals 方法
Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。 在 Object 类中, 这个方法将判断两个对象是否具有相同的引用。== 运算符检测的也是对象是否指向同一个存储区域。如果两个对象具有相同的引用,它们一定是相等的。 从这点上看, 将其作为默认操作也是合乎情理的。 然而, 对于多数类来说, 这种判断并没有什么意义。
Java 语言规范要求 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.equals(null) 应该返回 false。
equas方法编码建议:
- 显式参数命名为 otherObject, 稍后需要将它转换成另一个叫做 other 的变量。
- 检测 this 与 otherObject 是否引用同一个对象。这条语句只是一个优化。 实际上, 这是一种经常采用的形式。 因为计算这个等式要比一 个一个地比较类中的域所付出的代价小得多。
if (this = otherObject)
return true;
- 检测 otherObject 是否为 null , 如 果 为 null , 返 回 false。 这项检测是很必要的。
if (otherObject = null)
return false;
- 比较 this 与 otherObject 是否属于同一个类。
如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测:
if (getClass() != otherObject.getCIassO)
return false;
如果所有的子类都拥有统一的语义, 就使用 instanceof 检测:
if (!(otherObject instanceof ClassName))
return false;
- 将 otherObject 转换为相应的类类型变量:
ClassName other = (ClassName) otherObject
- 现在开始对所有需要比较的域进行比较了。使用 = 比较基本类型域,使用equals 比较对象域。如果所有的域都匹配,就返回true; 否则返回false。
return field1 == other.field1 && Objects.equals(field2, other.field2) && ...
hashCode 方法(集合章节详解)
- 散列码(hashcode) 是由对象导出的一个整型值。散列码是没有规律的。如果x和y是两个不同的对象, x.hashCode( ) 与 y.hashCode( ) 基本上不会相同。
- hashCode 方法定义在 Object 类中, 因此每个对象都有一个默认的散列码, 其值为对象的存储地址。如果重新定义 equals 方法, 就必须重新定义 hashCode 方法, 以便用户可以将对象插人到散列表中。
- equals 与 hashCode 的定义必须一致: 如果 x.equals(y) 返回 true, 那么 x.hashCode( ) 就必须与 y.hashCode( ) 具有相同的值。
toString 方法
在 Object 中还有一个重要的方法,就是 toString 方法,它用于返回表示对象值的字符串。Object 类定义了 toString 方法, 用来打印输出对象所属的类名和散列码。
toString 方法是一种非常有用的调试工具。 在标准类库中, 许多类都定义了 toString 方法, 以便用户能够获得一些有关对象状态的必要信息。强烈建议为自定义的每一个类增加toString方法。这样做不仅自己受益,而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。
对象包装器与自动装箱
ArrayList<Integer> list = new ArrayList<>();
//当将一个 int 对象赋给一个 Integer 值时,将会自动地装箱(autoboxing).
list.add(3); -> list .add(Integer.value0f(3));
//当将一个 Integer 对象赋给一个 int 值时,将会自动地拆箱。
int n = list.get(i); -> int n = list.get(i).intValue();
有时, 需要将 int 这样的基本类型转换为对象。 所有的基本类型都有一个与之对应的类。 例如,Integer 类对应基本类型 int。 通常, 这些类称为包装器 ( wrapper ) 。这些对象包装器类拥有很明显的名字: Integer、Long、Float、Double、Short、Byte、Character、Void 和 Boolean ( 前 6 个类派生于公共的超类 Number)。 对象包装器类是不可变的, 即一旦构造了包装器, 就不允许更改包装在其中的值。同时,对象包装器类还是final, 因此不能定义它们的子类。tips: 由于每个值分别包装在对象中, 所以 ArrayList<lnteger> 的效率远远低于 int[ ] 数组。 因此,应该用它构造小型集合,其原因是此时程序员操作的方便性要比执行效率更加重要。
Integer a = 1000;
Integer b = 1000;
a == b // it is false
Integer a = 100;
Integer b = 100;
a == b // it is true
自动装箱规范要求 boolean、byte、char <=127, 介于 -128 ~ 127 之间的 short 和 int 被包装到固定的对象中。介于上述原因,建议两个包装器对象比较时调用 equals 方法。
继承的设计技巧
- 将公共操作和域放在超类
- 不要使用受保护的域
- 使用继承实现“ is-a”关系
- 除非所有继承的方法都有意义, 否则不要使用继承
- 在覆盖方法时,不要改变预期的行为
- 使用多态,而非类型信息
- 不要过多地使用反射