我正在参加「掘金·启航计划」
上一篇分享的内容我觉得还是蛮有用的,但是感觉小伙伴们并不是很感冒,可能是大家平时用到的不多,没有产生共鸣,也有可能是我没有表达的很清楚🤣,今天继续整理两个小的知识点,内容比较基础,但是其中又有很多小的知识点,是我们平时不大注意的,今天就记录一下,加深一下记忆。
学习的内容
1. try-with-resources
Java小伙伴肯定都知道,我们平时开发大部分时候是不需要管理内存的,只是个别场景会遇到,比如很早的时候我们编码连接数据库,或者是平时有读取文件的操作(例如InputStream、OutputStream),我想大家开发的时候基本都能意识到去关闭这些资源。
- 写法示例1
public static String firstLineOffFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
- 写法示例2
public static void firstLineOffFile2(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
//可以把下面这行移到第一个try上面,就不需要第二个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();
}
}
应该可以看得出,如果某一个方法涉及到太多次的关闭操作,这样的代码让人看着逻辑很臃肿,即是这样,其实也是可以忍受的,只是有一个更严重的问题:如果try和finally都抛出了异常,那try中的异常会被后者覆盖,先看下示例代码
public static String firstLineOffFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
//异常1
return br.readLine();
} finally {
//异常2
br.close();
}
}
实际会出现什么情况呢,当异常1发生后,代码正常会执行到finally中,然后异常2发生。这样就会导致:我们线上出现bug之后,我们只能看到异常2的报错信息,但是看不到异常1的异常记录,也就导致我们找不到修改的重点,陷入迷茫🙃。
解决方式就是把try-finally 换成try-with-resources
public static void copy2(String src, String dst) throws IOException {
try(InputStream in = new FileInputStream(src);
OutputStream outputStream = new FileOutputStream(dst)) {
byte[] buf = new byte[10];
int n;
while ((n = in.read(buf)) >= 0) {
outputStream.write(buf, 0, n);
}
}
}
小结:
try-with-resource有下面这几个优点:
- 可以让代码更加简洁,这样主要逻辑就容易凸显出来
- 自动帮我们关闭资源
- 能够解决上面说的异常2覆盖异常1的情况
2.如何实现一个不可变类
今天分享的第二个知识点是不可变类,也就是被final修饰的类,这个知识点其实还是很基础的,因为我们最开始学Java的时候就知道怎么用String类定义一个字符串,而String就是这种类。
之所以又重新看了下这块的内容,是因为有时候可能是眼高手低,感觉这块知识很简单,自己完全掌握了,但是想说出来的时候说的又不是很全面,所以得经常琢磨琢磨。
- String源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash;
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
.......省略
}
上面贴了一点String的源码,大家也可以自己打开源码程序看一下,可以发现他有下面这些特点:
- 因为类使用了final修饰,所以不可被继承
- 所有的属性都是final的
- 所有的属性都是private私有的
- 不提供任何修改对象的方法
- 禁止外部调用时,能够直接或间接拿到可变对象的引用(String中的数组属性)
那么为什么要实现一个不可变类,他有哪些优缺点呢?
优点
不可变对象本质上是线程安全的, 这一点我想大家都应该清楚,线程不安全简单理解就是一个数据同一时刻被多个线程修改,而不可变类本身不能被修改,所以不会出现线程安全问题,这种狠人简直无懈可击....
- Boolean.java 提供两个fincal常量,供静态工厂方法使用,不用考虑线程安全问题
public final class Boolean implements java.io.Serializable,
Comparable<Boolean>
{
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
}
tips:使用静态工厂可以更灵活的添加缓存,减少系统开销
不可变对象可以共享内部信息, 这里可以看一个BigInteger类例子,当我们调用negate方法时,是直接实用的 this.mag,也就是新创建的BigInteger对象和之前的对象共享同一个数组,并没有重新创建一个新数组,这样做可以节省内存空间。
public class BigInteger extends Number implements Comparable<BigInteger> {
final int signum;
final int[] mag;
public BigInteger negate() {
return new BigInteger(this.mag, -this.signum);
}
}
缺点:
当然不可变类也是有缺点的,就是对于任意不同的值都需要新建一个对象,如果是特别大的对象,这种创建过程的代价还是很高的。
比如我们要拼接一系列的字符串,我们就先创建出一个个的单个字符串,最后拼接在一起,这样就需要创建很多对象,对内存有很大损耗。
String param1 = "hello";
String param2 = "world";
String param3 = param1 + param2;
对于这种问题,Java类库中也是给出了解决方案,比如使用StringBuilder来解决这类问题。
以退为进:
上面说的不可变类要求类的所有属性必须被final修饰,但其实这并不是绝对不可以改动的,有时候为了性能着想,还是可以进行改造,比如下面这个例子,不使用final修饰hashCode,当我们用到的时候再进行初始化并进行缓存,通过这种做法来节省性能开销。
private int hashCode;
@Override
public int hashCode(){
int result = hashCode;
if(result == 0){
//初始化hashCode
result = Short.hashCode(areaCode);
......
}
}
总结
今天先分享的两个知识点,虽然属于最基础的内容,但是我觉得里面还是有不少东西可以研究的。总结下来,对于释放资源,优先使用try-with-resource。
对于不可变类,我们要灵活运用,在没有充分理由的情况下,每个域都要使用private final进行修饰,不对外提供任何的set方法,最后就是除了构造器或者静态工厂,不再提供任何共有的初始化方法(除非不得不)。