JVM
Java is a statically typed language that is compiled into bytecode (i.e. with javac
) and understood only by the JVM, or Java Virtual Machine. The JVM is an interpreter that reads the Java bytecode and translates them into machine code before execution. The JVM also handles garbage collection.
Languages like C or C++ will compile the code into machine code (i.e. MASM), meaning that they skip one layer of abstraction. This means that in theory, C or C++ is faster, but Java will excel for cross-platform support due to the JVM.
Heap vs. Stack
The heap is a part of memory that is reserved for allocation of reference types (i.e. Objects
). The heap is generally slower than the stack but the heap is able to use a significantly larger amount of memory than the stack.
The stack on the other hand is a part of memory that is fixed in size and it is reserved for allocation of primitive types (i.e. int
, float
) and function calls (i.e. stack overflow). The stack is indeed faster, but much, much smaller than the heap in terms of memory space.
Box Types
Box Types are essentially wrappers around primitive data types. For example, Integer
is a box type for the primitive int
.
Box Types are useful for a few reasons. The biggest reason (arguably) is because Box Types allow primitive data type values to be stored into useful Java data structures, such as HashMap
or ArrayList
. Primitive data types do not have enough memory to have their own non-static methods. Thus, they do not have a .equals()
or .hashCode()
implementation, which is needed in order to store them into a HashMap
or ArrayList
.
Autoboxing
Autoboxing is a feature that is introduced after Java 1.5. It allows for interchangeable assignments between primitive data types and their corresponding box types.
A valid autoboxing example:
int a = 0;
Integer b = a;
int c = b;
Equality
For primitive types, comparing them with the ==
operator suffices.
For reference types however, ==
can often produce a misleading output.
This is simply due to the fact that the ==
operator checks for the memory address when compared against Objects. This means that if you have two Objects that were instantiated separately and are logically equivalent (that is, they both have the same exact data inside), they will still fail the ==
check since these two Objects are pointing to different areas in memory.
The solution to the problem above is to override the .equals
method, which is only provided by Objects and sub-classes of Objects (every class is a subclass of Object
). The .equals()
method will have an Object
parameter representing the value to compare. Thus, if a custom class overrides this method, it must typecast the Object
into the custom class type itself. Afterwards, the method should check for equality of all of the data variables inside the target Object.
Finally, the .hashCode()
method by default will hash the address of the object in question and return it. This is fine and dandy for most intents and purposes, but not quite so when you want to store an Object into a hashed data structure (i.e. HashSet
, HashMap
) and expect the Object to adhere to equality rules when stored as a key.
For example, observe the following example:
Dummy A = new Dummy(3);
Dummy B = new Dummy(3);
HashSet<Dummy> hs = new HashSet<>(Arrays.asList(A, B));
for (Dummy d : hs) {
System.out.println(d);
}
If the hashCode()
of Dummy
returns, say, 1
, and the .equals()
of Dummy
returns true
if two Objects are logically equivalent, then the output of the above code would be:
> Dummy@1
Therefore, one should override both .equals()
and .hashCode()
on custom Objects when they are used with data structures that rely on the hash.
Optionals
Introduced in Java 8, Optionals are a great way to replace the identity of null
objects. By doing this, it removes a lot of redundant null
checks plastered in Java code, removes the hassle of working with NullPointerExceptions and also makes it great for use with Java 8 Streams to simplify and make your code easier to read.
To check if an Optional<T>
object is present, you can simply call the .isPresent()
method. However, Optionals really shine with the .orElse
method, which allows you to return a substitute value in the case that the object is empty.
Optional<Integer> num = Optional.of(10);
System.out.println(num.isPresent()); // true
System.out.println(num); // 10
Optional<Integer> emptyNum = Optional.ofNullable(null);
System.out.println(emptyNum.isPresent()); // false
System.out.println(emptyNum.orElse(5)); // 5
Sorting
Comparable vs. Comparator
Comparable
and Comparator
are both interfaces that are provided to help with sorting.
In particular,
Comparable
: compares the object itself. You only have one opportunity to implement thecompareTo()
method. This means that if you want to provide additional ways to sort by different criteria, aComparable
may not be the best solution. In general,Comparable
is nice to implement when you care about natural sorting; that is, an object that implementsComparable
must know how it needs to be ordered.Comparator
: compares two objects for you, instead of itself. You can implement multipleComparator
objects to provide additional ways to sort.
Classes
Classes are the blueprints that allow you to instantiate objects.
Extends vs. Implements
The keyword extends
is used to create a subclass from another class. The parent class can be abstract
or a regular class, but not an interface
. The extended class does not necessarily have to implement all of the methods of the parent.
The keyword implements
is used to create a class based on an interface
. When a class implements
an interface, the class must actually implement all of the methods defined in the interface.
With implements
, classes can do multiple inheritance by implementing more than one interface
. The same cannot be said for extends
; a Java class can extend only one class at a time.
In general, object composition is preferred over inheritance, so interfaces are quite useful.
Anonymous Inner Classes
Anonymous classes allow you to declare and instantiate a class at the same time.
Here is a handy example with hash maps:
HashMap<String, String> hashMap = new HashMap<>(){{
put("A", "0");
put("B", "1");
}};
Lambda Functions
WIP
Data Structures
Empty array
ArrayList<T> list = new ArrayList<>();
Array with values
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
Empty hash map
HashMap<T, T> hashmap = new HashMap<>();
Hash map with values
Use ImmutableMap from the Google collections library. Note that this map will be read-only.
ImmutableMap.of("key1", "value1", "key2", "value2");
Empty set
HashSet<T> s = new HashSet<>();
Set with values
HashSet<Integer> s = new HashSet<>(Arrays.asList(1, 2, 3));
Min heap
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(1);
pq.add(2);
pq.add(3);
// Check HEAD
System.out.println(pq.peek()); // 1
// Remove and return HEAD
System.out.println(pq.poll()); // 1
Max heap
PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder());
pq.add(1);
pq.add(2);
pq.add(3);
// Check HEAD
System.out.println(pq.peek()); // 3
// Remove and return HEAD
System.out.println(pq.poll()); // 3
Pair/tuple
Pair<Integer, Integer> somePair = new Pair<>(1, 3);
System.out.println(somePair.getKey()); // 1
System.out.println(somePair.getValue()); // 3
Queue
LinkedList<String> queue = new LinkedList<>();
// Enqueue
queue.add("abc");
queue.add("def");
queue.add("ghi");
// Dequeue
queue.removeFirst();
// Enqueue
queue.add("jkl");
System.out.println(queue); // [def, ghi, jkl]
System.out.println(queue.poll()); // def
System.out.println(queue.pollLast()); // jkl
Stack
Stack<Double> stack = new Stack<>();
stack.push(0.3);
stack.push(0.1);
// Pop 0.1
stack.pop();
stack.push(1.3);
System.out.println(stack); // [0.3, 1.3]