用于定义接口。
接口:可以认为就是一份合同(契约)。出现的目的:体现封装性、分离契约和实现、区分开“甲方”(提要求)和“乙方”(干活)。
interface 接口名称 [extends 其他接口] { // 接口允许多继承
// 抽象方法列表
void method(); // 必须是普通方法,不写访问限定符时默认为 public abstract。为抽象方法(没有方法实现)
int a = 10; // 默认为 public static final
static void staticMethod() {
// 静态方法
}
[default ]void defaultMethod() {
// 默认方法,其子类可以不实现
}
}
// 容器
interface Colletion {}
// 数据结构
interface DataStructure {}
注意以下几点:
写在定义类的时候。
class 类名 [extends 父类 ]implements 接口名[, 接口名[, ...]] {
// 覆写接口中的所有抽象方法 或者 声明为抽象类后,实现部分方法
}
一个类中可以有一个 public 的类 或者 一个 public 的接口。
abstract class A {} // 抽象类
new A(); // Compile Error
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) {
}
}
上述代码分析:
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
final class A {}
class A { // 类 A 可以被继承
final void method() {} // 该方法无法被其子类覆写
}
前提:Java 中,类是第一成员,即只有类才有方法。
普通方法(方法) | 静态方法(类方法)
普通属性(属性) | 静态属性(类属性)
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
*/
}
}
普通方法和普通属性都绑定着一个隐含的对象(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,指向调用该方法的对象。可以按照下图理解:
(2) return new Person();
实际为 Person.createPerson(); 没有形参。 可以按照下图理解:
在静态方法(静态上下文)中,无法使用非静态的内容。原因:没有一个隐式的对象与该方法绑定。
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 模型的总结:
修饰代码块:发生在类的加载时,没有对象。
修饰变量:
修饰方法:
定义静态方法、静态属性的依据:
偷懒做法:main 直接调的,一般都是静态方法。
举例:通讯录类 操作:根据姓名查询电话 —> 非静态方法
新建一本通讯录 —> 静态方法
发生在对象的实例化时期。
普通属性初始化的方式:
普通属性初始化的顺序:
定义时的初始化 和 构造代码块的初始化 按书写顺序进行;
构造方法中的初始化一定发生在最后,与书写顺序无关。
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();
}
}
按照普通属性的初始化顺序规则,上述代码运行的结果为:
发送在类被加载的时候。
静态属性初始化的方式:
类加载: ==> 发生在运行时期
类的信息一开始是以字节码(bytecode)*.class 的形式保存在磁盘上的,
类加载的过程是:类加载器(ClassLoader)在对象的目录上找到指定类的字节码文件,并且进行解析,然后放到内存的方法区中的过程。
类只有在被使用到的时候才会进行加载(且一般不会卸载),其中类被使用到的情况有:
类的加载一定在对象实例化之前,且只加载一次。也就是说静态属性的初始化一定在普通属性的初始化之前。
关于类加载的总结:
做什么事:把类从磁盘加载到内存中。
什么时机:用到类的时候,即运行时期。
程序一开始就进行类的加载吗:不是,按需加载,即懒加载(进行懒加载的还有 HashMap、HashTable)。
什么样的情况下需要:
类的加载时会执行类的初始化,具体会做那些事情:
具体执行中会按定义的顺序执行上述两者。
静态属性初始化的顺序:
按照定义类是的书写顺序初始化。
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();
}
}
定义在类的内部的类,可分为:
class OutterClass {
static class StaticClass {}
class InnerClass {}
}
注意:
a. 定义在成员级别,不能用 static 修饰;
b. 定义在方法级别
class A {
class B {} // 定义一个内部类
// 定义一个匿名的内部类,同时实例化一个对象
new C() {}
}
Java 的每个对象中都有一个锁,叫监视器锁(monitor lock)。默认为打开的状态。
public synchronized void method() { // 带 synchronized 修饰的普通方法
// 具体代码
}
public static synchronized void staticMethod() {} // 带 synchronized 修饰的静态同步方法
public void block() {
synchronized (this) {}
}
执行带 synchronized 修饰的普通方法时,抢的是堆中构造的对象的锁。首先需要 lock 引用指向的对象中的锁:
如果一个线程 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)
锁的是引用指向的对象,而不是代码块。即便不是同一个方法,但只要是指向同一个对象,争抢的就是同一把锁。
public void block() {
synchronized (this) {
// 大括号开始,加锁
} // 大括号结束,释放锁
}
表现 | 锁的对象 | 什么时候加锁 | 什么时候释放锁 |
---|---|---|---|
修饰普通方法 | this 指向的对象中的锁 | 进入方法 | 退出方法 |
修饰静态方法 | 方法所在类的锁 | 进入方法 | 退出方法 |
修饰代码块 | () 中引用指向的对象 | 进入代码块 | 退出代码块 |
Person.class 就是类的对象(反射的知识)。
原子性:如“作用”中所述,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() | 不互斥 |
可见性:可以保证一定限度的可见性。
加锁和释放锁会伴随着工作内存的刷新,在这个时机,保证了可见性。但是在临界区,即加锁和释放锁之间的代码执行的中间不做任何保证。
加锁时所有的工作缓存刷新,即工作缓存加载到主内存中,工作缓存失效;释放锁时所有的新数据被重新加载到工作内存中。
即提及可见性,必定提及工作内存和主内存。
重排序:
语句 A
B
C // A、B、C 可以互相交换,但不能和其他语句交换
{
// 同步代码块
D
E
F
// D、E、F 可以互相交换,但不能和其他语句交换
}
G
H // G、H 可以互相交换,但不能和其他语句交换
保证重排序的正确性:锁之前的语句无法重排序到临界区(锁的代码部分),临界区内部的无法重排序到外边。
理论上所有的问题都可以通过 synchronized 解决。
但是成本非常大,主要是因为线程调度的成本大。