Java String Characters: charAt, chars(), toCharArray & the Character Class

A Java String is a sequence of char values, where each char is a 16-bit UTF-16 code unit. charAt(index) retrieves a single char; chars() returns an IntStream of char values for stream-based processing; toCharArray() gives you a mutable copy as a char[]. The Character class wraps char and provides static utility methods for classification (isDigit, isLetter, isWhitespace) and case conversion. This article covers all four plus the Unicode supplementary character subtlety that trips up many developers.

charAt(): Accessing a Single Character by Index

A String in Java is internally stored as a UTF-16 encoded sequence. Each position is a char: a 16-bit unsigned value. charAt(int index) returns the char at the given zero-based index. Like array access, the index must be between 0 (inclusive) and length() (exclusive); any out-of-bounds index throws StringIndexOutOfBoundsException.

A char is a numeric type: it can be used in arithmetic, compared with ==, and widened to int automatically. Its numeric value is the Unicode code point for characters in the Basic Multilingual Plane (BMP), which covers the vast majority of everyday characters.

Java
String s = "Hello";

char first = s.charAt(0);   // 'H'
char last  = s.charAt(s.length() - 1); // 'o'
System.out.println(first);  // H
System.out.println((int) first); // 72 — the Unicode code point for 'H'

// Iterate over characters with charAt
for (int i = 0; i < s.length(); i++) {
    char c = s.charAt(i);
    System.out.print(c + " "); // H e l l o
}

// Count vowels using charAt
long vowels = 0;
String text = "Hello World";
for (int i = 0; i < text.length(); i++) {
    char c = Character.toLowerCase(text.charAt(i));
    if ("aeiou".indexOf(c) >= 0) vowels++;
}
System.out.println("Vowels: " + vowels); // 3

chars() and codePoints(): Stream-Based Character Processing

chars(), added in Java 8, returns an IntStream where each element is the char value (as an int) at the corresponding position. Because IntStream does not have a CharStream equivalent in Java, you must cast back to char when you need the character type. chars() lets you apply all the Stream API power (filter, map, distinct, count, sorted) directly on a string's characters without a for-loop.

codePoints() is similar but produces Unicode code points rather than raw UTF-16 code units. For strings containing only BMP characters, both methods give the same result. For strings with emoji or rare Unicode characters outside the BMP (supplementary characters), codePoints() is correct: it returns each character as a single int code point, while chars() returns the two surrogate pair code units separately.

Java
String s = "Hello, World!";

// Count distinct letters (case-insensitive)
long distinctLetters = s.chars()
    .map(Character::toLowerCase)
    .filter(Character::isLetter)
    .distinct()
    .count();
System.out.println("Distinct letters: " + distinctLetters); // 8

// Count digits in a string
String mixed = "abc123def456";
long digitCount = mixed.chars()
    .filter(Character::isDigit)
    .count();
System.out.println("Digits: " + digitCount); // 6

// Reverse a string using chars()
String reversed = new StringBuilder(s).reverse().toString();
// Or with streams:
String rev2 = s.chars()
    .collect(StringBuilder::new,
             (sb, c) -> sb.insert(0, (char) c),
             StringBuilder::append)
    .toString();

// Frequency map of characters
Map<Character, Long> freq = "banana".chars()
    .mapToObj(c -> (char) c)
    .collect(java.util.stream.Collectors.groupingBy(
        c -> c, java.util.stream.Collectors.counting()));
System.out.println(freq); // {a=3, b=1, n=2}

// codePoints() — correct for strings with supplementary characters (emoji)
String withEmoji = "Hello \uD83D\uDE00"; // 😀 is U+1F600
// 8 (surrogate pair = 2)
System.out.println("chars length:      " + withEmoji.chars().count());
System.out.println("codePoints length: " + withEmoji.codePoints().count());  // 7 (emoji = 1)
Use codePoints() instead of chars() whenever your strings might contain emoji, mathematical symbols, rare CJK characters, or any character outside the Unicode BMP (code points above U+FFFF). chars() is fine for ASCII and most Western European text.

toCharArray(): Mutable Character Array

toCharArray() copies the string's characters into a new char[]. Because String is immutable, any character-level manipulation (sorting characters, swapping positions, building an anagram) requires working on a mutable copy. toCharArray() provides that copy. Changes to the array do not affect the original string, and changes to the original string (not possible since it's immutable) would not affect the array.

The array is also the right way to handle sensitive data such as passwords: a char[] can be zeroed out with Arrays.fill(arr, '\0') after use, whereas a String lingers in the JVM's string pool and heap until garbage collected.

Java
String s = "hello";
char[] chars = s.toCharArray();   // {'h', 'e', 'l', 'l', 'o'}

// Sort characters alphabetically
java.util.Arrays.sort(chars);
System.out.println(new String(chars)); // "ehllo"

// Check if two strings are anagrams
boolean isAnagram(String a, String b) {
    if (a.length() != b.length()) return false;
    char[] ca = a.toLowerCase().toCharArray();
    char[] cb = b.toLowerCase().toCharArray();
    java.util.Arrays.sort(ca);
    java.util.Arrays.sort(cb);
    return java.util.Arrays.equals(ca, cb);
}
System.out.println(isAnagram("listen", "silent")); // true
System.out.println(isAnagram("hello", "world"));   // false

// Build a string from a char array after modification
char[] letters = {'J', 'a', 'v', 'a'};
letters[0] = 'D';
String modified = new String(letters); // "Dava"

// Iterate a char[] — slightly faster than charAt() in tight loops
for (char c : "iterate me".toCharArray()) {
    System.out.print(c);
}

The Character Class: Classification and Conversion

The Character class is the wrapper type for char primitives. Beyond boxing, its real value is its collection of static utility methods for classifying and converting characters. These methods handle Unicode correctly, for example, Character.isLetter('é') returns true even though 'é' is not in the ASCII range, because it is defined as a letter in the Unicode standard.

Java
// Classification methods
System.out.println(Character.isDigit('5'));       // true
System.out.println(Character.isDigit('a'));       // false
System.out.println(Character.isLetter('a'));      // true
System.out.println(Character.isLetter('é'));      // true (Unicode-aware)
System.out.println(Character.isLetterOrDigit('5')); // true
System.out.println(Character.isWhitespace(' ')); // true
System.out.println(Character.isWhitespace('\t')); // true
System.out.println(Character.isUpperCase('A'));   // true
System.out.println(Character.isLowerCase('a'));   // true
System.out.println(Character.isAlphabetic('a'));  // true (broader than isLetter)

// Case conversion
System.out.println(Character.toUpperCase('a')); // A
System.out.println(Character.toLowerCase('Z')); // z
System.out.println(Character.toUpperCase('é')); // É (handles accented chars)

// Numeric value of a digit character
System.out.println(Character.getNumericValue('7'));  // 7
System.out.println(Character.getNumericValue('a'));  // 10 (hex digit)
System.out.println(Character.digit('F', 16));        // 15 (parse hex digit)

// Practical: remove all non-alphanumeric characters
String cleaned = "Hello, World! 123"
    .chars()
    .filter(Character::isLetterOrDigit)
    .collect(StringBuilder::new,
             (sb, c) -> sb.append((char) c),
             StringBuilder::append)
    .toString();
System.out.println(cleaned); // HelloWorld123

Note: Prefer Character.isWhitespace() over comparing with ' ': it also recognises tab (\t), newline (\n), carriage return (\r), form feed (\f), and the Unicode no-break space variants. For HTML/XML whitespace semantics, check the Java documentation for the full list.