【问题标题】:Why can't we switch on classes in Java 7+?为什么我们不能在 Java 7+ 中打开类?
【发布时间】:2015-07-27 22:00:23
【问题描述】:

在我看来,这样的 switch 语句会很有意义,但它会产生编译错误:

public void m(Class c) {
   switch (c) {
       case SubClassOfC1.class : //do stuff; break;
       case SubClassOfC2.class : //do stuff; break;
   }
} 

但是,不支持打开类。是什么原因?

我不是在尝试解决 instanceof,它实际上是在类级别上,我需要执行一些操作,那里没有实例。

编译错误在 SubClassOfC1 & SubClassOfC2 附近:constant expression required.

【问题讨论】:

  • 谢谢@Jashaszun,我对 Github MD 感到困惑。
  • 这是不允许的,但是您更简单的解决方法是使用c.getName(),以防万一,使用类名
  • 不是完全重复,但请参阅stackoverflow.com/questions/338206/…
  • 一个问题是类不是编译时常量。您也不能使用非常量字符串。在动态值上进行分支需要不同的机制。
  • 但是...但是...多态性。

标签: java class switch-statement java-8 java-7


【解决方案1】:

因为我们只能开启Constant Expressions (§15.28)Enum constants (§8.9.1)

From the JLS:

Expression 的类型必须是 char、byte、short、int、Character、Byte、Short、Integer、String 或枚举类型 (§8.9),否则会出现编译时错误。

要想象为什么会这样,请考虑一下 Java 编译器尝试编译 switch 语句时发生的优化。

  • 它要绝对、积极地保证平等
  • 它希望能够最大化所有案例的性能(分支预测)
  • 它希望有一种有效、一致的方式将常量表达式转换为整数查找表(这就是为什么不支持 longfloatdouble,但支持 String)。李>

请注意,switch 语句中支持的Stringadded only in Java 7。这是因为编译器使用了switch Stringswitch int 的幕后转换,即detailed in this article。快速总结:

这段代码:

public class StringInSwitchCase {
    public static void main(String[] args) {
        String mode = args[0];
        switch (mode) {
        case "ACTIVE":
            System.out.println("Application is running on Active mode");
            break;
        case "PASSIVE":
            System.out.println("Application is running on Passive mode");
            break;
        case "SAFE":
            System.out.println("Application is running on Safe mode");
        }
    }
}

变成这样的代码:

public class StringInSwitchCase {
    public StringInSwitchCase() {}

    public static void main(string args[]) {
        String mode = args[0];
        String s;
        switch ((s = mode).hashCode()) {
        default:
            break;
        case -74056953:
            if (s.equals("PASSIVE")) {
                System.out.println("Application is running on Passive mode");
            }
            break;
        case 2537357:
            if (s.equals("SAFE")) {
                System.out.println("Application is running on Safe mode");
            }
            break;
        case 1925346054:
            if (s.equals("ACTIVE")) {
                System.out.println("Application is running on Active mode");
            }
            break;
        }
    }
}

我们不能以同样的方式可靠地将Class 对象转换为整数。类不覆盖 hashCode,它使用 System.identityHashCode

还请注意,如果使用不同的ClassLoader 加载相同的类,它并不总是相同的Class

【讨论】:

  • 这很有趣。如果A.class 不是常量表达式,那是什么?
  • 更多关于类的开关为什么不使用 System.identityHashCode 而不是 hashcode ?没有理由不这样做。
  • @Snicolas:身份哈希码是一个通常从对象的内存位置(指针值)派生的值,或者简单地说,在每个运行时都不同。因此编译器不能创建依赖于对象的身份哈希码的代码。当然,每个类都有一个名字,切换名字的哈希码也不是那么难。
【解决方案2】:

有趣的是,到目前为止,所有答案基本上都在说“因为规范是这样说的”,这是正确的,但并不真正令人满意。在 Java 7 之前,Strings 是不允许使用的,这通常被视为刻在石头上。

但技术障碍不应推动语言设计。如果没有办法将其编译为高效的代码,它可能仍会被编译为等效于if … else … 子句,并且仍然在源代码简洁方面获胜。对于String 值,有一种有效的方法。只需切换不变的哈希码并对匹配候选者执行equals 检查。事实上,规范并没有强制使用哈希码,它可以是任何不变的int 属性,例如长度或第一个字符,但每个String的值应该不同。

同样,switch 上的 Class 对象也是可能的。它的哈希码保证相同,但每个类都有一个常量名和一个常量哈希码。例如。以下作品:

public class ClassSwitch {
    static final class Foo {}
    static final class Bar {}
    static final class Baz {}

    public static void main(String... arg) {
        Class<?> cl=Bar.class;
        switch(cl.getSimpleName().hashCode()) {
            case 70822: 
                if(cl==Foo.class) {
                    System.out.println("case Foo:");
                }
                break;
            case 66547: 
                if(cl==Bar.class) {
                    System.out.println("case Baz:");
                }
                break;
            case 66555: 
                if(cl==Baz.class) {
                    System.out.println("case Baz:");
                }
                break;
        }
    }
}

我使用简单名称而不是限定名称,因此此示例代码与包无关。但我认为,情况很清楚。可以为任何类型的对象实现有效的switch 语句,这些对象具有可以在编译时预测的常量int 属性。也就是说,也没有理由不支持long 开关。有很多方法可以从 long 计算出合适的 int...


所以还有另一个重要的决定要做。

这个功能真的有好处吗?看起来像是代码异味——即使添加 String 支持也是有争议的。它不会增加新的可能性,因为您可以对少量课程使用if-else 或对较大数量使用HashMap&lt;Class,SomeHandlerType&gt; 进行相同的操作。而且它看起来并不像经常需要的东西,值得扩展语言规范,即使它只是一个必须添加的句子。

这些是推动语言设计的考虑因素,但并不是思想和平衡不能改变。所以我并不是说未来的版本不可能获得这个功能。

但是好吧,看着at the quality of generated String switch code,我宁愿手动编码我的switches……

【讨论】:

  • 这很有趣,但如果您正在混合类加载器,则可能无法按预期工作,即来自不同类加载器的类可能不同但名称相同。
  • @Kiril:名称仅用于switch,之后与类文字进行参考比较。类文字,就像任何其他非反射使用类一样,总是在它们出现的类的上下文中解析,并且总是解析为同一个类。并且一个类中所有出现的相同类名总是被解析为同一个类。在这个级别,没有同名课程的空间。这些只能通过手动处理ClassLoaders 和反射来处理。
  • 我正在使用相同的解决方法,但它令人失望,因为它可能更有效。不过,我仍在朝这个方向寻找:stackoverflow.com/q/36969550/693752
【解决方案3】:

基于documentation on the switch statement

开关适用于 byte、short、char 和 int 原始数据类型。它还适用于枚举类型(在 Enum Types 中讨论)、String 类和一些包装某些原始类型的特殊类:Character、Byte、Short 和 Integer。

所以本质上,它只适用于那些类型,而没有别的。 Class 不是其中之一。

限制的原因是,正如您自己在评论中所说,切换表由int 索引。以上所有类型都可以轻松转换为int(包括String,通过散列),而Class则不是。

【讨论】:

  • 嘿等等。那就是说long无法开启?
  • @MCEmperor 这意味着一个 long 不能被打开。这可能是有意义的,因为切换表存储在数组(大小为 int)中。
  • @MCEmperor 试试看,你会看到:P。不,您不能使用 long 进行切换。
  • @Snicolas - 类文字可能在运行时显示为不同的对象;您的方法m(c) 的每次调用都可能在不同的类加载器中。
  • @Snicolas 转换为数字引用必须在编译时进行。字符串可能的唯一原因是因为 java.lang.String 的 API 指定了需要为所有 JVM 实现计算哈希码的机制。
【解决方案4】:

在 Java 8 中,您可以使用 lambdas 创建自己的“switch-case”。这是一个关于如何打开对象类的简单示例(不是您想要的 Class 对象本身,但这似乎更有用):

import java.util.function.Consumer;

public class SwitchClass<T> {
    private static final SwitchClass<?> EMPTY = new SwitchClass<Object>(null) {
        @Override
        public <S> SwitchClass<Object> when(Class<S> subClass,
                Consumer<? super S> consumer) { return this; }

        @Override
        public void orElse(Consumer<? super Object> consumer) { }
    };

    final T obj;

    private SwitchClass(T obj) {
        this.obj = obj;
    }

    @SuppressWarnings("unchecked")
    public <S> SwitchClass<T> when(Class<S> subClass,
            Consumer<? super S> consumer) {
        if (subClass.isInstance(obj)) {
            consumer.accept((S) obj);
            return (SwitchClass<T>) EMPTY;
        }
        return this;
    }

    public void orElse(Consumer<? super T> consumer) {
        consumer.accept(obj);
    }

    public static <T> SwitchClass<T> of(T t) {
        return new SwitchClass<>(t);
    }
}

使用示例:

SwitchClass.of(obj)
    .when(Integer.class, i -> System.out.println("Integer: "+i.intValue()))
    .when(Double.class, d -> System.out.println("Double: "+d.doubleValue()))
    .when(Number.class, n -> System.out.println("Some number: "+n))
    .when(String.class, str -> System.out.println("String of length "+str.length()))
    .orElse(o -> System.out.println("Unknown object: "+o));

如果只执行第一个匹配的分支,那么对于Double 对象,只会执行Double 分支,而不是Number 分支。巧妙的是,类型转换是自动执行的。

【讨论】:

  • 当然,switch 语句的承诺是提供有效的查找,最好是通过计算的跳转指令来找到正确的分支。另一方面,您的方法必须按顺序评估条件,如果 EA 和内联失败,甚至可能分配对象。
  • @the8472,我同意这个版本会慢一些(可能慢几倍)。实际上,我不建议任何人使用这种方法。尽管如此,我认为 OP 关心的是代码的清晰和美观,而不是性能开销。
  • 很好,但是 instanceof 太慢了。
  • @Snicolas,您实际测量过性能还是只是认为它很慢?
  • 对不起,你说得对,我更习惯于在 Android 上的反射表现。也许在 JVM 上它并没有那么糟糕。对不起
【解决方案5】:

好吧,根据documentationswitch 只支持有限的一组数据类型:

开关适用于 byte、short、char 和 int 原始数据类型。它还适用于枚举类型(在 Enum Types 中讨论)、String 类和一些包装某些原始类型的特殊类:Character、Byte、Short 和 Integer

我猜为什么会这样:因为编译器很难将generate efficient code 转换为switch,而不是相对简单的数据类型。

【讨论】:

  • 我同意如果你想考虑继承会令人困惑,但这不是 switch 的语法所建议的。只引用相等,没有别的。
  • 为什么只检查身份相等性,处理子类会成为问题?如果 c 是Object,做一些事情,如果是String,做一些别的事情。
  • 你说的都对 - 试图猜测为什么语言功能会以这种方式工作为时已晚。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-08
  • 2021-09-01
  • 2011-01-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多