Build a Type-Safe Storage Organizer
Build a Java console organizer with type-safe generic storage.
Introduction
30 Second Summary
A storage box is useful when its label matches what is inside. A misplaced quantity can stay hidden until a program tries to use it as text.
In this project, you will build a console organizer with Java generics that keeps inventory labels separate from numeric quantities. You will turn a runtime casting failure into an editor or compiler error through compile-time type safety.
What You'll Build
Your finished organizer prints paired inventory labels, paired quantities, plus their numeric total in the console.
By the end of this project, you'll have:
- An unsafe storage demonstration that prints Stored value: 42 before a ClassCastException exposes the bad cast.
- A reusable StorageBox<T> class that safely stores text labels or numeric quantities without runtime casts.
- Reusable generic tools that print matching box pairs and calculate a total quantity of 60.0.
- Secret Mission: Read and print boxes with unrelated types by using StorageBox<?>.
Are there any prerequisites?
You need IntelliJ IDEA plus a JDK installed on your Mac. Basic familiarity with Java variables, classes, and methods is enough because no prior generics experience is required.
Before We Start
This quick checkpoint helps you commit to building a Java console storage organizer that uses generics to catch invalid storage operations before the program runs.
Set Up the IntelliJ IDEA Project
Your organizer needs a small Java workspace where IntelliJ IDEA can report type errors as you write. A plain project keeps compiler feedback visible without adding dependency tools.
First, you'll connect the project to the JDK version 25. Then you'll create the smallest runnable Main class. A successful run proves your workspace is ready for the storage organizer.
In this step, get ready to:
- Create the type-safe-storage-organizer Java project.
- Configure the IntelliJ build system with JDK 25.
- Run Main to confirm the console output.
Create the plain Java project
A project connects your source files to a build system. It also selects the JDK that compiles your code.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type IntelliJ IDEA and press Enter to open it.
You'll see the IntelliJ IDEA welcome screen.
- Click New Project on the welcome screen.
You'll see the New Project wizard with the available project types.
- Select Java from the New Project list.
- Enter type-safe-storage-organizer as the project name.
The wizard now identifies the project as a Java application named type-safe-storage-organizer.
- Select IntelliJ in Build system.
- Open the JDK list to check the available versions.
✔️ The required JDK is listed
- Select the JDK entry whose version is 25.
The JDK selector should now show version 25.
- Leave Add sample code disabled.
- Click Create.
You'll see the Project tool window containing the src directory.
ⓧ The JDK is installed but missing
- Select Add JDK from Disk from the JDK list.
- Choose the home directory for your existing JDK version 25 installation.
The JDK selector should now show the installation you added.
- Leave Add sample code disabled.
- Click Create.
You'll see the Project tool window containing the src directory.
ⓧ The required JDK is not installed
The download may take a few minutes, so a quiet progress bar is expected.
- Select Download JDK from the JDK list.
- Choose version 25 in the download dialog.
The dialog should now show version 25 as the download target.
- Click Select to download the JDK.
IntelliJ IDEA returns to the project wizard when the JDK is ready.
- Leave Add sample code disabled.
- Click Create.
You'll see the Project tool window containing the src directory.
Your project shell is ready. IntelliJ IDEA now has the compiler needed to check your Java code.
Why use the IntelliJ build system?
Build tools such as Maven or Gradle can manage larger dependency graphs. This organizer has no third-party dependencies.
The IntelliJ build system keeps the setup focused on generics. You still get immediate compiler feedback in the editor.
Create the Main class
A Java application needs an entry point that the runtime can call. The exact entry point for this project is public static void main(String[] args).
- Select the src directory in the Project tool window.
- Right-click the src directory.
You'll see the context menu for files created inside src.
- Select New.
- Select Java Class.
You'll see a field for the new class name.
- Enter Main and press Enter.
IntelliJ IDEA creates src/Main.java and opens it in the editor.
- Create the starter entry point in src/Main.java by replacing the file contents with this code:
public class Main {
public static void main(String[] args) {
System.out.println("Storage organizer ready.");
}
}
What does this code do?
- The Main class gives the project a runnable class.
- The main(String[] args) method gives Java the application entry point.
- The System.out.println(...) call writes the readiness message to standard output.
- Save src/Main.java by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Confirm a green gutter icon appears beside main(String[] args).
No green gutter icon?
- Check that the file is named Main.java.
- Compare your entry point with public static void main(String[] args).
- Confirm every opening brace has a matching closing brace.
Ask for help with the entry point.
✔️ Awesome, I've got everything!
Great. Double-check that src/Main.java is saved before you run it.
ⓧ I'd like to double check the full code
public class Main {
public static void main(String[] args) {
System.out.println("Storage organizer ready.");
}
}
What should match?
This reference should match src/Main.java exactly. It contains the entry point plus the single readiness message.
Run the starter program
The gutter action compiles your class before launching it. The Run tool window shows the program's output.
Before you run Main, do you expect the readiness line to appear before or after the exit code?
- Click the green gutter icon beside main(String[] args).
- Select Run 'Main.main()'.
You should see Storage organizer ready. followed by exit code 0 in the Run tool window.
That's your setup proven. IntelliJ IDEA can compile your project with JDK 25 and run its entry point.
Starter program not running?
- Check that Main.java sits directly inside the src directory.
- Compare the printed text with Storage organizer ready..
- Confirm the project JDK still shows version 25.
Ask for help with the failed run.
Your Java workspace is ready. Next, you'll use Object storage to reproduce the runtime failure that generics prevent.
Experience the Unsafe Box
Your starter Main already proves the project can run in IntelliJ IDEA. Now the organizer needs a storage box that exposes what happens when the stored type stays vague.
An Object field accepts every reference type. A caller can use a cast that compiles even when the stored value does not match.
In this step, get ready to:
- Create an unsafe storage class that keeps its item as Object.
- Use Main to pass 42 through the unsafe box.
- Run the program to capture the runtime cast failure.
Create the unsafe storage class
The box needs a field broad enough to accept values without recording their intended type. Its methods use Object for that broad storage contract.
- In the Project tool window, right-click src.
- Select New.
- Select Java Class.
- Enter UnsafeStorageBox as the class name.
- Press Enter to create the file.
You will see UnsafeStorageBox.java under src in the Project tool window.
- Replace the generated contents of UnsafeStorageBox.java with the code below:
public class UnsafeStorageBox {
private Object item;
public void store(Object item) {
this.item = item;
}
public Object retrieve() {
return item;
}
}
What Does This Code Do?
- The private Object item field can refer to any object.
- The store(Object item) method accepts values through the broad Object type.
- The this.item = item assignment keeps the latest value in the box.
- The retrieve() method returns Object. The caller must decide which specific type to claim.
- Save UnsafeStorageBox.java.
- Confirm the editor shows the field plus both methods without red error highlights.
Seeing Red Error Highlights?
- Confirm the class name is UnsafeStorageBox.
- Check that each opening brace has a matching closing brace.
- Confirm store(Object item) assigns the value to this.item.
Help me diagnose the red highlights in my unsafe storage class.
✔️ Awesome, I've got everything!
Your unsafe storage class is saved with one broad field plus methods for storing and retrieving its value.
ⓧ I'd like to double check the full code
public class UnsafeStorageBox {
private Object item;
public void store(Object item) {
this.item = item;
}
public Object retrieve() {
return item;
}
}
Trigger and inspect the cast failure
The starter Main.java only prints its readiness message. The new caller stores one type before claiming that the retrieved value has a different type.
- Switch back to Main.java from earlier.
- Replace all code in Main.java with the code below:
public class Main {
public static void main(String[] args) {
UnsafeStorageBox box = new UnsafeStorageBox();
box.store(42);
System.out.println("Stored value: " + box.retrieve());
String label = (String) box.retrieve();
System.out.println(label);
}
}
How Does This Create the Obstacle?
- The new UnsafeStorageBox() expression creates the broad storage box.
- The box.store(42) call passes an integer value into the box. Java autoboxes that value as an Integer object.
- The first System.out.println(...) call proves the box can return its stored value.
- The String label = (String) box.retrieve() line asks Java to treat the retrieved object as a String.
- The final print statement runs only if that cast succeeds.
- Save Main.java.
Before you run the program, ask yourself what will happen when an Integer is cast to String.
- Click the green gutter icon beside public static void main(String[] args).
- Select the option that runs Main.
You will first see Stored value: 42 in the Run tool window. Execution then stops with an exception naming ClassCastException.
You reached the intended failure. The box accepted one type while the caller expected another.
Did the Expected Failure Not Appear?
- Save Main.java if the output still says Storage organizer ready..
- Confirm box.store(42) still stores the integer value.
- Confirm the String cast remains below the first print statement.
- Check that both Java files sit directly under src if IntelliJ IDEA cannot find the unsafe box class.
Help me reproduce the intended cast failure in this Java program.
✔️ Awesome, I've got everything!
Your caller stores 42 before making an incompatible String cast.
ⓧ I'd like to double check the full code
public class Main {
public static void main(String[] args) {
UnsafeStorageBox box = new UnsafeStorageBox();
box.store(42);
System.out.println("Stored value: " + box.retrieve());
String label = (String) box.retrieve();
System.out.println(label);
}
}
- In the Run tool window, locate Stored value: 42 to confirm retrieval worked.
- Find ClassCastException in the exception output to identify the failed cast.
- Switch back to Main.java.
- Locate box.store(42) to confirm the box accepted the integer.
- Locate String label = (String) box.retrieve() to identify the incompatible assumption.
What Did the Failure Prove?
The box exposes only Object to the compiler. That contract provides no connection between the type stored and the type retrieved.
The explicit cast moves responsibility to the caller. Stronger compile-time type safety would catch this mismatch before the program runs.
You have exposed the exact weakness hidden by Object storage. Next, you will make the box enforce its accepted type before the program can run.
Make Storage Type-Safe
The unsafe box accepted an Integer because its field used Object. The failure appeared only when Main cast that value to String at runtime.
Java generics move this check into IntelliJ IDEA. A type parameter lets each box declare the only type it accepts.
In this step, get ready to:
- Define a reusable StorageBox<T> class.
- Use separate type arguments for label boxes and quantity boxes.
- Verify that an invalid storage call fails during compilation.
Build the generic storage class
A generic class receives a type when you create an object from it. The conventional type parameter T represents that chosen type throughout the class.
- In the Project tool window, create StorageBox.java beneath src with New | Java Class.
- Replace the generated class content by pasting the following code:
public class StorageBox<T> {
private T item;
public void store(T item) {
this.item = item;
}
public T retrieve() {
return item;
}
}
What does this class do?
- StorageBox<T> declares T as the type selected for each box.
- item can hold only a value matching that selected type.
- store(T item) enforces the selected type when a value enters the box.
- retrieve() returns the selected type without a cast.
- Save StorageBox.java.
- Confirm the new file is listed beneath src in the Project tool window.
You should see StorageBox.java without red highlighting in the editor.
Seeing code highlighted in red?
- Check that the file is named StorageBox.java.
- Compare the class declaration with public class StorageBox<T>.
- Confirm that the field and both methods use the same uppercase T.
Help me fix my generic StorageBox class.
Test the compiler boundary
A type argument replaces T for one specific box. The constructor uses diamond inference so the compiler can infer that same type inside <>.
- Switch back to Main.java from the previous step.
- Replace all existing code in Main.java by pasting the following code:
public class Main {
public static void main(String[] args) {
StorageBox<String> firstLabelBox = new StorageBox<>();
StorageBox<Integer> firstQuantityBox = new StorageBox<>();
firstLabelBox.store("Bolts");
firstQuantityBox.store(24);
System.out.println("Label: " + firstLabelBox.retrieve());
System.out.println("Quantity: " + firstQuantityBox.retrieve());
}
}
How do the typed boxes work?
- StorageBox<String> binds firstLabelBox to text values.
- StorageBox<Integer> binds firstQuantityBox to integer values.
- new StorageBox<>() infers each constructor type from the variable declaration.
- retrieve() returns the correct declared type without a runtime cast.
- Save Main.java.
Before you test the type boundary, consider this question: will a quantity box accept a String.
- Add firstQuantityBox.store("wrong type"); immediately below firstQuantityBox.store(24);.
- Hover over the highlighted temporary call to read the compiler feedback.
IntelliJ IDEA rejects the call because a String cannot enter a StorageBox<Integer>. The invalid operation is blocked before the program runs.
What did this prove?
The type argument becomes a rule enforced by the editor and compiler. This is compile-time type safety in action.
The old Object box accepted the bad combination. StorageBox<Integer> stops it at the point where it enters your code.
No compiler error on the temporary call?
- Confirm that firstQuantityBox is declared as StorageBox<Integer>.
- Confirm that store accepts T inside StorageBox.java.
- Check that you did not use the raw type StorageBox in Main.java.
Why is IntelliJ IDEA accepting the wrong type in my generic box?
Run the type-safe organizer
The temporary call has proved that the compiler protects the quantity box. Removing that call leaves a valid program with no runtime cast.
- Delete firstQuantityBox.store("wrong type"); from Main.java.
- Remove UnsafeStorageBox.java through the Project tool window.
Before you run Main, predict what should replace the old runtime exception in the Run tool window.
- Click the green gutter icon beside public static void main(String[] args).
- Select the run action for Main.
The Run tool window shows Label: Bolts on the first line. It shows Quantity: 24 on the next line.
The process exits with code 0. You have moved a real storage mistake from a runtime crash to a compiler check.
Still seeing the old exception?
- Confirm that Main.java no longer creates an UnsafeStorageBox.
- Check that the cast to String has been removed.
- Confirm that you ran Main from the updated file.
Why is my updated Main class still throwing ClassCastException?
✔️ Awesome, I've got everything!
Your generic box and starter organizer are complete. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Your src/StorageBox.java file should match:
public class StorageBox<T> {
private T item;
public void store(T item) {
this.item = item;
}
public T retrieve() {
return item;
}
}
What should match in StorageBox.java?
The field and both methods use T. No Object type remains in this class.
Your src/Main.java file should match:
public class Main {
public static void main(String[] args) {
StorageBox<String> firstLabelBox = new StorageBox<>();
StorageBox<Integer> firstQuantityBox = new StorageBox<>();
firstLabelBox.store("Bolts");
firstQuantityBox.store(24);
System.out.println("Label: " + firstLabelBox.retrieve());
System.out.println("Quantity: " + firstQuantityBox.retrieve());
}
}
What should match in Main.java?
The two declarations use different type arguments. The temporary invalid call and the old cast are both absent.
Your organizer now protects each box before the program starts. Next, you will add generic tools that print pairs and total numeric quantities.
Add Reusable Generic Tools
Your StorageBox<T> now keeps labels and quantities separate at compile time in IntelliJ IDEA. However, Main still handles every box one at a time.
A generic method declares its own type parameter. This lets one algorithm accept several concrete types while keeping each call type-safe.
A bounded type parameter adds a specific capability to that algorithm. You will use a bound to make numeric operations available only for compatible boxes.
In this step, get ready to:
- Create StorageTools with a generic method that prints matching box pairs.
- Reuse the pair-printing method with label boxes or quantity boxes.
- Add a number-bounded method that totals quantity boxes.
Create the pair-printing utility
The retrieval plus printing pattern belongs in one utility class. The printPair() method places <T> before void because the type parameter belongs to the method.
- In the Project tool window, right-click src.
- Select New from the context menu.
- Select Java Class.
- Type StorageTools as the class name.
- Press Enter.
You will see StorageTools.java under src in the Project tool window.
- Replace the generated contents of StorageTools.java by pasting this code:
public final class StorageTools {
private StorageTools() {
}
public static <T> void printPair(
String heading,
StorageBox<T> first,
StorageBox<T> second) {
System.out.println(
heading + ": " + first.retrieve() + " | " + second.retrieve());
}
}
What does this code do?
- The private StorageTools() constructor keeps the class focused on static utility methods.
- The <T> declaration gives each call one shared type for both boxes.
- The heading parameter lets the caller label each printed pair.
- Each retrieve() call returns the value using the box's declared type.
- Save StorageTools.java.
- Confirm IntelliJ IDEA shows no red error highlighting in StorageTools.java.
Seeing errors in StorageTools?
- Confirm StorageTools.java sits directly under src beside StorageBox.java.
- Check that both parameters use the exact type StorageBox<T>.
- Match every opening brace with a closing brace.
Help me fix the compile errors in my generic StorageTools class.
Reuse the method in Main
The compiler infers T as String for a label pair. A separate call infers T as Integer for a quantity pair.
- Return to Main.java from earlier.
- Replace its contents by pasting this code:
public class Main {
public static void main(String[] args) {
StorageBox<String> firstLabelBox = new StorageBox<>();
StorageBox<String> secondLabelBox = new StorageBox<>();
StorageBox<Integer> firstQuantityBox = new StorageBox<>();
StorageBox<Integer> secondQuantityBox = new StorageBox<>();
firstLabelBox.store("Bolts");
secondLabelBox.store("Washers");
firstQuantityBox.store(24);
secondQuantityBox.store(36);
StorageTools.printPair("Labels", firstLabelBox, secondLabelBox);
StorageTools.printPair("Quantities", firstQuantityBox, secondQuantityBox);
}
}
How does type inference help?
- The second label box gives printPair() two StorageBox<String> arguments.
- The second quantity box gives the next call two StorageBox<Integer> arguments.
- The compiler infers the method type from each pair, so neither call needs an explicit type argument.
- Save Main.java.
Before you run it, picture the two headings you expect the shared method to print.
- Run Main by clicking the green gutter icon beside main().
You will see Labels: Bolts | Washers on the first line. You will see Quantities: 24 | 36 on the next line.
That is your first reusable algorithm working across two concrete types.
Pair output missing?
- Confirm both StorageTools.printPair() calls appear after the four store() calls.
- Check that the label call receives two StorageBox<String> variables.
- Check that the quantity call receives two StorageBox<Integer> variables.
Help me diagnose why my printPair() output is missing.
Add the bounded numeric total
Pair printing only needs retrieval. Summing needs the numeric operation doubleValue().
The bound <T extends Number> restricts the method to Number types. This gives the method safe access to numeric conversion.
- Return to StorageTools.java from earlier.
- Place your cursor before the final closing brace of StorageTools.
- Add the bounded method by pasting this code:
public static <T extends Number> double sumNumbers(
StorageBox<T> first,
StorageBox<T> second) {
return first.retrieve().doubleValue()
+ second.retrieve().doubleValue();
}
What does the bound unlock?
- The extends Number bound limits T to numeric wrapper types such as Integer.
- The bound lets the method call doubleValue() on each retrieved value.
- The method returns a double so different numeric box types can use the same summing pattern.
- Save StorageTools.java.
- Confirm IntelliJ IDEA shows no red error highlighting around sumNumbers().
Bounded method not compiling?
- Confirm <T extends Number> appears before the double return type.
- Check that sumNumbers() remains inside the outer StorageTools braces.
- Confirm both retrieved values call doubleValue().
Help me fix my sumNumbers() method.
The bounded method is ready to use from Main. Its returned double gives the organizer a visible total.
- Switch back to Main.java from earlier.
- Find the StorageTools.printPair() call whose heading is Quantities.
- Add the total calculation directly below that call by pasting this code:
double totalQuantity = StorageTools.sumNumbers(
firstQuantityBox,
secondQuantityBox);
System.out.println("Total quantity: " + totalQuantity);
How is the total produced?
- The sumNumbers() call receives the two StorageBox<Integer> variables.
- The returned value is stored in the double variable totalQuantity.
- The final System.out.println() call makes the computed result visible in the Run tool window.
- Save Main.java.
Before you run it, pause on the total you expect from 24 plus 36.
- Run Main by clicking the green gutter icon beside main().
Your Run tool window shows Labels: Bolts | Washers on the first line. The next line is Quantities: 24 | 36.
The final line is Total quantity: 60.0. You have finished an organizer with reusable type-safe tools.
Total not showing 60.0?
- Confirm firstQuantityBox stores 24.
- Confirm secondQuantityBox stores 36.
- Check that sumNumbers() receives both quantity boxes.
Help me diagnose the incorrect totalQuantity output.
✔️ Awesome, I've got everything!
Your three source files now match the finished type-safe storage organizer.
ⓧ I'd like to double check the full code
public class StorageBox<T> {
private T item;
public void store(T item) {
this.item = item;
}
public T retrieve() {
return item;
}
}
public final class StorageTools {
private StorageTools() {
}
public static <T> void printPair(
String heading,
StorageBox<T> first,
StorageBox<T> second) {
System.out.println(
heading + ": " + first.retrieve() + " | " + second.retrieve());
}
public static <T extends Number> double sumNumbers(
StorageBox<T> first,
StorageBox<T> second) {
return first.retrieve().doubleValue()
+ second.retrieve().doubleValue();
}
}
public class Main {
public static void main(String[] args) {
StorageBox<String> firstLabelBox = new StorageBox<>();
StorageBox<String> secondLabelBox = new StorageBox<>();
StorageBox<Integer> firstQuantityBox = new StorageBox<>();
StorageBox<Integer> secondQuantityBox = new StorageBox<>();
firstLabelBox.store("Bolts");
secondLabelBox.store("Washers");
firstQuantityBox.store(24);
secondQuantityBox.store(36);
StorageTools.printPair("Labels", firstLabelBox, secondLabelBox);
StorageTools.printPair("Quantities", firstQuantityBox, secondQuantityBox);
double totalQuantity = StorageTools.sumNumbers(
firstQuantityBox,
secondQuantityBox);
System.out.println("Total quantity: " + totalQuantity);
}
}
Secret mission
Print a Mixed Pair with a Wildcard
Your current pair printer accepts boxes that share one type. Extend it with an unbounded wildcard utility that displays a label box beside a quantity box on one line.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your Mac, so there are no ongoing costs. Your options are to keep it ready, pause it for later, or delete its local files.
Resources you used:
- The local type-safe-storage-organizer folder containing all project files.
Keep everything running
No action is needed. This option keeps the organizer ready to run or extend in IntelliJ IDEA.
- Leave the type-safe-storage-organizer folder on your Mac.
Pause - I'll come back to this later
This option frees the memory used by IntelliJ IDEA while preserving every project file.
- Close IntelliJ IDEA.
- Keep the type-safe-storage-organizer folder in its current location.
Delete - I don't want to use this again
Deleting the folder is permanent. The command below targets only the local type-safe-storage-organizer folder.
Your existing development tools stay installed.
- Check the full path to the type-safe-storage-organizer folder on your Mac.
- Record the full path here: full path to type-safe-storage-organizer.
- Close IntelliJ IDEA.
- Start a terminal session with your usual macOS app launcher.
- Remove the project folder by running this command:
rm -rf "[[PROJECT_FOLDER=\"full path to type-safe-storage-organizer\"]]"
What Does This Cleanup Do?
- The rm -rf command removes the selected folder with everything inside it.
- The quotation marks preserve a full path that contains spaces.
- Check the folder's previous location on your Mac.
You should no longer see type-safe-storage-organizer there. That clears every local project file while leaving your development tools ready for another project.
Still See the Project Folder?
- Confirm that the value saved for PROJECT_FOLDER ends with type-safe-storage-organizer.
- Run the cleanup command again after correcting the full path.
Help me delete the correct project folder safely.
Nice Work!
Nice Work!
You made your console organizer type-safe with Java generics. The compiler now blocks invalid storage operations before runtime.
You've learned how to:
- Reproduce unsafe Object storage that ends with a ClassCastException at runtime.
- Create text boxes plus quantity boxes from one reusable StorageBox<T> class through diamond inference.
- Write generic methods for same-type pair printing plus Number-bounded summation through doubleValue().
- Secret Mission: Use StorageBox<?> to read unrelated box types through printMixedPair().
Ready to quiz yourself?