• 《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;
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    • 不用重复的建对象-单例、Gson打日志

    • 可以返回子类型

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

          public static Animal getInstance(String name) {
              if ("Dog".equals(name)) {
                  return new Dog();
              }
              
              if ("Cat".equals(name)) {
                  return new Cat();
              }
              return new Animal();
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10

    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对象,会可能导致失败。
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

    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
    • 2
    • 3

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

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

    1、为什么私有化

    • 工具类不希望被实例化,因为实例化工具类没有意义
    @UtilityClass
    public class DateUtil {
    
        public Integer getInt() {
            return 1;
        }
    }
    
    //默认添加了,私有化的构造器
    private DateUtil(){
    }
    
    别的地方无法通过new的形式创建
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 但不提供构造函数的时候,编译器会自动提供一个默认的构造器【这样这个工具类无法具有子类了】

    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
      • 2
      • 3
      • 4
      • 5
      • 6

    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();
                  }
              }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

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

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

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

    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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类型
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 传递性:

    ​ 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
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    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());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 其中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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    
    public class User {
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    好处2

    
    //好处2:可以修改属性的名称而不用关注调用方
    //现在我觉得,name字段描述的不准确,换个名称为nickName,直接在本类中修改就ok,不用关注调用方
    public class User {
        private String nickName;
    
        public String getName() {
            return nickName;
        }
    
        public void setName(String name) {
            this.nickName = name;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

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

    解决:

    假的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[];
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以深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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 特殊情况:真final不可变,可以在类中提供public成员变量

      private修饰和public修饰效果一样

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

    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方法则类中的集合、数组内容会收到外部数据源的内容变化而变化
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 也不要,直接返给外部。可以内部提供深copy方法,返回内容和关键属性一样的对象,但是不执向同一块内存地址。操作返回结果,不影响类本身的关键字段内容【3.1好处2】

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

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

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

    1、复合

    @Data
    @Accessors(chain = true)
    public class SmallName {
    
        private String foreverName;
        
        @Resource
        private User user;//符合添加了user类,可以使用user对象的方法
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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();删除
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40

      HashSet的addAll方法

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

      Sup

      public class Super {
        public Super() {
        	overrideMe ();//02-造器能调用可了被覆盖的方法❌
        }
        public void overrideMe () {
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

      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.子类方法,有值了
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

    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
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
    • 接口中只能有常量【但是不建议,接口只提供行为规范】、抽象类中可以有成员变量

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

      1、原因
      类实现常量接口,这对于这个类的用户来讲并没有实际的价值。实际上,这样做返回会让他们感到更糊涂
      2、建议
      使用Enum
      
      • 1
      • 2
      • 3
      • 4
    • 抽象类单继承【但是了类可以实现多个接口】; 接口多继承接口可以同时继承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{
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 接口是行为规范,具体实现逻辑由实现类自己定义 ;

      抽象类定义了大部分公有的具体行为,根据不同子类定义了不同的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;
        }
      }
      优点:新增图形,则新增类即可,不需要修改原本的代码,遵行开闭原则
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64

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

    要表示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{
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 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类的方法外,有且仅有一个方法,这种就是函数式接口
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    方法作为对象传入方法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的大小升序排序
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    Lists.newArrayList(new User(1,"mjp")).stream().sorted(Comparator.comparing(User::getAge)).collect(Collectors.toList());
    这里的sort方法就是传入一个比较大小的方法,然后返回一个StreamStream<T> sorted(Comparator<? super T> comparator);
    
    • 1
    • 2
    • 3

    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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 非staic成员内部类对象,强绑定外部类对象【比如EntrySet对象和HashMap就是】,可能会影响GC

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

      Map-Entry

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

    四、泛型

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 方法入参,使用原生态类型
        @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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

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

    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
      
      • 1
      • 2
      • 3
      • 4
      • 5

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

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

    4.2 消除非受检异常

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

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

    4.3 泛型list优于数组

    1、原因

    • 数组是协变的

      ArrayStoreException

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

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

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

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

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

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

      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
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

    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
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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异常会在使用方程序报出来,提供方的代码没影响
      
      //如果内部使用这种方式,那么到处都是强转的代码,乱
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
    • 内部使用,返回泛型【方法内部统一帮你强转换了,避免了频繁的类型转换】

        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类型
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5

    2、 ? extends A

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

    • 读(comparable 和 comparator都是读取)

    • List,则?可以是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;
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

    3、 ? super A

    则?代表 A或者A的父类

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

    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接收,会编译提示错误
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 上述这种形式,需要在每个枚举中,都定义查询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       枚举类型
         * @param       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                枚举类型
         * @param                value类型
         * @param                异常类型
         * @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       枚举类型
         * @param       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       枚举类型
         * @param       value类型
         * @param       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          枚举类型
         * @param          value类型
         * @param          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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    判断集合整数,由哪些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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    给出整数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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    接口

    public interface Operate {
        double operate(double x, double y);
    }
    
    
    • 1
    • 2
    • 3
    • 4

    加减枚举

    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;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    public enum ModOperateEnum implements Operate{
        MOD() {
            @Override
            public double operate(double x, double y) {
                return x % y;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    
        @Test
        public void  t() {
            System.out.println(OperateEnum.ADD.operate(1, 2)); //3
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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{
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    常见注解
    • @Override :用来声明该方法的定义会重载基类中的某个方法。如果不小心拼错了 方法名,或者使用了不恰当的签名,该注解会使编译器报错。

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

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

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

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

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

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

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

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

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

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

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

    @Retention(RetentionPolicy.RUNTIME)
    public ©interface SQLInteger {
    	String name() default "";
    	Constraints constraintsO default ©Constraints(unique = true);
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Constraints {
    	boolean unique() default false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    通过反射获取注解
    getAnnotation:返回指定的注解
    isAnnotationPresent:判定当前元素是否被指定注解修饰
    getAnnotations:返回所有的注解
    
    getDeclaredAnnotation:返回本元素的指定注解
    getDeclaredAnnotations:返回本元素的所有注解,不包含父类继承而来的
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 获取方法上注解和注解的名称
        @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();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    使用

    自定义注解使用
    • 定义注解
    @Documented
    @Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Hello {
    
        String value();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 使用注解
    @Hello("go")
    public class Dog {
    }
    
    • 1
    • 2
    • 3
    • 获取注解
    @Test
        public void  t() {
            Class<Dog> dogClass = Dog.class;
            Hello proxyHandle = dogClass.getAnnotation(Hello.class);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    如何发现注解以及其属性

    通过反射获取了作用在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、根据属性值,进一步操作
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    参数校验注解

    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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计算合法性
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 使用注解
    public class User{
        @NotBlank(message = "姓名不能为空")
        private String name;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 校验
    User user = 从前端页面获取的Req
    ValidateUtil.validateParam(user);
    
    • 1
    • 2
    @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();
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • @Constraint,作用在注解上 ,内含ConstraintValidator接口
    @Documented
    @Target({ ANNOTATION_TYPE })
    @Retention(RUNTIME)
    public @interface Constraint {
    	Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 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;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 具体流程

      • 通过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 );
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    public class User{
        @NotBlank(message = "姓名不能为空")
        private String name;
    }
    
    • 1
    • 2
    • 3
    • 4
    User user = 从前端页面获取的Req
    ValidateUtil.validateParam(user);
    
    • 1
    • 2
    • 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {
    	@Override
        public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    分组校验

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    工具类校验

    User user = new User().setAge(3).setId(1);
    ValidateUtil.validateParam(user, User.Add.class, Default.class);
    1、工具类的validateParam方法,可以指定Group,当你是Add组,会去读取@Null注解
    2、最后必须加上默认的Default组,否则,其他所有注解都不生效了,只会生效你自定义的组
    
    • 1
    • 2
    • 3
    • 4
    级联校验
    @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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    注解实战

    自定义校验

    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 { };
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 自定义注解对应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获取数据,用这些数据,做复杂的用户输入校验。
    适合,用户输入的数据 需要和后端交互一次后,做校验的场景
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 使用
    @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
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 自定义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;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 使用
        @CollectionSizeCheck(value = 5,message = "当前用户输入的skuId集合最多{value}个")
        @NotEmpty(message = "skuIds集合不能为空")
        private List<Long> skuIds;
    
    • 1
    • 2
    • 3
    • 补充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();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 自定义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));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 使用
        @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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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 { };
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 自定义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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    public interface DateValidator {
        boolean isValid(String dateStr);
    }
    
    • 1
    • 2
    • 3
    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 使用
        @NotBlank(message = "日期不能为null")
        @DateCheck(message = "日期格式不正确")
        private String time;
    
    • 1
    • 2
    • 3
    • 将@DateFormatCheck注解中的值,传到DateFormatCheckConstraintValidatorImpl中,作为逻辑分支
    @DateFormatCheck(value = "yyyy-MM-dd", message = "sellTime(销售时间)必须为yyyy-MM-dd格式且必须为T-28到T+1", beforeCurrent = 28L,afterCurrent = 1L)
    将beforeCurrent值传递到impl中
    
    • 1
    • 2
    @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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    @Slf4j
    public class DateFormatCheckConstraintValidatorImpl implements ConstraintValidator<DateFormatCheck, String> {
    
        private long beforeCurrent;
    
        @Override
        public void initialize(DateFormatCheck constraintAnnotation) {
    
            this.beforeCurrent = constraintAnnotation.beforeCurrent();
          
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    根据用户输入日期的格式,自定义校验

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

    定制格式校验,可以为"2023-10-23 12:00:00"、"2023-10-23 12:00:00"、"12:45:00"、"12:45"
    
    • 1
    • 注解
    @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 { };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 接口和实现类
    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 注解使用
        @CustomizeDateFormat(value = "HH:mm", message = "算法售罄加量时间有误,正确格式为{value}")
        private String sellOutOrTime;
    
    • 1
    • 2
    • 补充:

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

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

    说明:这种可以校验卡住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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    字段允许为空,但非空时需要满足校验

    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 { };
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 实现类
    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3、扩展

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

    • 背景:

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

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

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

    • 处理

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

    @Constraint(validatedBy = StringDateFormatValidatorImpl.class, LongDateFormatValidatorImpl.class)
    public @interface MyStatus {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 实现类

    自定义两个实现类,一个实现校验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 "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 注解的使用1: key直接使用方法入参某个字段
    @RefundConcurrentControl(
                intervalTimeSeconds = 3,
                keyGenRule = "#operator",//类似于你要加锁的key(misId、orderNo、taskCode等)
                needRelease = true,
                specifyPrefix = "WriteShippingTaskDetail",//描述你加锁操作的目的,具体是对什么操作(创建退货单、导出加量日志等)
                specifyErrorMsg = "该单正在操作中,请稍后重试操作")
        public void confirmPDAShippingTaskDetail(ConfirmShippingTaskDetailRequest request,String operator) {
            
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 注解的使用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) {
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 注解的使用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) {
         
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    六、方法

    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;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
       @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
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 本质原因是:属性是非基本类型。外部传递的对象和属性对象都指向了同一块内存地址。一个改变了地址的内容,则属性对应地址的内容也改变了
    • 解法一: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
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    解法二:保护性拷贝[参考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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    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");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 方法报错

      删除集合中的某个元素

        @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
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    因为会自动装箱,所以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) {}
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

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

    return Collections.emptyList();
    
    • 1

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

    return Lists.newArrayList()
    
    • 1

    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)
    );
    
    • 1
    • 2
    • 3

    3、Optional注意事项:

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

    七、代码的艺术: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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    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}
             
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这里设置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}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
        @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)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    补充: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}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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都没值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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)
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

    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();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    序列化之前的对象: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>注释 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 转换:遍历的时候将指定索引下的元素换成其它值
    • 平行迭代:使用索引下标,使得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);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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;
    }
    
    • 1
    • 2
    • 3

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

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

    public void foo() throws Exception { //错误方式
    }
    推荐:
    public void foo() throws SpecificException1, SpecificException2 { //正确方式
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    try {
     	someMethod();
    } catch (Exception e) { //错误方式
     	LOGGER.error("method has failed", e);
    }
    推荐:
    try {
     	rpc();
    } catch (TException e) { 
     	LOGGER.error("method has failed", e);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    catch (NoSuchMethodException e) {
     	throw new MyServiceException("Some information: " + e.getMessage()); //错误方式
    }
    推荐:
    catch (NoSuchMethodException e) {
     	throw new MyServiceException("Some information: " , e); //正确方式
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    catch (NoSuchMethodException e) { 
    	//错误方式 
    	LOGGER.error("Some information", e);
     	throw e;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

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

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

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

    不要为了捕捉异常而捕捉,只有在想要处理异常时才捕捉异常,或者希望在该异常中提供其他上下文信息。如果你不能在 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");
    
    • 1
    • 2
    • 3
    • 4

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

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

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

    try {
      ...
    } catch (AException | BException | CException ex) {
      handleException(ex);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    catch(XxxException e){
     		 //WRONG
    		throw new MyException("message");
    
    		//RIGHT 记录日志后抛出新异常,向上次调用者屏蔽底层异常
    		logger.error("message", ex);
    		throw new MyException("message");
    
    		//RIGHT 传递底层异常
    		throw new MyException("message", ex); 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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); //该方法中对所有异常进行了捕获
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    十、并发

    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);
        }
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    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("全部完成");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    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) {
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

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

    try {
      ...
    } catch (AException | BException | CException ex) {
      handleException(ex);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    十一、序列化

    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

    • 序列化和持久化的关系

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

  • 相关阅读:
    c++征途 --- 数组
    java并发问题记录
    力扣第454题 四数相加 || c++哈希map使用
    日期格式化
    分辨率兼容学习笔记
    branch与tag
    java JVM设置
    数据结构前言
    JVM 性能调优参数
    SPark学习笔记:09SparkSQL 数据源之文件和JDBC
  • 原文地址:https://blog.csdn.net/tmax52HZ/article/details/132999138