最近在看《Effective Java》这本书,顺便就记录一些笔记,记录一下书中的一些知识点以及对知识点的总结。一般情况会记录所有的知识点,但是知识点太过简单或者无归纳点总结的就不做详细记录。书中分为11章78条知识点,暂定分为3篇blog来记录,随后视情况而定。
一、一些简单知识
1、Java支持四种类型:接口(interface)、类(class)、数组(array)、基本类型(primitive)。前三种类型通常被称为引用类型,类实例和数组是对象;二基本类型的值则不是对象。
Java1.5中增加了两个新的引用类型:一种新的类称为枚举类型,一种新的是注解类型。
2、创建对象的代价非常昂贵,我们应该尽可能的避免创建对象。这句话是不准确的,相反的理解应该是,小对象的构造器只做少量的显示工作,所以小对象的创建和回收也是对于jvm来说非常廉价的。小的对象能够提升程序的清晰性、简洁性和功能性。通常是个好事。
二、创建和销毁对象
1、静态工厂模式
2、建造者模式(Builder)
用于一个类有很复杂的构造函数,需要传递的 参数比较多,或者是利用空构造函数new出来一个对象,但是需要给它参数设置起来比较麻烦,可以使用Builder来替代传递参数的构造函数,简化了代码,也确保参数都能准确的传递
eg:
MClass mobj = new MClass.Builder(x,y)
.setArgA(a)
.setArgB(b)
.setArgC(c)
.setArgD(d)
.build();
3、枚举或私有构造器来实现单例
(1)用class
注意几点:
私有构造函数
私有成员变量
开放同步的getInstance方法(保证线程安全)
(2)用枚举
一个简单的例子(应该是单例的最佳方法)
public enum MSingleton {
ONE;
private String anotherField = "aa";
MSingleton() {
}
public void splitAlipay(Person p) {
System.out.println(anotherField+"-Person->"+p.toString());
}
}
4、通过私有构造函数来处理一些不希望被实例化的类
一些工具类,工厂类是不希望被实例化或者继承的,就可以定义一个私有的构造函数,而且在方法中throw exception,这样就达到不被实例化或者继承
eg:
public class Gs {
private Gs(){
throw new AssertionError();
}
public static void doA(){
}
}
5、避免创建不必要的对象
(1)String的用法,避免使用
String s = "ssss";
(2)重用已知不会被修改的对象
eg:原例,每次调用isBadyHefa都会创建不必要的对象
public class Person {
private Date birthday;
public boolean isBadyHefa(){
Calendar gmCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmCal.set(1949, Calendar.JANUARY, 1, 13, 44, 11);
Date timeStart = gmCal.getTime();
gmCal.set(1965, Calendar.JANUARY, 1, 13, 44, 11);
Date timeEnd = gmCal.getTime();
return birthday.compareTo(timeStart)>0 && birthday.compareTo(timeEnd)<0;
}
}
改良之后版本
public class Person {
private Date birthday;
private final static Date timeStart;
private final static Date timeEnd;
static{
Calendar gmCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmCal.set(1949, Calendar.JANUARY, 1, 13, 44, 11);
timeStart = gmCal.getTime();
gmCal.set(1965, Calendar.JANUARY, 1, 13, 44, 11);
timeEnd = gmCal.getTime();
}
public boolean isBadyHefa(){
return birthday.compareTo(timeStart)>0 && birthday.compareTo(timeEnd)<0;
}
}
(3)基本类型和装箱类,要优先使用基本类型,注意的一些自动装箱
public void sumplus(){
Long sum = 0L;//将sum定义为Long,执行完成之后会创建2的31次方个Long对象,这些都是没必要的,所以应该将sum定义为long,
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
6、清理过期的对象,以免内存泄露
1、清理一些集合对对象的过期引用
过期引用意思是这些对象在使用之后永远也不会再使用,但是还保持着对这些对象的引用
eg:
private Object[] elements;
private int size = 0;
private final int DEFAULT_SIZE = 16;
public Gs() {
super();
elements = new Object[DEFAULT_SIZE];
}
public void initEelement(){//init……}
}
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
return elements[--size];
}
return elements[–size];//这里会造成内存泄露,size –了之后,后面的对象永远不会被用到,但是数组还保持着对这些对象的引用
所以pop方法应该改成:
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;//释放对这个对象的引用
return result;
}
清空过期引用的另一个好处是,如果它们后面被错误的引用的时候,就立刻跑出NullPointException,不是让程序错误的运行下去,这样就能保证程序正确的执行。
2、缓存处理
一些数据缓存到内存中,很容易被遗忘忽略,长时间以后就会造成问题。所以需要将缓存数据使用弱引用或者软引用来缓存,这样,当system资源紧张的时候就可以清理这些缓存,避免造成内存泄露和溢出。
3、监听器和回调
很多api提供了注册回调的方法,却没有提供取消注册的方法,这样的话,一直长时间大批量的注册回调函数,这些监听器和回调对象就会堆积,而且是强引用形式的,就会造成内存泄露。
解决方法:
(1)提供取消注册方法,并在框架层控制,有注册的操作,在正常情况下必须有取消注册的操作
(2)对这些监听器使用弱引用或软引用的方式来保持
7、慎用finalize方法
尽量使用try-catch-finally来搞定
三、Common方法
8、override equals方法时请遵守通用约定
- 自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
- 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
- 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
- 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
- 对于任何非空引用值 x,x.equals(null) 都应返回 false
9、覆盖equals时总要覆盖hashcode
- 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
- 如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
- 如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。
ps:使用奇素数来当做hashcode的乘数因子
(2n−1)∗num=(i<<n)−num
eg:31∗num=(num<<5)−num
10、始终要覆盖toString
目的是为了明确信息,方便数据解析
11、谨慎的覆盖clone
implements Cloneable,要首先调用super.clone,class内部有集合对象的话,还需要deepCopy,使用迭代来进行copy
12、选择性的实现Comparable接口
有需要排序,判断大小功能的类,可以实现Comparable;不实现Comparable也可以使用Comparator来实现比较的需求
四、类和接口
13、使类和成员的可访问性最小化
尽可能的使每个类或者成员不被外界修改。
四种访问级别:private
public static final Thing[] VALUES = {……};
这种情况的解决方法:
1、将数组私有化,提供一个不可变列表
privatestatic final Thing[] VALUES = {……};
public static final List<Thine> VALUE_LIST = Collections.unmodifiableList(Arrays.asList(VALUES));
2、将数组私有化,提供一个公共的方法,返回数组的拷贝
private static final Thing[] VALUES = {……};
public static final Thing[] values(){
return VALUES.clone()
}
在编写api的时候,应该尽可能的降低可访问性,防止把任何散乱的类、接口和成员变量变成api的一部分;要确保公有静态final域所引用的对象都是不可变的。
14、公有类不应该直接暴漏数据域
15、使可变性最小
不可变类是实例不能被修改的类,每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的生命周期内固定不变。java中包含了许多,其中有String、基本类型的包装类、BigInteger、BigDecimal。
不可变类要遵循一下规则:
- 1、不要提供任何会修改对象状态的方法
- 2、保证类不会被扩展。一般做法使这个类成为final的,或者私有化构造函数然后添加公有的静态工厂来代替公有的构造函数。
- 3、使所有的域都是final的
- 4、使所有的域都成为私有的
- 5、确保对于任何可变组件的互斥访问
不可变类对象本质上是线程安全的,不要求同步。
16、复合优于继承,即使用包装类
一些已经编写好的功能类A,不易于扩展,但是又需要更多的功能的时候,一般会选择继承,但是继承会带来子类重写父类中的方法,进而有可能造成不可预料的问题。这个时候就可以使用复合类,定义一个新的类B,将原功能类A的实例通过构造函数或其他方式传递给新的类B,然后在B中定义出和功能类同同样的方法,但是方法内部的实现去调用A实例的对应方法,然后在B类中添加一些新的功能方法。这样就可以避免一些不必要的麻烦,而且更加灵活和健壮。
eg:
class A{//A类系统API或者平台sdk等不易扩展或修改的类
public int function1(){……}
public String function2(){……}
public void function3(){……}
}
class B{
A a;
B(A a){this.a = a}
public int function1(){return a.function1()}
public String function2(){return a.function2()}
public void function3(){a.function3()}
}
方法写起来比较琐碎,可以通过接口去实现。
17、要么为继承而设计,并提供文档说明,要么就禁止继承
用文档来说明这个类是可覆盖的方法。还需遵守约束:构造器不能调用可被覆盖的方法。
18、接口优于抽象类
19、接口只用于定义类型
避免使用常量接口,常量接口只包含final的常量域;但是目前很多的代码中都是使用常量类,如果大量利用工具类导出的常量,可以通过利用静态导入机制,避免使用类名修饰常量名(静态导入机制是在Java1.5之后才引入的)
import static com.xx.XXConstants.*;
public class Test{
public void dosomething(){
System.out.println(DEFUALT_NUM+10);//DEFUALT_NUM是在接口XXConstants中定义的常量
}
}
20、类层次优于标签类
以下面的这个类为例,就是一个典型的标签类,这样的类有比较冗余、混乱、易出错、效率低下、浪费内存空间。
public class Figure {
enum Shape{RECTANGLE,CIRCLE};
Shape shape;
double lenght;
double width;
double radius;
Figure(double radius){
shape = Shape.CIRCLE;
this.radius = radius;
}
Figure(double lenght,double width){
shape = Shape.RECTANGLE;
this.lenght = lenght;
this.width = width;
}
public double area(){
switch (shape) {
case RECTANGLE:
return lenght * width;
case CIRCLE:
return Math.PI * (radius* radius);
default:
throw new AssertionError();
}
}
}
利用类层次就可以解决标签类代理的问题,而且可以细分不同的功能,层次会分明,且易于扩展。
为了将标签类转变为层次类,首先要为标签类中的每个方法都定义一个包含抽象方法的抽象类,每个方法的行为都依赖于标签类。接下来,为每种原始标签类的功能都定义一个具体子类。
public abstract class Figure {
public abstract double area();
}
public class Rectangle extends Figure {
double lenght;
double width;
public Rectangle(double lenght, double width) {
this.lenght = lenght;
this.width = width;
}
@Override
public double area() {
return lenght * width;
}
}
public class Circle extends Figure {
double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * (radius * radius);
}
}
21、用函数对象表示策略
函数指针的主要用途就是实现策略模式,但是java中不支持函数指针,所以需要声明接口来表示该策略或者内部类来实现。比如Comparator
22、优先考虑静态成员类
嵌套类是指被定义在另一个类的内部的类。嵌套类的存在的目的应该只是为了他的outer类提供服务。如果嵌套类将来可能用于其他某个环境中,它就应该是普通类。嵌套类有四种:静态内部类、非静态内部类、匿名类、和局部类。后三种都被称为内部类。
- 静态内部类,常见的用法是作为公有类的辅助类,仅当它和公有类一起使用的时候才有意义。如果声明成员类不要求访问外围实例,那么就需要使之成为静态内部类
class MyAdapter extends BaseAdapter{
……//会用到ViewHolder
static class ViewHolder{……//不会用到MyAdapter
}
}
- 非静态内部类,语法上来说和静态内部类的区别就是少了static修饰符,但是却有很大不同,非静态成员类的没一个实例都隐含着与外围类的一个外围实例相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this构造获取外围实例的引用。如果嵌套类的实例可以在她外围类的实例之外独立存在,这个嵌套类就必须是静态成员类。当非静态成员类的实例被创建的时候,它和外围实例直接的关联关系也就随之被建立起来,而且这种关系以后不能被修改。
- 匿名类,没有名字,匿名类可以出现在任何允许存在表达式的地方
,当且仅当匿名类出现在非静态的环境中时,它才有外围实例。出现在静态的环境中,也不可能拥有任何静态成员。
new Thread(new Runnable()){
@Override
public void run(){
……
}
}
- 局部类,必须非常精短
public double area() {
class MM{
public int a;
public void print(){
System.out.println(a);
}
}
new MM().print();
return Math.PI * (radius * radius);
}
第一篇就先到这里,后面还有7章内容。以上内容有书中原内容,也有个人的理解,当然是以原书内容为主,如果有偏差,请联系我或者评论即可。