String作为Java中最常用的引用类型,相对来说基本上都比较熟悉,无论在平时的编码过程中还是在笔试面试中,String都很受到青睐,然而,在使用String过程中,又有较多需要注意的细节之处。

1.String是不可变类。

这句话其实大家都很熟悉了,那么具体什么是不可变类呢?一般认为:当对象一旦创建完成后,在正常情况下,对象的状态不会因外界的改变而改变(对象的状态是指对象的属性,包括属性的类型及属性值)。

首先看一个基本的例子:

 String s = "abc";
System.out.println("s:" + s); // 输出s:abc
s = "def";
System.out.println("s:" + s); // 输出s:def

此时,初看上去,输出的结果变了,发现s的值发生了变化,那么这与上面的说法——String类是不可变类是否矛盾呢?答案是否定的,因为s只是指向堆内存中的引用,存储的是对象在堆中的地址,而非对象本身,s本身存储在栈内存中。

实际上,此时堆内存中依然存在着"abc"和"def"对象。对于"abc"对象本身而言,对象的状态是没有发生任何变化的。

那么为什么String类具有不可变性呢,显然,既然不可变说明String类中肯定没有提供对外可setters方法。接下来来具体看一下String类的定义。

下面是String类中主要属性的定义(Java 1.7源码):

 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 }

与之前版本的Java String源码相比,String类减少了int offset 和 int count的定义。这样变化的结果主要体现在:

1.避免之前版本的String对象subString时可能引起的内存泄露问题;

2.新版本的subString时间复杂度将有O(1)变为O(n);

具体分析可见文章:

http://www.importnew.com/7656.html

http://www.importnew.com/14105.html

通过上面String类的定义,类名前面用了final class修饰,因此,String类不能被继承。对于其属性定义,可以看出,属性value[]和hash都是被定义成private类型,且由于没有提供对外的public setters方法,String类属性不可被改变。

其中,需要重点关注属性value[],其被final char修饰,因此字符型数组value只会被赋值一次就不可修改。其存储内容正好是String中的单个字符内容。

2.String相关的 +

String中的 + 常用于字符串的连接。此处为了说明清楚这个问题,首先可以安装Eclipse 查看字节码插件ByteCode Outline。

在线安装网址: http://zipeditor.sourceforge.net/update/ Disabled。Help >> install new software >> 输入网址 >> 选择 bytecode outline >> ... ... 安装成功。

Window >> show view >> other >> java >> ByteCode即可在Eclipse下方面板栏中查看。

看下面一个简单的例子:

 class D {

     public static void main(String[] args) {

         String a = "aa";
String b = "bb";
String c = "xx" + "yy " + a + "zz" + "mm" + b;
System.out.println(c);
}
}

编译运行后,点击ByteCode查看,主要字节码部分如下:

 public static main([Ljava/lang/String;)V
L0
LINENUMBER 5 L0
LDC "aa"
ASTORE 1
L1
LINENUMBER 6 L1
LDC "bb"
ASTORE 2
L2
LINENUMBER 7 L2
NEW java/lang/StringBuilder
DUP
LDC "xxyy "
INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
LDC "zz"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
LDC "mm"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 3
L3
LINENUMBER 8 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 3
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L4
LINENUMBER 9 L4
RETURN
L5
LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
LOCALVARIABLE a Ljava/lang/String; L1 L5 1
LOCALVARIABLE b Ljava/lang/String; L2 L5 2
LOCALVARIABLE c Ljava/lang/String; L3 L5 3
MAXSTACK = 3
MAXLOCALS = 4
}

显然,通过字节码我们可以得出如下几点结论:

1.String中使用 + 字符串连接符进行字符串连接时,连接操作最开始时如果都是字符串常量,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接(通过反编译工具jd-gui也可以方便的直接看出);

2.接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象,然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。

也就是说

String c = "xx" + "yy " + a + "zz" + "mm" + b; 实质上的实现过程是: String c = new StringBuilder("xxyy").append(a).append("zz").append("mm").append(b).toString();

由于得出结论:当使用+进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象。

 

3.String中的常用方法

1).与String类中value[]数组存储直接相关的有:

int length(); // 返回String长度,亦即value[]数组长度;

char charAt(int index); // 返回指定位置字符;

int indexOf(int ch, int fromIndex); //从fromIndex位置开始,查找ch字符在字符串中首次出现的位置。fromIndex默认为0,ch直接传入字符即可。如'C',区分大小写,未查找到返回-1;

char[] toCharArray() ;   // 将字符串转换成一个新的字符数组

2).与其他字符串即字串相关的方法有:

int indexOf(String str, int fromIndex) ;

与indexOf含义相反有lastIndexOf(..),反向索引。

boolean contains(String str); //实际上 contains内部实现也是调用的indexOf,然后将其结果与-1相比较。

boolean startsWith(String str); // 判断字符串是否以str开头

boolean endsWith(String str); //.....是否以str结尾

String replace(CharSequence target, CharSequence replacement) ;  // 替换

String substring(int beginIndex,  int endIndex);  //字符串截取,不传第二个参数则表示直接截取到字符串末尾

String[] split(String regex);  // 字符串分割

4.String中的equals()与hashCode()

String类重写了Object类的equlas方法,使得比较字符串内容是否相等可以直接使用equlas方法。关于equals以及相应的hashCode方法参见博文《Java总结篇系列:java.lang.Object 》

5.String字符串常量池

JVM为了提高性能和减少内存开销,内部维护了一个字符串常量池,每当创建字符串常量时,JVM首先检查字符串常量池,如果常量池中已经存在,则返回池中的字符串对象引用,否则创建该字符串对象并放入池中。

因此下述结果返回true。

 String a = "abc";
String b = "abc";
System.out.print(a == b); //true

但与创建字符串常量方式不同的是,当使用new String(String str)方式等创建字符串对象时,不管字符串常量池中是否有与此相同内容的字符串,都会在堆内存中创建新的字符串对象。

因此,下面代码片段有如下结果。

 String a = "Hello";
String b = new String("Hello");
System.out.println(a == b); //false
System.out.println(a.equals(b)); //true

即使字符串内容相同,字符串常量池中的字符串与通过new String(..)等方式创建的字符串对象之间没有直接的关系,但是,可以通过字符串的intern()方法找到此种关联。intern()方法返回字符串对象在字符串常量池中的对象引用,若字符串常量池中尚未有此字符串,则创建一新的字符串常量放置于池中。

于是,很据如上理解,很自然的,可以得到如下结果。

 String a = "Hello";
System.out.println(a == a.intern()); //true String b = new String("corn");
String c = b.intern(); System.out.println(b == c); //false String d = "corn"; System.out.println(c == d); //true

6.String/StringBuilder/StringBuffer区别

String是不可变字符串对象,StringBuilder和StringBuffer是可变字符串对象(其内部的字符数组长度可变),StringBuffer线程安全,StringBuilder非线程安全。

7.既然String是不可变字符串对象,如何才能改变让其可变?

既然String对象中没有对外提供可用的public setters等方法,因此只能通过Java中的反射机制实现。因此,前文中说到的String是不可变字符串对象只是针对“正常情况下”。而非必然。

 public static void stringReflection() throws Exception {

     String s = "Hello World";

     System.out.println("s = " + s); //Hello World

     //获取String类中的value字段
Field valueField = String.class.getDeclaredField("value"); //改变value属性的访问权限
valueField.setAccessible(true); char[] value = (char[]) valueField.get(s); //改变value所引用的数组中的第5个字符
value[5] = '_'; System.out.println("s = " + s); //Hello_World
}

由此可见Java中反射的强大之处。

Java总结篇系列:Java String的更多相关文章

  1. Java总结篇系列:Java多线程(三)

    本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题. 一.一个典型的Java线程安全例子 public class ThreadTest { public static void ma ...

  2. Java总结篇系列:Java多线程(二)

    本文承接上一篇文章<Java总结篇系列:Java多线程(一)>. 四.Java多线程的阻塞状态与线程控制 上文已经提到Java阻塞的几种具体类型.下面分别看下引起Java线程阻塞的主要方法 ...

  3. java基础解析系列(九)---String不可变性分析

    java基础解析系列(九)---String不可变性分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder java基础解析系列(二)---In ...

  4. java基础解析系列(一)---String、StringBuffer、StringBuilder

    java基础解析系列(一)---String.StringBuffer.StringBuilder 前言:本系列的主题是平时容易疏忽的知识点,只有基础扎实,在编码的时候才能更注重规范和性能,在出现bu ...

  5. Java总结篇:Java多线程

    Java总结篇系列:Java多线程 多线程作为Java中很重要的一个知识点,在此还是有必要总结一下的. 一.线程的生命周期及五种基本状态 关于Java中线程的生命周期,首先看一下下面这张较为经典的图: ...

  6. java提高篇-----理解java的三大特性之封装

    在<Think in java>中有这样一句话:复用代码是Java众多引人注目的功能之一.但要想成为极具革命性的语言,仅仅能够复制代码并对加以改变是不够的,它还必须能够做更多的事情.在这句 ...

  7. Java问题解读系列之String相关---String类为什么是final的?

    今天看到一篇名为<Java开发岗位面试题归类汇总>的博客,戳进去看了一下题目,觉得有必要夯实一下基本功了,所以打算边学边以博客的形式归纳总结,每天一道题, 并将该计划称为java问题解读系 ...

  8. Java总结篇系列:java.lang.Object

    从本篇开始,将对Java中各知识点进行一次具体总结,以便对以往的Java知识进行一次回顾,同时在总结的过程中加深对Java的理解. Java作为一个庞大的知识体系,涉及到的知识点繁多,本文将从Java ...

  9. Java总结篇系列:Java多线程(四)

    ThreadLocal是什么 早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地 ...

随机推荐

  1. C#实现的等额本息法、按月付息到期还本法、一次性还本付息法

    你若懂行,那便有用,如下: void Main(){    var x = DengEBenXi.Compute(11111, 12, 3);    x.Dump();    var y = AnYu ...

  2. 这里有个坑---[NotMapped]不要忘了加

    这里有个坑,我们定义实体的时候如果使用了在数据库没有的字段记得加[NotMapped]---------每一个遇到的坑总结后都是一比财富. 在项目中我们一般会使用EF等ORM框架来为我们服务,在ORM ...

  3. 01- Shell脚本学习--入门

    简介 Shell是一种脚本语言,那么,就必须有解释器来执行这些脚本. Unix/Linux上常见的Shell脚本解释器有bash.sh.csh.ksh等,习惯上把它们称作一种Shell.我们常说有多少 ...

  4. 360路由器c301最新固件支持万能中继

    360路由器c301现在已经停产了.出厂的固件是360_301_0.7.3.8.这个版本不支持中继.5G信号.本文主要介绍如何刷最新固件以及设置万能中继. 本文为作者原创,发表在博客园:http:// ...

  5. Atitit usbQb212 oo 面向对象封装的标准化与规范解决方案java c# php js

    Atitit usbQb212 oo 面向对象封装的标准化与规范解决方案java c# php js 1.1. 封装性是面象对象编程中的三大特性之一  三个基本的特性:封装.继承与多态1 1.2. 魔 ...

  6. C#教程(1) -- .Net与C#简介

    (1).Net .Net指.Net平台或者是.Net Framework框架. 如果你把.Net平台想象成一个厨房,那么.Net Framework框架就是其中的柴米油盐酱醋茶. 如果你把.Net平台 ...

  7. ToString()的各种用法(大全)

    常用例子: string str = ""; str = .ToString("N"); //生成 12,3456.00 str = .ToString(&qu ...

  8. China Mobile 免流原理

    用户添加包头之类(SSR软件),最后连接上了Server. 这个过程因为修改了数据包(混淆)以及服务器使用移动IP. 服务器得到信息后通过计费系统漏洞137 138端口等再返回给用户. 整个过程最重要 ...

  9. Spark MLlib - Decision Tree源码分析

    http://spark.apache.org/docs/latest/mllib-decision-tree.html 以决策树作为开始,因为简单,而且也比较容易用到,当前的boosting或ran ...

  10. 《BI那点儿事》Microsoft 线性回归算法

    Microsoft 线性回归算法是 Microsoft 决策树算法的一种变体,有助于计算依赖变量和独立变量之间的线性关系,然后使用该关系进行预测.该关系采用的表示形式是最能代表数据序列的线的公式.例如 ...