type
status
date
slug
summary
tags
category
icon
password
On Java 笔记
第一章
组合与继承
1.5 小节复用实现中提到
继承常被视为面向对象编程的重中之重,因此容易给新手程序员留下这样的印象:处处都应该使用继承。而实际上,这种全盘继承的做法会导致设计变得十分别扭和过于复杂。所以相比之下,在创建新类时应该首先考虑组合,因为使用组合更为简单灵活,设计也更为清晰简洁。一旦你拥有了足够的经验,何时使用继承就会变得非常清晰了。
为什么组合优先于继承?
组合优于继承的说法在软件设计中是一条重要的原则,主要是因为组合提供了更大的灵活性和更低的耦合度,利于代码的重用和维护
继承的问题:
- 继承使子类和父类之间有很强的依赖关系,子类会继承父类的实现细节。如果父类发生变化,子类也必须进行相应的修改
- 继承使得子类可以访问父类的受保护的成员,可能会破坏父类的封装性和内部实现的独立性
- 继承是静态编译时决定的关系,一旦建立,不容易在运行时动态改变类的行为
- 随着系统的演进,继承层次会变得复杂,增加理解和维护的难度
组合的优点:
- 组合通过将一个类的实例作为另一个类的成员变量,实现类之间的关系。更改被组合的对象不会影响到其他类,维护和扩展更加容易
- 组合保持了类的封装性,内部实现细节对外部透明,不会因为继承暴露不必要的细节
- 组合关系在运行时可以动态改变。可以通过接口和依赖注入实现运行时的行为变化,提升系统的灵活性
- 一个类可以组合多个其他类的实例,组合方式多样,不受单继承的限制,容易实现复杂的功能
组合与继承的区分
- 组合描述 has-a 关系:例如,汽车有发动机,电脑有CPU
- 继承描述 is-a 关系:例如,猫是动物,狗是动物
虽然继承和组合用于描述类之间的关系有区别,但某些场景下是可以互换的,因此需要比较和权衡
因此,如果需要复用现有类的功能,优先考虑组合而不是继承
基类的 private 成员会被继承吗?
子类确实会继承基类的所有成员,包括
private
成员,但子类不能直接访问这些private
成员private
成员对于子类是不可见的,这些成员只能通过基类提供的public
或protected
方法进行访问多态
1.7 小节多态中提到
如果你并不关心具体执行的是哪一段代码,那么当你添加新的子类时,即使不对其基类的代码做任何修改,该子类实际执行的代码可能也会有所不同。但如果编译器无法得知应该具体执行哪一段代码,它会怎么做呢?答案来自继承机制的一种重要技巧:编译器并非通过传统方式来调用方法。对于非面向对象编译器而言,其生成的函数调用会触发“前期绑定”(early binding),这是一个你可能从来都没听说过的词,因为你从未考虑过使用这种方式。前期绑定意味着编译器会生成对一个具体方法名的调用,该方法名决定了被执行代码的绝对地址。但是对于继承而言,程序直到运行时才能明确代码的地址,所以就需要引入其他可行的方案以确保消息可以顺利发送至对象。为了解决上面提及的问题,面向对象语言使用的机制是“后期绑定”(late binding)。也就是说,当你向某个对象发送消息时,直到运行时才会确定哪一段代码会被调用。编译器会确保被调用的方法是真实存在的,并对该方法的参数和返回值进行类型检查,但是它并不知道具体执行的是哪一段代码。为了实现后期绑定,Java使用了一些极为特殊的代码以代替直接的函数调用,这段代码使用存储在对象中的信息来计算方法体的地址(第9章会详细地描述这个过程)。其结果就是,在这些特殊代码的作用下,每一个对象会有不同的表现。通俗地讲,当你向一个对象发送消息时,该对象自己会找到解决之道。
多态的方法调用是如何实现的?
关键概念解释
- 前期绑定:也称为静态绑定,是指
在编译时确定函数调用
的具体实现,在非面向对象语言中,编译器会生成对具体方法名的调用,这个调用指向一个确定的内存地址。C、C++(非虚函数)、Fortran、Pascal、Ada等
- 后期绑定:也称为动态绑定,是指
在运行时确定函数调用
的具体实现,在面向对象语言中,编译器在编译时只检查方法的存在性及其参数和返回值类型,但不确定具体的实现,而在运行时,系统根据对象的实际类型来决定调用哪个方法,从而实现多态。JavaScript、Python、Ruby、PHP、Java、C#、Swift、Kotlin等
Java实现后期绑定的机制
Java通过使用虚方法表来实现后期绑定。每个类都有一个虚方法表,包含了类中所有方法的指针。当对象调用方法时,系统会通过该对象的虚方法表找到实际的方法实现。虚方法表确保了即使基类引用指向子类对象,调用的方法仍然是子类重写的方法,即多态
Java的终级基类是什么?
在Java中,终极基类(Ultimate Base Class)是
java.lang.Object
。所有类(无论是用户定义的类还是Java标准库中的类)都直接或间接继承自Object
类。这意味着Object
类提供的一些方法可以在所有Java对象上使用。第三章
第一个 Java 程序
3.8 节提到
如果你需要创建一个能够独立运行的程序,那么与文件同名的类中还必须包含一个程序启动的入口方法。这个特殊的方法叫作main(),main()的参数是一个String对象的数组,虽然目前我们并不会使用args参数,但 Java 编译器会强制你传递该参数,因为它用于获取控制台的输入。
为什么 main 方法必须传递 args 参数?
Java 语言规范规定,Java 程序的入口点是一个名为
main
的静态方法,它必须具有以下签名:这种规范确保了 JVM 能够一致地找到程序的入口点,并能够以统一的方式启动 Java 应用程序。
String[] args
参数允许将命令行参数传递给程序。在程序启动时,用户可以在命令行中指定一些参数,这些参数将被传递到 main
方法中。例如:在这种情况下,
main
方法中的 args
数组将包含 "arg1"
, "arg2"
, "arg3"
第六章
初始化
6.7.2 小节中提到
为了总结对象创建的过程,假设有一个名为Dog的类。
- 尽管没有显式使用static关键字,但构造器实际上也是静态方法。因此,第一次创 建类型为Dog的对象时,或者第一次访问类Dog的静态方法或静态字段时,Java解释 器会搜索类路径来定位Dog.class文件。
- 当Dog.class被加载后(这将创建一个Class对象,后面会介绍),它的所有静态 初始化工作都会执行。因此,静态初始化只在Class对象首次加载时发生一次。
- 当使用new Dog()创建对象时,构建过程首先会在堆上为Dog对象分配足够的存储 空间。
- 这块存储空间会被清空,然后自动将该Dog对象中的所有基本类型设置为其默认值 (数值类型的默认值是0,boolean和char则是和0等价的对应值),而引用会被设 置为null。
- 执行所有出现在字段定义处的初始化操作。
- 执行构造器。正如将在第8章中看到的,这实际上可能涉及相当多的动作,尤其是在 涉及继承时。
new一个对象发生了什么?
- 类加载检查:
- 当我们第一次创建Dog类型的对象,或者第一次访问Dog类的静态方法或静态字段时,Java虚拟机会检查Dog类是否已经被加载。如果未加载,Java类加载器会在类路径中搜索并加载Dog.class文件。
- 类的加载和初始化:
- 加载(Loading):类加载器(ClassLoader)将Dog.class文件读入内存。
- 连接(Linking):包括三个阶段
- 验证(Verification):确保字节码符合JVM规范,不会危害虚拟机。
- 准备(Preparation):为类的静态字段分配内存并初始化默认值。
- 解析(Resolution):将符号引用转换为直接引用。
- 初始化(Initialization):执行类的静态初始化块和静态字段初始化。这一步只在类首次加载时发生一次。
- 对象创建:
- 使用
new
关键字创建Dog对象时,JVM会在堆上为Dog对象分配内存。
- 内存分配和初始化:
- JVM会为新的Dog对象分配足够的存储空间,并将这块存储空间清空。
- 自动将Dog对象中的所有字段(包括基本类型和引用类型)设置为其默认值。基本类型如int会被设置为0,boolean会被设置为false,char会被设置为'\u0000',引用类型会被设置为null。
- 字段初始化:
- JVM会执行字段定义时的初始化操作。例如,如果字段在定义时有赋值操作,这些赋值操作会在这一步执行。
- 构造器调用:
- 最后,调用Dog类的构造器进行初始化操作。构造器初始化可能涉及到以下内容:
- 初始化父类部分:如果Dog类继承了某个父类,会先调用父类的构造器。
- 初始化子类部分:执行Dog类构造器中定义的初始化代码。
- 处理构造器重载和构造器链:即一个构造器调用另一个构造器。
总结一下,创建一个对象的过程不仅仅是分配内存,还涉及类加载、类的静态初始化、字段初始化和构造器调用等一系列步骤。所有这些步骤确保了对象在使用前已经被正确地初始化。
第九章
构造器和多态
9.3.3 小节中总结到
编写构造器时有一个很好的准则:“用尽可能少的操作使对象进入正常状态,如果可 以避免的话,请不要调用此类中的任何其他方法。”只有基类中的final方法可以在构造器 中安全调用(这也适用于private方法,它们默认就是final的)。这些方法不能被重 写,因此不会产生这种令人惊讶的问题。你可能并不总是遵循此准则,但是应该朝这个方 向努力。
为什么构造器中调用自身里的方法不安全?
因为在Java中,如果基类的构造器调用了一个方法,而该方法在子类中被重写了,那么在创建子类实例时,基类构造器调用的将是子类重写的方法。
这种行为的原因是Java的动态绑定(dynamic binding)机制。在Java中,方法调用是在运行时绑定到实际的方法实现的。这意味着,当一个方法在基类中被调用时,Java虚拟机(JVM)会在运行时确定实际调用的是基类的方法还是子类重写的方法。当创建子类实例时,基类的构造器会在子类构造器之前被调用。然而,在执行基类构造器时,子类对象已经部分构造完毕,因此如果基类构造器中调用一个方法,JVM会检查子类是否重写了这个方法,并调用子类的方法。
- Author:风之旅人
- URL:https://www.hrmi.fun//article/onjava-8
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!