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 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
}
}
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());
}
}
}
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());
}
}
}
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 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());
}
}
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());
}
}
}
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);
}
}
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 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());
}
}
catch
block.null
values before accessing object methods or fields.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.