转载

Java中的操作符、表达式和语句

Java是一门静态强类型的语言,因此如果操作符接收的值的类型不符合操作符规定的类型,就会在编译期产生编译错误,通常IDE会对这种情况进行提示,所以Java的操作符不会跟JavaScript中的操作符那样发生很多的类型转换,其行为更具有确定性,所以只需要进行简单的介绍。

运算符说明 Java运算符
分隔符 . [] (){} , ;
单目运算符 ++ -- - !
强制类型转换符 (type)
乘/除/求余 * / %
加/减 + -
位移运算符 << >> >>>
关系运算符 < <= >= > instanceof
等价运算符 == !=
按位与 &
按位异或 ^
按位或 |
条件与 && (短路)
条件或 || (短路)
三目运算符 ?:
赋值 = += -=*= /= &=|= ^= %=<<= >>= >>>=

特别注意 ==!=

==!= 这两个操作符用来进行相等性的判断,但是需要注意的是,这类似于一个全等操作,对于基本类型的值来说没有任何问题,但是对于引用类型的值来说,应该采用对象内部的 equals() 方法来进行比较。

Java中的表达式和合法的语句

在程序设计语言中,表达式和语句是完全不同的两个概念,在我的理解中:

  • 表达式是有值的,任何一个表达式计算完成之后都会返回一个结果值
  • 语句是一段可执行的代码,它不一定有值。
  • 表达式本身是可以作为语句存在的,我们称之为 表达式语句

通过上面的介绍,我们可以认为表达式是语句的一种特殊情况, 表达式是具有值的语句

同时,我们需要注意,在很多时候,我们提到语句的时候一般认为语句是没有值的,这种情况下的语句被狭义化了。

使用表达式的目的和合法的语句

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

基于这个原因,很多的编程语言中如果一个表达式不满足使用表达式的目的,那么这个表达式就不能作为一条单独的语句存在,也就是说不能运行这个表达式,因为这其实是没有任何意义的,在Java中就是这样的,如下:

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

同时我们需要注意,赋值语句在Java中被认为是表达式,所以赋值语句是有值的,如下:

public static void main(String[] args) {
    int a = 0;
    System.out.println(a = 3+2); //5
    System.out.println(a); //5
}
复制代码

语句

我们接下来要讨论的语句就是被狭义之后的语句,主要的功能是用于控制程序的执行流程。

程序的三种基本结构

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

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

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

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

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

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

顺序结构

任何编程语言中最常见的程序结构就是顺序结构。顺序结构就是程序从上到下逐行地执行,中间没有任何判断和跳转。

如果一个方法的多行代码之间没有任何流程控制,则程序总是从上向下依次执行,排在前面的代码先执行,排在后面的代码后执行。这意味着,没有流程控制,Java中方法的语句是一个顺序执行流,从上向下依次执行每一条语句

分支(选择)结构

Java中提供了两种常见的分支控制结构: if语句switch语句 。其中if语句使用布尔表达式或者布尔值作为分支条件来进行分支控制;而switch则用户对多个值进行匹配,从而实现分支控制。

if语句

if语句使用布尔表达式或者布尔值作为分支条件来进行分支控制。if语句有如下三种形式:

if(logic expression) {
	statement...
}
复制代码
if (logic expression) {
    statement...
} else {
    statement...
}
复制代码
if (logic expression) {
    statement...
} else if(logic expression) {
    statement...
} ...
复制代码

使用if语句的时候需要注意下面几点:

  • 当if和else后面之后一条语句的时候可以省略{},但是通常最好不要省略{}

  • 对于if语句,还有一个很容易出现的逻辑错误。看如下的程序:

    int age = 45;
    if (age > 20) {
        System.out.println("青年人");
    } else if (age > 40) {
        System.out.println("中年人");
    } else if (age > 60) {
        System.out.println("老年人");
    }
    复制代码

    表面上看来,上面的程序没有任何问题,但是age=45程序的运行结果却是“青年人”,这显然是有问题的!

    **对于任何 if else 语句,表面上看起来else后没有任何条件,或者 esle if 后面只有一个条件——但是这只是表面现象,因为else的含义是"否则"—— else本身就是一个条件! else 的隐含条件是对前面的条件取反。 **因此,上面的代码可以修改为:

    int age = 45;
    if (age > 60) {
        System.out.println("老年人");
    } else if (age > 40) {
        System.out.println("中年人");
    } else if (age > 20) {
        System.out.println("青年人");
    }
    复制代码

    上面的程序运行之后就能得到正确的结果,其实上面的程序就等同于下面的这段代码:

    int age = 45;
    if (age > 60) {
        System.out.println("老年人");
    }
    //在原本的if条件中增加了else的隐含条件
    if (age > 40 && !(age > 60)) {
        System.out.println("中年人");
    }
    //在原本的if条件中增加了else的隐含条件
    if (age > 20 && !(age > 40 && !(age > 60)) && !(age > 60) {
        System.out.println("青年人");
    }
    复制代码

    也就是说上面的判断逻辑转为如下三种情况:

    • age大于60岁,判断为“老年人”
    • age大于40岁,并且age小于等于60岁,判断为“中年人”
    • age大于20岁,并且age小于等于40岁,判断为“青年人”。

    上面的逻辑才是实际希望的判断逻辑。因此,使用 if...else 语句进行流程控制的时候, 一定不要忽略了else所带的隐含条件。

    如果每次都去计算if条件和else条件的交集也是一件麻烦的事情,为了避免出现上述的错误, 在使用 if...else 语句的时候有一条基本规则:总是优先把包含范围小(子集)的条件放在前面处理。

    如 age>60是age>40的子集,而age>40是age>20的子集,把自己放在前面进行处理就可以避免忽略else的隐含条件而造成程序错误。

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 块中的代码。

switch支持的数据类型

Java7之后,对switch语句进行了一定的增强,switch语句后的控制表达式的数据类型只能是 byteshortcharint 四种整型类型,还有 枚举类型String 类型。

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

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

switch的贯穿

需要注意的是,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
复制代码

关于 break 语句后面会有详细的介绍

我对case代码块的理解。

其实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也都具有贯穿的特性。

循环结构

循环语句可以在满足循环条件的情况下,反复执行某一段代码,这段被重复执行的代码被称为循环体。 循环语句可能包含如下四个部分

init_statement
test_expression
loop_statement
iteration_statement

上面的四个部分只是一般性的分类,并不是每个循环中都非常明确的分出了这4个部分。

直到型循环结构

直到型循环结构的特点就是循环体会无条件的执行一次,在Java中提供了 do while 语句对直到型循环提供了支持。

do while 语句

do while 循环的结构如下:

init_statement
do {
	loop_statement
	iteration_statement
	[break/continue] //[]代表可选的
} while(test_expression); //不要忘记末尾的分号
复制代码

需要注意的是,iteration_statement和loop_statement之间并没有明确的顺序关系,但是我们需要注意的是,iteration_statement应该位于continue语句之前,否则会容易造成死循环,如下:

public static void main(String[] args) {
    int i = 0;
    do {
        System.out.println(i);
        if (i%3 == 1)
            continue;
        i++;
    } while (i < 9);
}
复制代码

运行上面的main方法将会产生死循环。 所以迭代语句应该放在continue语句之前

当型循环结构

当型循环结构的特点就是,在执行循环体之前就进行循环条件的检测,如果循环条件在刚开始就不满足,那么循环体一次都不会执行。

Java提供了 while 语句和 for 语句对当型循环结构提供支持

while 语句

while循环的语法格式如下:

[init statement]
while(test_expression) {
	loop_statement
	[iteration_statement]
	[break/continue] //跟do类似的是,如果存在迭代语句,要放在continue语句之前,防止造成死循环
}
复制代码

for 语句

for循环是最简洁的一种循环语句,同时其写法也是最为灵活的,它把循环体独立出来,同时固定了迭代语句和测试表达式的执行顺序,for语句的基本格式如下:

for ([init_statement]; [test_statement], [iteration_statement]) {
    loop_statement
}
复制代码

可以看到,for语句汇总的init_statement、test_statement和iteration_statement都是可选的,这就意味着for循环可以完全取代while循环,for循环是使用频率最高的循环语句,原因如下:

  • 在for循环的初始化语句中声明的变量其作用于可以限制在for循环的语句体之内。
  • for循环的迭代语句没有与循环体放在一起,因此即使是在执行循环体的时候遇到了continue语句提前结束本次循环,迭代语句也会得到执行。

虽然for循环的写法比较自由,但是我们在使用for循环的时候尽量采用标准的写法,这样可以充分利用for循环的特性,同时提高代码的可读性,所以这里就不介绍关于for循环的骚操作了。

增强的 for 语句

Java5中,为 Iterable数组 类型新增了增强的 for 语句,其格式如下:

for ([variableModifier] Type variableDeclaratorId: Expression) {
    loop_statement
}
复制代码

需要注意的是,上述Express的类型必须是 Iterable 或者是 数组类型 ,否则会产生编译错误

增强的for循环只是一种语法糖,它在编译器的解糖阶段就会转换成普通的for循环,增强的for循环会按照如下规则转译为基本的for语句:

  • 如果Expression 的类型是 Iterable 的子类型,那么就按照如下方式进行转译: 以如下代码为例
List<String> nameList = Arrays.asList("zhangsan","lisi","wangwu");

//增强for循环
for(String name: nameList) {
   System.out.println(name);
}

//上述的增强的for循环就等价于如下的普通循环
for (Iterator<String> i = Expression.iterator(); i.hasNext(); ) {
   String name = i.next();
   System.out.println(name);
}
复制代码
  • 否则, Expression必须具有数组类型,这时候其转换方式如下: 以如下代码为例:
String[] nameArr = new String[]{"zhangsan","lisi","wangwu"};

//增强的for循环
for(String name: nameArr) {
   System.out,println(name);
}

//上述的增强的for循环等价于如下的普通for循环
for (int i = 0; i< nameArr.length; i++) {
   String name = nameArr[i];
   System.out.println(name);
}
复制代码

更精细的流程控制

分支和循环结构只是为我们提供了基本的流程控制功能,但是在循环中有时候我们需要提前结束某次或者是整个循环,又或者某段代码在执行到一定的流程的时候需要提前结束,这样基本的流程控制是满足不了的。

虽然Java中放弃了饱受诟病的 goto 语句,但是也为我们提供了一些对程序流程进行更加精细控制的方式。

前面也对 break 语句和 continue 语句做过一些介绍了,下面进行一下详细的说明。

break语句

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

不带标签的break语句

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

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

带标签的break语句

在了解带标签的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语句视图将控制流转移到将相同标号作为其标号的标号语句,该语句称为break目标语句,然后立即结束。

在带标号的break语句中 break目标不必是 switchdowhile 或者 for 语句

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

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

continue语句

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

continue 语句也有代标号和不带标号两种使用方式, 和break语句不同的是, continue语句的这两种使用方式都只能出现在迭代语句中,下文进行详细的介绍

不带标签的continue语句

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

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

带标签的continue语句

前面已经对标号进行了介绍,下面我们直接进入带标签的continue语句的讲解

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

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

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

其他语句

return 语句

return 语句会将控制流返回给方法或构造器的调用者。 return语句必须被包含在可执行成员中( 方法构造器lambda表达式 ,return语句也分为两种,一种是带有返回值的,一种是不带有返回值的。

需要注意的是,return语句触发的流程转移也是 视图转移控制流 ,而不是直接转移控制流,原因也是try的finally子句

对于return语句,我们不需要说明太多。

throw 语句

throw语句会导致异常对象被抛出,这将会视图将控制流程进行转移,有可能会退出多个语句和多个构造器、实例初始化器、静态初始化器和域初始化器的计算以及方法调用,知道找到可以捕获抛出值的try语句。如果没有这种try语句,那么执行throw的线程的执行会在该线程所属的线程组上调用uncaughtException 方法后被终止。

上述也提到了视图转移控制流程,也是因为try的finally子句,如下:

try {
     throw new RuntimeException("故意抛出");
} finally {
     System.out.println("还是会执行");
     throw new RuntimeException("finally中抛出");
}
复制代码

但是上述的用法是基本不会出现的,因为在一个执行体中抛出异常就是为了给调用这个执行体的上层执行体提供一种提示,并期望调用者能够处理这种异常。

关于异常会在后面进行更加详细的说明。

synchronized 语句

这涉及到了多线程编程,以后会进行专门的介绍。

try 语句

try语句会执行一个语句块。**如果抛出了值并且try语句有一个或者多个可以捕获它的catch子句,那么控制流就会转移到第一个这种catch子句上。**这一点在后面讲异常处理的时候也会讲到。

如果try语句有finally子句,那么将会执行另一个代码块,无论try块是正常结束还是猝然结束,并且无论控制流之前是否转移到某个catch块上。

try语句的格式如下所示,有三种形式:

// try-catch
try {
    statement
} catch(Type1 | Type2... e) {
    statement
}

//try-finally
try {
    statement
} finally {
    statement
}

//try-catch-finally
try {
    statement
} catch (Type1 | Type2... e) {
    statement
} finally {
    statement
}
复制代码

在try语句后紧跟的语句块我们称之为try语句的 try块

在finally关键字之后紧跟的语句块我们称之为 finally块

try语句的catch子句通常被称为 异常处理器 catch子句有且仅有一个参数,被称为异常参数,异常参数可以将它的类型表示成单一的类类型,或者表示成两个或者更多类型的联合体(这些类型称为可选项),联合体中的可选项在语法上使用 | 进行分割

需要注意的是,当catch子句的异常参数的类型是一个类型联合体的时候,这个异常参数就一定是final类型的,要么是隐式的,当然也可以显示声明为final的,但是这时候应该讲类型联合体看做是一个类型,final修饰符只需要书写一次就可以了。如下:

try {
    throw new RuntimeException();
} catch (final UnsupportedOperationException | NullPointerException e) { //final修饰符只需在类型联合体前面书写一次就可以了,这时候要把类型联合体看做一个类型
    System.out.println("是可以改变的!");
}
复制代码

这是因为catch类型处理器完全依赖于try块中抛出的异常的类型。而使用了类型联合体之后,异常参数的类型就只能在运行时才能确定,所以必须是final的,在被捕获的时候初始化一次。

同时我们上面也提到过一句话,**如果抛出了值并且try语句有一个或者多个可以捕获它的catch子句,那么控制流就会转移到第一个这种catch子句上。**这就提醒我们在使用catch子句的时候需要注意,要把子类类型放在前面进行捕获,否则就会被父类类型的异常处理器拦截。同时, 在catch子句中使用类型联合体的时候,类型联合体中的各个可选项之间不能具有任何的父子关系。

try语句的执行流程没有什么好说的,我们只需要记住一点, finally子句肯定会被执行

带资源的 try 语句

带资源的try语句是用变量(被称为资源)来参数化的,这些资源在try块执行之前被初始化,并且在try块执行之后,自动地以与初始化相反的顺序被关闭。当资源会被自动关闭的时候,catch子句和finally子句经常不是必须的。

关于资源,我们需要注意以下几点:

  • try中的资源变量肯定是final的,如果不显式声明为final,则会被隐式声明为final,这是因为需要对声明的资源进行自动关闭,所以不允许修改资源变量的指向。
  • 在带资源的try语句中声明的资源必须是 AutoCloseable 的子类型,否则会产生编译错误。
  • 资源是按照从左到右的顺序被初始化的。如果某个资源初始化失败了(即初始化表达式抛出了异常),那么所有已经完成了初始化的资源都将被关闭。如果所有的资源都初始化成功了,那么try块就会正常执行,然后带资源的try语句的所有非 null 资源都将被关闭。
  • 资源将以它们被初始化的顺序相反的顺序被关闭。资源只有在它们被初始化为非 null 值的时候才会被关闭。 在关闭资源时抛出的异常不会阻止其他资源的关闭,其实最终带资源的try语句都会被转化为try-finally语句

不可达语句

如果某条语句因为它是不可达的而不能被执行,那么就是一个编译时错误。我们只需知道这种语句的存在,在不可达表现的非常明显的时候IDE就会给出提示,但是有时候会有错误的逻辑造成的不可达语句,就需要我们自己对代码的结构进行分析了。

总结

以上就是我对Java中的表达式和操作符的理解和介绍,如有不当之处,欢迎进行指正。

原文  https://juejin.im/post/5d4fc129e51d453b7779d508
正文到此结束
Loading...