前言
对于Java初学者来言,一些代码真是整的我们云山雾罩的……
有这样的:
public class Test {
static {
System.out.println("static yo~");
}
}
这样的:
public class Test {
{
System.out.println("oh yo~");
}
}
还有这样的:
public void testMethod() {
{
System.out.println("oh yo yo yo~");
}
}
(注意,第二个是在类中,而这个是在方法中)
甚至还有……这样的?
@Override
public void run() {
synchronized (this) {
for(int i=0; i<3; i++) {
System.out.println("test i = " + i + " yo~ " + Thread.currentThread().getName());
/*try {
Thread.sleep(3000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}*/
}
}
}
看着很奇怪是叭,这些是个啥子?起啥子作用?啥子时候执行?跟普通的代码又有啥子区别?
今天我就要来探究,这些被称之为代码块的东西,它究竟是个啥子?
普通代码块
普通代码块,就是我们所见的放在方法中的,不加任何修饰符的代码块:
public void testMethod() {
{
System.out.println("oh yo yo yo~");
}
}
那这样写有啥用捏?又是为了啥捏?
别急,我们先来看下面一段代码:
public void testA() {
{
int a = 1;
}
int b = 2;
int c = 3;
}
public void testB() {
int a = 1;
int b = 2;
int c = 3;
}
乍一看,没…没区别啊?(睁着眼睛说瞎话,那不多一组大括号嘛!)
俗话说,透过现象看本质,那本质是啥捏?
我们试着对这个代码反编译一下,使用javap -verbose Test.class
,可得到字节码指令与局部变量表信息:
public void testA();
Code:
stack=1, locals=3, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_1
4: iconst_3
5: istore_2
6: return
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this LTest;
4 3 1 b I
6 1 2 c I
public void testB();
Code:
stack=1, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iconst_3
5: istore_3
6: return
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this LTest;
2 5 1 a I
4 3 2 b I
6 1 3 c I
哎?testA
方法的LocalVariableTable
好像缺了点啥子……
a
呢?a
咋没了?看这两条指令:
0: iconst_1
1: istore_1
先将常量1
压入操作数栈,然后出栈推入局部变量区slot = 1的位置,也就是说,a
应该占用slot = 1的位置啊,可是现在那个位置上是什么?是变量b
,这是为啥?
其实这就是代码块的作用,在代码块中的变量只在代码块中生效。
我们来看下面一段代码:
public class Test {
public static void main(String[] args) {
{
int a = 1;
System.out.println("a = " + a);
}
}
}/* Output
a = 1
*/
很正常的打印出来了结果,可是如果把访问变量a
的语句放在代码块外面呢?
public class Test {
public static void main(String[] args) {
{
int a = 1;
}
System.out.println("a = " + a);
}
}/* Output
Test.java:8: 错误: 找不到符号
System.out.println("a = " + a);
^
符号: 变量 a
位置: 类 Test
1 个错误
*/
其实这个结果我们已经猜到了,如果在代码块外访问代码块内定义的变量,根本访问不到。
所以普通代码块(或者称为局部代码块)的作用是:限定变量的生命周期,及早的释放无用变量(因为通常局部变量的生命周期包含整个方法域,在方法结束后才会进行回收),提高内存的利用率。
(注意,在方法体内定义的代码块才被称为局部代码块)
那么上面的反编译指令中,局部变量表中为什么变量b
占用了变量a
的slot我们也能理解了,因为变量a
在占用slot
后作用域结束立即被释放,而变量b
对变量a
的slot进行了复用。
关于局部变量表的具体信息可以参考我的另一篇文章:
Java 虚拟机初探(二)—— 虚拟机栈
这里不过多深入。
静态代码块
静态代码块就是在普通代码块的前面加上static
的修饰符,它出现的位置应该是java类中,方法体之外:
public class Test {
static {
System.out.println("static yo~");
}
}
静态代码块就像静态变量一样,其会在类加载时运行,而且只运行一次。
一个类中可以包含多个静态代码块,其会按照书写顺序依次执行。
那么静态代码块的作用是起什么作用的呢?
如果说,在项目启动时你有许多的配置文件要加载,或者你要建立连接的时候(比如JDBC连接数据库的时候),就可以使用静态代码块进行配置,这些代码会随着项目的启动(一般与项目相关的类都会进行加载,除非你根本没有用到这个类)而进行执行。
tips:静态代码块不能访问普通变量和普通方法,可以访问静态变量和方法。
构造代码块
既然有方法体内的代码块,那肯定有java类中方法体之外的代码块啦,将静态代码块的static
去掉,就变成了现在这个——构造代码块。
public void testMethod() {
{
System.out.println("oh yo yo yo~");
}
}
是不是感觉跟普通代码块长得一模一样?不过他们的作用可是完全不同,而且一定要注意,在方法体内定义的是普通代码块,而方法体外的是类的构造代码块。
构造代码块,顾名思义,它是用来对对象进行初始化的。
那你肯定有一个问题了,构造函数不就是干这个的,还要构造代码块干嘛?
所谓存在必有其道理,我们先来看构造代码块执行的时机:
public class Test {
{
System.out.println("This is Test init.");
}
Test() {
System.out.println("This is Test construction method.");
}
public static void main(String[] args) {
Test test = new Test();
}
}/* Output
This is Test init.
This is Test construction method.
*/
发现构造代码块中的代码先于构造函数执行,我们对其反编译看一下:
Test();
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String This is Test init.
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #5 // String This is Test construction method.
17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: return
可以看到,字节码指令中是先执行的invokespecial
(根据继承链调用Object
的初始化方法),然后再执行的构造代码块中的代码。
也就是说,构造代码块与相应的构造方法合并了,换句话说,就是构造代码块依托于构造方法执行,且先执行构造代码块中的代码,再执行相应的构造方法。
可是我还是没有看到它究竟有什么用啊?别着急,这只是一个构造方法的情况,如果是两个呢?
我们在程序中加一个构造方法,然后调用其中一个,结果会怎样?
public class Test {
{
System.out.println("This is Test init.");
}
Test() {
System.out.println("This is first Test construction method.");
}
Test(int a) {
System.out.println("This is second Test construction method.");
}
public static void main(String[] args) {
Test test = new Test(1);
}
}/* Output
This is Test init.
This is second Test construction method.
*/
这回,构造代码块和第二个构造方法结合起来了,没第一个构造方法什么事了。
可见,构造代码块在对象被创建时,是一定会执行的,而构造方法却不一定被执行。
那么构造代码块的作用也就呼之欲出了,初始化!没错,就是初始化!
看下面这个场景:
public class Test {
int a;
int b;
{
a = 1;
b = 2;
}
Test() {
System.out.println("This is a = " + a);
System.out.println("This is b = " + b);
}
Test(int a, int b) {
this.a = a;
this.b = b;
System.out.println("This is a = " + a);
System.out.println("This is b = " + b);
}
public static void main(String[] args) {
Test test = new Test();
}
}/* Output
This is a = 1
This is b = 2
*/
构造代码块担当了无论调用哪个构造方法都会为类属性初始化的角色,同时因为它每次创建对象必被调用的性质,它可以用来作为创建对象次数的计数器。
同步代码块
同步代码块就是上面讲的最后一个代码块:
@Override
public void run() {
synchronized (this) {
for(int i=0; i<3; i++) {
System.out.println("test i = " + i + " yo~ " + Thread.currentThread().getName());
/*try {
Thread.sleep(3000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}*/
}
}
}
同步代码块的使用主要跟线程相关,它是共用同一把锁的代码块(也就是说其访问互斥)。
同步代码块传入的参数称为同步监视器(锁),当线程开始执行同步代码块时必须先获得其对应锁,执行完成后对锁进行释放。
我们学线程的时候一定都知道同步方法,那么同步方法和同步代码块有什么区别呢?
我们来看同步方法的格式:
public synchronized void testMethod()
{
//需要被同步的代码块
}
发现了区别了吗?同步方法是不用指定锁的!
- 同步方法默认使用
this
或者当前类作为锁。 - 而同步代码块可以显式地决定以什么来加锁,这样的做法会更加灵活。
我们完全可以选择只在同步问题发生之处用同步代码块加锁,而不用对整个方法加锁,这就是同步代码块的作用。
tips:同步代码块的锁要在run()
方法外面声明。
这里拓展一下,使用同步代码块一定要注意死锁的问题,看下面一种场景:
synchronized(A){
synchronized(B);
}
synchronized(B){
synchronized(A);
}
同步发生嵌套时,双方都持有对方所需的资源,就会发生死锁。
所以,建议用Lock
类来进行同步代码的加锁。
那么synchronized
和Lock
的区别在哪里?
- 从锁的释放时机来说:
synchronized
在获取锁的线程执行完代码,或者线程执行发生异常时,才会释放。(或者主动调用wait()
方法)- 而
Lock
为了避免线程死锁,规定必须在finally
中手动释放锁。
- 从锁的获取时机来说:
synchronized
在线程A获取锁之后,线程B只能等待,哪怕线程A阻塞。- 而
Lock
为避免这种情况有多种获取锁的手段,比如tryLock(long time, TimeUnit unit)
方法(只等待一段时间),或者lockInterruptibly()
方法(能够响应中断)。
- 并且
Lock
中有特定的方法tryLock()
可以判断锁的状态(返回值类型为boolean
),此方法的作用为:仅在锁为空闲时才获取该锁,如果锁被占用则返回false
,而synchronized
是无法判断锁状态的。
这里不对Lock
做过多深入,感兴趣的可以自己查阅相关资料。
总结
其实各种不同的代码块,只是因为修饰符与位置的不同展现出了不同的特性。
从执行顺序来说,静态变量初始化 > 静态代码块 > 构造代码块 > 构造方法 > 普通代码块。
上面没有演示静态代码块和构造代码块执行顺序,但是想一下,静态代码块在类加载的时候就执行,并且只执行一次,就可以知道静态代码块一定是先于构造代码块执行的,就像静态变量初始化一样。
至于静态变量初始化的时机,我截取了反编译后的一段字节码指令:
static {};
Code:
stack=2, locals=0, args_size=0
0: iconst_1
1: putstatic #7 // Field a:I
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #8 // String This is Test.static
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
可以看到,就像构造代码块和构造方法一样,将静态变量的初始化加入到了static
代码块执行中,在类进行static
初始化的时候,是先进行静态变量初始化,之后执行静态代码块中的语句的。
在合适的地方用合适的代码块确实会事半功倍,但千万不要滥用以避免代码混乱,先规划好功能,了解各个代码块的作用,于最适合其功能的地方用才是最佳的选择。