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:
| Operation | Meaning |
|---|---|
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:
| Primitive | Wrapper |
|---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
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 withy.equals(x); - transitive: if
xequalsyandyequalsz, thenxequalsz; - 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:
| Type | Meaning |
|---|---|
LocalDate | Calendar date without time or zone |
LocalTime | Time of day without date or zone |
LocalDateTime | Date and time without zone |
Instant | Point on the UTC time line |
OffsetDateTime | Date-time with a fixed UTC offset |
ZonedDateTime | Date-time with a region zone and its rules |
Duration | Time-based amount, such as seconds |
Period | Date-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
| Mistake | Correct approach |
|---|---|
Comparing String content with == | Use equals or Objects.equals |
| Ignoring a returned String | Assign or use the new immutable value |
Splitting a literal dot with split(".") | Escape or quote the regex |
| Unboxing a nullable wrapper | Check or provide a default first |
| Trusting wrapper identity caches | Compare values |
| Overriding equals without hashCode | Implement both from the same state |
| Mutating a stored hash key | Use immutable keys |
| Treating LocalDateTime as a global instant | Supply the intended zone or offset |
| Sharing SimpleDateFormat across threads | Prefer 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.