编程规约
命名风格
16.接口和实现类的命名有两套规则:
1)【强制】对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用
Impl 的后缀与接口区别。
正例:CacheServiceImpl 实现 CacheService 接口。
2)【推荐】如果是形容能力的接口名称,取对应的形容词为接口名(通常是–able 的形容词)。
正例:AbstractTranslator 实现 Translatable 接口。
18.【参考】各层命名规约:
A) Service/DAO 层方法命名规约
1) 获取单个对象的方法用 get 做前缀。
2) 获取多个对象的方法用 list 做前缀,复数结尾,如:listObjects。
3) 获取统计值的方法用 count 做前缀。
4) 插入的方法用 save/insert 做前缀。
5) 删除的方法用 remove/delete 做前缀。
6) 修改的方法用 update 做前缀。
B) 领域模型命名规约
1) 数据对象:xxxDO,xxx 即为数据表名。
2) 数据传输对象:xxxDTO,xxx 为业务领域相关的名称。
3) 展示对象:xxxVO,xxx 一般为网页名称。
4) POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。
常量定义
- 【推荐】不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。
说明:大而全的常量类,杂乱无章,使用查找功能才能定位到修改的常量,不利于理解,也不利于维护。
正例:缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类 ConfigConsts 下。
代码格式
- 【强制】在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开。
正例:
long first = 1000000000000L;
int second = (int)first + 2;
OOP规约
- 强制】相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object。
说明:可变参数必须放置在参数列表的最后。(提倡同学们尽量不用可变参数编程)
正例:public List
解释:可变参数本质上就是一个数组(经编译器编译后转化为对应类型的数组)。因为如果遇到此种业务,绝对会定下类型,而不是Object,可变参数会增加jdk编译成本,会将它作为一个数组转换,增加编译成本,运行时也会预留内存空间,额外增加运行空间。
【强制】Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。
正例:”test”.equals(object);
反例:object.equals(“test”);
说明:推荐使用 java.util.Objects#equals(JDK7 引入的工具类)。- 【强制】任何货币金额,均以最小货币单位且整型类型来进行存储。
11.【强制】禁止使用构造方法 BigDecimal(double)的方式把 double 值转化为 BigDecimal 对象。
说明:BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。Java 开发手册
9/57
如:BigDecimal g = new BigDecimal(0.1f); 实际的存储值为:0.10000000149
正例:优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了
Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。
BigDecimal recommend1 = new BigDecimal(“0.1”);
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
12.关于基本数据类型与包装数据类型的使用标准如下:
1) 【强制】所有的 POJO 类属性必须使用包装数据类型。
2) 【强制】RPC 方法的返回值和参数必须使用包装数据类型。
3) 【推荐】所有的局部变量使用基本数据类型。
说明:POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或
者入库检查,都由使用者来保证。
正例:数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
反例:某业务的交易报表上显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调
用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线-。所以包装数据类型
的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。
16.【强制】POJO 类必须写 toString 方法。使用 IDE 中的工具:source> generate toString
时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。
说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排查问题。
25.【推荐】类成员与方法访问控制从严:
1) 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private。
2) 工具类不允许有 public 或 default 构造方法。
3) 类非 static 成员变量并且与子类共享,必须是 protected。
4) 类非 static 成员变量并且仅在本类使用,必须是 private。
5) 类 static 成员变量如果仅在本类使用,必须是 private。
6) 若是 static 成员变量,考虑是否为 final。Java 开发手册
11/57
7) 类成员方法只供类内部调用,必须是 private。
8) 类成员方法只对继承类公开,那么限制为 protected。
说明:任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。思考:如果
是一个 private 的方法,想删除就删除,可是一个 public 的 service 成员方法或成员变量,删除一下,不
得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你
会担心的。
【推荐】使用索引访问用String 的 split 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛 IndexOutOfBoundsException 的风险。
说明:
String str = “a,b,c,,”;
String[] ary = str.split(“,”);
// 预期大于 3,结果是 3
System.out.println(ary.length);
解释:看代码:oop.SpiltTest
日期时间
【强制】获取当前毫秒数:System.currentTimeMillis(); 而不是 new Date().getTime()。
说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime 的方式。在 JDK8 中,针对统计时间
等场景,推荐使用 Instant 类。- 【强制】不要在程序中写死一年为 365 天,避免在公历闰年时出现日期转换错误或程序逻辑
错误。Java 开发手册
12/57
正例:
// 获取今年的天数
int daysOfThisYear = LocalDate.now().lengthOfYear();
// 获取指定某年的天数
LocalDate.of(2011, 1, 1).lengthOfYear();
反例:
// 第一种情况:在闰年 366 天时,出现数组越界异常
int[] dayArray = new int[365];
// 第二种情况:一年有效期的会员制,今年 1 月 26 日注册,硬编码 365 返回的却是 1 月 25 日
Calendar calendar = Calendar.getInstance();
calendar.set(2020, 1, 26);
calendar.add(Calendar.DATE, 365);
- 【强制】不要在程序中写死一年为 365 天,避免在公历闰年时出现日期转换错误或程序逻辑
集合处理
- 【强制】关于 hashCode 和 equals 的处理,遵循如下规则:
1) 只要重写 equals,就必须重写 hashCode。
2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写
这两个方法。
3) 如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。
说明:String 因为重写了 hashCode 和 equals 方法,所以我们可以愉快地使用 String 对象作为 key 来使
用。
解释:
比特币:Collision resistance:哈希碰撞,哈希碰撞不可避免,因为输入空间远远大于输出空间。比如256位的哈希值,输出空间只有2的 256次方。根据鸽笼原理。(鸽笼原理(抽屉原理)就是”如果有五个鸽子笼,养鸽人养了6只鸽子,那么当鸽子飞回笼中后,至少有一个笼子中装有2只鸽子。”)
hashCode规范:
在程序的一次运行中,只要没有修改equals()中用于比较的信息,x.hashCode()总是返回一致的hash值
如果x.equals(y)返回true,则x和y具有相等的hash值
如果x和y具有相等的hash值,x.equals(y)并不一定返回true
map key要排除重复。这个问题主要和映射(Map接口)相关. 我们知道Map接口的类会使用到键(Key)的哈希码, 当我们调用put()/get()方法操作Map容器时, 都是根据Key的哈希码来计算存储位置的, 因此如果我们对哈希码的获取没有相关保证, 就可能会得不到预期的结果.
eqaul方法就可以判断对象相等,但是equal方法里面判断的逻辑会很多,很复杂,说白了就是性能不够好。
hashcode方法判断起来,就一行简单的代码,执行效率高,但是hashcode 有一个致命问题就是两个对象hash的时候值可能会一样。所以hashcode有时并不是都靠谱,但是如果hashcode不一样,两个对象肯定不一样。
正确的对比过程应该是这样:先用hash函数对比,如果结果一样,再用equal方法对比判断两个对象是否相等;如果结果不一样,直接判定这两个对象不是同一个。
在hashmap里key用的是自定义类的对象,那么这个自定义类的hashcode方法应该怎么实现
【强制】判断所有集合内部的元素是否为空,使用 isEmpty()方法,而不是 size()==0 的方式。
说明:前者的时间复杂度为 O(1),而且可读性更好。
正例:
Mapmap = new HashMap<>();
if(map.isEmpty()) {
System.out.println(“no element in this map.”);- 【强制】使用 Map 的方法 keySet()/values()/entrySet()返回集合对象时,不可以对其进行添
加元素操作,否则会抛出 UnsupportedOperationException 异常。
9. 【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一
致、长度为 0 的空数组。
反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现
ClassCastException 错误。
正例:
List list = new ArrayList<>(2); list.add(“guan”);
list.add(“bao”);
String[] array = list.toArray(new String[0]);
说明:使用 toArray 带参方法,数组空间大小的 length,
1) 等于 0,动态创建与 size 相同的数组,性能最好。
2) 大于 0 但小于 size,重新创建大小等于 size 的数组,增加 GC 负担。
3) 等于 size,在高并发情况下,数组创建完成之后,size 正在变大的情况下,负面影响与 2 相同。
4) 大于 size,空间浪费,且在 size 处插入 null 值,存在 NPE 隐患。
11.【强制】使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法, 它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。 说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配 器模式,只是转换接口,后台的数据仍是数组。 String[] str = new String[] { “yang”, “hao” }; List list = Arrays.asList(str); 第一种情况:list.add(“yangguanbao”); 运行时异常。 第二种情况:str[0] = “changed”; 也会随之修改,反之亦然。
- 【强制】使用 Map 的方法 keySet()/values()/entrySet()返回集合对象时,不可以对其进行添
可以使用以下方法解决:
// 直接使用empInfo.getPlateNumList() 得到的list.remove会报错,UnsupportedOperationExceptionList<String> plateNumList = new ArrayList<>(empInfo.getPlateNumList());if (CollectionUtils.isNotEmpty(plateNumList)){boolean remove = plateNumList.remove(plateNum);if (remove){String join = String.join(",", plateNumList);SpUserVo spUserVo = new SpUserVo();spUserVo.setSpId(empInfo.getSpId());spUserVo.setUserId(empInfo.getUserId());spUserVo.setPlateNums(join);success = profileService.updateSpUser(spUserVo);if (success){empInfo.setPlateNumList(plateNumList);empInfo.setPlateNums(join);}}}
- 【强制】泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用 add 方法,而<? super T>不能使用 get 方法,作为接口调用赋值时易出错。
说明:扩展说一下 PECS(Producer Extends Consumer Super)原则:第一、频繁往外读取内容的,适合用<? extends T>。第二、经常往里插入的,适合用<? super T>
解释:5.0出现的泛型时一种主观的认为,而不是实际上真正的东西。Lista = List 出错。List get(){return List } a = get();不出错。
Extends 读取撑死了是T,插入不知道插的啥,super插入撑死了T,读取不知道是啥。
PECS,的主角是 它本身。它自己往外输出是P,消费是C。 ```java package com.wsq.io.nio;
import java.util.ArrayList; import java.util.List;
/**
- java开发手册
- 一、编程规范
- (六)集合处理
- 【强制】泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用add方法,而<? super T>不能使用get方法,
- 两者在接口调用赋值的场景中容易出错。 说明:扩展说一下PECS(Producer Extends Consumer Super)原则:
- 第一、频繁往外读取内容的,适合用<? extends T>。第二、经常往里插入的,适合用<? super T> *
- @author midouwan
@date 2021-01-18 9:17 上午 */ public class Test {
public static void main(String[] args) {
//最小子类Student student = new Student();// 多态体现List<Animal> animals = new ArrayList<>();animals.add(student);List<Person> person = new ArrayList<>();person.add(student);List<Student> students = new ArrayList<>();students.add(student);
//List<?> 无界通配符
//List<? extends Person> 上界通配符 ?表示Person的子类
//List<? super Person> 下界通配符 ?表示Person的父类
// 调用上界通配符接口保存信息,接口入参为 List<? extends Person>,只能传入List<Person>或者List<Student>
saveExtendsList(person);
saveExtendsList(students);
saveExtendsList(animals);//不满足,编译器报错
// 调用下界通配符接口保存信息,接口入参为 List<? super Person>,只能传入List<Person>或者List<animals>
saveSuperList(person);
saveSuperList(animals);
saveSuperList(students);//不满足,编译器报错
//调用别人提供的接口,获取集合信息,不清楚提供方到底返回的是哪个集合,List<Person> 还是 List<Student> ?
List<? extends Person> extendList = getExtends();
//调用get会返回上边界父类对象,可以用父类方法操作子类
Person p = extendList.get(0);
Student s = extendList.get(0);//不满足,编译器报错,编译器表示,老色龙后代五花八样,谁知道你是啥样的人
//下面三行全报错,按理说应该可以添加student对象,从当前类看只有animal,student,person三个类,如果还有别的呢,
// 比如学生下面还有两个同级子类,高中生和小学生,别人提供的接口返回的是高中生,然后添加了小学生,编译器表示这个我懵逼,我得报错
extendList.add(new Student());//不满足,编译器报错
extendList.add(new Person());//不满足,编译器报错
extendList.add(new Animal());//不满足,编译器报错
//那有没有啥办法让编译器不懵逼呢?
//如果您很确定返回的是某个类的数据,强转
List<Student> studentList = (List<Student>) getExtends();
studentList.add(new Student());
List<Person> personList = (List<Person>) getExtends();
personList.add(new Student());
List<Animal> animalList = (List<Animal>) getExtends();
animalList.add(new Student());
// 若果您看懂了上面,下面soEasy了
//调用别人接口获取数据,只知道数据是person或者person父类的集合
List<? super Person> aSuper = getSuper();
// 所以用get获取数据,编译器表示老子怎么知道是你哪个老子,压根不晓得你的老子是啥,万一是禽兽呢,我只能object呀,你自己去判断
Object object1 = aSuper.get(0);
//同样如果知道是哪个,强转
Animal dd = (Animal)aSuper.get(0);
// add方法,编译器表示,你的父类肯定是你儿子孙子的父类,多态支持我这么搞
aSuper.add(new Person());
aSuper.add(new Student());
}
/**
* 接口:保存信息
* 此处,必须传入person类或者父类集合
* @param list 下界通配符 ?表示Person的父类
*/
public static void saveSuperList(List<? super Person> list) {
}
/**
* 接口:保存信息
* 此处,必须传入person类或者子类集合
* @param list ? extends Person 上界通配符
*/
public static void saveExtendsList(List<? extends Person> list) {
}
public static List<? super Person> getSuper() {
return new ArrayList<>();
}
public static List<? extends Person> getExtends() {
return new ArrayList<>();
}
static class Animal {
}
static class Person extends Animal {
}
static class Student extends Person {
}
}
<br /> 14.【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。 <br />正例: <br />List list = new ArrayList<>();<br /> list.add("1"); list.add("2"); <br />Iterator iterator = list.iterator(); <br />while (iterator.hasNext()) { <br />String item = iterator.next(); <br />if (删除元素的条件) { <br />iterator.remove(); <br />} <br />} <br />反例:<br />for (String item : list) { <br />if ("1".equals(item)) { <br />list.remove(item); <br />}<br />} <br />说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?
```java
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
System.out.println(list);
for (String item : list) {
if ("2".equals(item)) {
list.remove(item);
}
}
System.out.println(list);

这种情况可以使用removeIf()解决:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
System.out.println(list);
list.removeIf("2"::equals);
System.out.println(list);

【强制】在 JDK7 版本及以上,Comparator 实现类要满足如下三个条件,不然 Arrays.sort, Collections.sort 会抛 IllegalArgumentException 异常。
说明:三个条件如下
1) x,y 的比较结果和 y,x 的比较结果相反。
2) x>y,y>z,则 x>z。
3) x=y,则 x,z 比较结果和 y,z 比较结果相同。
反例:下例中没有处理相等的情况,交换两个对象判断结果并不互反,不符合第一个条件,在实际使用中可能会出现异常。
new Comparator() {
@Override
public int compare(Student o1, Student o2) {
return o1.getId() > o2.getId() ? 1 : -1;
}
};
解释:自反性,传递性,对称性。
Sort种用到了插入排序,快速排序,归并排序,并不是挨着比较,可能下标1和8比,后来2和8比,然后1和2比,1>8,2<8,如果compare方法有问题,不能确定1和2 的大小。就抛异常。18.【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。 说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用 Map.forEach 方法。 正例:values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对 象;entrySet()返回的是 K-V 值组合集合。
```java Mapmap = new HashMap<>(); Student student1 = new Student(); student1.setName(“1”); Student student2 = new Student(); student2.setName(“2”);
map.put(1L, student1); map.put(2L, student2); map.forEach((key, value) -> { System.out.println(key + “:” + value); });

<a name="GZJFM"></a>
## 并发处理
3. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。<br />说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。<br />如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
解释:就像我们使用各种连接池一样,jdbc连接池等。池化很重要。tcp和http一个长连接好处:效率高,减少了创建和销毁链接,http每次都得重新建立连接。<br />[面试题:每发送一个http请求就要建立一个tcp连接吗](https://blog.csdn.net/qq_44918090/article/details/120757316)
4. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下: 1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
```java
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());

解释:看代码。thread包下ExecutorsTest。点进去:newFixedThreadPool,发现其实Executor都是基于ThreadPoolExecutor创建的,只不过传入不同的参数,
参数含义如下:
corePoolSize:线程池中核心线程数量。
maximumPoolSize:线程池最大线程数量。
keepAliveTime:超出核心线程数量时,多出的空闲线程的存活时间。
unit: keepAliveTime的单位。
workQueue:任务队列,当没有空闲线程时,任务存储在该队列。
threadFactory:线程工厂,被用于创建线程。一般使用Executors.defaultThreadFactory()即可。若想清楚的了解自己任务的线程,可以利用该工厂定制化线程。
handler:拒绝策略,当任务太多,如何拒绝任务。
其他参数容易理解:重点:workQueue和handler。
有界队列:ArrayBlockingQueue
创建时必须指定容量。当有新任务时,如果即将使用线程数量< corePoolSize,则会创建新的线程执行,如果 > corePoolSize,则会将新的任务加入到任务队列中。当任务队列已满,如果线程数量
该队列没有界限,除非是系统资源耗尽,否则它不会出现入队失败情况。当新的任务提交,如果使用线程数量
该队列没有容量,也就是说它不存储任务。当有新的任务来时,如果没有空闲的线程,则会创建新的线程,如果线程数量已经达到最大值,就会执行拒绝策略。newCachedThreadPool()就是使用该队列,使用该队列时,应该把最大线程数量设置尽量大,否则很容易就会执行拒绝策略,newCachedThreadPool()就设置的为Integer.MAXVALUE。
优先任务队列:PriorityBlockingQueue
控制任务的执行顺序,它属于无界队列,但是比较特殊。不论是无界还是有界队列都是按照先进先出的顺序处理任务,而PriorityBlockingQueue则可以根据任务自身的优先级顺序执行,它总能确保高优先级的任务先执行。
拒绝策略:
AbortPolicy:直接抛出异常,阻止应用正常工作。(默认使用该策略)
CallerRunsPolicy:不会真正丢弃任务,当线程池未关闭,该策略直接在调用者线程中运行当前丢弃任务。
DiscardPolicy:丢弃无法处理的任务,不做任何处理。
DiscardOldestPolicy:丢弃最老的任务,也就是即将执行的任务,并尝试提交当前任务。
也可以自定义拒绝策略。
newFixedThreadPool和newSingleThreadExecutor点参数队列进去:
public LinkedBlockingQueue() {
this(Integer.**_MAX_VALUE);
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new** SynchronousQueue
}
- 【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为static,必须加锁,或者使用 DateUtils 工具类。
正例:注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocaldf = new ThreadLocal () { @Override
protected DateFormat initialValue() {
return new SimpleDateFormat(“yyyy-MM-dd”);
}
};
说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。
解释:看代码:SimpleDateFormatTest
代码中有两种方式,不安全和安全。
ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问。
当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。
但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。
// 1. 在方法内部使用,没有线程安全问题
private static final String FORMAT = "yyyy-MM-dd HH:mm:ss";
public String getFormat(Date date){
SimpleDateFormat dateFormat = new SimpleDateFormat(FORMAT);
return dateFormat.format(date);
}
// 2. 每次使用的时候加锁
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public void getFormat(){
synchronized (SIMPLE_DATE_FORMAT){
SIMPLE_DATE_FORMAT.format(new Date());
….;
}
// 3. 使用ThreadLocal,每个线程都有自己的SimpleDateFormat对象,互不干扰
private static final ThreadLocal<DateFormat> DATE_FORMATTER = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
// 4. 使用DateTimeFormatter(This class is immutable and thread-safe.)
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(timeFormatter.format(LocalDateTime.now()));
- 【推荐】使用CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果。
说明:注意,子线程抛出异常堆栈,不能在主线程 try-catch 到。
解释:看代码CountDownLatchTest
控制语句
- 【强制】当 switch 括号内的变量类型为 String 并且此变量为外部参数时,必须先进行 null
判断。
// 反例:如下的代码输出是什么?
public class SwitchString {
public static void main(String[] args) {
method(null);
}
public static void method(String param) {
switch (param) {
// 肯定不是进入这里
case "sth":
System.out.println("it's sth");
break;
// 也不是进入这里
case "null":
System.out.println("it's null");
break;
// 也不是进入这里
default:
System.out.println("default");
}
}
}
- 【强制】三目运算符 condition? 表达式 1 : 表达式 2 中,高度注意表达式 1 和 2 在类型对齐 时,可能抛出因自动拆箱导致的 NPE 异常。 说明:以下两种场景会触发类型对齐的拆箱操作: 1) 表达式 1 或表达式 2 的值只要有一个是原始类型。 2) 表达式 1 或表达式 2 的值的类型不一致,会强制拆箱升级成表示范围更大的那个类型。
```java // 反例: Integer a = 1; Integer b = 2; Integer c = null; Boolean flag = false; // ab 的结果是 int 类型,那么 c 会强制拆箱成 int 类型,抛出 NPE 异常 Integer result=(flag? ab : c);
参考资料:[https://mp.weixin.qq.com/s/iQ6qdNv7WTLa3drxNa9png](https://mp.weixin.qq.com/s/iQ6qdNv7WTLa3drxNa9png)
截取文档中的结论:<br />通过查看反编译之后的代码,我们准确的定位到了问题,分析之后我们可以得出这样的结论:NPE的原因应该是三目运算符和自动拆箱导致了空指针异常。<br />根据规定,三目运算符的第二、第三位操作数的返回值类型应该是一样的,这样才能当把一个三目运算符的结果赋值给一个变量。<br />如:Person i = a>b ? i1:i2; ,就要求i1和i2的类型都必须是Person才行。<br />因为Java中存在一种特殊的情况,那就是基本数据类型和包装数据类型可以通过自动拆装箱的方式互相转换。即可以定义int i = new Integer(10);也可以定义Integer i= 10;<br />那如果,三目运算符的第二位和第三位的操作数的类型分别是基本数据类型和包装类型对象时,就需要有一方需要进行自动拆装箱。<br />那到底如何做的呢,根据三目运算符的语法规范。参见jls-15.25,摘要如下:<br />If the second and third operands have the same type (which may be the null type), then that is the type of the conditional expression.<br />If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T.<br />If one of the second and third operands is of the null type and the type of the other is a reference type, then the type of the conditional expression is that reference type.<br />简单的来说就是:当第二,第三位操作数分别为基本类型和对象时,其中的对象就会拆箱为基本类型进行操作。<br />所以,结果就是:由于使用了三目运算符,并且第二、第三位操作数分别是基本类型和对象。所以对对象进行拆箱操作,由于该对象为null,所以在拆箱过程中调用null.booleanValue()的时候就报了NPE。
```java
// 保证了三目运算符的第二第三位操作数都为对象类型。
Map<String,Boolean> map = new HashMap<String, Boolean>();
Boolean b = (map!=null ? map.get("test") : Boolean.FALSE);
- 【强制】在高并发场景中,避免使用”等于”判断作为中断或退出的条件。 说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件 来代替。
反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数, 这样的话,活动无法终止。
其它
- 【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
说明:不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”);
意思是在方法外定义Pattern pattern = Pattern.compile(“规则”);,使其触发预编译
如: ```java private static final Pattern pattern = Pattern.compile(regexRule);
private void func(…) { Matcher m = pattern.matcher(content); if (m.matches()) { … } }
4. 【强制】注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x<1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法。<br />解释:看代码thread.RandomTest
<a name="f33U2"></a>
# 异常日志
<a name="pHsrL"></a>
## 异常处理
7. 【强制】不要在 finally 块中使用 return。<br />说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点。<br />反例:<br />private int x = 0;<br />public int checkReturn() {<br />try {<br />// x 等于 1,此处不返回<br />return ++x;<br />} finally {<br />// 返回的结果是 2<br />return ++x;<br />}<br />}<br />解释:做资源回收的,就做好资源回收就像,不要影响写字楼里的其他业务。别自作主张,替楼里的公司回复客户的业务信息。
8. 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。<br />说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。<br />解释:先兜住,再处理。兜不住,啥都没有意义。<br />举个栗子
```java
//BizException 是 Exception 的子类
public class BizException extends Exception {}
//抛出父类Exception
public static void test() throws Exception {}
try {
test(); //编译错误
} catch (BizException e) { //捕获异常子类是没法匹配的哦
log.error(e);
}
//抛出子类Exception
public static void test() throws BizException {}
try {
test();
} catch (Exception e) {
log.error(e);
}
- 【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
反例:public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
2) 数据库的查询结果可能为 null。
3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5) 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
正例:使用 JDK8 的 Optional 类来防止 NPE 问题。
解释:对自己职业的尊重。3,集合可以add null,isNotEmpty只判断size的大小。只要调用别人的方法一定判断空指针。严于律己宽以待人,允许别人犯错误。6,级联调用不推荐,如果返回当前对象,可以用级联调用,否则,别用。stringbuilder可以级联,返回当前对象可以级联调用。不是当前对象建议一步一调用。
Optional用法
https://blog.csdn.net/JoshuaXin/article/details/84849363
日志规约
- 【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);
解释:日志门面,是门面模式的一个典型的应用。
门面模式(Facade Pattern),也称之为外观模式,其核心为:外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用。
就像前面介绍的几种日志框架一样,每一种日志框架都有自己单独的API,要使用对应的框架就要使用其对应的API,这就大大的增加应用程序代码对于日志框架的耦合性。
为了解决这个问题,就是在日志框架和应用程序之间架设一个沟通的桥梁,对于应用程序来说,无论底层的日志框架如何变,都不需要有任何感知。只要门面服务做的足够好,随意换另外一个日志框架,应用程序不需要修改任意一行代码,就可以直接上线。
安全规约
- 【强制】用户请求传入的任何参数必须做有效性验证。
说明:忽略参数校验可能导致:
page size 过大导致内存溢出
恶意order by 导致数据库慢
查询任意重定向
SQL 注入
反序列化注入
正则输入源串拒绝服务ReDoS
说明:Java 代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题,但是如果攻击人员使用的是特殊构造的字符串来验证,有可能导致死循环的结果。
解释:
Orderby https://www.cnblogs.com/fengtingxin/p/11215164.html
正则拒绝服务aaaaaaaaaa8
ReDoS(Regular expression Denial of Service) 正则表达式拒绝服务攻击。开发人员使用了正则表达式来对用户输入的数据进行有效性校验, 当编写校验的正则表达式存在缺陷或者不严谨时, 攻击者可以构造特殊的字符串来大量消耗服务器的系统资源,造成服务器的服务中断或停止。
非确定型有限状态自动机(Nondeterministic Finite Automation: NFA)
NFA则是“表达式主导”捏着正则式去比文本,吃掉一个字符,就把它跟正则式比较,匹配就记下来,然后继续匹配。一旦不匹配,就把刚吃的这个字符吐出来,一个个的吐,直到回到上一次匹配的地方。
https://blog.csdn.net/qq_37053783/article/details/89021727
反正:用户输入的东西都要校验。还是有坏用户的。
