继承
类、超类和子类
子类将拥有父类/超类中的全部方法和域(private不算)。同时也可以在这基础上加入新的方法和域。子类能否访问并继承父类方法和域详见下表。
值得注意的是,访问protected方法和变量可以通过导入包的方式实现访问
访问包位置\类修饰符 | private | protected | public |
---|---|---|---|
本类 | 可见 | 可见 | 可见 |
同包其他类或子类 | 不可见 | 可见 | 可见 |
其他包的类或子类 | 不可见 | 不可见 | 可见 |
子类的定义
格式如下
public class ChildClass extends ParentClass
{
}
重写(override)
有些书里会翻译成覆盖,其实就是重写。常常与重载相比较:
- 重载:方法名相同,参数不同的多个重名函数
- 重写:子类中要求方法名和参数与父类完全相同的函数。要求返回值和异常比父类小或相同, 访问修饰符比父类大或相同。并且子类的静态方法不能重写,也能不能重写父类的静态方法。但是可以子类静态方法重写父类静态方法。
特殊注意:
- final修饰的方法不能被重写,可以被重载
- final修饰的类不能被继承
- private 方法隐式添加了final
- 若父类构造函数非空,则子类必须也有一个与父类同参的构造函数
super方法
由于在子类中,重写过的父类方法是隐藏的,所以调用时无法直接调用,否则会直接调用子类的方法,因此,我们需要使用super关键字来调用父类方法。
格式为:super.method();
super并非一种对象的引用,也无法赋值给其他对象变量,他只是个单纯的关键字,类似C++中的:ParentClass::method();
若父类构造函数含显式参数,则子类必须重写一个构造函数
并且需要用super();
方法进行初始化。
好了,现在你已经基本掌握了JAVA中继承的基础知识了,让我们来编写一个范例吧
例:
创建一个Employee类,定义name、salary、hireDay等属性
创建一个Manager类,使其继承Employee类并额外加一个奖金属性
在main函数中测试这两个类
class Employee
{
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String name,double salary,LocalDate hireDay)
{
this.name=name;
this.salary=salary;
this.hireDay=hireDay;
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise=salary*byPercent/100;
salary+=raise;
}
}
class Manager extends Employee
{
double bonus;
public Manager(String name,double salary,double bonus,LocalDate hireDay)
{
super(name,salary,hireDay);
this.bonus=bonus;
}
public double getSaraly()
{
double baseSaraly=super.getSalary();
return (baseSaraly+bonus);
}
}
其中,Employee类显然是含有一个带三个参数的构造函数,那么子类Manger就需要一个至少带三个以上的参数构造函数。
而且,这个构造方法需要用super方法来调用父类完成参数的继承。
即,类继承时,构造函数也要相应继承
除此之外,super还可以用于帮助子类调用父类私有变量
理解方法调用
假设要调用x.f(INTgs)。x为类C的一个对象,f为C中一个方法,INT为一个int型参数(同理STRING、DOUBLE等)
-
编译器查看对象的声明类型和方法名。假设调用x.f(INT)。需要注意的是:可能存在多个名字为f但是参数不同的方法。例如:可能同时存在f(int)和f(String)。编译器会一一列举所有C中的f方法和其超类中能访问到的名为f的方法。
-
编译器获得了所有待选方法名单后,就会查看方法的参数类型。若有完全匹配的参数类型就会立即调用。例如:类C中同时有f(int) and f(double),而调用语句为x.f(INT),则会立即调用x.f(int)。这个过程被称为重载解析
-
若是private、static、final方法或构造器,那么编译器会准确获知需要调用哪个方法。这种调用方式被称为静态绑定。
-
若每次调用方法都需要进行搜索,则会造成巨大的资源和时间的开销。因此,虚拟机为每个类预先创建了一个方法表,需要调用方法时,虚拟机直接查表即可。但仅仅在动态绑定时需要,静态方法由于在加载时直接和类信息一起加载到方法区,无需创建对象,因此也无须通过方法表调用
额外理清一下动态绑定与静态绑定
- 动态绑定:对大多数方法和类来说都需要通过构造器创建或绑定对象来创建。为了减少频繁搜索带来的巨额开销,虚拟机会为每个类创建一个方法表,每次调用时只需要查表即可获取需要调用的方法。这个过程即为动态绑定
-静态绑定:以private、static、final修饰的静态方法,在每次加载类时就会直接被加载到方法区,系统直接调用即可,无需构造器方法表等。此过程即为静态绑定
final类和方法——阻止继承
类中的final无法继承到子类中,子类也无法读取,除非使用super方法或关键字。除此之外fianl类也无法被其他类继承(final类中的方法也会自动成为fianl方法)
fianl可以让我们阻止某些重要的类或方法被错误重写,final
也可以避免动态绑定带来的额外系统开销
强制类型转换
double x=3.1415926;
int nx=(int)x;
基本数据类型转换没啥好说的,转换时可能会丢失部份信息量
重点是,有时很可能需要将某个类对象引用转换成其他类对象引用。
例如,此前我们的员工管理系统中, staff[] 数组中存储了Employee对象,但是我们如果要将 staff[0] 变为Manager对象呢
Manager boss =(Manager) staff[0];
但是staff[0]未必能满足转换为Manager对象的条件
- 若是将一个子类引用赋给超类变量,编译器是允许的(即可以向上)
- 若是将一个超类引用赋给子类变量,则会抛出一个ClassCastException的异常(不能向下)。
我们可以使用instanceof运算符来判断能否转换
boolean result = obj instanceof Class
其中obj表示一个对象,Class为一个类。若obj是Class的一个对象或是其直接或间接子类、接口实现对象等,result都返回true,否则false
那么我们就可以用下面这个办法来判断staff[]能否转换
if(staff[0] instanceof Manager)
{
boss =(Manager) staff[0];
}
若转换失败则会自动跳过而不会使得整个程序停止运行
总结:
-
只能在继承层次内进行转换,而且一般最好从下向上转换
-
超类转换为子类时最好使用 instanceof进行检查
-
尽量少使用类型转换和instanceof运算符,只要类设计合理,其实往往类型转换都是可以避免的
抽象类
通常来说,位于上层的类往往有着较高的通用性,甚至可能更抽象。比如说,Person类就可以作为Employee类和Student类的超类,因为无论雇员还是学生都是“人”。他们可以具有共有的属性,比如姓名、年龄等就可以直接放在高层次的Person类中。
但是雇员和学生显然存在着诸多不同的属性。如果我们需要对不同种类的人都有相应的“描述”,应该在Person类中提供何种内容呢?
-
一种办法是:在Person类中创建getDescription方法 ,并使得该方法返回空值,到了子类中再对这个方法重写
-
最好的办法还是:使用abstract关键字创建一个抽象类
特别提醒:拥有一或多个抽象方法的类本身也必须被定义为抽象类,但是抽象类中可以有非抽象方法
抽象类通常起到占位作用,相当于一个接口,他的具体实现在子类中。一般可以将所有的抽象方法在抽象超类全部定义出来。这样就不需要再定义抽象子类了。
例
定义一个Person抽象类,并在子类中实现相关功能
public abstract class Person
{
private String name;
private int age;
//private Date date;
public Person(String name,int age)
{
this.name=name;
this.age=age;
//this.date=date;
}
public abstract String getDescription();
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
}
public class text
{
public static void main(String[] args)
{
//Student student=new Student("San",17);
//System.out.println(student.getDescription());
/*Employee employee=new Employee("San",22,200000, LocalDate.of(2022,2,20));
System.out.println(employee.getDescription());
Manager manager=new Manager("San",29,100000000,10000000, LocalDate.of(2022,2,20));
System.out.println(manager.getDescription());
Person person=new Student("San",17);
System.out.println(person.getDescription());*/
Person [] people=new Person[3];
people[0]=new Student("San",17);
people[1]=new Employee("San",22,200000, LocalDate.of(2022,2,20));
people[2]=new Manager("San",29,100000000,10000000, LocalDate.of(2022,2,20));
for (Person p:people)
{
System.out.println(p.getDescription());
}
}
}
class Student extends Person
{
private String name;
private String major= this.getClass().getName();
public Student(String name, int age)
{
super(name, age);
}
public String getDescription()
{
return String.format("A %d yeas old "+major+" named "+getName(),getAge());
}
}
class Employee extends Person
{
private String name;
private double salary;
private LocalDate hireDay;
private String major=this.getClass().getName();
public Employee(String name,int age,double salary,LocalDate hireDay)
{
super(name, age);
this.name=name;
this.salary=salary;
this.hireDay=hireDay;
}
@Override
public String getDescription()
{
return String.format("A %d years %s with a salary of $ %.2f named "+name+" hired on "+hireDay,getAge(),major,getSalary());
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise=salary*byPercent/100;
salary+=raise;
}
}
class Manager extends Employee
{
double bonus;
private String major=this.getClass().getName();
public Manager(String name,int age,double salary,double bonus,LocalDate hireDay)
{
super(name,age,salary,hireDay);
this.bonus=bonus;
}
public String getDescription()
{
return String.format("A %d years %s with a salary of $ %.2f named "+getName()+" hired on "+getHireDay(),getAge(),major,getSalary()+bonus);
}
public double getSaraly()
{
double baseSaraly=super.getSalary();
return (baseSaraly+bonus);
}
}
抽象类不可被实例化,但是可以定义对象,但是他只能引用非抽象子类对象。例如:
Person person=new Student("San",17);
这是上面的例子的引用代码。最终实现和直接创建子类Student类对象是一样的:
A 17 yeas old Student named San
同理,p.getDescription()
也是调用的子类的方法
受保护访问
让我们再复习一下访问修饰符权限范围
域\修饰符 | private | protected | public |
---|---|---|---|
同类 | 可以 | 可以 | 可以 |
同包不同类 | 不可以 | 可以 | 可以 |
不同包 | 不可以 | 不可以 | 可以 |
一般来说最好将类中的域标记为private,将方法标记为public。
private私有内容对其他类是不可见的,包括子类也是如此。
但是protected是对子类可见的,即便不是同包也可见
Object:所有类的超类
Object是java所有类的始祖,但是不需要写明extends Object
可以用Object类型变量引用任何类型的对象:
Object obj = new Student("San",17)
但是Object数据类型只能作为各种值的“容器”,我们无法直接对Object类型的数据进行修改,而必须转换为其原始类型(一般Object向下转换是被允许的):
Student student=(Student) obj;
除此之外,当你不确定数组中的对象的类型时,也可以先都统一定为Object,后面根据需求可以对数组中进行变换。
后续还有150余种equals,hashCode和toString
未完待续