标签分类
技术文章
当前位置:主页 > 计算机编程 > java > Java继承概念详细解读

Java继承概念知识点分享

  • 发布时间:
  • 作者:码农之家原创
  • 点击:63

Java继承概念详细解读

这篇文章主要知识点是关于java,继承的概念,java,继承概念,继承的语法,java,合成的语法,Java继承概念详细解读,浅谈Java继承中的转型及其内存分配 的内容,如果大家想对相关知识点有系统深入的学习,可以参阅以下电子书

Java从小白到大牛
  • 类型:Java大小:15.6 MB格式:PDF出版:清华大学出版社作者:关东升
立即下载

更多相关的学习资源可以参阅 程序设计电子书Java电子书、等栏目。

继承与合成基本概念

继承:可以基于已经存在的类构造一个新类。继承已经存在的类就可以复用这些类的方法和域。在此基础上,可以添加新的方法和域,从而扩充了类的功能。

合成:在新类里创建原有的对象称为合成。这种方式可以重复利用现有的代码而不更改它的形式。

1.继承的语法

关键字extends表明新类派生于一个已经存在的类。已存在的类称为父类或基类,新类称为子类或派生类。例如:

class Student extends Person {
}

类Student继承了Person,Person类称为父类或基类,Student类称为子类或派生类。

2.合成的语法

合成比较简单,就是在一个类中创建一个已经存在的类。

class Student {
  Dog dog;
}

上溯造型

1.基本概念

继承的作用在于代码的复用。由于继承意味着父类的所有方法亦可在子类中使用,所以发给父类的消息亦可发给衍生类。如果Person类中有一个eat方法,那么Student类中也会有这个方法,这意味着Student对象也是Person的一种类型。

class Person {
	public void eat() {
		System.out.println("eat");
	}
	static void show(Person p) {
		p.eat();
	}
}
public class Student extends Person{
	public static void main(String[] args) {
		Student s = new Student();
		Person.show(s);
		// ①
	}
}

【运行结果】:

eat

在Person中定义的show方法是用来接收Person句柄的,但是在①处接收的却是Student对象的引用。这是因为Student对象也是Person对象。在show方法中,传入的句柄(对象的引用)可以是Person对象以及Person的衍生类对象。这种将Student句柄转换成Person句柄的行为成为上溯造型。

2.为什么要上溯造型

为什么在调用eat是要有意忽略调用它的对象类型呢?如果让show方法简单地获取Student句柄似乎更加直观易懂,但是那样会使衍生自Person类的每一个新类都要实现专属自己的show方法:

class Value {
	private int count = 1;
	private Value(int count) {
		this.count = count;
	}
	public static final Value
	      v1 = new Value(1),
	      v2 = new Value(2),
	      v3 = new Value(3);
}
class Person {
	public void eat(Value v) {
		System.out.println("Person.eat()");
	}
}
class Teacher extends Person {
	public void eat(Value v) {
		System.out.println("Teacher.eat()");
	}
}
class Student extends Person {
	public void eat(Value v) {
		System.out.println("Student.eat()");
	}
}
public class UpcastingDemo {
	public static void show(Student s) {
		s.eat(Value.v1);
	}
	public static void show(Teacher t) {
		t.eat(Value.v1);
	}
	public static void show(Person p) {
		p.eat(Value.v1);
	}
	public static void main(String[] args) {
		Student s = new Student();
		Teacher t = new Teacher();
		Person p = new Person();
		show(s);
		show(t);
		show(p);
	}
}

这种做法一个很明显的缺陷就是必须为每一个Person类的衍生类定义与之紧密相关的方法,产生了很多重复的代码。另一方面,对于如果忘记了方法的重载也不会报错。上例中的三个show方法完全可以合并为一个:

public static void show(Person p) {
   p.eat(Value.v1);
}

动态绑定

当执行show(s)时,输出结果是Student.eat(),这确实是希望得到的结果,但是似乎没有按照我们希望的形式来执行,再来看一下show方法:

public static void show(Person p) {
   p.eat(Value.v1);
}

它接收的是Person句柄,当执行show(s)时,它是如何知道Person句柄指向的是一个Student对象而不是Teacher对象呢?编译器是无从得知的,这涉及到接下来要说明的绑定问题。

1.方法调用的绑定

将一个方法同一个方法主体连接在一起就称为绑定(Binding)。若在运行运行前执行绑定,就称为“早期绑定”。上面的例子中,在只有一个Person句柄的情况下,编译器不知道具体调用哪个方法。Java实现了一种方法调用机制,可在运行期间判断对象的类型,然后调用相应的方法,这种在运行期间进行,以对象的类型为基础的绑定称为动态绑定。除非一个方法被声明为final,Java中的所有方法都是动态绑定的。

用一张图表示上溯造型的继承关系:

Java继承概念详细解读

用代码概括为:

Shapes=newShape();

按照继承关系,将创建的Circle对象句柄赋给一个Shape是合法的,因为Circle属于Shape的一种。

当调用其中一个基础类方法时:

Shapes=newShape();

此时,调用的是Circle.draw(),这是由于动态绑定的原因。

class Person {
	void eat() {
	}
	void speak() {
	}
}
class Boy extends Person {
	void eat() {
		System.out.println("Boy.eat()");
	}
	void speak() {
		System.out.println("Boy.speak()");
	}
}
class Girl extends Person {
	void eat() {
		System.out.println("Girl.eat()");
	}
	void speak() {
		System.out.println("Girl.speak()");
	}
}
public class Persons {
	public static Person randPerson() {
		switch ((int)(Math.random() * 2)) {
			default:
			    case 0:
			      return new Boy();
			case 1:
			      return new Girl();
		}
	}
	public static void main(String[] args) {
		Person[] p = new Person[4];
		for (int i = 0; i < p.length; i++) {
			p[i] = randPerson();
			// 随机生成Boy或Girl
		}
		for (int i = 0; i < p.length; i++) {
			p[i].eat();
		}
	}
}

对所有从Person衍生出来的类,Person建立了一个通用接口,所有衍生的类都有eat和speak两种行为。衍生类覆盖了这些定义,重新定义了这两种行为。在主类中,randPerson随机选择Person对象的句柄。**上诉造型是在return语句里发生的。**return语句取得一个Boy或Girl的句柄并将其作为Person类型返回,此时并不知道具体是什么类型,只知道是Person对象句柄。在main方法中调用randPerson方法为数组填入Person对象,但不知具体情况。当调用数组每个元素的eat方法时,动态绑定的作用就是执行对象的重新定义了的方法。

然而,动态绑定是有前提的,绑定的方法必须存在于基类中,否则无法编译通过。

class Person {
	void eat() {
		System.out.println("Person.eat()");
	}
}
class Boy extends Person {
	void eat() {
		System.out.println("Boy.eat()");
	}
	void speak() {
		System.out.println("Boy.speak()");
	}
}
public class Persons {
	public static void main(String[] args) {
		Person p = new Boy();
		p.eat();
		p.speak();
		// The method speak() is undefined for the type Person
	}
}

如果子类中没有定义覆盖方法,则会调用父类中的方法:

class Person {
	void eat() {
		System.out.println("Person.eat()");
	}
}
class Boy extends Person {
}
public class Persons {
	public static void main(String[] args) {
		Person p = new Boy();
		p.eat();
	}
}

【运行结果】:

Person.eat()

2.静态方法的绑定

将上面的方法都加上static关键字,变成静态方法:

class Person {
	static void eat() {
		System.out.println("Person.eat()");
	}
	static void speak() {
		System.out.println("Person.speak()");
	}
}
class Boy extends Person {
	static void eat() {
		System.out.println("Boy.eat()");
	}
	static void speak() {
		System.out.println("Boy.speak()");
	}
}
class Girl extends Person {
	static void eat() {
		System.out.println("Girl.eat()");
	}
	static void speak() {
		System.out.println("Girl.speak()");
	}
}
public class Persons {
	public static Person randPerson() {
		switch ((int)(Math.random() * 2)) {
			default:
			    case 0:
			      return new Boy();
			case 1:
			      return new Girl();
		}
	}
	public static void main(String[] args) {
		Person[] p = new Person[4];
		for (int i = 0; i < p.length; i++) {
			p[i] = randPerson();
			// 随机生成Boy或Girl
		}
		for (int i = 0; i < p.length; i++) {
			p[i].eat();
		}
	}
}

【运行结果】:
Person.eat()
Person.eat()
Person.eat()
Person.eat()
观察结果,对于静态方法而言,不管父类引用指向的什么子类对象,调用的都是父类的方法。

助记口诀

- 静态方法:静态方法看父类
- 非静态方法:非静态方法看子类

总结

以上就是本文关于Java继承概念详细解读的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

浅谈Java继承中的转型及其内存分配

看书的时候被一段代码能凌乱啦,代码是这样的:

package 继承;
abstract class People
  {
    public String tag = "疯狂Java讲义";     //①
    public String name = "Parent";
    String getName(){
      return name;
    }
  }
  class Student extends People
  {
    //定义一个私有的tag实例变量来隐藏父类的tag实例变量
    String tag = "轻量级Java EE企业应用实战";     //②
    public String name = "Student";
  }
  public class HideTest2
  {
    public static void main(String[] args)
    {
      Student d = new Student();
      //将d变量显式地向上转型为Parent后,即可访问tag实例变量
      //程序将输出:“疯狂Java讲义”
      System.out.println(((People)d).tag);     //④
      System.out.println(d.getName()); //parent
    }
  }

运行结果:

疯狂Java讲义
Parent

在这个代码中,抽象父类People定义了两个变量和一个getName()方法,子类student也定义了两个和父类同名的变量,把父类的隐藏。

关于这段代码的两个困惑:1.子类实例化时必须首先实例化父类对象,而父类是抽象类,不能有对象。那到底子类实例化时产不产生父类对象???

2.d.getName();//返回的是parent,而不是student.不应该把父类的隐藏么??

书中是这么解释的:

Student对象会保存两份实例变量,一份是people中定义的实例变量,一份是Student中定义的实例变量,d变量引用一个Student对象,内存示意图如下:

浅谈Java继承中的转型及其内存分配

将d向上转型为Parent对象,在通过它访问name变量是允许的,也就是输出“parent”。

但看着他的解释还是有点不明白,说的不是很清楚,又去网上搜了下:

java子类实例化时是否同时存在一个父类对象.

假如父类A中有个inta=1;

子类B继承A,同时B中覆盖个inta=2;

运行:

Atest=newB();

system.out.println(test.a);

结果是1,是父类中的属性.这个时候是否存在父类对象,我的理解是存在的.

我又试,把父类用抽象abstract修饰,按理说abstract累不能实例化吧,肯定不能得到父类中的a属性,结果还是一样的.

怎么理解.

问题补充:

是不是创建子类对象,肯定会出现一个父类的对象?

精彩回答

不会产生父类对象,只是用了父类的构造函数而已,并不是用到构造函数就会产生对象,构造函数只是起对象初始化作用的,而不是起产生对象作用的,如果newA();即只有new语句才会产生父类A的对象。

变量是静态绑定,方法是动态绑定。这里面变量在编译期间实现了变量调用语句与变量定义赋值语句的绑定,绑定的自然是父类的,因为调用时类型是父类的,所以值是父类中定义的值

其实你可以这么理解创建了一个子类对象时,在子类对象内存中,有两份这个变量,一份继承自父类,一份子类。

绝对不会产生父类对象,父类中的成员被继承到子类对象中,用指向子类对象的父类引用调用父类成员,只不过是从子类对象内存空间中找到那个被继承来的父类成员,也就是说实质是用子类对象调用变量a,这样就可以解释成员必须通过对象调用的规定,只不过这时调用的是子类对象中的继承自父类的a(子类对象中有两个a,一个继承自父类,一个属于自己)

哎,话说的有些乱。这个问题也困惑我很久,上网查询发现很多人是错误的,最后找到几篇好的文章才明白,可能很多java老手也都会犯“产生父类对象”这个错误,最近才搞明白。

你自己想想,如果产生父类对象,如果父类是抽象类,抽象类允许产生对象吗?所以这种说法不严谨

动态绑定定义

动态绑定是指在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法

静态绑定与动态绑定

除了限制访问,访问方式也决定哪个方法将被子类调用或哪个属性将被子类访问.函数调用与函数本身的关联,以及成员访问与变量内存地址间的关系,称为绑定.

在计算机语言中有两种主要的绑定方式,静态绑定和动态绑定.静态绑定发生于数据结构和数据结构间,程序执行之前.静态绑定发生于编译期,因此不能利用任何运行期的信息.

它针对函数调用与函数的主体,或变量与内存中的区块..动态绑定则针对运行期产生的访问请求,只用到运行期的可用信息.在面向对象的代码中,动态绑定意味着决定哪个方法被调用或哪个属性被访问,

将基于这个类本身而不基于访问范围.

子类在创建实例后,类初始化方法会调用父类的初始化方法(除了java.lang.Object类,因为java.lang.Object类没有父类),而这种调用会逐级追述,直到java.lang.Object的初始化方法。

这个地方我说的是初始化方法,而不是构造方法,因为构造方法是相对于java源程序而言,而编译后的class文件是初始化方法即"<init>"方法(红色部分为方法名),

初始化方法是由java源程序的三个部分组成的,一个部分是成员字段后的直接的初始化语句,例如privateinti=0;privateDatedate=newDate();等等,第二个部分是由初始化块组成,例如:

Javacode

publicclassTest{

privateinti=0;//初始化第一部分

//以下大括号内为初始化第二部分

{this.i=4;//dosomething......}}

第三个部分就是java源代码中的构造方法中的代码,java源代码中有几个构造方法,那么class文件中就有几个初始化方法,编译器会把第一部分与第二部分分别复制到每个初始化方法的前端,然后把初始化

方法对应参数的构造方法的代码复制到相应初始化方法中(这里说的复制其实应该说是编译,不过为了让你更好理解所以如此说).

那么说初始化方法如何追述其父类的,这也关系到初始化方法的结构,初始化方法的执行顺序以及结构就如上所说,但是每个初始化方法的第一个执行指令就是调用另外一个初始化方法,

这个初始化方法可能是自身类某个初始化方法,例如你的构造函数中第一句有类似this(...)这种语句,那么初始化方法就会调用自身类的指定构造方法;如果你的构造方法中没有指定构造方法调用,

那么初始化方法会默认调用父类无参数初始化方法,如果你的子类第一句为super(....),那么初始化方法会调用父类指定初始化方法。这种调用过程会递归进行调用,直到这个类是java.lang.Object类。

调用初始化方法并不代表会生成对象,你的java代码中出现new关键字加上构造方法的调用,只会生成一个对象,其父类对象不会生成,所以调用父类为抽象类的构造方法完全是合理的。

而且初始化方法对于虚拟机来说只是一个名称叫做"<init>"的普通方法,区别只是生成对象以后调用而已(sun的jdk私有包中有绕过构造方法生成对象的方式,可以证明之上说法,具体如何我这里不陈述)。

然后回答你的第二个问题,抽象类中的构造方法其实是用来给继承的子类来用的,因为构造方法相当于初始化方法,当子类调用构造方法时必须调用父类构造方法,

所以你可以在子类产生对象时抽象类中按需求初始化抽象类中的字段以及执行一些初始化代码。其实并不是一定要生成某个类的实例才调用构造方法,子类也需要调用父类构造方法。

而生成实例也并不一定会调用构造方法,在某些特殊实现中或者特殊情况下,生成实例不会调用构造方法。而调用了构造方法也不一定就生成了一个实例,但是那一定是一个实例调用的,就像一个普通的实例方法一样。

总结

以上就是本文关于浅谈Java继承中的转型及其内存分配的全部内容,希望对大家有所帮助,感兴趣的朋友可以继续参阅本站:Java编程之继承问题代码示例、Java面向对象编程(封装/继承/多态)实例解析等,有什么问题可以随时留言,小编会及时回复大家的。感谢朋友们对本站的支持!

以上就是本次给大家分享的全部知识点内容总结,大家还可以在下方相关文章里找到儿童python编程入门书籍推、 vue项目中使用md5加密以及、 解决axios.interceptors.respon、 等java文章进一步学习,感谢大家的阅读和支持。

上一篇:Java输入输出流代码实例用法

下一篇:Java获取文件路径出现乱码的问题的解决方法

展开 +

收起 -

学习笔记
网友NO.332278

详解 Java继承关系下的构造方法调用

详解 Java继承关系下的构造方法调用 在Java中创建一个类的对象时,如果该类存在父类,则先调用父类的构造方法,然后再调用子类的构造方法。如果父类没有定义构造方法,则调用编译器自动创建的不带参数的默认构造方法。如果父类定义了public的无参的构造方法,则在调用子类的构造方法前会自动先调用该无参的构造方法。如果父类只有有参的构造方法,没有无参的构造方法,则子类必须在构造方法中必须显式调用super(参数列表)来指定某个有参的构造方法。如果父类定义有无参的构造方法,但无参的构造方法声明为private,则子类同样必须在构造方法中必须显式调用super(参数列表)来指定某个有参的构造方法。如果父类没有其他的有参构造方法,则子类无法创建。 有父类 子类 Public无参构造方法 Private无参构造方法 有参构造方法 无 无 无 所有构造方法都会调用父类的默认构造方法 有 无 无 所有构造方法都会调用定义的无参构造方法 无 无 有 所有构造方法都必须指定调用某个有参的构造方法,或通过this调用某个其他的构造方法。 有 无 有 可以指定调用某个构造方法,如果没有指定,则调用无参构造方法。 无 有 无 子类无法构造(父类无法派生子类) 无 有 有 所有构造方法都必须指定调用某个有参的构造方法,或通过……

网友NO.226919

Java集合类的组织结构和继承、实现关系详解

Collection继承、实现关系如下(说明 (I) 表示接口, (C) 表示Java类,--表示继承,——表示实现): (I) Iterable |-- (I) Collection |-- (I) List |—— (C) ArrayList |—— (C) LinkedList |—— (C) Vector |-- (I) Set |—— (C) HashSet |-- (I) Queue [kju] Map继承、实现关系如下 Map |—— (C) HashMap |—— (C) HashTable Iterable: package java.lang;import java.util.Iterator;public interface IterableT { IteratorT iterator();} Iterator: package java.util;public interface IteratorE { boolean hasNext(); E next(); void remove();} Collection: package java.util;public interface CollectionE extends IterableE { int size(); boolean isEmpty(); boolean contains(Object o); IteratorE iterator(); Object[] toArray(); T T[] toArray(T[] a); boolean add(E e); boolean remove(Object o); boolean containsAll(Collection? c); boolean addAll(Collection? extends E c); boolean removeAll(Collection? c); boolean retainAll(Collection? c); void clear(); boolean equals(Object o); int hashCode();} Map: package java.util;public interface MapK,V { int size(); boolean isEmpty(); boolean containsKey(Object key); boolean containsValue(Object value); V get(Object key); V put(K key, V value); V remove(Object key); void putAll(Map? extends K, ? extends V m); void clear(); SetK keySet(); CollectionV values(); SetMap.EntryK, V entrySet(); interface EntryK,V { K getKey(); V getValue(); V setValue(V value); boolean equals(Object o); int hashCode(); } boolean equals(Object o); int……

网友NO.816226

JavaScript的六种继承方式(推荐)

继承是面向对象编程中又一非常重要的概念,JavaScript支持实现继承,不支持接口继承,实现继承主要依靠原型链来实现的。 原型链 首先得要明白什么是原型链,在一篇文章看懂proto和prototype的关系及区别中讲得非常详细 原型链继承基本思想就是让一个原型对象指向另一个类型的实例 function SuperType() { this.property = true } SuperType.prototype.getSuperValue = function () { return this.property } function SubType() { this.subproperty = false } SubType.prototype = new SuperType() SubType.prototype.getSubValue = function () { return this.subproperty } var instance = new SubType() console.log(instance.getSuperValue()) // true 代码定义了两个类型SuperType和SubType,每个类型分别有一个属性和一个方法,SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。 实现的本质是重写原型对象,代之以一个新类型的实例,那么存在SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。 我们知道,在创建一个实例的时候,实例对象中会有一个内部指针指向创建它的原型,进行关联起来,在这里代码SubType.prototype = new SuperType(),也会在SubType.prototype创建一个内部指针,将SubType.prototype与SuperType关联起来。 所以instance指向SubType的原型,SubType的原型又指向SuperType的原型,……

网友NO.380316

简单理解JavaScript中的封装与继承特性

JavaScript中的封装 封装简单地说就是让外界只能访问对象的共有变量和函数,隐藏细节和数据。 js中有三种方法创建对象,分别为门户大开型、用命名规范区分私有变量、闭包创建真正的私有变量三种。 1.门户大开型,是实现对象的最基础的方法,所有方法与变量都是共有的外界可以访问。 var Book = function(name){ if(this.check(name)){ console.log("error"); throw new Error("name null"); } this.name = name; } Book.prototype = { check:function(name){ if(!name){ return true; } }, getName:function(){ return this.name; } } var book = new Book("哈哈"); //output:哈哈 哈哈 console.log(book.name,book.getName()); 这个例子是门户大开型的典型,外界能直接访问对象的属性和方法。可以注意到属性和变量都有"this"来创建。 2.用命名规范区分私有变量,该方法是门户大开型的优化版本,只不过是在私有变量或方法前面用"_"区分,如果有程序员有意使用_getName()的方法来调用方法,还是无法阻止的,不是真正地将变量隐藏。 3.闭包创建真正的私有变量,该方法利用js中只有函数具有作用域的特性,在构造函数的作用域中定义相关变量,这些变量可以被定义域该作用域中的所有函数访问。 var Book2 = function(name){ if(check(name)){ console.log("error"); throw new Error("name null"); } name = name; function check(name){ if(!name){ return true; } ……

<
1
>

Copyright 2018-2019 xz577.com 码农之家

版权责任说明