Java语言中十大“坑爹”功能(站长版)
前言
一千个读者有一千个哈姆雷特,一万个Java用户,则能找出一万种“坑爹”功能。对于个人而言,每个人的思维习惯不同,感受到的结果不同,我认为违反思维常识而“坑爹”的功能,也许在他人眼里合乎情理、自然顺畅,所以请读者根据自己的情况自选阅读。
正文
作为一门面向对象的编程语言,Java 凭借其简单易用、功能强大的特点刚一出世变受到了编程爱好者的青睐。后来,随着互联网的发展,Java 语言更是席卷全球,势不可挡。Java 常年高居编程语言排行榜的首位,更是培训机构的摇钱树,足以表明 Java 的王者之风。
然而,即便是如此强大的编程语言,也有很多“坑爹”的功能,稍不注意,我们就会掉入坑里。因为大部分时候,都是由于我们没有深度思考,对违反思维常识的语法规范没有深度理解才造成了令人不愉快的后果。今天我们就来梳理一下 Java 中最“坑爹”、最违反常识的功能点,以飨读者。
1、启动线程用start,而不是用run
public class MyThread extends Thread
{
public void run()
{
for (int i = 0; i <= 10; i++)
{
System.out.println("当前线程为:" + Thread.currentThread().getName() + ",计算结果为:" + i);
}
}
public static void main(String[] args)
{
new MyThread().run();
}
}
如果程序从未调用线程对象的start方法来启动它,那么这个线程对象将一直处于“新建状态”,它永远也不会作为线程获得执行的机会,它只是一个普通的Java对象。当线程调用线程对象的run方法时,与调用普通的Java对象并没有任何区别,因此绝对不会启动一个新线程。
2、短路逻辑
在Java中逻辑运算符 && 和 ||,它们都存在短路效应。
2.1 长路与运算 &:需要判断两边,无短路效应
2.2 短路与运算 &&:存在短路效应
在Java中,首先运算表达式a,其分为以下两种情况:
(1)如果a为true,则继续运算表达式b,只有a和b同时为true,结果才是true
(2)如果表达式a为false,那么整个表达式也肯定为false,所以表达式b不会被运算
2.3 长路或运算 |:需要判断两边,无短路效应
2.4 短路或运算 ||:存在短路效应
对于a || b,只有当a和b同时为false时,整个表达式才为false(有一个为true,则表达式为true)。如果a为true,整个表达式的值为true,则没有必要再运算表达式b。
3、switch 必须加上 break 才结束
对于多重分支选择,一系列的 if-else-if 语句会让代码的可读性变差,建议使用 switch 语句来代替,然而 switch case 中的分支判断,必须加上 break 语句才会中止其它 case 的执行,比如:
int count = 1;
switch(count)
{
case 1:
System.out.println("less");
case 2:
System.out.println("no more no less");
case 3:
System.out.println("more");
}
上面的代码会输出:
less
no more no less
more
实际上,这并不是我们想要的结果,或者说违反了我们的常识认识。满足了某种条件,就只需要执行此条件下的逻辑即可,其他的 case 应该不予理会直接跳过,像上面这段代码,只需要输出 less 就行了。当然,在每个 case 结尾处加上 break 就可以达到我们期望的效果。这个功能点稍显“坑爹”,也是初学者常犯的错误。
4、ArrayList遍历删除时报错
如下代码所示:
public static void main(String[] args)
{
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
for(String s : list)
{
if(s.equals("c"))
{
list.remove(s);
}
}
}
上述代码会报错,错误的原因:这种for-each写法会报出著名的并发修改异常:java.util.ConcurrentModificationException,正确的写法应该采用迭代器,如下所示:
Iterator<String> it = list.iterator();
while (it.hasNext())
{
String s = it.next();
if (s.equals("b"))
{
it.remove();
}
}
5、Integer类有缓存
这个功能点也是面试的高频热点之一,稍不注意,也有可能被带入沟里,我们看看下面这段代码:
public static void main(String[] args){
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a==b);
System.out.println(c==d);
}
上面的代码竟然输出:
true
false
这确实太出乎意料了,同样的代码,只是数值不同(而且差别不太大的样子),就产生了不一样的输出,这也太离谱了。原来,Integer中有一个静态内部类IntegerCache,在类加载的时候,它会把[-128, 127]之间的值缓存起来,而Integer a = 100这样的赋值方式,会首先调用Integer类中的静态valueOf方法,这个方法会尝试从缓存里取值,如果在这个范围之内就不用重新new一个对象了,否则会重新生成新的Integer对象:
public static Integer valueOf(int i)
{
if (i >= IntegerCache.low && i <= IntegerCache.high)
{
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}
6、a+b+c不等于c+b+a
这是一个考察数据溢出和数据类型题目,可以分情况考虑:
(1)如果a,b,c都是String类型,就会有a+b+c != c+b+a
(2)如果a,b,c都是数据类型,使用数据溢出来使a+b+c!=c+b+a。例如a,b为窄的数据类型,c为宽的数据类型,一个窄的数据类型与宽的数据类型运算,会得到一个宽的数据类型。例如:
long a = 1L;
int b = Integer.MAX_VALUE;
int c = Integer.MAX_VALUE;
(a+b+c) != (c+b+a))
7、InterruptedException 异常 or 信号 ?
这种异常不同于普通的异常,甚至这个就不算是异常,这个是信号,不要看到异常就感觉是出问题了,这个是人畜无害的信号。Java的创造者真是费尽心机啊,将信号融入了异常。
try
{
TimeUnit.SECONDS.sleep(2);
}
catch (InterruptedException e)
{
//收到信号之后,可以做些其他的操作
}
8、Fail-Fast机制是多线程原因造成的吗?
8.1、Fail-Fast介绍
Fail-Fast机制是Java集合(Collection)中的一种错误机制。通俗观点是这样认为:
当多个线程对同一个集合的内容进行操作时,就可能会产生Fail-Fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生Fail-Fast事件。
果真如此吗?Fail-Fast是多线程造成吗?请看下文的分析。
8.2、Fail-Fast举例
8.2.1、单线程情况下
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test
{
public static void main(String[] args)
{
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++)
{
list.add(i);
}
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext())
{
int temp = iterator.next();
if (temp == 3)
{
list.remove(temp);
}
}
}
}
该段代码定义了一个Arraylist集合,并使用迭代器遍历,在遍历过程中,刻意在某一步迭代中remove一个元素,这个时候就会发生Fail-Fast。
8.2.2、多线程情况下
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test
{
public static List<Integer> mylist = new ArrayList<>();
private static class MyThread1 extends Thread
{
@Override
public void run()
{
Iterator<Integer> iterator = mylist.iterator();
while (iterator.hasNext())
{
int temp = iterator.next();
if (temp == 3)
{
mylist.remove(temp);
}
}
}
}
private static class MyThread2 extends Thread
{
public void run()
{
Iterator<Integer> iterator = mylist.iterator();
while (iterator.hasNext())
{
int temp = iterator.next();
if (temp == 4)
{
mylist.remove(temp);
}
}
}
}
public static void main(String[] args)
{
for (int i = 0; i < 10; i++)
{
mylist.add(i);
}
MyThread1 thread1 = new MyThread1();
MyThread2 thread2 = new MyThread2();
thread1.setName("thread1");
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}
该段代码定义了一个Arraylist集合,启动两个线程并使用迭代器遍历,在遍历过程中,刻意在某一步迭代中remove一个元素,这个时候也会发生Fail-Fast。
8.3、Fail-Fast的原理
Fail-Fast是如何抛出ConcurrentModificationException异常的,又是在什么情况下才会抛出?
我们知道,对于集合如List,Map类,我们都可以通过迭代器来遍历,而Iterator其实只是一个接口,具体的实现还是要看具体的集合类中的内部类去实现Iterator并实现相关方法。这里我们就以ArrayList类为例。在ArrayList中,当调用list.iterator()时,其源码是:
public Iterator<E> iterator()
{
return new Itr();
}
即它会返回一个新的Itr类,而Itr类是ArrayList的内部类,实现了Iterator接口,下面是该类的源码:
private class Itr implements Iterator<E> {
int cursor = 0;
int lastRet = -1;
int expectedModCount = modCount;//修改数的记录值。
}
需要注意:每次新建Itr()对象时,都会保存新建该对象时对应的modCount,这个值表示List当时的修改次数,但是List可能会不断修改的,modCount也在变化,所以以后每次遍历List中的元素的时候,都会比较expectedModCount和modCount是否相等。若不相等,说明List中的元素已经发生了变化,再照之前的状态遍历显然是不对的,故抛出ConcurrentModificationException异常,产生Fail-Fast事件。
8.4、Fail-Fast小结
Fail-Fast的发生条件与是否多线程环境并没有什么关系。Fail-Fast属于多副本数据一致性机制,非多线程安全机制。
9、面向过程编程与面向对象编程(函数转内部类)
此内容仅限站长收徒。
10、CopyOnWrite
此内容仅限站长收徒。
后记
站长收徒,2020年下半年已经开启,欢迎关注!