继承
1.继承是类和类之间的一种关系
java中的类和类之间的关系有很多中,继承只是其中一种,其他的还有依赖、组合、聚合等
2.继承关系的俩个类,一个是子类,一个是父类
子类也可以称为派生类,父类也可以称为基类
子类继承父类使用关键字extends来表示。
例如:
public class Student extends Person{
}
3.子类和父类之间,从意义上来说,需要有"is a"的关系
例如:
public class Student extends Person{}
注意:student is a person
例如:
public class Dog extends Animal{}
注意:dog is a animal
注意,对于A类和B类之间的继承这种关系,不仅要从语法角度去考虑怎么编写,还要从意义这个角度去考虑: A类和B类之间有继承关系是否合适。
例如,从语法上讲,一个类可以继承任何的另一个类,只要满足语法要求即可,但是从意义上来考虑的话,这种继承关系可能并不合适。这个关系是否合适,可以使用"is a"先做出一个基本的判定。
4.java中的【类和类】之间的继承,是单继承
类和类之间、接口和接口之间都可以有继承关系
但是类和类之间,是单继承
接口和接口之间,是多继承
注意,类和接口之间可以有【实现】的关系
单继承指的是,一个类只能有一个【直接】的父类型,但是一个类可能会有很多个超类,超类指的是一个类的父类型的父类型,以及再往上的父类型。
java中任何一个类(除了Object),都是有且只有一个【直接】继承的父类型,除此之外,一个类还可以有很多【间接】继承的父类型。
注意,虽然一个类只能【直接】继承一个父类型,但是一个父类型可以被多个不同的类继承,也就是一个父类可能会有很多【直接】的子类型。
5.父类中的属性和方法,可以被子类继承
一个子类继承了父类,那么就可以继承父类中的属性和方法,但是在子类中是否可以【直接】使用这些属性和方法,需要看这些属性和方法在父类中原有的访问权限修饰符是什么。
控制类、属性、方法是否可以被访问的修饰符有:public protected default private
例如:
父类中的属性和方法使用了public修饰,那么在子类中就【可以直接】使用
父类中的属性和方法使用了private修饰,那么在子类中就【不可以直接】使用
注意,父类中无论是public修饰的还是private修饰的属性和方法,都是会被子类继承的,只是在子类中能不能直接访问这些属性和方法,就和这些修饰符有关系了。
子类继承父类后,在子类中,怎么才能算是可以直接访问父类中的属性和方法?
例如:
public class Person{
private String name;
public int age;
private void say(){}
public void run(){}
}
public class Student extends Person{
private String msg;
//在子类中,是否可以直接访问从父类中继承过来的属性和方法
public void test(){
//访问子类自己的属性
System.out.println(msg);
//【直接】访问父类中继承过来的age属性
System.out.println(age);
//报错,name是父类中私有属性,在子类中不能直接访问
//System.out.println(name);
//【直接】调用父类中继承过来的run方法
run();
//报错,say方法是父类中私有方法,在子类中不能直接访问
//say();
}
}
注意,父类中的构造器,是不能被子类继承的,但是在子类的构造器中,可以调用父类的构造器,需要使用super关键字。
注意,子类调用父类的构造器有俩种情况:
1.隐式调用
如果父类中【有】默认的无参构造器,那么在子类的构造器中就会隐式的自动调用父类中的默认无参构造器
例如:
public class Person{
//父类中的无参构造器
public Perosn(){}
}
public class Student extends Person{
public Student(){
//在这里,就会默认自动调用父类的无参构造器
//这种方式就是隐式调用
}
}
2.显示调用
如果父类中【没有】默认的无参构造器,那么在子类中就需要我们显式的手动调用父类中的有参构造器
例如:
public class Person{
//父类中的有参构造器
public Perosn(String name){}
}
public class Student extends Person{
public Student(){
//这时候,无法隐式自动调用,因为父类中没有无参构造器
//必须手动显式调用父类中的构造器
super("tom");
}
}
注意,隐式调用和显式调用父类中的构造器,区别在于有没有把调用父类构造器的那一句代码给写出来。
注意,没有写出来的话,那么就是默认的隐式调用,但是隐式调用只能调用父类中无参的构造器,如果父类中没有无参构造器,这时候就会报错了。
5.Object类
每个类如果没有显式的指定它的父类型,那么编译就默认直接继承了Object类。
例如:
public class Student{}
编译后变为:
public class Student extends Object{}
所以,java中的每一个类都是直接或者间接的继承了Object类,并且每一个对象和Object都有"is a"的关系,
//一切皆为对象
anyObj is a Object
anyObj instanceof Object // true
注意,任意对象也包含数组对象
Object中已经提供了一个对象应该具备的最基本的方法,所以java中的任意对象,都可以调用到从Object中继承了最基本的方法,例如:toString equals getClass hashCode wait notify clone等
思考:继承有什么好处?
思考:子类构造器中为什么要调用父类的构造器?
子类继承了父类,那么就把父类中的属性和方法都继承了过来,但是我们希望子类中继承过来的属性都是已经在父类中完成了初始化工作之后的,因为这样我们就可以拿着这些属性直接使用了,父类中对这些属性完成初始化工作的代码默认就在构造器中,所以我们子类构造器里面首先会调用父类中的构造器,先完成对父类中属性的初始化工作,然后再执行子类自己的构造器中的代码,如果我们对父类构造器中初始化的工作不满意,那么我们还可以在子类构造器的代码中对继承过来的属性进行修改,以便覆盖掉父类初始化的值。
super关键字
子类继承父类之后,在子类中可以使用关键字this表示访问子类中的属性、方法、构造器,也可以在子类中使用super关键字表示访问父类中的属性、方法、构造器。
1.访问父类中的属性
例如:
public class Person{
protected String name = "zs";
}
public class Student extends Person{
private String name = "lisi";
public void test(){
System.out.println(name);
//注意,这时候this和super可以区分访问的是哪个name
System.out.println(this.name);
System.out.println(super.name);
}
}
注意,如果父类中的属性,是private修饰的,那么使用super也是不能访问的。
例如:
public class Person{
//注意父类中属性的修饰符
private String name = "zs";
}
public class Student extends Person{
private String name = "lisi";
public void test(){
//编译报错,无法访问父类中private修饰的属性
System.out.println(super.name);
}
}
注意,如果父类中的属性name被继承了,子类中有写了一个自己的属性那么,那么这时候this和super就可以区分我们访问的name到底是哪一个name属性
注意,如果父类中的属性name被继承了,子类中也没自己定义新的name属性,那么这时候,直接使用name和this.name和super.name这三种方式访问都是同一个name属性
例如:
public class Person{
//注意父类中属性的修饰符
public String name = "zs";
}
public class Student extends Person{
public void test(){
//编译都通过,且访问的同一个name属性
//就是从父类中继承过来的name属性
System.out.println(name);
System.out.println(this.name);
System.out.println(super.name);
}
}
2.调用父类中的方法
例如:
public class Person{
public void run(){
System.out.println("person run..");
}
}
public class Student extends Person{
public void run(){
System.out.println("student run..");
}
//在这里可以访问到自己类中的run方法
//也可以访问到父类中的run方法
//为了区分调用的是哪一个run方法
//可以使用this和super
public void test(){
run();
this.run();
super.run();
}
}
注意,如果调用或访问的时候,没有因为同名而带来的歧义的话,可以不使用this或者super,直接通过方法名或者属性名进行调用就可以了。
3.调用父类中的构造器
例如:
public class Person{
}
public class Student extends Person{
//编译通过
//默认使用super()调用父类中的无参构造器
//隐式调用
public Student(){
}
}
例如:
public class Person{
protected String name;
public Person(String name){
this.name = name;
}
}
public class Student extends Person{
//编译报错
//因为默认执行的代码是super()
//但是父类中已经没有了无参构造器
public Student(){
}
}
例如:
public class Person{
protected String name;
public Person(String name){
this.name = name;
}
}
public class Student extends Person{
//编译通过
//手动使用super调用父类中的有参构造器
//显式调用
public Student(){
super("tom");
}
}
注意,无论是隐式调用还是显式调用父类中的构造器,super语句一定要出现在子类中构造器中的第一行代码。
例如:
public class Person{
protected String name;
public Person(String name){
this.name = name;
}
}
public class Student extends Person{
//编译报错
//使用super调用了父类构造器,但是它不是第一行代码
public Student(){
System.out.println("子类构造器被调用");
super("tom");
}
}
注意,在子类的构造器中,可以使用this()来调用子类其他的构造器,也可以使用super()来调用父类中的构造器,但是这俩种用法不能同时出现,因为它们俩个任何一个出现都要放在构造器中的第一行代码的位置。
总结:在创建一个子类对象的时候,肯定要调用到子类中的一个构造器,并且在子类构造器执行之前,一定是会先调用到父类中的构造器的。这样做的目的就是在子类的构造器执行之前,先执行父类的构造器完成对父类中属性的初始化,保证子类继承过来的父类中的属性是已经初始化好可以直接使用的。
注意,this在类中是可以打印输出的,但是super不能
public class Student{
public void test(){
System.out.println(this);//编译通过
System.out.println(super);//编译报错,语法不支持
}
}
30.16 方法的重写(覆盖)
简单的理解,重写就是子类继承了父类中的某一个方法,然后在子类中又把这个方法重新定义了一遍。只不过在这个过程中,我们需要注意一些细节和规则。
1.方法的重写,存在于子类和父类之间(可以是直接父类,也可以是间接父类),之前学习过的方法重载,是存在于同一个类中。
方法的重载:同一个类中
方法的重写:子类和父类之间
2.类中静态方法和非静方法,对重写的影响
父类中的静态方法,不能被子类中重写为非静态方法,编译报错
错误信息:被覆盖的方法为static
父类中的非静态方法,不能被子类中重写为静态方法,编译报错
错误信息:覆盖的方法为static
父类中的静态方法,子类可以继承,子类中也可以定义一个和父类中相同名字的静态方法,但是这个不是重写。 编译通过
重写只能发生在子父之间非静态方法上。因为静态方法是属于类的,可以类名直接调用,它和对象没有关系的。
父类中的非静态方法,可以被子类中继承并且重写,但是重写的时候也必须是非静态方法。这个就是正常的方法重写现象。
3.父类中的私有方法不能被子类重写
子类继承父类后,同时也继承到了父类中的方法,如果需要对某个方法进行重写,这个被继承的方法需要满足以下条件:
必须是非静态的方法
该方法在子类中必须是可以直接访问的
注意,父类中的一个方法,它的修饰符可能是以下几种:
public 子类中继承后一定可以直接访问
protected 子类中继承后一定可以直接访问
default 子类中继承后可能可以直接访问
子父类同包,则可以直接访问
子父类不同包,则不可以直接访问
private 子类中继承后一定不可以直接访问
注意,能直接访问就说明这个继承过来的方法可以重写,不能直接访问则说明不能重写。
例如:
public class Person{
private void test(){}
}
//编译通过,但是【不是重写】,只是在俩个类中有各自的一个私有方法,只是这个俩个私有方法的名字和参数列表相同而已。
public class Student extends Person{
private void test(){}
}
4.方法重写的语法要求
方法名必须相同
参数列表必须相同
访问控制修饰符可以相同,也可以不同,如果不同的话,只能扩大,不能缩小
public > protected > default > private
方法抛出的异常可以相同,也可以不同,如果不同的话,只能缩小,不能扩大
注意,异常类型的范围大小,是根据异常类型中的子父类关系来确定,例如 Exception异常类型是ArraysIndexOutOfBoundsException异常类型的父类型,所以 Exception异常类型的范围比ArraysIndexOutOfBoundsException异常类型的范围大。
子类异常范围小,父类型异常范围大。
如果返回类型是引用类型:
方法的返回类型可以相同,也可以不同,如果不同的话,重写之后方法的返回类型必须是原方法的返回类型的子类型(兼容)
例如:
public class Person{
public Object test(){
return null;
}
}
public class Student extends Person{
//编译通过,保持一致可以
public Object test(){
return null;
}
//编译通过,返回类型和原方法返回类型兼容也可以
public String test(){
return null;
}
}
如果返回类型是基本类型:
重写后的方法的返回类型必须和父类中原方法保持一致。
例如:
public class Person{
public int test(){
return 0;
}
}
public class Student extends Person{
//编译通过,重写后必须和原方法的返回类型保持一致
public int test(){
return 0;
}
//编译报错
public long test(){
return 0;
}
//编译报错
public byte test(){
return 0;
}
//编译报错
public short test(){
return 0;
}
}
注意,在绝大数重写情况下,方法的修饰符、返回类型、方法名、参数列表、抛出异常这几个部分,子类中重写的方法默认情况下都是和父类中的方法保持一致的。
思考1,如果各个部分都和父类中方法保持一致,那么这个方法的重写,我们重写的是什么?
我们主要重写的是方法中的代码,所以大多数情况方法声明部分和父类中的方法保持一致。
//方法的声明
public void test();
//方法的声明和方法的实现
//重写的是方法的实现
public void test(){}
思考2,为什么要对从父类中继承过来的方法进行重写?
子类中继承过来的方法,原来是定义在父类中的,现在被子类继承了,而子类本身又是对父类型的扩展,所以这个时候,从父类中继承过来的方法,可能在子类中已经不是很适用了,所以我们需要重写对这个方法进行重写。
或者说子类中不满意原来父类中对这个方法的实现,那么在子类中就需要对这个方法进行重写
例如:通过这个例子,说明了为什么在程序中会进行重写,以及重写之前和重写之后,对方法的调用有什么影响
public class Person{
public void sayHello(){
System.out.println("你好!");
}
}
public class Student extends Person{
public void sayHello(){
System.out.println("hello! memeda");
}
}
main:
Student stu = new Student();
//注意,如果子类中没有重写sayHello方法,那么这里调用的就是从父类中继承过来的sayHello方法
//如果子类中重写了sayHello方法,那么这里调用的就是重写之后的sayHello方法
stu.sayHello();
例如:java-se api中的Object和String的示例
//toString方法定义在Object中
//java中所有类都是Object的子类
//任何类的对象都可以调用到toString方法
//toString方法的作用就是返回当前对象的默认字符串形式
public class Object{
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
}
//String继承了Object
//从而继承到了toString方法
//因为字符串本身已经是字符串了,所以不需要返回Object中toString方法里面定义的对象默认的字符串形式
//所以String类对toString方法进行了重写
//重写的代码中,把字符串自己本身(this)返回,因为这个字符串对象自己已经是一个字符串了
public class String extends Object{
/**
* This object (which is already a string!) is itself returned.
*
* @return the string itself.
*/
public String toString() {
return this;
}
}
main:
String str = "hello";
str.toString();