如果让你去读取 Android 设备的温度,并且告诉你这些温度的值都存在 /sys/class/thermal/thermal_zone
开头的目录下的 temp
文件当中,我们只需要读取它的平均值即可,那么我们要怎么去写这样的程序呢?
thermal_zone 表示不同的区域,简单起见,我们就只求平均值了。当然,实际测试过程中也遇到某些高版本的设备无法直接访问
/sys/class/thermal
这个目录,但它的子目录和文件是可以访问的,因此,如果大家测试过程中遇到thermalDir.listFiles
返回null
的问题,请不要感到惊讶。
Java 版本
先来看下 Java 的版本:
代码语言:javascript复制public class ThermalStatsJ {
public final double temperature;
public ThermalStatsJ() {
File thermalDir = new File("/sys/class/thermal/");
File[] thermalZoneFiles = thermalDir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isDirectory() && pathname.getName().startsWith("thermal_zone");
}
});
int sum = 0;
int count = 0;
for (File thermalZoneFile : thermalZoneFiles) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(new File(thermalZoneFile, "temp")));
String line = reader.readLine();
sum = Integer.valueOf(line);
count ;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
if(count > 0){
temperature = sum / 1000.0 / count;
} else {
temperature = 0;
}
}
}
我们定义了一个类,每一次构造这个类的对象的时候都会读取一个最新的温度的值存入这个对象的唯一的 final 字段当中。在 Java 版本当中,我们先把符合要求的文件列出来,接着遍历他们去读取这些文件中的唯一一行,实际上就是温度的 1000 倍的一个整数,读到之后我们再求平均值。
思路很简单,用 Java 代码写出来之后思路也不能算不清晰,就是写起来不是很顺畅。我们能看到的问题有几个呢?
- 我明明就只是想要读取文件的一行,结果前前后后写了那么多的模板代码
- 我明明就只是想要初始化一下 `temperature`,我们当然可以在最后用三元表达式来简化这一个过程,但如果条件更复杂呢?三元表达式简直就成了噩梦。而且整个构造方法不过就是为了初始化这样一个变量,却没有突出这样一个重点。
- 实际上整个程序就是一个完整的数据变换的过程,但这样的代码让我们并不能很直接的看到这一点,我们看到的更多的仍然是 Java 代码的“仪式感”。
Kotlin 版本
Kotlin 既然作为 Jvm 领域内 Java 的继任者,它确实在解决这些问题上面都花了功夫。作为对比,我们同样给出 Kotlin 的版本:
代码语言:javascript复制class ThermalStats {
val temperature: Double
init {
temperature = try {
File("/sys/class/thermal/")
.listFiles{ pathname -> pathname.isDirectory && pathname.name.startsWith("thermal_zone") }
.mapNotNull { File(it, "temp").bufferedReader().use { it.readLine().toIntOrNull() } }
.average() / 1000.0
} catch (e: Exception) {
e.printStackTrace()
0.0
}
}
}
整个构造方法体,我们一眼就能看出来其实我们就只做了一件事:初始化 temperature
,因为整个方法体,就是一个表达式赋值的过程,这样表意已经非常清楚了,代码阅读者也许看了这一个开头就无需继续阅读了,因为他已经很快的明白了这个类是在干什么(也许你需要再回去看看 Java 的版本对比下?)
知识点:try ... catch 是表达式,最后一行作为其值返回,表达式的类型推导取决于两个分支的返回值的公共父类(接口),如果有多个公共父类(接口),返回值类型默认推导为
Any
,如果表达式值的接受者的类型是前面提到的多个公共父类(接口)其中之一,那么推导为接受者的类型。
接着我们仔细看下整个读文件求温度平均值的写法,简直就是“一条龙服务”,先从起始目录当中找到温度文件存放的目录,再拿到这个文件, readLine
,求平均值。这样写的好处就是,我们能够很清晰的了解到温度平均值的读取流程,中间发生的每一步转换都清晰的展现在我们面前。
知识点:善于使用 Kotlin 标准库中 io 相关的扩展,能够达到事半功倍的效果。需要注意的是,
bufferedReader
的use
扩展会在数据读完之后安全地关闭流,以免造成泄露。
使用 Kotlin 编写逻辑能够让逻辑本身更加突出,显然这也是高级语言本身的意义所在:它们被创造出来的目的就是让人能够更轻易的了解程序的含义和逻辑。
再说点儿别的
其实这个程序里面还有一个点没有提到,那就是 temperature
这个变量的声明问题,我把它声明为 final
或者说 val
,用意自然很明显。但我们可爱的同事或者同行却大多不爱这样做,特别是在 Java 程序当中,理由嘛,也很简单:
- 需要额外写
final
加一个空格。显然,作为优秀的程序员,我们都具有“懒惰”的优秀品质,除非必要,我为什么要写这个烦人的东西? - 它确实很烦人,至少从 Java 代码的版本来看,我不仅需要在
count>0
的情形下为它赋值,而且还得写个else
,我为什么不能在声明它的时候直接给他初始化一个变量呢?
所以我们可爱的 Java 程序员就很容易写出下面的版本:
代码语言:javascript复制public class ThermalStatsJ {
public double temperature; //或者写上 = 0 表示初始化
public ThermalStatsJ() {
...
if(count > 0){
temperature = sum / 1000.0 / count;
}
}
}
但这样做就会有比较尴尬的事情发生了,例如:
代码语言:javascript复制ThermalStatsJ thermalStats = new ThermalStatsJ();
...
thermalStats.temperature = 2;
这样写程序并不会报错,也许写出这样的代码来的同学还以为温度可以被设置呢!尽管对于温度被设置这件事看上去不合理,但如果这里讨论的对象不是温度呢?
当然,这里也不是针对 Java 程序员了,Kotlin 程序员也存在一样的毛病,最近看到了不少让我感到惊讶的写法,例如对于前面的例子,他们可能会这样写:
代码语言:javascript复制class ThermalStats {
var temperature: Double? = null
init {
try {
temperature = ...
} catch (e: Exception) {
e.printStackTrace()
}
}
}
这么看来,这样似乎更符合他们的直觉,尽管看起来也没有什么过错。但稍微往后想想,结果可能就是:
代码语言:javascript复制thermalStats.temperature?
thermalStats.temperature!!
使用 ?
看上去让代码更“健壮”了,可如果通篇都是这样的东西,那跟 if(xxx!=null)
又有什么区别?而使用 !!
就更糟糕了,程序员的傲慢在这里展露无遗。在这一点上,使用 final
变量虽然只是个形式的问题,但却关乎我们对程序执行的思考,我们究竟应该把问题尽可能的在前面解决呢,还是说留给后面使用的人来处理呢?答案当然就像你的老师经常对你说的,要打好提前量。
至于 final
在并发时的语义问题,相比之下比较晦涩,我就不细说了,大家只需要知道 final
变量比 non-final
变量在并发环境下更安全就是了。《Java 并发编程的艺术》这本书对此有详细的介绍,大家可以参考一下。
小结
- 写人能看得懂的程序
- 打好提前量