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 Standard Edition (JSE) for desktop and standalone server applications
- Java Enterprise Edition (JEE) for developing and executing Java components that run embedded in a Java server
- Java Micro Edtion (JME) for developing and executing Java applications on mobile phones and embedded devices (Google’s Android is not JME but it uses its own subset of Java combined with a lot of specific 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:
- Variable operations:
- Variable assignment
- Variable reading
- Variable arithmetic
- Object instantiation
- Program flow:
- Loops
for
loopswhile
loops
- Branches
if
statementsswitch
statements
- Method calls
- Loops
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
- Package declaration
- Import statements
- Type declaration
- Fields
- Class initializers
- Constructors
- Methods
- Comments and annotations
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:
- Non-static fields: belongs to an object
- Static fields: same for all objects
- Local variables: declared inside a method
- Parameters: passed to a method wehn the method is called
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:
- Java variable names are case sensitive
- Java variable names must start with a letter, or
$
or_
- Java variable names can contain numbers as long as they don’t appear at the beginning of the name
- Java variable names cannot be equal to reserved keywords in Java, e..g
int
orfor
The convensions are:
- Variable names are written in lower case
- If variable names consist of multiple words, each word after the first word has its first letter capitalized (camel style)
- Even though it’s allowed, you do not normally start a Java variable with a
$
or_
- Static final fields (constants) are named in all upper case, typically using an
_
to separate the words in the name e.g.EXCHANGE_RATE
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:
- Primitive data types
- Object references
The following are the primitive data types:
boolean
: a binary value for true or falsbyte
: 8 bit signed value from -128 to 127short
: 16 bit signed value from -32,768 to 32,767char
: 16 bit Unicode characterint
: 32 bit signed value from - -2,147,483,648 to 2,147,483,647long
: 64 bit signed value from…float
: 32 bit floating point valuedouble
: 64 bit floating point value
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.
Boolean
Byte
Short
Character
Integer
Long
Float
Double
String
: N byte Unicode string of textual data, immutable
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:
+
: addition-
: subtraction*
: multiplication/
: division%
: modulo
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:
s1.equals(s2)
s1.equalsIgnoreCase(s2)
s1.startsWith(s2)
s1.endsWith(s2)
s1.compareTo(s2)
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:
- Fields
- Constructors
- Methods
- Nested classes
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:
private
: only code inside the class can access the fieldpackage
: only code inside the class or other classes in the same package can access the field (default)protected
: same aspackage
except that subclasses of the class can also access the field even if the subclass is not part of the packagepublic
: all classes can access the field
In terms of whether a field is static:
- static fields are located in the class
- non-static fields are located in an instance (of the class)
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:
- static nested class
- non-static nested class
- local class
- anonymous class
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.
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:
- Compiler instructions
- Build-time instructions
- Runtime instructions
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:
@Depreciated
: warning when using the underlying class, method or fields@Override
: warning when the overriden method doesn’t exist any more, or the signature doesn’t match the override@SuppressWarnings
: suppress warnings for the underlying method@Contended
: avoid false sharing (a concurrency performance degradation problem)
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:
@Retention
@Target
@Inherited
Documented
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