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. 访问控制
privateprotectedpublic
2. 类、方法和变量修饰符
abstractclassextendsfinalimplementsinterfacenativenewstaticstrictfpsynchronizedtransientvolatileenum
3. 程序控制
breakcontinuereturndowhileifelseforinstanceofswitchcasedefaultassert
4. 错误处理
trycatchthrowthrowsfinally
5. 包相关
importpackage
6. 基本类型
booleanbytechardoublefloatintlongshort
7. 变量引用
superthisvoid
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 = 11b = 9c = 10d = 10e = 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
d4. 编译原理
在编译过程中,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总结
- 可变长参数允许方法接受不定数量的参数,它会被转换成一个数组。
- 它只能是参数列表的最后一个参数。
- 通过可变长参数,可以减少方法重载的需求,简化方法的定义和调用。
