DEV Community

Cover image for Java 101: Zero to Hero Course
Louai Boumediene
Louai Boumediene

Posted on • Updated on

Java 101: Zero to Hero Course

Introduction:

Your introduction is engaging and sets a friendly tone for the blog. Here's a corrected version with some minor adjustments for clarity:

Welcome to Java 101, where I'll take you on my personal journey of learning Java. In this comprehensive blog post, we'll cover everything you need to know about the Java language, from setting up your development environment to diving deep into object-oriented programming and exploring advanced concepts.

Java is a versatile and powerful programming language used in a wide range of applications, from building web applications to developing mobile apps and enterprise systems. That's why it holds a special place in my heart.

So, get ready to embark on this adventure because it will be one of the longest blog posts I've ever written. I'll structure it as follows:

  1. Part 1: The Fundamentals (variables, data types, flow controls, logical operators, ...)
  2. Part 2: Object-Oriented Programming (Classes and Objects, Inheritance, Polymorphism, Encapsulation, Abstraction, Interfaces, ...)
  3. Part 3: Advanced Java Topics (Exceptions, Generics, Collections, Functional Programming, Concurrency, Multithreading)

Let's dive in and explore the world of Java together!


Section 1 | Language Fundamentals

Sure! Let's break down each part of data types in a simple and easy-to-understand way.

Primitive Data Types

Primitive data types are the most basic building blocks of data in Java. They represent single values with no internal structure. Here are some common primitive data types:

  • byte: Represents a 1 byte whole number in the range [-128, 127].
  • short: Represents a 2 bytes whole number in the range [-32,768, 32,767].
  • int: Represents a 4 bytes whole number in a wide range.
  • long: Represents an 8 bytes whole number in a very wide range.
  • float: Represents a 4 bytes floating-point number, suitable for approximate representations of real numbers.
  • double: Represents an 8 bytes floating-point number, providing higher precision compared to float.
  • boolean: Represents true or false values.
  • char: Represents single characters like 'A', 'b', '%'.

Let's see some examples:

byte age = 25;
short population = 32000;
int distance = 150000;
long globalPopulation = 7760000000L; // Note the 'L' suffix for long literals

float temperature = 20.5f; // Note the 'f' suffix for float literals
double height = 5.9;

boolean isSunny = true;
char grade = 'A';
Enter fullscreen mode Exit fullscreen mode

In this section, we've provided an overview of the sizes and ranges of various primitive data types in Java, including examples demonstrating their usage. Additionally, we introduced the float data type for representing floating-point numbers with approximate precision.

Reference Data Types

Reference data types refer to objects in Java. Unlike primitive data types, they have complex structures and are stored in memory dynamically. Here are some examples of reference data types:

  • String: Represents a sequence of characters, like "Hello, World!".
  • Array: Represents a collection of elements of the same type.

Let's see how we can use them:

String name = "Alice";
int[] numbers = {1, 2, 3, 4, 5};
Enter fullscreen mode Exit fullscreen mode

Here, name is a String that stores the name "Alice", and numbers is an array of integers.

Input Reading Techniques

In Java, we can read input from the user using the Scanner class. Here's how we can use it to read integers and strings:

import java.util.Scanner;

public class InputExample {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("Enter your age: ");
        int age = scanner.nextInt();

        System.out.print("Enter your name: ");
        String name = scanner.next();

        System.out.println("Hello, " + name + "! You are " + age + " years old.");

        scanner.close(); // Don't forget to close the scanner to release resources.
    }
}
Enter fullscreen mode Exit fullscreen mode

When you run this program, it will prompt you to enter your age and name. Then it will print a greeting message with your input values.

That's it! These are the basics of data types in Java, explained in a simple and easy-to-understand way. Feel free to experiment with these examples to deepen your understanding!

Absolutely! Let's break down each concept of control flow with code examples for better understanding:

Comparison Operators

Comparison operators are used to compare two values and return a boolean result. Here are some common comparison operators:

  • Equal to (==): Returns true if two values are equal.
  • Not equal to (!=): Returns true if two values are not equal.
  • Greater than (>): Returns true if the left operand is greater than the right operand.
  • Greater than or equal to (>=): Returns true if the left operand is greater than or equal to the right operand.
  • Less than (<): Returns true if the right operand is greater than the left operand.
  • Less than or equal to (<=): Returns true if the left operand is less than or equal to the right operand.
int x = 5;
int y = 10;

System.out.println(x == y);  // Outputs: false
System.out.println(x != y);  // Outputs: true
System.out.println(x > y);   // Outputs: false
System.out.println(x < y);   // Outputs: true
System.out.println(x >= y);  // Outputs: false
System.out.println(x <= y);  // Outputs: true
Enter fullscreen mode Exit fullscreen mode

Logical Operators

Logical operators are used to perform logical operations on boolean expressions. Here are the common logical operators:

  • AND (&&): Returns true if both operands are true.
  • OR (||): Returns true if at least one of the operands is true.
  • NOT (!): Returns the opposite of the operand's boolean value.
boolean isSunny = true;
boolean isWarm = false;

System.out.println(isSunny && isWarm);  // Outputs: false
System.out.println(isSunny || isWarm);  // Outputs: true
System.out.println(!isSunny);           // Outputs: false
Enter fullscreen mode Exit fullscreen mode

Conditional Statements (if, else-if, else)

Conditional statements are used to execute different blocks of code based on certain conditions.

int age = 20;

if (age >= 18) {
    System.out.println("You are an adult.");
} else if (age >= 13) {
    System.out.println("You are a teenager.");
} else {
    System.out.println("You are a child.");
}
Enter fullscreen mode Exit fullscreen mode

Ternary Operator

The ternary operator is a shorthand way of writing if-else statements.

int num = 10;
String result = (num % 2 == 0) ? "even" : "odd";
System.out.println(result);  // Outputs: even
Enter fullscreen mode Exit fullscreen mode

Switch Statements

Switch statements allow us to execute different blocks of code based on the value of a variable.

int day = 3;
String dayName;

switch (day) {
    case 1:
        dayName = "Monday";
        break;
    case 2:
        dayName = "Tuesday";
        break;
    // ...
    default:
        dayName = "Invalid day";
}

System.out.println("Today is " + dayName);
Enter fullscreen mode Exit fullscreen mode

Iterative Statements

Iterative statements are used to execute a block of code repeatedly.

For Loops
for (int i = 1; i <= 5; i++) {
    System.out.println("Count: " + i);
}
Enter fullscreen mode Exit fullscreen mode
Enhanced For Each Loop
int[] numbers = {1, 2, 3, 4, 5};

for (int num : numbers) {
    System.out.println(num);
}
Enter fullscreen mode Exit fullscreen mode
While Loops
int i = 1;
while (i <= 5) {
    System.out.println("Count: " + i);
    i++;
}
Enter fullscreen mode Exit fullscreen mode
Do-While Loops
int j = 1;
do {
    System.out.println("Count: " + j);
    j++;
} while (j <= 5);
Enter fullscreen mode Exit fullscreen mode

Exercise

Now after we are done with java fundamentals, lets take all what we've learned so far and package it a nice, simple and basic calculator

import java.util.Scanner;

public class Calculator {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("Welcome to the Simple Calculator!");
        System.out.println("Please enter the first number:");
        double num1 = scanner.nextDouble();

        System.out.println("Please enter the second number:");
        double num2 = scanner.nextDouble();

        System.out.println("Select operation:");
        System.out.println("1. Addition (+)");
        System.out.println("2. Subtraction (-)");
        System.out.println("3. Multiplication (*)");
        System.out.println("4. Division (/)");

        int choice = scanner.nextInt();
        double result = 0;

        switch (choice) {
            case 1:
                result = num1 + num2;
                break;
            case 2:
                result = num1 - num2;
                break;
            case 3:
                result = num1 * num2;
                break;
            case 4:
                if (num2 != 0) {
                    result = num1 / num2;
                } else {
                    System.out.println("Error! Division by zero is not allowed.");
                    return;
                }
                break;
            default:
                System.out.println("Invalid choice!");
                return;
        }

        System.out.println("Result: " + result);

        scanner.close();
    }
}
Enter fullscreen mode Exit fullscreen mode

Section 2 | Object Oriented Programming in java

Java is indeed a versatile programming language that supports multiple programming paradigms, including procedural, functional, and object-oriented programming (OOP). However, the OOP paradigm is the primary one in Java, as it focuses on modeling real-world entities as objects, which have attributes (data) and behaviors (methods).

Let's dive together into the different concepts and aspects of the OOP world in Java.

Absolutely! Let's break down each topic using simple language, analogies, and plenty of code examples.

Understanding Classes and Objects

Think of a class as a blueprint and an object as something built from that blueprint. For example, a class "Car" would define what a car is (its attributes like color, make, and model, and behaviors like driving and honking), while an object would be a specific car built from that blueprint.

Creating and Instantiating Classes

To create a class, you define its attributes and behaviors. To create an object, you use the class blueprint. Here's an example of a class and how to create an object from it:

// Class definition
class Car {
    String color;
    String make;
    String model;

    void drive() {
        System.out.println("The car is driving.");
    }
}

// Creating an object
Car myCar = new Car();
Enter fullscreen mode Exit fullscreen mode

Encapsulation

Encapsulation is like putting your things in a box and only allowing certain people to access them. In Java, you can hide the inner workings of a class and only allow access through specific methods, like getter and setter methods.

class BankAccount {
    private double balance;

    // Getter method
    public double getBalance() {
        return balance;
    }

    // Setter method
    public void setBalance(double amount) {
        balance = amount;
    }
}
Enter fullscreen mode Exit fullscreen mode

Abstraction

Abstraction is like driving a car without needing to know how the engine works. You interact with the car (object) using its interface (methods) without needing to understand its internal implementation.

Constructors

Constructors are like a special recipe used to create an object. They initialize the object's state when it's created. Here's an example:

class Dog {
    String breed;

    // Constructor
    public Dog(String dogBreed) {
        breed = dogBreed;
    }
}
Enter fullscreen mode Exit fullscreen mode

Polymorphism

Polymorphism is like a shape-shifter. An object can take on different forms depending on its context. Method overloading is one example:

class MathOperations {
    // Method overloading
    public int add(int x, int y) {
        return x + y;
    }

    public double add(double x, double y) {
        return x + y;
    }
}
Enter fullscreen mode Exit fullscreen mode

Inheritance

Inheritance is like passing down traits from parent to child. A subclass inherits attributes and behaviors from its superclass. For example:

class Animal {
    void eat() {
        System.out.println("The animal is eating.");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("The dog is barking.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Static Members

Static members are like shared resources among all objects of a class. They belong to the class itself rather than any specific object. For example:

class Circle {
    static final double PI = 3.14;

    static double calculateArea(double radius) {
        return PI * radius * radius;
    }
}
Enter fullscreen mode Exit fullscreen mode

Absolutely! Let's delve into each aspect of inheritance in Java with detailed explanations, code examples, and analogies.

Overview of Inheritance

Inheritance is like passing down traits from parents to their children. In Java, it allows a class (subclass) to inherit attributes and behaviors from another class (superclass). This promotes code reuse and helps in organizing and managing related classes.

The Object Class and Its Core Methods

In Java, every class implicitly extends the Object class. The Object class provides several core methods that are available to all classes. Some of these methods include:

  • toString(): Returns a string representation of the object.
  • equals(Object obj): Indicates whether some other object is "equal to" this one.
  • hashCode(): Returns a hash code value for the object.
class MyClass {
    int value;

    // toString method override
    @Override
    public String toString() {
        return "Value: " + value;
    }
}

MyClass obj = new MyClass();
obj.value = 10;
System.out.println(obj.toString());  // Outputs: Value: 10
Enter fullscreen mode Exit fullscreen mode

Object Comparison

Java provides the equals() method for comparing objects for equality. By default, it compares memory references. However, you can override this method in your classes to provide custom comparison logic.

class Person {
    String name;
    int age;

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || getClass() != obj.getClass())
            return false;
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }
}
Enter fullscreen mode Exit fullscreen mode

Method Overriding

Method overriding allows a subclass to provide a specific implementation of a method that is already defined in its superclass. This is useful for providing specialized behavior.

class Animal {
    void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Dog barks");
    }
}
Enter fullscreen mode Exit fullscreen mode

Abstract Classes

Abstract classes are like blueprints for other classes. They cannot be instantiated themselves but can have abstract methods that must be implemented by subclasses.

abstract class Shape {
    abstract double area();
}

class Circle extends Shape {
    double radius;

    @Override
    double area() {
        return Math.PI * radius * radius;
    }
}
Enter fullscreen mode Exit fullscreen mode

Final Classes

Final classes are like finished products that cannot be modified or extended. You can't create subclasses of a final class.

final class FinalClass {
    // Class definition
}

// Error: Cannot inherit from final class
class SubClass extends FinalClass {
}
Enter fullscreen mode Exit fullscreen mode

Interfaces in Java

Think of an interface as a contract or a set of rules that a class must follow. It defines a list of methods that implementing classes must provide, but it doesn't contain any implementation itself.

// Interface definition
interface Animal {
    void makeSound();
    void eat();
}

// Implementing class
class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }

    @Override
    public void eat() {
        System.out.println("Dog eats bones");
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Animal interface defines two methods: makeSound() and eat(). The Dog class implements these methods according to the contract specified by the Animal interface.

Key Features and Usage Scenarios

Interfaces provide several key features and are commonly used in various scenarios:

  • Multiple Inheritance: Unlike classes, a Java class can implement multiple interfaces. This allows a class to inherit behavior from multiple sources.
interface Flyable {
    void fly();
}

class Bird implements Animal, Flyable {
    // Implement methods from both Animal and Flyable interfaces
}
Enter fullscreen mode Exit fullscreen mode
  • Polymorphism: Interfaces enable polymorphic behavior, allowing objects to be treated as instances of their interface type. This promotes flexibility and code reusability.
void performAction(Animal animal) {
    animal.makeSound();
    animal.eat();
}
Enter fullscreen mode Exit fullscreen mode
  • Loose Coupling: Interfaces facilitate loose coupling between components of a system. Classes interact with each other through interfaces rather than concrete implementations, making the system more modular and easier to maintain.
interface DataAccess {
    void saveData(String data);
}

class DatabaseAccess implements DataAccess {
    @Override
    public void saveData(String data) {
        // Save data to the database
    }
}

class FileAccess implements DataAccess {
    @Override
    public void saveData(String data) {
        // Save data to a file
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, different classes can implement the DataAccess interface to provide different ways of saving data, such as to a database or a file.

Interfaces serve as powerful tools in Java programming, providing a way to define contracts, enable polymorphic behavior, and promote loose coupling between components. They are essential for building modular, flexible, and maintainable software systems.

Exercise

using all what we learned in this section about OOP in java let's rebuild our calculator in object oriented style:

// Interface for basic calculator operations
interface Calculator {
    double calculate(double num1, double num2);
}

// Addition operation
class Addition implements Calculator {
    @Override
    public double calculate(double num1, double num2) {
        return num1 + num2;
    }
}

// Subtraction operation
class Subtraction implements Calculator {
    @Override
    public double calculate(double num1, double num2) {
        return num1 - num2;
    }
}

// Multiplication operation
class Multiplication implements Calculator {
    @Override
    public double calculate(double num1, double num2) {
        return num1 * num2;
    }
}

// Division operation
class Division implements Calculator {
    @Override
    public double calculate(double num1, double num2) {
        if (num2 != 0) {
            return num1 / num2;
        } else {
            throw new IllegalArgumentException("Error! Division by zero is not allowed.");
        }
    }
}

// Calculator class encapsulating calculator operations
class BasicCalculator {
    private Calculator calculator;

    public BasicCalculator(Calculator calculator) {
        this.calculator = calculator;
    }

    public double performCalculation(double num1, double num2) {
        return calculator.calculate(num1, num2);
    }
}

public class CalculatorApp {
    public static void main(String[] args) {
        BasicCalculator calculator = new BasicCalculator(new Addition());
        double result = calculator.performCalculation(10, 5);
        System.out.println("Addition Result: " + result);

        calculator = new BasicCalculator(new Subtraction());
        result = calculator.performCalculation(10, 5);
        System.out.println("Subtraction Result: " + result);

        calculator = new BasicCalculator(new Multiplication());
        result = calculator.performCalculation(10, 5);
        System.out.println("Multiplication Result: " + result);

        calculator = new BasicCalculator(new Division());
        result = calculator.performCalculation(10, 5);
        System.out.println("Division Result: " + result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Section 3 | Advance Java Topics

Exception Handling?

Think of exceptions as unexpected events that can happen while your Java program is running. Just like when you're playing a game and suddenly something unexpected happens, like the power going out or your controller running out of batteries.

Checked Exceptions:

These are like warning signs that you can anticipate. It's like knowing that if you're playing with a ball indoors, it might break something. So, you handle it by playing more carefully. In Java, you're forced to handle these kinds of exceptions, either by catching them or declaring that your method might throw them.

Unchecked Exceptions:

These are the sneaky surprises you didn't see coming. It's like accidentally stepping on a Lego piece while walking barefoot. Ouch! These exceptions are not forced to be handled explicitly, but you should still try to handle them to keep your program safe.

Try-Catch Blocks:

Imagine you're catching a ball. You extend your hands, try to catch it, and if you miss, it falls on the ground. In Java, you use try to attempt something that might throw an exception, and then you catch the exception if it happens, just like catching a ball to prevent it from crashing on the ground.

try {
    // Code that might throw an exception
} catch (ExceptionType e) {
    // Code to handle the exception
}
Enter fullscreen mode Exit fullscreen mode

Finally Block:

This is like a safety net. No matter what happens, this block of code always runs. It's like cleaning up after playing with your toys. In Java, you use finally to ensure that some code gets executed, whether an exception occurs or not.

try {
    // Code that might throw an exception
} catch (ExceptionType e) {
    // Code to handle the exception
} finally {
    // Code that always runs
}
Enter fullscreen mode Exit fullscreen mode

Throw and Throws:

Sometimes you want to be the one causing the exception. It's like saying, "I'm not playing anymore, and I'm throwing the ball back." In Java, you use throw to throw an exception manually. And throws is like passing the responsibility to someone else. You declare that your method might throw a certain type of exception, leaving it to the caller to handle.

void myMethod() throws MyException {
    // Code that might throw MyException
}

// Somewhere else in the code
try {
    myMethod();
} catch (MyException e) {
    // Handle MyException
}
Enter fullscreen mode Exit fullscreen mode

Generics:

Generics in Java are like magical containers that can hold any type of data. They provide flexibility and type-safety to your code, ensuring that you can work with different types seamlessly. Let's dive deeper into the magical world of generics!

Generics in Classes:

Imagine a treasure chest that can hold various treasures – gold coins, precious gems, or ancient artifacts. In Java, you can create generic classes using <T>, where T represents a type parameter.

public class TreasureChest<T> {
    private T treasure;

    public void store(T item) {
        this.treasure = item;
    }

    public T retrieve() {
        return this.treasure;
    }
}
Enter fullscreen mode Exit fullscreen mode

Generics in Methods:

Now, envision a wand that can cast spells on any object. In Java, you can create generic methods that operate on different types of data, enhancing code reusability and flexibility.

public class Magic {
    public <T> void castSpell(T target) {
        // Perform magic on the target
    }
}
Enter fullscreen mode Exit fullscreen mode

Wildcards:

Wildcards in Java generics are like special lenses that allow you to view your code from different perspectives. Let's venture deeper into the jungle of generics and uncover the secrets of wildcards!

Upper Bounded Wildcards (<? extends T>):

Imagine a magical net that captures all creatures with specific characteristics. In Java, <? extends T> captures all objects of a type that is a subtype of T, allowing you to work with a broad range of data.

public void feedHerbivores(List<? extends Animal> herbivores) {
    // Feed the herbivores
}
Enter fullscreen mode Exit fullscreen mode

Lower Bounded Wildcards (<? super T>):

Now, picture a protective barrier that shields against specific dangers. In Java, <? super T> specifies a lower bound for the wildcard and captures all objects that are superclasses of T, ensuring safety and security in your code.

public void protectAgainstPredators(List<? super Lion> protections) {
    // Set up protections against predators
}
Enter fullscreen mode Exit fullscreen mode

Generic Methods:

Just as a skilled magician can perform various tricks with a single wand, generic methods in Java can operate on different types of data, enhancing code flexibility and versatility.

public <T> void performAction(T item) {
    // Perform action on the item
}
Enter fullscreen mode Exit fullscreen mode

Generic Classes with Multiple Type Parameters:

Imagine a treasure map with multiple clues leading to hidden riches. In Java, you can create generic classes with multiple type parameters, allowing you to handle various types of data simultaneously.

public class TreasureMap<X, Y> {
    private X clue1;
    private Y clue2;

    // Constructor, getters, and setters
}
Enter fullscreen mode Exit fullscreen mode

Collections:

Imagine you're on a quest to collect rare treasures scattered across a magical kingdom. Java collections are like your trusty backpacks, equipped with special compartments to store and organize your treasures. Whether it's shiny gems, ancient artifacts, or mystical potions, collections in Java provide a versatile way to manage and manipulate your data.

Collections are just Interfaces!:

In Java, collections are represented by a variety of interfaces, each with its unique characteristics and abilities. Think of interfaces as blueprints for different types of backpacks, each designed for specific purposes.

List<String> backpack = new ArrayList<>();  // A backpack for storing a list of treasures
Set<Integer> pouch = new HashSet<>();       // A pouch for storing unique treasures
Map<String, Integer> chest = new HashMap<>();// A chest for storing treasures with keys
Enter fullscreen mode Exit fullscreen mode

Collection Types:

Lists:

Imagine a list of treasures neatly arranged in a row, like books on a shelf. In Java, lists allow you to store a collection of elements in a specific order, making it easy to access and manipulate them.

List<String> backpack = new ArrayList<>();
backpack.add("Golden Sword");
backpack.add("Enchanted Ring");
backpack.add("Magic Potion");
Enter fullscreen mode Exit fullscreen mode
Sets:

Now picture a set of unique treasures, like a collection of rare gems. In Java, sets ensure that each element is unique, preventing duplicates and providing fast retrieval.

Set<String> pouch = new HashSet<>();
pouch.add("Ruby");
pouch.add("Emerald");
pouch.add("Sapphire");
Enter fullscreen mode Exit fullscreen mode
Maps:

Finally, envision a treasure map with clues leading to hidden riches. In Java, maps associate keys with values, allowing you to retrieve treasures based on their unique identifiers.

Map<String, Integer> chest = new HashMap<>();
chest.put("Gold Coins", 100);
chest.put("Silver Bars", 50);
chest.put("Diamonds", 10);
Enter fullscreen mode Exit fullscreen mode

Iterating Through Collections:

Just as you explore the vast landscapes of a kingdom, you can traverse through collections using iterators, uncovering treasures along the way.

for (String treasure : backpack) {
    System.out.println("Found: " + treasure);
}
Enter fullscreen mode Exit fullscreen mode

Sorting Collections:

Imagine arranging your treasures in order of value, from least to most precious. In Java, you can sort collections using comparators, ensuring that your treasures are organized efficiently.

Collections.sort(backpack);
Enter fullscreen mode Exit fullscreen mode

Manipulating Collections:

Now envision combining two collections to create an even larger collection of treasures. In Java, you can manipulate collections using various methods, such as merging, filtering, and transforming them.

List<String> additionalTreasures = new ArrayList<>();
additionalTreasures.add("Magic Wand");
backpack.addAll(additionalTreasures);
Enter fullscreen mode Exit fullscreen mode

Functional Programming:

Imagine you're a wizard in a magical kingdom, wielding powerful spells to manipulate the forces of nature. Functional programming in Java is like mastering the ancient art of spellcasting, allowing you to perform powerful transformations on your data with ease.

Lambda Expressions:

In Java, lambda expressions are like enchanted spells, allowing you to encapsulate behavior and pass functions as parameters to other methods.

// Traditional way of defining a Runnable
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello, world!");
    }
};

// Using lambda expression
Runnable runnable = () -> {
    System.out.println("Hello, world!");
};
Enter fullscreen mode Exit fullscreen mode

Suppliers:

Picture a mystical shopkeeper who can conjure any item you desire out of thin air. In Java, suppliers are like that shopkeeper, providing a way to lazily generate or supply values when needed.

Supplier<Integer> randomNumber = () -> (int) (Math.random() * 100);
System.out.println(randomNumber.get());  // Output: A random number between 0 and 100
Enter fullscreen mode Exit fullscreen mode

Predicates:

Imagine you're a wise wizard who can discern truth from falsehood with a single glance. In Java, predicates are like your magical eyes, allowing you to test conditions and filter elements based on those conditions.

Predicate<Integer> isEven = num -> num % 2 == 0;
System.out.println(isEven.test(4));  // Output: true
System.out.println(isEven.test(5));  // Output: false
Enter fullscreen mode Exit fullscreen mode

Consumers:

Now, envision a benevolent fairy who consumes your worries and leaves behind a sense of peace. In Java, consumers are like that fairy, accepting values and performing actions on them without returning anything.

Consumer<String> printUpperCase = str -> System.out.println(str.toUpperCase());
printUpperCase.accept("magic");  // Output: MAGIC
Enter fullscreen mode Exit fullscreen mode

Functions:

Finally, imagine a magical alchemist who transforms one substance into another. In Java, functions are like that alchemist, taking one input and producing another output based on some transformation.

Function<Integer, String> convertToString = num -> "Number: " + num;
System.out.println(convertToString.apply(42));  // Output: Number: 42
Enter fullscreen mode Exit fullscreen mode

Java Streams:

In Java, streams are sequences of elements that support various methods to perform operations on those elements. Think of them as magical conduits that flow through your data, allowing you to perform actions on each element as it passes through.

Functional Interfaces in Streams

Streams use functional interfaces to perform their work. These interfaces, such as consumers, predicates, suppliers, and functions, provide blueprints for behaviors that can be passed to stream operations.

Consumers in streams:

Consumers are like magical observers that accept a single input and perform some action on it without returning anything. In streams, consumers are used to perform terminal operations that consume the elements of the stream.

List<String> ingredients = Arrays.asList("Eye of newt", "Wing of bat", "Tooth of wolf");

// Printing each ingredient using a consumer
ingredients.stream()
           .forEach(System.out::println);
Enter fullscreen mode Exit fullscreen mode

Predicates in streams:

Predicates are like magical filters that test conditions and return true or false. In streams, predicates are used to filter elements based on certain criteria.

// Filtering ingredients that contain "bat"
ingredients.stream()
           .filter(ingredient -> ingredient.contains("bat"))
           .forEach(System.out::println);
Enter fullscreen mode Exit fullscreen mode

Suppliers in streams:

Suppliers are like magical providers that lazily generate or supply values when needed. In streams, suppliers are not as commonly used directly, but they play a role in certain stream operations.

// Generating a stream of random numbers using a supplier
Stream<Integer> randomNumberStream = Stream.generate(() -> (int) (Math.random() * 100));
Enter fullscreen mode Exit fullscreen mode

Functions in streams:

Functions are like magical transformers that take one input and produce another output based on some transformation. In streams, functions are used for mapping elements to another form.

// Converting ingredients to uppercase using a function
ingredients.stream()
           .map(String::toUpperCase)
           .forEach(System.out::println);
Enter fullscreen mode Exit fullscreen mode

Concurrency and Multithreading:

Concurrency is the ability of a program to execute multiple tasks simultaneously, allowing it to make progress on more than one task at a time.

What are Threads?

Threads are like individual sailors on your ship, each responsible for carrying out a specific task. In Java, you can create and manage threads to perform tasks concurrently.

Creating Threads:

In Java, you can create threads by extending the Thread class or implementing the Runnable interface. Let's create a simple thread that prints a message.

class MyThread extends Thread {
    public void run() {
        System.out.println("Hello from MyThread!");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // Start the thread
    }
}
Enter fullscreen mode Exit fullscreen mode

Implementing Runnable:

Alternatively, you can implement the Runnable interface and pass it to a Thread object.

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Hello from MyRunnable!");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start(); // Start the thread
    }
}
Enter fullscreen mode Exit fullscreen mode

Synchronization:

Imagine your sailors trying to access a shared resource, like a treasure chest, simultaneously. Synchronization in Java ensures that only one thread can access a critical section of code at a time, preventing conflicts and ensuring thread safety.

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}
Enter fullscreen mode Exit fullscreen mode

Executors:

Think of executors as managers who oversee a group of sailors (threads) and assign them tasks efficiently. Executors provide a higher-level abstraction for managing threads and executing tasks asynchronously.

ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(new MyTask());
executor.shutdown();
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this comprehensive guide, we've journeyed through the fundamental concepts of Java programming, explored the intricacies of object-oriented programming (OOP), and delved into advanced topics that unlock the full potential of Java. From setting up your development environment to mastering control flow, classes, objects, inheritance, interfaces, and beyond, you've gained a solid understanding of Java's core principles and features.

As we conclude our exploration of Java 101, it's clear that you've taken significant strides on your path to becoming a proficient Java programmer. Armed with this knowledge, you're well-equipped to tackle real-world coding challenges and embark on exciting projects with confidence.

But the journey doesn't end here. In our next adventure, we'll delve into the fascinating world of data structures and algorithms—the cornerstone of computer science and software development. Together, we'll unravel the mysteries of data organization, algorithmic efficiency, and problem-solving techniques that will elevate your coding skills to new heights.

So, stay tuned for our next chapter as we continue our quest for mastery in the realms of programming and software development. Until then, keep coding, keep learning, and let your passion for Java ignite your journey towards excellence!

Top comments (1)

Collapse
 
davidpro profile image
David

Very interesting, Its a perfect article to read for someone who is interested to dive into java and only needs a small push to do so, this is the perfect push!