8.面向对象编程(高级)
8.1 类变量
概念:类变量也叫静态变量/静态属性,是该类的所有对象共享的变量,任何一个类的对象去访问它时,取到的是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。
不加static的变量叫法:普通变量/实例变量/非静态变量
Java7 之前,类的静态变量存放在方法区;Java7 之后,将类变量的存储转移到了堆。
定义:
访问修饰符 static 数据类型 变量名;//常用
static 访问修饰符 数据类型 变量名;
访问:
类名.类变量名//推荐
对象名.类变量名
细节讨论:
- 类变量是该类的所有对象共享的,而实例变量是每个对象独享的。
- 类变量是在类加载时就初始化了,也就是说,即使你没有创建对象,只要类加载了,就可以使用类变量了。
- 类变量的生命周期是随类的加载开始,随着类消亡而销毁。
- 类变量的访问修饰符的访问权限和范围和实例变量是一样的。
8.2 类方法
类方法也叫静态方法。
// 定义语法:
访问修饰符 static 返回数据类型 方法名(){} // 推荐
static 访问修饰符 返回数据类型 方法名(){}
// 调用方法
类名.类方法名 // 推荐
对象名.类方法名
细节讨论:
- 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区。
- 类方法中不允许使用和对象有关的关键字,比如this和super。
- 静态方法,只能访问静态成员,非静态方法,可以访问静态成员和非静态成员。
8.3 深入了解main方法
main方法注意事项:main()方法时静态方法,所以我们可以直接调用main方法所在类的静态方法或静态属性,但不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,通过这个对象去访问类中的非静态成员。
解释main方法:
public static void main(String[] args){}
- main方法由java虚拟机调用
- java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public
- java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static
- 该方法接收String类的数组参数,该数组中保存执行java命令时传递所运行的类的参数
- 传参方式:java 执行的程序 参数1 参数2 参数3
8.4 代码块
代码块又称初始化块,属于类中的成员,类似于方法,将逻辑语句封装在方法体中,通过{}包围起来。但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显示调用,而是加载类时,或创建对象时隐式调用。
代码块相当于另一种形式的构造器(对构造器的补充机制),可以做初始化的操作,如果多个构造器中都有重复的语句,可以抽取到代码块中,提高代码的复用性。
基本语法:
[修饰符(可选)]{
代码
};
说明:
- 修饰符可选,要写的话,只能写 static
- 代码块分为两类,使用static修饰的叫静态代码块,没有static修饰的,叫普通代码块/非静态代码块
- 代码块中代码可以为任何逻辑语句(输入、输出、方法调用、循环、判断等)
- ;可以写,可以不写
代码块细节:
- 静态代码块作用是对类进行初始化,随着类的加载而执行,并且只会执行一次;如果是普通代码块,每创建一个对象就执行一次
- 类什么时候被加载
- 创建对象实例时(new)
- 创建子类对象时,父类也会被加载(父类先被加载,子类后被加载)
- 使用类的静态属性时(静态属性、静态方法),父类也会被加载
- 如果只使用静态成员时,普通代码块不会被执行。
- 创建一个类的调用顺序
- 调用静态代码块和静态属性初始化。其中,静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态属性初始化,则按他们定义的顺序调用
- 调用普通代码块和普通属性的初始化。其中,普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通属性初始化,则按定义顺序调用
- 调用构造方法。
- 构造器的最前面其实隐含了super()和调用普通代码块
总结:创建一个子类的调用顺序如下
- 父类的静态代码块和静态属性(优先级一样,按定义顺序执行)
- 子类的静态代码块和静态属性(优先级一样,按定义顺序执行)
- 父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
- 父类构造方法
- 子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
- 子类构造方法
注意:静态代码块只能直接调用静态成员,普通代码块可以调用任意成员
练习
分析下面代码的输出结果
public class CodeBlock {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println("========");
Dog dog1 = new Dog();
System.out.println("========");
System.out.println(Dog.name);
System.out.println("========");
Cat cat = new Cat();
System.out.println("========");
System.out.println(Cat.name.name.name.name.a);
}
}
class Animal{
{
System.out.println("父类代码块");
}
static {
System.out.println("父类静态代码块");
}
public Animal() {
System.out.println("父类构造器");
}
}
class Dog extends Animal{
public static String name = "大黄";
{
System.out.println("子类代码块被执行");
}
static {
System.out.println("子类静态代码块执行");
}
{
System.out.println("子类第二个代码块执行");
}
public Dog() {
System.out.println("构造器被执行");
}
}
class Cat{
public static Cat name = new Cat();
public int a = setA();
{
System.out.println("cat代码块");
}
public Cat() {
System.out.println("cat构造器");
}
public int setA(){
System.out.println("设置a的值为5");
return 5;
}
}
输出结果为:
父类静态代码块
子类静态代码块执行
父类代码块
父类构造器
子类代码块被执行
子类第二个代码块执行
构造器被执行
========
父类代码块
父类构造器
子类代码块被执行
子类第二个代码块执行
构造器被执行
========
大黄
========
设置a的值为5
cat代码块
cat构造器
设置a的值为5
cat代码块
cat构造器
========
5
结果分析:
区域一
Dog dog = new Dog();
- 类加载:main方法中,创建对象dog时,先加载类信息。加载Animal类——父类静态代码块执行;然后加载Dog类——子类静态代码块执行。
- 父类构造器:进入Dog类的构造器中,先进入隐藏的super(),进入Animal类中,进入Animal类的构造器,执行super()无结果,然后执行Animal类的普通代码块和普通属性初始化——父类代码块执行,接着执行Animal类构造器代码——父类构造器执行。Dog构造器的super()执行完毕。
- 子类构造器:Dog构造器的super()执行完毕。执行Dog类的普通代码块和普通属性初始化——子类代码块被执行;子类第二个代码块执行。接着执行Dog类构造器代码——构造器被执行
区域二
Dog dog1 = new Dog();
Dog类和Animal类已经加载过一次,不用重新加载。重复区域一的2,3
区域三
如果只使用静态成员时,没有创建对象,普通代码块不会被执行;并且Animal类Dog类已经加载过了,也不执行静态代码块,因此只输出大黄
区域四
- 类加载:创建cat对象,先加载Cat类,执行静态代码块和初始化静态属性。按照定义顺序,先初始化静态属性name,name又创建了一个Cat对象,这次创建不必加载类,直接执行构造器中的内容,先执行构造器隐藏部分,super()无结果,执行普通代码块和普通属性初始化,按照定义循序,先初始化普通属性a,调用了setA()方法——设置a的值为5;然后执行普通代码块——cat代码块。构造器隐藏部分执行完毕,接着执行构造器代码——cat构造器
- 构造器:构造器内容执行过程和 1 中相同。
区域五
结果很好懂,但有遗留问题:多层的name是什么时候初始化的?
8.5 单例模式
所谓单例模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
单例模式有两种方法:1.饿汉式 2.懒汉式
饿汉式
饿汉式实现步骤:
- 将构造器私有化(防止new对象)
- 在类的内部直接创建对象(对象是static)
- 提供一个公共的static方法,返回创建的对象
代码实现
class GirlFriend{
private String name;
// 2.在类的内部直接创建对象(对象是static)
private static GirlFriend girlFriend = new GirlFriend("中野晴");
// 1.将构造器私有化
private GirlFriend(String name) {
this.name = name;
}
// 3.提供一个公共的static方法,返回创建的对象
public static GirlFriend getInstance(){
return girlFriend;
}
}
懒汉式
懒汉式实现步骤
- 将构造器私有化
- 在类的内部定义对象(对象是static),不初始化
- 提供一个公共的static方法,初始化并返回定义的对象
代码实现
class GirlFriend{
private String name;
// 2.在类的内部定义对象(对象是static),不初始化
private static GirlFriend girlFriend;
// 1.将构造器私有化
private GirlFriend(String name) {
this.name = name;
}
// 3.提供一个公共的static方法,初始化并返回定义的对象
public static GirlFriend getInstance(){
if(girlFriend == null){
girlFriend = new GirlFriend("中野晴")
}
return girlFriend;
}
}
饿汉式和懒汉式对比
- 二者最主要的区别在于创建对象的时机不同:饿汉式是在类加载就创建了对象实例,而懒汉式是在调用getInstance()方法时才创建。
- 饿汉式不存在线程安全问题,懒汉式存在线程安全问题。因为有可能多个线程同时进入getInstance()方法获取实例。
- 饿汉式存在浪费资源的可能。因为如果没有使用实例,那么饿汉式直接创建的实例对象就浪费了。懒汉式是使用时才创建,不存在这个问题。
8.6 final关键字
fianl可以修饰类、属性、方法和局部变量。
使用final的情况:
- 修饰类:被修饰的类不能被继承
- 修饰方法:被修饰的方法不能被子类覆盖/重写
- 修饰属性:不能被修改
- 修饰局部变量:不能被修改
final使用细节:
-
final修饰的属性又叫常量,一般用XX_XX_XX来命名
-
final修饰的属性在定义时必须赋初值,并且以后不能再修改,赋值可以在以下位置之一:
- 定义时:public final double TAX_RATE = 0.08;
- 在构造器中
- 在代码块中
-
如果final修饰的属性是静态的,则初始化的位置只能是定义时或在静态代码块中,不能在构造器中。
-
final类不能被继承,但是可以实例化对象
-
如果类不是final类,但是含有final方法,该方法虽然不能被重写,但是能被继承使用
-
final类中的方法没必要再使用final修饰
-
final不能修饰构造器
-
final和static搭配使用,不会导致类加载(底层编译器做了优化处理)
public class Tset { public static void main(String[] args) { // 静态代码块不会执行 System.out.println(A.n); // 10 } } class A { public static final int n = 10; static { System.out.println("静态代码块"); } }
-
包装类、String类都是final类
8.7 抽象类
当父类的一些方法不能确定时,可以用abstract关键字来修饰该方法,这个方法就是抽象方法,用abstract来修饰的该类就是抽象类。
定义语法
抽象类
访问修饰符 abstract 类名 {}
抽象方法
访问修饰符 abstract 返回类型 方法名(参数列表); // 没有方法体
细节讨论
- 抽象类不能被实例化
- 抽象类不一定要包含abstract方法。
- 包含abstract方法的类必须声明为abstract
- abstract只能修饰类和方法,不能修饰属性和其他的
- 抽象方法不能有主体
- 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己也声明为abstract类。
- 一般来说,抽象类会被继承,由其子类来实现抽象方法
- 抽象方法不能使用private、final和static来修饰,因为这些关键字都是和重写相违背的。
8.8 接口
接口就是给一些没有实现的方法,封装到一起,到某个类要使用的时候,再根据具体情况把这些方法写出来
在jdk7.0前,接口里的所有方法都是抽象方法,jdk8.0后接口类可以有静态方法、默认方法。
语法:
//定义接口
interface 接口名{
//属性
//方法(抽象方法、静态方法、默认实现方法)
}
//实现接口
class 类名 implements 接口{
//自己的属性
//自己的方法
//必须实现接口的抽象方法
}
//调用接口,创建一个方法,参数为接口名
public void num(接口名 db){
//接口方法
}
细节讨论
-
接口不能被实例化
-
接口中所有方法都是public方法
-
接口中的默认实现方法,需要使用default关键字修饰
default public void test(){}
-
接口中的抽象方法可以省略abstract关键字
-
如果一个类实现接口,则它必须实现接口的所有抽象方法,除非它是抽象类
-
一个类同时可以实现多个接口
public class C implements A,B{}
-
接口中的属性,只能是final的,而且是public static final 修饰符。
int a = 1; // 实际上是 public static final int a = 1;
-
接口属性的访问形式:接口名.属性名
-
一个接口不能继承其他的类,但是可以继承多个别的接口
interface A extends B,C{}
-
接口的修饰符和类一样,只能是public和默认
如果子类需要扩展功能,可以通过实现接口的方式扩展,可以把实现接口理解为对Java单继承机制的一种补充
注意:继承和接口同时使用可能会出现二义性,可以使用接口名和super调用来区分,同时使用时先继承,再使用接口,否则会报错
接口的多态性
接口的多态性和类非常相似(接口引用可以指向实现了接口的类的对象),用法也有多态参数、多态数组
特别的,接口存在多态传递现象(其实把接口实现看做继承,和类的多态也一致)
多态传递:接口B继承接口A,类C实现接口B,那么接口A的引用可以指向类C的对象。
8.9 内部类
内部类是类的五大特征中最后一个(属性、方法、构造器、代码块、内部类)
内部类一个类的内部又完整嵌套了另一个类的结构,被嵌套的类称为内部类(inner class),嵌套其他类的类称为外部类。
内部类最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系。
基本语法:
class Outher{//外部类
class Inner{//内部类
}
}
class Other{//外部其他类
}
内部类可以分为四种,两两一组。
- 定义在外部类局部位置上(方法、代码块):
- 局部内部类
- 匿名内部类
- 定义在外部类的成员位置上:
- 成员内部类
- 静态内部类局部内部类
8.9.1 局部内部类
是定义在外部类的局部位置,比如方法中,并且有类名。
定义语法
// 外部类
class Outer{
// 外部类成员方法
public void method(){
// 定义内部类
class Inner{}
}
}
细节讨论
- 可以直接访问外部类的所有成员
- 不能添加访问修饰符(因为它本质是局部变量),但可以用final修饰
- 作用域:仅仅在定义它的方法或代码块中
- 内部类访问外部类成员:直接访问
- 外部类访问内部类成员:先创建内部类实例,再用内部类实例访问
- 外部其他类不能访问局部内部类
- 如果外部类和局部内部类的成员重名时,遵循就近原则。如果想访问外部类的成员,使用(外部类名.this.成员)访问;外部类名.this本质就是外部类的对象,即哪个对象调用了内部类所在的方法,外部类名.this就是哪个对象
使用示例
public class LocalInnerClass {
public static void main(String[] args) {
Outer outer = new Outer("小明");
outer.test();
System.out.println(outer);
}
}
class Outer{
private String name;
public Outer(String name) {
this.name = name;
}
private void m1(){
System.out.println("方法m1");
}
public void test(){
class Inner{
private String name = "中野晴";
public void m2(){
System.out.println("name=" + name + ", OuterName=" + Outer.this.name);
System.out.println(Outer.this);
m1();
}
}
new Inner().m2();
}
}
8.9.2 匿名内部类
匿名内部类和局部内部类一样,都是定义在外部类的局部位置,比如方法中,但没有类名
匿名内部类一般比较类似继承一个其他类、或实现某个接口,主要用途是需要一个只使用一次的类,没有必要单独为只使用一次的类专门写一个类。
先下面代码
class Outer{
public void method(){
// tiger 的编译类型:Animal
// tiger 的运行类型:? Outer$1
Animal tiger = new Animal(){
@Override
public void cry() {
System.out.println("老虎叫");
}
};
tiger.cry();
}
}
interface Animal{
public void cry();
}
在上面代码中,有一个Outer类和Animal接口,在Animal接口中有一个待实现的cry()方法,Outer类中有一个method()方法。
在Outer类的method()方法中定义了一个匿名内部类,匿名内部类没有名字,直接用于创建对象。因为这个匿名内部类实现了Animal接口,所以可以用Animal作为编译类型接收。因此,对象tiger的编译类型是Animal,运行类型是这个没有名字的匿名内部类。
另外,匿名内部类其实是有名字的,类名是:外部类名$number。number从1开始,随创建顺序累加,比如上面的匿名内部类可看作下面的代码(再创建一个匿名内部类的话number会变成2)
class Outer$1 implements Animal{
@Override
public void cry() {
System.out.println("老虎叫");
}
}
上面用匿名内部类实现接口为例讲解了什么是匿名内部类,匿名内部类继承其他类和继承抽象类实现方法类似。
另一种使用方法
由于jdk底层在创建匿名内部类后立即就会创建它的实例,所以匿名内部类是一个类的定义的同时,本身也是一个对象,因此如果只使用一次的话,匿名内部类可以直接调用方法,不必把创建一个变量接收匿名内部类。因此实现接口的例子可以改写如下代码
class Outer{
public void method(){
new Animal(){
@Override
public void cry() {
System.out.println("老虎叫");
}
}.cry();
}
}
interface Animal{
public void cry();
}
细节讨论部分参考局部内部类
8.9.3 成员内部类
成员内部类定义在外部类的成员位置,并且没有static修饰
定义语法
class Outer{
private String name = "中野晴";
private int age = 16;
class Inner{
public void m1(){
System.out.println("name=" + name + ", age=" + age);
}
}
public void test(){
new Inner().m1();
}
}
细节讨论
-
可以直接访问外部类的所有成员
-
可以添加任意访问修饰符(public、protected、默认、private),因为它也是类中的一个成员
-
作用域:和外部类其他成员一样
-
成员内部类访问外部类成员:直接访问
-
外部类访问内部类成员:先创建内部类实例,再用内部类实例访问
-
外部其他类访问成员内部类成员:先创建内部类实例,再用内部类实例访问
// 如何在外部其他类创建内部类实例 // 法一 Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); // 法二:在外部类中,编写一个方法,可以返回内部类对象 Outer.Inner inner = outer.getInnerInstance();
-
如果外部类和局部内部类的成员重名时,遵循就近原则。如果想访问外部类的成员,使用(外部类名.this.成员)访问;外部类名.this本质就是外部类的对象,即哪个对象调用了内部类所在的方法,外部类名.this就是哪个对象
8.9.4 静态内部类
静态内部类和成员内部类一样,都是定义在外部类的成员位置,但有static修饰
定义语法
class Outer{
private String name;
private static int age = 5;
public static class Inner{
public void m1(){
// System.out.println(name); // 不能访问外部类的非静态成员
System.out.println(age);
}
}
}
细节讨论
-
可以直接访问外部类的所有静态成员
-
可以添加任意访问修饰符(public、protected、默认、private),因为它也是类中的一个成员
-
作用域:和外部类其他成员一样
-
外部类访问内部类成员:先创建内部类实例,再用内部类实例访问
-
外部其他类访问成员内部类成员:先创建内部类实例,再用内部类实例访问
// 如何在外部其他类创建内部类实例 // 法一 Outer.Inner inner = new Outer.Inner(); // 法二:在外部类中,编写一个方法,可以返回内部类对象 Outer outer = new Outer(); Outer.Inner inner = outer.getInnerInstance();
-
如果外部类和局部内部类的成员重名时,遵循就近原则。如果想访问外部类的成员,使用(外部类名.成员)访问