Java中的String、常量池、反射机制

在介绍String类之前,先把Java中的反射机制和常量池这两个概念梳理清楚。

事先说明:本文中的内容是建立在目前所了解的Java知识的基础上,待日后逐渐深入,一些表达也会及时进行相关的更改。谢谢大家!

  • 反射机制

        首先,让我们来了解一下Class文件和Class类。一个Class文件(xxx.class)是一个对应着唯一一个类或接口的二进制流。至于Class类,个人认为其就是一个模板类。每个类都对应着一个Class对象,对应的Class对象中存放着该类的所有信息(比如前面提到的类变量、类方法、注解等等)        

        反射机制指的是程序在运行时能够获取自身的信息。可以理解为在运行中通过创建一个类的反射对象,调用这个反射对象的相关方法,对这个类及其实例进行相关操作。

         如何理解这一段话呢?让我们先回想一下之前提到过的在内存中,一个对象的创建过程:先是在方法区中加载类信息,然后在堆中开辟空间,再进行对象的初始化。

        “在方法区中加载类信息”这个过程,其实就是在方法区中加载Class文件,然后在堆中创建一个该类的Class对象(用于封装该类在方法区内的数据结构),这个Class对象就是上文说的反射对象。其提供了许多相关的方法,比如newInstance()可以得到一个实例,getMethod()可以拿到对应类中的方法。除此之外还能拿到类中的其他所有信息,进行相关操作。

        生成一个类的Class对象可以利用JVM自动加载,即加载类信息的三种方式:1、new一个对象;2、new一个子类的对象,父类的信息也要加载;3、调用类的静态成员。除了自动加载,我们也可以通过以下三种方式进行手动加载,得到类的Class对象:

1、类名.class:该方法不会做任何类的初始化工作

2、Class.forName(“类的全局定名,即包名+类名”):该方法会进行类的静态初始化

3、new 类名().getClass():该方法会进行静态初始化和非静态初始化

        这里要注意的是,每个类只有一个Class对象,即堆区中的对应的Class对象只有一个。

        比如有一个类A,我们平常创建类A的实例的方式是:A a = new A(); 这称为正射

        而先拿到A的class对象,即:Class classA = A.class; 然后再调用class对象的方法,得到A的实例,A aaa =(A) classA.newInstance(); 这称为反射

        这里需要进行强转是因为默认返回的是Object类型

        反射机制能够为Java带来动态特性,在设计模式中也有被用来进行相关的优化。

以上对反射机制进行了一个简要的说明,接下来让我们一起了解一下Java中的常量池。

       

  • 常量池

        Java中有三种常量池:

        1、静态常量池:Class文件中的常量池,用于存放各种字面量和符号引用(包括描述符、包名类名等等)

        2、运行时常量池(在方法区):装载Class文件到内存之后,JVM将静态常量池中的内容转移到运行时常量池,运行期间也有可能将新生成的常量放置在运行时常量池中。

        3、字符串常量池:存放字符串常量,在JDK1.6及之前,字符串常量池在方法区内,常量池中保存的是对象及引用;在JDK1.7之后,字符串常量池被移到了堆内,常量池中保存的是引用

  • String

        String是final类,表示其不能被继承。之前提到过的八个包装类也都是final类型的。

        追到源码里我们可以看到,String类有一个属性:private final char value[],用于存放字符串内容这里final修饰的是数组value,表示该数组的地址不能改变,但是内容是可以被改变的,即我们可以通过反射机制来修改数组里面的内容。

        创建String对象的三种方式:

//第一种:直接赋值
String s = "abc";

//第二种:利用构造器
char [] ch = new char[]{'a','b','c'};String s = new String(ch);

//第三种
String s = new String("abc");

        对于第一种创建方式,JVM会先从字符串常量池中看一下是否有“abc”这个String对象的引用,如果有,则直接返回该引用给s;如果没有,JDK1.6及之前,会在常量池中重新创建一个“abc”平面值对象,然后把新创建的对象和其引用保存在常量池中,并返回该引用给s,JDK1.7之后,会在堆中重新创建一个“abc”平面值对象,常量池中保存的只是其引用,然后将引用返回给s。

        对于第二种创建方式,JVM不会创建平面值对象,而是将ch数组中的值拷贝到s中的value[]属性里,直接在堆中创建一个String对象。

        对于第三种创建方式,JVM还是会去常量池看一下是否有“abc”平面值对象的引用,没有则创建。但是和第一种方式不同的是,这里的s指向的是堆中新建的一个String对象,只不过该对象的value[]和“abc”这个对象的value[]值指向的是同一块内存空间。其实我们追到源码里面去也能看到,构造器是把“abc”这个平面值对象的value[]和hash值都拷贝给s了。

        以上三种构建方式,我们可以总结出,只要有用双引号包着的字符串,都会创建其平面值对象,只不过根据JDK的版本不同,常量池中保持的内容不一样,但本质还是一样的。

        String是一个常量,创建好之后就不能再更改。

        这里的不能再更改,我个人理解的是:不管是用双引号创建的String,还是new出来的String,在堆中的常量池中都会有这个平面值对象的引用(JDK1.7之后)。如果对字符串进行更改,常量池中原有的字符串不会变(这就是所谓的不能更改)而是会添加一个更改过后的平面值引用,即一个新的对象。比如String对象的replace等改变字符串内容的方法,都是会返回一个新的字符串对象(是在堆中,而不是常量池,这个不会生成平面值对象。)其自身是不会改变的。

        总结一下:修改字符串的值实际上是重新开辟空间!!!

         String类还有一个非常重要的方法------intern(),下面我针对JDK的版本不同对该方法进行一个简要的说明。

        在JDK1.6及之前,字符串常量池在方法区,里面保存的是对象及其引用。当一个String对象调用这个方法的时候,如果常量池中有和该对象在内容上相等的字符串对象,则把常量池中的该对象的引用返回;否则,将和String值一样的平面值对象(即此时创建了一个平面值对象)添加到常量池中,然后再返回它的引用。即该方法最终返回的是常量池中存储的对象的地址 (方法区内的地址)。

        JDK1.7之后,字符串常量池在堆中,里面保存的是对象的引用。当一个String对象调用这个方法的时候,如果常量池中有和该对象在内容上相等的字符串对象,则把常量池中的该对象的引用返回;否则,将该String对象的引用添加到常量池中(这是因为常量池和对象都在堆中,所以常量池直接保存该对象的引用即可,不用再创建一个平面值对象了),然后返回该引用。

        下面我们看一个例子:(默认是JDK1.7以上的版本)

//方式1
String a = "hello"+"abc";

//方式2
String b = new String("hello") + new String("abc");

        对于方式1,这行代码编译器会优化,等价于String a = "helloabc"; 此时堆中只有一个“helloabc”平面值对象。可以这么理解,对于编译期能直接确定的常量值,会直接把表达式的结果的引用放进常量池。

        对于方式2,此时b指向的是堆中的一个值为“helloabc”的对象,这句话并不会创建一个“helloabc”的平面值对象!堆中只有“hello”和“abc”这两个平面值对象。

        因为字符串是一个常量,对其更改就是在开辟新的内存空间,这在实际操作中是很耗费资源的。下面介绍和String密切相关的另外两个类:StringBuilder和StringBuffer,这两个类都是可变的,并且提供了相关的增删改查操作。

        1、StringBuffer

        很多方法和String是相似的,但是它提供了一个可变的字符串序列,即其字符串内容是可变的。并且只有在存放字符串的数组value容量不够时,才开辟空间进行扩张。其支持多线程操作。

//String-->StringBuffer

//1、
StringBuffer sb = new StringBuffer("abc");

//2、
new StringBuffer().append("abc");

//StringBuffer-->String:

// 1、
sb.toString();
//2、
String str = new String(sb);

        2、StringBuilder

        和StringBuffer类似,该类也提供了字符串的增删改查等操作,并且字符串是可变的。不同之处在于,该类没有synchronized关键字,即方法没有做互斥处理。所以该类是不适合多线程的情况的。(不知道大家还记不记得操作系统中的线程互斥访问,如果没有进行互斥操作,则会出现死锁等情况,即不适合多线程的操作。这里也是类似的道理。)

        但也是因为该类不支持多线程同步,所以在单线程的情况下,它比StringBuffer运行速度要快一些。

        总结:

        String是不可变字符串序列,效率低,但是复用性高。(即常量池中的字符串可以被所有对象共享。)

        StringBuffer是可变字符序列,效率较高,线程安全,适合多线程。

        StringBuildr是可变字符序列,效率最高,线程不安全,适合单线程。

        所以如果我们要对字符进行大量修改,比如循环添加字符进字符串,不要使用String。如果字符串很少修改,并且被多个对象引用,使用String,比如配置信息。

       

        介绍完这两个类,给大家看一段代码:
String a = "aaa";
String b = "bbb";
String c = a + b;

        以字符串相加为例,前面说了字符串是一个常量,所以我们能够猜到,强大的Java一定会进行相关的优化。

        上面三行代码,JVM做了以下的工作:

    1、创建一个StringBuilder对象sb(名字是我随意取的)

    2、sb.append("aaa");

    3、sb.append("bbb");

    4、String c = sb.toString(); //这里返回了一个new的Stirng对象,创建了“aaabbb”的平面值

最后,c指向了堆中的一个String对象,该对象的value和堆中的平面值对象“aaabbb”指向同一块内存,即这句话等价于 String c = new String(“aaabbb”)

        如果是 String c = "aaa"+b,也会像上述的四个步骤一样,new一个字符串对象(而不是字面值对象)出来。只有两个常量相加的时候编译器才会优化成一个字面值对象,只要有变量相加,编译器就会使用StirngBuilder或StringBuffer进行优化,几个加号优化几次。
 

        总结:

        字符串常量相加,JVM会进行优化,只会创建一个字符串对象

        只要字符串变量参加相加的操作,JVM都会调用StringBuilder对象,调用其append方法、toString方法进行相关操作。

        常量相加,结果在编译期就能确定;变量相加,结果要运行时才能确定(虽然我们编写代码的时候知道结果是怎样的,但是对于编译器来说,变量的结果都是要等运行时才能确定的。

        

        以上就是这篇文章的内容了,如果有表达不清楚或者内容有误的地方,烦请大家批评指正!

        

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值