What is Java ?(一)
What is Java ?(一)
xiaoyan内容:Java基本概念
、数据类型
、面向对象
本篇博客是笔者作为初学者记录自己对Java一些基本概念的理解。内容参考了大量网络资源,篇幅很长,旨在作为个人学习笔记,供自己日后回顾和复习。
概念
Java的特点
Java语言以其三大核心特点著称:
- 跨平台性:Java的口号“一次编译,处处运行”体现了其强大的跨平台能力。Java源代码经过编译后生成字节码文件(.class文件),这些字节码文件可以在任何安装了Java虚拟机(JVM)的平台上运行。需要注意的是,虽然Java语言本身是跨平台的,但JVM并非跨平台,因此在不同的操作系统上需要安装相应的JDK(Java Development Kit)。
- 面向对象:Java是一门严格遵循面向对象编程范式的语言。它将现实世界中的业务逻辑抽象为对象,并通过对象的属性和行为来描述这些逻辑,从而使得代码更贴近现实世界的模型,便于理解和维护。
- 自动内存管理:Java内置了垃圾回收机制,能够自动回收不再使用的内存资源,避免了开发者手动管理内存的繁琐工作。这一特性大大减少了内存泄漏和内存溢出等常见问题,提升了程序的稳定性和开发效率。
Java如何实现跨平台
Java之所以能够实现跨平台运行,关键在于其核心组件——Java虚拟机(Java Virtual Machine,简称JVM)。JVM是Java Development Kit(JDK)中的一个重要组成部分,它负责将编译后的字节码文件解释并执行。
具体来说,Java源代码首先被编译成与平台无关的字节码文件(.class文件)。这些字节码文件随后被JVM解释执行。由于JVM在不同的操作系统上都有相应的实现版本,因此相同的字节码文件可以在安装了相应JVM的任何操作系统上运行。
这种机制使得Java具备了“一次编译,处处运行”的特性,极大地提高了代码的可移植性。开发者只需编写一次代码,并将其编译成字节码,就可以在多种平台上运行,而无需针对不同平台进行额外的编译工作。
Java与其他编程语言的区别
与人类能够理解的自然语言不同,计算机只能理解由“0”和“1”组成的机器指令集。常见的编程语言如C/C++、Java、Python、TypeScript等属于高级语言,这些语言编写的代码机器本身无法直接理解,需要经过特定的处理才能转化为机器指令。根据处理方式的不同,编程语言可以分为两大类:
- 编译型语言:
- 代表语言:C/C++
- 特点:源代码在运行前需要通过编译器编译成机器码,生成可执行文件。这种方式的优点是执行速度快,但缺点是可移植性较差,因为生成的机器码通常是针对特定平台的。
- 解释型语言:
- 代表语言:Python
- 特点:源代码在运行时由解释器逐行解释并执行。这种方式的优点是跨平台性好,但缺点是执行速度相对较慢。
Java结合了编译型和解释型语言的特点,采用了编译+解释+即时编译(Just-In-Time Compilation,JIT)的执行方式,JVM解释执行流程图如下:
- 编译阶段:
- Java源代码首先被编译成字节码文件(.class文件),这些字节码文件是与平台无关的中间代码。
- 解释阶段:
- 字节码文件在运行时由Java虚拟机(JVM)解释执行,JVM将字节码翻译成特定平台的机器指令。
- 需要注意的是,字节码文件在JVM中并不仅仅被解释执行,同时也会使用即时编译技术进行优化。
- 即时编译(JIT):
- 即时编译技术允许JVM在运行时将频繁执行的字节码直接编译成机器码,从而提高执行效率。
- 在JVM中,使用程序计数器(Program Counter,PC)来跟踪当前执行的字节码指令。当某个字节码指令被执行到一定次数时,JVM会启用即时编译技术。
- JIT编译器会监控字节码的执行频率,当发现某些代码块频繁执行时,会将这些代码块编译成机器码,并缓存起来,以便后续执行时直接使用机器码,从而提高执行速度。
这种混合执行方式使得Java既具备了编译型语言的高效性,又具备了解释型语言的跨平台性。开发者只需编写一次代码,并将其编译成字节码,就可以在安装了JVM的任何平台上运行,从而实现了“一次编译,处处运行”的特性。
总结来说,Java通过其独特的编译+解释执行方式,在保持高效性的同时,实现了高度的跨平台性,这是它与其他编程语言的主要区别之一。
JDK、JRE、JVM
JDK、JRE和JVM是Java开发和运行环境中的三个核心组件,它们之间的关系如下:
- JDK(Java Development Kit):JDK是Java开发工具包,包含了开发Java应用程序所需的所有工具和库。主要组件包括编译器(javac)、调试工具(jdb)、Java标准库和其他开发工具所需的库。JDK中包含了JRE,因此开发者可以在本地运行和测试他们编写的Java程序。
- JRE(Java Runtime Environment):JRE是Java程序运行时所需的最小环境,包括一组Java库和JVM。主要组件包括Java标准库和JVM,确保Java程序能够在任何安装了JRE的系统上运行。
- JVM(Java Virtual Machine):JVM是Java虚拟机,是Java程序运行的核心环境。主要功能包括字节码解释执行、内存管理(包括垃圾回收)、安全性和跨平台性。JVM使得Java程序能够在不同的操作系统上运行,实现了“一次编译,处处运行”的特性。
三者的关系
- JDK包含JRE:JDK是开发工具包,包含了开发Java应用程序所需的所有工具和库,其中就包括JRE。
- JRE包含JVM:JRE是运行Java程序所需的最小环境,包含了Java库和JVM。
简而言之,JDK是开发工具包,JRE是运行环境,JVM是执行引擎。JDK包含JRE,JRE包含JVM。
数据类型
基本数据类型
Java中有8种基本数据类型,主要分为3类:
- 数值型:
- 整型:
byte
、short
、int
、long
- 浮点型:
float
、double
- 整型:
- 字符型:
char
- 布尔型:
boolean
各个数据类型所占字节数和取值范围表示如下(一个字节占8个bit位):
数据类型 | 字节数 | 默认值 | 取值范围 |
---|---|---|---|
byte |
1 | 0 | -128 到 127(-2^7~2^7-1) |
short |
2 | 0 | -32,768 到 32,767(-2^15~2^15-1) |
int |
4 | 0 | -2,147,483,648 到 2,147,483,647(-2^31~2^31-1) |
long |
8 | 0L | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807(-2^63~2^63-1) |
float |
4 | 0.0f | 大约 ±3.4E+38(有效位数为6-7位) |
double |
8 | 0.0d | 大约 ±1.7E+308(有效位数为15位) |
char |
2 | ‘\u0000’ | 0 到 65,535(Unicode字符) |
boolean |
1 | false | true 或 false |
数据类型转换
数据类型转换方式
在Java中,数据类型转换主要有以下几种方式:
- 自动类型转换(隐式转换)
当目标类型的范围大于源类型时,Java会自动将源类型转换为目标类型,无需显式的类型转换。例如:
- 将
int
转换为long
- 将
float
转换为double
- 强制类型转换(显式转换)
当目标类型的范围小于源类型时,需要使用强制类型转换将源类型转换为目标类型。这可能导致数据丢失或溢出。语法为:
1 | 目标类型 变量名 = (目标类型) 源类型; |
例如:
- 将
long
转换为int
- 将
double
转换为int
- 字符串转换
Java提供了将字符串表示的数据转换为其他类型数据的方法。例如:
- 将字符串转换为整型
int
,可以使用Integer.parseInt()
方法。 - 将字符串转换为浮点型
double
,可以使用Double.parseDouble()
方法。
- 数值之间的转换
Java提供了一些数值类型之间的转换方法,如将整型转换为字符型、将字符型转换为整型等。这些转换方式可以通过类型的包装类来实现,例如 Character
类、Integer
类等提供了相应的转换方法。
类型互转可能出现的问题
- 数据丢失
当将一个范围较大的数据类型转换为一个范围较小的数据类型时,可能会发生数据丢失。例如:将一个 long
类型的值转换为 int
类型时,如果 long
值超出了 int
类型的范围,转换结果将是截断后的低位部分,高位部分的数据将丢失。
- 数据溢出
与数据丢失相反,当将一个范围较小的数据类型转换为一个范围较大的数据类型时,可能会发生数据溢出。例如:将一个 int
类型的值转换为 long
类型时,转换结果会填充额外的高位空间,但原始数据仍然保持不变。
- 精度损失
在进行浮点数类型的转换时,可能会发生精度损失。例如:将一个单精度浮点数(float
)转换为双精度浮点数(double
)时,精度可能会损失。
- 类型不匹配导致的错误
在进行类型转换时,需要确保源类型和目标类型是兼容的。如果两者不兼容,可能会导致编译错误或运行时错误。
基本数据类型与包装类
为何需要包装类?
在Java中,包装类(Wrapper Classes)的存在有以下几个重要原因:
对象封装:
包装类将基本数据类型(如
int
、char
、boolean
等)封装成对象,使得这些基本数据类型可以像对象一样进行操作。例如,Integer
类不仅封装了int
类型的数据,还提供了许多处理int
数据的方法,如parseInt()
、valueOf()
等。集合类的支持:
Java中的集合类(如
ArrayList
、HashMap
等)只能存储对象,不能直接存储基本数据类型。因此,如果需要将基本数据类型存储在集合中,必须将其包装成对应的包装类对象。例如,将int
类型的数据存储在ArrayList
中时,需要将其转换为Integer
对象。方法参数和返回值:
许多Java方法和API要求使用对象作为参数或返回值,而不是基本数据类型。例如,
java.util.Collections
类中的许多方法都要求使用List<Integer>
而不是List<int>
。提供额外功能:
包装类提供了许多有用的方法来处理基本数据类型,如类型转换、字符串解析、比较等。例如,
Integer
类提供了parseInt()
方法将字符串转换为int
,Double
类提供了parseDouble()
方法将字符串转换为double
。
以下是包装类应用的示例代码:
1 | // 基本数据类型 |
包装类及其对应的基本数据类型:
包装类 | 对应的基本数据类型 |
---|---|
Byte |
byte |
Short |
short |
Integer |
int |
Long |
long |
Float |
float |
Double |
double |
Character |
char |
Boolean |
boolean |
通过使用包装类,Java开发者可以更方便地处理基本数据类型,并充分利用面向对象编程的优势。
基本数据类型与包装类的转换:装箱和拆箱
在Java中,装箱(Boxing)和拆箱(Unboxing)是基本数据类型与其对应的包装类之间的自动转换过程。
装箱(Boxing)
装箱是指将基本数据类型转换为其对应的包装类对象。Java编译器会自动完成这个过程,称为自动装箱。例如:
1 | int num = 10; |
在这个例子中,int
类型的 num
被自动转换为 Integer
对象 wrappedNum
。
拆箱(Unboxing)
拆箱是指将包装类对象转换为其对应的基本数据类型。Java编译器也会自动完成这个过程,称为自动拆箱。例如:
1 | Integer wrappedNum = 10; |
在这个例子中,Integer
对象 wrappedNum
被自动转换为 int
类型的 num
。
装箱和拆箱的应用场景
集合类:集合类(如
ArrayList
、HashMap
等)只能存储对象,因此需要将基本数据类型装箱后才能存储在集合中。例如:1
2
3ArrayList<Integer> list = new ArrayList<>();
list.add(10); // 自动装箱
int num = list.get(0); // 自动拆箱方法参数和返回值:许多方法要求使用对象作为参数或返回值,因此需要将基本数据类型装箱后传递给这些方法。例如:
1
2
3
4
5
6public void printInteger(Integer num) {
System.out.println(num);
}
int primitiveNum = 42;
printInteger(primitiveNum); // 自动装箱
通过装箱和拆箱,Java开发者可以更方便地在基本数据类型和包装类之间进行转换,从而充分利用面向对象编程的优势。
自动装拆箱的弊端
虽然自动装箱(Autoboxing)和自动拆箱(Auto-unboxing)为Java开发者提供了便利,但它们也存在一些潜在的弊端和需要注意的问题:
性能开销:自动装箱和拆箱涉及到对象的创建和销毁,这会带来一定的性能开销。频繁的装箱和拆箱操作可能会导致性能下降,尤其是在循环或大量数据处理的情况下。
空指针异常:自动拆箱时,如果包装类对象为
null
,会抛出NullPointerException
。例如:1
2Integer boxedInt = null;
int primitiveInt = boxedInt; // 抛出 NullPointerException代码可读性:过多的自动装箱和拆箱可能会降低代码的可读性,尤其是在复杂的表达式中。例如:
1
2boolean result = new Integer(10) == new Integer(10); // false,因为比较的是对象引用
boolean result2 = new Integer(10).equals(new Integer(10)); // true,因为比较的是对象内容类型转换错误:自动装箱和拆箱可能会导致类型转换错误,尤其是在混合使用不同类型的包装类时。例如:
1
2Long boxedLong = 10L;
int primitiveInt = boxedLong; // 编译错误,Long不能直接转换为int
虽然自动装箱和拆箱为Java开发者提供了便利,但在使用时需要注意其潜在的性能开销、空指针异常、代码可读性和类型转换错误等问题。合理使用自动装箱和拆箱,可以提高代码的简洁性和可读性,但过度依赖可能会带来不必要的麻烦。
有了包装类,还留着基本数据类型干啥?
在Java中,保留基本数据类型(Primitive Types)而不全部使用包装类(Wrapper Classes)有以下几个重要原因:
性能优势:
- 内存占用:基本数据类型直接存储在栈内存中,占用空间小,访问速度快。而包装类对象存储在堆内存中,占用空间较大,访问速度相对较慢。
- 操作效率:基本数据类型的操作(如算术运算、逻辑运算)直接在硬件层面上进行,效率更高。而包装类对象的操作需要通过方法调用,效率较低。
简化编程:
- 代码简洁性:基本数据类型的使用使得代码更加简洁明了,减少了不必要的对象创建和销毁。
- 避免空指针异常:基本数据类型没有
null
值,因此不会出现空指针异常。而包装类对象可能为null
,需要额外的空值检查。
语言设计的一致性:
- 历史兼容性:Java从一开始就设计了基本数据类型,许多现有的代码库和框架都依赖于基本数据类型。完全移除基本数据类型会破坏大量的现有代码。
- 语言特性:基本数据类型是Java语言的一部分,提供了语言设计的一致性和完整性。
基本数据类型在内存占用、操作效率和代码简洁性方面具有显著优势,因此在性能敏感的场景中,使用基本数据类型是更好的选择。而包装类则提供了对象封装、集合类支持和额外功能等优势,适用于需要对象操作和面向对象编程的场景。
Java通过保留基本数据类型和提供包装类,兼顾了性能和功能需求,使得开发者可以根据具体场景选择合适的数据类型,从而实现高效、灵活的编程。
面向对象
面向对象编程简介
面向对象编程(Object-Oriented Programming, OOP)是一种编程范式,通过构建对象(对象具有属性和行为)来表示现实世界中的实体及其行为。这种编程思想使得代码更易于理解和维护。
面向对象编程的核心特性包括:
- 封装(Encapsulation):将对象的属性和行为结合在一起,隐藏内部实现细节,仅通过接口与外界交互。封装增强了代码的安全性和独立性,简化了编程复杂度。
- 继承(Inheritance):子类可以继承父类的属性和方法,从而实现代码的复用。继承有助于构建层次化的类结构,减少重复代码。
- 多态(Polymorphism):多态允许不同的类对象对同一消息做出不同的响应。多态分为两种类型:
- 编译时多态(静态多态),通过方法重载实现,即同一个方法名在不同参数下有不同的实现。
- 运行时多态(动态多态),通过方法重写实现,即子类重写父类的方法,在运行时根据对象类型调用相应的方法。(接口的实现也属于运行时多态。)
啥是多态?
以上对多态的解释有点点抽象,我们可以进一步讲讲。
多态的体现
多态(Polymorphism)是面向对象编程中的一个核心概念,它允许不同的对象对同一消息做出不同的响应。多态性使得代码更加灵活、可扩展和易于维护。多态性主要体现在以下几个方面:
1. 方法重载(Overloading)
方法重载是指在同一个类中定义多个同名方法,但这些方法的参数列表不同(参数类型、数量或顺序不同)。编译器在编译时根据调用时提供的参数类型和数量来决定调用哪个方法。
示例:
1 | class Calculator { |
在这个示例中,Calculator
类中有三个 add
方法,但它们的参数列表不同。编译器根据调用时提供的参数类型和数量来决定调用哪个方法。
2. 方法重写(Overriding)
方法重写是指子类重新定义父类中已有的方法,以实现不同的操作逻辑。重写的方法需要加上 @Override
注解。在程序运行时,系统会根据引用对象的实际类型来调用具体版本的方法。
示例:
1 | class Animal { |
在这个示例中,Dog
和 Cat
类都重写了 Animal
类的 makeSound
方法。在运行时,根据实际对象类型调用相应的方法。
3. 接口实现(Interface Implementation)
接口实现是指多个类可以实现同一个接口,并提供各自的方法实现。接口实现体现了多态性,因为不同的类可以对同一个接口方法提供不同的实现。
示例:
1 | interface Shape { |
在这个示例中,Circle
和 Rectangle
类都实现了 Shape
接口,并提供了各自的 draw
方法实现。在运行时,根据实际对象类型调用相应的方法。
4. 上转型与下转型(Upcasting and Downcasting)
- 上转型(Upcasting):将子类对象赋值给父类引用,称为上转型。上转型是安全的,因为子类对象包含了父类的所有属性和方法。
- 下转型(Downcasting):将父类引用强制转换为子类引用,称为下转型。下转型需要谨慎使用,因为如果父类引用指向的对象不是子类类型,会导致运行时错误。
示例:
1 | class Animal { |
在这个示例中,myAnimal
是 Animal
类型的引用,但实际上指向 Dog
对象。通过上转型,可以调用 Dog
重写的 makeSound
方法。通过下转型,可以调用 Dog
特有的 fetch
方法。
多态能够用来干啥?
多态(Polymorphism)是面向对象编程中的一个核心特性,它允许子类替换父类,并在实际代码运行过程中调用子类的方法实现。多态性需要编程语言提供特殊的语法机制来实现,比如继承、接口类等。多态可以提高代码的扩展性和复用性,是许多设计模式、设计原则和编程技巧的基础。
面向对象设计原则:SOLID原则
面向对象设计中有常见的五大设计原则,简称SOLID原则。SOLID原则是一组指导原则,旨在帮助开发者创建更灵活、可维护和可扩展的软件系统。这些原则分别是:单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)和依赖倒置原则(DIP)。
1. 单一职责原则(Single Responsibility Principle, SRP)
定义:一个类应该只有一个引起它变化的原因,即一个类应该只负责一个职责。
简单示例:
- 不好的设计:一个类既负责计算工资,又负责保存员工信息。
- 好的设计:将计算工资和保存员工信息分别放在两个不同的类中。
2. 开闭原则(Open/Closed Principle, OCP)
定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。即在不修改现有代码的情况下,可以通过扩展来增加新功能。
简单示例:
- 不好的设计:每次增加新图形时,都需要修改计算面积的代码。
- 好的设计:通过定义一个抽象的图形接口,新增图形时只需实现该接口,而不需要修改现有代码。
3. 里氏替换原则(Liskov Substitution Principle, LSP)
定义:子类应该能够替换所有其父类的引用,而不会影响程序的正确性。为了保证数据安全,子类的行为应该与父类一致或更严格。
简单示例:
- 不好的设计:鸟类可以飞,但企鹅不能飞,子类行为与父类不一致。
- 好的设计:将飞行的行为抽象出来,只有能飞的鸟类才实现该行为。
4. 接口隔离原则(Interface Segregation Principle, ISP)
定义:客户端不应该依赖于它不需要的接口,即接口应该小而精、细粒度。
简单示例:
- 不好的设计:一个接口包含多个方法,但某些类只需要其中一部分方法。
- 好的设计:将接口拆分为多个小接口,每个接口只包含相关的方法。
5. 依赖倒置原则(Dependency Inversion Principle, DIP)
定义:高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
简单示例:
- 不好的设计:高层模块直接依赖于低层模块的具体实现。例如,一个高层模块直接创建并使用低层模块的对象。
- 好的设计:通过依赖抽象接口,高层模块不直接依赖于低层模块的具体实现,而是通过接口注入的形式调用实现。例如,高层模块依赖于一个抽象接口,并通过构造函数或setter方法注入具体的实现类。
抽象类和普通类、抽象类和接口的区别
抽象类与普通类的区别
抽象类和普通类都可以被继承,但它们在功能和使用场景上存在显著差异:
- 实例化能力:
- 抽象类:无法被实例化,通常作为基类使用,用于定义子类的通用行为和属性。
- 普通类:可以被实例化,用于创建具体的对象。
- 方法实现:
- 抽象类:可以包含抽象方法(没有具体实现的方法),也可以包含具体实现的方法。抽象类不能使用
final
修饰符,因为final
修饰符用于禁止该类被继承或方法被子类重写,这与抽象类的设计目的相冲突。 - 普通类:必须实现所有方法,不能包含抽象方法。
- 抽象类:可以包含抽象方法(没有具体实现的方法),也可以包含具体实现的方法。抽象类不能使用
- 静态方法:
- 抽象类:允许包含静态方法,但静态方法无法访问抽象类的实例成员,因为抽象类无法实例化。
- 普通类:允许包含静态方法,静态方法可以访问类的实例成员。
抽象类与接口的区别
抽象类和接口在设计目的和使用方式上有所不同:
- 设计目的:
- 抽象类:用于定义类的通用行为和属性,提供部分实现,子类可以继承并扩展这些行为。
- 接口:用于定义一组行为契约,实现类必须遵循这些契约,接口不提供任何实现。
- 构造方法:
- 抽象类:可以包含构造方法,用于初始化抽象类的成员变量。
- 接口:不能包含构造方法,因为接口不涉及实例化。
- 方法实现:
- 抽象类:可以包含具体实现的方法,子类可以选择性地覆盖这些方法。
- 接口:除了定义静态方法外,其他方法默认是抽象的,必须由实现类来实现。但从 Java 8 开始,接口可以包含默认方法(
default
方法),这些方法可以有具体实现,并且实现类可以选择性地覆盖这些默认方法。但注意,静态方法不能被实现类覆盖。
- 继承与实现:
- 抽象类:一个类只能继承一个抽象类。
- 接口:一个类可以实现多个接口。
- 成员变量:
- 抽象类:可以包含成员变量,这些变量可以是静态的、非静态的、常量等。
- 接口:只能包含静态常量(默认是
public static final
)且必须赋予初始值,不能包含非静态成员变量。
- 访问修饰符:
- 抽象类:方法和成员变量可以使用所有访问修饰符(
public
、protected
、private
)。 - 接口:所有方法默认是
public
,不能使用其他访问修饰符。成员变量默认是public static final
。
- 抽象类:方法和成员变量可以使用所有访问修饰符(
静态
了解了面向对象的多态特性,那么Java中的静态也一同了解一下吧~
静态变量和静态方法
在 Java 中,静态变量和静态方法与类本身关联,而不与类的实例化对象关联。它们在内存中独此一份,可以被类的所有实例化对象共享。
静态变量(类变量)
静态变量是通过 static
关键字修饰的变量,属于类而不属于实例对象。静态变量在类被加载时初始化,只会分配一次内存。所有的实例对象都能共享该静态变量,也就是说,如果一个实例对象修改了该静态变量,其他实例对象调用该变量时也会看到修改后的值。静态变量可以通过类名访问,也可以通过实例对象访问(但推荐使用类名访问)。
1 | public class MyClass { |
静态方法
静态方法也是通过 static
关键字修饰的方法,属于类而不属于实例对象。静态方法在类被加载时初始化,可以被类的所有实例化对象共享。静态方法可以通过类名直接调用,不需要创建类的实例对象。
1 | public class MyClass { |
静态内部类
静态内部类是使用 static
关键字修饰的内部类。与静态变量和静态方法类似,静态内部类属于外部类本身,而不是外部类的实例对象。
静态内部类与非静态内部类的区别
- 访问方式:静态内部类可以通过外部类名直接访问,不需要创建外部类的实例对象;非静态内部类依赖于外部类的实例,需要通过外部类的实例对象来访问。
- 访问权限:
- 静态内部类:只能访问外部类的静态变量和静态方法。不能直接访问外部类的私有成员变量和方法,必须通过外部类的实例来访问(为什么呢?因为
private
修饰符表示成员只能在声明它的类内部访问,即使是私有静态成员变量和方法,对于静态内部类来说也是不可见的)。 - 非静态内部类:可以访问外部类的实例变量和方法,包括私有成员变量和方法。
- 静态内部类:只能访问外部类的静态变量和静态方法。不能直接访问外部类的私有成员变量和方法,必须通过外部类的实例来访问(为什么呢?因为
- 实例化方式:静态内部类可以独立实例化,不需要依赖外部类的实例;非静态内部类必须等待外部类实例化后,才能实例化自己的对象。
示例代码
1 | public class OuterClass { |
编译器如何实现非静态内部类直接访问其外部类方法?
非静态内部类可以直接访问外部类的实例变量和方法,包括私有成员。这是通过编译器在内部类中生成一个隐式的外部类引用实现的。
编译器生成的代码
编译器在生成非静态内部类的字节码时,会自动为内部类添加一个指向外部类实例的引用。这个引用通常命名为 this$0
,用于访问外部类的实例成员。
1 | public class OuterClass { |
编译器生成的字节码大致如下:
1 | public class OuterClass { |
通过这种方式,非静态内部类可以直接访问外部类的实例成员,而不需要显式地传递外部类的实例。
在继承关系中,实例化子类时静态加载顺序
在继承关系中,当一个父类与子类都存在静态变量、静态方法时,实例化子类时的加载顺序如下:
加载父类的静态代码块:父类的静态代码块(即静态变量、静态方法等)在首次使用到与父类相关的代码时加载,并且仅加载一次。
加载子类的静态代码块:子类的静态代码块(即静态变量、静态方法等)在首次使用到与子类相关的代码时加载,并且仅加载一次。
加载父类的构造函数:父类的构造函数在实例化子类时首先被调用。
加载子类的构造函数:子类的构造函数在父类的构造函数执行完毕后被调用。
示例代码
1 | class ParentClass { |
输出结果
1 | ParentClass static block |
解释
在首次使用到与父类相关的代码时(即实例化子类时),父类的静态代码块首先被加载并执行。接着,子类的静态代码块在首次使用到与子类相关的代码时(即实例化子类时)被加载并执行。然后,父类的构造函数在实例化子类时被调用。最后,子类的构造函数在父类的构造函数执行完毕后被调用。