String Basics

Su Suriya Ravichandran Updated 14 Nov 2026
7 min read ·Lesson 91 of 133

A String is an immutable sequence of characters. It is the type Java uses for all text: names, messages, file contents, JSON, SQL, URLs. String is an ordinary class in java.lang, but the language gives it special support: double-quoted literals, the + operator for joining, and a shared pool of literal values. This lesson looks closely at what a string is, how strings are created and stored, and the rules that follow from their design. The lessons after it cover length, methods, comparison, searching, and formatting in detail.

What a String Contains

Internally, a string is a sequence of UTF-16 code units, each represented in Java by a char. For most text (English, Tamil, Hindi, Chinese, and every other script in Unicode's Basic Multilingual Plane), one character is one char:

String word = "Java";
char first = word.charAt(0);      // 'J'
char[] chars = word.toCharArray();   // ['J', 'a', 'v', 'a']

Characters outside that plane, including most emoji, take two char values (a surrogate pair):

String wave = "Hi 👋";
System.out.println(wave.length());                          // 5: H, i, space, and two chars for the emoji
System.out.println(wave.codePointCount(0, wave.length()));  // 4: actual Unicode characters

Most code never needs to think about this, but it matters when counting user-visible characters, truncating text, or reversing strings that may contain emoji. The String Length lesson covers it in detail.

char versus String

charString
KindPrimitiveObject (reference type)
LiteralSingle quotes: 'A'Double quotes: "A"
HoldsExactly one UTF-16 code unitZero or more characters
Can be nullNoYes

'A' and "A" are different types and are not interchangeable: String s = 'A'; does not compile.

Creating Strings

String literal = "Chennai";                         // string literal: the usual way
String empty = "";                                  // empty string, length 0
String fromChars = new String(new char[]{'J', 'D', 'K'});    // "JDK"
String part = String.valueOf(new char[]{'a', 'b', 'c', 'd'}, 1, 2);   // "bc"
String fromNumber = String.valueOf(42);             // "42"
String fromBytes = new String(bytes, StandardCharsets.UTF_8);   // decode bytes as text
String repeated = "-".repeat(10);                   // "----------"
String joined = String.join(", ", "a", "b", "c");   // "a, b, c"

new String("Chennai") also works but is almost never useful: it creates an extra copy of a string you already have. When converting bytes to text, always specify the character set; relying on the platform default causes garbled text when the program runs on a differently configured machine.

Immutability

A String object never changes after it is created. Every method that seems to modify a string returns a new string and leaves the original untouched:

String city = "chennai";
city.toUpperCase();                 // result discarded
System.out.println(city);           // chennai

city = city.toUpperCase();          // reassign the variable to the new string
System.out.println(city);           // CHENNAI
before:  city ──► "chennai"
after:   city ──► "CHENNAI"      "chennai" (unchanged, collected if unused)

The variable now points to a different object; the original object was never modified.

Why strings are immutable

  • Safety. A string passed to a method, stored in a collection, or used as a HashMap key cannot be changed by other code. A file path checked for permission cannot be altered after the check.
  • Thread safety. Immutable objects can be shared between threads with no synchronization.
  • Hash caching. The hash code is computed once and cached, making strings fast map keys.
  • Sharing. Equal literals can safely share one object, which is the basis of the string pool.

The cost is that repeatedly "modifying" a string creates many intermediate objects. For building text in loops, use StringBuilder, covered in its own lesson.

The String Pool

The JVM keeps a pool of string literals. Identical literals in the program refer to one shared object:

String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = s3.intern();

System.out.println(s1 == s2);        // true:  same pooled object
System.out.println(s1 == s3);        // false: new String creates a separate object
System.out.println(s1 == s4);        // true:  intern() returns the pooled instance
System.out.println(s1.equals(s3));   // true:  same characters

Compile-time constant expressions are also pooled, but strings built at runtime are not:

String c = "Ab";
String d = "A" + "b";        // computed by the compiler: pooled
String x = "A";
String y = x + "b";          // computed at runtime: a new object

System.out.println(c == d);  // true
System.out.println(c == y);  // false

This is why == sometimes appears to "work" for strings and sometimes does not. Always compare string content with equals (or equalsIgnoreCase). The String Compare lesson covers this in detail.

null, Empty, and Blank

Three different situations are easy to confuse:

ValueMeaninglength()isEmpty()isBlank()
nullNo string object at allNullPointerExceptionNullPointerExceptionNullPointerException
""A string with no characters0truetrue
" "Only whitespace3falsetrue
String input = "   ";
if (input == null || input.isBlank()) {
    System.out.println("Please enter a value");
}

Check for null first, because calling any method on null throws. isBlank() (Java 11) treats strings containing only whitespace as empty, which is usually what form validation needs.

Concatenating null does not throw; it produces the text "null":

String name = null;
System.out.println("Hello, " + name);     // Hello, null

This is a frequent source of odd output in logs and user interfaces; check values before building messages from them.

Converting To and From Strings

// To String
String a = String.valueOf(3.5);           // "3.5"
String b = Integer.toString(255, 16);     // "ff": base 16
String c = Boolean.toString(true);        // "true"
String d = String.valueOf(someObject);    // calls toString(), or "null" if the object is null

// From String
int n = Integer.parseInt("42");
double x = Double.parseDouble("3.14");
boolean flag = Boolean.parseBoolean("TRUE");   // true (case-insensitive)
LocalDate date = LocalDate.parse("2026-11-11");

String.valueOf(obj) is safer than obj.toString() because it handles null. Parsing methods throw exceptions (such as NumberFormatException or DateTimeParseException) for malformed input, so input from users or files should be validated.

Strings as CharSequence

String implements the CharSequence interface, as do StringBuilder and StringBuffer. Methods that accept CharSequence work with all of them:

static boolean hasDigit(CharSequence text) {
    for (int i = 0; i < text.length(); i++) {
        if (Character.isDigit(text.charAt(i))) return true;
    }
    return false;
}

hasDigit("abc123");                       // true
hasDigit(new StringBuilder("no digits")); // false

String.join, String.contains, and the regular-expression APIs accept CharSequence for this reason.

Practical Example: Username Registration

A sign-up form must normalize usernames, reject invalid ones, and prevent duplicates.

import java.util.*;

public class Usernames {
    static String normalize(String raw) {
        if (raw == null || raw.isBlank()) {
            throw new IllegalArgumentException("username is required");
        }
        String u = raw.strip().toLowerCase();
        if (u.length() < 3 || u.length() > 15) {
            throw new IllegalArgumentException("must be 3-15 characters: \"" + u + "\"");
        }
        for (char c : u.toCharArray()) {
            if (!(Character.isLetterOrDigit(c) && c < 128) && c != '_') {
                throw new IllegalArgumentException("invalid character '" + c + "' in \"" + u + "\"");
            }
        }
        return u;
    }

    public static void main(String[] args) {
        Set<String> taken = new HashSet<>();
        String[] requests = {"  Ada_Lovelace ", "ada_lovelace", "Linus", "x", "grace hopper", null, "Ravi99"};

        for (String r : requests) {
            try {
                String name = normalize(r);
                if (!taken.add(name)) {
                    System.out.println("TAKEN    " + name);
                } else {
                    System.out.println("OK       " + name);
                }
            } catch (IllegalArgumentException e) {
                System.out.println("REJECTED " + e.getMessage());
            }
        }
        System.out.println("Registered: " + new TreeSet<>(taken));
    }
}

Output:

OK       ada_lovelace
TAKEN    ada_lovelace
OK       linus
REJECTED must be 3-15 characters: "x"
REJECTED invalid character ' ' in "grace hopper"
REJECTED username is required
OK       ravi99
Registered: [ada_lovelace, linus, ravi99]

normalize checks for null and blank input before calling any other method on the string. strip() and toLowerCase() each return a new string, which is stored in u; the original raw is never changed. Because normalization happens before the duplicate check, " Ada_Lovelace " and "ada_lovelace" are recognized as the same name. The character loop allows only ASCII letters, digits, and underscores; the c < 128 check is needed because Character.isLetterOrDigit also accepts letters from other scripts. A HashSet<String> detects duplicates through equals and hashCode, which compare the characters, not object identity, so the normalized strings match even though they are different objects.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about String Basics and get answers drawn from it.

Signed-in readers only.