一行一行遍历文本文件的内容 - 是否有最佳实践?(与PMD的AssignmentInOperand相比)

46
我们有一个Java应用程序,其中有几个模块能够读取文本文件。它们使用以下代码非常简单地执行此操作:

```

File file = new File("file.txt");
Scanner scanner = new Scanner(file);

while (scanner.hasNextLine()) {
    String line = scanner.nextLine();
    System.out.println(line);
}
scanner.close();

```

BufferedReader br = new BufferedReader(new FileReader(file));  
String line = null;  
while ((line = br.readLine()) != null)  
{  
   ... // do stuff to file here  
} 

我在我的项目上使用PMD,并在while (...)行上收到了“AssignmentInOperand”违规警告。

是否有比显而易见的方法更简单的方式来完成这个循环?

String line = br.readLine();  
while (line != null)  
{  
   ... // do stuff to file here  
   line = br.readLine();  
} 

这是否被认为是更好的实践?(尽管我们“重复”了line = br.readLine()这行代码?)


不错的 BufferedReaderIterator。 我不得不用 r.mark(2) 替换 r.mark(1),否则在一个大文件的100行左右会出现“无效标记”的错误。不明白为什么。 - user1202927
5
for循环怎么样?for (String line = br.readLine(); line != null; line = br.readLine()) { ... } - Mikey Boldt
8个回答

38

谢谢,这将来可能会派上用场。 - RonK
1
如果有人需要从Maven获取FileUtils依赖项,请使用以下代码:<dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency> - blue-sky
我相当确定你也可以用Scanner做同样的事情。 - anon
很遗憾它没有实现 AutoClosable :( - Radek Postołowicz
看起来你应该可以这样使用:for(String line: FileUtils.lineIterator(file,"UTF-8")) { /*do something */ }。但可悲的是,那不会干净地关闭迭代器。 - Michael Anderson

27

Java-8中对Lambda表达式的支持以及Java-7中的Try-With-Resources,可以以更紧凑的语法实现您想要的功能。

Path path = Paths.get("c:/users/aksel/aksel.txt");

try (Stream<String>  lines = Files.lines(path)) {
    lines.forEachOrdered(line->System.out.println(line));
} catch (IOException e) {
    //error happened
}

2
可以将lambda缩短为方法引用: lines.forEachOrdered(System.out::println) - Alex
我喜欢这个,但是我想在迭代文件时设置布尔值来表示我已经到达了感兴趣的区域,但它说我不能在循环体中使用非最终变量,所以对我来说不可行。我改用了@rolfl的答案 https://dev59.com/Xm445IYBdhLWcg3w1tgS#22351492 - Craig

22

我通常使用while((line = br.readLine()) != null)结构... 但是,最近我遇到了这个不错的替代方案

BufferedReader br = new BufferedReader(new FileReader(file));

for (String line = br.readLine(); line != null; line = br.readLine()) {
   ... // do stuff to file here  
}

这仍然是重复使用readLine()函数调用的代码,但逻辑很清晰等。

我在读取流到byte[]数组时使用while(( ... ) ...)结构的另一个场合...

byte[] buffer = new byte[size];
InputStream is = .....;
int len = 0;
while ((len = is.read(buffer)) >= 0) {
    ....
}

这也可以通过以下方式转换为for循环:

byte[] buffer = new byte[size];
InputStream is = .....;
for (int len = is.read(buffer); len >= 0; len = is.read(buffer)) {
    ....
}

我不确定我真的更喜欢for循环的替代方案...但是,它将满足任何PMD工具,而且逻辑仍然很清晰等。


1
不错的方法!如果在Java 7中使用,您还可以使用try-with-resources语句包装BufferedReader实例创建,它将减少变量的范围并自动关闭读取器,以便处理所有行。 - Pavel

22

通常我更喜欢前者。我不太喜欢在比较语句中使用副作用,但这个例子是一个非常常见和方便的习惯用语,所以我不反对它。

(在C#中有一个更好的选项:一个返回IEnumerable<string>的方法,你可以使用foreach遍历;在Java中这不那么好用,因为增强型for循环结束时没有自动释放...而且因为你不能从迭代器中抛出IOException,这意味着你不能将其作为另一种的即插即用的替代品。)

换句话说:重复的行问题比操作符内赋值的问题更让我困扰。我已经习惯了一眼就看到这个模式 – 对于重复行版本,我需要停下来检查所有东西是否放置正确。这可能只是习惯问题,但我认为这不是什么大问题。


我很好奇您对于创建一个装饰器作为方便机制来抽象迭代语义的想法有何看法,以便您可以使用foreach循环(请参见我的回复,其中包含一个粗略的建议)... - Mark Elliot
@Mark E: 它不像C#版本那么整洁,但也不错-除了异常。我会评论你的答案并编辑我的。 - Jon Skeet

4

根据Jon的回答,我开始思考是否可以很容易地创建一个装饰器作为文件迭代器,以便您可以使用foreach循环:

public class BufferedReaderIterator implements Iterable<String> {

    private BufferedReader r;

    public BufferedReaderIterator(BufferedReader r) {
        this.r = r;
    }

    @Override
    public Iterator<String> iterator() {
        return new Iterator<String>() {

            @Override
            public boolean hasNext() {
                try {
                    r.mark(1);
                    if (r.read() < 0) {
                        return false;
                    }
                    r.reset();
                    return true;
                } catch (IOException e) {
                    return false;
                }
            }

            @Override
            public String next() {
                try {
                    return r.readLine();
                } catch (IOException e) {
                    return null;
                }
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }

}

警告:这会压制在读取期间可能发生的IOException,并且只会停止读取过程。不清楚在Java中有没有绕过它的方法,而不是抛出运行时异常,因为迭代器方法的语义已经定义明确,必须遵守才能使用for-each语法。此外,在此运行多个迭代器将会产生一些奇怪的行为; 因此我不确定是否建议这样做。
我已经测试过这个方法并且它可以工作。
无论如何,您可以通过使用它作为一种装饰器来获得for-each语法的好处。
for(String line : new BufferedReaderIterator(br)){
    // do some work
}

我怀疑这段代码无法编译,因为readLine可能会抛出IOException异常。Iterator接口不允许这样做,所以你必须将其包装在未经检查的异常中,这时它看起来越来越不像原始代码了:( - Jon Skeet
@Jon:你说得对,不幸的是我相信没有办法隐藏异常来获得语义。虽然这很方便,但回报似乎很惨淡。 - Mark Elliot

4

我有点惊讶以下这种替代方法没有被提到:

while( true ) {
    String line = br.readLine();
    if ( line == null ) break;
    ... // do stuff to file here
}

在Java 8之前,这是我最喜欢的语言,因为它很清晰,不需要重复。在我看来,break是与具有副作用的表达式相比更好的选择。然而,这仍然是一种惯用法。


1
"while (true)"通常被视为不良实践。 - undefined
"while (true)"通常被认为是一种不良的编程实践。 - psychosys

3

谷歌的Guava库提供了一种替代方案,使用静态方法CharStreams.readLines(Readable, LineProcessor<T>)和一个LineProcessor<T>的实现来处理每一行。

try (BufferedReader br = new BufferedReader(new FileReader(file))) {
    CharStreams.readLines(br, new MyLineProcessorImpl());
} catch (IOException e) {
    // handling io error ...
}

while循环的主体现在放置在LineProcessor<T>实现中。

class MyLineProcessorImpl implements LineProcessor<Object> {

    @Override
    public boolean processLine(String line) throws IOException {
        if (// check if processing should continue) {
            // do sth. with line
            return true;
        } else {
            // stop processing
            return false;
        }
    }

    @Override
    public Object getResult() {
        // return a result based on processed lines if needed
        return new Object();
    }
}

1

AssignmentInOperand是PMD中一个备受争议的规则,其原因是:"这可能会使代码更加复杂和难以阅读"(请参考http://pmd.sourceforge.net/rules/controversial.html

如果您真的想这样做,可以禁用该规则。在我的看法中,我更喜欢前者。


1
或者加上注释// NOPMD - Andrew Spencer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接