详解JAVA中的关键字

在平时编码中,咱们可能只注意了这些static,final,volatile等关键字的使用,忽略了他们的细节,更深层次的意义。java

本文总结了Java中全部常见的关键字以及一些例子。react

 

static 关键字


概述:缓存

当static修饰类的属性或者方法时,那么就能够在没有建立对象的状况下使用该属性或方法。多线程

静态块也是static的一个应用,用于初始化类时的一些操做。dom

 

静态方法和静态变量

划重点ide

被static修饰后的属性或者方法,使用时不须要new 一个类,用.属性名或方法名访问.工具

好比java.lang.Math就存放了不少静态资源,能够直接使用Math.random()来获取随机数.学习

 

一些须要注意的地方测试

非静态方法是能够访问静态资源的,优化

静态方法是不能引用非静态资源的。

来看一个代码实例:

复制代码
1 public class TestStatic {
2 
3     protected int i = 100;
4 
5     public static void main(String args[]){
6         System.out.println(i);
7     }
8 }
复制代码

 

在以上代码,编译的时候会出错,main方法是静态方法,变量i是非静态的。

解决办法是,将变量i加上static修饰。

 

不经就要提出一个问题,

为何非静态方法能够访问静态资源,而静态方法不能访问非静态资源呢?

 从类加载机制上讲,静态资源是类初始化的时候加载的,而后非静态资源是new一个该类的对象的时候加载的。

 

这就带来一个问题:

加载类时默认先加载静态资源的,当new一个对象以后,才会加载其余资源,因此在new对象以前,静态资源是不知道类有哪些非静态资源的,

可是当对象new出来以后,该类的全部属性和方法都知道。

 

还有须要注意的是:

1.静态属性和方法能够经过.属性名或方法名,并且,该类的对象也是访问静态属性和变量的。

2.Java的语法规定,static不能修饰局部变量。没有为何,这就是规定。

 

静态块

静态块和静态变量、静态方法是没什么区别的,也是在类加载的时候执行,并且只执行一次。

关于静态块有两点须要注意:

1.静态资源的加载顺序严格按照静态资源的定义顺序加载的

2.静态块,对于定义在它以后的静态变量,能够赋值但不能访问。

static的题目

下面main()方法的输出结果是什么:

复制代码
public class InstanceClass extends ParentClass{

    public static String subStaticField = "子类静态变量";
    public String subField = "子类非静态变量";
    public static StaticClass staticClass = new StaticClass("子类");

    static {
        System.out.println("子类 静态块初始化");
    }

    {
        System.out.println("子类 [非]静态块初始化");
    }

    public InstanceClass(){
        System.out.println("子类构造器初始化");
    }

    public static void main(String args[]) throws InterruptedException {
        new InstanceClass();
    }
}

class ParentClass{
    public static String parentStaticField = "父类静态变量";
    public String parentField = "父类[非]静态变量";
    public static StaticClass staticClass = new StaticClass("父类");

    static {
        System.out.println("父类 静态块初始化");
    }

    {
        System.out.println("父类 [非]静态块初始化");
    }

    public ParentClass(){
        System.out.println("父类  构造器初始化");
    }
}

class StaticClass{
    public StaticClass(String name){
        System.out.println(name+" 静态变量加载");
    }
}
复制代码

 

 

输出结果:

  View Code

 

 

下面是我总结类加载流程,能够对照着这个流程,能够再从新看一下上面的例子,会有新的理解。

复制代码
1. 加载父类静态
    1.1 为静态属性分配存储空间并赋初始值
    1.2 执行静态初始化块和静态初始化语句(从上至下)

2. 加载子类静态
    2.1 为静态属性分配存储空间
    2.2 执行静态初始化块和静态初始化语句(从上至下)

3. 加载父类非静态
    3.1 为非静态块分配空间  
    3.2 执行非静态块

4. 加载子类非静态
    4.1 为非静态块分配空间  
    4.2 执行非静态块

5. 加载父类构造器
    5.1 为实例属性分配存数空间并赋初始值
    5.2 执行实例初始化块和实例初始化语句
    5.3 执行构造器内容

6. 加载子类构造器
    6.1 为实例属性分配存数空间并赋初始值
    6.2 执行实例初始化块和实例初始化语句
    6.3 执行构造器内容
复制代码

 

 

对照着刚才的规则,再看一下这个例子:

复制代码
 1 public class TestStaticLoad {
 2     Person person = new Person("TestStaticLoad");
 3     static{
 4         System.out.println("TestStaticLoad static");
 5     }
 6 
 7     public TestStaticLoad() {
 8         System.out.println("TestStaticLoad constructor");
 9     }
10 
11     public static void main(String[] args) {
12         new God();
13     }
14 
15 }
16 
17 class Person{
18     static{
19         System.out.println("person static");
20     }
21     public Person(String str) {
22         System.out.println("person "+str);
23     }
24 }
25 
26 
27 class God extends TestStaticLoad {
28     Person person = new Person("God");
29     static{
30         System.out.println("God static");
31     }
32 
33     public God() {
34         System.out.println("God constructor");
35     }
36 }
复制代码

 

输出结果:

  View Code

 

一步一步地解析:

  • 在TestStaticLoad 的main方法中,执行了new God(),那就就会去加载God类,在这以前会先加载它的父类:TestStaticLoad
  • 第一步:加载父类静态,执行System.out.println("TestStaticLoad static");  输出:TestStaticLoad static,
  • 第二步:加载子类静态,执行System.out.println("God static");,输出God static
  • 第三步:加载父类非静态,Person person = new Person("TestStaticLoad");,这里实例化了Person 对象,那就会去加载Person类。
  • 第四步:加载Person类,首先看有没有父类,没有。好,加载静态块,执行System.out.println("person static");输出person static
  • 第五步:Pernson类静态块加载完毕,加载构造器,new一个Person对象,输出person TestStaticLoad。这时TestStaticLoad 类非静态块加载完毕
  • 第六步:加载God 父类(TestStaticLoad )构造器,输出TestStaticLoad constructor
  • 第七步:God父类所有加载完毕,加载God的非静态块,Person person = new Person("God");这时又会去加载Person类,须要注意的是,static块只加载一次,由于以前在父类已经加载过了,这时只加载构造器,输出person God
  • 最后一步:加载本类God 的构造器,输出God constructor。

 

 static关键字的总结:

  1. static关键字 能够再没有建立对象的时候进行调用类的元素
  2. static 能够修饰类的方法 以及类的变量, 以及静态代码块
  3. 被static修饰的成为静态方法,静态方法是没有this的,静态方法不能访问同一个类中的非静态方法和静态变量,可是非静态方法 能够能够访问静态变量
  4. 类的构造器 也是静态的
  5. 静态变量被全部的内存全部的对象共享,在内存中只有一个副本。非静态变量是是在建立对象的时候初始化的,存在多个副本,每一个副本不受影响。
  6. static 静态代码块,static 代码块能够放在类中的任何地方,类加载的时候会按照static代码块的顺序来加载代码块,而且只会执行一次。
  7. 枚举类和静态代码块 赋值静态代码块的变量
  8. 非静态方法可以经过this访问静态变量
  9. 静态成员变量虽然独立于对象,可是不表明不能够经过对象去访问,全部的静态方法和静态变量均可以经过对象访问。
  10. static不能够修饰局部变量(java语法规定)

没想到static能有这么多须要注意的,能够说Java中的语法仍是有不少能够深究的.

 

final 关键字


概述:

final关键字,在平时的过程当中也是很常见的,在这里进行一下深刻的学习,加深对final关键字的理解。

 使用注意点:

1.在java中final能够用来修饰类、方法、和变量(包括成员变量和局部变量)

2.final修饰类的时候,这个类将永远不会被继承,类中的成员方法也会被隐式的修饰为final(尽可能不要用final修饰类)

3.若是不想方法被继承,能够用final修饰,private也会隐式的将方法指定为final

4.final修饰变量的时候,若是是基本类型的变量,那么他的值在初始化以后就不能更改

5.final在修饰对象的时候,在其初始化以后就不能指向其余对象

6.被static和final修饰的变量,将会占据一段不能改变的存储空间,将会被看作编译期常量

7.不可变的是变量的引用而非引用指向对象的内容

几个例子:

1.final变量和普通变量的区别

复制代码
public class TestFinal {
    public static void main(String args[]){
        String a = "test1";
        final String b = "test";
        String d = "test";
        String c = b + 1; 
        String e = d + 1;
        System.out.println((a == c));
        System.out.println((a.equals(e)));
    }
}
复制代码
  View Code

由于final变量是基本类型以及String时,在编译期的时候就把它当作常量来使用,不须要在运行时候使用。“==”是对比两个对象基于内存引用,若是两个对象的引用彻底相同,则返回true,因此这里b是用访问常量的方式去访问,d是连接的方式,因此a的内存引用和c的内存引用是相等的,因此结果为true,a和e两个对象的值是相等的,因此结果为true

 

2.final在修饰对象的时候

复制代码
1 public class TestFinal {
2     public static void main(String args[]){
3         final TestFinal obj1 = new TestFinal();
4         final TestFinal obj2 = new TestFinal();
5         
6         obj1 = obj2;
7     }
8 }
复制代码

 

在编译的时候,或报错, 不能指向一个final对象。

 

volatile关键字


缓存一致性:

首先来看看线程的内存模型图:

 

当执行代码:

i = i + 1;

 

  1. 首先从主存中读取i的值,
  2. 而后复制I到Cache中,
  3. CPU执行指令对i进行加1
  4. 将加1后的值写入到Cache中
  5. 最后将Cache中i的值刷新到主存中

这个在单线程的环境中是没有问题的,可是运行到多线程中就存在问题了。

问题出在主存中的变量,由于有可能其余线程读的值,线程的Cache尚未同步到主存中,每一个线程中的Cahe中的值副本不同,可能会形成"脏读"。

 

缓存一致性协议解决了这样的问题,它规定每一个线程中的Cache使用的共享变量副本是同样的。

核心内容是当CPU写数据时,若是发现操做的变量式共享变量,它将通知其余CPU该变量的缓存行为无效,

因此当其余CPU须要读取这个变量的时候,发现本身的缓存行为无效,那么就会从主存中从新获取。

 

三个概念

Jvm定义了内存规范,试图作到各个平台对内存访问的差别,可是依旧会发生缓存一致性的问题。

首先了解三个概念,原子性,可见性,有序性。

 

原子性:指某个操做,一个或者多个,要么所有执行而且执行的过程当中不会被任何因素打断,要么都不执行。

在JVM中,只有简单的读取、赋值(并且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操做)才是原子操做。看一个例子:

x = 70;         //语句1
y = x;         //语句2
y++;           //语句3
y = x + 1;     //语句4

 

上面四个语句中,只有语句1是原子性,其余都不是。

 

可见性:当多个线程访问一个变量时,一个线程修改了这个变量的值,其余线程可以看获得。

未加volatile变量修饰的变量,在被修改以后,何时写入到主存是不肯定的,所以其余线程读取该变量的值可能仍是未被修改的值。

若是改变了被volatile关键字修饰了,那么JVM将会标记它为共享变量,共享变量一经修改,就会当即同步到主存中,而且通知其余线程(CPU缓存)中值生效,请去主存中读取该值。

 

有序性:程序的执行顺序按照代码的前后顺序执行。可是JVM在执行语句的过程会对代码进行重排序(重排序:CPU为了提升程序运行效率,可能会对输入代码进行优化,可是不保证程序的执行前后顺序和代码中的顺序一致,可是会保证程序最终执行结果和代码顺序执行的结果是一致的)。

在多线程的环境下,原有的顺序执行会发生错误。

在JVM中保证了必定的有序性,好比被volatile修饰后的变量,那么该变量的写操做先行发生于后面对这个变量的读操做。

 

因此要想程序在多线程环境下正确运行,必须保证原子性,可见性,有序性。

 

volatile的做用

当一个变量(类的普通变量,静态变量)被volatile修饰以后,那么将具有两个属性:

1)保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。

2)禁止进行指令重排序

 

下面来看看线程池中一些变量的定义:

复制代码
    private volatile ThreadFactory threadFactory;

    private volatile RejectedExecutionHandler handler;

    private volatile long keepAliveTime;

    private volatile boolean allowCoreThreadTimeOut;

    private volatile int corePoolSize;

    private volatile int maximumPoolSize;
复制代码

 

 能够看到线程工厂threadFactory,拒绝策略handler,没有任务时的活跃时间keepAliveTime,keepAliveTime的开关allowCoreThreadTimeOut,核心池大小corePoolSize,最大线程数maximumPoolSize

都是被volatile修饰中,由于在线程池中有若干个线程,这些变量必需保持对线程可见性,否则会引发线程池运行不正确。

 

volatile不能保证原子性

i++;

它是非原子性的,当变量i被volatile修饰时,是否能保证原子性呢?

作个试验:

复制代码
public class TestAtomVolatile {
    public volatile int i = 0;

    public void increase() {
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        final TestAtomVolatile test = new TestAtomVolatile();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                    System.out.println(test.i);
                };
            }.start();
        }

    }
}
复制代码

 

以上代码就是10个线程,分别对变量i进行自增操做,预期结果应该是10000,可是总会存在着小于10000的状况。输出结果以下:

 对于这种状况,可使用锁,synchronize,Lock,也可使用原子变量。

原子变量的例子:

View Code

 

 volatile的原理

复制代码
 下面这段话摘自《深刻理解Java虚拟机》:

“”观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上至关于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操做已经所有完成;

2)它会强制将对缓存的修改操做当即写入主存;

3)若是是写操做,它会致使其余CPU中对应的缓存行无效。
复制代码

 

assert关键字

assert断言

在目前的java编码中,是不推荐使用的,这里只是稍微了解一下:

使用方式:

 
一、assert <boolean表达式>
若是<boolean表达式>为true,则程序继续执行。
若是为false,则程序抛出AssertionError,并终止执行。
 
二、assert <boolean表达式> : <错误信息表达式>
若是<boolean表达式>为true,则程序继续执行。
若是为false,则程序抛出java.lang.AssertionError,并输入<错误信息表达式>。
 
若是要开启断言检查,则须要用开关-enableassertions或-ea来开启,java中IDE工具默认支持开启-ea
下面是一个例子:
复制代码
public class LearnAssert {
    public static void main(String args[]){
        assert true;
        System.out.println("断言1成功执行");
        System.out.println("-----------");
        assert false:"error";
        System.out.println("断言2成功执行");
    }
}
复制代码

 assert是为了在调试程序时候使用的,默认不推荐使用,测试程序可使用junit。

 

synchronized关键字

关于锁关键字,有如下几个总结:

  • 不管synchronized关键字加在方法上仍是对象上,若是它做用的对象是非静态的,则它取得的锁是对象;若是synchronized做用的对象是一个静态方法或一个类,则它取得的锁是对类,该类全部的对象同一把锁。
  • 每一个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就能够运行它所控制的那段代码。
  • 实现同步是要很大的系统开销做为代价的,甚至可能形成死锁,因此尽可能避免无谓的同步控制。

下面介绍一个锁的实例:

复制代码
public class ManyThread {
    int count = 0;

    public synchronized void autoIncrement() {
        count++;
    }

    public static void main(String args[]) {
        ManyThread manyThread = new ManyThread();
        Runnable runnable = new MyRunnable2(manyThread);
        new Thread(runnable, "a").start();
        new Thread(runnable, "b").start();
        new Thread(runnable, "c").start();
        new Thread(runnable, "d").start();
    }


}

class MyRunnable2 implements Runnable {

    private  ManyThread manyThread;

    public MyRunnable2(ManyThread manyThread) {
        this.manyThread = manyThread;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            manyThread.autoIncrement();
            System.out.println(Thread.currentThread().getName() + " 执行中 " + "count:" + manyThread.count);
        }
    }


}
复制代码

 

用synchronized修饰后的autoIncrement()方法,会被加锁,确保它每次执行的时候都能保证只有一个线程在运行。

 

 

transient关键字

 Java中,一个类想要序列化,能够经过实现Serilizable接口的方式来实现,实现该接口以后,该类全部属性和方法都会自动序列化。

可是若是属性或方法被transient修饰,那么将不会被序列

相关文章
相关标签/搜索