Core and Web-based Java

Strings, Wrappers, Object Contracts and Date-Time

PGCP-AC

Text, numbers, equality and time appear in almost every Java application. Their APIs look simple, yet they cause frequent defects: reference identity is mistaken for value equality, null wrappers are unboxed, mutable keys disappear from hash lookups and date-time values are interpreted in the wrong zone. This chapter explains the underlying models so that the correct operation follows from the meaning of the data.

1. String objects and immutability

String represents a sequence of UTF-16 code units and is immutable. Once constructed, its contents never change. Methods that appear to modify a string return another string:

String original = "core java";
String upper = original.toUpperCase();

System.out.println(original); // core java
System.out.println(upper);    // CORE JAVA

Immutability makes strings safe to share, suitable as map keys and naturally thread-safe as values. It also lets the JVM reuse literal objects. Reassignment changes which object a variable refers to; it does not modify the earlier String.

Unicode characters outside the Basic Multilingual Plane use a surrogate pair, so length() counts UTF-16 code units rather than always counting visible characters. Use code-point APIs when a task requires Unicode code points rather than indexes compatible with ordinary String methods.

2. String pool and object identity

String literals are interned. Equal literals in the same runtime commonly refer to the same pooled object:

String a = "java";
String b = "java";
String c = new String("java");

System.out.println(a == b);       // true
System.out.println(a == c);       // false
System.out.println(a.equals(c));  // true

== compares reference identity: whether both variables hold the same object reference. equals compares String content. Code must not use pool behaviour as a substitute for value comparison. intern() returns a canonical pooled representation, but routine application comparison should still express its intent with equals.

For a possibly null variable, "ready".equals(status) safely returns false. Objects.equals(left, right) is a clear null-safe comparison for two variables.

3. Important String operations

String indexes begin at zero. Common methods include:

OperationMeaning
length()Number of UTF-16 code units
charAt(i)Character code unit at index i
substring(begin, end)Begin inclusive, end exclusive
indexOf(value)First position or -1
contains(value)Whether a character sequence occurs
replace(a, b)Literal replacement
replaceAll(regex, value)Regular-expression replacement
trim()Removes leading and trailing characters up to U+0020
strip()Removes Unicode-aware surrounding whitespace
String language = "Java";
System.out.println(language.substring(1, 3)); // av

An invalid index causes StringIndexOutOfBoundsException. The exclusive end convention makes the substring length equal to end - begin.

4. Splitting, joining and regular expressions

split accepts a regular expression, not a literal separator:

String[] parts = "api.example.com".split("\\.");
String path = String.join("/", "users", "42", "orders");

A dot means “any character” in regular expressions, so a literal dot must be escaped. In a Java string literal, the backslash itself is escaped, producing "\\.". Other regex metacharacters require the same care. Pattern.quote(separator) safely quotes a variable literal separator.

By default, split discards trailing empty strings. Supplying a negative limit, such as split(",", -1), preserves them. This matters when parsing row-like data where a final empty field is meaningful.

5. Concatenation and text blocks

The + operator concatenates when either operand is a String. Evaluation proceeds left to right:

System.out.println("Total: " + 2 + 3);   // Total: 23
System.out.println("Total: " + (2 + 3)); // Total: 5

The compiler can combine constant expressions and may translate simple concatenation efficiently. Repeated concatenation inside a loop can create many intermediate strings, so a mutable builder is preferable.

Java text blocks represent multiline text while applying defined indentation rules:

String json = """
        {
          "active": true
        }
        """;

They improve source readability but still create ordinary immutable String values.

6. StringBuilder and StringBuffer

StringBuilder stores a mutable character sequence:

StringBuilder builder = new StringBuilder();
for (int i = 1; i <= 3; i++) {
    if (builder.length() > 0) builder.append(", ");
    builder.append(i);
}
String result = builder.toString(); // 1, 2, 3

Methods such as append, insert, delete, replace and reverse modify the same builder and often return it for chaining. capacity() reports allocated storage, while length() reports used characters.

StringBuffer provides a similar API with synchronized individual methods. That synchronization adds overhead and does not automatically make a multi-call algorithm atomic. Use StringBuilder for normal local construction. Use explicit coordination or confinement when mutable text truly crosses threads.

Neither builder overrides equals for content, so compare the resulting strings or use an appropriate character-sequence comparison.

7. Wrapper classes

Every primitive has a wrapper class:

PrimitiveWrapper
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

Wrappers are immutable objects. They are required by generic collections, which cannot use primitive type arguments and they provide conversion methods, constants and utility operations.

Boxing converts a primitive to its wrapper; unboxing extracts the primitive:

Integer boxed = 42;  // boxing
int value = boxed;   // unboxing

If boxed is null, unboxing throws NullPointerException. Hidden unboxing also occurs in arithmetic, comparisons with primitives, switch expressions and conditional expressions. Validate nullable wrappers before such use.

8. Wrapper identity and numeric comparison

Autoboxing may reuse cached wrapper instances, making some == comparisons appear to work:

Integer x = 100;
Integer y = 100;
Integer p = 1000;
Integer q = 1000;

System.out.println(x == y); // commonly true by required Integer cache range
System.out.println(p == q); // normally false

Identity is not numeric equality. Use equals for equal wrapper type and value or unbox deliberately when primitive comparison is intended. Note that Integer.valueOf(1).equals(Long.valueOf(1)) is false because wrapper equality includes the wrapper type.

Floating-point values need special thought: NaN, infinities, signed zero and rounding make direct calculations different from exact decimal arithmetic. Financial values commonly require BigDecimal constructed from a decimal string.

9. Parsing and conversion

Parsing converts text to a primitive value:

int count = Integer.parseInt("125");
double rate = Double.parseDouble("7.5");

Integer.valueOf("125") returns an Integer object. Invalid numeric syntax causes NumberFormatException. Leading or trailing whitespace is not universally ignored; normalize deliberately when the input format permits it.

Radix-aware methods parse other bases:

int binary = Integer.parseInt("1010", 2); // 10
String hex = Integer.toHexString(255);     // ff

Parsing user input should report the invalid value and expected format at the application boundary instead of allowing a low-level exception to become an unclear user message.

10. The Object class contract

Every Java object inherits methods from Object, including equals, hashCode, toString and getClass. The default equals behaves like identity unless a class overrides it.

Value equality should represent the domain. Two student identifiers with the same number may be equal even if created separately. Two mutable service objects may intentionally retain identity equality. The choice must remain consistent with the class's purpose.

The equals contract requires:

  • reflexive: x.equals(x) is true;
  • symmetric: x.equals(y) agrees with y.equals(x);
  • transitive: if x equals y and y equals z, then x equals z;
  • consistent: repeated calls agree while relevant state is unchanged;
  • non-null: x.equals(null) is false.

11. Implementing equals and hashCode

Equal objects must have equal hash codes. Unequal objects may share a hash code; collisions are expected and resolved by hash-based collections.

final class StudentId {
    private final int value;
    StudentId(int value) { this.value = value; }

    @Override
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof StudentId that)) return false;
        return value == that.value;
    }

    @Override
    public int hashCode() {
        return Integer.hashCode(value);
    }
}

Use the same equality-defining fields in both methods. A record generates component-based equals, hashCode and toString, making it suitable for transparent data values:

record StudentId(int value) {}

Inheritance complicates value equality because adding fields can break symmetry between base and subtype objects. Immutable value classes are often final or records.

12. Mutable hash keys

A hash collection uses a key's hash code to choose a bucket. If an equality-defining field changes while the key is stored, a later lookup may search a different bucket:

Map<Person, String> roles = new HashMap<>();
Person key = new Person("Asha");
roles.put(key, "admin");
key.setName("Meera"); // dangerous if name defines hashCode

The entry still exists internally but may no longer be found by the mutated key. Prefer immutable keys. If mutation is unavoidable, remove the key before changing equality state and insert it again afterward.

13. Designing toString

toString should provide a concise, useful representation for logs and diagnostics:

@Override
public String toString() {
    return "Invoice{id=" + id + ", status=" + status + "}";
}

Do not expose passwords, tokens, personal data or full payment details. Do not assume the output is a stable machine-readable serialization unless an explicit documented format provides that guarantee.

14. Date-time concepts

The java.time API separates distinct meanings:

TypeMeaning
LocalDateCalendar date without time or zone
LocalTimeTime of day without date or zone
LocalDateTimeDate and time without zone
InstantPoint on the UTC time line
OffsetDateTimeDate-time with a fixed UTC offset
ZonedDateTimeDate-time with a region zone and its rules
DurationTime-based amount, such as seconds
PeriodDate-based amount, such as months
LocalDate exam = LocalDate.of(2026, 10, 15);
Instant recorded = Instant.now();
ZonedDateTime india = recorded.atZone(ZoneId.of("Asia/Kolkata"));

Choose the type from meaning. A birthday usually needs LocalDate; an audit event needs an Instant; a scheduled meeting tied to regional daylight-saving rules may need ZonedDateTime.

15. Date-time immutability and arithmetic

Java-time objects are immutable and thread-safe. Arithmetic returns a new value:

LocalDate start = LocalDate.of(2026, 1, 31);
LocalDate later = start.plusMonths(1);

start remains unchanged. Calendar arithmetic adjusts to valid dates, so adding one month to January 31 may produce the last valid day of February. Period.between describes calendar components, while Duration.between describes time-based elapsed units.

Time zones can contain daylight-saving transitions where a local time is skipped or occurs twice. An offset such as +05:30 is fixed; a region ID such as Europe/Paris carries historical and future zone rules.

16. Parsing and formatting date-time values

DateTimeFormatter is immutable and thread-safe:

DateTimeFormatter formatter =
        DateTimeFormatter.ofPattern("dd MMM uuuu", Locale.ENGLISH);

LocalDate date = LocalDate.parse("26 Sep 2026", formatter);
String text = date.format(formatter);

Parsing text must match the expected formatter. Prefer standard ISO formats for machine exchange. Use explicit patterns and locales for human formats.

Pattern letters are case-sensitive. M represents month, while m represents minute. H is a 24-hour clock, while h is a 12-hour clock generally paired with an AM/PM marker. A plausible-looking output can still be semantically wrong when the pattern is incorrect.

17. Legacy Date, Calendar and SimpleDateFormat

Older code uses java.util.Date, Calendar and SimpleDateFormat. Date primarily wraps a millisecond instant despite its historical methods. Calendar is mutable and has awkward conventions such as zero-based month constants. SimpleDateFormat is mutable and not safe for unsynchronized shared use.

Prefer java.time in new code. Interoperate at boundaries:

Instant instant = legacyDate.toInstant();
Date legacy = Date.from(instant);

Do not rely on object finalization for resources associated with legacy or modern APIs. Finalization is deprecated and has no timely execution guarantee; explicit closure remains the correct model.

Practical considerations

MistakeCorrect approach
Comparing String content with ==Use equals or Objects.equals
Ignoring a returned StringAssign or use the new immutable value
Splitting a literal dot with split(".")Escape or quote the regex
Unboxing a nullable wrapperCheck or provide a default first
Trusting wrapper identity cachesCompare values
Overriding equals without hashCodeImplement both from the same state
Mutating a stored hash keyUse immutable keys
Treating LocalDateTime as a global instantSupply the intended zone or offset
Sharing SimpleDateFormat across threadsPrefer DateTimeFormatter

Worked trace

String a = "42";
String b = new String("42");
Integer x = Integer.valueOf(a);
Integer y = Integer.valueOf(b);

System.out.println(a == b);
System.out.println(a.equals(b));
System.out.println(x.equals(y));

a == b is false because the explicit constructor creates a separate String object. a.equals(b) is true because both contain the same characters. Parsing or value conversion produces numerically equal Integer values, so x.equals(y) is true. The analysis does not depend on whether a wrapper cache reuses an instance because value comparison is used.

Continue learning

Related notes

Put this topic into timed practice

Open mock tests when you want full-exam pacing, or keep drilling in practice mode.