博客记录-day006-instanceof关键字、java不可变对象、方法重载和方法重写、java注解、java枚举+中介者模式、备忘录模式、观察者模式

127 阅读10分钟

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

1、instanceof关键字

instanceof 关键字的用法其实很简单(object) instanceof (type)判断对象是否符合指定的类型,结果要么是 true,要么是 false。在反序列化的时候,instanceof 操作符还是蛮常用的,因为这时候我们不太确定对象属不属于指定的类型,如果不进行判断的话,就容易抛出 ClassCastException 异常。

class Round {
}

class Ring extends Round {
}

Ring ring = new Ring();
System.out.println(ring instanceof Round);//true
  • instanceof 关键字用于判断对象是否符合指定类型,结果为 true 或 false。
  • 在反序列化过程中,使用 instanceof 可以避免 ClassCastException 异常。
  • instanceof 可以有效判断子类和父类之间的关系,如子类实例可以被视为父类类型。
  • 通过使用 instanceof 检查对象的接口实现,确保类型一致性。
  • 当比较对象和类型之间没有关系时,编译器会报错以防止类型不匹配。
  • 对于 null 值,instanceof 返回 false,因无法判断其类型。
  • JDK 16 引入的模式匹配使得 instanceof 更加方便,可以同时进行类型判断和变量声明
if (obj instanceof String s) {
    // 如果类型匹配 直接使用 s
}

2、java不可变对象

一个类的对象在通过构造方法创建后如果状态不会再被改变,那么它就是一个不可变(immutable)类。它的所有成员变量的赋值仅在构造方法中完成,不会提供任何 setter 方法供外部类去修改。

提到不可变类,几乎所有的程序员第一个想到的,就是 String 类。那为什么 String 类要被设计成不可变的呢?

1)常量池的需要

字符串常量池是 Java 堆内存中一个特殊的存储区域,当创建一个 String 对象时,假如此字符串在常量池中不存在,那么就创建一个;假如已经存,就不会再创建了,而是直接引用已经存在的对象。这样做能够减少 JVM 的内存开销,提高效率。

2)hashCode 需要

因为字符串是不可变的,所以在它创建的时候,其 hashCode 就被缓存了,因此非常适合作为哈希值(比如说作为 HashMap 的键),多次调用只返回同一个值,来提高效率。

3)线程安全

就像之前说的那样,如果对象的状态是可变的,那么在多线程环境下,就很容易造成不可预期的结果。而 String 是不可变的,就可以在多个线程之间共享,不需要同步处理。

一个不可变类,必须要满足以下 4 个条件

  • 1)确保类是 final 的,不允许被其他类继承*。
  • 2)确保所有的成员变量(字段)是 final 的,这样的话,它们就只能在构造方法中初始化值,并且不会在随后被修改。
  • 3)不要提供任何 setter 方法
  • 4)如果要修改类的状态,必须返回一个新的对象
  • 5)如果一个不可变类中包含了可变类的对象,那么就需要确保返回的是可变对象的副本
public final class Writer {//不可变类
    private final String name;
    private final int age;
    private final Book book;//可变类的对象

    public Writer(String name, int age, Book book) {
        this.name = name;
        this.age = age;
        this.book = book;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    //public Book getBook() {
    //   return book;
    //}
    public Book getBook() {
    Book clone = new Book();
    clone.setPrice(this.book.getPrice());
    clone.setName(this.book.getName());
    return clone;
}
}

3.方法重载和方法重写的区别

“如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载。 ”“如果方法的功能是一样的,但参数不同,使用相同的名字可以提高程序的可读性。”

“如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体可能不同),我们称之为方法重写。 方法重写用于提供父类已经声明的方法的特殊实现,是实现多态的基础条件。”

image.png

1)方法重载

在 Java 中,有两种方式可以达到方法重载的目的。

  • 第一,改变参数的数目
  • 第二,通过改变参数类型,也可以达到方法重载的目的
  • 由于可以通过改变参数类型的方式实现方法重载,那么当传递的参数没有找到匹配的方法时,就会发生隐式的类型转换。
image.png

2)方法重写

  • 重写的方法必须和父类中的方法有着相同的名字;
  • 重写的方法必须和父类中的方法有着相同的参数;
  • 必须是 is-a 的关系(继承关系)。
public class Bike extends Vehicle {
    @Override
    void run() {
        System.out.println("自行车在跑");
    }

    public static void main(String[] args) {
        Bike bike = new Bike();
        bike.run();
    }
}

class Vehicle {
    void run() {
        System.out.println("车辆在跑");
    }
}

注意事项:

重写是在子类重新实现从父类继承过来的方法时发生的,所以只能重写继承过来的方法,这很好理解。这就意味着,只能重写那些被 public、protected 或者 default 修饰的方法,private 修饰的方法无法被重写。

重写的方法不能使用限制等级更严格的权限修饰符。 可以这样来理解:

  • 如果被重写的方法是 default,那么重写的方法可以是 default、protected 或者 public。
  • 如果被重写的方法是 protected,那么重写的方法只能是 protected 或者 public。
  • 如果被重写的方法是 public, 那么重写的方法就只能是 public。

重写后的方法不能抛出比父类中更高级别的异常

可以在子类中通过 super 关键字来调用父类中被重写的方法

如果一个类继承了抽象类,抽象类中的抽象方法必须在子类中被重写

synchronized 关键字用于在多线程环境中获取和释放监听对象,因此它对重写规则没有任何影响,这就意味着 synchronized 方法可以去重写一个非同步方法。

4、java注解

注解(Annotation)是在 Java 1.5 时引入的概念,同 class 和 interface 一样,也属于一种类型。注解提供了一系列数据用来装饰程序代码(类、方法、字段等),但是注解并不是所装饰代码的一部分,它对代码的运行效果没有直接影响,由编译器决定该执行哪些操作。

注解的生命周期有 3 种策略,定义在 RetentionPolicy 枚举中。

1)SOURCE:在源文件中有效,被编译器丢弃。

2)CLASS:在编译器生成的字节码文件中有效,但在运行时会被处理类文件的 JVM 丢弃。

3)RUNTIME:在运行时有效。这也是注解生命周期中最常用的一种策略,它允许程序通过反射的方式访问注解,并根据注解的定义执行相应的代码。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonField {
    public String value() default "";
}

1)JsonField 注解的生命周期是 RUNTIME,也就是运行时有效。

2)JsonField 注解装饰的目标是 FIELD,也就是针对字段的。

3)创建注解需要用到 @interface 关键字。

4)JsonField 注解有一个参数,名字为 value,类型为 String,默认值为一个空字符串。

5、java枚举

枚举(enum),是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,继承自 java.lang.Enum。

“我们来新建一个枚举 PlayerType。”

public class Player {
    private PlayerType type;
    public enum PlayerType {//枚举
        TENNIS,
        FOOTBALL,
        BASKETBALL
    }
    
    public boolean isBasketballPlayer() {
      return getType() == PlayerType.BASKETBALL;
    }

    public PlayerType getType() {
        return type;
    }

    public void setType(PlayerType type) {
        this.type = type;
    }
}

由于枚举是 final 的,所以可以确保在 Java 虚拟机中仅有一个常量对象,基于这个原因,我们可以使用“==”运算符来比较两个枚举是否相等。

《Effective Java》这本书里还提到了一点,如果要实现单例的话,最好使用枚举的方式。”我说。

“单例(Singleton)用来保证一个类仅有一个对象,并提供一个访问它的全局访问点,在一个进程中。因为这个类只有一个对象,所以就不能再使用 new 关键字来创建新的对象了。”

“Java 标准库有一些类就是单例,比如说 Runtime 这个类。”

Runtime runtime = Runtime.getRuntime();

“Runtime 类可以用来获取 Java 程序运行时的环境。”

二、小博哥-java设计模式

1、中介者模式

中介者模式要解决的就是复杂功能应用之间的重复调用,在这中间添加一层中介者包装服务,对外提供简单、通用、易扩展的服务能力。

这样的设计模式几乎在我们日常生活和实际业务开发中都会见到,例如;飞机降落有小姐姐在塔台喊话、无论哪个方向来的候车都从站台上下、公司的系统中有一个中台专门为你包装所有接口和提供统一的服务等等,这些都运用了中介者模式。除此之外,你用到的一些中间件,他们包装了底层多种数据库的差异化,提供非常简单的方式进行使用。

image.png

优化后代码:

itstack-demo-design-16-02
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design
    │   │       ├── dao
    │   │       │	├── ISchool.java
    │   │       │	└── IUserDao.java
    │   │       ├── mediator
    │   │       │	├── Configuration.java
    │   │       │	├── DefaultSqlSession.java
    │   │       │	├── DefaultSqlSessionFactory.java
    │   │       │	├── Resources.java
    │   │       │	├── SqlSession.java
    │   │       │	├── SqlSessionFactory.java
    │   │       │	├── SqlSessionFactoryBuilder.java
    │   │       │	└── XNode.java
    │   │       └── po
    │   │         	├── School.java
    │   │         	└── User.java
    │   └── resources
    │       ├── mapper
    │       │   ├── School_Mapper.xml
    │       │   └── User_Mapper.xml
    │       └── mybatis-config-datasource.xml
    └── test
         └── java
             └── org.itstack.demo.design.test
                 └── ApiTest.java

image.png

  • 以上是对ORM框架实现的核心类,包括了;加载配置文件、对xml解析、获取数据库session、操作数据库以及结果返回。
  • 左上是对数据库的定义和处理,基本包括我们常用的方法:<T> T selectOne<T> List<T> selectList等。
  • 右侧蓝色部分是对数据库配置的开启session的工厂处理类,这里的工厂会操作DefaultSqlSession
  • 之后是红色地方的SqlSessionFactoryBuilder,这个类是对数据库操作的核心类:处理工厂、解析文件、拿到session等。

2、备忘录模式

备忘录模式是以可以恢复或者说回滚,配置、版本、悔棋为核心功能的设计模式,而这种设计模式属于行为模式。在功能实现上是以不破坏原对象为基础增加备忘录操作类,记录原对象的行为从而实现备忘录模式。

itstack-demo-design-17-00
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── Admin.java
    │           ├── ConfigFile.java
    │           ├── ConfigMemento.java
    │           └── ConfigOriginator.java
    └── test
        └── java
            └── org.itstack.demo.design.test
                └── ApiTest.java

image.png

  • 以上是工程结构的一个类图,其实相对来说并不复杂,除了原有的配置类(ConfigFile)以外,只新增加了三个类。
  • ConfigMemento:备忘录类,相当于是对原有配置类的扩展
  • ConfigOriginator:记录者类,获取和返回备忘录类对象信息
  • Admin:管理员类,用于操作记录备忘信息,比如你一些列的顺序执行了什么或者某个版本下的内容信息

3、观察者模式

简单来讲观察者模式,就是当一个行为发生时传递信息给另外一个用户接收做出相应的处理,两者之间没有直接的耦合关联。

image.png 优化后代码:

itstack-demo-design-18-02
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── event
                │    ├── listener
                │    │    ├── EventListener.java
                │    │    ├── MessageEventListener.java
                │    │    └── MQEventListener.java
                │    └── EventManager.java
                ├── LotteryResult.java
                ├── LotteryService.java
                └── LotteryServiceImpl.java

image.png

  • 从上图可以分为三大块看:事件监听事件处理具体的业务流程,另外在业务流程中 LotteryService 定义的是抽象类,因为这样可以通过抽象类将事件功能屏蔽,外部业务流程开发者不需要知道具体的通知操作。
  • 右下角圆圈图表示的是核心流程与非核心流程的结构,一般在开发中会把主线流程开发完成后,再使用通知的方式处理辅助流程。它们可以是异步的,在MQ以及定时任务的处理下,保证最终一致性。