Java继承 学习

思维导图

在这里插入图片描述

导学

在本章节内容中,我们将继续来学习面向对象三大特性之一的继承。之前的封装,实际上是针对一个类进行隐藏和访问控制操作,而即将要学习的继承,其实是在描述两个类之间的关系。
继承是软件实现复用的重要手段之一
首先,我们来想一想生活中的继承,比如儿子继承父亲的外貌与性格,徒弟继承师傅的手艺等等。这些都是继承,在两个主体之间有着传承的关系。
对于面向对象程序而言,它的编程思想同样来自于生活。我们在程序的开发中同样也会有和生活中一样的感受。比如,我们使用两个类来描述猫和狗:
在这里插入图片描述
我们发现在描述过程中,它们有一些相同的属性和方法,也有一些不同的属性和方法。那么,如果我们接着描述其他的动物,是不是也会产生大量的重复的代码呢?
最好能有一种方法能把这些重复代码收集起来,然后我每次要使用的时候,就直接调用这个方法,进行重复利用就可以了。而,这种方法就是我们今天要学习的继承了。
比如我们可以将猫类和狗类中的共同属性和方法抽取出来,组成一个动物类:
在这里插入图片描述
当猫狗类和动物类实现继承关系时,猫狗类就可以直接使用动物类中属性和方法了,而不必在写那些重复的代码了。即使我们再写如企鹅类,狮子类,乌龟类等,也可以去继承动物类,不必再写那些重复的代码。
将一些具有相似逻辑的类中的公共的属性和方法抽取出来,组成一个类,这个类我们称之为父类。父类和子类是一种一般和特殊的关系。例如水果和苹果,苹果是一种特殊的水果
所以,我们发现继承其实有着如下的特点:

  • 利于代码复用
  • 缩短开发周期

那么,说了这么多,我们再来对继承做个总结吧:

  • 一种类与类之间的关系
  • 使用已存在的类的定义作为基础建立新类
  • 新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类的功能!

继承的实现

在Java中使用extends关键字实现类与类之间的继承关系
在这里插入图片描述
在Java中,继承只能是单继承,即子类只能有一个父类,而父类可以被多个子类继承。就像现实生活中孩子只能有一个亲爹,而父亲可能有多个子女一样。
但是Java中存在多层继承,常用的比如每个子类只有一个直接父类,但是该父类依然存在其自身的父类。注意:虽然类继承可以实现代码的复用,但是如果继承的结构过多,也会造成代码段的阅读困难,一般不建议继承的结构超过3层。
当实现继承时,子类可以获得父类非私有属性和方法的使用权
接下来就结合就结合具体的案例来看看吧
父类:

public class Animal {
	private String name;//名称
	
	private int month;//月份
	
	private String species;//品种
	
	//动物共有方法吃东西
	public void eat() {
		System.out.println(this.getName() + "在吃东西");
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getMonth() {
		return month;
	}

	public void setMonth(int month) {
		this.month = month;
	}

	public String getSpecies() {
		return species;
	}

	public void setSpecies(String species) {
		this.species = species;
	}

	public Animal() {
		
	}

	public Animal(String name, int Month, String species) {
		super();
		this.setName(name);
		this.setMonth(Month);
		this.setSpecies(species);
	}
}

子类-猫:

public class Cat extends Animal{
	//猫自己的属性-体重
	private double weight;
	
	//猫自己的方法-跑动
	public void run() {
		//子类可以使用父类的非私有成员,这里可以使用父类的getName()方法,不能使用父类的name属性
		System.out.println(this.getName()+ "是一只" + this.getSpecies() +",它正在快乐地奔跑");
	}

	public double getWeight() {
		return weight;
	}

	public void setWeight(double weight) {
		this.weight = weight;
	}
	
	public Cat() {
		
	}
	
	public Cat(double weight) {
		this.setWeight(weight);
	}
}

子类-狗:

public class Dog extends Animal {
	//自有的属性
	private String sex;
	
	//自有的睡觉方法
	public void sleep() {
		System.out.println(this.getName() + "现在" + this.getMonth() + "个月大,它在睡觉~");
	}
	
	public String getSex() {
		return sex;
	}

	public void setSex(String sex) {
		this.sex = sex;
	}
	
	public Dog() {
		
	}

}

测试:

public class Test {
	public static void main(String[] args) {
		Cat one = new Cat();
		one.setName("花花");
		one.setSpecies("中华田园猫");
		one.eat();
		one.run();
		//sleep()方法不属于父类Animal,也不属于子类Cat中定义的方法
		//one.sleep();
		System.out.println("=====================");
		Dog two = new Dog();
		two.setName("田田");
		two.setMonth(1);
		two.eat();
		two.sleep();
		System.out.println("=====================");
		Animal three = new Animal();
		//three.run();无法访问
		//three.sleep();
	}
}

总结:

  • 一个子类只能有一个父类
  • 子类可以访问父类非私有成员
  • 子类自己自有成员其他兄弟类无法访问
  • 父类不可以访问子类自有成员

方法的重写

在上述案例中,子类都调用了父类的eat()方法。但是对于这样的方法,猫与狗都需要吃东西,但可能各自都有自己不同的想法。比如猫吃鱼,狗吃肉,具体的表现形式不同,那么各自类中的方法体描述也不应该相同。
对于这样的问题,我们可以使用方法的重写来完成这样的操作。对于方法的重写,指的是在子类中重新描述父类中的方法。
方法的重写,要求返回值类型,方法名,参数类型、顺序、个数都要与父类继承的方法完全一致。

public class Cat extends Animal{
	//猫自己的属性-体重
	private double weight;
	
	//猫自己的方法-跑动
	public void run() {
		//子类可以使用父类的非私有成员,这里可以使用父类的getName()方法,不能使用父类的name属性
		System.out.println(this.getName()+ "是一只" + this.getSpecies() +",它正在快乐地奔跑");
	}

	public double getWeight() {
		return weight;
	}

	public void setWeight(double weight) {
		this.weight = weight;
	}
	
	public Cat() {
		
	}
	
	public Cat(double weight) {
		this.setWeight(weight);
	}
	
	@Override//注解,表示方法的重写,方法前面加上@Override 系统可以帮你检查方法的正确性
	public void eat() {
		System.out.println(this.getName() + "爱吃鱼");
	}
}
public class Dog extends Animal {
	//自有的属性
	private String sex;
	
	//自有的睡觉方法
	public void sleep() {
		System.out.println(this.getName() + "现在" + this.getMonth() + "个月大,它在睡觉~");
	}
	
	public String getSex() {
		return sex;
	}

	public void setSex(String sex) {
		this.sex = sex;
	}
	
	public Dog() {
		
	}
	
	@Override
	public void eat() {
		System.out.println(this.getName() + "爱吃肉");
	}
}
public class Test {
	public static void main(String[] args) {
		Cat one = new Cat();
		one.setName("花花");
		one.setSpecies("中华田园猫");
		one.eat();
		System.out.println("=====================");
		Dog two = new Dog();
		two.setName("田田");
		two.setMonth(1);
		two.eat();
	}
}

输出结果:

花花爱吃鱼
=====================
田田爱吃肉

当子类重写父类方法之后,子类对象调用的是重写之后的方法。

方法重载与方法重写

在学习中,我们有可能会把方法重载和方法重写混淆。所以我们来通过一个表格认识一下方法重载与方法重写的区别
方法的签名是由方法的方法名和形参列表(注意:包含方法的参数和类型)组成
在这里插入图片描述

关于属性

对于重写,在Java中只有针对方法的重写,没有针对属性的重写。但是,我们在子类中也是可以定义与父类同名的属性的,此时子类对象调用的是子类的属性。

public class Animal {
    public int temp = 15;
}
public class Cat extends Animal {
    public int temp = 30;
}
public class Test {
    public static void main(String[] args) {
        Cat one = new Cat();
        System.out.println(one.temp);//30
    }
}

访问修饰符

访问控制修饰符能修饰的对象包括:属性、方法、构造器
访问修饰符可以用来限制成员被访问的范围,即用来控制被修饰的成员可以在哪里(包)被使用。
在这里插入图片描述

  • private: 只允许在本类中进行访问(访问权限最小的)
  • public: 允许在任意位置访问(访问权限最大的)
  • protected: 允许在当前类、同包子类/非子类、跨包子类调用;跨包非子类不允许
  • default: 允许在当前类、同包子类/非子类调用;跨包子类/非子类不允许调用

在实际的开发中,大部分的场景使用 private 定义属性,public 定义方法。

访问修饰符对方法重写的影响

子类中重写父类方法时,子类方法的访问修饰符的访问范围需要大于等于父类方法的访问范围

public class Animal {
    protected void eat() {
    }
}
public class Cat extends Animal {
    public void eat() {
    }
    /*void eat() {
        有问题
    }*/
}

super关键字

在上节内容的学习中,我们知道子类可以继承父类的方法,也可以使用自己重写的方法。那么该如何判定子类调用的方法是继承自父类,还是自己重写的呢。

public class Animal {

    public String name;

	//动物共有方法吃东西
	public void eat() {
		System.out.println(this.getName() + "在吃东西");
	}
}

public class Cat extends Animal{
	//猫自己的方法-跑动
	public void run() {
        eat();//调用的是子类重写的方法
		System.out.println(this.getName()+ "是一只" + this.getSpecies() +",它正在快乐地奔跑");
	}
	
	@Override
	public void eat() {
		System.out.println(this.getName() + "爱吃鱼");
	}
	
}

如果我想要使用父类的方法,则需要使用super关键字。super关键字代表着对父类对象的引用。

public void run() {
        super.eat();//调用的是子类重写的方法
	    System.out.println(this.getName()+ "是一只" + this.getSpecies() +",它正在快乐地奔跑");
	}

当然也可以使用super访问父类中允许被子类访问到的任意成员。

public void run() {
        super.eat();//调用的是子类重写的方法
        super.name = "猫猫";
	    System.out.println(this.getName()+ "是一只" + this.getSpecies() +",它正在快乐地奔跑");
	}
  1. 子类重写父类的方法,则父类的方法会被隐藏,隐藏的方法或者成员变量可以通过super关键字访问
  2. 引入super关键字的原因是可以使用被隐藏的成员变量和方法,而且super只能在子类的方法中定义使用
继承的初始化顺序

父类的构造方法不允许被继承,不允许被重写。 那么父类的构造器除了构建父类对象是否有其他作用呢?

public class Animal {
	private String name = "妮妮";//名称
	
	protected int month = 2;//月份
	
	String species = "英短";//品种
	
	public int temp = 15;
	
	private static int st1 = 22;
	private static int st2 = 23;
	
	static {
		System.out.println("我是父类的静态代码块");
	}
	
	{
		System.out.println("我是父类的构造代码块");
	}
	
	//动物共有方法吃东西
	public void eat() {
		System.out.println(this.getName() + "在吃东西");
	}
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getMonth() {
		return month;
	}

	public void setMonth(int month) {
		this.month = month;
	}

	public String getSpecies() {
		return species;
	}

	public void setSpecies(String species) {
		this.species = species;
	}

	public Animal() {
		System.out.println("我是父类的无参构造方法");
	}

	public Animal(String name, int Month, String species) {
		super();
		this.setName(name);
		this.setMonth(Month);
		this.setSpecies(species);
	}
}
public class Cat extends Animal{
	//猫自己的属性-体重
	private double weight;
	
	private static int st3 = 44;
	
	static {
		System.out.println("我是子类的静态代码块");
	}
	
	{
		System.out.println("我是子类的构造代码块");
	}
	
	//猫自己的方法-跑动
	public void run() {
		System.out.println(this.getName()+ "是一只" + this.getSpecies() +",它正在快乐地奔跑");
	}

	public double getWeight() {
		return weight;
	}

	public void setWeight(double weight) {
		this.weight = weight;
	}
	
	public Cat() {
		System.out.println("我是子类的无参构造方法");
	}
	
	public Cat(double weight) {
		this.setWeight(weight);
	}
	
	@Override
	public void eat() {
		System.out.println(this.getName() + "爱吃鱼");
	}
	
}
public class Test {
	public static void main(String[] args) {
		Cat one = new Cat();
		System.out.println(one.temp);
	}
}

继承后的初始化顺序(实例化子类对象的时候):先加载父类静态成员,然后加载子类静态成员,然后父类对象构造(构造代码块–>属性–>方法),最后子类对象构造。

super()

在上节内容中,我们提到子类对象在实例化的时候会先去调用父类对象的构造器。那么子类构造器是否有权利去选择具体使用父类的哪个构造器呢?
在父类中添加一个有参构造器

public Animal(String name, int month, String species) {
	System.out.println("我是父类的有参构造器");
}

在子类中也使用三参构造器

public Cat(String name, int Month, String species) {
	System.out.println("我是子类的有参构造器");
}

测试:

public class Test {
	public static void main(String[] args) {
		Cat one = new Cat("花花",3,"英短");
		System.out.println(one.temp);
	}
}

最终的运行结果表明,子类应用带参构造器实例化对象的时候,同样调用的是父类无参的构造器。
这也就是在实际的开发过程中,我们都会保证一个类中有一个无参的构造器存在的原因。
这也是Java继承的一个特点,如果在子类的构造器中,没有显式的调用父类的构造器,会默认的调用父类的构造器。
那么,问题又来啦,如何在子类的构造器中调用父类的构造器呢?

public Cat(String name, int month, String species) {
    //super();调用的是父类的无参构造方法
	super(name, month,species);//调用的是父类对应参数的构造方法
	System.out.println("我是子类的有参构造器");
}

使用super()关键字,可以完成对父类构造器的调用。
总结:

  1. 子类默认调用父类无参构造方法;
  2. 可以通过super()调用父类允许访问的其他构造方法;
  3. super()必须放在子类构造方法的有效方法的第一行,而且只能出现一次。
  4. this()和super()在同一构造器中只可能出现一个。
super,this,super()和this()

在这里插入图片描述
super和this可以同时出现的,而super()和this()不能同时出现在一个构造器中

子类调用父类的构造器情况

子类构造器调用父类的构造器可能出现的情况有

  1. 子类构造器执行体的第一行显式的使用super调用父类的构造器,此时系统将根据super(params)去调用对应的构造器。
  2. 子类构造器在执行体的第一行显式的使用this调用重载的构造器,系统将会根据this(params) 调用对应 的重载构造器,本类中的对应的构造器再去调用父类的构造器
  3. 子类构造器中既没有super有没有this,那么子类在执行构造器语句的时候会去执行父类的无参构造器。
  4. 无论如何子类都会调用一次父类的构造器

Object类

Object类是所有类的老祖宗,如果有一个类没有显式的说明继承自哪个类,那么该类就默认的继承Object类。
对于Object类,它有着如下的特点

  1. Obejct类是所有类的父类
  2. 一个类没有使用extends关键字明确标识继承关系,则默认继承Object类(包括数组)
  3. Java中每个类都可以使用Ojbect类中定义的方法

接下来,我们就来结合Java API(JDK文档)来看看Java中对于Object类的介绍
文档地址:http://www.matools.com/api/java8

equals()方法

在Object类中,我们首先要学习的就是equals()方法。
当直接继承Object类中的equals()方法时,它的作用是判断调用equals()方法的对象的内存空间引用与传入参数对象的内存空间引用是否一致(是否指向的是同一块内存地址)。
在Java中,==不但可以判断两个基本数据类型的数据是佛相等,也可以用来判断两个对象的内存地址是否一致。本章节中所讲的,equals()方法如果不对其进行重写,则作用和==没有什么太大的差异。

public class Test {
	public static void main(String[] args) {
		Animal one = new Animal("花花",2,"英短");
		Animal two = new Animal("花花",2,"英短");
		System.out.println(one == two);
		System.out.println(one.equals(two));
	}
}

上述代码的运行结果都为false。这是因为one对象通过new关键字开辟了一块堆内存空间,two对象通过new关键字开辟了另一块内存空间。one对象和two对象各自指向的内存地址不一样,即使对象中的属性值一致,也被判定为false。
在String类中,Java开发人员重写了equals()方法。那我们来看看在String类中重写后的equals()方法的作用。

public static void main(String[] args) {
	String str = "abc";
	//String作为一个类,同样有其构造方法
	String str1 = new String("abc");
	String str2 = new String("abc");
	System.out.println(str == str1);
	System.out.println(str == str2);
	System.out.println(str1 == str2);
	System.out.println("================");
	System.out.println(str.equals(str1));
	System.out.println(str.equals(str2));
	System.out.println(str1.equals(str2));
}

在这次的运行中,我们可以看到分割线下equals()方法的返回值都为true,这是因为String类中重写的equals()方法会用来比较保存的字符串内容。

重写equals()方法
public class Test {
	public static void main(String[] args) {
		Animal one = new Animal("花花",2,"英短");
		Animal two = new Animal("花花",2,"英短");
		System.out.println(one == two);
		System.out.println(one.equals(two));
	}
}

回到刚刚的代码,如果只想去比较one和two这两个对象属性值是否相同怎么办呢?这时候就需要我们去Animal类中重写equals()方法了。

public boolean equals(Object obj) {
	// 1. 首先判断传入的对象是否为null
	if (obj == null) {
		// 如果是null则直接返回false
		return false;
	} else {
		// 2. 接着判断两个对象中的属性值是否一致
		// 此时是引用数据类型的强制类型转换,形式也是类似于之前学习过的基本数据类型强制类型转换
		Animal animal = (Animal) obj;
		if (this.getName().equals(animal.getName()) && this.getMonth() == animal.getMonth()
				&& this.getSpecies().equals(animal.getSpecies())) {
			return true;
		} else {
			return false;
		}
	}
}

完成重写后,我们会发现再去运行测试类中的方法,结果已经发生了改变。
当然,上述代码还是有问题的,最大的问题在于强制类型转换的时候,如果传入的参数不能匹配Animal类,则会发生一个类型转换错误的异常。

/**
 * 此方法就不是对equals()方法的重载了,而是对Animal类中已经存在的equals()方法的重载
 * @param ani
 * @return
 */
public boolean equals(Animal ani) {
	// 1. 首先判断传入的对象是否为null
	if (ani == null) {
		// 如果是null则直接返回false
		return false;
	} else {
		// 2. 接着判断两个对象中的属性值是否一致
		// 此时是引用数据类型的强制类型转换,形式也是类似于之前学习过的基本数据类型强制类型转换
		if (this.getName().equals(ani.getName()) && this.getMonth() == ani.getMonth()
				&& this.getSpecies().equals(ani.getSpecies())) {
			return true;
		} else {
			return false;
		}
	}
}

针对于,有可能发生的类型转换异常,我们可以强制限定传入的参数为Animal类型,避免异常的发生。所以,我们重载了一个参数为Animal类型的方法,在测试类中代码运行的时候,会自动根据参数类型定位到类型匹配的重载方法上。
总结:

  1. 继承Object中的equals()方法时,比较的是两个引用是否指向同一个对象
  2. 子类可以通过重写equals()方法的形式,改变比较的内容

需要注意的是,需要针对传入的参数进行非空判断,避免空指针异常

toString()方法

toString()方法也是使用频率比较高的一个方法。
在没有重写toString()方法之前,对象调用toString()方法会返回一个类的字符串表现形式,这个表现形式就是一个类的类名 + @ + 对象在内存中位置表现的哈希值。

public class Test {
	public static void main(String[] args) {
		Animal one = new Animal("花花",2,"英短");
		Animal two = new Animal("花花",2,"英短");
		System.out.println(one);
		System.out.println(one.toString());

		String str = new String("Hello,world!");
		System.out.println(str);
		System.out.println(str.toString());
	}
}

输出结果

com.dodoke.animal.model.Animal@15db9742
com.dodoke.animal.model.Animal@15db9742
Hello,world!
Hello,world!

由此可见,当直接打印对象的对象名时,会默认调用对象继承自Object类的toString()方法

在类中重写toString()方法

可以通过eclipse完成重写toString()方法

@Override
public String toString() {
	return "Animal [name=" + name + ", month=" + month + ", species=" + species + "]";
}

总结:

  • 直接输出对象名时,默认会直接调用类中的toString方法
  • 继承Object中的toString()方法时,输出对象的字符串表现形式:类型信息+@+地址信息
  • 子类可以通过重写toString()方法的形式,改变输出的内容及其表现形式

final关键字

通过继承,我们可以提高代码的复用性和灵活性。但是在有些时候,我们并不希望这个类被继承,这个方法被重写和这个变量的值被修改,那么这个时候final关键字就起了大作用了。

  1. final修饰类,则该类不允许被继承
    我们常用的String类和System类等,都是使用final去修饰的。
public final class Animal {}
final public class Animal {}
  1. final修饰方法,则该方法不允许被重写,但可以正常被子类继承使用
public final void eat() {}
  1. final修饰变量
  • final修饰局部变量,初始化(赋值)后不允许被修改
public void eat() {
    int temp = 10;
    final int temp1 = 12;
    temp1 = 14;//不可修改
    final int temp2;
    temp2 = 13;
    temp2 = 14;//不可修改
    System.out.println(this.getName() + "在吃东西");
}
  • final修饰属性变量,同样复制后不可以修改
public class Animal {
    public final int temp = 15;

    public final int temp1;

    public final int temp2;

    public final int temp3;

    public Animal() {
        super();
        temp1 = 20;
    }

    {
        temp2 = 20;
    }

    static {
        temp3 = 20;//不可以赋值
    }
}

被final定义的成员属性,只有三种方式进行赋值:

  • 定义时直接赋值;
  • 在构造代码块中赋值;
  • 在构造方法中赋值。
final其他应用

之前我们使用final修饰变量的时候,实验的都是基本数据类型,如果使用final修饰引用数据类型会发生什么样的变化呢?

public void eat() {
	final Animal ani = new Animal();
	ani = new Animal();//不允许重新赋值,也就是ani所代表的引用地址不允许被修订
	ani.setMonth(3);//对象中的属性值是允许被修改的
	ani.setMonth(2);
	System.out.println(this.getName() + "在吃东西");
}

总结:
final当修饰的变量为引用类型时,对象不允许再被重新实例化(不允许再被new),但是此对象里的属性的值依然可以更改。
在实际的开发中, final用的最多的场景是结合 static 关键字定义类变量,即静态变量。定义的这个变量我们也称之为常量。
定义为final另一个意图就是将变量的值保护起来。

public class Animal {
    public static final int TEMP = 200;
	public final static int TEMP1 = 200;
}

final修饰的这个常量需要字母全部大写
final不能修饰构造方法

注解

在之前的的学习中,我们提到可以适应@Override来添加对于重写方法的约束,同样的在eclipse中采用快捷方式完成重写方法时也会出现@Override。这样一个事物,我们称之为注解。在本章节中,我们就来简单的学习使用注解。
注解是JDK1.5版本引入的一个特性,可以声明在包、类、属性、方法、局部变量、方法参数等的前面,用来对这些元素进行说明、注释。简单来说,注解就相当于一个标记,在程序中使用了注解就相当于给这个程序打上了某种标签。被打过标签的一些程序,以后Java编辑器、开发工具、以及其他的程序就会借由这个标签来了解你所写的程序。
按照运行机制分:

  • 源码注解:注解只在源码中存在,编译成.class文件就不存在了。如@Override
  • 编译时注解:注解在源码和.class文件中都存在。
  • 运行时注解:在运行阶段还起作用,甚至会影响运行逻辑的注解。如以后要学习到的框架的注解@Autowired

按照来源分:

  • 来自JDK的注解
  • 来自第三方的注解
  • 我们自己定义的注解

还有特殊的注解:元注解:对注解进行注释的

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值