Java——继承(Inheritance)

一、继承简要介绍

1、继承是什么

在Java中,继承是一种面向对象编程的重要特性,它允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。继承的目的是实现代码的重用和设计的层次化。

子类通常被称为派生类(Derived Class),而父类则被称为超类(Super Class)或基类(Base Class)。

2、继承语法

使用关键字 extends 来实现继承。子类可以继承父类的非私有成员(属性和方法)。、

class ChildClass extends ParentClass {
    // 子类可以添加自己的属性和方法,也可以重写父类的方法
}

3、为什么需要继承

我们可以先创建两个类,一个猫类,一个狗类:

class Cat {
	public String name;
	public int age;
	public double weight;

	public void meow() {
		System.out.println("Meow~");
	}

}

class Dog {
	public String name;
	public int age;
	public double weight;

	public void barking() {
		System.out.println("Woof~");
	}

}

可以发现这两个类中除了两个方法不同,其他的属性都是相同的,这样就造成了大量重复代码。继承就是用来解决这个问题的。

下面我们将猫类和狗类都继承动物类:

class Animal {
	public String name;
	public int age;
	public double weight;

	public void showName() {
		System.out.println(name);
	}
	
}

class Cat extends Animal {
	public void meow() {
		System.out.println("Meow~");
	}
}

class Dog extends Animal {
	public void barking() {
		System.out.println("Woof~");
	}
}

对于 Animal 的两个子类 Cat 和 Dog 都继承了父类的属性和方法,它们也可以有各自的特有的属性和方法。

4、继承的特点

  1. 子类继承父类的属性和方法: 在Java中,子类可以继承父类中所有的属性和方法。但私有属性和方法只能在父类内部访问,子类无法直接访问。如果需要,可以通过公共方法间接访问父类的私有成员。

  2. 子类的扩展: 子类不仅可以继承父类的成员,还可以定义自己的属性和方法。这允许子类在保持父类功能的基础上进行扩展,添加新的功能或修改已有功能的行为。

  3. 方法重写(Override): 子类可以重写(override)从父类继承的方法,以提供特定于子类的实现。重写的方法必须具有相同的签名(方法名、参数列表和返回类型),并且通常遵循里氏替换原则,即子类对象应该能够替换其父类对象而不影响程序的正确性。

  4. 单继承与多重继承: Java支持单继承,即一个类只能直接继承自一个父类。这与C++不同,C++支持多重继承,即一个类可以继承自多个直接父类。然而,Java通过接口(interface)实现了多重继承的某些方面,一个类可以实现多个接口。

  5. 耦合性: 继承确实可以增加类之间的耦合性,因为子类依赖于父类的结构和行为。这可能导致代码的灵活性和可维护性降低。为了减少这种耦合,设计时应该遵循良好的面向对象设计原则,如使用组合优于继承,以及遵循依赖倒置原则等。

5、单继承和多重继承

1)单继承

单继承是指一个类只能直接继承自另一个类。在Java中,这是支持的

2)多层继承

多层继承是指一个类继承自另一个类,而后者又继承自另一个类,形成一个继承链。在Java中,这也是支持的

3)多重继承

多重继承是指一个类可以同时继承自多个类。在Java中,类不支持多重继承,即一个类不能直接继承自两个或两个以上的类。

这样写就会报错:

这是为了避免潜在的复杂性和歧义,例如“钻石问题”(当两个父类有相同的方法时,子类应该继承哪一个?)。

然而,Java通过接口(Interface)支持了多重继承的概念。一个类可以实现多个接口,从而获得多重继承的某些好处,同时避免了多重继承的复杂性。

二、继承细节

1、子类继承父类所有属性和方法

子类继承父类的所有属性和方法,但是子类不能直接访问父类的私有属性和方法。

我们可以通过调试来观察,确定子类确实继承了父类的所有属性:

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Sub sub = new Sub();

	}
}

class Base {
	public int a;
	protected int b;
	int c;
	private int d;

}

class Sub extends Base {

}

这个例子中,父类 Base 有四种访问修饰符修饰的属性,Sub 子类继承自 Base 父类,然后我们在 main 函数中创建一个 Sub 类的对象 sub,然后进行调试,当我们调试到对象 sub 创建成功后,对 sub 对象进行监视,可以看到以下列表:

可以发现这里 Sub 这个子类的对象 sub 是有父类 Base 的四个属性的,即使 d 是 private 修饰的变量,这个子类对象也是继承了的,只不过这个子类对象不能直接访问这个 private 修饰的属性,需要通过公共的方法进行访问。

对于子类不能直接访问父类的 private 修饰的属性,这一点我们在之前的文章Java——访问修饰符中提到过,我们说到 private 修饰的属性和方法只能在同一个类中访问,是不能在其他类中直接访问的。

这里谈到的是在同一个包中的情况,因为在不同包中,父类中默认的访问修饰符修饰的属性和方法也是不能在子类中被访问的。

2、子类构造器必须调用父类构造器

当创建一个子类的对象时,子类的构造器会隐式或显式地调用父类的构造器来完成父类的初始化。这是Java继承机制的一部分,确保父类的状态在子类对象创建时得到正确初始化。

如果子类的构造器没有显式调用父类的构造器,Java编译器会自动插入对父类默认构造器(无参构造器)的调用,也就是 super(); 这个语句。如果父类没有定义默认构造器(无参构造器),或者你想调用父类的某个特定构造器,你需要在子类构造器中显式地使用super()关键字来指定调用父类的某个构造器。

下面给出例子:

class Base {
	public int data;

	public Base() {
		System.out.println("父类构造器被调用");
	}
}

class Sub extends Base {
	public Sub() {
		System.out.println("子类构造器被调用");
	}
}

这里我们给父类写了一个无参构造器,子类也写了一个无参构造器,但是子类中没有显式调用父类的构造器,这样我们创建一个子类对象

public class Test {
	public static void main(String[] args) {
		Sub sub = new Sub();
		
	}
}

运行后得到的结果为:

可以发现子类的构造器中虽然没有显式的调用父类的构造器,但是 Java 编译器自动插入了 super() ,以调用我们写的父类的无参构造器,这样在子类构造器被调用时,父类构造器也会被调用,以确保父类的状态在子类对象创建时得到正确初始化。

3、子类构造器默认调用父类无参构造器

当创建子类对象时,不管使用子类的那个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类构造器中使用 super() 指定使用父类的那个构造器,否则就会编译不通过。

package com.pack1;

public class Test {
	public static void main(String[] args) {
		System.out.println("sub1使用子类构造器Sub()创建");
		Sub sub1 = new Sub();
		System.out.println("sub2使用子类构造器Sub(double bar)创建");
		Sub sub2 = new Sub(2.2);

	}
}

class Base {
	public int data;

	public Base() {
		System.out.println("父类构造器Base()被调用");
	}

	public Base(int data) {
		this.data = data;
		System.out.println("父类构造器Base(int data)被调用");
	}
}

class Sub extends Base {
	public double bar;

	public Sub() {
		System.out.println("子类构造器Sub()被调用");
	}

	public Sub(double bar) {
		this.bar = bar;
		System.out.println("子类构造器Sub(double bar)被调用");
	}
}

运行结果:

可以发现我们的子类中的两个构造器都没有显式使用 super() 调用父类的构造器,虽然父类中有两个构造器,但是子类中的两个构造器都是默认调用了父类的无参构造器(默认构造器)。

如果我们将父类中的无参构造器去掉,这样运行就会报错:

package com.pack1;

public class Test {
	public static void main(String[] args) {
		System.out.println("sub1使用子类构造器Sub()创建");
		Sub sub1 = new Sub();
		System.out.println("sub2使用子类构造器Sub(double bar)创建");
		Sub sub2 = new Sub(2.2);

	}
}

class Base {
	public int data;

	/*public Base() {
		System.out.println("父类构造器Base()被调用");
	}*/

	public Base(int data) {
		this.data = data;
		System.out.println("父类构造器Base(int data)被调用");
	}
}

class Sub extends Base {
	public double bar;

	public Sub() {
		System.out.println("子类构造器Sub()被调用");
	}

	public Sub(double bar) {
		this.bar = bar;
		System.out.println("子类构造器Sub(double bar)被调用");
	}
}

运行结果:

这是因为在子类构造器中如果没有显式使用 super() 指定调用父类的构造器,则子类构造器的第一行默认有一句话:

super();

也就是默认调用父类的无参构造器,但是如果父类没有无参构造器的话,这一句就是会报错的。

如果父类类没有无参构造器或者我们想在子类中调用指定的父类构造器,就可以使用 super() 来指定调用父类的构造器:

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Sub sub = new Sub(1);
		Sub sub1 = new Sub(1,2.2);
	}
}

class Base {
	public int data;

	/*public Base() {
		System.out.println("父类构造器Base()被调用");
	}*/

	public Base(int data) {
		this.data = data;
		System.out.println("父类构造器Base(int data)被调用");
	}
}

class Sub extends Base {
	public double bar;

	public Sub(int data) {
		super(data);
		System.out.println("子类构造器Sub(int data)被调用");
	}

	public Sub(int data, double bar) {
		super(data);
		this.bar = bar;
		System.out.println("子类构造器Sub(int data, double bar)被调用");
	}
}

运行结果:

4、所有类都是 Object 类的子类

在Java中,所有的类都直接或间接地继承自java.lang.Object类。这里我们以 java.util.Arrays 类为例,可以发现 Arrays 这个类也是继承自 Object 类:

如果你在定义一个类时没有使用extends关键字来指定它继承自哪个类,那么这个类将默认继承自Object类。

也就是说如果我们定义了一个类:

public class MyClass {
    // 类的内容
}

就相当于:

public class MyClass extends Object {
    // 类的内容
}

一般情况下我们都是用上面的第一种的写法,不用显式指定某个类继承自 Object 类。

所有的类都直接或间接地继承自java.lang.Object类,这意味着Object类中定义的方法,如toString(), equals(Object obj), hashCode(), getClass(), clone(), finalize()等,都可以被任何Java类所使用,因为它们是继承自Object类的。

5、子类构造器对父类构造器的调用会追溯到 Object 类的构造器

对于 Sub 类是继承自 Base 类,Base 类是继承自 Object 类,所以当我们调用 Sub 类的构造器,必然会调用 Base 类的构造器,调用 Base 类的构造器必然会调用 Object 类的构造器。这种调用关系会一直追溯到 Object 类的构造器。

三、继承本质分析

1、创建一个子类对象在内存中的分布

用以下代码为例,分析创建了一个 C 类对象后,这个对象在内存中的分布(可以发现这几个类是在同一个包中的,访问修饰符是默认,在同一个包中都能被访问):

package com.pack1;

public class Test {
	public static void main(String[] args) {
		C c = new C();

	}
}

class A {
	String name = "AAA";
	double weight;
}

class B extends A {
	String name = "BBB";
	int age;
}

class C extends B {
	String name = "CCC";
}

可以看到继承关系为:

下面我们详细解释在内存中的布局。

首先在对象 c 还没有创建时,首先要加载类信息,因为是创建 C 类的对象,加载某个类信息前会查找这个类的父类,直到找到这个类的顶级父类 Object 才停止,然后从 Object 依次加载父类信息直到加载到本类信息。这个很像递归加载类信息。

这样说,也就是依次加载 Object 类信息,A 类信息,B 类信息,最后才是 C 类信息。所以在一个类信息加载完成后,它的父类信息也加载完成了:

然后就会对这个对象 c 进行创建:

对于对象 c 会拥有父类所有属性,这里的属性的访问修饰符都是默认,就算是父类中 private 修饰的属性也是会被子类继承的,只不过子类没法直接访问到 private 修饰的父类属性,上面我们已经对这个讲解过了。对象 c 有父类所有属性,而且它们在内存中是连续的。

在上图,可以看到对象 c 的属性被分为了三块,分别是 A 类属性,B 类属性,C 类属性。A 类属性中有父类 A 类中的所有属性,B 类属性中有父类 B 类中的所有属性,C 类属性中有这个子类 C 中所有的属性,这样对象 c 是拥有所有父类属性以及本类的所有属性的。

A 类部分

name = "AAA",weight = 0.0 (默认值)

B 类部分

name = "BBB"(B 类重新定义了 name,隐藏了 A 类中的 name),age = 0(默认值)

C 类部分

name = "CCC"(C 类重新定义了 name,隐藏了 B 类中的 name

可以看到三个类中都有 name 这个变量,尽管 C 类中存在三个 name 属性,但每个类的 name 属性是独立的。如果在 C 类中访问 name,将优先访问最近的 name 属性,即 C 类中的 name

我么可以对上面的代码进行调试,

可以发现:这三个 name 变量是互相独立的。

2、对属性的访问规则

那么从这个案例中,我们就要考虑一下,如果我们想要访问对应的 name,那么访问规则又是什么呢?

1)直接访问

(1)查看子类是否有该属性,如果子类有该属性且可以访问,则使用子类的这个属性。

(2)如果子类没有该属性,就看父类有没有该属性,如果父类有该属性且可以访问,则使用父类的这个属性。

(3)如果当前父类没有该属性,则接着向上面的父类按规则(2)寻找,直到 Object 类再停止。如果最终没有找到该属性,则报错。

如果一旦找到了这个属性,但这个属性无法访问,就不再向上寻找,会报错。例如:

package com.pack1;

public class Test {
	public static void main(String[] args) {
		C c = new C();
		System.out.println(c.age);
	}
}

class A {
	String name = "AAA";
	double weight;
	int age;
}

class B extends A {
	String name = "BBB";
	private int age;
}

class C extends B {
	String name;
}

就像这里我们通过子类对象访问 age 属性,但是子类 C 类中没有 age 属性,向上面的父类寻找,在父类 B 类中寻找到 private 修饰的 age 属性,这里显然无法访问,虽然上面的父类的父类即 A 类中有可访问的 age 属性,也不会向上找了,在 B 类这里遇到私有的 age 属性就会报错。

例子:

package com.pack1;

public class Test {
	public static void main(String[] args) {
		C c = new C();
		System.out.println(c.name);
	}
}

class A {
	String name = "AAA";
	double weight;
}

class B extends A {
	String name = "BBB";
	int age;
}

class C extends B {
	String name = "CCC";

}

运行结果:

这个例子中,我们在 main 方法中使用对象 c 对属性 name 进行访问,在子类 C 中就能直接找到 name 属性,并且可以直接访问,则使用子类 C 的 name 属性,子类 C 的属性 name 的默认值为 "CCC",所以这里的运行结果是上面那样。

2)强制访问

通过强制类型转换访问 ((B)c).name

  • 访问 B 类中的 name
  • 输出 "BBB"。

通过强制类型转换访问 ((A)c).name

  • 访问 A 类中的 name
  • 输出 "AAA"。
package com.pack1;

public class Test {
	public static void main(String[] args) {
		C c = new C();
		System.out.println("强制类型转换访问B类中的name属性:" + ((B)c).name);
		System.out.println("强制类型转换访问A类中的name属性:" + ((A)c).name);
	}
}

class A {
	String name = "AAA";
	double weight;
}

class B extends A {
	String name = "BBB";
	int age;
}

class C extends B {
	String name = "CCC";

}

运行结果:

这里的强制类型转换我们可以类比 C 语言中的将 int* 类型变量强制转换为 char* 从而可以只访问到 int* 指针变量指向的区域的第一个字节。

对于 main 方法中的 c 变量的本质是对象引用变量:

C c = new C();

它的本质也是类似于指针的,它指向的是一个在堆区的 C 类对象,因为 C 类对象应当拥有父类所有属性和本类所有属性,所以 C 类对象的结构应当是下面这样:

所以这个对象引用变量 c 是可以访问到 A 类、B 类和 C 类三个类(这里其实还有 Object 类,这里暂时忽略)的所有属性的,所以通过 c.name 访问 name 属性,则遵循的规则就是上面的直接访问的规则,从子类 C 中依次向上寻找这个属性。

对于 B 类对象,应当有它的父类所有属性和本类属性,所以 B 类对象的在堆区的结构应该是这样:

所以对于对象 c 强制转换成 B 类对象的引用变量后,(B)c 就只能访问到 A 类和 B 类两个类(这里其实还有 Object 类,这里暂时忽略)的所有属性,这样就可以实现访问到 B 类的 name 属性了,因为在这里使用:

((B)c).name

就只能从 B 类开始向上寻找 name 属性了,这里 B 类中就有可访问的 name 属性,所以就直接访问 B 类的 name 属性了,这里就是强制类型转换实现强制访问特定的类的属性的原理。

对于 A 类对象,应当有它的父类所有属性和本类属性,所以 A 类对象的在堆区的结构应该是这样:

所以对于对象 c 强制转换成 A 类对象的引用变量后,(A)c 就只能访问到 A 类一个类(这里其实还有 Object 类,这里暂时忽略)的所有属性,这样就可以实现访问到 A 类的 name 属性了,因为在这里使用:

((A)c).name

就只能从 A 类开始向上寻找 name 属性了,这里 A 类中就有可访问的 name 属性,所以就直接访问 A 类的 name 属性了。

可以看到这里是使用 c 对象的引用变量对对象 c 的多个不同类中的 name 属性进行访问,那如果我们在子类中想要访问父类中 name 属性,当然我们可以使用 super,那如果要访问父类的父类的 name 属性呢,就可以使用强制转换 this 的方法:

package com.pack1;

public class Test {
	public static void main(String[] args) {
		C c = new C();
		c.printName();
	}
}

class A {
	String name = "AAA";
	double weight;
}

class B extends A {
	String name = "BBB";
	int age;
}

class C extends B {
	String name = "CCC";

	public void printName() {
		System.out.println(name);//这里访问的是最近的name属性,C类的name属性
		System.out.println(((B)this).name);//这里访问的是B类的name属性
		System.out.println(((A)this).name);//这里访问的是A类的name属性
	}
}

运行结果:

以上谈论的基础是 C 类对这些属性有访问权限,如果父类的属性是 private 修饰的,使用强制类型转化也是访问不到的,只能通过公共方法获取。如果访问修饰符是默认,访问的地方与这个属性所在的类不在同一个包中也是无法访问的,也只能通过公共方法获取。

四、super

super 的语法与 this 很类似。

1、super 访问父类属性或方法

在Java中,super关键字是一个引用变量,用于指向当前类的父类。super可以用来访问父类的成员,包括属性和方法,这在子类中非常有用,尤其是当子类和父类中有同名的成员时。

1)super 访问父类属性

使用 super 关键字可以在同一个包中访问父类的非私有属性,在不同包中只能访问父类的 public 和 protected 修饰的属性。

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Sub sub = new Sub();
		sub.printName();
	}
}

class Base {
	String name = "Base";
}

class Sub extends Base {
	String name = "Sub";

	public void printName() {
		System.out.println("父类name:" + super.name);
		System.out.println("子类name:" + name);
	}
}

可以发现这里子类和父类都有 name 属性,我们可以使用 super.name 访问父类的 name 属性。

运行结果:

2)super 访问父类方法

使用 super 关键字可以在同一个包中访问父类的非私有方法,在不同包中只能访问父类的 public 和 protected 修饰的方法。

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Sub sub = new Sub();
		sub.greeting();
	}
}

class Base {
	public void sayHi() {
		System.out.println("hi~");
	}
}

class Sub extends Base {

	public void greeting() {
		super.sayHi();
	}
}

运行结果:

这里我们使用 super 关键字对父类方法进行调用。

2、super() 调用父类构造器

上面我们对此已经介绍的很详细了,这里就简单介绍一下。

子类中应当调用父类构造器以初始化继承至父类的属性,使用 super() 就是为了指定调用父类的构造器。如果子类中没有显式调用父类构造器,Java 编译器会自动在子类构造器的第一句加上:

super();

以默认调用父类的无参构造器,但是这样需要确保父类有无参构造器。如果父类没有无参构造器或者我们想自己指定调用特定的父类构造器,就需要使用 super() 传入特定的参数来调用特定的父类构造器。

super() 这个语句必须是子类构造器中的第一行非注释语句。

五、方法重写 / 覆盖(Override)

1、什么是方法重写

重写(Override)是指子类重新定义父类中已有的方法。重写是实现多态的一种方式,允许子类提供一个特定于自己的方法实现。

1. 基本概念

  • 重写方法:子类中定义一个与父类中方法签名(方法名、参数列表)相同的方法。
  • 目的:子类可以根据自己的需求重新实现父类的方法。

2. 重写的规则

  • 方法签名必须相同:子类方法的名称和参数列表必须与父类方法完全相同。
  • 返回类型:子类方法的返回类型可以与父类的返回类型一样,也可以是父类方法返回类型的子类型(例如父类返回 Object 类型,子类可以返回 String 类型)。
  • 访问权限:子类方法的访问权限不能比父类方法更严格。例如,如果父类方法是public,子类方法也必须是public
  • 异常:子类方法抛出的异常不能比父类方法抛出的异常更宽泛。子类方法可以不抛出异常,或者抛出父类方法抛出异常的子类异常。

3. 使用@Override注解

  • 注解:在子类方法上使用@Override注解可以明确表示这是一个重写方法。这有助于编译器检查方法签名是否正确,避免拼写错误等问题。

2、方法重写的示例

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Dog dog = new Dog();
		dog.makeSound();
	}
}

class Animal {
	public void makeSound() {
		System.out.println("Animal sound...");
	}
}

class Dog extends Animal {
	public void makeSound() {
		System.out.println("woof...");
	}
}

运行结果:

这里的 Dog 类中的 makeSound 方法就重写了父类 Animal 中的 makeSound 方法。

六、补充

1、this() 和 super() 的使用都必须是第一行

上面我们提到了 super() 必须是子类构造器的第一句非注释语句,在之前的文章中,我们提到在同一个类中调用其他构造器要使用 this() 语句,this() 语句也应该是构造器的第一句非注释语句。

但是如果我们在一个子类中的构造器中使用 this() 调用另一个构造器,从而使没有地方供 super() 调用,致使父类状态无法正常初始化呢。就像下面这样:

class Base {
	public Base() {

	}
}

class Sub extends Base {
	public Sub() {
		
	}

	public Sub(int a) {
		this();
	}
}

当我们调用 Sub 的一个参数的构造器时,父类构造器是否是没有被调用呢?

实际上父类构造器是被调用了的,因为我们使用 this() 调用 Sub 的无参构造器时,Sub 的无参构造器中会被编译器默认加上 super(),所以这样就会间接调用父类构造器,完成父类状态初始化。

class Base {
	public Base() {

	}
}

class Sub extends Base {
	public Sub() {
		//编译器默认加上 super();
	}

	public Sub(int a) {
		this();
	}
}

那有没有可能 Sub 子类中的两个构造器中都使用了 this() 语句呢,就像下面这样:

class Base {
	public Base() {

	}
}

class Sub extends Base {
	public Sub() {
		this(2);
	}

	public Sub(int a) {
		this();
	}
}

这样就没有地方加 super() 来调用父类构造器了,这样就不能正常初始化父类的状态了。

实际上这里也是不可以的,因为构造器不能递归调用,这是错误的。

这里会报错:

上面说构造器不能递归调用,所以当我们只有一个构造器时,也不能使用 this() 语句来调用这个构造器。

class Base {
	public Base() {

	}
}

class Sub extends Base {
	public Sub() {
		this();
	}
}

这里也会报错:

由上可知,当我们使用 this() 调用其他构造器时,必定有两个以上构造器,而且至少有一个构造器是没有使用 this() 语句的,这个构造器中就可以有 super() 来调用父类构造器(如果没有显式调用,编译器会自动加上 super(); 这个语句,用来调用父类无参构造器)。

其他构造器中最终就必定可以追溯到这个没有使用 this() 语句的构造器,所以这样所有的构造器都直接或间接的调用了父类构造器。

2、子类中对父类对象的访问补充

当子类有和父类属性和方法重名的属性或方法时,为了访问父类的成员,必须通过 super 关键字;如果子类中没有与父类重名的属性或方法,则使用 super、this或直接访问访问父类的属性或方法是一样的效果。 

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Sub sub = new Sub();
		sub.show();
	}
}

class Base {
	public String name = "Base";
}

class Sub extends Base {
	public void show() {
		System.out.println(name);
		System.out.println(this.name);
		System.out.println(super.name);
	}
}

运行结果:

这里子类中使用了三种方法访问 name 这个属性,这个属性是从父类 Base 中继承的,子类 Sub 中没有与 name 重名的属性,所以第一种方法使用直接访问就是根据上面我们提到的“对属性的访问规则”中的直接访问的规则来从子类中寻找,因为子类 Sub 中没有 name 这个属性,然后向上面的父类中寻找,在父类中寻找到,而且可以访问,所以就是使用父类中的这个 name 属性;第二种方法是使用 this 访问,这样就是对 Sub 类的对象的属性进行访问,这样的访问规则与上面的直接访问的规则是一致的,最终也是在父类中找到 name 这个属性,然后使用这个属性;第三种方法是使用 super 访问,因为 name 这个属性本就是从父类中继承的,使用 super 访问是完全可以的,对于 super 的查找顺序是跳过本类直接从父类开始查找,如果父类查找不到,接着向上面找父类的父类,直到 Object 类。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/777457.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

探索LlamaIndex:如何用Django打造高效知识库检索

简介 LlamaIndex(前身为 GPT Index)是一个数据框架,为了帮助我们去建基于大型语言模型(LLM)的应用程序。 主要用于处理、构建和查询自定义知识库。 它支持多种数据源格式 excel,txt,pdf&…

DaViT(ECCV 2022,Microsoft)

paper:DaViT: Dual Attention Vision Transformers official implementation:https://github.com/dingmyu/davit third-party implementation:https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/davit.py 出发点…

独家揭秘!格行随身WiFi‘骄傲’宣言背后的震撼行业的真相!随身WiFi行业内黑马

近几年以来,随行WiFi产品呈现爆发式增长,随行WiFi的火爆,是技术进步带给消费者的一种“福利”,各大直播间也充斥着品牌各异的随身WiFi。但真正脱颖而出、赢得消费者信赖的优质品牌却凤毛麟角。而其中最受欢迎的格行随身WiFi也因设…

Java语言+后端+前端Vue,ElementUI 数字化产科管理平台 产科电子病历系统源码

Java语言后端前端Vue,ElementUI 数字化产科管理平台 产科电子病历系统源码 Java开发的数字化产科管理系统,已在多家医院实施,支持直接部署。系统涵盖孕产全程,包括门诊、住院、统计和移动服务,整合高危管理、智能提醒、档案追踪等…

Stream练习

运用点&#xff1a; 流内数据类型转换(map)、filter、limit、skip、concat(让两个流合并) 题目&#xff1a; 操作1、2&#xff1a; ArrayList<String> manList new ArrayList<>();ArrayList<String> womanList new ArrayList<>();Collections.addAl…

C++之static关键字

文章目录 前提正文多重定义extern关键字使用staticstatic 全局变量(在.cpp文件中定义)static变量存放在哪里static变量可不可以放在.h文件中 static 函数static局部变量static 成员变量static 成员函数 总结参考链接 前提 好吧&#xff0c;八股&#xff0c;我又回来了。这次想…

8.14 矢量图层面要素2.5D渲染

文章目录 前言2.5D渲染QGis设置面符号为2.5D二次开发代码实现2.5D 总结 前言 本章介绍矢量图层面要素2.5D渲染的使用说明&#xff1a;文章中的示例代码均来自开源项目qgis_cpp_api_apps 2.5D渲染 2.5D渲染可以将多边形渲染为类3D效果。 QGis设置面符号为2.5D 以"hou…

数据库7.4

第二次作业 1.登陆数据库 2.创建数据库zoo 3.修改数据库zoo字符集为gbk 4.选择当前数据库为zoo 5.查看创建数据库zoo信息 6.删除数据库zoo C:\Windows\System32>mysql -uroot -p20040830Nmx mysql> create database zoo; alter database zoo character set gbk; mys…

Java springboot校园管理系统源码

Java springboot校园管理系统源码-014 下载地址&#xff1a;https://download.csdn.net/download/xiaohua1992/89364089 技术栈 运行环境&#xff1a;jdk8 tomcat9 mysql5.7 windows10 服务端技术&#xff1a;Spring Boot Mybatis VUE 使用说明 1.使用Navicati或者其它工…

VBA初学:零件成本统计之三(获取材料外协的金额)

第三步&#xff0c;从K3的数据库中获取金额 我这里是使用循环&#xff0c;通过任务单号将金额汇总出来&#xff0c;如果使用数组的话&#xff0c;还要按任务单写GROUP&#xff0c;还要去对应&#xff0c;不如循环直接一点 获取材料和外协金额的表格Sub getje()Dim rowcount A…

【JAVA入门】Day13 - 代码块

【JAVA入门】Day13 - 代码块 文章目录 【JAVA入门】Day13 - 代码块一、局部代码块二、构造代码块三、静态代码块 在 Java 中&#xff0c;两个大括号 { } 中间的部分叫一个代码块&#xff0c;代码块又分为&#xff1a;局部代码块、构造代码块、静态代码块三种。 一、局部代码块…

Linux应用---信号

写在前面&#xff1a;在前面的学习过程中&#xff0c;我们学习了进程间通信的管道以及内存映射的方式。这次我们介绍另外一种应用较为广泛的进程间通信的方式——信号。信号的内容比较多&#xff0c;是学习的重点&#xff0c;大家一定要认真学&#xff0c;多多思考。 一、信号概…

ASP.NET Core----基础学习01----HelloWorld---创建Blank空项目

文章目录 1. 创建新项目--方式一&#xff1a; blank2. 程序各文件介绍&#xff08;Project name &#xff1a;ASP.Net_Blank&#xff09;&#xff08;1&#xff09;launchSettings.json 启动方式的配置文件&#xff08;2&#xff09;appsettings.json 基础配置file参数的读取&a…

Vue 前端修改页面标题无需重新打包即可生效

在public文件夹下创建config.js文件 index.html页面修改 其他页面的标题都可以用window.title来引用就可以了&#xff01;

【算法】(C语言):冒泡排序、选择排序、插入排序

冒泡排序 从第一个数据开始到第n-1个数据&#xff0c;依次和后面一个数据两两比较&#xff0c;数值小的在前。最终&#xff0c;最后一个数据&#xff08;第n个数据&#xff09;为最大值。从第一个数据开始到第n-2个数据&#xff0c;依次和后面一个数据两两比较&#xff0c;数值…

商务办公优选!AOC Q27E3S2商用显示器,打造卓越新体验!

摘要&#xff1a;助办公室一族纵横职场&#xff0c;实现高效舒适办公&#xff01; 在日常商务办公中&#xff0c;对于办公室一族来说总有太多“难难难难难点”&#xff1a;工作任务繁琐&#xff0c;熬夜加班心力交瘁、长时间伏案工作导致颈椎、眼睛等出现问题&#xff0c;职业…

【吊打面试官系列-MyBatis面试题】为什么说 Mybatis 是半自动 ORM 映射工具?它与全自动的区别在哪里?

大家好&#xff0c;我是锋哥。今天分享关于 【为什么说 Mybatis 是半自动 ORM 映射工具&#xff1f;它与全自动的区别在哪里&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; 为什么说 Mybatis 是半自动 ORM 映射工具&#xff1f;它与全自动的区别在哪里&#xf…

[从0开始轨迹预测][NMS]:NMS的应用(目标检测、轨迹预测)

非极大值抑制&#xff08;Non-Maximum Suppression&#xff0c;简称NMS&#xff09;是一种在计算机视觉中广泛应用的算法&#xff0c;主要用于消除冗余和重叠的边界框。在目标检测任务中&#xff0c;尤其是在使用诸如R-CNN系列的算法时&#xff0c;会产生大量的候选区域&#x…

Redis基础教程(九):redis有序集合

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; &#x1f49d;&#x1f49…

华为仓颉可以取代 Java 吗?

大家好&#xff0c;我是君哥。 在最近的华为开发者大会上&#xff0c;华为亮相了仓颉编程语言&#xff0c;这是华为历经 5 年&#xff0c;投入大量研发成本沉淀的一门编程语言。 1 仓颉简介 按照官方报告&#xff0c;仓颉编程语言是一款面向全场景智能的新一代编程语言&#…