Ξ  


AlBasmala Archive Tags RSS About

Java CheatSheet

Article image
Abstract
This is a quick reference of concepts in modern Java.

Modern Java is a strongly-typed, eagery evaluated, case sensative, yet whitespace insensative language. It uses hierarchies of classes/types to structure data, but also has first-class support for functional-style algebraic datatypes.

Java programs are made up of ‘classes’, classes contain methods, and methods contain commands. To just try out a snippet of code, we can

1. The Philosophy of Classes & Interfaces

Real life objects have properties and behaviour. For example, my cat has the properties of name and age, and amongst its behaviours are sleep and meow. However, an apple does not have such features. The possible features of an object are understood when we classify it; e.g., my cat is an animal, whereas an apple is food. In Java, a set of features is known as a class and objects having those features are called “objects of that class”. Just as int is the type of the value 12, we say a class is a type of an object.

We tend to think in (disjoint, hierarchical) categories; for example, in my library, a book can be found in one section, either “Sports” or “History” or “Trees”. So where should books on the history of football be located? Or books on the history of trees? My library places such books under “Sports”, and “Tree” respecively, but then adds a “historical” tag to them. Likewise, in Java, to say different kinds of things have a feature in common, we “tag” them with an interface. (Real life tagging is a also known as multi-class-ificiation.)

Java's Main Organisational Mechanisms

    With state Without state
Attributes & properties   class record
Partial implementations   abstract class interface
Here is a teaser of nearly all of these packaging mechanisms
interface Hospitable {
    String name();   /* unimplemented method signature */
    default void greet() { /* derived, implemented, method */
        System.out.printf("%s says “Welcome!”", name());
    }
}

////////////////////////////////////////////////////////////////////////////////

class Person implements Hospitable {
    String name;
    int age;
    public String name() { return name; }
}

// Actual usage of “Person”:
Person me = new Person();
me.name = "Musa";
me.age = 31;
me.greet();

////////////////////////////////////////////////////////////////////////////////

record Person2(String name, int age) implements Hospitable { }

// Actual usage of “Person2”:
Person2 me = new Person2("Musa", 31);
me.greet();
“Interfaces are the types of classes”

A module is a bunch of utilities that can be defined from some shared set of parameters. Those utilities can be thought of as an interface 𝑰. Then a module is a function from parameters to an anonymous implementation of an interface. However, functions that return implementations are essentially records/classes that implement the interface; i.e.,

   𝑰 R(params) { return new 𝑰() { 𝑰_𝑶𝑽𝑬𝑹𝑰𝑫𝑬𝑺 }; } // module-as-function
≈  record R(params) implements 𝑰 { 𝑰_𝑶𝑽𝑬𝑹𝑰𝑫𝑬𝑺 }; // module-as-record

This equation justifies the phrase “interfaces are the types of records/classes” since a record declaration (i.e., the right side of the “≈”) can be converted to an (abstract) module (of type 𝑰) —i.e., the left side of the “≈”.

Algebraic Data Types :ignore

Finally, suppose there's something you want to do and there are a number of ways/configurations to get it done. You could write it as a method in a class with a bunch of if's to account for all of those ways. Better would be to create an interface, then have a bunch of classes that implement it: One class for each possible implementation. Finally, if you know all configurations, you can move those classes into the definition of the interface and make it sealed: This is known as an algebraic data-type, whose kill-feature is that you can use switch to pattern match on instances of the interface.

🔗
ADT example

An example 3-level hierarchy that can be easily represented with ADTs rather than a traditional class hierarchy.

sealed interface Monster {

    sealed interface Flying extends Monster { }
    record Griffin() implements Flying { }
    record Pegasus() implements Flying { }

    sealed interface Ground extends Monster { }
    record Ogre() implements Ground { }
}

Then we can actually use this new type as follows:

private static String glare(Monster m) {
    return switch (m) {
        case Monster.Griffin it -> "Roar";
        case Monster.Pegasus it -> "HeeHaw";
        case Monster.Ogre it -> "Grrr";
    };
}

glare(new Monster.Flying.Griffin()); // "Roar"

Or only look at the Flying sub-type:

private static int attackDamage(Monster.Flying f) {
    return switch (f) {
        case Monster.Flying.Griffin it -> 120;
        case Monster.Flying.Pegasus it -> 60;
    };
}

attackDamage(new Monster.Pegasus()); // 60
Reads
  • “MOOC” Massive Open Online Course - University of Helsinki
    • Useful for learning Java, Python, Haskell, JavaScript.
    • I highly reccommend their “full stack” course on web development, with JS!
  • Effective Java, 3rd Edition by Joshua Bloch
  • Seriously Good Software Code that Works, Survives, and Wins
  • Functional Programming in Java Harnessing the Power of Java 8 Lambda Expressions
  • Java Generics and Collections Speed Up the Java Development Process
  • Java 8 Lambdas Pragmatic Functional Programming - Richard Warburton
🔗
Null

There is a special value named null that denotes the absence of a meaningful value. Ironically, it is a value of every type (excluding the primitive types). Here is a neat story about null.

2. Primitive Objects

For performance reasons, there are a handful of types whose values are created by literals; i.e., “What you see is what you get”. (As such, primitives are a basic building block which cannot be broken apart; whereas non-primitives (aka references) are made-up from primitives and other references.) For example, to create a value of int we simply write 5.

There are no instance methods on literals; only a handful of operator methods. For example, we cannot write 2.pow(3) to compute 2³, but instead must write Math.pow(2, 3). Finally, variables of primitive types have default values when not initialised whereas object types default to null —note: null is a value of all object types, but not of primitive types.

// Declare a new object type
class Person { String name; }

Person obj; // ≈ null (OBJECT)
int prim;    // ≈ 0   (PRIMITIVE)

// Primitives are created as literals
prim = 1;    // ≈ 1

// Objects are created with “new”
obj = new Person(); // ≈ a reference,
   // like: Person@66048bfd

// Primitives are  identified by
// thier literal shape
assert prim == 1;

// Objects are identified by
/// references to their memory
// locations (not syntax shape!)
assert obj != new Person();

// Primitives copy values
int primCopy = prim;  // ≈ 1

/// Objects copy references
Person objCopy = obj;
  // ≈ a reference, like: Person@66048bfd

// Changing primitive copy has
// no impact on original
primCopy = 123;
assert prim == 1;

// Changing object copy also
// changes the original!
assert obj.name == null;
objCopy.name = "woah";    // Alter copy!
// Original is altered!
assert obj.name.equals("woah");
Wrapper Types

Java lets primitives shift back and forth from their literal representations and the world of reference objects somewhat-harmoniously by automatically “boxing” them up as objects when need be. This is done by having class versions of every primitive type; e.g., the primitive int has the class version Integer.

Integer x = 1; // auto-boxed to an object
int y = new Integer(2); // auto-unboxed to a primitive

Primitives require much less memory! An int requires 32-bits to represent, whereas an Integer requires 128-bits: The object requires as much space as 4 primitives, in this case.

3. Properties and methods have separate namespaces

Below we use the name plus1 in two different definitional roles. Which one we want to refer to depends on whether we use "dot-notation" with or without parenthesis: The parentheis indicate we want to use the method.

class SameNameNoProblem {
    public static int plus1(int x){ return x + 1; } // Method!
    public static String plus1 = "+1";             // Property!
}

class ElseWhere {
    String pretty = SameNameNoProblem.plus1;
    Integer three = SameNameNoProblem.plus1(2);
}

The consequence of different namespaces are

  1. Use apply to call functions bound to variables.
  2. Refer to functions outside of function calls by using a double colon, ::.

4. Anonymous functions:    (arg₁, …, argₙ) → bodyHere    

Functions are formed with the “→” notation and used with “apply”

// define, then invoke later on
Function<Integer, Integer> f  =  x -> x * 2;

f.apply(3) // ⇒ 6
// f(3)    // invalid!

// define and immediately invoke
((Function<Integer, Integer>) x -> x * 2).apply(3);

// define from a method reference, using “::”
Function<Integer, Integer> f = SameNameNoProblem::plus1;

Let's make a method that takes anonymous functions, and use it

// Recursion with the ‘tri’angle numbers: tri(f, n) = Σⁿᵢ₌₀ f(i).
public static int tri(Function<Integer, Integer> f, int n) {
    return n <= 0 ? 0 : f.apply(n) + tri(f, n - 1);
}

tri(x -> x / 2, 100);  //  ⇒  Σ¹⁰⁰ᵢ₌₀ i/2 = 2500

// Using the standard “do nothing” library function
tri(Function.identity(), 100);  //  ⇒  Σ¹⁰⁰ᵢ₌₀ i = 5050

Exercise! Why does the following code work?

int tri = 100;
tri(Function.identity(), tri); //  ⇒ 5050

Function<Integer, Integer> tri = x -> x;
tri(tri, 100); //  ⇒ 5050

In Java, everything is an object! (Ignoring primitives, which exist for the purposes of efficiency!) As such, functions are also objects! Which means, they must have a type: Either some class (or some interface), but which one? The arrow literal notation x -> e is a short-hand for an implementation of an interface with one abstract method…

5. Lambdas are a shorthand for classes that implement functional interfaces

Let's take a more theoretical look at anonymous functions.

5.1. Functional Interfaces

A lambda expression is a (shorthand) implementation of the only abstract method in a functional interface ——–which is an interface that has exactly one abstract method, and possibly many default methods.

For example, the following interface is a functional interface: It has only one abstract method.

  public interface Predicate<T> {

      boolean test(T t);  // This is the abstract method

      // Other non-abstract methods.
      default Predicate<T> and(Predicate<? super T> other) { ... }
      // Example usage: nonNull.and(nonEmpty).and(shorterThan5)
      static <T> Predicate<T> isEqual(T target) {...}
      // Example usage: Predicate.isEqual("Duke") is a new predicate to use.
  }

Optionally, to ensure that this is indeed a functional interface, i.e., it has only one abstract method, we can place @FunctionalInterface above its declaration. Then the complier will check our intention for us.

5.2. The Type of a Lambda

Anyhow, since a lambda is a shorthand implementation of an interface, this means that what you can do with a lambda depenends on the interface it's impementing!

As such, when you see a lambda it's important to know it's type is not "just a function"! This mean to run/apply/execute a lambda variable you need to remember that the variable is technically an object implementing a specific functional interface, which has a single named abstract method (which is implemented by the lambda) and so we need to invoke that method on our lambda variable to actually run the lambda. For example,

  Predicate<String> f = s -> s.length() == 3;   // Make a lambda variable
  boolean isLength3String = f.test("hola");     // Actually invoke it.

Since different lambdas may implement different interfaces, the actually method to run the lambda will likely be different! Moreover, you can invoke any method on the interface that the lambda is implementing. After-all, a lambda is an object; not just a function.

Moreover, Function has useful methods: Such as andThen for composing functions sequentially, and Function.identity for the do-nothing function.

5.3. Common Java Functional Types

Anyhow, Java has ~40 functional interfaces, which are essentially useful variations around the following 4:

Class runner Description & example
Supplier get Makes objects for us; e.g., () -> "Hello"!.
Consumer accept Does stuff with our objects, returning void;
    e.g., s -> System.out.println(s).
Predicate test Tests our object for some property, returning a boolean
    e.g., s -> s.length() == 3
Function apply Takes our object and gives us a new one; e.g., s -> s.length()

For example, 𝒞::new is a supplier for the class 𝒞, and the forEach method on iterables actually uses a consumer lambda, and a supplier can be used to reuse streams (discussed below).

The remaining Java functional interfaces are variations on these 4 that are optimised for primitive types, or have different number of inputs as functions. For example, UnaryOperator<T> is essentially Function<T, T>, and BiFunction<A, B, C> is essentially Function<A, Function<B, C>> ———not equivalent, but essentially the same thing.

  • As another example, Java has a TriConsumer which is the type of functions that have 3 inputs and no outputs —since Tri means 3, as in tricycle.

5.4. Eta Reduction: Writing Lambda Expressions as Method References

Lambdas can sometimes be simplified by using method reference:

Method type        
Static   \((x,ys) → τ.f(x, ys)\) \(τ::f\)
Instance   \((x,ys) → x.f(ys)\) \(τ::f\), where τ is the type of \(x\)
Constructor   args → new τ<A>(args) τ<A>::new

For example, (sentence, word) -> sentence.indexOf(word) is the same as String::indexOf. Likewise, (a, b) -> Integer.max(a, b) is just Integer::max.

  • Note that a class name τ might be qualified; e.g., x -> System.out.println(x) is just System.out::println.

6. Variable Bindings

Let's declare some new names, and assert what we know about them.

Integer x, y = 1, z;

assert x == null && y == 1 && z == null;

τ x₀ = v₀, …, xₙ = vₙ; introduces 𝓃-new names xᵢ each having value vᵢ of type τ.

  • The vᵢ are optional, defaulting to 0, false, '\000', null for numbers, booleans, characters, and object types, respectively.
  • Later we use xᵢ = wᵢ; to update the name xᵢ to refer to a new value wᵢ.

There are a variety of update statements: Suppose \(τ\) is the type of \(x\) then,

Augment:  x ⊕= y  ≈  x = (τ)(x ⊕ y)
Increment:   x++  ≈  x += 1)
Decrement:  x--  ≈  x -= 1)

The operators -- and ++ can appear before or after a name: Suppose \(𝒮(x)\) is a statement mentioning the name \(x\), then

𝒮(x++)  ≈  𝒮(x); x += 1
𝒮(++x)  ≈  x += 1; 𝒮(x)

Since compound assignment is really an update with a cast, there could be unexpected behaviour when \(x\) and \(y\) are not both ints/floats.


  • If we place the keyword final before the type τ, then the names are constant: They can appear only once on the right side of an ‘=’, and any further occurrences (i.e., to change their values) crash the program. final int x = 1, y; y = 3; is fine, but changing the second y to an x fails.
  • We may use var x = v, for only one declaration, to avoid writing the name of the type τ (which may be lengthy). Java then infers the type by inspecting the shape of v.
  • Chained assignments associate to the right:

    a += b /= 2 * ++c;  ≈  a += (b /= (2 * ++c));

    (The left side of an “=”, or “⊕=”, must a single name!)

7. Scope, Statements, and Control Flow

var x = 1;

{ // new local scope
  var x = 200; // “shadows” top x
  var y = 300;
  assert x + y == 500;
}

// y is not visible here
assert y == 20; // CRASH!

// The top-most x has not changed
assert x == 1;

⊙ Each binding has a scope, which is the part of the program in which the binding is visible.

local bindings are defined within a block and can only be referenced in it.

⊙ Names within a block /shadow//hide bindings with the same name.

Besides the assignment statement, we also have the following statements:

  • Blocks: If Sᵢ are statements, then {S₀; …; Sₙ;} is a statement.
  • Conditionals: if (condition) S₁ else S₂
  • The “for-each” syntax applies to iterable structures —we will define our own later.

    // Print all the elements in the given list.
    for (var x : List.of(1, 2, 3))
       System.out.printf("x ≈ %s\n", x);
    
  • While-Loops  while (condition) S  and for-loops  for(init; cond; change) body .

       var i = 0; while (i < 10) System.out.println(Math.pow(2, i++));
    ≈
       for(var i = 0; i < 10; i++) System.out.println(Math.pow(2, i));
    

    Exit the current loop with the break; statement. Similarly, the continue; statement jumps out of the body and continues with the next iteration of the loop.

8. switch

Dispatching on a value with switch

⟦Switch Statement⟧

switch (x){
  case v₁: S₁
  ⋮
  case vₙ: Sₙ
  default: Sₙ
}

The switch works as follows: Find the first 𝒾 with x == vᵢ, then execute {Sᵢ; ⋯; Sₘ;}, if there is no such 𝒾, execute the default statement Sₙ. Where Sₘ is the first statement after Sᵢ that ends with break;.

E.g., case v: S; case w: S′; break means do S;S′ if we see v but we do S′ when seeing both v and w.

switch (2){
  case 0: System.out.println(0);
  case 1: System.out.println(1);
  case 2: System.out.println(2);
  default: System.out.println(-1);
} // ⇒ Outputs: 2 -1

⟦Switch Expression⟧ If we want to perform case analysis without the fall-over behaviour, we use arrows ‘→’ instead of colons ‘:’.

   switch (2){
     case 0 -> 0;
     case 1 -> 1;
     case 2 -> 2;
     default -> -1;
   } // ⇒ 2

9. Strings

Any pair of matching double-quotes will produce a string literal —whereas single-quote around a single character produce a character value. For multi-line strings, use triple quotes, """, to produce text blocks.

String interpolation can be done with String.format using %s placeholders. For advanced interpolation, such as positional placeholders, use MessageFormat.

String.format("Half of 100 is %s", 100 / 2) // ⇒ "Half of 100 is 50"
  • s.repeat(𝓃) ≈ Get a new string by gluing 𝓃-copies of the string 𝓈.
  • s.toUpperCase() and s.toLowerCase() to change case.
  • Trim removes spaces, newlines, tabs, and other whitespace from the start and end of a string. E.g., " okay \n ".trim().equals("okay")
  • s.length() is the number of characters in the string.
  • s.isEmpty()  ≡  s.length() == 0
  • s.isBlank()  ≡  s.trim().isEmpty()
  • String.valueOf(x) gets a string representation of anything x.
  • s.concat(t) glues together two strings into one longer string; i.e., s + t.

10. Equality

  • In general, ‘==’ is used to check two primitives for equality, whereas .equals is used to check if two objects are equal.
  • The equality operator ‘==’ means “two things are indistinguishable: They evaluate to the same literal value, or refer to the same place in memory”.
  • As a method, .equals can be redefined to obtain a suitable notion of equality between objects; e.g., “two people are the same if they have the same name (regardless of anything else)”. If it's not redefined, .equals behaves the same as ‘==’. In contrast, Java does not support operator overloading and so ‘==’ cannot be redefined.
  • For strings, ‘==’ and .equals behave differently: new String("x") == new String("x") is false, but new String("x").equals(new String("x")) is true! The first checks that two things refer to the same place in memory, the second checks that they have the same letters in the same order.
    • If we want this kind of “two objects are equal when they have the same contents” behaviour, we can get it for free by using records instead of classes.

11. Arithmetic

In addition to the standard arithmetic operations, we have Math.max(x, y) that takes two numbers and gives the largest; likewise Math.min(x, y). Other common functions include Math.sqrt, Math.ceil, Math.round, Math.abs, and Math.random() which returns a random number between 0 and 1. Also, use % for remainder after division; e.g., n % 10 is the right-most digit of integer \(n\), and n % 2 == 0 exactly when \(n\) is even, and d % 1 gives the decimal points of a floating point number \(d\), and finally: If d is the index of the current weekday (0..6), then d + 13 % 7 is the weekday 13-days from today.

// Scientific notation: 𝓍e𝓎 ≈ 𝓍 × 10ʸ
assert 1.2e3 == 1.2 * Math.pow(10, 3)
// random integer x with 4 ≤ x < 99
var x = new Random().nextInt(4, 99);

Sum the digits of the integer $n = 31485$

int n = 31485;
int sum = 0;
while (n % 10 != 0) { sum += n % 10; n /= 10; }
assert sum == 3 + 1 + 4 + 8 + 5;

A more elegant, “functional style”, solution:

String.valueOf(n).chars().map(c -> c - '0').sum();

The chars() methods returns a stream of integers (Java characters are really just integers). Likewise, IntStream.range(0, 20) makes a sequence of numbers that we can then map over, then sum, min, max, average.

12. Collections and Streams

Collections are types that hold a bunch of similar data: Lists, Sets, and Maps are the most popular. Streams are pipelines for altering collections: Usually one has a collection, converts it to a stream by invoking .stream(), then performs map and filter methods, etc, then “collects” (i.e., runs the stream pipeline to get an actual collection value back) the result.


Lists are ordered collections, that care about multiplicity. Lists are made with List.of(x₀, x₁, …, xₙ). Indexing, xs.get(𝒾), yields the 𝒾-th element from the start; i.e., the number of items to skip; whence xs.get(0) is the first element.

Sets are unordered collections, that ignore multiplicity. Sets are made with Set.of(x₀, x₁, …, xₙ).

Maps are pairs of ‘keys’ along with ‘values’. Map<K, V> is essentially the class of objects that have no methods but instead have an arbitary number of properties (the ‘keys’ of type K), where each property has a value of type V. Maps are made with Map.of(k₀, v₀, …, k₁₀, v₁₀) by explicitly declaraing keys and their associated values. The method ℳ.get(k) returns the value to which the specified key k is mapped, or null if the map ℳ contains no mapping for the key. Maps have an entrySet() method that gives a set of key-value pairs, which can then be converted to a stream, if need be.


Other collection methods include, for a collection instance 𝒞:

  • 𝒞.size() is the number of elements in the collection
  • 𝒞.isEmpty()  ≡  𝒞.size() == 0
  • 𝒞.contains(e)  ≡  𝒞.stream().filter(x -> x.equals(e)).count() > 0
  • Collections.fill(, e)  ≅  ℒ.stream().map(_ -> e).toList(); i.e., copy list but replace all elements with e.
  • Collections.frequency(𝒞, e) counts how many times e occurs in a collection.
  • Collections.max(𝒞) is the largest value in a collection; likewise min.
  • Collections.nCopies(n, e) is a list of \(n\) copies of e.

Stream<τ> methods

  • Stream.of(x₀, ..., xₙ) makes a stream of data, of type τ, ready to be acted on.
  • s.map(f) changes the elements according to a function \(f : τ → τ′\).
    • s.flatMap(f) transforms each element into a stream since \(f : τ → Stream<τ′>\), then the resulting stream-of-streams is flattened into a single sequential stream.
    • As such, to merge a streams of streams just invoke .flatMap(s -> s).
  • s.filter(p) keeps only the elements that satisfy property p
  • s.count() is the number of elements in the stream
  • s.allMatch(p) tests if all elements satisfy the predicate p
  • s.anyMatch(p) tests if any element satisfies p
  • s.noneMatch(p)  ≡  s.allMatch(p.negate())
  • s.distinct() drops all duplicates
  • s.findFirst() returns an Optional<τ> denoting the first element, if any.
  • s.forEach(a) to loop over the elements and perform action a.
    • If you want to do some action, and get the stream s back for further use, then use s.peek(a).

13. Generics

Java only lets us return a single value from a method, what if we want to return a pair of values? Easy, let's declare record Pair(Object first, Object second) { } and then return Pair. This solution has the same problem as methods that just return Object: It communicates essentially no information —after all, everything is an object!— and so requires dangerous casts to be useful, and the compiler wont help me avoid type mistakes.

record Pair(Object first, Object second) { }

// This should return an integer and a string
Pair myMethod() { return new Pair("1", "hello"); } // Oops, I made a typo!

int num = (int) (myMethod().first()); // BOOM!

It would be better if we could say “this method returns a pair of an integer and a string”, for example. We can do just that with generics!

record Pair<A, B>(A first, B second) { }

Pair<Integer, String> myMethod() { return new Pair<>(1, "hello"); }

int num = myMethod().first();

This approach communicates to the compiler my intentions and so the compiler ensures I don't make any silly typos. Such good communication also means no dangerous casts are required.

We can use the new type in three ways:

Pair<A, B> explicitly providing the types we want to use Pair with
Pair<> letting Java infer, guess, the types for Pair by how we use it
Pair defaulting the types to all be Object

The final option is not recommended, since it looses type information. It's only allowed since older versions of Java do not have type parameters and so, at run time, all type parameters are ‘erased’. That is, type parameters only exist at compile time and so cannot be inspected/observed at run-time.




Generated by Emacs and Org-mode (•̀ᴗ•́)و
Creative Commons License
Life & Computing Science by Musa Al-hassy is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License