3.1 - Abstraction and Program Design
Abstraction is all about reducing complexity by focusing on the main idea. You hide the details that don’t matter for the question you’re trying to answer, and you group together the details that do matter. That way you can focus on the concept instead of getting buried in specifics.
For example, a dog shelter’s adoption program might describe each dog with just a name and an age. A real dog has tons of other details (weight, fur color, favorite toy), but the program leaves them out because they don’t matter for the job it’s doing.
Data abstraction separates the abstract idea of a piece of data from the messy concrete details of how it’s actually represented. You give data a name without worrying about the underlying implementation. This can be a single variable or a whole collection of data, like a class.
Example: without a class, a program tracking two dogs needs a loose variable for every single detail. With a
Dog class, each dog’s data is bundled under one name, and the code using it doesn’t need to know how Dog stores that data inside. Even a String is a data abstraction: you use a name like Rex as one value without caring how Java stores its characters.// Without data abstraction: a loose variable for every detail String dog1Name = "Rex"; int dog1Age = 3; String dog2Name = "Bella"; int dog2Age = 5; // With data abstraction: each Dog object holds its own data under one name Dog dog1 = new Dog("Rex", 3); Dog dog2 = new Dog("Bella", 5);
Within a class, an attribute is a piece of data defined outside any method or constructor:
- An instance variable is an attribute whose value is unique to each individual object (instance) of the class.
- A class variable is an attribute shared across every instance of the class.
Both kinds of attributes are declared inside the class but outside every method and constructor. The keyword
static is what makes a variable a class variable (more on that in 3.7).public class Dog { private String name; // instance variable: each Dog has its own name private int age; // instance variable: each Dog has its own age private static int totalDogs = 0; // class variable: one count shared by all Dogs public Dog(String n, int a) { name = n; age = a; totalDogs++; } }
If a program makes three
Dog objects, there are three names and three ages, but still only one totalDogs, and it equals 3.Procedural abstraction gives a name to a process, letting you use a method just by knowing what it does, not how it’s implemented. Breaking a big behavior into smaller methods is called method decomposition. This also lets you pull out shared functionality instead of copy-pasting code everywhere, which is a big part of managing complexity and enabling code reuse.
// Method decomposition example: breaking a big task into smaller pieces public void makeSandwich() { getBread(); addFillings(); cutInHalf(); }
Procedural abstraction also gets rid of duplicate code. When the same steps show up in more than one place, pull them out into one method and call that method instead:
// Before: the same two steps are copied into both methods public void morningWalk() { System.out.println("Grab the leash"); System.out.println("Walk around the block"); System.out.println("Feed breakfast"); } public void eveningWalk() { System.out.println("Grab the leash"); System.out.println("Walk around the block"); System.out.println("Feed dinner"); }
// After: the shared steps live in one method that both methods call private void walk() { System.out.println("Grab the leash"); System.out.println("Walk around the block"); } public void morningWalk() { walk(); System.out.println("Feed breakfast"); } public void eveningWalk() { walk(); System.out.println("Feed dinner"); }
Both versions print exactly the same thing. But now if the walk changes, you only fix it in one place, and any new method that needs a walk can just call
walk(). That’s code reuse.Parameters let you generalize a procedure so it works across a range of different inputs, instead of writing a separate method for every single case.
// Generalized with a parameter, works for any number public int square(int x) { return x * x; }
Without the parameter, you’d need a separate method for every number (
squareOfThree, squareOfFour, and so on). With it, one method works for any argument:int a = square(3); // 9 int b = square(12); // 144
Because of procedural abstraction, you can change how a method works internally (make it faster, more memory efficient, whatever) without breaking anything for the people using it, as long as the method signature and what it does stay the same.
For example, both versions of
humanYears below have the same signature and return the same number for any age. Switching from the loop to plain multiplication makes the method faster, and code that calls myDog.humanYears() doesn’t have to change at all.// Version 1: adds 7 once for every year of age public int humanYears() { int total = 0; for (int i = 0; i < age; i++) { total += 7; } return total; } // Version 2: same signature, same result, but no loop needed public int humanYears() { return age * 7; }
Before you actually write a class, it’s worth taking time to design it first, laying out its attributes and behaviors using plain language or a diagram.

In a class diagram like this one, the top box names the class, the middle box lists the attributes (instance variables) with their types, and the bottom box lists the behaviors (methods). The same design in natural language: a
Dog has a name (a String) and an age (an int), and a Dog can bark, eat, and tell you its name. When you write the class, each attribute becomes a private instance variable and each behavior becomes a method:public class Dog { private String name; // attribute private int age; // attribute public void bark() { // behavior System.out.println("Woof!"); } public void eat() { // behavior System.out.println(name + " is eating."); } public String getName() { // behavior return name; } }