《Effective Java》

文章目录

一、创建和销毁对象

1.1 使用静态方法代替构造器

1、静态方法好处

  • 静态方法见名知意

    		private volatile static Cat cat;
        public static Cat getInstanceSingleton() {
            if (cat == null) {
                synchronized (Cat.class) {
                    if (cat == null) {
                        cat = new Cat();
                    }
                }
            }
            return cat;
        }
    
  • 不用重复的建对象-单例、Gson打日志

  • 可以返回子类型

       public static Animal getInstance() {
            a = new Cat();
            return a;
        }
    
  • 可以根据静态方法的入参,返回不同类型的对象

        public static Animal getInstance(String name) {
            if ("Dog".equals(name)) {
                return new Dog();
            }
            
            if ("Cat".equals(name)) {
                return new Cat();
            }
            return new Animal();
        }
    

2、缺点

  • 如果此类只想通过静态工厂方法获取实例,由于单例模式需要private构造器。所以,此类无法被extend

1.2 @Builder vs @Accessors(chain = true)

1、使用场景

  • 类的属性太多,各种参数的有参构造器,人为都记不住属性名称了

  • setter方法,在构造过程中JavaBeans可能处于不一致的状态

    User user = new User(); 
    user.setName("mjp");
    user.setAge(1);
    有些对象从创建到销毁需要保持一致性,但是JavaBean对象不符合这点需求。
    JavaBean对象的构造过程则先是通过创建对象,随后在通过setter方法来设置必要的参数。
    直到销毁前,JavaBean对象都是可变的,或者说JavaBean一直在构造过程中。
    在需要一致性对象的程序使用JavaBean对象,会可能导致失败。
    

2、链式操作,都需要结合@Data注解

3、builder 和 Accessors

  • builder创建的对象 不如 Accessors轻量
  • 使用Accessors时,再使用对象copy的时候,使用org.springframework.beans.BeanUtils.copyProperties(s,t)

4、lombok注解

@Data : 注在类上,提供类的get、set、equals、hashCode、canEqual、toString方法
@AllArgsConstructor : 注在类上,提供类的全参构造
@NoArgsConstructor : 注在类上,提供类的无参构造

1.3 私有构造器,强化单例

    private volatile static Cat cat;  // 01.懒汉模式 + 03.对象创建过程中防止指令重排序
    public static Cat getInstanceSingleton() {
        if (cat == null) {
            synchronized (Cat.class) { // 02.防止并发都进来
                if (cat == null) {
                    cat = new Cat();
                }
            }
        }
        return cat;
    }

 volatile:防止指令重排序

 * instance = new Singleton();分为三个动作
 1.堆内存开辟一片空间,比如0X01
 2.堆中创建Singleton,有初始化赋值的话,赋值
 3.将对象内存地址返回给对象引用变量instance,instance存有0X01,instance在栈帧的局部变量表中
 
 cpu为了提高吞吐量,会自动的改变cpu流水线,即指令的操作先后顺序改变,对单线程没有影响,多线程有
 eg: 
执行顺序 23变成让你3,2,导致返回给instance引用对象地址,虽然不为Null,但是根本没有完成赋值
导致t1执行同步代码执行结束的时候,t2判读,Instance!=null,但是instance本身没赋值;t2会直接返回instance

1.4 通过私有化构造器,强化不可实力化的能力

1、为什么私有化

  • 工具类不希望被实例化,因为实例化工具类没有意义
@UtilityClass
public class DateUtil {

    public Integer getInt() {
        return 1;
    }
}

//默认添加了,私有化的构造器
private DateUtil(){
}

别的地方无法通过new的形式创建

GsonUtil

@Slf4j
public class GsonUtil {
    private static final Gson GSON = new GsonBuilder().serializeNulls().create();

    private GsonUtil() {
    }

    public static String toJson(Object object) {
        try {
            return GSON.toJson(object);
        } catch (Exception e) {
            log.error("序列化失败", e);
        }
        return StringUtils.EMPTY;
    }
}
  • 但不提供构造函数的时候,编译器会自动提供一个默认的构造器【这样这个工具类无法具有子类了】

1.5 避免创建不必要的对象

1、哪些方式会创建不必要的对象

  • new String、new Integer(建议使用Integr的valueOf()静态工厂方法)

  • 循环中拆箱装箱

  • while循环,为了防止死循环、不断创建对象等。需要集合业务指定超过最大的循环次数

    在高并发场景中,避免使用”等于”判断作为中断或退出的条件

    2、包装类:占用更大的空间(但是包装类能表达 null 的语义)

    正常情况下布尔值就是true和false,但是如果用户传递一个错误的skuId,那么此计算此skuId是否在灰度中,返回结果应该为null,因为true、false都不合适

    交易额,异常的时候就因该为null,而非0

    【推荐】自动转换(AutoBoxing)有一定成本,调用者与被调用函数间尽量使用同一类型,减少默认转换

    //WRONG, sum 类型为Long, i类型为long,每次相加都需要AutoBoxing。
    Long sum=0L;
    
    for( long i = 0; i < 10000; i++) {
      sum+=i;
    }
    

1.6 消除过期对象的引用

1、sop

  • static变量属于类,类在变量在堆中内存就一致在。如果是集合,则元素对象也不会被回收,对象链GCRoot都不会被回收

    尽量在方法内部,方法结束,变量也被回收了

  • 数组元素,不用则及时置为null,array[i] = null,避免内存泄漏。(参考list的remove方法:elementData[–size] = null)

1.7 关闭资源: try with resource

1、优点

  • 使用正常的try-catch。在finally中关闭资源时,close方法也可以能出现异常导致资源关闭失败,所以需要再次try close方法。过于繁琐

    FileInputStream fis = new FileInputStream("");
            try {
                int read = fis.read();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    

2、使用前提-资源类实现了Closeable接口

  • 确保了资源迅速释放,避免了资源耗尽,避免了异常和可能发生的错误
  • 更简洁,更清晰
try(FileInputStream fis = new FileInputStream(new File("a"))) {

        } catch (IOException e) {

        }

二、对于所有对象都通用的方

2.1 equals

1、为什么要重写

  • 希望类具有“逻辑相等”

2、重写了equals的类

String

public boolean equals(Object anObject) {
        if (this == anObject) { //01.地址相同,则一定“逻辑”相同
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) { //02.字符串的长度不同,则一定“逻辑”不同
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) { //03.每个字符对比
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

Integer

 public boolean equals(Object obj) {
        if (obj instanceof Integer) { //01.不是同类型的,肯定“逻辑”不同
            return value == ((Integer)obj).intValue();//02.是Integer类型的则比较值的大小  【-128,127】则相同,否则不同
        }
        return false;
    }

1Integer使用equals来比较值的大小
2、但是【-128127】可以直接使用 == 来比较大小
        Integer a = 127;
        Integer b = 127;
        System.out.println(a == b); //true
        Integer c = 128;
        Integer d = 128;
        System.out.println(c == d); //false

3、约定规范注意事项

  • 对称性:注意父子继承的设计

​ animal.equals(cat)//true

​ cat.equals(animal)//false

Cat的equals中

if(obj instanceof Cat){
   //显然这里animal不是Cat类型
}

Animal的equals中
if(obj instanceof Animal){
   // Cat extends Animal,所以cat 是Animal类型
}
  • 传递性:

​ x.equals(y) 为true: 比较的是x和y的a、b的属性

​ y.equals(z) 为true: 比较的是y和z的b、c的属性

​ x.equals(z)如果比较的是x和z的a、c属性,则不能保证传递性

  • 一致性:

​ 如果x.equasl(y)中equals方法代码中涉及random随机数、时间戳等可变的元素,则无法保证equals方法每次都返回同样的值

1、sop

  • 推荐使用:Objects.equals(x,y)

2.2 hashcode

1、sop

  • 重写euqals,最好重写hashcode
  • 重写hashCode方法时,属性之间尽量不要有关联

​ a = b和c属性的计算结果,则hashCode方法中,要么使用a,要么使用b和c。不要a和另外两个一起决定hash值

  • 尽量使用关键属性
  • hashCode方法中也不要使用随机数、日期等做逻辑

2、为什么重写hashcode

  • hashCode的通用约定:equals为ture,则hashcode也要为true
  • 如果不重写hashcode,则会产生下列问题:
String s1 = "majinpeng02";
String s2 = "majinpeng02";
s1.equals(s2); //true
但是hashcode没有重写,二者的hashcode值可能不一样,假如不一样。则hashmap.put(s1,1); hashmap.put(s2,1)则二者都能存入map。和我们hashmap约定的key冲突不一致了
所以,hashmap也需要重写hashcode

3、重写hashcode的作用/好处

  • 符合equals相同则hashcode也相同

  • 比较字符串是否为相同字符串的时候,首先可以使用hashcode值进行比较,hash值不一样则equals一定不同。如果hash值相同,再使用equals,一个一个字符进行对比判断。所以,可以提高效率

4、方法特点

  • equasl相同/不同,则hashcode一定相同/不同
  • hashcode相同,equals可能不同

5、String的hashcode方法:Object本身的hashcode是native方法

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

2.3慎重重写 clone方法

1、对象调用clone方法,可以跳过构造器的调用!

2、clone是浅copy,copy出来的对象和原对象指向同一块堆内存,一个修改了内存元素,会影响另一个。

3、不可变类,一定不要提供clone重写方法

2. 4 Comparable属于java.lang的接口

1、什么时候需要实现此接口

  • 当一个类,想要能够被分类、排序、搜素以及用于基于比较的集合中。则应该实现Comparable接口

2、String、Integer实现此接口重写compareTo方法

挨个字符比较大小

//   挨个字符比较大小
   public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }
    
    //自定义compare方法,不使用减法,避免int溢出
    public int compareTo(Integer anotherInteger) {
        return compare(this.value, anotherInteger.value);
    }

    public static int compare(int x, int y) {
      //这里没有使用x - y,而是使用了x > y进行比较。就是防止如果y是负数,则Integer.MAX_VALUE - 一个负数,结果溢出int值
        return (x < y) ? -1 : ((x == y) ? 0 : 1);
    }

3、使用注意事项

  • compareTo方法可以用于字典比较。但不能跨越不同类型的对象进行比较,否则classCastException

4、compareTo方法返回值含义

  • String实现Comparable接口的,重写compareTo方法,则可以比较字符串“大小”, 0相同、1大于、-1小于; 同样Integer实现Comparable接口,用于比值的大小

5、区别Comparator接口

  • Comparable是类的语言特性,表明这个类具有比较、排序等功能。本质上Comparable属于内部排序,Comparator是外部排序。

  • 属于java.util的接口,一般用于集合元素的排序

  • 是函数式接口,只有一个方法:int compare*(T o1, T o2) :* 0相同、1大于、-1小于;

    集合元素排序

//根据Dict对象的sort字段降序排序
dictList.sort(Comparator.comparing(Dict::getSort).reversed());
//根据Dict对象的sort字段升序排序
dictList.sort(Comparator.comparing(Dict::getSort));
//按照字段降序,相同的话,再按照另外一个字段降序
skuCategoryRuleDOS = skuCategoryRuleDOS.stream().
                sorted(Comparator.comparing(SKUCategoryRuleDO::getSkuCategoryId).reversed().
                       thenComparing(SKUCategoryRuleDO::getSkuTemperatureZone,Comparator.reverseOrder())
                      )
                .collect(Collectors.toList());
  • 其中Comparator接口中用于排序的Comparator.comparing()方法,内部也是调用了Comparable接口的compareTo方法

Comparator接口中其它reversed()、thenComparing()方法,则调用了本身的compare()方法进行再排序

三、接口和类

3.1 使类和成员的可访问权限最小化(封装)

-优先考虑使用private

1、封装的好处:

  • 对于外部,隐藏了内部数据和实现细节。把API和实现隔离。接触系统不同组件之间的耦合,各自可以独立开发不受影响

2、类属性为什么不建议public修饰

  • 一开始要是提供的public访问修饰符,后续版本迭代等都不可以再收回权限了
  • 子类中的访问级别就不允许低于超类中的访问级别

3、使用public getter、setter代替public成员变量:public类的实例,绝不能是public的

原因

public class User {
  public String name;
}
1、这个类的name是可以直接被访问的,当域被访问的时候,我们将失去对这个域的控制权。后续,想要将name字段改为nickName,那么使用方就全部报错
2、通常包含public属性的类,是线程不安全的
User user = new User();
user.setName(null);

    public void setName(String name) {
        if (StringUtils.isBlank(name)) {
            throw new NullPointerException();
        }
        this.name = name;
    }
   

好处1:可以在setter方法中,对set的值进行逻辑校验


public class User {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

好处2


//好处2:可以修改属性的名称而不用关注调用方
//现在我觉得,name字段描述的不准确,换个名称为nickName,直接在本类中修改就ok,不用关注调用方
public class User {
    private String nickName;

    public String getName() {
        return nickName;
    }

    public void setName(String name) {
        this.nickName = name;
    }
}

4、如果想要数组、集合,对象地址不可变 && 内容元素也不可变,建议使用 private final修饰,

  • final:虽然引用本身不能被修改,但是它所引用的对象却可以被修改
public static final String[] ARRAY = new String[];
影响:
虽然ARRAY引用不能修改,但是ARRAY内部的元素是可以被修改的

解决:

假的final,最好private修饰[数组、集合]

//假的final,最好private修饰[数组、集合]
对象不可变了,但是元素内容还是可以被操作改变的。如果不想被外部的操作影响,则必须private
private static final Map<String,Long> map = new HashMap<>;
private static final List<Long> list = new ArrayList<>;
private static final String[] ARRAY = new String[];

可以深copy成员变量,防止改变影响成员变量值


//可以深copy成员变量,防止改变影响成员变量值
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {

    private final char value[];//02.这个value数组,在String内部被很多其它地方使用。所以,不能改变它的值
  
     //01. 这样对toCharArray的返回结果数组进行操作,不会影响原本的value数组的元素内容
     public char[] toCharArray() {
        // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }
}
  • 特殊情况:真final不可变,可以在类中提供public成员变量

    private修饰和public修饰效果一样

public statci final Longsize = 200;
public statci final Integer code= 0;
public statci final String birthday = "2022-06-01";

3.2 可变性最小化-迪米特法则:即最小知道原则

-优先考虑使用final

1、不可变类特点

  • 不可变类:final修饰类 / private构造器

2、final类优点 & 缺点

  • 可以重复使用、共享。线程安全

    安全:(1.8中DateTimeFormatter不可变,线程安全、SimpleDateFormat可变并发不安全(线程A定义的合适为:YYYYMMDD,可能被线程B改为YYYY_MM_DD))

    共享:不需要进行保护性拷贝(拷贝始终等于原始的对象)。因此,不需要为不可变类提供clone方法

  • 唯一缺点:每一个不同的值都需要创建一个新的对象。

3、不可变类,需要遵循的原则

  • 保证类不会被继承,final无法被继承

  • 既不要从外部拿,要不要返给外部

    确保在该类的外部不会获取(get)到可变对象的引用、也不要从外部拿,然后set 可变对象。同时建议:使所有的域都是private final ,参考3

4、类属性是集合、数组。使用注意事项

  • 数组、集合最好是private属性,如果不希望关键属性数组、集合受到外部操作的影响,则 既不要从外部获取get并赋值
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Integer age;
    private Map<String, Long> map = new HashMap<>();
}


        Map<String, Long> source = new HashMap<>();
        source.put("mjp", 1L);

        // 01.外部数据源source,给类关键集合map赋值
        User user = new User(18, source);
        Map<String, Long> target = user.getMap();
        System.out.println(target);//{mjp=1}

        // 02.外部数据源发生了变化
        source.put("wxx", 1L);

        // 03.类集合也会内容也会发生变化
        System.out.println(target);//{mjp=1, wxx=1}

//正确做法
1、不要给关键数组、集合提供get、set方法。要想get则深copy返回。即不要使用@Data注解
2、建议private final
  
//补充
上述,基本都是针对final类。我们日常开发,还是不这样做的,所以,要清楚你提供了get、set方法则类中的集合、数组内容会收到外部数据源的内容变化而变化
  • 也不要,直接返给外部。可以内部提供深copy方法,返回内容和关键属性一样的对象,但是不执向同一块内存地址。操作返回结果,不影响类本身的关键字段内容【3.1好处2】

注意⚠️:Map<String,Map<String,Long>>这种,内部的map也需要深copy一份,要不然也会影响

3.3 复合优于继承-合成复用原则

尽可能使用对象组合而不是继承的方式达到复用的目的

1、复合

@Data
@Accessors(chain = true)
public class SmallName {

    private String foreverName;
    
    @Resource
    private User user;//符合添加了user类,可以使用user对象的方法

}

2、继承的缺点

  • 子类只会比父类更大,权限也是,只能放大。类会越来越大。无法收缩了

    特例:接口的实现类中实现的方法的修饰符,全部都是public

  • 父类的方法,子类也全部拥有。有时候继承某个类,并不是想拥有此类的所有方法!

  • 跨包的继承,则更加危险

  • 违背了封装的原则,子类需求去了解父类的实现,否则随着不同版本父类的代码发生了改变,即使子类完全没有改变代码,也有可能被破坏

    覆盖了父类的方法后,结果不符合预期:典型的:add元素的count统计

    public class MyCountSet extends HashSet<Integer> {
        /** 统计"有史以来"向该集合中添加过的元素个数 */
        private int count = 0;
    
        @Override
        public boolean add(Integer num) {
            count++;
            return super.add(num);
        }
    
        @Override
        public boolean addAll(Collection<? extends Integer> nums) {
            count += nums.size();
            return super.addAll(nums);
        }
    
        public int getCount() {
            return count;
        }
    
        public static void main(String[] args) {
            MyCountSet countingSet = new MyCountSet();
            countingSet.addAll(Lists.newArrayList(1));
            System.out.println(countingSet.getCount());//2
        }
    }
    1、现象:
    addAll方法,添加了集合中1个元素,count应该是2,但是实际输出2
    2、原因:
    子类重写了addAll方法,但是不知道父HashSet的addAll的具体实现
    父类HashSet的addAll,调用了自身的add方法。子类也重写了add方法,就导致:子类的addAll方法中count += nums.size();算了一次,同时,子类的add方法中count++又算了一次
    3、执行步骤
    a、子类addAll,count += nums.size();此时,count值为1
    b、super.addAll,调用HashSet的addAll
    c、HashSet的addAll,调用了自身的add()
    d、HashSetadd(),被子类重写了,所以,调用子类的add
    e、子类的add中,执行count++;此时,count值为2
    f、再调用父类add完成将元素加入
    4、解决
    将子类的addAll中的count += nums.size();删除
    

    HashSet的addAll方法

        public boolean addAll(Collection<? extends E> c) {
            boolean modified = false;
            for (E e : c)
                if (add(e))
                    modified = true;
            return modified;
        }
    
  • 父类构造器绝不能直接/间接调用可被覆盖的方法,否则可能不符合预期(程序失败、npe等)

    Sup

    public class Super {
      public Super() {
      	overrideMe ();//02-造器能调用可了被覆盖的方法❌
      }
      public void overrideMe () {
      }
    }
    

    Sub

    public final class Sub extends Super {
      private final Instant instant;
      Sub() {
        super();//01.
      	instant = Instant.now();//05.完成赋值
      }
      @Override
      public void overrideMe () {//03.进入子类的重写方法
      	System.out.println(instant);//04.此时instant为null,还没有值!!!
      }
      public static void main(String[] args) {
        Sub sub = new Sub();//00.执行顺序如上
        sub.overrideMe ();//06.子类方法,有值了
      }
    }
    

3.4 接口优于抽象类

1、接口和抽象类的区别

  • 接口,所有的方法默认是public,属性都是;类默认是default

    java8-default方法

    1、作用
    为了扩展接口的功能。接口新增方法,如果是public的,则所有实现类都需要实现,这样就不符合向下兼容
    实现类自动拥有和接口一样的default方法,直接用,不需要再实现
    2、eg
    List extend Collection接口,此接口自己实现了removeIf方法
        default boolean removeIf(Predicate<? super E> filter) {
            Objects.requireNonNull(filter);
            boolean removed = false;
            final Iterator<E> each = iterator();
            while (each.hasNext()) {
                if (filter.test(each.next())) {
                    each.remove();
                    removed = true;
                }
            }
            return removed;
        }
        
    3、最好不要在接口已经存在的情况下(可以在接口第一次创建的时候添加),再添加新的default方法,这对于接口来说非常危险
    eg:removeIf方法,对于大多数Collection接口接口的实现类,都没有影响,但是对于已经实现了Collection接口的org.apache.commons.collections4.collection.SynchronizedCollection
    则可能存在问题。
    如果客户端在SynchronizedCollection的实例上调用removeIf方法,同时另外一个线程对集合进行修改,就会导致并发修复爱Exception
    
  • 接口中只能有常量【但是不建议,接口只提供行为规范】、抽象类中可以有成员变量

    常量型接口-是接口的错误使用

    1、原因
    类实现常量接口,这对于这个类的用户来讲并没有实际的价值。实际上,这样做返回会让他们感到更糊涂
    2、建议
    使用Enum
    
  • 抽象类单继承【但是了类可以实现多个接口】; 接口多继承接口可以同时继承B、C接口【但是接口B和接口C,不能出现冲突方法】

    接口多继承注意事项

public interface BInterface {
    void eat();
}
public interface CInterface {
    String eat();
}

//这样A接口,就不知道eat方法具体是哪个【have unrelated return types】
public interface AInterface extends BInterface, CInterface{

}
  • 接口是行为规范,具体实现逻辑由实现类自己定义 ;

    抽象类定义了大部分公有的具体行为,根据不同子类定义了不同的abstract方法,由子类根据自己的特点实现即可

    抽象类优于标签类

    //01.标签类-eg:它能够表示圆形或者矩形
    public class Figure {
        enum Shape { RECTANGLE,CIRCLE };
        final Shape shape;
        double length;
        double width;
        double radius;
    
        public Figure(double redius){
            shape=Shape.CIRCLE;
            this.radius=radius;
        }
    
        public Figure (double lenght, double width) {
           shape=Shape.RECTANGLE;
            this.length=lenght;
            this.width=width;
        }
      
      //计算不同形状的面积
        public double area(){
            switch (shape){
                case RECTANGLE:return length*width;
                case CIRCLE:return Math.PI*(radius * radius);
                default:throw new AssertionError();
            }
        }
    }
    缺点:
    违背了开闭原则
    新增图形表示,求面积则需要:添加新的case;同时也有可能添加新的成员变量【梯形:(上底+下底)*/2//02.抽象类
    public abstract class AbstractFigure {
      public abstract double area();
    }
    
    // 圆形子类
    public class CircleFigure extends AbstractFigure {
      private double radius;
      public CircleFigure(double radius) {
        this.radius = radius;
      }
      @Override
      public double area() {
        return Math.PI * radius * radius;
      }
    }
    
    // 矩形子类
    public class RectangleFigure extends AbstractFigure {
      private double length;
      private double width;
      public RectangleFigure(double length, double width) {
        this.length = length;
        this.width = width;
      }
      @Override
      public double area() {
        return length * width;
      }
    }
    优点:新增图形,则新增类即可,不需要修改原本的代码,遵行开闭原则
    

如何决定使用抽象类还是接口

要表示is-a(圆形是一个图形、三角形是图形----)的关系,并且是为了解决代码复用的问题,就用抽象类;

表示has-a关系,并且是为了解决抽象和解耦而非代码复用的问题,那就使用接口。

2、abstract方法注意事项

  • 不能+final修饰,否则子类不可以实现了
  • abstract也不能private修饰,要不子类不可见了(可以protect)。abstrtact方法本身就是为了子类去实现的

3、常用接口:

​ Cloneable克隆、Serializable可序列化、Comparable可比较、CharSequence、Runnable可执行

4、为什么接口优于抽象类

  • 存在以下场景,A类即是可以克隆的、又是可以序列化的、又是可以比较大小的、又是可以作为任务执行的。如果上述常用接口都变成了抽象类,那么由于类的单继承,所以A类就不能同时具有以上功能。

    除非把上述接口变成抽象类,而且彼此之间有父子继承关系。但是A类可以只可克隆 + 可序列化,不需要可比较大小 + 可执行

5、接口和抽象类混合

​ A extends B抽象类 implement C接口

HashMap和TreeMap

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
  
}
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
}
  • HashMap和TreeMap都继承了AbstractMap抽象类,所以,二者都具有了AbstractMap的大部分功能【size、isEmpty】;二者分别实现不同的接口,重写不同接口的行为方法,然后具有各自的特点
  • 如果需要在Map接口中新增一个批量删除deleteByIds()方法,那么Map的所有实现类都需要重写方法,否则会报错。实现类很多的话,成本很高,不具有向下兼容性
  • 这个时候就可以,让AbstractMap 去实现 Map接口,然后在Map中新增方法。则仅仅需要AbstractMap去实现新的方法。对于HashMap根本不用感知。
  • 以后,只要接口Map新增方法,只需要AbstractMap去实现即可

6、函数式编程

  • 方法中的参数,只能是对象、值。不能是方法
  • java8后,允许函数式接口作为对象,作为参数传入方法

注意⚠️:这里的对象,本质是方法即策略【按age降序、按name降序、按skuId降序等等策略】

函数式接口特点:注解 + 单个方法

@FunctionalInterface//01.注解
public interface Comparator<T> {
    int compare(T o1, T o2);//02.方法
    //虽然接口还有其它Object对象的方法【equals等】
    //以及接口本身的default方法【java8新增】
    //以及静态方法static【java9新增】
}
除了default方法、static方法、Object类的方法外,有且仅有一个方法,这种就是函数式接口

方法作为对象传入方法eg1

//01.自定义函数式接口作为方法的入参对象:方法的作用是按照User的age降序排序方法
        User u1 = new User(1, "mjp");
        User u2 = new User(2, "wxx");
        List<User> result = Lists.newArrayList(u1, u2);
        //02.自定义函数式接口
        Comparator<User> myComparator = new Comparator<User>() {
            @Override
            public int compare(User o1, User o2) {
                return o2.getAge().intValue() - o1.getAge().intValue();//按照age降序
            }
        };
        result.sort(myComparator);//这里的myComparator,本质就是方法,当法的作用是按照age的大小降序
        
       //02. 函数式接口,可以使用lamda表达式
        Comparator<User> myComparator = (user1, user2) -> user2.getAge() - user1.getAge();

				User u1 = new User(1, "mjp");
        User u2 = new User(2, "wxx");
        List<User> result = Lists.newArrayList(u1, u2);
        result.sort(Comparator.comparing(User::getAge)); //03.这里的sort方法的参数对着,就是方法【函数式接口】,这个方法的内容按照User的age的大小升序排序
Lists.newArrayList(new User(1,"mjp")).stream().sorted(Comparator.comparing(User::getAge)).collect(Collectors.toList());
这里的sort方法就是传入一个比较大小的方法,然后返回一个StreamStream<T> sorted(Comparator<? super T> comparator);

3.5 内部类-优先使用static成员内部类

1、原因:

  • statci成员内部类属于Class类的;非static成员内部类属于对象的【必须先new出外部类对象】

    static和非static

@Data
public class User {
    private Integer age;
    private String name;
    private Emp emp;

    @Data
    public class Emp{
        private Long skuId;
    }

    @Data
    public static class Price{
        private Double money;
    }
}

    @Test
    public void  t(){
        Price price = new Price();
        price.setMoney(1.0);//01.static成员内部类,可以直接new,不依赖外部类对象

        User user = new User();
        Emp emp = user.new Emp();//02.要想获得非static成员内部类的对象,必须先获取外部类对象
        emp.setSkuId(1L);
    }
  • 非staic成员内部类对象,强绑定外部类对象【比如EntrySet对象和HashMap就是】,可能会影响GC

    除此之外,非静态成员类的实例被创建的时候,它和外围类的关联关系也随之建立起来,这种关联关系,需要消耗非静态成员类实例的空间,并且增加构造的时间开销

    Map-Entry

    Entry的getKey、setValue等方法,都不需要访问Map,所以,使用非静态成员类表示Entry则会浪费
    

四、泛型

4.0 背景

1、泛型作用
泛型最重要的初衷之一,是用于创建集合。指定集合能持有的对象类型,并且通过编译器来强制执行该规范。

4.1 不要使用原生态类型

1、使用原生态可能存在的安全问题,因为缺少类型的检查。可能会在运行时导致异常

  • 获取集合元素,并且强转时,会运行时才会报出ClassCastException异常【无法在编译时期IDEA就报出来】

    原生态类型

        List list = new ArrayList();
        list.add(1);
        String s = (String) list.get(0);
        System.out.println(s);
  • 方法入参,使用原生态类型
    @Test
    public void  t() {
        List<Integer> list = new ArrayList();
        add(list, "java");
        for (Integer item : list) {// 02.遍历集合元素,使用Integr进行强转接收时,异常
            System.out.println(item);
        }
    }

    public static void add(List list, Object obj) { //01.方法入参,没有指定泛型
        list.add(obj);
    }

2、带有泛型的类型,传参到无泛型方法中,尽量只读区不写

public static void add(List list) {//为了接受参数的通用性,这里没有带泛型
  //可以是原生态list,但是尽量只是读取list元素,不写(add方法等)
        for (Object o : list) {
            System.out.println(o);
        }
    }

3、泛型的擦除

  • 本质:运行时期,都是class java.util.ArrayList

    泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,即类型擦除。类型擦除主要是为了兼容之前没有泛型特性的代码

            List<Integer> l1 = new ArrayList<>();
            List<String> l2 = new ArrayList<>();
            Class<? extends List> aClass1 = l1.getClass();
            Class<? extends List> aClass2 = l2.getClass();
            System.out.println(aClass1 == aClass2);//true
    

    而数组在运行时,Integr[]和String[]对应的不同的class

  • 在编译时期,List、List无任何关系。所以,他们作为方法的入参时,可以理解为方法的重载

4.2 消除非受检异常

1、@SuppressWarnings*(“unchecked”)*范围

  • 变量、方法、类:尽可能范围要小

4.3 泛型list优于数组

1、原因

  • 数组是协变的

    ArrayStoreException

    Object[] obj 是 String[]的父类型
    List<Object>不是List<String>的父类型
    

    数组 编译时期,不会报异常。运行时会

           Object[] array = new String[3];
           array[0] = 1;
           System.out.println(array[0]);//运行时ArrayStoreException,编译时没问题
    
  • 数组一但创建,大小不可变

2、泛型和可变参数一起使用注意事项

  • 当调用可变参数时,将创建一个数组来保存参数

    void foo(String... args);
    void foo(String[] args); // 两种方法本质上没有区别
    

    ArrayStoreException

        @Test
        public void  t() {
            func("mjp","wxx");
        }
    
        public static void func(String...args) {
            String[] strArray = args; //01.可变参数,本质是数组
            Object[] objArray = strArray;//02.数组的协变的,args、strArray、objArray三者都指向同一块堆内存地址
            objArray[0] = 1; //03.堆地址内元素做了改变,相当于在字符串数组中添加了整型
            String arg = args[0];// 04.ArrayStoreException
        }
    

4.4 优先考虑泛型类和泛型方法

1、什么时候,使用泛型类方法

  • 涉及写后,读取
  • 类型还原

2、什么时候,建议直接使用Object

  • 只读不写

    eg:thrift中定义roc接口中BaseResponse中的数据Data

public class ThriftBaseTResponse<T> {//02.删除T
      public int code = Constants.SUCCESS;
      public String message;
      public T data;//01.这里,set给data值后,直接返回给前端了。后续,不再有读取操作了,其实可以直接使用Object
  }

3、方法入参,不限制类型时,方法返回值返回Object还是泛型

  • 外部交互:返回Object。由使用方自己强转换

        private final Map<Object,Object> map = Maps.newHashMap();
    
        public Object getValueByKey(Object key) {
            return map.get(key);
        }
    
       Object obj = getValueByKey("java");
       Integer u = (Integer)obj;
    
    //使用方法,自己知道key对应的value是什么类型,是Integer、String
    //使用自己强转错误了,ClassCastException异常会在使用方程序报出来,提供方的代码没影响
    
    //如果内部使用这种方式,那么到处都是强转的代码,乱
    
  • 内部使用,返回泛型【方法内部统一帮你强转换了,避免了频繁的类型转换】

    private final Map<Object,Object> map = Maps.newHashMap();

    public <T> T getValueByKey(Object key) {
        return (T)map.get(key);
    }

    Integer res = getValueByKey("java");//这里就不用强转了。但是要求,内部使用方知道,key-“java”,对应的value类型

4.5 使用通配符?提高api的灵活性

1、通配符
钻石形状的 <> 符号, 所以它有时也叫作“钻石语法

  • List和List本质一样

    本质上,T,E,K,V,?都是通配符,没什么区别,只不过是编码时的一种约定俗成的东西

    • E:Element(元素,集合中使用,特性是枚举)
    • T:Type(表示一个具体的 Java 类型)【和U一样】
    • R:返回的返回类型
    • K:Key(键)
    • V:Value(值)
    • N:Number(数值类型)
    • ?:表示不确定的 Java 类型
  • 泛型方法

    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    	Set<E> result = new HashSet<>(s1);
    	result.addAll(s2);
    	return result;
    }
    

2、 ? extends A

则?代表A或者A的子类(类A被继承)或A的实现类(接口A被实现)

  • 读(comparable 和 comparator都是读取)

  • List<? extends Number>,则?可以是Integr、Double、Long都可以

    只可以读

        @Test
        public void  t() {
            List<Integer> l1 = Lists.newArrayList(1,2,3);
            List<Double> l2 = Lists.newArrayList(1.0,2.0,3.0);
            sum(l1);
            sum(l2);
        }
    
        private Double sum(List<? extends Number> list) {
            Double sum = 0.0;
            for (Number num : list) {
                sum += num.doubleValue();
            }
            return sum;
        }
    

3、 ? super A

则?代表 A或者A的父类

    private void add(List<? super Number> list, Number num) { // 这里的list,必须是List<Number>或List<Object>之类的, >=Number
        list.add(num);
    }

4.6 优先考虑类型安全的异构容器

1、背景

  • 为了存什么类型,就可以直接取出来什么类型,不用关心类型转换且不会存在强转错误

  • map本身不限制存入的对象,用户可通过代码将k-v关联起来

        private static final Map<Class<?>, Object> map = Maps.newHashMap();
    
        public static <T> void putInstance(Class<T> aclass, T instance) {
            //这里可以加强校验,如果类型不一致,则throw。防止cast转换异常
            map.put(aclass, aclass.cast(instance));//01.传入的Class和instance是一种类型的
        }
    
        public static  <T> T getInstance(Class<T> aclass) {
            return aclass.cast(map.get(aclass));// 02.取出来的实例一定也是这种类型的
        }
    
        @Test
        public void  t() {
            putInstance(User.class, new User().setName("mjp"));
            putInstance(Animal.class, new Animal().setColour("pink"));
    
            User user = getInstance(User.class);
            Animal animal = getInstance(Animal.class);//03.如果用User接收,会编译提示错误
        }
    

2、无法保存List list这种形式。List.class编译不通过

List、List运行时期一样的class都是ArrayList

只能存、取原生态

putInstance(List.class, Lists.newArrayList(1, "a"));
List list = getInstance(List.class);

//无法->编译报错
putInstance(List<String>.class, Lists.newArrayList("a"));
List<String> list = getInstance(List<String>.class);

4.7 元组

1、背景
有时我们想通过一个函数返回两个值(比如商品价格Double、商品xiao)。

五、枚举和注解

5.0 简介枚举

@AllArgsConstructor
@Getter
public enum FlowTypeEnum{
    RETURN_SUPPLIER(1, "退"),
    REVERSE_ALLOCATION(2, "逆");
    
    private final Integer val;
    private final String desc;
}
  • FlowTypeEnum : 枚举类型

  • RETURN_SUPPLIER: 实例。因为实例是常量,需要需要大写

    • 每个实例,如果定义了val、desc属性

      (使用final修饰是常量,一旦确认则不可变可读不可写,但不能使用static修饰,因为使用了static final就必须进行初始化)

    • 每个实例,都有ordinal()方法,显示该实例(常量)在枚举中的声明顺序(第一个常量顺序为0)

  • @AllArgsConstructor,类似于类的构造器,枚举类(1, “退供”)也需要有参构造器

  • @Getter,类似于类定义了属性后,通过getter方法获取属性值,常量RETURN_SUPPLIER也有两个属性,可以通过getter方法获取其对应的val、desc

  • enum类父类是Enum,不是Object

  • 枚举有构造方法,但是无法执行newInstance反射会报错

5.1 使用枚举代替int常量

三个值形式

@AllArgsConstructor
@Getter
public enum ExecuteTypeEnum {
    UNKNOW(-1, "未知", "WZ"),
    REVERSE_ALLOCATE(1, "逆向调拨", "HT"),
    RETURN_SUPPLY(2, "退供", "TG");

    private Integer code;
    private String desc;
    private String orderNoPrefix;

        //01.根据code获取枚举
           public static Optional<XtAllocateEnum> findByIntValue(Integer value) {
        return Arrays.stream(XtAllocateEnum.values())
                .filter(xtAllocateEnum -> xtAllocateEnum.getIntValue().equals(value)).findFirst();
    }
     
      //02.根据desc获取code
      public static Integer resolveByDesc(String desc) {
        return Arrays.stream(values())
                .filter(executeTypeEnum -> StringUtils.equals(desc, executeTypeEnum.getDesc())).findAny()
                .map(ExecuteTypeEnum::getCode)
                .orElse(-1);
    }
}


//03.使用
    @Test
    public void  t() {
        Optional<ExecuteTypeEnum> optional = ExecuteTypeEnum.findByIntValue(-99);
        if (optional.isPresent()) {
            System.out.println(optional.get().getDesc());
        } else {
            System.out.println("不存在的code");
        }

        Integer code = ExecuteTypeEnum.resolveByDesc("哈哈");
        System.out.println(code);
    }
  • 上述这种形式,需要在每个枚举中,都定义查询desc和code方法。可以抽取出枚举工具类

    枚举工具类

// 01.定义Value接口
public interface HaveValueEnum<T> {
    T getValue();
}

// 01.定义Desc接口
public interface HaveDescEnum<T> {
    T getDesc();
}

//03.定义工具类
@UtilityClass
public class EnumUtils {
    /**
     * 根据value获取对应的枚举
     *
     * @param value    value
     * @param enumType 枚举类型class
     * @param <E>      枚举类型
     * @param <T>      value类型
     * @return 对应的枚举Optional
     */
    public static <E extends Enum<E> & HaveValueEnum<T>, T> Optional<E> getEnumByValue(T value, Class<E> enumType) {
        for (E item : enumType.getEnumConstants()) {
            if (item.getValue().equals(value)) {
                return Optional.of(item);
            }
        }
        return Optional.empty();
    }

    /**
     * 根据value获取对应的枚举,获取不到则抛出异常
     *
     * @param value             value
     * @param enumType          枚举类型class
     * @param exceptionSupplier 异常提供者
     * @param <E>               枚举类型
     * @param <T>               value类型
     * @param <X>               异常类型
     * @return 对应的枚举
     * @throws X 异常
     */
    public static <E extends Enum<E> & HaveValueEnum<T>, T, X extends Throwable> E getEnumByValueOrElseThrow(T value, Class<E> enumType, Supplier<? extends X> exceptionSupplier) throws X {
        return getEnumByValue(value, enumType).orElseThrow(exceptionSupplier);
    }

    /**
     * 根据value获取对应的枚举,获取不到则抛出异常
     *
     * @param value    value
     * @param enumType 枚举类型class
     * @param <E>      枚举类型
     * @param <T>      value类型
     * @return 对应的枚举
     */
    public static <E extends Enum<E> & HaveValueEnum<T>, T> E getEnumByValueOrElseThrow(T value, Class<E> enumType) {
        return getEnumByValueOrElseThrow(value, enumType, () -> new IllegalArgumentException("can't find " + value + " in " + enumType));
    }

    /**
     * 根据value获取对应的枚举描述
     *
     * @param value    value
     * @param enumType 枚举类型class
     * @param <E>      枚举类型
     * @param <T>      value类型
     * @param <V>      desc类型
     * @return 对应的枚举描述
     */
    public static <E extends Enum<E> & HaveValueEnum<T> & HaveDescEnum<V>, T, V> Optional<V> getEnumDescByValue(T value, Class<E> enumType) {
        // jdk8 bug, https://bugs.openjdk.java.net/browse/JDK-8141508
        // http://mail.openjdk.java.net/pipermail/compiler-dev/2015-November/009824.html
        return getEnumByValue(value, enumType).map(haveDescEnum -> haveDescEnum.getDesc());
    }

    /**
     * 根据value获取对应的枚举描述,获取不到则返回默认值
     *
     * @param value       value
     * @param defaultDesc 默认值
     * @param enumType    枚举类型class
     * @param <E>         枚举类型
     * @param <T>         value类型
     * @param <V>         desc类型
     * @return 对应的枚举描述,获取不到则返回默认值
     */
    public static <E extends Enum<E> & HaveValueEnum<T> & HaveDescEnum<V>, T, V> V getEnumDescByValueOrElseDefault(T value, V defaultDesc, Class<E> enumType) {
        return getEnumDescByValue(value, enumType).orElse(defaultDesc);
    }
}

//04.定义枚举,实现Value和Desc接口
@Getter
@RequiredArgsConstructor
public enum ExecuteTypeEnum implements HaveValueEnum<Integer>, HaveDescEnum<String> {

    RETURN_WAREHOUSE(1,"逆向调拨"),

    RETURN_SUPPLY(2,"退供"),

    RETURN_WAREHOUSE_AND_SUPPLY(3,"逆向调拨+退供");

    private final Integer value;

    private final String desc;
}

//05.使用
String secondCategoryName = EnumUtils.getEnumDescByValueOrElseDefault(source.getSkuCategoryId(), "-", SkuSecondCategoryNameEnum.class);

5.2 使用位域表示枚举值

1、场景

枚举值,全部使用2的幂次方的形式表示。当给出7,算出7 = 1 + 2 + 4,即枚举中的LO 和 L 和 A这三种枚举组合而成

7 等效 LO && L && A,所以在db中不用存"LO && L && A",直接存7就可以了

  • Integer的MAX_VALUE最多表示2的30次方

  • Long的MAX_VALUE最多表示2的62次方【当枚举值比较多的时候,建议使用Long,但是最多也只支持2的62次方即62种不同的枚举】

    枚举类型的值,都是2的幂次方

@AllArgsConstructor
@NoArgsConstructor
@Getter
public enum RuleEnum {

    UNKNOW(-1L, "未知"),

    LO(1L, "小于or值可修改"),

    L(2L, "锁库不可更改"),

    A(4L, "允许"),

    NA(8L, "不允许"),

    XT(16L, "自动加量允许修改");

    private Long code;

    private String desc;

    //01.根据code获取枚举
    public static Optional<RuleEnum> findByIntValue(Long value) {
        return Arrays.stream(RuleEnum.values())
            .filter(ruleEnum -> ruleEnum.getCode().equals(value)).findFirst();
    }

    //02.根据desc获取code
    public static Long resolveByDesc(String desc) {
        return Arrays.stream(values())
            .filter(ruleEnum -> StringUtils.equals(desc, ruleEnum.getDesc())).findAny()
            .map(RuleEnum::getCode)
            .orElse(-1L);
    }
}

判断集合整数,由哪些2的幂次方的数构成

@UtilityClass
public class BitwiseOperateUtil {

    private final static List<Long> result = Lists.newArrayList();
    private static Integer times = 0;

    public List<Long> dividePositive2ListByBitwiseOperate(Long positive) {
        bitwiseOperate(positive);
        //list深度拷贝 & 拷贝后的结果可以再写
        List<Long> newList = new ArrayList<>();
        Collections.addAll(newList, new Long[result.size()]);
        Collections.copy(newList, result);
        return newList;
    }

    private void bitwiseOperate(Long num) {
        Long splitNum;

        if (num < 1) {
            return;
        }
        if ((num & 1) == 1) {
            result.add((long) Math.pow(2, times));
            splitNum = (num - 1 ) >> 1;
        } else {
            splitNum = num >> 1;
        }

        times += 1;
        bitwiseOperate(splitNum);
    }

}

给出整数13,获得 小于or值可修改,允许,不允许 对应的枚举desc

    @Test
    public void  t() {
        List<Long> list = BitwiseOperateUtil.dividePositive2ListByBitwiseOperate(13L);
        StringBuilder sb = new StringBuilder();
        for (Long code : list) {
            Optional<RuleEnum> optional = RuleEnum.findByIntValue(code);
            if (optional.isPresent()) {
                RuleEnum ruleEnum = optional.get();
                String desc = ruleEnum.getDesc();
                sb.append(desc);
                sb.append(",");
            }
        }
        String res = sb.substring(0, sb.length() - 1);
        System.out.println(res);
    }

5.3 枚举实现接口,实现可伸缩

接口

public interface Operate {
    double operate(double x, double y);
}

加减枚举

public enum OperateEnum implements Operate {
    ADD() {
        @Override
        public double operate(double x, double y) {
            return x + y;
        }
    },

    MINUS() {
        @Override
        public double operate(double x, double y) {
            return x - y;
        }
    }
}

取模枚举- 不需要改变原有的OperateEnum枚举,只需要新增枚举

public enum ModOperateEnum implements Operate{
    MOD() {
        @Override
        public double operate(double x, double y) {
            return x % y;
        }
    }
}

    @Test
    public void  t() {
        System.out.println(OperateEnum.ADD.operate(1, 2)); //3
    }

5.4 坚持使用Override注解

  1. 添加了 @Override 注解后,编译器能够帮助我们检查代码的错误。
  2. 能够让代码通熟易懂,清晰地看到哪些方法是重写方法。

5.5 反射

简介

本质
  • 注解的本质是实现了Annocation接口的接口

    但是注解不能使用extends继承其他注解|接口,但是接口可以继承其他接口

  • 注解和任何其他Java接口一样,也会编译成类文件

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}
//本质等价
public interface Override extends Annotation{
    
}
常见注解
  • @Override :用来声明该方法的定义会重载基类中的某个方法。如果不小心拼错了 方法名,或者使用了不恰当的签名,该注解会使编译器报错。

    当你在C#中承载某个方法时,必须使用override关键字,而在Java 中,对应的@Override注解是可有可无的

  • @Deprecated :如果该元素被使用了,则编译器会发出警告。

  • @SuppressWarnings :关闭不当的编译警告。

  • @Functiona!Inteiface : Java 8引入,用于表明类型声明是函数式接口

元注解
  • RetentionPolicy:保存周期/生命周期

    编译时期,直接扫描的注解:SOURCE

    运行时期,通过反射进行操作:RUNTIME

    在源文件中,通过反射添加一些补充信息

    RetentionPolicy.SOURCE:注解会被编译器丢弃,不会写入 class 文件(不符合的话,编译时期会报错)
    RetentionPolicy.CLASS:在类文件中可被编译器使用。类加载阶段丢弃,会写入 class 文件【不写默认】
    RetentionPolicy.RUNTIME:注解在运行时仍被虚拟机保留,因此可以通过反射读取到注解信息
    
  • Target:注解生效作用范围

public enum ElementType {
PARAMETRE:参数声明
TYPE: 类、接口(包括注解类型)或枚举的声明
   //成员属性
    FIELD,
    //方法
    METHOD,
    //方法参数
    PARAMETER,
    //构造器
    CONSTRUCTOR,
    /** Local variable declaration */
    LOCAL_VARIABLE,
    ///注解
    ANNOTATION_TYPE,
    /** Package declaration */
    PACKAGE,
    TYPE_USE
}
  • Documented
@Documented//对应Retention保存周期必须是RUNTIME
@Retention(RetentionPolicy.RUNTIME)
  • Inherited:继承性
其他用于修饰注解的注解
  • @Constraint
@Constraint(validatedBy = MyStatusValidatorImpl.class)
//@Constraint这个注解中的validatedBy属性含义:@MyStatus注解 和 哪个对应的实现类进行绑定
//因为javax自带的44个,通过Helper的map帮助我们绑定了,自定义的注解,则需要这种形式进行绑定
注解属性
  • 必须要有默认值

int 可以使用-1表示默认值

@Retention(RetentionPolicy.RUNTIME)
public ©interface SQLInteger {
	String name() default "";
	Constraints constraintsO default ©Constraints(unique = true);
)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
	boolean unique() default false;
}
通过反射获取注解
getAnnotation:返回指定的注解
isAnnotationPresent:判定当前元素是否被指定注解修饰
getAnnotations:返回所有的注解

getDeclaredAnnotation:返回本元素的指定注解
getDeclaredAnnotations:返回本元素的所有注解,不包含父类继承而来的
  • 获取方法上注解和注解的名称
    @Test
    public void  t() {
        Class<Dog> dogClass = Dog.class;
        try {
            Method method = dogClass.getDeclaredMethod("func", null);
            if (method.isAnnotationPresent(Hello.class)) {
                Hello annotation = method.getAnnotation(Hello.class);//@com.sankuai.wos.entity.Hello(value=[go])
                Class<? extends Annotation> aClass = annotation.annotationType();
                String aClassName = aClass.getName();;//com.sankuai.wos.entity.Hello
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

使用

自定义注解使用
  • 定义注解
@Documented
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Hello {

    String value();
}
  • 使用注解
@Hello("go")
public class Dog {
}
  • 获取注解
@Test
    public void  t() {
        Class<Dog> dogClass = Dog.class;
        Hello proxyHandle = dogClass.getAnnotation(Hello.class);
    }
如何发现注解以及其属性

通过反射获取了作用在Dog类上的@Hello注解(接口) 的代理类AnnotationInvocationHandler

  • AnnotationInvocationHandler代理类handle中内容
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private final Map<String, Object> memberValues; //01.是个map,key就是@Hello注解的属性名称value, 值是属性名称对应的值”go“
    private transient volatile Method[] memberMethods = null;


    //02.代理类handle实现了@Hello注解(接口)的所有方法,对任意方法的调用,都会走到代理类handle的invoke方法中
    public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();// 03.注解方法名称
        Class[] var5 = var2.getParameterTypes();// 04.注解方法的参数
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            byte var7 = -1;
            switch(var4.hashCode()) {
            case -1776922004:
                if (var4.equals("toString")) { //05.注解本质是extends Annocation接口的接口,Annocation自带4个方法,判断Hello注解调用的方法是不是这4个
                    var7 = 0;
                }
                break;
            case 147696667:
                if (var4.equals("hashCode")) {
                    var7 = 1;
                }
                break;
            case 1444986633:
                if (var4.equals("annotationType")) {
                    var7 = 2;
                }
            }

            switch(var7) { // 06.是这4个方法,则直接调用方法的impl实现
            case 0:
                return this.toStringImpl();
            case 1:
                return this.hashCodeImpl();
            case 2:
                return this.type;
            default:
                Object var6 = this.memberValues.get(var4); //07.如果是@Hello注解本身的方法,eg:value()方法,则将key名称”value“,在map中对应的值"go"获取并返回
                if (var6 == null) {
                    throw new IncompleteAnnotationException(this.type, var4);
                } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }

                    return var6;
                }
            }
        }
    }
}

总结

1、先校验注解

校验注解的使用范围、保存周期等是否合理

2、反射获取注解的实现类

jvm将所有生命周期是Runtime的注解取出来,将名称和值放入map。并创建注解的代理类

3、任何对注解方法的调用,都会通过代理类的invoke,返回注解的属性值

4、根据属性值,进一步操作

参数校验注解

Validator注解校验参数

1、Validator接口和最佳实践:hibernate.validator

2、使用

        <!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.18.Final</version>
        </dependency>

3、Bean Validation

其内置constraint。除了非空检验注解外,其他注解校验(时间、大小、范围、正负、邮箱)必须在属性不为null的时候才生效,才会去校验。所以,一般这些注解都会结合@NotNull、@NotBlank、@NotEmpty一起使用

常见校验注解
@Valid
需要验证的实体是另外一个实体的属性。则需要加上这个注解
    public class RuleDTO {
    // 你要用我,你就要在你用的地方加这个注解
    	@Valid //这个集合的元素实体中的另外一个实体NetPoiInfo属性也需要验证,则需要加这个注解
   		@NotEmpty(message = "网店信息(netPoiInfos)不能为空")
    	private List<NetPoiInfo> netPoiInfos;
    }
    
	public class NetPoiInfo {
    	@NotNull(message = "网店ID不能为空")
    	@Positive(message = "网店ID不合法")
    	private Long netPoiId;
    }
    
     /**
     * 预警阈值,0.0000-1.0000
     */
    @FieldDoc(description = "预警阈值,0.0000-1.0000", example = {}, requiredness = Requiredness.REQUIRED)
    @NotBlank(message = "概率阈值不能为空")
    @Digits(integer = 1, fraction = 4, message = "概率阈值最多4位精度")
    @DecimalMax(value = "1", message = "概率阈值不能超过1")
    @DecimalMin(value = "0", message = "概率阈值不能小于0", inclusive = false)
    private String warnThreshold;
    
    
@Digits(integer,fraction)	带批注的元素必须是一个在可接受范围内的数字
@Email	顾名思义
@Future	将来的日期
@FutureOrPresent	现在或将来的日期
@Max	被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Min	被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@Max(200)
@Min(1) 
private Integer age;//age为null,不赋值,校验不会生效。

建议:
@NotNull
@Max(200)
@Min(1) 
private Integer age;//这样null 和 值范围,都会检验了

@NotNull
@Min(value = 18, message = "年龄小于{value}禁止入内")//将value的18,通过el表达式赋给message信息 
private Integer age;//这样null 和 值范围,都会检验了

@Negative	带注释的元素必须是一个严格的负数(0为无效值)
@NegativeOrZero	带注释的元素必须是一个严格的负数(包含0<=0
@NotBlankStringUtils.isNotBlank
@NotEmptyStringUtils.isNotEmpty
@NotNull	不能是Null
@Past	被注释的元素必须是一个过去的日期
@PastOrPresent	过去和现在
@Pattern	被注释的元素必须符合指定的正则表达式
@Positive	被注释的元素必须严格的正数(0为无效值)
@PositiveOrZero	被注释的元素必须严格的正数(包含0>=0
@Szie(max,min)	带注释的元素大小必须介于指定边界(包括)之间
@DecimalMin(value)>=
@DecimalMax(value)<=
@Pattern(regexp = "/^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/"):正则表达式(手机号等)
    
@Length(min=, max=)	被注释的字符串的大小必须在指定的范围内
@Range(min=, max=)	闭区间,被注释的元素必须在合适的范围内 作用等效 @Min() + @Max()
@URL 被注释的字符串必须是一个有效的url(protocol=,host=, port=, regexp=, flags=)	
@CreditCardNumber被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性
ValidatorUtil校验工具类
@UtilityClass
public class ValidateUtil {
    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    public static <T> void validateParam(T param, Class<?>... groups) throws IllegalArgumentException {
        if (Objects.isNull(param)) {
            throw new IllegalArgumentException("参数不能为空");
        }
        Set<ConstraintViolation<T>> validateResult = validator.validate(param, groups);
        if (CollectionUtils.isEmpty(validateResult)) {
            return;
        }
        String validateMsg = validateResult.stream().map(ConstraintViolation::getMessage).collect(
                Collectors.joining(";"));
        throw new IllegalArgumentException(validateMsg);
    }
}
  • 使用注解
public class User{
    @NotBlank(message = "姓名不能为空")
    private String name;
}
  • 校验
User user = 从前端页面获取的Req
ValidateUtil.validateParam(user);
@NotBlank以及校验原理

1、NotBlankValidator实现类:

  • 内含有@Constraint注解
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface NotBlank {

	String message() default "{javax.validation.constraints.NotBlank.message}";

	Class<?>[] groups() default { };

	Class<? extends Payload>[] payload() default { };

	/**
	 * Defines several {@code @NotBlank} constraints on the same element.
	 *
	 * @see NotBlank
	 */
	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
	@Retention(RUNTIME)
	@Documented
	public @interface List {
		NotBlank[] value();
	}
}

  • @Constraint,作用在注解上 ,内含ConstraintValidator接口
@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {
	Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}
  • NotBlankValidator实现了ConstraintValidator接口
public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {
    public NotBlankValidator() {
    }

    public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
        if (charSequence == null) {
            return false;
        } else {
            return charSequence.toString().trim().length() > 0;
        }
    }
}

2、看到注解NotBlank 如何 去找NotBlankValidator实现类

  • ConstraintHelper中,有个map,将注解和实现类绑定
Map<Class<? extends Annotation>, List<ConstraintValidatorDescriptor<?>>> tmpConstraints = new HashMap();

putConstraint(tmpConstraints, NotBlank.class, NotBlankValidator.class);
putConstraints(tmpConstraints, NotEmpty.class, notEmptyValidators);
  • 具体流程

    • 通过spi服务提供接口,找到Validator接口的HibernateValidator厂商实现。

    (关于SPI可以参考我的另外一篇文章SPI

    创建Validator接口的实现类ValidatorImpl
    
    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();//通过工厂创建Validator接口的Hibernate实现类
    
    通过buildDefaultValidatorFactory()->configure()->getValidationProviders() ->run() ->loadProviders( classloader ) ->
      ServiceLoader<ValidationProvider> loader = ServiceLoader.load( ValidationProvider.class, classloader );
    

3、ValidatorUtil校验工具类校验流程

@UtilityClass
public class ValidateUtil {
    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    public static <T> void validateParam(T param, Class<?>... groups) throws IllegalArgumentException {
        if (Objects.isNull(param)) {
            throw new IllegalArgumentException("参数不能为空");
        }
        Set<ConstraintViolation<T>> validateResult = validator.validate(param, groups);
        if (CollectionUtils.isEmpty(validateResult)) {
            return;
        }
        String validateMsg = validateResult.stream().map(ConstraintViolation::getMessage).collect(
                Collectors.joining(";"));
        throw new IllegalArgumentException(validateMsg);
    }
}
public class User{
    @NotBlank(message = "姓名不能为空")
    private String name;
}
User user = 从前端页面获取的Req
ValidateUtil.validateParam(user);
  • spi: 工厂创建Validator接口对应的实现类ValidatorImpl ->
  • 执行ValidateUtil方法validateParam ->
  • 反射:获取param上对应的注解 (@NotBlank)【这一步原理可以参考上文:如何发现注解以及其属性】->
  • ConstraintHelper:map将@NotBlank注解 和 NotBlankValidator实现类绑定 ->
  • 判断是否是javax的内置44个注解,是的话则从map中拿到@NotBlank对应的NotBlankValidator实现类 ->
  • 调用NotBlankValidator实现类的isValid方法
  • 具体方法流程调用链
ValidateUtil.validate->

ValidatorImpl.validateInContext -> validateConstraintsForCurrentGroup -> validateConstraintsForDefaultGroup -> validateConstraintsForSingleDefaultGroupElement -> validateConstraintsForSingleDefaultGroupElement -> 


MetaConstraint.validateConstraint -> doValidateConstraint -> validateConstraints -> validateConstraints ->


SimpleConstraintTree.validateConstraints -> validateSingleConstraint -> 


ConstraintValidator.isValid

其中自定义注解对应的实现类,正是实现了ConstraintValidator这个接口,然后重写了isValid方法

public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {
	@Override
    public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {

    }
}
分组校验

1、场景

db中的主键id,添加的时候不需要,修改的时候不能为空。这种就需要分组校验

  • 实体类
@Data
@Accessors(chain = true)
public class User {

    public interface Add {} //01.定义2个接口,用于指定Group
    public interface Update{}
    
	// 02.更新操作,id不能为null
    @NotNull(groups = Update.class,message = "修改User,主键id不能为null")
    @Null(groups = Add.class, message = "新增user,主键id必须为null")
    private Integer id;

    @NotNull
    @Min(value = 18, message = "未满{value}禁止入内")
    private Integer age;
    private String name;
}

工具类校验

User user = new User().setAge(3).setId(1);
ValidateUtil.validateParam(user, User.Add.class, Default.class);
1、工具类的validateParam方法,可以指定Group,当你是Add组,会去读取@Null注解
2、最后必须加上默认的Default组,否则,其他所有注解都不生效了,只会生效你自定义的组
级联校验
@Data
@Accessors(chain = true)
public class User {

    @Valid//01.级联校验
    @NotNull(message = "Dog不能为null")
    private Dog dog;
}

@Data
public class Dog{
    @NotBlank(message = "dogName不能为空")
    private String name;

}

    @Test
    public void  t() {

        User user = new User();
        Dog dog = new Dog();
        user.setDog(dog); //02.这里的dog病灭有赋值name,如果不加上@Valid注解校验,则不会报错message = "dogName不能为空"
        ValidateUtil.validateParam(user);
    }

注解实战

自定义校验

1、用户输入的Integer status字段,只能是10、20、30三个值

  • 自定义注解
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
//@Constraint这个注解中的validatedBy属性含义:@MyStatus注解 和 哪个对应的实现类进行绑定
//因为javax自带的44个,通过Helper的map帮助我们绑定了,自定义的注解,则需要这种形式进行绑定
@Constraint(validatedBy = MyStatusValidatorImpl.class) //将自定义注解 和 对应的Validator实现类绑定
public @interface MyStatus {

    String message() default "用户输入status只能为10、20、30";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

}
  • 自定义注解对应Validator实现类
public class MyStatusValidatorImpl implements ConstraintValidator<MyStatus, Integer> { 
//将自定义注解 和 对应的Validator实现类绑定
//<第一个是实现类和哪个注解进行绑定, 第二个是这个注解作用在什么类型上,是作用在Integer,还是Collection(@NotEmpty)集合,还是String【@NotBlank】,还是Object【@NotNull】>
  
    @Override
    public void initialize(MyStatus constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        if (value == null) { //这里值为null,返回true校验成功的原因是:自定义注解或者javax的@Min、@Positive等注解,他们本身都不校验空,他们都认为值为null时符合校验的
           //如果你想校验null或空,那就配合着@NotNull、@NotBlank、@NotEmpty一起使用。
           //各个注解只定义自己的功能,不要包含@NotNull、@NotBlank、@NotEmpty的功能
            return true;
        }

        //真正的校验逻辑
        Set<Integer> set = new HashSet<>();
        set.add(10);
        set.add(20);
        set.add(30);
        return set.contains(value);
    }
}

补充:
这个自定义注解逻辑处理类由于实现了ConstraintValidator接口,所以它默认被spring管理成bean
所以可以在这个逻辑处理类里面用@Autowiredu或者@Resources注入别的服务,而且不用在类上面用@Compent注解成spring的bean.
这样就可以rpc请求服务/查db获取数据,用这些数据,做复杂的用户输入校验。
适合,用户输入的数据 需要和后端交互一次后,做校验的场景
  • 使用
@Data
@Accessors(chain = true)
public class User {

    @NotNull
    @MyStatus(message = "用户输入status只能为10、20、30")//本身不校验null的场景
    private Integer status;
}

    @Test
    public void  t() {
        User user = new User();
        user.setStatus(1);
        ValidateUtil.validateParam(user);//用户输入status只能为10、20、30
    }

2、用户输入的skuId集合最多30个元素

  • 自定义注解@CollectionSizeCheck
@Constraint(validatedBy = {CollectionSizeCheckConstraintValidatorImpl.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface CollectionSizeCheck {
    int value();

    String message() default "集合大小不合法";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        CollectionSizeCheck[] value();
    }
}
  • 自定义CollectionSizeCheckConstraintValidatorImpl校验类
@Slf4j
public class CollectionSizeCheckConstraintValidatorImpl implements ConstraintValidator<CollectionSizeCheck,
        Collection> {

    private int collectionSizeThreshold;

    @Override
    public void initialize(CollectionSizeCheck constraintAnnotation) {
        this.collectionSizeThreshold = constraintAnnotation.value();
    }
          
    @Override
    public boolean isValid(Collection value, ConstraintValidatorContext context) {
        if (Objects.isNull(value)) {
            return true;
        }
        return value.size() <= collectionSizeThreshold;
    }
}

  • 使用
    @CollectionSizeCheck(value = 5,message = "当前用户输入的skuId集合最多{value}个")
    @NotEmpty(message = "skuIds集合不能为空")
    private List<Long> skuIds;
  • 补充EL表达式

在注解校验的message中,使用EL表达式

@Max*(message = “年龄大小不能超过{value}”,value = 180)*

3、日期必须为yyyy-MM-dd格式且必须为T-28到T+1

  • 2022-7-5“这种格式不行
  • 今天是2023-11-28,则可选范围为:[11.1 - 11.29]
  • 自定义注解@DateFormatCheck
@Constraint(validatedBy = {DateFormatCheckConstraintValidatorImpl.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface DateFormatCheck {
    String value();

    String message() default "日期格式不合法";

    /**
     * T-N,小于0不做校验,时间范围校验
     *
     * @return T-N
     */
    long beforeCurrent() default -1L;

    /**
     * T+N,小于0不做校验,时间范围校验
     *
     * @return T+N
     */
    long afterCurrent() default -1L;

    /**
     * 过去或当前时间校验,优先级高于时间范围校验
     *
     * @return 过去或当前时间 true
     */
    boolean pastOrPresent() default false;

    /**
     * 未来或当前时间校验,优先级高于时间范围校验
     *
     * @return 未来或当前时间校验 true
     */
    boolean futureOrPresent() default false;

    /**
     * 当前时间
     *
     * @return 当前时间检验
     */
    boolean onlyPresent() default false;

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        DateFormatCheck[] value();
    }
}

  • 自定义DateFormatCheckConstraintValidatorImpl
@Slf4j
public class DateFormatCheckConstraintValidatorImpl implements ConstraintValidator<DateFormatCheck, String> {

    private String formatDateString;
    private long beforeCurrent;
    private long afterCurrent;
    private boolean pastOrPresent;
    private boolean futureOrPresent;
    private boolean onlyPresent;

    @Override
    public void initialize(DateFormatCheck constraintAnnotation) {
        this.formatDateString = constraintAnnotation.value();
        this.beforeCurrent = constraintAnnotation.beforeCurrent();
        this.afterCurrent = constraintAnnotation.afterCurrent();
        this.pastOrPresent = constraintAnnotation.pastOrPresent();
        this.futureOrPresent = constraintAnnotation.futureOrPresent();
        this.onlyPresent = constraintAnnotation.onlyPresent();
    }
  
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (StringUtils.isBlank(value)) {
            return true;
        }
        try {
            LocalDate inputLocalDate = LocalDate.parse(value, DateTimeFormatter.ofPattern(formatDateString));
            LocalDate now = LocalDate.now();
            if (pastOrPresent) {
                // a.isBefore(a) == false
                return inputLocalDate.isBefore(now.plusDays(1L));
            }
            if (futureOrPresent) {
                // a.isAfter(a) == false
                return inputLocalDate.isAfter(now.minusDays(1L));
            }
            if (onlyPresent) {
                // t-1.23:00:00--->t:22:59:29
                return isCurrentSaleDate(inputLocalDate);
            }

            boolean validResult = true;
            if (beforeCurrent >= 0) {
                // a.isAfter(a) == false
                LocalDate beforeLocalDate = now.minusDays(beforeCurrent + 1L);
                validResult = inputLocalDate.isAfter(beforeLocalDate);
            }
            if (afterCurrent >= 0) {
                // a.isBefore(a) == false
                LocalDate afterLocalDate = now.plusDays(afterCurrent + 1L);
                validResult = validResult && inputLocalDate.isBefore(afterLocalDate);
            }
            return validResult;

        } catch (Exception e) {
            log.debug("日期格式校验不合法", e);
        }
        return false;
    }

    /**
     * 22:59:59.999
     */
    public static final LocalTime SELL_END_LOCAL_TIME = LocalTime.parse("22:59:59.999",
        DateTimeFormatter.ofPattern("HH:mm:ss.SSS"));

    /**
     * 23:00:00
     */
    public static final LocalTime SELL_START_LOCAL_TIME = LocalTime.parse("23:00:00", DateTimeFormatter.ISO_LOCAL_TIME);

    public static boolean isCurrentSaleDate(LocalDate sellLocalDate) {
        LocalTime nowLocalTime = LocalTime.now();
        LocalDate nowLocalDate = LocalDate.now();
        LocalDateTime sellLocalDateTime = sellLocalDate.atTime(nowLocalTime);
        return sellLocalDateTime.isAfter(
            nowLocalDate.minusDays(1).atTime(SELL_START_LOCAL_TIME)) && sellLocalDateTime.isBefore(
            nowLocalDate.atTime(SELL_END_LOCAL_TIME));
    }
}
  • 使用
    @NotBlank(message = "sellTime(销售时间)不能为null")
    @DateFormatCheck(value = "yyyy-MM-dd", message = "sellTime(销售时间)必须为yyyy-MM-dd格式且必须为T-28到T+1",
            beforeCurrent = 28L, //可以通过before和after指定时间范围
            afterCurrent = 1L)//这里before和after可以不指定
    @FieldDoc(description = "当前用户选择的销售时间", example = {}, requiredness = Requiredness.REQUIRED)
    private String sellTime;

4、用户输入的日期必须符合日期格式(”2022-7-5“这种格式也可以)

  • 自定义@DateCheck
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateCheckValidatorImpl.class)
public @interface DateCheck {

    String message() default "日期格式错误";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

}

  • 自定义DateCheckValidatorImpl
public class DateCheckValidatorImpl implements ConstraintValidator<DateCheck, String> {

    private static final DateTimeFormatter PARTITION_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd", Locale.CHINA);
    private static final DateTimeFormatter dateFormatter = DATE_FORMATTER.withResolverStyle(ResolverStyle.STRICT);
    private static final DateValidator validator = new DateValidatorUsingDateTimeFormatter(dateFormatter);

    @Override
    public void initialize(DateCheck constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(String date, ConstraintValidatorContext context) {
        if (StringUtils.isBlank(date)) {
            return true;
        }

        // 将2022/06/05转换为2022-06-05
        if (StringUtils.isNotBlank(date) && date.contains("/")) {
            date = date.replaceAll("/", "-");
        }

        // 格式转换,将字符串2022-6-5或者2022-6-05或者2022-06-5,转成2022-06-05
        LocalDate timeLocal;
        try {
            timeLocal = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-M-d"));
        } catch (RuntimeException e) {
            return false;
        }
        date = PARTITION_DATE_FORMAT.format(timeLocal);
        // 判断日期格式是否合规
        if (!validator.isValid(date)) {
            return false;
        }

        return true;
    }
}
public interface DateValidator {
    boolean isValid(String dateStr);
}
public class DateValidatorUsingDateTimeFormatter implements DateValidator {
    private final DateTimeFormatter dateFormatter;

    public DateValidatorUsingDateTimeFormatter(DateTimeFormatter dateFormatter) {
        this.dateFormatter = dateFormatter;
    }

    @Override
    public boolean isValid(String dateStr) {
        try {
            this.dateFormatter.parse(dateStr);
        } catch (DateTimeParseException e) {
            return false;
        }
        return true;
    }
}
  • 使用
    @NotBlank(message = "日期不能为null")
    @DateCheck(message = "日期格式不正确")
    private String time;
  • 将@DateFormatCheck注解中的值,传到DateFormatCheckConstraintValidatorImpl中,作为逻辑分支
@DateFormatCheck(value = "yyyy-MM-dd", message = "sellTime(销售时间)必须为yyyy-MM-dd格式且必须为T-28到T+1", beforeCurrent = 28L,afterCurrent = 1L)
将beforeCurrent值传递到impl中
@Constraint(validatedBy = {DateFormatCheckConstraintValidatorImpl.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface DateFormatCheck {
    String value();

    String message() default "日期格式不合法";

    /**
     * T-N,小于0不做校验,时间范围校验
     *
     * @return T-N
     */
    long beforeCurrent() default -1L;
@Slf4j
public class DateFormatCheckConstraintValidatorImpl implements ConstraintValidator<DateFormatCheck, String> {

    private long beforeCurrent;

    @Override
    public void initialize(DateFormatCheck constraintAnnotation) {

        this.beforeCurrent = constraintAnnotation.beforeCurrent();
      
    }
根据用户输入日期的格式,自定义校验

根据自定义校验格式,校验用户对应的输入值

定制格式校验,可以为"2023-10-23 12:00:00"、"2023-10-23 12:00:00"、"12:45:00"、"12:45"
  • 注解
@Constraint(validatedBy = {CustomizeDateFormatConstraintValidatorImpl.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface CustomizeDateFormat {

    /**
     * 定制格式校验,可以为"2023-10-23 12:00:00"、"2023-10-23 12:00:00"、"12:45:00"、"12:45"
     */
    String value();

    String message() default "时间格式不正确";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}
  • Impl
public class CustomizeDateFormatConstraintValidatorImpl implements ConstraintValidator<CustomizeDateFormat, String> {

    /**
     * 注解@CustomizeDateFormat中value值
     */
    private String customizeDateFormat;

    @Override
    public void initialize(CustomizeDateFormat constraintAnnotation) {
        this.customizeDateFormat = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (StringUtils.isBlank(value)) {
            return true;
        }

        // 不同的customizeDateFormat,对应不同的校验
        DateTimeFormatter dateFormatter = DateTimeFormatter
                .ofPattern(customizeDateFormat, Locale.CHINA)
                .withResolverStyle(ResolverStyle.STRICT);
        DateValidator validator = new DateValidatorImpl(dateFormatter);

        // 判断日期格式是否合规
        if (!validator.isValid(value)) {
            return false;
        }

        // 扩展:如果格式customizeDateFormat,是带有月和日的,validator可以对月值、日值进行校验
        //  0 < 月 < 13
        //  0 < 日 < 31(1、3、5、7、8、10、12是31天 ; 4、6、9、11是30天,2月闰月29天否则28天)
        return true;
    }
}
  • 接口和实现类
public interface DateValidator {
    boolean isValid(String dateStr);
}


public class DateValidatorImpl implements DateValidator {
    private final DateTimeFormatter dateFormatter;

    public DateValidatorImpl(DateTimeFormatter dateFormatter) {
        this.dateFormatter = dateFormatter;
    }

    @Override
    public boolean isValid(String dateStr) {
        try {
            this.dateFormatter.parse(dateStr);
        } catch (DateTimeParseException e) {
            return false;
        }
        return true;
    }
}
  • 注解使用
    @CustomizeDateFormat(value = "HH:mm", message = "算法售罄加量时间有误,正确格式为{value}")
    private String sellOutOrTime;
  • 补充:

    如果想校验输入是否为20230501这种类型,则

        Preconditions.checkArgument(StringUtils.isNotBlank(triggerDate), "触发日期不能为空");
        LocalDate triggerLocalDate;
        try {
            triggerLocalDate = LocalDate.parse(triggerDate, DateTimeFormatter.BASIC_ISO_DATE);
        } catch (Exception e) {
            throw new IllegalArgumentException("触发日期不合法");
        }

说明:这种可以校验卡住20230229、20230230、20230431这种不合法的日期,因为DateTimeFormatter.BASIC_ISO_DATE里面写的

值为null,不生效的注解校验

原因:正常情况下我们是使用

@NotNull
@NotBlank
@NotEmpty

这些专门用来判断对象、字符串、集合非空的注解。所以,当我们自定义注解的时候,这些注解都不会去校验value是否为空,即value为空在自定义注解中默认是ok的,return true放行的(常见的现有注解@Max这些也是这个思路)

public abstract class AbstractMinValidator<T> implements ConstraintValidator<Min, T> {
    protected long minValue;
    public AbstractMinValidator() {}

    public void initialize(Min maxValue) {this.minValue = maxValue.value();}

    public boolean isValid(T value, ConstraintValidatorContext constraintValidatorContext) {
        if (value == null) { //@Min注解,如果值为null,注解不会生效,直接返回true
            return true;
        } else {
            return this.compare(value) >= 0;
        }
    }

    protected abstract int compare(T var1);
}
字段允许为空,但非空时需要满足校验

1、背景:年龄字段可以为空,但是非空时,必须满足0-200

2、实现

  • 注解
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyStatusValidatorImpl.class)
public @interface MyStatus {

    String message() default "年龄0-200";

    boolean nullable() default true;//默认允许为空

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

}
  • 实现类
public class MyStatusValidatorImpl implements ConstraintValidator<MyStatus, Integer> {
    private MyStatus myStatus;
    @Override
    public void initialize(MyStatus constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
        this.myStatus = constraintAnnotation;
    }

    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        // 这里判断是否允许为空
        if (value == null) {
            return myStatus.nullable();
        }
        return value > 0 && value < 200;
    }
}

3、扩展

注解可以对多个类型生效。

  • 背景:

    日期可以使用String saleDate接收,格式为yyyy-MM-dd

    也可以使用Long saleDate接收,格式为yyyyMMdd

    想实现一个注解对两种类型字段校验

  • 处理

    @DateFormat(message = "格式必须为yyyy-MM-dd")
    private String saleDate;
    
    @DateFormat(message = "格式必须为yyyyMMdd")
    private Long saleDate;
    
  • 注解定义

@Constraint(validatedBy = StringDateFormatValidatorImpl.class, LongDateFormatValidatorImpl.class)
public @interface MyStatus {

}
  • 实现类

自定义两个实现类,一个实现校验String类型,一个实现校验Long类型即可。实现方式同上

分布式锁注解
  • 注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefundConcurrentControl {
    /** 操作间隔时间,分布式锁场景就是锁的超时时间 */
    int intervalTimeSeconds();

    /** 并发控制键计算规则 */
    String keyGenRule();//貌似必须要加#前缀,expression解析的时候需要

    /** 是否需要释放,一般分布式锁场景需要释放 */
    boolean needRelease();

    /** 指定前缀;如果不指定前缀就是类名+方法名*/
    String specifyPrefix() default "";

    /** 指定错误提示;如果不指定就按系统默认值*/
    String specifyErrorMsg() default "";
}
  • aop
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Objects;

@Aspect
@Component
@Slf4j
public class ConcurrentControlAspect implements Ordered {
    private static final String CAT_TYPE = ConcurrentControlAspect.class.getSimpleName();

    private SpelExpressionParser elExpressionParser = new SpelExpressionParser();
    private DefaultParameterNameDiscoverer parameterNameDiscoverer =
            new DefaultParameterNameDiscoverer();

    @Autowired private RefundDistributeLock distributeLock;
    //默认错误提示
    private static final String COMMON_ERROR_MSG = "当前操作过于频繁,请稍后再试";
    // 0.生效的范围是注解,around方式(加锁 - 方法 - 解锁)环绕方式
    @Around(
            "@annotation(com.sankuai.grocerywms.logistics.sharedrefund.annotation.RefundConcurrentControl)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        // 1、根据方法注解解析接口配置信息
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        RefundConcurrentControl controlConfig = method.getAnnotation(RefundConcurrentControl.class);
        // 解析参数
        if (Objects.isNull(controlConfig)) {
            throw new BusinessException(Constants.CONCURRENT_CONTROL_CONFIG_ERROR, "参数为空");
        }
        // 两次操作间隔时间
        int intervalTimeSeconds = controlConfig.intervalTimeSeconds();
        // 生成键规则
        String specifyPrefix = controlConfig.specifyPrefix();
        // 生成键规则
        String keyGenRule = controlConfig.keyGenRule();
        // 是否需要释放
        boolean needRelease = controlConfig.needRelease();
        //错误提示
        String errorMsg = !Strings.isBlank(controlConfig.specifyErrorMsg()) ? controlConfig.specifyErrorMsg() : COMMON_ERROR_MSG;

        // 2、计算lockKey
        String lockKey = generateLockKey(pjp, specifyPrefix,keyGenRule);
        // 3、加锁(aop前缀增强)
        boolean lockResult = false;
        try {
            // 注意:这里的lock方法就是使用redis封装的加锁方法
            lockResult = distributeLock.lock(lockKey, intervalTimeSeconds);
            if (!lockResult) {
                Cat.logEvent(CAT_TYPE, "LOCK_FAIL");
                throw new BusinessException(
                        Constants.CONCURRENT_CONTROL_LOCK_FAIL, errorMsg);
            }
            // 4.执行方法本身(本前缀增强 和 后缀增强环绕)
            Object result = pjp.proceed();
            return result;
        } catch (Exception e) {
            log.warn("方法执行异常,e:{}", e.getMessage());
            throw e;
        } finally {
            // 5、解锁(aop后缀增强)
            if (needRelease && lockResult) {
                // 注意:这里的unlock方法就是使用redis封装的释放锁方法
                distributeLock.unlock(lockKey);
            }
        }
    }

    private String generateLockKey(ProceedingJoinPoint pjp,String specifyPrefix ,String keyGenRule) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        String methodFullName = pjp.getTarget().getClass().getSimpleName() + method.getName();
        //1、lockKey前缀;用户不指定前缀则默认为类名+方法名
        String prefix = !Strings.isBlank(specifyPrefix) ? specifyPrefix : methodFullName;

        Object[] args = pjp.getArgs();
        String[] paramNames = parameterNameDiscoverer.getParameterNames(method);
        EvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < args.length; i++) {
            context.setVariable(paramNames[i], args[i]);//key: 方法参数名称,val方法入参参数对象本身(含字段值)。方法参数可能多个,故循环添加
        }
        Expression expression = elExpressionParser.parseExpression(keyGenRule);//el表达式解析"#内容"
        // 2、方法名-参数解析结果
        return prefix + "-" + expression.getValue(context).toString();//等效map.get(key),context为map,key是el表达式
    }

    @Override
    public int getOrder() {
        //5、配置为最小值 在事务切面之前执行
        return Integer.MIN_VALUE;
    }
}
  • 注解的使用1: key直接使用方法入参某个字段
@RefundConcurrentControl(
            intervalTimeSeconds = 3,
            keyGenRule = "#operator",//类似于你要加锁的key(misId、orderNo、taskCode等)
            needRelease = true,
            specifyPrefix = "WriteShippingTaskDetail",//描述你加锁操作的目的,具体是对什么操作(创建退货单、导出加量日志等)
            specifyErrorMsg = "该单正在操作中,请稍后重试操作")
    public void confirmPDAShippingTaskDetail(ConfirmShippingTaskDetailRequest request,String operator) {
        
    }
  • 注解的使用2: key间接使用方法入参request中,某个字段名称
    @RefundConcurrentControl(
            specifyPrefix = "changePdaPickingTaskTaker",
            intervalTimeSeconds = 10,
            needRelease = true,
            specifyErrorMsg = "正在更改执行人,请勿重复操作",
            keyGenRule = "#request.newOperator") //ChangeOperatorTRequest request中的newOperator字段作为key
    @Transactional(rollbackFor = Exception.class)
    public void changeOperator(ChangeOperatorTRequest request) {

    }
  • 注解的使用3: key间接使用方法入参request中,某2个字段组合
@RefundConcurrentControl(
            specifyPrefix = "OperateRDCPickingSkuDetail",
            intervalTimeSeconds = 2,
            needRelease = true,
            specifyErrorMsg = "重复性互斥提交,请稍后重试",
            keyGenRule = "#request.pickingTaskNo + '-' + #request.pickingTaskSkuDetailId")//SubmitPickingTaskDetailRequest request中的两个字段拼接成key
    
    public SubmitPickingTaskDetailResponse submitPickingTaskDetail(long poiId, SubmitPickingTaskDetailRequest request) {
     
}

六、方法

6.1 检查参数的有效性

1、不要相信前端的入参

2、不要相信依赖接口的返回值非空、值符合预期

6.2 必要时进行保护性拷贝

User

@Data
@NoArgsConstructor
public class User {

    private String name;
    private Date inBirthday;

    public void setInBirthday(Date outBirthday) {
        this.inBirthday = outBirthday;
    }
    
    public Date getInBirthday() {
        return this.inBirthday;
    }
}

   @Test
    public void t() {
        Date outBirthday = Date.valueOf("2022-08-16");
        User user = new User();
        user.setName("mjp");
        user.setInBirthday(outBirthday);

        outBirthday.setTime(1660747569532L);//2022-08-17
        System.out.println(user.getInBirthday());//0817

    }
1、outBirthday是0816,通过set方法,设置给inBirthday,二者都执行同一块内存地址0X01
2、outBirthday重新设置为0817了,0X01地址对应的值变为0817,所以inBirthday也是08173、get方法,return的是inBirthday,自然就是0817
  • 本质原因是:属性是非基本类型。外部传递的对象和属性对象都指向了同一块内存地址。一个改变了地址的内容,则属性对应地址的内容也改变了
  • 解法一:Date属性,使用Long基本类型代替。传入属性和对象属性不再公用一块堆内存
    @Test
    public void t() {
        long outBirthday = 1660661169000L;//0816
        String name = "mjp";
        User user = new User();
        user.setName(name);
        user.setInBirthday(outBirthday);

        outBirthday = 1660747569532L;//2022-08-17
        System.out.println(user.getInBirthday());//还是0816
    }

解法二:保护性拷贝[参考3.1和3.2]

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {

    private final char value[];//02.这个value数组,在String内部被很多其它地方使用。所以,不能改变它的值
  
     //01. 这样对toCharArray的返回结果数组进行操作,不会影响原本的value数组的元素内容
     //其实这里的toCharArray方法,就是getXxx方法。内部进行了保护性拷贝,没有直接将value数组对象返回出去,而是新创建一块内存地址返回出去
     //对新地址对应的内容进行操作,不会影响value对应内存地址的值
     public char[] toCharArray() {
        // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }
}

6.3 谨慎设计方法签名

1、方法参数尽量 <=4,超过了建议使用类代替

2、对于boolean参数,优先使用两个元素的枚举类型来表示。使得代码更易于阅读和编写以及后续扩展。

6.4 谨慎使用重载

1、重载方法的选择是在编译时期就确定的,而非运行时期确定的

    @Test
    public void t() {
        String s = "mjp";
        Object obj = s;
        doSomeThing(obj);
    }

    private void doSomeThing(Object obj) {//运行时是字符串,但是编译时是Object,所以走这个方法。
      //把这个方法删除会走下面重载方法。若二者的业务逻辑不一致,则有可能造成调用结果不符合预期
        System.out.println("obj");
    }

    private void doSomeThing(String s) {
        System.out.println("s");
    }
    @Test
    public void t() {
        Collection<?> coll = new ArrayList<>();
        doSomeThing(coll);
    }

    private void doSomeThing(Collection<?> coll) {//编译时期是Collection类型,走这个方法
        System.out.println("coll");
    }

    private void doSomeThing(List<?> list) {
        System.out.println("list");
    }

2、方法重载可能存在的问题

  • 方法提供方,删除了某个重载方法,使用方可能会自动使用另外一个重载方法
        //A类中调用B类的重载方法doSomeThing,默认是调用方法1
        //如果哪天,B类中方法1被删除了,则会走方法2。若方法2和1业务逻辑不一致,则有可能造成调用结果不符合预期(但是不会报错)
        String s = "mjp";
        Object obj = s;
        doSomeThing(obj);
    
    
    //B类
    //方法1
    private void doSomeThing(Object obj) {//运行时是字符串,但是编译时是Object,所以默认走这个方法。
        System.out.println("obj");
    }

		//方法2
    private void doSomeThing(String s) {
        System.out.println("s");
    }
  • 方法报错

    删除集合中的某个元素

    @Test
    public void t() {
        List<String> strList = Lists.newArrayList("mjp","wxx");
        List<Integer> list = Lists.newArrayList(18,23);
        removeEle(strList, "mjp");
        removeEle(list, 18);
    }

    private void removeEle(List<String> strList, String ele) {
        if (strList.contains(ele)) {
            strList.remove(ele);
        }
    }

    private void removeEle(List<Integer> list, Integer ele) {
        if (list.contains(ele)) {
            list.remove(ele);
        }
    }
    private void removeEle(List<Integer> list, int ele) {
        if (list.contains(ele)) {
            list.remove(ele);//这里是删除指定下标元素,remove(int index),会报数组越界异常java.lang.IndexOutOfBoundsException: Index: 18, Size: 2
        }
    }

因为会自动装箱,所以int ele重载方法在if判断时候等效list.contains(Integer.valueOf(ele));

list.remove(int index):删除指定下标的元素

list.remove(Object obj):删除指定元素

3、SOP

当方法背后的逻辑一致时,才应该使用重载。

eg:PC、小程序,web、h5不同端传递的方法入参类型不一样,但是业务逻辑都一样。这样就可以提供重载方法

6.5 谨慎使用可变参数

1、可变参数特点

  • 本质是数组。String…args对应String数组;Integer…args对应Integer数组
  • 可传>=0个参数。不传默认是空数组而非null
  • 只能方法方法的最后。所以,方法最多只能有一个可变参数

2、可能存在的问题

  • 频繁的生成数组,可能存在性能问题。可以使用方法重载,多个参数替代
public void foo() {}
public void foo(int a1) {}
public void foo(int a1, int a2) {}
public void foo(int a1, int a2, int a3) {}
public void foo(int a1, int a2, int a3, int... rest) {}

6.6 返回零长度的集合,而非null

1、可以返回长度为0的集合

return Collections.emptyList();

2、循环遍历查询,没有数据可以返回集合

return Lists.newArrayList()

6.7 Optional

1、Optional简介

点击展开内容

  • Optional 类的引入很好的解决空指针异常。
  • Optional 是个容器:它可以保存类型T的值,或者仅仅保存null;Optinal类本质上是一个只能存放一个元素的不可变集合
  • Optional.empty ()返回一个空的optional;Optional.of(value)返回一个包含了指定非null值的optional

2、Optional使用

点击展开内容

方法名称作用eg备注
empty()返回空的 Optional 实例Optional*<Integer>* optional = Optional.empty();//Optional.emptyOptional集合中有一个为Null的元素,则ifPresent返回false
ifPresent值存在则方法会返回trueOptional*<Integer>* optional2 = Optional.ofNullable(1);//Optional[1]Optional集合中有一个不为Null的元素1,则ifPresent返回true
ofNullable(T value)如果为非空,返回 Optional 描述的指定值,否则返回空的 OptionalOptional*<Integer>* optional2 = Optional.ofNullable(null);//Optional.empty Optional*<Integer>* optional3 = Optional.ofNullable(3);//Optional[3]
map如果调用方有值,则对其执行调用映射函数得到返回值。 如果返回值不为 null,则创建包含映射返回值的Optional作为map方法回值,调用方无值,否则返回空Optional。Optional*<Integer>* optional = Optional.ofNullable(3);//Optional[3]optional有值,且map映射后的返回值也不为null,则最终返回:Optional[“0011”]Optional*<String>* optionalByte = optional.map*(Integer::toBinaryString);Optional<Integer>* optional = Optional.ofNullable(null);//Optional.emptyOptional*<String>* optionalByte = optional.map*(*Integer::toBinaryString); optional无值则返回Optional.emptyOptional*<Integer>* optional = Optional.ofNullable(3);Optional*<String>* result = optional.map*(null)*; 报错npe
orElse**(T other)**如果存在该值,返回值, 否则返回 other。Optional*<Integer>* optional = Optional.ofNullable(1);//Optional[3] Integer result = optional.orElse*(23); System.out.println(result);//1 Optional<Integer>* optional1 = Optional.empty(); Integer result1 = optional1.orElse*(23); System.out.println(result1)*;//23

优雅的取值

dto.setSupplierId(
  Optional.ofNullable(source.getVendorDTO()).map(VendorDTO::getVendorId).orElse(null)
);

3、Optional注意事项:

  • 不要给 Optional 变量赋值 null,否则违背了Optional的初衷
        Optional<Integer> optional = Optional.empty();
        Optional<String> result = optional.map(Integer::toBinaryString);
        System.out.println(result);//Optional.empty
        Optional<Integer> optional = null;
        Optional<String> result = optional.map(Integer::toBinaryString);
        System.out.println(result);//npe

七、代码的艺术:Don’t make me think

一眼看过去,如果无法看清逻辑,这不是好代码

好的代码不需要你思考太多

一定记住:代码更是写给别人看的

一流代码的特性

• 高效 (Fast)

• 鲁棒 (Solid and Robust)

• 简洁 (Maintainable and Simple)

• 简短 (Small)

• 可测试 (Testable)

• 可移植 (Portable)

• 可监控 (Monitorable)

• 可扩展(Scalable & Extensible):功能的单一是复用和扩展的基础

视频链接

7.1 把信息装进名字

1、使用专业的名词代替空洞的名次(maxAge而非age、height而非size、distribute而非send、compute而非get)

2、有单位的,需要带上单位:hex、Ms、Min、Secs、MB、CM

7.2 不要使用让人产生误解的名称

1、boolean的变量名称不要使用反义词:dis、not(disLock)

2、在定义类的属性xxx是boolean类型时,不建议属性名为isXXX

原因:isXXX自动生成的getter方法 ,方法名称就是isXXX。

常见的序列化反序列化工具:

  • 只有Gson是通过反射遍历获取到属性,然后将其值进行序列化,

  • fastJson和JackJson(SpringBoot集成了jackson,默认使用jackson来进行json序列化)是反射遍历获取对象的getter方法

二者对属性赋值时,属性名称被解析为:

  • 正常情况下: skuId属性,对应get方法为getSkuId,属性名称解析为去掉get,首字母小写,skuId。 和属性名一致
  • 属性为boolean isNeedGood基本类时,对应的get方法为isNeedGood,属性名称解析为去掉is,首字符小写,needGood。和属性名称不一致了,这样序列化赋值就失败了
  • 属性为Boolean isNeedGood包装类时,默认的get方法为getNeedGood,属性名称解析为去掉get,首字符小写,needGood。和属性名称不一致
public class Mjp {

    private boolean isNeedGood;
    private Long skuId;


//isNeedGood属性对应的get方法为getNeedGood,会把is吃掉。
//正常情况下没有什么影响,但是在json序列化的时候,对于is开头的方法,会默认(即isNeedGood去掉is,然后第一个字母小写)needGood
//这样在序列化的时候,希望是将true赋值给isNeedGood,但是实际情况是 “needGood”:true,显然没有needGood属性,这么一来,isNeedGood就未被赋值了
    
    public boolean isNeedGood() {
    	return isNeedGood;
		}

    public Long getSkuId() {
        return skuId;
    }
}
public class Demo {

    private Boolean isNeedMater;

    public Boolean getNeedMater() {
        return isNeedMater;
    }

    public void setNeedMater(Boolean needMater) {
        isNeedMater = needMater;
    }
}

        Demo demo = new Demo();
        demo.setNeedMater(Boolean.TRUE);
        System.out.println(GsonUtil.toJsonStr(demo)); //{"isNeedMater":true}
        System.out.println(new ObjectMapper().writeValueAsString((demo)));//{"needMater":true}
        System.out.println(JSON.toJSONString(demo));//{"needMater":true}
         

这里设置isNeedMater为true,当使用fastJson进行序列化后,再通过Gson进行反序列化,结果就会出问题。

本来给isNeedMater赋值的是true,但是反序列化以后的结果是false

public class Demo {
    private boolean isNeedMater;

    public boolean getNeedMater() {
        return isNeedMater;
    }

    public void setNeedMater(boolean needMater) {
        isNeedMater = needMater;
    }

    @Override
    public String toString() {
        return "Demo{" +
                "isNeedMater=" + isNeedMater +
                '}';
    }
}


Demo demo = new Demo();
demo.setNeedMater(Boolean.TRUE);
System.out.println(GsonUtil.fromJson(JSON.toJSONString(demo), Demo.class));//Demo{isNeedMater=false}

fastJson通过反射遍历找到属性isNeedMater对应的getter方法,解析认为这个类的属性是needMater,然后获取其值,将其序列化为{“needMater”,true}

然后Gson解析字符串,通过needMater找该类的属性,结果发现该类就一个属性isNeedMater,没有needMater属性。

因此Gson反序列化后isNeedMater会使用其默认值false。同理如果Boolean isNeedMater则为Demo{isNeedMater=null}

解决方式:

1、布尔类型的属性名,不建议为isXXX

2、人为使用@Data注解,注解帮忙生成getter方法,因为其生成的方法名为:getIsNeedMater

@Data : 注在类上,提供类的get、set、equals、hashCode、canEqual、toString方法

@Data
public class Demo {
    private Boolean isNeedMater;
}
    @Test
    public void t() {
        Demo demo = new Demo();
        demo.setIsNeedMater(Boolean.TRUE);

        demo.getIsNeedMater();//这里的getter方法名称为:getIsNeedMater
        System.out.println(GsonUtil.fromJson(JSON.toJSONString(demo), Demo.class));//Demo(isNeedMater=true)
    }
补充:Boolean|boolean isXxx命名的理解

1、什么场景下,这样命名会有问题

  • fastJson和JackJson在序列化和反序列化时可能会有问题(有些框架集成了相应工具,eg:SpringBoot集成了jackson,默认使用jackson来进行json序列化)

2、出现问题的原因

  • 正常情况下: skuId属性,对应get方法为getSkuId,属性名称解析为去掉get,首字母小写,skuId。 和属性名一致

  • 属性为boolean isNeedGood基本类时,对应的get方法为isNeedGood,属性名称解析为去掉is,首字符小写,needGood。和属性名称不一致

  • 属性为Boolean isNeedGood包装类时,默认的get方法为getNeedGood,属性名称解析为去掉get,首字符小写,needGood。和属性名称不一致

  • fastJson、jackson在序列化和反序列化时,是通过反射遍历找到属性isNeedMater对应的getter方法,通过get方法解析得到对应属性名称。

    认为这个类的属性名称为needMater即{“needMater”,true},我们期望的是{“isNeedMater”,true}

补充:使用Gson序列化和反序列化时,不会存在上述问题:Gson是通过反射遍历直接获取到属性(不是通过解析get方法名称),对其进行序列化和反序列化

public class Demo {
    private Boolean isNeedMater;
    public Boolean getNeedMater() {
        return isNeedMater;
    }
}

        Demo demo = new Demo();
        demo.setNeedMater(Boolean.TRUE);
				//Gson
        System.out.println(GsonUtil.toJsonStr(demo)); //{"isNeedMater":true}
				//jackSon
        System.out.println(new ObjectMapper().writeValueAsString((demo)));//{"needMater":true}
				//fastJson
        System.out.println(JSON.toJSONString(demo));//{"needMater":true}

3、问题复现

Boolean isNeedMater属性,使用set方法赋值后,使用fastJson序列化,再使用Gson进行反序列化,得到的属性isNeedMater无值

public class Demo {
    private boolean isNeedMater;
    public boolean getNeedMater() {
        return isNeedMater;
    }

    public void setNeedMater(boolean needMater) {
        isNeedMater = needMater;
    }

    @Override
    public String toString() {
        return "Demo{" +
                "isNeedMater=" + isNeedMater +
                '}';
    }
}


Demo demo = new Demo();
demo.setNeedMater(Boolean.TRUE);
System.out.println(GsonUtil.fromJson(JSON.toJSONString(demo), Demo.class));//Demo{isNeedMater=false}
如果是Boolean isNeedMater,同理Demo{isNeedMater=null},isNeedMater都没值

4、如何解

  • 方式1:布尔类型属性,不要以is开头命名

  • 方式2:使用lombok的@Data注解,代替get、set方法

    @Data
    public class Demo {
        private Boolean isNeedMater;
    }
    
    @Test
    public void t() {
        Demo demo = new Demo();
        demo.setIsNeedMater(Boolean.TRUE);
    
        //demo.getIsNeedMater();//这里的getter方法名称为:getIsNeedMater
        System.out.println(GsonUtil.fromJson(JSON.toJSONString(demo), Demo.class));//Demo(isNeedMater=true)
    }
    

5、mthrift这样命名会有问题么

  • 美团的rpc是thfirt,使用protocol进行序列化和反序列化
namespace java com.sankuai.groceryscm.vmi.client.thrift
struct User{
        1: i32 id= 0;
        2: required string name;
        3: bool isNeedMaster;
}
@Test
    public void new_test(){
        byte[] bytes = serial();
        System.out.println("序列化以后的对象:" + Arrays.toString(bytes));
        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
        parse(bis);

    }

    /**
     * 序列化方法
     */
    private static byte[] serial() {
        User user = new User();
        user.setId(100);
        user.setName("sss");
        user.setIsNeedMaster(true);
        System.out.println("序列化之前的对象:" + user);

        // 序列化
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        TTransport transport = new TIOStreamTransport(out);
        TBinaryProtocol tp = new TBinaryProtocol(transport);//二进制编码格式进行数据传输
//        TCompactProtocol tp = new TCompactProtocol (transport);
        try {
            user.write(tp);
        } catch (TException e) {
            e.printStackTrace();
        }
        byte[] buf = out.toByteArray();
        return buf;
    }

    /**
     * 反序列化方法
     * @param bis
     */
    private static void parse(ByteArrayInputStream bis) {
        User user = new User();
        TTransport transport = new TIOStreamTransport(bis);
        TBinaryProtocol tp = new TBinaryProtocol(transport);
//        TCompactProtocol tp = new TCompactProtocol(transport);
        try {
            user.read(tp);
            System.out.println("反序列化后的对象:" + user);
        } catch (TException e) {
            e.printStackTrace();
        }
    }

序列化之前的对象:User(id:100, name:sss, isNeedMaster:true)

序列化以后的对象:[8, 0, 1, 0, 0, 0, 100, 11, 0, 2, 0, 0, 0, 3, 115, 115, 115, 2, 0, 3, 1, 0]

反序列化后的对象:User(id:100, name:sss, isNeedMaster:true)

  • 所以,我们rpc这样命名不会存在问题。

7.3 审美

1、对齐:注释参数、变量

2、相似的代码,格式要一样(注释要么都在一行,要么都在末尾)

3、使用空行将大段代码分为逻辑上的”段落“(处理req的、处理resp的)

7.4 好的注释

1、好的名字 > 坏的名字 + 好的注释

2、想到什么先记录下来 -> 改进一下 -> 不断改进

3、在读者的立场思考

4、Map<Map<>>注释 k1 -> (k2,v2)

5、 描述方法的业务行为,而非代码行为

6、可适当加入输入输出的example

7.5 更易于阅读的代码

1、if优先处理正向逻辑

2、do while -> while

3、提前return可以让代码更整洁

4、if里面判断条件如果过于复杂,要抽取出一个函数或者临时变量

5、if正向逻辑过于复杂的时,可以考虑反方向

7.6 变量可读性

1、 while控制变量可以抽取为boolean方法,提前return

2、在第一次使用的时候再定义变量

3、避免一个操作的局部变量出现在另一个操作方法中

7.7 抽取无关的代码,方法职责单一

1、 切分模块的一种角度

• 计算数据方法(数据为中心,面向对象面向数据)

• 过程方法

八、通用编程

8.1 优先使用增强For循环

1、三种场景下只能使用普通For循环

  • 边遍历边删除【不要使用增强For进行】
        List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);
        for (int i = 0; i < list.size(); i++) {
            list.removeIf(integer -> integer > 3);
        }
        
        //这里也可以使用迭代器进行边遍历边删除
         List<Integer> list2 = Lists.newArrayList(1, 2, 3, 4, 5);
        Iterator<Integer> iterator = list2.iterator();
        while (iterator.hasNext()) {
            Integer next = iterator.next();
            if (next > 3) {
                iterator.remove();
            }
        }
        System.out.println(list2);
  • 转换:遍历的时候将指定索引下的元素换成其它值
  • 平行迭代:使用索引下标,使得list1和list2中元素可以同步前进
        Map<Integer, String> map = Maps.newHashMap();
        List<Integer> list1 = Lists.newArrayList(1, 2, 3);
        List<String> list2 = Lists.newArrayList("mjp","xyz","cc");
        for (int i = 0; i < list1.size(); i++) {
            Integer age = list1.get(i);
            String name = list2.get(i);
            map.put(age, name);
        }

8.2 了解和使用类库

1、java8的LongAdder在高并发下优于AutomicLong

        AtomicLong atomicInteger = new AtomicLong(0L);
        long i = atomicInteger.addAndGet(1L);
        System.out.println(i);

        LongAdder adder = new LongAdder();
        adder.add(7L);
        System.out.println(adder);

8.3 需要精确,不要使用double

1、缺点:金钱类的不要使用double回丢失精度

2、替代

  • 使用long,单位为分【推荐】
  • 使用BigDecimal,但是初始化小数的时候,只能用字符串,若使用double(0.1)初始化的时候就丢失了精度
        double a = 1.0;
        double b = 0.9;
        double c = a - b;
        System.out.println(c);//0.09999999999999998

        BigDecimal b1 = new BigDecimal(1.0);
        BigDecimal b2 = new BigDecimal(0.9);
        BigDecimal subtract = b1.subtract(b2);
        System.out.println(subtract);//0.09999999999999997779553950749686919152736663818359375

        BigDecimal b3 = new BigDecimal("1.0");
        BigDecimal b4 = new BigDecimal("0.1");
        BigDecimal subtract1 = b3.subtract(b4);
        System.out.println(subtract1);//0.9

8.4 字符串连接

1、s1 + s2 + s3会被自动优化为sb.append(s1).append(s2).append(s3).toString()

由于字符串的不可变性,连接 n 个字符串重复使用字符串连接操作,需要 n2 的时间。

sb 对象内部维护一个字符数组。操作都是在字符数组上进行,append 方法的时间是线性的

2、字符串不适合替代其他值类型,数据本质上确实是文本信息时,使用字符串才合理

3、参考:https://www.cnblogs.com/frankyou/p/9828555.html 和 唯品会的工具类https://github.com/vipshop/vjtools/blob/master/vjkit/src/main/java/com/vip/vjtools/vjkit/text/StringBuilderHolder.java

九、异常处理

1、不要在 finally 块中使用 return(说明:finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句)

2、sop

  • 可以使用warn日志级别来记录用户输入参数错误的情况。如非必要,请不要在此场景打出 error 级别,避免频繁报警(说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息)

  • Business_error和interalError的区别:interal异常主要是一些无法预料的原因导致的rpc失败,比如网络抖动超时等。

  • catch匹配到异常后,会把异常吃掉。如果你在catch中打了相关信息,没有再向上抛出异常,则异常就在此处被吃掉了。如果是@Trasactional注解,异常就不能被吃掉,就需要在catch中再向上throw,这样事物才能一致。

  • 调用者为前端的时候,如果你不想让前端在调用时抛出红色异常。那么你就不在最外层catch中再次throw一个异常,而是吃掉这个异常,并且给出相应的code值和message即可。打出error日志即可

1、不要忽略捕捉的异常

catch (NoSuchMethodException e) {
 return null;
}

虽然捕捉了异常但是却没有做任何处理,除非你确信这个异常可以忽略,不然不应该这样做。这样会导致外面无法知晓该方法发生了错误,无法确定定位错误原因。

2、在你的方法里抛出定义具体的检查性异常

public void foo() throws Exception { //错误方式
}
推荐:
public void foo() throws SpecificException1, SpecificException2 { //正确方式
}

3、捕获具体的子类而不是捕获 Exception 类

try {
 	someMethod();
} catch (Exception e) { //错误方式
 	LOGGER.error("method has failed", e);
}
推荐:
try {
 	rpc();
} catch (TException e) { 
 	LOGGER.error("method has failed", e);
}

4、始终正确包装自定义异常中的异常,以便堆栈跟踪不会丢失

catch (NoSuchMethodException e) {
 	throw new MyServiceException("Some information: " + e.getMessage()); //错误方式
}
推荐:
catch (NoSuchMethodException e) {
 	throw new MyServiceException("Some information: " , e); //正确方式
}

5、要么记录异常要么抛出异常,但不要一起执行

catch (NoSuchMethodException e) { 
	//错误方式 
	LOGGER.error("Some information", e);
 	throw e;
}

正如上面的代码中,记录和抛出异常会在日志文件中产生多条日志消息,代码中存在单个问题,并且对尝试分析日志的同事很不友好。

6、finally 块中永远不要抛出任何异常

7、始终只捕获实际可处理的异常

catch (NoSuchMethodException e) {
 	throw e; //避免这种情况,因为它没有任何帮助
}

不要为了捕捉异常而捕捉,只有在想要处理异常时才捕捉异常,或者希望在该异常中提供其他上下文信息。如果你不能在 catch 块中处理它,那么最好的建议就是不要只为了重新抛出它而捕获它。

8、不要使用 printStackTrace() 语句或类似的方法

最终别人可能会得到这些堆栈,并且对于如何处理它完全没有任何方法,因为它不会附加任何上下文信息。

9、记住早 throw 晚 catch 原则

应该尽快抛出(throw)异常,并尽可能晚地捕获(catch)它。应该等到有足够的信息来妥善处理它。

10、在异常处理后清理资源

则仍应使用 try-finally 块来清理资源。 在 try 模块里面访问资源,在 finally 里面最后关闭资源。即使在访问资源时发生任何异常,资源也会优雅地关闭。

11、尽早验证用户输入以在请求处理的早期捕获异常

12、一个异常只能包含在一个日志中,在日志文件中这两个日志消息可能会间隔 100 多行。应该这样做:

LOGGER.debug("Using cache sector A");
LOGGER.debug("Using retry sector B");
推荐:
LOGGER.debug("Using cache sector A, using retry sector B");

13、编写多重catch语句块注意事项:顺序问题:先小后大,即先子类后父类

否则,捕获底层异常类的catch子句将可能会被屏蔽。

14、多个异常的处理逻辑一致时,使用JDK7的语法避免重复代码

try {
  ...
} catch (AException | BException | CException ex) {
  handleException(ex);
}

15、异常处理不能吞掉原异常,要么在日志打印,要么在重新抛出的异常里包含原异常

catch(XxxException e){
 		 //WRONG
		throw new MyException("message");

		//RIGHT 记录日志后抛出新异常,向上次调用者屏蔽底层异常
		logger.error("message", ex);
		throw new MyException("message");

		//RIGHT 传递底层异常
		throw new MyException("message", ex); 
}

16、如果处理过程中有抛出异常的可能,也要做try-catch,否则finally块中抛出的异常,将代替try块中抛出的异常

//WRONG
try {
  ...
  throw new TimeoutException();
} finally {
  file.close();//如果file.close()抛出IOException, 将代替TimeoutException
}

//RIGHT, 在finally块中try-catch
try {
  ...
  throw new TimeoutException();
} finally {
  IOUtil.closeQuietly(file); //该方法中对所有异常进行了捕获
}

17、不能在finally块中使用return,finally块中的return将代替try块中的return及throw Exception

//WRONG
try {
  ...
  return 1;
} finally {
  return 2; //实际return 2 而不是1
}

try {
  ...
  throw TimeoutException();
} finally {
  return 2; //实际return 2 而不是TimeoutException
}

十、并发

1、同步访问可变数据

  • 若共享的可变数据只需要可见,则使用Volatile即可(不提供互斥)。多线程要注意互斥,正常情况下需要使用同步、锁

  • 对字符串加锁,为了互斥性,需要使用synchronized(s.intern())

    因为字符串常量池和堆内存中,地址不一样,不互斥。加上intern()就互斥了

2、避免过度同步

并发集合,代替使用锁

  • CopyOnWriteArrayList,适合读多写少
  • ConcurrentHashMap,若一致性,略有数据同步延时

同步区域内少执行任务,计算工作最好放在锁外部

  • 获得锁
  • 检查共享数据
  • 操作数据
  • 释放锁

3、优先使用线程池而非new Thread

  • 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式

Executors 返回的线程池对象的弊端如下:

\1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM

\2) CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

4、在高并发场景中,避免使用”等于”判断作为中断或退出的条件(说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间 判断条件来代替)

4、并发工具(CountDownLatch)优于wait notify

MapKeyValue
HashMapNullableNullable
ConcurrentHashMapNotNullNotNull
TreeMapNotNullNullable

Executor 框架;并发集合;同步器:CountDownLatch

    @Test
    public void t() throws InterruptedException {
        // 01.创建门栓
        int threadCount = 5;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        AtomicInteger atomicInteger = new AtomicInteger(0);

        // 02.创建线程执行
        Integer baseScore = 10000;
        Random random = new Random();
        List<CompletableFuture> cfList = new ArrayList<>();
        for (int i = 1; i < threadCount+1; i++) {
            int finalI = i;
            CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
                int score = baseScore + random.nextInt(2000);
                atomicInteger.addAndGet(score);
                System.out.println("第" + finalI + "个运动员的成绩:" + score + "");
                countDownLatch.countDown();
            });
            cfList.add(cf);
        }

        cfList.forEach(CompletableFuture::join);

        // 03.解开
        countDownLatch.await();
        System.out.println(atomicInteger.get() / threadCount);
    }
    

countDownLatch-执行先后顺序【可实现分布式锁】


    @Test
    public void t() throws InterruptedException{

        CountDownLatch u1 = new CountDownLatch(1);
        CountDownLatch u2 = new CountDownLatch(1);
        CountDownLatch u3 = new CountDownLatch(1);
        CountDownLatch u4 = new CountDownLatch(1);
        CountDownLatch u5 = new CountDownLatch(1);

        // 0.1通过count和await定义执行顺序
        Thread top = new Thread(() -> {
            System.out.println("上单选择英雄完毕");
            u1.countDown();
        });

        Thread jog = new Thread(() -> {
            try {
                u1.await();
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            }
            System.out.println("打野选择英雄完毕");
            u2.countDown();
        });

        Thread mid = new Thread(() -> {
            try {
                u2.await();
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            }
            System.out.println("中单选择英雄完毕");
            u3.countDown();
        });

        Thread adc = new Thread(() -> {
            try {
                u3.await();
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            }
            System.out.println("ADC选择英雄完毕");
            u4.countDown();
        });

        Thread assist = new Thread(() -> {
            try {
                u4.await();
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            }
            System.out.println("辅助选择英雄完毕");
            u5.countDown();
        });

        // 02.执行
        assist.start();
        mid.start();
        top.start();
        adc.start();
        jog.start();


        u5.await();
        System.out.println("全部完成");
    }

10.5 正确的停止线程

停止单条线程,执行Thread.interrupt()。

  • 并不保证能中断正在运行的线程
  • 执行Thread.interrupt()时,如果线程处于sleep(), wait(), join(), lock.lockInterruptibly()等blocking状态,当阻塞方法收到中断请求的时候就会抛出InterruptedException异常,如果线程未处于上述状态,则将线程状态设为interrupted。

停止线程池:参考:唯品会工具类gracefulShutdown

  • ExecutorService.shutdown(): 不允许提交新任务,等待当前任务及队列中的任务全部执行完毕后退出;
  • ExecutorService.shutdownNow(): 通过Thread.interrupt()试图停止所有正在执行的线程,并不再处理还在队列中等待的任务。

10.6 处理InterruptedException异常

public class InterrupTest implements Runnable{

    @Override
    public void run(){
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {

                boolean interrupted1 = Thread.interrupted();
                System.out.println("t线程收到main的请求中断,但是t处于阻塞,所以抛出异常,并将中断信号变成: "+interrupted1+"");


                //恢复中断状态,即恢复线程t被main线程告知应该中断的信号,以便main线程中能知道t线程的中断,并且对中断作出响应
                //如果这里不恢复中断请求,等于停止了main要求t中断的请求,外层函数将收不到中断请求,继续原有循环(一直while循环)
                Thread.currentThread().interrupt();

                boolean interrupted2 = Thread.interrupted();
                System.out.println("恢复线程t被main线程告知应该中断: "+interrupted2+"");
            }
    }

    public static void main(String[] args) {
        InterrupTest si = new InterrupTest();
        Thread t = new Thread(si);
        t.start();

        // 01.主线程sleep 2s 后再执行对t线程的中断,让t执行一会
        sleepSecond(2);

        // 02.中断线程t
        t.interrupt();

        // 03.如果线程t未被中断,则xxx,中断了则结束
        while (!t.isInterrupted()) {
            System.out.println("t继续执行");
        }
    }


    
    public static void sleepSecond(int time) {
        try {
            TimeUnit.SECONDS.sleep(time);
        } catch (InterruptedException exception) {
        }
    }
}

10.7 多个异常的处理逻辑一致时,使用JDK7的语法避免重复代码

try {
  ...
} catch (AException | BException | CException ex) {
  handleException(ex);
}

十一、序列化

1.谨慎使用Serializable

  • 序列化不走构造器(clone也是),在构造器中进行了提前的安全检查,会被绕过

  • 大大降低了灵活性:一旦确认了序列化的形式,后续任何变动都可能导致使用这个格式进行反序列化的程序报错

  • 建议显示的指定serialVersionUID:版本控制,表明类的不同版本间的兼容性

    不指定可能存在的问题

    点击展开内容

    User类实现了序列化,属性age和name【版本1】

    但是未指定serialVersionUID,再序列化的时候JVM会根据age、name计算出一个id-A值,和属性一起,共同组成user1后,序列化,再进行网络传输并以二进制字节流的形式持久化到磁盘(数据的id位A值)

    反序列化user1的时候,JVM会再根据属性name、age自动生成一个id-B,比较id-B和id-A,相同则反序列化成功,否则报错

    问题:

    现在user类新增了一个属性sex性别【版本2】

    那么,在对旧版本1的持久化数据user1,进行反序列化操作时

    JVM会再根据版本2的属性age、name、sex进行计算生成一个id-B2,比较id-B2和id-A,此时两个值明显不一样【计算时的属性个数都不一样】,所以反序列化use1时候,会报错

    (反序列化时系统会自动检测二进制文件中的serialVersionUID,判断它是否与当前类中的serialVersionUID【用户定义了则使用定制值,没有定义则JVM根据类属性等实时计算出一个值】一致。如果一致说明序列化文件的版本与当前类的版本是一样的,可以反序列化成功,否则就失败)

    解决问题:

    User类实现了序列化,属性age和name【版本1】

    指定serialVersionUID = 1

    再序列化的时候JVM会id-A = 1值,和属性一起,共同组成user1后,序列化,再进行网络传输并以二进制字节流的形式持久化到磁盘(数据的id位A=1值)

    反序列化user1的时候,JVM会再根据属性name、age自动生成一个id-B,比较id-B和id-A=1,相同则反序列化成功,否则报错InvalidClassExceptions

    问题:

    现在user类新增了一个属性sex性别【版本2】

    那么,在对旧版本1的持久化数据user1,进行反序列化操作时

    JVM会再根据版本2得到 id-B2 = 1,比较id-B2和id-A,此时两个值都是1,所以反序列化use1成功

    建议自定义生成serialVersionUID而不是使用默认值1:https://blog.csdn.net/wufaqidong1/article/details/127295513

2.序列化相关知识点

  • 反序列化的对象,不会调用构造函数重新构造,而是基于二进制文件进行生成的新对象

  • 序列化前的对象和序列后的对象,地址不一样,但是equals是ture,因为是是深copy

  • 序列化和持久化的关系

    前者是为了跨进程调用,后者为了写入磁盘

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值