用于定义类、函数、变量修饰符的关键字
interface
用于定义接口。
接口:可以认为就是一份合同(契约)。出现的目的:体现封装性、分离契约和实现、区分开“甲方”(提要求)和“乙方”(干活)。
1、定义的语法为:
interface 接口名称 [extends 其他接口] { // 接口允许多继承
// 抽象方法列表
void method(); // 必须是普通方法,不写访问限定符时默认为 public abstract。为抽象方法(没有方法实现)
int a = 10; // 默认为 public static final
static void staticMethod() {
// 静态方法
}
[default ]void defaultMethod() {
// 默认方法,其子类可以不实现
}
}
// 容器
interface Colletion {}
// 数据结构
interface DataStructure {}
注意以下几点:
- 接口的定义允许多继承。
- 接口无法实例化对象。
- 接口中给出的方法列表默认为:
- public 访问限定符修饰;
- 无法使用 static 修饰(有特例,接口也可以定义 static 方法,但使用的场景不多,效果和普通的静态方法一样);
- 是一个抽象方法,直接用分号结尾,不用给出方法体。
- 接口中不能出现属性,如果出现默认都是被 final static 修饰的。
2、类实现接口的语法:
写在定义类的时候。
class 类名 [extends 父类 ]implements 接口名[, 接口名[, ...]] {
// 覆写接口中的所有抽象方法 或者 声明为抽象类后,实现部分方法
}
一个类中可以有一个 public 的类 或者 一个 public 的接口。
abstract
- 修饰类:表示这个类为抽象类,无法被实例化对象。
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) {
}
}
上述代码分析:
- List(接口)包含抽象方法:insert、pushFront。
- AbstractList(抽象类)实现了 List。覆写了 pushFront 方法,抽象方法 insert 没有实现。
- ArrayList(类)继承了 AbstractList ,实现了 List。覆写了 insert 方法。
- LinkedList(类)继承了 AbstractList ,实现了 List。
- 抽象方法一旦被实现了一次,就不再是抽象方法了;
- AbstractList 只是线性表,把公共代码提取出来服用。所以无法实现 insert 方法,因为顺序表(ArrayList)和链表(LinkedList)的 insert 是不同的。
final
- 修饰变量,为不可变变量,只有一次赋值的机会。
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() {} // 该方法无法被其子类覆写
}
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
*/
}
}
- 限定符 static:
- 被 static 修饰的属性就是静态属性;
- 被 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,指向调用该方法的对象。可以按照下图理解:
(2) return new Person();
实际为 Person.createPerson(); 没有形参。 可以按照下图理解:
- 访问静态属性、调用静态方法的语法:
- 内部访问(调用):
- 属性:
- 属性名称; // 比较推荐使用
- 类名称.属性名称;
- this.属性名称; // 使用时要保证当前的方法不是静态方法,不推荐
- 方法:
- 方法名称(实参列表); // 比较推荐使用
- 类名称.方法名称(实参列表);
- this.方法名称(实参列表); // 使用时要保证当前的方法不是静态方法,不推荐
- 属性:
- 外部访问(调用):
- 属性:
- 类名称.属性名称; // 比较推荐使用
- 对象的引用.属性名称;
- 方法:
- 类名称.方法名称(实参列表); // 比较推荐使用
- 对象的引用.方法名称(实参列表);
- 属性:
- 内部访问(调用):
在静态方法(静态上下文)中,无法使用非静态的内容。原因:没有一个隐式的对象与该方法绑定。
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 模型的总结:
-
修饰代码块:发生在类的加载时,没有对象。
-
修饰变量:
- 如果不用 static 修饰,属性放在对象中,即对象存储在堆区。普通属性先有对象,才有属性;
- 如果用 static 修饰,属性放在类中,类放在方法区。所以static 修饰的变量放在方法区。
-
修饰方法:
- 如果不用 static 修饰(有 this)。(运行时期)在方法的调用时,参数中会有一个隐式的引用,即this,指向放到的调用对象;
- 如果用 static 修饰,表现为不能直接使用属性和方法 (没有 this)。(运行时期)在方法的调用时调用栈,参数中没有这个隐式的引用。
-
定义静态方法、静态属性的依据:
- 如果方法是某一个对象的,则为非静态方法;
- 如果方法与对象无关,则为静态方法。
偷懒做法:main 直接调的,一般都是静态方法。
举例:通讯录类 操作:根据姓名查询电话 —> 非静态方法
新建一本通讯录 —> 静态方法
3、普通属性的初始化:
发生在对象的实例化时期。
普通属性初始化的方式:
- 定义时初始化 int a = 10;
- 构造代码块中初始化 { a = 10; }
- 在构造方法中初始化 Person() { a = 10; }
普通属性初始化的顺序:
-
定义时的初始化 和 构造代码块的初始化 按书写顺序进行;
-
构造方法中的初始化一定发生在最后,与书写顺序无关。
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();
}
}
按照普通属性的初始化顺序规则,上述代码运行的结果为:
4、静态属性的初始化:
发送在类被加载的时候。
静态属性初始化的方式:
- 定义时初始化 static int a = 10;
- 静态构造代码块中初始化 static { a = 20; }
类加载: ==> 发生在运行时期
类的信息一开始是以字节码(bytecode)*.class 的形式保存在磁盘上的,
类加载的过程是:类加载器(ClassLoader)在对象的目录上找到指定类的字节码文件,并且进行解析,然后放到内存的方法区中的过程。
类只有在被使用到的时候才会进行加载(且一般不会卸载),其中类被使用到的情况有:
- 用类去实例化对象;
- 调用静态方法;
- 访问静态属性。
类的加载一定在对象实例化之前,且只加载一次。也就是说静态属性的初始化一定在普通属性的初始化之前。
关于类加载的总结:
-
做什么事:把类从磁盘加载到内存中。
- 谁负责:类的加载器。
- 从磁盘的哪个位置加载:由配置的方式决定,体现为 CLASSPATH 环境变量。
- 加载到内存的哪个位置:方法区,主要加载的是整个类的结构、方法代码。
-
什么时机:用到类的时候,即运行时期。
-
程序一开始就进行类的加载吗:不是,按需加载,即懒加载(进行懒加载的还有 HashMap、HashTable)。
-
什么样的情况下需要:
- 构造对象。举例:造自行车(对象)需要自行车图纸(类)。
- 调用静态方法、访问静态属性时。举例:自行车图纸本身的属性,与自行车无关。
- 反射。举例:Class<?> cls = Class.forName(“Person”); 类也是用保存对象的形式保存的,即类对象的类。
- 用到这个类的子类时,会优先加载父类。
-
类的加载时会执行类的初始化,具体会做那些事情:
- 执行静态属性的初始化;
- 执行静态代码块。
具体执行中会按定义的顺序执行上述两者。
-
静态属性初始化的顺序:
按照定义类是的书写顺序初始化。
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();
}
}
按照静态属性的初始化顺序规则,上述代码运行的结果为:
5、内部类
定义在类的内部的类,可分为:
- 静态内部类和普通内部类
class OutterClass {
static class StaticClass {}
class InnerClass {}
}
注意:
a. 定义在成员级别,不能用 static 修饰;
b. 定义在方法级别
- 有名的内部类和匿名的内部类(可以用 Lambda 表达式替换)
class A {
class B {} // 定义一个内部类
// 定义一个匿名的内部类,同时实例化一个对象
new C() {}
}
synchronized
Java 的每个对象中都有一个锁,叫监视器锁(monitor lock)。默认为打开的状态。
1、语法
- 作为方法的修饰符
public synchronized void method() { // 带 synchronized 修饰的普通方法
// 具体代码
}
public static synchronized void staticMethod() {} // 带 synchronized 修饰的静态同步方法
- 作为代码块出现
public void block() {
synchronized (this) {}
}
2、作用
-
执行带 synchronized 修饰的普通方法时,抢的是堆中构造的对象的锁。首先需要 lock 引用指向的对象中的锁:
- 如果可以锁,就正常执行代码;
- 否则,需要等其他线程把锁 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)
锁的是引用指向的对象,而不是代码块。即便不是同一个方法,但只要是指向同一个对象,争抢的就是同一把锁。
- 执行带 synchronized 修饰的静态同步方法时,抢的是方法区中类的元对象的锁。有时把类里的对象叫全局锁。
- 执行带 synchronized 修饰的代码块时:
public void block() {
synchronized (this) {
// 大括号开始,加锁
} // 大括号结束,释放锁
}
表现 | 锁的对象 | 什么时候加锁 | 什么时候释放锁 |
---|---|---|---|
修饰普通方法 | this 指向的对象中的锁 | 进入方法 | 退出方法 |
修饰静态方法 | 方法所在类的锁 | 进入方法 | 退出方法 |
修饰代码块 | () 中引用指向的对象 | 进入代码块 | 退出代码块 |
Person.class 就是类的对象(反射的知识)。
3、synchronized 和原子性、可见性、重排序的关系:
-
原子性:如“作用”中所述,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 可以互相交换,但不能和其他语句交换
保证重排序的正确性:锁之前的语句无法重排序到临界区(锁的代码部分),临界区内部的无法重排序到外边。
4、缺点:
理论上所有的问题都可以通过 synchronized 解决。
但是成本非常大,主要是因为线程调度的成本大。