看完这本《Effective Java》,我悟了 —— 日更ing

这也是当今主流微服务的特色


说了这么多,实现封装和解耦其实很简单,尽可能使每个类或成员不被外部访问即可。如果一个类只是在某一个类里被用到,则应该让这个类成为使用它的那个类的私有嵌套类。

但是里氏替换原则限制了我们降低方法可访问性的目标,即如果方法是覆盖父类中的方法,子类方法的访问级别就不能低于父类中的访问级别。这样可以确保任何可使用父类实例的地方也都可以使用子类的实例。

这条规则有条特殊的:如果是类实现接口,则所有被实现的方法必须是public的

public类的实例域绝不能是public的,因为public属性通常并不是线程安全的。如果有常量是需要暴露的,可以使用public static final来修饰这些属性。

对于数组这种数据结构,让类具有public static final数组属性是错误的,因为数组里面的内容还是可能会被修改的,下面就是一种错误的写法:

// wrong

public static final Thing[] VALUES = {…};

解决这个问题有两种写法:

方法一:

private static final Thing[] PRIVATE_VALUES = {…};

public static final List VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

方法二:

private static final Thing[] PRIVATE_VALUES = {…};

public static final Thing[] values(){

return PRIVATE_VALUES.clone();

}

16 在public类中使用访问方法而不是public属性


有时候需要编写一些退化类,用来聚合一些属性,比如这样:

class Point {

public double x;

public double y;

}

上面的写法违反了面向对象设计中封装的规定,应该被杜绝,而是使用下面这种setter的风格代替:

public class Point {

private double x;

private double y;

public Point(double x, double y){

this.x = x;

this.y = y;

}

public double getX() {

return x;

}

public double getY() {

return y;

}

public void setX(double x) {

this.x = x;

}

public void setY(double y) {

this.y = y;

}

}

上面秉承了一个原则:如果类可以在它所在的包之外访问,就提供访问方法。才外还有另一个原则:如果类是default的或是private的嵌套类,可以直接暴露其属性

Java平台类库里java.awt.Pointjava.awt.Dimension类违反了这一条原则,这是个错误!

如果属性是final的,可以将其可见范围设为public并强加约束条件,就像下面一样加限制条件:

public class Time {

private static final int HOURS_PER_DAY = 24;

private static final int MINUTES_PER_HOUR = 60;

public final int hour;

public final int minute;

public Time(int hour, int minute){

if (hour < 0 || hour >= HOURS_PER_DAY)

throw new IllegalArgumentException("Hour: " + hour);

if (minute < 0 || minute >= MINUTES_PER_HOUR)

throw new IllegalArgumentException("Minute: " + minute);

this.hour = hour;

this.minute = minute;

}

}

17 可变性最小化原则


不可变类是指shilling不能被修改的类,比如:Sting、基本类型包装类、BigInteger、BigDecimal

之所以要使用不可变类,大致原因有:不可变的类更容易实现,且安全性更高,之后会有更详细的讨论。

不可变类的实现遵循下面四条原则:

1. 不提供任何setter方法

2. 保证类不会被继承

一般做法是将类声明为final的。还有一种做法是让类的构造函数是private或default的,然后使用静态工厂来代替公有的构造函数:

public class Complex {

private final double re;

private final double im;

private Complex(double re, double im){

this.re = re;

this.im = im;

}

public static Complex valueOf(double re, double im){

return new Complex(re, im);

}

}

3. 所有的属性都是private final

这条规定比实际使用里严格了一点,实际上为了提高性能,只要保证方法对对象的修改不会被外部可见即可。比如有一些不可变类也有不是final的属性,在第一次请求计算时会将一些昂贵计算开销的结果缓存在这些属性里。

4. 确保对任何可变组件的互斥访问

这句话读起来比较拗口,实际上意思就是:不可变类里面如果有指向可变对象的字段,必须确保客户端不能获得这些字段。


下面是一个不可变类的例子,该类定义了数学上面的复数:

public class Complex {

private final double re;

private final double im;

public Complex(double re, double im){

this.re = re;

this.im = im;

}

public double realPart() {return re;}

public double imaginaryPart() {return im;}

public Complex plus(Complex c){

return new Complex(re + c.re, im + c.im);

}

public Complex minus(Complex c){

return new Complex(re - c.re, im - c.im);

}

}

注意这里算术运算返回的是新创建的Complex实例,它可以保证参与计算的实例是不可变的,这种方式被称为函数式。

上面的方法名都是用的介词(plus),而不是动词(add),强调该方法不会改变对象的值。BigIntegerBigDecimal类是没有遵循这个习惯。


之所以提倡使用不可变类,还是因为它有太多的优点:

1. 不可变对象比较简单

因为它只有一种状态且不会变

2. 不可变对象是线程安全的,可以被随意共享

我们也应该充分利用这种优势,对于经常要用的值,提供public static final常量来复用。比如:

public static final Complex ZERO = new Complex(0, 0);

public static final Complex ONE = new Complex(1, 0);

public static final Complex I = new Complex(0, 1);

3. 可以共享类的内部信息

例如BigInteger内使用了符号数值表示法。符号用int存储,绝对值用数组存储。negate方法会产生一个符号相反的BigInteger,并不需要拷贝数组,而是指向原实例内的同一个数组

4. 提供了原子性

这个很好理解,因为它是不变的

不可变类具有唯一的缺点就是,对于不同的值要有不同的对象。

如果执行一个多步骤的操作,每个步骤都产生一个新的临时对象的话,性能瓶颈就会显露出来。

对此,也有两种解决方案:

  1. 猜测一下会用到哪些多步骤操作,使用基本数据类型

  2. 提供一个public的可变配套类

例如String的可变配套类StringBuilder

StringBuffer被废弃掉了

BigIntegerBigDecimal被编写时,还没有很好地贯彻不可变的类必须为final的理念,所以他们的所有方法都可能被覆盖,所以如果我们代码里面出现了他们的子类,就必须对其进行保护性拷贝:

public static BigInteger safeInstance(BigInteger val){

return val.getClass() == BigInteger.class ? val : new BigInteger(val.toByteArray());

}

此外,还有一些其他需要注意的细节:

  1. 如果类不能被做成不可变的,应尽可能限制它的可变性,比如降低其可以存在的状态数

  2. 除非属性必须是非final的,否则都应该是private final

18 复合优先于继承


普通的类如果要继承一个来自其他包的类,是非常危险的。原因是继承打破了封装性。

由于子类依赖于父类的某些实现细节,所以子类必须跟着其父类的更新而更新,下面用两个实际的例子来具体解释一下这一条规则:

例一:

假设我们要给HashSet添加一个方法,返回自从创建依赖添加了多少元素:

public class InstrumentedHashSet extends HashSet {

private int addCount = 0;

public InstrumentedHashSet(){}

public InstrumentedHashSet(int initCap, float loadFactor){

super(initCap, loadFactor);

}

@Override

public boolean add(E e) {

addCount ++;

return super.add(e);

}

@Override

public boolean addAll(Collection<? extends E> c) {

addCount += c.size();

return super.addAll©;

}

public int getAddCount(){

return addCount;

}

}

上面的代码,如果调用addAll方法添加元素就会有bug

InstrumentedHashSet s = new InstrumentedHashSet<>();

s.addAll(List.of(“Snap”, “Crackle”, “Pop”));

我们期望getAddCount方法返回3,但实际上返回的是6。我们分析一下程序的执行过程就能知道原因,InstrumentedHashSet的addAll方法先给addCount加3,然后执行到super.addAll(c)调用HashSet的addAll

在这里插入图片描述

这里又会调用被InstrumentedHashSet覆盖了的add方法里,所以addCount又被加了一遍,最终结果等于6。

解决方法也很简单,去掉子类中的addAll即可,但是这样修改的正确性在于:HashSet的addAll方法是在它的add方法上实现的,我们没有办法保证HashSet的addAll方法是一成不变的,所以InstrumentedHashSet类就很脆弱。

例二:

假设我们有一个集合,添加进集合的所有元素都必须满足一定的条件。如果要用一个子类继承它的话,我们就需要覆盖所有能添加元素的方法。

这样一来,一旦父类新增了能插入元素的方法,由于子类并未覆盖,所以就少了判断是否满足条件这一步骤,就可能将非法的元素给加入到集合里了。

将HashTable和Vector加入到Collections框架里面时,就修正了几个这样的漏洞


复合可以避免前面所有的问题,即不继承现有的类,而是在新的类中增加一个私有域,引用现有类的一个实例。

public class InstrumentedSet extends HashSet {

private int addCount = 0;

public InstrumentedSet(Set s){

super(s);

}

@Override

public boolean add(E e) {

addCount ++;

return super.add(e);

}

@Override

public boolean addAll(Collection<? extends E> c) {

addCount += c.size();

return super.addAll©;

}

public int getAddCount(){

return addCount;

}

}

public class ForwardingSet implements Set {

private final Set s;

public ForwardingSet(Set s){

this.s = s;

}

@Override

public void clear() {

s.clear();

}

@Override

public boolean contains(Object o) {

return s.contains(o);

}

@Override

public boolean isEmpty() {

return s.isEmpty();

}

@Override

public int size() {

return s.size();

}

@Override

public Iterator iterator() {

return s.iterator();

}

@Override

public boolean add(E e) {

return s.add(e);

}

@Override

public boolean remove(Object o) {

return s.remove(o);

}

@Override

public boolean containsAll(Collection<?> c) {

return s.containsAll©;

}

@Override

public boolean addAll(Collection<? extends E> c) {

return s.addAll©;

}

@Override

public boolean removeAll(Collection<?> c) {

return s.removeAll©;

}

@Override

public boolean retainAll(Collection<?> c) {

return s.retainAll©;

}

@Override

public Object[] toArray() {

return s.toArray();

}

@Override

public T[] toArray(T[] a) {

return s.toArray(a);

}

@Override

public boolean equals(Object o) {

return s.equals(o);

}

@Override

public int hashCode() {

return s.hashCode();

}

@Override

public String toString() {

return s.toString();

}

}

构造函数的入参类型是Set,这个类把入参的Set变成了另一个Set并添加了计数器的功能。这里的包装类可以被用来包装任何Set的实现:

Set times = new InstrumentedSet<>(new TreeSet<>(cmp));

Set s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));

因为每一个InstrumentedSet实例都将Set实例包装起来了,所以他也是包装类,对应装饰器模式。

包装类不适用于回调框架,在回调框架里,对象将自身的引用传递给其他的对象用于回调,被包装起来的对象不知道外面包着它的对象,所以会传一个this,回调时就会和外面包装对象无关。—— SELF问题

那什么时候用继承呢?

对于两个类A,B,只有他们之间确实存在"is a"关系时才应该使用继承。如果打算让B继承A,就要确保每个B都是A,否则应该使用复合。

Java的类库里也有很多违反这一原则的地方,例如栈并不是向量,所以Stack不应该继承Vector。同理,Properties也不应该继承Hashtable

在这里插入图片描述

如果在适合使用复合的地方使用了继承,则会不必要地暴露实现细节,可能导致语义混淆。例如,如果p是Properties实例,p.getProperty(key)就可能产生与p.get(key)不同的结果,前一个方法 考虑了属性表,后一个方法继承自Hashtable。

决定使用继承之前还要再问自己一个问题,父类是否有缺陷?,继承会把父类API中的所有缺陷传播到子类里,复合允许设计新的API来隐藏缺陷。

19 要么设计继承并提供文档,要么禁止继承


这一条讨论专门为继承而设计并具有文档说明的类。

这种类必须有文档说明它可被覆盖的方法的自用性。即,对于每个public或protected的方法,文档必须指明在哪些情况下会调用可覆盖的方法。

如果方法调用了可覆盖的方法,在其文档注释的末尾应该包含关于这些调用的描述信息,以Implementation Requirements开头,以java.util.AbstractCollection规范为例:

// 如果集合中包含一个或多个元素e,就从中删除一个。如果集合中包含e则返回true

public boolean remove(Object o)

Implementation Requirements:该实现遍历整合集合来查找指定的元素。如果找到钙元素,将会用迭代器的remove方法将其从集合中删除。

注意:如果该集合的iTerator方法返回的迭代器没有实现remove方法,就会抛UnsupportedOperationException异常

在这里插入图片描述

实际上,关于程序文档有一个通识:好的文档应该描述给定的方法做了什么工作,而不是如何做到的。上面这种做法显然违背了,这也是继承破坏了封装性所带来的后果。


对于为继承而设计的类,有以下几点要求:

1. 类必须提供精心挑选的protected方法作为钩子(hook),以便进入其内部工作中

例如:java.util.AbstractList的removeRange方法

// 删除列表中所有索引位于[fromIndex, toIndex)的元素

protected void removeRange(int fromIndex, int toIndex)

这个方法是通过clear操作在这个列表及其子列表中调用的。覆盖这个方法以利用列表实现的内部信息,可以充分改善这个列表及其子列表中clear的性能

Implementation Requirements:这项实现获得了一个处在fromIndex之前的的列表迭代器,并依次重复调用ListIterator.next和ListIterator.remove,直到整个范围都被移除位置

这个方法对于Lst实现的最终用户并无意义,唯一目的在于使子类更易于提供针对子列表的快速clear方法。

2. 为继承而设计的类,唯一的测试方法是编写子类

这也是我们决定要暴露哪些protected方法或属性的依据,如果遗漏了关键的protected成员,尝试编写子类时就会遭受遗漏的“痛苦”。所以必须要在发布类之前先编写好子类对类进行测试。

3. 构造器不能调用可被覆盖的方法

如果违反了这条规则,可能导致调用失败。原因在于父类的构造器先于子类的执行,所以子类中覆盖版本的方法会在子类构造器之前先被调用(我们期望是子类方法在构造器之后运行)。

例如:

public class Super {

public Super() {

overrideMe();

}

public void overrideMe(){

}

}

public class Sub extends Super{

private final Instant instant;

Sub(){

instant = Instant.now();

}

@Override

public void overrideMe() {

System.out.println(instant);

}

public static void main(String[] args) {

Sub sub = new Sub();

sub.overrideMe();

}

}

我们期望它打印两次日期,但实际上它第一次打印的是null,因为overrideMe方法被Super构造器调用时,构造器Sub还没有机会初始化instant域。

4. 当为继承设计的类要实现CloneableSerializable接口时,clonereadObject都不能调用可覆盖的方法

因为clone和readObject的行为非常类似于构造器,所以类似的规则限制也是适用的

5. 当为继承设计的类实现了Serializable接口,并且该类有一个readResolvewriteReplace方法,必须声明他们是protected的

因为如果这些方法是private的,子类就会直接忽略掉这两个方法

6. 对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化

有两种方法可以禁止子类化:

① 把这个类声明为final的;

② 把所有的构造器都变成private,并增加一些公有的静态工厂来代替构造器。

7. 如果具体的类没有实现标准的接口,并且必须子类化,需要确保这个类的方法永远不会调用它的其他任何可覆盖的方法

这样被覆盖的方法就不会影响调用它的类的方法

20 接口优于抽象类


接口优于抽象类的主要原因就是:Java只允许单继承,所以用抽象类作为类型定义受到了一定限制。接口就不一样了,一个类可以继承多个接口,灵活度更高。

具体的思考点有以下几个:

1. 类可以很容易实现新的接口来更新,但无法通过多继承一个抽象类的方式来更新

当Comparabl、Iterable、Autocloseable接口被引入Java时,很多已有的类就被更新了,而且只用实现新的接口即可。但如果已经继承了一个类的话,就不能再多继承一个来实现更新。

2. 接口是定义mixin的理想选择

mixin类型:类实现了这个mixin类型,以表明它提供了某些可供选择的行为。之所以叫mixin是因为它允许函数可被混合到类的主要功能中

例如Comparable是一个mixin接口,它允许实现它的实例可以与其他实例比较。抽象类不能用与定义mixin,原因是:不可能有一个以上的父类,类层次结构里没有合适的地方来插入mixin

3. 接口允许构造非层次的类框架

例如,我们定义代表singer和songwriter的接口

public interface Singer {

AudioClip sing(Song s);

}

public interface Songwriter {

Song compose(int chartPosition);

}

在实际生活里,有些歌唱家本身也是作曲家,所以类同时实现Singer和Songwriter也是可以的:

public interface SingerSongwriter extends Singer, Songwriter{

AudioClip strum();

void actSensitive();

}

4. 包装类(wrapper)模式使得接口可以安全地增强类的功能

如果使用抽象类来定义类型,只能使用继承的手段来增加功能,这样得到的类与包装类相比,功能更差也更脆弱。

5. 通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来

接口负责定义类型,骨架实现类则负责实现非基本类型接口方法 —— 模板方法模式。

例如Coleections Framework为每个重要的集合接口(AbstractCollectionAbstractSetAbstractListAbstractMap)提供了一个骨架实现,例如下面这种实现:

static List intArrayList(int[] a){

Objects.requireNonNull(a);

return new AbstractList() {

@Override

public Integer get(int index) {

return a[index];

}

@Override

public Integer set(int index, Integer element) {

int oldVal = a[index];

a[index] = element;

return oldVal;

}

@Override

public int size() {

return a.length;

}

};

}

此外,还有一个模拟多重继承的概念:实现了接口的类可以把对于接口方法的调用转发到一个内部private类的实例上,这个内部private类继承了骨架实现类。


编写骨架类比较简单,这里用一个例子说明,以Map.Entry接口为例,明显的方法是getKeygetValuesetValue,接口里面定义了equalshashCode方法,但不允许Object方法提供默认实现,所以所有实现都放在骨架实现类里:

public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {

// 必须重写这个方法

@Override

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

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

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

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

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

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

img

最后

分享一些资料给大家,我觉得这些都是很有用的东西,大家也可以跟着来学习,查漏补缺。

《Java高级面试》

《Java高级架构知识》

《算法知识》

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
法的调用转发到一个内部private类的实例上,这个内部private类继承了骨架实现类。


编写骨架类比较简单,这里用一个例子说明,以Map.Entry接口为例,明显的方法是getKeygetValuesetValue,接口里面定义了equalshashCode方法,但不允许Object方法提供默认实现,所以所有实现都放在骨架实现类里:

public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {

// 必须重写这个方法

@Override

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

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

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-3lDQBgSY-1712492881760)]

[外链图片转存中…(img-FEyGhWqx-1712492881761)]

[外链图片转存中…(img-elAfgwKI-1712492881761)]

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

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

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

img

最后

分享一些资料给大家,我觉得这些都是很有用的东西,大家也可以跟着来学习,查漏补缺。

《Java高级面试》

[外链图片转存中…(img-m8U7CrJD-1712492881761)]

《Java高级架构知识》

[外链图片转存中…(img-bPDSxJtz-1712492881762)]

《算法知识》

[外链图片转存中…(img-g1tYfICR-1712492881762)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 30
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值