第四周学习总结

概述

本周理论知识学习的不是很多,主要是JavaSE中关于继承的一些概念,包括:

  1. 继承关系与聚合关系的取舍
  2. 类、超类、子类的相关概念
  3. 泛型数组列表
  4. 对象包装器与自动装箱
  5. 参数可变的方法声明
  6. 抽象类
  7. 枚举类
  8. 密封类
  9. 反射的基本概念与应用
    在实践方面,本周主要学习了三层架构的一些基本思想,并基于三层架构思想,简单设计了一个j银行模拟系统:
    用户首先进入登陆界面输入用户名与密码进行登录
    在主界面中显示用户的余额,同时用户通过输入框和单选框进行存款、取款操作

理论部分

第五章 继承

继承 inheritance:是OOP中的一个基本概念

核心思想:基于已有的类创建新的类

新的类可以继承旧类的方法,并拥有自己的字段和方法来适应新情况

这是Java程序设计中的一种核心技术

继承关系与聚合关系

里氏替换原则 LSP

LSP的核心思路是:子类必须能够完全替代父类,并且父类的行为对子类合理

经典的例子是类:Animal、Bird、Penguin

Animal类只有动物的基本的属性:

  • 吃——eat
  • 喝——drink
  • 生孩子 ——produce

Bird很显然完全能够代表Animal,同时抽象的动物类中的行为对于鸟都是合理的,那么动物与鸟可以是继承关系

但是类Penguin不考虑实际生物科学,仅从行为合理的角度考虑,Bird的行为飞——fly显然不适合企鹅,让企鹅继承鸟就不符合LSP

共享接口与重用实现
  • 继承适合的场景:

    • 子类需要复用父类的代码(字段与方法)
    • 子类需要与父类共享相同的接口 ——多态性
  • 聚合has -a 适合的场景:

    • 对象需要组合多个独立的功能,而不是通过层级关系扩展
  • 例子:

  •   class Square extends Shape{//
          @override
          public double calculateArea(){
              return this.length*this.length;
          }
      }
    
  • 这里正方形与更笼统的图形,之间的关系,虽然从我们自然语言的角度思考,似乎可以说:“图形中有正方形”,好像是has-a关系

  • 但其实如果我们更加清晰地思考这句话的本质:“图形中有一种被称为正方形的图形”

  • ==>等价于“叫正方形的图形,是一种图形”

  • 是很明显的“is-a”关系

  •   // 聚合:Car 使用 Engine 的功能,但 Engine 独立存在
      class Car {
          private Engine engine;
          public Car(Engine engine) {
              this.engine = engine;
          }
      }
    
  • 这里车与引擎很显然就是聚合关系了,从逻辑上也不可能说车是一种引擎

动态扩展行为

动态扩展行为:将类的功能委托给其他对象(构成组合关系),从而允许在程序运行期间灵活的替换或调整这些功能

这种设计方式的核心是解耦与灵活性

对比继承
  • 继承的局限性:
    • 继承关系在编译时就会确定,子类一旦继承了父类,其行为在运行时无法修改
  • 组合的灵活性:
    • 通过组合,一个类可以持有其他对象的引用,并在运行时动态的替换这些对象,从而改变自身行为
    • 比如一个Robat类可以组合不同的Weapon对象,实现在运行时更换武器
实现动态行为

(1)定义接口——实现对行为的抽象

通过接口定义行为规范,具体实现由不同的类完成:

//定义攻击行为的接口
public interface AttackBehavior{
    void attack();
}

//具体实现:使用剑攻击
public class SwordAttack implements AttackBehavior{
    @override
    public void attack(){
        System.out.println("用🗡攻击!");
    }
}

//具体实现:用魔法攻击
public class MagicAttack implements AttackBehavior{
    @override
    public void attack(){
        System.out.println("🔥攻击!");
    }
}

(2)组合对象并委托行为

在我们需要行为的类中持有接口类型的成员变量,并通过该变量调用具体行为:

public class Robot{
    //组合一个攻击行为对象,通过接口引用
    private AttackBehavior attackBehavior;
    
    //注入攻击行为
    public Character(AttackBehavior attackBehavior) {
        this.attackBehavior = attackBehavior;
    }
    
    
    //动态切换攻击行为
    public void setAttackBehavior(AttackBehavior attackBehvior){
        this.attackBehavior = attackBehavior;
    }
    
    //委托给攻击行为执行对象
    public void performAttack(){
        attackBehavior.attack();
    }
   
}

(3) 在运行时动态改变行为

通过替换组合的对象,实现行为的动态调整:

public class Main {
    public static void main(String[] args) {
        // 初始使用剑攻击
        Character knight = new Character(new SwordAttack());
        knight.performAttack(); // 输出:🗡

        // 运行时切换为魔法攻击
        knight.setAttackBehavior(new MagicAttack());
        knight.performAttack(); // 输出:🔥
    }
}
动态行为的优势
  1. 符合开闭原则
    • 无需修改现有代码即可扩展新功能
    • 例如,新增一个BowAttack类实现AttackBehavior,直接注入即可使用
  2. 降低耦合
    • Robot不依赖具体的攻击实现,只依赖接口
    • 修改 SwordAttackMagicAttack 的代码不会影响 Robot
  3. 支持运行时灵活性
    • 根据场景可以动态的切换行为,例如游戏角色更换装备、支付系统切换支付方式
实际应用场景

策略模式:

场景:需要根据上下文选择不同 算法

示例:排序算法(冒泡、快速)的动态切换

// 定义排序策略接口
public interface SortStrategy {
    void sort(int[] array);
}

// 具体策略实现
public class BubbleSort implements SortStrategy { /* ... */ }
public class QuickSort implements SortStrategy { /* ... */ }

// 上下文类(组合策略对象)
public class Sorter {
    private SortStrategy strategy;
    public void setStrategy(SortStrategy strategy) {
        this.strategy = strategy;
    }
    public void executeSort(int[] array) {
        strategy.sort(array);
    }
}

状态模式:

场景:对象的行为岁状态改变而改变

示例:电梯的不同状态 运行中 停止 故障

// 定义电梯状态接口
public interface ElevatorState {
    void handleRequest();
}

// 具体状态实现
public class RunningState implements ElevatorState { /* ... */ }
public class StoppedState implements ElevatorState { /* ... */ }

// 电梯类(组合状态对象)
public class Elevator {
    private ElevatorState state;
    public void setState(ElevatorState state) {
        this.state = state;
    }
    public void request() {
        state.handleRequest();
    }
}

注入依赖(我还没学习到JavaEE知识,这部分做了解,但是原理与上面的场景类似):

场景:通过外部容器管理对象依赖

示例:Spring框架中的Bean注入

// 服务接口
public interface NotificationService {
    void send(String message);
}

// 具体实现
public class EmailService implements NotificationService { /* ... */ }
public class SMSService implements NotificationService { /* ... */ }

// 客户端类(组合服务对象)
public class Client {
    private NotificationService service;
    // 通过构造函数注入依赖
    public Client(NotificationService service) {
        this.service = service;
    }
    public void doSomething() {
        service.send("Hello!");
    }
}
与继承的对比

继承的静态性:

// 静态继承:无法在运行时改变行为
class Dog extends Animal {
    void bark() { System.out.println("汪汪!"); }
}

// 如果想让 Dog 改为“喵喵叫”,必须创建新子类
class CatDog extends Animal {
    void bark() { System.out.println("喵喵!"); }
}

组合的动态性:

// 通过组合动态改变行为
class Animal {
    private SoundBehavior soundBehavior;
    public void setSoundBehavior(SoundBehavior soundBehavior) {
        this.soundBehavior = soundBehavior;
    }
    public void makeSound() {
        soundBehavior.makeSound();
    }
}

// 运行时切换
Animal animal = new Animal();
animal.setSoundBehavior(new DogSound()); // 汪汪!
animal.makeSound();
animal.setSoundBehavior(new CatSound()); // 喵喵!
animal.makeSound();
总结

通过组合对象实现动态行为的核心思想是:

  1. 定义接口规范行为
  2. 委托给组合的对象执行具体操作
  3. 运行时替换组合对象以改变行为

这种设计方式使得代码更灵活、可扩展,并符合面向对象设计原则(如开闭原则、单一职责原则)。在实际开发中,应优先考虑组合而非继承,尤其是在需要动态调整功能的场景中。

继承与聚合的抉择

实际上,滥用继承可能导致高耦合,但是如果过度追求聚合,也可能导致代码冗余,无法满足多态需求

在这里插入图片描述

5.1 类、超类、子类

业务中判断两个类是否应该有继承关系的一个比较常用的经验是寻找“is-a”关系

“is-a”是继承关系的一个明显特征

例如,公司中除了普通员工还需要新的员工——经理Manager,经理与员工之间存在is-a关系:所有的经理都是员工

(注:实际生活中会有员工升职成经理、经理变成员工的复杂情况,这里是假定员工永远是员工,经理永远是经理)

5.1.1 定义子类

使用关键字extends表示继承

public class Manager extends Employee{
    ...
}

关键字extends指示正在构造的新类派生于一个已经存在的类

已经存在的类称为超类或父类、基类

新的类称为子类或派生类

一般Java程序员习惯说超类与子类

子类比超类拥有更多的功能,封装了更多的数据

但是,从集合论的角度,新派生的类是旧的类的子集,旧类是新类的超集

假设Manager比Employee多出一个字段:bound

public class Manager extends Employee{
    private BigDecimal bounds;
    ....
   	public void setBounds(double bounds){
        this.bounds = BigDecimal.valueof(bounds);
    }
}

这里与正常的类没有太多区别,如果有一个Manager对象,它就可以使用setBounds方法

但是

Manager中没有显式的定义getName()、gettId()等方法,Manager对象仍然可以直接调用这些方法

因为Manager类自动的从超类中继承了这些方法,同时也继承了字段

一个Manager对象有name,salary,hireDay,id四个继承自超类的实例字段和自己创建的实例字段bound。

Java规范指出:声明为私有的类成员不会被这个类的子类继承

但这里这个继承的意思不是说Manager对象没有这些类成员,而是说Manager对象不能直接访问这些类成员

也就是说:

public class Manager extends Employee{
    private BigDecimal bounds;
    ....
   	public void setBounds(double bounds){
        this.bounds = BigDecimal.valueof(bounds);
    }
    ...
    this.name.....//是错误的,Manager对象无法直接访问超类的私有字段
    String name = this.getName(); //是可行的,得到的是this的Name
}

这样的设计乍看似乎非常麻烦

但其实这是封装的核心思想的体现

这样设计有以下好处:

  • 隐藏实现细节

    • 超类的内部状态是超类的实现细节,直接暴露会破坏封装性

    • 假设:

    •   class BankAccount {
            private double balance;  // 私有字段,隐藏实现细节
        
            public void deposit(double amount) {
                if (amount > 0) balance += amount;  // 通过方法控制逻辑
            }
        }
        
        class SavingsAccount extends BankAccount {
            // 子类无法直接修改 balance,必须通过 deposit() 方法
        }
      
    • 这里,超类可以自由的修改balance的存储方式,比如修改成BigDecimal而无需通知子类

  • 保持数据一致性

    • 通过公共方法访问字段,父类可以添加校验逻辑,避免非法操作

    • 示例:如果子类直接修改balance,可能绕过超类的校验

    •   // 错误设计:允许子类直接访问 balance
        class BankAccount {
            public double balance;  // 公开字段,危险!
        }
        
        class SavingsAccount extends BankAccount {
            void withdraw(double amount) {
                balance -= amount;  // 可能使余额为负数
            }
        }
      
    • 正确设计:将字段设置为prviate,通过方法控制:

    •   class BankAccount{
            private double balance;
            
            public void withdraw(double amount){
                if(amount >0 && balance >= amount){
                    balance -=amount;
                }
            }
        }
      
  • 降低耦合度

    • 如果子类直接依赖父类的字段实现细节,会导致紧耦合,如果超类修改字段名或类型,所有子类都需要同步修改
    • 比如例子中超类将balance重命名为accountBalance,子类中所有直接访问balance的代码将全部报错
5.1.2 覆盖方法

超类中的有些方法不一定对子类都适用,比如Manager类中的getSalary方法就应该返回薪水和奖金的总和

为此,我们需要提供新的方法来**覆盖(override)**超类

public class Manager extends Employee{
    ...
    @override
    public double getSalary(){
       return this.salary+this.bound;//错误,Manager不能直接访问salary字段
    }
}

这里,由于salary是继承自超类的字段,我们无法直接访问

所以,我们需要适用公共接口,假设我们这样修改代码:

public class Manager extends Employee{
    ...
    @override
    public double getSalary(){
       return this.getSalary()+this.bound();//错误,会无限递归
    }
}

似乎没有问题,但是要注意,我们本来就在重写getSalary方法,这样会导致无限的调用getSalary()直到程序崩溃

总结下来,这里我们的目的是调用超类的getSalry()方法而不是当前的getSalary()方法

这里,就需要使用一种特殊的关键字 supper

supper.getSalary()表示调用超类Employee中的getSalary方法

最终,将我们的getSalary()修改为:

public double getSalary(){
    return supper.getSalary()+this.bound;
}

注意,从功能上看,supper与this有些类似

但是两者有很大的区别,this的本质是对一个对象的引用,可以将this赋值给一个对象变量;

但是supper只是一个关键字,只是编译器调用超类方法

5.1.3 子类构造器

对于子类的构造器:

class Manager extends Employee{
    private double bounds;
    ...
    public Manager(String name,double salary,int year,int month,int day,double bounds){
        supper(name,salary,year,month,day);
        this.bounds = bounds;
    }
}

这里,supper(name,salary,year,month,day)表示调用超类的对应的构造器

要时刻牢记,Manager类中构造器不能直接访问继承自超类的私有字段

supper调用超类构造器必须是第一条语句

如果子类构造对象时不显式的使用supper调用超类构造器,那么超类必须有一个无参数的构造器,这个构造器要在子类构造之前调用

示例:主程序创建一个Employee数组,并创建两个普通员工一个经理,对所有对象的状态进行遍历:

package inheritance;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Objects;

public class ManagerTest {
    public static void main(String[] args) {
        Employee[] staff = new Employee[3];
        staff[0] = new Employee("小明",75000,2024,12,31);
        staff[1] = new Employee("王老五",80000,2024,7,15);
        staff[2] = new Manager("王鑫",90000,2018,9,1);
       // staff[2].setBound()
        //Manager boss = staff[2];


//        Manager[] managers = new Manager[3];
//        Employee[] staff2 = managers;
//        staff2[0] = new Employee(1000,2020,11,11);
//        System.out.println(staff2[0]);

        for (Employee e : staff) {
            System.out.println(e);
            e.raiseSalary(15);
          // System.out.println("提升15%薪水后,"+e.getName()+"的薪水是"+e.getSalary());
            System.out.printf("%s提升15%%薪水后,薪水为%,.2f\n",e.getName(),e.getSalary());
        }

    }

}

/**
 * 一个{@code Employee}对象表示了一名普通员工,
 * 拥有id,姓名,薪水。入职日期四个属性
 */
class Employee {
    private static long nextId = 20250001;

    private long id;
    private String name;
    private BigDecimal salary;
    private LocalDate hireDay;

    {
        this.id = nextId;
        nextId++;
    }

    public Employee( String name, double salary, int year,int month, int day) {
        this.name = Objects.requireNonNull(name,"员工姓名不能为null!");
        this.salary = BigDecimal.valueOf(salary);
        this.hireDay = LocalDate.of(year, month, day);
    }

    public Employee(double salary, int year, int month, int day) {
        this("Employee#"+nextId, salary,year,month,day);
        this.id = nextId;
        nextId++;
    }



    public long getId() {
        return id;
    }
    public String getName() {
        return name;
    }
    public double getSalary() {
        return this.salary.doubleValue();
    }
    public LocalDate getHireDay() {
        return hireDay;
    }

    /**
     * 根据百分比提升员工的薪资,e.g.10代表提高10%
     * @param byPercent 提升薪资的百分比
     */
    public void raiseSalary(int byPercent) {
        double raise = salary.doubleValue() * byPercent / 100;
        salary = salary.add(BigDecimal.valueOf(raise));
    }

    public String toString() {
        return "Employee [id=" + id + ", name=" + name + ", salary=" + salary+ ", hireDay=" + hireDay + "]";
    }

}

/**
 * 一个{@code Manager}对象继承自Employee,代表经理员工
 * 除了Employee的四种属性(姓名,ID,薪水,入职日期)外,还有一个额外的属性是经理独有的奖金
 */
class Manager extends Employee {
    private BigDecimal bound;
    public final double BASE_FOUND = 5000.0;

    public Manager(String name, double salary,int year,int month, int day) {
        super(name, salary, year, month, day);
        this.bound = BigDecimal.valueOf(BASE_FOUND);
    }

    public double getBound() {
        return bound.doubleValue();
    }

    public void setBound(double bound) {
        this.bound = BigDecimal.valueOf(bound);
    }

    /**
     * 由于经理的工资构成与普通的Employee不同,因此需要重写该方法
     * 经理的工资为基本工资+奖金
     * @return 经理工资的double值
     */
    @Override
    public double getSalary() {
        return super.getSalary()+this.bound.doubleValue();
    }

    /**
     * 经历的工资计算方法不同,因为经理的工资要加上奖金部分
     * @param byPercent 提升薪资的百分比
     */
    public void raiseSalary(int byPercent) {
        double raise = (bound.doubleValue()+super.getSalary()) * byPercent / 100;
        bound = bound.add(BigDecimal.valueOf(raise));
    }

    public String toString() {
        return "Manager [id="+super.getId()+",name = "+super.getName()+",salary="+this.getSalary()
                +",hireDay="+super.getHireDay()+ ",bound=" + bound + ", base=" + BASE_FOUND + "]";
    }


}	
5.1.4 继承层次结构

继承不仅限于一个层次

由一个公共类派生出来的所有类的集合称为继承层次结构

从某个特定的类到其祖先的路径称为该类的继承链

Java中,一个类只能有一个超类,但可以有多个子类

在这里插入图片描述

这里,Programmer与Secretary和Manager之间没有任何关系

5.1.5多态

is-a关系本质就是说“所有的子类都是超类”

因此,程序中需要超类对象的地方都可以使用子类对象替换

Employee staff = new Manager(...);//这里,本来超类对象变量需要引用超类对象,但根据替换原则,完全可以让它引用子类对象

超类变量 ===>子类对象 √

子类变量 ===>超类对象×

在Java程序设计语言中,对象变量是**多态(polymorphic)**的

一个超类变量既可以引用超类对象,也可以引用任何一个子类的对象

Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
...
staff[0] = boss;//这是完全合法的,让一个Employee变量引用一个子类Manager对象

但是虽然这两个变量staff[0]boss引用同一个对象

但是在编译器看来,staff[0]始终是一个Employee类型变量,boss才是Manager类型变量

因此,boss.setBounds(5000)是合法的调用

但是,staff[0].setBounds(5000)在编译器的角度就是一个超类变量试图调用子类独有的方法,会报错

由此,我们也可以更好的理解,不能给子类变量赋值一个超类对象

Manager m = new Employee(...)

否则,就会产生m.setBounds()这样让一个超类对象调用了它自己本身并不拥有的方法的错误

注意:

以下有一种情况不会发生编译错误:

Manager[] managers = new Manager[10];  //声明一个子类数组
Employee[] staffs = managers; //这是合法的,因为每个Manager确实都是Employee
//此时,staff和managers指向了同一个数组
staff[0] = new Employee(...);//这里编译器接受了将一个超类对象赋值给staff[0]
//因为编译器角度看staff[0]确实是一个Employee变量

//但是,staff和managers指向了同一个数组!
//也就是说,现在managers[0] 引用了一个超类对象
managers[0].setBounds(500); //这里就是让超类对象调用了子类独有的方法!将访问一个不存在的实例字段,会扰乱相邻存储空房间

为了确保不发生这样的破坏,我们必须要牢记创建时的元素数据类型,并负责监督仅将类型兼容的引用存储到数组中

例如,当Manager数组试图存储一个Employee类型的引用时,会抛出ArrayStoreException异常

5.1.6 理解方法调用

假设一个对象变量调用方法:

child.function(args)

其中,child是声明为类Child的一个对象

下面是方法调用过程的详细描述:

  1. 编译器查看对象的声明类型和方法名
    • 有可能存在多个名为function但参数类型不一样的方法
    • 编译器会挨个列举Child类中所有为function的方法以及超类中所有名为function且可访问的方法(不会列举超类的私有方法)
  2. 编译器要确定方法调用中提供的参数类型
    • 如果在所有名为function的方法中存在与一个与所提供参数类型完全匹配的方法,就会选择这个方法
    • 这个过程称为重载解析 orverloading resolution
    • 比如,对于调用child.function("Hello")会调用function(String)
    • 但由于存在类型转换(int可以自动转换成double,Manager可以自动转换成Employee),实际过程可能复杂一些
    • 如果编译器没有找到与参数类型匹配的方法,或者通过类型转换后有多个方法与之匹配,编译器就会报告错误
  3. 至此,编译器确认了要调用的方法名和参数类型

在这里插入图片描述

注意:

方法签名不包含返回类型

子类覆盖超类的方法,就是在子类中定义一个方法,这个方法与超类中的一个方法会有相同的方法签名(方法名+参数类型)

但是,覆盖时,我们需要保证子类中返回值的类型兼容性

也就是说,允许子类将覆盖方法的返回类型修改成原返回类型的子类型

例如,假设Employee有一个方法public Employee getBuddy(){...}

我们在子类Manager对这个方法进行覆盖 → public Manager getBuddy(){....}

我们称,这两个getBuddy方法有**协变(covariant)**的返回类型

对于private方法、static方法、final方法或者构造器,编译器可以直接准确的知道调用哪个方法,这称为静态绑定 static binding

与此对应的是,如果调用的方法依赖于隐式参数的实际类型,那么必须在运行时使用动态绑定

在例子中,编译器会利用动态绑定生成一个调用function(String)的指令


当程序运行并且采用动态绑定调用方法时,虚拟机必须调用变量所引用对象的实际类型的对应的方法。

假设:x.f(args)中,x的类型是D,它是C的子类

如果D定义了方法f(String)虚拟机调用这个方法;否则,会在D类的超类中寻找f(String),以此类推

每次调用方法时都要进行这个搜索,时间开销比较大

因此,虚拟机会预先为每个类计算一个方法表,其中列出所有方法的签名和要调用的实际方法

每当虚拟机加载一个类到内存,就会构建这个方法表,(会结合虚拟机在类文件中找到的所有方法以及超类的方法表)

这样,当真正调用方法时,虚拟机只要查询这个表就可以找到对应的方法

在上文的演示程序中,我们使用了foreach循环调用了e.getSalary(),可以具体的分析一下这里的调用过程:

e声明为了Employee类型

Employee只有一个getSalary()方法,因此不存在重载解析的问题

getSalary()不是private、static或final方法,因此会采用动态绑定

虚拟机为Employee和Manager两个类分别生成方法表

Employee:
getId() -> Employee.getId()
getName() -> Employee.getName()
getSalary() -> Employee.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)

当然,这个放发表并不完整,Employee还有一个超类Object,Employee从这个超类中继承了大量的方法,这里,仅列出了我们自定义的Employee方法

对于Manager,其中有3个方法是继承来的,两个方法是重写的,还有一个新增的方法

Manager:
getId() -> Employee.getId()
getName() -> Employee.getName()
getSalary() -> Manager.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Manager.raiseSalary(double)
setBounds(double) -> Manager.setBounds(double)

运行时,e.getSalary()的执行过程为:

  1. 虚拟机获取e的实际类型的方法表,可能是Employee的,可能是Manager的,也可能是任何Employee的子类
  2. 虚拟机查找定义了getSalary()签名的类,让虚拟机确定该调用哪个实际方法
  3. 最后,虚拟机调用这个方法

在这里插入图片描述

动态绑定的一个重要特性——无须修改现有的代码实现对程序的扩展

假设我们增加了一个新类Excutive,并且变量e可能引用这个对象

我们不需要对包含e.getSalary()的代码重新编译,当e恰好引用一个新类Executive对象时,会自动调用Excutive.getSalary()方法

注意:子类覆盖方法时,可见性不能低于超类的方法的可见性

比如超类方法声明为public,子类中覆盖的方法必须是public

5.1.7 阻止继承:final类和方法

如果我们希望阻止对某个类进行集继承,可以使用final修饰符修饰

final类无法进行继承

public final class Executive extends Manager{
    ....
}

也可以将类中某个特定的方法声明为final,如果这样做,所有子类都不能覆盖这个方法,(final类中所有方法自动为final)

例如:

public class Employee{
    ...
    public final String getName(){
        return this.name;
    }
        
}

当然,字段也能声明为final,final字段表示构造之后不能再改变了

如果将一个类声明为final之后,方法会自动改为final修饰,但是字段不会变成final

将类和方法声明为final只有一个原因,确保它们不会在子类中改变语义

有人认为除非有足够的理由使用多态,否则所有方法都应该声明为final

这有些偏激,应该在设计类的层次结构式,仔细考虑哪些字段和方法应该是final

早期的Java中,如果一个方法没有被覆盖并且很短,编译器会对它进行优化处理,这个过程称为内联

例如内联调用e.getName()会替换成访问字段e.name

这样做,可以比较大的优化CPU效率,因为CPU处理当前指令时,如果有分支,即方法调用,会扰乱预取指令的策略

因此,将e.getName()替换成e.name可以提升CPU效率

而如果一个方法被子类覆盖,编译器将无法知道覆盖代码会执行哪些操作,无法对它进行内联处理

但是,现代的即时编译器比传统编译器能力强得多,编译器可以准确的知道哪些类扩展了一个给定类,并且能够检查是否有类覆盖了给定了的方法,如果方法很简短、被频繁调用且确实没有被覆盖,即时编译器就会对它进行内联;而如果虚拟机加载了一个子类,子类覆盖了一个内联方法,优化器就会取消对这个方法的内联,不过这种情况比较少见

注:枚举 enum和记录 record总是final,不允许被扩展

5.1.8 强制类型转换

将一个类型强制转换成另一个类型称为强制类型转换

基本数据类型中,当我们试图把一个大存储范围的类型赋给一个小存储范围的时候,就需要强制类型转换

double x = 3.854;
int nx = (int)x;

对于引用类型,也可以使用强制类型转换,当然,只能在继承层次结构内进行强制类型转换

不可能让一个String类型强制类型转换成Employee类

语法上没有太大区别:

var staff = new Employee[3];
staff[0] = new Manager(...);
....
Manager boss = (Manager)staff[0];

Java中,每个对象变量都有一个类型,这个类型描述了这个变量引用哪种对象,以及能够做什么

当我们给一个超类变量赋值一个子类对象时,就像子类对象转成了超类对象一样, 称为向上转型 Upcasting

编译器认为这种行为是安全的,可以自动进行

因为:

  • 一个子类对象会至少包含超类对象的所有方法与字段,或者说,子类对象是一种特殊的超类

  •   class Parent {
          void show() { System.out.println("Parent show"); }
      }
      
      class Child extends Parent {
          void display() { System.out.println("Child display"); }
      }
      
      public class Main {
          public static void main(String[] args) {
              Parent p = new Child(); // 赋值是安全的
              p.show(); // 可以调用,因为 show() 是 Parent 的方法
          }
      }
      
    
  • 这里Child继承了Parent的show方法,因此无论p的实际类型是child还是parent,p.show()都是安全的,编译器能够确信p一定有这个方法

  • 即使子类Child有更多的方法和字段,但是,变量p只能“看到”超类继承下来的部分

  • 也就是说,我们如果直接用指向子类对象的超类变量,去访问子类独有的字段或方法,就会产生编译错误

但是,当我们试图让子类变量引用超类对象时:

假设允许隐式转换

Animal animal = new Animal();
Dog dog = animal; // 编译错误:需要强制类型转换
dog.bark();       // 若允许,这里会调用不存在的 bark() 方法!

因此,强制类型转换的意义就是告诉编译器:开发者明确知道这是子类对象的实例

不过,虽然编译器能通过,但是如果我们通过强制转换将超类对象赋给子类变量,运行时仍然会产生classCastException异常

Animal animal = new Dog();       // 合法:向上转型(无需转换)
Dog dog = (Dog) animal;          // 合法:animal 实际指向 Dog
dog.bark();                      // 安全调用

Animal notADog = new Animal();
Dog invalidDog = (Dog) notADog;  // 运行时抛出 ClassCastException

总结:

  • 当超类变量引用子类对象,完全合法,称为向上转型
    • 但是超类变量仍然只能访问从超类继承的字段或方法
  • 当子类变量引用超类对象,需要强制类型转换,称为向下转型
    • 强制类型转换可以通过编译,但如果子类变量引用超类对象,运行时会产生classCastException异常

事实上,我们应该有这样一种理念:

只有当我们需要暂时忘记对象的实际类型之后使用对象的全部功能时,才进行强制类型转换

就像例子中我们创建的Employee[]数组,这里数组必须声明为Employee[],因为有的员工是普通的Employee类型

当我们在这个数组中存储了子类对象引用,又需要使用子类对象的独有功能时,才考虑新建一个子类变量,将对应的数组元素进行类型转换。


如果我们确实需要使用强制类型转换,可以使用instanceof操作符检查是否能够成功转换,

例如:

if(staff[i] instanceof Manager){
    boss = (Manager) staff[i];
}

注:

如果x为null,x instanceof C不会产生异常,只会返回false

总而言之,需要对强制类型转换保持警惕,这不是一种好行为

大多说情况都不会需要将一个超类如Employee对象强制类型转换为Manager对象

这是因为实现多态性的动态绑定机制能够自动找到正确的方法

只有希望使用子类中特有的方法时我们才可能会考虑强制类型转换

如果出现了需要在超类对象上调用子类的方法,就应该思考超类的设计是否真的合理

5.1.9 instanceof 模式匹配

Java16之前,如果我们希望使用instanceof操作符,大概要使用以下的语句:

if(staff[i] instanceof Manager){
    Manager boss = (Manager) staff[i];
    boss.setBounds(500);
}

这里Manager出现了3次,整个代码有些冗长

Java16提供了一种简便方法,直接在instanceof测试中声明变量:

if(staff[i] instanceof Manager boss){
    boss.setBounds(5000);
}

这里,如果staff[i]是Manager的一个实例,那么会将变量boss的值设置为staff[i]

省略了手动进行强制类型转换的步骤

如果staff[i]不能通过instanceof,那么不会设置boss,instanceof会设置为false,跳过if语句

注:理论上我们可以这样写:

Manager boss = new Manager(...);
if(boss instanceof Employee e){
    ....
}

从语法层面讲没有什么错误,但是boss作为子类对象引用,本来就能隐式的向上转型赋值给超类变量,这个语法没有任何意义

当使用intsanceof模式引入一个变量时,我们可以立即在同一表达式中使用这个变量:

if(e instanceof Manager m && m.getBounds()>1000)....

当&&左边为true时,才会去运算右边的值,因此如果进行右边的运算,说明m必然为一个Manager的实例,不会有什么问题

但是,在||时,却可能有些问题:

if(e instanceof Manager m || m.getBounds() > 1000)....

因为||同样采用短路策略:只有左边表达式为false才会运算右边的表达式,

因此,只有m没有绑定到Manager实例时才会进行右边的运算,这必定会产生异常

也可以在三目运算中使用instanceof模式:

double bounds = e instanceof Manager m ? m.getBounds():0;

在Java17中,引入了增强的switch类型模式,可以支持更强大的instanceof模式:

String descripiton = switch(e){
        case Excutive exec -> "一名有着"+exec.getTitle()+"的漂亮的头衔的主管";
        case Manager m ->"一名有着金额为"+m.getBounds()+"的奖金的经理";
        case null -> "为null!";
    	default -> "一名有着金额为"+e.getSalary()+"的工资的员工";
}

这里,switch直接进行模式匹配,可以省略instanceof和强制类型转换,同时,还支持对null的处理

5.1.10 受保护访问

根据封装的思想,我们应该讲字段设置为private,而方法标记为public。

任何声明为private的特性不允许其他任何类访问,即使对子类也适用

不过,有时候,我们可能希望某个方法只允许子类访问,或者,更少见的,可能允许子类的方法访问超类中的某一个字段。

这种情况下,可以将一个类特性(方法或字段)声明为受保护 protected

不过,Java中,受保护字段protected只能由同一个包中的类访问(这与protected的本意有些不同,对于protected的类特性,Java中不止是子类可以查看,同一个包中其他类也可以访问)

假设有一个Employee的子类Administrator,这个类位于不同的包中

Adminstrator类中的方法只能查看Adimistrator对象自己的字段,不能查看其他Employee对象的字段即使声明为protected


在实际应用中,我们应该谨慎使用protected字段

因为,如果我们使用了protected字段,有其他人继承了我们的类进行扩写使用了这些字段

而一旦我们对这个protected字段进行修改,就一定会影响到其他人的代码,这违背了OOP精神


受保护的方法更有意义,比如一个类中某个方法的使用非常棘手,就可以将它声明为protected,表明相信子类(很可能由熟悉超类实现的人来创建)可以正确的使用这个棘手的方法,而其他类则不行

总结:

Java中有4个访问控制修饰符:

  1. 仅本类可以访问——private
  2. 可由外部访问——public
  3. 本包和所有子类可以访问——protected
  4. 本包中可以访问——默认的,不需要访问修饰符

5.2 Object:所有类的超类

可以认为,Object是Java中所有类的始祖,每一个类都默认扩展了Object类

当我们不明确指出一个类的超类,那么它的超类就是Object

5.2.1Object类型的变量

可以使用Object类型引用任何类型变量,可以把Object视作一个泛型容器

Object obj = new Employee(...);

这相当于向上转型,是合法的,不过正如其他用超类变量应用子类对象的情况一样,Object变量也只能使用Object类型的方法与字段,当我们想使用独有的类特性时,必须使用强制类型转换(向下转型)

Employee e = (Employee)obj;

注意:

  • Java中,基本数据类型的变量不是对象
  • 任何类型的数组都是对象,也就是说,任何数组类型都扩展了Object类
5.2.2 equals方法

Object类中的equals方法用于检测一个对象是否等于另一个对象

具体来说,Object类中equals方法会判定两个对象引用是否相同——两个对象存储空间是否在一起

这是合理的,如果两个对象存储在同一个位置,那么当然这两个对象是相等的

这个方法对于大部分类其实已经足够了,比如,比较两个PrintStream对象是否相等没有什么意义

但是,我们经常遇到另一种情况:只有两个对象的实例字段值,也就是状态相同,才认为两个对象相等

例如,两个Employee对象,我们认为如果两个员工姓名、薪水、入职日期都相同,那么它们就是相等的:

public class Employee{
    ...
    @Override
    public boolean eaquls(Object otherObject){
        //如果两个对象引用相同,直接认为相等
        if(this == otherObject){
            return true;
        }
        //如果显式参数为null,则认为不相等
        if(otherObject == null){
            return false;
        }
        //如果类型不同,认为两个对象不相等
        if(getClass() != otherObject.getClass()){
            return false;
        }
        //到此为止,我们确定了otherObject是一个非空的Employee对象
        Employee other = (Employee)otherObject;
        
        //比较两个对象具体的实例字段值
        return name.equals(other.name)
               && salary == ohter.salary
               && hireDay.equals(other.hireDay);
        
    }
}

其中,getClass方法会返回一个对象所属的类

为了防备name或者hireDay可能 为null的情况,可以使用Objects.equals方法:

  • 当两个对象都为null,Objects.equals(a,b)返回true
  • 如果其中一个参数为null,Objects.equals(a,b)返回false
  • 如果都不会null,Objects(a,b)会调用a.equals(b)

因此,可以讲演示中的最后一条语句改为:

return Objects.equals(name,other.name)
       && salary == other.salary
       && Objecets.equals(hireDay,other.hireDay);

当子类中定义equals时,首先调用超类的equals方法,如果检测失败,那么对象就不可能相等:

public class Manager extends Employee{
    ...
    @Override
    public boolean equals(Objects otherObject){
        if(!super.equals(otherObject)){
            return false;
        }
        //在超类中的equals方法会检测两个类型是否相同
        Manager other = (Manager) otherObject;
        
        return this.bounds == other.bounds;
    }
}

注:记录record会自动提供equals方法,这个方法会比较两个记录相应字段的值

5.2.3 相等测试与继承

对于equals方法显式参数与隐式参数不属于同一个类型时,如何处理相等有一些争议

有些人喜欢使用instanceof检测:

if(!(otherobject instanceof Employee)) return false;

这代表,允许otherobject 是一个Employee类的子类

但是,这样使用有些问题:

Java语言规范要求equals方法有以下性质:

  • 自反性:对于任何非null引用x,x.equals(x) 应该返回true
  • 对称性:对于任何非null引用x和y,当且仅当x.equals(y)返回true时,y.equals(x)也要返回true
  • 传递性:对于任何引用x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)也应该返回true

而如果我们的equals方法允许不同类型,对称性可能会出些问题

假设一个Employee对象e和一个Manager对象m,假设他们有相同的姓名、薪水和入职日期

e.equals(m)

这里,由于m是e的子类,会通过intsanceof检测

因此这里会返回true

但是,由于对称性规则要求,我们调用m.equals(e)也必须返回true,而不能返回false或者抛出异常

这样,就有一个比较大的问题,首先e intstanceof Manager是false,会导致整个方法返回false

而且,由于Manager有自己独有的字段bounds,那么就要求e访问一个不存在的属性

总结下来,关于超类与子类的相等性问题,现在有两种情况:

  • 子类有自己独特的属性,因此子类有自己的相等性概念,此时我们必须使用getClass方法判断equals的两个参数是否是同一类型
  • 由超类决定相等性概念,此时可以使用instanceof检测,并且最好我们将equals方法声明为final

比如例子中的Employee与Manager

如果我们希望分别比较每个属性都相等才认为两个员工相等,那么就应该在equals方法中使用getClass;

如果我们希望直接通过Employee中的id字段是否相同来判断两个对象是否相等,那么就应该使用instanceof 检测,确保Manager对象也能与Employee比较,并且将Employee中的equals声明为final


因此,我们可以总结下编写equals方法的技巧:

  1. 将显式参数命名为otherObject,稍后才将他强制类型转换为名为other的变量
  2. 检测this与otherObject引用是否相同:if(this == otherObject)
    • 这其实是一种优化,实际上这种情况比较常见,检查同一性的开销比逐个比较字段开销小
  3. 检测otherObject是否为null,如果为null,返回false
  4. 比较this与otherObject的类型:
    • 如果equals方法的语义在子类中可以改变,就使用getClass()检测
    • 如果所有子类都有相同的相等性语义,则可以使用instanceof 检测
    • if(!(otherObject instanceof ClassName other)) return false 这里使用了instanceof模式,如果匹配,直接将otherObject转成了other变量
  5. 到这个阶段,根据相等性概念要求来比较字段:
    • 使用==比较基本数据类型
    • 使用Objects.equals(this.字段1,other.字段1)比较对象字段
  6. 对于子类中的equals方法,需要包含一个super.equals(otherObject)

注意:

  • 对于数组类型字段,可以使用静态的Arrays.equals方法检查相应的数组元素是否相等(多维数组使用Arrays.deepEquals)

当我们实现equals方法时,要注意显式参数应当声明为Object,否则,就无法覆盖Object类中的equals方法,而是自己定义了一个无关的方法

为了避免这种错误,我们可以使用@Override来标记那些要覆盖超类方法的子类方法

如果我们没有成功覆盖,编译器就会报告一个错误

例如,如果我们这样写:

@Override
public boolean equals(Employee other){...}

编译器就会报告一个错误,因为这个方法不会覆盖超类Object中任何方法

5.2.4 hashCode方法
散列码 hash Code
定义

散列码是通过哈希函数计算得出的一个固定长度的数值。

该数值用于表示输入数据的唯一标识

散列码通常用于数据存储、数据索引、数据校验和加密等场景

Java中,hashcode()方法用于返回对象的哈希码,通常用于哈希表如(HashMap、HashSet)中快速查找

哈希函数

哈希函数是一种数学函数,它将任意长度的输入映射为固定长度的输出一个优秀的哈希函数应该满足以下特性:

  • 确定性:相同输入始终生成相同的散列码
  • 高校计算:计算散列码的时间复杂度应该尽可能低
  • 均匀分布:散列值应该均匀分布,避免大量碰撞
  • 少碰撞:不同的输入尽量映射到不同的哈希值
哈希冲突

由于哈希函数是任意长度的输入,但是输出长度是固定的,那么一定会出现一种情况:不同的数据经过哈希函数运算后得到相同的散列码

这种情况就成为哈希冲突,理论上,哈希冲突是无法避免的

为了尽可能解决哈希冲突,有一些常见策略:

  • 链地址法:在每个哈希桶中使用链表存储冲突的元素
  • 开放地址法:当冲突发生时,在哈希表中孕照下一个可用的位置存储数据(如线性探测、二次探测、双重散列)
常见哈希算法

不同场景使用的哈希函数不同,常见的包括:

  • MD5
    • 生成128位哈希值(16字节),目前,已被认为不够安全
  • SHA
    • SHA-1:160位,已被淘汰
    • SHA-256:256位,目前广泛运用于密码学和数字签名
  • CRC
    • 用于检验数据完整性
  • MurmurHash
    • 高效的哈希算法,常用于哈希表索引
哈希码的应用
  • 哈希表 Hash Table
    • 用于高效存储和查找数据,如HashMapHashSet
  • 数据完整性校验
    • 通过SHA算法等验证文件或数据是否被篡改
  • 密码存储
    • 系统不会存储用户的明文密码,而是存储其哈希值
  • 数字签名和安全加密
    • 哈希算法常用于确保数据的完整性,如区块链技术
Java中的hashCode方法

Java中的hashCode定义在Object类中,因此,Java中每个对象都有一个默认的散列码,其值由对象的存储地址得出。

但是,我们通常希望得到的散列码更加有意义一些,例如,String类中就重写了hashCode方法:

public final class String{
    ...
    @Override
    public int hashCode(...){
        ...
        int hash = 0;
        for(int i = 0;i<length();i++){
            hash = 31 * hash + charAt(i);
            //charAt(i):返回给定位置的代码单元
        }
    }
}

我们可以看以下的一个例子:

var s = "OK",
var sBuilder = new StringBuilder(s);
System.out.println(s.hashcode()+""+sBuilder.hashCode());

var t = new String("OK");
var tBuilder = new StringBuilder(t);
System.out.println(t.hashcode()+""+tBuilder.hashCode());

最终,会得到以下输出:

对象散列码
s2556
t2556
sBuilder20526976
tBuilder20527144

可以发现,字符串s与t具有相同的散列码,这代表了字符串的散列码是由内容导出

而字符串构造器对象sBuilder和tBuilder却有不同的散列码,这是因为StringBuilder没有覆盖hashCode方法,它是使用的默认的Object中的hashCode方法,这是由地址导出的。


实际上,如果我们在类中重定义了equals方法,就必须为用户可能插入散列表的对象重新定义hashCode方法

hashCode方法应该返回一个整数,可以是负

我们设计hashCode时,应该合理组合实例字段的散列码,尽量分开不同对象的散列码

例如:

public class Employee{
    ...
    @Override
    public int hashCode(){
        return 7 * name.hashCode()
            +  11 * Double.valueOf(salary).hashCode()
            +  13 * hireDay.hashCode();
    }
}

当然,可以对这个方法进行进一步优化:

首先,为了确保null安全,我们应当使用Objects.hashCode:

  • 如果其参数为null,会返回0
  • 否则,返回对参数调用hashCode的结果

其次,可以使用静态的Double.hashCode来避免创建Double对象

public class Employee{
    ...
    @Override
    public int hashCode(){
        return 7 * Objects.hashCode(this.name)
            +  11 * Double.hashCode(this.salary)
            +  13 * Objects.hashCode(this.hireDay);
    }
}

在Objects工具类中,还有一个更加方便地方法Objects.hash()

这个方法专门用来组合多个散列值

这个方法会对各个参数调用Objects.hashCode,并组合这些散列码

因此可以进一步简写上面的hashCode()方法

public class Employee{
    ...
    @Override
    public int hashCode(){
        return Objects.hash(this.name,this.salary,this.hireDay);
    }
}

equals方法必须与hashCode的定义相容

也就是说:如果x.equals(y)返回true,那么x.hashcode()必须返回y.hashCode()相同的值

例如,如果定义Employee中equals是比较员工的ID,那么,hashCode就必须对ID值计算散列值,而不考虑员工的姓名存储地址等

对于数组类型字段,可以调用静态的Arrays.hashCode方法计算散列值,这个散列码由数组元素的散列码组成

记录record类型会自动提供hashCode方法,会由字段值的散列码得出一个散列码

注意:如果实例变量的取值范围很小,我们需要尽可能地得到不同的散列码

另外:当我们重写了hashCode方法后,如果是根据具体字段值来计算散列码的话,一定要记住,此时:

哈希码相同不代表存储空间上是同一个对象

5.2.5 toString方法

Object中还有一个很重要的方法 -toString

会返回一个字符串,表示对象的值

绝大多数toString方法遵循这样的格式:类名:[属性1名 =属性1值,属性2名 = 属性2值....]

例如,在Employee中提供这样的toString方法

public class Employee{
    ...
    @Overrride
    public String toString(){
        return "Employee[name="+ this.name
            +  ",salary=" + this.salary
            + ",hireDay=" + this.hireDay
            +"]";
    }
}

不过,最好不要硬编码的形式写类名,而是使用getClass().getName()的方法动态获得类名

Class getClass()
返回一个类对象,其中包含了对象的信息

这样在子类中也可以直接使用超类的toString

实际上,当我们使用+进行字符串拼接时,就是等效的调用了toString

在Object类中,toString()会打印对象的类名和散列码

例如,int[] numbers = {1,2,5,7,11};

如果我们直接使用numbers.toString()

会输出“[I@1a46e30”

前缀[I表示整型数组,@后面是散列码

因此对于数组类型,如果我们希望输出元素类容应该使用Arrays.toString()

5.3 范型数组列表

在c与c++中,必须在编译时确定数组大小,也就是不能用变量声明数组大小

Java中允许在运行时确定大小,也就是可以用变量声明数组大小

不过,数组的大小始终都是固定的,运行时如果希望动态修改数组大小,仍然非常麻烦

为了处理这种情况,Java提供了一个类ArrayList

ArrayList类似数组,但在添加或删除元素时,它能够自动调整容量

ArrayList是一种有类型参数泛型类

为了指定数组列表ArrayList的保存的元素的类型,需要用一对尖括号将元素类名括起来追加到ArrayList后面

ArrayList<元素类型> 数组列表变量名 = ...

5.3.1声明数组列表

一般,我们可以这样声明:

ArrayList<Employee> staff = new ArrayList<Employee>();

在Java10以后,我们可以使用var关键字避免反复编写类名:

var staff = new ArrayList<Employee>();

不过,如果我们在左边给出了类名,可以使用一种菱形语法进行简写:

ArrayList<Employee> staff = new ArrayList<>();

注意:

如果使用var关键字就不应该使用菱形语法

var elements = new ArrayList<>();

虽然这里代码语法上没有错误,但是本质会生成一个ArrayList<Object>

添加元素

可以使用add方法将元素添加到数组列表中

staff.add(new Employee(...));
staff.add(new Employee(....));
....

数组列表的内部管理着一个对象引用数组

当这个数组空间用尽时,我们再次调用add方法添加元素,数组列表就会自动的创建一个更大数组,并将所有对象引用从较小的数组拷贝到大的数组中


如果我们能知道或者估计出数组可能存储的元素数量,可以在填充数组之前调用ensureCapacity方法

staff.ensureCapacity(100);

这个方法调用会分配一个包含100个对象的内部数组

这样,前100次调用add方法不会带来开销很大的重新分配空间

可以直接把初始容量传递给ArrayList构造器

ArrayList<Employee> staff = new ArrayList<>(100);


size方法返回数组列表中包含的实际元素个数

等价于数组的a.length


如果我们确认数组列表的大小将保持恒定,不再变化,可以调用trimToSize方法

这个方法将内存块的大小调整为保存当前元素数量所需要的存储空间,垃圾回收器会回收多余的空间

注意:一定要确定数组列表大小真的不会发生变化才使用这个方法,否则重新移动内存块很耗时间

5.3.2 访问数组列表元素

Java中没有对ArrayList重载[]操作符

我们如果想访问ArrayList中的元素,只能使用get和set方法

例如,想设置第i个元素(ArrayList中仍然从0开始计数)

staff.set(i,emoloyee1);

不过,这里有一个与数组非常大的区别:

只有数组列表大小大于i时我们才能调用list.set(i,e);

例如:

var list = new ArrayList<Employee>(100);
list.set(0,x);

这是错误的用法

ArrayList中还没有任何元素!

不像数组我们声明好后直接每个元素都存在了

因此,要用add添加新元素而不是setset只能用于修改存在的元素

可以使用get读取元素

Employee e = staff.get(i);

等价于array[i]

没有泛型类时,原始ArrayLis的get会返回Obeject类型,此时我们需要强制类型转换

Employee e = (Employee) staff.get(i);

原始的ArrayList存在一定危险性:它的add和set方法接受任意类型的对象,如果传入类型不兼容的对象,编译器不会发出任何警告

可以使用for each循环遍历ArrayList元素

5.3.3类型化与原始数组列表的兼容性

有些时候,我们可能需要处遗留代码中没有类型参数的数组列表交互操作

public class EmployeeDB{
    ...
    public void update(ArrayList list){
        ...
    }
    public ArrayList find(String query){
        ...
    }
}

在这样的例子中,我们可以将类型化的数组列表传递给update方法,不需要任何强制类型转换

ArrayList<Employee> staff = ...;
employeeDB.update(staff);

但是,如果将一个原始的ArrayList赋值给一个类型化ArrayList时,会得到一个警告

ArrayList<Employee> result = employeeDB.find(query);

这个警告信息主要是指出类型转换可能不匹配

当然,如果我们确实要这样做,可以使用@SuppressWarnings("unchicked")注解来命令编译器忽视这条警告

5.4对象包装器与自动装箱

有时候,我们需要将基本数据类型变量如int等转换成对象,所有的基本类型都有一个与之对应的类。

Integer对应int,通常,这些类称为包装器

包装器的名字都非常地清晰:Integer、Long、Float、Double、Short、Byte、Character、Boolean

其中前六个类派生于公共超类Number

包装器类是不可变的

即:一旦构造了包装器,就不允许更改包装在其中地值,同时,包装器类还是final,因此不能派生它们的子类


当我们希望构造一个整型数组列表时,要注意尖括号中不允许使用基本类型,因此,必须这样声明:ArrayList<Integet> list = ...

不过,从效率角度来讲,由于每个值都分别包装了一个对象,所以ArrayList<Integer>的效率远低于int[]

因此,只有当程序员操作的方便性比执行效率更重要时,才考虑对较小的集合使用这种构造


在向ArrayList<Integet>调用add方法时,我们可以直接这样添加:

list.add(3);

这个调用会自动转换成list.add(Integer.valueOf(3));

这种转换称为自动装箱

反过来,将一个Integer对象赋给一个int变量时,将会自动拆箱,也就是说:

int n = list.get(i);

将会自动完成这样的转换:

int n = list.get(i).intValue();


自动装箱和自动拆箱可以适用于算术表达式

Integer n = 3; //自动装箱
n++; //拆箱 -> 运算  -> 装箱

这里实际上,编译器会首先自动插入指令将对象拆箱,然后将结果值增1,最后将其装箱

大多数情况下,包装器和基本数据类型很相似,但是它们之间有一个很大的不同: 同一性

理论上,我们可以使用==运算符用于包装器对象,不过,这里本质是检查对象是否有相同的内存位置

因此,如果我们这样比较:

Integer a = 1000;
Integer b = 1000;
if(a == b)....

很有可能得到false结果

注意:

  • 自动装箱规范要求boolean、byte、char(=<127)、介于-128和127之间的short和int包装到固定对象中
  • 因此,如果我们使用==比较这些范围的对象,它们的比较结果一定会成功
  • 但是,这样的不确认性完全不可靠,我们应当使用equals方法而不是==

绝不应该依赖包装器对象的同一性,不要使用==比较包装器对象,也不要将包装器对象作为锁

不应该使用包装器类的构造器,它们已经被舍弃,并将完全删除

应当使用静态方法Integer.valueOf()或者自动装箱


当我们在一个条件表达式混合使用Integer和Double类型时,Integer会自动拆箱然后类型转换成double,再装箱成Double

Integer n = 1;
Double x = 2.0;
System.out.prinln(true? n: x); //输出结果会变成1.0

注意:包装器类引用可以为null

因此自动装箱时可能抛出NullPointerException

Integer n = null;
System.out.println(2 * n);//throws NullPotinerException

自动装箱和拆箱是编译器的工作,而不是虚拟机。编译器生成类的字节码时会插入必要的方法调用,虚拟机只会执行这些字节码


包装器类中,封装了很多许多很好用的方法

例如,将一个数字字符串转换成数值类型:

int x = Integer.parseInt(str);

这里与Interger对象没有任何关系,parseInt是一个静态方法,但是在Integer中放置这个方法比较合适

其他数值类也实现了相应的方法

注意:

不能通过包装器类来实现能修改数值参数的方法

比如:

public static void swap(int x){
    x = 3 * x;
    
}

我们清楚的知道,这个方法本质上不能修改参数的值,但是,即使我们把int换成了Integer也不能实现这个效果,这是因为:Integer对象是不可变的

5.5 参数个数可变的方法

可以提供参数个数可变的方法,有时,这些方法被称为变参方法

一个典型的变参方法就是printf

System.out.printf("%d %s",n,"widgets");

System.out.printf("%d",n);

这里一次调用传递了三个参数一次传递了两个参数

printf方法是这样定义的

public class PrintStream{
    public PrintStream printf(String fmt,Object... args){
        return format(fmt,args);
    }
}

这里的省略号...是Java代码的一部分,它表明这个方法的这个参数可以接受任意数量的对象

可以这样理解,printf起始接收了两个参数:一个是格式字符串fmt,一个是Object[]数组

其中数组保存着所有其他参数,如果提供的是整型或其他基本类型,会自动装箱

之后,会扫描fmt字符串,将第i个格式说明符与args[i]的值匹配

换句话来说,一句System.out.prinf("%d %s",n,"widgets")

编译器会进行这样的调用:System.out.printf("%d %s",new Object[]{Integer.valueOf(n),"widgets"});


可以定义自己的变参方法,可以为参数指定任意类型,甚至是基本数据类型

public static double max(double... values){
    double max = Double.NEGATIVE_INFINITY;
    for(double v : values){
        if(v > max){
            max = v;
        }
    }
    return v;
}

这个方法可以比较一系列的double型值得最大值

相当于参数有一个double[]数组


允许将数组作为最后一个参数传递给有可变参数的方法

比如System.out.println("%d %s",new Object[] = {Integer.valueOf(n),"widgets"});

因此,如果以一个已有方法的最后一个参数是数组,我们就可以把它变成变参方法而不会破坏任何已有代码

甚至,我们可以这样声明main方法:public static void main(String... args){...}

5.6 抽象类

在一个继承层次中,一般来讲越往上层的类就更加具有一般性,更加的抽象

例如,一个Employee类和一个Student类,员工是人,学生也是人,我们提供一个更加抽象的概念Person类,让员工类和学生类都派生于Person,将name属性与getName这种共用的类特性都放到公共超类中

public class Person{
    private String name;
    public  String getName(){
        return this.name;
    }
}
public class Employee extends Person{
    ...
}
public class Student extends Person{
    ...
}

这样的好处是首先实现了代码复用,公共的方法或属性只需要编写一次,维护与阅读更加方便,当我们需要设计新的类时,也可以直接从高层的Person直接派生

其次,我们可以实现动态绑定或者说,多态

在代码中只需要使用Person p = new ...; p.getName()编译器与虚拟机会确保调用正确的方法

不过,考虑这么一个问题:

我们希望增加一个方法getDescription()这个方法用于描述一个对象的简要信息

理论上不管是学生还是员工,都应该有这个方法,这个方法应该放在超类Person中

可是,在超类中,这个方法应该加入什么信息呢?

每个子类都有可能有自己独特的属性,我们不可能在超类中硬编码输出哪些信息,容易想到的是我们只返回name属性,不过,更好的做法时:将这个方法声明为抽象的 abstract

public abstract String getDescription();

关键字abstract表明这个方法是抽象方法,抽象方法不允许有任何实现类容,只是表明这个地方有这个方法

包含一个或多个抽象方法的类本身也必须声明为抽象的:

public abstract class Person{
    ...
    public abstract String getDescription();
    ...
}

除了抽象方法之外,抽象类还可以包含字段和具体的方法,例如,Person类还保存着一个人的姓名和返回姓名的具体方法

public abstract class Person{
    private String name;
    public String getName(){
        return this.name;
    }
    public abstract String getDescription();
}

有些观点认为抽象类中不应该包含具体方法。但是,更合理的设计是将通用的字段和方法,不管是不是抽象的,都放在超类(不论超类抽象与否)中。


抽象方法相当于子类中实现的具体方法的占位符,当我们扩展一个抽象类时,有两种选择:

  • 在子类中保留抽象类中的部分或者全部抽象方法仍然不实现,这样子类仍然是抽象的
  • 在子类中实现所有抽象方法,这样子类就不再是抽象的了

当然,即使不含抽象方法,也可以将一个类声明为absstract

当一个类是抽象的,我们就不能对它实例化 Person p = new Person()是错误的用法

但是,我们可以用抽象类变量引用具体的子类对象:Person p = new Student(....);

当我们调用p.getDescription()就会动态绑定到具体实现的子类上


这也体现了抽象方法的意义,如果我们不在超类中声明抽象方法而直接在子类中实现具体方法,就没有办法用超类变量p调用任意实现的getDescription()

5.7 枚举类

一个典型的枚举例子:

public enum Size{
    SMALL,MEDIUM,LARGE,EXTRA_LARGE
}

实际上,枚举也是一种类,刚好有4个实例,不能再构造新的对象

枚举对象不需要使用equals,可以直接使用==比较

如果需要的话可以给枚举类型增加构造器、方法和字段

不过,构造器只是在定义枚举常量时使用,枚举的构造器默认是私有的,不能在外部调用:

public enum Size{
    SMALL("s"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
    private String abbreviation//缩写;
    Size(String abbreviation){
        this.abbreviation = abbreviation;
    }
    public String getAbbreviation(){
        return this.abbreviation;
    }
}

所有枚举都是抽象类Enum的子类,继承了一些比较有用的方法

比如,toString(),这个方法会返回枚举常量的字符串

Size.SMALL.toString()

返回字符串:“SMALL”

toString的逆方法是静态的方法valuesOf

Size s = Enum.valueOf(Size.class,"SMALL");

除此之外,每个枚举类型都有一个静态的values方法,它将返回一个包含枚举所有实例的数组:

Size[] values = Size.value();

ordinal方法会返回一个枚举常量在枚举种声明的位置,从0开始计数

5.8 密封类

在Java17中,引入了一种新的特性,称为密封类

密封类使用sealed关键字定义,并且需要搭配permits关键字,permits关键字指示了哪些类可以继承sealed密封类

基本语法格式:

public sealed class 父类名 permits 子类1,子类2,...{...}

继承密封类的子类,有以下三种情况:

  • final : 最终类,不能被再继承
  • sealed:继续作为密封类,限制进一步的继承
  • non-sealed:非密封类,允许其他任何类继承

例子:

public sealed class shape 
permits Circle,Rectangle,Triangle{
	abstract double area();    
}
//允许的子类:最终类
final class Circle extends Shape{
    private final double radius;
    Circle(double radius){
        this.radius = radius;
    }
    @Override
    double area(){
        return Math.PI * radius * radius;
    }
}
//允许的子类:继续作为密封类
sealed class Rectangle extends Shape permits Square{
    private final double width,height;
    Rectangle(double width,double height){
        this.width = width;
        this.height = height;
    }
    @Overrride
    double area(){
        return width * height;
    }
}
//允许的子类:非密封类,其他类可以自由继承,需要关键字non-sealed
non-sealed class Triangle extends Shape{
    private final double base,height;
    Triangle(double base,double height){
        this.base = base;
        this.height = height;
    }
}

注意:

密封类必须显式声明允许继承的子类

所有的子类都是finalsealednon-sealed之一

如果省略了petmits关键字,必须确保所有子类都在同一个文件中


密封类主要是为了防止某些类被任意继承,但我们又希望进行有限扩展的情景


比如:我们希望建立一个XML节点类:

public abstract sealed class Node permits Element,Text,Comment,CDATASection,EntityReference,ProcessingInstruction{
    ....
}

//其中,Elemnt节点我们希望它能任意的被扩展,因为这符合XML的规则
public non-sealed Element extends Node{
    ...
}
public class HTMLDivElement extends Element{
    .
}

5.9 反射

反射:能够分析类的能力称为反射

反射库java.lang.reflect提供了丰富且精巧的工具库,可以用来编写动态操作Java代码的程序。

Java可以支持用户界面生成器、对象关系映射器以及很多其他需要动态查询类能力的开发工具

利用反射机制,可以:

  • 在运行时分析类
  • 在运行时检查对象,例如,编写一个适用所有类的toString方法
  • 实现泛型数组操作代码
5.9.1Class类

在程序运行期间,Java运行时系统始终为所有对象维护着一个运行时类型标识,这个信息会跟踪每个对象所属的类,虚拟机利用运行时类型信息选择要执行的正确的方法

除此之外,还可以适用一个特殊的Java类访问运行时类的信息,这个类称为Class

Class类保存类运行时的所有信息,Object中的getClass就是返回一个Class对象

Class对象描述一个特定类的属性

最常用的Class类方法可能是getName,这个方法返回类的名字,例如:

System.out.println(e.getClass().getName())

输出e的类名

对应的,Class类有一个静态的forName方法用来根据代表类名的字符串获取一个Class对象

Class cl = Class.forName("java.util.Random");

这里要注意的是类名必须是完全限定名,而且这个类的class文件必须存储在你的类路径中,否则就无法获得它的Class对象

严格来说,Class对象描述的是一个类型,也就是可能描述的是类,也可能不是:

比如,int并不是对象类型,但是int.class确实是一个Class类型的对象

注意:

Class实际上是一个泛型类,例如Employee.class的类型实际上是Class<Employee>,不过,大多数实际应用中, 我们都可以直接使用Class的原始类型,忽略类型参数

由于历史原因,getName方法对数组类型返回的名字可能有些奇怪:

Double[].class.getName()返回[Ljava.lang.Double

int[].class.getName()返回[I


虚拟机为每个类型管理着唯一的Class对象,所以,可以用==运算符比较两个Class对象,比如

if(e.getClass() == Employee.class)...能够准确判断e是否是Employee类的实例

如果有一个Class类型对象,可以利用它构造类的实例,调用getConstructor方法将得到一个Constructor类型对象,然后使用newInstance方法来构造一个实例:

var className = "java.uti.Random"; //当然也可以是任意的类完全限定名
Class cl = Class.forName(className);
Object obj = cl.getConstructor().newInstance();

这里会使用cl描述的类的无参数方法,如果没有无参数构造器,会抛出InvocationTargetException

同时,上文提到的Class.forName也有可能抛出ReflectiveOperationException异常,这是一个检查型异常,我们必须要对它进行处理,最简单的处理策略就是我们直接将这个异常抛出:

public static void doSomtingWithClass(String name)
    throws ReflectiveOperationException
{
	Class cl = Clss.forName(name);
    ...
}

调用这个方法的任何地方也都需要提供一个throws声明,包括main方法,如果一个异常确实出现,并且我们不捕获,那么main方法将会终止,同时提供一个轨迹栈

5.9.2利用反射分析类的能力

java.lang.reflect包中,有三个类Field、Method和Constructor,分别用于描述类的字段、方法和构造器

三个类都有一个getName方法,返回字段、方法或构造器的名字;

Field有一个getType()方法,返回一个描述字段类型的对象,这个对象同样也是Class类型对象;

Method和Constructor都有报告参数类型的方法,Method还有一个报告返回类型的方法。

这三个类都有一个getModifiers的方法,这个方法会返回一个整数,这个整数用不同的0/1位描述所使用的修饰符,如public 或static 等

同时,对应的,可以使用java.util.reflect包中Modifiers类的静态方法类分析这个整数,得到对应的修饰符

例如,可以使用isPublic、isPrivate等方法来判断一个方法构造器或字段是否为public等


Class类中的getFields、getMethods、getConstructors方法将分别返回这个类支持的公共字段、方法和构造器,其中包括超类中公共的成员;

而getDeclaredFields、getDeclaredMethods、getDeclaredConstructors将返回这个类中所有的字段、方法、构造器组成的成员,甚至包括私有成员。


利用这些方法,我们可以编写一个程序,来输出一个类的所有信息:

public class ReflectionTest {
    

    public static void main(String[] args)
            throws ReflectiveOperationException
    {
        String name;
        //从执行名名读取参数
        if(args.length > 0){
            name = args[0];
        }
        else{
            //读取用户输入作为要展现的类
            var in = new Scanner(System.in);
            System.out.println("请输入要展示的类的完全限定名: ");
            name = in.next();
        }

        //按照:访问修饰符 类名 超类名(非Object类)的格式输出类名
        Class cl = Class.forName(name);
        String modifiers = Modifier.toString(cl.getModifiers());
        if(modifiers.length() > 0){
            System.out.print(modifiers+" ");
        }
        //打印sealed关键字
        if(cl.isSealed()){
            System.out.print("sealed");
        }
        //打印枚举关键字
        if(cl.isEnum()){
            System.out.print("enum" + name);
        }
        //打印记录关键字
        else if(cl.isRecord()){
            System.out.print("record" + name);
        }
        //打印接口关键字
        else if(cl.isInterface()){
            System.out.print("interface" + name);
        }
        else {
            System.out.print("class" + name);
        }

        Class superclass = cl.getSuperclass();
        if(superclass != null && superclass != Object.class){
            System.out.print(" extends "+superclass.getName());
        }
        //如果有接口,打印接口信息
        printInterfaces(cl);
        //如果是密封类,显示允许的子类
        printPermittedSubclasses(cl);

        //完成类名一行的编写,进入下一行
        System.out.print("\n{\n");
        //打印构造器信息
        printConstructors(cl);
        System.out.println();
        //打印方法信息
        printMethods(cl);
        System.out.println();
        //打印字段信息
        printFields(cl);
        System.out.println("}");

    }

    private static void printFields(Class cl) {
        Field[] fields = cl.getDeclaredFields();

        for(Field f : fields){
            //描述这个字段的类型的Class对象
            Class type = f.getType();
            String fieldName = f.getName();
            System.out.print("  ");
            //打印字段的修饰符
            String modifiers = Modifier.toString(f.getModifiers());
            if(modifiers.length()>0){
                System.out.print(modifiers+" ");
            }
            System.out.println(type.getName()+" "+fieldName+";");

        }



    }

    private static void printMethods(Class cl) {
        Method[] methods = cl.getDeclaredMethods();

        for(Method m : methods){
            //获得返回值类型
            Class retType = m.getReturnType();
            String methodName = m.getName();

            System.out.print("  ");
            //输出修饰符
            String modifiers = Modifier.toString(m.getModifiers());
            if(modifiers.length()>0){
                System.out.print(modifiers+" ");
            }
            System.out.print(retType.getName()+methodName+"(");
            //输出参数列表
            Class[] paramTypes = m.getParameterTypes();
            for(int j = 0; j< paramTypes.length;j++){
                if(j>0){
                    System.out.print(", ");
                }
                System.out.print(paramTypes[j].getName());
            }
            System.out.println(");");
        }

    }

    private static void printConstructors(Class cl) {
        //接收描述所有构造器的数组
        Constructor[] constructors = cl.getDeclaredConstructors();

        for (Constructor c : constructors) {
            String constructorName = c.getName();
            //输出两个空格的缩进
            System.out.print("  ");
            //getModifiers返回一个整数,描述这个构造器的所有修饰符
            String modifiers = Modifier.toString(c.getModifiers());
            if (modifiers.length() > 0) {
                System.out.print(modifiers + " ");
            }
            System.out.print(constructorName+"(");

            //输出参数列表
            //getParameterTypes()描述参数类型
            Class[] paramTypes = c.getParameterTypes();
            for (int j = 0; j < paramTypes.length; j++) {
                if (j > 0) {
                    System.out.print(",");
                }
                System.out.print(paramTypes[j].getName());
            }
            //完成一个构造器的输出,进行换行
            System.out.println(");");

        }
    }

    private static void printPermittedSubclasses(Class cl) {
        if (cl.isSealed()) {
            Class<?>[] permittedSubclasses = cl.getPermittedSubclasses();
            for (int i = 0; i < permittedSubclasses.length; i++) {
                if (i == 0) {
                    System.out.print(" permits ");
                } else {
                    System.out.print(", ");
                }
                System.out.print(permittedSubclasses[i].getName());
            }
        }
    }

    private static void printInterfaces(Class cl) {
        Class<?>[] interfaces = cl.getInterfaces();
        for (int i = 0; i < interfaces.length; i++) {
            if (i == 0) {
                /*这里,如果cl描述的类型是接口,就只能继承extends其他的接口
                 * 否则,是一个普通的类,才实现implements接口*/
                System.out.print(cl.isInterface() ? " extends " : " implements ");
            } else {
                System.out.print(", ");
            }
            System.out.print(interfaces[i].getName());
        }
    }
}

实践部分——模拟银行系统

体设计与系统架构

根据分层模式,将整个系统分为四层:

  • 表示层 Presentation Layer
    • 职责:负责用户界面(UI)的展示和交互逻辑
    • 组件:
      • LoginFrame:登录界面,包含账户、密码输入框和登录按钮
      • mainFrame:主界面,展示余额、存取款输入框以及交互按钮
      • AppLauncher:整个程序的启动入口,初始化各层组件,打开登录界面
    • 关键逻辑:
      • 登录按钮触发验证操作,调用业务逻辑层的服务
      • 存取金额触发余额更新,调用业务逻辑层的方法
  • 业务逻辑层 Bussiness Logic Layer
    • 职责:协调调度各个层之间的关系
    • 组件:
      • AuthService:处理用户登录验证逻辑
      • BankService:处理存款、取款操作,展示余额
    • 关键逻辑:
      • 登陆时调度领域模型层验证用户登录
      • 存取款时调度领域模型层进行存取款、余额更新
  • 数据访问层 Data Access Layer
    • 职责:数据的持久化(存储和读取)
    • 组件:
      • AccountDAO:数据访问的接口,让上层(业务逻辑层)只依赖于抽象接口,不去接触数据访问的具体实现
      • FileAccessDAO:文件存储数据的具体实现
    • 关键逻辑:
      • 利用文件系统实现对数据的持久化
      • 与高层之间只通过接口交互,实现数据访问层与业务逻辑层的解耦
  • 领域模型层 Domain Model Layer
    • 职责:封装核心实体业务,统一数据结构和行为,集中管理规则,体现数据的真正概念
    • 组件:
      • Account:账户类实体,包含字段:accountNumberaccountPasswordbalance
    • 关键逻辑:
      • 业务逻辑层提供存取款服务时,调用领域模型的公共方法,实现真正的改变数据

请添加图片描述

2.各层之间交互流程

在这里插入图片描述

登录时:

表示层的LoginFream收集用户的输入

===>authService字段会接收用户输入作为参数调用Login()方法

===>Login()方法中,根据AuthServiceAccountDAO动态绑定FileAccountDAO

==>FileAccountDAO从文件读取数据

===>利用Account对象的公共方法实现账户与密码的匹配检查

===>返回结果

这里还可以从程序运行的角度思考:

public class AppLauncher {
    public static void main(String[] args) {
        // 1. 搭建生产线:初始化各层组件
        // 数据访问层(工人)
        AccountDAO accountDAO = new FileAccountDAO();
        
        // 业务逻辑层(车间主任)
        AuthService authService = new AuthService(accountDAO);
        BankService bankService = new BankService(accountDAO);
        
        // 2. 启动生产线:打开登录界面(用户入口)
        SwingUtilities.invokeLater(() -> {
            LoginFrame loginFrame = new LoginFrame(authService, bankService);
            loginFrame.setVisible(true);
        });
    }
}

存钱、取钱时:

MainFream读取到用户的选择与输入的具体金额,

同时,mainFream收到LoginFream从数据访问层查询到的Account对象

===>调用bankService.deposit(account,amous)

===>BankService调用account.deposit(amous)修改金额

===>BankService调用数据接口的updateAccount(account)跟新文件中的数据

在这里插入图片描述

3.关键代码实现

表示层

每个表示页面的类LoginFrameMainFrameRegisterFrame只依赖对应的服务类的字段,不与数据访问层有直接联系

其中,LoginFrame作为启动界面代表所有界面接收服务对象的注入,再按照程序工作流程对服务对象进行分发

LogFrame

在这里插入图片描述

这里,main方法中完成对业务逻辑层初始化后,会直接将所有服务逻辑对象交给Login界面,再由LoginFrame进行分发

在这里插入图片描述

登录界面主要实现的功能除了接收用户的输入以外,最关键的通过按钮实现与用户的交互

在这里插入图片描述

在这里插入图片描述

  • 这里,关键在于根据分层架构思想,在表示层不会处理任何业务逻辑的实现。
  • 表示层接到用户的输入以后,一切的业务实现都委托给了AuthService对象,即if(authService.login(accountNumber,password)){...}
  • 根据注册的结果:
    • 如果成功注册,按照业务需求,需要进入主界面,因此创建MainFrame对象,将main方法中注入的对应主页面业务实现服务的BankService对象传递给主页面——也是OOP思想的核心体现,每个页面都有逻辑上对应的类来处理。并且用Account对象来组织数据而不是直接传递数据的原始数据类型String
    • 如果注册失败,根据业务层传递出来的异常信息,显示提示

在这里插入图片描述

  • 注册按钮对应的监听器也是大同小异的思路
  • 不过有一点不同的是,按照我的想法,当注册成功以后,需要回到登录界面,因此,这里不直接使用this.dispose()释放当前登陆界面的资源,只是将其设置为不可见,并且将对本页面的引用传递给RegisterFrame,方便回到当前界面
MainFrame

在这里插入图片描述

MainFrame中除了用于对接业务逻辑层的bankService字段以外,由于该界面需要显示用户当前余额,需要访问到用户数据,因此引入了领域模型curentAccount,从抽象的逻辑上讲,这也代表了当前主页面的唯一用户。

在这里插入图片描述

这里,实现对金额的格式控制主要使用了String.format()方法,这个方法有点类似于printf函数,只是会生成一个格式控制的字符串并返回它的值而不是输出到控制台上

在这里插入图片描述

不过,更值得一体的是,这个方法是动态更新余额,会部署到监听器中,而不是放到构造器中作为UI部署的一部分,真正实现一进入主页面就在JLabel中显示余额是调用curentAccount.getBalance()方法:

在这里插入图片描述

这也反应了领域模型层在整个分层架构中的作用:它不会严格的部署在哪一层,而是作为业务实体,灵活的出现在不同的层次中需要数据交互的地方,同时完成对真正的业务逻辑的封装。


在这里插入图片描述

这里响应函数中使用了try_catch来包围业务逻辑的实现,主要是因为取钱产生失败的可能性比较多,单纯的使用boolean型作为返回值无法对应所有错误情况,因此在业务实现中我使用throws抛出异常和异常情况的具体说明,然后在显示层使用catch捕获异常信息

RegisterFrame

在这里插入图片描述

注册界面RegisterFrame实现思路上与前两个界面没有太大的区别,唯一值得注意的是,当我们完成注册以后,需要回到LoginFrame界面,因此,需要在回调函数中使用LoginFrame传递进来的对登陆界面的引用,将它重新设为可见:

在这里插入图片描述

AppLancher

在这里插入图片描述

其实严格来讲,AppLancher只是程序的入口main方法的容器,对用户本身是不可见的,似乎放到表示层不是很标准。不过我感觉影响不大,不会影响到整个程序的部署,因此就放在了表示层。

在主函数中,主要需要按照程序工作流程,依次完成对各层的初始化,并将数据访问层的具体实现对象注入到业务逻辑层中

最后打开启动界面——LoginFrame

业务逻辑层

AuthService

这里主要实现针对登录的逻辑实现。

在这里插入图片描述

根据分层架构设计思想,业务逻辑层不会与数据访问层的具体实现对接,而是依赖数据访问的标准接口AccountDAO,因此这里也是持有的接口对象字段,在程序运行过程中,通过动态绑定对接数据访问层,这样设计的最大好处是高层不会关心底层的实现,可以非常方便的切换到数据库系统等,而不会影响到整个程序的其他代码

在这里插入图片描述

按照分层架构规范,理论上真正的业务实现都应该封装到领域模型Account类中

但是我考虑到登录过程只会涉及对数据的查询(读取),不会涉及到修改数据,而且查询数据的话势必需要依赖数据访问层(无论是接口还是具体实现),这样又不太符合领域模型层的设计概念,因此把这部分逻辑直接放到了业务逻辑层。

BankService

在这里插入图片描述

对于存钱、取钱逻辑实现的BankServiceAuthService不同,涉及的所有业务都要修改数据,因此,我将这部分逻辑都完全封装到了Account类中,整个BankService其实只起到了对于各层之间调度的作用

在这里插入图片描述

在这里插入图片描述

同时,正如前文所述,由于取钱的逻辑实现中,可能出现的异常情况比较多,需要使用具体的文字信息才能表达清楚,因此在这里作为过度层,我又重新使用了一次try-catch,并在catch中补充对应的异常,方便表示层捕获处理

在这里插入图片描述

RegisterService

在这里插入图片描述

注册实现的业务逻辑层基本没有太多区别。

不过,为了避免用户注册相同的账号,我在调用真正的注册逻辑之前,首先进行了一次检索判断

这里要注意的是,不能直接写成:

if(accountDAO.findAccountById(accountNumber).getAccountNumber() == accountNumber)....

的形式。

因为用户这里是进行注册,正常情况下传入的accountNumber都不会在文件中存在

因此findAccountById会返回null

如果直接这样调用getAccountNumber()就会产生异常

因此只能这样进行对findAccountById(accountNumber)的是否为null的判断

数据访问层

数据访问接口——AccountDAO

在这里插入图片描述

数据访问层的核心是接口AccountDAO

在接口中,约束了:

  • 数据读取标准方法 findAccountById
  • 数据改写标准方法updateAccount

高层的业务逻辑层不会直接的接触底层数据访问层的具体实现,正是由于接口的存在,在其他层中,完全不需要关心底层数据持久化的具体实现方案,体现了抽象思想,也非常有利于程序的分层开发和功能扩展

FileAccountDAO

在这里插入图片描述

本次银行模拟系统,我选择了文件系统实现数据持久化

在文本文件中,按照账号,密码,余额的格式存储每一条记录

文件系统的关键问题在于:

  • 由于本次项目我选择使用Maven目录结构,理论上文件资源应该在src/main/resources子目录下
  • 在程序中使用相对路径/accounts.txt会自动对应到这个路径下的文件
  • 但是Maven会自动复制文件到target/classes目录下,并且会将程序输出结果只放到这个目录下的文件而不会输出到原始的src/main/resources/accounts.txt
  • 可是某些情况下,整个target/目录中,所有文件都会被视作只读的
  • 因此如果文件路径只用“/accounts.txt”表示,就会有以下效果:
    • 程序读取位于src/main/reources/的文件accounts.txt
    • 正常执行写入功能updateAccount
    • 由于targer/目录是只读的,因此实际无法写入
    • 最终,虽然程序正常运行了,但根本没有把任何数据存储到文件accounts.txt

因此,为了解决这个问题,我只好在项目根目录下面再重新建一个可读写目录appdata,并实际使用这个目录下的文件而不是Maven标准的src/maim/resource目录

在这里插入图片描述

为了实现这个功能,代码中增加了很多额外的读取文件的步骤:

在这里插入图片描述

由此也暴露了我的问题,完全不熟悉该如何管理,或者说构建项目的目录结构,产生了很多意料之外的问题。

领域模型层

根据分层架构思想,领域模型层是对真正的业务逻辑的封装,换句话说,这里是真正意义上实现我们程序需要的功能的地方。

这个层中的领域模型,和数据持久化的实现方式、界面的显示都完全没有关系

所有的业务逻辑真正实现都集中在领域模型中,也非常方便代码的维护与调试,不需要去其他层各处找业务逻辑运行可能出现的问题。

是整个分层架构的核心。

在这里插入图片描述

不过,由于本次系统要求的功能并不复杂,领域模型中不存在非常困难的逻辑实现。

但是要注意的是,根据封装思想,领域模型的构造器是私有的private

领域模型只会提供静态的creatAccount()方法供其他层使用时调用

这样能够更加严格的保护整个程序运转的核心逻辑

不会让其他层创建出不受领域模型控制的非法对象

完整代码如下:

package com.guoxinan.bank.ui;

import com.guoxinan.bank.dao.AccountDAO;
import com.guoxinan.bank.dao.FileAccountDAO;
import com.guoxinan.bank.service.AuthService;
import com.guoxinan.bank.service.BankService;
import com.guoxinan.bank.service.RegisterService;

import javax.swing.*;

public class AppLauncher {
    public static void main(String[] args) {
        //初始化数据访问层,选择文件系统实现
        AccountDAO accountDAO = new FileAccountDAO();

        //初始化业务逻辑层
        AuthService authService = new AuthService(accountDAO);
        BankService bankService = new BankService(accountDAO);
        RegisterService registerService = new RegisterService(accountDAO);

        //线程安全的打开登录界面
        SwingUtilities.invokeLater(() ->{
            LoginFrame loginFrame = new LoginFrame(authService, bankService, registerService);
            loginFrame.setVisible(true);
        });

    }
}

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class LoginFrame extends javax.swing.JFrame {
    private AuthService authService;
   // private AccountDAO accountDAO;
    private BankService bankService;
    private RegisterService registerService;

    private JTextField accountField;
    private JPasswordField passwordField;
    private JButton loginButton;

    private JButton registerButton;

    public LoginFrame(AuthService authService, BankService bankService, RegisterService registerService) {
        this.authService = authService;
        this.bankService = bankService;
        this.registerService = registerService;

        layoutLoginFrame();

    }

    private void layoutLoginFrame() {
        setTitle("登录界面");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  //设置用户在启动关闭时结束程序
        setSize(300, 150);//设置组件的宽和高
        setLocationRelativeTo(null);  //位于屏幕中央

        // 创建面板并使用 GridBagLayout
        JPanel panel = new JPanel();//一个轻量级容器
        panel.setLayout(new GridBagLayout());//水平垂直对齐不要求大小的布局管理器
        GridBagConstraints gbc = new GridBagConstraints();//与GridBagLayout关联的约束对象
        gbc.insets = new Insets(5, 5, 5, 5);//组件和边缘的距离
        gbc.fill = GridBagConstraints.HORIZONTAL;//调整组件,水平填充,不改变高度

        // 添加账号字段
        gbc.gridx = 0;
        gbc.gridy = 0;
        panel.add(new JLabel("账户:"), gbc);

        gbc.gridx = 1;
        accountField = new JTextField(15);
        panel.add(accountField, gbc);

        // 添加密码字段
        gbc.gridx = 0;
        gbc.gridy = 1;
        panel.add(new JLabel("密码:"), gbc);

        gbc.gridx = 1;
        passwordField = new JPasswordField(15);
        panel.add(passwordField, gbc);

        // 添加登录按钮
        gbc.gridx = 1;
        gbc.gridy = 2;
        loginButton = new JButton("登录");
        panel.add(loginButton, gbc);

        gbc.gridx = 2;
        registerButton = new JButton("注册");
        panel.add(registerButton, gbc);

        //添加监听器
        loginButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e){
                onLoginButtonClick();
            }
        });
        registerButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e){
                onRegisterButton();
            }
        });


        // 将面板添加到窗口
        add(panel);
    }

    private void onRegisterButton() {
        System.out.println("注册");
        RegisterFrame registerFrame = new RegisterFrame(registerService,this);
        registerFrame.setVisible(true);
        //隐藏当前界面
        this.setVisible(false);
    }

    /**
     * 登录按钮回调函数
     */
    public void onLoginButtonClick(){
        String accountNumber = accountField.getText();
        String password = new String(passwordField.getPassword());
        if(authService.login(accountNumber, password)){
            //登录成功,打开主界面,ui层不应该直接接触数据访问层,而是通过业务逻辑层调用
            Account account = authService.getAccountDAO().findAccountById(accountNumber);
            new MainFrame(bankService,account).setVisible(true);//显示mainFrame
            this.dispose();//释放loginFrame的所有组件
        }else{
            JOptionPane.showMessageDialog(this, "账户与密码不匹配!");
        }

    }
}

package com.guoxinan.bank.ui;

import com.guoxinan.bank.domain.Account;
import com.guoxinan.bank.domain.State;
import com.guoxinan.bank.service.BankService;

import javax.swing.*;
import java.awt.*;
import java.math.BigDecimal;

public class MainFrame extends javax.swing.JFrame {
    private BankService bankService;
    private Account curentAccount;

    private JLabel balanceLabel;
    private JTextField amountField;
    private JRadioButton depositButton, withdrawButton;
    private JButton confirmButton;


    public MainFrame(BankService bankService, Account account) {
        this.bankService = bankService;
        this.curentAccount = account;

        layoutMainFrame();



    }
    public MainFrame() {
        this.curentAccount = Account.createAccount("test", "test");
        layoutMainFrame();
    }

    private void layoutMainFrame() {
        // 设置窗口属性
        setTitle("银行账户管理");
        setSize(400, 300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setLayout(new GridBagLayout());

        // 初始化组件
        balanceLabel = new JLabel("当前余额: ¥" + curentAccount.getBalance() );
        amountField = new JTextField(15);
        depositButton = new JRadioButton("存款");
        withdrawButton = new JRadioButton("取款");
        confirmButton = new JButton("确认");

        // 组单选按钮,确保只有一个选项可选
        ButtonGroup group = new ButtonGroup();
        group.add(depositButton);
        group.add(withdrawButton);
        depositButton.setSelected(true); // 默认选中存款

        // 添加组件到界面
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(10, 10, 10, 10); // 组件间距

        // 余额显示(第一行)
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.gridwidth = 2; // 占据两列
        gbc.anchor = GridBagConstraints.CENTER;
        add(balanceLabel, gbc);

        // 金额输入框(第二行)
        gbc.gridx = 0;
        gbc.gridy = 1;
        gbc.gridwidth = 2;
        add(amountField, gbc);

        // 单选按钮组(第三行)
        gbc.gridx = 0;
        gbc.gridy = 2;
        gbc.gridwidth = 1;
        add(depositButton, gbc);

        gbc.gridx = 1;
        add(withdrawButton, gbc);

        // 确认按钮(第四行)
        gbc.gridx = 0;
        gbc.gridy = 3;
        gbc.gridwidth = 2;
        confirmButton.addActionListener(e -> onConfirmButtonClick());
        add(confirmButton, gbc);
    }

    /**
     * 确认按钮的响应方法
     */
    public void onConfirmButtonClick(){
        double amount = Double.parseDouble(amountField.getText());
        //用户选择存钱
        if(depositButton.isSelected()){
            try {
                bankService.deposit(curentAccount,amount);
            } catch (Exception e) {
                JOptionPane.showMessageDialog(this, "金额必须为正");
            }
        }
        //用户选择取钱
        else if(withdrawButton.isSelected()){
            boolean success = bankService.withdraw(curentAccount,amount);
            if(!success){
                JOptionPane.showMessageDialog(this,"余额不足");
            }
        }
        updateBalanceDisplay();
    }

    /**
     * 更新余额显示
     */
    private void updateBalanceDisplay() {
        balanceLabel.setText("¥" + String.format("%.2f", curentAccount.getBalance()));
    }

    public static void main(String[] args) {
        MainFrame ui = new MainFrame();
        ui.setVisible(true);
    }

}

package com.guoxinan.bank.ui;

import com.guoxinan.bank.service.RegisterService;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class RegisterFrame extends JFrame {
    private RegisterService registerService;
    private LoginFrame loginFrame; //持有登录界面引用,方便注册成功后跳转回去

    private JLabel accountNumberHintLabel;
    private JTextField accountNumberField;
    private JLabel passwordHintLabel;
    private JPasswordField passwordField;
    private JButton registerButton;


    public RegisterFrame(RegisterService registerService, LoginFrame loginFrame) {
        this.registerService = registerService;
        this.loginFrame = loginFrame;

        layoutRegisterFrame();
    }

    /**
     * 实现对注册页面的布局
     */
    private void layoutRegisterFrame() {
        setTitle("注册页面");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setBounds(100, 100, 450, 300);
        setLocationRelativeTo(null);

        JPanel panel = new JPanel();
        panel.setLayout(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(5, 10, 5, 10); // 组件边距(上、左、下、右)
        gbc.fill = GridBagConstraints.HORIZONTAL;

        // 用户名标签
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.WEST;
        panel.add(new JLabel("用户名:"), gbc);

        // 用户名输入框
        accountNumberField = new JTextField(20);
        gbc.gridx = 1;
        gbc.gridy = 0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.weightx = 1; // 允许横向拉伸
        panel.add(accountNumberField, gbc);

        // 用户名格式提示
        accountNumberHintLabel = new JLabel("格式要求:8位数字");
        accountNumberHintLabel.setForeground(Color.GRAY);
        accountNumberHintLabel.setFont(new Font("宋体", Font.PLAIN, 10));
        gbc.gridx = 0;
        gbc.gridy = 1;
        gbc.gridwidth = 2; // 跨两列
        gbc.anchor = GridBagConstraints.WEST;
        panel.add(accountNumberHintLabel, gbc);

        // 密码标签
        gbc.gridx = 0;
        gbc.gridy = 2;
        gbc.gridwidth = 1; // 重置跨列
        panel.add(new JLabel("密码:"), gbc);

        // 密码输入框
        passwordField = new JPasswordField(20);
        gbc.gridx = 1;
        gbc.gridy = 2;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        panel.add(passwordField, gbc);

        // 密码格式提示
        passwordHintLabel = new JLabel("格式要求:8位以上,含大小写和数字");
        passwordHintLabel.setForeground(Color.GRAY);
        passwordHintLabel.setFont(new Font("宋体", Font.PLAIN, 10));
        gbc.gridx = 0;
        gbc.gridy = 3;
        gbc.gridwidth = 2;
        panel.add(passwordHintLabel, gbc);

        // 注册按钮
        registerButton = new JButton("注册");
        gbc.gridx = 0;
        gbc.gridy = 4;
        gbc.gridwidth = 2;
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.fill = GridBagConstraints.NONE; // 不拉伸按钮
        gbc.insets = new Insets(20, 0, 10, 0); // 增加上下边距
        registerButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                onRegisterButtonClicked();
            }
        });
        panel.add(registerButton, gbc);

        add(panel);

    }

    public void onRegisterButtonClicked() {
        String accountNumber = accountNumberField.getText();
        String password = new String(passwordField.getPassword());
        try{
            //调用注册逻辑
            boolean success = registerService.register(accountNumber, password);
            if (success) {
                //处理成功情况
                JOptionPane.showMessageDialog(this,"注册成功!");
                this.dispose(); //关闭注册页面回到登录界面
                loginFrame.setVisible(true);
            }else {
                //JOptionPane.showMessageDialog(this,"注册失败:"+e.getMessage());
            }
        }catch (Exception e){
            JOptionPane.showMessageDialog(this,"注册失败:"+e.getMessage());
        }

    }
}

package com.guoxinan.bank.service;

import com.guoxinan.bank.dao.AccountDAO;
import com.guoxinan.bank.domain.Account;
import com.guoxinan.bank.domain.State;

public class AuthService {
    private AccountDAO accountDAO;

    public AuthService(AccountDAO accountDAO) {
        this.accountDAO = accountDAO;
    }

    public AccountDAO getAccountDAO() {
        return accountDAO;
    }

    /**
     * 调用DAO接口,验证登录是否正确
     * @param accountNumber 用户输入的账号
     * @param password 用户输入的密码
     * @return 账号与密码匹配,则返回true,否则返回false
     */
    public boolean login(String accountNumber, String password) {
        Account account = accountDAO.findAccountById(accountNumber);
        System.out.println(account);
        return account != null && account.getPassword().equals(password);
    }





}

package com.guoxinan.bank.service;

import com.guoxinan.bank.dao.AccountDAO;
import com.guoxinan.bank.domain.Account;
import com.guoxinan.bank.domain.State;

import java.math.BigDecimal;

public class BankService {
    private AccountDAO accountDAO;


    public BankService(AccountDAO accountDAO) {
        this.accountDAO = accountDAO;
    }

    /**
     * 调度领域模型,封装取钱逻辑
     * @param amount 用户要存入的金额
     * @param account 要存钱的实体
     */
    public boolean withdraw(Account account, double amount) {
        try {
            account.withdraw(amount);
            accountDAO.updateAccount(account);
            return true;
        }catch (IllegalArgumentException | IllegalStateException e) {
            System.err.println("操作失败"+e.getMessage());
            return false;
        }


    }

    /**
     * 调度领域模型,完成存钱逻辑
     * @param account 要存钱的账户实体
     * @param amount 要存的金额
     */
    public void deposit(Account account, double amount) {
        account.deposit(amount);
        accountDAO.updateAccount(account);
    }


}
package com.guoxinan.bank.service;

import com.guoxinan.bank.dao.AccountDAO;
import com.guoxinan.bank.dao.FileAccountDAO;
import com.guoxinan.bank.domain.Account;
import com.guoxinan.bank.domain.State;

public class RegisterService {

    private AccountDAO accountDAO;

    public RegisterService(AccountDAO accountDAO) {
        this.accountDAO = accountDAO;
    }

    /**
     * 调度DAO,完成注册逻辑
     * @param accountNumber
     * @param password
     * @return
     */
    public boolean register(String accountNumber, String password) {
        //判断如果账号已存在,则不许注册
        Account existingAccount = accountDAO.findAccountById(accountNumber);
        if (existingAccount != null) {
            throw new IllegalArgumentException("账号已存在");
        }

        try {
            Account account = Account.createAccount(accountNumber, password);
            accountDAO.updateAccount(account);
            System.out.println("Account " + account.getAccountNumber() + "成功注册");
            return true;
        } catch (IllegalArgumentException | IllegalStateException e) {
            System.err.println("操作失败格式不正确:" + e.getMessage());
            throw new IllegalArgumentException(e.getMessage());
            //return false;
        }

    }

    public static void main(String[] args) {
        RegisterService registerService = new RegisterService(new FileAccountDAO());
        String testAccountNumber = "20250002";
        String testPassword = "Hexuanyu1111";
        if ( registerService.register(testAccountNumber, testPassword)) {
           System.out.println("成功");
        }else {
            System.out.println("失败");
        }
    }
}

package com.guoxinan.bank.dao;

import com.guoxinan.bank.domain.Account;

/**
 * 表示对用户数据处理的接口,规定了对数据进行访问的两个标准方法
 * 业务逻辑层只依赖接口,而不会去依赖DAO层的具体实现
 */
public interface AccountDAO {
    /**
     * 通过账户号返回一个Account对象
     * @param accountNumber 要查询到Account对象的accountNumber
     * @return 对应的Account对象
     */
    Account findAccountById(String accountNumber);

    /**
     * 完成对账户的更新
     * @param account 要更新的账户
     */
    void updateAccount(Account account);

}

package com.guoxinan.bank.dao;

import com.guoxinan.bank.domain.Account;

import java.io.*;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Objects;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

/**
 * 文件实现类,实现对文件中的数据的访问
 */
public class FileAccountDAO implements AccountDAO {
    //Maven将src/main/resources的文件复制到target/classes中
    private static final String RESOURCE_FILE = "/accounts.txt";//资源文件路径
    private static final String PROJECT_ROOT = "D:/workspace/code/javase/bank_system"; // 项目根目录
    private static final String APPDATA_DIR = PROJECT_ROOT + "/appdata"; // 存储数据的目录
    private static final String STORAGE_FILE = APPDATA_DIR + "/accounts.txt"; // 存储数据的文件路径


    /***
     * 键值对,key = accountNumber,value = account对象
     * 用来将文件中的数据存储到内存中
     */
    private HashMap<String, Account> accounts = new HashMap<>();
    //记录更新的记录
    private HashMap<String,Account> updatedAccounts = new HashMap<>();

    public FileAccountDAO() {
        loadFromFile();//从文件中加载数据
    }

    private void loadFromFile() {
       //确保appdata目录存在
       File directory = new File(APPDATA_DIR);
       if (!directory.exists()) {
           directory.mkdirs(); //如果不存在,创建个目录
       }

       //确保数据文件存在,如果不存在,从资源文件复制
        File file = new File(STORAGE_FILE);
       if (!file.exists()) {
           //打开资源文件流
           try(InputStream resourceStream = getClass().getResourceAsStream(RESOURCE_FILE) )
           {
                if (resourceStream == null) {
                    throw new FileNotFoundException("没有找到资源文件:"+RESOURCE_FILE);
                }
                Files.copy(resourceStream, Path.of(STORAGE_FILE), StandardCopyOption.REPLACE_EXISTING);
           }
           catch (IOException e){
               e.printStackTrace();
           }
       }

        try (BufferedReader reader = new BufferedReader(
                //从可读写位置的资源文件读取数据
                new FileReader(STORAGE_FILE))) {
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim(); // 去除前后空格
                if (line.isEmpty()) continue; // 跳过空行

                //从文件中读取数据,构造新的账户
                String[] parts = line.split(",");
                if (parts.length != 3) {
                    System.err.println("数据格式错误,跳过该行: " + line);
                    continue;
                }

                try {
                    /*
用来暂存文件中的一条账户信息
                    Account acc = new Account(
                            parts[0],//账户号
                            parts[1],//账户密码
                            BigDecimal.valueOf(Double.parseDouble(parts[2]))//账户余额
                    );
*/
                    Account acc = Account.createAccount(parts[0],parts[1],Double.parseDouble(parts[2]));
                    //System.out.println(acc);

                    //将账户号与账户组合
                    accounts.put(acc.getAccountNumber(), acc);
                } catch (NumberFormatException e) {
                    System.out.println("无效的数字格式,跳过改行: " + line);
                }

            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Account findAccountById(String accountNumber) {
        //通过对应的账户号获取对应的Account对象
        return accounts.get(accountNumber);
    }

    @Override
    public void updateAccount(Account account) {
        //更新内存中的键值对
        accounts.put(account.getAccountNumber(), account);
        //加入文件
        persistToFile();
    }


    /**
     * 向文件中写入数据
     */
    private void persistToFile() {
        try(BufferedWriter writer = new BufferedWriter(new FileWriter(STORAGE_FILE, false)))
        {
            for(Account acc : accounts.values()) {
                writer.write(acc.getAccountNumber() + "," + acc.getPassword() + "," + acc.getBalance());
                writer.newLine();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        FileAccountDAO dao = new FileAccountDAO();
        //Account account = new Account("20250001","haoyueran1109",BigDecimal.valueOf(100.00));

       // dao.updateAccount(account);
        System.out.println(dao.findAccountById("20250001"));
    }
}

package com.guoxinan.bank.domain;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Pattern;

public class Account {
    private String accountNumber;
    private String password;
    private BigDecimal balance;


    private Account(String accountNumber, String password, BigDecimal balance) {
        this.accountNumber = Objects.requireNonNull(accountNumber,"账号不能为空");
        this.password = Objects.requireNonNull(password,"密码不能为空");
        this.balance = balance;
    }
    private Account(){

    }

    public String getAccountNumber() {
        return accountNumber;
    }
    public String getPassword() {
        return password;
    }
    public BigDecimal getBalance() {
        return balance;
    }

//    public void setAccountNumber(String accountNumber) {
//        this.accountNumber = accountNumber;
//    }
//    public void setPassword(String password) {
//        this.password = password;
//    }
//    public void setBalance(BigDecimal balance) {
//        this.balance = balance;
//    }


    /**
     * 完成取钱的业务
     * @param amount 用户要取的金额
     */
    public void withdraw(double amount) {
        if(amount <= 0){
            throw new IllegalArgumentException("金额必须为正");
        }

        BigDecimal userAmount = new BigDecimal(amount);

        if (balance.compareTo(userAmount) < 0) {
            throw new IllegalArgumentException("余额不足");
        }

        balance = balance.subtract(userAmount);
    }

    /**
     * 完成用户要存钱的逻辑
     * @param amount 用户要存入的金额
     */
    public void deposit(double amount) {
        if(amount < 0){
            throw new IllegalArgumentException("金额必须大于等于0");
        }
        BigDecimal userAmount = new BigDecimal(amount);
        balance = balance.add(userAmount);
    }

    /**
     * 创建一个Account,金额为0
     * @param accountNumber 用户自定义的账户号
     * @param password 用户密码
     * @return 创建的新的账户实体
     */
    public static Account createAccount(String accountNumber, String password) throws IllegalArgumentException {
        //账户要求:8位数字
        String accountPattern = "[0-9]{8}";
        //密码要求:至少包含8个字符,必须包含大小写字母和数字
        String passwordPattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$";

        //校验账户
        if(!Pattern.matches(accountPattern, accountNumber)){
            throw new IllegalArgumentException("账户必须是8位纯数字!");
        }
        //校验密码
        if(!Pattern.matches(passwordPattern, password)){
            throw new IllegalArgumentException("密码必须包含大小写字母与数字,且8位以上!");
        }

        return new Account(accountNumber, password, new BigDecimal(0));

    }

    /**
     * 创建一个Account,金额由调用者提供
     * @param accountNumber 创建的账户id
     * @param password 创建的账户密码
     * @param balance 创建的账户金额
     * @return 创建的账户
     */
    public static Account createAccount(String accountNumber, String password,double balance)  {
        Account account = createAccount(accountNumber, password);
        account.deposit(balance);
        return account;
    }

    public String toString() {
        return getClass().getName()+"[accountNumber=" + accountNumber
                + ", password=" + password +
                ", balance=" + balance + "]";
    }


//    public static void main(String[] args){
//       String password = "123456";
//        Account acc = new Account(12345+"",password,BigDecimal.valueOf(1000L));
//        System.out.println(acc);
//    }

}

后记

本周理论学习不多,主要时间都用在了实践上,其实,除了这个银行项目以外,本周我还练习了其他几个小项目,比如在CIL界面实现的三层架构租车模拟系统、基于面向对象思想的打字游戏等。不过感觉这几个项目也没有什么太好总结的就没有记录在博客中。
不过本周,我个人最大的感受就是,面向对象程序设计中,在抽象出类这一过程,找名词是很简单的,即一个系统有什么,我们能很快的意识到。
比如:

  • 一个控制台界面的打字游戏,程序显示一系列的字符,根据玩家的积分来显示不同的长度
  • 我们能很容易的意识到应该设计一个类表示显示的字符,然后设计一个类表示符号的串;
  • 然而比较难以想到的是,对于游戏这个容器,它实现的功能:控制玩家等级、统计玩家耗时、计算得分;这三个基础功能其实我们也应该分别抽象三个类来实现具体的功能;
  • 总之,一个系统“有什么”是能一眼看出的,可是通过系统“做什么”来抽象出类是比较困难的。
    本周阅读书籍:《Java核心技术卷Ⅰ》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值