前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >String类不可变分析以及普通不可变类

String类不可变分析以及普通不可变类

作者头像
名字是乱打的
发布2021-12-22 15:03:20
6170
发布2021-12-22 15:03:20
举报
文章被收录于专栏:软件工程软件工程
一、String为什么不可变?

要了解String类创建的实例为什么不可变,首先要知道final关键字的作用:final的意思是“最终,最后”。

final关键字可以修饰类、方法、字段。 修饰类时,这个类不可以被继承修饰方法时,这个方法就不可以被覆盖(重写),在JVM中也就只有一个版本的方法--实方法; 修饰字段时,这个字段就是一个常量。

查看java.lang.String方法时,可以看到:

代码语言:javascript
复制
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
   ...
}
String本质上是一个被final修饰的char数组。

我们在创建一个String时候 如:String str=new String("ab");实际上是创建了final char value['a','b'],而这里的str仅仅是保存的这个char数组的引用地址,我们在修改时候,比如str="ccc";实际上是将str的引用地址给改变了,但是我们原来的被final修饰的数组并没有改变.

String一些方法诸如replace,substring等等看似改变了字符串的值,时间上只是改变了引用指向的地址,其实他们底层都是通过创建新的String对象来返回的,并不是修改以前的. 可以看下源码.

二、String类不可变有什么好处?
  • 最简单的就是为了安全和效率。 从安全上讲,因为不可变的对象不能被改变,他们可以在多个线程之间进行自由共享,这消除了进行同步的要求;
  • 从效率上讲,设计成final,JVM才不用对相关方法在虚函数表中查询,而是直接定位到String类的相关方法上,提高执行效率;
  • 总之,由于效率和安全问题,String被设计成不可变的,这也是一般情况下,不可变的类是首选的原因。
三String对象真的不可变吗?

从上文可知String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。

那么用什么方式可以访问私有成员呢? 没错,用反射可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。 示例代码:

代码语言:javascript
复制
public static void testReflection() throws Exception {
 
        //创建字符串"Hello World", 并赋给引用s
        String s = "Hello World"; 
 
        System.out.println("s = " + s); //Hello World
 
        //获取String类中的value字段
        Field valueFieldOfString = String.class.getDeclaredField("value");
 
        //改变value属性的访问权限
        valueFieldOfString.setAccessible(true);
 
        //获取s对象上的value属性的值
        char[] value = (char[]) valueFieldOfString.get(s);
 
        //改变value所引用的数组中的第5个字符
        value[5] = '_';
 
        System.out.println("s = " + s);  //Hello_World
    }
代码语言:javascript
复制
//打印结果
  s = Hello World
 s = Hello_World

在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。

五、不可变类

不可变类只是它的实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例时就提供,并在对象 的整个生命周期内固定不变。String、基本类型的包装类、BigInteger和BigDecimal就是不可变得类。

为了使类成为不可变,必须遵循以下5条规则: ①不要提供任何会修改对象状态的方法。 ②保证类不会被扩展。 ③使所有的域都是final。 ④使所有的域都成为私有的。 ⑤确保 对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。

六、不可变类的优点和缺点

不可变类实例不可变性,具有很多优点。 ①不可变类对象比较简单。不可变对象可以只有一种状态,即被创建时的状态。 ②不可变对象本质上是线程安全的,它们不要求同步。当多个线程并发访问这样的对象时,它们不会遭到破坏。实际上,没有任何线程会注意到其他线程对于不可变对象的影响。所以,不可变对象可以被自由地分配。“不可变对象可以被自由地分配”导致的结果是:永远不需要进行保护性拷贝。 ③不仅可以共享不可变对象,甚至也可以共享它们的内部信息。 ④不可变对象为其他对象提供了大量的构件。如果知道一个复杂对象内部的组件不会改变,要维护它的不变性约束是比较容易的

不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。创建这种对象的代价很高。

七、如何构建不可变类?

构建不可变类有两种方式:

  • 用关键字final修饰类
  • 让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂来替代公有的构造器。

为了具体说明用静态工厂方法来替代公有的构造器,下面以Complex为例:

代码语言:javascript
复制
//复数类
public class Complex{
    //实数部分
    private final double re;
    //虚数部分
    private final double im;
    //私有构造器
    private Complex(double re,double im){
        this.re = re;
        this.im = im;
    }
    //静态工厂方法,返回对象唯一实例
    public static Complex valueOf(double re,double im){
        return new Complex(re,im);
    }
    ...
}

不可变的类提供一些静态工厂方法,它们把频繁请求的实例缓存起来,从而当现在实例符合请求的时候,就不必创建新的实例。使用这样的静态工厂方法使得客户端之间可以共享现有的实例,而不是创建新的实例,从而减低内存占用和垃圾回收的成本。

总之,使类的可变性最小化。不要为每个get方法编写一个相对应的set方法,除非有很好的理由要让类成为可变的类,否则就应该是不可变的。如果有些类不能被做成是不可变的,仍然应该尽可能地限制它的可变性。不可变的类有很多优点,但唯一的缺点就是在特定的情况下存在潜在的性能问题。

PS:静态工厂方法是什么? 静态工厂方法只是一个返回类的实例的静态方法,如下面是一个Boolean的简单实例。这个方法将boolean基本类型值转换成一个Boolean对象引用。

代码语言:javascript
复制
public static Boolean valueOf(boolean b){
    return b?Boolean.TRUE?Boolean.FALSE;
}
静态工厂方法相对于构造器来说,具有很多优势:

①创建的方法有名字; ②不必在每次调用它们的时候都创建一个新的对象;

可以返回原返回类型的任何子类的对象。这样我们在选择返回对象的类时就有更大的灵活性,这种灵活性的一种应用是API可以返回对象,同时又不会使对象的类变成公有的。以这种方式隐藏实现类会使API变得非常简洁,这项技术适用于基于接口的框架。

④在创建参数化类型实例时,它们使代码变得更加简洁。编译器可以替你找到类型参数,这被称为类型推导。如下面这个例子

代码语言:javascript
复制
public static<k,v> HashMap<k,v> newInstance(){
    return new HashMap<k,v>();
}
静态工厂方法也有缺点:

类如果不含公有的或者受保护的构造器,就不能被子类化。对于公有的静态工厂方法所返回的非公有类也同样如此。

它们与静态方法实际上没有什么区别。

简而言之,静态工厂方法和公有构造器都各有用处,我们需要理解它们各自的长处。结合实际情况,再做选择。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/10/28 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、String为什么不可变?
  • 二、String类不可变有什么好处?
  • 三String对象真的不可变吗?
  • 五、不可变类
  • 六、不可变类的优点和缺点
  • 七、如何构建不可变类?
    • 静态工厂方法相对于构造器来说,具有很多优势:
      • 静态工厂方法也有缺点:
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档