Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java: The elegant way to add a line number to a file with lambda

I get used to using lambda to parse file line by line(much neater than bufferedReader.readLine()) for a long time. But today I faced a problem: add a line number to each line.

It need a counter, but the variable in lambda should be effectively final. Finally, I hacked it with an int array.

Code:

public static void main(String[] args) {
    int[] counter = new int[1];
    counter[0] = 0;
    try (Stream<String> lines = Files.lines(Paths.get("/tmp/timeline.txt"), Charset.defaultCharset())) {
        lines.limit(10).forEachOrdered(line -> {
            line = line.trim();
            counter[0] ++;
            System.out.println("Line " + counter[0] + ": " + line);
        });
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Output:

Line 1: p 5714026 wEkQ
Line 2: v 8235889
Line 3: v 6534726
...

My question is, how to avoid my hack and solve that problem elegantly?

like image 562
Sayakiss Avatar asked Apr 07 '16 14:04

Sayakiss


Video Answer


3 Answers

There is no elegant functional solution to a non-functional task. The first you may consider, is just resorting to an ordinary anonymous inner class:

String path = "/tmp/timeline.txt";
try(Stream<String> lines = Files.lines(Paths.get(path), Charset.defaultCharset())) {
    lines.limit(10).forEachOrdered(new Consumer<String>() {
        int counter = 0;
        public void accept(String line) {
            System.out.println("Line " + counter++ + ": " + line.trim());
        }
    });
} catch (IOException e) {
    e.printStackTrace();
}

The advantage is that it doesn’t pretend to be functional where it isn’t and the scope of the counter variable has the smallest scope needed for this task.


If you are going to do more than just-printing these numbered lines and need a solution compatible with all stream operations, re-implementing the stream source is a straight-forward solution:

static Stream<String> numberedLines(Path path, Charset cs) throws IOException {
    BufferedReader br = Files.newBufferedReader(path, cs);
    return StreamSupport.stream(new Spliterators.AbstractSpliterator<String>(
            Long.MAX_VALUE, Spliterator.ORDERED|Spliterator.NONNULL) {
        int counter;
        public boolean tryAdvance(Consumer<? super String> action) {
            String line;
            try {
                line = br.readLine();
                if(line==null) return false;
                action.accept("Line " + counter++ + ": " + line.trim());
                return true;
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }
    }, true).onClose(()->{ try { br.close(); }
        catch (IOException ex) { throw new UncheckedIOException(ex); }
    });
}

Of course, this isn’t as simple as a single lambda expression but using this reusable method, you can use all stream operations without problems, e.g.

String path = "/tmp/timeline.txt";
try(Stream<String> lines = numberedLines(Paths.get(path), Charset.defaultCharset())) {
    lines.skip(10).limit(10).forEachOrdered(System.out::println);
} catch(IOException e) {
    e.printStackTrace();
}
like image 71
Holger Avatar answered Nov 15 '22 00:11

Holger


You might use an AtomicInteger1 like

AtomicInteger ai = new AtomicInteger();
// ...
lines.limit(10).forEachOrdered(line -> {
    System.out.printf("Line %d: %s%n", ai.incrementAndGet(), line.trim());
});

1And I would prefer formatted IO with printf to using String concatenation.

like image 37
Elliott Frisch Avatar answered Nov 15 '22 00:11

Elliott Frisch


I would implement a Function to number the lines :

public static class LineNumberer implements Function<String,String> {
    private int lineCount;
    public lineNumberer() { lineCount = 0; }
    public String apply(String in) {
        return String.format("%d %s", lineCount++, in);
    }
}


public static void main (String[] args) throws java.lang.Exception
{
    Files.lines(Paths.get("/tmp/timeline.txt")).map(new LineNumberer()).forEach(System.out::println);
}
like image 31
Aaron Avatar answered Nov 14 '22 22:11

Aaron