当前位置: 首页 > article >正文

Effective Java 学习笔记 通用编程

这篇文章总结本书第九章,主要涉及Java中一些零碎的原则和注意事项,包括最小化局部变量作用域、控制结构、类库用法、基本数据类型和引用数据类型的用法,以及Java的反射机制和本地方法的使用、最后讨论代码优化和命名。嗯,这一章非常的杂,本文将依次进行总结。

最小化局部变量作用域

这个原则已经老生常谈了,其实无论是什么变量,都要尽可能的最小化其作用域,这是封装原则的要求。对于局部变量尤其重要。

局部变量是指生命周期在某一作用域内部的变量,这个作用域可以是一个类或者一个方法,因为其生命周期有特定的范围,所以它的声明和结束的位置不能超过该范围,否则就会有扩散的风险。

首先是声明,要使得局部变量的作用域最小,最好的方法就是在第一次要使用的时候进行声明(但是个人觉得类中变量的声明似乎应该集中在类的开头,这样方便读者的阅读,同时如果做好变量的权限管理也不会有对外泄露的风险)。这里的重点一是不要在变量的使用域之外声明,否则变量的生命周期会大于其使用域,造成泄漏的风险。二是每一个局部变量的声明都应该包含一个初始化的表达式,因为未进行初始化的引用可能会含有系统设定的默认值,有出错的可能,但是如果变量的初始化可能会抛出受检的异常,那最好通过try-catch语句来执行初始化,在语句之间声明变量,但是这个声明也最好自行设置一个默认值比如null。

Class<? extends Set<String>> cl = null;
   try {
         cl = (Class<? extends Set<String>>) Class.forName(args[0]);
       } catch (Exception e) {
            // TODO: handle exception
       }

最后作者举了一个循环的例子,当需要在循环中使用循环变量的时候,用for循环要优于while循环,因为for循环可以在循环条件声明时定义循环变量,而while不行,因此while循环的循环变量非常容易被扩散到程序的其他地方。

循环结构的优劣比较

这里作者重点推荐了for-each循环,只要实现Iterable接口的任何对象都可以应用,它通过隐藏迭代器或者索引变量,相比于for循环更加简洁,降低出错的风险:

//for循环的迭代器方法
for(Iterator<Element> i = c.iterator; i.hasnext()){
        Element t = i.next();    
        ...
}


//for循环的循环变量方法
for(int i = 0; i<c.length(); i++){
    Element t = c[I];
}


//for-each方法
for(Element t: c){
    ...
}

这里for-each方法没有烦人的循环变量,也不需要声明看似累赘的迭代器,非常的简洁。但是,相对来说for-each方法的使用场景也较为有限,面向所有元素进行扫描。 作者也说明了三种情况下无法使用for-each方法:

  • 在解构迭代中无法使用:即在遍历集合的时候删除选定的元素(这里需要用到迭代器的remove方法)。
  • 元素转换中无法使用:遍历列表或者数组时,替换部分或者全部元素值(这里需要用到列表迭代器的方法或者数组索引)。
  • 平行迭代时无法使用:当需要并行遍历多个集合时,需要显式控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以同步前进。(for-each方法无法控制内外循环并行进行)。

类库的使用

这一条作者建议开发者不要重复造轮子,尤其是Java标准类库有的方法,不要自己再去实现一遍了,标准类库的稳定性和安全性一定比你自己造的轮子更优秀。使用类库的优势还包括:

  • 使得开发者不必浪费时间为那些与工作不太相关的问题提供特别的解决方案。
  • 类库方法的性能往往会随着时间的推移而不断提高,而这种优化无需开发者做任何努力,因为这些类库有专业的组织在负责维护。同时类库的功能也会随着时间的推移而增加新的功能。
  • 使用类库方法可以提高代码的复用能力,一个是其他开发者相比于你的方法肯定更了解类库的方法,二是类库的方法更加稳定和通用。

最后作者推荐了Java的几个类库,强烈建议Java的开发者们去了解并且跟踪它们的更新情况。

  • java.lang
  • java.util
  • java.io
  • Collections Framework
  • Stream类库

除了Java的标准类库以外,像Google的Guava类库为代表的三方库也很有参考价值。

基本数据类型的使用

作者主要介绍了float与double用于精确计算时的问题、装箱基本类型的缺陷和字符串的缺陷。

在执行精确计算的时候,请用int或者BigDecimal代替float与double

float 和 double 类型使用二进制浮点数表示法来存储数值。由于十进制的小数(如 0.1)在二进制中无法精确表示,因此 float 和 double 类型无法精确表示 0.1。所以要使用同样由十进制存储的数据类型BigDecimal来做精确表示,但是由于BigDecimal是一个类,因此其实例的大小要远远大过基本数据类型。还有一个折中的办法就是使用int来表示,但是要开发者自己指定制定表示小数的方式(比如用100表示1.00等)。

除了特定的场景,尽量使用基本数据类型而非装箱类型

相比于基本数据类型,装箱类型有几个显著的缺陷:

  • 默认值为null,有空指针的风险。
  • 装箱类型的同一性是实例的比较而非值的比较,这往往和开发者的意图有冲突。
  • 装箱类型的内存占用相比于基本数据类型较大,而且当装箱类型与基本数据类型进行交互的时候会触发自动拆装箱机制,如果这个操作很多就会损耗性能,而且存在一定的风险(如果装箱类型的实例为null,自动拆箱就会抛出空指针异常)。

但是,也有一些特定的场景必须要使用装箱类型:主要是集合中的元素、参数化类型和方法以及反射的调用。

谨慎使用字符串

字符串只能用来表示文本,超出这个场景就不是那么适用:

字符串不适合代替其他的数据类型:这里主要是通过input输入的数据,通常是以字符串的形式存在,要记得在输入之后转成实际的数据类型,比如数值、布林值等。

字符串不适合代替枚举类型:枚举常量更加合适。

字符串不适合代替聚合类型:比如一个Key不要用字符串粘合在一起,最好定义一个类来描述这个数据,这样便于调用数据的相关部分。

字符串不适合代替索引:比如一个数据的ID或者Key,使用字符串容易违背索引的唯一性或者不可篡改性。

另外字符串在通过"+"进行连接时的时间复杂度为n的平方级,因此对于有大量连接操作的场景应该使用StringBuilder的append方法来实现。

引用数据类型的使用

应该优先使用接口而不是具体的类来引用对象,这会使得程序更加的灵活,一旦要更改具体实现时,可以直接更改类的构造器(改变实现往往是由于新的实现有更好的性能或者提供了新的功能对于程序有更大的价值)。


//用具体类作为引用变量,不推荐
LinkedHashSet<String> StringSet = new LinkedHashSet<>() 


//用接口作为引用变量,可以直接更改实现
Set<String> StringSet = new LinkedHashSet<>();

StringSet = new HashSet<>();

当然也有一些场景本身就不支持接口用接口作为引用对象

  • 没有合适的接口:这个自然而然没有办法。
  • 对象属于一个框架的基本类:这里往往是框架的使用要求,需要用抽象类作为基本类来实现,比如outputStream,那就请尊重框架的要求。
  • 潜在的实现类有除对应接口外的其他方法:那相关的接口就无法引用这个类了,因此接口作为引用只适合与框架类的接口(即接口定义了一群类的方法框架(比如Set对于HashSet),还有一类接口是功能型的是作为一些类的功能补充(比如Comparable<Integer>接口对于Integer类),这就不适用了)。

 除了引用类型以外,实现类型的选择需要做一个取舍,对于层次越高的类越可以提供更好的性能,但是层次越低的子类往往能提供更多特定的功能。比如上例中的LinkedHashSet和HashSet前一个能够实现自动排序功能而HashSet没有,但是HashSet的实现可以获得更好的性能。

反射机制

Java的反射机制可以让开发者通过程序来访问任意类的内部,可以获得类的构造器、方法和属性,这在一定程度上破坏了类的封装性,但是反射机制最大的优势就是能够在实例甚至是类都还没有定义的情况下先提前设计好相应的程序,这使得开发者无需具体的类就可以设计调用它们要素的通用程序:


import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Set;

//这个类由第一个命令行指定Set的实现方式,将命令行后续的内容收集起来
public class ReflectionTest {
    public static void main(String[] args) {
        Class<? extends Set<String>> cl = null;
        try {
            cl = (Class<? extends Set<String>>) Class.forName(args[0]);
        } catch (ClassNotFoundException e) {
            // TODO: handle exception
        }

        Constructor<? extends Set<String>> cons =null;
        try {
            cons = cl.getConstructor();
        } catch (NoSuchMethodException e) {
            // TODO: handle exception
        }

        //Initiate the class
        Set<String> s = null;
        try {
            s = cons.newInstance();
        } catch (IllegalAccessException e) {
            // TODO: handle exception
        } catch (InstantiationException e){
            // TODO: handle exception
        } catch (InvocationTargetException e){
            // TODO: handle exception
        } catch (ClassCastException e){
            // TODO: handle exception
        }

        s.addAll(Arrays.asList(args).subList(1, args.length));
        System.out.println(s);


    }
}

当然,使用反射机制也有很大的代价:

  • 破坏了封装:容易造成类的私有方法或者域外泄。
  • 损失了编译时类型检查的能力: 如果反射机制企图调用类不存在的方法或者有问题的方法,只有在运行的时候才能发现。
  • 代码量多:主要是处理各种异常的代码(见上例)。
  • 性能损失:反射方法的调用比普通方法会慢很多。

本地方法使用

JNI允许Java调用由本地编程语言(C或者C++)编写的代码,比如可以利用本地方法来编写应用程序中注重性能的部分,但是作者认为没有必要,因为目前的Java足够的强大,从Java9开始Java增加了进程API,提供了访问操作系统的能力,解决了高级语言被一直诟病的离操作系统太远的问题。而且本地方法由于没有经过体系化的验证使用是有很大危险的,而且还涉及到很多胶合代码的工作量,所以作者非常不建议。

代码优化

这章很长,但是总结起来就是四个字“不要优化”,不要费力的去一味追求编写快速的程序而要以编写好的、稳定的程序为更高优先级,速度自然会随之而来。对于性能的追求不应该在优化阶段作为重点考虑,而应该作为设计阶段重点考虑的因素,尤其是在设计API、交互层协议和永久数据格式的时候。就算在代码开发完成后发现了性能问题,也要通过性能剖析器去分析原因,不要自以为是。

Java的命名惯例

作者介绍了Java的命名惯例(还有很多不一一列举了):

对象命名规则
类和接口包括一个或者多个单词,每个单词首字母大写,接口可以使用able或者ible后缀FutureTask、Comparable
方法和域第一个单词首字母小写,方法可以使用对应的动词ensureCapacity
常量域单词大写,词间用下划线隔开

NEGATIVE_INFINITY

局部变量与普通域类似,但是允许缩写houseNum
类型参数单个字母,K和V表示映射的键和值、T表示所有类型、X表示异常、R是返回类型、TUV表示多个类型Comparable<T>
可被实例化的类每个单词首字母大写PriorityQueue
不可被实例化的类复数名词Collections
特定的方法

返回boolean的方法用is开头

setter和getter用set和get开头

转换对象类型的方法用to开头

返回视图的方法用as开头

返回与被调用对象同值的方法:typeValue

isEmpty、getName、toString、asList、intValue

但是,如果项目有特定的命名习惯,请以项目要求为准。


http://www.kler.cn/news/341028.html

相关文章:

  • java -jar 指定配置 logback.xml
  • c++剪枝
  • 如何使用Colly库进行大规模数据抓取?
  • Lumerical脚本语言——添加实体对象(Adding Objects)
  • wordpress常见数据库连接错误原因及其解决方案
  • 【音频可视化】通过canvas绘制音频波形图
  • 什么是静态加载-前端
  • Mosaic for Mac:让你的Mac窗口管理更智能
  • python发邮件附件:配置SMTP服务器与认证?
  • BKP读写备份寄存器
  • vscode软件中可以安装的一些其他插件
  • YOLO11改进|注意力机制篇|引入矩形自校准模块RCM
  • 案例分享—国外优秀UI设计作品赏析
  • C语言 | Leetcode C语言题解之第467题环绕字符串中唯一的子字符串
  • Wasserstein距离
  • 支持向量机-笔记
  • Ethernet IP 转 Profinet网关在流量计中的应用
  • vmware下ubuntu18.04中使用笔记本的摄像头
  • 传统的机器学习在自然语言处理领域中对比深度学习和大语言模型有哪些优势?
  • 鸿蒙fork()功能