A case for keeping primitives in Java

Primitives are essential for applications dominated by numerical calculations

Primitives have been part of the Java programming language since its initial release in 1996, and yet they remain one of the more controversial language features. John Moore makes a strong case for keeping primitives in the Java language by comparing simple Java benchmarks, both with and without primitives. He then compares the performance of Java to that of Scala, C++, and JavaScript in a particular type of application, where primitives make a notable difference.

Question: What are the three most important factors in purchasing real estate?
Answer: Location, location, location.

This old and often-used adage is meant to imply that location completely dominates all other factors when it comes to real estate. In a similar argument, the three most important factors to consider for using primitive types in Java are performance, performance, performance. There are two differences between the argument for real estate and the argument for primitives. First, with real estate, location dominates in almost all situations, but the performance gains from using primitive types can vary greatly from one kind of application to another. Second, with real estate, there are other factors to consider even though they are usually minor in comparison to location. With primitive types, there is only one reason to use them — performance; and then only if the application is the kind that can benefit from their use.

Primitives offer little value to most business-related and Internet applications that use a client-server programming model with a database on the backend. But the performance of applications that are dominated by numerical calculations can benefit greatly from the use of primitives.

The inclusion of primitives in Java has been one of the more controversial language design decisions, as evidenced by the number of articles and forum posts related to this decision. Simon Ritter noted in his JAX London in November 2011 keynote address that serious consideration was being given to the removal of primitives in a future version of Java (see slide 41). In this article I'll briefly introduce primitives and Java's dual-type system. Using code samples and simple benchmarks, I'll make my case for why Java primitives are needed for certain types of applications. I will also compare Java's performance to that of Scala, C++, and JavaScript.

Primitives versus objects

As you probably already know if you are reading this article, Java has a dual-type system, usually referred to as primitive types and object types, often abbreviated simply as primitives and objects. There are eight primitive types predefined in Java, and their names are reserved keywords. Commonly used examples include int, double, and boolean. Essentially all other types in Java, including all user-defined types, are object types. (I say "essentially" because array types are a bit of a hybrid, but they are much more like object types than primitive types.) For each primitive type there is a corresponding wrapper class that is an object type; examples include Integer for int, Double for double, and Boolean for boolean.

Primitive types are value based, but object types are reference based, and therein lies both the power and the source of controversy of primitive types. To illustrate the difference, consider the two declarations below. The first declaration uses a primitive type and the second uses a wrapper class.


int n1 = 100;
Integer n2 = new Integer(100);

Using autoboxing, a feature added to JDK 5, I could shorten the second declaration to simply


Integer n2 = 100;

but the underlying semantics don't change. Autoboxing simplifies the use of wrapper classes and reduces the amount of code a programmer has to write, but it doesn't change anything at runtime.

The difference between the primitive n1 and the wrapper object n2 is illustrated by the diagram in Figure 1.

A diagram of the memory layout of primitives versus objects. John I. Moore, Jr.

Figure 1. Memory layout of primitives versus objects

The variable n1 holds an integer value, but the variable n2 contains a reference to an object, and it is the object that holds the integer value. In addition, the object referenced by n2 also contains a reference to the class object Double.

The problem with primitives

Before I try to convince you of the need for primitive types, I should acknowledge that many people won't agree with me. Sherman Alpert in "Primitive types considered harmful" argues that primitives are harmful because they mix "procedural semantics into an otherwise uniform object-oriented model. Primitives are not first-class objects, yet they exist in a language that involves, primarily, first-class objects." Primitives and objects (in the form of wrapper classes) provide two ways of handling logically similar types, but they have very different underlying semantics. For example, how should two instances be compared for equality? For primitive types, one uses the == operator, but for objects the preferred choice is to call the equals() method, which isn't an option for primitives. Similarly, different semantics exist when assigning values or passing parameters. Even the default values are different; e.g., 0 for int versus null for Integer.

For more background on this issue, see Eric Bruno's blog post, "A modern primitive discussion," which summarizes some of the pros and cons of primitives. A number of discussions on Stack Overflow also focus on primitives, including "Why do people still use primitive types in Java?" and "Is there a reason to always use Objects instead of primitives?." Programmers Stack Exchange hosts a similar discussion entitled "When to use primitive vs class in Java?".

Memory utilization

A double in Java always occupies 64 bits in memory, but the size of a reference depends on the Java virtual machine (JVM). My computer runs the 64-bit version of Windows 7 and a 64-bit JVM, and therefore a reference on my computer occupies 64 bits. Based on the diagram in Figure 1 I would expect a single double such as n1 to occupy 8 bytes (64 bits), and I would expect a single Double such as n2 to occupy 24 bytes — 8 for the reference to the object, 8 for the double value stored in the object, and 8 for the reference to the class object for Double. Plus, Java uses extra memory to support garbage collection for objects types but not for primitive types. Let's check it out.

Using an approach similar to that of Glen McCluskey in "Java primitive types vs. wrappers," the method shown in Listing 1 measures the number of bytes occupied by an n-by-n matrix (two-dimensional array) of double.

Listing 1. Calculating memory utilization of type double


public static long getBytesUsingPrimitives(int n)
  {
    System.gc();   // force garbage collection
    long memStart = Runtime.getRuntime().freeMemory();
    double[][] a = new double[n][n];

    // put some random values in the matrix
    for (int i = 0;  i < n;  ++i)
      {
        for (int j = 0; j < n;  ++j)
            a[i][j] = Math.random();
      }

    long memEnd = Runtime.getRuntime().freeMemory();

    return memStart - memEnd;
  }

Modifying the code in Listing 1 with the obvious type changes (not shown), we can also measure the number of bytes occupied by an n-by-n matrix of Double. When I test these two methods on my computer using 1000-by-1000 matrices, I get the results shown in Table 1 below. As illustrated, the version for primitive type double equates to a little more than 8 bytes per entry in the matrix, roughly what I expected. However, the version for object type Double required a little more than 28 bytes per entry in the matrix. Thus, in this case, the memory utilization of Double is more than three times the memory utilization of double, which should not be a surprise to anyone who understands the memory layout illustrated in Figure 1 above.

Runtime performance

To compare the runtime performances for primitives and objects, we need an algorithm dominated by numerical calculations. For this article I have chosen matrix multiplication, and I compute the time required to multiply two 1000-by-1000 matrices. I coded matrix multiplication for double in a straightforward manner as shown in Listing 2 below. While there may be faster ways to implement matrix multiplication (perhaps using concurrency), that point is not really relevant to this article. All I need is common code in two similar methods, one using the primitive double and one using the wrapper class Double. The code for multiplying two matrices of type Double is exactly like that in Listing 2 with the obvious type changes.

Listing 2. Multiplying two matrices of type double


public static double[][] multiply(double[][] a, double[][] b)
  {
    if (!checkArgs(a, b))
        throw new IllegalArgumentException("Matrices not compatible for multiplication");

    int nRows = a.length;
    int nCols = b[0].length;

    double[][] result = new double[nRows][nCols];

    for (int rowNum = 0;  rowNum < nRows;  ++rowNum)
      {
        for (int colNum = 0;  colNum < nCols;  ++colNum)
          {
                double sum = 0.0;

                for (int i = 0;  i < a[0].length;  ++i)
                    sum += a[rowNum][i]*b[i][colNum];

                result[rowNum][colNum] = sum;
          }
      }

    return result;
  }

I ran the two methods to multiply two 1000-by-1000 matrices on my computer several times and measured the results. The average times are shown in Table 2. Thus, in this case, the runtime performance of double is more than four times as fast as that of Double. That is simply too much of a difference to ignore.

The SciMark 2.0 benchmark

Thus far I've used the single, simple benchmark of matrix multiplication to demonstrate that primitives can yield significantly greater computing performance than objects. To reinforce my claims I'll use a more scientific benchmark. SciMark 2.0 is a Java benchmark for scientific and numerical computing available from the National Institute of Standards and Technology (NIST). I downloaded the source code for this benchmark and created two versions, the original version using primitives and a second version using wrapper classes. For the second version I replaced int with Integer and double with Double to get the full effect of using wrapper classes. Both versions are available in the source code for this article.

The SciMark benchmark measures performance of several computational routines and reports a composite score in approximate Mflops (millions of floating point operations per second). Thus, larger numbers are better for this benchmark. Table 3 gives the average composite scores from several runs of each version of this benchmark on my computer. As shown, the runtime performances of the two versions of the SciMark 2.0 benchmark were consistent with the matrix multiplication results above in that the version with primitives was almost five times faster than the version using wrapper classes.

You've seen a few variations of Java programs doing numerical calculations, using both a homegrown benchmark and a more scientific one. But how does Java compare to other languages? I'll conclude with a quick look at how Java's performance compares to that of three other programming languages: Scala, C++, and JavaScript.

Benchmarking Scala

Scala is a programming language that runs on the JVM and appears to be gaining in popularity. Scala has a unified type system, meaning that it doesn't distinguish between primitives and objects. According to Erik Osheim in Scala's Numeric type class (Pt. 1), Scala uses primitive types when possible but will use objects if necessary. Similarly, Martin Odersky's description of Scala's Arrays says that "... a Scala array Array[Int] is represented as a Java int[], an Array[Double] is represented as a Java double[] ..."

So does this mean that Scala's unified type system will have runtime performance comparable to Java's primitive types? Let's see.

I am not as proficient with Scala as I am with Java, but I attempted to convert the code for the matrix multiplication benchmark directly from Java to Scala. The result is shown in Listing 3 below. When I ran the Scala version of the benchmark on my computer, it averaged 12.30 seconds, which puts the performance of Scala very close to that of Java with primitives. That outcome is much better than I expected and supports the claims about how Scala handles numeric types.

Listing 3. Multiplying two matrices in Scala


def multiply(a : Array[Array[Double]], b : Array[Array[Double]]) : Array[Array[Double]] =
  {
    if (!checkArgs(a, b))
        throw new IllegalArgumentException("Matrices not compatible for multiplication");

    val nRows : Int = a.length;
    val nCols : Int = b(0).length;

    var result = Array.ofDim[Double](nRows, nCols);

    for (rowNum <- 0 until nRows)
      {
        for (colNum <- 0 until nCols)
          {
            val sum : Double = 0.0;

            for (i <- 0  until a(0).length)
                sum += a(rowNum)(i)*b(i)(colNum);

            result(rowNum)(colNum) = sum;
          }
      }

    return result;
  }

Benchmarking C++

Since C++ runs directly on "bare metal" rather than in a virtual machine, one would naturally expect C++ to run faster than Java. Moreover, Java performance is reduced slightly by the fact that Java checks accesses to arrays to ensure that each index is within the bounds declared for the array, whereas C++ does not (a C++ feature that can lead to buffer overflows, which can be exploited by hackers). I found C++ to be somewhat more awkward than Java in dealing with basic two-dimensional arrays, but fortunately much of this awkwardness can be hidden inside the private parts of a class. For C++, I created a simple version of a Matrix class, and I overloaded the operator * for multiplying two matrices, but the basic matrix multiplication algorithm was converted directly from the Java version. The C++ source code is shown in Listing 4.

Listing 4. Multiplying two matrices in C++


Matrix operator*(const Matrix& m1, const Matrix& m2) throw(invalid_argument)
  {
    if (!Matrix::checkArgs(m1, m2))
          throw invalid_argument("matrices not compatible for multiplication");

    Matrix result(m1.nRows, m2.nCols);

    for (int i = 0;  i < result.nRows;  ++i)
      {
        for (int j = 0;  j < result.nCols;  ++j)
          {
            double sum = 0.0;

            for (int k = 0;  k < m1.nCols;  ++k)
                sum += m1.p[i][k]*m2.p[k][j];

            result.p[i][j] = sum;
          }
      }

    return result;
  }

Using Eclipse CDT (Eclipse for C++ Developers) with the MinGW C++ compiler, it is possible to create both debug and release versions of an application. To test C++ I ran the release version several times and averaged the results. As expected, C++ ran noticeably faster on this simple benchmark, averaging 7.58 seconds on my computer. If raw performance is the primary factor for selecting a programming language, then C++ is the language of choice for numerically-intensive applications.

Benchmarking JavaScript

Okay, this one surprised me. Given that JavaScript is a very dynamic language, I expected its performance to be the worst of all, even worse than Java with wrapper classes. But in fact, JavaScript's performance was much closer to that of Java with primitives. To test JavaScript I installed Node.js, a JavaScript engine with a reputation for being very efficient. The results averaged 15.91 seconds. Listing 5 shows the JavaScript version of the matrix multiplication benchmark that I ran on Node.js

Listing 5. Multiplying two matrices in JavaScript


function multiply(a, b)
  {
    if (!checkArgs(a, b))
        throw ("Matrices not compatible for multiplication");

    var nRows = a.length;
    var nCols = b[0].length;

    var result = Array(nRows);

    for(var rowNum = 0;  rowNum < nRows; ++rowNum)
      {
        result[rowNum] = Array(nCols);

        for(var colNum = 0;  colNum < nCols;  ++colNum)
          {
            var sum = 0;

            for(var i = 0;  i < a[0].length;  ++i)
                sum += a[rowNum][i]*b[i][colNum];

            result[rowNum][colNum] = sum;
          }
      }

    return result;
  }

In conclusion

When Java first arrived on the scene some 18 years ago, it wasn't the best language from a performance perspective for applications that are dominated by numerical calculations. But over time, with technological advancements in areas such as just-in-time (JIT) compilation (aka adaptive or dynamic compilation), Java's performance for these kinds of applications is now comparable to that of languages that are compiled into native code when primitive types are used.

Moreover, primitives eliminate the need for garbage collection, thereby providing another performance advantage of primitives over object types. Table 4 summarizes the runtime performance of the matrix multiplication benchmark on my computer. Other factors such as maintainability, portability, and developer expertise make Java a better choice for many such applications.

As previously discussed, Oracle appears to be giving serious consideration to the removal of primitives in a future version of Java. Unless the Java compiler can generate code with performance comparable to that of primitives, I think that their removal from Java would preclude the use of Java for certain classes of applications; namely, those applications dominated by numerical calculations. In this article I have used a simple benchmark based on matrix multiplication and a more scientific benchmark, SciMark 2.0, to argue this point.

About the Author

John I. Moore, Jr., Professor of Mathematics and Computer Science at The Citadel, has a wide range of experience in both industry and academia, with specific expertise in the areas of object-oriented technology, software engineering, and applied mathematics. For more than three decades he has designed and developed software using relational databases and several high-order languages, and he has worked extensively in Java since version 1.1. In addition, he has developed and taught numerous academic courses and industrial seminars on advanced topics in computer science.

Further reading

  1. Paul Krill wrote about Oracle's long-range plans for Java in "Oracle lays out long-range Java intentions" (JavaWorld, March 2012). This article, along with the associated comments thread, motivated me to write this defense of primitives.
  2. Szymon Guz writes about his results in benchmarking primitive types and wrapper classes in "Primitives and objects benchmark in Java" (SimonOnSoftware, January 2011).
  3. On the support website for Programming — Principles and Practice Using C++ (Addison-Wesley, 2009), C++ creator Bjarne Stroustrup provides an implementation for a matrix class that is much more complete than the one accompanying this article.
  4. John Rose, Brian Goetz, and Guy Steele discuss a concept called value types in "State of the Values" (OpenJDK.net, April 2014). Value types can be thought of as immutable user-defined aggregate types without identity, therby combining properties of both objects and primitives. The mantra for value types is "codes like a class, works like an int."
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more