Java 实现凯撒密码:完整实现指南
通过完整代码示例学习如何在 Java 中实现凯撒密码。涵盖基础加密、使用 CaesarCipher 类的面向对象设计、文件加密、单元测试以及命令行工具。
凯撒密码是密码学中最简单、最广为人知的加密技术之一。对于 Java 开发者而言,实现这个密码是练习核心语言特性的绝佳机会,包括面向对象编程、文件 I/O、字符串操作和单元测试。无论你是正在完成课程作业的计算机科学学生,还是复习密码学基础的开发者,本指南都将带你逐步实现从简单到复杂的多种 Java 实现方案。
在本教程中,你将构建五个完整的程序:基础的加密/解密函数、完整的面向对象 CaesarCipher 类、文件加密工具、JUnit 测试套件,以及精心设计的命令行工具。所有代码示例均可使用标准 Java 17 或更高版本编译运行。
在线体验:在动手编写代码之前,先用我们免费的凯撒密码编码器体验不同位移值如何变换文本。
理解 Java 中的凯撒密码逻辑
凯撒密码的工作原理是将明文中的每个字母在字母表中向后移动固定数量的位置。例如,位移为 3 时,字母 A 变为 D,B 变为 E,以此类推。字母到达末尾后会回绕,X 变为 A,Y 变为 B,Z 变为 C。
在 Java 中,可以利用 char 值是数值这一特性。大写字母 A 到 Z 的 ASCII 值为 65 到 90,小写字母 a 到 z 的 ASCII 值为 97 到 122。通过对这些字符值进行算术运算并使用取模运算符,无需查找表即可简洁地实现位移操作。
核心加密公式为:
encryptedChar = (char - base + shift) % 26 + base
其中 base 对大写字母为 'A',对小写字母为 'a',shift 为向前移动的位置数。解密只需使用逆向位移(26 减去加密位移)。
基础凯撒密码实现
最简单的方式是使用两个静态方法分别进行加密和解密。这个版本同时处理大写和小写字母,非字母字符保持不变。
public class BasicCaesarCipher {
public static String encrypt(String plaintext, int shift) {
// 将位移规范化到 0-25 范围
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 {
// 非字母字符保持不变
result.append(ch);
}
}
return result.toString();
}
public static String decrypt(String ciphertext, int 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);
}
}
运行这段程序,输出为:
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.
该实现中有几个重要细节值得注意:
- 位移规范化:表达式
((shift % 26) + 26) % 26确保位移始终在 0 到 25 的范围内,即使传入负值也能正确处理。Java 的取模运算符对负操作数可能返回负值,这种双重取模模式能处理此边界情况。 - StringBuilder:使用
StringBuilder而非字符串拼接,在逐字符构建结果时效率更高,尤其是处理较长文本时。 - 字符保留:数字、标点、空格以及其他非字母字符原样传递,这是经典凯撒密码实现的标准行为。
面向对象的 CaesarCipher 类
在实际 Java 项目中,面向对象设计更为合适。以下 CaesarCipher 类将位移值封装为实例状态,支持方法链式调用,并提供包括暴力破解在内的额外工具方法。
import java.util.ArrayList;
import java.util.List;
public class CaesarCipher {
private final int shift;
/**
* 使用指定位移值创建 CaesarCipher 实例。
* 位移值会被规范化到 0-25 范围。
*
* @param shift 字母移动的位置数
*/
public CaesarCipher(int shift) {
this.shift = ((shift % 26) + 26) % 26;
}
/**
* 返回规范化后的位移值。
*/
public int getShift() {
return shift;
}
/**
* 使用当前密码的位移值加密给定的明文。
*
* @param plaintext 待加密的文本
* @return 加密后的密文
*/
public String encrypt(String plaintext) {
if (plaintext == null) {
throw new IllegalArgumentException("Input text cannot be null");
}
return transform(plaintext, shift);
}
/**
* 使用当前密码的位移值解密给定的密文。
*
* @param ciphertext 待解密的文本
* @return 解密后的明文
*/
public String decrypt(String ciphertext) {
if (ciphertext == null) {
throw new IllegalArgumentException("Input text cannot be null");
}
return transform(ciphertext, 26 - shift);
}
/**
* 执行暴力破解攻击,返回所有 26 种可能的解密结果。
*
* @param ciphertext 待暴力破解的文本
* @return 包含所有可能解密结果及对应位移值的列表
*/
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);
}
}
}
该类设计遵循了多项 Java 最佳实践:
- 不可变性:
shift字段声明为final,使实例具备线程安全性和可预测性。 - 校验:null 输入会立即抛出
IllegalArgumentException,而不是在代码深处引发NullPointerException。 - 封装:
transform方法声明为私有,因为它是类的使用者不需要了解的实现细节。 - 静态工具方法:
bruteForce方法声明为静态,因为它不依赖任何特定实例的位移值。
使用凯撒密码加密文件
文件处理是密码实现的常见需求。以下程序读取文本文件,加密其内容,并将结果写入新文件。它使用 Java NIO 实现现代、高效的文件处理。
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 {
/**
* 加密文件内容并将结果写入输出文件。
*
* @param inputPath 明文文件路径
* @param outputPath 加密输出文件路径
* @param shift 凯撒密码位移值
* @throws IOException 文件读写失败时抛出
*/
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);
}
/**
* 解密文件内容并将结果写入输出文件。
*
* @param inputPath 加密文件路径
* @param outputPath 解密输出文件路径
* @param shift 加密时使用的凯撒密码位移值
* @throws IOException 文件读写失败时抛出
*/
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);
}
}
}
从命令行使用:
# 以位移 5 加密文件
java CaesarFileEncryptor encrypt 5 secret.txt secret.enc
# 解密文件
java CaesarFileEncryptor decrypt 5 secret.enc secret_decrypted.txt
该实现使用 Files.readString() 和 Files.writeString(),这两个方法在 Java 11 及更高版本中可用。对于无法完整加载到内存的超大文件,应改用 BufferedReader 和 BufferedWriter 的流式处理方式。
使用 JUnit 5 进行单元测试
彻底的测试对于任何加密实现都至关重要。以下 JUnit 5 测试类涵盖了正常操作、边界情况以及加密与解密之间的关系。
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);
}
}
使用 Maven 或 Gradle 运行测试:
# Maven
mvn test
# Gradle
gradle test
该测试套件涵盖了几个重要的测试类别:
- 正确性:验证特定输入和位移值下已知的加密输出。
- 往返完整性:确保对加密消息解密后能恢复原始文本,覆盖所有可能的位移值。
- 边界情况:空字符串、位移为零、位移为 26、负位移以及大于 26 的位移。
- 字符处理:非字母字符原样传递,字母大小写得到保留。
- 错误处理:null 输入抛出预期的异常。
构建命令行工具
最后一个示例将所有内容整合为一个精心设计的命令行应用程序,具备参数解析、多种操作模式和友好的输出格式。该工具支持加密、解密和暴力破解。
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 "";
}
}
// 将剩余参数拼接为文本
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));
}
}
编译并运行该工具:
# 编译所有文件
javac CaesarCipher.java CaesarCipherCLI.java
# 加密消息
java CaesarCipherCLI encrypt 13 "Meet me at midnight"
# 解密消息
java CaesarCipherCLI decrypt 13 "Zrrg zr ng zvqavtug"
# 暴力破解未知密文
java CaesarCipherCLI bruteforce "Wklv lv d vhfuhw phvvdjh"
# 从文件加密
java CaesarCipherCLI encrypt 7 --file message.txt
# 交互模式
java CaesarCipherCLI interactive
这个命令行工具展示了几项高级 Java 特性:
- switch 表达式,使用箭头语法(Java 14+)
- 文本块,用于多行字符串(Java 15+)
- 字符串格式化,使用
formatted()方法 - 局部变量类型推断,在类型可从上下文推断时使用
var
性能考量
凯撒密码本身的计算量极小,时间复杂度为 O(n),其中 n 为输入文本的长度。不过,Java 实现中有几点性能建议值得关注:
StringBuilder 容量预分配:当预先知道输出长度时(凯撒密码的输出长度等于输入长度),可以通过 new StringBuilder(text.length()) 预分配容量,避免内部缓冲区扩容。
字符数组与 charAt:对于非常长的字符串,先用 toCharArray() 转换为 char[] 再遍历,有时比在循环中调用 charAt(i) 略快,因为避免了每次访问的边界检查。不过,这会额外创建一份字符串数据的副本。
基于 Stream 的方式:虽然可以使用 Java Stream 处理字符,但由于将 char 装箱为 Character 对象的开销,这类逐字符变换操作用传统循环反而更快。
常见陷阱及规避方法
在 Java 中实现凯撒密码时,需要注意以下常见错误:
负数取模:Java 的 % 运算符对负操作数返回负值。表达式 -3 % 26 的结果是 -3,而非 23。请始终使用双重取模模式进行规范化:((value % 26) + 26) % 26。
字符类型溢出:将位移值与 char 值相加时,结果会被提升为 int。必须显式强制转换回 char,否则 StringBuilder 会追加整数值而非字符。
忘记处理两种情况:常见的错误是正确加密了大写字母,却遗漏了小写字母(或反之)。请始终使用混合大小写的输入进行测试。
硬编码字母表大小:在代码中大量使用魔法数字 26 是英语凯撒密码的惯例,但可以考虑将其定义为具名常量(private static final int ALPHABET_SIZE = 26;),以提高代码的可读性和可维护性。
总结
你现在拥有了一套完整的 Java 凯撒密码工具集。基础实现展示了核心字符串操作。面向对象的类提供了可复用、经过充分测试的组件。文件加密器展示了 Java NIO 的实际应用。JUnit 测试确保了边界情况下的正确性。命令行工具则将一切整合为一个实用的应用程序。
这些实现为探索更高级的密码奠定了坚实的基础。你可以扩展 CaesarCipher 类以支持自定义字母表,添加频率分析以自动检测密钥,或将其作为维吉尼亚密码的构建模块——维吉尼亚密码对文本中不同位置应用不同的凯撒位移,从而实现更强的加密效果。
下一步:准备好探索更多密码了吗?了解维吉尼亚密码,它在凯撒密码的基础上引入多个位移值,实现更强的加密效果。