@rkenmi - Java Essentials

Java Essentials


Java Essentials


Back to Top

Updated on November 21, 2019

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 the compareTo() method. This means that if you want to provide additional ways to sort by different criteria, a Comparable may not be the best solution. In general, Comparable is nice to implement when you care about natural sorting; that is, an object that implements Comparable must know how it needs to be ordered.
  • Comparator: compares two objects for you, instead of itself. You can implement multiple Comparator 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]  

Article Tags:
Javajvmbox typesunlistedwiki