avatar


1.基础语法

环境与工具准备

关于Java开发环境的搭建,我们暂时只讨论两个

  1. JDK
  2. IDEA

有了JDK,至少Java程序能跑起来。有了IDEA,写代码会方便许多。

JDK

JVM

“Write Once,Run Anywhere”,这是1995年SUN公司(Stanford University Network)推出Java 1.0的时候,用来展示Java程序设计语言的跨平台特性的广告词。一次编译,处处运行。
而Java的这个特性,其实基于JVM虚拟机。
我们的Java程序并不直接运行在操作系统上,而是在操作系统上先搭建了一个JVM虚拟机,Java程序运行在JVM虚拟机上。需要注意的是,JVM虚拟机本身并不具有跨平台的特性,Windows上有Windows的JVM虚拟机,Linux上有Linux的JVM虚拟机,MacOS又有MacOS的JVM虚拟机。

安装JDK

接下来就让我们来安装一个JVM。
我们可能会找到这么两个网址:

那么,到底是哪个网址呢?
“Java SE Development Kit”,简称JDK,Java开发工具包。
“Java SE Runtime Environment”,简称JRE,Java程序运行环境。
顾名思义,我们开发Java,当然得下载JDK。
如图是JDK、JRE和JVM的关系
jdk

  • JRE,包括JVM和运行时所需要的核心类库。
  • JDK,包括JRE和开发人员所使用的工具。

我们通过网页,下载对应版本的JDK,下一步,下一步,安装即可。
(建议,安装路径不要包含中文或者空格等特殊字符。)
我们可以通过这个命令试一下,检查是否安装完成。
示例代码:

1
java -version

运行结果:

1
2
3
java version "1.8.0_301"
Java(TM) SE Runtime Environment (build 1.8.0_301-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.301-b09, mixed mode)

有些资料还会说需要添加环境变量等,其实新版本的安装包已经会自动添加环境变量。
但是,如果我们要卸载JDK,或者安装新版本JDK的话,建议检查一下环境变量中的内容,因为卸载程序不一定会帮我们删除环境变量中的内容。在桌面右键单击计算机,高级系统设置,环境变量,在系统变量中找到"Path",把"C:\Program Files (x86)\Common Files\Oracle\Java\javapath"删除,此外我们还可以检查我们环境变量中的"JAVA_HOME"。

那么,如果我们没有配置环境变量,可以运行Java吗?当然也可以,环境变量的配置可以理解成创建快捷方式,我们不用快捷方式,直接运行当然也可以。

示例代码:

1
C:\Java\jdk1.8.0_301\bin\java.exe -version

运行结果:

1
2
3
java version "1.8.0_301"
Java(TM) SE Runtime Environment (build 1.8.0_301-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.301-b09, mixed mode)

但是这样就不快捷啊。

最后,我们打开安装目录,了解一下各个路径存放的内容。

目录 说明
bin 该路径下存放了JDK的各种工具命令。javac和java就放在这个目录。
conf 该路径下存放了JDK的相关配置文件。
include 该路径下存放了一些平台特定的头文件。
jmods 该路径下存放了JDK的各种模块。
legal 该路径下存放了JDK各模块的授权文档。
lib 该路径下存放了JDK工具的一些补充JAR包。

关于macOS上JDK的安装,与Windows上类似,下一步,下一步即可。
关于Linux上JDK的安装,可以参考《ElasticSearch实战入门(6.X):1.工具、概念、集群和倒排索引》的讨论。

IDEA

编译与运行

安装JDK之后,我们能写Java代码了。
打开记事本,敲入代码。
示例代码:

1
2
3
4
5
public class HelloWorld {
public static void main(String[] args) {
System.out.print("HelloWorld");
}
}

然后,我们将其保存为"HelloWorld.java"。
再输入命令:

1
javac HelloWorld.java

注意,可能返回如下:

1
'javac'不是内部或外部命令,也不是可运行的程序或批处理文件。

这是因为javac没有被加入到环境变量中。在环境变量中新增C:\Java\jdk1.8.0_301\bin即可。(建议同时删除安装程序添加的环境变量)

然后,我们会发现多了一个文件"HelloWorld.class"。
HelloWorld.class

再输入命令:

1
java HelloWorld

输出了"HelloWorld"。

java HelloWorld

解释一下我们上述的过程。
首先,我们用编程语言(Java)写下了代码,但这个只是源文件,并不能直接运行。
然后,我们敲入命令javac HelloWorld.java,这时候,我们写的java源文件被翻译成JVM认识的class文件,在这个过程中,javac编译器会检查我们所写的代码是否有错误,有错误就会提示出来,如果没有错误就会编译成功。接下来,我们敲入命令java HelloWorld,将class文件交给JVM去运行,此时JVM就会去运行我们编写的程序了。

对于编译型,是上述过程,会生成中间代码文件。对于解释型的编程语言,存在不同,即时解释并立即执行。在《基于JavaScript的前端开发入门:1.基础语法》,我们有做一些简单的讨论。

也有一种观点,认为Java既是编译型语言,也是解释型语言。因为class文件是通过编译得到的,但执行class文件是一个解释过程。

关于该部分更详细的过程,可以参考《8.多线程 [2/2]》中关于Java内存模型的讨论。

在上文,我们还讨论了,Java的"一次编译,处处运行。"基于的是JVM,JVM是和平台有关的。但是呢,class文件是编译后的产物,是和平台平台无关的。而且,我们或许都有过这种经历,在Windows机器上编译的class文件,直接拿到Linux服务器上去用。

安装IDEA

在上文中,我们已经体验过了用记事本写java,但这只是为了方便大家对编译和运行有一个初步的认识,真要写代码,做项目,我认为没有谁会用记事本。我们有集成开发工具(IDE),这里给大家推荐的 IDEA。除了这个,还有Eclipse,以及被戏称为宇宙Studio的Visual Studio(不是Visual Studio Code),但我个人比较建议IDEA,毕竟一个乘手的IDE,是可以提高工作效率的。

IDEA的下载地址:

IntelliJ IDEA:JetBrains 功能强大、符合人体工程学的 Java IDE
https://www.jetbrains.com/zh-cn/idea/

IDEA的安装就非常简单了,下一步,下一步。也没有所谓的环境变量等需要配置,就和我们安装QQ一样简单,不赘述。

需要注意的是,IDEA专业版是收费的,只能体验30天。如果未能正常完成体验的话,可以通过如下方法延长体验期。
新增Plugins的repo:https://plugins.zhile.io
安装IDE Eval Reset

实际上不仅仅是IDEA,整个JetBrains系列产品都可以利用这种方法延长体验期。
具体参考Jetbrains系列产品重置试用方法

如果再次reset的时候,已经超过30天了,那么就需要依赖我们某个仍在有效期的JetBrains产品,可以参考如下命令。
示例代码:

1
cp /Users/kaka/Library/Application\ Support/JetBrains/DataGrip2021.2/eval/DataGrip212.evaluation.key /Users/kaka/Library/Application\ Support/JetBrains/IntelliJIdea2021.2/eval/idea212.evaluation.key
  • Windows中的路径一般是C:\Users\xxx\AppData\Roaming\JetBrains\IntelliJIdea2021.2\eval
  • 从Windows拷贝到Mac,同样有效。

注释

首先讨论注释。
注释是在代码指定位置添加的说明性信息,不参与运行
注释分为:

  1. 单行注释
  2. 多行注释
  3. 文档注释

单行注释

//,从//开始至本行结尾的文字将作为注释。
示例代码:

1
// 这是单行注释文字

上文我们说,一个趁手的IDE,可以提高工作效率。在IDEA中,单行注释有快捷键,是:Ctrl + /

多行注释

/**/将一段较长的注释括起来。
示例代码:

1
2
3
4
5
/*
这是多行注释文字
这是多行注释文字
这是多行注释文字
*/

在IDEA中也有快捷键:Ctrl+shift+/

文档注释

文档注释以/**开始,以*/结束。

1
2
3
4
5
6
7
/**
* 发短信
* @param msg 短信内容
*/
public void sendMessage(String msg) {
System.out.println(msg);
}

关键字

关键字是指被Java语言赋予了特殊含义的单词。
关键字的特点有:

  1. 关键字的字母全部小写。
  2. 一般的IDE对于关键字都有特殊的颜色标记。

常见的关键字有以下几种:

  1. 用于数据类型:
    booleanbytechardoublefalsefloatintlongnewshorttruevoidinstanceof
  2. 用于语句:
    breakcasecatchcontinuedefaultdoelseforifreturnswitchtrywhilefinallythrowthissuper
  3. 用于修饰:
    abstractfinalnativeprivateprotectedpublicstaticsynchronizedtransientvolatile
  4. 用于方法、类、接口、包和异常:
    classextendsimplementsinterfacepackageimportthrows

基本数据类型

常量

在讨论基本数据类型之前,我们先讨论一下常量。

常量:在程序运行过程中,其值不可以发生改变的量。
常见的常量有六类:

  1. 整数常量:不带小数的数字,例如:-1、0
  2. 小数常量:带小数的数字,例如:-1.0、0.0
  3. 字符串常量:用双引号括起来的多个字符
  4. 字符常量:用单引号括起来的一个字符
  5. 布尔常量:布尔值
  6. 空常量:空值,null

关于常量,有一个特别有意思的现象。
除了空常量,其他的常量都可以直接通过System.out.println输出。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 整数常量
System.out.println(-1);
System.out.println(0);
System.out.println("------");
// 小数常量
System.out.println(-1.0);
System.out.println(0.0);
System.out.println("------");
// 字符串常量
System.out.println("用双引号括起来的多个字符");
System.out.println("------");
// 字符常量
System.out.println('单');
System.out.println('引');
System.out.println('号');
System.out.println("------");
// 布尔常量
System.out.println(true);
System.out.println(false);

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-1
0
------
-1.0
0.0
------
用双引号括起来的多个字符
------



------
true
false

上述都OK,但是唯独空常量,不能通过System.out.println输出。
示例代码:

1
2
// 空常量
System.out.println(null);

运行结果:

1
2
java: 对println的引用不明确
java.io.PrintStream 中的方法 println(char[]) 和 java.io.PrintStream 中的方法 println(java.lang.String) 都匹配

一个System.out.println,可以输出这么多类型的常量,但是为什么null不能输出呢?
具体原因,涉及到方法的重载。而关于方法的重载,我们会在本章的最后讨论。

基本数据类型

Java是一个强类型语言,即一旦一个变量被指定了某个数据类型,如果不经过数据类型转换,那么它就永远是这个数据类型了。
而且,Java中的数据必须明确数据类型。不同的数据类型分配不同的内存空间,其能表示的数据范围也不一样。
数据类型有:
数据类型分类

题外话
《算法入门经典(Java与Python描述):7.哈希表》这一章,讨论"Python中的哈希表"的时候,我们讨论了Python的一个特点。

在Python中,布尔值是int的子类,True的整数值是1,而False的整数值是0

而在Java中,布尔值属于非数值型,这也是Java和Python的区别之一

基本数据类型的内存占用和取值范围如下表所示:

数据类型 关键字 内存占用(字节) 取值范围
整数类型 byte 1 [128,127][-128,127]
short 2 [32768,32767][-32768,32767]
int 4 [231,2311][-2^{31},2^{31}-1]
long 8 [263,2631][-2^{63},2^{63}-1]
浮点类型 float 4 [3.4028231038,1.4012981045][-3.402823 * 10^{38},-1.401298 * 10^{-45}],[1.4012981045,3.4028231038][1.401298 * 10^{45},3.402823 * 10^{38}]
double 8 [1.79769310308,4.900000010324][-1.797693 * 10^{308},-4.9000000 * 10^{-324}],[4.900000010324,1.79769310308][4.9000000 * 10^{324},1.797693 * 10^{308}]
字符类型 char 2 [0,65535][0,65535]
布尔类型 boolean 1 true,false

另外,有两个需要注意:

  1. 整数默认是int类型
  2. 浮点数默认是double类型

我们在下文定义long类型变量的时候,就会见到应用。

在这个表格中,有一列是字节。我们简单解释一下。计算机中最小信息单元叫"位(bit)“,通常用小写的字母"b"表示。最基本的存储单元叫"字节(byte)”,通常用大写字母"B"表示,字节是由连续的8个位组成。即:1B = 8bit。

而通常我们说的一兆,其单位是"MB",字节,但有些运营商说的网速,单位是"Mbps",位。

变量

变量的定义

变量,与常量相对应的,在程序运行过程中,其值可以发生改变的量。
从本质上讲,变量是内存中的一小块区域。
定义变量的方式有:

  1. 声明变量并赋值
    示例代码:
    1
    int age = 35;
  2. 先声明后赋值
    示例代码:
    1
    2
    double money;
    money = 55.5;

此外,还可以在同一行定义多个同一种数据类型的变量,中间使用逗号隔开。
示例代码:

1
2
3
4
5
6
// 定义int类型的变量a和b,中间使用逗号隔开
int a = 10, b = 20;
// 声明int类型的变量c和d,中间使用逗号隔开
int c,d;
c = 30;
d = 40;

但是!不建议这么写!降低了程序的可读性!

变量使用的注意

接下来,我们就来使用变量。

首先,我们定义两个变量,分别是byte类型和boolean类型。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
package com.kakawanyifan;

public class M {
public static void main(String[] args) {
// 定义byte类型的变量
byte b = 1;
System.out.println(b);
// 定义boolean类型的变量
boolean b = true;
System.out.println(b);
}
}

运行结果:

1
java: 已在方法 main(java.lang.String[])中定义了变量 b

报错了。

第一个注意:在一对花括号中,不能定义名称相同的变量。

修改成下面这种格式即可。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
package com.kakawanyifan;

public class M {
public static void main(String[] args) {
// 定义byte类型的变量
byte b = 1;
System.out.println(b);
// 定义boolean类型的变量
boolean bl = true;
System.out.println(bl);
}
}

运行结果:

1
2
1
true

然后,我们来定义一个long类型的变量。
示例代码:

1
2
3
// 定义long类型的变量
long l;
System.out.println(l);

运行结果:

1
变量在使用之前,必须初始化(赋值)。

报错了。

第二个注意:变量在使用之前,必须初始化(赋值)。

我们对其赋值为"1"。
示例代码:

1
2
3
// 定义long类型的变量
long l = 1;
System.out.println(l);

运行结果:

1
1

再换一个大一点的数字试试,10000000000。
示例代码:

1
2
3
// 定义long类型的变量
long l = 10000000000;
System.out.println(l);

运行结果:

1
java: 过大的整数: 10000000000

报错了?
为什么会报错?是超过了long类型的表示范围吗?回到上文的表格,long类型的表示范围是[263,2631][-2^{63},2^{63}-1],这看起来也没超过啊。
再回到上文,“整数默认是int类型”、“浮点数默认是double类型”。

第三个注意:定义long类型的变量时,需要在整数的后面加L(大小写均可),因为整数默认是int类型,整数太大可能超出int范围。

(在阿里巴巴的开发规范中,建议用大写的L,因为在某些字体下,小写的l1不容易区分。)

我们改一下。
示例代码:

1
2
3
// 定义long类型的变量
long l = 10000000000L;
System.out.println(l);

运行结果:

1
10000000000

对于定义float类型的,道理类似。

第四个注意:定义float类型的变量时,需要在小数的后面加F(大小写均可)。因为浮点数的默认类型是double,double的取值范围是大于float的,类型不兼容。

我们可以分别试一下。
首先,不加"F"。
示例代码:

1
2
3
// 定义float类型的变量
float f = 12.34;
System.out.println(f);

运行结果:

1
java: 不兼容的类型: 从double转换到float可能会有损失

把"F"加上。
示例代码:

1
2
3
// 定义float类型的变量
float f = 12.34F;
System.out.println(f);

运行结果:

1
12.34

小结一下,变量使用的四个注意:

  1. 在一对花括号中,不能定义名称相同的变量。
  2. 变量在使用之前,必须初始化(赋值)。
  3. 定义long类型的变量时,需要在整数的后面加L(大小写均可,建议大写)。因为整数默认是int类型,整数太大可能超出int范围。
  4. 定义float类型的变量时,需要在小数的后面加F(大小写均可,建议大写)。因为浮点数的默认类型是double,double的取值范围是大于float的,类型不兼容。

数值型的类型转换

在上文我们讨论了,Java是一个强类型语言,即一旦一个变量被指定了某个数据类型,如果不经过类型转换,那么它就永远是这个数据类型了。
注意看,“如果不经过类型转换”,现在我们讨论类型转换。
在Java中,一些数值型的数据之间是可以相互转换的。

类型转换分为两种情况:

  1. 自动类型转换
  2. 强制类型转换

从数据范围小到数据范围大可以自动类型转换,从数据范围大到数据范围小,需要强制类型转换。
类型转换

  • 特别注意:char的数据范围是[0,65535][0,65535],byte是[128,127][-128,127],short是[32768,32767][-32768,32767]。所以,char不能转换成byte和short,byte和short也不能转换成char。

自动类型转换

把一个表示数据范围小的数值或者变量赋值给另一个表示数据范围大的变量。这种转换方式是自动的,直接写即可。
例如:

1
2
// 将int类型的10直接赋值给double类型
double num = 10;

强制类型转换

把一个表示数据范围大的数值或者变量赋值给另一个表示数据范围小的变量。
强制类型转换格式:

1
目标数据类型 变量名 = (目标数据类型) 值或者变量

示例代码:

1
2
3
4
double num1 = 5.5;
// 将double类型的num1强制转换为int类型
int num2 = (int) num1;
System.out.println(num2);

运行结果:

1
5
  • 注意,有数据精度丢失。

char类型的转换方式

char类型的数据转换为int类型是按照ASCII码中对应的int值进行转换的。
关于ASCII码,最多只需要记住三个:

  1. a97,a-z是连续的,所以b对应的数值是98,c99,依次递加。
  2. A65,A-Z是连续的,所以B对应的数值是66,C67,依次递加。
  3. 048,0-9是连续的,所以0对应的数值是48,149,依次递加。

示例代码:

1
2
3
4
5
6
int a = 'a';
System.out.println(a);
int b = '0';
System.out.println(b);
int c = 0;
System.out.println(c);

运行结果:

1
2
3
97
48
0

boolean类型的注意

boolean类型不能与其他基本数据类型相互转换。
这个也很好理解,回到这张图。
boolean类型不能与其他基本数据类型相互转换
boolean类型属于非数值型,独立于数值型之外的。boolean类型不能与其他基本数据类型相互转换,其他基本数据类型就是数值型。

标识符

在上文,我们定义变量的时候,给每一个变量都取名字了,这个名字就是标识符。
标识符是用户编程时使用的名字,用于给类、方法、变量、常量等命名。
Java中标识符的组成规则:

  1. 由字母、数字、下划线、美元符号组成,第一个字符不能是数字。
  2. 不能使用Java中的关键字作为标识符。
  3. 标识符对大小写敏感(区分大小写)。

Java中标识符的命名约定:

  1. 小驼峰式命名:变量名、方法名
    首字母小写,从第二个单词开始每个单词的首字母大写。
  2. 大驼峰式命名:类名
    每个单词的首字母都大写。

另外,标识符的命名最好可以做到见名知意。
否则,就像这个段子一样:

程序出了点问题,公司派了一个人去修复这个问题,等他回来后发现精神有点反常,不是哭就是笑,嘴里嘟囔着什么"匹萨调用汉堡,并且传入了包子"。程序员的代码里通常体现着自己对幽默的理解,以及对"工作保密"这个词的认识。

运算符

接下来,我们讨论运算符。
运算符:对常量或者变量进行操作的符号。
用运算符把常量或者变量连接起来,并且符合Java语法的式子就称为表达式。不同运算符连接的表达式体现的是不同类型的表达式。
常见的运算符有:

  1. 算数运算符
  2. 字符串连接运算符
  3. 赋值运算符
  4. 自增自减运算符
  5. 关系运算符
  6. 逻辑运算符
  7. 短路逻辑运算符
  8. 三元运算符

我们分别讨论。

算术运算符

算术运算符

符号 作用
+
-
*
/
% 取余

示例代码:

1
2
3
int a = 10;
int b = 20;
int c = a + b;
  • +是运算符,并且是算术运算符。
  • a + b是表达式,由于+是算术运算符,所以这个表达式叫算术表达式。

这些都很简单,我们讨论一下需要注意的地方,分别是:

  1. /的注意
  2. +的注意
  3. 算术表达式的类型自动提升

实际上只有一个需要注意的地方,“算术表达式的类型自动提升”,前两个不过是第三个的例子。

/ 的注意

对于/,整数操作只能得到整数,要想得到小数,必须有浮点数参与运算。
示例代码:

1
2
3
4
System.out.println(1/2);
System.out.println(1/2.0);
System.out.println(1.0/2);
System.out.println(1.0/2.0);

运行结果:

1
2
3
4
0
0.5
0.5
0.5

+ 的注意

在上文我们讨论过,整数默认是int类型。所以呢,byte、short和char类型数据参与运算均会自动转换为int类型。
示例代码:

1
2
3
4
5
6
7
8
9
// 输出98,97 + 1 = 98
char ch1 = 'a';
System.out.println(ch1 + 1);
// 输出66,65 + 1 = 66
char ch2 = 'A';
System.out.println(ch2 + 1);
// 输出49,48 + 1 = 49
char ch3 = '0';
System.out.println(ch3 + 1);

运行结果:

1
2
3
98
66
49

算术表达式的类型自动提升

算术表达式中包含不同的基本数据类型的值的时候,整个算术表达式的类型会自动提升。
提升规则:

  1. 所有的byte型、short型和char型将被提升到int型。
    例外: final修饰的short、char变量相加后不会被自动提升。
  2. 整个表达式的类型自动提升到表达式中最高等级操作数同样的类型。
    byte,short,char \rightarrow int \rightarrow long \rightarrow float \rightarrow double

所以呢,上述两个注意,其实都可以解释,也都是只是"算术表达式的类型自动提升"的一个例子。

  1. 对于/,整数操作只能得到整数,要想得到小数,必须有浮点数参与运算。
    因为:只有浮点数参与运算了,整个表达式的类型才会提升。
  2. byte、short和char类型数据参与运算均会自动转换为int类型。
    因为:整数默认是int类型。

也正因为算术表达式的类型自动提升。
所以:

  1. 在程序开发中我们很少使用byte或者short类型定义整数。
  2. 很少会使用char类型定义字符,而使用字符串类型,更不会使用char类型做算术运算。

最后,我们来体验一下算术表达式的类型自动提升。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
byte b1 = 10;
byte b2 = 20;
// 该行报错,因为byte类型参与算术运算会自动提示为int,int赋值给byte可能损失精度
// byte b3 = b1 + b2;
System.out.println(b1);
System.out.println(b2);

// 这行也会报错,减法也会报错。
// java: 不兼容的类型: 从int转换到byte可能会有损失
// byte b3 = b2 - b2;

// 应该使用int接收
int i3 = b1 + b2;
// 或者将结果强制转换为byte类型
byte b3 = (byte) (b1 + b2);
System.out.println(i3);
System.out.println(b3);

int num1 = 10;
double num2 = 20.0;
// 使用double接收,因为num1会自动提升为double类型
double num3 = num1 + num2;
System.out.println(num3);

运行结果:

1
2
3
4
5
10
20
30
30
30.0

字符串的 + 操作

在上文我们讨论算数运算符的时候,讨论了+这个运算符,而且我们知道关于+的注意,其实就是算术表达式的类型自动提升的一个例子,不单单+,其他算数运算符都类似。
现在,我们讨论字符串的+操作。
+操作中出现字符串时,这个+是字符串连接运算符,而不是算术运算符。

示例代码:

1
2
// 输出:TongJi1907
System.out.println("TongJi"+ 1907);

运行结果:

1
TongJi1907

再来看个例子。
示例代码:

1
2
3
4
5
6
7
// 输出:3String
System.out.println(1 + 2 + "String");
// 输出:3String34
System.out.println(1 + 2 + "String" + 3 + 4);
// 可以使用小括号改变运算的优先级
// 输出:3String7
System.out.println(1 + 2 + "String" + (3 + 4));

运行结果:

1
2
3
3String
3String34
3String7

解释:
当连续进行+操作时,从左到右逐个执行,没有出现字符串,+就依旧是算术运算符,如果出现了,就是字符串连接运算符。

赋值运算符

赋值运算符的作用是将一个表达式的值赋给左边,左边必须是可修改的,不能是常量。

符号 作用 说明
= 赋值 a=10,将10赋值给变量a
+= 加后赋值 a+=b,将a+b的值给a
-= 减后赋值 a-=b,将a-b的值给a
*= 乘后赋值 a*=b,将a×b的值给a
/= 除后赋值 a/=b,将a÷b的商给a
%= 取余后赋值 a%=b,将a÷b的余数给a

除了第一种,其他都是"扩展的赋值运算符",其特点是隐含了强制类型转换。
我们来看具体的例子。
示例代码:

1
2
3
4
5
6
7
8
short s = 10;

// 此行代码报错,因为运算中s提升为int类型,运算结果int赋值给short可能损失精度
// s = s + 10;

// 此行代码没有问题,隐含了强制类型转换,相当于 s = (short) (s + 10);
s += 10;
System.out.println(s);

运行结果:

1
20

自增自减运算符

符号 作用 说明
++ 自增 变量的值加1
-- 自减 变量的值减1

++--,既可以放在变量的后边,也可以放在变量的前边。

单独使用的时候:

  • ++--无论是放在变量的前边还是后边,结果是一样的。

参与操作的时候:

  • 如果放在变量的后边,先拿变量参与操作,后拿变量做++或者--
  • 如果放在变量的前边,先拿变量做++或者--,后拿变量参与操作。

我们来看具体的例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int i = 10;
// 单独使用
i++;
// i:11
System.out.println("i:" + i);

int j = 10;
// 单独使用
++j;
// j:11
System.out.println("j:" + j);

int x = 10;
// 赋值运算,++在后边,所以是使用x原来的值赋值给y,x本身自增1
int y = x++;
// x:11,y:10
System.out.println("x:" + x + ", y:" + y);

int m = 10;
// 赋值运算,++在前边,所以是使用m自增后的值赋值给n,m本身自增1
int n = ++m;
// m:11,m:11
System.out.println("m:" + m + ", m:" + m);

运行结果:

1
2
3
4
i:11
j:11
x:11, y:10
m:11, m:11

关系运算符

关系运算符有6种,分别为等于、不等于、大于、大于等于、小于、小于等于。

符号 说明
== a==b,判断a和b的值是否相等,成立为true,不成立为false。
!= a!=b,判断a和b的值是否不相等,成立为true,不成立为false。
> a>b,判断a是否大于b,成立为true,不成立为false。
>= a>=b,判断a是否大于等于b,成立为true,不成立为false。
< a<b,判断a是否小于b,成立为true,不成立为false。
<= a<=b,判断a是否小于等于b,成立为true,不成立为false。

关系运算符的结果都是boolean类型,要么是true,要么是false。

我们来看具体的例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
int a = 10;
int b = 20;
System.out.println(a == b);
System.out.println(a != b);
System.out.println(a > b);
System.out.println(a >= b);
System.out.println(a < b);
System.out.println(a <= b);

// 关系运算的结果肯定是boolean类型,所以也可以将运算结果赋值给boolean类型的变量
boolean flag = a > b;
// 输出false
System.out.println(flag);

运行结果:

1
2
3
4
5
6
7
false
true
false
false
true
true
false

需要注意的是,一定不要把==写成=
来看反例。
示例代码:

1
2
3
int a = 10;
int b = 20;
System.out.println(a = b);

运行结果:

1
20
  • 把b的值赋值给a,然后输出a的值。

逻辑运算符

逻辑运算符把各个运算的关系表达式连接起来组成一个复杂的逻辑表达式,以判断程序中的表达式是否成立,运算的结果依旧是boolean类型,要么是true,要么是false。

符号 作用 说明
& 逻辑与 a&b,a和b都是true,结果为true,否则为false。
| 逻辑或 a|b,a和b都是false,结果为false,否则为true。
^ 逻辑异或 a^b,a和b结果不同为true,相同为false。
! 逻辑非 !a,结果和a的结果正好相反。

我们来看具体的例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//定义变量
int i = 10;
int j = 20;
int k = 30;

// & “与”,并且的关系,只要表达式中有一个值为false,结果即为false
// false & false,输出false
System.out.println((i > j) & (i > k));
// true & false,输出false
System.out.println((i < j) & (i > k));
// false & true,输出false
System.out.println((i > j) & (i < k));
// true & true,输出true
System.out.println((i < j) & (i < k));
System.out.println("--------");

// | “或”,或者的关系,只要表达式中有一个值为true,结果即为true
// false | false,输出false
System.out.println((i > j) | (i > k));
// true | false,输出true
System.out.println((i < j) | (i > k));
// false | true,输出true
System.out.println((i > j) | (i < k));
// true | true,输出true
System.out.println((i < j) | (i < k));
System.out.println("--------");

// ^ “异或”,相同为false,不同为true
// false ^ false,输出false
System.out.println((i > j) ^ (i > k));
// true ^ false,输出true
System.out.println((i < j) ^ (i > k));
// false ^ true,输出true
System.out.println((i > j) ^ (i < k));
// true ^ true,输出false
System.out.println((i < j) ^ (i < k));
System.out.println("--------");

// ! “非”,取反
// false
System.out.println((i > j));
// !false,,输出true
System.out.println(!(i > j));

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
false
false
false
true
--------
false
true
true
true
--------
false
true
true
false
--------
false
true

短路逻辑运算符

符号 作用 说明
&& 短路与 作用和&相同,但是有短路效果。
|| 短路或 作用和|相同,但是有短路效果。

我们来讨论了一下什么是短路效果。在逻辑与运算中,只要有一个表达式的值为false,那么结果就可以判定为false了,没有必要将所有表达式的值都计算出来,短路与操作就有这样的效果,可以提高效率。同理在逻辑或运算中,一旦发现值为true,右边的表达式将不再参与运算。

逻辑与&,无论左边真假,右边都要执行。
短路与&&,如果左边为真,右边执行;如果左边为假,右边不执行。

逻辑或|,无论左边真假,右边都要执行。
短路或||,如果左边为假,右边执行;如果左边为真,右边不执行。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
int x = 3;
int y = 4;
// 两个表达都会运算
System.out.println((x++ > 4) & (y++ > 5));
System.out.println(x);
System.out.println(y);

// 左边已经可以确定结果为false,右边不参与运算
System.out.println((x++ > 4) && (y++ > 5));
System.out.println(x);
System.out.println(y);

运行结果:

1
2
3
4
5
6
false
4
5
false
5
5

短路运算符很好用,比如我们要判断某个东西是否为空,如果不为空,再判断其size是否为0。如果没有短路的话,我们需要嵌套写if,如果有短路的话,可以这么写。

1
2
3
if (不为空 && size不为0){

}

三元运算符

三元运算符语法格式:

1
关系表达式 ? 表达式1 : 表达式2;

问号前面的位置是判断的条件,判断结果为boolean型,为true时执行表达式1,为false时执行表达式2。

我们来看具体的例子。
示例代码:

1
2
3
4
5
int a = 10;
int b = 20;
// 判断 a>b 是否为真,如果为真取a的值,如果为假,取b的值
int c = a > b ? a : b;
System.out.println(c);

运行结果:

1
20

流程控制

在一个程序执行的过程中,各条语句的执行顺序对程序的结果是有直接影响的。所以,我们需要清楚每条语句的执行流程。而且,很多时候要通过控制语句的执行顺序来实现我们想要的功能。

流程控制语句分类:

  1. 顺序结构
  2. 分支结构(if, switch)
  3. 循环结构(for, while, do…while)

顺序结构

顺序结构是程序中最简单最基本的流程控制,没有特定的语法结构,按照代码的先后顺序,依次执行,程序中大多数的代码都是这样执行的。

分支结构

if结构

if

1
2
3
if (关系表达式) {
语句体;
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
//定义两个变量
int a = 10;
int b = 20;
//需求:判断a和b的值是否相等,如果相等,就在控制台输出:a等于b
if(a == b) {
System.out.println("a等于b");
}
//需求:判断a和c的值是否相等,如果相等,就在控制台输出:a等于c
int c = 10;
if(a == c) {
System.out.println("a等于c");
}

运行结果:

1
a等于c

if-else

1
2
3
4
5
if (关系表达式) {
语句体1;
} else {
语句体2;
}

示例代码:

1
2
3
4
5
6
7
8
9
//定义两个变量
int a = 10;
int b = 50;
//需求:判断a是否大于b,如果是,在控制台输出:a的值大于b,否则,在控制台输出:a的值不大于b
if(a > b) {
System.out.println("a的值大于b");
} else {
System.out.println("a的值不大于b");
}

运行结果:

1
a的值不大于b

if-else if

1
2
3
4
5
6
7
8
9
if (关系表达式1) {
语句体1;
} else if (关系表达式2) {
语句体2;
}

else {
语句体n+1;
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
//定义两个变量
int a = 10;
int b = 10;
//需求:判断a是否大于b,如果是,在控制台输出:a的值大于b,否则,在控制台输出:a的值不大于b
if(a > b) {
System.out.println("a的值大于b");
} else if (a < b){
System.out.println("a的值小于b");
} else {
System.out.println("a的值等于b");
}

运行结果:

1
a的值等于b

switch结构

switch

格式:

1
2
3
4
5
6
7
8
9
10
11
12
switch (表达式) {
case 1:
语句体1;
break;
case 2:
语句体2;
break;
...
default:
语句体n+1;
break;
}

执行流程:

  • 首先计算出表达式的值。
  • 其次,和case依次比较,一旦有对应的值,就会执行相应的语句,在执行的过程中,遇到break就会结束。
  • 最后,如果所有的case都和表达式的值不匹配,就会执行default语句体部分,然后结束。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.Scanner;

public class M {
public static void main(String[] args) {
//键盘录入月份数据,使用变量接收
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个月份:");
int month = sc.nextInt();
switch(month) {
case 1:
case 2:
case 12:
System.out.println("冬季");
break;
case 3:
case 4:
case 5:
System.out.println("春季");
break;
case 6:
case 7:
case 8:
System.out.println("夏季");
break;
case 9:
case 10:
case 11:
System.out.println("秋季");
break;
default:
System.out.println("你输入的月份有误");
}
}
}

运行结果:

1
2
3
请输入一个月份:
1
冬季

case穿透

注意:如果switch中的case,没有对应break的话,则会出现case穿透的现象。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.Scanner;

public class M {
public static void main(String[] args) {
//键盘录入月份数据,使用变量接收
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个月份:");
int month = sc.nextInt();
//case穿透
switch(month) {
case 1:
case 2:
case 12:
System.out.println("冬季");
// break;
case 3:
case 4:
case 5:
System.out.println("春季");
break;
case 6:
case 7:
case 8:
System.out.println("夏季");
break;
case 9:
case 10:
case 11:
System.out.println("秋季");
break;
default:
System.out.println("你输入的月份有误");
}
}
}

运行结果:

1
2
3
4
请输入一个月份:
1
冬季
春季
  • 注意第15行

一个case中有多行代码

如果我们想在一个case中执行多行代码呢?
建议用一对花括号括起来。

否则就像这样。
没有用花括号

特别的,如果case 3case 4case 5中,没有初始化变量str,也是没问题的。
具体原因,应该和类的加载过程有关,在准备阶段,就会把str定义好,在内存中占个位置。
关于类的加载,可以参考《9.类的加载与反射》

、、

但是,总之,建议使用花括号。

循环结构

for循环

循环:循环语句可以在满足循环条件的情况下,反复执行某一段代码,这段被重复执行的代码被称为循环体语句,当反复执行这个循环体时,需要在合适的时候把循环判断条件修改为false,从而结束循环,否则循环将一直执行下去,形成死循环。

for循环格式:

1
2
3
for (初始化语句;条件判断语句;条件控制语句) {
循环体语句;
}
  • 初始化语句:用于表示循环开启时的起始状态,简单说就是循环开始的时候什么样。
  • 条件判断语句:用于表示循环反复执行的条件,简单说就是判断循环是否能一直执行下去。
  • 循环体语句:用于表示循环反复执行的内容,简单说就是循环反复执行的事情。
  • 条件控制语句:用于表示循环执行中每次变化的内容,简单说就是控制循环是否能执行下去。

执行流程:

  1. 执行初始化语句
  2. 执行条件判断语句,看其结果是true还是false。如果是false,循环结束;如果是true,继续执行。
  3. 执行循环体语句
  4. 执行条件控制语句
  5. 回到第二步继续

示例代码:

1
2
3
4
5
6
7
8
9
//需求:输出数据1-5
for(int i=1; i<=5; i++) {
System.out.println(i);
}
System.out.println("--------");
//需求:输出数据5-1
for(int i=5; i>=1; i--) {
System.out.println(i);
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
--------
5
4
3
2
1

解释说明:

在上文,我们说:

  • ++如果放在变量的后边,先拿变量参与操作,后拿变量做++
  • ++如果放在变量的前边,先拿变量做++,后拿变量参与操作。

但是!在这里的++,不属于上述的两种,属于单独使用!(毕竟,我们是用;隔开的。)

示例代码:

1
2
3
for(int i=1; i<=5; ++i) {
System.out.println(i);
}

运行结果:

1
2
3
4
5
1
2
3
4
5

while循环

1
2
3
4
5
初始化语句;
while (条件判断语句) {
循环体语句;
条件控制语句;
}

while循环执行流程:

  1. 执行初始化语句
  2. 执行条件判断语句,看其结果是true还是false。如果是false,循环结束;如果是true,继续执行。
  3. 执行循环体语句
  4. 执行条件控制语句
  5. 回到第二步继续

举个例子。
需求:世界最高山峰是珠穆朗玛峰(8844.43米=8844430毫米),假如我有一张足够大的纸,它的厚度是0.1毫米。请问,我折叠多少次,可以折成珠穆朗玛峰的高度?

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义一个计数器,初始值为0
int count = 0;
//定义纸张厚度
double paper = 0.1;
//定义珠穆朗玛峰的高度
int zf = 8844430;
//因为要反复折叠,所以要使用循环,但是不知道折叠多少次,这种情况下更适合使用while循环
//折叠的过程中当纸张厚度大于珠峰就停止了,因此继续执行的要求是纸张厚度小于珠峰高度
while(paper <= zf) {
//循环的执行过程中每次纸张折叠,纸张的厚度要加倍
paper *= 2;
//在循环中执行累加,对应折叠了多少次
count++;
}
//打印计数器的值
System.out.println("需要折叠:" + count + "次");

运行结果:

1
27

do…while循环

1
2
3
4
5
初始化语句;
do {
循环体语句;
条件控制语句;
}while(条件判断语句);

执行流程:

  1. 执行初始化语句
  2. 执行循环体语句
  3. 执行条件控制语句
  4. 执行条件判断语句,看其结果是true还是false。如果是false,循环结束;如果是true,继续执行。
  5. 回到第二步继续

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
//需求:在控制台输出5次"HelloWorld"
//for循环实现
for(int i=1; i<=5; i++) {
System.out.println("HelloWorld");
}
System.out.println("--------");
//do...while循环实现
int j = 1;
do {
System.out.println("HelloWorld");
j++;
}while(j<=5);

运行结果:

1
2
3
4
5
6
7
8
9
10
11
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
--------
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld

跳转控制语句

跳转控制语句(break),跳出循环,结束循环。
跳转控制语句(continue),跳过本次循环,继续下次循环。

  • 注意:continue只能在循环中进行使用!

循环嵌套

循环嵌套概述:在循环中,继续定义循环

示例代码:

1
2
3
4
5
6
7
//外循环控制小时的范围,内循环控制分钟的范围
for (int hour = 0; hour < 24; hour++) {
for (int minute = 0; minute < 60; minute++) {
System.out.println(hour + "时" + minute + "分");
}
System.out.println("--------");
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0时0分
0时1分
0时2分
0时3分
0时4分
0时5分

【部分运行结果略】

23时55分
23时56分
23时57分
23时58分
23时59分

三种循环的区别

  • for循环和while循环先判断条件是否成立,然后决定是否执行循环体(先判断后执行)
  • do…while循环先执行一次循环体,然后判断条件是否成立,是否继续执行循环体(先执行后判断)
  • for循环和while的区别:
    • 对于for循环,条件控制语句所控制的自增变量,归属for循环的语法结构中,在for循环结束后,就不能再次被访问到了
    • 对于while循环,条件控制语句所控制的自增变量,不归属while循环的其语法结构中,在while循环结束后,该变量还可以继续使用

数组

在上文,我们提到了Java中的数据类型可以分为基本数据类型引用数据类型两种。
基本数据类型有四大类、共计八种。分别是:

  1. 整数类型:byteshortintlong
  2. 浮点类型:floatdouble
  3. 字符类型:char
  4. 布尔类型:boolean

引用数据类型有:接口以及数组

接下来我们讨论"数组"。
这不是我们第一次讨论数组,在《算法入门经典(Java与Python描述):1.数组、链表》中,我们也讨论过数组,当时讨论的是数组的原理,插入一个数据、删除一个数据、查找一个数据,具体是如何实现的。
这次我们的讨论会偏向于应用以及在Java内存中的管理方式。

数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

数组的初始化

Java中的数组必须先初始化,然后才能使用。
所谓的初始化就是为数组中的数组元素分配内存空间,并为每个数组元素赋值。

  1. 静态初始化
  2. 动态初始化

静态初始化

格式:

1
数据类型[] 数组名 = new 数据类型[]{元素1,元素2,...};

但是,更多的时候,我们见到的是这种简化版格式。

1
数据类型[] 数组名 = {元素1,元素2,...};

此外,大家还会见到这种格式:

1
数据类型 数组名[] = {元素1,元素2,...};

注意!这种方法不建议使用,虽然效果相同。这其实是"C/C++"风格的,Java采用这种风格是因为历史包袱,为了让"C/C++"的开发者能快速理解Java。而且这种方法不符合我们常规的数据类型 变量名的定义格式。

示例代码:

1
2
int[] arr = {1,2,3};
System.out.println(arr[0]);

运行结果:

1
1

如果我们要传入一个数组参数,应该用如下的哪种方式?

  1. func({"haha"})
  2. func(new String[] {"haha"})

当然是第二种,第二种声明了数组的类型,是一个String类型的数组,但是第一种没有声明。

动态初始化

上述,我们在定义数组的时候,就需要指定数组的内容,这是静态初始化。还有一种方法是动态初始化,即只给定数组的长度,由系统给出默认初始值。

1
数据类型[] 数组名 = new 数据类型[数组长度];

不同数据类型的默认初始化值不一样,具体如下

  • 整数:默认值 0
  • 浮点数:默认值 0.0
  • 布尔:默认值 false
  • 字符:默认值 空字符
  • 引用数据类型:默认值 null

示例代码:

1
2
3
4
int[] arr = new int[3];
System.out.println(arr[0]);
arr[0] = 1;
System.out.println(arr[0]);

运行结果:

1
2
0
1

我们解释一下上述代码:

  • 等号左边:
    • int:数组的数据类型
    • []:代表这是一个数组
    • arr:代表数组的名称
  • 等号右边:
    • new:为数组开辟内存空间
    • int:数组的数据类型
    • []:代表这是一个数组
    • 3:代表数组的长度

数组元素访问

每一个存储到数组的元素,都会自动的拥有一个编号,从0开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。关于为什么从0开始编号,我们在《算法入门经典(Java与Python描述):1.数组、链表》已经有过讨论。

内存结构

栈内存和堆内存

Java程序在运行时,需要在内存中分配空间。为了提高运算效率,就对空间进行了不同区域的划分,每一片区域都有特定的数据处理方式和内存管理方式。
其中一片被称为栈内存,另一片被称为堆内存。

注意!
实际上Java的内存模型并不是这么简单,我们这里进行了简化。
关于更完整的Java内存模型,可以参考《8.多线程 [2/2]》

单个数组的内存结构

我们以这么一段代码为例
示例代码:

1
2
3
4
5
int[] arr = new int[3];
System.out.println(arr);
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);

运行结果:

1
2
3
4
[I@29453f44
0
0
0

接下来,我们解释上述代码的过程。

首先,我们有两片内存空间。
两片内存空间

第一行int[] arr = new int[3],先执行等号左边,会开辟一个内存空间用来存储;
然后执行等号右边,也会开辟一个内存空间用来存储。而且,根据我们刚刚的讨论,我们知道默认值是0。
(根据我们在《算法入门经典(Java与Python描述):1.数组、链表》的讨论,我们知道右边的会是一个连续的空间。)
假设右边开辟的内存的首地址是001,执行等号之后,左边的int[] arr会指向右边的地址001
此时内存空间的如下:
第一行

第二行System.out.println(arr),输出的是arr的内存地址。
第二行

第三行、第四行以及第五行的代码,都是根据数组寻址公式,输出内容。
第三行、第四行以及第五行

再来讨论一下两片内存区域。
栈内存和堆内存
左边的区域,我们称之为栈内存,右边的区域,我们称之为堆内存。
栈内存:存储局部变量,使用完毕,立即消失。
堆内存:存储new出来的内容(实体、对象),使用完毕,由垃圾回收器空闲时进行回收。

多个数组指向相同内存结构

那么如果内存中有多个数组呢?其实没有区别,和单个数组是一样的。

示例代码:

1
2
3
4
int[] arr = new int[2];
System.out.println(arr);
int[] arr2 = new int[3];
System.out.println(arr2);

运行结果:

1
2
[I@29453f44
[I@5cad8086

内存结构如下:
多个数组

我们需要注意的是多个数组指向相同的内存。
示例代码:

1
2
3
4
5
6
7
8
9
int[] arr = new int[3];
System.out.println(arr);
System.out.println(arr[0]);
int[] arr2 = arr;
System.out.println(arr);
System.out.println(arr2);
arr2[0] = 1;
System.out.println(arr[0]);
System.out.println(arr2[0]);

运行结果:

1
2
3
4
5
6
[I@29453f44
0
[I@29453f44
[I@29453f44
1
1

具体如图所示
多个数组指向相同内存结构

方法

方法(method)是将具有独立功能的代码块组织成为一个整体,使其具有特殊功能的代码集。
注意:

  • 方法必须先创建才可以使用,该过程称为方法定义。
  • 方法创建后并不是直接可以运行的,需要手动使用后,才执行,该过程称为方法调用。

方法的定义和调用

定义格式:

1
2
3
4
public static 返回值类型 方法名(形参) {
方法体;
return;
}

调用格式:

1
方法名(实参);
  • 形参:方法定义中的参数,等同于变量定义格式,例如:int number。
  • 实参:方法调用中的参数,等同于使用变量或常量,例如:10 number。
  • public static:修饰符
  • 返回值类型:方法操作完毕之后返回的数据的数据类型
    如果方法操作完毕,没有数据返回,这里写void,而且没有返回的方法的方法体中一般不写return。如果有返回类型,一般用变量接收。
  • 方法名:调用方法时候使用的标识
  • 参数:由数据类型和变量名组成,多个参数之间用逗号隔开
  • 方法体:完成功能的代码块
  • return:如果方法操作完毕,有数据返回,用于把数据返回给调用者

嵌套定义和递归调用

方法不能嵌套定义,错误示范如下:

示例代码:

1
2
3
4
5
6
7
8
9
10
11
public class MethodDemo {
public static void main(String[] args) {

}

public static void methodOne() {
public static void methodTwo() {
// 这里会引发编译错误!!!
}
}
}

但是方法可以递归调用。关于递归,具体可以参考《算法入门经典(Java与Python描述):4.递归》
示例代码:

1
2
3
4
5
6
7
8
9
10
public class MethodDemo {
public static void main(String[] args) {

}

public static void methodOne() {
// 递归调用
methodOne()
}
}

方法重载

方法重载指同一个类中定义的多个方法之间,同时满足下列条件,多个方法相互构成重载

  1. 多个方法在同一个类中
  2. 多个方法具有相同的方法名
  3. 多个方法的参数不相同,类型不同或者数量不同

需要注意的是:

  1. 重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式。
  2. 重载仅针对同一个类中方法的名称与参数进行识别,与返回值类型无关,换句话说不能通过返回值类型来判定两个方法是否相互构成重载。

例如,如下的两个方法,名称相同,参数不同,返回值类型不同,同样构成方法重载。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
System.out.println(func(1));
System.out.println(func("a"));
}

public static int func(int v) {
return v;
}

public static String func(String v) {
return v;
}

运行结果:

1
2
1
a

解释一下,为什么System.out.println,可以输出intdoublechar等各种类型的变量,因为方法的重载,多个方法再用一个类中、名称相同,但是参数不同。
那么,为什么无法通过System.out.println(null);输出null呢?
因为println(char[])println(java.lang.String)都能与null匹配上。

方法的参数传递

传递基本类型

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class M {
public static void main(String[] args) {
int number = 100;
System.out.println("调用change方法前:" + number);
change(number);
System.out.println("调用change方法后:" + number);
}

public static void change(int number) {
number = 200;
}
}

运行结果:

1
2
调用change方法前:100
调用change方法后:100

我们来解释一下上述代码的运行过程。

首先,main方法进入栈内存。
main方法进入栈内存

执行int number = 100;
int number = 100;

执行System.out.println("调用change方法前:" + number);。、
System.out.println("调用change方法前:" + number);

执行change(number);,调用change方法,change方法入栈。
change方法中有一个形参number,其值来自main方法。
change(number);

执行change方法中的number = 200;,修改的是change方法内部的number。
number = 200;

之后,change方法执行完毕,出栈。
继续执行main方法,System.out.println("调用change方法后:" + number);
System.out.println("调用change方法后:" + number);

结论:基本数据类型的参数,形式参数的改变,不影响实际参数。
原因:每个方法在栈内存中,都会有独立的栈空间,方法运行结束后就会出栈。

传递引用类型

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class M {
public static void main(String[] args) {
int[] arr = {10, 20, 30};
System.out.println("调用change方法前:" + arr[1]);
change(arr);
System.out.println("调用change方法后:" + arr[1]);
}

public static void change(int[] arr) {
arr[1] = 200;
}
}

运行结果:

1
2
调用change方法前:20
调用change方法后:200

我们来解释一下上述代码的运行过程。

main方法入栈。
依次执行:
int[] arr = {10, 20, 30};
System.out.println("调用change方法前:" + arr[1]);

main

执行change(arr);
change方法入栈,change方法的形参来自main方法。传递的是内存地址。
change(arr);

执行change方法的arr[1] = 200;
修改堆内存中的内容。
change(arr);

change方法执行完毕,出栈。
继续执行main方法,System.out.println("调用change方法后:" + arr[1]);
System.out.println("调用change方法后:" + arr[1]);

结论:对于引用类型的参数,形式参数的改变,影响实际参数的值。
原因:引用数据类型的传参,传入的是地址值,内存中会造成两个引用指向同一个内存的效果;所以即使方法出栈,堆内存中的数据也已经是改变后的结果。

Junit

最后一个话题,Junit,这是一个软件测试的工具。

测试分类

测试分两种。
测试分两种

删档和不删档?

不,是黑盒测试和白盒测试。
黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望的值。可以理解成业务验证。
白盒测试:需要写代码的。关注程序具体的执行流程。

使用步骤

Junit就是一种白盒测试工具。
使用步骤如下:

  1. 定义一个测试类(测试用例)
    建议:
    • 测试类名:被测试的类名+Test,例如:CalculatorTest
    • 包名:被测试的包加+test,例如com.kakawanyifan.test
  2. 定义测试方法:可以独立运行
    建议:
    • 方法名:test + 测试的方法名,例如testAdd()
    • 返回值:void
    • 参数列表:空参
  3. 给方法加@Test,同时导入junit依赖环境

例子

示例代码:

1
2
3
4
5
6
7
8
9
package com.kakawanyifan;

public class Calculator {

public int add(int a,int b){
return a + b;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.kakawanyifan.test;

import com.kakawanyifan.Calculator;
import org.junit.Assert;
import org.junit.Test;

public class CalculatorTest {

@Test
public void testAdd(){
Calculator calculator = new Calculator();
// 断言
Assert.assertEquals(3,calculator.add(1,2));
}

}

运行结果:
运行结果

解释说明:

  1. 需要关注的不是控制台输出,而是IDEA的测试结果,一般在左下方。
  2. 一般我们会使用断言操作来处理结果
    1
    Assert.assertEquals(期望的结果,运算的结果);

补充

  • @Before:修饰的方法会在测试方法之前被自动执行,通常用于资源申请。
  • @After:修饰的方法会在测试方法执行之后自动被执行,通常用于资源释放。
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10801
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

评论区