第一章 Java程序设计概述
1.2.7 可移植性
基本数据类型的大小和有关运算的行为是明确的
1.2.9 高性能
Java编译器有两种主要的编译方式:即时编译器(Just-In-Time Compiler,JIT)和传统编译器(Ahead-Of-Time Compiler,AOT)。两者在编译时机和优化策略上存在显著差异。以下是它们的简介:
1.2.9.1即时编译器(JIT)
工作原理:
- JIT编译器在程序运行时动态将字节码编译成机器码。字节码是Java程序在编译阶段生成的中间代码,由Java虚拟机(JVM)解释执行。
- JIT编译器在程序运行时识别和编译经常执行的代码路径,将这些路径的字节码转换为机器码,缓存并重复使用,以提高性能。
优点:
- 优化运行性能: JIT可以在运行时分析代码的执行情况,进行实时优化,例如内联方法、消除冗余代码等。
- 适应性强: 根据运行时的实际情况进行优化,可以更好地利用当前系统资源和状态。
缺点:
- 初始启动速度慢: 因为在程序开始执行时需要进行即时编译,可能导致启动时间稍长。
- 运行时开销: 编译过程会消耗一定的系统资源,在某些情况下可能影响程序的响应速度。
1.2.9.2 传统编译器(AOT)
工作原理:
- AOT编译器在程序部署之前将字节码编译成机器码。这种编译是在开发阶段完成的,生成的机器码在部署时直接执行。
- 这种方法类似于C/C++等语言的编译方式,在发布之前将源代码编译为目标机器的本地代码。
优点:
- 启动速度快: 因为程序在运行时已经是机器码,不需要再进行即时编译,所以启动速度较快。
- 减少运行时开销: 编译工作在部署前完成,运行时不需要消耗资源进行编译,提高了运行效率。
缺点:
- 缺乏运行时优化: AOT编译器无法利用运行时信息进行动态优化,可能在某些情况下性能不如JIT编译器。
- 编译复杂度: 需要在开发阶段对各种可能的运行环境进行充分考虑,确保编译后的代码在各种环境下都能高效运行。
总结
JIT和AOT编译器各有优缺点,具体选择哪种方式需要根据具体应用场景和需求进行权衡。现代Java虚拟机(如HotSpot)通常结合两者的优点,在不同阶段和场景下灵活使用这两种编译方式,以达到最佳性能。
第二章 Java编程环境
在Java开发环境中,JDK、JVM、JRE等术语经常出现。理解它们的区别对开发者来说非常重要。以下是对这些术语的详细介绍:
1. Java Development Kit (JDK)
JDK:
- 全称: Java开发工具包(Java Development Kit)。
- 用途: 提供了开发Java应用程序所需的所有工具和资源。
- 组成: 包含了JRE(Java运行时环境)、编译器(javac)、调试器(jdb)、文档生成器(javadoc)以及其他开发工具。
- 版本: 根据不同的Java版本发布不同的JDK,例如JDK 8、JDK 11、JDK 17等。
2. Java Runtime Environment (JRE)
JRE:
- 全称: Java运行时环境(Java Runtime Environment)。
- 用途: 提供运行Java应用程序所需的环境。
- 组成: 包含JVM(Java虚拟机)、核心类库(Java标准库)以及支持文件。
- 特点: JRE不包含编译器和开发工具,只用于运行已经编译好的Java程序。
3. Java Virtual Machine (JVM)
JVM:
- 全称: Java虚拟机(Java Virtual Machine)。
- 用途: 执行Java字节码,使得Java程序能够在不同平台上运行。
- 组成: 包含类加载器、字节码解释器和即时编译器(JIT),负责将Java字节码转换为机器码执行。
- 特点: JVM是跨平台的关键,使得Java程序可以“编写一次,到处运行”。
4. 各部分的关系
- JDK包含JRE: JDK不仅包括开发工具,还包括JRE,因此安装JDK也会同时安装JRE。
- JRE包含JVM: JRE提供了运行Java程序的环境,其中包含了JVM。
- JVM是核心: JVM是Java程序运行的核心,它解释和执行字节码。
5. 示例和使用场景
- 开发者使用JDK: 开发Java应用程序时需要使用JDK,因为它提供了编译器和其他开发工具。
- 用户使用JRE: 只需运行Java应用程序的用户只需要安装JRE,因为JRE包含了运行程序所需的环境。
图示解释
+------------------------+
| JDK |
| +-------------------+ |
| | JRE | |
| | +-------------+ | |
| | | JVM | | |
| | +-------------+ | |
| +-------------------+ |
+------------------------+
总结
- JDK 是开发Java应用程序的工具包,包含JRE和开发工具。
- JRE 是运行Java应用程序的环境,包含JVM和核心类库。
- JVM 是执行Java字节码的虚拟机,实现了Java的跨平台特性。
了解这些基本概念有助于更好地掌握Java开发和运行环境,合理选择和配置开发工具,提高开发效率。
jshell
进入:jshell
退出:/exit
第三章 Java的基本程序设计结构
命名规范:骆驼命名法
3.3 数据类型
8种:4,2,1,1
3.3.2 浮点类型
浮点数值不适用于无法接受舍入误差的运算,因为浮点数值采用二进制表示。
3.3.3 char 类型
在Java中,\u000A
是一个Unicode转义序列,表示换行符(newline)。由于Java编译器在解析代码时会首先处理Unicode转义序列,因此这段代码中的 \u000A
会被替换为一个实际的换行符,导致代码被分成两行。这会引发语法错误,因为注释中的换行符会导致注释被截断,并且可能会破坏代码的结构。
例如,以下代码:
// \u000A is a newline
在解析Unicode转义序列之后,实际上变成了:
//
is a newline
这会导致语法错误,因为“is a newline”已经不在注释中,而是成为了非法的代码。
为了避免这种情况,请不要在注释中使用Unicode转义序列来表示换行符。如果需要使用换行符,可以直接使用实际的换行符或者确保注释中没有误用的Unicode转义序列。例如:
// This is a comment with a newline character
// The actual newline character is not represented as \u000A
在Java代码中使用 \u000A
这样的Unicode转义序列会被编译器解析为实际的换行符,从而可能会导致语法错误。为了避免这种错误,应当避免在注释中使用这样的Unicode转义序列。
3.3.4 Unicode 和 char 类型
没看懂
3.6.6 码点和代码单元
在Java中,字符串是一个由字符(char
)组成的序列。理解字符串的码点(code point)和代码单元(code unit)对于处理包含非基本多文字符(如表情符号或非拉丁字符)的字符串非常重要。
码点(Code Point)
Unicode 码点是指特定字符在Unicode标准中的唯一编号。Unicode标准定义了一个字符集,其中每个字符都有一个唯一的整数编号,称为码点。码点可以表示为十六进制,并且范围从 U+0000
到 U+10FFFF
。例如:
- 字母 “A” 的 Unicode 码点是
U+0041
- 表情符号 😀 的 Unicode 码点是
U+1F600
代码单元(Code Unit)
代码单元是存储字符串数据的最小单位。在Java中,字符串内部是以 UTF-16
编码格式存储的。UTF-16
使用一个或两个 16
位的代码单元来表示一个 Unicode 码点:
- BMP(基本多文种平面)字符使用一个代码单元(16位)。
- 补充字符使用一对称为代理对(surrogate pair)的代码单元。
例如:
- 字母 “A” 使用一个代码单元:
\u0041
- 表情符号 😀 使用两个代码单元:
\uD83D
和\uDE00
示例代码
以下是一个示例代码,演示如何在Java中处理字符串的码点和代码单元:
public class CodePointAndCodeUnitExample {
public static void main(String[] args) {
String str = "A😀";
// 获取字符串的长度(代码单元数)
int length = str.length();
System.out.println("Length (code units): " + length);
// 获取字符串的码点数
int codePointCount = str.codePointCount(0, length);
System.out.println("Code point count: " + codePointCount);
// 遍历字符串的每个代码单元
System.out.println("Code units:");
for (int i = 0; i < length; i++) {
System.out.printf("%04X ", str.charAt(i));
}
System.out.println();
// 遍历字符串的每个码点
System.out.println("Code points:");
for (int i = 0; i < codePointCount; i++) {
int index = str.offsetByCodePoints(0, i);
int codePoint = str.codePointAt(index);
System.out.printf("U+%04X ", codePoint);
}
}
}
输出
Length (code units): 3
Code point count: 2
Code units:
0041 D83D DE00
Code points:
U+0041 U+1F600
解释
str.length()
返回字符串的代码单元数,即3个代码单元。str.codePointCount(0, length)
返回字符串的码点数,即2个码点。- 通过
str.charAt(i)
遍历并打印每个代码单元。 - 通过
str.codePointAt(index)
和str.offsetByCodePoints(0, i)
遍历并打印每个码点。
这种处理方式在处理包含非基本多文字符(例如表情符号或非拉丁字符)的字符串时非常有用。
3.8.5 多重选择:switch语句
在Java中,switch
语句用于选择执行多个代码块之一。Java的 switch
语句有两种行为方式:直通行为(fall-through behavior)和无直通行为。理解这两种行为对编写和调试代码非常重要。
直通行为(Fall-Through Behavior)
直通行为是指在 switch
语句中,当一个 case
匹配成功后,如果没有 break
语句,那么程序将继续执行后续的 case
语句,直到遇到 break
、return
、throw
或者到达 switch
语句的末尾。
示例代码
public class FallThroughExample {
public static void main(String[] args) {
int dayOfWeek = 3;
switch (dayOfWeek) {
case 1:
System.out.println("Monday");
case 2:
System.out.println("Tuesday");
case 3:
System.out.println("Wednesday");
case 4:
System.out.println("Thursday");
case 5:
System.out.println("Friday");
break;
case 6:
System.out.println("Saturday");
break;
case 7:
System.out.println("Sunday");
break;
default:
System.out.println("Invalid day");
break;
}
}
}
输出
Wednesday
Thursday
Friday
在这个示例中,dayOfWeek
为 3
,匹配 case 3
后,没有 break
语句,所以程序继续执行 case 4
和 case 5
的代码,直到遇到 case 5
的 break
语句。
无直通行为(Non-Fall-Through Behavior)
无直通行为是指在每个 case
语句块的结尾使用 break
语句,这样当一个 case
语句块执行完毕后,程序跳出 switch
语句,而不会继续执行后续的 case
语句。
示例代码
public class NonFallThroughExample {
public static void main(String[] args) {
int dayOfWeek = 3;
switch (dayOfWeek) {
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
case 4:
System.out.println("Thursday");
break;
case 5:
System.out.println("Friday");
break;
case 6:
System.out.println("Saturday");
break;
case 7:
System.out.println("Sunday");
break;
default:
System.out.println("Invalid day");
break;
}
}
}
输出
Wednesday
在这个示例中,dayOfWeek
为 3
,匹配 case 3
后,执行 case 3
语句块,并在遇到 break
语句时跳出 switch
语句,不再执行后续的 case
语句。
总结
- 直通行为(Fall-Through Behavior): 当没有
break
语句时,switch
语句将继续执行后续的case
语句。 - 无直通行为(Non-Fall-Through Behavior): 在每个
case
语句块末尾使用break
语句,switch
语句只执行匹配的case
语句块,然后跳出switch
语句。
通过理解这两种行为,可以更好地控制 switch
语句的执行流程,编写更清晰、易读的代码。
快排算法
快速排序(QuickSort)是一种高效的排序算法,通常用于大数据集。它的基本思想是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序。整个排序过程可以递归进行,以达到整个数据变成有序序列。
算法步骤
-
选择基准(Pivot):
选择一个基准元素,一般可以选择数组的第一个元素、最后一个元素或中间元素。 -
划分操作(Partition):
将数组分成两部分:一部分比基准元素小,另一部分比基准元素大。具体操作是从数组两端交替向中间扫描,找到一个比基准元素大的元素和一个比基准元素小的元素,然后交换它们的位置,直到所有元素都排好位置。 -
递归排序(Recursion):
对基准元素左右两边的子数组分别进行快速排序,直到子数组的长度为0或1,即所有元素都排好位置。
代码实现
以下是一个典型的快速排序的Python实现:
def quicksort(arr):
if len(arr) <= 1:
return arr
else:
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quicksort(left) + middle + quicksort(right)
# 示例使用
arr = [3, 6, 8, 10, 1, 2, 1]
print(quicksort(arr))
算法分析
-
时间复杂度:
- 最优时间复杂度:O(n log n),在每次划分时能够均匀地将数组分成两部分时。
- 最差时间复杂度:O(n²),在每次划分时总是选择到极端情况(例如,已经有序的数组且选择第一个或最后一个元素作为基准)。
- 平均时间复杂度:O(n log n)。
-
空间复杂度:
- 平均情况下的空间复杂度是O(log n),因为递归调用栈的深度为log n。
- 最差情况下(例如,每次划分都只分出一个元素),空间复杂度是O(n)。
优化方法
-
选择更好的基准:
- 可以选择随机基准或“三数取中”(选择第一个、中间和最后一个元素的中间值作为基准)来避免最坏情况。
-
尾递归优化:
- 可以通过尾递归优化来减少递归调用栈的深度。
-
小数组优化:
- 对于小数组(一般为10个或更少),可以使用插入排序来代替快速排序,以减少递归调用的开销。
快速排序因其平均情况下的高效性和实现的简洁性,被广泛应用于各种实际场景。
第四章 对象与类
4.7 记录概念
Java 记录(record)是 Java 14 引入的一种新特性,并在 Java 16 正式发布。记录是一种特殊的类,旨在简化不可变数据传输对象(DTOs,Data Transfer Objects)的定义。这些对象通常只包含数据,没有太多行为(方法)。通过使用记录,开发者可以减少样板代码,使代码更简洁和易读。
定义记录
记录的定义非常简单,使用 record
关键字来声明。例如:
public record Point(int x, int y) {}
上面的代码定义了一个 Point
记录类,它包含两个字段:x
和 y
。
特性和行为
-
自动生成成员变量:
x
和y
自动成为Point
的成员变量,并且它们是final
的。
-
自动生成构造函数:
Point
记录类自动生成一个包含所有成员变量的构造函数。
-
自动生成访问器方法:
Point
记录类自动生成x()
和y()
方法来访问成员变量。
-
自动生成
equals()
,hashCode()
和toString()
方法:Point
记录类自动生成这些方法,并基于成员变量进行实现。
示例使用
以下是一个完整的示例,包括记录的定义和使用:
public record Point(int x, int y) {}
public class Main {
public static void main(String[] args) {
Point p1 = new Point(3, 4);
System.out.println(p1.x()); // 输出:3
System.out.println(p1.y()); // 输出:4
System.out.println(p1); // 输出:Point[x=3, y=4]
Point p2 = new Point(3, 4);
System.out.println(p1.equals(p2)); // 输出:true
System.out.println(p1.hashCode() == p2.hashCode()); // 输出:true
}
}
自定义记录行为
尽管记录自动生成了很多代码,开发者仍然可以自定义一些行为。例如,可以定义自己的构造函数或覆盖默认生成的方法。
自定义构造函数
可以在记录中添加额外的逻辑到构造函数:
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Coordinates must be non-negative");
}
}
}
覆盖 toString
方法
可以覆盖记录的默认 toString
方法:
public record Point(int x, int y) {
@Override
public String toString() {
return String.format("Point[x=%d, y=%d]", x, y);
}
}
限制和注意事项
-
不可变性:
- 记录类的成员变量是不可变的,即它们是
final
的,不能在创建后修改。
- 记录类的成员变量是不可变的,即它们是
-
继承:
- 记录类不能显式地继承其他类(它隐式地继承
java.lang.Record
),也不能被继承。
- 记录类不能显式地继承其他类(它隐式地继承
-
序列化:
- 记录类实现了
Serializable
接口,但需要注意序列化和反序列化过程中一些特殊情况。
- 记录类实现了
总结
Java 的记录是一种简化数据传输对象定义的新特性,通过减少样板代码提高开发效率。记录类具有不可变性,自动生成构造函数、访问器方法、equals()
、hashCode()
和 toString()
方法,但仍允许开发者进行一些自定义以满足特定需求。
4.8.1 包名
Java 中的 import
和 C++ 中的 #include
虽然都用于在代码中引入外部模块或库,但它们在工作机制和作用方面有显著的区别。
import
在 Java 中的作用
- 作用域扩展:
import
语句在 Java 中主要用于导入包中的类或接口,使得这些类或接口在当前文件中可以直接使用它们的简单名称,而不需要写全限定名称。例如,import java.util.List
允许在代码中直接使用List
而不必每次都写java.util.List
。 - 不复制代码:
import
语句并不实际将任何代码插入到文件中,它只是告诉编译器去特定包中查找类或接口。编译时,Java 编译器使用这些信息来解析和编译类的引用。 - 编译时处理:
import
语句在编译时处理,而不是在预处理阶段。Java 编译器在编译阶段会处理所有的import
语句,并解析类的依赖关系。
#include
在 C++ 中的作用
- 代码插入:
#include
是一个预处理指令,它在预处理阶段展开。它会将被包含文件的内容直接插入到包含指令的位置。因此,#include
文件的内容实质上成为了包含文件的一部分。 - 宏和模板:C++ 的
#include
可以包含不仅限于类和函数的声明,还可以包含宏定义、模板定义等。因此,它的功能更加广泛。 - 防止重复包含:为了防止头文件被多次包含导致重复定义错误,C++ 通常使用头文件保护措施(如
#ifndef
,#define
,#endif
或#pragma once
)。
总结
虽然 Java 的 import
和 C++ 的 #include
都用于将外部代码引入到当前文件中,但它们的工作原理和使用目的存在明显的区别:
- Java 的
import
只是导入命名空间,使类和接口在当前文件中可见,实际的代码并未被插入。 - C++ 的
#include
则是直接将头文件的内容插入到当前文件中,是一种文本替换机制。
因此,尽管它们在高层次的目的上相似,但在实现和具体功能上有本质的不同。
第五章 继承
5.1.8 强制类型转换
在 Java 中,类型转换分为向上转型(Upcasting)和向下转型(Downcasting)。这两种类型转换在使用和要求上有明显的区别。
向上转型(Upcasting)
概念
向上转型是将子类对象引用赋值给父类类型的引用变量。因为子类是父类的扩展,所以子类对象可以被看作是父类对象。
需求
- 自动进行:向上转型是自动进行的,不需要显式的类型转换。
- 安全性:向上转型总是安全的,因为子类包含父类的所有功能。
- 例子:
class Animal { void makeSound() { System.out.println("Animal sound"); } } class Dog extends Animal { void makeSound() { System.out.println("Bark"); } void fetch() { System.out.println("Fetch the ball"); } } public class Main { public static void main(String[] args) { Dog dog = new Dog(); Animal animal = dog; // 向上转型,自动进行 animal.makeSound(); // 调用的是 Dog 类的 makeSound 方法,输出 "Bark" } }
向下转型(Downcasting)
概念
向下转型是将父类类型的引用变量赋值给子类类型的引用变量。这通常用于访问子类特有的方法或属性。
需求
- 显式进行:向下转型必须显式进行,使用强制类型转换操作符
(Type)
. - 类型检查:在运行时需要进行类型检查,以确保对象实际是目标类型或其子类型,否则会抛出
ClassCastException
。 - 安全性:向下转型可能不安全,如果对象不是目标类型,强制转换会失败。
- 例子:
class Animal { void makeSound() { System.out.println("Animal sound"); } } class Dog extends Animal { void makeSound() { System.out.println("Bark"); } void fetch() { System.out.println("Fetch the ball"); } } public class Main { public static void main(String[] args) { Animal animal = new Dog(); // 向上转型 animal.makeSound(); // 调用的是 Dog 类的 makeSound 方法,输出 "Bark" if (animal instanceof Dog) { Dog dog = (Dog) animal; // 向下转型 dog.fetch(); // 调用 Dog 类特有的方法,输出 "Fetch the ball" } } }
总结
-
向上转型:
- 自动进行。
- 安全。
- 子类对象可以被父类引用。
-
向下转型:
- 必须显式进行。
- 需要在运行时检查实际类型,可能会抛出
ClassCastException
。 - 父类引用必须指向一个实际的子类对象,否则会导致转换失败。
理解向上转型和向下转型的区别和使用场景有助于正确地使用多态和类型转换。
在向下转型(Downcasting)中,instanceof
运算符的作用是确保安全的类型转换。它用于检查一个对象是否是某个特定类的实例,或是否是该类的子类的实例,从而防止在运行时发生 ClassCastException
异常。以下是 instanceof
的具体用法和作用:
instanceof
运算符的作用
- 类型检查:在进行向下转型之前,使用
instanceof
运算符来检查对象是否是某个特定类型的实例,确保转换的安全性。 - 避免异常:通过类型检查,可以避免因不正确的类型转换而导致的
ClassCastException
异常,提高程序的健壮性。
使用示例
class Animal {
void makeSound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
void makeSound() {
System.out.println("Bark");
}
void fetch() {
System.out.println("Fetch the ball");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Dog(); // 向上转型
animal.makeSound(); // 调用的是 Dog 类的 makeSound 方法,输出 "Bark"
if (animal instanceof Dog) {
Dog dog = (Dog) animal; // 向下转型,安全的类型转换
dog.fetch(); // 调用 Dog 类特有的方法,输出 "Fetch the ball"
} else {
System.out.println("animal 不是 Dog 类型的实例");
}
}
}
instanceof
的具体作用
-
确保正确的类型:
instanceof
运算符在进行向下转型之前,确保对象确实是目标类型或其子类型的实例。- 例如,
if (animal instanceof Dog)
检查animal
是否是Dog
类的实例或其子类的实例。
-
防止
ClassCastException
:- 如果不进行类型检查就直接进行向下转型,可能会抛出
ClassCastException
。 - 通过
instanceof
运算符,可以在转换之前确认类型,避免运行时异常。
- 如果不进行类型检查就直接进行向下转型,可能会抛出
-
提高代码的健壮性和可读性:
- 使用
instanceof
可以让代码更健壮,更具可读性,因为它明确地表明了类型转换的前提条件。 - 它使代码的意图更清晰,表明只有在类型匹配时才会进行转换和后续操作。
- 使用
总结
在 Java 中,instanceof
运算符在向下转型中起到了关键的安全保障作用。它通过在运行时检查对象的实际类型,确保类型转换的安全性,避免了不必要的异常,提高了代码的健壮性和可读性。在进行向下转型时,始终推荐使用 instanceof
来进行类型检查,从而保证程序的稳定性。
5.1.9 instanceof模式匹配
在Java 16中引入的instanceof
模式匹配是一项语法改进,使得类型检查和类型转换更简洁和易读。传统的instanceof
检查通常需要结合类型转换,代码显得冗长且不够优雅。而使用模式匹配后,可以在一个步骤中完成类型检查和类型转换。
传统的instanceof
用法
if (obj instanceof String) {
String str = (String) obj;
// 使用str变量
}
在这种传统用法中,我们需要首先使用instanceof
进行类型检查,然后再进行类型转换。
模式匹配的instanceof
用法
在Java 16之后,我们可以使用模式匹配来简化这一步骤:
if (obj instanceof String str) {
// 直接使用str变量
}
这里,instanceof
不仅检查obj
是否是String
类型,还会在检查通过后将obj
转换为String
类型,并将结果赋值给str
变量。这样可以减少冗余的代码,提高代码的可读性和安全性。
更复杂的例子
模式匹配的instanceof
也可以用于更复杂的逻辑中,比如在if-else
语句或者switch
语句中:
if (obj instanceof String str) {
System.out.println("This is a String: " + str);
} else if (obj instanceof Integer num) {
System.out.println("This is an Integer: " + num);
} else {
System.out.println("Unknown type");
}
模式匹配的局限性
虽然模式匹配的instanceof
提供了很多便利,但也有一些局限性。例如,模式匹配的变量作用域仅限于当前的代码块,这意味着在块外无法访问这些变量。此外,模式匹配无法与已有的泛型类型一起使用,这在某些复杂场景下可能会有所限制。
总结
Java中的instanceof
模式匹配简化了类型检查和类型转换的过程,使得代码更加简洁和易读。这是一项非常实用的语法改进,特别是在需要频繁进行类型检查的场景中。
如果你有任何具体的代码例子或者问题需要进一步讨论,请告诉我!
5.2.3 相等测试与继承
在Java中,比较两个对象有多种方式,每种方式都有其特定的用途和行为。主要的比较方式包括equals
方法、instanceof
操作符和==
操作符。下面对这三种方式进行详细解释:
1. equals
方法
equals
方法是一个用于比较两个对象是否相等的实例方法。默认情况下,Object
类中的equals
方法比较的是对象的引用是否相同。然而,许多类(如String
、Integer
等)重写了equals
方法,以便基于对象的内容进行比较。
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1.equals(str2)); // 输出: true,因为String类重写了equals方法进行内容比较
如果自定义类需要基于其内容进行比较,通常也会重写equals
方法:
class Person {
String name;
int age;
// 构造函数、getters和setters省略
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
2. instanceof
操作符
instanceof
操作符用于检查一个对象是否是某个类或其子类的实例。它在类型检查时非常有用,但不能用于对象的相等性比较。
Person person = new Person();
System.out.println(person instanceof Person); // 输出: true
System.out.println(person instanceof Object); // 输出: true
3. ==
操作符
==
操作符用于比较两个引用是否指向同一个对象。对于基本类型,==
比较的是值;对于对象引用,==
比较的是引用是否相同。
String str1 = new String("hello");
String str2 = new String("hello");
String str3 = str1;
System.out.println(str1 == str2); // 输出: false,因为str1和str2是不同的对象
System.out.println(str1 == str3); // 输出: true,因为str1和str3指向同一个对象
总结
equals
方法:用于比较两个对象的内容是否相等,通常需要重写以实现自定义比较逻辑。instanceof
操作符:用于检查一个对象是否是某个类或其子类的实例,不能用于相等性比较。==
操作符:用于比较两个引用是否指向同一个对象,对于基本类型比较的是值,对于对象引用比较的是引用是否相同。
理解这三种比较方式及其用途对于编写正确且高效的Java代码非常重要。
5.2.4 hashcode方法
在Java中,equals
方法和hashCode
方法是Object
类中定义的两个方法,它们在对象比较和哈希集合(如HashMap
、HashSet
等)中扮演着重要角色。这两个方法虽然有不同的用途,但它们之间存在紧密的联系。
equals
方法
equals
方法用于比较两个对象的内容是否相等。默认情况下,Object
类中的equals
方法比较的是对象的引用是否相同。许多类(如String
、Integer
等)重写了equals
方法,以便基于对象的内容进行比较。
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
hashCode
方法
hashCode
方法返回一个整数值,称为哈希码。这个哈希码用于在哈希表中确定对象的存储位置。默认情况下,Object
类中的hashCode
方法返回对象的内存地址的哈希值。许多类重写了hashCode
方法,以便与重写的equals
方法保持一致。
@Override
public int hashCode() {
return Objects.hash(name, age);
}
equals
和hashCode
方法的联系
-
一致性:如果两个对象根据
equals
方法比较相等,那么它们的hashCode
方法必须返回相同的整数值。这是为了保证在哈希集合中的正确性。例如,在HashMap
中,如果两个键对象是相等的,它们必须有相同的哈希码,以确保能够找到存储的值。 -
不相等的对象:如果两个对象不相等,它们的
hashCode
方法不一定要返回不同的整数值(虽然不同的哈希码可以提高哈希集合的性能)。这意味着不同的对象可以有相同的哈希码(称为哈希碰撞)。
为什么要重写equals
和hashCode
在使用基于哈希的集合(如HashMap
、HashSet
)时,重写equals
和hashCode
方法是必要的,以确保对象的比较和存储行为正确。下面是一个示例,展示了如果不重写这些方法可能会出现的问题:
class Person {
String name;
int age;
// 构造函数、getters和setters省略
}
Person p1 = new Person("John", 25);
Person p2 = new Person("John", 25);
HashSet<Person> set = new HashSet<>();
set.add(p1);
System.out.println(set.contains(p2)); // 输出: false,因为默认的equals和hashCode方法是基于引用比较的
通过重写equals
和hashCode
方法,我们可以确保在集合中正确地处理对象:
class Person {
String name;
int age;
// 构造函数、getters和setters省略
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
Person p1 = new Person("John", 25);
Person p2 = new Person("John", 25);
HashSet<Person> set = new HashSet<>();
set.add(p1);
System.out.println(set.contains(p2)); // 输出: true,因为重写了equals和hashCode方法
总结
equals
方法用于比较对象的内容是否相等。hashCode
方法返回对象的哈希码,用于确定对象在哈希表中的存储位置。- 如果两个对象根据
equals
方法比较相等,那么它们的hashCode
方法必须返回相同的整数值。 - 在重写
equals
方法时,通常也需要重写hashCode
方法,以确保在哈希集合中的正确性和一致性。
5.8 密封类
在Java中,密封类(Sealed Class)是从Java 15开始引入的一种类,用于限制类的继承层次结构。密封类允许开发者明确规定哪些类可以继承它,这在增强代码安全性和可读性方面非常有用。
密封类的定义
密封类使用 sealed
关键字来声明,并且需要明确指定哪些类可以继承它。这些可以继承密封类的类需要使用 permits
子句来指定。例如:
public sealed class Shape permits Circle, Rectangle, Triangle {
// 定义 Shape 类的内容
}
在上面的例子中,Shape
类是一个密封类,只有 Circle
, Rectangle
, Triangle
这三个类可以继承 Shape
。
密封类的继承关系
继承自密封类的子类必须遵循以下规则之一:
-
最终类 (
final
): 子类如果不希望再被继承,可以声明为final
。public final class Circle extends Shape { // Circle 的具体实现 }
-
密封类 (
sealed
): 子类可以继续是一个密封类,这样它也可以限制哪些类可以继承它。public sealed class Rectangle extends Shape permits Square { // Rectangle 的具体实现 }
-
非密封类 (
non-sealed
): 子类可以声明为non-sealed
,这意味着它不再对继承有任何限制,可以被任意类继承。public non-sealed class Triangle extends Shape { // Triangle 的具体实现 }
密封类的特性
-
继承控制: 密封类可以严格控制哪些类能够继承它,从而避免了不必要的扩展,增强了代码的安全性和可维护性。
-
模式匹配: 密封类与 Java 中的模式匹配功能(如
switch
表达式)配合使用时,可以让编译器检查是否处理了所有可能的子类型,从而减少出现遗漏分支的可能性。 -
设计意图明确: 密封类明确表达了设计意图,指出继承层次结构的边界,有助于阅读和理解代码。
完整示例
以下是一个完整的示例,展示了如何使用密封类以及如何定义其子类:
public sealed class Shape permits Circle, Rectangle, Triangle {
public abstract double area();
}
public final class Circle extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public sealed class Rectangle extends Shape permits Square {
private final double length;
private final double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
public double area() {
return length * width;
}
}
public final class Square extends Rectangle {
public Square(double side) {
super(side, side);
}
}
public non-sealed class Triangle extends Shape {
private final double base;
private final double height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
}
在这个示例中:
Shape
是一个密封类,只允许Circle
,Rectangle
, 和Triangle
继承它。Circle
是最终类 (final
),因此不能再被继承。Rectangle
是一个密封类,且只允许Square
继承它。Square
是一个最终类 (final
),不能再被继承。Triangle
是非密封类 (non-sealed
),因此可以被其他类继续继承。
密封类为开发者提供了更强的控制力,使得类层次结构的设计更加明确和安全。
5.9.5 使用反射在运行时分析对象
在Java中,Field
类是 java.lang.reflect
包的一部分,用于表示类的属性(即字段)。Field
类提供了多个方法来获取和操作对象的字段,其中最常用的是 get()
方法。
Field.get()
方法概述
Field.get()
方法用于从指定对象中获取该字段的值。根据字段的类型,返回相应的对象类型值。如果字段是基本类型(如 int
, boolean
等),返回的是其包装类(如 Integer
, Boolean
等)。
方法签名
Field.get()
方法的签名如下:
public Object get(Object obj) throws IllegalAccessException, IllegalArgumentException
参数
obj
:这是包含该字段的对象实例。如果字段是静态的,obj
可以为null
。
返回值
- 返回字段的值,类型为
Object
。如果字段是基本类型,返回的就是该类型的包装类对象。
异常
IllegalAccessException
: 如果无法访问该字段(例如字段是私有的且未被设为可访问),则抛出此异常。IllegalArgumentException
: 如果obj
参数不表示声明该字段的类或接口的实例,则抛出此异常。NullPointerException
: 如果指定对象为null
,且该字段是实例字段(即非静态字段),则抛出此异常。
使用示例
以下是使用 Field.get()
方法获取对象字段值的一个简单示例:
import java.lang.reflect.Field;
class Person {
public String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Main {
public static void main(String[] args) {
try {
Person person = new Person("Alice", 30);
// 获取 public 字段
Field nameField = Person.class.getField("name");
String nameValue = (String) nameField.get(person);
System.out.println("Name: " + nameValue);
// 获取 private 字段
Field ageField = Person.class.getDeclaredField("age");
ageField.setAccessible(true); // 使私有字段可访问
int ageValue = (int) ageField.get(person);
System.out.println("Age: " + ageValue);
} catch (Exception e) {
e.printStackTrace();
}
}
}
关键点总结
-
访问修饰符的限制:如果要访问一个私有字段,需要先调用
setAccessible(true)
使其可访问。 -
静态字段:如果字段是静态的,那么
get()
方法的参数可以为null
。 -
返回值的类型:
get()
方法返回Object
类型的值,如果你知道具体类型,需要进行类型转换。 -
异常处理:必须处理可能抛出的
IllegalAccessException
和IllegalArgumentException
异常。
Field.get()
方法是Java反射机制中非常重要的一部分,允许你在运行时动态获取对象的字段值,这在编写通用代码或处理不确定类型的对象时非常有用。