
val str1 = "Lorem ipsum dolor sit amet" val str2 = "Lorem ipsum dolor sit amet" print(str1 == str2) // true print(str1 === str2) // true
Integer, Long) are also reused in JVM when they are small (by default, the Integer Cache holds numbers in the range from -128 to 127).val i1: Int? = 1 val i2: Int? = 1 print(i1 == i2) // true print(i1 === i2) // true, because i2 was taken from cache
===) shows that this is the same object. However, if we use a number that is either smaller than -128 or bigger than 127, different objects will be created:val j1: Int? = 1234 val j2: Int? = 1234 print(j1 == j2) // true print(j1 === j2) // false
Notice that a nullable type is used to forceIntegerinstead ofintunder the hood. When we useInt, it is generally compiled to the primitiveint, but if we make it nullable or when we use it as a type argument,Integeris used instead. This is because a primitive cannot benulland cannot be used as a type argument.
-
Objects take additional space. In a modern 64-bit JDK, an object has a 12-byte header that is padded to a multiple of 8 bytes, so the minimum object size is 16 bytes. For 32-bit JVMs, the overhead is 8 bytes. Additionally, object references also take space. Typically, references are 4 bytes on 32-bit or 64-bit platforms up to -Xmx32G, and they are 8 bytes for memory allocation pool set above 32Gb (-Xmx32G). These are relatively small numbers, but they can add up to a significant cost. When we think about small elements like integers, they make a difference.
Intas a primitive fit in 4 bytes, but when it is a wrapped type on the 64-bit JDK we mainly use today, it requires 16 bytes (it fits in the 4 bytes after the header), and its reference requires 4 or 8 bytes. In the end, it takes 5 or 6 times more space[^47_1]. This is why an array of primitive integers (IntArray) takes 5 times less space than an array of wrapped integers (Array<Int>), as explained in the Item 58: Consider Arrays with primitives for performance-critical processing. - Access requires an additional function call when elements are encapsulated. Again, this is a small cost as function use is very fast, but it can add up when we need to operate on a huge pool of objects. We will see how this cost can be eliminated in Item 51: Use the inline modifier for functions with parameters of functional types and Item 49: Consider using inline classes.
- Objects need to be created and allocated in memory, references need to be created, etc. These are small numbers, but they can rapidly accumulate when there are many objects. In the snippet below, you can see the cost of object creation.
class A private val a = A() // Benchmark result: 2.698 ns/op fun accessA(blackhole: Blackhole) { blackhole.consume(a) } // Benchmark result: 3.814 ns/op fun createA(blackhole: Blackhole) { blackhole.consume(A()) } // Benchmark result: 3828.540 ns/op fun createListAccessA(blackhole: Blackhole) { blackhole.consume(List(1000) { a }) } // Benchmark result: 5322.857 ns/op fun createListCreateA(blackhole: Blackhole) { blackhole.consume(List(1000) { A() }) }
- When we operate on a nullable type (primitives cannot be
null). - When we use a type as a generic type argument.
|-------------|---------------|
| Int | int |
| Int? | Integer |
| List<Int> | List<Integer> |
class Snapshot( val afterHours: SessionDetails, val preMarket: SessionDetails, val regularHours: SessionDetails, ) data class SessionDetails( val open: Double? = null, val high: Double? = null, val low: Double? = null, val close: Double? = null, val volume: Long? = null, val dollarVolume: Double? = null, val trades: Int? = null, val last: Double? = null, val time: Int? = null, )
SessionDetails many times per second, which will require a lot of effort from the garbage collector. To avoid this, you can change the SessionDetails class to use primitives instead of wrapped types by eliminating nullability.data class SessionDetails( val open: Double = Double.NaN, val high: Double = Double.NaN, val low: Double = Double.NaN, val close: Double = Double.NaN, val volume: Long = -1L, val dollarVolume: Double = Double.NaN, val trades: Int = -1, val last: Double = Double.NaN, val time: Int = -1, )
null is a better way to represent the lack of a value than a special value like NAN or -1. However, in this case we decided to make this change because we are dealing with a performance-critical part of the application. By eliminating nullability, we’ve made our object allocate far fewer objects and much less memory. On a typical machine, the first version of SessionDetails allocates 192 bytes and needs to create 10 objects; in contrast, the second version allocates only 80 bytes and needs to create only one object. This is a significant difference that might be worth the trouble when we are dealing with tens of thousands of objects.If such interventions are not enough in your application, you can consider using a very powerful but also very dangerous pattern object pool. Its core idea is to make objects mutable and to store and reuse unused objects. This pattern is hard to implement correctly, and it is easy to introduce synchronization issues, which is why I don’t recommend using it unless you’re sure that you need it.
[^47_2]: Java Language Specification, Java SE 8 edition, 3.10.5