控制语句之选择语句之switch [详解]

本篇重点:switch的值包含byte、short、int、char、String、枚举这些数据类型的值在底层都是转为整数的,研究转换为整数的过程是?

1、switch 语句是java控制语句下的选择语句

也可以叫分支语句;先简单过一遍switch及其用法

1.1、基本语法形式

switch(表达式) {
    case 值1:
        java语句1;
        break;
    case 值2:
        java语句2;
        break;
    …
    case 值n:
        java语句n;
        break;
    default:
        java语句n+1;
    break;
}

其中,switch、case、break、default 都是 Java 的关键字

1.2、解析各个关键字

1.2.1、switch

        switch翻译为'开关',而开关实际是指switch 关键字后面小括号里的值。小括号内的值一般称呼为"选项值",选项值一般是int或String类型的

        然后case后跟的值一般叫"cas标签"。一般谈及"选项值和case标签的各自支持哪些类型"会有下面两种说法:1、"switch语句支持byte、short、int、char、String、枚举" ; 2、"选项值一般是int或String类型的" ; 3、"case标签可以是byte、short、int、char的表达式"。其实不管是哪种说法,两者适用的数据类型的范围都是一致的

注1:Java7 增强了 switch 语句的功能,允许 switch 语句的控制表达式是 java.lang.String 类型的变量或表达式,就是在字面量可以取整数的基础上可以再取String。只能是 java.lang.String 类型,不能是 StringBuffer 或 StringBuilder 这两种字符串的类型。所以switch支持的数据类型有int、String->就是想说"java7开始允许switch支持String类型"(java7之前说的是"支持int类型")

1.2.2、case

case表示“情况、情形” ,标签可以是

  • 类型为 byte、short、int、char 的常量表达式
  • 枚举常量  (枚举JDK1.5)
  • 在 switch 语句中使用枚举常量时,不必在每个标签中指明枚举名,可以由 switch 的表达式值确定
    
    Size sz = ...;
    switch (sz) {
        case SMALL:      no need to use Size.SMALL
            ...
            break;
        ...
    }
  • 从 Java SE 7 开始 case 标签还可以是字符串字面量
  • 底层都是将byte、short、char、字符串(JDK1.7)、枚举(JDK1.5)转换为int类型,再做比较
                        byte   向上转型成 int
                        short  向上转型成 int
                        char   向上转型成 int
                        字符串 获取到字符串的hash值 int
                        枚举   获取枚举类对象里的数值 int
  • 每个标签中的 statement 部分是一条语句,也可以是{}包裹的一个块
  • 多个标签可以合并,之间用逗号分隔
int tag = 3;
switch (tag){
  case 1:
    System.out.println("111");
    break;
  case 2:
    System.out.println("222");
    break;
  case 3, 4:         这里
    System.out.println("3 or 4");
    break;
  default:
    System.out.println("else");
}

关于case合并也有下面这种说法

        例如多个case分支的值的执行操作是相同的,这种情况下就可以去合并多个case,只写1次java操作语句即可。例如接收用户输入,如果用户输入的是0,1,2则进行输出;如果用户输入的是3,4,5则进行加一然后再输出

Scanner in=new Scanner(System.in);
        int data=in.nextInt();
        switch (data) {
            case 0:
            case 1:
            case 2:System.out.print(data); break;
            case 3:
            case 4:
            case 5:System.out.print(data+1);break;
        }

 输入3,4,5,输出结果分别为4,5,6

 输入1,2,3,输出结果分别为1,2,3

再查得

所以一开始说的"case合并"的例子实际是"case并列"

case击穿

和break有关。当某个case分支连通,执行其后的java语句后,如果没有遇到break语句则会继续执行,但是无需判断后边case分支的值,直接执行java语句。如此,直到遇到break语句后或遇到结束switch的}后,switch的执行才会彻底执行结束->所以也可以说"break语句的存在可以避免击穿现象的发生"

注1: 关于"case标签支持String类型",有说法说是java8开始才支持的。查阅《java核心技术卷一》

书上靠谱,所以case标签支持String类型应当是java7开始的

注2:case后面的值,其规范叫法是“case常量表达式”,其只是起语句标号的作用,并不是在该处进行判断。在执行 switch 语句时,根据 switch 后面表达式的值找到匹配的入口标号,就从此标号开始执行下去,不再进行判断

1.2.3、default

表示“默认”,即其他情况都不满足就执行默认的语句这里

default 后要紧跟冒号,default 块和 case 块的先后顺序可以变动,不会影响程序执行结果。通常,default 块放在末尾,也可以省略不写

 为什么default后面规范是没有break

一般是在最后编写defalut顺序,也是考虑可读性的原因。放在最后也因为有没有break都一样,有就break退出switch,没有就switch的最后一个}来退出switch ( 如果将default 语句用作 switch 中的最后一条语句,则不需要 break 中断)

1.2.4、break

表示“停止”,即跳出当前结构   


如果在 case 分支语句的末尾没有 break 语句,有可能触发/执行多个 case 分支。我们常管这种情况叫做case穿透。下面看《java核心技术卷一》中的说法

 注1:因为break一般给我们的印象都是跳出“循环”,所以不要看到break就以为是循环语句。这里介绍了break还有一个用法,就是跳出switch

下面看switch的使用

1.3、switch 语句的执行过程如下

        表达式的值与每个 case 语句中的常量作比较。如果发现了一个与之相匹配的则执行该 case 语句后的代码。如果没有一个 case 常量与表达式的值相匹配则执行 default 语句。当然,default 语句是可选的。如果没有相匹配的 case 语句,也没有 default 语句,则什么也不执行

代码演示

switch不难,主要这个获取时间我没见过

public static void main(String[] args) {
    String weekDate = "";

    Calendar calendar = Calendar.getInstance();   获取当前时间
    int week = calendar.get(Calendar.DAY_OF_WEEK) - 1;   获取星期的第几日

    switch (week) {
        case 0:
            weekDate = "星期日";
            break;
        case 1:
            weekDate = "星期一";
            break;
        case 2:
            weekDate = "星期二";
            break;
        case 3:
            weekDate = "星期三";
            break;
        case 4:
            weekDate = "星期四";
            break;
        case 5:
            weekDate = "星期五";
            break;
        case 6:
            weekDate = "星期六";
            break;
    }
    System.out.println("今天是 " + weekDate);
}

2、转换为int        (本篇重点)

"byte、short、int、char、String、枚举"在底层都可以转为int,怎么转?下面研究

2.1、我们已知的

        byte、short、char类型的字面量如果字面量的值不超出对应byte、short、char的范围则可以直接赋值给int类型的变量;如果是byte、short、char类型的变量也可以直接赋值给int类型的变量->那么接下来主要是研究String、枚举怎么转为int

2.2、指令指示器

        程序最终都是一条条的指令。CPU有一个指令指示器,指向下一条要执行的指令,CPU根据指示器的指示加载指令并且执行。指令大部分是具体的操作和运算,在执行这些操作时,执行完一个操作后指令指示器会自动指向挨着的下一条指令。但有一些特殊的指令,称为跳转指令,这些指令会修改指令指示器的值,让CPU跳到一个指定的地方执行。跳转有两种:一种是条件跳转;另一种是无条件跳转。条件跳转检查某个条件,满足则进行跳转,无条件跳转则是直接进行跳转->也就是1、程序皆是指令;2、cpu中有一个指令指示器。cpu只是处理指令的,指令的处理顺序要看指令指示器的安排。一般情况是:指令指示器让cpu按序执行指令;而遇到有一些特殊的指令叫跳转指令,跳转指令可以修改指令指示器的值,继而让cpu去跳到一个指定的地方执行。这种跳转有两种:一种是条件跳转;另一种是无条件跳转。什么条件目前不需要掌握,不重要

2.3、switch 语句底层和指令指示器有啥关系呢

因为switch 语句会被编译成跳转指令

在分支较少的情况下可能会被转换成跳转指令,但是如果分支比较多就会进行多次比较运算,效率自然就低,因此会使用另外一种方式叫跳转表来提高效率

跳转表是一个映射表,存储了可能的值和要跳转的地址,里面的值都是整数,而且按大小排了序,因此可以使用高效的二分查找。并且如果case值如果是连续的数字,跳转表还会被优化成数组,这样连比较都不用了,直接根据值找到要跳转的地址。而且就算不是连续的值,但是数字比较密集,差的不多,编译器也会将跳转表优化成数组

所以case 取整型(byte、short、int)优势很大,那为什么 long 类型不行呢?

因为跳转表的存储空间一般是 32位的,long 类型太长了。char 本质上其实还是整型,枚举也有其对应的整数,string 的话可以通过 hashcode 转换成整数,不过 hashcode 有可能是相同的,因此在跳转之后会根据 string 内容再次进行判断->知道了底层是int类型的效率高,转int的意义在这里

2.4、下面看String是怎么转为整数的

public class Test {
    public static void main(String[] args) {
        String str = "test";
        switch (str) {
        case "a":
            System.out.println("a");
            break;
        case "b":
            System.out.println("b");
            break;
        case "c":
            System.out.println("c");
            break;
        default:
            System.out.println("c");
            break;
        }
    }
}

看反编译之后的代码

public class Test {
    public Test() {
    }
    public static void main(String[] args) {
        String str = "test";
        byte var3 = -1;
        switch(str.hashCode()) {
        case 97:
            if(str.equals("a")) {
                var3 = 0;
            }
            break;
        case 98:
            if(str.equals("b")) {
                var3 = 1;
            }
            break;
        case 99:
            if(str.equals("c")) {
                var3 = 2;
            }
        }
        switch(var3) {
        case 0:
            System.out.println("a");
            break;
        case 1:
            System.out.println("b");
            break;
        case 2:
            System.out.println("c");
            break;
        default:
            System.out.println("c");
        }
    }
}

可以发现

1.传入switch的字符串值经过hashCode()转换为了哈希值,case的标签值转为了int类型;但java语句部分还是字符串的比较。按照上面的比较那只能是执行default了

2、不管是成功执行了哪一个case或执行的是default,有一个给var3赋值的语句。这又指向一个新的switch,所以输出是靠的这第二个switch

综上,比较的时候先是通过hashcode()来比较,如果hashcode一样就再通过equals方法来比较。所以本质上还是没有脱离int比较的原则

所以switch对String的支持,实际上是通过编译器做了一次优化

那么如果两个case的String的hashcode冲突了会怎么样呢?

public class Test {
    public static void main(String[] args) throws Exception {
        String str = "test";
        switch (str) {
        case "AaAa":
            System.out.println("a");
            break;
        case "BBBB":
            System.out.println("b");
            break;
        case "AaBB":
            System.out.println("c");
            break;
        default:
            System.out.println("c");
            break;
        }
    }
}

可见case "AaBB"和default都是输出"c" 

再看反编译之后

public class Test {
    public Test() {
    }
    public static void main(String[] args) throws Exception {
        String str = "test";
        byte var3 = -1;
        switch(str.hashCode()) {
        case 2031744:
            if(str.equals("AaBB")) {
                var3 = 2;
            } else if(str.equals("BBBB")) {
                var3 = 1;
            } else if(str.equals("AaAa")) {
                var3 = 0;
            }
        default:
            switch(var3) {
            case 0:
                System.out.println("a");
                break;
            case 1:
                System.out.println("b");
                break;
            case 2:
                System.out.println("c");
                break;
            default:
                System.out.println("c");
            }
        }
    }
}

进入default里面还是判断一次其他case的值,匹配则优先执行匹配的case的java语句

为什么要分成两步的switch来做呢?

其实很简单,方便给编译器定一个规则。设想一下,如果不是两步switch,那么会是如下的代码

public class Test {
    public Test() {
    }
    public static void main(String[] args) throws Exception {
        String str = "test";
        byte var3 = -1;
        switch(str.hashCode()) {
        case 2031744:
            if(str.equals("AaBB")) {
                System.out.println("a");
            } else if(str.equals("BBBB")) {
                System.out.println("b");
            } else if(str.equals("AaAa")) {
                System.out.println("c");
            }
        default:
            System.out.println("c");
        }
    }
}

 如果我们的case "AaAa":是没有break条件的,那么编译器又要做额外的优化才能达到这个效果。这样对编译器的编写会十分复杂,不如如就分为两步:第一步的switch先计算出要走哪个case,然后再在第二个switch去执行具体的case

补充:"switch底层是==就够用了"主要是因为String不明确,"String是引用类型,比较不应该用equals()吗?",现在可知String是通过hashCode()转为int值进行比较的,所以使用==够用了

2.5、枚举类型怎么转int类型

目前查到是这样的,再说吧

public enum QQState{
        OnLine=1,
        OffLine,
        Leave,
        Busy,
        QMe
    }

 

枚举转int

QQState state = QQState.OnLine;
            枚举类型默认可以跟int类型互相转换  枚举类型跟int类型是兼容的
            int n = (int)state;

3、拓展

3.1、嵌套switch

嵌套switch就是switch里面套一层switch,或者说外层是我们熟悉的switch,再内层switch是代替某一个case的java语句的存在。如上,外层switch的case 1:原本跟的java语句部分改为了一个内层的switch

问:如上外层switch的case 1:的"java语句(指的是嵌套的内层switch)"结束后,有必要跟break吗?

具体点,值1传入嵌套switch,外层的switch的case 1:符合所以执行"java语句(即内存switch)",再内层switch也有case  1:符合,执行其java语句后不是有break吗?不能结束嵌套switch吗?

有这么一种情况,就是进入了内层switch但内层switch没有一个匹配,所以外层的第一个case块的break是有必要的;再有就是这么理解:内层switch的break就是跳出内层switch

目前是上面这么解释,不过还是记着内层switch是代替java语句的存在吧,break不算在java语句内

3.2、增强switch(enhanced switch)

增强switch是在 Java 12 中以预览功能的形式引入,在 Java 13 中再次预览,在 Java 14 中成为正式功能

增强switch是为了解决这两点原因:1、break容易忽略;2、增加了可以在不同的分支中对同一个变量进行赋值

switch 有自己的值,因为可以作为表达式来使用。这就简化了赋值操作 

switch 表达式的值由分支来确定。下面的代码再次展示增强switch 表达式的基本用法

public class SwitchExpression {
 
  public String formatGifts(int number) {
    return switch (number) {
      case 0 -> "no gifts";
      case 1 -> "only one gift";
      default -> number + " gifts";
    };
  }
}

"不同的分支有不同的返回值" 

可以看到,return后面跟的是switch块。switch 表达式(case部分)使用了箭头之后,代码执行不会转到下一个分支(不会case穿透),相当于添加了 break->"break容易忽略"的问题解决了,冒号改用箭头解决的

再增强switch不仅仅是返回值,同样可以{}

大多数情况下,箭头标签后使用单个表达式就可以满足需求。如果有复杂的逻辑,可以使用代码块。这个时候就需要用 yield 来提供值

public class YieldValue {
 
  public String formatGifts(int number) {
    return switch (number) {
      case 0 -> "no gifts";
      case 1 -> "only one gift";
      default -> {
        if (number < 0) {
          yield "no gifts";
        } else {
          yield number + " gifts";
        }
      }
    };
  }
}

仅仅返回一个值就是箭头跟值,如果是java语句则还需要借助yield来返回值->也就是增强switch不是说增强了,增强的地方在于"java语句改用返回值",再就不能使用java语句了 

在使用传统标签的 switch 语句中也可以使用 yield

public class YieldValue2 {
 
  public String formatGifts(int number) {
    return switch (number) {
      case 0:
        yield "no gifts";
      case 1:
        yield "only one gift";
      default: {
        if (number < 0) {
          yield "no gifts";
        } else {
          yield number + " gifts";
        }
      }
    };
  }
}

应该是说yield很早就存在了,可以代替break,只是增强switch中使用最好,可以代替break、可以用于返回值 

yield 用来返回值并跳出当前 switch 语句块,所以也可以下面这么写

private static void test(Integer value) {
    int number = switch (value) {
        case 3:
            System.out.println("3");
            yield 3;
        case 5:
            System.out.println("5");
            yield 5;
        default:
            System.out.println("default");
            yield 0;
    };
    System.out.println(number);
}

结合箭头表达式同时使用就是

private static void test(Status status) {
    var result = switch (status) {
        case OPEN -> 1;
        case PROCESS, PENDING -> 2;
        case CLOSE -> {
            System.out.println("closed");
            yield 3;
        }
        default -> throw new RuntimeException("状态不正确");
    };
    System.out.println("result is " + result);
}

3.3、区别

主要是研究switch语句if语句的区别

    switch条件语句,switch条件语句是一个很常用的选择语句。和if 条件语句不同,它只能针对某个表达式的值做出判断从而决定程序执行哪一个代码

对于switch来讲一定要记住,它无法像if语句那样使用逻辑表达式进行判断,仅仅支持数值的操作

第三点后面对其的解释说明了if..else效率没switch高的原因

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值