将字符串类型的数据反转输出

本文探讨了Java中字符串反转的四种方法,包括利用字符数组、StringBuilder和StringBuilder的reverse()方法。同时,讲解了String、StringBuffer和StringBuilder的区别,重点在于它们的可变性和线程安全性。还涉及到了字符串常量池、intern方法的工作原理以及如何优化字符串连接操作,以提高性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这是一个老面试题了,可能因为有段时间没写代码的原因吧,竟然被问懵逼了,真是可耻。

String类型数据是java中最长使用的数据之一,区别去其他基础类型的数据(byte、short、int、long、float、double、char、boolean),String类型的数据是引用类型的。

String相关类继承关系:

言归正传:

public class FlashBackTest {

    public static void main(String[] args) {
        String val = method1("123456789");
        System.out.println("方法一:"+val);
        String val2 = method2(val);
        System.out.println("方法二:"+val2);
        String val3 = method3(val2);
        System.out.println("方法三:"+val3);
        String val4 = method4(val3);
        System.out.println("方法四:"+val4);
    }
    //方法一
    public static String method1(String str){
        char [] strArr = str.toCharArray();
        for(int i=0,j=strArr.length-1;i<j;i++,j--){
            char tmp = strArr[i];
            strArr[i] = strArr[j];
            strArr[j] = tmp;
        }
        return new String(strArr);
    }

    //方法二
    public static String method2(String str){
        StringBuilder sbr = new StringBuilder(str);
        StringBuilder val = sbr.reverse();
        return val.toString();
    }
    //方法三
    public static String method3(String str){
        String num = "";
        for(int i = str.length()-1;i>=0;i--){
            num = num + str.charAt(i);
        }
        return num;
    }
    //方法四
    public static String method4(String str){
        StringBuilder stringBuilder = new StringBuilder();
        for(int i = str.length()-1;i>=0;i--){
            stringBuilder.append(str.charAt(i));
        }
        return stringBuilder.toString();
    }

}

方法一:

    这种方法类似于二分查找算法,找到字符串的中间值,两两互换;

方法二:

    这种方法借助于StringBuilder类提供的reverse()方法进行直接输出反转后的值,StringBuilder同样也可以得到相应的效果,但是reverse方法并不是由StringBuilder和StringBuilder提供的,而是由他们的父类AbstractStringBuilder的抽象类来具体的实现的;

/**
 * Causes this character sequence to be replaced by the reverse of
 * the sequence. If there are any surrogate pairs included in the
 * sequence, these are treated as single characters for the
 * reverse operation. Thus, the order of the high-low surrogates
 * is never reversed.
 *
 * Let <i>n</i> be the character length of this character sequence
 * (not the length in {@code char} values) just prior to
 * execution of the {@code reverse} method. Then the
 * character at index <i>k</i> in the new character sequence is
 * equal to the character at index <i>n-k-1</i> in the old
 * character sequence.
 *
 * <p>Note that the reverse operation may result in producing
 * surrogate pairs that were unpaired low-surrogates and
 * high-surrogates before the operation. For example, reversing
 * "\u005CuDC00\u005CuD800" produces "\u005CuD800\u005CuDC00" which is
 * a valid surrogate pair.
 *
 * @return  a reference to this object.
 */
public AbstractStringBuilder reverse() {
    boolean hasSurrogates = false;
    int n = count - 1;
    for (int j = (n-1) >> 1; j >= 0; j--) {
        int k = n - j;
        char cj = value[j];
        char ck = value[k];
        value[j] = ck;
        value[k] = cj;
        if (Character.isSurrogate(cj) ||
            Character.isSurrogate(ck)) {
            hasSurrogates = true;
        }
    }
    if (hasSurrogates) {
        reverseAllValidSurrogatePairs();
    }
    return this;
}

方法三:

    这种方法通过倒叙查询,累加起来查询就可以得到最后的值;

方法四:

    这种方法和方法三原理一样只是借用stringBuilder方法的append方法,同方法二一样,append方法也不是stringBuilder和StringBuilder实现的,也是由他们的父类(AbstractStringBuilder)实现的;

/**
 * Appends the specified string to this character sequence.
 * <p>
 * The characters of the {@code String} argument are appended, in
 * order, increasing the length of this sequence by the length of the
 * argument. If {@code str} is {@code null}, then the four
 * characters {@code "null"} are appended.
 * <p>
 * Let <i>n</i> be the length of this character sequence just prior to
 * execution of the {@code append} method. Then the character at
 * index <i>k</i> in the new character sequence is equal to the character
 * at index <i>k</i> in the old character sequence, if <i>k</i> is less
 * than <i>n</i>; otherwise, it is equal to the character at index
 * <i>k-n</i> in the argument {@code str}.
 *
 * @param   str   a string.
 * @return  a reference to this object.
 */
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;
}

    我们再深入的了解一下String的底层源码,通过查看源码我们可以知道,不管是String、StringBuffer或StringBuilder都是通过char数组来实现的,我们复制给他们一段字符串,但是实际上确实以char数组进行存储的。

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、StringBuffer和StringBuilder三者的区别:

    String是不可变字符串,由上面的成员变量,我们可以发现string的成员变量(private final char value[];)是由final修饰,每次修改String字符串的值都是在new一个新的String对象;

    StringBuffer是可变字符串,声明一个StringBuffer的对象,底层默认大小为16的插入数组,StringBuffer相对于StringBuilder是线程安全的,因为StringBuffer提供的方法都是有synchronized修饰的同步方法;

/**
 * @since   JDK1.0.2
 */
@Override
public synchronized StringBuffer reverse() {
    toStringCache = null;
    super.reverse();
    return this;
}

   StringBuilder同样也是可变字符串,但是相对于StringBuffer它是线程不安全的,但是对应的StringBuilder的执行效率更高,所以在开发中这个应用更广泛。

@Override
public StringBuilder reverse() {
    super.reverse();
    return this;
}

String连接符(“+”)的实现原理:

String的连接符是由StringBuilder类的append方法实现的,我们可以通过将编译好的.class文件进行反编译验证;

源代码:

public class DecompileTest {
    public static void main(String[] args) {
        int i = 10;
        String s = "abc";
        System.out.println(s + i);
    }
}

反编译后的代码:

public class DecompileTest
{

    public DecompileTest()
    {
    }

    public static void main(String args[])
    {
        int i = 10;
        String s = "abc";
        System.out.println((new StringBuilder()).append(s).append(i).toString());
    }
}

由反编译代码,我们可以发现上面Java中在通过使用“+”对字符串类型进行连接操作的时候,会创建一个StringBuilder对象,通关StringBuilder类提供的append的方法进行拼接,再通过toString方法返回拼接好的字符串。我们通过查看源码可知,append方法的各种重载方法的作用实际上是与String.valueOf(i)的效果是一样的;

/**
 * Appends the string representation of the {@code int}
 * argument to this sequence.
 * <p>
 * The overall effect is exactly as if the argument were converted
 * to a string by the method {@link String#valueOf(int)},
 * and the characters of that string were then
 * {@link #append(String) appended} to this character sequence.
 *
 * @param   i   an {@code int}.
 * @return  a reference to this object.
 */
public AbstractStringBuilder append(int i) {
    if (i == Integer.MIN_VALUE) {
        append("-2147483648");
        return this;
    }
    int appendedLength = (i < 0) ? Integer.stringSize(-i) + 1
                                 : Integer.stringSize(i);
    int spaceNeeded = count + appendedLength;
    ensureCapacityInternal(spaceNeeded);
    Integer.getChars(i, spaceNeeded, value);
    count = spaceNeeded;
    return this;
}

所以,我们可以认为以下是等价的:

  //以下两者是等价的 s = i + ""; 

  s = String.valueOf(i);

  //以下两者也是等价的

  s = "abc" + i;

  s = new StringBuilder("abc").append(i).toString();

由于,在使用“+”对字符串进行字符串拼接的时候,java会隐式的创建StringBuilder对象,这种方式一般不会照成效率问题,但是如果进行大量的循环拼接字符串的时候就需要小心了。

源代码:

public class ForStringTest {

    public static void main(String[] args) {
        String val = "def";
        for(int i=0; i<100;i++){
            val += i;
        }
    }

}

反编译后:

public class ForStringTest
{

    public ForStringTest()
    {
    }

    public static void main(String args[])
    {
        String val = "def";
        for(int i = 0; i < 100; i++)
            val = (new StringBuilder()).append(val).append(i).toString();

    }
}
我们可以从上面的实例看到,在进行循环拼接字符串的时候,会创建大量StringBuilder对象,如果循环次数很大,会加大系统负荷,肯定也会造成效率的损失,这样的情况下建议,在循环体外创建StringBuilder对象,循环体内通过append方法对字符串进行拼接。

当“+”两端的值都在编译期都是可以确定的字符串常量时,编译器会对其进行相应的优化,直接将这两个字符串常量拼接好:

源代码:

System.out.println("Reason"+"Conservative");

反编译:

System.out.println("ReasonConservative");

另外,在以final修饰的值,在编译期可以确定,会被解释为常量;

String v0 = "abc";
final String v1 = "ab";//final修饰的值,可以在编译期确定,会被解释成常量;
String v2 = v1 + "c";//因为编译期可以确定,java会直接将这两个值拼接起来,而v2存的值则是“abc”在常量池的引用。
System.out.println(v0 == v2);//result = true

下面的例子,v1是由getString方法提供的,虽然v1是由final修饰的,但是却只能在运行期确定,所以他们指向的对象不一样,结果返回false;

public static void main(String[] args) {
    String v0 = "abc";
    //final String v1 = "ab";
    final String v1 = getString();
    String v2 = v1 + "c";
    System.out.println(v0 == v2);//result=false
}

public static String getString(){
    return "ab";
}

字符串常量池:

    字符串类型的数据作为引用对象的一种,字符串的分配和其他对象的分配一样,是需要消耗高昂的时间和空间的,而且字符串的使用非常广泛,jvm为了提高性能和减少内存的开销,针对字符串实例化进行了一些优化,通过使用字符串常量池,在创建相同字符串的时候的系统性能消耗。每当创建字符串的时候,jvm会首先检查字符串常量池,如果所要创建的字符串应景存在常量此种,那就直接返回该字符串在常量池中的实例引用,如果字符串不存在常量池中,就会实例化该字符串并将其放到常量池中。

    全局字符串常量池的内容是在类加载时完成的,准备阶段之后,在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到String Pool(string pool也有叫做string literal pool)中,String Pool中存的是引用值,而不是具体的实例对象,具体的实例对象是在堆中开辟出来的内存空间中存放的。在HotSpot VM中,string pool功能是通过StringTable类来实现的,它是一个Hash表,默认大小长度为“1009”;这个String Table在每个HotSpotVM的实例中只有一份,被所有的类共享。字符串常量的引用被放在这个StringTable上,也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了“驻流字符串”的身份。在jdk6以及之前,String Pool放的都是字符串常量;在jdk7之后,存放的是队内的字符串对象的引用。

    在JDK6及之前的版本,字符串长两次是放在PermGen区(方法区)中的,StringTable的长度固定为:1009;在jdk7中字符串长两次被一刀了堆中,StringTable的长度不是固定的,可以通过**_XX:StringTableSize=11111**参数指定。常量池移动到堆上的原因可能是由于方法区的内存空间太小且不太方便,而对的内存空间比较大切扩展方便。

intern方法:

我们平时通过“常量值”声明的String对象会存储在字符串常量池中,至于不是通过“常量值”声明的String对象,可以通过String提供intern方法。通过intern方法在创建字符串的时候,会先从常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将新创建的字符串放入到常量池中,再返回。

/**
 * Returns a canonical representation for the string object.
 * <p>
 * A pool of strings, initially empty, is maintained privately by the
 * class {@code String}.
 * <p>
 * When the intern method is invoked, if the pool already contains a
 * string equal to this {@code String} object as determined by
 * the {@link #equals(Object)} method, then the string from the pool is
 * returned. Otherwise, this {@code String} object is added to the
 * pool and a reference to this {@code String} object is returned.
 * <p>
 * It follows that for any two strings {@code s} and {@code t},
 * {@code s.intern() == t.intern()} is {@code true}
 * if and only if {@code s.equals(t)} is {@code true}.
 * <p>
 * All literal strings and string-valued constant expressions are
 * interned. String literals are defined in section 3.10.5 of the
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * @return  a string that has the same contents as this string, but is
 *          guaranteed to be from a pool of unique strings.
 */
public native String intern();

使用intern方法在创建字符串对象的时候,会增加内存的负担(通过intern方法创建实例会有一步查找常量池和新字符串实例需要新增到常量池的工作),相应的会减轻存储的负担(常量池中的内容是共用的),以及相应的jvm回收的时间(新生成的字符串在没用的时候也需要垃圾回收);

补充:

以下两个注解都是将超链接的“地址”当做其文本,这两个注解都是用在备注中,作用是一样的,但是用法有些差异,@see:必须顶头写,而@link:任意位置写。

@see:

语法格式: @see 全路径包名#方法名(参数类型列表)

@link:

语法格式:{@link 全路径包名#方法名(参数类型列表)}

.class文件反编译成.java文件:

  jad -sjava DecompileTest.class

jad命令的参数:

       -o:覆盖旧文件,而且不用提示确认。
       -r:重新加载生成包结构。
       -s (java):定义输出文件的扩展名。jad为默认扩展名,我们反编译后当然是要.java源文件了。
       -d:输出文件的目录。src表示反编译后的所有文件都放在src目录下。

反编译工具下载地址:

  http://varaneckas.com/jad/ 

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值