Java
Java is a purely object oriented language that provides high-level abstractions inside a platform independent runtime environment.
| Paradigms | Typing | Memory Management | Execution |
|---|---|---|---|
| Object Oriented | Strong Static | Garbage Collected | Compiled into JIT compiled bytecode |
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Table of Contents
- 1 Backgrounds
- 2 Toolchain
- 3 Compilation and Execution
- 4 Syntax
- 5 Structure
- 6 Comments
- 7 Variables
- 8 Constants
- 9 Data Types
- 10 Literals
- 11 Operators
- 12 Control Flow Structures
- 13 Functions
- 14 Object Orientation
- 15 Annotations
- 16 Exceptions
- 17 Collections
- 18 IO
- 19 Threads
1 Backgrounds
1.1 Resources
- Official Java website: https://www.oracle.com/java/
- Official Java documentation: https://docs.oracle.com/en/java/
- Official Java tutorial: https://docs.oracle.com/javase/tutorial/index.html
- Comprehensive Java reference: https://www.w3schools.com/java/java_ref_reference.asp
1.2 Advantages and Disadvantages
| Advantages | Disadvantages |
|---|---|
| High-level abstractions | Overly complicated abstraction layers |
| Platform independent execution | Less runtime performance than other compiled languages |
| Automatic memory management | High memory consumption |
| Large ecosystem and community | Conventions prefer verbose coding practices |
| Wide platform support |
1.3 History
- Java was developed in the 1990s by James Gosling at Sun Microsystems
- It was intended as a platform independent version of C++ for embedded devices
- It was named after coffee bar that was visited frequently by James Gosling
- The first official Java version (JDK 1.0) was released in 1996
- Thereby “JDK” stands for “Java Development Kit” and includes the entire Java toolchain
- It prioritized high-level abstractions over performance and fine-grained control
- Java is published as an open specification since version 1.2 in 1998, called the Java SE (Java Standard Edition)
- The rights on Java passed to Oracle after it acquired Sun Microsystems in 2010
- Java SE shifted to a six months release cycle after the release of Java SE 9 in 2017
- The following LTS versions of Java were published:
- Java SE 8 (2014): Inner classes, Beans, JDBC, RMI, Collection, Swing GUI, JIT compilation, JNDI, Proxy classes, Assertions, Regular expressions, Generics, Enhanced for-loop, autoboxing, Annotations, JShell, Try with resources, Binary literals, Strings in switches, Lambda expressions, Stream API, Date-time API
- Java SE 11 (2018): Module system, Type inference, HTTP client, TLS 1.3, Flight recorder, Performance improvements
- Java SE 17 (2021): , Garbage collection optimization, Multiline strings, Record classes, Pattern matching, Sealed classes, Hidden classes, Foreign functions, Memory API
- Java SE 21 (2023): General language and feature improvements
- Java SE 25 (2025): General language and feature improvements
2 Toolchain
Java comes in a self-contained toolchain called the JDK (Java Development Kit). Thereby the official JDK is licensed proprietary by Oracle, but free versions of the JDK, called OpenJDKs, are available by other companies.
The following JDKs are available:
2.1 Compiler
Java source code files are compiled into executable Java bytecode files by the Java compiler, called the Javac. These bytecode files are platform independent and highly optimized for fast execution.
# compile Java source files into according Java bytecode files
javac SomeFile SomeOtherFile
2.2 Runtime
Java bytecode files are JIT compiled inside the Java virtual machine, called the JVM. This is bundled inside the Java runtime environment, called the JRE, that also includes the precompiled Java standard library.
# execute Java bytecode file
java SomeFile
2.3 Bundler
Java bytecode files can be bundled into Jar (Java Archive) files by the Jar tool. This way entire Java projects consisting of multiple bytecode files can be distributed more easily and even executed directly.
To make Jar files executable they must contain a META-INF/MANIFEST.MF file which specifies metadata including the class containing the main method. This file can be generated automatically or created and included manually.
# bundle Java bytecode files into Jar file
jar cf App.jar Main.class Util.class
# bundle Java bytecode files into executable Jar file
jar cfe App.jar Main Main.class Util.class
# bundle Java bytecode files into executable Jar file with manually created manifest file
jar cfm App.jar MANIFEST.MF Main.class Util.class
# execute executable Jar file
java -jar App.jar
# list contents of Jar file
jar tf App.jar
2.4 Build Systems
Java doesn’t come with a build system or package manager, therefore multiple community driven projects exist:
- Ant: Legacy build system and package manager using advanced XML configurations
- Maven: Traditional build system and package manager using simple XML configurations
- Gradle: Modern build system and package manager using Groovy and Kotlin as DSLs
2.5 Debuggers
Java programs can be debugged by the Java debugger, a CLI tool called the JDB.
# start debugging session for bytecode file
jdb Main
The following commands inside the debugging sessions exist:
| Command | Effect |
|---|---|
stop at Main:12 | Set breakpoint at specified line |
stop in Main:add | Set breakpoint at specified method |
run | Run the Java program |
cont | Continue execution until the next breakpoint |
step | Step into method |
next | Step over method |
finish | Step out of current method |
locals | Show all variables in scope |
print x | Show value of specified variable |
eval x + y | Evaluate expressions |
2.6 JShell
Java comes with a REPL that interprets Java statements on the fly inside a REPL session, called the JShell.
# start Jshell REPL session in current directory
jshell
3 Compilation and Execution
graph TD
source_files[Source files] --> |passed to| javac[Java Compiler];
javac --> |compiles| bytecode_files[Bytecode Files];
bytecode_files --> |passed to| jre[Java Runtime Environment];
jre --> |passes bytecode to| jvm[Java Virtual Machine];
jre --> |passes standard library to| jvm;
- JavaC: Produces Java bytecode files (
.class) from Java source files (.java) - JRE: Passes compiled Java bytecode files and the precompiled standard library into the JVM
- JVM: JIT compiles the received bytecode
4 Syntax
4.1 Whitespace
Whitespace is used to separate tokens (identifiers, literals, keywords, and operators) from each other and as characters inside string literals. Outside of these whitespace has no meaning and is ignored by the Java compiler.
int x=10; // valid
int x = 10; // valid
intx = 10; // invalid
4.2 Statements
Statements are predefined control structures, definitions and any combination of expressions that end with a semicolon ;. Thereby compound statements can be formed by enclosing any number of statements inside curly braces {}, which are then treated as a single statement.
// line statement
int x;
// compound statement
{
int y = 5;
int z = x + y;
}
// empty statement
;
4.3 Identifiers
The following rules apply for identifiers:
- They mustn’t start with digit
- They mustn’t only contain underscores
- They may contain letters, digits, and underscores
- They cannot be predefined keywords
- They are case-sensitive
// valid identifiers
int age;
int _count;
int value123;
int myVariableName;
// invalid identifiers
int 2fast;
int my-var;
int class;
int my var;
4.4 Scope
Every compound statement forms its own scope, which can be nested indefinitely. Additionally, the program itself forms the global scope in which every other scope lives.
An identifier is visible at a given point in the program if:
- It was declared earlier in the current scope, or
- It was declared in an outer scope
Identifiers can shadow identifiers from outer scopes by redefining them and are active until the end of their scope.
int x = 10; // global scope
void foo()
{
int y = 20; // block scope
{
int z = 30; // nested block scope
y = 10; // variables from outer scopes are accessible
int x = 5; // shadows global x
}
}
4.5 Keywords
The following identifiers are reserved as keywords with special meaning:
byteclassdoublefloatintlong
5 Structure
5.1 Files
Java source files must contain a class, interface, enum or annotation definition and have the file suffix .java. Thereby they can contain optional additional definitions, but they must include exactly one public definition.
Java source and bytecode files must be named like their according public definitions. Java bytecode files have the file suffix .class.
5.2 Projects
Java projects follow the project structures of their according build tool.
5.3 Entry Point
Java requires a class with a static and public method called main as entry point for executable programs. This main method takes the program’s command-line arguments as parameter/argument.
public class Main {
public static void main(String[] args) {
System.out.printf("First command-line argument: %s", args[0]);
}
}
Best practices:
- The class containing the main method should be named
Mainor after the program itself
5.4 Packages
Packages act as namespaces for Java files to group them logically. They correspond to the directory structure, which means that every package should be named after its current directory and that they can be nested inside each other.
Java files from other packages can be imported to make them usable in the current file. Thereby Java files are always accessible inside their own package without needing to import them.
// declare package
package dirname;
// declare package nested inside other packages
package path.to.dir;
// import file from package
import path.to.File;
// import all files from package
import path.to.*;
// use file from package without importing it
new path.to.File();
Best practices:
- Package declarations should occur at the beginning of files
- Package imports should occur after package declarations
- Files from other packages should always be imported when used
- Packages should be nested inside reversed domain names to make them unique (e.g.
com.google.calculatorfor a calculator app by google)
5.5 Standard Library
The Java standard library is a set of precompiled Java packages that are included inside the JVM and are imported implicitly in every Java file.
The following packages exist in the standard library:
java.lang: Contains fundamental data structures and utilities
6 Comments
Comments are treated as whitespace by the Java compiler and are therefore mostly ignored.
6.1 Single-Line Comments
// this is a single-line comment
int x = 0; // this is another single-line comment
6.2 Multi-Line Comments
/* This
is a
multi-line
comment */
6.3 Documentation Comments
Documentation comments are used by some tools and editors to generate documentation for according code, but are still regular comments for the Java compiler.
/**
* Some random class.
*
* @param <T> usage of generic
*
* @author John Doe
* @version 1.0.0
* @since 2020-12-31
*/
public class FooBar<T> {
/**
* Some random field.
*
* @see FooBar#bar
* @since 1.0.0
*/
public int foo;
/**
* Does some random stuff.
*
* <pre>Example:</p>
* <p>{@code
* int result = fb.bar(2, 3);
* result == 5;
* }</pre>
*
* @param x first operand
* @param y second operand
* @return sum of the parameters
* @throws IllegalArgumentException if x or y are negative
*
* @see FooBar#foo
* @since 1.0.0
*/
public int bar(int x, int y) {
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Ooops");
}
return x + y;
}
}
/**
* Some random enum.
*
* @author John Doe
* @version 1.0.0
* @since 2020-12-31
*/
public enum BarFoo {
/**
* Some enum element.
*/
FOO,
/**
* Some other enum element.
*/
BAR
}
7 Variables
Variables can only exist inside of classes.
// declare variables
int x;
int x, y ,z;
// define variables
x = 12;
y = z = 12;
// initialize variables
int a = 12;
int b = 10, int c = 20;
// shadow variable
int foo = 1;
{
int foo = 2; // shadow variable from outer scope by redeclaring it
foo == 2;
}
foo == 1; // shadowing is revoked when its scope is left
Best practices:
- Variables should be named in camel case
8 Constants
Constants can only exist inside of classes.
// initialize constant
final float EULER = 2.71;
// declare and define constant
final float PI;
PI = 3.14; // only possible once
Best practices:
- Constants should be named in constant case
9 Data Types
Data types in Java have default values that get assigned automatically to undefined variables.
9.1 Primitive Data Types
| Keyword | Representation | Byte Size | Values | Default |
|---|---|---|---|---|
byte | Signed Integer | 1 | $-2^{7}$ to $2^{7}-1$ | 0 |
short | Signed Integer | 2 | $-2^{15}$ to $2^{15}-1$ | 0 |
int | Signed Integer | 4 | $-2^{31}$ to $2^{31}-1$ | 0 |
long | Signed Integer | 8 | $-2^{63}$ to $2^{63}-1$ | 0 |
float | Floating Point Number | 4 | $\approx$ 7 digits | 0.0 |
double | Floating Point Number | 8 | $\approx$ 15 digits | 0.0 |
boolean | Boolean Value | 1 | true, false | false |
char | Unicode Character | 2 | '\u0000' to '\uffff' | '\u0000' |
9.1.1 Type Conversion
Primitive data types are converted automatically in according contexts when the new data type is of the same or a larger size as the original data type.
9.1.2 Type Casting
// casting floating point numbers into integers
(int)12.3 == 12;
// casting integers into floating point numbers
(double)12 == 12.0
// casting number data types into smaller number data types
(byte)46 == 46
(byte)1 == 257 % 127
// casting characters into unicode values and vice versa
int ascii = 'A';
char letter = (char)65;
9.1.3 Data Type Wrappers
Data type wrappers are objects representing primitive data types that are wrapping these. Their contained primitive data type is boxed und unboxed automatically in according contexts and therefore they can be used in place of any primitive data type.
Data type wrappers have the benefit that they can be used for generic classes and methods, because these require objects to implement them. Also they add some utility methods to the data types.
| Wrapper | Wrapped Primitive |
|---|---|
Byte | byte |
Short | short |
Integer | int |
Long | long |
Float | float |
Double | double |
Boolean | boolean |
Character | char |
Best practices:
- Data type wrappers should only be used when they’re needed, because they add additional overhead
9.2 Compound Data Types
9.2.1 Arrays
The default value of arrays are null.
// declare arrays
char[] letters;
// initialize arrays with specified size and default values
double[] reals = new double[5];
// initialize arrays with specified size and values
int[] nums = {1, 3, 5, 8, 11};
// access array elements
int x = nums[0]; // get array element
nums[1] = 4; // assign array element
// get array length
nums.length == 5;
// using multi-dimensional arrays
int[][] matrix = new int[4][4]; // create
matrix = { // initialize
{1, 2, 3, 4},
{2, 3, 4, 5},
{3, 4, 5, 6}
};
int[] row = matrix[0]; // get row
matrix[0] = new int[]{4, 2, 3, 1}; // assign row
int cell = matrix[0][0]; // get cell
matrix[0][5] = 3; // assign cell
// using jagged arrays
int[][] peaks = new int[3][]; // create
peaks = { // initialize
{1, 2},
{2, 3, 4, 5},
{3, 4, 5}
};
int[] line = peaks[0]; // get row
peaks[0] = new int[]{4, 2}; // assign row
int point = peaks[0][0]; // get cell
peaks[0][4] = 3; // assign cell
9.2.2 Strings
Strings are stored as immutable objects inside an internal global string table, that adds new entries when strings are created and removes entries when strings are garbage collected. Therefore string manipulations don’t manipulate the underlying string, but create new strings.
String data types themselves are only wrappers for pointers referencing entries inside the string table. Their concatenation and slicing is performing according pointer combination and slicing under the hood.
The default value of strings are "".
// create strings
String firstName = "John"; // literal syntax
String lastName = new String("Doe"); // object syntax
// create strings from character arrays
char[] letters = {'J', 'o', 'h', 'n'};
String name = new String(letters);
// format strings
String pitch = String.format(
"My name is %s, I'm %d years old and %.02f meters high", // use format specifiers
"John", 21, 1.8 // use values for format specifiers
);
pitch == "My name is John, I'm 21 years old and 1.80 meters high";
// create multiline string
String multi = """
This
is a
multiline
string
""";
multi == "This\nis a\nmultiline\nstring";
9.2.2.1 String Processing
// comparing strings
String name = "John";
name == "John";
name.euqlas("John");
// concatenate strings
String foobar = "Foo" + "Bar"; // operator syntax
foobar.equals("FooBar");
foobar = "Bar".concat("Foo") // method syntax
foobar.equals("FooBar");
// get string size
"John".length() == 4;
// get string characters
"John".charAt(0) == 'J';
"John".charAt(3) == 'n';
// convert case
"Hello, World!".toLowerCase() == "hello, world!";
"Hello, World!".toUpperCase() == "HELLO, WORLD!";
// check for substrings
"Hello, World!".contains("Hello") == true;
"Hello, World!".contains("hello") == false;
// get substrings
"Hello, World!".substring(7) == "World!"; // until end
"Hello, World!".substring(7, 12) == "World"; // until exclusive index
// parse integers
Integer.parseInt("12") == 12;
// create character arrays from strings
String name = "John";
char[] letters = name.toCharArray();
9.2.2.2 Buffered Strings
Buffered strings don’t use the global internal string table, but are wrapping a dynamic character array, that keeps track of its length and capacity and that gets reallocated when it needs to grow beyond its capacity. They have the benefit that they’re more flexible and that they can be more performant in cases where many string manipulations are performed.
// create buffered strings
StringBuffer name = new StringBuffer("John");
// set buffered strings lengths
name.setLength(5);
// set buffered strings capacities
name.ensureCapacity(10);
// concatenate strings to buffered strings
name.append("Doe");
name == "John Doe";
// insert substrings into buffered strings
name.insert(4, ",ny");
name.equals("Johnny Doe");
Best practices:
- Buffered strings should be used when strings are expected to be manipulated often
9.2.3 Enums
Enums are classes that wrap enumerations.
The default value of enums are null.
// use ordinal enums
enum Status {
RUNNING, // 0
SUCCESS, // 1
FAILURE, // 2
PENDING // 3
}
Status request = Status.RUNNING; // assign enum element
request.ordinal() == 0; // get value of assigned enum element
// use enums with custom values
enum Status {
RUNNING(300),
SUCCESS(200),
FAILURE(500),
PENDING(400);
// field to hold enum value
public final int code;
// initialize enum with custom value with private constructor
private Status(int code) {
this.code = code;
}
}
Status request = Status.RUNNING; // assign enum element
request.code == 300; // get value of assigned enum element
// get all possible values of enums
Status[] stats = Status.values();
stats[0] == Status.RUNNING;
stats[3] == Status.PENDING;
// use enums with members
enum Status {
RUNNING(300),
SUCCESS(200),
FAILURE(500),
PENDING(400);
public final int code;
private Status(int code) {
this.code = code;
}
// initialize enum field
public String message = "hi";
// define enum method
public String greet() {
return "hello";
}
}
Status request = Status.RUNNING;
request.message == "hi";
request.greet() == "hello";
// use switch statements on enums
switch (request) {
case RUNNING:
System.out.println("running...");
case SUCCESS:
System.out.println("success...");
case FAILURE:
System.out.println("failure...");
case PENDING:
System.out.println("pending...");
}
9.2.4 Optionals
import java.util.Optional;
// create optional values that might be `null`
Optional<String> maybeEmpty = Optional.ofNullable("foo");
Optional<String> maybeNotEmpty = Optional.ofNullable(null);
// create optional value that isn't `null`
Optional<String> notEmpty = Optional.of("foo");
// check wether optional contains value
maybeEmpty.isPresent() == true;
// check wether optional contains `null`
maybeNotEmpty.isEmpty() == true;
// get contained value
maybeEmpty.get() == "foo";
// return values of optionals or alternative values if empty
maybeEmpty.orElse("bar") == "foo";
maybeNotEmpty.orElse("bar") == "bar";
10 Literals
// use integer literals with different bases
0b10011011; // binary
01772; // octal
0xF12A2; // hexadecimal
// use separators in number literals
1_000_000 == 1000000;
12_343.623_22 == 12343.62322;
// specify data types of floating point literals
23.15; // `double`
23.15f; // `float`
23.15F; // `float`
// specify data types of integer literals
12; // `int`
12l; // `long`
12L; // `long`
11 Operators
11.1 Precedence
| Category | Operators | Precedence Level |
|---|---|---|
| Multiplicative | *, /, % | 2 |
| Additive | +, - | 1 |
The precedence of expressions can be maximized by surrounding them in parenthesis ().
11.2 Arithmetic Operators
| Operation | Operator | Syntax |
|---|---|---|
| Addition | + | x + y |
| Unary Plus | + | +x |
| Subtraction | - | x - y |
| Negation | - | -x |
| Multiplication | * | x * y |
| Division | / | x / y |
| Integer Division | / | x / y |
| Modulo | % | x % y |
| Pre-Increment | ++ | ++x |
| Post-Increment | ++ | x++ |
| Pre-Decrement | -- | --x |
| Post-Decrement | -- | x-- |
11.3 Comparison Operators
| Operation | Operator | Syntax |
|---|---|---|
| Equality | == | x == y |
| Inequality | != | x != y |
| Less Than | < | x < y |
| Less Equal Than | <= | x <= y |
| Greater Than | > | x > y |
| Greater Equal Than | >= | x >= y |
11.4 Logical Operators
Logical operators in Java are short circuited.
| Operation | Operator | Syntax |
|---|---|---|
| AND | && | x && y |
| OR | \|\| | x \|\| y |
| NOT | ! | !x |
11.5 Assignment Operators
The left operand of an assignment must be a variable or assignable expression.
| Operation | Operator | Syntax |
|---|---|---|
| Assignment | = | x = y |
| Addition Assignment | += | x += y |
| Subtraction Assignment | -= | x -= y |
| Multiplication Assignment | *= | x *= y |
| Division Assignment | /= | x /= y |
| Modulo Assignment | %= | x %= y |
11.6 Ternary Operator
boolean toCheck = true;
String result = toCheck ? System.out.println("Is true") : System.out.println("Is false");
Best practices:
- Ternary operations should only be used for simple and short if-else checks
12 Control Flow Structures
12.1 Conditions
int x = 9;
if (x % 3 == 0) {
System.out.println("x is divisible by 3");
}
else if (x % 5 == 0) {
System.out.println("x is divisible by 5");
}
else if (x % 2 == 0) {
System.out.println("x is divisible by 2");
}
else {
System.out.println("x is divisible by 1");
}
12.2 Switches
// define switches without fallthroughs
int x = 3;
switch (x) {
case 1:
System.out.println("x is 1");
break;
case 2:
System.out.println("x is 2");
break;
case 3:
System.out.println("x is 3");
break;
// optional default case
default:
System.out.println("x isn't 1, 2 or 3");
}
// define switches with fallthroughs
int countdown = 3;
switch (countdown) {
case 3:
System.out.println("Tick");
case 2:
System.out.println("Tick");
case 1:
System.out.println("Tick");
default:
System.out.println("RING!!!");
}
12.3 Loops
// define while-loops
int i = 0;
while (i < 10) {
System.out.println("Current index: " + i);
i++;
}
// define do-while-loops
int j = 0;
do {
System.out.println("Current index: " + j);
j++;
} while (j < 10);
// define for-loops
for (int i = 0; i < 10; i++) {
System.out.println("Current index: " + i);
}
// define enhanced-for-loops that loop through arrays and collections
int[] nums = new int[5];
for (int n : nums) {
System.out.println("Current number: " + n);
}
// break loops
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
break; // break loop immediately
}
}
// skip loop iterations
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue; // skip iteration immediately
}
}
13 Functions
Functions can only exist as methods and functional interfaces of classes.
// define functions without parameters and return values
void greet() {
System.out.println("Hi!");
}
greet(); // execute function without parameters and return values
// define functions with parameters and return values
int add(int x, int y) {
return x + y; // return value of expression
}
int x = add(2, 3); // execute function with parameters nad return values
Best practices:
- Methods should be named in camel case
13.1 Function Overloading
// overload already defined functions
int add(int x, int y) {
return x + y;
}
int add(int x, int y, int z) {
return x + y + z;
}
double add(double x, double y) {
return x + y;
}
// use according function overloads implicitly
add(5, 10) == 15;
add(5, 10, 8) == 23;
add(5.0, 10.0) == 20.0;
13.2 Generic Functions
// define generics that can be implemented by any compatible class
<T, U> T add(T x, U y) {
System.out.println(y);
return x;
}
// implement generics by inserting any compatible classes
Integer x = add<Integer, Double>(12, 4.5);
14 Object Orientation
Classes are custom compound data types with a default value of null for their objects.
// define classes
class FooBar {
// define fields of classes
String foo;
// define constructor methods of classes
FooBar(String foo) {
this.foo = foo; // access members of classes inside class definitions themselved
}
// overload constructor methods of classes
FooBar(String foo, String bar) {
this(foo); // call constructora of classes themselves
this.foo += bar;
}
// define methods of classes
String getFoo() {
return this.foo;
}
}
// instantiate objects of classes
FooBar foobar = new FooBar("Foo");
FooBar barfoo = new FooBar("Foo", "Bar");
// access members of objects
foobar.foo == "Foo";
barfoo.getFoo() == "FooBar";
// check if objects are instances of classes
foobar instanceof FooBar == true;
Best practices:
- Classes should be named in pascel case
14.1 Inheritance
Objects of derivations are also considered to be instances of their base classes, which enables polymorphism between inherited classes. Classes can only be derived from one class, but derived classes can also be derived from. Thereby derived objects that are used as instances of base classes can only use members defined for these base classes.
public class Foo {
protected String foo;
public Foo(String foo) {
this.foo = foo;
}
public String getFoo() {
return this.foo;
}
}
// derive classes
public class Bar extends Foo {
protected String bar;
public Bar(String foo, String bar) {
super(foo); // call constructors of base classes
this.bar = bar;
}
// override inherited methods (parameters and return types must match or be subtypes)
@Override // annotate as override for compile-time checking
public String getFoo() {
return new String(this.foo);
}
// mark methods as final (not overridable)
public final String getBar() {
return this.bar;
}
}
// mark classes as final (not derivable)
public final class FooBar extends Bar {
public void greet() {
System.out.println("Hello!");
}
}
// access members of base class from derived class instances
BarFoo barfoo = new BarFoo("Foo", "Bar");
barfoo.getFoo() == "Foo";
barfoo.getBar() == "Bar";
// upcast instances to base classes
Foo foo = new BarFoo("Foo", "Bar");
foo.getFoo() == "Foo"; // can only access members of "Foo"
// downcast instances to derived classes
FooBar foobar = (FooBar)foo;
// check if objects are instances of base classes
barfoo instanceof Foo == true;
// derive classes anonymously for one-time usage (must have default constructors)
FooBar foofoo = new FooBar() {
private String foofoo = "Foo Foo";
public String getFooFoo() {
return this.foofoo;
}
};
foofoo.getFoo() == "Foo";
foofoo.getBar() == "Bar";
foofoo.getFooFoo() == "Foo Foo";
14.2 Access Modifiers
The following access modifiers do exist for class members and classes:
- Default: Member and class can only be accessed inside its current package (default)
public: Member and class can be freely accessedprivate: Member can only be accessed inside its class and class can only be accessed inside its fileprotected: Member can only be accessed inside its class or classes derived from it
// define default classes
public class FooBar {
// define protected members
protected String foo = "Foo";
// define default members
String bar = "Bar";
// define public members
public String getFoo() { // getter method
return this.foo;
}
public void setFoo(String foo) { // setter method
this.foo = foo;
}
}
FooBar foobar = new FooBar();
// use getters and setters
foobar.getFoo() == "foo";
foobar.setFoo("FOO");
Best practices:
- Members and classes should be as less privileged as possible
- Fields should be private and only accessible from outside via getter and setter methods
14.3 Static Classes
class John {
// define static fields
static String name = "John";
// define static methods
static String greet() {
return "Hi, I'm " + name; // access static fields inside static methods
}
// define static blocks that only execute at startup on loading of classes
static {
System.out.println("Class loaded!");
}
}
John john = new John();
// access static fields
John.name == "John"; // through classes
john.name == "John"; // through objects
// call static methods
John.greet() == "Hi, I'm John"; // through classes
john.greet() == "Hi, I'm John"; // through objects
Best practices:
- Static members should be accessed through their classes
14.4 Inner Classes
public class Foo {
public String name = "Foo";
// define inner classes
public class Bar {
public String name = "Bar";
}
}
// access inner classes
Foo foo = new Foo();
Foo.Bar bar = foo.new Bar();
bar.name == "bar";
14.5 Abstract Classes
// define class as abstract (only inheritable)
public abstract class Foo {
protected String foo = "Foo";
// define method as abstract (must be overriden)
public abstract String getFoo();
}
// derive from abstract clases
public class FooBar extends Foo {
// override abstract classes
@Override
public String getFoo {
return this.foo;
}
}
FooBar foobar = new FooBar();
foobar.getFoo() == "Foo";
14.6 Generic Classes
// define generics that can be implemented by any compatible class
class FooBar<T, U> {
T foo;
U bar;
T getFoo() {
return foo;
}
U getBar() {
return bar;
}
}
// implement generics by inserting any compatible classes
FooBar<String, Integer> foobar = new FooBar<String, Integer>();
foobar.foo = "Foo";
foobar.getFoo() == "Foo";
foobar.bar = 12;
foobar.getBar() == 12;
// infer implementations of generics
FooBar<String, Integer> barfoo = new FooBar<>();
14.7 Interfaces
Implementations of interfaces are also considered to be instances of that interface, which enables polymorphism between implemented interfaces. Thereby implementations of interfaces that are used instance of specific interfaces can only use members defined by that interface.
// define interfaces
public interface Person {
// initialize static properties that are inherited
double BASE_DISTANCE = 10.0; // public, static and final
// declare methods that must be implemented
double walk(double distance); // public
}
public interface Greeter {
// declare methods with default implementations that don't have to be implemented
default String greet() {
return "Hi";
}
}
// derive interfaces
public interface Talker extends Greeter {
String pass();
}
// implement interfaces
public class Student implements Person, Talker {
@Override
public double walk(double distance) {
return BASE_DISTANCE + distance;
}
// "greet" method is implemented per default
@Override
public String pass() {
return "Bye!";
}
}
// access members of interfaces from implementations
Person john = new Student(); // can only use members declared by "Person"
john.walk(5.0) == 15.0;
Talker jane = new Student(); // can only use members declared by "Talker"
jane.greet() == "Hi!";
jane.pass() == "Bye!";
// abstract classes don't have to implement interface methods, only their derivations
public abstract class Pupil implements Human {}
// implement interfaces anonymously for one-time usage
Person jonny = new Person() {
public double walk(double distance) {
return distance - 1.0;
}
};
jonny.walk(10.0) == 9.0;
// check if objects implement interfaces
Student jack = new Student();
jack instanceof Person == true;
Best practices:
- Interfaces should be named in camel case
14.7.1 Generic Interfaces
// define generics that can be implemented by any compatible class
public interface FooBar<T, U> {
T foo(T some);
U bar(U thing);
}
// implement generics by inserting any compatible classes
public class BarFoo implements FooBar<String, Integer> {
public String foo(String some) {
return some;
}
public Integer bar(Integer thing) {
return thing;
}
}
14.7.2 Functional Interfaces
Functional interfaces allow the usage of lambda expressions as values for them and therefore the utilization of functional programming patterns.
// define functional interfaces
@FunctionalInterface // enable compile-time checks
public interface Greeter {
String greet(String name); // declare single method for interface
}
// implement functional interfaces by providing lambda expressions
Greeter hi = (String name) -> {
return "Hi " + name + "!";
};
hi.greet("John") == "Hi John!";
// implement functional interfaces by providing inline lambda expressions
Greeter hey = (String name) -> "Hey " + name "!";
greeter.hey("Jane") == "Hey Jane!";
// implement functional interfaces by providing shortened inline lambda expressions
Greeter hello = name -> "Hello " + name "!";
greeter.hello("Jonny") == "Hello Jonny!";
Best practices:
- Lambda expressions should be written in the shortest possible way to make the most out of their enhanced readability capabilities
14.8 Object Class
Every class in Java is a derivation of the Object class. Therefore every class is guaranteed to have its members and polymorphism is possible between every class if used as the Object type.
public class Foo {
public String foo = "Foo";
// override inherited string representation method
@Override
public String toString() {
return "Foo{ foo: " + this.foo + " }";
}
// override inherited comparison method
@Override
public boolean equals(Foo other) {
return this.foo == other.foo;
}
// override inherited hash generation method
@Override
public int hashCode() {
return this.foo.length;
}
}
Foo foo = new Foo();
Foo bar = new Foo();
String.format("%s", foo) == "Foo{foo: Foo}"; // use custom string representations
foo.equals(bar= == true; // compare objects based on custom criterias
foo.hashCode() == 3; // compute custom hash codes
15 Annotations
Annotations are metadata attached to program elements (classes, methods, fields, etc.) that can be processed by the compiler, tools, or at runtime via reflection. Thereby the following distinction exists:
- Compile-time annotations add additional metadata and/or compile-time checks and are optional
- Runtime annotations add additional features and are therefore mandatory to access these features
@Deprecated // Mark interfaces as deprecated (compile-time)
@FunctionalInterface // Mark interfaces as functional interface (compile-time)
public interface Greeter {
@Deprecated // Mark method declarations as deprecated (compile-time)
public String greet();
}
@Deprecated // Mark classes as deprecated (compile-time)
public class Person {
@Deprecated // Mark fields as deprecated (compile-time)
protected String name;
@Deprecated // Mark methods as deprecated (compile-time)
public String getName() {
return "";
}
}
public class John extends Person {
@Override // Mark methods as overriding inherited methods (compile-time)
public String getName() {
return this.name;
}
}
Best practices:
- Compile-time annotations should be used when applicable
16 Exceptions
Java uses exceptions to handle runtime erros. Thereby the following distinction between exceptions exist:
- Checked exceptions have to be handled or propagated when they can occur
- Unchecked exceptions don’t have to be handled or propagated when they can occur
| Exception | Meaning | Checked |
|---|---|---|
ClassNotFoundException | Accessed invalid class identifier | Yes |
InterruptedException | Interrupted thread execution | Yes |
ArithmeticException | Used invalid arithmetic expression | No |
ArrayIndexOutOfBoundsException | Accessed invalid array index | No |
NullPointerException | Accessed null pointer | No |
16.1 Catching Exceptions
// only executes until exception is thrown
try {
int x = 5 / 0;
}
// only executes when one of the specified exceptions is thrown
catch (ArithmeticException | ArrayIndexOutOfBoundsException e) {
System.out.println(e.getMessage()); // print message of exception
e.printStackTrace(); // print stack trace of exception
}
// only executes when yet uncatched exception is thrown
catch (Exception e) {
System.out.println(e.getMessage()); // print message of exception
e.printStackTrace(); // print stack trace of exception
}
// always executes
finally {
System.out.println("Everything handled!");
}
16.2 Throwing Exceptions
// propagate exceptions that can be thrown by functions
void doSomething() throws ClassNotFoundException {
// manually throw exceptions with optional messages
throw new ClassNotFoundException("Can't do this");
}
16.3 Creating Exceptions
// create custom exceptions
class MyException extends Exception {
// optionally define super constructor to handle exception messages
public MyException(String message) {
super(message);
}
}
17 Collections
All collections in Java implement the java.util.Collection interface.
17.1 Lists
All list types in Java implement the java.util.List interface, which itself extends the java.util.Collection interface. This interface defines common methods for lists, so that they share an API, but can be implemented differently.
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
// create lists
List<Integer> array = new ArrayList<>(); // list implemented as dynamic array
List<Integer> list = new LinkedList<>(); // list implemented as linked list
// create prefilled lists
List<Integer> fromArgs = Arrays.asList(5, 8, 5); // create from arguments
int[] arr = {5, 8, 5};
List<Integer> fromArray = Arrays.asList(arr); // create from array
List<Integer> fromList = new LinkedList<>(array); // create from other list
List<Integer> fromSet = new LinkedList<>(new HashSet<Integer>()); // create from set
// add elements to lists
list.add(5);
list.add(-8);
// get elements from lists by their index
list.get(0) == 5;
list.get(1) == -8;
list.get(2); // throws IndexOutOfBoundsException
// get indices of first occurring list elements
list.indexOf(-8) == 1;
list.indexOf(10) == -1; // element not found
// get indices of last occurring list elements
list.lastIndexOf(-8) == 1;
list.lastIndexOf(10) == -1; // element not found
// get sizes of lists
list.size() == 2;
// remove elements from lists
list.remove(1); // remove by index
list.remove(Integer.valueOf(5)); // remove by value (first occurence)
// get list iterators
Iterator<Integer> iterator = list.iterator();
List<Integer> list = Arrays.asList(5, 8, 5);
// call functions on all list elements
list.forEach((e) -> System.out.println(e)); // implement `Consumer` functional interface
list.forEach(System.out::println); // pass predefined function
// sort lists
Collections.sort(list); // sort in ascending natural order
list.sort((first, second) -> first - second); // implement `Comparator` functional interface
// create natural orders for classes as list elements
public class Foo implements Comparable<Foo> {
public String foo;
// implement `compareTo` method that creates natural order in sorting
public int compareTo(Foo that) {
return this.foo.length() - that.foo.length();
}
}
17.2 Sets
All sets types in Java implement the java.util.Set interface, which itself extends the java.util.Collection interface. This interface defines common methods for sets, so that they share an API, but can be implemented differently.
import java.util.Comparable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
// create sets
Set<Integer> hash = new HashSet<>(); // set implemented as unordered hash set
Set<Integer> linked = new LinkedHashSet<>(); // set implemented as ordered linked hash set
Set<Integer> tree = new TreeSet<>(); // set implemented as sorted tree set
// create prefilled sets
int[] arr = {5, 8, 5};
Set<Integer> fromArray = Arrays.asSet(arr); // create from array
Set<Integer> fromSet = new HashSet<>(linked); // create from other set
Set<Integer> fromList = new HashSet<>(new ArrayList<Integer>()); // create from list
// add elements to sets
hash.add(5);
hash.add(-8);
// get sizes of sets
hash.size() == 2;
// get set iterators
Iterator<Integer> iterator = hash.iterator();
// call functions on all set elements
hash.forEach((e) -> System.out.println(e)); // implement `Consumer` functional interface
hash.forEach(System.out::println); // pass predefined function
// create natural orders for classes as set elements
public class Foo implements Comparable<Foo> {
public String foo;
// implement `compareTo` method that creates natural order in sorting
public int compareTo(Foo that) {
return this.foo.length() - that.foo.length();
}
}
17.3 Maps
All maps types in Java implement the java.util.Map interface. This interface defines common methods for maps, so that they share an API, but can be implemented differently.
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Set;
// create maps
Map<String, Integer> map = new HashMap<>(); // implemented as unordered thread unsafe hash map
Map<String, Integer> table = new Hashtable<>(); // implemented as ordered thread safe hash table
// add key-value pairs to maps
map.put("first", 5);
map.put("second", -8);
// update key-value pairs in maps
map.put("first", 3);
// get values from maps by their key
map.get("first") == 3;
map.get("second") == -8;
map.get("third") == null;
// remove key-value pairs from maps
map.remove("second") == -8; // get removed value
map.remove("third") == null; // get null when key didn't exist
map.clear(); // remove all values
// get all keys from maps
Set<String> keys = map.keySet();
17.4 Streams
Streams are wrappers for collection elements that can perform actions on copies of them that don’t mutate the original collection. Thereby streams can only be transformed once and are invalidated after they were transformed.
import java.util.ArrayList;
import java.util.HashSet;
import java.util.stream.Stream;
// create streams from collections
Stream<Integer> stream = Arrays.asList(5, 8, 5, 12, 7).stream();
// perform operations on streams
Stream<Integer> result = stream
.filter((e) -> e % 2 == 0) // filter stream elements with predicate
.map((e) -> e + 10) // apply function on each stream element
.sorted(); // sort stream elements in ascending natural order
// transform streams
Stream<Integer> toReduce= Arrays.fromArgs(5, 8, 5, 12, 7).stream();
int prod = toReduce.reduce(0, (carry, elem) -> carry * elem); // reduce into single value
Stream<Integer> toList = Arrays.fromArgs(5, 8, 5, 12, 7).stream();
ArrayList<Integer> toList = result.toList(); // convert into list
// perform operations on streams in parallel threads
int fastSum = Arrays.fromArgs(5, 8, 5, 12, 7).parallelStream()
.filter((e) -> e % 2 == 0)
.map((e) -> e + 10)
.sorted();
18 IO
18.1 Terminal
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Scanner;
// print to stdout
System.out.print("Hello"); // print string
System.out.print(4); // print string representation
// print to stdout with appended line breaks
System.out.println("Hello"); // print string
System.out.println(4); // print string representation
// read tokens from stdin
Scanner tokenScanner = new Scanner(System.in); // create scanner of stdin
String token = tokenScanner.next(); // read next token from stdin
tokenScanner.close(); // close scanner of stdin
// read lines from stdin
Scanner lineScanner = new Scanner(System.in); // create scanner of stdin
String line = lineScanner.nextLine(); // read next line from stdin
lineScanner.close(); // close scanner of stdin
// read lines buffered from stdin
InputStreamReader in = new InputStreamReader(System.in); // create reader of stdin
BufferedReader reader = new BufferedReader(in); // create buffered reader of stdin reader
String line = reader.readLine(); // read next line from buffered reader
reader.close(); // close buffered reader
// automatically close resources
try (Scanner scanner = new Scanner(System.in)) {
System.out.println(scanner.next());
}
Best practices:
- Stdin should be read with buffered readers when performance is critical
18.2 Files
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Scanner;
// create file object
File file = new File("path/to/some/file.txt");
// read tokens from stdin
Scanner tokenScanner = new Scanner(file); // create scanner of file
String token = tokenScanner.next(); // read next token from file
tokenScanner.close(); // close scanner of file
// read lines from file
Scanner lineScanner = new Scanner(file); // create scanner of file
String line = lineScanner.nextLine(); // read next line from file
lineScanner.close(); // close scanner of file
// read lines buffered from file
InputStreamReader in = new InputStreamReader(file); // create reader of file
BufferedReader reader = new BufferedReader(in); // create buffered reader of file reader
String line = reader.readLine(); // read next line from buffered reader
reader.close(); // close buffered reader
// read files as streams
InputStream input = FileInputStream(file);
Scanner scanner = new Scanner(input);
String token = scanner.next();
scanner.close();
input.close();
// automatically close resources
try (Scanner scanner = new Scanner(file)) {
System.out.println(scanner.next());
}
Best practices:
- Files should be read with buffered readers when many readings are performed
- Files should be read as streams when they’re large to avoid memory overflows
19 Threads
Threads can be in one of the following states:
- NEW: Thread has been newly created
- RUNNABLE: Thread is executing
- BLOCKED: Thread waits for monitor lock
- WAITING: Thread execution has been halted
- TIMED_WAITING: Thread waits for timeout
- TERMINATED: Thread has been terminated
public class Counter {
public int count = 0;
// lock method for other threads while it is being executed
public synchronized void increment() {
this.count++;
}
}
Counter counter = new Counter();
// create threads via inheritance
public class Foo extends Thread {
// override `run` method that is used to run in new thread
@Override
public void run() {
counter.increment();
}
}
Foo foo = new Foo(); // instantiate thread
foo.start(); // run thread
// create thread by implementing the `Runnable` functional interface
Thread bar = new Thread(() -> counter.increment());
bar.start(); // run thread
// halt current thread until specified thread has been finished
foo.join();
// pause current thread for some amount of milliseconds
Thread.sleep(1000);
// manage thread priorities (from 1 as lowest to 10 as highest)
foo.getPriority() == 5; // get priority
foo.setPriority(10); // set priority
// thread priority constants
Thread.MIN_PRIORITY == 1;
Thread.NORM_PRIORITY == 5;
Thread.MAX_PRIORITY == 10;
// manage thread names
foo.getName() == "Foo"; // get name
foo.setName("FooBar"); // set name
// access main thread
Thread current = Thread.currentThread();