Java中不可或缺的50个小技巧,好用!

《Effective JavaJava》名著,必读。如果能严格遵从本文的原则,以编写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域。

关注公众号【程序员白楠楠】获取2020年末总结面试资料一套!

考虑静态类成员

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

如果成员类不需要访问外围类,则需要添加static,是他成为静态成员类,否则每个实例都将包含一个额外指向外围对象的引用。将会影响垃圾回收机制。

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

例如,应该指定List<E>,而不建议直接使用List。

24、消除非首检警告

在使用IDE进行编码时,强大的IDE都会在你编码过程中提示warning,需要尽可能的消除warning,至少,应该小心这些warning。慎用SuppresWarning,如果IDE提示你可以通过添加该注解解决掉warning,请不要那么做。如果实在要使用,请添加注释说明原因。

25、列表优先于数组

类比泛型,数组是有一定缺陷的。List和List是没有关系的,而Sub[]是Super[]的子类。

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

// won't compileList<Object> ol = new ArrayList<Long>();   // Incompatible typesol.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 solutionpublic enum Ensemble {    SOLO, DUET, TRIO, QUARTET, QUINTET,     SEXTET, SEPTET, OCTET, NONET, DECTET;

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

// improvementpublic 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注解

37、检查参数的有效性

公有方法检查参数,参数异常需要跑出Exception。私有方法利用断言assertion检查参数。

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

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

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);}

39、谨慎设计方法签名

40、慎用重载

41、慎用可变参数

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

null一般用于表示没有被初始化或处理,如果方法返回了null,则需要在上层做更多的处理,以防止NPE。

43、为所有导出的API元素编写文档注释

正确的javadoc文档,需要每个被导出的类、接口、构造器、方法和域之前增加文档注释。注释应该是对实现透明的,只需要简洁的描述它和客户端之间的约定。并且,还应该附上该方法的副作用。

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

45、for-each优先于for循环

for-each规避掉了for循环的index变量的引用,通常来说它是不必要的——会增加引入错误的风险,并且风险一旦发生,很难被发现。不过有三种情况下,无法使用for-each(注:在jdk1.8中已经很好的解决了这些问题)。

  • 过滤
  • 转换
  • 平行迭代

46、如果需要精确的答案,请避免使用float和double

float和double是执行的二进制浮点运算,目的是在广泛数值范围上使用精确的快速近似计算而设计的。然而他们并没有提供完全精确的计算(实际应用中,经常会碰到出现x.99999等结果)。尤其是,在进行货币计算时,他们并不适用。比如:

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

得到的结果将是:0.610000000001。

为了解决这个问题,需要使用BigDecimal。然而这也有一些问题,相对于普通的运算,它显得更加麻烦,而且也更慢。通常来说后一个缺点可以忽略,但是前者可能会让人很不舒服。有一种做法是将需要处理的数值*10(或更多),使用int进行计算,不过需要你自己处理四舍五入等操作。

47、基本类型优先于装箱基本类型

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

48、如果有更精确的类型,请避免使用字符串

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

49、当心字符串连接的性能

操作符“+”可以将多个字符串进行连接。但是在大规模使用“+”的情况下,连接n个字符串的开销是n的平房级时间。这是由于字符串的不可变性导致的。在这种情况下请使用StringBuilder进行连接。

50、通过接口引用对象

最后,小编总结了2020面试题,这份面试题的包含的模块分为19个模块,分别是: Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM 。

关注我的公众号:程序员白楠楠,获取上述资料。

(0)

相关推荐

  • 怎么一本正经地秀技

    前言 修饰符怎么使用也是Java基础中比较重要的知识点,彻底理解了之后,后面学习更高深的东西才能得心应手.今天,以修饰符中比较常见的final为切入点,来谈谈final的使用的奇淫技巧以及一些相关的知 ...

  • 设计模式-单例模式

    单例模式 优点 由于单例模式在内存中只有一个实例,减少了内存开支,特别是 一个对象需要频繁地创建.销毁时,而且创建或销毁时性能又无法优 化,单例模式的优势就非常明显. 由于单例模式只生成一个实例,所以 ...

  • Java基础之:OOP——代码块

    代码块又称初始化块,是类中的成员(即类的一部分),类似于方法,将逻辑语句封装在方法体中,通过{}包围起来,也是通过调用执行. 但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象 ...

  • 微信中4个冷门的小技巧,每个都超实用,手把手教你学会

    一.朋友圈快速搜索 4.打开朋友圈后在搜索栏输入要搜索的内容 5.比如说:"大家",输入后我们按下回车键 6.这时就会列出包含"大家"关键字的所有内容 有了这个 ...

  • 涨姿势!让你唱歌水平暴涨50%的小技巧

    唱歌宝典 搜索歌谱 歌曲点播 声乐万能表 合唱大全 听歌识谱 你有没有这种经历,在唱歌的过程中会遇到各种各样的问题,比如说唱歌总是跑调,高音不上去低音下不来,唱一会歌嗓子就疼. 实际上,出现这些问题都 ...

  • PLC编程中使用定时器的一些小技巧

    相信大家在PLC编程中常会用到定时器,定时器指令也是比较常用的指令,一般分为接通延时定时器.断开延时定时器.脉冲定时器等,相信大家对于指令也已经很熟悉,在本文中就不具体介绍定时器指令了. 本文重点介绍 ...

  • 如何提取PDF文件中的表格?一个小技巧,2分钟就学会,有用

    MID YEAR SUMMARY 老铁们,今天分享一个比较实用的小技巧,"如何提取PDF文件中的表格?" 方法很简单,大概2分钟就学会,请耐心往下看,相信对你以后工作有帮助. 01 ...

  • [快乐生活]告诉你生活中一些非常实用的小技巧

    生活里你会遇到很多小麻烦,让我们有点小烦恼,其实一些很简单的方法就可以帮我们解决. 切洋葱时嚼口香糖可以避免泪流满面. 自己钉钉子,爸爸再也不用担心我钉到手. 总是找不到胶带的头在哪儿?用曲别针别住! ...

  • 健身过程中,学会几个小技巧,能提高增肌减脂效率!

    随着健身文化的流行,越来越多的人开始理解健身,并且慢慢的走上了健身行列.健身的确是一项可以强身健体的运动,不仅能够有效增肌减脂,还能让身体越来越健康. 做每件事情都有技巧,健身也不例外,那么我们在健身 ...

  • 爱情长久保鲜50个小技巧,你来说说看?

    #创作热词# #搜索热词任务# 当今时代发展迅速,城市化进程发展加快.改革开放40年使中国赶上了欧美300年的发展历程.在当前男多女少的时代,工作竞争压力增大,婚姻选择竞争激烈.人们的思想观念解放了, ...

  • 过年了,煮饺子中的化学知识 包饺子小技巧一条龙知识点,让你变成年夜饭的高手!

    过年了,煮饺子中的化学知识 包饺子小技巧一条龙知识点,让你变成年夜饭的高手!

  • 看穿别人心理的50条小技巧

    -- 现在社会的压力本来就很大,千万别再给自己施加其他方面的压力了,学学心理学,你就知道为什么你的内心总是能让别人看透了.其实"看穿"这个词太有攻击性,不如说学了心理学会使我们更理 ...