羿先生的学习笔记[2]: Java中的可变与不可变类

本文是羿先生的学习笔记,探讨Java中可变与不可变类的概念,强调不变性的优势和使用场景。通过示例解释如何通过private、final关键字和保护性拷贝来创建不可变类,防止对象状态被意外修改。
摘要由CSDN通过智能技术生成

羿先生的学习笔记[2]: Java中的可变与不可变类

0. 序言

本系列博客用以记录本人在学习HIT-CS 软件构造课程中的一些收获,内容会涉及到课堂内容、感悟和本人关于Java语言的一些认识。

1. 定义

不变数据类型(Immutable type)是指一旦被创建后其值不能被改变的对象,可变数据类型(Mutable type)则指被创建后可以改变其值的对象。
需要注意的是,改变对象的值与改变实例化引用的对象是完全不同的。例如,String类为不可变类型,但可以通过改变对实例化对象的引用来获得我们需要的字符串,这实际上是在内存中创建了一个新的字符串对象并引用。执行以下代码段:

        String str = "abc";
        System.out.println("字符串的内容为" + str);
        System.out.println("字符串的地址为" + System.identityHashCode(str));
        str = "ABC";
        System.out.println("字符串的内容为" + str);
        System.out.println("字符串的地址为" + System.identityHashCode(str));

输出的结果为:

字符串的内容为abc
字符串的地址为157627094
字符串的内容为ABC
字符串的地址为357863579

可以看到,在使用等号对str的引用进行了改变前后的字符串地址不同。这实际上说明了我们在内存中新建立了一个字符串对象,而不是对原有对象内部的值进行修改。

2.如何编写不可变类型的类

(0) 什么时候和为什么要使用不可变类型

不可变类型的优点在于安全性、易于理解,可以阻止程序员和用户对于不应被改变的数据进行不合法的操作。不可变类型也有会产生大量内存垃圾的缺点。
在实际应用中,有的数据在业务逻辑上是一旦创建就不能被改变的,应该被设定为不可变类型。
下面以一个包含不可变的一个人的姓名、性别、身份证号、生日,和可变的身高、体重的Person类为例探讨如何编写不可变类型。

(1) private关键字

Java借助private、protected、public与默认修饰符提供了成员访问控制。适用于字段、方法或类。如果实体使用private修饰符声明,那么实体将只能由定义实体的类中的包含的代码访问。如果不使用private关键字修饰变量可能会导致变量在不应被访问的地方被修改。

(2) final关键字

Java中final关键字可以修饰类、方法与变量。

  • 用final关键字修饰类会使该类不能被其他类所继承,通常工具类都应被设定成final类
  • 用final关键字修饰方法会使方法不能被子类重写。
  • 用final关键字修饰变量,则变量只能直接赋值或在构造方法中赋值,无法再次对其赋值。对于基本数据类型,其值无法改变。对于引用类型,则其引用的地址不能改变。

简而言之,使用final关键字可以确保变量不被直接改变,例如:

public class Person{
    private String name;
    private final int id;
    private Calendar birthday;
    private Gender gender;
    private double weight;
    private double height;

    public Person(String name, int id, Calendar birthday, Enum<gender> gender, double weight, double height){
        this.name = name;
        this.id = id;
        this.birthday = birthday;
        this.gender = gender;
        this.weight = weight;
        this.height = height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    public void setWeight(double weight) {
        this.weight = weight;
    }
}

public enum Gender{
    MALE, FEMALE
}

name未被final修饰,而id被final修饰。此时如果我们再加入一个改变姓名的方法和改变id的方法:
编译器报错

编译器就会报错提示我们id不能被改变。

(3) 保护性拷贝(Defensive Copy)

那么是不是把所有不可变变量设为private final就万事大吉了呢?不是的。之前提到final关键字修饰引用型变量的效果是使其无法指向新的对象,但如果这个对象本身就是可变的呢?
我们将所有应为不可变的类型用final修饰并向我们的Person类里添加一个返回生日的方法:

public class Person{
    private final String name;
    private final int id;
    private final Calendar birthday;
    private final Gender gender;
    private double weight;
    private double height;

    public Person(String name, int id, Calendar birthday, Enum<gender> gender, double weight, double height){
        this.name = name;
        this.id = id;
        this.birthday = birthday;
        this.gender = gender;
        this.weight = weight;
        this.height = height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    public void setWeight(double weight) {
        this.weight = weight;
    }

    public Calendar getBirthday() {
        return birthday;
    }
}

我们执行下列代码:

	Calendar naiveManBirthday = Calendar.getInstance();
    naiveManBirthday.set(2000, Calendar.MARCH,28);
    Person naiveMan = new Person("Naive!", 123456, naiveManBirthday, gender.MALE, 70, 180);
    System.out.println("出生年份1:" + naiveMan.getBirthday().get(Calendar.YEAR));
    Calendar oldManBirthday = naiveMan.getBirthday();
    oldManBirthday.set(1926, Calendar.AUGUST, 17);
    System.out.println("出生年份2:" + naiveMan.getBirthday().get(Calendar.YEAR));

得到输出:

出生年份1:2000
出生年份2:1926

于是我们将2000年出生的年轻人的生日更改成了一位90多岁老人的生日,显然birthday不符合不可变的要求。因为Calendar类本身是可变的,所以我们通过调用getBirthday()获得的对象内的方法,来改变对象内部的值,从而篡改了年轻小伙的生日。如何防范呢?我们需要使用保护性拷贝(Defensive Copy)来实现。
我们对getBirthday()方法稍作修改:

    public Calendar getBirthday() {
        return (Calendar) birthday.clone();
    }

用Calendar方法提供的clone()方法返回一个内容完全一致但地址不同的新对象,从而规避掉前述的篡改生日的操作。再次运行前述的代码试图操作小伙子的时间,得到结果:

出生年份1:2000
出生年份2:2000

符合不可变的要求。

(4) 总结

终于,我们编写出了一个规范的,正确地设置了不可变性与可变性的类:

public class Person{
    private final String name;
    private final int id;
    private final Calendar birthday;
    private final Gender gender;
    private double weight;
    private double height;

    public Person(String name, int id, Calendar birthday, Enum<gender> gender, double weight, double height){
        this.name = name;
        this.id = id;
        this.birthday = birthday;
        this.gender = gender;
        this.weight = weight;
        this.height = height;
    }

    public final void setHeight(double height) {
        this.height = height;
    }

    public final void setWeight(double weight) {
        this.weight = weight;
    }

    public final String getName() {
        return name;
    }

    public final int getId() {
        return id;
    }
    
    public final Calendar getBirthday() {
        return (Calendar) birthday.clone();
    }

    public final Gender getGender() {
        return gender;
    }

    public final double getWeight() {
        return weight;
    }
    
    public final double getHeight() {
        return height;
    }
    
}

总结:

  • 不应被外部访问的成员变量使用private修饰,不可变类型的变量用private final修饰且不提供set方法
    改变他们的值或引用;
  • 不希望被子类重载的方法使用final修饰,不希望被继承的类使用final修饰;
  • 对于成员变量中的可变类,在用get方法获得该变量的时候使用保护性拷贝,即创建一个新的内容相同的对象来返回。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值