三、关键字
关键字简单介绍
Java关键字一共53个,其中包含了两个保留字 goto, const
。
关键字 | 含义 |
---|---|
abstract | 表明类或者成员方法具有抽象属性 |
assert | 断言,用来进行程序调试 |
boolean | 基本数据类型之一,布尔类型 |
break | 提前跳出一个块 |
byte | 基本数据类型之一。字节类型 |
case | 用来 switch 语句中,表示其中一个分支 |
catch | 用在异常处理中,用来捕获异常 |
char | 基本数据类型之一,字符类型 |
class | 申明一个类 |
const | 保留关键字,没有具体含义 |
continue | 回到一个块的开始处 |
default | 默认。例如用在 swtich 语句中表明一个默认的分支 |
do | 用在 do-while 循环结构中 |
double | 基本数据类型之一,双精度浮点数类型 |
else | 用在条件语句中,表明当条件不成立时的分支 |
enum | 枚举 |
extends | 表明一个类型是另一个类型的子类型,这里常见的类型有类和接口 |
final | 用来说明最终属性,表明一个类不能派生出子类,或者成员方法不能被覆盖,或者成员变量的值不能被改变,用来定义常量 |
finally | 用于处理异常情况,用来生命一个基本肯定会执行到的语句 |
float | 基本数据类型之一,单精度浮点数类型 |
for | 一种循环结构的引导词 |
goto | 保留关键字,没有具体含义 |
if | 条件语句的引导词 |
implements | 表明一个类实现了给定的接口 |
import | 表明要访问指定的类和包 |
instanceof | 用来测试一个对象是否是指定类型的实例对象 |
int | 基本数据类型之一,整数类型 |
interface | 接口 |
long | 基本数据类型之一,长整数类型 |
native | 用来声明一个方法是由其他语言(C、C++等)实现的 |
new | 用来创建新的实例对象 |
package | 包 |
private | 一种访问控制方式:私有模式 |
protect | 一种访问控制方式:保护模式 |
public | 一种访问控制方式:公有模式 |
return | 从成员方法中返回数据 |
short | 基本数据类型之一,短整数类型 |
static | 表明具有静态属性 |
strictfp | 用来声明FP_strict(单精度或双精度浮点数)表达式遵循 IEEE 754 算数规范 |
super | 表明当前对象的父类型的引用或者父类型的构造方法 |
switch | 分支语句结构的引导词 |
synchronized | 表明一段代码需要同步执行 |
this | 指向当前实例对象的引用 |
throw | 抛出一个异常 |
throws | 声明在当前定义的成员方法中有需要抛出的异常 |
transient | 申明不用序列化的成员 |
try | 尝试一个可能抛出异常的程序块 |
void | 声明当前成员方法没有返回值 |
volatile | 表明两个或者多个变量必须同步地发生变化 |
while | 用在循环结构中 |
部分关键字详解
由于关键字太多,且部分关键字没有什么好说的,本文就挑几个原理比较复杂的或者面试经常问到的关键字进行深入了解。
break
我们都知道,break 常用语 while、for 等循环以及 switch 分支中,但是奇怪的是很少有人知道 break 和 java 标签(label)的配合使用。。。
比如下面这段代码:
for(...){
for(...){
...
break;
}
}
break 只能跳出当前代码块,怎么跳出外层循环呢?我们可以用标签。
label1:for(...){
label2:for(...){
...
break label1;
}
}
continue 也一样,也可以配合标签使用。
final
final 可以修饰的对象有:1、数据;2、方法;3、类
1、数据
声明数据为常量,可以使编译时常量,也可以是运行时被初始化后不能被改变的常量。
- 对于基本类型,final 使数值不变;
- 对于引用类型,final 使引用不变,也就是不能引用其他对象,但是被引用的对象本身是可以修改的。
final int x = 1;
// x = 2; // cannot assign value to final variable 'x'
final A y = new A();
y.a = 1;
2、方法
声明方法不能被子类重写。
private 方法隐式地被指定为 final,如果子类中定义的方法和父类中的一个 private 方法签名相同,此时子类的方法不是重写父类方法,而是在子类中定义了一个新的方法。
3、类
声明类不允许被继承(如 String)。
finally
finally 关键字经常给人一种“是在 try...catch 语句后肯定会执行的部分”的感觉,但其实 finally 修饰的代码块并不一定会被执行,比如下面三种情况:
- case1:try-catch 异常退出
try{
...
System.exit(1);
}finally{
打印("finally");
}
- case2:无限循环
try{
while(true);
}
finally{
...
}
- case1:try-catch 异常退出
执行try-catch的线程被杀死
static
1、静态变量
- 静态变量,又称类变量,也就是说这个变量属于类的,类的所有实例都共享静态变量,可以通过类名来访问它。静态变量在内存中只存在一份。(java 8 以后,静态成员也从方法区转到堆中了)
- 实例变量,每创建一个实例就会产生一个实例变量,它与该实例同生共死
public class A {
private int x; // 实例变量
private static int y; // 静态变量
public static void main(String[] args) {
// int x = A.x; // Non-static field 'x' cannot be referenced from a static context
A a = new A();
int x = a.x;
int y = A.y;
}
}
2、静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
public abstract class A {
public static void func1(){
}
// public abstract static void func2(); // Illegal combination of modifiers: 'abstract' and 'static'
}
只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,因此这两个关键字与具体对象关联。
public class A {
private static int x;
private int y;
public static void func1(){
int a = x;
// int b = y; // Non-static field 'y' cannot be referenced from a static context
// int b = this.y; // 'A.this' cannot be referenced from a static context
}
}
3、静态语句块
静态语句块在类初始化时运行一次。
如下面代码
public class A {
static {
System.out.println("123");
}
public static void main(String[] args) {
A a1 = new A();
A a2 = new A();
}
}
输出只有一个123
4、静态内部类
非静态内部类依赖于外部类的实例化,也就是说需要先创建外部类的实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。
public class OuterClass {
class InnerClass {
}
static class StaticInnerClass {
}
public static void main(String[] args) {
// InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
StaticInnerClass staticInnerClass = new StaticInnerClass();
}
}
静态内部类不能访问外部类的非静态的变量和方法。
5、静态导包
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
import static com.xxx.ClassName.*
6、初始化顺序
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于他们在代码中的顺序。(这边篇幅已经很长了,就不用实验证明了。大家可以自己去做试试看)
public static String staticField = "静态变量";
static {
System.out.println("静态语句块");
}
public String field = "实例变量";
{
System.out.println("普通语句块");
}
最后才是构造函数的初始化
public InitialOrderTest() {
System.out.println("构造函数");
}
存在集成的情况下,初始化顺序为:
- 父类(静态变量、静态语句块)
- 子类(静态变量、静态语句块)
- 父类(实例变量、普通语句块)
- 父类(构造函数)
- 子类(实例变量、普通语句块)
- 子类(构造函数)
synchronized
(本节需要并发编程的基础知识)
synchronized 关键字最主要有以下三种应用方式:
- 修饰实例方法,将当前实例变成锁,进入同步代码前要获得当前实例(this)的锁;
- 修饰静态方法,将当前类对象变成锁,进入同步代码块前要获得当前类对象(.class)的锁;
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
synchronized 作用于实例方法
所谓的实例对象锁就是用 synchronized 修饰类中的普通成员方法(不包括静态方法),此时被调用成员方法的实例成为锁,拥有该实例对象锁的线程才能进行被调用成员方法的运行。如下面代码:
public class Test implements Runnable {
//共享资源(临界资源)
public Integer i = 0;
private synchronized void increase() {
i++;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10000;i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
Test test = new Test();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(test.i);
}
}
输出20000
。
在上面代码中,我们创建了一个实现 Runnable 接口的类 Test,其中实现了 synchronized 修饰的方法 increase,负责对共享资源 i 进行 i++ 操作。我们都知道,由于 i++ 操作不具有原子性,因此在多线程进行 increase 方法调用时,会出现脏数据的现象,导致的结果就是最后输出内容与我们逻辑上设置的不同,如上述代码结果基本上都会小于 20000 。但是这里我们结果无论运行多少遍,都是2000,这是因为我们使用了 synchronized 关键字对 increase 方法进行修饰。上面我们讲了,synchronized 修饰普通成员方法时,锁是类的实例,当一个线程拿到这个锁的时候,该线程可以执行 synchronized 修饰的方法,其他线程没有锁于是他们不能执行 i++。这就保证了在一个时间点有且仅有一个线程可以进行完整的 i++ 操作。不过其他没有被 synchronized 修饰的成员方法,其他线程还是可以正常调用的。
synchronized 作用于静态方法
所谓的类对象锁就是用 synchronized 修饰类中的静态方法(和上面正好相反),此时该类(class)就变成了锁。拥有该类对象锁的线程才能进行被调用成员方法的运行。为什么会需要类对象锁呢?看下面代码:
public class Test implements Runnable {
public static Integer i = 0;
private synchronized void increase() {
i++;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10000;i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
Thread t1 = new Thread(new Test());
Thread t2 = new Thread(new Test());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
改代码运行的结果是多少呢?答案是15027
(每次运行不太一样)。因为上述代码反了一个错误,synchronized 修饰的方法是普通的成员方法,因此锁是实例对象锁,但是这边 new 了两个实例,因此他们不会产生竞争锁的关系,因此 static 修饰的共享资源 i 会小于20000。对于这种情况,我们要将 synchronized 修饰静态方法。代码如下:
...
private static synchronized void increase() {
i++;
}
...
此时无论多少次,输出都是 20000。因为这时的锁变成了类对象锁,而类对象不属于任何一个实例。
要注意的是,当一个类中同时有 synchronized 修饰的普通成员方法和静态方法时,这两者之间不会产生竞争关系(因为锁不同)。
synchronized 作用于代码块
如果光是上面两种作用范围,相比我们编程的时候会发生诸多不便,比如说如果方法体特别大,然而涉及到同步的操作只有一小部分,又懒得去额外写一个方法,这时候用上面两种方式就不太合适。Java 给我们提供了一种更加灵活地方式,同步代码块。同步代码块需要我们自己指定锁,锁可以是一个普通成员对象,也可以是静态成员对象,也可以是this,甚至是class!代码如下:
// 普通成员对象作为锁
public class Test implements Runnable {
final String LOCK = "LOCK";
public Integer i = 0;
private void increase() {
i++;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10000;i++) {
synchronized (LOCK) {
increase();
}
}
}
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
Test test = new Test();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(test.i);
}
}
更神奇的是,你完全可以根据需要将锁对象进行修改!比如说下面:
// 静态对象作为锁,这就跟同步静态方法差不多了
public class Test implements Runnable {
static final String LOCK = "LOCK";
public static Integer i = 0;
private void increase() {
i++;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10000;i++) {
synchronized (LOCK) {
increase();
}
}
}
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
// Test test = new Test();
Thread t1 = new Thread(new Test());
Thread t2 = new Thread(new Test());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
// 实例作为锁,就是实例对象锁,只是这边显示表达了,跟 synchronized 修饰成员方法原理一样
public class Test implements Runnable {
// static final String LOCK = "LOCK";
public Integer i = 0;
private void increase() {
i++;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10000;i++) {
synchronized (this) {
increase();
}
}
}
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
Test test = new Test();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(test.i);
}
}
// class作为锁,其实就是类对象锁,跟 synchronized 修饰静态方法意思差不大
public class Test implements Runnable {
// static final String LOCK = "LOCK";
public static Integer i = 0;
private void increase() {
i++;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10000;i++) {
synchronized (Test.class) {
increase();
}
}
}
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
// Test test = new Test();
Thread t1 = new Thread(new Test());
Thread t2 = new Thread(new Test());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
synchronized 原理
还没整理,可以看大佬博客
volatile
并发编程的三个概念:原子性、可见性、有序性。
volatile的特性:
1、保证可见性,但不保证原子性
volatile 修饰变量时,JMM 会把该线程本地内存中的变量强制刷新到主内存中,并使其他线程中的缓存无效。但不能保证多线程下结果的正确。
2、禁止指令重排
重排序的规则是不会对有数据依赖的操作进行重排,运行结果不会发生变化。volatile 关键字起到了内存屏障的作用,保证了 volatile 前后操作隔离,重排序不会串。