image-20220916174714019

面向过程篇

前面我们已经认识了Java语言的相关特性,并且已经成功配置好了开发环境,从这节课开始,我们就可以正式进入到Java语言的学习当中了。Java语言是一门面向对象的语言,但是在面向对象之前,我们还得先学会如何面向过程编程。

Java程序基础

首先我们还是从最基本的Java程序基础开始讲解。

程序代码基本结构

还记得我们之前使用的示例代码吗?

1
2
3
4
5
public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}

这段代码要实现的功能很简单,就是将 Hello World 输出到控制台就行。

由于我们还没有学习到类的相关性质,所以在第二章之前,各位小伙伴直接记住固定模式即可,首先我们创建的源文件名称需要为Main.java然后编写的代码第一行:

1
2
3
public class Main {

}

注意需要区分大小写,Java语言严格区分大小写,如果我们没有按照规则来编写,那么就会出现红色波浪线报错:

image-20220916213529426

只要源代码中存在报错的地方,就无法正常完成编译得到二进制文件,会提示构建失败:

image-20220916213641899

注意最后还有一个花括号,并且此花括号是成对出现的,一一对应。

所以说各位小伙伴在编写代码时一定要注意大小写。然后第二行,准确的说是最外层花括号内部就是:

1
2
3
public static void main(String[] args) {

}

这是我们整个Java程序的入口点,我们称为主方法(如果你学习过C肯定能够联想到主函数,只不过Java中不叫函数,叫方法)最后也会有一个花括号成对出现,而在主方法的花括号中编写的代码,就是按照从上往下的顺序依次执行的。

比如我们之前编写的:

1
System.out.println("Hello World!");

这段代码的意思就是将双引号括起来的内容(字符串,我们会在后面进行讲解)输出(打印)到控制台上,可以看到最后还加上了一个;符号,表示这一句代码结束。我们每一段代码结束时都需要加上一个分号表示这一句的结束,就像我们写作文一样。

比如下面的代码,我们就可以实现先打印Hello World!,然后再打印YYDS!到控制台。

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
System.out.println("YYDS!");
}
}

效果如下:

image-20220916214557378

如果两段代码没有加上分号分割,那么编译器会认为这两段代码是同一句代码中的,即使出现换行或者是空格:

image-20220916214736541

这里IDEA很聪明,会提醒我们这里少加了分号,所以说这个IDEA能够在初期尽可能地帮助新手。

再比如下面的代码:

image-20220916214822072

image-20220916214929651

这里我们尝试在中途换行和添加空格,因为没有添加分号,所以说编译器依然会认为是一行代码,因此编译不会出现错误,能够正常通过。当然,为了代码写得工整和规范,我们一般不会随意进行换行编写或者是添加没必要的空格。

同样的,如果添加了分号,即使在同一行,也会被认为是两句代码:

image-20220916221833145

如果在同一行就是从左往右的顺序,得到的结果跟上面是一样的。

注释

我们在编写代码时,可能有些时候需要标记一下这段代码表示什么意思:

image-20220916221711430

但是如果直接写上文字的话,会导致编译不通过,因为这段文字也会被认为是程序的一部分。

这种情况,我们就可以告诉编译器,这段文字是我们做的笔记,并不是程序的一部分,那么要怎么告诉编译器这不是代码呢?很简单,我们只需要在前面加上双斜杠就可以了:

image-20220916222035778

添加双斜杠之后(自动变成了灰色),后续的文本内容只要没有发生换行,那么都会被认为是一段注释,并不属于程序,在编译时会被直接忽略,之后这段注释也不会存在于程序中。但是一旦发生换行那就不行了:

image-20220916222225047

那要是此时注释很多,一行写不完,我们想要编写很多行的注释呢?我们可以使用多行注释标记:

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
/*
这里面的内容
无论多少行
都可以
*/
System.out.println("Hello World!");
}
}

多行可以使用/**/的组合来囊括需要编写的注释内容。

当然还有一种方式就是使用/**来进行更加详细的文档注释:

image-20220916222636943

这种注释可以用来自动生成文档,当我们鼠标移动到Main上时,会显示相关的信息,我们可以自由添加一些特殊的注释,比如作者、时间等信息,也可以是普通的文字信息。

变量与常量

我们的程序不可能永远都只进行上面那样的简单打印操作,有些时候可能需要计算某些数据,此时我们就需要用到变量了。

那么,什么是变量呢?我们在数学中其实已经学习过变量了:

变量,指值可以变的量。变量以非数字的符号来表达,一般用拉丁字母。变量的用处在于能一般化描述指令的方式。结果只能使用真实的值,指令只能应用于某些情况下。变量能够作为某特定种类的值中任何一个的保留器。

比如一个公式 $x^2 + 6 = 22$ 此时x就是一个变量,变量往往代表着某个值,比如这里的x就代表的是4这个值。在Java中,我们也可以让变量去代表一个具体的值,并且变量的值是可以发生变化的。

要声明一个变量,我们需要使用以下格式:

1
[数据类型] [变量名称];

这里的数据类型我们会在下节课开始逐步讲解,比如整数就是int类型,不同类型的变量可以存储不同的类型的值。后面的变量名称顾名思义,就像x一样,这个名称我们可以随便起一个,但是注意要满足以下要求:

  • 标识符可以由大小写字母、数字、下划线(_)和美元符号($)组成,但是不能以数字开头。
  • 变量不能重复定义,大小写敏感,比如A和a就是两个不同的变量。
  • 不能有空格、@、#、+、-、/ 等符号。
  • 应该使用有意义的名称,达到见名知意的目的(一般我们采用英文单词),最好以小写字母开头。
  • 不可以是 true 和 false。
  • 不能与Java语言的关键字或是基本数据类型重名,关键字列表如下:

![image-20220916224014438](/Users/nagocoler/Library/Application Support/typora-user-images/image-20220916224014438.png)

当然各位小伙伴没必要刻意去进行记忆,我们会在学习的过程中逐步认识到这些关键字。新手要辨别一个单词是否为关键字,只需要通过IDEA的高亮颜色进行区分即可,比如:

image-20220916224129597

深色模式下,关键字会高亮为橙色,浅色模式下会高亮为深蓝色,普通的代码都是正常的灰白色。

比如现在我们想要定义一个变量a,那么就可以这样编写:

1
2
3
4
5
public class Main {
public static void main(String[] args) {
int a; //声明一个整数类型变量a
}
}

但是这个变量一开始没有任何值,比如现在我们要让这个变量表示10,那么就可以将10赋值给这个变量:

1
2
3
public static void main(String[] args) {
int a = 10; //直接在定义变量后面添加 = 10,表示这个变量的初始值为10,这里的10就是一个常量数字
}

或者我们可以在使用时再对其进行赋值:

1
2
3
4
public static void main(String[] args) {
int a;
a = 10; //使用时再赋值也可以
}

是不是感觉跟数学差不多?这种写法对于我们人来说,实际上是很好理解的,意思表达很清晰。

我们可以一次性定义多个变量,比如现在我们想定义两个int类型的变量:

1
2
3
public static void main(String[] args) {
int a, b; //定义变量a和变量b,中间使用逗号隔开就行了
}

或者两个变量单独声明也可以:

1
2
3
4
public static void main(String[] args) {
int a; //分两句进行声明
int b;
}

为了更直观地查看变量的值,我们可以直接将变量的值也给打印到控制台:

1
2
3
4
5
public static void main(String[] args) {
int a = 666;
System.out.println(a); //之前我们在小括号写的是"",现在我们直接将变量给进去就可以打印变量的值了
System.out.println(888); //甚至直接输出一个常量值都可以
}

得到结果:

image-20220916225037221

变量的值也可以在中途进行修改:

1
2
3
4
5
public static void main(String[] args) {
int a = 666;
a = 777;
System.out.println(a); //这里打印得到的值就是777了
}

变量的值也可以直接指定为其他变量的值:

1
2
3
4
5
public static void main(String[] args) {
int a = 10;
int b = a; //直接让b等于a,那么a的值就会给到b
System.out.println(b); //这里输出的就是10了
}

我们还可以让变量与数值之间做加减法(运算符会在后面详细介绍):

1
2
3
4
5
public static void main(String[] args) {
int a = 9; //a初始值为9
a = a + 1; //a = a + 1也就是将a+1的结果赋值给a,跟数学是一样的,很好理解对吧
System.out.println(a); //最后得到的结果就是10了
}

有时候我们希望变量的值一直保持不变,我们就可以将其指定为常量,这里我们介绍Java中第一个需要认识的关键字:

1
2
3
4
public static void main(String[] args) {
final int a = 666; //在变量前面添加final关键字,表示这是一个常量
a = 777; //常量的值不允许发生修改
}

编译时出现:

image-20220916225429474

常量的值只有第一次赋值可以修改,其他任何情况下都不行:

1
2
3
4
public static void main(String[] args) {
final int a;
a = 777; //第一次赋值
}

至此,Java的基础语法部分介绍完毕,下一部分我们将开始介绍Java中的几大基本数据类型。


基本数据类型

我们的程序中可能需要表示各种各样的数据,比如整数、小数、字符等等,这一部分我们将探索Java中的八大基本数据类型。只不过在开始之前,我们还需要先补充一点简单的计算机小知识。

计算机中的二进制表示

在计算机中,所有的内容都是二进制形式表示。十进制是以10为进位,如9+1=10;二进制则是满2进位(因为我们的计算机是电子的,电平信号只有高位和低位,你也可以暂且理解为通电和不通电,高电平代表1,低电平代表0,由于只有0和1,因此只能使用2进制表示我们的数字!)比如1+1=10=2^1+0,一个位也叫一个bit,8个bit称为1字节,16个bit称为一个字,32个bit称为一个双字,64个bit称为一个四字,我们一般采用字节来描述数据大小。

注意这里的bit跟我们生活中的网速MB/s是不一样的,小b代表的是bit,大B代表的是Byte字节(8bit = 1Byte字节),所以说我们办理宽带的时候,100Mbps这里的b是小写的,所以说实际的网速就是100/8 = 12.5 MB/s了。

十进制的7 -> 在二进制中为 111 = 2^2 + 2^1 + 2^0

现在有4个bit位,最大能够表示多大的数字呢?

  • 最小:0000 => 0
  • 最大:1111 => 23+22+21+20 => 8 + 4 + 2 + 1 = 15

在Java中,无论是小数还是整数,他们都要带有符号(和C语言不同,C语言有无符号数)所以,首位就作为我们的符号位,还是以4个bit为例,首位现在作为符号位(1代表负数,0代表正数):

  • 最小:1111 => -(22+21+2^0) => -7
  • 最大:0111 => +(22+21+2^0) => +7 => 7

现在,我们4bit能够表示的范围变为了-7~+7,这样的表示方式称为原码。虽然原码表示简单,但是原码在做加减法的时候,很麻烦!以4bit位为例:

1+(-1) = 0001 + 1001 = 怎么让计算机去计算?(虽然我们知道该去怎么算,但是计算机不知道!)

我们得创造一种更好的表示方式!于是我们引入了反码

  • 正数的反码是其本身
  • 负数的反码是在其原码的基础上, 符号位不变,其余各个位取反

经过上面的定义,我们再来进行加减法:

1+(-1) = 0001 + 1110 = 1111 => -0 (直接相加,这样就简单多了!)

思考:1111代表-0,0000代表+0,在我们实数的范围内,0有正负之分吗?0既不是正数也不是负数,那么显然这样的表示依然不够合理!根据上面的问题,我们引入了最终的解决方案,那就是补码,定义如下:

  • 正数的补码就是其本身 (不变!)
  • 负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1(即在反码的基础上+1,此时1000表示-8)
  • 对补码再求一次补码就可得该补码对应的原码。

比如-7原码为1111,反码为1000,补码就是1001了,-6原码为1110,反码为1001,补码就是1010。所以在补码下,原本的1000就作为新增的最小值-8存在。

所以现在就已经能够想通,-0已经被消除了!我们再来看上面的运算:

1+(-1) = 0001 + 1111 = (1)0000 => +0 (现在无论你怎么算,也不会有-0了!)

所以现在,1111代表的不再是-0,而是-1,相应的,由于消除-0,负数多出来一个可以表示的数(1000拿去表示-8了),那么此时4bit位能够表示的范围是:-8~+7(Java使用的就是补码!)在了解了计算机底层的数据表示形式之后,我们再来学习这些基本数据类型就会很轻松了。

整数类形

整数类型是最容易理解的类型!既然我们知道了计算机中的二进制数字是如何表示的,那么我们就可以很轻松的以二进制的形式来表达我们十进制的内容了。

在Java中,整数类型包括以下几个:

  • byte 字节型 (8个bit,也就是1个字节)范围:-128~+127
  • short 短整形(16个bit,也就是2个字节)范围:-32768~+32767
  • int 整形(32个bit,也就是4个字节)最常用的类型:-2147483648 ~ +2147483647
  • long 长整形(64个bit,也就是8个字节)范围:-9223372036854775808 ~ +9223372036854775807

这里我们来使用一下,其实这几种变量都可以正常表示整数:

1
2
3
4
public static void main(String[] args) {
short a = 10;
System.out.println(a);
}

因为都可以表示整数,所以说我们可以将小的整数类型值传递给大的整数类型:

1
2
3
4
5
public static void main(String[] args) {
short a = 10;
int b = a; //小的类型可以直接传递给表示范围更大的类型
System.out.println(b);
}

反之会出现报错:

image-20220916231650085

这是由于我们在将小的整数类型传递给大的整数类型时发生了隐式类型转换,只要是从存储范围小的类型到存储范围大的类型,都支持隐式类型转换,它可以自动将某种类型的值,转换为另一种类型,比如上面就是将short类型的值转换为了int类型的值。

隐式类型转换不仅可以发生在整数之间,也可以是其他基本数据类型之间,我们后面会逐步介绍。

实际上我们在为变量赋一个常量数值时,也发生了隐式类型转换,比如:

1
2
3
public static void main(String[] args) {
byte b = 10; //这里的整数常量10,实际上默认情况下是int类型,但是由于正好在对应类型可以表示的范围内,所以说直接转换为了byte类型的值
}

由于直接编写的整数常量值默认为int,这里需要特别注意一下,比如下面这种情况:

image-20220916232420547

按照long类型的规定,实际上是可以表示这么大的数字的,但是为什么这里报错了呢?这是因为我们直接在代码中写的常量数字,默认情况下就是int类型,这么大肯定是表示不下的,如果需要将其表示为一个long类型的常量数字,那么需要在后面添加大写或是小写的L才可以。

1
2
3
public static void main(String[] args) {
long a = 922337203685477580L; //这样就可以正常编译通过了
}

当然,针对于这种很长的数字,为了提升辨识度,我们可以使用下划线分割每一位:

1
2
3
public static void main(String[] args) {
int a = 1_000_000; //当然这里依然表示的是1000000,没什么区别,但是辨识度会更高
}

我们也可以以8进制或是16进制表示一个常量值:

1
2
3
4
public static void main(String[] args) {
System.out.println(0xA);
System.out.println(012);
}
  • 十六进制:0x开头的都是十六进制表示法,十六进制满16进一,但是由于我们的数学只提供了0-9这十个数字,10、11、12…15该如何表示呢,我们使用英文字母A按照顺序开始表示,A表示10、B表示11…F表示15。比如上面的0xA实际上就是我们十进制中的10。
  • 八进制:以0开头的都是八进制表示法,八进制就是满8进一,所以说只能使用0-7这几个数字,比如上面的012实际上就是十进制的10。

我们最后再来看一个问题:

1
2
3
4
5
public static void main(String[] args) {
int a = 2147483647; //int最大值
a = a + 1; //继续加
System.out.println(a);
}

此时a的值已经来到了int类型所能表示的最大值了,那么如果此时再继续+1,各位小伙伴觉得会发生什么?可以看到结果很奇怪:

image-20220916234540720

什么情况???怎么正数加1还变成负数了?请各位小伙伴回想一下我们之前讲解的原码、反码和补码。

我们先来看看,当int为最大值时,二进制表示形式为什么:

  • 2147483647 = 01111111 11111111 11111111 11111111(第一个是符号位0,其他的全部为1,就是正数的最大值)

那么此时如果加1,会进位成:

  • 10000000 00000000 00000000 00000000

各位想一想,符号位为1,那么此时表示的不就是一个负数了吗?我们回想一下负数的补码表示规则,瞬间就能明白了,这不就是补码形式下的最小值了吗?

所以说最后的结果就是int类型的最小值:-2147483648,是不是感觉了解底层原理会更容易理解这是为什么。

浮点类型

前面我们介绍了整数类型,我们接着来看看浮点类型,在Java中也可以轻松地使用小数。

首先来看看Java中的小数类型包含哪些:

  • float 单精度浮点型 (32bit,4字节)
  • double 双精度浮点型(64bit,8字节)

那么小数在计算机中又是如何存放的呢?

image-20220917102209246

根据国际标准 IEEE 754,任意一个二进制浮点数 V 可以表示成下面的形式:
$$
V = (-1)^S \times M \times 2^E
$$

  • $(-1)^S$ 表示符号位,当 S=0,V 为正数;当 S=1,V 为负数。
  • M 表示有效数字,大于等于 1,小于 2,但整数部分的 1 不变,因此可以省略。(例如尾数为1111010,那么M实际上就是1.111010,尾数首位必须是1,1后面紧跟小数点,如果出现0001111这样的情况,去掉前面的0,移动1到首位;题外话:随着时间的发展,IEEE 754标准默认第一位为1,故为了能够存放更多数据,就舍去了第一位,比如保存1.0101 的时候, 只保存 0101,这样能够多存储一位数据)
  • $2^E$ 表示指数位。(用于移动小数点,所以说才称为浮点型)

比如, 对于十进制的 5.25 对应的二进制为:101.01,相当于:$1.0101 \times 2^2$。所以,S 为 0,M 为 1.0101,E 为 2。因此,对于浮点类型,最大值和最小值不仅取决于符号和尾数,还有它的阶码,所以浮点类型的大致取值范围:

  • 单精度:$±3.40282347 \times 10^{38}$
  • 双精度:$±1.79769313486231570 \times 10^{308}$

我们可以直接创建浮点类型的变量:

1
2
3
public static void main(String[] args) {
double a = 10.5, b = 66; //整数类型常量也可以隐式转换到浮点类型
}

注意,跟整数类型常量一样,小数类型常量默认都是double类型,所以说如果我们直接给一个float类型赋值:

image-20220917105141288

由于float类型的精度不如double,如果直接给其赋一个double类型的值,会直接出现错误。

同样的,我们可以给常量后面添加大写或小写的F来表示这是一个float类型的常量值:

1
2
3
public static void main(String[] args) {
float f = 9.9F; //这样就可以正常编译通过了
}

但是反之,由于double精度更大,所以说可以直接接收float类型的值:

1
2
3
4
5
public static void main(String[] args) {
float f = 9.9F;
double a = f; //隐式类型转换为double值
System.out.println(a);
}

只不过由于精度问题,最后的打印结果:

image-20220917105849862

这种情况是正常的,因为浮点类型并不保证能够精确计算,我们会在下一章介绍 BigDecimal 和 BigInteger,其中BigDecimal更适合需要精确计算的场景。

我们最后来看看下面的例子:

1
2
3
4
5
public static void main(String[] args) {
long l = 21731371236768L;
float f = l; //这里能编译通过吗?
System.out.println(f);
}

此时我们发现,long类型的值居然可以直接丢给float类型隐式类型转换,很明显float只有32个bit位,而long有足足64个,这是什么情况?怎么大的还可以隐式转换为小的?这是因为虽然float空间没有那么大,但是由于是浮点类型,指数可以变化,最大的数值表示范围实际上是大于long类型的,虽然会丢失精度,但是确实可以表示这么大的数。

所以说我们来总结一下隐式类型转换规则:byte→short(char)→int→long→float→double

字符类型

字符类型也是一个重要的基本数据类型,它可以表示计算机中的任意一个字符(包括中文、英文、标点等一切可以显示出来的字符)

  • char 字符型(16个bit,也就是2字节,它不带符号)范围是0 ~ 65535

可以看到char类型依然存储的是数字,那么它是如何表示每一个字符的呢?实际上每个数字在计算机中都会对应一个字符,首先我们需要介绍ASCII码:

img

比如我们的英文字母A要展示出来,那就是一个字符的形式,而其对应的ASCII码值为65,所以说当char为65时,打印出来的结果就是大写的字母A了:

1
2
3
4
public static void main(String[] args) {
char c = 65;
System.out.println(c);
}

得到结果为:

image-20220917110854266

或者我们也可以直接写一个字符常量值赋值:

1
2
3
4
public static void main(String[] args) {
char c = 'A'; //字符常量值需要使用单引号囊括,并且内部只能有一个字符
System.out.println(c);
}

这种写法效果与上面是一样的。

不过,我们回过来想想,这里的字符表里面不就128个字符吗,那char干嘛要两个字节的空间来存放呢?我们发现表中的字符远远没有我们所需要的那么多,这里只包含了一些基础的字符,中文呢?那么多中文字符(差不多有6000多个),用ASCII编码表那128个肯定是没办法全部表示的,但是我们现在需要在电脑中使用中文。这时,我们就需要扩展字符集了。

我们可以使用两个甚至多个字节来表示一个中文字符,这样我们能够表示的数量就大大增加了,GB2132方案规定当连续出现两个大于127的字节时(注意不考虑符号位,此时相当于是第一个bit位一直为1了),表示这是一个中文字符(所以为什么常常有人说一个英文字符占一字节,一个中文字符占两个字节),这样我们就可以表示出超过7000种字符了,不仅仅是中文,甚至中文标点、数学符号等,都可以被正确的表示出来。

不过这样能够表示的内容还是不太够,除了那些常见的汉字之外,还有很多的生僻字,比如龘、錕、釿、拷这类的汉字,后来干脆直接只要第一个字节大于127,就表示这是一个汉字的开始,无论下一个字节是什么内容(甚至原来的128个字符也被编到新的表中),这就是Windows至今一直在使用的默认GBK编码格式。

虽然这种编码方式能够很好的解决中文无法表示的问题,但是由于全球还有很多很多的国家以及很多很多种语言,所以我们的最终目标是能够创造一种可以表示全球所有字符的编码方式,整个世界都使用同一种编码格式,这样就可以同时表示全球的语言了。所以这时就出现了一个叫做ISO的(国际标准化组织)组织,来定义一套编码方案来解决所有国家的编码问题,这个新的编码方案就叫做Unicode(准确的说应该是规定的字符集,包含了几乎全世界所有语言的字符),规定每个字符必须使用两个字节,即用16个bit位来表示所有的字符(也就是说原来的那128个字符也要强行用两位来表示)

但是这样的话实际上是很浪费资源的,因为这样很多字符都不会用到两字节来保存,肯定不能直接就这样去表示,这会导致某些字符浪费了很多空间,我们需要一个更加好用的具体的字符编码方式。所以最后就有了UTF-8编码格式(它是Unicode字符集的一个编码规则),区分每个字符的开始是根据字符的高位字节来区分的,比如用一个字节表示的字符,第一个字节高位以“0”开头;用两个字节表示的字符,第一个字节的高位为以“110”开头,后面一个字节以“10开头”;用三个字节表示的字符,第一个字节以“1110”开头,后面俩字节以“10”开头;用四个字节表示的字符,第一个字节以“11110”开头,后面的三个字节以“10”开头:

1
10000011 10000110    //这就是一个连续出现都大于127的字节(注意这里是不考虑符号位的)

所以如果我们的程序需要表示多种语言,最好采用UTF-8编码格式,随着更多的字符加入,实际上两个字节也装不下了,可能需要3个甚至4个字节才能表示某些符号,后来就有了UTF-16编码格式,Java在运行时采用的就是UTF-16,几乎全世界的语言用到的字符都可以表示出来。

Unicode符号范围(十六进制) UTF-8编码方式(二进制)
0000 0000 ~ 0000 007F 0xxxxxxx
0000 0080 ~ 0000 07FF 110xxxxx 10xxxxxx
0000 0800 ~ 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 ~ 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

注意:Unicode 是“字符集”,也就是有哪些字符,而UTF-8、UTF-16 是“编码规则”,也就是怎么对这些字符编码,怎么以二进制的形式保存,千万不要搞混了。

简而言之,char实际上需要两个字节才能表示更多种类的字符,所以,char类型可以直接表示一个中文字符:

1
2
3
4
public static void main(String[] args) {
int a = '淦'; //使用int类型接收字符类型常量值可以直接转换为对应的编码
System.out.println(a);
}

得到结果为:

image-20220917111838629

Java程序在编译为.class文件之后,会采用UTF-8的编码格式,支持的字符也非常多,所以你甚至可以直接把变量名写成中文,依然可以编译通过:

image-20220917112033102

介绍完了字符之后,我们接着来看看字符串,其实字符串我们在一开始就已经接触到了。字符虽然可以表示一个中文,但是它没办法表示多个字符:

image-20220917114628564

但是实际上我们使用率最高的还是多个字符的情况,我们需要打印一连串的字符。这个时候,我们就可以使用字符串了:

1
2
3
public static void main(String[] args) {
String str = "啊这"; //字符串需要使用双引号囊括,字符串中可以包含0-N个字符
}

注意,这里使用的类型是String类型,这种类型并不是基本数据类型,它是对象类型,我们会在下一章继续对其进行介绍,这里我们只需要简单了解一下就可以了。

布尔类型

布尔类型是Java中的一个比较特殊的类型,它并不是存放数字的,而是状态,它有下面的两个状态:

  • true - 真
  • false - 假

布尔类型(boolean)只有truefalse两种值,也就是要么为真,要么为假,布尔类型的变量通常用作流程控制判断语句(不同于C语言,C语言中一般使用0表示false,除0以外的所有数都表示true)布尔类型占据的空间大小并未明确定义,而是根据不同的JVM会有不同的实现。

1
2
3
4
public static void main(String[] args) {
boolean b = true; //值只能是true或false
System.out.println(b);
}

如果给一个其他的值,会无法编译通过:

image-20220917115424504

至此,基本数据类型的介绍就结束了。


运算符

前面我们介绍了多种多样的基本数据类型,但是光有这些基本数据类型还不够,我们还需要让这些数据之间进行运算,才可以真正意义上发挥计算机的作用。

要完成计算,我们需要借助运算符来完成,实际上我们在数学中就已经接触过多种多样的运算符了。

比如:+ - × ÷

这些运算符都是我们在初等数学中学习的,而使用规则也很简单,我们只需要将需要进行运算的两个数放到运算符的两边就可以了:

比如:10 ÷ 2

上面运算的结果就是5了,而在Java中,我们同样可以使用这样的方式来进行运算。

赋值运算符

首先我们还是来回顾一下之前认识的老朋友:赋值运算符。

赋值运算符可以直接给某个变量赋值:

1
2
3
public static void main(String[] args) {
int a = 666; //使用等号进行赋值运算
}

使用规则为:赋值运算符的左边必须是一个可以赋值的目标,比如变量,右边可以是任意满足要求的值,包括变量。

当然,赋值运算符并不只是单纯的赋值,它是有结果的:

1
2
3
4
public static void main(String[] args) {
int a;
int b = a = 777;
}

当出现连续使用赋值运算符时,按照从右往左的顺序进行计算,首先是a = 777,计算完成后,a的值就变成了777,计算完成后,会得到计算结果(赋值运算的计算结果就是赋的值本身,就像1 + 1的结果是2一样,a = 1的结果就是1)此时继续进行赋值计算,那么b就被赋值为a = 777的计算结果,同样的也是 777 了。

所以,使用连等可以将一连串变量都赋值为最右边的值。

算术运算符

算术运算符也就是我们初等数学中认识的这些运算符,包括加减乘除,当然Java还支持取模运算,算术运算同样需要左右两边都有一个拿来计算的目标。

1
2
3
4
public static void main(String[] args) {
int a = 1 + 1;
System.out.println(a);
}

可以看到a赋值为1+1的结果,所以说最后a就是2了。

当然变量也是可以参与到算术运算中:

1
2
3
4
5
public static void main(String[] args) {
int a = 3;
int b = a - 10;
System.out.println(b);
}

不同类型之间也可以进行运算:

1
2
3
4
5
6
public static void main(String[] args) {
int a = 5;
short b = 10;
int c = a + b;
//不同类型的整数一起运算,小类型需要转换为大类型,short、byte、char一律转换为int再进行计算(无论算式中有无int,都需要转换),结果也是int;如果算式中出现了long类型,那么全部都需要转换到long类型再进行计算,结果也是long,反正就是依大的来
}

因为运算时会发生隐式类型转换,所以说这里b自动转换为了int类型进行计算,所以说最后得到结果也一定是转换后的类型:

image-20220917141359260

小数和整数一起计算同样会发生隐式类型转换:

image-20220917141955891

因为小数表示范围更广,所以说整数会被转换为小数再进行计算,而最后的结果也肯定是小数了。

我们也可以将加减号作为正负符号使用,比如我们现在需要让a变成自己的相反数:

1
2
3
4
5
public static void main(String[] args) {
int a = 10;
a = -a; //减号此时作为负号运算符在使用,会将右边紧跟的目标变成相反数
System.out.println(a); //这里就会得到-10了
}

同样的,正号也可以使用,但是似乎没什么卵用:

1
2
3
4
5
public static void main(String[] args) {
int a = 10;
a = +a; //正号本身在数学中就是可以省略的存在,所以Java中同样如此
System.out.println(a);
}

注意加法支持对字符串的拼接:

1
2
3
4
public static void main(String[] args) {
String str = "伞兵" + "lbw"; //我们可以使用加号来拼接两个字符串
System.out.println(str);
}

最后这个字符串就变成了拼接后的结果了:

image-20220917145901135

字符串不仅可以跟字符串拼接,也可以跟基本数据类型拼接:

1
2
3
4
public static void main(String[] args) {
String str = "伞兵" + true + 1.5 + 'A';
System.out.println(str);
}

最后就可以得到对应的结果了:

image-20220917150010919

当然,除了加减法之外乘除法也是支持的:

1
2
3
4
5
public static void main(String[] args) {
int a = 8, b = 2;
System.out.println(a * b); //乘法使用*表示乘号
System.out.println(a / b); //除法就是一个/表示除号
}

注意,两个整数在进行除法运算时,得到的结果也是整数(会直接砍掉小数部分,注意不是四舍五入)

1
2
3
4
public static void main(String[] args) {
int a = 8, b = 5;
System.out.println(a / b);
}

上面是两个int类型的值进行的除法运算,正常情况下8除以5应该得到1.6,但是由于结果也是整数,所以说最后小数部分被丢弃:

image-20220917141816599

但是如果是两个小数一起计算的话,因为结果也是小数,所以说就可以准确得到结果:

1
2
3
4
public static void main(String[] args) {
double a = 8.0, b = 5.0;
System.out.println(a / b);
}

image-20220917142201392

同样的,整数和小数一起计算,由于所有的整数范围都比小数小,根据我们上一部分介绍的转换规则,整数和小数一起计算时,所有的整数都会变成小数参与运算,所以说最后的结果也就是小数了,同样可以得到正确的结果:

1
2
3
4
5
public static void main(String[] args) {
double a = 8.0;
int b = 5;
System.out.println(a / b);
}

那么问题来了,现在我们有两个整数需要进行计算,但是我们就是希望可以得到一个小数的结果该怎么办呢?

1
2
3
4
5
public static void main(String[] args) {
int a = 8, b = 5;
double c = a; //我们可以将其先隐式转换为小数类型,再那转换后的小数变量去参与计算
System.out.println(c / b); //同样可以得到正确结果
}

在下一节,我们将介绍强制类型转换,通过使用强制类型转换,我们可以更轻松地让整数计算出小数的结果。

除了最基本的加减乘除操作,我们也可以进行取模操作:

1
2
3
4
public static void main(String[] args) {
int a = 10;
System.out.println(a % 3); //比如这里对a进行取模操作,实际上就是计算除以3的余数
}

比如上面的是 10 % 3 得到的结果就是10除以3最后的余数1,取模操作也是非常好用的。

比如我们查看某个数是否为双数,只需要将其对2进行取模操作即可,因为如果是双数一定是可以整除的,如果是单数一定会余1:

1
2
3
public static void main(String[] args) {
System.out.println(17 % 2); //17不是双数,所以说最后会得到1
}

注意,运算符之间是有优先级之分的,比如乘除法优先级高于加减法:

1
2
3
public static void main(String[] args) {
System.out.println(10 + 3 * 4);
}

上面的算式按照数学中的规则,应该先计算3 * 4,然后再进行加法计算,而Java中同样遵循这样的规律。我们来总结一下到目前为止所有学习到的运算符相关性质:

优先级 运算符 结合性(出现同优先级运算符时)
1 -(负号) +(正号) 从右向左
2 * / % 从左往右
3 +(加法,包括字符串) -(减法) 从左往右
4 = 从右向左

比如下面的结果:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
int a = 10;
int b = a = 8 * -a + 10;
/*
1. 正负号优先级最高,所有首先计算的是-a,得到-10
2. 其次是乘除号优先级更高,所以说这里计算 8 * -10,得到 -80
3. 然后是加减法,-80 + 10 = -70
4. 最后是赋值运算,因为等号运算符从右往左结合,先算a = -70的结果就是 -70
5. 最后b就是 -70
*/
System.out.println(b);
}

通过使用这些基本算术运算符,我们就可以更加快速地计算我们想要的结果了。

括号运算符

前面我们介绍了算术运算符,我们接着来看括号运算符。

我们常常在数学中使用括号提升某些运算的优先级,比如:

(1 + 7) × (3 - 6) = -24

虽然加法优先级比乘法要低但是我们给其添加括号之后,相当于提升了内部加法运算的优先级,所以说需要先计算括号中的再去计算括号外的,Java同样满足这个要求。

我们可以通过添加括号的方式来提升某些运算的优先级:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
int a = 10;
int b = (a = 8) * (-a + 10);
/*
1. 括号的优先级是最高的,我们需要先计算括号中的内容,如果存在多个括号,就从左往右计算
2. 首先是 a = 8,计算完成之后a变成8,并且运算结果也为8
3. 然后是后面的加法,-a就是-8,加上10就是2
4. 最后才是乘法,左边此时是8,右边是2,最后结果为16
*/
System.out.println(b);
}

所以,通过添加括号,就可以更加灵活的控制计算。

当然,括号是可以嵌套的,这一点跟数学中也是一样的,只不过我们不需要使用方括号和花括号,一律使用小括号就行了。

在嵌套的情况下,会优先计算最内层括号中的算式:

1
2
3
4
public static void main(String[] args) {
int b = (2 + (3 + 1) * 3) * 2;
System.out.println(b);
}

这里会优先计算 3 + 1的结果,然后由于第二层都在一个括号中,所以说按照正常优先级计算,2 + 4 * 3 = 14,最后来到最外层14*2 = 28,计算结束。

括号除了可以用来提升运算优先级,也可以用作强制类型转换,前面我们介绍了隐式类型转换,但是隐式类型转换存在局限性,比如此时我们希望将一个大的类型转换为一个小的类型:

image-20220917150256987

正常情况下无法编译通过,但是实际上a的值并没有超出short的范围,理论上是可以直接给到b存放的,此时我们就可以使用强制类型转换:

1
2
3
4
public static void main(String[] args) {
int a = 10;
short b = (short) a; //在括号中填写上强制转换的类型,就可以强制转换到对应的类型了
}

只不过强制类型转换存在一定的风险,比如:

1
2
3
4
5
public static void main(String[] args) {
int a = 128; //已经超出byte的范围了
byte b = (byte) a; //此时强制类型转换为byte类型,那么只会保留byte能够表示的bit位
System.out.println(b);
}

比如这里的128:

  • 00000000 00000000 00000000 10000000 -> byte只有一个字节,所以说只保留最后8位 -> 10000000

这里的10000000,由于第一个位置是符号位,导致此时直接变成了byte的最小值:

image-20220917151028191

所以说强制类型转换只有在明确不会出现问题的情况下,才可以使用。当然,强制类型转换也可以用在后面的类中,我们将会在下一章继续探讨。

有了强制类型转换,我们就可以很轻松地让两个整数计算出小数的结果了:

1
2
3
4
5
6
7
public static void main(String[] args) {
int a = 8, b = 5;
double c = a/(double)b;
//强制类型转换的优先级跟正负号一样
//计算时,只需要将其中一者转换为double类型,此时按照隐式类型转换规则,全都会变成double参与运算,所以结果也就是小数了
System.out.println(c);
}

各位思考一下下面的这种情况可以正确得到小数的结果吗?

1
2
3
4
5
public static void main(String[] args) {
int a = 8, b = 5;
double c = (double) (a/b);
System.out.println(c);
}

不能得到,因为括号将a/b的运算优先进行了,此时得到的结果已经是一个整数结果,再转换为double毫无意义。

最后我们还是来总结一下目前遇到的所有运算符:

优先级 运算符 结合性
1 ( ) 从左向右
2 - + (强制类型转换) 从右向左
3 * / % 从左向右
4 +(加法,包括字符串) -(减法) 从左向右
5 = 从右向左

自增自减运算符

注意:这一节很容易搞晕,请务必记清楚顺序!

有时候我们可能需要让变量自己进行增加操作,比如我们现在想要进行跳绳计数,每转动一圈,计数+1,当我们想要对一个变量进行这样的自增操作时,可以:

1
2
3
4
5
public static void main(String[] args) {
int a = 8;
a = a + 1; //让a等于a本身+1,相当于自增了1
System.out.println(a); //得到9
}

当然,除了这种方式,我们也可以使用自增自减运算符:

1
2
3
4
5
6
public static void main(String[] args) {
int a = 8;
a++; //自增运算符就是两个加号连在一起,效果跟上面是一样的,a都会自增1
a--; //自减不用我多说了吧
System.out.println(a);
}

自增自减运算符可以放到操作数的前后:

1
2
3
4
5
public static void main(String[] args) {
int a = 8;
++a; //自增运算符在前在后最终效果都是让a自增1,是一样的
System.out.println(a);
}

自增自减操作同样是有结果的,注意,这两种方式自增操作的结果不一样,我们来看下面的例子:

1
2
3
4
5
public static void main(String[] args) {
int a = 8;
int b = a++; //先出结果,再自增
System.out.println(b); //b得到的是a自增前的值
}
1
2
3
4
5
public static void main(String[] args) {
int a = 8;
int b = ++a; //先自增,再出结果
System.out.println(b); //b得到的是a自增之后的结果
}

第一个结果为8,而第二个结果却是9,这是因为,自增运算符放在前面,是先自增再得到结果,而自增运算符放到后面,是先出结果再自增(自减同理),这个新手很容易记混,所以说一定要分清楚。

自增自减运算符的优先级与正负号等价比如:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
int a = 8;
int b = -a++ + ++a;
//我们首先来看前面的a,因为正负号和自增是同一个优先级,结合性是从右往左,所以说先计算a++
//a++的结果还是8,然后是负号,得到-8
//接着是后面的a,因为此时a已经经过前面变成9了,所以说++a就是先自增,再得到10
//最后得到的结果为 -8 + 10 = 2
System.out.println(b);
}

一般情况下,除了考试为了考察各位小伙伴对运算符的优先级和结合性的理解,会出现这种恶心人的写法之外,各位小伙伴尽量不要去写这种难以阅读的东西。

当然,有些时候我们并不是希望以1进行自增,可能希望以其他的数进行自增操作,除了按照之前的方式老老实实写之外:

1
2
3
4
5
public static void main(String[] args) {
int a = 8;
a = a + 4;
System.out.println(a);
}

我们可以将其缩写:

1
2
3
4
5
public static void main(String[] args) {
int a = 8;
a += 4; //加号和等号连在一起,与a = a + 4效果完全一样
System.out.println(a);
}

并且结果也是操作之后的结果:

1
2
3
4
5
public static void main(String[] args) {
int a = 8;
int b = a += 4; //+=的运算结果就是自增之后的结果
System.out.println(b); //所以b就是12
}

不止加法,包括我们前面介绍的全部算术运算符,都是支持这种缩写的:

1
2
3
4
5
public static void main(String[] args) {
int a = 8;
a *= 9; //跟 a = a * 9 等价
System.out.println(a); //得到72
}

是不是感觉能够编写更简洁的代码了?

优先级 运算符 结合性
1 ( ) 从左向右
2 - + (强制类型转换) ++ – 从右向左
3 * / % 从左向右
4 +(加法,包括字符串) -(减法) 从左向右
5 = += -= *= /= %= 从右向左

位运算符

我们接着来看位运算符,它比较偏向于底层,但是只要各位小伙伴前面的计算机二进制表示听明白了,这里就不是问题。

我们可以使用位运算符直接以二进制形式操作目标,位运算符包括:& | ^ ~

我们先来看按位与&,比如下面的两个数:

1
2
3
4
5
public static void main(String[] args) {
int a = 9, b = 3;
int c = a & b; //进行按位与运算
System.out.println(c);
}

按位与实际上就是让这两个数每一位都进行比较,如果这一位两个数都是1,那么结果就是1,否则就是0:

  • a = 9 = 1001
  • b = 3 = 0011
  • c = 1 = 0001(因为只有最后一位,两个数都是1,所以说结果最后一位是1,其他都是0)

同样的,按位或,其实就是只要任意一个为1(不能同时为0)那么结果就是1:

1
2
3
4
5
public static void main(String[] args) {
int a = 9, b = 3;
int c = a | b;
System.out.println(c);
}
  • a = 9 = 1001
  • b = 3 = 0011
  • c =11= 1011(只要上下有一个是1或者都是1,那结果就是1)

按位异或符号很多小伙伴会以为是乘方运算,但是Java中并没有乘方运算符,^是按位异或运算符,不要记错了。

1
2
3
4
5
public static void main(String[] args) {
int a = 9, b = 3;
int c = a ^ b;
System.out.println(c);
}

异或的意思就是只有两边不相同的情况下,结果才是1,也就是说一边是1一边是0的情况:

  • a = 9 = 1001
  • b = 3 = 0011
  • c =10= 1010(从左往右第二位、第四位要么两个都是0,要么两个都是1,所以说结果为0)

按位取反操作跟前面的正负号一样,只操作一个数,最好理解,如果这一位上是1,变成0,如果是0,变成1:

1
2
3
4
public static void main(String[] args) {
byte c = ~127;
System.out.println(c);
}
  • 127 = 01111111
  • -128 = 10000000

所以说计算的结果就是-128了。

除了以上的四个运算符之外,还有位移运算符,比如:

1
2
3
4
public static void main(String[] args) {
byte c = 1 << 2; //两个连续的小于符号,表示左移运算
System.out.println(c);
}
  • 1 = 00000001
  • 4 = 00000100(左移两位之后,1跑到前面去了,尾部使用0填充,此时就是4)

我们发现,左移操作每进行一次,结果就会x2,所以说,除了直接使用*进行乘2的运算之外,我们也可以使用左移操作来完成。

同样的,右移操作就是向右移动每一位咯:

1
2
3
4
public static void main(String[] args) {
byte c = 8 >> 2;
System.out.println(c);
}
  • 8 = 00001000
  • 2 = 00000010(右移两位之后,1跑到后面去了,头部使用符号位数字填充,此时变成2)

跟上面一样,右移操作可以快速进行除以2的计算。

对于负数来说,左移和右移操作不会改变其符号位上的数字,符号位不受位移操作影响:

1
2
3
4
public static void main(String[] args) {
byte c = -4 >> 1;
System.out.println(c);
}
  • -4 = 11111100
  • -2 = 11111110(前面这一长串1都被推到后面一位了,因为是负数,头部需要使用符号位数字来进行填充)

我们来总结一下:

  • 左移操作<<:高位直接丢弃,低位补0
  • 右移操作>>:低位直接丢弃,符号位是什么高位补什么

我们也可以使用考虑符号位的右移操作,一旦考虑符号位,那么符号会被移动:

1
2
3
4
public static void main(String[] args) {
int c = -1 >> 1; //正常的右移操作,高位补1,所以说移了还是-1
System.out.println(c);
}
1
2
3
4
public static void main(String[] args) {
int c = -1 >>> 1; //无符号右移是三个大于符号连在一起,移动会直接考虑符号位
System.out.println(c);
}

比如:

  • -1 = 11111111 11111111 11111111 11111111
  • 右移: 01111111 11111111 11111111 11111111(无符号右移使用0填充高位)

此时得到的结果就是正数的最大值 2147483647 了,注意,不存在无符号左移。

位移操作也可以缩写:

1
2
3
4
5
public static void main(String[] args) {
int c = -1;
c = c << 2;
System.out.println(c);
}

可以缩写为:

1
2
3
4
5
public static void main(String[] args) {
int c = -1;
c <<= 2; //直接运算符连上等号即可,跟上面是一样的
System.out.println(c);
}

最后我们还是来总结一下优先级:

优先级 运算符 结合性
1 ( ) 从左向右
2 ~ - + (强制类型转换) ++ – 从右向左
3 * / % 从左向右
4 + - 从左向右
5 << >> >>> 从左向右
6 & 从左向右
7 ^ 从左向右
8 | 从左向右
9 = += -= *= /= %= &= |= ^= <<= >>= >>>= 从右向左

关系运算符

到目前为止,我们发现有一个基本数据类型很低调,在前面的计算中boolean类型一直都没有机会出场,而接下来就是它的主场。

我们可以对某些事物进行判断,比如我们想判断两个变量谁更大,我们可以使用关系运算符:

1
2
3
4
public static void main(String[] args) {
int a = 10, b = 20;
boolean c = a > b; //进行判断,如果a > b那么就会得到true,否则会得到false
}

关系判断的结果只可能是真或是假,所以说得到的结果是一个boolean类型的值。

关系判断运算符包括:

1
2
3
4
5
6
>   大于
< 小于
== 等于(注意是两个等号连在一起,不是一个等号,使用时不要搞混了)
!= 不等于
>= 大于等于
<= 小于等于

关系运算符的计算还是比较简单的。

逻辑运算符

前面我们介绍了简单的关系运算符,我们可以通过对关系的判断得到真或是假的结果,但是只能进行简单的判断,如果此时我们想要判断a是否小于等于100且大于等于60,就没办法了:

image-20220917223047110

注意不能像这样进行判断,这是错误的语法,同时只能使用其中一种关系判断运算符。

为了解决这种问题,我们可以使用逻辑运算符,逻辑运算符包括:

1
2
3
&&     与运算,要求两边同时为true才能返回true
|| 或运算,要求两边至少要有一个为true才能返回true
! 非运算,一般放在表达式最前面,表达式用括号扩起来,表示对表达式的结果进行反转

现在,我们就可以使用逻辑运算符进行复杂条件判断:

1
2
3
4
public static void main(String[] args) {
int a = 10;
boolean b = 100 >= a && a >= 60; //我们可以使用与运算符连接两个判断表达式
}

与运算符要求左右两边同时为真,得到的结果才是真,否则一律为假,上面的判断虽然满足第一个判断表达式,但是不满足第二个,所以说得到的结果就是false

我们再来看下面的这个例子:

1
2
3
4
public static void main(String[] args) {
int a = 150;
boolean b = 100 >= a && a >= 60; //此时上来就不满足条件
}

这个例子中,第一个判断表达式就得到了false,此时不会再继续运行第二个表达式,而是直接得到结果false(逻辑运算符会出现短路的情况,只要第一个不是真,就算第二个是真也不可能了,所以说为了效率,后续就不用再判断了,在使用时一定要注意这一点)

同样的,比如我们现在要判断a要么大于10,要么小于0,这种关系就是一个或的关系:

1
2
3
4
public static void main(String[] args) {
int a = 150;
boolean b = a < 0 || a > 10; //或运算要求两边只要有至少一边满足,结果就为true,如果都不满足,那么就是false
}

或运算同样会出现短路的情况,比如下面的例子:

1
2
3
4
public static void main(String[] args) {
int a = -9;
boolean b = a < 0 || a > 10; //此时上来就满足条件
}

因为第一个判断表达式就直接得到了true,那么第二个表达式无论是真还是假,结果都一定是true,所以说没必要继续向后进行判断了,直接得到结果true

我们来看看下面的结果是什么:

1
2
3
4
5
public static void main(String[] args) {
int a = 10;
boolean b = a++ > 10 && ++a == 12;
System.out.println("a = "+a + ", b = "+b);
}

得到结果为:

image-20220917224320699

这是为什么呢?很明显我们的判断中a进行了两次自增操作,但是最后a的结果却是11,这是因为第一个表达式判断的结果为false,由于此时进行的是与运算,所以说直接短路,不会再继续判断了,因此第二个表达式就不会执行。

当然,除了与运算和或运算,还有一个非运算,这个就比较简单了,它可以将结果变成相反的样子,比如:

1
2
3
4
public static void main(String[] args) {
int a = 10;
boolean b = !(a > 5); //对a>5的判断结果,进行非运算
}

因为上面的a > 5判断为真,此时进行非运算会得到相反的结果,所以说最后b就是false了。

最后我们还需要介绍一个叫做三元运算符的东西,三元运算符可以根据判断条件,返回不同的结果,比如我们想要判断:

  • 当a > 10时,给b赋值’A’
  • 当a <= 10时,给b赋值’B’

我们就可以使用三元运算符来完成:

1
2
3
4
5
public static void main(String[] args) {
int a = 10;
char b = a > 10 ? 'A' : 'B'; //三元运算符需要三个内容,第一个是判断语句,第二个是满足判断语句的值,第三个是不满足判断语句的值
System.out.println(b);
}

三元运算符:

1
判断语句 ? 结果1 : 结果2

因此,上面的判断为假,所以说返回的是结果2,那么最后b得到的就是B这个字符了。

最后,我们来总结整个运算符板块学习到的所有运算符:

优先级 运算符 结合性
1 ( ) 从左向右
2 ~ - + (强制类型转换) ++ – 从右向左
3 * / % 从左向右
4 + - 从左向右
5 << >> >>> 从左向右
6 > < >= >= 从左向右
7 == != 从左向右
8 & 从左向右
9 ^ 从左向右
10 | 从左向右
11 && 从左向右
12 || 从左向右
13 ? : 从右向左
14 = += -= *= /= %= &= |= ^= <<= >>= >>>= 从右向左

至此,我们已经学习了Java基础部分中所有的运算符。


流程控制

我们的程序都是从上往下依次运行的,但是,仅仅是这样还不够,我们需要更加高级的控制语句来使得程序更加有趣。比如,判断一个整数变量,大于1则输出yes,小于1则输出no,这时我们就需要用到选择结构来帮助我们完成条件的判断和程序的分支走向。学习过C语言就很轻松!

在前面我们介绍了运算符,我们可以通过逻辑运算符和关系运算符对某些条件进行判断,并得到真或是假的结果。这一部分我们将继续使用这些运算符进行各种判断。

代码块与作用域

在开始流程控制语句之前,我们先来介绍一下代码块和作用域。

不知道各位小伙伴是否在一开始就注意到了,为什么程序中会有一些成对出现的花括号?这些花括号代表什么意思呢?

1
2
3
4
5
public class Main {   //外层花括号
public static void main(String[] args) { //内层花括号开始

} //内层花括号结束
}

我们可以在花括号中编写一句又一句的代码,实际上这些被大括号囊括起来的内容,我们就称为(代码块),一个代码块中可以包含多行代码,我们可以在里面做各种各样的事情,比如定义变量、进行计算等等。

我们可以自由地创建代码块:

1
2
3
4
5
6
7
public static void main(String[] args) {   //现目前这个阶段,我们还是在主方法中编写代码,不要跑去外面写
System.out.println("外层");
{ //自由创建代码块
int a = 10;
System.out.println(a);
}
}

虽然创建了代码块,但实际上程序依然是按照从上到下的顺序在进行的,所以说这里还是在逐行运行,即使使用花括号囊括。那咋一看这不就是没什么卵用吗?我们来看看变量。

我们创建的变量,实际上是有作用域的,并不是在任何地方都可以使用,比如:

image-20220917231014796

变量的使用范围,仅限于其定义时所处的代码块,一旦超出对应的代码块区域,那么就相当于没有这个变量了。

1
2
3
4
5
6
7
public static void main(String[] args) {
int a = 10; //此时变量在最外层定义
{
System.out.println(a); //处于其作用域内部的代码块可以使用
}
System.out.println(a); //这里肯定也可以使用
}

我们目前所创建的变量都是局部变量(有范围限制),后面我们会介绍更多种类型的变量,了解了代码块及作用域之后,我们就可以正式开启流程控制语句的学习了。

选择结构

某些时候,我们希望进行判断,只有在条件为真时,才执行某些代码,这种情况就需要使用到选择分支语句,首先我们来认识一下if语句:

1
if (条件判断) 判断成功执行的代码;
1
2
3
4
5
6
public static void main(String[] args) {
int a = 15;
if(a == 15) //只有当a判断等于15时,才会执行下面的打印语句
System.out.println("Hello World!");
System.out.println("我是外层"); //if只会对紧跟着的一行代码生效,后续的内容无效
}

if会进行判断,只有判断成功时才会执行紧跟着的语句,否则会直接跳过,注意,如果我们想要在if中执行多行代码,需要使用代码块将这些代码囊括起来(实际上代码块就是将多条语句复合到一起)所以说,我们以后使用if时,如果分支中有多行代码需要执行,就需要添加花括号,如果只有一行代码,花括号可以直接省略,包括我们后面会讲到的else、while、for语句都是这样的。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int a = 15;
if(a > 10) { //只有判断成功时,才会执行下面的代码块中内容,否则直接跳过
System.out.println("a大于10");
System.out.println("a的值为:"+a);
}
System.out.println("我是外层");
}

如果我们希望判断条件为真时执行某些代码,条件为假时执行另一些代码,我们可以在后面继续添加else语句:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
int a = 15;
if(a > 10) { //只有判断成功时,才会执行下面的代码块中内容,否则直接跳过
System.out.println("a大于10");
System.out.println("a的值为:"+a);
} else { //当判断不成功时,会执行else代码块中的代码
System.out.println("a小于10");
System.out.println("a的值为:"+a);
}
System.out.println("我是外层");
}

if-else语句就像两个分支,跟据不同的判断情况从而决定下一步该做什么,这跟我们之前认识的三元运算符性质比较类似。

那如果此时我们需要判断多个分支呢?比如我们现在希望判断学生的成绩,不同分数段打印的等级不一样,比如90以上就是优秀,70以上就是良好,60以上是及格,其他的都是不及格,那么这种我们又该如何判断呢?要像这样进行连续判断,我们需要使用else-if来完成:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
int score = 2;
if(score >= 90) //90分以上才是优秀
System.out.println("优秀");
else if (score >= 70) //当上一级if判断失败时,会继续判断这一级
System.out.println("良好");
else if (score >= 60)
System.out.println("及格");
else //当之前所有的if都判断失败时,才会进入到最后的else语句中
System.out.println("不及格");
}

当然,if分支语句还支持嵌套使用,比如我们现在希望低于60分的同学需要补习,0-30分需要补Java,30-60分需要补C++,这时我们就需要用到嵌套:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
int score = 2;
if(score < 60) { //先判断不及格
if(score > 30) //在内层再嵌套一个if语句进行进一步的判断
System.out.println("学习C++");
else
System.out.println("学习Java");
}
}

除了if自己可以进行嵌套使用之外,其他流程控制语句同样可以嵌套使用,也可以与其他流程控制语句混合嵌套使用。这样,我们就可以灵活地使用if来进行各种条件判断了。

前面我们介绍了if语句,我们可以通过一个if语句轻松地进行条件判断,然后根据对应的条件,来执行不同的逻辑,当然除了这种方式之外,我们也可以使用switch语句来实现,它更适用于多分支的情况:

1
2
3
4
5
switch (目标) {   //我们需要传入一个目标,比如变量,或是计算表达式等
case 匹配值: //如果目标的值等于我们这里给定的匹配值,那么就执行case后面的代码
代码...
break; //代码执行结束后需要使用break来结束,否则会溜到下一个case继续执行代码
}

比如现在我们要根据学生的等级进行分班,学生有ABC三个等级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
char c = 'A';
switch (c) { //这里目标就是变量c
case 'A': //分别指定ABC三个匹配值,并且执行不同的代码
System.out.println("去尖子班!准备冲刺985大学!");
break; //执行完之后一定记得break,否则会继续向下执行下一个case中的代码
case 'B':
System.out.println("去平行班!准备冲刺一本!");
break;
case 'C':
System.out.println("去职高深造。");
break;
}
}

switch可以精准匹配某个值,但是它不能进行范围判断,比如我们要判断分数段,这时用switch就很鸡肋了。

当然除了精准匹配之外,其他的情况我们可以用default来表示:

1
2
3
4
5
switch (目标) {
case: ...
default:
其他情况下执行的代码
}

我们还是以刚才那个例子为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
char c = 'A';
switch (c) {
case 'A':
System.out.println("去尖子班!");
break;
case 'B':
System.out.println("去平行班!");
break;
case 'C':
System.out.println("去差生班!");
break;
default: //其他情况一律就是下面的代码了
System.out.println("去读职高,分流");
}
}

当然switch中可以继续嵌套其他的流程控制语句,比如if:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
char c = 'A';
switch (c) {
case 'A':
if(c == 'A') { //嵌套一个if语句
System.out.println("去尖子班!");
}
break;
case 'B':
System.out.println("去平行班!");
break;
}
}

目前,我们已经认识了两种选择分支结构语句。

循环结构

通过前面的学习,我们了解了如何使用分支语句来根据不同的条件执行不同的代码,我们接着来看第二种重要的流程控制语句:循环语句。

我们在某些时候,可能需要批量执行某些代码:

1
2
3
4
5
public static void main(String[] args) {
System.out.println("伞兵一号卢本伟准备就绪!"); //把这句话给我打印三遍
System.out.println("伞兵一号卢本伟准备就绪!");
System.out.println("伞兵一号卢本伟准备就绪!");
}

遇到这种情况,我们由于还没学习循环语句,那么就只能写N次来实现这样的多次执行。但是如果此时要求我们将一句话打印100遍、1000遍、10000遍,那么我们岂不是光CV代码就要搞一下午?

现在,要解决这种问题,我们可以使用for循环语句来多次执行:

1
for (表达式1;表达式2;表达式3) 循环体;

介绍一下详细规则:

  • 表达式1:在循环开始时仅执行一次。
  • 表达式2:每次循环开始前会执行一次,要求为判断语句,用于判断是否可以结束循环,若结果为真,那么继续循环,否则结束循环。
  • 表达式3:每次循环完成后会执行一次。
  • 循环体:每次循环都会执行一次循环体。

一个标准的for循环语句写法如下:

1
2
3
4
5
public static void main(String[] args) {
//比如我们希望让刚刚的打印执行3次
for (int i = 0; i < 3; i++) //这里我们在for语句中定义一个变量i,然后每一轮i都会自增,直到变成3为止
System.out.println("伞兵一号卢本伟准备就绪!"); //这样,就会执行三轮循环,每轮循环都会执行紧跟着的这一句打印
}

我们可以使用调试来观察每一轮的变化,调试模式跟普通的运行一样,也会执行我们的Java程序,但是我们可以添加断点,也就是说当代码运行到断点位置时,会在这里暂停,我们可以观察当代码执行到这个位置时各个变量的值:

image-20220918112006020

调试模式在我们后面的学习中非常重要,影响深远,所以说各位小伙伴一定要学会。调试也很简单,我们只需要点击右上角的调试选项即可(图标像一个小虫子一样,因为调试的英文名称是Debug)

image-20220918112101677

调试开始时,我们可以看到程序在断点位置暂停了:

image-20220918112227207

此时我们可以观察到当前的局部变量i的值,也可以直接在下方的调试窗口中查看:

image-20220918112409944

随着循环的进行,i的值也会逐渐自增:

image-20220918112628585

i增长到2时,此时来到最后一轮循环,再继续向下运行,就不再满足循环条件了,所以说此时就会结束循环。

当然,如果要执行多条语句的话,只需要使用花括号囊括就行了:

1
2
3
4
for (int i = 0; i < 3; i++) {
System.out.println("伞兵一号卢本伟准备就绪!");
System.out.println("当前i的值为:"+i);
}

注意这里的i仅仅是for循环语句中创建的变量,所以说其作用域被限制在了循环体中,一旦离开循环体,那么就无法使用了:

image-20220918112923978

但是我们可以将i的创建放到外面:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int i = 0; //在外面创建变量i,这样全部范围内都可以使用了
for (; i < 3; i++) { //for循环的三个表达式并不一定需要编写
System.out.println("伞兵一号卢本伟准备就绪!");
System.out.println("当前i的值为:"+i);
}
System.out.println("当前i的值为:"+i);
}

和之前的if一样,for循环同样支持嵌套使用:

1
2
3
4
5
public static void main(String[] args) {
for (int i = 0; i < 3; i++) //外层循环执行3次
for (int j = 0; j < 3; j++) //内层循环也执行3次
System.out.println("1!5!");
}

上面的代码中,外层循环会执行3轮,而整个循环体又是一个循环语句,那么也就是说,每一轮循环都会执行里面的整个循环,里面的整个循环会执行3,那么总共就会执行3 x 3次,也就是9次打印语句。

实际上,for循环的三个表达式并不一定需要编写,我们甚至可以三个都不写:

1
2
3
4
public static void main(String[] args) {
for (;;) //如果什么都不写,相当于没有结束条件,这将会导致无限循环
System.out.println("伞兵一号卢本伟准备就绪!");
}

如果没有表达式2,那么整个for循环就没有结束条件,默认会判定为真,此时就会出现无限循环的情况(无限循环是很危险的,因为它会疯狂地消耗CPU资源来执行循环,可能很快你的CPU就满载了,一定要避免)

当然,我们也可以在循环过程中提前终止或是加速循环的进行,这里我们需要认识两个新的关键字:

1
2
3
4
5
6
7
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
if(i == 1) continue; //比如我们希望当i等于1时跳过这一轮,不执行后面的打印
System.out.println("伞兵一号卢本伟准备就绪!");
System.out.println("当前i的值为:"+i);
}
}

我们可以使用continue关键字来跳过本轮循环,直接开启下一轮。这里的跳过是指,循环体中,无论后面有没有未执行的代码,一律不执行,比如上面的判断如果成功,那么将执行continue进行跳过,虽然后面还有打印语句,但是不会再去执行了,而是直接结束当前循环,开启下一轮。

在某些情况下,我们可能希望提前结束循环:

1
2
3
4
5
for (int i = 0; i < 3; i++) {
if(i == 1) break; //我们希望当i等于1时提前结束
System.out.println("伞兵一号卢本伟准备就绪!");
System.out.println("当前i的值为:"+i);
}

我们可以使用break关键字来提前终止整个循环,和上面一样,本轮循环中无论后续还有没有未执行的代码,都不会执行了,而是直接结束整个循环,跳出到循环外部。

虽然使用break和continue关键字能够更方便的控制循环,但是注意在多重循环嵌套下,它只对离它最近的循环生效(就近原则):

1
2
3
4
5
6
for (int i = 1; i < 4; ++i) {
for (int j = 1; j < 4; ++j) {
if(i == j) continue; //当i == j时加速循环
System.out.println(i+", "+j);
}
}

这里的continue加速的对象并不是外层的for,而是离它最近的内层for循环,break也是同样的规则:

1
2
3
4
5
6
for (int i = 1; i < 4; ++i) {
for (int j = 1; j < 4; ++j) {
if(i == j) break; //当i == j时终止循环
System.out.println(i+", "+j);
}
}

那么,要是我们就是想要终止或者是加速外层循环呢?我们可以为循环语句打上标记:

1
2
3
4
5
6
outer: for (int i = 1; i < 4; ++i) {   //在循环语句前,添加 标签: 来进行标记
inner: for (int j = 1; j < 4; ++j) {
if(i == j) break outer; //break后紧跟要结束的循环标记,当i == j时终止外层循环
System.out.println(i+", "+j);
}
}

如果一个代码块中存在多个循环,那么直接对当前代码块的标记执行break时会直接跳出整个代码块:

1
2
3
4
5
6
7
8
9
10
outer: {    //直接对整个代码块打标签
for (int i = 0; i < 10; i++) {
if (i == 7){
System.out.println("Test");
break outer; //执行break时,会直接跳出整个代码块,而不是第一个循环
}
}

System.out.println("???");
}

虽然效果挺奇特的,但是一般情况下没人这么玩,所以说了解就行了。

前面我们介绍了for循环语句,我们接着来看第二种while循环,for循环要求我们填写三个表达式,而while相当于是一个简化版本,它只需要我们填写循环的维持条件即可,比如:

1
while(循环条件) 循环体;

相比for循环,while循环更多的用在不明确具体的结束时机的情况下,而for循环更多用于明确知道循环的情况,比如我们现在明确要进行循环10次,此时用for循环会更加合适一些,又比如我们现在只知道当i大于10时需要结束循环,但是i在循环多少次之后才不满足循环条件我们并不知道,此时使用while就比较合适了。

1
2
3
4
5
6
7
public static void main(String[] args) {
int i = 100; //比如现在我们想看看i不断除以2得到的结果会是什么,但是循环次数我们并不明确
while (i > 0) { //现在唯一知道的是循环条件,只要大于0那么就可以继续除
System.out.println(i);
i /= 2; //每次循环都除以2
}
}

上面的这种情况就非常适合使用while循环。

和for循环一样,while也支持使用break和continue来进行循环的控制,以及嵌套使用:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int i = 100;
while (i > 0) {
if(i < 10) break;
System.out.println(i);
i /= 2;
}
}

我们也可以反转循环判断的时机,可以先执行循环内容,然后再做循环条件判断,这里要用到do-while语句:

1
2
3
4
5
6
7
public static void main(String[] args) {
int i = 0; //比如现在我们想看看i不断除以2得到的结果会是什么,但是循环次数我们并不明确
do { //无论满不满足循环条件,先执行循环体里面的内容
System.out.println("Hello World!");
i++;
} while (i < 10); //再做判断,如果判断成功,开启下一轮循环,否则结束
}

至此,面向过程相关的内容就讲解完毕了,从下一章开始,我们将进入面向对象编程的学习(类、数组、字符串)


实战练习

面向过程的内容全部学习完成,我们来做几个练习题吧!

寻找水仙花数

“水仙花数(Narcissistic number)也被称为超完全数字不变数(pluperfect digital invariant, PPDI)、自恋数、自幂数、阿姆斯壮数或阿姆斯特朗数(Armstrong number),水仙花数是指一个 3 位数,它的每个位上的数字的 3次幂之和等于它本身。例如:1^3 + 5^3+ 3^3 = 153。”

现在请你设计一个Java程序,打印出所有1000以内的水仙花数。

打印九九乘法表

img

现在我们要做的是在我们的程序中,也打印出这样的一个乘法表出来,请你设计一个Java程序来实现它。

img

斐波那契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从 1963 年起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

斐波那契数列:1,1,2,3,5,8,13,21,34,55,89…,不难发现一个规律,实际上从第三个数开始,每个数字的值都是前两个数字的和,现在请你设计一个Java程序,可以获取斐波那契数列上任意一位的数字,比如获取第5个数,那么就是5。

1
2
3
4
5
6
7
public static void main(String[] args) {
int target = 7, result; //target是要获取的数,result是结果

//请在这里实现算法

System.out.println(result);
}