叠甲:以下文章主要是依靠我的实际编码学习中总结出来的经验之谈,求逻辑自洽,不能百分百保证正确,有错误、未定义、不合适的内容请尽情指出!
文章目录
- 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
语句
结语:…