Update: If you're looking for a real world use case on the following material, check out how to read a CSV file in Java.
FileReader, BufferedReader, and Scanner are all classes for handling I/O in Java.
To understand what makes them different, its important to look at the difference between byte streams, character streams, and buffered streams.
A byte stream reads bytes of data from an input such as a text or audio file. Byte streams descend from InputStream or OutputStream.
Byte streams are generally too low level to use in isolation. Character streams are wrappers for byte streams.
Character streams use byte streams to handle I/O with the added benefit of character encoding.
Character streams descend from Reader or Writer. FileReader and FileWriter are examples of character streams.
Byte streams and character streams are unbuffered I/O. This means they rely on the underlying OS to read/write a byte at a time. This is inefficient because reading/writing a single byte from disk is a lot slower than using memory.
Buffered streams read/write x number of bytes at a time to a memory area called a buffer. Only when the buffer is empty do buffered streams utilize the native input API.
BufferedReader and BufferedWriter are examples of buffered character streams. BufferedInputStream and BufferedOutputStream are examples of buffered byte streams.
FileReader is an unbuffered character stream. This means it translates bytes from their internal character set to the local character set one byte at a time.
BufferedReader is a buffered stream. A FileReader can be wrapped in a BufferedReader for more efficient reading of a file.
BufferedReader reader = new BufferedReader(new FileReader("data.txt")); reader.readLine()
It's generally a good idea to wrap a FileReader in a BufferedReader for improved performance.
Unlike a FileReader, a BufferedReader can read a line at a time via readLine().
An InputStreamReader reads bytes from a byte stream and converts those bytes into characters. InputStreamReader allows you to create character streams that specify encodings like UTF-8, etc.
A FileReader is a type of InputStreamReader. We already saw how a BufferedReader can be used as a wrapper for an InputStreamReader for more efficient streaming.
FileInputStream extends a regular InputStream. Remember that an InputStream is a byte stream. A BufferedReader takes an InputStreamReader which is a character stream.
A Scanner performs parsing of input data using regular expressions. A BufferedReader reads a sequence of characters.
A BufferedReader is synchronous while a Scanner is not.
A BufferedReader has a larger buffer memory (1KB char buffer vs 8KB byte buffer)
A BufferedReader can be used as input to a Scanner...
Scanner s = new Scanner(new BufferedReader(new FileReader("data.txt")));
Remember that these classes can work together to more efficiently handle I/O in Java. A BufferedReader is often used to wrap a InputStreamReader which takes an InputStream as input.
In other words, buffered streams can be used to wrap character streams which wrap byte streams to handle i/o.
Be sure to check out How to read a CSV in Java for real world use cases and examples.