BLOG POSTS
Java Read Text File Tutorial

Java Read Text File Tutorial

Reading text files in Java is one of those fundamental tasks that every developer runs into sooner or later, especially when you’re dealing with server configurations, log files, or data processing scripts. Whether you’re parsing nginx access logs, reading configuration files on your production server, or processing batch data uploads, knowing how to efficiently read text files in Java can save you countless hours and headaches. This guide will walk you through various methods to read text files in Java, from simple one-liners to robust enterprise-ready solutions that handle edge cases gracefully. We’ll cover everything from basic file I/O to modern NIO.2 approaches, with real-world examples you can actually use in your server management scripts.

How Does Java File Reading Actually Work?

Under the hood, Java provides several layers of abstraction for file operations. At the lowest level, you’ve got the traditional I/O streams that have been around since Java 1.0, and then there’s the newer NIO (New I/O) and NIO.2 packages that offer more efficient and flexible approaches.

The classic approach uses FileInputStream and BufferedReader classes. When you create a FileInputStream, Java opens a file descriptor at the OS level and creates a stream of bytes. The BufferedReader wraps this stream and adds buffering (typically 8192 bytes by default), which dramatically improves performance by reducing system calls.

The newer NIO.2 approach (introduced in Java 7) uses the Files class and Path objects. This is generally more efficient and provides better error handling. Here’s why:

  • Memory mapping: NIO.2 can map files directly into memory for ultra-fast access
  • Non-blocking I/O: Better for concurrent operations
  • Better exception handling: More specific exceptions help with debugging
  • Atomic operations: Safer for concurrent file access scenarios

Performance-wise, here’s what you can expect on a typical server setup:

Method Small Files (<1MB) Large Files (>100MB) Memory Usage Best For
Files.readAllLines() ~2ms High memory usage Loads entire file Small config files
BufferedReader ~3ms ~200ms Low, streaming Log processing
Scanner ~5ms ~400ms Medium Parsing structured data
Files.lines() ~2ms ~150ms Very low, lazy Stream processing

Step-by-Step Setup and Implementation

Let’s dive into the practical stuff. I’ll show you several approaches, starting with the most commonly used ones.

Method 1: Files.readAllLines() – The Quick and Dirty Approach

This is probably the easiest method if you’re dealing with relatively small files (think configuration files, small logs, etc.):

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.List;

public class FileReaderExample {
    public static void readSmallFile() {
        try {
            Path filePath = Paths.get("/var/log/application.log");
            List<String> lines = Files.readAllLines(filePath);
            
            for (String line : lines) {
                System.out.println(line);
            }
            
            // Or use streams for more advanced processing
            lines.stream()
                 .filter(line -> line.contains("ERROR"))
                 .forEach(System.out::println);
                 
        } catch (IOException e) {
            System.err.println("Failed to read file: " + e.getMessage());
        }
    }
}

Pros: Simple, clean code. Great for small files.
Cons: Loads entire file into memory. Will crash with OutOfMemoryError on large files.

Method 2: BufferedReader – The Reliable Workhorse

This is your go-to method for most scenarios, especially when processing server logs or data files:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class BufferedReaderExample {
    
    // Modern approach with try-with-resources
    public static void readWithBufferedReader(String filePath) {
        try (BufferedReader reader = Files.newBufferedReader(Paths.get(filePath))) {
            String line;
            int lineNumber = 0;
            
            while ((line = reader.readLine()) != null) {
                lineNumber++;
                
                // Process line here - example: find error logs
                if (line.toLowerCase().contains("error")) {
                    System.out.printf("Line %d: %s%n", lineNumber, line);
                }
                
                // Add rate limiting for large files
                if (lineNumber % 10000 == 0) {
                    System.out.printf("Processed %d lines...%n", lineNumber);
                }
            }
            
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
        }
    }
    
    // Legacy approach (still works, but less clean)
    public static void readWithLegacyBufferedReader(String filePath) {
        try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
}

Method 3: Files.lines() – The Stream Processing Champion

This is the most modern and efficient approach, especially powerful when combined with Java 8+ stream operations:

import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.stream.Stream;

public class StreamFileReader {
    
    public static void readWithStreams(String filePath) {
        try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
            
            // Example 1: Count error occurrences
            long errorCount = lines
                .filter(line -> line.contains("ERROR"))
                .count();
            System.out.println("Found " + errorCount + " errors");
            
        } catch (IOException e) {
            System.err.println("Failed to read file: " + e.getMessage());
        }
        
        // Example 2: More complex processing
        try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
            lines
                .filter(line -> !line.trim().isEmpty()) // Skip empty lines
                .filter(line -> line.startsWith("[")) // Only log entries
                .map(String::trim)
                .limit(100) // Process only first 100 matching lines
                .forEach(System.out::println);
                
        } catch (IOException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
    
    // Advanced example: Parse nginx access logs
    public static void parseNginxLogs(String logPath) {
        try (Stream<String> lines = Files.lines(Paths.get(logPath))) {
            lines
                .filter(line -> line.contains(" 404 "))
                .map(line -> {
                    String[] parts = line.split(" ");
                    return parts[0] + " - " + parts[6]; // IP and requested URL
                })
                .distinct()
                .forEach(System.out::println);
                
        } catch (IOException e) {
            System.err.println("Error processing nginx logs: " + e.getMessage());
        }
    }
}

Method 4: Scanner – The Swiss Army Knife

Scanner is particularly useful when you need to parse structured data or when you need more control over how the input is tokenized:

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class ScannerFileReader {
    
    public static void readWithScanner(String filePath) {
        try (Scanner scanner = new Scanner(new File(filePath))) {
            
            // Read line by line
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine();
                System.out.println(line);
            }
            
        } catch (FileNotFoundException e) {
            System.err.println("File not found: " + filePath);
        }
    }
    
    // Parse structured data (e.g., CSV-like files)
    public static void parseStructuredData(String filePath) {
        try (Scanner scanner = new Scanner(new File(filePath))) {
            scanner.useDelimiter(",|\\n"); // Split on commas or newlines
            
            while (scanner.hasNext()) {
                String token = scanner.next().trim();
                System.out.println("Token: " + token);
            }
            
        } catch (FileNotFoundException e) {
            System.err.println("File not found: " + filePath);
        }
    }
    
    // Read numbers from a file
    public static void readNumbers(String filePath) {
        try (Scanner scanner = new Scanner(new File(filePath))) {
            
            while (scanner.hasNextDouble()) {
                double number = scanner.nextDouble();
                System.out.println("Number: " + number);
            }
            
        } catch (FileNotFoundException e) {
            System.err.println("File not found: " + filePath);
        }
    }
}

Real-World Examples and Use Cases

Let’s look at some practical scenarios you might encounter when managing servers or processing data.

Use Case 1: Log File Analysis

Here’s a complete example that processes Apache/Nginx access logs to find the most frequently accessed pages:

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.regex.Pattern;

public class LogAnalyzer {
    
    // Pattern for common log format: IP - - [timestamp] "GET /path HTTP/1.1" status size
    private static final Pattern LOG_PATTERN = Pattern.compile(".*?\"\\w+ (.*?) HTTP.*?\" (\\d+) .*");
    
    public static void analyzeAccessLogs(String logPath) {
        try (Stream<String> lines = Files.lines(Paths.get(logPath))) {
            
            Map<String, Long> urlCounts = lines
                .filter(line -> line.contains("GET"))
                .map(LOG_PATTERN::matcher)
                .filter(matcher -> matcher.matches())
                .map(matcher -> matcher.group(1)) // Extract URL path
                .collect(Collectors.groupingBy(
                    url -> url,
                    Collectors.counting()
                ));
            
            // Print top 10 most accessed URLs
            urlCounts.entrySet().stream()
                .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
                .limit(10)
                .forEach(entry -> 
                    System.out.printf("%s: %d requests%n", entry.getKey(), entry.getValue()));
                    
        } catch (Exception e) {
            System.err.println("Error analyzing logs: " + e.getMessage());
        }
    }
    
    // Find 404 errors
    public static void find404Errors(String logPath) {
        try (Stream<String> lines = Files.lines(Paths.get(logPath))) {
            
            lines
                .filter(line -> line.contains(" 404 "))
                .limit(50) // Don't spam the output
                .forEach(System.out::println);
                
        } catch (Exception e) {
            System.err.println("Error finding 404s: " + e.getMessage());
        }
    }
}

Use Case 2: Configuration File Processing

Processing configuration files is a bread-and-butter task for server management:

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

public class ConfigProcessor {
    
    // Parse key=value configuration files
    public static Map<String, String> loadConfig(String configPath) {
        Map<String, String> config = new HashMap<>();
        
        try (Stream<String> lines = Files.lines(Paths.get(configPath))) {
            
            lines
                .filter(line -> !line.trim().isEmpty()) // Skip empty lines
                .filter(line -> !line.trim().startsWith("#")) // Skip comments
                .filter(line -> line.contains("=")) // Only key=value pairs
                .forEach(line -> {
                    String[] parts = line.split("=", 2);
                    if (parts.length == 2) {
                        config.put(parts[0].trim(), parts[1].trim());
                    }
                });
                
        } catch (Exception e) {
            System.err.println("Error loading config: " + e.getMessage());
        }
        
        return config;
    }
    
    // Validate database connection settings
    public static boolean validateDbConfig(String configPath) {
        Map<String, String> config = loadConfig(configPath);
        
        String[] requiredKeys = {"db.host", "db.port", "db.name", "db.user", "db.password"};
        
        for (String key : requiredKeys) {
            if (!config.containsKey(key) || config.get(key).isEmpty()) {
                System.err.println("Missing or empty config key: " + key);
                return false;
            }
        }
        
        // Validate port is numeric
        try {
            Integer.parseInt(config.get("db.port"));
        } catch (NumberFormatException e) {
            System.err.println("Invalid port number: " + config.get("db.port"));
            return false;
        }
        
        return true;
    }
}

Use Case 3: CSV Data Processing

Here’s how to handle CSV files efficiently:

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class CsvProcessor {
    
    public static void processCsvFile(String csvPath) {
        try (Stream<String> lines = Files.lines(Paths.get(csvPath))) {
            
            lines
                .skip(1) // Skip header row
                .map(line -> line.split(","))
                .filter(columns -> columns.length >= 3) // Ensure minimum columns
                .forEach(columns -> {
                    // Process each row
                    System.out.printf("ID: %s, Name: %s, Value: %s%n", 
                        columns[0].trim(), 
                        columns[1].trim(), 
                        columns[2].trim());
                });
                
        } catch (Exception e) {
            System.err.println("Error processing CSV: " + e.getMessage());
        }
    }
    
    // Handle CSV with quoted fields and commas inside quotes
    public static void processComplexCsv(String csvPath) {
        try (Stream<String> lines = Files.lines(Paths.get(csvPath))) {
            
            lines
                .skip(1)
                .map(CsvProcessor::parseCsvLine)
                .filter(fields -> !fields.isEmpty())
                .forEach(fields -> {
                    System.out.println("Fields: " + fields);
                });
                
        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
    
    // Simple CSV parser that handles quoted fields
    private static List<String> parseCsvLine(String line) {
        // This is a simplified parser - for production use a library like OpenCSV
        return Arrays.asList(line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"));
    }
}

Error Handling and Edge Cases

Here’s a robust file reader that handles common edge cases:

import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.util.stream.Stream;

public class RobustFileReader {
    
    public static void safeFileRead(String filePath) {
        Path path = Paths.get(filePath);
        
        // Check if file exists
        if (!Files.exists(path)) {
            System.err.println("File does not exist: " + filePath);
            return;
        }
        
        // Check if it's actually a file (not a directory)
        if (!Files.isRegularFile(path)) {
            System.err.println("Path is not a regular file: " + filePath);
            return;
        }
        
        // Check if file is readable
        if (!Files.isReadable(path)) {
            System.err.println("File is not readable: " + filePath);
            return;
        }
        
        // Check file size before loading into memory
        try {
            long fileSize = Files.size(path);
            if (fileSize > 100_000_000) { // 100MB limit
                System.err.println("File too large for in-memory processing: " + fileSize + " bytes");
                processLargeFile(path);
                return;
            }
        } catch (IOException e) {
            System.err.println("Cannot determine file size: " + e.getMessage());
            return;
        }
        
        // Process the file with explicit charset
        try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
            
            lines.forEach(System.out::println);
            
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
        } catch (OutOfMemoryError e) {
            System.err.println("File too large for available memory");
            processLargeFile(path);
        }
    }
    
    private static void processLargeFile(Path path) {
        System.out.println("Processing large file in chunks...");
        
        try (Stream<String> lines = Files.lines(path)) {
            
            lines
                .limit(1000) // Process in batches
                .forEach(System.out::println);
                
        } catch (IOException e) {
            System.err.println("Error processing large file: " + e.getMessage());
        }
    }
    
    // Handle different encodings
    public static void readWithEncoding(String filePath, String encoding) {
        try {
            Path path = Paths.get(filePath);
            
            // Try to detect charset or use specified one
            java.nio.charset.Charset charset = java.nio.charset.Charset.forName(encoding);
            
            try (Stream<String> lines = Files.lines(path, charset)) {
                lines.forEach(System.out::println);
            }
            
        } catch (java.nio.charset.UnsupportedCharsetException e) {
            System.err.println("Unsupported encoding: " + encoding);
        } catch (IOException e) {
            System.err.println("IO Error: " + e.getMessage());
        }
    }
}

Performance Comparison Example

Here’s a benchmarking utility to compare different methods:

import java.nio.file.*;
import java.io.*;
import java.util.Scanner;
import java.util.stream.Stream;

public class FileReadingBenchmark {
    
    public static void benchmarkMethods(String filePath) {
        System.out.println("Benchmarking file reading methods for: " + filePath);
        
        // Method 1: Files.readAllLines()
        long start = System.currentTimeMillis();
        try {
            Files.readAllLines(Paths.get(filePath));
            long end = System.currentTimeMillis();
            System.out.println("Files.readAllLines(): " + (end - start) + "ms");
        } catch (Exception e) {
            System.out.println("Files.readAllLines() failed: " + e.getMessage());
        }
        
        // Method 2: BufferedReader
        start = System.currentTimeMillis();
        try (BufferedReader reader = Files.newBufferedReader(Paths.get(filePath))) {
            while (reader.readLine() != null) {
                // Just read, don't process
            }
            long end = System.currentTimeMillis();
            System.out.println("BufferedReader: " + (end - start) + "ms");
        } catch (Exception e) {
            System.out.println("BufferedReader failed: " + e.getMessage());
        }
        
        // Method 3: Files.lines()
        start = System.currentTimeMillis();
        try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
            lines.count(); // Force evaluation
            long end = System.currentTimeMillis();
            System.out.println("Files.lines(): " + (end - start) + "ms");
        } catch (Exception e) {
            System.out.println("Files.lines() failed: " + e.getMessage());
        }
        
        // Method 4: Scanner
        start = System.currentTimeMillis();
        try (Scanner scanner = new Scanner(new File(filePath))) {
            while (scanner.hasNextLine()) {
                scanner.nextLine();
            }
            long end = System.currentTimeMillis();
            System.out.println("Scanner: " + (end - start) + "ms");
        } catch (Exception e) {
            System.out.println("Scanner failed: " + e.getMessage());
        }
    }
}

Related Tools and Integration Possibilities

Java file reading becomes even more powerful when combined with other tools and libraries:

  • Apache Commons IO: Provides utilities like FileUtils.readLines() and IOUtils.toString()
  • Google Guava: Offers Files.readLines() and CharStreams.toString()
  • Jackson: Perfect for reading JSON configuration files
  • OpenCSV: Industry-standard CSV parsing library
  • Log4j/Logback: For structured log file processing

Here’s an interesting integration example with system monitoring:

import java.nio.file.*;
import java.time.LocalDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class LogMonitor {
    
    private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    
    public static void startLogMonitoring(String logPath) {
        scheduler.scheduleAtFixedRate(() -> {
            try {
                long errorCount = Files.lines(Paths.get(logPath))
                    .filter(line -> line.contains("ERROR"))
                    .count();
                
                if (errorCount > 10) {
                    System.out.printf("[%s] WARNING: %d errors found in log%n", 
                        LocalDateTime.now(), errorCount);
                    // Could send email, webhook, etc.
                }
                
            } catch (Exception e) {
                System.err.println("Monitoring error: " + e.getMessage());
            }
        }, 0, 60, TimeUnit.SECONDS); // Check every minute
    }
}

For server deployments, you might want to consider containerized solutions. If you’re looking to set up a proper development or production environment, check out these hosting options: VPS hosting for smaller applications or dedicated servers for high-performance requirements.

Automation and Scripting Possibilities

File reading in Java opens up tons of automation possibilities. Here are some creative uses:

  • Automated log rotation: Read, process, and archive log files based on size or age
  • Configuration hot-reloading: Monitor config files for changes and update application settings
  • Data ETL pipelines: Extract data from various text formats and transform for databases
  • Server health monitoring: Parse system logs to detect anomalies
  • Automated report generation: Process data files to generate summary reports

Here’s a practical example of a file watcher that automatically processes new files:

import java.nio.file.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class FileWatcher {
    
    public static void watchDirectory(String dirPath) throws IOException, InterruptedException {
        Path dir = Paths.get(dirPath);
        WatchService watcher = FileSystems.getDefault().newWatchService();
        
        dir.register(watcher, 
            StandardWatchEventKinds.ENTRY_CREATE,
            StandardWatchEventKinds.ENTRY_MODIFY);
        
        System.out.println("Watching directory: " + dirPath);
        
        while (true) {
            WatchKey key = watcher.poll(10, TimeUnit.SECONDS);
            if (key == null) continue;
            
            for (WatchEvent<?> event : key.pollEvents()) {
                WatchEvent.Kind<?> kind = event.kind();
                Path filename = (Path) event.context();
                
                if (filename.toString().endsWith(".log")) {
                    System.out.println("Processing new log file: " + filename);
                    processLogFile(dir.resolve(filename));
                }
            }
            
            if (!key.reset()) break;
        }
    }
    
    private static void processLogFile(Path logFile) {
        try (var lines = Files.lines(logFile)) {
            long errorCount = lines
                .filter(line -> line.contains("ERROR"))
                .count();
            
            System.out.println("Found " + errorCount + " errors in " + logFile.getFileName());
            
        } catch (IOException e) {
            System.err.println("Error processing " + logFile + ": " + e.getMessage());
        }
    }
}

Performance Tips and Best Practices

Based on extensive testing and real-world usage, here are some performance tips:

  • Use appropriate buffer sizes: Default 8KB is good for most cases, but 64KB can be better for large files
  • Prefer Files.lines() for large files: It’s lazy and memory-efficient
  • Always use try-with-resources: Prevents resource leaks
  • Consider parallel processing: Use .parallel() on streams for CPU-intensive operations
  • Handle encoding explicitly: Always specify charset to avoid platform-dependent behavior
// Optimized large file processing
public static void processLargeFileOptimized(String filePath) {
    try (Stream<String> lines = Files.lines(Paths.get(filePath), StandardCharsets.UTF_8)) {
        
        lines
            .parallel() // Enable parallel processing
            .filter(line -> !line.trim().isEmpty())
            .filter(line -> line.contains("ERROR"))
            .map(String::toUpperCase)
            .forEach(System.out::println);
            
    } catch (IOException e) {
        System.err.println("Error: " + e.getMessage());
    }
}

Conclusion and Recommendations

After working with Java file I/O for years in production environments, here’s my take on when to use each approach:

Use Files.readAllLines() when:

  • Files are small (<10MB)
  • You need to access lines randomly or multiple times
  • Simplicity is more important than memory efficiency

Use BufferedReader when:

  • You’re dealing with legacy code or Java versions < 8
  • You need line-by-line processing with complex logic
  • Memory usage is a concern

Use Files.lines() when:

  • Files are large or size is unknown
  • You want to leverage Stream API features
  • Performance and memory efficiency are priorities

Use Scanner when:

  • You need to parse structured data
  • Input format is complex (mixed types, delimiters)
  • You’re prototyping or learning

For production server environments, I recommend sticking with Files.lines() as your default choice. It’s modern, efficient, and handles most edge cases gracefully. Always include proper error handling and consider the file size before choosing your approach.

Remember that file I/O is often a bottleneck in server applications, so profiling your specific use case is crucial. The methods shown here should handle most scenarios you’ll encounter when processing configuration files, logs, or data files on your servers.

If you’re planning to deploy Java applications that do heavy file processing, consider the infrastructure requirements carefully. For development and testing, a VPS solution might be sufficient, but for production workloads with large files, you might need dedicated server resources to ensure consistent performance.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

Your email address will not be published. Required fields are marked