①JAVA 面向对象
①JAVA 面向对象
学习核心
面向对象
- 面向对象的三大特性?
- 如何理解多态?
- 什么是向上转型和向下转型?
- 多态可以继承吗?为什么只能单继承
抽象类和接口
- 接口和抽象类的区别
方法重写和重载
- 方法重写和重载有什么区别?
内部类(扩展)
- 为什么需要内部类?什么是匿名内部类?
- 静态内部类和非静态内部类有什么区别?
- 静态内部类的使用场景是什么?
面向对象
面向对象的三大特性:封装、继承、多态。结合案例场景分析三者的应用
面向对象编程:基本设计原则(S.O.L.I.D原则)
- 单一职责(Single Responsibility),类或者对象最好是只有单一职责,在程序设计中如果发现某个类承担着多种义务,可以考虑进行拆分。
- 开关原则(Open-Close, Open for extension, close for modification),设计要对扩展开放,对修改关闭。换句话说,程序设计应保证平滑的扩展性,尽量避免因为新增同类功能而修改已有实现,这样可以少产出些回归(regression)问题。
- 里氏替换(Liskov Substitution),进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换。
- 接口分离(Interface Segregation),在进行类和接口设计时,如果在一个接口里定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏了程序的内聚性。对于这种情况,可以通过拆分成功能单一的多个接口,将行为进行解耦。在未来维护中,如果某个接口设计有变,不会对使用其他接口的子类构成影响。
- 依赖反转(Dependency Inversion),实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之间适当耦合度的法宝
1.封装
✨概念梳理
封装:将对象的状态信息封装在对象内部,不允许外部程序直接访问,而是通过该类所提供的方法实现对内部信息的操作和访问
使用封装的好处
- 隐藏类实现的细节
- 良好的封装能够减少耦合,提高代码的可维护性
- 可以对成员进行更精确的控制,让使用者只能通过预定的方法访问数据,进行逻辑控制校验,限制对成员变量不合理的访问
为了实现封装需要从两个方面考虑
将成员变量和实现细节隐藏起来,不允许外部直接访问(Java提供了访问控制修饰符)
把公有的方法暴露出来,让方法控制这些成员变量进行安全的访问和操作
✨案例分析
定义一个Person类,通过思考几个问题,进一步了解【封装】的好处
public class Person {
/**
* 对属性的封装:姓名、年龄、性别
*/
private String name;
private int age;
private String gender;
/**
* 对外提供getter、setter方法(供外部程序访问)
* 可在方法中加入逻辑校验,控制外部程序对成员变量的访问
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
}
【1】假设没有使用封装,那么成员变量的访问权限设置为public、且不提供setter、getter
public class Person {
public String name;
public int age;
public String gender;
}
// 外部程序访问,直接访问成员变量
Person person = new Person();
person.name = "noob";
person.age = 18;
person.gender = "x";
此处引申一个问题,当哪天需要变动person的属性类型,则需要相应调整代码。例如需要将gender属性的数据类型从String改为int类型,当程序中多处都用到了这个属性,则每一处都要对应修改。但是如果使用了封装,则可基于对外提供的封装方法对数据类型做转换处理,而不用去动到每一处的引用代码
// 未使用封装时,修改gender的字段类型为int,则凡是涉及到引用该字段的都需要相应调整(一旦涉及多处就造成程序可维护性极差)
person.gender=1;
// 使用封装后,只需要在对外提供程序调用的getter、setter统一进行处理,而不需要动到引用代码
person.setGender("x");
public void setGender(String gender) {
this.gender = Integer.valueOf(gender);
}
【2】在没有使用封装的场景,无法对成员变量的访问做控制
例如在编写程序的时候设置person的年龄为300(person.age=300);但实际上这个设置是不符合业务场景的,因此需要考虑对age的访问入口做控制(setter校验)
引入封装,则可通过setter构造器对数据做校验(入口访问控制,以限制对成员变量不合理的访问)
public void setAge(int age) {
if(age<0||age>100){
System.out.println("age设置非法");
}else{
this.age = age;
}
}
除了setter之外,也可在getter构造器中进行数据转化(例如对gender进行转化:1-男;2-女)
public String getGenderName() {
if("1".equals(gender)){
return "男";
}else if("2".equals(gender)){
return "女";
}else{
System.out.println("无效的gender");
}
}
2.继承
继承是面向对象的特征,也是实现代码重用的重要手段。Java继承具有单继承的特点,每个类只能继承一个直接父类。
继承:子类继承父类的属性和方法,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的方法
继承的一个最经典的案例就是Animal类(父类),其他动物(Cat、Dog、Chicken)通过继承Animal类获取到父类的属性和方法
✨为什么需要继承?
为什么需要继承?=》减少代码冗余、实现代码重用
例如一个系统内往往有很多类有着相似的属性和方法定义,可以理解为这些类归属于一个体系内(属于同类),例如动物界的猫、狗等均归属于动物、人中的老师、学生等均属于人,同类的定义存在相同属性或者方法的定义,因此可以将这些公共的属性、方法抽离到基类去定义,然后子类通过继承获取到父类的属性和方法,通过继承的方式大大减少了代码量、使得代码结构更加清晰可见
/**
* 动物类
*/
public class Animal {
// 动物公共属性定义
public int id;
public String name;
public int age;
public int weight;
// 构造函数
public Animal(int id, String name, int age, int weight) {
this.id = id;
this.name = name;
this.age = age;
this.weight = weight;
}
// 公共方法定义(此处省略setter、getter)
public void say(){
System.out.println("hello world");
}
public void eat(){
System.out.printf("eating...");
}
}
Dog继承Animal,重写构造方法(可调用父类构造方法或进一步扩展自身方法)
/**
* Dog类定义
*/
public class Dog extends Animal{
public Dog(int id, String name, int age, int weight) {
// 调用父类构造方法
super(id, name, age, weight);
}
}
Cat继承Animal,还可自行扩展自己的方法
/**
* Cat定义
*/
public class Cat extends Animal{
public Cat(int id, String name, int age, int weight) {
// 调用父类构造方法
super(id, name, age, weight);
}
// 猫会额外喵喵叫
public void meow(){
System.out.println("miao...");
}
}
✨继承的分类(JAVA为什么只能单继承?)
继承分为单继承和多继承,Java 语言只支持类的单继承,但可以通过实现接口的方式达到多继承的目的
- 单继承:一个子类只拥有一个父类;在类的层次结构上比较清晰,但有时无法满足结构的丰富度
- 多继承:一个子类拥有多个直接的父类;子类的丰富度很高,但是容易造成混乱
如何理解结构混乱的场景?
假设一个多继承场景:有一个怪物类Monster同时继承了Cat、Dog,而Cat、Dog中都刚好定义了say方法(两个类对say方法的实现可能有所不同),那么此时Monster要调用say方法时,究竟是该输出Cat的say内容、还是Dog的say内容?
如何扩展多继承场景?
Java虽然不支持多继承,但是可以借助其他方案实现多继承效果,分别是内部类、多层继承和实现接口
- 内部类:内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,可以达到多继承的效果
- 多层继承:子类继承父类、父类还继承其他的类,以此类推构建多层继承。这样子类就会拥有所有被继承类的属性和方法
- 实现接口:一个类可以实现多个接口(满足子类在丰富性和复杂环境的使用需求)
✨如何实现继承?(extends、implements)
extends关键字
Java中通过extends关键字实现单一继承:子类继承父类之后就拥有了父类的非私有的属性和方法
修饰符 class SubClass extends SuperClass
{
// 定义代码
}
例如此处Dog继承Animal,则拥有了Animal的非私有的属性和方法(例如可以调用父类的say方法)
/**
* Dog类定义
*/
public class Dog extends Animal{
public Dog(int id, String name, int age, int weight) {
// 调用父类构造方法
super(id, name, age, weight);
}
public static void main(String[] args) {
// Dog继承Animal,拥有了Animal的非私有的属性和方法
Dog dog = new Dog(1,"小白",2,20);
dog.say();
}
}
implements关键字
使用 implements 关键字可以变相使 Java 拥有多继承的特性,使用范围为类实现接口的情况,一个类可以实现多个接口(接口与接口之间用逗号分开)。但此处需注意接口实现有其相应的约束场景,不同于直接继承一个类,接口更倾向于一组规范定义。
参考案例:类C实现A、B接口方法
interface A{
public void say();
}
interface B{
public void eat();
}
class C implements A,B{
// 实现A的方法定义
@Override
public void say() {
System.out.println("say hello");
}
// 实现B的方法定义
@Override
public void eat() {
System.out.println("eating");
}
}
/**
* implements 关键字变相实现"多继承"
*/
public class ImpDemo {
public static void main(String[] args) {
C c = new C();
c.say();
c.eat();
}
}
✨继承的特点
继承的核心:子类继承父类并重写父类方法。此处涉及到this
、super
关键字的使用
this
:表示当前对象的引用;super
:表示父类对象的引用
// this:表示当前对象,指向自己的引用
this.属性 // 调用成员变量(需要区分成员变量和局部变量)
this.方法() // 调用本类的某个方法
this() // 调用本类构造方法
// super:表示父类对象,指向父类的引用
super.属性 // 调用父类对象中的成员变量
super.方法() // 调用父类对象中定义的方法
super() // 调用父类构造方法
构造方法
概念:构造方法(与类同名)是一种特殊的方法(它不能够被继承),但可以在子类中引用父类的构造方法
父类的构造方法不能够被继承
如何理解构造方法不能被继承?
继承的核心是子类继承父类并重写方法,重写是不会改变方法名的(假设一种场景:Son继承Father,Father的构造方法是Father(),如果说这个方法被子类继承(也就是理解为Son的构造方法是Father()),基于这个概念就和构造方法的定义冲突了,所以反向推出构造方法不能够被继承)
子类的构造过程必须调用父类的构造方法
Java虚拟机构造子类对象前会先构造父类对象,父类对象构造完成之后再来构造子类特有的属性,称之为内存叠加。而 Java 虚拟机构造父类对象会执行父类的构造方法,所以子类构造方法必须调用 super()即父类的构造方法
如果子类的构造方法没有显式调用父类构造方法,则系统默认调用父类无参的构造方法
class A{
public String name;
public A() {
// 无参构造
}
public A (String name){
// 有参构造
}
}
class B extends A{
public B() {
// 无参构造
super();
}
public B(String name) {
// 有参构造
//super();
super(name);
}
}
方法重写和方法重载
- 方法重写(Override)
- 概念:子类继承父类并重写父类方法
- 特点:"两同两小一大"(方法名相同、参数列表相同;子类抛出的异常比父类小、子类方法的返回值类型比父类小;子类的作用权限比父类大;)
// 父类定义
class Father{
public void speak(){
System.out.println("father speak");
}
}
// 子类定义
class Son extends Father{
// 方法重写:子类继承父类并重写父类方法
@Override
public void speak() {
// 调用父类speak方法
super.speak();
System.out.println("son speak");
}
}
/**
* 方法重写
*/
public class OverrideDemo {
public static void main(String[] args) {
Son son = new Son();
son.speak();
}
}
- 方法重载(Overload)
- 概念:两个方法的方法名相同,但是参数不一致(则可说一个方法是另一个方法的重载)
- 特点:"两同一不同"(在同一个类中,方法名相同,参数列表不同(参数类型、参数个数、参数位置),与返回值、修饰类型那些无关)
/**
* 计算器
*/
class Calculator{
// add方法
public int add(int a, int b){
return a+b;
}
// add方法:可以看作是上面add方法的重载(同一个类中方法名相同参数列表不同)
public int add(int a, int b, int c){
return a+b+c;
}
}
/**
* 方法重载
*/
public class OverloadDemo {
public static void main(String[] args) {
Calculator calculator = new Calculator();
System.out.println(calculator.add(1,2));
System.out.println(calculator.add(1,2,3));
}
}
✨修饰符(访问修饰符和非访问修饰符)
Java 修饰符的作用就是对类或类成员进行修饰或限制,每个修饰符都有自己的作用,而在继承中可能有些特殊修饰符使得被修饰的属性或方法不能被继承,或者继承需要一些其他的条件。
Java 语言提供了很多修饰符,修饰符用来定义类、方法或者变量,通常放在语句的最前端。主要分为以下两类:
- 访问权限修饰符:public、private、protected
- 非访问修饰符:static、final、abstract
访问权限修饰符在方法重写中的应用
方法重写:"两同两小一大"
其中的两小一大:子类的方法返回类型和抛出的异常要比父类小;子类的作用域要比父类大
非访问修饰符
static
static:静态(可以修饰变量、方法、类),用static修饰的变量和方法可以直接通过类名访问(可不需要创建类的对象来访问成员)
比较常见的场景就是:一些工具类的构建(设计类的时候使用静态方法,例如Math、Arrays)
class C1{
public int a;
public C1(){}
// public static C1(){}// 构造方法不允许被声明为static
public static void doA() {}
public static void doB() {}
}
class C2 extends C1{
public static void doC()//静态方法中不存在当前对象,因而不能使用this和super
{
//System.out.println(super.a);
}
public static void doA(){}//静态方法能被静态方法重写
// public void doB(){}//静态方法不能被非静态方法重写
}
final
final:最后、最终(可以用来修饰变量、方法、类),可以理解为不可更改
- final修饰的变量一旦赋值后不能更改,被final修饰的实例变量必须显式指定初始值(不能只声明)
- final修饰的方法不能被更改(子类继承父类被final修饰的方法,但是不能重写这个方法),一般声明final方法的主要目的防止该方法内容被修改
- final修饰的类不能被继承
abstract
abstract:抽象(主要用来修饰类和方法:抽象类、抽象方法)
抽象方法:有很多不同的类的方法是相似的,但是具体内容又不太一样,因此抽取它的声明、没有具体的方法体(抽象方法用于抽离方法表达概念,但无具体实现)
抽象类:有抽象方法的类必须是抽象类(抽象类是可以表达概念但是无法构造实体的类)
案例分析:Person(核心:父类定义、子类实现)
例如可以设计一个Person抽象类和抽象方法,然后构建不同的人种去重写这些方法的具体实现
// Person 抽象类
abstract class Person{
// 定义一个抽象方法sayHello
public abstract void sayHello();
}
// 构建不同的人种继承Person,重写sayHello方法内容
class Chinese extends Person{
@Override
public void sayHello() {
System.out.println("你好");
}
}
class American extends Person{
@Override
public void sayHello() {
System.out.println("hello");
}
}
/**
* 抽象方法demo
*/
public class AbstractDemo {
public static void main(String[] args) {
Chinese chinese = new Chinese();
chinese.sayHello();
American american = new American();
american.sayHello();
}
}
✨Object类和转型
Object(java.lang.Object):是所有类的根类(是 Java 所有类的父类,是整个类继承结构的顶端,也是最抽象的一个类)。如果一个类没有显式声明它的父类(即没有写 extends xx),那么默认这个类的父类就是 Object 类,任何类都可以使用 Object 类的方法,创建的类也可和 Object 进行向上、向下转型。
- Object 是类层次结构的根类,所有的类都隐式的继承自 Object 类
- Java 中,所有的对象都拥有 Object 的默认方法
- Object 类有一个构造方法,并且是无参构造方法
常用的Object方法有:toString()、equals()、hashCode()、wait()、notify()、getClass(),场景中比较常用的是toString()、equals()方法
toString()方法
toString()方法表示返回该对象的字符串,由于各个对象构造不同所以需要重写,如果不重写的话默认返回类名@hashCode
格式
通过重写toString()方法,可以自定义输出内容,简化对象内容输出操作
class T1{
private String name;
private int age;
public T1(String name, int age){
this.name = name;
this.age = age;
}
}
class T2{
private String name;
private int age;
public T2(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "T1{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
/**
* toString方法demo
*/
public class ToStringDemo {
public static void main(String[] args) {
T1 t1 = new T1("Jack", 18);
System.out.println(t1.toString());
T2 t2 = new T2("Noob", 20);
System.out.println(t2.toString());
}
}
// 运行效果
com.noob.base.inheritance.T1@11028347
T1{name='Noob', age=20}
equals()方法
equals()方法主要比较两个对象是否相等,因为对象的相等不一定非要严格要求两个对象地址上的相同,有些场景上内容上的相同就会认为它相等(例如String类重写equals方法,通过字符串的内容比较是否相等)
向上转型、向下转型
向上转型 : 通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换。父类引用变量指向子类对象后,只能使用父类已声明的方法,但方法如果被重写会执行子类的方法,如果方法未被重写那么将执行父类的方法
向下转型:通过父类对象(大范围)实例化子类对象(小范围),在书写上父类对象需要加括号()
强制转换为子类类型。但父类引用变量实际引用必须是子类对象才能成功转型
案例分析:类型转换
Object object=new Integer(666);//向上转型
Integer i=(Integer)object;//向下转型Object->Integer,object的实质还是指向Integer
String str=(String)object;//错误的向下转型,虽然编译器不会报错但是运行会报错
✨子父类初始化顺序
初始化顺序:静态>非静态,父类>子类,非构造方法>构造方法。同一类别(例如普通变量和普通代码块)成员变量和代码块执行从前到后
初始化顺序概念理解:(如果基于更深层次理解需要去关注JVM执行流程)
- 静态变量(类变量,可以看作全局变量),静态成员变量和静态代码块在类加载的时候进行初始化、非静态成员变量和代码块在对象创建的时候初始化,所以静态>非静态
- 创建子类对象的时候需要先创建父类对象,所以父类优先于子类
- 调用构造方法是对成员变量进行一些初始化操作,所以普通成员变量和代码块优先于构造方法执行
class Parent{
public Parent() {
System.out.println(++b1+"父类构造方法");
}
static int a1=0;
static {
System.out.println(++a1+"父类static");
}
int b1=a1;
{
System.out.println(++b1+"父类代码块");
}
}
class Sub extends Parent{
public Sub() {
System.out.println(++b2+"子类构造方法");
}
static {
System.out.println(++a1+"子类static");
}
int b2=b1;
{
System.out.println(++b2 + "子类代码块");
}
}
// 执行顺序
1父类static
2子类static
3父类代码块
4父类构造方法
5子类代码块
6子类构造方法
3.多态
Java 的多态(多种状态)是指在面向对象编程中,同一个类的对象在不同情况下表现出来的不同行为和状态。多态的目的是为了提高代码的灵活性和可扩展性,使得代码更容易维护和扩展
Java引用变量有两个类型,一个是编译时的类型,一个是运行时的类型。其中编译时的类型由声明该变量时使用的类型决定,而运行时的类型由实际赋值给该变量的对象决定
✨多态基础
多态的前提
- 子类继承父类
- 子类重写父类方法
- 父类引用指向子类对象
多态语法格式:
父类类名 引用名称 = new 子类类名();
编译时该引用名称只能访问父类的属性和方法,运行时则基于实际对象类型进行后期动态绑定(优先访问子类重写后的方法)
// 父类定义
class Car{
public void run(){
System.out.println("car run");
}
}
// 子类继承父类,并重写父类方法
class Bmw extends Car{
@Override
public void run(){
System.out.println("bmw run");
}
}
class Banz extends Car{
@Override
public void run(){
System.out.println("banz run");
}
}
/**
* 多态案例
*/
public class PolDemo {
public static void main(String[] args) {
// 父类引用指向子类对象(父类声明)
Car[] cars = {new Car(),new Bmw(), new Banz()};
for (Car car : cars) {
car.run();
}
}
}
// 依次输出
car run
bmw run
banz run
此处思考一个问题:程序在执行car.run
方法的时候如何知道该调用父类的run方法还是子类的run方法?
编译器在编译阶段并不知道对象的类型,在运行时根据对象的类型进行后期绑定,Java的方法调用机制可以找到正确的方法体并执行得到对应的结果。基于这种多态机制,可以使得程序具有良好的扩展性。(例如后期想要引入新的Car类型或者扩展Car的方法,均可在此基础上进行构建,而不会影响原有的逻辑实现)
✨多态的实现方式
- 子类重写父类方法
- 接口(典型案例:插座)
- 抽象类、抽象方法
✨多态与构造方法
案例分析:在构造方法中调用多态方法
class Wang{
Wang(){
System.out.println("调用前");
say(); // 在父类的构造方法中调用say方法(say是一个多态方法)
System.out.println("调用后");
}
public void say(){
System.out.println("我的年龄是30岁");
}
}
class XiaoWang extends Wang{
private int age = 3;
public XiaoWang(int age){
this.age = age;
System.out.println("小王的年龄是" + this.age);
}
// 重写write方法
@Override
public void say() {
System.out.println("小王的实际年龄是" + this.age);
}
}
/**
* 多态
*/
public class PolDemo2 {
public static void main(String[] args) {
XiaoWang xiaoWang = new XiaoWang(10);
}
}
// 执行结果
调用前
小王的实际年龄是0
调用后
小王的年龄是10
此处分析执行流程,在创建子类对象XiaoWang会先调用父类的构造方法Wang(),在父类的构造方法中又调用了多态方法(say();
),则程序会自动根据运行时的对象类型进行后期绑定调用XiaoWang重写后的say方法,但是这个时候父类并不知道子类对象中的字段值是什么(因为父类的构造优先于子类构造),所以暂时将age(int类型)的属性值初始化为0。然后当父类的构造方法初始化完成,则继续子类构造方法的初始化,进而输出实际age的值
✨多态中的类型转换
向上转型
向上转型(自动类型转换):子类类型赋值给父类类型(父类型的引用指向子类型:父类类型 引用名称 = new 子类类名();
)
当使用多态方式调用方法时,该引用名称只能访问父类中的属性和方法。编译器首先检查父类中是否有该方法,如果没有,则编译错误。如果有,再去调用子类的同名(重写)方法。
// 父类:动物类定义
class Animal{
public void say(){
System.out.println("Animal");
}
}
class Dog extends Animal{
// 子类重写say方法
@Override
public void say(){
System.out.println("Dog say....");
}
// 子类自定义扩展的makeNoise方法
public void makeNoise(){
System.out.println("Dog makeNoise");
}
}
public class PolDemo3 {
public static void main(String[] args) {
// 向上转型:父类声明,子类赋值
Animal a = new Dog();
a.say(); // 编译时引用对象是父类Animal对象,因此不能调用父类中没有声明的方法(例如子类的makeNoise)
// 向下转型:当使用多态并访问子类独有的属性或者方法,必须进行向下转型。如果转换类型不合法则会出现类型转换异常 java.lang.ClassCastException
Animal[] animals = {new Animal(),new Dog()};
// ((Dog)animals[0]).makeNoise(); // 向下转型失败(抛出类型转换异常)
((Dog)animals[1]).makeNoise();
}
}
向下转型
向下转型是指将父类引用强转为子类类型;这是不安全的,因为有的时候,父类引用指向的是父类对象,向下转型就会抛出 ClassCastException,表示类型转换失败;但如果父类引用指向的是子类对象,那么向下转型就是成功的
当使用多态时,并且访问子类独有的属性或方法时,则必须进行向下转型
当进行向下转型时,建议先使用 instance of 关键字进行判断,判断合法时,则在转为对应的类型,否则可能会出现类型转换异常 java.lang.ClassCastException。
说明:instance of 关键字用于判断一个对象,是否属于某个指定的类或其子类的实例
public class PolDemo3 {
public static void main(String[] args) {
// 向上转型:父类声明,子类赋值
Animal a = new Dog();
a.say(); // 编译时引用对象是父类Animal对象,因此不能调用父类中没有声明的方法(例如子类的makeNoise)
// 向下转型:当使用多态并访问子类独有的属性或者方法,必须进行向下转型。如果转换类型不合法则会出现类型转换异常 java.lang.ClassCastException
Animal[] animals = {new Animal(),new Dog()};
// ((Dog)animals[0]).makeNoise(); // 向下转型失败(抛出类型转换异常)
((Dog)animals[1]).makeNoise();
if(animals[0] instanceof Dog){
((Dog)animals[0]).makeNoise(); // 向下转型失败(抛出类型转换异常)
}else{
System.out.println("类型不匹配");
}
}
}
接口和抽象类
接口和抽象类是Java面向对象设计的两个基础机制
接口:对行为的抽象,它是抽象方法的集合,利用接口可以达到API定义和实现分离的目的。其主要用于方法规范定义,不能被实例化,任何字段都是隐含着public static final的意义。例如Java标准类库中的集合接口定义(java.util.List等)
抽象类:不能被实例化的类(用abstract修饰class),其目的主要是代码重用,主要用于抽取类的公共方法实现(或者共同成员变量),然后通过继承的方式达到代码重用的目的。例如Java标准类库中的collection框架,其中的通用部分就会被抽取为抽象类(java.util.List)
概念引入
Java的单继承机制(不支持多继承)在规范代码的同时也产生了一定的局限性,影响程序结构设计。因此引入接口(一个类可以实现多个接口),通过类实现多个接口变相扩展多继承机制。但是Java不能通过扩展多个抽象类来重用逻辑(抽象类的应用是通过继承来扩展重用逻辑,Java是单继承)
在一些特定场景下,需要抽象出与具体实现、实例化无关的通用逻辑(纯调用关系的逻辑),但是如果使用传统的抽象类则会陷入到单继承的窘境,因此常用的做法是构建静态方法组成的工具类(Utils),例如集合操作工具类Collections(java.util.Collections)
接口扩展:接口定义新增方法,类实现接口也要相应新增方法实现,否则就会编译错误
抽象类扩展:子类继承抽象类,相应享受到能力扩展,而不需要担心编译问题
抽象类和接口的选择
Java8中接口引入default method方法之后,选择用抽象类还是接口可能变得更加混淆,因此可以基于其基本概念、目的、应用场景做相应区分
特性 | 接口 | 抽象类 |
---|---|---|
组合 | 新类可以组合多个接口 | 只能继承单一抽象类 |
状态 | 不能包含属性(除了静态属性,不支持对象状态) | 可以包含属性,非抽象方法可能引用这些属性 |
默认方法 和 抽象方法 | 不需要在子类中实现默认方法。默认方法可以引用其他接口的方法 | 必须在子类中实现抽象方法 |
构造器 | 没有构造器 | 可以有构造器 |
可见性 | 隐式 public | 可以是 protected 或友元 |
最核心的区分:一个类可以实现多个接口,一个类只能继承一个抽象类。
在实际经验应用中,应该尽可能抽象(倾向使用接口),与此同时也要避免接口和抽象类的滥用(不要为了用技术而用)
1.接口
学习资料
接口概念
Marker Interface:一类没有任何方法的接口(只是为了声明某些东西),例如Cloneable、Serializable。从表面上看和Annotation异曲同工,其好处在于简单直接,但Annotation可以指定参数和值,在表达能力上更加强大。
Java8中增加了【函数式编程】的支持,增加了一类定义:functional Interface(只有一个抽象方法的接口),通过@FunctionalInterface Annotation来进行标记。具体可以参考Lambda表达式和函数式接口的应用
Java8开始,interface增加了对default method的支持(Java 9之后甚至可以定义private default method),default method提供了一种二进制兼容的扩展已有接口的办法。例如java.util.Collection是collection体系的root interface,在Java 8中添加了一系列default method,主要是增加Lambda、Stream相关的功能。类似Collections之类的工具类,很多方法都适合作为default method实现在基础接口里面。
public interface Collection<E> extends Iterable<E> {
/**
* Returns a sequential Stream with this collection as its source
* ...
**/
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
“多继承”概念扩展
Java的单继承机制虽然规范了代码,但在程序设计上具备一定的限制。通过实现多个接口的形式变相实现多继承概念。Java8之后通过提供默认方法使其变相具备“多继承”的特性,通过实现带有默认方法的接口,可以结合多个基类的行为(但是不会存在状态的多继承,因为接口中不允许存在非静态属性(静态属性不适用与状态的多继承))
参考下述案例:Sub实现One、Two、Three多个接口,可以结合多个接口的default method,并实现相应的接口定义
// 多个接口定义:定义default method扩展基类功能
interface One{
default void first(){
System.out.println("first");
}
// 定义接口(默认包含public static final属性)
void doSth();
}
interface Two{
default void second(){
System.out.println("second");
}
}
interface Three{
default void third(){
System.out.println("third");
}
}
// 子类实现多个接口,拥有default method扩展功能
class Sub implements One,Two,Three{
// 实现One接口的doSth方法
@Override
public void doSth() {
System.out.println("do sth");
}
}
/**
* java default method demo
*/
public class DefaultMethodDemo {
public static void main(String[] args) {
Sub sub = new Sub();
sub.doSth();
// 拥有接口的default method方法扩展
sub.first();
sub.second();
sub.third();
}
}
结合多个interface的实现,主要基类方法中的方法名和参数列表不同,就能够很好地工作,否则会得到编译器错误。因为实现多个接口的时候如果不同的接口中存在方法名相同、参数列表相同的方法定义,则编译器无法明确具体后续要调用的是哪个接口方法,因此要么确保方法签名的唯一性,要么通过override方式覆写冲突的方法
// 接口定义
interface Ia{
default void doSth(){
System.out.println("IA doSth");
}
}
interface Ib{
default void doSth(){
System.out.println("IB doSth");
}
}
// 存在方法签名相同导致的冲突,编译不允许通过 class Sub1 implements Ia,Ib{ }
class Sub2 implements Ia,Ib{
// 通过覆写冲突的方法,借助super关键字来明确要调用的方法
@Override
public void doSth() {
// 调用Ia接口的doSth方法
Ia.super.doSth();
// 调用Ib接口的doSth方法
Ib.super.doSth();
// 自行额外扩展的操作定义
System.out.println("sub2 doSth");
}
}
public class DefaultMethodDemo1 {
public static void main(String[] args) {
Sub2 sub2 = new Sub2();
sub2.doSth();
}
}
需要注意的是,当组合接口使用的时候,在不同的接口中使用相同的方法名通常会造成代码可读性的混乱(例如覆写、实现、重载都混在一起),应该尽量避免这种情况
接口中的静态方法
Java8中允许在接口中添加静态方法(基于此可以将功能置于接口中,从而操作接口或者构建通用的工具)
// 模板方法设计模式版本
interface Operations{
// 执行方法定义
void execute();
// 静态方法定义(可变参数列表),runOps是一个模板方法,使用可变参数列表可以传入任意的Operations参数并按顺序运行
static void runOps(Operations... ops){
for (Operations op : ops) {
op.execute();
}
}
// 静态方法定义
static void show(String msg){
System.out.println(msg);
}
}
// 定义多个类实现Operations
class Bing implements Operations{
@Override
public void execute() {
System.out.println("Bing");
}
}
class Crack implements Operations{
@Override
public void execute() {
System.out.println("Crack");
}
}
class Twist implements Operations{
@Override
public void execute() {
System.out.println("Twist");
}
}
/**
* 静态方法 demo(java8 运行接口定义静态方法)
*/
public class StaticDemo {
public static void main(String[] args) {
// Operations工具类模式操作接口
Operations.runOps(new Bing(),new Crack(),new Twist());
}
}
👻接口适配
接口:相同的接口定义可以有多个实现(简单场景下,一个方法接收接口作为参数,该接口的实现和传递对象取决于方法的使用者)
接口的一种常见用法:策略设计模式,通过编写一个方法执行某些操作、接收一个指定的接口作为参数(只要对象遵循接口,就可以调用方法),从而使得方法更加灵活、通用,更具可复用性
案例分析:Scanner
类 Scanner 的构造器接受的是一个 Readable 接口。Readable 没有用作 Java 标准库中其他任何方法的参数,它是单独为 Scanner 创建的,因此 Scanner 没有将其参数限制为某个特定类。通过这种方式,Scanner 可以与更多的类型协作,可以通过创建一个新类实现 Readable 接口,让 Scanner 作用于它
class RandomStrings implements Readable{
private static Random rand = new Random(47);
private static final char[] CAPITALS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
private static final char[] LOWERS = "abcdefghijklmnopqrstuvwxyz".toCharArray();
private static final char[] VOWELS = "aeiou".toCharArray();
private int count;
public RandomStrings(int count) {
this.count = count;
}
@Override
public int read(CharBuffer cb) {
if (count-- == 0) {
return -1; // indicates end of input
}
cb.append(CAPITALS[rand.nextInt(CAPITALS.length)]);
for (int i = 0; i < 4; i++) {
cb.append(VOWELS[rand.nextInt(VOWELS.length)]);
cb.append(LOWERS[rand.nextInt(LOWERS.length)]);
}
cb.append(" ");
return 10; // Number of characters appended
}
}
/**
* 接口适配demo
*/
public class AdapterDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(new RandomStrings(10));
while (sc.hasNext()) {
System.out.println(sc.next());
}
}
}
👻接口字段
接口中定义的字段不能是“空 final”,但可以用非常量表达式进行初始化
// 接口定义
interface RandVals{
Random RAND = new Random(47);
int RANDOM_INT = RAND.nextInt(10);
long RANDOM_LONG = RAND.nextLong() * 10;
float RANDOM_FLOAT = RAND.nextLong() * 10;
double RANDOM_DOUBLE = RAND.nextDouble() * 10;
}
/**
* 接口字段 demo
*/
public class FieldDemo {
public static void main(String[] args) {
// static字段,在类第一次被加载的时候初始化
System.out.println(RandVals.RANDOM_INT);
System.out.println(RandVals.RANDOM_LONG);
System.out.println(RandVals.RANDOM_FLOAT);
System.out.println(RandVals.RANDOM_DOUBLE);
}
}
👻接口和工厂方法模式
接口是多实现的途径,而生成符合某个接口的对象的典型方式是工厂方法设计模式。
不同于直接调用构造器,只需调用工厂对象中的创建方法就能生成对象的实现——理论上,通过这种方式可以将接口与实现的代码完全分离,使得可以透明地将某个实现替换为另一个实现。(以Service为例)
// 服务接口定义
interface MyService{
void doSth();
}
// 不同服务实现
class ServiceOne implements MyService{
@Override
public void doSth() {
System.out.println("ServiceOne");
}
}
class ServiceTwo implements MyService{
@Override
public void doSth() {
System.out.println("ServiceTwo");
}
}
// 定义工厂方法,初始化不同服务
class ServiceFactory{
public MyService getMyService(MyService myService) {
return myService;
}
}
/**
* 工厂方法模式与接口:demo构建
*/
public class FactoryMethodDemo {
public static void main(String[] args) {
ServiceFactory sf = new ServiceFactory();
sf.getMyService(new ServiceOne()).doSth();
sf.getMyService(new ServiceTwo()).doSth();
}
}
2.抽象类
定义:
抽象类和抽象方法都必须使用abstract修饰符进行定义,有抽象方法的类只能被定义为抽象类,抽象类中可以没有抽象方法。
作用:
抽象类是从多个具体类中抽象出来的父类,它具有相同特征的类中抽象出一个抽象类,以这个抽象类为模板,从而避免子类的随意设计。抽象类实现的就是一种模板策略,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展和改造
方法重写和重载
基于面向对象的基本概念,首先先理解重写和重载的场景,然后结合经典问题去梳理
面试题:重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分
重写:重写的场景是发生在子类继承父类重写父类方法、类实现接口。子类重写的方法必须和父类保持一致("两同两小一大原则")
- 子类继承父类:最经典的案例(Animal、Dog、Cat)
- 类实现接口:Java中的Thread线程创建(实现Runnable接口),接口是一种行为规范,本身不提供实现(这点和类继承有一定的区别)
重载:在同一个类中,如果出现方法名相同、参数列表不同的情况,可以称之为一个方法对另一个方法的重载("两同一不同原则")。其好处在于让类以统一的方式处理不同类型的一种手段,调用方法时通过传递给他们的不同个数和类型的参数来决定具体使用哪个方法,进而实现多态性。但需注意方法重载时,方法之间需要存在一定的联系,因为这样可以提高程序的可读性,并且一般只重载功能相似的方法
- 场景:构造器重载(一个类中多个不同构造器),参考ThreadPoolExecutor线程池的实现类中的构造器。
重载方法无法根据返回值类型来区分:函数的返回值只是作为函数运行之后的一个“状态”,是保持方法的调用者与被调用者进行通信的关键。并不能作为某个方法的“标识”,而重载方法调用的核心应该是根据不同的参数列表进行区分。举例分析:
// 这段代码是不符合方法重载定义的,当要调用max方法,编译器无法明确调用的是哪个
float max(int a, int b);
int max(int a, int b);
// 如果存在一种情况,编译器可以根据上下文语境判断(例如用指定返回类型的变量接收结果),但实际场景还有一种直接调用max(1,2)的方式并不会去特意声明返回值,所以基于这种情况的存在,这个理论不成立,进一步说明了重载方法是无法根据返回值类型去区分的
float x=max(1,2);
int y=max(2,3);
内部类
学习资料
1.基本概念
内部类创建
内部类:一个定义在另一个类中的类
内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可见性。
内部类与组合是完全不同的概念。在最初,内部类看起来就像是一种代码隐藏机制:将类置于其他类的内部。但是,内部类远不止如此,它了解外部类,并能与之通信,而且用内部类写出的代码更加优雅而清晰。某些场景可能并不绝对, Java 8 的 Lambda 表达式和方法引用减少了编写内部类的需求
// 外部类定义
class OuterClass{
// 内部类定义
class InnerClass{
private int value = 10;
public int getValue(){
return value;
}
}
// 定义方法返回一个指向内部类的引用
InnerClass getInnerClass(){
return new InnerClass();
}
// 定义方法使用内部类
public void doSth(){
// 和使用普通类一样使用内部类
InnerClass innerClass = getInnerClass();
System.out.println(innerClass.getValue());
// 从外部类的非静态方法之外的任意位置
OuterClass.InnerClass m = new OuterClass.InnerClass();
System.out.println(m.getValue());
}
}
/**
* 内部类 demo
*/
public class InnerClassDemo {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
outerClass.doSth();
}
}
.this和.new
如果在内部类中要生成对外部类对象的引用,可使用.this
(使其在编译期被知晓并受到检查,没有任何运行时开销)
class DoThis{
void doSth(){
System.out.println("DoThis doSth");
}
// 定义内部类
class InnerClass{
// 在内部类中访问外部类
public DoThis outer(){
return DoThis.this;
}
}
// 定义方法获取内部类对象
public InnerClass inner(){
return new InnerClass();
}
}
/**
* inner class demo(.this使用)
*/
public class DoThisDemo {
public static void main(String[] args) {
DoThis dt = new DoThis();
DoThis.InnerClass dti = dt.inner();
dti.outer().doSth();
}
}
如果想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,必须在 new 表达式中提供对其他外部类对象的引用,使用 .new 语法。内部类对象的创建必须使用外部类的对象通过.new
表达式进行构建
class DoNew{
// 内部类定义
public class Inner{}
}
/**
* inner class demo
*/
public class DoNewDemo {
public static void main(String[] args) {
DoNew doNew = new DoNew();
DoNew.Inner inner = doNew.new Inner();
}
}
内部类方法和作用域
场景分析:在一个方法里面或者任意的作用域内定义内部类
基于这种场景选择内部类的理由:
- 实现了某类型的接口,可以创建并返回对其的引用
- 解决一个复杂的问题,想创建一个类来辅助这个解决方案,但是又不希望这个类是公共可用的
局部内部类
// 局部内部类
class LocalOuterClass{
public void doSth(){
// 局部内部类定义:在方法的作用域中定义内部类,而不是其他类的作用域
final class Inner{
private String val = "hello inner class";
public String getVal() {
return val;
}
}
// 调用内部类方法
Inner inner = new Inner();
System.out.println(inner.getVal());
}
public static void main(String[] args) {
LocalOuterClass localOuterClass = new LocalOuterClass();
localOuterClass.doSth();
}
}
// 任意作用域嵌入内部类
class RandomScopeOuterClass{
public void doSth(boolean b){
// 满足条件才执行
if(b){
// 在任意作用域中嵌入内部类
class Inner{
private String val = "hello inner class";
public String getVal() {
return val;
}
}
// 调用内部类方法
Inner inner = new Inner();
System.out.println(inner.getVal());
}
// Inner inner = new Inner(); 脱离内部类的作用域,不可在此处引用
}
public static void main(String[] args) {
RandomScopeOuterClass randomScopeOuterClass = new RandomScopeOuterClass();
randomScopeOuterClass.doSth(true);
}
}
匿名内部类
语法格式
new 类名或接口名(){
重写方法;
};
场景案例1:接口实现
传统接口实现方法:定义一个接口、定义实现类,然后创建实现类对象,通过这个对象去调用接口方法
// 场景案例1:接口定义
interface Swim{
void swim();
}
// 定义学生类实现Swim接口
class Student implements Swim{
@Override
public void swim() {
System.out.println("student swim");
}
public static void main(String[] args) {
// 创建Student对象调用接口方法
Student s = new Student();
s.swim();
}
}
匿名内部类的引入:将接口实现部分抽离出来,简化成匿名内部类的定义,实现相同的调用效果(对比上述方式,匿名内部类只能使用一次)。例如某些场景下不需要单独创建一个类去实现接口,则直接使用匿名内部类的方式去实现接口方法,基于匿名内部类是为了简化开发、解决代码冗余
class AnonymousStudent{
public static void main(String[] args) {
new Swim(){
@Override
public void swim() {
System.out.println("匿名内部类 swim");
}
}.swim();
}
}
嵌套类
如果不需要内部类对象与其外部类对象之间有联系,可以将内部类声明为 static,这通常称为嵌套类。
普通的内部类对象隐式地保存了一个引用,指向创建它的外部类对象。但当内部类是 static 的时就不是这样了。嵌套类意味着:
要创建嵌套类的对象,并不需要其外部类的对象
不能从嵌套类的对象中访问非静态的外部类对象
嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类。但是嵌套类可以包含所有这些东西
在一个普通的(非 static)内部类中,通过一个特殊的 this 引用可以链接到其外部类对象。嵌套类就没有这个特殊的 this 引用,这使得它类似于一个 static 方法
1)接口内部的类
嵌套类可以作为接口的一部分。放到接口中的任何类都自动地是 public 和 static 的。因为类是 static 的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则,还可以在内部类中实现其外部接口
// 案例1:接口内部的类
interface InInterfaceClass{
void show();
// 内部类
class InnerClass implements InInterfaceClass{
// 可以在接口内部类中实现其外部接口
@Override
public void show() {
System.out.println("in interface inner class");
}
}
public static void main(String[] args) {
InInterfaceClass inInterfaceClass = new InnerClass();
inInterfaceClass.show();
}
}
如果想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,则可以使用接口内部的嵌套类。例如在每个类中写一个main方法用于测试这个类(存在缺点:必须带着已经编译过的额外代码),则可考虑使用嵌套类来放置测试代码。
// 案例2:测试代码
class TestBed {
public void f() { System.out.println("f()"); }
public static class Tester {
public static void main(String[] args) {
TestBed t = new TestBed();
t.f();
}
}
}
2)从多层嵌套类中访问
一个内部类被嵌套多少层并不重要——它能透明地访问所有它所嵌入的外部类的所有成员
// 案例3:多层嵌套类
class MNA {
private void f() {}
class A {
private void g() {}
public class B {
void h() {
g();
f();
}
}
}
public static void main(String[] args) {
MNA mna = new MNA();
MNA.A mnaa = mna.new A();
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
}
}
继承内部类
因为内部类的构造器必须连接到指向其外部类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外部类对象的“秘密的”引用必须被初始化,而在派生类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联
class WithInner {
class Inner {}
}
public class InheritInner extends WithInner.Inner {
//- InheritInner() {} // Won't compile
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
内部类可以被覆盖吗?
如果创建了一个内部类,然后继承其外部类并重新定义此内部类时,会发生什么呢?也就是说,内部类可以被覆盖吗?这看起来似乎是个很有用的思想,但是“覆盖”内部类就好像它是外部类的一个方法,其实并不起什么作用
class Egg {
private Yolk y;
protected class Yolk {
public Yolk() {
System.out.println("Egg.Yolk()");
}
}
Egg() {
System.out.println("New Egg()");
y = new Yolk();
}
}
public class BigEgg extends Egg {
public class Yolk {
public Yolk() {
System.out.println("BigEgg.Yolk()");
}
}
public static void main(String[] args) {
new BigEgg();
}
}
默认的无参构造器是编译器自动生成的,这里是调用基类的默认构造器。你可能认为既然创建了 BigEgg 的对象,那么所使用的应该是“覆盖后”的 Yolk 版本,但从输出中可以看到实际情况并不是这样的。
这个例子说明,当继承了某个外部类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间内
内部类标识符
由于编译后每个类都会产生一个**.class** 文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个"meta-class",叫做 Class 对象)。内部类也必须生成一个**.class** 文件以包含它们的 Class 对象信息。这些类文件的命名有严格的规则:外部类的名字,加上“$",再加上内部类的名字。例如,LocalInnerClass.java 生成的 .class 文件包括
Counter.class
LocalInnerClass$1.class
LocalInnerClass$LocalCounter.class
LocalInnerClass.class
如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外部类标识符与“$”的后面。
虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况。因为这是 java 的标准命名方式,所以产生的文件自动都是平台无关的。(注意,为了保证你的内部类能起作用,Java 编译器会尽可能地转换它们。)
2.为什么需要内部类?
内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外部类的对象。所以可以认为内部类提供了某种进入其外部类的窗口。每个内部类都能独立地继承自一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(译注:类或抽象类)
基于接口变相实现多继承,是通过结合多个接口的实现进行构建,但是如果拥有的不是接口而是抽象类或者具体的类,则这种模式将会受限,因此需要借助内部类实现“多重继承”。
// 接口模式:变相实现"多重继承"
interface A{ }
interface B{ }
// 类实现多个接口,结合接口功能,变相实现多继承
class C implements A,B{ }
// 内部类:变相实现"多重继承"
class D{}
abstract class E { }
// 类继承父类并通过内部类(独立继承实现),变相实现多继承
class X extends D {
E makeE(){
return new E() {};
}
}
/**
* 内部类“多重继承”概念
*/
public class InnerClassMuiltDemo {
static void takesD(D d) {}
static void takesE(E e) {}
public static void main(String[] args) {
X x = new X();
takesD(x);
takesE(x.makeE());
}
}
如果不需要解决“多重继承”的问题,那么自然可以用别的方式编码,而不需要使用内部类。但如果使用内部类,还可以获得其他一些特性:
内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外部类对象的信息相互独立
在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。 稍后就会展示一个这样的例子
创建内部类对象的时刻并不依赖于外部类对象的创建
内部类并没有令人迷惑的"is-a”关系,它就是一个独立的实体
闭包和回调
闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外部类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外部类对象的引用,在此作用域内,内部类有权操作所有的成员,包括 private 成员
Java8之前,内部类是实现闭包的唯一方式。在 Java 8 中,可以使用 lambda 表达式来实现闭包行为,并且语法更加优雅和简洁。
内部类与控制框架
应用程序框架(application framework)就是被设计用以解决某类特定问题的一个类或一组类。要运用某个应用程序框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案,以解决特定问题。这是设计模式中模板方法的一个例子,模板方法包含算法的基本结构,并且会调用一个或多个可覆盖的方法,以完成算法的动作。设计模式总是将变化的事物与保持不变的事物分离开,在这个模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物。
控制框架是一类特殊的应用程序框架,它用来解决响应事件的需求。主要用来响应事件的系统被称作事件驱动系统。应用程序设计中常见的问题之一是图形用户接口(GUI),它几乎完全是事件驱动的系统。
要理解内部类是如何允许简单的创建过程以及如何使用控制框架的,请考虑这样一个控制框架,它的工作就是在事件“就绪”的时候执行事件。虽然“就绪”可以指任何事,但在本例中是指基于时间触发的事件。接下来的问题就是,对于要控制什么,控制框架并不包含任何具体的信息。那些信息是在实现算法的 action()
部分时,通过继承来提供的。