1 The Problem
We want a tool that creates a strong password: a random mix of letters, numbers, and symbols, of a length the user chooses. It teaches building a string from random choices — and an important lesson about which randomness is safe for security.
2 How to Think About It
Two decisions drive the whole program: which character sets to include, and how many characters to pick from the combined pool.
3 The Build — explained part by part
Here is the complete generator. Each character set is a case of an enum, from the course’s Enums lesson — a real class, not just a named integer, which is exactly what lets each constant carry its own character string.
import java.security.SecureRandom;
import java.util.EnumSet;
import java.util.Set;
/**
* Password Generator: builds a random password from a chosen mix of
* character sets, using SecureRandom for genuinely unpredictable output.
*/
public class PasswordGenerator {
enum CharSet {
LOWER("abcdefghijklmnopqrstuvwxyz"),
UPPER("ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
DIGITS("0123456789"),
SYMBOLS("!@#$%^&*()-_=+");
final String chars;
CharSet(String chars) { this.chars = chars; }
}
/** Returns null (an empty pool) if no character sets are selected. */
static String buildPool(Set<CharSet> sets) {
StringBuilder pool = new StringBuilder();
for (CharSet set : sets) pool.append(set.chars);
return pool.isEmpty() ? null : pool.toString();
}
static String generate(int length, Set<CharSet> sets, SecureRandom random) {
String pool = buildPool(sets);
if (pool == null) {
throw new IllegalArgumentException("Select at least one character set.");
}
StringBuilder password = new StringBuilder(length);
for (int i = 0; i < length; i++) {
password.append(pool.charAt(random.nextInt(pool.length())));
}
return password.toString();
}
public static void main(String[] args) {
Set<CharSet> sets = EnumSet.of(
CharSet.LOWER, CharSet.UPPER, CharSet.DIGITS, CharSet.SYMBOLS);
String password = generate(16, sets, new SecureRandom());
System.out.println(password);
}
}
EnumSet<CharSet> — a specialized, highly efficient
Set implementation that only works with enum types, used here to represent “which character sets are selected” instead of four separate boolean parameters.buildPool returns null for an empty selection, instead of an empty string — this is a deliberate signal the caller cannot ignore:
generate checks for null explicitly and throws a clear IllegalArgumentException, rather than silently producing a password from an empty pool (which would either loop forever or throw an unrelated StringIndexOutOfBoundsException deep inside charAt).new SecureRandom(), not
new Random() — java.util.Random is a fast, predictable pseudo-random generator, fine for a guessing game but genuinely unsuitable for anything security-sensitive; SecureRandom is cryptographically strong and is what a real password generator must use.
java.util.Random for a password generator — it is statistically predictable, which defeats the entire purpose of a password.java.security.SecureRandom for anything security-related, as the code above does.pool.charAt(...) on an empty string throws a confusing, unrelated exception instead of a clear error.IllegalArgumentException, as generate does — the same bug the Rust version of this project caught for real during testing.4 Test & Prove Each Part
We test the pool-building logic, the output length and character membership, and the empty-selection error — the same “empty pool” case that turned into a real bug in this project's Rust version.
import org.junit.Test;
import java.security.SecureRandom;
import java.util.EnumSet;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class PasswordGeneratorTest {
private static final EnumSet<PasswordGenerator.CharSet> NONE =
EnumSet.noneOf(PasswordGenerator.CharSet.class);
@Test
public void emptySelectionIsRejected() {
assertNull(PasswordGenerator.buildPool(NONE));
}
@Test
public void generatedPasswordHasTheRequestedLength() {
Set<PasswordGenerator.CharSet> sets = EnumSet.of(PasswordGenerator.CharSet.LOWER);
String password = PasswordGenerator.generate(20, sets, new SecureRandom());
assertEquals(20, password.length());
}
@Test
public void onlyUsesCharactersFromTheSelectedSets() {
Set<PasswordGenerator.CharSet> sets = EnumSet.of(PasswordGenerator.CharSet.DIGITS);
String password = PasswordGenerator.generate(50, sets, new SecureRandom());
assertTrue(password.chars().allMatch(Character::isDigit));
}
@Test(expected = IllegalArgumentException.class)
public void generateWithNoSetsThrowsInsteadOfCrashingObscurely() {
PasswordGenerator.generate(10, NONE, new SecureRandom());
}
}
Compile and run with javac -cp junit-4.13.2.jar and hamcrest-core-1.3.jar PasswordGenerator.java PasswordGeneratorTest.java then java -cp .:junit-4.13.2.jar:hamcrest-core-1.3.jar org.junit.runner.JUnitCore PasswordGeneratorTest. The “only uses selected characters” test generates a long, 50-character password specifically to make it statistically very unlikely a bug would slip through by chance.
5 The Interface
What it expects
EnumSet.of(LOWER, UPPER, DIGITS, SYMBOLS), length 16What it returns
uyL8S$6lKngwiad)6 Run It & Automate It
Save the code as PasswordGenerator.java and compile it with javac — that turns your source into .class bytecode files, which java then runs on the JVM. No separate install step: any real JDK ships both tools.
javac PasswordGenerator.java && java PasswordGeneratorRun it again and you will get a different password — that unpredictability is the entire point.
A CI tool like Jenkins runs the same compile-then-test steps automatically whenever the code changes — every line below has a plain explanation.
$ java PasswordGenerator
uyL8S$6lKngwiad)
$ java PasswordGenerator
mdG3z1MtFa1RGOD7generate working as designed — the EnumSet you passed in was empty. Add at least one CharSet constant to it.SecureRandom() and are not accidentally reusing a Random seeded with a fixed value from testing code.// Jenkinsfile — runs the tests automatically every time the code changes.
pipeline {
agent any // run on any available machine
environment {
CP = 'junit-4.13.2.jar:hamcrest-core-1.3.jar' // JUnit + its one dependency
}
stages {
stage('Get the code') {
steps { checkout scm } // download the latest code
}
stage('Set up JDK') {
steps {
sh 'java -version' // confirm a JDK is installed
sh 'javac -cp "$CP" *.java' // compile the program and its tests together
}
}
stage('Run the tests') {
steps {
sh 'java -cp ".:$CP" org.junit.runner.JUnitCore PasswordGeneratorTest'
}
}
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Read the selection from args. Let the command line choose which character sets and length to use, instead of hardcoding them in
main. (Teaches: parsingString[] argsinto anEnumSet.) - Guarantee at least one of each selected type. A 16-character password with symbols selected could randomly contain zero symbols. (Teaches: post-generation validation and regeneration.)
- Estimate entropy. Print how many possible passwords exist for the chosen pool and length. (Teaches: basic combinatorics with
Math.pow.)
EnumSet for a type-safe set of options, why SecureRandom replaces Random for anything security-sensitive, and returning null as a deliberate, checked signal rather than letting a downstream call fail confusingly. Related: Enums, Standard Library.