Skip to content

How to learn Java programming by modifying open source code

Purpose

This post demonstrates how to learn Java programming effectively by modifying open source code.

The Problem

When I started learning Java, I read tutorials and documentation but struggled to apply concepts to real-world code. I understood syntax basics but couldn’t trace execution flow or modify existing systems effectively. I knew what methods did, but not how or why they worked.

I found this advice on Reddit r/learnjava: User asked for “best open source Java projects for me to read?” and received key advice: “Take a good piece of software and modify it to do something a bit different. Change the front end or the back end to something entirely different. You might understand what it does, but not how it is achieved. By modifying it, you truly understand how it works.”

Environment

  • Java 21
  • Maven/Gradle build tools
  • VS Code or IntelliJ IDEA
  • Git for version control

The Approach

The hands-on modification approach has three core goals:

  • Understanding internals: See how code really works, not just what it does
  • Practical debugging: Learn to trace execution and fix errors
  • Architecture patterns: Recognize common design patterns in real code

We will start with simple projects and gradually increase complexity.

The Configuration

Example 1: Simple Calculator Project

Here’s a basic calculator implementation:

Calculator.java
// Calculator.java - Basic calculator
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
}

I can explain the key parts:

  • The Calculator class contains simple arithmetic methods
  • Each method takes two integer parameters
  • Returns the result of the operation

But this basic version has limitations. It doesn’t handle edge cases well.

Enhanced Version with Validation

I tried to add input validation:

EnhancedCalculator.java
// Enhanced Calculator.java with validation and additional features
public class Calculator {
private boolean isValid(int num) {
return !Double.isInfinite(num) && !Double.isNaN(num);
}
public int add(int a, int b) {
if (!isValid(a) || !isValid(b)) {
throw new IllegalArgumentException("Invalid numbers provided");
}
return a + b;
}
public int subtract(int a, int b) {
if (!isValid(a) || !isValid(b)) {
throw new IllegalArgumentException("Invalid numbers provided");
}
return a - b;
}
public double add(double a, double b) {
if (!isValid(a) || !isValid(b)) {
throw new IllegalArgumentException("Invalid numbers provided");
}
return a + b;
}
}

What changed and why:

  • Added isValid() method to check for infinite/NaN values
  • Added validation before each operation
  • Added double-overloaded methods for more precision
  • Used exceptions instead of silent failures

Now test again:

public class CalculatorTest {
public static void main(String[] args) {
Calculator calc = new Calculator();
// Normal operations work
System.out.println(calc.add(5, 3)); // 8
System.out.println(calc.subtract(5, 3)); // 2
// Edge cases now throw exceptions
try {
calc.add(Double.POSITIVE_INFINITY, 5);
} catch (IllegalArgumentException e) {
System.out.println("Caught: " + e.getMessage()); // Caught: Invalid numbers provided
}
}
}

You can see that I succeeded to add robust validation while maintaining original functionality.

Example 2: Project Structure Evolution

Here’s a typical beginner project structure:

MyProject/
├── src/
│ └── main/
│ └── java/
│ └── com/
│ └── example/
│ └── App.java
└── pom.xml

But when I tried to scale this to a real application, I hit problems. All code in one file became unmanageable.

So I restructured with proper layering:

MyProject/
├── src/
│ └── main/
│ └── java/
│ └── com/
│ └── example/
│ ├── controller/
│ │ └── UserController.java
│ ├── service/
│ │ └── UserService.java
│ ├── model/
│ │ └── User.java
│ └── App.java
└── pom.xml

This separation allows better organization and maintainability.

Practical Modification Exercises

Beginner Level: Todo Application

I modified a simple command-line todo application:

Before: Basic add/delete functionality After: Added persistent storage using JSON files

TodoApp.java
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class TodoApp {
private List<String> tasks = new ArrayList<>();
private ObjectMapper mapper = new ObjectMapper();
private File dataFile = new File("todos.json");
public void addTask(String task) {
tasks.add(task);
saveTasks();
}
public void removeTask(int index) {
if (index >= 0 && index < tasks.size()) {
tasks.remove(index);
saveTasks();
}
}
private void saveTasks() {
try {
mapper.writeValue(dataFile, tasks);
} catch (Exception e) {
System.out.println("Failed to save tasks: " + e.getMessage());
}
}
}

The key improvement: persistence across application restarts.

Intermediate Level: REST API Enhancement

I took a basic Spring Boot REST API project and added:

  • New endpoint with proper error handling
  • Caching layer for frequently accessed data
  • Authentication using Spring Security
UserController.java
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
@Cacheable(value = "users", key = "#id")
@PreAuthorize("hasRole('USER')")
public User getUser(@PathVariable String id) {
return userService.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
}

The key improvement: Security and caching at the controller level.

How It Works

When I run the enhanced todo application:

Terminal window
$ java TodoApp
Add: Buy groceries
Add: Call mom
List:
1. Buy groceries
2. Call mom
Remove: 1
List:
2. Call mom
$ java TodoApp # Restarted
List:
2. Call mom

I get this output showing persistence works.

Common Mistakes

But when I tried to modify complex projects immediately, I encountered several problems:

  1. Jumping into complex codebases without understanding the architecture
  2. Making random changes without testing thoroughly
  3. Not documenting changes for learning purposes

The most important lesson: start small and understand before modifying.

Learning Path

Based on my experience, here’s a structured approach:

Week 1-2: Foundation

  • Choose a simple project (calculator, todo app)
  • Understand project structure and dependencies
  • Make small modifications to understand build process

Week 3-4: Building Skills

  • Make 3-5 small, intentional modifications
  • Add new features to existing functionality
  • Test each change thoroughly

Week 5-6: Intermediate Projects

  • Enhance REST APIs with proper error handling
  • Add authentication and security
  • Implement caching and performance improvements

Week 7-8: Open Source Contribution

  • Fix simple bugs in existing projects
  • Add documentation for new features
  • Optimize database queries
  1. Java Calculator Application - Simple, self-contained
  2. Todo REST API - Basic Spring Boot project
  3. File Processing Tool - Real-world utility
  4. Simple Game - Engaging for learning

Summary

In this post, I showed how to learn Java programming effectively by modifying open source code. The key point is that hands-on modification creates deeper understanding than passive reading alone. Start small, make intentional changes, and test thoroughly to build practical Java skills.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments