前言
碎碎念
hi。又见面了,各位大佬,我是敲敲牛皮糖。这次是复工的第一个周末,外面下着雨,不是春雨,也不是冬雨。那就给大家拜个年,新的一年虎虎生威,买的基金必涨(主要是我的都亏拉了 hhh),写的代码无bug,早日实现财富自由。
正文
上回我们一起学习了常量定义、代码格式和opp规约。今天继续学习日期时间、集合处理规约。
日期时间
- 【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y。 说明:日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7 之后 引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY 就是下一年。
正例:表示日期和时间的格式如下所示:
new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”)
代码演示:
public static void main(String[] args) {
long l = Date.parse("Mon 27 Dec 2021 13:3:00");
Date date1 = new Date(l);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-dd");
SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat("yyyy-MM-dd");
System.out.println(date1);
System.out.println(simpleDateFormat.format(date1));
System.out.println(simpleDateFormat1.format(date1));
}
控制台的输出:
Mon Dec 27 13:03:00 CST 2021
2022-12-27
2021-12-27
- 【强制】在日期格式中分清楚大写的M和小写的m,大写的H和小写的h分别指代的意义。
说明:日期格式中的这两对字母表意如下:- 表示月份是大写的 M;
- 表示分钟则是小写的 m;
- 24 小时制的是大写的 H;
- 12 小时制的则是小写的 h。
- 【强制】获取当前毫秒数:System.currentTimeMillis(); 而不是 new Date().getTime()。
说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime 的方式。在 JDK8 中,针对统计时间 等场景,推荐使用 Instant 类。
源码:
public Date() {
this(System.currentTimeMillis());
}
通过new Date()源码发现,还是通过System.currentTimeMillis()获取当前系统的时间,而 new Date().getTime()需要创建对象,会耗费时间,因此带来误差。
- 【强制】不允许在程序任何地方中使用:1)java.sql.Date。 2)java.sql.Time。 3)java.sql.Timestamp。
说明:第 1 个不记录时间,getHours()抛出异常;第 2 个不记录日期,getYear()抛出异常;第 3 个在构造 方法 super((time/1000)*1000),在 Timestamp 属性 fastTime 和 nanos 分别存储秒和纳秒信息。
反例:
java.util.Date.after(Date)进行时间比较时,当入参是 java.sql.Timestamp 时,会触发 JDK BUG(JDK9 已修复),可能导致比较时的意外结果。
代码实例:
public static void main(String[] args) {
java.sql.Date sqlDate = new Date(System.currentTimeMillis());
System.out.println("java.sql.Date只记录日期,不记录时间。如下展示:");
System.out.println(sqlDate);
java.sql.Time sqlTime = new Time(System.currentTimeMillis());
System.out.println("java.sql.Time只记录时间,不记录日期。如下展示:");
System.out.println(sqlTime);
}
控制台输出
java.sql.Date只记录日期,不记录时间。如下展示:
2022-02-12
java.sql.Time只记录时间,不记录日期。如下展示:
16:20:37
部分源码
java.sql.Timestamp构造函数源码
public Timestamp(long time) {
super((time/1000)*1000);
nanos = (int)((time%1000) * 1000000);
if (nanos < 0) {
nanos = 1000000000 + nanos;
super.setTime(((time/1000)-1)*1000);
}
}
java.util.Date构造函数源码
public Date() {
this(System.currentTimeMillis());
}
public Date(long date) {
fastTime = date;
}
java.util.Date().after()源码
public boolean after(Date when) {
return getMillisOf(this) > getMillisOf(when);
}
static final long getMillisOf(Date date) {
if (date.cdate == null || date.cdate.isNormalized()) {
return date.fastTime;
}
BaseCalendar.Date d = (BaseCalendar.Date) date.cdate.clone();
return gcal.getTime(d);
}
可以看到fastTime 和 nanos 分别存储秒和纳秒信息。而java.util.Date是将两者存在一起。通过java.util.Date().after()源码可以发现getMillisOf的时候会有直接返回fastTime的情况,则此时会造成
java.sql.Timestamp纳秒的丢失。
- 【强制】不要在程序中写死一年为365天,避免在公历闰年时出现日期转换错误或程序逻辑 错误。
正例:
// 获取今年的天数
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);
集合处理
- 【强制】关于hashCode和equals的处理,遵循如下规则:
1.1 只要覆写 equals,就必须覆写 hashCode。
1.2 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两种方法。
1.3 如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。
说明:String 因为覆写了 hashCode 和 equals 方法,所以可以愉快地将 String 对象作为 key 来使用。
set.add()方法的部分源码
从源码中可以看到当插入重复数据的时候,会进行p.hash == hash 和 key.equals(k)的比较。final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {
代码示例
public static void main(String[] args) {
public static void main(String[] args) {
Map<Role, String> map = new HashMap<>();
String string = "1231";
Role role = new Role();
role.setTouchType("2342");
Role role1 = new Role();
role1.setTouchType("2342");
map.put(role,string);
System.out.print("role的hashcode:");
System.out.println(role.hashCode());
System.out.print("rol2的hashcode:");
System.out.println(role1.hashCode());
System.out.print("调用equals()结果::");
System.out.println(role.equals(role1));
System.out.print("调用map的get()方法:");
System.out.print(map.get(role1));
}
控制台输出
role的hashcode:1173230247
rol2的hashcode:856419764
调用equals()结果::false
调用map的get()方法:null
从控制台可以看出,虽然role对象存储的内容是一样的,但是由于没有重写hashcode和equals两者会被map识别成两个对象,自然也就拿不出来自己存放的值了。
- 【强制】判断所有集合内部的元素是否为空,使用isEmpty()方法,而不是size()==0的方式。
说明:在某些集合中,前者的时间复杂度为 O(1),而且可读性更好。
正例:
Map<String, Object> map = new HashMap<>(16); if(map.isEmpty()) {
System.out.println(“no element in this map.”); }
总结:
建议大家直接使用org.apache.commons.collections下的集合xxxUtils进行判空。能避免烦人的空指针问题。例如:你接收过来的map=null,此时调用map.isEmptu()自然报空指针异常。
MapUtils.isEmpty();
StringUtils.isBlank();
CollectionUtils.isEmpty();
- 【强制】在使用java.util.stream.Collectors类的toMap()方法转为Map集合时,一定要使
用含有参数类型为 BinaryOperator,参数名为 mergeFunction 的方法,否则当出现相同 key
值时会抛出 IllegalStateException 异常。
说明:参数 mergeFunction 的作用是当出现 key 重复时,自定义对 value 的处理策略。
正例:
List<Pair<String, Double>> pairArrayList = new ArrayList<>(3); pairArrayList.add(new Pair<>(“version”, 12.10)); pairArrayList.add(new Pair<>(“version”, 12.19)); pairArrayList.add(new Pair<>(“version”, 6.28));
Map<String, Double> map = pairArrayList.stream().collect( // 生成的 map 集合中只有一个键值对:{version=6.28} Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
反例:
String[] departments = new String[] {“iERP”, “iERP”, “EIBU”}; // 抛出 IllegalStateException 异常
Map<Integer, String> map = Arrays.stream(departments)
.collect(Collectors.toMap(String::hashCode, str -> str));
代码示例
public static void main(String[] args) {
List<Role> roleList = new ArrayList<>();
roleList.add(new Role("213","213123"));
roleList.add(new Role("213","hhh"));
roleList.add(new Role("213","huijia"));
Map<String, String> map = roleList.stream()
.collect(Collectors.toMap(Role::getBizType,Role::getTouchType,(v1,v2)->v2));
System.out.println(JSON.toJSONString(map));
}
控制台输出:
{"213":"huijia"}
toMap()方法部分源码
两个参数的toMap()源码
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
三个参数的toMap()源码
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction) {
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
两个参数的toMap()方法
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value);
V oldValue = get(key);
V newValue = (oldValue == null) ? value :
remappingFunction.apply(oldValue, value);
if(newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
}
从源码可以看出,当key出现重复的时候,会调用remappingFunction.apply()。当你使用两个参数的toMap()方法的时候,会自动将mergeFunction赋值成throwingMerger()。因此会直接抛出异常。
- 【强制】在使用java.util.stream.Collectors类的toMap()方法转为Map集合时,一定要注意当 value 为 null 时会抛 NPE 异常。
说明:在 java.util.HashMap 的 merge 方法里会进行如下的判断:
if (value == null || remappingFunction == null)
throw new NullPointerException();
反例:
List<Pair<String, Double>> pairArrayList = new ArrayList<>(2); pairArrayList.add(new Pair<>(“version1”, 8.3)); pairArrayList.add(new Pair<>(“version2”, null));
Map<String, Double> map = pairArrayList.stream().collect(
// 抛出 NullPointerException 异常
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2)); - 【强制】ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出 ClassCastException 异 常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。
说明:subList()返回的是 ArrayList 的内部类 SubList,并不是 ArrayList 本身,而是 ArrayList 的一个视 图,对于 SubList 的所有操作最终会反映到原列表上。
public static void main(String[] args) {
List<Role> roleList = new ArrayList<>();
roleList.add(new Role("213","213123"));
List<Role> roleList1 = roleList.subList(1,1);
System.out.println(roleList1.getClass());
}
控制台输出
class java.util.ArrayList$SubList
- 【强制】使用Map的方法keySet()/values()/entrySet()返回集合对象时,不可以对其进行添 加元素操作,否则会抛出 UnsupportedOperationException 异常。
以.values()方法为例,通过查看源码发现,values()方法返回的类型是HashMap的内部类Valuse。而Values类继承了AbstractCollection。查看AbstractCollection类中的add方法便了然了。
public boolean add(E e) {
throw new UnsupportedOperationException();
}
- 【强制】Collections 类返回的对象,如:emptyList()/singletonList()等都是 immutable list,不可对其进行添加或者删除元素的操作。
反例:如果查询无结果,返回 Collections.emptyList()空集合对象,调用方一旦进行了添加元素的操作,就会触发 UnsupportedOperationException 异常。
因此,在日常的开发中,如果需要返回空list,则不要new一个空list,直接返回Collections.emptyList()。 - 【强制】在subList场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常。
代码示例
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("213");
list.add("12355");
list.add("123");
for(String string : list){
list.add("qwe");
}
}
控制台输出:
Exception in thread "main" java.util.ConcurrentModificationException
- 【强制】使用集合转数组的方法,必须使用集合的 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 隐患。 - 【强制】在使用 Collection 接口任何实现类的 addAll()方法时,都要对输入的集合参数进行NPE 判断。
说明:在 ArrayList#addAll 方法的第一行代码即Object[] a = c.toArray(); 其中 c 为输入集合参数,如果为 null,则直接抛出异常。
部分源码
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
源码中可以看到c.toArray()。如果是null参传入的话,则直接会抛出空指针异常。
11.【强制】使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法, 它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[] { “chen”, “yang”, “hao” };
List list = Arrays.asList(str);
第一种情况:list.add(“yangguanbao”); 运行时异常。
第二种情况:str[0] = “change”; 也会随之修改,反之亦然。
注意:Arrays内部也有一个ArrayList(java.util.ArrayList#ArrayList())的类,但是不同于java.util.ArrayList。
asList()部分源码
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
/**
* @serial include
*/
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
从源码可以看出 new ArrayList<>(a)中a的类型是一个final的数组。
12.【强制】泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用 add 方法, 而<? super T>不能使用 get 方法,两者在接口调用赋值的场景中容易出错。
说明:扩展说一下 PECS(Producer Extends Consumer Super)原x则:第一、频繁往外读取内容的,适合用 <? extends T>。第二、经常往里插入的,适合用<? super T>
13. 【强制】在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行 instanceof 判断,避免抛出 ClassCastException 异常。
说明:毕竟泛型是在 JDK5 后才出现,考虑到向前兼容,编译器是允许非泛型集合与泛型集合互相赋值。
反例:
List generics = null;
List notGenerics = new ArrayList(10);
notGenerics.add(new Object());
notGenerics.add(new Integer(1));
generics = notGenerics;
// 此处抛出 ClassCastException 异常
String string = generics.get(0);
注意:编译器不能校验出非泛型集合与泛型集合互相赋值之间的值的类型是否相同,但是运行的时候会明确出值的具体类型,所以赋值之前一定要使用instanceof判断下,是不是相同的类型,避免抛出异常。
14. 【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator
方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
List list = new ArrayList<>(); list.add(“1”);
list.add(“2”);
Iterator iterator = list.iterator(); while (iterator.hasNext()) {
String item = iterator.next(); if (删除元素的条件) {
iterator.remove(); }
}
反例:
for (String item : list) {
if (“1”.equals(item)) {
list.remove(item); }
}
说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?
15. 【强制】在 JDK7 版本及以上,Comparator 实现类要满足如下三个条件,不然 Arrays.sort, Collections.sort 会抛 IllegalArgumentException 异常。
说明:三个条件如下
- x,y 的比较结果和 y,x 的比较结果相反。
- x>y,y>z,则 x>z。
- x=y,则 x,z 比较结果和 y,z 比较结果相同。
反例:下例中没有处理相等的情况,交换两个对象判断结果并不互反,不符合第一个条件,在实际使用中
可能会出现异常。
new Comparator() {
@Override
public int compare(Student o1, Student o2) {
return o1.getId() > o2.getId() ? 1 : -1;
} };
总结
今天的内容比之前两期都要多,也很重要。特别是第二部分:集合处理,是在日常开发中,经常会用得到,也可能会疏忽造成异常问题的。好事多磨,今天就到这里吧。最近Winter Olympics举行的如火如荼,冰墩墩也火爆起来了,不知道各位抢到了没呢?