final
final可以修饰成员变量、局部变量、方法和类
final修饰的类不能被子类继承
final修饰的方法不能被子类重写
final修饰的基本数据类型变量,无法进行修改
final修饰的引用类型的变量,只保证地址不变,对象中的内容可以发生改变
补充:final可以和static联用,static和final的顺序可以颠倒,例static final int num = 2 表示静态成员常量一经赋值不可以改变,可通过类名.变量名来访问,注:这种方式不会加载类)
public class Test02 {
final static int num = 2;
static {
System.out.println("静态代码块");
}
public static void main(String[] args) {
System.out.println(Test02.num);
}
}
//下面是另一个测试类
public class Test03 {
public static void main(String[] args) {
System.out.println(Test02.num);
}
}
运行结果:
静态代码块
2
//下面的测试结果
2
第一种情况,因为main方法是静态的,所以会先去加载类,打印2条信息,第二种情况不会加载类只会打印2
//将常量值修改为随机数
final static int num = (int)(Math.random()*30);
运行结果:
静态代码块
28
//下面的测试结果
静态代码块
22
静态成员常量的值在加载前无法确定,那么会导致类加载。
math.random在加载前无所确定具体的值,故会先将Test02类加载
abstract
abstract关键字是和final对立的
abstract修饰类时,表示这个类是抽象类。
abstract修饰方法时,必须要被子类重写
abstract关键字不能修饰成员变量或局部变量
abstract不能和final一起修饰类
抽象类和接口
抽象类:
为什么需要抽象类?
避免子类的随意设计,提高了代码可读性 ,提高了子类的健壮性
1:抽象类中只能包含抽象方法吗?
否,抽象类既可以定义抽象方法也可以定义普通方法
2:是否可以定义构造器
抽象类可以存在构造器但是无法实例化
抽象类中的构造器是给子类准备的
抽象类就是用来被继承的 抽象方法就是被重写的
3:子类继承了抽象了之后一定要重写所有的抽象方法
接口的基本概念:
- 接口是一个规范,是一套标准 ,比抽类更抽象,故不能实例化
- 学习接口的方法和学习类是一样的
- 接口定义的方式:修饰符 interface 接口名{}
- 接口中的变量都是公开的,静态的最终常量值 ,默认情况下变量都是public static final修饰,调用时不会加载当前类
- 接口中可以定义静态方法(不建议1.8)
- 接口中定义的对象方法都是抽象方法 接口中的方法默认就是通过abstract修饰的
- 接口中的默认方法从1.8之后才开始被使用 允许在接口中定义default方法 而且存在方法体
接口深入:
1、类和接口直接通过implements 发生关系 类实现接口
2、类必须要实现接口中的所有抽象方法,
3、一个类可以实现多个接口 类名 implements 接口1,接口2。。。。。
4、一个类实现了接口之后 要将当前接口以及接口的父接口中的所有抽象方法全部重写
5、接口可以多继承
6、接口无法实例化
7、接口没有构造器
8、接口中也可以使用多态
抽象类和接口经常被拿来进行比较,下面对它们进行了比较
抽象类 | 接口 |
---|---|
不可以实例化 | 不可以实例化 |
用extends来继承 | 用implements来实现 |
单继承、多实现 | 可以多继承 |
抽象类里允许含有非抽象方法,抽象方法不能用private修饰 | 接口的方法默认为public abstract型的,且只能为这个类型,(jdk8之后可以含有默认类型和静态方法) |
可以有构造器 | 不能有构造器 |
抽象类的所有抽象方法必须被子类重写,如果该子类也是抽象类不允许有同名的抽象方法,它的抽象方法最终要被一个不是抽象类的子类重写 | 接口的所有方法必须被实现类实现即重写 |
class Son extends Father {
void eat() {
System.out.println("儿子吃饭");
}
}
abstract class Father extends Grandfather{
}
abstract class Grandfather{
abstract void eat();
}
抽象类的抽象方法必须被最终子类(非抽象类)重写
类执行顺序
要理解类中的执行顺序,需要先了解代码块在类中的意义
代码块就是放在{}里的一段代码,可分为静态代码块、成员代码块 和局部代码块
静态代码块:
- 声明在类中、方法外的代码块(有static关键字修饰)
- 类加载的时候就会被加载并且只加载1次静态内容
成员代码块(也称初始化块):
- 声明在类中、方法外的代码块 (没有static关键字修饰)
- 初始化块在类加载的时候是不会执行的
- 在创建对象之前会被调用(对于对象中的一些初始进行初始化操作)
局部代码块:
- 声明在方法中的代码块
- 缩减局部变量的生命周期,提高内存的使用率
类中的执行顺序:
- 首先执行静态内容(加载) 静态代码块
- 初始化块
- 构造器
public class Test04 {
public static void main(String[] args) {
new Son2().m();
}
}
class Father{
static {
System.out.println("Father static");
}
{
System.out.println("Father init");
}
public Father() {
System.out.println("Father construct");
}
}
class Son1 extends Father{
static {
System.out.println("Son1 static");
}
{
System.out.println("Son1 init");
}
public Son1() {
System.out.println("Son1 construct");
}
}
class Son2 extends Father{
static {
System.out.println("Son2 static");
}
{
System.out.println("Son2 init");
}
public Son2() {
System.out.println("Son2 construct");
}
public void m() {
new Son1();
}
}
//运行结果:
Father static
Son2 static
Father init
Father construct
Son2 init
Son2 construct
Son1 static
Father init
Father construct
Son1 init
Son1 construct
当发生继承关系时,系统会先去加载父类,所以会先执行父类的静态代码块,然后再加载子类的静态代码块,执行完这步后子类要去调用构造器来创建对象,同样要先去调用父类的构造器,由于调用构造器是在创建对象,所以先执行初始代码块,再执行构造器,父类执行完后子类再执行,完成对象创建后,调用方法去创建另一个子类对象,同样先去加载父类的静态代码块,因为已经加载过一次所以不执行,后面重复前面的操作。
补充:(类加载的2种方式)
使用当前类中的静态方法、静态变量 (如果是调用final static修饰的静态成员常量不会进行类加载)
创建当前类的实例对象
类型转换(引用类型)
引用类型也有类型转换:
自动转换(向上转型):
父类型 变量名 = 子类对象;(new 子类对象|子类对象的变量)
强制转换(向下转型):
子类型 变量名 = (子类型)父类变量;
注:引用类型里面的类型转换和基本数据类型里的不同
要事先确定了父类变量中实际存储的对象是什么类型否则会报出
ClassCastException 类型转换异常
F f = new F();
S1 s1 = new S1();
s1.fun();
f = s1;
//S2 s2 = (S2)f;编译错误
S1 ss = (S1)f;
在这里f实际存储的是s1对象的地址,强转时必须转化为s1类型的,如果为其他类型,会报类型转换异常。
补充:null可以强转为任意类型 ,null也可以是任意类型(引用类型)