本文首发于微信公众号——世界上有意思的事,搬运转载请注明出处,否则将追究版权责任。微信号:a1018998632,交流qq群:859640274
最近把Effective Java复习了一遍,其中有比较多的java最佳实践可以在平时编程中用到。反序列化、并发、注解和枚举这四章没看,并发这本书里讲的比较简单,推荐java并发编程实战这本书。注解和枚举与 Thinking in java中讲的差不多。反序列化用的不多就没看了,以后用到了再复习一下。放上一些书籍笔记的源码,肯定会有纰漏大家可以选择性看看Thinking in Java、算法导论、Effective Java笔记源码
1.创建和销毁对象
- 1.考虑用静态工厂方法代替构造器:
- 优势:
- 1.有名字:如果有多个构造器,那么用户可能不能区分各个构造器的用途。
- 2.能复用:不必每次调用都创建新的类型,对于频繁创建而变化又不是很大的类有大帮助
- 3.可抽象:构造方法只能返回本类的实例,而这个可以返回本类的任何子类型
- 劣势:
- 1.如果类没有public或protect的构造方法就不能被子类化
- 2.静态工厂方法就是一个静态方法,没有特别之处,所以没有构造器实例化那么明显。
- 优势:
- 2.遇到多个构造器参数时要考虑用构建器:就是Builder模式。
- 3.用私有构造器或者枚举类型强化Singleton属性:
- 1.使用A中的静态工厂方法,将Singleton和构造函数设置成private
- 2.因为通过反射可以使用private的构造方法,所以可以在创建第二个实例的时候抛出异常
- 3.在序列化的时候必须声明类中所有域都是transient和提供一个readResolve方法,否则每次反序列化都会创建一个新实例
- 4.可以使用枚举来创建单例,这时候2、3的麻烦都会消失
- 4.通过私有构造器强化不可实例化的能力:一些工具类不希望被实例化,而编译器会自动给类提供一个无参构造函数,所以我们可以提供private的构造函数,并且为了防止反射,在里面抛出异常。
- 5.消除过期的对象引用:
- 1.一些对象在没有用了之后,还被有用的对象持有引用,此时就产生了内存泄漏
- 2.为了解决1,需要在最紧凑的作用域中定义每一个类型,而不是自己每次都手动去清理不需要的对象。
- 3.对于自己管理内存的类,最需要注意内存泄漏问题,因为垃圾回收器并不知道类中那些对象是失效的,这里需要程序员其告知垃圾回收器
- 4.缓存可能会内存泄漏:
- 1.当外部没有缓存的引用项的时候,某些情况下这个缓存就可以当成没用的了,所以可以使用WeakHashMap来让垃圾回收器自动清理缓存。
- 2.更多的情况是,虽然这个缓存没有引用项了,但是其在未来可能会有用,此时可以使用LRU算法来清理缓存
- 5.监听器和回调可能会内存泄漏:有时候一个回调被注册了,但是使用了以后没有被取消注册,此时回调就会被堆积起来,解决方法就是WeakHashMap
- 6.避免使用终结方法
- 1.终结方法的缺点在于其不能保证被立即执行。
- 2.从一个对象不可达到其终结方法被执行,会花费相当长的时间。
- 3.如果用终结方法去关闭文件,可能直到文件打开资源消耗光了,终结方法都不一定会被执行。
- 4.可能当一个对象被回收的时候,终结方法根本就没执行过
- 5.可能在一个JVM中终结方法被执行了,但是另一个中却不会被执行
2.对于所有对象都通用的方法
- 1.覆盖equals时请遵守通用规定:
- 1.在不覆盖equals的情况下,一个实例只和自身相等,只要满足了以下条件之一就不需要覆盖equals
- 1.类的每个实例唯一,这是Object的equals提供的
- 2.用户部关心该类的两个实例是否存在逻辑上的相等。
- 3.超类覆盖了equals,其行为对子类也适用
- 4.类是私有或者包级私有,就可以确认其equals方法永远不会被调用,此时应该在其equals方法中抛出异常
- 2.在什么时候应该覆盖equals呢?
- 1.这个类我们需要比较其逻辑上的相等,如Integer
- 2.该类的超类没有覆盖equals实现期望的行为
- 3.覆盖一个equals的时候要满足以下几个关系:
- 1.自反性:非null,x.equals(x)为true
- 2.对称性:非null,x.equals(y)为true 有 y.equals(x)为true
- 3.传递性:非null,x.equals(y)为true,y.equals(z)为true 有 x.equals(z)为true
- 4.一致性:非null,x.equals(y),只要x,y没改变,一直返回相同的结果
- 4.覆盖equals有以下几个技巧
- 1.先使用==,判断两个随想是不是同一个引用,以提高性能
- 2.使用instanceof判断是否是正确的类型,否则返回false
- 3.在2的基础上把参数转换成正确的类型
- 4.对类中每个需要比较的域进行检测比较,如果是引用可以递归调用equals
- 5.编写完了之后,问自己equals是否符合前面的四个关系
- 5.覆盖equals的告诫:
- 1.覆盖equals时候,总要覆盖hashCode
- 2.不要让equals太过智能,比如一个File类型和一个String的文件路径相等
- 3.不要将equals中传入的Object改成其他类型。
- 1.在不覆盖equals的情况下,一个实例只和自身相等,只要满足了以下条件之一就不需要覆盖equals
- 2.覆盖equals的时候总要覆盖hashCode:
- 1.只要一个对象的equals中比较的信息没改变,hashCode就不能改变
- 2.两个对象equals相等,那么其返回的hashCode也要相等
- 3.两个对象equals不等,hashCode返回不一定奥不等
- 3.始终覆盖toString
- 4.谨慎覆盖clone:
- 1.Cloneable 是一个空的接口,决定了clone()的实现方式,如果一个类实现了Cloneable,那么Object的clone()方法就会返回该对象的逐域拷贝,否则会抛出异常
- 2.如果实现了Cloneable,就表示该类和所偶超类都要遵循一个机制:无需构造器就可以创建对象
- 3.Object的clone()在拷贝的的时候是拷贝引用和数值,所以原对象和clone对象中域的引用是指向相同的对象,因此如果域引用了可变的对象,那么就会造成问题。
- 4.为了解决3的问题,要对一个对象实施clone()的时候,需要根据类的域的类型来判断是否要对域进行clone(),而在对域进行clone()的时候同样也会有3的问题。
- 5.所以为了克隆复杂的对象,最后的方法就是先调用Object的clone(),然后将所有域置为空白,最后调用类的api来重新产生对象状态。
- 6.另一个实现对象拷贝的办法就是提供一个对象拷贝构造器或者拷贝工厂。
- 5.考虑实现Comparable接口
3.接口和类
- 1.使类和成员的可访问性最小化:
- 1.public和protect是导出api的一部分,需要永远支持
- 2.子类覆盖的方法访问级别需要大于超类,接口方法默认是public
- 3.实例域绝不能是public的
- 2.使可变性最小化:
- 1.不可变类需要遵循以下几个条件:
- 1.不提供任何对外部可见的修改对象状态的方法
- 2.保证类不会被扩展,final或者将所有构造器设置成私有或包级私有
- 3.所有域都是final
- 4.所有域都是私有
- 5.保证和可变组件的互斥访问
- 2.不可变类有以下好处:
- 1.线程安全
- 2.不仅可以被共享还能共享内部信息
- 3.可以为其他对象提供大量不变的构件
- 3.坏处:对于每个不同的值都要一个单独对象
- 1.不可变类需要遵循以下几个条件:
- 3.复合优先于继承:
- 1.继承的缺点:
- 1.一个类进行了继承,或许当时类是可用的,但是随着超类的演化,某一时刻其可能就不可用了
- 2.超类的有些可继承的方法,可能在自身实现的时候进行了“自用”,而如果我们覆盖的时候不小心就会出现问题。
- 3.就算不覆盖方法,可能某些时候超类新增了一个和实现类相同签名的方法,那么实现类又会出问题。
- 2.复合转发就可以代替一部分继承的情况
- 1.如HashSet,我们需要在其上面扩展,可以让类实现Set并内置一个HashSet对象,然后在各个方法中调用HashSet的相应方法,当然可以在调用前后进行我们自己的操作
- 2.注意这不是"委托",除非把包装对象传给被包装对象。
- 3.只有子类是真正的超类的子类型的时候,才适合继承。这就是 is a和like a的区别。
- 4.在绝大部分情况下用复合替代继承,能隐藏被拓展类细节,恶意避免被扩展类中api的缺陷,被传染到拓展类中
- 1.继承的缺点:
- 4.要么为继承而设计并提供文档说明要么禁止继承:
- 1.要设计一个可被继承的类,需要做的事情
- 1.需要在文档中精确描述覆盖每个方法带来的影响,如可覆盖方法的自用性。如:AbstractCollection中,文档很清楚的说明了如果覆盖了iterator方法会影响remove方法
- 2.对于为了继承而设计的类,唯一的测试方法就是为其编写子类,如果编写了3个子类都没有使用到某些受保护的成员,那么就可设置为私有
- 3.构造器不能调用可被覆盖的方法,
- 4.对于不是为了继承而设计的普通类,可以禁止子类化
- 5.对于自用性,可以使用替代私有辅助方法代替需要自用的可覆盖的方法
- 1.要设计一个可被继承的类,需要做的事情
- 5.接口优于抽象类
- 6.接口只用于定义类型:
- 1.常量接口模式是不良的模式
- 2.导出常量可以在具体类之中
- 7.用函数对象表示策略:策略模式
4.泛型
- 1.不要在新代码中使用原生态类型:
- 1.每个泛型都定义了一 个原生态类型 ,即不带任何实际泛型参数的泛型,List<E>的原生态类型就是List
- 2.在没有泛型之前,所有的集合内都是一个Object的数组,所以经常会出现将狗插入猫的列表的情况,而且还能运行很久。
- 3.泛型的好处就是编译器帮你自动处理类型转换,而且如果出现狗插入猫的列表的时候,会在编译的时候就进行提示
- 4.List和List<Object>的区别在于:前者没有泛型检测,后者在检测的时候表明任何对象都能放入其中。另一个区别就是List=List<String>成立,List<Object>=List<String>会报错,所以如果使用List,那么就可能会出现把猫转化成狗的情况,而使用List<Object>在编译时就会报错
- 5.如果你对List中的元素毫不在乎,只是想使用他内部的元素的话,可以使用List<?>,这里可以达到和List相同的效果,但是并不能向其内部插入任何元素,也就杜绝了把狗插入到猫列表的情况
- 6.由于在运行时泛型会擦除,所以在使用instanceof的时候 使用泛型参数是多余的,所以在使用了instanceof之后需要将原生态类型转化成<?>,以避免狗插入猫列表的情况
- 2.消除非受检警告:
- 1.首先需要尽可能消除所有的关于泛型的警告,以保证自己的代码是类型安全的
- 2.如果实在有一些消除不了,那么在确保类型转换正确的情况下,可以使用@SuppressWarnings("unchecked")来禁止警告
- 3.在每条@SuppressWarnings("unchecked")旁都需要加条注释告诉使用者为什么是正确的
- 3.优先考虑泛型
- 4.优先考虑泛型方法
- 5.利用有限制通配符来提升api的灵活性:
- 1.参数化类型是不可变的,List<String>不是List<Object>的子类型,我们不能将List<Object>=List<String>,即不能使List<String>向上转型为List<Object>。
- 2.为了让List<String>可以向上转化,可以将List<Object>替换为List<?extends Object>,这里表示List中可以放一切继承于Object的对象,也就是说List<?extends Object>是List<String>的基类,这样一来就可以有List<?extends Object>=List<String>。
- 3.但是2中就出现了一个问题:我们在使用List<?extends Object>的插入方法的时候,并不知道究竟应该放入什么类型的对象,因为使用这个对象的时候String的类型信息已经被擦除了。所以为了不让把狗插入猫列表的情况出现。List<?extends Object>禁止插入任何对象。而获取其中的对象都是Object类型。
- 4.有了2,那么我们如何让List<String>向下转型呢?这时候要引入List<?super String>,这里表示,这个List里面可以放所有为String父类的对象,此时就可以有List<?super String>=List<String>。
- 5.4中也会有问题,当我们获取List<?super String>中的数据的时候,由于里面放的全是String父类的对象,所以这里所有的返回都是Object。而插入的时候因为每个空都能容纳String父类的对象,所以这个List最少也能插入String和其子类的对象。
- 6.总结一下:PECS,extends善于提供精确的对象,Super善于插入精确的对象
5.方法:
- 1.必要时进行保护性拷贝:在返回一个类中可变的组件的时候,如果我们不希望返回到客户端的组件的改变会影响到原来组件。那么我们需要进行保护性拷贝,即重新创建一个与内部组件信息相同的组件然后返回,这里可以用clone方法。我们可能会担心性能的问题,所以说如果实在不想多创建对象,那么就在文档中标明,不想该组件被改变
- 2.谨慎设计方法签名:
- 1.谨慎选择方法名
- 2.不过于追求复用或者拆分过多的方法:类中的方法数据应该适中,每一个方法需要有比较齐全的功能,只有某一个操作经常被用到才对其进行拆分。
- 3.避免过长参数:4个以内,
- 3.慎用重载:
- 1.永远不用导出两个相同数目参数的重载方法
- 2.对于可变参数,永远不要重载
- 4.返回长度为0的集合而不是null
- 5.为所有导出api写注释
6.将局部变量的作用域最小化
- 1.将局部变量的作用域最小化
- 1.在第一次使用它的时候声明
- 2.声明的时候需要包含一个初始化表达式,如果不能初始化,就应该推迟声明
- 3.使用for比while更好
- 2.foreach比传统循环好
- 3.需要精确答案避免使用float和double
- 1.float和double会产生类似0.399999999999999这样的数字,所以可以使用BigDecimal或int或long来代替
- 2.由于BigDecimal比较不方便和速度慢,所以可以使用int或long分成整数和小数来计算
- 4.基本类型优于封装类型:
- 1.如果一个Integer返回了null,然后又被自动拆箱,那么就会抛出异常
- 2.在一些循环中,如果使用了封装类型,就会返回装箱和拆箱影响性能
- 3.同一个值的封装类型,进行==比较的时候会返回false
- 5.尽量避免使用字符串:不推荐用字符串代替其他值类型
- 6.使用接口引用对象
- 7.接口优先于反射机制
- 1.性能损失
- 2.丧失了编译时检测的好处
- 3.代码笨重
7.异常
- 1.只针对异常情况使用异常:不能使用异常控制代码的正常流程,只能在出现意料之外的情况下使用异常
- 2.对可恢复的情况使用受检异常对编程错误使用运行时异常
- 1.一个api如果抛出异常,说明需要客户端在调用的时候自己处理并恢复,或者抛出异常
- 2.异常也是对象
- 3.优先使用标准异常:
- 1.IllegalArgumentException 传参不对
- 2.IllegalStateException 非法调用一个对象
- 3.IndexOutOfBoundsException 参数越界
- 4.ConcurrentModificationException 单线程类被并发修改
- 5.UnsupportedOperationException 不支持用户调用的方法
- 4.抛出与抽象对应的异常:
- 1.高层捕获了低层异常之后,需要抛出高层的异常
- 2.在构造高层异常的时候传入底层异常被称为异常链。
不贩卖焦虑,也不标题党。分享一些这个世界上有意思的事情。题材包括且不限于:科幻、科学、科技、互联网、程序员、计算机编程。下面是我的微信公众号:世界上有意思的事,干货多多等你来看。
网友评论