浅谈 Java 开发规范与开发细节 (下)
type.
@param <T> The enum type whose constant is to be returned
@param enumType the {@code Class} object of the enum type from which
to return a constant
@param name the name of the constant to return
@return the enum constant of the specified enum type with the
specified name
@throws IllegalArgumentException if the specified enum type has
no constant with the specified name, or the specified
class object does not represent an enum type
@throws NullPointerException if {@code enumType} or {@code name}
is null
@since 1.5*/public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {T result = enumType.enumConstantDirectory().get(name);if (result != null)return result;if (name == null)throw new NullPointerException("Name is null");throw new IllegalArgumentException("No enum constant " + enumType.getCanonicalName() + "." + name);}
从源码和注释中我们都可以看出来,如果此时 A 服务使用的枚举类为旧版本,只有五个常量,而 B 服务的枚举中包含了新的常量,这个时候在反序列化的时候,由于 name == null,则会直接抛出异常,从这我们也终于看出来,为什么规范中会强制不允许使用枚举类型作为参数进行序列化传递了
慎用可变参数
在翻阅各大规范手册的时候,我看到阿里手册中有这么一条:
【强制】相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object 。说明:可变参数必须放置在参数列表的最后。(提倡同学们尽量不用可变参数编程)正例: public List<User> 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 (Boolean...) in BooleanDemo match.
模糊的函数匹配,因为编译器认为有两个方法都完全满足当前的函数,那么为什么会这样的呢?我们知道在 Java1.5 以后加入了自动拆箱装箱的过程,为了兼容 1.5 以前的 jdk 版本,将此过程设置为了三个阶段:
而我们使用的测试方法中,在第一阶段,判断 jdk 版本,是不是不允许自动装箱拆箱,明显 jdk 版本大于 1.5,允许自动拆箱装箱,因此进入第二阶段,此时判断是否存在更符合的参数方法,比如我们传递了三个布尔类型的参数,但是如果此时有三个布尔参数的方法,则会优先匹配此方法,而不是匹配可变参数的方法,很明显也没有,此时就会进入第三阶段,完成装箱拆箱以后,再去查找匹配的变长参数的方法,这个时候由于完成了拆箱装箱,两个类型会视为一个类型,发现方法上有两个匹配的方法,这时候就会报错了。
那么我们有木有办法处理这个问题呢?毕竟我们熟悉的org.apache.commons.lang3.BooleanUtils
工具类中也有类似的方法,我们都明白,变长参数其实就是会将当前的多个传递的参数装入数组后,再去处理,那么可以在传递的过程中,将所有的参数通过数组包裹,这个时候就不会发生拆箱装箱过程了!例如:
@Testpublic 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 tojava.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.
<p>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:
<pre>
list.subList(from, to).clear();
</pre>
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.
<p>The semantics of the list returned by this method become undefined if
the backing list (i.e., this list) is <i>structurally modified</i> 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<E> 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 <tt>true</tt> (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<E> 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 <T> Set<T> removeDuplicateBySet(List<T> 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(c);}
主要是创建了一个 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 <tt>true</tt> if this list contains the specified element.
More formally, returns <tt>true</tt> if and only if this list contains
at least one element <tt>e</tt> such that
<tt>(o==null ? e==null : o.equals(e))</tt>.
@param o element whose presence in this list is to be tested
@return <tt>true</tt> 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 <tt>i</tt> such that
<tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>,
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,则遍历全部元素判断是否有 nul
l,如果不为 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();}
//第二种写法
评论