Java中的表达式和语句

《Java中的表达式和语句》源站链接,阅读体验更佳~
首先说明一点,在学习一门语言的时候,我并不会花费大量的时间和精力去对基础的语法做笔记,编程是一门实践科学,学习一门语言最好的方式就是使用一门语言。

《高级语言中的单词——5种类型的token》一文中我们曾经提到一句话——对于任何计算机语言来说,必定是“用规定的文法,去表达特定语义,最终操作运行时的”一个过程。

对于一门语言的语法,这应该是最基本的,最表面的知识。无论是多么复杂的语法,无论语法所代表的语义和运行时过程多么复杂,比如JavaScript中的解构语法、Rust语言中的模式匹配语法等,相信在使用过一段时间之后我们也可以形成“肌肉记忆”。

所以在我写文章的时候,很少会花大篇幅来对语言的语法进行总结。即使是有总结,也是对一些比较复杂的语法的思考,以及一些经常被忽略,但是特定场景下可以起到很大作用的语法知识。

所以,这篇文章我重点介绍的是Java中的switch语句的贯穿特性以及带标签的break语句和带标签的continue语句。

程序的三种基本结构

无论我们使用哪种编程范式(无论是结构化编程范式,面向对象的编程范式还是函数式编程范式),书写算法都是必须的,而算法的实现过程是由一系列操作组成的,这些操作之间的执行次序就是程序的控制结构。

在以前,很多编程语言都提供了goto语句,goto语句非常灵活,可以让程序的控制流程任意流转。但是goto语句太随意了,大量使用goto语句会使得程序难以理解并且容易出错。

1996年,计算机科学家Bohm和Jacopini证明了这样的事实:**任何简单或者复杂的算法都可以有顺序结构,分支(选择)结构和循环结构这三种基本结构组合而成。**所以这三种结构就被称为程序设计的三种基本结构。

不论哪一种编程语言,都会提供两种基本的流程控制结构:分支结构和循环结构。

其中分支结构用于实现根据条件来选择性地执行某段代码,循环结构则用于实现根据循环条件重复执行某段代码,通过这两种控制结构,就可以改变程序原来顺序执行的顺序,实现流程的控制进而实现任意复杂的算法。

Java中也为我们提供了分支和循环语句,同时还提供了其他的一些语句用来更加灵活的控制程序流程。

Java的语法是类C语言的语法,这种类型的语法应该是最广为人知的一类了。所以这里我们就不一一介绍各种语句的语法结构了,只重点介绍一些有趣的特性。

switch语句

switch语句由一个控制表达式和多个case标签组成,switch语句根据表达式的值将控制流程转移到了多个case标签中的某一个上。其形式如下:

switch (expression) {
    case value1: {
        statement...;
        break;
    }
    case value2: {
        statement...;
        break;
    }
    ...
    case valuen: {
       statement...;
       break;
    }
    default: {
        statement...;
    }
}

switch语句先执行对expression的求值,然后依次匹配value1、value2…valuen,遇到匹配的值则从对应的case块开始向下执行代码,如果没有任何一个case块能够匹配,则会执行default块中的代码。

需要注意的是Java中的switch语句后面的表达式并不是支持任意类型的,甚至在Java7之前都不能支持String类型,Java7之后,对switch语句进行了一定的增强,switch语句后的控制表达式的数据类型只能是byteshortcharint四种整型类型,还有枚举类型String类型。

  • switch中不支持浮点数,因为二进制保存浮点数字不是完全精确的,所以对浮点数进行相等性判断并不可靠
  • switch中不支持boolean类型的数据,因为对于boolean类型,使用if语句是更好的选择
  • switch中不支持long整型,我认为一个原因是根本使用不了这么多的分支

同时,我们需要注意的是,case关键字后面只能跟一个和switch中表达式的数据类型相同的常量表达式

switch的case贯穿

需要注意的是,switch语句并不是一个多选一的分支控制语句,考虑到物理上的多种情况可能是逻辑上的一种情况switch块中语句的执行会贯穿标号,除非遇到break语句。如下代码:

public void switchTest(int a) {
    switch (a) {
        case 1: System.out.print("1");
        case 2: System.out.print("2");
        case 3: System.out.print("3");
        default: System.out.print("default");
    }
}

switchTest(1);

上面的代码中执行switchTest(1)方法输出的结果如下:

123default

但是如果在某个case块中加入break语句,那么这个case块就不会被贯穿,如下:

public void switchTest(int a) {
    switch (a) {
        case 1: {
            System.out.print("1");
            break;
        }
        case 2: System.out.print("2");
        case 3: System.out.print("3");
        default: System.out.print("default");
    }
}

switchTest(1);
switchTest(2);

上面的代码中执行switchTest(1)输出结果为:

1

可见加入break之后没有被贯穿,

而执行switchTest(2)输出结果为:

23default

其实case代码块和其他的代码块在本质上是一样的,它有自己的作用域,如下:

public static void main(String[] args) {
       int a = 1;
       switch(a) {
           case 1: {
               int b = 1;
               System.out.println(b);
           }
           case 2: {
               int b = 2;
               System.out.println(b);
           }
       }
    }

上述main方法执行的结果是

1
2

case代码块和如下的代码块本质上是一样的:

//普通的代码块,使用的主要目的就是创建一个单独的词法作用域
{
	int a = 3;
    System.out.println(a);
}

//带标签的代码块,除了能够创建一个单独的词法作用域,还能够使用break进行一定的流程控制
labelName: {
    int a = 3;
    System.out.println(a);
}

case代码块不同的地方在于,case代码块只能出现在switch语句中,并且其后面跟着一个跟switch中的表达式的值的类型相同的常量表达式,其实case的作用就是声明了一个个的锚点,能够跟switch后的表达式的值匹配的哪个case锚点就是switch程序开始执行的地方,这也就解释了switch的贯穿

基本上所有拥有switch语句的语言中,case的作用大都是相同的,同时switch也都具有贯穿的特性。

由于switch中的case会被贯穿,当我们遇到多个case需要使用同样的逻辑的时候往往就会向下面这样写:

switch(exp) {
    case 1:
    case 2:
      dosomething1();
      break;
    case 3:
    case 4:
    case 5:
      dosomething2();
    default:
      dosomethingElse();
}

某些语言对于这种情况又更加优雅的语法,比如Kotlin中的when:

fun ageDes(age: Int): String = when {
 age < 18 -> "未成年"
 age in 18 until 30 -> "青年"
 age in 30 until 40 -> "壮年"
 age in 40 .. 55 -> "中年"
 else -> "老年"
}

不带break的case语句具有状态模式的特点

我们在书写switch case语句的时候,大多数情况下会强调为每一个case语句提供break,以防止逻辑的错误执行。但是不为case提供break也有其独特的应用场景,我们可以用不带break的switch case来实现一个简单的状态模式。

本人就遇到过一个非常典型的场景:我们的代码会从MQ中接收一个消息进行处理,而消息的处理分为多个步骤,而处理消息的过程中每一个步骤都有可能发生错误而导致消息的处理被终止,然后过一段时间需要对消息进行重新处理,这个时候已经处理过的步骤就不用再进行处理了。消息体中有一个step字段来记录消息处理到了哪一个步骤。

我们的代码大体如下:

public void processMessage(Message message) {
    loadMessage(message); // 从数据库中加载当前消息的步骤等信息
    switch(message.step) {
        case 1:
           processStep1(message);
           increMessgeStep(message);// message的步骤自增并保存到库中
        case 2:
           processStep2(message);
           increMessgeStep(message);
        case 3:
           processStep3(message);
           increMessgeStep(message);
        ...
        case n:
           processStepN(message);
           increMessgeStep(message);
        default:
           postProcess(message);
           finishMessage(message); // 把消息标记为已经处理完成
    }
}

采用了上面的写法之后,如果我们在处理消息的某个步骤失败了,消息进行重试的时候就会接着失败的步骤继续向下进行处理。当然,在每一个步骤的处理中需要注意保证幂等,因为有可能某个步骤执行了一部分才发生错误。

上面的写法其实就实现了一个简单的状态模式,虽然不够优雅,但是简单实用。甚至我们可以把上面的case 1、case 2等改成case 10、case 20,在每个步骤之间留出足够的间隙,以便以后在步骤之间插入新的步骤。

带标签的break语句

break语句会将控制流程转移到包含它的语句或者块之外。不带标号的 break语句只能用在循环结构或者是switch语句之中,而带标号的 break可以使用在任何语句块中。

不带标号的break语句视图将控制流转移到包围它的最内层的switchdowhile或者for语句中(一定要注意这一点,因为这四种语句之间可以相互嵌套或者是自嵌套)。这条语句被称为break目标,然后立即正常结束。

如果该不带标号的break语句不被包含在任何switchdowhile或者for语句中,那么就会编译失败。

在了解带标签的break语句之前,我们先来了解一下什么是标号语句

语句前可以有标号前缀,如下:

label: {
    System.out.println("a1");
}

java编程语言没有任何goto语句,标识符语句标号会用于出现在标号语句内的任何地方的break或者continue语句之上。

标号本质上其实也是一个标识符,而这个标识符的作用域就是其直接包含的语句的内部,因为Java中规定,只能在该标号所包含的语句之中才能引用这个标号。,如下代码说明了标号语句的标号标识符的作用域:

public class a {
    public static void main(String[] args) {

        a: {
            int a = 3;
            System.out.println("a1");
        }
		
        //这个地方编译通过, 说明a的作用域并不是在main方法中,而是在它包含的语句之中
        a: {
            System.out.println("a2");
        }
    }
}

同时,通过上面的代码我们也可以看出,不同类型的标识符之间是互不影响的(Java中的标识符有类型名、变量名、标号等等),即对于将相同的标识符同时用做标号和包名、类名、接口名、方法名、域名城、方法参数名、或者局部变量名这种做法,Java没有做出任何限制

带有标号的break语句视图将控制流转移到将相同标号作为其标号的标号语句,该语句称为break目标语句,然后立即结束。也就是说他会让代码接着标号语句的下一条语句继续执行,如下:

public static void main(String[] args) {
    outFor: for (int i = 0; i < 100; i++) {
        System.out.println(i);
        for (int i1 = 0; i1 < 2; i1++) {
            System.out.println(i1);
            if (i > 10) {
                break outFor;
            }
        }
    }
    System.out.println("嵌套循环执行完成");
}

上面的代码中,第7行的break outFor会直接把外层的for循环给中断掉,当然,在带标号的break语句中break目标不必是switchdowhile或者for语句

public static void main(String[] args) {
    String name = "laomst";
    setName: {
        int age = getAge(); // 从某个地方获取age
        if (age < 18) {
            break setName;
        }
        name = "oldMa";
    }
}

当然,上面的写法没有任何实际意义,但凡脑子正常的程序员就不会这么写,在这里只是为了演示才编造出了上面的代码。

需要注意的是,前面讲到过标号语句表标号的作用范围是在标号语句的语句体内部,所以被break的标号语句必须包裹break语句

前面的描述中称“视图转移控制流程”而不是"直接转移控制流程",是因为如果在 break目标 内有任何try语句,其try子句或者catch子句包含break语句,那么在控制流程转移到 break 目标之前,这些 try语句的所有的 finally 子句会按照从最内部到最外部的顺序被执行,而 finally 子句的猝然结束都会打断break语句触发的控制流转移

带标签的continue语句

continue语句只能出现在while、do或者for语句汇总,这三种语句被称为迭代语句。控制流会传递到迭代语句的循环持续点上。

continue语句也有代标号和不带标号两种使用方式,和break语句不同的是,continue语句的这两种使用方式都只能出现在迭代语句中

不带标签的continue试图将控制流转移到包围它的最内层的switchdowhile或者for语句中(一定要注意这一点,因为这四种语句之间可以相互嵌套或者是自嵌套)。这条语句被称为continue目标,然后立即结束当前的迭代并进行下一轮的迭代。

如果该不带标号的continue语句不被包含在任何switchdowhile或者for语句中,那么就会编译失败。

带标号的continue语句视图将控制流转移到将相同标号作为标号的标号语句,但是同时,这个标号语句必须是一个迭代语句,这条语句被称为continue目标然后理解结束当前的迭代并进行新一轮的迭代。

如果continue目标不是一个迭代语句,那么就会产生一个编译时错误。

因为有了这个限制,带标签的continue语句唯一的作用就是在嵌套循环中临时跳过一次外层循环:

public static void main(String[] args) {
    outFor: for (int i = 0; i < 100; i++) {
        System.out.println(i);
        for (int i1 = 0; i1 < 100; i1++) {
            System.out.println(i1);
            if (i1 > 10) {
                continue outFor;
            }
        }
    }
    System.out.println("嵌套循环执行完成");
}

前面的描述中称“视图转移控制流程”而不是"直接转移控制流程",是因为如果在 continue目标 内有任何try语句,其try子句或者catch子句包含continue语句,那么在控制流程转移到 continue目标之前,这些 try语句的所有的 finally 子句会按照从最内部到最外部的数学被执行,而 finally 子句的猝然结束都会打断 continue 语句触发的控制流转移

Java中语句的合法性

语言的静态期检查是分很多方面的,比如有的语言支持静态期对变量的类型检查(对于静态类型的语言这是最基本的也是强制的要求,而动态类型语言的静态期变量类型检查的典型代表是TypeScript语言),除此之外,有的语言还会进行其他方面的检查,比如Go语言不允许声明未使用的变量(其他语言也可以通过lint工具来配置类似的检查):

image-20220720232559972

Java语言不会限制你声明未使用的变量,因为这种悬垂变量Java编译器会把它给优化掉,但是Java语言中有两个额外的静态检查——语句是否具有副作用以及语句是否可达

没有副作用的语句是不合法的

程序中的许多动作都是通过计算表达式而完成的,计算一个表达式要么是为了他们的副作用,例如对变量赋值;要么是为了得到他们的值,将其用做更大的表达式的引元或操作数,要么用来改变语句的执行顺序(如if中接收bool值进行流程控制),这个时候其实发生了隐式的副作用,我们可以假定有一个变量接收了这个表达式的值。

基于这个原因,很多的编程语言中如果一个表达式不满足使用表达式的目的,那么这个表达式就不能作为一条单独的语句存在。也就是说,如果一条语句被执行之后,对于程序的上下文没有任何的实际作用,那么它就是不合法的,在Java中就是这样的,如下:

int a = 0;
int b = 1;
a+b; //ERROR,是一个正确的表达式,但是由于不能满足使用表达式的目的,把他作为一个语句执行没有任何意义,所以Java认为这不是一个合法的语句。

这种类型的检查主要是针对纯粹使用操作符构建的表达式语句的。如果语句中掺杂了方法调用,这个时候编译器单凭静态检查是无法确定被调用的方法是否具有副作用的,这个时候安全起见编译器只能认为这个语句是合法的。

不可达语句是不合法的

还存在一种情况,如果某条语句因为它是不可达的而永远不能被执行,在java中认为这是不合法的,是一个编译时错误。

image-20220720233455713

比如上面的println函数,其实是永远不可能被执行的,这种情况下Java认为这是一个语法错误。

在不可达表现的非常明显的时候IDE就会给出提示,但是有时候会有错误的逻辑造成的不可达语句,就需要我们自己对代码的结构进行分析了。

总结

这篇文章中,我们介绍了一些不太常用的Java语法特性,比如我们可以用不带break的switch语句来实现一个简单的状态模式,可以用带标签的break和带标签的continue语句来实现更加精细的流程控制。但是带标签的break和continue在一定程度上和goto比较类似,我们知道在结构化编程中,goto语句是被禁止使用的,何况Java还是一门面向对象的编程语言,所以对于带标号的break和continue语句,我们应该谨慎使用甚至禁止使用。

感谢你耐心读完。本人深知技术水平和表达能力有限,如果文中有什么地方不合理或者你有其他不同的思考和看法,欢迎随时和我进行讨论(laomst@163.com)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

劳码识途

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值