3. Java 语言特性

行莫
行莫
发布于 2025-06-04 / 24 阅读
0
0

3. Java 语言特性

“我的语言之局限,即我的世界之局限。” —— Ludwig Wittgenstein

这句话放在编程语言的世界里同样适用。当我们讨论一门编程语言时,应该尽量客观的来看待它,我们应该尽可能的了解它的优势、劣势,然后在适合的地方使用它。而不是指责它或者吹捧它。当一名程序员常年使用某一种编程语言后他的思维方式会被该语言极大的绑定,这便是习惯的力量也是认知固化的一种表现,这是人类思维与生俱来的缺陷之一。为了打破这种缺陷,我们可以多了解尝试多种不同语言,理解它们的设计思想,帮助我们的思维更加开放。

跨平台

Java 发布之初的一个口号是 "Write Once, Run Anywhere" 即 “一次编写到处执行” 。这也表明了 Java 语言的平台无关性。这里的平台指的是操作系统如 Windows 、 Linux 、macOS 、Solaris 、Android 等,和计算机硬件架构如 x86、x64、ARM、MIPS、RISC-V 等。Java 之所以能够做到 "Write Once, Run Anywhere" 是因为它是一种解释型语言。Java 代码源文件被编译器编译为 .class 文件,运行时由 JVM 对 .class 文件进行解释为机器码再执行。

JVM 是平台相关的不同的操作系统和计算机硬件架构需要使用不同的 JVM ,Java 编译后的 .class 文件会被部署到服务器上,由对应平台的 JVM 去执行。JVM 在执行 .class 字节码的时候完全不知道该字节码的来源,理论上来说 JVM 可以执行由任何语言编译成的字节码,只要编译器可以把源代码文件编译为符合字节码规范的字节码文件。Sun的开发设计团队在最初设计的时候就把Java的规范拆分成了Java语言规范《The Java Language Specification》及Java虚拟机规范《The Java Virtual Machine Specification》。

对应的编译型语言比如 C 语言,在编译器编译阶段就会产生最终可执行的机器码,而在 windows 平台下编译成功的二进制可执行文件在 linux 上就不能执行,想要在 linux 平台成功执行需要用源代码文件在 linux 平台下再次进行编译。

垃圾回收

垃圾指的是不再被使用的内存空间,当程序运行起来后随着一直被使用需要不断的占用内存,如果只是占用不释放的话再多的内存也不够用,所以就需要循环利用。把一些使用过的内存空间进行释放以便后续程序的执行有足够的可用的内存空间。在 Java 中这个回收过程是自动化完成的,不需要程序员的介入。这就是垃圾回收 Garbage Collection,缩写为GC,是指一种自动的存储器管理机制,当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。

垃圾回收最早起源于LISP语言。在没有垃圾回收机制的时候内存的释放由程序员手动进行。这意味着,程序员需要负责动态分配的内存空间(例如使用 new 或 malloc 分配的内存)的释放。如果程序员忘记释放已经分配的内存,就会导致内存泄漏,最终导致程序运行缓慢甚至崩溃。凡是需要人工介入的部分出错的概率就越高,但是人工控制又往往能够更精细。

目前许多语言如Smalltalk、Java、C#、Python、JavaScript、Ruby、PHP、Kotlin、Scala、Go和D语言都支持垃圾回收器。C、C++ 、Rust和Pascal 等语言是没有垃圾回收机制,程序员需要手动管理内存。

垃圾回收机制很大的提高了程序的安全性,降低了复杂性,让程序员更不容易出错,把程序员们照顾的很好,这也是Java语言广泛流行的重要原因之一。

面向对象

计算机革命起源于一台机器,而编程语言就好比是那台机器。然而计算机并不只是机器而已,它们还是扩展思维的工具(就像乔布斯喜欢说的一句话:计算机时“思维的自行车”),也是一种与众不同的表达媒介。结果就是,工具越来越不像机器,而是越来越像思维的一部分。编程语言是用于创建应用程序的思维模式。语言本身可以从写作、绘画、雕塑、动画、电影制作等表达方式中获取灵感,而面向对象编程(Object-Oriented Programming , OOP)则是用计算机作为表达媒介的一种尝试。

Java 语言自 1995 年一经问世起就是完全面向对象的编程语言。这并不是 Java 的首创,在 Java 语言面世前已经有很多的面向对象的编程语言。比如 C++SmallTalk 等,其中 SmallTalk 强调对象是编程的基本单位,任何东西都必须是对象。C++ 是在 C 语言的基础上扩展了面向对象特性,支持类、继承、多态、封装等面向对象的核心概念。Java 便是从这些前辈中派生出来的语言。

面向对象是一种思维方式,强调更加关注宏观抽象而不是细节。将事务的特性和规则总结提炼,然后归类。归类后的特性和规则上升一个层级,在这个层级上再次总结提炼特性和规则,归类然后再上升一个新的层级,不断地重复这个过程,这就是一个抽象化的过程。我个人认为这种方式更加适合于分工合作的社会结构。

抽象是需要一个发展过程的,所有的编程语言都是一种抽象。比如,汇编语言是对计算机底层的一个极简抽象。还有很多的命令式编程语言,如 FORTRAN 、BASIC 、C 语言等都是各自对汇编语言的抽象。这些语言是站在解决方案角度看待问题,要求你要根据计算机的结构而不是要解决的问题的结构来思考。而面向对象则是让你首先就站在问题领域来思考该如何解决问题。你可以把问题抽象成一个个对象,而不是首先去着眼于具体的算法或是数据结构等计算机相关的内容。

Simula 作为历史上第一门面向对象的编程语言,就如同它的名字一样 “模拟”,是用于在计算机上模拟出问题域。SmallTalk 是历史上第一门获得成功的面向对象语言,为后起之秀 Java 提供了灵感的借鉴意义。Alan Key 总结了 SmallTalk 语言的5个基本特征,这些特征代表了纯粹的面向对象编程方式:

  1. 万物皆对象。你可以把对象想象成一种神奇的变量,它可以存储数据,同时你可以“发送请求”,让它执行一些操作。对于你想要解决的问题中的任何元素,你都可以在程序中用对象来呈现(比如狗,建筑,服务等)。

  2. 一段程序实际上就是多个对象通过发送消息来通知彼此要干什么。当你向一个对象“发送消息”时,实际情况是你发送了一个请求去调用该对象的某个方法。

  3. 从内存角度而言,每一个对象都是由其他更为基础的对象组成的。通过将现有的及格对象打包在一起,你就创建了一个全新的对象。这种方法展现了对象的简单性,同时隐藏了复杂性。

  4. 每一个对象都有类型。对象不会凭空产生的,它是某一个抽象类别的具体个例,每一个对象都是通过某个类生成的实例,这里的“类”就几乎等同于“类型”。一个类最为显著的特征是“你可以发送什么消息给它”。

  5. 同一类型的对象可以接收相同的消息。这句话具有丰富的含义。举例来说,因为一个“圆形”对象同样是也是一个“形状”对象,所以“圆形”也可以接收“形状”类型的消息。这意味着,你为“形状”对象编写的代码自然可以适用于任何的“形状”子类对象比如说“圆形”对象。这种可替换性是面向对象编程的一个基石。

封装

封装(Encapsulation)指的是对数据和行为的聚集,将他们放在一起组织起来。隐藏了内部细节只对外部暴漏允许可见的接口,保护了数据不被外部直接操作。

继承

继承(Inheritance)是一个具体化的过程。抽象过程是自下而上的,具象过程是自上而下的。下图中的箭头表示了继承关系。

继承描述了对象之间的一种从属关系,比如“猫”是属于“宠物”的一种,也就是“猫”继承自“宠物”。“猫”是“宠物”的子类,反过来“宠物”是“猫”的父类。在 Java 语言中有继承 extends 和实现 implements 都是在描述这一关系本质上是相同的。

多态

多态(Polymorphism)是同一抽象的不同具象化,由抽象和继承共同作用产生。比如“宠物”是一个抽象,“猫”、“狗”是“宠物”这一抽象的更具象化表示。再比如“人”是一个抽象,“男人”、“女人”、“老人”、“小孩”是“人”这一抽象的更具象化表示。这其中的关系是以抽象和继承作为逻辑基础。

抽象层决定行为规范,具象层要遵守这些行为规范,同时也可发展出自己特有的行为。这就决定了不同的具象之间是可以互相替换并且适配的。比如“宠物”中有一个行为“陪主人散步”,我们可以说“宠物陪主人散步”,也可以替换为“狗陪主人散步”或“猫陪主人散步”。多态的威力就在于此,它是抽象层的一套行为规范,符合该规范的都可以轻松的进行替换。

强类型与静态类型

Java 是静态、强类型语言,变量、函数参数和返回值的类型在编译时就必须确定,编译器会进行类型检查。强类型指语言对类型的约束非常严格,不同类型之间不能随意转换,类型错误不会被自动忽略或隐式转换,必须显式处理。Java 是强类型语言,类型转换需要显式声明。

优点:

  • ✅更安全:Java 设计之初就强调“让程序员少犯错”,编译时发现错误,减少运行时崩溃。

  • ✅ 性能优化:编译器知道类型,可生成更高效的机器码。

  • ✅ 可维护性:代码意图更清晰,适合大型项目。

缺点:

  • ❌ 灵活性低:需要更多样板代码(如类型声明)。

  • ❌ 开发速度稍慢:相比动态语言,前期需要更多类型设计。

  • ❌ 代码量多:所有的变量都需要声明类型增加了代码量。

异常处理

Java 的异常处理机制旨在分离正常业务逻辑与错误处理逻辑,提升代码的健壮性和可维护性。通过异常机制,Java 能够优雅地捕获和处理运行时出现的各种错误,避免程序因小问题崩溃,同时为开发者提供了丰富的错误信息,便于调试和维护。

Java 中的异常体系结构如下:

Throwable

├── Error(错误,程序无法处理)

└── Exception(异常,程序可以处理)

├── 受检异常(Checked Exception)

└── 非受检异常(Unchecked Exception/RuntimeException)

Error 由 JVM 抛出,表示严重问题,如 OutOfMemoryErrorStackOverflowError。程序一般无法处理,通常不需要捕获。

Exception(异常)程序可以处理的异常。受检异常(Checked Exception):编译器强制要求处理,如 IOException、SQLException。非受检异常(Unchecked Exception/RuntimeException):编译器不强制要求处理,如 NullPointerException、ArrayIndexOutOfBoundsException。

异常的传播机制:

  • 当方法内部发生异常时,如果没有被捕获(catch),异常会沿着方法调用栈向上传播,直到被某个 catch 块捕获或最终到达 JVM,导致程序终止。

  • 受检异常必须显式捕获或声明抛出,否则编译不通过。

  • 非受检异常可以选择捕获,也可以不处理。

异常链(Exception Chaining),Java 支持异常链,可以在捕获一个异常后,抛出另一个异常,并将原始异常作为新异常的 cause,便于追踪异常根源。

优点:

  • 安全性高:强制处理受检异常,减少遗漏。

  • 代码清晰:业务逻辑与异常处理分离。

  • 调试方便:异常栈信息详细,便于定位问题。

  • 可扩展性强:支持自定义异常类型。

缺点:

  • 代码冗长:频繁捕获异常会导致代码量增加。

  • 性能开销:异常的抛出和捕获有一定性能损耗(但一般可忽略)。

  • 滥用风险:不恰当的异常处理可能掩盖真实问题。

C/C++:主要通过返回值或全局变量表示错误,容易被忽略,异常处理不如 Java 规范。

Python:异常机制与 Java 类似,但没有受检异常,灵活性更高。

Go:不使用异常,采用多返回值(error)方式,鼓励显式错误处理。

多线程支持

Java 诞生于 1995 年,正值互联网和分布式计算兴起的时代。其设计团队深刻认识到并发编程对于服务器端、网络通信、图形界面等应用场景的重要性。因此,Java 在语言层面就内置了对多线程的原生支持,目标是让并发编程变得更简单、更安全、更高效。

Java 的多线程设计理念主要体现在以下几个方面:

  • 平台无关性:线程的实现由 JVM 屏蔽了底层操作系统的差异,开发者只需关注业务逻辑。

  • 易用性:通过简单的语法(如 Thread 类、Runnable 接口)即可创建和管理线程。

  • 安全性:提供了同步机制(如 synchronized、volatile、Lock 等)来保证多线程环境下的数据一致性。

  • 高层抽象:随着 JDK 版本升级,引入了线程池、并发集合、原子变量、并发工具类等,极大简化了并发编程的复杂度。

在 Java 之前,C/C++ 的多线程编程高度依赖操作系统 API(如 POSIX 线程、Win32 线程),移植性差,易出错,调试困难。Java 作为面向对象语言,天然适合将线程、任务、锁等抽象为对象,便于管理和扩展。追求“让程序员少犯错”,多线程机制也强调安全和健壮,减少死锁、竞态等问题极大降低了并发编程的门槛。

优势:

  • 平台无关:一次编写,到处运行,线程机制由 JVM 统一管理。

  • 安全性高:内置同步机制,减少并发错误。

  • 生态丰富:并发工具类、线程池、并发集合等一应俱全。

  • 适合大型系统:广泛应用于企业级开发、服务器端、分布式系统等。

不足:

  • 线程较重:Java 线程本质上是操作系统线程,创建和切换开销较大。

  • 编程复杂:多线程编程本身难度高,死锁、竞态等问题仍需开发者关注。

  • 内存消耗大:每个线程有独立的栈空间,线程数过多易导致内存溢出。

  • 不适合极高并发:对于百万级并发,需结合异步、NIO、协程等技术。


评论