Tutorials

Numbers and Strings in Modern Java Programming

Key Takeaways

  • Java has eight primitive-to-wrapper pairs. Six numeric wrappers extend Number; Boolean and Character don't.
  • Autoboxing converts a primitive to its corresponding wrapper; unboxing reverses that conversion and throws NullPointerException for null.
  • Choose numeric types for their range and arithmetic semantics. Formatting doesn't make floating-point arithmetic exact.
  • String is immutable, not a primitive wrapper. Compare text with equals(), and consider StringBuilder for repeated concatenation.

Java's wrapper classes represent primitive values as objects, while String is an immutable reference type for text—not a primitive wrapper. Together with Math and the formatting APIs, they let you convert, compare, calculate, and display values, but each solves a different problem.

If you're comfortable with Java primitive data types, the next step is understanding where objects are useful and where they introduce avoidable surprises.

Why Use Java Wrapper Classes?

A wrapper gives a primitive value an object representation. The java.lang package documentation identifies these eight pairs; primitives cannot hold null, but references to any of these wrapper types can.

PrimitiveWrapperExtends Number?Wrapper reference can be null?
byteByteYesYes
shortShortYesYes
intIntegerYesYes
longLongYesYes
floatFloatYesYes
doubleDoubleYesYes
charCharacterNoYes
booleanBooleanNoYes

Number is an abstract superclass, not a wrapper you instantiate. Byte, Short, Integer, Long, Float, and Double extend it; Character and Boolean do not.

Use wrappers when an API requires objects, when you need a generic collection such as List<Integer>, or when null deliberately means “not supplied.” Generic type arguments cannot be primitives, so List<int> isn't valid Java; see Java generics and type inference for the underlying model.

Otherwise, prefer primitives for ordinary arithmetic. Boxing can introduce allocations and conversion work; it isn't a performance upgrade just because it uses objects. The eight wrapper classes are immutable: updating an Integer variable doesn't mutate the existing Integer object.

Java Wrapper Classes and Autoboxing

Autoboxing is the compiler's automatic conversion from a primitive to its corresponding wrapper. Unboxing extracts the primitive value. Both were introduced in Java 5, rather than being recent additions to “modern Java.”

import java.util.ArrayList;
import java.util.List;

public class BoxingDemo {
    public static void main(String[] args) {
        Integer myInt = 4;              // Autoboxing: int to Integer
        int primitive = myInt;          // Unboxing: Integer to int

        List<Integer> values = new ArrayList<>();
        values.add(primitive);          // Autoboxing
        int first = values.get(0);      // Unboxing
        System.out.println(first);      // 4
    }
}

Boxing isn't arbitrary numeric conversion. Integer wraps int and Long wraps long: Long count = 4L is valid, but Long count = 4 isn't. By contrast, assigning an Integer to a primitive long can unbox it to int and then widen that value to long. The language specification's conversion rules distinguish these operations.

Handle Null Before Unboxing

Unboxing null throws NullPointerException. Arithmetic can trigger unboxing too, so a nullable wrapper requires a missing-value policy before you calculate with it.

// Method-body fragment.
Integer suppliedCount = null;
// int unsafe = suppliedCount; // Throws NullPointerException if uncommented.

int count = suppliedCount != null ? suppliedCount : 0;
System.out.println(count); // 0

Defaulting to zero is appropriate only if zero has the intended meaning. Otherwise, reject the missing input or handle it as a separate case rather than hiding it.

The Number Class: Conversions and Comparisons

Numeric wrappers expose conversion methods such as floatValue() and longValue(). Integer also provides value equality and ordering through equals() and compareTo(), as the Integer API reference specifies.

public class NumberDemo {
    public static void main(String[] args) {
        Integer myInt = 25;
        System.out.println(myInt.floatValue()); // 25.0
        System.out.println(myInt.longValue());  // 25
        System.out.println(myInt.compareTo(25)); // 0
        System.out.println(myInt.equals(26));    // false
    }
}

compareTo() returns a negative number, zero, or a positive number when this Integer is smaller than, equal to, or larger than the argument. equals() returns a boolean and requires another Integer with the same value; an Integer holding 25 isn't equal to a Long holding 25. The primitive arguments in the example are boxed for these method calls.

Compare Values, Not Wrapper Identity

Between two Integer references, == asks whether they refer to the same object. It doesn't perform Integer value equality. Cached wrapper instances can make identity comparisons appear to work, so don't depend on the result.

// Method-body fragment; both references are non-null.
Integer left = 1000;
Integer right = 1000;
System.out.println(left == right);      // Identity: don't rely on this result.
System.out.println(left.equals(right)); // true

For primitives, comparison operators such as <, >, and == already compare numeric values. For the broader object contract, read equals() and hashCode() in Java.

Conversions Can Lose Information

Number conversion methods aren't range validators. Narrowing a large long to int can change its magnitude and sign; converting a fractional double to int can discard its fractional part.

// Method-body fragment.
Number large = Long.valueOf(3_000_000_000L);
System.out.println(large.intValue()); // -1294967296

Number fraction = Double.valueOf(12.9);
System.out.println(fraction.intValue()); // 12

Even some widening conversions, including int to float, can lose precision. Check the destination type's range and representation rather than assuming a method ending in Value() preserves everything.

Using Math and Choosing Arithmetic Types

Math provides static utilities: min() and max() select a bound, abs() computes absolute value subject to the type's range, and sqrt() computes a square root.

public class MathDemo {
    public static void main(String[] args) {
        System.out.println(Math.min(22.33, 44.54)); // 22.33
        System.out.println(Math.max(3, 7));         // 7
        System.out.println(Math.sqrt(81.0));       // 9.0
        System.out.println(Math.abs(-12));         // 12
    }
}

Know the edge cases: Math.sqrt(-1.0) returns NaN, meaning “not a number.” Math.abs(Integer.MIN_VALUE) remains negative because its positive counterpart doesn't fit in int. Ordinary integer addition can overflow without throwing; Math.addExact() and Math.multiplyExact() throw ArithmeticException when their integer result overflows.

Choose int or long for bounded whole-number arithmetic, and float or double when approximate binary floating-point arithmetic fits the problem. Double is the object form of double, not an exact-decimal alternative. Many decimal fractions, including 0.1, aren't exactly representable in binary floating point.

Use BigDecimal for Exact Decimal Arithmetic

For exact decimal quantities, start with BigDecimal values constructed from decimal strings. BigDecimal supports exact decimal arithmetic, but non-terminating division still needs an explicit rounding policy.

import java.math.BigDecimal;
import java.math.RoundingMode;

public class DecimalArithmeticDemo {
    public static void main(String[] args) {
        BigDecimal total = new BigDecimal("0.10")
                .add(new BigDecimal("0.20"));
        System.out.println(total); // 0.30

        BigDecimal share = new BigDecimal("10.00")
                .divide(new BigDecimal("3"), 2, RoundingMode.HALF_EVEN);
        System.out.println(share); // 3.33
    }
}

Avoid new BigDecimal(0.1) when you mean the exact decimal 0.1: that constructor preserves the already-approximate double value. For monetary calculations, define rounding rules explicitly; displaying two decimal places doesn't establish those rules.

Formatting Numeric Output

Introduce Format Specifiers with PrintStream.format()

System.out is a PrintStream. Its format() and printf() methods write formatted output; printf() is a convenience equivalent of format().

import java.util.Locale;

public class OutputDemo {
    public static void main(String[] args) {
        int myAge = 25;
        String myName = "Sam";
        System.out.format(Locale.US,
                "My name is %s and I'm %d years old.%n", myName, myAge);
        System.out.printf(Locale.US, "Value: %.2f%n", 123.456);
    }
}

The first line prints “My name is Sam and I'm 25 years old.” The second prints “Value: 123.46”. Here, %s formats a string, %d a decimal integer, %.2f a decimal value with two fractional digits, and %n the platform-specific line separator. Explicit Locale.US makes the locale-sensitive numeric presentation intentional.

Use compatible argument types: supplying a floating-point argument for %d causes IllegalFormatConversionException. Keep the format string under your control and pass variable content as arguments.

Format Decimals and Currency with an Explicit Locale

DecimalFormat controls localized display, not arithmetic precision. In its patterns, 0 requests a digit, including a trailing zero when needed, while # makes a digit optional. Its default rounding mode is HALF_EVEN; you can set the policy explicitly.

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Locale;

public class NumberDisplayDemo {
    public static void main(String[] args) {
        DecimalFormat decimal = new DecimalFormat(
                "0.00", DecimalFormatSymbols.getInstance(Locale.US));
        decimal.setRoundingMode(RoundingMode.HALF_EVEN);
        System.out.println(decimal.format(new BigDecimal("123.456"))); // 123.46

        NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.US);
        System.out.println(currency.format(new BigDecimal("123.46"))); // $123.46
    }
}

Use NumberFormat.getCurrencyInstance(locale) for locale-aware currency presentation instead of embedding a dollar sign in a general decimal pattern. Formatting a value doesn't modify the original numeric object.

Parsing Is Separate from Boxing

Integer.parseInt("25") parses text into a primitive int, while Integer.valueOf("25") returns an Integer. Neither operation is autoboxing, and invalid or out-of-range integer text causes NumberFormatException.

For localized user input, choose a parser such as NumberFormat.getNumberInstance(locale). Parsing can accept only a prefix of the input, so use the parse(String, ParsePosition) overload and check both success and complete consumption when validating a whole field. If DecimalFormat must produce BigDecimal for finite decimal input, configure setParseBigDecimal(true).

The String Class and Immutability

A String's contents cannot change after construction. You normally create text with a literal such as "Hello world"; concatenation returns a result rather than changing that original text.

public class StringDemo {
    public static void main(String[] args) {
        String helloWorld = "Hello world";
        String greeting = helloWorld.concat("!");

        System.out.println(helloWorld); // Hello world
        System.out.println(greeting);   // Hello world!
        System.out.println(helloWorld.concat("!").length()); // 12
    }
}

To keep a transformation, assign its result. Immutability doesn't mean every method always allocates a new object: an operation that changes nothing may return the existing String.

Compare and Manipulate Strings

Use equals() to compare text. Between String references, == compares object identity, so matching content doesn't imply that == is true.

import java.util.Locale;

public class StringOperationsDemo {
    public static void main(String[] args) {
        String myString = "Hello world";
        String copy = new String(myString); // Deliberately distinct for this demo.

        System.out.println(myString == copy);       // false
        System.out.println(myString.equals(copy));  // true
        System.out.println(myString.toUpperCase(Locale.ROOT)); // HELLO WORLD
        System.out.println(myString.toLowerCase(Locale.ROOT)); // hello world
        System.out.println(myString.replace("l", "z")); // Hezzo worzd
    }
}

The no-argument case-conversion methods use the default locale. Use Locale.ROOT for locale-independent identifiers and an appropriate user locale for human-language text. replace() performs literal replacement; replaceAll() interprets its first argument as a regular expression.

One important boundary: length() counts UTF-16 code units, not necessarily user-perceived characters. Some Unicode characters occupy two code units.

When StringBuilder Makes Sense

Keep ordinary + concatenation for simple expressions. Its implementation is compiler-dependent and can use specialized concatenation machinery; manually rewriting every expression as a builder isn't automatically better.

When you repeatedly append text in a loop, StringBuilder lets you update one mutable character sequence before creating the final String. It isn't a String wrapper.

// Method-body fragment.
StringBuilder builder = new StringBuilder();
for (int i = 1; i <= 3; i++) {
    if (i > 1) {
        builder.append(", ");
    }
    builder.append(i);
}
String result = builder.toString();
System.out.println(result); // 1, 2, 3

StringBuilder doesn't provide synchronization. StringBuffer synchronizes its operations where necessary, but use it only when that synchronization is actually needed; keeping a builder local is usually simpler. Measure performance-sensitive code rather than assuming either class guarantees less memory use.

FAQ

When should I use Integer instead of int?

Use Integer when an API requires an object, a generic collection needs an element type, or null deliberately represents a missing value. Prefer int for ordinary arithmetic when a value must always be present.

What happens when Java unboxes a null wrapper?

Java throws NullPointerException when it unboxes null. Check for missing input before arithmetic or assignment to a primitive, and choose deliberately whether to reject it or supply a default.

What is the difference between Number and Integer in Java?

Number is an abstract superclass that defines numeric conversion methods. Integer is a concrete wrapper for int that extends Number and adds integer-specific utilities, value equality, and ordering.

Should I use BigDecimal or Double for money?

Use BigDecimal when monetary calculations require exact decimal values and explicit rounding rules. Double uses approximate binary floating point; currency formatting changes presentation, not arithmetic correctness.

When should I use StringBuilder instead of String?

Use StringBuilder when you need to build text through repeated mutable appends, particularly in a loop. Keep String and ordinary + concatenation for simple expressions; StringBuffer is the synchronized alternative when its synchronization is needed.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me