通过实例解析java String不可变性

一、原理

  1、不变模式(不可变对象)

  在并行软件开发过程中,同步操作似乎是必不可少的。当多线程对同一个对象进行读写操作时,为了保证对象数据的一致性和正确性,有必要对对象进行同步。而同步操作对系统性能是相当的损耗。为了能尽可能的去除这些同步操作,提高并行程序性能,可以使用一种不可改变的对象,依靠对象的不变性,可以确保其在没有同步操作的多线程环境中依然始终保持内部状态的一致性和正确性。这就是不变模式。

  不变模式天生就是多线程友好的,它的核心思想是,一个对象一旦被创建,则它的内部状态将永远不会发生改变。所以,没有一个线程可以修改其内部状态和数据,同时其内部状态也绝不会自行发生改变。基于这些特性,对不变对象的多线程操作不需要进行同步控制。

  同时还需要注意,不变模式和只读属性是有一定的区别的,不变模式是比读属性具有更强的一致性和不变性。对只读属性的对象而言,对象本身不能被其他线程修改,但是对象身状态却可能自行修改比如,一个对象的存活时间(对象创建时间和当前时间的时间差)是只读的,因为任何个第三方线程都不能修改这个属性,但是这是一个可变的属性,因为随着时间的推移,存活时司时刻都在发生变化。而不变模式则要求,无论出于什么原因,对象自创建后,其内部状态和数据保持绝对的稳定。

  2、怎么实现不可变对象

  在Java语言中,不变模式的实现很简单。为确保对象被创建后,不发生任何改变,并保证不变模式正常工作,只需要注意以下4点:

  • 去除 setter方法以及所有修改自身属性的方法。
  • 将所有属性设置为私有,并用final标记,确保其不可修改
  • 确保没有子类可以重载修改它的行为。
  • 有一个可以创建完整对象的构造函数。

是不是和final的功能很吻合。我们复习一下java中final的作用。

  • final修饰类,表示该类不能被继承,俗称断子绝孙类,该类的所有方法自动地成为final方法
  • final修饰方法,表示子类不可重写该方法
  • final修饰基本数据类型变量,表示该变量为常量,值不能再修改
  • final修饰引用类型变量,表示该引用在构造对象之后不能指向其他的对象,但该引用指向的对象的状态可以改变

  这里需要说明的是:当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。例如某个指向数组的final引用,它必须从此至终指向初始化时指向的数组,但是这个数组的内容完全可以改变。

二、String源码分析

以下是jdk1.8中String类的部分源码。 

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
  private final char value[];  /** Cache the hash code for the string */
    private int hash; // Default to 0

  /** use serialVersionUID from JDK 1.0.2 for interoperability */
  private static final long serialVersionUID = -6849794470754667710L;  /**
     ...}

  首先可以看到,String类使用了final修饰符,表明String类是不可继承的。然后,我们主要关注String类的成员变量value,value是char[]类型,因此String对象实际上是用这个字符数组进行封装的。再看value的修饰符,使用了private,也没有提供setter方法,所以在String类的外部不能修改value,同时value也使用了final进行修饰,那么在String类的内部也不能修改value,也就是说value一旦赋予初始值之后,value指向的地址就不能再改变了。但是上面final修饰引用类型变量的内容提到,这只能保证value不能指向其他的对象,但value指向的对象的状态是可以改变的。通过查看String类源码可以发现,String类不可变,关键是因为SUN公司的工程师,在后面所有String的方法里都很小心的没有去动字符数组里的元素。所以String类不可变的关键都在底层的实现,而不仅仅是一个final。

三、修改String使其“可变”

  虽然value是final修饰的,只是说明value不能再重新指向其他的引用。但是value指向的数组可以改变,一般情况下我们是没有办法访问到这个value指向的数组的元素。But,反射,对,反射可以,牛逼吧。可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。

public static void main(String[] args) throws Exception {
  String str = "Hello World";
  System.out.println("修改前的str:" + str);
  System.out.println("修改前的str的内存地址" + System.identityHashCode(str));
  // 获取String类中的value字段
  Field valueField = String.class.getDeclaredField("value");
  // 改变value属性的访问权限
  valueField.setAccessible(true);
  // 获取str对象上value属性的值
  char[] value = (char[]) valueField.get(str);
  // 改变value所引用的数组中的字符
  value[3] = '?';
  System.out.println("修改后的str:" + str);
  System.out.println("修改前的str的内存地址" + System.identityHashCode(str));
}
// 运行结果
// 可以看到str的字符串序列已经被改变了,但是str的内存地址还是没有改变。
修改前的str:Hello World
修改前的str的内存地址1922154895
修改后的str:Hel?o World
修改前的str的内存地址1922154895

四、String设计成不可变性的原因

  在Java中,将String设计成不可变的是综合考虑到内存、同步、数据结构及安全等各种因素的结果,下文将为各种因素做一个小结。

  1、运行时常量池的需要

  比如执行 String s = "abc";执行上述代码时,JVM首先在运行时常量池中查看是否存在String对象“abc”,如果已存在该对象,则不用创建新的String对象“abc”,而是将引用s直接指向运行时常量池中已存在的String对象“abc”;如果不存在该对象,则先在运行时常量池中创建一个新的String对象“abc”,然后将引用s指向运行时常量池中创建的新String对象。
这样在运行时常量池中只会创建一个String对象"abc",这样就节省了内存空间。

    2、同步

  因为String对象是不可变的,所以是多线程安全的,同一个String实例可以被多个线程共享。这样就不用因为线程安全问题而使用同步。

  3、允许String对象缓存hashcode

  查看上文JDK1.8中String类源码,可以发现其中有一个字段hash,String类的不可变性保证了hashcode的唯一性,所以可以用hash字段对String对象的hashcode进行缓存,就不需要每次重新计算hashcode。所以Java中String对象经常被用来作为HashMap等容器的键。

  4、安全性

  如果String对象是可变的,那么会引起很严重的安全问题。比如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为String对象是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变String引用指向的对象的值,造成安全漏洞。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 浅谈为什么Java里面String类是不可变的

    在Java里面String类型是不可变对象,这一点毫无疑问,那么为什么Java语言的设计者要把String类型设计成不可变对象呢?这是一个值得思考的问题 Java语言的创建者James Gosling,曾经在一次采访中被人问到:什么时候应该使用不可变对象(immutable object),他回答:任何可以使用的时候都会使用. 在这之前,我们先来简单了解一下,什么是不可变对象? 不可变对象指的是在对象创建之后,对象的内部状态以及对象的内存指针地址都不不能被改变.在Java里面final关键字就是

  • Java String源码分析并介绍Sting 为什么不可变

    Java String源码分析 什么是不可变对象? 众所周知, 在Java中, String类是不可变的.那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的.不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变. 区分对象和对象的引用 对于Java初学者, 对于String是不可变对象总是存有疑惑.看下面代码: String s =

  • 浅谈java String不可变的好处

    一.java内部String类的实现: java 8: public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; } java 9 及之后:(使用coder标识了编码) public final class Stri

  • Java的string类为什么是不可变的

    答案一: 最流行的Java面试题之一就是:什么是不可变对象(immutable object),不可变对象有什么好处,在什么情况下应该用,或者更具体一些,Java的String类为什么要设成immutable类型?不可变对象,顾名思义就是创建后不可以改变的对象,典型的例子就是Java中的String类. 复制代码 代码如下: String s = "ABC";  s.toLowerCase(); 如上s.toLowerCase()并没有改变"ABC"的值,而是创建了

  • JAVA不可变类(immutable)机制与String的不可变性(推荐)

    一.不可变类简介 不可变类:所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值.如JDK内部自带的很多不可变类:Interger.Long和String等. 可变类:相对于不可变类,可变类创建实例后可以改变其成员变量值,开发中创建的大部分类都属于可变类. 二.不可变类的优点 说完可变类和不可变类的区别,我们需要进一步了解为什么要有不可变类?这样的特性对JAVA来说带来怎样的好处? 1.线程安全 不可变对象是线程安全的,在线程之间可以相互共享,不需要利用特殊机制来保证同步问题,因

  • Java 中的 String对象为什么是不可变的

    什么是不可变对象? String对象是不可变的,但这仅意味着你无法通过调用它的公有方法来改变它的值. 众所周知, 在Java中, String类是不可变的.那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的.不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变. 区分对象和对象的引用 对于Java初学者, 对于String是不可变对

  • 通过实例解析java String不可变性

    一.原理 1.不变模式(不可变对象) 在并行软件开发过程中,同步操作似乎是必不可少的.当多线程对同一个对象进行读写操作时,为了保证对象数据的一致性和正确性,有必要对对象进行同步.而同步操作对系统性能是相当的损耗.为了能尽可能的去除这些同步操作,提高并行程序性能,可以使用一种不可改变的对象,依靠对象的不变性,可以确保其在没有同步操作的多线程环境中依然始终保持内部状态的一致性和正确性.这就是不变模式. 不变模式天生就是多线程友好的,它的核心思想是,一个对象一旦被创建,则它的内部状态将永远不会发生改变

  • 实例解析Java中的构造器初始化

    1.初始化顺序 当Java创建一个对象时,系统先为该对象的所有实例属性分配内存(前提是该类已经被加载过了),接着程序开始对这些实例属性执行初始化,其初始化顺序是:先执行初始化块或声明属性时制定的初始值,再执行构造器里制定的初始值. 在类的内部,变量定义的先后顺序决定了初始化的顺序,即时变量散布于方法定义之间,它们仍就会在任何方法(包括构造器)被调用之前得到初始化. class Window { Window(int maker) { System.out.println("Window(&quo

  • 通过实例解析Java不可变对象原理

    不可变对象想必大部分朋友都不陌生,大家在平时写代码的过程中100%会使用到不可变对象,比如最常见的String对象.包装器对象等,那么到底为何Java语言要这么设计,真正意图和考虑点是什么?可能一些朋友没有细想过这些问题,今天我们就来聊聊跟不可变对象有关的话题. 一.什么是不可变对象 下面是<Effective Java>这本书对于不可变对象的定义: 不可变对象(Immutable Object):对象一旦被创建后,对象所有的状态及属性在其生命周期内不会发生任何变化. 从不可变对象的定义来看,

  • 实例解析Java的Jackson库中的数据绑定

    数据绑定API用于JSON转换和使用属性访问或使用注解POJO(普通Java对象).以下是它的两个类型. 简单数据绑定 - 转换JSON,从Java Maps, Lists, Strings, Numbers, Booleans 和 null 对象. 完整数据绑定 - 转换JSON到任何JAVA类型.我们将在下一章分别绑定. ObjectMapper读/写JSON两种类型的数据绑定.数据绑定是最方便的方式是类似XML的JAXB解析器. 简单的数据绑定 简单的数据绑定是指JSON映射到Java核心

  • Java String不可变性实现原理解析

    一.原理 1.不变模式(不可变对象) 在并行软件开发过程中,同步操作似乎是必不可少的.当多线程对同一个对象进行读写操作时,为了保证对象数据的一致性和正确性,有必要对对象进行同步.而同步操作对系统性能是相当的损耗.为了能尽可能的去除这些同步操作,提高并行程序性能,可以使用一种不可改变的对象,依靠对象的不变性,可以确保其在没有同步操作的多线程环境中依然始终保持内部状态的一致性和正确性.这就是不变模式. 不变模式天生就是多线程友好的,它的核心思想是,一个对象一旦被创建,则它的内部状态将永远不会发生改变

  • 实例解析Java关于static的作用

    概述 只要是有学过Java的都一定知道static,也一定能多多少少说出一些作用和注意事项.如果已经对static了如指掌的请点击关闭按钮,看下去也只是浪费您宝贵时间而已.这篇随笔只是个人的习惯总结. 为什么需要static? 有时候我们并不想去new一个对象,只是单纯的想要调用一个函数,并且希望这个函数不会与包含它的类的其他对象有所关联.说得通俗点,即使没有创建对象,也能通过类本身来调用函数. static静态变量 被static修饰的变量属于类变量,通过字面意思就说明了这个变量的归属(类),

  • 实例解析Java中的synchronized关键字与线程安全问题

    首先来回顾一下synchronized的基本使用: synchronized代码块,被修饰的代码成为同步语句块,其作用的范围是调用这个代码块的对象,我们在用synchronized关键字的时候,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步.这叫减小锁的粒度,使代码更大程度的并发. synchronized方法,被修饰的方法成为同步方法,其作用范围是整个方法,作用对象是调用这个方法的对象. synchronized静态方法,修饰一个static静态方法,其作用范围是整个

  • 实例解析Java日期格式工具类DateUtil.java

    话不多说,请看代码: DateUtil.java package pers.kangxu.datautils.utils; import java.text.SimpleDateFormat; import java.util.Date; import pers.kangxu.datautils.common.exception.DefineException; /** * * <b> * 处理日期 工具类 * </b> * @author kangxu * */ public c

  • 实例解析Java程序中正则表达式的贪婪模式匹配

    贪婪模式(Greedy): 数量表示符默认采用贪婪模式,除非另有表示.贪婪模式的表达式会一直匹配下去,直到无法匹配为止.如果你发现表达式匹配的结果与预期的不符,很有可能是因为--你以为表达式只会匹配前面几个字符,而实际上它是贪婪模式,所以会一直匹配下去. 贪婪与非贪婪,加上?为非贪婪: var s = '1023000'.match(/(\d+)(0*)/); s ["1023000", "1023000", ""] var s = '10230

  • 实例解析Java设计模式编程中的适配器模式使用

    平时我们会经常碰到这样的情况,有了两个现成的类,它们之间没有什么联系,但是我们现在既想用其中一个类的方法,同时也想用另外一个类的方法.有一个解决方法是,修改它们各自的接口,但是这是我们最不愿意看到的.这个时候Adapter模式就会派上用场了. Adapter模式也叫适配器模式,是由GoF提出的23种设计模式的一种.Adapter模式是构造型模式之一,通过Adapter模式,可以改变已有类(或外部类)的接口形式. 适配器 模式 有三种方式,一种是对象适配器,一种是类适配器, 一种是接口适配器 以下

随机推荐