Java|从Integer和int的区别认识包装类

Java|Integer和int

  • 概述
  • 包装类
  • 自动拆箱/自动装箱
  • Integer 设计要点

极客时间的课程中,看到一篇讲Integer和int的区别,其中提到了一些没有想过的设计思路(比如Integer的值缓存),扩展了一些知识。文章不是很详细,编排有点混乱,并且很多地方只是提到了,但没有展开来说,导致我读来觉得好像很棒,但实际还是不知其所以然。所以本文根据该课程,整理课程中提到的关键概念,有这样几个疑问待本文解决:

  • Integer和int的区别,为什么有了int还需要Integer?或者说可不可以只要Integer不要int?
  • 什么叫包装类,它是怎样包装基本类型的?
  • Integer自动装箱/自动拆箱的原理?以及其发生的阶段?带来的好处/引入的弊端?什么情况自动装箱/自动拆箱?
  • Integer的值缓存是什么?
  • Integer源码的设计要点
  • 基本类型的类型转换(暂时不写)

概述

1.Java的类型系统

1.1 Java的类型系统由两部分组成:基本类型和引用类型(非基本类型)。

  • 基本类型:基本类型就是那些最常用的类型,例如:boolean/char/byte/short/int/long/float/double,这些类型有个特点,就是变量直接存储值。举个栗子:int num=100;,变量num中存储的值就是100。

  • 非基本类型:除了基本类型之外的都是非基本类型。非基本类型有个显著特点就是初始化的时候一般需要使用new来创建一个对象,变量存储的是指向对象的引用

非基本类型跟基本类型的本质区别是非基本类型变量存储的不是值,而是引用。

1.2 基本类型和非基本类型的内存存储位置:

  • 当变量声明在方法中时(局部变量),如果是基本类型,变量名和值都存放在栈上;如果是非基本类型,变量名在栈上(存放的是指向对象的地址),变量所指向的地址在堆上。
  • 当变量声明在类中时(全局变量),如果是基本类型,变量名和值都放在堆内存中;如果是非基本类型,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中。

2. int 和 Integer 是什么?

  • int 是我们常说的整形数字,是 Java 的 8 个基本数据类型(boolean、byte 、short、char、int、float、double、long)之一。Java语言虽然号称一切都是对象,但基本数据类型是例外。
  • Integer是int对应的包装类,它有一个int 类型的字段存储据,并且提供了基本操作,比如数学运算、int和字符串之间的转换等。

每个基本类型都有一个对应的引用类型,称为装箱基本类型。在Java 5 中,引入了自动装箱和自动拆箱功能,Java可以根据上下文,自动进行转换,极大地简化了相关编程。但是自动装箱/自动拆箱模糊了但并没有完全抹去基本类型和装箱基本类型之间的区别,这两种类型是有真正差别的

3. int 和 Integer 的区别

  • Integer是int的包装类,int则是java的一种基本数据类型
  • Integer变量必须实例化后才能使用,而int变量不需要
  • Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
  • Integer的默认值是null,int的默认值是0

如下代码段,i 中存放的就是10,而 in 中存放的是 new 出来的对象在堆中的地址。

int i = 10;
Integer in = new Integer(10);

4. 理一理思路

上面写了一些int和Integer的概念,但好像对理解这个问题也并没有什么直接的影响,总感觉不痛不痒,说的东西大家都知道,但没有进一步思考,int 和 Integer 的具体使用场合。下面理一理思路:

首先我们知道在编写程序的过程中一定会存在大量的计算操作,那么计算操作直接用基本类型就好了啊。从两个方面考虑:

  • 内存占用角度:包装类是对象,除了基本数据以外,还需要有对象头。所以包装类占的空间比基本类型大。
  • 处理速度角度:基本类型在内存中存的是值,找到基本类型的内存位置就可以获得值;而包装类型存的是对象的引用,找到指定的包装类型后,还要根据引用找到具体对象的内存位置。会产生更多的IO,计算性能比基本类型差。

所以如果需要做计算,就使用基本类型。

“原则上,建议避免无意中的装箱、拆箱行为,尤其是在性能敏感的场合,创建爱你10W个Java对象的10W个整数的开销可不是一个数量级的,不管是内存使用还是处理速度,光是对象头的空间占用就已经是数量级的差距了。“

既然如此,那为什么要有包装类呢?

  • object具备generic的能力,更抽象,解决业务问题编程效率高。
  • 原始数据类型和 Java 泛型并不能配合使用。Java的泛型某种程度上可以算作违反性,它完全是一种编译期的技巧,Java编译期会自动将类型转换为对应的特定类型,这就决定了使用泛型,必须保证相应类型可以转换为Object。

(泛型后面专门写)

于是JAVA设计者的初衷估计是这样的:如果开发者要做计算,就应该使用primitive value如果开发者要处理业务问题,就应该使用object,采用Generic机制;反正JAVA有auto-boxing/unboxing机制,对开发者来讲也不需要注意什么。然后为了弥补object计算能力的不足,还设计了static valueOf()方法提供缓存机制,算是一个弥补。

包装类

基本了解和概念参考这篇:https://blog.csdn.net/weixin_40739833/article/details/80093527

自动拆箱/自动装箱

语法糖。后面再写。

下面整理一下《Effective Java》第49条的内容:基本类型优先于装箱基本类型

基本类型和装箱基本类型有三个主要区别:

  • 基本类型只有值,装箱基本类型则具有与他们的值不同的同一性。两个装箱基本类型可以具有相同的值和不同的同一性
  • 基本类型只有功能完备的值,而每个装箱基本类型除了对应的基本类型的功能值之外,还有个非功能值null。
  • 基本类型通常比装箱基本类型更节省时间和空间。
Comparator<Integer> naturalOrder = new Comparator<Integer>() {
	public int compare(Integer first, Integer second) {
		return first < second ? -1: (first == second ? 0: 1);
	}
};

对上面的比较器如果这样使用:

	System.out.println(naturalOrder.compare(new Integer(42),new Integer(42)));

则会输出1,标明第一个Integer的值大于第二个。

Integer 设计要点

1. 对Integer类的总览

Integer 主要提供方法:

  • 转换为不同进制的字符串String toXXXString(int)
  • 各种进制字符串转int int parseInt(String, int)
  • Integer.valueOf()
  • 基本类型转换的方法(继承自Number)
  • hashCode(int)仅仅返回value值,说明值相同hashCode()相同
  • equals(Object obj)比较vlaue是否相等
  • Integer.getInteger(String)
  • Integer decode(String nm)
  • 比较的方法
  • max sum min

大多数都是静态方法。

Integer满足Java语言中不可变类型的要求:

  • 如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。
  • 如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行。保证对象行为不影响自己状态的途径有很多种,最简单的就是把对象中带有状态的变量都声明为final,这样构造函数结束后,它就是不可变的。

2. 变量
首先可以看到,Integer类型里,所有的变量都被final修饰。并且除了value是成员变量,其余都是类变量(static修饰)。
Java|从Integer和int的区别认识包装类_第1张图片
value 是一个 int 类型的变量,也就是 Integer 所包装的值。
该变量被 private final 修饰,即无法被访问且经过构造函数赋值后无法被改变。

// Integer 的值
private final int value;

3. 构造方法

Integer对象有两种构造方式:

  • 一种是直接new一个Integer对象出来,如Integer i = new Integer(10);
  • 一种是利用自动装箱,如Integer j = 10;
// 构造函数
public Integer(int value) {
	this.value = value;
}
// 构造函数
public Integer(String s) throws NumberFormatException {
	this.value = parseInt(s, 10);
}

使用Integer.valueOf()函数进行自动装箱。这个函数的逻辑如下:

  1. 如果 i 的值在 IntegerCache.low ~ IntegerCache.high,则直接返回 IntegerCache.cache[i + (-IntegerCache.low)]。(注意没有新构建对象哦,而是返回的一个数组中的元素。)
  2. 如果 i 的值不在以上范围内,则用 i 构造一个 Integer 对象。
// 返回表示指定的{@code int}值的{@code Integer}实例。
// 如果不需要新的{@code Integer}实例,通常应该优先使用此方法,而不是构造函数{@link #Integer(int)}。
// 因为此方法可能通过频繁缓存产生明显更好的空间和时间性能 要求的价值。
// 此方法将始终缓存-128到127(包括端点)范围内的值,并可以缓存此范围之外的其他值。
public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

4. 缓存设计
其中 IntegerCache 是 Integer 类的一个私有静态内部类,其中关键部分是static final Integer cache[];,即它内部保存了Integer类型的数组,用以缓存值在 IntegerCache.low ~ IntegerCache.high 之间的Integer对象。

为什么设计这个缓存?

实践发现,大部分数据操作都是集中在有限的、较小的数值范围。所以在Java 5 中增加了静态工厂方法 valueOf(),在调用它的时候利用缓存机制,这样不用反复new 值相同的Integer对象,减少了内存占用。

个人认为可以用缓存的原因在于,Integer 的设计使得其内部包装的 value 在赋值之后是不可被改变的,所以只要 value 相同,这个Integer对象的状态就是相同的,没必要构建多个具有相同 value 的 Integer 对象。

// 根据JLS的要求,缓存以支持-128和127(包括)之间的值的自动装箱。
// 缓存在首次使用时初始化。
// 缓存的大小可以由{@code -XX:AutoBoxCacheMax = }选项控制。
// 在VM初始化期间,可以设置java.lang.Integer.IntegerCache.high属性并将其保存在私有系统属性sun.misc.VM类。
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() {}
}

除了 Integer 有缓存,其他包装类也有:
Java|从Integer和int的区别认识包装类_第2张图片
5. 继承关系
Integer 类继承了 Number 类,实现了 Comparable 接口。

public final class Integer extends Number implements Comparable<Integer> 

Number 是一个抽象类,主要表示基本类型之间的转换。

public abstract class Number implements java.io.Serializable {

    public abstract int intValue();

    public abstract long longValue();

    public abstract float floatValue();

    public abstract double doubleValue();

    public byte byteValue() {
        return (byte)intValue();
    }

    public short shortValue() {
        return (short)intValue();
    }
    
    private static final long serialVersionUID = -8742448824652078965L;
}

Integer 类中对 Nubmer 类方法的实现,涉及类型转换。
记得《深入理解JVM》里有相关内容,稍后再整理 。

    public byte byteValue() {
        return (byte)value;
    }
    public short shortValue() {
        return (short)value;
    }
    public int intValue() {
        return value;
    }
    public long longValue() {
        return (long)value;
    }
    public float floatValue() {
        return (float)value;
    }
    public double doubleValue() {
        return (double)value;
    }

其中自动拆箱被替换成intValue()方法。

6. equals(Object)方法

  • object 是否是 Integer 的实例
  • 如果是则比较其 value 是否相等
public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
}

7. int和Integer的比较

这部分内容全部来自:https://www.cnblogs.com/guodongdidi/p/6953217.html

1、Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。

Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false

2、Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)

Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true

3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)

Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false

4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false。

Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false

对于第4条的原因:
java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);而java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了。

补一个《深入理解JVM》里的例子:

		Integer a = 1;
		Integer b = 2;
		Integer c = 3;
		Integer d = 3;
		Integer e = 321;
		Integer f = 321;
		Long g = 3L;
		
		System.out.println(c==d);			// true
		System.out.println(e==f);			// false
		System.out.println(c==(a+b));		// true
		System.out.println(c.equals(a+b));  // true
		System.out.println(g==(a+b));       // true
		System.out.println(g.equals(a+b));	// false
		

简单解释一下:
e == f:比较Integer对象(即指向的地址),自动装箱时值超过默认缓存范围,new出来不一样。所以为false。
c == (a+b):a+b 先用基本类型做运算,按照上面第2条,c 自动拆箱了,两边比较值的大小是否相等。相等则true。
c.equals(a+b):a+b 先用基本类型做运算,然后自动装箱,由于值在缓存范围,所以都指向缓存。所以为true。
g==(a+b):比较的是值吧。
g.equals(a+b)have no idea

参考:
Java的基本类型存在哪里:https://blog.csdn.net/guchenjun789/article/details/82903775
https://blog.csdn.net/yang_154116/article/details/81227073
《深入理解JVM》Java语言中的线程安全
《Effective Java》
int和Integer的比较:https://www.cnblogs.com/guodongdidi/p/6953217.html

你可能感兴趣的:(Java)