<Java>9 面向对象编程(高级)
本文最后更新于:2023年6月27日 上午
9 面向对象编程(高级)
9.1 类变量和类方法
9.1.1 类变量
类变量:也叫 静态变量/静态属性。是该类所有对象共享的变量。任何一个该类对象访问时都是相同的值,任何一个该类对象修改时也是同一个变量。
语法(推荐):
访问修饰符 static 数据类型 变量名;
或者也可以:
static 访问修饰符 数据类型 变量名;
根据 JDK 版本的不同,类变量存放在 堆 中或 方法区 中。
-
什么时候需要用类变量:
当我们需要让某个类的所有对象都共享一个变量时,就可以考虑使用类变量(静态变量)
-
类变量 与 实例变量(普通属性)的区别:
类变量 是该类所有对象共享的,而 实例变量 是每个对象独享的
-
加上
static
称为 类变量 或 静态变量。否则称为 实例变量/普通变量/非静态变量 -
静态变量 可以通过
类名.类变量名;
或对象名.类变量名;
来访问。但 Java 设计者推荐我们用类名.类变量名;
来访问。(需满足访问权限和范围) -
类变量 是在加载类时就初始化了。所以,没有创建对象实例也能访问。
-
类变量 的生命周期是随着 类的加载 开始,随着 类的消亡 而销毁。
-
特别地:一个 null 对象也可以访问静态变量 / 静态方法
public class Test{ static int n = 0; static void met() { System.out.println(++n); } public static void main(String[] args){ Test t = null; System.out.println(t.n); //这样不会报错 t.met(); //这样也不会报错 } }
9.1.2 类方法
当方法使用
static
修饰后,就是 静态方法。静态方法就能访问静态属性。如果我们不希望创建实例,也能调用方法,这个场合把方法做成静态方法是合适的。开发工具类时就可以如此做。
- 类方法和普通方法都是随着类的加载而加载,将结构信息存储在 方法区。
- 类方法中不允许使用和对象有关的关键字。所以,类方法没有
this
或super
- 类方法可以通过类名调用,也能通过对象名调用。普通方法不能通过类名调用。
- 类方法 中只能访问 类变量 或 类方法
- 普通方法既可以访问普通方法也可以访问类方法
9.2 理解 main
方法语法
public static void main(String[] args){...}
-
main
方法 是 JVM 调用的方法。所以该方法的 访问权限 必须为public
-
JVM 在执行
main
方法时不必创建对象,所以main
方法 必须为static
-
该方法接收
String
类型的数组参数。该数组中保存执行 Java 命令 时传递给所运行的类的参数。工作台中:
javac 执行的程序.java
java 执行的程序 参数1(arg[0]) 参数2(arg[1]) 参数3(arg[2]) ..
-
在
main
方法 中,我们可以直接调用main
方法 所在类的静态方法或静态属性。但是,不能直接访问该类中的非静态成员,必须创建该类的一个实例后才能通过该实例访问非静态成员。
9.3 代码块
代码块:又称为 初始化块。属于类中的成员。类似于方法,将逻辑语句封装在方法体中,通过
{ }
包围起来。和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类 显式调用,而是加载类时,或创建对象时 隐式调用。
语法:
[修饰符]{代码};
- 修饰符 是可选项,可不写。要写的话,只能写
static
- 代码块分为两类:
- 静态代码块:有
static
- 普通代码块:无
static
- 静态代码块:有
- 逻辑语句可以为任意的逻辑语句。
;
可以写,也可以省略。建议写上。- 代码块相当于另一种形式的构造器(构造器的补充机制),可以做初始化操作
- 如果多个构造器中都有重复语句,就可以抽取到初始化块中,提高代码复用率。这样,不管用哪个构造器,都会执行代码块。
9.3.1 使用细节
static
代码块:作用是对类进行初始化。随着 类的加载 会且只会执行一次。相对的:普通代码块每创建一个对象就执行一次。
-
**类什么时候被加载? **
-
创建对象实例时(new)
-
创建子类对象实例,父类也会加载
-
使用类的静态成员时(父类也会加载)
以上情况下类会被加载。加载后不需要再次加载,所以,静态代码块也只会执行一次。
-
-
创建一个对象时,在 一个类里 调用顺序是:
- 调用静态代码块 和 静态属性初始化。这两者优先级相同,多个存在时按照定义的顺序依次执行。
- 调用普通代码块 和 普通属性初始化。这两者优先级也相同。
- 调用构造器。
-
构造器
的最前面其实隐含了super();
和调用普通代码块
。而静态相关的代码块,属性初始化,在类加载时就执行完毕了。这样,创建一个对象时,在 有继承关系的多个类里 调用顺序是:
- 父类 静态代码块 和 静态初始化
- 子类 静态代码块 和 静态初始化
- 父类 普通代码块 和 普通初始化
- 父类 构造器
- 子类 普通代码块 和 普通初始化
- 子类 构造器
-
静态代码块 只能调用 静态成员。普通代码块 能调用 任意成员。
9.4 单例设计模式
什么是设计模式:设计模式是在大量的实践中总结和理论化后优选的代码结构、编程风格、解决问题的思考方式。设计模式就像是经典的棋谱,免去我们自己再思考和摸索。
单例设计模式:采取一定的方法,保证再整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法
9.4.1 应用实例
后面会学更多,这里先展示两种:饿汉式、懒汉式
#9.4.1.1 饿汉式
步骤如下:
-
构造器私有化(防止用户直接 new)
-
类的内部创建对象
-
向外暴露一个静态的公共方法
-
代码实现
class GF{ private String name; private static GF gf = new GF("萝茵"); private GF(String name){ this.name = name; } public static GF getGF(){ return gf; } }
对象,通常都是重量级的对象
有时,我们用不到这个创建的对象,那个场合,会造成资源浪费。
#9.4.1.2 懒汉式
步骤如下:
-
构造器私有化
-
定义一个静态属性对象
-
提供一个静态的公共方法,可以返回对象。如果静态对象为空,则创建对象
-
代码实现
>class GF{ private String name; private static GF gf; private GF(String name){ this.name = name; } public static GF getGF(){ if(gf == null){ gf = new GF("萝茵"); } return gf; } >}
#9.4.1.3 两种方法对比
- 二者创建对象的时机不同。饿汉式在加载类信息时创建,懒汉式在使用时才创建
- 饿汉式可能造成资源浪费,懒汉式可能存在线程安全问题(学习[线程]后会进行完善)。
- Java SE 标准类中 java.lang.Runtime 就是一个单例模式。
9.5 final
关键字
final
可以修饰 类、属性、方法、局部变量以下情况下,可能用到
final
final
修饰类:该类不能被继承final
修饰方法:该方法不能被重写final
修饰值:该值不能被修改
9.5.1 使用细节
-
final
修饰的属性又叫常量,一般用 XX_XX_XX 来命名(全大写字母+下划线) -
final
修饰的属性在定义时,必须赋初始值,且之后不能再修改。赋值可以在下列位置之一:- 定义时
- 构造器中
- 代码块中
注意:如果
final
修饰的属性是静态的,则只能在以下位置赋值。- 定义时
- 静态代码块中
-
final
类不能继承,但能实例化对象。对的,是可以的。 -
如果不是
final
类,但含有final
方法,虽然该方法不能重写,但能被继承。 -
final
类可以有final
方法。可以,但没必要。 -
final
不能修饰构造方法。 -
final
和static
搭配使用,效率更高(那个场合,虽然顺序不限,还是推荐static
在前)。底层编译器做了优化处理。这样做,调用 属性(定义时赋值) 时居然 不会造成类的加载! -
包装类(Integer、Double、Float、Boolean、String等)都是
final
类,都不能被继承。
9.6 抽象类
当父类的某些方法需要声明,却不知道如何实现时,可以将其声明为抽象方法。那个场合,要将该类声明为
abstract
类。抽象类的价值更多是用于设计。设计者设计好后,让子类继承并实现。也是考官爱问的考点。
定义抽象类:
访问修饰符 abstract 类名{...}
定义抽象方法(注意:无方法体):
访问修饰符 abstract 返回值 方法名(形参列表);
9.6.1 使用细节
- 抽象类不能被实例化
- 抽象类不一定包含抽象方法。也就是说,抽象类可以没有
abstract
方法 - 一旦包含
abstract
方法,则该类一定要声明为abstract
abstract
只能修饰 类 和 方法,不能修饰其他。- 抽象类可以有任意成员(非抽象方法、构造器、静态属性等)。即,抽象类本质还是类。
- 抽象方法不能有主体。即,抽象方法不能实现。
- 如果一个类继承了
abstract
类,则其必须实现所有abstract
方法,除非其自己也是abstract
类。 - 抽象方法不能用
private
final
static
来修饰。因为,这些关键词都和 重写 相违背。
9.6.2 模板设计模式
9.7 接口
接口就是给出一些没有实现的方法,封装到一起,到某个类要用的时候,再根据具体情况把这些方法写出来。
语法:
interface 接口名{...}
class 类名 implements 接口名{...必须实现接口的抽象方法...}
注意:JDK 7.0 以前,接口中只能是抽象方法。而 JDK 8.0 后,接口可以有静态(
static
)方法、默认(default
)方法。在接口中,抽象方法可以省略
abstract
接口中可以存在:
- 属性(只有静态
static
属性,可以不加static
关键字) - 方法(抽象
abstract
方法、默认default
实现方法、静态static
方法)
9.7.1 使用细节
- 接口 不能被实例化。
- 接口中所有方法都是
public
方法。接口中的 抽象方法 可以不用abstract
修饰。 - 一个普通类实现接口,就必须把该接口所有方法都实现。(用快捷键吧
alt + enter
) - 抽象类实现接口,可以不用实现接口的方法。
- 一个类可以同时实现多个接口。
class Name implements In1,In2{...}
- 接口中的属性只能是
final
的,并且是public static final
修饰符。修饰符就算不写,还是这样。 - 接口中属性的访问形式:
接口名.属性名
- 接口不能 继承 其他的类,但可以 继承 多个别的接口。(不是也不能 实现 别的接口)
- 接口的修饰符只能是
public
和 默认。这点和类的修饰符相同。
9.7.2 实现接口 vs 继承类
- 当子类继承父类,就自动拥有父类的所有功能。如果需要扩展功能,可以通过接口方式扩展。
- 可以认为,接口 是对于 Java 单继承机制的补充。
- 继承的价值主要在于:解决代码的复用性和可维护性。
- 接口的价值主要在于:设计。设计好各种规范,让其他类去实现这些方法。
- 接口比继承更加灵活。继承需要满足 is - a 的关系,而接口只需要满足 like - a 关系。
- 接口在一定程度上实现代码解耦。(即:接口规范性 + 动态绑定机制)
9.7.3 接口的多态特性
-
多态参数(接口的引用可以指向实现了接口的类的对象)
viod work(Inerface01 i1){...}
参数可以传入任意实现该接口的类 -
多态数组
-
接口存在多态传递现象
9.8 内部类
一个类的内部又完整的嵌套了另一个类结构。被嵌套的类被称为 内部类。
class Outer{ //外部类 class Inner{ //内部类 } } class Other{ //外部其他类 }
内部类的最大特点是可以直接访问私有属性,并且可以体现类与类之间的包含关系。
9.8.1 四种内部类
分别是:
- 定义在外部类的局部位置上
- 局部内部类:有 类名
- 匿名内部类:无 类名
- 定义在外部类的成员位置上
- 成员内部类:无
static
修饰 - 静态内部类:
static
修饰的类
- 成员内部类:无
9.8.2 局部内部类
局部内部类:定义在外部类的局部位置上,并且有类名。(局部位置?比如:方法/代码块里)
class Outer { //外部类 public void tools01() { class Inner { //局部内部类 } } }
#9.8.2.1 使用细节
-
定义在外部类的局部位置上,并且有类名。
-
可以访问外部类的所有成员,包含私有成员
-
局部内部类可以 直接访问 外部类的成员。
-
不能添加 访问修饰符,因为其地位相当于局部变量。但,可以使用
final
,因为局部变量也能用final
-
作用域 仅仅在定义它的方法或代码块中
-
外部类 在方法中,可以创建 局部内部类 的对象实例,然后调用方法。
-
外部其他类 不能访问 局部内部类
-
如果外部类和局部内部类的成员重名时,默认遵循就近原则。那个场合,访问外部类成员使用
外部类名.this.变量名
外部类名.this
本质就是 外部类的对象。即,调用了该方法(上例的tools01
)的对象还不懂的话,看一下 这个视频 悟一悟
9.8.3 匿名内部类
匿名内部类:定义在外部类的局部位置,且没有类名
>new 类/接口 (参数列表) { 类体 >}
匿名内部类本质是没有名字的类,而且是内部类。同时,还是一个对象。
可以用匿名内部类简化开发
一个例子
>class Outer { //外部类 public void tools01() { Inter whatEver = new Inter(){ //匿名内部类 }; } >} >interface Inter{ >}
其实,这个匿名内部类
new Inter(){}
的运行类型就是class XXXX implements Inter
。系统自动分配的名字是Outer$1
(whatEver.getClass = "Outer$1"
)JDK 在创建匿名内部类
Outer$1
时,立即创建了一个对象实例,并将地址返回给了whatEver
匿名内部类使用一次后就不能再次使用(
Outer$1
就这一个了)
#9.8.3.1 使用细节
- 匿名内部类语法比较独特。其既是一个类的定义,也是一个对象。因此,从语法上看,其既有 定义类的特征,也有 创建对象的特征。
- 可以访问外部类的所有成员,包括私有的。
- 局部内部类可以 直接访问 外部类的成员。
- 不能添加 访问修饰符,因为其地位相当于局部变量。但,可以使用
final
,因为局部变量也能用final
- 作用域:仅仅在定义它的方法或方法快中
- 外部其他类 不能访问 匿名内部类
- 如果外部类和匿名内部类的成员重名时,默认遵循就近原则。那个场合,访问外部类成员使用
外部类名.this.变量名
#9.8.3.2 使用场景
-
当作实参直接传递,简洁高效
public class Homework1 { public static void main(String[] args) { new Cellphone().clock(new Bell() { //看这里看这里 @Override public void belling() { System.out.println("小懒猪起床了!"); } }); } } interface Bell { void ringing(); } class Cellphone{ public void clock(Bell bell){ bell.ringing(); } }
9.8.4 成员内部类
成员内部类:定义在外部类的成员位置,并且没有
static
修饰。
class Outer{ class Inner{ } }
#9.8.4.1 使用细节
- 可以直接访问外部类的所有成员,包括私有的
- 可以添加任意访问修饰符。因为,成员内部类的地位就是一个成员。
- 作用域 和外部类其他成员相同,为整个类体。
- 局部内部类可以 直接访问 外部类的成员。
- 外部类可以通过创建对象的方式访问成员内部类
- 外部其他类访问成员内部类
Outer.Inner name = Outer.new Inner();
下个方法的缩写Outer.Inner name = new Outer().new Inner();
- 在外部类中编写一个方法,返回一个
Inner
的对象实例(就是对象的 getter)
- 如果外部类和匿名内部类的成员重名时,默认遵循就近原则。那个场合,访问外部类成员使用
外部类名.this.变量名
9.8.5 静态内部类
静态内部类:定义在外部类的成员位置,经由
static
修饰。
class Outer{ static class Inner{ } }
#9.8.5.1 使用细节
- 可以直接访问外部类的所有 静态 成员,包括私有的。但不能访问非静态成员
- 可以添加访问修饰符。因为,静态内部类的地位就是一个成员。
- 作用域 和其他成员相同,为整个类体。
- 静态内部类可以 直接访问 外部类的成员。
- 外部类可以通过创建对象的方式访问静态内部类
- 外部其他类访问静态内部类
Outer.Inner name = new Outer.Inner();
即通过类名直接访问- 在外部类中编写一个方法,返回一个
Inner
的对象实例 - 如果外部类和匿名内部类的成员重名时,默认遵循就近原则。那个场合,访问外部类成员使用
外部类名.变量名
。(怎么不一样了呢?因为静态内部类访问的都是静态成员)