浅谈Java开发规范与开发细节(下)

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核心库中,有三个字符串操作的类,分别为StringStringBufferStringBuilder,那么势必会涉及到一个问题,企业开发中经常使用到字符串操作,例如字符串拼接,但是使用的不对会导致出现大量的性能陷阱,那么在什么场合下使用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”;
}
return targetString;
}

使用JMH测试的性能测试结果可以看出来,使用StringBuffer拼接比String += 的方式效率快了200倍,StringBuilder的效率比Stirng += 的效率快了700倍,这是为什么呢?原来String的 += 操作的时候每一次都需要创建一个新的String对象,然后将两次的内容copy进来,再去销毁原来的String对象,再去创建。。。。而StringBuffer和StringBuilder之所以快,是因为内部预先分配了一部分内存,只有在内存不足的时候,才会去扩展内存,而StringBuffer和StringBuilder的实现几乎一样,唯一的区别就是方法都是synchronized包装,保证了在并发下的字符串操作的安全性,因此导致性能会有一定幅度的下降。那么是不是String拼接一定就是最快的呢?也不一定,例如下面的例子:

public void measureSimpleStringApend() {
for (int i = 0; i < 10000; i++) {
String targetString = "Hello, " + “world!”;
}
}
//StringBuilder拼接
public void measureSimpleStringBuilderApend() {
for (int i = 0; i < 10000; i++) {
StringBuilder builder = new StringBuilder();
builder.append("hello, ");
builder.append(“world!”);
}
}

相信有经验的就会发现,直接两个字符串片段直接 + 的拼接方式,效率竟然比StringBuilder还要快!这个巨大的差异,主要来自于 Java 编译器和 JVM 对字符串处理的优化。" Hello, " + "
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

对于很多Java工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

整理的这些资料希望对Java开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

再分享一波我的Java面试真题+视频学习详解+技能进阶书籍

美团二面惜败,我的凉经复盘(附学习笔记+面试整理+进阶书籍)

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
3032d36.jpg" alt=“img” style=“zoom: 33%;” />

最后

对于很多Java工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

整理的这些资料希望对Java开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

再分享一波我的Java面试真题+视频学习详解+技能进阶书籍

[外链图片转存中…(img-bEE26s0a-1713743427163)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 24
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值