从项目代码出发,梳理 Java 基础:单例、枚举、接口、抽象类

13 阅读10分钟

前言

  1. 博客仅作为个人梳理学习内容的方式
  2. 从代码出发学习知识点
  3. 初学java,表达不当处望谅解

1.为什么需要这些机制?

在实际开发中,我们常会遇到几个问题:

如何保证某个类只有一个实例

如何避免“魔法值”,限制变量的合法取值范围?

如何复用代码,避免重复?

如何降低模块之间的耦合度

👉 本文的四个核心内容,正是对这些问题的回答:

机制解决问题
单例模式控制实例数量
枚举限制取值范围
抽象类模板复用
接口解耦与扩展

2.单例模式

概念:单例模式的目标是“一个类在系统中只有一个实例”。模式即为实现方式,而实现单例有以下几种常见方式

a.饿汉式

  1. 构造器私有化
  2. 静态成员提前创建实例
  3. 静态方法取得实例(可选)
ublic class A {
    //1.将构造方法私有化,避免创建多个对象
    private A(){}
    //2.创建一个静态变量,保存对象
    //因为 static 变量在内存中只有一份,所以确保了整个系统中只有一个 A 的实例
    //无论调用多少次 getInstance(),返回的都是同一个对象
    private static A instance = new A();
    //也可以通过final 关键字修饰变量,这样创建对象时就会报错,这样就不需要第三步了
    // private static final A instance = new A();
    //3.创建一个静态方法,返回对象
    public static A getInstance(){
        return instance;
    }
}

2.懒汉式

  1. 构造器私有
  2. 静态成员定义对象,第一次调用getInstance时才创建对象

(以下的代码在多线程下存在线程安全问题)

//懒汉单例模式,区别于饿汉单例模式,在第一次调用才的时候创建对象
public class B {

    private static B instance;//声明了引用,但对象未创建
    private B() {
    }
    public static B getInstance() {
        if (instance == null) {
            instance = new B();
        }
        return instance;
    }
}

3.单例模式的实际意义

在实际开发工作中,单例模式的意义有以下几点:

a.节约内存:对于一些内存要求高的对象,如线程池,如果每次调用都要创建不现实

b.实现控制行为:对于某些调度类,如任务管理器,日志中心,如果存在多个实例可能会出现重复调用或者执行冲突

c.保证全局状态一致:满足一些配置类的需求

d.提供全局访问对象:通过getInstance方法调用对象

下面给出一段体现第二点“控制行为”的实际代码

//因为控制器只需要一个,采用单例模式
public class SmartController{
    private static SmartController instance = new SmartController();
    private SmartController(){}
    public static SmartController getInstance(){
        return instance;
    }

    void control(Appliance  appliances){
        System.out.println("设备名称  "+appliances.getName()+"   当前设备状态  "+appliances.getStatus());
        appliances.press();
        System.out.println("设备名称  "+appliances.getName()+"   当前设备状态  "+appliances.getStatus());
    }
    void printAll(Appliance[] appliances){
        for (Appliance appliance : appliances) {
            System.out.println("设备名称  "+appliance.getName()+"   当前设备状态  "+appliance.getStatus());
        }
    }

}

3.枚举类

1.定义:枚举类是一种表示“有限个固定取值”的特殊类,枚举不仅是“语法糖”,本质是一个继承自 java.lang.Enum 的类

public enum A {
    //第一行只能罗列枚举对象的名称,这些名称本质上是常量
    //枚举类代码被反汇编后,就类似一个多例类,即每个对象枚举对象都是是单例的,因此枚举类也是多例的
    //枚举都是final的,不可被继承,枚举构造器也是私有的(这两个性质和单例子类类一致)
    //枚举对象由jvm管理,具有线程安全,因此可以通过枚举创建一个枚举对象来实现单例模式
    x,y,z;
}

2.枚举类与单例模式的联系

将上面的枚举类代码进行反汇编,得到的结果如下

public static final A x;
public static final A y;
public static final A z;

不难看出,A作为一个枚举类,在定义时本质上是创建了多个单例对象。并且由于枚举对象是由JVM统一管理的,具有线程安全,因此在实现单例模式时,利用枚举类是一种合理的选择

public enum A {
    x;
}

3.实际意义

枚举类在开发中的有一些常见用途,下面以其信息标志和分类作用为例子

场景:实现数字华容道的盘游戏,用枚举类表示移动方向,同时以用常量定义方向的方式作为对比

public class TestDirection {
    public static void main(String[] args) {
        //方法1.通过常量来做信息标志和分类,但缺点是参数不受控制
        move(Constant.UP);
         move(Constant.DOWN);
      
        //方法2.使用枚举类做信息标志和分类,参数受枚举类约束
        move1(Direction.up);
        move1(Direction.down);

    }
    

👉 枚举带来:

类型安全
可读性强
避免非法值

//枚举类
public enum Direction {
    up,down,left,right;
}

//常量
public class Constant {
    public static final int UP=0;
    public static final int DOWN=1;
    public static final int RIGHT=2;
    public static final int LEFT=3;

}
  1. move这个基于常量实现的方法虽然是可行的,但是存在诸多缺陷,最明显的一点就是这个方法的参数本质上是一个int类型的数据,参数不受控制,容易传入无意义的值,比如move(99)
  2. 使用枚举类做信息标志和分类,参数受枚举类约束,不会出现传无意义值的情况。而且从swicth语句可以看到,在分类是甚至可以不用完整写法 Direction.up,而是可以直接用枚举类的值作为分类参数,大大提升了代码的可读性,这一点也侧面体现出java官方对于枚举类信息标记和分类作用的认可
public static void move1(Direction direction) {
        switch (direction) {
            case up:
                System.out.println("向上移动");
                break;
            case down:
                System.out.println("向下移动");
                break;
            case right:
                System.out.println("向右移动");
                break;
            case left:
                System.out.println("向左移动");
                break;
            default:
                System.out.println("无效的移动方向");
        }
    }
    public static void move(int direction) {
        switch (direction) {
            case Constant.UP:
                System.out.println("向上移动");
                break;
            case Constant.DOWN:
                System.out.println("向下移动");
                break;
            case Constant.RIGHT:
                System.out.println("向右移动");
                break;
            case Constant.LEFT:
                System.out.println("向左移动");
                break;
            default:
                System.out.println("无效的移动方向");
        }

    }
}

4.抽象方法

1.定义

抽象类用于提取“共性字段 + 共性行为 + 强制子类实现的差异行为”,换句话说,就是复用与约束

以下面的代码为例子

其中的模版就是dailyRountine方法

package com.ithema.noobStudy.abstractdemo;
//抽象类可以不定义抽象方法,但有抽象方法的类一定要定成抽象类
public abstract class Animal {
    private String name;
    private String gender;
    //模版方法,减少代码重复
    //标准写法是加上final,避免在子类中重写
    public final void  dailyRoutine(){
        System.out.println("早晨醒来");
        cry();
        System.out.println("夜晚睡去");
    }

    public abstract void cry();

子类一定要实现抽象方法(体现“约束),否则也定义为抽象类

//一定要实现抽象方法,要么就也定义为抽象类
public class Cat extends Animal {

    @Override
    public void cry() {
        System.out.println("meow");
    }
}

public class Dog extends  Animal{

    @Override
    public void cry() {
        System.out.println("wolf");
    }
}

5.接口

1.定义:1.接口(Interface)是一种抽象类型,用于描述类应具备的行为规范,接口的两大优势是解耦合和多继承

  1. 接口是不能直接被创建的,必须通过实现类来实现
  2. 区别于类,接口是可以被多继承的
  public static void main(String[] args) {
        //接口有两大好处:多继承和解耦合,这里主要体现多继承的好处:可以拥有多能力
        Bunnies bunnies = new Bunnies();
        bunnies.cry();
        bunnies.go();
    }
}


//区分与继承,接口是可以多实现的,得重写所有的抽象方法
 class Bunnies implements Hanni, Minji {
    @Override
    public void cry() {
    }

    @Override
    public void go() {

    }
}
  1. 接口中的字段默认是常量,方法默认是抽象方法
public interface Minji {
String name = "Minji";//静态常量
public static final String id = "123456"; //可以看到接口中的常量是默认定义的,不需要我们显示的声明为final

public void cry();//抽象方法的定义也是默认的
}
  1. jdk8后,接口除了定义常量和抽象方法,还新增了三个功能

默认方法,静态方法和私有方法

public interface A {
    default void show(){
        System.out.println("默认方法被调用");
        show2();
    }

    static void show1(){
        System.out.println("静态方法被调用");
    }

    private void show2(){
        System.out.println("私有方法在类内部被调用");
    }
}

默认方法是新增功能其中比较有实际意义的,比如想要给已经上线的项目增加功能,如果没有默认方法而只有抽象方法,那么只能通过实现类一个个重写,很麻烦。现在只要将新功能在接口的默认方法实现即可

2.接口VS抽象类

维度接口(interface)抽象类(abstract class)
设计定位行为规范(能力)类的抽象(模板)
关系语义“能做什么”(can-do)“是什么”(is-a)
成员变量仅常量(public static final可以有实例字段
方法抽象方法 + default/static(Java 8+)抽象方法 + 已实现方法
构造器❌ 无✅ 有(供子类调用)
多继承✅ 可实现多个接口❌ 单继承
代码复用较弱(少量默认方法)强(可复用字段与方法)
适用场景解耦、可替换、扩展点模板方法、共享状态/逻辑

二者的侧重点不同:

接口更像是一种“声明”,用于规定实现类需要完成什么(What),至于具体如何完成,接口并不关心;

抽象类则更偏向于“模板 + 部分实现”,不仅规定要做什么,还对部分实现过程进行了约束(What + How)。其中模板方法通常用于定义整体流程,一般不希望被子类重写(可通过 final 限制),而具体的差异行为由子类通过实现抽象方法来完成。

3.面向接口编程

  1. 引言:面向接口编程体现了接口的另一个主要优势:解耦合,耦合就是:类与类之间的依赖强度,而引入接口后代码就可以依赖接口而不是具体类

场景:A公司提供一个接口(规范思想),内部的实现方案让B和C公司提供实现类

public interface ServiceInterface {
    public  void getInfo(Student[] students);
    public void getAvgGrade(Student[] students);
}

B公司负责实现:打印学生的信息,打印平均分

public class ServiceB implements ServiceInterface{
    @Override
    public void getInfo(Student[] students){
        for (int i = 0; i < students.length; i++) {
            System.out.println("学生姓名:  "+students[i].getName()+"   学生成绩:   "+students[i].getScore());

        }
        System.out.println("总人数: "+students.length);
    }

    @Override
    public void getAvgGrade(Student[] students) {
        if (students == null || students.length == 0) {
            System.out.println("平均分:  0");
            return;
        }
        int sum = 0;
        for (Student student : students) {
            sum += student.getScore();
        }
        double avg = (double) sum / students.length;
        System.out.println("平均分:  " + avg);
    }

}

C公司实现:打印学生信息(包括性别),打印平均分(除去最高和最低分)

public class ServiceC implements ServiceInterface{
    @Override
    public void getInfo(Student[] students) {
        int femaleCount = 0;
        int maleCount = 0;
        for (Student student : students) {
            System.out.println(student);
            if(student.getGender().equals("female")){
                femaleCount++;
            }
            else{
                maleCount++;
            }
        }
        System.out.println("总人数:  "+students.length+"  女生:"+femaleCount+"  男生:"+maleCount);
    }

    @Override
    public void getAvgGrade(Student[] students) {
        double sum = 0;
        int max = students[0].getScore();
        int min = students[0].getScore();
        for (Student student : students) {
            if(student.getScore() > max){
                max = student.getScore();
            }
            if(student.getScore() < min){
                min = student.getScore();
            }
        }
        for (Student student : students) {
            sum += student.getScore();

        }
        sum -= max;
        sum -= min;
        System.out.println("去掉最大最小值平均分:  "+ sum / (students.length - 2));

    }
}

学生类

import lombok.AllArgsConstructor;
import lombok.Data;//getter,setter,tosString
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private String name;
    private String gender;
    private int score;
}

测试类

public class Test {
    public static void main(String[] args) {
        //手动创建学生对象
        Student[] allStudents = new Student[5];
        allStudents[0] = new Student("Minji","female" ,100);
        allStudents[1] = new Student("Hanni","female" ,99);
        allStudents[2] = new Student("Danielle","female" ,98);
        allStudents[3] = new Student("Haerin","female" ,97);
        allStudents[4] = new Student("Hyrein","female" ,96);
        //提供两套业务实现方案,支持灵活切换,体现面向接口编程,也体现了接口的另一个好处:接耦合
        //A公司提供一个接口(规范思想),内部的实现方案让B和C公司提供实现类
        ServiceInterface service1 = new ServiceB();
        ServiceInterface service2 = new ServiceC();
        service1.getInfo(allStudents);
        service1.getAvgGrade(allStudents);
        System.out.println("-------------------------");
        service2.getInfo(allStudents);
        service2.getAvgGrade(allStudents);

    }
}

用接口接收服务对象,可以自由的选取实现类,代码依赖接口而不是实现,从而实现“可替换、可扩展”的设计


**6.总结:

单例:控制实例数量(唯一性)

枚举:限制取值范围(类型安全)

抽象类:复用代码 + 模板控制

接口:定义规范 + 实现解耦**