Note/青空笔记/JavaSE 笔记 2023重制版/JavaSE笔记(二)重制版.md

81 KiB
Raw Blame History

image-20220916174714019

面向过程篇

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

Java程序基础

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

程序代码基本结构

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

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

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

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

public class Main {
  
}

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

image-20220916213529426

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

image-20220916213641899

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

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

public static void main(String[] args) {
    
}

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

比如我们之前编写的:

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

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

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

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

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

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中我们也可以让变量去代表一个具体的值并且变量的值是可以发生变化的。

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

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

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

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

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

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

image-20220916224129597

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

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

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

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

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

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

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

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

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

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

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

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

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

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

得到结果:

image-20220916225037221

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

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

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

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

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

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中第一个需要认识的关键字

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

编译时出现:

image-20220916225429474

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

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一个位也叫一个bit8个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代表-00000代表+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~+7Java使用的就是补码在了解了计算机底层的数据表示形式之后我们再来学习这些基本数据类型就会很轻松了。

整数类形

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

在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

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

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

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

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

反之会出现报错:

image-20220916231650085

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

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

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

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

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

image-20220916232420547

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

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

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

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

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

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。

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

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 单精度浮点型 32bit4字节
  • double 双精度浮点型64bit8字节

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

image-20220917102209246

根据国际标准 IEEE 754任意一个二进制浮点数 V 可以表示成下面的形式:


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

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

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

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

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

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

image-20220917105141288

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

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

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

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

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

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

image-20220917105849862

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

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

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了

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

得到结果为:

image-20220917110854266

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

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”开头

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类型可以直接表示一个中文字符:

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

得到结果为:

image-20220917111838629

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

image-20220917112033102

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

image-20220917114628564

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

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

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

布尔类型

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

  • true - 真
  • false - 假

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

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

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

image-20220917115424504

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


运算符

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

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

比如:+ - × ÷

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

比如10 ÷ 2

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

赋值运算符

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

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

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

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

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

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还支持取模运算算术运算同样需要左右两边都有一个拿来计算的目标。

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

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

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

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

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

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变成自己的相反数

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

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

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

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

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

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

image-20220917145901135

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

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

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

image-20220917150010919

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

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

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

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

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

image-20220917141816599

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

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

image-20220917142201392

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

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

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

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

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

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

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

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

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

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

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

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

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

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

比如下面的结果:

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同样满足这个要求。

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

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);
}

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

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

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

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存放的此时我们就可以使用强制类型转换

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

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

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

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

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

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

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

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当我们想要对一个变量进行这样的自增操作时可以

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

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

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

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

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

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

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

第一个结果为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进行自增可能希望以其他的数进行自增操作除了按照之前的方式老老实实写之外

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

我们可以将其缩写:

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

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

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

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

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

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

优先级 运算符 结合性
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

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中并没有乘方运算符^是按位异或运算符,不要记错了。

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

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

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

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

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

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

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

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

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

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

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

我们来总结一下:

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

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

public static void main(String[] args) {
    int c = -1 >> 1;   //正常的右移操作高位补1所以说移了还是-1
    System.out.println(c);
}
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 了,注意,不存在无符号左移。

位移操作也可以缩写:

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

可以缩写为:

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类型一直都没有机会出场,而接下来就是它的主场。

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

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

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

关系判断运算符包括:

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

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

逻辑运算符

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

image-20220917223047110

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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,由于此时进行的是与运算,所以说直接短路,不会再继续判断了,因此第二个表达式就不会执行。

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

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'

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

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

三元运算符:

判断语句 ? 结果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语言就很轻松

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

代码块与作用域

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

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

public class Main {   //外层花括号
    public static void main(String[] args) {   //内层花括号开始
       
    }  //内层花括号结束
}

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

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

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

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

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

image-20220917231014796

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

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

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

选择结构

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

if (条件判断) 判断成功执行的代码;
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语句都是这样的。

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语句

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来完成:

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分需要补Java30-60分需要补C++,这时我们就需要用到嵌套:

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语句来实现,它更适用于多分支的情况:

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

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

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来表示

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

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

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

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;
    }
}

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

循环结构

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

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

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

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

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

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

介绍一下详细规则:

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

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

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时此时来到最后一轮循环再继续向下运行就不再满足循环条件了所以说此时就会结束循环。

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

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

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

image-20220918112923978

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

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循环同样支持嵌套使用

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("15");
}

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

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

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

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

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

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进行跳过,虽然后面还有打印语句,但是不会再去执行了,而是直接结束当前循环,开启下一轮。

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

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

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

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

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也是同样的规则:

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);
    }
}

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

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时会直接跳出整个代码块:

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相当于是一个简化版本它只需要我们填写循环的维持条件即可比如

while(循环条件) 循环体;

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

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来进行循环的控制以及嵌套使用

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

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

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以内的水仙花数。

public class Main {
    public static void main(String[] args) {
        for (int i = 100; i < 1000; i++) {
            int a = i % 10;
            int b = i / 10 % 10;
            int c = i / 100 % 10;
            if (a * a * a + b * b * b + c * c * c == i) {
                System.out.println(i + "是水仙花数");
            }
        }
    }
}

打印九九乘法表

img

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

img

public class Main {
    public static void main(String[] args) {
        for (int i = 1; i <= 9; i++) {
            for (int j = 1; j < 9; j++) {
                if (j <= i) {
                    System.out.print(j + "*" + i + "=" + i * j + "  ");
                }
            }
            System.out.println();
        }
    }
}

斐波那契数列

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

斐波那契数列1123581321345589...不难发现一个规律实际上从第三个数开始每个数字的值都是前两个数字的和现在请你设计一个Java程序可以获取斐波那契数列上任意一位的数字比如获取第5个数那么就是5。

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

    //请在这里实现算法

    System.out.println(result);
}