throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + “.” + name);
}
从源码和注释中我们都可以看出来,如果此时A服务使用的枚举类为旧版本,只有五个常量,而B服务的枚举中包含了新的常量,这个时候在反序列化的时候,由于name == null,则会直接抛出异常,从这我们也终于看出来,为什么规范中会强制不允许使用枚举类型作为参数进行序列化传递了
慎用可变参数
在翻阅各大规范手册的时候,我看到阿里手册中有这么一条:
【强制】相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Obje
ct 。说明:可变参数必须放置在参数列表的最后。(提倡同学们尽量不用可变参数编程)
正例: public List listUsers(String type, Long… ids) {…}
吸引了我,因为在以前开发过程中,我就遇到了一个可变参数埋下的坑,接下来我们就来看看可变参数相关的一个坑。
相信很多人都编写过企业里使用的工具类,而我当初在编写一个Boolean类型的工具类的时候,编写了大概如下的两个方法:
private static boolean and(boolean… booleans) {
for (boolean b : booleans) {
if (!b) {
return false;
}
}
return true;
}
private static boolean and(Boolean… booleans) {
for (Boolean b : booleans) {
if (!b) {
return false;
}
}
return true;
}
这两个方法看起来就是一样的,都是为了传递多个布尔类型的参数进来,判断多个条件连接在一起,是否能成为true的结果,但是当我编写测试的代码的时候,问题出现了:
public static void main(String[] args) {
boolean result = and(true, true, true);
System.out.println(result);
}
这样的方法会返回什么呢?其实当代码刚刚编写完毕的时候,就会发现编译器已经报错了,会提示:
Ambiguous method call. Both and (boolean…) in BooleanDemo and and (Boolea
n…) in BooleanDemo match.
模糊的函数匹配,因为编译器认为有两个方法都完全满足当前的函数,那么为什么会这样的呢?我们知道在Java1.5以后加入了自动拆箱装箱的过程,为了兼容1.5以前的jdk版本,将此过程设置为了三个阶段:
而我们使用的测试方法中,在第一阶段,判断jdk版本,是不是不允许自动装箱拆箱,明显jdk版本大于1.5,允许自动拆箱装箱,因此进入第二阶段,此时判断是否存在更符合的参数方法,比如我们传递了三个布尔类型的参数,但是如果此时有三个布尔参数的方法,则会优先匹配此方法,而不是匹配可变参数的方法,很明显也没有,此时就会进入第三阶段,完成装箱拆箱以后,再去查找匹配的变长参数的方法,这个时候由于完成了拆箱装箱,两个类型会视为一个类型,发现方法上有两个匹配的方法,这时候就会报错了。
那么我们有木有办法处理这个问题呢?毕竟我们熟悉的org.apache.commons.lang3.BooleanUtils
工具类中也有类似的方法,我们都明白,变长参数其实就是会将当前的多个传递的参数装入数组后,再去处理,那么可以在传递的过程中,将所有的参数通过数组包裹,这个时候就不会发生拆箱装箱过程了!例如:
@Test
public void testAnd_primitive_validInput_2items() {
assertTrue(
! BooleanUtils.and(new boolean[] { false, false })
}
而参考其他框架源码大神的写法中,也有针对这个的编写的范例:
通过此种方法可以保证如果传入的是基本类型,直接匹配当前方法,如果是包装类型,则在第二阶段以后匹配到当前函数,最终都是调用了BooleanUtils中基本类型的and方法
List的去重与xxList方法
List作为我们企业开发中最常见的一个集合类,在开发过程中更是经常遇到去重,转换等操作,但是集合类操作的不好很多时候会导致我们的程序性能缓慢或者出现异常的风险,例如阿里手册中提到过:
【 强 制 】 ArrayList 的 subList 结 果 不 可 强 转 成 ArrayList , 否 则 会 抛 出
ClassCastException 异 常,即 java.util.RandomAccessSubList cannot be cast to
java.util.ArrayList。
【强制】在 SubList 场景中,高度注意对原集合元素的增加或删除,均会导致子列表的
遍历、增加、删除产生 ConcurrentModificationException 异常。
【强制】使用工具类 Arrays.asList () 把数组转换成集合时,不能使用其修改集合相关的
方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
而手册中的这些xxList方法则是我们开发过程中比较常用的,那么为什么阿里手册会有这些规范呢?我们来看看第一个方法subList
,首先我们先看看SubList类和ArrayList类的区别,从类图上我们可以看出来两个类之间并没有继承关系:
所以手册上不允许使用subList
强转为ArrayList,那么为什么原集合不能进行增删改查操作呢?我们来看看其源码:
/**
- Returns a view of the portion of this list between the specified
- {@code fromIndex}, inclusive, and {@code toIndex}, exclusive. (If
- {@code fromIndex} and {@code toIndex} are equal, the returned list is
- empty.) The returned list is backed by this list, so non-structural
- changes in the returned list are reflected in this list, and vice-versa.
- The returned list supports all of the optional list operations.
-
This method eliminates the need for explicit range operations (of
- the sort that commonly exist for arrays). Any operation that expects
- a list can be used as a range operation by passing a subList view
- instead of a whole list. For example, the following idiom
- removes a range of elements from a list:
- list.subList(from, to).clear();
- Similar idioms may be constructed for {@link #indexOf(Object)} and
- {@link #lastIndexOf(Object)}, and all of the algorithms in the
- {@link Collections} class can be applied to a subList.
-
The semantics of the list returned by this method become undefined if
- the backing list (i.e., this list) is structurally modified in
- any way other than via the returned list. (Structural modifications are
- those that change the size of this list, or otherwise perturb it in such
- a fashion that iterations in progress may yield incorrect results.)
- @throws IndexOutOfBoundsException {@inheritDoc}
- @throws IllegalArgumentException {@inheritDoc}
*/
public List subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
我们可以看到代码的逻辑只有两步,第一步检查当前的索引和长度是否变化,第二步构建新的SubList出来并且返回。从注释我们也可以了解到,SubList中包含的范围,如果对其进行增删改查操作,都会导致原来的集合发生变化,并且是从当前的index + offSet进行变化。那么为什么我们这个时候对原来的ArrayList进行增删改查操作的时候会导致SubList集合操作异常呢?我们来看看ArrayList的add方法:
/**
- Appends the specified element to the end of this list.
- @param e element to be appended to this list
- @return true (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
我们可以看到一点,每次元素新增的时候都会有一个 ensureCapacityInternal(size + 1);
操作,这个操作会导致modCount长度变化,而modCount则是在SubList的构造中用来记录长度使用的:
SubList(AbstractList parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount; // 注意:此处复制了 ArrayList的 modCount
}
而SubList的get操作的源码如下:
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
可以看到每次都会去校验一下下标和modCount,我们来看看checkForComodification
方法:
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
可见每次都会检查,如果发现原来集合的长度变化了,就会抛出异常,那么使用SubList的时候为什么要注意原集合是否被更改的原因就在这里了。
那么为什么asList方法的集合不允许使用新增、修改、删除等操作呢?
我们来看下和ArrayList的方法比较:
很明显我们能看出来,asList构建出来的List没有重写add
、 remove
函数,说明该类的集合操作的方法来自父类AbstactList
,我们来看看父类的add方法:
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
从这我们可以看出来,如果我们进行add或者remove操作,会直接抛异常。
集合去重操作
我们再来看一个企业开发中最常见的一个操作,将List集合进行一次去重操作,我本来以为每个人都会选择使用Set
来进行去重,可是当我翻看团队代码的时候发现,居然很多人偷懒选了List自带的contains
方法判断是否存在,然后进行去重操作!我们来看看一般我们使用Set去重的时候编写的代码:
public static Set removeDuplicateBySet(List data) {
if (CollectionUtils.isEmpty(data)) {
return new HashSet<>();
}
return new HashSet<>(data);
}
而HashSet的构造方法如下:
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll©;
}
主要是创建了一个HashMap以后进行addAll操作,我们来看看addAll方法:
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
从这我们也可以看出来,内部循环调用了add方法进行元素的添加:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
而add方法内部依赖了hashMap的put方法,我们都知道hashMap的put方法中的key是唯一的,即天然可以避免重复,我们来看看key的hash是如何计算的:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看到如果 key 为 null ,哈希值为 0,否则将 key 通过自身 hashCode 函数计算的的哈希值和其右移 16 位进行异或运算得到最终的哈希值,而在最终的putVal
方法中,判断是否存在的逻辑如下:
p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
而看到这我们基本已经明了了,set的hash计算还是依靠元素自身的hashCode计算,只要我们需要去重的元素实例遵循了重写hashCode也重写equals的规则,保持一致,直接使用set进行去重还是很简单的。反过来我们再来看看List的contains
方法的实现:
/**
- Returns true if this list contains the specified element.
- More formally, returns true if and only if this list contains
- at least one element e such that
- (onull ? enull : o.equals(e)).
- @param o element whose presence in this list is to be tested
- @return true if this list contains the specified element
*/
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
可以看到其实是依赖于indexOf方法来判断的:
/**
- Returns the index of the first occurrence of the specified element
- in this list, or -1 if this list does not contain the element.
- More formally, returns the lowest index i such that
- (o==null ? get(i)==null : o.equals(get(i))),
- or -1 if there is no such index.
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
可以看到indexOf的逻辑为,如果为null,则遍历全部元素判断是否有null,如果不为null也会遍历所有元素的equals方法来判断是否相等,所以时间复杂度接近O (n^2)
,而Set的containsKey方法主要依赖于getNode方法:
/**
- Implements Map.get and related methods.
- @param hash hash for key
- @param key the key
- @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
可以看到优先通过计算的hash值找到table的第一个元素比较,如果相等直接返回第一个元素,如果是树节点则从树种查找,不是则从链中查找,可以看出来,如果hash冲突不是很严重的话,查找的速度接近O (n)
,很明显看出来,如果数量较多的话,List的contains
速度甚至可能差距几千上万倍!
字符串与拼接
在Java核心库中,有三个字符串操作的类,分别为String
、 StringBuffer
、StringBuilder
,那么势必会涉及到一个问题,企业开发中经常使用到字符串操作,例如字符串拼接,但是使用的不对会导致出现大量的性能陷阱,那么在什么场合下使用String拼接什么时候使用其他的两个比较好呢?我们先来看一个案例:
public String measureStringBufferApend() {
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < 10000; i++) {
buffer.append(“hello”);
}
return buffer.toString();
}
//第二种写法
public String measureStringBuilderApend() {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10000; i++) {
builder.append(“hello”);
}
return builder.toString();
}
//直接String拼接
public String measureStringApend() {
String targetString = “”;
for (int i = 0; i < 10000; i++) {
targetString += “hello”;
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://i-blog.csdnimg.cn/blog_migrate/cfac74f2a226ab5c519c2a7a191a42fa.jpeg)
最后
由于篇幅原因,就不多做展示了
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
存中…(img-zwi4xwQ1-1713743394220)]
[外链图片转存中…(img-nqfW7OhX-1713743394220)]
[外链图片转存中…(img-w0sZwGS2-1713743394220)]
[外链图片转存中…(img-KjV4QyDT-1713743394220)]
[外链图片转存中…(img-8si87BlY-1713743394220)]
[外链图片转存中…(img-mbaneM1M-1713743394221)]
由于篇幅原因,就不多做展示了
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!