Introduction

Java in typed language, we all know that. It requires JVM (java virtual machine) to execute the Java programs. Java needs a compiler which converts a java code into bytecode (fileName.class) file. This fileName.class is used by the JVM to execute the programs. To execute a Java program we need a main method, no matter how many java files we have we only need one main method to execute.

class Example {
	public static void main(String args[]) {
		// Something here to execute.
	}
}

Variables

They are two types:

  1. Primitive
  2. Reference

Primitive

All the basic types int, boolean, float, short , byte, long, double come under primitive. When you use a primitive variable you are actually storing the value of that variable in the bit form in the memory. So when you compare two primitive variable if there are the same value the result will be true which is not the case for the reference variable.

double > float > long > int > short > byte > boolean

When you assign a higher primitive variable to a lower primitive variable java gives you an error you can’t do that unless you do explicit casting.

int i = 1;
bollean matched = false;
float decimal = 32.11;
 
// Explicit casting
int j = (int) decimal;

Reference

These are used to refer to a object, you can’t store an object in a variable in java you only get the reference to that object in other words you can control that object but can’t have the actual object. So when two reference variable refer to a objects which are equal you can’t compare them using the == operator, but you can use equals() method.

If a object doesn’t have any references then that object is eligible for garbage collection. Arrays are objects in java even if they hold primitive values.

Dog d = new Dog();
d.name = "Mikkey";
Dog c = new Dog();
c.name = "Mikkey"
(d == c) // results false;
(d.equals(c)) // results true, if equals is implements based on name

Wrappers

In java you can only use objects in collections, which means using primitive variables gives an error, but the compiler uses Wrapper classes when you pass a primitive variable. Wrapper classes wrappers around primitive variables with some static methods.

int i = 1;
Integer i = 1; // Wrapper class
boolean matched = true;
Boolean matched = true; // Wrapper class

Optional Wrapper

There is a special Wrapper Optional, when we can’t guarantee method returns something we use optional wrapper to check whether it’s present or not

Optional<IceCream> optional = getIceCream("Strawberry");
if (optional.isPresent()) {
	IceCream ice = optional.get();
	// do something
}

Objects

In Java everything is an object and we should think in that way. We need classes to create objects. Classes are blueprints which tells how a object should be. An object contains two things behaviour (methods) and state (instance variables). A class is not an object it is used to construct an object. We access the state of object using dot operator.

class Dog {
	String name; // state
 
	public void bard() {
		System.out.println("bow, bow");
	}
 
	public static void main(String args[]) {
		Dog d = new Dog();
		d.name = "Mikkey"; // dot operator to access name variable
		d.bark();
	}
}

Encapsulation

This just means protecting your instance variables. Your instance variables can’t be open they can be changed by the user how ever they want. YOU HAVE TO PROTECT THEM. The way you do that is have the keyword private in-front of instance variables. Then how to actually change or access them? By using getters and setter methods. By the way instance variables always have a default value if though you didn’t initialize them.

Encapsulation gives you control over who changes the data in your class and how.

class Dog {
	private String name;
 
	public void setName(String name) {
		this.name = name // this referes to the current object
	}
 
	public String getName() {
		return name;
	}
}

Stack and Heap

Java has two places stack and heap to store things. Stack is for the local variables. Heap for instance variables. When you call a another method from inside a method, one more stack gets added to the stack. All objects live on the heap regardless of where they are instantiated but references to those objects live with respect to their scope (local or instance).

Constructors

Every class has a constructor, if you don’t write one the compiler will create one for you. Constructor are used to construct an object. They don’t have return types unlike methods. You can overload constructors for a single class. A constructor is called when you use new keyword to initialize an object. Constructors have the same name has class name.

public class Dog {
	private int size;
	private String breed;
 
	// constructor
	public Dog (int size) {
		this.size = size;
	}
	// overloading a constructor
	public Dog(int size, String breed) {
		this.size = size;
		this.breed = breed;
	}
}

Inner Classes

Inner classes are used to implement the same interface more than once. They have access to all variables and methods of the outer class including the private ones. They can initialized from outside the class but it’s a bit tricky.

Methods

Methods can have parameters, which you have to have while calling that method, also they can have return types. A method has parameters and user passes arguments to the method. Java is a pass-by-value which mean pass-by-copy. Whenever you pass an argument to a method the bits of the arguments are copied into the parameters of the method. So when you pass primitives they are copied one-to-one by bit-by-bit, which means you change that variable inside the method the original primitive value doesn’t change. When you pass a reference variable the method parameter gets the copy of reference to a object so when you change something in the object, the actual object changes.

You can use enhanced for loop for collections You can also declare more than one variable inside a for-loop

int[] nums = {1, 2, 3, 4};
// Regular loop
for (int i = 0; i < nums.length; i++) {
	System.out.println(i);
}
// Enhanced for loop
for (int i : nums) {
	System.out.println(i);
}

OverLoading

When can multiple methods with the same name in the same class but we need to have different parameters. This only works if the parameters are in different order or the number of parameters are different

public void hello(String name) {
	System.out.println("hello " + name);
}
 
// overloading
public void hello(String name, int age) {
	System.out.println("hello " + name + " you are " + age + " years old.");
}

Libraries

In java by default java.lang is imported, all the default things we use such as (Strings, System) are part of java.lang package. We import packages to use already existing code so that we can focus on important things. Java includes many predefined packages. The main package we use is java.util which contains all the collections.

import java.util.ArrayList
 
ArrayList<String> arr = new ArrayList<>();

In the above code we used parameterized type (angle brackets) to indicate which type of objects the ArrayList hold, we left angle brackets on the right side empty because compiler can figure out based on the left hand side that ArrayList is of type String. Instead of importing the package we can use the full name of ArrayList everywhere in the code.

java.util.ArrayList<String> arr = new java.util.ArrayList<>();

Inheritance

Java’s main feature is to reuse the code everywhere we can. Inheritance can help us achieve that. When a class inherits another class it gains all the functionality of that super class. For example if class B inherits class A then class B gets all the features of class A and class B can have extra functionality and also if class C inherits class B then class C all functionality of both class A and B. But a class can have only one superclass.

If a class has private methods or variables then they don’t get inherited to the subclasses.

If a subclass wan’t a different behaviour from a method inherited from the superclass then the subclass can override the method and it’s own functionality.

Class A {
	public String mother = "Mom";
 
	public void hello() {
		System.out.println("Hello World");
	}
}
 
Class B extends A {
	public void hi() {
		System.out.println("Hey there!");
	}
	public static void main(String args[]) {
		B child = new B();
		System.out.println(B.mother) // accessing superclass instance variable
		B.hello() // calling superclass method
	}
}

Java doesn’t support multiple inheritance (we use interfaces for that). You can invoke a superclass method by using super.methodName(). When a subclass is instantiated all the superclass constructors are called.

Polymorphism

In java you can reference a subclass object using a superclass reference variable this is polymorphism, which means many forms.

Abstract and Concrete Classes

When you don’t a class to initialized by itself unless it’s extended then we use abstract keyword in-front of the class to mark it (obviously) abstract. You can also mark a method abstract, abstract methods don’t contain any body and you must override the abstract method to implement it.

abstract public class Animal {
	public void eat() {
		// somethidng;
	}
}
// abstract method
public abstract void eat();

Concrete classes are classes which can be initialized.

Every class in java extends Object class.

Some methods of Object class are:

  1. equals()
  2. getClass()
  3. toString()
  4. hashCode()

If you use polymorphism to refer to objects you can only call methods of the referred type rather then the actual object.

Interface

It is used to solve the problem of multiple inheritance. Interfaces lets us treat the objects by the role it plays rather than by the class type from which it was instantiated. All methods in an interface should be abstract. We use implements keyword to implement an interface.

public interface Pet {
	public abstract void bark();
	public abstract void eat();
}
 
public class Dog implements Pet {
	public void bard() {
		System.out.println("bow, bow");
	}
	public void eat() {
		// something.
	}
}

You can implement multiple interfaces for a single class.

Keywords

Static

In java everything is an object but sometimes we want global methods and instance variables, static keyword lets us do that.

Static variables are per class rather than per object Static methods don’t require an instance to run them

Static methods can only use and refer to another static methods and static variables, it can’t have non-static things in it. All static variables in class are initialized before any object is created.

class Player {
	static score;
	private String name;
 
	// Constructor
	public Player(String name) {
		this.name = name;
		score++;
	}
 
	// Static method
	public static void mian(String args[]) {
		// Something
	}
}

Final

When you use a final keyword, it means it can’t be changed. You can’t change the value of variable declared with final keyword. You can’t override a method declared with final. You can’t have a class has a superclass if it is final.

In general final static variables are used as global constants (Ex: PI)

Data Structures

Most of the data structures we are in java.util package and most of directly or indirectly extends Collections class.

graph TD
    Collection["Collection (interface)"]
    Set["Set (interface)"]
    List["List (interface)"]
    SortedSet["SortedSet (interface)"]
    
    Collection --> Set
    Collection --> List
    Set --> SortedSet
    
    Set -.-> LinkedHashSet
    Set -.-> HashSet
    SortedSet -.-> TreeSet
    List -.-> ArrayList
    List -.-> LinkedList
    List -.-> Vector

    class Collection,Set,List,SortedSet interface;
graph TB
    Map["Map (interface)"]
    SortedMap["SortedMap (interface)"]
    Map --> SortedMap
    Map -.-> HashMap
    Map -.-> LinkedHashMap
    Map -.-> Hashtable
    SortedMap -.-> TreeMap
    class Map,SortedMap interface;

We use some helpful static methods from Collections to make our life a bit more efficient, we use Collections.sort() to sort a List, but to use it a class must implement Comparable.

public interface Comparable<T> {
	public int compareTo(T o);
}

We can also use Comparator which is an interface with a compare method.

Streams and Lamdas

Streams

A Streams API is a set of operations we can perform on collections. A steam as three things:

  1. Source
  2. Intermediate Operations
  3. Terminal Operation We call this stream pipeline When we add operations to stream pipeline it doesn’t change the original collection and also it doesn’t store the result of each operation, only when the terminal operation is used it gives the result.
List<String> list = new ArrayList<>(); // A collection
List<String> ans = list.stream() // source
					   .sorted() // intermediate operation
					   .collect(Collections.toList()) // termianl operation

Lamdas

They make the code shorter. If an interface has only Single Abstract Method (SAM) also known as FunctionalInterface we can use lamda expressions.

class Song {
	private String artist;
	// something
}
 
List<Song> songs = new ArrayList<>();
songs.sort((a, b) -> a.getArtist().compareTo(b.getArtist())); // Lamda expression

Exceptions

Exceptions are objects which extends the class Exception. When you have risky behaviour in your method like server crashing down you want to alert the user that there are getting into danger zone. So the user has to handle those exceptions. Exceptions are two types:

  1. Checked
  2. Unchecked Checked exceptions are thrown by the compiler. We use the keyword throws to specify a class can throw an exception. we throw a new exception use throw new keywords.
class Server throws SomeException {
	public void crash() {
		throw new SomeException();
	}
}

Unchecked exceptions are thrown at runtime so they don’t have to be handled. We handle a exception by using try and catch blocks

try {
	Server s = new Server();
	s.crash();
} catch(SomeException ex) {
	// Print Something
}

We will also use finally keyword to perform something in the end no matter what.

try {
	Server s = new Server();
	s.crash();
} catch(SomeException ex) {
	// Do Something
} finally {
	// Do Something Else
}

You can have multiple catch blocks for a single try Exceptions are polymorphic If you are not prepared to handle the exception you can duck it by throwing the same exception.

Serialization

It is saving the state of an object (instance variables). If your instance variables contains references to objects then those objects will also be serialized. If you want a class to be serialized they should implement Serializable. Every object inside a class also must implement Serializable for it to be serialized. In java data moves in streams from one place to another. Java I/O API has connection streams which connect source and destination.

We can use transient keyword to skip a instance variable from being serialized.

import java.io.* // has Serializable and other i/o things
 
class Square implements Serializable {
	private int length;
 
	public Square(int length) {
		this.length = length;
	}
 
	public staic void main(String args[]) {
		Square s = new Square(3);
 
		try {
			FielOutputStream fs = new FileOutputStream("file.ser");
			ObjectOutputStream os = new ObjectOutputStream(fs);
			os.writeObject(s);
			os.close();
		} catch (Exception ex) {
			ex.printStactTrace();
		}
 
		// deserialze
		try {
			ObjectInputStream is = new ObjectInputStream(new FileInputStream("file.ser"));
			Square s2 = (Square) is.readObject();
		} catch (Exception ex) {
			// Something
		}
	}
}

Threads

Threads are way to run different parts of the program simultaneously, but we can’t guarantee which part runs when. This behaviour leads to unexpected behaviour. There are several ways to solve this, we can use Atomic variables with compareAndSet() or synchronized keyword. Threads leads to Concurrency issues so we use synchronized keyword to ensure only a single thread can access that part at a time, other threads have to wait. If two threads are waiting for the other thread to complete, this leads to DEADLOCK.

Instead of using Threads directly we use Executors and ExecutorService to manage threads. To make a job for your thread it needs to implement runnable.

class Test {
	public static void main(String args[]) {
		Runnable threadJob = new MyRunnable();
		Thread myThread =  new Thread(threadJob);
		myThread.start();
 
		// using execuotrservice
		Runnable job = new MyRunnable();
		ExecutorService executor = Executors.newSingleThreadExecutor();
		executor.execute(job);
		executor.shutdown(job);
		
		// using synchronized
		private void goShopping(int amount) {
			if (account.getBalance() >= amount) {
				System.out.println(name + " is about to spend");
				account.spend(amount);
				System.out.println(name + " finishes spending");
			} else {
				System.out.println("Sorry, not enough for " + name);
			}
		}
 
		// using Atomic
		private AtomicInteger balance = new AtomicInteger(100);
		boolean success = balance.compareAndSet(expectedValue, newValue);
	}
}

Extras

Strings are Wrapper classes are immutable

Using StringBuilder instead of Strings to manipulate strings

TWR (Try with Resource), when you use this you don’t have explicitly close the things you have opened

Varags lets us take has many parameters has we want.

Annotations give compiler extra info about the topic and helps the IDE to better understand the code.

You can use Streams parallelly, but only do that for large data things.

Enums are replacement for global constants.

We can use var in java in some places.

There are records in java which are used to store data instead of using a class we use records.