《Effective.Java》 阅码草堂笔记 一

首先来一个开宗明义

查理论吸引客户的关键是把手头的事情做好。把已经拥有的客户照顾好,其他的自然会来找你,这一种说法我觉得不只是商业,投资…放之四地皆适之,这里我只是想告诫自己,对于现在的自己来说,我可能不能做很多事情,但是我应该把每一件事情尽可能的做好,我可以不那么快,但是我应该要那么好,刘慈欣用彻夜赶工的妓女来形容程序员,是二百多年前的码头背起重负的搬运工,说我们是「技术无产阶级」,确实在马云爸爸给我们争取的九九六的福报下好像确实是这样,顺应互联网的洪流,代码作为程序员的一种谋生手段,已经祛魅,因为不在神秘,所以平常,可能就是因为如此再加上各种外部因素亦或是内部原因,我们不少人都是为了打代码而打代码🌧,这一种心态的出现直接导致了各种程序员常用语句,没有关系,能跑就行,各种佛系程序员接踵而来,我见众生皆草木,众生见我应如是,佛系程序员中也包括我自己,所以抱着纵使今日离世,不碍明日浇花(这里这么说好像不那么适当,原谅鄙人文学修养不够😂)的心态我再一次重温了我已经忘记得七七八八的《阿里巴巴 Java 开发手册 (v1.5.0 华山版)》和享誉业界的《Effective.Java》,这一系列的文章记录了我看《Effective.Java》的中的笔记和一些心得,除了帮助自己记忆更多的是分享,是知识的传播。其中,有错误的请各位大神指教,日拱一卒无有尽,功不唐捐终入海,每天只需要进步一点点

《Effective.Java》

1. 考虑使用静态工厂方法替代构造方法

传统的我们利用构造方法来获取一个类的实例,其实还存在一种技术,在类里面提供一个公共的静态工厂方法,返回一个类实例的静态方法

/* 
ps :阿里巴巴 Java 开发手册指出所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
但是对于 Integer 值,假如Integer在-128 至 127 范围内可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,推荐使用 equals 方法进行判断。
Integer.valueOf(0)在-128 至 127 范围内是利用享元模式从IntegerCache.cache数组中获取的值
*/
Integer.valueOf(0);
String.valueOf("");

Boolean.valueOf(false);
public static Boolean valueOf(boolean b) {
       return (b ? TRUE : FALSE);
  } 
注意 ⚠:此静态工厂非设计模式中的静态工厂
提供静态工厂方法优点
  • 不像构造方法,他们是有名字的,对于有名称的静态工厂来说,它更方便阅读和使用
  • 并不需要每一次调用都创建一个对象
  • 可以返回其返回类型的任何子类型对象
  • 返回对象的类可以根据输入参数的不同而不同
  • 编写包含这个方法的类时,返回对象的类不需要存在
提供静态工厂方法缺点
  • 没有公共或者是保护构造方法的类不能被子类化
  • 程序员很难找到他们
常用静态工厂名称
  • from 类型转换方法
Date d = Date.from(instant);
  • of 聚合,接收多个参数并返回该参数的实例,并把他们合并在一起
Set faceCards = EnumSet.of(JACK,QUEEN,KING);
  • valueOf from和to更详细的替代方式
Boolean.valueOf(false);
  • instance或getInstance
Object newArray = Array.newInstance(classObject, arrayLen);
  • getType 与 getInstance 类似,但是如果在工厂方法中不同的类中使用。Type 是工厂方法返回的对象类型
FileStore fs = Files.getFileStore(path);
  • newType 与 newInstance 类似,但是如果在工厂方法中不同的类中使用。Type 是工厂方法返回的对象类型
BueredReader br = Files.newBueredReader(path);
  • type getType 和 newType 简洁的替代方式
List litany = Collections.list(legacyLitany);

静态工厂方法和公共构造方法都有它们的用途,通常,静态工厂更可取,因此避免在没有考虑静态工厂的情况下提供公共构造方法

2. 当构造方法参数过多使用Builder模式

Builder模式即使建造者模式,我在我的前一篇文章
里面也说了谷歌编程大多数采用了Builder模式进行链式编程,客户端调用代码很清爽,lombok也提供了@Builder注解可以为我们简单封装一下(这里网上有声音说不建议使用lombok,具体原因没有细看,反正我是一直用的这个,没出现什么大Bug),关于这个,书上举了一个包装食品的例子,先看一下一个小现象

这里的原因是final修饰的非静态的字段, 在虚拟机为它开辟空间时必须得保证它会被显式赋值一次且只被赋值一次,可以利用构造方法为它赋值,全部代码如下:

/**
 * @author 谭婧杰
 * @date 2020/11/25
 *  
 */
public class NutritionFacts {
	private final int servingSize; // 每日摄入量 (mL) required
	private final int servings; // 分量 (per container) required
	private final int calories; // 卡路里 (per serving) optional
	private final int fat; // 总脂肪 (g/serving) optional
	private final int sodium; // 钠 (mg/serving) optional
	private final int carbohydrate;// 碳水化合物 (g/serving) optional

	public NutritionFacts(int servingSize, int servings) {
		this(servingSize, servings, 0);
	}

	public NutritionFacts(int servingSize, int servings, int calories) {
		this(servingSize, servings, calories, 0);
	}

	public NutritionFacts(int servingSize, int servings, int calories, int fat) {
		this(servingSize, servings, calories, fat, 0);
	}

	public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
		this(servingSize, servings, calories, fat, sodium, 0);
	}

	public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
		super();
		this.servingSize = servingSize;
		this.servings = servings;
		this.calories = calories;
		this.fat = fat;
		this.sodium = sodium;
		this.carbohydrate = carbohydrate;
	}

}

客户端调用方式

public static void main(String[] args) {
	NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 10, 35, 27);
}

咋一看,完美,但是其实如果我们不需要某一个值的话,还是不得不给它传递一个值,打个比方sodium不是必选值,但是我们还是得要给它传递一个0值,而且如果参数越来越多,参数数量也会越来越多的,当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式,在这种模式中,调用一个无参数的构造函
数来创建对象,然后调用 setter 方法来设置每个必需的参数和可选参数,如:

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

这一种模式虽然解决了我们上面说的问题,但是也引发了不少新问题,例如构造过程中的不一致现象,或参数太多容易导致遗落(反正我就老是漏写字段。。。),更好的解决方式是 Builder
模式的一种形式,客户端不直接调用需要的对象,而是调用构造方法,并使用必要的参数,获取一个builder对象

/**
 * @author 谭婧杰
 * @date 2020/11/25
 *  
 */
public class NutritionFacts {
	// 实际开发中需要检查每一此参数的有效性, 如果检查失败,则抛出 IllegalArgumentException 异常

	private final int servingSize; // 每日摄入量 (mL) required
	private final int servings; // 分量 (per container) required
	private final int calories; // 卡路里 (per serving) optional
	private final int fat; // 总脂肪 (g/serving) optional
	private final int sodium; // 钠 (mg/serving) optional
	private final int carbohydrate;// 碳水化合物 (g/serving) optional

	public static class Builder {
		// Required parameters
		private final int servingSize;
		private final int servings;
		// Optional parameters - initialized to default values
		private int calories = 0;
		private int fat = 0;
		private int sodium = 0;
		private int carbohydrate = 0;

		public Builder(int servingSize, int servings) {
			this.servingSize = servingSize;
			this.servings = servings;
		}

		public Builder calories(int val) {
			calories = val;
			return this;
		}

		public Builder fat(int val) {
			fat = val;
			return this;
		}

		public Builder sodium(int val) {
			sodium = val;
			return this;
		}

		public Builder carbohydrate(int val) {
			carbohydrate = val;
			return this;
		}

		public NutritionFacts build() {
			return new NutritionFacts(this);
		}
	}

	private NutritionFacts(Builder builder) {
		servingSize = builder.servingSize;
		servings = builder.servings;
		calories = builder.calories;
		fat = builder.fat;
		sodium = builder.sodium;
		carbohydrate = builder.carbohydrate;
	}
	
}

客户端调用为:

public static void main(String[] args) {
		new NutritionFacts.Builder(10, 20).calories(12).fat(13).build();
	}

这样看起来,不仅仅容易编写,更加重要的是方便于阅读,两个字专业

如果是利用各种逆向工程自动生成的pojo代码,可以创建一个基础的抽象实现类,让后代实现他的build方法

优点
  • 有多个可变参数,每一个参数都是在它自己的方法中指定的
  • 灵活性很强,一个bulider可以重复使用构建多个对象
缺点
  • 为了创建对象,首先必须创建他的builder,builder 模式比伸缩构造方法模式更冗长,因此只有在
    有足够的参数时才值得使用它,比如四个或更多

3. 使用私有构造方法或枚举类实现Singleton

Singleton翻译过来是单例,也就是我们设计模式中的单例模式(保证一个类仅有一个实例,并提供一个访问它的全局访问点),一般来说,实现单例模式,通常有很多种,例如懒汉式,饿汉式,双检锁检查式,作为创建类的全局属性存在,创建类被装载时创建,枚举等,这节说的是创建单例模式更加推荐使用枚举创建,文中首先例出了两种单例模式的例子,如下:

一种单例模式实现是通过final来修饰成员变量,如

public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}

私有构造方法只调用一次,来初始化公共静态 final Elvis.INSTANCE 属性。这一种方式存在一个漏洞就是:特权客户端可以使用AccessibleObject.setAccessible 方法,利用反射方式调用私有构造方法(解决方案:修改构造函数,在请求创建第二个实例时抛出异
常。)
还有一种是公共成员是一个静态的工厂方法

// Singleton with static factory
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() { ... }
}

公共属性方法的主要优点是 API 明确表示该类是一个单例:公共静态属性被final修饰,所以它总是包含相同的对象引用。 第二个好处是它更简单。最后是方法引用可以用 supplier ,例如 Elvis::instance 等同于 Supplier

注意 ⚠:使用这两中方法需要重写Serializable的readResolve方法,防止它在反序列化创建实例,而且实例属性也应该声明为transient

最后是提议的一种方式,枚举生成

public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}

类似于公共属性方法,提供了免费的序列化机制,能够抵制序列化和反射攻击

4. 使用私有构造方法执行非实例化

假如有一个只包含静态方法和静态属性的类,这样的类获得了不好的名声,因为有些人滥用这些类从而避免以面向对象方式思考,但是它们确实有着特殊的用途。 这里的意思是,这个类不需要用来设计被实例化,因为静态方法可以直接类名.方法名(属性名)进行调用,所以说实例化是没有意义的,但是在明确的声明一个构造方法时候,编译器会主动提供一个无参构造方法,对于用户来说,该构造方法与其他构造方法没有什么区别

文中说了试图通过创建抽象类来强制执行非实例化是行不通的。该类可以被子类化,子类可以被实例化。此外,它误导用户认为该类是为继承而设计的。不过,有一个简单的方法来确保非实例化。只有当类不包含显式构造方法时,才会生成一个默认构造方法,因此可以通过包含一个私有构造方法来实现类的非实例化,例如:

/*
*这种习惯有一个副作用,阻止了类的子类化。所有的构造方法都必须显式或隐式地调用父类构造方法,而子类则没有可访问的父类构造方法来调用。
/
public class UtilityClass {
	private UtilityClass() {
              // 禁止非实例化的默认构造函数
               //  AssertionError 异常不是严格要求的,但是它提供了一种保证,以防在类中意外地调用构造方法。
		throw new AssertionError();
 	}
}

05. 依赖注入优于硬连接资源(hardwiring resources)

依赖注入是的你没有看错,就是Spring的那个依赖注入,在项目中许多类依赖于一个或多个底层资源。文中例子是一个拼写检查器依赖于字典,有那么几种实现方式

// 静态实用工具类
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // Noninstantiable
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}

// 单例实现
public class SpellChecker {
private final Lexicon dictionary = ...;
private SpellChecker(...) {}
public static INSTANCE = new SpellChecker(...);
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}

文中这样点评这两种方式,这两种方法都不令人满意,因为他们假设只有一本字典值得使用。在实际中,每种语言都有自己的字典,特殊的字典被用于特殊的词汇表。另外,使用专门的字典来进行测试也是可取的。想当然地认为一本字典就足够了,这是一厢情愿的想法,正确的应该是创建新实例时将资源传递到构造方法中。这是**依赖项注入(dependency injection)**的一种形式:字典是拼写检查器的一个依赖项,当它创建时被注入到拼写检查器中,改进后的代码为:

// Dependency injection provides flexibility and testability
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
// Objects.requireNonNull非空检查
this.dictionary = Objects.requireNonNull(dictionary); 
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
优点
  • 依赖注入模式很简单,它可以使用任意数量的资源和任意依赖图,保持了不变性,因此多个客户端可以共享依赖对象(同样适用于构造方法,静态工厂和builder模式)
  • 提供了类的灵活性和可测性
缺点

它可能使大型项目变得混乱,这些项目通常包含数千个依赖项。使用依赖注入框架(如 Dagger [Dagger]、Guice [Guice] 或 Spring [Spring])可以消除这些混乱。

总之,不要使用单例或静态的实用类来实现一个类,该类依赖于一个或多个底层资源,这些资源的行为会影响类的行为,并且不让类直接创建这些资源。相反,将资源或工厂传递给构造方法(或静态工厂或 builder模式)。这种称为依赖注入的实践将极大地增强类的灵活性、可重用性和可测试性。

6. 避免创建不必要的对象

这一章,其实是老生常谈了,但凡讲一点码德的程序员都知道,没有意义的东西,现在不丢留着被炒时带回家吗,哈哈哈😀,所以最好的方式就是每次需要时重用一个对象而不是创建一个新的相同功能对象,有几个极端例子(注意是反例,别搞蒙了)

// 这一条语句每一次执行都会创建一个String,String 构造方法("bikini") 的参数本身就是一个 bikini 实例,它与构造方法创建的所有对象的功能相同

String s = new String("谭婧杰");
// 最好的声明方式应该是直接赋值
String s = "谭婧杰"

// 或者通过使用静态工厂方法创建
Boolean.valueOf(false)

文中还提到了一种方式是说对于昂贵的那些对象,我们可以将它缓存起来,我觉得上面说的Integer就是一个很nice的例子,最后一个是说要优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。

注意 ⚠:这个条目不应该被误解为暗示对象创建是昂贵的,应该避免创建对象。 相反,使用构造方法创建和回收小的对象是非常廉价,构造方法只会做很少的显示工作,尤其是在现代 JVM 实现上。 创建额外的对象以增强程序的清晰度,简单性或功能性通常是件好事。相反,除非池中的对象非常重量级,否则通过维护自己的对象池来避免对象创建是一个坏主意。对象池的典型例子就是数据库连接。建立连接的成本非常高,因此重用这些对象是有意义的。但是,一般来说,维护自己的对象池会使代码混乱,增加内存占用,并损害性能。现代 JVM 实现具有高度优化的垃圾收集器,它们在轻量级对象上轻松胜过此类对象池。

这个条目的对应点是针对条目 50 的防御性复制(defensive copying)。 目前的条目说:「当你应该重用一个现有的对象时,不要创建一个新的对象」,而条目 50 说:「不要重复使用现有的对象,当你应该创建一个新的对象时。」请注意,重用防御性复制所要求的对象所付出的代价,要远远大于不必要地创建重复的对象。 未能在需要的情况下防御性复制会导致潜在的错误和安全漏洞;而不必要地创建对象只会影响程序的风格和性能。

7. 消除过期的对象引用

虽然java不像c一样,需要程序员手动回收资源,但是在一些条件下做为java程序员的我们依然需要考虑对象回收问题,看文中的实例代码

// Can you spot the "memory leak"?
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];
}
/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}

这个程序虽然看起来没有什么问题,但是实际上它存在一个漏洞即内存泄漏,由于垃圾回收器的活动的增加,或内存占用的增加,静默地表现为性能下降,甚至还会产生OutOfMemoryError

那么哪里发生了内存泄漏?

如果一个栈增长后收缩,那么从栈弹出的对象不会被垃圾收集,即使使用栈的程序不再引用这些对象。 这是因为栈维护对这些对象的过期引用( obsolete references)。 过期引用简单来说就是永远不
会解除的引用。 在这种情况下,元素数组“活动部分(active portion)”之外的任何引用都是过期的。 活动部分是由索引下标小于 size 的元素组成

解决办法

这类问题的解决方法很简单:一旦对象引用过期,将它们设置为 null。 在我们的 Stack 类的情景下,只要从栈中弹出,元素的引用就设置为过期。 pop 方法的修正版本如下所示:

public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}

这样子的话如果它们随后被错误地引用,程序立即抛出 NullPointerException 异常

清空引用的时机

其实存储池就是一个大的数组,c中所谓的指针就是数组的索引,数组中除了活动的元素也就是我们赋值的元素被分配,其他的都是空闲的,话虽如此这是心照不宣的事实但是你知道不代表垃圾回收器知道啊,所以不管有用没有用,数组中所有的对象都是有效果的,js里面也是一样的(JavaScript高级程序设计里面说到过,但是它是说闭包的作用域链中保存着一个HTML 元素,那么就意味着该元素将无法被销毁,详情见7.2.3内存泄漏章节),所以我们必须告诉垃圾回收器,一旦数组中的元素变成非活动的一部分,就可以手动清空这些元素的引用。即使当一个类自己管理内存时,我们应该警惕内存泄漏问题。 每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。

还有一个常见的内存泄露来源是缓存,对于这个问题有几种解决方案。一是只要在缓存之外存在对某个项(entry)的键(key)引用,那么这项就是明确有关联的,就可以用 WeakHashMap 来表示缓存;这些项在过期之后自动删除,ThreadLocal就是这么实现的,里面的ThreadLocalMap就继承了WeakReference,他的key=当前线程,value=线程局部变量缓存值,但是它还是会存在内存泄漏问题,具体原因网上有很多博客,可以求证,下面是 ThreadLocal一部分代码可以看下:

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 这里的map就是它的类部类ThreadLocalMap
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    // 这里的Entry是ThreadLocalMap类里面的 
    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
 

还有一种是,缓存项有用的生命周期不太明确,随着时间的推移一些项变得越来越没有价值。在这种情况
下,缓存应该偶尔清理掉已经废弃的项。这可以通过一个后台线程 (也许是 ScheduledThreadPoolExecutor ) 或将新的项添加到缓存时顺便清理,例如 LinkedHashMap 类使用它的 removeEldestEntry 方法实现了后一种方案

最后一种常见内存泄漏的来源是监听器和其他回调,如果你实现了一个 API,其客户端注册回调,但是没有显式地
撤销注册回调,除非采取一些操作,否则它们将会累积。确保回调是垃圾收集的一种方法是只存储弱引用(weak
references),例如,仅将它们保存在 WeakHashMap 的键(key)中。

8. 避免使用 Finalizer 和 Cleaner 机制

java提供了一个finalize方法,可以帮助我们进行资源释放,类似于C++中的析构函数(只是类似,不能划等号),又叫收尾机制(卧槽,我想起来收尾效益,我可能掉💴眼里面去了),Finalizer一般来说是不可预知的,Java 9 中 Cleaner 机制代替了Finalizer 机制。Cleaner虽然没有那么危险但仍然是不可预知的,在 Java 中,当一个对象变得不可达时,垃圾收集器回收与对象相关联的存储空间,不需要开发人员做额外的工作。但是不可达并不代表一定会被回收,《深入理解 Java 虚拟机-JVM 高级特性与最佳实践(第版)》有如下章节生存还是死亡,后面那个这是一个问题是我自己妄自加上去的🤭,莎士比亚经典名言生存还是死亡,这是一个问题(To be or not to be–this is the question )


生存还是死亡,这是一个问题(To be or not to be–this is the question )

可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处
于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程

  • 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,(筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。)

  • 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中(稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行,“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束)

如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。


当然上面在这篇文章中不是重点,是我自己联想到的,我们的重点在于不要相信 System.gcSystem.runFinalization方法。 他们可能会增加 Finalizer 和 Cleaner 机制被执行的几率,但不能保证一定会执行。

释放对象的方法

为对象封装需要结束的资源 (如文件或线程),不应该为这个类编写 Finalizer 和 Cleaner 机
制,而是让你的类实现 AutoCloseable接口,或者是使用通常使用 try-with-resources(jdk1.7 语法)确保终止,即使面对有异常抛出情况

9. 使用 try-with-resources 语句替代 try-finally 语句

try-with-resources是jdk1.7 提出的方法,以前我们都是使用try-finally来释放资源的,如

static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}

这一种方法的问题就是,添加第二个资源的时候,你的代码就会变成这样

// try-finally is ugly when used with more than one resource!
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[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}

但是try-with-resources就不一样了,使用这个构造的资源都实现了AutoCloseable接口,这个接口由一个返回为 void 的 close 组成。
重写上面的方法,代码如下:

// try-with-resources - the the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
}
}

// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}

结论很明确:在处理必须关闭的资源时,使用 try-with-resources 语句替代 try-finally 语句。 生成的代码更简洁,更清晰,并且生成的异常更有用。 try-with-resources 语句在编写必须关闭资源的代码时会更容易,也不会出错,而使用 try-finally 语句实际上是不可能的。

10. 重写 equals 方法时遵守通用约定

重写equals时机

如果一个类包含一个逻辑相等( logical equality)的概念,此概念有别于对象标识(object identity),而且父类还没有重写过 equals 方法。文中还提到了一种不需要 equals 方法重写的值类是使用单例模式的类。(枚举类型就属于这个类别)

具体约定
  • 自反性: 对于任何非空引用 x, x.equals(x) 必须返回 true。

  • 对称性: 对于任何非空引用 x 和 y,如果且仅当 y.equals(x) 返回 true 时 x.equals(y) 必须返回 true。

  • 传递性: 对于任何非空引用 x、y、z,如果 x.equals(y) 返回 true, y.equals(z) 返回 true,则x.equals(z) 必须返回 true。

  • 一致性: 对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则 x.equals(y) 的多次调用必须始终返回 true 或始终返回 false。

  • 对于任何非空引用 x, x.equals(null) 必须返回 false。

更多关注掘金同名账号谭婧杰
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值