Java进阶01-面向对象高级
本文介绍面向对象的进阶内容,包括static关键字、继承、多态、抽象类、接口、内部类、枚举和泛型等
1. static
可以修饰成员变量、成员方法。
1.1 类变量
成员变量按有无static修饰分为两类:
- 类变量:有static修饰,属于类,在计算机中只有一份,会被类和类的所有对象共享,可以通过类名直接访问。
- 实例变量:无static修饰,属于对象,必须先创建实例,然后通过实例访问。
应用场景:当数据只需要一份,且希望被共享使用或修改,可以使用类变量。
1.2 类方法
成员方法按有无static分为两类:
- 类方法:有static修饰的方法,属于类,可以用类名直接调用。
- 实例方法:无static修饰的方法,属于对象,只能通过实例访问。
应用场景:设计工具类,可以通过类名直接调用,调用方便,提升了代码复用性和效率,节省内存。由于工具类不需要创建对象,建议将工具类的构造方法私有化。
注意事项:
- 类方法中可以访问类成员,不能直接访问实例成员。
- 实例方法中可以直接访问类成员,也可以直接访问实例成员。
- 实例方法中可以出现this关键字,类方法中不能出现this关键字。
1.3 应用
1.3.1 代码块
代码块是类的五大成分之一(成员变量、构造器、方法、代码块、内部类)。
代码块分为两种:
- 静态代码块
- 格式:static {}
- 特点:类加载时自动执行,由于类只会加载一次,所以静态代码块也只会执行一次。
- 作用:完成类的初始化,例如:对类变量的初始化。
- 实例代码块
- 格式:{}
- 特点:每次创建对象时,执行实例代码块,并在构造器前执行。
- 作用:和构造器一样,都是用来完成对象的初始化,例如:对实例变量进行初始化赋值。
1.3.2 单例设计模式
单例设计模型
- 解决什么问题:确保一个类只有一个实例,可以避免浪费内存。
- 饿汉式怎么写
- 把类的构造器私有化。
- 定义一个类变量,记住类的一个对象。
- 定义一个类方法,返回对象。
1 | // 饿汉式:迫不及待,确保想要就已经有了 |
- 懒汉式怎么写
- 把类的构造器私有化。
- 定义一个类变量用于存储对象。
- 提供一个类方法,保证返回的时同一个对象。
1 | // 懒汉式:要用到的时候才创建 |
建议:如果单例需要频繁使用,可以用饿汉式;如果单例使用不频繁,可以用懒汉式。
2. 继承
2.1 认识继承
Java中提供了一个关键字extends,用这个关键字,可以让一个类和另一个类建立起父子关系。
1 | public class B extends A { |
- 继承的特点:子类能继承父类的非私有方法(成员变量、成员方法),可以直接使用。
- 继承后对象的创建:子类的对象是由子类、父类共同完成的。
2.2 继承的使用
好处:减少重复代码的编写,提高代码的复用性。
注意事项:
- 权限修饰符
- 单继承、Object类
- 方法重写
- 子类访问其他成员的特点
- 子类构造器的特点
权限修饰符
- private:只能在本类中访问
- default(不写):本类和同一个包的类可以访问
- protected:本类、同一个包的类、任意包的子类可以访问
- public:任何类都能访问
注意:protected修饰的成员,可以在子类内部访问,而非子类对象可以对父类的成员进行调用。即:
1 | public class Parent { |
单继承
Java不支持多继承,即一个子类只能直接继承一个父类;
但Java支持多层继承,即父类还可以继承父类,类似于祖、父、子三代单传。
为什么不支持多继承?
若支持多继承,两个父类中出现同名方法,子类不知道继承哪个方法。
Object类
Object类是Java中所有子类的祖宗类,任何类的继承关系向上追溯,源头都是Object类。
自己创建的类,Java默认会使其继承Object类。
任何类都可以直接调用Object的方法。
方法重写
当子类认为父类的方法不好用,或者无法满足自己的需求时,可以重写一个方法名称、参数列表一样的方法,来代替父类的这个方法,这就是方法重写。
注意事项:
- 使用@Override注解,它可以检查方法重写的格式是否正确,代码可读性也更好。
- 子类重写父类方法时,访问权限必须大于等于父类方法的权限。
- 重写方法的返回值类型必须和被重写方法一样,或者范围更小。
- 私有方法、静态方法不能被重写。
“声明不变,重新实现”
子类访问其他成员的特点
在子类中访问其他成员(成员变量、成员方法),是依照“就近原则”的,即变量名相同的情况下,访问优先级:局部变量 > 子类成员变量 > 父类成员变量。
可以在访问时添加关键字,来声明要访问的成员。
- 访问父类的成员:super
- 访问子类的成员:this
- 访问局部变量:不加
子类构造器的特点
子类的全部构造器,都会先调用父类的构造器,再执行自己。
子类构造器默认存在super()方法,因此默认会调用父类的无参构造器。
若父类中只存在有参构造器,子类构造器会报错,需要显式地声明一个父类无参构造器,或者在子类构造器中显式地调用父类有参构造器。
应用场景:子类的成员变量拆分到父类和子类中,为了能够完整地构造成员变量,需要调用父类的构造器。
注意事项:
- 调用本类的构造器:this(构造参数);调用父类的构造器:super(构造参数)。
- this(构造参数)、super(构造参数)都只能写在构造器的第一行,因此二者不能同时出现。
3. 多态
3.1 认识多态
多态是在继承/实现情况下的一种现象,表现为:对象多态、行为多态。
多态的前提:
- 有继承/实现关系。
- 存在父类引用子类对象,如
People p1 = new Teacher();
- 存在方法重写。
注意:多态是对象、行为的多态,Java中的属性(成员变量)是不谈多态的,引用是什么类就使用什么类的成员变量。
3.2 多态的作用
多态的好处
- 在多态形式下,右边的对象是耦合的,更便于扩展和维护。
- 定义方法时,使用父类类型的形参,可以接收一切子类对象,扩展性更强、更便利。
- 多态下不能使用子类独有的方法,需要进行类型转换。
类型转换
- 自动类型转换:
父类 变量名 = new 子类();
- 强制类型转换:
子类 变量名= (子类) 父类变量;
注意事项
- 存在继承/实现关系可以在编译阶段进行强制类型转换,编译阶段不会报错。
- 运行时,如果发现对象的真实类型与强转后的类型不同,会报类型转换异常(ClassCastExecption)。
- 强转前,建议使用
instanceof
关键字,判断当前对象的真实类型,再进行强转:o instanceof 类型
。
4. 关键字、抽象类
4.1 final关键字
final关键字
final关键字是最终的意思,可以修饰(类、方法、变量)
- 修饰类:该类被称为最终类,特点是不能被继承了。
- 修饰方法:该方法被称为最终方法,特点是不能被重写了。
- 修饰变量:该变量只能被赋值一次。
final修饰变量的注意事项
- final修饰基本类型的变量,变量存储的数据不能被改变。
- final修饰引用类型的变量,变量存储的地址不能被改变,但地址所指向对象的内容是可以被改变的。
常量
什么是常量
- 常量是使用了
static final
修饰的成员变量,通常用于记录系统的配置信息。 - 命名规范:建议使用大写英文单词,多个单词之间用下划线相连。
使用常量记录系统配置信息的优势、执行原理
- 代码可读性更好,可维护性也更好。
- 程序编译后,常量会被“宏替换”:出现常量的地方全部会被替换成其记住的字面量,这样可以保证使用常量和直接用字面量的性能是一样的。
4.2 抽象类
什么是抽象类
多个类中只要有重复代码(包括相同的方法签名),我们都应该抽取到父类中去,此时,父类中就有可能存在只有方法签名的方法,这时,父类必定是一个抽象类了,我们抽出这样的抽象类,就是为了更好地支持多态。
简言之,只有方法签名(方法名、参数、返回值)的方法称为抽象方法;而包含抽象方法的类称为抽象类。抽象类中可以现有方法体的正常的成员方法。
抽象方法与抽象类需要用abstract
关键字修饰,如下就是一个抽象类A:
1 | public class abstract A { |
抽象类的应用常用和好处是什么
抽象类有两种主要的应用场景:
- 支持多态:使用抽象类,可以将子类中相同的代码(包括方法签名)都抽取出来,可以更好地支持多态,以提高代码的灵活性。
- 提升扩展性:在不知道业务系统未来具体的实现时,可以先定义抽象类,将来让子类去继承实现,以方便系统的扩展。
4.3 模板方法设计模式
模板方法设计模式是抽象类的经典应用。
模板方法设计模式主要解决方法中存在重复代码的问题。如果不同类的方法中,存在大部分相同的代码,仅有部分不相同,则可以将相同代码抽取出来作为模板方法,不同代码则作为抽象方法,交给子类各自实现,则有利于降低代码重复,提高复用率和可扩展性。
模板方法的书写过程:
- 定义一个抽象类。
- 在抽象类中定义两个方法
- 模板方法:存放相同部分的代码。
- 抽象方法:具体实现交给子类。
模板方法改造如下例:
改造前:
1 | public class A { |
1 | public class B { |
这里两个类的方法有相同的部分,也有不同的部分,可以适用模板方法进行改造。
改造后:
1 | public class C { |
1 | // 继承C类 |
1 | // 继承C类 |
最后调用A、B的sing()方法如下:
1 | public class Main { |
输出结果:
1 | 我想唱一首歌 |
5. 接口
5.1 认识接口
Java提供了一个关键字interface
,用这个关键字可以定义出一个特殊的结构:接口。
1 | public interface 接口名 { |
需要注意的是,成员变量默认使用public static final
修饰,成员方法默认使用public abstract
修饰。
注意事项:
- 接口不能创建对象
- 接口是用来被类实现(implements)的,实现类的接口称为实现类。
1 | 修饰符 class 实现类 implements 接口1,接口2,接口3... { |
- 一个类可以实现多个接口,实现类实现多个接口,必须重写全部接口的全部抽象方法,否则实现类需要定义为抽象类。
5.2 接口的作用
接口的作用
- 可以解决单类继承的问题,通过接口,可以让一个类在已经继承的情况下,还可以通过实现接口来扩展自己的功能。
- 通过实现接口,可以显式地表明类所包含的功能,可以放心地调用。
- 面向接口编程,一个接口可以被多个类实现,其方法可以包括不同的实现,可以方便灵活地切换各种业务。
JDK 8开始新增的接口方法
JDK 8开始,接口新增了几种方法,如下:
1 | public interface A { |
从JDK 8开始,接口中新增这些方法,可以增强接口的能力,更便于项目的扩展和维护。
例如默认方法,如果一个接口被多个类实现,在后期开发中需要在接口中新增一个通用方法,此时再去实现类中一一实现接口方法,显然是很繁琐的。因此我们可以直接在接口中定义一个默认方法(default),这样所有该接口的实现类都可以直接调用这个方法。
接口的注意事项(了解)
- 一个接口可以继承多个接口,即接口可以多继承。
- 一个接口继承多个接口,如果多个接口中存在方法签名冲突,则此时不支持多继承。
- 一个类实现多个接口,如果多个接口中存在方法签名冲突,则此时不支持多实现。
- 一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会优先用父类的。
- 一个类实现了多个接口,多个接口中存在同名的默认方法,可以不冲突,这个类重写该方法即可。
6. 内部类
6.1 静态内部类
什么是静态内部类
有static修饰的内部类,属于外部类自己持有。
1 | public class Outer { |
创建对象的格式
1 | 外部类名.内部类名 对象名 = new 外部类.内部类(...); |
静态内部类中访问外部类成员的特点
可以直接访问外部类的静态成员,不可以直接访问外部类的实例成员。
局部内部类(了解即可)
局部内部类是定义在方法中、代码块中、构造器等执行体中的内部类。
1 | public class Test { |
6.2 匿名内部类
匿名内部类就是一种特殊的局部内部类,所谓匿名,指的是程序员不需要为这个类声明名字。
1 | new 类或接口(参数值...) { |
1 | new Animal() { |
特点:匿名内部类本质就是一个子类,并会立即创建出一个子类对象。
作用:用于更方便地创建一个子类对象。
应用场景:匿名内部类通常作为一个参数传输给方法。
7. 枚举
7.1 认识枚举
枚举是一种特殊的Java类,格式如下:
1 | public enum A { |
枚举类编译后,再反编译可以看到如下源码:
1 | Compiled from "A.java" |
枚举类的特点
- 枚举类的第一行只能罗列一些名称,这些名称都是常量,并且每个常量记住的都是枚举类的一个对象。
- 枚举类的构造器都是私有的(写不写都只能是私有的),因此,枚举类对外不能创建对象。
- 枚举都是最终类,不可以被继承。
- 枚举类中,从第二行开始,可以定义类的其他各种成员。
- 编译器为枚举类新增了几个方法,并且枚举类都是继承
java.lang.Enum
类的,从num类也会继承到一些方法。
7.2 枚举的作用
以下是枚举类的常见应用场景
- 用来表示一组信息,然后作为参数传递,比如男女性别、一年四季等等常量。
- 可以定义枚举表示一组信息并作为参数传输,使传入参数值内容时受到枚举类的约束。
- 代码可读性好,参数值得到了约束,对使用者更友好,更建议使用。
8. 泛型
8.1 认识泛型
什么是泛型
定义类、接口、方法时,同时声明了一个或者多个类型变量(如:<E>),称为泛型类、泛型接口、泛型方法,它们统称为泛型。
1 | public class ArrayList<E> { |
泛型的作用
泛型提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力。这样可以避免强制类型转换,以及可能出西安的异常。
泛型的本质
把具体的数据类型作为参数传递给变量,即类型参数化。