Going Beyond Basics: An Intermediate Java Tutorial

Java is one of the most popular and versatile programming languages in the world, known for its platform - independence, object - orientation, and robust standard library. While beginners usually start with basic concepts like variables, data types, control structures, and simple class definitions, there’s a vast world of more advanced features waiting to be explored. This intermediate Java tutorial aims to take you beyond the basics and introduce you to concepts that will make your Java programming skills more refined and powerful.

Table of Contents

  1. [Advanced Class Features](#advanced - class - features)
  2. [Exception Handling Enhancements](#exception - handling - enhancements)
  3. [Multithreading in Java](#multithreading - in - java)
  4. [Java Collections Framework - Advanced Usage](#java - collections - framework - advanced - usage)
  5. [Java Generics](#java - generics)
  6. [Best Practices and Common Pitfalls](#best - practices - and - common - pitfalls)
  7. Conclusion
  8. References

Advanced Class Features

Inner Classes

Inner classes are classes defined inside other classes. They can access the members (even private ones) of the outer class. There are four types of inner classes: member inner classes, static inner classes, local inner classes, and anonymous inner classes.

// Outer class
class Outer {
    private int outerData = 10;

    // Member inner class
    class Inner {
        void display() {
            System.out.println("Outer data: " + outerData);
        }
    }
}

public class InnerClassExample {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.display();
    }
}

Method Overriding and Method Hiding

Method overriding occurs when a subclass provides a specific implementation of a method that is already defined in its superclass. Method hiding is similar but for static methods.

class Parent {
    static void staticMethod() {
        System.out.println("Parent static method");
    }

    void instanceMethod() {
        System.out.println("Parent instance method");
    }
}

class Child extends Parent {
    static void staticMethod() {
        System.out.println("Child static method");
    }

    @Override
    void instanceMethod() {
        System.out.println("Child instance method");
    }
}

public class OverrideAndHideExample {
    public static void main(String[] args) {
        Parent parent = new Child();
        parent.instanceMethod(); // Calls child's instance method
        Parent.staticMethod();   // Calls parent's static method
    }
}

Exception Handling Enhancements

Multiple Exception Handling

In Java 7 and later, you can handle multiple exceptions in a single catch block.

public class MultipleExceptionExample {
    public static void main(String[] args) {
        try {
            int[] arr = {1, 2, 3};
            System.out.println(arr[10]);
        } catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
            System.out.println("Exception caught: " + e.getClass().getName());
        }
    }
}

Try - with - Resources

The try - with - resources statement simplifies the management of resources that implement the AutoCloseable interface.

import java.io.FileInputStream;
import java.io.IOException;

public class TryWithResourcesExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("test.txt")) {
            // Use the file input stream
        } catch (IOException e) {
            System.out.println("IOException: " + e.getMessage());
        }
    }
}

Multithreading in Java

Creating Threads

There are two main ways to create threads in Java: by extending the Thread class or by implementing the Runnable interface.

// Extending Thread class
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread created by extending Thread class");
    }
}

// Implementing Runnable interface
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread created by implementing Runnable interface");
    }
}

public class ThreadCreationExample {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        thread1.start();

        Thread thread2 = new Thread(new MyRunnable());
        thread2.start();
    }
}

Synchronization

Synchronization is used to control access to shared resources in a multithreaded environment.

class Counter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

public class SynchronizationExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Count: " + counter.getCount());
    }
}

Java Collections Framework - Advanced Usage

Using Iterators

Iterators are used to traverse collections.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class IteratorExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

Sorting Custom Objects

You can sort custom objects by implementing the Comparable or Comparator interface.

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    @Override
    public int compareTo(Person other) {
        return this.age - other.age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

public class SortingCustomObjectsExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 25));
        people.add(new Person("Bob", 20));
        people.add(new Person("Charlie", 30));

        // Sort using Comparable
        Collections.sort(people);
        System.out.println("Sorted by age using Comparable: " + people);

        // Sort using Comparator
        Comparator<Person> nameComparator = Comparator.comparing(p -> p.toString().split("'")[1]);
        Collections.sort(people, nameComparator);
        System.out.println("Sorted by name using Comparator: " + people);
    }
}

Java Generics

Generic Classes

Generic classes allow you to create classes that can work with different data types.

class GenericClass<T> {
    private T data;

    public GenericClass(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }
}

public class GenericClassExample {
    public static void main(String[] args) {
        GenericClass<Integer> intObj = new GenericClass<>(10);
        System.out.println("Integer data: " + intObj.getData());

        GenericClass<String> stringObj = new GenericClass<>("Hello");
        System.out.println("String data: " + stringObj.getData());
    }
}

Bounded Type Parameters

Bounded type parameters restrict the types that can be used as type arguments.

class GenericNumber<T extends Number> {
    private T number;

    public GenericNumber(T number) {
        this.number = number;
    }

    public double getDoubleValue() {
        return number.doubleValue();
    }
}

public class BoundedTypeExample {
    public static void main(String[] args) {
        GenericNumber<Integer> intNumber = new GenericNumber<>(10);
        System.out.println("Double value of integer: " + intNumber.getDoubleValue());
    }
}

Best Practices and Common Pitfalls

Best Practices

  • Code Readability: Use meaningful variable and method names. Add comments to explain complex logic.
  • Resource Management: Always use try - with - resources for resource management to avoid resource leaks.
  • Exception Handling: Catch specific exceptions instead of using a broad catch block.
  • Multithreading: Use synchronization carefully to avoid deadlocks and race conditions.

Common Pitfalls

  • Null Pointer Exceptions: Always check for null values before accessing object methods or fields.
  • Class Loading Issues: Be aware of classpath issues when working with external libraries.
  • Incorrect Synchronization: Over - or under - synchronization can lead to performance issues or incorrect behavior.

Conclusion

In this intermediate Java tutorial, we’ve explored a variety of advanced concepts that go beyond the basics of Java programming. From advanced class features and exception handling enhancements to multithreading, collections framework usage, and Java generics, these concepts will help you write more efficient, robust, and maintainable Java code. By following the best practices and being aware of common pitfalls, you can take your Java programming skills to the next level.

References