转载自 :https://dunwu.github.io/javacore/basics/java-data-type.html

2. 数据转换

3. 装箱和拆箱

4. 判等问题

5. 数值计算

6. 参考资料

# 1. 数据类型分类

Java 中的数据类型有两类:

  • 值类型(又叫内置数据类型,基本数据类型)

引用类型(除值类型以外,都是引用类型,包括 String、数组)

# 1.1. 值类型

Java 语言提供了 8 种基本类型,大致分为 4

基本数据类型分类比特数默认值取值范围说明
boolean 布尔型 8 位 false {false, true}
char 字符型 16 位 '\u0000' [0, $2^{16} - 1$] 存储 Unicode 码,用单引号赋值
byte 整数型 8 位 0 [-$2^7$, $2^7 - 1$]
short 整数型 16 位 0 [-$2^{15}$, $2^{15} - 1$]
int 整数型 32 位 0 [-$2^{31}$, $2^{31} - 1$]
long 整数型 64 位 0L [-$2^{63}$, $2^{63} - 1$] 赋值时一般在数字后加上 lL
float 浮点型 32 位 +0.0F [$2^{-149}$, $2^{128} - 1$] 赋值时必须在数字后加上 fF
double 浮点型 64 位 +0.0D [$2^{-1074}$, $2^{1024} - 1$] 赋值时一般在数字后加 dD

尽管各种数据类型的默认值看起来不一样,但在内存中都是 0。

在这些基本类型中,booleanchar 是唯二的无符号类型。

# 1.2. 值类型和引用类型的区别

  • 从概念方面来说
  • 基本类型:变量名指向具体的数值。
  • 引用类型:变量名指向存数据对象的内存地址。

从内存方面来说

  • 基本类型:变量在声明之后,Java 就会立刻分配给他内存空间。
  • 引用类型:它以特殊的方式(类似 C 指针)向对象实体(具体的值),这类变量声明时不会分配内存,只是存储了一个内存地址。

从使用方面来说

  • 基本类型:使用时需要赋具体值,判断时使用 == 号。
  • 引用类型:使用时可以赋 null,判断时使用 equals 方法。

👉 扩展阅读:Java 基本数据类型和引用类型

(opens new window)

这篇文章对于基本数据类型和引用类型的内存存储讲述比较生动。

# 2. 数据转换

Java 中,数据类型转换有两种方式:

  • 自动转换

强制转换

# 2.1. 自动转换

一般情况下,定义了某数据类型的变量,就不能再随意转换。但是 JAVA 允许用户对基本类型做有限度的类型转换。

如果符合以下条件,则 JAVA 将会自动做类型转换:

  • 由小数据转换为大数据

    显而易见的是,“小”数据类型的数值表示范围小于“大”数据类型的数值表示范围,即精度小于“大”数据类型。

    所以,如果“大”数据向“小”数据转换,会丢失数据精度。比如:long 转为 int,则超出 int 表示范围的数据将会丢失,导致结果的不确定性。

    反之,“小”数据向“大”数据转换,则不会存在数据丢失情况。由于这个原因,这种类型转换也称为扩大转换

    这些类型由“小”到“大”分别为:(byte,short,char) < int < long < float < double。

    这里我们所说的“大”与“小”,并不是指占用字节的多少,而是指表示值的范围的大小。

转换前后的数据类型要兼容

由于 boolean 类型只能存放 true 或 false,这与整数或字符是不兼容的,因此不可以做类型转换。

整型类型和浮点型进行计算后,结果会转为浮点类型

示例:

long x = 30;
float y = 14.3f;
System.out.println("x/y = " + x/y);

输出:

x/y = 1.9607843

可见 long 虽然精度大于 float 类型,但是结果为浮点数类型。

# 2.2. 强制转换

在不符合自动转换条件时或者根据用户的需要,可以对数据类型做强制的转换。

强制转换使用括号 ()

引用类型也可以使用强制转换。

示例:

float f = 25.5f;
int x = (int)f;
System.out.println("x = " + x);

# 3. 装箱和拆箱

# 3.1. 包装类、装箱、拆箱

Java 中为每一种基本数据类型提供了相应的包装类,如下:

Byte <-> byte
Short <-> short
Integer <-> int
Long <-> long
Float <-> float
Double <-> double
Character <-> char
Boolean <-> boolean

引入包装类的目的就是:提供一种机制,使得基本数据类型可以与引用类型互相转换

基本数据类型与包装类的转换被称为装箱拆箱

  • 装箱(boxing)是将值类型转换为引用类型。例如:intInteger
  • 装箱过程是通过调用包装类的 valueOf 方法实现的。

拆箱(unboxing)是将引用类型转换为值类型。例如:Integerint

  • 拆箱过程是通过调用包装类的 xxxValue 方法实现的。(xxx 代表对应的基本数据类型)。

# 3.2. 自动装箱、自动拆箱

基本数据(Primitive)型的自动装箱(boxing)拆箱(unboxing)自 JDK 5 开始提供的功能。

自动装箱与拆箱的机制可以让我们在 Java 的变量赋值或者是方法调用等情况下使用原始类型或者对象类型更加简单直接。 因为自动装箱会隐式地创建对象,如果在一个循环体中,会创建无用的中间对象,这样会增加 GC 压力,拉低程序的性能。所以在写循环时一定要注意代码,避免引入不必要的自动装箱操作。

JDK 5 之前的形式:

Integer i1 = new Integer(10); // 非自动装箱

JDK 5 之后:

Integer i2 = 10; // 自动装箱

Java 对于自动装箱和拆箱的设计,依赖于一种叫做享元模式的设计模式(有兴趣的朋友可以去了解一下源码,这里不对设计模式展开详述)。

👉 扩展阅读:深入剖析 Java 中的装箱和拆箱

(opens new window)

结合示例,一步步阐述装箱和拆箱原理。

# 3.3. 装箱、拆箱的应用和注意点

# 装箱、拆箱应用场景

  • 一种最普通的场景是:调用一个含类型为 Object 参数的方法,该 Object 可支持任意类型(因为 Object 是所有类的父类),以便通用。当你需要将一个值类型(如 int)传入时,需要使用 Integer 装箱。

另一种用法是:一个非泛型的容器,同样是为了保证通用,而将元素类型定义为 Object。于是,要将值类型数据加入容器时,需要装箱。

== 运算符的两个操作,一个操作数是包装类,另一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。

【示例】装箱、拆箱示例

Integer i1 = 10; // 自动装箱
Integer i2 = new Integer(10); // 非自动装箱
Integer i3 = Integer.valueOf(10); // 非自动装箱
int i4 = new Integer(10); // 自动拆箱
int i5 = i2.intValue(); // 非自动拆箱
System.out.println("i1 = [" + i1 + "]");
System.out.println("i2 = [" + i2 + "]");
System.out.println("i3 = [" + i3 + "]");
System.out.println("i4 = [" + i4 + "]");
System.out.println("i5 = [" + i5 + "]");
System.out.println("i1 == i2 is [" + (i1 == i2) + "]");
System.out.println("i1 == i4 is [" + (i1 == i4) + "]"); // 自动拆箱
// Output:
// i1 = [10]
// i2 = [10]
// i3 = [10]
// i4 = [10]
// i5 = [10]
// i1 == i2 is [false]
// i1 == i4 is [true]

【说明】

上面的例子,虽然简单,但却隐藏了自动装箱、拆箱和非自动装箱、拆箱的应用。从例子中可以看到,明明所有变量都初始化为数值 10 了,但为何会出现 i1 == i2 is [falsei1 == i4 is [true]

原因在于:

  • i1、i2 都是包装类,使用 == 时,Java 将它们当做两个对象,而非两个 int 值来比较,所以两个对象自然是不相等的。正确的比较操作应该使用 equals 方法。

i1 是包装类,i4 是基础数据类型,使用 == 时,Java 会将两个 i1 这个包装类对象自动拆箱为一个 int 值,再代入到 == 运算表达式中计算;最终,相当于两个 int 进行比较,由于值相同,所以结果相等。

【示例】包装类判等问题

Integer a = 127; //Integer.valueOf(127)
Integer b = 127; //Integer.valueOf(127)
log.info("\nInteger a = 127;\nInteger b = 127;\na == b ? {}", a == b);    // true

Integer c = 128; //Integer.valueOf(128)
Integer d = 128; //Integer.valueOf(128)
log.info("\nInteger c = 128;\nInteger d = 128;\nc == d ? {}", c == d);   //false
//设置-XX:AutoBoxCacheMax=1000再试试

Integer e = 127; //Integer.valueOf(127)
Integer f = new Integer(127); //new instance
log.info("\nInteger e = 127;\nInteger f = new Integer(127);\ne == f ? {}", e == f);   //false

Integer g = new Integer(127); //new instance
Integer h = new Integer(127); //new instance
log.info("\nInteger g = new Integer(127);\nInteger h = new Integer(127);\ng == h ? {}", g == h);  //false

Integer i = 128; //unbox
int j = 128;
log.info("\nInteger i = 128;\nint j = 128;\ni == j ? {}", i == j); //true

通过运行结果可以看到,虽然看起来永远是在对 127 和 127、128 和 128 判等,但 == 却并非总是返回 true。

# 装箱、拆箱应用注意点

  1. 装箱操作会创建对象,频繁的装箱操作会造成不必要的内存消耗,影响性能。所以应该尽量避免装箱。

基础数据类型的比较操作使用 ==,包装类的比较操作使用 equals 方法。

 

 

# 4. 判等问题

Java 中,通常使用 equals== 进行判等操作。equals 是方法而 == 是操作符。此外,二者使用也是有区别的:

  • 基本类型,比如 intlong,进行判等,只能使用 ==,比较的是字面值。因为基本类型的值就是其数值。

引用类型,比如 IntegerLongString,进行判等,需要使用 equals 进行内容判等。因为引用类型的直接值是指针,使用 == 的话,比较的是指针,也就是两个对象在内存中的地址,即比较它们是不是同一个对象,而不是比较对象的内容。

# 4.1. 包装类的判等

我们通过一个示例来深入研究一下判等问题。

【示例】包装类的判等

Integer a = 127; //Integer.valueOf(127)
Integer b = 127; //Integer.valueOf(127)
log.info("\nInteger a = 127;\nInteger b = 127;\na == b ? {}", a == b);    // true

Integer c = 128; //Integer.valueOf(128)
Integer d = 128; //Integer.valueOf(128)
log.info("\nInteger c = 128;\nInteger d = 128;\nc == d ? {}", c == d);   //false
//设置-XX:AutoBoxCacheMax=1000再试试

Integer e = 127; //Integer.valueOf(127)
Integer f = new Integer(127); //new instance
log.info("\nInteger e = 127;\nInteger f = new Integer(127);\ne == f ? {}", e == f);   //false

Integer g = new Integer(127); //new instance
Integer h = new Integer(127); //new instance
log.info("\nInteger g = new Integer(127);\nInteger h = new Integer(127);\ng == h ? {}", g == h);  //false

Integer i = 128; //unbox
int j = 128;
log.info("\nInteger i = 128;\nint j = 128;\ni == j ? {}", i == j); //true

第一个案例中,编译器会把 Integer a = 127 转换为 Integer.valueOf(127)。查看源码可以发现,这个转换在内部其实做了缓存,使得两个 Integer 指向同一个对象,所以 == 返回 true。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

第二个案例中,之所以同样的代码 128 就返回 false 的原因是,默认情况下会缓存[-128,127]的数值,而 128 处于这个区间之外。设置 JVM 参数加上 -XX:AutoBoxCacheMax=1000 再试试,是不是就返回 true 了呢?

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

第三和第四个案例中,New 出来的 Integer 始终是不走缓存的新对象。比较两个新对象,或者比较一个新对象和一个来自缓存的对象,结果肯定不是相同的对象,因此返回 false。

第五个案例中,我们把装箱的 Integer 和基本类型 int 比较,前者会先拆箱再比较,比较的肯定是数值而不是引用,因此返回 true。

【总结】综上,我们可以得出结论:包装类需要使用 equals 进行内容判等,而不能使用 ==

# 4.2. String 的判等

String a = "1";
String b = "1";
log.info("\nString a = \"1\";\nString b = \"1\";\na == b ? {}", a == b); //true

String c = new String("2");
String d = new String("2");
log.info("\nString c = new String(\"2\");\nString d = new String(\"2\");\nc == d ? {}", c == d); //false

String e = new String("3").intern();
String f = new String("3").intern();
log.info("\nString e = new String(\"3\").intern();\nString f = new String(\"3\").intern();\ne == f ? {}", e == f); //true

String g = new String("4");
String h = new String("4");
log.info("\nString g = new String(\"4\");\nString h = new String(\"4\");\ng == h ? {}", g.equals(h)); //true

在 JVM 中,当代码中出现双引号形式创建字符串对象时,JVM 会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回;否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。这种机制,就是字符串驻留或池化。

第一个案例返回 true,因为 Java 的字符串驻留机制,直接使用双引号声明出来的两个 String 对象指向常量池中的相同字符串。

第二个案例,new 出来的两个 String 是不同对象,引用当然不同,所以得到 false 的结果。

第三个案例,使用 String 提供的 intern 方法也会走常量池机制,所以同样能得到 true。

第四个案例,通过 equals 对值内容判等,是正确的处理方式,当然会得到 true。

虽然使用 new 声明的字符串调用 intern 方法,也可以让字符串进行驻留,但在业务代码中滥用 intern,可能会产生性能问题。

【示例】String#intern 性能测试

//-XX:+PrintStringTableStatistics
//-XX:StringTableSize=10000000
List<String> list = new ArrayList<>();
long begin = System.currentTimeMillis();
list = IntStream.rangeClosed(1, 10000000)
    .mapToObj(i -> String.valueOf(i).intern())
    .collect(Collectors.toList());
System.out.println("size:" + list.size());
System.out.println("time:" + (System.currentTimeMillis() - begin));

上面的示例执行时间会比较长。原因在于:字符串常量池是一个固定容量的 Map。如果容量太小(Number of buckets=60013)、字符串太多(1000 万个字符串),那么每一个桶中的字符串数量会非常多,所以搜索起来就很慢。输出结果中的 Average bucket size=167,代表了 Map 中桶的平均长度是 167。

解决方法是:设置 JVM 参数 -XX:StringTableSize=10000000,指定更多的桶。

为了方便观察,可以在启动程序时设置 JVM 参数 -XX:+PrintStringTableStatistic,程序退出时可以打印出字符串常量表的统计信息。

执行结果比不设置 -XX:StringTableSize 要快很多。

【总结】没事别轻易用 intern,如果要用一定要注意控制驻留的字符串的数量,并留意常量表的各项指标

# 4.3. 实现 equals

如果看过 Object 类源码,你可能就知道,equals 的实现其实是比较对象引用

public boolean equals(Object obj) {
    return (this == obj);
}

之所以 Integer 或 String 能通过 equals 实现内容判等,是因为它们都覆写了这个方法。

对于自定义类型,如果不覆写 equals 的话,默认就是使用 Object 基类的按引用的比较方式。

实现一个更好的 equals 应该注意的点:

  • 考虑到性能,可以先进行指针判等,如果对象是同一个那么直接返回 true;

需要对另一方进行判空,空对象和自身进行比较,结果一定是 fasle;

需要判断两个对象的类型,如果类型都不同,那么直接返回 false;

确保类型相同的情况下再进行类型强制转换,然后逐一判断所有字段。

【示例】自定义 equals 示例

自定义类:

class Point {
    private final int x;
    private final int y;
    private final String desc;
}

自定义 equals:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Point that = (Point) o;
    return x == that.x && y == that.y;
}

# 4.4. hashCode 和 equals 要配对实现

Point p1 = new Point(1, 2, "a");
Point p2 = new Point(1, 2, "b");

HashSet<PointWrong> points = new HashSet<>();
points.add(p1);
log.info("points.contains(p2) ? {}", points.contains(p2));

按照改进后的 equals 方法,这 2 个对象可以认为是同一个,Set 中已经存在了 p1 就应该包含 p2,但结果却是 false。

出现这个 Bug 的原因是,散列表需要使用 hashCode 来定位元素放到哪个桶。如果自定义对象没有实现自定义的 hashCode 方法,就会使用 Object 超类的默认实现,得到的两个 hashCode 是不同的,导致无法满足需求。

要自定义 hashCode,我们可以直接使用 Objects.hash 方法来实现。

@Override
public int hashCode() {
    return Objects.hash(x, y);
}

# 4.5. compareTo 和 equals 的逻辑一致性

【示例】自定义 compareTo 出错示例

@Data
@AllArgsConstructor
static class Student implements Comparable<Student> {

    private int id;
    private String name;

    @Override
    public int compareTo(Student other) {
        int result = Integer.compare(other.id, id);
        if (result == 0) { log.info("this {} == other {}", this, other); }
        return result;
    }

}

调用:

List<Student> list = new ArrayList<>();
list.add(new Student(1, "zhang"));
list.add(new Student(2, "wang"));
Student student = new Student(2, "li");

log.info("ArrayList.indexOf");
int index1 = list.indexOf(student);
Collections.sort(list);
log.info("Collections.binarySearch");
int index2 = Collections.binarySearch(list, student);

log.info("index1 = " + index1);
log.info("index2 = " + index2);

binarySearch 方法内部调用了元素的 compareTo 方法进行比较;

  • indexOf 的结果没问题,列表中搜索不到 id 为 2、name 是 li 的学生;

binarySearch 返回了索引 1,代表搜索到的结果是 id 为 2,name 是 wang 的学生。

修复方式很简单,确保 compareTo 的比较逻辑和 equals 的实现一致即可。

@Data
@AllArgsConstructor
static class StudentRight implements Comparable<StudentRight> {

    private int id;
    private String name;

    @Override
    public int compareTo(StudentRight other) {
        return Comparator.comparing(StudentRight::getName)
            .thenComparingInt(StudentRight::getId)
            .compare(this, other);
    }

}

# 4.6. 小心 Lombok 生成代码的“坑”

Lombok 的 @Data 注解会帮我们实现 equals 和 hashcode 方法,但是有继承关系时, Lombok 自动生成的方法可能就不是我们期望的了。

@EqualsAndHashCode 默认实现没有使用父类属性。为解决这个问题,我们可以手动设置 callSuper 开关为 true,来覆盖这种默认行为。

# 5. 数值计算

# 5.1. 浮点数计算问题

计算机是把数值保存在了变量中,不同类型的数值变量能保存的数值范围不同,当数值超过类型能表达的数值上限则会发生溢出问题。

System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(1.0 - 0.8); // 0.19999999999999996
System.out.println(4.015 * 100); // 401.49999999999994
System.out.println(123.3 / 100); // 1.2329999999999999
double amount1 = 2.15;
double amount2 = 1.10;
System.out.println(amount1 - amount2); // 1.0499999999999998

上面的几个示例,输出结果和我们预期的很不一样。为什么会是这样呢?

出现这种问题的主要原因是,计算机是以二进制存储数值的,浮点数也不例外。Java 采用了 IEEE 754 标准实现浮点数的表达和运算,你可以通过这里查看数值转化为二进制的结果。

比如,0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是 0.1000000000000000055511151231257827021181583404541015625。对于计算机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。

浮点数无法精确表达和运算的场景,一定要使用 BigDecimal 类型

使用 BigDecimal 时,有个细节要格外注意。让我们来看一段代码:

System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));
// Output: 0.3000000000000000166533453693773481063544750213623046875

System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.8)));
// Output: 0.1999999999999999555910790149937383830547332763671875

System.out.println(new BigDecimal(4.015).multiply(new BigDecimal(100)));
// Output: 401.49999999999996802557689079549163579940795898437500

System.out.println(new BigDecimal(123.3).divide(new BigDecimal(100)));
// Output: 1.232999999999999971578290569595992565155029296875

为什么输出结果仍然不符合预期呢?

使用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化 BigDecimal

# 5.2. 浮点数精度和格式化

浮点数的字符串格式化也要通过 BigDecimal 进行

private static void wrong1() {
    double num1 = 3.35;
    float num2 = 3.35f;
    System.out.println(String.format("%.1f", num1)); // 3.4
    System.out.println(String.format("%.1f", num2)); // 3.3
}

private static void wrong2() {
    double num1 = 3.35;
    float num2 = 3.35f;
    DecimalFormat format = new DecimalFormat("#.##");
    format.setRoundingMode(RoundingMode.DOWN);
    System.out.println(format.format(num1)); // 3.35
    format.setRoundingMode(RoundingMode.DOWN);
    System.out.println(format.format(num2)); // 3.34
}

private static void right() {
    BigDecimal num1 = new BigDecimal("3.35");
    BigDecimal num2 = num1.setScale(1, BigDecimal.ROUND_DOWN);
    System.out.println(num2); // 3.3
    BigDecimal num3 = num1.setScale(1, BigDecimal.ROUND_HALF_UP);
    System.out.println(num3); // 3.4
}

# 5.3. BigDecimal 判等问题

private static void wrong() {
    System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1")));
}

private static void right() {
    System.out.println(new BigDecimal("1.0").compareTo(new BigDecimal("1")) == 0);
}

BigDecimal 的 equals 方法的注释中说明了原因,equals 比较的是 BigDecimal 的 value 和 scale,1.0 的 scale 是 1,1 的 scale 是 0,所以结果一定是 false。

如果我们希望只比较 BigDecimal 的 value,可以使用 compareTo 方法

BigDecimal 的 equals 和 hashCode 方法会同时考虑 value 和 scale,如果结合 HashSet 或 HashMap 使用的话就可能会出现麻烦。比如,我们把值为 1.0 的 BigDecimal 加入 HashSet,然后判断其是否存在值为 1 的 BigDecimal,得到的结果是 false。

Set<BigDecimal> hashSet1 = new HashSet<>();
hashSet1.add(new BigDecimal("1.0"));
System.out.println(hashSet1.contains(new BigDecimal("1")));//返回false


解决办法有两个:

第一个方法是,使用 TreeSet 替换 HashSet。TreeSet 不使用 hashCode 方法,也不使用 equals 比较元素,而是使用 compareTo 方法,所以不会有问题。

第二个方法是,把 BigDecimal 存入 HashSet 或 HashMap 前,先使用 stripTrailingZeros 方法去掉尾部的零,比较的时候也去掉尾部的 0,确保 value 相同的 BigDecimal,scale 也是一致的。

Set<BigDecimal> hashSet2 = new HashSet<>();
hashSet2.add(new BigDecimal("1.0").stripTrailingZeros());
System.out.println(hashSet2.contains(new BigDecimal("1.000").stripTrailingZeros()));//返回true

Set<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(new BigDecimal("1.0"));
System.out.println(treeSet.contains(new BigDecimal("1")));//返回true

# 5.4. 数值溢出

数值计算还有一个要小心的点是溢出,不管是 int 还是 long,所有的基本数值类型都有超出表达范围的可能性。

long l = Long.MAX_VALUE;
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true

显然这是发生了溢出,而且是默默的溢出,并没有任何异常。这类问题非常容易被忽略,改进方式有下面 2 种。

方法一是,考虑使用 Math 类的 addExact、subtractExact 等 xxExact 方法进行数值运算,这些方法可以在数值溢出时主动抛出异常。

try {
    long l = Long.MAX_VALUE;
    System.out.println(Math.addExact(l, 1));
} catch (Exception ex) {
    ex.printStackTrace();
}

方法二是,使用大数类 BigInteger。BigDecimal 是处理浮点数的专家,而 BigInteger 则是对大数进行科学计算的专家。

BigInteger i = new BigInteger(String.valueOf(Long.MAX_VALUE));
System.out.println(i.add(BigInteger.ONE).toString());

try {
    long l = i.add(BigInteger.ONE).longValueExact();
} catch (Exception ex) {
    ex.printStackTrace();
}

# 6. 参考资料

 

Integer.parseInt()和这个Integer.valueOf()的详解

new Integer.valueof()返回的是Integer的对象。
Integer.parseInt() 返回的是一个int的值。
new Integer.valueof().intValue();返回的也是一个int的值。

 

parseInt

public static int parseInt(String s, int radix) throws NumberFormatException
以第二个参数所指定基数将字符串参数分析为一个带符号的整数。除了第一个字符可以用 ASCII 的减号  '-' 来表示一个负值外,字符串中的字符必须是指定基数的数(由  Character.digit 是否返回非负值决定)。返回作为结果的整数值。

 

参数:
s - 包含整数的  String 。
radix - 使用的进制
返回值:
指定基数的字符串参数所表示的整数。
抛出:
 
NumberFormatException
若该串不包含一个可分析的整数。
parseInt
public static int parseInt(String s) throws NumberFormatException
将字符串参数作为带符号十进制整数来分析。除过第一个字符为 ASCII 字符中减号  '-' 表示的负数,字符串中的字符都必须是十进制数。

 

参数:
s - 串。
返回值:
十进制参数表示的整数。
抛出:
 
NumberFormatException
若该串不包含一个可分析的整数。

valueOf

public static Integer valueOf(String s, int radix) throws NumberFormatException
返回初始化为指定 String 值的新的 Integer 对象。若该 String 不能作为 int 分析,则抛出异常。

 

参数:
s - 待分析的字符串。
返回值:
新创建的  Integer ,将其以指定基数初始化为字符串参数所表示的值。
抛出:
 
NumberFormatException
若  String不包含可分析的整数。

valueOf

public static Integer valueOf(String s) throws NumberFormatException
返回初始化为指定 String 值的新的 Integer 对象。若该 String 不能作为 int 分析,则抛出异常。假设基数为 10 。

 

参数:
s - 待分析的字符串。
返回值:
新创建的  Integer ,将其初始化为字符串参数所表示的值。
抛出:
 
NumberFormatException
如果该串不包含一个可分析的整数。

 

 

static int parseInt(String s)
          将字符串参数作为有符号的十进制整数进行分析。

static Integer valueOf(int i)
          返回一个表示指定的 int 值的 Integer 实例。
static Integer valueOf(String s)
          返回保持指定的 String 的值的 Integer 对象。

从返回值可以看出他们的区别   parseInt()返回的是基本类型int
而valueOf()返回的是包装类Integer  Integer是可以使用对象方法的  而int类型就不能和Object类型进行互相转换

int zhuan=Integer.parseInt(chuan);
int zhuanyi=Integer.valueOf(chuan); 为什么你的程序返回值都可以用int来接收呢?  因为Integer和int可以自动转换
Integer i = 5;   int k  = i;像是这样表示是没有编译错误的

 


例题:

设有下面两个赋值语句:
a = Integer.parseInt(“123”);
b = Integer.valueOf(“123”).intValue();
下述说法正确的是(  d  )。
A、a是整数类型变量,b是整数类对象。
B、a是整数类对象,b是整数类型变量。
C、a和b都是整数类对象并且值相等。 
D、a和b都是整数类型变量并且值相等。

详细解析:
parseInt(String s )方法是类Integer的静态方法,它的作用就是将形参 s 转化为整数,比如:
Interger.parseInt("1")=1;
Integer.parseInt("20")=20;
Integer.parseInt("324")=324;
当然,s 表示的整数必须合法,不然是会抛异常的。
valueOf(String s )也是Integer类的静态方法,它的作用是将形参 s 转化为Integer对象,
什么是Integer对象,Integer就是基本数据类型int型包装类,就是将int包装成一个类,这样在很多场合下是必须的。如果理解不了,你就认为int是Integer的mini版,好用了很多,但也丢失了一些功能,好了,看代码:
Interger.valueOf("123")=Integer(123)
这时候Integer(123)就是整数123的对象表示形式,它再调用intValue()方法,就是将123的对象表示形式转化为基本数据123
所以,选择D

转载于:https://www.cnblogs.com/flord/p/6056969.html

 

 

Java BigDecimal和double区别

对于不需要任何准确计算精度的数字可以直接使用float或double,但是如果需要精确计算的结果,则必须使用BigDecimal类,而且使用BigDecimal类也可以进行大数的操作。BigDecimal类的常用方法如表11-15所示。

表11-15 BigDecimal类的常用方法

序号

    法

类型

    述

1

public BigDecimal(double val)

构造

将double表示形式转换

为BigDecimal

2

public BigDecimal(int val)

构造

将int表示形式转换为

BigDecimal

3

public BigDecimal(String val)

构造

将字符串表示

形式转换为BigDecimal

4

public BigDecimal add(BigDecimal augend)

普通

加法

5

public BigDecimal subtract(BigDecimal
subtrahend)

普通

减法

6

public BigDecimal multiply(BigDecimal
multiplicand)

普通

乘法

7

public BigDecimal divide(BigDecimal
divisor)

普通

除法

范例:进行四舍五入的四则运算

复制代码
    package org.lxh.demo11.numberdemo;
    import java.math.BigDecimal;
    class MyMath {
        public static double add(double d1, double d2)
    {        // 进行加法运算
             BigDecimal b1 = new BigDecimal(d1);
             BigDecimal b2 = new BigDecimal(d2);
            return b1.add(b2).doubleValue();
         }
        public static double sub(double d1, double d2)
    {        // 进行减法运算
             BigDecimal b1 = new BigDecimal(d1);
             BigDecimal b2 = new BigDecimal(d2);
            return b1.subtract(b2).doubleValue();
         }
        public static double mul(double d1, double d2)
    {        // 进行乘法运算
             BigDecimal b1 = new BigDecimal(d1);
             BigDecimal b2 = new BigDecimal(d2);
            return b1.multiply(b2).doubleValue();
         }
        public static double div(double d1,
    double d2,int len) {// 进行除法运算
             BigDecimal b1 = new BigDecimal(d1);
             BigDecimal b2 = new BigDecimal(d2);
            return b1.divide(b2,len,BigDecimal.
    ROUND_HALF_UP).doubleValue();
         }
        public static double round(double d,
    int len) {     // 进行四舍五入
    操作
             BigDecimal b1 = new BigDecimal(d);
             BigDecimal b2 = new BigDecimal(1);
            // 任何一个数字除以1都是原数字
            // ROUND_HALF_UP是BigDecimal的一个常量,
    表示进行四舍五入的操作
            return b1.divide(b2, len,BigDecimal.
    ROUND_HALF_UP).doubleValue();
         }
    }
    public class BigDecimalDemo01 {
        public static void main(String[] args) {
             System.out.println("加法运算:" +
    MyMath.round(MyMath.add(10.345,
    3.333), 1));
             System.out.println("乘法运算:" +
    MyMath.round(MyMath.mul(10.345,
    3.333), 3));
             System.out.println("除法运算:" +
    MyMath.div(10.345, 3.333, 3));
             System.out.println("减法运算:" +
    MyMath.round(MyMath.sub(10.345,
    3.333), 3));
         }
    }
复制代码

 

BigDecimal是Java中用来表示任意精确浮点数运算的类,在BigDecimal中,使用unscaledValue × 10-scale来表示一个浮点数。其中,unscaledValue是一个BigInteger,scale是一个int。从这个表示方法来看,BigDecimal只能标识有限小数,不过可以表示的数据范围远远大于double,在实际应用中基本足够了。

下面提一下两个精度问题:
 
问题一:BigDecimal的精度问题(StackOverflow上有个家伙问了相关的问题
System.out.println(new BigDecimal(0.1).toString()); // 0.1000000000000000055511151231257827021181583404541015625
System.out.println(new BigDecimal("0.1").toString()); // 0.1
System.out.println(new BigDecimal(
Double.toString(0.1000000000000000055511151231257827021181583404541015625)).toString());// 0.1
System.out.println(new BigDecimal(Double.toString(0.1)).toString()); // 0.1

 

分析一下上面代码的问题(注释的内容表示此语句的输出)

第一行:事实上,由于二进制无法精确地表示十进制小数0.1,但是编译器读到字符串"0.1"之后,必须把它转成8个字节的double值,因此,编译器只能用一个最接近的值来代替0.1了,即0.1000000000000000055511151231257827021181583404541015625。因此,在运行时,传给BigDecimal构造函数的真正的数值是0.1000000000000000055511151231257827021181583404541015625。
第二行:BigDecimal能够正确地把字符串转化成真正精确的浮点数。
第三行:问题在于Double.toString会使用一定的精度来四舍五入double,然后再输出。会。Double.toString(0.1000000000000000055511151231257827021181583404541015625)输出的事实上是"0.1",因此生成的BigDecimal表示的数也是0.1。
第四行:基于前面的分析,事实上这一行代码等价于第三行
 
结论:
1.如果你希望BigDecimal能够精确地表示你希望的数值,那么一定要使用字符串来表示小数,并传递给BigDecimal的构造函数
2.如果你使用Double.toString来把double转化字符串,然后调用BigDecimal(String),这个也是不靠谱的,它不一定按你的想法工作。
3.如果你不是很在乎是否完全精确地表示,并且使用了BigDecimal(double),那么要注意double本身的特例,double的规范本身定义了几个特殊的double值(Infinite,-Infinite,NaN),不要把这些值传给BigDecimal,否则会抛出异常。
 
问题二:把double强制转化成int,难道不是扔掉小数部分吗?
int x=(int)1023.99999999999999; // x=1024为什么?

原因还是在于二进制无法精确地表示某些十进制小数,因此1023.99999999999999在编译之后的double值变成了1024。

所以, 把double强制转化成int确实是扔掉小数部分,但是你写在代码中的值,并不一定是编译器生成的真正的double值。
验证代码:
double d = 1023.99999999999999;
int x = (int) d;
System.out.println(new BigDecimal(d).toString()); // 1024
System.out.println(Long.toHexString(Double.doubleToRawLongBits(d))); // 4090000000000000
System.out.println(x); // 1024

前面提过BigDecimal可以精确地把double表示出来还记得吧。

我们也可以直接打印出d的二进制形式,根据IEEE 754的规定,我们可以算出0x4090000000000000=(1024)。
 
 

double与BigDecimal使用姿势

总在项目中看到Double与BigDecimal被用错的情况,竟然有人告诉我:“一律使用BigDecimal,避免后患”,我相信这位兄弟肯定是被精度问题搞蒙了,因此我想同步一下我的使用姿势,仅提供参考。

两种类型使用过程中都有可能出坑

1.double-计算时容易出现不精确的问题

double的小数部分容易出现使用二进制无法准确表示

如十进制的0.1,0.2,0.3,0.4 都不能准确表示成二进制;

可参考:

 

2.dobule的=比较要注意

 

3.BigDecimal-除法除不尽会出现异常:ArithmeticException

 

4.new BigDecimal(double)-也许不是你想要的

一般情况下都不使用new BigDecimal(double) 应该使用BigDecimal.valueOf(double)

  BigDecimal d1 = BigDecimal.valueOf(12.3)//结果是12.3 你预期的
 BigDecimal d2 = new BigDecimal(12.3) //结果是12.300000000000000710542735760100185871124267578125

我想12.300000000000000710542735760100185871124267578125肯定不是你想要的结果,因此 new BigDecimal(double)可能会产生不是你预期的结果,原理可以自行看一下底层源代码,还是比较容易的;

另:BigDecimal.valueOf(xxx ) 是静态工厂类,永远优先于构造函数(摘自<<Effecitve java>>,此书也是非常推荐的一本经典书)

 

5.BigDecimal-是不可变对象

如原来d1=1.11 ,又加了一个数2.11,这种操作要注意结果要指向新对象;

任何针对BigDecimal对象的修改都会产生一个新对象;

BigDecimal newValue = BigDecimal.valueOf(1.2222).add(BigDecimal.valueOf(2.33333));

BigDecimal newValue = BigDecimal.valueOf(1.2222).setScale(2);

总之每次修改都要重新指向新对象,才能保证计算结果是对的。

 

6.BigDecimal比较大小操作不方便,毕竟是对象操作

比较大小和相等都使用compareTo,如果需要返回大数或小数可使用max,min。且注意不能使用equals

三、效率比较

比如:1累加到1000000(以本人机器 MacBookPro 2018 i7 2.2G)double比BigDecimal快大约10倍

double: 2ms

BigDecimal:16ms

四、优缺点总结

double的优缺点:

1.double在计算过程中容易出现丢失精度问题

2.使用方便,有包装类,可自动拆装箱,计算效率高

BigDecimal:

1.精度准确,但做除法时要注意除不尽的异常

2.BigDecimal是对象类型,也没有自动拆封箱机制,操作起来总是有些不顺手

五、使用场景推荐

涉及到精准计算如金额,一定要使用BigDecimal或转成long或int计算

若不需要精准的,如一些统计值:(本身就没有精确值)

用户平均价格,店铺评分,用户经纬度等本身就没有精准值一说的推荐使用double或float,

写代码更方便,计算效率也高得多;

值得一提的说,如果double或float仅是用于传值,并不会有精度问题,但如果参与了计算就要小心了,要区分是不是需要精准值,如果需要精准值,需要转成BigDecimal计算以后再转成double;

但依然约定在DTO定义金额时使用BigDecimal或整形值,是为了减少或避免double参与金额计算的机会,避免出bug;

其他1:代码中看到碰到让我觉得有问题的地方

以下代码在不同的类中抓出来的觉得用得不太恰当的地方:

代码中真的不需要那么多地方使用BigDecimal,相反用到BigDecimal的地方并不多,反而用Double的地方更多。以上代码我希望的方式是:

提醒:DTO中尽量使用包装类,防止反系列化时null的造成的格式转换异常

分析:

经纬度:一般业务代码中也不太会去计算,仅用于传给地图api等,经纬度一般用于计算距离,如果保留到6位小数时其实已经是1米级别的了,也满足绝大多数场景了,因此使用Double是确实是可行的;

店铺平均消费:本身就是一个归纳统计值,也一般用来比较大小做参考,因此也用不着BigDecimal;

当前价格:这个不一样了,为了减少double参与金钱计算,统一使用BigDecimal代替带有小数的金额;

其他2:关于Mysql中如何选用这两种类型

1.首先与java不同的是mysql是用来持久化数据的,而java中使用的数据一般更多的是过一下内存;

2.数据库都要除了指定数据类型指外还需要指定精度,因此在DB中Double计算时精度的丢失比Java高得多;

因为Java默认精确到15-16位了;

3.更改数据类型的成本,Mysql比Java代码要难得多;

考虑到以上与java中不同几点,做点个人使用总结:

1.与商业金融相关字段要使用Decimal来表示,如金额,费率等字段;

2.参与各类计算如加,减,乘,除,sum,avg等等,也要使用Decimal;

3.经纬度,可以使用double来表示,这个可参考Java,只要保证精度范围即可;

4.如果确实不确定使用什么double或Decimal哪种类型合适,那最好使用Decimal,毕竟稳定,安全高于一切;

注:阿里的编码规范中强调统一带小数的类型一律使用Decimal类型,也是有道理的,使用Decimal可以大大减少计算踩坑的概率

 

Java基本数据类型和引用类型


作者:清浅池塘
链接:https://juejin.cn/post/6844903501256998926

下面是测试的main方法:

先看第一句代码:

方法体里声明的基本数据类型在栈内存里,我们画一下

继续执行以下代码

对于基本数据类型来说赋值(=号)就相当于拷贝了一份值,把int1的值100,拷贝给int2,继续画图

int1=500,直接修改int1的值为500,表现如下图

分别打印int1,int2的值,相信没有人会答错,分别是500,100。

再来看数组的初始化

先初始化arr1,当执行到new这个关键字,会在堆内存分配内存空间,并把该内存空间的地址赋值给arr1。

继续执行以下代码

这儿arr2初始化时并没有new关键字,并不会在堆内存里新开辟一块空间,而是把arr1里存的堆内存地址直接赋值给了arr2,对于引用类型来说赋值(=号)就相当于拷贝了一份内存地址,也就是说arr1,arr2现在指向了同一块堆内存,表现形势如下图

这时候执行如下代码

虽然只是修改arr1数组下标位置为3的值

但由于数组arr1和数组arr2指向同一块堆内存,打印arr1[3]和arr2[3]的值,都是8。你答对了吗?

再来看对象的初始化

当看到这个new,这货肯定在堆内存里开辟了一块内存空间,Person里有一个叫name的String对象,String这个对象有点特殊,虽然没有new这个关键字,但还是在堆内存中开辟了一块空间,在String是一个很普通的类一文中已经讲解过了,这里就不再细讲了,String底层是数组实现的,数组也是引用类型,age为基本数据类型,表现如下图

上图中大框里的内容就是整个Person对象在堆内存中的体现,继续执行以下代码

没有new关键字,per2不会在堆内存中新开辟空间,和数组一样,也是把per1的内存地址直接赋值给了per2

当我们修改per1的属性的时候

如下图两个红框里的内容,给对象(数组也是对象)赋值其实就是相当于引用重新指向一块堆内存,基本数据类型是直接修改值,表现如下图

所以,不管打印per1还是per2的name、age,打印出来的结果都是“李四”、35,这个你也答对了吗?最后,我们来验证一下,结果是不是和文中说的一致。

结果完全一致,回过头来看看Java里的“==”比较符,结果就不难理解了,代码如下

结果分别是false,true,true,当==两边是基本数据类型时,==于比较的是两边的两个值是否相等,当==两边是引用类型时比较的是两个内存地址,也可以看成是看这两个引用是否指向堆内存里的同一块地址,如下图

新手在学习Java时,在引用类型上可能经常容易犯错误,如本文中所讲的arr1,arr2,很多人在写代码的时候是想拷贝一份值来用,却不知道在修改arr1的时候,arr2的值也变了。

本篇内容就讲解到这儿,画图不易,希望大家以后多想想变量在内存中的样子,学习起来可以事半功倍。四类八种基本数据类型,本文只列举了int类型,其它的7中基本类型和int的表现形式一致,这里就不一一举例了。

 

最后,说一下前面的文章留的文末思考,首先是让人疑惑的Java代码一文中的文末思考

很简单是false

两个对象都是新new出来的,开辟了两块内存空间,i7和i8的引用不是指向堆内存里的同一块地址,因此打印出来是false。

 

深入剖析Java中的装箱和拆箱


出处:http://www.cnblogs.com/dolphin0520/

  自动装箱和拆箱问题是Java中一个老生常谈的问题了,今天我们就来一些看一下装箱和拆箱中的若干问题。本文先讲述装箱和拆箱最基本的东西,再来看一下面试笔试中经常遇到的与装箱、拆箱相关的问题。

  以下是本文的目录大纲:

  一.什么是装箱?什么是拆箱?

  二.装箱和拆箱是如何实现的

  三.面试中相关的问题

  若有不正之处,请谅解和批评指正,不胜感激。

  请尊重作者劳动成果,转载请标明原文链接:

   http://www.cnblogs.com/dolphin0520/p/3780005.html

一.什么是装箱?什么是拆箱?

  在前面的文章中提到,Java为每种基本数据类型都提供了对应的包装器类型,至于为什么会为每种基本数据类型提供包装器类型在此不进行阐述,有兴趣的朋友可以查阅相关资料。在Java SE5之前,如果要生成一个数值为10的Integer对象,必须这样进行:

1
Integer i = new Integer(10);

  而在从Java SE5开始就提供了自动装箱的特性,如果要生成一个数值为10的Integer对象,只需要这样就可以了:

1
Integer i = 10;

  这个过程中会自动根据数值创建对应的 Integer对象,这就是装箱。

  那什么是拆箱呢?顾名思义,跟装箱对应,就是自动将包装器类型转换为基本数据类型:

1
2
Integer i = 10//装箱
int n = i;   //拆箱

  简单一点说,装箱就是  自动将基本数据类型转换为包装器类型;拆箱就是  自动将包装器类型转换为基本数据类型。

  下表是基本数据类型对应的包装器类型:

int(4字节) Integer
byte(1字节) Byte
short(2字节) Short
long(8字节) Long
float(4字节) Float
double(8字节) Double
char(2字节) Character
boolean(未定) Boolean

二.装箱和拆箱是如何实现的

  上一小节了解装箱的基本概念之后,这一小节来了解一下装箱和拆箱是如何实现的。

  我们就以Interger类为例,下面看一段代码:

1
2
3
4
5
6
7
public class Main {
    public static void main(String[] args) {
         
        Integer i = 10;
        int n = i;
    }
}

  反编译class文件之后得到如下内容:

  

  从反编译得到的字节码内容可以看出,在装箱的时候自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法。

  其他的也类似,比如Double、Character,不相信的朋友可以自己手动尝试一下。

  因此可以用一句话总结装箱和拆箱的实现过程:

  装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。(xxx代表对应的基本数据类型)。

三.面试中相关的问题

  虽然大多数人对装箱和拆箱的概念都清楚,但是在面试和笔试中遇到了与装箱和拆箱的问题却不一定会答得上来。下面列举一些常见的与装箱/拆箱有关的面试题。

1.下面这段代码的输出结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
    public static void main(String[] args) {
         
        Integer i1 = 100;
        Integer i2 = 100;
        Integer i3 = 200;
        Integer i4 = 200;
         
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

  也许有些朋友会说都会输出false,或者也有朋友会说都会输出true。但是事实上输出结果是:

true
false

   为什么会出现这样的结果?输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现:

public static Integer valueOf(int i) {
        if(i >= -128 && i <= IntegerCache.high)
            return IntegerCache.cache[i + 128];
        else
            return new Integer(i);
    }

  而其中IntegerCache类的实现为:

复制代码
 private static class IntegerCache {
        static final int high;
        static final Integer cache[];

        static {
            final int low = -128;

            // high value may be configured by property
            int h = 127;
            if (integerCacheHighPropValue != null) {
                // Use Long.decode here to avoid invoking methods that
                // require Integer's autoboxing cache to be initialized
                int i = Long.decode(integerCacheHighPropValue).intValue();
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - -low);
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
        }

        private IntegerCache() {}
    }
复制代码

  从这2段代码可以看出,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。

  上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象。

2.下面这段代码的输出结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
    public static void main(String[] args) {
         
        Double i1 = 100.0;
        Double i2 = 100.0;
        Double i3 = 200.0;
        Double i4 = 200.0;
         
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

  也许有的朋友会认为跟上面一道题目的输出结果相同,但是事实上却不是。实际输出结果为:

false
false

  至于具体为什么,读者可以去查看Double类的valueOf的实现。

  在这里只解释一下为什么Double类的valueOf方法会采用与Integer类的valueOf方法不同的实现。很简单:在某个范围内的整型数值的个数是有限的,而浮点数却不是。

  注意,Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的。

     Double、Float的valueOf方法的实现是类似的。

3.下面这段代码输出结果是什么:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
    public static void main(String[] args) {
         
        Boolean i1 = false;
        Boolean i2 = false;
        Boolean i3 = true;
        Boolean i4 = true;
         
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

  输出结果是:

true
true

  至于为什么是这个结果,同样地,看了Boolean类的源码也会一目了然。下面是Boolean的valueOf方法的具体实现:

public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

  而其中的 TRUE 和FALSE又是什么呢?在Boolean中定义了2个静态成员属性:

复制代码
 public static final Boolean TRUE = new Boolean(true);

    /** 
     * The <code>Boolean</code> object corresponding to the primitive 
     * value <code>false</code>. 
     */
    public static final Boolean FALSE = new Boolean(false);
复制代码

  至此,大家应该明白了为何上面输出的结果都是true了。

4.谈谈Integer i = new Integer(xxx)和Integer i =xxx;这两种方式的区别。

  当然,这个题目属于比较宽泛类型的。但是要点一定要答上,我总结一下主要有以下这两点区别:

  1)第一种方式不会触发自动装箱的过程;而第二种方式会触发;

  2)在执行效率和资源占用上的区别。第二种方式的执行效率和资源占用在一般性情况下要优于第一种情况(注意这并不是绝对的)。

5.下面程序的输出结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
    public static void main(String[] args) {
         
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        Long h = 2L;
         
        System.out.println(c==d);
        System.out.println(e==f);
        System.out.println(c==(a+b));
        System.out.println(c.equals(a+b));
        System.out.println(g==(a+b));
        System.out.println(g.equals(a+b));
        System.out.println(g.equals(a+h));
    }
}

  先别看输出结果,读者自己想一下这段代码的输出结果是什么。这里面需要注意的是:当 "=="运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。另外,对于包装器类型,equals方法并不会进行类型转换。明白了这2点之后,上面的输出结果便一目了然:

复制代码
true
false
true
true
true
false
true
复制代码

  第一个和第二个输出结果没有什么疑问。第三句由于  a+b包含了算术运算,因此会触发自动拆箱过程(会调用intValue方法),因此它们比较的是数值是否相等。而对于c.equals(a+b)会先触发自动拆箱过程,再触发自动装箱过程,也就是说a+b,会先各自调用intValue方法,得到了加法运算后的数值之后,便调用Integer.valueOf方法,再进行equals比较。同理对于后面的也是这样,不过要注意倒数第二个和最后一个输出的结果(如果数值是int类型的,装箱过程调用的是Integer.valueOf;如果是long类型的,装箱调用的Long.valueOf方法)。