java豆知识

一些零散的java知识点。

关于泛型

似乎Class<? extends Xface>Class<Xface>在很多情况下是等价的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static void main(String[] s) throws Exception {
Class<? extends TestInterface<String>> cls = getClass(String.class);
// 这两种写法都是正确的
// Class<TestInterface<String>> cls = getClass(String.class);
cls.newInstance().test("StringTest");
// 注释掉的这段代码也是能正常编译的,运行时才会报类转换错误,Map不能转成String
// 因为是伪泛型吧,泛型只在编译期检查。只要编译不出错,getClass返回的必定StringTest.class
// Class<TestInterface<Map>> cls = getClass(Map.class);
// cls.newInstance().test(new HashMap());
}
public static interface TestInterface<T> {
public void test(T param);
}
public static class StringTest implements TestInterface<String> {
@Override
public void test(String param) {
System.out.println(param);
}
}
// StringTest.class不能直接转换成Class<TestInterface<T>>,而是要先转换为Class<? extends TestInterface<T>>
// 只有Class<? extends TestInterface<T>>才能转换成Class<TestInterface<T>>
// 所以又写了个getClass2方法做“中转”
public static <T> Class<TestInterface<T>> getClass(Class<T> cls) {
return (Class<TestInterface<T>>) getClass2(cls);
}
public static <T> Class<? extends TestInterface<T>> getClass2(Class<T> cls) {
return (Class<? extends TestInterface<T>>) StringTest.class;
}

Class.newInstance

有些区别以前就知道,比如Class.newInstance要求必须有无参的public构造函数,而Constructor.newInstance更灵活,利用反射的手段还可以调用私有的构造函数。
但为何不推荐用Class.newInstance?因为二者对异常处理的差别。见:http://stackoverflow.com/questions/195321/why-is-class-newinstance-evil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static final Class<?>[] EMPTY_ARRAY = new Class[] {};
public static class TestReflect {
// 这个类的构造函数会抛出IOException
public TestReflect() throws IOException {
if (System.currentTimeMillis() > 0) {
throw new IOException();
}
}
}
public static void main(String[] s) {
Class<TestReflect> cls = TestReflect.class;
// 这个try-catch看似捕获了所有异常,其实没有
try {
cls.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
// 这条语句根本不会执行
System.out.println("end of main");
}

上面这段代码编译是没问题的,但运行时会由于IOException异常终止。正常情况下IOException必须被捕获并处理的(只有RuntimeException及其子类可以不被捕获),但Class.newInstance绕过了编译期的这个检查。
相当于IOException直接被抛到最上层,程序直接终止,后面的语句都不会执行。而且打印出的堆栈信息也有问题:

1
2
3
4
5
6
7
8
9
10
Exception in thread "main" java.io.IOException
at alg.Test2$TestReflect.<init>(Test2.java:21)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at java.lang.Class.newInstance0(Class.java:355)
at java.lang.Class.newInstance(Class.java:308)
at alg.Test2.test(Test2.java:55)
at alg.Test2.main(Test2.java:46)

堆栈给出的异常代码是构造函数,而不是Class.newInstance,debug时会非常麻烦。

Constructor.newInstance会将所有异常包装成一个InvocationTargetException再次抛出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Class<TestReflect> cls = TestReflect.class;
try {
Constructor<TestReflect> c = cls
.getDeclaredConstructor(EMPTY_ARRAY);
c.newInstance();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// 这个语句会被执行
System.out.println("end of main");

异常堆栈:

1
2
3
4
5
6
7
8
9
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at alg.Test2.main(Test2.java:33)
Caused by: java.io.IOException
at alg.Test2$TestReflect.<init>(Test2.java:21)
... 5 more

这个异常信息清晰很多,也说明了异常的语句确实是newInstance那一步。

综上,确实应该尽量少用Class.newInstance

volatile

这篇文章讲的比较详细:http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

简言之,每个线程有自己的本地内存和共享的主内存(只是逻辑上的概念)。线程在赋值a=1时,只是将本地内存中的值修改,并不保证将修改刷新到主内存中。
如何保证刷新主内存?1.加synchronized关键字。线程在离开synchronized语句块时,会自动刷新,JVM强制。2.使用volatile关键字。volatile的变量可以保证读写操作是原子性的,会立即刷到主内存。
volatile保证对某个变量(其实就是某块内存地址)的读写都是原子性的。必定不会有多个线程同时读volatile变量、或者同时写volatile变量。所以可以用作简单的同步机制。
i++这种操作,即使加了volatile不能保证原子性。i++可以分解为三步:读/增加/写回。第一步的读和第三步的写是原子性的。但整体不是原子性。
volatile一般用于基本类型,int/boolean之类,其实也可以用于对象,见文中的例子。但这种用法非常容易出错,还是不要用的好,给自己找麻烦。
用volatile要仔细思考,否则得不偿失,只为了一点点性能提升,还不如用synchronized直观。

transient

这个关键字感觉很少见。
只在实现Serializable接口的类中有用,被这个关键字修饰的变量不会序列化。
不过本来java自带的序列化机制就用的不多吧。。。
感觉这是个历史遗留问题,要实现这种功能完全可以用注解,就像Jackson JSON一样,没必要新增个关键字。

wait()

看到一个比较有意思的问题。作者最后提的几个问题比较有意思。

第一个问题,代码中的while()不能换成if()。根源还是在于notifyAll不能精确唤醒某个线程。notifyAll会唤醒所有等待的线程,但其中有些线程是”目标”,要输出数据.有些线程只是”误伤”,需要继续wait。 所以即使唤醒后也要在while循环里继续判断是否wait。
改成if的话,各个线程的执行顺序无法确定了,有可能死锁。如果线程3先执行完毕,线程2获得锁,把state改为3,线程1和线程2就只能一直等待。

第二个问题,notifyAll也不能改成notify。notify是随机唤醒一个等待的线程。可能死锁。比如线程1设置state=2,然后notify,假设线程3被唤醒,发现state不是3,继续wait。接着线程1获得锁,发现state不是1,也wait。于是3个线程都处于wait状态。

几个注意点:wait()一般都要在while循环里调用;sth.wait()的线程会释放sth对象的锁,在唤醒前不会再参与锁的竞争;sth.wait()的前提是当前线程持有sth对象的锁,否则会抛IllegalMonitorStateException异常;wait()的线程被唤醒后,会重新尝试获得锁,然后从wait()开始执行。

wait/notify是比较底层的线程同步手段,能不用尽量不用。concurrent包提供了很多替代品。

题外话:1.JVM中的所有对象都有一个隐含的锁,或者叫监视器(monitor),包括XX.class对象。synchronized的原理就是利用这个;2.JVM中的线程都是映射到系统内核本身的线程机制的。

CountDownLatch

最近用CountDownLatch碰到一个死锁的问题。代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 线程池
private static ExecutorService exec = Executors
.newCachedThreadPool(new ThreadFactory() {
public Thread newThread(Runnable runnable) {
Thread thread = Executors.defaultThreadFactory().newThread(
runnable);
thread.setDaemon(true);
return thread;
}
});
// main函数
public static void main(String[] args) throws IOException,
InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
exec.submit(new PutRunner(conf,latch));
}
latch.await();
}
// runnable
private static class PutRunner implements Runnable {
private Configuration conf = null;
private CountDownLatch latch = null;
public PutRunner(Configuration conf, CountDownLatch latch) {
this.conf = conf;
this.count = count;
}
public void run() {
try {
// do something
} catch (IOException e) {
e.printStackTrace();
}
latch.countDown();
}
}

运行这个程序的时候,有时能正常结束,有时就会死锁。debug了半天,请教了同事才明白。
PutRunner.run()方法里将latch.countDown()放到最后,前面的try-catch看似捕获了所有的异常(checked exception),但如果有RuntimeException(unchecked exception)线程会直接终止,导致latch.countDown()不会执行,于是main线程一直卡在latch.await()不能结束。
坑爹的是这个RuntimeException会被吃掉。。。在命令行中根本看不到任何异常信息。一个类似的问题见StackOverflow
catch (IOException e)改成catch (Exception e),就可以了。当然也有其他更优雅的解决方法。

NoClassDefFoundError vs ClassNotFoundException

java的classpath机制还是比较头疼的,最常见的两个错误就是NoClassDefFoundErrorClassNotFoundException,关于二者的区别可以参考这个帖子

从原理上说,jvm会从classpath中寻找class并加载到内存中。在使用一个类时,会先从内存中找,然后从classpath中找。如果“内存-classpath”这个链路中找不到,或者找到后出了其他错误,抛NoClassDefFoundError;如果从classpath找不到,抛ClassNotFoundException。注意二者之间微妙的联系:很多情况下,NoClassDefFoundError是由ClassNotFoundException引起的,比如下面这种(这个错误是由于编译时classpath中有guava,但运行时classpath中没有导致的):

1
2
3
4
5
6
7
Exception in thread "main" java.lang.NoClassDefFoundError: com/google/common/collect/Maps
at com.vdian.stm.test.TestDO.main(TestDO.java:67)
Caused by: java.lang.ClassNotFoundException: com.google.common.collect.Maps
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)

但NoClassDefFoundError也不一定全是由ClassNotFoundException引起。如果classpath能找到对应的类,但初始化类的过程中出错,也会出现NoClassDefFoundError:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws Exception {
// 第一次调用,出现java.lang.ExceptionInInitializerError
try {
new TestStatic();
} catch (Throwable e) {
e.printStackTrace();
}
// 第二次调用,出现java.lang.NoClassDefFoundError: Could not initialize class leetcode.TestStatic
try {
new TestStatic();
} catch (Throwable e) {
e.printStackTrace();
}
// 第三次调用,结果跟第二次调用一样
try {
new TestStatic();
} catch (Throwable e) {
e.printStackTrace();
}
}
TestStatic.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestStatic {
// 无论是静态方法还是静态成员变量,都会导致这个类初始化出错
// 有趣的是抛出的异常是跟static代码的顺序有关的
// 如果这段代码先运行,会抛出RuntimeException
static {
if (System.currentTimeMillis() > 0)
throw new RuntimeException("hahaha");
}
// 如果这段代码先运行,会抛出ArithmeticException
static int undefined = 1 / 0;
}

错误信息:

1
2
3
4
5
6
7
8
9
java.lang.ExceptionInInitializerError
at com.vdian.stm.test.TestDO.main(TestDO.java:69)
Caused by: java.lang.RuntimeException: hahaha
at leetcode.TestStatic.<clinit>(TestStatic.java:16)
... 1 more
java.lang.NoClassDefFoundError: Could not initialize class leetcode.TestStatic
at com.vdian.stm.test.TestDO.main(TestDO.java:75)
java.lang.NoClassDefFoundError: Could not initialize class leetcode.TestStatic
at com.vdian.stm.test.TestDO.main(TestDO.java:81)

另外注意异常信息中的<clinit>,这是一个特殊的标志,表示初始化某个类的static方法和变量。类似的还有一个<init>,表示调用类的构造函数。

从使用场景上来说:

  1. 如果某个类是编译时需要的并且编译成功:
    1.1 运行时不存在,一般会抛NoClassDefFoundError + 类名,第一次调用时会有个Caused by ClassNotFoundException
    1.2 运行时存在但类初始化出错,第一次使用该类时出现ExceptionInInitializerError,以后再使用时就会抛NoClassDefFoundError: Could not initialize class + 类名
  2. 如果某个类是通过反射加载的(换句话说,不需要编译时存在),比如Class.forName
    2.1 运行时不存在,直接抛ClassNotFoundException(这些反射的方法会跳过内存中查找的步骤,直接从classpath中查找)
    2.2 运行时存在但类初始化出错,跟1.2是一样的情况

Java Cookies

一些有用的小技巧

1
2
3
4
5
6
7
8
9
10
11
// java.nio.file.Paths
Path path = Paths.get("c:\\data\\myfile.txt");
Path.normalize() // 将路径标准化,去除./..等
// java.nio.file.Files
// guava中也有一个Files工具类
Files.createDirectory()
Files.copy()/move()/delete()
// java.nio.channels.Channels
// 很有意思的工具类,用于channel和stream直接的转换

除了guava外,netty可以提供了很多有意思的工具类:
http://netty.io/wiki/using-as-a-generic-library.html
其中的Listenable Future、对象池感觉比较有用

接口的默认实现

Java中的interface中只能定义方法签名,不能定义方法体,但Java8中这个限制被打破了,接口中可以带默认实现,称作default方法:
https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html

一个例子就是JDK自带的java.util.Iterator

Apache vs Tomcat

他们的区别是啥?经常有同学问到这个问题。。。网上也有很多解答,但都比较笼统。
“既然php可以通过mod_php在Apache中使用,为啥没有一个mod_java?”
“既然tomcat已经能处理http请求了,为何前端还要一个Apache/Nginx?”

回答这些问题需要先去考下古。
Apache(httpd)出现的比较早,第一个版本是1995年的,是专门处理http请求的服务端软件。概括的说,对http请求主要有3种处理方式:1. 如果是静态文件直接返回;2. 结合CGI动态生成内容;3. 转发请求给其他host。
所谓的CGI,是一种很古老的技术,让各种语言都能处理http请求。简单的说,每次Apache接收到一个请求,就会fork-and-join产生一个新的进程,这个进程的标准输入/标准输出就对应于socket的输入/输出,还有其他一些环境变量也对应于各种参数,比如IP/UA之类。绝大多数语言都可以实现CGI,毕竟处理标准输入输出/环境变量是所有语言的标配。
php的mod_php就是这个原理。
这就是所谓的多进程模型,但多进程时毫无疑问吞吐量会有问题。于是有了所谓的FastCGI,大概原理就是一个进程池,不用为每个请求都新建一个进程了。后续还有所谓的SCGIWSGI等衍生。

既然如此,为什么不直接用java+CGI去处理http请求呢?还真有这么做的:
https://opensource.apple.com/source/FastCGI/FastCGI-4/fcgi/doc/fcgi-java.htm
注意下这篇文章是1996年的,而且还是苹果的,真是少见。。。96年的时候,JDK还只是1.0版本,Servlet规范还没出现(Servlet 1.0规范是97年6月提出的),如果想用java处理http,确实也只能用CGI。估计当时的java对服务端也没什么想法吧,感觉像玩具一样的语言,在前端做各种applet。。。直到JDK1.2,Sun才提出了J2EE,也就是现在的JavaEE,开始发力服务端。

那么,为啥没发展出所谓的mod_java呢?看上面文章中的代码就知道,用CGI去处理http非常“不OO”,这跟java的理念是完全相悖的。而且这是一种比较“底层”的API,使用不方便,维护、扩展都有很多问题,很多特性(比如session处理)都需要另外去补充。而且java是基于多线程模型去处理请求的,跟Apache的多进程模型也不同。当然也不能忽略Sun的推动与宣传。

不管怎样,java+CGI没有成为主流,servlet成了java世界的标准,应该说这是历史的选择?从设计上来讲Servlet可以支持各种类型的请求,但实际中绝大多数实现都是用于处理http请求。Tomcat就是所谓的Servlet容器,符合servlet规范的java工程,直接部署到tomcat中即可运行。

那么,为啥不能用tomcat直接处理所有请求?为啥要加一层Apache/Nginx?从理论上来说,是可以用tomcat处理所有请求的,但还要考虑到效率的问题。虽然Apache“结合CGI动态生成内容”这个功能被废掉了,但“处理静态文件”和“转发请求”这两个功能还在啊,可以用来减轻tomcat的负载,还能实现负载均衡等功能。
单纯说处理静态请求的话,各种Servlet容器还是不如Apache/Nginx,毕竟不是专门干这个的。虽然也能用DefaultServlet处理静态文件,但多了一层Servlet的处理逻辑,性能上总会有些损耗。

一篇比较好的介绍Servlet的文章:
https://www.ibm.com/developerworks/cn/java/j-lo-servlet/