Java工程师成神之路 基础篇 Java基础知识 String

    技术2022-07-12  84

    String

    字符的不可变性

    定义一个字符串

    String s = "abcd";

    s 中保存了String对象的引用。下面的箭头可以理解为“存储他的引用”。


    使用变量来赋值变量

    String s2 = s;

    s2 保存了相同的引用,因为它们代表的是同个对象。


    字符串连接

    s = s.concat("ef");

    s中保存的是一个重新创建出来的string对象的引用。


    总结

    一旦一个string 对象在内存(堆)中被创建出来,他就无法被修改。特别注意的是,String类的所有方法都没有改变字符串本身的值,都是返回一个新的对象。

    如果你需要一个可修改的字符串,应该使用StringBuffer 或者 StringBuilder。否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的string对象被创建出来。


    JDK6和JDK7中substring的原理及区别

    String是Java中一个比较基础的类,每一个开发人员都会经常接触到。而且,String也是面试中经常会考的知识点。String有很多方法,有些方法比较常用,有些方法不太常用。今天要介绍的subString就是一个比较常用的方法,而且围绕subString也有很多面试题。

    substring(int beginIndex, int endIndex)方法在不同版本的JDK中的实现是不同的。了解他们的区别可以帮助你更好的使用他。为简单起见,后文中用substring()代表substring(int beginIndex, int endIndex)方法。

    substring() 的作用

    substring(int beginIndex, int endIndex)方法截取字符串并返回其[beginIndex,endIndex-1]范围内的内容。

    String x = "abcdef"; x = x.substring(1,3); System.out.println(x);

    输出内容:

    bc

    调用substring()时发生了什么?

    你可能知道,因为x是不可变的,当使用x.substring(1,3)对x赋值的时候,它会指向一个全新的字符串: 然而,这个图不是完全正确的表示堆中发生的事情。因为在jdk6 和 jdk7中调用substring时发生的事情并不一样。

    JDK6 中的substring

    String是通过字符数组实现的。在jdk6中,String类包含三个成员变量:char value[],int offset,int count。它们分别用来存储真正的字符数组,数组的第一个位置索引以及字符串中包含的字符个数。

    当调用substring方法的时候,会创建一个新的string对象,但是这个string 的值仍然指向堆中的同一个字符数组。这两个对象中只有count 和 offset 的值不同的。 下面是证明上说观点的Java源码中的关键代码:

    //JDK 6 String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count; } public String substring(int beginIndex, int endIndex) { //check boundary return new String(offset + beginIndex, endIndex - beginIndex, value); }

    JDK 6中的substring导致的问题

    如果你有一个很长很长的字符串,但是当你使用substring进行切割的时候你只需要很短的一段。这可能导致性能问题,因为你需要的只是一小段字符序列,但是你却引用了整个字符串(因为这个非常长的字符数组一直在被引用,所以无法被回收,就可能导致内存泄露)。在JDK 6中,一般用以下方式来解决该问题,原理其实就是生成一个新的字符串并引用他。

    x = x.substring(x, y) + ""

    关于JDK 6中subString的使用不当会导致内存系列已经被官方记录在Java Bug Database中:

    内存泄露:在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。 内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。


    JDK 7中的substring

    上面的问题,在 jdk 7 中得到解决。在jdk7中,substring方法会在堆内存中创建一个新的数组。 Java源码中关于这部分的主要代码如下:

    //JDK 7 public String(char value[], int offset, int count) { //check boundary this.value = Arrays.copyOfRange(value, offset, offset + count); } public String substring(int beginIndex, int endIndex) { //check boundary int subLen = endIndex - beginIndex; return new String(value, beginIndex, subLen); }

    以上是JDK 7中的subString方法,其使用new String创建了一个新字符串,避免对老字符串的引用。从而解决了内存泄漏问题。

    所以,如果你的生产环境中使用的JDK版本小于1.7,当你使用String的subString方法时一定要注意,避免内存泄露。


    replaceFirst、replaceAll、replace 区别

    replace、replaceAll和replaceFirst是Java中常用的替换字符的方法,它们的方法定义是:

    replace(CharSequence target, CharSequence replacement),用replacement 替换所有的target,两个参数都是字符串。

    replaceAll(String regex,String replacement),用replacement替换所有的regex匹配项,regex很明显是个正则表达式,replacement是字符串。

    replaceFirst(String regex,String replacement),基本和replaceAll相同,区别是只替换第一个匹配项。

    可以看到,其中replaceAll以及replaceFirst是和正则表达式有关的,而replace和正则表达式无关。

    replaceAll和replaceFirst的区别主要是替换的内容不同,replaceAll是替换所有匹配的字符,而replaceFirst()仅替换第一次出现的字符

    用法例子

    replaceAll()替换符合正则的所有文字 //文字替换(全部) Pattern pattern = Pattern.compile("正则表达式"); Matcher matcher = pattern.matcher("正则表达式 Hello World,正则表达式 Hello World"); //替换第一个符合正则的数据 System.out.println(matcher.replaceAll("Java")); replaceFirst()替换第一个匹配的正则的数据 //文字替换(首次出现字符) Pattern pattern = Pattern.compile("正则表达式"); Matcher matcher = pattern.matcher("正则表达式 Hello World,正则表达式 Hello World"); //替换第一个符合正则的数据 System.out.println(matcher.replaceFirst("Java")); replaceAll()替换所有html标签 //去除html标记 Pattern pattern = Pattern.compile("<.+?>", Pattern.DOTALL); Matcher matcher = pattern.matcher("<a href=\"index.html\">主页</a>"); String string = matcher.replaceAll(""); System.out.println(string); replaceAll() 替换指定文字 //替换指定{}中文字 String str = "Java目前的发展史是由{0}年-{1}年"; String[][] object = { new String[] { "\\{0\\}", "1995" }, new String[] { "\\{1\\}", "2007" } }; System.out.println(replace(str, object)); public static String replace(final String sourceString, Object[] object) { String temp = sourceString; for (int i = 0; i < object.length; i++) { String[] result = (String[]) object[i]; Pattern pattern = Pattern.compile(result[0]); Matcher matcher = pattern.matcher(temp); temp = matcher.replaceAll(result[1]); } return temp; } replace()替换字符串 System.out.println("abac".replace("a", "\a")); //\ab\ac

    String 对“+” 的重载

    String s = “a” + “b” ,编译器会进行常量折叠(因为两个都是编译期常量,编译器可知),即变成 String s = “ab”;对于能够进行优化的(String s = “a” + 变量等)用StringBuilder 的append()方法替代,最后调用toString()方法(底层就是一个new String())

    字符串拼接的几种方式和区别

    字符串,是Java中最常用的一个数据类型了。

    本文,也是对于Java中字符串相关知识的一个补充,主要来介绍一下字符串拼接相关的知识。本文基于jdk1.8.0_181。

    字符串拼接

    字符串拼接是我们在Java代码中比较经常要做的事情,就是把多个字符串拼接到一起。

    我们都知道,String是Java中一个不可变的类,所以他一旦被实例化就无法被修改。

    不可变类的实例一旦创建,其成员变量的值就不能被修改。这样设计有很多好处,比如可以缓存hashcode、使用更加便利以及更加安全等。

    但是,既然字符串是不可变的,那么字符串拼接又是怎么回事呢?

    字符串不变性与字符串拼接

    其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码:

    String s = "abcd"; s = s.concat("ef");

    其实最后我们得到的s已经是一个新的字符串了。如下图 s中保存的是一个重新创建出来的String对象的引用。

    那么,在Java中,到底如何进行字符串拼接呢?字符串拼接有很多种方式,这里简单介绍几种比较常用的。

    使用 +拼接字符串

    在Java中,拼接字符串最简单的方式就是直接使用符号+来拼接。如:

    String wechat = "Hollis"; String introduce = "每日更新Java相关技术文章"; String hollis = wechat + "," + introduce;

    这里要特别说明一点,有人把Java中使用+拼接字符串的功能理解为运算符重载。其实并不是,Java 是不支持运算符重载的。这其实是Java提供的一个语法糖。后面再详细介绍。

    运算符重载:在计算机程序设计中,运算符重载(英语:operator overloading)是多态的一种。运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。 语法糖:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

    concat 除了使用+拼接字符串之外,还可以使用String类中的方法concat方法来拼接字符串。如:

    String wechat = "Hollis"; String introduce = "每日更新Java相关技术文章"; String hollis = wechat.concat(",").concat(introduce);

    StringBuffer

    关于字符串,Java除了定义一个可以用来定义字符串常量的String类以外,还提供了用来定义字符串变量的StringBuffer类,它的对象是可以扩充和修改的。

    使用StringBuffer可以方便的对字符串进行拼接。如:

    StringBuffer wechat = new StringBuffer("Hollis"); String introduce = "每日更新Java相关技术文章"; StringBuffer hollis = wechat.append(",").append(introduce);

    StringBuilder 除了StringBuffer以外,还有一个类StringBuilder也可以使用,其用法和StringBuffer类似。如:

    StringBuilder wechat = new StringBuilder("Hollis"); String introduce = "每日更新Java相关技术文章"; StringBuilder hollis = wechat.append(",").append(introduce);

    StringUtils.join 除了JDK中内置的字符串拼接方法,还可以使用一些开源类库中提供的字符串拼接方法名,如apache.commons中提供的StringUtils类,其中的join方法可以拼接字符串。

    String wechat = "Hollis"; String introduce = "每日更新Java相关技术文章"; System.out.println(StringUtils.join(wechat, ",", introduce));

    这里简单说一下,StringUtils中提供的join方法,最主要的功能是:将数组或集合以某拼接符拼接到一起形成新的字符串,如:

    String []list ={"Hollis","每日更新Java相关技术文章"}; String result= StringUtils.join(list,","); System.out.println(result); //结果:Hollis,每日更新Java相关技术文章

    并且,Java8中的String类中也提供了一个静态的join方法,用法和StringUtils.join类似。

    以上就是比较常用的五种在Java种拼接字符串的方式,那么到底哪种更好用呢?为什么阿里巴巴Java开发手册中不建议在循环体中使用+进行字符串拼接呢?


    使用+拼接字符串的实现原理

    前面提到过,使用+拼接字符串,其实只是Java提供的一个语法糖, 那么,我们就来解一解这个语法糖,看看他的内部原理到底是如何实现的。

    还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。

    String wechat = "Hollis"; String introduce = "每日更新Java相关技术文章"; String hollis = wechat + "," + introduce;

    反编译后的内容如下,反编译工具为jad。

    String wechat = "Hollis"; String introduce = "\u6BCF\u65E5\u66F4\u65B0Java\u76F8\u5173\u6280\u672F\u6587\u7AE0";//每日更新Java相关技术文章 String hollis = (new StringBuilder()).append(wechat).append(",").append(introduce).toString();

    通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。

    那么也就是说,Java中的+对字符串的拼接,其实现原理是使用StringBuilder.append。


    concat 是如何实现的

    我们再来看一下concat方法的源代码,看一下这个方法又是如何实现的。

    public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); }

    这段代码首先创建了一个字符数组,长度是已有字符串和待拼字符串的长度之和。再把两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的String对象并返回。

    通过源码我们也可以看到,经过concat方法,其实是new了一个新的String,这也就呼应到前面我们说的字符串的不变性问题上了。


    StringBuffer 和 StringBuilder

    接下来我们看看StringBuffer和StringBuilder的实现原理。

    和String类类似,StringBuilder类也封装了一个字符数组,定义如下:

    char[] value;

    与 String 不同的是,它并不是final的,所以它是可以修改的。另外,与String 不同,字符数组不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:

    int count;

    其 append源码如下:

    public StringBuilder append(String str) { super.append(str); return this; }

    该类继承了AbstractStringBuilder类,看下其append方法:

    public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; }

    append会直接拷贝字符到内部字符数组中,如果字符数组长度不够,会进行扩展。

    StringBuffer 和 StringBuilder 类似,最大的区别就是StringBuffer是线程安全的,看一下StringBuffer的append方法。

    public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }

    该方法使用synchronized进行声明,说明是一个线程安全的方法。而StringBuilder则不是线程安全的。


    StringUtils.join 是如何实现的

    通过查看StringUtils.join的源代码,我们可以发现,其实他也是通过StringBuilder来实现的。

    public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) { if (array == null) { return null; } if (separator == null) { separator = EMPTY; } // endIndex - startIndex > 0: Len = NofStrings *(len(firstString) + len(separator)) // (Assuming that all Strings are roughly equally long) final int noOfItems = endIndex - startIndex; if (noOfItems <= 0) { return EMPTY; } final StringBuilder buf = new StringBuilder(noOfItems * 16); for (int i = startIndex; i < endIndex; i++) { if (i > startIndex) { buf.append(separator); } if (array[i] != null) { buf.append(array[i]); } } return buf.toString(); }

    效率比较

    既然有这么多种字符串拼接的方法,那么到底哪一种效率最高呢?我们来简单对比一下。

    long t1 = System.currentTimeMillis(); //这里是初始字符串定义 for (int i = 0; i < 50000; i++) { //这里是字符串拼接代码 } long t2 = System.currentTimeMillis(); System.out.println("cost:" + (t2 - t1));

    我们使用形如以上形式的代码,分别测试下五种字符串拼接代码的运行时间。得到结果如下:

    + cost:5119 StringBuilder cost:3 StringBuffer cost:4 concat cost:3623 StringUtils.join cost:25726

    从结果可以看出,用时从短到长的对比是: StringBuilder < StringBuffer < concat < + < StringUtils.join

    StringBuffer 在 StringBuilder 的基础上,做了同步处理,所以在耗时上会相对多一些。

    StringUtils.join也是使用了StringBuilder,并且其中还是有很多其他操作,所以耗时较长,这个也容易理解。其实StringUtils.join更擅长处理字符串数组或者列表的拼接。

    那么问题来了,前面我们分析过,其实使用+拼接字符串的实现原理也是使用的StringBuilder,那为什么结果相差这么多,高达1000多倍呢?

    我们再把以下代码反编译下:

    long t1 = System.currentTimeMillis(); String str = "hollis"; for (int i = 0; i < 50000; i++) { String s = String.valueOf(i); str += s; } long t2 = System.currentTimeMillis(); System.out.println("+ cost:" + (t2 - t1));

    反编译后代码如下:

    long t1 = System.currentTimeMillis(); String str = "hollis"; for(int i = 0; i < 50000; i++) { String s = String.valueOf(i); str = (new StringBuilder()).append(str).append(s).toString(); } long t2 = System.currentTimeMillis(); System.out.println((new StringBuilder()).append("+ cost:").append(t2 - t1).toString());

    我们可以看到,反编译之后的代码,在for循环中,每次都是new了一个StringBuilder,然后再把String转成StringBuilder,再进行append

    而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。

    所以,阿里巴巴Java开发手册建议:循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。而不要使用+。

    总结

    本文介绍了什么是字符串拼接,虽然字符串是不可变的,但是还是可以通过新建字符串的方式来进行字符串的拼接。

    常用的字符串拼接方式有五种,分别是使用+、使用concat、使用StringBuilder、使用StringBuffer以及使用StringUtils.join。

    由于字符串拼接过程中会创建新的对象,所以如果要在一个循环体中进行字符串拼接,就要考虑内存问题和效率问题。

    因此,经过对比,我们发现,直接使用StringBuilder的方式是效率最高的。因为StringBuilder天生就是设计来定义可变字符串和字符串的变化操作的。

    但是,还要强调的是:

    如果不是在循环体中进行字符串拼接的话,直接使用+就好了。如果在并发场景中进行字符串拼接的话,要使用StringBuffer来代替StringBuilder。

    String.valueOf 和 Integer.toString 的区别

    我们有三种方式将一个int类型的变量变成呢过String类型,那么他们有什么区别?

    1.int i = 5; 2.String i1 = "" + i; 3.String i2 = String.valueOf(i); 4.String i3 = Integer.toString(i);

    第三行和第四行没有任何区别,因为String.valueOf(i)也是调用Integer.toString(i)来实现的。

    第二行代码其实是String i1 = (new StringBuilder()).append(i).toString();,首先创建一个StringBuilder对象,然后再调用append方法,再调用toString方法。


    switch 对 String 的支持

    Java 7中,switch的参数可以是String类型了,这样对我们来说是一个很方便的改进。到目前为止switch支持这样几种数据类型:byte、short、int、char、String 。但是,作为一个程序员我们不仅要知道他有多么好用,还要知道它是如何实现的,switch对整型的支持是怎么实现的呢?对字符型是怎么实现的呢?String类型呢?有一点Java开发经验的人这个时候都会猜测switch对String的支持是使用equals()方法和hashcode()方法。那么到底是不是这两个方法呢?接下来我们就看一下,switch到底是如何实现的。

    一、switch 对整型支持的实现

    下面是一段很简单的Java代码,定义一个int型变量a,然后使用switch语句进行判断。执行这段代码输出内容为5,那么我们将下面这段代码反编译,看看他到底是怎么实现的。

    public class switchDemoInt { public static void main(String[] args) { int a = 5; switch (a) { case 1: System.out.println(1); break; case 5: System.out.println(5); break; default: break; } } } //output 5

    反编译后的代码如下:

    **public class switchDemoInt { public switchDemoInt() { } public static void main(String args[]) { int a = 5; switch(a) { case 1: // '\001' System.out.println(1); break; case 5: // '\005' System.out.println(5); break; } } }**

    我们发现,反编译后的代码和之前的代码比较除了多了两行注释以外没有任何区别,那么我们就知道,switch对int的判断是直接比较整数的值。


    二、switch 对字符型支持的实现

    直接上代码

    public class switchDemoInt { public static void main(String[] args) { char a = 'b'; switch (a) { case 'a': System.out.println('a'); break; case 'b': System.out.println('b'); break; default: break; } } }

    编译后的代码如下:

    public class switchDemoChar { public switchDemoChar() { } public static void main(String args[]) { char a = 'b'; switch(a) { case 97: // 'a' System.out.println('a'); break; case 98: // 'b' System.out.println('b'); break; } } }

    通过以上的代码作比较我们发现:对char类型进行比较的时候,实际上比较的是ascii码,编译器会把char型变量转换成对应的int型变量


    三、switch 对字符串支持的实现

    还是先上代码:

    public class switchDemoString { public static void main(String[] args) { String str = "world"; switch (str) { case "hello": System.out.println("hello"); break; case "world": System.out.println("world"); break; default: break; } } }

    对代码进行反编译:

    public class switchDemoString { public switchDemoString() { } public static void main(String args[]) { String str = "world"; String s; switch((s = str).hashCode()) { default: break; case 99162322: if(s.equals("hello")) System.out.println("hello"); break; case 113318802: if(s.equals("world")) System.out.println("world"); break; } } }

    看到这个代码,你知道原来字符串的switch是通过equals和hashCode()方法来实现的。记住,switch中只能使用整型,比如byte、short、char(ASCII码是整型)以及int。还好hashCode()返回的是int,而不是long。通过这个很容易记住hashCode返回的是int这个事实。仔细看下可以发现,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希值可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。因为Java编译器只增加了一个equals方法,如果你比较的是字符串字面量的话会非常快,比如”abc” ==”abc”。如果你把hashCode()方法的调用也考虑进来了,那么还会再多一次的调用开销,因为字符串一旦创建了,它就会把哈希值缓存起来。因此如果这个switch语句是用在一个循环里的,比如逐项处理某个值,或者游戏引擎循环地渲染屏幕,这里hashCode()方法的调用开销其实不会很大。

    好,以上就是关于switch对整型、字符型、和字符串型的支持的实现方式,总结一下我们可以发现,其实switch只支持一种数据类型,那就是整型,其他数据类型都是转换成整型之后在使用switch的。

    Processed: 0.011, SQL: 9