[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的虚拟机也是“瘦身版”
┌───────────────────────────┐
│Java EE │
│ ┌────────────────────┐ │
│ │Java SE │ │
│ │ ┌─────────────┐ │ │
│ │ │ Java ME │ │ │
│ │ └─────────────┘ │ │
│ └────────────────────┘ │
└───────────────────────────┘
- JRE(Java Runtime Environment): 运行Java字节码的虚拟机
- JDK(Java Development Kit)
如果只有Java源码,要编译成Java字节码,就需要JDK,JDK除了包含JRE,还提供了编译器、调试器等开发工具。
┌─ ┌──────────────────────────────────┐
│ │ 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
,代码如下
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
javac
命令将Hello.java
文件编译成字节码文件Hello.class
,然后用java
命令执行这个字节码文件
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的每一行语句必须以分号结束。
public class Hello {
public static void main(String[] args) { // 方法名是main
System.out.println("Hello, world!"); // 语句分号结束
}
}
注释有三种:
// 单行注释
/*
多行注释
这是个注释
还是个注释
*/
/**
* 可以用来自动创建文档的注释
*
* @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字节
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又可以精确地表示,因此,浮点数比较大小有时是不准确的(见下方代码)
- 其他与整形运算相似
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
*/
如果非要比较,可以设置一个阈值,看二者相差是否在这之间:
// 比较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)了:
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,它表示不存在,即该变量不指向任何对象。例如:
String s1 = null; // s1是null
String s2 = s1; // s2也是null
String s3 = ""; // s3指向空字符串,不是null
常量:定义变量的时候,如果加上final修饰符,这个变量就变成了常量,常量在定义时进行初始化后就不可再次赋值,再次赋值会导致编译错误。根据习惯,常量名通常全部大写。
final double PI = 3.14; // PI是一个常量
double r = 5.0;
double area = PI * r * r;
PI = 300; // compile error!
有些时候,类型的名字太长,写起来比较麻烦。例如:
StringBuilder sb = new StringBuilder();
这个时候,如果想省略变量类型,可以使用var关键字,编译器会根据赋值语句自动推断出变量sb的类型是StringBuilder
var sb = new StringBuilder();
多行语句用{ }括起来,括号内部是自身的范围,在语句块中定义的变量,它有一个作用域,就是从定义处开始,到语句块结束。超出了作用域引用这些变量,编译器会报错。
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作用域到此结束
}
}
数组:
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判断语句:
if (条件) {
// 条件满足时执行
} else if (条件) {
} else {
}
- 引用类型判断内容相等要使用equals(),注意避免NullPointerException【重点】
- 浮点数判断相等不能直接用==运算符
- 不推荐省略花括号{} 一条语句时大括号可以省,但不推荐
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)
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);
}
}
循环语句
while (条件表达式) {
循环语句
}
do {
执行循环语句
} while (条件表达式);
for (初始条件; 循环检测条件; 循环后更新计数器) {
// 执行语句
}
// 遍历数组时可使用for-each,n为ns中的元素
for (int n : ns) {
// 执行语句
}
// 当然,也是支持break、continue的
数组操作
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的变量都是引用变量
方法
修饰符 方法返回类型 方法名(方法参数列表) {
若干方法语句;
return 方法返回值;
}
- 方法内部可以使用this访问当前实例
- 方法支持可变参数,如
public void setNames(String... names) {}
- 可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null
setNames
此时的参数names是String[]
类型
构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new操作符。
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
】
向下转型举例:
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
方法判断是否为该实例
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后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:
Object obj = "hello";
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}
可以改写如下:
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来调用。
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";
}
抽象类
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:
class Person {
public abstract void run();
}
把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person类也无法被实例化。编译器会告诉我们,无法编译Person类,因为它包含抽象方法。
必须把Person类本身也声明为abstract,才能正确编译它:
abstract class Person {
public String name;
public abstract void run();
}
我们无法实例化一个抽象类:Person p = new Person(); // 编译错误
。抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。
当我们定义了抽象类Person,以及具体的Student、Teacher子类的时候,我们可以通过抽象类Person类型去引用具体的子类的实例,这种引用抽象类的好处在于,我们对其进行方法调用,并不关心Person类型变量的具体子类型:
Person s = new Student();
Person t = new Teacher();
// 不关心Person变量的具体子类型:
s.run();
t.run();
这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
面向抽象编程的本质就是:
- 上层代码只定义规范(例如:abstract class Person)
- 不需要子类就可以实现业务逻辑(正常编译)
- 具体的业务逻辑由不同的子类实现,调用者并不关心
接口
如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口:interface
所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。
interface Person {
void run();
String getName();
}
当一个具体的class去实现一个interface时,需要使用implements关键字。举个例子:
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中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface
:class Student implements Person, Hello {...}
abstract class | interface | |
---|---|---|
继承 | 只能extends一个class | 可以implements多个interface |
字段 | 可以定义实例字段 | 不能定义实例字段,可定义静态字段且为final 类型 |
抽象方法 | 可以定义抽象方法 | 可以定义抽象方法 |
非抽象方法 | 可以定义非抽象方法 | 可以定义default方法 |
在接口中,可以定义default方法。实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。
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;
}
}