代码行统计并使用Java8和Scala重构

2 篇文章 0 订阅

代码行统计并使用Java8流重构

昨天看了一个关于统计大文本的的例子,想起来老师说,对于我们专业的学生,大学四年应该累计代码量达到五万行才算是比较合格的。正好想起来,计算一下自己的Java代码量

问题定义

我现在主要学习Java以后,很多东西都在Eclipse的工程目录里面了,这个目录下面的文件信息如下。
Java代码所在文件夹的信息

所以,现在问题就转变为遍历2423个文件夹,寻找Java源文件,并统计代码行数了。

一个简单的工程目录大概如下:
在这里插入图片描述
重点就是遍历这个文件夹,找到 CodeCounter.java 文件,并统计代码行数。

分析思路

获取文件的思路

因为文件夹和文件的组织形式是树形的,所以,考虑我们需要访问以下这个目录。所以,可以采用递归的形式,来遍历每一个文件夹,寻找目标文件。

具体思路为:
1.访问一个文件夹,获取其下所有的文件(文件包括文件夹)。
2.如果获取结果为空,则退出当前访问。否则,对于每一个文件,如果其是文件夹,则执行步骤1;否则,记录当前文件的信息,并退出当前访问。

提示: 把文件夹当作左子树,文件当作右子树,类似二叉树的先序遍历。

在这里插入图片描述
这里我使用一个 List 记录所有Java源文件的路径。

	/**
	 * 获取指定目录下的代码文件路径,并存入一个 List。
	 * */
	private void ListCodeFile(File file) {
		//这里需要注意,这个 name 是文件的 getName 所获取的名字,如果需要访问这个文件,
		//必须在当前目录下才可以,即 new File(name) 是不存在的,必须是 new File(dir, name)。
		File[] files = file.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String name) {
				File f = new File(dir, name);
				if (f.isDirectory()) {
					return true;
				} else {
					return name.endsWith(suffix);  //以扩展名的方式来过滤指定格式的文件
				}
			}
		});
		if (files == null) {
			return ;
		}
		for (File f : files) {
			if (f.isDirectory()) {
				ListCodeFile(f);
			} else {
				String path = f.getAbsolutePath();
				this.pathList.add(path);
			}
		}
	}

统计文本文件的行数思路

这里我采用通过换行符 \n 的数据来确定文件的行数。下面这张图中的 CRLF 就是换行符,注意左边的行号是3,但是换行符的数量是2。这是因为文件结尾是没有换行符的,所以可以得到换行符和文件行数的对应关系:
文件行数 = 文件数 + 文件换行符数。
代码行数示例

思路: 对于每一个文件,每次读取一个字节数组的数据,然后对数组内的 \n 符号进行计数,循环累加,得到一个文件的换行符数。

注意:源代码中含有空行和注释,我认为注释是必要的,体现了自己的思考,应该计算入代码行数内。但是空行只是为了显示起来不那么拥挤,所以我认为应该去掉空行的数量。

例如,对于如下字节数组序列(空白表示空白字符):
在这里插入图片描述
显示为文本应该是:

a = 3;

b = 4;

这应该算做两行代码。所以,对于第一个 \n 和 第二个 \n 之间的内容,应该被认为是一个空行,不计算入代码行数,这里采用一个标志位 flag 来处理。

private int count(String filename) {
	int count = 1;   //初始化计数器为 1,这里比较特殊,因为最后一行是没有换行符的,所有初始值应该是1。
	byte[] data = new byte[1024];  //字节数组
	int hasRead = 0;
	try (InputStream in = new BufferedInputStream(new FileInputStream(filename))){
		while ((hasRead = in.read(data))!= -1) {
			boolean flag = true;
			for (int i = 0; i < hasRead; i++) {
				
				if (!flag || !Character.isWhitespace(data[i])) {
					flag = false;
				}
				if (data[i] == '\n') {
					if (!flag)    //如果不是空行
						count++;
					flag = true;  //重置标识位
				}
			}
		}
	} catch (FileNotFoundException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}
	return count++;
}

完整代码

package dragon;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * 对指定目录及其子目录中所有代码文件进行代码行数统计。
 * @author Alfred
 * @version 1.0
 * */
public class CodeCounter {
	private String suffix;
	public List<String> pathList;
	
	public CodeCounter(String suffix) {
		this.suffix = suffix;
		this.pathList = new ArrayList<>();
	}
	
	/**
	 * 获取指定目录下的代码文件路径,并存入一个 List。
	 * */
	private void ListCodeFile(File file) {
		//这里需要注意,这个 name 是文件的 getName 所获取的名字,如果需要访问这个文件,
		//必须在当前目录下才可以,即 new File(name) 是不存在的,必须是 new File(dir, name)。
		File[] files = file.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String name) {
				File f = new File(dir, name);
				if (f.isDirectory()) {
					return true;
				} else {
					return name.endsWith(suffix);
				}
			}
		});
		if (files == null) {
			return ;
		}
		for (File f : files) {
			if (f.isDirectory()) {
				ListCodeFile(f);
			} else {
				String path = f.getAbsolutePath();
				this.pathList.add(path);
			}
		}
	}
	
	/**
	 * 统计每一个文件的代码行数总和
	 * */
	private int codeCount(List<String> pathList) {
		int sum = 0;
		for (String filename : pathList) {
			sum += this.count(filename);
		}
		return sum;
	}
	
	//统计一个文件的代码行数
	private int count(String filename) {
		int count = 1;   //初始化计数器为 1,这里比较特殊,因为最后一行是没有换行符的,所有初始值应该是1。
		byte[] data = new byte[1024];  //字节数组
		int hasRead = 0;
		try (InputStream in = new BufferedInputStream(new FileInputStream(filename))){
			while ((hasRead = in.read(data))!= -1) {
				boolean flag = true;
				for (int i = 0; i < hasRead; i++) {
					
					if (!flag || !Character.isWhitespace(data[i])) {
						flag = false;
					}
					if (data[i] == '\n') {
						if (!flag)    //如果不是空行
							count++;
						flag = true;  //重置标识位
					}
				}
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return count++;
	}
	
	public static void main(String[] args) {
		long start = System.currentTimeMillis();
		CodeCounter counter = new CodeCounter(".java");
		counter.ListCodeFile(new File("D:\\JavaProject"));
		System.out.println("Java源文件数:" + counter.pathList.size());
		int sum = counter.codeCount(counter.pathList);
		System.out.println("代码行数:" + sum);
		System.out.println("运行时间毫秒数:" + (System.currentTimeMillis()-start));
		counter.pathList.forEach(System.out::println);
	}
	
}

运行结果:
过滤空格:
在这里插入图片描述

最初不过滤空格的结果:
在这里插入图片描述

说明

通过一个简单的代码统计,也用到了不少的知识。这样也好,活学活用,对于理解一下概念,也是很有帮助的。以前对于文件的操作都用不好,现在也可以较为熟练的使用了,算是一个较大的进步吧。




重构代码

使用Java8学到的一些知识,来重构一下代码,这里不考虑性能的问题。上面统计文本行数的方式,我也看了几个相关的博客,有很好的处理大文件的方式,不过我这个只是很小的数据量了。我就采用这种方式了,对于思考是很有好处。别人推荐的都是工具类了,处理确实很简单。但是我们的思考过程就没了!

获取文件方法重构

1.对于过滤文件的代码,采用 Lambda 表达式来代替匿名对象。 但是,这里我又使用了三目运算符,把代码给简化成一行了,有点失去了易读性。(不过,可以多用用,不然看别人的代码不容易看懂!)

2.对于for循环遍历数组的方式,改为使用内部迭代的方式,利用流来处理。 不过感觉也没有什么简化,可能我只能简化到这一步吧,哈哈!

/**
	 * 获取指定目录下的代码文件路径,并存入一个 List。
	 * */
	private void ListCodeFile(File file) {
		//这里需要注意,这个 name 是文件的 getName 所获取的名字,如果需要访问这个文件,
		//必须在当前目录下才可以,即 new File(name) 是不存在的,必须是 new File(dir, name)。
		File[] files = file.listFiles((dir, name)->true == new File(dir, name).isDirectory() ? true : name.endsWith(this.suffix));
		if (files == null) {
			return ;
		}
		
		Stream.of(files).forEach(f->{
			if (f.isDirectory()) {
				ListCodeFile(f);
			} else {
				String path = f.getAbsolutePath();
				this.pathList.add(path);
			}
		});
	}
统计文本文件的行数的方法重构

1.使用字符流的 readLine 方法,来统计代码行数,不使用字节流的方式,这样处理起来比较方便。 对于只有一行代码的文件,却又一个换行符,即下一行是空行,但是上面那种方式会计算为两行代码,所以就造成了一个代码量统计偏高。但是因为一个文件最多就多统计一行,所以也就认为是正确的吧。下面使用 readLine 来处理之后,可以很方便的处理这种问题,这也体现了工具的重要性了。

注意:这个方法是静态的,这是有意为之!为了下面使用方法引用。

public static int countByLine(String filename) {
	int count = 0;   //这里不是上面那种方式了,初始值为 0
	try (BufferedReader reader = new BufferedReader(new FileReader(filename))){
		String line = null;
		while ((line = reader.readLine()) != null) {
			if (!line.trim().equals("")) {   //不计算空行
				count++;
			}
		}
	} catch (FileNotFoundException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}
	return count;
}

2.函数式编程的一个目的就是减少使用 for、while 等外部迭代的方式,转而使用内部迭代的形式处理代码。

	/**
	 * 统计每一个文件的代码行数
	 * */
	private int codeCount(List<String> pathList) {
		int sum = 0;
		for (String filename : pathList) {
			sum += this.count(filename);  //因为我不使用字节流的方式了,所以这个 count 方法,应修改为上面使用的 countByLine 方法。
		}
		return sum;
	}

上面这段代码中使用了循环、累加,正好可以很方便的重构一下代码。

//上面的方法是静态的,所以这里可以使用方法引用。
private int codeCounteByLine(List<String> pathList) {
	return pathList.stream()
			.map(CodeCounter::countByLine) //对每一个文件名,经过 countByLine 方法,转为 int,达到了一个映射的目的。即 String -> Integer 
			.reduce(0, (a, b)->a+b);    // 使用 reduce 来进行累加操作。
}

这样处理以后,显得逻辑清晰,很方便理解。

3.使用字符流的 readLine 方法,来统计代码行数,不使用字节流的方式,这样处理起来比较方便。
注意:这个方法不是静态的,为了下面使用另一种形式的方法引用。

public int countByLine1(String filename) {
   //方法体同上面的 countByLine 		
}

使用并行流进行处理,这个并行流我并不熟悉,这里就是尝试一下, 似乎效果不明显。可能是数据量太少了,并行不一定比串行速度快。这里可以忽略这个并行流,还是用上面的 Stream,可以比较两种形式方法引用的区别。

	private int parallelCodeCounteByLine(List<String> pathList) {
		return pathList.parallelStream()   //使用并行流处理了
				.map(this::countByLine1)  //另一种形式的方法引用
				.reduce(0, (a, b)->a+b);
	}



使用Scala重构代码

最近看了一点点Scala,因为使用Java8重构代码,主要就是使用学习到的函数式编程方式,所以就想着利用 Scala 来重构一次。我以为很简单的事情,结果搞了几个小时,说起来都是泪。我只是看了几页的Scala就敢来重构,踩了太多的坑,查了很多资料,才最终解决了。下面只是把代码贴出来,就不详细说明了,写的不好,而且我也不是很理解这个Scala。

简要说明:使用 Scala 确实简洁了许多,代码量大为减少,当时因为不是很熟悉,所以写起来很难受,搞得我的学习热情都消退了。

import java.io.File
import scala.io.Source

object CodeCounter {
  def main(args: Array[String]): Unit = {
    val counter: CodeCounter = new CodeCounter(".java")
    counter.ListCodeFile(new File("D:\\JavaProject"))
    println("Java 源文件数:" + counter.pathList.size)
    var sum: Int = counter.count
    println("代码行数" + sum)
  }
}

class CodeCounter {
  var suffix: String = _;
  var pathList: List[String] = _;

  def this(suffix: String) {
    this()
    this.suffix = suffix
    this.pathList = List() //创建一个空列表
  }

  def ListCodeFile(file: File): Unit = {
    val files: Array[File] = file.listFiles((dir: File, name: String)=>{
      if (new File(dir, name).isDirectory) {
        true  //似乎不能使用 return,否则函数就结束了,这是一个大坑!
      } else {
        name.endsWith(this.suffix)
      }
    })
    if (files == null) {
      return
    }

    files.foreach(f => {
      if (f.isDirectory){
        ListCodeFile(f)
      } else {
        val path = f.getAbsolutePath
        pathList = pathList :+ path  //在列表的尾部添加一个元素,这是一个坑,好奇怪的用法。
      }
    })
  }
  
  //如果文件不是使用UTF-8 会报错,导致我手动改了十几个使用 GBK 编码的文件才解决。
  def countByLine(filename: String) = Source.fromFile(filename, "UTF-8")  
                                                .getLines()
                                                .filter(line => !line.trim().equals(""))
                                                .map(line => 1)
                                                .reduce(_ + _)  //.reduce((a: Int, b: Int) => a+b)

  def count() = pathList.map(countByLine).reduce(_ + _)
}

运行结果:
在这里插入图片描述

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值