为什么人们在 Java 中仍然使用原始类型?

从 Java 5开始,我们已经对原语类型进行装箱/解箱,以便将 int包装为 java.lang.Integer,以此类推。

我最近看到很多新的 Java 项目(当然需要至少版本5的 JRE,如果不是版本6的话)使用 int而不是 java.lang.Integer,尽管使用后者更方便,因为它有一些转换为 long值的助手方法等等。

为什么有些 还是在 Java 中使用原语类型? 有什么实际的好处吗?

85140 次浏览

盒装类型的性能较差,需要更多内存。

首先也是最重要的,习惯。如果您已经用 Java 编写了八年的代码,那么您积累了相当大的惰性。如果没有令人信服的理由,为什么要改变呢?使用盒装原语并不会带来任何额外的好处。

另一个原因是断言 null不是一个有效的选项。将两个数字或一个循环变量的和声明为 Integer是毫无意义和误导的。

它还有性能方面的问题,虽然性能差异在很多情况下并不重要(尽管当性能差异很严重时) ,但没有人喜欢编写可以像我们已经习惯的那样容易地以更快的方式编写的代码。

对象比基元类型更重量级,因此基元类型比包装类的实例更高效。

基本类型非常简单: 例如,int 是32位的,占用的内存恰好是32位,可以直接操作。Integer 对象是一个完整的对象,它(像任何对象一样)必须存储在堆中,并且只能通过对它的引用(指针)访问。它很可能还占用超过32位(4字节)的内存。

也就是说,Java 具有原语和非原语类型之间的区别这一事实也是 Java 编程语言时代的标志。较新的编程语言没有这种区别; 这种语言的编译器足够聪明,能够自己判断您使用的是简单值还是更复杂的对象。

例如,在 Scala 中没有原语类型; 有一个用于整数的 Int 类,而 Int 是一个实际的对象(可以对其进行方法等)。当编译器编译您的代码时,它在幕后使用原语 Int,因此使用 Int 与在 Java 中使用原语 Int 一样有效。

原始类型:

int x = 1000;
int y = 1000;

现在评估:

x == y

这是 true。一点也不奇怪。现在试试盒装类型:

Integer x = 1000;
Integer y = 1000;

现在评估:

x == y

它是 false。可能。取决于运行时。这个理由够吗?

在 Joshua Bloch 的 有效的 Java第5项: “避免创建不必要的对象”中,他提供了以下代码示例:

public static void main(String[] args) {
Long sum = 0L; // uses Long, not long
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}

它需要43秒才能跑完。把 Long 引入到原语中可以使它缩短到6.8秒... ... 如果这就是我们使用原语的原因的话。

缺乏本机值相等性也是一个问题(与 ==相比,.equals()相当冗长)

对于 biziclop 来说:

class Biziclop {


public static void main(String[] args) {
System.out.println(new Integer(5) == new Integer(5));
System.out.println(new Integer(500) == new Integer(500));


System.out.println(Integer.valueOf(5) == Integer.valueOf(5));
System.out.println(Integer.valueOf(500) == Integer.valueOf(500));
}
}

结果:

false
false
true
false

为什么(3)返回 true而(4)返回 false

因为它们是两个不同的物体。最接近零的256个整数[-128; 127]被 JVM 缓存,因此它们返回相同的对象。但是,在这个范围之外,它们没有被缓存,因此创建了一个新对象。为了使事情更加复杂,JLS 要求缓存 至少256轻量级。如果 JVM 实现者愿意,他们可以添加更多,这意味着这可以运行在一个缓存了最接近的1024的系统上,并且所有这些都返回 true... # 尴尬

基本类型的 很多速度更快:

int i;
i++;

整数(所有数字,也是一个字符串)是一个 永恒不变类型: 一旦创建它不能被更改。如果 i是 Integer,那么 i++将创建一个新的 Integer 对象——这在内存和处理器方面要昂贵得多。

除了其他人所说的,原始本地变量不是从堆中分配的,而是在堆栈中分配的。但是对象是从堆中分配的,因此必须进行垃圾回收。

你真的能想象

  for (int i=0; i<10000; i++) {
do something
}

使用 java.lang 循环。用整数代替?爪哇狼。Integer 是不可变的,因此循环中的每次增量都会在堆上创建一个新的 java 对象,而不仅仅是使用单个 JVM 指令在堆栈上增量 int。表演一定会很精彩。

我真的不认为使用 java.lang 在模式上很方便。整型比 int 型。恰恰相反。自动装箱意味着您可以在不使用 Integer 的情况下使用 int,而 Java 编译器负责插入代码来为您创建新的 Integer 对象。自动装箱就是允许您在需要 Integer 的地方使用 int,同时编译器插入相关的对象构造。它从一开始就没有移除或减少对 int 的需要。通过自动装箱,你可以得到两个世界的最好结果。当你需要一个基于堆的 java 对象时,你会自动创建一个 Integer,当你只是做算术和本地计算时,你会得到一个 int 的速度和效率。

我们很难知道在这些掩盖下究竟发生了什么样的优化。

对于本地使用,当编译器有足够的信息进行优化时,排除空值的可能性,我希望性能是相同的或类似的

然而,原语数组显然是来自装箱原语集合的 非常不同。这是有意义的,因为很少有优化可以深入到集合中。

此外,与 int相比,Integer逻辑开销要高得多: 现在您必须考虑 int a = b + c;是否抛出异常。

我将尽可能多地使用原语,并依赖于工厂方法和自动装箱,以便在需要时提供语义上更强大的装箱类型。

自动装箱会导致难以发现 NPE

Integer in = null;
...
...
int i = in; // NPE at runtime

在大多数情况下,对 in的 null 赋值不如上面那么明显。

顺便说一下,Smalltalk 只有对象(没有原语) ,但是他们已经优化了他们的小整数(使用的不是全部32位,只有27或其他) ,不分配任何堆空间,而只是使用一个特殊的位模式。其他常见对象(true、 false、 null)在这里也有特殊的位模式。

因此,至少在64位的 JVM (带有64位指针名称空间)上,应该可以根本不存在任何 Integer、 Property、 Byte、 Short、 Boolean、 Float (和 small Long)对象(除了这些由显式 new ...()创建的对象之外) ,只存在特殊的位模式,这些对象可以被普通操作符有效地操作。

我不敢相信没有人提到我认为最重要的原因: “ int”比“ Integer”容易输入多了。我认为人们低估了简洁语法的重要性。性能并不是避免它们的真正原因,因为大多数情况下使用数字的时候都是在循环索引中,而在任何非平凡的循环中(无论是使用 int 还是 Integer) ,递增和比较这些数字都不会带来任何成本。

另一个给定的原因是您可以获得 NPE,但是使用装箱类型非常容易避免(并且只要您总是将它们初始化为非空值,就可以保证避免这种情况)。

另一个原因是(new Long (1000)) = = (new Long (1000))是错误的,但这只是另一种说法。“ equals”不支持盒装类型(与运算符 < 、 > 、 = 等不同) ,所以我们回到“更简单的语法”的原因。

我认为 Steve Yegge 的非原始循环例子很好地说明了我的观点: Http://sites.google.com/site/steveyegge2/language-trickery-and-ejb

想想看: 在语法良好的语言中(比如任何函数式语言,python,ruby,甚至 C)使用函数类型的频率,与使用 Runnable、 Callable 和无名类等接口模拟函数类型的 Java 相比有多高。

除了性能和内存问题之外,我还想提出另一个问题: 如果没有 intList接口就会崩溃。
问题在于重载的 remove()方法(remove(int)remove(Object))。remove(Integer)总是解析为调用后者,因此不能通过索引删除元素。

另一方面,在尝试添加和删除 int时存在一个陷阱:

final int i = 42;
final List<Integer> list = new ArrayList<Integer>();
list.add(i); // add(Object)
list.remove(i); // remove(int) - Ouch!

原始类型有许多优点:

  • 编写更简单的代码
  • 由于不为变量实例化对象,因此性能更好
  • 因为它们不表示对对象的引用,所以不需要检查 null
  • 除非需要利用装箱特性,否则应使用基元类型。

不去掉原语的几个原因:

  • 向后兼容性。

如果它被消除了,任何旧的程序都不会运行。

  • JVM 重写。

必须重写整个 JVM 以支持这个新东西。

  • 更大的内存占用。

您需要存储值和引用,这将使用更多的内存。如果你有一个巨大的字节数组,使用 byte比使用 Byte要小得多。

  • 空指针问题。

声明 int i然后使用 i进行操作将不会产生任何问题,但声明 Integer i然后进行同样的操作将导致 NPE。

  • 平等问题。

考虑下面的代码:

Integer i1 = 5;
Integer i2 = 5;


i1 == i2; // Currently would be false.

操作符将不得不被重载,这将导致对内容的重大重写。

  • 慢点

对象包装器明显慢于它们的原始对应物。

int loops = 100000000;


long start = System.currentTimeMillis();
for (Long l = new Long(0); l<loops;l++) {
//System.out.println("Long: "+l);
}
System.out.println("Milliseconds taken to loop '"+loops+"' times around Long: "+ (System.currentTimeMillis()- start));


start = System.currentTimeMillis();
for (long l = 0; l<loops;l++) {
//System.out.println("long: "+l);
}
System.out.println("Milliseconds taken to loop '"+loops+"' times around long: "+ (System.currentTimeMillis()- start));

在 Long: 468上循环’100000000’所花费的毫秒

在“1000000000”周期内循环所需的毫秒长度: 31

顺便说一句,我不介意看到这样的东西进入 Java。

Integer loop1 = new Integer(0);
for (loop1.lessThan(1000)) {
...
}

其中 for 循环自动将 loop 1从0递增到1000 或者

Integer loop1 = new Integer(1000);
for (loop1.greaterThan(0)) {
...
}

其中 for 循环自动将 loop 11000递减为0。

因为 JAVA 以原语类型执行所有数学运算:

public static int sumEven(List<Integer> li) {
int sum = 0;
for (Integer i: li)
if (i % 2 == 0)
sum += i;
return sum;
}

在这里,提醒和一元加操作不能应用于 Integer (Reference)类型,编译器执行拆箱操作并执行操作。

因此,确保有多少自动装箱和拆箱操作发生在 Java 程序中。因为,执行这些操作需要时间。

一般来说,最好保留 Reference 类型的参数和基元类型的 result。

  1. 进行数学运算需要原语
  2. 基元占用较少的内存(如上所述) ,并且性能更好

您应该询问为什么需要 Class/Object 类型

使用 Object 类型的原因是为了在处理集合时使我们的生活更容易。原语不能直接添加到 List/Map 中,而是需要编写一个包装类。Readymade Integer 类在这里可以帮助您,另外它还有许多实用方法,比如 Integer.pareseInt (str)

我同意以前的答案,使用原语包装对象可能代价高昂。 但是,如果应用程序的性能不是关键的,那么在使用对象时就可以避免溢出。例如:

long bigNumber = Integer.MAX_VALUE + 2;

bigNumber的值是 -2147483647,您会期望它是2147483649。这是代码中的一个 bug,可以通过以下方法修复:

long bigNumber = Integer.MAX_VALUE + 2l; // note that '2' is a long now (it is '2L').

bigNumber是2147483649。这些类型的错误有时很容易被忽略,并可能导致未知的行为或漏洞(参见 CWE-190)。

如果使用包装器对象,等效的代码将无法编译。

Long bigNumber = Integer.MAX_VALUE + 2; // Not compiling

因此,通过使用原语包装对象更容易停止这类问题。

你的问题已经得到了回答,我只是想补充一点之前没有提到的信息。

基本类型快多了,需要很多 更少的记忆。因此,我们可能希望更喜欢使用他们。

另一方面,当前的 Java 语言规范不允许在参数化类型(泛型)、 Java 集合或反射 API 中使用原语类型。

当我们的应用程序需要包含大量元素的集合时,我们应该考虑使用尽可能“经济”类型的数组。

* 详细信息见来源: https://www.baeldung.com/java-primitives-vs-objects

简而言之: 基元类型比盒装类型更快,需要的内存更少