关于java:读完-Effective-Java我整理这-59-条技巧

36次阅读

共计 11679 个字符,预计需要花费 30 分钟才能阅读完成。

作者:Dong GuoChao\
链接:https://blog.dogchao.cn/?p=70

Effective Java,Java 名著,必读。

如果能严格听从本文的准则,以编写 API 的品质来奢求本人的代码,会大大晋升编码素质。

以下内容只记录了我本人整顿的货色,还是倡议读原文。为了聚焦知识点,一些阐明成心疏忽掉了。相当于是一篇摘要。


1、思考用动态工厂办法代替构造函数

例子:

Integer.valueOf(“1”)、Boolean.valueOf(“true”) 等。

劣势:

  • 可读性高(办法名)
  • 性能(不肯定创建对象)
  • 灵活性高

上面针对三个劣势进行一些解读。

可读性高

new Point(x,y) 和 Point.at(x,y)、Point.origin()。构造函数只能看出两个参数,不知其意,后者更易了解。

性能

在某些状况下,能够当时进行实例化一些对象,调用时间接调用即可,不须要进行扭转。比方,Boolean。

public final class Boolean implements Serializable, Comparable<Boolean> {
    // 事后设置两个对象
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

    public Boolean(boolean var1) {this.value = var1;}

    public Boolean(String var1) {this(parseBoolean(var1));
    }

    // 工厂办法
    public static Boolean valueOf(boolean var0) {return var0?TRUE:FALSE;    // 返回事后设置的对象,而不是创建对象}
    // 工厂办法
    public static Boolean valueOf(String var0) {return parseBoolean(var0)?TRUE:FALSE;
    }
    // ... other code
}

灵活性高

可依据具体情况,返回子类。相当于更弱小的工厂。间接从父类获取到子类。尤其实用于工具类(提供各种 API)。例子:Collections。

public class Collections {
    // 公有,典型工厂
    private Collections() {}

    public static final List EMPTY_LIST = new EmptyList<>();
    // 工厂办法
    public static final <T> List<T> emptyList() {return (List<T>) EMPTY_LIST;
    }
    private static class EmptyList<E> extends AbstractList<E> implements RandomAccess, Serializable {// code}

    // 工厂办法
    public static <E> List<E> checkedList(List<E> list, Class<E> type) {
    // 依据具体情况,获取相应子类
        return (list instanceof RandomAccess ?
                new CheckedRandomAccessList<>(list, type) :
                new CheckedList<>(list, type));
    }

    // 子类 1
    static class CheckedRandomAccessList<E> extends CheckedList<E> implements RandomAccess {CheckedRandomAccessList(List<E> list, Class<E> type) {super(list, type);
        }

        public List<E> subList(int fromIndex, int toIndex) {
            return new CheckedRandomAccessList<>(list.subList(fromIndex, toIndex), type);
        }
    }

    // 子类 2
    static class CheckedList<E> extends CheckedCollection<E> implements List<E> {// code}
}

2、多个构造函数时,思考应用结构器

尤其在进行 Android 开发时,会碰到这种状况。通常是一个对象,具备多个成员变量可能须要初始化,惯例办法,须要提供大量构造函数。例如:

// 非 Android 中的 AlertDialog,便于阐明问题,举个例子
public class AlertDialog {
    private int width;
    private int height;
    private String title;
    private String confirmText;
    private String denyText;

    private AlertDialog(){}
    public AlertDialog(int width, int height){    // 空白的正告框
         AlertDialog(width,height,null);
    }

    // 带题目的正告框
    public AlertDialog(int width, int height, String title){    // 带题目的正告框
        AlertDialog(width, height, title, "确定");
    }

    // 带题目的正告框,有确定按钮
    public AlertDialog(int width, int height, String title, String confirm){AlertDialog(width, height, title, confirm, null);
    }

    // 带题目的正告框,有确定按钮,勾销按钮
    public AlertDialog(int width, int height, String title, String confirm, String denyText){// set every thing.}
}

有多种款式的正告框,为了调用不便,必须提供多个构造函数。否则用户在调用时,只能应用残缺构造函数,容易犯错且无奈进行浏览。极不灵便。如果采纳另外一种形式,则能够解决,但会破费很多经验解决并发的状况:

// 非 Android 中的 AlertDialog,便于阐明问题,举个例子
public class AlertDialog {
    private int width;
    private int height;
    private String title;
    private String confirmText;
    private String denyText;

    public AlertDialog(){}// 空白的构造函数
   
    public void setWidth(int width){this.width = width;}
    // 其余 set 办法
}

调用时,通过调用各个参数的 set 办法进行设置。问题来了:

  1. 并发
  2. 无奈进行参数校验。例如,只创立了对象,设置了题目,却没有尺寸,相当于创立了一个没有尺寸的正告框。

在 Android 中,大量的控件都应用了结构器 Builder。

// 非 Android 中的 AlertDialog,便于阐明问题,举个例子
public class AlertDialog {
    private int width;
    private int height;
    private String title;
    private String confirmText;
    private String denyText;

    // private
    private AlertDialog(){}

    // Builder 中应用
    protected AlertDialog(Builder b){
        width = b.width;
        height = b.height;
        // .....
        if(width==0||height==0) throws new Exception("size must be set");
    }

    // 结构器
    public static class Builder {
        private int width;
        private int height;
        private String title;
        private String confirmText;
        private String denyText;

        // 留神:返回的 Builder。public Builder setTitle(String title) {
            this.title = title;
            return this;
        }
        // 其余 set...
        
        public AlertDialog build(){return AlertDialog(this);
        }
    }
}

于是,能够依据相应需要,进行相应设置,并在 AlertDialog 真正结构时,进行参数校验。就像这样:

new AlertDialog.Builder().setTitle("提醒").build();

上述例子,会胜利抛出异样。

3、用私有化结构器或者枚举型强化 Singleton。

Singleton 指最多会被实例化一次的类。通常状况下,以前的做法是没有问题的。然而在某些高级状况,通过应用反射的相干常识拜访 private 的构造函数,毁坏 Singleton。

public class Elvis{
    // 留神,私有 final 对象
    public static final Elvis INSTANCE = new Elvis();
    private Elvis(){}
}

另一种状况,在序列化的过程中,反序列化失去的对象曾经不再是以前的对象(毁坏了 Singleton),这种状况下,能够通过单元素枚举型解决。

public enum Elvis{
    INSTANCE;
    // some methods
}

4、通过私有化结构器强化不可实例化的能力

有一些工具类,仅仅是提供一些能力,本人自身不具备任何属性,所以,不适宜提供构造函数。然而,缺失构造函数编译器会主动增加上一个无参的结构器。所以,须要提供一个私有化的构造函数。为了避免在类外部误用,再加上一个保护措施和正文。

public class Util{private Util(){
        // 抛出异样,避免外部误调用
        throw new AssertionError();}
}

弊病是无奈对该类进行继承(子类会调用 super())。

5、防止创立不必要的对象

  • 对象的重用
  • 低廉的对象,应用对象池
  • 便宜的对象,慎用对象池。古代 JVM 对便宜对象的创立和销毁十分快,此时不适于应用对象池。

6、打消过期的对象援用

以下三种状况可能会造成内存泄露:

  • 本人治理的内存(数组长度减小后,pop 出的对象容易导致内存透露)
  • 缓存
  • 监听和回调

本人治理的内存

对于本人治理的内存要小心,比方:

public class Stack{private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    public Stack(){elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e){ensureCapacity();
        elements[size++]=e;    // allocate 新的堆内存和栈内存
    }

    public Object pop(){if(size==0) throw new EmptyStackException();
        return element[--size];    // pop 出 element[size],该对象不再无效。内存透露起因。}
    
    private void ensureCapacity(){if(elements.length==size)
            elements = Arrays.copyOf(elements, 2*size+1);
    }
}

弹出的对象不再无效,但 JVM 不晓得,所以会始终放弃该对象,造成内存泄露。

解决:

    public Object pop(){if(size==0) throw new EmptyStackException();
        elements[size] = null;        // 期待回收
        return element[--size];
    }

缓存

缓存的对象容易被程序员忘记,须要设置机制来保护缓存,例如不定期回收不再应用的缓存(应用定时器)。某些状况下,应用 WeakHashMap 能够达到缓存回收的效用。注,只有缓存依赖于外部环境,而不是依赖于值时,WeakHashMap 才无效。

监听或回调

应用监听和回调要记住勾销注册。确保回收的最好的实现是应用弱援用(weak reference),例如,只将他们保留成 WeakHashMap 的键。

7、防止显示调用 GC

Java 的 GC 有弱小的回收机制,能够简略的记住:不要显示调用 finalizer。能够这样了解:

jvm 是针对具体的硬件设计的,然而程序却不是针对具体硬件设计的,所以,java 代码无奈很好的解决 gc 问题(因为他具备平台差异化)。另外,finalizer 的性能开销也十分大,从这个角度上思考也不应该应用它。

8、笼罩 equals 办法请恪守通用约定

  • 自反性。x.equals(x) == true
  • 对称性。以后仅当 y.equals(x)==true 时,x.equals(y)==true
  • 传递性。if(x.equals(y)&&y.equals(z)),y.equals(z)==true
  • 一致性。
  • 非空性。x.equals(null)==false

9、笼罩 equals 办法时总要笼罩 hashCode

为了保障基于散列的汇合应用该类(HashMap、HashSet、HashTable),同时,也是 Object.hashCode 的通用约定,笼罩 equals 办法时,必须笼罩 hashCode。

10、始终笼罩 toString

Object 的 toString 办法的通用约定是该对象的形容。留神笼罩时,如果有格局,请备注或者严格依照格局返回。

11、审慎笼罩 clone

12、思考实现 Comparable 接口

13、使类和成员的可拜访性最小化

目标是解耦。简略来讲,应用修饰符的优先级从大到小,private>protected>default(缺省)>public。如果在设计之初,设计为 private 修饰符后,在之后的编码过程如果不得不扩充其作用于,应该先查看是否设计的确如此。

子类笼罩超类,不容许拜访级别低于超类的拜访级别。(超类的 protected,子类笼罩后不能改为 default)。

成员变量决不允许是私有的。一旦设置为私有,则放弃了对他解决的能力。这品种并不是线程平安的。即便是 final 的,也不容许。除非心愿通过 public static final 来裸露常量。成员变量总是须要应用 setter 和 getter 来保护。有一个例外:长度非零的数组。这是安全漏洞的一个本源。

// 安全漏洞!此处的数组,并不是不可变的
public static final Thing[] VALUES = {...}

改良:

private static final Thing[] PRIVATE_VALUES = {...}
// 此时获取到的才是“常量”public static final List<Thing> VALUS = 
    Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES))

另一种:

private static final Thing[] PRIVATE_VALUES = {...}
// 此时获取到的才是“常量”public static final Thing[] values(){return PRIVATE_VALUES.clone();
}

14、在私有类中应用拜访办法而非公有成员变量(相似 13)

15、使可变性最小化

16、复合优先于继承

继承有利于代码复用,然而尽可能不要进行跨包的继承。包内的继承是优良的设计形式,一个包里的文件处在同一个程序员的管制之下。然而继承有其局限性:子类依赖于超类。超类一旦产生更改,将可能毁坏子类。并且,如果超类是有缺点的,子类也会得“遗传病”。

复合,即不扩大已有的类,而是在的类中新增一个现有类的。相当于现有类作为一个组建存在于新类中。如此,将只会用到须要用到的货色,而不体现现有类所有的办法和成员变量。新类也能够称为“包装类”,也就是设计模式中的 Decorate 模式。

17、要么就为继承而设计,并提供文档阐明,要么就禁止继承

18、接口优于抽象类

19、接口只用于定义类型

20、类档次优先于标签类

21、用函数对象示意策略

函数参数能够传入相似 listener 的对象,目标是应用 listener 中的办法。如果应用匿名的参数,每一次调用会创立新的对象。能够将 listener 申明为成员变量,每次都复用同一个对象,并且能够应用动态域(static 变量)。比方 String 类的 CASE_INSENSITIVE_ORDER 域。

22、优先思考动态类成员

嵌套类的目标应该只是为了他的外围类提供服务,如果当前还可能用于其余环境中,则应该设计为顶层类。动态类相当于一个一般的外部类,只是恰好申明在了一个类外部。通常的用户是:Calculator.Operation.PLUS 等。和一般类的区别只是,在 PLUS 前,有了 2 个前缀,来表明其含意。而非动态类必须存在于外部类对象中。不要手动在内部创立一个外部非动态类对象,创立的过程是:instance.New MemberClass()。这十分奇怪。

如果成员类不须要拜访外围类,则须要增加 static,是他成为动态成员类,否则每个实例都将蕴含一个额定指向外围对象的援用。将会影响垃圾回收机制。

23、应指定泛型的具体类型,而不是间接应用原生类型。

例如,应该指定 List<E>,而不倡议间接应用 List。

24、打消非首检正告

在应用 IDE 进行编码时,弱小的 IDE 都会在你编码过程中提醒 warning,须要尽可能的打消 warning,至多,应该小心这些 warning。慎用 SuppresWarning,如果 IDE 提醒你能够通过增加该注解解决掉 warning,请不要那么做。如果切实要应用,请增加正文阐明起因。

25、列表优先于数组

类比泛型,数组是有肯定缺点的。List<SuperClass> 和 List<SubClass> 是没有关系的,而 Sub[] 是 Super[] 的子类。

// Fails at runtime
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in";       // throw exception

// won't compile
List<Object> ol = new ArrayList<Long>();   // Incompatible types
ol.add("I don't fit in");

从代码中能够看到,应用泛型,会提前发现错误。

26、优先思考泛型

27、优先思考泛型办法

28、利用有限度通配符来晋升 API 的灵活性

PECS,producer-extends,consumer-super。

//public class Stack<E>{//    public Stack();
//    public void push(E e);
//    public E pop();
//    public boolean isEmpty();
//}

public void pushAll(Iterator<? extends E> src){for(E e : src)
        push(e);
}

public void popAll(Collection<? super E> dst){while(!isEmpty()){dst.add(pop());
    }
}

// Get and Put Principle

所有 comparable 和 comparator 都是消费者(Consumer)。

29、优先思考类型平安的异构容器

30、用 enum 代替 int 常量

public enum Apple {FUJI, PIPPIN, GRANNY_SMITH}
public enum Orange {NAVEL, TEMPLE, BLOOD}

枚举型在 java 中十分弱小,当须要一组固定常量时,应用 enum 比 int 好很多。比方代码可读性,安全性等。

31、enum 用实例域代替序数

// bad solution
public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET, 
    SEXTET, SEPTET, OCTET, NONET, DECTET;

    public int numberOfMusicians() { return ordinal() + 1; }
}
// 

// improvement
public enum Ensemble {SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), 
    SEXTET(6), SEPTET(7), OCTET(8), NONET(9), DECTET(10), TRIPLE_QUARTET(12);

    private final int numberOfMusicians;
    Ensemble(int size) {this.numberOfMusicians = size;}
    public int numberOfMusicians() { return numberOfMusicians;}
}

永远不要像第一种的形式,利用序数拜访 enum,须要在构造函数中应用参数来初始化。

32、用 EnumSet 代替位域

public class Text{
    public static final int STYLE_BOLD                     = 1 << 0;    // 1
    public static final int STYLE_ITALIC                    = 1 << 1;    // 2
    public static final int STYLE_UNDERLINE          = 1 << 2;    // 4
    public static final int STYLE_STRIKETHROUGH = 1 << 3;    // 8

    public void applyStyles(int styles){// ...}
}



// 
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

以上叫做位图法,然而有更好的计划来传递多组常量——EnumSet。

public class Text{public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}

    // 留神此处,应用的是 Set 而不是 EnumSet
    public void applyStyles(Set<Style> styles){// ...}
}



// 
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

33、用 EnumMap 代替序数索引

任何时候都不要应用 enum 的 ordinal() 办法。

34、用接口模仿可伸缩的枚举

35、注解优先于命名模式

36、保持应用 Override 注解

38、查看参数的有效性

私有办法查看参数,参数异样须要跑出 Exception。公有办法利用断言 assertion 查看参数。

39、必要时进行保护性拷贝

假如类的客户端会尽其所能来毁坏这个类的约束条件,因而你必须保护性的设计程序。以下是一个不可变类的设计。

public Period(Date start, Date end){this.start  = new Date(start);        // 应用了值的拷贝,没有应用原对象(指针)this.end = new Date(end);
    if(this.start.compareTo(this.end)>0)
        throw new IllegalArgumentException(start + "after" + end)
}

留神:保护性拷贝是在查看参数之前进行的,避免多线程的影响。不要应用 clone 办法进行保护性拷贝。

以上办法进攻了传入参数的批改,然而对于 get 办法获取到的对象,依然能够被批改,通过以下办法能够避免这种攻打。

public Date start(){return new Date(start);
}

public Date end(){return new Date(end);
}

40、审慎设计办法签名

41、慎用重载

42、慎用可变参数

43、返回 0 长度的数组或者汇合,而不是 null

null 个别用于示意没有被初始化或解决,如果办法返回了 null,则须要在下层做更多的解决,以避免 NPE。

44、为所有导出的 API 元素编写文档正文

正确的 javadoc 文档,须要每个被导出的类、接口、结构器、办法和域之前减少文档正文。正文应该是对实现通明的,只须要简洁的形容它和客户端之间的约定。并且,还应该附上该办法的副作用。

45、将局部变量的作用域最小化

46、for-each 优先于 for 循环

for-each 躲避掉了 for 循环的 index 变量的援用,通常来说它是不必要的——会减少引入谬误的危险,并且危险一旦产生,很难被发现。不过有三种状况下,无奈应用 for-each(注:在 jdk1.8 中曾经很好的解决了这些问题)。

  • 过滤
  • 转换
  • 平行迭代

48、如果须要准确的答案,请防止应用 float 和 double

float 和 double 是执行的二进制浮点运算,目标是在宽泛数值范畴上应用准确的疾速近似计算而设计的。然而他们并没有提供齐全准确的计算(理论利用中,常常会碰到呈现 x.99999 等后果)。尤其是,在进行货币计算时,他们并不实用。比方:

System.out.println(1.03-.42);

失去的后果将是:0.610000000001。

为了解决这个问题,须要应用 BigDecimal。然而这也有一些问题,绝对于一般的运算,它显得更加麻烦,而且也更慢。通常来说后一个毛病能够疏忽,然而前者可能会让人很不难受。有一种做法是将须要解决的数值 *10(或更多),应用 int 进行计算,不过须要你本人解决四舍五入等操作。

49、根本类型优先于装箱根本类型

  • 根本类型只有值,装箱类具备与他们值不同的同一性。
  • 根本类型只有性能齐备的值,装箱类还具备非性能值:null。所以你可能会碰到 NPE
  • 根本类型省空间省工夫

50、如果有更准确的类型,请防止应用字符串

  • 字符串不适宜代替其余值的类型。例如:int,boolean 等
  • 不适宜代替枚举类型(第 30 条)
  • 不适宜汇集类型

51、当心字符串连贯的性能

操作符“+”能够将多个字符串进行连贯。然而在大规模应用“+”的状况下,连贯 n 个字符串的开销是 n 的平房级工夫。这是因为字符串的不可变性导致的。在这种状况下请应用 StringBuilder 进行连贯。

52、通过接口援用对象

53、接口优先于反射机制

应用反射机制会带来以下的问题:

  • 丢失了编译期类型查看
  • 代码蠢笨简短
  • 性能损失

反射基本上只适宜用在编写组件时、代码分析器、RPC 等场景下应用。在应用反射机制时,如果可能,尽可能只通过反射机制实例化对象,而拜访办法时,应用已知的接口或者超类。

54、审慎应用 JNI

55、审慎进行优化

很多计算上的差错都被归咎于效率(没有必要达到的效率),而不是任何其余起因——甚至包含自觉的做傻事。

——William A. Wulf

不要去计较效率上的一些小小的得失,在 97% 的状况下,不成熟的优化才是所有问题的本源。

——Donald E. Knuth

在优化方面,咱们应该恪守两条规定:

规定 1: 不要进行优化。

规定 2(仅针对专家):还是不要进行优化——也就是说,在你还没有相对清晰的优化计划前,请不要进行优化。

——M. A. Jackson

这些格言比 java 的呈现还要早 20 年。他们讲述了一个对于优化的粗浅事实:优化的弊大于利。

要致力编写好的程序,而不是快的程序。低耦合的重要性远远大于性能。当程序编写得足够低耦合后,通过工具发现了性能瓶颈的代码块,才能够保障对其的批改不影响任何外部环境。

56、恪守广泛的命名规定

57、只针对异常情况才应用异样

不要尝试通过异样机制来做失常代码应该做的事件,比方,查看数组下标。

jvm 很少对异样进行优化,因为它只用于不失常的状况。并且,如果你将代码放入 try-catch 代码块,jvm 就丢失了原本能够对它进行的优化。

58、对于可复原的状况应用受检异样,对于编程谬误的状况应用运行时异样

  • 如果冀望调用者适当的复原,则须要应用受检异样,强制调用者食用 try-catch 代码块,或者将他们抛出去
  • 当调用产生前提违例——违反约定的状况时,应用运行时异样,这个时候程序曾经无奈再执行上来了。例如调用数组的 - 1 索引。

58、防止不必要的受检异样

最初,关注公众号 Java 技术栈,在后盾回复:面试,能够获取我整顿的 Java 系列面试题和答案,十分齐全。
近期热文举荐:

1.600+ 道 Java 面试题及答案整顿 (2021 最新版)

2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!

3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0