Ξ
- 1. The Philosophy of Classes & Interfaces
- 2. Primitive Objects
- 3. Properties and methods have separate namespaces
- 4. Anonymous functions:
(arg₁, …, argₙ) → bodyHere
- 5. Lambdas are a shorthand for classes that implement functional interfaces
- 6. Variable Bindings
- 7. Scope, Statements, and Control Flow
- 8.
switch
- 9. Strings
- 10. Equality
- 11. Arithmetic
- 12. Collections and Streams
- 13. Generics
Java CheatSheet
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
Open a terminal and enter
jshell
; then enter:1 + 2 // The jshell lets you try things out! // Say hello in a fancy way import javax.swing.*; JOptionPane.showMessageDialog(new JFrame(), "Hello, World!");
- Alternatively, in IntelliJ, click Tools then Groovy Console to try things out!
- Finally, VSCode allows arbitrary Java code to be sent to a
jshell
in the background(!) and it echoes the result in a friendly way.
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
- Use
apply
to call functions bound to variables. - 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 —sinceTri
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 justSystem.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 to0, false,
'\000'
,null
for numbers, booleans, characters, and object types, respectively. - Later we use
xᵢ = wᵢ;
to update the namexᵢ
to refer to a new valuewᵢ
.
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 secondy
to anx
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 ofv
. 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-loopsfor(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, thecontinue;
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
char
acter 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()
ands.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 anythingx
.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, butnew 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
record
s instead ofclass
es.
- 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
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
char
acters 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 withe
.Collections.frequency(𝒞, e)
counts how many timese
occurs in a collection.Collections.max(𝒞)
is the largest value in a collection; likewisemin
.Collections.nCopies(n, e)
is a list of \(n\) copies ofe
.
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 propertyp
s.count()
is the number of elements in the streams.allMatch(p)
tests if all elements satisfy the predicatep
s.anyMatch(p)
tests if any element satisfiesp
s.noneMatch(p)
≡s.allMatch(p.negate())
s.distinct()
drops all duplicatess.findFirst()
returns anOptional<τ>
denoting the first element, if any.s.forEach(a)
to loop over the elements and perform actiona
.- If you want to do some action, and get the stream
s
back for further use, then uses.peek(a)
.
- If you want to do some action, and get the stream
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 (•̀ᴗ•́)و
Life & Computing Science by Musa Al-hassy is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License