什么是“可能的有损转换”,我该怎么修复它?

27

新的Java编程人员经常因编译错误信息感到困惑,例如:

"incompatible types: possible lossy conversion from double to int"

对于这行代码:

int squareRoot = Math.sqrt(i);

一般来说,“可能有损转换”错误消息是什么意思,如何修复它?

2个回答

43
首先,这是一个编译错误。如果你在运行时的异常消息中看到它,那是因为你运行了一个有编译错误的程序。
消息的一般形式如下:
"incompatible types: possible lossy conversion from to "
其中和都是原始数值类型;即byte、char、short、int、long、float或double之一。
当你的代码尝试进行从到的隐式转换,但转换可能会有损失时,就会发生这个错误。
在问题的示例中:
  int squareRoot = Math.sqrt(i);

sqrt方法返回一个double类型的值,但是从double类型转换为int类型可能会有损失。

"可能有损失"是什么意思?

让我们来看几个例子。

将一个`long`转换为`int`是一种潜在的有损转换,因为存在一些`long`值没有对应的`int`值。例如,任何大于2^31 - 1的`long`值都太大无法表示为`int`。同样,任何小于-2^31的数值也太小。
将一个`int`转换为`long`不是有损转换,因为每个`int`值都有对应的`long`值。
将一个`float`转换为`long`是一种潜在的有损转换,因为存在一些`float`值超出了可以表示为`long`值的范围。这些数值会(有损地)转换为`Long.MAX_VALUE`或`Long.MIN_VALUE`,NaN和Inf值也是如此。
将一个`long`转换为`float`不是有损转换,因为每个`long`值都有对应的`float`值。(转换后的值可能不够精确,但在这个上下文中,“有损性”并不意味着...)
这些都是可能有损失的转换:
- 从 `short` 到 `byte` 或 `char` - 从 `char` 到 `byte` 或 `short` - 从 `int` 到 `byte`、`short` 或 `char` - 从 `long` 到 `byte`、`short`、`char` 或 `int` - 从 `float` 到 `byte`、`short`、`char`、`int` 或 `long` - 从 `double` 到 `byte`、`short`、`char`、`int`、`long` 或 `float`
如何修复错误?
为了消除编译错误,需要添加类型转换。例如:
  int i = 47;
  int squareRoot = Math.sqrt(i);         // compilation error!

变成

  int i = 47;
  int squareRoot = (int) Math.sqrt(i);   // no compilation error

但这真的是一个解决办法吗?考虑一下,根号47的值是6.8556546004,但squareRoot将得到6的值。(转换将截断,而不是四舍五入。)
那么这个又怎么办呢?
  byte b = (int) 512;

这导致b的值为0。将较大的int类型转换为较小的int类型是通过屏蔽高位比特并将512的低8位全部置零来完成的。
简而言之,你不应该简单地添加一个类型转换,因为它可能无法正确地执行你的应用程序。
相反,你需要理解为什么你的代码需要进行转换:
- 这是因为你在代码中犯了其他错误吗? - <type1>应该是不同的类型,这样就不需要进行损失转换吗? - 如果需要进行转换,类型转换所执行的“静默”损失转换是否是正确的行为? - 或者你的代码应该进行四舍五入而不是截断,或者通过抛出异常来处理不正确/意外的值?

在下标操作时出现“可能的损失转换”。

第一个例子:
for (double d = 0; d < 10.0; d += 1.0) {
    System.out.println(array[d]);  // <<-- possible lossy conversion
}

问题在于数组索引值必须是整数。所以d必须从double转换为int。一般来说,使用浮点数作为索引是没有意义的。要么有人误以为Java数组的工作方式类似于Python字典,要么他们忽视了浮点数运算通常是不精确的这一事实。
解决方案是重写代码,避免使用浮点数作为数组索引。(添加类型转换可能是不正确的解决方案。)
第二个例子:
for (long l = 0; l < 10; l++) {
    System.out.println(array[l]);  // <<-- possible lossy conversion
}

这是前一个问题的一个变种,解决方案是相同的。不同之处在于根本原因是Java数组的索引限制为32位。如果你想要一个具有超过2^31 - 1个元素的“类似数组”的数据结构,你需要定义或找到一个类来实现它。
方法或构造函数调用中的“可能丢失精度的转换”
考虑以下情况:
public class User {
    String name;
    short age;
    int height;

    public User(String name, short age, int height) {
        this.name = name;
        this.age = age;
        this.height = height;
    }

    public static void main(String[] args) {
        User user1 = new User("Dan", 20, 190);
    }
}

使用Java 11编译上述代码会得到以下结果:
$ javac -Xdiags:verbose User.java 
User.java:20: error: constructor User in class User cannot be applied to given types;
    User user1 = new User("Dan", 20, 190);
                 ^
  required: String,short,int
  found: String,int,int
  reason: argument mismatch; possible lossy conversion from int to short
1 error

问题在于字面值20是一个int类型,而构造函数中对应的参数声明为short类型。将int转换为short会有信息丢失。

返回语句中可能存在信息丢失的转换。

示例:

public int compute() {
    long result = 42L;
    return result;  // <<-- possible lossy conversion
}

一个带有值/表达式的return可以被看作是对返回值的“赋值”。但无论你如何思考,都需要将提供的值转换为方法的实际返回类型。可能的解决方案是添加一个类型转换(表示“我承认有损失”)或者改变方法的返回类型。

由于表达式中的提升而导致的“可能有损失的转换”

考虑以下情况:

byte b1 = 0x01;
byte mask = 0x0f;
byte result = b1 & mask;  // <<-- possible lossy conversion

这将告诉你有一个"从int到byte的可能丢失转换"。这实际上是第一个例子的变体。可能令人困惑的是理解int是从哪里来的。
答案是它来自于&运算符。事实上,所有整数类型的算术和位运算符都会产生一个int或long,取决于操作数。所以在上面的例子中,b1 & mask实际上产生一个int,但我们试图将其赋值给一个byte。
为了修复这个例子,我们必须在赋值之前将表达式结果强制转换回byte类型。
byte result = (byte) (b1 & mask); 

当将字面值赋给变量时出现"可能的损失转换"。
考虑以下情况:
int a = 21;
byte b1 = a;   // <<-- possible lossy conversion
byte b2 = 21;  // OK

发生了什么?为什么一个版本被允许,而另一个版本却不被允许?(毕竟它们都“做”同样的事情!)
首先,JLS规定21是一个类型为int的数值字面量。(没有byteshort字面量。)因此,在这两种情况下,我们都将一个int赋值给一个byte
在第一种情况下,错误的原因是并不是所有的int值都能放入一个byte中。
在第二种情况下,编译器知道21是一个始终能放入byte中的值。
技术上的解释是,在“赋值上下文”中,如果以下条件都成立,就可以进行“原始缩小转换”到bytecharshort
  • 该值是一个编译时的常量表达式的结果(包括字面量)。
  • 表达式的类型是byteshortcharint
  • 被赋值的常量值在"目标"类型的域中是可表示的(没有损失)。

请注意,这仅适用于赋值语句,或者更准确地说,在赋值上下文中。因此:

Byte b4 = new Byte(21);  // incorrect

给出一个编译错误。
例如,Eclipse IDE有一个选项,允许您忽略编译错误并运行代码。如果您选择此选项,IDE的编译器将创建一个`.class`文件,其中包含有错误的方法,如果调用该方法,将会抛出一个未检查的异常。异常消息将提到编译错误的消息。

1
有没有更多有用的“特定案例”的建议?我正在寻找一些示例,其中根本原因是有关Java语言的某些明显可识别的误解...而这些误解尚未被覆盖到。 - Stephen C
你没有提到的用例是用于数组初始化。例如:int[] a = {23L};long l = 23; int[] a = {l}; - Lino

0
比较 BigDecimal .valueOf (Math .sqrt(2)) .intValueExact ().intValue ()。如果参数应该是整数,请使用 intValueExact。如果截断对您来说可以接受或者它可以被证明是一个整数(如 Math .sqrt (Math .multiplyExact (i, i))),则使用后者。在后一种情况下,您可能希望 assert r * r == ir 代表您的 squareRoot)。
不幸的是,没有 .charValueExact
另请参见 JDK-8279986

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