allenfrostline

Notes on Java Basics


2024-05-14

This post covers some notes I took when learning Java from scratch (literally and oddly, I had no prior experience to this language at all) using Jakob Jenkov’s tutorials. I take no credit on any of the contents below except typos if any.

What is Java?

Java is a language stored in .java files, which are then compiled into Java byte code using the Java compiler, and the byte code is then executed using the Java Virtual Machine (JVM). Both the Java compiler and the JVM are part of the Java Development Kit (JDK).

Java is a type of byte code files stored in binary .class files.

Java is an interpreted language, which means the language gets compiled and executed by the JVM just like assembler instructions for a PC.

Java is a bunch of Application Programming Interface (API) which come bundled with the Java Runtime ENvironment (JRE) or with the JDK which also includes a JRE.

Java has evolved into three different sets of APIs:

Java Applets are Java programs that are downloaded and executed inside a web browser. Today those applets have died out (except for the popular game Minecraft) and been replaced by JavaFX

Installing Java SDK

You can install the latest JRE from here or the latest JDK from here which includes a JRE. To verify you have it properly installed, check

java -version

which on my machine (Macbook M3) prints

java version "1.8.0_411"
Java(TM) SE Runtime Environment (build 1.8.0_411-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.411-b09, mixed mode)

You can install multiple Java SDKs but usually it would be the latest version that is actually used when running java or javac. You can use the explicit full path to the correctly version, or let IDE help with that.

Your First Java App

A Java project is a collection of Java files (and possibly other files) that belong together in a project. For our very first Java app, (without really understanding what the following code is doing) we can have a simple MyJavaApp.java file with the following content:

public class MyJavaApp {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
};

To compile it:

javac MyJavaApp.java 

To run it:

java MyJavaApp

which prints in console:

Hello world!

Java Main Method

A Java program is a sequence of Java instructions that are executed in a certain order. In Java, all instructions (code) have to be located in side a Java class. A class is a way of grouping data and instructions that beloong together. Thus, a class may contain both variables and methods. A variable can contain data, and a method groups together a set of operations on data (instructions).

You can declare a simple class without anything like this:

public class MyJavaApp {
}

This Java code needs to be located in a file with the same file name as the class and ending with the file suffix .java. More specifically, the file name has to be MyJavaApp.java. Once the file is located in a file matching its class name and ending with .java, you can compile it with the Java compiler from the JDK.

It’s recommended to locate your class in a Java package, which is simply a directory in your file system which can contain one or more Java files. Packages can be nested, just like directories can normally. This is all you need to do if you want to put the class above inside the package myfirstapp:

package myfirstapp;

public class MyJavaApp {
}

Notice that the file now must be inside the directory myfirstapp.

A Java program needs to start its execution somewhere. It starts by executing the main() method of some class. You can choose the name of the class to execute but not the name of the method.

package myfirstapp;

public class MyJavaApp {
    public static void main(String[] args) {
        System.out.println("Hello world");
    }
}

We then can compile and run the program:

javac src/myfirstapp/MyJavaApp.java 
java -cp src myfirstapp.MyJavaApp

We can also pass arguments to the main() method, if we change the code to

package myfirstapp;

public class MyJavaApp {
    public static void main(String[] args) {
        System.out.println(args[0]);
        System.out.println(args[1]);
    }
}

Now we can compile again and run as follows

javac src/myfirstapp/MyJavaApp.java 
java -cp src myfirstapp.MyJavaApp Hello World

which prints

Hello
World

Java Project Overview, Compilation and Execution

A simple Java project contains a single directory inside which all the Java source files are stored. The Java source files are usually not stored directly inside the source directory (which is usually called src) but inside subdirectories matching their package structure. Once compiled, the compiler will produce a .class file for each .java file and it’s those .class files that the JVM can execute. It’s normal to separately generate these .class files in a different directory e.g. called classes via instructing the compiler, but it’s not a requirement:

javac src/myfirstapp/*.java -d classes

If we have all .class file inside classes already, we can execute like below:

java -cp classes myfirstapp.MyJavaApp First Second

Java Core Concepts

There are a handful of core concepts that are essential to the language.

Variables are typed and contains data. For example:

int myNumber;
myNumber = 0;
myNumber = myNumber + 5;

The first line declares a variable named myNumber of type int. The second and third lines modifies the variable.

Operations are instructions to process the data. Some operations read and write the values of variables, while others control the program flow:

For example:

int number = 0;
int abs = 0;

// assume some operations that modify the variables here

if (number >= 0) {
    abs = number;
} else {
    abs = -number;
}

Classes group variables and operations together in coherent modules. A class can have fields, constructors and methods (and more). Objects, on the other hand, are instances of classes. When you create an object, that object is of a certain class. The class is here like a template / blueprint telling how objects should look. For example, the following is a class with field brand and two methods Car (which is a constructor) and getBrand:

public class Car {
    private String brand;
    
    public Car(String theBrand) {
        this.brand = theBrand;
    }

    public String getBrand() {
        return this.brand;
    }
}

Interface is a concept that describes what methods a certain object should have available on it. A class can implement an interface.

Packages is another central concept, which is a directory containing Java classes and interfaces. Packages provide a handy way of grouping related classes and interfaces, thus making modularization of your code easier.

Java Syntax

A Java file can contain the following elements

Below is an example:

package javacore;

// example import (not used here though)
import java.util.HashMap;

public class MyClass {
    protected final String name = "John";
    
    {
        // class initializer
    }
    
    public MyClass() {
    }

    public String getName() {
        return this.name;
    }

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

Package declaration contains the keyword package and space and the name of the package, ending with a semicolon.

Import statement contains the keyword import and the package you want to import, ending with a semicolon.

Type declaration covers both variables and classes. A type can be a class, an abstract class, an interface, an enum, or an annotation. When the type is declared a class, the declaration is delimited by a pair of curly brackets.

Field declaration ends with a semicolon. A type (class/interface/enum) can have multiple fields.

Class initializer block is wrapped by a pair of curly brackets and is to be executed when an instance of the class is created. Alternatively, when we add the keyword static before the left bracket {, we’re telling the compiler that the initializer block is to be executed when the class is loaded and only once, since the class is only loaded in the JVM once.

Constructors are similar to class initializers except that they can take parameters.

Methods are functions that you can execute but only upon an instance, which is why they’re sometimes called “instance methods”. Similar to initializer block, you can add static to the method declaration to make it belong to the class instead of the class. That means you can then call a static method without instantiating the class.

Java Variables

In Java there are four types of variables:

Here are examples of how to declare variables of all the primitive data types in Java:

byte myByte;
short myShort;
char myChar;
int myInt;
long myLong;
float myFloat;
double myDouble;

and here is how to declare them as object types:

Byte myByte;
Short myShort;
Character myChar;
Integer myInt;
Long myLong;
Float myFloat;
Double myDouble;
String myString;

Assigning a value to a variable is as easy as

myByte = 127;
myFloat = 12.76;;
myString = "This is okay";

There are a few rules and conventions related to the naming of variables. The rules are:

The convensions are:

Starting from Java 10, it’s no longer necessary to specify the type of the variable when declared, if the type can be inferred from the value assigned to the variable:

var myVar = "This is a string";
var myList = new ArrayList();
var myClassObj = new MyClass();

Java Data Types

There are two data types in Java:

The following are the primitive data types:

The fact that they’re the primitive data types means that they’re not objects or references to objects.

The primitive types also come in versions that are full-blown objects. That means you can reference them via an object reference, that you can have multiple references to the same value, and you can call methods on them like on any other object in Java.

It’s worth noting that the object versions of primitive data types are immutable. This means you cannot modify the value, but only re-point the reference to another object.

There is, however, a concept called “boxing” in Java. Before Java 5, you have to call a method to get the value from an object:

Integer myInteger = new Integer(45);
int myInt = myInteger.intValue();

Starting from Java 5, you have “auto boxing” which means you can retrieve the value automatically (without explicitly calling a method)

Integer myInteger = new Integer(45);
int myInt = myInteger;

Similarly we have the same logic but the other way around:

int myInt = 45;
Integer myInteger = new Integer(myInt);   // before java 5
Integer myInteger = myInt;                // since java 5

There is one caveat, though, as an object type can point to null but a primitive type cannot. The following, for example, will compile fine but result in a NullPointerException:

Integer myInteger = null;
int myInt = myInteger;     // NullPointerException

Java Math Operators and Math Class

Java contains a set of built-in math operations for performing simple math operations on Java variables. The Java math opeartions are reasonably simple. Therefore, Java also provides the Java Math class which contains methods for performing more advanced math calculations in Java.

There are four basic math operators:

In terms of precedence, the multiplication and division (including modulo) have higher precedence than the addition and subtraction. Expressions inside parentheses have higher precedence than any other operator.

Integer division in Java behaves differently from normal math operations – the floating point part in the result will be cut off to keep the type consistent (integer).

As for floating point division, simply declaring the LHS as a double won’t do the magic :

double res = 100 / 8;   // the answer will still be 12

In order to force Java to properly run floating point operation, we need to declare the RHS as double first:

double x = 100;
double y = 8;
double res = x / y;     // the answer is now 12.5

or we can use type suffixes:

double res = 100D / 8D; // the answer is now 12.5

Java floating point data types are not 100% precise:

double res = 0D;
System.out.println("res = " + res);

for (int i=0; i<100; i++) {
    res += 0.01D;
}
System.out.println("res = " + res);

The above prints

res = 0.0
res = 1.0000000000000007

However, usually this imprecision is insignificant as long as we’re aware of that.

Enough for the basic operations. The Java Math class provides more advanced mathematical calculations when we need to do some real work. The fully qualified class name of the Math class is java.lang.Math. Here comes the code dump:

Math.abs(-10);            // 10
Math.ceil(5.6);           // 6.0
Math.ceil(2.1);           // 2.0
Math.floorDiv(-100, 8);   // -13 (not 12!)
Math.min(2, 3);           // 2
Math.max(2, 3);           // 3
Math.round(23.34);        // 23 
Math.random();            // random on [0, 1]
Math.exp(2);              // e^2 
Math.log(5);              // ln5
Math.log10(10);           // 1 
Math.pow(2, 3);           // 8
Math.sqrt(4);             // 2
Math.PI;                  // 3.1415926...
Math.sin(Math.PI);        // 0
Math.cos(Math.PI);        // -1
Math.tan(Math.PI);        // 0
Math.asin(0);             // 0 
Math.acos(1);             // 0 
Math.atan(0);             // 0
Math.sinh(1);             // 1.175201
Math.cosh(1);             // 1.543081
Math.tanh(1);             // 0.761594
Math.toDegrees(Math.PI);  // 180
Math.toRadians(180);      // 3.1415926...

Java Array

A Java array is a collection of variables of the same type. To declare a Java array:

int[] intArray;
int intArray[];  // yes, this works too
String[] stringArray;  // object types too

However, the above declaration doesn’t really instantiate the elements of the array. To properly instantiate an array, you need to

int[] intArray;
intArray = new int[10];

// or directly like below:
int[] intArray = new int[10];

// or write out the elements as array literals 
int[] intArray = new int[]{ 1,2,3,4,5,6,7,8,9,10 };

// or even omit the `new int[]` part:
int[] intArray = {1,2,3,4,5,6,7,8,9,10};

// this works for object types as well:
String[] stringArray = { "one", "two", "three" };

Once an array has been created, its size cannot be resized. In some programming languages (e.g. JavaScript) arrays can change their sizes after creation, but this is not the case in Java. If you need an array-like data structure that can change its size, you may be thinking about a list, or a user-defined resizable array class. In some cases you can also consider a Java RingBuffer which is essentially implemented using a Java array internally.

We can access elements in a Java array by its index:

intArray[0] = 0;
int firstInt = intArray[0];

We can access the size of a Java array through it’s length field:

int arrayLength = intArray.length;

There are two ways to interate through an array:

for (int i=0; i<arr.length; i++) {
    System.out.println(arr[i]);
}  // through explicit indexing and for loop

for (int i : arr) {
    System.out.println(i);
}  // through for-each loop

To expand the concept of 1d arrays to multidimensional, we can simply use multiple square brackets:

int[][] intArray = new int[3][5];
intArray[2][2] = 120;
int someInt = intArray[2][2];

for (int i=0; i<intArray.length; i++) {
    for (int j=0; j<intArray[i].length; j++) {
        System.out.println("i=" + i + " j=" + j);
    }
}

Notice there’s a convenient method to convert an array to a string for quick printing:

System.out.println(Arrays.toString(intArray));

which is housed under the java.util.Arrays class. The Arrays class also provides easy copying:

int targetArray = Arrays.copyOf(intArray, intArray.length);          // 0:to
int targetArray = Arrays.copyOfRange(intArray, 2, intArray.length);  // from:to

Sorting (inplace) is also implemented:

Arrays.sort(intArray);

which can get a little bit intimidating if the elements of the array are of an object type:

private static class Employee {
    public String name;
    public int employeeId;

    public Employee(String name, int employeeId) {
        this.name = name;
        this.employeeId = employeeId;
    }
}

Employee[] arr = new Employee[4];
arr[0] = new Employee("Ellen", 1);
arr[1] = new Employee("Betty", 2);
arr[2] = new Employee("Chloe", 3);
arr[3] = new Employee("Betty", 3);

Comparater comp = new Comparater<Employee>() {
    @Override 
    public int compare(Employee e1, Employee e2) {
        int nameDiff = e1.name.compareTo(e2.name);
        if (nameDiff != 0) {
            return nameDiff;
        }
        return e1.employeeId - e2.employeeId;
    }
};

Arrays.sort(arr, comp);
for (Employee e : arr) {
    System.out.println(e.name);
}

We can fill an array with Arrays.fill():

int[] intArray = new int[10];
Arrays.fill(intArray, 3, 5, 123);
System.out.println(Arrays.toString(intArray));

which will print (notice the range is only left-inclusive)

0, 0, 0, 123, 123, 0, 0, 0, 0, 0

We can also search a sorted array with Arrays.binarySearch():

int[] ints = {1,2,3,4,5};
int i = Arrays.binarySearch(ints, 3);  // returns 2
int j = Arrays.binarySearch(ints, 6);  // return -5 (negative index to be inserted in)

Lastly, we can check if two arrays are equal using Arrays.equals():

int[] ints1 = {0,2,4,6,8,10};
int[] ints2 = {0,2,4,6,8,10};
int[] ints3 = {10,8,6,4,2,0};

boolean ints1EqualsInts2 = Arrays.equals(ints1, ints2);
boolean ints1EqualsInts3 = Arrays.equals(ints1, ints3);

System.out.println(ints1EqualsInts2);
System.out.println(ints1EqualsInts3);

which prints

true
false

Java String

The Java String data type can contain a sequence of characters, like pearls on a string. Once a Java String is created you can search inside it, create substrings from it, create new strings based on the first but with some parts replaced, plus many other things.

A Java String before Java 9 is represented internally in the JVM using bytes, encoded as UTF-16, which means 2 bytes per character. From Java 9 and forward, the JVM can optimize strings using a new Java feature called compact strings. In order to create a string, we can

String myString = new String("Hello world");  // or 
String myString = "Hello world";  // using string literal
String myString2 = "Hello world";

Notice that JVM may only create a single String instance in memory for both myString and myString2. If you want to make sure that the two String variables point to separate String objects, you need to use the new operator and the constructor explicitly.

A Java text block, on the other hand, is a multi-line string that was added in Java 13 (in preview). You can create a text block as follows

String textBlock = """
                   This is a
                   multi-line
                   text block
                   """;

Notice how indentation works implicitly for text blocks:

String text1 = """
               This is a
               multi-line 
               text block
               """;
String text2 = """
               This is a
              multi-line
             text block
             """;
System.out.println(text1);
System.out.println(text2);

which prints

This is a
multi-line 
text block 

  This is a 
 multi-line
text block

To concatenate two strings, we can simply add them, or append one onto another:

String text1 = "Hello";
String text2 = "world";
String text3 = text1 + " " + text2;  // or:
String text3 = new StringBuilder(text1).append(" ").append(text2).toString();

Using the same logic, we can concatenate multiple strings in a for loop:

String[] strings = { "one", "two", "three", "four", "five" };

// first method 
String result = null;
for (String s : strings) {
    result += s;
}

// second method (same as the first)
String result = null;
for (String s : strings) {
    result = new StringBuilder(result).append(s).toString();
}

// third method (faster)
StringBuilder tmp = new StringBuilder();
for (String s : strings) {
    tmp.append(s);
}
String result = tmp.toString();

To get the length of a string:

String s = "Hello world";
int len = s.length();

To get just part of a string:

String substring = s.substring(0, 5);  // [0,5)

To search inside a string:

int idx1 = s.indexOf("wor");    // returns 6
int idx2 = s.indexOf("wow");    // returns -1 
int idx3 = s.lastIndexOf("o");  // returns 7

To check if a string matches a certain regex:

boolean match = s.matches(".*wor.*");  // returns true

A more general comparison between strings can be any of the following:

where s1.compareTo(s2) returns a negative number if s1 is earlier in sorting order than s2, zero if the two are equal, and a positive number if s1 comes after s2. Notice that compareTo won’t work correctly if we’re comparing across different languages. To properly compare multi-lingual contexts, use a Collator.

We can trim white spaces around a string with trim:

String s = "  hello world ";
String s_trimmed = s.trim();

We can replace characters with replace (for character), replaceFirst (for string) and replaceAll (for string):

String source = "123abcab";
String target = source.replace('a', '@');           // target is 123@bc@b
String target = source.replaceFirst("ab", "@p");    // target is now 123@pcab
String target = source.replaceAll("ab", "@p");      // target is now 123@pc@p

We can split a string with split:

String source = "a sentence has multiple words";
String[] arr = source.split(" ");      // ["a", "sentence", "has", "multiple", "words"]
String[] arr2 = source.split(" ", 2);  // ["a", "sentence has multiple words"]

We can convert a number to string with valueOf:

String intStr = String.valueOf(12);

and object to string with toString:

Integer integer = new Interger(123);
String intStr = integer.toString();

We can convert between upper/lower cases:

String source = "This is cool";
String target = source.toUpperCase();   // target is THIS IS COOL
String target = source.toLowerCase();   // target is this is cool

Starting from Java 13, we can string intendation from multi-line strings with stripIndent:

String source = "  Hey\n  This is\n         indented";
String target = source.stripIndent();
System.out.println(target);

which prints

Hey
This is 
indented

Java Operations

Variable assignment:

int age;
age = 25;

int yearBorn = 1995;

Variable reading:

String name = "Jacob";
String name2 = name;

Variable arithmetics:

int p1 = 123;
int p2 = 234;
int p3 = p1 + p2;

Object instantiation:

MyClass myClassInstance = new MyClass();

If statement:

if (amount > 8) {
    System.out.println("amount is greater than 8");
} else {
    System.out.println("amount 8 or less");
}

Switch statement:

switch(amount) {
    case 1:
        System.out.println("amount is 1");
        break;
    case 5:
        System.out.println("amount is 5");
        break;
    default:
        System.out.println("amount is something else ");
}

For loop:

for (int i=0; i<10; i++) {
    System.out.println("i = " + i);
}

While loop:

int counter = 0;
while (counter < 10) {
    System.out.println("counter = " + counter);
    counter++;
}

Method calls:

public class MyClass() {
    public void printBoth(String s1, String s2) {
        print(s1);
        print(s2);
    }

    public void print(String s) {
        System.out.println(s);
    }
}

Java Ternary Operator

The Java ternary operator is like a simplified if statement:

String tmp = "what";
String name = tmp.equals("what") ? "allen" : "erin";

Java Switch Statements

Besides the basic switch statements mentioned above, we can also switch on a Java enum:

public class SwitchOnEnum {
    private stataic enum Size {
        SMALL, MEDIUM, LARGE, X_LARGE
    }

    private static void switchOnEnum(Size size) {
        switch(size) {
            case SMALL:
                System.out.println("size is small");
                break;
            case MEDIUM:
                System.out.println("size is medium");
                break;
            case LARGE:
                System.out.println("size is large");
                break;
            case X_LARGE:
                System.out.println("size is x-large");
                break;
            default:
                System.out.println("size is not recognized");
        }
    }
}

We can also combine multiple cases for the same operation:

switch(key) {
    case ' ':
    case '\t':
        System.out.println("key is white space");  // combining both tab and whitespace
        break;
    default:
        System.out.println("key is not recognized");
}

or (starting from Java 13):

switch(key) {
    case ' ', '\t':
        System.out.println("key is white space");
        break;
    default:
        System.out.println("key is not recognized");
}

Java 12 added the switch expression as experimental feature, which is basically a switch statement that can return a value:

int digitIntDecimal = 12;
char digitInHex = 
    switch(digitIntDecimal) {
        case 0 -> '0';
        case 1 -> '1';
        case 2 -> '2';
        case 3 -> '3';
        case 4 -> '4';
        case 5 -> '5';
        case 6 -> '6';
        case 7 -> '7';
        case 8 -> '8';
        case 9 -> '9';
        case 10 -> 'A';
        case 11 -> 'B';
        case 12 -> 'C';
        case 13 -> 'D';
        case 14 -> 'E';
        case 15 -> 'F';
        default -> '?';
    };

Starting from Java 13 you can use yield instead of -> for switch expressions:

int tokenType = switch(token) {
    case '123': yield 0;
    case 'abc': yield 1;
    default: yield -1;
};

Java instanceof Operator

The Java instanceof operator will evaluate whether the object is of a certain class:

Map<Object, Object> map = new HashMap();
boolean mapIsObject = map instanceof Map;      // true
boolean mapIsObject = map instanceof HashMap;  // true 
boolean mapIsObject = map instanceof Object;   // true

Map<Object, Object> map = null;
boolean mapIsObject = map instanceof Map;      // false 

One of the most common uses of the Java instanceof operator is to downcast an object of supertype to a subtype:

Map<Object, Object> map = new TreeMap;

if (map instanceof SortedMap) {
    SortedMap sortedMap = (SortedMap) map;
    // do something assuming sortedMap is a SortedMap 
} else if (map instanceof Integer) {
    Integer intMap = (Integer) map;
    // do something assuming intMap is an Integer 
}

Java for Loops

A for loop in Java is

for (int i=0; i<10; i++) {
    // do something
}

Depending on how the loop iterator is defined, you don’t necessarily need a loop initializer:

int i = 0;
for (; i<10; i++) {
    // do something 
}

There is also for each loop introduced in Java 5:

String[] strings = {"one", "two", "three"};
for (String s : strings) {
    // do something
}

You can use continue and break to interfere the loop behavior.

Java while Loops

The Java while loop is similar to the for loop.

int counter = 0;
while (counter < 10) {
    System.out.println("counter: " + counter);
    counter++;
}

Alternatively, there’s do while loop:

int data;
do {
    data = inputStream.read();
} while (data != -1);

Both continue and break work just like in for loops.

Java Classes

A Java class can contain the following building blocks:

This is a simple class:

public class MyClass {
}

This is a class with three fields:

public class Car {
    public String brand = null;
    public String model = null;
    public String color = null;
}

This is a class with two constructors:

public class Car {
    public Car() {}

    public Car(String theBrand, String theModel, String theColor) {
        this.brand = theBrand;
        this.model = theModel;
        this.color = theColor;
    }
}

This is a class with a method:

public class Car {
    public void setColor(String newColor) {
        this.color = newColor;
    }
}

This is a class with a nested class:

public class MyClass {
    public static class MyNestedClass {
    }
}

Java Fields

A Java field is declared using the following syntax:

[access_modifier] [static] [final] type name [= initial_value];

where the square brackets [] mean that the part is optional. Only type and name are required.

The are four modifiers for Java fields:

In terms of whether a field is static:

As for final keyword: a final field cannot have its value changed once assigned. It’s worth noting that static comes with final a lot of the times, and the naming convention is to write the field name in all uppercase:

puublic class Customer {
    static final String CONSTANT = "Fixed Value";
}

Java Methods

All things similar to a general function, a method in Java has final keyword on its parameters as well. When a parameter is passed as final, it means the value of the parameter cannot be changed.

Java Constructors

A Java constructor is a special method that is called when an object is instantiated. In other words, when you use the new keyword. There can be multiple overloaded constructors for the same class:

public class MyClass {
    private int number = 0;
    public MyClass {}  // default one; not necessary
    public MyClass(int theNumber) {   // overload
        this.number = theNumber;   // you can also omit `this.` if there is no name collision:
        number = theNumber;
    }
}

You can call a constructor from another:

public class MyClass {
    private String firstName = null;
    private String lastName = null;
    private int birthYear = 0;

    public Employee(String first, String last, int year) {
        this.firstName = first;
        this.lastName = last;
        this.birthYear = year;
    }

    public Employee(String first, String last) {
        this(first, last, -1);
    }
}

We can also call the constructor of the superclass:

public class Vehicle {
    private String regNo = null;

    public Vehicle(String no) {
        this.regNo = no;
    }
}

public class Car extends Vehicle {  // inherited from Vehicle
    private String brand = null;

    public Car(String br, String no) {
        super(no);
        this.brand = br;
    }
}

Java Packages

The following is an example of a Java project with multiple packages (com, concurrency and concurrent, each potentially with subpackages).

To declare a package inside a Java source file:

package com.jenkov.navigation;

public class Page {
    ...
}

When two classes are located in different packages, we can import one from the other package so that it can be used together:

import another_package.B;

public class A {
    public static void main(String[] args) {
        B bObj = new B();
        bObj.doSomething();
    }
}

We can also import all classes from another package:

import another_package.util.*;

You can also just use the fully qualified class name without importing a class explicitly:

// without import line at all 

public class A {
    public static void main(String[] args) {
        another_package.util.TimeUtil timeUtil =
            new another_package.util.TimeUtil();
        timeUtil.doSomething();
    }
}

You can usually divide your classes into packages by layers or by their application functionalities.

Java Access Modifiers

A Java access modifier specifies which classes can access a given class and its fields, constructors and methods. Access modifiers can be specified separately for a class, its constructors, fields and methods. They can take one of four different types:

can be specified on private default protected public
class no yes no yes
nested class yes yes yes yes
constructor yes yes yes yes
method yes yes yes yes
field yes yes yes yes

Java Inheritance

Java Inheritance refers to the ability in Java for one class to inherit from another.

public class Vehicle {
    protected String licensePlate = null;
    public void setLicensePlate(String license) {
        this.licensePlate = license;
    }
}

public class Car extends Vehicle {
    int numberOfSeats = 0;
    
    public String getNumberOfSeats() {
        return this.numberOfSeats;
    }

    public String getLicensePlate() {
        return this.licensePlate;
    }
}

You can always cast an object of a subclass to one of its superclasses. This is called upcasting:

Car car = new Car();

// upcast to Vehicle 
Vehicle vehicle = car;

// downcast to Car again 
Car car2 = (Car) vehicle;

As shown above, it’s also possible to downcast from a superclass to a subclass, but only if the object really is an instance of that subclass already. For example, the following code will throw a ClassCastException:

Truck truck = new Truck();  // assuming Truck is another subclass of Vehicle 
Vehicle vehicle = truck;    // upcast: works 
Car car = (Car) vehicle;    // downcast: error 

When inheriting classes, you can override methods in the subclass as you like. However, it’s worth noting that in Java you cannot override private methods from a superclass. Even if you create a private override in the subclass, you will still be calling the private method from the superclass.

If you override a method in a subclass and would like the compiler to tell you if the original method is removed or its signature changed, you can use the @Override annotation:

public class Car extends Vehicle {
    @Override 
    public void setLicensePlate(String license) {
        this.liicensePlate = license.toLowerCase();
    }
}

If you want to call a method defined in the superclass but now overriden by the subclass, you can use the super reference:

public class Car extends Vehicle {
    public void setLicensePlate(String license) {
        super.setLicensePlate(license);
    }
}

A class can be declared final, which means it cannot be extended any more:

public final class MyClass {
}

Java Nested Classes

In Java nested classes are classes that are defined inside another class. There are four different types of nested classes:

A static nested class is declared as

public class OutClass {
    public static class InClass {
    }
}

To create an instance of InClass you must reference it by prefixing it with OutClass:

OutClass.InClass instance = new OutClass.InClass();

In Java these are essentially just a normal class that has been nested inside another class.

A non-static nested class in Java are also called an inner class. They are associated with the instance of the outer class:

public class OutClass {
    public class InClass {
    }
}

This is how you create an instance of InClass:

OutClass outInstance = new OutClass();
OutClass.InClass inInstance = outInstance.new InClass();

Non-staic nested classes have access to the fields (even private ones) of the outer class.

Local classes in Java are like inner classes that are defined inside a method or scope block {}:

class OutClass {
    public void printText() {
        class LocalClass {
        }
        Local local = new Local();
        // do something
    }
}

Anonymous classes are nested classes without a class name. They are typically declared as either subclasses of an existing class, or as implementations of some interface:

public class SuperClass {
    public void doIt() {
        System.out.println("SuperClass doIt()");
    }
}

SuperClass instance = new SuperClass() {
    public void doIt() {
        System.out.println("Anonymous class doIt()");
    }
};

instance.doIt();  // prints "Anonymous class doIt()"

You can declare fields and methods inside an anonymous class, but you cannot declare a constructor. You can, however, declare a static initializer for the anonymous class:

final String textToPrint = "Text...";

MyInterface instance = new MyInterface() {
    private String text;
    { this.text = textToPrint; } // static initializer 
    public void doIt() {
        System.out.println(this.text);
    }
};

instnace.doIt();

A nested class is typically only used by or with its enclosing class. Sometimes a nested class is even only visible to its enclosing class and used internally. An example would be a Cache class with a CacheEntry nested class inside it, so users cannot see or interact with the cache entries directly.

public class Cache {

    private Map<String, CacheEntry> cacheMap = new HashMap<String, CacheEntry>();

    private class CacheEntry {
        public long   timeInserted = 0;
        public object value        = null;
    }

    public void store(String key, Object value){
        CacheEntry entry = new CacheEntry();
        entry.value = value;
        entry.timeInserted = System.currentTimeMillis();
        this.cacheMap.put(key, entry);
    }

    public Object get(String key) {
        CacheEntry entry = this.cacheMap.get(key);
        if(entry == null) return null;
        return entry.value;
    }

}

Java Record

A Java Record is a special kind of class which has a concise syntax for defining immutable data-only classes. The Java compiler automatically generates getter methods, toString, hashcode and equals methods. For example:

public record Vehicle(String brand, String licenseePlate) {}

public class RecordsExample {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle("Mercedes", "UX 1238 A95");
        System.out.println(vehicle.brand);
        System.out.println(vehicle.licenseePlate);
        System.out.println(vehicle.toString());
    }
}

which prints

Mercedes
UX 1238 A95
Vehicle[brand=Mercedes, licensePlate=UX 1238 A95]

It’s important to notice that a record is by design final. However, you can define alternative constructors for a record, or define custom methods for it just like normal classes:

public record Vehicle(String brand, String licensePlate) {
    public Vehicle(String brand) {
        this(brand, null);
    }

    public String brandAsLowerCase() {
        return brand().toLowerCase();
    }
}

Java Abstract Classes

A Java abstract class is a class which cannot be instantiated, meaning you cannot create a new instance of it. The purpose of an abstract class is to function as a base for subclasses.

public abstract class MyAbstractClass {
}

An abstract class can have abstract methods

public abstract class MyAbstractClass {
    public abstract void abstractMethod();
}

An abstract method has no implementation. It just has a method signature, like methods in a Java interface.

The purpose of an abstract class is to function as a base to be extended by subclasses to create a full implementation.

Java Interfaces

A Java interface is a bit like a Java class, except that an interface can only contain method signatures and fields. A Java interface is not intended to contain implementations of the methods, only the signature of the method. However, it is possible to provide default implementations of a method in a Java interface, to make the implementation of the interface easier for classes implementating the interface.

public interface MyInterface {
    public String hello = "Hello";
    public void sayHello();
}

To implement an interface:

public class MyInterfaceImplementation implements MyInterface {
    public void sayHello() {
        System.out.println(MyInterface.hello);
    }
}

A Java class can implement multiple interfaces:

public class MyInterfaceImplementation
    implements MyInterface, MyInterface2 {
    
    public void sayHello() {
        System.out.println("Hello");
    }

    public void sayGoodbye() {
        System.out.println("Goodbye");
    }
}

However, there’s a risk of overlapping method signatures (multiple interfaces with conflicting method signatures) and there’s no official solution to that issue by far. It’s up to the developer to decide what to do in that case.

Static methods in interfaces can be useful when you want to define some utility methods.

Interfaces can inherit from other interfaces, just like classes:

public interface MySuperInterface {
    public void sayHello();
}

public interface MySubInterface extends MySuperInterface {
    public void sayGoodbye();
}

Java Interfaces vs Abstract Classes

One key fact: a Java class can only have one superclass, but it can implement multiple interfaces. If you need to separate an interface from its implementation, use an interface. If you need to provide a base class or default implementation of the interface, add an abstract class (or a normal class) that implements this interface.

graph TD A[class] B[interface] C(abstract class) D[subclass] A-->B-.->C D-->C

Java Enums

A Java Enum is a special Java type that is used to define collections of constants.

public enum Level {
    HIGH,
    MEDIUM,
    LOW
}

You can then test against possible values of an Enum:

Level lvl = Level.HIGH;

if (lvl == Level.HIGH) {
    // do something
}

switch (lvl) {
    case HIGH:
        // do something 
        break;
    case MEDIUM:
        // do something 
        break;
    case LOW:
        // do something 
        break;
}

Or iterate through all possible values:

for (Level lvl : Level.values()) {
    System.out.println(lvl);
}

You can print out the literal string of an Enum value:

String text = Level.HIGH.toString();
System.out.println(text);

// or just print directly:
System.out.println(Level.HIGH);

You can obtain the Enum instance by string:

Level lvl = Level.valueOf("HIGH");

You can add fields to a Java Enum and get each constant value these fields:

public enum Level {
    HIGH(3),
    MEDIUM(2),
    LOW(1);  // semicolon is needed when you want to define methods and fields 

    private final int val;
    private Level(int lvlVal) {
        this.val = lvlVal;
    }

    public int getLevelVal() {
        return this.val;
    }
}

Level lvl = Level.HIGH;
System.out.println(lvl.getLevelVal());

Notice the Enum constructors must be private.

It’s possible for a Java Enum to have abstract methods too. If an Enum class has an abstract method, then each instance of the Enum class must implement it:

public enum Level {
    HIGH {
        @Override 
        public String toText() {
            return "HiGh";
        }
    },
    MEDIUM {
        @Override 
        public String toText() {
            return "meDIUm";
        }
    },
    LOW {
        @Override 
        public String toText() {
            return "low";
        }
    };

    public abstract String toText();  // abstract method
}

Similarly, a Java Enum can also implement a Java interface.

Java also contains a special set implementation called EnumSet which can hold enums more efficiently than the standard set implementation. Similarly, there’s EnumMap which handles enums better than the standard map.

Java Annotations

Added from Java 5, Java annotations are used to provide meta data for your Java code. Being meta data, they don’t directly affect the execution of the code. Typically they serve one of the following purposes:

An annotation can take elements or not:

@Entity // or 
@Entity(2)  // or 
@Entity(tableName="vehicles")

They can be placed above classes, interfaces, methods, method parameters, fields and local variables. For example:

@Entity 
public class Vehicle {
    @Persistent 
    protected String vehicleName = null;

    @Getter 
    public String getVehicleName() {
        return this.vehicleName;
    }

    public void setVehicleName(@Optional vehicleName) {
        this.vehicleName = vehicleName;
    }

    public List addVehicleNameToList(List names) {
        @Optional 
        List localNames = names;

        if (localNames == null) {
            localNames = new ArrayList();
        }
        localNames.add(getVehicleName());
        return localNames;
    }
}

These are the built-in annotations given by the Java compiler:

We can also define our own custom Java annocation:

@interface MyAnnotation {
    String value() default ""; // default value
    String name();
    int age();
    String[] newNames() default {};
}

Here are some more annotations that you may come across:

Java Lambda Expressions

Java lambda expressions, introduced in Java 8, are Java’s first step into functional programming. A Java lambda expression is a function which can be created without belonging to any class. It can be passed around as if it’s an object. For example:

(arg1, arg2) -> System.out.println(arg1 + arg2)

which is quite convenient.

Even though lambda expressions are close to anonymous interface implementations, there are a few differences that are worth noting. The major difference is that an anonymous interface implementation can have state (member variables) whereas a lambda expression cannot.

() -> System.out.println("this");           // zero parameter 

arg -> System.out.println("wow");           // one parameter

(arg1, arg2) -> System.out.println("that"); // more than one parameters 

(arg1, arg2) -> {
    System.out.println("well");             // multi-line function body 
    return arg2;                            // returning value from lamdba expression 
}

(arg1, arg2) -> arg1 > arg2;                // return in one line 

Below is how we create a static method reference using Java lambda expressions:

public interface Finder {
    public int find(String s1, String s2);
}

public class MyClass {
    public static int doFind(String s1, String s2) {
        return s1.lastIndexOf(s2);
    }
}

Finder finder = MyClass::doFind;

Notice the last line above is assigning the method reference to the function interface finder directly. Java compiler will automatically consider MyClass::doFind as the implementation of finder.find because there’s only one method to be implemented.

Java Modules

To declare a module:

module com.allen.mymodule {
    requires javafx.graphics;          // dependency
    exports com.allen.mymodule;        // module
    exports com.allen.mymodule.util;   // and/or submodule
}

In order to compile a Java module:

javac -d out --module-source-path src/main/java --module com.jenkov.mymodule

In order to run a Java module:

java --module-path out --module com.jenkov.mymodule/com.jenkov.mymodule.Main

Youu can build a Java module into a JAR file:

jar -c --file=out-jar/com-jenkov-mymodule.jar -C out/com.jenkov.mymodule .

and set the JAR main class:

jar -c --file=out-jar/com-jenkov-mymodule.jar --main-class=com.jenkov.mymodule.Main -C out/com.jenkov.mymodule .

and run the module from the JAR from:

java --module-path out-jar -m com.jenkov.mymodule/com.jenkov.mymodule.Main  # if you have not set the main class 
java -jar out-jar/com-jenkov-javafx.jar   # if you have set the main class 

You can use jlink to package a Java module into a standalone application:

jlink --module-path "out;C:\Program Files\Java\jdk-9.0.4\jmods" --add-modules com.jenkov.mymodule --output out-standalone

and run it with

java --module com.jenkov.mymodule/com.jenkov.mymodule.Main

Java Scoped Assignment and Scoped Access

In Java, scoped assignment :() is very similar to the walrus operator := in Python:

String s1 = null;
String s2 = null;

s1:(s2:("Jane Doe".toUpperCase()).toLowerCase());

and then s1 becomes “jane doe”, while s2 becomes “JANE DOE”.

Scoped access, on the other hand, construct an object in one line using :{}:

// instead of 
MyObject myObj = new MyObject();

myObj.field1 = 123;
myObj.field2 = "John Doe";
myObj.verifyConfiguration();
myObj.init();


// we can use scoped access in one line:
MyObject myObj = new MyObject():{
    .field1 = 123;
    .field2 = "John Doe";
    .verifyConfiguration();
    .init();
};  // you can collapse everything into one line for real