Lazy loaded image
技术分享
OnJava笔记
00 min
2020-12-12
2024-11-25
type
status
date
slug
summary
tags
category
icon
password

On Java 笔记

第一章

组合与继承

1.5 小节复用实现中提到
继承常被视为面向对象编程的重中之重,因此容易给新手程序员留下这样的印象:处处都应该使用继承。而实际上,这种全盘继承的做法会导致设计变得十分别扭和过于复杂。所以相比之下,在创建新类时应该首先考虑组合,因为使用组合更为简单灵活,设计也更为清晰简洁。一旦你拥有了足够的经验,何时使用继承就会变得非常清晰了。

为什么组合优先于继承?

组合优于继承的说法在软件设计中是一条重要的原则,主要是因为组合提供了更大的灵活性和更低的耦合度,利于代码的重用和维护
继承的问题:
  • 继承使子类和父类之间有很强的依赖关系,子类会继承父类的实现细节。如果父类发生变化,子类也必须进行相应的修改
  • 继承使得子类可以访问父类的受保护的成员,可能会破坏父类的封装性和内部实现的独立性
  • 继承是静态编译时决定的关系,一旦建立,不容易在运行时动态改变类的行为
  • 随着系统的演进,继承层次会变得复杂,增加理解和维护的难度
组合的优点:
  • 组合通过将一个类的实例作为另一个类的成员变量,实现类之间的关系。更改被组合的对象不会影响到其他类,维护和扩展更加容易
  • 组合保持了类的封装性,内部实现细节对外部透明,不会因为继承暴露不必要的细节
  • 组合关系在运行时可以动态改变。可以通过接口和依赖注入实现运行时的行为变化,提升系统的灵活性
  • 一个类可以组合多个其他类的实例,组合方式多样,不受单继承的限制,容易实现复杂的功能
组合与继承的区分
  • 组合描述 has-a 关系:例如,汽车有发动机,电脑有CPU
  • 继承描述 is-a 关系:例如,猫是动物,狗是动物
虽然继承和组合用于描述类之间的关系有区别,但某些场景下是可以互换的,因此需要比较和权衡
因此,如果需要复用现有类的功能,优先考虑组合而不是继承

基类的 private 成员会被继承吗?

子类确实会继承基类的所有成员,包括private成员,但子类不能直接访问这些private成员
private成员对于子类是不可见的,这些成员只能通过基类提供的publicprotected方法进行访问

多态

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的类。
  1. 尽管没有显式使用static关键字,但构造器实际上也是静态方法。因此,第一次创 建类型为Dog的对象时,或者第一次访问类Dog的静态方法或静态字段时,Java解释 器会搜索类路径来定位Dog.class文件。
  1. 当Dog.class被加载后(这将创建一个Class对象,后面会介绍),它的所有静态 初始化工作都会执行。因此,静态初始化只在Class对象首次加载时发生一次。
  1. 当使用new Dog()创建对象时,构建过程首先会在堆上为Dog对象分配足够的存储 空间。
  1. 这块存储空间会被清空,然后自动将该Dog对象中的所有基本类型设置为其默认值 (数值类型的默认值是0,boolean和char则是和0等价的对应值),而引用会被设 置为null。
  1. 执行所有出现在字段定义处的初始化操作。
  1. 执行构造器。正如将在第8章中看到的,这实际上可能涉及相当多的动作,尤其是在 涉及继承时。

new一个对象发生了什么?

  1. 类加载检查
      • 当我们第一次创建Dog类型的对象,或者第一次访问Dog类的静态方法或静态字段时,Java虚拟机会检查Dog类是否已经被加载。如果未加载,Java类加载器会在类路径中搜索并加载Dog.class文件。
  1. 类的加载和初始化
      • 加载(Loading):类加载器(ClassLoader)将Dog.class文件读入内存。
      • 连接(Linking):包括三个阶段
        • 验证(Verification):确保字节码符合JVM规范,不会危害虚拟机。
        • 准备(Preparation):为类的静态字段分配内存并初始化默认值。
        • 解析(Resolution):将符号引用转换为直接引用。
      • 初始化(Initialization):执行类的静态初始化块和静态字段初始化。这一步只在类首次加载时发生一次。
  1. 对象创建
      • 使用new关键字创建Dog对象时,JVM会在堆上为Dog对象分配内存。
  1. 内存分配和初始化
      • JVM会为新的Dog对象分配足够的存储空间,并将这块存储空间清空。
      • 自动将Dog对象中的所有字段(包括基本类型和引用类型)设置为其默认值。基本类型如int会被设置为0,boolean会被设置为false,char会被设置为'\u0000',引用类型会被设置为null。
  1. 字段初始化
      • JVM会执行字段定义时的初始化操作。例如,如果字段在定义时有赋值操作,这些赋值操作会在这一步执行。
  1. 构造器调用
      • 最后,调用Dog类的构造器进行初始化操作。构造器初始化可能涉及到以下内容:
        • 初始化父类部分:如果Dog类继承了某个父类,会先调用父类的构造器。
        • 初始化子类部分:执行Dog类构造器中定义的初始化代码。
        • 处理构造器重载和构造器链:即一个构造器调用另一个构造器。
总结一下,创建一个对象的过程不仅仅是分配内存,还涉及类加载、类的静态初始化、字段初始化和构造器调用等一系列步骤。所有这些步骤确保了对象在使用前已经被正确地初始化。

第九章

构造器和多态

9.3.3 小节中总结到
编写构造器时有一个很好的准则:“用尽可能少的操作使对象进入正常状态,如果可 以避免的话,请不要调用此类中的任何其他方法。”只有基类中的final方法可以在构造器 中安全调用(这也适用于private方法,它们默认就是final的)。这些方法不能被重 写,因此不会产生这种令人惊讶的问题。你可能并不总是遵循此准则,但是应该朝这个方 向努力。

为什么构造器中调用自身里的方法不安全?

因为在Java中,如果基类的构造器调用了一个方法,而该方法在子类中被重写了,那么在创建子类实例时,基类构造器调用的将是子类重写的方法。
这种行为的原因是Java的动态绑定(dynamic binding)机制。在Java中,方法调用是在运行时绑定到实际的方法实现的。这意味着,当一个方法在基类中被调用时,Java虚拟机(JVM)会在运行时确定实际调用的是基类的方法还是子类重写的方法。当创建子类实例时,基类的构造器会在子类构造器之前被调用。然而,在执行基类构造器时,子类对象已经部分构造完毕,因此如果基类构造器中调用一个方法,JVM会检查子类是否重写了这个方法,并调用子类的方法。
上一篇
后端知识笔记
下一篇
Kafka配置