前言
Java 作为一门广泛应用的编程语言,其基础内容是深入学习和开发的基石。本文将系统梳理 Java 的基础知识点,帮助初学者快速入门,也为有一定经验的开发者提供复习参考。
一、Java 概述
1. Java 特点
Java 具有诸多显著特性。它是面向对象的语言,将数据和操作封装在对象中,提高了代码的可维护性与可扩展性。同时,Java 是健壮的,异常处理机制让程序在遇到错误时能妥善应对;垃圾自动收集功能减轻了开发者管理内存的负担;强类型机制则保证了数据的安全性和稳定性。
Java 的跨平台性是其一大优势,能在多种操作系统下运行,实现 “一次编写,到处运行”。此外,Java 是解释性语言,不能直接被机器执行,需要解释器将字节码转换为机器码。
2. Java 运行机制及运行过程:跨平台性
Java 程序的运行分两步:首先使用javac
命令进行编译,将.java
源文件编译成.class
字节码文件;然后通过java
命令运行字节码文件。Java 虚拟机(JVM)是 Java 跨平台的关键,针对不同的操作系统,有对应的 JVM 版本(如 JVM for Linux、JVM for Windows、JVM for Mac)。JVM 负责执行指令、管理数据、内存和寄存器,屏蔽了底层平台的差异。
3. JDK 和 JRE
JDK(Java Development Kit)即 Java 开发工具包,是提供给 Java 开发人员使用的工具集,它包含了 JRE 和一系列开发工具,如java
、javac
、javadoc
、javap
等 。JRE(Java Runtime Environment)是 Java 运行环境,由 JVM 和 Java 核心类库(Java SE 标准类库)组成。如果只是运行开发好的 Java 程序(.class
文件),安装 JRE 即可。
4. 注意事项
Java 源文件的扩展名为.java
,类(class)是其基本组成部分。Java 应用程序的执行入口是main()
方法,其标准格式为public static void main(String[] args){...}
。Java 对大小写非常敏感,编写代码时需严格区分。
Java 方法由语句构成,每个语句以分号 “;” 结尾,大括号必须成对出现。一个源文件中最多只能有一个public
类,若有,源文件名必须与该public
类名一致。main
方法也可以写在非public
类中,运行时指定该非public
类即可。
5. 转义字符
转义字符用于表示一些特殊的字符。比如\t
表示一个制表位,可实现文本对齐;\n
是换行符;\\
表示一个反斜杠;\"
表示一个双引号;\'
表示一个单引号;\r
表示回车。
6. 注释
注释用于解释和说明代码,提高代码可读性,且不影响程序的编译和运行。Java 中有单行注释//
,用于注释一行代码;多行注释/* */
,可注释一段代码,但多行注释不允许嵌套;还有文档注释/** */
,主要用于生成 API 文档。
7. DOS 命令(Disk Operating System 磁盘操作系统)
在开发过程中,常使用 DOS 命令操作文件和目录。相对路径从当前目录开始定位,绝对路径从顶级目录(如C:\
或D:\
)开始定位。常用的 DOS 命令有:查看当前目录dir
,如dir d:\abc\test
;切换盘符cd /D
,如cd /D c:
;切换目录cd
,如cd d:\abc\test
、cd..\..\abc\test
;切换到上一级目录cd..
;切换到根目录cd\
;查看指定目录下所有子级目录tree
;清屏cls
;退出 DOSexit
。
二、变量
1. 基本概念
变量是程序的重要组成部分,它相当于内存中一个数据存储空间的标识。变量具有三要素:类型、名称和值。变量的类型决定了它能存储的数据种类和占用的内存空间大小,名称用于在程序中引用该变量,值则是存储在变量中的数据。
2. 变量使用(先声明,后使用)
使用变量前,需先声明变量的类型和名称,如int a;
,声明后可对其进行赋值,如a = 10;
。当+
号左右两边都为数值型时,执行加法运算;当有一方为字符串时,进行拼接运算,例如"hello" + 100 + 3
的结果为hello1003
。
3. 数据类型
Java 的数据类型分为基本数据类型和引用数据类型。基本数据类型类似化学中的基本元素,是构成复杂数据结构的基础;引用数据类型则用于处理更复杂的数据,如类、接口和数组。
基本数据类型包括数值型、字符型char
和布尔型boolean
。数值型又分为整数类型和小数类型。
- 整数类型:包括
byte
(1 字节,范围 -128 ~ 127)、short
(2 字节,范围 -2^15 ~ 2^15 - 1)、int
(4 字节,范围 -2^31 ~ 2^31 - 1)、long
(8 字节,范围 -2^63 ~ 2^63 - 1) 。Java 整型常量默认为int
类型,声明long
型常量时需在值后加l
或L
。计算机中最小存储单位是bit
,基本存储单元是byte
,1 byte = 8 bit。 - 浮点数:有单精度
float
(4 字节,范围 -3.403E38 ~ 3.403E38)和双精度double
(8 字节,范围 -1.798E308 ~ 1.798E308) 。浮点数在机器中的存放形式为浮点数 = 符号位 + 指数位 + 尾数位,由于尾数部分可能丢失,会造成精度损失,浮点数是不准确的。Java 中浮点数默认类型为double
,声明float
型时需加f
或F
。还可以用科学计数法表示浮点数,如5.12e2
表示5.12 * 10^2
,5.12E - 2
表示5.12 * 10^ - 2
。 - 字符类型:
char
用于表示单个字符,占用两个字节,能存放汉字,如char c1 = 'a';
、char c3 = '韩';
。字符常量用单引号(‘’)括起来,char
本质上是整数,对应 Unicode 码,因此可以进行运算,输出时显示的是 Unicode 码对应的字符。 - 布尔类型:
boolean
类型只有true
和false
两个取值,占用 1 个字节,常用于程序流程控制。
4. 字符编码表
字符编码表用于给字符分配唯一的数字编号。常见的字符编码有 ASCII 码,它规定了 128 个字符的编码,最高位为 0,后 7 位表示字符,适用于美国。ISO 8859 - 1(Latin - 1)的 0~127 位与 ASCII 相同,128~255 位中,128~159 表示一些控制字符,160~255 表示一些西欧字符。
Windows - 1252 与 ISO 8859 - 1 基本相同,区别在 128~159,它更全面,取代了 ISO 8859 - 1 编码。GB2312 是中文的第一个标准,针对简体中文常见字符,包含约 7000 个汉字和一些罕用词、繁体字。GBK 在 GB2312 基础上扩展,向下兼容,增加了 14000 个汉字,共约 21000 个汉字,包含繁体字。
GB18030 向下兼容 GBK,增加了 55000 多个字符,共 76000 多个字符,涵盖了很多少数民族字符及中日韩统一字符,采用变长编码。Big5 针对繁体中文,广泛用于台湾和香港等地。
Unicode 给世界上所有字符都分配了唯一编号,范围从 0x000000~0xFFFF,一般写成十六进制并在前面加 U + ,中文范围是 U + 4E00~U + 9FFF,兼容 ASCII 码,英文和汉字都占两个字节。UTF - 32 用四个字节表示字符编号的整数二进制形式;UTF - 16 是变长字节,U + 0000~U + FFFF 用两个字节表示,U + 10000~U + 10FFFF 用四个字节表示;UTF - 8 也是变长字节,1~6 个字节不等,字母占一个字节,汉字占 3 个字节,是使用最广泛的 Unicode 实现方式。
字符编码转换时,先根据原编码格式找到字符的 Unicode 编号,再通过该编号在目标编码的映射表中查找对应的编码格式。乱码通常是由于解析错误或编码转换错误导致的,可以使用 UltraEdit 多次尝试恢复,也可以利用 Java 处理字符串的类String
来解决。
5. 数据类型转换
- 自动类型转化:Java 在赋值或运算时,精度小的数据类型会自动转换为精度大的数据类型 ,顺序为
byte
<short
<char
<int
<long
<float
<double
。表达式结果的类型会自动提升为操作数中最大的类型,byte
、short
、char
在计算时会先转化为int
类型,多种数据混合运算时,会先转化为容量最大的数据类型再进行计算。 - 强制类型转换:这是自动类型转换的逆过程,将容量大的数据类型转化为容量小的数据类型,可能会导致精度降低或溢出,如
int i = (int)1.9;
。强制类型转换只对最近的操作数有效,常使用小括号提升优先级。char
可以保存int
常量值,但不能保存int
变量值,需要进行强转。 - 基本数据类型和 String 类型:基本类型转
String
类型,只需将基本类型的值与空字符串相加,如String str = n + "";
。String
转基本类型,通过基本类型的包装类调用parseXX
方法,如int n = Integer.parseInt(str);
,但要确保String
能转化为有效的数据,否则会抛出异常导致程序终止。
三、运算符
1. 算术运算符
算术运算符用于基本的数学运算。包括正号+
、负号-
、加法+
、减法-
、乘法*
、除法/
、取模(取余)%
、自增++
和自减--
。自增和自减运算符有前置和后置之分,前置(如++i
)是先自增或自减后赋值,后置(如i++
)是先赋值后自增或自减。整数之间做除法时,只保留整数部分,舍弃小数部分,例如int x = 10 / 3
的结果是 3。取模运算a % b = a - a / b * b
。此外,+
号还可用于字符串相加,实现字符串拼接。
2. 关系运算符(比较运算)
关系运算符用于比较两个值,结果为boolean
型,即true
或false
。常见的关系运算符有==
(相等于)、!=
(不等于)、<
(小于)、>
(大于)、<=
(小于等于)、>=
(大于等于)以及instanceof
(检查是否是类的对象)。关系表达式常用于if
结构的条件判断或循环结构的条件控制中。
3. 逻辑运算符
逻辑运算符用于连接多个条件,最终结果也是boolean
值。逻辑与(&)
要求所有条件都为true
时,结果才为true
,否则为false
,且所有条件都需判断完才得出结果。逻辑或(|)
只要有一个条件为true
,结果就为true
,只有所有条件都为false
时,结果才为false
,同样需要判断完所有条件。取反(!)
对变量进行非运算,true
变为false
,false
变为true
。异或(^)
表示两个条件相同为false
,不同为true
。
短路与(&&)
和短路或(||)
是具有短路功能的逻辑运算符。短路与如果第一个条件为false
,后续条件不再判断,结果直接为false
,效率更高;短路或若第一个条件为true
,后续条件也不再判断,结果为true
。
4. 赋值运算符
赋值运算符用于给变量赋值。基本赋值运算符是=
,复合赋值运算符有+=
、-=
、/=
、*=
、%=
等。复合赋值运算符会进行类型转化,且运算顺序从右往左,赋值运算符左边只能是变量。例如,a += b;
等价于a = a + b;
。
5. 三元运算符
三元运算符的基本语法是:条件表达式 ? 表达式 1 :表达式 2 。其运算规则为:当条件表达式为true
时,运算结果为表达式 1;当条件表达式为false
时,运算结果为表达式 2。
6. 运算符优先级
运算符优先级决定了表达式中不同运算符的运算顺序。优先级高的运算符先进行运算,优先级低的后运算。例如,后缀运算符()
、[]
、.
优先级最高,赋值运算符=
、+=
等优先级较低。具体优先级顺序可参考相关表格,同一优先级的运算符,按照规定的关联性(如左到右或从右到左)进行运算。
7. 标识符命名规则规范及关键字
- 标识符命名规则:包名由多个单词组成,所有字母小写,如
aaa.bbb.ccc
;类名和接口名多单词组成时,所有单词首字母大写,采用大驼峰命名法,如XxxYyyZzz
;变量名和方法名多单词组成时,第一个单词首字母小写,后续单词首字母大写,即小驼峰命名法,如xxxYyyZzz
;常量名所有字母大写,多单词时用下划线连接,如XXX_YYY_ZZZ
。 - 关键字:Java 中有许多关键字,用于定义数据类型(如
class
、interface
、enum
、byte
等)、定义数据类型值(true
、false
、null
)、控制程序流程(if
、else
、switch
等)、定义访问权限修饰符(private
、protected
、public
)、定义类和成员的修饰符(abstract
、final
、static
等)、表示类之间的关系(extends
、implements
)、创建和操作对象(new
、this
、super
、instanceof
)、处理异常(try
、catch
、finally
等)以及用于包的管理(package
、import
)等。此外,还有一些保留字,虽尚未使用但可能在未来使用,编程时应避免使用,如byValue
、cast
、future
等。
四、进制
1. 概念
在 Java 中,常用的进制有二进制、十进制、八进制和十六进制。二进制由 0 和 1 组成,满 2 进 1,以0b
或0B
开头;十进制是最常用的进制,由 0 - 9 组成,满 10 进 1;八进制由 0 - 7 组成,满 8 进 1,以数字 0 开头;十六进制由 0 - 9 及 A - F(不区分大小写)组成,满 16 进 1,以0x
或0X
开头 。
2. 源码补码反码
在二进制中,最高位为符号位,0 表示正数,1 表示负数。正数的原码、补码和反码相同;负数的反码是原码符号位不变,其余位取反;负数的补码是反码加 1,反之,负数的反码是补码减 1 。0 的反码和补码都是 0。计算机运算以补码方式进行,但最终运算结果要转换回原码查看。
3. 位运算
位运算直接对二进制位进行操作。按位与(&)
要求两位都为 1 时结果才为 1;按位或(|)
只要有一位为 1,结果就为 1;按位取反(~)
将 1 变为 0,0 变为 1;按位异或(^)
两个位相异为 1,相同为 0。
左移(<<)
是算数左移,向左移动时右边低位补 0,高位舍弃,左移 1 位相当于乘以 2。无符号右移(>>>)
是逻辑右移,向右移动时右边舍弃,左边补 0。有符号右移(>>)
是算术右移,右边舍弃,左边补什么取决于符号位,符号位为 1 则补 1,为 0 则补 0。
五、程序控制结构
1. 顺序控制
顺序控制是程序最基本的执行结构,程序按照代码书写的顺序逐行执行,中间没有判断和跳转。在 Java 中,定义变量时要遵循合法的向前引用原则,即变量在使用前必须先声明。例如:
public class SequentialControl {
public static void main(String[] args) {
int num1 = 5;
int num2 = 10;
int sum = num1 + num2;
System.out.println("两数之和为:" + sum);
}
}
在上述代码中,先声明并初始化了num1
和num2
两个变量,然后计算它们的和并输出结果,完全按照代码的先后顺序依次执行。
2. 分支控制
1)单分支 if
基本语法:
if (条件表达式) {
执行代码块;(可以有多条语句)
}
当条件表达式
的结果为true
时,会执行执行代码块
中的内容;若为false
,则直接跳过该代码块。例如:
public class SingleBranchIf {
public static void main(String[] args) {
int score = 85;
if (score >= 60) {
System.out.println("考试通过!");
}
}
}
2)双分支 if - else
if (条件表达式) {
执行代码块1;
} else {
执行代码块2;
}
如果条件表达式
为true
,执行执行代码块1
;否则,执行执行代码块2
。比如:
public class DoubleBranchIfElse {
public static void main(String[] args) {
int score = 55;
if (score >= 60) {
System.out.println("考试通过!");
} else {
System.out.println("考试未通过,需要努力!");
}
}
}
3)多分支 if - else if - … - else
if (条件表达式1) {
执行代码块1;
} else if (条件表达式2) {
// 这里可以有多个else if
} else {
执行代码块n;
}
程序会依次判断各个条件表达式
,当某个条件满足时,执行对应的执行代码块
,然后跳出整个多分支结构。若所有条件都不满足,则执行else
后的执行代码块n
。示例如下:
public class MultipleBranchIfElseIf {
public static void main(String[] args) {
int score = 78;
if (score >= 90) {
System.out.println("成绩优秀!");
} else if (score >= 80) {
System.out.println("成绩良好!");
} else if (score >= 60) {
System.out.println("成绩中等!");
} else {
System.out.println("成绩较差!");
}
}
}
4)嵌套分支
一个分支中完整地嵌套另一个完整的分支结构。例如:
public class NestedBranch {
public static void main(String[] args) {
int score = 75;
int bonus = 10;
if (score >= 60) {
if (score + bonus >= 90) {
System.out.println("综合评价为优秀!");
} else {
System.out.println("成绩合格,但综合评价未达优秀。");
}
} else {
System.out.println("成绩不合格。");
}
}
}
5)switch 分支结构
基本语法:
switch (表达式) {
case 常量1:
语句块1;
break;
case 常量2:
语句块2;
break;
// 可以有多个case
case 常量n:
语句块n;
break;
default:
default语句块;
break;
}
switch
根据表达式
的值与各个case
后的常量
进行匹配,若匹配成功,则执行对应case
后的语句块
,直到遇到break
语句跳出switch
结构。如果所有case
都不匹配,且有default
分支,则执行default
后的default语句块
。switch
适用于判断具体数值且取值不多的情况,支持的数据类型有byte
、short
、int
、char
、enum
、String
。对于区间判断或结果为boolean
类型的判断,更适合使用if
语句。例如:
public class SwitchBranch {
public static void main(String[] args) {
int day = 3;
switch (day) {
case 1:
System.out.println("今天是星期一");
break;
case 2:
System.out.println("今天是星期二");
break;
case 3:
System.out.println("今天是星期三");
break;
default:
System.out.println("未知的日期");
break;
}
}
}
3. 循环控制
1)for 循环
基本语法:
for (循环变量初始化; 循环条件; 循环变量迭代) {
循环操作;
}
for
循环有四个要素:循环变量初始化、循环条件、循环操作、循环变量迭代。循环开始时,先执行循环变量初始化
,然后判断循环条件
,若条件为true
,则执行循环操作
,接着进行循环变量迭代
,之后再次判断循环条件,如此反复,直到循环条件为false
时结束循环。例如:
public class ForLoop {
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
System.out.println("当前循环次数:" + i);
}
}
}
2)while 循环
基本语法:
循环变量初始化;
while (循环条件) {
循环体;
循环变量迭代;
}
while
循环先进行循环变量初始化
,然后判断循环条件
,若条件为true
,则执行循环体
,执行完循环体
后进行循环变量迭代
,接着再次判断循环条件,直到条件为false
时结束循环。需要注意的是,while
循环是先判断再执行语句。例如:
public class WhileLoop {
public static void main(String[] args) {
int i = 1;
while (i <= 5) {
System.out.println("当前循环次数:" + i);
i++;
}
}
}
3)do…while 循环
基本语法:
循环变量初始化;
do {
循环体(语句);
循环变量迭代;
} while (循环条件);
do…while
循环先执行循环体
,然后进行循环变量迭代
,最后判断循环条件
。无论循环条件是否满足,do…while
循环至少会执行一次循环体
。例如:
public class DoWhileLoop {
public static void main(String[] args) {
int i = 1;
do {
System.out.println("当前循环次数:" + i);
i++;
} while (i <= 5);
}
}
4)多重循环控制
将一个循环放在另一个循环体内,形成嵌套循环。三种循环(for
、while
、do…while
)都可以作为外层和内层循环。嵌套循环中,内层循环会被当作外层循环的循环体,只有内层循环条件为false
时,才会完全跳出内层循环,结束外层的当次循环,开始下一次外层循环。假设外层循环执行m
次,内层循环执行n
次,那么内层循环总共会执行m * n
次。例如:
public class MultipleLoops {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 2; j++) {
System.out.println("i = " + i + ", j = " + j);
}
}
}
}
5)continue 和 break
continue
语句用于结束本次循环,跳过本次循环内后续的操作,直接进入下一次循环。在多层嵌套的循环体中,可以通过标签指明要跳过的是哪层循环。例如:
public class ContinueExample {
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
if (i == 3) {
continue;
}
System.out.println(i);
}
}
}
break
语句用于提前结束循环。比如在数组中查找元素时,如果已经找到目标元素,就可以使用break
提前结束循环。例如:
public class BreakExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5};
int target = 3;
for (int i = 0; i < numbers.length; i++) {
if (numbers[i] == target) {
System.out.println("找到了目标元素,位置为:" + i);
break;
}
}
}
}
return
语句用于跳出所在方法,使程序返回到调用该方法的位置继续执行。
6)foreach 语句
基本语法:
int[] arr = {1, 2, 3, 4};
for (int element : arr) {
System.out.println(element);
}
foreach
语句使用冒号:
,冒号前是循环中的每个元素(包括数据类型和变量名称),冒号后是要遍历的数组或集合。每次循环时,element
会自动更新为数组或集合中的下一个元素。在仅需要简单遍历数组或集合的情况下,foreach
语法更加简洁。
例如,有一个存储整数的数组,我们想要打印出数组中的每一个元素:
public class ForeachExample {
public static void main(String[] args) {
int[] numbers = {10, 20, 30, 40, 50};
for (int number : numbers) {
System.out.println(number);
}
}
}
上述代码中,int number
表示从numbers
数组中依次取出的每一个元素,在每次循环时,number
会自动获取数组中的下一个值,然后执行循环体中的System.out.println(number);
语句,将元素打印出来。
当涉及到对象类型的数组或集合时,foreach
语句同样适用且能简化代码。假设有一个自定义的Student
类:
class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
现在有一个Student
类型的数组,我们想要遍历并输出每个学生的信息:
public class StudentForeachExample {
public static void main(String[] args) {
Student[] students = {
new Student("Alice", 20),
new Student("Bob", 21),
new Student("Charlie", 22)
};
for (Student student : students) {
System.out.println("Name: " + student.getName() + ", Age: " + student.getAge());
}
}
}
在这个例子中,Student student
表示从students
数组中取出的每个Student
对象,通过student
可以方便地访问对象的属性和方法。
需要注意的是,foreach
语句虽然简洁,但它也有一定的局限性。如果在遍历过程中需要对数组或集合进行删除、添加元素等操作,使用foreach
语句可能会导致错误。因为foreach
是基于迭代器的遍历方式,在遍历过程中修改集合结构可能会引发ConcurrentModificationException
异常。这种情况下,使用传统的for
循环会更加合适,因为可以通过索引来精确控制遍历和修改操作。例如:
import java.util.ArrayList;
import java.util.List;
public class ListModifyExample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
// 使用普通for循环删除元素
for (int i = 0; i < numbers.size(); i++) {
if (numbers.get(i) == 2) {
numbers.remove(i);
// 注意:删除元素后,索引需要减1,避免跳过下一个元素
i--;
}
}
System.out.println(numbers);
}
}
而如果使用foreach
语句来尝试删除元素:
import java.util.ArrayList;
import java.util.List;
public class ListModifyWithForeachExample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
// 尝试使用foreach语句删除元素,会抛出ConcurrentModificationException异常
for (Integer number : numbers) {
if (number == 2) {
numbers.remove(number);
}
}
System.out.println(numbers);
}
}
运行上述代码会抛出异常,这体现了foreach
语句在遍历和修改集合时的限制。所以在实际编程中,需要根据具体需求选择合适的遍历方式。
此外,在 Java 8 及之后的版本中,结合流(Stream)API,foreach
的功能得到了进一步拓展。通过流操作,可以更灵活地对集合进行遍历、过滤、映射等操作。例如,对上述Student
数组,想要筛选出年龄大于 20 岁的学生并打印其姓名:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class StudentStreamForeachExample {
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("Alice", 20),
new Student("Bob", 21),
new Student("Charlie", 22)
);
students.stream()
.filter(student -> student.getAge() > 20)
.map(Student::getName)
.forEach(System.out::println);
}
}
在这段代码中,students.stream()
将集合转换为流,filter
方法用于筛选出年龄大于 20 岁的学生,map
方法将筛选后的学生对象映射为其姓名,最后通过forEach
遍历并打印这些姓名。这种方式借助流 API 的链式调用,使代码更加简洁和易读,同时也提高了代码的功能性和表达力。
在嵌套集合的场景下,foreach
语句同样可以发挥作用。比如有一个二维列表(List<List<Integer>>
),想要打印出其中的所有元素:
import java.util.ArrayList;
import java.util.List;
public class NestedListForeachExample {
public static void main(String[] args) {
List<List<Integer>> nestedList = new ArrayList<>();
List<Integer> innerList1 = new ArrayList<>();
innerList1.add(1);
innerList1.add(2);
List<Integer> innerList2 = new ArrayList<>();
innerList2.add(3);
innerList2.add(4);
nestedList.add(innerList1);
nestedList.add(innerList2);
for (List<Integer> innerList : nestedList) {
for (Integer number : innerList) {
System.out.print(number + " ");
}
System.out.println();
}
}
}
这里通过两层foreach
循环,外层循环遍历二维列表中的每一个内层列表,内层循环遍历每个内层列表中的元素,实现了对嵌套集合的遍历和打印。
foreach
语句在遍历集合时还可以结合 Lambda 表达式实现更复杂的操作。比如对一个包含整数的列表,想要计算所有偶数的平方和:
import java.util.Arrays;
import java.util.List;
public class LambdaForeachExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int sumOfSquares = 0;
numbers.forEach(number -> {
if (number % 2 == 0) {
sumOfSquares += number * number;
}
});
System.out.println("所有偶数的平方和为:" + sumOfSquares);
}
}
在这个示例中,通过foreach
结合 Lambda 表达式,在遍历列表元素时进行条件判断和计算,简洁地完成了复杂的业务逻辑。这展示了foreach
语句在 Java 编程中的强大灵活性和实用性,无论是简单的遍历操作还是复杂的业务处理,都能在合适的场景下发挥重要作用。
在使用foreach
遍历集合时,还能借助方法引用来简化代码。例如,有一个字符串列表,想要将每个字符串转换为大写并打印:
import java.util.Arrays;
import java.util.List;
public class MethodReferenceForeachExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "cherry");
words.forEach(String::toUpperCase);
words.forEach(System.out::println);
}
}
这里,String::toUpperCase
是一个方法引用,它告诉foreach
对每个字符串元素调用toUpperCase
方法进行转换。之后再次使用foreach
结合System.out::println
,将转换后的字符串打印出来,使代码更加紧凑和易读。
当遍历的集合元素是自定义类,且该类实现了特定接口时,foreach
可以与接口方法配合使用。假设定义一个Shape
接口和实现该接口的Circle
类、Rectangle
类:
interface Shape {
void draw();
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("绘制圆形");
}
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
现在有一个Shape
类型的列表,使用foreach
遍历并调用每个形状的draw
方法:
import java.util.ArrayList;
import java.util.List;
public class InterfaceForeachExample {
public static void main(String[] args) {
List<Shape> shapes = new ArrayList<>();
shapes.add(new Circle());
shapes.add(new Rectangle());
shapes.forEach(Shape::draw);
}
}
在上述代码中,shapes.forEach(Shape::draw)
使得列表中的每个形状对象都调用自身的draw
方法,实现了对不同形状绘制操作的统一遍历调用,充分体现了面向对象编程中多态的特性与foreach
语句的结合优势。
在处理集合元素间的依赖关系时,foreach
也能派上用场。例如,有一个表示任务依赖关系的列表,每个任务可能依赖于其他任务,任务类定义如下:
import java.util.ArrayList;
import java.util.List;
class Task {
private String name;
private List<Task> dependencies;
public Task(String name) {
this.name = name;
this.dependencies = new ArrayList<>();
}
public void addDependency(Task task) {
dependencies.add(task);
}
public void execute() {
System.out.println("执行任务:" + name);
}
}
假设有多个任务并构建了依赖关系,现在要按顺序执行这些任务(先执行依赖的任务):
public class DependencyForeachExample {
public static void main(String[] args) {
Task task1 = new Task("任务1");
Task task2 = new Task("任务2");
Task task3 = new Task("任务3");
task2.addDependency(task1);
task3.addDependency(task2);
List<Task> tasks = new ArrayList<>();
tasks.add(task3);
tasks.forEach(task -> {
task.dependencies.forEach(Task::execute);
task.execute();
});
}
}
在这段代码中,tasks.forEach
遍历任务列表,对于每个任务,先通过内部的task.dependencies.forEach(Task::execute)
执行其所有依赖任务,然后再执行该任务本身,利用foreach
简洁地处理了任务依赖关系的执行逻辑。
在处理复杂业务逻辑时,foreach
语句结合自定义方法可以让代码逻辑更加清晰。例如,在一个电商系统中,有一个订单列表,订单类包含订单编号、客户信息、订单明细等。现在要对每个订单进行复杂的业务处理,如检查订单状态、计算订单总价、更新库存等。
import java.util.ArrayList;
import java.util.List;
class OrderItem {
private String productName;
private int quantity;
private double price;
public OrderItem(String productName, int quantity, double price) {
this.productName = productName;
this.quantity = quantity;
this.price = price;
}
public double getTotalPrice() {
return quantity * price;
}
}
class Order {
private String orderId;
private String customerName;
private List<OrderItem> orderItems;
public Order(String orderId, String customerName) {
this.orderId = orderId;
this.customerName = customerName;
this.orderItems = new ArrayList<>();
}
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
}
public double calculateTotalPrice() {
return orderItems.stream()
.mapToDouble(OrderItem::getTotalPrice)
.sum();
}
}
public class EcommerceForeachExample {
public static void main(String[] args) {
Order order1 = new Order("1001", "Alice");
order1.addOrderItem(new OrderItem("Book", 2, 20.0));
order1.addOrderItem(new OrderItem("Pen", 5, 5.0));
Order order2 = new Order("1002", "Bob");
order2.addOrderItem(new OrderItem("Notebook", 3, 15.0));
List<Order> orders = new ArrayList<>();
orders.add(order1);
orders.add(order2);
orders.forEach(EcommerceForeachExample::processOrder);
}
private static void processOrder(Order order) {
System.out.println("处理订单:" + order.orderId);
double totalPrice = order.calculateTotalPrice();
System.out.println("订单总价:" + totalPrice);
// 模拟检查订单状态、更新库存等其他业务逻辑
System.out.println("订单状态检查通过,库存更新完成");
System.out.println();
}
}
在上述代码中,processOrder
方法封装了对单个订单的复杂业务处理逻辑。通过orders.forEach(EcommerceForeachExample::processOrder)
,对订单列表中的每个订单进行统一处理,使得代码结构清晰,易于维护和扩展。
在 Java 的集合框架中,foreach
还可以用于遍历不同类型的集合,如Set
和Map
。对于Set
集合,它不允许包含重复元素,遍历方式与List
类似,但顺序可能是无序的(取决于具体的Set
实现类,如HashSet
是无序的,TreeSet
是有序的)。例如:
import java.util.HashSet;
import java.util.Set;
public class SetForeachExample {
public static void main(String[] args) {
Set<String> fruits = new HashSet<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
fruits.forEach(System.out::println);
}
}
在这个例子中,使用HashSet
存储水果名称,通过foreach
遍历并打印出每个水果名称。
对于Map
集合,它存储的是键值对(key - value pairs)。foreach
遍历Map
时,可以使用entrySet()
方法获取键值对集合,然后分别访问键和值。例如:
import java.util.HashMap;
import java.util.Map;
public class MapForeachExample {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 85);
scores.put("Charlie", 95);
scores.forEach((key, value) -> System.out.println(key + "的分数是:" + value));
}
}
在上述代码中,scores.forEach((key, value) -> System.out.println(key + "的分数是:" + value))
使用foreach
遍历scores
这个Map
集合,key
代表学生姓名,value
代表学生分数,通过 Lambda 表达式打印出每个学生的姓名和分数。
此外,foreach
在处理并行流(Parallel Stream)时也有独特的表现。并行流可以利用多核处理器的优势,提高遍历和处理集合元素的效率。例如,有一个包含大量整数的列表,要对每个整数进行平方操作并求和:
import java.util.Arrays;
import java.util.List;
public class ParallelStreamForeachExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
long sumOfSquares = 0;
sumOfSquares = numbers.parallelStream()
.mapToLong(n -> n * n)
.sum();
System.out.println("平方和为:" + sumOfSquares);
}
}
在这段代码中,numbers.parallelStream()
将列表转换为并行流,mapToLong(n -> n * n)
对每个元素进行平方操作,sum()
计算平方和。使用并行流可以在处理大规模数据时显著提高计算速度,但需要注意线程安全问题,因为并行处理可能会导致多个线程同时访问和修改共享资源。
六、数组、排序、查找
1. 基本概念
数组是一种引用类型的数据类型,可用于存放多个同一类型的数据。其定义方式有多种,例如:
// 声明并创建一个能存放5个整数的数组
int[] arr1 = new int[5];
// 先声明,后创建
int[] arr2;
arr2 = new int[10];
// 直接初始化数组
int[] arr3 = {1, 2, 3};
int[] arr4 = new int[]{1, 2, 3};
需要注意,数组不能在给定初始值的同时给定长度,像int[] arr = new int[3]{1, 2, 3};
这种写法是错误的。数组中的元素可以是任何数据类型,但不能混用。数组创建后若未赋值,会有默认值,如int
、short
、byte
、long
类型的默认值是0
,float
、double
类型的默认值是0.0
,char
类型的默认值是\u0000
,boolean
类型的默认值是false
,String
类型的默认值是null
。数组属于引用类型,数组型数据是对象(object
),在默认情况下是引用传递,赋值时传递的是地址,在内存中,栈中存放数据地址,堆中存放数据内容。
2. 排序
排序分为内部排序和外部排序。内部排序是将所有需要处理的数据都加载到内部存储器中进行排序,常见的内部排序算法包括交换式排序法(如冒泡排序)、选择式排序法和插入式排序法。外部排序则是在数据量过大,无法全部加载到内存中时,借助外部存储进行排序,例如合并排序法和直接合并排序法 。
以冒泡排序法为例,它通过对排序序列从后向前(从下标较大的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部。以下是冒泡排序的代码实现:
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {64, 34, 25, 12, 22, 11, 90};
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换arr[j]和arr[j + 1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
for (int num : arr) {
System.out.print(num + " ");
}
}
}
3. 查找
常见的查找算法有顺序查找和二分查找。顺序查找是从数组的第一个元素开始,逐个与目标元素进行比较,直到找到目标元素或遍历完整个数组。二分查找则要求数组是有序的,它通过不断将数组分成两部分,比较中间元素与目标元素的大小,来缩小查找范围,从而提高查找效率。
以下是顺序查找的代码示例:
public class SequentialSearch {
public static int sequentialSearch(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i;
}
}
return -1; // 表示未找到
}
public static void main(String[] args) {
int[] arr = {10, 20, 30, 40, 50};
int target = 30;
int result = sequentialSearch(arr, target);
if (result != -1) {
System.out.println("目标元素在数组中的索引为:" + result);
} else {
System.out.println("未找到目标元素");
}
}
}
二分查找的代码示例:
public class BinarySearch {
public static int binarySearch(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // 表示未找到
}
public static void main(String[] args) {
int[] arr = {10, 20, 30, 40, 50};
int target = 30;
int result = binarySearch(arr, target);
if (result != -1) {
System.out.println("目标元素在数组中的索引为:" + result);
} else {
System.out.println("未找到目标元素");
}
}
}
4. 二维数组
二维数组可以看作是数组的数组,其语法如下:
// 方式一
int[][] a = new int[2][3];
// 方式二
int[][] b = new int[2][];
// 方式三
int[][] c = {{1, 1, 1}, {2, 2}, {3}};
二维数组的声明方式有int[][] arr;
、int[] arr[];
、int arr[][];
这几种。在使用二维数组时,可以通过两个索引来访问其中的元素,例如a[0][1]
表示访问a
数组中第一行第二列的元素 。二维数组在处理矩阵运算、表格数据等场景中非常有用。例如,计算一个二维数组(矩阵)的转置:
public class TransposeMatrix {
public static void main(String[] args) {
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
int rows = matrix.length;
int cols = matrix[0].length;
int[][] transpose = new int[cols][rows];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
transpose[j][i] = matrix[i][j];
}
}
for (int i = 0; i < cols; i++) {
for (int j = 0; j < rows; j++) {
System.out.print(transpose[i][j] + " ");
}
System.out.println();
}
}
}
在上述代码中,首先定义了一个二维数组matrix
,然后创建了一个转置后的二维数组transpose
,通过两层循环将原矩阵的行和列进行交换,实现矩阵的转置,并打印出转置后的矩阵。