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.

 

Monday, June 16, 2025

Sorting objects with Comparable or Comparator

 Some things—primitives—are really easy to sort. 1 comes before 2 comes before 3; z comes before q comes before c comes before a (if we’re going in reverse); and so on. But not everything we want to sort is so easy. Suppose, for example, we wanted to sort Students. Students have many properties—names, IDs, years, GPAs, majors, and so on. How do we determine who comes first? What criteria matter? Does a senior with a low GPA come before a junior with a high GPA? Does a zoology major early in the alphabet come before or after an African-American Studies major late in the alphabet?  


When it comes to sorting custom objects like Students in Java, you have two main tools at your disposal: the Comparable interface and the Comparator interface. Both serve to tell Java how your objects should be ordered, but they do so in different ways and are suited to different scenarios. If your class has a natural way of being ordered—say, students by their ID number or last name—you can implement the Comparable interface. This means you define a compareTo() method inside your Student class that tells Java how to compare one Student to another.

compareTo() returns one of three numbers—by convention, -1, 0, or 1. If x comes before y however you want to order it, then x.compareTo(y) returns -1; if x comes after y, then the same call returns 1; if they’re equivalent, then it returns 0.

For example, you might decide that students should be sorted alphabetically by last name. Now, whenever you call Collections.sort() on a list of Students, Java will use this method to determine the order. This is great when there is a single, obvious way to sort your objects.

However, if you want to sort Students by GPA one day, by year the next, and by major after that, the Comparator interface is the better choice. A Comparator is a separate object that defines a compare() method, allowing you to specify different sorting strategies without changing your Student class. For instance, to sort by GPA, you can create a Comparator that compares the GPA values of two students.

 Comparable requires you to implement the compareTo() method inside your class, establishing one default sorting behavior. Comparator, on the other hand, lets you define multiple external sorting strategies through separate objects or lambda expressions. By understanding and applying these two interfaces, you can efficiently sort even complex objects like Students in Java, tailoring the sorting logic to fit your specific needs.

Let’s look at both in action. First, Comparable:

import java.util.Arrays;

 

class Student implements Comparable<Student> {

    private String name;

    private int id;

 

    public Student(String name, int id) {

        this.name = name;

        this.id = id;

    }

 

    public String getName() {

        return name;

    }

 

    public int getId() {

        return id;

    }

 

    @Override

    public String toString() {

        return "Student [name=" + name + ", id=" + id + "]";

    }

 

    @Override

    public int compareTo(Student other) {

        if (this.id < other.id) {

            return -1;

        } else if (this.id > other.id) {

            return 1;

        } else {

            return 0;

        }

    }

}

 

public class ComparableExample {

    public static void main(String[] args) {

        Student[] students = {

            new Student("Alice", 103),

            new Student("Bob", 101),

            new Student("Charlie", 105),

            new Student("David", 102)

        };

 

        System.out.println("Students before sorting:");

        for (Student student : students) {

            System.out.println(student);

        }

 

        Arrays.sort(students);

 

        System.out.println("\nStudents after sorting by ID:");

        for (Student student : students) {

            System.out.println(student);

        }

    }

}

The fact that Student implements Comparable means that Students are comparable. Notice how in main(), the ordering by IDs is not the same as the “Alice, Bob, Charlie, David” ordering we’d get by names. The fact that we want to order by ID, not by name, is defined in the compareTo() method, a method that must be implemented if something is Comparable.

compareTo() rearranges the students in order by ID as we want them, based on the results of those comparisons returning -1, 0, or 1.

Now in this other example, a whole new class—a Comparator—is invoked to make the comparison, by way of Comparator’s compare() method. Note that this time, the method is just “compare,” not “compareTo” as with Comparable.

import java.util.Arrays;

import java.util.Comparator;

 

class Student {

    private String name;

    private int id;

 

    public Student(String name, int id) {

        this.name = name;

        this.id = id;

    }

 

    public String getName() {

        return name;

    }

 

    public int getId() {

        return id;

    }

 

    @Override

    public String toString() {

        return "Student [name=" + name + ", id=" + id + "]";

    }

}

 

public class ComparatorExample {

    public static void main(String[] args) {

        Student[] students = {

            new Student("Alice", 103),

            new Student("Bob", 101),

            new Student("Charlie", 105),

            new Student("David", 102)

        };

 

        System.out.println("Students before sorting:");

        for (Student student : students) {

            System.out.println(student);

        }

 

        // Define a Comparator to sort by name

        Comparator<Student> byName = new Comparator<Student>() {

            @Override

            public int compare(Student s1, Student s2) {

                return s1.getName().compareTo(s2.getName());

            }

        };

 

        Arrays.sort(students, byName);

 

        System.out.println("Students after sorting by name:");

        for (Student student : students) {

            System.out.println(student);

        }

    }

}

Here, the comparison strategy changes. Comparing strings is very easy to do with compareTo. (Strings are naturally Comparable.) Feeding two strings into compareTo() as we have done here will put them in A-Z alphabetical order. (As an exercise, think about how you could reverse this to Z-A order.) Therefore, the Comparator, calling Comparable, puts the Students in order by name, as opposed to ID in the previous example.

If you have an array that isn’t easily sorted (as in our case of the array of Students, since Students have more than one property), Java allows you, in Arrays.sort, to pass in a Comparator to tell sort on what criteria to perform the sort.

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. 

Saturday, June 14, 2025

Sets

 In plain English, “set” and “group” mean the same thing—but not in math or computer science. In computer science, a “set,” properly, is a collection of things where each thing is unique: {red, blue, yellow, turquoise, white, black, fuchsia}, or {1, 2, 3, 5, 8, 13, 21}—but not {P, E, N, N, S, Y, L, V, A, N, I, A}.


(As you’ll see soon, the last of these is not, in the Java sense, a “Set”.)

Java maintains Sets, and they have a number of use cases.  At this basic level, perhaps the most obvious is transforming a non-unique collection into a unique collection. That may look like, as roundabout as this is (it works):

  1. Make a List of things
  2. Look at one element at a time
  3. If the element is unique, add it to the Set
  4. Once everything is processed, make a List out of the contents of the set, and you’ll have all the original List, but no duplicates anymore

As usual, before we go further, let me put it out there that there exists a ton of great documentation on Sets, if you know where to look.

(In fact, if you look through the documentation, you’ll see that the above-described 4-step process could be just one—make a call to Set.of(), which will create a set out of whatever you pass.)

Sets are intended to behave just as their mathematical cousins would, so in addition to using of() to quickly make a Set out of something else, you should of course know that there is add() to add things one by one (if they can be added; if thery’re there already, they’ll just be ignored), or addAll() to add all contents of a Collection—more on this later, but the most common you’ve seen so far is a List; or remove (or removeAll, again, passing in a Collection); size(), which gives the count of unique elements in the set; or toArray(), which converts the Set into an array of elements of the type of the Set. Of course, just like with Lists, Queues, and Stacks, it’s quite useful in many circumstances to know if a Set is empty or not; naturally enough, the method for this (as has previously been the case) is isEmpty().

Other operations you might want to do include easily combining several sets into one, checking if a particular item is present or not, or deleting everything inside. These are all supported, and the documentation online does a great job of walking you through how to use these methods—how to call them and what to expect back.

We’ll soon get to dealing with graphs (not bar graphs and pie charts, but rather networks of dots and lines), and many of the algorithms we’ll work through on graphs—certain searching algorithms, or path-finding ones, for example—use Sets extensively.

Data structures like Lists, arrays, Maps, Queues, Stacks, and Sets—you already know 6, and soon, you’ll know many more, so go ahead and celebrate!—can be daunting at first. (I know that when I was taking Data Structures, at least in the beginning, it felt like climbing Mount Everest. But there is good news: It does get easier, and you do get better over time! Don’t worry!)

                                                                                                                          

Friday, June 13, 2025

Nulls

 Every data type has a default value. For primitives, if they’re numbers, its their form of 0. For booleans, it’s false. For the char type, it’s ASCII 0—which is not literally the character ‘0’, but a NULL value, a sort of way to say “this space contains a thing, and that thing is ‘nothing’”, as distinct from “this is an empty box.”

For objects, then, of any sort, the default value is null. “null” is reserved in Java, so just as you can’t name a variable “int” or “for” or “while,” you can’t name it “null” either.

When you create an object, before it gets a value, if you try to access it—say, to print out its contents—the value you will get on the other end will be null.

If, for example, you’re moving along a List, and you’ve reached the end of the list, then the “next” pointer won’t point to another element of the list, but will point to null, and accessing that phantom element (the one after the last one) will produce a NullPointerException.

This behavior is a core part of how Java manages memory and object references. The use of null is both practical and intentional—it signals that a reference exists, but it doesn’t point to any actual object in memory. However, this design also introduces a common pitfall: attempting to call methods or access fields on a null reference will trigger a NullPointerException. For example, if you try to call .length() on a String variable that’s null, or .next on a null node in a linked list, Java will throw this exception to alert you that your code is trying to operate on nothingness. To avoid these errors, it’s considered best practice to perform explicit null checks before accessing object members.

Java developers often encounter null in more complex scenarios, such as when working with collections or APIs. For instance, some methods may return null to indicate the absence of a value—like when searching for a key that doesn’t exist in a Map, or when a database query finds no matching row. In these cases, it’s up to the programmer to decide how to handle null values: should they throw an exception (we’ll cover this very soon), return a default value, or perhaps propagate the null up the call stack? This decision can have significant implications for the robustness and clarity of your codebase.

Look at this example:

public class NullExample {

    public static void main(String[] args) {

        String message = null; // message is declared but not set to a real string

 

        // This would throw a NullPointerException if uncommented:

        // System.out.println("Message length: " + message.length());

 

        // Safe way: check for null before using message

        if (message != null) {

            System.out.println("Message length: " + message.length());

        } else {

            System.out.println("Message is null!");

        }

    }

}

The String message is declared null, so accessing it a few lines later (that part is commented out, so it woud not execute) would return an error that, in this case, would crash the program. That is why it’s so important to take the safe approach and check whether something is null or not, and only take action on it if we know it isn’t null, as we do at the end. 

Thursday, June 12, 2025

HashMaps

 A map is a very powerful data structure that lets us relate one thing to another in pairs. One of these things is a “key,” and the other is a “value,” so we call the pairs “key-value” pairs. For now, know that one of the benefits of maps is that they have very efficient lookup. Whereas arrays or lists might require a linear search, there is some magic going on (which we will eventually cover) that allows maps to find whatever you are looking for almost instantaneously, both in the human-time sense and the computer-time sense.

As I’m sure you’ve gathered by now, I’m a huge fan of putting links to official Java documentation in these articles. Look at it here.

If you open the documentation link, you’ll immediately see how HashMaps are created: with the declaration of two types in the angle brackets, just as we had one type in angle brackets for Lists, Queues, and Stacks. Recall that, since we need both a key and a value, we need the two types this time, not just one.

Look at this example code snippet:

import java.util.HashMap;

 

public class HashMapExample {

    public static void main(String[] args) {

        // Create a HashMap

        HashMap<String, Integer> map = new HashMap<>(); // notice the pair of types

 

        // Add key-value pairs

        map.put("Apple", 3);

        map.put("Banana", 5);

        map.put("Orange", 2);

 

        // Access a value by key

        System.out.println("Quantity of Apples: " + map.get("Apple"));

 

        // Check if a key exists

        if (map.containsKey("Banana")) {

            System.out.println("Banana is in the map.");

        }

 

        // Remove a key-value pair

        map.remove("Orange");

 

        // Iterate through the HashMap

        for (String key : map.keySet()) {

            System.out.println(key + ": " + map.get(key));

        }

    }

}

First, notice the import statement and how we declare the HashMap. (The declaration is legal because every HashMap is a Map.) Second, take note of the names of the operations: the “add” operation is called “put,” and the “retrieve” operation is called “get.” To check if a particular key exists, there is a containsKey() method; for values, there is a similar containsValue(). Removing a pair—not just a key, or not just a value—is done by remove(), passing in the key. get() takes in a key and returns the associated value.

In this case, the mapping is one-directional: “Delta 105” maps to “Atlanta to Guarulhos.” Naturally, “Atlanta to Guarulhos” should map to “Delta 105,” since it does in the real world: the delta flight from ATL to GRU is 105; and Delta 105 is the flight from ATL to GRU. These kinds of maps do exist, but the HashMap is not one by default. To get bidirectional mapping, either make two HashMaps with opposite keys and values (the key of one is the value of the other) or use a BiMap. Google has an implementation in their Guava library, but, whenever I’ve wanted to do something bidirectional, I have always found it easier to write (and to understand later) the code that just uses two opposite maps the regular way.

This is how bidirectional mapping would look:
import java.util.HashMap;

import java.util.Map;

public class SimpleBiMap<K, V> {

    private final Map<K, V> forwardMap = new HashMap<>();

    private final Map<V, K> reverseMap = new HashMap<>();

 

    public void put(K key, V value) {

        if (forwardMap.containsKey(key) || reverseMap.containsKey(value)) {

            throw new IllegalArgumentException("Key or value already exists in the bimap");

        }

        forwardMap.put(key, value);

        reverseMap.put(value, key);

    }

 

    public V getValue(K key) {

        return forwardMap.get(key);

    }

 

    public K getKey(V value) {

        return reverseMap.get(value);

    }

 

    public void removeByKey(K key) {

        V value = forwardMap.remove(key);

        if (value != null) {

            reverseMap.remove(value);

        }

    }

 

    public void removeByValue(V value) {

        K key = reverseMap.remove(value);

        if (key != null) {

            forwardMap.remove(key);

        }

    }

}

 

public class AirlineRouteMap {

    public static void main(String[] args) {

        SimpleBiMap<String, String> routeMap = new SimpleBiMap<>();

 

        // Assign routes

        routeMap.put("Delta 105", "Atlanta to Sao Paulo");

        routeMap.put("United 222", "New York to London");

        routeMap.put("American 333", "Los Angeles to Tokyo");

 

        // Look up by flight code

        System.out.println("Route for Delta 105: " + routeMap.getValue("Delta 105"));

        // Output: Route for Delta 105: Atlanta to Sao Paulo

 

        // Look up by route description

        System.out.println("Flight code for 'New York to London': " + routeMap.getKey("New York to London"));

        // Output: Flight code for 'New York to London': United 222

    }

}

Here, SimpleBiMap maintains the two opposite maps. AirlineRouteMap simply implements a specific case for airline routes: given a city pair, you get the number, and given the number, you get the city pair.

Keys must be unique in a HashMap; values need not be. Because of this, if you want to get all the keys, the data structure that is returned is a Set, and the data structure of all the values is a Collection. If what you want is a Set of key-value pairs, that can come out of a HashMap as the entrySet. Map has inside of itself the class Map.Entry<K,V>, so should you want a Set of key-value pairs rather than a Map, you can get it.  

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...