Java 关键字:interface, abstract, final, static, synchronized

用于定义类、函数、变量修饰符的关键字

interface

​ 用于定义接口。

接口:可以认为就是一份合同(契约)。出现的目的:体现封装性、分离契约和实现、区分开“甲方”(提要求)和“乙方”(干活)。

1、定义的语法为:

interface 接口名称 [extends 其他接口] {	// 接口允许多继承
	// 抽象方法列表
	void method();	// 必须是普通方法,不写访问限定符时默认为 public abstract。为抽象方法(没有方法实现)
	int a = 10;	// 默认为 public static final
	static void staticMethod() {
		// 静态方法
	}
	[default ]void defaultMethod() {
		// 默认方法,其子类可以不实现
	}
}

// 容器
interface Colletion {}

// 数据结构
interface DataStructure {}

注意以下几点:

  1. 接口的定义允许多继承。
  2. 接口无法实例化对象。
  3. 接口中给出的方法列表默认为:
    1. public 访问限定符修饰;
    2. 无法使用 static 修饰(有特例,接口也可以定义 static 方法,但使用的场景不多,效果和普通的静态方法一样);
    3. 是一个抽象方法,直接用分号结尾,不用给出方法体。
  4. 接口中不能出现属性,如果出现默认都是被 final static 修饰的。

2、类实现接口的语法:

​ 写在定义类的时候。

class 类名 [extends 父类 ]implements 接口名[, 接口名[, ...]] {
	// 覆写接口中的所有抽象方法 或者 声明为抽象类后,实现部分方法
}

一个类中可以有一个 public 的类 或者 一个 public 的接口。

abstract

  1. 修饰类:表示这个类为抽象类,无法被实例化对象。
abstract class A {}	// 抽象类
new A();	// Compile Error
  1. 修饰方法:表示该方法为抽象方法,即只有方法签名,没有方法实现。只能实例化,不能继承。
abstract void method();

一个类如果是抽象类,不一定有抽象方法;而一个方法如果是抽象方法,就一定在抽象类中。

public interface List {
	public void insert(int index, int element);
	public void pushFront(int element);
}

abstract class AbstractList implements List {
	protected int size = 0;
	
	@Override
	public void insert(int index, int element) {}
	
	@Override
	public void pushFront(int element) {
		insert(size, element);
	}
}

class ArrayList extends AbstractList implements List {
	@Override
	public void insert(int index, int element) {
		// 具体实现
	}
}

class LinkedList extends AbstractList implements List {
	@Override
	public void insert(int index, int element) {
	}
}

上述代码分析:

  1. List(接口)包含抽象方法:insert、pushFront。
  2. AbstractList(抽象类)实现了 List。覆写了 pushFront 方法,抽象方法 insert 没有实现。
  3. ArrayList(类)继承了 AbstractList ,实现了 List。覆写了 insert 方法。
  4. LinkedList(类)继承了 AbstractList ,实现了 List。
    1. 抽象方法一旦被实现了一次,就不再是抽象方法了;
    2. AbstractList 只是线性表,把公共代码提取出来服用。所以无法实现 insert 方法,因为顺序表(ArrayList)和链表(LinkedList)的 insert 是不同的。

final

  1. 修饰变量,为不可变变量,只有一次赋值的机会。
final int a = 10;
a = 100;	// Compile Error

final int[] a = new int[10];
A) a = new int[100];	// Compile Error
B) a[0] = 100;	// Compile OK

final Animal animal = new Animal();
A) p = null;	// Compile Error
B) p.name = "hello";	// Compile OK
  1. 修饰类,作为类修饰器,表示这个类不能被继承。
final class A {}
  1. 修饰方法,代表这个方法无法被其子类覆写。
class A {	// 类 A 可以被继承
	final void method() {}	// 该方法无法被其子类覆写
}

static

​ 前提:Java 中,类是第一成员,即只有类才有方法。

1、方法和属性的分类:

​ 普通方法(方法) | 静态方法(类方法)

​ 普通属性(属性) | 静态属性(类属性)

2、static 相关语法:

​ static 只可出现在成员级别,修饰类、方法、变量、代码块。

​ 只有“import static tools.Tools.*;”时,static 才可以出现在顶级。

/**
 * 此处为 顶级
 * 不允许出现 static
 * 可以有访问限定符:public、default
 */
class A {
    /**
     * 此处为 成员级别
     * 允许出现 static,可以为静态“类、方法、变量、代码块”
     * 访问限定符:public、private、protected、default
     */
    static int a;
    static void staticMethod() {}
    static class B {}
    static {}
    void method() {
        /**
         * 此处为 方法级别
         * 不允许出现 static
         * 可以有访问限定符:public、private、protected、default
         */
    }
}
  1. 限定符 static:
    1. 被 static 修饰的属性就是静态属性;
    2. 被 static 修饰的方法就是静态方法。

普通方法和普通属性都绑定着一个隐含的对象(this),static 的含义就是和对象解绑。

​ a. 静态属性不再保存在对象(堆区)中,而是保存在类(方法区)中。

​ b. 静态方法调用时,没有隐含着的对象,所以就无法使用 this 关键字。

class Person {
	String name = "Doris";
	String toString() {
		return this.name;	// --- (1)
	}
	static Person createPerson() {
		return new Person();	// --- (2)
	}
}

(1) return this.name;

​ 其实有一个隐式的形参 Person this,指向调用该方法的对象。可以按照下图理解:

Java 关键字:interface, abstract, final, static, synchronized_第1张图片

(2) return new Person();

​ 实际为 Person.createPerson(); 没有形参。 可以按照下图理解:

Java 关键字:interface, abstract, final, static, synchronized_第2张图片

  1. 访问静态属性、调用静态方法的语法:
    1. 内部访问(调用):
      1. 属性:
        1. 属性名称; // 比较推荐使用
        2. 类名称.属性名称;
        3. this.属性名称; // 使用时要保证当前的方法不是静态方法,不推荐
      2. 方法:
        1. 方法名称(实参列表); // 比较推荐使用
        2. 类名称.方法名称(实参列表);
        3. this.方法名称(实参列表); // 使用时要保证当前的方法不是静态方法,不推荐
    2. 外部访问(调用):
      1. 属性:
        1. 类名称.属性名称; // 比较推荐使用
        2. 对象的引用.属性名称;
      2. 方法:
        1. 类名称.方法名称(实参列表); // 比较推荐使用
        2. 对象的引用.方法名称(实参列表);

在静态方法(静态上下文)中,无法使用非静态的内容。原因:没有一个隐式的对象与该方法绑定。

​ a. 不能访问普通属性;

​ b. 不能调用普通方法;

​ c. 无法使用 this 关键字。

public class Test {
	static int b = 1;
	int a;
	private void print() {}
	public static void main(String[] args) {
		a = 10;	// Compile Error.	--- 隐式使用了 this.a
		print();	// Compile Error.	--- 隐式使用了 this.print();
		new Test().b = 10;	// Compile OK
	}
}

表现出来的特性:

​ 静态属性存在并且只存在一份,表现出共享的特性。

​ 一个类的所有对象,时可以共享静态属性的。(可以适当理解为 C 中的全局变量)

规范:public static 推荐使用

static public 语法没错,不推荐使用

关于 static 模型的总结:

  1. 修饰代码块:发生在类的加载时,没有对象。

  2. 修饰变量:

    1. 如果不用 static 修饰,属性放在对象中,即对象存储在堆区。普通属性先有对象,才有属性;
    2. 如果用 static 修饰,属性放在类中,类放在方法区。所以static 修饰的变量放在方法区。
  3. 修饰方法:

    1. 如果不用 static 修饰(有 this)。(运行时期)在方法的调用时,参数中会有一个隐式的引用,即this,指向放到的调用对象;
    2. 如果用 static 修饰,表现为不能直接使用属性和方法 (没有 this)。(运行时期)在方法的调用时调用栈,参数中没有这个隐式的引用。
  4. 定义静态方法、静态属性的依据:

    1. 如果方法是某一个对象的,则为非静态方法;
    2. 如果方法与对象无关,则为静态方法。

    偷懒做法:main 直接调的,一般都是静态方法。

    举例:通讯录类 操作:根据姓名查询电话 —> 非静态方法

    ​ 新建一本通讯录 —> 静态方法

3、普通属性的初始化:

​ 发生在对象的实例化时期。

普通属性初始化的方式:

  1. 定义时初始化 int a = 10;
  2. 构造代码块中初始化 { a = 10; }
  3. 在构造方法中初始化 Person() { a = 10; }

普通属性初始化的顺序:

  1. 定义时的初始化 和 构造代码块的初始化 按书写顺序进行;

  2. 构造方法中的初始化一定发生在最后,与书写顺序无关。

public class A {
	A() {
		System.out.println("构造方法中,a = 30");
		a = 30;
	}
	
	{
		System.out.println("构造代码块 1 中,a = 0");
		a = 0;
	}
	int a = init();
	{
		System.out.println("构造代码块 2 中,a = 20");
		a = 20;
	}
	
	int init() {
		System.out.println("定义时,a = 10");
		retuen 10;
	}
	
	public static void main(String[] args) {
		A p = new A();
	}
}

按照普通属性的初始化顺序规则,上述代码运行的结果为:

Java 关键字:interface, abstract, final, static, synchronized_第3张图片

4、静态属性的初始化:

​ 发送在类被加载的时候。

静态属性初始化的方式:

  1. 定义时初始化 static int a = 10;
  2. 静态构造代码块中初始化 static { a = 20; }

类加载: ==> 发生在运行时期

类的信息一开始是以字节码(bytecode)*.class 的形式保存在磁盘上的,

类加载的过程是:类加载器(ClassLoader)在对象的目录上找到指定类的字节码文件,并且进行解析,然后放到内存的方法区中的过程。

类只有在被使用到的时候才会进行加载(且一般不会卸载),其中类被使用到的情况有:

  1. 用类去实例化对象;
  2. 调用静态方法;
  3. 访问静态属性。

类的加载一定在对象实例化之前,且只加载一次。也就是说静态属性的初始化一定在普通属性的初始化之前。

关于类加载的总结:

  1. 做什么事:把类从磁盘加载到内存中。

    1. 谁负责:类的加载器。
    2. 从磁盘的哪个位置加载:由配置的方式决定,体现为 CLASSPATH 环境变量。
    3. 加载到内存的哪个位置:方法区,主要加载的是整个类的结构、方法代码。
  2. 什么时机:用到类的时候,即运行时期。

    1. 程序一开始就进行类的加载吗:不是,按需加载,即懒加载(进行懒加载的还有 HashMap、HashTable)。

    2. 什么样的情况下需要:

      1. 构造对象。举例:造自行车(对象)需要自行车图纸(类)。
      2. 调用静态方法、访问静态属性时。举例:自行车图纸本身的属性,与自行车无关。
      3. 反射。举例:Class cls = Class.forName(“Person”); 类也是用保存对象的形式保存的,即类对象的类。
      4. 用到这个类的子类时,会优先加载父类。
    3. 类的加载时会执行类的初始化,具体会做那些事情:

      1. 执行静态属性的初始化;
      2. 执行静态代码块。

      具体执行中会按定义的顺序执行上述两者。

静态属性初始化的顺序:

​ 按照定义类是的书写顺序初始化。

public class A {
	A() {
		System.out.println("构造方法中,a = 30");
		a = 30;
	}
	
	{
		System.out.println("构造代码块 1 中,a = 0");
		a = 0;
	}
	int a = init();
	{
		System.out.println("构造代码块 2 中,a = 20");
		a = 20;
	}
	
	int init() {
		System.out.println("定义时,a = 10");
		retuen 10;
	}
	
	static {
		System.out.println("静态代码块 1 中,staticA = 100");
		staticA = 100;
	}
	static int staticA = staticInit();
    static {
        System.out.println("静态代码块 2 中,staticA = 300");
        staticA = 300;
    }
    
    static int staticInit() {
        System.out.println("静态定义时,staticA = 200");
        return 200;
    }
    
   
    public static void main(String[] args) {
        A p = new A();
        A q = new A();
    }
}

按照静态属性的初始化顺序规则,上述代码运行的结果为:
Java 关键字:interface, abstract, final, static, synchronized_第4张图片

5、内部类

​ 定义在类的内部的类,可分为:

  1. 静态内部类和普通内部类
class OutterClass {
	static class StaticClass {}
	class InnerClass {}
}

注意:

​ a. 定义在成员级别,不能用 static 修饰;

​ b. 定义在方法级别

  1. 有名的内部类和匿名的内部类(可以用 Lambda 表达式替换)
class A {
	class B {}	// 定义一个内部类
	
	// 定义一个匿名的内部类,同时实例化一个对象
	new C() {}
}

synchronized

​ Java 的每个对象中都有一个锁,叫监视器锁(monitor lock)。默认为打开的状态。

1、语法

  1. 作为方法的修饰符
public synchronized void method() {    // 带 synchronized 修饰的普通方法
	// 具体代码
}
public static synchronized void staticMethod() {}    // 带 synchronized 修饰的静态同步方法
  1. 作为代码块出现
public void block() {
    synchronized (this) {}
}

2、作用

  1. 执行带 synchronized 修饰的普通方法时,抢的是堆中构造的对象的锁。首先需要 lock 引用指向的对象中的锁:

    1. 如果可以锁,就正常执行代码;
    2. 否则,需要等其他线程把锁 unlock。

    如果一个线程 lock 到了锁,到方法执行结束时就会 unlock 这把锁。

    关键在:1)锁在什么地方:针对普通方法,锁在调用该方法的引用指向的对象中(this,即当前对象)。

    ​ 2)什么时机加锁:当线程加载到 CPU 并且对象 unlock 时,加锁,lock。

    ​ 3)什么时机释放锁:当前线程运行结束时,释放锁,unlock。释放锁不意味着释放 CPU,释放 CPU 也并不意味着释放锁。

锁的持有和释放 —— 线程状态之间的联系:

​ 线程在 Runnable 队列中等待,当一个线程加载到 CPU 中,则被 lock;

​ 当前线程放弃 CPU 后仍为 lock,此时如果其他线程被加载到 CPU 中,就会移到 Block 队列(每个线程都有自己的 Block 队列)中,不再具有竞争资格,直到 unlock 又被重新移到 Runnable 队列中。

​ 抢锁之前必须先抢到 CPU,即代码执行必须在 CPU 上,否则不会执行。(任何代码的执行都必须先加载到 CPU)

​ 锁的是引用指向的对象,而不是代码块。即便不是同一个方法,但只要是指向同一个对象,争抢的就是同一把锁。

  1. 执行带 synchronized 修饰的静态同步方法时,抢的是方法区中类的元对象的锁。有时把类里的对象叫全局锁。
  2. 执行带 synchronized 修饰的代码块时:
public void block() {
    synchronized (this) {
        // 大括号开始,加锁
    }   // 大括号结束,释放锁
}
表现 锁的对象 什么时候加锁 什么时候释放锁
修饰普通方法 this 指向的对象中的锁 进入方法 退出方法
修饰静态方法 方法所在类的锁 进入方法 退出方法
修饰代码块 () 中引用指向的对象 进入代码块 退出代码块

​ Person.class 就是类的对象(反射的知识)。

3、synchronized 和原子性、可见性、重排序的关系:

  1. 原子性:如“作用”中所述,synchronized 可以满足原子性。

    加锁和释放锁会伴随着工作内存的刷新,在这个时机,保证了可见性。

    但是在临界区,即加锁和释放锁之间的代码执行的中间不做任何保证。

class Person {
    public static synchronized void m1() {}
    public static void m2() {}
    public synchronized void m3() {}
    public void m4() {}
}
Person p1 = new Person();
Person p2 = p1;
Person p3 = new Person();
A B 是否互斥
m1 m1 互斥
m1 m2 不互斥
p1.m3() p3.m3() 不互斥
p1.m3() p1.m4() 不互斥
  1. 可见性:可以保证一定限度的可见性。

    加锁和释放锁会伴随着工作内存的刷新,在这个时机,保证了可见性。但是在临界区,即加锁和释放锁之间的代码执行的中间不做任何保证。

    加锁时所有的工作缓存刷新,即工作缓存加载到主内存中,工作缓存失效;释放锁时所有的新数据被重新加载到工作内存中。

    即提及可见性,必定提及工作内存和主内存。

  2. 重排序:

语句 A
B
C	// A、B、C 可以互相交换,但不能和其他语句交换
{
	// 同步代码块
	D
	E
	F
	// D、E、F 可以互相交换,但不能和其他语句交换
}
G
H	// G、H 可以互相交换,但不能和其他语句交换

​ 保证重排序的正确性:锁之前的语句无法重排序到临界区(锁的代码部分),临界区内部的无法重排序到外边。

4、缺点:

​ 理论上所有的问题都可以通过 synchronized 解决。

​ 但是成本非常大,主要是因为线程调度的成本大。

你可能感兴趣的:(java)