2024年Java最全快速带你看完《Effective Java》—— 创建和销毁对象篇(1),Java开发从零开始

笔者福利

以下是小编自己针对马上即将到来的金九银十准备的一套“面试宝典”,不管是技术还是HR的问题都有针对性的回答。

有了这个,面试踩雷?不存在的!

回馈粉丝,诚意满满!!!




本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  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是无法回收的,所以可以用清除方法来进行回收,当然前提是回收的不能是关键资源。

9 try-with-resources优先于try-finally


主要讲的是关闭资源的方法。

在Java7之前,关闭资源使用的是try-finally语句,但它有两个明显的缺点:

  1. 如果有多个资源需要关闭,代码会非常丑陋,比如下面这样:

static void copy(String src, String dst) throws IOException {

InputStream in = new FileInputStream(src);

try {

OutputStream out = new FileOutputStream(dst);

try {

byte[] buf = new byte[10];

int n;

while ((n = in.read(buf)) >= 0)

out.write(buf, 0, n);

} finally {

out.close();

}

} finally {

in.close();

}

}

  1. 异常信息会覆盖

static String firstLineOfFile(String path) throws Exception{

BufferedReader br = new BufferedReader(new FileReader(path));

try {

return br.readLine();

} finally {

br.close();

}

}

如果底层物理设备异常了,br.readLine();就会抛出异常,调用close也会出现异常,此时第二个异常会覆盖第一个异常,这会导致调试起来很麻烦,因为第一个异常才是真正诊断问题的入口。

正是因为存在上面两个问题,当Java7引入try-with-resources时,上面两个问题就迎刃而解了。

使用这个语法时,需要先实现AutoCloseable,接口Java类库与第三方类库中很多类都实现了这个接口。

  1. 当有多个资源需要被关闭时

static void copy(String src, String dst) throws IOException {

try (InputStream in = new FileInputStream(src);

OutputStream out = new FileOutputStream(dst)){

byte[] buf = new byte[10];

int n;

while ((n = in.read(buf)) >= 0)

out.write(buf, 0, n);

}

}

  1. 当有方法抛出异常时

static String firstLineOfFile(String path) throws IOException {

try(BufferedReader br = new BufferedReader(new FileReader(path))) {

return br.readLine();

}

}

如果readLine和close方法都抛出异常,前面的异常仍然会被打印出来,而不会覆盖。

  1. 还可以使用catch子句来处理异常

static String firstLineOfFile(String path, String defaultVal) throws IOException {

try(BufferedReader br = new BufferedReader(new FileReader(path))) {

return br.readLine();

} catch (IOException e){

写在最后

学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

最后再分享的一些BATJ等大厂20、21年的面试题,把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

蚂蚁金服三面直击面试官的Redis三连,Redis面试复习大纲在手,不慌

Mybatis面试专题

蚂蚁金服三面直击面试官的Redis三连,Redis面试复习大纲在手,不慌

MySQL面试专题

蚂蚁金服三面直击面试官的Redis三连,Redis面试复习大纲在手,不慌

并发编程面试专题

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

defaultVal) throws IOException {

try(BufferedReader br = new BufferedReader(new FileReader(path))) {

return br.readLine();

} catch (IOException e){

写在最后

学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

最后再分享的一些BATJ等大厂20、21年的面试题,把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

[外链图片转存中…(img-o2ZZiarg-1714889015211)]

Mybatis面试专题

[外链图片转存中…(img-2yXU4hql-1714889015212)]

MySQL面试专题

[外链图片转存中…(img-rTWSMtyd-1714889015213)]

并发编程面试专题

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 10
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值