Char Array to String — Zeroing Doesn't Protect Passwords
Heap dumps expose plaintext passwords after char[] to String — zeroing the array doesn't protect the copy.
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
- Core concept: four methods convert char[] to String, but differ in null handling and intent
- String constructor: most common, throws NPE on null
- String.valueOf(): returns literal "null" for null input — cleaner for safe defaults
- StringBuilder.append(): useful when building strings incrementally, not for direct conversion
- Performance insight: all four methods copy the char array — O(n) time, O(n) memory
- Production insight: String.valueOf(null) returns "null" string, not null — silent bug if you expect null propagation
Converting a char[] to a String in Java is a common operation that becomes a security footgun when handling sensitive data like passwords. The core problem: once a String is created, its underlying char[] is immutable and can only be garbage-collected, not programmatically cleared.
Even if you zero out the original char[] after conversion, the String constructor or String.valueOf() internally copies the array into a new, permanent backing array. That copy persists in heap memory until GC reclaims it — and GC doesn't zero memory.
So your password lives on in plaintext in multiple locations, vulnerable to memory dumps, core dumps, or heap inspection tools like jmap or VisualVM. This is why security guidelines (OWASP, NIST) recommend using char[] over String for secrets, and why zeroing the source array after conversion is insufficient.
The four primary methods for char[] to String conversion are: new String(char[]), String.valueOf(char[]), String.copyValueOf(char[]), and new String(char[], int offset, int count) for partial conversion. Under the hood, new String(char[]) and String.valueOf(char[]) are functionally identical — both call Arrays.copyOf to create a defensive copy of the array. String.copyValueOf() is a deprecated wrapper that does the same thing.
The partial conversion variant (offset and count) is useful when you only need a substring from a larger buffer, but it still copies the specified range into a new array. None of these methods allow you to pass ownership of the original array to the String — the copy is mandatory to maintain immutability guarantees.
Security-wise, the only way to truly protect a password after conversion is to never convert it to a String in the first place. If you must convert (e.g., to pass to a legacy API that requires String), you should zero the original char[] immediately after conversion, but understand that the String copy remains.
Best practice: use char[] throughout your authentication flow, and when you absolutely need a String, use new String(char[]) and then call Arrays.fill(charArray, '\0') on the source. For zeroing to be effective, ensure the array reference is not leaked and that the JVM doesn't optimize away the fill (use Arrays.fill with a volatile write pattern or a dedicated clearPassword method).
Performance-wise, the overhead of copying is negligible for typical password lengths (tens of characters), but for large char arrays (e.g., reading entire files), the partial conversion variant can avoid allocating a full copy when you only need a slice. In high-throughput systems, prefer String.valueOf() for readability — it's identical to new in bytecode after JIT compilation.String()
A char array in Java is a sequence of individual characters. A String is an immutable object wrapping character data. Converting between them is a basic operation you'll need constantly — parsing input, processing text from legacy APIs, working with cryptography code that deals in char[] rather than String for security reasons.
char[] to String conversion comes up surprisingly often in real codebases. Security-conscious APIs (like Java's own KeyStore and JPasswordField) return passwords as char[] rather than String precisely because char arrays can be zeroed out after use, while String literals are interned and may linger in the heap. Understanding the conversion and knowing which method to use matters for correctness, not just convenience.
Why char[] to String Is a Security Footgun
Converting a char array to a String in Java is trivial — call new String(char[]) or String.valueOf(char[]). The core mechanic is that the String constructor copies the array contents into its internal, immutable byte[] (or char[] pre-Java 9). This means the original char array and the resulting String share no memory after construction. The problem: you cannot erase a String. Once created, its backing array lives in memory until garbage collection, and even then, the data may persist in heap dumps or swap files. Zeroing the source char array after conversion does nothing to the String's internal copy. This is why security guidelines (OWASP, CERT) mandate using char[] for passwords and clearing it immediately — but only if you never convert it to a String. The moment you call new String(passwordChars), you've created an immortal copy of the secret. In practice, this bites teams using libraries that accept only String parameters (e.g., JDBC connections, encryption APIs). The conversion is O(n) in time and space, but the real cost is the loss of control over secret lifetime.
new String(char[]) does not erase the String's internal copy. The secret remains in heap memory until GC — and possibly after.javax.security.auth.callback.PasswordCallback).new String(char[]) is theater; the copy persists.Four Methods for char[] to String Conversion
All four methods produce the same output for typical use cases. The differences matter at the margins: performance for large arrays, null-safety, and intent signalling. Here's a closer look at each one and where you'd use it in production.
package io.thecodeforge.strings; import java.util.Arrays; public class CharArrayConversionExample { public static void main(String[] args) { char[] chars = {'T', 'h', 'e', 'C', 'o', 'd', 'e', 'F', 'o', 'r', 'g', 'e'}; // Method 1: String constructor — most common and idiomatic String s1 = new String(chars); System.out.println("Constructor: " + s1); // Method 2: String.valueOf() — same implementation, cleaner intent String s2 = String.valueOf(chars); System.out.println("valueOf: " + s2); // Method 3: StringBuilder append — useful when building incrementally StringBuilder sb = new StringBuilder(); sb.append(chars); String s3 = sb.toString(); System.out.println("StringBuilder: " + s3); // Method 4: String.copyValueOf() — explicit copy semantics String s4 = String.copyValueOf(chars); System.out.println("copyValueOf: " + s4); // Partial range conversion — constructor with offset and count String partial = new String(chars, 3, 8); // offset=3, count=8 System.out.println("Partial [3,8]: " + partial); // CodeForg // Security use case: zero out the char array after conversion char[] password = {'s', 'e', 'c', 'r', 'e', 't'}; String passwordStr = new String(password); Arrays.fill(password, '\0'); // Overwrite sensitive data System.out.println("Password zeroed: " + Arrays.toString(password)); } }
- String constructor calls Arrays.copyOf internally — a full copy.
- String.valueOf(char[]) delegates to the same constructor.
- StringBuilder.append(char[]) copies into its own buffer first, then toString() copies again.
String.copyValueOf()is identical to valueOf — legacy alias.
Under the Hood: String Constructor vs String.valueOf()
Many engineers assume these two have different implementations. They don't. Open the OpenJDK source for String.class and you'll see valueOf(char data[]) simply returns new String(data). The only difference is how they handle null.
When you call new String((char[]) null), the JVM immediately throws NullPointerException before the constructor can even check the argument. But String.valueOf handles null differently: it returns the literal string "null". This is consistent with other valueOf methods in Java, but it often surprises developers expecting a null return.
So why have both? Intent. new String(chars) signals "I expect this to be non-null — fail early". String.valueOf(chars) signals "I accept null and want a safe string representation". Pick based on what your downstream code expects.
package io.thecodeforge.strings; public class NullHandlingExample { public static void main(String[] args) { char[] nullArray = null; // Throws NullPointerException // String s1 = new String(nullArray); // Returns "null" string String s2 = String.valueOf(nullArray); System.out.println("valueOf(null): '" + s2 + "'"); // Check length — it's 4, not 0 System.out.println("Length: " + s2.length()); // If you need null propagation: String s3 = nullArray == null ? null : new String(nullArray); System.out.println("Ternary result: " + s3); } }
String() and handle null separately.String.copyValueOf()Partial Char Array Conversion Using Offset and Count
Sometimes you have a char[] that contains more data than you need. Maybe you're parsing a fixed-width record from a mainframe, or you've received a buffer that includes headers. The String constructor accepts an offset and count: new String(chars, offset, length). This creates a String using only a subset of the array, starting at offset and taking length characters.
This is not the same as copying the array and then calling substring. The constructor directly reads only the specified range, avoiding an intermediate copy. It's both memory-efficient and faster than the alternative.
Watch out: if offset + count exceeds chars.length, you'll get a StringIndexOutOfBoundsException. Always validate bounds in production code, especially when the offset comes from untrusted input.
package io.thecodeforge.strings; public class PartialConversionExample { public static void main(String[] args) { char[] buffer = {'S', 'T', 'A', 'R', 'T', 't', 'h', 'e', 'C', 'o', 'd', 'e', 'F', 'o', 'r', 'g', 'e', 'E', 'N', 'D'}; int headerSize = 5; // 'START' int trailerSize = 3; // 'END' int dataOffset = headerSize; int dataLength = buffer.length - headerSize - trailerSize; // Convert only the data portion String data = new String(buffer, dataOffset, dataLength); System.out.println("Data portion: " + data); // theCodeForge // Unsafe — this throws StringIndexOutOfBoundsException // String bad = new String(buffer, 0, 100); // Safe conversion with validation int offset = 0; int length = 100; if (offset >= 0 && length >= 0 && offset + length <= buffer.length) { String safe = new String(buffer, offset, length); } else { throw new IllegalArgumentException("Invalid offset/length"); } } }
Security: Why char[] and How to Zero Out After Conversion
Java's security APIs (JPasswordField, KeyStore, Console.readPassword) return passwords as char[] instead of String for a specific reason: char arrays are mutable and can be cleared explicitly after use. String is immutable — once created, the backing array lives on the heap until garbage collected. And because String objects are interned, they can survive long after you discard the reference.
When you convert a char[] password to a String, you create an immutable copy. Even if you zero the original char[], the String still holds the password in its private array. The only way to truly clear it is to ensure the String object is garbage collected quickly — but you can't zero it.
Best practice: avoid converting char[] to String for sensitive data. If you must, null the String reference right after use and trust the GC (it's not immediate). For additional protection, use a char[] throughout the sensitive operation and only convert at the exact point that requires a String (e.g., passing to a legacy API).
package io.thecodeforge.security; import java.util.Arrays; public class SecurePasswordConversion { public static void main(String[] args) { char[] password = retrievePasswordFromSecureStore(); // Only convert when absolutely necessary String passwordString = null; try { passwordString = new String(password); authenticate(passwordString); } finally { // Zero original char array Arrays.fill(password, '\0'); // Nullify the String reference to help GC passwordString = null; } } private static char[] retrievePasswordFromSecureStore() { // Simulating a secure store return new char[]{'s', '3', 'c', 'r', '3', 't'}; } private static void authenticate(String password) { System.out.println("Authenticating with: " + password); } }
Performance Considerations: When Method Choice Matters
For most use cases, the performance difference between methods is negligible. All four methods ultimately copy the character data. But there are scenarios where method choice impacts memory and CPU.
For small arrays (< 1000 characters), any method works. For large arrays (millions of characters), use the direct constructor or valueOf — both make exactly one copy. StringBuilder.append makes two copies: first into its internal buffer (which may resize), then into the final String. String.copyValueOf is identical to valueOf but kept for legacy compatibility — no performance penalty.
Memory overhead: Each copy consumes 2 bytes per character (char in Java is UTF-16). A 1 million character array → 2 MB for the source + 2 MB for the String = 4 MB peak. StringBuilder adds another 2 MB during append (its buffer may be larger due to growth factor). Avoid StringBuilder for direct conversion.
Consider using the partial constructor (offset+count) when you only need a portion of the array. It avoids copying the entire input.
Java 8 Streams: The Convert-Char-Array-To-String-Using-Streams Trap
Streams are a hammer. Not every problem is a nail. Converting a char[] to String via streams is the kind of code that makes senior devs reach for their coffee and sigh. Why? Because you're boxing each primitive char into a Character object, streaming them, collecting them into an array, then building a String. That's three allocations and a boxing tax for something new String(chars) does in one native call.
If you're already in a stream pipeline and need to join char arrays into a single String, use Collectors.joining() with a StringBuilder internally. But if all you have is a char[], stop showing off. Use the constructor. Streams are for data transformation pipelines, not for reinventing String.valueOf() with extra steps and a memory footprint.
The real production sin? Using streams inside a tight loop. That's how you get GC pauses in your latency-sensitive components. Don't do it. Know your APIs. Pick the right tool.
// io.thecodeforge — java tutorial // Don't do this. Seriously. import java.util.stream.Collectors; import java.util.stream.IntStream; public class StreamsAntiPattern { public static void main(String[] args) { char[] payload = {'p', 'a', 'y', 'l', 'o', 'a', 'd'}; // This boxes every char into Character String result = new String(payload).chars() .mapToObj(c -> String.valueOf((char) c)) .collect(Collectors.joining()); System.out.println(result); // Versus this String correct = new String(payload); System.out.println(correct); } }
The copyValueOf() Ghost: Why You Should Never Use This Method in Modern Java
Some methods should have been deprecated years ago. String.copyValueOf() is one of them. It's a relic from Java 1.0 that does exactly what String.valueOf(char[]) does — calls the same String constructor internally. There's zero difference in behavior or performance. It's a redundant method that only exists because the original API designers didn't clean up after themselves.
Why does this matter? Because I've seen codebases where junior devs use copyValueOf() because "it sounds more explicit." It's not. It's confusing. Anyone maintaining your code has to stop and think: "Is this doing something special with defensive copying?" The answer is no. It's a copy of valueOf() in name only.
If you find this in a code review, flag it. Replace it with new String(chars) or String.valueOf(chars). Less cognitive overhead. Consistent with the rest of the codebase. And it signals you understand Java's evolution rather than blindly cargo-culting 90s APIs.
// io.thecodeforge — java tutorial import java.util.Arrays; public class CopyValueOfRedundancy { public static void main(String[] args) { char[] secret = {'s', 'e', 'c', 'r', 'e', 't'}; // Don't String obsolete = String.copyValueOf(secret); // Do String correct = String.valueOf(secret); // Identical output System.out.println(obsolete.equals(correct)); // true // Proof they're the same implementation: System.out.println(Arrays.equals( obsolete.toCharArray(), correct.toCharArray() )); // true } }
String.copyValueOf() is pure redundancy. Never use it. Replace with String.valueOf() or the constructor.1. Overview
Converting a Java char array to a String seems trivial, yet it's a gateway to understanding how Java manages memory, security, and performance. At its core, the operation must translate a mutable, indexed sequence of 16-bit Unicode characters into an immutable, interned-capable String object. The Java platform offers multiple roads to this destination: the String constructor, String.valueOf(), StringBuilder, and legacy methods like copyValueOf(). Each path carries distinct runtime characteristics, memory implications, and security semantics. A senior engineer selects not the first method that comes to mind, but the one that best fits the context—whether that's minimizing object allocations in a hot loop, preventing sensitive data from lingering in the heap, or safely carving a substring from a buffer. This section lays the foundation by framing the conversion as a design decision, not a mechanical step.
// io.thecodeforge — java tutorial // Overview: char[] -> String conversion public class OverviewExample { public static void main(String[] args) { char[] chars = {'H', 'e', 'l', 'l', 'o'}; String str = new String(chars); System.out.println(str); } }
3. String.valueOf()
String.valueOf(char[]) is often the recommended choice for converting a character array to a String. Under the hood, it simply calls the String constructor that accepts a char array, making it functionally equivalent to new String(charArray). Why prefer valueOf over the constructor? Primarily for semantic clarity and type safety: the method name communicates intent ('give me the string representation'), and it avoids the visual noise of a constructor call. Furthermore, valueOf is null-safe for other primitive overloads (like valueOf(int)), but for char arrays, passing null will throw a NullPointerException—identical behavior to the constructor. In performance-critical code, both generate identical bytecode after inlining. The real advantage shines when dealing with method references or lambda expressions, where String::valueOf reads more naturally than a constructor reference. Use valueOf when you want self-documenting code that signals 'convert this to a String' rather than 'create a new String instance'.
// io.thecodeforge — java tutorial // String.valueOf() usage public class ValueOfExample { public static void main(String[] args) { char[] password = {'s', 'e', 'c', 'r', 'e', 't'}; String pwdStr = String.valueOf(password); System.out.println(pwdStr); // Clear sensitive data java.util.Arrays.fill(password, '\u0000'); } }
String.valueOf() for clarity; it's bytecode-identical to the constructor but communicates purpose better.7. Conclusion
Converting a Java char[] to a String is far more than a syntactic exercise—it's a decision that impacts security, performance, and code maintainability. From the mutable safety of char arrays for sensitive data (and the imperative to zero them after use), to the subtle bytecode equivalence of String.valueOf() and the constructor, each approach has its role. Avoid legacy pitfalls like copyValueOf() and resist the temptation of Java 8 Streams for this trivial operation. When you need partial conversions, the offset-and-count overloads of both the constructor and valueOf give precise control without creating intermediate arrays. The best practice, distilled: use String.valueOf() for clarity, zero char arrays containing secrets, and never sacrifice correctness for micro-optimizations. Your choice tells a story about your understanding of Java's memory model and your commitment to secure, performant code. Choose wisely.
// io.thecodeforge — java tutorial // Conclusion: recommended pattern public class ConclusionExample { public static String safeConversion(char[] data) { String result = String.valueOf(data); java.util.Arrays.fill(data, '\u0000'); return result; } }
The Password Leak That Could Have Been Avoided
- Zeroing a char[] after conversion to String does NOT protect the copy inside the String object.
- If you must convert a password char[] to String, minimise its lifetime and null the reference immediately after use.
- Prefer char[] for sensitive data throughout the entire code path when possible.
System.out.println("chars == null: " + (chars == null));String s = chars == null ? null : new String(chars);System.out.println(chars.getClass().getName()); // [CString s = new String(chars); // correctSystem.out.println("Char array length: " + chars.length);If > 10 million, consider incremental StringBuilder with append(char[], int, int)jcmd <pid> GC.heap_dump /tmp/dump.hprofAnalyze dump with Eclipse MAT, search for password field stringsSystem.out.println("Offset=" + offset + ", count=" + count + ", length=" + chars.length);Ensure offset + count <= chars.lengthIllegalArgumentException();| Method | Syntax | Null-safe? | Use When | Copies |
|---|---|---|---|---|
| String constructor | new String(chars) | NPE on null | Default choice — clear intent, fail fast on null | 1 copy |
String.valueOf() | String.valueOf(chars) | Returns 'null' string | Null safety matters, safe fallback token | 1 copy |
StringBuilder.append() | sb.append(chars).toString() | No | Building incrementally from multiple sources | 2 copies |
String.copyValueOf() | String.copyValueOf(chars) | NPE on null | Legacy code compatibility, explicit copy semantics | 1 copy |
Key takeaways
String.copyValueOf() is a legacy alias for valueOfCommon mistakes to avoid
5 patternsUsing Arrays.toString(chars) for conversion
Not zeroing the char array after converting a password
Calling String.valueOf(null) expecting null back
Using StringBuilder.append(chars) for a single array conversion
Forgetting to validate offset and count in partial conversion
IllegalArgumentException();Interview Questions on This Topic
What are the different ways to convert a char array to a String in Java?
Why do some security APIs return char[] instead of String for passwords?
What happens if you pass null to String.valueOf(char[]) vs new String(char[])?
Is there any performance difference between the four conversion methods for large char arrays?
String.copyValueOf() is identical to valueOf. For large arrays (millions of characters), the StringBuilder approach can double memory usage and increase GC pressure. Always prefer the constructor or valueOf for direct conversion.How do you convert only a portion of a char array to a String?
Frequently Asked Questions
The most common approach is new String(chars) or String.valueOf(chars) — both produce the same result. Use String.valueOf() if you need null-safe behaviour (it returns the literal string 'null' for a null input rather than throwing NullPointerException).
Arrays.toString() formats the array as a string representation: '[T, h, e, C, o, d, e]' with brackets and commas between each character. Use new String(chars) or String.valueOf(chars) to concatenate the characters without any formatting.
No. String is immutable and must have its own backing array. All methods copy the char data. The closest you can get is using StringBuilder or StringBuffer, but they also copy internally. For read-only access, you could create a custom CharSequence wrapper over the char[], but that is not a String.
It is not formally deprecated in modern Java, but it is a legacy method. Its implementation is identical to String.valueOf(char[]). Oracle recommends using valueOf or the String constructor for new code. copyValueOf exists solely for backward compatibility with pre-Java 2 code.
No. The String constructor copies the characters into its own array. Zeroing the original char[] does not affect the String's internal array. The String persists in the heap until garbage collected, and there is no safe way to explicitly clear it. For sensitive data, avoid conversion to String entirely.
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
That's Strings. Mark it forged?
7 min read · try the examples if you haven't