叠甲:以下文章主要是依靠我的实际编码学习中总结出来的经验之谈,求逻辑自洽,不能百分百保证正确,有错误、未定义、不合适的内容请尽情指出!
文章目录
- 1.第一份程序
- 1.1.代码编写
- 1.2.代码运行
- 1.2.1.命令行编译
- 1.2.2.IEDA 编译
- 1.3.代码文档
- 2.运行过程
- 3.关键字
- 4.标识符
- 5.常量与变量
- 5.1.数据类型
- 5.1.1.基本类型
- 5.1.2.引用类型
- 5.2.数据的量
- 5.2.1.字面常量
- 5.2.2.数据变量
- 6.类型转化
- 6.1.隐式类型转换
- 6.2.显式类型转换
- 6.3.数据类型提升
- 7.运算符
- 8.控制语句
- 8.1.分支语句
- 8.1.1.if 语句
- 8.1.2.switch 语句
- 8.2.循环语句
- 8.2.1.for 语句
- 8.2.2.while 语句
- 8.2.3.do-while 语句
- 9.输入输出
- 9.1.输出数据
- 9.2.输入数据
- 10.随机数
- 11.方法
- 11.1.方法的定义
- 11.2.方法的参数
- 11.3.方法的递归
- 11.4.方法的重载
- 12.数组
- 12.1.数组的使用
- 12.2.数组的引用
- 12.3.数组的传递
- 12.4.数组的拷贝
- 13.字符串
- 14.IDEA 快捷键
概要:…
资料:…
1.第一份程序
1.1.代码编写
/* (块注释)
HelloWord.java 内部
*/
/** (文档注释)
* 作者:limou3434
*/
public class HelloWord {
public static void main(String[] args){
System.out.println("Hello Word!"); // (普通注释)打印“Hello Word!”
}
}
直接上代码,上面就是一段 Java 代码,我们先来观察一下这段代码的细节:
-
首先看到的是一个
main()函数(每一个类只能有一个main()),主函数没有所谓的返回值,因此返回类型是void,在main()的内部有一个字符数组类型的args变量,用来接收命令行参数 -
在一个
Java文件中只能有一个public类,并且pubilc类的名字必须和.java文件名字相同。而main()函数被包含在这个类中,不允许被写在类外 -
在
main()内部,有一个调用,调用了打印函数println(),这个函数后面的ln指的是换行,打印函数内部包含了需要被打印的内容 -
在
Java0中有三种注释:行注释、段注释、文档注释,这里故意都使用了一遍
1.2.代码运行
那么如何编译这一份代码呢?我将带您用两种方式进行运行,一种是脱离 IDE 环境的命令行运行,另一种就是在 IDEA 中运行。这两种方法您都需要掌握…
1.2.1.命令行编译
-
在最原始的情况下只需要将上述代码写入
HelloWord.java文件中(注意文件名一定要使用大驼峰,实际上在Java中一个文件就对应一个类,一般直接使用类名来命名文件) -
然后通过使用
Java的编译器javac.exe这个可执行程序,使用命令javac ./HelloWord.java(注意需要正确写好Hellow.java的相对路径或者绝对路劲) -
此时在
HelloWord.java的同级目录中就存在一个经过编译的字节码文件HelloWord.class -
运行
Java代码代码直接使用Java HelloWord即可 -
需要注意的是,
Java不是单个文件就生成单个字节码的文件,而是有多少个类就有多少个字节码文件,并且字节码文件名和类相同,这点和很多编程语言有很大的不同。
注意:这里如果因为添加了中文注释导致无法通过编译,则可以尝试在编译的时候设置某个编码运行,例如
javac -encoding utf-8 HellowWord.java就可以使用utf-8来进行编码。而关于命令行中的其他操作,可以查阅其他资料…
1.2.2.IEDA 编译

1.3.代码文档
Java 有一个非常独特的功能,给代码生成文档,使用 javadoc -d [存放文档的目录路径] -sourcepath [存放源代码的目录路径] [-docencoding UTF-8 -charset UTF-8] [指定需要生成文档的 .java 文件]> 即可生成一个 html 帮助文档。
不过这种注释需要搭配一些特殊的注释,这些注释的使用和意义有点类似 C/Cpp 的 Doxygen 注释,主要有以下特殊注释(这里只举例常用的)。
- 类和接口文档:使用
/** ... */放在类或接口声明之前,描述类或接口的用途和功能 - 成员变量文档:使用
/** ... */放在成员变量声明之前,描述变量的用途 - 成员方法文档:使用
/** ... */放在方法声明之前,描述方法的功能、参数、返回值和可能抛出的异常 - 参数文档:使用
@param标记在方法文档中,为每个参数提供描述 - 返回值文档:使用
@return标记在方法文档中,描述方法的返回值 - 异常文档:使用
@throws标记在方法文档中,描述方法可能抛出的异常及其条件 - 版本和作者文档:使用
@version标记来记录类或接口的版本信息,使用@author标记来记录代码的作者 - 自描述:使用
@see标记来引用其他主题或URL,在最后文档生成中会达到跳转的目的 - 弃用文档:使用
@deprecated标记来标明某个API成员(类、方法、字段等)已过时,一般添加到整个注释文档的最后即可 - 序列化文档:使用
@serial标记来描述序列化字段
以下是一些示例 javadoc 的注释,以后遇到我也会使用上其他的注释。
// 使用 javadoc 注释
/**
* 表示一个简单的计算器类。
*/
public class Calculator {
/**
* 计算两个整数的和。
*
* @param a 第一个加数。
* @param b 第二个加数。
* @return 两个数的和。
*/
public int add(int a, int b) {
return a + b;
}
/**
* 这个字段表示计算器的版本。
*
* @version 1.0
* @author John Doe
*/
private String version;
}
这也可以根据需要使用不同的注释标记组合来生成详细的 API 文档。
2.运行过程
在我们安装 JDK(Java 开发开发工具包)的时候
JDK里包含JRE(Java运行时环境)JRE里包含JVM(Java所需虚拟机)
.java 后缀的 Java 文件使用 javac 编译成 .class 后缀的字节码文件,字节码文件再通过不同操作系统实现的具体 JVM 虚拟机转化为机器码运行起来(因此 Java 是半解释半编译的语言)。
因此哪怕是其他语言,如果能被转化成字节码并且运行的操作系统上实现了对应的 JVM,也同样可以在虚拟机上运行。通过中间层来达到 一次编译到处运行 的目的,使得 Java 在跨平台能力要优于 C/Cpp。
3.关键字
有些关键字被 Java 所保留,不可以给用户创建标识符来使用,这些关键字的类别有很多,例如:int、class、catch 等等,我们后面再来一一介绍。
4.标识符
在 Java 中可以将类名、对象名、变量名、方法名称为“标识符”。Java 的标识符可以包含:字母、数字、下划线、$ 符号等。
不过需要注意的是不可以使用数字作为标识符的起始字符,但是可以把 $ 作为标识符的开头(但是不建议)。
在命名的时候,不仅要注意命名合法,还要注意合理。在本系列文章中我统一采用:
-
类名:大驼峰
-
方法名:小驼峰
-
变量名:小驼峰
吐槽:从编译原理的角度来看,实际上关键字就是一种特殊的标识符。
5.常量与变量
在提及常量和变量的时候,就需要先提及数据类型,注意这里和 C/Cpp 有很大的不同之处。
5.1.数据类型
Java 的数据类型分为两种:基本类型和引用类型。
- 其中基本数据类型有四类八种
- 引用类型通常指类(
class)、接口(interface)、数组(array)等
5.1.1.基本类型
四类即:
-
整型(整数)
-
浮点型(小数)
-
字符型(一个字符)
-
布尔类型(
true和false,对应对和错,和整型没关系)
八种即:
| 数据类型 | 关键字 | 内存占用 | 范围 |
|---|---|---|---|
| 字节型 | byte | 1 字节 | [ − 128 , 127 ] [-128,127] [−128,127] |
| 字符型 | char | 2 字节 | [ 0 , 65535 ] [0,65535] [0,65535] |
| 短整型 | short | 2 字节 | [ − 32768 , 32767 ] [-32768,32767] [−32768,32767] |
| 整型 | int | 4 字节 | [ − 2 31 , 2 31 − 1 ] [-2^{31},2^{31-1}] [−231,231−1] |
| 长整型 | long | 8 字节 | [ − 2 63 , 2 63 − 1 ] [-2^{63},2^{63}-1] [−263,263−1] |
| 单精度浮点数 | float | 4 字节 | 有,但是不关注 |
| 双精度浮点数 | double | 8 字节 | 有,但是不关注 |
| 布尔型 | boolean | 无说明 | true 和 false |
Java 的数据类型是固定的不会受平台影响,因此很方便做代码移植。
吐槽:
C/Cpp在不同平台,甚至在同平台的不同编译器上,对于类型的限定都可能会不一样,尤其是在32/64平台上有两种解释,因为标准只规定了大致范围和限定,剩下的几乎都交给了编译器来实现。
5.1.2.引用类型
引用类型主要依赖引用这一区别于 C/Cpp 指针的机制,后面再来详细解释,您先简单看作类(class)、接口(interface)、数组(array)等创建的类型即可。
5.2.数据的量
有了数据类型,就可以创建出对应的容器来指代某些量,这些量可以被分为字面常量和数据变量。
5.2.1.字面常量
数据类型可以给字面常量(数据)做出分类。
类似 100、3.14、"abcdef"、false 等这种一眼就能看出数据的都是字面常量,字面常量的类别也是根据数据类型来分类的。
其中 100 就是整型常量、3.14 就是浮点数常量、"abcdef" 就是字符串常量、构成字符串的每一个字符 a、b、c…就是字符常量、false 和 true 就是布尔常量。
5.2.2.数据变量
变量可以理解为一个容器,可以用来存储一个常量,不同类别的常量需要靠不同类别的变量来存储。
而我们需要用一些关键字(也就是前面在数据类型中提到的),使得变量变成只存储某一字面常量类型的变量。
// 定义变量的语法形式
int a = 10; // int 是关键字, a 是标识符, 标记一个变量, 10 为字面常量
// 此时变量 a 存储了 10 这个字面量
补充:在数据变量上,
Java和C/Cpp有很大的不同。
Java没有全局变量这个术语的,但有类似的。- 在
Java编程中如果没有对变量初始化就使用的话,很多编译器直接就会报错,编译也不会通过。- 如果赋值给变量过大或者过小的值,
Java也是不会通过编译的。
补充:
Java的字符串类型是包装类型,不是基本数据类型。为了方便后续一些代码的使用,这里提前讲解一下字符类的相关概念。
C没有字符类型,但是Java利用类的优势,使用String类定义字符串类型。// 使用 String 类 public static viod main() { String s1 = "Hello"; String s2 = "I am limou3434"; String s3 = "100"; System.out.prinln(s1 + s2); //String 转 int int number = Inreger.parseInt(str); //int 转 String String s = String.valueOf(number); }如果您学过
Cpp其实您能很快理解什么是包装类,您可以简单理解为基本数据类型被包装为一个类类型,这么做的好处是统一参数。而由于Java是纯粹的面向对象语言,因此传递参数的时候,大部分使用的是类的实例化对象,将基本数据类型就被包装为包装类,供程序传递参数使用时就会更加方便。
6.类型转化
需要注意的是,Java 是强类型语言,有些不合适的类型转化将会直接报错(C/Cpp 语言是弱类型语言,在这方面会宽松很多)。
6.1.隐式类型转换
代码不需要经过处理,编译器会进行处理不同类型的转换,但是 Java 在这一方面检查得比较严格,不允许溢出和不相关类型转化。
// 使用隐式类型转化
public class Main {
public static void main(String[] args) {
int a = 10;
long b = 100L;
b = a;// 可以,发生了隐式类型转化, a 从 int 变成 long
// a = b; // 不可以, 不够存
float f = 3.14F;
double d = 5.12;
d = f; // 可以, f 从 float 转化为 double
// f = d; // 不可以, 不够存
}
}
6.2.显式类型转换
这方面类似 C 语言,也是使用 () 来转换,但并不总是能成功,小范围可以转化为大范围的,赋值的数值一定不能溢出,且强制转换的类型要相关且合理。
// 使用显示类型转化
public class Main {
public static void main(String[] args) {
int a = 10;
long b = 100L;
b = a; // 可以, 发生了隐式类型转化, a 从 int 变成 long
a = (int)b; // 可以, 将 b 从 long 强制转化为 int
float f = 3.14F;
double d = 5.12;
d = f; // 可以, f 从 float 转化为 double
f = (float)d; // 可以, 将 d 从 double 前置转化为 float
}
}
6.3.数据类型提升
在不同变量的运算的运算中,Java 也存在整型提升,和 C 基本差不多。需要注意是 Java 对溢出极其敏感(C 对待溢出十分迟钝),提升后需要赋给对应的容器,除非进行强转,否则会报错。
//查看整型提升现象
public class Main {
public static void main(String[] args) {
byte a = 127;
byte b = 127;
// byte c = (byte)(a + b); // a 和 b 提升为 int 进行计算,结果也为 int, 虽然可以强转为 byte, 但很危险
int d = a + b; // 这样才比较好
}
}
7.运算符
这里我们只挑出几个比较特殊的运算符,其余的运算符和 C 基本差不多(包括“结合性”和“优先级”),这里就不再赘述。
-
Java的除法和C类似,会向下取整,并且除以0会抛出异常ArithmeticException(算数异常)// 使用除法的示例 public class Main { public static void main(String[] args) { System.out.println(5 / 2); // 2 System.out.println(5.0 / 2); // 2.5 System.out.println(5 / 2.0); // 2.5 System.out.println((float)5 / 2); // 2.5 System.out.println(5 / (float)2); // 2.5 System.out.println((float)(5 / 2)); // 2.0 } } -
由于除法一样,所以
%运算也是一样的,需要注意的是,该运算符也可以对小数进行操作(就是很少用)// 使用取模的示例 public class Main { public static void main(String[] args) { System.out.println(10 % 3); // 1 System.out.println(-10 % 3); // -1 System.out.println(10 % -3); // 1 System.out.println(-10 % -3); // -1 System.out.println(-10.3 % 3); // -1.3000000000000007 } } -
在增量运算符中有的时候会发生类型转化,等价于强转
// 使用增量运算符发生隐式强转 public class Main { public static void main(String[] args) { int num1 = 10; double num2 = 3.14; int add1 = 0; double add2 = 0.0; // 方式一 // add1 = num1 + num2; // 4 字节和 8 字节相加,发生整形提升, 只用 4 个字节是放不下去的 // System.out.println(add1); add2 = num1 + num2; System.out.println(add2); System.out.println(); // 方式二 add1 = (int) (num1 + num2); System.out.println(add1); add2 = (double) (num1 + num2); System.out.println(add2); System.out.println(); // 方式三 num1 += num2; // 等价于 a = (int)(a + b) System.out.println(num1); } }而自增、自减运算符还有一种特殊的情况需要您注意,您应该避免出现下面这样的代码…
// 有关于加加和减减的一个很坑的点 public class Main { public static void main(String[] args) { int a = 10; a = a++; System.out.println(a); // 结果为 10,而不是 11,这个原因涉及底层 // 以后我有机会再来填坑,这里充分体现了 Java 和 C/C++ 不一样! } } -
关系运算符、逻辑运算符(也具有短路效应)的操作表达式必须为布尔表达式,其运算结果为
true和flase,不是非0和0。而if和for判断的环节中使用的也是布尔表达式 -
Java的移位操作符有三个<<、>>、>>>,>>是左补符号位,>>>是左补0 -
也有一个唯一的三目操作符:条件运算符
表达式1 ? 表达式2 : 表达式3。并且该表达式必须是被使用的(其结果必须被使用),不能单独存在 -
对于位操作,建议还是使用括号引导表达式的逻辑,避免出现意想不到的后果(在
C中也最好一样)
注意:如果摒弃掉
C的“非零为真”的概念在接下来的编码中会轻松很多…
8.控制语句
和大部分编程语言类似,Java 也有自己的控制语句,和 C 也有些类似,但是在入口判断有很大的不同,不同的根源来自于:C 使用整数的非零和 0 来判断真假,而 Java 完全使用布尔类型来判断真假,而 Java 的布尔类型和整数是无法进行比较的,这就导致 Java 写出来的入口判断会更加清晰(也可以说没有 C/C++ 那样整洁,至于是清晰好还是简洁好,看您喜好而定)
8.1.分支语句
8.1.1.if 语句
Java 的 if 语句,几乎和 C/C++ 的使用一样,并且也有类似的悬挂 else 的问题,真要说有哪些点不同,就是 C/C++ 和 Java 的代码缩进风格不太一样。
//使用 if 语句
public class Main {
public static void main(String[] args) {
int a = 1;
if (a == 1) { //这里只能是整型
System.out.println(a);
} else {
System.out.println("a != 1");
}
}
}
8.1.2.switch 语句
同样,几乎和 C 一样,就是需要注意的是:switch 的入口只能使用 char、byte、short、int、Character、Byte、Short、Integer、String、enum 类型,其他类型一概不支持(比如 long 就不行),这点很重要。
//使用 switch 语句
public class Main {
public static void main(String[] args) {
int a = 1;
switch (a) { //这里只能是整型
case 1:
System.out.println(a);
break;
case 2:
System.out.println(a);
break;
default:
System.out.println("default");
}
}
}
8.2.循环语句
8.2.1.for 语句
//使用 for 语句
public class Main {
public static void main(String[] args) {
for (int a = 1; a < 10; a++) {
System.out.println(a);
}
}
}
8.2.2.while 语句
//使用 while 语句
public class Main {
public static void main(String[] args) {
int a = 1;
while (a < 10) {
System.out.println(a);
a++;
}
}
}
8.2.3.do-while 语句
//使用 do-while 语句
public class Main {
public static void main(String[] args) {
int a = 1;
do {
System.out.println(a);
a++;
} while (a < 10);
}
}
让我们借助循环语句,顺便来讲解一下在 IDEA 中如何调试代码:

9.输入输出
9.1.输出数据
Java 有三种常用的输出,均体现在下述代码中:
// 使用三种输出语句
public class Main {
public static void main(String[] args) {
System.out.print("limou"); //输出,但不换行(无需关注类型)
System.out.println("limou"); //输出,但是换行(无需关注类型)
System.out.printf("%s", "limou"); //格式化输出输出
}
}
您可能会好奇格式化输出是否和 C 一样,实际上有些类似,也有些不同:
%o、%d、%x:整数的八进制、十进制、十六进制输出%f、%e、%g、%a:定点浮点数、指数浮点数、通用浮点数、十六进制浮点数输出%s、%c:字符串、字符输出%b:布尔值输出%h:散列码输出%%:百分号输出
此外,也有一些修饰符,您稍微了解一下即可。
9.2.输入数据
// 使用输入语句
// 导入相关的包(可以将光标停在对应关键字上,使用快捷键 [alt+enter] 来快速导入)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// 创建一个 scan 对象, 且 System.in 代表设置为“从键盘输入”
Scanner scan = new Scanner(System.in);
System.out.println("请输入您的年龄"); // 提示用户输入
int age = scan.nextInt(); // 通过方法获取输入
System.out.println("请输入您的名字"); // 提示用户输入
String name = scan.next(); // 通过方法获取输入(会忽略一开始的空白字符, 再次遇到空白字符就会停下)
// 使用 nextLine() 则会获取空白字符, 但是有可能有失效的问题, 也就是多余空白字符被误输入的问题
String ch = scan.nextLine(); // 先除去上述输入得最后产生得换行符
System.out.println("输入您的爱好");
String hobby = scan.nextLine(); // 忽略一开始的空白字符(除了换行字符), 获取连续的字符串, 包括空白字符(除了换行字符)
System.out.println("请输入您的体重"); // 提示用户输入
float weight = scan.nextFloat(); // 通过方法获取输入(会忽略空白字符)
System.out.println("年龄:" + age); // 输出信息
System.out.println("名字:" + name); // 输出信息
System.out.println("爱好:" + hobby); // 输出信息
System.out.println("体重:" + weight); // 输出信息
scan.close(); //类似 C 语言的文件关闭
}
}
/* 输出结果
请输入您的年龄
18
请输入您的名字
limou 3434
输入您的爱好
game and programme
请输入您的体重
51.2
年龄: 18
名字: limou
爱好: game and programme
体重: 51.2
*/
注意:关于
Scanner还有很多的知识,我将会在IO详细讲解。
当然,一般建议文本输入最好放在最前面处理(尤其是多数据类型输入的时候)。还有一个循环输入的代码也值得您一看。
// 多组输入
// 导入相关的包(可以将光标停在对应关键字上,使用快捷键 [alt+enter] 来快速导入)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.print("请输入一个数字 ");
while (scan.hasNextInt()) {
int number = scan.nextInt();
System.out.println("您输入的数字是:" + number);
System.out.print("请输入一个数字 ");
}
// 在终端中使用 [ctrl+d] 会终止循环
}
}
/* 输出结果
请输入一个数字 18
您输入的数字是: 18
请输入一个数字 20
您输入的数字是: 20
请输入一个数字 35
您输入的数字是: 35
请输入一个数字 5
您输入的数字是: 5
请输入一个数字 100
您输入的数字是: 100
请输入一个数字
...
*/
10.随机数
Java 的随机数生成也比较简单,使用如下代码即可:
// 随机数生成
// 导入相关的包(可以将光标停在对应关键字上,使用快捷键 [alt+enter] 来快速导入)
import java.util.Random;
public class Main {
public static void main(String[] args) {
int count = 0;
Random random1 = new Random();
while (count < 10) { // 循环打印 10 次查看随机数
int n = random1.nextInt();
System.out.println(n);
count++;
}
Random random2 = new Random();
while (count > 0) { // 循环打印 10 次查看随机数
int n = random2.nextInt(100); // 限定 [0, 100) 的随机数
System.out.println(n);
count--;
}
}
}
/* 输出结果(随机的)
1341966210
210008845
453512808
804932370
28871118
-913616368
469568144
-904536397
190689066
-546299782
69
65
21
54
6
83
0
1
81
41
*/
还有一个数学库的随机数,您也可以去了解一下…
11.方法
11.1.方法的定义
方法和 C 中的函数是否类似(因为它们的工作是差不多的,都是通过调用来达到简化代码的目的),而为什么不继续延用“函数”这个术语,而使用“方法”呢?
简单来说就是类的出现导致的,Java 使用类来创建一个又一个的对象,这些对象很类似普通的变量,而类内写入的对象可以执行的方法,这样创建出一个对象就可以使用配套的对象方法,这些内容我将会在下一节的类中重新阐述(现在您把方法简单视为函数)。
// 方法的使用
public class Main {
public static void main(String[] args) {
System.out.println(add(1, 2));
}
public static int Add(int a, int b) {
return a + b;
}
}
/* 输出结果
3
*/
另外,方法不能嵌套定义,一个方法的内部是不能定义另外一个方法的。
方法在类内是全局的,也就是说无论把 Add() 写到类的哪里,类内的 main() 都可以执行该方法。
11.2.方法的参数
Java 的方法也有和 C 函数类似的形参和实参的问题,但由于 Java 没有 C/C++ 的指针,如果传递一个参数过来,该怎么进行修改呢?
//形参和实参的一个问题
public class Main {
public static void main(String[] args) {
int num1 = 5, num2 = 10;
System.out.println("交换方法一");
System.out.println("交换前 num1:" + num1 + " " + "num2:" + num2);
int tmp = num1;
num1 = num2;
num2 = tmp;
System.out.println("交换后 num1:" + num1 + " " + "num2:" + num2);
System.out.println("交换方法二");
System.out.println("交换前 num1:" + num1 + " " + "num2:" + num2);
Swap(num1, num2);//使用方法交换(交换失败)
System.out.println("交换后 num1:" + num1 + " " + "num2:" + num2);
}
public static void Swap(int num1, int num2) {
int tmp = num1;
num1 = num2;
num2 = tmp;
}
}
/*
交换方法一
交换前 num1: 5 num2: 10
交换后 num1: 10 num2: 5
交换方法二
交换前 num1: 10 num2: 5
交换后 num1: 10 num2: 5
*/
可以采用数组的方法来规避这一问题。
// 使用数组的引用达到目的
public class Main {
public static void main(String[] args) {
int[] nums = {5, 10};
System.out.println("交换前 num1:" + nums[0] + " " + "num2:" + nums[1]);
swap(nums, 0, 1);
System.out.println("交换后 num1:" + nums[0] + " " + "num2:" + nums[1]);
}
public static void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
11.3.方法的递归
除此之外,Java 也支持递归调用方法。
//使用方法递归计算阶乘
public class Main {
public static void main(String[] args) {
int number = 5;
System.out.println(Test(number));
}
public static int Test(int number) {
if (number == 0 || number == 1) {
return 1;
} else if (number < 0) {
return -1;
}
return Test(number - 1) * number;
}
}
/* 输出结果
120
*/
11.4.方法的重载
函数重载和 C++ 的重载类似,也就是说 Java 允许类内一个方法可以有多种实现。这些方法实现的方法名字是一样的,并且都 在同一个类内,但参数列表是不一样的(体系在 参数个数 或 参数顺序 不一样,但是不包括返回值不一样)。
// 需要使用方法重载的例子
public class Main {
public static void main(String[] args) {
int number1 = 5;
System.out.println(Test(number1));
double number2 = 5.0;
System.out.println(Test(number2)); // 无法调用 Test()
}
public static int Test(int number) {
if (number == 0 || number == 1) {
return 1;
} else if (number < 0) {
return -1;
}
return Test(number - 1) * number;
}
}
// 为 Test() 提供重载版本
public class Main {
public static void main(String[] args) {
int number1 = 5;
System.out.println(Test(number1));
double number2 = 5.0;
System.out.println(Test(number2)); // 成功调用 Test() 的重载版本
}
public static int Test(int number) {
if (number == 0 || number == 1) {
return 1;
} else if (number < 0) {
return -1;
}
return Test(number - 1) * number;
}
public static double Test(double number) {
if (number == 0 || number == 1) {
return 1;
} else if (number < 0) {
return -1;
}
return Test(number - 1) * number;
}
}
/* 输出结果
120
120.0
*/
12.数组
12.1.数组的使用
C/C++ 诡异的数组、指针创建风格曾折磨过不少初入门的家伙们,而 Java 的数组创建在这里和 C/C++ 有很大的不同,并且有更加便捷的操作。
// 尝试使用数组
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
// 1.创建数组
// 创建静态数组: 数组类型 数组名 = { 元素列表 };
// 创建动态数组: 数组类型 数组名 = new 数组类型 { 元素列表 }
// 无论是静态创建还是动态创建, 其实最终数组元素都会存储到堆上
// (1)创建一维数组
int[] arr1 = { 3, 2, 1 };
int[] arr2 = new int[]{ 3, 2, 1 };
int[] arr3 = new int[3]; /* 空数组默认元素都为 0 */
// (2)创建二维数组
int[][] arr4 = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12} };
int[][] arr5 = new int[][] { {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12} };
int[][] arr6 = new int[4][3]; /* 空数组默认元素都为 0 */
// 初始化的元素列表只有在定义阶段使用, 不能先定义然后另起一行进行初始化元素列表
// 如果希望定义出一个空数组, 那么需要携带部分值供编译器识别
// 数组类型 数组名 = new 数组类型[量][...]
// 这里的量可以是常量也可以是变量
// 2.遍历数组
// (1)使用普通 for 循环
for (int i = 0; i < arr1.length; i++) { // 属性 array.length 用来获取数组的长度
System.out.print(arr1[i] + ", ");
}
System.out.println();
// (2)使用增强 for 循环 foreach(Cpp 中的范围 for)
for (int index : arr2) {
System.out.print(index + ", ");
}
System.out.println();
// (3)使用数组类打印
System.out.println(Arrays.toString(arr4)); // 关于 Arrays 后续再来提及
System.out.println(Arrays.deepToString(arr5));
// 3.排序数组
System.out.println("排序后");
// (1)全局排序
Arrays.sort(arr1);
System.out.println(Arrays.toString(arr1));
// (2)局部排序
Arrays.sort(arr2, 0, 2); // 排序范围是 [0, 2)
System.out.println(Arrays.toString(arr2));
/* 逆向排序有些麻烦, 后面再提及 */
// 4.填充数组
System.out.println("填充前" + Arrays.toString(arr3)); // 填充前
Arrays.fill(arr3, 1, 3, -1); // 从 [1, 3) 的范围中填充 -1 这个数字
System.out.println("填充后" + Arrays.toString(arr3)); // 填充后
// 5.查找数组
System.out.println("在数组 arr5[3] 中, 存在元素 12, 其对应的索引为 " + Arrays.binarySearch(arr5[3], 12)); // 使用二分查找来查找
// 6.比较数组
if(Arrays.equals(arr1, arr1)) {
System.out.println("arr1 自己和自己等价");
}
if(Arrays.deepEquals(arr4, arr5)) { // 深度等价
System.out.println("arr4 和 arr5 是等价的");
}
if(!Arrays.equals(arr1, arr3)) {
System.out.println("arr1 和 arr3 是不等价的");
}
}
}
警告:无论是静态创建还是动态创建,最终数组元素都会存储到堆上,而数组名作为引用变量指向/引用堆空间里的数组元素,并且使用下标/索引来进行访问。
警告:对于基本类型的数组,默认初始化为
0。
补充:数组被创建时,如果类型时基本数据类型会自动进行初始化,如果时自定义类类型,也会调用构造函数进行初始化,这点在和
Cpp是一样的。
补充:
Java的数组可以使用索引来查找和修改对应元素,并且索引也是从0开始的,Java的数组越界会抛出异常,这比C/C++的基本数组要安全得多。
补充:当
Java数组发生索引越界时会发生异常。
12.2.数组的引用
首先您需要注意,数组是一个引用类型,什么是引用类型呢?首先您需要了解一下 Java 的内存分布。首先内存是连续分布的存储空间,程序中运行所需要的数据就存储在内存空间中,而 JVM 虚拟机对内存的划分大致如下:
flowchart TD
subgraph "运行时数据区"
subgraph "所有线程共享的数据区"
a["方法区"]
b["堆区"]
end
subgraph "线程隔离的数据区"
c["虚拟机栈"] ~~~ d["本地方法栈"] ~~~ e["程序计数器"]
end
end
- 方法区(
Method Area): 存储虚拟机加载的类信息、常量、静态变量等,即编译器编译后的代码数据。 - 堆区(
Heap):JVM所管理的最大内存区域,所有使用new创建的对象都是在堆上保存,堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有程序在使用,就不会被销毁。其中销毁数据的GC,也是Java最重要的特征之一… - 虚拟机栈(
JVM stack):与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧,栈帧中包含有了局部变量表、操作数栈、动态链接、返回地址…保存的都是与方法执行时相关信息(例如局部变量,在方法运行结束后,栈帧就会被销毁,栈帧中保存的数据也跟着销毁了。 - 本地方法栈(
Native Method Stack): 本地方法栈与虚拟机栈的作用类似,只不过保存的内容是Native方法(即原生方法)的局部变员,在有些版本的JVM实现中(例如HotSpot),本地方法栈和虚拟机栈是在一起使用的,这些原生方法很大程度上就是C/Cpp代码实现的。 - 程序计数器(
PC Register):只是一个很小的空间,保存下一条执行的指令的地址
补充:此外还有个
jdk 1.8的概念,即本地内存(元空间、运行时常量池以及直接内存)
这里简单看一下即可…我们主要焦距在堆和虚拟机栈上,让我们来分析下面这个代码的变量和数组的存储情况:
// 分析内存情况
public class Main {
public static void main(String[] args) {
int a = 10;
int b = 5;
int[] arr = { 1, 2, 3, 4 };
}
}
可以看到 arr 这个标识符引用了数组,因此在 Stack 区域中不是直接存储数组的数据,只是存储了数组的一个“标记”。
注意:这里的形式地址不是
C/Cpp中的地址,但概念有些类似。
因此我们在 Java 中就会遇到传递引用的情况,并且我还给出了图示:
// 分析传引用的情况
public class Main {
public static void main(String[] args) {
int a = 10;
int b = 5;
int[] arr1 = { 1, 2, 3, 4 };
for(int e : arr1) {
System.out.print(e + " ");
}
System.out.println();
int[] arr2 = arr1;
for(int e : arr2) {
System.out.print(e + " ");
}
System.out.println();
arr2[2] = 100;
for(int e : arr1) {
System.out.print(e + " ");
}
System.out.println();
}
}
/* 输出结果
1 2 3 4
1 2 3 4
1 2 100 4
*/
注意:强调一下,从这里的引用就可以看出
Java的引用和C++的引用有很大的不同,我建议直接认为是两个不同的东西,以避免混淆…
还有一些特殊并且值得注意的传递引用情况:
// 改变引用
public class Main {
public static void main(String[] args) {
int a = 10;
int b = 5;
int[] arr1 = { 1, 2, 3, 4 };
for(int e : arr1) {
System.out.print(e + " ");
}
System.out.println();
int[] arr2 = { 4, 3, 2, 1 };
for(int e : arr2) {
System.out.print(e + " ");
}
System.out.println();
arr1 = arr2;
for(int e : arr1) {
System.out.print(e + " ");
}
System.out.println();
// 尝试修改并观察
arr2[1] = 10000;
for(int e : arr1) {
System.out.print(e + " ");
}
System.out.println();
for(int e : arr2) {
System.out.print(e + " ");
}
System.out.println();
}
}
/* 输出结果
1 2 3 4
4 3 2 1
4 3 2 1
4 10000 2 1
4 10000 2 1
*/
也就是说,一个引用不能同时指向多个对象,但是一个对象能被多个引用指向。同时,如果堆中的数组没有任何“标记”存在于 Stack 中,也就是没有任何一个标识符引用这个数组,那么 Java GC 会根据自己的决策来释放该数组。
引用类型的初始化可以使用 null,代表引用不指向任何的对象(直接使用索引进行读写操作就会出现空指针异常)。因此Java 的 null 和 C/C++ 不一样,它不是指内存上的 0 地址,只是代表不指向任何对象,两者没有直接关联。
12.3.数组的传递
下面这个代码您需要好好分析一下:
// 传递数组参数
import java.util.Arrays;
public class Main {
public static void func1(int[] arr) { // 形参 arr
arr = new int[] {0, 0, 0, 0};
}
public static void func2(int[] arr) { // 形参 arr
arr[1] = 100;
}
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4 }; // 实参 arr
func1(arr);
func2(arr);
System.out.println(Arrays.toString(arr));
}
}
/* 输出结果
[1, 100, 3, 4]
*/
这份代码可能会让您吃惊,让我们来看看引用指向和内存分布的分析图,调用方法 func1() 且尚未执行 arr = new int[]{0, 0, 0, 0}; 时,发生以下事情。
执行 arr = new int[]{0, 0, 0, 0}; 后发生以下事情。
而 func1() 调用结束后,形参 arr 被销毁,原本指向的对象没有被引用,就会被 Java 自动销毁。最终什么事情没有发生,实参 arr 没有发生任何改变。
而调用 func2() 并且执行语句 arr[1] = 100; 就会导致实参指向的数组也会跟着变化。
利用数组的引用传递,我们可以使用数组引用的特性来完成两数交换的目的。
// 两数交换
import java.util.Arrays;
public class Main {
public static int[] Swap(int[] arr) {
int tmp = arr[0];
arr[0] = arr[1];
arr[1] = tmp;
return arr;
}
public static void main(String[] args) {
int[] arr = { 5, 10 };
System.out.println(Arrays.toString(arr));
int[] swapArr = Swap(arr);
System.out.println(Arrays.toString(swapArr));
}
}
/* 输出结果
[5, 10]
[10, 5]
*/
12.4.数组的拷贝
但对于方法来说,传应用有时太过于危险,在不考虑拷贝开销的情况下,可以考虑对数组进行拷贝后再进行处理(可以是拷贝后传递给方法,也可以是方法获得数组后多一步拷贝,这样就有可能会有多次拷贝,这种情况以后补充…)。
使用 Arrays 的方法 copyOf() 可以拷贝数组的内容,并且可以带有拷贝长度的参数,长度小于源 s 数组就拷贝子数组,长度大于原数组则多余的部分默认初始为 0。
// 数组拷贝
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] arr = new int[] {1, 2, 3, 4, 5};
int[] copy1 = Arrays.copyOf(arr, 3); // 只拷贝三个元素
for (int e : copy1) {
System.out.print(e + " ");
}
System.out.println();
int[] copy2 = Arrays.copyOf(arr, arr.length * 2); // 拷贝数组所有元素, 不够拷贝就填零
for (int e : copy2) {
System.out.print(e + " ");
}
}
}
/* 输出结果
1 2 3
1 2 3 4 5 0 0 0 0 0
*/
补充:实际上
copyOf()的底层实现使用arraycopy(),其底层是使用C/C++实现的,如果打开实现就会出现关键字native,代表该实现不是使用Java代码实现的,而是使用C/C++(这还是要看具体的实现需要看JVM的源码)。// 另一种拷贝(比较底层) public class Main { public static void main(String[] args) { int[] arr = new int[]{1, 2, 3, 4, 5}; int[] copy = new int[arr.length * 2]; System.arraycopy(arr, 2, copy, 1, arr.length - 2); //底层使用 C/C++ 实现 for (int e : copy) { System.out.print(e + " "); } } } /* 输出结果 0 3 4 5 0 0 0 0 0 0 */
还有另外一个方法 copyOfRange() 可以拷贝局部的子数组。
//拷贝局部子数组
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] arr = new int[] {1, 2, 3, 4, 5};
int[] copy = Arrays.copyOfRange(arr, 2, 4); // 拷贝 [2, 4) 的数组元素
for (int e : copy) {
System.out.print(e + " ");
}
}
}
/* 输出结果
3 4
*/
如果只是单纯想拷贝全部的数组,直接使用 clone() 会更加方便。
// 直接克隆数组整体
public class Main {
public static void main(String[] args) {
int[] arr = new int[] {1, 2, 3, 4, 5};
int[] copy = arr.clone();
for (int e : copy) {
System.out.print(e + " ");
}
}
}
/* 输出结果
1 2 3 4 5
*/
注意:目前我只讨论基本数据类型构成的数组,不涉及到深拷贝的问题,关于深浅拷贝我们以后再来提及。
13.字符串
在您学习了如何使用数组后,就能很快理解字符串,从另外一个角度上来说,字符串实际上是数组的一种特殊形式(虽然我不知道在 Java 内部 String 是否有进行复用,但理解终究是和数组一样的)。因此前面有关数组的使用我尽可能讲的详细,但有关字符串的部分我只提供一份代码和注释供您研究。
// 尝试使用字符串
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
// 1.获取字符串中的单字符
String str1 = new String("Hello, World!");
String str2 = "你好,世界";
String str3 = "limou3434.com/blog/login";
String str4 = "23";
System.out.println("str1.charAt(0): " + str1.charAt(0)); // 获取索引为 0 的字符, 不允许使用 str1[0] 进行访问
System.out.println("str2.length(): " + str2.length()); // 获取字符串的长度, 可以看到汉字也和字母一样做处理
// 2.比较字符串是否等价
if (str1.equals("Hello, World!")) { // 比较两个字符串是否相等
System.out.println("两个字符串相等");
}
if (str1.equalsIgnoreCase("hello, world!")) { // 忽略大小写比较字符串
System.out.println("两个字符串相等");
}
// 3.字符串大小写转换
System.out.println("大写: " + str1.toUpperCase()); // 将字符串转换为大写
System.out.println("小写: " + str1.toLowerCase()); // 将字符串转换为小写
// 4.截取子串
System.out.println("子串: " + str1.substring(3, 6)); // 截取 str1 中 [3, 6) 的部分
// 5.分割子串
System.out.println("分割结果为: " + Arrays.toString(str3.split("/"))); // 根据指定的分隔符将此字符串分割成子字符串数组
// 6.替换和搜索
System.out.println(str1.indexOf("World")); // 返回指定子字符串在此字符串中第一次出现处的索引
System.out.println(str1.replace('o', '0')); // 将字符串中所有的 'o' 替换为 '0'
if (str1.matches(".*Hello.*")) {
System.out.println("符合给定的匹配模式");
}
// 7.格式化字符串
System.out.println("格式化结果: " + String.format("Hello, %s!", "World"));
// 8.转为目标类型
int number = Integer.parseInt(str4);
System.out.println("转化结果: " + number);
// 9.字符串编码和解码
byte[] bytes = str1.getBytes(StandardCharsets.UTF_8); // 将字符串编码为字节序列
System.out.println("编码后: " + Arrays.toString(bytes));
System.out.println("解码后: " + new String(bytes, StandardCharsets.UTF_8)); // 将字节序列解码为字符串
}
}
14.IDEA 快捷键
和其他编程语言有一个很大的不同在于 IDEA 几乎是每一个 Java 程序员必须要学会使用的软件,这里简单列出几个快捷键,在后续的章节中,我还会开启关于 IDEA 的详细使用。
[ctrl + /]注释和取消注释ctrl + alt + l格式化[psvm + tab]/[m + tab]生成main方法[sout + tab]生成println语句
结语:…


















