53、Java 运算符与类型转换详解

Java 运算符与类型转换详解

1. 一元运算符
  • 负号运算符(-)
    • 对于整数, -x 等于 (~x)+1
    • 对于浮点数,取负操作与从 0 中减去该数不同。例如,若 x +0.0 ,则 0.0 - x +0.0 ,而 -x -0.0 。一元负号仅反转浮点数的符号。特殊情况如下:
      • 若操作数为 NaN ,结果为 NaN
      • 若操作数为无穷大,结果为相反符号的无穷大。
      • 若操作数为零,结果为相反符号的零。
  • 按位取反运算符(~)
    • 一元 ~ 运算符的操作数表达式类型必须可转换为基本整数类型,否则会发生编译时错误。
    • 对操作数执行一元数值提升,一元按位取反表达式的类型是操作数的提升类型。
    • 运行时,一元按位取反表达式的值是操作数提升值的按位取反。在所有情况下, ~x 等于 (-x)-1
  • 逻辑取反运算符(!)
    • 一元 ! 运算符的操作数表达式类型必须为 boolean Boolean ,否则会发生编译时错误。
    • 一元逻辑取反表达式的类型为 boolean
    • 运行时,必要时对操作数进行拆箱转换。若(可能已转换的)操作数值为 false ,则一元逻辑取反表达式的值为 true ;若操作数值为 true ,则结果为 false
2. 类型转换表达式
  • 语法形式
    • CastExpression 有以下几种形式:
      • ( PrimitiveType ) UnaryExpression
      • ( ReferenceType {AdditionalBound} ) UnaryExpressionNotPlusMinus
      • ( ReferenceType {AdditionalBound} ) LambdaExpression
    • AdditionalBound 的形式为 & InterfaceType 。括号及其包含的类型或类型列表有时称为类型转换运算符。
  • 编译时规则
    • 若类型转换运算符包含类型列表(即 ReferenceType 后跟一个或多个 AdditionalBound 项),则必须满足以下所有条件,否则会发生编译时错误:
      • ReferenceType 必须表示类或接口类型。
      • 所有列出类型的擦除形式必须两两不同。
      • 列出的任意两个类型不能是同一泛型接口不同参数化的子类型。
  • 目标类型与结果类型
    • 类型转换表达式引入的转换上下文的目标类型,要么是类型转换运算符中出现的 PrimitiveType ReferenceType (若后面没有 AdditionalBound 项),要么是类型转换运算符中出现的 ReferenceType AdditionalBound 项所表示的交集类型。
    • 类型转换表达式的类型是对该目标类型应用捕获转换的结果。

下面是类型转换表达式的流程图:

graph TD;
    A[CastExpression] --> B[( PrimitiveType ) UnaryExpression];
    A --> C[( ReferenceType {AdditionalBound} ) UnaryExpressionNotPlusMinus];
    A --> D[( ReferenceType {AdditionalBound} ) LambdaExpression];
    C --> E{ReferenceType 是类或接口类型?};
    E -- 是 --> F{列出类型擦除两两不同?};
    F -- 是 --> G{无同泛型接口不同参数化子类型?};
    G -- 是 --> H[编译通过];
    E -- 否 --> I[编译错误];
    F -- 否 --> I;
    G -- 否 --> I;
3. 乘法运算符
  • 运算符概述
    • 乘法运算符包括 * / % 。它们具有相同的优先级,并且在语法上是左结合的(从左到右分组)。
    • 乘法运算符的每个操作数类型必须可转换为基本数值类型,否则会发生编译时错误。对操作数执行二元数值提升,乘法表达式的类型是操作数的提升类型。若提升类型为 int long ,则执行整数运算;若为 float double ,则执行浮点运算。
  • 乘法运算符(*)
    • 二元 * 运算符执行乘法,产生操作数的乘积。若操作数表达式没有副作用,乘法是可交换的操作。整数乘法在操作数类型相同时是可结合的,而浮点乘法不是。
    • 若整数乘法溢出,结果是数学乘积在足够大的二进制补码格式下的低阶位。因此,若发生溢出,结果的符号可能与两个操作数值的数学乘积的符号不同。
    • 浮点乘法的结果由 IEEE 754 算术规则确定:
      • 若任一操作数为 NaN ,结果为 NaN
      • 若结果不为 NaN ,若两个操作数符号相同,结果为正;若符号不同,结果为负。
      • 无穷大乘以零结果为 NaN
      • 无穷大乘以有限值结果为带符号的无穷大,符号由上述规则确定。
      • 在其他情况下(不涉及无穷大或 NaN ),计算精确的数学乘积,然后选择浮点值集:
        • 若乘法表达式是 FP - strict 的:
          • 若乘法表达式类型为 float ,则必须选择 float 值集。
          • 若乘法表达式类型为 double ,则必须选择 double 值集。
        • 若乘法表达式不是 FP - strict 的:
          • 若乘法表达式类型为 float ,则可选择 float 值集或 float - extended - exponent 值集。
          • 若乘法表达式类型为 double ,则可选择 double 值集或 double - extended - exponent 值集。
      • 从所选值集中选择一个值来表示乘积。若乘积的大小太大无法表示,则称操作溢出,结果为适当符号的无穷大。否则,使用 IEEE 754 舍入到最接近模式将乘积舍入到所选值集中最接近的值。Java 编程语言要求支持 IEEE 754 定义的渐进下溢。尽管可能发生溢出、下溢或信息丢失,但乘法运算符 * 的计算永远不会抛出运行时异常。
操作数情况 乘法结果
任一操作数为 NaN NaN
无穷大乘以零 NaN
无穷大乘以有限值 带符号的无穷大
其他情况 按规则计算并选择值集表示
4. 除法运算符(/)
  • 整数除法
    • 二元 / 运算符执行除法,产生操作数的商。左操作数是被除数,右操作数是除数。整数除法向 0 舍入,即对于经过二元数值提升后为整数的操作数 n d ,商 q 是一个整数值,其绝对值尽可能大,同时满足 |d ⋅ q| ≤ |n| 。当 |n| ≥ |d| n d 符号相同时, q 为正;当 |n| ≥ |d| n d 符号相反时, q 为负。
    • 有一种特殊情况不满足此规则:若被除数是其类型可能的最大负整数,且除数为 -1 ,则会发生整数溢出,结果等于被除数。尽管发生溢出,但这种情况下不会抛出异常。若整数除法中的除数为 0,则会抛出 ArithmeticException
  • 浮点除法
    • 浮点除法的结果由 IEEE 754 算术规则确定:
      • 若任一操作数为 NaN ,结果为 NaN
      • 若结果不为 NaN ,若两个操作数符号相同,结果为正;若符号不同,结果为负。
      • 无穷大除以无穷大结果为 NaN
      • 无穷大除以有限值结果为带符号的无穷大。
      • 有限值除以无穷大结果为带符号的零。
      • 零除以零结果为 NaN ;零除以任何其他有限值结果为带符号的零。
      • 非零有限值除以零结果为带符号的无穷大。
      • 在其他情况下(不涉及无穷大或 NaN ),计算精确的数学商,然后选择浮点值集,规则与乘法类似。从所选值集中选择一个值来表示商,若商的大小太大无法表示,则操作溢出,结果为适当符号的无穷大。否则,使用 IEEE 754 舍入到最接近模式将商舍入到所选值集中最接近的值。尽管可能发生溢出、下溢、除以零或信息丢失,但浮点除法运算符 / 的计算永远不会抛出运行时异常。

下面是除法运算的流程图:

graph TD;
    A[除法运算] --> B{操作数是否有 NaN?};
    B -- 是 --> C[结果为 NaN];
    B -- 否 --> D{是否为整数除法?};
    D -- 是 --> E{除数是否为 0?};
    E -- 是 --> F[抛出 ArithmeticException];
    E -- 否 --> G{被除数是最大负整数且除数为 -1?};
    G -- 是 --> H[溢出,结果等于被除数];
    G -- 否 --> I[正常整数除法];
    D -- 否 --> J{是否为无穷大相除?};
    J -- 是 --> C;
    J -- 否 --> K{是否有无穷大参与?};
    K -- 是 --> L[按规则得到带符号结果];
    K -- 否 --> M[计算精确商并选择值集];
5. 取余运算符(%)
  • 整数取余
    • 二元 % 运算符用于计算操作数的余数,左操作数是被除数,右操作数是除数。在 Java 中,对于经过二元数值提升后为整数的操作数,余数运算结果满足 (a/b)*b+(a%b) 等于 a 。即使在被除数是其类型可能的最大负整数且除数为 -1 的特殊情况下,该等式也成立(此时余数为 0)。由此可知,余数运算结果仅当被除数为负时可能为负,仅当被除数为正时可能为正,且结果的绝对值总是小于除数的绝对值。若整数取余运算符的除数为 0,则会抛出 ArithmeticException
    • 示例代码:
class Test1 { 
    public static void main(String[] args) { 
        int a = 5%3;  // 2 
        int b = 5/3;  // 1 
        System.out.println("5%3 produces " + a + 
                           " (note that 5/3 produces " + b + ")"); 

        int c = 5%(-3);  // 2 
        int d = 5/(-3);  // -1 
        System.out.println("5%(-3) produces " + c + 
                           " (note that 5/(-3) produces " + d + ")"); 

        int e = (-5)%3;  // -2 
        int f = (-5)/3;  // -1 
        System.out.println("(-5)%3 produces " + e + 
                           " (note that (-5)/3 produces " + f + ")"); 

        int g = (-5)%(-3);  // -2 
        int h = (-5)/(-3);  // 1 
        System.out.println("(-5)%(-3) produces " + g + 
                           " (note that (-5)/(-3) produces " + h + ")"); 
    } 
}
- 输出结果:
5%3 produces 2 (note that 5/3 produces 1)
5%(-3) produces 2 (note that 5/(-3) produces -1)
(-5)%3 produces -2 (note that (-5)/3 produces -1)
(-5)%(-3) produces -2 (note that (-5)/(-3) produces 1)
  • 浮点取余
    • Java 中 % 运算符计算的浮点余数结果与 IEEE 754 定义的余数运算不同。IEEE 754 余数运算从舍入除法计算余数,而 Java 中的 % 运算符在浮点运算中的行为类似于整数取余运算符,可与 C 库函数 fmod 比较。IEEE 754 余数运算可通过库例程 Math.IEEEremainder 计算。
    • 浮点余数运算结果由 IEEE 754 算术规则确定:
      • 若任一操作数为 NaN ,结果为 NaN
      • 若结果不为 NaN ,结果的符号等于被除数的符号。
      • 若被除数为无穷大,或除数为零,或两者皆为,则结果为 NaN
      • 若被除数为有限值且除数为无穷大,结果等于被除数。
      • 若被除数为零且除数为有限值,结果等于被除数。
      • 在其他情况下(不涉及无穷大、零或 NaN ),被除数 n 除以除数 d 的浮点余数 r 由数学关系 r = n - (d ⋅ q) 定义,其中 q 是一个整数,仅当 n/d 为负时为负,仅当 n/d 为正时为正,且其绝对值尽可能大但不超过 n d 的真实数学商的绝对值。
    • 浮点取余运算符 % 的计算永远不会抛出运行时异常,即使右操作数为零,也不会发生溢出、下溢或精度损失。
    • 示例代码:
class Test2 { 
    public static void main(String[] args) { 
        double a = 5.0%3.0;  // 2.0 
        System.out.println("5.0%3.0 produces " + a); 

        double b = 5.0%(-3.0);  // 2.0 
        System.out.println("5.0%(-3.0) produces " + b); 

        double c = (-5.0)%3.0;  // -2.0 
        System.out.println("(-5.0)%3.0 produces " + c); 

        double d = (-5.0)%(-3.0);  // -2.0 
        System.out.println("(-5.0)%(-3.0) produces " + d); 
    } 
}
- 输出结果:
5.0%3.0 produces 2.0
5.0%(-3.0) produces 2.0
(-5.0)%3.0 produces -2.0
(-5.0)%(-3.0) produces -2.0
6. 加法运算符
  • 语法形式
    • 加法运算符包括 + - ,它们具有相同的优先级,并且在语法上是左结合的。
    • + 运算符的任一操作数类型为 String ,则该操作是字符串连接。否则, + 运算符的每个操作数类型必须可转换为基本数值类型,否则会发生编译时错误。对于二元 - 运算符,每个操作数类型必须可转换为基本数值类型,否则会发生编译时错误。
  • 字符串连接运算符(+)
    • 若只有一个操作数表达式类型为 String ,则在运行时对另一个操作数执行字符串转换以生成字符串。字符串连接的结果是一个 String 对象的引用,该对象是两个操作数字符串的连接,左操作数的字符位于新创建字符串中右操作数字符之前。除非表达式是常量表达式,否则 String 对象是新创建的。
    • 实现可以选择一步完成转换和连接,以避免创建并丢弃中间 String 对象。为提高重复字符串连接的性能,Java 编译器可以使用 StringBuffer 类或类似技术来减少表达式计算创建的中间 String 对象数量。对于基本类型,实现还可以通过直接从基本类型转换为字符串来优化掉包装对象的创建。
    • 示例:
System.out.println("The square root of 2 is " + Math.sqrt(2));
- 输出结果:
The square root of 2 is 1.4142135623730952
- 由于 `+` 运算符在语法上是左结合的,在某些情况下需要注意以获得期望的结果。例如:
System.out.println(1 + 2 + " fiddlers"); // 输出 "3 fiddlers"
System.out.println("fiddlers " + 1 + 2); // 输出 "fiddlers 12"
  • 代码示例:字符串连接与条件语句结合
class Bottles { 
    static void printSong(Object stuff, int n) { 
        String plural = (n == 1) ? "" : "s"; 
        loop: while (true) { 
            System.out.println(n + " bottle" + plural 
                    + " of " + stuff + " on the wall,"); 
            System.out.println(n + " bottle" + plural 
                    + " of " + stuff + ";"); 
            System.out.println("You take one down " 
                    + "and pass it around:"); 
            --n; 
            plural = (n == 1) ? "" : "s"; 
            if (n == 0) 
                break loop; 
            System.out.println(n + " bottle" + plural 
                    + " of " + stuff + " on the wall!"); 
            System.out.println(); 
        } 
        System.out.println("No bottles of " + 
                    stuff + " on the wall!"); 
    } 

    public static void main(String[] args) { 
        printSong("slime", 3); 
    } 
}
- 输出结果:
3 bottles of slime on the wall,
3 bottles of slime;
You take one down and pass it around:
2 bottles of slime on the wall!
2 bottles of slime on the wall,
2 bottles of slime;
You take one down and pass it around:
1 bottle of slime on the wall!
1 bottle of slime on the wall,
1 bottle of slime;
You take one down and pass it around:
No bottles of slime on the wall!

在实际编程中,我们需要根据具体的需求和场景,正确使用这些运算符和类型转换,以确保程序的正确性和性能。例如,在进行数值计算时,要注意溢出和精度问题;在进行字符串处理时,要合理利用字符串连接的优化技巧。

Java 运算符与类型转换详解(续)

7. 总结与注意事项
  • 运算符优先级与结合性
    • 在 Java 编程中,运算符的优先级和结合性至关重要。乘法运算符( * / % )和加法运算符( + - )具有不同的优先级,且它们都是左结合的。这意味着在复杂的表达式中,运算符会按照从左到右的顺序进行计算。例如,表达式 1 + 2 * 3 会先计算 2 * 3 ,再将结果与 1 相加。
    • 为了避免因优先级和结合性导致的错误,建议在编写复杂表达式时使用括号明确指定计算顺序。例如, (1 + 2) * 3 会先计算 1 + 2 ,再将结果乘以 3。
  • 类型转换的影响
    • 类型转换在 Java 中是一个常见的操作,但需要注意其可能带来的影响。在进行类型转换时,要确保目标类型能够容纳源类型的值,否则可能会导致数据丢失。例如,将一个大整数转换为小整数类型时,可能会发生溢出。
    • 对于浮点类型的转换,要注意精度问题。例如,将 double 类型转换为 float 类型时,可能会损失精度。在进行类型转换时,建议先检查源值的范围,确保转换的安全性。
  • 异常处理
    • 在使用运算符时,要注意可能抛出的异常。例如,整数除法中除数为 0 会抛出 ArithmeticException ,在编写代码时要进行相应的异常处理。可以使用 try-catch 语句来捕获并处理这些异常,以增强程序的健壮性。
运算符 异常情况 处理建议
/ (整数除法) 除数为 0 抛出 ArithmeticException 使用 try-catch 语句捕获异常
% (整数取余) 除数为 0 抛出 ArithmeticException 使用 try-catch 语句捕获异常

下面是一个处理整数除法异常的示例代码:

public class DivideByZeroExample {
    public static void main(String[] args) {
        int dividend = 10;
        int divisor = 0;
        try {
            int result = dividend / divisor;
            System.out.println("结果: " + result);
        } catch (ArithmeticException e) {
            System.out.println("错误: 除数不能为 0");
        }
    }
}
8. 最佳实践
  • 数值计算
    • 在进行数值计算时,要根据具体需求选择合适的类型。如果需要高精度计算,建议使用 BigDecimal 类,避免使用 float double 类型可能带来的精度问题。例如,在处理金融数据时,使用 BigDecimal 可以确保计算的准确性。
    • 对于可能发生溢出的计算,要进行范围检查或使用更大范围的类型。例如,在处理大整数时,可以使用 long 类型替代 int 类型。
  • 字符串处理
    • 在进行字符串连接时,对于少量的字符串连接,可以直接使用 + 运算符。但对于大量的字符串连接,建议使用 StringBuilder StringBuffer 类,以提高性能。 StringBuilder 是非线程安全的,性能较高; StringBuffer 是线程安全的,性能相对较低。
    • 示例代码:
public class StringConcatenationExample {
    public static void main(String[] args) {
        // 少量字符串连接
        String str1 = "Hello";
        String str2 = " World";
        String result1 = str1 + str2;
        System.out.println(result1);

        // 大量字符串连接
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 1000; i++) {
            sb.append("a");
        }
        String result2 = sb.toString();
        System.out.println(result2.length());
    }
}
  • 类型转换
    • 在进行类型转换时,要明确转换的目的和可能的风险。如果是从子类转换为父类,通常是安全的;但从父类转换为子类时,需要进行类型检查,避免 ClassCastException 。可以使用 instanceof 运算符进行类型检查。
    • 示例代码:
class Animal {
    public void makeSound() {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof");
    }
}

public class TypeCastExample {
    public static void main(String[] args) {
        Animal animal = new Dog();
        if (animal instanceof Dog) {
            Dog dog = (Dog) animal;
            dog.makeSound();
        }
    }
}
9. 复杂表达式计算流程

下面是一个复杂表达式计算流程的 mermaid 流程图,展示了在一个包含多种运算符和类型转换的表达式中,程序是如何进行计算的。

graph TD;
    A[开始] --> B[解析表达式];
    B --> C{是否有括号?};
    C -- 是 --> D[先计算括号内表达式];
    C -- 否 --> E{是否有乘法或除法运算符?};
    D --> E;
    E -- 是 --> F[计算乘法或除法];
    E -- 否 --> G{是否有加法或减法运算符?};
    F --> G;
    G -- 是 --> H[计算加法或减法];
    G -- 否 --> I{是否有类型转换?};
    H --> I;
    I -- 是 --> J[进行类型转换];
    I -- 否 --> K[得到最终结果];
    J --> K;
    K --> L[结束];
10. 总结

通过本文的介绍,我们详细了解了 Java 中的各种运算符(一元运算符、乘法运算符、除法运算符、取余运算符、加法运算符)和类型转换。在实际编程中,要根据具体需求正确使用这些运算符和类型转换,注意运算符的优先级、结合性,以及类型转换可能带来的影响。同时,要进行合理的异常处理和性能优化,以提高程序的正确性和性能。希望本文能够帮助你更好地掌握 Java 中的运算符和类型转换,编写出高质量的 Java 代码。

在未来的 Java 编程中,不断实践和总结经验,将这些知识运用到实际项目中,你将能够更加熟练地处理各种复杂的计算和数据处理任务。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值