为什么下面的Java程序输出结果奇怪?

4

我在《Java Puzzlers》中发现了以下难题:

public class DosEquis {
    public static void main(String[] args) {
        char x = 'X';
        int i = 0;
        System.out.print(true  ? x : 0);
        System.out.print(false ? i : x); 
    }
}

我尝试了这段代码并成功运行,但输出结果与我的猜测不符。我猜测的输出应该是:XX,但实际输出是:X88
我尝试了很多次去理解,但我无法理解。有人能给我们解释一下吗?为什么会出现不同的输出结果?我能理解第一个print()打印变量char x的字符值,但第二个print()打印的是char x中值的ASCII表示中的88。如果我像这样简化第二个print()中的三元运算表达式。
if(false){
    System.out.print(i);
}else{
    System.out.print(x);
}

然后输出结果是XX,非常奇怪,有人能解决这个问题吗?

对我来说,理解三元运算符将会是很大的帮助。

提前致谢!


请注意:在每种情况下(包括参数类型),将调用哪些精确的重载System.out.print方法? - Tassos Bassoukos
@TassosBassoukos,对于第一个 System.out.print,调用了一个带有 char 的方法,而对于第二个情况,则是调用了一个带有 int 的方法。这就是奇怪的地方,不理解为什么第二个 print 调用的是 int 数据类型而不是 char 数据类型? - Jameer Mulani
a ? b : c 表达式的类型由什么决定? - Tassos Bassoukos
如果你想回到语言规范,请点击此链接:http://docs.oracle.com/javase/specs/jls/se5.0/html/expressions.html#15.25 -- 阅读起来并不容易。 - laune
顺便提一句,将“88”称为char x的ASCII表示需要经过一长串的推理。Java使用Unicode。char保存UTF-16代码单元。在UTF-16中,“X”恰好被编码为一个代码单元。因此,'X'适合于一个char (十进制中为88)。没有继续讨论ASCII的必要了。 - Tom Blodget
8个回答

6
这种行为的原因是三元运算符只有一个结果类型,编译器必须提前选择并响应调用相应的print。在true ? x : 0这种情况下,0被视为char值,并调用print(char)。在第二种情况下,由于i是一个intx也会被隐式转换为int(扩展转换),然后调用print(int),输出数字。相反的,将i强制转换为char是不合法的,因为它可能会丢失精度。
通过这个例子可以展示静态解析类型的影响 - 不是通过print,因为有print(Object),但考虑这个例子:
void method(boolean b);
void method(Integer i);
...
method(cond? false:0);

无论cond是什么,都有一个与参数兼容的重载。然而,在编译时,编译器需要选择一个重载,这是不可能的。编译器会自动装箱并将表达式分配为Object*,但没有method(Object)方法。
*实际上,我的编译器说:“Test类型中的method(boolean)方法对于参数(Object&Serializable&Comparable<?>)不适用”,但要点仍然存在。

为什么 System.out.print(false ? 1: x); 的输出是 'X'? char x = 'X'; int i = 0; System.out.print(true ? x : 0); System.out.print(false ? i: x); System.out.print(false ? 1: x); X88X - iamct
因为1既可以是char类型的有效值,也可以是int类型的有效值,所以编译器使用了x的类型,即char - Silly Freak
我尝试了(String和char),编译器接受了它。我认为char变成了String。或者它们变成了对象(char自动装箱为Character),因为println接受对象。 - ADTC
你说得对,是我的错!自动装箱和print(Object)使这个程序正常工作。我会在我的答案中反映这一点。 - Silly Freak

3
无论你看到的结果是什么,都是根据JLS 15.25中规定的条件运算类型确定的。以下是它提到的要点:
如果第二个和第三个操作数的类型可以转换为数字类型(§5.1.8),则有以下几种情况:
  • 如果其中一个操作数是byte或Byte类型,另一个操作数是short或Short类型,则条件表达式的类型为short。

  • 如果其中一个操作数是T类型,其中T是byte、short或char类型,另一个操作数是int类型的常量表达式(§15.28),其值可表示为T类型,则条件表达式的类型为T。

  • 如果其中一个操作数是T类型,其中T是Byte、Short或Character类型,另一个操作数是int类型的常量表达式(§15.28),其值可表示为对T应用拆箱转换后的类型U,则条件表达式的类型为U。

  • 否则,将应用二进制数值提升(§5.6.2)到操作数类型上,并且条件表达式的类型是第二个和第三个操作数提升后的类型。

在你的问题中,最后一点是跟随的。 xchar 类型,而 iint 类型,因此 x 被提升为 int。因此第二行的输出是 88,这是字符 Xint 转换。

2
为了理解这里发生的事情,我们需要追踪类型。让我们逐个考虑每个打印语句,首先是:
System.out.print(true ? x : 0);

这里的类型是布尔型?字符:常量整数。
编译器希望三元语句返回一个单一类型,它不能返回字符或整数。它发现0可以被转换为字符常量,因此将其视为表达式“boolean? char:char”。最后一个字符的值为零。因为布尔值为真,所以第一个字符被打印为X。
System.out.print(false ? i : x);

这里的类型是布尔型?整型:字符型,整型是一个整数变量,不能被视为字符型,因此编译器的选择是要么报错(因为类型不同),要么缩小整型或扩大字符型。缩小整型会丢失精度,因此编译器不会在背后自动进行转换(需要显式转换)。扩大类型不会丢失精度,因此字符型被转换为整型。'X'的整数值为88。因此打印出的值为88。
有关此转换过程的更多详细信息,请阅读Java语言规范

1

在这一行中:

System.out.print(false ? i : x);

将X转换为int类型,这样就会打印出char X的十进制值。

1
你的观察是正确的。但三元运算符处理相同的数据类型,并且它考虑真值的数据类型,如果需要,则转换假值的数据类型。在你的情况下,在第一个打印语句中,三元运算符将考虑x的数据类型为Char,在第二个打印语句中,三元运算符将考虑i的数据类型为int,这就是你在char x中获得ASCII表示形式的原因。
    System.out.print(true  ? x : 0);
 /*ternary operator(compiler) consider the resulting data 
   type as char as the true Value type is char.*/
    System.out.print(false ? i : x); 
/*ternary operator(compiler) consider the resulting data 
   type as int as the true Value type is int.*/

/* but if you replace the print statement with if else condition 
 then there is no point of ternary concept at all*/

1
在一个三元语句中,格式如下:(boolean ?if type:else type),只有if typeelse type对于最终类型的确定起作用。编译器处理情况如下:
  1. 如果if typeelse type相同,则最终类型也相同。
  2. 如果if type可以转换为else type而不会失去精度,则最终类型与else type相同。
  3. 如果else type可以转换为if type而不会失去精度,则最终类型与if type相同。
  4. 如果2和3都可能,则最终类型与较低数据范围的类型相同。
  5. 如果上述情况都不符合,则编译器会抛出编译错误*。
让我们看看这些不同版本:
System.out.println(true  ? x : 0); //X

因为if typechar,而else type是可以表示0的任何类型,所以这变成了X。常量0char数据范围内是有效的(0到65,535)。可以将x0视为int,但是int数据范围更高(-231到231-1)。

根据第4点,编译器选择较低范围的类型(char)。

System.out.println(true  ? x : i); //88

在这里,“if type”是一个字符,而“else type”是一个整数。当您将变量声明为非最终的“int”时,编译器无法确定该值是否永远不会从“0”更改(即使您的代码没有在任何地方更改它!)。
因此,只有“point 2”适用于此处。 “if type”字符可以转换为“else type”整数而不会失去精度(因为“int”的范围更高),但如果将“else type”整数转换为“if type”字符,则“char”的较低范围可能会导致精度损失(“i”的值可能超出“char”的范围)。
因此,编译器选择“int”以避免精度损失,根据“point 2”。
一些其他测试案例(原因与上述相似):
System.out.println(false ? i : x); //88 (as per point 3)
System.out.println(false ? 0 : x); //X  (as per point 4)

然而,在这个代码中,结果是不同的:
final int i = 0;
System.out.println(true  ? x : i); //X
System.out.println(false ? i : x); //X

Can you tell why? (你能说出原因吗?)
* 注意:所有类型最终都可以转换为Object,包括可以自动装箱的基本类型(在最近的Java版本上)。如果重载方法以接受Object类型,则您可能永远不会遇到此情况,因为编译器将最终类型转换为Object。

1
当你使用三元运算符条件?valTrue:valFalse时,它将在两种情况下(真或假)返回相同的数据类型。在行System.out.print(condition? i : x);中,如果为真则期望int,如果为假则期望Char,但它将在两种情况下仅返回int。同样,在行System.out.print(condition? x : i);中,在两种情况下都将返回Char。

1
在第一个表达式中,一侧是字符,而0可以被分配给一个char;因此第一个三元表达式是一个字符。char zero = 0; 在第二个表达式中,有int和char,char被扩展:(int)'X'是88。
c ? "" : null这样的常量因此不会生成对象,而是生成字符串(在此处)。

严格来说,'X'(不带转换)也是88。关键是调用适当的print()重载,根据它所接收的类型以不同的方式显示88。 - Silly Freak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接