Effective java

类和接口

第20条 接口优于抽象类

interface中 default method(缺省方法) 继承类才能调用 static method 。直接接口调用
在这里插入图片描述

何为抽象骨架类(模板模式)

接口负责定义类型,抽象类负责实现扩展该接口。个人理解:接口定义了该类型的行为,但是没有给定具体的实现。(如果没有子类都具备则可以通过default method定义)。然后作为抽象类(也叫骨架实现类)第一作用缩小范围,第二作用便于子类创造。抽象类是不能new的不是真正的工具类,如果没有接口的方法都要重写的话,真正的实现类就变得很臃肿很不方便。第三。模拟多种继承。抽象类实现接口,调用自身内部类的实现。做到横向扩展。

第24条 静态成员类优于非静态成员类

在这里插入图片描述

内部类虽然和外部类写在同一个文件中, 但是编译完成后, 还是生成各自的class文件,内部类通过this访问外部类的成员。1 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象(this)的引用;2 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为内部类中添加的成员变量赋值;3在调用内部类的构造函数初始化内部类对象时,会默认传入外部类的引用。
这样可能导致的问题:外围类在符合垃圾回收时候,因为存在非静态内部类一个隐藏指向他的引用。导致无法被清楚。这常常很难被发现。

如果内部类不需要外部类的this,也就是不需要他的非静态域/方法。建议使用静态内部类

泛型

第26条: 请不要使用原生态类型

在这里插入图片描述
==List< String >是List的子类型,所以可以如图传参,如果把List改成List< Object >。就无法传参 因为List< String > 不是 List< Object> 的子类型 ==这可能与你的直觉相矛盾。举个栗子。里氏替换原则。
认为将父类替换成子类,程序还能正常运行。List< Object >能接收 任意类型,List< String >不可以、所以。。。

在目前的java版本使用原来态类型是不安全的行为,他会逃避泛型检测。所以如果你确实觉得类型不管要紧,请使用通配符<?>。他会告诉编译器,他能持有任意类型的对象。是程序员故意为之。但是你会发现给List添加了通配符后,就不能add任何元素(除了null之外),编译时错误!!在这里插入图片描述
编译器不知道你里面存放的是什么类型,所以为了安全,防止你破坏集合的类型约束条件。你不能往里面添加除null外的元素。

List< Object > 和 List<?> 和 List 的区别

List< Object >:他告诉编译器他能存放任何类型的元素。安全
List<?>:只能包含某种位置对象类型的有且只有一个的集合。安全
List:原生态类型,脱离了泛型系统,只是为了兼容之前的代码。不安全

第28条 列表由于数组

泛型也不能作用于数组创建当中的,因为不知道传进来的泛型对象是否具备new的方法。
new T[ ] illegal

数组是具体化的,会在运行时知道和强化它们的安全(数组提供了运行时类型的安全),然后泛型不可具体化的(擦除)不可以具体化类型是指运行时表示法包含的信息比它的编译时表示法包含的信息更少

public class Chooser {
    private final Object[] choiceArray;

	//Collection位置类型
    public Chooser(Collection collection) {
        this.choiceArray = collection.toArray();
    }
    public Object choose(){
        Random random = ThreadLocalRandom.current();
        //return 返回值需要typeof有可能的类型
        return choiceArray[random.nextInt(choiceArray.length)];
    }
}
public class Chooser<T> {
    private final List<T> choiceArray;

    public Chooser(Collection<T> collection) {
        this.choiceArray = new ArrayList<>(collection)
    }
    public Tchoose(){
        Random random = ThreadLocalRandom.current();
        return choiceArray.get(random.nextInt(choiceArray.length);
    }
}

比较下两端代码你就知道差别了~~。

注意:由于在性能上集合的性能比数组差,还有数组的大小被固定。如果在开发的过程当中知道要处理的业务逻辑是一个定长的,未知类型。然后选择用来提高性能;

  @SuppressWarnings("unchecked")
  public T[]  ret(){
        T[] ele = (T[]) new Object[length];
        return ele;
    }

第31条 利用有限制通配符来提升API的灵活性

在这里再强调一次参数化类型。
在这里插入图片描述
之前说过。根据里氏替换原则,List< String > 不能代替 List < Object > 所以不是它的子类型,但是反过来。按照正常逻辑。List < Object > 应该可以代替 List < String > 接收它的参数才对。那为什么还是不可以呢?因为参数化类型是不可变的。不管是谁给谁传值。都是违法的。单单一个泛型也没有协变的,它有且只有代表一个未知的类型。如果能用List < Object > 接收 List < String > 进行转化。 那么 List< Object >.get(0) 可以是任意类型。这样很不安全!为此泛型推出了有限制的通配符。进行协变和裂变这里就不展开来讲了,不会的小朋友可以自行百度。简单讲一下他的用法。

List<? extends E> 协变 又称为上边界

这种方法能帮助确认它的子类型。编译器知道里面存放着是他的子类(含自身)。具体是什么子类,就不清楚了。为此编译器允许你通过get方法获取里面的数据 E e = List<? extends E>* list.get(...); 想要进行转换需要使用typeof。然而为什么该数据只能add为null的数据,不能添加子类数据呢?==个人理解,如果有错请提出==之前说过泛型代表有且只有一种确切的类型,该类型是未知的。打个比方 List< Fruit > 和 List< ? extend Fruit> 前者可以添加水果,后者却只能添加null。WHY? **List<?>:只能包含某种位置对象类型的有且只有一个的集合。安全* List <? extend Fruit > 也只能有且只有一种子类。不知道哪一种?所以只能拿不能添加。

List<? super E > 逆变 又称为下边界

这个前面跟上面有点相反。只能add,不能get。编译器知道里面存放着是他的父类(含自身)。具体存放着是什么父类。也是不清楚的。但是可以确定一点就是?能够容纳 参数E(保存他的子类)。你可以向它添加 E 和 E的子类的数据。但是如果你想get值。也是跟上面一个原因。有且只有一种类型。该类型未知。所以你不能get。

在书中有这样一句 PECS 表示 producer-extends,consumer-super 换句话说,如果参数化类型表示一个生产者T,就是用< ? extend T >; 如果他表示消费者T,就是用< ? super T >。何为生产者,何为消费者。生产者有东西,对外调用只能给get;消费者穷得只剩钱了,对外调用只能买买买 add。

第32条:谨慎并用泛型和可变参数

这里要提醒大家。如果你看到这里不知道什么是堆污染的话。可以自行百度了。我的见解尚浅。Sorry!
留个白,回头再刷得时候补

第33条 有限考虑类型安全的异构容器

何为异构容器,见到来说 用Set< Class<?>>,List < Class<?> >。但是最常用还是HashMap< Class<?>,Object>
在这里插入图片描述### 如图我有个大问题。我试着去破坏它,还是未能成功,大牛们提点提点。

注意1: Object和Class<?>是没有任何关系的,换句话说HashMap中key value是没有对应的关系。如果不是在方法中维护这种关系。该类型是一个不安全,没有很合作用的数据类型。
注意2: 上图的疑惑
注意3: Favorites不能存取不可以具体化的来行,你可以保存String或String[],因为他们都有对应的Class类。你不能保存List< String >,List < Integer > 。他们只有同一个 Class类。就是原生类,List.Class.。虽说可以存放,但是会破坏Favorites 内部结构。图上用例也显示了可以这样。

枚举和注解

第34条 用enum代替int常量


import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @Author: CCN
 * @Date: 2021-05-29 14:14
 */
public enum Operation {
    PLUS("+"){
        @Override
        public double apply (double a , double b) {
            return a + b;
        }
    },
    MINUS("-") {
        @Override
        public double apply (double a , double b) {
            return a - b;
        }
    },
    TIMES("*") {
        @Override
        public double apply (double a , double b) {
            return a * b;
        }
    },
    DIVIDE("/") {
        @Override
        public double apply (double a , double b) {
            return a / b;
        }
    };
    private final String symbol;

    Operation(String symbol) {
        this.symbol = symbol;
        //stringToEnum.put(symbol,this); illegal
    }
    @Override
    public String toString() {
        return  symbol ;
    }

    abstract double apply (double a , double b);
    public static final Map<String,Operation> stringToEnum = Stream.of(values()).collect(Collectors.toMap(Operation::toString,e ->e));

    public static Optional<Operation> fromString(String symbol) {
        return Optional.ofNullable(stringToEnum.get(symbol));
    }

}

注意到illegal没有。如果是正常的类,你是可以在构造函数中调用自己的静态属性的。在我的java基础 一些可能还未知的java知识.明确知道了被static final 修饰的属性还是方法 是在类初始化之前就已经初始化完成。理应可以在构造函数当中调用这些值。为什么在枚举类中不可以!!!反编译一下该代码,一切就明白了
在这里插入图片描述
也就是说 static final 在类未初始化就已经存在,这不包括枚举类!!!!这也导致了一个问题即装载和初始化枚举类余姚空间和时间的成本,但是实际中几乎注意不到这个问题。.

第38条 用接口模拟可扩展的枚举

不管在枚举类中用switch,抽象方法的方法。都很难扩展枚举类型。最好的方式如图代码

 public interface Operation { double apply(double x, double y)}
 public enum Basicoperation implements Operation {
  PLUS("+") { public double apply (double x, double y) { return x +y}}
  ...省略
  private final String symbol;
  BasicoperationString symbol) { this.symbol = symbol};
  }
 //扩展的
 public enum ExtendedOperation implements Operation {
   EXP("^"{ } 
   ...省略
 }

第39条 注解优先于命名模式

单个注解复用

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTest1Container.class)
public @interface ExceptionTest1 {
    Class<? extends Exception> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest1Container {
    ExceptionTest1[] value();
}
}

在这里插入图片描述

第41条 用标记接口定义类型

有这样两个概念 标记接口(Serializable)和标记注解。同样是为了获取相应的运行时信息。到底应该怎么用?怎么区分?经验不足,以后再补充。

Lambda和Stream

第43条 方法引用优先于lambda

在这里插入图片描述
唯一有疑惑的就是有限制类型。看着对应的lambda表达式。感觉像调用类构造器后在调用方法。写一个实例。感觉像是在调用非静态方法。。
在这里插入图片描述

第44条 坚持使用标准的函数接口

在这里插入图片描述
这6个基本接口各自还有3种变体,分别可以作用于基本类型int, long, double。只需要在对应的接口命名前加上想要的基本类型首字母大写就能找到了!!

第46条 有限选择Steam中无副作用的函数

foreach:它是显示迭代,因而不是适合并行。应该只用于报告Steam计算的结果,而不是执行计算。

静态带入Collectors的所有成员是惯例也是明治的,因为可以提升Steam pipiline的可读性

多用toMap,groupingBy,toList,toSet,koining整合出新的数组对象
详细的介绍各个API可能会在二刷之后单独展开。待完成!

方法

第50条 必要时进行保护性拷贝

    public final class Period {
      private final Date start;
      private final Date end;
    
    public Period(Date start, Date end) {
         if(start.compareTo(end) > 0) throw new IllegalArgumentException(start + "after" + end);
         this.start = start;
         this.end = end;
      }
   }

如图的代码,当你正确创建对象的时候,因为是引用传递。Date的对象还不是一个不可变对象
end.setYear(…)的话就会破坏了你的构造函数。(什么是不可变对象。一般就是final修饰的属性,private修饰不让外部调用,构造一个不可变对象有多种方法,不了解的同学百度一下).这时候你需要把代码改成

 public Period(Date start, Date end) {
         this.start = new Date(start);
         this.end = new Date(end);
         if(start.compareTo(end) > 0) throw new IllegalArgumentException(start + "after" + end);  
      }
      //这里还要注意如果你对外发布了get方法。返回值别傻傻的把引用传出去。一样会遭受到破坏。正确的做法
      public Date getStart() { return new Date(start.getTime())}

这又出现了一个新的问题?为什么需要把赋值提前,检验拷贝后的值!这跟多线程又有关系了,如果你第一时间检验旧址,通过了,然而另一个线程修改了该值。还是会造成破坏。被称作:TOCTOU攻击(我:啥玩意)
对于非final类型,禁止使用clone记性保护性拷贝。方法可能被重写了!!!
再顺带说一句啊,书中所讲Date已经过时了,但是我看到的项目里面的关于时间还是用这个。主张要用Instant/ LocalDateTime/ ZonedDateTime

在你的可控范围,就是你开发的包中。不对外发布的给其他人用的接口,可以不必每次都那么麻烦进行检验,约束。但你要知道代码任何一次出错都会造成巨大的损失。用心写好代码,做一个专业人士。

第52条 慎用重载

在这里插入图片描述
答案可能和你出乎你的意料。三次都是调用了同一个方法。重载方法好像失效一般。这里重新巩固一下RTTI,多态。

重载方法 :调用哪个重载方法编译时做出的决定。声明是什么类型,他就是什么类型。对于重载方法的选择是静态的。基于RTTI,在编译时查看类型信息。为此基于安全而保守的策略,永远不要导出两个具有相同参数数目的重载方法,也不要定义可变参数的重载方法。这会导致代码很混乱。

覆盖方法: 对于被覆盖的方法的选择是动态的,及在运行时才确切知道要调用的方法。这和多态有关。

在这里插入图片描述
上面的栗子都是不正确的方法重载导致的问题。这里重点介绍一下p.submit为何编译不通过。
在这里插入图片描述
可以很明确知道submit方法是可以接收Runable函数接口的。但还是无法通过。这就因为System.out::println重载的方法太多。是一个不精确的方法引用导致。
在这里插入图片描述java8类型推导:点击这里.

第54条 返回零长度的数组或者集合,而不是null

在下方法放回数组 特别是在查询数据库的时候,一般查询返回结果为null回不加修饰的返回给调用者。这样做十分的不友善。导致在调用者那还需要做一次非空判断。如下例子

 public List<Fruit> listFruit() {
     return session.selectList(namespace)List<Fruit> list = service.listFruit();
 if(list!=null) {...}
 }

是不是特别容易写出这样的代码,特别是全栈开发的攻城狮。

 public List<Fruit> listFruit() {
        List<Fruit> list=  session.selectList(namespace)
     return ObjectUtils.isNotEmpty(list)? list:Collections.emptyList()}

千万别指望通过预先分配传入同A仍然有的数组来提升性能,研究表明,这样只会适得其反
简单来说,不要通过预先初始化多个为null的数组。如下图所示在这里插入图片描述

第55条 谨慎返回Optional

在java8中新加入了处理特定环境下无法返回任何值得方法。Optional< T >类。之前方法要么抛出异常,要么返回null。经常配合steam来判断对象的是否为空。

   steam.filter(Optional::isPresent)
   .map(Optional::get)

详细的api请查看文档。

因为Optional属于给值对了一层包装。所以他是需要额外的开销的。那么在什么情况下方法应该返回一个Optional< T >对象呢? 如果无法放回结果并且当没有返回结果时客户端(调用者)必须执行特殊的处理,那么就应该声明该方法返回值Optional< T > 强烈建议只用作单一对象,而不是map,collection,arrays中;

第56条 为所有导出的API元素编写文档注释

先模仿,以后再看深入理解。~~~~

异常

第76条 努力使失败保持原子性

这是一个程序员经常不注意的一个规范。在调用会出现异常方法时。没有备份原来的数据。导致异常前后对象状态不一致的情况。如下代码

     public Object pop() {
     if( size == 0) throw new EmptyStackException(); [1]
     Object result = elements[--size];
     elements[size] = null;//避免内存泄漏
     return result; 
    }

如果取消的【1】中的检查。当这个方法企图从一个空栈中弹出元素时,他然仍会抛出异常。然而却将会导致size保持在不一致的状态(负数)。从而导致抛出ArrayIndexOutOfBoundException异常。这个异常会还会扰乱调用者。
==所以:==应该在全部改变状态之前,做完全部的异常测试。保证数据前后的一致。

并发

第78条 同步访问共享可变资源

在这里插入图片描述

先猜想下一下这段程序大概执行多长时间会停止?

再看一个小知识:java语言规范保证了读或者写一个变量是原子的,所谓原子其实就是对应的反编译过后的一条指令。除非这个标量的类型是long或者double。因为在java中每次读出4个字节的数据给变量赋值。然后long,和double字长为8字节。所以需要两次赋值。所以在高并发下,有可能导致数据状态不唯一,再赋第一次值的时候,数据就被读取了。从而导致数据不完整。

回到正题,直接的那段代码是永远不会停止的:因为后台线程永远在循环。
这里出现有一个我还未懂的知识。先记录下来。

由于没有同步,就不能保证后台线程何时”看到“主线程对flag值得修改们,没有同步,虚拟机将以下代码

     while(!flag) i++;
     
     转变为
     if!flag)
     while(true) i++;

**不懂点:**这种优化被称作提升(hoisting),正是OpenJDK Server VM的工作(也称活性失败! )

这里用我自己的理解来解释这个问题(如果有误请提出!):每个一线程在执行程序的时候都有自己独自的stack(栈)空间,在自己的栈空间里面执行自己的程序。对于用到的变量,他们会复制一份到自己的缓存当中,这跟jmm有关系(这里不深入讲解了,后面继续学习jvm时候会深入),所以这样会导致主线程有自己的flag值,后台线程也有自己的flag值。我也测试了一下把boolean变成Boolean(有懂我为什么这样么想的朋友吗?)。然是会导致无法整成结束。

有两种编程方式能让代码顺利结束

第一种:

     public class StopThread {
    private  static boolean flag;
    private static synchronized void stop() {
        flag = true;
    }
    private static synchronized boolean isStop() {
        return flag;
    }
    public static void main(String[] args) throws InterruptedException {

        new Thread(() ->{
            int i = 0;
            while (!isStop()) i++;
        }).start();
        TimeUnit.SECONDS.sleep(1);
        stop();
    }

}

这种理解起来比较简单。synchronized锁的是静态方法,是类锁。全局唯一。只要一个线程调用一个带有synchronized的静态方法,其余的线程无法访问其他带有synchronized的静态方法。当主线程调用stop()修改了flag的值。后台线程是不会判断isStop()的,会导致堵塞。然后主线程释放了锁之后。后台线程执行isStop。获取最新的flag属性值。停止该线程。

第二种:直接给变量flag 加一个 volatile 修饰。让该属性带有可见性.其实就是每次读的时候你让全部线程都给我去内存当中去读取,别缓存起来。每次改的时候直接改内存的值。。这样所有的线程就能得到实时的值。(同样也不深入了,想深入了解的同学百度一下)

第83条 谨慎延迟初始化

静态域的初始化,建议使用静态内部类,因为内部类只有在只用到的时候才会加载到jvm当中。

   private FidldType field;
   private synchronized FieldType getFiled() {
   		if(field == null) field = FildHolder.field;
   		return field;
   }
    private static class FildHolder {
      static final FieldType field = init();
      private static FieldTyoe init() { return new FieldType}
 
    }

如果处于性能的考虑而需要对实例域使用延迟初始化,就使用双重检测模式,代码如下
在这里插入图片描述
先分析方法2中的代码:当多个线程同时调用getFiledType2()。如果已经初始化完成了,就会直接返回结果,不会发生堵塞。如果尚未初始化,。synchronized(this)锁当前对象,首要前提是当前对象是一个单例对象。现在只有一个线程获取到了锁,进去下一步,为啥这里还需要判断fieldType 是否已经初始化呢。因为多个线程同时在外面堵塞。不对尝试获得锁。当第一个完成初始化的线程释放锁的时候,其余的线程也会拿着锁进去内部查看,是否真正的初始化完成。第二个判断能避免被堵塞的线程进程2次初始化。

方法1是书中给出的代码。==理由如下:==局部变量result的做哦那个是确保field旨在已经被初始化的情况下读取一次。可以提升性能。而更加优雅。我试着去分析一下为何如此。因为volatile修饰的域是直接从内存当中取值。像方法二那样的话,每用一次fieldType都是需要去内存拿值得,这会导致多出很多条取值指令。

doubleCheckLock弊端(指令重排)

import java.util.Date;
	public class MySystem {
		private Static MySystem instance = null;
		private Date date = new Date();
		private MySystem() {}
		public Date getDate() {
			return data;
		}
		public static MySystem getInstance() {
			if(instance == null) {             // [1] 第一次test
				synchronized(MySystem.class) { // [2] 进入synchonized代码块
					if(instance == null) // [3] 第二次test
					instance = new MySystem(); //[4] set
				}
			} //[5] 退出 synchonrized代码块
			return instance; //[6];
		}
	}

如上图的代码,可能存在的工作场景

线程执行步骤
A在[1]处的判断结果为 instance == null
A在[2]处进入synchronized代码块
A在[3]处判断结果是 instance == null
A在[4]创建MySystem的实例并将其赋值给instance字段
" "此时线程B进入
B在[1]处判断结果是instance != null;
B在[6]得到instance结果
B调用getDate()试图获取data的值

这里就出现问题了,普通成员变量的赋值,不一定要构造指令函数内部执行完成,详细看我的java内存模型。也就是说date还没有被初始化成功,那么引用类型默认值是null。线程B调用getDate()返回值为null。这就是doublechecklock的弊端。但是可以给data加上final保证在new指令内部完成。也可以给instance添加上volatile。保证其他线程看到volatile字段赋值,也可以看到之前对volatile字段的赋值的结果。但是volatile字段与synchronized性能开销几乎相同。本来就double-checked-lock就是为了避免synchronized引起的性能下降。

序列化(讲得是啥玩意,那么难)

在这里插入图片描述
EnumSet源码反序列化代理上写了这句话,有被笑到

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值