DEV Community

Cover image for How I finally understood what a class is
Christian Vasquez
Christian Vasquez

Posted on • Updated on

How I finally understood what a class is

There's a saying that goes:

"Each head is a whole different world."

Which I find to be true. Some may argue that a certain teacher doesn't know how to do his/her job, but maybe it's that the way he/she teaches is not compatible with the way your brain is "wired". I guess this is one of those things that is not just right or wrong.

Many things are open to interpretation, which may lead to the loss of valuable information.

So, today I want to explain the way the idea of a class clicked for me. Not just in a conceptual way, but also on why they are useful in our code:

One of the first things we learn when we start programming are variables and constants.

So, if we are given the task of printing on the console your name, age and height, you'll probably do something like this:

Note: I decided not to include using a class like Scanner to request input from the user to keep it as simple as possible.

// Java

public class Main {
    public static void main(String[] args) {
        String name = "Chris";
        int age = 23;
        double height = 1.85;
        System.out.println("name: " + name + ", age: " + age + ", height: " + height);
    }
}
Enter fullscreen mode Exit fullscreen mode

Which would output:

name: Chris, age: 23, height: 1.85
Enter fullscreen mode Exit fullscreen mode

Great!

But, now we want to make our variable names a little more descriptive, how about adding a "chris" prefix? Check it out:

// Java

public class Main {
    public static void main(String[] args) {
        String chrisName = "Chris";
        int chrisAge = 23;
        double chrisHeight = 1.85;
        System.out.println("chrisName: " + chrisName + ", chrisAge: " + age + ", chrisHeight: " + chrisHeight);
    }
}
Enter fullscreen mode Exit fullscreen mode

Nice!

Now, let's add another person's info:

// Java

public class Main {
    public static void main(String[] args) {
        String chrisName = "Chris";
        int chrisAge = 23;
        double chrisHeight = 1.85;
        System.out.println("chrisName: " + chrisName + ", chrisAge: " + age + ", chrisHeight: " + chrisHeight);

        String danielName = "Daniel";
        int danielAge = 27;
        double danielHeight = 1.71;
        System.out.println("danielName: " + danielName + ", danielAge:" + danielAge + ", danielHeight:" + danielHeight)
    }
}
Enter fullscreen mode Exit fullscreen mode

Hmmm... 馃

Are you starting to see a pattern?

We are storing the same 3 values from 2 different people...

This is when a class comes to the rescue!

Superman on his way

Let's refactor our code step by step by focusing on one person at a time.

The Chris class

In OOP (Object Oriented Programming), classes are made to represent both state and behaviour with a high level of cohesion. In english: that means that the variables and methods of a given class are related to each other. If a method does not use any of the variables inside a class, it's probably a sign that it is where it doesn't belong.

Now that we have this idea, we can make a Chris class with a default constructor that will take the values of chrisName, chrisAge and chrisHeight:

A constructor is a special kind of function that must have the same name as the class it belongs to + have no return value (doesn't even need the void keyword), which is normally used to make sure that an instance of that class is in a valid state.

Valid state means that an object has the values that are expected.

An object is a concrete implementation of a class (we will see it in action later).

So, our Chris class would look like this:

// Java

public class Chris {
    private String name;
    private int age;
    private double height;

    public Chris(String name, int age, double height) {
        this.name = name;
        this.age = age;
        this.height = height;
    }
}
Enter fullscreen mode Exit fullscreen mode

The keyword this is used to refer to the global scope of our class in order to differentiate the name parameter of the constructor from the actual global scope's name.

Now we can refactor our code in order to use our new and shiny Chris class!

So, instead of:

String chrisName = "Chris";
int chrisAge = 23;
double chrisHeight = 1.85;
Enter fullscreen mode Exit fullscreen mode

We can have:

Chris chris = new Chris("Chris", 23, 1.85);
Enter fullscreen mode Exit fullscreen mode

The keyword new is used to refer to the constructor of our Chris class.

And chris (notice the lower case "c") is what we call an object, because it is a concrete or "real" implementation of our Chris class.

But since we don't have our chrisName, chrisAge and chrisHeight anymore, our code won't compile correctly.

Interesting...

How can we fix this?

Well, if we go back to our Chris class implementation, we can see that our global scope's variables (also known as fields, private fields or member variables) are private. So, we can't access them from the outside.

In order to be able to do that, we must add public methods that can help us access that data.

// Java

public class Chris {
    private String name;
    private int age;
    private double height;

    public Chris(String name, int age, double height) {
        this.name = name;
        this.age = age;
        this.height = height;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public double getHeight() {
        return height;
    }
}
Enter fullscreen mode Exit fullscreen mode

Did you notice how those get...() methods don't use the this keyword? That's because there are no parameters that can match their names, so the compiler knows that we are referring to the global scope's variables instead.

Now that we have these methods, we can access them by using the dot (.) operator, like this:

// Java

public class Main {
    public static void main(String[] args) {
        Chris chris = new Chris("Chris", 23, 1.85);
        System.out.println("chrisName: " + chris.getName() + ", chrisAge: " + chris.getAge() + ", chrisHeight: " + chris.getHeight());

        String danielName = "Daniel";
        int danielAge = 27;
        double danielHeight = 1.71;

        System.out.println("danielName: " + danielName + ", danielAge:" + danielAge + ", danielHeight:" + danielHeight)
    }
}
Enter fullscreen mode Exit fullscreen mode

That's better!

No, wait...

Now we gotta do the same for Daniel, let's create a Daniel class:

The Daniel class

// Java

public class Daniel {
    private String name;
    private int age;
    private double height;

    public Daniel(String name, int age, double height) {
        this.name = name;
        this.age = age;
        this.height = height;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public double getHeight() {
        return height;
    }
}
Enter fullscreen mode Exit fullscreen mode

And now our Main class would be:

// Java

public class Main {
    public static void main(String[] args) {
        Chris chris = new Chris("Chris", 23, 1.85);
        System.out.println("chrisName: " + chris.getName() + ", chrisAge: " + chris.getAge() + ", chrisHeight: " + chris.getHeight());

        Daniel daniel = new Daniel("Daniel", 27, 1.71);
        System.out.println("danielName: " + daniel.getName() + ", danielAge:" + daniel.getAge() + ", danielHeight:" + daniel.getHeight())
    }
}
Enter fullscreen mode Exit fullscreen mode

Nice!

Now our Main class is shorter than before.

But there's still one more thing we need to do...

If you really think about it, we are basically doing the same logic inside our Chris and Daniel classes 馃.

Which means we are not using the right abstraction for this particular case. In order to find a solution, we need to know what Chris and Daniel are...

That's right! They are both a Person.

So if we delete the Daniel class and rename our Chris class to "Person", our code would end up being:

// Java

public class Main {
    public static void main(String[] args) {
        Person chris = new Person("Chris", 23, 1.85);
        System.out.println("chrisName: " + chris.getName() + ", chrisAge: " + chris.getAge() + ", chrisHeight: " + chris.getHeight());

        Person daniel = new Person("Daniel", 27, 1.71);
        System.out.println("danielName: " + daniel.getName() + ", danielAge:" + daniel.getAge() + ", danielHeight:" + daniel.getHeight())
    }
}
Enter fullscreen mode Exit fullscreen mode

What benefits did we get?

  • Our code is now shorter and without losing it's meaning.
  • We reduced the need of 2 new classes to 1.
  • We reduced the noise of repeating both "Chris" and "Daniel" in more places than needed.
  • We managed to reuse our logic in a single class.
  • Now we can quickly know that both chris and daniel are concrete implementations of the same class, or how I like to call them: brothers.
  • Now we can use this Person class even in other projects and it will still work just fine.

Final words

I hope this example can help you, Mr./Mrs. Reader, to clear out your thoughts on what class are and why we use them in Object Oriented Programming :)

See you in the next post!

Bonus tip

This one comes from @alphashuro's comment down below: another benefit is that now we can replace the parts in our code where we print a Person's information by making a function that takes a Person object, like this:

// Java

public class Main {
    public static void main(String[] args) {
        Person chris = new Person("Chris", 23, 1.85);
        printPersonalInfo(chris);

        Person daniel = new Person("Daniel", 27, 1.71);
        printPersonalInfo(daniel);
    }

    public static void printPersonalInfo(Person person) {
        System.out.println("name: " + person.getName() + ", age:" + person.getAge() + ", height:" + person.getHeight());
    }
}
Enter fullscreen mode Exit fullscreen mode

And by making this "small" change we get the benefit of not having to maintain two different lines of code. There's just only 1 place in our code that we have to change in case we need to present someone's information in a different way.

Thanks to Alpha for bringing this up.

Now, there's another adjustment that I would do to our code.

Since the printPersonalInfo() function is only accepting Person objects, this means this method is directly dependent of the Person class. Which means, it should actually be part of it!

So let's go ahead and move our function to the Person class instead of having it hanging around inside our Main:

// Java

public class Person {
    private String name;
    private int age;
    private double height;

    public Person(String name, int age, double height) {
        this.name = name;
        this.age = age;
        this.height = height;
    }

    // Imagine we still have the getters here :P
    // this is just to make the code block shorter.

    public void printInfo() {
        System.out.println("name: " + name + ", age:" + age + ", height:" + height);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, you may have noticed that I had to make a few adjustments:

  • Remove the person parameter.
  • Replace each Getter methods calls for the global variables.
  • Renamed the method from "printPersonalInfo()" to "printInfo()".

The last point can be optional or personal preference. Personally, I find the "Personal" part of the name to be little redundant since we know we will create a Person object later.

Ok, so now we also have to make some adjustments to our Main class with this new implementation:

// Java

public class Main {
    public static void main(String[] args) {
        Person chris = new Person("Chris", 23, 1.85);
        chris.printInfo();

        Person daniel = new Person("Daniel", 27, 1.71);
        daniel.printInfo();
    }
}
Enter fullscreen mode Exit fullscreen mode

If you think about it for a second, we don't even need the Getter methods, this way of doing things is related to one of OOP's principles known as "encapsulation" which is a topic for another post 馃槈.

馃帄 Bonus points for you if you read the whole thing! 馃帀

Discussion (15)

Collapse
mortoray profile image
edA鈥憅a mort鈥憃ra鈥憏

The "prefix" variable pattern is one I mention often in the interviews I do. It's a strong indicator that there's a class hiding in your code: anytime you have multiple sets of variables with a common prefix, or suffix.

Collapse
ben profile image
Ben Halpern

I'm sure this will be helpful to many 馃檪

Collapse
alphashuro profile image
Alpha Shuro

This is great, well done! I think an important benefit of Classes that can be added in the final section of this article is that you can create a method that takes a parameter of type Person and calls println using the parameter's getters, then you can use this function on each instance of Person, i.e. chris and daniel, without writing out println twice.

Collapse
chrisvasqm profile image
Christian Vasquez Author

Thanks Alpha!

I'll edit the post to add your suggestion 馃檶

Collapse
gustavoagt profile image
Gustavo G贸mez

Nice post, well explained, i like it, if i would teach in a future some OO language i would go back to this post as a reference. I would like to add two little conceptual details.

In Java a default constructor is constructor that gets generated if you don't explicitly create one. That's the reason why you can create and no-param object without creating an explicitly constructor.

Finally, i would like to add that in the above example "chris" works as a reference of an object assigned to an object in memory.

Collapse
chrisvasqm profile image
Christian Vasquez Author

Thanks for the tips, Gustavo.

Regarding the default constructor, I thought it would be a correct way of calling it since that implementation of the Person class does not allow the client of our code to create an object without passing any arguments to it. Would it be better to say "overriding the default constructor"?

Collapse
gustavoagt profile image
Gustavo G贸mez

Thanks to you, i really like it as an introduction for classes, you should continue doing this kind of post.

I considered can be called just param-constructor, if we explicitly add no-args-contructor can be called as a overloaded constructor. The Overrinding is not possible for constructors because the behavior strictly belongs to the class.

Collapse
delbetu profile image
M Bellucci

Nice post!
From my point of view, the main concern of classes is joining behavior and data into an encapsulated piece of code.

This split the code into cohesive pieces and decreases coupling. Two good properties of maintainable code.

The caller doesn't know how the behavior is implemented it only cares about what the method does.

Creating a class as a bag of data and expose data through getters and setters is not object-oriented since it delegates the manipulation of the data to the caller. That is procedural code using classes.

My suggestion for making your final example a little more OOP is:
MoveSystem.out.println from Person class to caller class (Main).
Remove all getters from Person class. (Nobody uses them and nobody should)
Implement to_string method instead of printInfo

Collapse
jangerhofer profile image
JD Angerhofer

Christian -- terrific walkthrough of the practicalities of classes (as opposed to the abstract concept thereof that seems to be the canonical explanation for no good reason)!

Whenever I explain classes to the unacquainted, I find it useful to compare classes to a brand new data type that we, as users of any given language, get to create ourselves! This might be an imperfect analogy, depending on the language, but we all learn the basic primitive types when we first start (long before we get to groking classes). Later, we all develop some intuition about how those types behave. Ergo, a custom type (i.e. a class) is just a type which we get to direct!

Collapse
chrisvasqm profile image
Christian Vasquez Author

"I find it useful to compare classes to a brand new data type that we, as users of any given language, get to create ourselves!"

That's a really nice way of looking at it too!

Collapse
arj profile image
arj

Nicely explained! I am missing one thing though: the cohesion also implies a common lifetime of the objects. One of the things where behavior gets actually different from comparing classes vs. prefixed variables.

Collapse
jbristow profile image
Jon Bristow

Ok! Can we now discuss the tradeoffs between using a hash tables vs classes? POJOs and their kin are basically just heavy tuples/hash entries... What do you gain when you use a class vs. a lookup table? What do you lose?

Collapse
echoes2099 profile image
echoes2099

Not to troll or sound douchy but it would be more helpful if you shared more context and what you thought about classes before and after. The reason is because a "classification" (i.e., the concept of a class) is such a fundamental concept to being human, that this post is super surprising. For example, my toddler learned that fido and rover are both types of dogs, that is, he knows about the class of dogs. Later in school, he will learn that people and dogs are both mammals, another class.

Not surprisingly when I first learned Java many years ago, i took to classes like a fish to water. I realized that it made programming more relate-able i.e., easier to understand. As for my context, I had previous experience with procedural programming, which made OOP a refreshing, more natural alternative. Maybe that's what made classes so hard for you?

Where I did struggle is with overengineering---that is, not everything should be a class (rushing to abstractions to early). Later I also learned from experience that using classes for code sharing can be bad in large projects---you get too much coupling. That means shallow hierarchies are preferable, and object composition for code sharing is even better.

Collapse
rcarrasquillo19 profile image
Robert Carrasquillo

馃憤馃徏

Collapse
katzefudder profile image
Florian Dehn

Great post! But, one thing: instead of using 麓printInfo麓, you could simply overload the toString method and print out the object.