一、继承
1.继承:继承是面向对象编程中非常强大的一种机制。它首先可以复用代码:当我们让Student从Person继承时,Student就获得了Person的所有功能,我们只需要为Student编写新增的功能。“is-a”关系是继承的一个明显特征,代表每个学生都是一个人类。
2.Java使用extends关键字来实现继承:
class Person {
private String name;
private int age;
public String getName() {...}
public void setName(String name) {...}
public int getAge() {...}
public void setAge(int age) {...}
}
class Student extends Person {
// 不要重复name和age字段/方法,
// 只需要定义新增score字段/方法:
private int score;
public int getScore() { … }
public void setScore(int score) { … }
}
extends表明正在构造的新类派生于一个已存在的类。我们把这个已存在的类称为超类(super class),父类(parent class),基类(base class),把新类称为子类(subclass),扩展类(extended class)或孩子类(child class)。
3.继承层次:继承并不仅限于一个层次,由一个公共超类派生出来的所有类的集合称为继承层次。Java不支持多重继承,一个class只能继承自一个类,因此,一个类有且仅有一个父类。在继承层次中,从某个特定的类到其祖先的路径称为该类的继承链。
4.重写:超类中有些方法对子类并不适用,因此需要提供一个新的方法来覆盖超类中的方法。在继承后,重复提供该方法,就叫做方法的重写,又叫覆盖(override)。加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。
package property;
public class Item {
String name;
int price;
public void buy(){
System.out.println("购买");
}
public void effect() {
System.out.println("物品使用后,可以有效果");
}
}
package property;
public class MagicPotion extends Item{
public void effect(){
System.out.println("蓝瓶使用后,可以回魔法");
}
}
即重写是:如果在子类中定义了一个与超类签名相同的方法,那么子类中的这个方法会覆盖超类中相同签名的方法。而返回类型不是签名的一部分,但在覆盖方法时,需要保证返回类型的兼容性。允许子类将覆盖方法的返回类型改为原返回类型的子类型。
5.super关键字:当我们希望能调用超类中某个方法或字段而不是当前类的方法或字段,我们可以使用super解决这个问题。super关键字表示超类:子类引用父类的字段或方法时,可以用super.fieldName或super.methodName。
6.子类构造器:由于子类不能访问超类的私有字段,所以必须通过一个构造器来初始化这些私有字段。可以使用super来调用超类的构造器,且必须是子类构造器的第一条语句。
如果子类构造器没有显式地调用超类的构造器,将自动调用超类的无参构造器。如果超类没有无参数的构造器,并且在子类的构造器中又没有显式地调用超类的其它构造器,Java编译器就会报告一个错误。
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(); // 默认自动调用父类的无参构造方法(但是无)
this.score = score;
}
}
//解决方法:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age); // 显式调用父类的有参构造方法Person(String, int)
this.score = score;
}
}
7.受保护访问(protected):已知,子类也不能访问超类的私有字段。但你可能希望限制超类中的某个方法只允许子类访问,或者希望允许子类的方法能访问超类的某个字段。此时可以使用protected关键字,它允许子类直接访问超类中的某个字段。
但保护字段只能由同一个包中的类访问,有了这个限制,就能避免滥用保护机制,不能通过派生子类来访问受保护字段。应该谨慎使用受保护字段,因为其他程序员可能派生子类访问你的受保护字段,如果你想修改类的实现,则会影响这些程序员,这违背了数据封装的精神。而受保护的方法更具有实际意义。
访问修饰符小结:
1)private:仅对本类可见。
2)public:对外部完全可见。
3)protected:对本包和所有子类可见。
4)默认(不需要修饰符):对本包可见。
8.继承与组合:继承是is关系,组合是has关系。比如一本书,应该是学生拥有这本书(组合),而不是学生是这本书(继承)。
class Book {
protected String name;
public String getName() {...}
public void setName(String name) {...}
}
class Student extends Person {
protected Book book;//组合
protected int score;
}
9.继承设计技巧:
1)将公共操作和字段放在超类中。
2)不要使用受保护字段。
//破坏封装性
//同一个包中的所有类都可以访问protected字段,而不管他们是否在子类中定义。
3)使用继承实现“is-a”关系。
//可能存在滥用,钟点工不是特殊的员工。
4)除非所有继承的方法都有意义,否则不要使用继承。
//因为超类中的某些方法,可能打破你对子类的定义。
5)在覆盖方法时,不要改变预期的行为。
//替换原则不仅适用于语法,也适用于行为。关键在于在子类中覆盖方法时,不要偏离最初的设计想法。
6)使用多态,而不要使用类型信息。
//使用多态性固有的动态分配机制执行正确的动作,远比使用多个类型检测的代码更易于维护和扩展。
二、多态
1.向上转型:“is-a”规则指出,子类的每个对象也是超类的对象,例如:每个经理都是员工。“is-a”规则的另一种表述是替换原则:程序中出现超类对象的任何地方都可以使用子类对象替换。即,可以将子类对象赋给超类变量:Person p = new Student();
这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
2.多态:一个对象变量可以指示多种实际类型的现象称为多态。即一个Employee类型的变量既可以引用一个Employee类型的对象,也可以引用Employee类的任何一个子类的对象。
Manager boss = new Manger(...);
Employee[] staff = new Employee[3];
staff[0] = boss;
boss.setBonus(5000); //ok
staff[0].setBonus(5000); //error!
staff[0]声明的类型是Employee,而setBonus不是Employee类的方法。所以父类不能调用子类的方法。
3.动态绑定:在运行时能够自动地选择适当的方法,称为动态绑定。已知子类Manager重写了父类Employee的getSalary()方法:
public double getSalary(){
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
我们创建一个经理并设置他的奖金,然后定义一个包含3个员工的数组:
Manager boss = new Manager("Carl",80000);
boss.setBonus(5000);
var staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee("Harry",50000);
staff[2] = new Employee("Tony",40000);
//输出每个人的薪水:
for(Employee e : staff)
System.out.println(e.getName() + " " + e.getSalary());
//输出:
Carl 85000.0
Harry 50000.0
Tommy 40000.0
我们可以看到staff[0] 调用了子类重写的getSalary()方法,而staff[1] 和staff[2] 调用了父类本身的getSalary()方法。也就是说,尽管这里将e 声明为Employee 类型,但实际上e 既可以引用Employee 类型的对象,也可以引用Manager类型的对象。staff[0] 实际引用为一个Manager对象,虚拟机知道e 实际引用对象的类型,因此能够正确地调用相应的方法。
4.隐藏:与重写类似,方法的重写是子类覆盖父类的对象方法。隐藏,就是子类覆盖父类的静态方法。
//父类
public class Hero {
public String name;
protected float hp;
//类方法,静态方法
//通过类就可以直接调用
public static void battleWin(){
System.out.println("hero battle win");
}
}
//子类
public class ADHero extends Hero{
//隐藏父类的battleWin方法
public static void battleWin(){
System.out.println("ad hero battle win");
}
public static void main(String[] args) {
Hero.battleWin(); //hero battle win
ADHero.battleWin(); //ad hero battle win
}
}
而与动态绑定不同,当父类引用指向子类对象时,依然调用的是父类的静态方法。
Hero h =new ADHero();
h.battleWin(); //hero battle win
h= null;
h.battleWin(); //hero battle win
虽然h指向的是一个子类对象,但是依然调用的是父类的类方法。甚至在h指向一个空对象null的时候,也能够成功调用battleWin()方法。所以和h指向哪个对象无关,只和h的类型有关系。当然,也不建议通过对象去调用类方法,而应该直接通过类去调用类方法,从而规避理解上的歧义。
5.理解对象方法调用:假设调用x.f(args), x是类C的一个对象。
1)编译器首先查看对象的声明类型和方法名。
//编译器会一一列举C类中所有名为f的方法和其超类中所有名为f而且可访问的方法(超类私有方法不可访问)
2)编译器要确定方法调用中提供的参数类型。
//重载解析:在所有名为f的方法中选择与所提供的参数类型完全匹配的方法。
3)静态绑定:如果调用的方法是private、static、final或者构造器,那么编译器可以准确的知道该调用哪个方法。//与此对应,如果调用的方法依赖于隐式参数的实际类型,那么必须在运行时使用动态绑定。//本例中,编译器会利用动态绑定生成一个调用f(String)的指令。
4)采用动态绑定时虚拟机必须调用与x(隐式参数)所引用对象的实际类型对应的那个方法。
//若它的实际类型的类没有定义此方法,则会在它的超类中寻找。而每次搜索时间开销很大,因此虚拟机预先为每个类计算了一个方法表。其中列出了所有方法的签名和要调用的实际方法,在真正调用方法的时候,虚拟机仅查找这个表即可。
动态绑定有一个非常重要的特性:无须对现有代码进行修改就可以对程序进行扩展。
6.向下转型(强制类型转换):如果把一个超类类型强制转型为子类类型,就是向下转型(downcasting)。使用强制类型转换的唯一原因就是:要在暂时忽略对象的实际类型之后使用对象的全部功能。
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!
Person类型p1实际指向Student实例,Person类型变量p2实际指向Person实例。在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,把p2转型为Student会失败,因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
判断向下转型是否能成功的方法是:观察某个父类引用是否指向这个子类对象。Java运行时发现转换失败,Java虚拟机会产生ClassCastException异常。
7.因此我们在强制类型转换之前,需要使用instanceof操作符判断。instanceof实际上判断一个变量所指向的实例是否是指定类型,或者指定类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false。
Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false
//在这里,Person和Student即为指定类型
Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true
//因为null没有引用任何对象,当然也不会引用Student类型的对象
Student n = null;
System.out.println(n instanceof Student); // false
Person p = new Student();
if (p instanceof Student) {
// 只有判断成功才会向下转型:
Student s = (Student) p; // 一定会成功
}
综上所述:1)只能在继承层次内进行强制类型转换。
2)在将超类转换成子类前,应该使用instanceof检查。
但是实际上通过强制类型转换来转换对象类型通常不是一种好的做法。因为实现多态性的动态绑定机制能够自动地找到正确的方法。只有在使用子类中的特有方法时才需要进行强制类型转换。
8.Final修饰符:
1)用final修饰方法:继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。
class Person {
protected String name;
public final String hello() {
return "Hello, " + name;
}
}
Student extends Person {
// compile error: 不允许覆写
@Override
public String hello() {
}
}
2)用final修饰类:如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。
final class Person {
protected String name;
}
// compile error: 不允许继承自Person
Student extends Person {
}
3)用final修饰字段:构造对象后就不允许改变他们的值,如果将一个类声明为final,只有其中的方法自动成为final,而不包括字段。也可以在构造方法中初始化final字段:
class Person {
public final String name;
public Person(String name) {
this.name = name;
}
}
//这种方法更为常用,因为可以保证实例一旦创建,其final字段就不可修改。
将方法或类声明为final的主要原因是:确保它们不会在子类中改变语义。如:String类是一个final类,这意味着不允许任何人定义String的子类,即如果有一个String引用,它引用的一定是String对象,而不可能是其他类的对象。
三、抽象类
1.抽象方法:在继承层次结构中,位于上层的类更具有一般性。它的方法在子类中实现很容易,但对于父类来说并没有什么意义。此时可以使用abstract关键字:把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为抽象方法本身无法执行,所以该类也无法被实例化,编译报错。故包含一个或多个抽象方法的类必须被声明为抽象的,才能正确编译它。
public class Main {
public static void main(String[] args) {
Person p = new Student();
p.run();
}
}
abstract class Person {
public abstract void run();
}
class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}
2.抽象类:使用abstract修饰的类就是抽象类。除了抽象方法之外,抽象类还可以包含字段和具体方法。而抽象方法充当着占位方法的角色,它们在子类中具体实现。即使不含抽象方法,也可以将类声明为抽象类。抽象类不能实例化,但可以创建一个具体子类的对象。扩展抽象类有两种方法:1)在子类中保留抽象类中的部分或所有方法仍未定义,这样就必须将子类也标记为抽象类。2)定义全部方法,则子类不为抽象。
3.面向抽象编程:当我们定义了抽象类Person,以及具体的Student、Teacher子类的时候,我们可以通过抽象类Person类型去引用具体的子类的实例:
Person s = new Student();
Person t = new Teacher();
// 不关心Person变量的具体子类型:
s.run();
t.run();
这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现。
四、Object类
Object类是所有类的父类,在Java中每个类都扩展了Object。
1.Object类型变量:可以使用Object类型的变量引用任何类型的对象。
Object obj = new Emploee("Harry",35000);
但Object类型的变量只能用于作为各种值的一个泛型容器,要想对其中的内容进行具体的操作,必须要弄清楚对象的原始类型,并进行相应的强制类型转换。
Employee e = (Employee) obj;
在Java中,只有基本类型不是对象,例如:数值、字符、布尔类型等。而所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。
2.equals方法:Object类中的equals方法用于检测一个对象是否等于另一个对象,Object类中实现的equals方法将确定两个对象引用是否相等(“==”)。不过,经常需要基于状态检测对象的相等性,如果两个对象有相同的状态,才认为这两个对象是相等的。此时我们可以在子类中重写equals方法:首先调用超类的equals,然后再比较子类中的实例字段。
public boolean equals(Object otherObject)
{
if(!super.equals(Object otherObject)) return false;
Manager other = (Manager) otherObject; //强制类型转换
return bonus == other.bonus;
}
3.相等性测试与继承:Java语言规范要求equals方法具有以下特性:
1)自反性:对于任意非空引用x,x.equals(x)应当返回true。
2)对称性:对于任意非空引用x和y,当且仅当y.equals(x)返回true时,x.equals(y)也返回true。
3)传递性:对于任意非空引用x,y和z,如果x.equals(y)返回true,y.equals(z)也返回true,x.equals(z)也应该返回true。
4)一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。
5)对于任意非空引用x,x.equals(null)应该返回false。
但是许多程序员喜欢使用instanceof进行检测:
if(!(otherObject instanceof Employee)) return false;
这表明允许otherObject是Employee类的一个子类,就对称性规则来说,当隐式参数和显式参数不属于同一个类时:父类对象调用equals与子类对象比较,如果使用instanceof进行检测,将返回true。而子类对象调用equals与父类对象比较,将返回false或抛出异常。这不满足对称原则。
所以有两种不同的情形:
1)如果子类可以有自己的相等性概念,则对称性需求将强制使用getClass检测(getClass方法将返回一个对象所属的类)。如:只要对应的字段相等,就认为两个对象相等,这时我们就要使用getClass检测。
2)如果由超类决定相等性概念,那么可以使用instanceof检测,这样可以在不同子类的对象之间进行相等性比较。如:使用员工ID作为相等性检测标准,并且这个相等性概念适用于所有子类,就可以使用instanceof检测。
4.编写完美equals方法的建议:
1)显式参数命名为otherObject;
2)检测this与otherObject是否相等;
if(this == otherObject) return true;
3)检测otherObject是否为null;
if(otherObject == null) return fasle;
4)比较this与otherObject的类,如果equals语义在子类中改变,使用getClass检测:
if(getClass() != otherObject.getClass()) return false;
如果所有子类有相同的相等性含义,可以使用instanceof检测:
if(!(otherObject instanceof ClassName)) return false;
5)将otherObject强制转换为相应的类类型变量:
ClassName other = (ClassName) otherObject;
6)根据相等性概念的要求来比较字段。使用 == 比较基本类型字段,使用Objects.equals比较对象字段。
return field1 == other.field1 && Objects.equals(field2,other.field2) && ...;
//static boolean equals(Object a, object b)
//如果a,b都为null,返回true;如果只有其中之一为null,返回false;否则返回a.equal(b)
如果在子类中重新定义equals,就要在其中包含一个super.equals(other) 调用。
5.toString 方法:它会返回表示对象值的一个字符串。
绝大多数的toString方法都遵循以下格式:类的名字,随后是一对方括号括起来的字段值。
public String toString(){
return getClass().getName() + "[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]";
}
通过getClass().getName()获得类名的字符串,这样toString方法也可以由子类调用。当然子类的程序员应该定义自己的toString方法,并加入子类的字段:
public String toString(){
return super.toString() + "[bonus=" + bonus + "]";
}
6.只要对象与一个字符串通过操作符“+”相连接,Java编译器就会自动地调用toString方法来获得这个对象的字符串描述。即,“”+x,代表一个空串与x的字符串表示(x.toString())相连接。与toString不同,即使x是基本类型,这条语句照样能够执行。Object类定义的toString方法,可以打印对象的类名和散列码。toString方法是一种非常有用的调试工具,在标准类库中,许多类都定义了toString方法,以便用户能获取一些有关对象状态的有用信息。强烈建议为自定义的每个类添加toString方法。
五、接口
1.接口(interface):接口不是类,而是对希望符合这个接口的类的一组需求。接口用来描述类应该做什么,而不指定它们具体应该怎么做。在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。如果一个抽象类没有字段,所有方法全部都是抽象方法:
abstract class Person {
public abstract void run();
public abstract String getName();
}
就可以把该抽象类改写为接口:interface。在Java中,使用interface可以声明一个接口:
interface Person {
void run();
String getName();
}
接口定义的所有方法默认都是public abstract的,所以不必提供这两个修饰符。接口还可以定义常量,但接口绝不会有实例字段,在Java 8之前,接口中也不会实现方法。提供实例字段和方法实现的任务应该由实现接口的那个类来完成,因此,可以将接口看做是没有实例字段的抽象类。
2.