博客记录-day004-java内部类、java封装继承多态+外观模式、享元模式、代理模式

53 阅读20分钟

一、沉默王二-面向对象编程

1.java内部类

在 Java 中,可以将一个类定义在另外一个类里面或者一个方法里面,这样的类叫做内部类。一般来说,内部类分为成员内部类、局部内部类、匿名内部类和静态内部类。

  • 1、内部类可以使用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
  • 2、在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
  • 3、创建内部类对象的时刻并不依赖于外部类对象的创建。
  • 4、内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
  • 5、内部类提供了更好的封装,除了该外围类,其他类都不能访问。

1)成员内部类

成员内部类可以随心所欲地访问外部类的成员,但外部类想要访问内部类的成员,就不那么容易了,必须先创建一个成员内部类的对象,再通过这个对象来访问。

如果想要在静态方法中访问成员内部类的时候,就必须先得创建一个外部类的对象,因为内部类是依附于外部类的。

public class Wanger {
    int age = 18;
    private String name = "沉默王二";
    static double money = 1;

    public Wanger () {//外部类想要访问内部类的成员
        new Wangxiaoer().print();
    }

    public static void main(String[] args) {//静态方法中访问成员内部类的时候
        Wanger wanger = new Wanger();
        Wangxiaoer xiaoer = wanger.new Wangxiaoer();
        xiaoer.print();
    }

    class Wangxiaoer {
        int age = 81;

        public void print() {
            System.out.println(name);//内部类访问外部类
            System.out.println(money);
        }
    }
}

2)局部内部类

局部内部类是定义在一个方法或者一个作用域里面的类,所以局部内部类的生命周期仅限于作用域内。局部内部类就好像一个局部变量一样,它是不能被权限修饰符修饰的。

public class Wangsan {
    public Wangsan print() {
        class Wangxiaosan extends Wangsan{//局部内部类
            private int age = 18;
        }
        return new Wangxiaosan();
    }
}

3)匿名内部类

匿名内部类是我们平常用得最多的,尤其是启动多线程的时候,会经常用到,并且 IDE 也会帮我们自动生成。匿名内部类就好像一个方法的参数一样,用完就没了,以至于我们都不需要为它专门写一个构造方法,它的名字也是由系统自动命名的。

public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {//匿名内部类
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
        t.start();
    }
}

匿名内部类是唯一一种没有构造方法的类。就上面的写法来说,匿名内部类也不允许我们为其编写构造方法,因为它就像是直接通过 new 关键字创建出来的一个对象。

匿名内部类的作用主要是用来继承其他类或者实现接口,并不需要增加额外的方法,方便对继承的方法进行实现或者重写。

4)静态内部类

public class Wangsi {
    static int age;
    double money;
    
    static class Wangxxiaosi {//静态内部类
        public Wangxxiaosi (){
            System.out.println(age);
        }
    }
}

由于 static 关键字的存在,静态内部类是不允许访问外部类中非 static 的变量和方法的

2.java封装继承多态

1)封装

封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体

数据被保护在类的内部,尽可能地隐藏内部的实现细节,只保留一些对外接口使之与外部发生联系。

其他对象只能通过已经授权的操作来与这个封装的对象进行交互。也就是说用户是无需知道对象内部的细节(当然也无从知道),但可以通过该对象对外的提供的接口来访问该对象。

封装确实可以使我们更容易地修改类的内部实现,而无需修改使用了该类的代码

封装可以对成员变量进行更精确的控制

2)继承

继承(英语:inheritance)是面向对象软件技术中的一个概念。它使得复用以前的代码非常容易。

Java 语言是非常典型的面向对象的语言,在 Java 语言中继承就是子类继承父类的属性和方法,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的方法

继承的优势:可以直接使用父类的属性和方法,自己也可以有自己新的属性和方法满足拓展,父类的方法如果自己有需求更改也可以重写。这样使用继承不仅大大的减少了代码量,也使得代码结构更加清晰可见

继承分为单继承和多继承,Java 语言只支持类的单继承,但可以通过实现接口的方式达到多继承的目的。

  • 单继承,一个子类只有一个父类,如我们上面讲过的 Animal 类和它的子类。单继承在类层次结构上比较清晰,但缺点是结构的丰富度有时不能满足使用需求
  • 多继承,一个子类有多个直接的父类。这样做的好处是子类拥有所有父类的特征,子类的丰富度很高,但是缺点就是容易造成混乱

Java 虽然不支持多继承,但是 Java 有三种实现多继承效果的方式,分别是内部类、多层继承和实现接口。

内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,可以达到多继承的效果。

多层继承:子类继承父类,父类如果还继承其他的类,那么这就叫多层继承。这样子类就会拥有所有被继承类的属性和方法。

image.png

实现接口无疑是满足多继承使用需求的最好方式,一个类可以实现多个接口满足自己在丰富性和复杂环境的使用需求。

类和接口相比,类就是一个实体,有属性和方法,而接口更倾向于一组方法

继承实现方式:

在 Java 中,类的继承是单一继承,也就是说一个子类只能拥有一个父类,所以extends只能继承一个类。其使用语法为:

class 子类名 extends 父类名{}

使用 implements 关键字可以变相使 Java 拥有多继承的特性,使用范围为类实现接口的情况,一个类可以实现多个接口(接口与接口之间用逗号分开)。

我们来看一个案例,创建一个 test2 类做测试,分别创建 doA 接口和 doB 接口,doA 接口声明 sayHello()方法,doB 接口声明 eat()方法,创建 Cat2 类实现 doA 和 doB 接口,并且在类中需要重写 sayHello()方法和 eat()方法。

image.png

继承的主要内容就是子类继承父类,并重写父类的方法。使用子类的属性或方法时候,首先要创建一个对象,而对象通过构造方法去创建,在构造方法中我们可能会调用子父类的一些属性和方法,所以就需要提前掌握 this 和 super 关键字。

  1. 父类的构造方法不能被继承:

因为构造方法语法是与类同名,而继承则不更改方法名,如果子类继承父类的构造方法,那明显与构造方法的语法冲突了。比如 Father 类的构造方法名为 Father(),Son 类如果继承 Father 类的构造方法 Father(),那就和构造方法定义:构造方法与类同名冲突了,所以在子类中不能继承父类的构造方法,但子类会调用父类的构造方法。

  1. 子类的构造过程必须调用其父类的构造方法:

Java 虚拟机构造子类对象前会先构造父类对象,父类对象构造完成之后再来构造子类特有的属性,这被称为内存叠加。而 Java 虚拟机构造父类对象会执行父类的构造方法,所以子类构造方法必须调用 super()即父类的构造方法。

  1. 如果子类的构造方法中没有显示地调用父类构造方法,则系统默认调用父类无参数的构造方法。

你可能有时候在写继承的时候子类并没有使用 super()调用,程序依然没问题,其实这样是为了节省代码,系统执行时会自动添加父类的无参构造方式。

  • 方法重写(Override)

方法重写也就是子类中出现和父类中一模一样的方法(包括返回值类型,方法名,参数列表),它建立在继承的基础上。你可以理解为方法的外壳不变,但是核心内容重写

  • 方法重载(Overload)

如果有两个方法的方法名相同,但参数不一致,那么可以说一个方法是另一个方法的重载。

重载可以通常理解为完成同一个事情的方法名相同,但是参数列表不同其他条件也可能不同

Java 子类重写继承的方法时,不可以降低方法的访问权限子类继承父类的访问修饰符作用域不能比父类小,也就是更加开放,假如父类是 protected 修饰的,其子类只能是 protected 或者 public,绝对不能是 default(默认的访问范围)或者 private。所以在继承中需要重写的方法不能使用 private 修饰词修饰。

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 方法的主要目的是防止该方法的内容被修改。

抽象方法:有很多不同类的方法是相似的,但是具体内容又不太一样,所以我们只能抽取他的声明,没有具体的方法体,即抽象方法可以表达概念但无法具体实现。

抽象类有抽象方法的类必须是抽象类,抽象类可以表达概念但是无法构造实体的类

Object 类概述

  1. Object 是类层次结构的根类,所有的类都隐式的继承自 Object 类。
  2. Java 中,所有的对象都拥有 Object 的默认方法。
  3. Object 类有一个构造方法,并且是无参构造方法

Object 是 Java 所有类的父类,是整个类继承结构的顶端,也是最抽象的一个类。

image.png 向上转型 : 通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换。用一张图就能很好地表示向上转型的逻辑。父类引用变量指向子类对象后,只能使用父类已声明的方法,但方法如果被重写会执行子类的方法,如果方法未被重写那么将执行父类的方法。

image.png 向下转型 : 通过父类对象(大范围)实例化子类对象(小范围),在书写上父类对象需要加括号()强制转换为子类类型。但父类引用变量实际引用必须是子类对象才能成功转型,这里也用一张图就能很好表示向下转型的逻辑。子类引用变量指向父类引用变量指向的对象后(一个 Son()对象),就完成向下转型,就可以调用一些子类特有而父类没有的方法

image.png

Object object=new Integer(666);//向上转型

Integer i=(Integer)object;//向下转型Object->Integer,object的实质还是指向Integer

String str=(String)object;//错误的向下转型,虽然编译器不会报错但是运行会报错

3)多态

Java 的多态是指在面向对象编程中,同一个类的对象在不同情况下表现出来的不同行为和状态

  • 子类可以继承父类的字段和方法,子类对象可以直接使用父类中的方法和字段(私有的不行)。
  • 子类可以重写从父类继承来的方法,使得子类对象调用这个方法时表现出不同的行为。
  • 可以将子类对象赋给父类类型的引用,这样就可以通过父类类型的引用调用子类中重写的方法,实现多态。

多态的目的是为了提高代码的灵活性和可扩展性,使得代码更容易维护和扩展。

比如说,通过允许子类继承父类的方法并重写,增强了代码的复用性。

再比如说多态可以实现动态绑定,这意味着程序在运行时再确定对象的方法调用也不迟。

多态的前提条件有三个:

  • 子类继承父类
  • 子类重写父类的方法
  • 父类引用指向子类的对象
//子类继承父类
public class Wangxiaoer extends Wanger {
    public void write() { // 子类重写父类方法
        System.out.println("记住仇恨,表明我们要奋发图强的心智");
    }

    public static void main(String[] args) {
        // 父类引用指向子类对象
        Wanger[] wangers = { new Wanger(), new Wangxiaoer() };

        for (Wanger wanger : wangers) {
            // 对象是王二的时候输出:勿忘国耻
            // 对象是王小二的时候输出:记住仇恨,表明我们要奋发图强的心智
            wanger.write();
        }
    }
}

class Wanger {
    public void write() {
        System.out.println("勿忘国耻");
    }
}

在构造方法中调用多态方法,会产生一个奇妙的结果:

public class Wangxiaosan extends Wangsan {
    private int age = 3;
    public Wangxiaosan(int age) {
        this.age = age;
        System.out.println("王小三的年龄:" + this.age);
    }

    public void write() { // 子类覆盖父类方法
        System.out.println("我小三上幼儿园的年龄是:" + this.age);
    }

    public static void main(String[] args) {
        new Wangxiaosan(4);
//      上幼儿园之前
//      我小三上幼儿园的年龄是:0
//      上幼儿园之后
//      王小三的年龄:4
    }
}

class Wangsan {
    Wangsan () {
        System.out.println("上幼儿园之前");
        write();
        System.out.println("上幼儿园之后");
    }
    public void write() {
        System.out.println("老子上幼儿园的年龄是3岁半");
    }
}

从输出结果上看,是不是有点诧异?明明在创建 Wangxiaosan 对象的时候,年龄传递的是 4,但输出结果既不是“老子上幼儿园的年龄是 3 岁半”,也不是“我小三上幼儿园的年龄是:4”。

因为在创建子类对象时,会先去调用父类的构造方法,而父类构造方法中又调用了被子类覆盖的多态方法,由于父类并不清楚子类对象中的字段值是什么,于是把 int 类型的属性暂时初始化为 0,然后再调用子类的构造方法(子类构造方法知道王小二的年龄是 4)。

向下转型是指将父类引用强转为子类类型;这是不安全的,因为有的时候,父类引用指向的是父类对象,向下转型就会抛出 ClassCastException,表示类型转换失败;但如果父类引用指向的是子类对象,那么向下转型就是成功的。

public class Wangxiaosi extends Wangsi {
    public void write() {
        System.out.println("记住仇恨,表明我们要奋发图强的心智");
    }

    public void eat() {
        System.out.println("我不喜欢读书,我就喜欢吃");
    }

    public static void main(String[] args) {
        Wangsi[] wangsis = { new Wangsi(), new Wangxiaosi() };

        // wangsis[1]能够向下转型
        ((Wangxiaosi) wangsis[1]).write();
        // wangsis[0]不能向下转型
        ((Wangxiaosi)wangsis[0]).write();
    }
}

class Wangsi {
    public void write() {
        System.out.println("勿忘国耻");
    }

    public void read() {
        System.out.println("每周读一本好书");
    }
}
  • 封装:是对类的封装,封装是对类的属性和方法进行封装,只对外暴露方法而不暴露具体使用细节,所以我们一般设计类成员变量时候大多设为私有而通过一些 get、set 方法去读写。
  • 继承:子类继承父类,即“子承父业”,子类拥有父类除私有的所有属性和方法,自己还能在此基础上拓展自己新的属性和方法。主要目的是复用代码
  • 多态:多态是同一个行为具有多个不同表现形式或形态的能力。即一个父类可能有若干子类,各子类实现父类方法有多种多样,调用父类方法时,父类引用变量指向不同子类实例而执行不同方法,这就是所谓父类方法是多态的。

image.png

二、小博哥-重学java设计模式

1.外观模式

外观模式也叫门面模式,主要解决的是降低调用方的使用接口的复杂逻辑组合。这样调用方与实际的接口提供方提供方提供了一个中间层,用于包装逻辑提供API接口。有些时候外观模式也被用在中间件层,对服务中的通用性复杂逻辑进行中间件层包装,让使用方可以只关心业务开发。

优化前:

itstack-demo-design-10-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── HelloWorldController.java

优化后:

itstack-demo-design-10-02
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design.door
    │   │       ├── annotation
    │   │       │	└── DoDoor.java	
    │   │       ├── config
    │   │       │	├── StarterAutoConfigure.java
    │   │       │	├── StarterService.java
    │   │       │	└── StarterServiceProperties.java
    │   │       └── DoJoinPoint.java
    │   └── resources	
    │       └── META_INF
    │           └── spring.factories
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

image.png

  • 以上是外观模式的中间件实现思路,右侧是为了获取配置文件,左侧是对于切面的处理。
  • 门面模式可以是对接口的包装提供出接口服务,也可以是对逻辑的包装通过自定义注解对接口提供服务能力。

2.享元模式

享元模式,主要在于共享通用对象,减少内存的使用,提升系统的访问效率。而这部分共享对象通常比较耗费内存或者需要查询大量接口或者使用数据库资源,因此统一抽离作为共享对象使用。

另外享元模式可以分为在服务端和客户端,一般互联网H5和Web场景下大部分数据都需要服务端进行处理,比如数据库连接池的使用、多线程线程池的使用,除了这些功能外,还有些需要服务端进行包装后的处理下发给客户端,因为服务端需要做享元处理。但在一些游戏场景下,很多都是客户端需要进行渲染地图效果,比如:树木、花草、鱼虫,通过设置不同元素描述使用享元公用对象,减少内存的占用,让客户端的游戏更加流畅。

优化前:

itstack-demo-design-11-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── ActivityController.java

优化后:

itstack-demo-design-11-02
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── util
    │           │	└── RedisUtils.java	
    │           ├── Activity.java
    │           ├── ActivityController.java
    │           ├── ActivityFactory.java
    │           └── Stock.java
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

image.png

  • 以上是我们模拟查询活动场景的类图结构,左侧构建的是享元工厂,提供固定活动数据的查询,右侧是Redis存放的库存数据。

  • 最终交给活动控制类来处理查询操作,并提供活动的所有信息和库存。因为库存是变化的,所以我们模拟的RedisUtils中设置了定时任务使用库存。

  • 关于享元模式的设计可以着重学习享元工厂的设计,在一些有大量重复对象可复用的场景下,使用此场景在服务端减少接口的调用,在客户端减少内存的占用。是这个设计模式的主要应用方式。

  • 另外通过map结构的使用方式也可以看到,使用一个固定id来存放和获取对象,是非常关键的点。而且不只是在享元模式中使用,一些其他工厂模式、适配器模式、组合模式中都可以通过map结构存放服务供外部获取,减少ifelse的判断使用。

3.代理模式

代理模式有点像老大和小弟,也有点像分销商。主要解决的问题是为某些资源的访问、对象的类的易用操作上提供方便使用的代理服务。而这种设计思想的模式经常会出现在我们的系统中,或者你用到过的组件中,它们都提供给你一种非常简单易用的方式控制原本你需要编写很多代码的进行使用的服务类。

类似这样的场景可以想到:

  1. 你的数据库访问层面经常会提供一个较为基础的应用,以此来减少应用服务扩容时不至于数据库连接数暴增。
  2. 使用过的一些中间件例如;RPC框架,在拿到jar包对接口的描述后,中间件会在服务启动的时候生成对应的代理类,当调用接口的时候,实际是通过代理类发出的socket信息进行通过。
  3. 另外像我们常用的MyBatis,基本是定义接口但是不需要写实现类,就可以对xml或者自定义注解里的sql语句进行增删改查操作。

使用代理类模式来模拟实现一个Mybatis中对类的代理过程,也就是只需要定义接口,就可以关联到方法注解中的sql语句完成对数据库的操作。

优化后:

itstack-demo-design-12-00
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design
    │   │       ├── agent
    │   │       │	├── MapperFactoryBean.java
    │   │       │	├── RegisterBeanFactory.java
    │   │       │	└── Select.java
    │   │       └── IUserDao.java
    │   └── resources	
    │       └── spring-config.xml
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

image.png

  • 关于这部分代理模式的讲解我们采用了开发一个关于mybatis-spring中间件中部分核心功能来体现代理模式的强大之处,所以涉及到了一些关于代理类的创建以及spring中bean的注册这些知识点,可能在平常的业务开发中都是很少用到的,但是在中间件开发中确实非常常见的操作。
  • 代理模式除了开发中间件外还可以是对服务的包装,物联网组件等等,让复杂的各项服务变为轻量级调用、缓存使用。你可以理解为你家里的电灯开关,我们不能操作220v电线的人肉连接,但是可以使用开关,避免触电。
  • 代理模式的设计方式可以让代码更加整洁、干净易于维护,虽然在这部分开发中额外增加了很多类也包括了自己处理bean的注册等,但是这样的中间件复用性极高也更加智能,可以非常方便的扩展到各个服务应用中。

三、小型支付商城系统

1.支付订单场景表设计

为了能让读者更加清晰地看到这些相关规范都是如何体现的,小傅哥这里准备了个大图,把库表字段和规范全部整合在一起,方便学习使用。

image.png