文章目录
一、创建和销毁对象
1.1 使用静态方法代替构造器
1、静态方法好处
-
静态方法见名知意
private volatile static Cat cat; public static Cat getInstanceSingleton() { if (cat == null) { synchronized (Cat.class) { if (cat == null) { cat = new Cat(); } } } return cat; }
-
不用重复的建对象-单例、Gson打日志
-
可以返回子类型
public static Animal getInstance() { a = new Cat(); return a; }
-
可以根据静态方法的入参,返回不同类型的对象
public static Animal getInstance(String name) { if ("Dog".equals(name)) { return new Dog(); } if ("Cat".equals(name)) { return new Cat(); } return new Animal(); }
2、缺点
- 如果此类只想通过静态工厂方法获取实例,由于单例模式需要private构造器。所以,此类无法被extend
1.2 @Builder vs @Accessors(chain = true)
1、使用场景
-
类的属性太多,各种参数的有参构造器,人为都记不住属性名称了
-
setter方法,在构造过程中JavaBeans可能处于不一致的状态
User user = new User(); user.setName("mjp"); user.setAge(1); 有些对象从创建到销毁需要保持一致性,但是JavaBean对象不符合这点需求。 JavaBean对象的构造过程则先是通过创建对象,随后在通过setter方法来设置必要的参数。 直到销毁前,JavaBean对象都是可变的,或者说JavaBean一直在构造过程中。 在需要一致性对象的程序使用JavaBean对象,会可能导致失败。
2、链式操作,都需要结合@Data注解
3、builder 和 Accessors
- builder创建的对象 不如 Accessors轻量
- 使用Accessors时,再使用对象copy的时候,使用org.springframework.beans.BeanUtils.copyProperties(s,t)
4、lombok注解
@Data : 注在类上,提供类的get、set、equals、hashCode、canEqual、toString方法
@AllArgsConstructor : 注在类上,提供类的全参构造
@NoArgsConstructor : 注在类上,提供类的无参构造
1.3 私有构造器,强化单例
private volatile static Cat cat; // 01.懒汉模式 + 03.对象创建过程中防止指令重排序
public static Cat getInstanceSingleton() {
if (cat == null) {
synchronized (Cat.class) { // 02.防止并发都进来
if (cat == null) {
cat = new Cat();
}
}
}
return cat;
}
volatile:防止指令重排序
* instance = new Singleton();分为三个动作
1.堆内存开辟一片空间,比如0X01
2.堆中创建Singleton,有初始化赋值的话,赋值
3.将对象内存地址返回给对象引用变量instance,instance存有0X01,instance在栈帧的局部变量表中
cpu为了提高吞吐量,会自动的改变cpu流水线,即指令的操作先后顺序改变,对单线程没有影响,多线程有
eg:
执行顺序 2,3变成让你3,2,导致返回给instance引用对象地址,虽然不为Null,但是根本没有完成赋值
导致t1执行同步代码执行结束的时候,t2判读,Instance!=null,但是instance本身没赋值;t2会直接返回instance
1.4 通过私有化构造器,强化不可实力化的能力
1、为什么私有化
- 工具类不希望被实例化,因为实例化工具类没有意义
@UtilityClass
public class DateUtil {
public Integer getInt() {
return 1;
}
}
//默认添加了,私有化的构造器
private DateUtil(){
}
别的地方无法通过new的形式创建
GsonUtil
@Slf4j
public class GsonUtil {
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
private GsonUtil() {
}
public static String toJson(Object object) {
try {
return GSON.toJson(object);
} catch (Exception e) {
log.error("序列化失败", e);
}
return StringUtils.EMPTY;
}
}
- 但不提供构造函数的时候,编译器会自动提供一个默认的构造器【这样这个工具类无法具有子类了】
1.5 避免创建不必要的对象
1、哪些方式会创建不必要的对象
-
new String、new Integer(建议使用Integr的valueOf()静态工厂方法)
-
循环中拆箱装箱
-
while循环,为了防止死循环、不断创建对象等。需要集合业务指定超过最大的循环次数
在高并发场景中,避免使用”等于”判断作为中断或退出的条件
2、包装类:占用更大的空间(但是包装类能表达 null 的语义)
正常情况下布尔值就是true和false,但是如果用户传递一个错误的skuId,那么此计算此skuId是否在灰度中,返回结果应该为null,因为true、false都不合适
交易额,异常的时候就因该为null,而非0
【推荐】自动转换(AutoBoxing)有一定成本,调用者与被调用函数间尽量使用同一类型,减少默认转换
//WRONG, sum 类型为Long, i类型为long,每次相加都需要AutoBoxing。 Long sum=0L; for( long i = 0; i < 10000; i++) { sum+=i; }
1.6 消除过期对象的引用
1、sop
-
static变量属于类,类在变量在堆中内存就一致在。如果是集合,则元素对象也不会被回收,对象链GCRoot都不会被回收
尽量在方法内部,方法结束,变量也被回收了
-
数组元素,不用则及时置为null,array[i] = null,避免内存泄漏。(参考list的remove方法:elementData[–size] = null)
1.7 关闭资源: try with resource
1、优点
-
使用正常的try-catch。在finally中关闭资源时,close方法也可以能出现异常导致资源关闭失败,所以需要再次try close方法。过于繁琐
FileInputStream fis = new FileInputStream(""); try { int read = fis.read(); } catch (IOException e) { e.printStackTrace(); } finally { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } }
2、使用前提-资源类实现了Closeable接口
- 确保了资源迅速释放,避免了资源耗尽,避免了异常和可能发生的错误
- 更简洁,更清晰
try(FileInputStream fis = new FileInputStream(new File("a"))) {
} catch (IOException e) {
}
二、对于所有对象都通用的方
2.1 equals
1、为什么要重写
- 希望类具有“逻辑相等”
2、重写了equals的类
String
public boolean equals(Object anObject) {
if (this == anObject) { //01.地址相同,则一定“逻辑”相同
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) { //02.字符串的长度不同,则一定“逻辑”不同
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) { //03.每个字符对比
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
Integer
public boolean equals(Object obj) {
if (obj instanceof Integer) { //01.不是同类型的,肯定“逻辑”不同
return value == ((Integer)obj).intValue();//02.是Integer类型的则比较值的大小 【-128,127】则相同,否则不同
}
return false;
}
1、Integer使用equals来比较值的大小
2、但是【-128,127】可以直接使用 == 来比较大小
Integer a = 127;
Integer b = 127;
System.out.println(a == b); //true
Integer c = 128;
Integer d = 128;
System.out.println(c == d); //false
3、约定规范注意事项
- 对称性:注意父子继承的设计
animal.equals(cat)//true
cat.equals(animal)//false
Cat的equals中
if(obj instanceof Cat){
//显然这里animal不是Cat类型
}
Animal的equals中
if(obj instanceof Animal){
// Cat extends Animal,所以cat 是Animal类型
}
- 传递性:
x.equals(y) 为true: 比较的是x和y的a、b的属性
y.equals(z) 为true: 比较的是y和z的b、c的属性
x.equals(z)如果比较的是x和z的a、c属性,则不能保证传递性
- 一致性:
如果x.equasl(y)中equals方法代码中涉及random随机数、时间戳等可变的元素,则无法保证equals方法每次都返回同样的值
1、sop
- 推荐使用:Objects.equals(x,y)
2.2 hashcode
1、sop
- 重写euqals,最好重写hashcode
- 重写hashCode方法时,属性之间尽量不要有关联
a = b和c属性的计算结果,则hashCode方法中,要么使用a,要么使用b和c。不要a和另外两个一起决定hash值
- 尽量使用关键属性
- hashCode方法中也不要使用随机数、日期等做逻辑
2、为什么重写hashcode
- hashCode的通用约定:equals为ture,则hashcode也要为true
- 如果不重写hashcode,则会产生下列问题:
String s1 = "majinpeng02";
String s2 = "majinpeng02";
s1.equals(s2); //true
但是hashcode没有重写,二者的hashcode值可能不一样,假如不一样。则hashmap.put(s1,1); hashmap.put(s2,1)则二者都能存入map。和我们hashmap约定的key冲突不一致了
所以,hashmap也需要重写hashcode
3、重写hashcode的作用/好处
-
符合equals相同则hashcode也相同
-
比较字符串是否为相同字符串的时候,首先可以使用hashcode值进行比较,hash值不一样则equals一定不同。如果hash值相同,再使用equals,一个一个字符进行对比判断。所以,可以提高效率
4、方法特点
- equasl相同/不同,则hashcode一定相同/不同
- hashcode相同,equals可能不同
5、String的hashcode方法:Object本身的hashcode是native方法
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
2.3慎重重写 clone方法
1、对象调用clone方法,可以跳过构造器的调用!
2、clone是浅copy,copy出来的对象和原对象指向同一块堆内存,一个修改了内存元素,会影响另一个。
3、不可变类,一定不要提供clone重写方法
2. 4 Comparable属于java.lang的接口
1、什么时候需要实现此接口
- 当一个类,想要能够被分类、排序、搜素以及用于基于比较的集合中。则应该实现Comparable接口
2、String、Integer实现此接口重写compareTo方法
挨个字符比较大小
// 挨个字符比较大小
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
//自定义compare方法,不使用减法,避免int溢出
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
public static int compare(int x, int y) {
//这里没有使用x - y,而是使用了x > y进行比较。就是防止如果y是负数,则Integer.MAX_VALUE - 一个负数,结果溢出int值
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
3、使用注意事项
- compareTo方法可以用于字典比较。但不能跨越不同类型的对象进行比较,否则classCastException
4、compareTo方法返回值含义
- String实现Comparable接口的,重写compareTo方法,则可以比较字符串“大小”, 0相同、1大于、-1小于; 同样Integer实现Comparable接口,用于比值的大小
5、区别Comparator接口
-
Comparable是类的语言特性,表明这个类具有比较、排序等功能。本质上Comparable属于内部排序,Comparator是外部排序。
-
属于java.util的接口,一般用于集合元素的排序
-
是函数式接口,只有一个方法:int compare*(T o1, T o2) :* 0相同、1大于、-1小于;
集合元素排序
//根据Dict对象的sort字段降序排序
dictList.sort(Comparator.comparing(Dict::getSort).reversed());
//根据Dict对象的sort字段升序排序
dictList.sort(Comparator.comparing(Dict::getSort));
//按照字段降序,相同的话,再按照另外一个字段降序
skuCategoryRuleDOS = skuCategoryRuleDOS.stream().
sorted(Comparator.comparing(SKUCategoryRuleDO::getSkuCategoryId).reversed().
thenComparing(SKUCategoryRuleDO::getSkuTemperatureZone,Comparator.reverseOrder())
)
.collect(Collectors.toList());
- 其中Comparator接口中用于排序的Comparator.comparing()方法,内部也是调用了Comparable接口的compareTo方法
Comparator接口中其它reversed()、thenComparing()方法,则调用了本身的compare()方法进行再排序
三、接口和类
3.1 使类和成员的可访问权限最小化(封装)
-优先考虑使用private
1、封装的好处:
- 对于外部,隐藏了内部数据和实现细节。把API和实现隔离。接触系统不同组件之间的耦合,各自可以独立开发不受影响
2、类属性为什么不建议public修饰
- 一开始要是提供的public访问修饰符,后续版本迭代等都不可以再收回权限了
- 子类中的访问级别就不允许低于超类中的访问级别
3、使用public getter、setter代替public成员变量:public类的实例,绝不能是public的
原因
public class User {
public String name;
}
1、这个类的name是可以直接被访问的,当域被访问的时候,我们将失去对这个域的控制权。后续,想要将name字段改为nickName,那么使用方就全部报错
2、通常包含public属性的类,是线程不安全的
User user = new User();
user.setName(null);
public void setName(String name) {
if (StringUtils.isBlank(name)) {
throw new NullPointerException();
}
this.name = name;
}
好处1:可以在setter方法中,对set的值进行逻辑校验
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
好处2
//好处2:可以修改属性的名称而不用关注调用方
//现在我觉得,name字段描述的不准确,换个名称为nickName,直接在本类中修改就ok,不用关注调用方
public class User {
private String nickName;
public String getName() {
return nickName;
}
public void setName(String name) {
this.nickName = name;
}
}
4、如果想要数组、集合,对象地址不可变 && 内容元素也不可变,建议使用 private final修饰,
- final:虽然引用本身不能被修改,但是它所引用的对象却可以被修改
public static final String[] ARRAY = new String[];
影响:
虽然ARRAY引用不能修改,但是ARRAY内部的元素是可以被修改的
解决:
假的final,最好private修饰[数组、集合]
//假的final,最好private修饰[数组、集合]
对象不可变了,但是元素内容还是可以被操作改变的。如果不想被外部的操作影响,则必须private
private static final Map<String,Long> map = new HashMap<>;
private static final List<Long> list = new ArrayList<>;
private static final String[] ARRAY = new String[];
可以深copy成员变量,防止改变影响成员变量值
//可以深copy成员变量,防止改变影响成员变量值
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];//02.这个value数组,在String内部被很多其它地方使用。所以,不能改变它的值
//01. 这样对toCharArray的返回结果数组进行操作,不会影响原本的value数组的元素内容
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
}
-
特殊情况:真final不可变,可以在类中提供public成员变量
private修饰和public修饰效果一样
public statci final Longsize = 200;
public statci final Integer code= 0;
public statci final String birthday = "2022-06-01";
3.2 可变性最小化-迪米特法则:即最小知道原则
-优先考虑使用final
1、不可变类特点
- 不可变类:final修饰类 / private构造器
2、final类优点 & 缺点
-
可以重复使用、共享。线程安全
安全:(1.8中DateTimeFormatter不可变,线程安全、SimpleDateFormat可变并发不安全(线程A定义的合适为:YYYYMMDD,可能被线程B改为YYYY_MM_DD))
共享:不需要进行保护性拷贝(拷贝始终等于原始的对象)。因此,不需要为不可变类提供clone方法
-
唯一缺点:每一个不同的值都需要创建一个新的对象。
3、不可变类,需要遵循的原则
-
保证类不会被继承,final无法被继承
-
既不要从外部拿,要不要返给外部
确保在该类的外部不会获取(get)到可变对象的引用、也不要从外部拿,然后set 可变对象。同时建议:使所有的域都是private final ,参考3
4、类属性是集合、数组。使用注意事项
- 数组、集合最好是private属性,如果不希望关键属性数组、集合受到外部操作的影响,则 既不要从外部获取get并赋值
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer age;
private Map<String, Long> map = new HashMap<>();
}
Map<String, Long> source = new HashMap<>();
source.put("mjp", 1L);
// 01.外部数据源source,给类关键集合map赋值
User user = new User(18, source);
Map<String, Long> target = user.getMap();
System.out.println(target);//{mjp=1}
// 02.外部数据源发生了变化
source.put("wxx", 1L);
// 03.类集合也会内容也会发生变化
System.out.println(target);//{mjp=1, wxx=1}
//正确做法
1、不要给关键数组、集合提供get、set方法。要想get则深copy返回。即不要使用@Data注解
2、建议private final
//补充
上述,基本都是针对final类。我们日常开发,还是不这样做的,所以,要清楚你提供了get、set方法则类中的集合、数组内容会收到外部数据源的内容变化而变化
- 也不要,直接返给外部。可以内部提供深copy方法,返回内容和关键属性一样的对象,但是不执向同一块内存地址。操作返回结果,不影响类本身的关键字段内容【3.1好处2】
注意⚠️:Map<String,Map<String,Long>>这种,内部的map也需要深copy一份,要不然也会影响
3.3 复合优于继承-合成复用原则
尽可能使用对象组合而不是继承的方式达到复用的目的
1、复合
@Data
@Accessors(chain = true)
public class SmallName {
private String foreverName;
@Resource
private User user;//符合添加了user类,可以使用user对象的方法
}
2、继承的缺点
-
子类只会比父类更大,权限也是,只能放大。类会越来越大。无法收缩了
特例:接口的实现类中实现的方法的修饰符,全部都是public
-
父类的方法,子类也全部拥有。有时候继承某个类,并不是想拥有此类的所有方法!
-
跨包的继承,则更加危险
-
违背了封装的原则,子类需求去了解父类的实现,否则随着不同版本父类的代码发生了改变,即使子类完全没有改变代码,也有可能被破坏
覆盖了父类的方法后,结果不符合预期:典型的:add元素的count统计
public class MyCountSet extends HashSet<Integer> { /** 统计"有史以来"向该集合中添加过的元素个数 */ private int count = 0; @Override public boolean add(Integer num) { count++; return super.add(num); } @Override public boolean addAll(Collection<? extends Integer> nums) { count += nums.size(); return super.addAll(nums); } public int getCount() { return count; } public static void main(String[] args) { MyCountSet countingSet = new MyCountSet(); countingSet.addAll(Lists.newArrayList(1)); System.out.println(countingSet.getCount());//2 } } 1、现象: addAll方法,添加了集合中1个元素,count应该是2,但是实际输出2 2、原因: 子类重写了addAll方法,但是不知道父HashSet的addAll的具体实现 父类HashSet的addAll,调用了自身的add方法。子类也重写了add方法,就导致:子类的addAll方法中count += nums.size();算了一次,同时,子类的add方法中count++又算了一次 3、执行步骤 a、子类addAll,count += nums.size();此时,count值为1 b、super.addAll,调用HashSet的addAll c、HashSet的addAll,调用了自身的add() d、HashSet的add(),被子类重写了,所以,调用子类的add e、子类的add中,执行count++;此时,count值为2 f、再调用父类add完成将元素加入 4、解决 将子类的addAll中的count += nums.size();删除
HashSet的addAll方法
public boolean addAll(Collection<? extends E> c) { boolean modified = false; for (E e : c) if (add(e)) modified = true; return modified; }
-
父类构造器绝不能直接/间接调用可被覆盖的方法,否则可能不符合预期(程序失败、npe等)
Sup
public class Super { public Super() { overrideMe ();//02-造器能调用可了被覆盖的方法❌ } public void overrideMe () { } }
Sub
public final class Sub extends Super { private final Instant instant; Sub() { super();//01. instant = Instant.now();//05.完成赋值 } @Override public void overrideMe () {//03.进入子类的重写方法 System.out.println(instant);//04.此时instant为null,还没有值!!! } public static void main(String[] args) { Sub sub = new Sub();//00.执行顺序如上 sub.overrideMe ();//06.子类方法,有值了 } }
3.4 接口优于抽象类
1、接口和抽象类的区别
-
接口,所有的方法默认是public,属性都是;类默认是default
java8-default方法
1、作用 为了扩展接口的功能。接口新增方法,如果是public的,则所有实现类都需要实现,这样就不符合向下兼容 实现类自动拥有和接口一样的default方法,直接用,不需要再实现 2、eg List extend Collection接口,此接口自己实现了removeIf方法 default boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); boolean removed = false; final Iterator<E> each = iterator(); while (each.hasNext()) { if (filter.test(each.next())) { each.remove(); removed = true; } } return removed; } 3、最好不要在接口已经存在的情况下(可以在接口第一次创建的时候添加),再添加新的default方法,这对于接口来说非常危险 eg:removeIf方法,对于大多数Collection接口接口的实现类,都没有影响,但是对于已经实现了Collection接口的org.apache.commons.collections4.collection.SynchronizedCollection 则可能存在问题。 如果客户端在SynchronizedCollection的实例上调用removeIf方法,同时另外一个线程对集合进行修改,就会导致并发修复爱Exception
-
接口中只能有常量【但是不建议,接口只提供行为规范】、抽象类中可以有成员变量
常量型接口-是接口的错误使用
1、原因 类实现常量接口,这对于这个类的用户来讲并没有实际的价值。实际上,这样做返回会让他们感到更糊涂 2、建议 使用Enum
-
抽象类单继承【但是了类可以实现多个接口】; 接口多继承接口可以同时继承B、C接口【但是接口B和接口C,不能出现冲突方法】
接口多继承注意事项
public interface BInterface {
void eat();
}
public interface CInterface {
String eat();
}
//这样A接口,就不知道eat方法具体是哪个【have unrelated return types】
public interface AInterface extends BInterface, CInterface{
}
-
接口是行为规范,具体实现逻辑由实现类自己定义 ;
抽象类定义了大部分公有的具体行为,根据不同子类定义了不同的abstract方法,由子类根据自己的特点实现即可
抽象类优于标签类
//01.标签类-eg:它能够表示圆形或者矩形 public class Figure { enum Shape { RECTANGLE,CIRCLE }; final Shape shape; double length; double width; double radius; public Figure(double redius){ shape=Shape.CIRCLE; this.radius=radius; } public Figure (double lenght, double width) { shape=Shape.RECTANGLE; this.length=lenght; this.width=width; } //计算不同形状的面积 public double area(){ switch (shape){ case RECTANGLE:return length*width; case CIRCLE:return Math.PI*(radius * radius); default:throw new AssertionError(); } } } 缺点: 违背了开闭原则 新增图形表示,求面积则需要:添加新的case;同时也有可能添加新的成员变量【梯形:(上底+下底)*高/2】 //02.抽象类 public abstract class AbstractFigure { public abstract double area(); } // 圆形子类 public class CircleFigure extends AbstractFigure { private double radius; public CircleFigure(double radius) { this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } } // 矩形子类 public class RectangleFigure extends AbstractFigure { private double length; private double width; public RectangleFigure(double length, double width) { this.length = length; this.width = width; } @Override public double area() { return length * width; } } 优点:新增图形,则新增类即可,不需要修改原本的代码,遵行开闭原则
如何决定使用抽象类还是接口
要表示is-a(圆形是一个图形、三角形是图形----)的关系,并且是为了解决代码复用的问题,就用抽象类;
表示has-a关系,并且是为了解决抽象和解耦而非代码复用的问题,那就使用接口。
2、abstract方法注意事项
- 不能+final修饰,否则子类不可以实现了
- abstract也不能private修饰,要不子类不可见了(可以protect)。abstrtact方法本身就是为了子类去实现的
3、常用接口:
Cloneable克隆、Serializable可序列化、Comparable可比较、CharSequence、Runnable可执行
4、为什么接口优于抽象类
-
存在以下场景,A类即是可以克隆的、又是可以序列化的、又是可以比较大小的、又是可以作为任务执行的。如果上述常用接口都变成了抽象类,那么由于类的单继承,所以A类就不能同时具有以上功能。
除非把上述接口变成抽象类,而且彼此之间有父子继承关系。但是A类可以只可克隆 + 可序列化,不需要可比较大小 + 可执行
5、接口和抽象类混合
A extends B抽象类 implement C接口
HashMap和TreeMap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
}
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
}
- HashMap和TreeMap都继承了AbstractMap抽象类,所以,二者都具有了AbstractMap的大部分功能【size、isEmpty】;二者分别实现不同的接口,重写不同接口的行为方法,然后具有各自的特点
- 如果需要在Map接口中新增一个批量删除deleteByIds()方法,那么Map的所有实现类都需要重写方法,否则会报错。实现类很多的话,成本很高,不具有向下兼容性
- 这个时候就可以,让AbstractMap 去实现 Map接口,然后在Map中新增方法。则仅仅需要AbstractMap去实现新的方法。对于HashMap根本不用感知。
- 以后,只要接口Map新增方法,只需要AbstractMap去实现即可
6、函数式编程
- 方法中的参数,只能是对象、值。不能是方法
- java8后,允许函数式接口作为对象,作为参数传入方法
注意⚠️:这里的对象,本质是方法即策略【按age降序、按name降序、按skuId降序等等策略】
函数式接口特点:注解 + 单个方法
@FunctionalInterface//01.注解
public interface Comparator<T> {
int compare(T o1, T o2);//02.方法
//虽然接口还有其它Object对象的方法【equals等】
//以及接口本身的default方法【java8新增】
//以及静态方法static【java9新增】
}
除了default方法、static方法、Object类的方法外,有且仅有一个方法,这种就是函数式接口
方法作为对象传入方法eg1
//01.自定义函数式接口作为方法的入参对象:方法的作用是按照User的age降序排序方法
User u1 = new User(1, "mjp");
User u2 = new User(2, "wxx");
List<User> result = Lists.newArrayList(u1, u2);
//02.自定义函数式接口
Comparator<User> myComparator = new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
return o2.getAge().intValue() - o1.getAge().intValue();//按照age降序
}
};
result.sort(myComparator);//这里的myComparator,本质就是方法,当法的作用是按照age的大小降序
//02. 函数式接口,可以使用lamda表达式
Comparator<User> myComparator = (user1, user2) -> user2.getAge() - user1.getAge();
User u1 = new User(1, "mjp");
User u2 = new User(2, "wxx");
List<User> result = Lists.newArrayList(u1, u2);
result.sort(Comparator.comparing(User::getAge)); //03.这里的sort方法的参数对着,就是方法【函数式接口】,这个方法的内容按照User的age的大小升序排序
Lists.newArrayList(new User(1,"mjp")).stream().sorted(Comparator.comparing(User::getAge)).collect(Collectors.toList());
这里的sort方法就是传入一个比较大小的方法,然后返回一个Stream流
Stream<T> sorted(Comparator<? super T> comparator);
3.5 内部类-优先使用static成员内部类
1、原因:
-
statci成员内部类属于Class类的;非static成员内部类属于对象的【必须先new出外部类对象】
static和非static
@Data
public class User {
private Integer age;
private String name;
private Emp emp;
@Data
public class Emp{
private Long skuId;
}
@Data
public static class Price{
private Double money;
}
}
@Test
public void t(){
Price price = new Price();
price.setMoney(1.0);//01.static成员内部类,可以直接new,不依赖外部类对象
User user = new User();
Emp emp = user.new Emp();//02.要想获得非static成员内部类的对象,必须先获取外部类对象
emp.setSkuId(1L);
}
-
非staic成员内部类对象,强绑定外部类对象【比如EntrySet对象和HashMap就是】,可能会影响GC
除此之外,非静态成员类的实例被创建的时候,它和外围类的关联关系也随之建立起来,这种关联关系,需要消耗非静态成员类实例的空间,并且增加构造的时间开销
Map-Entry
Entry的getKey、setValue等方法,都不需要访问Map,所以,使用非静态成员类表示Entry则会浪费
四、泛型
4.0 背景
1、泛型作用
泛型最重要的初衷之一,是用于创建集合。指定集合能持有的对象类型,并且通过编译器来强制执行该规范。
4.1 不要使用原生态类型
1、使用原生态可能存在的安全问题,因为缺少类型的检查。可能会在运行时导致异常
-
获取集合元素,并且强转时,会运行时才会报出ClassCastException异常【无法在编译时期IDEA就报出来】
原生态类型
List list = new ArrayList();
list.add(1);
String s = (String) list.get(0);
System.out.println(s);
- 方法入参,使用原生态类型
@Test
public void t() {
List<Integer> list = new ArrayList();
add(list, "java");
for (Integer item : list) {// 02.遍历集合元素,使用Integr进行强转接收时,异常
System.out.println(item);
}
}
public static void add(List list, Object obj) { //01.方法入参,没有指定泛型
list.add(obj);
}
2、带有泛型的类型,传参到无泛型方法中,尽量只读区不写
public static void add(List list) {//为了接受参数的通用性,这里没有带泛型
//可以是原生态list,但是尽量只是读取list元素,不写(add方法等)
for (Object o : list) {
System.out.println(o);
}
}
3、泛型的擦除
-
本质:运行时期,都是class java.util.ArrayList
泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,即类型擦除。类型擦除主要是为了兼容之前没有泛型特性的代码
List<Integer> l1 = new ArrayList<>(); List<String> l2 = new ArrayList<>(); Class<? extends List> aClass1 = l1.getClass(); Class<? extends List> aClass2 = l2.getClass(); System.out.println(aClass1 == aClass2);//true
而数组在运行时,Integr[]和String[]对应的不同的class
-
在编译时期,List、List无任何关系。所以,他们作为方法的入参时,可以理解为方法的重载
4.2 消除非受检异常
1、@SuppressWarnings*(“unchecked”)*范围
- 变量、方法、类:尽可能范围要小
4.3 泛型list优于数组
1、原因
-
数组是协变的
ArrayStoreException
Object[] obj 是 String[]的父类型 List<Object>不是List<String>的父类型
数组 编译时期,不会报异常。运行时会
Object[] array = new String[3]; array[0] = 1; System.out.println(array[0]);//运行时ArrayStoreException,编译时没问题
-
数组一但创建,大小不可变
2、泛型和可变参数一起使用注意事项
-
当调用可变参数时,将创建一个数组来保存参数
void foo(String... args); void foo(String[] args); // 两种方法本质上没有区别
ArrayStoreException
@Test public void t() { func("mjp","wxx"); } public static void func(String...args) { String[] strArray = args; //01.可变参数,本质是数组 Object[] objArray = strArray;//02.数组的协变的,args、strArray、objArray三者都指向同一块堆内存地址 objArray[0] = 1; //03.堆地址内元素做了改变,相当于在字符串数组中添加了整型 String arg = args[0];// 04.ArrayStoreException }
4.4 优先考虑泛型类和泛型方法
1、什么时候,使用泛型类方法
- 涉及写后,读取
- 类型还原
2、什么时候,建议直接使用Object
-
只读不写
eg:thrift中定义roc接口中BaseResponse中的数据Data
public class ThriftBaseTResponse<T> {//02.删除T
public int code = Constants.SUCCESS;
public String message;
public T data;//01.这里,set给data值后,直接返回给前端了。后续,不再有读取操作了,其实可以直接使用Object
}
3、方法入参,不限制类型时,方法返回值返回Object还是泛型
-
外部交互:返回Object。由使用方自己强转换
private final Map<Object,Object> map = Maps.newHashMap(); public Object getValueByKey(Object key) { return map.get(key); } Object obj = getValueByKey("java"); Integer u = (Integer)obj; //使用方法,自己知道key对应的value是什么类型,是Integer、String //使用自己强转错误了,ClassCastException异常会在使用方程序报出来,提供方的代码没影响 //如果内部使用这种方式,那么到处都是强转的代码,乱
-
内部使用,返回泛型【方法内部统一帮你强转换了,避免了频繁的类型转换】
private final Map<Object,Object> map = Maps.newHashMap();
public <T> T getValueByKey(Object key) {
return (T)map.get(key);
}
Integer res = getValueByKey("java");//这里就不用强转了。但是要求,内部使用方知道,key-“java”,对应的value类型
4.5 使用通配符?提高api的灵活性
1、通配符
钻石形状的 <> 符号, 所以它有时也叫作“钻石语法
-
List和List本质一样
本质上,T,E,K,V,?都是通配符,没什么区别,只不过是编码时的一种约定俗成的东西
- E:Element(元素,集合中使用,特性是枚举)
- T:Type(表示一个具体的 Java 类型)【和U一样】
- R:返回的返回类型
- K:Key(键)
- V:Value(值)
- N:Number(数值类型)
- ?:表示不确定的 Java 类型
-
泛型方法
public static <E> Set<E> union(Set<E> s1, Set<E> s2) { Set<E> result = new HashSet<>(s1); result.addAll(s2); return result; }
2、 ? extends A
则?代表A或者A的子类(类A被继承)或A的实现类(接口A被实现)
-
读(comparable 和 comparator都是读取)
-
List<? extends Number>,则?可以是Integr、Double、Long都可以
只可以读
@Test public void t() { List<Integer> l1 = Lists.newArrayList(1,2,3); List<Double> l2 = Lists.newArrayList(1.0,2.0,3.0); sum(l1); sum(l2); } private Double sum(List<? extends Number> list) { Double sum = 0.0; for (Number num : list) { sum += num.doubleValue(); } return sum; }
3、 ? super A
则?代表 A或者A的父类
- 写
private void add(List<? super Number> list, Number num) { // 这里的list,必须是List<Number>或List<Object>之类的, >=Number
list.add(num);
}
4.6 优先考虑类型安全的异构容器
1、背景
-
为了存什么类型,就可以直接取出来什么类型,不用关心类型转换且不会存在强转错误
-
map本身不限制存入的对象,用户可通过代码将k-v关联起来
private static final Map<Class<?>, Object> map = Maps.newHashMap(); public static <T> void putInstance(Class<T> aclass, T instance) { //这里可以加强校验,如果类型不一致,则throw。防止cast转换异常 map.put(aclass, aclass.cast(instance));//01.传入的Class和instance是一种类型的 } public static <T> T getInstance(Class<T> aclass) { return aclass.cast(map.get(aclass));// 02.取出来的实例一定也是这种类型的 } @Test public void t() { putInstance(User.class, new User().setName("mjp")); putInstance(Animal.class, new Animal().setColour("pink")); User user = getInstance(User.class); Animal animal = getInstance(Animal.class);//03.如果用User接收,会编译提示错误 }
2、无法保存List list这种形式。List.class编译不通过
List、List运行时期一样的class都是ArrayList
只能存、取原生态
putInstance(List.class, Lists.newArrayList(1, "a"));
List list = getInstance(List.class);
//无法->编译报错
putInstance(List<String>.class, Lists.newArrayList("a"));
List<String> list = getInstance(List<String>.class);
4.7 元组
1、背景
有时我们想通过一个函数返回两个值(比如商品价格Double、商品xiao)。
五、枚举和注解
5.0 简介枚举
@AllArgsConstructor
@Getter
public enum FlowTypeEnum{
RETURN_SUPPLIER(1, "退"),
REVERSE_ALLOCATION(2, "逆");
private final Integer val;
private final String desc;
}
-
FlowTypeEnum : 枚举类型
-
RETURN_SUPPLIER: 实例。因为实例是常量,需要需要大写
-
每个实例,如果定义了val、desc属性
(使用final修饰是常量,一旦确认则不可变可读不可写,但不能使用static修饰,因为使用了static final就必须进行初始化)
-
每个实例,都有ordinal()方法,显示该实例(常量)在枚举中的声明顺序(第一个常量顺序为0)
-
-
@AllArgsConstructor,类似于类的构造器,枚举类(1, “退供”)也需要有参构造器
-
@Getter,类似于类定义了属性后,通过getter方法获取属性值,常量RETURN_SUPPLIER也有两个属性,可以通过getter方法获取其对应的val、desc
-
enum类父类是Enum,不是Object
-
枚举有构造方法,但是无法执行newInstance反射会报错
5.1 使用枚举代替int常量
三个值形式
@AllArgsConstructor
@Getter
public enum ExecuteTypeEnum {
UNKNOW(-1, "未知", "WZ"),
REVERSE_ALLOCATE(1, "逆向调拨", "HT"),
RETURN_SUPPLY(2, "退供", "TG");
private Integer code;
private String desc;
private String orderNoPrefix;
//01.根据code获取枚举
public static Optional<XtAllocateEnum> findByIntValue(Integer value) {
return Arrays.stream(XtAllocateEnum.values())
.filter(xtAllocateEnum -> xtAllocateEnum.getIntValue().equals(value)).findFirst();
}
//02.根据desc获取code
public static Integer resolveByDesc(String desc) {
return Arrays.stream(values())
.filter(executeTypeEnum -> StringUtils.equals(desc, executeTypeEnum.getDesc())).findAny()
.map(ExecuteTypeEnum::getCode)
.orElse(-1);
}
}
//03.使用
@Test
public void t() {
Optional<ExecuteTypeEnum> optional = ExecuteTypeEnum.findByIntValue(-99);
if (optional.isPresent()) {
System.out.println(optional.get().getDesc());
} else {
System.out.println("不存在的code");
}
Integer code = ExecuteTypeEnum.resolveByDesc("哈哈");
System.out.println(code);
}
-
上述这种形式,需要在每个枚举中,都定义查询desc和code方法。可以抽取出枚举工具类
枚举工具类
// 01.定义Value接口
public interface HaveValueEnum<T> {
T getValue();
}
// 01.定义Desc接口
public interface HaveDescEnum<T> {
T getDesc();
}
//03.定义工具类
@UtilityClass
public class EnumUtils {
/**
* 根据value获取对应的枚举
*
* @param value value
* @param enumType 枚举类型class
* @param <E> 枚举类型
* @param <T> value类型
* @return 对应的枚举Optional
*/
public static <E extends Enum<E> & HaveValueEnum<T>, T> Optional<E> getEnumByValue(T value, Class<E> enumType) {
for (E item : enumType.getEnumConstants()) {
if (item.getValue().equals(value)) {
return Optional.of(item);
}
}
return Optional.empty();
}
/**
* 根据value获取对应的枚举,获取不到则抛出异常
*
* @param value value
* @param enumType 枚举类型class
* @param exceptionSupplier 异常提供者
* @param <E> 枚举类型
* @param <T> value类型
* @param <X> 异常类型
* @return 对应的枚举
* @throws X 异常
*/
public static <E extends Enum<E> & HaveValueEnum<T>, T, X extends Throwable> E getEnumByValueOrElseThrow(T value, Class<E> enumType, Supplier<? extends X> exceptionSupplier) throws X {
return getEnumByValue(value, enumType).orElseThrow(exceptionSupplier);
}
/**
* 根据value获取对应的枚举,获取不到则抛出异常
*
* @param value value
* @param enumType 枚举类型class
* @param <E> 枚举类型
* @param <T> value类型
* @return 对应的枚举
*/
public static <E extends Enum<E> & HaveValueEnum<T>, T> E getEnumByValueOrElseThrow(T value, Class<E> enumType) {
return getEnumByValueOrElseThrow(value, enumType, () -> new IllegalArgumentException("can't find " + value + " in " + enumType));
}
/**
* 根据value获取对应的枚举描述
*
* @param value value
* @param enumType 枚举类型class
* @param <E> 枚举类型
* @param <T> value类型
* @param <V> desc类型
* @return 对应的枚举描述
*/
public static <E extends Enum<E> & HaveValueEnum<T> & HaveDescEnum<V>, T, V> Optional<V> getEnumDescByValue(T value, Class<E> enumType) {
// jdk8 bug, https://bugs.openjdk.java.net/browse/JDK-8141508
// http://mail.openjdk.java.net/pipermail/compiler-dev/2015-November/009824.html
return getEnumByValue(value, enumType).map(haveDescEnum -> haveDescEnum.getDesc());
}
/**
* 根据value获取对应的枚举描述,获取不到则返回默认值
*
* @param value value
* @param defaultDesc 默认值
* @param enumType 枚举类型class
* @param <E> 枚举类型
* @param <T> value类型
* @param <V> desc类型
* @return 对应的枚举描述,获取不到则返回默认值
*/
public static <E extends Enum<E> & HaveValueEnum<T> & HaveDescEnum<V>, T, V> V getEnumDescByValueOrElseDefault(T value, V defaultDesc, Class<E> enumType) {
return getEnumDescByValue(value, enumType).orElse(defaultDesc);
}
}
//04.定义枚举,实现Value和Desc接口
@Getter
@RequiredArgsConstructor
public enum ExecuteTypeEnum implements HaveValueEnum<Integer>, HaveDescEnum<String> {
RETURN_WAREHOUSE(1,"逆向调拨"),
RETURN_SUPPLY(2,"退供"),
RETURN_WAREHOUSE_AND_SUPPLY(3,"逆向调拨+退供");
private final Integer value;
private final String desc;
}
//05.使用
String secondCategoryName = EnumUtils.getEnumDescByValueOrElseDefault(source.getSkuCategoryId(), "-", SkuSecondCategoryNameEnum.class);
5.2 使用位域表示枚举值
1、场景
枚举值,全部使用2的幂次方的形式表示。当给出7,算出7 = 1 + 2 + 4,即枚举中的LO 和 L 和 A这三种枚举组合而成
7 等效 LO && L && A,所以在db中不用存"LO && L && A",直接存7就可以了
-
Integer的MAX_VALUE最多表示2的30次方
-
Long的MAX_VALUE最多表示2的62次方【当枚举值比较多的时候,建议使用Long,但是最多也只支持2的62次方即62种不同的枚举】
枚举类型的值,都是2的幂次方
@AllArgsConstructor
@NoArgsConstructor
@Getter
public enum RuleEnum {
UNKNOW(-1L, "未知"),
LO(1L, "小于or值可修改"),
L(2L, "锁库不可更改"),
A(4L, "允许"),
NA(8L, "不允许"),
XT(16L, "自动加量允许修改");
private Long code;
private String desc;
//01.根据code获取枚举
public static Optional<RuleEnum> findByIntValue(Long value) {
return Arrays.stream(RuleEnum.values())
.filter(ruleEnum -> ruleEnum.getCode().equals(value)).findFirst();
}
//02.根据desc获取code
public static Long resolveByDesc(String desc) {
return Arrays.stream(values())
.filter(ruleEnum -> StringUtils.equals(desc, ruleEnum.getDesc())).findAny()
.map(RuleEnum::getCode)
.orElse(-1L);
}
}
判断集合整数,由哪些2的幂次方的数构成
@UtilityClass
public class BitwiseOperateUtil {
private final static List<Long> result = Lists.newArrayList();
private static Integer times = 0;
public List<Long> dividePositive2ListByBitwiseOperate(Long positive) {
bitwiseOperate(positive);
//list深度拷贝 & 拷贝后的结果可以再写
List<Long> newList = new ArrayList<>();
Collections.addAll(newList, new Long[result.size()]);
Collections.copy(newList, result);
return newList;
}
private void bitwiseOperate(Long num) {
Long splitNum;
if (num < 1) {
return;
}
if ((num & 1) == 1) {
result.add((long) Math.pow(2, times));
splitNum = (num - 1 ) >> 1;
} else {
splitNum = num >> 1;
}
times += 1;
bitwiseOperate(splitNum);
}
}
给出整数13,获得 小于or值可修改,允许,不允许 对应的枚举desc
@Test
public void t() {
List<Long> list = BitwiseOperateUtil.dividePositive2ListByBitwiseOperate(13L);
StringBuilder sb = new StringBuilder();
for (Long code : list) {
Optional<RuleEnum> optional = RuleEnum.findByIntValue(code);
if (optional.isPresent()) {
RuleEnum ruleEnum = optional.get();
String desc = ruleEnum.getDesc();
sb.append(desc);
sb.append(",");
}
}
String res = sb.substring(0, sb.length() - 1);
System.out.println(res);
}
5.3 枚举实现接口,实现可伸缩
接口
public interface Operate {
double operate(double x, double y);
}
加减枚举
public enum OperateEnum implements Operate {
ADD() {
@Override
public double operate(double x, double y) {
return x + y;
}
},
MINUS() {
@Override
public double operate(double x, double y) {
return x - y;
}
}
}
取模枚举- 不需要改变原有的OperateEnum枚举,只需要新增枚举
public enum ModOperateEnum implements Operate{
MOD() {
@Override
public double operate(double x, double y) {
return x % y;
}
}
}
@Test
public void t() {
System.out.println(OperateEnum.ADD.operate(1, 2)); //3
}
5.4 坚持使用Override注解
- 添加了 @Override 注解后,编译器能够帮助我们检查代码的错误。
- 能够让代码通熟易懂,清晰地看到哪些方法是重写方法。
5.5 反射
简介
本质
-
注解的本质是实现了Annocation接口的接口
但是注解不能使用extends继承其他注解|接口,但是接口可以继承其他接口
-
注解和任何其他Java接口一样,也会编译成类文件
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
//本质等价
public interface Override extends Annotation{
}
常见注解
-
@Override :用来声明该方法的定义会重载基类中的某个方法。如果不小心拼错了 方法名,或者使用了不恰当的签名,该注解会使编译器报错。
当你在C#中承载某个方法时,必须使用override关键字,而在Java 中,对应的@Override注解是可有可无的
-
@Deprecated :如果该元素被使用了,则编译器会发出警告。
-
@SuppressWarnings :关闭不当的编译警告。
-
@Functiona!Inteiface : Java 8引入,用于表明类型声明是函数式接口
元注解
-
RetentionPolicy:保存周期/生命周期
编译时期,直接扫描的注解:SOURCE
运行时期,通过反射进行操作:RUNTIME
在源文件中,通过反射添加一些补充信息
RetentionPolicy.SOURCE:注解会被编译器丢弃,不会写入 class 文件(不符合的话,编译时期会报错) RetentionPolicy.CLASS:在类文件中可被编译器使用。类加载阶段丢弃,会写入 class 文件【不写默认】 RetentionPolicy.RUNTIME:注解在运行时仍被虚拟机保留,因此可以通过反射读取到注解信息
-
Target:注解生效作用范围
public enum ElementType {
PARAMETRE:参数声明
TYPE: 类、接口(包括注解类型)或枚举的声明
//成员属性
FIELD,
//方法
METHOD,
//方法参数
PARAMETER,
//构造器
CONSTRUCTOR,
/** Local variable declaration */
LOCAL_VARIABLE,
///注解
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
TYPE_USE
}
- Documented
@Documented//对应Retention保存周期必须是RUNTIME
@Retention(RetentionPolicy.RUNTIME)
- Inherited:继承性
其他用于修饰注解的注解
- @Constraint
@Constraint(validatedBy = MyStatusValidatorImpl.class)
//@Constraint这个注解中的validatedBy属性含义:@MyStatus注解 和 哪个对应的实现类进行绑定
//因为javax自带的44个,通过Helper的map帮助我们绑定了,自定义的注解,则需要这种形式进行绑定
注解属性
- 必须要有默认值
int 可以使用-1表示默认值
@Retention(RetentionPolicy.RUNTIME)
public ©interface SQLInteger {
String name() default "";
Constraints constraintsO default ©Constraints(unique = true);
)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
boolean unique() default false;
}
通过反射获取注解
getAnnotation:返回指定的注解
isAnnotationPresent:判定当前元素是否被指定注解修饰
getAnnotations:返回所有的注解
getDeclaredAnnotation:返回本元素的指定注解
getDeclaredAnnotations:返回本元素的所有注解,不包含父类继承而来的
- 获取方法上注解和注解的名称
@Test
public void t() {
Class<Dog> dogClass = Dog.class;
try {
Method method = dogClass.getDeclaredMethod("func", null);
if (method.isAnnotationPresent(Hello.class)) {
Hello annotation = method.getAnnotation(Hello.class);//@com.sankuai.wos.entity.Hello(value=[go])
Class<? extends Annotation> aClass = annotation.annotationType();
String aClassName = aClass.getName();;//com.sankuai.wos.entity.Hello
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
使用
自定义注解使用
- 定义注解
@Documented
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Hello {
String value();
}
- 使用注解
@Hello("go")
public class Dog {
}
- 获取注解
@Test
public void t() {
Class<Dog> dogClass = Dog.class;
Hello proxyHandle = dogClass.getAnnotation(Hello.class);
}
如何发现注解以及其属性
通过反射获取了作用在Dog类上的@Hello注解(接口) 的代理类AnnotationInvocationHandler
- AnnotationInvocationHandler代理类handle中内容
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Map<String, Object> memberValues; //01.是个map,key就是@Hello注解的属性名称value, 值是属性名称对应的值”go“
private transient volatile Method[] memberMethods = null;
//02.代理类handle实现了@Hello注解(接口)的所有方法,对任意方法的调用,都会走到代理类handle的invoke方法中
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();// 03.注解方法名称
Class[] var5 = var2.getParameterTypes();// 04.注解方法的参数
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
byte var7 = -1;
switch(var4.hashCode()) {
case -1776922004:
if (var4.equals("toString")) { //05.注解本质是extends Annocation接口的接口,Annocation自带4个方法,判断Hello注解调用的方法是不是这4个
var7 = 0;
}
break;
case 147696667:
if (var4.equals("hashCode")) {
var7 = 1;
}
break;
case 1444986633:
if (var4.equals("annotationType")) {
var7 = 2;
}
}
switch(var7) { // 06.是这4个方法,则直接调用方法的impl实现
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4); //07.如果是@Hello注解本身的方法,eg:value()方法,则将key名称”value“,在map中对应的值"go"获取并返回
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
}
}
总结
1、先校验注解
校验注解的使用范围、保存周期等是否合理
2、反射获取注解的实现类
jvm将所有生命周期是Runtime的注解取出来,将名称和值放入map。并创建注解的代理类
3、任何对注解方法的调用,都会通过代理类的invoke,返回注解的属性值
4、根据属性值,进一步操作
参数校验注解
Validator注解校验参数
1、Validator接口和最佳实践:hibernate.validator
2、使用
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.18.Final</version>
</dependency>
3、Bean Validation
其内置constraint。除了非空检验注解外,其他注解校验(时间、大小、范围、正负、邮箱)必须在属性不为null的时候才生效,才会去校验。所以,一般这些注解都会结合@NotNull、@NotBlank、@NotEmpty一起使用
常见校验注解
@Valid
需要验证的实体是另外一个实体的属性。则需要加上这个注解
public class RuleDTO {
// 你要用我,你就要在你用的地方加这个注解
@Valid //这个集合的元素实体中的另外一个实体NetPoiInfo属性也需要验证,则需要加这个注解
@NotEmpty(message = "网店信息(netPoiInfos)不能为空")
private List<NetPoiInfo> netPoiInfos;
}
public class NetPoiInfo {
@NotNull(message = "网店ID不能为空")
@Positive(message = "网店ID不合法")
private Long netPoiId;
}
/**
* 预警阈值,0.0000-1.0000
*/
@FieldDoc(description = "预警阈值,0.0000-1.0000", example = {}, requiredness = Requiredness.REQUIRED)
@NotBlank(message = "概率阈值不能为空")
@Digits(integer = 1, fraction = 4, message = "概率阈值最多4位精度")
@DecimalMax(value = "1", message = "概率阈值不能超过1")
@DecimalMin(value = "0", message = "概率阈值不能小于0", inclusive = false)
private String warnThreshold;
@Digits(integer,fraction) 带批注的元素必须是一个在可接受范围内的数字
@Email 顾名思义
@Future 将来的日期
@FutureOrPresent 现在或将来的日期
@Max 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Min 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(200)
@Min(1)
private Integer age;//age为null,不赋值,校验不会生效。
建议:
@NotNull
@Max(200)
@Min(1)
private Integer age;//这样null 和 值范围,都会检验了
@NotNull
@Min(value = 18, message = "年龄小于{value}禁止入内")//将value的18,通过el表达式赋给message信息
private Integer age;//这样null 和 值范围,都会检验了
@Negative 带注释的元素必须是一个严格的负数(0为无效值)
@NegativeOrZero 带注释的元素必须是一个严格的负数(包含0)<=0
@NotBlank 同StringUtils.isNotBlank
@NotEmpty 同StringUtils.isNotEmpty
@NotNull 不能是Null
@Past 被注释的元素必须是一个过去的日期
@PastOrPresent 过去和现在
@Pattern 被注释的元素必须符合指定的正则表达式
@Positive 被注释的元素必须严格的正数(0为无效值)
@PositiveOrZero 被注释的元素必须严格的正数(包含0)>=0
@Szie(max,min) 带注释的元素大小必须介于指定边界(包括)之间
@DecimalMin(value) :>=
@DecimalMax(value) :<=
@Pattern(regexp = "/^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/"):正则表达式(手机号等)
@Length(min=, max=) 被注释的字符串的大小必须在指定的范围内
@Range(min=, max=) 闭区间,被注释的元素必须在合适的范围内 作用等效 @Min() + @Max()
@URL 被注释的字符串必须是一个有效的url(protocol=,host=, port=, regexp=, flags=)
@CreditCardNumber被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性
ValidatorUtil校验工具类
@UtilityClass
public class ValidateUtil {
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
public static <T> void validateParam(T param, Class<?>... groups) throws IllegalArgumentException {
if (Objects.isNull(param)) {
throw new IllegalArgumentException("参数不能为空");
}
Set<ConstraintViolation<T>> validateResult = validator.validate(param, groups);
if (CollectionUtils.isEmpty(validateResult)) {
return;
}
String validateMsg = validateResult.stream().map(ConstraintViolation::getMessage).collect(
Collectors.joining(";"));
throw new IllegalArgumentException(validateMsg);
}
}
- 使用注解
public class User{
@NotBlank(message = "姓名不能为空")
private String name;
}
- 校验
User user = 从前端页面获取的Req
ValidateUtil.validateParam(user);
@NotBlank以及校验原理
1、NotBlankValidator实现类:
- 内含有@Constraint注解
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface NotBlank {
String message() default "{javax.validation.constraints.NotBlank.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
/**
* Defines several {@code @NotBlank} constraints on the same element.
*
* @see NotBlank
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface List {
NotBlank[] value();
}
}
- @Constraint,作用在注解上 ,内含ConstraintValidator接口
@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {
Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}
- NotBlankValidator实现了ConstraintValidator接口
public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {
public NotBlankValidator() {
}
public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
if (charSequence == null) {
return false;
} else {
return charSequence.toString().trim().length() > 0;
}
}
}
2、看到注解NotBlank 如何 去找NotBlankValidator实现类
- ConstraintHelper中,有个map,将注解和实现类绑定
Map<Class<? extends Annotation>, List<ConstraintValidatorDescriptor<?>>> tmpConstraints = new HashMap();
putConstraint(tmpConstraints, NotBlank.class, NotBlankValidator.class);
putConstraints(tmpConstraints, NotEmpty.class, notEmptyValidators);
-
具体流程
- 通过spi服务提供接口,找到Validator接口的HibernateValidator厂商实现。
(关于SPI可以参考我的另外一篇文章SPI)
创建Validator接口的实现类ValidatorImpl private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();//通过工厂创建Validator接口的Hibernate实现类 通过buildDefaultValidatorFactory()->configure()->getValidationProviders() ->run() ->loadProviders( classloader ) -> ServiceLoader<ValidationProvider> loader = ServiceLoader.load( ValidationProvider.class, classloader );
3、ValidatorUtil校验工具类校验流程
@UtilityClass
public class ValidateUtil {
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
public static <T> void validateParam(T param, Class<?>... groups) throws IllegalArgumentException {
if (Objects.isNull(param)) {
throw new IllegalArgumentException("参数不能为空");
}
Set<ConstraintViolation<T>> validateResult = validator.validate(param, groups);
if (CollectionUtils.isEmpty(validateResult)) {
return;
}
String validateMsg = validateResult.stream().map(ConstraintViolation::getMessage).collect(
Collectors.joining(";"));
throw new IllegalArgumentException(validateMsg);
}
}
public class User{
@NotBlank(message = "姓名不能为空")
private String name;
}
User user = 从前端页面获取的Req
ValidateUtil.validateParam(user);
- spi: 工厂创建Validator接口对应的实现类ValidatorImpl ->
- 执行ValidateUtil方法validateParam ->
- 反射:获取param上对应的注解 (@NotBlank)【这一步原理可以参考上文:如何发现注解以及其属性】->
- ConstraintHelper:map将@NotBlank注解 和 NotBlankValidator实现类绑定 ->
- 判断是否是javax的内置44个注解,是的话则从map中拿到@NotBlank对应的NotBlankValidator实现类 ->
- 调用NotBlankValidator实现类的isValid方法
- 具体方法流程调用链
ValidateUtil.validate->
ValidatorImpl.validateInContext -> validateConstraintsForCurrentGroup -> validateConstraintsForDefaultGroup -> validateConstraintsForSingleDefaultGroupElement -> validateConstraintsForSingleDefaultGroupElement ->
MetaConstraint.validateConstraint -> doValidateConstraint -> validateConstraints -> validateConstraints ->
SimpleConstraintTree.validateConstraints -> validateSingleConstraint ->
ConstraintValidator.isValid
其中自定义注解对应的实现类,正是实现了ConstraintValidator这个接口,然后重写了isValid方法
public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {
@Override
public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
}
}
分组校验
1、场景
db中的主键id,添加的时候不需要,修改的时候不能为空。这种就需要分组校验
- 实体类
@Data
@Accessors(chain = true)
public class User {
public interface Add {} //01.定义2个接口,用于指定Group
public interface Update{}
// 02.更新操作,id不能为null
@NotNull(groups = Update.class,message = "修改User,主键id不能为null")
@Null(groups = Add.class, message = "新增user,主键id必须为null")
private Integer id;
@NotNull
@Min(value = 18, message = "未满{value}禁止入内")
private Integer age;
private String name;
}
工具类校验
User user = new User().setAge(3).setId(1);
ValidateUtil.validateParam(user, User.Add.class, Default.class);
1、工具类的validateParam方法,可以指定Group,当你是Add组,会去读取@Null注解
2、最后必须加上默认的Default组,否则,其他所有注解都不生效了,只会生效你自定义的组
级联校验
@Data
@Accessors(chain = true)
public class User {
@Valid//01.级联校验
@NotNull(message = "Dog不能为null")
private Dog dog;
}
@Data
public class Dog{
@NotBlank(message = "dogName不能为空")
private String name;
}
@Test
public void t() {
User user = new User();
Dog dog = new Dog();
user.setDog(dog); //02.这里的dog病灭有赋值name,如果不加上@Valid注解校验,则不会报错message = "dogName不能为空"
ValidateUtil.validateParam(user);
}
注解实战
自定义校验
1、用户输入的Integer status字段,只能是10、20、30三个值
- 自定义注解
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
//@Constraint这个注解中的validatedBy属性含义:@MyStatus注解 和 哪个对应的实现类进行绑定
//因为javax自带的44个,通过Helper的map帮助我们绑定了,自定义的注解,则需要这种形式进行绑定
@Constraint(validatedBy = MyStatusValidatorImpl.class) //将自定义注解 和 对应的Validator实现类绑定
public @interface MyStatus {
String message() default "用户输入status只能为10、20、30";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
- 自定义注解对应Validator实现类
public class MyStatusValidatorImpl implements ConstraintValidator<MyStatus, Integer> {
//将自定义注解 和 对应的Validator实现类绑定
//<第一个是实现类和哪个注解进行绑定, 第二个是这个注解作用在什么类型上,是作用在Integer,还是Collection(@NotEmpty)集合,还是String【@NotBlank】,还是Object【@NotNull】>
@Override
public void initialize(MyStatus constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
if (value == null) { //这里值为null,返回true校验成功的原因是:自定义注解或者javax的@Min、@Positive等注解,他们本身都不校验空,他们都认为值为null时符合校验的
//如果你想校验null或空,那就配合着@NotNull、@NotBlank、@NotEmpty一起使用。
//各个注解只定义自己的功能,不要包含@NotNull、@NotBlank、@NotEmpty的功能
return true;
}
//真正的校验逻辑
Set<Integer> set = new HashSet<>();
set.add(10);
set.add(20);
set.add(30);
return set.contains(value);
}
}
补充:
这个自定义注解逻辑处理类由于实现了ConstraintValidator接口,所以它默认被spring管理成bean
所以可以在这个逻辑处理类里面用@Autowiredu或者@Resources注入别的服务,而且不用在类上面用@Compent注解成spring的bean.
这样就可以rpc请求服务/查db获取数据,用这些数据,做复杂的用户输入校验。
适合,用户输入的数据 需要和后端交互一次后,做校验的场景
- 使用
@Data
@Accessors(chain = true)
public class User {
@NotNull
@MyStatus(message = "用户输入status只能为10、20、30")//本身不校验null的场景
private Integer status;
}
@Test
public void t() {
User user = new User();
user.setStatus(1);
ValidateUtil.validateParam(user);//用户输入status只能为10、20、30
}
2、用户输入的skuId集合最多30个元素
- 自定义注解@CollectionSizeCheck
@Constraint(validatedBy = {CollectionSizeCheckConstraintValidatorImpl.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface CollectionSizeCheck {
int value();
String message() default "集合大小不合法";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
public @interface List {
CollectionSizeCheck[] value();
}
}
- 自定义CollectionSizeCheckConstraintValidatorImpl校验类
@Slf4j
public class CollectionSizeCheckConstraintValidatorImpl implements ConstraintValidator<CollectionSizeCheck,
Collection> {
private int collectionSizeThreshold;
@Override
public void initialize(CollectionSizeCheck constraintAnnotation) {
this.collectionSizeThreshold = constraintAnnotation.value();
}
@Override
public boolean isValid(Collection value, ConstraintValidatorContext context) {
if (Objects.isNull(value)) {
return true;
}
return value.size() <= collectionSizeThreshold;
}
}
- 使用
@CollectionSizeCheck(value = 5,message = "当前用户输入的skuId集合最多{value}个")
@NotEmpty(message = "skuIds集合不能为空")
private List<Long> skuIds;
- 补充EL表达式
在注解校验的message中,使用EL表达式
@Max*(message = “年龄大小不能超过{value}”,value = 180)*
3、日期必须为yyyy-MM-dd格式且必须为T-28到T+1
- 2022-7-5“这种格式不行
- 今天是2023-11-28,则可选范围为:[11.1 - 11.29]
- 自定义注解@DateFormatCheck
@Constraint(validatedBy = {DateFormatCheckConstraintValidatorImpl.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface DateFormatCheck {
String value();
String message() default "日期格式不合法";
/**
* T-N,小于0不做校验,时间范围校验
*
* @return T-N
*/
long beforeCurrent() default -1L;
/**
* T+N,小于0不做校验,时间范围校验
*
* @return T+N
*/
long afterCurrent() default -1L;
/**
* 过去或当前时间校验,优先级高于时间范围校验
*
* @return 过去或当前时间 true
*/
boolean pastOrPresent() default false;
/**
* 未来或当前时间校验,优先级高于时间范围校验
*
* @return 未来或当前时间校验 true
*/
boolean futureOrPresent() default false;
/**
* 当前时间
*
* @return 当前时间检验
*/
boolean onlyPresent() default false;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
public @interface List {
DateFormatCheck[] value();
}
}
- 自定义DateFormatCheckConstraintValidatorImpl
@Slf4j
public class DateFormatCheckConstraintValidatorImpl implements ConstraintValidator<DateFormatCheck, String> {
private String formatDateString;
private long beforeCurrent;
private long afterCurrent;
private boolean pastOrPresent;
private boolean futureOrPresent;
private boolean onlyPresent;
@Override
public void initialize(DateFormatCheck constraintAnnotation) {
this.formatDateString = constraintAnnotation.value();
this.beforeCurrent = constraintAnnotation.beforeCurrent();
this.afterCurrent = constraintAnnotation.afterCurrent();
this.pastOrPresent = constraintAnnotation.pastOrPresent();
this.futureOrPresent = constraintAnnotation.futureOrPresent();
this.onlyPresent = constraintAnnotation.onlyPresent();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.isBlank(value)) {
return true;
}
try {
LocalDate inputLocalDate = LocalDate.parse(value, DateTimeFormatter.ofPattern(formatDateString));
LocalDate now = LocalDate.now();
if (pastOrPresent) {
// a.isBefore(a) == false
return inputLocalDate.isBefore(now.plusDays(1L));
}
if (futureOrPresent) {
// a.isAfter(a) == false
return inputLocalDate.isAfter(now.minusDays(1L));
}
if (onlyPresent) {
// t-1.23:00:00--->t:22:59:29
return isCurrentSaleDate(inputLocalDate);
}
boolean validResult = true;
if (beforeCurrent >= 0) {
// a.isAfter(a) == false
LocalDate beforeLocalDate = now.minusDays(beforeCurrent + 1L);
validResult = inputLocalDate.isAfter(beforeLocalDate);
}
if (afterCurrent >= 0) {
// a.isBefore(a) == false
LocalDate afterLocalDate = now.plusDays(afterCurrent + 1L);
validResult = validResult && inputLocalDate.isBefore(afterLocalDate);
}
return validResult;
} catch (Exception e) {
log.debug("日期格式校验不合法", e);
}
return false;
}
/**
* 22:59:59.999
*/
public static final LocalTime SELL_END_LOCAL_TIME = LocalTime.parse("22:59:59.999",
DateTimeFormatter.ofPattern("HH:mm:ss.SSS"));
/**
* 23:00:00
*/
public static final LocalTime SELL_START_LOCAL_TIME = LocalTime.parse("23:00:00", DateTimeFormatter.ISO_LOCAL_TIME);
public static boolean isCurrentSaleDate(LocalDate sellLocalDate) {
LocalTime nowLocalTime = LocalTime.now();
LocalDate nowLocalDate = LocalDate.now();
LocalDateTime sellLocalDateTime = sellLocalDate.atTime(nowLocalTime);
return sellLocalDateTime.isAfter(
nowLocalDate.minusDays(1).atTime(SELL_START_LOCAL_TIME)) && sellLocalDateTime.isBefore(
nowLocalDate.atTime(SELL_END_LOCAL_TIME));
}
}
- 使用
@NotBlank(message = "sellTime(销售时间)不能为null")
@DateFormatCheck(value = "yyyy-MM-dd", message = "sellTime(销售时间)必须为yyyy-MM-dd格式且必须为T-28到T+1",
beforeCurrent = 28L, //可以通过before和after指定时间范围
afterCurrent = 1L)//这里before和after可以不指定
@FieldDoc(description = "当前用户选择的销售时间", example = {}, requiredness = Requiredness.REQUIRED)
private String sellTime;
4、用户输入的日期必须符合日期格式(”2022-7-5“这种格式也可以)
- 自定义@DateCheck
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateCheckValidatorImpl.class)
public @interface DateCheck {
String message() default "日期格式错误";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
- 自定义DateCheckValidatorImpl
public class DateCheckValidatorImpl implements ConstraintValidator<DateCheck, String> {
private static final DateTimeFormatter PARTITION_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd", Locale.CHINA);
private static final DateTimeFormatter dateFormatter = DATE_FORMATTER.withResolverStyle(ResolverStyle.STRICT);
private static final DateValidator validator = new DateValidatorUsingDateTimeFormatter(dateFormatter);
@Override
public void initialize(DateCheck constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String date, ConstraintValidatorContext context) {
if (StringUtils.isBlank(date)) {
return true;
}
// 将2022/06/05转换为2022-06-05
if (StringUtils.isNotBlank(date) && date.contains("/")) {
date = date.replaceAll("/", "-");
}
// 格式转换,将字符串2022-6-5或者2022-6-05或者2022-06-5,转成2022-06-05
LocalDate timeLocal;
try {
timeLocal = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-M-d"));
} catch (RuntimeException e) {
return false;
}
date = PARTITION_DATE_FORMAT.format(timeLocal);
// 判断日期格式是否合规
if (!validator.isValid(date)) {
return false;
}
return true;
}
}
public interface DateValidator {
boolean isValid(String dateStr);
}
public class DateValidatorUsingDateTimeFormatter implements DateValidator {
private final DateTimeFormatter dateFormatter;
public DateValidatorUsingDateTimeFormatter(DateTimeFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}
@Override
public boolean isValid(String dateStr) {
try {
this.dateFormatter.parse(dateStr);
} catch (DateTimeParseException e) {
return false;
}
return true;
}
}
- 使用
@NotBlank(message = "日期不能为null")
@DateCheck(message = "日期格式不正确")
private String time;
- 将@DateFormatCheck注解中的值,传到DateFormatCheckConstraintValidatorImpl中,作为逻辑分支
@DateFormatCheck(value = "yyyy-MM-dd", message = "sellTime(销售时间)必须为yyyy-MM-dd格式且必须为T-28到T+1", beforeCurrent = 28L,afterCurrent = 1L)
将beforeCurrent值传递到impl中
@Constraint(validatedBy = {DateFormatCheckConstraintValidatorImpl.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface DateFormatCheck {
String value();
String message() default "日期格式不合法";
/**
* T-N,小于0不做校验,时间范围校验
*
* @return T-N
*/
long beforeCurrent() default -1L;
@Slf4j
public class DateFormatCheckConstraintValidatorImpl implements ConstraintValidator<DateFormatCheck, String> {
private long beforeCurrent;
@Override
public void initialize(DateFormatCheck constraintAnnotation) {
this.beforeCurrent = constraintAnnotation.beforeCurrent();
}
根据用户输入日期的格式,自定义校验
根据自定义校验格式,校验用户对应的输入值
定制格式校验,可以为"2023-10-23 12:00:00"、"2023-10-23 12:00:00"、"12:45:00"、"12:45"
- 注解
@Constraint(validatedBy = {CustomizeDateFormatConstraintValidatorImpl.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface CustomizeDateFormat {
/**
* 定制格式校验,可以为"2023-10-23 12:00:00"、"2023-10-23 12:00:00"、"12:45:00"、"12:45"
*/
String value();
String message() default "时间格式不正确";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
- Impl
public class CustomizeDateFormatConstraintValidatorImpl implements ConstraintValidator<CustomizeDateFormat, String> {
/**
* 注解@CustomizeDateFormat中value值
*/
private String customizeDateFormat;
@Override
public void initialize(CustomizeDateFormat constraintAnnotation) {
this.customizeDateFormat = constraintAnnotation.value();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.isBlank(value)) {
return true;
}
// 不同的customizeDateFormat,对应不同的校验
DateTimeFormatter dateFormatter = DateTimeFormatter
.ofPattern(customizeDateFormat, Locale.CHINA)
.withResolverStyle(ResolverStyle.STRICT);
DateValidator validator = new DateValidatorImpl(dateFormatter);
// 判断日期格式是否合规
if (!validator.isValid(value)) {
return false;
}
// 扩展:如果格式customizeDateFormat,是带有月和日的,validator可以对月值、日值进行校验
// 0 < 月 < 13
// 0 < 日 < 31(1、3、5、7、8、10、12是31天 ; 4、6、9、11是30天,2月闰月29天否则28天)
return true;
}
}
- 接口和实现类
public interface DateValidator {
boolean isValid(String dateStr);
}
public class DateValidatorImpl implements DateValidator {
private final DateTimeFormatter dateFormatter;
public DateValidatorImpl(DateTimeFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}
@Override
public boolean isValid(String dateStr) {
try {
this.dateFormatter.parse(dateStr);
} catch (DateTimeParseException e) {
return false;
}
return true;
}
}
- 注解使用
@CustomizeDateFormat(value = "HH:mm", message = "算法售罄加量时间有误,正确格式为{value}")
private String sellOutOrTime;
-
补充:
如果想校验输入是否为20230501这种类型,则
Preconditions.checkArgument(StringUtils.isNotBlank(triggerDate), "触发日期不能为空");
LocalDate triggerLocalDate;
try {
triggerLocalDate = LocalDate.parse(triggerDate, DateTimeFormatter.BASIC_ISO_DATE);
} catch (Exception e) {
throw new IllegalArgumentException("触发日期不合法");
}
说明:这种可以校验卡住20230229、20230230、20230431这种不合法的日期,因为DateTimeFormatter.BASIC_ISO_DATE里面写的
值为null,不生效的注解校验
原因:正常情况下我们是使用
@NotNull
@NotBlank
@NotEmpty
这些专门用来判断对象、字符串、集合非空的注解。所以,当我们自定义注解的时候,这些注解都不会去校验value是否为空,即value为空在自定义注解中默认是ok的,return true放行的(常见的现有注解@Max这些也是这个思路)
public abstract class AbstractMinValidator<T> implements ConstraintValidator<Min, T> {
protected long minValue;
public AbstractMinValidator() {}
public void initialize(Min maxValue) {this.minValue = maxValue.value();}
public boolean isValid(T value, ConstraintValidatorContext constraintValidatorContext) {
if (value == null) { //@Min注解,如果值为null,注解不会生效,直接返回true
return true;
} else {
return this.compare(value) >= 0;
}
}
protected abstract int compare(T var1);
}
字段允许为空,但非空时需要满足校验
1、背景:年龄字段可以为空,但是非空时,必须满足0-200
2、实现
- 注解
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyStatusValidatorImpl.class)
public @interface MyStatus {
String message() default "年龄0-200";
boolean nullable() default true;//默认允许为空
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
- 实现类
public class MyStatusValidatorImpl implements ConstraintValidator<MyStatus, Integer> {
private MyStatus myStatus;
@Override
public void initialize(MyStatus constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
this.myStatus = constraintAnnotation;
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
// 这里判断是否允许为空
if (value == null) {
return myStatus.nullable();
}
return value > 0 && value < 200;
}
}
3、扩展
注解可以对多个类型生效。
-
背景:
日期可以使用String saleDate接收,格式为yyyy-MM-dd
也可以使用Long saleDate接收,格式为yyyyMMdd
想实现一个注解对两种类型字段校验
-
处理
@DateFormat(message = "格式必须为yyyy-MM-dd") private String saleDate; @DateFormat(message = "格式必须为yyyyMMdd") private Long saleDate;
-
注解定义
@Constraint(validatedBy = StringDateFormatValidatorImpl.class, LongDateFormatValidatorImpl.class)
public @interface MyStatus {
}
- 实现类
自定义两个实现类,一个实现校验String类型,一个实现校验Long类型即可。实现方式同上
分布式锁注解
- 注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefundConcurrentControl {
/** 操作间隔时间,分布式锁场景就是锁的超时时间 */
int intervalTimeSeconds();
/** 并发控制键计算规则 */
String keyGenRule();//貌似必须要加#前缀,expression解析的时候需要
/** 是否需要释放,一般分布式锁场景需要释放 */
boolean needRelease();
/** 指定前缀;如果不指定前缀就是类名+方法名*/
String specifyPrefix() default "";
/** 指定错误提示;如果不指定就按系统默认值*/
String specifyErrorMsg() default "";
}
- aop
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Objects;
@Aspect
@Component
@Slf4j
public class ConcurrentControlAspect implements Ordered {
private static final String CAT_TYPE = ConcurrentControlAspect.class.getSimpleName();
private SpelExpressionParser elExpressionParser = new SpelExpressionParser();
private DefaultParameterNameDiscoverer parameterNameDiscoverer =
new DefaultParameterNameDiscoverer();
@Autowired private RefundDistributeLock distributeLock;
//默认错误提示
private static final String COMMON_ERROR_MSG = "当前操作过于频繁,请稍后再试";
// 0.生效的范围是注解,around方式(加锁 - 方法 - 解锁)环绕方式
@Around(
"@annotation(com.sankuai.grocerywms.logistics.sharedrefund.annotation.RefundConcurrentControl)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 1、根据方法注解解析接口配置信息
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
RefundConcurrentControl controlConfig = method.getAnnotation(RefundConcurrentControl.class);
// 解析参数
if (Objects.isNull(controlConfig)) {
throw new BusinessException(Constants.CONCURRENT_CONTROL_CONFIG_ERROR, "参数为空");
}
// 两次操作间隔时间
int intervalTimeSeconds = controlConfig.intervalTimeSeconds();
// 生成键规则
String specifyPrefix = controlConfig.specifyPrefix();
// 生成键规则
String keyGenRule = controlConfig.keyGenRule();
// 是否需要释放
boolean needRelease = controlConfig.needRelease();
//错误提示
String errorMsg = !Strings.isBlank(controlConfig.specifyErrorMsg()) ? controlConfig.specifyErrorMsg() : COMMON_ERROR_MSG;
// 2、计算lockKey
String lockKey = generateLockKey(pjp, specifyPrefix,keyGenRule);
// 3、加锁(aop前缀增强)
boolean lockResult = false;
try {
// 注意:这里的lock方法就是使用redis封装的加锁方法
lockResult = distributeLock.lock(lockKey, intervalTimeSeconds);
if (!lockResult) {
Cat.logEvent(CAT_TYPE, "LOCK_FAIL");
throw new BusinessException(
Constants.CONCURRENT_CONTROL_LOCK_FAIL, errorMsg);
}
// 4.执行方法本身(本前缀增强 和 后缀增强环绕)
Object result = pjp.proceed();
return result;
} catch (Exception e) {
log.warn("方法执行异常,e:{}", e.getMessage());
throw e;
} finally {
// 5、解锁(aop后缀增强)
if (needRelease && lockResult) {
// 注意:这里的unlock方法就是使用redis封装的释放锁方法
distributeLock.unlock(lockKey);
}
}
}
private String generateLockKey(ProceedingJoinPoint pjp,String specifyPrefix ,String keyGenRule) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
String methodFullName = pjp.getTarget().getClass().getSimpleName() + method.getName();
//1、lockKey前缀;用户不指定前缀则默认为类名+方法名
String prefix = !Strings.isBlank(specifyPrefix) ? specifyPrefix : methodFullName;
Object[] args = pjp.getArgs();
String[] paramNames = parameterNameDiscoverer.getParameterNames(method);
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i], args[i]);//key: 方法参数名称,val方法入参参数对象本身(含字段值)。方法参数可能多个,故循环添加
}
Expression expression = elExpressionParser.parseExpression(keyGenRule);//el表达式解析"#内容"
// 2、方法名-参数解析结果
return prefix + "-" + expression.getValue(context).toString();//等效map.get(key),context为map,key是el表达式
}
@Override
public int getOrder() {
//5、配置为最小值 在事务切面之前执行
return Integer.MIN_VALUE;
}
}
- 注解的使用1: key直接使用方法入参某个字段
@RefundConcurrentControl(
intervalTimeSeconds = 3,
keyGenRule = "#operator",//类似于你要加锁的key(misId、orderNo、taskCode等)
needRelease = true,
specifyPrefix = "WriteShippingTaskDetail",//描述你加锁操作的目的,具体是对什么操作(创建退货单、导出加量日志等)
specifyErrorMsg = "该单正在操作中,请稍后重试操作")
public void confirmPDAShippingTaskDetail(ConfirmShippingTaskDetailRequest request,String operator) {
}
- 注解的使用2: key间接使用方法入参request中,某个字段名称
@RefundConcurrentControl(
specifyPrefix = "changePdaPickingTaskTaker",
intervalTimeSeconds = 10,
needRelease = true,
specifyErrorMsg = "正在更改执行人,请勿重复操作",
keyGenRule = "#request.newOperator") //ChangeOperatorTRequest request中的newOperator字段作为key
@Transactional(rollbackFor = Exception.class)
public void changeOperator(ChangeOperatorTRequest request) {
}
- 注解的使用3: key间接使用方法入参request中,某2个字段组合
@RefundConcurrentControl(
specifyPrefix = "OperateRDCPickingSkuDetail",
intervalTimeSeconds = 2,
needRelease = true,
specifyErrorMsg = "重复性互斥提交,请稍后重试",
keyGenRule = "#request.pickingTaskNo + '-' + #request.pickingTaskSkuDetailId")//SubmitPickingTaskDetailRequest request中的两个字段拼接成key
public SubmitPickingTaskDetailResponse submitPickingTaskDetail(long poiId, SubmitPickingTaskDetailRequest request) {
}
六、方法
6.1 检查参数的有效性
1、不要相信前端的入参
2、不要相信依赖接口的返回值非空、值符合预期
6.2 必要时进行保护性拷贝
User
@Data
@NoArgsConstructor
public class User {
private String name;
private Date inBirthday;
public void setInBirthday(Date outBirthday) {
this.inBirthday = outBirthday;
}
public Date getInBirthday() {
return this.inBirthday;
}
}
@Test
public void t() {
Date outBirthday = Date.valueOf("2022-08-16");
User user = new User();
user.setName("mjp");
user.setInBirthday(outBirthday);
outBirthday.setTime(1660747569532L);//2022-08-17
System.out.println(user.getInBirthday());//0817
}
1、outBirthday是0816,通过set方法,设置给inBirthday,二者都执行同一块内存地址0X01
2、outBirthday重新设置为0817了,0X01地址对应的值变为0817,所以inBirthday也是0817了
3、get方法,return的是inBirthday,自然就是0817了
- 本质原因是:属性是非基本类型。外部传递的对象和属性对象都指向了同一块内存地址。一个改变了地址的内容,则属性对应地址的内容也改变了
- 解法一:Date属性,使用Long基本类型代替。传入属性和对象属性不再公用一块堆内存
@Test
public void t() {
long outBirthday = 1660661169000L;//0816
String name = "mjp";
User user = new User();
user.setName(name);
user.setInBirthday(outBirthday);
outBirthday = 1660747569532L;//2022-08-17
System.out.println(user.getInBirthday());//还是0816
}
解法二:保护性拷贝[参考3.1和3.2]
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];//02.这个value数组,在String内部被很多其它地方使用。所以,不能改变它的值
//01. 这样对toCharArray的返回结果数组进行操作,不会影响原本的value数组的元素内容
//其实这里的toCharArray方法,就是getXxx方法。内部进行了保护性拷贝,没有直接将value数组对象返回出去,而是新创建一块内存地址返回出去
//对新地址对应的内容进行操作,不会影响value对应内存地址的值
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
}
6.3 谨慎设计方法签名
1、方法参数尽量 <=4,超过了建议使用类代替
2、对于boolean参数,优先使用两个元素的枚举类型来表示。使得代码更易于阅读和编写以及后续扩展。
6.4 谨慎使用重载
1、重载方法的选择是在编译时期就确定的,而非运行时期确定的
@Test
public void t() {
String s = "mjp";
Object obj = s;
doSomeThing(obj);
}
private void doSomeThing(Object obj) {//运行时是字符串,但是编译时是Object,所以走这个方法。
//把这个方法删除会走下面重载方法。若二者的业务逻辑不一致,则有可能造成调用结果不符合预期
System.out.println("obj");
}
private void doSomeThing(String s) {
System.out.println("s");
}
@Test
public void t() {
Collection<?> coll = new ArrayList<>();
doSomeThing(coll);
}
private void doSomeThing(Collection<?> coll) {//编译时期是Collection类型,走这个方法
System.out.println("coll");
}
private void doSomeThing(List<?> list) {
System.out.println("list");
}
2、方法重载可能存在的问题
- 方法提供方,删除了某个重载方法,使用方可能会自动使用另外一个重载方法
//A类中调用B类的重载方法doSomeThing,默认是调用方法1
//如果哪天,B类中方法1被删除了,则会走方法2。若方法2和1业务逻辑不一致,则有可能造成调用结果不符合预期(但是不会报错)
String s = "mjp";
Object obj = s;
doSomeThing(obj);
//B类
//方法1
private void doSomeThing(Object obj) {//运行时是字符串,但是编译时是Object,所以默认走这个方法。
System.out.println("obj");
}
//方法2
private void doSomeThing(String s) {
System.out.println("s");
}
-
方法报错
删除集合中的某个元素
@Test
public void t() {
List<String> strList = Lists.newArrayList("mjp","wxx");
List<Integer> list = Lists.newArrayList(18,23);
removeEle(strList, "mjp");
removeEle(list, 18);
}
private void removeEle(List<String> strList, String ele) {
if (strList.contains(ele)) {
strList.remove(ele);
}
}
private void removeEle(List<Integer> list, Integer ele) {
if (list.contains(ele)) {
list.remove(ele);
}
}
private void removeEle(List<Integer> list, int ele) {
if (list.contains(ele)) {
list.remove(ele);//这里是删除指定下标元素,remove(int index),会报数组越界异常java.lang.IndexOutOfBoundsException: Index: 18, Size: 2
}
}
因为会自动装箱,所以int ele重载方法在if判断时候等效list.contains(Integer.valueOf(ele));
list.remove(int index):删除指定下标的元素
list.remove(Object obj):删除指定元素
3、SOP
当方法背后的逻辑一致时,才应该使用重载。
eg:PC、小程序,web、h5不同端传递的方法入参类型不一样,但是业务逻辑都一样。这样就可以提供重载方法
6.5 谨慎使用可变参数
1、可变参数特点
- 本质是数组。String…args对应String数组;Integer…args对应Integer数组
- 可传>=0个参数。不传默认是空数组而非null
- 只能方法方法的最后。所以,方法最多只能有一个可变参数
2、可能存在的问题
- 频繁的生成数组,可能存在性能问题。可以使用方法重载,多个参数替代
public void foo() {}
public void foo(int a1) {}
public void foo(int a1, int a2) {}
public void foo(int a1, int a2, int a3) {}
public void foo(int a1, int a2, int a3, int... rest) {}
6.6 返回零长度的集合,而非null
1、可以返回长度为0的集合
return Collections.emptyList();
2、循环遍历查询,没有数据可以返回集合
return Lists.newArrayList()
6.7 Optional
1、Optional简介
点击展开内容
- Optional 类的引入很好的解决空指针异常。
- Optional 是个容器:它可以保存类型T的值,或者仅仅保存null;Optinal类本质上是一个只能存放一个元素的不可变集合
- Optional.empty ()返回一个空的optional;Optional.of(value)返回一个包含了指定非null值的optional
2、Optional使用
点击展开内容
方法名称 | 作用 | eg | 备注 |
---|---|---|---|
empty() | 返回空的 Optional 实例 | Optional*<Integer>* optional = Optional.empty();//Optional.empty | Optional集合中有一个为Null的元素,则ifPresent返回false |
ifPresent | 值存在则方法会返回true | Optional*<Integer>* optional2 = Optional.ofNullable(1);//Optional[1] | Optional集合中有一个不为Null的元素1,则ifPresent返回true |
ofNullable(T value) | 如果为非空,返回 Optional 描述的指定值,否则返回空的 Optional | Optional*<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.empty | Optional*<Integer>* optional = Optional.ofNullable(3);Optional*<String>* result = optional.map*(null)*; 报错npe |
orElse**(T other)** | 如果存在该值,返回值, 否则返回 other。 | Optional*<Integer>* optional = Optional.ofNullable(1);//Optional[3] Integer result = optional.orElse*(23); System.out.println(result);//1 Optional<Integer>* optional1 = Optional.empty(); Integer result1 = optional1.orElse*(23); System.out.println(result1)*;//23 |
优雅的取值
dto.setSupplierId(
Optional.ofNullable(source.getVendorDTO()).map(VendorDTO::getVendorId).orElse(null)
);
3、Optional注意事项:
- 不要给 Optional 变量赋值 null,否则违背了Optional的初衷
Optional<Integer> optional = Optional.empty();
Optional<String> result = optional.map(Integer::toBinaryString);
System.out.println(result);//Optional.empty
Optional<Integer> optional = null;
Optional<String> result = optional.map(Integer::toBinaryString);
System.out.println(result);//npe
七、代码的艺术:Don’t make me think
一眼看过去,如果无法看清逻辑,这不是好代码
好的代码不需要你思考太多
一定记住:代码更是写给别人看的
一流代码的特性
• 高效 (Fast)
• 鲁棒 (Solid and Robust)
• 简洁 (Maintainable and Simple)
• 简短 (Small)
• 可测试 (Testable)
• 可移植 (Portable)
• 可监控 (Monitorable)
• 可扩展(Scalable & Extensible):功能的单一是复用和扩展的基础
7.1 把信息装进名字
1、使用专业的名词代替空洞的名次(maxAge而非age、height而非size、distribute而非send、compute而非get)
2、有单位的,需要带上单位:hex、Ms、Min、Secs、MB、CM
7.2 不要使用让人产生误解的名称
1、boolean的变量名称不要使用反义词:dis、not(disLock)
2、在定义类的属性xxx是boolean类型时,不建议属性名为isXXX
原因:isXXX自动生成的getter方法 ,方法名称就是isXXX。
常见的序列化反序列化工具:
-
只有Gson是通过反射遍历获取到属性,然后将其值进行序列化,
-
fastJson和JackJson(SpringBoot集成了jackson,默认使用jackson来进行json序列化)是反射遍历获取对象的getter方法
二者对属性赋值时,属性名称被解析为:
- 正常情况下: skuId属性,对应get方法为getSkuId,属性名称解析为去掉get,首字母小写,skuId。 和属性名一致
- 属性为boolean isNeedGood基本类时,对应的get方法为isNeedGood,属性名称解析为去掉is,首字符小写,needGood。和属性名称不一致了,这样序列化赋值就失败了
- 属性为Boolean isNeedGood包装类时,默认的get方法为getNeedGood,属性名称解析为去掉get,首字符小写,needGood。和属性名称不一致
public class Mjp {
private boolean isNeedGood;
private Long skuId;
//isNeedGood属性对应的get方法为getNeedGood,会把is吃掉。
//正常情况下没有什么影响,但是在json序列化的时候,对于is开头的方法,会默认(即isNeedGood去掉is,然后第一个字母小写)needGood
//这样在序列化的时候,希望是将true赋值给isNeedGood,但是实际情况是 “needGood”:true,显然没有needGood属性,这么一来,isNeedGood就未被赋值了
public boolean isNeedGood() {
return isNeedGood;
}
public Long getSkuId() {
return skuId;
}
}
public class Demo {
private Boolean isNeedMater;
public Boolean getNeedMater() {
return isNeedMater;
}
public void setNeedMater(Boolean needMater) {
isNeedMater = needMater;
}
}
Demo demo = new Demo();
demo.setNeedMater(Boolean.TRUE);
System.out.println(GsonUtil.toJsonStr(demo)); //{"isNeedMater":true}
System.out.println(new ObjectMapper().writeValueAsString((demo)));//{"needMater":true}
System.out.println(JSON.toJSONString(demo));//{"needMater":true}
这里设置isNeedMater为true,当使用fastJson进行序列化后,再通过Gson进行反序列化,结果就会出问题。
本来给isNeedMater赋值的是true,但是反序列化以后的结果是false
public class Demo {
private boolean isNeedMater;
public boolean getNeedMater() {
return isNeedMater;
}
public void setNeedMater(boolean needMater) {
isNeedMater = needMater;
}
@Override
public String toString() {
return "Demo{" +
"isNeedMater=" + isNeedMater +
'}';
}
}
Demo demo = new Demo();
demo.setNeedMater(Boolean.TRUE);
System.out.println(GsonUtil.fromJson(JSON.toJSONString(demo), Demo.class));//Demo{isNeedMater=false}
fastJson通过反射遍历找到属性isNeedMater对应的getter方法,解析认为这个类的属性是needMater,然后获取其值,将其序列化为{“needMater”,true}
然后Gson解析字符串,通过needMater找该类的属性,结果发现该类就一个属性isNeedMater,没有needMater属性。
因此Gson反序列化后isNeedMater会使用其默认值false。同理如果Boolean isNeedMater则为Demo{isNeedMater=null}
解决方式:
1、布尔类型的属性名,不建议为isXXX
2、人为使用@Data注解,注解帮忙生成getter方法,因为其生成的方法名为:getIsNeedMater
@Data : 注在类上,提供类的get、set、equals、hashCode、canEqual、toString方法
@Data
public class Demo {
private Boolean isNeedMater;
}
@Test
public void t() {
Demo demo = new Demo();
demo.setIsNeedMater(Boolean.TRUE);
demo.getIsNeedMater();//这里的getter方法名称为:getIsNeedMater
System.out.println(GsonUtil.fromJson(JSON.toJSONString(demo), Demo.class));//Demo(isNeedMater=true)
}
补充:Boolean|boolean isXxx命名的理解
1、什么场景下,这样命名会有问题
- fastJson和JackJson在序列化和反序列化时可能会有问题(有些框架集成了相应工具,eg:SpringBoot集成了jackson,默认使用jackson来进行json序列化)
2、出现问题的原因
-
正常情况下: skuId属性,对应get方法为getSkuId,属性名称解析为去掉get,首字母小写,skuId。 和属性名一致
-
属性为boolean isNeedGood基本类时,对应的get方法为isNeedGood,属性名称解析为去掉is,首字符小写,needGood。和属性名称不一致
-
属性为Boolean isNeedGood包装类时,默认的get方法为getNeedGood,属性名称解析为去掉get,首字符小写,needGood。和属性名称不一致
-
fastJson、jackson在序列化和反序列化时,是通过反射遍历找到属性isNeedMater对应的getter方法,通过get方法解析得到对应属性名称。
认为这个类的属性名称为needMater即{“needMater”,true},我们期望的是{“isNeedMater”,true}
补充:使用Gson序列化和反序列化时,不会存在上述问题:Gson是通过反射遍历直接获取到属性(不是通过解析get方法名称),对其进行序列化和反序列化
public class Demo {
private Boolean isNeedMater;
public Boolean getNeedMater() {
return isNeedMater;
}
}
Demo demo = new Demo();
demo.setNeedMater(Boolean.TRUE);
//Gson
System.out.println(GsonUtil.toJsonStr(demo)); //{"isNeedMater":true}
//jackSon
System.out.println(new ObjectMapper().writeValueAsString((demo)));//{"needMater":true}
//fastJson
System.out.println(JSON.toJSONString(demo));//{"needMater":true}
3、问题复现
Boolean isNeedMater属性,使用set方法赋值后,使用fastJson序列化,再使用Gson进行反序列化,得到的属性isNeedMater无值
public class Demo {
private boolean isNeedMater;
public boolean getNeedMater() {
return isNeedMater;
}
public void setNeedMater(boolean needMater) {
isNeedMater = needMater;
}
@Override
public String toString() {
return "Demo{" +
"isNeedMater=" + isNeedMater +
'}';
}
}
Demo demo = new Demo();
demo.setNeedMater(Boolean.TRUE);
System.out.println(GsonUtil.fromJson(JSON.toJSONString(demo), Demo.class));//Demo{isNeedMater=false}
如果是Boolean isNeedMater,同理Demo{isNeedMater=null},isNeedMater都没值
4、如何解
-
方式1:布尔类型属性,不要以is开头命名
-
方式2:使用lombok的@Data注解,代替get、set方法
@Data public class Demo { private Boolean isNeedMater; } @Test public void t() { Demo demo = new Demo(); demo.setIsNeedMater(Boolean.TRUE); //demo.getIsNeedMater();//这里的getter方法名称为:getIsNeedMater System.out.println(GsonUtil.fromJson(JSON.toJSONString(demo), Demo.class));//Demo(isNeedMater=true) }
5、mthrift这样命名会有问题么
- 美团的rpc是thfirt,使用protocol进行序列化和反序列化
namespace java com.sankuai.groceryscm.vmi.client.thrift
struct User{
1: i32 id= 0;
2: required string name;
3: bool isNeedMaster;
}
@Test
public void new_test(){
byte[] bytes = serial();
System.out.println("序列化以后的对象:" + Arrays.toString(bytes));
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
parse(bis);
}
/**
* 序列化方法
*/
private static byte[] serial() {
User user = new User();
user.setId(100);
user.setName("sss");
user.setIsNeedMaster(true);
System.out.println("序列化之前的对象:" + user);
// 序列化
ByteArrayOutputStream out = new ByteArrayOutputStream();
TTransport transport = new TIOStreamTransport(out);
TBinaryProtocol tp = new TBinaryProtocol(transport);//二进制编码格式进行数据传输
// TCompactProtocol tp = new TCompactProtocol (transport);
try {
user.write(tp);
} catch (TException e) {
e.printStackTrace();
}
byte[] buf = out.toByteArray();
return buf;
}
/**
* 反序列化方法
* @param bis
*/
private static void parse(ByteArrayInputStream bis) {
User user = new User();
TTransport transport = new TIOStreamTransport(bis);
TBinaryProtocol tp = new TBinaryProtocol(transport);
// TCompactProtocol tp = new TCompactProtocol(transport);
try {
user.read(tp);
System.out.println("反序列化后的对象:" + user);
} catch (TException e) {
e.printStackTrace();
}
}
序列化之前的对象:User(id:100, name:sss, isNeedMaster:true)
序列化以后的对象:[8, 0, 1, 0, 0, 0, 100, 11, 0, 2, 0, 0, 0, 3, 115, 115, 115, 2, 0, 3, 1, 0]
反序列化后的对象:User(id:100, name:sss, isNeedMaster:true)
- 所以,我们rpc这样命名不会存在问题。
7.3 审美
1、对齐:注释参数、变量
2、相似的代码,格式要一样(注释要么都在一行,要么都在末尾)
3、使用空行将大段代码分为逻辑上的”段落“(处理req的、处理resp的)
7.4 好的注释
1、好的名字 > 坏的名字 + 好的注释
2、想到什么先记录下来 -> 改进一下 -> 不断改进
3、在读者的立场思考
4、Map<Map<>>注释 k1 -> (k2,v2)
5、 描述方法的业务行为,而非代码行为
6、可适当加入输入输出的example
7.5 更易于阅读的代码
1、if优先处理正向逻辑
2、do while -> while
3、提前return可以让代码更整洁
4、if里面判断条件如果过于复杂,要抽取出一个函数或者临时变量
5、if正向逻辑过于复杂的时,可以考虑反方向
7.6 变量可读性
1、 while控制变量可以抽取为boolean方法,提前return
2、在第一次使用的时候再定义变量
3、避免一个操作的局部变量出现在另一个操作方法中
7.7 抽取无关的代码,方法职责单一
1、 切分模块的一种角度
• 计算数据方法(数据为中心,面向对象面向数据)
• 过程方法
八、通用编程
8.1 优先使用增强For循环
1、三种场景下只能使用普通For循环
- 边遍历边删除【不要使用增强For进行】
List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);
for (int i = 0; i < list.size(); i++) {
list.removeIf(integer -> integer > 3);
}
//这里也可以使用迭代器进行边遍历边删除
List<Integer> list2 = Lists.newArrayList(1, 2, 3, 4, 5);
Iterator<Integer> iterator = list2.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
if (next > 3) {
iterator.remove();
}
}
System.out.println(list2);
- 转换:遍历的时候将指定索引下的元素换成其它值
- 平行迭代:使用索引下标,使得list1和list2中元素可以同步前进
Map<Integer, String> map = Maps.newHashMap();
List<Integer> list1 = Lists.newArrayList(1, 2, 3);
List<String> list2 = Lists.newArrayList("mjp","xyz","cc");
for (int i = 0; i < list1.size(); i++) {
Integer age = list1.get(i);
String name = list2.get(i);
map.put(age, name);
}
8.2 了解和使用类库
1、java8的LongAdder在高并发下优于AutomicLong
AtomicLong atomicInteger = new AtomicLong(0L);
long i = atomicInteger.addAndGet(1L);
System.out.println(i);
LongAdder adder = new LongAdder();
adder.add(7L);
System.out.println(adder);
8.3 需要精确,不要使用double
1、缺点:金钱类的不要使用double回丢失精度
2、替代
- 使用long,单位为分【推荐】
- 使用BigDecimal,但是初始化小数的时候,只能用字符串,若使用double(0.1)初始化的时候就丢失了精度
double a = 1.0;
double b = 0.9;
double c = a - b;
System.out.println(c);//0.09999999999999998
BigDecimal b1 = new BigDecimal(1.0);
BigDecimal b2 = new BigDecimal(0.9);
BigDecimal subtract = b1.subtract(b2);
System.out.println(subtract);//0.09999999999999997779553950749686919152736663818359375
BigDecimal b3 = new BigDecimal("1.0");
BigDecimal b4 = new BigDecimal("0.1");
BigDecimal subtract1 = b3.subtract(b4);
System.out.println(subtract1);//0.9
8.4 字符串连接
1、s1 + s2 + s3会被自动优化为sb.append(s1).append(s2).append(s3).toString()
由于字符串的不可变性,连接 n 个字符串重复使用字符串连接操作,需要 n2 的时间。
sb 对象内部维护一个字符数组。操作都是在字符数组上进行,append 方法的时间是线性的
2、字符串不适合替代其他值类型,数据本质上确实是文本信息时,使用字符串才合理
3、参考:https://www.cnblogs.com/frankyou/p/9828555.html 和 唯品会的工具类https://github.com/vipshop/vjtools/blob/master/vjkit/src/main/java/com/vip/vjtools/vjkit/text/StringBuilderHolder.java
九、异常处理
1、不要在 finally 块中使用 return(说明:finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句)
2、sop
-
可以使用warn日志级别来记录用户输入参数错误的情况。如非必要,请不要在此场景打出 error 级别,避免频繁报警(说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息)
-
Business_error和interalError的区别:interal异常主要是一些无法预料的原因导致的rpc失败,比如网络抖动超时等。
-
catch匹配到异常后,会把异常吃掉。如果你在catch中打了相关信息,没有再向上抛出异常,则异常就在此处被吃掉了。如果是@Trasactional注解,异常就不能被吃掉,就需要在catch中再向上throw,这样事物才能一致。
-
调用者为前端的时候,如果你不想让前端在调用时抛出红色异常。那么你就不在最外层catch中再次throw一个异常,而是吃掉这个异常,并且给出相应的code值和message即可。打出error日志即可
1、不要忽略捕捉的异常
catch (NoSuchMethodException e) {
return null;
}
虽然捕捉了异常但是却没有做任何处理,除非你确信这个异常可以忽略,不然不应该这样做。这样会导致外面无法知晓该方法发生了错误,无法确定定位错误原因。
2、在你的方法里抛出定义具体的检查性异常
public void foo() throws Exception { //错误方式
}
推荐:
public void foo() throws SpecificException1, SpecificException2 { //正确方式
}
3、捕获具体的子类而不是捕获 Exception 类
try {
someMethod();
} catch (Exception e) { //错误方式
LOGGER.error("method has failed", e);
}
推荐:
try {
rpc();
} catch (TException e) {
LOGGER.error("method has failed", e);
}
4、始终正确包装自定义异常中的异常,以便堆栈跟踪不会丢失
catch (NoSuchMethodException e) {
throw new MyServiceException("Some information: " + e.getMessage()); //错误方式
}
推荐:
catch (NoSuchMethodException e) {
throw new MyServiceException("Some information: " , e); //正确方式
}
5、要么记录异常要么抛出异常,但不要一起执行
catch (NoSuchMethodException e) {
//错误方式
LOGGER.error("Some information", e);
throw e;
}
正如上面的代码中,记录和抛出异常会在日志文件中产生多条日志消息,代码中存在单个问题,并且对尝试分析日志的同事很不友好。
6、finally 块中永远不要抛出任何异常
7、始终只捕获实际可处理的异常
catch (NoSuchMethodException e) {
throw e; //避免这种情况,因为它没有任何帮助
}
不要为了捕捉异常而捕捉,只有在想要处理异常时才捕捉异常,或者希望在该异常中提供其他上下文信息。如果你不能在 catch 块中处理它,那么最好的建议就是不要只为了重新抛出它而捕获它。
8、不要使用 printStackTrace() 语句或类似的方法
最终别人可能会得到这些堆栈,并且对于如何处理它完全没有任何方法,因为它不会附加任何上下文信息。
9、记住早 throw 晚 catch 原则
应该尽快抛出(throw)异常,并尽可能晚地捕获(catch)它。应该等到有足够的信息来妥善处理它。
10、在异常处理后清理资源
则仍应使用 try-finally 块来清理资源。 在 try 模块里面访问资源,在 finally 里面最后关闭资源。即使在访问资源时发生任何异常,资源也会优雅地关闭。
11、尽早验证用户输入以在请求处理的早期捕获异常
12、一个异常只能包含在一个日志中,在日志文件中这两个日志消息可能会间隔 100 多行。应该这样做:
LOGGER.debug("Using cache sector A");
LOGGER.debug("Using retry sector B");
推荐:
LOGGER.debug("Using cache sector A, using retry sector B");
13、编写多重catch语句块注意事项:顺序问题:先小后大,即先子类后父类
否则,捕获底层异常类的catch子句将可能会被屏蔽。
14、多个异常的处理逻辑一致时,使用JDK7的语法避免重复代码
try {
...
} catch (AException | BException | CException ex) {
handleException(ex);
}
15、异常处理不能吞掉原异常,要么在日志打印,要么在重新抛出的异常里包含原异常
catch(XxxException e){
//WRONG
throw new MyException("message");
//RIGHT 记录日志后抛出新异常,向上次调用者屏蔽底层异常
logger.error("message", ex);
throw new MyException("message");
//RIGHT 传递底层异常
throw new MyException("message", ex);
}
16、如果处理过程中有抛出异常的可能,也要做try-catch,否则finally块中抛出的异常,将代替try块中抛出的异常
//WRONG
try {
...
throw new TimeoutException();
} finally {
file.close();//如果file.close()抛出IOException, 将代替TimeoutException
}
//RIGHT, 在finally块中try-catch
try {
...
throw new TimeoutException();
} finally {
IOUtil.closeQuietly(file); //该方法中对所有异常进行了捕获
}
17、不能在finally块中使用return,finally块中的return将代替try块中的return及throw Exception
//WRONG
try {
...
return 1;
} finally {
return 2; //实际return 2 而不是1
}
try {
...
throw TimeoutException();
} finally {
return 2; //实际return 2 而不是TimeoutException
}
十、并发
1、同步访问可变数据
-
若共享的可变数据只需要可见,则使用Volatile即可(不提供互斥)。多线程要注意互斥,正常情况下需要使用同步、锁
-
对字符串加锁,为了互斥性,需要使用synchronized(s.intern())
因为字符串常量池和堆内存中,地址不一样,不互斥。加上intern()就互斥了
2、避免过度同步
并发集合,代替使用锁
- CopyOnWriteArrayList,适合读多写少
- ConcurrentHashMap,若一致性,略有数据同步延时
同步区域内少执行任务,计算工作最好放在锁外部
- 获得锁
- 检查共享数据
- 操作数据
- 释放锁
3、优先使用线程池而非new Thread
- 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式
Executors 返回的线程池对象的弊端如下:
\1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
\2) CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM
4、在高并发场景中,避免使用”等于”判断作为中断或退出的条件(说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间 判断条件来代替)
4、并发工具(CountDownLatch)优于wait notify
Map | Key | Value |
---|---|---|
HashMap | Nullable | Nullable |
ConcurrentHashMap | NotNull | NotNull |
TreeMap | NotNull | Nullable |
Executor 框架;并发集合;同步器:CountDownLatch
@Test
public void t() throws InterruptedException {
// 01.创建门栓
int threadCount = 5;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
AtomicInteger atomicInteger = new AtomicInteger(0);
// 02.创建线程执行
Integer baseScore = 10000;
Random random = new Random();
List<CompletableFuture> cfList = new ArrayList<>();
for (int i = 1; i < threadCount+1; i++) {
int finalI = i;
CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
int score = baseScore + random.nextInt(2000);
atomicInteger.addAndGet(score);
System.out.println("第" + finalI + "个运动员的成绩:" + score + "");
countDownLatch.countDown();
});
cfList.add(cf);
}
cfList.forEach(CompletableFuture::join);
// 03.解开
countDownLatch.await();
System.out.println(atomicInteger.get() / threadCount);
}
countDownLatch-执行先后顺序【可实现分布式锁】
@Test
public void t() throws InterruptedException{
CountDownLatch u1 = new CountDownLatch(1);
CountDownLatch u2 = new CountDownLatch(1);
CountDownLatch u3 = new CountDownLatch(1);
CountDownLatch u4 = new CountDownLatch(1);
CountDownLatch u5 = new CountDownLatch(1);
// 0.1通过count和await定义执行顺序
Thread top = new Thread(() -> {
System.out.println("上单选择英雄完毕");
u1.countDown();
});
Thread jog = new Thread(() -> {
try {
u1.await();
} catch (InterruptedException exception) {
exception.printStackTrace();
}
System.out.println("打野选择英雄完毕");
u2.countDown();
});
Thread mid = new Thread(() -> {
try {
u2.await();
} catch (InterruptedException exception) {
exception.printStackTrace();
}
System.out.println("中单选择英雄完毕");
u3.countDown();
});
Thread adc = new Thread(() -> {
try {
u3.await();
} catch (InterruptedException exception) {
exception.printStackTrace();
}
System.out.println("ADC选择英雄完毕");
u4.countDown();
});
Thread assist = new Thread(() -> {
try {
u4.await();
} catch (InterruptedException exception) {
exception.printStackTrace();
}
System.out.println("辅助选择英雄完毕");
u5.countDown();
});
// 02.执行
assist.start();
mid.start();
top.start();
adc.start();
jog.start();
u5.await();
System.out.println("全部完成");
}
10.5 正确的停止线程
停止单条线程,执行Thread.interrupt()。
- 并不保证能中断正在运行的线程
- 执行Thread.interrupt()时,如果线程处于sleep(), wait(), join(), lock.lockInterruptibly()等blocking状态,当阻塞方法收到中断请求的时候就会抛出InterruptedException异常,如果线程未处于上述状态,则将线程状态设为interrupted。
停止线程池:参考:唯品会工具类gracefulShutdown
- ExecutorService.shutdown(): 不允许提交新任务,等待当前任务及队列中的任务全部执行完毕后退出;
- ExecutorService.shutdownNow(): 通过Thread.interrupt()试图停止所有正在执行的线程,并不再处理还在队列中等待的任务。
10.6 处理InterruptedException异常
public class InterrupTest implements Runnable{
@Override
public void run(){
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
boolean interrupted1 = Thread.interrupted();
System.out.println("t线程收到main的请求中断,但是t处于阻塞,所以抛出异常,并将中断信号变成: "+interrupted1+"");
//恢复中断状态,即恢复线程t被main线程告知应该中断的信号,以便main线程中能知道t线程的中断,并且对中断作出响应
//如果这里不恢复中断请求,等于停止了main要求t中断的请求,外层函数将收不到中断请求,继续原有循环(一直while循环)
Thread.currentThread().interrupt();
boolean interrupted2 = Thread.interrupted();
System.out.println("恢复线程t被main线程告知应该中断: "+interrupted2+"");
}
}
public static void main(String[] args) {
InterrupTest si = new InterrupTest();
Thread t = new Thread(si);
t.start();
// 01.主线程sleep 2s 后再执行对t线程的中断,让t执行一会
sleepSecond(2);
// 02.中断线程t
t.interrupt();
// 03.如果线程t未被中断,则xxx,中断了则结束
while (!t.isInterrupted()) {
System.out.println("t继续执行");
}
}
public static void sleepSecond(int time) {
try {
TimeUnit.SECONDS.sleep(time);
} catch (InterruptedException exception) {
}
}
}
10.7 多个异常的处理逻辑一致时,使用JDK7的语法避免重复代码
try {
...
} catch (AException | BException | CException ex) {
handleException(ex);
}
十一、序列化
1.谨慎使用Serializable
-
序列化不走构造器(clone也是),在构造器中进行了提前的安全检查,会被绕过
-
大大降低了灵活性:一旦确认了序列化的形式,后续任何变动都可能导致使用这个格式进行反序列化的程序报错
-
建议显示的指定serialVersionUID:版本控制,表明类的不同版本间的兼容性
不指定可能存在的问题
点击展开内容
User类实现了序列化,属性age和name【版本1】
但是未指定serialVersionUID,再序列化的时候JVM会根据age、name计算出一个id-A值,和属性一起,共同组成user1后,序列化,再进行网络传输并以二进制字节流的形式持久化到磁盘(数据的id位A值)
反序列化user1的时候,JVM会再根据属性name、age自动生成一个id-B,比较id-B和id-A,相同则反序列化成功,否则报错
问题:
现在user类新增了一个属性sex性别【版本2】
那么,在对旧版本1的持久化数据user1,进行反序列化操作时
JVM会再根据版本2的属性age、name、sex进行计算生成一个id-B2,比较id-B2和id-A,此时两个值明显不一样【计算时的属性个数都不一样】,所以反序列化use1时候,会报错
(反序列化时系统会自动检测二进制文件中的serialVersionUID,判断它是否与当前类中的serialVersionUID【用户定义了则使用定制值,没有定义则JVM根据类属性等实时计算出一个值】一致。如果一致说明序列化文件的版本与当前类的版本是一样的,可以反序列化成功,否则就失败)
解决问题:
User类实现了序列化,属性age和name【版本1】
指定serialVersionUID = 1
再序列化的时候JVM会id-A = 1值,和属性一起,共同组成user1后,序列化,再进行网络传输并以二进制字节流的形式持久化到磁盘(数据的id位A=1值)
反序列化user1的时候,JVM会再根据属性name、age自动生成一个id-B,比较id-B和id-A=1,相同则反序列化成功,否则报错InvalidClassExceptions
问题:
现在user类新增了一个属性sex性别【版本2】
那么,在对旧版本1的持久化数据user1,进行反序列化操作时
JVM会再根据版本2得到 id-B2 = 1,比较id-B2和id-A,此时两个值都是1,所以反序列化use1成功
建议自定义生成serialVersionUID而不是使用默认值1:https://blog.csdn.net/wufaqidong1/article/details/127295513
2.序列化相关知识点
-
反序列化的对象,不会调用构造函数重新构造,而是基于二进制文件进行生成的新对象
-
序列化前的对象和序列后的对象,地址不一样,但是equals是ture,因为是是深copy
-
序列化和持久化的关系
前者是为了跨进程调用,后者为了写入磁盘