Java基础常见知识点总结(上)
一、Java基础概念与常识
Java 语言有哪些特点?
- 简单易学(语法简单,上手容易);
- 面向对象(封装,继承,多态);
- 平台无关性( Java 虚拟机实现平台无关性);
- 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);
- 可靠性(具备异常处理和自动内存管理机制);
- 安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源);
- 高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的);
- 支持网络编程并且很方便;
- 编译与解释并存;
- ……
🌈 拓展一下:
“Write Once, Run Anywhere(一次编写,随处运行)”这句宣传口号,真心经典,流传了好多年!以至于,直到今天,依然有很多人觉得跨平台是 Java 语言最大的优势。实际上,跨平台已经不是 Java 最大的卖点了,各种 JDK 新特性也不是。目前市面上虚拟化技术已经非常成熟,比如你通过 Docker 就很容易实现跨平台了。在我看来,Java 强大的生态才是!
Java SE vs Java EE
- Java SE(Java Platform,Standard Edition): Java 平台标准版,Java 编程语言的基础,它包含了支持 Java 应用程序开发和运行的核心类库以及虚拟机等核心组件。Java SE 可以用于构建桌面应用程序或简单的服务器应用程序。
- Java EE(Java Platform, Enterprise Edition ):Java 平台企业版,建立在 Java SE 的基础上,包含了支持企业级应用程序开发和部署的标准和规范(比如 Servlet、JSP、EJB、JDBC、JPA、JTA、JavaMail、JMS)。 Java EE 可以用于构建分布式、可移植、健壮、可伸缩和安全的服务端 Java 应用程序,例如 Web 应用程序。
简单来说,Java SE 是 Java 的基础版本,Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序,Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序。
除了 Java SE 和 Java EE,还有一个 Java ME(Java Platform,Micro Edition)。Java ME 是 Java 的微型版本,主要用于开发嵌入式消费电子设备的应用程序,例如手机、PDA、机顶盒、冰箱、空调等。Java ME 无需重点关注,知道有这个东西就好了,现在已经用不上了。
JVM vs JDK vs JRE
JVM
Java 虚拟机(Java Virtual Machine, JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
不同编程语言(Java、Groovy、Kotlin、JRuby、Clojure 等)通过各自的编译器编译成 .class
文件,并最终通过 JVM 在不同平台(Windows、Mac、Linux)上运行。
JVM 并不是只有一种!只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。 例如,常见的 HotSpot VM 仅仅是 JVM 规范的一种实现,其他实现如 J9 VM、Zing VM、JRockit VM 等也都存在。
JDK 和 JRE
JDK(Java Development Kit)是一个功能齐全的 Java 开发工具包,供开发者使用,用于创建和编译 Java 程序。它包含了 JRE(Java Runtime Environment),以及编译器 javac
和其他工具,如 javadoc
(文档生成器)、jdb
(调试器)、jconsole
(监控工具)、javap
(反编译工具)等。
JRE 是运行已编译 Java 程序所需的环境,主要包含以下两个部分:
- JVM:即 Java 虚拟机。
- Java 基础类库(Class Library):提供常用功能和 API,如 I/O 操作、网络通信、数据结构等。
简单来说,JRE 只包含运行 Java 程序所需的环境和类库,而 JDK 不仅包含 JRE,还包括用于开发和调试 Java 程序的工具。
从 JDK 9 开始,JDK 和 JRE 的区分已不再显著,取而代之的是模块系统(JDK 被重新组织成 94 个模块)以及 jlink
工具,用于生成自定义的 Java 运行时映像,仅包含给定应用程序所需的模块。且从 JDK 11 开始,Oracle 不再提供单独的 JRE 下载。
通过使用 jlink
,开发者可以根据需求创建一个更小的运行时镜像,减少 Java 运行时环境的大小,并更好地支持现代应用程序架构(如虚拟化、容器化、微服务和云原生开发)。
什么是字节码?采用字节码的好处是什么?
字节码定义
在 Java 中,字节码是 JVM(Java Virtual Machine)可以理解并执行的代码,文件扩展名为 .class
。字节码是平台无关的,意味着它不针对特定的硬件或操作系统,而是面向虚拟机,这样 Java 程序可以在任何支持 JVM 的平台上运行。
Java 程序通过编译器 javac
将源代码(.java
文件)编译成字节码(.class
文件)。字节码是介于源代码和机器码之间的一种中间形式,包含了一系列的指令,JVM 可以执行这些指令,而不关心底层平台的具体实现。
采用字节码的好处
平台独立性:
字节码不针对特定的硬件平台,因此 Java 程序可以在任何安装了 JVM 的操作系统上运行。这实现了 Java 的 "一次编译,到处运行" 特性。提高效率:
Java 通过字节码解决了传统解释型语言的执行效率低的问题。虽然字节码最初是通过解释器执行的,但 JVM 还支持 JIT 编译技术,可以将热点代码编译为机器码,从而显著提高运行效率。跨平台性:
字节码本身独立于平台,因此 Java 程序无需针对每个操作系统进行重新编译。只要 JVM 实现满足 Java 规范,就可以在任何支持 JVM 的平台上运行。便于优化:
由于字节码是虚拟机执行的中间代码,JVM 在执行时可以进行多种优化,比如 JIT 编译、内存管理优化等,从而提高程序的执行效率。
Java 程序从源代码到运行的过程
- 编写源代码:开发者编写 Java 源代码文件(
.java
)。 - 编译成字节码:
javac
编译器将.java
文件编译成.class
字节码文件。 - JVM 类加载:JVM 通过类加载器加载字节码文件,将其读取到内存中。
- 字节码解释执行:JVM 通过解释器逐行解释字节码,执行相应的操作。
- JIT 编译:对于经常执行的代码,JVM 会通过 JIT 编译器将字节码转换为机器码,缓存下来,下次直接执行机器码,提高执行效率。
JIT(Just In Time Compilation)
JIT 编译是 JVM 的一种优化技术,它在运行时将字节码编译为机器码,并将其缓存,之后直接执行缓存的机器码。这是 Java 的一个重要特性,它结合了编译型语言的执行效率和解释型语言的可移植性。
- 热点代码:JVM 根据代码的执行频率,识别出“热点代码”,这些代码被优先编译成机器码,以提高执行效率。
- 惰性评估:JVM 通过惰性评估,只对频繁执行的代码进行 JIT 编译,从而减少了不必要的编译工作,提升了效率。
JVM 的工作流程
- 类加载器:加载字节码文件。
- 执行引擎:解释执行字节码,或者通过 JIT 编译执行热点代码。
- 垃圾回收:自动管理内存,回收不再使用的对象。
- 优化:根据执行情况对代码进行优化(例如通过 JIT)。
总结
字节码使得 Java 程序能够实现跨平台和高效的执行。通过结合解释执行和 JIT 编译,Java 提供了高效且平台无关的运行环境。字节码的存在是 Java 实现“一次编译,随处运行”的关键所在。
为什么说 Java 语言“编译与解释并存”?
Java 语言被称为“编译与解释并存”的原因在于其独特的执行过程:Java 程序在运行时需要经历先编译,再解释的两个步骤。
编译阶段:
Java 源代码(.java
文件)首先由编译器javac
编译成字节码(.class
文件)。字节码是平台无关的中间代码,它不针对特定操作系统或硬件平台,而是面向 JVM(Java Virtual Machine)。这意味着字节码可以在任何支持 JVM 的平台上运行。解释执行阶段:
生成的字节码文件并不直接运行在硬件上,而是通过 JVM 来执行。JVM 的字节码解释器会逐行解释字节码并将其转换为机器码执行,这就是所谓的“解释执行”。这种解释型执行方式使得 Java 程序在不同平台上都能运行,体现了 Java 的跨平台特性。但仅仅依靠解释器逐行解释字节码效率较低,因此 JVM 引入了 JIT(Just-In-Time)即时编译 技术,它会在程序运行时动态地将热点代码编译成机器码,以提高执行效率。
关键点:编译与解释的结合
编译阶段:Java 程序首先被编译成字节码(
.class
文件)。这个过程类似于编译型语言(如 C、C++),在编译时生成与平台无关的中间代码。解释执行阶段:字节码文件通过 JVM 解释器逐行执行,类似于解释型语言(如 Python、JavaScript),但这种解释执行是通过一个中间层——JVM,能够跨平台执行。
JIT 编译:JVM 在运行时使用 JIT 编译器对热点代码进行优化,将其编译为机器码,避免了每次都通过解释器逐行执行,从而提高了效率。
因此,Java 语言既继承了编译型语言的高效性(通过字节码和 JIT 编译),又保持了解释型语言的可移植性(字节码可在任何支持 JVM 的平台上运行),这就是为什么 Java 被称为“编译与解释并存”的语言。
AOT 有什么优点?为什么不全部使用 AOT 呢?
AOT(Ahead-Of-Time)编译是一种在程序运行之前将源代码或字节码编译成机器码的技术。与JIT(Just-In-Time)编译不同,JIT是在程序运行时动态编译字节码,而AOT则在程序执行前完成编译。
AOT 的优点
启动时间快:
AOT 编译将 Java 程序在执行之前提前编译成机器码,避免了 JIT 编译时需要的预热过程。因此,使用 AOT 的 Java 程序启动时更加迅速,因为机器码已经准备好,减少了启动时的延迟。内存占用低:
由于 AOT 编译不需要像 JIT 那样在运行时动态编译字节码,因此 AOT 编译后的应用占用的内存较少。这对于内存资源有限的环境(如微服务、容器化部署等)尤为重要。打包体积小:
AOT 编译后的代码是直接机器码,省去了包含字节码和 JIT 编译时所需的中间结构。这使得使用 AOT 编译的 Java 应用在打包时体积相对较小。增强的安全性:
AOT 编译后的机器码不容易被反编译和修改,减少了代码被破解的风险。因此,对于对安全性要求较高的场景(如云原生应用),AOT 提供了更好的保护。适合云原生场景:
云原生应用往往需要快速启动和低内存消耗,AOT 编译的 Java 应用符合这一需求,尤其是在容器化和微服务架构中。
为什么不全部使用 AOT?
尽管 AOT 有许多优点,但也存在一些限制,导致它无法完全取代 JIT:
动态特性不兼容:
Java 是一种动态语言,许多框架和库(如 Spring、CGLIB)依赖于 Java 的反射、动态代理、动态类加载和 JNI(Java Native Interface)等特性。这些特性依赖于在运行时动态生成和修改字节码,AOT 编译无法提前处理这些动态行为。因此,如果只使用 AOT 编译,无法支持这些特性,或者需要针对性地进行适配和优化。举个例子,CGLIB 动态代理使用 ASM 技术,在运行时直接生成并加载修改后的字节码。如果将整个应用编译为 AOT,那么这类基于动态字节码生成的技术就无法使用。
JIT 提供更高的执行优化:
JIT 编译在程序运行时分析代码执行情况,特别是针对热点代码进行优化,能够动态地生成针对特定环境优化的机器码,从而提高执行效率。对于长时间运行的程序,JIT 的动态优化能够提供更高的执行性能。而 AOT 编译只能在编译时进行优化,无法适应运行时的变化。缺少运行时的灵活性:
JIT 编译能够根据应用的运行时环境和行为动态生成机器码,因此能够对程序的执行路径进行优化。AOT 编译则是在编译时固定了所有的优化策略,缺乏运行时的灵活性和适应性。对一些高级优化支持较弱:
JIT 编译器通常会进行一些深层次的优化,如方法内联、循环展开、逃逸分析等。这些优化能显著提升程序的执行性能,而 AOT 编译在这方面的支持相对较弱,尤其是在处理复杂的优化场景时,可能无法达到 JIT 的效果。
总结
AOT 编译的主要优点在于快速启动、低内存占用和增强的安全性,特别适合云原生和微服务场景。然而,它也存在一些局限,特别是在支持动态特性(如反射、动态代理)和高性能优化(如 JIT 的运行时优化)方面的不足。因此,AOT 和 JIT 各有优势,在实际应用中需要根据场景和需求做出选择,通常是两者结合使用,以平衡启动速度、内存占用和运行性能。
Oracle JDK vs OpenJDK
1. 开源与闭源
- OpenJDK 是完全开源的,符合 GPL v2 协议,可以自由修改和分发。
- Oracle JDK 基于 OpenJDK,但并不是完全开源,Oracle JDK 的一些特性(如 Java Flight Recorder、Java Mission Control)是闭源的。
2. 许可证和费用
- OpenJDK:完全免费,可以自由使用和分发,适合所有使用场景。
- Oracle JDK:虽然 Oracle JDK 在 JDK 8u221 及之前的版本是可以长期免费的,但从 JDK 17 开始,Oracle JDK 提供免费的版本仅限于 3 年,3 年后需要付费商业支持。对于生产环境的商业使用,Oracle JDK 的更新和维护会有费用。
3. 功能性差异
- Oracle JDK 提供了一些 OpenJDK 没有的功能,例如:
- Java Flight Recorder (JFR):一种用于采集 JVM 性能数据的工具。
- Java Mission Control (JMC):用于分析 JFR 收集到的数据的工具。
- OpenJDK:这些工具在 OpenJDK 中并没有,直到 Java 11,Oracle 将这些工具捐赠给了开源社区。Java 11 之后,Oracle JDK 和 OpenJDK 基本功能一致。
4. 稳定性和长期支持(LTS)
- Oracle JDK:会发布长期支持(LTS)版本,通常每 3 年发布一次 LTS,提供长期的更新和安全补丁。
- OpenJDK:没有官方的 LTS 支持,但许多公司(如 Amazon、Alibaba)基于 OpenJDK 提供了长期支持的发行版,例如 Amazon Corretto 和 Alibaba Dragonwell,它们与 Oracle JDK 的 LTS 版本相对应。
5. 更新频率
- OpenJDK:更新更频繁,通常每 3 个月发布一次新版本。
- Oracle JDK:每 6 个月发布一次新版本。Oracle 会先在 OpenJDK 中进行实验,解决问题后再将这些改进应用到 Oracle JDK 中。
6. 协议
- OpenJDK:使用 GPL v2 协议,这意味着你可以修改和重新分发源码。
- Oracle JDK:使用 BCL(Binary Code License)协议或 OTN 协议,限制了使用的方式,特别是商用时,可能需要支付费用。
7. 为什么选择 OpenJDK?or为什么选择 Oracle JDK?
选择 OpenJDK
- 开源和免费:OpenJDK 作为完全开源且免费的解决方案,是许多开发者和公司首选的 JDK。
- 更高的定制性:OpenJDK 可以根据需要进行修改和优化,特别是一些企业或定制版本,如 Amazon Corretto 和 Alibaba Dragonwell。
- 更新频繁:OpenJDK 的更新周期较短,且支持更快的 bug 修复和新特性。
选择 Oracle JDK
- 企业级支持:如果需要获得商业支持、长期更新、安全补丁,或者使用 Oracle 提供的额外工具(如 Java Flight Recorder 和 Java Mission Control),Oracle JDK 是一个合适的选择。
- 稳定性保障:尽管 OpenJDK 已经是一个稳定的开源版本,但 Oracle JDK 提供了更多的商业支持和保障,适用于大规模企业应用。
总结
- 如果是个人开发者或中小型企业,且希望避免额外的费用,OpenJDK 或者基于 OpenJDK 的其他发行版(如 Amazon Corretto 或 Alibaba Dragonwell)是一个理想选择。
- 如果需要商业支持、长期安全更新、并且需要 Oracle JDK 提供的特定工具和功能,那么 Oracle JDK 会更适合你,尤其在企业级应用中。
拓展阅读
- BCL 协议:Oracle JDK 使用的协议,允许商用但不能修改。
- OTN 协议:Oracle 新的协议,适用于 JDK 11 及之后版本,允许私下使用,但商用需要付费。
Java 和 C++ 的主要区别
Java 和 C++ 都是广泛使用的面向对象编程语言,但它们在设计理念、内存管理、功能特性等方面有很大的不同。以下是一些常见的区别:
1. 内存管理
- Java:Java 具有自动内存管理机制,使用垃圾回收(GC)自动清理不再使用的对象,程序员无需手动释放内存。这样减少了内存泄漏和悬挂指针的风险。
- C++:C++ 需要程序员手动管理内存,通过
new
和delete
来分配和释放内存,容易出现内存泄漏和悬挂指针问题。
2. 指针
- Java:Java 不提供指针,因此程序员无法直接操作内存地址。这使得 Java 更加安全,因为它避免了野指针和缓冲区溢出的风险。
- C++:C++ 提供了指针,可以直接访问和操作内存地址。指针是 C++ 强大的特性之一,但也可能导致内存管理方面的错误。
3. 继承机制
- Java:Java 采用单继承机制,每个类只能继承一个父类,但可以实现多个接口,支持接口的多重继承。
- C++:C++ 支持多重继承,一个类可以继承多个父类,这种灵活性虽然强大,但也容易引发“菱形继承”问题,需要使用虚拟继承来解决。
4. 方法重载与操作符重载
- Java:Java 只支持方法重载,即一个类中可以定义多个相同名称但参数不同的方法。Java 不支持操作符重载,因为它增加了语言的复杂性,并且与 Java 的设计哲学不符。
- C++:C++ 支持方法重载和操作符重载,程序员可以根据需要自定义操作符行为(例如,重载
+
操作符用于类的对象)。
5. 多线程处理
- Java:Java 从语言层面支持多线程,通过
Thread
类和Runnable
接口提供了简单的并发编程支持,并且有内置的线程安全机制,如synchronized
关键字。 - C++:C++11 开始支持多线程,通过
std::thread
提供多线程支持,但相比于 Java,C++ 的多线程支持相对较低级,需要程序员手动管理线程同步和数据共享等问题。
6. 内存模型
- Java:Java 运行在 Java 虚拟机(JVM)上,程序的执行环境与底层硬件解耦,程序代码编译为字节码,JVM 负责将字节码转换为本地机器码执行,具备较好的平台独立性。
- C++:C++ 程序直接编译为机器码,运行时依赖于具体的操作系统和硬件平台,因此程序需要重新编译以适配不同平台。
7. 异常处理
- Java:Java 提供了强制性的异常处理机制,所有的异常都需要被显式捕获或声明抛出,采用
try-catch-finally
块来捕获和处理异常。 - C++:C++ 也支持异常处理,但不像 Java 那样强制要求,异常处理的使用较为灵活。C++ 中的异常处理是可选的,程序员可以选择不捕获某些异常。
8. 标准库
- Java:Java 拥有一个丰富的标准库(JDK),提供了广泛的 API 用于网络编程、数据库连接、文件 I/O、并发编程等。
- C++:C++ 标准库包含了 STL(标准模板库),提供了数据结构(如
vector
、map
)和算法(如排序、查找等),但在一些领域(如网络编程)不如 Java 的标准库强大。
9. 垃圾回收(GC)
- Java:Java 内置垃圾回收机制(GC),可以自动回收不再使用的对象,减少了内存泄漏的风险,但也带来了一些性能开销。
- C++:C++ 没有内置的垃圾回收机制,程序员需要手动管理内存。虽然有一些第三方库可以实现垃圾回收(如 Boost 或自定义实现),但标准 C++ 不支持垃圾回收。
10. 跨平台性
- Java:Java 提供了极好的跨平台性,代码编译为字节码后,可以在任何支持 JVM 的操作系统上运行,遵循“一次编写,处处运行”的原则。
- C++:C++ 由于直接编译为机器码,程序必须为每个平台单独编译,因此需要针对不同平台进行移植。
11. 执行速度
- Java:虽然 Java 有 JIT 编译优化,但由于 Java 代码需要通过 JVM 执行,执行效率通常低于 C++(尤其是在没有经过 JIT 优化时)。
- C++:由于 C++ 代码直接编译为机器码,执行效率通常更高,尤其是在需要高性能的场景中,C++ 更具优势。
12. 开发效率
- Java:Java 提供了很多内置的功能,如自动内存管理和线程管理,开发者可以更加专注于业务逻辑,开发效率较高。
- C++:C++ 由于涉及手动内存管理、多重继承和指针等复杂特性,开发效率较低,程序员需要更多地考虑底层实现和性能优化。
总结
Java 和 C++ 都是强大的编程语言,适用于不同的场景:
- Java 更加注重开发效率和跨平台性,适合用于企业级应用、Web 开发、移动应用(通过 Android)、大数据处理等领域。
- C++ 则更加适合性能要求较高的场景,如系统开发、游戏引擎、嵌入式编程、实时计算等。
二、Java基本语法
单行注释和多行注释
在 Java 中,注释主要有三种形式,它们各自有不同的使用场景和功能:
1. 单行注释(Single-line Comments)
格式:
//
后跟注释内容用途:用于对代码中的一行或一段代码进行简短说明,通常用于解释某个特定操作或标记。
示例:
// 计算总价 int totalPrice = price * quantity;
2. 多行注释(Multi-line Comments)
格式:
/*
开始,*/
结束,可以跨多行用途:用于对多行代码进行注释,适合较长的解释或注释,尤其是当需要注释掉一大块代码时。
示例:
/* * 这是一个多行注释 * 用于解释一段较长的代码逻辑 * 或者临时禁用某段代码 */ int totalPrice = price * quantity;
3. 文档注释(Documentation Comments)
格式:
/**
开始,*/
结束,通常用于类、方法、构造函数的描述用途:用于生成 Java API 文档,描述类、方法、字段等的功能、使用方法和参数等。通过 Java 的
javadoc
工具,可以从这些注释中生成 HTML 格式的文档。示例:
/** * 计算两数之和 * * @param num1 第一个数字 * @param num2 第二个数字 * @return 两数之和 */ public int add(int num1, int num2) { return num1 + num2; }
注释的最佳实践
- 简洁而明确:好的代码应该尽量避免过多的注释,因为代码本身应该具有足够的可读性。注释应该用来解释复杂的逻辑或提供上下文信息,而不是简单地描述每一行代码。
- 避免注释废话:避免过多的无意义注释。例如,不需要注释
int a = 0; // 初始化变量
,因为变量名和操作已经很清楚。 - 文档注释的应用:对于公共 API、复杂的算法或函数,使用文档注释是非常重要的。通过 Javadoc 生成的 API 文档可以帮助团队成员、使用者了解代码的功能。
- 注释与代码同步:确保注释与代码保持同步。过时的注释会误导其他开发人员,因此每当修改代码时,及时更新注释。
总的来说,良好的注释习惯能够提高代码的可读性和可维护性,但要避免过度注释,注重代码的自说明性。
标识符和关键字的区别是什么?
在编程语言中,标识符和关键字是两个重要的概念,它们之间有着本质的区别:
1. 标识符(Identifier)
定义:标识符是程序中用来标识变量、函数、类、方法等元素的名字。它是开发者用来命名不同程序元素的符号。
特点:
- 可以是字母、数字、下划线 (
_
) 或美元符号 ($
) 的组合。 - 不能以数字开头。
- 区分大小写(例如
myVar
和myvar
是两个不同的标识符)。 - 可以根据需要自由定义,但必须遵循命名规则。
- 可以是字母、数字、下划线 (
用途:标识符用来命名程序中的各种实体,如类名、变量名、方法名、常量等。
示例:
int totalPrice; // 'totalPrice' 是一个标识符 String productName; // 'productName' 也是一个标识符
2. 关键字(Keyword)
定义:关键字是 Java 语言中已经被赋予特定意义的保留字,不能用作标识符。它们是 Java 语言语法的一部分,用来定义结构、控制流程等。
特点:
- 关键字有固定的含义,Java 语言解析器根据这些关键字来执行相应的操作。
- 不能用作类、变量、方法名等标识符。
- Java 中的关键字是固定的,不能被修改或重定义。
用途:关键字用于定义语法结构和控制程序流等。
示例:
int x = 10; // 'int' 是关键字,用于声明变量类型 if (x > 5) { // 'if' 是关键字,用于条件判断 System.out.println("x is greater than 5"); }
关键字的举例
Java 中的常见关键字包括:
- 数据类型:
int
,float
,char
,boolean
,long
,double
,short
,byte
- 控制结构:
if
,else
,switch
,case
,for
,while
,do
,break
,continue
,return
- 类和对象:
class
,interface
,extends
,implements
,new
- 访问控制:
public
,private
,protected
,default
- 异常处理:
try
,catch
,finally
,throw
,throws
- 其他:
static
,final
,super
,this
,null
,true
,false
,package
,import
总结
- 标识符是程序中自定义的名字,用于标识不同的程序元素。
- 关键字是编程语言内置的保留字,具有特殊意义,无法用于自定义标识符。
比喻
就像你给自己的商店起名字时,你可以自由选择名字(标识符),但如果你想取个名字叫“政府”,那是不行的,因为“政府”这个名字已经有了固定的含义(关键字)。
Java 语言关键字有哪些?
Java 语言的关键字有很多,按照不同的分类,它们的用途和功能各有不同。以下是完整的分类和每个类别下的关键字:
1. 访问控制
private
protected
public
2. 类、方法和变量修饰符
abstract
class
extends
final
implements
interface
native
new
static
strictfp
synchronized
transient
volatile
enum
3. 程序控制
break
continue
return
do
while
if
else
for
instanceof
switch
case
default
assert
4. 错误处理
try
catch
throw
throws
finally
5. 包相关
import
package
6. 基本类型
boolean
byte
char
double
float
int
long
short
7. 变量引用
super
this
void
8. 保留字
goto
(保留但未使用)const
(保留但未使用)
特别说明
true
,false
, 和null
虽然看起来像关键字,但它们实际上是字面量(literal),并且不能作为标识符使用。default
在 Java 中有多个用途:- 在程序控制中,作为
switch
语句的默认分支。 - 从 JDK 8 开始,作为接口中默认方法的修饰符。
- 在类、方法和变量修饰符中,
default
表示默认的访问级别。
- 在程序控制中,作为
注意
- 所有关键字都是小写的,通常在 IDE 中会以不同的颜色突出显示。
goto
和const
是保留字,但并未在 Java 中使用,可以放心地忽略它们。
更多详细内容可以参考官方文档:Java 关键字。
自增自减运算符
自增自减运算符解析
在 Java 中,++
和 --
运算符用于对变量进行递增或递减。它们有两种形式:前缀形式和后缀形式。
前缀形式
++a
:先自增a
,然后返回自增后的值。--a
:先自减a
,然后返回自减后的值。
后缀形式
a++
:先返回a
的当前值,然后再自增a
。a--
:先返回a
的当前值,然后再自减a
。
示例代码分析
int a = 9; // 初始化 a 为 9
int b = a++; // b = 9 (先将 a 的当前值赋给 b,再将 a 增加 1)
int c = ++a; // c = 11 (先将 a 增加 1,再将增加后的值赋给 c)
int d = c--; // d = 10 (先将 c 的当前值赋给 d,再将 c 减少 1)
int e = --d; // e = 9 (先将 d 减少 1,再将减少后的值赋给 e)
执行结果
a = 11
:因为a++
是后缀形式,先给b
赋值为9
,然后a
增加到10
。接着++a
是前缀形式,a
先加 1 变为11
,然后赋值给c
。b = 9
:在执行b = a++
时,a
先赋值给b
,然后a
增加 1。c = 10
:执行int c = ++a;
时,a
在前缀形式中先加 1,然后赋值给c
。d = 10
:执行int d = c--;
时,d
先赋值为c
的当前值(10),然后c
自减。e = 10
:执行int e = --d;
时,d
先自减(变为 9),然后赋值给e
。
因此,最终的值是:
a = 11
b = 9
c = 10
d = 10
e = 10
总结
- 前缀形式
++a
/--a
:先增加/减少变量,再使用其值。 - 后缀形式
a++
/a--
:先使用变量的值,再增加/减少它。
移位运算符详解
移位运算符是 Java 中非常基础且高效的运算符,主要用于对整数类型数据的二进制位进行操作。理解移位运算符的作用和特性对于编写高效代码至关重要,尤其是在底层操作和优化时,移位运算符发挥着重要作用。
移位运算符种类
<<
左移运算符- 向左移动二进制位,低位补零,高位丢弃。
- 等价于数值乘以 2 的 n 次方(不溢出的情况下)。
- 例如:
x << n
等同于x * 2^n
。
>>
带符号右移运算符- 向右移动二进制位,符号位补充原符号位(正数补 0,负数补 1),低位丢弃。
- 等价于数值除以 2 的 n 次方(考虑符号位)。
- 例如:
x >> n
等同于x / 2^n
。
>>>
无符号右移运算符- 向右移动二进制位,忽略符号位,空位都补 0。
- 无符号右移不考虑数值的符号位,适用于对无符号数的处理。
- 例如:
x >>> n
等同于无符号除以 2 的 n 次方。
使用场景
- 快速乘除以 2 的幂次方:利用左移和右移可以非常高效地进行乘法和除法运算,尤其在性能要求较高的情况下,移位运算符比普通的乘法和除法要快得多。
- 位字段管理:通过移位,可以高效地操作多个布尔标志位,在内存中压缩存储。
- 哈希算法和加密:许多哈希算法(如
HashMap
的hash
方法)和加密算法使用移位操作来混淆数据,提升安全性。 - 数据压缩与校验:例如 CRC 校验中使用移位操作来生成校验值。
示例代码解析
左移运算符示例
int i = -1;
System.out.println("初始数据:" + i);
System.out.println("初始数据对应的二进制字符串:" + Integer.toBinaryString(i));
i <<= 10;
System.out.println("左移 10 位后的数据 " + i);
System.out.println("左移 10 位后的数据对应的二进制字符串 " + Integer.toBinaryString(i));
输出结果:
初始数据:-1
初始数据对应的二进制字符串:11111111111111111111111111111111
左移 10 位后的数据 -1024
左移 10 位后的数据对应的二进制字符串 11111111111111111111110000000000
- 解释:
i = -1
时,二进制表示为 32 个 1(11111111111111111111111111111111
)。- 当左移 10 位时,符号位保持为 1,其它部分向左移动,末尾补 0,最终变为
11111111111111111111110000000000
,对应的十进制值为-1024
。
位数超限时的移位
Java 对移位运算数值的位数有规定,移位操作时,如果位数超过了数据类型所能表示的最大位数,会自动进行求余操作。
- 对于
int
类型(32 位):移位次数超过 32 位时,会执行n % 32
的操作。例如,i << 42
等同于i << 10
。 - 对于
long
类型(64 位):移位次数超过 64 位时,会执行n % 64
的操作。
例如,执行如下代码:
int i = -1;
System.out.println("初始数据:" + i);
System.out.println("初始数据对应的二进制字符串:" + Integer.toBinaryString(i));
i <<= 42; // 42 % 32 = 10
System.out.println("左移 42 位后的数据 " + i);
System.out.println("左移 42 位后的数据对应的二进制字符串 " + Integer.toBinaryString(i));
输出与前一个例子相同,因为 42 % 32 = 10:
初始数据:-1
初始数据对应的二进制字符串:11111111111111111111111111111111
左移 42 位后的数据 -1024
左移 42 位后的数据对应的二进制字符串 11111111111111111111110000000000
总结
<<
左移运算符:每次左移一位,相当于乘以 2。>>
带符号右移运算符:每次右移一位,相当于除以 2(保留符号位)。>>>
无符号右移运算符:每次右移一位,相当于无符号除以 2,忽略符号位。
通过移位操作,可以高效进行乘除以 2 的幂次方的计算,常用于性能优化、哈希算法、数据压缩等领域。
continue
、break
和 return
的区别
在控制结构中,continue
、break
和 return
是用来控制程序流向的关键字,它们可以在循环或方法中实现不同的行为:
continue
:跳过当前循环的剩余部分,直接进入下一次循环。适用场景:当某个条件满足时,跳过当前循环的其余语句,继续下一轮循环。
使用示例:
for (int i = 0; i < 5; i++) { if (i == 2) { continue; // 跳过 i = 2 时的输出 } System.out.println(i); }
输出:
0 1 3 4
break
:终止当前循环,跳出循环体,执行循环之后的语句。适用场景:当某个条件满足时,结束循环并继续执行后续代码。
使用示例:
for (int i = 0; i < 5; i++) { if (i == 3) { break; // 当 i == 3 时,退出循环 } System.out.println(i); }
输出:
0 1 2
return
:结束方法的执行,并可选择性地返回一个值。适用场景:用来提前退出方法,不管是否在方法的最后一行。
使用示例:
public static void printMessage(int number) { if (number < 0) { return; // 直接退出方法,不再执行下面的代码 } System.out.println("Number is " + number); }
调用
printMessage(-1)
将直接退出方法,而不会输出任何内容。
分析给定代码的执行过程
public static void main(String[] args) {
boolean flag = false;
for (int i = 0; i <= 3; i++) {
if (i == 0) {
System.out.println("0");
} else if (i == 1) {
System.out.println("1");
continue; // 跳过当前迭代,进入下一次循环
} else if (i == 2) {
System.out.println("2");
flag = true; // 设置 flag 为 true
} else if (i == 3) {
System.out.println("3");
break; // 跳出循环
} else if (i == 4) {
System.out.println("4");
}
System.out.println("xixi");
}
if (flag) {
System.out.println("haha");
return; // 结束方法执行
}
System.out.println("heihei"); // 如果 flag 为 false,则执行
}
代码执行过程
第一轮循环 (
i = 0
):- 条件
i == 0
成立,输出"0"
。 - 输出
"xixi"
。
- 条件
第二轮循环 (
i = 1
):- 条件
i == 1
成立,输出"1"
。 - 执行
continue
,跳过当前迭代,直接进入下一轮循环。 - 输出
"xixi"
。
- 条件
第三轮循环 (
i = 2
):- 条件
i == 2
成立,输出"2"
。 - 设置
flag = true
。 - 输出
"xixi"
。
- 条件
第四轮循环 (
i = 3
):- 条件
i == 3
成立,输出"3"
。 - 执行
break
,跳出循环。
- 条件
方法结束后的判断:
flag
为true
,输出"haha"
。- 执行
return
,方法结束,不再继续执行后面的代码。
输出结果
0
xixi
1
xixi
2
xixi
3
haha
总结
continue
跳过当前循环的剩余部分,进入下一轮循环。break
跳出整个循环体,继续执行循环外的语句。return
结束当前方法的执行,并可选择性地返回一个值。
在本例中,continue
使得 i == 1
时跳过了后续的 println
,而 break
使得 i == 3
时退出了循环。return
使得方法在 flag
为 true
时提前结束,跳过了 System.out.println("heihei");
的执行。
三、基本数据类型
Java 中的几种基本数据类型
Java 提供了 8 种基本数据类型,它们可以分为数字类型、字符类型和布尔类型:
数字类型
整数类型
byte
:占 8 位(1 字节),取值范围为-128 ~ 127
。short
:占 16 位(2 字节),取值范围为-32768 ~ 32767
。int
:占 32 位(4 字节),取值范围为-2147483648 ~ 2147483647
。long
:占 64 位(8 字节),取值范围为-9223372036854775808 ~ 9223372036854775807
。
浮点类型
float
:占 32 位(4 字节),表示单精度浮点数,取值范围为1.4E-45 ~ 3.4028235E38
。double
:占 64 位(8 字节),表示双精度浮点数,取值范围为4.9E-324 ~ 1.7976931348623157E308
。
字符类型
char
:占 16 位(2 字节),表示一个字符,取值范围为0 ~ 65535
(可以表示 Unicode 字符)。
布尔类型
boolean
:表示真或假,取值为true
或false
。占用的存储空间通常依赖于 JVM 实现,理论上是 1 位,但实际存储可能会占用更多位以适应内存对齐。
基本数据类型的默认值
基本类型 | 位数 | 字节 | 默认值 | 取值范围 |
---|---|---|---|---|
byte | 8 | 1 | 0 | -128 ~ 127 |
short | 16 | 2 | 0 | -32768 ~ 32767 |
int | 32 | 4 | 0 | -2147483648 ~ 2147483647 |
long | 64 | 8 | 0L | -9223372036854775808 ~ 9223372036854775807 |
char | 16 | 2 | 'u0000' | 0 ~ 65535 |
float | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 |
double | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 |
boolean | 1 | - | false | true、false |
基本数据类型的细节
- 符号位的影响:在整数类型中,由于采用二进制补码表示法,最高位表示符号(0 为正,1 为负),因此能表示的最大正整数比最大负整数少 1。例如,
int
类型的最大值为2147483647
,而最小值为-2147483648
。 long
和float
需要加后缀:当使用long
类型时,数值后必须加上L
或l
,而float
类型的数值后必须加上f
或F
。如果不加后缀,编译器会把数值当作int
或double
类型来处理。
基本类型和包装类型的区别?
用途
基本类型用于存储原始数据。
包装类型可用于泛型、集合类等需要对象的场景。
存储方式
基本类型的局部变量存放在栈中,成员变量存放在堆中。
包装类型的对象存放在堆中。
占用空间
基本类型占用的内存空间较小。
包装类型占用的内存空间较大,因包含对象头信息。
默认值
基本类型的默认值为对应类型的默认值(如 0
、false
)。
包装类型的默认值为 null
。
比较方式
基本类型使用 ==
比较值。
包装类型使用 ==
比较引用地址,使用 equals()
比较值。
关于存储位置
基本类型存放位置依赖于作用域:
局部变量存放在栈中。
成员变量存放在堆中。
包装类型是对象,存放在堆中。
示例代码
public class Test {
// 成员变量,存放在堆中
int a = 10;
// 被 static 修饰的成员变量,JDK 1.7 及之前位于方法区,1.8 后存放于元空间
static int b = 20;
public void method() {
// 局部变量,存放在栈中
int c = 30;
// 编译错误,不能在方法中使用 static 修饰局部变量
// static int d = 40;
}
}
包装类型的缓存机制了解么?
Java 基本数据类型的包装类型大部分都用到了缓存机制来提升性能。
Byte
、Short
、Integer
、Long
这四种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
或 False
。
Integer 缓存源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static {
// high value may be configured by property
int h = 127;
}
}
Character
缓存源码:
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
private static class CharacterCache {
private CharacterCache(){}
static final Character cache[] = new Character[127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Character((char)i);
}
}
Boolean
缓存源码:
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
两种浮点数类型的包装类 Float
、Double
并没有实现缓存机制。
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2); // 输出 true
Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22); // 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4); // 输出 false
示例分析
下面我们来看一个问题:下面的代码的输出结果是 true
还是 false
呢?
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1 == i2);
Integer i1 = 40
这一行代码会发生装箱,也就是说这行代码等价于 Integer i1 = Integer.valueOf(40)
。因此,i1
直接使用的是缓存中的对象。而 Integer i2 = new Integer(40)
会直接创建新的对象。
因此,答案是 false
。
记住
所有整型包装类对象之间值的比较,全部使用 equals()
方法比较。
自动装箱与拆箱了解吗?原理是什么?
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
举例:
Integer i = 10; // 装箱
int n = i; // 拆箱
上面这两行代码对应的字节码为:
L1
LINENUMBER 8 L1
ALOAD 0
BIPUSH 10
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;
L2
LINENUMBER 9 L2
ALOAD 0
ALOAD 0
GETFIELD AutoBoxTest.i : Ljava/lang/Integer;
INVOKEVIRTUAL java/lang/Integer.intValue ()I
PUTFIELD AutoBoxTest.n : I
RETURN
从字节码中,我们发现:
- 装箱其实就是调用了包装类的
valueOf()
方法; - 拆箱其实就是调用了
xxxValue()
方法。
因此,
Integer i = 10
等价于Integer i = Integer.valueOf(10)
;int n = i
等价于int n = i.intValue()
。
性能影响
注意:如果频繁拆装箱,可能会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
private static long sum() {
// 应该使用 long 而不是 Long
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
为什么浮点数运算的时候会有精度丢失的风险?
浮点数运算精度丢失代码演示:
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.printf("%.9f", a); // 0.100000024
System.out.println(b); // 0.099999905
System.out.println(a == b); // false
为什么会出现这个问题呢?
这与计算机存储浮点数的方式有关。计算机内部使用二进制表示数字,而浮点数是通过近似的方式存储的。由于计算机内存的宽度是有限的,无法完全精确表示某些小数,尤其是无限循环的小数(如 1/3、0.2 等)。在这种情况下,计算机会截断浮点数,这就导致了精度丢失。
例如,十进制下的 0.2
无法精确转换为二进制小数。转换过程如下:
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0 (发生循环)
这种循环的产生会导致计算机存储时只保留有限的精度,从而出现浮点数精度丢失的情况。
如何解决浮点数运算的精度丢失问题?
浮点数运算精度丢失问题通常出现在对小数的存储和运算上,特别是涉及到货币、财务计算等精度要求高的场景。为了解决这个问题,我们可以使用 BigDecimal
类,它提供了高精度的数值运算能力,并且避免了传统浮点数类型(float
和 double
)的精度丢失问题。
为什么 BigDecimal
能解决精度丢失问题?
BigDecimal
是一种任意精度的数值类型,可以表示任意精度的数字,并且通过任意精度运算来避免舍入误差。它在内部使用字符数组来存储数值,而不是二进制浮点表示,这使得它能够精确表示十进制的小数。
示例分析
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(c); // x = 0.2
BigDecimal y = b.subtract(c); // y = 0.20
System.out.println(x); // 0.2
System.out.println(y); // 0.20
在这个示例中,BigDecimal
对两个小数 1.0
和 1.00
进行了精确计算,结果为 0.2
和 0.20
,并且保留了精度,避免了浮点数类型中常见的精度丢失问题。
如何比较两个 BigDecimal
对象
BigDecimal
对象的比较不能使用 ==
操作符,而是应该使用 compareTo()
方法。compareTo()
会比较两个 BigDecimal
的值,返回一个整数:
0
:两个值相等;负数
:当前对象小于参数对象;正数
:当前对象大于参数对象。
System.out.println(Objects.equals(x, y)); // false
System.out.println(0 == x.compareTo(y)); // true
性能考量
虽然 BigDecimal
能够避免浮点数精度丢失问题,但它的运算性能相对较差,因为它是通过高精度的字符数组来存储和运算的,适用于需要高精度计算的场景。在对性能要求不高的情况(如简单计算)下,float
或 double
仍然是合适的选择,但对于金融和货币运算等高精度要求的应用,BigDecimal
是首选。
总结
使用 BigDecimal
可以确保在计算中避免浮点数精度丢失问题,特别是在涉及精确小数计算的场景下,如货币、金融计算等。
超过 long
整型的数据应该如何表示?
在 Java 中,long
类型是 64 位的整数类型,它的表示范围是从 -2^63
到 2^63-1
(即 Long.MIN_VALUE
到 Long.MAX_VALUE
)。如果数据超过了这个范围,long
类型会发生溢出,导致不可预期的结果。
long l = Long.MAX_VALUE;
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true
上面代码展示了 long
类型的溢出问题。当 l
超过 Long.MAX_VALUE
时,会从最小值 Long.MIN_VALUE
重新开始,这就导致了溢出。
使用 BigInteger
处理超大整数
如果需要处理超过 long
范围的整数,可以使用 BigInteger
类型。BigInteger
是 Java 提供的用于表示任意大小整数的类,能够处理超大范围的整数运算。
BigInteger
内部使用 int[]
数组来存储数字,并支持任意精度的整数计算。因此,无论数字多大,都能够存储并进行运算。
示例代码
import java.math.BigInteger;
BigInteger bigInt = new BigInteger("1234567890123456789012345678901234567890");
BigInteger bigInt2 = new BigInteger("9876543210987654321098765432109876543210");
BigInteger result = bigInt.add(bigInt2);
System.out.println(result); // 输出大于 long 类型范围的数字
性能考虑
虽然 BigInteger
提供了任意大小的整数表示,但其运算效率相对较低。BigInteger
的运算比基本数据类型的运算要慢,主要因为它使用了动态分配的数组来存储大数并进行运算。因此,在需要处理大整数时,应该权衡效率和精度。
总结来说,BigInteger
是处理超过 long
范围数据的解决方案,能够避免整数溢出,但在性能上相较于基本类型的整数运算会有所折扣。
四、变量
成员变量与局部变量的区别?
语法形式
- 成员变量:属于类或对象,可以使用访问修饰符(如
public
、private
)及static
修饰。成员变量可以在类中声明,并且可以在构造方法中、方法中或直接赋值。 - 局部变量:局部于方法或代码块,只在定义它们的块或方法内有效。局部变量不能使用访问修饰符或
static
修饰符。局部变量通常是方法参数或在方法内声明的临时变量。
存储方式
- 成员变量:存储在堆内存中(如果是实例变量),或者存储在方法区/元空间中(如果是
static
变量)。实例变量是对象的一部分,与对象的生命周期绑定。 - 局部变量:存储在栈内存中。每当方法调用时,局部变量会被创建,并在方法调用结束后销毁。
生存时间
- 成员变量:随着对象的生命周期存在,直到对象被垃圾回收。
- 局部变量:随着方法的调用而存在,方法执行完毕后销毁。
默认值
- 成员变量:如果未显式赋值,会根据类型自动赋予默认值(如
0
、false
、null
)。如果是final
变量,则必须显式赋值。 - 局部变量:不会自动赋值,必须在使用前显式赋值,否则编译器会报错。
为什么成员变量有默认值?
防止使用未初始化的变量:成员变量的默认值保证了对象的完整性。如果没有默认值,未初始化的变量会存储随机内存值,可能导致程序出错。
默认值提供了容错机制:成员变量在实例化时可能被动态初始化,使用默认值能够避免误报并提供一致性。
局部变量无默认值的原因:局部变量通常可以在编译时检测到其是否被初始化,未初始化的局部变量直接报错能够提前捕捉到问题。
示例代码
public class VariableExample {
// 成员变量
private String name;
private int age;
// 方法中的局部变量
public void method() {
int num1 = 10; // 栈中分配的局部变量
String str = "Hello, world!"; // 栈中分配的局部变量
System.out.println(num1);
System.out.println(str);
}
// 带参数的方法中的局部变量
public void method2(int num2) {
int sum = num2 + 10; // 栈中分配的局部变量
System.out.println(sum);
}
// 构造方法中的局部变量
public VariableExample(String name, int age) {
this.name = name; // 对成员变量进行赋值
this.age = age; // 对成员变量进行赋值
int num3 = 20; // 栈中分配的局部变量
String str2 = "Hello, " + this.name + "!"; // 栈中分配的局部变量
System.out.println(num3);
System.out.println(str2);
}
}
总结
- 成员变量:属于类或实例,可以有默认值,存储在堆内存中,生存时间与对象相关,通常需要在对象构造时进行初始化。
- 局部变量:属于方法或代码块,仅在方法调用期间存在,存储在栈内存中,必须显式初始化。
静态变量有什么作用?
静态变量是通过 static
关键字修饰的变量,它的主要作用包括:
共享性
静态变量属于类本身,而不是某个特定的对象。所有类的实例共享同一个静态变量,不论创建多少个对象,静态变量的内存空间只有一份。
内存节省
由于静态变量只会被分配一次内存,无论对象创建多少次,静态变量始终指向相同的内存地址,这样可以避免重复的内存分配,节省内存空间。
访问方式
静态变量通常通过类名访问,例如 ClassName.staticVariable
,也可以通过类的实例访问(不推荐这样做)。如果静态变量被 private
修饰,则只能通过该类内部的代码来访问。
示例代码
public class StaticVariableExample {
// 静态变量
public static int staticVar = 0;
}
public class Main {
public static void main(String[] args) {
// 通过类名访问静态变量
StaticVariableExample.staticVar = 5;
System.out.println(StaticVariableExample.staticVar); // 输出 5
}
}
常量
静态变量通常会与 final
关键字结合,作为常量使用。常量在程序中值不变,通常是全局共享的配置或数据。
public class ConstantVariableExample {
// 常量
public static final int constantVar = 100;
}
总结
- 共享性:静态变量对所有实例是共享的。
- 内存节省:静态变量只分配一次内存。
- 访问方式:通常通过类名来访问。
- 常量定义:静态变量常常与
final
配合,作为常量使用。
字符型常量和字符串常量的区别?
形式
- 字符常量:由单引号包裹,例如
'A'
。 - 字符串常量:由双引号包裹,可以包含零个或多个字符,例如
"Hello"
。
含义
- 字符常量:代表一个单一的字符,实际上是该字符的 ASCII 或 Unicode 值。可以作为数字参与运算。
- 字符串常量:表示一个字符串,实际上是指向该字符串内存地址的引用。
占用内存大小
- 字符常量:占用 2 个字节(因为
char
类型在 Java 中使用 UTF-16 编码,占 2 字节)。 - 字符串常量:占用的内存大小取决于字符串的长度,每个字符占 2 个字节(由于 UTF-16 编码)。
示例代码
public class StringExample {
// 字符型常量
public static final char LETTER_A = 'A';
// 字符串常量
public static final String GREETING_MESSAGE = "Hello, world!";
public static void main(String[] args) {
// 输出字符常量的字节数
System.out.println("字符型常量占用的字节数为:" + Character.BYTES);
// 输出字符串常量的字节数
System.out.println("字符串常量占用的字节数为:" + GREETING_MESSAGE.getBytes().length);
}
}
输出
字符型常量占用的字节数为:2
字符串常量占用的字节数为:13
总结
- 字符常量:由单个字符构成,表示该字符的数值,通常占用 2 字节。
- 字符串常量:由多个字符构成,表示一个字符串,内存占用依字符串长度而变化。
五、方法
什么是方法的返回值? 方法有哪几种类型?
方法的返回值
方法的返回值是指方法执行后产生的结果,它将通过 return
语句传递给方法的调用者。返回值允许我们在方法内进行某些操作后,把结果传递出去,以便在其他地方使用。例如,返回一个计算结果、状态信息等。
方法的类型
根据方法的返回值和参数的情况,可以将方法分为以下几种类型:
1、无参数无返回值的方法
这类方法没有接收参数,也没有返回值。方法主要用于执行某些操作,但不返回任何结果。
public void f1() {
// 执行一些操作
}
public void f(int a) {
if (...) {
// 结束方法的执行,下面的输出语句不会执行
return;
}
System.out.println(a);
}
2、有参数无返回值的方法
这类方法接收一个或多个参数,但不返回任何结果。它用于执行操作,而参数用来定制这些操作。
public void f2(int a, String b) {
// 执行操作
System.out.println(a + " " + b);
}
3、有返回值无参数的方法
这类方法不接收任何参数,但返回一个结果。返回值的类型可以是任何数据类型,例如 int
、String
等。
public int f3() {
int x = 10;
return x; // 返回结果
}
4、有返回值有参数的方法
这类方法既接收参数,又返回一个结果。通过输入参数进行操作并返回计算结果。
public int f4(int a, int b) {
return a * b; // 返回两个数的乘积
}
总结
- 无参数无返回值:没有输入参数和返回结果。
- 有参数无返回值:接收输入,但不返回结果。
- 有返回值无参数:不接收输入,返回一个结果。
- 有返回值有参数:接收输入,返回一个计算结果。
静态方法为什么不能调用非静态成员?
静态方法不能调用非静态成员的原因主要涉及 Java 类和对象的内存管理方式,以及它们在类加载过程中的生命周期。具体原因如下:
1. 静态方法属于类
静态方法是属于类本身的,而不是类的实例。它在类加载时就会被分配内存并且可以通过类名直接访问。因此,静态方法可以在没有创建类实例的情况下调用。
2. 非静态成员属于对象
非静态成员(如实例变量和实例方法)属于类的实例,只有在类的实例被创建后,非静态成员才会存在,并且只能通过该实例对象访问。因此,静态方法在没有创建对象实例的情况下无法访问非静态成员。
3. 静态方法和非静态成员生命周期不同
- 静态方法在类加载时即存在,不依赖于对象的创建。
- 非静态成员则在对象实例化时才会存在,且每个对象有一份独立的非静态成员。
因此,静态方法无法访问非静态成员,因为在静态方法调用时,类的实例可能尚未创建,非静态成员还不存在。
示例代码
public class Example {
// 非静态成员
private String instanceVariable = "Instance Variable";
// 静态方法
public static void staticMethod() {
// 不能直接访问非静态成员
// System.out.println(instanceVariable); // 编译错误
// 可以通过实例化对象访问非静态成员
Example obj = new Example();
System.out.println(obj.instanceVariable); // 正确访问
}
public static void main(String[] args) {
staticMethod(); // 调用静态方法
}
}
总结
静态方法不能直接访问非静态成员,因为它们的生命周期和访问方式不同:静态方法属于类,在类加载时就可以访问,而非静态成员属于实例,只有在对象实例化之后才能访问。因此,静态方法无法在没有实例的情况下访问非静态成员。
静态方法和实例方法有何不同?
静态方法和实例方法的区别主要体现在调用方式、成员访问限制、内存分配等方面。以下是详细的对比:
1. 调用方式
静态方法:可以通过
类名.方法名
的方式调用,也可以通过对象实例调用(不推荐)。静态方法属于类,不依赖于类的实例。因此,不需要创建对象即可调用静态方法。示例:
public class Person { public static void staticMethod() { System.out.println("Static Method"); } } public class Test { public static void main(String[] args) { // 使用类名调用静态方法 Person.staticMethod(); // 也可以使用对象调用静态方法(不推荐) Person person = new Person(); person.staticMethod(); } }
实例方法:只能通过类的实例来调用。需要先创建对象实例,然后使用该实例来调用实例方法。
示例:
public class Person { public void instanceMethod() { System.out.println("Instance Method"); } } public class Test { public static void main(String[] args) { // 通过对象实例调用实例方法 Person person = new Person(); person.instanceMethod(); } }
2. 访问类成员的限制
静态方法:只能访问静态成员(静态变量和静态方法)。它不能直接访问实例变量和实例方法,因为静态方法在类加载时就已经存在,而实例变量和实例方法只有在创建对象时才会存在。
示例:
public class Person { private String name = "John"; public static String species = "Human"; public static void staticMethod() { // 只能访问静态成员 System.out.println(species); // 正确 // System.out.println(name); // 错误:无法访问实例变量 } public void instanceMethod() { // 可以访问实例成员和静态成员 System.out.println(name); // 正确 System.out.println(species); // 正确 } }
实例方法:可以访问类的所有成员(静态成员和实例成员)。实例方法与对象实例关联,因此可以通过实例方法访问对象的实例变量和实例方法,也可以访问类的静态变量和静态方法。
3. 内存分配
- 静态方法:静态方法属于类的一部分,加载类时就被分配内存,因此所有实例共享同一份静态方法。
- 实例方法:实例方法属于对象实例,只有在创建对象时才会存在,每个对象有自己的实例方法。
4. 使用场景
- 静态方法:适用于不依赖于对象的行为。例如,工具类方法、工厂方法、单例模式等。
- 实例方法:适用于与对象状态相关的行为。通常需要访问实例变量或改变对象的状态。
示例总结
public class Example {
// 静态变量和方法
private static String staticVar = "Static Variable";
public static void staticMethod() {
System.out.println("Static Method");
// 只能访问静态变量
System.out.println(staticVar);
}
// 实例变量和方法
private String instanceVar = "Instance Variable";
public void instanceMethod() {
System.out.println("Instance Method");
// 可以访问实例变量和静态变量
System.out.println(instanceVar);
System.out.println(staticVar);
}
public static void main(String[] args) {
// 调用静态方法
Example.staticMethod();
// 调用实例方法
Example example = new Example();
example.instanceMethod();
}
}
输出:
Static Method
Static Variable
Instance Method
Instance Variable
Static Variable
重载 (Overloading) 和 重写 (Overriding) 的区别
1. 定义
- 重载 (Overloading):发生在同一类中,方法名相同,但参数列表(类型、个数、顺序)不同。重载方法可以有不同的返回类型和访问修饰符,关键点是方法签名的不同。
- 重写 (Overriding):发生在子类中,子类重新定义父类的方法。方法名、参数列表、返回类型都必须与父类方法完全相同,目的是改变父类方法的实现逻辑。
2. 发生的范围
- 重载:同一个类中,方法名相同,参数不同。
- 重写:子类重新定义父类的相同方法。
3. 参数列表
- 重载:方法参数必须不同,可以是参数类型、个数或顺序不同。
- 重写:方法的参数列表必须完全相同。
4. 返回类型
- 重载:返回类型可以不同。
- 重写:返回类型必须与父类方法相同,或者是父类方法返回类型的子类型(即协变返回类型)。
5. 异常
- 重载:可以修改或不声明异常。
- 重写:子类方法抛出的异常类型必须是父类方法抛出的异常类型的子类型,不能抛出父类方法没有声明的异常。
6. 访问修饰符
- 重载:可以使用不同的访问修饰符。
- 重写:子类方法的访问权限必须大于或等于父类方法的访问权限(即访问权限不能更严格)。
7. 编译与运行
- 重载:在编译时通过参数类型解析来确定调用的重载方法。
- 重写:在运行时通过对象的实际类型来决定调用哪个方法(动态绑定)。
示例代码
重载(Overloading)
class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
}
public class Main {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(2, 3)); // 调用 int 类型的 add
System.out.println(calc.add(2.5, 3.5)); // 调用 double 类型的 add
System.out.println(calc.add(1, 2, 3)); // 调用三个 int 参数的 add
}
}
输出:
5
6.0
6
重写(Overriding)
class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
animal.sound(); // 调用父类的 sound
Animal dog = new Dog();
dog.sound(); // 调用子类的 sound(运行时多态)
}
}
输出:
Animal makes a sound
Dog barks
总结
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一类 | 子类继承父类 |
参数列表 | 必须不同 | 必须完全相同 |
返回类型 | 可以不同 | 必须相同或是父类返回类型的子类型 |
异常 | 可以不同 | 子类抛出的异常必须是父类抛出的异常的子类 |
访问修饰符 | 可以不同 | 子类访问权限必须大于等于父类方法的访问权限 |
发生阶段 | 编译时确定 | 运行时通过动态绑定确定 |
重写返回值类型说明
如果方法的返回类型是引用类型,重写时可以返回该类型的子类对象。例如:
public class Hero {
public String name() {
return "超级英雄";
}
}
public class SuperMan extends Hero {
@Override
public String name() {
return "超人";
}
}
public class SuperSuperMan extends SuperMan {
@Override
public SuperMan hero() {
return new SuperMan();
}
}
这符合协变返回类型的规则。
可变长参数 (Varargs)
可变长参数是从 Java 5 开始引入的功能,允许在方法定义时使用 ...
来接收不定数量的参数。这使得方法可以接受任意数量的参数,包括零个参数,简化了方法重载的使用场景。
1. 定义和使用
public static void method1(String... args) {
// args 是一个数组,方法内可以像使用数组一样使用它
}
在调用 method1
时,可以传入任意数量的 String
参数,包括不传参数。
method1(); // 不传任何参数
method1("Hello"); // 传入一个参数
method1("Hello", "World"); // 传入多个参数
2. 规则
- 可变长参数必须是方法参数列表的最后一个参数。
- 可变长参数本质上是一个数组,因此在方法内部,可以通过数组的方式访问它。
public static void method2(String arg1, String... args) {
System.out.println(arg1);
for (String arg : args) {
System.out.println(arg);
}
}
在这个例子中,arg1
是一个常规参数,args
是一个可变长参数,可以接受 0 个或多个 String
参数。
3. 方法重载与可变长参数
当方法重载时,如果一个方法使用了可变长参数,那么它会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
public class VariableLengthArgument {
public static void printVariable(String... args) {
for (String s : args) {
System.out.println(s);
}
}
public static void printVariable(String arg1, String arg2) {
System.out.println(arg1 + arg2);
}
public static void main(String[] args) {
printVariable("a", "b"); // 调用固定参数方法
printVariable("a", "b", "c", "d"); // 调用可变长参数方法
}
}
输出:
ab
a
b
c
d
4. 编译原理
在编译过程中,Java 会将可变长参数转换为一个数组。因此,编译后的代码实际上是将所有传入的参数转换成一个数组,并通过数组来访问它们。
编译后的代码示例:
public class VariableLengthArgument {
public static void printVariable(String... args) {
String[] var1 = args; // 可变参数变成数组
int var2 = args.length;
for (int var3 = 0; var3 < var2; ++var3) {
String s = var1[var3]; // 遍历数组
System.out.println(s);
}
}
// 其他方法
}
5. 注意事项
- 可变参数是数组,在方法内部的使用与数组一样。
- 可变参数的顺序不能被改变,它总是作为参数列表的最后一个参数。
- 如果方法中有固定参数和可变参数,固定参数必须放在可变参数之前。
示例:固定参数和可变参数一起使用
public static void printDetails(int count, String... details) {
System.out.println("Count: " + count);
for (String detail : details) {
System.out.println(detail);
}
}
public static void main(String[] args) {
printDetails(3, "Apple", "Banana", "Cherry");
}
输出:
Count: 3
Apple
Banana
Cherry
总结
- 可变长参数允许方法接受不定数量的参数,它会被转换成一个数组。
- 它只能是参数列表的最后一个参数。
- 通过可变长参数,可以减少方法重载的需求,简化方法的定义和调用。