快速带你看完《Effective Java》—— 创建和销毁对象篇

多参数构造函数:

假设一个类的构造函数有多个参数时,如果想要创建实例时,就会出现这样的代码:

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

这个调用通常需要许多你根本不想设置的参数,但却不得不设置,比如第三个传入的0。这种方式是可行的,但如果有很多参数的时候,客户端代码会很难编写,程序员会因为要避免传错参而小心翼翼。

记得之前在公司里面用了其他人封装的日志工具类LogUtil,里面的构造函数就是这样的风格~~,不仅传的参数多,还提供了各种各样不同参数组合的构造函数,最后当然逃不过被重写的命运 : )

JavaBeans模式:

NutritionFacts cocaCola = new NutritionFacts();

cocaCola.setServingSize(240);

cocaCola.setServings(8);

cocaCola.setCalories(100);

cocaCola.setSodium(35);

cocaCola.setCarbohydrate(27);

这种模式下先调用无参构造器创建对象,再调用setter方法来设置每个必要的参数,这样的代码可读性要更高,但却有严重的缺点:

  • 创建对象的过程不是原子的,于是在高并发场景下可能产生不一致的对象

  • JavaBeans模式无法将类做成不可变的,因为调用了setter方法本身就意味着“可变”了

建造者(Builder)模式:

Mybatis中的SqlSessionFactoryBuilder就是建造者模式的体现,在类的定义里面,builder通常是类的静态成员类,并且调用无参的build方法生成的通常是不可变的对象,使用示例:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();

Builder模式十分灵活,可以利用单个builder构建多个对象;builder的参数可以在调用build方法来创建对象期间进行调整,甚至可以填充某些域,比如创建新对象是自动增加序号值。

3 使用private构造器或枚举类型强化Singleton属性


相信接触过设计模式的同学都知道,单例的一种经典实现方式就是private的构造函数,但直到我看到这一章节内容的时候,才突然顿悟原来枚举类型也是可以强化单例属性的,自愧个人的融会贯通能力还有待加强。

实现单例方式一:

// Singleton with public final field

public class Elvis {

public static final Elvis INSTANCE= new Elvis();

private Elvis() { … . }

}

这种实现方式有一个缺点就是可以借助反射机制里的AccessibleObject.setAccessible来强制修改构造函数成为public的

实现单例方式二:

// Singleton with static factory

public class Elvis {

private static final Elvis INSTANζE = new Elvis();

private Elvis() { … }

public static Elvis getInstance() {return INSTANCE; }

}

在实际抉择中,如果用到了以下某种优势,则优先考虑第二种实现,否则考虑第一种单例实现:

  1. 可以很容易被修改成非单例的(修改getInstance的返回语句即可)

  2. 可以使用Elvis::instance语法

此外,为了防止每次序列化和反序列化都创建一个新实例而破坏了单例特性,需要在Elvis类中添加一个方法:

private Object readResolve () {

return INSTANCE;

}

实现单例方式三:

这种方式也是我原先没有想到的,声明包含单个元素的枚举类型来实现:

public enum Elvis {

INSTANCE;

public void leaveTheBuilding() { … . }

}

其中Elvis.INSTANCE可以获得单例,Elvis.INSTANCE.leaveTheBuilding();调方法。

这种方式也是作者极力推荐的一种方式,无偿地提供了序列化机制,绝对防止多次实例化。单元素的枚举类型经常成为实现Singleton的最佳方法。

4 使用privete的构造函数强化不可实例化的能力


这一条主要讨论的是在编写工具类时,往往这些类是不希望其被实例化出来的,例如java.lang.Math,一个好的做法就是手动编写一个private的构造函数。

缺点是这个类就不能被子类化了,因为子类没有可以调用的超类构造器了。

5 引用资源时应优先考虑依赖注入


举个例子说明引用资源:拼写检查器需要依赖词典,这个“词典”就是所谓的资源。

有两种常见的引用资源的方式:

  1. 封装成静态工具类

private static final Lexicon dictionary;

public static boolean isValid(String word){…};

  1. 设计成单例类

private 构造函数;

private final Lexicon dictionary;

public boolean isValid(String word){…};

实际上,上面两种实现方式是错误的,因为它们都假定了只有一本词典可用,现实生活里每一种语言都需要自己的词典。

通过上面的讨论,我们可以用一个最简单的思路去解这个问题:每创建一个新实例时,都将其依赖的资源传到构造器里即可。

public class SpellChecke {

private final Lexicon dictionary;

public SpellChecker(Lexicon dictionary) {…}

public boolean isValid(String word) {…}

}

其实这就是依赖注入的一种形式。

依赖注入的一个变体是将资源工厂(factory)作为参数传给构造器,例如:

Mosaic create(Supplier<? extends Tile> tileFactory){...}

对了,Spring就是一种经典的依赖注入框架!

6 避免创建不必要的对象


从字面意思上来看,大家肯定都知道创建不必要的对象是错误的做法。但这一节其实主要是提醒我们避免无意识的创建不必要对象的代码写法。

例1:

String s = new String(“abc”);

是错误的写法,正确的写法应该是:

String s = “abc”;

原因是第一种写法每次被执行的时候都会创建一个新的String实例,但这些全都是重复的!

例2:

我们要优先使用静态工厂方法而不是构造器来避免创建不必要的对象,如Boolean.valueOf(String)总是要优先于构造器Boolean(String)使用。因为构造器每次被调用都会创建一个新对象,静态工厂不这样。

例3:

创建成本昂贵的对象时,应该将其缓存起来。

例如正则表达式匹配的代码中,String.matches方法内部创建了一个Pattern实例,这个创建的成本很高,因为需要将正则表达式编译成有限状态机,所以应该将其缓存起来:

public class RomanNumerals {

private static final Pattern ID = Pattern.compile(“\d{15}$)|(^\d{18}$)|(\d{17}(\d|X|x)$”);

static boolean isRomanNumeral(String s){

return ID.matcher(s).matches();

}

}

这样一来,每次调用isRomanNumeral时都会重用同一个ID实例

例4:

上面的Pattern实例是不变的,但在某些场景下实例是可变的,这时就可以考虑适配器。适配器是这样一个对象:它将功能委托给一个后备对象,为后备对象提供一个替代前面功能的接口。

例如Map接口的KeySet方法,每次调用返回的都是同一个Set实例,虽然Set实例是可变的,但其中一个变化时其他的也会跟着变,因为他们本身就是一个。

例5:

优先使用基本类型而不是装箱类型,原因在于下面这个例子:

private static long sum(){

Long sum = 0L;

for(long i = 0; i <= Integer.MAX_VALUE; i ++)

sum += i;

return sum;

}

这段程序执行起来没有任何问题,但实际情况会慢一点,因为sum的类型是Long而不是long,所以程序构造了大约2^31个Long实例。

这一点在我记忆中和工作里的要求不一致,为此我专门去翻阅了阿里巴巴Java开发手册,里面是这样描写的:

在这里插入图片描述

可见公司在这个问题的考虑上是业务优先了,所以小伙伴们可以斟酌使用时的取舍,我个人还是推荐使用包装类型的。

避免一个误区:

不要看完这一章节就陷入了创建对象的代价非常昂贵的逻辑怪圈里去了,反之维护自己的对象池来避免创建对象是一种错误的做法。因为现代JVM的实现里有高度优化的垃圾收集器,其性能很容易就超过了轻量级对象池的性能。

一个正确的示例是数据库连接池,因为建立一个数据库的连接是非常昂贵的。

7 消除过期的对象引用


这一条建议主要讲的是要规避内存泄漏。因为像Java这种具有垃圾回收机制的语言,内存泄漏一般都是比较隐蔽的。

例如:

package com.wjw;

import java.util.Arrays;

import java.util.EmptyStackException;

/**

  • 2 * @Author: 小王同学

  • 3 * @Date: 2021/11/23 20:50

  • 4

*/

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;

}

public Object pop(){

if (size == 0)

throw new EmptyStackException();

return elements[-- size];

}

private void ensureCapacity() {

if (elements.length == size)

elements = Arrays.copyOf(elements, 2 * size + 1);

}

}

上述代码中存在着内存泄漏,如果向栈中先添加元素再弹出元素,弹出来的对象不会被回收,因为栈内部维护着弹出对象的过期引用。

解决这个问题很简单,将出栈元素的引用设为过期即可:

在这里插入图片描述


内存泄漏的其他来源:

  • 缓存

原因是被放入缓存的对象引用容易被我们遗忘。利用缓存中存储数据的价值与存储时间的长短成反比的特点,可以开一个后台线程及时清理掉失效项。

  • 监听器和其他回调

原因是客户端在我们提供的API中注册回调,但却没有取消回调时,它们就会堆积起来。如果我们希望回调立即被回收的话可以只保留它们的弱引用(WeakHashMap中的键)。

这里还要补充一点关于WeakHashMap的知识:WeakHashMap其实是一种弱引用Map,key会存储为弱引用,当GC时,如果这些key没有外部强引用存在的话(当回调对应的强引用被不存在了时),就会被垃圾回收掉。它的这个特性也多被用来实现缓存,如果外面对某个key的引用不存在了,缓存中key对应的这一项就会被自动删除。

例如:使用WeakHashMap存储BigImage实例,key是ImageName类型,value是BigImage实例,如果令imageName = null ,这样就没有强引用指向这一个key了,BigImage实例就会在GC时被回收掉

WeakHashMap<UniqueImageName, BigImage> map = new WeakHashMap<>();

BigImage bigImage = new BigImage(“image_id”);

UniqueImageName imageName = new UniqueImageName(“name_of_big_image”); // 强引用

map.put(imageName, bigImage);

assertTrue(map.containsKey(imageName));

imageName = null; //map中的values对象成为弱引用对象

System.gc(); //主动触发一次GC

8 避免使用终结方法和清除方法


终结方法(finalizer)和清除方法(cleaner)不可预测,也有一定的危险性,应该避免使用

它们的缺点主要有以下几点:

1. 不能保证会被及时执行

终结方法线程优先级比应用其他线程的优先级低得多,甚至还会有程序终止时都还没来得及执行的情况。所以如果使用它们来释放共享资源上的锁时,很容易让系统崩溃。

2. 不处理终结过程中抛出的异常时,终结过程会停止

正常情况下程序如果出异常了会打印异常信息,但如果异常出现在终结方法里面则什么都不会打印,根本无法下手去debug。

3. 终结方法和清除方法有一个非常严重的性能损失

主要原因是因为终结方法阻止了有效的垃圾回收。

4. 终结方法有一个严重的安全问题

黑客可以利用终结方法发起攻击。

如果构造函数抛异常了,恶意子类的终结方法就可以在构造了一部分的对象上运行,阻止该对象被垃圾回收。这样就可以在这个对象上调用原本不允许出现在这里的方法。

正常情况下,构造函数抛异常了,对象也就创建失败了,使用终结方法的话就没有这个特性了。为了防止受此攻击,要写一个空的final的finalize方法。如果对象中封装的资源确实需要终止,绕过编写终结方法或清除方法的方式是让类implements AutoCloseable,客户端在每个实例不再需要时调用close方法。


当然存在即合理,在下面两个场景里终结方法和清除方法还是很有用的:

1. 充当“安全网”,防止忘记调用close方法

安全网这个词看似高大上,实际上这里就是兜底逻辑的意思

2. 终止非关键的本地资源

本地对等体是一个native的对象,Java对象会通过native方法委托给一个本地对象,这个本地对象JVM是无法回收的,所以可以用清除方法来进行回收,当然前提是回收的不能是关键资源。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img
线程、数据库、算法、JVM、分布式、微服务、框架、Spring相关知识

一线互联网P7面试集锦+各种大厂面试集锦

学习笔记以及面试真题解析

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
13684746870)]

[外链图片转存中…(img-mta8rpuO-1713684746871)]

[外链图片转存中…(img-GuAvW4KR-1713684746871)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img
线程、数据库、算法、JVM、分布式、微服务、框架、Spring相关知识

[外链图片转存中…(img-n0S5wLJ0-1713684746871)]

一线互联网P7面试集锦+各种大厂面试集锦

[外链图片转存中…(img-XGMY5Qgx-1713684746872)]

学习笔记以及面试真题解析

[外链图片转存中…(img-TpvUFaW1-1713684746872)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值