java学习笔记
tbghg

[TOC]

记录一下学习java时觉得重点或一时没记住的知识点,方便日后复习,持续更新,有东西就加

java基础

认识java

Java介于编译型语言和解释型语言之间。编译型语言如C、C++,代码是直接编译成机器码执行,但是不同的平台(x86、ARM等)CPU的指令集不同,因此,需要编译出每一种平台的对应机器码。

解释型语言如Python、Ruby没有这个问题,可以由解释器直接加载源码然后运行,代价是运行效率太低。

而Java是将代码编译成一种“字节码”,它类似于抽象的CPU指令,然后,针对不同平台编写虚拟机,不同平台的虚拟机负责加载字节码并执行,这样就实现了“一次编写,到处运行”的效果。当然,这是针对Java开发者而言。

对于虚拟机,需要为每个平台分别开发。为了保证不同平台、不同公司开发的虚拟机都能正确执行Java字节码,SUN公司制定了一系列的Java虚拟机规范。从实践的角度看,JVM的兼容性做得非常好,低版本的Java字节码完全可以正常运行在高版本的JVM上。

  • Java SE(Standard Edition): 标准版,包含标准的JVM和标准库
  • Java EE(Enterprise Edition): 企业版,在Java SE的基础上加上了大量的API和库,以便方便开发Web应用、数据库、消息服务等
  • Java ME(Micro Edition): 针对嵌入式设备的“瘦身版”,Java SE的标准库无法在Java ME上使用,Java ME的虚拟机也是“瘦身版”
1
2
3
4
5
6
7
8
9
┌───────────────────────────┐
│Java EE │
│ ┌────────────────────┐ │
│ │Java SE │ │
│ │ ┌─────────────┐ │ │
│ │ │ Java ME │ │ │
│ │ └─────────────┘ │ │
│ └────────────────────┘ │
└───────────────────────────┘
  • JRE(Java Runtime Environment): 运行Java字节码的虚拟机
  • JDK(Java Development Kit)

如果只有Java源码,要编译成Java字节码,就需要JDK,JDK除了包含JRE,还提供了编译器、调试器等开发工具。

1
2
3
4
5
6
7
8
9
10
11
┌─    ┌──────────────────────────────────┐
│ │ Compiler, debugger, etc. │
│ └──────────────────────────────────┘
JDK ┌─ ┌──────────────────────────────────┐
│ │ │ │
│ JRE │ JVM + Runtime Library │
│ │ │ │
└─ └─ └──────────────────────────────────┘
┌───────┐┌───────┐┌───────┐┌───────┐
│Windows││ Linux ││ macOS ││others │
└───────┘└───────┘└───────┘└───────┘
  • JSR规范:Java Specification Request
  • JCP组织:Java Community Process

为了保证Java语言的规范性,SUN公司搞了一个JSR规范,凡是想给Java平台加一个功能,比如说访问数据库的功能,大家要先创建一个JSR规范,定义好接口,这样,各个数据库厂商都按照规范写出Java驱动程序,开发者就不用担心自己写的数据库代码在MySQL上能跑,却不能跑在PostgreSQL上。

所以JSR是一系列的规范,从JVM的内存模型到Web程序接口,全部都标准化了。而负责审核JSR的组织就是JCP。

一个JSR规范发布时,为了让大家有个参考,还要同时发布一个“参考实现”,以及一个“兼容性测试套件”:

  • RI:Reference Implementation
  • TCK:Technology Compatibility Kit

比如有人提议要搞一个基于Java开发的消息服务器,这个提议很好啊,但是光有提议还不行,得贴出真正能跑的代码,这就是RI。
如果有其他人也想开发这样一个消息服务器,如何保证这些消息服务器对开发者来说接口、功能都是相同的?所以还得提供TCK。

通常来说,RI只是一个“能跑”的正确的代码,它不追求速度,所以,如果真正要选择一个Java的消息服务器,一般是没人用RI的,大家都会选择一个有竞争力的商用或开源产品。

运行一个Demo,文件名为Hello.java,代码如下

1
2
3
4
5
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}

javac命令将Hello.java文件编译成字节码文件Hello.class,然后用java命令执行这个字节码文件

1
2
3
4
5
6
7
8
9
10
D:\Desktop\Java_project>javac Hello.java

D:\Desktop\Java_project>dir
2023/11/30 16:05 <DIR> .
2023/11/29 22:19 <DIR> ..
2023/11/30 16:05 417 Hello.class
2023/11/30 16:04 122 Hello.java

D:\Desktop\Java_project>java Hello
Hello, world!

注意:给虚拟机传递的参数Hello是我们定义的类名,虚拟机自动查找对应的class文件并执行,

Java 11之后,可以通过java Hello.java可以直接运行一个单文件源码,需要注意的是,在实际项目中,单个不依赖第三方库的Java源码是非常罕见的。
所以,绝大多数情况下,我们无法直接运行一个Java源码文件,原因是它需要依赖其他的库。

  • 一个Java源码只能定义一个public类型的class,并且class名称和文件名要完全一致;
  • 使用javac可以将.java源码编译成.class字节码;
  • 使用java可以运行一个已编译的Java程序,参数是类名。

基础语法

Java是面向对象的语言,一个程序的基本单位就是class,类名要求:必须以英文字母开头(大写),后接字母,数字和下划线的组合

Java入口程序规定的方法必须是静态方法,方法名必须为main,括号内的参数必须是String数组。

方法名也有命名规则,命名和class一样,但是首字母小写,在方法内部,语句才是真正的执行代码。Java的每一行语句必须以分号结束。

1
2
3
4
5
public class Hello {
public static void main(String[] args) { // 方法名是main
System.out.println("Hello, world!"); // 语句分号结束
}
}

注释有三种:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 单行注释

/*
多行注释
这是个注释
还是个注释
*/

/**
* 可以用来自动创建文档的注释
*
* @auther tbghg
*/

第三种以/**开头,以*/结束,如果有多行,每行通常以星号开头,需要写在类和方法的定义处,可以用于自动创建文档。

数据类型

基本类型

基本类型如下:

  • 整数类型:byte,short,int,long
  • 浮点数类型:float,double
  • 字符类型:char
  • 布尔类型:boolean

计算机内存的最小存储单元是字节(byte),一个字节就是一个8位二进制数,即8个bit,不同数据类型所占字节数:

  • byte 1字节 -128 ~ 127
  • short 2字节 -32768 ~ 32767
  • char 2字节
  • int 4字节 -2147483648 ~ 2147483647
  • float 4字节
  • long 8字节 -9223372036854775808 ~ 9223372036854775807
  • double 8字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Main {
public static void main(String[] args) {
int i = 2147483647;
int i2 = -2147483648;
int i3 = 2_000_000_000; // 加下划线更容易识别
int i4 = 0xff0000; // 十六进制表示的16711680
int i5 = 0b1000000000; // 二进制表示的512

long n1 = 9000000000000000000L; // long型的结尾需要加L
long n2 = 900; // 没有加L,此处900为int,但int类型可以赋值给long
int i6 = 900L; // 错误:不能把long型赋值给int


float f1 = 3.14f;
float f2 = 3.14e38f; // 科学计数法表示的3.14x10^38
float f3 = 1.0; // 错误:不带f结尾的是double类型,不能赋值给float

double d = 1.79e308;
double d2 = -1.79e308;
double d3 = 4.9e-324; // 科学计数法表示的4.9x10^-324

// Java语言对布尔类型的存储并没有做规定,因为理论上存储布尔类型只需要1 bit,但是通常JVM内部会把boolean表示为4字节整数。
boolean b1 = true;
boolean b2 = false;
boolean isGreater = 5 > 3; // 计算结果为true
int age = 12;
boolean isAdult = age >= 18; // 计算结果为false

// 字符类型char表示一个字符。字符使用单引号。一个char保存一个Unicode字符
char a = 'A'; // Unicode编码,都占用两字节
char zh = '中'; // Unicode编码,都占用两字节

System.out.println(a);
System.out.println(zh);

// 可以直接用转义字符\u+Unicode编码来表示一个字符,必须使用十六进制
char c3 = '\u0041'; // 'A',因为十六进制0041 = 十进制65
char c4 = '\u4e2d'; // '中',因为十六进制4e2d = 十进制20013

System.out.println(c3);
System.out.println(c4);

}
}

整数运算:

  • 整数的除法对于除数为0时运行时将报错,但编译不会报错
  • 整数除法不能整除会舍弃小数
  • 溢出不会出错,却会得到一个奇怪的结果
  • i++ ++i << >> >>>无符号右移 & | ~非 ^异或
  • 类型不统一时,会自动转换:在运算过程中,如果参与运算的两个数类型不一致,那么计算结果为较大类型的整型
  • 支持强制转换,如short s = (short) i

浮点数运算:

  • 只能进行加减乘除这些数值计算,不能做位运算和移位运算
  • 浮点数常常无法精确表示,如0.1,二进制是无限循环小数,但0.5又可以精确地表示,因此,浮点数比较大小有时是不准确的(见下方代码)
  • 其他与整形运算相似
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Main {
public static void main(String[] args) {
// 0.1在二进制是无限循环小数,只能存储一个近似值,所以x与y不相等
double x = 1.0 / 10;
double y = 1 - 9.0 / 10;
// 观察x和y是否相等:
System.out.println(x);
System.out.println(y);
System.out.println(x == y);

// 0.5在二进制中可精确表示,所以x与y相等
x = 5.0 / 10;
y = 1 - 5.0 / 10;
System.out.println(x);
System.out.println(y);
System.out.println(x == y);
}
}

/* output:
0.1
0.09999999999999998
false
0.5
0.5
true
*/

如果非要比较,可以设置一个阈值,看二者相差是否在这之间:

1
2
3
4
5
6
7
8
// 比较x和y是否相等,先计算其差的绝对值:
double r = Math.abs(x - y);
// 再判断绝对值是否足够小:
if (r < 0.00001) {
// 可以认为相等
} else {
// 不相等
}

布尔运算:

  • 短路运算:如果一个布尔运算的表达式能提前确定结果,则后续的计算不再执行,直接返回结果
  • 支持三元运算符:b ? x : y
引用类型

除了基本类型,剩下的都是引用类型,最常用的是String,引用类型的变量类似于C语言的指针,它内部存储一个“地址”,指向某个对象在内存的位置

String,用双引号”…”表示字符串。一个字符串可以存储0个到任意个字符,转义:

  • " 表示字符”
  • ' 表示字符’
  • \ 表示字符\
  • \n 表示换行符
  • \r 表示回车符
  • \t 表示Tab
  • \u#### 表示一个Unicode编码的字符

字符串拼接可直接使用+。如果用+连接字符串和其他数据类型,会将其他数据类型先自动转型为字符串,再连接。

从Java 13开始,字符串可以用”””…”””表示多行字符串(Text Blocks)了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) {
// 末尾会带上换行,总共五行
String s = """
SELECT * FROM
users
WHERE id > 100
ORDER BY name DESC
""";

// 末尾没换行,共四行
String s2 = """
SELECT * FROM
users
WHERE id > 100
ORDER BY name DESC""";
System.out.println(s);
}
}

引用类型的变量可以指向一个空值null,它表示不存在,即该变量不指向任何对象。例如:

1
2
3
String s1 = null; // s1是null
String s2 = s1; // s2也是null
String s3 = ""; // s3指向空字符串,不是null

常量:定义变量的时候,如果加上final修饰符,这个变量就变成了常量,常量在定义时进行初始化后就不可再次赋值,再次赋值会导致编译错误。根据习惯,常量名通常全部大写。

1
2
3
4
final double PI = 3.14; // PI是一个常量
double r = 5.0;
double area = PI * r * r;
PI = 300; // compile error!

有些时候,类型的名字太长,写起来比较麻烦。例如:

1
StringBuilder sb = new StringBuilder();

这个时候,如果想省略变量类型,可以使用var关键字,编译器会根据赋值语句自动推断出变量sb的类型是StringBuilder

1
var sb = new StringBuilder();

多行语句用{ }括起来,括号内部是自身的范围,在语句块中定义的变量,它有一个作用域,就是从定义处开始,到语句块结束。超出了作用域引用这些变量,编译器会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static void main(String[] args) {
{
int i = 0; // 变量i从这里开始定义
{
int x = 1; // 变量x从这里开始定义
{
String s = "hello"; // 变量s从这里开始定义
} // 变量s作用域到此结束

// System.out.println(s); // 报错,无法解析符号 's'

// 注意,这是一个新的变量s,它和上面的变量同名,
// 但是因为作用域不同,它们是两个不同的变量:
String s = "hi";

} // 变量x和s作用域到此结束
} // 变量i作用域到此结束
}
}

数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
// 5位同学的成绩:
int[] ns = new int[5];
System.out.println(ns.length); // 5

// 也可以在定义数组时直接指定初始化的元素,这样就不必写出数组大小,而是由编译器自动推算数组大小
int[] ns = new int[] { 68, 79, 91, 85, 62 };

// 可以进一步简写为
int[] ns = { 68, 79, 91, 85, 62 };

}
}
  • 数组所有元素初始化为默认值,整型都是0,浮点型是0.0,布尔型是false
  • 数组一旦创建后,大小就不可改变

流程控制

// todo 输入输出
输入输出:这块之后系统整理下

if判断语句:

1
2
3
4
5
6
7
if (条件) {
// 条件满足时执行
} else if (条件) {

} else {

}
  • 引用类型判断内容相等要使用equals(),注意避免NullPointerException【重点】
  • 浮点数判断相等不能直接用==运算符
  • 不推荐省略花括号{} 一条语句时大括号可以省,但不推荐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
// 引用类型 使用equals判断相等
if (s1.equals(s2)) {
System.out.println("s1 equals s2");
}

String s3 = null;
// 必须先判断null,否则会报NullPointerException错误
if (s3 != null && s3.equals("hello")) {
System.out.println("hello");
}
}
}

switch-case语句

  • switch的计算结果必须是整型、字符串或枚举类型
  • case语句具有穿透性,要带break,可以打开fall-through警告
  • 不要漏掉default,可以打开missing default警告
  • Java 14开始,switch语句正式升级为表达式,使用->格式时,不再需要break,允许携带返回值,通过yield提前中断(类似于return)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) {
String fruit = "orange";
int opt = switch (fruit) {
// 直接返回1
case "apple" -> 1;
// 直接返回2
case "pear", "mango" -> 2;
// 整个代码块用{}包裹,yield可提前结束switch-case语句,并返回结果
default -> {
int code = fruit.hashCode();
yield code; // switch语句返回值
}
};
System.out.println("opt = " + opt);
}
}

循环语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while (条件表达式) {
循环语句
}

do {
执行循环语句
} while (条件表达式);

for (初始条件; 循环检测条件; 循环后更新计数器) {
// 执行语句
}

// 遍历数组时可使用for-each,n为ns中的元素
for (int n : ns) {
// 执行语句
}

// 当然,也是支持break、continue的

数组操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.util.Arrays;

public class Main {
public static void main(String[] args) {
int[] ns = { 1, 1, 8, 3, 5, 4 };

// 1. 遍历

// 直接打印,打印出来的是JVM中的地址,如
System.out.println(ns);

for (int n : ns) {
System.out.print(n+" ");
}
System.out.println();

// java提供了Arrays.toString方法便于打印
System.out.println(Arrays.toString(ns));

/* output:
[I@b4c966a
1 1 8 3 5 4
[1, 1, 8, 3, 5, 4]
*/

// 2. 排序

Arrays.sort(ns);
if (Arrays.toString(ns).equals("[1, 1, 3, 4, 5, 8]")) {
System.out.println("排序正确");
}

// 3. 二维数组

int[][] ns2 = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};

// 可以两层for遍历
for (int[] arr : ns2) {
for (int n : arr) {
System.out.print(n);
System.out.print(" ");
}
System.out.println();
}

// 可以Arrays.deepToString()打印
System.out.println(Arrays.deepToString(ns2));

/* output:
1 2 3 4
5 6 7 8
9 10 11 12
[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
*/
}
}

面向对象

Java是一种面向对象的编程语言。面向对象编程,英文是Object-Oriented Programming,简称OOP

  • 一个Java源文件可以包含多个类的定义,但只能定义一个public类,且public类名必须与文件名一致。如果要定义多个public类,必须拆到多个Java源文件中
  • class和instance是“模版”和“实例”的关系,定义class就是定义了一种数据类型,对应的instance是这种数据类型的实例
  • class定义的field,在每个instance都会拥有各自的field,且互不干扰
  • 通过new操作符创建新的instance,然后用变量指向它,即可通过变量来引用这个instance
  • 指向instance的变量都是引用变量

方法

1
2
3
4
修饰符 方法返回类型 方法名(方法参数列表) {
若干方法语句;
return 方法返回值;
}
  • 方法内部可以使用this访问当前实例
  • 方法支持可变参数,如public void setNames(String... names) {}
    • 可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null
    • setNames此时的参数names是String[]类型

构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new操作符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) {
Person p1 = new Person("Xiao Ming", 15); // 既可以调用带参数的构造方法
Person p2 = new Person(); // 也可以调用无参数构造方法
}
}

class Person {
private String name;
private int age;

public Person() {
}

public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
  • 实例在创建时通过new操作符会调用其对应的构造方法,构造方法用于初始化实例
  • 没有定义构造方法时,编译器会自动创建一个默认的无参数构造方法
  • 可以定义多个构造方法,编译器根据参数自动判断
  • 可以在一个构造方法内部调用另一个构造方法,便于代码复用

方法重载(overload):如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。

注意:方法重载的返回值类型通常都是相同的。

方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。举个例子,String类提供了多个重载方法indexOf(),可以查找子串:

  • int indexOf(int ch):根据字符的Unicode码查找
  • int indexOf(String str):根据字符串查找
  • int indexOf(int ch, int fromIndex):根据字符查找,但指定起始位置
  • int indexOf(String str, int fromIndex)根据字符串查找,但指定起始位置

继承
  • 继承是面向对象编程的一种强大的代码复用方式
  • Java只允许单继承,所有类最终的根类是Object
  • protected允许子类访问父类的字段和方法
  • 子类的构造方法可以通过super()调用父类的构造方法
  • 可以安全地向上转型为更抽象的类型
  • 可以强制向下转型,最好借助instanceof判断
  • Java 14后instanceof判断完可直接赋值
  • 子类和父类的关系是is,has关系不能用继承,可考虑组合

使用extends关键字来实现继承。超类(super class),父类(parent class),基类(base class) | 子类(subclass),扩展类(extended class)

注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段!

子类无法访问父类的private字段或者private方法,为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问

使用super可以指向父类(对应this),子类的构造方法必须调用父类的构造方法,方式为super()并携带上对应参数

正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。从Java 15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。

  • 向上转型:把一个子类型安全地变为更加抽象的父类型【没问题】
  • 向下转型:把一个父类类型强制转型为子类类型【看原本是什么,如果原本就能转那没问题,否则会报错ClassCastException

向下转型举例:

1
2
3
4
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!

Person类型p1实际指向Student实例,Person类型变量p2实际指向Person实例。在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,把p2转型为Student会失败,因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。

因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException。

为了防止向下转型发生问题,一般先通过instanceof方法判断是否为该实例

1
2
3
4
5
6
7
8
9
10
Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false

Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true

Student n = null;
System.out.println(n instanceof Student); // false

从Java 14开始,判断instanceof后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:

1
2
3
4
5
Object obj = "hello";
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}

可以改写如下:

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
Object obj = "hello";
// instanceof判断完成后转换并赋到s中
if (obj instanceof String s) {
// 可以直接使用变量s:
System.out.println(s.toUpperCase());
}
}
}
多态
  • 子类可以覆写父类的方法(Override),覆写在子类中改变了父类方法的行为
  • Java的方法调用总是作用于运行期对象的实际类型,这种行为称为多态
  • final修饰符有多种作用
    • final修饰的方法可以阻止被覆写
    • final修饰的class可以阻止被继承
    • final修饰的field必须在创建对象时初始化,随后不可修改

重载overload方法签名不同,覆写(重写)override方法签名不同相同

加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。

多态的特性就是,运行期才能动态决定调用的子类方法。多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Main {
public static void main(String[] args) {
Person p = new Student();
p.run(); // 应该打印Person.run还是Student.run?

p.runTwice(p);
}
}

class Person {
public void run() {
System.out.println("Person.run");
}
public void runTwice(Person p) {
// run方法有可能会被重写,我们并不知道执行时是person的还是重写后student的,只有根据运行时期实际类型才知道
p.run();
p.run();
}
// 被final修饰,无法重写
public final String hello() {
return "Hello, " + name;
}
}

class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}

// final修饰的类无法被继承
final class Apple {
// 可直接赋值,也可在构造方法中赋值,之后不能再修改该字段
public final String name = "Unamed";
}
抽象类

如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:

1
2
3
class Person {
public abstract void run();
}

把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person类也无法被实例化。编译器会告诉我们,无法编译Person类,因为它包含抽象方法。

必须把Person类本身也声明为abstract,才能正确编译它:

1
2
3
4
abstract class Person {
public String name;
public abstract void run();
}

我们无法实例化一个抽象类:Person p = new Person(); // 编译错误。抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。

当我们定义了抽象类Person,以及具体的Student、Teacher子类的时候,我们可以通过抽象类Person类型去引用具体的子类的实例,这种引用抽象类的好处在于,我们对其进行方法调用,并不关心Person类型变量的具体子类型:

1
2
3
4
5
6
Person s = new Student();
Person t = new Teacher();

// 不关心Person变量的具体子类型:
s.run();
t.run();

这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。

面向抽象编程的本质就是:

  • 上层代码只定义规范(例如:abstract class Person)
  • 不需要子类就可以实现业务逻辑(正常编译)
  • 具体的业务逻辑由不同的子类实现,调用者并不关心
接口

如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口:interface

所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。

1
2
3
4
interface Person {
void run();
String getName();
}

当一个具体的class去实现一个interface时,需要使用implements关键字。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Student implements Person {
private String name;

public Student(String name) {
this.name = name;
}

@Override
public void run() {
System.out.println(this.name + " run");
}

@Override
public String getName() {
return this.name;
}
}

在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interfaceclass Student implements Person, Hello {...}

abstract class interface
继承 只能extends一个class 可以implements多个interface
字段 可以定义实例字段 不能定义实例字段,可定义静态字段且为final类型
抽象方法 可以定义抽象方法 可以定义抽象方法
非抽象方法 可以定义非抽象方法 可以定义default方法

在接口中,可以定义default方法。实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。

default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Main {
public static void main(String[] args) {
Person p = new Student("Xiao Ming");
p.run();
}
}

interface Person {
String getName();
default void run() {
System.out.println(getName() + " run");
}
}

class Student implements Person {
private String name;

public Student(String name) {
this.name = name;
}

public String getName() {
return this.name;
}
}
 评论
评论插件加载失败
正在加载评论插件