Showing posts with label oop. Show all posts
Showing posts with label oop. Show all posts

Sunday, July 6, 2025

Assignment inheritance hierarchies

Assignment compatibility is a key concept in Java. It may sound daunting at first, but I promise it isn’t. Basically, it comes down to this:

  • Is the thing you want to assign the same type as the thing you want to assign to? Or, if not, then at least
  • Is the thing you want to assign guaranteed to have all the properties of the thing you want to assign to?

We’ve already covered this when we looked at inheritance.

Suppose I have a CollegeStudent class that extends Student.

Then, I can always say

Student s = new CollegeStudent();

because every CollegeStudent is a Student because CollegeStudent extends Student.

CollegeStudent objects, because of this “extends” relationship (thought of as “is a”), have all the properties of a Student, so the assignment of a CollegeStudent object into a Student reference is perfectly legal. There is nothing in a Student that is not also present in a CollegeStudent. A CollegeStudent may have more than just a Student, but it has at least all the properties and behaviors of a Student.  

There, of course, exists a trivial case of this.

You can always say that Object o = new CuteFluffyPony();

because every CuteFluffyPony by definition is an Object because every class implicitly extends Object.

An example of an assignment that would not be legal would be the following:

suppose:

Bird extends Animal
and also that
Cat extends Animal

Bird myBird =  new Cat();

Bird objects and Cat objects are both Animal objects, but Birds and Cats do not have a relationship like Students and CollegeStudents do. Birds and Cats are, at best, siblings, so there is no guarantee  that all the promises of a Bird are fulfilled in a Cat, so this assignment (or the reverse, Cat myCat = new Bird();) are always illegal.

In other words, an assignment is legal if:

  • The left and right object types are the same or, if not
  • The right object type is a sibling of the left object type, such that the right and left objects (in that direction) are perfectly interchangeable
You see this often with Lists, for example. List is an interface, with at least two very common implementations: ArrayList and LinkedList.

It is perfectly legal to declare

List<T> myList = new ArrayList<>();

just as it is perfectly legal to declare

List<T> myList = new LinkedList<>();

because every LinkedList is, by definition, a List, and every ArrayList is also by definition a List. Since the type on the right fulfils all the conditions of the type on the left, even though they’re different, the assignment is always perfectly legal.

 

 

Wednesday, June 18, 2025

Exceptions

 Things go wrong in Java all the time, especially in more complex systems, or when user input is required: data is entered incorrectly, or a file is sent in the wrong format, or you keep looping beyond where there is no more material to consume—the list of potential bugs is endless.


Exception handling is a fundamental concept in Java that allows you to manage errors gracefully and keep your program running even when unexpected issues occur. The basic structure involves three main keywords: try, catch, and finally. The try block contains code that might throw an exception—essentially, code that could go wrong at runtime. For example, if you try to divide a number by zero inside a try block, Java will throw an ArithmeticException. When an exception occurs, the normal flow of the program is interrupted, and control jumps to the catch block, which is designed to handle specific types of exceptions. In the case of dividing by zero, the catch block can print an error message or take corrective action, ensuring the program doesn’t crash abruptly. Perhaps, after the printing of the error message, the user can be invited to try to input a number again, this time trying consciously to avoid whatever the exception was.

Let’s look at a simple try-catch:

public class TryCatchExample {

    public static void main(String[] args) {

        try {

            int result = 10 / 0; // This will throw ArithmeticException

            System.out.println("Result: " + result);

        } catch (ArithmeticException e) {

            System.out.println("Error: Cannot divide by zero!");

        }

        System.out.println("Program continues after the try-catch block.");

    }

}

We enter the try-block, and attempt to assign 10/0 into an integer. Just as in math, in Java, you cannot divide by 0 without causing problems. (I don’t know if Java is actually trying to “divide” by zero somehow, the process is running away, and it’s throwing in the towel, or if Java has simply been written in a way such that “if someone tries to divide by 0, don’t even entertain that—just throw an error ASAP.”) In any case, dividing by zero throws an error, which, if not handled properly (by a catch-block, for example), will crash the program.

The finally block is optional but highly useful. Code inside finally will always execute, regardless of whether an exception was thrown or caught. This makes it ideal for cleanup actions, such as closing files or releasing resources. For instance, even if an exception occurs while reading a file or dividing numbers, the finally block can still close the file or print a message, guaranteeing that essential cleanup steps are performed. 

public class TryCatchFinallyExample {

    public static void main(String[] args) {

        try {

            int result = 10 / 0; // This will throw ArithmeticException

            System.out.println("Result: " + result);

        } catch (ArithmeticException e) {

            System.out.println("Error: Cannot divide by zero!");

        } finally {

            System.out.println("This is the finally block. It always runs.");

        }

        System.out.println("Program continues after the try-catch-finally block.");

    }

}

Consider this simple example: inside the try block, you attempt to divide by zero (exactly like the previous example above); the catch block prints out the exception message (again, exactly the same), and the band-new finally block prints “This is the finally block” regardless of what happened in try or catch.

A few years ago, I went to an Atlanta Java Users Group meeting at which Venkat Subramanian talked about good practices in exception handling. One thing stuck out at that talk more than probably any other talk I’ve ever been to: if you’re going to have something fail with an exception, make sure the failure is as soon, as loud, and as clear as possible. That is, fail early, make it obvious that something failed, and, in the statement that something failed, be as detailed as you can. For example, you could always just use a RuntimeException, passing in no message to handle a problem, like our old friend division by zero. That technically works, but it would be much better if

  • The exception was triggered as early as possible, rather than delaying acknowledging the problem, or passing it down endlessly
  • You used a more descriptive exception, like an ArithmeticException, or perhaps even your own custom-written CannotDivideByZeroException
  • You passed a message into your exception to explain where, how, why, or all three, the code failed

Tuesday, June 17, 2025

Nesting Classes

We’ve seen this once or twice before already, but I wanted to create an article about it because it’s such an important concept: You can have a class inside a class in Java. Now that we’ve been exposed to this idea, with, let’s say, a Song class inside Playlist, which represents a List<Song>—let’s dive more deeply into it.

When you define a class inside another class in Java, you’re using what’s called a nested class or inner class. This approach is especially useful for grouping related classes together, which can make your code more organized, readable, and maintainable. For example, if you have a Song class inside a Playlist class, it signals that Song is only relevant within the context of Playlist, and it helps encapsulate the logic that belongs together. Inner classes can access the fields and methods of their outer class, including private ones, which allows for tight integration between the two.

There are several types of inner classes in Java. The most common is the non-static inner class, which is tied to an instance of the outer class. To use it, you first create an instance of the outer class, then use it to create an instance of the inner class. There are also static nested classes, which behave like regular classes but are nested for logical grouping; they can only access static members of the outer class and don’t require an instance of the outer class to be instantiated. Additionally, Java supports local inner classes (defined within methods) and anonymous inner classes (used for concise, one-off implementations, often for event handling or callbacks).

Using inner classes not only improves encapsulation but also keeps the code for closely related classes together, making it easier to manage and understand. For instance, in a Playlist class, having a Song inner class makes it clear that Song objects are meant to be managed by Playlist, and it hides the Song implementation from the rest of the application if you make it private or protected. This design strategy is particularly powerful in large projects, where logical grouping and encapsulation are key to maintainability and clarity.

 

Sunday, June 15, 2025

equals()

Now that we’ve created plenty of objects in Java and we’ve seen several data structures, we need to address a very large elephant in the room. I still haven’t told you how to check—easily, anyway—if two objects are “equal”, or what that means about a non-primitive, anyway.


Let’s start with the easy stuff—equality in primitives:

  • Integers are equal if their values are the same. 2 is equal to 2, but not to 327.
  • Doubles are equal if their values are the same: 3.14159 is equal to 3.14159, but not 3.15
  • And so on with shorts, bytes, longs, and floats
  • Characters are equal if they’re the same: ‘A’ is equal to ‘A’, but not ‘a’
  • Booleans are equal if they have the same value: false and false are equal, but false and true are not

Because they’re primitives, we learned we make comparisons with == (or !=, or whatever you want), since single = is used for assignment, so it can’t be lifted directly from math.

Objects are a little trickier. Equality by = still works, but there’s a catch. That checks if the memory addresses of two things are the same.  

So let’s suppose I have

String s1 = “hello”;

and


String s2 = “hello”;

 

Intuitively, there’s nothing different about s1 and s2. Both have exactly the same contents, an all-lowercase “hello” and nothing else.

But Java doesn’t see those as the same. s1 and s2 are different objects, stored in different places in memory, so their addresses are different, so s1==s2 would return false.

Now, in Java, we have a concept called aliasing.

Let’s say we do the following instead:

  • We create String s1 = “hello”; as before
  • But now, instead, we sat String s2 = s1;

Now, s2 is an alias of s1—s2 and s1 point to the same location in memory.

Suppose s2 then changes to “oi” instead. After this change, s1 points to “hello” (nothing changed), but s2 points to the brand new, different, in a different place, “oi”.

Let’s see that in action:

class Main {

    public static void main(String[] args) {

        String s1 = "hello";

// s2 points to the same place
String s2 = s1;

// hello       
System.out.println(s1);

// hello       
System.out.println(s2);

s2 = "oi";

// hello      
System.out.println(s1);

// oi       
System.out.println(s2);

    }

}

While it is still true that s1 and s2 point to the same place (before a new String is created, when s2 becomes “oi”), then s1==s2 would return true. But once the value of s2 changes, s1 and s2 no longer point to the same place, so s1==s2 would return false.  

Without aliasing, let’s consider the following scenario:

class Main {

    public static void main(String[] args) {

       

        String s1 = "hello";

        String s2 = "hello";

    }

}

In this case, is it true that s1==s2? Again, it feels like it should be true, but it’s false. By not saying s2=s1 as before, we are telling the computer that s1 and s2 are completely different objects, which we don’t intend to point to the same place in memory.

But we, wise humans who can read and know there’s more to the story, know the contents of the Strings are identical. Surely, then, there must be a way to check “even if the Strings don’t have the same address, are they equal in the sense that every character in one matches every character in the other?”

In fact, most of the time, you won’t care where things are stored in memory—after all, this isn’t C, so you aren’t allocating and freeing memory yourself, leaving that all to Java. What you’ll most likely want to know is whether the contents of a String are equal to that of another.

For that, we have the equals() method.

equals() isn’t unique to Strings, however. All objects—whether you created them, or someone else did, like String, or Random, or anything else—since they descend from the Object class (this is never stated explicitly, but it’s always true) inherit some of its methods. If you didn’t when we covered this fact when we looked at inheritance, then I suggest you look at the API documentation now. You’ll see that one of the methods in Object—thus inherited by every class, whether you use it or not—is the equals() method.

Recall the difference between overloading and overriding. Overloading is, for example, having an add() method that takes in 2 ints, another that takes in 2 doubles, another for 2 bytes, another for 2 longs, etc. Overriding, meanwhile, is changing the behavior of whatever you inherited, to better fit the needs of the child class. The default behavior is exactly what we saw earlier, with the equality (or not) of the addresses of “hi”.  

Since you’ve inherited that method from Object, you can, of course, override it to better suit your needs. String has done this already, and String’s equals() checks if the contents are equal. If every character in two Strings is the same, they are the same.

Note that, for example, “I LIKE PIZZA” is not equal to “I like pizza” or “i like pizza” or “ I LIKE PIZZA” or “I LIKE PIZZA ”. “I LIKE PIZZA” matches exactly only with “I LIKE PIZZA” and only in that instance would equals() return true.

Suppose you have a Student class and you want to check if two Students are the same. Suppose a Student has an ID, name, date of birth, year, GPA, and major. You could certainly plausibly override the equals() in Student to return true if and only if all those fields—ID, name, date of birth, year, GPA, and major—all are exactly the same. You may have "the same" Student in different places in memory, so what you care about are the stats, not the memory addresses. If they are all the same, then presumably you and your program would both conclude that the Students are the same. But if any of those properties mismatch, you’re looking at different Students.

How you define equals() is largely up to you. Just make sure that whatever criteria you pick make sense and are specific enough to get the definition of equality you want—and be aware that, if, in the course of comparing Puppy objects, you need to check if their owner Strings are the same, then leading and trailing spaces, or capitalization, all matter when detetmining equality or not. 

Monday, June 9, 2025

Generic Types

Generics allow you to write code that works with any data type, making your classes and methods more reusable and type-safe. Instead of writing the same code for every type (imagine how much repetition you would need, if you had to cover every type you can come up with!), you can write it once and use it for many types.

Here’s a simple generic class that can store any type of data:

// A generic class that can store any type of data
class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<Integer> intBox = new Box<>(123);
        Box<String> strBox = new Box<>("Hello Generics!");

        System.out.println(intBox.getValue()); // Output: 123
        System.out.println(strBox.getValue()); // Output: Hello Generics!
    }
}



Here, T is a placeholder for the type you want to use. When you create a Box<Integer>, T becomes Integer; for Box<String>, it becomes String. 

You can also make methods generic:

public class Main {
    // Generic method
    public static <T> void printItem(T item) {
        System.out.println("Item: " + item);
    }

    public static void main(String[] args) {
        printItem(42);           // Output: Item: 42
        printItem("Java");       // Output: Item: Java
    }
}



This method works with any type you pass to it because you're passing a T, a generic. A "T" can be a CollegeStudent, a FluffyUnicorn, a PepperoniPizza, whatever you want.

Java has primitive data types like int, double, and char. However, sometimes you need to use objects instead of primitives—for example, when working with collections like ArrayList. That’s where wrapper classes come in.
What Are Wrapper Classes?

Wrapper classes “wrap” primitive values in an object. The name of the wrapper class is, capitalized, the written-out name of the data type (as in, "Integer" or "Double", etc.)


You use wrapper classes whenever you need an object instead of a primitive.

You can’t use primitives in collections like ArrayList, as we've already seen. You must use their wrapper classes:

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        // ArrayList<int> numbers = new ArrayList<int>(); // Invalid!
        ArrayList<Integer> numbers = new ArrayList<>();   // Valid!
        numbers.add(10);
        numbers.add(20);

        System.out.println(numbers); // Output: [10, 20]
    }
}



Here, Integer is used instead of int.


Autoboxing is when Java automatically converts a primitive to its wrapper class when needed. Unboxing is the opposite process by which Java automatically converts a wrapper object back to a primitive.


Integer myInt = 5;      // Autoboxing: int to Integer
int num = myInt;        // Unboxing: Integer to int

System.out.println(myInt); // Output: 5
System.out.println(num);   // Output: 5



This makes it easy to switch between primitives and objects.


Generics make your code flexible and type-safe, letting you write reusable classes and methods.


Wrapper classes let you use primitive values as objects, especially useful in collections and when object methods are needed.


Java handles most conversions between primitives and wrapper classes automatically (autoboxing/unboxing).

These concepts are foundational for modern Java programming—mastering them will make your code safer, cleaner, and more powerful

Monday, June 2, 2025

Inheritance

 Everyone agrees that, for example, a Dog is an Animal, biologically speaking. In Java, just as in biology, this is called “inheritance.”


Without giving too many specifics, let’s say we want for the Dog class to receive some behaviors from Animal, or we want to slightly tweak Animal’s behavior as it comes across in a Dog.

Before we go any further, it’s important to realize the obvious fact that all Dogs are Animals, but not necessarily all Animals are Dogs. This matters when we are talking about inheritance.

Let’s say an Animal has:

  • a name—this is a String
  • an age—this is an integer
  • a type—this is a String
  • a color—this is a String
  • a munch(int) method—this represents that the Animal can eat, and takes in a number; “much” is then printed that many times, each time on a new line
  • and a makeSound(int) method—this represents that the animal can make a sound, and takes in the number of times that sound should happen; the default sound is “moo”

We like all that in Animal, but obviously, Dogs don’t “moo”, they “woof.” Through inheritance—linking Animal and Dog in a way that gives Dog Animal’s properties for free—we can then say “in the special case that an Animal is a Dog, change the makeSound() behavior to ‘woof’ from ‘moo.’”

We imply the “is a” relationship (as in, “a Dog is an Animal”) with the extends keyword in Java.

  • Implement Animal
  • Then start working on Dog, declared as public class Dog extends Animal
  • Then, when you create a Dog object—let’s say, by Dog lucky = new Dog();, you can call lucky.makeSound(3); and you will see
    Moo
    Moo
    Moo


But Lucky (my actual dog) is a Golden Retriever, not a cow, so he does not moo.

Inside Dog, we can then change this behavior by declaring the following method: one that has the same signature as makeSound() did in Animal, but appears exclusively in Dog.

Inside that, we can print “Woof” instead, and when lucky.makeSound(n) is called, we will see many “Woof”s instead of many “Moo”s. 

There are two things we can do with those method signatures: overload and override.

  • Overloading is when you use the same name with different parameters, like two versions of an add() method, one that takes in ints and another that takes in doubles
  • Overriding, on the other hand, is when Dog extends Animal and Dog’s makeSound() says “Woof” when the generic Animal makeSound()  says “Moo”—you change the behavior in the more specific class to fit your needs, from what it was in the more generic.
It is a fact that everything you ever create—a MathLesson, a HelloWorld, a Puppy, a Student—is a child of Object. A MathLesson is an Object; a HelloWorld is an Object; and so on. This is so universally true that it is never explicit.

Since you get access to all the public fields and methods of an object upon inheritance from that—this is why Dog objects can, for free, get the default behavior of makeSound() and the other Animal fields and methods—every object ever has several common methods before anything is ever explicitly defined.

Two of the most common methods from Object are:

  • hashCode(): which feeds the object in question (that is, one particular FluffyUnicorn) through a function that is only easily computable in one direction (and nearly impossible in the other, with the caveat that checking the original direction’s result is just as easy), to get a unique identifier for that object
  • toString(): which finds some way—by default, with where the object is in memory—of converting the object to a String for visual representation

If I have a CollegeStudent who extends Student (explicitly, and, of course, Object, implicitly), where CollegeStudent samJones is in memory is of next to no value to anyone. I would much rather know what samJones.major, samJones.year, samJones.college, samJones.GPA, etc., are. So, many times, people override the toString method to something much more useful, like a “{“ plus something like “GPA:“ + samJones.GPA, and then comma-separating the name of the property and its value, before closing out with a matching “}”

like, for instance

    @Override

    public String toString() {

        return "CollegeStudent{" +

                "name='" + name + '\'' +

                ", idNumber=" + idNumber +

                ", age=" + age +

                ", creditHours=" + creditHours +

                ", points=" + points +

                ", GPA=" + String.format("%.2f", getGPA()) +

                ", courses=" + courses +

                '}';

    }

which might produce an output something like CollegeStudent{name='Alice Smith', idNumber=12345, age=20, creditHours=30, points=90, GPA=3.00, courses=[Math 101, English 201]}

The @Override line is called an “annotation.” They’re good practice but omitting them is not an error that will trip the compiler. They simply tell the compiler that some default behavior is being replaced by more desirable behavior.  

Overloading and overriding are dealt with by the compiler at different times: overloading at compile-time and overriding only at runtime.

Extending a parent class into a child (in “Dog extends Animal,” Animal is the parent class of Dog, since all Dogs are Animals but not vice versa) gives you access to anything public in the parent class, for free. It also means that, for instance

Animal myAnimal = new Dog();

Is valid, since every Dog is an Animal, but that

Dog myDog = new Animal() is not allowed, since not every Animal is guaranteed to be a Dog.

This (correct usage, like Animal myAnimal = new Dog()) is an example of polymorphism in Java, which is a really important concept. I cannot stress enough how critical it is to understand this relationship, and why the first of these two last code snippets works, and why the second one does not.

Finally, let’s say I have a class Percheron. There are some properties in Horse I would like to get, and some other particular properties in DraftHorse I would like to get. In some languages, you could certainly say

public class Percheron extends DraftHorse, Horse

and be totally fine.

However, this is called “multiple inheritance” and, except for the fact that everything implicitly inherits from Object, this (inheriting from both Horse and DraftHorse) is not allowed at all in Java. You must pick whether Percheron should inherit from Horse or DraftHorse, and then if it extends any of them, it must extend at most one of them. 


Tuesday, May 27, 2025

Implementing Dog

 In our last article, I mentioned that there are two kinds of constructors—those taking arguments and those that don’t. The one that doesn’t have any arguments exists by default, unless others are created, in which case it must be created explicitly.

To understand constructors, we need to learn a new keyword in Java: this.

Suppose I have a Dog class.

I want that class to have:

  1. My dog’s name
  2. My dog’s owner’s name
  3. My dog’s age
  4. My dog’s breed

This is because name, age, owner’s name, and breed are fundamental to who a dog is.

Suppose I want to also be able to let my Dog bark—simply by printing “woof!” to the screen

I can do this by creating a class to represent my dog.

public class Dog{

public static void main(String[] args){
}

}

So far, everything looks familiar, right? Now, for the new parts.

We’ve created variables inside methods before, like this:

public static void doSomething(){
        int x = 0;
}

For a reason we’ll address very shortly, that x only belongs to doSomething, not to anything else around it.

We want a way to make the Dog’s age, breed, owner, etc., intrinsic to the Dog, and so visible to all of the Dog class. For this, we need what are called instance variables—those declared not inside a method (like x), but only inside a class.

That looks something like

public class Dog{

        public String name;
        public  String owner;
        public int age;
        public String breed;

         public static void main(String[] args){
         }

}

By placing those variables there, we make them a part of any (every) Dog, not just one or two methods, and not just one or two Dog objects.

Obviously, it does us no good to have variables but not be able to look inside them and know their values, or change them. For this, we need some methods called getters and setters. These, appropriately, get back the values of variables, or set them to be certain values. The names these methods take should correspond to that. For instance, setAge sets the Dog’s age; getOwner() gets the owner of a Dog; and so on.

Fully implemented, the getters and setters look something like this:

public class Dog {

    private String name;

    private String owner;

    private int age;

    private String breed;


public Dog(String name, String owner, int age, String breed) {

        this.name = name;

        this.owner = owner;

        this.age = age;

        this.breed = breed;

    }

public Dog(){

}

    public String getName() {

        return name;

    }

    public void setName(String name) {

        this.name = name;

    }

    public String getOwner() {

        return owner;

    }

    public void setOwner(String owner) {

        this.owner = owner;

    }

    public int getAge() {

        return age;

    }

    public void setAge(int age) {

        this.age = age;

    }

    public String getBreed() {

        return breed;

    }

    public void setBreed(String breed) {

        this.breed = breed;

    }

}

All these methods make use of the “this” keyword for a rather simple reason: the parameter’s name in the constructor or setter is the same as the property’s name. What we mean by this is, for example, with

    public void setBreed(String breed) {

        this.breed = breed;

    }

This means, “Set the breed field of the current Dog to be the parameter we just passed”, and likewise with the others. We do this since it would get very confusing if the parameter name and the field name were the same, without this modification. As you’re getting used to constructors and setters, I highly recommend doing things by hand and color-coding—perhaps with pens or highlighters—what it is you mean for your code to do before actually writing it. And remember: direction matters in assignment, and flows from right to left!

Now, implementing the last thing on my wishlist is trivial: Into the Dog class, I add

public void sayWoof(){
        System.out.println(“Woof!”);
}

and I’m done! All we need now is a main method (public static void main(String[] args), of course!) and we’re all set

Switch

 Other than if/if-else/if-else if-else and the ternary operator, there is yet another common and important conditional expression in Java th...