前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java中String对象最容易被忽略的知识

Java中String对象最容易被忽略的知识

作者头像
beifengtz
发布2019-06-17 18:38:19
6740
发布2019-06-17 18:38:19
举报
文章被收录于专栏:北风IT之路北风IT之路

一、String类的两种定义方法

String是一个字符串类型的类,使用""定义的内容都是字符串,但是String在使用上有一点特殊,它有两种定义方式,相信所有java程序员都知道,但是有些细节却很容易被忽略,我们接下来从内存关系上来分析一下。

1.直接赋值(匿名类)

相信很多人在初学程序的时候都写过hello word!,它是一个字符串,那么我们通过第一种直接赋值的方式来定义一个hello world!

代码语言:javascript
复制
ublic class StringTest {
    public static void main(String args[]){
        String str = "hello world!";    //直接赋值
        System.out.println(str);
    }
}

运行结果图

代码语言:javascript
复制
hello world!

通过这样一个代码String类对象已经实例化了,并且其中有内容,就是hello world!

2.new实例化(构造方法)

String对象也是可以通过关键字new来进行实例化的,接下来我们看个简单的例子。

代码语言:javascript
复制
public class StringTest {
    public static void main(String args[]){
        String str = new String("hello world!");
        System.out.println(str);
    }
}

运行结果图

代码语言:javascript
复制
hello world!

不难看出来这个是通过构造方法来给String对象赋值的,在String类中的构造方法是这样写的:

代码语言:javascript
复制
/**
 * Initializes a newly created {@code String} object so that it represents
 * the same sequence of characters as the argument; in other words, the
 * newly created string is a copy of the argument string. Unless an
 * explicit copy of {@code original} is needed, use of this constructor is
 * unnecessary since Strings are immutable.
 *
 * @param  original
 *         A {@code String}
 */
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

对于哈希我们后面再仔细解读,先看这个value,可以看出String对象的属性有一个是value,当通过构造函数传入一个字符串时该对象的value将被赋值,并且构造方法传入的对象也是String类,相当于自己作为参数传进去,这样的做法在java中是允许的,那么传进去的String又是哪儿来的呢?我们继续来看。

在解决这个问题之前我们先看一下字符串比较,通过这个来引入。

二、字符串比较

我们来看一下代码

代码语言:javascript
复制
public class StringTest {
    public static void main(String args[]){
        String str1 = "hello";
        String str2 = new String("hello");
        String str3 = str2;                 //引用传递
        System.out.println(str1 == str2);
        System.out.println(str2 == str3);
        System.out.println(str3 == str1);
    }
}

运行结果

代码语言:javascript
复制
false
true
false

以上三个String类对象的内容完全一样,但是结果有的是true有的是false,原因就是在java中String类的比较用==并不是比较其内容,而是比较其所在堆内存中的地址值,并非比较其数值。我们来分析一下内存关系图

我们通过最后一个内存图可以看出str1和str2指向的地址不一样,所以第一个输出false;str2和str3指向的地址都是XO0020,所以第二个输出true,str3和str1指向的地址不一样,所以第三个输出false。

如果在String中想比较大小要用到String类中的equals()方法,该方法比较的就是对象中所存的值。

代码语言:javascript
复制
public class StringTest {
    public static void main(String args[]){
        String str1 = "hello";
        String str2 = new String("hello");
        String str3 = str2;                 //引用传递
        System.out.println(str1.equals(str2));
        System.out.println(str2.equals(str3));
        System.out.println(str3.equals(str1));
    }
}

运行结果

代码语言:javascript
复制
true
true
true

在平时使用的时候很容易对这两个搞混淆,一般使用eauals的会比较多。

不难看出在字符串比较时有比较内存地址和内容值之分,回顾之前写的一篇文章java实例化对象过程中的内存分配,我们继续来通过内存分配的方式分析上面讲的两个String定义的方式。

三、两种实例化方式的区别

1.直接赋值过程

在java中,如果直接用双引号里面加上字符串,就是实例化了一个String匿名类对象,此过程就会在堆内存中开辟一个空间。

代码语言:javascript
复制
String str = "hello world!";

那么在这个过程中会开辟几个堆内存几个栈内存呢?我们来看一下内存图

此过程中开辟了一块堆内存一块栈内存,但是如果我们声明多个字符串并且都赋值同一内容的字符串呢?

2.new实例化过程

代码语言:javascript
复制
String str = new String("hello world!");

以上的代码其实可以将其初略地划分为三个步骤:

  1. 先实例化一个String类对象str
  2. 再实例化一个匿名对象"hello world!"
  3. 将str对象栈内存指向"hello world!"的堆内存空间

我们来看一下其内存关系

通过这个图可以看出此种方法创建String对象的缺陷,每次都会产生一块垃圾空间,所以建议在平时开发中尽量使用第一种方式。

四、JVM对象池

我们来看一下代码

代码语言:javascript
复制
public class StringTest {
    public static void main(String args[]){
        String str1 = "hello";
        String str2 = "hello";
        String str3 = str2;                 //引用传递
        System.out.println(str1 == str2);
        System.out.println(str2 == str3);
        System.out.println(str3 == str1);
    }
}

运行结果

代码语言:javascript
复制
true
true
true

我们发现上面代码运行结果都是true,说明三个String对象所指向的堆内存是同一个地址,我们来看一下其内存关系图

那么如果我们再加一个str4指向“world”呢?

代码语言:javascript
复制
public class StringTest {
    public static void main(String args[]){
        String str1 = "hello";
        String str2 = "hello";
        String str3 = str2;                 //引用传递
        String str4 = "world";
        System.out.println(str1 == str2);
        System.out.println(str2 == str3);
        System.out.println(str3 == str1);
        System.out.println(str3 == str4);
    }
}

运行结果

代码语言:javascript
复制
true
true
true
false

我们可以得到,实例化的匿名对象如果内容相同则使用的是同一个堆内存空间,并不是说实例化了三个"hello"就会开辟三个堆内存空间,如果内容不同则会开辟新的堆内存空间。

但是按道理来说应该是每实例化一个对象就开辟一个空间,之所以会出现这种情况是因为JVM对象池的原因,它用到了一个共享设计模式,目的是为了节省资源消耗。

共享设计模式: 在JVM的底层实际上会存在有一个对象池(不一定只保存String对象,其他对象也可保存),当代码之中通过直接赋值的方式定义了String对象时,会将此字符串对象所使用的匿名对象入池保存,而后如果后续还有其他String对象也采用了直接赋值的方式,并且设置了同样内容的时候并不会开辟新的堆内存空间,而是使用已有的对象进行引用的分配,从而继续使用。

那么如果通过构造方法来创建String对象能使用对象池吗?我们来看一段代码

代码语言:javascript
复制
public class StringTest {
    public static void main(String args[]){
        String str1 = new String("hello");
        String str2 = new String("hello");
        System.out.println(str1 == str2);
    }
}

运行结果

代码语言:javascript
复制
false

很明显通过构造方法来赋值的方式并没有将其存入对象池,其原因是使用了关键字new开辟的新内存。如果希望开辟的新内存也可以利用对象池,这个时候我们就需要手动入池,用String类中的方法intern()

我们来看看使用手工入池的代码

代码语言:javascript
复制
public class StringTest {
    public static void main(String args[]){
        // 使用构造方法定义了新的内存空间,而后入池
        String str1 = new String("hello").intern();
        String str2 = new String("hello").intern();
        String str3 = "hello";
        System.out.println(str1 == str2);
        System.out.println(str1 == str3);
    }
}

运行结果

代码语言:javascript
复制
true
true

通过上面的分析,如果以后面试问到“String类对象两种实例化方式区别是什么?”,那么我们就能清楚的回答啦~

  • 直接赋值(String str = "字符串";):只会开辟一块堆内存空间,并且会在自动保存在对象池之中以供下次重复使用;
  • 构造方法(String str = new String("字符串");):会开辟两块堆内存空间,其中有一块空间将成为垃圾,并且不会自动入池,但是可以使用intern()方法手工入池。

五、字符串常量的不可改变性

字符串一旦被定义就不可改变,但是我们不能从平时编写的代码表面地去理解它,要从内存分析上才能理解它为什么是不可改变的。

我们来看一下下面的代码

代码语言:javascript
复制
public class StringTest {
    public static void main(String args[]){
        String str = "hello ";
        str = str + "world ";
        str += "!";
        System.out.println(str);
    }
}

运行结果

代码语言:javascript
复制
hello world !

如果按照代码来理解可能认为str的内容被改变了,并且被改变了两次!之前记得有人问过我类似的问题:上面的代码str对象赋值过程中进行了几步操作?当时我也不是很清楚,不过经过这次学习就能解释这个问题了。

以上操作可以看到,所谓的字符串的内容实际上并未改变(Java定义好了String的内容不能改变),改变的是地址的指向。对于字符串对象内容的改变,是利用了引用关系的改变而实现的,但是每一次的变化都会产生垃圾空间。

其实我们可以从jdk中对String对象的定义中找到其注释可以发现这一规定,下面是String类定义完整注释,在前面就可以看到这一句Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings.

对于这些知识平时使用时可能不太注意,但是了解之后对以后开发是有很大帮助的,最好的学习方法:多读读API和jdk源码吧~~~

END

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 北风IT之路 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、String类的两种定义方法
    • 1.直接赋值(匿名类)
      • 2.new实例化(构造方法)
      • 二、字符串比较
      • 三、两种实例化方式的区别
        • 1.直接赋值过程
          • 2.new实例化过程
          • 四、JVM对象池
          • 五、字符串常量的不可改变性
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档