Caesar Cipher in Java: Complete Implementation Guide
Learn how to implement Caesar cipher in Java with complete code examples. Covers basic encryption, OOP design with a CaesarCipher class, file encryption, unit testing, and a command-line tool.
The Caesar cipher is one of the simplest and most widely known encryption techniques in cryptography. For Java developers, implementing this cipher provides an excellent opportunity to practice core language features including object-oriented programming, file I/O, string manipulation, and unit testing. Whether you are a computer science student completing a class assignment or a developer brushing up on cryptographic fundamentals, this guide walks you through progressively more sophisticated Java implementations.
In this tutorial, you will build five complete programs: a basic encrypt/decrypt function, a full object-oriented CaesarCipher class, a file encryption utility, a JUnit test suite, and a polished command-line tool. Every code example compiles and runs with standard Java 17 or later.
Try It Online: Want to experiment with Caesar cipher encryption before writing code? Use our free Caesar Cipher Encoder to see how different shift values transform your text.
Understanding Caesar Cipher Logic in Java
The Caesar cipher works by shifting each letter in the plaintext by a fixed number of positions in the alphabet. For example, with a shift of 3, the letter A becomes D, B becomes E, and so on. Letters wrap around so that X becomes A, Y becomes B, and Z becomes C.
In Java, you can leverage the fact that char values are numeric. The uppercase letters A through Z occupy ASCII values 65 through 90, and lowercase letters a through z occupy 97 through 122. By performing arithmetic on these character values and using the modulo operator, you can implement the shift cleanly without lookup tables.
The core encryption formula is:
encryptedChar = (char - base + shift) % 26 + base
where base is 'A' for uppercase or 'a' for lowercase letters, and shift is the number of positions to move forward. Decryption simply uses the inverse shift (26 minus the encryption shift).
Basic Caesar Cipher Implementation
The simplest approach uses two static methods for encryption and decryption. This version handles both uppercase and lowercase letters while leaving non-alphabetic characters untouched.
public class BasicCaesarCipher {
public static String encrypt(String plaintext, int shift) {
// Normalize shift to 0-25 range
shift = ((shift % 26) + 26) % 26;
StringBuilder result = new StringBuilder();
for (char ch : plaintext.toCharArray()) {
if (Character.isUpperCase(ch)) {
char encrypted = (char) ((ch - 'A' + shift) % 26 + 'A');
result.append(encrypted);
} else if (Character.isLowerCase(ch)) {
char encrypted = (char) ((ch - 'a' + shift) % 26 + 'a');
result.append(encrypted);
} else {
// Non-alphabetic characters remain unchanged
result.append(ch);
}
}
return result.toString();
}
public static String decrypt(String ciphertext, int shift) {
// Decryption is encryption with the inverse shift
return encrypt(ciphertext, 26 - (((shift % 26) + 26) % 26));
}
public static void main(String[] args) {
String message = "Hello, World! The quick brown fox jumps over the lazy dog.";
int shift = 7;
String encrypted = encrypt(message, shift);
String decrypted = decrypt(encrypted, shift);
System.out.println("Original: " + message);
System.out.println("Encrypted: " + encrypted);
System.out.println("Decrypted: " + decrypted);
}
}
When you run this program, the output is:
Original: Hello, World! The quick brown fox jumps over the lazy dog.
Encrypted: Olssv, Dvysk! Aol xbpjr iyvdu mve qbtwz vcly aol shgf kvn.
Decrypted: Hello, World! The quick brown fox jumps over the lazy dog.
A few important details in this implementation:
- Shift normalization: The expression
((shift % 26) + 26) % 26ensures the shift is always in the 0 to 25 range, even when negative values are passed. Java's modulo operator can return negative values for negative operands, so the double-modulo pattern handles this edge case. - StringBuilder: Using
StringBuilderrather than string concatenation is more efficient for building the result character by character, especially with longer texts. - Character preservation: Digits, punctuation, spaces, and any other non-letter characters pass through unchanged, which is the standard behavior for classical Caesar cipher implementations.
Object-Oriented CaesarCipher Class
For real-world Java projects, an object-oriented design is more appropriate. The following CaesarCipher class encapsulates the shift value as instance state, supports method chaining, and provides additional utility methods including brute-force decryption.
import java.util.ArrayList;
import java.util.List;
public class CaesarCipher {
private final int shift;
/**
* Creates a CaesarCipher with the given shift value.
* The shift is normalized to the 0-25 range.
*
* @param shift the number of positions to shift letters
*/
public CaesarCipher(int shift) {
this.shift = ((shift % 26) + 26) % 26;
}
/**
* Returns the normalized shift value.
*/
public int getShift() {
return shift;
}
/**
* Encrypts the given plaintext using this cipher's shift value.
*
* @param plaintext the text to encrypt
* @return the encrypted ciphertext
*/
public String encrypt(String plaintext) {
if (plaintext == null) {
throw new IllegalArgumentException("Input text cannot be null");
}
return transform(plaintext, shift);
}
/**
* Decrypts the given ciphertext using this cipher's shift value.
*
* @param ciphertext the text to decrypt
* @return the decrypted plaintext
*/
public String decrypt(String ciphertext) {
if (ciphertext == null) {
throw new IllegalArgumentException("Input text cannot be null");
}
return transform(ciphertext, 26 - shift);
}
/**
* Performs a brute force attack, returning all 26 possible decryptions.
*
* @param ciphertext the text to brute force
* @return a list of all possible decryptions with their shift values
*/
public static List<String> bruteForce(String ciphertext) {
List<String> results = new ArrayList<>();
for (int s = 0; s < 26; s++) {
CaesarCipher cipher = new CaesarCipher(s);
results.add("Shift " + String.format("%2d", s) + ": " + cipher.decrypt(ciphertext));
}
return results;
}
private static String transform(String text, int shift) {
StringBuilder sb = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (ch >= 'A' && ch <= 'Z') {
sb.append((char) ((ch - 'A' + shift) % 26 + 'A'));
} else if (ch >= 'a' && ch <= 'z') {
sb.append((char) ((ch - 'a' + shift) % 26 + 'a'));
} else {
sb.append(ch);
}
}
return sb.toString();
}
@Override
public String toString() {
return "CaesarCipher{shift=" + shift + "}";
}
public static void main(String[] args) {
CaesarCipher cipher = new CaesarCipher(13);
System.out.println(cipher);
String original = "Attack at dawn!";
String encrypted = cipher.encrypt(original);
String decrypted = cipher.decrypt(encrypted);
System.out.println("Original: " + original);
System.out.println("Encrypted: " + encrypted);
System.out.println("Decrypted: " + decrypted);
System.out.println("\n--- Brute Force Attack ---");
String mystery = "Xlmw mw e wigvix qiwweki!";
List<String> attempts = CaesarCipher.bruteForce(mystery);
for (String attempt : attempts) {
System.out.println(attempt);
}
}
}
This class design follows several Java best practices:
- Immutability: The
shiftfield isfinal, making instances thread-safe and predictable. - Validation: Null input throws
IllegalArgumentExceptionimmediately rather than causing aNullPointerExceptiondeeper in the code. - Encapsulation: The
transformmethod is private since it is an implementation detail not needed by consumers of the class. - Static utility: The
bruteForcemethod is static because it does not depend on any particular instance's shift value.
Encrypting Files with Caesar Cipher
Processing files is a common requirement for cipher implementations. The following program reads a text file, encrypts its contents, and writes the result to a new file. It uses Java NIO for modern, efficient file handling.
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class CaesarFileEncryptor {
/**
* Encrypts the contents of a file and writes the result to an output file.
*
* @param inputPath path to the plaintext file
* @param outputPath path for the encrypted output file
* @param shift the Caesar cipher shift value
* @throws IOException if file reading or writing fails
*/
public static void encryptFile(Path inputPath, Path outputPath, int shift)
throws IOException {
CaesarCipher cipher = new CaesarCipher(shift);
String content = Files.readString(inputPath, StandardCharsets.UTF_8);
String encrypted = cipher.encrypt(content);
Files.writeString(outputPath, encrypted,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}
/**
* Decrypts the contents of a file and writes the result to an output file.
*
* @param inputPath path to the encrypted file
* @param outputPath path for the decrypted output file
* @param shift the Caesar cipher shift value used for encryption
* @throws IOException if file reading or writing fails
*/
public static void decryptFile(Path inputPath, Path outputPath, int shift)
throws IOException {
CaesarCipher cipher = new CaesarCipher(shift);
String content = Files.readString(inputPath, StandardCharsets.UTF_8);
String decrypted = cipher.decrypt(content);
Files.writeString(outputPath, decrypted,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}
public static void main(String[] args) {
if (args.length < 4) {
System.err.println("Usage: java CaesarFileEncryptor <encrypt|decrypt> <shift> <input> <output>");
System.exit(1);
}
String mode = args[0].toLowerCase();
int shift = Integer.parseInt(args[1]);
Path input = Path.of(args[2]);
Path output = Path.of(args[3]);
try {
if ("encrypt".equals(mode)) {
encryptFile(input, output, shift);
System.out.println("File encrypted successfully: " + output);
} else if ("decrypt".equals(mode)) {
decryptFile(input, output, shift);
System.out.println("File decrypted successfully: " + output);
} else {
System.err.println("Invalid mode. Use 'encrypt' or 'decrypt'.");
System.exit(1);
}
} catch (IOException e) {
System.err.println("Error processing file: " + e.getMessage());
System.exit(1);
}
}
}
Usage from the command line:
# Encrypt a file with shift of 5
java CaesarFileEncryptor encrypt 5 secret.txt secret.enc
# Decrypt the file
java CaesarFileEncryptor decrypt 5 secret.enc secret_decrypted.txt
This implementation uses Files.readString() and Files.writeString(), which are available in Java 11 and later. For very large files that do not fit in memory, you would want to use a streaming approach with BufferedReader and BufferedWriter instead.
Unit Testing with JUnit 5
Thorough testing is essential for any encryption implementation. The following JUnit 5 test class covers normal operation, edge cases, and the relationship between encryption and decryption.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
class CaesarCipherTest {
@Test
@DisplayName("Encrypt with shift of 3 produces correct output")
void testBasicEncryption() {
CaesarCipher cipher = new CaesarCipher(3);
assertEquals("Khoor Zruog", cipher.encrypt("Hello World"));
}
@Test
@DisplayName("Decrypt reverses encryption")
void testDecryptReversesEncrypt() {
CaesarCipher cipher = new CaesarCipher(7);
String original = "The quick brown fox jumps over the lazy dog.";
String encrypted = cipher.encrypt(original);
assertEquals(original, cipher.decrypt(encrypted));
}
@ParameterizedTest
@ValueSource(ints = {0, 1, 5, 13, 25, -1, -13, 52, -52})
@DisplayName("Encrypt then decrypt returns original for all shift values")
void testRoundTripAllShifts(int shift) {
CaesarCipher cipher = new CaesarCipher(shift);
String original = "ABCxyz 123!@#";
assertEquals(original, cipher.decrypt(cipher.encrypt(original)));
}
@Test
@DisplayName("Non-alphabetic characters are preserved")
void testNonAlphaPreserved() {
CaesarCipher cipher = new CaesarCipher(10);
String input = "12345 !@#$% \t\n";
assertEquals(input, cipher.encrypt(input));
}
@Test
@DisplayName("Empty string returns empty string")
void testEmptyString() {
CaesarCipher cipher = new CaesarCipher(5);
assertEquals("", cipher.encrypt(""));
assertEquals("", cipher.decrypt(""));
}
@Test
@DisplayName("Shift of 0 returns original text")
void testShiftZero() {
CaesarCipher cipher = new CaesarCipher(0);
String text = "No change expected!";
assertEquals(text, cipher.encrypt(text));
}
@Test
@DisplayName("Shift of 26 is equivalent to shift of 0")
void testShiftTwentySix() {
CaesarCipher cipher = new CaesarCipher(26);
assertEquals(0, cipher.getShift());
assertEquals("Hello", cipher.encrypt("Hello"));
}
@ParameterizedTest
@CsvSource({
"abc, 1, bcd",
"xyz, 3, abc",
"ABC, 1, BCD",
"XYZ, 3, ABC",
"Hello, 13, Uryyb"
})
@DisplayName("Known encryption pairs are correct")
void testKnownPairs(String input, int shift, String expected) {
CaesarCipher cipher = new CaesarCipher(shift);
assertEquals(expected, cipher.encrypt(input));
}
@Test
@DisplayName("Null input throws IllegalArgumentException")
void testNullInput() {
CaesarCipher cipher = new CaesarCipher(5);
assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(null));
assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(null));
}
@Test
@DisplayName("Brute force returns 26 results")
void testBruteForceSize() {
var results = CaesarCipher.bruteForce("test");
assertEquals(26, results.size());
}
@Test
@DisplayName("Case is preserved during encryption")
void testCasePreservation() {
CaesarCipher cipher = new CaesarCipher(5);
String encrypted = cipher.encrypt("HeLLo WoRLd");
assertEquals("MjQQt BtWQi", encrypted);
}
}
Run the tests with Maven or Gradle:
# Maven
mvn test
# Gradle
gradle test
The test suite covers several important categories:
- Correctness: Verifying known encryption outputs for specific inputs and shift values.
- Round-trip integrity: Ensuring that decrypting an encrypted message returns the original text for every possible shift value.
- Edge cases: Empty strings, shift of zero, shift of 26, negative shifts, and shifts larger than 26.
- Character handling: Non-alphabetic characters pass through unchanged, and letter case is preserved.
- Error handling: Null input throws the expected exception.
Building a Command-Line Tool
The final example brings everything together into a polished command-line application with argument parsing, multiple operation modes, and user-friendly output. This tool supports encryption, decryption, and brute-force cracking.
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Scanner;
public class CaesarCipherCLI {
private static final String VERSION = "1.0.0";
public static void main(String[] args) {
if (args.length == 0) {
printUsage();
System.exit(0);
}
String command = args[0].toLowerCase();
switch (command) {
case "encrypt", "enc", "e" -> handleEncrypt(args);
case "decrypt", "dec", "d" -> handleDecrypt(args);
case "bruteforce", "brute", "bf" -> handleBruteForce(args);
case "interactive", "i" -> handleInteractive();
case "--version", "-v" -> System.out.println("Caesar Cipher CLI v" + VERSION);
case "--help", "-h" -> printUsage();
default -> {
System.err.println("Unknown command: " + command);
printUsage();
System.exit(1);
}
}
}
private static void handleEncrypt(String[] args) {
if (args.length < 3) {
System.err.println("Usage: encrypt <shift> <text|--file path>");
System.exit(1);
}
int shift = parseShift(args[1]);
String text = resolveText(args, 2);
CaesarCipher cipher = new CaesarCipher(shift);
System.out.println(cipher.encrypt(text));
}
private static void handleDecrypt(String[] args) {
if (args.length < 3) {
System.err.println("Usage: decrypt <shift> <text|--file path>");
System.exit(1);
}
int shift = parseShift(args[1]);
String text = resolveText(args, 2);
CaesarCipher cipher = new CaesarCipher(shift);
System.out.println(cipher.decrypt(text));
}
private static void handleBruteForce(String[] args) {
if (args.length < 2) {
System.err.println("Usage: bruteforce <text|--file path>");
System.exit(1);
}
String text = resolveText(args, 1);
System.out.println("=== Brute Force Results ===\n");
var results = CaesarCipher.bruteForce(text);
for (String result : results) {
System.out.println(result);
}
}
private static void handleInteractive() {
Scanner scanner = new Scanner(System.in);
System.out.println("Caesar Cipher Interactive Mode");
System.out.println("Type 'quit' to exit.\n");
while (true) {
System.out.print("Enter command (encrypt/decrypt/bruteforce): ");
String command = scanner.nextLine().trim().toLowerCase();
if ("quit".equals(command) || "exit".equals(command)) {
System.out.println("Goodbye!");
break;
}
if ("bruteforce".equals(command) || "bf".equals(command)) {
System.out.print("Enter ciphertext: ");
String text = scanner.nextLine();
var results = CaesarCipher.bruteForce(text);
results.forEach(System.out::println);
} else if ("encrypt".equals(command) || "decrypt".equals(command)) {
System.out.print("Enter shift value: ");
int shift = Integer.parseInt(scanner.nextLine().trim());
System.out.print("Enter text: ");
String text = scanner.nextLine();
CaesarCipher cipher = new CaesarCipher(shift);
String result = "encrypt".equals(command)
? cipher.encrypt(text)
: cipher.decrypt(text);
System.out.println("Result: " + result);
} else {
System.out.println("Unknown command. Try encrypt, decrypt, or bruteforce.");
}
System.out.println();
}
scanner.close();
}
private static String resolveText(String[] args, int startIndex) {
if ("--file".equals(args[startIndex])) {
if (args.length <= startIndex + 1) {
System.err.println("Error: --file requires a file path argument.");
System.exit(1);
}
try {
return Files.readString(Path.of(args[startIndex + 1]), StandardCharsets.UTF_8);
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
System.exit(1);
return "";
}
}
// Join remaining arguments as the text
StringBuilder sb = new StringBuilder();
for (int i = startIndex; i < args.length; i++) {
if (i > startIndex) sb.append(" ");
sb.append(args[i]);
}
return sb.toString();
}
private static int parseShift(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
System.err.println("Error: Shift must be an integer. Got: " + value);
System.exit(1);
return 0;
}
}
private static void printUsage() {
System.out.println("""
Caesar Cipher CLI v%s
Usage:
caesar encrypt <shift> <text> Encrypt text with given shift
caesar decrypt <shift> <text> Decrypt text with given shift
caesar bruteforce <text> Try all 26 possible shifts
caesar encrypt <shift> --file <path> Encrypt a file's contents
caesar interactive Start interactive mode
Options:
--help, -h Show this help message
--version, -v Show version number
Examples:
caesar encrypt 3 "Hello World"
caesar decrypt 3 "Khoor Zruog"
caesar bruteforce "Khoor Zruog"
caesar encrypt 5 --file secret.txt
""".formatted(VERSION));
}
}
Compile and run the tool:
# Compile all files
javac CaesarCipher.java CaesarCipherCLI.java
# Encrypt a message
java CaesarCipherCLI encrypt 13 "Meet me at midnight"
# Decrypt a message
java CaesarCipherCLI decrypt 13 "Zrrg zr ng zvqavtug"
# Brute force an unknown cipher
java CaesarCipherCLI bruteforce "Wklv lv d vhfuhw phvvdjh"
# Encrypt from a file
java CaesarCipherCLI encrypt 7 --file message.txt
# Interactive mode
java CaesarCipherCLI interactive
This command-line tool demonstrates several advanced Java features:
- Switch expressions with arrow syntax (Java 14+)
- Text blocks for multi-line strings (Java 15+)
- String formatting with the
formatted()method - Var for local variables where the type is clear from context
Performance Considerations
The Caesar cipher itself is computationally trivial with O(n) time complexity where n is the length of the input text. However, there are a few performance tips worth considering for Java implementations:
StringBuilder capacity: When you know the output length in advance (it equals the input length for Caesar cipher), pre-allocate the StringBuilder with new StringBuilder(text.length()). This avoids internal buffer resizing.
Character array vs charAt: For very long strings, converting to a char[] with toCharArray() and iterating over the array can be slightly faster than calling charAt(i) in a loop, since it avoids bounds checking on each access. However, this creates an extra copy of the string data.
Stream-based approach: While you could use Java Streams to process characters, the traditional loop is faster for this type of character-by-character transformation due to the overhead of boxing char values to Character objects.
Common Pitfalls and How to Avoid Them
When implementing Caesar cipher in Java, watch out for these frequent mistakes:
Negative modulo: Java's % operator returns negative results for negative operands. The expression -3 % 26 evaluates to -3, not 23. Always normalize with the double-modulo pattern: ((value % 26) + 26) % 26.
Character type overflow: When adding a shift to a char value, the result is promoted to int. You must cast it back to char explicitly, or your StringBuilder will append the integer value instead of the character.
Forgetting to handle both cases: A common bug is encrypting uppercase letters correctly but forgetting lowercase letters (or vice versa). Always test with mixed-case input.
Hard-coding the alphabet size: Using the magic number 26 throughout your code is standard for English Caesar cipher, but consider defining it as a named constant (private static final int ALPHABET_SIZE = 26;) for clarity and maintainability.
Summary
You now have a complete Java toolkit for Caesar cipher operations. The basic implementation demonstrates core string manipulation. The OOP class provides a reusable, well-tested component. The file encryptor shows Java NIO in action. The JUnit tests ensure correctness across edge cases. And the command-line tool ties everything together into a practical application.
These implementations serve as a strong foundation for exploring more advanced ciphers. You might extend the CaesarCipher class to support custom alphabets, add frequency analysis for automatic key detection, or use it as a building block for the Vigenere cipher, which applies different Caesar shifts to different positions in the text.
Next Steps: Ready to explore more ciphers? Learn about the Vigenere Cipher, which extends the Caesar cipher concept with multiple shift values for much stronger encryption.