Prelude to Java

Image of Author
August 24, 2017 (last updated September 21, 2022)

Introduction

Java can be intimidating for people new to programming, or people coming from a dynamic language background. When I first learned Java (coming from Ruby and JavaScript) it felt esoteric, scary. It felt like there were many unknown unknowns: I didn't know what I didn't know.

In what follows, I will introduce some of the "unknown unknowns" I wish I knew when I began my Java adventures. The topics I will address are:

  • Java is not a scripting language
  • Types
  • Interfaces
  • Annotations
  • Generics
  • Use an IDE

Java is not a scripting language

"Scripting language" is not well defined, but whatever it is, Java ain't it. Java code has to be compiled into an executable first.

Let's walk through what it takes to actually print a "hello, world" in Java.

First, download the SDK from Oracle. Choose Java SE (the SE stands for Standard Edition). Next, write a file called Hi.java:

class Hi {
    public static void main(String[] args) {
        System.out.println("hello, world");
    }
}
  • Public means the main method is accessible anywhere.
  • Static means the main method is a class method.
  • Void means the main method does not return a value.

Next, compile it. On the command line run javac Hi.java (the c stands for compiler). It generates .class files from .java files. In our case, the Hi.java file contains a single class: Hi. Thus, javac will generate Hi.class. Once we have our class file, we can run java Hi on the command line to greet the world.

Types

Types are a way to identify objects you’re working with. For example, Pizza is the type of myPizza in the follow code:

Pizza myPizza = new Pizza("pepperoni");

This implies the existence of a Pizza class, which could look as follows:

public class Pizza {
    private String topping;

    public Pizza(String topping) {
        this.topping = topping;
    }
}

Explicitly typed languages like Java can perform robust type checking at compile time to determine if your code is coherent. It will not catch all problems, but it does increase reliability at runtime when compared to languages that lack type-checking compilation steps.

Interfaces

Java interfaces specify some, or all, of the behavior of an object. For example, we could define a PizzaMaker interface that specifies a bakePizza behavior:

interface PizzaMaker {
    Pizza bakePizza(Pizza pizza)
}

Notice that this does not define how the bakePizza function is implemented. That is a job for the class implementing this interface. What the PizzaMaker interface does is state that something claiming to implement PizzaMaker must implement some function called bakePizza. For example, we could have a Chef class that implements PizzaMaker:

public class Chef implements PizzaMaker {
    @Override
    public Pizza bakePizza(Pizza pizza) {
        [implementation code goes here]
    }
}

(@Override is an annotation. We will discuss them later.)

If Chef did not define a bakePizza function, while still claiming to implement PizzaMaker, the compiler would error (or your IDE would notice the problem and warn you).

Interfaces seemed cumbersome to me at first, but I have since come to appreciate the rigidity and specificity they provide.

Annotations

Java annotations (like the @Override we saw earlier) are generally referred to as "syntactic metadata". The @ sign is what designates an annotation. Annotations can inform the compiler to perform additional actions, like checking for non-nullity: @NonNull String string;. They can generate code on your behalf @Getter(lazy=true) private final String firstName;. They can augment behavior during runtime as well, such as handling network requests: @Controller class APIController {...}.

Custom annotations are used widely by frameworks and packages. The first two annotations used in the above paragraph came from project Lombok, and the last one came from the Spring framework. Java SE 8 also has many annotations out of the box.

A resource for learning more about annotations is the Java tutorial on annotations.

Generics

A generic is a type variable. Let’s imagine we want a list of waiters for our pizza restaurant, and the ability to add a new waiter to the list. We could implement a specific List interface:

public interface WaiterList {
    boolean add(Waiter waiter);
}

Or we could implement a more generic interface that can hold a list of any type, not just Waiter:

public interface List<E> {
    boolean add(E e);
}

This latter interface is defined with reference to a generic type, <E>. These are like parameters to a function, and so are often called "type parameters". You declare your generic type parameters within the <> section, often called "diamond notation". (There can be multiple generic type parameters, e.g., Pair <A, B>.)

The add function above takes an object of (generic) type E (represented by the parameter e) and adds it to the list.

The generic list interface above allows us to reuse it across multiple types. To do so, we replace E with the specific, non-generic, type we want:

Waiter waiter1 = new Waiter("Alice");
Waiter waiter2 = new Waiter("Bob");
List<Waiter> waiters = new ArrayList();
waiters.add(waiter1);
waiters.add(waiter2);

Generics DRY up code, increasing maintainability and reliability.

A resource for learning more about generics is the Java tutorial on generics.

Use an IDE

I think some of my initial Java confusions stemmed from my Vim-loving resistance to IDEs. I didn’t want to learn (what felt to me like) some bloated editor just to type a line of Java. I think this is wrong. Not all languages are alike. Different languages warrant different development environments, and for Java, IDEs give you superpowers. The reason for this is due to the Java language itself. Let me explain.

Java is on the extreme end of type safety, it's type system is static, manifest, and nominal. It arguably has the strongest type safety of any language ("strong" here is ill-defined). It sacrifices readability, writability, and rememberability all for an unbelievably reliable runtime.

All those sacrifices sound bad at first (and at last, too), but they come with a huge upside. At compile time, thanks to all the explicit typing, your IDE can infer a lot about your code. So much, in fact, that your code can almost write itself at times.

The downside of explicit typing is you have the misfortune of one day writing something like this:

Map<String, Pair<Int, List<Optional<Set<Double>>>>> map;

You might never say to yourself, "wow, that code looks really pretty", but, when you actually use that map, the IDE can discover the only other entity in your codebase with that type signature and auto-complete the rest.

TL;DR: use an IDE when writing Java.

Conclusion

I hope you enjoyed this prelude to Java, and I wish you luck in your future Java adventures!