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


163

从Java 5开始,我们已经对原始类型进行装箱/拆箱,因此将int其包装为java.lang.Integer,等等。

我最近看到许多新的Java项目(肯定要求JRE的版本至少为5,如果不是6的话)正在使用int而不是java.lang.Integer,尽管使用后者要方便得多,因为它具有一些用于转换的辅助方法到long的值等。

为什么有些人仍在 Java中使用原始类型?有什么切实的好处吗?


49
有没有想过内存消耗和性能?
泰迪尔2011年

76
我添加了自动装箱标签...,发现实际上有3个人跟随了它。真?人们遵循AUTOBOXING标签吗?
corsiKa 2011年

4
@glowcoder他们不是真实的人,他们只是抽象的概念,它们假定人类以某种形式回应。:)
biziclop 2011年

9
@TK Kocheran通常是因为new IntegeR(5) == new Integer(5)应该根据规则将其评估为false。
biziclop 2011年

10
有关原始类型集合的解决方案,请参见GNU Trove或Mahout集合或HPPC或...。我们中那些关心速度的人会花更多的时间原始类型上,而不是更少。
bmargulies

Answers:


395

在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个flyweight。JVM实现者可以根据需要添加更多内容,这意味着它可以在缓存了最近的1024个并且都返回true的系统上运行... #awkward


54
现在想象一下是否i也被宣布Long
ColinD 2011年

14
@TREE-规范实际上要求 VM在一定范围内创建动态权重。但是令人遗憾的是,它允许它们扩展该范围,这意味着程序在不同的VM上的行为可能有所不同。对于跨平台来说是如此……
Daniel Earwicker 2011年

12
Java越来越繁重,有越来越多的不良设计选择。自动装箱是完全​​失败的,它既不健壮,不可预测也不可移植。我真的很想知道他们在想什么……他们没有解决可怕的原始对象对偶问题,反而使它变得比最初更糟。
Pop Catalin 2011年

34
@Catalin我不同意您的自动装箱完全失败。它有一些缺陷,与可以使用的任何其他设计(包括什么都没有)没什么不同。它们非常清楚您可以期望和不能期望的东西,并且像其他希望开发人员知道并遵守合同的设计一样。这些设计中。
corsiKa 2011年

9
@NaftuliTzviKay这不是“失败”。它们使操作员非常清楚地对==表达式进行引用标识比较,并对Integer表达式进行值相等比较intInteger.equals()因为这个原因而存在。您绝不要使用==任何非原始类型的值进行比较。这是Java的101
NullUserException

86

自动拆箱可能导致难以发现NPE

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

在大多数情况下,对的空分配in要比上述情况少很多。


43

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


40

基本类型:

int x = 1000;
int y = 1000;

现在评估:

x == y

true。不足为奇。现在尝试盒装类型:

Integer x = 1000;
Integer y = 1000;

现在评估:

x == y

false。大概。取决于运行时。那足够吗?


36

除了性能和内存问题外,我还要提出另一个问题:如果不使用,该List接口将被破坏int
问题是重载remove()方法(remove(int)vs. 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!

7
会坏的,是的。但是,remove(int)是IMO的设计缺陷。如果混和的可能性很小,则永远不要重载方法名称。
MrBackend

4
@MrBackend公平。有趣的是,VectorremoveElementAt(int)从一开始。remove(int)是Java 1.2中的collections框架引入的。
xehpuk 2015年

6
@MrBackend:在当ListAPI的设计,无论是仿制药,也没有自动装箱存在,所以没有混淆的机会remove(int)remove(Object)......
霍尔格·

@Franklin Yu:当然可以,但是在设计没有兼容性限制的新语言/版本时,您不会停止更改这种不幸的重载。您将完全摆脱基元和装箱值的区别,从而永远不会出现要使用的问题。
霍尔格

27

你真的能想象一个

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

用java.lang.Integer循环?java.lang.Integer是不可变的,因此循环中的每次增量都会在堆上创建一个新的Java对象,而不是仅使用一条JVM指令就可以递增堆栈上的int。表演将是恶魔般的。

我真的不同意使用java.lang.Integer比int方便得多。反之。自动装箱意味着您可以在可能被强制使用Integer的地方使用int,并且Java编译器负责插入代码来为您创建新的Integer对象。自动装箱就是允许您在期望使用Integer的地方使用int,而编译器会插入相关的对象构造。首先,它绝不会消除或减少对int的需求。通过自动装箱,您可以两全其美。当您需要一个基于堆的java对象时,您会自动为您创建一个Integer,而当您执行算术和局部计算时,您将获得一个int的速度和效率。


19

基本类型快得多:

int i;
i++;

整数(所有数字,还有一个字符串)是不可变的类型:一旦创建,就不能更改。如果i是Integer,i++则将创建一个新的Integer对象-在内存和处理器方面要昂贵得多。


您不希望对另一个变量进行更改i++,因此Integer必须完全不可变才能执行此操作(或者至少i++必须创建一个新的Integer对象)。(并且原始值也是不可变的-因为它们不是对象,所以您不必对此加以说明。)
PaŭloEbermann 2011年

4
@Paŭlo:说原始值是不可变的,这是毫无意义的。当您将原始变量重新分配给新值时,您并没有创建任何新内容。不涉及内存分配。彼得的观点是:对于原语,i ++不分配内存,但对于对象则必须这样做。
艾迪(Eddie)

@Eddie :(它不一定需要内存分配,它还可以返回一个缓存的值。我认为它确实需要一些小的值。)我的观点是,这里的Integers不变性不是决定性的点,无论如何您都想要不管不变性如何,都有另一个对象。
圣保罗Ebermann

@Paŭlo:我唯一要说的是Integer比基本类型慢一个数量级。这是由于以下事实:装箱的类型是不可变的,并且每次更改值时都会创建一个新对象。我并没有声称它们有什么问题或它们是不可变的。只是他们速度较慢,编码人员应该知道这一点。看看没有原始类型的Groovy票价如何jroller.com/rants/entry/why_is_groovy_so_slow
Peter Knego 2011年

1
不变性++在这里是红鲱鱼。想象一下Java得到了增强,它以一种非常简单的方式来支持运算符重载,例如,如果一个类(例如Integer具有一个方法plus,则可以编写i + 1而不是i.plus(1)。)并且还假定编译器足够智能,可以扩展i++到其中i = i + 1。现在您可以说i++切实做到“增加变量i”没有Integer被改变的。
丹尼尔·埃里克

16

首先,要养成习惯。如果您使用Java编写代码已有八年之久,那么您将积累相当多的惯性。如果没有令人信服的理由为什么要进行更改?似乎使用盒装基元没有任何额外的优势。

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

它也具有性能方面的优点,尽管在许多情况下性能差异并不重要(尽管很糟糕),但是没有人喜欢编写可以以更快的速度编写的代码,而我们已经过去。


15
我不同意。性能方面可能很关键。这可能很少是惯性或习惯力。
艾迪(Eddie)

7
@Eddie可以,但是很少。相信我,对于大多数人而言,性能争论只是一个借口。
biziclop 2011年

3
我也想保护性能论点。在具有Dalvik的Android上,您创建的每个对象都会增加被调用GC的“风险”,并且您拥有更多停顿的对象将更长。因此,在循环中创建Integer而不是int可能会花费您一些丢失的帧。
IgorČordaš2014年

1
@PSIXO这是一个公平的观点,我在编写时就考虑了纯粹的服务器端Java。移动设备是完全不同的动物。但是我的观点是,即使是那些编写了糟糕代码而不考虑性能的开发人员,也都将这作为一个理由,从他们那里看来,这听起来像是一个借口。
biziclop 2014年

12

顺便说一句,Smalltalk只有对象(没有基元),但是他们已经优化了它们的小整数(不使用全部32位,仅使用27个左右)以不分配任何堆空间,而只是使用特殊的位模式。另外,其他常见对象(true,false,null)在这里也具有特殊的位模式。

因此,至少在64位JVM(具有64位指针名称空间)上,应该完全没有整数,字符,字节,短,布尔,浮点(和小长整数)的任何对象(除了创建的对象之外)通过显式new ...()),只有特殊的位模式,普通操作员可以非常有效地对其进行操作。


我应该说“一些实现”,因为我认为这不受语言规范的约束。(可悲的是,我在这里无法引用任何消息来源,仅来自我在某处听到的消息。)
PaŭloEbermann

ŭlo,JIT已将meta保留在指针中;包括指针可以保留GC信息或Klass(优化类比优化整数好得多,我对此不太在意)。在每次加载指针之前,更改指针都需要shift / cmp / jnz代码(或类似的代码)。硬件可能无法很好地预测分支(因为它既可以是值类型,也可以是普通对象),并且会导致性能下降。
2011年

3
我做了Smalltalk几年了。优化仍然非常昂贵,因为对于int上的每个操作,他们都必须取消屏蔽并重新应用它们。目前,在处理原始数字时,java与C相当。使用unmask + mask,可能会慢30%以上。
R.Moeller 2014年

9

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

给定的另一个原因是您可以获得NPE,但是使用装箱类型非常容易避免(并且只要始终将其初始化为非null值,就可以避免)。

另一个原因是(new Long(1000))==(new Long(1000))是错误的,但这只是说“ .equals”不对盒装类型提供语法支持的另一种方式(不同于运算符<,> ,=等),因此我们回到“简单语法”的原因。

我认为史蒂夫·耶格(Steve Yegge)的非原始循环示例很好地说明了我的观点:http : //sites.google.com/site/steveyegge2/language-trickery-and-ejb

想一想:与Java相比,您需要多久使用一种具有良好语法的语言(例如任何功能语言,python,ruby甚至C)来使用函数类型,而Java必须使用Runnable和Callable等接口来模拟它们。无名阶级。


8

不摆脱基元的几个原因:

  • 向后兼容。

如果消除了它,那么任何旧程序都将无法运行。

  • JVM重写。

为了支持这一新功能,必须重写整个JVM。

  • 更大的内存占用量。

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

  • 空指针问题。

声明int i然后进行处理i不会导致任何问题,但是声明Integer i然后进行同样的处理会导致NPE。

  • 平等问题。

考虑以下代码:

Integer i1 = 5;
Integer i2 = 5;

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

会是错误的。操作员将不得不超载,这将导致大量的内容重写。

对象包装器比原始包装器慢得多。


i1 == i2; 仅当i1> = 128时才会为假。因此,当前示例是错误的
Geniy

7

对象比原始类型要重得多,因此原始类型比包装器类的实例要有效得多。

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

也就是说,Java在原始类型和非原始类型之间有区别的事实也是Java编程语言时代的标志。较新的编程语言没有这种区别。如果您使用的是简单值或更复杂的对象,则这种语言的编译器足够聪明,可以自行判断。

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


1
我本来以为JRE足够“聪明”,可以用Java包装的原语来做到这一点。失败。
Naftuli Kay 2011年

7

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


3
抱歉,这是错误的。智能JVM可以对任何对象分配进行转义分析,如果无法转义,则可以在堆栈上分配它们。
rlibby 2011年

2
是的,这已开始成为现代JVM的功能。在接下来的五年中,您所说的对于当时正在使用的大多数JVM都是正确的。今天不是。我几乎对此发表了评论,但决定不对此发表评论。也许我应该说些什么。
艾迪(Eddie)

6

基本类型具有许多优点:

  • 编写更简单的代码
  • 由于没有实例化变量的对象,因此性能更好
  • 由于它们不代表对对象的引用,因此无需检查null
  • 除非需要利用拳击功能,否则请使用基本类型。

5

很难知道幕后进行了什么样的优化。

对于本地使用,当编译器具有足够的信息以进行优化(不包括null值的可能性)时,我希望性能相同或相似

但是,基元数组显然与盒装基元集合非常不同。鉴于在一个集合的深处几乎不可能进行优化,因此这是有道理的。

此外,与相比,逻辑开销Integer要高得多:现在,您必须担心是否会引发异常。intint a = b + c;

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


5
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));

长时间绕圈“ 100000000”次所花费的毫秒数:468

长时间绕一圈'100000000'所花费的毫秒数:31

附带一提,我不介意看到类似这样的东西发现它已经进入Java。

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

其中for循环会自动将loop1从0增加到1000或

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

其中for循环会将loop1自动递减1000到0。


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

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

使用对象类型的原因是使我们在处理集合时更加轻松。无法将基元直接添加到列表/地图,而是需要编写包装器类。现成的整数类型的类在这里可以为您提供帮助,此外它还具有许多实用的方法,例如Integer.pareseInt(str)


2

我同意以前的答案,使用原始包装对象可能会很昂贵。但是,如果性能在您的应用程序中并不重要,则可以避免使用对象时发生溢出。例如:

long bigNumber = Integer.MAX_VALUE + 2;

的价值 bigNumber -2147483647,您希望它为2147483649。这是代码中的错误,可以通过执行以下操作来修复:

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

因此,通过使用原语包装对象可以更轻松地阻止此类问题。

您的问题已经得到了如此答案,我只想补充一点以前没有提到的信息。


1

因为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的参数和原始类型的结果。


1

基本类型更快,需要多大的内存更少。因此,我们可能更希望使用它们。

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

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

*有关详细信息,请参见源:https : //www.baeldung.com/java-primitives-vs-objects


0

简而言之:原始类型比盒装类型更快并且需要更少的内存

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.